package app import ( "encoding/json" "errors" "fmt" "net" "net/netip" "net/url" "regexp" "strings" "time" ) const ProxyGroup = "CW-PROXY" const AutoGroup = "CW-AUTO" type Subscription struct { ID string `json:"id"` Name string `json:"name"` URL string `json:"url"` Enabled bool `json:"enabled"` IntervalHours int `json:"intervalHours"` UpdatedAt string `json:"updatedAt"` AttemptedAt string `json:"attemptedAt"` LastError string `json:"lastError"` Usage string `json:"usage,omitempty"` Nodes []map[string]any `json:"nodes"` } type Rule struct { ID string `json:"id"` Type string `json:"type"` Value string `json:"value"` Action string `json:"action"` Enabled bool `json:"enabled"` } type Settings struct { ProxyEnabled bool `json:"proxyEnabled"` TunEnabled bool `json:"tunEnabled"` SelectionMode string `json:"selectionMode"` SelectedNode string `json:"selectedNode"` LANIP string `json:"lanIP"` LANCIDR string `json:"lanCIDR"` Gateway string `json:"gateway"` Interface string `json:"interface"` MixedPort int `json:"mixedPort"` DNSPort int `json:"dnsPort"` TestURL string `json:"testURL"` TestTimeoutMS int `json:"testTimeoutMS"` } type State struct { Subscriptions []Subscription `json:"subscriptions"` Rules []Rule `json:"rules"` Settings Settings `json:"settings"` } type Backup struct { Version int `json:"version"` ExportedAt string `json:"exportedAt"` State State `json:"state"` } func defaultState() State { return State{Subscriptions: []Subscription{}, Rules: []Rule{}, Settings: Settings{ ProxyEnabled: true, SelectionMode: "manual", MixedPort: 7890, DNSPort: 1053, TestURL: "https://www.gstatic.com/generate_204", TestTimeoutMS: 5000, }} } func cloneState(s State) State { b, _ := json.Marshal(s) var copy State _ = json.Unmarshal(b, ©) return copy } func validHTTPURL(raw string) error { u, err := url.Parse(raw) if err != nil || u.Hostname() == "" || (u.Scheme != "http" && u.Scheme != "https") || u.User != nil || u.Fragment != "" { return errors.New("请输入有效的 HTTP 或 HTTPS 地址(不支持 userinfo 或 URL fragment)") } return nil } func validateSubscription(s Subscription) error { if strings.TrimSpace(s.Name) == "" || len(s.Name) > 160 { return errors.New("订阅名称不能为空,且不能超过 160 字节") } if s.IntervalHours < 1 || s.IntervalHours > 720 { return errors.New("更新间隔应为 1–720 小时") } return validHTTPURL(s.URL) } var interfacePattern = regexp.MustCompile(`^[a-zA-Z0-9_.:-]{1,15}$`) var domainPattern = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?$`) func validateSettings(s Settings) error { if s.SelectionMode != "manual" && s.SelectionMode != "auto" { return errors.New("无效的节点选择模式") } if s.MixedPort < 1024 || s.MixedPort > 65535 || s.DNSPort < 1 || s.DNSPort > 65535 || s.MixedPort == s.DNSPort || s.MixedPort == 9090 || s.DNSPort == 9090 || s.MixedPort == 8080 || s.DNSPort == 8080 { return errors.New("代理端口应为 1024–65535,DNS 端口为 1–65535,且不能与管理端口或控制端口冲突") } if s.TestTimeoutMS < 1000 || s.TestTimeoutMS > 15000 { return errors.New("测速超时应为 1000–15000 毫秒") } if err := validHTTPURL(s.TestURL); err != nil { return err } if s.Interface != "" && !interfacePattern.MatchString(s.Interface) { return errors.New("无效的网卡名称") } for label, raw := range map[string]string{"VM 地址": s.LANIP, "上游网关": s.Gateway} { if raw != "" { ip, err := netip.ParseAddr(raw) if err != nil || !ip.Is4() || !ip.IsPrivate() { return fmt.Errorf("%s应为内网 IPv4 地址", label) } } } if s.LANCIDR != "" { prefix, err := netip.ParsePrefix(s.LANCIDR) if err != nil || !prefix.Addr().Is4() || !prefix.Addr().IsPrivate() || prefix.Bits() < 8 || prefix.Bits() > 30 { return errors.New("请输入有效的内网 IPv4 网段,例如 192.168.1.0/24") } if s.LANIP != "" { ip, _ := netip.ParseAddr(s.LANIP) if !prefix.Contains(ip) { return errors.New("VM 地址不在内网网段内") } } if s.Gateway != "" { ip, _ := netip.ParseAddr(s.Gateway) if !prefix.Contains(ip) { return errors.New("上游网关不在内网网段内") } } } if s.LANIP != "" && s.LANIP == s.Gateway { return errors.New("VM 的上游网关必须指向原路由器,不能指向自己") } if s.TunEnabled && (s.LANIP == "" || s.LANCIDR == "" || s.Gateway == "" || s.Interface == "" || s.DNSPort != 53) { return errors.New("透明代理需要填写 VM 地址、网段、上游网关、网卡,并把 DNS 端口设为 53;先完成 Linux 网络部署步骤") } return nil } func validateRule(r Rule) error { if r.Action != "DIRECT" && r.Action != "PROXY" { return errors.New("规则动作应为直连或代理") } if r.Type == "IP-CIDR" { p, err := netip.ParsePrefix(r.Value) if err != nil || !p.Addr().Is4() { return errors.New("请输入有效的 IPv4 网段") } } else if r.Type == "DOMAIN" || r.Type == "DOMAIN-SUFFIX" { if len(r.Value) > 253 || !domainPattern.MatchString(r.Value) || strings.Contains(r.Value, "..") { return errors.New("请输入域名,不要包含协议、端口或路径") } } else { return errors.New("不支持此规则类型") } return nil } func validateState(s State) error { if len(s.Subscriptions) > 50 || len(s.Rules) > 1000 { return errors.New("最多允许 50 个订阅、1000 条规则") } if err := validateSettings(s.Settings); err != nil { return err } ids := map[string]bool{} for _, sub := range s.Subscriptions { if !validID(sub.ID) || ids[sub.ID] { return errors.New("订阅 ID 无效或重复") } ids[sub.ID] = true if err := validateSubscription(sub); err != nil { return err } if len(sub.Nodes) > 5000 { return errors.New("单个订阅节点过多") } } ids = map[string]bool{} for _, rule := range s.Rules { if !validID(rule.ID) || ids[rule.ID] { return errors.New("规则 ID 无效或重复") } ids[rule.ID] = true if err := validateRule(rule); err != nil { return err } } return nil } func validID(s string) bool { return regexp.MustCompile(`^[a-f0-9]{24}$`).MatchString(s) } func now() string { return time.Now().UTC().Format(time.RFC3339) } func interfaceExists(name string, ip string) bool { i, err := net.InterfaceByName(name) if err != nil { return false } addresses, _ := i.Addrs() for _, a := range addresses { if strings.Split(a.String(), "/")[0] == ip { return true } } return false }