跳到主要内容

使用Netty搭建一个DNS服务器

1. 前景提要

本来是想回答知乎上的问题:如何搭建DNS服务器?

写着写着,加上知乎编辑器真的很难用,还是写在自己的博客吧。

完整代码你可以在这里找到:https://github.com/MingGH/demo-dns-java

需要用到的库是:https://github.com/dnsjava/dnsjava

2. How to Start

这里使用 projectreactor 集成 dnsjava + netty-all进行实现,需要引入的依赖是:

<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>

接着使用netty构建一个UDP服务器来处理DNS请求,使用 NioEventLoopGroup 作为事件循环组,NioDatagramChannel 作为UDP通道。

@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. 使用 DnsHandler 处理请求

然后使用 DnsHandler 处理请求,该方法是Netty框架的回调方法,当有UDP数据包到达时会被调用。

代码中处理了:解析DNS请求、记录查询日志、转发到上游DNS服务器、返回响应给客户端。

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> {

/**
* 处理接收到的DNS请求数据包
*
* @param ctx ChannelHandlerContext对象,用于与Channel进行交互
* @param packet 接收到的UDP数据包,包含DNS请求数据
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) {
// 从数据包中读取请求的字节数据
byte[] requestBytes = new byte[packet.content().readableBytes()];
packet.content().readBytes(requestBytes);

// 获取客户端地址信息
InetSocketAddress clientAddress = packet.sender();

try {
// 解析DNS请求消息
Message query = new Message(requestBytes);
// 获取查询问题部分
Record question = query.getQuestion();

// 记录客户端IP和查询的域名
String clientIp = clientAddress.getAddress().getHostAddress();
log.info("Received DNS query from {}: {}", clientIp, question.getName());
// TODO: 异步记录 clientAddress + question.getName()

// 转发到上游 DNS
UpstreamDnsResolver.queryUpstream(query).subscribe(response -> {
// 将上游DNS响应转换为字节数组
byte[] responseBytes = response.toWire();
// 创建ByteBuf包装响应数据
ByteBuf buffer = Unpooled.copiedBuffer(responseBytes);
// 将响应数据包发送回客户端
ctx.writeAndFlush(new DatagramPacket(buffer, clientAddress));
}, error -> {
// 处理上游DNS查询失败的情况
log.info("Upstream DNS query failed: " + error.getMessage());
});

} catch (Exception e) {
// 处理无效DNS请求的情况
log.info("Invalid DNS request: " + e.getMessage());
}
}

}

当中还有个细节需要注意:通过 UpstreamDnsResolver.queryUpstream 转发到上游 DNS

4. 通过 UpstreamDnsResolver.queryUpstream 转发到上游 DNS

/**
* 上游DNS解析器
*
* 该类负责将DNS查询请求转发到上游DNS服务器(如Cloudflare或Google的公共DNS)。
* 提供了异步的响应式编程接口,支持故障转移机制。
*/
public class UpstreamDnsResolver {

/**
* 上游DNS服务器列表
* 包含Cloudflare DNS (1.1.1.1) 和 Google DNS (8.8.8.8)
*/
private static final List<String> UPSTREAM_SERVERS = List.of("1.1.1.1", "8.8.8.8");

/**
* 向上游DNS服务器查询DNS记录
*
* 该方法采用异步响应式编程模型,会依次尝试列表中的DNS服务器,
* 一旦有一个服务器响应成功就立即返回结果。
*
* @param query 需要查询的DNS消息对象
* @return Mono<Message> 包含DNS响应消息的异步结果
*/
public static Mono<Message> queryUpstream(Message query) {
// 将上游DNS服务器列表转换为Flux流
return Flux.fromIterable(UPSTREAM_SERVERS)
// 对每个DNS服务器并行执行查询操作
.flatMap(dns ->
// 将同步的DNS查询操作包装为Mono异步操作
Mono.fromCallable(() -> {
// 创建指定DNS服务器的解析器
Resolver resolver = new SimpleResolver(dns);
// 设置查询超时时间为2秒
resolver.setTimeout(2);
// 发送DNS查询请求并返回响应
return resolver.send(query);
})
// 在弹性调度器上执行阻塞的DNS查询操作
.subscribeOn(Schedulers.boundedElastic())
)
// 获取第一个成功的响应结果(实现故障转移)
.next();
}

}

5. 最后启动试试

显示 DNS Server started on port 53 就表示端口打开了。

提示

53端口是dns服务器默认端口,不可改为其他端口

接着可以配置一下自己的电脑dns试试,就能看到电脑请求dns的记录了