import Foundation /// Transport flavour as reported by `nettop` (the trailing 4/6 is the IP version). public enum NetProtocol: String, Sendable, Hashable { case tcp4, tcp6, udp4, udp6, quic4, quic6, other public init(_ raw: String) { self = NetProtocol(rawValue: raw) ?? .other } } /// One end of a connection. `port` is nil for a wildcard (`*`) — i.e. a listener. public struct Endpoint: Sendable, Hashable { public let host: String public let port: Int? public init(host: String, port: Int?) { self.host = host self.port = port } public var isWildcard: Bool { host == "*" || host.isEmpty } public var display: String { guard let port else { return host } return "\(host):\(port)" } } /// A single connection sampled from `nettop`. Byte counts are cumulative /// (lifetime of the connection), so traffic *rates* come from diffing snapshots. public struct Connection: Sendable, Hashable { public let proto: NetProtocol public let local: Endpoint public let remote: Endpoint public let bytesIn: UInt64 public let bytesOut: UInt64 public let processName: String public let pid: Int32 public init( proto: NetProtocol, local: Endpoint, remote: Endpoint, bytesIn: UInt64, bytesOut: UInt64, processName: String, pid: Int32 ) { self.proto = proto self.local = local self.remote = remote self.bytesIn = bytesIn self.bytesOut = bytesOut self.processName = processName self.pid = pid } /// Stable identity for diffing across snapshots (ignores byte counts). public var key: String { "\(proto.rawValue) \(local.display)<->\(remote.display)" } /// True when the remote is a real off-box destination — the only kind that /// becomes a "building" in the city. Filters listeners, loopback, link-local. public var isExternal: Bool { let h = remote.host if remote.isWildcard { return false } if h == "::1" || h.hasPrefix("127.") { return false } // loopback if h.hasPrefix("fe80") || h == "::" { return false } // link-local / unspecified return true } /// True when the remote is on the local network (a "neighbourhood" district). public var isPrivateLAN: Bool { let h = remote.host return h.hasPrefix("10.") || h.hasPrefix("192.168.") || h.hasPrefix("169.254.") || (h.hasPrefix("172.") && (16...31).contains(Int(h.split(separator: ".").dropFirst().first ?? "") ?? -1)) } } /// All connections observed at one instant. public struct ConnectionSnapshot: Sendable { public let timestamp: Date public let connections: [Connection] public init(timestamp: Date, connections: [Connection]) { self.timestamp = timestamp self.connections = connections } }