mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-10-29 21:53:43 +00:00
feat(app): annotate and filter IPv6 command addresses by kernel flags for #72
经 SIOCGIFAFLAG_IN6 逐地址查询内核 IPv6 标志(与 ifconfig 的注记同源, Darwin 模块不导出该 ioctl 请求宏,手工展开 _IOWR 编码): - IN6_IFF_TEMPORARY(RFC 4941 隐私临时地址,会轮换)不进候选 - 其余 IPv6 候选在标签 IP 后附类型注记(位于中段): autoconf secured / autoconf / dynamic;标志查询失败不注记也不过滤 - 注记只进标签不进命令;地址身份仍为 BSD 接口名 + 规范化 IP 真实主机非视觉验证扩展:每 IPv6 地址标志查询非空、候选注记属 ifconfig 同源闭集、临时地址被过滤;替身用例覆盖 temporary 过滤与标签格式。
This commit is contained in:
+1
-1
@@ -32,7 +32,7 @@
|
||||
- **Listener mode**: The user-facing choice that determines which address families the local proxy listeners accept: this Mac only (IPv4 loopback), all IPv4 interfaces, all IPv4 and IPv6 interfaces, or all IPv6 interfaces only. One mode applies to both the local SOCKS and HTTP proxy inbounds. _Avoid_: Listen scope.
|
||||
- **HTTP proxy environment command**: An HTTP-only terminal command that sets lowercase `http_proxy` and `https_proxy` for this Mac's local HTTP proxy inbound. It always targets a loopback address for the selected listener mode.
|
||||
- **Terminal proxy environment commands**: Shell-specific commands for zsh/bash and fish that set lowercase `http_proxy` and `https_proxy` through the local HTTP inbound, `all_proxy` through the local SOCKS inbound, and `no_proxy` for `localhost`, `127.0.0.1`, `::1`, and `.local` hostnames. The proxy endpoints use the terminal command address (defaulting to a compatible loopback address), and the commands affect child processes that honor these variables.
|
||||
- **Terminal command address**: The user-selected IP address used by both HTTP and SOCKS endpoints in copied terminal proxy environment commands. It defaults to a compatible loopback address; non-local listener modes also offer addresses of enabled Wi-Fi and Ethernet interfaces in the accepted address families, and the selection lasts only for the current app session without changing listener or system-proxy settings.
|
||||
- **Terminal command address**: The user-selected IP address used by both HTTP and SOCKS endpoints in copied terminal proxy environment commands. It defaults to a compatible loopback address; non-local listener modes also offer addresses of enabled Wi-Fi and Ethernet interfaces in the accepted address families, excluding rotating temporary addresses, and the selection lasts only for the current app session without changing listener or system-proxy settings.
|
||||
- **Proxy runtime**: The per-user background host process, independent of the GUI's lifetime, that runs the external tunnel service and loads the optional generated ACL sidecar.
|
||||
- **Legacy configuration**: The server list and preferences persisted by the frozen implementation in `Legacy/`.
|
||||
- **Legacy import**: The user-confirmed, read-only translation of a Legacy configuration's server records into a separate manual group in 2.0. It migrates no preferences, activates no target, and neither starts proxying nor alters the Legacy source.
|
||||
|
||||
@@ -3,8 +3,22 @@ import Foundation
|
||||
|
||||
// MARK: - 原始接口事实(issue #72)
|
||||
|
||||
/// 单个本机接口单播地址的原始事实:族与 IPv6 链路本地标记已判明,过滤、
|
||||
/// 排序与取舍全部由工作流层策略完成。
|
||||
/// IPv6 地址的内核标志(经 `SIOCGIFAFLAG_IN6` 逐地址查询,netinet6/in6_var.h
|
||||
/// 的 `IN6_IFF_*` 子集)。仅 IPv6 单播地址携带;IPv4 或查询失败为 nil。
|
||||
struct LocalInterfaceV6Flags: OptionSet, Sendable {
|
||||
let rawValue: Int32
|
||||
/// `IN6_IFF_AUTOCONF`:SLAAC 自动配置地址。
|
||||
static let autoconf = Self(rawValue: IN6_IFF_AUTOCONF)
|
||||
/// `IN6_IFF_TEMPORARY`:RFC 4941 隐私临时地址(会轮换)。
|
||||
static let temporary = Self(rawValue: IN6_IFF_TEMPORARY)
|
||||
/// `IN6_IFF_SECURED`:稳定接口标识生成的地址。
|
||||
static let secured = Self(rawValue: IN6_IFF_SECURED)
|
||||
/// `IN6_IFF_DYNAMIC`:DHCPv6 分配的地址。
|
||||
static let dynamic = Self(rawValue: IN6_IFF_DYNAMIC)
|
||||
}
|
||||
|
||||
/// 单个本机接口单播地址的原始事实:族与 IPv6 标记已判明,过滤、排序与
|
||||
/// 取舍全部由工作流层策略完成。
|
||||
struct LocalInterfaceAddress: Equatable, Sendable {
|
||||
enum Family: Equatable, Sendable {
|
||||
case ipv4
|
||||
@@ -17,6 +31,9 @@ struct LocalInterfaceAddress: Equatable, Sendable {
|
||||
/// IPv6 链路本地(fe80::/10);IPv4 链路本地不置位(属于允许接口的单播
|
||||
/// 地址时可以列出)。
|
||||
let isIPv6LinkLocal: Bool
|
||||
/// IPv6 地址内核标志(含临时地址、DHCPv6 等类型事实);IPv4 或查询失败
|
||||
/// 为 nil——nil 不触发过滤,也不注记。
|
||||
let v6Flags: LocalInterfaceV6Flags?
|
||||
}
|
||||
|
||||
/// 单个本机接口的原始事实(issue #72):系统适配器逐接口提供,类型白名单、
|
||||
|
||||
@@ -61,6 +61,15 @@ final class SystemInterfaceFactsProvider: LocalInterfaceFactsReading {
|
||||
guard getifaddrs(&interfacesPtr) == 0, let first = interfacesPtr else { return nil }
|
||||
defer { freeifaddrs(first) }
|
||||
|
||||
// IPv6 地址标志查询共用一个 DGRAM socket;打开失败只失去标志事实
|
||||
// (nil = 不过滤、不注记),不阻断枚举。
|
||||
let flagsSocket = socket(AF_INET6, SOCK_DGRAM, 0)
|
||||
defer {
|
||||
if flagsSocket >= 0 {
|
||||
close(flagsSocket)
|
||||
}
|
||||
}
|
||||
|
||||
// 按首次出现顺序逐接口累积;同一接口的多个地址条目归并到同一事实。
|
||||
var order: [String] = []
|
||||
var loopbackByBSDName: [String: Bool] = [:]
|
||||
@@ -78,7 +87,7 @@ final class SystemInterfaceFactsProvider: LocalInterfaceFactsReading {
|
||||
order.append(bsdName)
|
||||
loopbackByBSDName[bsdName] = flags & IFF_LOOPBACK == IFF_LOOPBACK
|
||||
}
|
||||
if let address = interfaceAddress(socketAddress) {
|
||||
if let address = interfaceAddress(socketAddress, flagsSocket: flagsSocket, bsdName: bsdName) {
|
||||
addressesByBSDName[bsdName, default: []].append(address)
|
||||
}
|
||||
}
|
||||
@@ -97,7 +106,7 @@ final class SystemInterfaceFactsProvider: LocalInterfaceFactsReading {
|
||||
/// 单个地址条目的族与链路本地判定;非 IPv4/IPv6 条目(链路层等)或无法
|
||||
/// 规范化的地址为 nil。
|
||||
private static func interfaceAddress(
|
||||
_ socketAddress: UnsafeMutablePointer<sockaddr>
|
||||
_ socketAddress: UnsafeMutablePointer<sockaddr>, flagsSocket: Int32, bsdName: String
|
||||
) -> LocalInterfaceAddress? {
|
||||
switch socketAddress.pointee.sa_family {
|
||||
case sa_family_t(AF_INET):
|
||||
@@ -105,21 +114,56 @@ final class SystemInterfaceFactsProvider: LocalInterfaceFactsReading {
|
||||
$0.pointee
|
||||
}
|
||||
guard let text = addressText(family: AF_INET, address: socket.sin_addr) else { return nil }
|
||||
return LocalInterfaceAddress(address: text, family: .ipv4, isIPv6LinkLocal: false)
|
||||
return LocalInterfaceAddress(
|
||||
address: text, family: .ipv4, isIPv6LinkLocal: false, v6Flags: nil)
|
||||
case sa_family_t(AF_INET6):
|
||||
let socket = socketAddress.withMemoryRebound(to: sockaddr_in6.self, capacity: 1) {
|
||||
$0.pointee
|
||||
}
|
||||
guard let text = addressText(family: AF_INET6, address: socket.sin6_addr) else { return nil }
|
||||
let v6Flags =
|
||||
flagsSocket >= 0
|
||||
? v6AddressFlags(flagsSocket: flagsSocket, bsdName: bsdName, address: socketAddress)
|
||||
: nil
|
||||
return LocalInterfaceAddress(
|
||||
address: text,
|
||||
family: .ipv6,
|
||||
isIPv6LinkLocal: isIPv6LinkLocal(socket.sin6_addr))
|
||||
isIPv6LinkLocal: isIPv6LinkLocal(socket.sin6_addr),
|
||||
v6Flags: v6Flags)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// `SIOCGIFAFLAG_IN6` 逐地址查询内核 IPv6 标志(与 ifconfig 的
|
||||
/// 「autoconf secured / autoconf temporary / dynamic」注记同源)。查询
|
||||
/// 失败返回 nil。
|
||||
private static func v6AddressFlags(
|
||||
flagsSocket: Int32, bsdName: String, address: UnsafeMutablePointer<sockaddr>
|
||||
) -> LocalInterfaceV6Flags? {
|
||||
var request = in6_ifreq()
|
||||
// _IOWR('i', 73, struct in6_ifreq) 手工展开:Darwin 模块不导出带结构体
|
||||
// 参数的 ioctl 请求宏。
|
||||
let requestCode =
|
||||
UInt32(IOC_INOUT)
|
||||
| (UInt32(MemoryLayout<in6_ifreq>.size) & UInt32(IOCPARM_MASK)) << 16
|
||||
| UInt32(0x69) << 8 | 73
|
||||
withUnsafeMutableBytes(of: &request.ifr_name) { nameBuffer in
|
||||
_ = strlcpy(
|
||||
nameBuffer.baseAddress!.assumingMemoryBound(to: CChar.self), bsdName, Int(IFNAMSIZ))
|
||||
}
|
||||
address.withMemoryRebound(to: sockaddr_in6.self, capacity: 1) { sin6 in
|
||||
request.ifr_ifru.ifru_addr = sin6.pointee
|
||||
}
|
||||
guard ioctl(flagsSocket, UInt(requestCode), &request) == 0 else { return nil }
|
||||
// ifru_flags 与 ifru_addr 在联合上重叠:内核回写在偏移 IFNAMSIZ 的首
|
||||
// 4 字节,直接按位读取(不依赖 Swift 对 C 联合成员的重叠读写语义)。
|
||||
let rawFlags = withUnsafeBytes(of: &request) {
|
||||
$0.loadUnaligned(fromByteOffset: Int(IFNAMSIZ), as: Int32.self)
|
||||
}
|
||||
return LocalInterfaceV6Flags(rawValue: rawFlags)
|
||||
}
|
||||
|
||||
/// inet_ntop 规范化呈现形;转换失败不产生候选地址。
|
||||
private static func addressText<T>(family: Int32, address: T) -> String? {
|
||||
var buffer = [CChar](repeating: 0, count: Int(INET6_ADDRSTRLEN))
|
||||
|
||||
@@ -4,23 +4,36 @@ import SystemConfiguration
|
||||
// MARK: - 命令地址投影(issue #72)
|
||||
|
||||
/// 首页命令地址候选(CONTEXT.md「Terminal command address」)。身份 = BSD
|
||||
/// 接口身份 + 规范化 IP;本地化名称仅用于呈现,不参与身份。`Hashable` 供
|
||||
/// SwiftUI Picker 标签匹配使用,选择校验一律走 `identity`。
|
||||
/// 接口身份 + 规范化 IP;显示名与类型注记仅用于呈现,不参与身份。`Hashable`
|
||||
/// 供 SwiftUI Picker 标签匹配使用,选择校验一律走 `identity`。
|
||||
struct TerminalCommandAddress: Equatable, Hashable, Sendable {
|
||||
/// BSD 接口名;回环固定为 lo0。
|
||||
let bsdName: String
|
||||
/// 规范化 IP 文本。
|
||||
let address: String
|
||||
/// `{IP} - {name}` 中的名称:系统本地化接口名称,缺失回退 BSD 名。
|
||||
/// 系统本地化接口名称,缺失回退 BSD 名;回环显示 lo0。
|
||||
let displayName: String
|
||||
/// IPv6 地址类型注记(与 ifconfig 同源:autoconf secured / autoconf /
|
||||
/// dynamic);IPv4 或标志缺失为 nil。
|
||||
let annotation: String?
|
||||
|
||||
/// 跨刷新匹配既有选择的身份(不含显示名;显示名变化不使选择失效)。
|
||||
init(bsdName: String, address: String, displayName: String, annotation: String? = nil) {
|
||||
self.bsdName = bsdName
|
||||
self.address = address
|
||||
self.displayName = displayName
|
||||
self.annotation = annotation
|
||||
}
|
||||
|
||||
/// 跨刷新匹配既有选择的身份(不含显示名与注记;两者变化不使选择失效)。
|
||||
var identity: TerminalCommandAddressIdentity {
|
||||
TerminalCommandAddressIdentity(bsdName: bsdName, address: address)
|
||||
}
|
||||
|
||||
/// 候选标签:`{IP} - {name}`。
|
||||
var label: String { "\(address) - \(displayName)" }
|
||||
/// 候选标签:`{IP} - {name}`;带类型注记的 IPv6 为 `{IP} ({注记}) - {name}`。
|
||||
var label: String {
|
||||
let addressPart = annotation.map { "\(address) (\($0))" } ?? address
|
||||
return "\(addressPart) - \(displayName)"
|
||||
}
|
||||
}
|
||||
|
||||
/// 命令地址身份:BSD 接口名 + 规范化 IP。
|
||||
@@ -88,7 +101,7 @@ enum TerminalCommandAddressPolicy {
|
||||
/// 当前监听方式的默认回环地址(Domain 回环清单首项)。
|
||||
static func defaultLoopback(for mode: ListenerMode) -> TerminalCommandAddress {
|
||||
TerminalCommandAddress(
|
||||
bsdName: "lo0", address: mode.proxyLoopbackAddress, displayName: "lo0")
|
||||
bsdName: "lo0", address: mode.proxyLoopbackAddress, displayName: "lo0", annotation: nil)
|
||||
}
|
||||
|
||||
// MARK: - 派生细节
|
||||
@@ -96,7 +109,7 @@ enum TerminalCommandAddressPolicy {
|
||||
/// 回环候选(置顶)直接来自 Domain 的监听方式回环清单,展示名固定 lo0。
|
||||
private static func loopbackCandidates(for mode: ListenerMode) -> [TerminalCommandAddress] {
|
||||
mode.compatibleLoopbackAddresses.map {
|
||||
TerminalCommandAddress(bsdName: "lo0", address: $0, displayName: "lo0")
|
||||
TerminalCommandAddress(bsdName: "lo0", address: $0, displayName: "lo0", annotation: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +143,11 @@ enum TerminalCommandAddressPolicy {
|
||||
return lhs.address < rhs.address
|
||||
}
|
||||
.map {
|
||||
TerminalCommandAddress(bsdName: interface.bsdName, address: $0.address, displayName: name)
|
||||
TerminalCommandAddress(
|
||||
bsdName: interface.bsdName,
|
||||
address: $0.address,
|
||||
displayName: name,
|
||||
annotation: annotation(for: $0))
|
||||
}
|
||||
if !addresses.isEmpty {
|
||||
groups.append(InterfaceGroup(name: name, bsdName: interface.bsdName, addresses: addresses))
|
||||
@@ -154,6 +171,8 @@ enum TerminalCommandAddressPolicy {
|
||||
guard !wildcardAddresses.contains(address.address) else { return false }
|
||||
// IPv6 链路本地需要额外接口作用域,不作为命令目标;IPv4 链路本地允许。
|
||||
if address.family == .ipv6, address.isIPv6LinkLocal { return false }
|
||||
// RFC 4941 隐私临时地址会轮换,不适合作为其他设备长期使用的命令目标。
|
||||
if address.v6Flags?.contains(.temporary) == true { return false }
|
||||
switch (mode, address.family) {
|
||||
case (.allIPv4Interfaces, .ipv4):
|
||||
return true
|
||||
@@ -165,4 +184,18 @@ enum TerminalCommandAddressPolicy {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// IPv6 地址类型注记(与 ifconfig 同源的打印逻辑);无标志(标志查询
|
||||
/// 失败或纯回环)不注记。
|
||||
private static func annotation(for address: LocalInterfaceAddress) -> String? {
|
||||
guard let flags = address.v6Flags else { return nil }
|
||||
if flags.contains(.autoconf) {
|
||||
if flags.contains(.secured) { return "autoconf secured" }
|
||||
if flags.contains(.temporary) { return "autoconf temporary" }
|
||||
return "autoconf"
|
||||
}
|
||||
if flags.contains(.secured) { return "secured" }
|
||||
if flags.contains(.dynamic) { return "dynamic" }
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@ import Foundation
|
||||
/// 测试专用地址便捷构造(数组字面量内以 `.ipv4("…")` 使用)。
|
||||
extension LocalInterfaceAddress {
|
||||
static func ipv4(_ text: String) -> LocalInterfaceAddress {
|
||||
LocalInterfaceAddress(address: text, family: .ipv4, isIPv6LinkLocal: false)
|
||||
LocalInterfaceAddress(address: text, family: .ipv4, isIPv6LinkLocal: false, v6Flags: nil)
|
||||
}
|
||||
|
||||
static func ipv6(_ text: String, linkLocal: Bool = false) -> LocalInterfaceAddress {
|
||||
LocalInterfaceAddress(address: text, family: .ipv6, isIPv6LinkLocal: linkLocal)
|
||||
static func ipv6(
|
||||
_ text: String, linkLocal: Bool = false, flags: LocalInterfaceV6Flags? = nil
|
||||
) -> LocalInterfaceAddress {
|
||||
LocalInterfaceAddress(address: text, family: .ipv6, isIPv6LinkLocal: linkLocal, v6Flags: flags)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,33 @@ final class ProxyCommandAddressWorkflowTests: XCTestCase {
|
||||
XCTAssertEqual(picker.selected.address, "127.0.0.1")
|
||||
}
|
||||
|
||||
func testTemporaryIPv6ExcludedAndAnnotationsInLabelMiddle() {
|
||||
setListenerMode(.allIPv4AndIPv6Interfaces)
|
||||
interfaceFacts.interfaces = [
|
||||
FakeLocalInterfaceFacts.wifi(
|
||||
addresses: [
|
||||
.ipv6("240e::8150", flags: [.autoconf, .secured]),
|
||||
.ipv6("240e::855", flags: [.autoconf, .temporary]),
|
||||
.ipv6("240e::17", flags: [.dynamic]),
|
||||
])
|
||||
]
|
||||
interfaceFacts.emitChange()
|
||||
|
||||
let picker = workflow.snapshot.commandAddressPicker
|
||||
XCTAssertEqual(
|
||||
picker.candidates.map(\.address),
|
||||
["127.0.0.1", "::1", "240e::17", "240e::8150"],
|
||||
"RFC 4941 隐私临时地址被过滤(会轮换);其余按地址稳定排序")
|
||||
XCTAssertEqual(
|
||||
picker.candidates.first { $0.address == "240e::8150" }?.label,
|
||||
"240e::8150 (autoconf secured) - Wi-Fi",
|
||||
"注记位于标签中间,紧贴其描述的 IP")
|
||||
XCTAssertEqual(
|
||||
picker.candidates.first { $0.address == "240e::17" }?.label,
|
||||
"240e::17 (dynamic) - Wi-Fi",
|
||||
"DHCPv6 地址注记 dynamic")
|
||||
}
|
||||
|
||||
func testIPv6OnlyModeDefaultsToV1AndFiltersFamilies() {
|
||||
setListenerMode(.allIPv6Interfaces)
|
||||
interfaceFacts.interfaces = [
|
||||
@@ -208,20 +235,22 @@ final class ProxyCommandAddressLifecycleTests: XCTestCase {
|
||||
func testIPv6SelectionProducesBracketedURLs() {
|
||||
setListenerMode(.allIPv4AndIPv6Interfaces)
|
||||
interfaceFacts.interfaces = [
|
||||
FakeLocalInterfaceFacts.wifi(addresses: [.ipv6("2001:db8::1")])
|
||||
FakeLocalInterfaceFacts.wifi(
|
||||
addresses: [.ipv6("2001:db8::1", flags: [.autoconf, .secured])])
|
||||
]
|
||||
interfaceFacts.emitChange()
|
||||
|
||||
let v6Candidate = workflow.snapshot.commandAddressPicker.candidates[2]
|
||||
_ = workflow.selectCommandAddress(v6Candidate)
|
||||
|
||||
let commands = workflow.snapshot.terminalProxyEnvironmentCommands
|
||||
XCTAssertTrue(
|
||||
workflow.snapshot.terminalProxyEnvironmentCommands.zshBash
|
||||
.contains("export http_proxy='http://[2001:db8::1]:11087'"),
|
||||
commands.zshBash.contains("export http_proxy='http://[2001:db8::1]:11087'"),
|
||||
"IPv6 URL 使用方括号与已保存 HTTP 端口")
|
||||
XCTAssertTrue(
|
||||
workflow.snapshot.terminalProxyEnvironmentCommands.fish
|
||||
.contains("set -gx all_proxy 'socks5://[2001:db8::1]:11086'"))
|
||||
commands.fish.contains("set -gx all_proxy 'socks5://[2001:db8::1]:11086'"))
|
||||
XCTAssertFalse(
|
||||
commands.zshBash.contains("autoconf"), "类型注记只进标签,不进命令")
|
||||
}
|
||||
|
||||
func testSelectionSurvivesLeavingAndReenteringHome() {
|
||||
|
||||
@@ -34,7 +34,8 @@ final class SystemInterfaceFactsProviderTests: XCTestCase {
|
||||
XCTAssertNil(lo0.interfaceType, "回环无 SystemConfiguration 类型映射")
|
||||
XCTAssertTrue(
|
||||
lo0.addresses.contains(
|
||||
LocalInterfaceAddress(address: "127.0.0.1", family: .ipv4, isIPv6LinkLocal: false)),
|
||||
LocalInterfaceAddress(
|
||||
address: "127.0.0.1", family: .ipv4, isIPv6LinkLocal: false, v6Flags: nil)),
|
||||
"回环单播含规范化 127.0.0.1")
|
||||
}
|
||||
|
||||
@@ -56,10 +57,16 @@ final class SystemInterfaceFactsProviderTests: XCTestCase {
|
||||
XCTAssertFalse(
|
||||
address.address.contains("%"),
|
||||
"\(interface.bsdName) 地址不含作用域后缀:\(address.address)")
|
||||
if address.family == .ipv6, address.address.hasPrefix("fe80") {
|
||||
XCTAssertTrue(address.isIPv6LinkLocal, "fe80 地址必须标记链路本地")
|
||||
} else if address.family == .ipv6 {
|
||||
XCTAssertFalse(address.isIPv6LinkLocal, "非 fe80 地址不应标记链路本地")
|
||||
if address.family == .ipv6 {
|
||||
XCTAssertNotNil(
|
||||
address.v6Flags, "\(interface.bsdName) IPv6 地址标志查询不应失败")
|
||||
if address.address.hasPrefix("fe80") {
|
||||
XCTAssertTrue(address.isIPv6LinkLocal, "fe80 地址必须标记链路本地")
|
||||
} else {
|
||||
XCTAssertFalse(address.isIPv6LinkLocal, "非 fe80 地址不应标记链路本地")
|
||||
}
|
||||
} else {
|
||||
XCTAssertNil(address.v6Flags, "IPv4 地址不携带 IPv6 标志")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,12 +82,21 @@ final class SystemInterfaceFactsProviderTests: XCTestCase {
|
||||
let nonLoopback = candidates.dropFirst(2)
|
||||
XCTAssertFalse(nonLoopback.isEmpty, "真实主机存在可列出的接口地址候选")
|
||||
for candidate in nonLoopback {
|
||||
let fact = interfaces.first { $0.bsdName == candidate.bsdName }?
|
||||
.addresses.first { $0.address == candidate.address }
|
||||
let type = interfaces.first { $0.bsdName == candidate.bsdName }?.interfaceType
|
||||
XCTAssertTrue(
|
||||
type == kSCNetworkInterfaceTypeIEEE80211 as String
|
||||
|| type == kSCNetworkInterfaceTypeEthernet as String,
|
||||
"候选 \(candidate.label) 必须来自允许类型接口(实际类型 \(type ?? "nil"))")
|
||||
XCTAssertFalse(candidate.address.contains("%"), "候选地址已规范化")
|
||||
XCTAssertFalse(
|
||||
fact?.v6Flags?.contains(.temporary) ?? false,
|
||||
"临时地址(会轮换)不进候选:\(candidate.label)")
|
||||
let knownAnnotations: Set<String?> = ["autoconf secured", "autoconf", "dynamic", nil]
|
||||
XCTAssertTrue(
|
||||
knownAnnotations.contains(candidate.annotation),
|
||||
"IPv6 候选注记只能来自 ifconfig 同源闭集:\(candidate.label)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
仅包含 loopback,以及本机当前启用的 Wi-Fi、Ethernet 接口的单播 IP。VPN、其他虚拟接口和其他接口类型排除;不能仅依据 BSD 接口前缀或通用 Ethernet 链路类型来接受虚拟接口。无法确认是允许类型的非回环接口不纳入列表。
|
||||
|
||||
不提供手动输入,不提供通配绑定地址,IPv6 链路本地地址排除。IPv4 链路本地地址属于允许接口的单播地址时可以列出。候选列表不表示网络连通性已验证。
|
||||
不提供手动输入,不提供通配绑定地址,IPv6 链路本地地址排除;IPv6 地址经 `SIOCGIFAFLAG_IN6` 查询内核标志,`IN6_IFF_TEMPORARY`(RFC 4941 隐私临时地址,会轮换)不进候选。IPv4 链路本地地址属于允许接口的单播地址时可以列出。候选列表不表示网络连通性已验证。
|
||||
|
||||
| 已保存的监听方式 | 地址族 | 默认地址 |
|
||||
| --- | --- | --- |
|
||||
@@ -22,7 +22,7 @@
|
||||
| 所有 IPv4 与 IPv6 接口 | IPv4、IPv6,包含两个回环地址 | 127.0.0.1 |
|
||||
| 仅所有 IPv6 接口 | IPv6 | ::1 |
|
||||
|
||||
每项显示 `{IP} - {name}`。接口名称优先取 `SCNetworkInterfaceGetLocalizedDisplayName(interface)`;名称缺失或无法映射到 SystemConfiguration 接口时,回退到 BSD 接口名,但不会因此放宽接口类型过滤。回环项回退名称为 lo0。
|
||||
每项显示 `{IP} - {name}`;带内核标志的 IPv6 地址在 IP 后附与 ifconfig 同源的类型注记,标签为 `{IP} ({注记}) - {name}`(如 `autoconf secured`、`dynamic`),便于识别地址稳定性。接口名称优先取 `SCNetworkInterfaceGetLocalizedDisplayName(interface)`;名称缺失或无法映射到 SystemConfiguration 接口时,回退到 BSD 接口名,但不会因此放宽接口类型过滤。回环项回退名称为 lo0。
|
||||
|
||||
回环项置顶,双栈时 127.0.0.1 在 ::1 前。其余按接口名称排序,同一接口先 IPv4 后 IPv6,再按地址稳定排序。同一接口的重复 IP 合并;不同接口即使 IP 相同也保留接口身份。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user