你在 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 校验后,可以直接允许网络连接吗?
不能。解析成功只说明地址文本有效;回环、私有、链路本地和映射地址仍需按业务策略判断,域名解析结果也要单独审核。