Skip to content

Encrypt VLAN/VXLAN tunnel traffic with per-tunnel AES-256 keys#117

Draft
antoncxx wants to merge 6 commits into
NullNet-ai:mainfrom
antoncxx:feature/vlan-vxlan-encryption
Draft

Encrypt VLAN/VXLAN tunnel traffic with per-tunnel AES-256 keys#117
antoncxx wants to merge 6 commits into
NullNet-ai:mainfrom
antoncxx:feature/vlan-vxlan-encryption

Conversation

@antoncxx

@antoncxx antoncxx commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Every VLAN/VXLAN link the server builds now gets its own random AES-256 key, sent to both endpoints alongside the existing VlanSetup/VxlanSetup control messages. VLAN traffic is encrypted/decrypted in nullnet-client's userspace forwarder (AES-256-GCM); VXLAN traffic is protected by a per-tunnel kernel IPsec (XFRM/ESP) SA, since that path is handled entirely by the kernel's native vxlan interface. Each VXLAN tunnel also gets its own UDP dstport (replacing the shared 4789 default) so XFRM policies can tell concurrent tunnels between the same host pair apart.

antoncxx added 6 commits July 9, 2026 19:46
Every VLAN/VXLAN link the server builds now gets its own random AES-256
key, sent to both endpoints alongside the existing VlanSetup/VxlanSetup
control messages. VLAN traffic is encrypted/decrypted in nullnet-client's
userspace forwarder (AES-256-GCM); VXLAN traffic is protected by a
per-tunnel kernel IPsec (XFRM/ESP) SA, since that path is handled entirely
by the kernel's native vxlan interface. Each VXLAN tunnel also gets its own
UDP dstport (replacing the shared 4789 default) so XFRM policies can tell
concurrent tunnels between the same host pair apart.
Previously, tunnels whose two endpoints happened to be colocated on the
same physical host got no encryption at all: VXLAN's same-host branch used
a plain veth pair with no IPsec, and VLAN traffic between two same-host
access ports was switched directly by OVS without ever transiting the
encrypting userspace forwarder. Neither is reachable by a network
sniffer, but both are readable by anything else with sufficient privilege
on that same host (a differently-privileged container, a host-level
process) — which matters for a project whose whole premise is not trusting
other things on the network by default.

VXLAN: wrap the same-host veth pair in MACsec (802.1AE, GCM-AES-256),
keyed with the same per-tunnel key already delivered for the cross-host
IPsec path. Cascades away on teardown when the underlying veth is deleted;
also deleted explicitly for clarity.

VLAN: replace the single default-normal OVS flow with two more specific
ones — traffic arriving from the trunk (already decrypted) is delivered
by normal switching; traffic arriving from any access port always exits
via the trunk, never directly to another access port. This forces every
packet through the TAP and therefore through the existing encrypt/decrypt
path in forward/send.rs and forward/receive.rs, with no changes needed
there.
iproute2's macsec option parser is positional: `port` (part of this
device's own SCI) must appear before `cipher`, not after. The previous
order failed with "macsec: unknown command \"port\"?" every time,
silently, because the command's stderr was suppressed to tolerate an
unrelated race (the veth-pair creation a few lines earlier). None of the
four macsec setup commands actually race against the sibling script
invocation — each side only touches its own uniquely-named interface — so
their stderr is no longer suppressed either, making a real failure here
loud instead of silent next time.

Found by live-testing on a real same-host deployment: `ip macsec show`
was empty despite the veth pair (and its MTU bump) being created
correctly, which pointed straight at the interface-creation line right
after it.
Diagnostic aid for tracking down intermittent "connection reset" on
proxied SSH-over-VLAN: every silent-drop point (missing key, decrypt/auth
failure, malformed datagram) and every firewall verdict on both send and
receive now logs to stderr with [DEBUG] prefix. In particular, receive.rs
now logs before crafting a REJECT reply — that's the one code path that
sends a real TCP RST back to the peer, so if a REJECT verdict is firing
intermittently on legitimate traffic, this will show it directly instead
of us guessing from symptoms alone.

Intended to be reverted once the root cause is found — not a permanent
logging addition.
The same-host defense-in-depth fix replaced OVS's single default flow
(priority=0,actions=normal) with a generic "everything from an access
port goes to the trunk" rule using a raw `output:<TAP>` action. Unlike
`actions=normal`, `output` does not re-add the 802.1Q tag that access
ports only carry internally (as an implicit port association, not part
of the packet's own bytes) — so every redirected frame arrived at
nullnet-client's TAP already stripped of its VLAN tag, and got dropped
by forward/send.rs as malformed ("Packet missing VLAN tag"), silently,
for every single packet on every VLAN tunnel that hit this path.

Replaced the one generic redirect rule with one precise rule per access
port (installed alongside the port itself in configure_access_port, torn
down alongside it in remove_vlan), each matching only its own in_port and
explicitly pushing the correct 802.1Q tag (push_vlan + mod_vlan_vid)
before sending to the trunk. The original priority=0 default-normal rule
is restored as a safety fallback for the narrow window before a fresh
access port's own rule lands.

Found via live testing: intermittent SSH-over-proxy connection resets
that traced back to a flood of "Packet missing VLAN tag" errors in
nullnet-client's own logs.
Same diagnostic purpose as the earlier debug commit — need to see what
this frame actually was (IPv6 NDP/multicast noise vs. something that
should have been carried) rather than just knowing the match fell
through to the catch-all arm.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant