Lafine Systems Design · Technical document

RoamSwitch for Linux
Architecture & Security Whitepaper

A technical document explaining what RoamSwitch for Linux does at its privilege boundary. It avoids marketing language; the claims can be checked against the actual behavior of the distributed package and the self-check in the appendix.

Version v1.6.0 Scope RoamSwitch for Linux 1.1.0 (kernel LPE hardening, eBPF runtime protection, millisecond SIGSTOP freeze of attacking processes, Falco low-overhead optimization) Requirements systemd + nftables Published 2026-09-06

§1

RoamSwitch for Linux is a resident network-security and system-diagnostics application that brings the zero-trust network defense model of macOS RoamSwitch to Linux (systemd + nftables environments).

This edition targets client machines (workstations, laptops, single-board computers) and assumes autonomous behaviour that follows the machine as it moves between networks. A hardened server / headless-infrastructure edition (RoamSwitch Server Edition — always-on default-deny, critical-path FIM integrity, remote webhooks, and static policy enforcement) is delivered as a dedicated server build.

It identifies the connected Wi-Fi / wired network by its gateway MAC address and autonomously switches the active nftables firewall profile. On top of that it provides three layers of man-in-the-middle defense on untrusted networks — a VPN tunnel + kill-switch (WireGuard or Tailscale, the primary defense), a preventive ARP/NDP lock, and ARP spoof detection — plus behavioral ransomware detection (fanotify + Shannon entropy + canaries), a passive link guard that flags or blocks phishing connections (NFQUEUE + SNI), a Port Anomaly Guard, BadUSB mitigation, kernel local privilege escalation (LPE) preemption (Frag Gap CVE-2026-53362 mitigation), eBPF runtime threat sensor (Falco / Tetragon) integration with sub-second process freezing (SIGSTOP) and Air-Gap network isolation, a 24-item security health assessment, and a built-in Model Context Protocol (MCP) server.

This whitepaper describes the architecture, threat model, and design of each defense mechanism — and, in particular, how the principle of “fully local processing; the only thing sent off the machine is a request for signed detection data, carrying nothing about you” is guaranteed at the code level. It avoids marketing language; the claims can be checked against the actual behavior of the distributed package (binary) and the self-check in the appendix. RoamSwitch for Linux is proprietary freeware and its source code is not published (§11).

Design principles
PrincipleDetail
Zero telemetryThe product sends nothing about you or your machine to anyone. There is no telemetry, analytics, crash reporting, usage metrics, remote config, or license-activation server.
Receive-only updatesThe one outbound request the product makes is a once-a-day GET of a fixed, signed manifest from lafine.net/updates/v1 — the scam-site list for the link guard, the ClamAV signature version, the latest app version, the package-CVE maps (Homebrew/npm/PyPI/crates.io/RubyGems/Packagist/Go/Maven), the active-vulnerability-scan CVE map, and the popular-npm-package-name list used for typosquat detection. It carries no query string, no cookies, no identifying headers, and no information about you. updates.enabled = false turns it off entirely; the product then runs on bundled data with zero external contact.
Fully local processingEvery assessment, detection, and audit runs to completion on the local machine. URL safety analysis and secret scanning evaluate their input heuristically without transmitting it anywhere.
Least privilegePrivileged operations are confined to a root daemon; the UI, CLI, and MCP server request them over a Unix-domain-socket IPC. The daemon opens no TCP/UDP socket; the link guard uses only the AF_NETLINK NFQUEUE verdict channel (kernel ↔ userspace, no packet egress).
Fail closedAir-Gap (emergency isolation) survives a daemon restart and is re-asserted within seconds if its rules are externally deleted.
Offline licensingPro / Business licenses are verified purely by Ed25519 signature check. No call to an activation server occurs.

§2

RoamSwitch for Linux primarily targets the laptop / mobile workstation that roams between untrusted networks.

Attackers in scope

  • An attacker on the same LAN — port scans on shared Wi-Fi, ARP / NDP spoofing (including rogue Router Advertisements) for a man-in-the-middle, rogue DHCP, passive sniffing after MAC flooding, unauthorized access to a dev server or local LLM API accidentally bound to 0.0.0.0.
  • Malicious USB devices (BadUSB) — impersonating a keyboard and injecting keystrokes before the screen locks; unregistered USB mass storage for data exfiltration or to carry in malware.
  • Ransomware / fileless malware — entering via a downloaded file and bulk-encrypting the user’s documents.
  • DNS-based C2 and phishing — malware using name resolution to reach a C2 server or phishing site.

Out of scope

  • An attacker who already holds root; kernel- or firmware-level rootkits.
  • Attacks that physically remove the disk (the domain of full-disk encryption; RoamSwitch diagnoses whether encryption is present but does not encrypt).
  • AirDrop-style proximity wireless sharing control (no OS-native mechanism on Linux; not supported).

§3

RoamSwitch for Linux is implemented in Rust across seven crates. All communication with the daemon goes through /run/roamswitch/roamswitch.sock (a Unix domain socket). There is no listening TCP/UDP socket in the daemon, the app, or the MCP server. The only thing the app binds is a single Unix socket to prevent a second instance.

# User space (unprivileged)
roamswitch-app      GTK3 + AppIndicator tray
roamswitch (CLI)    Thin shell frontend
roamswitch-mcp      stdio JSON-RPC (launched by AI clients)
      │
      │  AF_UNIX  /run/roamswitch/roamswitch.sock  (local IPC only)
      ▼
roamswitch-daemon   root / systemd Type=notify + WatchdogSec=30
  · nftables profile control (inet roamswitch table)
  · Sentinel loop (network detection & profile enforcement)
  · VPN tunnel + kill-switch (WireGuard: wg-quick / roamswitch_vpn, Tailscale: roamswitch_ts)
  · Preventive ARP/NDP lock (neigh table pin) / detection
  · Port anomaly guard (roamswitch_dev_guard)
  · fanotify guard (ransomware / malware)
  · Link guard (NFQUEUE — inet roamswitch_linkguard)
  · Canary engine / Quarantine Vault
  · Kernel LPE hardening (userns disable, Yama LSM)
  · eBPF runtime integration (/var/run/roamswitch/events.sock)
  · Millisecond SIGSTOP freeze of attacking processes & Air-Gap tie-in
  · USB monitoring (evdev EVIOCGRAB — BadUSB) / approval queue
  · Ed25519 license verification

roamswitch-updater  A standalone systemd-timer helper. The only crate
                    with an HTTP/TLS stack (daily signed-manifest GET)
roamswitch-core     Pure logic library (shared by every crate)
roamswitchkit       MCP client SDK
Privilege separation
ComponentPrivilegeRole
roamswitch-daemonroot (systemd, Type=notify + WatchdogSec=30)All privileged operations. As the single writer, controls nftables, DNS, and systemd units.
roamswitch-applogin userGTK3 GUI + AppIndicator tray. Configuration editing and visualization only; every privileged action goes through IPC.
roamswitch (CLI)login userThin shell front-end using the same IPC socket.
roamswitch-mcpprocess spawned by the AI clientstdio JSON-RPC. Provides read-only diagnostic information.

