diff --git a/README.en.md b/README.en.md index d861047..c49da56 100644 --- a/README.en.md +++ b/README.en.md @@ -6,7 +6,6 @@ > Languages / 语言: [README](./README.md) · **English** · [简体中文](./README.zh-CN.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Español](./README.es.md) · [Français](./README.fr.md) · [Deutsch](./README.de.md) · [Русский](./README.ru.md) · [Português (Brasil)](./README.pt-BR.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md) · [ไทย](./README.th.md) · [Türkçe](./README.tr.md) · [العربية](./README.ar.md) · [हिन्दी](./README.hi.md) · [Polski](./README.pl.md) · [Українська](./README.uk.md) - **Mihomo server web management panel** > **Stable first:** install and update use formal releases by default. Pre-releases require an explicit choice. Full bundles and Docker images ship a pinned Mihomo core — see [installation docs](https://3m-ui.top/docs/install.html). @@ -28,8 +27,6 @@ Lightweight and self-hosted. Manage [Mihomo](https://github.com/MetaCubeX/mihomo | **Users** | Bind nodes, traffic/expiry, **per-node usage** (billed = raw × node multiplier), IP/sub-pull limits, start-on-first-use, cycle renew/reset, groups/tags, external sub merge, batch ops, subscription tokens | | **Subscriptions** | UA routing for Clash/Mihomo YAML, v2ray Base64 (`?target=v2ray` always Base64), sing-box JSON; optional `?target=`; HTML info page; TUIC/HY2 share TLS params | | **Config** | Generate → validate → apply; rollback previous `config.yaml` on failure | -| **Routing** | Structured Mihomo rules, proxy-groups, templates, WARP inject; save then generate & apply | -| **Ops** | Core start/stop/update, logs, dashboard, online connections, Geo, panel SSL/ACME, WARP one-click, backup/restore | | **Telegram** | Alerts and bot commands (token + chat IDs) | | **Cluster** | Register remote panels, health checks, node mirror sync, merged subscriptions; **push local nodes by name** (created disabled on remote) | @@ -52,7 +49,6 @@ Default panel: `http://SERVER_IP:8080/` — user `admin`, one-time random passwo --- - ### Install with an AI assistant prompt Copy the prompt into ChatGPT / Claude / Cursor (and state your OS, e.g. Ubuntu 22.04, and whether you have root SSH). The assistant should only use the official installer — not rewrite URLs or invent third-party commands. The full prompt and usage notes live in [the AI install prompt doc](docs/ai-install-prompt.md). diff --git a/README.md b/README.md index 66491cb..d5e79e6 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,8 @@ > Languages / 语言: **README** · [English](./README.en.md) · [简体中文](./README.zh-CN.md) · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Español](./README.es.md) · [Français](./README.fr.md) · [Deutsch](./README.de.md) · [Русский](./README.ru.md) · [Português (Brasil)](./README.pt-BR.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md) · [ไทย](./README.th.md) · [Türkçe](./README.tr.md) · [العربية](./README.ar.md) · [हिन्दी](./README.hi.md) · [Polski](./README.pl.md) · [Українська](./README.uk.md) - **Mihomo 服务端 Web 管理面板** - > **稳定版优先:** 默认安装和更新只使用正式 Release。预发布需要主动选择。完整安装包及 Docker 镜像包含固定版本的 Mihomo;详见 [安装、升级与恢复](docs/installation.md)。 轻量、自托管,用于在 Linux 上管理 [Mihomo](https://github.com/MetaCubeX/mihomo) Listener、用户、订阅与运行状态。 @@ -31,13 +29,10 @@ | **用户** | 绑定节点、流量限额、**按节点分别计量**(计费 = 实际 × 节点倍率)、到期、IP/订阅拉取限制、首次使用起算、周期续期/重置、分组标签、外部订阅合并、批量操作、订阅 Token | | **订阅** | UA 自动识别 Clash/Mihomo YAML、v2ray Base64(`?target=v2ray` 始终 Base64)、sing-box JSON;可选 `?target=`;HTML 订阅页;TUIC/HY2 分享链含 SNI/`allow_insecure` 等客户端参数 | | **配置** | 生成 → 校验 → 应用 分离;失败回滚上一份 `config.yaml` | -| **路由** | 结构化规则(类型/目标/排序)、策略组、模板、WARP 注入出站;保存后需生成并应用 | -| **运维** | 核心启停/更新、日志、仪表盘、在线连接、Geo、面板 SSL/ACME、WARP 一键、备份恢复 | | **Telegram** | 告警与管理命令(需 Token + Chat ID) | | **多机** | 登记远程面板、健康检查、同步节点镜像、合并订阅;**按名称选择本机节点推送**到远程(默认禁用,可改名/端口) | | **安全** | JWT、首登改密、TOTP 两步验证、web_path 路径前缀、凭据加密、CORS | - ### 补充说明(用户限制 / 证书 / 订阅) - [用户限制:IP · 订阅拉取](docs/users-limits.md) @@ -155,7 +150,6 @@ JWT / 凭据密钥请使用独立随机值(≥ 32 字节),不要使用文 自定义面板端口:`3m-ui config port ` 或编辑配置后重启服务。 新建节点默认随机填写 10000~60000 的监听端口(轻量抽样并避开已加载节点端口;支持倒序范围),可手动修改或点击「随机端口」。 - --- ## 架构示意 @@ -188,7 +182,6 @@ JWT / 凭据密钥请使用独立随机值(≥ 32 字节),不要使用文 --- - ## 致谢 特别感谢以下开源项目和社区: @@ -230,7 +223,6 @@ JWT / 凭据密钥请使用独立随机值(≥ 32 字节),不要使用文 --- - --- ## English & other languages diff --git a/README.zh-CN.md b/README.zh-CN.md index fbf39cd..5496757 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -6,7 +6,6 @@ > Languages / 语言: [README](./README.md) · [English](./README.en.md) · **简体中文** · [繁體中文](./README.zh-TW.md) · [日本語](./README.ja.md) · [한국어](./README.ko.md) · [Español](./README.es.md) · [Français](./README.fr.md) · [Deutsch](./README.de.md) · [Русский](./README.ru.md) · [Português (Brasil)](./README.pt-BR.md) · [Tiếng Việt](./README.vi.md) · [Bahasa Indonesia](./README.id.md) · [ไทย](./README.th.md) · [Türkçe](./README.tr.md) · [العربية](./README.ar.md) · [हिन्दी](./README.hi.md) · [Polski](./README.pl.md) · [Українська](./README.uk.md) - **Mihomo 服务端 Web 管理面板** > **稳定版优先:** 默认安装和更新只使用正式 Release。预发布需主动选择。完整安装包及 Docker 镜像包含固定版本 Mihomo;详见 [安装文档](https://3m-ui.top/docs/install.html)。 @@ -27,9 +26,7 @@ | **节点** | 协议注册表驱动的 Listener:VLESS / VMess / Trojan / Shadowsocks / Hysteria2 / TUIC / AnyTLS / Snell / ShadowQUIC 等;REALITY、TLS 自签、一键批量证书、**流量倍率**、传输层字段校验 | | **用户** | 绑定节点、流量限额、**按节点分别计量**(计费 = 实际 × 节点倍率)、到期、IP/订阅拉取限制、首次使用起算、周期续期/重置、分组标签、外部订阅合并、批量操作、订阅 Token | | **订阅** | UA 识别 Clash/Mihomo YAML、v2ray Base64(`?target=v2ray` 始终 Base64)、sing-box JSON;可选 `?target=`;HTML 订阅页;TUIC/HY2 分享链含客户端 TLS 参数 | -| **路由** | 结构化规则、策略组、模板、WARP 注入;保存后生成并应用 | | **配置** | 生成 → 校验 → 应用;失败回滚上一份 `config.yaml` | -| **运维** | 核心启停/更新、日志、仪表盘、在线连接、Geo、面板 SSL/ACME、WARP 一键、备份恢复 | | **Telegram** | 告警与管理命令(需 Token + Chat ID) | | **多机** | 登记远程面板、健康检查、节点镜像同步、合并订阅;按名称选择本机节点推送到远程(默认禁用) | @@ -78,7 +75,6 @@ curl -fsSL https://raw.githubusercontent.com/kazeyukiro/3m-ui/main/docs/ai-insta curl -fsSL https://raw.githubusercontent.com/kazeyukiro/3m-ui/main/scripts/install.sh | sudo sh ``` - --- ## 源码编译 diff --git a/backend/internal/converter/client_routing.go b/backend/internal/converter/client_routing.go index 09e4d0d..7a96720 100644 --- a/backend/internal/converter/client_routing.go +++ b/backend/internal/converter/client_routing.go @@ -24,9 +24,8 @@ func clientSubscriptionDocument(proxies []map[string]interface{}, names []string var groups []interface{} var rules []string - // Merge panel visual outbounds (e.g. WARP WireGuard) into the client document - // so inject-warp + MATCH,WARP-OUT works on phones/PCs. Server config still - // strips these (inbound-only). + // Merge panel visual outbounds (e.g. visual outbounds) into the client document + // so optional visual outbounds for client YAML. Server strips visual proxies. if visual != nil && len(visual.Proxies) > 0 { proxies, names = mergeVisualProxiesForClient(proxies, names, visual.Proxies) } @@ -171,7 +170,7 @@ func adaptVisualGroupsForClient(src []mihomocfg.GroupEntry, names []string) []in members = append(members, m) continue } - // Drop server-only outbounds (e.g. WARP-OUT) not present in this sub. + // Drop outbounds not present in this subscription. } if shouldInjectSubscriptionNodes(name, members, groupNameSet) && len(names) > 0 { injected := make([]string, 0, len(names)+len(members)) @@ -258,7 +257,7 @@ func filterClientRules(rules []string, groupNames map[string]struct{}, proxyName target = strings.TrimSpace(parts[targetIdx]) } if _, ok := valid[target]; !ok { - // Server-only outbound (e.g. WARP) — fall back to PROXY group or DIRECT. + // Unknown outbound — fall back to PROXY group or DIRECT. if _, has := valid["PROXY"]; has { parts[targetIdx] = "PROXY" } else if _, has := valid["代理"]; has { diff --git a/backend/internal/converter/client_routing_test.go b/backend/internal/converter/client_routing_test.go index d4352e9..e6d6893 100644 --- a/backend/internal/converter/client_routing_test.go +++ b/backend/internal/converter/client_routing_test.go @@ -73,10 +73,10 @@ func TestClientSubscriptionUsesVisualGroups(t *testing.T) { func TestClientSubscriptionIncludesVisualProxy(t *testing.T) { v := &mihomocfg.VisualConfig{ Proxies: []mihomocfg.ProxyEntry{ - {Name: "WARP-OUT", Type: "wireguard", Server: "engage.cloudflareclient.com", Port: 2408, + {Name: "EXTRA-OUT", Type: "ss", Server: "example.com", Port: 2408, Options: map[string]interface{}{"private-key": "x", "public-key": "y"}}, }, - Rules: []string{"MATCH,WARP-OUT"}, + Rules: []string{"MATCH,EXTRA-OUT"}, } doc := clientSubscriptionDocument( []map[string]interface{}{{"name": "node-a", "type": "vless"}}, @@ -88,7 +88,7 @@ func TestClientSubscriptionIncludesVisualProxy(t *testing.T) { t.Fatal(err) } s := string(raw) - for _, want := range []string{"WARP-OUT", "wireguard", "MATCH,WARP-OUT", "node-a"} { + for _, want := range []string{"EXTRA-OUT", "ss", "MATCH,EXTRA-OUT", "node-a"} { if !strings.Contains(s, want) { t.Fatalf("missing %q in:\n%s", want, s) } diff --git a/backend/internal/mihomo/config/generator.go b/backend/internal/mihomo/config/generator.go index 88d0c2f..f49b9b0 100644 --- a/backend/internal/mihomo/config/generator.go +++ b/backend/internal/mihomo/config/generator.go @@ -49,9 +49,8 @@ func (ce *ConfigEngine) GenerateFinalConfig() (string, error) { // Listeners are owned by the panel DB; fragments must not inject them // or Mihomo will report duplicate listener names after merge. delete(fragMap, "listeners") - // visual-config primarily holds **client subscription** routing. - // Community rules/groups stay off the server; WARP-style exit proxies - // are re-applied below via applyServerExitFromVisual. + // visual-config is client-subscription routing only; never merge + // rules / proxy-groups / proxies into the serving Mihomo process. if fragment.Name == visualConfigName || fragment.Type == "visual" { delete(fragMap, "rules") delete(fragMap, "proxy-groups") @@ -61,12 +60,9 @@ func (ce *ConfigEngine) GenerateFinalConfig() (string, error) { merged[k] = v } } - // Default: inbound panel exits DIRECT. If visual-config has a WARP (or - // similar) outbound, use it so user traffic via listeners exits with that IP. + // Inbound panel: always DIRECT exit (no server-side split). + merged["rules"] = []interface{}{"MATCH,DIRECT"} merged["proxy-groups"] = []interface{}{} - if err := applyServerExitFromVisual(ce.db, merged); err != nil { - return "", err - } var listeners []models.Listener if err := ce.db.Where("enabled = ?", true).Find(&listeners).Error; err != nil { return "", fmt.Errorf("load enabled listeners: %w", err) diff --git a/backend/internal/mihomo/config/proxy_entry_yaml_test.go b/backend/internal/mihomo/config/proxy_entry_yaml_test.go index d14d895..e901b5e 100644 --- a/backend/internal/mihomo/config/proxy_entry_yaml_test.go +++ b/backend/internal/mihomo/config/proxy_entry_yaml_test.go @@ -9,9 +9,9 @@ import ( func TestProxyEntryYAMLRoundTripKeepsPrivateKey(t *testing.T) { in := ProxyEntry{ - Name: "WARP-OUT", + Name: "WG-OUT", Type: "wireguard", - Server: "engage.cloudflareclient.com", + Server: "example.com", Port: 2408, Options: map[string]interface{}{ "private-key": "eCtXsJZ27+4PbhDkHnB923tkUn2Gj59wZw5wFA75MnU=", diff --git a/backend/internal/mihomo/config/server_exit.go b/backend/internal/mihomo/config/server_exit.go deleted file mode 100644 index a71204f..0000000 --- a/backend/internal/mihomo/config/server_exit.go +++ /dev/null @@ -1,208 +0,0 @@ -package config - -import ( - "encoding/base64" - "fmt" - "strings" - - "gorm.io/gorm" -) - -// applyServerExitFromVisual optionally sets the serving Mihomo process to exit -// via a WARP-style outbound stored in visual-config (user → node → WARP IP). -// Client community rules/groups are never applied here. -func applyServerExitFromVisual(db *gorm.DB, merged map[string]interface{}) error { - if db == nil || merged == nil { - if merged != nil { - merged["rules"] = []interface{}{"MATCH,DIRECT"} - } - return nil - } - cfg, err := GetVisualConfig(db) - if err != nil { - merged["rules"] = []interface{}{"MATCH,DIRECT"} - return nil - } - exits := serverExitProxies(cfg.Proxies) - if len(exits) == 0 { - merged["rules"] = []interface{}{"MATCH,DIRECT"} - return nil - } - - existing := proxiesAsMaps(merged["proxies"]) - nameSet := map[string]struct{}{} - for _, m := range existing { - if n, _ := m["name"].(string); n != "" { - nameSet[n] = struct{}{} - } - } - var primary string - for _, p := range exits { - m, err := normalizeServerExitProxy(p) - if err != nil { - return fmt.Errorf("server exit proxy %q: %w", p.Name, err) - } - n, _ := m["name"].(string) - if n == "" { - continue - } - if _, ok := nameSet[n]; ok { - for i, em := range existing { - if en, _ := em["name"].(string); en == n { - existing[i] = m - break - } - } - } else { - existing = append(existing, m) - nameSet[n] = struct{}{} - } - if primary == "" { - primary = n - } - } - if primary == "" { - merged["rules"] = []interface{}{"MATCH,DIRECT"} - return nil - } - merged["proxies"] = existing - merged["rules"] = serverExitRules(cfg.Rules, primary) - return nil -} - -func serverExitProxies(list []ProxyEntry) []ProxyEntry { - var out []ProxyEntry - for _, p := range list { - if isServerExitProxy(p) { - out = append(out, p) - } - } - return out -} - -func isServerExitProxy(p ProxyEntry) bool { - typ := strings.ToLower(strings.TrimSpace(p.Type)) - name := strings.ToUpper(strings.TrimSpace(p.Name)) - if typ == "wireguard" { - return true - } - // MASQUE uses different crypto; only promote wireguard to server exit for now. - if strings.Contains(name, "WARP") && typ == "wireguard" { - return true - } - if strings.Contains(name, "WARP") && typ == "" { - // type lost on bad round-trip — still try if private-key looks like WG - return true - } - return false -} - -func normalizeServerExitProxy(p ProxyEntry) (map[string]interface{}, error) { - m := proxyEntryToMap(p) - typ := strings.ToLower(strings.TrimSpace(fmt.Sprint(m["type"]))) - if typ == "" || typ == "masque" { - // Force wireguard for Cloudflare WARP server exit (MASQUE schema differs). - m["type"] = "wireguard" - typ = "wireguard" - } - if typ != "wireguard" { - return nil, fmt.Errorf("unsupported exit type %q (want wireguard)", typ) - } - pk := strings.TrimSpace(fmt.Sprint(m["private-key"])) - if pk == "" || pk == "" { - return nil, fmt.Errorf("missing private-key (re-inject WARP)") - } - if err := validateWireGuardPrivateKey(pk); err != nil { - return nil, err - } - m["private-key"] = pk - // Ensure peer public key (Cloudflare WARP account key is well-known). - if strings.TrimSpace(fmt.Sprint(m["public-key"])) == "" || fmt.Sprint(m["public-key"]) == "" { - m["public-key"] = "bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=" - } - if _, ok := m["udp"]; !ok { - m["udp"] = true - } - if _, ok := m["allowed-ips"]; !ok { - m["allowed-ips"] = []string{"0.0.0.0/0", "::/0"} - } - if srv := strings.TrimSpace(fmt.Sprint(m["server"])); srv == "" || srv == "" { - m["server"] = "engage.cloudflareclient.com" - } - if m["port"] == nil || fmt.Sprint(m["port"]) == "0" { - m["port"] = 2408 - } - return m, nil -} - -func validateWireGuardPrivateKey(pk string) error { - // Accept standard base64 (with padding) of 32 raw bytes — WireGuard/Curve25519. - raw, err := base64.StdEncoding.DecodeString(pk) - if err != nil { - // try raw std without padding - raw, err = base64.RawStdEncoding.DecodeString(pk) - } - if err != nil { - return fmt.Errorf("private-key is not valid base64 (got %q): %w", truncateKey(pk), err) - } - if len(raw) != 32 { - return fmt.Errorf("private-key must decode to 32 bytes (WireGuard), got %d — re-inject WARP", len(raw)) - } - return nil -} - -func truncateKey(s string) string { - if len(s) <= 16 { - return s - } - return s[:8] + "…" + s[len(s)-4:] -} - -func proxyEntryToMap(p ProxyEntry) map[string]interface{} { - m := map[string]interface{}{ - "name": p.Name, - "type": p.Type, - "server": p.Server, - "port": p.Port, - } - for k, v := range p.Options { - m[k] = v - } - return m -} - -func proxiesAsMaps(v interface{}) []map[string]interface{} { - out := []map[string]interface{}{} - switch t := v.(type) { - case []map[string]interface{}: - return append(out, t...) - case []interface{}: - for _, item := range t { - if m, ok := item.(map[string]interface{}); ok { - out = append(out, m) - } - } - } - return out -} - -func serverExitRules(visualRules []string, exitName string) []interface{} { - if exitName == "" { - return []interface{}{"MATCH,DIRECT"} - } - cnDirect := false - for _, line := range visualRules { - u := strings.ToUpper(strings.TrimSpace(line)) - if strings.Contains(u, "GEOIP,CN,DIRECT") { - cnDirect = true - break - } - } - if cnDirect { - return []interface{}{ - "GEOIP,CN,DIRECT,no-resolve", - "MATCH," + exitName, - } - } - return []interface{}{"MATCH," + exitName} -} diff --git a/backend/internal/mihomo/config/server_exit_test.go b/backend/internal/mihomo/config/server_exit_test.go deleted file mode 100644 index b02fb92..0000000 --- a/backend/internal/mihomo/config/server_exit_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package config - -import "testing" - -func TestServerExitRules(t *testing.T) { - r := serverExitRules(nil, "WARP-OUT") - if len(r) != 1 || r[0] != "MATCH,WARP-OUT" { - t.Fatalf("got %#v", r) - } - r = serverExitRules([]string{"GEOIP,CN,DIRECT", "MATCH,WARP-OUT"}, "WARP-OUT") - if len(r) != 2 || r[1] != "MATCH,WARP-OUT" { - t.Fatalf("cn mode %#v", r) - } -} - -func TestIsServerExitProxy(t *testing.T) { - if !isServerExitProxy(ProxyEntry{Name: "WARP-OUT", Type: "wireguard"}) { - t.Fatal("expected wireguard") - } - if isServerExitProxy(ProxyEntry{Name: "ss1", Type: "ss"}) { - t.Fatal("ss should not be server exit") - } -} diff --git a/backend/internal/mihomo/config/visual.go b/backend/internal/mihomo/config/visual.go index 5f32a2a..f93e218 100644 --- a/backend/internal/mihomo/config/visual.go +++ b/backend/internal/mihomo/config/visual.go @@ -194,111 +194,3 @@ func SaveVisualConfig(db *gorm.DB, cfg VisualConfig) error { return db.Create(&fragment).Error } -// InjectWARPProxy merges a WARP outbound (from register YAML) into visual proxies. -// ruleMode: ""|"none" — no rule change; "match" — append MATCH, (before last MATCH if any); -// "cn_direct" — ensure GEOIP,CN,DIRECT then MATCH,. -func InjectWARPProxy(db *gorm.DB, proxyYAML string, proxyName string, ruleMode string) (VisualConfig, error) { - cfg, err := GetVisualConfig(db) - if err != nil { - return cfg, err - } - var frag map[string]interface{} - if err := yaml.Unmarshal([]byte(proxyYAML), &frag); err != nil { - return cfg, fmt.Errorf("parse WARP yaml: %w", err) - } - // Accept either {proxies:[{...}]} or a single proxy map, or full config snippet. - var proxyMap map[string]interface{} - if list, ok := frag["proxies"].([]interface{}); ok && len(list) > 0 { - if m, ok := list[0].(map[string]interface{}); ok { - proxyMap = m - } - } else if frag["type"] != nil && frag["name"] != nil { - proxyMap = frag - } - if proxyMap == nil { - return cfg, fmt.Errorf("WARP yaml has no proxies entry") - } - name := proxyName - if name == "" { - if n, ok := proxyMap["name"].(string); ok && n != "" { - name = n - } else { - name = "WARP-OUT" - } - } - proxyMap["name"] = name - typ, _ := proxyMap["type"].(string) - typ = strings.ToLower(strings.TrimSpace(typ)) - if typ == "" || typ == "masque" { - // Server exit + client sub both use WireGuard schema for Cloudflare WARP. - // MASQUE private-key is not interchangeable with WG; force wireguard. - typ = "wireguard" - proxyMap["type"] = "wireguard" - } - server, _ := proxyMap["server"].(string) - port := proxyMap["port"] - opts := map[string]interface{}{} - for k, v := range proxyMap { - if k == "name" || k == "type" || k == "server" || k == "port" { - continue - } - opts[k] = v - } - if _, ok := opts["private-key"]; !ok { - return cfg, fmt.Errorf("WARP yaml missing private-key") - } - if _, ok := opts["allowed-ips"]; !ok { - opts["allowed-ips"] = []string{"0.0.0.0/0", "::/0"} - } - entry := ProxyEntry{Name: name, Type: typ, Server: server, Port: port, Options: opts} - // Replace existing same name - replaced := false - for i, p := range cfg.Proxies { - if p.Name == name { - cfg.Proxies[i] = entry - replaced = true - break - } - } - if !replaced { - cfg.Proxies = append(cfg.Proxies, entry) - } - - ruleMode = strings.ToLower(strings.TrimSpace(ruleMode)) - switch ruleMode { - case "", "none": - // leave rules - case "match": - cfg.Rules = appendRulePreferLastMatch(cfg.Rules, "MATCH,"+name) - case "cn_direct": - cfg.Rules = []string{"GEOIP,CN,DIRECT", "MATCH," + name} - default: - return cfg, fmt.Errorf("invalid rule_mode %q", ruleMode) - } - if err := SaveVisualConfig(db, cfg); err != nil { - return cfg, err - } - return cfg, nil -} - -func appendRulePreferLastMatch(rules []string, matchRule string) []string { - out := make([]string, 0, len(rules)+1) - inserted := false - for _, r := range rules { - tr := strings.TrimSpace(r) - if strings.HasPrefix(strings.ToUpper(tr), "MATCH,") { - if !inserted { - out = append(out, matchRule) - inserted = true - } - continue // drop old MATCH lines; single final MATCH - } - if tr != "" { - out = append(out, tr) - } - } - if !inserted { - out = append(out, matchRule) - } - return out -} diff --git a/backend/internal/mihomo/config/visual_warp_test.go b/backend/internal/mihomo/config/visual_warp_test.go deleted file mode 100644 index bd88536..0000000 --- a/backend/internal/mihomo/config/visual_warp_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package config - -import "testing" - -func TestAppendRulePreferLastMatch(t *testing.T) { - got := appendRulePreferLastMatch([]string{"GEOIP,CN,DIRECT", "MATCH,DIRECT"}, "MATCH,WARP-OUT") - if len(got) != 2 || got[1] != "MATCH,WARP-OUT" { - t.Fatalf("got %#v", got) - } - got = appendRulePreferLastMatch([]string{}, "MATCH,WARP-OUT") - if len(got) != 1 || got[0] != "MATCH,WARP-OUT" { - t.Fatalf("empty %#v", got) - } -} diff --git a/backend/internal/router/routing_routes.go b/backend/internal/router/routing_routes.go index b84f9c9..bb0dde7 100644 --- a/backend/internal/router/routing_routes.go +++ b/backend/internal/router/routing_routes.go @@ -5,7 +5,6 @@ import ( "github.com/gin-gonic/gin" mihomoConfig "github.com/kazeyukiro/3m-ui/backend/internal/mihomo/config" - "github.com/kazeyukiro/3m-ui/backend/internal/system" "gorm.io/gorm" ) @@ -63,48 +62,5 @@ func registerRoutingRoutes(api *gin.RouterGroup, db *gorm.DB) { } c.JSON(http.StatusOK, visual.Rules) }) - - group.POST("/routing/inject-warp", func(c *gin.Context) { - var body struct { - Mode string `json:"mode"` - RuleMode string `json:"rule_mode"` - Name string `json:"name"` - } - _ = c.ShouldBindJSON(&body) - if body.Mode == "" { - body.Mode = "wireguard" - } - res, err := system.RegisterWARP() - if err != nil { - c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) - return - } - yamlStr := res.YAML - name := body.Name - if name == "" { - name = "WARP-OUT" - } - if body.Mode == "masque" { - if res.MasqueYAML == "" { - c.JSON(http.StatusBadGateway, gin.H{"error": "MASQUE YAML empty"}) - return - } - yamlStr = res.MasqueYAML - if body.Name == "" { - name = "WARP-Masque-OUT" - } - } - cfg, err := mihomoConfig.InjectWARPProxy(db, yamlStr, name, body.RuleMode) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - c.JSON(http.StatusOK, gin.H{ - "status": "ok", - "name": name, - "rules": cfg.Rules, - "proxies": cfg.Proxies, - "groups": cfg.Groups, - }) }) } diff --git a/backend/internal/system/api.go b/backend/internal/system/api.go index 501af2e..a52895d 100644 --- a/backend/internal/system/api.go +++ b/backend/internal/system/api.go @@ -39,8 +39,6 @@ func (h *Handler) RegisterRoutes(rg *gin.RouterGroup) { rg.POST("/templates/reverse-proxy", h.ReverseProxy) rg.POST("/templates/acme", h.ACME) rg.POST("/geofiles/update", h.UpdateGeoFiles) - rg.POST("/templates/warp", h.WARP) - rg.POST("/templates/warp/register", h.WARPRegister) } func (h *Handler) GetSystemStatus(c *gin.Context) { @@ -154,79 +152,6 @@ func (h *Handler) UpdateGeoFiles(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"dir": dir, "files": result}) } -// WARP builds a Mihomo WireGuard fragment from operator-supplied fields -// (m-ui compatible schema). Prefer POST /templates/warp/register for one-click. -func (h *Handler) WARP(c *gin.Context) { - var body struct { - PrivateKey string `json:"private_key"` - PublicKey string `json:"public_key"` // peer public key - Server string `json:"server"` - Port int `json:"port"` - Address string `json:"address"` - IPv6 string `json:"ipv6"` - Reserved []int `json:"reserved"` - } - _ = c.ShouldBindJSON(&body) - ipv4, ipv6 := body.Address, body.IPv6 - if i := strings.IndexByte(ipv4, ','); i >= 0 { - if ipv6 == "" { - ipv6 = strings.TrimSpace(ipv4[i+1:]) - } - ipv4 = strings.TrimSpace(ipv4[:i]) - } - pub := body.PublicKey - if pub == "" { - pub = "bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=" - } - server := body.Server - if server == "" { - server = "engage.cloudflareclient.com" - } - port := body.Port - if port == 0 { - port = 2408 - } - yaml, err := WARPTemplate(body.PrivateKey, pub, server, port, ipv4, ipv6, body.Reserved) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - c.JSON(http.StatusOK, gin.H{"yaml": yaml}) -} - -func (h *Handler) WARPRegister(c *gin.Context) { - res, err := RegisterWARP() - if err != nil { - c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) - return - } - // Optional `mode` query param: "masque" returns only the MASQUE YAML, - // "wireguard" (default) returns only the WireGuard YAML, "both" returns - // the full result (including both YAMLs). This keeps the response shape - // backward-compatible (the default `yaml` field stays WireGuard). - mode := strings.TrimSpace(c.Query("mode")) - switch mode { - case "", "wireguard": - c.JSON(http.StatusOK, gin.H{ - "yaml": res.YAML, - "masque_yaml": res.MasqueYAML, // bonus, for clients that support it - "address": res.Address, - "ipv6": res.IPv6, - "reserved": res.Reserved, - }) - case "masque": - c.JSON(http.StatusOK, gin.H{ - "yaml": res.MasqueYAML, - "address": res.Address, - "ipv6": res.IPv6, - }) - case "both": - c.JSON(http.StatusOK, res) - default: - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid mode %q: must be wireguard, masque, or both", mode)}) - } -} - func (h *Handler) backupDir() string { return backupsDirFromDB(h.dbPath) } diff --git a/backend/internal/system/warp.go b/backend/internal/system/warp.go deleted file mode 100644 index ff8514a..0000000 --- a/backend/internal/system/warp.go +++ /dev/null @@ -1,212 +0,0 @@ -package system - -import ( - "fmt" - "strings" - - "gopkg.in/yaml.v3" -) - -// warpWireGuardSpec is the structured shape of the Mihomo WireGuard outbound -// we render for Cloudflare WARP. Keeping it as a struct lets yaml.v3 handle -// escaping / indentation, which prevents a malicious operator-supplied -// private_key or address from breaking out of the YAML scalar and injecting -// sibling keys. -// Field names align with mihomo's wireguard outbound schema documented at -// https://wiki.metacubex.one/en/config/proxies/wg/ — in particular, IPv4 and -// IPv6 are separate fields (ip + ipv6), NOT a comma-joined string. -type warpWireGuardSpec struct { - PrivateKey string `yaml:"private-key,omitempty"` - Server string `yaml:"server"` - Port int `yaml:"port"` - IP string `yaml:"ip"` // IPv4 address (no CIDR) - IPv6 string `yaml:"ipv6,omitempty"` // IPv6 address (no CIDR) - PublicKey string `yaml:"public-key"` - UDP bool `yaml:"udp"` - Reserved []int `yaml:"reserved,omitempty"` - MTU int `yaml:"mtu"` -} - -type warpProxyGroup struct { - Name string `yaml:"name"` - Type string `yaml:"type"` - Proxies []string `yaml:"proxies"` -} - -type warpConfig struct { - Proxies []map[string]interface{} `yaml:"proxies"` - Groups []warpProxyGroup `yaml:"proxy-groups,omitempty"` -} - -// validateWARPField rejects shell-significant / YAML-significant characters in -// a WARP WireGuard field. WireGuard private keys and addresses are restricted -// to base64 (key) and dotted-quad/CIDR (address); anything else is suspicious. -func validateWARPField(s string) bool { - for _, r := range s { - switch { - case r >= 'a' && r <= 'z': - case r >= 'A' && r <= 'Z': - case r >= '0' && r <= '9': - case r == '+' || r == '/' || r == '=': // base64 alphabet - case r == ':' || r == '.' || r == '-' || r == '_': - case r == ',': // reserved list separator (kept simple) - default: - return false - } - } - return true -} - -// WARPMasqueTemplate returns a Mihomo YAML fragment for Cloudflare WARP using -// the MASQUE protocol (RFC 9298 / HTTP/3 CONNECT-UDP). Cloudflare WARP now -// defaults to MASQUE per the registration API's `policy.tunnel_protocol` -// field — this template emits the schema documented at -// https://wiki.metacubex.one/config/proxies/masque/ -// -// Key differences vs WireGuard WARP: -// - `ip` / `ipv6` carry a CIDR (/32 and /128) per mihomo's masque schema -// - No `reserved` field (that's wireguard-only) -// - Same X25519 keypair (Curve25519 private/public key from WARP register) -// - Optional `network` field: "" (default UDP) | "h2" | "h3-l4proxy" -// - Optional `congestion-controller`: "bbr" recommended for WARP -func WARPMasqueTemplate(privateKey, ipv4, ipv6, network string) (string, error) { - privateKey = strings.TrimSpace(privateKey) - if privateKey != "" { - if !validateWARPField(privateKey) { - return "", fmt.Errorf("invalid private_key: must be base64 / wireguard-safe") - } - } - ipv4 = strings.TrimSpace(ipv4) - ipv6 = strings.TrimSpace(ipv6) - if ipv4 == "" && ipv6 == "" { - ipv4 = "172.16.0.2" - } - // masque requires CIDR form. Append /32 / /128 if caller passed bare IP. - ipv4 = ensureCIDR(ipv4, "/32") - ipv6 = ensureCIDR(ipv6, "/128") - if !validateWARPField(ipv4) { - return "", fmt.Errorf("invalid IPv4 address: %q", ipv4) - } - if !validateWARPField(ipv6) { - return "", fmt.Errorf("invalid IPv6 address: %q", ipv6) - } - key := privateKey - if key == "" { - key = "YOUR_WARP_PRIVATE_KEY" - } - network = strings.TrimSpace(network) - if network != "" && network != "h2" && network != "h3-l4proxy" { - return "", fmt.Errorf("invalid network %q: must be '', 'h2', or 'h3-l4proxy'", network) - } - proxy := map[string]interface{}{ - "name": "WARP-Masque", - "type": "masque", - "server": "engage.cloudflareclient.com", - "port": 2408, - "ip": ipv4, - "private-key": key, - "public-key": "bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=", - "udp": true, - "mtu": 1280, - } - if ipv6 != "" { - proxy["ipv6"] = ipv6 - } - if network != "" { - proxy["network"] = network - } - cfg := warpConfig{ - Proxies: []map[string]interface{}{proxy}, - Groups: []warpProxyGroup{ - {Name: "WARP-Masque-OUT", Type: "select", Proxies: []string{"WARP-Masque", "DIRECT"}}, - }, - } - out, err := yaml.Marshal(cfg) - if err != nil { - return "", err - } - header := "# Cloudflare WARP (MASQUE) outbound for Mihomo\n" - footer := "\n# rules:\n# - MATCH,WARP-Masque-OUT\n" - return header + string(out) + footer, nil -} - -// ensureCIDR appends suffix if s does not already contain a '/'. -func ensureCIDR(s, suffix string) string { - if s == "" { - return s - } - if strings.IndexByte(s, '/') >= 0 { - return s - } - return s + suffix -} - -// WARPTemplate returns a Mihomo WireGuard outbound for Cloudflare WARP -// (aligned with RomanovCaesar/m-ui warpOutbound). -func WARPTemplate(privateKey, peerPublicKey, server string, port int, ipv4, ipv6 string, reserved []int) (string, error) { - privateKey = strings.TrimSpace(privateKey) - peerPublicKey = strings.TrimSpace(peerPublicKey) - server = strings.TrimSpace(server) - if privateKey == "" { - return "", fmt.Errorf("private_key required") - } - if !validateWARPField(privateKey) { - return "", fmt.Errorf("invalid private_key") - } - if peerPublicKey == "" { - return "", fmt.Errorf("peer public_key required") - } - if !validateWARPField(peerPublicKey) { - return "", fmt.Errorf("invalid peer public_key") - } - if server == "" { - server = "engage.cloudflareclient.com" - } - if port < 1 || port > 65535 { - port = 2408 - } - ipv4 = strings.TrimSpace(ipv4) - ipv6 = strings.TrimSpace(ipv6) - if ipv4 == "" && ipv6 == "" { - return "", fmt.Errorf("at least one of ipv4/ipv6 required") - } - if ipv4 != "" && !validateWARPField(ipv4) { - return "", fmt.Errorf("invalid IPv4 %q", ipv4) - } - if ipv6 != "" && !validateWARPField(ipv6) { - return "", fmt.Errorf("invalid IPv6 %q", ipv6) - } - proxy := map[string]interface{}{ - "name": "WARP", - "type": "wireguard", - "server": server, - "port": port, - "private-key": privateKey, - "public-key": peerPublicKey, - "udp": true, - "mtu": 1420, - "allowed-ips": []string{"0.0.0.0/0", "::/0"}, - } - if ipv4 != "" { - proxy["ip"] = ipv4 - } - if ipv6 != "" { - proxy["ipv6"] = ipv6 - } - if len(reserved) == 3 { - proxy["reserved"] = reserved - } - cfg := warpConfig{ - Proxies: []map[string]interface{}{proxy}, - Groups: []warpProxyGroup{ - {Name: "WARP-OUT", Type: "select", Proxies: []string{"WARP", "DIRECT"}}, - }, - } - out, err := yaml.Marshal(cfg) - if err != nil { - return "", err - } - header := "# Cloudflare WARP (WireGuard) — m-ui compatible outbound for Mihomo\n" - footer := "\n# rules:\n# - MATCH,WARP-OUT\n" - return header + string(out) + footer, nil -} diff --git a/backend/internal/system/warp_register.go b/backend/internal/system/warp_register.go deleted file mode 100644 index f141b1a..0000000 --- a/backend/internal/system/warp_register.go +++ /dev/null @@ -1,268 +0,0 @@ -package system - -import ( - "bytes" - "context" - "crypto/ecdh" - "crypto/rand" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "net" - "net/http" - "net/netip" - "strconv" - "strings" - "time" -) - -// Cloudflare consumer registration API (same endpoint family as m-ui / 3x-ui / wgcf). -const warpAPIBase = "https://api.cloudflareclient.com/v0a2158" - -// WARPRegisterResult is the material needed to build a Mihomo WARP outbound. -type WARPRegisterResult struct { - PrivateKey string `json:"private_key"` - PublicKey string `json:"public_key"` // peer (server) public key - Address string `json:"address"` - IPv6 string `json:"ipv6,omitempty"` - Reserved []int `json:"reserved,omitempty"` - Server string `json:"server,omitempty"` - Port int `json:"port,omitempty"` - DeviceID string `json:"device_id,omitempty"` - YAML string `json:"yaml"` - MasqueYAML string `json:"masque_yaml,omitempty"` -} - -type warpDevice struct { - ID string `json:"id"` - Token string `json:"token,omitempty"` - Name string `json:"name"` - Model string `json:"model"` - Enabled bool `json:"enabled"` - Account warpAccountInfo `json:"account"` - Config warpTunnelConfig `json:"config"` -} - -type warpAccountInfo struct { - License string `json:"license"` - AccountType string `json:"account_type"` - Role string `json:"role"` - PremiumData uint64 `json:"premium_data"` - Quota uint64 `json:"quota"` - Usage uint64 `json:"usage"` -} - -type warpTunnelConfig struct { - ClientID string `json:"client_id"` - Interface struct { - Addresses struct { - V4 string `json:"v4"` - V6 string `json:"v6"` - } `json:"addresses"` - } `json:"interface"` - Peers []struct { - PublicKey string `json:"public_key"` - Endpoint struct { - Host string `json:"host"` - V4 string `json:"v4"` - V6 string `json:"v6"` - } `json:"endpoint"` - } `json:"peers"` -} - -type warpHTTPClient struct { - baseURL string - httpClient *http.Client -} - -func defaultWarpClient() *warpHTTPClient { - return &warpHTTPClient{ - baseURL: warpAPIBase, - httpClient: &http.Client{ - Timeout: 20 * time.Second, - CheckRedirect: func(*http.Request, []*http.Request) error { - return http.ErrUseLastResponse - }, - }, - } -} - -func (c *warpHTTPClient) request(ctx context.Context, method, path string, input, output any) error { - var body io.Reader - if input != nil { - data, err := json.Marshal(input) - if err != nil { - return err - } - body = bytes.NewReader(data) - } - req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body) - if err != nil { - return fmt.Errorf("warp: build request: %w", err) - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") - req.Header.Set("CF-Client-Version", "a-7.21-0721") - resp, err := c.httpClient.Do(req) - if err != nil { - return fmt.Errorf("warp: connect Cloudflare failed (check outbound network): %w", err) - } - defer resp.Body.Close() - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return fmt.Errorf("warp: Cloudflare HTTP %d", resp.StatusCode) - } - data, err := io.ReadAll(io.LimitReader(resp.Body, (1<<20)+1)) - if err != nil || len(data) > 1<<20 { - return fmt.Errorf("warp: response read failed or too large") - } - trimmed := bytes.TrimSpace(data) - if len(trimmed) == 0 || trimmed[0] != '{' { - return fmt.Errorf("warp: invalid response object") - } - var envelope struct { - Success *bool `json:"success"` - Errors []json.RawMessage `json:"errors"` - Result json.RawMessage `json:"result"` - } - if err = json.Unmarshal(data, &envelope); err != nil { - return fmt.Errorf("warp: invalid JSON") - } - if envelope.Success != nil && !*envelope.Success || len(envelope.Errors) > 0 { - return fmt.Errorf("warp: Cloudflare rejected the request") - } - if len(envelope.Result) > 0 && string(envelope.Result) != "null" { - data = envelope.Result - } - if output != nil { - if err = json.Unmarshal(data, output); err != nil { - return fmt.Errorf("warp: decode device: %w", err) - } - } - return nil -} - -func decodeWarpKey(value string) ([]byte, error) { - data, err := base64.StdEncoding.DecodeString(strings.TrimSpace(value)) - if err != nil || len(data) != 32 { - return nil, fmt.Errorf("WireGuard key must be 32-byte base64") - } - return data, nil -} - -func warpAddress(value string, ipv4 bool) (string, error) { - if value == "" { - return "", nil - } - address, err := netip.ParseAddr(value) - if err != nil { - if prefix, prefixErr := netip.ParsePrefix(value); prefixErr == nil { - address = prefix.Addr() - err = nil - } - } - if err != nil || address.Is4() != ipv4 { - return "", fmt.Errorf("invalid tunnel address %q", value) - } - return address.String(), nil -} - -func valueOr(a, b string) string { - if strings.TrimSpace(a) != "" { - return a - } - return b -} - -// RegisterWARP registers a Cloudflare WARP device (m-ui / 3x-ui style) and -// returns a Mihomo WireGuard outbound YAML fragment. -func RegisterWARP() (*WARPRegisterResult, error) { - return registerWARPWithClient(context.Background(), defaultWarpClient()) -} - -func registerWARPWithClient(ctx context.Context, client *warpHTTPClient) (*WARPRegisterResult, error) { - if client == nil { - client = defaultWarpClient() - } - key, err := ecdh.X25519().GenerateKey(rand.Reader) - if err != nil { - return nil, fmt.Errorf("warp: generate key: %w", err) - } - privB64 := base64.StdEncoding.EncodeToString(key.Bytes()) - pubB64 := base64.StdEncoding.EncodeToString(key.PublicKey().Bytes()) - - input := map[string]any{ - "key": pubB64, - "tos": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"), - "type": "PC", - "model": "3m-ui", - "name": "3m-ui", - } - var device warpDevice - if err = client.request(ctx, http.MethodPost, "/reg", input, &device); err != nil { - return nil, err - } - if device.ID == "" { - return nil, fmt.Errorf("warp: empty device id") - } - if _, err = decodeWarpKey(privB64); err != nil { - return nil, err - } - - cfg := device.Config - if len(cfg.Peers) == 0 { - return nil, fmt.Errorf("warp: no WireGuard peer yet — retry registration") - } - peer := cfg.Peers[0] - if _, err = decodeWarpKey(peer.PublicKey); err != nil { - return nil, fmt.Errorf("warp: peer public-key invalid") - } - endpoint := valueOr(peer.Endpoint.Host, valueOr(peer.Endpoint.V4, peer.Endpoint.V6)) - host, portText, err := net.SplitHostPort(endpoint) - port, portErr := strconv.Atoi(portText) - if err != nil || portErr != nil || port < 1 || port > 65535 || host == "" || strings.ContainsAny(host, "/\r\n \t") { - // Fallback: host-only endpoint + default WARP port - host = strings.TrimSpace(endpoint) - if host == "" || strings.ContainsAny(host, "/\r\n \t") { - return nil, fmt.Errorf("warp: invalid peer endpoint %q", endpoint) - } - port = 2408 - } - - reservedRaw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(cfg.ClientID)) - if err != nil || len(reservedRaw) != 3 { - return nil, fmt.Errorf("warp: client_id must be 3-byte base64 (got %q)", cfg.ClientID) - } - reserved := []int{int(reservedRaw[0]), int(reservedRaw[1]), int(reservedRaw[2])} - - v4, err := warpAddress(cfg.Interface.Addresses.V4, true) - if err != nil { - return nil, err - } - v6, err := warpAddress(cfg.Interface.Addresses.V6, false) - if err != nil { - return nil, err - } - if v4 == "" && v6 == "" { - return nil, fmt.Errorf("warp: empty interface addresses") - } - - yamlStr, err := WARPTemplate(privB64, peer.PublicKey, host, port, v4, v6, reserved) - if err != nil { - return nil, err - } - masqueYAML, _ := WARPMasqueTemplate(privB64, v4, v6, "") - - return &WARPRegisterResult{ - PrivateKey: privB64, - PublicKey: peer.PublicKey, - Address: v4, - IPv6: v6, - Reserved: reserved, - Server: host, - Port: port, - DeviceID: device.ID, - YAML: yamlStr, - MasqueYAML: masqueYAML, - }, nil -} diff --git a/backend/internal/system/warp_register_test.go b/backend/internal/system/warp_register_test.go deleted file mode 100644 index 49627d0..0000000 --- a/backend/internal/system/warp_register_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package system - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - "testing" -) - -func TestWARPTemplate_MUIShape(t *testing.T) { - yaml, err := WARPTemplate( - "CJiuBUMZWavAdfelvnUUnee+sQqHwU5ObGkFxjb6zWo=", - "bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=", - "engage.cloudflareclient.com", - 2408, - "172.16.0.2", - "2606:4700:110:8216:dac5:4a83:49de:6997", - []int{171, 91, 109}, - ) - if err != nil { - t.Fatal(err) - } - for _, want := range []string{ - "type: wireguard", - "ip: 172.16.0.2", - "private-key: CJiuBUMZWavAdfelvnUUnee+sQqHwU5ObGkFxjb6zWo=", - "public-key: bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=", - "server: engage.cloudflareclient.com", - "port: 2408", - "mtu: 1420", - "reserved:", - } { - if !strings.Contains(yaml, want) { - t.Fatalf("missing %q in:\n%s", want, yaml) - } - } -} - -func TestRegisterWARP_WithMockServer(t *testing.T) { - device := map[string]any{ - "id": "f0844485-ee81-4c51-a022-a86549ebb070", - "token": "test-token", - "name": "3m-ui", - "model": "3m-ui", - "account": map[string]any{ - "license": "aaaaaaaa-bbbbbbbb-cccccccc", - "account_type": "free", - }, - "config": map[string]any{ - "client_id": "qVtt", - "interface": map[string]any{ - "addresses": map[string]any{ - "v4": "172.16.0.2", - "v6": "2606:4700:110::1", - }, - }, - "peers": []map[string]any{ - { - "public_key": "bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=", - "endpoint": map[string]any{ - "host": "engage.cloudflareclient.com:2408", - "v4": "162.159.192.1:2408", - }, - }, - }, - }, - } - body, _ := json.Marshal(device) - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost || r.URL.Path != "/reg" { - http.NotFound(w, r) - return - } - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write(body) - })) - defer srv.Close() - - client := &warpHTTPClient{baseURL: srv.URL, httpClient: srv.Client()} - res, err := registerWARPWithClient(context.Background(), client) - if err != nil { - t.Fatal(err) - } - if res.Address != "172.16.0.2" { - t.Fatalf("address=%q", res.Address) - } - if len(res.Reserved) != 3 { - t.Fatalf("reserved=%v", res.Reserved) - } - if !strings.Contains(res.YAML, "type: wireguard") { - t.Fatalf("yaml:\n%s", res.YAML) - } - if !strings.Contains(res.YAML, "public-key: bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=") { - t.Fatalf("peer key missing:\n%s", res.YAML) - } - if res.Server != "engage.cloudflareclient.com" || res.Port != 2408 { - t.Fatalf("endpoint %s:%d", res.Server, res.Port) - } -} - -func TestRegisterWARP_RejectsBadEnvelope(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte(`{"success":false,"errors":[{"message":"nope"}]}`)) - })) - defer srv.Close() - client := &warpHTTPClient{baseURL: srv.URL, httpClient: srv.Client()} - if _, err := registerWARPWithClient(context.Background(), client); err == nil { - t.Fatal("expected error") - } -} - -func TestDecodeWarpKey(t *testing.T) { - if _, err := decodeWarpKey("CJiuBUMZWavAdfelvnUUnee+sQqHwU5ObGkFxjb6zWo="); err != nil { - t.Fatal(err) - } - if _, err := decodeWarpKey("bad"); err == nil { - t.Fatal("expected error") - } -} diff --git a/docs/routing.md b/docs/routing.md index 0822016..7193271 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -1,105 +1,11 @@ - -# Routing & proxy-groups +# Routing (client subscription) -Panel page **Routing** edits the **client subscription** layout only (`proxy-groups` + `rules` in `visual-config`). Mihomo/Clash YAML subscriptions export these groups with your nodes injected into leaf selects. +The **Routing** page edits **client subscription** layout only: Mihomo/Clash `proxy-groups` and `rules` stored in the `visual-config` fragment. -**Server-side split is disabled:** the panel Mihomo process always uses `MATCH,DIRECT` (inbound panel). Visual rules/groups/proxies are **not** merged into the serving `config.yaml`. +- Community templates (YiXuanZX / echs-top / AIsouler) fill groups + rules for the **client** YAML. +- After **Save**, update the subscription in the client (Mihomo/Clash YAML target). +- The **panel Mihomo process** always uses `MATCH,DIRECT` (inbound panel). Visual rules/groups/proxies are **not** merged into the serving `config.yaml`. -## Defaults +## Config engine -Fresh install uses: - -```text -MATCH,DIRECT -``` - -That is intentional for a **server inbound** panel: client traffic that reaches this host leaves via DIRECT unless you add rules. - -## Structured rules (UI) - -Each rule has: - -| Field | Meaning | -|-------|---------| -| Type | e.g. `DOMAIN`, `DOMAIN-SUFFIX`, `GEOIP`, `GEOSITE`, `IP-CIDR`, `MATCH`, … | -| Payload | Match value (empty for `MATCH`) | -| Target | `DIRECT`, `REJECT`, a **proxy-group** name, or an outbound **proxy** name | -| no-resolve | Optional on `IP-CIDR` / `IP-CIDR6` / `GEOIP` | - -Rules can be reordered. Advanced lines that the UI cannot parse are kept as **raw** and saved unchanged. - -**Save** writes the database. Rules take effect only after **Generate & apply** (same path as node config). - -### Validation - -- Type, target required; payload required except `MATCH` -- Warning if the last rule is not `MATCH` - -### Templates - -| Template | Result | -|----------|--------| -| DIRECT only | `MATCH,DIRECT` | -| CN direct | `GEOIP,CN,DIRECT` + `MATCH,` | -| Sample ads | Several `DOMAIN-*` → `REJECT` + `MATCH,DIRECT` | -| Via group | `MATCH,` | - -### WARP inject - -**Routing → WARP** calls `POST /api/v1/config/routing/inject-warp`: - -1. Registers Cloudflare WARP -2. Merges the outbound into `visual-config` proxies -3. Optional rule update: none / final `MATCH,` / `GEOIP,CN,DIRECT` + `MATCH,` - -Then generate & apply. Requires the panel host to reach Cloudflare. - -## Proxy-groups - -Simple groups: `select` / `url-test` / `fallback` / `load-balance`, member list, optional URL/interval for url-test. - -## APIs - -```http -GET/PUT /api/v1/config/rules -GET/PUT /api/v1/config/groups -POST /api/v1/config/routing/inject-warp -``` - -```json -{ "mode": "wireguard", "rule_mode": "match", "name": "WARP-OUT" } -``` - -`rule_mode`: `none` | `match` | `cn_direct`. - -Also related: `GET/POST /api/v1/config/proxies`, visual config under **Config**. - -## Related - -- [Installation](installation.md) — frontend stack -- [Cluster](cluster.md) — multi-panel (separate from Mihomo rules) - - -## Community rule templates - -Routing → **Templates** includes adaptations of public Mihomo rule projects (for server visual-config; not a full client overwrite script): - -| Template | Upstream | Intent | -|----------|----------|--------| -| YiXuanZX/rules | [YiXuanZX/rules](https://github.com/YiXuanZX/rules) | Groups: 香港/新加坡/日本/美国/其他 + **代理 / AI / TG**. Rules: CN direct, OpenAI→AI, Telegram→TG, GFW→代理 | -| echs-top/proxy | [echs-top/proxy](https://github.com/echs-top/proxy) | Groups: **代理连接 / TELEGRAM / 国外AI / GOOGLE / 海外媒体 / …**. Ads REJECT + CN direct + category rules | -| AIsouler/MyClash | [AIsouler/MyClash](https://github.com/AIsouler/MyClash) | Groups: **直连 / AdBlock / Google / AI / Telegram / Steam / 默认代理 / 漏网之鱼**. Lite GEOSITE map | - -Applying a community template **writes matching proxy-groups** (merged by name) and rules. Leaf members use existing visual outbounds if any, else `DIRECT` — put real nodes/WARP into the region or policy groups afterward. - -These use **GEOSITE/GEOIP** against MetaCubeX geodata (Settings → Geo). Full upstream `rule-providers` / region filters remain optional via custom YAML fragments. - - -## Server WARP exit (user → node → WARP IP) - -1. Routing page → **Inject WARP** (optional rule: all MATCH WARP, or CN DIRECT + MATCH WARP). -2. **Save** then **Generate & apply**. -3. Panel Mihomo loads the WireGuard outbound and sets `MATCH,` (community templates stay client-only). -4. User traffic through listeners exits with a Cloudflare WARP IP. - -Remove WARP proxies from visual-config (or clear routing proxies) and re-apply to return to `MATCH,DIRECT`. +**Generate & apply** rebuilds listener config for the server core. It does not apply client community rules to the host process. diff --git a/frontend/src/api/routing.ts b/frontend/src/api/routing.ts index a3707ea..152e5fa 100644 --- a/frontend/src/api/routing.ts +++ b/frontend/src/api/routing.ts @@ -16,20 +16,3 @@ export const fetchRules = () => withNetworkRetry(() => client.get('/config/rules').then((r) => r.data)); export const saveRules = (rules: string[]) => withNetworkRetry(() => client.put('/config/rules', rules).then((r) => r.data)); - -export const injectWarpRouting = (payload: { - mode?: 'wireguard' | 'masque'; - rule_mode?: 'none' | 'match' | 'cn_direct'; - name?: string; -}) => - withNetworkRetry(() => - client - .post<{ - status: string; - name: string; - rules: string[]; - proxies: unknown[]; - groups: GroupEntry[]; - }>('/config/routing/inject-warp', payload) - .then((r) => r.data), - ); diff --git a/frontend/src/api/system.ts b/frontend/src/api/system.ts index c51f4ea..a3588d9 100644 --- a/frontend/src/api/system.ts +++ b/frontend/src/api/system.ts @@ -62,10 +62,6 @@ export const restoreDatabase = (file: File) => { export const openApiUrl = '/api/v1/openapi.yaml'; -export const registerWarp = () => - client.post<{ private_key: string; public_key: string; address: string; reserved?: string; yaml: string }>( - '/system/templates/warp/register', - ).then((r) => r.data); export interface LocalBackupItem { diff --git a/frontend/src/i18n/locales/ar.ts b/frontend/src/i18n/locales/ar.ts index 2beb97f..1f16c1d 100644 --- a/frontend/src/i18n/locales/ar.ts +++ b/frontend/src/i18n/locales/ar.ts @@ -724,17 +724,6 @@ export default { "rawRule": "Advanced / unparsed rule (saved as-is)", "moveDown": "Move down", "moveUp": "Move up", - "warpNote": "Panel needs outbound access to Cloudflare.", - "warpInjected": "WARP outbound added", - "warpRuleCn": "GEOIP CN DIRECT + MATCH outbound", - "warpRuleMatch": "Final MATCH → this outbound", - "warpRuleNone": "Do not change rules", - "warpRuleMode": "Rule update", - "warpName": "Outbound name", - "warpMode": "Mode", - "warpInjectHint": "Registers Cloudflare WARP into visual-config. The outbound is exported in Mihomo/Clash client subscriptions (not applied as server-side split). Then update the subscription in the client.", - "warpInjectTitle": "Inject Cloudflare WARP", - "warpInject": "WARP", "tplViaGroup": "MATCH → first group / PROXY", "tplCommunityHint": "Update GeoIP/GeoSite under Settings if GEOSITE rules fail", "tplAisouler": "Community: AIsouler/MyClash lite", @@ -870,12 +859,6 @@ export default { "restoreRestart": "أعد التشغيل للتطبيق", "restoreRestartHint": "Restart the 3m-ui service after restoring the database", "security": "الأمان", - "warp": "Cloudflare WARP", - "warpHint": "تسجيل WARP WireGuard بنقرة واحدة (مقطع YAML لـ outbound Mihomo)", - "warpRegister": "تسجيل WARP", - "warpWireguard": "WireGuard", - "warpMasque": "MASQUE", - "warpDone": "تم تسجيل WARP — تم نسخ YAML", "totp": "المصادقة الثنائية (TOTP)", "totpOn": "مفعّل", "totpSetup": "امسح بتطبيق المصادقة", @@ -889,8 +872,6 @@ export default { "totpSetupSteps": "1) امسح QR 2) أو أدخل السر 3) أكّد برمز من 6 أرقام", "totpSecret": "المفتاح السري", "totpEnabledOk": "تم تفعيل المصادقة الثنائية", - "warpDoneNoCopy": "WARP registered. Copy the YAML manually if clipboard is unavailable.", - "warpEmpty": "No WARP configuration returned.", "localBackups": "On-disk backups", "backupKeep": "Keep last", "backupOlderDays": "Older than (days)", diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index ab7ef76..7a7fd69 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -724,17 +724,6 @@ export default { "rawRule": "Advanced / unparsed rule (saved as-is)", "moveDown": "Move down", "moveUp": "Move up", - "warpNote": "Panel needs outbound access to Cloudflare.", - "warpInjected": "WARP outbound added", - "warpRuleCn": "GEOIP CN DIRECT + MATCH outbound", - "warpRuleMatch": "Final MATCH → this outbound", - "warpRuleNone": "Do not change rules", - "warpRuleMode": "Rule update", - "warpName": "Outbound name", - "warpMode": "Mode", - "warpInjectHint": "Registers Cloudflare WARP into visual-config. The outbound is exported in Mihomo/Clash client subscriptions (not applied as server-side split). Then update the subscription in the client.", - "warpInjectTitle": "Inject Cloudflare WARP", - "warpInject": "WARP", "tplViaGroup": "MATCH → first group / PROXY", "tplCommunityHint": "Update GeoIP/GeoSite under Settings if GEOSITE rules fail", "tplAisouler": "Community: AIsouler/MyClash lite", @@ -870,12 +859,6 @@ export default { "restoreRestart": "Neu starten zum Anwenden", "restoreRestartHint": "Restart the 3m-ui service after restoring the database", "security": "Sicherheit", - "warp": "Cloudflare WARP", - "warpHint": "Ein-Klick WARP-WireGuard-Config (YAML-Fragment für Mihomo-Outbound)", - "warpRegister": "WARP registrieren", - "warpWireguard": "WireGuard", - "warpMasque": "MASQUE", - "warpDone": "WARP registriert — YAML in Zwischenablage", "totp": "Zwei-Faktor (TOTP)", "totpOn": "Aktiv", "totpSetup": "Mit Authenticator scannen", @@ -889,8 +872,6 @@ export default { "totpSetupSteps": "1) QR scannen 2) Oder Geheimnis manuell 3) Mit 6-stelligem Code bestätigen", "totpSecret": "Geheimschlüssel", "totpEnabledOk": "Zwei-Faktor-Authentifizierung aktiviert", - "warpDoneNoCopy": "WARP registered. Copy the YAML manually if clipboard is unavailable.", - "warpEmpty": "No WARP configuration returned.", "localBackups": "On-disk backups", "backupKeep": "Keep last", "backupOlderDays": "Older than (days)", diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 75a0bd0..c014abe 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -724,17 +724,6 @@ export default { "rawRule": "Advanced / unparsed rule (saved as-is)", "moveDown": "Move down", "moveUp": "Move up", - "warpNote": "Panel needs outbound access to Cloudflare.", - "warpInjected": "WARP outbound added", - "warpRuleCn": "GEOIP CN DIRECT + MATCH outbound", - "warpRuleMatch": "Final MATCH → this outbound", - "warpRuleNone": "Do not change rules", - "warpRuleMode": "Rule update", - "warpName": "Outbound name", - "warpMode": "Mode", - "warpInjectHint": "Register WARP into visual-config. After Generate & apply, listener traffic exits via WARP (server). The same outbound is also exported in client subscriptions.", - "warpInjectTitle": "Inject Cloudflare WARP", - "warpInject": "WARP", "tplViaGroup": "MATCH → first group / PROXY", "tplCommunityHint": "Update GeoIP/GeoSite under Settings if GEOSITE rules fail", "tplAisouler": "Community: AIsouler/MyClash lite", @@ -749,11 +738,11 @@ export default { "ruleInvalid": "Invalid rules", "applyDone": "Configuration applied", "applyNow": "Generate & apply", - "applyPromptBody": "Saved. Generate & apply reloads the panel core: WARP exit applies on the server if injected; client groups need a subscription update.", + "applyPromptBody": "Saved for client subscriptions. Generate & apply refreshes the panel core (server stays MATCH,DIRECT). Update the subscription in your client for new groups.", "applyPromptTitle": "Apply configuration?", "groupAdded": "Group added", "addRule": "Add rule", - "pageHint": "Client groups/rules go into subscriptions. WARP inject also becomes the server exit (user → node → WARP IP) after Generate & apply. Server does not use community rule templates.", + "pageHint": "Proxy-groups and rules are for client Mihomo/Clash subscriptions only. Save, then update the subscription in the client. Panel server always uses MATCH,DIRECT.", "rulesSaved": "Rules saved", "groupSaved": "Group saved", "groupDeleted": "Group deleted" @@ -870,12 +859,6 @@ export default { "restoreRestart": "Restart to apply", "restoreRestartHint": "Restart the 3m-ui service after restoring the database", "security": "Security", - "warp": "Cloudflare WARP", - "warpHint": "One-click register a WARP WireGuard config (YAML fragment for Mihomo outbound). Requires outbound HTTPS to Cloudflare.", - "warpRegister": "Register WARP", - "warpWireguard": "WireGuard", - "warpMasque": "MASQUE", - "warpDone": "WARP registered — YAML copied to clipboard", "totp": "Two-factor (TOTP)", "totpOn": "Enabled", "totpSetup": "Scan with authenticator", @@ -889,8 +872,6 @@ export default { "totpSetupSteps": "1) Scan the QR code 2) Or enter the secret manually 3) Confirm with a 6-digit code", "totpSecret": "Secret key", "totpEnabledOk": "Two-factor authentication enabled", - "warpDoneNoCopy": "WARP registered. Copy the YAML manually if clipboard is unavailable.", - "warpEmpty": "No WARP configuration returned.", "localBackups": "On-disk backups", "backupKeep": "Keep last", "backupOlderDays": "Older than (days)", diff --git a/frontend/src/i18n/locales/es.ts b/frontend/src/i18n/locales/es.ts index 608b2e3..0e85682 100644 --- a/frontend/src/i18n/locales/es.ts +++ b/frontend/src/i18n/locales/es.ts @@ -724,17 +724,6 @@ export default { "rawRule": "Advanced / unparsed rule (saved as-is)", "moveDown": "Move down", "moveUp": "Move up", - "warpNote": "Panel needs outbound access to Cloudflare.", - "warpInjected": "WARP outbound added", - "warpRuleCn": "GEOIP CN DIRECT + MATCH outbound", - "warpRuleMatch": "Final MATCH → this outbound", - "warpRuleNone": "Do not change rules", - "warpRuleMode": "Rule update", - "warpName": "Outbound name", - "warpMode": "Mode", - "warpInjectHint": "Registers Cloudflare WARP into visual-config. The outbound is exported in Mihomo/Clash client subscriptions (not applied as server-side split). Then update the subscription in the client.", - "warpInjectTitle": "Inject Cloudflare WARP", - "warpInject": "WARP", "tplViaGroup": "MATCH → first group / PROXY", "tplCommunityHint": "Update GeoIP/GeoSite under Settings if GEOSITE rules fail", "tplAisouler": "Community: AIsouler/MyClash lite", @@ -870,12 +859,6 @@ export default { "restoreRestart": "Reiniciar para aplicar", "restoreRestartHint": "Restart the 3m-ui service after restoring the database", "security": "Seguridad", - "warp": "Cloudflare WARP", - "warpHint": "Registro WARP WireGuard en un clic (fragmento YAML para outbound Mihomo)", - "warpRegister": "Registrar WARP", - "warpWireguard": "WireGuard", - "warpMasque": "MASQUE", - "warpDone": "WARP registrado — YAML copiado", "totp": "Dos factores (TOTP)", "totpOn": "Activado", "totpSetup": "Escanear con el autenticador", @@ -889,8 +872,6 @@ export default { "totpSetupSteps": "1) Escanea el QR 2) O introduce el secreto 3) Confirma con un código de 6 dígitos", "totpSecret": "Clave secreta", "totpEnabledOk": "Autenticación en dos factores activada", - "warpDoneNoCopy": "WARP registered. Copy the YAML manually if clipboard is unavailable.", - "warpEmpty": "No WARP configuration returned.", "localBackups": "On-disk backups", "backupKeep": "Keep last", "backupOlderDays": "Older than (days)", diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index d8d9b0f..d1d086d 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -724,17 +724,6 @@ export default { "rawRule": "Advanced / unparsed rule (saved as-is)", "moveDown": "Move down", "moveUp": "Move up", - "warpNote": "Panel needs outbound access to Cloudflare.", - "warpInjected": "WARP outbound added", - "warpRuleCn": "GEOIP CN DIRECT + MATCH outbound", - "warpRuleMatch": "Final MATCH → this outbound", - "warpRuleNone": "Do not change rules", - "warpRuleMode": "Rule update", - "warpName": "Outbound name", - "warpMode": "Mode", - "warpInjectHint": "Registers Cloudflare WARP into visual-config. The outbound is exported in Mihomo/Clash client subscriptions (not applied as server-side split). Then update the subscription in the client.", - "warpInjectTitle": "Inject Cloudflare WARP", - "warpInject": "WARP", "tplViaGroup": "MATCH → first group / PROXY", "tplCommunityHint": "Update GeoIP/GeoSite under Settings if GEOSITE rules fail", "tplAisouler": "Community: AIsouler/MyClash lite", @@ -870,12 +859,6 @@ export default { "restoreRestart": "Redémarrer pour appliquer", "restoreRestartHint": "Restart the 3m-ui service after restoring the database", "security": "Sécurité", - "warp": "Cloudflare WARP", - "warpHint": "Enregistrement WARP WireGuard en un clic (fragment YAML pour outbound Mihomo)", - "warpRegister": "Enregistrer WARP", - "warpWireguard": "WireGuard", - "warpMasque": "MASQUE", - "warpDone": "WARP enregistré — YAML copié", "totp": "Double authentification (TOTP)", "totpOn": "Activé", "totpSetup": "Scanner avec l’authenticator", @@ -889,8 +872,6 @@ export default { "totpSetupSteps": "1) Scanner le QR 2) Ou saisir le secret 3) Confirmer avec un code à 6 chiffres", "totpSecret": "Clé secrète", "totpEnabledOk": "Double authentification activée", - "warpDoneNoCopy": "WARP registered. Copy the YAML manually if clipboard is unavailable.", - "warpEmpty": "No WARP configuration returned.", "localBackups": "On-disk backups", "backupKeep": "Keep last", "backupOlderDays": "Older than (days)", diff --git a/frontend/src/i18n/locales/hi.ts b/frontend/src/i18n/locales/hi.ts index 034ac7d..0f17388 100644 --- a/frontend/src/i18n/locales/hi.ts +++ b/frontend/src/i18n/locales/hi.ts @@ -724,17 +724,6 @@ export default { "rawRule": "Advanced / unparsed rule (saved as-is)", "moveDown": "Move down", "moveUp": "Move up", - "warpNote": "Panel needs outbound access to Cloudflare.", - "warpInjected": "WARP outbound added", - "warpRuleCn": "GEOIP CN DIRECT + MATCH outbound", - "warpRuleMatch": "Final MATCH → this outbound", - "warpRuleNone": "Do not change rules", - "warpRuleMode": "Rule update", - "warpName": "Outbound name", - "warpMode": "Mode", - "warpInjectHint": "Registers Cloudflare WARP into visual-config. The outbound is exported in Mihomo/Clash client subscriptions (not applied as server-side split). Then update the subscription in the client.", - "warpInjectTitle": "Inject Cloudflare WARP", - "warpInject": "WARP", "tplViaGroup": "MATCH → first group / PROXY", "tplCommunityHint": "Update GeoIP/GeoSite under Settings if GEOSITE rules fail", "tplAisouler": "Community: AIsouler/MyClash lite", @@ -870,12 +859,6 @@ export default { "restoreRestart": "लागू करने के लिए पुनः आरंभ", "restoreRestartHint": "Restart the 3m-ui service after restoring the database", "security": "सुरक्षा", - "warp": "Cloudflare WARP", - "warpHint": "एक-क्लिक WARP WireGuard पंजीकरण (Mihomo outbound के लिए YAML अंश)", - "warpRegister": "WARP पंजीकृत करें", - "warpWireguard": "WireGuard", - "warpMasque": "MASQUE", - "warpDone": "WARP पंजीकृत — YAML कॉपी हुआ", "totp": "दो-चरण (TOTP)", "totpOn": "सक्रिय", "totpSetup": "प्रमाणक से स्कैन करें", @@ -889,8 +872,6 @@ export default { "totpSetupSteps": "1) QR स्कैन 2) या सीक्रेट दर्ज करें 3) 6 अंकों के कोड से पुष्टि", "totpSecret": "गुप्त कुंजी", "totpEnabledOk": "दो-चरण प्रमाणीकरण सक्रिय", - "warpDoneNoCopy": "WARP registered. Copy the YAML manually if clipboard is unavailable.", - "warpEmpty": "No WARP configuration returned.", "localBackups": "On-disk backups", "backupKeep": "Keep last", "backupOlderDays": "Older than (days)", diff --git a/frontend/src/i18n/locales/id.ts b/frontend/src/i18n/locales/id.ts index 0346fd5..4c7f9a4 100644 --- a/frontend/src/i18n/locales/id.ts +++ b/frontend/src/i18n/locales/id.ts @@ -724,17 +724,6 @@ export default { "rawRule": "Advanced / unparsed rule (saved as-is)", "moveDown": "Move down", "moveUp": "Move up", - "warpNote": "Panel needs outbound access to Cloudflare.", - "warpInjected": "WARP outbound added", - "warpRuleCn": "GEOIP CN DIRECT + MATCH outbound", - "warpRuleMatch": "Final MATCH → this outbound", - "warpRuleNone": "Do not change rules", - "warpRuleMode": "Rule update", - "warpName": "Outbound name", - "warpMode": "Mode", - "warpInjectHint": "Registers Cloudflare WARP into visual-config. The outbound is exported in Mihomo/Clash client subscriptions (not applied as server-side split). Then update the subscription in the client.", - "warpInjectTitle": "Inject Cloudflare WARP", - "warpInject": "WARP", "tplViaGroup": "MATCH → first group / PROXY", "tplCommunityHint": "Update GeoIP/GeoSite under Settings if GEOSITE rules fail", "tplAisouler": "Community: AIsouler/MyClash lite", @@ -870,12 +859,6 @@ export default { "restoreRestart": "Restart untuk menerapkan", "restoreRestartHint": "Restart the 3m-ui service after restoring the database", "security": "Keamanan", - "warp": "Cloudflare WARP", - "warpHint": "Daftar WARP WireGuard sekali klik (fragmen YAML untuk outbound Mihomo)", - "warpRegister": "Daftarkan WARP", - "warpWireguard": "WireGuard", - "warpMasque": "MASQUE", - "warpDone": "WARP terdaftar — YAML disalin", "totp": "Dua faktor (TOTP)", "totpOn": "Aktif", "totpSetup": "Pindai dengan authenticator", @@ -889,8 +872,6 @@ export default { "totpSetupSteps": "1) Pindai QR 2) Atau masukkan secret 3) Konfirmasi dengan kode 6 digit", "totpSecret": "Kunci rahasia", "totpEnabledOk": "Autentikasi dua faktor diaktifkan", - "warpDoneNoCopy": "WARP registered. Copy the YAML manually if clipboard is unavailable.", - "warpEmpty": "No WARP configuration returned.", "localBackups": "On-disk backups", "backupKeep": "Keep last", "backupOlderDays": "Older than (days)", diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index bd79c6f..ab322e2 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -724,17 +724,6 @@ export default { "rawRule": "Advanced / unparsed rule (saved as-is)", "moveDown": "Move down", "moveUp": "Move up", - "warpNote": "Panel needs outbound access to Cloudflare.", - "warpInjected": "WARP outbound added", - "warpRuleCn": "GEOIP CN DIRECT + MATCH outbound", - "warpRuleMatch": "Final MATCH → this outbound", - "warpRuleNone": "Do not change rules", - "warpRuleMode": "Rule update", - "warpName": "Outbound name", - "warpMode": "Mode", - "warpInjectHint": "Registers Cloudflare WARP into visual-config. The outbound is exported in Mihomo/Clash client subscriptions (not applied as server-side split). Then update the subscription in the client.", - "warpInjectTitle": "Inject Cloudflare WARP", - "warpInject": "WARP", "tplViaGroup": "MATCH → first group / PROXY", "tplCommunityHint": "Update GeoIP/GeoSite under Settings if GEOSITE rules fail", "tplAisouler": "Community: AIsouler/MyClash lite", @@ -870,12 +859,6 @@ export default { "restoreRestart": "適用するには再起動", "restoreRestartHint": "Restart the 3m-ui service after restoring the database", "security": "セキュリティ", - "warp": "Cloudflare WARP", - "warpHint": "ワンクリックで WARP WireGuard 設定(Mihomo outbound 用 YAML)", - "warpRegister": "WARP を登録", - "warpWireguard": "WireGuard", - "warpMasque": "MASQUE", - "warpDone": "WARP 登録済み — YAML をコピーしました", "totp": "二要素認証 (TOTP)", "totpOn": "有効", "totpSetup": "認証アプリでスキャン", @@ -889,8 +872,6 @@ export default { "totpSetupSteps": "1) QRをスキャン 2) またはシークレットを手入力 3) 6桁コードで確認", "totpSecret": "シークレット", "totpEnabledOk": "二要素認証が有効になりました", - "warpDoneNoCopy": "WARP registered. Copy the YAML manually if clipboard is unavailable.", - "warpEmpty": "No WARP configuration returned.", "localBackups": "On-disk backups", "backupKeep": "Keep last", "backupOlderDays": "Older than (days)", diff --git a/frontend/src/i18n/locales/ko.ts b/frontend/src/i18n/locales/ko.ts index bb24e08..ec2b4d0 100644 --- a/frontend/src/i18n/locales/ko.ts +++ b/frontend/src/i18n/locales/ko.ts @@ -724,17 +724,6 @@ export default { "rawRule": "Advanced / unparsed rule (saved as-is)", "moveDown": "Move down", "moveUp": "Move up", - "warpNote": "Panel needs outbound access to Cloudflare.", - "warpInjected": "WARP outbound added", - "warpRuleCn": "GEOIP CN DIRECT + MATCH outbound", - "warpRuleMatch": "Final MATCH → this outbound", - "warpRuleNone": "Do not change rules", - "warpRuleMode": "Rule update", - "warpName": "Outbound name", - "warpMode": "Mode", - "warpInjectHint": "Registers Cloudflare WARP into visual-config. The outbound is exported in Mihomo/Clash client subscriptions (not applied as server-side split). Then update the subscription in the client.", - "warpInjectTitle": "Inject Cloudflare WARP", - "warpInject": "WARP", "tplViaGroup": "MATCH → first group / PROXY", "tplCommunityHint": "Update GeoIP/GeoSite under Settings if GEOSITE rules fail", "tplAisouler": "Community: AIsouler/MyClash lite", @@ -870,12 +859,6 @@ export default { "restoreRestart": "적용하려면 재시작", "restoreRestartHint": "Restart the 3m-ui service after restoring the database", "security": "보안", - "warp": "Cloudflare WARP", - "warpHint": "원클릭 WARP WireGuard 설정(Mihomo outbound용 YAML)", - "warpRegister": "WARP 등록", - "warpWireguard": "WireGuard", - "warpMasque": "MASQUE", - "warpDone": "WARP 등록됨 — YAML 복사됨", "totp": "2단계 인증 (TOTP)", "totpOn": "사용 중", "totpSetup": "인증 앱으로 스캔", @@ -889,8 +872,6 @@ export default { "totpSetupSteps": "1) QR 스캔 2) 또는 시크릿 수동 입력 3) 6자리 코드 확인", "totpSecret": "시크릿 키", "totpEnabledOk": "2단계 인증이 켜졌습니다", - "warpDoneNoCopy": "WARP registered. Copy the YAML manually if clipboard is unavailable.", - "warpEmpty": "No WARP configuration returned.", "localBackups": "On-disk backups", "backupKeep": "Keep last", "backupOlderDays": "Older than (days)", diff --git a/frontend/src/i18n/locales/pl.ts b/frontend/src/i18n/locales/pl.ts index 104675b..a3d0d77 100644 --- a/frontend/src/i18n/locales/pl.ts +++ b/frontend/src/i18n/locales/pl.ts @@ -724,17 +724,6 @@ export default { "rawRule": "Advanced / unparsed rule (saved as-is)", "moveDown": "Move down", "moveUp": "Move up", - "warpNote": "Panel needs outbound access to Cloudflare.", - "warpInjected": "WARP outbound added", - "warpRuleCn": "GEOIP CN DIRECT + MATCH outbound", - "warpRuleMatch": "Final MATCH → this outbound", - "warpRuleNone": "Do not change rules", - "warpRuleMode": "Rule update", - "warpName": "Outbound name", - "warpMode": "Mode", - "warpInjectHint": "Registers Cloudflare WARP into visual-config. The outbound is exported in Mihomo/Clash client subscriptions (not applied as server-side split). Then update the subscription in the client.", - "warpInjectTitle": "Inject Cloudflare WARP", - "warpInject": "WARP", "tplViaGroup": "MATCH → first group / PROXY", "tplCommunityHint": "Update GeoIP/GeoSite under Settings if GEOSITE rules fail", "tplAisouler": "Community: AIsouler/MyClash lite", @@ -870,12 +859,6 @@ export default { "restoreRestart": "Zrestartuj, aby zastosować", "restoreRestartHint": "Restart the 3m-ui service after restoring the database", "security": "Bezpieczeństwo", - "warp": "Cloudflare WARP", - "warpHint": "Rejestracja WARP WireGuard jednym kliknięciem (fragment YAML dla outbound Mihomo)", - "warpRegister": "Zarejestruj WARP", - "warpWireguard": "WireGuard", - "warpMasque": "MASQUE", - "warpDone": "WARP zarejestrowany — YAML skopiowany", "totp": "Dwuskładnikowe (TOTP)", "totpOn": "Włączone", "totpSetup": "Zeskanuj authenticatorem", @@ -889,8 +872,6 @@ export default { "totpSetupSteps": "1) Zeskanuj QR 2) Lub wpisz sekret 3) Potwierdź 6-cyfrowym kodem", "totpSecret": "Klucz tajny", "totpEnabledOk": "Włączono uwierzytelnianie dwuskładnikowe", - "warpDoneNoCopy": "WARP registered. Copy the YAML manually if clipboard is unavailable.", - "warpEmpty": "No WARP configuration returned.", "localBackups": "On-disk backups", "backupKeep": "Keep last", "backupOlderDays": "Older than (days)", diff --git a/frontend/src/i18n/locales/pt-BR.ts b/frontend/src/i18n/locales/pt-BR.ts index e11e7c1..cd51b58 100644 --- a/frontend/src/i18n/locales/pt-BR.ts +++ b/frontend/src/i18n/locales/pt-BR.ts @@ -724,17 +724,6 @@ export default { "rawRule": "Advanced / unparsed rule (saved as-is)", "moveDown": "Move down", "moveUp": "Move up", - "warpNote": "Panel needs outbound access to Cloudflare.", - "warpInjected": "WARP outbound added", - "warpRuleCn": "GEOIP CN DIRECT + MATCH outbound", - "warpRuleMatch": "Final MATCH → this outbound", - "warpRuleNone": "Do not change rules", - "warpRuleMode": "Rule update", - "warpName": "Outbound name", - "warpMode": "Mode", - "warpInjectHint": "Registers Cloudflare WARP into visual-config. The outbound is exported in Mihomo/Clash client subscriptions (not applied as server-side split). Then update the subscription in the client.", - "warpInjectTitle": "Inject Cloudflare WARP", - "warpInject": "WARP", "tplViaGroup": "MATCH → first group / PROXY", "tplCommunityHint": "Update GeoIP/GeoSite under Settings if GEOSITE rules fail", "tplAisouler": "Community: AIsouler/MyClash lite", @@ -870,12 +859,6 @@ export default { "restoreRestart": "Reiniciar para aplicar", "restoreRestartHint": "Restart the 3m-ui service after restoring the database", "security": "Segurança", - "warp": "Cloudflare WARP", - "warpHint": "Registro WARP WireGuard com um clique (fragmento YAML para outbound Mihomo)", - "warpRegister": "Registrar WARP", - "warpWireguard": "WireGuard", - "warpMasque": "MASQUE", - "warpDone": "WARP registrado — YAML copiado", "totp": "Dois fatores (TOTP)", "totpOn": "Ativado", "totpSetup": "Escanear com o autenticador", @@ -889,8 +872,6 @@ export default { "totpSetupSteps": "1) Escaneie o QR 2) Ou digite o segredo 3) Confirme com código de 6 dígitos", "totpSecret": "Chave secreta", "totpEnabledOk": "Autenticação em dois fatores ativada", - "warpDoneNoCopy": "WARP registered. Copy the YAML manually if clipboard is unavailable.", - "warpEmpty": "No WARP configuration returned.", "localBackups": "On-disk backups", "backupKeep": "Keep last", "backupOlderDays": "Older than (days)", diff --git a/frontend/src/i18n/locales/ru.ts b/frontend/src/i18n/locales/ru.ts index 73f0cd4..2fd4fe2 100644 --- a/frontend/src/i18n/locales/ru.ts +++ b/frontend/src/i18n/locales/ru.ts @@ -724,17 +724,6 @@ export default { "rawRule": "Advanced / unparsed rule (saved as-is)", "moveDown": "Move down", "moveUp": "Move up", - "warpNote": "Panel needs outbound access to Cloudflare.", - "warpInjected": "WARP outbound added", - "warpRuleCn": "GEOIP CN DIRECT + MATCH outbound", - "warpRuleMatch": "Final MATCH → this outbound", - "warpRuleNone": "Do not change rules", - "warpRuleMode": "Rule update", - "warpName": "Outbound name", - "warpMode": "Mode", - "warpInjectHint": "Registers Cloudflare WARP into visual-config. The outbound is exported in Mihomo/Clash client subscriptions (not applied as server-side split). Then update the subscription in the client.", - "warpInjectTitle": "Inject Cloudflare WARP", - "warpInject": "WARP", "tplViaGroup": "MATCH → first group / PROXY", "tplCommunityHint": "Update GeoIP/GeoSite under Settings if GEOSITE rules fail", "tplAisouler": "Community: AIsouler/MyClash lite", @@ -870,12 +859,6 @@ export default { "restoreRestart": "Перезапустить для применения", "restoreRestartHint": "Restart the 3m-ui service after restoring the database", "security": "Безопасность", - "warp": "Cloudflare WARP", - "warpHint": "Однокнопочная регистрация WARP WireGuard (фрагмент YAML для outbound Mihomo)", - "warpRegister": "Зарегистрировать WARP", - "warpWireguard": "WireGuard", - "warpMasque": "MASQUE", - "warpDone": "WARP зарегистрирован — YAML скопирован", "totp": "Двухфакторная (TOTP)", "totpOn": "Включено", "totpSetup": "Сканируйте аутентификатором", @@ -889,8 +872,6 @@ export default { "totpSetupSteps": "1) Сканируйте QR 2) Или введите секрет 3) Подтвердите 6-значным кодом", "totpSecret": "Секретный ключ", "totpEnabledOk": "Двухфакторная аутентификация включена", - "warpDoneNoCopy": "WARP registered. Copy the YAML manually if clipboard is unavailable.", - "warpEmpty": "No WARP configuration returned.", "localBackups": "On-disk backups", "backupKeep": "Keep last", "backupOlderDays": "Older than (days)", diff --git a/frontend/src/i18n/locales/th.ts b/frontend/src/i18n/locales/th.ts index a8c68ea..ce5a842 100644 --- a/frontend/src/i18n/locales/th.ts +++ b/frontend/src/i18n/locales/th.ts @@ -724,17 +724,6 @@ export default { "rawRule": "Advanced / unparsed rule (saved as-is)", "moveDown": "Move down", "moveUp": "Move up", - "warpNote": "Panel needs outbound access to Cloudflare.", - "warpInjected": "WARP outbound added", - "warpRuleCn": "GEOIP CN DIRECT + MATCH outbound", - "warpRuleMatch": "Final MATCH → this outbound", - "warpRuleNone": "Do not change rules", - "warpRuleMode": "Rule update", - "warpName": "Outbound name", - "warpMode": "Mode", - "warpInjectHint": "Registers Cloudflare WARP into visual-config. The outbound is exported in Mihomo/Clash client subscriptions (not applied as server-side split). Then update the subscription in the client.", - "warpInjectTitle": "Inject Cloudflare WARP", - "warpInject": "WARP", "tplViaGroup": "MATCH → first group / PROXY", "tplCommunityHint": "Update GeoIP/GeoSite under Settings if GEOSITE rules fail", "tplAisouler": "Community: AIsouler/MyClash lite", @@ -870,12 +859,6 @@ export default { "restoreRestart": "รีสตาร์ทเพื่อใช้", "restoreRestartHint": "Restart the 3m-ui service after restoring the database", "security": "ความปลอดภัย", - "warp": "Cloudflare WARP", - "warpHint": "ลงทะเบียน WARP WireGuard คลิกเดียว (ชิ้นส่วน YAML สำหรับ outbound Mihomo)", - "warpRegister": "ลงทะเบียน WARP", - "warpWireguard": "WireGuard", - "warpMasque": "MASQUE", - "warpDone": "ลงทะเบียน WARP แล้ว — คัดลอก YAML แล้ว", "totp": "สองขั้นตอน (TOTP)", "totpOn": "เปิดใช้", "totpSetup": "สแกนด้วยแอปยืนยันตัวตน", @@ -889,8 +872,6 @@ export default { "totpSetupSteps": "1) สแกน QR 2) หรือใส่รหัสลับ 3) ยืนยันด้วยรหัส 6 หลัก", "totpSecret": "รหัสลับ", "totpEnabledOk": "เปิดการยืนยันตัวตนสองขั้นตอนแล้ว", - "warpDoneNoCopy": "WARP registered. Copy the YAML manually if clipboard is unavailable.", - "warpEmpty": "No WARP configuration returned.", "localBackups": "On-disk backups", "backupKeep": "Keep last", "backupOlderDays": "Older than (days)", diff --git a/frontend/src/i18n/locales/tr.ts b/frontend/src/i18n/locales/tr.ts index 231ac30..8f5d9b8 100644 --- a/frontend/src/i18n/locales/tr.ts +++ b/frontend/src/i18n/locales/tr.ts @@ -724,17 +724,6 @@ export default { "rawRule": "Advanced / unparsed rule (saved as-is)", "moveDown": "Move down", "moveUp": "Move up", - "warpNote": "Panel needs outbound access to Cloudflare.", - "warpInjected": "WARP outbound added", - "warpRuleCn": "GEOIP CN DIRECT + MATCH outbound", - "warpRuleMatch": "Final MATCH → this outbound", - "warpRuleNone": "Do not change rules", - "warpRuleMode": "Rule update", - "warpName": "Outbound name", - "warpMode": "Mode", - "warpInjectHint": "Registers Cloudflare WARP into visual-config. The outbound is exported in Mihomo/Clash client subscriptions (not applied as server-side split). Then update the subscription in the client.", - "warpInjectTitle": "Inject Cloudflare WARP", - "warpInject": "WARP", "tplViaGroup": "MATCH → first group / PROXY", "tplCommunityHint": "Update GeoIP/GeoSite under Settings if GEOSITE rules fail", "tplAisouler": "Community: AIsouler/MyClash lite", @@ -870,12 +859,6 @@ export default { "restoreRestart": "Uygulamak için yeniden başlat", "restoreRestartHint": "Restart the 3m-ui service after restoring the database", "security": "Güvenlik", - "warp": "Cloudflare WARP", - "warpHint": "Tek tıkla WARP WireGuard kaydı (Mihomo outbound için YAML parçası)", - "warpRegister": "WARP kaydet", - "warpWireguard": "WireGuard", - "warpMasque": "MASQUE", - "warpDone": "WARP kaydedildi — YAML kopyalandı", "totp": "İki adımlı (TOTP)", "totpOn": "Açık", "totpSetup": "Doğrulayıcı ile tarayın", @@ -889,8 +872,6 @@ export default { "totpSetupSteps": "1) QR tarayın 2) Veya gizli anahtarı girin 3) 6 haneli kodla onaylayın", "totpSecret": "Gizli anahtar", "totpEnabledOk": "İki adımlı doğrulama etkin", - "warpDoneNoCopy": "WARP registered. Copy the YAML manually if clipboard is unavailable.", - "warpEmpty": "No WARP configuration returned.", "localBackups": "On-disk backups", "backupKeep": "Keep last", "backupOlderDays": "Older than (days)", diff --git a/frontend/src/i18n/locales/uk.ts b/frontend/src/i18n/locales/uk.ts index 3873009..a2de14f 100644 --- a/frontend/src/i18n/locales/uk.ts +++ b/frontend/src/i18n/locales/uk.ts @@ -724,17 +724,6 @@ export default { "rawRule": "Advanced / unparsed rule (saved as-is)", "moveDown": "Move down", "moveUp": "Move up", - "warpNote": "Panel needs outbound access to Cloudflare.", - "warpInjected": "WARP outbound added", - "warpRuleCn": "GEOIP CN DIRECT + MATCH outbound", - "warpRuleMatch": "Final MATCH → this outbound", - "warpRuleNone": "Do not change rules", - "warpRuleMode": "Rule update", - "warpName": "Outbound name", - "warpMode": "Mode", - "warpInjectHint": "Registers Cloudflare WARP into visual-config. The outbound is exported in Mihomo/Clash client subscriptions (not applied as server-side split). Then update the subscription in the client.", - "warpInjectTitle": "Inject Cloudflare WARP", - "warpInject": "WARP", "tplViaGroup": "MATCH → first group / PROXY", "tplCommunityHint": "Update GeoIP/GeoSite under Settings if GEOSITE rules fail", "tplAisouler": "Community: AIsouler/MyClash lite", @@ -870,12 +859,6 @@ export default { "restoreRestart": "Перезапустити для застосування", "restoreRestartHint": "Restart the 3m-ui service after restoring the database", "security": "Безпека", - "warp": "Cloudflare WARP", - "warpHint": "Реєстрація WARP WireGuard в один клік (фрагмент YAML для outbound Mihomo)", - "warpRegister": "Зареєструвати WARP", - "warpWireguard": "WireGuard", - "warpMasque": "MASQUE", - "warpDone": "WARP зареєстровано — YAML скопійовано", "totp": "Двофакторна (TOTP)", "totpOn": "Увімкнено", "totpSetup": "Скануйте автентифікатором", @@ -889,8 +872,6 @@ export default { "totpSetupSteps": "1) Скануйте QR 2) Або введіть секрет 3) Підтвердіть 6-значним кодом", "totpSecret": "Секретний ключ", "totpEnabledOk": "Двофакторну автентифікацію увімкнено", - "warpDoneNoCopy": "WARP registered. Copy the YAML manually if clipboard is unavailable.", - "warpEmpty": "No WARP configuration returned.", "localBackups": "On-disk backups", "backupKeep": "Keep last", "backupOlderDays": "Older than (days)", diff --git a/frontend/src/i18n/locales/vi.ts b/frontend/src/i18n/locales/vi.ts index 0d3fed8..dfdd593 100644 --- a/frontend/src/i18n/locales/vi.ts +++ b/frontend/src/i18n/locales/vi.ts @@ -724,17 +724,6 @@ export default { "rawRule": "Advanced / unparsed rule (saved as-is)", "moveDown": "Move down", "moveUp": "Move up", - "warpNote": "Panel needs outbound access to Cloudflare.", - "warpInjected": "WARP outbound added", - "warpRuleCn": "GEOIP CN DIRECT + MATCH outbound", - "warpRuleMatch": "Final MATCH → this outbound", - "warpRuleNone": "Do not change rules", - "warpRuleMode": "Rule update", - "warpName": "Outbound name", - "warpMode": "Mode", - "warpInjectHint": "Registers Cloudflare WARP into visual-config. The outbound is exported in Mihomo/Clash client subscriptions (not applied as server-side split). Then update the subscription in the client.", - "warpInjectTitle": "Inject Cloudflare WARP", - "warpInject": "WARP", "tplViaGroup": "MATCH → first group / PROXY", "tplCommunityHint": "Update GeoIP/GeoSite under Settings if GEOSITE rules fail", "tplAisouler": "Community: AIsouler/MyClash lite", @@ -870,12 +859,6 @@ export default { "restoreRestart": "Khởi động lại để áp dụng", "restoreRestartHint": "Restart the 3m-ui service after restoring the database", "security": "Bảo mật", - "warp": "Cloudflare WARP", - "warpHint": "Đăng ký WARP WireGuard một cú nhấp (mảnh YAML cho outbound Mihomo)", - "warpRegister": "Đăng ký WARP", - "warpWireguard": "WireGuard", - "warpMasque": "MASQUE", - "warpDone": "Đã đăng ký WARP — YAML đã sao chép", "totp": "Xác thực 2 bước (TOTP)", "totpOn": "Đã bật", "totpSetup": "Quét bằng ứng dụng xác thực", @@ -889,8 +872,6 @@ export default { "totpSetupSteps": "1) Quét QR 2) Hoặc nhập secret 3) Xác nhận bằng mã 6 số", "totpSecret": "Khóa bí mật", "totpEnabledOk": "Đã bật xác thực 2 bước", - "warpDoneNoCopy": "WARP registered. Copy the YAML manually if clipboard is unavailable.", - "warpEmpty": "No WARP configuration returned.", "localBackups": "On-disk backups", "backupKeep": "Keep last", "backupOlderDays": "Older than (days)", diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index b0557af..23e983b 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -724,17 +724,6 @@ export default { "rawRule": "高级/未解析规则(原样保存)", "moveDown": "下移", "moveUp": "上移", - "warpNote": "面板需能访问 Cloudflare。", - "warpInjected": "已添加 WARP 出站", - "warpRuleCn": "GEOIP CN 直连 + MATCH 出站", - "warpRuleMatch": "最终 MATCH → 该出站", - "warpRuleNone": "不改规则", - "warpRuleMode": "规则处理", - "warpName": "出站名称", - "warpMode": "模式", - "warpInjectHint": "将 WARP 写入 visual-config。生成并应用后,入站用户流量经 WARP 出站(服务端)。同一出站也会进入客户端订阅。", - "warpInjectTitle": "注入 Cloudflare WARP 出站", - "warpInject": "注入 WARP", "tplViaGroup": "MATCH → 第一个策略组 / PROXY", "tplCommunityHint": "若 GEOSITE 无效,请在设置中更新 Geo 数据", "tplAisouler": "社区:AIsouler/MyClash 精简思路", @@ -749,11 +738,11 @@ export default { "ruleInvalid": "规则无效", "applyDone": "配置已应用", "applyNow": "生成并应用", - "applyPromptBody": "已保存。生成并应用会重载面板核心:若已注入 WARP,服务端出口为 WARP;客户端策略组需更新订阅。", + "applyPromptBody": "已保存供客户端订阅使用。生成并应用会刷新面板核心(服务端仍为 MATCH,DIRECT)。请在客户端更新订阅以加载新策略组。", "applyPromptTitle": "应用配置?", "groupAdded": "策略组已添加", "addRule": "添加规则", - "pageHint": "客户端策略组/规则写入订阅。注入 WARP 并「生成并应用」后,用户经节点出去的流量会走 WARP IP。社区分流模板不会加载到服务端。", + "pageHint": "策略组与规则仅用于客户端 Mihomo/Clash 订阅。保存后请在客户端更新订阅。面板服务端固定 MATCH,DIRECT。", "rulesSaved": "规则已保存", "groupSaved": "策略组已保存", "groupDeleted": "策略组已删除" @@ -870,12 +859,6 @@ export default { "restoreRestart": "重启以应用", "restoreRestartHint": "恢复数据库后请重启 3m-ui 服务", "security": "安全", - "warp": "Cloudflare WARP", - "warpHint": "一键向 Cloudflare 注册 WARP,生成 Mihomo WireGuard 出站 YAML。需要面板能访问 Cloudflare。", - "warpRegister": "一键注册 WARP", - "warpWireguard": "WireGuard", - "warpMasque": "MASQUE", - "warpDone": "WARP 已注册,YAML 已复制到剪贴板", "totp": "两步验证 (TOTP)", "totpOn": "已启用", "totpSetup": "请用验证器扫描二维码", @@ -889,8 +872,6 @@ export default { "totpSetupSteps": "1)扫描二维码 2)或手动录入密钥 3)输入 6 位验证码确认", "totpSecret": "密钥", "totpEnabledOk": "两步验证已启用", - "warpDoneNoCopy": "WARP 已注册。若无法写入剪贴板,请手动复制 YAML。", - "warpEmpty": "未返回 WARP 配置。", "localBackups": "磁盘备份", "backupKeep": "保留最近", "backupOlderDays": "早于(天)", diff --git a/frontend/src/i18n/locales/zh-TW.ts b/frontend/src/i18n/locales/zh-TW.ts index 5910ea6..384ce17 100644 --- a/frontend/src/i18n/locales/zh-TW.ts +++ b/frontend/src/i18n/locales/zh-TW.ts @@ -724,17 +724,6 @@ export default { "rawRule": "Advanced / unparsed rule (saved as-is)", "moveDown": "Move down", "moveUp": "Move up", - "warpNote": "Panel needs outbound access to Cloudflare.", - "warpInjected": "WARP outbound added", - "warpRuleCn": "GEOIP CN DIRECT + MATCH outbound", - "warpRuleMatch": "Final MATCH → this outbound", - "warpRuleNone": "Do not change rules", - "warpRuleMode": "Rule update", - "warpName": "Outbound name", - "warpMode": "Mode", - "warpInjectHint": "將 Cloudflare WARP 寫入 visual-config。該出站會出現在 Mihomo/Clash 客戶端訂閱中(不會作為服務端分流)。請在客戶端更新訂閱。", - "warpInjectTitle": "Inject Cloudflare WARP", - "warpInject": "WARP", "tplViaGroup": "MATCH → first group / PROXY", "tplCommunityHint": "若 GEOSITE 無效,請在設定中更新 Geo 資料", "tplAisouler": "社群:AIsouler/MyClash 精簡思路", @@ -870,12 +859,6 @@ export default { "restoreRestart": "重新啟動以应用", "restoreRestartHint": "還原数据库后請重新啟動 3m-ui 服务", "security": "安全", - "warp": "Cloudflare WARP", - "warpHint": "一键向 Cloudflare 注册 WARP,產生 Mihomo WireGuard 出站 YAML。需要面板能访问 Cloudflare。", - "warpRegister": "一键注册 WARP", - "warpWireguard": "WireGuard", - "warpMasque": "MASQUE", - "warpDone": "WARP 已注册,YAML 已複製到剪贴板", "totp": "兩步驟驗證 (TOTP)", "totpOn": "已啟用", "totpSetup": "請用驗證器掃描 QR 碼", @@ -889,8 +872,6 @@ export default { "totpSetupSteps": "1)掃描 QR 碼 2)或手動錄入金鑰 3)輸入 6 位驗證碼確認", "totpSecret": "金鑰", "totpEnabledOk": "兩步驟驗證已啟用", - "warpDoneNoCopy": "WARP 已註冊。若無法寫入剪貼簿,請手動複製 YAML。", - "warpEmpty": "未回傳 WARP 設定。", "localBackups": "磁碟備份", "backupKeep": "保留最近", "backupOlderDays": "早於(天)", diff --git a/frontend/src/pages/Routing.tsx b/frontend/src/pages/Routing.tsx index adcd184..6ccbdf3 100644 --- a/frontend/src/pages/Routing.tsx +++ b/frontend/src/pages/Routing.tsx @@ -33,7 +33,6 @@ import { saveGroups, fetchRules, saveRules, - injectWarpRouting, type GroupEntry, } from '../api/routing'; import { generateConfig, applyConfigYAML, fetchProxies } from '../api/config'; @@ -63,10 +62,7 @@ const RoutingPage: React.FC = () => { const [saving, setSaving] = useState(false); const [applying, setApplying] = useState(false); const [groupOpen, setGroupOpen] = useState(false); - const [warpOpen, setWarpOpen] = useState(false); - const [warpBusy, setWarpBusy] = useState(false); const [form] = Form.useForm(); - const [warpForm] = Form.useForm(); const targetOptions = useMemo(() => { const set = new Set(['DIRECT', 'REJECT', 'COMPATIBLE']); @@ -256,33 +252,6 @@ const RoutingPage: React.FC = () => { ); }; - const onInjectWarp = async () => { - const values = await warpForm.validateFields().catch(() => null); - if (!values) return; - setWarpBusy(true); - try { - const res = await injectWarpRouting({ - mode: values.mode || 'wireguard', - rule_mode: values.rule_mode || 'match', - name: values.name || undefined, - }); - setRules(parseRulesText((res.rules || []).join('\n'))); - setProxyNames((prev) => { - const n = res.name || 'WARP-OUT'; - return prev.includes(n) ? prev : [...prev, n]; - }); - message.success( - (t('routing.warpInjected') || 'WARP outbound added') + (res.name ? `: ${res.name}` : ''), - ); - setWarpOpen(false); - offerApply(); - } catch (e: any) { - message.error(errMsg(e)); - } finally { - setWarpBusy(false); - } - }; - const templateMenu = { items: [ { key: 'direct_only', label: t('routing.tplDirect') || 'MATCH → DIRECT only' }, @@ -309,13 +278,6 @@ const RoutingPage: React.FC = () => { const ruleCardExtra = ( - - - @@ -501,7 +463,7 @@ const RoutingPage: React.FC = () => { - - - - - -