✍️ 引言
在开发面向全球或特定复杂网络环境的 App(如 XXX、跨境电商、海外加速等)时,最大的痛点往往不是业务逻辑,而是服务端的生存能力。为了对抗域名污染 (DNS Poisoning) 、SNI 阻断 以及 证书审查,我们通常需要一套极其灵活的「备用链路」与「动态发现」机制。
本文将结合在 iOS/Swift 项目中的实际落地经验,深度剖析一套基于 DNS TXT 记录 派发动态入口域名、双向 mTLS 证书(p12)基码 以及 原生 TCP 直连 IP 的高可用架构,并详解其间的技术难点与避坑指南。
🛠 一、 核心架构设计
我们的目标是:哪怕主 Base 域名完全死锁,客户端只要能向公用 DNS 发一个查询,就能满血复活。
1. 数据如何藏在 DNS TXT 里?
由于一台域名的 A记录 只能存 IP,且极其容易被封锁,我们选择将配置加密后塞入 DNS 的 TXT 记录。 我们使用了多级子域名来承载不同的模块(由于 TXT 字符长度限制,需要分片):
| 子域名 (Subdomain) | 承载内容特征 | 安全措施 |
|---|---|---|
root.yourbase.com | 加密后的后备 HTTPS 业务 API 域名列表 | AES-128-ECB 加密 + Base64 |
1.yourbase.com | mTLS 客户端证书 P12 文件的 Base64 前半段 | 纯文本分片拼装 |
2.yourbase.com | mTLS 客户端证书 P12 文件的 Base64 后半段 | 纯文本分片拼装 |
ip.yourbase.com | 绕过 SNI 审查的裸 TCP 直连 IP 点对点通道 | 纯文本 |
🧠 二、 技术难点与避坑指南
难点 1:iOS 系统 API 无法直接发起原生 UDP DNS 查询
🚨 问题背景: iOS 的 getaddrinfo 或者 NWHostResolver 是高层级 API,它们往往只返回处理好的 IP 地址(A/AAAA 记录),极难直接读取到 TXT、SRV 记录。如果调用系统的 res_nquery(属于 C 层的 libresolv),在弱网下容易造成线程死锁,且容易触发 iOS 严格的后台审计。
💡 解决方案:使用 Network 框架手工构建 UDP 53 端口查询 我们在 Swift 中封装了一个 DNSResolver,通过 NWConnection(to: 53, using: .udp) 手工下发标准 DNS 报文(RFC 1035) 。
-
构造 DNS 查询帧:
swift var data = Data() let id = UInt16.random(in: 1...65535) data.append(contentsOf: id.bigEndianBytes) data.append(contentsOf: UInt16(0x0100).bigEndianBytes) // Flags: 标准查询 data.append(contentsOf: UInt16(1).bigEndianBytes) // Question 数量 1 // ... 拼接子域名 QNAME、QTYPE 为 16 (TXT) -
并发查询优化: 由于国内 DNS 偶尔会有运营商后门或缓存污染,我们使用
withTaskGroup并发地向四个公共 DNS 服务器发送请求 (223.5.5.5,114.114.114.114,8.8.8.8,1.1.1.1),谁最快返回合法的 TXT 内容,就直接cancelAll()结束任务。
难点 2:UDP 的截断陷阱 (Truncated) 与 TCP 回退
🚨 问题背景: 由于拼装了庞大的客户端 p12 证书 Base64 字符串,TXT 记录往往会合在一起超过 512 字节。 在标准的 DNS UDP 查询中,如果响应超过 512 字节,包头部的 TC (Truncated) 标志位会被置为 1,代表数据被截断。
💡 解决方案:标志位侦测与 TCP Fallback 我们在 UDP 接收处做了一层守卫:
swift
if (data[2] & 0x02) != 0 { // TC Flag is set!
// UDP 遭遇截断,降级使用 TCP 53 端口进行可靠全量查询
return await queryTCP(domain: domain, server: server)
}
进入 queryTCP 时,会在帧最前面补上 2 字节的大端序长度头,直接利用 NWConnection.tcp 握手拿到绝对完整的几千字节 TXT 加密串,完美解决大文件丢失问题。
难点 3:防劫持的 “端到端解密” 校验
🚨 问题背景: 如果中间人(Mitm)故意把你的 TXT 记录篡改成钓鱼网站或错误信息,即便配置下发了,APP 也会崩溃或中招。
💡 解决方案:AES + TCP 握手活性测试
-
对称加密:对
root的分流域名进行AES-128-ECB加密。中间人即使拿到了,没有客户端的硬编码 Key 也无法篡改。 -
TCP 通信握手探测活性: 在真正切换配置前,Manager 会多跑一遍
tcpTest。由于有些域名可能已经“挂了”,客户端会在后台静默并发跑:swift let connection = NWConnection(to: host, using: .tcp) connection.stateUpdateHandler = { state in if state == .ready { finish(true) } // 代表服务器可通达,不是死域名 }
难点 4:动态 mTLS 证书灌入 (Security Manager)
经过 AES 解密和两片 TXT (1.txt + 2.txt) 拼装后,我们得到了完整的证书 Base64 编码。 我们要实实现本地无感知实例化,不需要把证书文件落地写死到沙盒里(防止反编译静态检查):
- 直接在内存中将组合好的 Base64 数据转为
Data。 - 使用
SecPKCS12Import函数,并将空密码(或者约定的暗号)传入,从内存里动态吐出SecIdentity和关联的SecCertificate。 - 把 Identity 灌入全局
SessionDelegate。当走 HTTPS 握手时,若触发.clientCertificate的URLAuthenticationChallenge,直接从 cache 提取该 Identity 给系统使用。
难点 5:SNI 阻断应急方案 —— 18字节头部纯裸 TCP 定制通道
对于国内在极限阻断(如 SNI 嗅探)下的特殊业务,HTTPS 甚至会被阻断。我们追加了 ip.yourbase.com 提取裸 IP:
- 业务无感降级:当 HTTPS 全灭,
NetworkChannelManager自动引导流量降级到我们自己用原生NWConnection敲出来的裸 TCP 直连。 - 自定义封包协议:由于对端没有 TLS 证书做阻断,我们在应用层通过自研非对称二进制报头(
[18字节头部][Path][Hdr][Body]及 响应14字节头部)在服务端和客户端穿梭自如,极大增强了业务的可达率骨干。
📈 三、 业务安全成效
通过这套机制的上线,我们成功做到了:
- 云端无感知脱壳切换:后台可随意增减高防域名、甚至随时全量更替 TLS 的客户端校验私钥,对老版本客户端保持完美兼容。
- 零阻断时长:冷启动到成功跑通业务 HTTPS 的时间通过 TaskGroup 的竞赛机制下降到了 平均 0.3 秒以内。
💡 总结
服务高防链路的最佳伴侣不是冗余服务器,而是灵活、弹性的 发现机制。 利用 DNS 53 这个处于网络信任基座的协议,将 分片、加密数据 优雅地回传至 iOS 客户端并发解码,不仅安全可靠,更筑起了一道无法轻易折断的强硬长廊。
提示: 在使用 114 / 223 等大陆 DNS 查询时,注意频率控制以心跳避免被运营商拉入恶意解析黑名单。对于更深层的防污染,甚至可搭配 DNS over HTTPS (DoH) 来取代 53 端口查询。