From 29183ad3074ede593ed3cb7c93db84acfbaa7e06 Mon Sep 17 00:00:00 2001 From: SunBK201 Date: Wed, 5 Nov 2025 00:40:48 +0800 Subject: [PATCH] feat: enhance user-agent handling and logging in rewriter --- openwrt/files/ua3f.init | 9 +++++++ src/internal/rewrite/rewriter.go | 40 ++++++++++++++++++++------------ src/internal/server/utils/tcp.go | 4 +++- src/internal/sniff/common.go | 26 --------------------- src/internal/sniff/http.go | 11 +-------- 5 files changed, 38 insertions(+), 52 deletions(-) diff --git a/openwrt/files/ua3f.init b/openwrt/files/ua3f.init index 76d7e84..a3d3594 100755 --- a/openwrt/files/ua3f.init +++ b/openwrt/files/ua3f.init @@ -24,6 +24,7 @@ UA3F_GROUP="nogroup" SKIP_GIDS="" SIDECAR="OC" FAKEIP_RANGE="198.18.0.0/16, 198.18.0.1/15, 28.0.0.1/8" +SKIP_PORTS="22,51080,51090" LOG_FILE="/var/log/ua3f/ua3f.log" @@ -195,6 +196,7 @@ fw_setup_nft_tproxy_tcp() { nft add rule ip $NFT_TABLE prerouting mark {0x1ed4} counter return comment '"sc tproxy mark 7892"' nft add rule ip $NFT_TABLE prerouting ip daddr {$FAKEIP_RANGE} counter return comment '"fakeip range"' nft add rule ip $NFT_TABLE prerouting ip daddr @$UA3F_LANSET counter return + nft add rule ip $NFT_TABLE prerouting tcp dport {$SKIP_PORTS} return nft add rule ip $NFT_TABLE prerouting meta l4proto tcp mark $UA3F_FWMARK tproxy to 127.0.0.1:$SERVER_PORT counter accept comment '"cap oc"' nft add rule ip $NFT_TABLE prerouting meta l4proto tcp mark set $UA3F_FWMARK tproxy to 127.0.0.1:$SERVER_PORT counter accept comment '"default less hit. sc"' @@ -205,6 +207,7 @@ fw_setup_nft_tproxy_tcp() { nft add rule ip $NFT_TABLE output ip daddr {$FAKEIP_RANGE} counter return nft add rule ip $NFT_TABLE output meta skgid {$SKIP_GIDS} counter return nft add rule ip $NFT_TABLE output ip daddr @$UA3F_LANSET counter return + nft add rule ip $NFT_TABLE output tcp dport {$SKIP_PORTS} return nft add rule ip $NFT_TABLE output meta l4proto tcp meta skgid $UA3F_GID mark set $UA3F_FWMARK counter accept comment '"ghost oc"' nft add rule ip $NFT_TABLE output meta l4proto tcp mark set $UA3F_FWMARK counter accept comment '"default tproxy mark. bypass sc pre pollution"' } @@ -220,6 +223,7 @@ fw_setup_nft_redirect_tcp() { nft add rule ip $NFT_TABLE prerouting mark {0x162} counter return comment '"354"' nft add rule ip $NFT_TABLE prerouting mark {0x1ed4} counter return comment '"sc tproxy mark 7892"' nft add rule ip $NFT_TABLE prerouting ip daddr {$FAKEIP_RANGE} counter return comment '"fakeip range"' + nft add rule ip $NFT_TABLE prerouting tcp dport {$SKIP_PORTS} return nft add rule ip $NFT_TABLE prerouting ip daddr @$UA3F_LANSET counter return nft add rule ip $NFT_TABLE prerouting tcp dport != {22} counter redirect to :$SERVER_PORT @@ -230,6 +234,7 @@ fw_setup_nft_redirect_tcp() { nft add rule ip $NFT_TABLE output ip daddr {$FAKEIP_RANGE} counter return nft add rule ip $NFT_TABLE output ip daddr @$UA3F_LANSET counter return nft add rule ip $NFT_TABLE output meta skgid {$SKIP_GIDS} counter return + nft add rule ip $NFT_TABLE output tcp dport {$SKIP_PORTS} return nft add rule ip $NFT_TABLE output meta l4proto tcp mark {0x1ed6} counter redirect to :$SERVER_PORT comment '"cap sc meta"' nft add rule ip $NFT_TABLE output meta skgid $UA3F_GID tcp dport != {22} counter redirect to :$SERVER_PORT comment '"cap oc"' nft add rule ip $NFT_TABLE output tcp dport != {22} counter redirect to :$SERVER_PORT comment '"cap scc"' @@ -280,6 +285,7 @@ fw_setup_ipt_tproxy_tcp() { iptables -t mangle -A $UA3F_CHAIN -d 198.18.0.0/16 -j RETURN iptables -t mangle -A $UA3F_CHAIN -d 28.0.0.1/8 -j RETURN iptables -t mangle -A $UA3F_CHAIN -d 198.18.0.1/15 -j RETURN + iptables -t mangle -A $UA3F_CHAIN -p tcp -m multiport --dports $SKIP_PORTS -j RETURN iptables -t mangle -A $UA3F_CHAIN -m set --match-set $UA3F_LANSET dst -j RETURN iptables -t mangle -A $UA3F_CHAIN -p tcp -m mark --mark $UA3F_FWMARK -j TPROXY --on-ip 127.0.0.1 --on-port $SERVER_PORT iptables -t mangle -A $UA3F_CHAIN -p tcp -j TPROXY --on-ip 127.0.0.1 --on-port $SERVER_PORT --tproxy-mark $UA3F_FWMARK @@ -294,6 +300,7 @@ fw_setup_ipt_tproxy_tcp() { iptables -t mangle -A $UA3F_OUT_CHAIN -d 198.18.0.0/16 -j RETURN iptables -t mangle -A $UA3F_OUT_CHAIN -d 28.0.0.1/8 -j RETURN iptables -t mangle -A $UA3F_OUT_CHAIN -d 198.18.0.1/15 -j RETURN + iptables -t mangle -A $UA3F_OUT_CHAIN -p tcp -m multiport --dports $SKIP_PORTS -j RETURN iptables -t mangle -A $UA3F_OUT_CHAIN -p tcp -m owner --gid-owner 453 -j RETURN iptables -t mangle -A $UA3F_OUT_CHAIN -m set --match-set $UA3F_LANSET dst -j RETURN iptables -t mangle -A $UA3F_OUT_CHAIN -p tcp -m owner --gid-owner $UA3F_GID -j MARK --set-mark $UA3F_FWMARK @@ -316,6 +323,7 @@ fw_setup_ipt_redirect_tcp() { iptables -t nat -A $UA3F_CHAIN -d 198.18.0.0/16 -j RETURN iptables -t nat -A $UA3F_CHAIN -d 28.0.0.1/8 -j RETURN iptables -t nat -A $UA3F_CHAIN -d 198.18.0.1/15 -j RETURN + iptables -t nat -A $UA3F_CHAIN -p tcp -m multiport --dports $SKIP_PORTS -j RETURN iptables -t nat -A $UA3F_CHAIN -m set --match-set $UA3F_LANSET dst -j RETURN iptables -t nat -A $UA3F_CHAIN -p tcp -j REDIRECT --to-ports $SERVER_PORT @@ -329,6 +337,7 @@ fw_setup_ipt_redirect_tcp() { iptables -t nat -A $UA3F_OUT_CHAIN -d 198.18.0.0/16 -j RETURN iptables -t nat -A $UA3F_OUT_CHAIN -d 28.0.0.1/8 -j RETURN iptables -t nat -A $UA3F_OUT_CHAIN -d 198.18.0.1/15 -j RETURN + iptables -t nat -A $UA3F_OUT_CHAIN -p tcp -m multiport --dports $SKIP_PORTS -j RETURN iptables -t nat -A $UA3F_OUT_CHAIN -m set --match-set $UA3F_LANSET dst -j RETURN iptables -t nat -A $UA3F_OUT_CHAIN -m owner --gid-owner 453 -j RETURN iptables -t nat -A $UA3F_OUT_CHAIN -p tcp -m mark --mark 0x1ed6 -j REDIRECT --to-ports $SERVER_PORT diff --git a/src/internal/rewrite/rewriter.go b/src/internal/rewrite/rewriter.go index 5f25e7a..0b96716 100644 --- a/src/internal/rewrite/rewriter.go +++ b/src/internal/rewrite/rewriter.go @@ -48,6 +48,7 @@ func New(cfg *config.Config) (*Rewriter, error) { whitelist: []string{ "MicroMessenger Client", "Bilibili Freedoooooom/MarkII", + "Valve/Steam HTTP Client 1.0", "Go-http-client/1.1", "ByteDancePcdn", }, @@ -78,25 +79,33 @@ func (r *Rewriter) buildUserAgent(originUA string) string { func (r *Rewriter) ShouldRewrite(req *http.Request, srcAddr, destAddr string) bool { originalUA := req.Header.Get("User-Agent") - log.LogInfoWithAddr(srcAddr, destAddr, fmt.Sprintf("Original User-Agent: (%s)", originalUA)) + log.LogInfoWithAddr(srcAddr, destAddr, fmt.Sprintf("original User-Agent: (%s)", originalUA)) + if originalUA == "" { + req.Header.Set("User-Agent", "") + } var err error - matches := true + matches := false isWhitelist := r.inWhitelist(originalUA) - if !isWhitelist && r.pattern != "" { - matches, err = r.uaRegex.MatchString(originalUA) - if err != nil { - log.LogErrorWithAddr(srcAddr, destAddr, fmt.Sprintf("User-Agent Regex Match Error: %s", err.Error())) + if !isWhitelist { + if r.pattern == "" { matches = true + } else { + matches, err = r.uaRegex.MatchString(originalUA) + if err != nil { + log.LogErrorWithAddr(srcAddr, destAddr, fmt.Sprintf("User-Agent regex match error: %s", err.Error())) + matches = true + } } } + if isWhitelist { - log.LogInfoWithAddr(srcAddr, destAddr, fmt.Sprintf("Hit User-Agent Whitelist: %s", originalUA)) + log.LogInfoWithAddr(srcAddr, destAddr, fmt.Sprintf("hit User-Agent whitelist: %s, add to cache", originalUA)) r.Cache.Add(destAddr, struct{}{}) } if !matches { - log.LogDebugWithAddr(srcAddr, destAddr, fmt.Sprintf("Not Hit User-Agent Regex: %s", originalUA)) + log.LogDebugWithAddr(srcAddr, destAddr, fmt.Sprintf("not hit User-Agent regex: %s", originalUA)) } hit := !isWhitelist && matches @@ -140,12 +149,14 @@ func (r *Rewriter) Process(dst net.Conn, src net.Conn, destAddr string, srcAddr if err != nil { log.LogDebugWithAddr(srcAddr, destAddr, fmt.Sprintf("Process: %s", err.Error())) } - io.Copy(dst, reader) + if _, err = io.Copy(dst, reader); err != nil { + log.LogErrorWithAddr(srcAddr, destAddr, fmt.Sprintf("Process io.Copy: %s", err.Error())) + } }() if strings.HasSuffix(destAddr, "443") && sniff.SniffTLSClientHello(reader) { r.Cache.Add(destAddr, struct{}{}) - log.LogInfoWithAddr(srcAddr, destAddr, "tls client hello detected, pass forward") + log.LogInfoWithAddr(srcAddr, destAddr, "tls client hello detected, added to cache") return } @@ -157,7 +168,7 @@ func (r *Rewriter) Process(dst net.Conn, src net.Conn, destAddr string, srcAddr } if !isHTTP { r.Cache.Add(destAddr, struct{}{}) - log.LogInfoWithAddr(srcAddr, destAddr, "Not HTTP, added to cache") + log.LogInfoWithAddr(srcAddr, destAddr, "sniff first request is not http, added to cache, switching to raw proxy") return } @@ -165,12 +176,11 @@ func (r *Rewriter) Process(dst net.Conn, src net.Conn, destAddr string, srcAddr for { if isHTTP, err = sniff.SniffHTTPFast(reader); err != nil { - err = fmt.Errorf("isHTTP: %w", err) + err = fmt.Errorf("sniff.SniffHTTPFast: %w", err) return } if !isHTTP { - r.Cache.Add(destAddr, struct{}{}) - log.LogInfoWithAddr(srcAddr, destAddr, "Not HTTP, added to LRU Relay Cache") + log.LogWarnWithAddr(srcAddr, destAddr, "sniff subsequent request is not http, switching to raw proxy") return } if req, err = http.ReadRequest(reader); err != nil { @@ -185,7 +195,7 @@ func (r *Rewriter) Process(dst net.Conn, src net.Conn, destAddr string, srcAddr return } if req.Header.Get("Upgrade") == "websocket" && req.Header.Get("Connection") == "Upgrade" { - log.LogInfoWithAddr(srcAddr, destAddr, "WebSocket Upgrade detected, switching to raw proxy") + log.LogInfoWithAddr(srcAddr, destAddr, "websocket upgrade detected, switching to raw proxy") return } } diff --git a/src/internal/server/utils/tcp.go b/src/internal/server/utils/tcp.go index 09890b7..63c64df 100644 --- a/src/internal/server/utils/tcp.go +++ b/src/internal/server/utils/tcp.go @@ -34,6 +34,7 @@ func CopyHalf(dst, src net.Conn) { } else { _ = src.Close() } + log.LogDebugWithAddr(src.RemoteAddr().String(), dst.RemoteAddr().String(), "Connections half-closed") }() _, _ = io.Copy(dst, src) } @@ -51,12 +52,13 @@ func ProxyHalf(dst, src net.Conn, rw *rewrite.Rewriter, destAddr string) { } else { _ = src.Close() } + log.LogDebugWithAddr(src.RemoteAddr().String(), destAddr, "Connections half-closed") }() // Fast path: known pass-through srcAddr := src.RemoteAddr().String() if rw.Cache.Contains(destAddr) { - log.LogDebugWithAddr(srcAddr, destAddr, "LRU Relay Cache Hit, pass-through") + log.LogInfoWithAddr(srcAddr, destAddr, fmt.Sprintf("destination (%s) in cache, passing through", destAddr)) io.Copy(dst, src) return } diff --git a/src/internal/sniff/common.go b/src/internal/sniff/common.go index 87a1dbc..9bf2430 100644 --- a/src/internal/sniff/common.go +++ b/src/internal/sniff/common.go @@ -5,7 +5,6 @@ import ( "bytes" "errors" "io" - "time" ) var ErrPeekTimeout = errors.New("peek timeout") @@ -48,28 +47,3 @@ func peekLineString(br *bufio.Reader, maxSize int) (string, error) { } return string(lineBytes), nil } - -// PeekWithTimeout peeks n bytes from bufio.Reader with a timeout. -func PeekWithTimeout(r *bufio.Reader, n int, timeout time.Duration) ([]byte, error) { - if buffered := r.Buffered(); buffered >= n { - data, err := r.Peek(n) - return data, err - } - type result struct { - data []byte - err error - } - ch := make(chan result, 1) - - go func() { - data, err := r.Peek(n) - ch <- result{data, err} - }() - - select { - case res := <-ch: - return res.data, res.err - case <-time.After(timeout): - return nil, ErrPeekTimeout - } -} diff --git a/src/internal/sniff/http.go b/src/internal/sniff/http.go index eb29db6..ee611b2 100644 --- a/src/internal/sniff/http.go +++ b/src/internal/sniff/http.go @@ -3,7 +3,6 @@ package sniff import ( "bufio" "strings" - "time" ) // HTTP methods used to detect HTTP by request line. @@ -22,17 +21,9 @@ func parseRequestLine(line string) (method, requestURI, proto string, ok bool) { // beginWithHTTPMethod peeks the first few bytes to check for known HTTP method prefixes. func beginWithHTTPMethod(reader *bufio.Reader) (bool, error) { const maxMethodLen = 7 - const minMethodLen = 3 var hint []byte - hint, err := PeekWithTimeout(reader, maxMethodLen, 3*time.Second) + hint, err := reader.Peek(maxMethodLen) if err != nil { - if err != ErrPeekTimeout { - return false, err - } - hint, err = PeekWithTimeout(reader, minMethodLen+1, time.Second) - if err != nil { - return false, err - } } method, _, _ := strings.Cut(string(hint), " ") for _, m := range methods {