Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 18 additions & 12 deletions nfqueue_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,11 @@ func newNFQueueHandler(options nfqueueOptions) (*nfqueueHandler, error) {
}, nil
}

func (h *nfqueueHandler) setVerdict(packetID uint32, verdict int, mark uint32) {
func (h *nfqueueHandler) setVerdict(packetID uint32, verdict int, mark uint32, existingMark uint32) {
var err error
if mark != 0 {
err = h.nfq.SetVerdictWithOption(packetID, verdict, nfqueue.WithMark(mark))
combined := (existingMark &^ AutoRedirectMarkMask) | mark
err = h.nfq.SetVerdictWithOption(packetID, verdict, nfqueue.WithMark(combined))
} else {
err = h.nfq.SetVerdict(packetID, verdict)
}
Expand Down Expand Up @@ -165,18 +166,23 @@ func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int {
payload := *attr.Payload

if len(payload) < header.IPv4MinimumSize {
h.setVerdict(packetID, nfqueue.NfAccept, 0)
h.setVerdict(packetID, nfqueue.NfAccept, 0, 0)
return 0
}

var existingMark uint32
if attr.Mark != nil {
existingMark = *attr.Mark
}

var srcAddr, dstAddr M.Socksaddr
var tcpOffset int

version := payload[0] >> 4
if version == 4 {
ipv4 := header.IPv4(payload)
if !ipv4.IsValid(len(payload)) || ipv4.Protocol() != uint8(unix.IPPROTO_TCP) {
h.setVerdict(packetID, nfqueue.NfAccept, 0)
h.setVerdict(packetID, nfqueue.NfAccept, 0, 0)
return 0
}
srcAddr = M.SocksaddrFrom(ipv4.SourceAddr(), 0)
Expand All @@ -185,20 +191,20 @@ func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int {
} else if version == 6 {
transportProto, transportOffset, ok := parseIPv6TransportHeader(payload)
if !ok || transportProto != unix.IPPROTO_TCP {
h.setVerdict(packetID, nfqueue.NfAccept, 0)
h.setVerdict(packetID, nfqueue.NfAccept, 0, 0)
return 0
}
ipv6 := header.IPv6(payload)
srcAddr = M.SocksaddrFrom(ipv6.SourceAddr(), 0)
dstAddr = M.SocksaddrFrom(ipv6.DestinationAddr(), 0)
tcpOffset = transportOffset
} else {
h.setVerdict(packetID, nfqueue.NfAccept, 0)
h.setVerdict(packetID, nfqueue.NfAccept, 0, 0)
return 0
}

if len(payload) < tcpOffset+header.TCPMinimumSize {
h.setVerdict(packetID, nfqueue.NfAccept, 0)
h.setVerdict(packetID, nfqueue.NfAccept, 0, 0)
return 0
}

Expand All @@ -208,7 +214,7 @@ func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int {

flags := tcp.Flags()
if !flags.Contains(header.TCPFlagSyn) || flags.Contains(header.TCPFlagAck) {
h.setVerdict(packetID, nfqueue.NfAccept, 0)
h.setVerdict(packetID, nfqueue.NfAccept, 0, 0)
return 0
}

Expand All @@ -220,13 +226,13 @@ func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int {
// the chain immediately, skipping any rules after the queue statement.
switch {
case errors.Is(pErr, ErrBypass):
h.setVerdict(packetID, nfqueue.NfRepeat, h.outputMark)
h.setVerdict(packetID, nfqueue.NfRepeat, h.outputMark, existingMark)
case errors.Is(pErr, ErrReset):
h.setVerdict(packetID, nfqueue.NfRepeat, h.resetMark)
h.setVerdict(packetID, nfqueue.NfRepeat, h.resetMark, existingMark)
case errors.Is(pErr, ErrDrop):
h.setVerdict(packetID, nfqueue.NfDrop, 0)
h.setVerdict(packetID, nfqueue.NfDrop, 0, 0)
default:
h.setVerdict(packetID, nfqueue.NfAccept, 0)
h.setVerdict(packetID, nfqueue.NfAccept, 0, 0)
}

return 0
Expand Down
5 changes: 5 additions & 0 deletions redirect.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ const (
DefaultAutoRedirectOutputMark = 0x2024
DefaultAutoRedirectResetMark = 0x2025
DefaultAutoRedirectNFQueue = 100

// AutoRedirectMarkMask defines which bits of the 32-bit mark field are
// reserved for auto_redirect loop prevention. Bits outside this mask
// (the upper 16) are available for routing_mark / WAN selection.
AutoRedirectMarkMask = 0x0000FFFF
)

type AutoRedirect interface {
Expand Down
94 changes: 26 additions & 68 deletions redirect_nftables.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,69 +213,29 @@ func (r *autoRedirect) setupNFTables() error {
},
},
})
inputMarkValue := binaryutil.NativeEndian.PutUint32(r.tunOptions.AutoRedirectInputMark)
outputMarkValue := binaryutil.NativeEndian.PutUint32(r.tunOptions.AutoRedirectOutputMark)
// If iface != tun AND (ct mark & mask) == InputMark → set InputMark on meta mark (preserving high bits)
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chainPreRoutingUDP,
Exprs: []expr.Any{
&expr.Meta{
Key: expr.MetaKeyIIFNAME,
Register: 1,
},
&expr.Cmp{
Op: expr.CmpOpNeq,
Register: 1,
Data: nftablesIfname(r.tunOptions.Name),
},
&expr.Ct{
Key: expr.CtKeyMARK,
Register: 1,
},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: binaryutil.NativeEndian.PutUint32(r.tunOptions.AutoRedirectInputMark),
},
&expr.Meta{
Key: expr.MetaKeyMARK,
Register: 1,
SourceRegister: true,
},
Exprs: append(append(append([]expr.Any{
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
&expr.Cmp{Op: expr.CmpOpNeq, Register: 1, Data: nftablesIfname(r.tunOptions.Name)},
}, maskedCtMarkCmp(expr.CmpOpEq, nftMarkMaskBytes, inputMarkValue)...),
maskedMetaMarkSet(inputMarkValue)...),
&expr.Counter{},
},
),
})
// If (ct mark & mask) != InputMark → set OutputMark (preserving high bits), copy to ct mark
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chainPreRoutingUDP,
Exprs: []expr.Any{
&expr.Ct{
Key: expr.CtKeyMARK,
Register: 1,
},
&expr.Cmp{
Op: expr.CmpOpNeq,
Register: 1,
Data: binaryutil.NativeEndian.PutUint32(r.tunOptions.AutoRedirectInputMark),
},
&expr.Immediate{
Register: 1,
Data: binaryutil.NativeEndian.PutUint32(r.tunOptions.AutoRedirectOutputMark),
},
&expr.Meta{
Key: expr.MetaKeyMARK,
Register: 1,
SourceRegister: true,
},
&expr.Meta{
Key: expr.MetaKeyMARK,
Register: 1,
},
&expr.Ct{
Key: expr.CtKeyMARK,
Register: 1,
SourceRegister: true,
},
Exprs: append(append(
maskedCtMarkCmp(expr.CmpOpNeq, nftMarkMaskBytes, inputMarkValue),
maskedMetaMarkSetWithCtCopy(outputMarkValue)...),
&expr.Counter{},
},
),
})
}

