Skip to content

XHTTP: packet-down in stream-down with downlink keep-alive - #7159

Closed
izislesar wants to merge 1 commit into
XTLS:mainfrom
izislesar:packet-down-keepalive
Closed

izislesar wants to merge 1 commit into
XTLS:mainfrom
izislesar:packet-down-keepalive

Conversation

@izislesar

@izislesar izislesar commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

XHTTP: packet-down in stream-down with downlink keep-alive

Follows RPRX's packet-down design in #6755 (comment), which was written while closing the earlier downlink-resume attempt (#6755). That attempt was rejected because "restore connection" is too complex, with latency, ACK and so on. This one does not attempt connection restoration at all. No reconnection, no state machine, nothing to tune: each stream-down GET stays a single request; if the GET dies, the connection dies the ordinary way.

Problem (from #4846)

CDNs kill a stream-down GET on body inactivity. This matters during uploads: with packet-up the uplink posts keep flowing, but the downlink proxy payload only arrives while the origin sends, so during a pure upload the GET idles and gets cut. Same story for any target that goes quiet for a while after responding.

What this PR adds

When enabled on both ends, the stream-down response body is cut into packet-down frames with the five elements from the design comment:

  • omittable session id excerpt: first 8 bytes of the session id, present on data and keep-alive frames (absent in stream-one where the session id is empty)
  • omittable packet seq: uint64, counting data/keep-alive frames per stream-down GET
  • mandatory type: data or padding
  • mandatory length: uint16
  • payload

Use today is the first one from the design comment: downlink keep-alive. While the downlink has been idle for a whole period, the server writes a padding frame (base62, in the spirit of tokenish x-padding) instead of staying silent. Everything the dispatcher sends rides in data frames, which is what lets the client tell padding apart from payload safely; the seq keeps the format ready for the future uses listed in the design comment (retransmission, multi-path-down) without another wire change.

h1, h2 and h3 are covered, since the framers sit on the body, not on a specific transport. stream-one gets the same treatment on its single response body. Browser dialer is out of scope for now: it has no place to read the response header that confirms the framing.

Negotiation

Strictly opt-in and per request, so old servers are unaffected and nothing breaks behind middleboxes:

  1. client config has packetDown: XHTTP stream requests not used as pure uplink (stream-down GET, stream-one request) carry X-Packet-Down: 1
  2. server has packetDown configured and sees the marker: it answers with X-Packet-Down: 1 on the response, and frames; without the confirmed header the client never parses frames
  3. any other combination: byte-identical wire format as today

Config

"xhttpSettings": {
  "path": "/sh",
  "packetDown": {              // absent = disabled, as today
    "keepAlive": true,
    "keepAlivePeriod": "10-30",  // seconds; padded when inactive this long. defaults to 10-30
    "keepAliveLength": "24-64"   // padding size. defaults to 24-64
  }
}

Server side is the same block in the inbound; the server only honors the feature when the client asks.

Not in this PR (deliberately)

  • obfs of the frames (later, per the design comment)
  • retransmission, multi-path-down, same-packet racing, ACK: the format carries seq and the session excerpt, but none of the machinery, exactly like packet-up carries seq today without using it
  • nothing about [feature request] XHTTP Support packet down & retransmission #4846's larger downlink freeze problem (the 15-20 KB per TCP connection cut): this only discards the CDN idle timer, it does not split data across connections

Tests

  • packet_down_test.go: frame round trip (with/without excerpt, with/without seq), byte-stream shaped like a socket, splitting payloads larger than one frame, padding skipped, malformed frame rejected, keep-alive written when idle, keep-alive suppressed while active, config normalization
  • splithttp_test.go: full listen/dial round trip with packet-down on both end (100 KiB), and the fallback case with the client setting present but the server without one
  • infra/conf: JSON unmarshal of the packetDown block, negative values rejected, absent block keeps the field unset
  • manual check: vless + xhttp h1 through both xray binaries, 3 MiB round trip matches sha256

Note: I opened this after #7135 (utls bump), which is currently parked until a tagged utls release per the discussion there.

Implements the packet-down frame format as specified for stream-down in
XTLS#6755 (comment 5720463978): an omittable session id excerpt, an omittable
packet seq, a mandatory type (data/padding), a mandatory length, and the
payload. The first use is downlink keep-alive, per XTLS#4846: the server sends
padding frames while the stream-down response is idle, so a middlebox idle
timer does not kill the GET while the uplink carries traffic.

packet-down is strictly opt-in and negotiated per request: the client sends
X-Packet-Down on stream-down / stream-one requests, the server only frames
when it has packetDown configured and the marker is present, and confirms
with the same header on the response. Everything stays wire-compatible for
clients and servers that do not ask for it.
@Fangliding

Copy link
Copy Markdown
Member

暂时不计划为了这种打断引入帧机制

@Fangliding Fangliding closed this Oct 15, 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