Harden guest TAP bridge ports against cross-guest frame leakage - #476
Harden guest TAP bridge ports against cross-guest frame leakage#476ulziibay-kernel wants to merge 2 commits into
Conversation
Port isolation only blocks guest-to-guest forwarding. Unknown-unicast frames arriving from the uplink for a MAC the bridge has forgotten (aged out, or a guest that was just torn down) are still flooded to every guest port, so a guest with a raw socket can passively capture traffic addressed to other guests on the same bridge. On isolated networks, for each guest TAP: - disable unicast flooding so unknown-destination frames are never delivered to the port - pin the guest MAC to its port with a permanent FDB entry so inbound delivery never depends on flooding - disable MAC learning on the port so a guest cannot relocate FDB entries by spoofing a source MAC Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 7e52be6. Configure here.
| State: netlink.NUD_PERMANENT, | ||
| Flags: netlink.NTF_MASTER, | ||
| HardwareAddr: hwAddr, | ||
| }, nil |
There was a problem hiding this comment.
FDB entry marks guest MAC local
High Severity
guestFDBEntry sets NUD_PERMANENT, which the kernel maps to BR_FDB_LOCAL. Frames for the guest MAC are passed up to the host instead of forwarded to the TAP. With unicast flooding disabled, isolated guests lose inbound unicast entirely.
Reviewed by Cursor Bugbot for commit 7e52be6. Configure here.
Move the flood/learning/FDB steps into hardenIsolatedPort and cover them with root-gated tests that build an actual bridge and TAP, so the netlink requests are exercised rather than only the entry builder. Build the FDB entry before touching the port so an unparseable MAC leaves the port at its defaults instead of flooding off with nothing pinned, and use NeighSet rather than NeighAppend: NLM_F_APPEND carries multi-destination semantics, while replace is the idempotent primitive for pinning a single unicast MAC. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sjmiller609
left a comment
There was a problem hiding this comment.
Requesting more information and validation of this change:
- before/after benchmarks: test latency in a development hypeman server deployed from this code version (e.g. working on the deft server). try this skill
/benchmark-hypeman-fork, which is the most-critical path for our platform, but also please test normal creation and restore. compare latency and failure rates, including concurrent operations. Post in this PR in a comment or similar to know latency doesn't suffer majorly from this. - security proof or documented what you tried before / after: either a test or comment showing the practical issue demonstrated in the before case and resolved in the after case.
- staging QA: deploy the change to staging hypeman via our CI and validate it there, ensuring normal platform behavior. I think it would be conspicuously broken in the case of not working. Rollback is safe, i.e. we can undo this change in staging and so long as the associated VMs are deleted, then the change is cleanly reverted. Document in this PR what was validated.


Summary
Guest VMs on a shared bridge can passively capture unicast frames addressed to other guests. Port isolation is already enabled on isolated networks, but it only blocks guest-to-guest forwarding: when the bridge has no FDB entry for a destination MAC (aged out, or a guest that was just deleted while upstream packets were still in flight), it floods the frame to every port, including other guests' TAPs. A guest with a raw socket sees those frames.
This applies the standard multi-tenant bridge-port hardening to each guest TAP on isolated networks, right after isolation is set:
bridge fdb replace <mac> dev <tap> master permanent)LinkSetFlood(tap, false)LinkSetLearning(tap, false)The three steps live in
hardenIsolatedPort. The FDB entry is built (and the MAC parsed) before the port is touched, so an unparseable MAC leaves the port at its defaults rather than flooding-off with nothing pinned to it.createTAPDevicenow takes the allocated guest MAC; both call sites (CreateAllocation,RecreateAllocation) already had it. Only isolated networks are affected — non-isolated networks keep their current behaviour.Notes for review
NeighSet(NLM_F_REPLACE) rather thanNeighAppend:NLM_F_APPENDcarries multi-destination semantics, and replace is the idempotent primitive for pinning a single unicast MAC.NeighAdd(NLM_F_EXCL) is wrong here — it returnsEEXISTif the MAC is still pinned to a stale port.bcast_flood/mcast_flood), so DHCP and ND still work. Guests can therefore still observe broadcast ARP from the uplink; suppressing that (neigh_suppress) is a separate change.Testing
go vet ./lib/network/on linux, andGOOS=darwin go vet ./lib/network/for the stub — both clean.go test ./lib/network/passes.TestHardenIsolatedPortOnRealBridgeandTestHardenIsolatedPortLeavesPortAloneOnBadMACcreate an actual bridge and enslaved TAP and assert the resulting port state, so the netlink requests themselves are covered rather than just the entry builder. They skip when not root, following the existing convention for privileged tests in this repo. Run as root on kernel 6.12:flood off/learning offread back viaLinkGetProtinfoNeighList(tap, AF_BRIDGE)asNUD_PERMANENTflood on/learning on— the port is untouchedip -br linkafterwardsTestGuestFDBEntry/TestGuestFDBEntryRejectsBadMACstill cover the pure entry builder.go build ./...reports three//go:embederrors for downloaded binaries; identical onmain(pre-existing, unrelated).