waynblog

Java Netty框架自建DNS代理服务器教程

前言

DNS协议作为着互联网客户端-服务器通信模式得第一关,在当下每天都有成千上亿上网记录产生得当今社会,其重要性自然不可言喻。在国内比较有名得DNS服务器有电信得114.114.114.114、阿里云得223.5.5.5,DNSPod得119.29.29.29,配置一个好的DNS服务器可以缩短请求响应时间、降低DNS劫持概率,提升上网体验。

上面这些都是互联网公用DNS服务器,本文博主教大家使用 Java Netty 自建DNS代理服务器,目前网上对于使用Netty自建DNS服务器得教程良莠不齐,大多没有代理步骤,达不到博主想要得代理效果,因而创建此文。觉得本文有帮助得可以关注博主github

  • • https://github.com/wayn111

一、自建DNS代理服务器有哪些优势

  1. 1. 域名控制:对于特定域名可以自由控制访问权限(屏蔽对特定网站访问)

  2. 2. 域名记录:记录局域网内各个主机得域名访问(记录员工上网记录)

  3. 3. 配置内网域名:通过自建DNS服务器可以配置内网域名,节约成本

  4. 4. DNS负载均衡:通过自建DNS服务器可以轻松实现对于访问域名得负载均衡配置

  5. 5. ...

二、自建DNS代理服务器代码

  1. 1. 添加域名黑名单文件,resources 文件夹下添加 black_list.txt 文件

google.com.
facebook.com.

初始化 BLACK_LIST_DOMAIN

privatestaticfinalList<String> BLACK_LIST_DOMAIN =newArrayList<>();
static{
String s;
try(InputStreamis=DnsServer.class.getClassLoader().getResourceAsStream("black_list.txt");
BufferedReaderbr=newBufferedReader(newInputStreamReader(is))){
while(StrUtil.isNotBlank(s = br.readLine())){
                BLACK_LIST_DOMAIN.add(s);
}
}catch(Exception e){
            log.error(e.getMessage(), e);
}
}
  1. 1. 使用 UDP 协议绑定本机53端口,并初始化 ProxyUdp DNS请求代理对象

@Slf4j
publicfinalclassDnsServer{
privatestaticfinalList<String> BLACK_LIST_DOMAIN =newArrayList<>();
static{
...
}

publicstaticvoidmain(String[] args)throwsException{
ProxyUdpproxyUdp=newProxyUdp();
        proxyUdp.init();
finalint[] num ={0};
finalNioEventLoopGroupgroup=newNioEventLoopGroup();
Bootstrapbootstrap=newBootstrap();
        bootstrap.group(group).channel(NioDatagramChannel.class)
.handler(newChannelInitializer<NioDatagramChannel>(){
@Override
protectedvoidinitChannel(NioDatagramChannel nioDatagramChannel){
                        nioDatagramChannel.pipeline().addLast(...);
}
}).option(ChannelOption.SO_BROADCAST,true);

intport=53;
ChannelFuturefuture= bootstrap.bind(port).addListener(future1 ->{
            log.info("server listening port:{}", port);
});

        future.channel().closeFuture().addListener(future1 ->{
if(future.isSuccess()){
                log.info(future.channel().toString());
}
});
}
}

  1. 1. 给 nioDatagramChannel.pipeline() 添加 ChannelHandler

