利用Java實(shí)現(xiàn)輕松解析DNS報(bào)文
前言
最近在做網(wǎng)絡(luò)包分析的一個(gè)功能,其中有一點(diǎn)就是要判斷抓包期間應(yīng)用是否有存在異常的DNS解析行為,并且要寫在一個(gè)使用Java語(yǔ)言的后端服務(wù)中,由于之前使用Go語(yǔ)言做過(guò)類似的事情,所以我第一反應(yīng)就是這個(gè)應(yīng)該實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單,但是實(shí)際上手后,發(fā)現(xiàn)在Java中要拿到DNS報(bào)文還沒(méi)那么容易,當(dāng)然也有可能是我沒(méi)有找到正確的工具,如果有方便的工具,歡迎在評(píng)論區(qū)留言。那么本文就結(jié)合我如何從網(wǎng)絡(luò)包中拿到DNS報(bào)文,進(jìn)行一個(gè)簡(jiǎn)單介紹。
一. DNS報(bào)文格式簡(jiǎn)析
DNS報(bào)文本質(zhì)是UDP報(bào)文,UDP報(bào)文的格式如下所示。

而DNS相關(guān)內(nèi)容,就體現(xiàn)在UDP報(bào)文載荷中。DNS報(bào)文分為DNS請(qǐng)求報(bào)文和DNS響應(yīng)報(bào)文,兩種報(bào)文結(jié)構(gòu)一樣,只是內(nèi)容稍有差別,如下是DNS報(bào)文的格式。

DNS報(bào)文內(nèi)容包含如下三個(gè)部分。
第一部分是基礎(chǔ)結(jié)構(gòu)部分,解釋如下。
- Transaction ID。DNS報(bào)文的事務(wù)ID標(biāo)識(shí),DNS請(qǐng)求報(bào)文和DNS響應(yīng)報(bào)文的Transaction ID是一樣的,故可以通過(guò)這個(gè)ID關(guān)聯(lián)DNS請(qǐng)求和響應(yīng)報(bào)文;
- Flags。DNS報(bào)文的標(biāo)志,標(biāo)志由若干個(gè)含義不同的字段組成,較為常用的是第0位可以表示當(dāng)前是請(qǐng)求DNS報(bào)文還是響應(yīng)DNS報(bào)文,第12-15位可以表示響應(yīng)DNS報(bào)文的Reply Code;
- Questions。問(wèn)題數(shù),表示后面Queries的數(shù)量;
- Answer RRs。回答資源記錄數(shù),表示后面Answers的數(shù)量;
- Authority RRs。授權(quán)資源記錄數(shù),表示后面Authoritative nameservers的數(shù)量;
- Additional RRs。附加資源記錄數(shù),表示后面Additional records的數(shù)量。
第二部分是問(wèn)題部分,對(duì)應(yīng)Queries,該部分表示DNS查詢請(qǐng)求的問(wèn)題信息,包含查詢的域名Name,查詢的類型Type和查詢的類Class,解釋如下。
Name。就是請(qǐng)求DNS解析的域名地址,這里的Name是一個(gè)不定長(zhǎng)的字段,格式示意如下。

