RFA-289 · Case file with fixtures · Case 261 of 694 · Runtime evidence
IPv6 Socket Addresses Need Brackets Around the Address
IPv6 addresses contain colons, so a socket address uses bracketed address syntax such as [::1]:8080 to keep the port delimiter unambiguous. Parse the address and port separately when inputs arrive separately.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all targets with std networking
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- IPv6 addresses already contain colons, so socket text requires brackets around the address to make the following port delimiter unambiguous.
- First discriminating check
- Parse [::1]:8080 as a socket address, or parse the IPv6 address and u16 port separately and construct the typed endpoint.
I joined an IPv6 host and a port with a colon, exactly as I would for IPv4. The result looked like ::1:8080, and Rust refused to parse it as a socket address. Every character was valid, but the boundary between host and port was ambiguous.
The failing program tries to parse that text as SocketAddrV6. The repaired form is [::1]:8080.
Colons already belong to IPv6
An IPv4 socket string can use 127.0.0.1:8080 because the address itself has no colon. An IPv6 address uses colons between hexadecimal groups and can compress zero groups with ::.
Appending another colon does not reveal which part is the port. In ::1:8080, the final group can be read as part of the IPv6 address. Brackets establish a boundary:
[::1]:8080
| | | |
host port
The brackets are socket-address syntax. They are not part of the underlying Ipv6Addr value.
An IP address and a socket address are different values
::1 can be parsed as Ipv6Addr. It names the IPv6 loopback address but carries no transport port. [::1]:8080 can be parsed as SocketAddrV6, which combines address, port, flow information, and a scope ID.
SocketAddr is the enum accepting either IPv4 or IPv6 socket addresses. I use it when configuration can contain both families.
Confusing these layers leads to more than formatting bugs. A hostname, an IP address, and an endpoint with a port have different resolution and validation requirements. I preserve those distinctions in configuration types when possible.
Construct typed values when the pieces are already separate
If my program already receives an Ipv6Addr and a u16, formatting a string and parsing it again adds an unnecessary grammar boundary. SocketAddrV6::new constructs the typed value directly.
String parsing belongs at a text boundary such as a command-line argument, environment variable, or configuration file. Inside the program, carrying the structured type prevents repeated bracket handling and invalid intermediate strings.
When a user enters host and port in separate fields, I parse each field separately and combine them. This also gives better errors: “invalid port” is more helpful than a generic socket-address parse failure.
Do not add brackets blindly
A host might already be bracketed, might be an IPv4 address, or might be a DNS name. Wrapping every host in brackets can create invalid text for other forms. Splitting an arbitrary endpoint at the last colon is also fragile because IPv6 has many colons and a scope zone can add more syntax at system boundaries.
I either ask Rust to parse a complete documented socket-address form or keep host and port separate. Hand-written punctuation heuristics tend to become an incomplete network grammar.
For URLs, I use a URL parser rather than assuming socket-address rules cover schemes, user information, paths, query strings, or percent encoding. Bracketed IPv6 appears there too, but the complete grammar is larger.
Scope IDs need special attention
SocketAddrV6 also stores a flow label and scope ID. Link-local addresses may require interface scope to identify the intended network. A string that parses successfully can still be unusable if deployment configuration omits required routing context.
This is an operational boundary rather than only a parser detail. I test addresses representative of the target environment and surface bind or connect errors with the complete endpoint and operation, without leaking unrelated sensitive configuration.
I also avoid assuming that successful parsing performs DNS resolution or proves a port is reachable. Parsing validates representation. Binding, connecting, and name resolution are later operations with their own failures.
When logging an endpoint, I format the typed socket address instead of rebuilding its text. This preserves the brackets and gives the parser's canonical representation back to operators.
What I test
The repaired program parses [::1]:8080 and asserts both the loopback address and port. It separately confirms that ::1 is a valid plain IPv6 address.
My broader table includes IPv4 with a port, full and compressed IPv6, IPv6 with a port, zero and maximum ports, missing brackets, missing ports, hostnames when the API allows them, and environment-specific scoped addresses. I test formatting followed by parsing when round trips are part of storage or configuration.
The core principle is that separators only work when the contained grammar does not use them ambiguously. IPv6 already owns the colon. Brackets create the explicit host boundary required before a socket port can be appended.