nioDatagramChannel.pipeline().addLast(newDatagramDnsQueryDecoder());
                        nioDatagramChannel.pipeline().addLast(newSimpleChannelInboundHandler<DatagramDnsQuery>(){
@Override
protectedvoidchannelRead0(ChannelHandlerContext ctx,DatagramDnsQuery msg){
try{
DefaultDnsQuestiondnsQuestion= msg.recordAt(DnsSection.QUESTION);
Stringname= dnsQuestion.name();
                                    log.info(name +++num[0]);
Channelchannel= ctx.channel();
intid= msg.id();
                                    channel.attr(AttributeKey.<DatagramDnsQuery>valueOf(String.valueOf(id))).set(msg);
if(BLACK_LIST_DOMAIN.contains(name)){
DnsQuestionquestion= msg.recordAt(DnsSection.QUESTION);
DatagramDnsResponsednsResponse= getDatagramDnsResponse(msg, id, question);
                                        channel.writeAndFlush(dnsResponse);
return;
}
                                    proxyUdp.send(name, msg.id(), channel);
}catch(Exception e){
                                    log.error(e.getMessage(), e);
}
}

privateDatagramDnsResponsegetDatagramDnsResponse(DatagramDnsQuery msg,int id,DnsQuestion question){
DatagramDnsResponsednsResponse=newDatagramDnsResponse(msg.recipient(), msg.sender(), id);
                                dnsResponse.addRecord(DnsSection.QUESTION, question);
DefaultDnsRawRecordqueryAnswer=newDefaultDnsRawRecord(
                                        question.name(),
DnsRecordType.A,600,Unpooled.wrappedBuffer(newbyte[]{(byte)192,(byte)168,1,1}));
                                dnsResponse.addRecord(DnsSection.ANSWER, queryAnswer);
return dnsResponse;
}

@Override
publicvoidexceptionCaught(ChannelHandlerContext ctx,Throwable e){
                                log.error(e.getMessage(), e);
}
});
                        nioDatagramChannel.pipeline().addLast(newDatagramDnsResponseEncoder());

在 new SimpleChannelInboundHandler<DatagramDnsQuery>() 中 解析客户端DNS查询报文, 获取访问域名信息,如果访问域名在黑名单中,则通过 getDatagramDnsResponse() 直接返回 192.168.1.1 的DNS响应报文,反之则通过 proxyUdp 对象转发DNS查询。

  1. 1. ProxyUdp 作为DNS查询代理类会通过 send(String domain, int id, Channel serverChannel) 方法传入DnsServer类收到的访问域名、DNS事务ID、serverChannel。随后包装访问域名请求DNS服务器114.114.114.114,最后通过 new SimpleChannelInboundHandler<DatagramDnsResponse>() 将收到的DNS响应报文通过上一步传入得 serverChannel 输出到客户端。

@Slf4j
classProxyUdp{
privateChannel serverChannel;
privateChannel proxyChannel;

publicvoidinit()throwsInterruptedException{
EventLoopGroupproxyGroup=newNioEventLoopGroup();
Bootstrapb=newBootstrap();
        b.group(proxyGroup)
.channel(NioDatagramChannel.class)
.handler(newChannelInitializer<DatagramChannel>(){
@Override
protectedvoidinitChannel(DatagramChannel ch){
ChannelPipelinep= ch.pipeline();
                        p.addLast(newDatagramDnsQueryEncoder())
.addLast(newDatagramDnsResponseDecoder())
.addLast(newSimpleChannelInboundHandler<DatagramDnsResponse>(){
@Override
publicvoidchannelActive(ChannelHandlerContext ctx){
                                        log.info(ctx.channel().toString());
}

@Override
protectedvoidchannelRead0(ChannelHandlerContext ctx,DatagramDnsResponse msg){
DatagramDnsQuerydnsQuery= localChannel.attr(AttributeKey.<DatagramDnsQuery>valueOf(String.valueOf(msg.id()))).get();
DnsQuestionquestion= msg.recordAt(DnsSection.QUESTION);
DatagramDnsResponsednsResponse=newDatagramDnsResponse(dnsQuery.recipient(), dnsQuery.sender(), msg.id());
                                        dnsResponse.addRecord(DnsSection.QUESTION, question);

for(inti=0, count = msg.count(DnsSection.ANSWER); i < count; i++){
DnsRecordrecord= msg.recordAt(DnsSection.ANSWER, i);
if(record.type()==DnsRecordType.A){
// just print the IP after query
DnsRawRecordraw=(DnsRawRecord) record;
DefaultDnsRawRecordqueryAnswer=newDefaultDnsRawRecord(
                                                        question.name(),
DnsRecordType.A,600,Unpooled.wrappedBuffer(ByteBufUtil.getBytes(raw.content())));
                                                dnsResponse.addRecord(DnsSection.ANSWER, queryAnswer);
}
}

                                        serverChannel.writeAndFlush(dnsResponse);
}

@Override
publicvoidexceptionCaught(ChannelHandlerContext ctx,Throwable e){
                                        log.error(e.getMessage(), e);
}
});

}
});
        proxyChannel = b.bind(0).sync().addListener(future1 ->{
            log.info("绑定成功");
}).channel();
}

publicvoidsend(String domain,int id,Channel serverChannel){
this.serverChannel = serverChannel;
DnsQueryquery=newDatagramDnsQuery(null,newInetSocketAddress("114.114.114.114",53), id).setRecord(
DnsSection.QUESTION,
newDefaultDnsQuestion(domain,DnsRecordType.A));
this.proxyChannel.writeAndFlush(query);
}
}

  1. 1. 自建DNS服务器全部代码

