agronholm / agronholm/anyio

connect_tcp blindly prioritizes IPv6 even when the system has no globally routable IPv6 address

Offen
#1,230 0 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen
Vorherrschende Sprache
Python
Sterne
2.5k
Forks
260
Ø Merge
1 T. 8 Std.
Gemergte PRs (30 T.)
17

Beschreibung

# Bug: `connect_tcp` blindly prioritizes IPv6 even when the system has no globally routable IPv6 address

## Description

`anyio.connect_tcp()` reorders `getaddrinfo` results to always try IPv6 first, even when the system only has a link-local IPv6 address (`fe80::/10`) and no globally routable IPv6 address. This causes unnecessary connection delays and failures on networks where IPv6 is not actually available.

## Root Cause

In `anyio/_core/_sockets.py`, the `connect_tcp()` function explicitly reorders addresses to put IPv6 first:

```python
# Organize the list so that the first address is an IPv6 address (if available)
# and the second one is an IPv4 addresses. The rest can be in whatever order.
v6_found = v4_found = False
for af, *_, sa in gai_res:
if af == socket.AF_INET6 and not v6_found:
v6_found = True
target_addrs.insert(0, (af, sa[0])) # ← IPv6 forced to front
elif af == socket.AF_INET and not v4_found and v6_found:
v4_found = True
target_addrs.insert(1, (af, sa[0])) # ← IPv4 pushed to second
```

The problem: `getaddrinfo` with `AI_ADDRCONFIG` returns IPv6 results when the system has **any** IPv6 address, including link-local (`fe80::/10`). But link-local addresses cannot reach the internet. When `connect_tcp` tries IPv6 first, it immediately fails with "Network is unreachable", wasting time before falling back to IPv4.

## Reproduction

```python
import socket
import asyncio
import anyio

async def test():
# On a system with only link-local IPv6 (no global IPv6):
# 1. getaddrinfo returns both IPv4 and IPv6
infos = socket.getaddrinfo('api.telegram.org', 443, socket.AF_UNSPEC, socket.SOCK_STREAM)
print('getaddrinfo order:', ['IPv6' if i[0] == socket.AF_INET6 else 'IPv4' for i in infos])
# → ['IPv4', 'IPv6'] or ['IPv6', 'IPv4'] depending on system

# 2. anyio.connect_tcp reorders to put IPv6 first
# 3. IPv6 connection fails immediately (no route)
# 4. Falls back to IPv4 after happy_eyeballs_delay (0.25s default)

try:
stream = await anyio.connect_tcp('api.telegram.org', 443)
print(f'Connected to: {stream.extra(anyio.abc.SocketAttribute.remote_address)}')
except Exception as e:
print(f'Error: {e}')

asyncio.run(test())
```

## Expected Behavior

`connect_tcp()` should check whether the system has a **globally routable** IPv6 address before prioritizing IPv6. If only link-local or loopback IPv6 addresses are available, IPv4 should be tried first.

## Suggested Fix

Before reordering, check if the system has a global IPv6 address:

```python
def _has_global_ipv6() -> bool:
"""Check if the system has any globally routable IPv6 address."""
try:
for iface in socket.if_nameindex():
addrs = socket.getaddrinfo(iface[1], None, socket.AF_INET6)
for family, *_, addr in addrs:
ip = ipaddress.IPv6Address(addr[0].split('%')[0])
if not ip.is_link_local and not ip.is_loopback:
return True
except (OSError, ValueError):
pass
return False

# In connect_tcp():
has_global_v6 = _has_global_ipv6()
for af, *_, sa in gai_res:
if af == socket.AF_INET6 and not v6_found and has_global_v6:
v6_found = True
target_addrs.insert(0, (af, sa[0]))
elif af == socket.AF_INET and not v4_found:
v4_found = True
# Put IPv4 first if no global IPv6
idx = 0 if not has_global_v6 else 1
target_addrs.insert(idx, (af, sa[0]))
```

## Environment

- anyio version: 4.13.0
- Python: 3.11/3.13
- OS: Linux (containerized environments with only link-local IPv6)
- glibc: 2.41

## Related

- This affects HTTP clients (httpx → httpcore → anyio) connecting to dual-stack services
- The `happy_eyeballs_delay` (0.25s) mitigates but doesn't eliminate the problem
- Container/orchestration platforms (Docker, Kubernetes, lightweight VMs) commonly have only link-local IPv6

Beitragsleitfaden

Beitragsleitfaden öffnen

Bewertung

Dieses Issue wurde noch nicht bewertet.

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.