Skip to content

splithttp: bind sendThrough=origin to the real per-connection local IP#6476

Open
echoowall wants to merge 2 commits into
XTLS:mainfrom
echoowall:fix-xhttp-origin-xtls
Open

splithttp: bind sendThrough=origin to the real per-connection local IP#6476
echoowall wants to merge 2 commits into
XTLS:mainfrom
echoowall:fix-xhttp-origin-xtls

Conversation

@echoowall

Copy link
Copy Markdown

Problem

With sendThrough: "origin" (source-in-source-out), the outbound gateway is derived from inbound.Local, which is set from conn.LocalAddr() of the accepted connection (app/proxyman/inbound/worker.go).

For the XHTTP (splithttp) inbound, every accepted connection's LocalAddr is set to h.localAddr, which is the listener address (l.listener.Addr()). On a wildcard listener that is the unspecified address ([::] or 0.0.0.0).

Consequences on a multi-IP host:

  • sendThrough: origin reads the wildcard [::] for every connection, so all egress is bound to a single (often IPv6) source address regardless of which local IP the client actually connected to — source-in-source-out silently stops working.
  • On hosts whose [::] has no route, the outbound fails entirely: the REALITY handshake and VLESS tunnel come up, but no data flows (network is unreachable).

TCP inbounds are unaffected because worker.go already uses the real conn.LocalAddr(); only XHTTP substitutes the listener address.

Fix

Read the concrete per-connection local address from the request context via http.LocalAddrContextKey, which net/http populates with the address the client actually connected to. Fall back to the listener address when the key is absent (e.g. HTTP/3, where net/http does not set it), preserving current behavior there.

Verification

Confirmed under HTTP/2 on a wildcard [::] listener that request.Context().Value(http.LocalAddrContextKey) returns the concrete IP the client connected to (different entry IP → different value), whereas l.listener.Addr() is always [::].

End-to-end on a 5-IP host running sendThrough: origin + VLESS/REALITY/XHTTP: before the patch all egress bound [::] (broken on a host without IPv6 route); after the patch each entry IP egresses via the matching source IP (.38→.38, .49→.49, …), all reachable.

echoowall and others added 2 commits July 12, 2026 03:39
The XHTTP inbound set every accepted connection's LocalAddr to the
listener's address (h.localAddr = l.listener.Addr()). On a wildcard
listener that is the unspecified address ("[::]" / "0.0.0.0").

sendThrough "origin" derives the outbound gateway from inbound.Local,
which comes from conn.LocalAddr(). So with XHTTP every connection's
egress was bound to the wildcard, collapsing all entry IPs onto one
(often IPv6) source address and breaking source-in-source-out on
multi-IP hosts (and failing outright when that address has no route).

Read the concrete per-connection local address from the request context
(http.LocalAddrContextKey), which net/http populates with the address
the client actually connected to. Fall back to the listener address when
the key is absent (e.g. HTTP/3, where net/http does not set it).
@Fangliding

Copy link
Copy Markdown
Member

哎 小修复

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