Android开发 RFC 2136 DNS动态更新协议

        功能背景:通过服务器的DHCP、DNS服务器直接管理所有客户端IP信息。

        在Windows Server 已有的机制下,DHCP是会默认 向DNS 发送 主机+域名后缀的域名信息的,但是可能存在记录延迟、丢失等问题,所以希望通过“DNS动态更新”协议,客户端直接向DNS服务器发送域名信息,尽可能保证数据完整。

        下面会从“服务器开启功能的配置”"客户端协议包内容解析"两个方面来总结学习这个的过程中我是如何实现的。

目录

一. 服务器开启功能的配置

1. 开启对应的角色功能: “DHCP 服务器”、”DNS服务器“、“Active Directory 域服务(AD DS)”

2. DHCP服务器向DNS服务器自动更新的方式。

3. 开启AD DS,客户端向DNS服务器动态更新域名

4. 通过Windows 10 测试 “DNS动态更新域名” 功能是否可用

二. 客户端协议包内容解析

1. 解析Windows客户端向DNS服务器发送的报文信息

1.1 查询报文

1.2 DNS动态更新报文


一. 服务器开启功能的配置

        以现场环境以Windows Server 2019为例,其他版本应该也大同小异,目的是打开服务器的”DNS服务器动态更新域名的功能“。

 官方说明文档: 如何在 Windows Server 中配置 DNS 动态更新 - Windows Server | Microsoft Docs

1. 开启对应的角色功能: “DHCP 服务器”、”DNS服务器“、“Active Directory 域服务(AD DS)

 

2. DHCP服务器向DNS服务器自动更新的方式。

        如果不需要"DNS动态更新功能",由DHCP服务器向DNS服务器更新域名信息,则开启 “DHCP 服务器”、”DNS服务器“即可,在DHCP、DNS中配置对应的配置即可。

        DNS新建一个区域为: 父域后缀为:jayce1220.com 动态更新选为:非安全

 

        DNCP也需要新建一个域进行配置:DNS服务器地址需要填写服务器IP地址,DNS域名即为刚刚设置的域名。

         这样配置好了之后,有客户端接入进来的时候,如果是通过这个DHCP服务器获取的IP地址就会生成一条记录,并且会在一段时间后同步到DNS服务器上(可能会延迟)。

 

3. 开启AD DS,客户端向DNS服务器动态更新域名

        因为之前角色已经添加了“Active Directory 域服务(AD DS)”功能,如图所示,点击图标:

 

        然后"添加新林",按提示下一步至安装即可,安装完成之后会提示重启计算机。

 

        重启之后在"属性"中可以看见,计算机已经在创建的域名下了,表示添加域成功。

 

        打开"DNS管理器",可以把刚刚创建的DNS服务器的域添加到 "Active Directory集成区域"中了。

 

        最后保存确认即可,"DNS动态更新域名"服务端就搭建完毕了。

   

4. 通过Windows 10 测试 “DNS动态更新域名” 功能是否可用

        这个会直接绕开DHCP服务器,设置windows的网络适配器网络参数配置 - IP获取方式采用静态设置的方式 - 设置DNS服务器地址(指向刚刚的DNS服务器地址) - 高级

 

        配置里面的DNS栏目,然后确定保存所有配置。

 

        再重启一下网络,使客户端重新加载网络,再回到服务端的DNS服务器,即可看见添加了一条DNS域名解析记录(抓取网络数据包可以看到这条信息,下面再搭配RFC协议分析网络数据包内容),以此可以验证"DNS动态更新域名"功能已打开。

 

二. 客户端协议包内容解析

        Windown doc可知 动态更新协议是基于RFC 2136 协议规范通信的,而 RFC 2136 报文内容 又是由 RFC 1035协议规范统一说明的,这里主要是通过RFC 1035/2136 协议,结合Windows客户端向DNS服务器抓包的报文信息来进行分析。这里需要注意的是,DNS的通信协议 udp和tcp的端口都是: 53

 

1. 解析Windows客户端向DNS服务器发送的报文信息

        在Windows客户端上配置完DNS等网络信息后,重启网络状态,可以在DNS的服务器上 获取到两条来自Windows客户端的发包信息。一条是查询信息query,一条是更新信息Dynamic update。

        根据更新报文,搜索出来的大部分中文资料都是DNS查询报文的分析资料,这里以这两位大佬的博客为引:

链接1. DNS报文格式解析(非常详细)

链接2. DNS 中的协议字段详细定义

1.1 查询报文

        先分析查询的报文结构:

        完全符合协议报文格式:

        其中我遇到较为难理解的是 "标志(Flags)""查询问题区域(Queries)"

        "标志(Flags)" 是由二进制 16Bit 转换成16进制表示。 说明及用Java代码封装说明如下:

