This commit is contained in:
@@ -133,7 +133,66 @@ public enum LocalNetworkPermission {
|
||||
}
|
||||
}
|
||||
|
||||
return AllowlistResult(requested: subnets, observed: LocalNetworkPolicy.status())
|
||||
return AllowlistResult(requested: subnets, observed: observedStatus())
|
||||
}
|
||||
|
||||
/// The host's allowlist, read with root's privileges when this process's
|
||||
/// own are not enough.
|
||||
///
|
||||
/// `sudo defaults write <domain>` lands in `/var/root/Library/Preferences`
|
||||
/// on a stock host, and that directory is mode 700 — so the plain read in
|
||||
/// ``LocalNetworkPolicy/status()`` is refused and a write that worked
|
||||
/// perfectly looks like it vanished. Re-read the refused candidates as
|
||||
/// root instead.
|
||||
///
|
||||
/// Always `sudo -n`, so this can never turn a status query into a password
|
||||
/// prompt. Right after a write the credentials are still cached and it
|
||||
/// simply works; later — after a reboot, say — it fails and the result
|
||||
/// stays ``LocalNetworkPolicy/Status/isIndeterminate``, which callers
|
||||
/// report as "cannot tell without root" rather than as "not configured".
|
||||
public static func observedStatus() -> LocalNetworkPolicy.Status {
|
||||
let unprivileged = LocalNetworkPolicy.status()
|
||||
guard !unprivileged.unreadablePaths.isEmpty else { return unprivileged }
|
||||
|
||||
var sources: [(path: String, data: Data)] = []
|
||||
for path in LocalNetworkPolicy.preferenceCandidates() {
|
||||
if let data = FileManager.default.contents(atPath: path) {
|
||||
sources.append((path, data))
|
||||
} else if let data = readAsRoot(path) {
|
||||
sources.append((path, data))
|
||||
}
|
||||
}
|
||||
|
||||
let recovered = LocalNetworkPolicy.status(fromContentsOf: sources)
|
||||
// Nothing came back from the privileged read either: keep the
|
||||
// unprivileged answer, which still carries why it could not tell.
|
||||
guard recovered.isConfigured else { return unprivileged }
|
||||
return recovered
|
||||
}
|
||||
|
||||
/// `sudo -n cat <path>`, or nil if that fails for any reason.
|
||||
///
|
||||
/// Both failure modes are ordinary rather than exceptional — the candidate
|
||||
/// usually does not exist, and `sudo -n` legitimately refuses when no
|
||||
/// credentials are cached — so stderr is discarded instead of being shown
|
||||
/// to the operator. `Process` is fine here, unlike in ``runInForeground``:
|
||||
/// `-n` never touches the terminal.
|
||||
private static func readAsRoot(_ path: String) -> Data? {
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: "/usr/bin/sudo")
|
||||
process.arguments = ["-n", "/bin/cat", path]
|
||||
|
||||
let output = Pipe()
|
||||
process.standardOutput = output
|
||||
process.standardError = FileHandle.nullDevice
|
||||
process.standardInput = FileHandle.nullDevice
|
||||
|
||||
guard (try? process.run()) != nil else { return nil }
|
||||
let data = output.fileHandleForReading.readDataToEndOfFile()
|
||||
process.waitUntilExit()
|
||||
|
||||
guard process.terminationStatus == 0, !data.isEmpty else { return nil }
|
||||
return data
|
||||
}
|
||||
|
||||
/// Reboots the host. Only ever called from an explicit confirmation — the
|
||||
|
||||
Reference in New Issue
Block a user