/ipad-internet off: A Claude Code Skill That Cuts One Device's Internet at the Firewall
- 6 minutes read - 1274 wordsI wanted one command — /ipad-internet off — that instantly cuts the household iPad’s internet, and /ipad-internet on to give it back. Simple ask. The interesting part is doing it correctly: instantly (no waiting on a config save), reversibly, without disturbing the LAN (AirPlay, printers, local media should all keep working), and — most importantly — in a way that can never accidentally knock a different device offline.
It ended up as a Claude Code skill backed by a careful bit of OPNsense / pf design. This post is about that design: why it uses a pf table instead of editing the config, how it survives the iPad’s shifting IP and Apple’s private Wi-Fi MAC, and the ownership guard that makes the “wrong device” failure mode structurally impossible.
The naive version, and why it’s wrong #
The obvious approach is a firewall rule blocking the iPad’s IP, toggled on and off. Two problems sink it immediately:
- The IP moves. The iPad gets a DHCP lease from the pool (
.80–.247on my LAN). Block192.168.1.97today; tomorrow it’s.112and your block points at nothing — or worse, at whatever device now holds.97. - Editing
config.xmlto toggle is slow and risky. Every change means a config write and a filter reload. Do that ten times a day and you’re churning the firewall’s config for something that should be instant and stateless.
So the design goal became: pin the identity, block statelessly, and never write to config.xml on toggle.
The finding-it phase (and a lesson about stale leases) #
First I had to actually identify the iPad. The DHCP lease file said it was at 192.168.1.231. ARP said otherwise — the live address was 192.168.1.97. The lease file was stale. This matters: if I’d trusted the lease file I’d have built a block around the wrong IP. Throughout this project, ARP is ground truth for “what IP does this MAC have right now,” and the lease file is a historical record that may lie.
A small platform gotcha while poking around: OPNsense’s root shell is csh, not sh. Remote one-liners with sh-isms ($(...), && chains, here-strings) misbehave until you wrap them: ssh root@fw "sh -c '...'". Minor, but it’ll waste ten minutes if you don’t spot it.
The design: a pf table the config never populates #
The one-time setup (applied to the firewall by an idempotent, re-runnable Python patcher that backs up config.xml and validates the XML before installing) puts three things in place:
| Piece | Value |
|---|---|
| Static DHCP reservation | d6:45:4e:1a:be:a4 → 192.168.1.60 (outside the DHCP pool) |
| Firewall alias | ipad_block, type External (advanced) |
| Automation filter rule (seq 31) | block in quick on lan from <ipad_block> to !lan net |
The keystone is the “External (advanced)” alias type. That creates a pf table (ipad_block) that OPNsense defines but never fills — its contents are managed entirely from outside the config, by pfctl. So the standing rule (“block anything in this table from leaving the LAN”) lives permanently in the config, but toggling is pure pfctl and never rewrites config.xml. That makes it instant and impossible to corrupt the firewall configuration.
Note the rule’s destination: !lan net — not the LAN. Only traffic leaving the LAN is dropped. AirPlay, printers, local DNS, and local media keep working with the internet off. That’s a deliberate feature, not a side effect.
off / on #
~/.claude/skills/ipad-internet/ipad-internet.sh off # cut internet
~/.claude/skills/ipad-internet/ipad-internet.sh on # restore
~/.claude/skills/ipad-internet/ipad-internet.sh status # default; always safe
offadds the iPad’s addresses to the pf table and runspfctl -kto kill its existing state entries, so open connections drop immediately rather than lingering until they time out.onflushes the table.
Both are idempotent — off twice is harmless, so is on when nothing is blocked. And because the reservation pins the iPad to .60 but a stale pre-reservation lease could still be floating in ARP, off blocks both .60 and any address currently in ARP for that MAC. A stale lease can’t be used to slip past the block.
The ownership guard: why this can’t hit the wrong device #
Here’s the part I’m proudest of. Before adding any address to the block table, the script checks ARP and refuses any IP that doesn’t resolve to the iPad’s MAC (the reservation is allowed even when idle). If an address doesn’t belong to the iPad, it prints refusing <ip>: does not belong to <mac> and skips it.
That single check makes it structurally impossible to knock an unrelated machine off the internet through this skill — even if the lease file is stale, even if an IP got recycled, even if I fat-finger something. The guard lives in the script; the one way to bypass it is to call pfctl -t ipad_block -T add by hand, which the skill’s instructions explicitly tell Claude never to do. Safety by construction beats safety by carefulness.
I proved the mechanism end-to-end before trusting it — including a controlled test that added this Mac’s own IP to the table and confirmed its curl to the internet failed while LAN stayed up, then removed it. Test the block on a machine you’re sitting at, not on the target you can’t see.
Wrapping it as a Claude Code skill #
The skill is a SKILL.md plus the shell script. The SKILL.md frontmatter description is what lets natural language route to it — “block the iPad,” “cut the iPad off,” “give the iPad internet back,” or a literal /ipad-internet off all map to the right argument. The instructions tell the model to run the script and report its output verbatim, never hand-roll ssh/pfctl, and to surface the script’s error: … non-zero exits instead of claiming a success that didn’t happen. The script does the privileged, guarded work; the model just picks off/on/status from what I said and relays the result.
The honest caveats #
Every real system has edges, and writing them down is the difference between a tool and a footgun:
- Apple’s Private Wi-Fi Address. That MAC (
d6:45:…) is Apple’s per-SSID randomized address. It’s stable — until Private Wi-Fi Address is toggled for the SSID, or the network is forgotten and rejoined, at which point the MAC changes and the block would target a stale identity. Theoffpath warns when it sees other iPad-named MACs in the DHCP leases; adetectsubcommand re-identifies the device. - Reboot is fail-open. pf table contents don’t survive an OPNsense reboot, so a reboot while blocked restores the iPad’s internet. Re-run
off. (I consider fail-open the correct default here — a firewall reboot shouldn’t silently leave a device bricked.) - IPv4 only. The LAN and WAN are IPv4-only today, so the rule is
inetonly. Enabling IPv6 later would need a matchinginet6rule, or the block leaks over v6.
Takeaways #
- Pin identity with a DHCP reservation; never block a raw pool IP. DHCP addresses move, and blocking a recycled IP hits the wrong device.
- Use an “External” pf table for stateful toggles. The rule lives in the config; the contents live in
pfctl. Toggling never rewritesconfig.xml, so it’s instant and can’t corrupt the firewall. - Block
to !lan net, not everything — kill internet while leaving the LAN fully usable. pfctl -kon block so live connections drop now, not on timeout.- Put the safety in the code, not the operator. An ARP ownership check makes “took the wrong device offline” impossible by construction — far better than remembering to be careful.
- ARP is truth; the lease file can lie. Always confirm the live IP before acting.
/ipad-internet off. Dinner is quieter, the printer still prints, and no other device on the network ever notices.