Know the attack, stop the attack

Traffic coming in, traffic going out

Using a peer address in a network namespace, you run inbound default deny, port-scan blocking with its source check, egress blocking to malicious IPs and DNS exfiltration detection, all inside one VM.

  • Level: Basics
  • About 60 min
  • 2026-11-12

#Traffic coming in, traffic going out

Network attacks split by direction. Some come from outside into the server (probing ports, touching an open service). Others go from the server out (a hijacked program connecting to a C2 server, data being carried off).

Server Edition watches both directions. This module runs these five.

DirectionFeatureDefaultWhat it does
InDefault-deny inbound and SSH lockout preventionOnLets in only the ports you allow
InAudit of externally exposed portsRun by handLists listening ports and points out risky ones
InDetection and blocking of inbound port scansOnFinds a source probing many ports and blocks it for 10 minutes
OutBlocking outbound traffic to malicious IPsOnDrops traffic to IPs on a list
OutDNS tunneling detectionOnReports the shapes of DNS exfiltration and name brute-forcing

Three of them block: the default-deny inbound rules, the scan-source block and the malicious-IP egress block. The DNS watch only reports. It never stops name resolution.

#Set up a practice network

A scan or a connection looks real only when it comes from another address. So you build a small "other machine" inside the same host with a network namespace. Nothing is sent to the outside network.

Create a peer address in a namespace

This changes the system. Try it in a practice VM. Create a namespace named scanner, with this host at 10.200.0.1 and the peer at 10.200.0.2.

sudo ip netns add scanner
sudo ip link add veth-host type veth peer name veth-scan
sudo ip link set veth-scan netns scanner
sudo ip addr add 10.200.0.1/24 dev veth-host
sudo ip link set veth-host up
sudo ip netns exec scanner ip addr add 10.200.0.2/24 dev veth-scan
sudo ip netns exec scanner ip link set veth-scan up
sudo ip netns exec scanner ping -c1 -W1 10.200.0.1
Excerpt
64 bytes from 10.200.0.1: icmp_seq=1 ttl=64 time=0.045 ms
1 packets transmitted, 1 received, 0% packet loss, time 0ms

sudo ip netns exec scanner <command> runs a command from the peer's address. The ping gets through because the inbound rules accept ICMP.

To clean up, stop the programs you started inside the namespace, then delete it. While a program was still running there, veth-host stayed after ip netns del.

sudo kill $(sudo ip netns pids scanner)
sudo ip netns del scanner

#Inbound: only the ports you allow

Server Edition's firewall denies inbound traffic by default. With the default settings, the rules let in TCP on ssh_ports and allowed_ports, ICMP, replies to connections this host opened, and loopback. UDP gets in only on the ports in allowed_udp_ports. The defaults are allowed_ports=80,443 and an empty allowed_udp_ports.

The SSH ports (ssh_ports) are always let in, even when they are not in allowed_ports. A missing entry should not close the admin entrance.

Find a listener, then open and close a port

Start a development web server on every address (0.0.0.0). First, look at the Audit of externally exposed ports list.

nohup python3 -m http.server 8080 --bind 0.0.0.0 >/dev/null 2>&1 &
sudo roamswitch ports
Output
=====================================================
  🔍 Exposed / Listening Ports & Dev Server Audit