All communication with the daemon goes through /run/roamswitch/roamswitch.sock (a directory with 0700-equivalent permissions). User configuration is stored as plaintext JSON at ~/.config/roamswitch/config.json. The daemon reads /home/*/.config/roamswitch/config.json with root privileges (it runs with HOME=/root). Configuration never leaves the machine; there is no account registration and no cloud sync.

§4

The sentinel loop (5-second cycle by default) reads the default gateway’s MAC from /proc/net/arp and ip neigh and matches it against the registered MACs in config.json. MAC is used rather than SSID because an SSID is trivially spoofed. The desktop notification shown when the profile switches states the reason for the switch (“an unregistered network” / “your setting for the registered network X” / “no network connection”).

Profiles
ProfileSituationnftables behavior
open (trusted)A registered home or corporate networkpolicy accept. Local traffic allowed.
balancedUnregistered but configured as relatively safeDefault-deny inbound. Only established connections and lo allowed; exposed-port exposure monitored.
lockdown (away protection)Public Wi-Fi / unregistered networkAll inbound packets stealth-dropped. Sharing services auto-stopped (opt-in).

All rules are isolated in a dedicated nftables table named inet roamswitch and never collide with other firewall configuration (ufw / firewalld). Manual-override expiry (1 hour / 2 hours / 4 hours / until midnight / until cleared / until next disconnect) is enforced by the daemon as the single writer. The GUI never writes an expiry; it re-reads the config.json the daemon rewrote on the next tick and reflects it in the UI (avoiding a conflict).

Air-Gap (emergency network isolation)

A total-isolation mode that sets policy drop (priority -100) on both the input and output hooks of the inet roamswitch table, permitting only lo.

  • Fail closed — on daemon restart, detecting the marker file (/run/roamswitch/airgap.active) re-asserts isolation.
  • Self-healing — if an external process deletes the rules with nft delete table, the sentinel loop re-applies within seconds.
  • Automatic expiry — 600 seconds after activation (identical to the macOS maxAirGapDuration) the isolation lifts automatically, preventing a permanent loss of connectivity from a misclick.
table inet roamswitch {
    chain input {
        type filter hook input priority -100; policy <accept|drop>;
        iif "lo" accept
        ct state established,related accept
        # balanced/lockdown: drop everything else
    }
    chain output {
        type filter hook output priority -100; policy <accept|drop>;
        # policy drop only during Air-Gap; only lo and ct established are allowed
    }
}

Passive link guard (phishing-connection detection)

The daemon watches where outbound connections are going and flags — or, for the narrow “clearly phishing” set, blocks — connections to dangerous hosts. It replaces the older paste-a-URL checker, which required the user to act.

  • Interception — a dedicated nftables table inet roamswitch_linkguard hooks the output path and queues DNS questions and new/early web connections to a userspace worker via NFQUEUE. NFQUEUE is a kernel↔userspace verdict channel over AF_NETLINK; the daemon binds no socket and originates no packet. The queue rules carry bypass, so if the worker is not running, traffic passes unimpeded (Air-Gap remains the separate hard stop).
  • Hostname sources — the DNS question name, the TLS ClientHello SNI (so a browser using DoH is still covered by hostname), and the plaintext HTTP Host header. Encrypted ClientHello (ECH), once widely deployed, is a known blind spot.
  • Verdict (roamswitch-core, pure logic) — a host is checked against the local phishing/threat feed and a set of offline heuristics: IDN/confusable homograph of a protected brand, brand token in a non-official domain, raw-IP host, high-risk TLD.
Mode (linkGuard.mode)Feed hit or brand homographOther suspicious signals
offnot inspectednot inspected
warnconnection held up to 8 s + an allow/block approval promptnotification only
block (default)connection dropped + an allow dialog (once per host)notification only

Only a feed hit or a brand-homograph host is ever hard-blocked; everything the heuristics alone flag is a warning. warn mode holds the connection to a dangerous host for up to 8 seconds while it asks the user; if the hold expires with no answer it is fail-closed (the packets are dropped and the verdict is not cached) — not answering “is this site OK?” must not mean “yes”, and the next attempt prompts again. block mode shows one notification and one allow dialog per host (a browser opening many connections to the same blocked host no longer floods notifications). On a dangerous approval dialog the default button is “Keep blocking” and “Allow” is styled as the quiet, non-default choice. A blocked host can be allowed permanently or for five minutes from the notification (linkGuard.allowlist).

DNS is inspected, never forged: a blocklisted DNS question produces a notification only; enforcement happens on the subsequent TCP connection, so RoamSwitch never injects a synthetic DNS response. The feed is a compiled list of phishing/scam domains, shipped in the package and refreshed by the daily updater (§11). With no feed present the offline heuristics still run. The feed file’s Ed25519 signature is checked before load; a bad or missing feed fails to “heuristics only”, never to “allow all”.

VPN tunnel + kill-switch (the primary MITM defense)

Anti-MITM protection that does not depend on the integrity of layer 2. On an untrusted network, an always-on encrypted tunnel to a trusted endpoint makes local ARP/NDP spoofing, rogue DHCP and passive sniffing irrelevant — the attacker sees only ciphertext. The ARP/NDP lock and the detection below are secondary to this feature.

Two backends to choose from — “WireGuard (config file)” or “Tailscale (exit node)”. RoamSwitch implements no cryptography of its own and is not a VPN provider. Both backends are optional dependencies; if absent the GUI shows the install command.

(A) WireGuard backend

  • No VPN server is provided — uses a WireGuard config the user supplies: a .conf from a provider such as Mullvad, IVPN or Proton VPN, or the user’s own WireGuard server (a VPS or a home server). No default config is shipped.
  • Config — the daemon validates the .conf and stores it at /etc/wireguard/roamswitch.conf (0600, interface roamswitch). It cannot live elsewhere: the AppArmor wg-quick profile on Ubuntu 24.04+ / Debian 13 denies wg-quick reading a .conf outside /etc/wireguard/ — even as root. A split-tunnel config (AllowedIPs not 0.0.0.0/0 / ::/0) is flagged as “not all traffic is protected”.
  • Kill-switch — a dedicated nftables table inet roamswitch_vpn (policy drop, both hooks) drops everything except loopback, the tunnel interface (roamswitch, matched by oifname), UDP to the pinned endpoint IP, DHCP, and ICMP. It is installed before the tunnel is brought up (endpoint resolved to fixed IPs via getent ahosts first), so there is no leak window. If wg-quick up fails, the kill-switch stays up.

(B) Tailscale backend

  • For users who already run Tailscale. RoamSwitch does not run tailscale up / login / install tailscaled — it reads tailscale status --json and runs tailscale set --exit-node=<node> / --exit-node= (clear).
  • An exit node is required — anti-MITM protection only holds with an exit node that routes all traffic. With none configured, protection is not armed and the UI warns “not protected”. If the chosen node goes offline, protection is disarmed automatically and the user notified (routing everything through a dead node would black-hole the connection).
  • Kill-switch (inet roamswitch_ts, policy drop) — Tailscale’s transport cannot be pinned to a single endpoint IP (DERP relays + roaming peers), so this is looser than the WireGuard one. It permits: loopback, the tunnel interface (tailscale0), the CGNAT ranges 100.64.0.0/10 and fd7a:115c:a1e0::/48, STUN (udp/3478), DERP (tcp/443), udp/41641, DHCP, ICMP, MagicDNS (100.100.100.100:53), established connections. Everything else — arbitrary UDP, DNS to a local resolver, SMB, mDNS, plaintext HTTP, arbitrary TCP — is dropped.
  • Kill-switch caveats — (1) broader allow-list than the WireGuard backend (udp/3478, tcp/443, the whole CGNAT /10). (2) An on-path attacker still sees metadata (that Tailscale is in use, DERP region, timing) and can drop/delay — but cannot read the tunnelled payload. (3) When direct UDP is blocked, confidentiality then rests on Tailscale’s own pinned-cert DERP TLS. (4) With MagicDNS off + a local resolver, DNS breaks while armed (UI warning). (5) Captive portals must be logged into before enabling.

Common — armed only when vpn_on_untrusted_enabled and the network is untrusted (level != open). On a trusted network the exit node is cleared and the kill-switch removed (never tailscale down). Off by default, opt-in. The WireGuard tunnel / Tailscale transport goes to the user’s own VPN endpoint / tailnet and is unrelated to lafine’s infrastructure (§9).

Preventive ARP/NDP lock

The moment an untrusted network is joined, the MAC addresses of the on-link infrastructure a MITM attacker would need to impersonate are pinned into the kernel neighbour table as PERMANENT (static) entries. Once pinned, the kernel ignores spoofed ARP replies / neighbour advertisements.

  • What is pinned — (1) the IPv4 default gateway, (2) the IPv6 default router (often a link-local fe80:: address), (3) any on-link DNS resolver (a public resolver is reached via the gateway and is skipped). IPv6 neighbours are pinned with ip -6 neigh replace ... nud permanent.
  • Trusted networks are never pinned — so a home-router reboot can’t black-hole the user. Re-pinned on every network switch (TOFU); a legitimate failover is picked up on the next reconnect. Pinned entries are recorded in /var/lib/roamswitch/arp_lock.json.
  • On by default (Community Edition policy; same breakdown as the macOS Pro build). Toggle: gateway_arp_lock_enabled.

ARP spoof detection (notify-first)

Monitors /proc/net/arp for one IP mapping to multiple MACs and for a suspicious sudden change of the gateway MAC. In lockdown: cut all traffic (Air-Gap) + notify. On balanced / trusted networks: notify without cutting; the user triggers the emergency Air-Gap from RoamSwitch. This (a) stops an attacker from weaponising a single spoofed ARP packet into a self-inflicted outage, and (b) stops a router reboot or access-point roam from triggering it by mistake. Toggle: arp_spoof_guard_enabled.

Port Anomaly Guard

Automatically contains a listening port newly exposed to the external LAN (a backdoor, C2, or a dev server / local LLM API accidentally bound to 0.0.0.0). The verdict is baseline-relative, not “dangerous port number”. On enable, the currently-exposed executables are learned as “known”; thereafter only a newly appeared exposed executable is flagged (identity = the executable path, snaps normalised to snap:<name>). System daemons are out of scope. Containment is via the inet roamswitch_dev_guard table: drop TCP to that port from anything but lo — the machine itself keeps using it. Upon auto-containment, an immediate desktop notification and confirmation dialog (“Allow This Port” / “Keep Blocked”) is presented, allowing one-click permanent whitelisting. Any port can also be allowed individually, and the baseline recaptured, from the port-diagnostics screen. On by default. Toggle: port_anomaly_guard_enabled. Ported from macOS.

Docker Risk Detection & Firewall Bypass Protection

Docker's published ports (-p) are known to bypass the host's standard firewall, so inspection rules are permanently inserted into the DOCKER-USER chain, blocking any external traffic that isn't explicitly permitted (no configuration needed, on by default). In addition, docker events is watched in real time to detect the instant a --privileged container starts or /var/run/docker.sock is bind-mounted — a container-escape risk — and notifies you immediately. This flags a risky configuration, not confirmed compromise, so it is notify-only with no automatic blocking.

Secret & API Key Leak Scanner

Detects API keys (OpenAI, Anthropic, AWS, GitHub, Slack, Stripe, and more) and SSH private keys using Shannon entropy plus pattern matching. roamswitch audit-secrets <text|file|directory> scans text and files, plus an entire directory recursively (skipping .git, node_modules, and similar; files over 2MB or that look binary are skipped). The same capability is also available via the audit_secrets MCP tool and the audit_secrets_directory() Python SDK method. Content is never transmitted anywhere.

First-run setup wizard and confirmation UI

The package ships /etc/xdg/autostart/roamswitch.desktop, so the setup wizard launches on the first login after install (afterwards it is tray-resident only). Enabling / disabling a security guard shows a confirmation dialog for the same guards as the macOS build (Port Anomaly, ARP auto-containment, USB storage, BadUSB keyboard, Bluetooth). Event-time decisions (an unknown USB keyboard, an unregistered USB drive, an unknown port auto-blocked, an emergency Air-Gap) are presented as modal dialogs.

§5

Shannon-entropy burst detection · YARA scanning

Write events are tracked per PID. If a single process creates 20 or more distinct files with entropy ≥ 7.92 within 5 seconds, it is treated as ransomware bulk-encryption: the process is frozen with SIGSTOP and Air-Gap is triggered.

  • Complete separation of YARA scanning and entropy tracking — YARA malware inspection is applied to all written files without exception (zero bypass, scanning ~/.cache/, node_modules/, /tmp/, and archives). Meanwhile, ransomware entropy burst analysis excludes naturally compressed formats (.zip, .tgz, .tar.gz, .gz, .png, .jpg, .mp4, .wasm) and verified package managers (npm, npx, cargo, rustc, dpkg, apt, tar) operating in build/cache trees to eliminate false-positive SIGSTOP freezes.
  • Repeated overwrites of the same file (e.g. shred’s three passes) count as one event.
  • Critical-process protection — PIDs are checked against a hard-coded NEVER_FREEZE list (systemd, dockerd, NetworkManager, sshd, gnome-shell, …) and are never frozen, preventing a machine-wide hang.

Canaries (decoy files)

Decoy files are placed in the user’s Documents, Desktop, Downloads, and Pictures folders and a dedicated directory, and compared against a SHA-256 baseline on a 3-second cycle. Any rename, deletion, truncation, or content change triggers Air-Gap immediately. Because the daemon runs as root, it enumerates /home/* and deploys canaries into every real user’s directories.

fanotify monitoring · Quarantine Vault · ClamAV

  • The daemon monitors /home, /var/tmp, /dev/shm at the mount level and / at the filesystem level via FAN_CLASS_CONTENT (fallback FAN_CLASS_NOTIF).
  • Notify-first, no silent quarantine. A bare YARA / ClamAV hit is never silently quarantined or FAN_DENY’d. A real signature raises a modal request in /run/roamswitch/approvals.json (Quarantine / Allow / Later); nothing enters the vault until the user confirms. Only an actual execve is FAN_DENY’d, and only when pre_exec_blocking_enabled is on. The approval times out fail-open.
  • Test signatures such as EICAR are history-only — the industry-standard test string raises no notification, only an entry in notification history, and is never quarantined or blocked, in both the fanotify guard and the download guard.
  • A user-configured scan_exclusions path list (absolute paths, applied recursively; it subsumes the legacy clamav_exclusions) is honoured by both the YARA and ClamAV scanners. “Allow” on the prompt and “Restore” in the Quarantine tab both append to it, so the same file is not re-flagged.
  • Detected and confirmed real malware is relocated to ~/.local/share/roamswitch/quarantine/. The vault is 0700; each sample is 0400 (read-only, non-executable). On restore, the file returns to 0644 and is chowned to the destination directory’s owner.
  • The permission-event loop does zero filesystem I/O — config is read by a dedicated background thread into a Mutex<GuardConfig> snapshot; hot-path accessors just lock and copy. This is a design invariant: blocking on FAN_OPEN_PERM would freeze the machine.
  • When ClamAV is present, newly downloaded executables (.sh, .deb, .elf, …) and dangerous AI models (.pkl, .pt) are scanned against the local DB. ClamAV is an optional dependency; the other defenses work without it. When it is installed, the daily updater (§11) invokes freshclam so the signature database stays current.

Kernel Local Privilege Escalation (LPE) Preemption & Hardening

When an attacker or malware gains an unprivileged user foothold, defense-in-depth prevents exploiting Linux kernel vulnerabilities to obtain root privileges.

  • Disabling Unprivileged User Namespaces (Frag Gap CVE-2026-53362 Mitigation) — Many modern Linux kernel LPE vulnerabilities rely on unprivileged users calling unshare(CLONE_NEWUSER) to create isolated user namespaces, subsequently accessing uninitialized kernel structs or legacy network subsystems with elevated namespace capabilities. RoamSwitch assesses and enforces user.max_user_namespaces = 0 (or kernel.unprivileged_userns_clone = 0) via one-click hardening or setup wizard. This closes off the attack surface from unprivileged processes even with unpatched kernel zero-days.
  • Yama LSM Memory Protection — Enforcing kernel.yama.ptrace_scope = 1 (or 2) prevents unauthorized processes from attaching via ptrace or reading memory via /proc/$pid/mem, safeguarding browser sessions and GPG/SSH private keys.
  • Core Dump Suppression — Setting fs.suid_dumpable = 0 prevents crashing sensitive processes from writing plaintext memory dumps to disk.
  • /tmp & /dev/shm noexec Mount Defense — To prevent payload drop-and-execute attacks, RoamSwitch remounts /tmp and /dev/shm with noexec,nosuid,nodev when connecting to untrusted networks.

eBPF Runtime Threat Sensor & Autonomous Process Freezing (Desktop eBPF Guard)

The RoamSwitch client daemon includes native integration with in-kernel modern eBPF sensors (Falco / Tetragon / BTF).

# eBPF runtime threat detection & autonomous interception flow
[Malicious process (LPE/malware)]
    │ Illegal syscall execution (Frag Gap exploitation attempt)
    ▼
[Linux kernel (eBPF probe)]
    │ eBPF ring buffer (2MB compact design)
    ▼
[Falco (RoamSwitch Optimized)]
    │ Direct in-memory UNIX socket (/var/run/roamswitch/events.sock)
    ▼
[roamswitch-daemon (root)]
    ├─► ① Millisecond freeze of the attacking process: kill(pid, SIGSTOP) [state: Ts]
    ├─► ② Full network cutoff: nftables input/output drop [Air-Gap engaged]
    └─► ③ Desktop emergency alert: GTK3 modal & libnotify notification
  • Direct UNIX Domain Socket IPC (/var/run/roamswitch/events.sock) — Alerts from Falco or other eBPF sensors are received directly over an in-memory UNIX socket without writing to disk logs, eliminating disk I/O latency and enabling sub-second response handoffs.
  • Autonomous Multi-Layer Mitigation (Process Freezing & Air-Gap) — When an event with priority Critical or Emergency (such as kernel LPE, namespace escape, or ransomware execution) is received, the daemon executes SIGSTOP on the offending PID to immediately pause process execution (state: Ts), deploys policy drop across inet roamswitch (Air-Gap), and triggers an immediate desktop notification.
  • Low-overhead desktop tuning & log-bloat prevention (RoamSwitch Optimized) — to curb battery drain, CPU load, and disk exhaustion on laptops and workstations, an optimized profile (99-roamswitch-optimized.yaml) ships bundled:
    • Severity Filtering (priority: warning) — Filters out 95% of routine Notice/Info events.
    • Kernel Early Drop (drop_failed_exit: true) — Discards failed syscall exits directly in the kernel driver, halving context switches and CPU load.
    • Compact Ring Buffer (cpus_for_each_buffer: 2) — Reduces ring buffer size to 2MB, maintaining resident RAM at 30–50MB and CPU usage <1%.
    • Mandatory Log Rotation Cap (/etc/logrotate.d/roamswitch-falco) — Enforces a strict 50MB ceiling with 7-generation compressed rotation.
  • Hybrid Resilience (Standalone vs. eBPF Mode) — Without Falco, RoamSwitch protects the system via kernel sysctl (user.max_user_namespaces = 0), nftables roaming profiles, fanotify entropy tracking, and canaries. With Falco, deep in-kernel behavioral telemetry intercepts advanced zero-day exploits and freezes attacker processes in real time.

§6

USB events are monitored; keyboard-class (HID) devices and USB mass storage are tracked. Off by default (shipping isEnabled = false, matching the macOS USBKeyboardGuard / USBStorageGuard). Opt in from the first-run wizard or the USB tab; enabling shows a confirmation dialog.

Rogue USB keyboard (keystroke injection)

  • No kernel-level physical port de-authorization (important). The old implementation (≤1.0.6) wrote 0 to /sys/bus/usb/devices/*/authorized for an unregistered keyboard, physically disconnecting it. On a combined keyboard+mouse receiver (common on a Raspberry Pi or mini PC) this also disconnects the mouse, locking out a machine with no built-in input. RoamSwitch now hard-refuses writing authorized=0 to any device exposing a HID interface (bInterfaceClass 03), and the daemon re-authorizes any authorized==0 HID device at startup to self-heal machines a previous version bricked.
  • Keystrokes are dropped in software. On detecting an unregistered keyboard, an exclusive evdev EVIOCGRAB is taken on that device’s /dev/input/eventN (only nodes with a kbd handler in /proc/bus/input/devices, matching VID:PID). The mouse node is untouched. This is the Linux equivalent of the macOS CGEventTap; the device stays powered.
  • Safety — the grab is only taken if there is another keyboard to type on (a built-in one, or an approved-and-connected USB one), so a machine with a single new keyboard is never locked out.
  • Approval dialog — alongside the grab, an approval request is queued in /run/roamswitch/approvals.json and the GUI shows a modal (Allow / Deny). Allow whitelists the device and releases the grab. Fail-open after 3 minutes.

