Setup and configuration

Three levels, and the order that picks one

Switch between Trusted, Balanced and maximum lockdown by editing the settings file, decide which networks to register, and see trust lowered when the machine shows signs of compromise.

  • Level: Intro
  • About 30 min
  • 2026-11-12

#Three levels, and the order that picks one

Protection level switching by network switches how strict the firewall is, based on the network you are on. There are three levels. This module is for the client edition (the roamswitch package). The Server Edition does not have this switch.

LevelSetting valueWhat inbound traffic passes (the inet roamswitch table seen in this module)
TrustedopenEverything
BalancedbalancedReplies to connections this machine started, loopback, and ICMP such as ping
Maximum lockdownlockdownReplies to connections this machine started, loopback, and IPv6 neighbor discovery only. Ping is dropped too

No level blocks outbound traffic. The daemon picks the level in this order (checked in the source):

  1. During Air-Gap, it does not switch
  2. If there is a manual override (manual_override), it uses that
  3. If the gateway's MAC address is registered (trusted_networks), it uses the registered level
  4. With no network connection, it uses maximum lockdown
  5. On an unregistered network, it uses away_protection_level (default lockdown)

The settings live in the user's ~/.config/roamswitch/config.json. Right after installing, neither the file nor its folder exists. The daemon watches the file and applies a change within seconds. This module uses that file and the command line only, with no GUI.

Practice in a throwaway environment

The steps in this module change the firewall and the state of SSH. The VM here is driven with tart exec, so work goes on even when SSH stops. On a real machine, set up a way in that is not SSH (a local screen, for example) first.

#Check the current level

Install the client edition. It cannot be installed alongside the Server Edition. In this module's VM, roamswitch-server was removed first.

Install it and read the first level

These commands install a package and add network settings (they change the machine). The apt repository is the same one the Server Edition uses. This VM was tried without recommended packages.

sudo apt install --no-install-recommends roamswitch
systemctl is-active roamswitch
Output
active

There are three places to read the level: the state file the daemon writes, the firewall table, and the state of sharing services.

sudo cat /run/roamswitch/state.json
sudo nft list table inet roamswitch
roamswitch sharing status
Output
{"active_level":"lockdown","air_gap":false,"fanotify_ready":true,"network_trusted":false,"usb_zero_trust":true}
table inet roamswitch {
	chain input {
		type filter hook input priority filter; policy drop;
		ct state established,related accept
		iif "lo" accept
		icmpv6 type { nd-neighbor-solicit, nd-neighbor-advert } ip6 hoplimit 255 accept
		icmpv6 type nd-router-advert ip6 hoplimit 255 ip6 saddr fe80::/10 accept
	}

	chain output {
		type filter hook output priority filter; policy accept;
	}
}
  🔗 Automatic sharing-service control
  Setting: Enabled
  Currently stopped: ssh.socket, ssh.service

Nothing is registered yet, so active_level is lockdown. air_gap says whether Air-Gap is on. Automatic stop and restore of sharing services is at work, and the SSH listener (ssh.socket) is stopped too.

To see what reaches the machine from outside, you need "another device on the LAN". Create a network namespace lan inside the VM, then try ping and the SSH port from 10.99.0.2 to the VM at 10.99.0.1.

sudo ip netns add lan
sudo ip link add veth-h type veth peer name veth-l
sudo ip link set veth-l netns lan
sudo ip addr add 10.99.0.1/24 dev veth-h
sudo ip link set veth-h up
sudo ip netns exec lan ip addr add 10.99.0.2/24 dev veth-l
sudo ip netns exec lan ip link set veth-l up
sudo ip netns exec lan ping -c 2 -W 1 10.99.0.1 | grep transmitted
sudo ip netns exec lan nc -z -w 2 10.99.0.1 22; echo "rc=$?"
Output
2 packets transmitted, 0 received, 100% packet loss, time 1016ms
rc=1

Neither ping nor the SSH port gets through. Maximum lockdown drops every connection that starts outside.

When you finish the module, remove the namespace with sudo ip netns del lan. Its partner veth-h goes with it.

#Register your home network

A network is recognized by the MAC address of its gateway (the router), not by its name.

Register the gateway as Trusted

This rewrites the settings file (it changes the machine). To undo, delete the trusted_networks entry and save.

Look up the gateway's address and MAC address.

ip route show default
ip neigh show 192.168.64.1
Output
default via 192.168.64.1 dev enp0s1 proto dhcp src 192.168.64.17 metric 100 
192.168.64.1 dev enp0s1 lladdr d2:c0:50:cd:95:66 PERMANENT 

PERMANENT shows that RoamSwitch has pinned the gateway's ARP entry. At every level other than Trusted, it pins it against spoofing. Put this MAC address in trusted_networks.

