行動網路、企業 Wi-Fi 或電信商測試環境切換成 IPv6-only 後,最常見的問題通常不是「完全無法連線」,而是登入成功卻無法上傳圖片、第一次請求正常但重新連線時逾時,或某個舊介面仍在設定中寫死 IPv4 位址。雲端 Mac 適合把這些偶發問題轉成固定的 CI 門檻:每次合併都掃描位址字面值、確認 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 上的執行節點只要維持工具鏈、測試計畫與環境變數一致,就能持續重複執行這套檢查,而不必等到使用者回報後才臨時重建現場。
常見問題
App 只使用網域名稱,就一定相容 IPv6-only 網路嗎?
不一定。服務端必須能透過 DNS64/NAT64 存取,App 也不能快取 IPv4 位址、強制 AF_INET,或繞過系統解析器自行建立位址。
IPv6-only 測試只用 iOS 模擬器就足夠嗎?
模擬器適合持續回歸,但發布前仍應在受控 DNS64/NAT64 網路中進行一次實機驗收,涵蓋登入、上傳、回呼與長連線恢復。
為下一次建置或推論任務設定雲端 Mac
從三種 Apple Silicon 設定、六個節點與固定租期中選擇,實際可用狀態以控制台即時回傳為準。