@Slf4j
publicfinalclassDnsServer{
privatestaticfinalList<String> BLACK_LIST_DOMAIN =newArrayList<>();
static{
String s;
try(InputStreamis=DnsServer.class.getClassLoader().getResourceAsStream("black_list.txt");
BufferedReaderbr=newBufferedReader(newInputStreamReader(is))){
while(StrUtil.isNotBlank(s = br.readLine())){
                BLACK_LIST_DOMAIN.add(s);
}
}catch(Exception e){
            log.error(e.getMessage(), e);
}
}

publicstaticvoidmain(String[] args)throwsException{
ProxyUdpproxyUdp=newProxyUdp();
        proxyUdp.init();
finalint[] num ={0};
finalNioEventLoopGroupgroup=newNioEventLoopGroup();
Bootstrapbootstrap=newBootstrap();
        bootstrap.group(group).channel(NioDatagramChannel.class)
.handler(newChannelInitializer<NioDatagramChannel>(){
@Override
protectedvoidinitChannel(NioDatagramChannel nioDatagramChannel){
                        nioDatagramChannel.pipeline().addLast(newDatagramDnsQueryDecoder());
                        nioDatagramChannel.pipeline().addLast(newSimpleChannelInboundHandler<DatagramDnsQuery>(){

@Override
protectedvoidchannelRead0(ChannelHandlerContext ctx,DatagramDnsQuery msg){
try{
DefaultDnsQuestiondnsQuestion= msg.recordAt(DnsSection.QUESTION);
Stringname= dnsQuestion.name();
                                    log.info(name +++num[0]);
Channelchannel= ctx.channel();
intid= msg.id();
                                    channel.attr(AttributeKey.<DatagramDnsQuery>valueOf(String.valueOf(id))).set(msg);
if(BLACK_LIST_DOMAIN.contains(name)){
DnsQuestionquestion= msg.recordAt(DnsSection.QUESTION);
DatagramDnsResponsednsResponse= getDatagramDnsResponse(msg, id, question);
                                        channel.writeAndFlush(dnsResponse);
return;
}
                                    proxyUdp.send(name, msg.id(), channel);
}catch(Exception e){
                                    log.error(e.getMessage(), e);
}
}

privateDatagramDnsResponsegetDatagramDnsResponse(DatagramDnsQuery msg,int id,DnsQuestion question){
DatagramDnsResponsednsResponse=newDatagramDnsResponse(msg.recipient(), msg.sender(), id);
                                dnsResponse.addRecord(DnsSection.QUESTION, question);

// just print the IP after query
DefaultDnsRawRecordqueryAnswer=newDefaultDnsRawRecord(
                                        question.name(),
DnsRecordType.A,600,Unpooled.wrappedBuffer(newbyte[]{(byte)192,(byte)168,1,1}));
                                dnsResponse.addRecord(DnsSection.ANSWER, queryAnswer);
return dnsResponse;
}

@Override
publicvoidexceptionCaught(ChannelHandlerContext ctx,Throwable e){
                                log.error(e.getMessage(), e);
}
});
                        nioDatagramChannel.pipeline().addLast(newDatagramDnsResponseEncoder());

}
}).option(ChannelOption.SO_BROADCAST,true);

intport=553;
ChannelFuturefuture= 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
classProxyUdp{
privateChannel localChannel;
privateChannel proxyChannel;

publicvoidinit()throwsInterruptedException{
EventLoopGroupproxyGroup=newNioEventLoopGroup();
Bootstrapb=newBootstrap();
        b.group(proxyGroup)
.channel(NioDatagramChannel.class)
.handler(newChannelInitializer<DatagramChannel>(){
@Override
protectedvoidinitChannel(DatagramChannel ch){
ChannelPipelinep= ch.pipeline();
                        p.addLast(newDatagramDnsQueryEncoder())
.addLast(newDatagramDnsResponseDecoder())
.addLast(newSimpleChannelInboundHandler<DatagramDnsResponse>(){
@Override
publicvoidchannelActive(ChannelHandlerContext ctx){
                                        log.info(ctx.channel().toString());
}

@Override
protectedvoidchannelRead0(ChannelHandlerContext ctx,DatagramDnsResponse msg){
DatagramDnsQuerydnsQuery= localChannel.attr(AttributeKey.<DatagramDnsQuery>valueOf(String.valueOf(msg.id()))).get();
DnsQuestionquestion= msg.recordAt(DnsSection.QUESTION);
DatagramDnsResponsednsResponse=newDatagramDnsResponse(dnsQuery.recipient(), dnsQuery.sender(), msg.id());
                                        dnsResponse.addRecord(DnsSection.QUESTION, question);

for(inti=0, count = msg.count(DnsSection.ANSWER); i < count; i++){
DnsRecordrecord= msg.recordAt(DnsSection.ANSWER, i);
if(record.type()==DnsRecordType.A){
// just print the IP after query
DnsRawRecordraw=(DnsRawRecord) record;
DefaultDnsRawRecordqueryAnswer=newDefaultDnsRawRecord(
                                                        question.name(),
DnsRecordType.A,600,Unpooled.wrappedBuffer(ByteBufUtil.getBytes(raw.content())));
                                                dnsResponse.addRecord(DnsSection.ANSWER, queryAnswer);
}
}

                                        localChannel.writeAndFlush(dnsResponse);
}

@Override
publicvoidexceptionCaught(ChannelHandlerContext ctx,Throwable e){
                                        log.error(e.getMessage(), e);
}
});

}
});
        proxyChannel = b.bind(0).sync().addListener(future1 ->{
            log.info("绑定成功");
}).channel();
}

publicvoidsend(String domain,int id,Channel localChannel){
this.localChannel = localChannel;
DnsQueryquery=newDatagramDnsQuery(null,newInetSocketAddress("114.114.114.114",53), id).setRecord(
DnsSection.QUESTION,
newDefaultDnsQuestion(domain,DnsRecordType.A));
this.proxyChannel.writeAndFlush(query);
}
}

三、本地测试

  1. 1. 修改本机DNS设置(win11),修改首选、备选DNS地址为127.0.0.1

    Image
    image.png
  2. 2. 打开命令行工具,执行DNS缓存清除命令 ipconfig/flushdns

    Image
    image.png

自此就可以打开浏览器访问常用网站,看是否能正常访问,来验证自建的DNS服务器效果了

参考资料

  • • 用 Node.js 手写一个 DNS 服务器[1]

  • • DNS中有哪些值得学习的优秀设计[2]

  • • netty dns example[3]

引用链接

[1] 用 Node.js 手写一个 DNS 服务器: https://juejin.cn/post/7105620424368586782
[2] DNS中有哪些值得学习的优秀设计: https://juejin.cn/post/7158963624608792584
[3] netty dns example: https://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example/dns