docker: also null-bind udevadm in GNS3_MASK_UDEV

Masking the udev systemd units stopped the daemon's coldplug (host audio
resets), but host USB devices still reconnected on every XRd start. A/B
testing with plain `docker run` isolated the trigger: XRd's own
xr_startup.sh calls udevadm directly (USB license-dongle probing, e.g.
`udevadm trigger --action=add --parent-match=<usb device>`), which
synthesizes uevents into the host kernel from the privileged container --
no udevd required.

GNS3_MASK_UDEV=1 now also binds /dev/null over the udevadm binary
(/bin, /sbin, /usr/bin). Verified with a plain-run experiment: with the
bind, host udev monitor shows zero usb/input/hid/sound events during XRd
boot (only normal docker veth traffic), and XRd itself boots to running
state -- it does not need udevadm under GNS3 (interfaces are pre-created
veths).
This commit is contained in:
YueGuobin 2026-08-14 19:09:48 +08:00
parent b928a2f48a
commit e8b51aa12c
No known key found for this signature in database
2 changed files with 17 additions and 2 deletions

View File

@ -80,6 +80,19 @@ class DockerVM(BaseNode):
"systemd-udev-settle.service",
)
# udevadm binary paths also null-bound by GNS3_MASK_UDEV=1. NOS startup
# scripts call udevadm directly -- Cisco XRd's xr_startup.sh runs
# `udevadm trigger --action=add --parent-match=<usb device>` (USB license
# dongle probing), which synthesizes uevents into the host kernel from a
# privileged container and reconnects host USB devices. Masking the units
# alone does not stop this; the binary must be neutralized too. XRd boots
# fine without udevadm (interfaces are pre-created by GNS3).
_UDEVADM_PATHS = (
"/bin/udevadm",
"/sbin/udevadm",
"/usr/bin/udevadm",
)
def __init__(
self,
name,
@ -561,11 +574,11 @@ class DockerVM(BaseNode):
# audio/disk devices, reconnecting/muting them on every start.
# XRd doesn't need udev (interfaces are pre-created by GNS3), so
# bind /dev/null over the udev units to keep it from running.
for unit in self._UDEV_UNITS:
for target in [f"/etc/systemd/system/{u}" for u in self._UDEV_UNITS] + list(self._UDEVADM_PATHS):
params["HostConfig"]["Mounts"].append({
"Type": "bind",
"Source": "/dev/null",
"Target": f"/etc/systemd/system/{unit}",
"Target": target,
"ReadOnly": True,
})
elif line.startswith("GNS3_MASK_SYSTEMD="):

View File

@ -327,6 +327,8 @@ async def test_create_masks_systemd_units(compute_project, manager):
if m.get("Source") == "/dev/null"}
for unit in DockerVM._UDEV_UNITS:
assert f"/etc/systemd/system/{unit}" in masked
for path in DockerVM._UDEVADM_PATHS:
assert path in masked
assert "/etc/systemd/system/foo.service" in masked
assert "/etc/systemd/system/bar.socket" in masked