7814 Commits

Author SHA1 Message Date
YueGuobin
4fa7f158fd
fix(schemas): one definition of the capabilities payload
GET /computes/{id} answered with only part of the capabilities the compute
reports: the response schema kept a second, private Capabilities class of
its own (it knew ubridge_tc and nothing else), and FastAPI's response model
silently drops fields it does not define — so ubridge_tap and
ubridge_iol_tap never reached clients even though the compute had sent
them and links were already being built on those datapaths.

Import the one definition instead of duplicating it, so the next capability
cannot drift the same way.
2026-10-06 00:21:09 +08:00
YueGuobin
3b8172cbc8
docs(iou): record the IOU kernel datapath and its spec
* docs/features/iou-kernel-datapath.md: architecture (one irreducible
  userspace hop on the fabric leg, kernel link segment), datapath
  selection, the anchor lifecycle with the reverse-of-QEMU stop order,
  capture/markers/filters, relay fallback, and the verified numbers (an
  isolated server with two IOU nodes behind a fake ELF image, the test
  playing the IOL fabric: 64/64 kernel, 62/62 relay).
* docs/design/ubridge-iol-tap-anchor-spec.md: delivered — implemented on
  the ubridge fork's feature/iol-tap-anchor branch (both commands, the
  listener hardening, plus releasing every port NIO on iol_bridge delete
  even when the bridge was not running), with the implementation notes
  that came back (208 vs the older 214 for a missing bridge; one perror
  line per frame dropped on a down anchor).
* The Docker/QEMU feature docs point at it from their roadmaps.
2026-10-06 00:21:09 +08:00
YueGuobin
f23c3bb6e6
fix(iou): carry suspend from the NIO update body
The IOU NIO update route never copied the body's suspend flag onto the
NIO; the Docker and QEMU routes always did. Harmless on the relay, where
suspend is emulated by the synthetic frequency_drop filter the controller
injects, but fatal on a kernel link, whose suspend *is* the anchor's
admin state: a suspended link kept forwarding traffic, and the resume
looked like it worked because nothing had ever stopped.
2026-10-06 00:21:09 +08:00
YueGuobin
806afec0d8
feat(controller): offer IOU the kernel datapath
Eligibility gains an IOU branch gated on the compute's ubridge_iol_tap
(strict True: an unreported capability keeps the link on the relay), and
a port-level rule: a kernel link is an Ethernet segment, so any endpoint
whose port is not Ethernet (IOU serial) disqualifies the link before it
is dispatched. Without that, a serial link between two capable IOU nodes
would be sent as nio_bridge and fail at the compute.

The IOU NIO routes accept nio_bridge alongside the UDP/TAP/Ethernet NIOs
— without the widening the controller's POST is 422'd before the node
ever sees it, the same trap the QEMU routes hit.
2026-10-06 00:21:09 +08:00
YueGuobin
200c946eef
feat(ubridge): probe iol_bridge's TAP-port support
Whether an IOU port can terminate on a persistent TAP (iol_bridge
add_nio_tap) is a capability of its own: the tap module and that command
land independently, so a build may have one without the other. Probed on
a scratch IOL bridge with a scratch TAP — one add/delete cycle, no
iol_bridge start needed (attaching a NIO works on a stopped bridge) —
cached by binary identity, with the scratch bridge id above the per-node
id space the fabric locks (513..1024) so a probe cannot collide with a
running node. Any failure, including an old build's "Unknown command",
means unknown: IOU stays on the relay datapath.

Reported through /capabilities as ubridge_iol_tap, next to ubridge_tap.
2026-10-06 00:21:09 +08:00
YueGuobin
9a14a91293
feat(iou): anchor every Ethernet bay/unit on a persistent TAP
IOU's only wire is the IOL fabric (Unix datagrams, 8-byte headers,
NETMAP), so it has no host netdev to enslave — the kernel datapath gives
each Ethernet bay/unit a persistent TAP anchor the uBridge IOL bridge
binds the port to (iol_bridge add_nio_tap), which links then enslave into
their per-link kernel bridge exactly like Docker's veth and QEMU's TAP.

* IOUVM joins KernelDatapathMixin: one TAP per Ethernet bay/unit
  (gi{id8}e{bay}p{unit}), created at start after a capability probe and a
  stale sweep, born down and address-free; serial bays never anchor.
* _networking dispatches per NIO type: a relay link keeps iol_bridge
  add_nio_udp, a kernel link binds the anchor then runs _kernel_attach.
