Skip to content

Fix HTTP-01 challenge for IPv6 literal addresses#377

Merged
mholt merged 1 commit intocaddyserver:masterfrom
SagerNet:fix/http01-ipv6-literal-addresses
Apr 21, 2026
Merged

Fix HTTP-01 challenge for IPv6 literal addresses#377
mholt merged 1 commit intocaddyserver:masterfrom
SagerNet:fix/http01-ipv6-literal-addresses

Conversation

@nekohasekai
Copy link
Copy Markdown
Contributor

@nekohasekai nekohasekai commented Apr 21, 2026

What changed

Strip square brackets from bare IPv6 literals in hostOnly() when net.SplitHostPort returns an error.

Why

For HTTP-01 validation against an IPv6 literal, the CA sends a Host header like [2001:db8::1] without a port.

hostOnly() returns that bracketed value, while challenge.Identifier.Value holds the bare IPv6 address, so the host check fails.

Validation

  • go test ./...

When the ACME CA sends an HTTP-01 challenge request to an IPv6 address,
the Host header is bracketed (e.g. [2001:db8::1]) without a port.
net.SplitHostPort fails on this input, causing hostOnly() to return the
bracketed form, which doesn't match the bare IP in challenge.Identifier.Value.

Strip brackets from bare IPv6 addresses in hostOnly().
@nekohasekai nekohasekai force-pushed the fix/http01-ipv6-literal-addresses branch from 2ba2129 to 68acab0 Compare April 21, 2026 09:05
@nekohasekai nekohasekai marked this pull request as ready for review April 21, 2026 09:11
@mholt
Copy link
Copy Markdown
Member

mholt commented Apr 21, 2026

Huh... is that even valid form? I thought [] only should be used when there's a port. And there's no ports in ACME challenge identifier values. Where are you seeing this?

@nekohasekai
Copy link
Copy Markdown
Contributor Author

Yes, it's valid form — see RFC 9112 §3.2: Host = uri-host [ ":" port ], and uri-host is defined in RFC 3986 §3.2.2 where IP-literal = "[" IPv6address "]". The brackets are part of the IP-literal grammar, not the port separator, so the port is independently optional.

It's exactly what Boulder produces. va/http.go wraps IPv6 identifiers in brackets and then constructs url.URL{Scheme: "http", Host: host, Path: path} without a port (httpPort is only handed to the dialer). When Go's net/http serializes that to the wire, the Host header is literally [2001:db8::1] — no port.

Reproducer:

req, _ := http.NewRequest("GET", "http://[2001:db8::1]/.well-known/acme-challenge/x", nil)
// req.Host == "[2001:db8::1]"
// wire: "Host: [2001:db8::1]"

Downstream this hits hostOnly() in certificates.go:651. net.SplitHostPort("[2001:db8::1]") returns missing port in address, so it falls through to return the bracketed input, and strings.EqualFold("[2001:db8::1]", "2001:db8::1") fails the DNS-rebinding check in solveHTTPChallenge.

Copy link
Copy Markdown
Member

@mholt mholt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Fair enough I guess, thank you

@mholt mholt merged commit 60d9d8b into caddyserver:master Apr 21, 2026
12 checks passed
mholt added a commit that referenced this pull request Apr 23, 2026
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.

2 participants