Skip to content

Commit 11776c6

Browse files
zxspiritclaude
andcommitted
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. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent d86eda1 commit 11776c6

4 files changed

Lines changed: 236 additions & 12 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/go.mod

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ require (
1212
github.com/gofrs/uuid/v5 v5.4.0
1313
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4
1414
github.com/sagernet/sing v0.8.12-0.20260702081104-2ded2af32d3d
15-
github.com/sagernet/sing-quic v0.6.2-0.20260525051024-9467ede27fb7
15+
github.com/sagernet/sing-quic v0.6.4-0.20260709034545-e23afe1172dc
1616
github.com/sagernet/sing-shadowsocks v0.2.8
1717
github.com/sagernet/sing-shadowsocks2 v0.2.1
1818
github.com/spyzhov/ajson v0.9.4
@@ -66,20 +66,26 @@ require (
6666
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
6767
github.com/google/btree v1.1.3 // indirect
6868
github.com/google/go-cmp v0.7.0 // indirect
69+
github.com/google/gopacket v1.1.19 // indirect
6970
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect
7071
github.com/google/uuid v1.6.0 // indirect
7172
github.com/hashicorp/yamux v0.1.2 // indirect
7273
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
74+
github.com/huin/goupnp v1.2.0 // indirect
7375
github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91 // indirect
76+
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
7477
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
7578
github.com/keybase/go-keychain v0.0.1 // indirect
7679
github.com/klauspost/compress v1.18.0 // indirect
7780
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
81+
github.com/koron/go-ssdp v0.0.4 // indirect
7882
github.com/kr/fs v0.1.0 // indirect
7983
github.com/libdns/acmedns v0.5.0 // indirect
8084
github.com/libdns/alidns v1.0.6 // indirect
8185
github.com/libdns/cloudflare v0.2.2 // indirect
8286
github.com/libdns/libdns v1.1.1 // indirect
87+
github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138 // indirect
88+
github.com/libp2p/go-netroute v0.2.1 // indirect
8389
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
8490
github.com/mdlayher/netlink v1.9.0 // indirect
8591
github.com/mdlayher/socket v0.5.1 // indirect
@@ -142,12 +148,12 @@ require (
142148
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 // indirect
143149
github.com/sagernet/sing-mux v0.3.5 // indirect
144150
github.com/sagernet/sing-shadowtls v0.2.1 // indirect
145-
github.com/sagernet/sing-snell v0.0.0-20260705044717-4e9e73be7814 // indirect
146-
github.com/sagernet/sing-tun v0.8.12-0.20260708091449-be1a05a4c962 // indirect
151+
github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb // indirect
152+
github.com/sagernet/sing-tun v0.8.12-0.20260711072757-9e647fcd6b38 // indirect
147153
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb // indirect
148154
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 // indirect
149155
github.com/sagernet/smux v1.5.50-sing-box-mod.1 // indirect
150-
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260706062137-ae2dde1295a3 // indirect
156+
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260711004045-7d3e867147ad // indirect
151157
github.com/sagernet/wireguard-go v0.0.5-0.20260706153856-2c27bbf4f97f // indirect
152158
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect
153159
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect

test/go.sum

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
108108
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
109109
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
110110
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
111+
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
112+
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
111113
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
112114
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
113115
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -118,8 +120,12 @@ github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8
118120
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
119121
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
120122
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
123+
github.com/huin/goupnp v1.2.0 h1:uOKW26NG1hsSSbXIZ1IR7XP9Gjd1U8pnLaCMgntmkmY=
124+
github.com/huin/goupnp v1.2.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
121125
github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91 h1:u9i04mGE3iliBh0EFuWaKsmcwrLacqGmq1G3XoaM7gY=
122126
github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91/go.mod h1:qfvBmyDNp+/liLEYWRvqny/PEz9hGe2Dz833eXILSmo=
127+
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
128+
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
123129
github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
124130
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
125131
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
@@ -130,6 +136,8 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt
130136
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
131137
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
132138
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
139+
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
140+
github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
133141
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
134142
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
135143
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -150,6 +158,10 @@ github.com/libdns/cloudflare v0.2.2 h1:XWHv+C1dDcApqazlh08Q6pjytYLgR2a+Y3xrXFu0v
150158
github.com/libdns/cloudflare v0.2.2/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60=
151159
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
152160
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
161+
github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138 h1:YohuNPT/1k3VcThCQlBZ43PCPWPfMRS1zcxWBF2SLK8=
162+
github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138/go.mod h1:TXQg5tfSy+bUjnhT5728j5j/MBj7keIYqqZ1+8k/ui8=
163+
github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU=
164+
github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ=
153165
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
154166
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
155167
github.com/mdlayher/netlink v1.9.0 h1:G8+GLq2x3v4D4MVIqDdNUhTUC7TKiCy/6MDkmItfKco=
@@ -283,26 +295,26 @@ github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 h1:3y6
283295
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3/go.mod h1:XEqEDYRCAYLaoPjZ1ifVWJg5iWAJHL2gOAXe/PM28Cg=
284296
github.com/sagernet/sing-mux v0.3.5 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONfhI=
285297
github.com/sagernet/sing-mux v0.3.5/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
286-
github.com/sagernet/sing-quic v0.6.2-0.20260525051024-9467ede27fb7 h1:hFLPJ21uNZSbRnzhOKz4Zv0b4F93mpDorWyN93BeRcM=
287-
github.com/sagernet/sing-quic v0.6.2-0.20260525051024-9467ede27fb7/go.mod h1:+oqD54aHel4ALKkp1hVXWCgLU/EjLojvm6AUzDfvj0I=
298+
github.com/sagernet/sing-quic v0.6.4-0.20260709034545-e23afe1172dc h1:zdc0fj4JdAdgAmQIoh7ZF+B/wPTEF2X75lYDqTmvlaw=
299+
github.com/sagernet/sing-quic v0.6.4-0.20260709034545-e23afe1172dc/go.mod h1:9k+dzGsWMttUGldBzq3dU792YHXzW6NgfbOGltnXq+0=
288300
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
289301
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
290302
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
291303
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
292304
github.com/sagernet/sing-shadowtls v0.2.1 h1:ZiHZdnEnP+YS73NMsxiZmIFCwNd0M4k7PkGCKNXhbaM=
293305
github.com/sagernet/sing-shadowtls v0.2.1/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
294-
github.com/sagernet/sing-snell v0.0.0-20260705044717-4e9e73be7814 h1:xfnkRpjVRVeJhVvDZA8PzTLlKGTb1o2kdI4uv1YymXo=
295-
github.com/sagernet/sing-snell v0.0.0-20260705044717-4e9e73be7814/go.mod h1:PcwzX/Xvqky0EP3kGt8OCjYb3R1pydenPHNQZcPZmXY=
296-
github.com/sagernet/sing-tun v0.8.12-0.20260708091449-be1a05a4c962 h1:dmJoWdTQygt4P2rAwScy2IvHnFp1mKrW6OsI0qig6O8=
297-
github.com/sagernet/sing-tun v0.8.12-0.20260708091449-be1a05a4c962/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ=
306+
github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb h1:VvU2/PZqP5tbKTDq0BxkhRO8ZnKI4UJzziakgBiP2Qg=
307+
github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb/go.mod h1:PcwzX/Xvqky0EP3kGt8OCjYb3R1pydenPHNQZcPZmXY=
308+
github.com/sagernet/sing-tun v0.8.12-0.20260711072757-9e647fcd6b38 h1:O+Ci3Re4JyPa19JqYqs116WOT29Nmo+QwTSlL3rV2Ao=
309+
github.com/sagernet/sing-tun v0.8.12-0.20260711072757-9e647fcd6b38/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ=
298310
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb h1:KEMbfexD4DvrQGYWwx6r+AwH9Veh8z6cnBZmtCS2G+0=
299311
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb/go.mod h1:D4CnJX3MNAAANhbQUxfIRgBdnvlTEaV7h6ojedcs+pw=
300312
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o=
301313
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY=
302314
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=
303315
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8=
304-
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260706062137-ae2dde1295a3 h1:eczvica8YiS5j3GfpHg6JG1Icur4Z2D6ffSrLZTfD1E=
305-
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260706062137-ae2dde1295a3/go.mod h1:p8Ms8FbGlwQJyHb862XmdShTS50fFJ8C71VdO6xvWyk=
316+
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260711004045-7d3e867147ad h1:WfqPcs43BJINn2IjQd10cLdd3lvJSanbk7MxGk6RTcA=
317+
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260711004045-7d3e867147ad/go.mod h1:p8Ms8FbGlwQJyHb862XmdShTS50fFJ8C71VdO6xvWyk=
306318
github.com/sagernet/wireguard-go v0.0.5-0.20260706153856-2c27bbf4f97f h1:TzN97RL07xWb3gZtmqFhsdkud4f6G/pohiaOLiqSBj4=
307319
github.com/sagernet/wireguard-go v0.0.5-0.20260706153856-2c27bbf4f97f/go.mod h1:hEqi4y5czEg6LYtX2Bpjg+lV0b/J1n+5rA885Z66Mx0=
308320
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
@@ -400,6 +412,8 @@ golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1i
400412
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
401413
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
402414
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
415+
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
416+
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
403417
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
404418
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
405419
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
@@ -443,6 +457,7 @@ golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
443457
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
444458
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
445459
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
460+
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
446461
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
447462
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
448463
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=

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)