XTLS Vision: Defer Splice handoff until write completes#5737
Conversation
|
@HeXis-YS thanks for your work. |
There was a problem hiding this comment.
Pull request overview
This PR aims to eliminate an intermittent XTLS Vision splice handoff race (reported in #4878) by making splice enablement happen deterministically only after the relevant direct write completes, and by refactoring splice-copy state transitions behind session helper methods.
Changes:
- Adds a session-level splice readiness signal (
spliceCopySignal) and helper methods (ArmSpliceCopy,EnableSpliceCopy,DisableSpliceCopy,SpliceCopyReady). - Updates many inbounds/outbounds to use the new splice-copy helper methods instead of mutating
CanSpliceCopydirectly. - Adjusts Vision splice handoff logic in
proxy/proxy.goto defer enabling splice until afterWriteMultiBufferreturns, and updates splice gating checks to useSpliceCopyReady/State.
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
common/session/splice.go |
Introduces splice readiness signaling + helper APIs for Inbound/Outbound splice-copy state. |
common/session/session.go |
Adds internal spliceCopy field to Inbound/Outbound session metadata. |
proxy/proxy.go |
Defers Vision splice enable until after write completion; updates splice checks to use new helpers. |
proxy/socks/server.go |
Uses new splice-copy helpers for arming/enabling/disabling splice. |
proxy/socks/client.go |
Uses new splice-copy helpers for arming/enabling splice during request/response handling. |
proxy/http/server.go |
Uses new splice-copy helpers for arming/enabling/disabling splice around CONNECT handling. |
proxy/http/client.go |
Uses new splice-copy helpers for arming/enabling splice in response path. |
proxy/vless/inbound/inbound.go |
Replaces direct CanSpliceCopy manipulation with helper methods in flow handling. |
proxy/vless/outbound/outbound.go |
Replaces direct CanSpliceCopy manipulation with helper methods in flow handling. |
proxy/vmess/inbound/inbound.go |
Disables splice via helper method. |
proxy/vmess/outbound/outbound.go |
Disables splice via helper method. |
proxy/trojan/server.go |
Disables splice via helper method. |
proxy/trojan/client.go |
Disables splice via helper method. |
proxy/shadowsocks/server.go |
Disables splice via helper method. |
proxy/shadowsocks/client.go |
Disables splice via helper method. |
proxy/shadowsocks_2022/inbound.go |
Disables splice via helper method. |
proxy/shadowsocks_2022/inbound_multi.go |
Disables splice via helper method. |
proxy/shadowsocks_2022/inbound_relay.go |
Disables splice via helper method. |
proxy/shadowsocks_2022/outbound.go |
Disables splice via helper method. |
proxy/hysteria/server.go |
Disables splice via helper method. |
proxy/hysteria/client.go |
Disables splice via helper method. |
proxy/wireguard/server.go |
Uses helper method to disable splice; minor formatting alignment in routingInfo. |
proxy/wireguard/client.go |
Uses helper method to disable splice. |
proxy/freedom/freedom.go |
Uses helper method to enable splice. |
proxy/dokodemo/dokodemo.go |
Uses helper method to enable splice. |
common/mux/server.go |
Disables splice for mux connections via helper method. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
common/session/splice.go
Outdated
| func (i *Inbound) EnableSpliceCopy() { | ||
| if i.CanSpliceCopy == 3 { | ||
| return | ||
| } | ||
| if i.spliceCopy != nil { | ||
| i.spliceCopy.once.Do(func() { | ||
| close(i.spliceCopy.ready) | ||
| }) | ||
| } | ||
| i.CanSpliceCopy = 1 |
There was a problem hiding this comment.
EnableSpliceCopy / SpliceCopyReady access i.spliceCopy multiple times without any synchronization. Since these flags are checked/updated across goroutines (splice loop vs protocol handlers), DisableSpliceCopy() can nil spliceCopy between the nil-check and subsequent field access, leading to a potential nil-pointer panic (and still a data race). Consider (1) capturing sc := i.spliceCopy once and only using sc (including inside the once.Do closure), and (2) adding proper synchronization (e.g., atomic.Pointer + atomic.Int32, or a mutex) so CanSpliceCopy/spliceCopy updates are race-free.
common/session/splice.go
Outdated
| func (i *Inbound) SpliceCopyReady() bool { | ||
| if i.spliceCopy != nil { | ||
| select { | ||
| case <-i.spliceCopy.ready: | ||
| return true | ||
| default: | ||
| return false | ||
| } |
There was a problem hiding this comment.
SpliceCopyReady checks i.spliceCopy != nil and then reads i.spliceCopy.ready again in the select. If another goroutine calls DisableSpliceCopy() concurrently, this can panic due to i.spliceCopy becoming nil between those two reads. At minimum, read the pointer once into a local variable before the select; ideally also guard reads/writes to spliceCopy/CanSpliceCopy with atomics or a mutex.
|
@yuhan6665 |
|
|
|
@HeXis-YS sorry but I can't understand what you added for the ready channel. I don't think it does anything more since there is no other new code signal than the existing place to call EnableSpliceCopy() Maybe we can do a beta release and see if the issue is fixed. |
|
话说vless入站不是早就改成dispatchlink不走pipe了吗 就一个gouroutine在搬东西 还有竞争? |
|
Done. |
Seems you are right, let's give it a try |
9529a82 to
2320416
Compare
Summary
This PR addresses an intermittent Vision handoff race that can lead to TLS stream breakage (
ERR_SSL_PROTOCOL_ERROR), as reported in #4878.Root Cause
In
VisionWriter.WriteMultiBuffer,inbound.CanSpliceCopycould become1before the current direct-write buffer had fully flushed.That allowed splice mode to start while Vision was still writing to the same socket, creating a race at the handoff boundary.
Changes
VisionWriter.WriteMultiBufferinproxy/proxy.go:w.Writer.WriteMultiBuffer(mb)first;inbound.CanSpliceCopy = 1(if still eligible).This keeps existing Vision behavior intact while making splice handoff ordering deterministic.