Skip to content

Parse bracketed IPv6 authorities - #155

Open
AYastrebov wants to merge 2 commits into
cfal:masterfrom
AYastrebov:fix/bracketed-ipv6
Open

AYastrebov wants to merge 2 commits into
cfal:masterfrom
AYastrebov:fix/bracketed-ipv6

Conversation

@AYastrebov

Copy link
Copy Markdown
Contributor

Problem

A bracketed IPv6 authority is parsed as a hostname. On master:

NetLocation::from_str("[2001:db8::1]:443", None)
// => Ok((Hostname("[2001:db8::1]"), 443))

Address::from classifies by scanning bytes, and a leading [ is neither a
digit, a hex letter, : nor ., so it clears the IPv4 and IPv6 flags and the
string falls through to the hostname branch. It then goes to the resolver,
which answers "Name or service not known". [host]:port is the form RFC 3986
defines and the form HTTP clients send, so an IPv6 outbound written the normal
way is unreachable, and the error says nothing about why.

The HTTP inbound has the same problem for a different reason: it splits the
CONNECT authority on the first :, which for an IPv6 literal sits inside the
address. CONNECT [2001:db8::1]:443 is refused, and so is a forward URL like
GET http://[2001:db8::1]/.

Fix

address: parse the bracketed IPv6 form strips the brackets in Address::from
and parses the interior as an Ipv6Addr. Brackets mean an IP literal and
nothing else, so a bracketed string that is not an IPv6 address is now an error
rather than a name to look up: [example.com], [1.2.3.4] and an unterminated
[::1 are all refused. NetLocation::from_str needed no change, since its
rfind(':') already lands on the port separator once the address itself parses.

http: accept a bracketed IPv6 authority parses the CONNECT target and the
forward-URL authority through NetLocation instead of splitting on the first
colon.

Tests

Five in address.rs: the bracketed form (compressed, full eight-group and
IPv4-mapped), the refusals above, a bracketed authority with a port, a
bracketed port range, and unbracketed addresses still parsing as they did. Two
in http_handler.rs drive a request through the handler, one for CONNECT and
one for a forward URL.

cargo test passes (783), cargo fmt --check is clean, and cargo clippy
reports nothing new.

AYastrebov and others added 2 commits August 24, 2026 10:40
RFC 3986 wraps an IPv6 literal in brackets so its colons cannot be read
as the port separator, and that is the form both configs and HTTP
clients use: [2001:db8::1]:443. Address::from cleared possible_ipv4 and
possible_ipv6 on the leading '[' but left possible_hostname set, so the
whole bracketed string became a hostname and went to the resolver, which
answered "Name or service not known". An IPv6 outbound was unreachable
and the message said nothing about why.

The unbracketed form worked, and rule masks already accepted brackets
(NetLocationMask::from), so this was also an inconsistency between the
two notations.

Brackets denote an IP literal and nothing else - a hostname cannot
contain them - so a bracketed string that is not an IPv6 address is now
an error rather than a name to look up.

Found while testing an outbound against a dual-stack server: the same
address worked without brackets and failed with them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both request forms split the authority on the first ':'. For the
bracketed IPv6 form an HTTP client sends - CONNECT [2001:db8::1]:443 -
that colon is inside the address, so the host came out as "[2001" and
the port failed to parse. Every IPv6 literal was unreachable through the
HTTP inbound.

Both sites now use NetLocation::from_str, which knows where the address
ends: None for CONNECT, which has no default port, and Some(80) for an
absolute http:// URL, which may leave the port out. That also drops the
hand-rolled bounds check the CONNECT path carried.

The file had no tests for request parsing. There are now five, driving
the handler over a loopback pair and asserting the destination it picks;
they cover both request forms, both address kinds, the default port and
the missing-port rejection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@AYastrebov
AYastrebov marked this pull request as ready for review August 24, 2026 08:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant