前言
DNS协议作为着互联网客户端-服务器通信形式得第一关,在当下每天都有成千上亿上网记载产生得当今社会,其重要性自然不可言喻。在国内比较有名得DNS服务器有电信得114.114.114.114、阿里云得223.5.5.5,DNSPod得119.29.29.29,装备一个好的DNS服务器能够缩短恳求呼应时刻、下降DNS劫持概率,提高上网体验。
上面这些都是互联网公用DNS服务器,本文博主教我们运用 Java Netty
自建DNS署理服务器,目前网上关于运用Netty自建DNS服务器得教程良莠不齐,大多没有署理过程,达不到博主想要得署理作用,因而创建此文。觉得本文有协助得能够重视博主
- github.com/wayn111
一、自建DNS署理服务器有哪些优势
- 域名控制:关于特定域名能够自在控制拜访权限(屏蔽对特定网站拜访)
- 域名记载:记载局域网内各个主机得域名拜访(记载职工上网记载)
- 装备内网域名:经过自建DNS服务器能够装备内网域名,节省成本
- DNS负载均衡:经过自建DNS服务器能够轻松实现关于拜访域名得负载均衡装备
- CDN网络:回来距离客户端最近的可用IP
- …
二、自建DNS署理服务器代码
1. 增加域名黑名单文件,Maven项目在 resources
文件夹下增加 black_list.txt
文件,注意域名最后的 .
,在 Netty
自带的DNS查询Handler中,报文回来的域名最后都是带 .
的。实际上每个域名后都有一个 .
,它表示根域名,通常它都是被隐藏的。
// 增加域名黑名单
google.com.
facebook.com.
接着解析 black_list.txt
文件,初始化内容放入 BLACK_LIST_DOMAIN
private static final List<String> BLACK_LIST_DOMAIN = new ArrayList<>();
static {
String s;
try (InputStream is = DnsServer.class.getClassLoader().getResourceAsStream("black_list.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
while (StrUtil.isNotBlank(s = br.readLine())) {
BLACK_LIST_DOMAIN.add(s);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
2. 运用UDP协议绑定本机53端口(DNS协议默认运用UDP 53端口),并初始化 ProxyUdp
DNS恳求署理目标
@Slf4j
public final class DnsServer {
private static final List<String> BLACK_LIST_DOMAIN = new ArrayList<>();
static {
...
}
public static void main(String[] args) throws Exception {
ProxyUdp proxyUdp = new ProxyUdp();
proxyUdp.init();
final int[] num = {0};
final NioEventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioDatagramChannel.class)
.handler(new ChannelInitializer<NioDatagramChannel>() {
@Override
protected void initChannel(NioDatagramChannel nioDatagramChannel) {
nioDatagramChannel.pipeline().addLast(...);
}
}).option(ChannelOption.SO_BROADCAST, true);
int port = 53;
ChannelFuture future = bootstrap.bind(port).addListener(future1 -> {
log.info("server listening port:{}", port);
});
future.channel().closeFuture().addListener(future1 -> {
if (future.isSuccess()) {
log.info(future.channel().toString());
}
});
}
}
3. 给 nioDatagramChannel.pipeline()
增加多个 ChannelHandler
目标,接受DNS查询报文以及发送DNS呼应报文。划要点来了 在 new SimpleChannelInboundHandler<DatagramDnsQuery>()
中解析客户端发送DNS查询报文, 获取拜访域名信息,假如拜访域名在黑名单中,则经过 getDatagramDnsResponse()
办法直接回来 192.168.1.1
的DNS呼应报文,反之则经过 proxyUdp
目标转发DNS查询。
nioDatagramChannel.pipeline().addLast(new DatagramDnsQueryDecoder());
nioDatagramChannel.pipeline().addLast(new SimpleChannelInboundHandler<DatagramDnsQuery>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramDnsQuery msg) {
try {
DefaultDnsQuestion dnsQuestion = msg.recordAt(DnsSection.QUESTION);
String name = dnsQuestion.name();
log.info(name + ++num[0]);
Channel channel = ctx.channel();
int id = msg.id();
channel.attr(AttributeKey.<DatagramDnsQuery>valueOf(String.valueOf(id))).set(msg);
if (BLACK_LIST_DOMAIN.contains(name)) {
DnsQuestion question = msg.recordAt(DnsSection.QUESTION);
DatagramDnsResponse dnsResponse = getDatagramDnsResponse(msg, id, question);
channel.writeAndFlush(dnsResponse);
return;
}
proxyUdp.send(name, msg.id(), channel);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
private DatagramDnsResponse getDatagramDnsResponse(DatagramDnsQuery msg, int id, DnsQuestion question) {
DatagramDnsResponse dnsResponse = new DatagramDnsResponse(msg.recipient(), msg.sender(), id);
dnsResponse.addRecord(DnsSection.QUESTION, question);
DefaultDnsRawRecord queryAnswer = new DefaultDnsRawRecord(
question.name(),
DnsRecordType.A, 600, Unpooled.wrappedBuffer(new byte[]{(byte) 192, (byte) 168, 1, 1}));
dnsResponse.addRecord(DnsSection.ANSWER, queryAnswer);
return dnsResponse;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable e) {
log.error(e.getMessage(), e);
}
});
nioDatagramChannel.pipeline().addLast(new DatagramDnsResponseEncoder());
4. ProxyUdp
作为DNS查询署理类会经过 send(String domain, int id, Channel serverChannel)
办法传入DnsServer类收到的拜访域名、DNS事务ID、serverChannel。随后包装拜访域名恳求DNS服务器114.114.114.114
,最后经过 new SimpleChannelInboundHandler<DatagramDnsResponse>()
将收到的DNS呼应报文经过上一步传入得 serverChannel
输出到客户端。
@Slf4j
class ProxyUdp {
private Channel serverChannel;
private Channel proxyChannel;
public void init() throws InterruptedException {
EventLoopGroup proxyGroup = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(proxyGroup)
.channel(NioDatagramChannel.class)
.handler(new ChannelInitializer<DatagramChannel>() {
@Override
protected void initChannel(DatagramChannel ch) {
ChannelPipeline p = ch.pipeline();
p.addLast(new DatagramDnsQueryEncoder())
.addLast(new DatagramDnsResponseDecoder())
.addLast(new SimpleChannelInboundHandler<DatagramDnsResponse>() {
@Override
public void channelActive(ChannelHandlerContext ctx) {
log.info(ctx.channel().toString());
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramDnsResponse msg) {
DatagramDnsQuery dnsQuery = localChannel.attr(AttributeKey.<DatagramDnsQuery>valueOf(String.valueOf(msg.id()))).get();
DnsQuestion question = msg.recordAt(DnsSection.QUESTION);
DatagramDnsResponse dnsResponse = new DatagramDnsResponse(dnsQuery.recipient(), dnsQuery.sender(), msg.id());
dnsResponse.addRecord(DnsSection.QUESTION, question);
for (int i = 0, count = msg.count(DnsSection.ANSWER); i < count; i++) {
DnsRecord record = msg.recordAt(DnsSection.ANSWER, i);
if (record.type() == DnsRecordType.A) {
// just print the IP after query
DnsRawRecord raw = (DnsRawRecord) record;
DefaultDnsRawRecord queryAnswer = new DefaultDnsRawRecord(
question.name(),
DnsRecordType.A, 600, Unpooled.wrappedBuffer(ByteBufUtil.getBytes(raw.content())));
dnsResponse.addRecord(DnsSection.ANSWER, queryAnswer);
}
}
serverChannel.writeAndFlush(dnsResponse);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable e) {
log.error(e.getMessage(), e);
}
});
}
});
proxyChannel = b.bind(0).sync().addListener(future1 -> {
log.info("绑定成功");
}).channel();
}
public void send(String domain, int id, Channel serverChannel) {
this.serverChannel = serverChannel;
DnsQuery query = new DatagramDnsQuery(null, new InetSocketAddress("114.114.114.114", 53), id).setRecord(
DnsSection.QUESTION,
new DefaultDnsQuestion(domain, DnsRecordType.A));
this.proxyChannel.writeAndFlush(query);
}
}
5. 客户端恳求DNS署理服务器流程图如下
6. 自建DNS服务器悉数代码
@Slf4j
public final class DnsServer {
private static final List<String> BLACK_LIST_DOMAIN = new ArrayList<>();
static {
String s;
try (InputStream is = DnsServer.class.getClassLoader().getResourceAsStream("black_list.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
while (StrUtil.isNotBlank(s = br.readLine())) {
BLACK_LIST_DOMAIN.add(s);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
public static void main(String[] args) throws Exception {
ProxyUdp proxyUdp = new ProxyUdp();
proxyUdp.init();
final int[] num = {0};
final NioEventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioDatagramChannel.class)
.handler(new ChannelInitializer<NioDatagramChannel>() {
@Override
protected void initChannel(NioDatagramChannel nioDatagramChannel) {
nioDatagramChannel.pipeline().addLast(new DatagramDnsQueryDecoder());
nioDatagramChannel.pipeline().addLast(new SimpleChannelInboundHandler<DatagramDnsQuery>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramDnsQuery msg) {
try {
DefaultDnsQuestion dnsQuestion = msg.recordAt(DnsSection.QUESTION);
String name = dnsQuestion.name();
log.info(name + ++num[0]);
Channel channel = ctx.channel();
int id = msg.id();
channel.attr(AttributeKey.<DatagramDnsQuery>valueOf(String.valueOf(id))).set(msg);
if (BLACK_LIST_DOMAIN.contains(name)) {
DnsQuestion question = msg.recordAt(DnsSection.QUESTION);
DatagramDnsResponse dnsResponse = getDatagramDnsResponse(msg, id, question);
channel.writeAndFlush(dnsResponse);
return;
}
proxyUdp.send(name, msg.id(), channel);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
private DatagramDnsResponse getDatagramDnsResponse(DatagramDnsQuery msg, int id, DnsQuestion question) {
DatagramDnsResponse dnsResponse = new DatagramDnsResponse(msg.recipient(), msg.sender(), id);
dnsResponse.addRecord(DnsSection.QUESTION, question);
// just print the IP after query
DefaultDnsRawRecord queryAnswer = new DefaultDnsRawRecord(
question.name(),
DnsRecordType.A, 600, Unpooled.wrappedBuffer(new byte[]{(byte) 192, (byte) 168, 1, 1}));
dnsResponse.addRecord(DnsSection.ANSWER, queryAnswer);
return dnsResponse;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable e) {
log.error(e.getMessage(), e);
}
});
nioDatagramChannel.pipeline().addLast(new DatagramDnsResponseEncoder());
}
}).option(ChannelOption.SO_BROADCAST, true);
int port = 53;
ChannelFuture future = bootstrap.bind(port).addListener(future1 -> {
log.info("server listening port:{}", port);
});
future.channel().closeFuture().addListener(future1 -> {
if (future.isSuccess()) {
log.info(future.channel().toString());
}
});
}
}
@Slf4j
class ProxyUdp {
private Channel localChannel;
private Channel proxyChannel;
public void init() throws InterruptedException {
EventLoopGroup proxyGroup = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(proxyGroup)
.channel(NioDatagramChannel.class)
.handler(new ChannelInitializer<DatagramChannel>() {
@Override
protected void initChannel(DatagramChannel ch) {
ChannelPipeline p = ch.pipeline();
p.addLast(new DatagramDnsQueryEncoder())
.addLast(new DatagramDnsResponseDecoder())
.addLast(new SimpleChannelInboundHandler<DatagramDnsResponse>() {
@Override
public void channelActive(ChannelHandlerContext ctx) {
log.info(ctx.channel().toString());
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramDnsResponse msg) {
DatagramDnsQuery dnsQuery = localChannel.attr(AttributeKey.<DatagramDnsQuery>valueOf(String.valueOf(msg.id()))).get();
DnsQuestion question = msg.recordAt(DnsSection.QUESTION);
DatagramDnsResponse dnsResponse = new DatagramDnsResponse(dnsQuery.recipient(), dnsQuery.sender(), msg.id());
dnsResponse.addRecord(DnsSection.QUESTION, question);
for (int i = 0, count = msg.count(DnsSection.ANSWER); i < count; i++) {
DnsRecord record = msg.recordAt(DnsSection.ANSWER, i);
if (record.type() == DnsRecordType.A) {
// just print the IP after query
DnsRawRecord raw = (DnsRawRecord) record;
DefaultDnsRawRecord queryAnswer = new DefaultDnsRawRecord(
question.name(),
DnsRecordType.A, 600, Unpooled.wrappedBuffer(ByteBufUtil.getBytes(raw.content())));
dnsResponse.addRecord(DnsSection.ANSWER, queryAnswer);
}
}
localChannel.writeAndFlush(dnsResponse);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable e) {
log.error(e.getMessage(), e);
}
});
}
});
proxyChannel = b.bind(0).sync().addListener(future1 -> {
log.info("绑定成功");
}).channel();
}
public void send(String domain, int id, Channel localChannel) {
this.localChannel = localChannel;
DnsQuery query = new DatagramDnsQuery(null, new InetSocketAddress("114.114.114.114", 53), id).setRecord(
DnsSection.QUESTION,
new DefaultDnsQuestion(domain, DnsRecordType.A));
this.proxyChannel.writeAndFlush(query);
}
}
三、本地测试
- 修正本机DNS设置(win11),修正首选、备选DNS地址为127.0.0.1
- 翻开指令行工具,履行DNS缓存清除指令
ipconfig/flushdns
- 翻开浏览器拜访常用网站,控制台输出如下
自此,自建的DNS署理服务器就能够正常运用了
参考资料
- 用 Node.js 手写一个 DNS 服务器
- DNS中有哪些值得学习的优异规划
- netty dns example