42 KiB
42 KiB
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)
策略:
- 多节点轮询:主节点失败自动切备用
- 超时降级:HTTPDNS 超时自动降级到系统 DNS
- 黑名单机制:特定域名强制走系统 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 降级验证
- 监控数据上报验证
- 预解析功能验证