Skip to content

Commit 07460d1

Browse files
committed
group: Fix interrupt_exist_connections for routed connections
Connections routed to a selector (route target or final) enter through the ConnectionHandler path (NewConnection/NewPacketConnection), which never goes through Selector.DialContext/ListenPacket, so they were never registered in the interrupt group. As a result, SelectOutbound interrupted nothing and existing connections kept flowing over the previously selected outbound. Register the inbound connection in the interrupt group as an external connection on both handler branches, so switching the selected outbound interrupts routed connections when interrupt_exist_connections is enabled, while leaving them untouched by default. Fixes #4281.
1 parent 6d87a9c commit 07460d1

2 files changed

Lines changed: 203 additions & 0 deletions

File tree

protocol/group/selector.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ func (s *Selector) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
162162

163163
func (s *Selector) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
164164
ctx = interrupt.ContextWithIsExternalConnection(ctx)
165+
conn = s.interruptGroup.NewConn(conn, true)
165166
selected := s.selected.Load()
166167
if outboundHandler, isHandler := selected.(adapter.ConnectionHandler); isHandler {
167168
outboundHandler.NewConnection(ctx, conn, metadata, onClose)
@@ -172,6 +173,7 @@ func (s *Selector) NewConnection(ctx context.Context, conn net.Conn, metadata ad
172173

173174
func (s *Selector) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
174175
ctx = interrupt.ContextWithIsExternalConnection(ctx)
176+
conn = s.interruptGroup.NewSingPacketConn(conn, true)
175177
selected := s.selected.Load()
176178
if outboundHandler, isHandler := selected.(adapter.PacketConnectionHandler); isHandler {
177179
outboundHandler.NewPacketConnection(ctx, conn, metadata, onClose)

test/group_test.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"errors"
6+
"net"
7+
"net/netip"
8+
"testing"
9+
"time"
10+
11+
C "github.com/sagernet/sing-box/constant"
12+
"github.com/sagernet/sing-box/option"
13+
"github.com/sagernet/sing-box/protocol/group"
14+
"github.com/sagernet/sing/common"
15+
"github.com/sagernet/sing/common/json/badoption"
16+
M "github.com/sagernet/sing/common/metadata"
17+
N "github.com/sagernet/sing/common/network"
18+
"github.com/sagernet/sing/protocol/socks"
19+
20+
"github.com/stretchr/testify/require"
21+
)
22+
23+
func startSelectorInstance(t *testing.T, interruptExistConnections bool) *group.Selector {
24+
instance := startInstance(t, option.Options{
25+
Inbounds: []option.Inbound{
26+
{
27+
Type: C.TypeSOCKS,
28+
Tag: "socks-in",
29+
Options: &option.SocksInboundOptions{
30+
ListenOptions: option.ListenOptions{
31+
Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
32+
ListenPort: clientPort,
33+
},
34+
},
35+
},
36+
},
37+
Outbounds: []option.Outbound{
38+
{
39+
Type: C.TypeSelector,
40+
Tag: "sel",
41+
Options: &option.SelectorOutboundOptions{
42+
Outbounds: []string{"memA", "memB"},
43+
Default: "memA",
44+
InterruptExistConnections: interruptExistConnections,
45+
},
46+
},
47+
{
48+
Type: C.TypeDirect,
49+
Tag: "memA",
50+
},
51+
{
52+
Type: C.TypeDirect,
53+
Tag: "memB",
54+
},
55+
},
56+
Route: &option.RouteOptions{
57+
Final: "sel",
58+
},
59+
})
60+
outbound, loaded := instance.Outbound().Outbound("sel")
61+
require.True(t, loaded)
62+
selector, isSelector := outbound.(*group.Selector)
63+
require.True(t, isSelector)
64+
return selector
65+
}
66+
67+
func startTCPEchoServer(t *testing.T, port uint16) {
68+
listener, err := net.ListenTCP("tcp", &net.TCPAddr{IP: localIP.AsSlice(), Port: int(port)})
69+
require.NoError(t, err)
70+
t.Cleanup(func() {
71+
listener.Close()
72+
})
73+
go func() {
74+
for {
75+
conn, err := listener.Accept()
76+
if err != nil {
77+
return
78+
}
79+
go func() {
80+
buffer := make([]byte, 1024)
81+
for {
82+
n, err := conn.Read(buffer)
83+
if err != nil {
84+
conn.Close()
85+
return
86+
}
87+
_, err = conn.Write(buffer[:n])
88+
if err != nil {
89+
conn.Close()
90+
return
91+
}
92+
}
93+
}()
94+
}
95+
}()
96+
}
97+
98+
func startUDPEchoServer(t *testing.T, port uint16) chan net.Addr {
99+
packetConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: localIP.AsSlice(), Port: int(port)})
100+
require.NoError(t, err)
101+
t.Cleanup(func() {
102+
packetConn.Close()
103+
})
104+
sourceCh := make(chan net.Addr, 16)
105+
go func() {
106+
buffer := make([]byte, 1024)
107+
for {
108+
n, addr, err := packetConn.ReadFrom(buffer)
109+
if err != nil {
110+
close(sourceCh)
111+
return
112+
}
113+
sourceCh <- addr
114+
_, err = packetConn.WriteTo(buffer[:n], addr)
115+
if err != nil {
116+
return
117+
}
118+
}
119+
}()
120+
return sourceCh
121+
}
122+
123+
func tcpPingPong(t *testing.T, conn net.Conn) {
124+
_, err := conn.Write([]byte("ping"))
125+
require.NoError(t, err)
126+
buffer := make([]byte, 4)
127+
require.NoError(t, conn.SetReadDeadline(time.Now().Add(5*time.Second)))
128+
_, err = conn.Read(buffer)
129+
require.NoError(t, err)
130+
require.Equal(t, []byte("ping"), buffer)
131+
require.NoError(t, conn.SetReadDeadline(time.Time{}))
132+
}
133+
134+
func TestSelectorInterruptRoutedConnection(t *testing.T) {
135+
selector := startSelectorInstance(t, true)
136+
startTCPEchoServer(t, testPort)
137+
138+
dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "")
139+
conn, err := dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", testPort))
140+
require.NoError(t, err)
141+
defer conn.Close()
142+
tcpPingPong(t, conn)
143+
144+
require.True(t, selector.SelectOutbound("memB"))
145+
146+
require.NoError(t, conn.SetReadDeadline(time.Now().Add(3*time.Second)))
147+
_, err = conn.Read(make([]byte, 4))
148+
require.Error(t, err, "existing connection routed through selector must be interrupted")
149+
var netErr net.Error
150+
if errors.As(err, &netErr) {
151+
require.False(t, netErr.Timeout(), "existing connection routed through selector must be interrupted")
152+
}
153+
}
154+
155+
func TestSelectorInterruptRoutedPacketConnection(t *testing.T) {
156+
selector := startSelectorInstance(t, true)
157+
sourceCh := startUDPEchoServer(t, testPort)
158+
159+
dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "")
160+
packetConn, err := dialer.ListenPacket(context.Background(), M.ParseSocksaddrHostPort("127.0.0.1", testPort))
161+
require.NoError(t, err)
162+
defer packetConn.Close()
163+
164+
serverAddr := &net.UDPAddr{IP: localIP.AsSlice(), Port: int(testPort)}
165+
buffer := make([]byte, 1024)
166+
167+
_, err = packetConn.WriteTo([]byte("ping"), serverAddr)
168+
require.NoError(t, err)
169+
require.NoError(t, packetConn.SetReadDeadline(time.Now().Add(5*time.Second)))
170+
_, _, err = packetConn.ReadFrom(buffer)
171+
require.NoError(t, err)
172+
firstSource := <-sourceCh
173+
174+
require.True(t, selector.SelectOutbound("memB"))
175+
// The interrupted NAT session cannot signal the SOCKS client; instead verify
176+
// that a subsequent packet no longer flows over the old outbound socket.
177+
_, err = packetConn.WriteTo([]byte("ping"), serverAddr)
178+
require.NoError(t, err)
179+
require.NoError(t, packetConn.SetReadDeadline(time.Now().Add(3*time.Second)))
180+
_, _, err = packetConn.ReadFrom(buffer)
181+
if err == nil {
182+
secondSource := <-sourceCh
183+
require.NotEqual(t, firstSource.String(), secondSource.String(),
184+
"packet connection routed through selector must be interrupted")
185+
}
186+
}
187+
188+
func TestSelectorKeepRoutedConnection(t *testing.T) {
189+
selector := startSelectorInstance(t, false)
190+
startTCPEchoServer(t, testPort)
191+
192+
dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "")
193+
conn, err := dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", testPort))
194+
require.NoError(t, err)
195+
defer conn.Close()
196+
tcpPingPong(t, conn)
197+
198+
require.True(t, selector.SelectOutbound("memB"))
199+
200+
tcpPingPong(t, conn)
201+
}

0 commit comments

Comments
 (0)