Rogue USB mass storage

When an unregistered USB mass-storage device (no HID interface) is inserted, the guard is enabled, and the network is untrusted / in lockdown, it is held with authorized=0 (safe for a non-HID device) and an approval dialog is shown. Allow re-authorizes and whitelists; Deny keeps it held. The macOS build was aligned to this “hold read-only + approval prompt” model in 1.7.4.

Identification and grandfathering

HID is determined from bInterfaceClass == "03" and the Handlers= line in /proc/bus/input/devices. A combo receiver (one VID:PID with both a kbd and a mouse section) is merged by VID:PID. An unregistered keyboard present at daemon startup is grandfathered; only a keyboard hot-plugged afterwards is grabbed.

§7

The LinuxHealthChecker in roamswitch-core evaluates 24 items and produces a 0–100 score, an A–F grade, and per-item advice. From the GTK dashboard, the CLI (roamswitch status), and MCP (get_security_report), the title, detail, and recommendation of all 24 items appear in the user’s language (10 languages).

Assessment categories
CategoryExample items
Disk / bootLUKS / dm-crypt encryption, UEFI Secure Boot
Access controlLSM (AppArmor / SELinux), SSH / sudo config audit
Kernel hardeningsysctl hardening, unprivileged user namespaces disablement (Frag Gap mitigation), core-dump control, noexec on /tmp and /dev/shm
Updatesautomatic security update configuration
NetworkARP spoofing monitoring, gateway ARP/NDP lock state, exposed-port audit, Wi-Fi encryption strength
MalwareClamAV / fanotify (also checks the guard is actually running) / entropy monitoring / noexec
BrowserFirefox / Chrome / Chromium Safe Browsing settings
DNS / USBthreat-protection DNS, zero-trust state of the USB bus

