24 KiB
UA-Mask 架构说明
本文描述当前 UA-Mask 代码库的实际运行架构,重点覆盖:
- OpenWrt 侧如何把流量导入 UA-Mask
- Go daemon 的启动装配与生命周期
- TCP 会话、HTTP 探测、UA 改写的热路径
- 事件、目标画像、策略、流量卸载之间的关系
- 统计、LuCI 与防火墙 set 的集成方式
- SIGINT/SIGTERM 下的有序停止语义
当前架构只有一条生产流量路径:proxy 只负责接收与调度连接,业务热路径位于 session;目标级防火墙绕过统一通过 events -> state -> policy -> offload -> firewall.Executor 管线完成。旧的 proxy.Processor 与 firewall.DecisionEngine 已删除。
1. 总览
UA-Mask 是一个 OpenWrt REDIRECT 透明代理。OpenWrt 防火墙先把指定 TCP 流量重定向到本地监听端口,Go core 恢复原始目标地址并连接上游。
进入 core 后:
- HTTP 请求会读取
User-Agent,根据规则决定是否改写。 - 非 HTTP 流量会 fallback 成普通 TCP 转发,同时被记录为目标画像的一部分。
- 命中 UA 防火墙白名单,或多次表现为纯非 HTTP 的
IP:Port,会被动态加入 ipset/nfset。 - 被加入 set 的目标后续会在 OpenWrt 防火墙层直接 RETURN,不再进入 UA-Mask。
flowchart LR
Client[客户端] --> FW[OpenWrt REDIRECT 规则]
FW --> Ingress[core/internal/ingress]
Ingress --> Server[core/internal/proxy.Server]
Server --> Session[core/internal/session.Manager]
Session --> Rewrite[core/internal/rewrite]
Session --> Events[core/internal/events.Dispatcher]
Events --> State[core/internal/state.Store]
Events --> Stats[core/internal/stats.Stats]
Events --> Offload[core/internal/offload.Coordinator]
State --> Offload
Offload --> Firewall[core/internal/firewall.Executor]
Firewall --> Set[ipset / nfset]
Session --> Upstream[目标服务器]
Stats --> StatsFile["/tmp/UAmask.stats"]
StatsFile --> LuCI[openwrt/luci]
Set --> FW
2. 目录与职责
UA-Mask/
├── core/
│ ├── cmd/UAmask/ # 进程入口,只调用 app.Run(version)
│ └── internal/
│ ├── app/ # 运行时装配与生命周期
│ ├── config/ # JSON/legacy flags 加载、默认值、校验与运行时预处理
│ ├── ingress/ # REDIRECT 监听与原始目标地址恢复
│ ├── proxy/ # Accept 循环与 worker pool 调度
│ ├── session/ # 当前热路径中心:协议探测、转发、改写、事件发射
│ ├── rewrite/ # UA 判定、缓存、请求头改写
│ ├── events/ # 会话事件分发
│ ├── state/ # 按目标维护画像与冷却状态
│ ├── policy/ # 会话策略与目标卸载策略
│ ├── offload/ # 把策略信号转换为卸载动作
│ ├── firewall/ # 批量写 ipset/nfset
│ ├── stats/ # 事件计数与统计落盘
│ └── model/ # 跨模块共享的数据模型
├── openwrt/
│ ├── package/ # OpenWrt 包构建入口
│ ├── root/ # init.d 与默认 UCI 配置
│ └── luci/ # LuCI 页面
└── docs/
可以按三条线理解:
- 启动装配线:
cmd -> app -> config -> runtime components - 请求热路径线:
ingress -> proxy.Server -> session.Manager -> rewrite -> upstream - 卸载控制线:
session events -> state/profile -> policy -> offload -> firewall set
3. 启动与运行时装配
core/cmd/UAmask/main.go 是很薄的进程入口,只保存版本号并调用 app.Run(version)。
app.Run() 负责:
- 调用
config.NewFromFlags()解析进程参数,并按需加载版本化 JSON。 - 按
defaults -> JSON -> 显式 legacy flags -> normalize -> validate -> compile生成最终配置。 - 处理
-v、-check-config、-dump-effective-config等进程动作。 - 应用 GC 参数并初始化 logrus。
- 调用
NewRuntime(cfg, version)装配所有运行时组件。 - 监听 SIGINT/SIGTERM,并用对应 context 执行
runtime.RunContext()。
sequenceDiagram
participant Main as cmd/UAmask
participant App as app.Run
participant Config as config loader
participant Runtime as app.NewRuntime
participant Server as proxy.Server
Main->>App: Run(version)
App->>Config: 解析进程参数 / 加载 JSON
Config->>Config: defaults → normalize → validate → compile
Config-->>App: Command + Config
App->>App: setupLogging()
App->>Runtime: NewRuntime(cfg, version)
Runtime-->>App: Runtime
App->>Runtime: Runtime.RunContext(ctx)
Runtime->>Server: server.RunContext(ctx)
app.NewRuntime() 是当前最重要的代码图谱入口。它会创建:
stats.Stats与stats.FileReporterrewrite.UACachefirewall.Executorrewrite.Engine与rewrite.RequestRewriterstate.Storeoffload.FirewallTargetOffloaderpolicy.SessionPolicy与policy.TargetPolicyevents.Dispatcheroffload.Coordinatorsession.DefaultManageringress.Redirectproxy.Server
装配关系如下:
flowchart TB
Config[config.Config]
Config --> Stats[stats.Stats]
Config --> Reporter[stats.FileReporter]
Config --> Cache[rewrite.UACache]
Config --> RewriteEngine[rewrite.Engine]
Config --> Rewriter[rewrite.RequestRewriter]
Config --> ProfileStore[state.Store]
Config --> SessionPolicy[policy.SessionPolicy]
Config --> TargetPolicy[policy.TargetPolicy]
Config --> Ingress[ingress.Redirect]
Config --> Server[proxy.Server]
Config --> FirewallExecutor[firewall.Executor]
RewriteEngine --> Rewriter
Cache --> Rewriter
Dispatcher[events.Dispatcher] --> ProfileStore
Dispatcher --> Stats
Dispatcher --> Offload[offload.Coordinator]
ProfileStore --> Offload
TargetPolicy --> Offload
FirewallExecutor --> TargetOffloader[offload.FirewallTargetOffloader]
TargetOffloader --> Offload
SessionPolicy --> Session[session.Manager]
Rewriter --> Session
ProfileStore --> Session
Offload --> Session
Dispatcher --> Session
Ingress --> Server
Session --> Server
Runtime.RunContext() 的启动顺序是:
- 打印配置。
- 启动
firewall.Executorworker。 - 启动
state.Store清理 worker。 - 启动
stats.FileReporter落盘 worker。 - 启动
events.Dispatcher事件 worker。 - 最后进入
proxy.Server.RunContext()的 Accept 循环。
context 取消或 server 返回错误时:
proxy.Server关闭 ingress,停止接收新连接,并丢弃 worker pool 中尚未开始处理的 session。- context 传入
session.Manager;拨号使用DialContext,取消时同时关闭 client 和 upstream,再等待已开始的 handler 返回。 offload.Coordinator.Stop()取消待决策 timer 并拒绝新动作。firewall.Executor.Flush()完成此前入队的批次与 completion,然后停止 worker。events.Dispatcher.WaitIdle()完成 result event 的状态和统计投影,然后停止分发器。- 最后停止 stats reporter 和 profile store。
这个顺序保证“停止接收流量”先于“停止控制面”,并且防火墙执行结果不会因为关闭顺序而丢失。
4. 配置模型
config.Config 已经按职责拆成几个子配置:
classDiagram
class Config {
+Listen ListenConfig
+Rewrite RewriteConfig
+RewriteRuntime RewriteRuntimeConfig
+Firewall FirewallConfig
+Observe ObserveConfig
+Performance PerformanceConfig
}
class ListenConfig {
+Port int
+PoolSize int
+BufferSize int
+DialTimeout duration
+ClientKeepAlive duration
+UpstreamKeepAlive duration
}
class RewriteConfig {
+UserAgent string
+Whitelist []string
+ForceReplace bool
+EnableRegex bool
+EnablePartialReplace bool
+Keywords []string
+Pattern string
+CacheSize int
}
class RewriteRuntimeConfig {
+UARegexp *regexp.Regexp
}
class FirewallConfig {
+UAWhitelist []string
+EnableBypass bool
+SetName string
+Backend string
+DropOnMatch bool
+NonHTTPThreshold int
+Timeout int
+DecisionDelay duration
+HTTPCooldownPeriod duration
+ProfileCleanupInterval duration
+ImmediateBypassTimeout int
}
class ObserveConfig {
+LogLevel string
+LogFile string
+StatsFilePath string
+StatsInterval duration
}
class PerformanceConfig {
+Profile string
+GCPercent int
}
Config --> ListenConfig
Config --> RewriteConfig
Config --> RewriteRuntimeConfig
Config --> FirewallConfig
Config --> ObserveConfig
Config --> PerformanceConfig
RewriteConfig.Pattern 保存用户输入语义,RewriteRuntimeConfig.UARegexp 保存编译后的正则。这样业务代码只消费已经预处理好的运行时配置。
OpenWrt LuCI/UCI 仍然是用户配置的唯一真相源。openwrt/root/etc/init.d/UAmask 使用 jshn.sh 将 core 所需字段写入 /var/run/UAmask/config.json,调用 UAmask -check-config 校验后原子替换,再通过以下命令启动:
/usr/bin/UAmask -config /var/run/UAmask/config.json
JSON 使用 schema_version 做显式版本管理,未知字段会导致加载失败。duration 使用 30s、10m、8h 等字符串,列表使用 JSON 数组。完整示例见 docs/config.example.json。
独立运行时支持以下进程级参数:
-config <path>-check-config-dump-effective-config-v
旧业务 flags 暂时作为兼容覆盖层保留;优先级为默认值、JSON、显式 legacy flag。
5. 接入层:ingress 与 proxy
ingress.Redirect 负责本地监听和原始目标地址恢复。
sequenceDiagram
participant FW as OpenWrt REDIRECT
participant Ingress as ingress.Redirect
participant Server as proxy.Server
participant Manager as session.Manager
FW->>Ingress: TCP 连接进入监听端口
Ingress->>Ingress: AcceptTCP()
Ingress->>Ingress: getOriginalDst()
Ingress-->>Server: AcceptedSession
Server->>Manager: Handle(accepted)
proxy.Server 当前只负责连接调度:
- 持有
ingress.Ingress - 持有
session.Manager - 根据
ListenConfig.PoolSize选择 worker pool 或一连接一 goroutine - 对每个
AcceptedSession调用manager.Handle()
这意味着 proxy 不再理解 UA 规则、状态画像或防火墙策略。
6. 热路径中心:session.Manager
session.DefaultManager 是当前最重要的热路径模块。它负责:
- 为每条连接创建
SessionState - 连接原始目标上游
- 启动双向转发
- 在客户端到上游方向做 HTTP 探测
- 对 HTTP 请求调用
rewrite.RequestRewriter - 对非 HTTP 流量 fallback 到
io.Copy - 向
events.Dispatcher发出会话事件 - 调用
policy.SessionPolicy得到连接动作 - 把即时 offload signal 交给
offload.Coordinator
整体流程如下:
sequenceDiagram
participant Client as 客户端
participant Manager as session.Manager
participant Upstream as 目标服务器
participant Rewriter as rewrite.RequestRewriter
participant Policy as policy.SessionPolicy
participant Events as events.Dispatcher
participant Offload as offload.Coordinator
Client->>Manager: AcceptedSession
Manager->>Events: EventSessionOpened
Manager->>Upstream: Dial(target.Address)
Manager->>Events: EventUpstreamConnected
par client -> upstream
Manager->>Manager: Peek(7) 判断 HTTP
alt 非 HTTP
Manager->>Events: EventProtocolClassified(non_http)
Manager->>Policy: Evaluate(classification)
Manager->>Events: EventNonHTTPObserved
Manager->>Offload: Handle(decision.FlowSignal)
Manager->>Events: EventSessionForwardFallback
Manager->>Upstream: io.Copy fallback
else HTTP
Manager->>Manager: http.ReadRequest()
Manager->>Events: EventProtocolClassified(http)
Manager->>Rewriter: Rewrite(request)
Rewriter-->>Manager: rewrite.Result
Manager->>Policy: Evaluate(request)
Manager->>Events: EventHTTPRequestObserved
Manager->>Events: EventRewriteApplied 或 EventRewriteSkipped
Manager->>Offload: Handle(decision.FlowSignal)
Manager->>Upstream: request.Write()
end
and upstream -> client
Upstream->>Manager: response bytes
Manager->>Client: io.Copy
end
Manager->>Events: EventSessionClosed
协议探测非常轻量:只 Peek(7) 并判断是否以常见 HTTP 方法开头。判断为非 HTTP 后,不再尝试解析应用层协议,而是记录事件并直接转发剩余 TCP 字节。
7. Rewrite 层
rewrite 层拆成三个对象:
Engine:只根据 UA 和配置做纯决策。UACache:缓存originUA -> finalUA。RequestRewriter:面向http.Request的封装,负责读写 Header 和串联缓存/决策。
flowchart LR
Request[http.Request] --> Rewriter[RequestRewriter]
Rewriter --> Cache[UACache]
Rewriter --> Engine[Engine]
Engine --> Decision[Decision]
Decision --> Rewriter
Rewriter --> Mutated[可能被改写的 Request]
UA 决策顺序:
flowchart TD
Start([读取 originUA]) --> Empty{UA 为空?}
Empty -- 是 --> NoUA[NoUserAgent: 不缓存、不改写]
Empty -- 否 --> FWUA{命中防火墙 UA 白名单?}
FWUA -- 是 --> FWDecision[FirewallBypassHit=true<br/>可选 DropConnection]
FWUA -- 否 --> ExactWL{命中精确 UA 白名单?}
ExactWL -- 是 --> Whitelist[不改写,可缓存]
ExactWL -- 否 --> Force{ForceReplace?}
Force -- 是 --> ForceReplace[直接替换,可缓存]
Force -- 否 --> RegexMode{EnableRegex?}
RegexMode -- 是 --> RegexHit{正则命中?}
RegexHit -- 是 --> RegexReplace[完整或部分替换,可缓存]
RegexHit -- 否 --> RegexMiss[不改写,可缓存]
RegexMode -- 否 --> KeywordHit{命中关键词?}
KeywordHit -- 是 --> KeywordReplace[替换,可缓存]
KeywordHit -- 否 --> KeywordMiss[不改写,可缓存]
Decision 字段表达了后续模块需要知道的结果:
MatchedReplaceFinalUAReasonCacheableFirewallBypassHitDropConnection
FirewallBypassHit 不直接写防火墙,而是通过会话事件和目标策略进入卸载管线。
8. 事件模型
当前代码用 model.SessionEvent 作为模块之间的主要事实记录。
核心事件包括:
EventSessionOpenedEventUpstreamConnectedEventProtocolClassifiedEventHTTPRequestObservedEventRewriteAppliedEventRewriteSkippedEventNonHTTPObservedEventSessionForwardFallbackEventSessionClosedEventFlowOffloadSucceededEventFlowOffloadFailedEventTargetOffloadSucceededEventTargetOffloadFailed
events.Dispatcher 是一个简单的异步 fan-out:
flowchart LR
Session[session.Manager] --> Dispatcher[events.Dispatcher]
Dispatcher --> State[state.Store]
Dispatcher --> Stats[stats.Stats]
Dispatcher --> Offload[offload.Coordinator]
运行中 Dispatcher.Append(event) 会把事件放入固定容量的环形队列,由后台 worker 依次分发给 sink。队列满时拒绝最新事件并累计 Dropped() 计数,防止慢 sink 导致内存无限增长;Stop() 只排空调用时已经入队的事件,停止边界之后产生的新事件会被拒绝,确保关闭过程有限;未启动时则同步分发,便于测试。
9. 状态画像与策略
state.Store 按目标 IP:Port 维护 TargetProfile:
NonHTTPScoreHTTPCooldownUntilLastActivityPendingTargetOffloadAtTargetOffloadUntilFirewallWhitelistHitsLastFirewallHitLastEventTypeLastReason
它消费事件并更新画像:
flowchart TD
Event[SessionEvent] --> Kind{事件类型}
Kind -- HTTPRequestObserved --> HTTP[清零 NonHTTPScore<br/>设置 HTTP cooldown<br/>取消 pending target offload]
Kind -- NonHTTPObserved --> NonHTTP[如果不在 cooldown<br/>NonHTTPScore++]
NonHTTP --> Threshold{达到阈值?}
Threshold -- 是 --> Pending[设置 PendingTargetOffloadAt]
Threshold -- 否 --> Wait[继续观察]
Kind -- RewriteSkipped 且 FirewallHit --> FWHit[记录防火墙白名单命中]
Kind -- TargetOffloadSucceeded --> Offloaded[记录 TargetOffloadUntil<br/>清理分数和 pending]
policy 有两类策略:
SessionPolicy:根据一次会话输入决定继续、透传或断开。TargetPolicy:根据目标画像决定是否发出SignalTryTargetOffload。
TargetPolicy 的两个主要触发条件是:
- HTTP 请求命中防火墙 UA 白名单,并产生
EventRewriteSkipped+FirewallHit=true。 - 非 HTTP 目标达到阈值,且经过
DecisionDelay后仍不处于 HTTP cooldown。
两条路径的开关彼此独立:UA 防火墙白名单始终可产生目标卸载信号,-fw-bypass 只控制非 HTTP 画像与策略。
10. 流量卸载管线
offload.Coordinator 同时扮演两个角色:
- 作为
events.Dispatcher的 sink,观察目标事件。 - 作为
session.Manager的 signal handler,处理策略产生的卸载信号。
目标卸载流程如下:
sequenceDiagram
participant Session as session.Manager
participant Events as events.Dispatcher
participant Store as state.Store
participant Coord as offload.Coordinator
participant Policy as policy.TargetPolicy
participant Executor as firewall.Executor
participant FW as ipset/nfset
Session->>Events: EventNonHTTPObserved 或 EventRewriteSkipped
Events->>Store: 更新 TargetProfile
Events->>Coord: observeTargetEvent(event)
Coord->>Store: Snapshot(target)
alt 非 HTTP 达到阈值
Coord->>Coord: scheduleTargetEvaluation(delay)
Coord->>Store: 到期后重新 Snapshot(target)
Coord->>Policy: Evaluate(snapshot)
else UA 防火墙白名单命中
Coord->>Policy: Evaluate(snapshot)
end
Policy-->>Coord: SignalTryTargetOffload
Coord->>Executor: TryEnqueue(BypassTarget, completion)
Executor->>FW: 批量写入 set
FW-->>Executor: 实际执行结果
Executor-->>Coord: completion(error)
Coord->>Events: EventTargetOffloadSucceeded 或 Failed
offload.FirewallTargetOffloader 把 model.OffloadSignal 转成 firewall.BypassTarget 并投递给 firewall.Executor。Coordinator 只在 Executor 的批处理命令实际完成后发出成功或失败事实;单纯进入队列不会再被视为卸载成功。
NoopFlowOffloader 当前是空实现,说明代码模型里预留了“单连接级 flow offload”的概念,但当前实际起作用的是目标级 IP:Port offload。
11. Firewall 执行层
firewall.Executor 是异步批处理执行器,不负责策略判断。
它的职责是:
- 校验
BypassTarget - 接收待写入 set 的目标
- 按
backend:setName分桶 - 对同一
IP:Port去重 - 达到批次大小或最大等待时间后执行
- 停止时 drain 队列并执行剩余 batch
flowchart LR
Offload[offload.FirewallTargetOffloader] --> Executor[firewall.Executor]
Executor --> Queue[queue]
Queue --> Batch[按 backend:setName 分桶<br/>按 IP:Port 去重]
Batch --> Applier[firewall.CommandApplier]
Applier -->|nft add element| NFT[nfset]
Applier -->|ipset restore| IPT[ipset]
firewall.CommandApplier 根据 backend 选择命令:
nft:执行nft add element inet fw4 <set> { ip . port timeout Ns }ipt:执行ipset restore
基础 REDIRECT 规则和 set 创建不在 Go 里完成,而是在 OpenWrt init 脚本里完成。Go 进程只动态追加需要绕过的目标。
12. Stats 与 LuCI
stats.Stats 是事件 sink,使用原子计数器记录:
- 当前连接数
- HTTP 请求总数
- UA 修改次数
- 缓存命中并修改次数
- 缓存命中但不修改次数
stats.FileReporter 周期性读取快照并写入 /tmp/UAmask.stats。
flowchart LR
Events[events.Dispatcher] --> Stats[stats.Stats]
Stats --> Reporter[stats.FileReporter]
Reporter --> File["/tmp/UAmask.stats"]
File --> LuCI[openwrt/luci/model/cbi/UAmask.lua]
当前落盘指标包括:
current_connectionstotal_requestsrpssuccessful_modificationsdirect_passthroughrule_processingcache_hit_modifycache_hit_passtotal_cache_ratio
LuCI 页面读取这个文件并展示运行状态、请求数、RPS、修改数和缓存效率。
13. OpenWrt 集成
OpenWrt 侧不是单纯打包壳,而是运行架构的一部分。
主要文件:
openwrt/package/Makefile:构建并安装 Go daemon、init 脚本、默认配置、LuCI 文件。openwrt/root/etc/config/UAmask:默认 UCI 配置。openwrt/root/etc/init.d/UAmask:procd 生命周期、flags 转换、防火墙规则装配与清理。openwrt/luci/model/cbi/UAmask.lua:配置页面与统计展示。openwrt/luci/controller/UAmask.lua:LuCI 菜单入口。
服务启动关系:
sequenceDiagram
participant LuCI as LuCI / UCI
participant Init as /etc/init.d/UAmask
participant Core as /usr/bin/UAmask
participant FW as nftables / iptables
LuCI->>Init: start / reload / stop
Init->>Init: detect_firewall()
Init->>Init: 从 UCI 读取配置
Init->>Init: 从 UCI 生成并校验 config.json
Init->>Core: procd 启动并传 -config
Init->>FW: 创建 REDIRECT 规则和 bypass set
Core->>FW: 运行中动态追加绕过目标
Core->>LuCI: 写 /tmp/UAmask.stats
init.d 会根据是否存在 fw4 选择 nftables 或 iptables:
- nftables 路径:生成
/tmp/UAmask_rules.nft,注册为 firewall include,并创建inet fw4 UAmask_bypass_set。 - iptables 路径:创建
UAmask_prerouting、UAmask_output链,并可创建ipset hash:ip,port。
当启用 proxy_host 时,init 脚本还会用 GID 规则避免 UA-Mask 自身流量和 OpenClash 流量再次被重定向。
14. 唯一生产路径与阅读顺序
旧的 proxy.Processor、firewall.DecisionEngine 和直接写防火墙的 sink 已删除。业务行为不得再绕过 session -> event/state/policy/offload 边界增加第二条路径。
proxy.Server 只负责 Accept、worker pool、活跃 session 跟踪和 context 关闭;HTTP 检测、rewrite、drop、fallback 和事件发射全部由 session.Manager 负责。firewall 只保留系统执行边界,目标状态与决策不放回该包。
建立代码图谱时按以下顺序阅读:
阅读代码时,应优先从这些入口建立图谱:
core/cmd/UAmask/main.gocore/internal/app/app.gocore/internal/session/manager.gocore/internal/rewrite/engine.gocore/internal/events/dispatcher.gocore/internal/state/store.gocore/internal/policy/evaluator.gocore/internal/offload/coordinator.gocore/internal/firewall/executor.go
15. 扩展点
当前代码最自然的扩展点在这些位置:
- 新增 UA 匹配模式:扩展
rewrite.Engine。 - 新增请求级行为:扩展
policy.SessionPolicy与model.PolicyDecision。 - 调整非 HTTP 卸载策略:扩展
state.Store的画像字段或policy.TargetPolicy。 - 支持新的防火墙后端:实现新的
firewall.Applier或调整CommandApplier。 - 增加观测方式:给
events.Dispatcher添加新的 sink,或替换/扩展stats.FileReporter。 - 实现真正的 flow-level offload:替换当前
offload.NoopFlowOffloader。
16. 一句话总结
当前 UA-Mask 可以理解成一条清晰的双管线:
flowchart LR
Conn[连接接入] --> Session[session 热路径]
Session --> Rewrite[HTTP UA 改写]
Rewrite --> Forward[转发上游]
Session --> Event[事件流]
Event --> Profile[目标画像]
Profile --> Policy[卸载策略]
Policy --> Firewall[动态写 set]
Firewall --> Bypass[后续流量绕过]
也就是:热路径负责尽快识别、改写、转发;控制路径异步观察流量特征,并把确定可以绕过的目标交给内核防火墙处理。