封面PID=126010542
由于OpenWrt的dhcp服务默认是下发IPv6的dns给客户端的,导致AdguardHome的日志中会出现很多IPv4混杂IPv6的客户端,对于排查问题不是很友好
遂决定禁用IPv6的dns下发,让所有客户端只通过IPv4 dns来查询解析。
禁用v6 DNS服务
根据官方文档介绍,禁用调odhcpd的v6 dns:
1
2
3
4
|
uci set dhcp.lan.dns_service="0"
uci set dhcp.lan.ra_dns="0"
uci commit dhcp
service odhcpd restart
|
同时,修改dhcp下发的dns地址为自建的服务地址,我是直接在op上跑的,所以就是路由器本身:
1
2
3
4
|
uci -q delete dhcp.lan.dhcp_option
uci add_list dhcp.lan.dhcp_option="6,192.168.1.1"
uci commit dhcp
service dnsmasq restart
|
这样重新连接网络,客户端就不会再拿到IPv6的dns啦。查询也都是走的v4,效果如图:

DNS污染问题
去年手机换成了小米15,然后发现访问谷歌时不时会出现dns污染,通过查看服务日志发现v4的查询记录均是正常的,没发现返回了错误的ip。最后在v2ex的一贴讨论中找到了线索,貌似是小米在系统中预设了几个公共DNS,导致有部分场景下DNS泄漏了。
解决方法也比较简单,手动设置下DNS重定向就行了,由于我已经使用了AdguardHome代替了dnsmasq监听53端口,因此需要手动配置下防火墙转发:
1
2
3
4
5
6
7
8
|
# V4 DNS Hijack
uci add firewall redirect
uci set firewall.@redirect[-1].target='DNAT'
uci set firewall.@redirect[-1].family='ipv4'
uci set firewall.@redirect[-1].src='lan'
uci set firewall.@redirect[-1].src_dport='53'
uci set firewall.@redirect[-1].name='DNS'
uci commit firewall
|
当然也不能忘了我们亲爱的v6 DNS
由于我们没有提供v6服务给客户端,因此针对客户端的v6查询直接禁用即可:
1
2
3
4
5
6
7
8
9
|
# V6 DNS Block
uci add firewall rule
uci set firewall.@rule[-1].src='lan'
uci set firewall.@rule[-1].dest='*'
uci set firewall.@rule[-1].family='ipv6'
uci set firewall.@rule[-1].dest_port='53'
uci set firewall.@rule[-1].target='REJECT'
uci set firewall.@rule[-1].name='DNS6BLOCK'
uci commit firewall
|
优先返回IPv4解析
问题
我在本地有使用jackett + flaresolverr + sonarr进行pt资源的抓取,自动订阅番剧。flaresolverr可以简单理解为一个代理工具,负责帮我们处理jackett抓取pt资源时遇到的人机校验,主要是cloudflare的。然而flaresolverr要求自身访问的请求环境必须和客户端(jackett)一致,否则可能出现获取cookie无效的情况。
在我的设备环境下有个比较尴尬的情况,就是这两服务的容器分别使用了IPv4和IPv6的解析去获取数据,导致没法绕过人机验证。
那有什么办法可以解决吗?直接禁用容器IPv6?这会误伤我一些只支持v6的pt站点,比如北邮人。以我的场景,更希望实现的是如果一个域名同时拥有v4和v6地址时,所有客户端只使用v4的解析,但又不影响纯v6域名的访问。
问为什么优先使用v4?因为现在国外一些域名的v6路由的异常的逆天抽象。不信?しょうがないなあ,让你看看拷贝漫画cdn域名的路由吧
:


接下来介绍下DNS查询的逻辑。查询由客户端发起,并且由客户端指定查询的记录类型(比如A或AAAA),一般客户端会同时发起两个查询请求,分别查询v6和v4的解析记录,最后由客户端选择一个解析来访问服务(现代客户端一般会优先使用AAAA)。那目前就有两种解法,一是让客户端优先使用v4解析;二是尝试过滤查询结果。
让客户端优先使用v4解析,大部分linux可以通过设置对应发行版的解析配置文件来解决,例如debain系的/etc/gai.conf文件,但是不巧的是这两容器使用的系统为alpine,并不支持配置,因此第一个方法可以排除。
剩下一种方法便是过滤查询结果了,这里的简单思路是在客户端查询AAAA记录时,同时检查域名是否存在A记录,如果存在,则将AAAA的请求拦截并返回空给客户端,这样客户端拿不到AAAA记录,就只能使用A记录了。
代码实现
这里用Go写了个简单的DNS服务器作过滤,实现这个效果:
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
35
36
37
38
39
40
41
42
43
44
|
func HandleDNSRequest(writer dns.ResponseWriter, req *dns.Msg) {
c := new(dns.Client)
c.Net = "udp"
var upstreamDNS = *UpstreamAddress
resp, _, err := c.Exchange(req, upstreamDNS)
if err != nil {
log.Printf("Query upstream DNS error: %v\n", err)
return
}
defer writer.WriteMsg(resp)
// 非标准DNS查询,或结果为空,直接返回
if len(req.Question) != 1 || len(resp.Answer) == 0 {
return
}
// 非AAAA查询,直接返回
if req.Question[0].Qtype != dns.TypeAAAA {
return
}
// 尝试获取域名A记录
m := new(dns.Msg)
m.SetQuestion(req.Question[0].Name, dns.TypeA)
r, _, err := c.Exchange(m, upstreamDNS)
if err != nil {
log.Printf("Query upstream DNS error: %v\n", err)
return
}
if len(r.Answer) == 0 {
return
}
for _, rr := range r.Answer {
if rr.Header().Rrtype == dns.TypeA {
// 存在A记录,则将AAAA响应清空
resp.Answer = make([]dns.RR, 0)
return
}
}
}
|
测试下:
1
2
3
4
5
6
7
|
$ nslookup -port=5367 www.google.com 127.0.0.1
Server: 127.0.0.1
Address: 127.0.0.1#5367
Non-authoritative answer:
Name: www.google.com
Address: 142.250.76.132
|
嗯~看起来效果不错,接下来再加些缓存优化,减少多次DNS查询带来的耗时:
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
|
func HandleDNSRequest(writer dns.ResponseWriter, req *dns.Msg) {
...
if C != nil {
// 先查看缓存是否已有记录
if hasA, exist := C.Get(req.Question[0].Name); exist {
// 是否存在A记录
if hasA.(bool) {
resp.Answer = make([]dns.RR, 0)
}
return
}
}
...
for _, rr := range r.Answer {
if rr.Header().Rrtype == dns.TypeA {
// 设置A缓存记录存在
setARecordCache(req.Question[0].Name, true)
resp.Answer = make([]dns.RR, 0)
return
} else {
// 设置A缓存记录不存在
setARecordCache(req.Question[0].Name, false)
}
}
}
|
完美搞定
完整代码可以去看我GitHub的仓库dns-filter。