* Stop order is QEMU's reverse, because uBridge holds the anchor fds:
  iol_bridge delete (releases every port NIO) -> tap delete (a held TAP
  answers EBADFD) -> brctl delete -> hypervisor stop. Removing a single
  link releases its fd the same way (delete_nio_tap).
* The IOU-specific marker reconcile override is gone: marker primitives
  are polymorphic on the anchor (marker *_kernel on a TAP, iol_bridge
  commands on an IOL location), so the base reconcile serves both.

Capture on a kernel link anchors on the TAP (capture start_kernel);
filters are tc on the anchor, never combined with the userspace
iol_bridge filters a relay link uses.
2026-10-06 00:21:09 +08:00
YueGuobin
81ad9b837f
docs(spec): record the delivered L2-only anchors and their residual
uBridge delivered `link l2only` (e2e3155) and the brctl delete refusal
(83a84fb); verified from the server side -- anchors and per-link bridges
report addrgenmode none, carry no addresses, and an idle anchor is silent
once the one-shot membership burst has settled.

That burst is the one thing the command cannot remove, so it is now recorded
where the acceptance criteria live, with the measurement: enslaving a port
makes the kernel announce the bridge's multicast memberships (90 B MLDv2 at
+0.00 s, 54 B IGMPv3 at +0.09 s, 90 B MLDv2 at +0.73 s, then silence; no
addresses involved, and IPv4 has no per-device IGMP switch). Decision:
accepted and documented, not suppressed -- §E.2 measures idle silence after a
settle window instead.

Pins the other delivered behaviour with a test: a bridge now survives a
single-sided node stop, so brctl create answers EEXIST on the common path
rather than only in a create race -- the attach must not short-circuit there,
because the restarted side has a brand-new anchor whose filters and markers
still have to be re-applied.
2026-10-06 00:21:08 +08:00
YueGuobin
a1fcd29bce
fix(qemu): build a command line for a headless node
_console_options() fell through the "none" branch and returned None, so
_build_command() blew up on `command.extend(None)`: a QEMU node with
console_type "none" -- offered by the GUI, and what API/MCP-driven labs use
to skip the console entirely -- failed to start with a 500 and a traceback.

Return an empty list, exactly like _aux_options() already does, and cover it
with a test that builds the command line for a headless node. Verified on a
live instance: such a node now starts (204) with `console: null` and no
-serial device in the QEMU command line.
2026-10-06 00:21:08 +08:00
YueGuobin
187201a08c
feat(controller): offer QEMU the kernel datapath, gated per compute
Kernel eligibility becomes "both endpoints on one compute, each able to
anchor": Docker always (adapters are born as veth pairs), QEMU when that
compute's uBridge can create the persistent TAPs its adapters anchor on. The
question is asked per compute, because the link needs both ends anchored --
so a mixed or half-upgraded deployment degrades to the relay instead of
failing an attach.

probe_tap_support() creates and deletes one throwaway persistent TAP through a
scratch uBridge, cached by binary identity like the tc report; /capabilities
exposes `ubridge_tap` (absent = unknown, which keeps links on the relay).

The compute route for a QEMU adapter NIO took UDPNIO only, so a kernel link
was rejected with 422: its body and response now accept
Union[UDPNIO, BridgeNIO] exactly like Docker's.

Stale "Docker-to-Docker" wording in the config, link and MCP descriptions now
talks about anchors, and docs/features/qemu-kernel-datapath.md documents the
datapath -- verified end-to-end on a real instance: 32/32 kernel-datapath
checks and 29/29 on the relay variant.
2026-10-06 00:21:08 +08:00
YueGuobin
7d2d8c6747
feat(qemu): anchor QEMU adapters on a persistent TAP
QEMU had no host-side interface at all: every adapter was a local UDP tunnel
into uBridge (-netdev socket plus two UDP ports per adapter), so a link could
only ever be a userspace relay. Each adapter now owns a persistent TAP
(gq{node_id[:8]}e{adapter}p{port}), created by uBridge before QEMU is launched
and handed to this user, and the netdev is that TAP -- the anchor role Docker's
veth host end plays. Links attach to it:

* kernel link (NIOBridge): brctl create/up/addif enslaves the TAP into the
  per-link bridge and capture, markers and tc filters are installed on it,
  verbatim through the shared KernelDatapathMixin -- a TAP and a veth host end
  are interchangeable there (an egress qdisc on either impairs traffic entering
  the node, and PACKET_OUTGOING on either is the node receiving);
* relay link: bridge add_nio_ethernet plus add_nio_udp -- AF_PACKET, never the
  TAP NIO, which would take the fd QEMU itself holds.

