在云端 Mac 上建立 iOS IPv6-only 网络兼容性门禁

CI/CD 实践 ·约 8 分钟阅读

在云端 Mac 上建立 iOS IPv6-only 网络兼容性门禁

移动网络、企业 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。日志中不要记录令牌、请求正文或完整授权头。若要输出请求信息,只记录方法、经过脱敏的路径、状态码、重试次数和错误域。

排查顺序可以固定为:

  1. 确认环境预检是否通过,目标域名是否被系统解析。
  2. 检查失败请求是否使用域名,而不是解析后缓存的地址。
  3. 检查底层库是否限制为 AF_INET,或自行创建 IPv4 套接字。
  4. 对比首次连接与重连路径,确认两者使用同一解析策略。
  5. 在受控 DNS64/NAT64 网络的真机上复验关键流程。

模拟器门禁负责高频回归,真机负责发布前确认。两者职责分开后,日常构建不必依赖人工操作,网络团队也能根据归档证据判断问题位于解析、连接还是业务重试层。VPSGit 上的执行节点只需保持工具链、测试计划与环境变量一致,就能让这套检查持续重复,而不是等用户报告后再临时搭建现场。

常见问题

应用使用域名,是否就一定兼容 IPv6-only 网络?

不一定。服务端必须能被 DNS64/NAT64 正确访问,客户端还不能缓存 IPv4 地址、强制 AF_INET,或绕过系统解析器自行拼接套接字地址。

IPv6-only 测试可以只在 iOS 模拟器中完成吗?

模拟器适合做持续回归和失败取证,但发布前仍应在受控的 DNS64/NAT64 网络中补一次真机验收,重点检查登录、上传、推送回调和长连接恢复。

独享物理节点

为下一次构建或推理任务配置云端 Mac

从三档 Apple Silicon 配置、六个节点和固定租期中选择,实际可用状态以控制台实时返回为准。

配置云端 Mac