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

1291 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<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 实现**
```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<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)
**上报指标**
- 解析成功率
- 解析耗时
- 缓存命中率
- 降级次数
**数据结构**
```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<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 初始化**
```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<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) }
}
}
}
```
**使用示例**
```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: <valid_signature>"
```
### 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 降级验证
- [ ] 监控数据上报验证
- [ ] 预解析功能验证