diff --git a/ipkg/CONTROL/control b/ipkg/CONTROL/control
index a5b750b..6e8a82a 100644
--- a/ipkg/CONTROL/control
+++ b/ipkg/CONTROL/control
@@ -1,11 +1,11 @@
Package: ua3f
Version: 0.9.0-1
-Depends: luci-compat
+Depends: luci-compat, ipset, iptables, kmod-nft-tproxy, iptables-mod-tproxy, iptables-mod-extra, iptables-mod-nat-extra, kmod-ipt-conntrack
Source: /feed/openwrt
SourceName: UA3F
License: GPL-3.0-only
Section: net
SourceDateEpoch: 1711267200
Architecture: all
-Installed-Size: 3563520
+Installed-Size: 3573760
Description: Advanced HTTP User-Agent Rewriting Tool.
diff --git a/ipkg/CONTROL/control-e b/ipkg/CONTROL/control-e
index 07bb4d9..3b37c9c 100644
--- a/ipkg/CONTROL/control-e
+++ b/ipkg/CONTROL/control-e
@@ -1,11 +1,11 @@
Package: ua3f
Version: 0.9.0-1
-Depends: luci-compat
+Depends: luci-compat, ipset, iptables, kmod-nft-tproxy, iptables-mod-tproxy, iptables-mod-extra, iptables-mod-nat-extra, kmod-ipt-conntrack
Source: /feed/openwrt
SourceName: UA3F
License: GPL-3.0-only
Section: net
SourceDateEpoch: 1711267200
Architecture: all
-Installed-Size: 3696640
+Installed-Size: 3706880
Description: Advanced HTTP User-Agent Rewriting Tool.
diff --git a/openwrt/Makefile b/openwrt/Makefile
index cec80e9..419163c 100644
--- a/openwrt/Makefile
+++ b/openwrt/Makefile
@@ -28,7 +28,7 @@ define Package/ua3f
SUBMENU:=Web Servers/Proxies
TITLE:=A SOCKS5 Server for User-Agent Rewriting
URL:=https://github.com/SunBK201/UA3F
- DEPENDS:=$(GO_ARCH_DEPENDS) +luci-compat
+ DEPENDS:=$(GO_ARCH_DEPENDS) +luci-compat +ipset +iptables +kmod-nft-tproxy +iptables-mod-tproxy +iptables-mod-extra +iptables-mod-nat-extra +kmod-ipt-conntrack
endef
define Package/ua3f/description
diff --git a/openwrt/files/luci/cbi.lua b/openwrt/files/luci/cbi.lua
index a27a29c..be6a857 100644
--- a/openwrt/files/luci/cbi.lua
+++ b/openwrt/files/luci/cbi.lua
@@ -20,10 +20,10 @@ running.cfgvalue = function(self, section)
local pid = luci.sys.exec("pidof ua3f")
if pid == "" then
return ""
+ translate("Stop") .. "'/>"
else
return ""
+ translate("Running") .. "'/>"
end
end
@@ -31,6 +31,11 @@ general:tab("general", translate("Settings"))
general:tab("stats", translate("Statistics"))
general:tab("log", translate("Log"))
+server_mode = general:taboption("general", ListValue, "server_mode", translate("Server Mode"))
+server_mode:value("SOCKS5", "SOCKS5")
+server_mode:value("TPROXY", "TPROXY")
+server_mode:value("REDIRECT", "REDIRECT")
+
port = general:taboption("general", Value, "port", translate("Port"))
port.placeholder = "1080"
diff --git a/openwrt/files/ua3f.init b/openwrt/files/ua3f.init
index 4ba7556..3debf0d 100755
--- a/openwrt/files/ua3f.init
+++ b/openwrt/files/ua3f.init
@@ -7,6 +7,198 @@ START=99
NAME="ua3f"
PROG="/usr/bin/$NAME"
+SERVER_MODE=""
+SERVER_PORT="1080"
+
+FW_BACKEND=""
+NFT_TABLE="UA3F"
+UA3F_CHAIN="UA3F"
+UA3F_OUT_CHAIN="UA3F_OUTPUT"
+IPSET_NAME="localnetwork"
+UA3FMARK="0xc9"
+FWMARK="0x1c9"
+ROUTE_TABLE="0x1c9"
+
+RUNDIR="/var/run/${NAME}"
+[ -d "$RUNDIR" ] || mkdir -p "$RUNDIR"
+ROUTE_CREATED_FLAG="$RUNDIR/route_created"
+IPSET_CREATED_FLAG="$RUNDIR/ipset_created"
+
+log() { logger -t "$NAME" -- "$*"; }
+
+try_modprobe() { command -v modprobe >/dev/null 2>&1 && modprobe "$1" 2>/dev/null; }
+
+nft_available() { command -v nft >/dev/null 2>&1; }
+ipt_available() { command -v iptables >/dev/null 2>&1; }
+detect_backend() {
+ if nft_available; then
+ FW_BACKEND="ipt"
+ return 0
+ fi
+ if ipt_available; then
+ FW_BACKEND="ipt"
+ return 0
+ fi
+ FW_BACKEND=""
+ return 1
+}
+
+ensure_tproxy_route() {
+ ip rule add fwmark "$FWMARK" table "$ROUTE_TABLE" 2>/dev/null
+ ip route add local 0.0.0.0/0 dev lo table "$ROUTE_TABLE" 2>/dev/null
+ echo 1 >"$ROUTE_CREATED_FLAG"
+}
+cleanup_tproxy_route() {
+ ip route del local 0.0.0.0/0 dev lo table "$ROUTE_TABLE" 2>/dev/null
+ ip rule del fwmark "$FWMARK" table "$ROUTE_TABLE" 2>/dev/null
+ rm -f "$ROUTE_CREATED_FLAG"
+}
+
+nft_drop_table() { nft delete table ip "$NFT_TABLE" 2>/dev/null; }
+
+nft_reinit_table() {
+ nft_drop_table
+ nft add table ip "$NFT_TABLE" || return 1
+
+ # set: localnetwork
+ nft "add set ip $NFT_TABLE $IPSET_NAME { type ipv4_addr; flags interval; auto-merge; }" || return 1
+ nft "add element ip $NFT_TABLE $IPSET_NAME { 0.0.0.0/8, 127.0.0.0/8, 10.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4, 100.64.0.0/10 }" >/dev/null 2>&1
+}
+
+fw_setup_nft_tproxy_tcp() {
+ nft_reinit_table || return 1
+ ensure_tproxy_route
+
+ # PREROUTING -> UA3F
+ nft add chain ip $NFT_TABLE prerouting '{ type filter hook prerouting priority mangle; }'
+ nft add rule ip $NFT_TABLE prerouting mark $UA3FMARK return
+ nft add rule ip $NFT_TABLE prerouting ip daddr @$IPSET_NAME return
+ nft add rule ip $NFT_TABLE prerouting ct direction reply return
+ nft add rule ip $NFT_TABLE prerouting meta l4proto tcp mark set $FWMARK tproxy to 127.0.0.1:$SERVER_PORT counter accept
+
+ # OUTPUT -> UA3F_OUTPUT
+ nft add chain ip $NFT_TABLE output '{ type route hook output priority mangle; }'
+ nft add rule ip $NFT_TABLE output mark $UA3FMARK return
+ nft add rule ip $NFT_TABLE output meta skgid 65534 return
+ nft add rule ip $NFT_TABLE output ip daddr @$IPSET_NAME return
+ nft add rule ip $NFT_TABLE output meta l4proto tcp mark set $FWMARK counter accept
+}
+
+fw_setup_nft_redirect_tcp() {
+ nft_reinit_table || return 1
+
+ # PREROUTING -> UA3F
+ nft add chain ip $NFT_TABLE prerouting '{ type nat hook prerouting priority mangle; }'
+ nft add rule ip $NFT_TABLE prerouting mark $UA3FMARK return
+ nft add rule ip $NFT_TABLE prerouting ip daddr @$IPSET_NAME return
+ nft add rule ip $NFT_TABLE prerouting ct direction reply return
+ nft add rule ip $NFT_TABLE prerouting tcp dport != {22} redirect to :$SERVER_PORT
+
+ # OUTPUT -> UA3F_OUTPUT
+ nft add chain ip $NFT_TABLE output '{ type nat hook output priority mangle; }'
+ nft add rule ip $NFT_TABLE output mark $UA3FMARK return
+ nft add rule ip $NFT_TABLE output meta skgid 65534 return
+ nft add rule ip $NFT_TABLE output ip daddr @$IPSET_NAME return
+ nft add rule ip $NFT_TABLE output tcp dport != {22} redirect to :$SERVER_PORT
+}
+
+fw_revert_nft() {
+ nft_drop_table
+ [ -f "$ROUTE_CREATED_FLAG" ] && cleanup_tproxy_route
+}
+
+ensure_local_set_ipt() {
+ if ! ipset list "$IPSET_NAME" >/dev/null 2>&1; then
+ ipset create "$IPSET_NAME" hash:net maxelem 1048576 || return 1
+ echo 1 >"$IPSET_CREATED_FLAG"
+ ipset add "$IPSET_NAME" 0.0.0.0/8
+ ipset add "$IPSET_NAME" 127.0.0.0/8
+ ipset add "$IPSET_NAME" 10.0.0.0/8
+ ipset add "$IPSET_NAME" 169.254.0.0/16
+ ipset add "$IPSET_NAME" 172.16.0.0/12
+ ipset add "$IPSET_NAME" 192.168.0.0/16
+ ipset add "$IPSET_NAME" 224.0.0.0/4
+ ipset add "$IPSET_NAME" 240.0.0.0/4
+ ipset add "$IPSET_NAME" 100.64.0.0/10
+ fi
+}
+
+fw_setup_ipt_tproxy_tcp() {
+ ensure_local_set_ipt || return 1
+ ensure_tproxy_route
+
+ # PREROUTING
+ iptables -t mangle -F "$UA3F_CHAIN" 2>/dev/null
+ iptables -t mangle -D PREROUTING -p tcp -j "$UA3F_CHAIN" 2>/dev/null
+ iptables -t mangle -X "$UA3F_CHAIN" 2>/dev/null
+ iptables -t mangle -N "$UA3F_CHAIN"
+ iptables -t mangle -A PREROUTING -p tcp -j "$UA3F_CHAIN"
+ iptables -t mangle -A "$UA3F_CHAIN" -p tcp -m mark --mark "$UA3FMARK" -j RETURN
+ iptables -t mangle -A "$UA3F_CHAIN" -m set --match-set "$IPSET_NAME" dst -j RETURN
+ iptables -t mangle -A "$UA3F_CHAIN" -m conntrack --ctdir REPLY -j RETURN
+ iptables -t mangle -A "$UA3F_CHAIN" -p tcp -j TPROXY --on-port "$SERVER_PORT" --tproxy-mark "$FWMARK"
+
+ # OUTPUT
+ iptables -t mangle -F "$UA3F_OUT_CHAIN" 2>/dev/null
+ iptables -t mangle -D OUTPUT -p tcp -j "$UA3F_OUT_CHAIN" 2>/dev/null
+ iptables -t mangle -X "$UA3F_OUT_CHAIN" 2>/dev/null
+ iptables -t mangle -N "$UA3F_OUT_CHAIN"
+ iptables -t mangle -A OUTPUT -p tcp -j "$UA3F_OUT_CHAIN"
+ iptables -t mangle -A "$UA3F_OUT_CHAIN" -p tcp -m mark --mark "$UA3FMARK" -j RETURN
+ iptables -t mangle -A "$UA3F_OUT_CHAIN" -p tcp -m owner --gid-owner 65534 -j RETURN
+ iptables -t mangle -A "$UA3F_OUT_CHAIN" -m set --match-set "$IPSET_NAME" dst -j RETURN
+ iptables -t mangle -A "$UA3F_OUT_CHAIN" -p tcp -j MARK --set-mark "$FWMARK"
+}
+
+fw_setup_ipt_redirect_tcp() {
+ ensure_local_set_ipt || return 1
+
+ # PREROUTING
+ iptables -t nat -F "$UA3F_CHAIN" 2>/dev/null
+ iptables -t nat -D PREROUTING -p tcp -j "$UA3F_CHAIN" 2>/dev/null
+ iptables -t nat -X "$UA3F_CHAIN" 2>/dev/null
+ iptables -t nat -N "$UA3F_CHAIN"
+ iptables -t nat -A PREROUTING -p tcp -j "$UA3F_CHAIN"
+ iptables -t nat -A "$UA3F_CHAIN" -p tcp -m mark --mark "$UA3FMARK" -j RETURN
+ iptables -t nat -A "$UA3F_CHAIN" -m set --match-set "$IPSET_NAME" dst -j RETURN
+ iptables -t nat -A "$UA3F_CHAIN" -m conntrack --ctdir REPLY -j RETURN
+ iptables -t nat -A "$UA3F_CHAIN" -p tcp -j REDIRECT --to-ports "$SERVER_PORT"
+
+ # OUTPUT
+ iptables -t nat -F "$UA3F_OUT_CHAIN" 2>/dev/null
+ iptables -t nat -D OUTPUT -p tcp -j "$UA3F_OUT_CHAIN" 2>/dev/null
+ iptables -t nat -X "$UA3F_OUT_CHAIN" 2>/dev/null
+ iptables -t nat -N "$UA3F_OUT_CHAIN"
+ iptables -t nat -A OUTPUT -p tcp -j "$UA3F_OUT_CHAIN"
+ iptables -t nat -A "$UA3F_OUT_CHAIN" -p tcp -m mark --mark "$UA3FMARK" -j RETURN
+ iptables -t nat -A "$UA3F_OUT_CHAIN" -p tcp -m owner --gid-owner 65534 -j RETURN
+ iptables -t nat -A "$UA3F_OUT_CHAIN" -m set --match-set "$IPSET_NAME" dst -j RETURN
+ iptables -t nat -A "$UA3F_OUT_CHAIN" -p tcp -j REDIRECT --to-ports "$SERVER_PORT"
+}
+
+fw_revert_ipt() {
+ # mangle
+ iptables -t mangle -D PREROUTING -p tcp -j "$UA3F_CHAIN" 2>/dev/null
+ iptables -t mangle -F "$UA3F_CHAIN" 2>/dev/null
+ iptables -t mangle -X "$UA3F_CHAIN" 2>/dev/null
+ iptables -t mangle -D OUTPUT -p tcp -j "$UA3F_OUT_CHAIN" 2>/dev/null
+ iptables -t mangle -F "$UA3F_OUT_CHAIN" 2>/dev/null
+ iptables -t mangle -X "$UA3F_OUT_CHAIN" 2>/dev/null
+ # nat
+ iptables -t nat -D PREROUTING -p tcp -j "$UA3F_CHAIN" 2>/dev/null
+ iptables -t nat -F "$UA3F_CHAIN" 2>/dev/null
+ iptables -t nat -X "$UA3F_CHAIN" 2>/dev/null
+ iptables -t nat -D OUTPUT -p tcp -j "$UA3F_OUT_CHAIN" 2>/dev/null
+ iptables -t nat -F "$UA3F_OUT_CHAIN" 2>/dev/null
+ iptables -t nat -X "$UA3F_OUT_CHAIN" 2>/dev/null
+ # ipset
+ if [ -f "$IPSET_CREATED_FLAG" ]; then
+ ipset destroy "$IPSET_NAME" 2>/dev/null
+ rm -f "$IPSET_CREATED_FLAG"
+ fi
+ [ -f "$ROUTE_CREATED_FLAG" ] && cleanup_tproxy_route
+}
+
start_service() {
config_load "$NAME"
@@ -16,12 +208,8 @@ start_service() {
return 1
fi
- local port
- local bind
- local ua
- local log_level
- local ua_regex
- local partial_replace
+ local server_mode port bind ua log_level ua_regex partial_replace
+ config_get server_mode "main" "server_mode" "SOCKS5"
config_get port "main" "port" "1080"
config_get bind "main" "bind" "127.0.0.1"
config_get ua "main" "ua" "FFF"
@@ -29,13 +217,67 @@ start_service() {
config_get_bool partial_replace "main" "partial_replace" 0
config_get log_level "main" "log_level" "info"
+ SERVER_MODE="$(echo "$server_mode" | tr '[:lower:]' '[:upper:]')"
+ SERVER_MODE="$server_mode"
+
mkdir -p /var/log/ua3f
chmod o+w /var/log/ua3f
+ detect_backend || {
+ log "no firewall backend found"
+ return 1
+ }
+
+ # Always cleanup first (idempotent)
+ if [ "$FW_BACKEND" = "nft" ]; then
+ fw_revert_nft
+ else
+ fw_revert_ipt
+ fi
+
+ case "$SERVER_MODE" in
+ SOCKS5)
+ # No firewall interception
+ ;;
+ TPROXY)
+ if [ "$FW_BACKEND" = "nft" ]; then
+ try_modprobe nft_tproxy
+ fw_setup_nft_tproxy_tcp || {
+ log "nft TPROXY setup failed"
+ return 1
+ }
+ else
+ try_modprobe xt_TPROXY
+ fw_setup_ipt_tproxy_tcp || {
+ log "iptables TPROXY setup failed"
+ return 1
+ }
+ fi
+ ;;
+ REDIRECT)
+ if [ "$FW_BACKEND" = "nft" ]; then
+ fw_setup_nft_redirect_tcp || {
+ log "nft REDIRECT setup failed"
+ return 1
+ }
+ else
+ fw_setup_ipt_redirect_tcp || {
+ log "iptables REDIRECT setup failed"
+ return 1
+ }
+ fi
+ ;;
+ *)
+ log "unknown server_mode: $SERVER_MODE"
+ return 1
+ ;;
+ esac
+
procd_open_instance "$NAME"
procd_set_param command "$PROG"
- procd_append_param command -b "$bind"
+ procd_append_param command -m "$server_mode"
procd_append_param command -p $port
+ procd_append_param command -b "$bind"
procd_append_param command -f "$ua"
procd_append_param command -r "$ua_regex"
procd_append_param command -l $log_level
@@ -56,6 +298,15 @@ start_service() {
procd_close_instance
}
+stop_service() {
+ detect_backend
+ if [ "$FW_BACKEND" = "nft" ]; then
+ fw_revert_nft
+ else
+ fw_revert_ipt
+ fi
+}
+
reload_service() {
stop
start
diff --git a/openwrt/files/ua3f.uci b/openwrt/files/ua3f.uci
index 14c4bf8..982480a 100644
--- a/openwrt/files/ua3f.uci
+++ b/openwrt/files/ua3f.uci
@@ -2,6 +2,7 @@ config 'ua3f' 'enabled'
option enabled '0'
config 'ua3f' 'main'
+ option server_mode 'SOCKS5'
option port '1080'
option bind '127.0.0.1'
option ua 'FFF'
diff --git a/openwrt/po/zh_cn/ua3f.po b/openwrt/po/zh_cn/ua3f.po
index c66ad80..921a254 100644
--- a/openwrt/po/zh_cn/ua3f.po
+++ b/openwrt/po/zh_cn/ua3f.po
@@ -26,6 +26,9 @@ msgstr "统计信息"
msgid "Log"
msgstr "运行日志"
+msgid "Server Mode"
+msgstr "服务模式"
+
msgid "Port"
msgstr "端口"
diff --git a/src/go.mod b/src/go.mod
index cd588bf..fcd6941 100644
--- a/src/go.mod
+++ b/src/go.mod
@@ -6,10 +6,8 @@ require (
github.com/dlclark/regexp2 v1.11.4
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/sirupsen/logrus v1.9.3
+ golang.org/x/sys v0.26.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
-require (
- github.com/stretchr/testify v1.8.2 // indirect
- golang.org/x/sys v0.26.0 // indirect
-)
+require github.com/stretchr/testify v1.8.2 // indirect
diff --git a/src/internal/config/config.go b/src/internal/config/config.go
index 974dead..7a59e10 100644
--- a/src/internal/config/config.go
+++ b/src/internal/config/config.go
@@ -3,11 +3,13 @@ package config
import (
"flag"
"fmt"
+ "strings"
)
const (
- ServerModeSocks5 = "socks5"
- ServerModeTProxy = "tproxy"
+ ServerModeSocks5 = "SOCKS5"
+ ServerModeTProxy = "TPROXY"
+ ServerModeRedirect = "REDIRECT"
)
type Config struct {
@@ -33,7 +35,7 @@ func Parse() (*Config, bool) {
showVer bool
)
- flag.StringVar(&serverMode, "m", ServerModeSocks5, "server mode: socks5 or tproxy (default: socks5)")
+ flag.StringVar(&serverMode, "m", ServerModeSocks5, "server mode: SOCKS5, TPROXY, REDIRECT (default: SOCKS5)")
flag.StringVar(&bindAddr, "b", "127.0.0.1", "bind address (default: 127.0.0.1)")
flag.IntVar(&port, "p", 1080, "port")
flag.StringVar(&payloadUA, "f", "FFF", "User-Agent")
@@ -44,7 +46,7 @@ func Parse() (*Config, bool) {
flag.Parse()
cfg := &Config{
- ServerMode: serverMode,
+ ServerMode: strings.ToUpper(serverMode),
BindAddr: bindAddr,
Port: port,
ListenAddr: fmt.Sprintf("%s:%d", bindAddr, port),
diff --git a/src/internal/log/log.go b/src/internal/log/log.go
index b7b0d77..b2ed512 100644
--- a/src/internal/log/log.go
+++ b/src/internal/log/log.go
@@ -65,6 +65,7 @@ func SetLogConf(level string) {
func LogHeader(version string, cfg *config.Config) {
logrus.Info("UA3F v" + version)
+ logrus.Info("Server Mode: " + cfg.ServerMode)
logrus.Infof("Listen on %s", cfg.ListenAddr)
logrus.Infof("User-Agent: %s", cfg.PayloadUA)
logrus.Infof("User-Agent Regex Pattern: '%s'", cfg.UAPattern)
diff --git a/src/internal/server/redirect/redirect_linux.go b/src/internal/server/redirect/redirect_linux.go
new file mode 100644
index 0000000..3bddb6a
--- /dev/null
+++ b/src/internal/server/redirect/redirect_linux.go
@@ -0,0 +1,92 @@
+//go:build linux
+
+package redirect
+
+import (
+ "fmt"
+ "net"
+
+ "github.com/sirupsen/logrus"
+ "github.com/sunbk201/ua3f/internal/config"
+ "github.com/sunbk201/ua3f/internal/rewrite"
+ "github.com/sunbk201/ua3f/internal/server/utils"
+ "github.com/sunbk201/ua3f/internal/statistics"
+ "golang.org/x/sys/unix"
+)
+
+type Server struct {
+ cfg *config.Config
+ rw *rewrite.Rewriter
+ listener net.Listener
+}
+
+func New(cfg *config.Config, rw *rewrite.Rewriter) *Server {
+ return &Server{
+ cfg: cfg,
+ rw: rw,
+ }
+}
+
+func (s *Server) Start() (err error) {
+ if s.listener, err = net.Listen("tcp", s.cfg.ListenAddr); err != nil {
+ return fmt.Errorf("listen failed: %w", err)
+ }
+
+ go statistics.StartRecorder()
+
+ var client net.Conn
+ for {
+ if client, err = s.listener.Accept(); err != nil {
+ logrus.Error("Accept failed: ", err)
+ continue
+ }
+ logrus.Debugf("Accept connection from %s", client.RemoteAddr().String())
+ go s.HandleClient(client)
+ }
+}
+
+func (s *Server) HandleClient(client net.Conn) {
+ addr, err := getOriginalDstAddr(client)
+ if err != nil {
+ _ = client.Close()
+ logrus.Errorf("Get original dst addr failed: %v", err)
+ return
+ }
+ logrus.Debugf("Original destination address: %s", addr)
+
+ target, err := utils.ConnectWithMark(addr, utils.SO_MARK)
+ if err != nil {
+ _ = client.Close()
+ logrus.Errorf("Dial target %s failed: %v", addr, err)
+ return
+ }
+
+ s.ForwardTCP(client, target, addr)
+}
+
+// ForwardTCP proxies traffic in both directions.
+// target->client uses raw copy.
+// client->target is processed by the rewriter (or raw if cached).
+func (s *Server) ForwardTCP(client, target net.Conn, destAddr string) {
+ // Server -> Client (raw)
+ go utils.CopyHalf(client, target)
+
+ // Client -> Server (rewriter)
+ go utils.ProxyHalf(target, client, s.rw, destAddr)
+}
+
+// getOriginalDstAddr retrieves the original destination address of the redirected connection.
+func getOriginalDstAddr(conn net.Conn) (addr string, err error) {
+ fd, err := utils.GetConnFD(conn)
+ if err != nil {
+ return "", fmt.Errorf("failed to get file descriptor: %v", err)
+ }
+ raw, err := unix.GetsockoptIPv6Mreq(fd, unix.SOL_IP, unix.SO_ORIGINAL_DST)
+ if err != nil {
+ return "", fmt.Errorf("getsockopt SO_ORIGINAL_DST failed: %v", err)
+ }
+
+ ip := net.IPv4(raw.Multiaddr[4], raw.Multiaddr[5], raw.Multiaddr[6], raw.Multiaddr[7])
+ port := uint16(raw.Multiaddr[2])<<8 + uint16(raw.Multiaddr[3])
+ return fmt.Sprintf("%s:%d", ip.String(), port), nil
+}
diff --git a/src/internal/server/redirect/redirect_others.go b/src/internal/server/redirect/redirect_others.go
new file mode 100644
index 0000000..4e225d5
--- /dev/null
+++ b/src/internal/server/redirect/redirect_others.go
@@ -0,0 +1,37 @@
+//go:build !linux
+
+package redirect
+
+import (
+ "fmt"
+ "net"
+
+ "github.com/sunbk201/ua3f/internal/config"
+ "github.com/sunbk201/ua3f/internal/rewrite"
+ "github.com/sunbk201/ua3f/internal/server/utils"
+)
+
+type Server struct {
+ cfg *config.Config
+ rw *rewrite.Rewriter
+}
+
+func New(cfg *config.Config, rw *rewrite.Rewriter) *Server {
+ return &Server{
+ cfg: cfg,
+ rw: rw,
+ }
+}
+
+func (s *Server) Start() error {
+ return fmt.Errorf("REDIRECT Mode is only supported on Linux")
+}
+
+func (s *Server) HandleClient(client net.Conn) {
+ defer client.Close()
+}
+
+func (s *Server) ForwardTCP(client, target net.Conn, _ string) {
+ go utils.CopyHalf(client, target)
+ go utils.CopyHalf(target, client)
+}
diff --git a/src/internal/server/server.go b/src/internal/server/server.go
index 754ae0a..8f4ae5b 100644
--- a/src/internal/server/server.go
+++ b/src/internal/server/server.go
@@ -6,6 +6,7 @@ import (
"github.com/sunbk201/ua3f/internal/config"
"github.com/sunbk201/ua3f/internal/rewrite"
+ "github.com/sunbk201/ua3f/internal/server/redirect"
"github.com/sunbk201/ua3f/internal/server/socks5"
"github.com/sunbk201/ua3f/internal/server/tproxy"
)
@@ -20,6 +21,10 @@ func NewServer(cfg *config.Config, rw *rewrite.Rewriter) (Server, error) {
switch cfg.ServerMode {
case config.ServerModeSocks5:
return socks5.New(cfg, rw), nil
+ case config.ServerModeTProxy:
+ return tproxy.New(cfg, rw), nil
+ case config.ServerModeRedirect:
+ return redirect.New(cfg, rw), nil
default:
return nil, fmt.Errorf("unknown server mode: %s", cfg.ServerMode)
}
diff --git a/src/internal/server/tproxy/tproxy_linux.go b/src/internal/server/tproxy/tproxy_linux.go
new file mode 100644
index 0000000..eac5951
--- /dev/null
+++ b/src/internal/server/tproxy/tproxy_linux.go
@@ -0,0 +1,113 @@
+//go:build linux
+
+package tproxy
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "syscall"
+
+ "golang.org/x/sys/unix"
+
+ "github.com/sirupsen/logrus"
+ "github.com/sunbk201/ua3f/internal/config"
+ "github.com/sunbk201/ua3f/internal/rewrite"
+ "github.com/sunbk201/ua3f/internal/server/utils"
+ "github.com/sunbk201/ua3f/internal/statistics"
+)
+
+type Server struct {
+ cfg *config.Config
+ rw *rewrite.Rewriter
+ listener net.Listener
+}
+
+func New(cfg *config.Config, rw *rewrite.Rewriter) *Server {
+ return &Server{
+ cfg: cfg,
+ rw: rw,
+ }
+}
+
+func (s *Server) Start() error {
+ lc := net.ListenConfig{
+ Control: func(network, address string, c syscall.RawConn) error {
+ var err error
+ c.Control(func(fd uintptr) {
+ if e := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1); e != nil {
+ err = fmt.Errorf("set SO_REUSEADDR: %v", e)
+ return
+ }
+ if e := unix.SetsockoptInt(int(fd), unix.SOL_IP, unix.IP_TRANSPARENT, 1); e != nil {
+ err = fmt.Errorf("set IP_TRANSPARENT: %v", e)
+ return
+ }
+ })
+ return err
+ },
+ }
+
+ var err error
+ if s.listener, err = lc.Listen(context.TODO(), "tcp", s.cfg.ListenAddr); err != nil {
+ return fmt.Errorf("listen failed: %w", err)
+ }
+
+ go statistics.StartRecorder()
+
+ var client net.Conn
+ for {
+ if client, err = s.listener.Accept(); err != nil {
+ logrus.Error("Accept failed: ", err)
+ continue
+ }
+ logrus.Debugf("Accept connection from %s", client.RemoteAddr().String())
+ go s.HandleClient(client)
+ }
+}
+
+func (s *Server) HandleClient(client net.Conn) {
+ addr, err := getOriginalDstAddr(client)
+ if err != nil {
+ _ = client.Close()
+ logrus.Errorf("Get original dst addr failed: %v", err)
+ return
+ }
+ logrus.Debugf("Original destination address: %s", addr)
+
+ target, err := utils.ConnectWithMark(addr, utils.SO_MARK)
+ if err != nil {
+ _ = client.Close()
+ logrus.Errorf("Dial target %s failed: %v", addr, err)
+ return
+ }
+
+ s.ForwardTCP(client, target, addr)
+}
+
+// ForwardTCP proxies traffic in both directions.
+// target->client uses raw copy.
+// client->target is processed by the rewriter (or raw if cached).
+func (s *Server) ForwardTCP(client, target net.Conn, destAddr string) {
+ // Server -> Client (raw)
+ go utils.CopyHalf(client, target)
+
+ // Client -> Server (rewriter)
+ go utils.ProxyHalf(target, client, s.rw, destAddr)
+}
+
+// getOriginalDstAddr retrieves the original destination address of a TProxy connection.
+func getOriginalDstAddr(conn net.Conn) (addr string, err error) {
+ fd, err := utils.GetConnFD(conn)
+ if err != nil {
+ return "", fmt.Errorf("failed to get file descriptor: %v", err)
+ }
+ raw, err := unix.GetsockoptIPv6Mreq(fd, unix.SOL_IP, unix.SO_ORIGINAL_DST)
+ if err != nil {
+ return "", fmt.Errorf("getsockopt SO_ORIGINAL_DST failed: %v", err)
+ }
+
+ ip := net.IPv4(raw.Multiaddr[4], raw.Multiaddr[5], raw.Multiaddr[6], raw.Multiaddr[7])
+ port := uint16(raw.Multiaddr[2])<<8 + uint16(raw.Multiaddr[3])
+ return fmt.Sprintf("%s:%d", ip.String(), port), nil
+}
diff --git a/src/internal/server/tproxy/tproxy_others.go b/src/internal/server/tproxy/tproxy_others.go
new file mode 100644
index 0000000..a04ec01
--- /dev/null
+++ b/src/internal/server/tproxy/tproxy_others.go
@@ -0,0 +1,37 @@
+//go:build !linux
+
+package tproxy
+
+import (
+ "fmt"
+ "net"
+
+ "github.com/sunbk201/ua3f/internal/config"
+ "github.com/sunbk201/ua3f/internal/rewrite"
+ "github.com/sunbk201/ua3f/internal/server/utils"
+)
+
+type Server struct {
+ cfg *config.Config
+ rw *rewrite.Rewriter
+}
+
+func New(cfg *config.Config, rw *rewrite.Rewriter) *Server {
+ return &Server{
+ cfg: cfg,
+ rw: rw,
+ }
+}
+
+func (s *Server) Start() error {
+ return fmt.Errorf("TPROXY Mode is only supported on Linux")
+}
+
+func (s *Server) HandleClient(client net.Conn) {
+ defer client.Close()
+}
+
+func (s *Server) ForwardTCP(client, target net.Conn, _ string) {
+ go utils.CopyHalf(client, target)
+ go utils.CopyHalf(target, client)
+}
diff --git a/src/internal/server/utils/tcp.go b/src/internal/server/utils/tcp.go
index 115e164..425aedc 100644
--- a/src/internal/server/utils/tcp.go
+++ b/src/internal/server/utils/tcp.go
@@ -1,6 +1,7 @@
package utils
import (
+ "fmt"
"io"
"net"
@@ -9,12 +10,12 @@ import (
)
// Connect dials the target address and returns the connection.
-func Connect(Addr string) (target net.Conn, err error) {
- logrus.Debugf("Connecting %s", Addr)
- if target, err = net.Dial("tcp", Addr); err != nil {
+func Connect(addr string) (target net.Conn, err error) {
+ logrus.Debugf("Connecting %s", addr)
+ if target, err = net.Dial("tcp", addr); err != nil {
return nil, err
}
- logrus.Debugf("Connected %s", Addr)
+ logrus.Debugf("Connected %s", addr)
return target, nil
}
@@ -52,3 +53,16 @@ func ProxyHalf(dst, src net.Conn, rw *rewrite.Rewriter, destAddrPort string) {
}()
_ = rw.ProxyHTTPOrRaw(dst, src, destAddrPort)
}
+
+func GetConnFD(conn net.Conn) (fd int, err error) {
+ tcpConn, ok := conn.(*net.TCPConn)
+ if !ok {
+ return 0, fmt.Errorf("not a TCP connection")
+ }
+ file, err := tcpConn.File()
+ if err != nil {
+ return 0, err
+ }
+
+ return int(file.Fd()), nil
+}
diff --git a/src/internal/server/utils/tcp_linux.go b/src/internal/server/utils/tcp_linux.go
new file mode 100644
index 0000000..2fa5f73
--- /dev/null
+++ b/src/internal/server/utils/tcp_linux.go
@@ -0,0 +1,34 @@
+//go:build linux
+
+package utils
+
+import (
+ "net"
+ "syscall"
+
+ "github.com/sirupsen/logrus"
+ "golang.org/x/sys/unix"
+)
+
+const SO_MARK = 0xc9
+
+// ConnectWithMark dials the target address with SO_MARK set and returns the connection.
+func ConnectWithMark(addr string, mark int) (target net.Conn, err error) {
+ logrus.Debugf("Connecting %s with SO_MARK=%d", addr, mark)
+
+ dialer := net.Dialer{
+ Control: func(network, address string, c syscall.RawConn) error {
+ return c.Control(func(fd uintptr) {
+ unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_MARK, mark)
+ })
+ },
+ }
+
+ conn, err := dialer.Dial("tcp", addr)
+ if err != nil {
+ return nil, err
+ }
+
+ logrus.Debugf("Connected %s with SO_MARK=%d", addr, mark)
+ return conn, nil
+}