§1Overview
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).
It identifies the connected Wi-Fi / wired network by its gateway MAC address and autonomously switches the active nftables firewall profile. It adds behavioral ransomware detection (fanotify + Shannon entropy + canaries), BadUSB mitigation, a 20-item security health assessment, and a built-in Model Context Protocol (MCP) server.
This document describes the architecture, threat model, and design of each defense mechanism, and how the principle of “fully local processing, zero data sent off the machine” 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).
| Principle | Detail |
|---|---|
| Zero telemetry | The product code never contacts an external server for any purpose. No telemetry, analytics, crash reporting, usage metrics, remote config, auto-update download, or license-activation server. |
| Fully local processing | Every assessment, detection, and audit runs to completion locally. URL safety analysis and secret scanning evaluate their input heuristically without transmitting it. |
| Least privilege | Privileged operations are confined to a root daemon; the UI, CLI, and MCP request them over a Unix-domain-socket IPC. No TCP/UDP sockets anywhere. |
| Fail closed | Air-Gap (emergency isolation) survives a daemon restart and is re-asserted within seconds if its rules are externally deleted. |
| Offline licensing | Pro / Business licenses are verified purely by Ed25519 signature check. No call to an activation server occurs. |
§2Threat model
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 spoofing (MitM), 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.
- 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).
§3Components and trust boundaries
RoamSwitch for Linux is implemented in Rust across six crates. All communication with the daemon goes through /run/roamswitch/roamswitch.sock (a Unix domain socket). There is no listening TCP/UDP socket in any component. 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 front-end
roamswitch-mcp stdio JSON-RPC (spawned by the AI client)
│
│ 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 application)
· fanotify guard / canary engine / quarantine vault
· udev USB monitoring (BadUSB) / Ed25519 license verification
roamswitch-core pure-logic library (shared by all crates)
roamswitchkit MCP client SDK
| Component | Privilege | Role |
|---|---|---|
| roamswitch-daemon | root (systemd) | All privileged operations. As the single writer, controls nftables, DNS, and systemd units. |
| roamswitch-app | login user | GTK3 GUI + tray. Configuration editing and visualization only; privileged actions go through IPC. |
| roamswitch (CLI) | login user | Thin shell front-end using the same IPC socket. |
| roamswitch-mcp | spawned by the AI client | stdio JSON-RPC. Provides read-only diagnostic information. |
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.
§4Autonomous network defense
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.
| Profile | Situation | nftables behavior |
|---|---|---|
| open (trusted) | A registered home or corporate network | policy accept. Local traffic allowed. |
| balanced | Unregistered but configured as relatively safe | Default-deny inbound. Only established connections and lo allowed; exposed-port exposure monitored. |
| lockdown (away protection) | Public Wi-Fi / unregistered network | All 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 under Air-Gap; only lo and ct established allowed
}
}
§5Layered malware / ransomware defense
Shannon-entropy burst detection
Write events are tracked per PID. If a single process creates 6 or more distinct files with entropy ≥ 7.85 within 3 seconds, it is treated as ransomware bulk-encryption: the process is frozen with SIGSTOP and Air-Gap is triggered.
- False-positive mitigation — roughly 60 tools that legitimately write high-entropy data (
shred,gpg,ffmpeg,tar,borg,restic,dockerd,qemu, …) are allow-listed. 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/shmat the mount level and/at the filesystem level viaFAN_CLASS_CONTENT(fallbackFAN_CLASS_NOTIF). - Detected malware is relocated to
~/.local/share/roamswitch/quarantine/. The vault is0700; each sample is0400(read-only, non-executable). On restore, the file returns to0644and is chowned to the destination directory’s owner. - 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.
§6BadUSB mitigation
udev USB events are monitored and keyboard-class (HID) devices are tracked.
- On by default. The first-run wizard adds the keyboards connected at that moment to a whitelist.
- Thereafter, an unregistered keyboard present at daemon startup is treated as suspect (a device is not trusted unconditionally just because it was present on first connection).
- When an unregistered keyboard connects, a desktop notification is shown and the warning persists until the user explicitly authorizes it.
§7Security health assessment (20 items)
The LinuxHealthChecker in roamswitch-core evaluates 20 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 20 items appear in the user’s language (10 languages).
| Category | Example items |
|---|---|
| Disk / boot | LUKS / dm-crypt encryption, UEFI Secure Boot |
| Access control | LSM (AppArmor / SELinux), SSH / sudo config audit |
| Kernel hardening | sysctl hardening, core-dump control, noexec on /tmp and /dev/shm |
| Updates | automatic security update configuration |
| Network | ARP spoofing monitoring, exposed-port audit, Wi-Fi encryption strength |
| Malware | ClamAV / fanotify / entropy monitoring / noexec |
| Browser | Firefox / Chrome / Chromium Safe Browsing settings |
| DNS / USB | threat-protection DNS (Pro), 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.
§8MCP server security model
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.
| Tool | Purpose |
|---|---|
| get_security_report | 20-item assessment, score, recommendations |
| get_exposed_ports | exposed ports and their firewall block status |
| get_guard_status | current protection level and defense-module state |
| audit_url_safety | heuristic URL safety analysis (does not fetch the target) |
| audit_secrets | detection of API keys / secrets in text (does not transmit the text) |
| audit_security_logs | aggregation of local journald / auth logs |
| get_quarantine_status / get_canary_status | quarantine and canary state |
| get_app_help | search of the built-in knowledge base |
The MCP server uses no network at all; it either connects to the daemon’s Unix socket via roamswitchkit or calls roamswitch-core logic directly.
§9Data handling and Zero Telemetry
lafine systems design audited the entire RoamSwitch for Linux source (v1.0.0, six crates) for outbound communication. RoamSwitch for Linux is proprietary freeware and its source is not published, so the dependency-tree and source inspections below were performed by lafine; the results are in 9.2. The steps in Appendix A are reproducible from the distributed package alone.
Audit method
- Dependency tree inspection — the full
Cargo.lockwas searched for known HTTP clients (reqwest,hyper,ureq,curl, …), TLS stacks (rustls,native-tls,openssl), and telemetry / analytics / crash-reporting SDKs. - Source code inspection — every
.rsfile was searched forTcpStream,TcpListener,UdpSocket,to_socket_addrs,lookup_host, and HTTP URL literals. - Subprocess enumeration — every
Command::new(...)call site was reviewed.
| Audit item | Result |
|---|---|
| HTTP client library | Not present in the dependency tree |
| TLS stack | Not present |
| Telemetry / analytics / crash-reporting SDK | Not present |
TcpStream / TcpListener / UdpSocket | Not a single occurrence in the source |
| Socket API in use | AF_UNIX (local IPC) only |
| License activation | Offline Ed25519 signature verification. No activation-server traffic |
| Auto-update | Reads local apt-cache policy output only. Performs no network fetch |
The RoamSwitch for Linux product code never initiates communication with an external server for any purpose.
Complete list of externally-facing operations
For transparency, every operation that could cause a packet to leave the machine even indirectly. None is RoamSwitch itself sending an HTTP request; each is an explicit user action or a configuration change to an existing OS mechanism.
| # | Operation | Trigger | What is sent |
|---|---|---|---|
| 1 | ping -c1 -W1 <gw> | to refresh the ARP cache during ARP-spoofing detection; destination is the LAN default gateway only | ICMP echo request only. No DNS resolution, no payload |
| 2 | launching the system browser (xdg-open) | 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 |
| 3 | freshclam | only when the user clicks “Update Virus Signatures”. Never automatic or scheduled | traffic of ClamAV itself (a separate project); RoamSwitch only launches it |
| 4 | resolvectl dns … | threat-protection DNS on an untrusted network (Pro, opt-in) | only changes the OS resolver setting; RoamSwitch itself sends no DNS query |
| 5 | package-manager updates | when the user runs apt upgrade, etc. | RoamSwitch only reads local apt-cache policy output; it initiates no network fetch |
Every other RoamSwitch feature (nftables control, port scanning, ARP monitoring, health assessment, entropy monitoring, canaries, fanotify, USB monitoring, quarantine, log auditing, URL and secret auditing) completes using only local file reads and analysis of subprocess output.
What stays on the machine (privacy)
- No account — no user registration, no login, no email address ever requested.
- Configuration is local only — plaintext at
~/.config/roamswitch/config.json, never leaves the machine. No cloud sync. - Assessment results are local only — displayed on screen and, if the user chooses, exported to a local file.
- License keys are verified offline — Ed25519 signature verified with a public key. No query to a lafine server.
- No crash reporting — on panic, no stack trace is sent anywhere.
§10Licensing and distribution
| Edition | For | Price | License |
|---|---|---|---|
| Community Edition | Linux personal use | Free | Proprietary freeware (bundled EULA) |
| Business (planned) | fleet management, policy distribution, signed internal apt, SLA | Paid | Commercial license. Same binary + tier gate |
Purchasers of macOS RoamSwitch Pro Lifetime receive Business features free on their own Linux machines.
Distribution and supply chain
| Channel | Detail |
|---|---|
| APT | https://lafine.net/apt (stable main). Release signed with an RSA-4096 key |
| RPM | https://lafine.net/rpm (separate Fedora / openSUSE). repomd.xml GPG-signed |
| AUR | roamswitch-bin (fetches the GitHub Releases tarball, verifies sha256) |
| tarball | amd64 / aarch64 .tar.gz + .sha256 on GitHub Releases |
- Pinned glibc floor — release binaries are built inside a Debian 12 (bookworm, glibc 2.36) container, preventing symbol-version mismatches on Debian 12 / Ubuntu 22.04 and later. A “verify glibc floor” CI step enforces this.
- Signing key management — the private signing key exists only in a GitHub Actions encrypted secret; only the public key is committed to the repository.
§11Requirements
- OS: Ubuntu 22.04 / 24.04+, Debian 12+, Linux Mint / Pop!_OS / elementary / Zorin, Raspberry Pi OS 64-bit (Bookworm), Ubuntu for Raspberry Pi
- Architecture: x86_64 / aarch64
- Required:
systemd,nftables(pulled in automatically by the package dependencies) - Optional:
clamav(malware scanning),nmcli(Wi-Fi encryption detection),libnotify(desktop notifications) - 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)
§12Direct dependency crates
| Crate | Purpose | Network |
|---|---|---|
| serde / serde_json | serialization of config and IPC | none |
| tokio | async runtime (only Unix sockets used) | none (no TCP) |
| tracing | logging (local / journald) | none |
| url | URL parsing (audit heuristics) | none (does not fetch) |
| ed25519-dalek | offline verification of license signatures | none |
| base64 / sha2 | license tokens, canary hashes | none |
| libc | chown / kill / syscalls | none |
| open | calls xdg-open for “Open release notes” | the process itself does not communicate |
| GTK3 / ksni (app only) | GUI and tray | none |
There is no HTTP client, TLS stack, or telemetry SDK anywhere in the dependency tree.
Appendix ASelf-check (verify it yourself)
Every claim in this document can be verified on your own machine. The following runs on a host with roamswitch installed.
A.1 Confirm “zero data sent off the machine” yourself (from the package alone)
The source is not published, but the following run on a host with roamswitch installed and each supports “no outbound communication”. The full dependency-tree audit results are in §9.
The distributed binary links no HTTP / TLS library:
ldd "$(command -v roamswitch-daemon)" | grep -iE 'curl|ssl|crypto|nghttp|http'
# -> no output = no HTTP/TLS shared library is linked at all
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
Live measurement over time (no RoamSwitch-owned outbound connection appears):
watch -n 5 'sudo ss -tupn | grep roamswitch || echo "(no external sockets)"'
# -> always "(no external sockets)"
Syscall-level check (connect targets are AF_UNIX only):
sudo strace -f -e trace=connect -p "$(pgrep -x roamswitch-daemon)" 2>&1 | grep -i connect
# -> only connect() to AF_UNIX. No AF_INET / AF_INET6 appears
A.2 Destructive self-test suite (run by lafine during the audit)
lafine maintains a disposable --privileged container (its own network namespace, so an Air-Gap does not cut the host). It is built on Debian 12 (the same glibc floor as the release CI), starts the daemon, and verifies the following in sequence. Results are in A.3.
| ID | Scenario | Pass criterion |
|---|---|---|
| SP-1 | Ransomware-decoy (canary) tamper detection | “Canary tampered” detected within seconds, Air-Gap triggered |
| SP-2 | Ransomware bulk-encryption burst detection | the process is frozen with SIGSTOP, Air-Gap triggered |
| SP-2b | No false positive on shred | no “RANSOMWARE BURST” (allow-listed) |
| SP-2c | Critical processes never frozen | dockerd and similar are not frozen |
| SP-3 | Air-Gap enable → isolation | policy drop on the output hook; external traffic blocked |
| SP-3b | Air-Gap self-healing | external nft delete table → the sentinel loop re-applies within 5 s |
| SP-3c | Air-Gap fail-closed | daemon restart mid-Air-Gap → isolation persists |
| SP-3d | Air-Gap disable → recovery | drop policy lifted, all marker files removed, connectivity restored |
| SP-4 | Malware quarantine (EICAR) | removed from Downloads, relocated to the Quarantine Vault |
| SP-4b | Quarantine Vault permission hardening | directory 0700, sample 0400 |
Additionally verified on real hardware (ext4 ~/) since the container’s overlayfs / tmpfs delivers no fanotify events: SP-2 on host (a python3 burst of 6+ files → freeze → Air-Gap), SP-1 on host (rename of one of 16 monitored canaries detected in ~2 s), SP-5 (MCP get_guard_status manual-override label consistency), SP-6 (BadUSB: an unregistered keyboard present at startup treated as suspect), and every CLI subcommand (status / guards / wifi / ports / canary / quarantine / audit-url / audit-secrets / audit-logs / knowledge / sharing) in both languages.
A.3 Most recent run
Docker self-test (roamswitch-test image): 14 / 15 PASS. The single non-pass is SP-2 (in-container entropy burst), due to the known limitation that a container’s overlayfs / tmpfs delivers no fanotify permission events from the kernel. The harness detects this with a probe and treats it as SKIP rather than FAIL, with a separate PASS confirmed on real hardware (ext4 ~/).
Every defect found during the self-penetration test (the canary’s $HOME-relative path bug, the MCP serde camelCase mismatch, a missing re-assert in disable_air_gap, over-permissive Quarantine Vault modes, a shred false positive, an erroneous freeze of the Docker daemon) has been fixed and re-verified.
A.4 Known limitations
| Limitation | Impact | Mitigation |
|---|---|---|
On some kernels, FAN_MARK_MOUNT on tmpfs (/tmp) delivers no events | file 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 / tmpfs | in-container entropy-burst detection cannot be validated | validated on real hardware (ext4); the harness auto-SKIPs via a probe |
RoamSwitch’s nftables operations share the netfilter space with Docker’s iptables-nft | RoamSwitch rule operations can interfere with Docker’s NAT rules | documented as an operational caveat; isolated in the dedicated inet roamswitch table, but whole-ruleset operations such as nft flush ruleset should be avoided |