/**
 * 头部flags标识结构 16bit位=2字节表示
 * 0 : qr 查询标识=1bit
 * 1-4 : opcode  0=Query 1=IQuery(Inverse Query) 2=Status 3=Unsassined 4=Notify 5=Update
 * 5 : AC (authoritative answer) 授权回答=1bit
 * 6 : TC (Truncated):表示是否被截断。值为 1 时,表示响应已超过 512 字节并已被截断,只返回前     512 个字节。=1bit
 * 7 : RD (Recursion Desired):期望递归 =1bit
 * 8 : RA (Recursion Available):可用递归=1bit
 * 9-11 : Z 保留字段,在所有的请求和应答报文中,它的值必须为 0。=3bit
 * 12-15 rcode =4bit
 * 总共 16bit --结构如下:
 * 0 | 1-4 | 5 | 6 | 7 | 8 | 9-11 | 12-15 |
 *
 * @param opcode BUILD_TYPE_**** 定义的类型
 * @return 2个字节的flage
 */
private static byte[] getDnsFlags(int opcode) {
    //缓存 16 bit的数据
    byte[] bits = new byte[16];
    switch (opcode) {
        case BUILD_OPCODE_QUERY:
            bits[7] = 1;
            break;
        case BUILD_OPCODE_UPDATA:
            bits[4] = 5;//opcode=5=Update
            break;
    }
    //最后转换成字节数据
    int temp = 0;
    for (int i = 0; i < bits.length; i++) {
        temp |= bits[i] << (bits.length - 1 - i);
    }
    return DataUtils.toBigEndian((short) temp);
}

        "查询问题区域(Queries)" 由 域名(ascii码16进制)+Type+Class 组成。type 和 Class 在RFC 1035协议中定义,链接2 的博客也有列出了这部分内容。

 

        域名规则如上图所示以“DESKTOP-4686QLT.jayce777.com”为例,因为每一域名级别最长仅支持63个字符,域名总长度则不能超过253个字符,所以按域名级别划分 转换ascii后,会在前面每一域名级别前面添加一个字节表示域名级别长度(红色、橙色、黄色),末尾用00表示域名地址结束。例如0x0f=15,4445534b544f502d34363836514c54=DESKTOP-4686QLT...

        使用Java代码封装如下:

/**
 * 获取域名地址按照规则转换的字节信息:
 * 1.字符串以"."分割,获得一个数组,数组的每个字符串(域名等级名)元素转换成**编码格式字节。
 * 2.每个元素的的字节数组在前面加上一个表示长度的字节(顶级域名长度理论长度为63个字节)。
 * 3.转换之后将各个元素组合起来,末尾增加一个"0"字节的表示介绍。
 * 以二级域名地址("DESKTOP-4686QLT.jayce777.com")举例,装换为16进制,结构如下:
 * 0f4445534b544f502d34363836514c54086a6179636537373703636f6d00 分割一下--->
 * 0f 4445534b544f502d34363836514c54 08 6a61796365373737 03 636f6d 00
 * 0f 08 03表示长度,末尾00表示结束
 *
 * @param address 域名地址
 * @return
 */
private static byte[] dnsAddress2DnsMessage(String address) {
    byte[] bytes = new byte[0];
    String[] split = address.split("\\.");
    for (String part : split) {
        byte[] partBytes = part.getBytes();
        byte[] bytesCache = concatBytes(new byte[]{(byte) partBytes.length}, partBytes);
        bytes = concatBytes(bytes, bytesCache);
    }
    bytes = concatBytes(bytes, new byte[]{0});//结尾需要00标识
    return bytes;
}

        至此通过抓包获取的报文,结合协议文档及链接的博客文献基本可以封装出查询的报文数据,没有特殊的查询条件,关键点也仅在于 "标志(Flags)"中的qr 和 opcode参数,以及 "查询问题区域(Queries)"域名报文生成规则、Type和Class参数。规则长度为 id(2)+flags(2)+4个计数器(8)+查询问题(域名规则生成(n)+type(2)+class(2)=n+16),使用Java封装了一个类,详见文末。

        顺带一提,返回的的参数是通过"标志(Flags)" 中的 Reply code,

        查询到的:

        查询不到的:

         

        本次需求不需要DNS的返回结果,所以这里不再赘述。

1.2 DNS动态更新报文

        通过查询报文的分析,大致了解个DNS协议各个报文空间的含义。再来分析DNS更新的报文内容。

  

        这里发现Opcode的值 变成了 5 = Update,记录计数器也发生了变化,但是还是用总共用8个字节表示4个记录计数器,12字节构成DNS报文首部。通过RFC 2136 协议文档也可以看见报文结构:

        针对Zone、Prerequisites、Upadate三个区域含义,在RFC 2136摘要中是这么描述的:

 

        结合后续的开发过程,这里我是这么理解的,"Zone"和“Prerequisites”是必须的,“Zone”指向这次更新的是哪个DNS服务器正向区域,“Prerequisites”直译即为"必要条件",这个在“update”中的压缩报文中也有重要意义。协议中也涉及类型是SOA类型,所以开发时报文的type参数和class参数,直接沿用了抓包报文里面的参数,没有过多的再去研究RFC 2136。需要注意的是这两个数据信息字节长度规则:

        Zone域名地方的方式沿用"查询问题区域(Queries)" 的域名规范,长度为:"域名(n)+type(2)+class(2)=n+4"

 

