利用Java实现轻松解析DNS报文

 更新时间:2023年11月10日 08:25:48   作者:半夏之沫  
这篇文章主要为大家详细介绍了如何利用Java实现轻松解析DNS报文,文中的示例代码讲解详细,具有一定的学习价值,感兴趣的小伙伴可以跟随小编一起了解一下

前言

最近在做网络包分析的一个功能,其中有一点就是要判断抓包期间应用是否有存在异常的DNS解析行为,并且要写在一个使用Java语言的后端服务中,由于之前使用Go语言做过类似的事情,所以我第一反应就是这个应该实现起来比较简单,但是实际上手后,发现在Java中要拿到DNS报文还没那么容易,当然也有可能是我没有找到正确的工具,如果有方便的工具,欢迎在评论区留言。那么本文就结合我如何从网络包中拿到DNS报文,进行一个简单介绍。

一. DNS报文格式简析

DNS报文本质是UDP报文,UDP报文的格式如下所示。

DNS相关内容,就体现在UDP报文载荷中。DNS报文分为DNS请求报文和DNS响应报文,两种报文结构一样,只是内容稍有差别,如下是DNS报文的格式。

DNS报文内容包含如下三个部分。

第一部分是基础结构部分,解释如下。

  • Transaction IDDNS报文的事务ID标识,DNS请求报文和DNS响应报文的Transaction ID是一样的,故可以通过这个ID关联DNS请求和响应报文;
  • FlagsDNS报文的标志,标志由若干个含义不同的字段组成,较为常用的是第0位可以表示当前是请求DNS报文还是响应DNS报文,第12-15位可以表示响应DNS报文的Reply Code
  • Questions。问题数,表示后面Queries的数量;
  • Answer RRs。回答资源记录数,表示后面Answers的数量;
  • Authority RRs。授权资源记录数,表示后面Authoritative nameservers的数量;
  • Additional RRs。附加资源记录数,表示后面Additional records的数量。

第二部分是问题部分,对应Queries,该部分表示DNS查询请求的问题信息,包含查询的域名Name,查询的类型Type和查询的类Class,解释如下。

Name。就是请求DNS解析的域名地址,这里的Name是一个不定长的字段,格式示意如下。

Type。表示DNS查询的资源类型,Name字段结束后的两个字节就是Type,通常关注较多的有0x0001,助记符是A,表示通过域名查询IPv4地址,以及0x001C,助记符是AAAA,表示通过域名查询IPv6地址;

Class。表示地址类型,通常为0x0001,表示互联网地址。

第三部分是资源记录部分,解释如下。

  • Answers。记录域名解析出来的地址信息;
  • Authoritative nameservers。记录解析该域名对应的权威名称服务器信息;
  • Additional records。记录解析该域名对应的一些附加信息。

二. DNS报文解析实现

先回顾一下需求,就是需要得到应用是否存在异常的DNS解析,那么结合上面的DNS报文格式,我们的判断逻辑可以像下面这样。

  • 先找到只有请求DNS报文但没有响应DNS报文的DNS解析,这是一种异常的DNS解析情况,即DNS服务器没有响应解析请求;
  • 拿到响应DNS报文的Reply Code,然后根据Reply Code得到异常的DNS解析情况,Reply Code的枚举如下所示。
Reply Code说明
0正常
1报文格式错误
2域名服务器异常
3域名不存在
4解析类型Type不支持
5域名服务器拒绝请求

计算DNS解析的请求和响应报文的时间差,得到解析耗时过长的异常情况。

那么其实我们需要的DNS报文的内容就很明确了,如下所示。

1. Transaction ID,事务ID

2. Reply Code,响应码;

3. Name,域名;

4. Type,解析类型。

现在就开始本文的正题,如何使用Java语言来解析网络包并得到DNS报文。在Go语言中,可以使用google/gopacket来方便的拿到DNS报文,但是在Java中,要一步拿到DNS报文尚有点困难,但是可以基于如下步骤来操作。

  • 基于io.pkts的工具包来解析网络包并拿到UDP报文;
  • 过滤出源或目标端口号为53的UDP报文,这是因为通常DNS服务器会工作在53号端口上;
  • 拿到过滤后的UDP报文的载荷,按照DNS报文的格式解析得到Transaction IDReply CodeNameType

