Merge nucleic/mellow-dewy-falcon-rjhr into main
This commit is contained in:
@@ -92,10 +92,11 @@ public enum Doctor {
|
||||
/// same verb the real download uses, because the presigned redirect
|
||||
/// target is signed per method. Catches a version bump that no longer
|
||||
/// has a darwin-arm64 asset.
|
||||
/// 10. **Local Network privacy note** (informational). On macOS 15+ the
|
||||
/// first attempt to reach a guest over the NAT link can be blocked by
|
||||
/// the Local Network permission prompt, which a background agent cannot
|
||||
/// answer; the operator must approve the app once.
|
||||
/// 10. **Local Network privacy**. Passes when a subnet allowlist is set in
|
||||
/// `com.apple.network.local-network`; otherwise informational. On
|
||||
/// macOS 15+ the first attempt to reach a guest over the NAT link can
|
||||
/// be blocked by the Local Network permission prompt, which a
|
||||
/// background agent cannot answer.
|
||||
///
|
||||
/// - Parameter config: Validated configuration. Gitea-dependent checks are
|
||||
/// skipped with a ``DoctorCheck/Result/warn`` when no admin token is set.
|
||||
@@ -523,19 +524,70 @@ public enum Doctor {
|
||||
}
|
||||
|
||||
/// The macOS 15+ Local Network permission note.
|
||||
///
|
||||
/// Reports `.pass` when the host carries a subnet allowlist, because that
|
||||
/// bypasses the prompt entirely. Otherwise it stays informational: we
|
||||
/// cannot see the grant itself, since Local Network privacy is a Network
|
||||
/// Extension packet filter rather than a TCC entry, so there is no
|
||||
/// database to query and `tccutil` does not apply (Apple, TN3179).
|
||||
public static func localNetworkNote() -> DoctorCheck {
|
||||
DoctorCheck(
|
||||
name: "local network access",
|
||||
let name = "local network access"
|
||||
let allowed = localNetworkAllowlist()
|
||||
if !allowed.isEmpty {
|
||||
return DoctorCheck(
|
||||
name: name,
|
||||
result: .pass,
|
||||
detail: "subnet allowlist set: \(allowed.joined(separator: ", "))"
|
||||
)
|
||||
}
|
||||
|
||||
return DoctorCheck(
|
||||
name: name,
|
||||
result: .info,
|
||||
detail: "guests are reached over the host-private NAT link",
|
||||
remediation: """
|
||||
on macOS 15+ the first connection to a guest can be blocked by the Local Network \
|
||||
privacy prompt, which a background LaunchAgent cannot answer. Approve the app once \
|
||||
under System Settings → Privacy & Security → Local Network.
|
||||
privacy prompt, which a background LaunchAgent cannot answer. The app cannot be \
|
||||
pre-approved: it only appears under System Settings → Privacy & Security → Local \
|
||||
Network once it has actually attempted a guest connection. To trigger and answer \
|
||||
the prompt by hand, run `gitea-macos-runner vm boot --image default` once from a \
|
||||
Terminal in the GUI session. On an unattended CI host prefer the subnet \
|
||||
allowlist, which needs no prompt and survives rebuilds: sudo defaults write \
|
||||
com.apple.network.local-network AllowedEthernetLocalNetworkAddresses -array \
|
||||
"192.168.64.0/24" (then reboot). See docs/setup.md §2.6.
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
/// 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