/**
 * Zone包构建
 *
 * @param dnsSuffix dns后缀地址
 * @param type      DNS资源记录
 * @param classes   dns后缀地址
 * @return
 */
private static byte[] getZone(String dnsSuffix, short type, short classes) {
    byte[] dnsBytes = dnsAddress2DnsMessage(dnsSuffix);
    byte[] typeBytes = DataUtils.toBigEndian(type);
    byte[] classesBytes = DataUtils.toBigEndian(classes);
    return concatBytes(dnsBytes, typeBytes, classesBytes);
}

        Prerequisites也基本和zone一样,但是添加了ttl和data length的信息。长度为:"域名(n)+type(2)+class(2)+ttl(4)+DL(2)+Data(DL)=n+DL+10"

/**
 * @param dnsAddress dns正向解析地址
 * @param type       Dns Type
 * @param classes    Dns classes
 * @param ttl        TTL时间,单位秒
 * @param dataLength 数据长度,一般为0
 * @return
 */
private static byte[] getPrerequisites(String dnsAddress, short type, short classes, int ttl, short dataLength) {
    byte[] dnsBytes = dnsAddress2DnsMessage(dnsAddress);
    byte[] typeBytes = DataUtils.toBigEndian(type);
    byte[] classesBytes = DataUtils.toBigEndian(classes);
    byte[] timeToLiveBytes = DataUtils.toBigEndian(ttl);
    byte[] dataLengthAndDataBytes = DataUtils.toBigEndian(dataLength);
    return concatBytes(dnsBytes, typeBytes, classesBytes, timeToLiveBytes, dataLengthAndDataBytes);
}

        Updates 里面3份数据(我这显示4是因为电脑配置了两个IP),测试是发现有最后一份带地址的数据的即可实现,因为没有完全理解RFC 2136这里还是和抓包报文保持一致,发送三份更新信息。(Updates 从文档上看是可以实现删除功能的,但是我这边暂时还没有做出来 =。=!)

        Update的数据格式结构基本也和Prerequisites一致,包含"域名+type+class+ttl+data length+data" 组成,但是这里的域名采用的是压缩域名的方式。压缩规则:从TransactionID,index=0开始计算至域名(Prerequisites区域)第一个字符的的长度,为偏移量,用以压缩报文大小。

        以上图为例,4be9为TransactionID 4b为偏移计算位置start=0,0f为偏移计算位置end,域名位置起始,差值为1f=31,c0为预留字段。报文及Java代码封装如下:

 
/**
 * @param offset    从TransactionID,index=0开始计算至域名第一个字符的的长度,为偏移量,用以压缩报文大小
 * @param type      Dns Type
 * @param classes   Dns classes
 * @param ttl       TTL时间,单位秒
 * @param ipAddress IP地址
 * @return
 */
private static byte[] getUpdata(byte offset, short type, short classes, int ttl, String ipAddress) {
    byte[] dnsBytes = {(byte) 0xc0, offset};//C0是预留字段
    byte[] typeBytes = DataUtils.toBigEndian(type);
    byte[] classesBytes = DataUtils.toBigEndian(classes);
    byte[] timeToLiveBytes = DataUtils.toBigEndian(ttl);
    byte[] dataLengthAndDataBytes = null;
    if (ipAddress != null && !"".equals(ipAddress) && ipAddress.contains(".")) {
        String[] split = ipAddress.split("\\.");
        int length = split.length;
        dataLengthAndDataBytes = new byte[2 + length];
        dataLengthAndDataBytes[0] = (byte) (length >>> 8 & 0xff);
        dataLengthAndDataBytes[1] = (byte) (length & 0xff);
        for (int i = 0; i < length; i++) {
            dataLengthAndDataBytes[2 + i] = (byte) (Integer.parseInt(split[i]) & 0xff);
        }
    } else {
        dataLengthAndDataBytes = new byte[]{00, 00};
    }

    return concatBytes(dnsBytes, typeBytes, classesBytes, timeToLiveBytes, dataLengthAndDataBytes);
}

        至此DNS动态报文:Header + Zone + Prerequisite + Update + Additional Data(没有为0) 均已构建出来,再使用生产报文发送到DNS服务器的53端口即可。

        相关文档及Java代码及源码见附件,可以jdk环境下运行直接测试。

Java代码封装RFC2136-DNS动态更新协议-Android文档类资源-CSDN下载

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值