From 8abb28254781e21cc8a39c6c34c5b5982558ddca Mon Sep 17 00:00:00 2001 From: SunBK201 Date: Tue, 25 Nov 2025 02:35:51 +0800 Subject: [PATCH] fix: prevent flow offload hijack packet --- src/internal/netfilter/firewall.go | 72 +++++++++++++++++++++++-- src/internal/server/netlink/iptables.go | 35 ++++++++++++ src/internal/server/netlink/nftables.go | 39 ++++++++++++++ 3 files changed, 143 insertions(+), 3 deletions(-) diff --git a/src/internal/netfilter/firewall.go b/src/internal/netfilter/firewall.go index e471529..76f090d 100644 --- a/src/internal/netfilter/firewall.go +++ b/src/internal/netfilter/firewall.go @@ -9,7 +9,9 @@ import ( "net" "os/exec" "os/user" + "strings" + "github.com/coreos/go-iptables/iptables" "github.com/gonetx/ipset" "github.com/sunbk201/ua3f/internal/config" "sigs.k8s.io/knftables" @@ -255,8 +257,8 @@ func (f *Firewall) DeleteTproxyRoute(fwmark, routeTable string) error { } func detectFirewallBackend(cfg *config.Config) string { - nftAvailable := isCommandAvailable("nft") - iptAvailable := isCommandAvailable("iptables") + nftAvailable := IsCommandAvailable("nft") + iptAvailable := IsCommandAvailable("iptables") nftTproxyAvailable := isOpkgPackageInstalled("kmod-nft-tproxy") && nftAvailable nftNfqueueAvailable := isOpkgPackageInstalled("kmod-nft-queue") && nftAvailable tproxyNeeded := cfg.ServerMode == config.ServerModeTProxy @@ -334,7 +336,7 @@ func getLocalIPv4CIDRs() ([]string, error) { return cidrs, nil } -func isCommandAvailable(cmd string) bool { +func IsCommandAvailable(cmd string) bool { _, err := exec.LookPath(cmd) return err == nil } @@ -430,3 +432,67 @@ func initLanCidrs() { } LAN_CIDRS = append(LAN_CIDRS, localCIDRs...) } + +func GetLanDevice() (string, error) { + out, err := exec.Command("ubus", "call", "network.interface.lan", "status").Output() + if err != nil { + return "", err + } + var lanInterface struct { + Device string `json:"device"` + } + if err := json.Unmarshal(out, &lanInterface); err != nil { + return "", err + } + if lanInterface.Device == "" { + return "", errors.New("no device found for lan interface") + } + // get real device if it's a bridge + out, err = exec.Command("ubus", "call", "network.device", "status").Output() + if err != nil { + return "", err + } + var devices map[string]struct { + Type string `json:"type"` + Bridges []string `json:"bridge-members"` + } + if err := json.Unmarshal(out, &devices); err != nil { + return "", err + } + dev, ok := devices[lanInterface.Device] + if !ok { + return "", fmt.Errorf("device %s not found", lanInterface.Device) + } + if dev.Type != "bridge" { + return lanInterface.Device, nil + } + if len(dev.Bridges) == 0 { + return "", fmt.Errorf("bridge %s has no members", lanInterface.Device) + } + return dev.Bridges[0], nil +} + +func FlowOffloadEnabled() bool { + cmd := exec.Command("nft", "list", "chain", string(knftables.InetFamily), "fw4", "forward") + if output, err := cmd.CombinedOutput(); err == nil { + if strings.Contains(string(output), "flow add") { + return true + } + } + + ipt, err := iptables.New() + if err != nil { + return false + } + + rules, err := ipt.List("filter", "forward") + if err != nil { + return false + } + for _, rule := range rules { + if strings.Contains(rule, "FLOWOFFLOAD") { + return true + } + } + return false +} diff --git a/src/internal/server/netlink/iptables.go b/src/internal/server/netlink/iptables.go index 158e0cb..17a3c03 100644 --- a/src/internal/server/netlink/iptables.go +++ b/src/internal/server/netlink/iptables.go @@ -3,10 +3,13 @@ package netlink import ( + "context" + "errors" "strconv" "github.com/coreos/go-iptables/iptables" "github.com/sunbk201/ua3f/internal/netfilter" + "sigs.k8s.io/knftables" ) const ( @@ -51,6 +54,12 @@ func (s *Server) iptSetup() error { if err != nil { return err } + if netfilter.FlowOffloadEnabled() { + err = s.IptSetTTLIngress(ipt) + if err != nil { + return err + } + } } if s.cfg.DelTCPTimestamp && !s.cfg.SetIPID { err = s.IptDelTCPTS(ipt) @@ -75,6 +84,9 @@ func (s *Server) iptCleanup() error { _ = ipt.DeleteIfExists(table, chain, RuleTTL...) _ = ipt.DeleteIfExists(table, chain, RuleIP...) _ = ipt.DeleteIfExists(table, chain, RuleDelTCPTS...) + if s.cfg.SetTTL { + _ = s.NftCleanup() + } return nil } @@ -101,3 +113,26 @@ func (s *Server) IptSetIP(ipt *iptables.IPTables) error { } return nil } + +func (s *Server) IptSetTTLIngress(ipt *iptables.IPTables) error { + if !netfilter.IsCommandAvailable("nft") { + return errors.New("nft command not available") + } + + nft, err := knftables.New(s.Nftable.Family, s.Nftable.Name) + if err != nil { + return err + } + + tx := nft.NewTransaction() + tx.Add(s.Nftable) + if err := nft.Run(context.TODO(), tx); err != nil { + return err + } + + lanDev, err := netfilter.GetLanDevice() + if err != nil { + return err + } + return s.NftSetTTLIngress(nft, s.Nftable, lanDev) +} diff --git a/src/internal/server/netlink/nftables.go b/src/internal/server/netlink/nftables.go index 632f267..813cec2 100644 --- a/src/internal/server/netlink/nftables.go +++ b/src/internal/server/netlink/nftables.go @@ -7,6 +7,7 @@ import ( "fmt" "log/slog" + "github.com/sunbk201/ua3f/internal/netfilter" "sigs.k8s.io/knftables" ) @@ -37,6 +38,18 @@ func (s *Server) nftSetup() error { if err := nft.Run(context.TODO(), tx); err != nil { return err } + + if s.cfg.SetTTL && netfilter.FlowOffloadEnabled() { + lanDev, err := netfilter.GetLanDevice() + if err != nil { + slog.Warn("nftSetup netfilter.GetLanDevice", slog.Any("error", err)) + } else { + err = s.NftSetTTLIngress(nft, s.Nftable, lanDev) + if err != nil { + slog.Warn("NftSetTTLIngress", slog.Any("error", err)) + } + } + } return nil } @@ -73,6 +86,32 @@ func (s *Server) NftSetTTL(tx *knftables.Transaction, table *knftables.Table) { tx.Add(rule) } +func (s *Server) NftSetTTLIngress(nft knftables.Interface, table *knftables.Table, device string) error { + tx := nft.NewTransaction() + + chain := &knftables.Chain{ + Name: "TTL64_INGRESS", + Table: table.Name, + Type: knftables.PtrTo(knftables.FilterType), + Hook: knftables.PtrTo(knftables.IngressHook), + Priority: knftables.PtrTo(knftables.ManglePriority), + Device: knftables.PtrTo(device), + } + rule := &knftables.Rule{ + Chain: chain.Name, + Rule: knftables.Concat( + "ip ttl set 65", + ), + } + tx.Add(chain) + tx.Add(rule) + + if err := nft.Run(context.TODO(), tx); err != nil { + return err + } + return nil +} + func (s *Server) NftDelTCPTS(tx *knftables.Transaction, table *knftables.Table) { chain := &knftables.Chain{ Name: "DEL_TCPTS",