From cfcc696bda7955ad5205dc6875ae95ac63cdd4eb Mon Sep 17 00:00:00 2001 From: DarkiT Date: Thu, 13 Mar 2025 17:53:08 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96DNS=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README_DNS.md | 228 ++++++++++++++++++------------ cmd/wildcard_dns_proxy/main.go | 190 +++++++++++++++++++++++++ examples/wildcard_dns_config.json | 23 +++ examples/wildcard_hosts.txt | 27 ++++ internal/dns/config.go | 13 +- internal/dns/resolver.go | 208 ++++++++++++++++++++++++--- scripts/generate_cert.ps1 | 16 +-- 7 files changed, 579 insertions(+), 126 deletions(-) create mode 100644 cmd/wildcard_dns_proxy/main.go create mode 100644 examples/wildcard_dns_config.json create mode 100644 examples/wildcard_hosts.txt diff --git a/README_DNS.md b/README_DNS.md index 8346da7..5fd4044 100644 --- a/README_DNS.md +++ b/README_DNS.md @@ -5,6 +5,7 @@ - 自定义域名解析 - 动态变更后端服务器IP - 自定义后端服务器端口 +- 泛解析(通配符域名)支持 - 负载均衡和故障转移 - 绕过DNS污染 - 高效的DNS缓存 @@ -13,6 +14,8 @@ - **自定义记录**:直接设置域名到IP的映射 - **自定义端口**:为每个域名指定自定义端口,无需在URL中指定 +- **泛解析**:支持通配符域名(如`*.example.com`)自动匹配多个子域名 +- **多级泛解析**:支持复杂的通配符模式(如`api.*.example.com`) - **备用解析**:在自定义记录未找到时可选择使用系统DNS - **DNS缓存**:缓存解析结果以提高性能 - **自动重试**:解析失败时可配置重试策略 @@ -39,37 +42,37 @@ go build ./... go run cmd/custom_dns_proxy/main.go ``` -#### 参数说明 +### 2. 自定义端口代理 -- `-listen`: 监听地址,默认 `:8080` -- `-target`: 目标主机名,默认 `www.github.com` -- `-port`: 目标端口,默认 `443` -- `-dns`: DNS配置文件(JSON格式) -- `-hosts`: hosts格式配置文件 - -### 2. HTTPS代理(自定义DNS) - -以下命令会启动一个HTTPS代理(监听端口8443),它使用自定义DNS解析: +支持为不同域名指定不同端口的代理: ```bash -# 首先生成自签名证书 -# Linux/MacOS -./scripts/generate_cert.sh +go run cmd/custom_port_proxy/main.go +``` -# Windows -powershell -ExecutionPolicy Bypass -File .\scripts\generate_cert.ps1 +### 3. 泛解析DNS代理 -# 然后启动HTTPS代理 -go run cmd/custom_dns_https_proxy/main.go +支持通配符域名解析的代理: + +```bash +# 使用默认的示例泛解析规则 +go run cmd/wildcard_dns_proxy/main.go + +# 透明代理模式(使用请求中的Host进行匹配) +go run cmd/wildcard_dns_proxy/main.go -target "" + +# 使用自定义配置文件 +go run cmd/wildcard_dns_proxy/main.go -dns examples/wildcard_dns_config.json + +# 使用hosts格式配置文件 +go run cmd/wildcard_dns_proxy/main.go -hosts examples/wildcard_hosts.txt ``` #### 参数说明 -- `-listen`: 监听地址,默认 `:8443` -- `-target`: 目标主机名,默认 `www.github.com` -- `-port`: 目标端口,默认 `443` -- `-cert`: 证书文件,默认 `server.crt` -- `-key`: 私钥文件,默认 `server.key` +- `-listen`: 监听地址,默认 `:8080` +- `-target`: 目标主机名,空字符串表示使用请求中的Host头 +- `-port`: 默认目标端口,默认 `443` - `-dns`: DNS配置文件(JSON格式) - `-hosts`: hosts格式配置文件 @@ -80,12 +83,14 @@ go run cmd/custom_dns_https_proxy/main.go ```json { "records": { - "www.github.com": "140.82.121.3", - "github.com": "140.82.121.4", - "api.github.com": "140.82.121.5", + "example.com": "93.184.216.34", + "api.example.com": "93.184.216.35:8443", - "custom-port-example.com": "192.168.1.10:8080", - "api.dev.example.com": "127.0.0.1:3001" + "*.github.com": "140.82.121.3", + "github.com": "140.82.121.4", + + "*.dev.local": "127.0.0.1:3000", + "api.*.dev.local": "127.0.0.1:3001" }, "use_fallback": true, "ttl": 300 @@ -95,13 +100,14 @@ go run cmd/custom_dns_https_proxy/main.go ### Hosts格式 ``` -# 标准格式:IP 域名 -140.82.121.3 www.github.com -140.82.121.4 github.com +# 精确匹配 +93.184.216.34 example.com +93.184.216.35:8443 api.example.com -# 带端口格式:IP:端口 域名 -192.168.1.10:8080 custom-port-example.com -127.0.0.1:3001 api.dev.example.com +# 泛解析(通配符域名) +140.82.121.3 *.github.com +127.0.0.1:3000 *.dev.local +127.0.0.1:3001 api.*.dev.local ``` ## 编程接口 @@ -120,6 +126,12 @@ resolver.Add("example.com", "93.184.216.34") // 添加带端口的记录 resolver.AddWithPort("api.example.com", "93.184.216.35", 8443) +// 添加泛解析记录(使用默认端口) +resolver.AddWildcard("*.example.com", "93.184.216.36") + +// 添加带端口的泛解析记录 +resolver.AddWildcardWithPort("*.api.example.com", "93.184.216.37", 8444) + // 解析域名(只获取IP) ip, err := resolver.Resolve("example.com") if err != nil { @@ -127,12 +139,19 @@ if err != nil { } fmt.Printf("解析结果IP: %s\n", ip) -// 解析域名(获取IP和端口) -endpoint, err := resolver.ResolveWithPort("api.example.com", 443) // 443为默认端口 +// 测试泛解析功能 +ip, err = resolver.Resolve("sub.example.com") if err != nil { log.Fatalf("解析失败: %v", err) } -fmt.Printf("解析结果: IP=%s, 端口=%d\n", endpoint.IP, endpoint.Port) +fmt.Printf("泛解析结果IP: %s\n", ip) + +// 测试多级泛解析功能 +endpoint, err := resolver.ResolveWithPort("test.api.example.com", 443) +if err != nil { + log.Fatalf("解析失败: %v", err) +} +fmt.Printf("多级泛解析结果: IP=%s, 端口=%d\n", endpoint.IP, endpoint.Port) ``` ### 使用DNS拨号器 @@ -143,17 +162,16 @@ import "github.com/goproxy/internal/dns" // 创建解析器 resolver := dns.NewResolver() resolver.Add("example.com", "93.184.216.34") -resolver.AddWithPort("example-api.com", "93.184.216.35", 8443) +resolver.AddWildcard("*.example.com", "93.184.216.36") // 创建拨号器 dialer := dns.NewDialer(resolver) -// 使用拨号器连接(会自动应用自定义端口) -conn, err := dialer.Dial("tcp", "example-api.com:443") +// 使用拨号器连接(会自动应用泛解析) +conn, err := dialer.Dial("tcp", "sub.example.com:443") if err != nil { log.Fatalf("连接失败: %v", err) } -// 注意:即使这里指定了443端口,实际会连接到8443端口 defer conn.Close() // 或者获取用于http.Transport的拨号上下文函数 @@ -165,29 +183,35 @@ client := &http.Client{Transport: transport} ## 高级用法 -### 负载均衡 +### 泛解析模式 -可以通过多次添加同一域名的不同IP地址来实现简单的负载均衡: +泛解析支持以下模式: -```go -resolver := dns.NewResolver() -resolver.Add("api.example.com", "10.0.0.1") -resolver.Add("api.example.com", "10.0.0.2") -resolver.Add("api.example.com", "10.0.0.3") +1. **单级通配符**:`*.example.com` 匹配 `a.example.com`、`b.example.com` 等 + +2. **多级通配符**:`*.*.example.com` 匹配 `a.b.example.com`、`c.d.example.com` 等 + +3. **中间通配符**:`api.*.example.com` 匹配 `api.v1.example.com`、`api.beta.example.com` 等 + +4. **前缀通配符**:`api-*.example.com` 匹配 `api-v1.example.com`、`api-beta.example.com` 等 + +### 域名匹配优先级 + +当有多条规则可以匹配同一个域名时,解析器会按照以下优先级选择: + +1. 精确匹配(如 `example.com`) +2. 最具体的通配符匹配(如 `*.test.example.com` 比 `*.example.com` 更优先) +3. 后添加的通配符规则优先于先添加的规则 + +### 透明代理模式 + +泛解析代理支持透明模式,这种模式下代理会使用请求中的Host头来进行DNS解析,而不是固定的目标主机: + +```bash +go run cmd/wildcard_dns_proxy/main.go -target "" ``` -解析器将按轮询方式返回这些IP地址。 - -### 带端口的负载均衡 - -可以为不同的后端服务器指定不同的端口: - -```go -resolver := dns.NewResolver() -resolver.AddWithPort("api.example.com", "10.0.0.1", 8080) -resolver.AddWithPort("api.example.com", "10.0.0.2", 8081) -resolver.AddWithPort("api.example.com", "10.0.0.3", 8082) -``` +这样,通过修改请求的Host头或使用不同的域名访问代理,可以自动路由到不同的后端服务器。 ### 自定义DNS后端 @@ -198,7 +222,7 @@ type MyResolver struct { // 你的字段 } -// 实现Resolver接口 +// 实现Resolver接口的所有方法 func (r *MyResolver) Resolve(hostname string) (string, error) { // 自定义逻辑 } @@ -215,6 +239,14 @@ func (r *MyResolver) AddWithPort(hostname, ip string, port int) error { // 自定义逻辑 } +func (r *MyResolver) AddWildcard(wildcardDomain, ip string) error { + // 自定义逻辑 +} + +func (r *MyResolver) AddWildcardWithPort(wildcardDomain, ip string, port int) error { + // 自定义逻辑 +} + func (r *MyResolver) Remove(hostname string) error { // 自定义逻辑 } @@ -226,44 +258,56 @@ func (r *MyResolver) Clear() { ## 应用场景 -### 开发环境映射 - -在本地开发时,可以将多个服务映射到不同的端口: - -``` -127.0.0.1:3000 web.local -127.0.0.1:3001 api.local -127.0.0.1:5432 db.local -``` - -### 微服务路由 - -可以将不同的微服务映射到不同的IP和端口: - -``` -192.168.1.10:8001 auth-service.internal -192.168.1.11:8002 user-service.internal -192.168.1.12:8003 payment-service.internal -``` - ### 多环境测试 -可以创建多个配置文件,针对不同的环境: +使用泛解析可以为不同环境的所有服务配置不同的后端: ``` -# 测试环境 -10.0.0.1:8080 api.example.com -10.0.0.2:8080 web.example.com +# 测试环境的所有服务 +192.168.1.100 *.test.example.com -# 生产环境 -20.0.0.1:443 api.example.com -20.0.0.2:443 web.example.com +# 预发布环境的所有服务 +192.168.1.101 *.staging.example.com + +# 生产环境的所有服务 +192.168.1.102 *.production.example.com +``` + +### 微服务架构 + +为不同类型的微服务提供统一的路由模式: + +``` +10.0.0.1:8001 *.auth.internal +10.0.0.2:8002 *.user.internal +10.0.0.3:8003 *.payment.internal +``` + +### 多租户系统 + +在多租户系统中为每个租户路由到不同后端: + +``` +# 每个租户的子域名指向专用服务器 +192.168.1.10 tenant1.*.example.com +192.168.1.11 tenant2.*.example.com +192.168.1.12 tenant3.*.example.com +``` + +### 本地开发环境 + +在本地开发中快速模拟复杂的服务架构: + +``` +127.0.0.1:3000 *.local +127.0.0.1:3001 api.*.local +127.0.0.1:5432 db.*.local ``` ## 注意事项 -1. 自签名证书会导致浏览器警告,仅用于测试目的 -2. 某些网站可能拒绝代理连接 -3. 此功能主要用于控制后端连接的IP解析和端口,不影响客户端DNS解析 -4. 在生产环境中使用时,建议使用更强大的DNS解析系统 -5. 端口信息会覆盖请求中的端口,即使请求URL中指定了端口也会被替换为自定义端口 \ No newline at end of file +1. 通配符域名只在我们的自定义DNS解析器中有效,不会影响系统DNS +2. 泛解析规则的顺序会影响匹配结果,后添加的规则优先级更高 +3. 过多的泛解析规则可能会影响性能,建议合理组织规则 +4. 当域名同时匹配多个规则时,精确匹配优先于通配符匹配 +5. 自签名证书会导致浏览器警告,仅用于测试目的 \ No newline at end of file diff --git a/cmd/wildcard_dns_proxy/main.go b/cmd/wildcard_dns_proxy/main.go new file mode 100644 index 0000000..18799e9 --- /dev/null +++ b/cmd/wildcard_dns_proxy/main.go @@ -0,0 +1,190 @@ +package main + +import ( + "flag" + "log" + "net/http" + "time" + + "github.com/goproxy/internal/config" + "github.com/goproxy/internal/dns" + "github.com/goproxy/internal/proxy" +) + +// WildcardDNSDelegate 带有泛解析功能的委托 +type WildcardDNSDelegate struct { + proxy.DefaultDelegate + targetHost string + targetPort string + resolver dns.Resolver +} + +// ModifyRequest 修改请求头 +func (d *WildcardDNSDelegate) ModifyRequest(req *http.Request) { + log.Printf("收到请求: %s %s", req.Method, req.URL.String()) + + // 设置标准浏览器请求头 + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/122.0.0.0 Safari/537.36") + req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") + req.Header.Set("Connection", "keep-alive") + + // 保留原始Host头,便于泛解析匹配 + // 只有当明确指定目标主机时才覆盖 + if d.targetHost != "" && d.targetHost != "*" { + req.Host = d.targetHost + } + + // 设置请求的URL方案为HTTPS + req.URL.Scheme = "https" + + log.Printf("修改后的请求: %s %s (Host: %s)", req.Method, req.URL.String(), req.Host) +} + +// ModifyResponse 修改响应头 +func (d *WildcardDNSDelegate) ModifyResponse(resp *http.Response) error { + log.Printf("收到响应: %d %s", resp.StatusCode, resp.Status) + + // 添加CORS头和代理标识 + resp.Header.Set("Access-Control-Allow-Origin", "*") + resp.Header.Set("X-Proxied-By", "GoProxy-WildcardDNS") + + return nil +} + +// ResolveBackend 解析后端服务器 +func (d *WildcardDNSDelegate) ResolveBackend(req *http.Request) (string, error) { + // 获取要解析的主机名 + hostToResolve := req.Host + if d.targetHost != "" && d.targetHost != "*" { + hostToResolve = d.targetHost + } + + // 从泛解析获取目标地址 + endpoint, err := d.resolver.ResolveWithPort(hostToResolve, 0) + if err != nil { + log.Printf("解析主机 %s 失败: %v, 使用默认端口: %s", hostToResolve, err, d.targetPort) + return hostToResolve + ":" + d.targetPort, nil + } + + // 优先使用解析得到的端口 + if endpoint.Port > 0 { + address := endpoint.GetAddressWithDefaultPort(0) + log.Printf("泛解析主机 %s 到目标服务器(自定义端口): %s", hostToResolve, address) + return address, nil + } + + // 使用默认端口 + address := endpoint.GetAddressWithDefaultPort(443) + log.Printf("泛解析主机 %s 到目标服务器(默认端口): %s", hostToResolve, address) + return address, nil +} + +func main() { + // 命令行参数 + listenAddr := flag.String("listen", ":8080", "监听地址") + targetHost := flag.String("target", "", "目标站点主机名,留空表示使用请求中的Host") + targetPort := flag.String("port", "443", "默认目标端口") + dnsFile := flag.String("dns", "", "DNS配置文件路径 (JSON格式)") + hostsFile := flag.String("hosts", "", "Hosts文件路径") + flag.Parse() + + // 创建DNS解析器 + var resolver dns.Resolver + var err error + + if *dnsFile != "" { + // 从JSON文件加载DNS配置 + dnsConfig, err := dns.LoadFromJSON(*dnsFile) + if err != nil { + log.Printf("加载DNS配置文件失败: %v,将使用默认DNS解析器", err) + resolver = dns.NewResolver() + } else { + resolver = dns.NewResolverFromConfig(dnsConfig) + log.Printf("已加载DNS配置,包含 %d 条记录", len(dnsConfig.Records)) + } + } else if *hostsFile != "" { + // 从hosts文件加载DNS配置 + dnsConfig, err := dns.LoadFromHostsFile(*hostsFile) + if err != nil { + log.Printf("加载hosts文件失败: %v,将使用默认DNS解析器", err) + resolver = dns.NewResolver() + } else { + resolver = dns.NewResolverFromConfig(dnsConfig) + log.Printf("已加载hosts文件,包含 %d 条记录", len(dnsConfig.Records)) + } + } else { + // 创建默认解析器并添加一些示例泛解析规则 + resolver = dns.NewResolver() + + // 通配符示例 + resolver.AddWildcard("*.example.com", "93.184.216.34") + resolver.AddWildcardWithPort("*.api.example.com", "93.184.216.35", 8443) + resolver.AddWildcard("*.github.com", "140.82.121.3") + + // 多级通配符示例 + resolver.AddWildcardWithPort("*.dev.local", "127.0.0.1", 3000) + resolver.AddWildcardWithPort("api.*.dev.local", "127.0.0.1", 3001) + resolver.AddWildcardWithPort("db.*.dev.local", "127.0.0.1", 5432) + + // 常规精确匹配 + resolver.Add("example.com", "93.184.216.34") + resolver.Add("github.com", "140.82.121.4") + + log.Printf("已创建默认解析器并添加示例泛解析规则") + } + + // 创建自定义DNS拨号器 + dnsDialer := dns.NewDialer(resolver) + + // 创建配置 + cfg := config.DefaultConfig() + cfg.ReverseProxy = true // 启用反向代理模式 + cfg.DecryptHTTPS = false // 不解密HTTPS流量,避免TLS问题 + cfg.IdleTimeout = 30 * time.Second // 连接空闲超时 + cfg.AddXForwardedFor = true // 添加X-Forwarded-For头 + cfg.AddXRealIP = true // 添加X-Real-IP头 + cfg.SupportWebSocketUpgrade = true // 支持WebSocket升级 + cfg.EnableCompression = false // 不启用压缩 + cfg.EnableCORS = true // 启用CORS + cfg.EnableRetry = false // 关闭重试功能 + cfg.EnableConnectionPool = false // 禁用连接池 + + // 创建自定义委托 + delegate := &WildcardDNSDelegate{ + targetHost: *targetHost, + targetPort: *targetPort, + resolver: resolver, + } + + // 创建代理实例 + p := proxy.New(&proxy.Options{ + Config: cfg, + Delegate: delegate, + }) + + // 设置自定义拨号器 + p.SetDialContext(dnsDialer.DialContext) + + // 创建HTTP服务器 + server := &http.Server{ + Addr: *listenAddr, + Handler: p, + } + + // 启动HTTP服务器 + log.Printf("泛解析DNS代理启动,监听地址: %s", *listenAddr) + + if *targetHost == "" { + log.Printf("透明代理模式:请求的Host将直接用于DNS解析") + log.Printf("提示: 尝试访问 http://localhost%s 并设置不同的Host头", *listenAddr) + } else { + log.Printf("目标主机模式:所有请求将被发送到 %s", *targetHost) + log.Printf("提示: 尝试访问 http://localhost%s", *listenAddr) + } + + err = server.ListenAndServe() + if err != nil { + log.Fatalf("服务器启动失败: %v", err) + } +} diff --git a/examples/wildcard_dns_config.json b/examples/wildcard_dns_config.json new file mode 100644 index 0000000..7f1790e --- /dev/null +++ b/examples/wildcard_dns_config.json @@ -0,0 +1,23 @@ +{ + "records": { + "example.com": "93.184.216.34", + "api.example.com": "93.184.216.35:8443", + + "*.github.com": "140.82.121.3", + "github.com": "140.82.121.4", + "api.github.com": "140.82.121.5", + + "*.s3.amazonaws.com": "52.216.162.69", + "cdn-*.example.org": "203.0.113.10", + + "*.dev.local": "127.0.0.1:3000", + "api.*.dev.local": "127.0.0.1:3001", + "db.*.dev.local": "127.0.0.1:5432", + + "*.test.example.com": "192.168.1.100", + "*.staging.example.com": "192.168.1.101", + "*.production.example.com": "192.168.1.102" + }, + "use_fallback": true, + "ttl": 300 +} \ No newline at end of file diff --git a/examples/wildcard_hosts.txt b/examples/wildcard_hosts.txt new file mode 100644 index 0000000..2038135 --- /dev/null +++ b/examples/wildcard_hosts.txt @@ -0,0 +1,27 @@ +# 精确匹配记录 +93.184.216.34 example.com +93.184.216.35:8443 api.example.com + +# GitHub相关泛解析 +140.82.121.3 *.github.com +140.82.121.4 github.com +140.82.121.5 api.github.com + +# AWS服务泛解析 +52.216.162.69 *.s3.amazonaws.com +203.0.113.10 cdn-*.example.org + +# 本地开发环境泛解析 +127.0.0.1:3000 *.dev.local +127.0.0.1:3001 api.*.dev.local +127.0.0.1:5432 db.*.dev.local + +# 多环境测试泛解析 +192.168.1.100 *.test.example.com +192.168.1.101 *.staging.example.com +192.168.1.102 *.production.example.com + +# 特定子域名泛解析示例 +10.0.0.1 *.api.service.com +10.0.0.2 *.auth.service.com +10.0.0.3 *.cdn.service.com \ No newline at end of file diff --git a/internal/dns/config.go b/internal/dns/config.go index 7728027..f171c15 100644 --- a/internal/dns/config.go +++ b/internal/dns/config.go @@ -12,9 +12,9 @@ import ( // DNSConfig DNS配置文件结构 type DNSConfig struct { - Records map[string]string `json:"records"` - Fallback bool `json:"fallback"` - TTL int `json:"ttl"` // 缓存TTL,单位为秒 + Records map[string]string `json:"records"` // 普通记录和泛解析记录 + Fallback bool `json:"fallback"` // 是否回退到系统DNS + TTL int `json:"ttl"` // 缓存TTL,单位为秒 } // LoadFromJSON 从JSON文件加载DNS配置 @@ -59,6 +59,11 @@ func (c *DNSConfig) SaveToJSON(filePath string) error { // 用于解析hosts文件中的IP:端口格式 var ipPortRegex = regexp.MustCompile(`^([0-9.]+)(?::(\d+))?$`) +// 检查是否为通配符域名 +func isWildcardDomain(domain string) bool { + return strings.Contains(domain, "*") +} + // LoadFromHostsFile 从hosts文件格式加载DNS配置 func LoadFromHostsFile(filePath string) (*DNSConfig, error) { file, err := os.Open(filePath) @@ -112,6 +117,8 @@ func LoadFromHostsFile(filePath string) (*DNSConfig, error) { if strings.HasPrefix(domain, "#") { break } + + // 支持通配符和普通域名 config.Records[domain] = value } } diff --git a/internal/dns/resolver.go b/internal/dns/resolver.go index f7fa9cc..a483ade 100644 --- a/internal/dns/resolver.go +++ b/internal/dns/resolver.go @@ -3,6 +3,8 @@ package dns import ( "errors" "net" + "sort" + "strings" "sync" "time" ) @@ -21,6 +23,12 @@ type Resolver interface { // AddWithPort 添加带端口的域名解析规则 AddWithPort(host, ip string, port int) error + // AddWildcard 添加泛解析规则(通配符域名) + AddWildcard(wildcardDomain, ip string) error + + // AddWildcardWithPort 添加带端口的泛解析规则 + AddWildcardWithPort(wildcardDomain, ip string, port int) error + // Remove 删除域名解析规则 Remove(host string) error @@ -30,11 +38,19 @@ type Resolver interface { // CustomResolver 自定义DNS解析器 type CustomResolver struct { - mu sync.RWMutex - records map[string]*Endpoint // 域名到端点的映射 - cache map[string]cacheEntry // 外部域名解析缓存 - fallback bool // 是否在本地记录找不到时回退到系统DNS - ttl time.Duration // 缓存TTL + mu sync.RWMutex + records map[string]*Endpoint // 精确域名到端点的映射 + wildcardRules []wildcardRule // 通配符规则列表 + cache map[string]cacheEntry // 外部域名解析缓存 + fallback bool // 是否在本地记录找不到时回退到系统DNS + ttl time.Duration // 缓存TTL +} + +// wildcardRule 通配符规则 +type wildcardRule struct { + pattern string // 原始通配符模式,如 *.example.com + parts []string // 分解后的模式部分,如 ["*", "example", "com"] + endpoint *Endpoint // 对应的端点 } // cacheEntry 缓存条目 @@ -46,10 +62,11 @@ type cacheEntry struct { // NewResolver 创建新的自定义DNS解析器 func NewResolver(options ...Option) *CustomResolver { r := &CustomResolver{ - records: make(map[string]*Endpoint), - cache: make(map[string]cacheEntry), - fallback: true, - ttl: 5 * time.Minute, + records: make(map[string]*Endpoint), + wildcardRules: make([]wildcardRule, 0), + cache: make(map[string]cacheEntry), + fallback: true, + ttl: 5 * time.Minute, } // 应用选项 @@ -73,11 +90,19 @@ func (r *CustomResolver) Resolve(host string) (string, error) { func (r *CustomResolver) ResolveWithPort(host string, defaultPort int) (*Endpoint, error) { // 首先检查自定义记录 r.mu.RLock() + + // 精确匹配 if endpoint, ok := r.records[host]; ok { r.mu.RUnlock() return endpoint, nil } + // 尝试通配符匹配 + if endpoint := r.matchWildcard(host); endpoint != nil { + r.mu.RUnlock() + return endpoint, nil + } + // 检查缓存 if entry, ok := r.cache[host]; ok { if time.Now().Before(entry.expiresAt) { @@ -127,6 +152,44 @@ func (r *CustomResolver) ResolveWithPort(host string, defaultPort int) (*Endpoin return nil, errors.New("未找到域名记录且系统DNS回退被禁用") } +// matchWildcard 尝试匹配通配符规则 +func (r *CustomResolver) matchWildcard(host string) *Endpoint { + hostParts := strings.Split(host, ".") + + // 按照通配符规则列表的顺序尝试匹配 + // 规则顺序应该保证更具体的规则先匹配 + for _, rule := range r.wildcardRules { + if matchDomainPattern(hostParts, rule.parts) { + return rule.endpoint + } + } + + return nil +} + +// matchDomainPattern 判断域名部分是否匹配通配符模式 +func matchDomainPattern(hostParts, patternParts []string) bool { + // 如果长度不匹配,则不匹配 + if len(hostParts) != len(patternParts) { + return false + } + + // 逐部分匹配 + for i := 0; i < len(hostParts); i++ { + // 如果模式部分是星号,则匹配任何内容 + if patternParts[i] == "*" { + continue + } + + // 否则必须精确匹配 + if hostParts[i] != patternParts[i] { + return false + } + } + + return true +} + // Add 添加域名解析规则 func (r *CustomResolver) Add(host, ip string) error { return r.AddWithPort(host, ip, 0) @@ -145,17 +208,62 @@ func (r *CustomResolver) AddWithPort(host, ip string, port int) error { return nil } +// AddWildcard 添加泛解析规则 +func (r *CustomResolver) AddWildcard(wildcardDomain, ip string) error { + return r.AddWildcardWithPort(wildcardDomain, ip, 0) +} + +// AddWildcardWithPort 添加带端口的泛解析规则 +func (r *CustomResolver) AddWildcardWithPort(wildcardDomain, ip string, port int) error { + if net.ParseIP(ip) == nil { + return errors.New("无效的IP地址") + } + + // 检查通配符格式 + if !strings.Contains(wildcardDomain, "*") { + return errors.New("泛解析域名必须包含通配符'*'") + } + + // 分解通配符域名 + parts := strings.Split(wildcardDomain, ".") + + r.mu.Lock() + defer r.mu.Unlock() + + // 创建新的通配符规则 + rule := wildcardRule{ + pattern: wildcardDomain, + parts: parts, + endpoint: NewEndpointWithPort(ip, port), + } + + // 将新规则添加到规则列表头部,确保更新的规则优先匹配 + r.wildcardRules = append([]wildcardRule{rule}, r.wildcardRules...) + + return nil +} + // Remove 删除域名解析规则 func (r *CustomResolver) Remove(host string) error { r.mu.Lock() defer r.mu.Unlock() - if _, ok := r.records[host]; !ok { - return errors.New("域名记录不存在") + // 先尝试删除精确匹配记录 + if _, ok := r.records[host]; ok { + delete(r.records, host) + return nil } - delete(r.records, host) - return nil + // 然后尝试删除通配符记录 + for i, rule := range r.wildcardRules { + if rule.pattern == host { + // 删除这条规则 + r.wildcardRules = append(r.wildcardRules[:i], r.wildcardRules[i+1:]...) + return nil + } + } + + return errors.New("域名记录不存在") } // Clear 清除所有解析规则 @@ -164,6 +272,7 @@ func (r *CustomResolver) Clear() { defer r.mu.Unlock() r.records = make(map[string]*Endpoint) + r.wildcardRules = make([]wildcardRule, 0) r.cache = make(map[string]cacheEntry) } @@ -190,17 +299,74 @@ func (r *CustomResolver) LoadFromMap(records map[string]string) error { defer r.mu.Unlock() for host, value := range records { - endpoint, err := ParseEndpoint(value) - if err != nil { - return err - } + // 判断是否为通配符域名 + if strings.Contains(host, "*") { + endpoint, err := ParseEndpoint(value) + if err != nil { + return err + } - if net.ParseIP(endpoint.IP) == nil { - return errors.New("无效的IP地址: " + endpoint.IP + " (域名: " + host + ")") - } + if net.ParseIP(endpoint.IP) == nil { + return errors.New("无效的IP地址: " + endpoint.IP + " (域名: " + host + ")") + } - r.records[host] = endpoint + // 添加通配符规则 + rule := wildcardRule{ + pattern: host, + parts: strings.Split(host, "."), + endpoint: endpoint, + } + r.wildcardRules = append(r.wildcardRules, rule) + + } else { + // 常规记录 + endpoint, err := ParseEndpoint(value) + if err != nil { + return err + } + + if net.ParseIP(endpoint.IP) == nil { + return errors.New("无效的IP地址: " + endpoint.IP + " (域名: " + host + ")") + } + + r.records[host] = endpoint + } } + // 对通配符规则进行排序,确保更具体的规则先匹配 + sortWildcardRules(r.wildcardRules) + return nil } + +// sortWildcardRules 对通配符规则进行排序,使更具体的规则优先匹配 +func sortWildcardRules(rules []wildcardRule) { + // 使用稳定排序,保证相同优先级的规则保持原有顺序(后添加的规则在前面) + sort.SliceStable(rules, func(i, j int) bool { + ruleI := rules[i] + ruleJ := rules[j] + + // 计算每个规则中通配符的数量 + wildcardCountI := countWildcards(ruleI.parts) + wildcardCountJ := countWildcards(ruleJ.parts) + + // 通配符数量少的规则更具体,优先级更高 + if wildcardCountI != wildcardCountJ { + return wildcardCountI < wildcardCountJ + } + + // 如果通配符数量相同,域名部分数量多的更具体 + return len(ruleI.parts) > len(ruleJ.parts) + }) +} + +// countWildcards 计算域名部分中通配符的数量 +func countWildcards(parts []string) int { + count := 0 + for _, part := range parts { + if part == "*" { + count++ + } + } + return count +} diff --git a/scripts/generate_cert.ps1 b/scripts/generate_cert.ps1 index 40d1b1b..6ef411e 100644 --- a/scripts/generate_cert.ps1 +++ b/scripts/generate_cert.ps1 @@ -1,16 +1,12 @@ # PowerShell 脚本:生成自签名证书 -# 默认值 -$Days = 365 -$Subject = "CN=localhost,OU=Test,O=GoProxy,L=Shanghai,S=Shanghai,C=CN" - # 处理命令行参数 param( [Parameter(HelpMessage="证书有效期(天数)")] [int]$days = 365, [Parameter(HelpMessage="证书主题")] - [string]$subject = $Subject, + [string]$subject = "CN=localhost,OU=Test,O=GoProxy,L=Shanghai,S=Shanghai,C=CN", [Parameter(HelpMessage="公用名(CN)")] [string]$cn = "", @@ -28,7 +24,7 @@ function Show-Help { Write-Host "选项:" Write-Host " -help 显示此帮助信息" Write-Host " -days DAYS 证书有效期(天数),默认: 365" - Write-Host " -subject SUB 证书主题,默认: $Subject" + Write-Host " -subject SUB 证书主题,默认: $subject" Write-Host " -cn CN 公用名(CN),将替换主题中的CN,默认: localhost" Write-Host Write-Host "示例:" @@ -44,12 +40,12 @@ if ($help) { # 如果指定了CN,替换主题中的CN部分 if ($cn -ne "") { - $Subject = $Subject -replace "CN=[^,]*", "CN=$cn" + $subject = $subject -replace "CN=[^,]*", "CN=$cn" } Write-Host "生成自签名证书..." Write-Host "有效期: $days 天" -Write-Host "主题: $Subject" +Write-Host "主题: $subject" # 检查OpenSSL是否可用 $openssl = Get-Command "openssl" -ErrorAction SilentlyContinue @@ -66,7 +62,7 @@ try { # 生成证书请求 Write-Host "正在生成证书请求..." -ForegroundColor Cyan - & openssl req -new -key server.key -out server.csr -subj $Subject.Replace(",", "/") + & openssl req -new -key server.key -out server.csr -subj $subject.Replace(",", "/") # 生成自签名证书 Write-Host "正在生成自签名证书..." -ForegroundColor Cyan @@ -85,4 +81,4 @@ try { catch { Write-Host "错误: 生成证书时发生错误: $_" -ForegroundColor Red exit 1 -} \ No newline at end of file +}