Gumdrop

Gumdrop DNS Server

Gumdrop includes a full DNS proxy server that demonstrates the framework's UDP protocol support. The DNS server can resolve queries locally or proxy them to upstream servers, and is designed to be easily subclassed for custom name resolution. It serves both as a practical DNS proxy and as a reference implementation for developers building UDP-based services.

Contents

Architecture

The DNS server is built on Gumdrop's UDPEndpoint abstraction, which provides non-blocking UDP I/O using the same SelectorLoop architecture as TCP servers. Each DNS query arrives as a datagram, is parsed, processed, and the response is sent back—all within the event-driven framework.

Query Processing Flow

  1. Receive - UDP datagram arrives, parsed into DNSMessage
  2. Cache lookup - check for cached response (if caching enabled)
  3. Custom resolution - call resolve() for local handling
  4. Upstream proxy - forward to upstream DNS servers if not resolved locally
  5. Cache storage - cache the response respecting TTL
  6. Send - serialize response and send UDP datagram back to client

Core Components

Features

Upstream Proxying

When the server cannot resolve a query locally, it forwards the request to configured upstream DNS servers. Multiple upstreams can be specified; they are tried in order until one responds.

Response Caching

The server maintains an in-memory cache to reduce upstream queries:

DTLS Support

For secure DNS (DNS over DTLS), the server can be configured with TLS credentials. DTLS session state is maintained per remote address, providing encrypted DNS for privacy-conscious deployments.

Upstream TCP Fallback

When a UDP response from an upstream server has the TC (truncated) flag set, the server automatically retries the query over TCP using standard DNS-over-TCP framing (2-byte length prefix per RFC 1035 section 4.2.1). This is transparent to the client and ensures that large responses (such as DNSSEC-signed or multi-record answers) are delivered intact.

Response ID Validation

Upstream responses are validated to ensure the DNS message ID matches the query that was sent (RFC 5452). Mismatched IDs are logged and discarded, protecting against cache poisoning and spoofed responses.

Protocol Compliance

Gumdrop's DNS implementation goes beyond basic query/response handling with comprehensive protocol compliance across multiple RFCs.

EDNS0 (RFC 6891)

Both the server proxy and the client resolver advertise EDNS0 support by including an OPT pseudo-record in outgoing queries. This signals the maximum UDP payload size (4096 bytes by default), allowing larger responses without TCP fallback. EDNS0 options are also used as the transport for DNS cookies and padding.

DNSSEC Validation (RFC 4033–4035, RFC 5155)

Gumdrop supports DNSSEC validation for DNS responses. When enabled, the resolver sets the DO (DNSSEC OK) bit in EDNS0 queries to request DNSSEC records from upstream servers, then validates the chain of trust from the signer zone to a configured trust anchor.

Supported algorithms (per RFC 8624):

Supported DS digest types: SHA-1 (1), SHA-256 (2), SHA-384 (4).

All cryptographic operations use java.security from the java.base module and are purely CPU-bound — no blocking I/O, fully safe for the NIO event loop. DNSKEY and DS records needed for chain-of-trust validation are fetched asynchronously through the existing resolver callback model.

Configuration:

// Enable DNSSEC on the client resolver
DNSResolver resolver = new DNSResolver();
resolver.setDnssecEnabled(true);
resolver.addServer("8.8.8.8");
resolver.open();

// Enable DNSSEC-aware proxying on the server
DNSService service = new DNSService();
service.setDnssecEnabled(true);

When the server has DNSSEC enabled, it sets DO in upstream queries and strips DNSSEC records from responses to clients that did not request them (no DO bit). The AD (Authenticated Data) flag is preserved from upstream responses.

DNS Message Compression (RFC 1035 section 4.1.4)

Outgoing DNS messages use name compression as specified in RFC 1035 section 4.1.4. Repeated domain name suffixes are replaced with pointers to earlier occurrences, reducing message size on the wire. This is especially effective for responses containing multiple records with shared suffixes.

DNS Cookies (RFC 7873)

The server and resolver support DNS cookies, a lightweight mechanism for source address verification carried as an EDNS0 option. The client generates a random cookie and includes it in queries; the server replies with a server cookie computed using HMAC-SHA256. On subsequent queries, the client presents the cached server cookie for validation. This provides protection against off-path spoofing without the overhead of DNSSEC or encrypted transports.

DNS over TLS — DoT (RFC 7858)

The DoT client transport includes several optimizations and security features:

DNS over QUIC — DoQ (RFC 9250)

DoQ support includes error handling, session resumption, and connection reuse:

DNS over HTTPS — DoH (RFC 8484)

DoHClientTransport provides DNS resolution over HTTPS, sending queries as HTTP POST requests with application/dns-message content type. The response body contains the raw DNS wire-format answer.

