diff --git a/nfqueue_linux.go b/nfqueue_linux.go index 7b81470..beb2fc4 100644 --- a/nfqueue_linux.go +++ b/nfqueue_linux.go @@ -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: diff --git a/redirect_nftables.go b/redirect_nftables.go index fc09ec4..bf6460a 100644 --- a/redirect_nftables.go +++ b/redirect_nftables.go @@ -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 }