看了2天RFC,终于让DNS支持edns-client-subnet协议,通过google dns resolver的请求,可以获取用户的ip地址。
国内很多CDN和DNS提供商都已经实现了,但网上的中文资料比较少,所以在这里分享一下,能力有限,错误之处还请谅解。
问题
- CDN使用DNS获取查询IP,根据IP对用户进行地域调度。但这里获取的IP地址是DNS地址,而不是用户真实的IP地址。
- 大多数情况下,我们假设用户通过会使用离自己网络最近的DNS resolver,CDN调度基本还是准确的。
- 但也有很多nameserver设置错误,或者用户使用google public dns(nameserver 8.8.8.8/8.8.4.4)或opendns进行DNS resolver
比如:
- 国内用户设置nameserver 8.8.8.8 (dig xxx.com @8.8.8.8)
- 我们得到的DNS query IP是74.125.16.208,判断IP属于美国,,,加利福尼亚州山景市谷歌公司
- 这个时候,我们的DNS会返回离美国加州最近的CDN节点IP给用户。
- 国内用户错误的调度到美国节点……
edns-client-subnet
- google提交了一份DNS扩展协议,允许DNS resolver传递用户的ip地址给authoritative DNS server.
- CDN的DNS支持该协议,就可以获取用户真实的IP地址,进行准确的调度。
- OpenDNS和Google Public DNS已经支持了该协议,如果希望他们的query中带有用户IP,需要联系他们添加白名单。提供nameserver的hostname、ip以及可以用来测试解析的域名即可,一般几天就可以搞定。(注:我是晚上22:l00提交的申请,第二天10:00就已经生效了)
实现
一. 支持发送和接收edns-client-subnet的dig
- 先下载bind,下载地址
- 下载edns-client-subnet dig patch,下载地址
下载上述2个包,将patch打进bind,编译出dig进行测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
.
/
dig
www
.
baidu
.
com
@
8.8.8.8
+
client
=
104.119.200.200
;
<<
>>
DiG
9.7.3
<<
>>
www
.
baidu
.
com
@
8.8.8.8
+
client
=
104.119.200.200
;
;
global
options
:
+
cmd
;
;
Got
answer
:
;
;
->
>
HEADER
<<
-
opcode
:
QUERY
,
status
:
NOERROR
,
id
:
1068
;
;
flags
:
qr
rd
ra
;
QUERY
:
1
,
ANSWER
:
3
,
AUTHORITY
:
0
,
ADDITIONAL
:
1
;
;
OPT
PSEUDOSECTION
:
;
EDNS
:
version
:
0
,
flags
:
;
udp
:
512
;
CLIENT
-
SUBNET
:
104.119.200.200
/
32
/
0
;
;
QUESTION
SECTION
:
;
www
.
baidu
.
com
.
IN
A
;
;
ANSWER
SECTION
:
www
.
baidu
.
com
.
1030
IN
CNAME
www
.
a
.
shifen
.
com
.
www
.
a
.
shifen
.
com
.
130
IN
A
220.181.112.143
www
.
a
.
shifen
.
com
.
130
IN
A
220.181.111.148
;
;
Query
time
:
42
msec
;
;
SERVER
:
8.8.8.8
#53(8.8.8.8)
;
;
WHEN
:
Wed
Jun
26
14
:
38
:
13
2013
;
;
MSG
SIZE
rcvd
:
113
|
注意上面的OPT PSEUDOSECTION
,已经可以发送和接收edns-client-subnet请求了
二. 协议
- DNS协议
- DNS query会包含header和RR 2部分,这里只介绍我们关注地方,网上可以搜到很多协议的介绍,比如这个http://archercai.blog.sohu.com/60779796.html
- header会描述本次请求中Questions、Answer RRs、Authority RRs和Additional RRs的数量,RR部分会详细描述每个资源的内容,所有的RR格式是相同的,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
1
1
1
1
1
1
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
|
|
/
/
/
NAME
/
|
|
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
|
TYPE
|
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
|
CLASS
|
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
|
TTL
|
|
|
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
|
RDLENGTH
|
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
|
/
RDATA
/
/
/
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
--
+
|
- 个人理解edns-client-subnet是对edns协议的扩展,附加在一个DNS请求的Additional RRs区域,这里
重点描述edns-client-subnet的结构
- EDNS协议 Extension mechanisms for DNS (EDNS0):http://tools.ietf.org/html/draft-ietf-dnsind-edns0-01
- EDNS0每个字段的结构和描述如下:
1
2
3
4
5
6
7
8
|
Field
Name
Field
Type
Description
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
--
NAME
domain
name
empty
(
root
domain
)
TYPE
u_int16_t
OPT
CLASS
u_int16_t
sender'
s
UDP
payload
size
TTL
u_int32_t
extended
RCODE
and
flags
RDLEN
u_int16_t
describes
RDATA
RDATA
octet
stream
{
attribute
,
value
}
pairs
|
- OPT 的值41,详细的协议值如下:
1
2
3
4
5
6
7
8
9
|
(
A
,
NS
,
MD
,
MF
,
CNAME
,
SOA
,
MB
,
MG
,
MR
,
NULL
,
WKS
,
PTR
,
HINFO
,
MINFO
,
MX
,
TXT
,
RP
,
AFSDB
)
=
range
(
1
,
19
)
AAAA
=
28
SRV
=
33
NAPTR
=
35
A6
=
38
DNAME
=
39
SPF
=
99
OPT
=
41
|
- RDLENGTH描述RDATAD的长度,edns-client-subnet的详细格式存在RDATA中,如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
+
0
(
MSB
)
+
1
(
LSB
)
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
0
:
|
OPTION
-
CODE
|
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
2
:
|
OPTION
-
LENGTH
|
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
4
:
|
FAMILY
|
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
6
:
|
SOURCE
NETMASK
|
SCOPE
NETMASK
|
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
7
:
|
ADDRESS
.
.
.
/
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
--
-
+
|
- OPTION-CODE: 2个字节
- OPTION-LENGTH: 2个字节,描述它之后的内容长度(BYTE)
- FAMILY: 2个字节,1表示ipv4, 2表示ipv6
- ADDRESS: 实际存放IP地址的地方,ipv4长度为4,google发送过来的长度一般为3,隐藏了ip地址最后一位
三. 开发
完成前2个步骤,就可以开搞了,逻辑很简单:
1. 判断dns query是否包含Additional RRs,读取NAME部分
2. 读取10个字节(byte),判断TYPE是否为41,rdlength > 8
3. 如果rdlength > 8,再读取8个字节,对应OPTION-CODE(2)–>OPTION-LENGTH(2)–>FAMILY(2)–>SOURCE NETMASK(1)–>SCOPE NETMASK(1)
4. 读取剩下的address,长度 rdlength – 8 或者 option-length – 4都行
注:读取到的地址长度为4,可以用socket.inet_ntoa变成ip地址,如果不够4个字节,需要后面补\x00
5. 获取到的IP地址就可以用来进行判断调度了
6. respond时也需要增加一个Additional RRs区域,直接把请求的Additional内容发过去就可以(如果支持source netmask,将请求中的source netmask复制到scope netmask中
,OpenDNS要求必须支持scope netmask)