The netdev type is fixed at launch, so this is all-or-nothing per VM: on the
TAP datapath every link type attaches to (or detaches from) a running VM,
including switching a link between the two datapaths. Stop un-persists the
TAPs (QEMU's open fd keeps each device alive until it exits) and removes the
per-link bridges; start sweeps leftovers first. A uBridge without the tap
module keeps the previous socket-netdev datapath, so an unupgraded install is
unaffected -- such a node simply cannot carry kernel links.
2026-10-06 00:21:08 +08:00
YueGuobin
12d8abce45
refactor(compute): share the kernel-datapath machinery across node types
Move the datapath-agnostic half of DockerKernelDatapathMixin into a new
KernelDatapathMixin (compute/kernel_datapath.py) so a second node type can
reuse it verbatim: the three marker primitives, the tc netem / cls_bpf / eBPF
appliers, the uBridge tc-capability probe, kernel marker teardown, link
detach, the per-link bridge cleanup and the adapter carrier -- plus
_kernel_attach(), lifted from DockerVM._connect_nio's kernel branch.

The mixin talks about "the anchor" (the host-side interface an adapter owns in
the root namespace) instead of "the veth host end": a node type provides
_kernel_host_ifc(), _kernel_anchors() and, when needed, _relay_carrier_bridge()
and _kernel_error(). Docker keeps only the veth lifecycle and those hooks;
behaviour is unchanged.

The C901 baseline entry moves with _netem_command_parts (the docker module is
no longer over the limit) and the bpf-compile-failure test patches the logger
of the module that now owns the code -- net zero new ignores.
2026-10-06 00:21:08 +08:00
YueGuobin
0b507773b9
docs(spec): require L2-only host anchors in uBridge
Every host-side device uBridge creates for the data plane (persistent TAP
anchors, veth host ends, per-link kernel bridges) must be pure L2: no IPv4
address, no IPv6 address and no IPv6 stack activity on that device.

Measured today: an idle TAP emits ~6 frames / 2 s of the host's own MLD/DAD
from its own link-local, the production Docker veth host end carries
fe80::5c30:e8ff:fe00:5d8d/64 and the per-link bridge fe80::f0d9:94ff:feac:e42b/64.
On a kernel link the bridge floods those frames into the emulated segment, on a
relay link uBridge relays them to the peer, and the host answers neighbor
solicitations for its own address (a phantom IPv6 neighbour for the emulated
nodes). IPv4 needs no equivalent knob: nothing self-provisions it, and the
anchors hold no IPv4 address.

The spec adds one command (link l2only <iface> [on|off], netlink addr-gen-mode
none -- never a /proc sysctl write, which fails the DAC check for a non-root
setcap'd uBridge), applied by the four creators themselves, with an
idle-silence acceptance test and a known-noise note in the Docker kernel
datapath document. No change to existing command grammar.
2026-10-06 00:21:08 +08:00
YueGuobin
c1d0e6ee21
docs(spec): define the gemodel acceptance target
The F section asked for "gemodel loss within +/-5% of target" without
saying what the target is, and the A.1 table gave ranges only. Spell out
the semantics the kernel implements (and that the veth calibration
confirmed): p is the good-to-bad transition, r the bad-to-good
transition, 1-h the loss chance inside the bad state (the good state
loses nothing), mean loss p/(p+r)*(1-h) per direction. The functional
bullet now names that target and how to measure it (per-direction
counters, round-trip reads 1-(1-L)^2, pre-warm ARP when 1-h=100).

No wire-format or grammar change.
2026-10-06 00:21:08 +08:00
YueGuobin
1db9f171ca
fix(controller): re-arm window_drop and expose kernel_datapath
Three fixes from live-calibrating the kernel impairment filters on an
isolated instance (Docker-to-Docker veth link, per-sequence evidence):

- update_filters skipped an unchanged filters dict as a no-op, so a
  one-shot window_drop outage that had already passed could never be
  re-armed: re-applying the same values produced no datapath command at
  all. A PUT still carrying window_drop now reconciles, restarting the
  schedule from the apply as documented. Measured: identical re-apply
  moves the outage window from seq 50-70 to 70-91 (arm at +8s of the
  new apply).

- gemodel parameter semantics were documented backwards in the
  validation rules, available_filters, the MCP/copilot descriptions and
  the feature docs. Measured against the kernel (per-direction counters,
  offered = sent + dropped): p is the good-to-bad transition chance, r
  the bad-to-good transition, 1-h the loss chance in the bad state (the
  good state loses nothing) - mean one-way loss p/(p+r)*(1-h), e.g.
  [10,30,100] -> 24.8%/25.7% measured vs 25% predicted, [5,45,100] ->
  10.8% both ways vs 10%.

- kernel_datapath was silently stripped from every REST/MCP link payload
  by the response model (LinkBase carried no such field), so clients
  could not tell a kernel link from a relay one.

Also verified on the same setup: the 3-argument window_drop form works
exactly as specified ([3000,5000,100] -> a single seq 31-55 outage,
[20000,6000,100] -> seq 115-144), and both generations of the command
builder emit the parameters unpadded.

Adds an API test for the field and controller tests for the no-op skip
and the window_drop re-arm exemption.
2026-10-06 00:21:08 +08:00
YueGuobin
5ac1521217
feat(mcp): accept project/node names as tool locators
LLM agents transcribe UUIDs unreliably — a 2-character slip invents an
id that never existed and is invisible to the eye. Tool parameters now
accept names wherever the server guarantees uniqueness: project names
among live projects, node names within a project. Links, drawings,
appliances and snapshots have no names and stay UUID-only.

Resolution sits in the single dispatch choke point (_run_handler_sync):
UUID-shaped values pass through untouched, names resolve with one list
call, and an unknown name fails fast listing the known names instead of
a confusing downstream 404. As a backstop for residual UUID typos,
error replies carrying "<UUID> doesn't exist" are matched against the
in-scope ids; a single candidate within edit distance 2 earns a
"Did you mean ...?" hint.
2026-10-06 00:21:08 +08:00
YueGuobin
935c00802f
docs(mcp): refresh filter vocabulary in the MCP/copilot tool descriptions
link_update, link_available_filters and the copilot packet-filter tool
still advertised the pre-kernel-datapath five types. List the full
thirteen with positional parameter formats, ranges and kernel-only
marking, point at link_available_filters as the per-link authority
(topology + compute capability gated), and document window_drop's
apply-relative schedule restart. Also add kernel_datapath to the link
field lists.

Description-only: handlers pass filters through unchanged.
2026-10-06 00:21:08 +08:00
YueGuobin
cecaf298bb
feat(controller): expose kernel_datapath on the link payload
Runtime fact recomputed from the NIO wiring on every load (not
persisted). Without it clients cannot tell a relay link from a kernel
link whose compute lacks a filter capability — both show up as
kernel-only types missing from available_filters.
2026-10-06 00:21:08 +08:00
YueGuobin
b4e4739d52
feat(compute): report uBridge tc capabilities and gate filters on them
Consume uBridge's ebpf_modes contract (feature/tc-window 40c36ae): a
stateful mode is usable iff ebpf=1 and its token is listed; builds
predating the field keep their shipped modes (nth/quota/flow), so the
pre-correction window semantics can never be installed silently.

- gns3server/utils/tc_capabilities.py: shared reply parser, mode
  policy and filter-type mappings (compute and controller gate on the
  same rules)
- gns3server/compute/ubridge/tc_probe.py: standalone probe spawning a
  throwaway uBridge as the server's own user, cached by binary
  identity (path/mtime/size - the version string cannot distinguish
  rebuilds); failures cache as unknown
- GET /v3/compute/capabilities gains ubridge_tc {netem, ebpf,
  ebpf_modes, cbpf}; the controller forwards it (Compute.capabilities)
  and available_filters hides kernel-datapath filter types an endpoint
  compute cannot run (both ends apply their own filters, so one
  incapable compute hides the type); computes without a report keep
  the full list and apply-time 409 stays the guard
- docker: _ubridge_apply_ebpf_drops gates per mode token; the cleanup
  path only sends "off" for modes the build declares
- split DockerVM's kernel-datapath machinery (veth lifecycle, carrier,
  marker primitives, tc filter translation, kernel NIO teardown) into
  compute/docker/docker_kernel_datapath.py (DockerKernelDatapathMixin)
2026-10-06 00:21:08 +08:00
YueGuobin
48dbdbe4a9
fix(docker): delete the per-link kernel bridge when a node stops
The bridge teardown only hung off the link-removal path, but deleting a
node or closing a project never issues a per-link delete — the veths
disappeared with their containers and left the bridge behind as an empty
orphan (reproduced: create a kernel link, DELETE the project, the
gns3{link_id} bridge survives). `_remove_kernel_veths` now deletes the
node's kernel-NIO bridges alongside the veth host ends: both endpoints do
it, the peer's still-enslaved port answers EBUSY (suppressed, last one
wins) exactly like link deletion, a relay NIO's uBridge bridge is left
alone (it dies with the process), and a node restart rebuilds the bridge
from the NIO in _connect_nio.

