sing-tun-lx/nfqueue_linux.go
2026-07-27 23:11:48 +08:00

303 lines
8.1 KiB
Go

//go:build linux
package tun
import (
"context"
"net/netip"
"sync/atomic"
"github.com/sagernet/sing-tun/gtcpip/header"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/florianl/go-nfqueue/v2"
"github.com/mdlayher/netlink"
"golang.org/x/sys/unix"
)
type nfqueueHandler struct {
ctx context.Context
cancel context.CancelFunc
handler Handler
logger logger.Logger
nfq *nfqueue.Nfqueue
queue uint16
outputMark uint32
resetMark uint32
closed atomic.Bool
}
type nfqueueOptions struct {
Context context.Context
Handler Handler
Logger logger.Logger
Queue uint16
OutputMark uint32
ResetMark uint32
}
func newNFQueueHandler(options nfqueueOptions) (*nfqueueHandler, error) {
ctx, cancel := context.WithCancel(options.Context)
return &nfqueueHandler{
ctx: ctx,
cancel: cancel,
handler: options.Handler,
logger: options.Logger,
queue: options.Queue,
outputMark: options.OutputMark,
resetMark: options.ResetMark,
}, nil
}
func (h *nfqueueHandler) setVerdict(packetID uint32, verdict int, mark uint32) {
var err error
if mark != 0 {
err = h.nfq.SetVerdictWithOption(packetID, verdict, nfqueue.WithMark(mark))
} else {
err = h.nfq.SetVerdict(packetID, verdict)
}
if err != nil && !h.closed.Load() && h.ctx.Err() == nil {
h.logger.Trace(E.Cause(err, "set verdict"))
}
}
func (h *nfqueueHandler) Start() error {
config := nfqueue.Config{
NfQueue: h.queue,
MaxPacketLen: 0xFFFF,
MaxQueueLen: 4096,
Copymode: nfqueue.NfQnlCopyPacket,
AfFamily: unix.AF_UNSPEC,
Flags: nfqueue.NfQaCfgFlagFailOpen | nfqueue.NfQaCfgFlagGSO,
}
nfq, err := nfqueue.Open(&config)
if err != nil {
return E.Cause(err, "open nfqueue")
}
if err = nfq.SetOption(netlink.NoENOBUFS, true); err != nil {
nfq.Close()
return E.Cause(err, "set nfqueue option")
}
err = nfq.RegisterWithErrorFunc(h.ctx, h.handlePacket, func(e error) int {
if h.ctx.Err() != nil {
return 1
}
h.logger.Error("nfqueue error: ", e)
return 0
})
if err != nil {
nfq.Close()
return E.Cause(err, "register nfqueue")
}
h.nfq = nfq
return nil
}
const ipv6AuthenticationHeaderIdentifier header.IPv6ExtensionHeaderIdentifier = 51
type preMatchPacket struct {
protocol uint8
source netip.AddrPort
destination netip.AddrPort
firstPacket []byte
}
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
}
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.source = netip.AddrPortFrom(source, tcpHdr.SourcePort())
parsed.destination = netip.AddrPortFrom(destination, tcpHdr.DestinationPort())
case uint8(header.UDPProtocolNumber):
if len(transport) < header.UDPMinimumSize {
return preMatchPacket{}, false
}
udpHdr := header.UDP(transport)
udpLength := int(udpHdr.Length())
if udpLength < header.UDPMinimumSize {
return preMatchPacket{}, false
}
if udpLength < len(transport) {
transport = transport[:udpLength]
}
parsed.source = netip.AddrPortFrom(source, udpHdr.SourcePort())
parsed.destination = netip.AddrPortFrom(destination, udpHdr.DestinationPort())
parsed.firstPacket = header.UDP(transport).Payload()
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
}
identifier := icmpHdr.Ident()
parsed.source = netip.AddrPortFrom(source, identifier)
parsed.destination = netip.AddrPortFrom(destination, identifier)
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
}
identifier := icmpHdr.Ident()
parsed.source = netip.AddrPortFrom(source, identifier)
parsed.destination = netip.AddrPortFrom(destination, identifier)
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 header.IPv6ExtensionHeaderIdentifier(nextHeader) {
case header.IPv6HopByHopOptionsExtHdrIdentifier,
header.IPv6RoutingExtHdrIdentifier,
header.IPv6DestinationOptionsExtHdrIdentifier:
if len(packet) < offset+2 {
return 0, 0, false
}
nextHeader = packet[offset]
extensionLength := (int(packet[offset+1]) + 1) * 8
if len(packet) < offset+extensionLength {
return 0, 0, false
}
offset += extensionLength
case header.IPv6FragmentExtHdrIdentifier:
if len(packet) < offset+header.IPv6FragmentHeaderSize {
return 0, 0, false
}
fragmentHdr := header.IPv6Fragment(packet[offset:])
if fragmentHdr.FragmentOffset() != 0 {
return 0, 0, false
}
nextHeader = fragmentHdr.NextHeader()
offset += header.IPv6FragmentHeaderSize
case ipv6AuthenticationHeaderIdentifier:
if len(packet) < offset+2 {
return 0, 0, false
}
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
}
}
}
func (h *nfqueueHandler) handlePacket(attr nfqueue.Attribute) int {
if h.closed.Load() {
return 0
}
if attr.PacketID == nil || attr.Payload == nil {
return 0
}
packetID := *attr.PacketID
payload := *attr.Payload
packet, loaded := parsePreMatchPacket(payload)
if !loaded {
h.setVerdict(packetID, nfqueue.NfAccept, 0)
return 0
}
verdict := h.handler.JudgeFlow(
packet.protocol,
packet.source,
packet.destination,
)
// Use NfRepeat for bypass/reset so the packet re-enters the chain
// from the beginning, allowing mark-checking rules to save the mark
// to conntrack. NfAccept is a terminal verdict in nftables — it exits
// the chain immediately, skipping any rules after the queue statement.
switch verdict.Action {
case ActionBypass:
h.setVerdict(packetID, nfqueue.NfRepeat, h.outputMark)
case ActionReject:
if packet.protocol == uint8(unix.IPPROTO_TCP) {
h.setVerdict(packetID, nfqueue.NfRepeat, h.resetMark)
} else {
h.setVerdict(packetID, nfqueue.NfAccept, 0)
}
case ActionDrop:
h.setVerdict(packetID, nfqueue.NfDrop, 0)
default:
h.setVerdict(packetID, nfqueue.NfAccept, 0)
}
return 0
}
func (h *nfqueueHandler) Close() error {
h.closed.Store(true)
h.cancel()
if h.nfq != nil {
h.nfq.Close()
}
return nil
}