Featured image of post 在OpenWrt上遇到的v6 DNS相关问题

在OpenWrt上遇到的v6 DNS相关问题

愿世界v6早日完善

封面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的一贴讨论1中找到了线索,貌似是小米在系统中预设了几个公共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域名的路由吧

拷贝漫画v6路由

拷贝漫画v4路由

接下来介绍下DNS查询的逻辑。查询由客户端发起,并且由客户端指定查询的记录类型(比如A或AAAA),一般客户端会同时发起两个查询请求,分别查询v6和v4的解析记录,最后由客户端选择一个解析来访问服务(现代客户端一般会优先使用AAAA)。那目前就有两种解法,一是让客户端优先使用v4解析;二是尝试过滤查询结果。

让客户端优先使用v4解析,大部分linux可以通过设置对应发行版的解析配置文件来解决,例如debain系的/etc/gai.conf文件2,但是不巧的是这两容器使用的系统为alpine,并不支持配置3,因此第一个方法可以排除。

剩下一种方法便是过滤查询结果了,这里的简单思路是在客户端查询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