Skip to content

Concurrent REALITY dials send only the first TCP segment of a ClientHello #7306

Open

Description

@sinipelto

Integrity requirements

  • I have read all the comments in the issue template and ensured that this issue meet the requirements.
  • I confirm that I have read the documentation, understand the meaning of all the configuration items I wrote, and did not pile up seemingly useful options or default values.
  • I provided the complete config and logs, rather than just providing the truncated parts based on my own judgment.
  • I searched issues and did not find any similar issues.
  • The problem can be successfully reproduced in the latest Release

Description

Summary

A single VLESS REALITY dial with fingerprint chrome sends a complete ClientHello. A burst of dials from the same client leaves most connections with only the first TCP segment transmitted. The remainder is never sent and never retransmitted.

Client: Xray 26.10.29 (0afec66, go1.27.3 windows/amd64), v2rayN 7.25.6, Windows 10. Outbound is VLESS, network: raw, REALITY, xtls-rprx-vision, fingerprint chrome, mux off, VLESS encryption mlkem768x25519plus. Server: Xray 26.10.21. nginx 1.31.8 stream on public port 443 had ssl_preread on and proxy_protocol on, and forwarded that SNI to 127.0.0.1:8443. The inbound had acceptProxyProtocol true. preread_timeout is the default 30s. Path MSS was 1452.

The Chrome ClientHello is one TLS record of 1748 bytes. The SNI is inside the first 1452 bytes. The overflow is the tail of key_share (extension 51, 1263 bytes).

One connection through the same ssl_preread completes: 1452 bytes, then 264 bytes 1.7 ms later. nginx logs the SNI and Xray accepts the destination. firefox (1881 bytes) and safari (1529 bytes) also complete on a direct dial. The 517-byte fingerprints (ios, android, edge, 360, qq, random, randomized) are rejected with a real certificate. Every fingerprint this build accepts for REALITY is larger than the path MSS.

In a burst of a few hundred connections, about 280 stayed ESTABLISHED with Recv-Q 1452, bytes_received 1452, data_segs_in 1, and segs_in 3. No second segment and no retransmission. The window after the ACK was about 64 KB. Reading the 1452 bytes out later did not produce the tail. nginx waited 30s, logged an empty SNI, and sent the connection to the HTTPS vhost. Those sockets never appear in the Xray log.

nginx stream access for that client on the public 443 listener:

<client-ip> [31/Oct/2026:21:08:27 +0200] sni= -> nginx_https
<client-ip> [31/Oct/2026:21:24:05 +0200] sni=<reality server name> -> xray_reality
<client-ip> [31/Oct/2026:22:24:31 +0200] sni= -> nginx_https

Counts: 11793 empty SNI to the HTTPS vhost, 2180 sni=- to the same vhost, 117 with the REALITY name proxied to Xray. The empty line is repeated; those three lines are the distinct records. The stream error log has no line for the unfinished ClientHellos.

Single dial on a separate listener, same core and ssl_preread:

<client-ip> sni=<reality server name>
2026/10/31 23:06:19 [info] client <client-ip> connected to 0.0.0.0:18445
2026/10/31 23:06:19 [info] proxy 127.0.0.1 connected to 127.0.0.1:8443
2026/10/31 23:06:20 [info] client disconnected, bytes from/to client:9222/18819, bytes from/to upstream:18819/9222

Detailed info

A single VLESS REALITY dial with fingerprint chrome sends a complete ClientHello. A burst of dials from the same client leaves most connections with only the first segment transmitted. The remainder is never sent and never retransmitted.

Environment

  • Client: Xray 26.10.29 (0afec66, go1.27.3 windows/amd64), started by v2rayN 7.25.6 on Windows 10
  • Outbound: VLESS, network: raw, security: reality, flow: xtls-rprx-vision, fingerprint: chrome, mux off, VLESS encryption mlkem768x25519plus
  • Server: Xray 26.10.21. During the failure, nginx 1.31.8 stream listened on public port 443 with ssl_preread on and proxy_protocol on, and forwarded that SNI to 127.0.0.1:8443. preread_timeout is the default 30s. The inbound had acceptProxyProtocol true
  • Path MSS from the client SYN: 1452

The Chrome ClientHello is one TLS record of 1748 bytes (16 03 01, record length 1743). The SNI is inside the first 1452 bytes. The overflow is the tail of key_share (extension 51, 1263 bytes).

What happened

