PAC Server Part 1: State Management, PacManager, and Boot
File: kinetic-pac/src/main.rs — Lines 1–280
Crate: kinetic-pac | Stage: 11
Reading Time: 25 minutes
1. What Is This?
The first half of main.rs defines everything that makes OS-level proxy injection safe and reversible. It defines the core types and the PacManager — the struct responsible for atomically installing and cleanly removing the Kinetic PAC URL as the system-wide proxy autoconfiguration.
Three things are defined here that underpin the entire crate:
SavedState: A snapshot of the OS proxy settings before Kinetic modified them. Serialized to disk so it survives crashes.ProxyConfiguratortrait: The abstract interface that each platform (Linux, macOS, Windows) implements independently.PacManager: The lifecycle controller. It calls the configurator, manages the lockfile, and ensures that if the process is killed mid-run, the previous proxy state can always be recovered.
2. Why Kinetic Needs This
Warning
When the PAC server runs, it must modify the OS-wide network proxy settings. These are global, persistent system settings — not local to the application. If the PAC server crashes or is killed with
SIGKILL, the OS will be left with Kinetic’s PAC URL permanently set. The user’s internet traffic would then route through a dead local proxy, breaking all network access.
Note
SavedState+ the lockfile pattern solves this completely. Before overwriting the OS settings, the original settings are captured and written toproxy_active.lockon disk. The next time the process starts, it reads that lockfile, restores the old settings, then applies the new ones. This means a crashed daemon automatically cleans up its own mess on the next launch.
The ProxyConfigurator trait is needed because there is no cross-platform API for “set the system proxy”. Each OS does it differently — Linux via gsettings or kwriteconfig5, macOS via networksetup, Windows via PowerShell registry edits.
3. How It Works
SavedState — the crash recovery contract
#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 31–39
}
SavedState is a simple struct that gets serialized to JSON and written to disk. It holds:
previous_pac_url: The PAC URL that was configured before Kinetic changed it (if any).proxy_type: For GNOME/KDE, the mode string (e.g.,'none','auto',"2").macos_services: AHashMap<String, String>mapping each macOS network service name to its previously configured PAC URL. macOS has multiple network services (Wi-Fi, Ethernet, VPN) and each must be independently saved and restored.
ProxyConfigurator trait — the platform interface
#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 45–69
}
Four methods:
install(&self, pac_url: &str): Sets the OS proxy to the Kinetic PAC URL.uninstall(&self): Removes the Kinetic PAC URL from OS settings.save_previous_state(&self): Reads the current OS proxy config and returns it as aSavedState.restore_state(&self, state: &SavedState): Takes a previously savedSavedStateand writes it back to the OS.
The trait requires Send + Sync because the PacManager holding the boxed configurator must be shareable across the Tokio shutdown task.
FallbackConfigurator — the safe default
#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 75–99
}
When running on an unrecognized OS (or in a headless server environment with no desktop), FallbackConfigurator is used. Its install() simply prints a warning message telling the user to configure their proxy manually. All other methods are no-ops.
This prevents the PAC server from crashing just because it can’t automatically inject proxy settings — it degrades gracefully.
detect_configurator() — runtime platform dispatch
#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 103–120
}
Inspects std::env::consts::OS at runtime and returns the correct boxed ProxyConfigurator:
"linux"→ callsdetect_linux_configurator()(inos/linux.rs), which further inspectsXDG_CURRENT_DESKTOP."macos"→ returnsBox::new(MacosConfigurator)(inside#[cfg(target_os = "macos")]guard)."windows"→ returnsBox::new(WindowsConfigurator)(inside#[cfg(target_os = "windows")]guard).- Everything else →
Box::new(FallbackConfigurator).
The #[cfg] guards inside this function ensure that even if the OS string says "macos" at runtime on a cross-compiled binary, the macOS struct isn’t compiled in for non-macOS targets.
PacManager — the lifecycle controller
#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 123–180
}
PacManager holds a Box<dyn ProxyConfigurator> and the path to the lockfile (proxy_active.lock).
PacManager::install(pac_url) — the exact sequence:
- Check for existing lockfile. If
proxy_active.lockexists, a previous run may have left the OS in a modified state. The code reads the lockfile usingserde_json::from_readerand immediately callsrestore_state()to clean up. This is the crash recovery in action — even if the previous run was killed, this restores the original settings before overwriting them again. - Save current OS state. Calls
configurator.save_previous_state()to capture whatever the OS currently has configured. - Atomically write the lockfile. The saved state is written to a
.tmpfile first, thenstd::fs::rename()moves it toproxy_active.lock.rename()is atomic on POSIX systems — the file either exists completely or not at all. There is no window where a half-written lockfile can be read by a concurrent process. - Apply the new PAC URL. Calls
configurator.install(pac_url)to write the Kinetic PAC URL to the OS network settings.
#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 141–158
}
PacManager::uninstall() — the exact sequence:
- Check if lockfile exists.
- If yes, read
SavedStatefrom the lockfile and callrestore_state()to write the original user settings back to the OS. - If the lockfile is missing or unreadable, fall back to calling
configurator.uninstall()which sets a safe default (no proxy). - Delete the lockfile.
#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 164–179
}
Service management (install/uninstall/start/stop)
#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 221–279
}
Identical in pattern to kinetic-dns. Uses <dyn ServiceManager>::native() to register the binary with the OS service manager:
install_service(): Registers the binary withautostart: true.uninstall_service(): Deregisters it, then also explicitly callspac_manager.uninstall()to strip the OS proxy setting — so uninstalling the service also cleans up the proxy config.
4. Key Pieces
struct SavedState
The serializable snapshot of OS proxy state. Written to disk before any modification. The key to making this entire system crash-recoverable.
#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 31–39
}
trait ProxyConfigurator
The platform-agnostic interface for OS proxy management. The Send + Sync bounds allow the concrete implementor to be stored in the PacManager and shared across threads.
#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 45–69
}
struct PacManager
The high-level controller. Manages the install/uninstall lifecycle with crash recovery via the atomic lockfile.
#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 123–180
}
fn detect_configurator() -> Box<dyn ProxyConfigurator>
The runtime OS detector. Returns the correct platform-specific implementor, wrapped in Box<dyn ProxyConfigurator> so the PacManager doesn’t need to know the concrete type.
#![allow(unused)]
fn main() {
-> See: `kinetic-pac/src/main.rs` — Lines 103–120
}
5. Cross-Crate Connections
kinetic_core::constants::NETWORK_ID: Used to form theServiceLabel(e.g.,kinetic-pac) and the directory name for theproxies/registry folder.os/linux.rs: Containsdetect_linux_configurator(),KdeConfigurator,GnomeConfigurator.os/macos.rs: ContainsMacosConfigurator(compiled only ontarget_os = "macos").os/windows.rs: ContainsWindowsConfigurator(compiled only ontarget_os = "windows").error.rs: DefinesPacError— the custom error type returned by allProxyConfiguratormethods.
6. Quick Reference
| Type | Purpose |
|---|---|
SavedState | Serialized original OS proxy state, written to lockfile |
ProxyConfigurator | Trait abstracting OS-specific proxy commands |
FallbackConfigurator | No-op for unsupported environments |
PacManager | Lifecycle controller with atomic lockfile |
detect_configurator() | Returns correct ProxyConfigurator for current OS |
Lockfile path: <data_local_dir>/kinetic_global/proxy_active.lock
Atomic write: .tmp → rename() → .lock
Crash recovery: On next install() call, reads lockfile and restores old state first
7. Open Questions
Important
- SIGKILL race on first install: Between
save_previous_state()and writing the lockfile, aSIGKILLleaves no lockfile and no way to restore automatically. Only mitigated by never having a gap where the OS is modified but the lockfile isn’t written — the current code writes the lockfile before callinginstall().
Note
- Multi-user environments: The lockfile path is in
data_local_dir()which is per-user. If the daemon runs as a system service (not per-user), the lockfile location may not be accessible during restore.
Warning
- Headless Linux servers:
XDG_CURRENT_DESKTOPis empty. The FallbackConfigurator is used. No automatic proxy injection. The user must configure their proxy manually.
8. Rust Concepts
| Concept | Description |
|---|---|
Box<dyn Trait> | A heap-allocated trait object. Used here so PacManager can hold any ProxyConfigurator implementor without knowing its concrete type. The dynamic dispatch overhead is negligible — proxy installation happens once per boot. |
std::fs::rename() for atomic writes | On POSIX systems, rename() is guaranteed to be atomic. The old file path is atomically replaced by the new one. This prevents partial writes to the lockfile from corrupting the crash recovery data. |
serde_json::from_reader | Reads and deserializes JSON directly from a File handle without loading the entire file into memory as a String first. More efficient for structured config files. |
Send + Sync on trait bounds | Required to store Box<dyn ProxyConfigurator> in a struct that will be moved into a Tokio async task. Without these bounds, the compiler would reject the code at the point where the PacManager is moved into the shutdown handler. |