移动网络、企业 Wi-Fi 或运营商测试环境切到 IPv6-only 后,最常见的故障并不是“完全断网”,而是登录成功但图片上传失败、首个请求正常而重连超时,或者某个旧接口仍把 IPv4 地址写死在配置里。云端 Mac 适合把这些偶发问题变成固定门禁:每次合并都扫描地址字面量、核对 DNS64 解析条件、运行网络测试,并归档失败现场。
先定义门禁边界
IPv6-only 兼容不等于服务端必须只提供 AAAA 记录。只要测试网络具备 DNS64/NAT64,使用域名并交给系统解析器处理的客户端,仍可访问只提供 IPv4 的服务。真正危险的是客户端绕过系统解析流程,例如缓存解析后的 IPv4、强制使用 AF_INET,或把 192.0.2.10 一类地址写进业务配置。
建议把门禁拆成三层:
| 层级 | 检查内容 | 失败时保留 |
|---|---|---|
| 静态检查 | IPv4 字面量、地址族常量、手写套接字 | 文件名与行号 |
| 环境预检 | DNS、默认路由、目标域名解析 | scutil 与解析结果 |
| 行为测试 | 登录、上传、重试、长连接恢复 | 测试日志与结果包 |
不要用
ping成功代替业务验收。应用真实使用的是 HTTPS、WebSocket 或文件上传,请求路径、超时策略和连接复用都可能产生不同结果。
扫描仓库中的 IPv4 假设
先在 CI 工作区执行保守扫描。下面的表达式只负责找候选项,不应直接把所有命中都判为缺陷,因为测试夹具、文档和回环地址可能是合法内容。
set -euo pipefail
mkdir -p artifacts/ipv6
rg -n \
--glob '*.{swift,m,mm,h,plist,json,yaml,yml,xcconfig}' \
'([0-9]{1,3}\.){3}[0-9]{1,3}|AF_INET([^6]|$)|sockaddr_in([^6]|$)' \
. | tee artifacts/ipv6/ipv4-candidates.txt
if rg -n \
--glob 'Sources/**' \
--glob 'Config/**' \
'https?://([0-9]{1,3}\.){3}[0-9]{1,3}' .; then
echo "Production configuration contains an IPv4 URL" >&2
exit 1
fi
候选清单要人工分类一次,再把允许项放进有注释的白名单。不要直接排除整个 Tests 目录,因为测试辅助代码同样可能复制到正式网络层。对于 C 或 Objective-C 封装,重点检查 getaddrinfo 的 ai_family 是否使用 AF_UNSPEC;只有明确要求单一地址族时才固定它。
避免误修
不要把 IPv4 地址机械改写成 IPv4-mapped IPv6,也不要自行拼接 NAT64 前缀。前缀由当前网络决定,硬编码后换一个网络就会失效。正确做法是保存域名,在发起连接时让系统解析器返回当前可用地址。
把 DNS64 条件做成前置检查
测试开始前先记录网络状态,避免环境本身不满足条件时产生一批误报。云端 Mac 的执行节点应使用团队已准备好的受控 DNS64/NAT64 网络;CI 脚本只验证条件,不在任务中临时改系统网络配置。
set -euo pipefail
OUT="artifacts/ipv6"
mkdir -p "$OUT"
scutil --dns > "$OUT/scutil-dns.txt"
route -n get default > "$OUT/default-route.txt" 2>&1 || true
dscacheutil -q host -a name api.test.example \
> "$OUT/target-resolution.txt"
if ! grep -Eq 'ipv6_address|ip_address' "$OUT/target-resolution.txt"; then
echo "Target hostname did not resolve in the test environment" >&2
exit 2
fi
测试域名应属于团队自己的受控环境,且返回稳定、无敏感信息的健康检查响应。不要拿公共网页作为门禁依赖,否则外部限流、证书变更或区域策略都会让构建结果失去确定性。
用 URLSession 验证真实行为
网络测试应覆盖“首次成功”和“失败后恢复”两条路径。请求必须设置有限超时,并通过依赖注入传入基础 URL,避免测试代码再次写死地址。
import XCTest
final class IPv6ReadinessTests: XCTestCase {
func testHealthEndpointReturnsExpectedStatus() async throws {
let value = try XCTUnwrap(
ProcessInfo.processInfo.environment["TEST_BASE_URL"]
)
let baseURL = try XCTUnwrap(URL(string: value))
let url = baseURL.appending(path: "health")
let configuration = URLSessionConfiguration.ephemeral
configuration.timeoutIntervalForRequest = 15
configuration.timeoutIntervalForResource = 30
let session = URLSession(configuration: configuration)
let (_, response) = try await session.data(from: url)
let http = try XCTUnwrap(response as? HTTPURLResponse)
XCTAssertEqual(http.statusCode, 200)
}
}
执行时固定测试计划、模拟器型号与结果目录:
set -o pipefail
xcodebuild test \
-workspace App.xcworkspace \
-scheme AppNetworkTests \
-testPlan IPv6Readiness \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-resultBundlePath artifacts/ipv6/IPv6Readiness.xcresult \
TEST_BASE_URL='https://api.test.example/' \
| tee artifacts/ipv6/xcodebuild.log
如果业务包含上传和长连接,再增加小文件上传、连接中断后的指数退避、前后台切换后的恢复测试。断言应面向业务结果,不要依赖本地化错误文案。
失败时保留可诊断证据
门禁失败后,至少保留静态扫描结果、DNS 状态、目标解析结果、xcodebuild 日志和 xcresult。日志中不要记录令牌、请求正文或完整授权头。若要输出请求信息,只记录方法、经过脱敏的路径、状态码、重试次数和错误域。
排查顺序可以固定为:
- 确认环境预检是否通过,目标域名是否被系统解析。
- 检查失败请求是否使用域名,而不是解析后缓存的地址。
- 检查底层库是否限制为
AF_INET,或自行创建 IPv4 套接字。 - 对比首次连接与重连路径,确认两者使用同一解析策略。
- 在受控 DNS64/NAT64 网络的真机上复验关键流程。
模拟器门禁负责高频回归,真机负责发布前确认。两者职责分开后,日常构建不必依赖人工操作,网络团队也能根据归档证据判断问题位于解析、连接还是业务重试层。VPSGit 上的执行节点只需保持工具链、测试计划与环境变量一致,就能让这套检查持续重复,而不是等用户报告后再临时搭建现场。
常见问题
应用使用域名,是否就一定兼容 IPv6-only 网络?
不一定。服务端必须能被 DNS64/NAT64 正确访问,客户端还不能缓存 IPv4 地址、强制 AF_INET,或绕过系统解析器自行拼接套接字地址。
IPv6-only 测试可以只在 iOS 模拟器中完成吗?
模拟器适合做持续回归和失败取证,但发布前仍应在受控的 DNS64/NAT64 网络中补一次真机验收,重点检查登录、上传、推送回调和长连接恢复。
为下一次构建或推理任务配置云端 Mac
从三档 Apple Silicon 配置、六个节点和固定租期中选择,实际可用状态以控制台实时返回为准。