Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. SavedState: A snapshot of the OS proxy settings before Kinetic modified them. Serialized to disk so it survives crashes.
  2. ProxyConfigurator trait: The abstract interface that each platform (Linux, macOS, Windows) implements independently.
  3. 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 to proxy_active.lock on 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: A HashMap<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 a SavedState.
  • restore_state(&self, state: &SavedState): Takes a previously saved SavedState and 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" → calls detect_linux_configurator() (in os/linux.rs), which further inspects XDG_CURRENT_DESKTOP.
  • "macos" → returns Box::new(MacosConfigurator) (inside #[cfg(target_os = "macos")] guard).
  • "windows" → returns Box::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:

  1. Check for existing lockfile. If proxy_active.lock exists, a previous run may have left the OS in a modified state. The code reads the lockfile using serde_json::from_reader and immediately calls restore_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.
  2. Save current OS state. Calls configurator.save_previous_state() to capture whatever the OS currently has configured.
  3. Atomically write the lockfile. The saved state is written to a .tmp file first, then std::fs::rename() moves it to proxy_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.
  4. 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:

  1. Check if lockfile exists.
  2. If yes, read SavedState from the lockfile and call restore_state() to write the original user settings back to the OS.
  3. If the lockfile is missing or unreadable, fall back to calling configurator.uninstall() which sets a safe default (no proxy).
  4. 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 with autostart: true.
  • uninstall_service(): Deregisters it, then also explicitly calls pac_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 the ServiceLabel (e.g., kinetic-pac) and the directory name for the proxies/ registry folder.
  • os/linux.rs: Contains detect_linux_configurator(), KdeConfigurator, GnomeConfigurator.
  • os/macos.rs: Contains MacosConfigurator (compiled only on target_os = "macos").
  • os/windows.rs: Contains WindowsConfigurator (compiled only on target_os = "windows").
  • error.rs: Defines PacError — the custom error type returned by all ProxyConfigurator methods.

6. Quick Reference

TypePurpose
SavedStateSerialized original OS proxy state, written to lockfile
ProxyConfiguratorTrait abstracting OS-specific proxy commands
FallbackConfiguratorNo-op for unsupported environments
PacManagerLifecycle controller with atomic lockfile
detect_configurator()Returns correct ProxyConfigurator for current OS

Lockfile path: <data_local_dir>/kinetic_global/proxy_active.lock Atomic write: .tmprename().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, a SIGKILL leaves 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 calling install().

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_DESKTOP is empty. The FallbackConfigurator is used. No automatic proxy injection. The user must configure their proxy manually.

8. Rust Concepts

ConceptDescription
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 writesOn 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_readerReads 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 boundsRequired 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.