Something went wrong. Try again.
MacOS native v2ray client. Code is slop btw.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166import Foundation
/// Captures the pieces of macOS networking state that Teleport mutates, so the log/// shows what the machine actually looked like on either side of a change rather/// than only what Teleport intended to do.////// macOS has no iptables: what Teleport touches is the kernel routing table/// (`route`/`netstat`), the resolver configuration (`scutil --dns`), the packet/// filter (`pfctl`, root only) and the SystemConfiguration proxy settings/// (`networksetup`). Those are the four things captured here.////// Off by default — each capture spawns several processes and adds a few KB to the/// log, which is worth it while diagnosing a problem and not worth it otherwise.nonisolated enum SystemStateDump { static let subsystem = "state"
/// User default read by the app; `defaults write dev.x.teleport /// TeleportDiagnosticsStateDumps -bool YES` works as well as the Settings toggle. static let defaultsKey = "TeleportDiagnosticsStateDumps"
/// The root helper has no user defaults of its own, so the app's choice reaches /// it as a marker file it writes when a session starts. static var markerFileURL: URL { URL(fileURLWithPath: PrivilegedHelperConstants.helperStateDirectoryPath, isDirectory: true) .appendingPathComponent("diagnostics-state-dumps") }
static var isEnabled: Bool { if getuid() == 0 { return FileManager.default.fileExists(atPath: markerFileURL.path) } return UserDefaults.standard.bool(forKey: defaultsKey) }
/// Only the start request carries the flag. Stop follows whatever the marker /// says, so a session started with dumps on is dumped at both ends even if the /// toggle was flipped in between — the pair is what makes a dump useful. static func setMarker(_ enabled: Bool) { if enabled { FileManager.default.createFile(atPath: markerFileURL.path, contents: nil, attributes: [.posixPermissions: 0o600]) } else { try? FileManager.default.removeItem(at: markerFileURL) } }
/// Writes a labelled snapshot to the log. `label` should say where in a sequence /// this is ("before enabling system proxy", "after tunnel start"), because the /// point of a dump is the diff between two of them. /// /// Runs synchronously on the caller's thread — in the app that is the main /// actor, so with dumps on, connect and disconnect stall for the time it takes /// to spawn the probes, a few hundred milliseconds. Deliberate: the capture has /// to land between the two states it brackets, which moving it off-thread would /// not guarantee. static func capture(_ label: String, log: TeleportLog = .shared) { guard isEnabled else { return }
for probe in probes() { guard let output = run(probe.executablePath, probe.arguments) else { continue } log.dump(subsystem, "\(label): \(probe.title)", output) } }
private struct Probe { let title: String let executablePath: String let arguments: [String] }
private static func probes() -> [Probe] { var probes: [Probe] = [ Probe(title: "routes inet", executablePath: "/usr/sbin/netstat", arguments: ["-rn", "-f", "inet"]), Probe(title: "routes inet6", executablePath: "/usr/sbin/netstat", arguments: ["-rn", "-f", "inet6"]), Probe(title: "resolvers", executablePath: "/usr/sbin/scutil", arguments: ["--dns"]), Probe(title: "interfaces", executablePath: "/sbin/ifconfig", arguments: ["-a"]) ]
if getuid() == 0 { // pfctl needs root, so these only ever produce output inside the helper. probes.append(Probe(title: "pf rules", executablePath: "/sbin/pfctl", arguments: ["-s", "rules"])) probes.append(Probe(title: "pf nat", executablePath: "/sbin/pfctl", arguments: ["-s", "nat"])) } else { // networksetup reads SystemConfiguration as the logged-in user; running it // from the root helper would report a different preference session. probes.append(Probe(title: "network services", executablePath: "/usr/sbin/networksetup", arguments: ["-listallnetworkservices"])) }
return probes }
/// Proxy settings are dumped per service rather than as one command, because /// `networksetup` has no "show every proxy" form. /// /// `services` is an autoclosure because listing them costs a process spawn, and /// with dumps off — the default — that spawn would be paid for nothing. static func captureProxySettings(_ label: String, services: @autoclosure () -> [String], log: TeleportLog = .shared) { guard isEnabled else { return }
var text = "" for service in services() { for arguments in [["-getwebproxy", service], ["-getsecurewebproxy", service], ["-getsocksfirewallproxy", service]] { guard let output = run("/usr/sbin/networksetup", arguments) else { continue } text += "\(service) \(arguments[0]):\n" for outputLine in output.split(separator: "\n") { text += " \(outputLine)\n" } } } log.dump(subsystem, "\(label): proxy settings", text.isEmpty ? "<no services>" : text) }
private static func run(_ executablePath: String, _ arguments: [String], timeout: TimeInterval = 5) -> String? { guard FileManager.default.isExecutableFile(atPath: executablePath) else { return nil }
let process = Process() process.executableURL = URL(fileURLWithPath: executablePath) process.arguments = arguments
let pipe = Pipe() process.standardOutput = pipe // Nothing reads stderr, so it must not be a pipe: a probe that fills an // unread pipe blocks on the write, never closes stdout, and the read below // would wait on it forever. process.standardError = FileHandle.nullDevice
let exited = DispatchSemaphore(value: 0) process.terminationHandler = { _ in exited.signal() }
guard (try? process.run()) != nil else { return nil }
// The read happens on its own thread so the deadline applies to the whole // probe. Reading inline would block until the child closed stdout, which a // hung child never does — and this runs inside the helper's start handler, // so a hung probe would be a hung connect. let collected = OutputBox() let reader = Thread { collected.data = pipe.fileHandleForReading.readDataToEndOfFile() } reader.start()
let timedOut = exited.wait(timeout: .now() + timeout) == .timedOut if timedOut { process.terminate() // Reap the child; otherwise it stays a zombie for the life of this process. process.waitUntilExit() } // Termination closes the child's stdout, so the reader finishes either way. collected.wait()
let output = String(decoding: collected.data, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines) if timedOut { return output + "\n<probe timed out after \(Int(timeout))s>" } return output.isEmpty ? "<empty>" : output }
/// Hands the reader thread's output back to the caller. `@unchecked` because the /// semaphore orders the two accesses: the reader writes, signals, and only then /// does the caller read. private final class OutputBox: @unchecked Sendable { var data = Data() { didSet { done.signal() } } private let done = DispatchSemaphore(value: 0) func wait() { done.wait() } }}