Every check is based on reading local files and command output (getenforce, mokutil, resolvectl, ss, …). There is no external query of any kind.

Trust-aware grading. Hardening that RoamSwitch deliberately does not apply on a trusted (home) network — pinning the gateway MAC as PERMANENT (which would black-hole the LAN on a router reboot), remounting /tmp and /dev/shm noexec (which breaks package builds and some installers), and threat-protection DNS under the default scope — is shown green with an explanation rather than a red “not hardened”, and notes that it is applied automatically on an untrusted network. Runtime truth comes from /run/roamswitch/state.json (active_level / network_trusted / fanotify_ready), written every cycle by the daemon. The fanotify item checks that the guard is actually marked and running (/run/roamswitch/fanotify.ready), not merely that the kernel supports it. The exposed-ports item reads the daemon’s live profile (the MCP hard-code is gone).

Per-item hardening. Failed items that have an automated fix (kernel sysctl / Yama / core dumps, /tmp noexec, USB zero-trust, gateway ARP pinning, threat-protection DNS, restarting the daemon when the fanotify guard is down) carry a “🔧 Harden” button in the GTK dashboard. The daemon runs as root, so no pkexec is needed except for the daemon restart.

§8

roamswitch-mcp speaks JSON-RPC over stdio and is spawned by AI clients such as Claude Desktop, Cursor, and Antigravity. Every tool is read-only and fully local.