mkdir -p ~/.config/roamswitch
cat > ~/.config/roamswitch/config.json <<'J'
{
  "language": "en",
  "trusted_networks": [
    {"name": "lab-home", "mac": "d2:c0:50:cd:95:66", "level": "open"}
  ]
}
J
sleep 8; sudo cat /run/roamswitch/state.json
Output
{"active_level":"open","air_gap":false,"fanotify_ready":true,"network_trusted":true,"usb_zero_trust":false}

A few seconds after saving, active_level is open. No daemon restart is needed. In this VM, the switch came about 3 seconds after the write. Now look at the inbound table and SSH.

sudo nft list table inet roamswitch
systemctl is-active ssh.socket
sudo ip netns exec lan ping -c 2 -W 1 10.99.0.1 | grep transmitted
sudo ip netns exec lan nc -z -w 2 10.99.0.1 22
roamswitch notifications | sed -n 5,7p
Excerpt
table inet roamswitch {
	chain input {
		type filter hook input priority filter; policy accept;
	}

	chain output {
		type filter hook output priority filter; policy accept;
	}
}
active
2 packets transmitted, 2 received, 0% packet loss, time 1023ms
Connection to 10.99.0.1 22 port [tcp/ssh] succeeded!
  2026-11-12T03:15:24.722542865+00:00
  🛡️ RoamSwitch Protection Profile Switched
    Protection level changed to [Trusted network (Open)]. Reason: your setting for the registered network “lab-home”

The table drops nothing. SSH, which had been stopped, is back, and the SSH port is reachable from the LAN. At Trusted the gateway's ARP pin is also released (ip neigh shows REACHABLE or similar instead of PERMANENT).

Leaving out `level`

An entry in trusted_networks with no level is treated as open (Trusted). In this module's VM, adding an entry with no level while at maximum lockdown turned active_level into open. Always write level when you register a network.

Which networks to register, and at which level
  • Trusted (open): your home network, on a router only you manage, when devices on the LAN use this machine's SSH or file sharing
  • Balanced (balanced): a network like an office, with an administrator but other people's devices on the same LAN. It answers ping and drops connections that start outside
  • Do not register: Wi-Fi in cafes, hotels and airports. Unregistered networks get maximum lockdown by default

When unsure, leave it unregistered. away_protection_level changes how unregistered networks are handled. In this VM, balanced gave Balanced and open gave Trusted. With open, protection is off everywhere.

#Balanced, and stopping sharing services

Balanced answers ping but drops connections that start outside. This section also looks at how sharing services are stopped.

Switch to Balanced and check sharing services

This rewrites the settings file (it changes the machine). Write level back to open to undo.

sed -i 's/"level": "open"/"level": "balanced"/' ~/.config/roamswitch/config.json
sleep 8; sudo cat /run/roamswitch/state.json
sudo nft list table inet roamswitch | sed -n 2,9p
systemctl is-active ssh.socket
sudo ip netns exec lan ping -c 2 -W 1 10.99.0.1 | grep transmitted
sudo ip netns exec lan nc -z -w 2 10.99.0.1 22; echo "rc=$?"
Output
{"active_level":"balanced","air_gap":false,"fanotify_ready":true,"network_trusted":false,"usb_zero_trust":false}
	chain input {
		type filter hook input priority filter; policy drop;
		ct state established,related accept
		iif "lo" accept
		ip protocol icmp accept
		ip6 nexthdr ipv6-icmp accept
	}

inactive
2 packets transmitted, 2 received, 0% packet loss, time 1041ms
rc=1

Ping passes; the SSH port does not. network_trusted is false on a registered network too, unless its level is open. Automatic sharing-service control stops SSH at every level other than Trusted.

Turn the automatic control off and SSH stays running. Even so, the Balanced firewall still drops connections from outside.

sudo roamswitch sharing off
systemctl is-active ssh.socket
sudo ip netns exec lan nc -z -w 2 10.99.0.1 22; echo "rc=$?"
sudo roamswitch sharing on
Output
  ✓ Automatic sharing-service control disabled. Stopped services will be restored.

active
rc=1
  ✓ Automatic sharing-service control enabled.
  ⚠️  SSH / Samba / remote desktop are stopped at every level other than Trusted (Open): on unregistered networks and on networks registered as Balanced or Lockdown (active SSH sessions will be disconnected).

There are two layers: stopping sharing services, and the firewall. sharing off writes false to sharing_service_control_enabled in the settings file. Run over SSH without sudo, it says "Only root or a user with a local login session on this machine can change settings" and changes nothing (exit code 1). Use sudo, or run it from a local login on the machine.

Turning the automatic control on for a machine you reach over SSH

As sharing on says, moving to an untrusted network cuts running SSH sessions. On a machine you can only reach over SSH, register the current network first.

#Pin a level for a while, or stop everything now