Custom Resolution

The DNS service is designed for extension. By subclassing DNSService and overriding the resolve() method, you can implement custom name resolution for any use case.

The resolve() Method

protected DNSMessage resolve(DNSMessage query)

Return a DNSMessage response to handle the query locally, or return null to proxy the query to upstream servers. This simple contract makes it easy to:

Example: Internal Domain Resolution

public class InternalDNSService extends DNSService {
    
    private Map<String, InetAddress> internalHosts = new HashMap<>();
    
    public InternalDNSService() {
        internalHosts.put("app.internal", InetAddress.getByName("10.0.1.10"));
        internalHosts.put("db.internal", InetAddress.getByName("10.0.1.20"));
        internalHosts.put("cache.internal", InetAddress.getByName("10.0.1.30"));
    }
    
    @Override
    protected DNSMessage resolve(DNSMessage query) {
        DNSQuestion question = query.getQuestions().get(0);
        String name = question.getName();
        
        // Handle .internal domain
        if (name.endsWith(".internal")) {
            InetAddress addr = internalHosts.get(name);
            if (addr != null) {
                List<DNSResourceRecord> answers = new ArrayList<>();
                answers.add(DNSResourceRecord.a(name, 300, addr));
                return query.createResponse(answers);
            }
            // Name not found in internal domain
            return query.createErrorResponse(DNSMessage.RCODE_NXDOMAIN);
        }
        
        // All other queries go to upstream
        return null;
    }
}

Example: DNS-Based Ad Blocking

public class AdBlockingDNSService extends DNSService {
    
    private Set<String> blockedDomains = new HashSet<>();
    
    public void loadBlocklist(Path file) throws IOException {
        blockedDomains.addAll(Files.readAllLines(file));
    }
    
    @Override
    protected DNSMessage resolve(DNSMessage query) {
        DNSQuestion question = query.getQuestions().get(0);
        String name = question.getName().toLowerCase();
        
        // Check against blocklist
        if (blockedDomains.contains(name)) {
            // Return NXDOMAIN for blocked domains
            return query.createErrorResponse(DNSMessage.RCODE_NXDOMAIN);
        }
        
        // Allow through to upstream
        return null;
    }
}

Example: Dynamic Service Discovery

public class ServiceDiscoveryDNS extends DNSService {
    
    private ServiceRegistry registry;
    
    @Override
    protected DNSMessage resolve(DNSMessage query) {
        DNSQuestion question = query.getQuestions().get(0);
        
        // Handle SRV queries for service discovery
        if (question.getType() == DNSType.SRV) {
            String serviceName = question.getName();
            List<ServiceInstance> instances = registry.lookup(serviceName);
            
            if (!instances.isEmpty()) {
                List<DNSResourceRecord> answers = new ArrayList<>();
                for (ServiceInstance inst : instances) {
                    answers.add(DNSResourceRecord.srv(serviceName, 300,
                        inst.getPriority(), inst.getWeight(),
                        inst.getPort(), inst.getHostname()));
                }
                return query.createResponse(answers);
            }
        }
        
        return null;
    }
}

Record Types

The DNSResourceRecord class provides factory methods for common record types:

TypeFactory MethodDescription
ADNSResourceRecord.a(name, ttl, address) IPv4 address
AAAADNSResourceRecord.aaaa(name, ttl, address) IPv6 address
CNAMEDNSResourceRecord.cname(name, ttl, canonical) Canonical name (alias)
MXDNSResourceRecord.mx(name, ttl, priority, exchange) Mail exchange
NSDNSResourceRecord.ns(name, ttl, nameserver) Authoritative name server
PTRDNSResourceRecord.ptr(name, ttl, hostname) Pointer (reverse DNS)
SOADNSResourceRecord.soa(...) Start of authority
TXTDNSResourceRecord.txt(name, ttl, text) Text record
SRVDNSResourceRecord.srv(name, ttl, priority, weight, port, target) Service location

DNSSEC Record Types

DNSSEC record types (RFC 4034, RFC 5155) are parsed from wire format and provide RDATA accessor methods:

TypeValueDescription
DS43Delegation signer — links child DNSKEY to parent zone
RRSIG46DNSSEC signature — covers an RRset
NSEC47Next secure record — denial of existence
DNSKEY48DNS public key — zone signing key or key signing key
NSEC350Hashed denial of existence (RFC 5155)
NSEC3PARAM51NSEC3 parameters (RFC 5155)

Metrics

When a TelemetryConfig is set on the service, DNSServerMetrics records the following OpenTelemetry metrics. Each metric includes a dns.transport attribute (udp, dot, or doq) identifying the listener that handled the query.

