#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.
| Level | Setting value | What inbound traffic passes (the inet roamswitch table seen in this module) |
|---|---|---|
| Trusted | open | Everything |
| Balanced | balanced | Replies to connections this machine started, loopback, and ICMP such as ping |
| Maximum lockdown | lockdown | Replies 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):
- During Air-Gap, it does not switch
- If there is a manual override (
manual_override), it uses that - If the gateway's MAC address is registered (
trusted_networks), it uses the registered level - With no network connection, it uses maximum lockdown
- On an unregistered network, it uses
away_protection_level(defaultlockdown)
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.
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.
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 roamswitchactiveThere 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{"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.serviceNothing 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=$?"2 packets transmitted, 0 received, 100% packet loss, time 1016ms
rc=1Neither 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.
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.1default 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{"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,7ptable 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).
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.
- 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.
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=$?"{"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=1Ping 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 ✓ 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.
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.
| Key | Values |
|---|---|
manual_override | open, balanced, lockdown. null returns to automatic |
manual_override_duration_mode | until_disconnect (default; until the network changes), 1hour, 2hours, 4hours, today, permanent |
manual_override_started_at | When the pin began (Unix seconds). Used to work out the duration |
The list of values was checked in the source. This module ran 1hour.
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{"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 1032msA 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{"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.
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=$?"🚨 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=28Ping 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 ✓ Air-Gap isolation lifted; normal network communication restored.
{"active_level":"open","air_gap":false,"fanotify_ready":true,"network_trusted":true,"usb_zero_trust":false}
200Once 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.
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{"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 ackThe notification history keeps the detection, the lowered level, and the stopped sharing services.
roamswitch notifications | sed -n 5,16p 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 ✓ 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
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 anuntil_disconnectpin 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
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
levelis treated asopen(Trusted). The machine opens to the LAN without you having written that. Always writelevelwhen you register a network.
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
ackwithout 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
- Where detection alerts are sent is covered in Notifications and event forwarding
- Adding false positives to the allowlist is covered in Tuning out false positives
- Every settings key is in the Linux CLI reference
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.