To be stricter than the automatic choice, pin a level by hand. The pin is manual_override in the settings file.

KeyValues
manual_overrideopen, balanced, lockdown. null returns to automatic
manual_override_duration_modeuntil_disconnect (default; until the network changes), 1hour, 2hours, 4hours, today, permanent
manual_override_started_atWhen the pin began (Unix seconds). Used to work out the duration

The list of values was checked in the source. This module ran 1hour.

Pin maximum lockdown for one hour

These commands rewrite the settings file (they change the machine). When the time is up, the daemon undoes it by itself.

python3 - <<'X'
import json, os, time
p = os.path.expanduser('~/.config/roamswitch/config.json')
c = json.load(open(p))
c['manual_override'] = 'lockdown'
c['manual_override_duration_mode'] = '1hour'
c['manual_override_started_at'] = int(time.time())
json.dump(c, open(p, 'w'), indent=2)
X
sleep 6; sudo cat /run/roamswitch/state.json
sudo ip netns exec lan ping -c 2 -W 1 10.99.0.1 | grep transmitted
Output
{"active_level":"lockdown","air_gap":false,"fanotify_ready":true,"network_trusted":false,"usb_zero_trust":true}
2 packets transmitted, 0 received, 100% packet loss, time 1032ms

A pin also takes effect within seconds of saving. The history keeps "Protection level changed to [Maximum lockdown]. Reason: you set it by hand (manual_override)". Instead of waiting an hour, move the start time one hour back to make it expire.

python3 - <<'X'
import json, os, time
p = os.path.expanduser('~/.config/roamswitch/config.json')
c = json.load(open(p))
c['manual_override_started_at'] = int(time.time()) - 3600
json.dump(c, open(p, 'w'), indent=2)
X
sleep 8; sudo cat /run/roamswitch/state.json
grep manual_override ~/.config/roamswitch/config.json
roamswitch notifications | sed -n 6,7p
Output
{"active_level":"open","air_gap":false,"fanotify_ready":true,"network_trusted":true,"usb_zero_trust":false}
  "manual_override": null,
  "manual_override_duration_mode": "1hour",
  "manual_override_started_at": null,
  🔄 Manual override cleared
    The selected duration elapsed — reverted to automatic network detection.

The daemon wrote manual_override back to null and returned to the registered level (Trusted). It also left a notification.

To end a pin before it expires, set manual_override and manual_override_started_at to null by hand. In this VM, it went back to the registered level (Trusted) about 3 seconds after saving.

Stronger than pinning a level is Air-Gap (emergency full cut-off). It stops everything, outbound traffic included.

Trigger Air-Gap, then lift it

This stops all network traffic (it changes the machine). Lift it with sudo roamswitch airgap disable.

sudo roamswitch airgap enable
sudo ip netns exec lan ping -c 2 -W 1 10.99.0.1 | grep transmitted
curl -s -o /dev/null -m 5 -w '%{http_code}\n' https://lafine.net/updates/v1/manifest; echo "rc=$?"
Excerpt
🚨 Triggering emergency Air-Gap isolation...
  ✓ All external network communication has been severed.
  There is no desktop here, so no recovery window was shown. To lift it, run `roamswitch airgap disable` on this machine (SSH cannot reach it while it is cut off).

2 packets transmitted, 0 received, 100% packet loss, time 1062ms
000
rc=28

