Building a DNS Server with Netty
1. Background
I originally wanted to answer a question on Zhihu: How to build a DNS server?
As I was writing, combined with the fact that Zhihu's editor is really hard to use, I decided to write it on my own blog instead.
You can find the complete code here: https://github.com/MingGH/demo-dns-java
The library needed is: https://github.com/dnsjava/dnsjava
2. How to Start
Here I use projectreactor integrated with dnsjava + netty-all for implementation. The dependencies needed are:
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.7.8</version>
</dependency>
<dependency>
<groupId>dnsjava</groupId>
<artifactId>dnsjava</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.100.Final</version>
</dependency>
Next, use Netty to build a UDP server to handle DNS requests, using NioEventLoopGroup as the event loop group and NioDatagramChannel as the UDP channel.
@Slf4j
public class DnsServer {
private final int port = 53;
public void start() {
NioEventLoopGroup group = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioDatagramChannel.class)
.option(ChannelOption.SO_BROADCAST, true)
.handler(new DnsHandler());
b.bind(port).addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
log.info("DNS Server started on port " + port);
} else {
log.error("Failed to bind DNS port: " + future.cause());
}
});
}
}
3. Using DnsHandler to Handle Requests
Then use DnsHandler to handle requests. This method is a Netty framework callback method that gets called when UDP packets arrive.
The code handles: parsing DNS requests, logging queries, forwarding to upstream DNS servers, and returning responses to clients.
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.DatagramPacket;
import lombok.extern.slf4j.Slf4j;
import org.xbill.DNS.Message;
import org.xbill.DNS.Record;
import java.net.InetSocketAddress;
public class DnsHandler extends SimpleChannelInboundHandler<DatagramPacket> {
/**
* Handle received DNS request packets
*
* @param ctx ChannelHandlerContext object for interacting with the Channel
* @param packet Received UDP packet containing DNS request data
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) {
// Read request byte data from the packet
byte[] requestBytes = new byte[packet.content().readableBytes()];
packet.content().readBytes(requestBytes);
// Get client address information
InetSocketAddress clientAddress = packet.sender();
try {
// Parse DNS request message
Message query = new Message(requestBytes);
// Get the query question section
Record question = query.getQuestion();
// Log client IP and queried domain name
String clientIp = clientAddress.getAddress().getHostAddress();
log.info("Received DNS query from {}: {}", clientIp, question.getName());
// TODO: Asynchronously log clientAddress + question.getName()
// Forward to upstream DNS
UpstreamDnsResolver.queryUpstream(query).subscribe(response -> {
// Convert upstream DNS response to byte array
byte[] responseBytes = response.toWire();
// Create ByteBuf wrapping response data
ByteBuf buffer = Unpooled.copiedBuffer(responseBytes);
// Send response packet back to client
ctx.writeAndFlush(new DatagramPacket(buffer, clientAddress));
}, error -> {
// Handle upstream DNS query failure
log.info("Upstream DNS query failed: " + error.getMessage());
});
} catch (Exception e) {
// Handle invalid DNS request
log.info("Invalid DNS request: " + e.getMessage());
}
}
}
There's also a detail to note: forwarding to upstream DNS via UpstreamDnsResolver.queryUpstream
4. Forwarding to Upstream DNS via UpstreamDnsResolver.queryUpstream
/**
* Upstream DNS Resolver
*
* This class is responsible for forwarding DNS query requests to upstream DNS servers (such as Cloudflare or Google's public DNS).
* It provides an asynchronous reactive programming interface with failover mechanism support.
*/
public class UpstreamDnsResolver {
/**
* List of upstream DNS servers
* Contains Cloudflare DNS (1.1.1.1) and Google DNS (8.8.8.8)
*/
private static final List<String> UPSTREAM_SERVERS = List.of("1.1.1.1", "8.8.8.8");
/**
* Query DNS records from upstream DNS servers
*
* This method uses an asynchronous reactive programming model and will try DNS servers in the list sequentially,
* returning the result immediately once one server responds successfully.
*
* @param query The DNS message object to query
* @return Mono<Message> Asynchronous result containing the DNS response message
*/
public static Mono<Message> queryUpstream(Message query) {
// Convert upstream DNS server list to Flux stream
return Flux.fromIterable(UPSTREAM_SERVERS)
// Execute query operation in parallel for each DNS server
.flatMap(dns ->
// Wrap synchronous DNS query operation as Mono async operation
Mono.fromCallable(() -> {
// Create resolver for specified DNS server
Resolver resolver = new SimpleResolver(dns);
// Set query timeout to 2 seconds
resolver.setTimeout(2);
// Send DNS query request and return response
return resolver.send(query);
})
// Execute blocking DNS query operation on elastic scheduler
.subscribeOn(Schedulers.boundedElastic())
)
// Get the first successful response result (implementing failover)
.next();
}
}
5. Finally, Let's Start It
When DNS Server started on port 53 is displayed, it means the port is open.
Port 53 is the default DNS server port and cannot be changed to other ports

Then you can configure your computer's DNS to try it out, and you'll be able to see your computer's DNS request records

