Files
timban c08fc277a9 Initial commit: NetworkCity — network traffic as a neon city
A native macOS network monitor that renders live traffic as a top-down
neon city: your Mac is downtown, remote orgs are glowing districts, and
traffic is cars of light driving the roads.

Architecture:
- NetworkCityCore: swappable data layer behind a ConnectionSource protocol
  (nettop-backed today), plus pure/tested logic — traffic diffing, org
  classification (reverse-DNS + CIDR), traffic classes, hub-and-spoke
  expansion policy, label anti-overlap, and latency→distance layout.
- NetworkCityApp: SwiftUI + SpriteKit city — auto-expanding districts,
  protocol-coloured cars, click-to-inspect panel, and latency-as-distance
  (districts glide to their measured RTT).
- nettop-probe: CLI proof of the data layer.

44 tests passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 08:43:32 -07:00

94 lines
2.9 KiB
Swift

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
}
}