Found while cleaning up the window_drop e2e: seven such bridges had
accumulated on this host. Re-verified live: the bridge is gone after the
project delete, and the window_drop e2e (which includes a node restart
with the kernel link attached) still passes 15/15.
2026-10-06 00:21:08 +08:00
YueGuobin
316e7002c1
feat(docker): expose the eBPF time-window filter on kernel links
uBridge's window mode gained the semantics the B.2 spec asked for —
`tc window_drop <if> <start> <outage> <pct> [<period> [<jitter>]]`: a
single outage that traffic passes before *and* after, recurring flaps
with period >= outage, and a per-cycle jitter — so the kernel-only
`window_drop` filter type is now exposed:

- validation mirrors the tc grammar (3-5 params, ranges, period >=
  outage, jitter cap) and adds window_drop to KERNEL_ONLY_FILTERS;
  filter_inactive_filters keeps start=0 — unlike every other filter a
  zero first parameter means "the outage starts now", not "disabled".
- `_ubridge_apply_ebpf_drops` sends `tc window_drop` (positional
  pass-through) and turns the mode off explicitly when absent, under the
  same ebpf=1 gate.
- FILTERS entry (Start/Outage/Chance/Period/Jitter) + available_filters.

The schedule is relative to the apply: reconcile re-sets the classifier
on every apply, so any filter change, node restart or link reset restarts
the window.

