fix: isolate boxes with the bridge's port-isolation flag #38

Merged
dan-claude-bot merged 2 commits from fix/firewall-restart into main 2026-07-14 01:42:35 +00:00
dan-claude-bot commented 2026-07-14 01:40:04 +00:00 (Migrated from github.com)

Boxes still reach each other. The nft bridge rule from #37 is live on the host — and does nothing:

$ sudo nft list table bridge claudebox
table bridge claudebox {
    chain forward {
        type filter hook forward priority filter; policy accept;
        meta ibrname "claudenet" meta obrname "claudenet" drop
    }
}
FAIL  BOX A REACHES BOX B (10.87.0.175) — sibling isolation does NOT hold [tcp: refused]

So the rule is right about intent and wrong about mechanism: whatever path those frames take, that hook does not stop them. I could theorise about why. Theorising is what put this hole in the repo in the first place — the original "boxes can't reach each other" claim was a code reading that turned out to be false, and I have now been wrong twice more about how to close it.

Use the mechanism Incus provides

security.port_isolation on the bridged NIC sets the kernel bridge port's isolated flag, so two isolated ports cannot exchange frames at all. It is first-class, documented, and does not depend on any reasoning of mine about netfilter hooks:

devices:
  eth0:
    type: nic
    network: claudenet
    security.port_isolation: "true"

The nft rule stays as a second layer — it costs nothing — but the profile flag is what carries the guarantee.

Also in this PR

  • systemctl restart, not enable --now. The unit is RemainAfterExit, so once active, --now is a no-op — meaning a firewall change would install to /usr/local/sbin and never run on any host that had already been set up. (It happens not to be what broke the rule here, since the rule did land — but it is a real trap and would have bitten the next change.)
  • setup-host.sh asserts the isolation is live instead of assuming, and doctor.sh checks port_isolation — because the absence of this particular guarantee is invisible: everything works, and boxes can simply reach each other.
  • dns.mode=none is now shipped, so the drill stops reverting it as leftover rehearsal dirt.

Verify: bash drill/drill.sh --yesA3 must flip from refused (the packet arrived) to dropped.

🤖 Generated with Claude Code

Boxes still reach each other. The nft bridge rule from #37 **is live on the host** — and does nothing: ``` $ sudo nft list table bridge claudebox table bridge claudebox { chain forward { type filter hook forward priority filter; policy accept; meta ibrname "claudenet" meta obrname "claudenet" drop } } ``` ``` FAIL BOX A REACHES BOX B (10.87.0.175) — sibling isolation does NOT hold [tcp: refused] ``` So the rule is right about *intent* and wrong about *mechanism*: whatever path those frames take, that hook does not stop them. I could theorise about why. **Theorising is what put this hole in the repo in the first place** — the original "boxes can't reach each other" claim was a code reading that turned out to be false, and I have now been wrong twice more about how to close it. ## Use the mechanism Incus provides `security.port_isolation` on the bridged NIC sets the **kernel bridge port's isolated flag**, so two isolated ports cannot exchange frames at all. It is first-class, documented, and does not depend on any reasoning of mine about netfilter hooks: ```yaml devices: eth0: type: nic network: claudenet security.port_isolation: "true" ``` The nft rule stays as a second layer — it costs nothing — but the profile flag is what carries the guarantee. ## Also in this PR - **`systemctl restart`, not `enable --now`.** The unit is `RemainAfterExit`, so once active, `--now` is a no-op — meaning a firewall change would install to `/usr/local/sbin` and never run on any host that had already been set up. (It happens *not* to be what broke the rule here, since the rule did land — but it is a real trap and would have bitten the next change.) - **`setup-host.sh` asserts the isolation is live** instead of assuming, and `doctor.sh` checks `port_isolation` — because the absence of this particular guarantee is **invisible**: everything works, and boxes can simply reach each other. - **`dns.mode=none` is now shipped**, so the drill stops reverting it as leftover rehearsal dirt. Verify: `bash drill/drill.sh --yes` → **A3 must flip from `refused` (the packet arrived) to `dropped`.** 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/box#38
No description provided.