Pierre's Blog

DNS sinkhole

/ Project

前言

DNS沉洞(DNS sinkhole)是一種特殊的DNS伺服器,對黑名單內的域名查詢給出無效結果(通常為NXDOMAIN或0.0.0.0),以此達到阻擋特定網域的目的,本文將以Typescript進行實作。

封包結構

DNS封包的結構可以分成五部分而這邊會用到的只有前三個區塊,分別為儲存查詢或回覆型態的Header(標頭區段)、儲存查詢內容的Question(問題區段)以及DNS回覆的Answer(答案區段)。

標頭區段

./src/interfaces/Flag.ts
1
2
3
4
5
6
7
8
9
interface Flag {
QR: boolean;
OPCode: number;
AA: boolean;
TC: boolean;
RD: boolean;
RA: boolean;
RCode: number;
};
./src/interfaces/DnsHeader.ts
1
2
3
4
5
6
7
8
interface DnsHeader {
ID: number;
Flag: Flag;
QuestionCount: number;
RRCount: number;
AuthRRCount: number;
AddRRCount: number;
};

對於DNS sinkhole來說解析DNS標頭主要有兩個值需要注意,回應時將RCODE設定為2(NXDOMAIN)以及由QDCOUNT判斷封包是否要進一步處理。

./src/modules/dnsParser.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const flag: Flag = {
QR: Boolean(msg.readUintBE(2, 2) >> 15),
OPCode: (msg.readUintBE(2, 2) >> 11) & ((1 << 4) - 1),
AA: Boolean((msg.readUintBE(2, 2) >> 10) & 1),
TC: Boolean((msg.readUintBE(2, 2) >> 9) & 1),
RD: Boolean((msg.readUintBE(2, 2) >> 8) & 1),
RA: Boolean((msg.readUintBE(2, 2) >> 7) & 1),
RCode: msg.readUintBE(2, 2) & ((1 << 5) - 1)
};

const header: DnsHeader = {
ID: msg.readUIntBE(0, 2),
Flag: flag,
QuestionCount: msg.readUintBE(4, 2),
RRCount: msg.readUintBE(6, 2),
AuthRRCount: msg.readUintBE(8, 2),
AddRRCount: msg.readUintBE(10, 2)
};

問題區段

./src/interfaces/DnsQuery.ts
1
2
3
4
5
interface DnsQuery {
Name: string;
Type: string;
Class: number;
};
./src/modules/dnsParser.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let domain = "";
let index = 12;
let subStringLength = msg.readUint8(index++);

while(subStringLength != 0) {
for(let i = 0; i < subStringLength; i++) {
domain = domain.padEnd(domain.length + 1, String.fromCharCode(msg.readUInt8(index++)));
}
subStringLength = msg.readUInt8(index++);
if(subStringLength != 0) {
domain = domain.padEnd(domain.length + 1, ".");
}
}

const query: DnsQuery = {
Name: domain,
Type: ResourceRecord[msg.readUIntBE(index, 2)],
Class: msg.readUIntBE(index + 2, 2)
};

黑/白名單&沉洞

在接收到DNS封包後,會先檢查Header,若Header中的問題數不為1,將會回傳Format error。

./src/modules/dnsHandler.ts
1
2
3
4
5
6
7
8
let domainIsSafe: boolean = true;
const Header = HeaderParser(msg);
let Query: DnsQuery;
if(Header.QuestionCount != 1) {
const errorResBuff = DnsSinker(msg, 1);

server.send(errorResBuff, rinfo.port, rinfo.address);
}

取得Query內的域名後,先對其與較短的白名單內的域名比對,若沒有符合的結果才會進行下一步黑名單的比對,以節省比對時間且較不會出現因為誤ban而導致部分網站服務無法使用的情形。

./src/modules/dnsHandler.ts
1
2
3
4
5
6
Query = QueryParser(msg);
if(!server.WhiteList.has(Query.Name)) {
if(server.BlackList.has(Query.Name)) {
domainIsSafe = false;
}
}

通過黑名單檢查的域名將直接回傳dns.google的請求結果,未通過者則回傳NXDOMAIN。

./src/modules/dnsHandler.ts
1
2
3
4
5
6
7
8
9
10
11
if(domainIsSafe) {
const dns = dgram.createSocket("udp4");
dns.on("message", (recvMsg, remoteInfo) => {
server.send(recvMsg, rinfo.port, rinfo.address);
});
dns.send(msg, 53, "8.8.8.8");
}
else {
const sinkMsg = DnsSinker(msg, 3);
server.send(sinkMsg, rinfo.port, rinfo.address);
}

nslookup的測試結果

Repo

pierre0210/dns-sinkhole

參考資料