- 菜单栏可每项独立(点击弹出该项窄详情)或合并为一个(点击打开主窗口),详情区块可在设置中勾选 - 网络详情:镜像流量历史、ICMP 连接探测格子、接口与 Wi-Fi、本地与公网 IPv4 / IPv6、SVG 国旗归属地与 ASN、各进程流量 - DNS:刷新缓存,一键切换自动 / Cloudflare / Google / 腾讯 / 阿里云或手动填写;辅助工具协议升至 3,服务名与地址二次校验 - 主窗口:侧边栏切换仪表盘、各指标详情与工具页,宽高可调,卡片按比例伸缩 - 统一外观:白底蓝 / 黑底蓝,窗口、设置与弹窗共用底色,去掉玻璃效果 - 新增网络解析、DNS 校验与国旗代码校验的单元测试;更新 README、架构文档与隐私说明
102 lines
4.7 KiB
Swift
102 lines
4.7 KiB
Swift
import Darwin
|
|
import Foundation
|
|
|
|
/// 用 ICMP 回显(ping)测量到目标地址的往返延迟。
|
|
/// macOS 允许普通用户创建 SOCK_DGRAM 类型的 ICMP 套接字,不需要 root,也不经过 shell。
|
|
public enum ConnectivityProbe {
|
|
/// 发送一次回显请求,返回往返毫秒数;超时或不可达时返回 nil。调用会阻塞,最长 `timeout`。
|
|
public static func ping(_ address: String, sequence: UInt16, timeout: TimeInterval) -> Double? {
|
|
var v4 = in_addr()
|
|
var v6 = in6_addr()
|
|
if inet_pton(AF_INET, address, &v4) == 1 {
|
|
var target = sockaddr_in()
|
|
target.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
|
|
target.sin_family = sa_family_t(AF_INET)
|
|
target.sin_addr = v4
|
|
return withUnsafeBytes(of: &target) { raw in
|
|
send(family: AF_INET, proto: IPPROTO_ICMP, target: raw, sequence: sequence, timeout: timeout)
|
|
}
|
|
}
|
|
if inet_pton(AF_INET6, address, &v6) == 1 {
|
|
var target = sockaddr_in6()
|
|
target.sin6_len = UInt8(MemoryLayout<sockaddr_in6>.size)
|
|
target.sin6_family = sa_family_t(AF_INET6)
|
|
target.sin6_addr = v6
|
|
return withUnsafeBytes(of: &target) { raw in
|
|
send(family: AF_INET6, proto: IPPROTO_ICMPV6, target: raw, sequence: sequence, timeout: timeout)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private static let echoRequest: [Int32: UInt8] = [AF_INET: 8, AF_INET6: 128]
|
|
private static let echoReply: [Int32: UInt8] = [AF_INET: 0, AF_INET6: 129]
|
|
private static let payloadSize = 16
|
|
|
|
private static func send(family: Int32, proto: Int32, target: UnsafeRawBufferPointer,
|
|
sequence: UInt16, timeout: TimeInterval) -> Double? {
|
|
let socketFD = socket(family, SOCK_DGRAM, proto)
|
|
guard socketFD >= 0 else { return nil }
|
|
defer { close(socketFD) }
|
|
|
|
// 内核会改写标识符,所以用序号 + 随机令牌匹配应答
|
|
let token = UInt32.random(in: .min ... .max)
|
|
var packet = [UInt8](repeating: 0, count: 8 + payloadSize)
|
|
packet[0] = echoRequest[family] ?? 8
|
|
packet[6] = UInt8(sequence >> 8)
|
|
packet[7] = UInt8(sequence & 0xFF)
|
|
withUnsafeBytes(of: token.bigEndian) { packet.replaceSubrange(8..<12, with: $0) }
|
|
if family == AF_INET {
|
|
// ICMPv6 的校验和由内核填写
|
|
let sum = checksum(packet)
|
|
packet[2] = UInt8(sum >> 8)
|
|
packet[3] = UInt8(sum & 0xFF)
|
|
}
|
|
|
|
let start = clock_gettime_nsec_np(CLOCK_UPTIME_RAW)
|
|
let sent = target.withMemoryRebound(to: sockaddr.self) { address in
|
|
sendto(socketFD, packet, packet.count, 0, address.baseAddress, socklen_t(target.count))
|
|
}
|
|
guard sent == packet.count else { return nil }
|
|
|
|
let deadline = start + UInt64(timeout * 1_000_000_000)
|
|
var buffer = [UInt8](repeating: 0, count: 512)
|
|
while true {
|
|
let now = clock_gettime_nsec_np(CLOCK_UPTIME_RAW)
|
|
guard now < deadline else { return nil }
|
|
var descriptor = pollfd(fd: socketFD, events: Int16(POLLIN), revents: 0)
|
|
let waitMilliseconds = Int32(max(1, (deadline - now) / 1_000_000))
|
|
guard poll(&descriptor, 1, waitMilliseconds) > 0 else { return nil }
|
|
let count = recv(socketFD, &buffer, buffer.count, 0)
|
|
guard count > 0 else { return nil }
|
|
if matches(buffer, count: count, family: family, sequence: sequence, token: token) {
|
|
return Double(clock_gettime_nsec_np(CLOCK_UPTIME_RAW) - start) / 1_000_000
|
|
}
|
|
}
|
|
}
|
|
|
|
static func matches(_ buffer: [UInt8], count: Int, family: Int32, sequence: UInt16, token: UInt32) -> Bool {
|
|
// IPv4 的 DGRAM 套接字在 macOS 上会带回 IP 头
|
|
var offset = 0
|
|
if family == AF_INET, count > 0, buffer[0] >> 4 == 4 {
|
|
offset = Int(buffer[0] & 0x0F) * 4
|
|
}
|
|
guard count >= offset + 12, buffer[offset] == echoReply[family] else { return false }
|
|
let replySequence = UInt16(buffer[offset + 6]) << 8 | UInt16(buffer[offset + 7])
|
|
let replyToken = buffer[(offset + 8)..<(offset + 12)].reduce(UInt32(0)) { $0 << 8 | UInt32($1) }
|
|
return replySequence == sequence && replyToken == token
|
|
}
|
|
|
|
static func checksum(_ bytes: [UInt8]) -> UInt16 {
|
|
var sum: UInt32 = 0
|
|
var index = 0
|
|
while index + 1 < bytes.count {
|
|
sum += UInt32(bytes[index]) << 8 | UInt32(bytes[index + 1])
|
|
index += 2
|
|
}
|
|
if index < bytes.count { sum += UInt32(bytes[index]) << 8 }
|
|
while sum >> 16 != 0 { sum = (sum & 0xFFFF) + (sum >> 16) }
|
|
return ~UInt16(sum)
|
|
}
|
|
}
|