Exposed tools
ToolPurpose
get_security_report24-item assessment, score, recommendations
get_exposed_portsexposed ports and their firewall block status
get_guard_statuscurrent protection level and defense-module state
audit_url_safetyheuristic URL safety analysis (does not fetch the target)
audit_secretsdetection of API keys / secrets in text (does not transmit the text)
audit_security_logsaggregation of local journald / auth logs
get_quarantine_status / get_canary_statusquarantine and canary state
get_app_helpsearch of the built-in knowledge base
run_active_vuln_scanNon-destructive, 127.0.0.1-only active vulnerability verification. The only tool that uses the network; off by default and requires opting in from Settings.
run_package_cve_scanChecks installed packages (Mac: Homebrew / Linux: dpkg, dnf, zypper, pacman) against a local CVE map. Sends no network traffic at all.
run_package_cve_scan_languagesChecks dependency lockfiles for npm, PyPI, crates.io, RubyGems, Packagist, Go, and Maven against the same local CVE map. Sends no network traffic at all.
verify_fimRe-hashes about 150 critical system files and compares them against the stored baseline to verify integrity.
get_file_scan_guard_statusReturns the File Scan Guard's (ClamAV) configuration and the state of the quarantine vault it feeds.
get_port_anomaly_incidentsReturns the Port Anomaly Guard's baseline state, currently auto-blocked ports, and up to the 50 most recent incidents. The response explicitly notes that the currently-blocked ports are a present-tense snapshot with no timestamp, distinct from the timestamped incident history.
get_ebpf_incidentsReturns the eBPF Runtime Guard's current containment status and the incident history behind it.
get_resource_guard_incidentsReturns the Resource Exhaustion / Process Anomaly Guard's history (memory leaks, crash loops) with a confidence tier.
get_notification_historyReturns the history of notifications RoamSwitch has sent (security log-audit anomalies, ClickFix detections, and the like) from the past 7 days, most recent first.
get_incident_timelineCorrelates every guard's detections into one chronological timeline, with process ancestry and MITRE ATT&CK tags (experimental).

The MCP server opens no network of its own; it either connects to the daemon’s Unix socket via roamswitchkit or calls roamswitch-core logic directly. The one exception is run_active_vuln_scan — off by default — which sends non-destructive probes to 127.0.0.1, this host itself.

A misattribution found — and fixed — during a local-LLM fire drill
In September 2026, while running a fire drill that had a local LLM (Qwen 3.8 27B) investigate an incident through this MCP server, we found a case where the model misread the data returned by get_port_anomaly_incidents and folded an unrelated, older port block into the narrative of an ongoing incident. The cause: a present-tense snapshot of currently-blocked ports, carrying no timestamp, was returned in the same response as the real, timestamped incident history, with nothing distinguishing the two. The fix adds explicit note fields marking which field is a timeline and which is a snapshot. Re-running the same scenario against the same model afterward, it quoted the note's wording and explicitly declined to attribute the old port to the current incident. Verified both by an automated regression test and by re-running the real local LLM.

§9

RoamSwitch for Linux makes exactly one kind of outbound request: the daily updater’s GET of a signed manifest (§11). Everything else — every assessment, detection, and audit — completes locally. RoamSwitch for Linux is proprietary freeware and its source is not published, so the inspections below were performed by lafine; the results are in 9.2, and the steps in Appendix C are reproducible from the distributed package alone.

9.1 Audit method

  1. Crate isolation — an HTTP/TLS stack is permitted in one crate only, roamswitch-updater, which builds the standalone timer-driven updater binary. scripts/audit_no_network.sh (run in CI, gating) fails the build if roamswitch-core, roamswitch-daemon, roamswitch-app, roamswitch-mcp, or roamswitchkit pulls in reqwest / hyper / ureq / rustls / native-tls / openssl, or if any .rs file outside roamswitch-updater uses TcpStream / TcpListener / UdpSocket / to_socket_addrs / lookup_host.
  2. Dependency tree inspection — the full Cargo.lock was searched for telemetry / analytics / crash-reporting SDKs (sentry, opentelemetry, …).
  3. roamswitch-updater review — its single request target, the absence of a query string / cookies / identifying headers, and its Ed25519 verification of everything it downloads.
  4. Subprocess enumeration — every Command::new(...) call site was reviewed.

9.2 Results

Audit results
Audit itemResult
HTTP / TLS stackPresent in roamswitch-updater only (ureq + rustls). Not in core / daemon / app / mcp / kit
Telemetry / analytics / crash-reporting SDKNot present anywhere
TcpStream / TcpListener / UdpSocketNot a single occurrence outside roamswitch-updater
Socket APIs in useAF_UNIX (local IPC); AF_NETLINK (NFQUEUE verdict channel, nftables and ip neigh control — no egress); AF_INET only in roamswitch-updater. The VPN tunnel (§4), when enabled, is wg-quick (WireGuard backend) or tailscaled (Tailscale backend) — a subprocess, not the daemon — sending traffic to the user’s own endpoint / tailnet
Daily updaterOne GET https://lafine.net/updates/v1/manifest per ~24 h. No query string, no cookies, generic User-Agent, no identifiers. All payloads Ed25519-verified. updates.enabled = false disables it entirely
VPN tunnel (§4)Off by default. When enabled, wg-quick builds a WireGuard tunnel to the endpoint in the user’s own .conf — unrelated to lafine. Endpoint hostname resolution via getent ahosts (glibc NSS), not a Rust socket API
License activationOffline Ed25519 signature verification. No communication with an activation server
Conclusion

RoamSwitch for Linux initiates exactly one kind of outbound request — a daily, receive-only download of signed detection data that carries no information about the user or the machine. Nothing else in the product ever contacts an external server.

9.3 Complete list of externally-facing operations

Every operation that could cause a packet to leave the local machine, directly or indirectly:

