zhangguanzhang's Blog

在 IPv6 单栈下适配部署 K8S、中间件、应用适配

字数统计: 5.2k阅读时长: 25 min
2024/08/02

记录近期 IPv6 单栈下 一些坑

由来

有客户的环境是 IPv6 单栈环境,需要部署我们产品,记录下适配遇到的坑。

过程

环境搭建

内部的虚机都是在 exsi 上,而到我们办公网台式机到 exsi 的链路上的交换机都不支持 IPv6 转发。但是视角放在同一个 exsi host 上,上面的所有虚机都是在 exsi 上的 vswitch 上,vswitch 是(内核)支持 IPv6 转发的。所以初步的模拟是一个 exsi host 上开下面的四台机器:

1
2
3
4
5
6
7
8
9
10
                                             
IPv4+IPv6 IPv6 IPv6 IPv6
┌──────┐ ┌───────┐ ┌───────┐ ┌───────┐
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
└──┬───┘ └───┬───┘ └───┬───┘ └───┬───┘
│ │ │ │
│ │ │ │
│ │ │ │
└──────────────┴─────────┴─────────┘

因为还涉及到后面的浏览器访问测试,所以左边是 Windows :

Os IPv4 IPv6 描述
Windows x.x.x.x 2002:db8:0:1::1/64 也是下面三台机器的网关
Centos 7.8 2002:db8:0:1::101/64
Centos 7.8 2002:db8:0:1::102/64
Centos 7.8 2002:db8:0:1::103/64

网卡配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TYPE=Ethernet
PROXY_METHOD=none
BROWSER_ONLY=no
BOOTPROTO=static
#IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
NAME=ens160
UUID=8813b773-201c-4fe1-80b3-5ab62586xxxx
DEVICE=ens160
ONBOOT=yes
IPV6_PRIVACY=no
IPV6INIT=yes
IPV6_AUTOCONF=no
IPV6ADDR=2002:db8:0:1::101/64
IPV6_DEFAULTGW=2002:db8:0:1::1

centos7 记得删掉 IPV6_ADDR_GEN_MODE=stable-privacy,不然我发现 IP 不生效,windows 记得开 文件和打印机共享(回显请求) 的 ipv4 ipv6 。

一些知识

IPv6 地址总长度为 128 比特,通常分为 8 组,每组为 4 个十六进制数的形式,每组十六进制数间用冒号分隔。 例如:

  • FC00:0000:130F:0000:0000:09C0:876A:130B
  • FC00:0000:130F::09C0:876A:130B 和上面是一样的,连续的 0 可以缩写,只能一处缩写

bind 的地址:

  • :: 等同于 ipv4 的 0.0.0.0
  • ::1 等同于ipv4 的 127.0.0.1localhost

url 格式是 http(s)://[xxx]:port ,host 部分多了方括号。

K8S

内部有 ansible 部署的,主要是基础上修改,之前节点名都是用的 IPv4 地址,直接部署后报错 kubelet 节点名无效,需要 RFC 1123 命名规范:

1
2
3
4
5
kubelet_node_status.go:92] "Unable to register node with API server" err="Node \"2002.db8.0.1..101\" is invalid: metadata.name: 
Invalid value: \"2002.db8.0.1..101\":
a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.',
and must start and end with an alphanumeric character (e.g. 'example.com',
regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')" node="2002.db8.0.1..101"

从1.21 版本支持双栈,下面的 CIDR 我只配置 IPv6 单栈,总结下改的点:

  • : 改成 . 当 nodeName 不行,因为存在缩写情况会被替换成两个 .. 而不匹配 RFC 1123 报错。
  • kubelet csr 的 CN "CN": "system:node:xxx", 也要和节点一致
  • 因为是单栈,涉及到 127.0.0.1 和 0.0.0.0 的都改成对应 IPv6
  • Service CIDR 设置为 2001:cafe:43::/112
    • kubernetes service 会使用第一个主机位 IP 2001:cafe:43::1,记得和 ::1 加到 csr 的证书 IP 里
    • DNS IP 是第十个主机位,我设置的 2001:cafe:43::a
  • Cluster CIDR 设置为 2001:cafe:42::/56

两个网段来源于参考 k3s 的 dual-stack-ipv4–ipv6-networking

flannel

最终部署完发现 flannel 报错:

1
2
3
4
5
6
7
8
9
10
11
12
vxlan_network.go:265] failed to add v6 vxlanRoute (2002:db8:42:2::/64 -> 2002:db8:42:2::): All attempts fail:
#1: no route to host
#2: no route to host

