feat: enhance user-agent handling and logging in rewriter

This commit is contained in:
SunBK201 2025-11-05 00:40:48 +08:00
parent 0a01c9f04f
commit 29183ad307
5 changed files with 38 additions and 52 deletions

View File

@ -24,6 +24,7 @@ UA3F_GROUP="nogroup"
SKIP_GIDS="" SKIP_GIDS=""
SIDECAR="OC" SIDECAR="OC"
FAKEIP_RANGE="198.18.0.0/16, 198.18.0.1/15, 28.0.0.1/8" 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" 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 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 {$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 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 $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"' 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 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 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 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 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"' 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 {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 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 {$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 ip daddr @$UA3F_LANSET counter return
nft add rule ip $NFT_TABLE prerouting tcp dport != {22} counter redirect to :$SERVER_PORT 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 {$FAKEIP_RANGE} counter return
nft add rule ip $NFT_TABLE output ip daddr @$UA3F_LANSET 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 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 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 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"' 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 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 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 -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 -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 -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 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 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 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 -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 -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 -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 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 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 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 -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 -m set --match-set $UA3F_LANSET dst -j RETURN
iptables -t nat -A $UA3F_CHAIN -p tcp -j REDIRECT --to-ports $SERVER_PORT 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 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 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 -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 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 -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 iptables -t nat -A $UA3F_OUT_CHAIN -p tcp -m mark --mark 0x1ed6 -j REDIRECT --to-ports $SERVER_PORT

View File

@ -48,6 +48,7 @@ func New(cfg *config.Config) (*Rewriter, error) {
whitelist: []string{ whitelist: []string{
"MicroMessenger Client", "MicroMessenger Client",
"Bilibili Freedoooooom/MarkII", "Bilibili Freedoooooom/MarkII",
"Valve/Steam HTTP Client 1.0",
"Go-http-client/1.1", "Go-http-client/1.1",
"ByteDancePcdn", "ByteDancePcdn",
}, },
@ -78,25 +79,33 @@ func (r *Rewriter) buildUserAgent(originUA string) string {
func (r *Rewriter) ShouldRewrite(req *http.Request, srcAddr, destAddr string) bool { func (r *Rewriter) ShouldRewrite(req *http.Request, srcAddr, destAddr string) bool {
originalUA := req.Header.Get("User-Agent") 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 var err error
matches := true matches := false
isWhitelist := r.inWhitelist(originalUA) isWhitelist := r.inWhitelist(originalUA)
if !isWhitelist && r.pattern != "" { if !isWhitelist {
matches, err = r.uaRegex.MatchString(originalUA) if r.pattern == "" {
if err != nil {
log.LogErrorWithAddr(srcAddr, destAddr, fmt.Sprintf("User-Agent Regex Match Error: %s", err.Error()))
matches = true 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 { 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{}{}) r.Cache.Add(destAddr, struct{}{})
} }
if !matches { 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 hit := !isWhitelist && matches
@ -140,12 +149,14 @@ func (r *Rewriter) Process(dst net.Conn, src net.Conn, destAddr string, srcAddr
if err != nil { if err != nil {
log.LogDebugWithAddr(srcAddr, destAddr, fmt.Sprintf("Process: %s", err.Error())) 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) { if strings.HasSuffix(destAddr, "443") && sniff.SniffTLSClientHello(reader) {
r.Cache.Add(destAddr, struct{}{}) 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 return
} }
@ -157,7 +168,7 @@ func (r *Rewriter) Process(dst net.Conn, src net.Conn, destAddr string, srcAddr
} }
if !isHTTP { if !isHTTP {
r.Cache.Add(destAddr, struct{}{}) 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 return
} }
@ -165,12 +176,11 @@ func (r *Rewriter) Process(dst net.Conn, src net.Conn, destAddr string, srcAddr
for { for {
if isHTTP, err = sniff.SniffHTTPFast(reader); err != nil { if isHTTP, err = sniff.SniffHTTPFast(reader); err != nil {
err = fmt.Errorf("isHTTP: %w", err) err = fmt.Errorf("sniff.SniffHTTPFast: %w", err)
return return
} }
if !isHTTP { if !isHTTP {
r.Cache.Add(destAddr, struct{}{}) log.LogWarnWithAddr(srcAddr, destAddr, "sniff subsequent request is not http, switching to raw proxy")
log.LogInfoWithAddr(srcAddr, destAddr, "Not HTTP, added to LRU Relay Cache")
return return
} }
if req, err = http.ReadRequest(reader); err != nil { 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 return
} }
if req.Header.Get("Upgrade") == "websocket" && req.Header.Get("Connection") == "Upgrade" { 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 return
} }
} }

View File

@ -34,6 +34,7 @@ func CopyHalf(dst, src net.Conn) {
} else { } else {
_ = src.Close() _ = src.Close()
} }
log.LogDebugWithAddr(src.RemoteAddr().String(), dst.RemoteAddr().String(), "Connections half-closed")
}() }()
_, _ = io.Copy(dst, src) _, _ = io.Copy(dst, src)
} }
@ -51,12 +52,13 @@ func ProxyHalf(dst, src net.Conn, rw *rewrite.Rewriter, destAddr string) {
} else { } else {
_ = src.Close() _ = src.Close()
} }
log.LogDebugWithAddr(src.RemoteAddr().String(), destAddr, "Connections half-closed")
}() }()
// Fast path: known pass-through // Fast path: known pass-through
srcAddr := src.RemoteAddr().String() srcAddr := src.RemoteAddr().String()
if rw.Cache.Contains(destAddr) { 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) io.Copy(dst, src)
return return
} }

View File

@ -5,7 +5,6 @@ import (
"bytes" "bytes"
"errors" "errors"
"io" "io"
"time"
) )
var ErrPeekTimeout = errors.New("peek timeout") var ErrPeekTimeout = errors.New("peek timeout")
@ -48,28 +47,3 @@ func peekLineString(br *bufio.Reader, maxSize int) (string, error) {
} }
return string(lineBytes), nil 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
}
}

View File

@ -3,7 +3,6 @@ package sniff
import ( import (
"bufio" "bufio"
"strings" "strings"
"time"
) )
// HTTP methods used to detect HTTP by request line. // 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. // beginWithHTTPMethod peeks the first few bytes to check for known HTTP method prefixes.
func beginWithHTTPMethod(reader *bufio.Reader) (bool, error) { func beginWithHTTPMethod(reader *bufio.Reader) (bool, error) {
const maxMethodLen = 7 const maxMethodLen = 7
const minMethodLen = 3
var hint []byte var hint []byte
hint, err := PeekWithTimeout(reader, maxMethodLen, 3*time.Second) hint, err := reader.Peek(maxMethodLen)
if err != nil { 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), " ") method, _, _ := strings.Cut(string(hint), " ")
for _, m := range methods { for _, m := range methods {