Verified on a two-container kernel link, 15/15: a single outage
[1000, 1500, 100] arrives per-seq as [0..4, 13..19] (passing before the
window, dropped inside, passing after); recurring 1000/2000 ms = 52 %
round trip (duty cycle); period == outage = permanent outage; period <
outage = 409 before uBridge; jittered flap keeps the ~50 % duty cycle;
30 % chance on a permanent window = 53 % round trip against the
per-direction theory 1-0.7^2 = 51 %; coexist with netem; node restart
restores; available_filters kernel vs relay (409 + hidden); teardown
clean. P6b re-run as a regression on the same uBridge build: 13/13.

Worth recording for the next rebuild: the first delivered window program
loaded as root but reported ebpf=0 through gns3-server — the verifier's
non-root path loses the loop counter's bound, treats the catch-up loop as
unbounded and rejects the *whole* program (E2BIG, 8193 pending states),
which also took frequency_drop and quota down, with ebpf=0 as the only
symptom. Smaller trip caps did not help; the loop had to go (uBridge
572ca0c), on top of a Makefile header-dependency fix (53e34bb) that had
been shipping a stale program object.
2026-10-06 00:21:08 +08:00
YueGuobin
0c4ec8c38c
feat(docker): eBPF stateful packet filters on kernel links
Consume the eBPF impairment classifier delivered in uBridge
feature/tc-precision: frequency_drop becomes the exact every-Nth mode
(tc nth_drop; -1 = drop everything maps to every 1st) and the new
kernel-only quota type is the byte-cap mode (tc quota_drop, 100 = hard
cutoff). KERNEL_UNSUPPORTED_FILTERS is gone entirely — kernel/relay
eligibility is now purely topological. Both modes gate on ebpf=1 from
the per-process tc capabilities probe (error names the setcap fix); the
cleanup path (modes off) only talks to the eBPF commands when the probe
reported them. Reconcile is a plain re-set: the netem reset-before-set
already detaches the program and clears uBridge's per-interface
registry, so a mode set after it is a fresh load+attach.

Documented semantic difference: the relay counts both directions
through one bridge counter, the kernel counts per direction — N=3
round-trip loss is 1-(2/3)^2 = 55.6% on kernel vs ~1/N on relay
(measured 57%). window_drop is NOT exposed: the delivered
implementation advances the window with no gap, making 'outside the
window packets pass' unreachable (needs outage/period fields —
recorded in the spec); flow_drop awaits a parameter-shape decision.

Also fixes a pre-existing restart bug the e2e exposed: _bridges (relay
bridge names) was never cleared on stop, so restarting a node with a
relay link skipped 'bridge create' and failed on add_nio_udp.