Expand Down Expand Up @@ -432,38 +392,36 @@ func (r *autoRedirect) nftablesAddPreMatchRules(nft *nftables.Conn, table *nftab
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chain,
Exprs: []expr.Any{
&expr.Meta{Key: expr.MetaKeyMARK, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: binaryutil.NativeEndian.PutUint32(r.effectiveOutputMark())},
Exprs: append(
maskedMetaMarkCmp(expr.CmpOpEq, nftMarkMaskBytes,
binaryutil.NativeEndian.PutUint32(r.effectiveOutputMark())),
&expr.Ct{Key: expr.CtKeyMARK, Register: 1, SourceRegister: true},
&expr.Counter{},
&expr.Verdict{Kind: expr.VerdictReturn},
},
),
})

// Reset mark: reject with TCP RST.
// When the NFQUEUE handler returns NF_REPEAT with the reset mark,
// the packet re-enters this chain and is rejected here.
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chain,
Exprs: []expr.Any{
&expr.Meta{Key: expr.MetaKeyMARK, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: binaryutil.NativeEndian.PutUint32(r.effectiveResetMark())},
Exprs: append(
maskedMetaMarkCmp(expr.CmpOpEq, nftMarkMaskBytes,
binaryutil.NativeEndian.PutUint32(r.effectiveResetMark())),
&expr.Counter{},
&expr.Reject{Type: unix.NFT_REJECT_TCP_RST},
},
),
})

// Already-tracked bypass connections: return immediately.
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chain,
Exprs: []expr.Any{
&expr.Ct{Key: expr.CtKeyMARK, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: binaryutil.NativeEndian.PutUint32(r.effectiveOutputMark())},
Exprs: append(
maskedCtMarkCmp(expr.CmpOpEq, nftMarkMaskBytes,
binaryutil.NativeEndian.PutUint32(r.effectiveOutputMark())),
&expr.Verdict{Kind: expr.VerdictReturn},
},
),
})

// TCP SYN: send to NFQUEUE for pre-match evaluation.
Expand Down
Loading