Fix missing UDP and ICMP bypass for auto redirect

This commit is contained in:
世界 2026-07-10 12:11:47 +08:00
parent 80ea96e5b4
commit 99fb96a4c8
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
2 changed files with 231 additions and 85 deletions

View file

@ -5,6 +5,7 @@ package tun
import ( import (
"context" "context"
"errors" "errors"
"net/netip"
"sync/atomic" "sync/atomic"
"github.com/sagernet/sing-tun/internal/gtcpip/header" "github.com/sagernet/sing-tun/internal/gtcpip/header"
@ -100,51 +101,147 @@ func (h *nfqueueHandler) Start() error {
return nil return nil
} }
func parseIPv6TransportHeader(payload []byte) (transportProto uint8, transportOffset int, ok bool) { const ipv6AuthenticationHeaderIdentifier header.IPv6ExtensionHeaderIdentifier = 51
if len(payload) < header.IPv6MinimumSize {
return 0, 0, false type preMatchPacket struct {
protocol uint8
network string
source M.Socksaddr
destination M.Socksaddr
}
func parsePreMatchPacket(packet []byte) (preMatchPacket, bool) {
if len(packet) < 1 {
return preMatchPacket{}, false
}
var (
protocol uint8
transportOffset int
source netip.Addr
destination netip.Addr
)
switch header.IPVersion(packet) {
case header.IPv4Version:
if len(packet) < header.IPv4MinimumSize {
return preMatchPacket{}, false
}
ipHdr := header.IPv4(packet)
transportOffset = int(ipHdr.HeaderLength())
if transportOffset < header.IPv4MinimumSize || transportOffset > len(packet) || int(ipHdr.TotalLength()) < transportOffset || ipHdr.FragmentOffset() != 0 {
return preMatchPacket{}, false
}
protocol = uint8(ipHdr.TransportProtocol())
source = ipHdr.SourceAddr()
destination = ipHdr.DestinationAddr()
case header.IPv6Version:
if len(packet) < header.IPv6MinimumSize {
return preMatchPacket{}, false
}
ipHdr := header.IPv6(packet)
var ok bool
protocol, transportOffset, ok = parsePreMatchIPv6Transport(packet)
if !ok {
return preMatchPacket{}, false
}
source = ipHdr.SourceAddr()
destination = ipHdr.DestinationAddr()
default:
return preMatchPacket{}, false
} }
ipv6 := header.IPv6(payload) transport := packet[transportOffset:]
nextHeader := ipv6.NextHeader() parsed := preMatchPacket{protocol: protocol}
switch protocol {
case uint8(header.TCPProtocolNumber):
if len(transport) < header.TCPMinimumSize {
return preMatchPacket{}, false
}
tcpHdr := header.TCP(transport)
flags := tcpHdr.Flags()
if !flags.Contains(header.TCPFlagSyn) || flags.Contains(header.TCPFlagAck) {
return preMatchPacket{}, false
}
parsed.network = N.NetworkTCP
parsed.source = M.SocksaddrFrom(source, tcpHdr.SourcePort())
parsed.destination = M.SocksaddrFrom(destination, tcpHdr.DestinationPort())
case uint8(header.UDPProtocolNumber):
if len(transport) < header.UDPMinimumSize {
return preMatchPacket{}, false
}
udpHdr := header.UDP(transport)
if int(udpHdr.Length()) < header.UDPMinimumSize {
return preMatchPacket{}, false
}
parsed.network = N.NetworkUDP
parsed.source = M.SocksaddrFrom(source, udpHdr.SourcePort())
parsed.destination = M.SocksaddrFrom(destination, udpHdr.DestinationPort())
case uint8(header.ICMPv4ProtocolNumber):
if !source.Is4() || len(transport) < header.ICMPv4MinimumSize {
return preMatchPacket{}, false
}
icmpHdr := header.ICMPv4(transport)
if icmpHdr.Type() != header.ICMPv4Echo || icmpHdr.Code() != 0 {
return preMatchPacket{}, false
}
parsed.network = N.NetworkICMP
parsed.source = M.SocksaddrFrom(source, 0)
parsed.destination = M.SocksaddrFrom(destination, 0)
case uint8(header.ICMPv6ProtocolNumber):
if !source.Is6() || len(transport) < header.ICMPv6MinimumSize {
return preMatchPacket{}, false
}
icmpHdr := header.ICMPv6(transport)
if icmpHdr.Type() != header.ICMPv6EchoRequest || icmpHdr.Code() != 0 {
return preMatchPacket{}, false
}
parsed.network = N.NetworkICMP
parsed.source = M.SocksaddrFrom(source, 0)
parsed.destination = M.SocksaddrFrom(destination, 0)
default:
return preMatchPacket{}, false
}
return parsed, true
}
func parsePreMatchIPv6Transport(packet []byte) (transportProto uint8, transportOffset int, ok bool) {
nextHeader := header.IPv6(packet).NextHeader()
offset := header.IPv6MinimumSize offset := header.IPv6MinimumSize
for { for {
switch nextHeader { switch header.IPv6ExtensionHeaderIdentifier(nextHeader) {
case unix.IPPROTO_HOPOPTS, case header.IPv6HopByHopOptionsExtHdrIdentifier,
unix.IPPROTO_ROUTING, header.IPv6RoutingExtHdrIdentifier,
unix.IPPROTO_DSTOPTS: header.IPv6DestinationOptionsExtHdrIdentifier:
if len(payload) < offset+2 { if len(packet) < offset+2 {
return 0, 0, false return 0, 0, false
} }
nextHeader = payload[offset] nextHeader = packet[offset]
extLen := int(payload[offset+1]+1) * 8 extensionLength := (int(packet[offset+1]) + 1) * 8
if len(payload) < offset+extLen { if len(packet) < offset+extensionLength {
return 0, 0, false return 0, 0, false
} }
offset += extLen offset += extensionLength
case header.IPv6FragmentExtHdrIdentifier:
case unix.IPPROTO_FRAGMENT: if len(packet) < offset+header.IPv6FragmentHeaderSize {
if len(payload) < offset+8 {
return 0, 0, false return 0, 0, false
} }
nextHeader = payload[offset] fragmentHdr := header.IPv6Fragment(packet[offset:])
offset += 8 if fragmentHdr.FragmentOffset() != 0 {
case unix.IPPROTO_AH:
if len(payload) < offset+2 {
return 0, 0, false return 0, 0, false
} }
nextHeader = payload[offset] nextHeader = fragmentHdr.NextHeader()
extLen := int(payload[offset+1]+2) * 4 offset += header.IPv6FragmentHeaderSize
if len(payload) < offset+extLen { case ipv6AuthenticationHeaderIdentifier:
if len(packet) < offset+2 {
return 0, 0, false return 0, 0, false
} }
offset += extLen nextHeader = packet[offset]
extensionLength := (int(packet[offset+1]) + 2) * 4
case unix.IPPROTO_NONE: if len(packet) < offset+extensionLength {
return 0, 0, false
}
offset += extensionLength
case header.IPv6NoNextHeaderIdentifier:
return 0, 0, false return 0, 0, false
default: default:
return nextHeader, offset, true return nextHeader, offset, true
} }
@ -162,56 +259,13 @@ func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int {
packetID := *attr.PacketID packetID := *attr.PacketID
payload := *attr.Payload payload := *attr.Payload
if len(payload) < header.IPv4MinimumSize { packet, loaded := parsePreMatchPacket(payload)
if !loaded {
h.setVerdict(packetID, nfqueue.NfAccept, 0) h.setVerdict(packetID, nfqueue.NfAccept, 0)
return 0 return 0
} }
var srcAddr, dstAddr M.Socksaddr _, pErr := h.handler.PrepareConnection(packet.network, packet.source, packet.destination, nil, 0)
var tcpOffset int
version := payload[0] >> 4
switch version {
case 4:
ipv4 := header.IPv4(payload)
if !ipv4.IsValid(len(payload)) || ipv4.Protocol() != uint8(unix.IPPROTO_TCP) {
h.setVerdict(packetID, nfqueue.NfAccept, 0)
return 0
}
srcAddr = M.SocksaddrFrom(ipv4.SourceAddr(), 0)
dstAddr = M.SocksaddrFrom(ipv4.DestinationAddr(), 0)
tcpOffset = int(ipv4.HeaderLength())
case 6:
transportProto, transportOffset, ok := parseIPv6TransportHeader(payload)
if !ok || transportProto != unix.IPPROTO_TCP {
h.setVerdict(packetID, nfqueue.NfAccept, 0)
return 0
}
ipv6 := header.IPv6(payload)
srcAddr = M.SocksaddrFrom(ipv6.SourceAddr(), 0)
dstAddr = M.SocksaddrFrom(ipv6.DestinationAddr(), 0)
tcpOffset = transportOffset
default:
h.setVerdict(packetID, nfqueue.NfAccept, 0)
return 0
}
if len(payload) < tcpOffset+header.TCPMinimumSize {
h.setVerdict(packetID, nfqueue.NfAccept, 0)
return 0
}
tcp := header.TCP(payload[tcpOffset:])
srcAddr = M.SocksaddrFrom(srcAddr.Addr, tcp.SourcePort())
dstAddr = M.SocksaddrFrom(dstAddr.Addr, tcp.DestinationPort())
flags := tcp.Flags()
if !flags.Contains(header.TCPFlagSyn) || flags.Contains(header.TCPFlagAck) {
h.setVerdict(packetID, nfqueue.NfAccept, 0)
return 0
}
_, pErr := h.handler.PrepareConnection(N.NetworkTCP, srcAddr, dstAddr, nil, 0)
// Use NfRepeat for bypass/reset so the packet re-enters the chain // Use NfRepeat for bypass/reset so the packet re-enters the chain
// from the beginning, allowing mark-checking rules to save the mark // from the beginning, allowing mark-checking rules to save the mark
@ -221,7 +275,11 @@ func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int {
case errors.Is(pErr, ErrBypass): case errors.Is(pErr, ErrBypass):
h.setVerdict(packetID, nfqueue.NfRepeat, h.outputMark) h.setVerdict(packetID, nfqueue.NfRepeat, h.outputMark)
case errors.Is(pErr, ErrReset): case errors.Is(pErr, ErrReset):
h.setVerdict(packetID, nfqueue.NfRepeat, h.resetMark) if packet.protocol == uint8(unix.IPPROTO_TCP) {
h.setVerdict(packetID, nfqueue.NfRepeat, h.resetMark)
} else {
h.setVerdict(packetID, nfqueue.NfAccept, 0)
}
case errors.Is(pErr, ErrDrop): case errors.Is(pErr, ErrDrop):
h.setVerdict(packetID, nfqueue.NfDrop, 0) h.setVerdict(packetID, nfqueue.NfDrop, 0)
default: default:

View file

@ -9,6 +9,7 @@ import (
"github.com/sagernet/nftables" "github.com/sagernet/nftables"
"github.com/sagernet/nftables/binaryutil" "github.com/sagernet/nftables/binaryutil"
"github.com/sagernet/nftables/expr" "github.com/sagernet/nftables/expr"
"github.com/sagernet/sing-tun/internal/gtcpip/header"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@ -393,7 +394,10 @@ func (r *autoRedirect) nftablesCreatePreMatchChains(nft *nftables.Conn, table *n
Priority: nftables.ChainPriorityRef(*nftables.ChainPriorityNATDest - 1), Priority: nftables.ChainPriorityRef(*nftables.ChainPriorityNATDest - 1),
Type: nftables.ChainTypeFilter, Type: nftables.ChainTypeFilter,
}) })
r.nftablesAddPreMatchRules(nft, table, chainPreroutingPreMatch, true) err := r.nftablesAddPreMatchRules(nft, table, chainPreroutingPreMatch, true)
if err != nil {
return err
}
if !r.shouldSkipOutputChain() { if !r.shouldSkipOutputChain() {
chainOutputPreMatch := nft.AddChain(&nftables.Chain{ chainOutputPreMatch := nft.AddChain(&nftables.Chain{
@ -403,13 +407,16 @@ func (r *autoRedirect) nftablesCreatePreMatchChains(nft *nftables.Conn, table *n
Priority: nftables.ChainPriorityRef(*nftables.ChainPriorityMangle - 1), Priority: nftables.ChainPriorityRef(*nftables.ChainPriorityMangle - 1),
Type: nftables.ChainTypeFilter, Type: nftables.ChainTypeFilter,
}) })
r.nftablesAddPreMatchRules(nft, table, chainOutputPreMatch, false) err = r.nftablesAddPreMatchRules(nft, table, chainOutputPreMatch, false)
if err != nil {
return err
}
} }
return nil return nil
} }
func (r *autoRedirect) nftablesAddPreMatchRules(nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, isPrerouting bool) { func (r *autoRedirect) nftablesAddPreMatchRules(nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, isPrerouting bool) error {
ifnameKey := expr.MetaKeyOIFNAME ifnameKey := expr.MetaKeyOIFNAME
if isPrerouting { if isPrerouting {
ifnameKey = expr.MetaKeyIIFNAME ifnameKey = expr.MetaKeyIIFNAME
@ -424,12 +431,35 @@ func (r *autoRedirect) nftablesAddPreMatchRules(nft *nftables.Conn, table *nftab
}, },
}) })
preMatchProtocols := &nftables.Set{
Table: table,
Anonymous: true,
Constant: true,
KeyType: nftables.TypeInetProto,
}
preMatchProtocolElements := []nftables.SetElement{{Key: []byte{unix.IPPROTO_TCP}}}
if r.tunOptions.AutoRedirectMarkMode {
preMatchProtocolElements = append(preMatchProtocolElements,
nftables.SetElement{Key: []byte{unix.IPPROTO_UDP}},
nftables.SetElement{Key: []byte{unix.IPPROTO_ICMP}},
nftables.SetElement{Key: []byte{unix.IPPROTO_ICMPV6}},
)
}
err := nft.AddSet(preMatchProtocols, preMatchProtocolElements)
if err != nil {
return E.Cause(err, "add pre-match protocol set")
}
nft.AddRule(&nftables.Rule{ nft.AddRule(&nftables.Rule{
Table: table, Table: table,
Chain: chain, Chain: chain,
Exprs: []expr.Any{ Exprs: []expr.Any{
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1}, &expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
&expr.Cmp{Op: expr.CmpOpNeq, Register: 1, Data: []byte{unix.IPPROTO_TCP}}, &expr.Lookup{
SourceRegister: 1,
SetID: preMatchProtocols.ID,
SetName: preMatchProtocols.Name,
Invert: true,
},
&expr.Verdict{Kind: expr.VerdictReturn}, &expr.Verdict{Kind: expr.VerdictReturn},
}, },
}) })
@ -459,6 +489,8 @@ func (r *autoRedirect) nftablesAddPreMatchRules(nft *nftables.Conn, table *nftab
Table: table, Table: table,
Chain: chain, Chain: chain,
Exprs: []expr.Any{ Exprs: []expr.Any{
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{unix.IPPROTO_TCP}},
&expr.Meta{Key: expr.MetaKeyMARK, Register: 1}, &expr.Meta{Key: expr.MetaKeyMARK, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: binaryutil.NativeEndian.PutUint32(r.effectiveResetMark())}, &expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: binaryutil.NativeEndian.PutUint32(r.effectiveResetMark())},
&expr.Counter{}, &expr.Counter{},
@ -477,11 +509,32 @@ func (r *autoRedirect) nftablesAddPreMatchRules(nft *nftables.Conn, table *nftab
}, },
}) })
if r.tunOptions.AutoRedirectMarkMode {
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.tunOptions.AutoRedirectInputMark)},
&expr.Verdict{Kind: expr.VerdictReturn},
},
})
}
queueExpression := func() *expr.Queue {
return &expr.Queue{
Num: r.effectiveNFQueue(),
Flag: expr.QueueFlagBypass,
}
}
// TCP SYN: send to NFQUEUE for pre-match evaluation. // TCP SYN: send to NFQUEUE for pre-match evaluation.
nft.AddRule(&nftables.Rule{ nft.AddRule(&nftables.Rule{
Table: table, Table: table,
Chain: chain, Chain: chain,
Exprs: []expr.Any{ Exprs: []expr.Any{
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{unix.IPPROTO_TCP}},
&expr.Payload{ &expr.Payload{
OperationType: expr.PayloadLoad, OperationType: expr.PayloadLoad,
DestRegister: 1, DestRegister: 1,
@ -498,10 +551,45 @@ func (r *autoRedirect) nftablesAddPreMatchRules(nft *nftables.Conn, table *nftab
}, },
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{0x02}}, &expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{0x02}},
&expr.Counter{}, &expr.Counter{},
&expr.Queue{ queueExpression(),
Num: r.effectiveNFQueue(),
Flag: expr.QueueFlagBypass,
},
}, },
}) })
if r.tunOptions.AutoRedirectMarkMode {
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chain,
Exprs: []expr.Any{
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{unix.IPPROTO_UDP}},
&expr.Counter{},
queueExpression(),
},
})
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chain,
Exprs: []expr.Any{
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{unix.IPPROTO_ICMP}},
&expr.Payload{OperationType: expr.PayloadLoad, DestRegister: 1, Base: expr.PayloadBaseTransportHeader, Offset: 0, Len: 2},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{byte(header.ICMPv4Echo), 0}},
&expr.Counter{},
queueExpression(),
},
})
nft.AddRule(&nftables.Rule{
Table: table,
Chain: chain,
Exprs: []expr.Any{
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{unix.IPPROTO_ICMPV6}},
&expr.Payload{OperationType: expr.PayloadLoad, DestRegister: 1, Base: expr.PayloadBaseTransportHeader, Offset: 0, Len: 2},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{byte(header.ICMPv6EchoRequest), 0}},
&expr.Counter{},
queueExpression(),
},
})
}
return nil
} }