Ping from outside and HTTPS going out both stopped (rc=28 is curl's timeout). The VM has no screen, so no recovery window opens, and the command tells you to lift it on this machine. While it is on, air_gap in state.json is true. roamswitch airgap with no argument only prints the state and usage. It does not cut anything.

sudo roamswitch airgap disable
sudo cat /run/roamswitch/state.json
curl -s -o /dev/null -m 5 -w '%{http_code}\n' https://lafine.net/updates/v1/manifest
Output
  ✓ Air-Gap isolation lifted; normal network communication restored.

{"active_level":"open","air_gap":false,"fanotify_ready":true,"network_trusted":true,"usb_zero_trust":false}
200

Once lifted, it goes back to the level that fits the current network (here, Trusted). state.json is rewritten before the disable command returns, so air_gap already reads false right after it. Three runs in a row on this VM all read false.

#Lower trust when the machine shows signs

Trusted is a judgment about the network. If the machine itself may be compromised, leaving it open to the LAN is a problem. Lowering trust by device state lowers a Trusted registration to Balanced while a serious detection is recent.

What lowers it is a detection of severity high or above from Canary, eBPF Guard, Honeytoken, Exec Recorder or Persistence. The window is 6 hours by default (device_posture_window_hours, 1 to 72). It never lowers to maximum lockdown. In the source, it is re-checked every 30 seconds.

Lower trust with an autostart entry

This places a harmless dummy autostart file. The /tmp/rs-lab-dummy it names does not exist, and nothing runs. At the end you delete the file and record a confirmation (it changes the machine).

mkdir -p ~/.config/autostart
printf '[Desktop Entry]\nType=Application\nName=rs-lab\nExec=/tmp/rs-lab-dummy\n' > ~/.config/autostart/rs-lab.desktop
sleep 45
sudo cat /run/roamswitch/state.json
roamswitch posture
Output
{"active_level":"balanced","air_gap":false,"fanotify_ready":true,"network_trusted":false,"usb_zero_trust":false}
=====================================================
  Trusted networks and this machine's state
=====================================================

  device_posture_trust  : on
  window                : 6 hours
  Detections that count: Persistence
  Newest detection: 0 min ago
  Goes back by itself (if nothing new): in 5 h 59 min
  Right now a network registered as Trusted (Open) runs at Balanced protection.
  Once you looked at the detections and they are fine: sudo roamswitch posture ack

The notification history keeps the detection, the lowered level, and the stopped sharing services.

roamswitch notifications | sed -n 5,16p
Excerpt
  2026-11-12T03:16:52.813532718+00:00
  Trusted network lowered to Balanced protection
    Persistence reported a detection within the last 6 hours. The firewall is now on instead of staying open for this trusted network. If you checked and it is fine, `sudo roamswitch posture ack` goes back to trusted. With no new detection it goes back by itself after 6 hours.

  2026-11-12T03:16:52.791434579+00:00
  🔒 Sharing services stopped
    SSH / file sharing / remote desktop were stopped because a detection on this machine lowered the trusted network to Balanced. They start again when it goes back to Trusted.

  2026-11-12T03:16:38.094531553+00:00
  🚨 Suspicious Autostart Entry Detected
    A new (or changed) autostart entry was found in /home/admin/.config/autostart/rs-lab.desktop. Command: /tmp/rs-lab-dummy / Reason: runs a program from a temporary or hidden folder. If you do not recognize it, inspect the file and disable it. (Notification only; nothing was blocked.)
    The process that made the change cannot be identified (it was found by comparing the state of the files periodically)

As the last line says, this detection came from comparing files periodically. The process that wrote the file is not known.

Deal with the cause of the detection, then record the confirmation.

rm ~/.config/autostart/rs-lab.desktop
sudo roamswitch posture ack
sleep 8; sudo cat /run/roamswitch/state.json
Output
  ✓ Confirmed. Within a few seconds a trusted network goes back to the level you gave it.
  A detection after this confirmation lowers it again.
{"active_level":"open","air_gap":false,"fanotify_ready":true,"network_trusted":true,"usb_zero_trust":false}

It is back to Trusted. Deleting the file alone does not bring it back until the 6 hours pass. Use ack only after you have looked and decided nothing is wrong.

On the same VM, the same file was put back after the ack. It was detected again, and about 45 seconds later the level dropped to Balanced again. Deleting and putting back counts as a new sign.

#What this does not show

What was checked, and what was not

Checked on a throwaway Ubuntu 24.04 VM (wired, no screen):

  • Wi-Fi was not tried. The VM is wired. That networks are told apart by the gateway's MAC address, not the Wi-Fi name, was checked in the source and in how this VM behaved
  • The GUI is not covered. This module used the settings file and commands only
  • No move between networks was made. The registration was read by rewriting config.json. That an until_disconnect pin ends on a move was only checked in the source
  • Detection keeps working on a Trusted network. The level changes the inbound table, sharing services, the gateway's ARP pin and similar. The Persistence detection in this module happened while the level was Trusted
  • The "device on the LAN" is a network namespace inside the VM. Traffic from a real separate device goes through the same inbound table
Check yourself

You added your home router to trusted_networks but forgot level. A few seconds after saving, what is active_level?

Show the explanation

An entry with no level is treated as open (Trusted). The machine opens to the LAN without you having written that. Always write level when you register a network.

Check yourself

At home, on a network registered as Trusted, the level dropped to Balanced and roamswitch posture shows "Detections that count: Persistence". What is the right response?

Show the explanation

It dropped because the machine shows a sign of compromise. An ack without looking opens the machine to the LAN with that sign still there. Only the firewall goes on and nothing is isolated, so there is time to look.

#What to learn next

Features covered
What was verified

Verified on 2026-11-12. Target: Linux 1.17.39. In a disposable Ubuntu 24.04 (arm64, wired, headless) VM, removed roamswitch-server, installed the client roamswitch 1.17.39 from the public apt repository, used a network namespace inside the VM as the LAN device, and ran every listed command plus state.json right after airgap disable (three times), a re-placed autostart file, a missing level, away_protection_level and sharing on over SSH, in Japanese and English.

Command output varies with the product version and the environment. If yours differs, check the version first.