Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

IPv6 addresses

An endpoint may name an IPv6 literal in brackets – dav://[::1]:8443/p, oci://[fe80::1]/repo, ssh://user@[::1]:2222/p, hdfs://[::1]:9870/p, nfs://[::1]/export – and one reader handles the authority for all of them, so a bracketed host cannot mean one thing to the endpoint parser and another to the transport’s.

The interesting part is the arm that was missing. Unifying the parsers and proving it by round-tripping Endpoint::parse into display() and back into each transport’s own parser is not enough: every such arm passed, and dav://[::1]:8443/p still could not open. A parser round-trip is not a connection, and nothing measured the connection. The host arrived at (host, port).to_socket_addrs() still wearing its brackets, and [::1] is not an address – it is a name, so it went to the resolver and came back failed to lookup address information.

The brackets are correct everywhere else: Host: [::1]:8443 needs them, CONNECT [::1]:443 needs them, RFC 3986 requires them in the URL. So the fix is a strip at the three points of use rather than a second host field: the resolver (unbracketed in connect_tcp, which every HTTP adapter and the proxy dial share, and in the NFS RPC client – those are the crate’s only two to_socket_addrs call sites), and rustls’s ServerName, which reads an IP or a DNS name and [::1] is neither.

Two parsers were also still splitting on the last colon with no bracket awareness, each reachable only by a route the table arm did not cover: -s3.endpoint http://[::1]:9000 became a host of that whole name on the scheme’s default port, and HTTPS_PROXY did the same. Both read the authority the way the other four do now, and the table arm covers all six.

nfs:// is deliberately different: its display form is host:/export, where brackets mean nothing, and the bare spelling ::1:/export already stored the address. The two spellings disagreed – one worked and one did not – so the URL form now stores the address too, per element, since an address list may be nfs://[::1],[::2]/export.

A zone id is the second half of this. A link-local address carries one – which interface to leave by – and RFC 6874 writes it %25eth0 in a URI because % must be escaped there. Strip only the brackets and the resolver gets fe80::1%25eth0, which is not an address either, so it goes to the name lookup exactly as [::1] used to. Measured, and it needs no IPv6 stack because it is a parse: fe80::1%25lo fails the lookup and fe80::1%lo resolves to [fe80::1%1]:80. A certificate is the opposite case – a zone is a local routing detail and no part of the peer’s identity, so nothing names one and rustls refuses the whole string – and unzoned drops it for the ServerName alone.

NO_PROXY was mangling the addresses it was told to exempt. The port strip split every entry on its last colon, which is right for example.com:8080 and destroys an address: NO_PROXY=::1 became :: and NO_PROXY=[::1] became [::, so neither matched the stored host and traffic went through the proxy the operator had just excluded. A port is stripped only when what follows the colon is one, both spellings are accepted, and the comparison is made on the bare address.

What is NOT measured here, and the arm says so in its own comment: this container has no IPv6 stack at all (bind [::1] answers EAFNOSUPPORT), so no arm in this suite makes an IPv6 connection. What the arm measures is the thing that was wrong – that the literal reaches the resolver as an address and not as a name – with a control that a genuine unresolvable name still fails the other way, so the distinction it reads is a real one.