Skip to content

Commit a850c4f

Browse files
committed
Add pre-matching support for auto redirect
1 parent 6516c2d commit a850c4f

File tree

9 files changed

+480
-12
lines changed

9 files changed

+480
-12
lines changed

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ module github.com/sagernet/sing-tun
33
go 1.24.7
44

55
require (
6+
github.com/florianl/go-nfqueue/v2 v2.0.2
67
github.com/go-ole/go-ole v1.3.0
78
github.com/google/btree v1.1.3
9+
github.com/mdlayher/netlink v1.7.2
810
github.com/sagernet/fswatch v0.1.1
911
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1
1012
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a
@@ -22,7 +24,6 @@ require (
2224
github.com/fsnotify/fsnotify v1.7.0 // indirect
2325
github.com/google/go-cmp v0.6.0 // indirect
2426
github.com/josharian/native v1.1.0 // indirect
25-
github.com/mdlayher/netlink v1.7.2 // indirect
2627
github.com/mdlayher/socket v0.4.1 // indirect
2728
github.com/pmezard/go-difflib v1.0.0 // indirect
2829
github.com/vishvananda/netns v0.0.4 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
22
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE=
4+
github.com/florianl/go-nfqueue/v2 v2.0.2/go.mod h1:VA09+iPOT43OMoCKNfXHyzujQUty2xmzyCRkBOlmabc=
35
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
46
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
57
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=

nfqueue_linux.go

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
//go:build linux
2+
3+
package tun
4+
5+
import (
6+
"context"
7+
"errors"
8+
"sync"
9+
"sync/atomic"
10+
11+
"github.com/sagernet/sing-tun/internal/gtcpip/header"
12+
E "github.com/sagernet/sing/common/exceptions"
13+
"github.com/sagernet/sing/common/logger"
14+
M "github.com/sagernet/sing/common/metadata"
15+
N "github.com/sagernet/sing/common/network"
16+
17+
"github.com/florianl/go-nfqueue/v2"
18+
"github.com/mdlayher/netlink"
19+
"golang.org/x/sys/unix"
20+
)
21+
22+
const nfqueueMaxPacketLen = 512
23+
24+
type nfqueueHandler struct {
25+
ctx context.Context
26+
cancel context.CancelFunc
27+
handler Handler
28+
logger logger.Logger
29+
nfq *nfqueue.Nfqueue
30+
queue uint16
31+
outputMark uint32
32+
resetMark uint32
33+
wg sync.WaitGroup
34+
closed atomic.Bool
35+
}
36+
37+
type nfqueueOptions struct {
38+
Context context.Context
39+
Handler Handler
40+
Logger logger.Logger
41+
Queue uint16
42+
OutputMark uint32
43+
ResetMark uint32
44+
}
45+
46+
func newNFQueueHandler(options nfqueueOptions) (*nfqueueHandler, error) {
47+
ctx, cancel := context.WithCancel(options.Context)
48+
return &nfqueueHandler{
49+
ctx: ctx,
50+
cancel: cancel,
51+
handler: options.Handler,
52+
logger: options.Logger,
53+
queue: options.Queue,
54+
outputMark: options.OutputMark,
55+
resetMark: options.ResetMark,
56+
}, nil
57+
}
58+
59+
func (h *nfqueueHandler) setVerdict(packetID uint32, verdict int, mark uint32) {
60+
var err error
61+
if mark != 0 {
62+
err = h.nfq.SetVerdictWithOption(packetID, verdict, nfqueue.WithMark(mark))
63+
} else {
64+
err = h.nfq.SetVerdict(packetID, verdict)
65+
}
66+
if err != nil && !h.closed.Load() && h.ctx.Err() == nil {
67+
h.logger.Trace(E.Cause(err, "set verdict"))
68+
}
69+
}
70+
71+
func (h *nfqueueHandler) Start() error {
72+
config := nfqueue.Config{
73+
NfQueue: h.queue,
74+
MaxPacketLen: nfqueueMaxPacketLen,
75+
MaxQueueLen: 4096,
76+
Copymode: nfqueue.NfQnlCopyPacket,
77+
AfFamily: unix.AF_UNSPEC,
78+
Flags: nfqueue.NfQaCfgFlagFailOpen,
79+
}
80+
81+
nfq, err := nfqueue.Open(&config)
82+
if err != nil {
83+
return E.Cause(err, "open nfqueue")
84+
}
85+
h.nfq = nfq
86+
87+
if err = nfq.SetOption(netlink.NoENOBUFS, true); err != nil {
88+
h.nfq.Close()
89+
return E.Cause(err, "set nfqueue option")
90+
}
91+
92+
h.wg.Add(1)
93+
go func() {
94+
defer h.wg.Done()
95+
err := nfq.RegisterWithErrorFunc(h.ctx, h.handlePacket, func(e error) int {
96+
if h.ctx.Err() != nil {
97+
return 1
98+
}
99+
h.logger.Error("nfqueue error: ", e)
100+
return 0
101+
})
102+
if err != nil && h.ctx.Err() == nil {
103+
h.logger.Error("nfqueue register error: ", err)
104+
}
105+
}()
106+
107+
return nil
108+
}
109+
110+
func parseIPv6TransportHeader(payload []byte) (transportProto uint8, transportOffset int, ok bool) {
111+
if len(payload) < header.IPv6MinimumSize {
112+
return 0, 0, false
113+
}
114+
115+
ipv6 := header.IPv6(payload)
116+
nextHeader := ipv6.NextHeader()
117+
offset := header.IPv6MinimumSize
118+
119+
for {
120+
switch nextHeader {
121+
case unix.IPPROTO_HOPOPTS,
122+
unix.IPPROTO_ROUTING,
123+
unix.IPPROTO_DSTOPTS:
124+
if len(payload) < offset+2 {
125+
return 0, 0, false
126+
}
127+
nextHeader = payload[offset]
128+
extLen := int(payload[offset+1]+1) * 8
129+
if len(payload) < offset+extLen {
130+
return 0, 0, false
131+
}
132+
offset += extLen
133+
134+
case unix.IPPROTO_FRAGMENT:
135+
if len(payload) < offset+8 {
136+
return 0, 0, false
137+
}
138+
nextHeader = payload[offset]
139+
offset += 8
140+
141+
case unix.IPPROTO_AH:
142+
if len(payload) < offset+2 {
143+
return 0, 0, false
144+
}
145+
nextHeader = payload[offset]
146+
extLen := int(payload[offset+1]+2) * 4
147+
if len(payload) < offset+extLen {
148+
return 0, 0, false
149+
}
150+
offset += extLen
151+
152+
case unix.IPPROTO_NONE:
153+
return 0, 0, false
154+
155+
default:
156+
return nextHeader, offset, true
157+
}
158+
}
159+
}
160+
161+
func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int {
162+
if h.closed.Load() {
163+
return 0
164+
}
165+
if attr.PacketID == nil || attr.Payload == nil {
166+
return 0
167+
}
168+
169+
packetID := *attr.PacketID
170+
payload := *attr.Payload
171+
172+
if len(payload) < header.IPv4MinimumSize {
173+
h.setVerdict(packetID, nfqueue.NfAccept, 0)
174+
return 0
175+
}
176+
177+
var srcAddr, dstAddr M.Socksaddr
178+
var tcpOffset int
179+
180+
version := payload[0] >> 4
181+
if version == 4 {
182+
ipv4 := header.IPv4(payload)
183+
if !ipv4.IsValid(len(payload)) || ipv4.Protocol() != uint8(unix.IPPROTO_TCP) {
184+
h.setVerdict(packetID, nfqueue.NfAccept, 0)
185+
return 0
186+
}
187+
srcAddr = M.SocksaddrFrom(ipv4.SourceAddr(), 0)
188+
dstAddr = M.SocksaddrFrom(ipv4.DestinationAddr(), 0)
189+
tcpOffset = int(ipv4.HeaderLength())
190+
} else if version == 6 {
191+
transportProto, transportOffset, ok := parseIPv6TransportHeader(payload)
192+
if !ok || transportProto != unix.IPPROTO_TCP {
193+
h.setVerdict(packetID, nfqueue.NfAccept, 0)
194+
return 0
195+
}
196+
ipv6 := header.IPv6(payload)
197+
srcAddr = M.SocksaddrFrom(ipv6.SourceAddr(), 0)
198+
dstAddr = M.SocksaddrFrom(ipv6.DestinationAddr(), 0)
199+
tcpOffset = transportOffset
200+
} else {
201+
h.setVerdict(packetID, nfqueue.NfAccept, 0)
202+
return 0
203+
}
204+
205+
if len(payload) < tcpOffset+header.TCPMinimumSize {
206+
h.setVerdict(packetID, nfqueue.NfAccept, 0)
207+
return 0
208+
}
209+
210+
tcp := header.TCP(payload[tcpOffset:])
211+
srcAddr = M.SocksaddrFrom(srcAddr.Addr, tcp.SourcePort())
212+
dstAddr = M.SocksaddrFrom(dstAddr.Addr, tcp.DestinationPort())
213+
214+
flags := tcp.Flags()
215+
if !flags.Contains(header.TCPFlagSyn) || flags.Contains(header.TCPFlagAck) {
216+
h.setVerdict(packetID, nfqueue.NfAccept, 0)
217+
return 0
218+
}
219+
220+
_, pErr := h.handler.PrepareConnection(N.NetworkTCP, srcAddr, dstAddr, nil, 0)
221+
222+
switch {
223+
case errors.Is(pErr, ErrBypass):
224+
h.setVerdict(packetID, nfqueue.NfAccept, h.outputMark)
225+
case errors.Is(pErr, ErrReset):
226+
h.setVerdict(packetID, nfqueue.NfAccept, h.resetMark)
227+
case errors.Is(pErr, ErrDrop):
228+
h.setVerdict(packetID, nfqueue.NfDrop, 0)
229+
default:
230+
h.setVerdict(packetID, nfqueue.NfAccept, 0)
231+
}
232+
233+
return 0
234+
}
235+
236+
func (h *nfqueueHandler) Close() error {
237+
h.closed.Store(true)
238+
h.cancel()
239+
if h.nfq != nil {
240+
h.nfq.Close()
241+
}
242+
h.wg.Wait()
243+
return nil
244+
}

redirect.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import (
55

66
"github.com/sagernet/sing/common/control"
77
"github.com/sagernet/sing/common/logger"
8-
N "github.com/sagernet/sing/common/network"
98

109
"go4.org/netipx"
1110
)
1211

1312
const (
1413
DefaultAutoRedirectInputMark = 0x2023
1514
DefaultAutoRedirectOutputMark = 0x2024
15+
DefaultAutoRedirectResetMark = 0x2025
16+
DefaultAutoRedirectNFQueue = 100
1617
)
1718

1819
type AutoRedirect interface {
@@ -24,7 +25,7 @@ type AutoRedirect interface {
2425
type AutoRedirectOptions struct {
2526
TunOptions *Options
2627
Context context.Context
27-
Handler N.TCPConnectionHandlerEx
28+
Handler Handler
2829
Logger logger.Logger
2930
NetworkMonitor NetworkUpdateMonitor
3031
InterfaceFinder control.InterfaceFinder

redirect_linux.go

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
E "github.com/sagernet/sing/common/exceptions"
1414
"github.com/sagernet/sing/common/logger"
1515
M "github.com/sagernet/sing/common/metadata"
16-
N "github.com/sagernet/sing/common/network"
1716
"github.com/sagernet/sing/common/x/list"
1817

1918
"go4.org/netipx"
@@ -22,7 +21,7 @@ import (
2221
type autoRedirect struct {
2322
tunOptions *Options
2423
ctx context.Context
25-
handler N.TCPConnectionHandlerEx
24+
handler Handler
2625
logger logger.Logger
2726
tableName string
2827
networkMonitor NetworkUpdateMonitor
@@ -41,6 +40,8 @@ type autoRedirect struct {
4140
suPath string
4241
routeAddressSet *[]*netipx.IPSet
4342
routeExcludeAddressSet *[]*netipx.IPSet
43+
nfqueueHandler *nfqueueHandler
44+
nfqueueEnabled bool
4445
}
4546

4647
func NewAutoRedirect(options AutoRedirectOptions) (AutoRedirect, error) {
@@ -125,13 +126,30 @@ func (r *autoRedirect) Start() error {
125126
listenAddr = netip.IPv4Unspecified()
126127
}
127128
server := newRedirectServer(r.ctx, r.handler, r.logger, listenAddr)
128-
err := server.Start()
129+
err = server.Start()
129130
if err != nil {
130131
return E.Cause(err, "start redirect server")
131132
}
132133
r.redirectServer = server
133134
}
134135
if r.useNFTables {
136+
var handler *nfqueueHandler
137+
handler, err = newNFQueueHandler(nfqueueOptions{
138+
Context: r.ctx,
139+
Handler: r.handler,
140+
Logger: r.logger,
141+
Queue: r.effectiveNFQueue(),
142+
OutputMark: r.effectiveOutputMark(),
143+
ResetMark: r.effectiveResetMark(),
144+
})
145+
if err != nil {
146+
r.logger.Warn("nfqueue not available, pre-match disabled: ", err)
147+
} else if err = handler.Start(); err != nil {
148+
r.logger.Warn("nfqueue start failed, pre-match disabled: ", err)
149+
} else {
150+
r.nfqueueHandler = handler
151+
r.nfqueueEnabled = true
152+
}
135153
r.cleanupNFTables()
136154
err = r.setupNFTables()
137155
} else {
@@ -142,6 +160,9 @@ func (r *autoRedirect) Start() error {
142160
}
143161

144162
func (r *autoRedirect) Close() error {
163+
if r.nfqueueHandler != nil {
164+
r.nfqueueHandler.Close()
165+
}
145166
if r.useNFTables {
146167
r.cleanupNFTables()
147168
} else {
@@ -181,3 +202,28 @@ func (r *autoRedirect) redirectPort() uint16 {
181202
}
182203
return M.AddrPortFromNet(r.redirectServer.listener.Addr()).Port()
183204
}
205+
206+
func (r *autoRedirect) effectiveOutputMark() uint32 {
207+
if r.tunOptions.AutoRedirectOutputMark != 0 {
208+
return r.tunOptions.AutoRedirectOutputMark
209+
}
210+
return DefaultAutoRedirectOutputMark
211+
}
212+
213+
func (r *autoRedirect) effectiveResetMark() uint32 {
214+
if r.tunOptions.AutoRedirectResetMark != 0 {
215+
return r.tunOptions.AutoRedirectResetMark
216+
}
217+
return DefaultAutoRedirectResetMark
218+
}
219+
220+
func (r *autoRedirect) effectiveNFQueue() uint16 {
221+
if r.tunOptions.AutoRedirectNFQueue != 0 {
222+
return r.tunOptions.AutoRedirectNFQueue
223+
}
224+
return DefaultAutoRedirectNFQueue
225+
}
226+
227+
func (r *autoRedirect) shouldSkipOutputChain() bool {
228+
return len(r.tunOptions.IncludeInterface) > 0 && !common.Contains(r.tunOptions.IncludeInterface, "lo") || common.Contains(r.tunOptions.ExcludeInterface, "lo")
229+
}

0 commit comments

Comments
 (0)