=====================================================

  • Port 8080  python3         PID: 1270   0.0.0.0 [MED] [DevServer] [Blocked from outside by the firewall]
    ↳ 0.0.0.0 bind (blocked by RoamSwitch's firewall): Restricting the host to 127.0.0.1 (localhost) in the launch options is recommended.
    ↳ Frontend / development web server exposed: Start the server with --host 127.0.0.1.
  • Port 22    systemd (ssh.socket) PID: 1      0.0.0.0 (exposed)[HIGH]
    ↳ 0.0.0.0 bind (listening on every network): This is an administrative SSH port (ssh_ports). Removing it would lock you out; keep it listening and limit who may connect with sudo roamswitch ssh-allow.

ports checks both the address a program listens on and whether RoamSwitch's inbound rules let that port in. 0.0.0.0 means it accepts on every address. Port 8080 is not in allowed_ports, so it shows as "Blocked from outside by the firewall".

With sudo, the names of root programs appear too. On this VM ssh.socket starts sshd, so port 22 shows as owned by systemd (ssh.socket).

Starting the listener also left one entry in the notification history. Automatic blocking of unknown port exposure reported it as a listener that was not there when the daemon started.

Excerpt (sudo roamswitch notifications)
  🚨 A new program started listening on the network: TCP 8080
    python3 (PID 1270) listens on TCP 8080. This listener was not there when the daemon started.
    It is not in the ports let in (allowed_ports / allowed_udp_ports), so the firewall drops new connections from outside.
    If you do not recognize it, look into this program (the record: sudo roamswitch port-anomaly).

Now check whether the peer can actually reach it.

sudo ip netns exec scanner curl -s -m 3 -o /dev/null -w '%{http_code}\n' http://10.200.0.1:8080/ ; echo "exit $?"
sudo ip netns exec scanner nc -vz -w 3 10.200.0.1 22
curl -s -m 3 -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/
Output
000
exit 28
Connection to 10.200.0.1 22 port [tcp/ssh] succeeded!
200

From the peer, port 8080 times out (curl exit code 28). The inbound rules drop it. Port 22 gets through, and inside the host (127.0.0.1) port 8080 works. This matches what ports showed.

To open port 8080 to the outside, add it to allowed_ports. From here on, this changes the system. Saving a setting is not enough: a restart applies it.

sudo roamswitch server config set allowed_ports 80,443,8080
sudo ip netns exec scanner curl -s -m 3 -o /dev/null -w 'before restart %{http_code}\n' http://10.200.0.1:8080/
sudo roamswitch server restart
sudo nft list chain inet roamswitch_server input | grep dport
sudo ip netns exec scanner curl -s -m 3 -o /dev/null -w 'after restart %{http_code}\n' http://10.200.0.1:8080/
sudo roamswitch ports | grep -A1 "Port 8080"
Excerpt
  ✓ Server configuration updated and saved. (allowed_ports = 80,443,8080)
  To apply: sudo systemctl restart roamswitch-server or sudo roamswitch server restart

before restart 000
  🔄 Restarting roamswitch-server service...
  ✓ roamswitch-server service successfully restarted.

		tcp dport { 22, 80, 443, 8080 } accept
after restart 200
  • Port 8080  python3         PID: 1270   0.0.0.0 (exposed)[HIGH] [DevServer]
    ↳ 0.0.0.0 bind (listening on every network): If it is not used from outside, change the bind address to 127.0.0.1. If it is, limit the sources that may connect with the firewall.

Once the port is open, ports also shows 8080 as "exposed" and "HIGH".

UDP works the same way. Run a small receiver on port 9999 and send one datagram at a time from the peer. The first goes out while allowed_udp_ports is empty, the second after adding 9999 and restarting.

/tmp/udprecv.py
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('0.0.0.0', 9999))
while True:
    d, a = s.recvfrom(2048)
    open('/tmp/udp.log', 'a').write(f"{a[0]} {d.decode().strip()}\n")
nohup python3 /tmp/udprecv.py >/dev/null 2>&1 &
sudo ip netns exec scanner sh -c 'echo hello-udp-1 | socat - UDP-SENDTO:10.200.0.1:9999'
sudo roamswitch server config set allowed_udp_ports 9999
sudo roamswitch server restart
sudo ip netns exec scanner sh -c 'echo hello-udp-2 | socat - UDP-SENDTO:10.200.0.1:9999'
cat /tmp/udp.log
Output
10.200.0.2 hello-udp-2

Only the second one reached the log. The inbound rules dropped the first.

Put things back.

sudo roamswitch server config set allowed_ports 80,443
sudo roamswitch server config set allowed_udp_ports ""
sudo roamswitch server restart
Don't
  • Leave a port in allowed_ports after you only needed it to try a development server. Remove it when you are done. If only this host uses it, starting it on 127.0.0.1 is enough
  • Edit the rules of another firewall such as ufw at the same time, so you can no longer tell which rule is in effect

#Trigger a port scan and watch the block

Reconnaissance usually starts by finding which ports are open. Detection and blocking of inbound port scans treats it as a scan when one source tries many distinct ports in a short time.

The default is 15 ports (port_scan_detect_min_distinct_ports) within 300 seconds (port_scan_detect_window_secs). On a match it reports, and with port_scan_block_enabled=true (the default) it blocks the source for 600 seconds (port_scan_block_duration_secs).

Scan with nmap and check the block

From the peer, probe ports 1 to 200 on this host. It stays inside your own VM.

