dns

回家后发现家里的新拉的光纤是移动的,但是移动居然对53端口的udp实行完全拦截,也就是dns拦截。想起之前在宿舍使用的cloudflare的json格式的DoH还有DoT,所以决定搞一搞。

DoH

把RFC8484关于实现的那部分看了,并没有JSON API格式的,一查,原来还只是草案.
DoH(Dns over HTTPS),cloudflare(1.1.1.1)和Google(8.8.8.8)的dns都支持JSON格式的,虽说还是草案,但是国内好几个私人的DoH供应商都有提供,所以讲一下

JSON API

JSON格式的请求,草案里写了必须要有Qname(请求的查询的域名),Qtype(请求查询的类型,A,AAAA,CNAME等),Qclass(这个不知道是啥,CF和Google的实现都没有这个字段,所以无视就行了)

请求:

1
https://1.0.0.1/dns-query?ct=application/dns-json&name=baidu.com&type=A

响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"Status": 0,
"TC": false,
"RD": true,
"RA": true,
"AD": false,
"CD": false,
"Question": [
{
"name": "baidu.com",
"type": 1
}
],
"Answer": [
{
"name": "baidu.com",
"type": 1,
"TTL": 389,
"data": "39.156.69.79"
},
{
"name": "baidu.com",
"type": 1,
"TTL": 389,
"data": "220.181.38.148"
}
]
}

好像很简单

Wireformat

格式按照RFC1035,刚开始不知道,然后就开始按着文档手撸实现,越撸越不对劲,发现这就是一个普通的DNS的UDP包格式……

按照RFC8484,DoH需要支持GET和POST两种格式

GET请求,需要把DNS包进行base64转一下,去除结尾的=后发送,如

1
https://1.0.0.1/dns-query?dns=AAMBAAABAAAAAAAABWJhaWR1A2NvbQAAAQAB

POST请求,DNS包不需要任何操作,当成body直接POST就可以了

返回数据也是一个标准的UDP的DNS报文,直接返回就可以了

所以按照RFC8484,用python的话,一个简单的本地DoH转UDP格式的本地服务器就可以搭建起来了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# -*- encoding: utf-8 -*-
from socketserver import DatagramRequestHandler, ThreadingUDPServer
import requests
import base64

server_ip = "127.0.0.1"
server_port = 53
server_name = "github.com/SodiumAzide"
s = requests.session()
class Server(DatagramRequestHandler):
def handle(self):
data = self.rfile.read()

# GET 请求
# txt = base64.b64encode(data).decode().strip("=")
# rs = s.get("https://223.5.5.5/dns-query?dns=" + txt).content

# POST请求
rs = s.post("https://223.5.5.5/dns-query", data=data).content
self.wfile.write(rs)

if __name__ == '__main__':
server = ThreadingUDPServer((server_ip, server_port), Server)

try:
server.serve_forever()

except KeyboardInterrupt:
server.shutdown()

DoT

最近把之前缺的DoT的RFC也看了.根据RFC7858关于数据的部分,也是按照RFC1035的数据包.看来真的是dns over xxxx了,就是将之前UDP的dns通过https或tls进行传输.那就没啥好说了.
发送数据前,需要发送udp数据包的长度,编码为两字节大端格式

(All messages (requests and responses) in the established TLS session
MUST use the two-octet length field described in Section 4.2.2 of
[RFC1035]).

刚开始看漏了这几句,卡了好久.具体看代码吧.
PS:cloudflare的dns,是把长度+响应数据一起发送,阿里的dns是先发长度,然后再发相应包.RFC里面也没说是那种,所以发现只收到两个字节的,记得把后面那段也收了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# -*- encoding: utf-8 -*-
from socketserver import DatagramRequestHandler, ThreadingUDPServer
import requests
import base64
import socket
import ssl

server_ip = "127.0.0.1"
server_port = 53
server_name = "github.com/SodiumAzide"
s = requests.session()
ss = "one.one.one.one"
port = 853

class Server(DatagramRequestHandler):
def handle(self):
data = self.rfile.read()
context = ssl.create_default_context()
with socket.create_connection((ss, 853)) as sock:
with context.wrap_socket(sock, server_hostname=ss) as ssock:
dataLen = len(data)
data = dataLen.to_bytes(2, "big") + data
ssock.send(data)
rs = ssock.recv(1024)
self.wfile.write(rs[2:])

if __name__ == '__main__':
server = ThreadingUDPServer((server_ip, server_port), Server)

try:
server.serve_forever()

except KeyboardInterrupt:
server.shutdown()

参考文献

  1. JSON format to represent DNS data
  2. (RFC1035)DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION
  3. (RFC7858)Specification for DNS over Transport Layer Security (TLS)
  4. (RFC8484)DNS Queries over HTTPS (DoH)