e2e (isolated instance): 13/13 — pref-1 tc_impair classifier on both
veths, 57% round-trip for N=3, -1 = exact 100%, quota hard cutoff,
delay coexistence, clean teardown, suspend/resume, node restart,
relay-leg frequency_drop regression through an Ethernet switch. Full
suite: 1925 passed.
2026-10-06 00:21:08 +08:00
YueGuobin
d4b57d49fb
feat(docker): expose netem-extension packet filters on kernel links
Surface the netem extensions delivered in uBridge feature/tc-netem-ext
as GNS3 filter types: rate (tc-style bandwidth string), reorder (needs
delay), gemodel (Gilbert-Elliot bursty loss, exclusive with
packet_loss), duplicate, seed and limit, plus the delay jitter
distribution and the loss/dup correlation parameters. All are
kernel-datapath only — the relay's packet-filter registry has no
equivalent, so available_filters hides them on relay links, setting one
on a created relay link returns 409, and the project-load path drops
them with a warning (update_filters runs before the datapath is
decided, so it cannot reject there). Validation mirrors the tc grammar:
reorder requires delay, gemodel and packet_loss are mutually exclusive,
a distribution requires jitter > 0, rate is integer+unit capped at
100gbit.

The translation in DockerVM._netem_command_parts emits the frozen
grammar order and gates extension keywords on the 'tc capabilities'
netem token list (probed once per uBridge process; a plain
delay/loss/corrupt/dup filter never probes, keeping old uBridge builds
untouched; the correl suffix has no token and gates on 'rate' as the
extension-build marker).

Reconcile became reset-before-set: the kernel's netem change merges
optional attributes (rate, correlation, reorder, corrupt, gemodel,
distribution, seed keep their previous value when the new message omits
them), so removing a parameter needs an explicit 'tc reset' — the
bpf_drop filters are re-added right after in the same flow, so the
clsact churn is invisible. The capability check runs before the reset
so a failed update leaves the previous qdisc in place.

e2e (isolated instance, kernel link): 24/24 — rate 512kbit adds exactly
2x the per-packet serialization to the RTT (45.5 ms for 1400-byte
pings), paretonormal jitter + rate-cleared leak check, reorder gap 5,
gemodel 50% bursty loss, duplicate, loss 30% correl 50%, seed 42 +
limit 5000 visible in the qdisc dump, suspend/resume and node restart
keep the filters, relay link rejects rate with 409, teardown leaves no
netem. Full unit suite: 1916 passed.
2026-10-06 00:21:08 +08:00
YueGuobin
b1eb0984bc
fix(docker): bring the veth host end up before a relay attach
The unified veth host end is born admin-down (carrier off until a link
attaches), and libpcap cannot open a packet socket on an admin-down
interface — its netlink promiscuous-mode transaction returns ENOENT, so
'bridge add_nio_ethernet' failed on every runtime relay link (and would
on any relay link at container start). Bring the interface up for the
attach; the carrier pass in the caller refines the state afterwards (a
suspended NIO sets it back down). Verified live with a direct uBridge
probe: attach on a down veth -> ENOENT, after 'link set up' -> OK.
2026-10-06 00:21:08 +08:00
YueGuobin
2b45f29b2c
feat(docker): bpf packet filter on kernel links via cls_bpf match-drop
The GNS3 bpf filter (one expression per line, any line matching drops the
packet) now runs on the kernel datapath: uBridge's tc bpf_drop compiles
each line with libpcap (same compiler as the relay filter) into classic
BPF, attaches it to the veth host end's clsact egress as a cls_bpf
classifier with a gact TC_ACT_SHOT action, and the kernel migrates the
cBPF to eBPF internally -- no CAP_BPF needed.

- reconcile is flush + one add per line at priorities 10, 11, ... (OR
  semantics, same as the relay's per-line filters); tc reset (link
  delete / clear) is the full restore in uBridge and clears them
- capability-gated: the compute probes "tc capabilities" once per uBridge
  process (cached, invalidated on stop); without cbpf=1 the filter apply
  fails with an actionable upgrade error, and old uBridge builds without
  the tc module are never touched when no bpf filter is present
- a line that compiled for the controller's tcpdump but not for the
  compute's uBridge libpcap is skipped with a warning, mirroring the
  relay path
- KERNEL_UNSUPPORTED_FILTERS shrinks to frequency_drop (its eBPF stateful
  classifier is still pending, spec part B)

e2e (22/22): bpf "icmp" = 100% loss; size-discriminating expression
("greater 150") passes 98-byte and drops 300-byte pings (real byte-level
match); two-line OR; coexistence with netem (delay 50 -> small pings
100.2 ms while big dropped); clear removes clsact; suspend/resume keeps
the drops; link delete + recreate leaves no residual filters.
2026-10-06 00:21:08 +08:00
YueGuobin
bd1c205651
docs: frozen uBridge spec for kernel impairment (netem extensions, eBPF/cBPF classifiers) 2026-10-06 00:21:08 +08:00
YueGuobin
0a8d430d50
feat(docker): packet filters on kernel-datapath links via tc netem
Impairment filters with a netem equivalent (delay, packet_loss, corrupt)
now run on the kernel datapath as one tc netem qdisc per veth host end,
pushed through uBridge's netlink tc module (tc netem set / tc reset).
This removes the relay delay filter's nanosleep bottleneck (upstream