#OperationDestinationTriggerWhat is sent
1Daily updaterGET /v1/manifest, and the feed file when its version changedlafine.net/updates/v1 (CDN)systemd timer, once per ~24 h with a randomized delay. Off when updates.enabled = false or roamswitch-update.timer is disableda plain HTTPS GET: no query string, no cookies, generic User-Agent, nothing about the user or the machine. Receives a signed manifest + feed
1afreshclam (ClamAV’s own updater), invoked by the daily updater when ClamAV is installedthe ClamAV project’s CDNas part of #1traffic of ClamAV itself (a separate project); RoamSwitch only launches it
2ping -c1 -W1 <gateway IP>the LAN default gateway (link-local)to refresh the ARP cache during ARP-spoofing detectionICMP echo request only. No DNS resolution, no payload
3launching the system browser (xdg-open)https://lafine.net/... (product site)only when the user clicks a button such as “Open release notes”the RoamSwitch process opens no socket; it hands the URL to the OS browser
4resolvectl dns <iface> …— (no communication occurs)threat-protection DNS on an untrusted network (opt-in). <iface> is every physical NIC carrying a default route (wired + wireless at once; lo / tailscale* / wg* / docker* / veth* excluded); also applied on a trusted network when dns_scope = always_on; reconciled every sentinel cycleit only changes the OS resolver setting; RoamSwitch itself sends no DNS query
5package-manager updatesthe distro’s apt / dnf / zypper repositorieswhen the user runs apt upgrade, etc., or clicks the GUI “Upgrade now” button (which runs apt-get / dnf / zypper via pkexec)RoamSwitch reads local apt-cache policy output and launches the package manager; unrelated to lafine
6VPN tunnel (§4) — WireGuard backend: the tunnel wg-quick builds / Tailscale backend: tailscale set --exit-node routes all traffic through the exit nodethe user’s own VPN endpoint / tailnet (WireGuard: the endpoint in the .conf; Tailscale: the chosen exit node and DERP relays)when vpn_on_untrusted_enabled and the network is untrusted. Off by defaultencrypted WireGuard / Tailscale traffic; the destination is the user’s own VPN server / tailnet, unrelated to lafine. WireGuard endpoint resolution via getent ahosts (glibc NSS); Tailscale is driven by tailscaled (a separate process)

Every other RoamSwitch feature — nftables control, ARP/NDP pinning of the neighbour table (ip neigh, no packet egress), the link guard (NFQUEUE, AF_NETLINK only), port scanning (parsing ss output), ARP monitoring, health assessment, entropy monitoring, canaries, fanotify, USB monitoring (evdev grab), quarantine, log auditing, URL and secret auditing — completes using only local file reads and analysis of subprocess output. The §4 VPN tunnel is the only “outbound communication the user explicitly configured”, and its destination is the user’s own VPN server — no data flows to lafine’s infrastructure.

§10

  • No account — no user registration, no login, no email address ever requested.
  • Configuration is local only — stored in plaintext at ~/.config/roamswitch/config.json and never leaves the machine. There is no cloud sync.
  • Assessment results are local only — security reports and log-audit results are only displayed on screen and, if the user chooses, exported to a local file.
  • License keys are verified offline — Pro / Business license tokens contain an Ed25519 signature and are verified with a public key. No query is made to a lafine server.
  • No crash reporting — on panic, no stack trace is sent anywhere.
  • The daily updater sends nothing about you — no query string, no cookies, no device or install identifier, a generic User-Agent. It is a download, not a report. Link-guard verdicts, blocked hosts, and scan results are written to the local activity view / journald only, never transmitted.

§11

Editions
EditionForPriceLicense
Community EditionLinux personal useFreeProprietary freeware (bundled EULA)
Business (planned)Fleet management, policy distribution, signed internal apt, SLA for organizationsPaidCommercial license. Same binary + tier gate

Purchasers of macOS RoamSwitch Pro Lifetime receive Business features free on their own Linux machines.

Distribution and supply chain

ChannelDetail
APThttps://lafine.net/apt (stable main). Release signed with an RSA-4096 key
RPMhttps://lafine.net/rpm (separate Fedora / openSUSE). repomd.xml GPG-signed
AURroamswitch-bin (fetches the GitHub Releases tarball, verifies sha256)
tarballamd64 / aarch64 .tar.gz + .sha256 on GitHub Releases
  • Pinned glibc floor — release binaries are built inside a Debian 12 (bookworm, glibc 2.36) container. This prevents symbol-version mismatches on Debian 12 / Ubuntu 22.04 and later. A “verify glibc floor” CI step fails the build if a dependency requiring more than 2.36 is introduced.
  • Signing key management — the private key for apt/rpm repository signing exists only in a GitHub Actions encrypted secret; only the public key is committed to the repository.

11.3 The daily updater

roamswitch-updater is a standalone binary run by a systemd timer (roamswitch-update.timer, OnCalendar=daily with a randomized 3-hour delay and Persistent=true). It is the only component that reaches the network. Each run:

  1. GET https://lafine.net/updates/v1/manifest and its detached signature.
  2. Verify the manifest’s Ed25519 signature with a dedicated threat-feed signing key — separate from the Sparkle app-update key. The feed is re-signed daily by server-side CI, so that private key lives in a CI secret; scoping it to the feed means a leak only lets an attacker serve a bad blocklist (sinkhole / warn — no code path, no app-update path). The app-update signing key never leaves the macOS keychain. A manifest that does not verify is discarded — the updater never falls back to unsigned data.
  3. If the threat-feed version changed, download the feed, verify its SHA-256 and Ed25519 signature, and install it atomically for the link guard (§4).
  4. If ClamAV is installed, invoke freshclam.
  5. Record the latest app version for a notification (the updater never installs the app; on Linux the user runs apt upgrade).

Failure of any step — offline, a bad signature, freshclam missing — is recorded as updates.lastError and the process still exits 0, so a timer run never generates a systemd failure notification. The whole mechanism is disabled by updates.enabled = false in updates.json or by systemctl disable --now roamswitch-update.timer; the product then runs on the data bundled in the package. The threat feed is also shipped inside the package, so a fresh install has a working feed before the updater has ever run, and apt upgrade refreshes it even with the updater disabled.

§12

  • OS: Ubuntu 22.04 / 24.04 and later, Debian 12 and later, Linux Mint / Pop!_OS / elementary / Zorin, Raspberry Pi OS 64-bit (Bookworm), Ubuntu for Raspberry Pi
  • Architecture: x86_64 / aarch64
  • Required: systemd, nftables, iproute2 (ip neigh — the ARP/NDP lock) (pulled in automatically by the .deb / .rpm dependencies)
  • Optional: clamav (malware scanning), nmcli (Wi-Fi encryption detection), libnotify (desktop notifications), wireguard-tools (the WireGuard backend of the VPN tunnel + kill-switch, §4), tailscale (its Tailscale backend, §4), polkit / pkexec (running an upgrade from the GUI)
  • Not supported: non-systemd distros (Alpine / Void / Devuan)
  • Display: 1280×720 or larger (the window and font auto-scale to the screen size, supporting small Raspberry Pi displays)

Appendix A

CratePurposeNetwork
serde / serde_jsonserialization of config and IPCnone
tokioasync runtime (features = ["full"], but only Unix sockets are used)none (no TCP)
tracing / tracing-subscriberlogging (local / journald)none
thiserror / anyhowerror handlingnone
regexstring parsing (diagnostics, CSS scaling)none
urlURL parsing (audit_url_safety heuristics)none (does not fetch)
chronodate/time handlingnone
ed25519-dalekoffline verification of license signaturesnone
base64 / sha2license tokens, canary hashesnone
libcchown / kill / syscallsnone
idnadecoding IDN/punycode hosts for homograph detectionnone
nfq (daemon only)NFQUEUE bindings for the link guard — pure Rust, AF_NETLINKnone (verdict channel, no egress)
opencalls xdg-open for “Open release notes”the process itself does not communicate
GTK3 / ksni (app only)GUI and traynone
ureq + rustls (roamswitch-updater only)the daily updater’s one HTTPS GETthe only HTTP/TLS stack in the tree; confined to the updater binary
Key point

