Skip to content

Sniffing: Add portsExcluded to skip the sniffer for given destination ports - #6661

Closed
rokokol wants to merge 1 commit into
XTLS:mainfrom
rokokol:sniffing/ports-excluded
Closed

Sniffing: Add portsExcluded to skip the sniffer for given destination ports#6661
rokokol wants to merge 1 commit into
XTLS:mainfrom
rokokol:sniffing/ports-excluded

Conversation

@rokokol

@rokokol rokokol commented Aug 21, 2026

Copy link
Copy Markdown

Adds portsExcluded to sniffing: a port list checked in the dispatcher before the sniffer reads anything, so a connection to a listed destination port takes the same path as sniffing being switched off.

Follows up on discussion #6654 — sending the patch since it is small enough to judge from the diff. Close it if the shape is unwanted.

The problem

Where the peer speaks first — SSH, IMAP, SMTP — the client sends nothing for the sniffer to read, so cacheDeadline in app/dispatcher/default.go burns down in full before the dispatch continues. The cost is fixed and paid on every such connection.

domainsExcluded and ipsExcluded do not help: both are consulted in shouldOverride, i.e. after the sniffer has already waited, and they only suppress the destination override. #5927 shows that placement is deliberate, which is why this is a separate knob rather than a change to those.

The change

  • ports_excluded in SniffingConfig (field 7), portsExcluded in the JSON config, taking the usual port-list syntax: "22,143,993-995"
  • SniffingRequest.ExcludeForPort as a net.MemoryPortList, filled by BuildSniffingRequest
  • one check in the dispatcher — shouldSniff(request, destination) replaces the bare request.Enabled test at both call sites
"sniffing": {
  "enabled": true,
  "destOverride": ["http", "tls"],
  "portsExcluded": "22,143,993-995"
}

Measurements

socks inbound → freedom outbound, against a loopback server that sends its banner on connect, 30 connections each, median from SOCKS5 CONNECT to the first byte:

setup median
direct, no proxy 0.2 ms
sniffing disabled 66.3 ms
sniffing on, port excluded 67.0 ms
sniffing on, port not excluded 268.1 ms

The excluded row lands on the sniffing-disabled row. Scope check on a single instance configured with "portsExcluded": "2222": port 2222 answers in 66.8 ms, port 2223 in 267.7 ms, so the exclusion stays on the listed ports.

Tests

  • shouldSniff table test in app/dispatcher — disabled, no exclusions, single port, both range ends, just past the range
  • BuildSniffingRequest with and without portsExcluded in app/proxyman; the nil case would panic in PortListFromProto without the guard
  • TestSniffingConfig_Build in infra/conf extended over the JSON → proto path

go build ./... and go test ./... pass and go run ./infra/vformat/main.go -mode check is clean. config.pb.go was regenerated with protoc v33.5 and protoc-gen-go v1.36.11, so its header still matches every other .pb.go for the check-proto job. One local failure, TestTCPLocalNameServer in app/dns, reproduces identically on a clean checkout here — network-dependent, unrelated.

Docs for the field belong in Xray-docs-next; happy to send that too if this lands.

…on ports

Sniffing costs a fixed ~200ms on every connection whose peer speaks
first - SSH, IMAP, SMTP - because the client sends nothing for the
sniffer to read, so cacheDeadline in app/dispatcher burns down in full
before the dispatch continues.

domainsExcluded and ipsExcluded do not help: both are consulted in
shouldOverride, after the sniffer has already waited, and they only
suppress the destination override. portsExcluded is checked before any
payload is read, so a listed port takes the same path as sniffing being
switched off.

Measured through a socks inbound and a freedom outbound against a
loopback server that sends its banner on connect, 30 connections each,
median time from CONNECT to the first byte:

    direct, no proxy                    0.2 ms
    sniffing disabled                  66.3 ms
    sniffing on, port excluded         67.0 ms
    sniffing on, port not excluded    268.1 ms

The last two rows are one instance configured with "portsExcluded":
"2222": port 2222 answers in 66.8 ms and port 2223 in 267.7 ms, so the
exclusion stays scoped to the ports listed.

Refs XTLS#6654
@Fangliding

Fangliding commented Aug 22, 2026

Copy link
Copy Markdown
Member

IAMP和SMTP很少被个人用户用作代理 专门的邮件用途完全可以关嗅探 ssh的并发也不多是典型的长时间连接 我觉得这点延迟就算了 主要是不想xxxxExcluded过于膨胀 如果目标已知那么合理使用 domainsExcluded 也行
以及ssh我记得并不是严格的server talk first 你可以找一个会事先发送自己的信息的

@Fangliding Fangliding closed this Aug 22, 2026
@rokokol

rokokol commented Aug 22, 2026

Copy link
Copy Markdown
Author

Fair on ssh, and I checked rather than argued: OpenSSH sends its ident first, without waiting for the server.

$ nc -l 127.0.0.1 2222      # listener stays silent
SSH-2.0-OpenSSH_10.4

22 bytes on connect, so the sniffers reject immediately, Sniff returns errUnknownContent, and the default: branch in sniffCall returns without spending cacheDeadline. My benchmark used a server that speaks first — it measures the deadline path correctly, but it does not describe ssh. Struck from the case.

What is left is protocols where the server genuinely opens: SMTP, IMAP, POP3, and also MySQL, PostgreSQL, FTP.

On turning sniffing off for mail — that works when mail has its own inbound. The setup I hit this on is a single client-facing inbound carrying everything, where there is nothing to split on: disabling sniffing there costs the domain routing it is enabled for. That is the case the field was meant for, and I did not state it clearly enough in the description.

Not asking to reopen — the call on the config surface is yours. Leaving this here for whoever arrives with the same setup.

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