From f926ee4aa0aafe905d96a294e747836897d80d6f Mon Sep 17 00:00:00 2001 From: HeXis-YS <40174982+HeXis-YS@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:48:33 -0700 Subject: [PATCH] XTLS Vision: Defer Splice handoff until write completes (#5737) Fixes https://github.com/XTLS/Xray-core/issues/4878 --- proxy/proxy.go | 66 ++++++++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/proxy/proxy.go b/proxy/proxy.go index ea8c3b06..dbd81d6b 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -322,6 +322,7 @@ func NewVisionWriter(writer buf.Writer, trafficState *TrafficState, isUplink boo func (w *VisionWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { var isPadding *bool var switchToDirectCopy *bool + var spliceReadyInbound *session.Inbound if w.isUplink { isPadding = &w.trafficState.Outbound.IsPadding switchToDirectCopy = &w.trafficState.Outbound.UplinkWriterDirectCopy @@ -333,7 +334,7 @@ func (w *VisionWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { if *switchToDirectCopy { if inbound := session.InboundFromContext(w.ctx); inbound != nil { if !w.isUplink && inbound.CanSpliceCopy == 2 { - inbound.CanSpliceCopy = 1 + spliceReadyInbound = inbound } // if w.isUplink && w.ob != nil && w.ob.CanSpliceCopy == 2 { // TODO: enable uplink splice // w.ob.CanSpliceCopy = 1 @@ -355,43 +356,51 @@ func (w *VisionWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { if *isPadding { if len(mb) == 1 && mb[0] == nil { mb[0] = XtlsPadding(nil, CommandPaddingContinue, &w.writeOnceUserUUID, true, w.ctx, w.testseed) // we do a long padding to hide vless header - return w.Writer.WriteMultiBuffer(mb) - } - isComplete := IsCompleteRecord(mb) - mb = ReshapeMultiBuffer(w.ctx, mb) - longPadding := w.trafficState.IsTLS - for i, b := range mb { - if w.trafficState.IsTLS && b.Len() >= 6 && bytes.Equal(TlsApplicationDataStart, b.BytesTo(3)) && isComplete { - if w.trafficState.EnableXtls { - *switchToDirectCopy = true + } else { + isComplete := IsCompleteRecord(mb) + mb = ReshapeMultiBuffer(w.ctx, mb) + longPadding := w.trafficState.IsTLS + for i, b := range mb { + if w.trafficState.IsTLS && b.Len() >= 6 && bytes.Equal(TlsApplicationDataStart, b.BytesTo(3)) && isComplete { + if w.trafficState.EnableXtls { + *switchToDirectCopy = true + } + var command byte = CommandPaddingContinue + if i == len(mb)-1 { + command = CommandPaddingEnd + if w.trafficState.EnableXtls { + command = CommandPaddingDirect + } + } + mb[i] = XtlsPadding(b, command, &w.writeOnceUserUUID, true, w.ctx, w.testseed) + *isPadding = false // padding going to end + longPadding = false + continue + } else if !w.trafficState.IsTLS12orAbove && w.trafficState.NumberOfPacketToFilter <= 1 { // For compatibility with earlier vision receiver, we finish padding 1 packet early + *isPadding = false + mb[i] = XtlsPadding(b, CommandPaddingEnd, &w.writeOnceUserUUID, longPadding, w.ctx, w.testseed) + break } var command byte = CommandPaddingContinue - if i == len(mb)-1 { + if i == len(mb)-1 && !*isPadding { command = CommandPaddingEnd if w.trafficState.EnableXtls { command = CommandPaddingDirect } } - mb[i] = XtlsPadding(b, command, &w.writeOnceUserUUID, true, w.ctx, w.testseed) - *isPadding = false // padding going to end - longPadding = false - continue - } else if !w.trafficState.IsTLS12orAbove && w.trafficState.NumberOfPacketToFilter <= 1 { // For compatibility with earlier vision receiver, we finish padding 1 packet early - *isPadding = false - mb[i] = XtlsPadding(b, CommandPaddingEnd, &w.writeOnceUserUUID, longPadding, w.ctx, w.testseed) - break + mb[i] = XtlsPadding(b, command, &w.writeOnceUserUUID, longPadding, w.ctx, w.testseed) } - var command byte = CommandPaddingContinue - if i == len(mb)-1 && !*isPadding { - command = CommandPaddingEnd - if w.trafficState.EnableXtls { - command = CommandPaddingDirect - } - } - mb[i] = XtlsPadding(b, command, &w.writeOnceUserUUID, longPadding, w.ctx, w.testseed) } } - return w.Writer.WriteMultiBuffer(mb) + if err := w.Writer.WriteMultiBuffer(mb); err != nil { + return err + } + if spliceReadyInbound != nil && spliceReadyInbound.CanSpliceCopy == 2 { + // Enable splice only after this write has completed to avoid racing + // concurrent direct writes to the same TCP connection. + spliceReadyInbound.CanSpliceCopy = 1 + } + return nil } // IsCompleteRecord Is complete tls data record @@ -744,7 +753,6 @@ func CopyRawConnIfExist(ctx context.Context, readerConn net.Conn, writerConn net errors.LogDebug(ctx, "CopyRawConn splice") statWriter, _ := writer.(*dispatcher.SizeStatWriter) //runtime.Gosched() // necessary - time.Sleep(time.Millisecond) // without this, there will be a rare ssl error for freedom splice timer.SetTimeout(24 * time.Hour) // prevent leak, just in case if inTimer != nil { inTimer.SetTimeout(24 * time.Hour)