No HTTP client, TLS stack, or telemetry SDK appears in roamswitch-core, roamswitch-daemon, roamswitch-app, roamswitch-mcp, or roamswitchkit. ureq + rustls are present only in roamswitch-updater (scripts/audit_no_network.sh enforces this in CI). There is no telemetry SDK anywhere.

Appendix B

table inet roamswitch {                 # Profile firewall (Air-Gap / balanced / lockdown)
    chain input {
        type filter hook input priority -100; policy <accept|drop>;
        iif "lo" accept
        ct state established,related accept
        # balanced/lockdown: drop everything else
    }
    chain output {
        type filter hook output priority -100; policy <accept|drop>;
        # policy drop only during Air-Gap; only lo and ct established are allowed
    }
}

table inet roamswitch_linkguard {        # §4.5 — independent of the profile table
    chain output {
        type filter hook output priority 0; policy accept;
        udp dport 53                       queue num 92 bypass
        tcp dport 53                       queue num 92 bypass
        tcp dport { 80, 443 } ct state new,established queue num 92 bypass
    }
}

table inet roamswitch_dev_guard {        # §4 Port anomaly guard / manual port isolation, separate table
    chain input {
        type filter hook input priority -10; policy accept;
        iif != "lo" tcp dport { … } drop
    }
}

table inet roamswitch_vpn {              # §4 WireGuard backend kill-switch
    chain output {
        type filter hook output priority -150; policy drop;
        oifname "lo" accept
        oifname "roamswitch" accept
        meta l4proto { icmp, ipv6-icmp } accept
        udp sport 68 udp dport 67 accept          # DHCP
        ip  daddr { <endpoint v4> } udp dport <port> accept
        ip6 daddr { <endpoint v6> } udp dport <port> accept
        ct state established,related accept
    }
    chain input  { type filter hook input priority -150; policy drop; … }  # symmetric
}

table inet roamswitch_ts {               # §4 Tailscale backend kill-switch (looser)
    chain output {
        type filter hook output priority -150; policy drop;
        oifname "lo" accept
        oifname "tailscale0" accept
        meta l4proto { icmp, ipv6-icmp } accept
        udp sport 68 udp dport 67 accept          # DHCP (v6 side is symmetric too)
        ip  daddr 100.64.0.0/10 accept            # tailnet CGNAT
        ip6 daddr fd7a:115c:a1e0::/48 accept
        ip  daddr 100.100.100.100 udp dport 53 accept   # MagicDNS
        ip  daddr 100.100.100.100 tcp dport 53 accept
        udp dport 3478 accept                     # STUN
        udp dport 41641 accept                    # Direct WireGuard connection
        tcp dport 443 accept                      # DERP fallback
        ct state established,related accept
    }
    chain input  { type filter hook input priority -150; policy drop; … }  # symmetric
}

The ARP/NDP pinning of the gateway / IPv6 router / DNS resolver (§4) is done in the kernel neighbour table (ip neigh replace … nud permanent), not in nftables. Pinned entries are recorded in /var/lib/roamswitch/arp_lock.json.

Appendix C

Every claim in this document can be verified on your own machine. The commands below run on a host with the roamswitch package installed.

C.1 Outbound Traffic Verification

The daemon / app / MCP binaries link no HTTP / TLS library:

for b in roamswitch-daemon roamswitch-app roamswitch-mcp; do
  ldd "$(command -v $b)" | grep -iE 'ssl|crypto|nghttp|curl' && echo "  ^ in $b"
done
# → No output. The HTTP/TLS stack is statically linked only into roamswitch-updater.

The running daemon holds no internet socket:

sudo lsof -p "$(pgrep -x roamswitch-daemon)" -a -i
# → No output = zero TCP/UDP internet connections
sudo lsof -p "$(pgrep -x roamswitch-daemon)" -a -U | grep roamswitch
# → Only /run/roamswitch/roamswitch.sock (the link guard adds an AF_NETLINK socket,
#   but this is a kernel channel, not an internet socket)

The only RoamSwitch-attributable egress is the daily updater:

watch -n 5 'sudo ss -tupn | grep -E "roamswitch-(daemon|app|mcp)" || echo "(none)"'
# → Always "(none)". None of these three ever opens an internet socket.
systemctl list-timers roamswitch-update.timer
sudo systemctl start roamswitch-update.service
journalctl -u roamswitch-update.service -n 20 --no-pager
# → A single HTTPS GET to lafine.net/updates/v1, then it exits.

Disable it and confirm total silence:

sudo systemctl disable --now roamswitch-update.timer
# or edit /var/lib/roamswitch/updates.json to set "enabled": false
sudo strace -f -e trace=network -p "$(pgrep -x roamswitch-daemon)" 2>&1 | grep -i 'connect('
# → Only connects to AF_UNIX / AF_NETLINK. AF_INET / AF_INET6 never appear.

C.2 Reproducible Defense Verification (Docker Test Suite)

