From 5e5d450b55321123b6f0f5c1a585be18252d0a01 Mon Sep 17 00:00:00 2001 From: SunBK201 Date: Wed, 29 Oct 2025 01:31:02 +0800 Subject: [PATCH] feat: add support for multiple server modes (SOCKS5, TPROXY, REDIRECT) --- ipkg/CONTROL/control | 4 +- ipkg/CONTROL/control-e | 4 +- openwrt/Makefile | 2 +- openwrt/files/luci/cbi.lua | 9 +- openwrt/files/ua3f.init | 265 +++++++++++++++++- openwrt/files/ua3f.uci | 1 + openwrt/po/zh_cn/ua3f.po | 3 + src/go.mod | 6 +- src/internal/config/config.go | 10 +- src/internal/log/log.go | 1 + .../server/redirect/redirect_linux.go | 92 ++++++ .../server/redirect/redirect_others.go | 37 +++ src/internal/server/server.go | 5 + src/internal/server/tproxy/tproxy_linux.go | 113 ++++++++ src/internal/server/tproxy/tproxy_others.go | 37 +++ src/internal/server/utils/tcp.go | 22 +- src/internal/server/utils/tcp_linux.go | 34 +++ 17 files changed, 619 insertions(+), 26 deletions(-) create mode 100644 src/internal/server/redirect/redirect_linux.go create mode 100644 src/internal/server/redirect/redirect_others.go create mode 100644 src/internal/server/tproxy/tproxy_linux.go create mode 100644 src/internal/server/tproxy/tproxy_others.go create mode 100644 src/internal/server/utils/tcp_linux.go 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 +}