retry.go:29] #0: no route to host
retry.go:29] #1: no route to host
retry.go:29] #2: no route to host

# 路由信息
# ip -6 r s | grep flannel
2001:cafe:42:: dev flannel-v6.1 proto kernel metric 256 pref medium
fe80::/64 dev flannel-v6.1 proto kernel metric 256 pref medium

最终在官方仓库文档找到说明:

1
2
# https://github.com/flannel-io/flannel/blob/master/Documentation/configuration.md#dual-stack
vxlan support ipv6 tunnel require kernel version >= 3.12

Centos 7 升级内核解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ kubectl  get node -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
2002.0db8.0000.0001.0000.0000.0000.0101 Ready master,node 11d v1.27.15 2002:db8:0:1::101 <none> CentOS Linux 7 (Core) 4.19.113-300.el7.x86_64 docker://25.0.5
2002.0db8.0000.0001.0000.0000.0000.0102 Ready master,node 11d v1.27.15 2002:db8:0:1::102 <none> CentOS Linux 7 (Core) 4.19.113-300.el7.x86_64 docker://25.0.5
2002.0db8.0000.0001.0000.0000.0000.0103 Ready master,node 11d v1.27.15 2002:db8:0:1::103 <none> CentOS Linux 7 (Core) 4.19.113-300.el7.x86_64 docker://25.0.5
$ kubectl get pod -n kube-system -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
coredns-5b69bc466f-bkv7t 1/1 Running 1 (7d4h ago) 11d 2001:cafe:42:2::1a 2002.0db8.0000.0001.0000.0000.0000.0103 <none> <none>
coredns-5b69bc466f-f8g7g 1/1 Running 1 (7d4h ago) 7d6h 2001:cafe:42:1::1a 2002.0db8.0000.0001.0000.0000.0000.0102 <none> <none>
coredns-5b69bc466f-svvgz 1/1 Running 1 11d 2001:cafe:42:1::1b 2002.0db8.0000.0001.0000.0000.0000.0102 <none> <none>
kube-flannel-ds-g6bkw 1/1 Running 5 (3d ago) 11d 2002:db8:0:1::101 2002.0db8.0000.0001.0000.0000.0000.0101 <none> <none>
kube-flannel-ds-r8nnb 1/1 Running 1 (7d4h ago) 11d 2002:db8:0:1::102 2002.0db8.0000.0001.0000.0000.0000.0102 <none> <none>
kube-flannel-ds-rlnlb 1/1 Running 1 (7d4h ago) 11d 2002:db8:0:1::103 2002.0db8.0000.0001.0000.0000.0000.0103 <none> <none>
kube-state-metrics-5d47d4878c-cf974 1/1 Running 1 (7d4h ago) 11d 2001:cafe:42:2::19 2002.0db8.0000.0001.0000.0000.0000.0103 <none> <none>
node-local-dns-92td8 1/1 Running 9 (3d ago) 11d 2002:db8:0:1::101 2002.0db8.0000.0001.0000.0000.0000.0101 <none> <none>
node-local-dns-c7tq6 1/1 Running 2 (7d4h ago) 11d 2002:db8:0:1::102 2002.0db8.0000.0001.0000.0000.0000.0102 <none> <none>
node-local-dns-r6g5f 1/1 Running 2 (7d4h ago) 11d 2002:db8:0:1::103 2002.0db8.0000.0001.0000.0000.0000.0103 <none> <none>

测试跨节点通信:

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
$ ping6 -c 2 2001:cafe:42:2::1a
PING 2001:cafe:42:2::1a(2001:cafe:42:2::1a) 56 data bytes
64 bytes from 2001:cafe:42:2::1a: icmp_seq=1 ttl=63 time=0.178 ms
64 bytes from 2001:cafe:42:2::1a: icmp_seq=2 ttl=63 time=0.132 ms

--- 2001:cafe:42:2::1a ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1027ms
rtt min/avg/max/mdev = 0.132/0.155/0.178/0.023 ms

# 域名解析
$ dig @2001:cafe:42:2::1a kubernetes.default.svc.cluster.local +short AAAA
2001:cafe:43::1

$ kubectl -n default describe svc kubernetes
Name: kubernetes
Namespace: default
Labels: component=apiserver
provider=kubernetes
Annotations: <none>
Selector: <none>
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv6
IP: 2001:cafe:43::1
IPs: 2001:cafe:43::1
Port: https 443/TCP
TargetPort: 6443/TCP
Endpoints: [2002:db8:0:1::101]:6443,[2002:db8:0:1::102]:6443,[2002:db8:0:1::103]:6443
Session Affinity: None
Events: <none>

发现 nodelocaldns 有 Add ipv6 support to node-local-dns 支持,之前 IPv4 nodelocaldns 是使用 IPv4 的 Link-local address169.254.20.10 ,然后参照 pr 里面的 IPv6 fe80:169:254::1 发现报错:

1
Error listening: listen tcp6 [fe80:169:254::1]:8080: bind: invalid argument

最后怀疑不是 nodelocaldns 问题,写了个小 demo 验证:

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
package main

import (
"fmt"
"net"
"os"
)

func main() {
serverIP := os.Args[1]
serverPort := 8080

// 监听地址和端口
listener, err := net.Listen("tcp6", fmt.Sprintf("[%s]:%d", serverIP, serverPort))
if err != nil {
fmt.Println("Error listening:", err.Error())
return
}
defer listener.Close()

fmt.Printf("Listening on [%s]:%d\n", serverIP, serverPort)

for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("Error accepting connection:", err.Error())
return
}
go handleConnection(conn)
}
}

func handleConnection(conn net.Conn) {
defer conn.Close()
fmt.Println("Connection accepted.")
}

发现无法 bind fe80 开头报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ ip addr add ee80:169:254::1/128 dev lo
$ ip addr add fe80:169:254::1/128 dev lo
$ go run main.go ee80:169:254::1
Listening on [ee80:169:254::1]:8080
^Csignal: interrupt
$ go run main.go fe80:169:254::1
Error listening: listen tcp6 [fe80:169:254::1]:8080: bind: invalid argument
[root@guan ~/test/gotest]# uname -a
Linux guan 5.4.0-182-generic #202-Ubuntu SMP Fri Apr 26 12:29:36 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux
[root@guan ~/test/gotest]# ip -6 a s lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
inet6 ee80:169:254::1/128 scope global
valid_lft forever preferred_lft forever
inet6 fe80:169:254::1/128 scope link
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever

调试发现最终是 syscall 报错,使用 python 写类似代码也一样,然发现后这个网段 fe80::/10 只能指定网卡名或者 index:

1
2
3
4
5
6
7
$ go run main.go fe80:169:254::1%lo
Listening on [fe80:169:254::1%lo]:8080
# 其他窗口 curl,带网卡名才行
$ curl -g [fe80:169:254::1%lo]:8080
curl: (56) Recv failure: Connection reset by peer
$ curl -g [fe80:169:254::1]:8080
curl: (7) Couldn't connect to server

中间件层面

大部分中间件都会自动判断而监听 IPv6 地址,但是有些会有问题。

elasticsearch

es 无法组件集群:

1
2
3
4
{"type": "server", "timestamp": "2024-07-23T10:59:43,648+08:00", "level": "WARN", "component": "o.e.d.HandshakingTransportAddressConnector", "cluster.name": "xxxes", 
"node.name": "es-host1.default", "message": "[connectToRemoteMasterNode[[2001:cafe:43::3250]:9300]] completed handshake with [{es-host3.default}{3e7q-VGnRmWdh1XRtvb0Ww}
{aCnHTNTLRp-rRVVg7N_z0Q}{127.0.0.1}{127.0.0.1:9300}{dm}{xpack.installed=true, transform.node=false}] but followup connection failed",
"stacktrace": ["org.elasticsearch.transport.ConnectTransportException: [es-host3.default][127.0.0.1:9300] handshake failed. unexpected remote node {es-host1.default}{n19UyOGnQpiErI0TW_jOPw}{6Wawn96FShGZyhYlo9BgDQ}{127.0.0.1}{127.0.0.1:9300}{dm}{xpack.installed=true, transform.node=false}",

看报错信息是其他几个 es 上报 IP 是 127.0.0.1 了,Pod 内的 lo 还是有 127.0.0.1 IP 的。参照官方文档 unicast.hostsmodules-network 尝试 network.publish_host: 设置 _global_ 也不行 _site_ ,最后设置的 network.publish_host: _eth0:ipv6_ 上报 IP 才正确。

应用层面

redis

最开始有个数据库初始化的 golang 应用,该应用(使用 github.com/redis/go-redis )需要连 redis 报错了:

1
redis setnx failed dial tcp: address 2002:db8:0:1::101:8531: too many colons in address

这个问题是代码里把 IPv6:Port 地址当成 IPv4:Port 去解析了,类似 Split(":") 不等于 2 而报错。但是研发反馈出错的地方是每次变化的,每个使用 redis 的地方都会随机出现,后面让他写了个最小 demo 源码发给我,我 dlv 远程调试了下

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
(dlv) so
> github.com/go-redis/redis.(*ClusterClient).defaultProcess() D:/Install/Go/GOPATH/pkg/mod/github.com/go-redis/redis@v6.15.9+incompatible/cluster.go:954 (PC: 0x7b9385)
Values returned:
moved: true
ask: false
addr: "2002:db8:0:1::103:8533"

949: continue
950: }
951:
952: var moved bool
953: var addr string
=> 954: moved, ask, addr = internal.IsMovedError(err)
955: if moved || ask {
956: node, err = c.nodes.GetOrCreate(addr)
957: if err != nil {
958: break
959: }


(dlv) so
> github.com/go-redis/redis.(*baseClient).defaultProcess() D:/Install/Go/GOPATH/pkg/mod/github.com/go-redis/redis@v6.15.9+incompatible/redis.go:186 (PC: 0x7fe612)
Values returned:
~r0: *github.com/go-redis/redis/internal/pool.Conn nil
~r1: error(*net.OpError) *{
Op: "dial",
Net: "tcp",
Source: net.Addr nil,
Addr: net.Addr nil,
Err: error(*net.AddrError) *{
Err: "too many colons in address",
Addr: "2002:db8:0:1::103:8533",},}

181: for attempt := 0; attempt <= c.opt.MaxRetries; attempt++ {
182: if attempt > 0 {
183: time.Sleep(c.retryBackoff(attempt))
184: }
185:
=> 186: cn, err := c.getConn()
187: if err != nil {
188: cmd.setErr(err)
189: if internal.IsRetryableError(err, true) {
190: continue
191: }
(dlv)


(dlv) p c.opt
("*github.com/go-redis/redis.Options")(0xc0000d8240)
*github.com/go-redis/redis.Options {
Network: "tcp",
Addr: "[2002:db8:0:1::101]:8531",
...
(dlv) c
> github.com/go-redis/redis.(*baseClient).defaultProcess() D:/Install/Go/GOPATH/pkg/mod/github.com/go-redis/redis@v6.15.9+incompatible/redis.go:186 (hits goroutine(1):45 total:52) (PC: 0x7fe5f3)
181: for attempt := 0; attempt <= c.opt.MaxRetries; attempt++ {
182: if attempt > 0 {
183: time.Sleep(c.retryBackoff(attempt))
184: }
185:
=> 186: cn, err := c.getConn()
187: if err != nil {
188: cmd.setErr(err)
189: if internal.IsRetryableError(err, true) {
190: continue
191: }
(dlv) p c.opt
("*github.com/go-redis/redis.Options")(0xc0000d8300)
*github.com/go-redis/redis.Options {
Network: "tcp",
Addr: "2002:db8:0:1::103:8533",
Dialer: github.com/go-redis/redis.(*Options).init.func1,
OnConnect: nil,

找到大概问题,这里 addr 变成了没带方括号的,addr 只有一个地方 moved, ask, addr = internal.IsMovedError(err) 获取的,内部逻辑是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func IsMovedError(err error) (moved bool, ask bool, addr string) {
if !IsRedisError(err) {
return
}

s := err.Error()
if strings.HasPrefix(s, "MOVED ") {
moved = true
} else if strings.HasPrefix(s, "ASK ") {
ask = true
} else {
return
}

ind := strings.LastIndex(s, " ")
if ind == -1 {
return false, false, ""
}
addr = s[ind+1:]
return
}

就是 redis 返回的 MOVED 14443 2002:db8:0:1::103:8533 重定向字样里取 IP 地址,也就是一开始从 DSN 字符串有方括号是能识别成 IPv6 地址,但是后面操作 redis 发生重定向就使用这个 IP 去做连接,看 redis 最新源码 void clusterRedirectClient 发现 redis server 并不会返回 IPv6 地址加方括号:

1
2
3
4
5
// https://github.com/redis/redis/blob/89742a95dbd4e95f5112136e4bcff698195e205c/src/cluster.c#L1179
addReplyErrorSds(c,sdscatprintf(sdsempty(),
"-%s %d %s:%d",
(error_code == CLUSTER_REDIR_ASK) ? "ASK" : "MOVED",
hashslot, clusterNodePreferredEndpoint(n), port));

搜索 redis move ipv6 搜到了 issue redis-cli ipv6 issue on redis cluster,发现其他语言的 SDK 都是处理 MOVE 后的地址加判断,于是去看 github.com/redis/go-redis 最新版本 isMovedError 方法发现 4个月之前修复了:

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
// https://github.com/redis/go-redis/blob/00d98485f8357d016139ece133bd3272e7ff2526/error.go#L114/
func isMovedError(err error) (moved bool, ask bool, addr string) {
if !isRedisError(err) {
return
}

s := err.Error()
switch {
case strings.HasPrefix(s, "MOVED "):
moved = true
case strings.HasPrefix(s, "ASK "):
ask = true
default:
return
}

ind := strings.LastIndex(s, " ")
if ind == -1 {
return false, false, ""
}

addr = s[ind+1:]
addr = internal.GetAddr(addr) // <---- 这里是处理
return
}

让升级引用库版本大于等于 9.5.2 后解决。

mysql

1
dial tcp [2002:db8:0:1::101:3306]:3306: connect: no route to host

该问题是因为 IPv6 地址可以缩写,然后 :3306 在 DSN 字符串里:

1
2
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]
[username[:password]@][tcp[(2002:db8:0:1::101:3306)]]/dbname[?param1=value1&...&paramN=valueN]

被业务的 mysql client SDK 解析当作 IPv6 主机位而连错 IP,可以参照前面 redis 结尾的函数先处理先 mysql IP 地址再传入 DSN 字符串里。

组网测试

随着后续应用开始介入,那个 windows 不支持多人同时登录非常难受,记得以前配置过,但是还有 windows/macOS 上 app 端,还要看 app 端能否使用 IPv6 连 server。就考虑有没有隧道的形式组网:

ipv6-on-ipv4-tunel

windows 自带的内置的最好了,不然省得后续给开发和测试写个安装文档,搜了下有个 Teredo 但是看了下只有 Client 部署,Server 的部署没有,搜到开源的 miredo,但是部署 server 后启动报错:

1
2
3
$ /usr/sbin/miredo-server -f -c /etc/miredo/miredo-server.conf
...
Teredo server UDP socket err: Server IPv4 address must be global unicast

报错说不是 IPv4 组播地址,但是主机位写 255 还是不行,代码里搜了下这个报错找到 校验逻辑。然后没用过这些 c 库看不懂逻辑,就换 headscale 得了。

双栈机器上:

  • 安装 headsacleip_prefix 关闭 IPv4 保留 IPv6 那个网段 fd7a:115c:a1e0::/48
  • 安装 derp + tailscale,这个 tailscale up 时候带上 --advertise-routes 2002:db8:0:1::/64
  • 然后开启这个的路由 headscale routes enable -r 1
  • 双栈机器开启 ipv4 ipv6 forward sysctl

后面发现 windows 上开了客户端后访问 101 发现来源 IP 是双栈机器的 IPv6 IP,因为 tailscale linux 客户端默认是开启了 SNAT,把双栈机器上的 snat 关了:

1
tailscale set --snat-subnet-routes=false

因为双栈机器是 IPv6 机器的网关,所以这样没做 SNAT 后:

  • windows/MacOS/手机(fd7a:115c:a1e0::/48) 访问 2002:db8:0:1::/64 走本机路由到接口 tailscale ,会封包隧道发往到双栈机器上。
  • 然后双栈机器上解包后,本机开了ipv4 ipv6 forward ,走路由 2002:db8:0:1::/64 到右侧的单栈机器,右侧机器回包(fd7a:115c:a1e0::/48)走自己默认路由到双栈机器,再发到客户端。

然后测试了下发现偶尔会断:

1
2
3
4
$ ssh 2002:db8:0:1::101
ssh: connect to host 2002:db8:0:1::101 port 22: No route to host
$ ssh 2002:db8:0:1::101
root@2002:db8:0:1::101's password:

双栈机器上 ip -6 r s | grep 2002:db8 看路由表又没问题,怀疑是 2002:db8:0:1::/64 的偶尔走出去到物理交换机上了,然后给双栈机器增加了个网卡隔离开,也就是把 IPv6 主机位 1 的 IP 配置在该网卡上,测试下发现很稳定。

然后给研发和测试写了个安装 tailscale 客户端和接入文档,很方便。

2024/10 后续的一些问题和发现

pc 端也有应用研发,适配后,反馈用 ipv6 IP url 软件内的登录页是有登录框显示,配置的 hosts 域名则不行也就是下面相关逻辑:

  1. 点击登录按钮后有个弹出的内嵌页面,会请求域名 url
  2. 把获取到 http body 展示在内嵌页

现象是内嵌页一直是空白,反馈说打开 Charles 抓包就能正常显示。没办法只有让他改源码,让在 1 的网络请求结果和错误都打印下看看,给我反馈说是:

1
ERR_NAME_NOT_RESOLVED

确实是域名无法解析,但是我们业务也可以直接浏览器访问,浏览器用 hosts 域名可以访问的,有些软件会无视系统的 hosts 文件是自己实现相关域名解析逻辑,让他从这方面排查下看看。

第二天他在 Linux 上发现相关问题,开了猫咪代理后内嵌页有内容,问他是不是猫咪自动更新了,给我截图了下界面,看了下 IPv6 选项开启的,让他关闭后再试试就不行了,而 windows 上猫咪开不开 IPv6 选项都不行。
发现很奇怪,就要来了安装包,自己开了个 windows 也复现这个了问题,想着看了下猫咪 meta 内核源码能不能找到眉目, yaml ipv6 字段使用的地方:

1
2
3
4
5
6
type General struct {
Inbound
Mode T.TunnelMode `json:"mode"`
UnifiedDelay bool `json:"unified-delay"`
LogLevel log.LogLevel `json:"log-level"`
IPv6 bool `json:"ipv6"`

然后传递配置变量里

1
2
3
if general.IPv6 != nil {
resolver.DisableIPv6 = !*general.IPv6
}

解析行为相关的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
func LookupIPWithResolver(ctx context.Context, host string, r Resolver) ([]netip.Addr, error) {
if node, ok := DefaultHosts.Search(host, false); ok {
return node.IPs, nil
}

if r != nil && r.Invalid() {
if DisableIPv6 {
return r.LookupIPv4(ctx, host)
}
return r.LookupIP(ctx, host)
} else if DisableIPv6 {
return LookupIPv4WithResolver(ctx, host, r)
}

上面开启 ipv6 走的是 r.LookupIP 方法,跳转过去是走的 ipv6 dns 解析

1
2
3
4
5
6
7
8
9
10
11
func (r *Resolver) LookupIP(ctx context.Context, host string) (ips []netip.Addr, err error) {
ch := make(chan []netip.Addr, 1)
go func() {
defer close(ch)
ip, err := r.lookupIP(ctx, host, D.TypeAAAA)
if err != nil {
return
}

ch <- ip
}()

开了猫咪的 ipv6 影响了解析相关,类似强制使用 ipv6 解析而导致正常,也就是说明不正常现象是因为走的 ipv4 解析了,怎么配置使用 IPv6 优先级,发现 windows 是下面查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
C:\Windows\system32>netsh interface ipv6 show prefixpolicies
查询活动状态...

优先顺序 标签 前缀
---------- ----- --------------------------------
50 0 ::1/128
40 1 ::/0
35 4 ::ffff:0:0/96
30 2 2002::/16
5 5 2001::/32
3 13 fc00::/7
1 11 fec0::/10
1 12 3ffe::/16
1 3 ::/96

搜到的都是这个默认配置是 IPv6 优先级比 IPv4 高,很多人是想优先使用 IPv4 而改上面优先级,我们这个问题是想 IPv6 优先级高,这个默认配置是没有问题。没办法了,就在上面的双栈机器搭建 dns server 看下解析记录:

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
cat > docker-compose.yml << EOF
services:
coredns:
image: docker.m.daocloud.io/coredns/coredns:1.11.3
container_name: coredns
restart: always
network_mode: host
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
volumes:
- ./config/:/etc/coredns/
- /etc/localtime:/etc/localtime:ro
command: ["-conf", "/etc/coredns/Corefile"]

mkdir -p ./config/
cat > ./config/Corefile << EOF
.:53 {
bind 0.0.0.0 ::
#bind ::
hosts {
10.xxx.xx.63 xxx.xxx.cn
2002:db8:0:1::101 xxx.xxx.cn
fallthrough
}
forward . 223.5.5.5
log
errors
reload
}
chmod a+r ./config/Corefile
docker-compose up -d

该域名 A 记录和 AAAA 记录都有,设置下测试 windows 的 dns server 指向双栈机器点了下登录后,发现 coredns 日志里解析的是 A 记录解析请求,问题就是这了,A 记录的 IP 机器上抓包发现 http GET 请求发过来了,但是机器没有监听这个端口,所以内嵌登录页还是空白内容。

但是很奇怪的是为啥 hosts 有还走 dns 解析请求,后面测试了下发现以下几个现象:

  • 机器上配置 IPv6 hosts,不启动 tailscale:
    • 无法 ping 通该域名
    • Resolve-DnsName xxx.xxx.cn 可以解析出域名,ipconfig /displaydns 里也有显示有 AAAA 记录
  • 机器上配置 IPv6 hosts,启动 tailscale:
    • 可以被路由的 IPv6 hosts 域名可以解析
    • 不可以被路由的 IPv6 hosts 域名无法解析

研发反馈内嵌页用到了 Chromium Embedded Framework 框架,但是我自己的 chrome 浏览器访问域名业务是可以的,想着会不会研发他们的 CEF 框架内的 chrome 版本低的问题,搜相关关键字 chrome not use hosts file ipv6 搜到了同样的问题 Chrome not using hosts file for IPv6 addresses since v73

链接里下面的回答是 chrome 早期版本探测 IPv6 是否可用是看 2001:4860:4860::8888/128 网段是否可以被路由,然后尝试了 powershell 管理员身份运行添加 IPv6 路由解决:

1
netsh interface ipv6  add route 2001:4860:4860::8888/128 interface=1

然后优雅解决的话,不能让相关 windows 测试人员本机添加路由,而是利用 headscale 下发路由,给双栈机器上的 tailscale 增加上这个路由下发到所有其他 tailscale 上,双栈机器上使用 tailscale set:

1
tailscale set --advertise-routes 2002:db8:0:1::/64,2001:4860:4860::8888/128 --snat-subnet-routes=false

参考

CATALOG
  1. 1. 由来
  2. 2. 过程
    1. 2.1. 环境搭建
    2. 2.2. 一些知识
    3. 2.3. K8S
    4. 2.4. flannel
    5. 2.5. nodelocaldns 和 link local 问题
    6. 2.6. 中间件层面
      1. 2.6.1. elasticsearch
    7. 2.7. 应用层面
      1. 2.7.1. redis
      2. 2.7.2. mysql
    8. 2.8. 组网测试
  3. 3. 2024/10 后续的一些问题和发现
  4. 4. 参考