feat: add packet modification features for TTL, IP ID, and TCP timestamp deletion

This commit is contained in:
SunBK201 2025-11-11 20:19:54 +08:00
parent db4888cd0c
commit 4cb071cd19
17 changed files with 538 additions and 47 deletions

View File

@ -190,6 +190,14 @@ function M.add_others_fields(section)
-- TTL Setting -- TTL Setting
local ttl = section:taboption("others", Flag, "set_ttl", translate("Set TTL")) local ttl = section:taboption("others", Flag, "set_ttl", translate("Set TTL"))
ttl.description = translate("Set the TTL 64 for packets") ttl.description = translate("Set the TTL 64 for packets")
-- TCP Timestamp Deletion
local tcpts = section:taboption("others", Flag, "del_tcpts", translate("Delete TCP Timestamps"))
tcpts.description = translate("Remove TCP Timestamp option")
-- IP ID Setting
local ipid = section:taboption("others", Flag, "set_ipid", translate("Set IP ID"))
ipid.description = translate("Set the IP ID to 0 for packets")
end end
return M return M

View File

@ -137,8 +137,8 @@ detect_backend() {
if [ "$SERVER_MODE" = "TPROXY" ]; then if [ "$SERVER_MODE" = "TPROXY" ]; then
if opkg list-installed kmod-nft-tproxy | grep -q 'kmod-nft-tproxy'; then if opkg list-installed kmod-nft-tproxy | grep -q 'kmod-nft-tproxy'; then
if nft_available; then if nft_available; then
FW_BACKEND="nft" FW_BACKEND="nft"
return 0 return 0
fi fi
else else
FW_BACKEND="ipt" FW_BACKEND="ipt"
@ -147,8 +147,8 @@ detect_backend() {
elif [ "$SERVER_MODE" = "NFQUEUE" ]; then elif [ "$SERVER_MODE" = "NFQUEUE" ]; then
if opkg list-installed kmod-nft-queue | grep -q 'kmod-nft-queue'; then if opkg list-installed kmod-nft-queue | grep -q 'kmod-nft-queue'; then
if nft_available; then if nft_available; then
FW_BACKEND="nft" FW_BACKEND="nft"
return 0 return 0
fi fi
else else
FW_BACKEND="ipt" FW_BACKEND="ipt"
@ -510,6 +510,7 @@ start_service() {
local port bind ua log_level ua_regex partial_replace set_ttl local port bind ua log_level ua_regex partial_replace set_ttl
local rewrite_mode rewrite_rules local rewrite_mode rewrite_rules
local set_ttl set_ipid del_tcpts
config_get server_mode "main" "server_mode" "SOCKS5" config_get server_mode "main" "server_mode" "SOCKS5"
config_get port "main" "port" "1080" config_get port "main" "port" "1080"
config_get bind "main" "bind" "127.0.0.1" config_get bind "main" "bind" "127.0.0.1"
@ -517,15 +518,21 @@ start_service() {
config_get ua_regex "main" "ua_regex" "" config_get ua_regex "main" "ua_regex" ""
config_get_bool partial_replace "main" "partial_replace" 0 config_get_bool partial_replace "main" "partial_replace" 0
config_get log_level "main" "log_level" "info" config_get log_level "main" "log_level" "info"
config_get_bool set_ttl "main" "set_ttl" 0
config_get rewrite_mode "main" "rewrite_mode" "GLOBAL" config_get rewrite_mode "main" "rewrite_mode" "GLOBAL"
config_get rewrite_rules "main" "rewrite_rules" "" config_get rewrite_rules "main" "rewrite_rules" ""
config_get_bool set_ttl "main" "set_ttl" 0
config_get_bool set_ipid "main" "set_ipid" 0
config_get_bool del_tcpts "main" "del_tcpts" 0
# compose set_ttl set_ipid del_tcpts with others ",ttl,ipid,tcpts,"
local others=","
[ "$set_ipid" -eq "1" ] && others="${others}ipid,"
[ "$del_tcpts" -eq "1" ] && others="${others}tcpts,"
[ "$set_ttl" -eq "1" ] && others="${others}ttl,"
SERVER_MODE="$(echo "$server_mode" | tr '[:lower:]' '[:upper:]')" SERVER_MODE="$(echo "$server_mode" | tr '[:lower:]' '[:upper:]')"
SERVER_MODE="$server_mode" SERVER_MODE="$server_mode"
SET_TTL="$set_ttl"
LOG "Server Mode: $SERVER_MODE" LOG "Server Mode: $SERVER_MODE"
LOG "Port: $port" LOG "Port: $port"
LOG "Bind: $bind" LOG "Bind: $bind"
@ -533,7 +540,6 @@ start_service() {
LOG "User-Agent Regex: $ua_regex" LOG "User-Agent Regex: $ua_regex"
LOG "Log level: $log_level" LOG "Log level: $log_level"
LOG "Partial Replace: $partial_replace" LOG "Partial Replace: $partial_replace"
LOG "Set TTL: $SET_TTL"
set_ua3f_group set_ua3f_group
LOG "Run as GID: $UA3F_GID, Group: $UA3F_GROUP" LOG "Run as GID: $UA3F_GID, Group: $UA3F_GROUP"
@ -553,6 +559,22 @@ start_service() {
fw_revert_ipt fw_revert_ipt
fi fi
# dump all fw rules for debug
if [ "$log_level" = "debug" ]; then
if [ "$FW_BACKEND" = "nft" ]; then
LOG "nftables before rules:"
nft --handle list ruleset >>"$LOG_FILE" 2>&1
elif [ "$FW_BACKEND" = "ipt" ]; then
LOG "iptables before rules:"
LOG "mangle table:"
iptables -t mangle -L -v -n >>"$LOG_FILE" 2>&1
LOG "nat table:"
iptables -t nat -L -v -n >>"$LOG_FILE" 2>&1
LOG "filter table:"
iptables -t filter -L -v -n >>"$LOG_FILE" 2>&1
fi
fi
case "$SERVER_MODE" in case "$SERVER_MODE" in
HTTP) HTTP)
# No firewall interception # No firewall interception
@ -607,20 +629,6 @@ start_service() {
;; ;;
esac esac
if [ "$SET_TTL" = "1" ]; then
if [ "$FW_BACKEND" = "nft" ]; then
set_ttl_nft || {
LOG "set_ttl_nft setup failed"
}
LOG "Set TTL 64 via nftables"
else
set_ttl_ipt || {
LOG "set_ttl_ipt setup failed"
}
LOG "Set TTL 64 via iptables"
fi
fi
# dump all fw rules for debug # dump all fw rules for debug
if [ "$log_level" = "debug" ]; then if [ "$log_level" = "debug" ]; then
if [ "$FW_BACKEND" = "nft" ]; then if [ "$FW_BACKEND" = "nft" ]; then
@ -653,6 +661,7 @@ start_service() {
procd_append_param command -x "$rewrite_mode" procd_append_param command -x "$rewrite_mode"
procd_append_param command -z "$rewrite_rules" procd_append_param command -z "$rewrite_rules"
[ "$partial_replace" = "1" ] && procd_append_param command -s [ "$partial_replace" = "1" ] && procd_append_param command -s
procd_append_param command -o "$others"
procd_set_param respawn procd_set_param respawn
procd_set_param stdout 1 procd_set_param stdout 1

View File

@ -253,3 +253,15 @@ msgstr "注意重写规则仅在规则判定模式下生效。NFQUEUE 模式
msgid "Rewrite Header" msgid "Rewrite Header"
msgstr "重写请求头" msgstr "重写请求头"
msgid "Delete TCP Timestamps"
msgstr "删除 TCP 时间戳"
msgid "Remove TCP Timestamp option"
msgstr "移除 TCP 时间戳选项"
msgid "Set IP ID"
msgstr "固定 IP ID"
msgid "Set the IP ID to 0 for packets"
msgstr "将数据包的 IP ID 设置为 0不推荐开启除非你知道这是什么"

View File

@ -1,23 +1,26 @@
module github.com/sunbk201/ua3f module github.com/sunbk201/ua3f
go 1.19 go 1.21
toolchain go1.24.6
require ( require (
github.com/coreos/go-iptables v0.8.0
github.com/dlclark/regexp2 v1.11.4 github.com/dlclark/regexp2 v1.11.4
github.com/florianl/go-nfqueue/v2 v2.0.2 github.com/florianl/go-nfqueue/v2 v2.0.2
github.com/google/gopacket v1.1.19 github.com/google/gopacket v1.1.19
github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/mdlayher/netlink v1.7.2 github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
golang.org/x/sys v0.30.0 golang.org/x/sys v0.30.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
sigs.k8s.io/knftables v0.0.19
) )
require ( require (
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/josharian/native v1.1.0 // indirect github.com/mdlayher/socket v0.5.0 // indirect
github.com/mdlayher/socket v0.4.1 // indirect golang.org/x/net v0.33.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/sync v0.7.0 // indirect
) )

View File

@ -1,3 +1,5 @@
github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc=
github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -11,12 +13,12 @@ github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
@ -31,8 +33,8 @@ golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPI
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
@ -51,3 +53,5 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
sigs.k8s.io/knftables v0.0.19 h1:0orK0+tYhY575F5X9uJGu80t+aVQ/hJj48I3fz3TBk8=
sigs.k8s.io/knftables v0.0.19/go.mod h1:f/5ZLKYEUPUhVjUCg6l80ACdL7CIIyeL0DxfgojGRTk=

View File

@ -25,16 +25,19 @@ const (
) )
type Config struct { type Config struct {
ServerMode ServerMode ServerMode ServerMode
BindAddr string BindAddr string
Port int Port int
ListenAddr string ListenAddr string
LogLevel string LogLevel string
RewriteMode RewriteMode RewriteMode RewriteMode
Rules string Rules string
PayloadUA string PayloadUA string
UARegex string UARegex string
PartialReplace bool PartialReplace bool
SetTTL bool
SetIPID bool
DelTCPTimestamp bool
} }
func Parse() (*Config, bool) { func Parse() (*Config, bool) {
@ -48,6 +51,7 @@ func Parse() (*Config, bool) {
partial bool partial bool
rewriteMode string rewriteMode string
rules string rules string
others string
showVer bool showVer bool
) )
@ -60,6 +64,7 @@ func Parse() (*Config, bool) {
flag.BoolVar(&partial, "s", false, "Enable regex partial replace") flag.BoolVar(&partial, "s", false, "Enable regex partial replace")
flag.StringVar(&rewriteMode, "x", string(RewriteModeGlobal), "Rewrite mode: GLOBAL, DIRECT, RULES") flag.StringVar(&rewriteMode, "x", string(RewriteModeGlobal), "Rewrite mode: GLOBAL, DIRECT, RULES")
flag.StringVar(&rules, "z", "", "Rules JSON string") flag.StringVar(&rules, "z", "", "Rules JSON string")
flag.StringVar(&others, "o", "", "Other options (tcpts, ttl, ipid)")
flag.BoolVar(&showVer, "v", false, "Show version") flag.BoolVar(&showVer, "v", false, "Show version")
flag.Parse() flag.Parse()
@ -79,5 +84,19 @@ func Parse() (*Config, bool) {
cfg.BindAddr = "0.0.0.0" cfg.BindAddr = "0.0.0.0"
cfg.ListenAddr = fmt.Sprintf("0.0.0.0:%d", port) cfg.ListenAddr = fmt.Sprintf("0.0.0.0:%d", port)
} }
// Parse other options
opts := strings.Split(others, ",")
for _, opt := range opts {
switch strings.ToLower(strings.TrimSpace(opt)) {
case "tcpts":
cfg.DelTCPTimestamp = true
case "ttl":
cfg.SetTTL = true
case "ipid":
cfg.SetIPID = true
}
}
return cfg, showVer return cfg, showVer
} }

View File

@ -78,6 +78,7 @@ func LogHeader(version string, cfg *config.Config) {
logrus.Infof("User-Agent Regex: '%s'", cfg.UARegex) logrus.Infof("User-Agent Regex: '%s'", cfg.UARegex)
logrus.Infof("Partial Replace: %v", cfg.PartialReplace) logrus.Infof("Partial Replace: %v", cfg.PartialReplace)
logrus.Infof("Log level: %s", cfg.LogLevel) logrus.Infof("Log level: %s", cfg.LogLevel)
logrus.Infof("Packet Modifications - SetTTL: %v, SetIPID: %v, DelTCPTimestamp: %v", cfg.SetTTL, cfg.SetIPID, cfg.DelTCPTimestamp)
} }
func LogDebugWithAddr(src string, dest string, msg string) { func LogDebugWithAddr(src string, dest string, msg string) {

View File

@ -107,7 +107,6 @@ func (s *NfqueueServer) Start() error {
s.wg.Wait() s.wg.Wait()
return fmt.Errorf("failed to register nfqueue handler: %w", err) return fmt.Errorf("failed to register nfqueue handler: %w", err)
} }
logrus.Info("NFQUEUE handler registered, listening for packets")
// Wait until context is done // Wait until context is done
<-ctx.Done() <-ctx.Done()

View File

@ -0,0 +1,72 @@
package netlink
import (
"fmt"
"github.com/sirupsen/logrus"
"github.com/sunbk201/ua3f/internal/config"
)
const (
NFT = "nft"
IPT = "ipt"
)
func (s *Server) setupFirewall() error {
s.cleanupFirewall()
backend := s.detectFirewallBackend()
switch backend {
case NFT:
return s.nftSetup()
case IPT:
return s.iptSetup()
default:
return fmt.Errorf("unsupported or no firewall backend: %s", backend)
}
}
func (s *Server) cleanupFirewall() error {
s.nftCleanup()
s.iptCleanup()
return nil
}
func (s *Server) detectFirewallBackend() string {
// Check if opkg is available (OpenWrt environment)
if isCommandAvailable("opkg") {
switch s.cfg.ServerMode {
case config.ServerModeTProxy:
// Check if kmod-nft-tproxy is installed
if isOpkgPackageInstalled("kmod-nft-tproxy") && isCommandAvailable("nft") {
logrus.Info("Detected nftables backend (kmod-nft-tproxy installed)")
return NFT
}
logrus.Info("Detected iptables backend (kmod-nft-tproxy not installed)")
return IPT
case config.ServerModeNFQueue:
// Check if kmod-nft-queue is installed
if isOpkgPackageInstalled("kmod-nft-queue") && isCommandAvailable("nft") {
logrus.Info("Detected nftables backend (kmod-nft-queue installed)")
return NFT
}
logrus.Info("Detected iptables backend (kmod-nft-queue not installed)")
return IPT
}
}
// Check if nft command is available
if isCommandAvailable("nft") {
logrus.Info("Detected nftables backend (nft command available)")
return NFT
}
// Check if iptables command is available
if isCommandAvailable("iptables") {
logrus.Info("Detected iptables backend (iptables command available)")
return IPT
}
// No backend detected
logrus.Warn("No firewall backend detected")
return ""
}

View File

@ -0,0 +1,96 @@
package netlink
import (
"github.com/coreos/go-iptables/iptables"
)
const (
table = "mangle"
chain = "POSTROUTING"
)
var RuleTTL = []string{
"-j", "TTL",
"--ttl-set", "64",
}
var RuleIP = []string{
"-j", "QUEUE",
"--queue-num", "10301",
"--queue-bypass",
}
var RuleDelTCPTS = []string{
"-p", "tcp",
"--tcp-flags", "SYN,RST,ACK,FIN",
"-j", "QUEUE",
"--queue-num", "10301",
"--queue-bypass",
}
func (s *Server) iptSetup() error {
_ = s.iptCleanup()
ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
if err != nil {
return err
}
if s.cfg.SetTTL {
err = IptSetTTL(ipt)
if err != nil {
return err
}
}
if s.cfg.DelTCPTimestamp && !s.cfg.SetIPID {
err = IptDelTCPTS(ipt)
if err != nil {
return err
}
}
if s.cfg.SetIPID {
err = IptSetIP(ipt)
if err != nil {
return err
}
}
return nil
}
func (s *Server) iptCleanup() error {
ipt, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
if err != nil {
return err
}
_ = ipt.DeleteIfExists(table, chain, RuleTTL...)
_ = ipt.DeleteIfExists(table, chain, RuleIP...)
err = ipt.DeleteIfExists(table, chain, RuleDelTCPTS...)
if err != nil {
return err
}
return nil
}
// IptSetTTL creates a chain that sets TTL to 64 for IPv4 packets
func IptSetTTL(ipt *iptables.IPTables) error {
err := ipt.Append(table, chain, RuleTTL...)
if err != nil {
return err
}
return nil
}
func IptSetIP(ipt *iptables.IPTables) error {
err := ipt.Append(table, chain, RuleIP...)
if err != nil {
return err
}
return nil
}
func IptDelTCPTS(ipt *iptables.IPTables) error {
err := ipt.Append(table, chain, RuleDelTCPTS...)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,122 @@
package netlink
import (
nfq "github.com/florianl/go-nfqueue/v2"
"github.com/google/gopacket/layers"
"github.com/sirupsen/logrus"
"github.com/sunbk201/ua3f/internal/config"
"github.com/sunbk201/ua3f/internal/netfilter"
"sigs.k8s.io/knftables"
)
type Server struct {
cfg *config.Config
nfqServer *netfilter.NfqueueServer
nftable *knftables.Table
}
func New(cfg *config.Config) *Server {
s := &Server{
cfg: cfg,
nfqServer: &netfilter.NfqueueServer{
QueueNum: 10301,
},
nftable: &knftables.Table{
Name: "UA3F_HELPER",
Family: knftables.InetFamily,
},
}
s.nfqServer.HandlePacket = s.handlePacket
return s
}
func (s *Server) Setup() (err error) {
err = s.setupFirewall()
if err != nil {
return err
}
return nil
}
func (s *Server) Start() (err error) {
if s.cfg.SetTTL || s.cfg.DelTCPTimestamp || s.cfg.SetIPID {
logrus.Info("Packet modification features enabled")
return s.nfqServer.Start()
}
return nil
}
func (s *Server) Close() (err error) {
err = s.cleanupFirewall()
if err != nil {
return err
}
// err = s.nfqServer.Nf.Close()
return nil
}
// handlePacket processes a single NFQUEUE packet
func (s *Server) handlePacket(packet *netfilter.Packet) {
nf := s.nfqServer.Nf
modified := false
if s.cfg.DelTCPTimestamp && packet.TCP != nil {
modified = s.clearTCPTimestamp(packet.TCP) || modified
}
if s.cfg.SetIPID {
modified = s.zeroIPID(packet) || modified
}
if modified {
newPacket, err := packet.Serialize()
if err != nil {
logrus.Errorf("packet.Serialize: %v", err)
_ = nf.SetVerdict(*packet.A.PacketID, nfq.NfAccept)
return
}
if err := nf.SetVerdictWithOption(*packet.A.PacketID, nfq.NfAccept, nfq.WithAlteredPacket(newPacket)); err != nil {
logrus.Errorf("nf.SetVerdictWithOption: %v", err)
_ = nf.SetVerdict(*packet.A.PacketID, nfq.NfAccept)
}
} else {
_ = nf.SetVerdict(*packet.A.PacketID, nfq.NfAccept)
}
}
// clearTCPTimestamp removes the TCP timestamp option from the TCP layer
// Returns true if the timestamp option was found and removed
func (s *Server) clearTCPTimestamp(tcp *layers.TCP) bool {
if len(tcp.Options) == 0 {
return false
}
modified := false
newOptions := make([]layers.TCPOption, 0, len(tcp.Options))
for _, opt := range tcp.Options {
// TCP Timestamp option kind is 8
if opt.OptionType == 8 {
modified = true
continue
}
newOptions = append(newOptions, opt)
}
if modified {
tcp.Options = newOptions
}
return modified
}
// zeroIPID sets the IP ID field to zero for IPv4 packets
// Returns true if the packet was modified
func (s *Server) zeroIPID(packet *netfilter.Packet) bool {
if packet.IsIPv6 {
return false
}
ip4 := packet.NetworkLayer.(*layers.IPv4)
if ip4.Id == 0 {
return false
}
ip4.Id = 0
return true
}

View File

@ -0,0 +1,107 @@
package netlink
import (
"context"
"github.com/sirupsen/logrus"
"sigs.k8s.io/knftables"
)
func (s *Server) nftSetup() error {
// Clean up existing table if any
_ = s.nftCleanup()
nft, err := knftables.New(s.nftable.Family, s.nftable.Name)
if err != nil {
logrus.Errorf("Failed to create nftables table: %v", err)
return err
}
tx := nft.NewTransaction()
tx.Add(s.nftable)
if s.cfg.SetTTL {
NftSetTTL(tx, s.nftable)
}
if s.cfg.DelTCPTimestamp && !s.cfg.SetIPID {
NftDelTCPTS(tx, s.nftable)
}
if s.cfg.SetIPID {
NftSetIP(tx, s.nftable)
}
if err := nft.Run(context.TODO(), tx); err != nil {
logrus.Errorf("Failed to run nftables transaction: %v", err)
return err
}
logrus.Info("Nftables setup completed")
return nil
}
func (s *Server) nftCleanup() error {
nft, err := knftables.New(s.nftable.Family, s.nftable.Name)
if err != nil {
return err
}
tx := nft.NewTransaction()
tx.Delete(s.nftable)
if err := nft.Run(context.TODO(), tx); err != nil {
return err
}
return nil
}
// NftSetTTL creates a chain that sets TTL to 64 for IPv4 packets
func NftSetTTL(tx *knftables.Transaction, table *knftables.Table) {
chain := &knftables.Chain{
Name: "TTL64",
Type: knftables.PtrTo(knftables.FilterType),
Table: table.Name,
Hook: knftables.PtrTo(knftables.PostroutingHook),
Priority: knftables.PtrTo(knftables.ManglePriority),
}
rule := &knftables.Rule{
Chain: chain.Name,
Rule: knftables.Concat(
"ip ttl set 64",
),
}
tx.Add(chain)
tx.Add(rule)
}
func NftSetIP(tx *knftables.Transaction, table *knftables.Table) {
chain := &knftables.Chain{
Name: "HELPER_QUEUE",
Type: knftables.PtrTo(knftables.FilterType),
Table: table.Name,
Hook: knftables.PtrTo(knftables.PostroutingHook),
Priority: knftables.PtrTo(knftables.ManglePriority),
}
rule := &knftables.Rule{
Chain: chain.Name,
Rule: knftables.Concat(
"counter queue num 10301 bypass",
),
}
tx.Add(chain)
tx.Add(rule)
}
func NftDelTCPTS(tx *knftables.Transaction, table *knftables.Table) {
chain := &knftables.Chain{
Name: "HELPER_QUEUE",
Type: knftables.PtrTo(knftables.FilterType),
Table: table.Name,
Hook: knftables.PtrTo(knftables.PostroutingHook),
Priority: knftables.PtrTo(knftables.ManglePriority),
}
rule := &knftables.Rule{
Chain: chain.Name,
Rule: knftables.Concat(
"tcp flags syn counter queue num 10301 bypass",
),
}
tx.Add(chain)
tx.Add(rule)
}

View File

@ -0,0 +1,19 @@
package netlink
import "os/exec"
// isCommandAvailable checks if a command is available in the system
func isCommandAvailable(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
// isOpkgPackageInstalled checks if a package is installed via opkg
func isOpkgPackageInstalled(pkg string) bool {
cmd := exec.Command("opkg", "list-installed", pkg)
output, err := cmd.Output()
if err != nil {
return false
}
return len(output) > 0
}

View File

@ -38,6 +38,8 @@ func (s *Server) Start() (err error) {
if client, err = s.listener.Accept(); err != nil { if client, err = s.listener.Accept(); err != nil {
if errors.Is(err, syscall.EMFILE) { if errors.Is(err, syscall.EMFILE) {
time.Sleep(time.Second) time.Sleep(time.Second)
} else if errors.Is(err, net.ErrClosed) {
return nil
} }
logrus.Error("s.listener.Accept:", err) logrus.Error("s.listener.Accept:", err)
continue continue

View File

@ -65,6 +65,8 @@ func (s *Server) Start() (err error) {
if client, err = s.listener.Accept(); err != nil { if client, err = s.listener.Accept(); err != nil {
if errors.Is(err, syscall.EMFILE) { if errors.Is(err, syscall.EMFILE) {
time.Sleep(time.Second) time.Sleep(time.Second)
} else if errors.Is(err, net.ErrClosed) {
return nil
} }
logrus.Error("s.listener.Accept:", err) logrus.Error("s.listener.Accept:", err)
continue continue

View File

@ -58,6 +58,8 @@ func (s *Server) Start() error {
if client, err = s.listener.Accept(); err != nil { if client, err = s.listener.Accept(); err != nil {
if errors.Is(err, syscall.EMFILE) { if errors.Is(err, syscall.EMFILE) {
time.Sleep(time.Second) time.Sleep(time.Second)
} else if errors.Is(err, net.ErrClosed) {
return nil
} }
logrus.Error("s.listener.Accept:", err) logrus.Error("s.listener.Accept:", err)
continue continue

View File

@ -11,6 +11,7 @@ import (
"github.com/sunbk201/ua3f/internal/log" "github.com/sunbk201/ua3f/internal/log"
"github.com/sunbk201/ua3f/internal/rewrite" "github.com/sunbk201/ua3f/internal/rewrite"
"github.com/sunbk201/ua3f/internal/server" "github.com/sunbk201/ua3f/internal/server"
"github.com/sunbk201/ua3f/internal/server/netlink"
"github.com/sunbk201/ua3f/internal/statistics" "github.com/sunbk201/ua3f/internal/statistics"
) )
@ -38,22 +39,35 @@ func main() {
log.LogHeader(version, cfg) log.LogHeader(version, cfg)
go statistics.StartRecorder() helper := netlink.New(cfg)
err = helper.Setup()
if err != nil {
logrus.Fatal(err)
}
cleanup := make(chan os.Signal, 1) cleanup := make(chan os.Signal, 1)
signal.Notify(cleanup, syscall.SIGINT, syscall.SIGTERM) signal.Notify(cleanup, syscall.SIGINT, syscall.SIGTERM)
go helper.Start()
go statistics.StartRecorder()
go func() { go func() {
<-cleanup <-cleanup
logrus.Info("Shutting down UA3F...") logrus.Info("Shutting down UA3F...")
if err := helper.Close(); err != nil {
logrus.Errorf("Error during helper close: %v", err)
}
if err := srv.Close(); err != nil { if err := srv.Close(); err != nil {
logrus.Errorf("Error during UA3F close: %v", err) logrus.Errorf("Error during UA3F close: %v", err)
} }
logrus.Info("UA3F exited gracefully") logrus.Info("UA3F exited gracefully")
os.Exit(0) os.Exit(0)
}() }()
if err := srv.Start(); err != nil { if err := srv.Start(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
helper.Close()
srv.Close()
} }