Files

220 lines
6.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, &copy)
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
}