MetricTypeDescription
dns.server.queriesCounterQueries received (by dns.question.type, dns.transport)
dns.server.responsesCounterResponses sent (by dns.response.code, dns.transport)
dns.server.query.durationHistogramQuery processing time (ms)
dns.server.cache.hitsCounterCache hits
dns.server.cache.missesCounterCache misses
dns.server.upstream.queriesCounterQueries forwarded upstream
dns.server.upstream.durationHistogramUpstream query duration (ms)
dns.server.upstream.failuresCounterUpstream query failures

Configuration

Listeners

A DNSService can have multiple listeners. Each listener exposes the same resolution logic over a different transport. You can combine any of the following:

ListenerClassTransportDescription
DNSListenerorg.bluezoo.gumdrop.dns.DNSListenerUDPStandard DNS over UDP, default port 53
DoTListenerorg.bluezoo.gumdrop.dns.DoTListenerTCP + TLSDNS over TLS (DoT), default port 853
DoQListenerorg.bluezoo.gumdrop.dns.DoQListenerQUICDNS over QUIC (DoQ), default port 853

Configure one or more <listener> elements per service. For example, you can serve plain DNS on 5353 and DoT on 853 from the same service.

DNSService Properties

Property names use kebab-case in configuration; the dependency injection framework maps them to camelCase setters (e.g. upstream-serverssetUpstreamServers). Same convention as HTTP and other services.

Listener Properties

TLS/DTLS Properties (DNSListener, DoTListener)

For UDP with DTLS and for DoT (TCP+TLS), use the same keystore properties as other TLS listeners (e.g. HTTP, SMTP):

QUIC Properties (DoQListener)

DoQ uses QUIC (TLS 1.3 with PEM certs), like the HTTP/3 listener. Use cert-file and key-file, not keystore properties:

Example Configuration

<!-- DNS proxy on non-privileged port -->
<service id="dns" class="org.bluezoo.gumdrop.dns.DNSService">
    <property name="upstream-servers">8.8.8.8 1.1.1.1</property>
    <property name="cache-enabled">true</property>
    <listener class="org.bluezoo.gumdrop.dns.DNSListener"
            port="5353"/>
</service>

<!-- DNS with DoT listener (keystore-file / keystore-pass like HTTP/SMTP) -->
<service id="dns-secure" class="org.bluezoo.gumdrop.dns.DNSService">
    <property name="upstream-servers">8.8.8.8</property>
    <listener class="org.bluezoo.gumdrop.dns.DNSListener"
            port="53"/>
    <listener class="org.bluezoo.gumdrop.dns.DoTListener"
            port="853" secure="true"
            keystore-file="keystore.p12"
            keystore-pass="secret"/>
</service>

<!-- DNS with UDP, DoT, and DoQ listeners (DoQ uses cert-file / key-file like HTTP/3) -->
<service id="dns-full" class="org.bluezoo.gumdrop.dns.DNSService">
    <property name="upstream-servers">8.8.8.8 1.1.1.1</property>
    <property name="cache-enabled">true</property>
    <listener class="org.bluezoo.gumdrop.dns.DNSListener"
            port="5353"/>
    <listener class="org.bluezoo.gumdrop.dns.DoTListener"
            port="853" secure="true"
            keystore-file="keystore.p12"
            keystore-pass="secret"/>
    <listener class="org.bluezoo.gumdrop.dns.DoQListener"
            port="853" secure="true"
            cert-file="cert.pem"
            key-file="key.pem"/>
</service>

Upstream Server Format

Upstream servers can be specified with optional ports:

DNS Resolver (Client)

DNSResolver is an asynchronous DNS client for outbound lookups. It uses non-blocking I/O and delivers results via the DNSQueryCallback interface. Use it when your Gumdrop service needs to resolve names (e.g. MX lookups for SMTP, TXT for SPF/DKIM/DMARC, or A/AAAA for connecting to backends).

Using DNSResolver Inside Gumdrop Services

You can configure a DNSResolver as a component and inject it into services that need DNS. The resolver runs on the event loop. When you set the resolver's SelectorLoop to match the handler's endpoint (see SelectorLoop Affinity below), callbacks are invoked on the same thread as the handler, so you can safely update handler state from the callback without extra locking.

Configure upstream servers (e.g. upstream-servers or use-system-resolvers), then call queryA, queryTXT, queryMX, etc. with a DNSQueryCallback. Results and errors are delivered asynchronously.

SelectorLoop Affinity

When a DNSResolver is used from a Gumdrop service (e.g. an HTTP or SMTP handler), set the resolver's SelectorLoop to the same loop as the connection's endpoint. That way all DNS callbacks run on the same thread as the handler, avoiding cross-thread coordination.

