Merge nucleic/vivid-glass-urchin-xoym into main
This commit is contained in:
@@ -771,28 +771,27 @@ public enum Doctor {
|
||||
/// does not apply (Apple, TN3179).
|
||||
public static func localNetworkNote() -> DoctorCheck {
|
||||
let name = "local network access"
|
||||
let allowed = localNetworkAllowlist()
|
||||
if !allowed.isEmpty {
|
||||
if allowed.contains(where: coversVMNetRange) {
|
||||
let status = LocalNetworkPolicy.status()
|
||||
|
||||
if status.isConfigured {
|
||||
if status.coversGuestRange {
|
||||
return DoctorCheck(
|
||||
name: name,
|
||||
result: .pass,
|
||||
detail: "subnet allowlist set: \(allowed.joined(separator: ", "))"
|
||||
detail: "subnet allowlist set: \(status.allowlist.joined(separator: ", "))"
|
||||
)
|
||||
}
|
||||
return DoctorCheck(
|
||||
name: name,
|
||||
result: .warn,
|
||||
detail: "subnet allowlist set but does not cover the guest range: "
|
||||
+ allowed.joined(separator: ", "),
|
||||
+ status.allowlist.joined(separator: ", "),
|
||||
remediation: """
|
||||
Virtualization.framework's NAT does not stay on 192.168.64.0/24 — it moves \
|
||||
to the next free /24 (192.168.65.x, .66.x, …) when one is taken, so \
|
||||
an allowlist pinned to a single /24 stops working the day the subnet shifts \
|
||||
and every guest connection then fails with "No route to host". Widen it to \
|
||||
cover the whole span: sudo defaults write com.apple.network.local-network \
|
||||
AllowedEthernetLocalNetworkAddresses -array "192.168.64.0/18" (same for \
|
||||
AllowedWiFiLocalNetworkAddresses), then reboot. See docs/setup.md §2.6.
|
||||
and every guest connection then fails with "No route to host". Widen it: \
|
||||
`gitea-macos-runner permissions grant`, then reboot. See docs/setup.md §2.6.
|
||||
"""
|
||||
)
|
||||
}
|
||||
@@ -807,84 +806,14 @@ public enum Doctor {
|
||||
has no UI to show it in; a run started from a shell is attributed to the \
|
||||
*responsible* process, so both the prompt and the System Settings → Privacy & \
|
||||
Security → Local Network row belong to Terminal rather than to this app — and \
|
||||
granting it to Terminal does not carry over to the LaunchAgent. Prefer the subnet \
|
||||
allowlist: it needs no prompt, covers every process, and survives rebuilds. \
|
||||
sudo defaults write com.apple.network.local-network \
|
||||
AllowedEthernetLocalNetworkAddresses -array "192.168.64.0/18" (same for \
|
||||
AllowedWiFiLocalNetworkAddresses), then reboot. See docs/setup.md §2.6.
|
||||
granting it to Terminal does not carry over to the LaunchAgent. Grant it with \
|
||||
`gitea-macos-runner permissions grant`, which writes a subnet allowlist that \
|
||||
needs no prompt, covers every process, and survives rebuilds — then reboot. \
|
||||
`permissions status` explains both routes. See docs/setup.md §2.6.
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
/// The span of addresses a vmnet NAT link can plausibly use.
|
||||
///
|
||||
/// `192.168.64.0/24` is only the *first* choice: the subnet is picked at
|
||||
/// runtime and steps to the next free /24 when that one is already in use,
|
||||
/// which is why a host that worked yesterday can hand out `192.168.65.x`
|
||||
/// today. Everything from 192.168.64.0 to 192.168.127.255 — a /18 — is
|
||||
/// treated as guest territory so the allowlist survives that drift.
|
||||
static let vmNetFirstAddress: UInt32 = 0xC0A8_4000 // 192.168.64.0
|
||||
static let vmNetLastAddress: UInt32 = 0xC0A8_7FFF // 192.168.127.255
|
||||
|
||||
/// Whether one allowlist entry covers the whole guest range.
|
||||
///
|
||||
/// Deliberately all-or-nothing: partial cover is the failure mode being
|
||||
/// warned about, so an entry that contains today's subnet but not
|
||||
/// tomorrow's is not treated as good enough.
|
||||
static func coversVMNetRange(_ entry: String) -> Bool {
|
||||
let parts = entry.split(separator: "/", maxSplits: 1)
|
||||
guard let base = ipv4Value(String(parts[0])) else { return false }
|
||||
let prefix = parts.count == 2 ? Int(parts[1]) : 32
|
||||
guard let prefix, (0...32).contains(prefix) else { return false }
|
||||
|
||||
let mask: UInt32 = prefix == 0 ? 0 : ~UInt32(0) << (32 - prefix)
|
||||
let network = base & mask
|
||||
let broadcast = network | ~mask
|
||||
return network <= vmNetFirstAddress && broadcast >= vmNetLastAddress
|
||||
}
|
||||
|
||||
/// Packs dotted-quad IPv4 into a comparable integer; nil for anything else
|
||||
/// (an IPv6 entry, a hostname, a typo).
|
||||
static func ipv4Value(_ text: String) -> UInt32? {
|
||||
let octets = text.split(separator: ".", omittingEmptySubsequences: false)
|
||||
guard octets.count == 4 else { return nil }
|
||||
var value: UInt32 = 0
|
||||
for octet in octets {
|
||||
guard let number = UInt32(octet), number <= 255 else { return nil }
|
||||
value = value << 8 | number
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/// Subnets pre-authorized for local network access on this host, if any.
|
||||
///
|
||||
/// Best effort and never fatal: an unreadable or absent preferences file
|
||||
/// simply reads as "no allowlist". The domain is written with `sudo`, so
|
||||
/// which preferences directory it lands in depends on whether that `sudo`
|
||||
/// preserved `HOME` — check each candidate rather than guess.
|
||||
static func localNetworkAllowlist() -> [String] {
|
||||
let keys = ["AllowedEthernetLocalNetworkAddresses", "AllowedWiFiLocalNetworkAddresses"]
|
||||
let candidates = [
|
||||
"/var/root/Library/Preferences/com.apple.network.local-network.plist",
|
||||
"/Library/Preferences/com.apple.network.local-network.plist",
|
||||
NSHomeDirectory() + "/Library/Preferences/com.apple.network.local-network.plist",
|
||||
]
|
||||
|
||||
var found: [String] = []
|
||||
for path in candidates {
|
||||
guard let data = FileManager.default.contents(atPath: path),
|
||||
let plist = try? PropertyListSerialization.propertyList(
|
||||
from: data, options: [], format: nil) as? [String: Any]
|
||||
else { continue }
|
||||
for key in keys {
|
||||
for entry in (plist[key] as? [String] ?? []) where !found.contains(entry) {
|
||||
found.append(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
/// Renders checks as aligned, human-readable lines for the CLI.
|
||||
public static func format(_ checks: [DoctorCheck]) -> String {
|
||||
let width = checks.map(\.name.count).max() ?? 0
|
||||
|
||||
Reference in New Issue
Block a user