现在进行实操。先引入io.pkts的依赖,如下所示。

<dependency>  
    <groupId>io.pkts</groupId>  
    <artifactId>pkts-streams</artifactId>  
    <version>3.0.10</version>  
</dependency>  
<dependency>  
    <groupId>io.pkts</groupId>  
    <artifactId>pkts-core</artifactId>  
    <version>3.0.10</version>  
</dependency>  

然后基于SpringMultipartFile来上传网络包并使用io.pkts工具解析出UDP报文,实现如下所示。

@RestController
public class FileUpload {

    @PostMapping("/upload/udp")
    public void uploadUdp(MultipartFile uploadFile) throws IOException {
        try (InputStream inputStream = uploadFile.getInputStream()) {
            GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream);
            Pcap pcap = Pcap.openStream(gzipInputStream);
            pcap.loop(packet -> {
                if (packet.hasProtocol(Protocol.UDP)) {
                    UDPPacket udpPacket = (UDPPacket) packet.getPacket(Protocol.UDP);
                    if (udpPacket.getSourcePort() == 53
                            || udpPacket.getDestinationPort() == 53) {
                        final byte[] payloadByteArray = udpPacket.getPayload().getArray();
                        // 在这里根据DNS报文格式解析数据
                        ......
                    }
                }
                return true;
            });
        }
    }

}

解析Transaction ID的逻辑如下所示。

private String parseTransactionId(byte[] array) {
    // 前两字节是Transaction ID,表示会话标识
    // DNS请求报文和响应报文通过Transaction ID进行匹配
    return HexUtils.toHexString(Arrays.copyOfRange(array, 0, 2));
}

解析请求或响应报文类型的逻辑如下所示。

private int parseDnsPackageType(byte[] array) {
    // 第3和第4字节是Flags,表示标志
    // Flags的第0位代表请求或响应的类型
    // 0表示DNS请求报文,1表示DNS响应报文
    String binaryString = String.format("%08d", Integer.parseInt(Integer.toBinaryString(array[2] & 0xFF)));
    return Integer.parseInt(binaryString.substring(0, 1), 2);
}

解析Reply Code的逻辑如下所示。

private int parseDnsRcode(byte[] array) {
    // 第3和第4字节是Flags,表示标志
    // Flags的最后四位表示Reply Code
    String binaryString = String.format("%08d", Integer.parseInt(Integer.toBinaryString(array[3] & 0xFF)));
    return Integer.parseInt(binaryString.substring(4, 8), 2);
}

解析Type的逻辑如下所示。

private int parseDnsQueryType(byte[] array) {
    // 第13字节开始,是域名,域名以0x00结尾
    // 域名结束后的两个字节就代表DNS查询类型
    int domainEndIndex = -1;
    for (int i = 12; i < array.length; i++) {
        if ((array[i] & 0xFF) == 0) {
            domainEndIndex = i;
            break;
        }
    }
    String s = HexUtils.toHexString(Arrays.copyOfRange(array, domainEndIndex + 1, domainEndIndex + 3));
    return Integer.parseInt(s, 16);
}

解析Name的逻辑如下所示。

private String parseDnsQueryDomain(byte[] array) {
    // 从第13字节开始,遵循[域名长度][域名][域名长度][域名]...0x00的规律
    // 故按照上述规律,从第13字节开始,将域名的所有组成部分获取出来并拼接
    List<String> domainParts = new ArrayList<>();
    int lengthIndex = 12;
    do {
        int partLength = array[lengthIndex] & 0xFF;
        String s = new String(Arrays.copyOfRange(array, lengthIndex + 1, lengthIndex + partLength + 2)).trim();
        domainParts.add(s);
        lengthIndex = lengthIndex + partLength + 1;
    } while ((array[lengthIndex] & 0xFF) != 0);
    return String.join(".", domainParts);
}

