# GoEdge HTTPDNS 技术实施方案 > 版本: 1.0 | 作者: AI Assistant | 日期: 2026-02-09 --- ## 一、项目概述 ### 1.1 目标 在 GoEdge 平台实现完整的 HTTPDNS 服务,包括: - 基于 HTTPS 的 DNS 解析接口 - 动态指纹校验(WAF) - App 管理后台 - 移动端 SDK 示例 ### 1.2 设计决策 | 决策项 | 选择 | 理由 | |--------|------|------| | WAF 指纹校验 | 必须 | 防止非法请求绕过解析直接攻击源站 | | App 管理界面 | 必须 | 标准产品功能,支持多租户 | | SDK 示例 | 提供 | 降低客户接入成本 | | 部署位置 | 复用 Edge-DNS | 减少运维复杂度 | | 接口路径 | 新增 `/httpdns/resolve` | 保持向后兼容 | --- ## 二、系统架构 ### 2.1 整体架构图 ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ 移动端 App │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ HTTPDNS SDK │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ Resolver │ │CacheManager │ │NetworkMonitor│ │ │ │ │ │ 解析引擎 │ │ 缓存管理 │ │ 网络感知 │ │ │ │ │ │ -多节点容错 │ │ -内存LRU │ │ -切换监听 │ │ │ │ │ │ -超时降级 │ │ -持久化 │ │ -自动清缓存 │ │ │ │ │ └─────────────┘ │ -软过期 │ │ -IPv6检测 │ │ │ │ │ └─────────────┘ └─────────────┘ │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ Signer │ │ Reporter │ │ Prefetch │ │ │ │ │ │ 签名模块 │ │ 监控上报 │ │ 预解析 │ │ │ │ │ │ -HMAC-SHA256│ │ -成功率 │ │ -冷启动优化 │ │ │ │ │ │ -防重放 │ │ -耗时统计 │ │ -批量解析 │ │ │ │ │ └─────────────┘ │ -缓存命中率│ └─────────────┘ │ │ │ │ └─────────────┘ │ │ │ └───────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ │ │ HTTPS ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ GoEdge 平台 │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Edge-DNS │ │ Edge-Node │ │ Edge-Admin │ │ │ │ │ │ │ │ │ │ │ │ /httpdns/ │ │ WAF 校验 │ │ App 管理 │ │ │ │ resolve │ │ 指纹验证 │ │ AppID/Secret│ │ │ │ │ │ │ │ │ │ │ │ 智能调度 │ │ 流量转发 │ │ SDK统计接收 │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ │ │ │ └───────────────────┴───────────────────┘ │ │ │ │ │ ┌──────────────┐ │ │ │ Edge-API │ │ │ │ 数据服务 │ │ │ └──────────────┘ │ │ │ │ │ ┌──────────────┐ │ │ │ MySQL │ │ │ └──────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ │ │ HTTPS (IP 直连) ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ 源站服务器 │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### 2.2 请求流程(带缓存与降级) ```mermaid sequenceDiagram participant App as 移动 App participant Cache as SDK缓存 participant Resolver as SDK解析器 participant DNS as Edge-DNS participant SysDNS as 系统DNS participant Node as Edge-Node participant API as Edge-API Note over App,API: 阶段一:DNS 解析(含缓存与降级) App->>Resolver: resolve("api.example.com") Resolver->>Cache: 查询缓存 alt 缓存有效 Cache-->>Resolver: 返回 IP (命中) Resolver-->>App: ["1.2.3.4"] else 缓存软过期 Cache-->>Resolver: 返回 IP + 标记需刷新 Resolver-->>App: ["1.2.3.4"] (先返回) Resolver->>DNS: 后台异步刷新 DNS-->>Resolver: 新 IP Resolver->>Cache: 更新缓存 else 缓存完全过期或不存在 Resolver->>DNS: GET /httpdns/resolve alt HTTPDNS 正常 DNS-->>Resolver: {"ips": ["1.2.3.4"], "ttl": 600} Resolver->>Cache: 写入缓存 Resolver-->>App: ["1.2.3.4"] else HTTPDNS 超时/失败 Resolver->>SysDNS: 降级到系统 DNS SysDNS-->>Resolver: 1.2.3.4 Resolver-->>App: ["1.2.3.4"] (降级) end end Note over App,API: 阶段二:业务请求(带签名) App->>Node: HTTPS://1.2.3.4/v1/user + 签名Header Node->>API: 查询 AppSecret API-->>Node: AppSecret Node->>Node: HMAC-SHA256 验证 alt 验证失败 Node-->>App: 403 Forbidden else 验证成功 Node-->>App: 200 OK + 响应数据 end Note over App,API: 阶段三:监控上报(异步) Resolver-->>API: 定时上报统计数据 ``` ### 2.3 网络切换处理流程 ```mermaid flowchart LR A[WiFi] -->|切换| B[4G/5G] B --> C{NetworkMonitor检测} C --> D[清空所有缓存] D --> E[下次请求重新解析] E --> F[获取新网络最优IP] ``` --- ## 三、模块详细设计 ### 3.1 Edge-DNS: HTTPDNS 解析接口 #### 3.1.1 接口定义 | 项目 | 说明 | |------|------| | **Endpoint** | `GET /httpdns/resolve` | | **协议** | HTTPS (443) | | **认证** | 无(解析接口公开,业务请求才需要签名) | #### 3.1.2 请求参数 | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `host` | string | 是 | 待解析的域名 | | `type` | string | 否 | 记录类型,默认 `A,AAAA`(同时返回) | | `ip` | string | 否 | 客户端 IP(用于调试/代理场景) | #### 3.1.3 响应格式 ```json { "status": "ok", "dns_server_time": 1700000000, "client_ip": "114.114.114.114", "data": [ { "host": "api.example.com", "type": "A", "ips": ["1.1.1.1", "1.1.1.2"], "ips_v6": ["240e:xxx::1"], "ttl": 600 } ] } ``` #### 3.1.4 代码实现 **文件**: `EdgeDNS/internal/nodes/httpdns.go` (新建) ```go package nodes import ( "encoding/json" "net" "net/http" "strings" "time" ) // HTTPDNSResponse HTTPDNS 响应结构 type HTTPDNSResponse struct { Status string `json:"status"` DNSServerTime int64 `json:"dns_server_time"` ClientIP string `json:"client_ip"` Data []HTTPDNSRecord `json:"data"` Error string `json:"error,omitempty"` } // HTTPDNSRecord 单条解析记录 type HTTPDNSRecord struct { Host string `json:"host"` Type string `json:"type"` IPs []string `json:"ips"` IPsV6 []string `json:"ips_v6"` TTL int `json:"ttl"` } // handleHTTPDNSResolve 处理 HTTPDNS 解析请求 func (this *Server) handleHTTPDNSResolve(writer http.ResponseWriter, req *http.Request) { writer.Header().Set("Content-Type", "application/json") writer.Header().Set("Access-Control-Allow-Origin", "*") // 1. 解析参数 query := req.URL.Query() host := strings.TrimSpace(query.Get("host")) if host == "" { this.writeHTTPDNSError(writer, "missing 'host' parameter") return } // 2. 获取客户端 IP clientIP := query.Get("ip") if clientIP == "" { clientIP = this.extractClientIP(req) } // 3. 查询 A 记录 ipsV4 := this.resolveRecords(host, "A", clientIP) // 4. 查询 AAAA 记录 ipsV6 := this.resolveRecords(host, "AAAA", clientIP) // 5. 获取 TTL ttl := this.getRecordTTL(host, clientIP) if ttl == 0 { ttl = 600 } // 6. 构造响应 resp := HTTPDNSResponse{ Status: "ok", DNSServerTime: time.Now().Unix(), ClientIP: clientIP, Data: []HTTPDNSRecord{{ Host: host, Type: "A", IPs: ipsV4, IPsV6: ipsV6, TTL: ttl, }}, } json.NewEncoder(writer).Encode(resp) } // extractClientIP 从请求中提取客户端真实 IP func (this *Server) extractClientIP(req *http.Request) string { xff := req.Header.Get("X-Forwarded-For") if xff != "" { parts := strings.Split(xff, ",") return strings.TrimSpace(parts[0]) } xri := req.Header.Get("X-Real-IP") if xri != "" { return xri } host, _, _ := net.SplitHostPort(req.RemoteAddr) return host } // resolveRecords 解析指定类型的记录 func (this *Server) resolveRecords(host, recordType, clientIP string) []string { var result []string if !strings.HasSuffix(host, ".") { host += "." } domain, recordName := sharedDomainManager.SplitDomain(host) if domain == nil { return result } routeCodes := sharedRouteManager.FindRouteCodes(clientIP, domain.UserId) records, _ := sharedRecordManager.FindRecords(domain.Id, routeCodes, recordName, recordType, false) for _, record := range records { if record.Value != "" { result = append(result, record.Value) } } return result } // getRecordTTL 获取记录 TTL func (this *Server) getRecordTTL(host, clientIP string) int { if !strings.HasSuffix(host, ".") { host += "." } domain, recordName := sharedDomainManager.SplitDomain(host) if domain == nil { return 0 } routeCodes := sharedRouteManager.FindRouteCodes(clientIP, domain.UserId) records, _ := sharedRecordManager.FindRecords(domain.Id, routeCodes, recordName, "A", false) if len(records) > 0 { return int(records[0].Ttl) } return 0 } // writeHTTPDNSError 写入错误响应 func (this *Server) writeHTTPDNSError(writer http.ResponseWriter, errMsg string) { writer.WriteHeader(http.StatusBadRequest) resp := HTTPDNSResponse{ Status: "error", DNSServerTime: time.Now().Unix(), Error: errMsg, } json.NewEncoder(writer).Encode(resp) } ``` **修改**: `EdgeDNS/internal/nodes/server.go` 第735行附近 ```go func (this *Server) handleHTTP(writer http.ResponseWriter, req *http.Request) { if req.URL.Path == "/dns-query" { this.handleHTTPDNSMessage(writer, req) return } // 新增 HTTPDNS JSON API if req.URL.Path == "/httpdns/resolve" { this.handleHTTPDNSResolve(writer, req) return } if req.URL.Path == "/resolve" { this.handleHTTPJSONAPI(writer, req) return } writer.WriteHeader(http.StatusNotFound) } ``` --- ### 3.2 Edge-Node: WAF 指纹校验 #### 3.2.1 校验逻辑 | 步骤 | 说明 | |------|------| | 1 | 提取 `X-GE-AppID`, `X-GE-Timestamp`, `X-GE-Token` | | 2 | 根据 AppID 查询 AppSecret | | 3 | 校验时间戳(±300 秒内有效) | | 4 | 计算 HMAC-SHA256 并比对 | #### 3.2.2 代码实现 **文件**: `EdgeNode/internal/waf/checkpoints/checkpoint_httpdns_fingerprint.go` (新建) ```go package checkpoints import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "math" "net/http" "strconv" "time" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeNode/internal/rpc" ) type CheckpointHTTPDNSFingerprint struct { Checkpoint } func (this *CheckpointHTTPDNSFingerprint) RequestValue( req CheckpointRequest, param string, options map[string]string, ruleId int64, ) (value any, hasRequestBody bool, sysErr error, userErr error) { httpReq, ok := req.WAFRaw().(*http.Request) if !ok { return "INVALID_REQUEST", false, nil, nil } appID := httpReq.Header.Get("X-GE-AppID") token := httpReq.Header.Get("X-GE-Token") tsStr := httpReq.Header.Get("X-GE-Timestamp") if appID == "" && token == "" && tsStr == "" { return "", false, nil, nil } if appID == "" { return "MISSING_APPID", false, nil, nil } if token == "" { return "MISSING_TOKEN", false, nil, nil } if tsStr == "" { return "MISSING_TIMESTAMP", false, nil, nil } appSecret, err := this.getAppSecret(appID) if err != nil || appSecret == "" { return "INVALID_APPID", false, nil, nil } ts, _ := strconv.ParseInt(tsStr, 10, 64) if math.Abs(float64(time.Now().Unix()-ts)) > 300 { return "TIMESTAMP_EXPIRED", false, nil, nil } mac := hmac.New(sha256.New, []byte(appSecret)) mac.Write([]byte(appID + tsStr + httpReq.URL.Path)) expected := hex.EncodeToString(mac.Sum(nil)) if token != expected { return "SIGNATURE_MISMATCH", false, nil, nil } return "OK", false, nil, nil } func (this *CheckpointHTTPDNSFingerprint) ResponseValue( req CheckpointRequest, param string, options map[string]string, ruleId int64, ) (value any, hasRequestBody bool, sysErr error, userErr error) { return "", false, nil, nil } func (this *CheckpointHTTPDNSFingerprint) getAppSecret(appID string) (string, error) { client, err := rpc.SharedRPC() if err != nil { return "", err } resp, err := client.HTTPDNSAppRPC.FindHTTPDNSAppSecret( client.Context(), &pb.FindHTTPDNSAppSecretRequest{AppId: appID}, ) if err != nil { return "", err } return resp.AppSecret, nil } ``` --- ### 3.3 Edge-API: App 管理服务 #### 3.3.1 数据库表 ```sql CREATE TABLE IF NOT EXISTS edgeHTTPDNSApps ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, appId VARCHAR(64) NOT NULL UNIQUE COMMENT 'App标识', appSecret VARCHAR(128) NOT NULL COMMENT 'App密钥', name VARCHAR(255) NOT NULL DEFAULT '' COMMENT '应用名称', description TEXT COMMENT '描述', userId BIGINT UNSIGNED DEFAULT 0 COMMENT '关联用户ID', isOn TINYINT(1) DEFAULT 1 COMMENT '是否启用', createdAt INT UNSIGNED DEFAULT 0, state TINYINT(1) DEFAULT 1, UNIQUE KEY uk_appId (appId), KEY idx_userId (userId) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` #### 3.3.2 gRPC Proto **文件**: `EdgeCommon/pkg/rpc/protos/service_httpdns_app.proto` (新建) ```protobuf syntax = "proto3"; option go_package = "./pb"; package pb; service HTTPDNSAppService { rpc createHTTPDNSApp(CreateHTTPDNSAppRequest) returns (CreateHTTPDNSAppResponse); rpc findHTTPDNSAppSecret(FindHTTPDNSAppSecretRequest) returns (FindHTTPDNSAppSecretResponse); rpc listHTTPDNSApps(ListHTTPDNSAppsRequest) returns (ListHTTPDNSAppsResponse); rpc deleteHTTPDNSApp(DeleteHTTPDNSAppRequest) returns (RPCSuccess); } message CreateHTTPDNSAppRequest { string name = 1; string description = 2; int64 userId = 3; } message CreateHTTPDNSAppResponse { int64 httpdnsAppId = 1; string appId = 2; string appSecret = 3; } message FindHTTPDNSAppSecretRequest { string appId = 1; } message FindHTTPDNSAppSecretResponse { string appSecret = 1; } message ListHTTPDNSAppsRequest { int64 userId = 1; int64 offset = 2; int64 size = 3; } message ListHTTPDNSAppsResponse { repeated HTTPDNSApp httpdnsApps = 1; } message DeleteHTTPDNSAppRequest { int64 httpdnsAppId = 1; } message HTTPDNSApp { int64 id = 1; string appId = 2; string appSecret = 3; string name = 4; int64 userId = 5; bool isOn = 6; int64 createdAt = 7; } ``` --- ### 3.4 SDK 完整设计 #### 3.4.1 SDK 架构概览 ``` ┌─────────────────────────────────────────────────────────────┐ │ HTTPDNS SDK │ ├─────────────────────────────────────────────────────────────┤ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │ │ 解析引擎 │ │ 缓存管理 │ │ 网络感知 │ │ │ │ Resolver │ │ CacheManager │ │ NetworkMonitor│ │ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │ │ 容错降级 │ │ 签名模块 │ │ 监控上报 │ │ │ │ Failover │ │ Signer │ │ Reporter │ │ │ └───────────────┘ └───────────────┘ └───────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` #### 3.4.2 缓存管理模块 (CacheManager) **设计原则**: - 两级缓存:内存 + 持久化 - 软过期策略:过期后先返回旧数据,后台异步更新 - LRU 淘汰:防止内存溢出 **Android 实现**: ```kotlin class HTTPDNSCacheManager(private val context: Context) { // 内存缓存 (LRU) private val memoryCache = object : LinkedHashMap(100, 0.75f, true) { override fun removeEldestEntry(eldest: Map.Entry): Boolean { return size > MAX_CACHE_SIZE } } // 持久化缓存 (MMKV) private val mmkv = MMKV.mmkvWithID("httpdns_cache", MMKV.MULTI_PROCESS_MODE) companion object { const val MAX_CACHE_SIZE = 100 const val SOFT_EXPIRE_SECONDS = 60 // 软过期:TTL 到期后仍可用 60s } data class CacheEntry( val ips: List, val ipsV6: List, val expireAt: Long, // 硬过期时间 val softExpireAt: Long, // 软过期时间 = expireAt + SOFT_EXPIRE_SECONDS val createAt: Long = System.currentTimeMillis() ) { fun isExpired(): Boolean = System.currentTimeMillis() > expireAt fun isSoftExpired(): Boolean = System.currentTimeMillis() > softExpireAt fun toJson(): String = Gson().toJson(this) } /** * 获取缓存(支持软过期) * @return Pair<缓存结果, 是否需要后台刷新> */ @Synchronized fun get(host: String): Pair { // 1. 先查内存 memoryCache[host]?.let { entry -> return when { !entry.isExpired() -> Pair(entry, false) // 未过期 !entry.isSoftExpired() -> Pair(entry, true) // 软过期,需刷新 else -> Pair(null, true) // 完全过期 } } // 2. 查持久化 val json = mmkv.decodeString(host) ?: return Pair(null, true) val entry = Gson().fromJson(json, CacheEntry::class.java) // 回填内存 memoryCache[host] = entry return when { !entry.isExpired() -> Pair(entry, false) !entry.isSoftExpired() -> Pair(entry, true) else -> Pair(null, true) } } /** * 写入缓存 */ @Synchronized fun put(host: String, ips: List, ipsV6: List, ttl: Int) { val now = System.currentTimeMillis() val entry = CacheEntry( ips = ips, ipsV6 = ipsV6, expireAt = now + ttl * 1000L, softExpireAt = now + (ttl + SOFT_EXPIRE_SECONDS) * 1000L ) memoryCache[host] = entry mmkv.encode(host, entry.toJson()) } /** * 清空所有缓存(网络切换时调用) */ @Synchronized fun clear() { memoryCache.clear() mmkv.clearAll() } } ``` **iOS 实现**: ```swift class HTTPDNSCacheManager { static let shared = HTTPDNSCacheManager() private var memoryCache: [String: CacheEntry] = [:] private let defaults = UserDefaults(suiteName: "com.goedge.httpdns")! private let queue = DispatchQueue(label: "httpdns.cache", attributes: .concurrent) private let maxCacheSize = 100 private let softExpireSeconds: TimeInterval = 60 struct CacheEntry: Codable { let ips: [String] let ipsV6: [String] let expireAt: Date let softExpireAt: Date let createAt: Date func isExpired() -> Bool { Date() > expireAt } func isSoftExpired() -> Bool { Date() > softExpireAt } } func get(host: String) -> (entry: CacheEntry?, needRefresh: Bool) { return queue.sync { // 查内存 if let entry = memoryCache[host] { if !entry.isExpired() { return (entry, false) } if !entry.isSoftExpired() { return (entry, true) } } // 查持久化 guard let data = defaults.data(forKey: host), let entry = try? JSONDecoder().decode(CacheEntry.self, from: data) else { return (nil, true) } // 回填内存 memoryCache[host] = entry if !entry.isExpired() { return (entry, false) } if !entry.isSoftExpired() { return (entry, true) } return (nil, true) } } func put(host: String, ips: [String], ipsV6: [String], ttl: Int) { queue.async(flags: .barrier) { let entry = CacheEntry( ips: ips, ipsV6: ipsV6, expireAt: Date().addingTimeInterval(TimeInterval(ttl)), softExpireAt: Date().addingTimeInterval(TimeInterval(ttl) + self.softExpireSeconds), createAt: Date() ) self.memoryCache[host] = entry // LRU 淘汰 if self.memoryCache.count > self.maxCacheSize { let oldest = self.memoryCache.min { $0.value.createAt < $1.value.createAt } if let key = oldest?.key { self.memoryCache.removeValue(forKey: key) } } // 持久化 if let data = try? JSONEncoder().encode(entry) { self.defaults.set(data, forKey: host) } } } func clear() { queue.async(flags: .barrier) { self.memoryCache.removeAll() // 清理持久化 for key in self.defaults.dictionaryRepresentation().keys { self.defaults.removeObject(forKey: key) } } } } ``` #### 3.4.3 网络感知模块 (NetworkMonitor) **功能**: - 监听网络切换(WiFi ↔ 4G/5G) - 网络切换时清空缓存 - 检测 IPv6 可用性 **Android 实现**: ```kotlin class NetworkMonitor(private val context: Context) { private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager private var lastNetworkType: String? = null var onNetworkChanged: (() -> Unit)? = null private val networkCallback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { checkNetworkChange() } override fun onLost(network: Network) { checkNetworkChange() } override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) { checkNetworkChange() } } fun start() { val request = NetworkRequest.Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .build() connectivityManager.registerNetworkCallback(request, networkCallback) lastNetworkType = getCurrentNetworkType() } fun stop() { connectivityManager.unregisterNetworkCallback(networkCallback) } private fun checkNetworkChange() { val currentType = getCurrentNetworkType() if (currentType != lastNetworkType) { Log.d("HTTPDNS", "Network changed: $lastNetworkType -> $currentType") lastNetworkType = currentType onNetworkChanged?.invoke() } } private fun getCurrentNetworkType(): String { val network = connectivityManager.activeNetwork ?: return "NONE" val caps = connectivityManager.getNetworkCapabilities(network) ?: return "UNKNOWN" return when { caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> "WIFI" caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> "CELLULAR" else -> "OTHER" } } /** * 检测当前网络是否支持 IPv6 */ fun isIPv6Supported(): Boolean { return try { val addresses = NetworkInterface.getNetworkInterfaces().toList() .flatMap { it.inetAddresses.toList() } addresses.any { it is Inet6Address && !it.isLoopbackAddress && !it.isLinkLocalAddress } } catch (e: Exception) { false } } } ``` **iOS 实现**: ```swift import Network class NetworkMonitor { static let shared = NetworkMonitor() private let monitor = NWPathMonitor() private let queue = DispatchQueue(label: "httpdns.network") private var lastInterfaceType: NWInterface.InterfaceType? var onNetworkChanged: (() -> Void)? func start() { monitor.pathUpdateHandler = { [weak self] path in let currentType = path.availableInterfaces.first?.type if currentType != self?.lastInterfaceType { print("HTTPDNS: Network changed \(self?.lastInterfaceType?.description ?? "nil") -> \(currentType?.description ?? "nil")") self?.lastInterfaceType = currentType DispatchQueue.main.async { self?.onNetworkChanged?() } } } monitor.start(queue: queue) } func stop() { monitor.cancel() } func isIPv6Supported() -> Bool { let path = monitor.currentPath return path.supportsIPv6 } } ``` #### 3.4.4 容错降级模块 (Failover) **策略**: 1. 多节点轮询:主节点失败自动切备用 2. 超时降级:HTTPDNS 超时自动降级到系统 DNS 3. 黑名单机制:特定域名强制走系统 DNS **Android 实现**: ```kotlin class HTTPDNSResolver( private val serverUrls: List, // 多个服务节点 private val cacheManager: HTTPDNSCacheManager, private val timeout: Long = 3000L ) { private val blacklist = setOf("localhost", "*.local", "*.internal") private var currentServerIndex = 0 suspend fun resolve(host: String): ResolveResult { // 1. 黑名单检查 if (isBlacklisted(host)) { return ResolveResult(systemResolve(host), ResolveSource.SYSTEM_DNS) } // 2. 查缓存 val (cached, needRefresh) = cacheManager.get(host) if (cached != null) { if (needRefresh) { // 后台异步刷新 CoroutineScope(Dispatchers.IO).launch { fetchFromServer(host) } } return ResolveResult(cached.ips, ResolveSource.CACHE) } // 3. 请求服务器(带重试) return try { withTimeout(timeout) { val ips = fetchFromServerWithRetry(host) ResolveResult(ips, ResolveSource.HTTPDNS) } } catch (e: TimeoutCancellationException) { // 4. 降级到系统 DNS Log.w("HTTPDNS", "Timeout, fallback to system DNS") ResolveResult(systemResolve(host), ResolveSource.SYSTEM_DNS) } } private suspend fun fetchFromServerWithRetry(host: String): List { var lastError: Exception? = null repeat(serverUrls.size) { attempt -> try { return fetchFromServer(host) } catch (e: Exception) { lastError = e currentServerIndex = (currentServerIndex + 1) % serverUrls.size Log.w("HTTPDNS", "Server ${serverUrls[currentServerIndex]} failed, trying next") } } throw lastError ?: Exception("All servers failed") } private suspend fun fetchFromServer(host: String): List { val url = "${serverUrls[currentServerIndex]}/httpdns/resolve?host=$host" val response = httpClient.get(url) val result = parseResponse(response.body) cacheManager.put(host, result.ips, result.ipsV6, result.ttl) return result.ips } private fun systemResolve(host: String): List { return try { InetAddress.getAllByName(host).map { it.hostAddress } } catch (e: Exception) { emptyList() } } private fun isBlacklisted(host: String): Boolean { return blacklist.any { pattern -> if (pattern.startsWith("*")) { host.endsWith(pattern.substring(1)) } else { host == pattern } } } enum class ResolveSource { CACHE, HTTPDNS, SYSTEM_DNS } data class ResolveResult(val ips: List, val source: ResolveSource) } ``` #### 3.4.5 监控上报模块 (Reporter) **上报指标**: - 解析成功率 - 解析耗时 - 缓存命中率 - 降级次数 **数据结构**: ```kotlin data class HTTPDNSStats( val host: String, val resolveCount: Int, val successCount: Int, val cacheHitCount: Int, val fallbackCount: Int, val avgLatencyMs: Long, val timestamp: Long ) ``` **Android 实现**: ```kotlin class HTTPDNSReporter(private val reportUrl: String) { private val stats = mutableMapOf() private val reportInterval = 60_000L // 60秒上报一次 data class MutableStats( var resolveCount: Int = 0, var successCount: Int = 0, var cacheHitCount: Int = 0, var fallbackCount: Int = 0, var totalLatencyMs: Long = 0 ) fun recordResolve(host: String, source: ResolveSource, latencyMs: Long, success: Boolean) { synchronized(stats) { val s = stats.getOrPut(host) { MutableStats() } s.resolveCount++ if (success) s.successCount++ s.totalLatencyMs += latencyMs when (source) { ResolveSource.CACHE -> s.cacheHitCount++ ResolveSource.SYSTEM_DNS -> s.fallbackCount++ else -> {} } } } fun startPeriodicReport() { CoroutineScope(Dispatchers.IO).launch { while (true) { delay(reportInterval) report() } } } private suspend fun report() { val snapshot = synchronized(stats) { val copy = stats.toMap() stats.clear() copy } if (snapshot.isEmpty()) return val reports = snapshot.map { (host, s) -> HTTPDNSStats( host = host, resolveCount = s.resolveCount, successCount = s.successCount, cacheHitCount = s.cacheHitCount, fallbackCount = s.fallbackCount, avgLatencyMs = if (s.resolveCount > 0) s.totalLatencyMs / s.resolveCount else 0, timestamp = System.currentTimeMillis() ) } try { httpClient.post(reportUrl) { contentType(ContentType.Application.Json) setBody(reports) } } catch (e: Exception) { Log.e("HTTPDNS", "Report failed: ${e.message}") } } } ``` #### 3.4.6 完整 SDK 集成示例 **Android 初始化**: ```kotlin class HTTPDNSManager private constructor(context: Context) { private val cacheManager = HTTPDNSCacheManager(context) private val networkMonitor = NetworkMonitor(context) private val resolver = HTTPDNSResolver( serverUrls = listOf( "https://httpdns1.goedge.cn", "https://httpdns2.goedge.cn" ), cacheManager = cacheManager ) private val signer = HTTPDNSSigner(appId = "ge_xxx", appSecret = "xxx") private val reporter = HTTPDNSReporter("https://api.goedge.cn/httpdns/report") companion object { @Volatile private var instance: HTTPDNSManager? = null fun init(context: Context): HTTPDNSManager { return instance ?: synchronized(this) { instance ?: HTTPDNSManager(context.applicationContext).also { instance = it } } } fun get(): HTTPDNSManager = instance ?: throw IllegalStateException("Must call init first") } init { // 监听网络变化 networkMonitor.onNetworkChanged = { Log.d("HTTPDNS", "Network changed, clearing cache") cacheManager.clear() } networkMonitor.start() // 启动监控上报 reporter.startPeriodicReport() } suspend fun resolve(host: String): List { val startTime = System.currentTimeMillis() val result = resolver.resolve(host) val latency = System.currentTimeMillis() - startTime reporter.recordResolve(host, result.source, latency, result.ips.isNotEmpty()) return result.ips } fun signRequest(request: Request): Request = signer.sign(request) /** * 预解析核心域名(App 启动时调用) */ suspend fun prefetch(hosts: List) { hosts.forEach { host -> launch { resolve(host) } } } } ``` **使用示例**: ```kotlin // Application.onCreate HTTPDNSManager.init(this) // 预解析 lifecycleScope.launch { HTTPDNSManager.get().prefetch(listOf( "api.example.com", "cdn.example.com", "img.example.com" )) } // 业务请求 lifecycleScope.launch { val ips = HTTPDNSManager.get().resolve("api.example.com") if (ips.isNotEmpty()) { val request = Request.Builder() .url("https://${ips[0]}/v1/user") .header("Host", "api.example.com") .build() val signedRequest = HTTPDNSManager.get().signRequest(request) // 发起请求... } } ``` --- ## 四、实施计划 ### 4.1 任务清单 | 阶段 | 任务 | 工时 | |------|------|------| | **Phase 1** | 数据库 + DAO + gRPC | 3天 | | **Phase 2** | Edge-DNS 接口 | 1天 | | **Phase 3** | Edge-Node WAF | 1天 | | **Phase 4** | Edge-Admin UI | 2天 | | **Phase 5** | SDK 核心 (解析+签名) | 1天 | | **Phase 6** | SDK 缓存模块 (内存+持久化+LRU+软过期) | 1天 | | **Phase 7** | SDK 网络感知 (切换监听+IPv6检测) | 0.5天 | | **Phase 8** | SDK 容错降级 (多节点重试+系统DNS降级) | 0.5天 | | **Phase 9** | SDK 监控上报 | 0.5天 | | **Phase 10** | SDK 集成测试 + 文档 | 0.5天 | | **总计** | | **11天** | ### 4.2 文件变更清单 | 操作 | 文件路径 | |------|----------| | 新建 | `EdgeDNS/internal/nodes/httpdns.go` | | 修改 | `EdgeDNS/internal/nodes/server.go` | | 新建 | `EdgeNode/internal/waf/checkpoints/checkpoint_httpdns_fingerprint.go` | | 修改 | `EdgeNode/internal/waf/checkpoints/init.go` | | 新建 | `EdgeAPI/internal/db/models/httpdns_app_dao.go` | | 新建 | `EdgeAPI/internal/rpc/services/service_httpdns_app.go` | | 新建 | `EdgeCommon/pkg/rpc/protos/service_httpdns_app.proto` | | 新建 | `EdgeAdmin/internal/web/actions/httpdns/*.go` | | 新建 | `EdgeAdmin/web/views/httpdns/apps/*.html` | --- ## 五、测试验证 ### 5.1 服务端测试 ```bash # 1. 测试 HTTPDNS 解析 curl "https://httpdns.example.com/httpdns/resolve?host=api.example.com" # 2. 测试无签名访问业务(应被拦截) curl "https://1.2.3.4/v1/user" -H "Host: api.example.com" # 3. 测试带签名访问 curl "https://1.2.3.4/v1/user" \ -H "Host: api.example.com" \ -H "X-GE-AppID: ge_abc123" \ -H "X-GE-Timestamp: 1700000000" \ -H "X-GE-Token: " ``` ### 5.2 SDK 测试矩阵 | 模块 | 测试场景 | 预期结果 | |------|----------|----------| | **缓存** | 首次解析 | 请求服务器,写入缓存 | | **缓存** | 缓存有效期内再次解析 | 直接返回缓存,不发请求 | | **缓存** | 缓存软过期 | 返回旧数据,后台异步刷新 | | **缓存** | 缓存完全过期 | 重新请求服务器 | | **缓存** | 缓存超过100条 | LRU 淘汰最旧条目 | | **网络感知** | WiFi → 4G 切换 | 清空所有缓存 | | **网络感知** | 4G → WiFi 切换 | 清空所有缓存 | | **网络感知** | IPv6 检测(双栈网络) | 返回 true | | **容错** | 主节点超时 | 自动切换备用节点 | | **容错** | 所有节点失败 | 降级到系统 DNS | | **容错** | 黑名单域名 | 直接走系统 DNS | | **监控** | 60 秒内多次解析 | 统计聚合后上报 | | **监控** | 上报失败 | 静默失败,不影响解析 | ### 5.3 Android 单元测试示例 ```kotlin @Test fun `cache hit returns immediately without network request`() = runTest { // Given cacheManager.put("api.example.com", listOf("1.2.3.4"), emptyList(), 600) // When val result = resolver.resolve("api.example.com") // Then assertEquals(listOf("1.2.3.4"), result.ips) assertEquals(ResolveSource.CACHE, result.source) verify(httpClient, never()).get(any()) } @Test fun `soft expired cache triggers background refresh`() = runTest { // Given: cache with TTL=1s, soft expire = TTL+60s cacheManager.put("api.example.com", listOf("1.2.3.4"), emptyList(), 1) advanceTimeBy(2000) // TTL expired but within soft expire // When val result = resolver.resolve("api.example.com") // Then assertEquals(listOf("1.2.3.4"), result.ips) // Returns stale data advanceUntilIdle() verify(httpClient).get(any()) // Background refresh triggered } @Test fun `network change clears cache`() = runTest { // Given cacheManager.put("api.example.com", listOf("1.2.3.4"), emptyList(), 600) // When networkMonitor.simulateNetworkChange() // Then val (cached, _) = cacheManager.get("api.example.com") assertNull(cached) } @Test fun `fallback to system DNS on timeout`() = runTest { // Given whenever(httpClient.get(any())).thenThrow(TimeoutException()) // When val result = resolver.resolve("api.example.com") // Then assertEquals(ResolveSource.SYSTEM_DNS, result.source) } ``` --- ## 六、上线清单 ### 6.1 服务端部署 - [ ] 数据库迁移(edgeHTTPDNSApps 表) - [ ] Edge-API 部署 - [ ] Edge-DNS 部署 - [ ] Edge-Node 部署(含 WAF 指纹校验) - [ ] Edge-Admin 部署(App 管理 UI) - [ ] 创建测试 App(获取 AppID/Secret) ### 6.2 SDK 发布 - [ ] Android SDK 单元测试通过 - [ ] iOS SDK 单元测试通过 - [ ] Android SDK 集成测试(真机) - [ ] iOS SDK 集成测试(真机) - [ ] SDK 打包发布(Maven/CocoaPods) - [ ] SDK 接入文档发布 ### 6.3 SDK 功能验收 - [ ] 缓存命中验证 - [ ] 软过期刷新验证 - [ ] LRU 淘汰验证 - [ ] 网络切换清缓存验证 - [ ] 多节点切换验证 - [ ] 系统 DNS 降级验证 - [ ] 监控数据上报验证 - [ ] 预解析功能验证