你在 Windows 上改好連線設定,交給雲端 Mac 驗收,卻發現同一台裝置的 IPv6 位址被當成兩個目標:一處完整寫出零區段,另一處則用了雙冒號。如果連線清單、去重邏輯與存取規則各自比較原始字串,判定結果就會不一致。以下使用 Swift 與系統解析函式建立一套離線檢查,不需要實際伺服器,也不受網路是否暢通影響。
先界定輸入範圍
本例只接受 IP 位址字面值,不接受網域名稱、連接埠或完整 URL。IPv4 使用四段十進位表示法;IPv6 則接受系統支援的標準表示法。輸入前後若有空白,一律拒絕,不在背後自動移除,以免介面顯示的值與實際連線使用的值不一致。
IPv6 的方括號屬於 URL 的主機表示法,不是位址本身的一部分;%en0 這類範圍識別碼也不交由本例的解析器處理。若需要連線至鏈路本地位址,應在另一層明確接收位址與介面資訊,不能隨手截掉百分號及其後內容就繼續連線。
解析成功只代表這段文字能表示一個位址,不代表允許存取該位址,也不代表對應的裝置在線上。
範例中的文件專用位址僅供本機解析使用,指令碼不會向這些位址發起連線。
用位址族與位元組儲存結果
在雲端 Mac 的專案目錄建立 ip-contract.swift。先確認 xcrun swiftc --version 能輸出編譯器資訊,再寫入以下程式碼。這裡不使用可能解析主機名稱的介面,避免將 DNS 行為混入字面值驗證。
import Darwin
import Foundation
struct IPAddress: Equatable {
let family: Int32
let bytes: [UInt8]
}
func parseIP(_ text: String) -> IPAddress? {
guard !text.isEmpty,
text.utf8.count <= 45,
!text.utf8.contains(0) else {
return nil
}
var v4 = in_addr()
let result4 = text.withCString {
inet_pton(AF_INET, $0, &v4)
}
if result4 == 1 {
return IPAddress(
family: AF_INET,
bytes: withUnsafeBytes(of: &v4) { Array($0) }
)
}
var v6 = in6_addr()
let result6 = text.withCString {
inet_pton(AF_INET6, $0, &v6)
}
if result6 == 1 {
return IPAddress(
family: AF_INET6,
bytes: withUnsafeBytes(of: &v6) { Array($0) }
)
}
return nil
}
func require(_ condition: Bool, _ message: String) {
if !condition {
fatalError(message)
}
}
let good = [
"192.0.2.1",
"2001:db8::1",
"2001:0DB8:0000:0000:0000:0000:0000:0001",
"::",
"::1",
"::ffff:192.0.2.1"
]
for text in good {
require(parseIP(text) != nil, "Rejected valid literal")
}
let bad = [
"",
" 192.0.2.1",
"192.0.2.1\n",
"192.0.2.256",
"192.0.2",
"192.0.02.1",
"0xC0000201",
"[2001:db8::1]",
"fe80::1%en0",
"2001:::1",
"192.0.2.1\u{0}ignored"
]
for text in bad {
require(parseIP(text) == nil, "Accepted invalid literal")
}
let short = parseIP("2001:db8::1")!
let expanded = parseIP(
"2001:0DB8:0000:0000:0000:0000:0000:0001"
)!
require(short == expanded, "Equivalent IPv6 mismatch")
require(short.bytes.count == 16, "Invalid IPv6 length")
let v4 = parseIP("192.0.2.1")!
let mapped = parseIP("::ffff:192.0.2.1")!
require(v4.bytes.count == 4, "Invalid IPv4 length")
require(v4 != mapped, "Address families collapsed")
長度上限是這份解析契約設定的位元組額度,不是適用於所有帶範圍識別碼位址的通用限制。位址的位元組直接取自系統結構體,保留網路位元組順序;不要再將整塊資料解讀為本機整數來比較。
編譯執行,再確認失敗時的行為
將指令碼編譯成獨立程式,既方便納入建置工作,也能避免不同啟動方式影響直譯執行:
set -eu
mkdir -p .ip-check
xcrun swiftc ip-contract.swift -o .ip-check/ip-contract
.ip-check/ip-contract
正常通過時不會有任何輸出,結束碼為零;只要任一契約檢查失敗,就會以非零狀態結束。這裡使用明確的檢查,而不是 assert,因為最佳化設定可能移除斷言。
先暫時將等價比較改成不等,確認工作確實失敗,再還原。這項負向演練能找出管線忽略結束碼、只儲存日誌卻仍顯示成功的問題。不要只確認指令碼「跑過一次」。
將正規化與存取政策分開
不要任意擴大等價關係
同一個 IPv6 位址的壓縮寫法與完整寫法應判定為相等,但 IPv4 與 IPv4 映射 IPv6 在這個模型中仍屬於不同位址族。如果業務需求要將兩者歸為同一個邏輯目標,應新增明確的轉換,並測試轉換前後的存取規則,不能靠刪除字串前綴來實作。
顯示時可以使用 inet_ntop 輸出統一的文字格式;去重與授權則繼續使用解析後的模型。不要將某個系統版本輸出的字串形式,視為跨版本永久不變的儲存協定。
網域名稱需要獨立審核
網域名稱無法通過這個函式,不代表網域名稱無效,只代表它不屬於這個輸入型別。若產品也支援網域名稱,應提供獨立入口,並在解析後檢查實際準備連線的位址集合。只檢查使用者輸入的主機文字,無法涵蓋解析結果的變動。
迴路、私有、鏈路本地等位址的允許範圍,應由產品用途決定。裝置除錯工具與公用網路資源擷取服務所需的規則不同,不宜共用一個意義模糊的「安全位址」布林值。
將失敗案例保留為迴歸測試資產
將解析器移入共用原始碼後,分別在 macOS 命令列檢查與 iOS 測試目標中執行同一組測試向量。前者適合在遠端環境快速找出問題,後者負責確認目標系統的行為;不能以主機上的通過結果取代目標平台驗收。
每次修正都要補上原始輸入、預期位址族與預期結果。對於內嵌的空位元組,尤其要保留原始位元組,因為日誌顯示時可能會將它隱藏。C 字串會在空位元組處結束,若缺少入口檢查,就可能只驗證到前面一段有效的內容。
最後確認三件事:不同文字能否歸為同一位址、不同位址族是否維持界線、無效輸入是否在連線前失敗。只要這些契約穩定,後續新增介面驗證或連線政策時,就有一組可以重複執行的驗證依據。
常見問題
IPv6 字串不同,就表示位址不同嗎?
不是。零段壓縮、前導零與十六進位字母大小寫都會改變表示方式。應比較解析後的位址族與網路位元組序資料。
通過 inet_pton 驗證後,可以直接允許連線嗎?
不可以。解析只確認文字有效;回環、私有、鏈路本地及映射位址仍須依政策判斷,網域名稱解析結果也要另外檢查。