Skip to content

Spread egress-lane VMs over several CNI networks - #58

Open
doge-rgb wants to merge 5 commits into
mainfrom
pr3-bridge-shards
Open

Spread egress-lane VMs over several CNI networks#58
doge-rgb wants to merge 5 commits into
mainfrom
pr3-bridge-shards

Conversation

@doge-rgb

@doge-rgb doge-rgb commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Stacked on #57 — the sharding commit was written on top of that egress work
and does not apply without it. Review the last commit; base will retarget to
main once #57 lands.

A Linux bridge holds 1024 ports: BR_MAX_PORTS is a compile-time constant, and
find_portno() scans linearly for a free one while holding the global rtnl
mutex
. So a single bridge caps the egress lane at 1024 VMs per node, and long
before that cap the scan itself becomes the bottleneck — measured on one node,
egress-lane creation peaked at 105/s and collapsed to single digits within five
seconds as the bridge filled, with load average reaching 287 while the CPU sat
idle. The none lane, which attaches nothing, held a flat ~200/s throughout.

A node may now name several CNI networks instead of one. Each VM is assigned by
hashing its own name, so VMs spread evenly across the configured networks and
the assignment is stable for a given VM without any per-node state. Four bridges
move the ceiling to ~4096 per node and cut the average port-scan length by the
same factor.

This does not remove the rtnl serialization, only divides the work that happens
under it. The lane that avoids the lock entirely is net=none (#57), which is
why that one is the default.

🤖 Generated with Claude Code

doge-rgb added 5 commits July 29, 2026 21:34
The proxy refuses every private and special-purpose target so a guest
cannot turn it into an SSRF. That also refuses the corporate services a
sandbox legitimately needs: internal names resolve to a ULA v6 (and an
RFC1918 v4 the node cannot route anyway), both of which the guard reads
as internal.

Name the prefixes instead of adding a "permit private" switch. The guest
bridges are themselves ULA and RFC1918, so a blanket permit would hand
every sandbox a route to its neighbours and to the host's own gateway
address — the two things the guard exists to prevent. An explicit list
admits the corporate ranges and nothing adjacent to them.

The list is validated at load, so a malformed prefix fails startup rather
than surfacing as a refused dial hours later. Empty is the default and
behaves exactly as before.
A node that lists the whole address space turns the guard off on purpose,
loopback and cloud metadata included. That is a deliberate configuration
for fleets that put a policy-enforcing forward proxy in front; the test
records what it opens so the choice stays visible.
…s out

A none-lane guest has no NIC at all — that is what makes the lane fast
(1000 VMs in 5s at a flat 200/s, against 86s and a collapsing rate for the
same count on the egress lane, where each VM takes a bridge port). Its
only way out is silkd's loopback relay on 3128, which splices to the
host's egress proxy over vsock.

Nothing reached it before because nothing told a client it was there. The
variables go on silkd's own unit rather than /etc/environment or a
profile script: every exec in the guest is a child of silkd, so this is
the one place that covers a bare `curl` as well as a login shell.

On the egress lane the NIC carries traffic directly and the relay simply
goes unused, so one image serves both.
An exec starts from a clean environment on purpose — silkd's own settings
are not the guest's. But that also drops the one thing a guest cannot
discover for itself. On the no-network lane there is no NIC at all; the
only way out is the loopback relay silkd runs, and an unconfigured client
never thinks to look for it. Baking the variables into silkd's unit was
not enough for the same reason: they reached silkd and stopped there.

Forward exactly the six proxy names, and only when set. Everything else
stays uninherited, and a request's own env still layers on top.
A Linux bridge holds at most BR_MAX_PORTS ports — 1024, a compile-time
kernel constant with no sysctl — and answers EXFULL for the next one. One
bridge per node therefore caps egress density near a thousand VMs however
much CPU and memory the node has, and a fleet run reached exactly that:
nineteen of twenty nodes wedged between 1004 and 1014 ports.

Measured on one node afterwards, the cost is not only the ceiling. Filling
a single bridge to 1000 took 86s and the per-second rate collapsed from
105 to single digits as it filled, because br_add_if walks the bridge's
port list while holding the global rtnl lock. The same node filled 1000
none-lane VMs, which take no bridge port at all, in 5s at a flat 200/s.

Accept a list of conflists and hash the VM name to pick one, so N bridges
give N×1024 and each stays in the range where that walk is short. Hashing
rather than a counter keeps the choice free of process state: a VM
resolves to the same conflist whoever asks and whenever, which matters
because its record persists the network it was built on and restore has
to agree.

The scalar `network` key normalizes into the list, so the binary rolls
onto nodes still carrying the old spelling without touching their config,
and a one-entry list is byte-for-byte the old argv.
@CMGS
CMGS force-pushed the pr2-none-lane-egress branch from 7e6e17d to 296acbc Compare July 29, 2026 16:33
Base automatically changed from pr2-none-lane-egress to main July 29, 2026 17:40
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