One connection through nginx 1.31.8 ssl_preread completed. The client sent 1452 bytes, then 264 bytes 1.7 ms later. nginx logged the SNI and proxied to Xray. Xray accepted the destination.

The same client, one dial at a time, directly to Xray:

Fingerprint ClientHello Second segment Result
chrome 1748 296 bytes, +3.8 ms REALITY accepted
firefox 1881 429 bytes, +4.2 ms REALITY accepted
safari 1529 77 bytes, +2.1 ms REALITY accepted
ios, android, edge, 360, qq, random, randomized 517 none rejected: real certificate, reported as MITM

chrome_120, firefox_120, safari_16, and ios_14 are not valid names in 26.10.29. Every fingerprint REALITY accepts is larger than this path MSS.

v2rayN then opened a few hundred connections to the nginx listener at once. About 280 stayed ESTABLISHED with Recv-Q 1452, bytes_received 1452, data_segs_in 1, and segs_in 3. No second segment arrived and the first was not retransmitted. The receive window after the ACK was about 64 KB. Reading those 1452 bytes out of the socket later did not make the client send the tail.

nginx held each socket until the 30s preread timeout, then logged an empty SNI and sent the connection to the HTTPS vhost. Xray logged no handshake error for those sockets, because the ClientHello never arrived whole. A few connections in the same burst did complete.

nginx logs

These are the stream access lines for that client while public port 443 was the preread proxy. The empty-SNI line is the same record; the file has 11793 copies of it. sni=- is 2180 copies. The REALITY name was logged 117 times.

<client-ip> [31/Oct/2026:21:08:27 +0200] sni= -> nginx_https
<client-ip> [31/Oct/2026:21:24:05 +0200] sni=<reality server name> -> xray_reality
<client-ip> [31/Oct/2026:22:24:31 +0200] sni= -> nginx_https

The single-dial test, on a separate nginx listener, port 18445, same ssl_preread and the same core:

<client-ip> sni=<reality server name>
2026/10/31 23:06:19 [info] client <client-ip> connected to 0.0.0.0:18445
2026/10/31 23:06:19 [info] proxy 127.0.0.1 connected to 127.0.0.1:8443
2026/10/31 23:06:20 [info] client disconnected, bytes from/to client:9222/18819, bytes from/to upstream:18819/9222

The stream error log has no line for these unfinished ClientHellos. Its only lines are the other client during Xray restarts.

Expected

Each concurrent REALITY dial writes the full ClientHello, as a single dial does. For chrome on this path that is two TCP segments, the second within a few milliseconds of the first.

Actual

Under a burst, most dials transmit exactly one MSS and never continue. ssl_preread waits for the whole TLS record before it reads the SNI (read the whole record before parsing), so those sockets sit until preread_timeout.

Reproduction Method

  1. Start the server config. It listens on 127.0.0.1:8443 and expects a PROXY header, matching the failing setup.
  2. Start this nginx in front of it. It is one complete file: the stream split from the failing setup, plus one HTTPS reverse-proxy vhost. preread_timeout is unset, so it stays 30s. www.example.com goes to Xray. Any other name, including an empty one, goes to 127.0.0.1:10443. Put a certificate and key at the two ssl_certificate paths before starting. nginx 1.31.8 accepted this file with nginx -t.
Details

worker_processes 1;
error_log /tmp/nginx-preread-error.log info;
pid /tmp/nginx-preread.pid;

events {
worker_connections 256;
}

stream {
map $ssl_preread_server_name $reality_upstream {
hostnames;
www.example.com xray_reality;
default nginx_https;
}

upstream xray_reality { server 127.0.0.1:8443; }
upstream nginx_https  { server 127.0.0.1:10443; }

log_format stream443 '$remote_addr [$time_local] sni=$ssl_preread_server_name -> $reality_upstream';

server {
    listen 18445;
    ssl_preread on;
    proxy_pass $reality_upstream;
    proxy_protocol on;
    proxy_timeout 7d;
    proxy_connect_timeout 5s;
    proxy_socket_keepalive on;
    access_log /tmp/nginx-preread-access.log stream443;
}

}

http {
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

map $server_port $forwarded_port {
    10443   443;
    default $server_port;
}

server {
    listen 127.0.0.1:10443 ssl proxy_protocol;
    listen [::1]:10443 ssl proxy_protocol;
    server_name app.example.com;

    real_ip_header proxy_protocol;
    set_real_ip_from 127.0.0.1;
    set_real_ip_from ::1;
    absolute_redirect off;
    port_in_redirect off;

    ssl_certificate /etc/nginx/cert.pem;
    ssl_certificate_key /etc/nginx/key.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Port $forwarded_port;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

}

  1. Start the client config. On another machine, change vnext.address to the server and point the port at the nginx listener. One request through 127.0.0.1:10808 completes. The second ClientHello segment arrives within a few milliseconds, nginx logs the SNI, and Xray accepts the destination.
  2. Open a few hundred connections at once through that same outbound. The captured failure was v2rayN in TUN mode, mux off, by reconnecting the tunnel or loading a page. Most sockets stay established with only the first 1452 bytes received. They do not appear in the Xray access log. After 30 seconds nginx logs an empty SNI.

Client config

Details

{
  "log": {
    "loglevel": "debug",
    "dnsLog": true
  },
  "inbounds": [
    {
      "listen": "127.0.0.1",
      "port": 10808,
      "protocol": "socks",
      "settings": {
        "udp": true
      },
      "sniffing": {
        "enabled": true,
        "destOverride": [
          "http",
          "tls",
          "quic"
        ]
      }
    }
  ],
  "outbounds": [
    {
      "protocol": "vless",
      "tag": "proxy",
      "settings": {
        "vnext": [
          {
            "address": "127.0.0.1",
            "port": 18445,
            "users": [
              {
                "id": "95c12c6f-cbd7-4426-aa62-ee1a138835d6",
                "encryption": "mlkem768x25519plus.native.0rtt.RY2etTg949FuamI2FwFJOUYt_fhqd91WJseEo9LRNyc",
                "flow": "xtls-rprx-vision"
              }
            ]
          }
        ]
      },
      "streamSettings": {
        "network": "raw",
        "security": "reality",
        "realitySettings": {
          "serverName": "www.example.com",
          "fingerprint": "chrome",
          "publicKey": "83Ze49cjQqKd5tihKPACggEnrLLjfKSToGEYDNW_qE4",
          "shortId": "137503bab98b4f1a",
          "spiderX": "/"
        }
      },
      "mux": {
        "enabled": false
      }
    }
  ]
}

Server config

Details

{
  "log": {
    "loglevel": "debug",
    "dnsLog": true
  },
  "inbounds": [
    {
      "listen": "127.0.0.1",
      "port": 8443,
      "protocol": "vless",
      "settings": {
        "clients": [
          {
            "id": "95c12c6f-cbd7-4426-aa62-ee1a138835d6",
            "flow": "xtls-rprx-vision",
            "email": "user"
          }
        ],
        "decryption": "mlkem768x25519plus.native.600s.OFiiXhxuI3BKSNBrQIIL6BUYCOleh90PedrUbsyOu0g"
      },
      "streamSettings": {
        "network": "raw",
        "security": "reality",
        "realitySettings": {
          "show": false,
          "target": "www.example.com:443",
          "serverNames": [
            "www.example.com"
          ],
          "privateKey": "qI9BO6s1CIAUApmmxjnnMkxrHlOIYKxqcCqQ2cwzE1w",
          "shortIds": [
            "137503bab98b4f1a"
          ]
        },
        "tcpSettings": {
          "acceptProxyProtocol": true,
          "header": {
            "type": "none"
          }
        }
      }
    }
  ],
  "outbounds": [
    {
      "protocol": "freedom",
      "tag": "direct"
    }
  ]
}

Client log

Details

No Xray-core client log exists for this failure.

The outbound was running at loglevel warning, and v2rayN 7.25.6 did not write the core log to a file. The panel log has no dial, REALITY, or ClientHello lines, so it is not included.

Server log

Details

Captured from the server core on 31 Oct 2026. The process was Xray 26.10.21 at loglevel warning, so this is not a debug trace. dnsLog was on. A debug rerun has not been captured.

The error log for that day is complete. It is only startup lines. There is no handshake error for the sockets that never sent the second segment, because those ClientHellos never reached Xray.

2026/10/31 19:27:18.872438 [Warning] transport/internet/tcp: accepting PROXY protocol
2026/10/31 19:27:18.873030 [Warning] core: Xray 26.10.21 started
2026/10/31 19:31:34.488423 [Warning] core: Xray 26.10.21 started
2026/10/31 20:58:50.954445 [Warning] transport/internet/tcp: accepting PROXY protocol
2026/10/31 20:58:50.954724 [Warning] core: Xray 26.10.21 started
2026/10/31 21:00:32.201279 [Warning] transport/internet/tcp: accepting PROXY protocol
2026/10/31 21:00:50.565875 [Warning] transport/internet/tcp: accepting PROXY protocol
2026/10/31 21:00:50.566079 [Warning] core: Xray 26.10.21 started
2026/10/31 21:02:45.531913 [Warning] transport/internet/tcp: accepting PROXY protocol
2026/10/31 21:02:45.532389 [Warning] core: Xray 26.10.21 started
2026/10/31 21:08:04.366207 [Warning] transport/internet/tcp: accepting PROXY protocol
2026/10/31 21:08:15.495144 [Warning] transport/internet/tcp: accepting PROXY protocol
2026/10/31 21:08:15.495623 [Warning] core: Xray 26.10.21 started
2026/10/31 22:24:22.552364 [Warning] core: Xray 26.10.21 started

The access log is every acceptance from the affected client between 21:24 and 21:48, plus the single-dial check at 23:06:19. Destination hosts are replaced with dest.example. The unfinished sockets are not in this log.

2026/10/31 21:24:03.996184 from x.x.x.x:22523 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:24:04.114164 from x.x.x.x:22525 accepted tcp:dest.example:80 [in-8443-tcp >> direct] email: user
2026/10/31 21:24:04.187543 from x.x.x.x:22520 accepted tcp:dest.example:80 [in-8443-tcp >> direct] email: user
2026/10/31 21:24:04.263625 from x.x.x.x:22519 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:24:31.441901 from x.x.x.x:20716 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:24:34.260264 from x.x.x.x:22506 accepted tcp:dest.example:7680 [in-8443-tcp >> direct] email: user
2026/10/31 21:24:38.511651 from x.x.x.x:16529 accepted tcp:dest.example:80 [in-8443-tcp >> direct] email: user
2026/10/31 21:24:38.777252 from x.x.x.x:31107 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:24:39.043615 from x.x.x.x:16539 accepted tcp:dest.example:80 [in-8443-tcp >> direct] email: user
2026/10/31 21:24:39.339649 from x.x.x.x:31118 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:24:39.366590 from x.x.x.x:31111 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:24:41.000678 from x.x.x.x:16578 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:24:46.190251 from x.x.x.x:22430 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:24:46.770644 from x.x.x.x:31135 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:24:46.864074 from x.x.x.x:31134 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:24:47.859062 from x.x.x.x:31132 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:24:47.876496 from x.x.x.x:31131 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:24:48.311962 from x.x.x.x:22440 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:24:50.142038 from x.x.x.x:22445 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:25:50.145914 from x.x.x.x:22429 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:25:58.237286 from x.x.x.x:22457 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:26:01.182980 from x.x.x.x:20698 accepted tcp:dest.example:80 [in-8443-tcp >> direct] email: user
2026/10/31 21:26:20.706019 from x.x.x.x:16581 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:26:23.370614 from x.x.x.x:22421 accepted tcp:dest.example:80 [in-8443-tcp >> direct] email: user
2026/10/31 21:26:31.828056 from x.x.x.x:22504 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:26:40.329045 from x.x.x.x:20735 accepted tcp:dest.example:80 [in-8443-tcp >> direct] email: user
2026/10/31 21:26:41.285147 from x.x.x.x:22482 accepted tcp:dest.example:80 [in-8443-tcp >> direct] email: user
2026/10/31 21:26:47.455779 from x.x.x.x:22492 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:26:48.137002 from x.x.x.x:16588 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:26:50.600301 from x.x.x.x:16576 accepted tcp:dest.example:80 [in-8443-tcp >> direct] email: user
2026/10/31 21:26:57.699052 from x.x.x.x:16541 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:26:58.542791 from x.x.x.x:22441 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:28:21.905638 from x.x.x.x:16532 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:37:36.763711 from x.x.x.x:16615 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:39:37.622719 from x.x.x.x:16548 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:42:02.576157 from x.x.x.x:16572 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 21:48:02.835189 from x.x.x.x:16521 accepted tcp:dest.example:443 [in-8443-tcp >> direct] email: user
2026/10/31 23:06:19.656742 from 127.0.0.1:55768 accepted tcp:example.com:443 [in-8443-tcp >> direct] email: user

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions