f5vpn で会社の VPN つないでる時に az login (というか az 全般) が無言で固まる問題を Claude と切り分けしたメモ。
- f5vpn は tun0 で IPv6 default route (
::/1+8000::/1) を吸い込む。が、tun0 自体は IPv6 GUA を持たない (link-local のみ) ので IPv6 が black hole になる。 - curl は happy-eyeballs で IPv4 にフォールバックするから動く。az-cli (中身は Python の urllib3) は happy-eyeballs を持たないので AAAA を引いた瞬間ハングする。
- 対策は
/etc/gai.confのprecedence ::ffff:0:0/96 100のコメントアウトを外して IPv4 優先にすること。
f5vpn で VPN 接続している状態で az を叩くと固まる。
❯❯ az login --debug
...
cli.azure.cli.core.auth.binary_cache: load: /home/taiki/.azure/msal_http_cache.bin
urllib3.util.retry: Converted retries value: 1 -> Retry(total=1, connect=None, read=None, redirect=None, status=None)
urllib3.connectionpool: Starting new HTTPS connection (1): login.microsoftonline.com:443
(ここで止まる)
^C
cli.__main__: Command ran in 7.562 seconds (init: 0.035, invoke: 7.527)
最初 strace az login を取ったけど、これは間違い。/usr/bin/az はただのシェルスクリプトで、本体は /opt/azure-cli/bin/python。つまり、strace の出力は bash が wait4(-1 で子プロセスを待ってるところで止まっていて、Python 側のシステムコールが見えない。子も追うなら strace -f、もしくは az --debug をするべきだった。
DNS まわりが怪しいかなと思って resolvectl status を見た。
❯❯ resolvectl status
Global
Protocols: +LLMNR +mDNS -DNSOverTLS DNSSEC=no/unsupported
resolv.conf mode: stub
Current DNS Server: 172.16.31.254
DNS Servers: 172.16.31.254
DNS Domain: ~.
社内 DNS が ~. で刺さってるので、全部の名前解決が VPN 経由で社内 DNS に行く構成。これは正しい。
名前解決自体は通る。
❯❯ resolvectl query login.microsoftonline.com
login.microsoftonline.com: 2603:1047:1:a0::c -- link: tun0
2603:1047:1:a0::11 -- link: tun0
...
40.126.13.9 -- link: tun0
20.190.141.37 -- link: tun0
...
AAAA も A も両方返ってくる。
curl もそのまま叩くと普通に通る。
❯❯ curl -v --max-time 10 https://login.microsoftonline.com/
* Trying [2603:1047:1:a0::8]:443...
* Trying 40.126.13.9:443...
* ALPN: curl offers h2,http/1.1
...
< HTTP/2 302
ここで curl は IPv6 を試した直後に IPv4 に切り替えている事に気付く。これは happy-eyeballs。
ということは、curl は気付かないだけで IPv6 は壊れているのでは? と思って -6 / -4 で確かめると、
❯❯ curl -6 -v --max-time 5 https://login.microsoftonline.com/
* Trying [2603:1047:1:150::1]:443...
* Trying [2603:1046:2000:148::3]:443...
...
* Connection timed out after 5001 milliseconds
❯❯ curl -4 -v --max-time 5 https://login.microsoftonline.com/
* Trying 20.190.141.34:443...
...
< HTTP/2 302
IPv6 だけ詰む。
ip -6 route と ip -6 addr show tun0 を見ると理由がわかる。
❯❯ ip -6 route
::/1 dev tun0 metric 1024 pref medium
fe80::/64 dev tun0 proto kernel metric 256 pref medium
8000::/1 dev tun0 metric 1024 pref medium
❯❯ ip -6 addr show tun0
25: tun0: <POINTOPOINT,UP,LOWER_UP> mtu 1371 ...
inet6 fe80::8c4c:4c93:7f05:db78/64 scope link stable-privacy proto kernel_ll
::/1 + 8000::/1 の二本で IPv6 default route を全部 tun0 に吸い込んでいる (full tunnel)。なのに tun0 には link-local の IPv6 しかない。つまり f5vpn は IPv4 only な tunnel なのに IPv6 の default route まで奪っている。AAAA で返ってきた 2603:... 宛てのパケットは tun0 に入って消える → TCP timeout までハング。
curl は happy-eyeballs で見限って IPv4 に切り替えるので問題が起こらない。Python の urllib3 / requests は happy-eyeballs を持たず、getaddrinfo が返した最初のアドレス (AAAA) に直列で繋ぎに行くから、ハングしているように見える。az-cli, Azure SDK for Python に限らず Python 系の HTTP クライアントは基本的に全てこの罠を踏むはず。
/etc/gai.conf の以下の行をコメントアウト解除する。
precedence ::ffff:0:0/96 100
これで glibc の getaddrinfo が IPv4-mapped アドレス (= IPv4) を IPv6 GUA より優先するようになって、Python が AAAA を選ばなくなる。VPN を切ってる時にも IPv6 native が使えるサイトでは IPv4 が選ばれるだけなので副作用はほぼ無い。
glibc は /etc/gai.conf.d/ はサポートしていない (man 5 gai.conf 参照)。ので、ファイル本体を直接編集する。
man 5 gai.confman systemd-resolved.service
おわり。