All detection, control, and defense mechanisms of RoamSwitch (Air-Gap enforcement, self-healing after firewall clobbering, ransomware canary tampering, /tmp noexec, Yama LSM, homograph phishing interception, and credential leak detection — 17 items total) can be safely reproduced and verified 100% in an isolated container without affecting host networks or files, using the official Docker test suite provided in the public support repository (roamswitch-support

# Fetch and reproduce the verification suite (automated verification inside an isolated container, using the official distribution package)
git clone https://github.com/lafine1211/roamswitch-support.git
cd roamswitch-support/test/docker
docker build -t roamswitch-test .
docker run --rm --privileged roamswitch-test

The test container automatically fetches the official distribution package from the APT repository (lafine.net/apt) and evaluates each of the following systematic test items (SP-1–11, PENT-1–7) within an isolated network and mount namespace.

(1) Autonomous Network Defense & Air-Gap Self-Healing

IDScenarioVerification MethodPass Criterion
SP-3Air-Gap enable → egress dropSend enable_air_gap IPCpolicy drop applied to output hook; all outbound traffic blocked
SP-3bFirewall forced purge & self-healingPurge rules via nft delete table inet roamswitchAnti-Clobber Self-Healing restores drop rules within 5 seconds
SP-3cFail-closed across daemon restartHard-kill daemon (SIGKILL) mid-Air-Gap and restartEgress drop policy persists seamlessly across restart
SP-3dAuthorized Air-Gap recoverySend authorized disable_air_gap IPCDrop policy lifted, all marker files removed, egress restored
SP-8Preventive ARP/NDP pinningEnable in isolated netns gateway environment → send spoofed gratuitous ARPGateway neigh entry remains PERMANENT; spoofed ARP ignored
SP-9VPN kill-switch (WireGuard)Live WireGuard server in separate netns, import .confvpn_upHandshake completes through inet roamswitch_vpn; zero leaks on tunnel death
SP-10VPN kill-switch (Tailscale)Stub tailscale CLI + separate netns, tailscale0 IFOff-tunnel plaintext TCP/80 and local DNS blocked; DERP and MagicDNS allowed

(2) Kernel & Mount Hardening

IDScenarioVerification MethodPass Criterion
SP-11aNetwork sysctl hardeningRun apply_kernel_sysctl_hardening IPCrp_filter=1, tcp_syncookies=1, accept_redirects=0 reach target values
SP-11bMemory protection & Yama LSMVerify ptrace_scope and suid_dumpablekernel.yama.ptrace_scope=1 (blocks snooping), fs.suid_dumpable=0 (no core dump)
SP-11c/tmp & /dev/shm noexec defenseRun apply_mount_hardening IPC/tmp and /dev/shm remounted noexec,nosuid,nodev; execution denied
SP-11dLegitimate user execution preservedExecute scripts in standard home directory (~/)Runs unimpeded without breaking legitimate developer/admin workflows

(3) Multi-Layer Malware & Ransomware Defense & Canaries

IDScenarioVerification MethodPass Criterion
SP-1Canary tamper detectionTruncate or rename decoy canary file (.xlsx) in ~/Documents/Tamper detected within seconds, triggering immediate Air-Gap
SP-2Ransomware bulk-encryption burst detectionSingle process rapidly generates multiple high-entropy filesOffending worker process frozen with SIGSTOP; Air-Gap triggered
SP-2bNo false positive on shredRun shred -n 3 -u across test filesAllow-list match prevents false-positive detection or freeze
SP-2cCritical processes never frozenValidate freeze target logic during burst detectionCritical daemons (dockerd, systemd, containerd) are never frozen
SP-4EICAR test string is notify-onlyWrite EICAR test string to ~/Downloads/No notification, notification-history entry only; file is never auto-quarantined
SP-4bReal malware signature confirm-firstWrite real signature outside watched foldersRaises modal approval dialog; quarantined only upon user confirmation
SP-4cQuarantine Vault permission hardeningInspect permissions of vault directory and sample filesVault directory set to 0700, quarantined samples set to 0400
SP-4dPrivileged IPC safe original-file restorationSend restore_quarantine_file IPCFile restored to original path with user permissions (0644) and added to exclusions

(4) Passive Link Guard & Phishing Defense

IDScenarioVerification MethodPass Criterion
SP-7mode = block hard enforcementConnect to feed-listed or blocklistExtra hostsDropped at TLS layer (SNI parsed); unlisted safe hosts reach HTTP 200
SP-7bmode = warn fail-closed holdConnect to warning host with no user response (timeout 8s)Drops packets upon timeout (fail-closed); allowed only on explicit approval
SP-7cmode = off complete teardownDisable link guardinet roamswitch_linkguard table is completely uninstalled

(5) BadUSB Mitigation & Device Control

IDScenarioVerification MethodPass Criterion
SP-6Connected keyboard grandfatheringCheck keyboards attached at daemon startupExisting devices grandfathered; only subsequent hot-plugs grabbed

(6) Penetration Testing Suite in Docker Environment (17 Checks)

IDAttack VectorSimulation DetailDefense / Detection Criteria
PENT-1C2 ExfiltrationEgress HTTP/TCP to C2 attempted under Air-GapAll packets dropped at netfilter; zero leakage
PENT-1bFW Force DeletionPurge rules with nft delete table inet roamswitchAnti-Clobber Self-Healing restores rules within seconds
PENT-1cAir-Gap Safe RestoreIssue authenticated disable_air_gap IPCDrop rules removed; markers deleted; connectivity restored
PENT-2/tmp BackdoorExecute dropped backdoor script in /tmpDenied by noexec mount protection (Permission denied)
PENT-2b/dev/shm Shared MemoryExecute binary payload placed in /dev/shmBlocked by noexec mount defense
PENT-2cUser Space PreservationRun script in legitimate user directory (/home/tester/)Unimpeded execution; zero workflow interruption
PENT-3ptrace Memory SnoopingPTRACE_ATTACH to target to extract secrets/tokensDenied by Yama LSM kernel.yama.ptrace_scope=1
PENT-3bCore Dump Secret ScrapingTrigger process crash to obtain memory core dumpPrevented by fs.suid_dumpable=0
PENT-4Canary TamperingZero-out canary decoy file in ~/Documents/Real-time detection of size anomaly & hash change
PENT-4bCanary Bulk WipingMass deletion of canary decoy filesFile disappearance detected immediately, raising incident
PENT-5IP Address SpoofingTransmit packets with spoofed source IPDropped by rp_filter=1 (reverse path filter)
PENT-5bSYN Flood DoSFlood half-open TCP connectionsState table protected by tcp_syncookies=1
PENT-5cICMP Route HijackInject fake ICMP Redirect packetsIgnored due to accept_redirects=0
PENT-6Cyrillic Homograph PhishingNavigate to gооgle.com (Cyrillic homograph)Identified by LinkGuard, scored 40 (DANGEROUS / block)
PENT-6bRaw IP & Plaintext TrafficDirect outbound connection to raw IPAccurately flagged as safety risk factor
PENT-7OpenAI API Key LeakExpose sk-proj-... token in code or logsDetected via Shannon entropy analysis & masked
PENT-7bGitHub Token LeakExpose ghp_... personal token in codeMatched by token pattern and entropy verification
PENT-8Kernel LPE (Frag Gap) PreemptionUnprivileged unshare -U -r (CVE-2026-53362 attempt)Rejected immediately by user.max_user_namespaces = 0 (Operation not permitted)
PENT-9eBPF Threat Sensor & Process FreezeCritical/Emergency alert injected via Falco eBPF socketProcess frozen via SIGSTOP within milliseconds (state: Ts); Air-Gap deployed

(7) Penetration Testing Summary (Standalone Hardened vs. eBPF Enhanced)

Summary of results from the full penetration test executed in an isolated Docker environment (Target: Ubuntu 24.04 with RoamSwitch Client Edition; Attacker: Nmap / curl / hping3; see official report docs/CLIENT_PENTEST_REPORT.ja.md):

Verification CategoryWithout Falco (Standalone Hardened)With Falco (eBPF Enhanced)Defense Verdict
External Port Scan Resistance (Nmap SYN)PASS (100% Filtered / DROP in Lockdown)PASS (100% Filtered / DROP in Lockdown)Total External Stealth
External Direct Access Cutoff (TCP/80, 3306)PASS (Timeout drop / zero response)PASS (Timeout drop / zero response)Zero Inbound Exposure
Kernel LPE (Frag Gap) MitigationPASS (unshare blocked via sysctl)PASS (eBPF alert + SIGSTOP freeze + Air-Gap)Defense-in-Depth Verified
Unauthorized Port Detection (Anomaly)PASS (0.0.0.0 bind flagged HIGH risk)PASS (0.0.0.0 bind flagged HIGH risk)100% Detected & Guided
Log Bloat & CPU Overhead PreventionZero log footprint (uninstalled)PASS (95% noise cut / 2MB buffer / 50MB cap)Desktop Optimized

C.3 Known limitations

LimitationImpactMitigation
On some kernels, FAN_MARK_MOUNT on tmpfs (/tmp) delivers no eventsfile writes under /tmp are not monitored by fanotify/tmp is ephemeral and not a monitored user folder; ransomware targets ~/. Mark failures are logged explicitly
No fanotify delivery on container overlayfs / tmpfsin-container entropy-burst detection cannot be validatedvalidated on real hardware (ext4); the harness auto-SKIPs via a probe
RoamSwitch’s nftables operations share the netfilter space with Docker’s iptables-nftRoamSwitch rule operations can interfere with Docker’s NAT rulesdocumented as an operational caveat; isolated in the dedicated inet roamswitch table, but whole-ruleset operations such as nft flush ruleset should be avoided
The link guard sees a browser’s DoH only via TLS SNI; Encrypted Client Hello (ECH) hides that tooonce ECH is widely deployed, an ECH-using connection to an ECH-supporting site is not identifiable by hostnamethe local blocklist and heuristics still act on DNS/HTTP and non-ECH TLS; DNS threat protection (§4.5, opt-in) covers the resolver path
The daily updater’s freshness depends on the release cadence when updates.enabled = falsewith updates off, the feed is only as current as the last apt upgradethe highest-severity detections (brand homograph) need no feed; opt back in, or enable threat-protection DNS, for always-current coverage