Call setSelectorLoop(SelectorLoop) with the endpoint's loop before calling open(). If you do not set a SelectorLoop, the resolver uses an arbitrary Gumdrop worker loop; callbacks will still be correct but may run on a different thread than the handler that started the query.

// In a handler that has access to the endpoint's SelectorLoop:
SelectorLoop loop = endpoint.getSelectorLoop();
resolver.setSelectorLoop(loop);
resolver.open();

resolver.queryTXT("_dmarc.example.com", new DNSQueryCallback() {
    public void onResponse(DNSMessage response) {
        // Runs on the same SelectorLoop thread as the handler
        for (DNSResourceRecord rr : response.getAnswers()) {
            if (rr.getType() == DNSType.TXT) {
                String txt = rr.getTxtData();
                // Use result...
            }
        }
    }
    public void onError(String error) {
        // Handle timeout or error
    }
});

In configuration, if your service obtains the resolver by reference (e.g. from the registry), the service's init() or the code that wires the resolver to the handler should set the SelectorLoop from the handler's endpoint when the first request arrives, or use a shared loop if your design assigns one loop per service.

Client Transports

The resolver uses a pluggable DNSClientTransport interface. The default is plain UDP, but you can select any of the following:

TransportClassRFCDefault Port
UDPUDPDNSClientTransport RFC 103553
TCPTCPDNSClientTransport RFC 103553
DoTTCPDNSClientTransport (secure) RFC 7858853
DoQDoQClientTransport RFC 9250853
DoHDoHClientTransport RFC 8484443

To use DoT, create a TCPDNSClientTransport with setSecure(true) and optionally configure SPKI fingerprints for the Strict privacy profile. For DoQ, use DoQClientTransport or DoQConnectionPool for persistent connections. For DoH, use DoHClientTransport and optionally set a custom URI path.

Truncated Response Retry

When a UDP response has the TC (truncated) flag set, DNSResolver automatically retries over TCP using standard DNS-over-TCP framing (2-byte length prefix, RFC 1035 section 4.2.2). The retry is transparent to the caller.

SRV Record Queries

DNSResolver provides a convenience method for SRV lookups (RFC 2782) commonly used for service discovery:

resolver.querySRV("_xmpp-server._tcp.example.com",
        new DNSQueryCallback() {
    public void onResponse(DNSMessage response) {
        for (DNSResourceRecord rr : response.getAnswers()) {
            System.out.println(rr.getSRVTarget() + ":"
                + rr.getSRVPort()
                + " (priority=" + rr.getSRVPriority()
                + ", weight=" + rr.getSRVWeight() + ")");
        }
    }
    public void onError(String error) {
        // Handle error
    }
});

UDP Protocol Support

The DNS server showcases Gumdrop's UDP protocol support through the UDPEndpoint abstraction, created via UDPTransportFactory. This demonstrates how to build UDP-based services within the event-driven framework.

UDPEndpoint

UDPEndpoint is the UDP equivalent of TCPEndpoint. Unlike TCP, there are no per-client endpoint objects—the server receives datagrams directly and sends responses to the source address. A UDPEndpoint is created by adding a UDPTransportFactory to a UDPListener.

public abstract class UDPEndpoint extends Endpoint {
    
    // Override to handle incoming datagrams
    protected abstract void receive(ByteBuffer data, InetSocketAddress source);
    
    // Send a response datagram
    public void sendTo(ByteBuffer data, InetSocketAddress destination);
}

Key Differences from TCP

AspectTCP (TCPListener)UDP (UDPEndpoint)
Endpoint modelPer-client TCPEndpoint instances Single endpoint, multiple sources
Message boundariesStream-based, must frame Preserved per datagram
ReliabilityGuaranteed delivery Best-effort, may lose/reorder
TLS supportSSL/TLS DTLS

Building Custom UDP Services

The DNS server serves as a template for building other UDP-based services. Create a UDPListener with a UDPTransportFactory and provide an ProtocolHandler to process datagrams:

UDPListener server = new UDPListener();
server.setPort(9000);
server.setDescription("my-service");
server.setTransportFactory(new UDPTransportFactory());
server.setHandlerFactory(new ProtocolHandlerFactory() {
    public ProtocolHandler createHandler() {
        return new MyUDPHandler();
    }
});

public class MyUDPHandler implements ProtocolHandler {
    
    @Override
    public void receive(ByteBuffer data, Endpoint endpoint) {
        // Parse the datagram
        MyProtocolMessage request = MyProtocolMessage.parse(data);
        
        // Process and generate response
        MyProtocolMessage response = process(request);
        
        // Send response back
        endpoint.write(response.serialize());
    }
}

Use Cases

The DNS server demonstrates patterns applicable to other UDP protocols:


← Back to Main Page | HTTP Server & Client | SMTP Server & Client | Telemetry

Gumdrop DNS Server