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

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

image.png

前言

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

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

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

  1. 域名控制:对于特定域名可以自由控制访问权限(屏蔽对特定网站访问)
  2. 域名记录:记录局域网内各个主机得域名访问(记录员工上网记录)
  3. 配置内网域名:通过自建DNS服务器可以配置内网域名,节约成本
  4. DNS负载均衡:通过自建DNS服务器可以轻松实现对于访问域名得负载均衡配置
  5. CDN网络:返回距离客户端最近的可用IP
  6. ...

二、自建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代理服务器流程图如下

image.png

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);
    }
}

三、本地测试

  1. 修改本机DNS设置(win11),修改首选、备选DNS地址为127.0.0.1
  2. image.png
  3. 打开命令行工具,执行DNS缓存清除命令 ipconfig/flushdns
  4. image.png
  5. 打开浏览器访问常用网站,控制台输出如下
  6. image.png
  7. 自此,自建的DNS代理服务器就可以正常使用了
  8. 参考资料用 Node.js 手写一个 DNS 服务器DNS中有哪些值得学习的优秀设计netty dns example
目录
相关文章
|
1月前
|
弹性计算 运维 安全
阿里云轻量应用服务器与云服务器ECS啥区别?新手帮助教程
阿里云轻量应用服务器适合个人开发者搭建博客、测试环境等低流量场景,操作简单、成本低;ECS适用于企业级高负载业务,功能强大、灵活可扩展。二者在性能、网络、镜像及运维管理上差异显著,用户应根据实际需求选择。
216 10
|
1月前
|
安全 前端开发 Java
《深入理解Spring》:现代Java开发的核心框架
Spring自2003年诞生以来,已成为Java企业级开发的基石,凭借IoC、AOP、声明式编程等核心特性,极大简化了开发复杂度。本系列将深入解析Spring框架核心原理及Spring Boot、Cloud、Security等生态组件,助力开发者构建高效、可扩展的应用体系。(238字)
|
1月前
|
弹性计算 网络协议 Linux
阿里云服务器简介及使用教程,附送云服务器ECS自定义创建流程
阿里云ECS是安全可靠、弹性灵活的云计算服务,支持多种实例规格与操作系统,可快速创建和管理云服务器。本文详解ECS介绍、购买流程及使用教程,涵盖配置选择、网络设置、安全组规则等,助您轻松上手。
360 16
|
1月前
|
存储 弹性计算 网络协议
超详细的阿里云服务器购买流程,ECS自定义购买配置教程
本文详细图解阿里云ECS服务器自定义购买全流程,涵盖付费模式、地域选择、网络配置、实例规格、镜像、存储、安全组及登录设置等核心步骤,助您轻松掌握专业级云服务器搭建方法。
|
1月前
|
存储 弹性计算 网络协议
阿里云服务器ECS是什么?ECS介绍、云服务器创建及使用教程
阿里云ECS是安全可靠、弹性灵活的云计算服务,支持多种实例规格与操作系统,可快速创建和管理云服务器。本文详解ECS介绍、购买流程(含付费模式、地域、网络、存储等设置)及使用教程,助您轻松上手云服务器。
482 4
|
1月前
|
存储 安全 Java
《数据之美》:Java集合框架全景解析
Java集合框架是数据管理的核心工具,涵盖List、Set、Map等体系,提供丰富接口与实现类,支持高效的数据操作与算法处理。
|
1月前
|
消息中间件 缓存 Java
Spring框架优化:提高Java应用的性能与适应性
以上方法均旨在综合考虑Java Spring 应该程序设计原则, 数据库交互, 编码实践和系统架构布局等多角度因素, 旨在达到高效稳定运转目标同时也易于未来扩展.
121 8
|
1月前
|
存储 弹性计算 Linux
阿里云账号注册、领取优惠券、试用云服务器和购买云服务器教程参考
对于还未使用阿里云任何云产品的用户来说,完整的上云流程包括账号注册并完成实名认证,然后才是试用或者购买云服务器,有的新手用户对这一流程还不是很清楚。本文为大家介绍新手用户从注册阿里云账号,完成实名认证,然后领取阿里云优惠券,并试用云服务器和购买云服务器的全部流程,适合初次购买和试用阿里云服务器的新手用户参考。
|
1月前
|
存储 算法 安全
Java集合框架:理解类型多样性与限制
总之,在 Java 题材中正确地应对多样化与约束条件要求开发人员深入理解面向对象原则、范式编程思想以及JVM工作机理等核心知识点。通过精心设计与周密规划能够有效地利用 Java 高级特征打造出既健壮又灵活易维护系统软件产品。
76 7

热门文章

最新文章

推荐镜像

更多
  • DNS
  • 下一篇
    oss云网关配置