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 (
"context"
"errors"
"net/netip"
"sync/atomic"
"github.com/sagernet/sing-tun/internal/gtcpip/header"
@ -100,51 +101,147 @@ func (h *nfqueueHandler) Start() error {
return nil
}
func parseIPv6TransportHeader(payload []byte) (transportProto uint8, transportOffset int, ok bool) {
if len(payload) < header.IPv6MinimumSize {
return 0, 0, false
const ipv6AuthenticationHeaderIdentifier header.IPv6ExtensionHeaderIdentifier = 51
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)
nextHeader := ipv6.NextHeader()
transport := packet[transportOffset:]
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
for {
switch nextHeader {
case unix.IPPROTO_HOPOPTS,
unix.IPPROTO_ROUTING,
unix.IPPROTO_DSTOPTS:
if len(payload) < offset+2 {
switch header.IPv6ExtensionHeaderIdentifier(nextHeader) {
case header.IPv6HopByHopOptionsExtHdrIdentifier,
header.IPv6RoutingExtHdrIdentifier,
header.IPv6DestinationOptionsExtHdrIdentifier:
if len(packet) < offset+2 {
return 0, 0, false
}
nextHeader = payload[offset]
extLen := int(payload[offset+1]+1) * 8
if len(payload) < offset+extLen {
nextHeader = packet[offset]
extensionLength := (int(packet[offset+1]) + 1) * 8
if len(packet) < offset+extensionLength {
return 0, 0, false
}
offset += extLen
case unix.IPPROTO_FRAGMENT:
if len(payload) < offset+8 {
offset += extensionLength
case header.IPv6FragmentExtHdrIdentifier:
if len(packet) < offset+header.IPv6FragmentHeaderSize {
return 0, 0, false
}
nextHeader = payload[offset]
offset += 8
case unix.IPPROTO_AH:
if len(payload) < offset+2 {
fragmentHdr := header.IPv6Fragment(packet[offset:])
if fragmentHdr.FragmentOffset() != 0 {
return 0, 0, false
}
nextHeader = payload[offset]
extLen := int(payload[offset+1]+2) * 4
if len(payload) < offset+extLen {
nextHeader = fragmentHdr.NextHeader()
offset += header.IPv6FragmentHeaderSize
case ipv6AuthenticationHeaderIdentifier:
if len(packet) < offset+2 {
return 0, 0, false
}
offset += extLen
case unix.IPPROTO_NONE:
nextHeader = packet[offset]
extensionLength := (int(packet[offset+1]) + 2) * 4
if len(packet) < offset+extensionLength {
return 0, 0, false
}
offset += extensionLength
case header.IPv6NoNextHeaderIdentifier:
return 0, 0, false
default:
return nextHeader, offset, true
}
@ -162,56 +259,13 @@ func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int {
packetID := *attr.PacketID
payload := *attr.Payload
if len(payload) < header.IPv4MinimumSize {
packet, loaded := parsePreMatchPacket(payload)
if !loaded {
h.setVerdict(packetID, nfqueue.NfAccept, 0)
return 0
}
var srcAddr, dstAddr M.Socksaddr
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)
_, pErr := h.handler.PrepareConnection(packet.network, packet.source, packet.destination, nil, 0)
// Use NfRepeat for bypass/reset so the packet re-enters the chain
// 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):
h.setVerdict(packetID, nfqueue.NfRepeat, h.outputMark)
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):
h.setVerdict(packetID, nfqueue.NfDrop, 0)
default:

View file

@ -9,6 +9,7 @@ import (
"github.com/sagernet/nftables"
"github.com/sagernet/nftables/binaryutil"
"github.com/sagernet/nftables/expr"
"github.com/sagernet/sing-tun/internal/gtcpip/header"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/control"
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),
Type: nftables.ChainTypeFilter,
})
r.nftablesAddPreMatchRules(nft, table, chainPreroutingPreMatch, true)
err := r.nftablesAddPreMatchRules(nft, table, chainPreroutingPreMatch, true)
if err != nil {
return err
}
if !r.shouldSkipOutputChain() {
chainOutputPreMatch := nft.AddChain(&nftables.Chain{
@ -403,13 +407,16 @@ func (r *autoRedirect) nftablesCreatePreMatchChains(nft *nftables.Conn, table *n
Priority: nftables.ChainPriorityRef(*nftables.ChainPriorityMangle - 1),
Type: nftables.ChainTypeFilter,
})
r.nftablesAddPreMatchRules(nft, table, chainOutputPreMatch, false)
err = r.nftablesAddPreMatchRules(nft, table, chainOutputPreMatch, false)
if err != nil {
return err
}
}
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
if isPrerouting {
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{
Table: table,
Chain: chain,
Exprs: []expr.Any{
&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},
},
})
@ -459,6 +489,8 @@ func (r *autoRedirect) nftablesAddPreMatchRules(nft *nftables.Conn, table *nftab
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_TCP}},
&expr.Meta{Key: expr.MetaKeyMARK, Register: 1},
&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: binaryutil.NativeEndian.PutUint32(r.effectiveResetMark())},
&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.
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_TCP}},
&expr.Payload{
OperationType: expr.PayloadLoad,
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.Counter{},
&expr.Queue{
Num: r.effectiveNFQueue(),
Flag: expr.QueueFlagBypass,
},
queueExpression(),
},
})
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
}