Type。表示DNS查詢的資源類型,Name字段結(jié)束后的兩個(gè)字節(jié)就是Type,通常關(guān)注較多的有0x0001,助記符是A,表示通過(guò)域名查詢IPv4地址,以及0x001C,助記符是AAAA,表示通過(guò)域名查詢IPv6地址;
Class。表示地址類型,通常為0x0001,表示互聯(lián)網(wǎng)地址。
第三部分是資源記錄部分,解釋如下。
- Answers。記錄域名解析出來(lái)的地址信息;
- Authoritative nameservers。記錄解析該域名對(duì)應(yīng)的權(quán)威名稱服務(wù)器信息;
- Additional records。記錄解析該域名對(duì)應(yīng)的一些附加信息。
二. DNS報(bào)文解析實(shí)現(xiàn)
先回顧一下需求,就是需要得到應(yīng)用是否存在異常的DNS解析,那么結(jié)合上面的DNS報(bào)文格式,我們的判斷邏輯可以像下面這樣。
- 先找到只有請(qǐng)求DNS報(bào)文但沒(méi)有響應(yīng)DNS報(bào)文的DNS解析,這是一種異常的DNS解析情況,即DNS服務(wù)器沒(méi)有響應(yīng)解析請(qǐng)求;
- 拿到響應(yīng)DNS報(bào)文的Reply Code,然后根據(jù)Reply Code得到異常的DNS解析情況,Reply Code的枚舉如下所示。
| Reply Code | 說(shuō)明 |
|---|---|
| 0 | 正常 |
| 1 | 報(bào)文格式錯(cuò)誤 |
| 2 | 域名服務(wù)器異常 |
| 3 | 域名不存在 |
| 4 | 解析類型Type不支持 |
| 5 | 域名服務(wù)器拒絕請(qǐng)求 |
計(jì)算DNS解析的請(qǐng)求和響應(yīng)報(bào)文的時(shí)間差,得到解析耗時(shí)過(guò)長(zhǎng)的異常情況。
那么其實(shí)我們需要的DNS報(bào)文的內(nèi)容就很明確了,如下所示。
1. Transaction ID,事務(wù)ID;
2. Reply Code,響應(yīng)碼;
3. Name,域名;
4. Type,解析類型。
現(xiàn)在就開(kāi)始本文的正題,如何使用Java語(yǔ)言來(lái)解析網(wǎng)絡(luò)包并得到DNS報(bào)文。在Go語(yǔ)言中,可以使用google/gopacket來(lái)方便的拿到DNS報(bào)文,但是在Java中,要一步拿到DNS報(bào)文尚有點(diǎn)困難,但是可以基于如下步驟來(lái)操作。
- 基于io.pkts的工具包來(lái)解析網(wǎng)絡(luò)包并拿到UDP報(bào)文;
- 過(guò)濾出源或目標(biāo)端口號(hào)為53的UDP報(bào)文,這是因?yàn)橥ǔ?strong>DNS服務(wù)器會(huì)工作在53號(hào)端口上;
- 拿到過(guò)濾后的UDP報(bào)文的載荷,按照DNS報(bào)文的格式解析得到Transaction ID,Reply Code,Name和Type。
現(xiàn)在進(jìn)行實(shí)操。先引入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>
然后基于Spring的MultipartFile來(lái)上傳網(wǎng)絡(luò)包并使用io.pkts工具解析出UDP報(bào)文,實(shí)現(xiàn)如下所示。
@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();
// 在這里根據(jù)DNS報(bào)文格式解析數(shù)據(jù)
......
}
}
return true;
});
}
}
}
解析Transaction ID的邏輯如下所示。
private String parseTransactionId(byte[] array) {
// 前兩字節(jié)是Transaction ID,表示會(huì)話標(biāo)識(shí)
// DNS請(qǐng)求報(bào)文和響應(yīng)報(bào)文通過(guò)Transaction ID進(jìn)行匹配
return HexUtils.toHexString(Arrays.copyOfRange(array, 0, 2));
}
解析請(qǐng)求或響應(yīng)報(bào)文類型的邏輯如下所示。
private int parseDnsPackageType(byte[] array) {
// 第3和第4字節(jié)是Flags,表示標(biāo)志
// Flags的第0位代表請(qǐng)求或響應(yīng)的類型
// 0表示DNS請(qǐng)求報(bào)文,1表示DNS響應(yīng)報(bào)文
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字節(jié)是Flags,表示標(biāo)志
// 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字節(jié)開(kāi)始,是域名,域名以0x00結(jié)尾
// 域名結(jié)束后的兩個(gè)字節(jié)就代表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字節(jié)開(kāi)始,遵循[域名長(zhǎng)度][域名][域名長(zhǎng)度][域名]...0x00的規(guī)律
// 故按照上述規(guī)律,從第13字節(jié)開(kāi)始,將域名的所有組成部分獲取出來(lái)并拼接
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報(bào)文的數(shù)據(jù),我們就拿到了,后續(xù)就是將這些數(shù)據(jù)組裝為一個(gè)Entity來(lái)方便我們?cè)诔绦蛑惺褂煤吞幚恚@里就不再演示了。
總結(jié)
我使用Java語(yǔ)言從網(wǎng)絡(luò)包中解析出DNS報(bào)文的步驟總結(jié)如下。
- 使用io.pkts解開(kāi)網(wǎng)絡(luò)包并過(guò)濾得到UDP報(bào)文;
- 過(guò)濾出源或目標(biāo)端口號(hào)為53的UDP報(bào)文;
- 拿到過(guò)濾后的UDP報(bào)文的載荷,按照DNS報(bào)文的格式解析得到想要的DNS數(shù)據(jù)。
以上就是利用Java實(shí)現(xiàn)輕松解析DNS報(bào)文的詳細(xì)內(nèi)容,更多關(guān)于Java解析DNS報(bào)文的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java通過(guò)exchange協(xié)議發(fā)送郵件
這篇文章主要為大家詳細(xì)介紹了Java通過(guò)exchange協(xié)議發(fā)送郵件,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-02-02
springboot配置flyway(入門級(jí)別教程)
本文介紹了springboot配置flyway,主要介紹基于SpringBoot集成flyway來(lái)管理數(shù)據(jù)庫(kù)的變更,具有一定的參考價(jià)值,感興趣的可以了解一下2023-09-09
SpringBoot項(xiàng)目POM文件的使用小結(jié)
本文主要詳細(xì)介紹了Maven中SpringBoot項(xiàng)目的POM文件配置,包括項(xiàng)目的依賴和插件,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-11-11
java向微信服務(wù)號(hào)發(fā)送消息的完整步驟實(shí)例
這篇文章主要介紹了java向微信服務(wù)號(hào)發(fā)送消息的相關(guān)資料,包括申請(qǐng)測(cè)試號(hào)獲取appID/appsecret、關(guān)注公眾號(hào)獲取openID、配置消息模板及代碼實(shí)現(xiàn),需要的朋友可以參考下2025-06-06
Spring?Cloud?OAuth2實(shí)現(xiàn)自定義token返回格式
Spring?Security?OAuth的token返回格式都是默認(rèn)的,但是往往這個(gè)格式是不適配系統(tǒng)。本文將用一個(gè)接口優(yōu)雅的實(shí)現(xiàn)?Spring?Cloud?OAuth2?自定義token返回格式,需要的可以參考一下2022-06-06
SpringBoot下無(wú)節(jié)制和數(shù)據(jù)庫(kù)建立連接的問(wèn)題及解決方法
本文介紹了無(wú)節(jié)制建立MySQL連接的危害,包括數(shù)據(jù)庫(kù)服務(wù)端資源耗盡、應(yīng)用端性能劣化和監(jiān)控與運(yùn)維困境,提出了系統(tǒng)性解決方案,包括連接池標(biāo)準(zhǔn)化配置、代碼規(guī)范與防御式編程、全鏈路監(jiān)控體系和架構(gòu)級(jí)優(yōu)化,感興趣的朋友一起看看吧2025-03-03
Java中的==、equals與hashCode區(qū)別與聯(lián)系最佳實(shí)踐
在Java開(kāi)發(fā)中,==、equals和hashCode是三個(gè)高頻出現(xiàn)的概念,也是初學(xué)者最容易混淆的知識(shí)點(diǎn),本文將從底層原理出發(fā),全面解析三者的區(qū)別、聯(lián)系及最佳實(shí)踐,幫你徹底理清它們的使用場(chǎng)景,感興趣的朋友一起看看吧2025-09-09
SpringBoot項(xiàng)目上高并發(fā)問(wèn)題的解決方案
本章演示在springboot項(xiàng)目中的高并發(fā)demo,演示導(dǎo)致的問(wèn)題,以及單機(jī)部署下的解決方案和集群部署下的解決方式以及分布式下的解決方案,文中通過(guò)圖文結(jié)合的方式講解的非常詳細(xì),需要的朋友可以參考下2024-06-06

