fix: add connection statistics tracking in HTTP server

This commit is contained in:
SunBK201 2025-11-10 13:26:54 +08:00
parent fe2427ddbe
commit 60a5c0801f
5 changed files with 63 additions and 70 deletions

View File

@ -1,6 +1,6 @@
# UA3F # UA3F
UA3F 是下一代 HTTP User-Agent 重写工具,作为一个 HTTP、SOCKS5、TPROXY、REDIRECT、NFQUEUE 服务对 HTTP 进行 User-Agent 透明重写。 UA3F 是一个 HTTP Header 重写工具,作为一个 HTTP、SOCKS5、TPROXY、REDIRECT、NFQUEUE 服务对 HTTP 请求 Header (例如 User-Agent) 进行透明重写。
## 特性 ## 特性
@ -103,7 +103,7 @@ sudo -u shellcrash /usr/bin/ua3f
UA3F 支持 5 种不同的服务模式,各模式的特点和使用场景如下: UA3F 支持 5 种不同的服务模式,各模式的特点和使用场景如下:
| 服务模式 | 工作原理 | 是否依赖 Clash 等 | 兼容性 | 能 | 能否与 Clash 等伴生运行 | | 服务模式 | 工作原理 | 是否依赖 Clash 等 | 兼容性 | 能 | 能否与 Clash 等伴生运行 |
| ------------ | ------------------ | ----------------- | ------ | ---- | ----------------------- | | ------------ | ------------------ | ----------------- | ------ | ---- | ----------------------- |
| **HTTP** | HTTP 代理 | 是 | 高 | 低 | 能 | | **HTTP** | HTTP 代理 | 是 | 高 | 低 | 能 |
| **SOCKS5** | SOCKS5 代理 | 是 | 高 | 低 | 能 | | **SOCKS5** | SOCKS5 代理 | 是 | 高 | 低 | 能 |
@ -111,6 +111,16 @@ UA3F 支持 5 种不同的服务模式,各模式的特点和使用场景如下
| **REDIRECT** | netfilter REDIRECT | 否 | 中 | 中 | 能 | | **REDIRECT** | netfilter REDIRECT | 否 | 中 | 中 | 能 |
| **NFQUEUE** | netfilter NFQUEUE | 否 | 中 | 高 | 能 | | **NFQUEUE** | netfilter NFQUEUE | 否 | 中 | 高 | 能 |
### 重写策略说明
UA3F 支持 3 种不同的重写策略:
| 重写策略 | 重写行为 | 适用服务模式 |
| ---------- | -------------------- | ---------------------------------- |
| **GLOBAL** | 所有请求均进行重写 | 适用于所有服务模式 |
| **DIRECT** | 不进行重写,纯转发 | 适用于所有服务模式 |
| **RULES** | 根据重写规则进行重写 | 适用于 HTTP/SOCKS5/TPROXY/REDIRECT |
## Clash 配置 ## Clash 配置
Clash 与 UA3F 的配置部署教程详见:[UA3F 与 Clash 从零开始的部署教程](https://sunbk201public.notion.site/UA3F-Clash-16d60a7b5f0e457a9ee97a3be7cbf557?pvs=4) Clash 与 UA3F 的配置部署教程详见:[UA3F 与 Clash 从零开始的部署教程](https://sunbk201public.notion.site/UA3F-Clash-16d60a7b5f0e457a9ee97a3be7cbf557?pvs=4)
@ -130,7 +140,7 @@ rules:
``` ```
> [!IMPORTANT] > [!IMPORTANT]
> 如果 Clash 使用 Fake-IP 模式,确保 OpenClash 本地 DNS 劫持选择「使用防火墙转发」不要使用「Dnsmasq 转发」。 > HTTP/SOCKS5 模式下,如果 Clash 使用 Fake-IP 模式,确保 OpenClash 本地 DNS 劫持选择「使用防火墙转发」不要使用「Dnsmasq 转发」。
### Clash 参考配置 ### Clash 参考配置

View File

@ -3,6 +3,7 @@ package config
import ( import (
"flag" "flag"
"fmt" "fmt"
"strings"
) )
type ServerMode string type ServerMode string
@ -63,7 +64,7 @@ func Parse() (*Config, bool) {
flag.Parse() flag.Parse()
cfg := &Config{ cfg := &Config{
ServerMode: ServerMode(serverMode), ServerMode: ServerMode(strings.ToUpper(serverMode)),
BindAddr: bindAddr, BindAddr: bindAddr,
Port: port, Port: port,
ListenAddr: fmt.Sprintf("%s:%d", bindAddr, port), ListenAddr: fmt.Sprintf("%s:%d", bindAddr, port),
@ -71,7 +72,7 @@ func Parse() (*Config, bool) {
PayloadUA: payloadUA, PayloadUA: payloadUA,
UARegex: uaRegx, UARegex: uaRegx,
PartialReplace: partial, PartialReplace: partial,
RewriteMode: RewriteMode(rewriteMode), RewriteMode: RewriteMode(strings.ToUpper(rewriteMode)),
Rules: rules, Rules: rules,
} }
if cfg.ServerMode == ServerModeRedirect || cfg.ServerMode == ServerModeTProxy { if cfg.ServerMode == ServerModeRedirect || cfg.ServerMode == ServerModeTProxy {

View File

@ -35,13 +35,11 @@ type Rewriter struct {
Cache *expirable.LRU[string, struct{}] Cache *expirable.LRU[string, struct{}]
} }
// RewriteDecision 重写决策结果
type RewriteDecision struct { type RewriteDecision struct {
Action rule.Action Action rule.Action
MatchedRule *rule.Rule MatchedRule *rule.Rule
} }
// ShouldRewrite 判断是否需要重写
func (d *RewriteDecision) ShouldRewrite() bool { func (d *RewriteDecision) ShouldRewrite() bool {
return d.Action == rule.ActionReplace || return d.Action == rule.ActionReplace ||
d.Action == rule.ActionReplacePart || d.Action == rule.ActionReplacePart ||
@ -57,7 +55,6 @@ func New(cfg *config.Config) (*Rewriter, error) {
return nil, err return nil, err
} }
// 创建规则引擎
var ruleEngine *rule.Engine var ruleEngine *rule.Engine
if cfg.RewriteMode == config.RewriteModeRules { if cfg.RewriteMode == config.RewriteModeRules {
ruleEngine, err = rule.NewEngine(cfg.Rules) ruleEngine, err = rule.NewEngine(cfg.Rules)
@ -94,7 +91,6 @@ func (r *Rewriter) inWhitelist(ua string) bool {
return false return false
} }
// GetRuleEngine 获取规则引擎
func (r *Rewriter) GetRuleEngine() *rule.Engine { func (r *Rewriter) GetRuleEngine() *rule.Engine {
return r.ruleEngine return r.ruleEngine
} }
@ -119,7 +115,7 @@ func (r *Rewriter) EvaluateRewriteDecision(req *http.Request, srcAddr, destAddr
req.Header.Set("User-Agent", "") req.Header.Set("User-Agent", "")
} }
// 「直接转发」模式:不进行任何重写 // DIRECT
if r.rewriteMode == config.RewriteModeDirect { if r.rewriteMode == config.RewriteModeDirect {
log.LogDebugWithAddr(srcAddr, destAddr, "Direct forward mode, skip rewriting") log.LogDebugWithAddr(srcAddr, destAddr, "Direct forward mode, skip rewriting")
statistics.AddPassThroughRecord(&statistics.PassThroughRecord{ statistics.AddPassThroughRecord(&statistics.PassThroughRecord{
@ -132,11 +128,11 @@ func (r *Rewriter) EvaluateRewriteDecision(req *http.Request, srcAddr, destAddr
} }
} }
// 「规则判定」模式:使用规则引擎(只匹配一次) // RULES
if r.rewriteMode == config.RewriteModeRules && r.ruleEngine != nil { if r.rewriteMode == config.RewriteModeRules && r.ruleEngine != nil {
matchedRule := r.ruleEngine.MatchWithRule(req, srcAddr, destAddr) matchedRule := r.ruleEngine.MatchWithRule(req, srcAddr, destAddr)
// 没有匹配到任何规则,默认直接转发 // no match rule, direct forward
if matchedRule == nil { if matchedRule == nil {
log.LogDebugWithAddr(srcAddr, destAddr, "No rule matched, direct forward") log.LogDebugWithAddr(srcAddr, destAddr, "No rule matched, direct forward")
statistics.AddPassThroughRecord(&statistics.PassThroughRecord{ statistics.AddPassThroughRecord(&statistics.PassThroughRecord{
@ -149,7 +145,7 @@ func (r *Rewriter) EvaluateRewriteDecision(req *http.Request, srcAddr, destAddr
} }
} }
// DROP 动作:丢弃请求 // DROP
if matchedRule.Action == rule.ActionDrop { if matchedRule.Action == rule.ActionDrop {
log.LogInfoWithAddr(srcAddr, destAddr, "Rule matched: DROP action, request will be dropped") log.LogInfoWithAddr(srcAddr, destAddr, "Rule matched: DROP action, request will be dropped")
return &RewriteDecision{ return &RewriteDecision{
@ -158,7 +154,7 @@ func (r *Rewriter) EvaluateRewriteDecision(req *http.Request, srcAddr, destAddr
} }
} }
// DIRECT 动作:直接转发 // DIRECT
if matchedRule.Action == rule.ActionDirect { if matchedRule.Action == rule.ActionDirect {
log.LogDebugWithAddr(srcAddr, destAddr, "Rule matched: DIRECT action, skip rewriting") log.LogDebugWithAddr(srcAddr, destAddr, "Rule matched: DIRECT action, skip rewriting")
statistics.AddPassThroughRecord(&statistics.PassThroughRecord{ statistics.AddPassThroughRecord(&statistics.PassThroughRecord{
@ -172,14 +168,14 @@ func (r *Rewriter) EvaluateRewriteDecision(req *http.Request, srcAddr, destAddr
} }
} }
// REPLACE、REPLACE-PART、DELETE 动作:需要重写 // REPLACE、REPLACE-PART、DELETE, Rewrite
return &RewriteDecision{ return &RewriteDecision{
Action: matchedRule.Action, Action: matchedRule.Action,
MatchedRule: matchedRule, MatchedRule: matchedRule,
} }
} }
// 「全局重写」模式:使用原有逻辑 // GLOBAL
var err error var err error
matches := false matches := false
isWhitelist := r.inWhitelist(originalUA) isWhitelist := r.inWhitelist(originalUA)
@ -226,11 +222,11 @@ func (r *Rewriter) Rewrite(req *http.Request, srcAddr string, destAddr string, d
action := decision.Action action := decision.Action
var rewritedUA string var rewritedUA string
// 「规则判定」模式:根据规则动作决定如何重写 // RULES
if r.rewriteMode == config.RewriteModeRules && r.ruleEngine != nil { if r.rewriteMode == config.RewriteModeRules && r.ruleEngine != nil {
rewritedUA = r.ruleEngine.ApplyAction(action, rewriteValue, originalUA, decision.MatchedRule) rewritedUA = r.ruleEngine.ApplyAction(action, rewriteValue, originalUA, decision.MatchedRule)
} else { } else {
// 「全局重写」模式:使用原有逻辑 // GLOBAL
rewritedUA = r.buildUserAgent(originalUA) rewritedUA = r.buildUserAgent(originalUA)
} }
@ -328,14 +324,13 @@ func (r *Rewriter) Process(dst net.Conn, src net.Conn, destAddr string, srcAddr
return return
} }
// 获取重写决策(只匹配一次规则)
decision := r.EvaluateRewriteDecision(req, srcAddr, destAddr) decision := r.EvaluateRewriteDecision(req, srcAddr, destAddr)
// 处理 DROP 动作
if decision.Action == rule.ActionDrop { if decision.Action == rule.ActionDrop {
log.LogInfoWithAddr(srcAddr, destAddr, "Request dropped by rule") log.LogInfoWithAddr(srcAddr, destAddr, "Request dropped by rule")
continue continue
} }
// 如果需要重写,执行重写操作
if decision.ShouldRewrite() { if decision.ShouldRewrite() {
req = r.Rewrite(req, srcAddr, destAddr, decision) req = r.Rewrite(req, srcAddr, destAddr, decision)
} }

View File

@ -12,50 +12,43 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// RuleType 规则类型
type RuleType string type RuleType string
const ( const (
RuleTypeKeyword RuleType = "KEYWORD" // 关键字匹配 RuleTypeKeyword RuleType = "KEYWORD"
RuleTypeRegex RuleType = "REGEX" // 正则表达式匹配 RuleTypeRegex RuleType = "REGEX"
RuleTypeIPCIDR RuleType = "IP-CIDR" // IP地址段匹配 RuleTypeIPCIDR RuleType = "IP-CIDR"
RuleTypeSrcIP RuleType = "SRC-IP" // 源IP地址匹配 RuleTypeSrcIP RuleType = "SRC-IP"
RuleTypeDestPort RuleType = "DEST-PORT" // 目标端口匹配 RuleTypeDestPort RuleType = "DEST-PORT"
RuleTypeFinal RuleType = "FINAL" // 兜底规则 RuleTypeFinal RuleType = "FINAL"
) )
// Action 重写策略
type Action string type Action string
const ( const (
ActionReplace Action = "REPLACE" // 替换整个 User-Agent ActionReplace Action = "REPLACE"
ActionReplacePart Action = "REPLACE-PART" // 部分替换 ActionReplacePart Action = "REPLACE-PART"
ActionDelete Action = "DELETE" // 删除 User-Agent ActionDelete Action = "DELETE"
ActionDirect Action = "DIRECT" // 直接转发 ActionDirect Action = "DIRECT"
ActionDrop Action = "DROP" // 丢弃请求 ActionDrop Action = "DROP"
) )
// Rule 重写规则
type Rule struct { type Rule struct {
Enabled bool `json:"enabled"` // 是否启用 Enabled bool `json:"enabled"`
Type RuleType `json:"type"` // 规则类型 Type RuleType `json:"type"`
Action Action `json:"action"` // 重写策略 Action Action `json:"action"`
MatchValue string `json:"match_value"` // 匹配值 MatchValue string `json:"match_value"`
RewriteValue string `json:"rewrite_value"` // 重写值 RewriteValue string `json:"rewrite_value"`
Description string `json:"description"` // 描述 Description string `json:"description"`
// 编译后的正则表达式(仅用于 REGEX 类型)
regex *regexp2.Regexp regex *regexp2.Regexp
// 解析后的 IP 网络(仅用于 IP-CIDR 和 SRC-IP 类型)
ipNet *net.IPNet ipNet *net.IPNet
} }
// Engine 规则引擎
type Engine struct { type Engine struct {
rules []*Rule rules []*Rule
} }
// NewEngine 创建规则引擎
func NewEngine(rulesJSON string) (*Engine, error) { func NewEngine(rulesJSON string) (*Engine, error) {
if rulesJSON == "" { if rulesJSON == "" {
return &Engine{rules: []*Rule{}}, nil return &Engine{rules: []*Rule{}}, nil
@ -66,7 +59,6 @@ func NewEngine(rulesJSON string) (*Engine, error) {
return nil, fmt.Errorf("failed to parse rules JSON: %w", err) return nil, fmt.Errorf("failed to parse rules JSON: %w", err)
} }
// 编译正则表达式和解析 IP 网络
for _, rule := range rules { for _, rule := range rules {
if !rule.Enabled { if !rule.Enabled {
continue continue
@ -103,7 +95,6 @@ func NewEngine(rulesJSON string) (*Engine, error) {
return &Engine{rules: rules}, nil return &Engine{rules: rules}, nil
} }
// MatchWithRule 匹配规则并返回匹配的规则
func (e *Engine) MatchWithRule(req *http.Request, srcAddr, destAddr string) *Rule { func (e *Engine) MatchWithRule(req *http.Request, srcAddr, destAddr string) *Rule {
for _, rule := range e.rules { for _, rule := range e.rules {
if !rule.Enabled { if !rule.Enabled {
@ -139,13 +130,11 @@ func (e *Engine) MatchWithRule(req *http.Request, srcAddr, destAddr string) *Rul
return nil return nil
} }
// matchKeyword 关键字匹配
func (e *Engine) matchKeyword(req *http.Request, rule *Rule) bool { func (e *Engine) matchKeyword(req *http.Request, rule *Rule) bool {
ua := req.Header.Get("User-Agent") ua := req.Header.Get("User-Agent")
return strings.Contains(strings.ToLower(ua), strings.ToLower(rule.MatchValue)) return strings.Contains(strings.ToLower(ua), strings.ToLower(rule.MatchValue))
} }
// matchRegex 正则表达式匹配
func (e *Engine) matchRegex(req *http.Request, rule *Rule) (bool, error) { func (e *Engine) matchRegex(req *http.Request, rule *Rule) (bool, error) {
if rule.regex == nil { if rule.regex == nil {
return false, nil return false, nil
@ -154,7 +143,6 @@ func (e *Engine) matchRegex(req *http.Request, rule *Rule) (bool, error) {
return rule.regex.MatchString(ua) return rule.regex.MatchString(ua)
} }
// matchIPCIDR 目标IP地址段匹配
func (e *Engine) matchIPCIDR(destAddr string, rule *Rule) bool { func (e *Engine) matchIPCIDR(destAddr string, rule *Rule) bool {
if rule.ipNet == nil { if rule.ipNet == nil {
return false return false
@ -170,7 +158,6 @@ func (e *Engine) matchIPCIDR(destAddr string, rule *Rule) bool {
return rule.ipNet.Contains(ip) return rule.ipNet.Contains(ip)
} }
// matchSrcIP 源IP地址匹配
func (e *Engine) matchSrcIP(srcAddr string, rule *Rule) bool { func (e *Engine) matchSrcIP(srcAddr string, rule *Rule) bool {
if rule.ipNet == nil { if rule.ipNet == nil {
return false return false
@ -186,7 +173,6 @@ func (e *Engine) matchSrcIP(srcAddr string, rule *Rule) bool {
return rule.ipNet.Contains(ip) return rule.ipNet.Contains(ip)
} }
// matchDestPort 目标端口匹配
func (e *Engine) matchDestPort(destAddr string, rule *Rule) bool { func (e *Engine) matchDestPort(destAddr string, rule *Rule) bool {
_, portStr, err := net.SplitHostPort(destAddr) _, portStr, err := net.SplitHostPort(destAddr)
if err != nil { if err != nil {
@ -203,7 +189,6 @@ func (e *Engine) matchDestPort(destAddr string, rule *Rule) bool {
return port == matchPort return port == matchPort
} }
// ApplyAction 应用规则动作
func (e *Engine) ApplyAction(action Action, rewriteValue string, originalUA string, rule *Rule) string { func (e *Engine) ApplyAction(action Action, rewriteValue string, originalUA string, rule *Rule) string {
switch action { switch action {
case ActionReplace: case ActionReplace:
@ -217,20 +202,19 @@ func (e *Engine) ApplyAction(action Action, rewriteValue string, originalUA stri
} }
return newUA return newUA
} }
// 如果不是正则规则,使用简单字符串替换 // if not regex, do simple string replacement
return strings.ReplaceAll(originalUA, rule.MatchValue, rewriteValue) return strings.ReplaceAll(originalUA, rule.MatchValue, rewriteValue)
case ActionDelete: case ActionDelete:
return "" return ""
case ActionDirect: case ActionDirect:
return originalUA return originalUA
case ActionDrop: case ActionDrop:
return originalUA // DROP 由上层处理 return originalUA // DROP action handled elsewhere
default: default:
return originalUA return originalUA
} }
} }
// HasRules 是否有启用的规则
func (e *Engine) HasRules() bool { func (e *Engine) HasRules() bool {
for _, rule := range e.rules { for _, rule := range e.rules {
if rule.Enabled { if rule.Enabled {
@ -240,7 +224,6 @@ func (e *Engine) HasRules() bool {
return false return false
} }
// GetRules 获取所有规则
func (e *Engine) GetRules() []*Rule { func (e *Engine) GetRules() []*Rule {
return e.rules return e.rules
} }

View File

@ -6,6 +6,7 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"time"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/sunbk201/ua3f/internal/config" "github.com/sunbk201/ua3f/internal/config"
@ -13,6 +14,8 @@ import (
"github.com/sunbk201/ua3f/internal/rewrite" "github.com/sunbk201/ua3f/internal/rewrite"
"github.com/sunbk201/ua3f/internal/rule" "github.com/sunbk201/ua3f/internal/rule"
"github.com/sunbk201/ua3f/internal/server/utils" "github.com/sunbk201/ua3f/internal/server/utils"
"github.com/sunbk201/ua3f/internal/sniff"
"github.com/sunbk201/ua3f/internal/statistics"
) )
type Server struct { type Server struct {
@ -28,6 +31,8 @@ func New(cfg *config.Config, rw *rewrite.Rewriter) *Server {
} }
func (s *Server) Start() (err error) { func (s *Server) Start() (err error) {
go statistics.StartRecorder()
server := &http.Server{ server := &http.Server{
Addr: s.cfg.ListenAddr, Addr: s.cfg.ListenAddr,
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@ -50,6 +55,12 @@ func (s *Server) handleHTTP(w http.ResponseWriter, req *http.Request) {
destPort = "80" destPort = "80"
} }
destAddr := fmt.Sprintf("%s:%s", req.URL.Hostname(), destPort) destAddr := fmt.Sprintf("%s:%s", req.URL.Hostname(), destPort)
statistics.AddConnection(&statistics.ConnectionRecord{
Protocol: sniff.HTTP,
SrcAddr: req.RemoteAddr,
DestAddr: destAddr,
StartTime: time.Now(),
})
logrus.Infof("HTTP request for %s", destAddr) logrus.Infof("HTTP request for %s", destAddr)
@ -79,6 +90,7 @@ func (s *Server) handleHTTP(w http.ResponseWriter, req *http.Request) {
} }
w.WriteHeader(resp.StatusCode) w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body) io.Copy(w, resp.Body)
statistics.RemoveConnection(req.RemoteAddr, destAddr)
} }
func (s *Server) handleTunneling(w http.ResponseWriter, req *http.Request) { func (s *Server) handleTunneling(w http.ResponseWriter, req *http.Request) {
@ -104,24 +116,16 @@ func (s *Server) handleTunneling(w http.ResponseWriter, req *http.Request) {
} }
func (s *Server) rewriteAndForward(target net.Conn, req *http.Request, dstAddr, srcAddr string) (err error) { func (s *Server) rewriteAndForward(target net.Conn, req *http.Request, dstAddr, srcAddr string) (err error) {
rw := s.rw decision := s.rw.EvaluateRewriteDecision(req, srcAddr, dstAddr)
// 获取重写决策(只匹配一次规则)
decision := rw.EvaluateRewriteDecision(req, srcAddr, dstAddr)
// Handle DROP action
if decision.Action == rule.ActionDrop { if decision.Action == rule.ActionDrop {
log.LogInfoWithAddr(srcAddr, dstAddr, "Request dropped by rule") log.LogInfoWithAddr(srcAddr, dstAddr, "Request dropped by rule")
return fmt.Errorf("request dropped by rule") return fmt.Errorf("request dropped by rule")
} }
// 如果需要重写,执行重写操作
if decision.ShouldRewrite() { if decision.ShouldRewrite() {
req = rw.Rewrite(req, srcAddr, dstAddr, decision) req = s.rw.Rewrite(req, srcAddr, dstAddr, decision)
} }
if err = s.rw.Forward(target, req); err != nil {
if err = rw.Forward(target, req); err != nil { err = fmt.Errorf("s.rw.Forward: %w", err)
err = fmt.Errorf("r.forward: %w", err)
return return
} }
return nil return nil
@ -137,7 +141,7 @@ func (s *Server) ForwardTCP(client, target net.Conn, destAddr string) {
// Server -> Client (raw) // Server -> Client (raw)
go utils.CopyHalf(client, target) go utils.CopyHalf(client, target)
if s.cfg.RewriteMode == "direct" { if s.cfg.RewriteMode == config.RewriteModeDirect {
// Client -> Server (raw) // Client -> Server (raw)
go utils.CopyHalf(target, client) go utils.CopyHalf(target, client)
return return