mirror of
https://github.com/SunBK201/UA3F.git
synced 2025-12-16 16:57:08 +00:00
feat: support nfqueue mode
This commit is contained in:
parent
43c01ba545
commit
f5a4238c73
@ -1,6 +1,6 @@
|
||||
Package: ua3f
|
||||
Version: 1.4.0-1
|
||||
Depends: luci-compat, ipset, iptables, iptables-mod-tproxy, iptables-mod-extra, iptables-mod-nat-extra, kmod-ipt-conntrack, iptables-mod-ipopt
|
||||
Depends: luci-compat, ipset, iptables, iptables-mod-tproxy, iptables-mod-extra, iptables-mod-nat-extra, kmod-ipt-conntrack, iptables-mod-ipopt, iptables-mod-nfqueue, iptables-mod-conntrack-extra, kmod-nf-conntrack-netlink
|
||||
Source: /feed/openwrt
|
||||
SourceName: UA3F
|
||||
License: GPL-3.0-only
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
Package: ua3f
|
||||
Version: 1.4.0-1
|
||||
Depends: luci-compat, ipset, iptables, iptables-mod-tproxy, iptables-mod-extra, iptables-mod-nat-extra, kmod-ipt-conntrack, iptables-mod-ipopt
|
||||
Depends: luci-compat, ipset, iptables, iptables-mod-tproxy, iptables-mod-extra, iptables-mod-nat-extra, kmod-ipt-conntrack, iptables-mod-ipopt, iptables-mod-nfqueue, iptables-mod-conntrack-extra, kmod-nf-conntrack-netlink
|
||||
Source: /feed/openwrt
|
||||
SourceName: UA3F
|
||||
License: GPL-3.0-only
|
||||
|
||||
@ -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 +ipset +iptables +iptables-mod-tproxy +iptables-mod-extra +iptables-mod-nat-extra +kmod-ipt-conntrack +iptables-mod-ipopt
|
||||
DEPENDS:=$(GO_ARCH_DEPENDS) +luci-compat +ipset +iptables +iptables-mod-tproxy +iptables-mod-extra +iptables-mod-nat-extra +kmod-ipt-conntrack +iptables-mod-ipopt +iptables-mod-nfqueue +iptables-mod-conntrack-extra +kmod-nf-conntrack-netlink
|
||||
endef
|
||||
|
||||
define Package/ua3f/description
|
||||
|
||||
@ -37,6 +37,7 @@ server_mode:value("HTTP", "HTTP")
|
||||
server_mode:value("SOCKS5", "SOCKS5")
|
||||
server_mode:value("TPROXY", "TPROXY")
|
||||
server_mode:value("REDIRECT", "REDIRECT")
|
||||
server_mode:value("NFQUEUE", "NFQUEUE")
|
||||
|
||||
port = general:taboption("general", Value, "port", translate("Port"))
|
||||
port.placeholder = "1080"
|
||||
|
||||
@ -82,6 +82,10 @@ set_ua3f_group() {
|
||||
UA3F_GID="0"
|
||||
UA3F_GROUP="root"
|
||||
return
|
||||
elif [ $server_mode = "NFQUEUE" ]; then
|
||||
UA3F_GID="0"
|
||||
UA3F_GROUP="root"
|
||||
return
|
||||
fi
|
||||
add_skip_gids "453"
|
||||
if openclash_running; then
|
||||
@ -130,12 +134,22 @@ add_skip_gids() {
|
||||
|
||||
detect_backend() {
|
||||
if opkg_available; then
|
||||
if opkg list-installed kmod-nft-tproxy | grep -q 'kmod-nft-tproxy'; then
|
||||
FW_BACKEND="nft"
|
||||
return 0
|
||||
else
|
||||
FW_BACKEND="ipt"
|
||||
return 0
|
||||
if [ "$SERVER_MODE" = "TPROXY" ]; then
|
||||
if opkg list-installed kmod-nft-tproxy | grep -q 'kmod-nft-tproxy'; then
|
||||
FW_BACKEND="nft"
|
||||
return 0
|
||||
else
|
||||
FW_BACKEND="ipt"
|
||||
return 0
|
||||
fi
|
||||
elif [ "$SERVER_MODE" = "NFQUEUE" ]; then
|
||||
if opkg list-installed kmod-nft-queue | grep -q 'kmod-nft-queue'; then
|
||||
FW_BACKEND="nft"
|
||||
return 0
|
||||
else
|
||||
FW_BACKEND="ipt"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if nft_available; then
|
||||
@ -174,7 +188,7 @@ cleanup_tproxy_route() {
|
||||
nft_drop_table() {
|
||||
nft delete table ip "$NFT_TABLE" 2>/dev/null
|
||||
nft delete table inet "$UA3F_TTL_TABLE" 2>/dev/null
|
||||
# nft delete table inet "$NFT_TABLE" 2>/dev/null
|
||||
nft delete table inet "$NFT_TABLE" 2>/dev/null
|
||||
# nft delete chain inet fw4 "$NFT_TABLE" 2>/dev/null
|
||||
}
|
||||
|
||||
@ -389,6 +403,37 @@ cleanup_ipset_ipt() {
|
||||
ipset destroy $UA3F_LANSET 2>/dev/null
|
||||
}
|
||||
|
||||
fw_setup_nft_nfqueue() {
|
||||
nft_reinit_table || {
|
||||
LOG "Failed to reinitialize nft table"
|
||||
return 1
|
||||
}
|
||||
|
||||
nft add chain ip $NFT_TABLE postrouting '{ type filter hook postrouting priority mangle - 20; }'
|
||||
nft add rule ip $NFT_TABLE postrouting meta l4proto != tcp counter return
|
||||
nft add rule ip $NFT_TABLE postrouting ct direction reply counter return
|
||||
nft add rule ip $NFT_TABLE postrouting ip daddr @$UA3F_LANSET counter return
|
||||
nft add rule ip $NFT_TABLE postrouting tcp dport {$SKIP_PORTS} return
|
||||
nft add rule ip $NFT_TABLE postrouting ct mark 201 counter return
|
||||
nft add rule ip $NFT_TABLE postrouting ct direction original ct state established ip length \> 40 counter queue num 10201 bypass
|
||||
}
|
||||
|
||||
fw_setup_ipt_nfqueue() {
|
||||
setup_ipset_ipt || return 1
|
||||
|
||||
# POSTROUTING
|
||||
iptables -t mangle -F $UA3F_CHAIN 2>/dev/null
|
||||
iptables -t mangle -D POSTROUTING -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 POSTROUTING -p tcp -j $UA3F_CHAIN
|
||||
iptables -t mangle -A $UA3F_CHAIN -m conntrack --ctdir REPLY -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 multiport --dports $SKIP_PORTS -j RETURN
|
||||
iptables -t mangle -A $UA3F_CHAIN -m connmark --mark 201 -j RETURN
|
||||
iptables -t mangle -A $UA3F_CHAIN -m conntrack --ctdir ORIGINAL --ctstate ESTABLISHED -m length --length 41:0xffff -j NFQUEUE --queue-num 10201 --queue-bypass
|
||||
}
|
||||
|
||||
set_ttl_nft() {
|
||||
nft drop table inet $UA3F_TTL_TABLE 2>/dev/null
|
||||
nft add table inet $UA3F_TTL_TABLE || return 1
|
||||
@ -433,6 +478,10 @@ fw_revert_ipt() {
|
||||
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
|
||||
# NFQUEUE
|
||||
iptables -t mangle -D POSTROUTING -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
|
||||
# ipset
|
||||
cleanup_ipset_ipt
|
||||
cleanup_tproxy_route
|
||||
@ -534,6 +583,19 @@ start_service() {
|
||||
}
|
||||
fi
|
||||
;;
|
||||
NFQUEUE)
|
||||
if [ "$FW_BACKEND" = "nft" ]; then
|
||||
fw_setup_nft_nfqueue || {
|
||||
LOG "fw_setup_nft_nfqueue setup failed"
|
||||
return 1
|
||||
}
|
||||
else
|
||||
fw_setup_ipt_nfqueue || {
|
||||
LOG "fw_setup_ipt_nfqueue setup failed"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
LOG "Unsupported server_mode: $SERVER_MODE"
|
||||
return 1
|
||||
|
||||
11
src/go.mod
11
src/go.mod
@ -4,12 +4,23 @@ go 1.19
|
||||
|
||||
require (
|
||||
github.com/dlclark/regexp2 v1.11.4
|
||||
github.com/florianl/go-nfqueue/v2 v2.0.2
|
||||
github.com/google/gopacket v1.1.19
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/mdlayher/netlink v1.7.2
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
golang.org/x/sys v0.30.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
|
||||
28
src/go.sum
28
src/go.sum
@ -3,8 +3,20 @@ 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/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE=
|
||||
github.com/florianl/go-nfqueue/v2 v2.0.2/go.mod h1:VA09+iPOT43OMoCKNfXHyzujQUty2xmzyCRkBOlmabc=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
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/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||
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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
@ -13,9 +25,25 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
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-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.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
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/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
|
||||
@ -11,6 +11,7 @@ const (
|
||||
ServerModeSocks5 = "SOCKS5"
|
||||
ServerModeTProxy = "TPROXY"
|
||||
ServerModeRedirect = "REDIRECT"
|
||||
ServerModeNFQueue = "NFQUEUE"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@ -38,7 +39,7 @@ func Parse() (*Config, bool) {
|
||||
showVer bool
|
||||
)
|
||||
|
||||
flag.StringVar(&serverMode, "m", ServerModeSocks5, "Server mode: HTTP, SOCKS5, TPROXY, REDIRECT")
|
||||
flag.StringVar(&serverMode, "m", ServerModeSocks5, "Server mode: HTTP, SOCKS5, TPROXY, REDIRECT, NFQUEUE")
|
||||
flag.StringVar(&bindAddr, "b", "127.0.0.1", "Bind address")
|
||||
flag.IntVar(&port, "p", 1080, "Port")
|
||||
flag.StringVar(&loglevel, "l", "info", "Log level")
|
||||
|
||||
151
src/internal/rewrite/packet.go
Normal file
151
src/internal/rewrite/packet.go
Normal file
@ -0,0 +1,151 @@
|
||||
package rewrite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/sunbk201/ua3f/internal/log"
|
||||
"github.com/sunbk201/ua3f/internal/sniff"
|
||||
"github.com/sunbk201/ua3f/internal/statistics"
|
||||
)
|
||||
|
||||
// RewriteResult contains the result of TCP rewriting
|
||||
type RewriteResult struct {
|
||||
Modified bool // Whether the packet was modified
|
||||
NewPayload []byte // New packet data (only if Modified is true)
|
||||
HasUA bool // Whether User-Agent was found
|
||||
InCache bool // Whether destination address is in cache
|
||||
InWhitelist bool // Whether User-Agent was in whitelist
|
||||
}
|
||||
|
||||
// shouldRewriteUA checks if the given User-Agent should be rewritten
|
||||
// Returns true if UA should be rewritten (not in whitelist and matches regex pattern)
|
||||
func (r *Rewriter) shouldRewriteUA(srcAddr, dstAddr string, ua string) bool {
|
||||
// If no pattern specified, rewrite all non-whitelist UAs
|
||||
if r.pattern == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check regex match
|
||||
matches, err := r.uaRegex.MatchString(ua)
|
||||
if err != nil {
|
||||
log.LogErrorWithAddr(srcAddr, dstAddr, fmt.Sprintf("Error matching User-Agent regex: %v", err))
|
||||
return true
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
// buildReplacement creates replacement content for User-Agent
|
||||
// If the original UA should not be rewritten, returns nil
|
||||
// Otherwise, uses buildUserAgent logic (partial or full replace) and adjusts to length n
|
||||
func (r *Rewriter) buildReplacement(srcAddr, dstAddr string, originalUA string, n int) []byte {
|
||||
if n <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build the new UA using the same logic as in Rewrite()
|
||||
newUA := r.buildUserAgent(originalUA)
|
||||
|
||||
log.LogInfoWithAddr(srcAddr, dstAddr, fmt.Sprintf("Rewritten User-Agent: %s", newUA))
|
||||
statistics.AddRewriteRecord(&statistics.RewriteRecord{
|
||||
Host: dstAddr,
|
||||
OriginalUA: originalUA,
|
||||
MockedUA: newUA,
|
||||
})
|
||||
|
||||
// Adjust to the exact length needed
|
||||
if len(newUA) >= n {
|
||||
return []byte(newUA[:n])
|
||||
}
|
||||
out := make([]byte, n)
|
||||
copy(out, newUA)
|
||||
// Pad with spaces if newUA is shorter than needed
|
||||
for i := len(newUA); i < n; i++ {
|
||||
out[i] = ' '
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// RewritePacketUserAgent rewrites User-Agent in a raw packet payload
|
||||
// Returns the rewritten payload and metadata about the operation
|
||||
func (r *Rewriter) RewritePacketUserAgent(payload []byte, srcAddr, dstAddr string) (newPayload []byte, hasUA, modified, inWhitelist bool) {
|
||||
// Find all User-Agent positions
|
||||
positions, unterm := sniff.FindUserAgentInPayload(payload)
|
||||
|
||||
if unterm {
|
||||
log.LogInfoWithAddr(srcAddr, dstAddr, "Unterminated User-Agent found, not rewriting")
|
||||
return payload, true, false, false
|
||||
}
|
||||
|
||||
if len(positions) == 0 {
|
||||
log.LogDebugWithAddr(srcAddr, dstAddr, "No User-Agent found in payload")
|
||||
return payload, false, false, false
|
||||
}
|
||||
|
||||
// Copy payload for in-place modification
|
||||
out := make([]byte, len(payload))
|
||||
copy(out, payload)
|
||||
|
||||
// Replace each User-Agent value
|
||||
for _, pos := range positions {
|
||||
valStart, valEnd := pos[0], pos[1]
|
||||
n := valEnd - valStart
|
||||
if n <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract original UA string
|
||||
originalUA := string(payload[valStart:valEnd])
|
||||
|
||||
log.LogInfoWithAddr(srcAddr, dstAddr, fmt.Sprintf("Original User-Agent: %s", originalUA))
|
||||
|
||||
// Check whitelist first
|
||||
if r.inWhitelist(originalUA) {
|
||||
log.LogInfoWithAddr(srcAddr, dstAddr, fmt.Sprintf("User-Agent in whitelist, not rewriting: %s", originalUA))
|
||||
statistics.AddPassThroughRecord(&statistics.PassThroughRecord{
|
||||
SrcAddr: srcAddr,
|
||||
DestAddr: dstAddr,
|
||||
UA: originalUA,
|
||||
})
|
||||
return payload, true, false, true
|
||||
}
|
||||
|
||||
// Check if should rewrite
|
||||
if !r.shouldRewriteUA(srcAddr, dstAddr, originalUA) {
|
||||
statistics.AddPassThroughRecord(&statistics.PassThroughRecord{
|
||||
SrcAddr: srcAddr,
|
||||
DestAddr: dstAddr,
|
||||
UA: originalUA,
|
||||
})
|
||||
return payload, true, false, false
|
||||
}
|
||||
|
||||
// Build replacement with regex matching
|
||||
repl := r.buildReplacement(srcAddr, dstAddr, originalUA, n)
|
||||
if repl != nil {
|
||||
copy(out[valStart:valEnd], repl)
|
||||
modified = true
|
||||
}
|
||||
}
|
||||
|
||||
return out, true, modified, false
|
||||
}
|
||||
|
||||
// RewriteTCP rewrites the TCP packet's User-Agent if applicable
|
||||
func (r *Rewriter) RewriteTCP(tcp *layers.TCP, srcAddr, dstAddr string) *RewriteResult {
|
||||
if len(tcp.Payload) == 0 {
|
||||
log.LogDebugWithAddr(srcAddr, dstAddr, "TCP payload is empty")
|
||||
return &RewriteResult{Modified: false}
|
||||
}
|
||||
|
||||
newPayload, hasUA, modified, inWhitelist := r.RewritePacketUserAgent(tcp.Payload, srcAddr, dstAddr)
|
||||
|
||||
return &RewriteResult{
|
||||
Modified: modified,
|
||||
NewPayload: newPayload,
|
||||
HasUA: hasUA,
|
||||
InWhitelist: inWhitelist,
|
||||
}
|
||||
}
|
||||
272
src/internal/server/nfqueue/nfqueue.go
Normal file
272
src/internal/server/nfqueue/nfqueue.go
Normal file
@ -0,0 +1,272 @@
|
||||
package nfqueue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
nfq "github.com/florianl/go-nfqueue/v2"
|
||||
"github.com/mdlayher/netlink"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/sunbk201/ua3f/internal/config"
|
||||
"github.com/sunbk201/ua3f/internal/log"
|
||||
"github.com/sunbk201/ua3f/internal/rewrite"
|
||||
"github.com/sunbk201/ua3f/internal/statistics"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
cfg *config.Config
|
||||
rw *rewrite.Rewriter
|
||||
nf *nfq.Nfqueue
|
||||
queueNum uint16
|
||||
maxQueueLen uint32
|
||||
maxPacketLen uint32
|
||||
numWorkers int
|
||||
workers []chan *nfq.Attribute
|
||||
wg sync.WaitGroup
|
||||
SniffMarkRangeLower uint32
|
||||
SniffMarkRangeUpper uint32
|
||||
HTTPMark uint32
|
||||
NotHTTPMark uint32
|
||||
}
|
||||
|
||||
func New(cfg *config.Config, rw *rewrite.Rewriter) *Server {
|
||||
numWorkers := runtime.NumCPU()
|
||||
if numWorkers < 2 {
|
||||
numWorkers = 2
|
||||
}
|
||||
|
||||
return &Server{
|
||||
cfg: cfg,
|
||||
rw: rw,
|
||||
queueNum: 10201,
|
||||
maxQueueLen: 2000,
|
||||
numWorkers: numWorkers,
|
||||
SniffMarkRangeLower: 10201,
|
||||
SniffMarkRangeUpper: 10216,
|
||||
NotHTTPMark: 201,
|
||||
HTTPMark: 202,
|
||||
}
|
||||
}
|
||||
|
||||
// worker processes packets from its assigned channel
|
||||
func (s *Server) worker(ctx context.Context, workerID int, aChan <-chan *nfq.Attribute) {
|
||||
defer s.wg.Done()
|
||||
|
||||
logrus.Debugf("Worker %d started", workerID)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
logrus.Debugf("Worker %d stopping", workerID)
|
||||
return
|
||||
case a, ok := <-aChan:
|
||||
if !ok {
|
||||
logrus.Debugf("Worker %d channel closed", workerID)
|
||||
return
|
||||
}
|
||||
if s.cfg.DirectForward {
|
||||
_ = s.nf.SetVerdict(*a.PacketID, nfq.NfAccept)
|
||||
continue
|
||||
} else {
|
||||
s.handlePacket(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) computeWorkerIndex(a *nfq.Attribute) int {
|
||||
var flowID uint32
|
||||
if a.Ct != nil {
|
||||
flowID = ctIDFromCtBytes(*a.Ct)
|
||||
} else {
|
||||
// Compute flow hash to determine which worker should handle this packet
|
||||
flowID = computeFlowHash(*a.Payload)
|
||||
}
|
||||
workerIdx := int(flowID % uint32(s.numWorkers))
|
||||
return workerIdx
|
||||
}
|
||||
|
||||
func (s *Server) SendVerdict(packet *IPPacket, result *rewrite.RewriteResult) {
|
||||
nf := s.nf
|
||||
id := *packet.a.PacketID
|
||||
setMark, nextMark := s.getNextMark(packet, result)
|
||||
|
||||
var newPacket []byte
|
||||
var err error
|
||||
|
||||
if result.Modified {
|
||||
newPacket, err = serializeIPPacket(packet.NetworkLayer, packet.TCP, result.NewPayload, packet.IsIPv6)
|
||||
if err != nil {
|
||||
_ = nf.SetVerdict(id, nfq.NfAccept)
|
||||
log.LogErrorWithAddr(packet.SrcAddr, packet.DstAddr, fmt.Sprintf("serializeIPPacket failed: %v", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !result.Modified {
|
||||
if setMark {
|
||||
nf.SetVerdictWithOption(id, nfq.NfAccept, nfq.WithConnMark(nextMark))
|
||||
} else {
|
||||
_ = nf.SetVerdict(id, nfq.NfAccept)
|
||||
}
|
||||
} else {
|
||||
if setMark {
|
||||
if err := nf.SetVerdictWithOption(id, nfq.NfAccept, nfq.WithAlteredPacket(newPacket), nfq.WithConnMark(nextMark)); err != nil {
|
||||
_ = nf.SetVerdict(id, nfq.NfAccept)
|
||||
log.LogErrorWithAddr(packet.SrcAddr, packet.DstAddr, fmt.Sprintf("SetVerdictWithOption failed: %v", err))
|
||||
}
|
||||
} else {
|
||||
if err := nf.SetVerdictWithOption(id, nfq.NfAccept, nfq.WithAlteredPacket(newPacket)); err != nil {
|
||||
_ = nf.SetVerdict(id, nfq.NfAccept)
|
||||
log.LogErrorWithAddr(packet.SrcAddr, packet.DstAddr, fmt.Sprintf("SetVerdictWithOption failed: %v", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handlePacket processes a single NFQUEUE packet
|
||||
func (s *Server) handlePacket(a *nfq.Attribute) {
|
||||
nf := s.nf
|
||||
if ok, verdict := packetAttributeSanityCheck(a); !ok {
|
||||
if a.PacketID != nil {
|
||||
_ = nf.SetVerdict(*a.PacketID, verdict)
|
||||
}
|
||||
return
|
||||
}
|
||||
packet, err := decodeIPPacket(a)
|
||||
if err != nil {
|
||||
_ = nf.SetVerdict(*a.PacketID, nfq.NfAccept)
|
||||
return
|
||||
}
|
||||
if s.rw.Cache.Contains(packet.DstAddr) {
|
||||
s.SendVerdict(packet, &rewrite.RewriteResult{Modified: false, InCache: true})
|
||||
log.LogDebugWithAddr(packet.SrcAddr, packet.DstAddr, "Destination in cache, skipping User-Agent rewrite")
|
||||
return
|
||||
}
|
||||
result := s.rw.RewriteTCP(packet.TCP, packet.SrcAddr, packet.DstAddr)
|
||||
s.SendVerdict(packet, result)
|
||||
}
|
||||
|
||||
func (s *Server) getNextMark(packet *IPPacket, result *rewrite.RewriteResult) (setMark bool, mark uint32) {
|
||||
if packet.a.Mark == nil {
|
||||
return true, s.SniffMarkRangeLower
|
||||
}
|
||||
|
||||
mark = *packet.a.Mark
|
||||
|
||||
// should not happen
|
||||
if mark == s.NotHTTPMark {
|
||||
return false, 0
|
||||
}
|
||||
|
||||
if mark == s.HTTPMark {
|
||||
return false, 0
|
||||
}
|
||||
|
||||
if result.InCache {
|
||||
return true, s.NotHTTPMark
|
||||
}
|
||||
|
||||
if result.InWhitelist {
|
||||
return true, s.NotHTTPMark
|
||||
}
|
||||
|
||||
if result.Modified {
|
||||
return true, s.HTTPMark
|
||||
}
|
||||
|
||||
if mark == 0 {
|
||||
return true, s.SniffMarkRangeLower
|
||||
}
|
||||
|
||||
if mark == s.SniffMarkRangeUpper {
|
||||
s.rw.Cache.Add(packet.DstAddr, struct{}{})
|
||||
return true, s.NotHTTPMark
|
||||
}
|
||||
|
||||
if mark >= s.SniffMarkRangeLower && mark < s.SniffMarkRangeUpper {
|
||||
return true, mark + 1
|
||||
}
|
||||
|
||||
return false, 0
|
||||
}
|
||||
|
||||
func (s *Server) Start() (err error) {
|
||||
logrus.Infof("Starting NFQUEUE mode on queue %d with %d workers...", s.queueNum, s.numWorkers)
|
||||
|
||||
config := nfq.Config{
|
||||
NfQueue: s.queueNum,
|
||||
MaxQueueLen: s.maxQueueLen,
|
||||
MaxPacketLen: s.maxPacketLen,
|
||||
Copymode: nfq.NfQnlCopyPacket,
|
||||
Flags: nfq.NfQaCfgFlagConntrack,
|
||||
}
|
||||
|
||||
nf, err := nfq.Open(&config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not open nfqueue socket: %w", err)
|
||||
}
|
||||
defer nf.Close()
|
||||
s.nf = nf
|
||||
|
||||
// Ignore ENOBUFS to prevent queue drop logs
|
||||
if err := nf.SetOption(netlink.NoENOBUFS, true); err != nil {
|
||||
return fmt.Errorf("failed to set netlink option: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Initialize worker channels and start worker goroutines
|
||||
s.workers = make([]chan *nfq.Attribute, s.numWorkers)
|
||||
for i := 0; i < s.numWorkers; i++ {
|
||||
s.workers[i] = make(chan *nfq.Attribute, 2000)
|
||||
s.wg.Add(1)
|
||||
go s.worker(ctx, i, s.workers[i])
|
||||
}
|
||||
|
||||
// Register callback function
|
||||
err = nf.RegisterWithErrorFunc(ctx,
|
||||
func(a nfq.Attribute) int {
|
||||
select {
|
||||
case s.workers[s.computeWorkerIndex(&a)] <- &a:
|
||||
default:
|
||||
// If worker channel is full, accept the packet to avoid blocking
|
||||
logrus.Warn("Worker channel full, accepting packet without processing")
|
||||
if a.PacketID != nil {
|
||||
_ = nf.SetVerdict(*a.PacketID, nfq.NfAccept)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
},
|
||||
func(e error) int {
|
||||
logrus.Errorf("Error in nfqueue handler: %v", e)
|
||||
return 0
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
// Close all worker channels
|
||||
for i := 0; i < s.numWorkers; i++ {
|
||||
close(s.workers[i])
|
||||
}
|
||||
s.wg.Wait()
|
||||
return fmt.Errorf("failed to register nfqueue handler: %w", err)
|
||||
}
|
||||
|
||||
logrus.Info("NFQUEUE handler registered, listening for packets")
|
||||
|
||||
go statistics.StartRecorder()
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
// Cleanup: close all worker channels and wait for workers to finish
|
||||
for i := 0; i < s.numWorkers; i++ {
|
||||
close(s.workers[i])
|
||||
}
|
||||
s.wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
168
src/internal/server/nfqueue/packet.go
Normal file
168
src/internal/server/nfqueue/packet.go
Normal file
@ -0,0 +1,168 @@
|
||||
package nfqueue
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
|
||||
nfq "github.com/florianl/go-nfqueue/v2"
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/mdlayher/netlink"
|
||||
)
|
||||
|
||||
type IPPacket struct {
|
||||
a *nfq.Attribute
|
||||
NetworkLayer gopacket.NetworkLayer
|
||||
TCP *layers.TCP
|
||||
SrcAddr string
|
||||
DstAddr string
|
||||
IsIPv6 bool
|
||||
}
|
||||
|
||||
func packetAttributeSanityCheck(a *nfq.Attribute) (ok bool, verdict int) {
|
||||
if a.PacketID == nil {
|
||||
return false, -1
|
||||
}
|
||||
if a.Payload == nil || len(*a.Payload) < 40 {
|
||||
return false, nfq.NfAccept
|
||||
}
|
||||
return true, 0
|
||||
}
|
||||
|
||||
// computeFlowHash computes a hash value based on TCP 4-tuple to ensure packets
|
||||
// from the same TCP stream are handled by the same worker goroutine
|
||||
func computeFlowHash(pktData []byte) uint32 {
|
||||
version := (pktData[0] >> 4) & 0xF
|
||||
|
||||
h := fnv.New32a()
|
||||
|
||||
switch version {
|
||||
case 4:
|
||||
// IPv4: IP header is at least 20 bytes
|
||||
if len(pktData) < 20 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Source IP (bytes 12-15) and Dest IP (bytes 16-19)
|
||||
h.Write(pktData[12:20])
|
||||
|
||||
// Check if it's TCP (protocol 6)
|
||||
protocol := pktData[9]
|
||||
if protocol == 6 {
|
||||
// IHL (IP Header Length) is in the lower 4 bits of byte 0
|
||||
ihl := (pktData[0] & 0x0F) * 4
|
||||
if len(pktData) >= int(ihl)+4 {
|
||||
// TCP source port and dest port (first 4 bytes of TCP header)
|
||||
h.Write(pktData[ihl : ihl+4])
|
||||
}
|
||||
}
|
||||
|
||||
case 6:
|
||||
// IPv6: IP header is at least 40 bytes
|
||||
if len(pktData) < 40 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Source IP (bytes 8-23) and Dest IP (bytes 24-39)
|
||||
h.Write(pktData[8:40])
|
||||
|
||||
// Check if it's TCP (next header 6)
|
||||
nextHeader := pktData[6]
|
||||
if nextHeader == 6 && len(pktData) >= 44 {
|
||||
// TCP source port and dest port (first 4 bytes of TCP header at offset 40)
|
||||
h.Write(pktData[40:44])
|
||||
}
|
||||
}
|
||||
|
||||
return h.Sum32()
|
||||
}
|
||||
|
||||
func ctIDFromCtBytes(ct []byte) uint32 {
|
||||
ctAttrs, err := netlink.UnmarshalAttributes(ct)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
for _, attr := range ctAttrs {
|
||||
if attr.Type == 12 { // CTA_ID
|
||||
return binary.BigEndian.Uint32(attr.Data)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// decodeIPPacket decodes IP packet and extracts TCP layer
|
||||
func decodeIPPacket(a *nfq.Attribute) (ipPacket *IPPacket, err error) {
|
||||
ipPacket = &IPPacket{
|
||||
a: a,
|
||||
}
|
||||
|
||||
var decoded []gopacket.LayerType
|
||||
var layerType gopacket.LayerType
|
||||
var ipLayer gopacket.DecodingLayer
|
||||
|
||||
ipPacket.TCP = &layers.TCP{}
|
||||
|
||||
pktData := *a.Payload
|
||||
version := (pktData[0] >> 4) & 0xF
|
||||
ipPacket.IsIPv6 = version == 6
|
||||
|
||||
if ipPacket.IsIPv6 {
|
||||
ip6 := &layers.IPv6{}
|
||||
layerType = layers.LayerTypeIPv6
|
||||
ipLayer = ip6
|
||||
ipPacket.NetworkLayer = ip6
|
||||
} else {
|
||||
ip4 := &layers.IPv4{}
|
||||
layerType = layers.LayerTypeIPv4
|
||||
ipLayer = ip4
|
||||
ipPacket.NetworkLayer = ip4
|
||||
}
|
||||
|
||||
parser := gopacket.NewDecodingLayerParser(layerType, ipLayer, ipPacket.TCP)
|
||||
parser.IgnoreUnsupported = true
|
||||
|
||||
if err = parser.DecodeLayers(pktData, &decoded); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ipPacket.IsIPv6 {
|
||||
ip6 := ipPacket.NetworkLayer.(*layers.IPv6)
|
||||
ipPacket.SrcAddr = fmt.Sprintf("%s:%d", ip6.SrcIP.String(), ipPacket.TCP.SrcPort)
|
||||
ipPacket.DstAddr = fmt.Sprintf("%s:%d", ip6.DstIP.String(), ipPacket.TCP.DstPort)
|
||||
} else {
|
||||
ip4 := ipPacket.NetworkLayer.(*layers.IPv4)
|
||||
ipPacket.SrcAddr = fmt.Sprintf("%s:%d", ip4.SrcIP.String(), ipPacket.TCP.SrcPort)
|
||||
ipPacket.DstAddr = fmt.Sprintf("%s:%d", ip4.DstIP.String(), ipPacket.TCP.DstPort)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// serializeIPPacket serializes IP packet with modified TCP payload
|
||||
func serializeIPPacket(networkLayer gopacket.NetworkLayer, tcp *layers.TCP, newPayload []byte, isIPv6 bool) ([]byte, error) {
|
||||
buffer := gopacket.NewSerializeBuffer()
|
||||
serOpts := gopacket.SerializeOptions{
|
||||
FixLengths: true,
|
||||
ComputeChecksums: true,
|
||||
}
|
||||
|
||||
tcp.Checksum = 0
|
||||
tcp.Payload = nil
|
||||
tcp.SetNetworkLayerForChecksum(networkLayer)
|
||||
|
||||
var err error
|
||||
if isIPv6 {
|
||||
ip6 := networkLayer.(*layers.IPv6)
|
||||
ip6.NextHeader = layers.IPProtocolTCP
|
||||
err = gopacket.SerializeLayers(buffer, serOpts, ip6, tcp, gopacket.Payload(newPayload))
|
||||
} else {
|
||||
ip4 := networkLayer.(*layers.IPv4)
|
||||
ip4.Checksum = 0
|
||||
err = gopacket.SerializeLayers(buffer, serOpts, ip4, tcp, gopacket.Payload(newPayload))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
@ -2,11 +2,11 @@ package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/sunbk201/ua3f/internal/config"
|
||||
"github.com/sunbk201/ua3f/internal/rewrite"
|
||||
"github.com/sunbk201/ua3f/internal/server/http"
|
||||
"github.com/sunbk201/ua3f/internal/server/nfqueue"
|
||||
"github.com/sunbk201/ua3f/internal/server/redirect"
|
||||
"github.com/sunbk201/ua3f/internal/server/socks5"
|
||||
"github.com/sunbk201/ua3f/internal/server/tproxy"
|
||||
@ -14,8 +14,6 @@ import (
|
||||
|
||||
type Server interface {
|
||||
Start() error
|
||||
HandleClient(net.Conn)
|
||||
ForwardTCP(client, target net.Conn, destAddr string)
|
||||
}
|
||||
|
||||
func NewServer(cfg *config.Config, rw *rewrite.Rewriter) (Server, error) {
|
||||
@ -28,6 +26,8 @@ func NewServer(cfg *config.Config, rw *rewrite.Rewriter) (Server, error) {
|
||||
return tproxy.New(cfg, rw), nil
|
||||
case config.ServerModeRedirect:
|
||||
return redirect.New(cfg, rw), nil
|
||||
case config.ServerModeNFQueue:
|
||||
return nfqueue.New(cfg, rw), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown server mode: %s", cfg.ServerMode)
|
||||
}
|
||||
|
||||
97
src/internal/sniff/packet.go
Normal file
97
src/internal/sniff/packet.go
Normal file
@ -0,0 +1,97 @@
|
||||
package sniff
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
var (
|
||||
// HTTP User-Agent header tag (case-sensitive search optimized)
|
||||
uaTag = []byte("\r\nUser-Agent:")
|
||||
)
|
||||
|
||||
// toLowerASCII converts an ASCII byte to lowercase (only A-Z)
|
||||
func toLowerASCII(b byte) byte {
|
||||
if b >= 'A' && b <= 'Z' {
|
||||
return b + 32
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// IndexFoldASCII performs case-insensitive search for needle in haystack (ASCII only)
|
||||
// Returns the first occurrence index or -1 if not found
|
||||
func IndexFoldASCII(haystack, needle []byte) int {
|
||||
if len(needle) == 0 {
|
||||
return 0
|
||||
}
|
||||
if len(haystack) < len(needle) {
|
||||
return -1
|
||||
}
|
||||
n0 := toLowerASCII(needle[0])
|
||||
limit := len(haystack) - len(needle)
|
||||
for i := 0; i <= limit; i++ {
|
||||
if toLowerASCII(haystack[i]) != n0 {
|
||||
continue
|
||||
}
|
||||
match := true
|
||||
for j := 1; j < len(needle); j++ {
|
||||
if toLowerASCII(haystack[i+j]) != toLowerASCII(needle[j]) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if match {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// FindUserAgentInPayload searches for User-Agent header(s) in raw HTTP payload
|
||||
// Returns slice of (startPos, endPos) pairs for each User-Agent value found
|
||||
// Returns empty slice if no User-Agent found, or if any UA is unterminated (missing \r)
|
||||
func FindUserAgentInPayload(payload []byte) (positions [][2]int, unterminated bool) {
|
||||
if len(payload) < len(uaTag) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
searchStart := 0
|
||||
for {
|
||||
if len(payload)-searchStart < len(uaTag) {
|
||||
break
|
||||
}
|
||||
|
||||
idx := IndexFoldASCII(payload[searchStart:], uaTag)
|
||||
if idx < 0 {
|
||||
break
|
||||
}
|
||||
|
||||
uaKeyPos := searchStart + idx
|
||||
valStart := uaKeyPos + len(uaTag)
|
||||
|
||||
// Support both "User-Agent:XXX" and "User-Agent: XXX" (with or without space)
|
||||
if valStart < len(payload) && payload[valStart] == ' ' {
|
||||
valStart++
|
||||
}
|
||||
if valStart >= len(payload) {
|
||||
// UA at the end of payload, no \r found
|
||||
return nil, true
|
||||
}
|
||||
|
||||
// Find line ending position: look for \r
|
||||
relEnd := bytes.IndexByte(payload[valStart:], '\r')
|
||||
if relEnd < 0 {
|
||||
// No \r found, UA is unterminated
|
||||
return nil, true
|
||||
}
|
||||
valEnd := valStart + relEnd
|
||||
|
||||
if valEnd > valStart {
|
||||
positions = append(positions, [2]int{valStart, valEnd})
|
||||
}
|
||||
|
||||
// Continue searching for more UA headers
|
||||
searchStart = valEnd
|
||||
}
|
||||
|
||||
return positions, false
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user