Both link endpoints carry the filters: each end's qdisc egress covers
the traffic entering that container, so every direction is impaired
exactly once -- the same net effect as the relay, where both directions
cross the single filtered bridge.

- controller: eligibility keys on relay-only types (frequency_drop, bpf)
  instead of any filter; kernel links push the real filters (the
  synthetic suspend frequency_drop no longer flips a suspended link to
  relay on reopen/reset); update_filters 409s only relay-only types;
  available_filters hides them on kernel links
- compute: create_nio whitelists netem-capable filter types; the NIO
  update path keeps its own guard (PUT mutates the NIO in place);
  _ubridge_apply_netem translates the filter dict into one atomic
  netem-set (full re-apply IS the reconcile); node restart restores the
  qdisc, link deletion detaches it (the veth survives the link)
- e2e (27/27): delay 100 -> 200.2 ms RTT, atomic replace 50 -> 100.2 ms,
  clear -> baseline, packet_loss 30 -> 48 % round-trip (expected 51 %),
  suspend/resume and node restart keep the qdisc, link delete leaves no
  residual impairment
2026-10-06 00:21:08 +08:00
YueGuobin
94b5495683
docs: Docker kernel datapath implementation document
Cover the four-stage stack (kernel datapath, AF_PACKET capture, markers,
veth-everywhere): adapter interface types per node class, datapath
selection, veth lifecycle, link operations, suspend/capture/markers
semantics, uBridge command surface and the tc netem filters roadmap.
Cross-link from the traffic-insight marker doc for the kernel variant.
2026-10-06 00:21:08 +08:00
YueGuobin
672efda592
feat(docker): unify adapter interfaces on veth, allow kernel links at runtime
Every Docker adapter is now born as a veth pair at container start
(unified interface) regardless of NIO type: guest end in the container
namespace, host end in the root namespace. Kernel links enslave the host
end into the per-link Linux bridge; relay links attach it to the uBridge
relay via AF_PACKET (bridge add_nio_ethernet) — the TAP path is gone.

This makes the datapath a runtime decision:
- links can be created on running containers (kernel or relay) without
  touching container interfaces — the controller stopped-node
  eligibility check is removed
- project reopen re-prepares relay links and upgrades eligible ones to
  the kernel datapath
- packet capture start/stop now key on the NIO type (not veth presence)
  so relay-over-veth captures use the relay capture, kernel ones the
  AF_PACKET module
- vendor unix-socket containers keep their wiring: no veth is required
  on the relay path
2026-10-06 00:21:08 +08:00
YueGuobin
61020d3bbc
feat(docker): traffic-insight markers on kernel-datapath links via AF_PACKET
Markers no longer disqualify the kernel datapath. They ride the NIO like
on the relay path and attach to the veth host end through uBridge's
marker add_kernel/delete_kernel/enable_kernel commands (AF_PACKET taps,
same BPF/direction/signal semantics as the relay mark filter).

- base_node: marker anchors are now polymorphic (relay bridge name or
  kernel veth host interface); the reconcile loop is datapath-agnostic
  and DockerVM overrides the add/delete/enable primitives to translate
  the commands. Marker-name validation extracted for both add paths.
- docker_vm: kernel NIO connect/update apply markers on the veth anchor;
  link deletion tears markers down explicitly (the veth survives the
  link, unlike a relay bridge whose death drops its filters).
- controller: eligibility and start_marker no longer exclude markers or
  project-level marker definitions; the kernel NIO carries the capture
  node's marker specs.
- NIO creation accepts markers on kernel NIOs (filters still rejected).
2026-10-06 00:21:07 +08:00
YueGuobin
f6b869be63
feat(docker): packet capture on kernel-datapath links via AF_PACKET
Replace the capture rejection with wiring to uBridge's capture module:
an AF_PACKET socket bound to the veth host end writes into the same pcap
file the relay path uses (capture start_kernel <if> "<pcap>" /
capture stop_kernel), so the pcap streaming to clients is unchanged.

- capture survives node restarts: _connect_nio re-arms it when the NIO
  is already capturing, mirroring the relay path
- uBridge's kernel capture is a singleton per process: a concurrent
  second capture on the same node surfaces uBridge's EALREADY error
- update the datapath docs: filters and markers remain relay-only,
  capture is now served on both datapaths
