/* * Copyright 2014 Red Hat, Inc. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * and Apache License v2.0 which accompanies this distribution. * * The Eclipse Public License is available at * http://www.eclipse.org/legal/epl-v10.html * * The Apache License v2.0 is available at * http://www.opensource.org/licenses/apache2.0.php * * You may elect to redistribute this code under either of these licenses. */ package io.vertx.test.fakedns; import org.apache.directory.server.dns.DnsServer; import org.apache.directory.server.dns.io.encoder.DnsMessageEncoder; import org.apache.directory.server.dns.io.encoder.ResourceRecordEncoder; import org.apache.directory.server.dns.messages.DnsMessage; import org.apache.directory.server.dns.messages.QuestionRecord; import org.apache.directory.server.dns.messages.RecordClass; import org.apache.directory.server.dns.messages.RecordType; import org.apache.directory.server.dns.messages.ResourceRecord; import org.apache.directory.server.dns.messages.ResourceRecordModifier; import org.apache.directory.server.dns.protocol.DnsProtocolHandler; import org.apache.directory.server.dns.protocol.DnsUdpDecoder; import org.apache.directory.server.dns.protocol.DnsUdpEncoder; import org.apache.directory.server.dns.store.DnsAttribute; import org.apache.directory.server.dns.store.RecordStore; import org.apache.directory.server.protocol.shared.transport.UdpTransport; import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolCodecFactory; import org.apache.mina.filter.codec.ProtocolCodecFilter; import org.apache.mina.filter.codec.ProtocolDecoder; import org.apache.mina.filter.codec.ProtocolEncoder; import org.apache.mina.filter.codec.ProtocolEncoderOutput; import org.apache.mina.transport.socket.DatagramAcceptor; import org.apache.mina.transport.socket.DatagramSessionConfig; import java.io.IOException; import java.net.InetSocketAddress; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * @author <a href="mailto:nmaurer@redhat.com">Norman Maurer</a> */ public final class FakeDNSServer extends DnsServer { public static RecordStore A_store(Map<String, String> entries) { return questionRecord -> entries.entrySet().stream().map(entry -> { ResourceRecordModifier rm = new ResourceRecordModifier(); rm.setDnsClass(RecordClass.IN); rm.setDnsName(entry.getKey()); rm.setDnsTtl(100); rm.setDnsType(RecordType.A); rm.put(DnsAttribute.IP_ADDRESS, entry.getValue()); return rm.getEntry(); }).collect(Collectors.toSet()); } public static final int PORT = 53530; private int port = PORT; private final RecordStore store; private DatagramAcceptor acceptor; public FakeDNSServer(RecordStore store) { this.store = store; } public InetSocketAddress localAddress() { return (InetSocketAddress) getTransports()[0].getAcceptor().getLocalAddress(); } public FakeDNSServer port(int p) { port = p; return this; } public static FakeDNSServer testResolveA(final String ipAddress) { return testResolveA(Collections.singletonMap("dns.vertx.io", ipAddress)); } public static FakeDNSServer testResolveA(Map<String, String> entries) { return new FakeDNSServer(A_store(entries)); } public static FakeDNSServer testResolveAAAA(final String ipAddress) { return new FakeDNSServer(new RecordStore() { @Override public Set<ResourceRecord> getRecords(QuestionRecord questionRecord) throws org.apache.directory.server.dns.DnsException { Set<ResourceRecord> set = new HashSet<>(); ResourceRecordModifier rm = new ResourceRecordModifier(); rm.setDnsClass(RecordClass.IN); rm.setDnsName("dns.vertx.io"); rm.setDnsTtl(100); rm.setDnsType(RecordType.AAAA); rm.put(DnsAttribute.IP_ADDRESS, ipAddress); set.add(rm.getEntry()); return set; } }); } public static FakeDNSServer testResolveMX(final int prio, final String mxRecord) { return new FakeDNSServer(new RecordStore() { @Override public Set<ResourceRecord> getRecords(QuestionRecord questionRecord) throws org.apache.directory.server.dns.DnsException { Set<ResourceRecord> set = new HashSet<>(); ResourceRecordModifier rm = new ResourceRecordModifier(); rm.setDnsClass(RecordClass.IN); rm.setDnsName("dns.vertx.io"); rm.setDnsTtl(100); rm.setDnsType(RecordType.MX); rm.put(DnsAttribute.MX_PREFERENCE, String.valueOf(prio)); rm.put(DnsAttribute.DOMAIN_NAME, mxRecord); set.add(rm.getEntry()); return set; } }); } public static FakeDNSServer testResolveTXT(final String txt) { return new FakeDNSServer(new RecordStore() { @Override public Set<ResourceRecord> getRecords(QuestionRecord questionRecord) throws org.apache.directory.server.dns.DnsException { Set<ResourceRecord> set = new HashSet<>(); ResourceRecordModifier rm = new ResourceRecordModifier(); rm.setDnsClass(RecordClass.IN); rm.setDnsName("dns.vertx.io"); rm.setDnsTtl(100); rm.setDnsType(RecordType.TXT); rm.put(DnsAttribute.CHARACTER_STRING, txt); set.add(rm.getEntry()); return set; } }); } public static FakeDNSServer testResolveNS(final String ns) { return new FakeDNSServer(new RecordStore() { @Override public Set<ResourceRecord> getRecords(QuestionRecord questionRecord) throws org.apache.directory.server.dns.DnsException { Set<ResourceRecord> set = new HashSet<>(); ResourceRecordModifier rm = new ResourceRecordModifier(); rm.setDnsClass(RecordClass.IN); rm.setDnsName("dns.vertx.io"); rm.setDnsTtl(100); rm.setDnsType(RecordType.NS); rm.put(DnsAttribute.DOMAIN_NAME, ns); set.add(rm.getEntry()); return set; } }); } public static FakeDNSServer testResolveCNAME(final String cname) { return new FakeDNSServer(new RecordStore() { @Override public Set<ResourceRecord> getRecords(QuestionRecord questionRecord) throws org.apache.directory.server.dns.DnsException { Set<ResourceRecord> set = new HashSet<>(); ResourceRecordModifier rm = new ResourceRecordModifier(); rm.setDnsClass(RecordClass.IN); rm.setDnsName("dns.vertx.io"); rm.setDnsTtl(100); rm.setDnsType(RecordType.CNAME); rm.put(DnsAttribute.DOMAIN_NAME, cname); set.add(rm.getEntry()); return set; } }); } public static FakeDNSServer testResolvePTR(final String ptr) { return new FakeDNSServer(new RecordStore() { @Override public Set<ResourceRecord> getRecords(QuestionRecord questionRecord) throws org.apache.directory.server.dns.DnsException { Set<ResourceRecord> set = new HashSet<>(); ResourceRecordModifier rm = new ResourceRecordModifier(); rm.setDnsClass(RecordClass.IN); rm.setDnsName("dns.vertx.io"); rm.setDnsTtl(100); rm.setDnsType(RecordType.PTR); rm.put(DnsAttribute.DOMAIN_NAME, ptr); set.add(rm.getEntry()); return set; } }); } public static FakeDNSServer testResolveSRV(final int priority, final int weight, final int port, final String target) { return new FakeDNSServer(new RecordStore() { @Override public Set<ResourceRecord> getRecords(QuestionRecord questionRecord) throws org.apache.directory.server.dns.DnsException { Set<ResourceRecord> set = new HashSet<>(); ResourceRecordModifier rm = new ResourceRecordModifier(); rm.setDnsClass(RecordClass.IN); rm.setDnsName("dns.vertx.io"); rm.setDnsTtl(100); rm.setDnsType(RecordType.SRV); rm.put(DnsAttribute.SERVICE_PRIORITY, String.valueOf(priority)); rm.put(DnsAttribute.SERVICE_WEIGHT, String.valueOf(weight)); rm.put(DnsAttribute.SERVICE_PORT, String.valueOf(port)); rm.put(DnsAttribute.DOMAIN_NAME, target); set.add(rm.getEntry()); return set; } }); } public static FakeDNSServer testLookup4(final String ip) { return new FakeDNSServer(new RecordStore() { @Override public Set<ResourceRecord> getRecords(QuestionRecord questionRecord) throws org.apache.directory.server.dns.DnsException { Set<ResourceRecord> set = new HashSet<>(); ResourceRecordModifier rm = new ResourceRecordModifier(); rm.setDnsClass(RecordClass.IN); rm.setDnsName("dns.vertx.io"); rm.setDnsTtl(100); rm.setDnsType(RecordType.A); rm.put(DnsAttribute.IP_ADDRESS, ip); set.add(rm.getEntry()); return set; } }); } public static FakeDNSServer testLookup6() { return new FakeDNSServer(new RecordStore() { @Override public Set<ResourceRecord> getRecords(QuestionRecord questionRecord) throws org.apache.directory.server.dns.DnsException { Set<ResourceRecord> set = new HashSet<>(); ResourceRecordModifier rm = new ResourceRecordModifier(); rm.setDnsClass(RecordClass.IN); rm.setDnsName("dns.vertx.io"); rm.setDnsTtl(100); rm.setDnsType(RecordType.AAAA); rm.put(DnsAttribute.IP_ADDRESS, "::1"); set.add(rm.getEntry()); return set; } }); } public static FakeDNSServer testLookup(final String ip) { return testLookup4(ip); } public static FakeDNSServer testLookupNonExisting() { return new FakeDNSServer(new RecordStore() { @Override public Set<ResourceRecord> getRecords(QuestionRecord questionRecord) throws org.apache.directory.server.dns.DnsException { return null; } }); } public static FakeDNSServer testReverseLookup(final String ptr) { return new FakeDNSServer(new RecordStore() { @Override public Set<ResourceRecord> getRecords(QuestionRecord questionRecord) throws org.apache.directory.server.dns.DnsException { Set<ResourceRecord> set = new HashSet<>(); ResourceRecordModifier rm = new ResourceRecordModifier(); rm.setDnsClass(RecordClass.IN); rm.setDnsName("dns.vertx.io"); rm.setDnsTtl(100); rm.setDnsType(RecordType.PTR); rm.put(DnsAttribute.DOMAIN_NAME, ptr); set.add(rm.getEntry()); return set; } }); } public static FakeDNSServer testResolveASameServer(final String ipAddress) { return new FakeDNSServer(A_store(Collections.singletonMap("vertx.io", ipAddress))); } public static FakeDNSServer testLookup4CNAME(final String cname, final String ip) { return new FakeDNSServer(new RecordStore() { @Override public Set<ResourceRecord> getRecords(QuestionRecord questionRecord) throws org.apache.directory.server.dns.DnsException { // use LinkedHashSet since the order of the result records has to be preserved to make sure the unit test fails Set<ResourceRecord> set = new LinkedHashSet<>(); ResourceRecordModifier rm = new ResourceRecordModifier(); rm.setDnsClass(RecordClass.IN); rm.setDnsName("vertx.io"); rm.setDnsTtl(100); rm.setDnsType(RecordType.CNAME); rm.put(DnsAttribute.DOMAIN_NAME, cname); set.add(rm.getEntry()); ResourceRecordModifier rm2 = new ResourceRecordModifier(); rm2.setDnsClass(RecordClass.IN); rm2.setDnsName(cname); rm2.setDnsTtl(100); rm2.setDnsType(RecordType.A); rm2.put(DnsAttribute.IP_ADDRESS, ip); set.add(rm2.getEntry()); return set; } }); } @Override public void start() throws IOException { UdpTransport transport = new UdpTransport("127.0.0.1", port); setTransports( transport ); acceptor = transport.getAcceptor(); acceptor.setHandler(new DnsProtocolHandler(this, store) { @Override public void sessionCreated(IoSession session) throws Exception { // Use our own codec to support AAAA testing session.getFilterChain().addFirst("codec", new ProtocolCodecFilter(new TestDnsProtocolUdpCodecFactory())); } }); // Allow the port to be reused even if the socket is in TIME_WAIT state ((DatagramSessionConfig) acceptor.getSessionConfig()).setReuseAddress(true); // Start the listener acceptor.bind(); } @Override public void stop() { acceptor.dispose(); } /** * ProtocolCodecFactory which allows to test AAAA resolution */ private final class TestDnsProtocolUdpCodecFactory implements ProtocolCodecFactory { private DnsMessageEncoder encoder = new DnsMessageEncoder(); private TestAAAARecordEncoder recordEncoder = new TestAAAARecordEncoder(); @Override public ProtocolEncoder getEncoder(IoSession session) throws Exception { return new DnsUdpEncoder() { @Override public void encode(IoSession session, Object message, ProtocolEncoderOutput out) { IoBuffer buf = IoBuffer.allocate( 1024 ); DnsMessage dnsMessage = (DnsMessage) message; encoder.encode(buf, dnsMessage); for (ResourceRecord record: dnsMessage.getAnswerRecords()) { // This is a hack to allow to also test for AAAA resolution as DnsMessageEncoder does not support it and it // is hard to extend, because the interesting methods are private... // In case of RecordType.AAAA we need to encode the RecordType by ourself if (record.getRecordType() == RecordType.AAAA) { try { recordEncoder.put(buf, record); } catch (IOException e) { // Should never happen throw new IllegalStateException(e); } } } buf.flip(); out.write( buf ); } }; } @Override public ProtocolDecoder getDecoder(IoSession session) throws Exception { return new DnsUdpDecoder(); } private final class TestAAAARecordEncoder extends ResourceRecordEncoder { @Override protected void putResourceRecordData(IoBuffer ioBuffer, ResourceRecord resourceRecord) { if (!resourceRecord.get(DnsAttribute.IP_ADDRESS).equals("::1")) { throw new IllegalStateException("Only supposed to be used with IPV6 address of ::1"); } // encode the ::1 ioBuffer.put(new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}); } } } }