Files
waf-platform/HTTPDNS_技术实施方案.md
2026-02-10 19:30:44 +08:00

42 KiB
Raw Blame History

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 请求流程(带缓存与降级)

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 网络切换处理流程

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 响应格式

{
  "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 (新建)

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行附近

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 (新建)

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 数据库表

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 (新建)

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 实现

class HTTPDNSCacheManager(private val context: Context) {
    // 内存缓存 (LRU)
    private val memoryCache = object : LinkedHashMap<String, CacheEntry>(100, 0.75f, true) {
        override fun removeEldestEntry(eldest: Map.Entry<String, CacheEntry>): 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<String>,
        val ipsV6: List<String>,
        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<CacheEntry?, Boolean> {
        // 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<String>, ipsV6: List<String>, 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 实现

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 实现

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 实现

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 实现

class HTTPDNSResolver(
    private val serverUrls: List<String>,  // 多个服务节点
    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<String> {
        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<String> {
        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<String> {
        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<String>, val source: ResolveSource)
}

3.4.5 监控上报模块 (Reporter)

上报指标

  • 解析成功率
  • 解析耗时
  • 缓存命中率
  • 降级次数

数据结构

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 实现

class HTTPDNSReporter(private val reportUrl: String) {
    private val stats = mutableMapOf<String, MutableStats>()
    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 初始化

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<String> {
        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<String>) {
        hosts.forEach { host ->
            launch { resolve(host) }
        }
    }
}

使用示例

// 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 服务端测试

# 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: <valid_signature>"

5.2 SDK 测试矩阵

模块 测试场景 预期结果
缓存 首次解析 请求服务器,写入缓存
缓存 缓存有效期内再次解析 直接返回缓存,不发请求
缓存 缓存软过期 返回旧数据,后台异步刷新
缓存 缓存完全过期 重新请求服务器
缓存 缓存超过100条 LRU 淘汰最旧条目
网络感知 WiFi → 4G 切换 清空所有缓存
网络感知 4G → WiFi 切换 清空所有缓存
网络感知 IPv6 检测(双栈网络) 返回 true
容错 主节点超时 自动切换备用节点
容错 所有节点失败 降级到系统 DNS
容错 黑名单域名 直接走系统 DNS
监控 60 秒内多次解析 统计聚合后上报
监控 上报失败 静默失败,不影响解析

5.3 Android 单元测试示例

@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 降级验证
  • 监控数据上报验证
  • 预解析功能验证