package agentd import ( "encoding/json" "fmt" "net" "os" "strconv" "strings" ) // WarpConfig 描述节点上「部分域名走 Cloudflare WARP 干净出口」的分流配置(#29)。 // 由节点本地文件(默认 /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)。 func (wc *WarpConfig) cleanDomains() []string { out := make([]string, 0, len(wc.Domains)) for _, d := range wc.Domains { d = strings.TrimSpace(strings.ToLower(d)) if d != "" { out = append(out, d) } } return out } // 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}, } } // warpRoute 构造分流 route:先 sniff 取出 SNI/Host(客户端多半发的是已解析 IP, // 不 sniff 域名规则无从命中),命中域名后缀走 warp,其余 final=direct。 func (wc *WarpConfig) warpRoute() map[string]any { return map[string]any{ "rules": []any{ map[string]any{"action": "sniff"}, map[string]any{"domain_suffix": wc.cleanDomains(), "outbound": warpOutboundTag}, }, "final": directOutboundTag, } }