那么至此我们期望得到的DNS报文的数据,我们就拿到了,后续就是将这些数据组装为一个Entity来方便我们在程序中使用和处理,这里就不再演示了。

总结

我使用Java语言从网络包中解析出DNS报文的步骤总结如下。

  • 使用io.pkts解开网络包并过滤得到UDP报文;
  • 过滤出源或目标端口号为53的UDP报文;
  • 拿到过滤后的UDP报文的载荷,按照DNS报文的格式解析得到想要的DNS数据。

以上就是利用Java实现轻松解析DNS报文的详细内容,更多关于Java解析DNS报文的资料请关注脚本之家其它相关文章!

相关文章

  • Java通过exchange协议发送邮件

    Java通过exchange协议发送邮件

    这篇文章主要为大家详细介绍了Java通过exchange协议发送邮件,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-02-02
  • springboot配置flyway(入门级别教程)

    springboot配置flyway(入门级别教程)

    本文介绍了springboot配置flyway,主要介绍基于SpringBoot集成flyway来管理数据库的变更,具有一定的参考价值,感兴趣的可以了解一下
    2023-09-09
  • SpringBoot项目POM文件的使用小结

    SpringBoot项目POM文件的使用小结

    本文主要详细介绍了Maven中SpringBoot项目的POM文件配置,包括项目的依赖和插件,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-11-11
  • java向微信服务号发送消息的完整步骤实例

    java向微信服务号发送消息的完整步骤实例

    这篇文章主要介绍了java向微信服务号发送消息的相关资料,包括申请测试号获取appID/appsecret、关注公众号获取openID、配置消息模板及代码实现,需要的朋友可以参考下
    2025-06-06
  • Spring Cloud OAuth2实现自定义token返回格式

    Spring Cloud OAuth2实现自定义token返回格式

    Spring Security OAuth的token返回格式都是默认的,但是往往这个格式是不适配系统。本文将用一个接口优雅的实现 Spring Cloud OAuth2 自定义token返回格式,需要的可以参考一下
    2022-06-06
  • SpringBoot下无节制和数据库建立连接的问题及解决方法

    SpringBoot下无节制和数据库建立连接的问题及解决方法

    本文介绍了无节制建立MySQL连接的危害,包括数据库服务端资源耗尽、应用端性能劣化和监控与运维困境,提出了系统性解决方案,包括连接池标准化配置、代码规范与防御式编程、全链路监控体系和架构级优化,感兴趣的朋友一起看看吧
    2025-03-03
  • Java中的==、equals与hashCode区别与联系最佳实践

    Java中的==、equals与hashCode区别与联系最佳实践

    在Java开发中,==、equals和hashCode是三个高频出现的概念,也是初学者最容易混淆的知识点,本文将从底层原理出发,全面解析三者的区别、联系及最佳实践,帮你彻底理清它们的使用场景,感兴趣的朋友一起看看吧
    2025-09-09
  • 一篇文章带你入门Java之编程规范

    一篇文章带你入门Java之编程规范

    这篇文章主要介绍了如何养成良好java代码编码规范,规范需要平时编码过程中注意,是一个慢慢养成的好习惯,下面小编就带大家来一起详细了解一下吧
    2021-08-08
  • Java文件与IO流操作原理详细分析

    Java文件与IO流操作原理详细分析

    在java中提供有对于文件操作系统的支持,这个支持在java.io.File类中进行了定义,也就是说在整个java.io包中File类是唯一一个与文件本身操作有关的类(创建,删除,重命名)有关的类,而如果想要进行File类的操作,我们需要提供有完整的路径支持,而后可以调用相应的方法进行处理
    2022-09-09
  • SpringBoot项目上高并发问题的解决方案

    SpringBoot项目上高并发问题的解决方案

    本章演示在springboot项目中的高并发demo,演示导致的问题,以及单机部署下的解决方案和集群部署下的解决方式以及分布式下的解决方案,文中通过图文结合的方式讲解的非常详细,需要的朋友可以参考下
    2024-06-06

最新评论