2026-10-06 00:21:07 +08:00
YueGuobin
58b97b4ce5
fix(controller): don't push the synthetic suspend filter to kernel links
update_suspend() piggybacks on the filter system: get_active_filters()
returns {"frequency_drop": [-1]} while a link is suspended so relay-based
node types drop all traffic at the uBridge filter. Kernel-datapath links
implement suspend natively via interface carrier, and pushing the
synthetic filter tripped the compute-side guard, making suspend fail
with 409 on every kernel link (and leaving controller/kernel state torn).
_get_node_filters() now returns empty filters for kernel links.
2026-10-06 00:21:07 +08:00
YueGuobin
37d7b9121e
feat(docker): wire Docker-to-Docker links through a kernel bridge
Eligible links (both endpoints Docker, same compute, no filters/markers,
stopped nodes, no unix-socket NIO containers) are wired as veth pairs
enslaved into a per-link Linux kernel bridge (gns3 + 11 hex of the link
id, filling IFNAMSIZ) instead of the uBridge UDP relay, so frames flow
entirely in the kernel.

- NIOBridge NIO type: the controller decides the datapath in
  UDPLink._prepare and both computes enslave their deterministic gv*/gc*
  veth pair via uBridge's docker/brctl modules
- carrier/suspend = host-end admin state; both endpoints race-safely
  create/delete the bridge (EEXIST/EBUSY/ENOENT tolerated)
- veth adapters can still attach relay (UDP) links on a running node:
  lazy relay bridge over AF_PACKET on the host end (add_nio_ethernet)
- filters/markers/capture are rejected with explicit errors on this
  datapath (they live in the uBridge relay); vendor unix-socket
  containers are excluded; Server.enable_kernel_datapath is the
  kill-switch (default on)
- host-end veths survive container death and are removed on stop plus
  swept by name on the next start (crash/stale recovery)
2026-10-06 00:21:07 +08:00
Jeremy Grossmann
65c858d455
Merge pull request #2942 from markparonyan/ruff/pr03-fstrings
refactor: use f-strings
2026-10-05 14:35:56 +02:00
Mark Paronyan
b3b1ba0d4e
Merge branch '3.1' into ruff/pr03-fstrings 2026-10-04 20:56:13 +03:00
Jeremy Grossmann
9f9be983cd
Merge pull request #2939 from markparonyan/patch-1
fix: bump asyncssh - at least 2.24.1 for CVE-2026-62949 fixes
2026-10-04 18:15:03 +02:00
Mark Paronyan
1c5c3d1f76
fix: bump asyncssh - at least 2.24.1 for CVE-2026-62949 fixes
Updated asyncssh version to 2.24.1 in requirements.
2026-10-04 19:01:30 +03:00
Jeremy Grossmann
38cc7cc0a0
Merge pull request #2938 from markparonyan/ruff/pr02-unused-imports
Ruff/pr02 unused imports
2026-10-03 19:33:45 +02:00
Mark Paronyan
7e1bc67be2
chore(ruff): drop UP032 from the ignore baseline 2026-10-03 19:44:02 +03:00
Mark Paronyan
3135c8b7e3
refactor: use f-strings in remaining modules, scripts and tests 2026-10-03 19:44:02 +03:00
Mark Paronyan
be943fcdf3
refactor: use f-strings in controller tests 2026-10-03 19:44:02 +03:00
Mark Paronyan
bee6b3f8b1
refactor: use f-strings in compute tests 2026-10-03 19:44:01 +03:00
Mark Paronyan
7a9c600f54
refactor: use f-strings in controller modules 2026-10-03 19:44:01 +03:00
Mark Paronyan
552c0ca3e0
refactor: use f-strings in remaining compute modules 2026-10-03 19:44:01 +03:00
Mark Paronyan
bbebf20825
refactor: use f-strings in dynamips manager 2026-10-03 19:44:01 +03:00
Mark Paronyan
c458459626
refactor: use f-strings in dynamips NIO modules 2026-10-03 19:44:01 +03:00
Mark Paronyan
fba3ecc5fc
refactor: use f-strings in dynamips nodes 2026-10-03 19:44:00 +03:00
Mark Paronyan
a5d2bca921
refactor: use f-strings in dynamips router node 2026-10-03 19:44:00 +03:00
Mark Paronyan
97f6805d0a
refactor: use f-strings in iou compute modules 2026-10-03 19:44:00 +03:00
Mark Paronyan
d5ad4c67c6
refactor: use f-strings in qemu compute modules 2026-10-03 19:44:00 +03:00
Mark Paronyan
7f2c0c7e9c
refactor: use f-strings in docker compute modules 2026-10-03 19:44:00 +03:00