A Japanese version of this is on Zenn.
I build and sell a macOS network-security menu bar app called RoamSwitch.
In a previous post, I wrote about attacking my own Mac from an Arch Linux box on the same LAN to see how it handled basic reconnaissance and rogue device probes.
Since then, as I kept adding features and refactoring, a nagging question kept resurfacing: Are we introducing regressions? Is our privileged helper still watertight? Did a recent update accidentally punch a hole in our packet filter rules?
Manually poking at firewalls on every release is tedious and risky—messing with Packet Filter (pf) and root LaunchDaemons on your primary dev machine is a great way to accidentally drop your own network connection.
So, to be absolutely thorough, I set up a repeatable, automated pentest suite inside a disposable macOS virtual machine on a Mac (using Tart) to rigorously probe all 5 defense boundaries from the outside.
Here is how the test harness works and what the logs showed when I attacked it.
Test Architecture: Host Mac ⇄ Target Guest VM
Running destructive firewall tests or killing root helpers on your daily driver is stressful.
Instead, I used Tart, a lightweight macOS virtualization tool, to spin up a clean macOS Sonoma guest VM as the Target, with the Host machine acting as the Attacker.
+------------------------------------+ +-----------------------------------------+
| Host Mac (Attacker) | | macOS VM (Target Guest) |
| - Inbound Port Probing (nc/nmap) | -----> | - RoamSwitch 1.4.8 (Defense Engine) |
| - Unauthorized HTTP Probing (curl)| Virtual | - Root Privileged Helper |
| - Rogue ARP Spoofing (scapy) | Bridge | - Packet Filter (pf) Ruleset Anchor |
+------------------------------------+ +-----------------------------------------+
The 5 Defense Boundaries Tested
graph TD
A[Automated Defense Suite] --> B[1. XPC Authorization Boundary (§3)]
A --> C[2. pf Ruleset & Air-Gap Precedence (§4, §5)]
A --> D[3. Port Anomaly & Global Exposure (§6)]
A --> E[4. MCP Read-Only Invariant & Fuzzing (§8)]
A --> F[5. ARP Gateway Consistency (§11)]
1. Privileged Helper XPC Boundary (§3)
The privileged helper (com.tetsuharu.RoamSwitch.Helper) runs as root. If an unapproved local process can send Mach messages to it, that’s an instant root privilege escalation.
- Test: Dynamically compiled an unsigned (ad-hoc) Swift probe binary lacking my Apple Developer Team ID and attempted calling
enableAirGap. -
Result:
Attempting XPC connection from unauthorized client to /Library/PrivilegedHelperTools/... REJECTED: Helper rejected unauthorized client as expected. [PASS] Privileged helper correctly rejected unauthorized XPC callerThe helper’s
ClientValidatorinspectedNSXPCConnection.audit_tokenand immediately severed the connection.
2. Packet Filter (pf) Ruleset Priority & Air-Gap Containment (§4, §5)
Verifies that when emergency Air-Gap containment is engaged, block drop all strictly takes precedence over any open user port rules (Fail-Closed).
- Test: Dumped active
pfctlanchor hierarchies and verified loopback (127.0.0.1) policy responsiveness while external egress was 100% dropped. - Result:
[PASS] pf ruleset anchor structure verified
3. Port Anomaly Guard & Exposure Detection (§6)
When a developer accidentally starts a local server bound to 0.0.0.0 (all interfaces), RoamSwitch should detect it and shield external access without breaking localhost (127.0.0.1).
- Test: Spawned
0.0.0.0:18888and127.0.0.1:18889simultaneously, querying the diagnostic engine. -
Result (MCP Diagnostics Output):
{ "port": 18888, "processName": "Python", "isGloballyExposed": true, "isFirewallShielded": true, "overallRisk": "warning", "findings": [ { "title": "0.0.0.0 Binding (All LAN Exposure)", "description": "Process is listening on 0.0.0.0 (all interfaces). While shielded by RoamSwitch firewall, consider binding to localhost." } ] }
4. MCP Server Read-Only Invariant & Parser Fuzzing (§8)
To prevent AI coding assistants (Claude Desktop, Cursor) from being tricked by prompt injection into modifying security settings (Confused Deputy):
- Test:
- Scanned
tools/listto enforce that 0 mutating/exec tools exist. - Injected a pathological 60-level nested JSON payload to fuzz parser recursion limits.
- Scanned
-
Result:
[PASS] Read-Only Invariant Confirmed: 0 mutating tools found in MCP catalog [PASS] Parser Robustness Confirmed: Malformed/pathological JSON safely rejected without crash
5. ARP Gateway Monitor & Integrity (§11)
Validates that the kernel ARP table tracking accurately binds default gateway IP to MAC address.
-
Result:
? (192.168.64.1) at d2:c0:50:cd:95:64 on en0 ifscope [ethernet] [PASS] Default gateway (192.168.64.1 -> d2:c0:50:cd:95:64) properly resolved and monitored
The Most Realistic Threat: Compromised Smart Home Devices
With smart plugs, IP cameras, and cheap IoT gadgets littered across modern home networks, rogue devices scanning the local subnet for lateral movement is no longer a theoretical edge case—it’s an everyday reality.
Even if you trust your home Wi-Fi, you can’t guarantee every random device on that same subnet is clean.
So I tested the most common real-world situation: “A compromised smart device on the home network tries to probe and attack the Mac.”
[Host Mac (Rogue IoT Device)] ────── Lateral Probe ──────> [Target Mac VM (Home Wi-Fi)]
(192.168.64.1) RoamSwitch Active (192.168.64.2)
- On the Target Mac, I spun up a dev server:
python3 -m http.server 8080 --bind 0.0.0.0
-
RoamSwitch fired a notification immediately:
🚨 Unknown Listening Port Automatically Blocked
Detected process “Python” exposing port 8080 to external LAN. Automatically shielded external access.
-
From the Attacker Node (Host), I tried to hit the server:
% curl -I --connect-timeout 2 http://192.168.64.2:8080
curl: (28) Failed to connect to 192.168.64.2 port 8080 after 2006 ms: Timeout was reached
The Efficacy
-
From the Outside: The
pffirewall dynamically injected an inbound drop rule. The attacking IoT device got a dead timeout—it couldn’t even tell a web service was running. -
For the Developer: The Mac retained full access via
http://localhost:8080(127.0.0.1), allowing normal local development without risk of lateral network intrusion.
Reproduce It Yourself: Open Source Audit Suite
All scripts used in this audit are published under the open-source support repository:
git clone https://github.com/lafine1211/roamswitch-support
cd roamswitch-support/audit
./rs-defense-audit.sh all
-
Live Audit Results Log:
audit/RESULTS-DEFENSE-2026-08-30.md -
Step-by-Step Guide:
audit/README-SECURITY.md
What it felt like seeing it work
Even though I built the app, there was a genuine rush of satisfaction seeing curl time out cleanly from the outside while localhost kept humming along seamlessly on the inside (along with a sigh of relief that our defense rules didn’t regress).
In security software, claiming “it’s secure” in marketing copy is cheap. Handing developers a runnable script that lets them attack the app and inspect the raw logs is the only honest way to build real trust.
If you have a Mac and Tart handy, feel free to clone roamswitch-support and try poking holes in rs-defense-audit.sh yourself.