sudo ip netns exec scanner nmap -sS -Pn -n -p 1-200 --max-retries 0 -T4 10.200.0.1
Excerpt
Not shown: 198 filtered tcp ports (no-response)
PORT   STATE  SERVICE
22/tcp open   ssh
80/tcp closed http

Nmap done: 1 IP address (1 host up) scanned in 1.51 seconds

filtered ports are the ones the inbound rules dropped silently. Port 80 is closed because it is allowed, but no program listens on it.

The guard reads the kernel log about once a minute. Wait a little, then look at the result.

sudo roamswitch port-scan
sudo nft list set inet roamswitch_portscan blocked_ips
sudo ip netns exec scanner nc -vz -w 3 10.200.0.1 22
Excerpt
  [Detected incidents]
   🚨 [2026-11-12T01:28:11.745990572+00:00] [BLOCKED 10m] 10.200.0.2 - distinct ports: 201 (sample: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)

table inet roamswitch_portscan {
	set blocked_ips {
		type ipv4_addr
		flags timeout
		elements = { 10.200.0.2 timeout 10m expires 9m24s521ms }
	}
}
nc: connect to 10.200.0.1 port 22 (tcp) timed out: Operation now in progress

Port 22 was reachable before the scan, but the peer can no longer open a new connection to it. The block is an entry in a timed set (blocked_ips). When expires runs out, the kernel removes it on its own.

The notification history has two entries, one for the detection and one for the block. The Incident timeline gets one line as well.

sudo roamswitch notifications
sudo roamswitch timeline
Excerpt
  2026-11-12T01:28:11.745766778+00:00
  Scanning source IP blocked: 10.200.0.2
    Host: ubuntu (IP: 192.168.64.5)
    Blocked source: 10.200.0.2
    Block duration: 10 minutes (expires automatically)
    Timestamp: 2026-11-12 01:28:11

  2026-11-12T01:28:11.560763514+00:00
  Incoming port scan detected: 10.200.0.2
    🔍 Connection attempts to 201 distinct ports from one source in a short window, likely reconnaissance (a port scan)
    Host: ubuntu (IP: 192.168.64.5)
    Source: 10.200.0.2
    Sample ports: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
    Timestamp: 2026-11-12 01:28:11

  2026-11-12T01:28:11.746055322+00:00  [Port Scan Guard]  open
    Incoming port scan: connection attempts from 10.200.0.2 to 201 distinct ports in a short window (e.g. 1, 2, 3, 4, 5, 6, 7, 8), source blocked for 10 min
    action taken: Source auto-blocked

  2026-11-12T01:25:25.314494300+00:00  [FIM]  recorded only
    Log tamper baseline was made again
    action taken: Logged

The times are UTC. The [FIM] line in the timeline is a record from the daemon's first start. It is recorded only and raises no notification.

To lift it before it expires, delete the entry from the set. This changes the system.

sudo nft delete element inet roamswitch_portscan blocked_ips "{ 10.200.0.2 }"
sudo ip netns exec scanner nc -vz -w 3 10.200.0.1 22
Output
Connection to 10.200.0.1 22 port [tcp/ssh] succeeded!

After removing it and waiting more than a minute, the same scan did not get the source blocked again. A follow-up probe of only 10 ports (1000 to 1009) raised no new detection either, since it stays under 15 ports.

A scan's source address can be forged. So before blocking a source on the same network, the guard checks that something real is there. If the neighbour table (ip neigh) has no MAC for it, the guard asks with ARP first, then decides.

Give the peer a real address, 10.200.0.3, and 10.200.0.77, which it uses only during the scan, then scan from each. 10.200.0.77 is removed before the guard judges it, so nothing answers ARP for it, just as with a forged source. Empty the neighbour table first, too.

sudo ip netns exec scanner ip addr add 10.200.0.3/24 dev veth-scan
sudo ip netns exec scanner ip addr add 10.200.0.77/24 dev veth-scan
sudo ip neigh flush dev veth-host
sudo ip netns exec scanner nmap -sS -Pn -n -e veth-scan -S 10.200.0.3 -p 1-200 --max-retries 0 -T4 10.200.0.1
sudo ip netns exec scanner nmap -sS -Pn -n -e veth-scan -S 10.200.0.77 -p 1-200 --max-retries 0 -T4 10.200.0.1
sudo ip netns exec scanner ip addr del 10.200.0.77/24 dev veth-scan
sudo ip neigh flush dev veth-host
# wait a little over a minute
sudo roamswitch port-scan | grep "10.200.0.[37]"
sudo nft list set inet roamswitch_portscan blocked_ips | grep elements
ip neigh show dev veth-host
Excerpt
   🚨 [2026-11-12T01:31:12.018525406+00:00] [BLOCKED 10m] 10.200.0.3 - distinct ports: 200 (sample: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
   🚨 [2026-11-12T01:31:15.498924939+00:00] [DETECTED ONLY] 10.200.0.77 - distinct ports: 200 (sample: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
		elements = { 10.200.0.3 timeout 10m expires 9m9s774ms }
10.200.0.3 lladdr 12:c2:5c:db:da:1c STALE 
10.200.0.77 FAILED 

10.200.0.3 answered the query even though the neighbour table was empty, so it was blocked. 10.200.0.77 got no answer (FAILED), so the guard treated it as forged and only reported it. The notification history gets one entry saying it was not blocked, and why.

Excerpt (sudo roamswitch notifications)
  2026-11-12T01:31:15.498638901+00:00
  Scan source not blocked (forged source): 10.200.0.77
    Host: ubuntu (IP: 192.168.64.5)
    Source not blocked: 10.200.0.77
    Why: its address looks forged (an address on this network that no device answers for). Blocking it would cut off the real owner of that address.
    Timestamp: 2026-11-12 01:31:15

The service log (sudo journalctl -u roamswitch-server) also has a line with "source looks forged".

Put it back.

sudo nft delete element inet roamswitch_portscan blocked_ips "{ 10.200.0.3 }"
sudo ip netns exec scanner ip addr del 10.200.0.3/24 dev veth-scan
Keep the automatic block, or only report?
  • Keep it (default): a normal client never connects to dozens of ports one after another. The block applies to new connections only. Established connections and replies to this host's own connections keep working
  • Report only (port_scan_block_enabled=false, applied by a restart): an environment where internal monitoring or assessment tools routinely sweep many ports. A block would cut that monitoring off for 10 minutes at a time

A source address can be forged. So the gateway, the DNS servers and this host's own addresses are never blocked, even when they look like a scan (checked in the code and the config file's comments). A source on the same network is blocked only when something answers ARP for it, as in the lab.

#Stop traffic going out to malicious IPs

Once a server is taken over, a program inside it reaches out to the attacker's server. Blocking outbound traffic to malicious IPs drops outbound traffic to the IPs on a list. That covers this host's own traffic (output) and traffic passing through it (forward, which is how a Docker container's outbound traffic leaves).

The list shipped in the package (/usr/share/roamswitch/threatfeed/malicious_ips.txt) is empty. The operator supplies the addresses. One way is to write them by hand into /var/lib/roamswitch/threatfeed/malicious_ips.txt. The other is to put an https URL in egress_ip_feed_urls: the updater then fetches the public list directly from its source once a day (the default is empty, and nothing is fetched).

Put the peer's address on the list

This changes the system. Run a web server in the peer's namespace, confirm you can reach it, then write its address into the list.

sudo ip netns exec scanner sh -c 'nohup python3 -m http.server 8000 --bind 10.200.0.2 >/dev/null 2>&1 &'
curl -s -m 3 -o /dev/null -w 'before %{http_code}\n' http://10.200.0.2:8000/
sudo mkdir -p /var/lib/roamswitch/threatfeed
echo "10.200.0.2" | sudo tee /var/lib/roamswitch/threatfeed/malicious_ips.txt
Output
before 200
10.200.0.2

The daemon re-reads the list every 5 minutes. No restart is needed. In this run it reached the rules about 297 seconds later. The wait depends on where the 5-minute cycle stands.

sudo nft list chain inet roamswitch_egress_block output
curl -s -m 3 -o /dev/null -w 'after %{http_code}\n' http://10.200.0.2:8000/; echo "exit $?"
Output
table inet roamswitch_egress_block {
	chain output {
		type filter hook output priority mangle; policy accept;
		ip daddr 10.200.0.2 limit rate 5/minute burst 5 packets log prefix "rs_egress_drop: "
		ip daddr 10.200.0.2 drop
	}
}
after 000
exit 28

Within about a minute it appears in the notification history.

Excerpt (sudo roamswitch notifications)
  2026-11-12T01:38:11.519381381+00:00
  Outbound connection blocked: 10.200.0.2
    🛡️ Egress guard dropped an outbound connection to a known-malicious IP
    Host: ubuntu (IP: 192.168.64.5)
    Blocked destination: 10.200.0.2
    Matches in this window: 5
    This includes traffic originating from a Docker container.
    Timestamp: 2026-11-12 01:38:11
    The process that made the connection cannot be identified (the firewall log carries no process information)

The notification shows which destination was attempted. It does not show which program.

To undo it, delete the file you wrote. The next re-read goes back to the empty list. In this run it went back about 213 seconds after the delete, and curl returned 200. Until then, every packet to that address is still dropped, DNS queries included.

sudo rm /var/lib/roamswitch/threatfeed/malicious_ips.txt
Before you add an entry

Traffic to an IP on the list is dropped from this host and from its containers alike. The address of a rented server can later go to someone else. If you find you are blocking a legitimate peer, delete that line.

There is also a setting that stops lookups at the name-resolution stage (dns_sinkhole_enabled). It is off by default. The config file's example says why: it breaks a server that relies on internal (split-horizon) DNS.

#DNS exfiltration and name brute-forcing

DNS queries get out of almost any network. Attackers use that by packing data into long names (DNS tunneling). They also try likely names one after another to find which subdomains exist.

DNS tunneling detection reads the queries this host sends and the replies it receives, and reports two shapes.

ShapeCondition (default)
Tunneling10 suspicious-shaped queries to one domain in 60 seconds. Suspicious means a label longer than 40 characters with high randomness, or the TXT or NULL type
Name brute-forcing40 different names under one domain in 60 seconds, all answered "does not exist" (NXDOMAIN)

It only reports and never stops a query. Even when the watch is down, name resolution is not delayed by design (checked in the code).

Trigger both shapes against a practice DNS server, then try the allowlist

Put a small DNS server in the peer's namespace that answers "does not exist" for every name. Queries reach only the peer and never go to an outside DNS server. The names use example.com and example.net, which are reserved for examples.

/tmp/fakedns.py
# Practice DNS server: answers "does not exist" (NXDOMAIN) for every name
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('10.200.0.2', 53))
while True:
    q, a = s.recvfrom(512)
    # Keep the ID, set the flags to response + NXDOMAIN, echo the question
    r = q[:2] + b'\x81\x83' + q[4:6] + b'\x00\x00\x00\x00\x00\x00' + q[12:]
    s.sendto(r, a)

First, query 12 long names that look like they carry data. Then query 40 common subdomain names.

sudo ip netns exec scanner sh -c 'nohup python3 /tmp/fakedns.py >/dev/null 2>&1 &'
for i in $(seq 1 12); do
  label=$(head -c 30 /dev/urandom | base32 | tr -d '=' | tr 'A-Z' 'a-z' | head -c 48)
  dig +noedns +short +tries=1 +time=1 @10.200.0.2 "$label.exfil-lab.example.com" >/dev/null
done
for w in admin api app beta blog cdn ci cms dev docs ftp git grafana jenkins jira k8s ldap mail mx ns1 ns2 portal proxy shop smtp sso stage static status test vpn web webmail wiki www intranet backup db files login m monitor; do
  dig +noedns +short +tries=1 +time=1 @10.200.0.2 "$w.example.net" >/dev/null
done
sudo roamswitch notifications
Excerpt
  2026-11-12T01:42:40.148964324+00:00
  Possible subdomain brute-forcing detected
    🔍 This host asked for 40 different names under example.net within 60 seconds, and every answer was NXDOMAIN (the name does not exist). That is the shape of subdomain brute-forcing
    Example names: admin.example.net, api.example.net, app.example.net, beta.example.net, blog.example.net
    This host, or a program running on it, is probing names. A security scanner you started, or a compromised process doing reconnaissance, is the usual cause
    To check, run sudo roamswitch events search --since 10m to see which programs started in that period (needs the exec recorder)
    This guard sees only the UDP DNS replies this host receives. DoH, DoT, TCP replies and queries from containers in their own network namespace are not visible
    Host: ubuntu (IP: 192.168.64.5)
    Timestamp: 2026-11-12 01:42:40
    The process that sent the queries cannot be identified (the record of DNS queries carries no process information)

  2026-11-12T01:42:35.736366447+00:00
  DNS tunneling/exfiltration pattern detected
    🔍 This host sent 10 suspicious-shaped DNS queries in a short window (long high-entropy labels, or TXT/NULL query types) — a possible sign of C2 traffic or data exfiltration via a DNS tunneling tool (iodine, dnscat2, etc.)
    Domain: example.com
    Breakdown: TXT 0 / NULL 0 / long high-entropy labels 10
    Host: ubuntu (IP: 192.168.64.5)
    Timestamp: 2026-11-12 01:42:35
    The process that sent the queries cannot be identified (the record of DNS queries carries no process information)

Of the 12 queries sent, the 10th triggered the report. The brute-force report fired at the 40th name. Both reached the notification history within seconds of the queries. The notification says which domain, not which program. The brute-force notification points you to sudo roamswitch events search --since 10m to see which programs started in that period.

An internal domain may use long, random-looking names as a matter of course (generated host names, for example). In that case, list the domain in dns_tunnel_entropy_allowlist. This changes the system. A restart applies it.

sudo roamswitch server config set dns_tunnel_entropy_allowlist example.com
sudo roamswitch server restart
count() { sudo roamswitch notifications --json | python3 -c 'import json,sys; print(len(json.load(sys.stdin)))'; }
before=$(count)
for i in $(seq 1 12); do
  label=$(head -c 30 /dev/urandom | base32 | tr -d '=' | tr 'A-Z' 'a-z' | head -c 48)
  dig +noedns +short +tries=1 +time=1 @10.200.0.2 "$label.exfil-lab.example.com" >/dev/null
done
echo "count before=$before after=$(count)"
Excerpt
  ✓ Server configuration updated and saved. (dns_tunnel_entropy_allowlist = example.com)
  To apply: sudo systemctl restart roamswitch-server or sudo roamswitch server restart

count before=13 after=13

The number of notifications stayed at 13 before and after the queries. After removing the allowlist entry and restarting, the same 12 queries raised the count by one. The allowlist skips the long-name check and the brute-force check. Queries of the TXT or NULL type are still reported for an allowlisted domain (checked in the code).

Put it back.

sudo roamswitch server config set dns_tunnel_entropy_allowlist ""
sudo roamswitch server restart

#What it cannot see

What was checked, and what stays out of view

Everything was checked inside a disposable Ubuntu 24.04 VM, using another address in a network namespace.

  • The port list knows only RoamSwitch's rules. roamswitch ports judges by the listening address and RoamSwitch's inbound rules. It does not see a router or cloud firewall in front of the host. Check from outside whether a port is really reachable
  • Slow scans and loopback scans. A scan that stays under 15 ports in 300 seconds is not counted. Connections arriving on loopback (lo) are not counted at all (checked in the code)
  • The malicious-IP list covers only what is on it. The shipped list is empty: only the addresses the operator wrote, and those of the lists named in egress_ip_feed_urls, are covered. Traffic to a C2 server not on the list is not stopped. The notification does not name the program either
  • What the DNS watch does not see. DoH and DoT, TCP replies, and queries from containers in their own network namespace (checked in the notification text and the code). Exfiltration split into small pieces and sent slower than 10 queries per 60 seconds is not counted either
  • Not covered here. Switching protection by the network you join, ARP spoofing and Wi-Fi checks are features of the desktop client edition. This module covers Server Edition
Check yourself

roamswitch ports shows port 8080 as "Blocked from outside by the firewall". How should you read it?

Show the explanation

ports combines the listening address with RoamSwitch's inbound rules. In the lab, curl from the peer timed out, and it got through after the port was added to allowed_ports and the service restarted. It does not see a router's rules in front of the host, so check real reachability from outside.

Check yourself

You added the peer's IP to the egress block list. When does it take effect?

Show the explanation

In the lab, it reached the rules about 297 seconds after the file was written. When the file is deleted, the next re-read applies that too. If you are in a hurry, sudo roamswitch server restart reloads it.

#What to learn next

Features covered

Related techniques (ATT&CK): T1046 T1071.004 T1590.002

What was verified

Verified on 2026-11-12. Target: Linux 1.17.39. Every lab was run in order on a disposable Ubuntu 24.04 VM with 1.17.39 from the public apt repository, and the outputs were retaken in Japanese and in English (LANG=en_US.UTF-8).

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