Files
pangolin/server/internal/agentd/warp.go
T
2026-07-23 01:52:39 +08:00

103 lines
3.7 KiB
Go

package agentd
import (
"encoding/json"
"fmt"
"net"
"os"
"strconv"
)
// WarpConfig 描述节点上「部分域名走 Cloudflare WARP 干净出口」的分流配置(#29)。
// 由节点本地文件(默认 <StateDir>/warp.json)提供,agent 渲染 sing-box 配置时读取:
// 存在且 enabled 且有域名 → 注入一个 WireGuard(WARP) endpoint + 域名分流 route 规则,
// 命中域名走 WARP、其余直连。运营改域名清单只需编辑该文件并重启 agent(sing-box 无热重载)。
//
// WARP 凭证(private_key / peer_public_key / endpoint / address / reserved)由 wgcf
// 注册免费匿名 WARP 账号得到,是节点私有的,不入 git、不经控制面。
type WarpConfig struct {
Enabled bool `json:"enabled"`
PrivateKey string `json:"private_key"`
PeerPublicKey string `json:"peer_public_key"`
Endpoint string `json:"endpoint"` // host:port,如 162.159.192.1:2408
Address []string `json:"address"` // 本端 WARP 分配地址,如 ["172.16.0.2/32","2606:4700:110:...::/128"]
Reserved []int `json:"reserved"` // WARP client reserved 三字节(可空)
MTU int `json:"mtu"` // 缺省 1280
Domains []string `json:"domains"` // 走 WARP 的域名后缀,如 ["reddit.com","redd.it"]
}
// LoadWarpConfig 读取并解析 warp.json。文件不存在 → 返回 (nil, nil)(WARP 未启用,
// 不是错误)。解析失败或字段缺失才返回 error,避免坏配置静默退化。
func LoadWarpConfig(path string) (*WarpConfig, error) {
data, err := os.ReadFile(path)
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("agentd: read warp config %q: %w", path, err)
}
var wc WarpConfig
if err := json.Unmarshal(data, &wc); err != nil {
return nil, fmt.Errorf("agentd: parse warp config %q: %w", path, err)
}
return &wc, nil
}
// active 报告本配置是否应真正注入分流(启用、凭证齐全、至少一个域名)。
// 任一必需字段缺失都返回 false —— 宁可不分流(全直连)也不产出坏 sing-box 配置。
func (wc *WarpConfig) active() bool {
if wc == nil || !wc.Enabled || len(wc.Domains) == 0 {
return false
}
if wc.PrivateKey == "" || wc.PeerPublicKey == "" || wc.Endpoint == "" || len(wc.Address) == 0 {
return false
}
host, _, err := net.SplitHostPort(wc.Endpoint)
return err == nil && host != ""
}
// mtu 返回配置的 MTU 或缺省 1280(WARP 常用值)。
func (wc *WarpConfig) mtu() int {
if wc.MTU > 0 {
return wc.MTU
}
return 1280
}
// cleanDomains 去空白/空项后返回域名清单(用于 domain_suffix)。
// 与 ACL 的目的地域名规范化共用 cleanHosts,避免两处逻辑漂移。
func (wc *WarpConfig) cleanDomains() []string {
return cleanHosts(wc.Domains)
}
// endpointHostPort 拆 Endpoint 为 host + port(active() 已校验可拆)。
func (wc *WarpConfig) endpointHostPort() (string, int) {
host, portStr, _ := net.SplitHostPort(wc.Endpoint)
port, _ := strconv.Atoi(portStr)
return host, port
}
// warpEndpoint 构造 sing-box 1.11+ 的 WireGuard endpoint(userspace,无需内核 wg 模块)。
// tag = "warp",route 规则以此 tag 作 outbound。
func (wc *WarpConfig) warpEndpoint() map[string]any {
host, port := wc.endpointHostPort()
peer := map[string]any{
"address": host,
"port": port,
"public_key": wc.PeerPublicKey,
"allowed_ips": []string{"0.0.0.0/0", "::/0"},
}
if len(wc.Reserved) == 3 {
peer["reserved"] = wc.Reserved
}
return map[string]any{
"type": "wireguard",
"tag": warpOutboundTag,
"system": false, // gVisor 用户态,不依赖内核 wireguard
"mtu": wc.mtu(),
"address": wc.Address,
"private_key": wc.PrivateKey,
"peers": []any{peer},
}
}