Skip to content

[feature request] XHTTP Support packet down & retransmission #4846

Open

Description

@hyperion-cs

Problem

A few weeks ago, a new method of blocking (especially on mobile networks) was introduced in Russia by the censor.
It works as follows. So, if:

  1. The client connects to the server via TCP using HTTPS and TLS 1.3 (Reality actually mimics HTTPS with TLS 1.3);
  2. The IP address of the server is “suspicious” (it is located outside (i.e. not in Russia), is part of the subnet AS which is owned by foreign data centers like Hetzner, Digital Ocean, etc.);
  3. Within one TCP connection (not within one HTTP request/response - this is important) the data size received from the server to the client is more than ≈15-20KB (the value varies depending on the provider) — the MAIN thing.

This connection is "frozen" (i.e. TCP packets from the server within this connection stop arriving after the specified limit on the size of transmitted data has been reached). It is curious to note that, as a rule, the censor (under the pretence of a response from the server) does not send, for example, an RST packet - instead, it simply “freezes” the TCP connection (from the client's point of view), and thus the client simply waits for an error on the timeout of waiting for a response from the server and everything stops working.

IMPORTANT: It doesn't matter whether it's "legitimate" HTTPS traffic or whether someone is mimicking it. They don't pay attention to that now.

Do I have these limitations?

You can check your "home" ISP with the RU :: TCP 16-20 checker from this repository. Works right from your browser, VPN should be disabled during the check.

Proposed Solution

The good news is that if you fragment the responses from the server into several ≈15-20KB data bundles (if it is large enough) in different TCP connections, everything will work fine. This is the option I propose to add to Reality (where the size of the data bundle will be set by a parameter), which would be compatible with VLESS.

It is important not to confuse this with fragmentation of TCP packets within one connection or splitting of transmitted data into several HTTP requests/responses (in one connection) - this is different and it won't help.

By the way, the same rules will probably apply to client-to-server traffic (if not already done by the censor) - so the fragmentation option described is worth adding on both server and client. And support for fragmented data, of course.

Notes

Yes, it will work noticeably slower than a single connection (especially if we're going to upload/download a large file). For example, it would take ~2560 TCP connections to download a 50MB file - that's a lot and quite suspicious. But it's clearly better than not working at all. Anyway, this issue will help to publicize the problem and maybe someone will think of better solutions.


Some links to related issues:

Activity

  1. hyperion-cs commented on Jun 27, 2025

    @hyperion-cs
    Author

    ❗ For those who are in Russia and suffer from the problem (VLESS/Reality): at the moment the only way I know of to get around this problem (without implementing the functionality above) is to use an intermediate server in Russia, which itself is not analyzed by the censor under the “new” rules. Probably, large data centers connected directly to the main communication channels ("магистральные каналы связи") would be suitable.

    This intermediate server should be configured as a transparent TCP proxy to the server with VLESS/Reality (which will already be out of the country, abroad). For this purpose you can use iptables, HAProxy, etc. Clients will have to change the host to the intermediate server (leave the other settings the same).

    upd: TLS 1.2 is also blocked (sometimes there is a different data size limit per TCP connection). However, SSH works without restrictions (it seems you can use SSH tunnel or mimic it).

  2. Fangliding commented on Jun 28, 2025

    @Fangliding
    Member

    20 kb就双向丢弃和被屏蔽了没有区别 这没有意义再去捞了
    你们需要的可能是发明一个只会在一条tcp连接上传输20kb的新协议而不是修改现有协议 不过即使是这样效率也会低到令人发指

  3. RPRX commented on Jul 6, 2025

    @RPRX
    Member

    最近有点无聊,想给 XHTTP 的下行也搞出上行那种流量聚合了,反正客户端 hub 可以大量复用上行的代码

  4. reopened this on Jul 6, 2025
  5. Fangliding commented on Jul 6, 2025

    @Fangliding
    Member

    虽然有packet down是好的 但是xhttp现在的上行机制至少也可以利用h2 mux h1.1 keepalive
    20kb只有xhttp默认最大post长度的2% 太小了 1MB的数据传下来要切成50个小部分还得通过50个独立的tcp连接接收 更别说xhttp无法从错误中恢复 这50个tcp连接任何一个被稍微阻断连接直接断开

  6. RPRX commented on Jul 6, 2025

    @RPRX
    Member

    其实我只是想给 XHTTP 加上聚合不同线路下载速度的能力,就像 mptcp,mekya 那些,但非常简单,具体效果几何需要实测,就像我说过的,如果单连接限速不明显,或者本来就是多线程下载,那就没什么提升,不过叠不同线路会不会对单线程有惊喜,可以试试

    就叫“多路径下行”吧,不光 TCP,还能叠上 UDP,实现这个 issue 所述的目标只是顺带的,如果不小心实现了 PLUX 也是顺带的

  7. RPRX commented on Jul 6, 2025

    @RPRX
    Member

    还有,一旦有了自定义的下行包装格式,HTTP 下行就可以保活了,不会被 CDN 100s 不活跃掐断,虽然我觉得这 100s 挺合理的

    还有玩得更花的,总是有人提“迁移现有连接”,以后再看看要不要补上 ACK 机制吧

    所以 XHTTP 终究还是不需要 mux.cool 了,XUDP 除外

  8. changed the title [-]`Reality` payload fragmentation over different TCP connections feature (especially important in Russia)[/-] [+][feature request] XHTTP Support packet down & retransmission[/+] on Jul 8, 2025
  9. zzlinwq commented on Jul 12, 2025

    @zzlinwq

    支持大佬 大佬比起马斯克有过之而无不及

  10. RPRX commented on Jul 31, 2025

    @RPRX
    Member
  11. Sebulba46 commented on Aug 4, 2025

    @Sebulba46

    ❗ For those who are in Russia and suffer from the problem (VLESS/Reality): at the moment the only way I know of to get around this problem (without implementing the functionality above) is to use an intermediate server in Russia, which itself is not analyzed by the censor under the “new” rules. Probably, large data centers connected directly to the main communication channels ("магистральные каналы связи") would be suitable.

    This intermediate server should be configured as a transparent TCP proxy to the server with VLESS/Reality (which will already be out of the country, abroad). For this purpose you can use iptables, HAProxy, etc. Clients will have to change the host to the intermediate server (leave the other settings the same).

    upd: TLS 1.2 is also blocked (sometimes there is a different data size limit per TCP connection). However, SSH works without restrictions (it seems you can use SSH tunnel or mimic it).

    So I'm trying to replicate idea with proxy server in Russia but I want to connect to proxy server through wireguard and then route traffic using iptables to the vless client config accepting connections on port 1080.
    My wireguard setup:

    [Interface]
    PrivateKey = ***
    Address = 10.20.10.1/24
    ListenPort = 51820
    PostUp = iptables -t nat -A PREROUTING -i wg0 -p tcp -j REDIRECT --to-port 1080
    PostUp = iptables -t nat -A PREROUTING -i wg0 -p udp -j REDIRECT --to-port 1080
    PostUp = iptables -t nat -A POSTROUTING -o ens1 -j MASQUERADE
    PostDown = iptables -t nat -D PREROUTING -i wg0 -p tcp -j REDIRECT --to-port 1080
    PostDown = iptables -t nat -D PREROUTING -i wg0 -p udp -j REDIRECT --to-port 1080
    PostDown = iptables -t nat -D POSTROUTING -o ens1 -j MASQUERADE
    DNS = 9.9.9.9
    
    
    [Peer]
    PublicKey = ***
    AllowedIPs = 10.20.10.2/32
    

    xray config:

    {
      "inbounds": [
        {
          "port": 1080,
          "listen": "127.0.0.1",
          "protocol": "socks",
          "settings": {
            "udp": true
          }
        }
      ],
      "outbounds": [
        {
          "protocol": "vless",
          "settings": {
            "vnext": [
              {
                "address": ***,
                "port": 443,
                "users": [
                  {
                    "id": ***,
                    "encryption": "none",
                    "flow": "xtls-rprx-vision"
                  }
                ]
              }
            ]
          },
          "streamSettings": {
            "network": "tcp",
            "security": "reality",
            "realitySettings": {
              "publicKey": ***,
              "fingerprint": "chrome",
              "serverName": "www.github.com",
              "shortId": "aabbccdd"
            }
          }
        },
        {
          "protocol": "freedom",
          "tag": "direct"
        }
      ],
      "routing": {
        "domainStrategy": "IPOnDemand",
        "rules": [
          {
            "type": "field",
            "ip": ["geoip:private"],
            "outboundTag": "direct"
          }
        ]
      }
    }
    

    Using this setup i can connect to the proxy server through wireguard, but forwarding is not working. And xray itselft is working fine: i can curl https://medium.com. Could you please help me?

  12. gfw-killer commented on Sep 27, 2025

    @gfw-killer

    the lack of Retransmission in XHTTP is a real issue

  13. iopq commented on Dec 12, 2025

    @iopq

    Using this setup i can connect to the proxy server through wireguard, but forwarding is not working. And xray itselft is working fine: i can curl https://medium.com . Could you please help me?

    Why are you using socks?

  14. 39 remaining items

  15. added a commit that references this issue on Sep 13, 2026
  16. Medium1992 commented on Sep 13, 2026

    @Medium1992

    I tested both approaches this thread discusses for the idle downlink cut.

    Keepalive over Mux.Cool works and is simple — the server just sends the existing SessionStatusKeepAlive frame — but it adds a small downstream frame every few seconds, a periodic pattern that isn't there in normal traffic.

    Resume (§6, prototyped in #6755) avoids that. Opening a fresh request after one ends is ordinary HTTP — browsers do it constantly — so a reconnect looks like normal traffic, not a signature, and it happens only reactively, after the CDN has already cut the stream, so there is no fixed interval to lock onto.

    On the "can't guarantee no byte loss" concern from #6755: it doesn't apply to the case this targets. The cut fires on an idle stream, so at that moment the server has sent nothing the client hasn't already received — the replay buffer is essentially empty, there is nothing to lose. Byte loss could only happen on a full-rate download outrunning the window, which is never when an idle cut fires. The two cases don't overlap.

    @RPRX @Fangliding worth reconsidering resume for this?

  17. RPRX commented on Sep 14, 2026

    @RPRX
    Member

    @Fangliding 话说那些重复开的 issue 不知道又是哪些 AI 智能体越狱了,直接删掉吧,群里的刷屏也清一下

  18. Fangliding commented on Sep 14, 2026

    @Fangliding
    Member

    这是蓄意破坏 故意复制的以前的 我开了交互限制但是没权限ban人和删帖(

  19. RPRX commented on Sep 14, 2026

    @RPRX
    Member

    不知道谁这么无聊,那还是别删了吧留存证据,估计每次都注册新号,ban 了也没用,把群里的刷屏删一下吧

  20. Fangliding commented on Sep 14, 2026

    @Fangliding
    Member

    又来了 这次是发评论 我限制到仅允许贡献者了 看着办吧

  21. RPRX commented on Sep 17, 2026

    @RPRX
    Member
  22. GIVLSE commented on Sep 26, 2026

    @GIVLSE

    Comment for XTLS/Xray-core #4846

    I have additional field evidence from a reproducible XHTTP packet-up / HTTP/2 / CDN failure on Xray 26.7.28.

    This looks related to #4846 and to the downlink failure mode discussed in PR #6755, but there is one important difference in our reproduction:

    the downlink is not only failing after a long idle period — an actively transferring response can reach 97–99.8% completion and then stall at the tail until the application timeout.

    Environment

    • Xray client: 26.7.28
    • Client platform: Linux / ARMv7
    • Transport: VLESS + XHTTP
    • XHTTP mode: packet-up
    • HTTP version: HTTP/2
    • Security: TLS
    • Path: client → TLS-terminating CDN/reverse proxy → origin Xray
    • Xray process remains alive during the failures

    Reproduction workload

    A playback-style test repeatedly downloads fixed 3,125,000-byte HTTPS segments through the tunnel.

    The first segments complete normally:

    segment 1: 3,125,000 bytes in 1121 ms
    segment 2: 3,125,000 bytes in  849 ms
    segment 3: 3,125,000 bytes in  815 ms
    

    Then stalls begin.

    Examples from failed requests:

    Attempt Bytes received Completion First byte Total time
    4 3,116,480 / 3,125,000 99.73% 118 ms 30,002 ms
    5 3,105,024 / 3,125,000 99.36% 416 ms 30,009 ms
    6 3,110,080 / 3,125,000 99.52% 553 ms 30,007 ms
    9 3,078,592 / 3,125,000 98.51% 122 ms 30,001 ms

    All of these requests:

    • receive HTTP 200
    • receive the first byte quickly
    • transfer almost the entire response body
    • then stop transferring the final part of the body
    • remain stuck until the 30-second client request timeout

    Across the failed playback requests, the received body before timeout was approximately:

    minimum: 97.38%
    median:  98.82%
    maximum: 99.76%
    

    The median missing tail was only about 37 KiB.

    Overall playback result:

    planned segments:     120
    request attempts:      34
    successful segments:   12
    failed requests:       22
    rebuffer events:        8
    completed:             false
    

    Separate long-session result

    A separate 30-minute stability test through the same tunnel produced:

    53 / 60 successful HTTPS checks
    7 TLS handshake timeouts
    ~8 seconds per timeout
    longest consecutive failure streak: 2
    

    Xray stayed alive during the whole run.

    Origin-side correlation

    We also collected bounded packet/socket evidence on the origin and correlated client failure windows with the actual outbound target sockets opened by Xray.

    For 15 / 15 captured failure windows:

    • Xray accepted the target connection
    • the target TCP connection was established
    • Xray sent data to the target
    • the target response payload arrived at the origin before the client-side timeout

    So in these failures the destination server had already responded and the response bytes had already reached the origin-side Xray process, while the client was still waiting and later timed out.

    This strongly suggests that the failure happens after the origin receives the target response, somewhere in the XHTTP downlink / intermediary / client delivery path.

    Client Xray log

    During the stalls, the client repeatedly logs:

    proxy/vless/outbound: failed to transfer response payload >
    stream error: stream ID ...; INTERNAL_ERROR; received from peer
    

    After the application times out and closes its local socket, follow-up errors such as:

    write: broken pipe
    

    also appear, which look like consequences of the timeout rather than the original cause.

    Why this may be relevant beyond idle-downlink cuts

    The failure mode discussed in #4846 / #6755 appears very close to what we see, especially the INTERNAL_ERROR; received from peer errors and the fact that the origin has already received the target response.

    However, our playback case shows something more specific:

    the XHTTP downlink can fail while the response is actively transferring, often only a few tens of KiB before the expected end of a multi-megabyte response.

    That may mean either:

    1. the same long-lived stream-down failure mode can also happen during active traffic, not only after a long idle period; or
    2. there is an additional response-tail / stream-finalization failure in the XHTTP packet-up downlink path through some CDN edges.

    I can provide a sanitized full client/server config, Xray debug logs, and the detailed per-failure trace evidence if that would help isolate the failure further.

  23. Medium1992 commented on Sep 26, 2026

    @Medium1992

    @GIVLSE As a stopgap, you could try the server build from my fork: https://github.com/Medium1992/Xray-core-fork/releases/tag/v26.9.9-mux-ka-4 (Xray 26.9.9, linux/amd64 and linux/arm64). The release notes describe the setup.

    It lets the origin send a Mux.Cool KeepAlive frame into an XHTTP downlink that has been idle for a while (packet-up and stream-up), so only the origin needs this build. The client can stay on stock Xray, but it must have mux enabled, since the keepalive is a Mux.Cool frame. Set muxKeepAliveSecs below the idle timeout your CDN applies to the downlink.

    If the tails still stall with a keepalive tuned to your CDN and mux on the client, then idling is most likely not the cause, which would fit your observation that the stream is cut while data is still flowing.

  24. Waretame commented on Oct 2, 2026

    @Waretame

    Following RPRX's direction in #6755 (#6755 (comment)): packet-down frames carried inside stream-down, header with optional session id excerpt, optional seq, type (padding/data), length, payload, and the first target is downlink keep-alive only.

    Is there a fuller write-up of the design somewhere, or is that comment the spec so far? Two things I'm not sure about:

    1. the session id "excerpt": is the idea to take the low bytes of the session id so the receiver can match a frame to its session cheaply?
    2. keep-alive padding: should it go only on the main stream-down, or also on extra download sessions when they exist?

    Meanwhile I'll start from that comment as is: a minimal implementation, frames in stream-down, keep-alive only, off by default, with knobs for period / length range / padding content, and adjust once there's a clearer picture.

  25. izislesar commented on Oct 15, 2026

    @izislesar
    Contributor

    I implemented the packet-down frame format from RPRX's design comment in #6755 (comment) and opened #7159.

    In short:

    • the stream-down response body is cut into packet-down frames with the five elements from the design comment: an omittable session id excerpt, an omittable seq, a mandatory type (data/padding), a mandatory length, and the payload;
    • the current use is the one the comment named first: downlink keep-alive. While the downlink has been idle for a full period, the server writes a padding frame instead of staying silent, so the CDN idle timer does not kill the GET while the uplink carries traffic;
    • it is opt-in and negotiated per request (the client sends X-Packet-Down on stream-down / stream-one requests, the server confirms in the response header), so servers and clients without the feature are byte-identical to today;
    • deliverably out of scope for now, exactly like packet-up today: no ACK, no reordering, no multi-path-down, no retransmission machinery, no obfs. The seq and the excerpt are in the format so these can come later without changing the wire again.

    h1 / h2 / h3 are covered, since the framers sit on the response body rather than on one transport. stream-one's single response body is framed the same way.

    It does not address the 15-20 KB per TCP connection cut from this issue yet; that needs the bigger packet-down uses later. Testing and review are welcome, especially from people with CDNs in front of the origin, since the target there is exactly the inactivity timer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions