From 229799a7a37a7477560630ef24aac0a1d634f6a7 Mon Sep 17 00:00:00 2001 From: solodecode <47976752+solodecode@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:02:55 +0500 Subject: [PATCH] Direct/Freedom outbound: Fix `blockDelay` for DispatchLink() inbounds such as VLESS, Hysteria, etc. (#7238) https://github.com/XTLS/Xray-core/pull/7238#pullrequestreview-5627787937 https://github.com/XTLS/Xray-core/pull/7238#issuecomment-6467092152 https://github.com/XTLS/Xray-core/pull/6060#issuecomment-4384511163 --- proxy/freedom/freedom.go | 15 ++++-- testing/scenarios/vless_test.go | 82 +++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 5 deletions(-) diff --git a/proxy/freedom/freedom.go b/proxy/freedom/freedom.go index 59973594a..85ae6eec7 100644 --- a/proxy/freedom/freedom.go +++ b/proxy/freedom/freedom.go @@ -229,14 +229,19 @@ func (h *Handler) blockDelay(rule *FinalRule) time.Duration { func (h *Handler) blackhole(ctx context.Context, input buf.Reader, output buf.Writer, rule *FinalRule, dest *net.Destination) error { delay := h.blockDelay(rule) errors.LogInfo(ctx, "blocked target: ", *dest, ", blackholing connection for ", delay) - timer := time.AfterFunc(delay, func() { + defer common.Close(output) + done := make(chan struct{}) + go func() { // a DispatchLink inbound's reader ignores Interrupt, so don't wait for this copy + _ = buf.Copy(input, buf.Discard) + close(done) + }() + select { + case <-done: + case <-time.After(delay): common.Interrupt(input) common.Interrupt(output) errors.LogInfo(ctx, "closed blackholed connection to blocked target: ", *dest) - }) - defer timer.Stop() - defer common.Close(output) - _ = buf.Copy(input, buf.Discard) + } return nil } diff --git a/testing/scenarios/vless_test.go b/testing/scenarios/vless_test.go index 1ff4809dd..9733a93b8 100644 --- a/testing/scenarios/vless_test.go +++ b/testing/scenarios/vless_test.go @@ -3,6 +3,8 @@ package scenarios import ( "encoding/base64" "encoding/hex" + "fmt" + "io" "sync" "testing" "time" @@ -20,6 +22,7 @@ import ( "github.com/xtls/xray-core/proxy/dokodemo" "github.com/xtls/xray-core/proxy/freedom" "github.com/xtls/xray-core/proxy/vless" + "github.com/xtls/xray-core/proxy/vless/encoding" "github.com/xtls/xray-core/proxy/vless/inbound" "github.com/xtls/xray-core/proxy/vless/outbound" "github.com/xtls/xray-core/testing/servers/tcp" @@ -657,3 +660,82 @@ func TestVlessRealityFingerprints(t *testing.T) { } wg.Wait() } + +// A blocked connection must be closed after blockDelay even while the client keeps it open and sends nothing. +func TestVlessBlockDelay(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + for _, delay := range []uint64{0, 1} { + t.Run(fmt.Sprint("blockDelay=", delay), func(t *testing.T) { + userID := protocol.NewID(uuid.New()) + serverPort := tcp.PickPort() + serverConfig := &core.Config{ + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}}, + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + Users: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vless.Account{ + Id: userID.String(), + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{ + FinalRules: []*freedom.FinalRuleConfig{ + { + Action: freedom.RuleAction_Block, + BlockDelay: &freedom.Range{Min: delay, Max: delay}, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig) + common.Must(err) + defer CloseAllServers(servers) + + conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{ + IP: []byte{127, 0, 0, 1}, + Port: int(serverPort), + }) + common.Must(err) + defer conn.Close() + + start := time.Now() + common.Must(encoding.EncodeRequestHeader(conn, &protocol.RequestHeader{ + Version: encoding.Version, + User: &protocol.MemoryUser{Account: &vless.MemoryAccount{ID: userID}}, + Command: protocol.RequestCommandTCP, + Address: dest.Address, + Port: dest.Port, + }, &encoding.Addons{})) + + blockDelay := time.Duration(delay) * time.Second + common.Must(conn.SetReadDeadline(time.Now().Add(blockDelay + 5*time.Second))) + _, err = io.Copy(io.Discard, conn) + elapsed := time.Since(start) + if err, ok := err.(net.Error); ok && err.Timeout() { + t.Fatalf("connection is still open after %v", elapsed) + } + if elapsed < blockDelay { + t.Fatalf("connection is closed after %v, before blockDelay", elapsed) + } + }) + } +}