Merge nucleic/mellow-dewy-falcon-rjhr into main

This commit is contained in:
2026-08-07 00:44:36 -07:00
parent 749f0be4fb
commit bc2b6cd33b
47 changed files with 13159 additions and 0 deletions
+246
View File
@@ -0,0 +1,246 @@
import Foundation
/// One entry from macOS's `/var/db/dhcpd_leases`.
///
/// The Virtualization NAT attachment hands guests addresses from the host's
/// built-in `bootpd`, which records each lease in that file. There is no API for
/// this, so parsing the file keyed by the guest's MAC is how we learn a VM's IP.
public struct DHCPLease: Sendable, Equatable {
/// The guest's advertised hostname (`name=` in the lease block). Often the
/// guest's local hostname, sometimes absent.
public let name: String?
/// The leased IPv4 address, e.g. `192.168.64.7`.
public let ipAddress: String
/// The hardware address, **normalized**: lowercase, colon-separated, each
/// octet zero-padded to two hex digits, with the `1,` type prefix stripped.
public let hwAddress: String
/// Lease expiry, parsed from the `lease=` hex epoch, when present.
public let leaseExpiry: Date?
public init(name: String?, ipAddress: String, hwAddress: String, leaseExpiry: Date?) {
self.name = name
self.ipAddress = ipAddress
self.hwAddress = hwAddress
self.leaseExpiry = leaseExpiry
}
}
/// Parser for `/var/db/dhcpd_leases`.
///
/// ## File format
///
/// A sequence of brace-delimited blocks of `key=value` lines:
///
/// ```
/// {
/// name=macos-guest
/// ip_address=192.168.64.7
/// hw_address=1,aa:bb:c:dd:ee:ff
/// identifier=1,aa:bb:c:dd:ee:ff
/// lease=0x67a1b2c3
/// }
/// ```
///
/// Two details bite:
///
/// 1. `hw_address` carries a leading hardware-type prefix (`1,` for Ethernet)
/// that is not part of the MAC.
/// 2. Octets are **not zero-padded** — `aa:bb:c:dd:ee:ff` is the same address
/// that `VZMACAddress.string` renders as `aa:bb:0c:dd:ee:ff`. Comparing raw
/// strings silently fails to match; both sides must be normalized.
///
/// Blocks accumulate: a MAC can appear more than once as leases are renewed or
/// reissued, so lookups take the **newest** lease (latest `leaseExpiry`, falling
/// back to last-in-file when expiry is missing).
///
/// - Note: macOS's DHCP lease time is 24 hours. That is exactly why clones must
/// reuse a small set of **persistent per-slot MACs** rather than randomizing a
/// MAC per VM: a randomized fleet would fill this file with day-long stale
/// leases and exhaust the NAT subnet.
public enum DHCPLeaseParser {
/// The canonical path of the lease database.
public static let defaultPath = "/var/db/dhcpd_leases"
/// Parses the whole file.
///
/// Malformed blocks are skipped rather than throwing — the file is written
/// by another process and may be observed mid-write.
///
/// - Parameter text: The file's contents.
/// - Returns: Leases in file order.
public static func parse(_ text: String) -> [DHCPLease] {
var leases: [DHCPLease] = []
var fields: [String: String] = [:]
var inBlock = false
for rawLine in text.split(separator: "\n", omittingEmptySubsequences: false) {
let line = rawLine.trimmingCharacters(in: .whitespaces)
if line.isEmpty { continue }
if line.hasPrefix("{") {
// A `{` while already inside a block means the previous one was
// truncated (the file is written by bootpd and can be observed
// mid-write). Drop it and start over rather than merging.
inBlock = true
fields = [:]
continue
}
if line.hasPrefix("}") {
if inBlock, let lease = makeLease(from: fields) { leases.append(lease) }
inBlock = false
fields = [:]
continue
}
guard inBlock, let separator = line.firstIndex(of: "=") else { continue }
let key = line[line.startIndex..<separator].trimmingCharacters(in: .whitespaces).lowercased()
let value = line[line.index(after: separator)...].trimmingCharacters(in: .whitespaces)
if key.isEmpty { continue }
fields[key] = value
}
return leases
}
/// Builds a lease from one block's `key=value` pairs, or `nil` when the block
/// lacks the two fields that make it useful (an address and a MAC we can
/// normalize). Never throws: a half-written block is simply not a lease.
private static func makeLease(from fields: [String: String]) -> DHCPLease? {
guard
let ip = fields["ip_address"], !ip.isEmpty,
let rawMAC = fields["hw_address"] ?? fields["identifier"],
let mac = normalizeMAC(rawMAC)
else { return nil }
let name = fields["name"].flatMap { $0.isEmpty ? nil : $0 }
return DHCPLease(
name: name,
ipAddress: ip,
hwAddress: mac,
leaseExpiry: fields["lease"].flatMap(parseLeaseTime)
)
}
/// Parses a `lease=` value. `bootpd` writes a hex epoch (`0x66b2c0de`), but
/// a plain decimal epoch has been observed too, so both are accepted.
private static func parseLeaseTime(_ raw: String) -> Date? {
let text = raw.trimmingCharacters(in: .whitespaces).lowercased()
guard !text.isEmpty else { return nil }
let seconds: UInt64?
if text.hasPrefix("0x") {
seconds = UInt64(text.dropFirst(2), radix: 16)
} else {
seconds = UInt64(text, radix: 10)
}
guard let seconds else { return nil }
return Date(timeIntervalSince1970: TimeInterval(seconds))
}
/// Reads and parses the lease database from disk.
///
/// - Parameter path: Defaults to ``defaultPath``.
/// - Returns: Leases, or `[]` when the file does not exist yet (no guest has
/// ever leased an address).
public static func parseFile(at path: String = DHCPLeaseParser.defaultPath) -> [DHCPLease] {
guard let text = try? String(contentsOfFile: path, encoding: .utf8) else { return [] }
return parse(text)
}
/// Finds the current IP for a MAC.
///
/// Both `mac` and each lease's `hwAddress` are normalized before comparison.
///
/// - Parameters:
/// - mac: The guest's MAC, in any common rendering.
/// - leases: Leases from ``parse(_:)``.
/// - Returns: The newest matching lease's IP, or `nil`.
public static func ipAddress(forMAC mac: String, in leases: [DHCPLease]) -> String? {
lease(forMAC: mac, in: leases)?.ipAddress
}
/// Finds the newest lease for a MAC.
///
/// Callers that must distinguish a *fresh* lease from the 24 h-old one the
/// slot's previous guest left behind need the whole record, not just its
/// address — see ``isNewer(_:than:)``.
///
/// - Parameters:
/// - mac: The guest's MAC, in any common rendering.
/// - leases: Leases from ``parse(_:)``.
/// - Returns: The newest matching lease, or `nil`.
public static func lease(forMAC mac: String, in leases: [DHCPLease]) -> DHCPLease? {
guard let wanted = normalizeMAC(mac) else { return nil }
var best: DHCPLease?
for lease in leases where lease.hwAddress == wanted {
guard let current = best else {
best = lease
continue
}
// Newest expiry wins; a missing expiry sorts oldest. `>=` means that
// among equally-dated (or equally-undated) duplicates the last block
// in the file wins, which is the one bootpd wrote most recently.
let candidate = lease.leaseExpiry ?? .distantPast
let incumbent = current.leaseExpiry ?? .distantPast
if candidate >= incumbent { best = lease }
}
return best
}
/// Whether `candidate` is a lease `bootpd` wrote *after* `previous`.
///
/// Slot MACs are persistent and macOS leases live 24 h, so a MAC almost
/// always still has its previous guest's entry when the next clone boots.
/// A caller that accepted the first entry it saw would hand out a stale
/// address and then spend the whole boot timeout SSHing at nothing.
///
/// `bootpd` rewrites the block — bumping `lease=` — whenever it hands the
/// address out again, so a strictly later expiry means a new lease. A
/// changed address means the same thing. With no `previous` (first boot on
/// this MAC) anything counts as new.
///
/// - Parameters:
/// - candidate: The lease just read from the file.
/// - previous: The lease observed before the guest was started.
/// - Returns: `true` when `candidate` may be used.
public static func isNewer(_ candidate: DHCPLease, than previous: DHCPLease?) -> Bool {
guard let previous else { return true }
if candidate.ipAddress != previous.ipAddress { return true }
guard let previousExpiry = previous.leaseExpiry else { return true }
guard let candidateExpiry = candidate.leaseExpiry else { return false }
return candidateExpiry > previousExpiry
}
/// Normalizes a MAC to lowercase, colon-separated, zero-padded octets.
///
/// Accepts an optional `<type>,` prefix (as written by `bootpd`), and
/// tolerates `-` separators.
///
/// - Parameter raw: For example `1,aa:bb:c:dd:ee:ff` or `AA-BB-0C-DD-EE-FF`.
/// - Returns: For example `aa:bb:0c:dd:ee:ff`, or `nil` if unparseable.
public static func normalizeMAC(_ raw: String) -> String? {
var text = raw.trimmingCharacters(in: .whitespaces)
// `bootpd` prefixes the hardware type: `1,` for Ethernet.
if let comma = text.lastIndex(of: ",") {
text = String(text[text.index(after: comma)...])
}
text = text.replacingOccurrences(of: "-", with: ":")
let octets = text.split(separator: ":", omittingEmptySubsequences: false)
guard octets.count == 6 else { return nil }
var normalized: [String] = []
normalized.reserveCapacity(6)
for octet in octets {
guard (1...2).contains(octet.count), octet.allSatisfy(\.isHexDigit) else { return nil }
normalized.append(String(repeating: "0", count: 2 - octet.count) + octet.lowercased())
}
return normalized.joined(separator: ":")
}
}