Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
setuid/setuid
prun/*.1
mx-networkd/target/
11 changes: 10 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,14 @@ INSTALL_DATA = $(INSTALL) -m 644

manpages += prun/prun.1 prun/pman.1 prun/ptype.1

all: $(manpages)
CARGO ?= cargo
mx_networkd = mx-networkd/target/release/mx-networkd

all: $(manpages) $(mx_networkd)

clean:
@rm -f $(manpages)
@rm -rf mx-networkd/target

install: all
@prefix="$(prefix)" usr_prefix="$(usr_prefix)" usrlocal_prefix="$(usrlocal_prefix)" \
Expand All @@ -89,3 +93,8 @@ install: all
%.1: %.md
@if [ "$$UID" = 0 ]; then echo "Please build (\`make\`) as non-root before running \`make install\` as root" >&2;exit 1;fi
pandoc --standalone --to man $< -o $@

$(mx_networkd): mx-networkd/Cargo.toml $(wildcard mx-networkd/src/*.rs)
@if [ "$$UID" = 0 ]; then echo "Please build (\`make\`) as non-root before running \`make install\` as root" >&2;exit 1;fi
@command -v $(CARGO) >/dev/null || { echo "$(CARGO) not found - it is needed to build mx-networkd" >&2; exit 1; }
$(CARGO) build --release --manifest-path mx-networkd/Cargo.toml
4 changes: 4 additions & 0 deletions etc/systemd/system/mxvlan.service
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ DefaultDependencies=no
After=sysinit.target
After=network.service
Before=network.target
# Machines carrying the hostconfig tag mxnetworkgenerator are configured
# by systemd-networkd, fed by the mx-networkd generator instead; see
# mx-networkd/README.md, section Migration.
ConditionPathExists=!/node/tags/mxnetworkgenerator

[Service]
Type=oneshot
Expand Down
4 changes: 4 additions & 0 deletions etc/systemd/system/network.service
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ DefaultDependencies=no
After=sysinit.target
Before=network.target
Wants=network.target
# Machines carrying the hostconfig tag mxnetworkgenerator are configured
# by systemd-networkd, fed by the mx-networkd generator instead; see
# mx-networkd/README.md, section Migration.
ConditionPathExists=!/node/tags/mxnetworkgenerator

[Service]
EnvironmentFile=/etc/local/mxhost.conf
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Run systemd-networkd only on machines that have been switched over to the
# mx-networkd generator (hostconfig tag mxnetworkgenerator). On all other
# machines network.service/mxvlan.service keep configuring the network; the
# two mechanisms must never run at the same time. See mx-networkd/README.md,
# section Migration. /node/tags/ is populated by startup-tags.service, which
# runs Before=sysinit.target, i.e. before this unit's condition is checked.
[Unit]
ConditionPathExists=/node/tags/mxnetworkgenerator
8 changes: 8 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ fi
: ${sysconfdir:=$prefix/etc}

: ${systemdunitdir:=$sysconfdir/systemd/system}
: ${systemdgeneratordir:=$usr_libdir/systemd/system-generators}
: ${udev_rulesdir:=$sysconfdir/udev/rules.d}
: ${crond_dir:=$sysconfdir/cron.d}
: ${udev_helperdir:=$prefix/lib/udev}
Expand Down Expand Up @@ -145,6 +146,13 @@ install_exec mkmotd/mkmotd.pl "$DESTDIR$usr_sbindir/m
install_data mkmotd/motd.service "$DESTDIR$systemdunitdir/motd.service"
install_exec mxgrub/mxgrub "$DESTDIR$usr_sbindir/mxgrub"
install_exec mxnetctl/mxnetctl "$DESTDIR$usr_sbindir/mxnetctl"
# mx-networkd is built by `make`; it is installed but not enabled -- the
# generator only does something on a machine that runs systemd-networkd.
if [ -x mx-networkd/target/release/mx-networkd ]; then
install_exec mx-networkd/target/release/mx-networkd "$DESTDIR$usr_sbindir/mx-networkd"
install_exec mx-networkd/target/release/mx-networkd "$DESTDIR$systemdgeneratordir/mx-networkd-generator"
fi
install_exec mx-networkd/migrate-to-mx-networkd.sh "$DESTDIR$usr_sbindir/migrate-to-mx-networkd"
install_exec mxrouter/mxrouterctl "$DESTDIR$usr_sbindir/mxrouterctl"
install_exec mxvlan/mxvlanctl "$DESTDIR$usr_sbindir/mxvlanctl"
install_exec netlog/netlog "$DESTDIR$usr_sbindir/netlog"
Expand Down
7 changes: 7 additions & 0 deletions mx-networkd/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions mx-networkd/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "mx-networkd"
version = "0.1.0"
edition = "2021"
rust-version = "1.70"
description = "Translate the central MarIuX network configuration into systemd-networkd and udev configuration"
license = "MIT"
publish = false

# No dependencies on purpose: this runs as a systemd generator before any
# file system other than / is guaranteed to be there, and it has to be
# auditable by everyone who maintains the network configuration.
[dependencies]

[profile.release]
opt-level = "s"
panic = "abort"
strip = true
169 changes: 169 additions & 0 deletions mx-networkd/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# mx-networkd

Translates the centrally managed MarIuX network configuration into
systemd-networkd and udev configuration, so that `mxnetctl`, `mxvlanctl`,
`network.service` and `mxvlan.service` can be retired without changing any of
the pushed configuration files.

Status: **not yet in use anywhere.** Nothing in this directory is installed or
enabled by default; the old units keep running until someone switches a
machine over by hand (see [Migration](#migration)).

## What it does

It runs as a **systemd generator**. Generators run at every boot and at every
`systemctl daemon-reload`, before any unit starts — including
`systemd-udevd.service`. That is early enough for the `.link` files to be in
place before the first network device is coldplugged, and it means
`/etc/systemd/network/` stays permanently empty: everything is written to
`/run/systemd/network/`, which is also a search path for udev and networkd.

/etc/local/mxnet ─┐ ┌─ /run/systemd/network/10-mx-<name>.link
/etc/local/mxhost.conf ─┼─ mx-networkd ──────┼─ /run/systemd/network/10-mx-<vlan>.netdev
/etc/mxvlans ─┘ └─ /run/systemd/network/20-mx-<iface>.network

The three input files keep their current format and stay the single source of
truth. No file below `/etc` is written (except by `mx-networkd learn`, see
below).

### Mapping

| today | with mx-networkd |
| --- | --- |
| `mxnetctl start` renames by MAC | `.link` file per interface, applied by udev |
| `network.service` adds `MX_IPADDR/20` and the default route | `.network` file for `MX_NETDEV` |
| `mxvlanctl start` creates VLANs and their addresses | `.netdev` + `.network` per VLAN |
| `mxvlanctl` brings the base device up | `.network` for the base device |

The prefix length (20), the gateway (141.14.16.128) and the broadcast address
were hard coded in `network.service`. They are defaults here and can be
overridden per host with `MX_PREFIXLEN`, `MX_GATEWAY` and `MX_BROADCAST` in
`mxhost.conf` without touching the tool.

### One rule worth knowing

systemd-networkd applies **only the first matching `.network` file** to an
interface. Everything belonging to one interface therefore has to be in one
file, even when it comes from two different inputs — the primary address from
`mxhost.conf` and the `VLAN=` entries from `mxvlans` end up in the same
`20-mx-net00.network`. That is why this is a program and not a pair of
`sed` scripts.

## Usage

mx-networkd generate [--root DIR] [--output DIR] [-n]
mx-networkd check [--root DIR]
mx-networkd learn [--root DIR] [-n]

`generate` is what the generator symlink runs; systemd passes three directory
arguments, which are accepted and ignored. Output is deterministic — no time
stamps — so re-running it rewrites nothing and `daemon-reload` does not churn
the network configuration. Files that mx-networkd generated earlier and no
longer wants are removed; files in `/run/systemd/network/` belonging to
anything else are never touched.

`check` parses the same inputs and exits non-zero if anything is wrong. Run it
on the master before pushing `/etc/mxvlans`, and a typo stops there instead of
on 300 machines. At boot the same problems are logged and the offending line
is skipped: a partially configured network beats no network at all.

`learn` replaces the one thing `.link` files cannot do — inventing a name for a
MAC address nobody has seen before. It uses mxnetctl's algorithm (prefer
`netNN` matching the kernel's `ethN`, otherwise the lowest free number), writes
`/etc/local/mxnet`, and like mxnetctl it refuses to write when
`/etc/local/USB.usb` exists. It does not rename anything; that is udev's job
after the next `generate`. Only needed on first installation.

To preview on any machine without touching it:

mx-networkd generate --root / --dry-run

## Building

cargo build --release
cargo test

No dependencies, no build script, `std` only. This is deliberate: the binary
runs as a generator before most of the system exists, and everyone who
maintains the network configuration should be able to read all of it.

`make` in the repository root builds it and `make install` installs the binary
as `/usr/sbin/mx-networkd` and as the generator
`/usr/lib/systemd/system-generators/mx-networkd-generator`.

## Migration

The switch per machine is the **hostconfig tag `mxnetworkgenerator`**
(hostconfig allows hyphens in tag names: a tag term matches
`[a-z][a-z0-9$_-]*`). Tags live in the centrally pushed `/etc/hostconfig`
and are materialised as files below `/node/tags/` by `startup-tags.service`
(`hostconfig --populate-node`) at every boot. Everything is conditioned on
that one tag:

| | with the tag | without the tag |
| --- | --- | --- |
| `mx-networkd generate` | writes to `/run/systemd/network/` | writes nothing, prunes its old files |
| `systemd-networkd.service` | runs (drop-in `ConditionPathExists=/node/tags/mxnetworkgenerator`) | skipped |
| `network.service`, `mxvlan.service` | skipped (`ConditionPathExists=!/node/tags/mxnetworkgenerator`) | run as before |

`/node/tags/` is on the root file system and persists across boots, so the
tag file is already there when the generators run — before
`startup-tags.service` has run again. The old and the new mechanism never
run at the same time. `--dry-run` ignores the tag, so the preview always
works.

Per machine, reversible at every step — use the helper script:

migrate-to-mx-networkd

It validates the prerequisites and the configuration (`mx-networkd check`),
shows the `--dry-run` preview, verifies the tag is set in `/etc/hostconfig`
(and tells you what to add on the distmaster if it is not), refreshes
`/node/tags/` and enables `systemd-networkd.service` for the next boot. It
never starts, stops or reloads anything, so it is safe to run over SSH and
safe to run repeatedly. Do **not** flip the services by hand on a running
machine: `network.service` deletes the primary address on stop
(`ExecStop=ip addr del …`), so stopping it over SSH cuts the connection and
locks you out. The switchover is the reboot:

1. Add `tag mxnetworkgenerator` for the host to `/etc/hostconfig` on the
distmaster and push it.
2. Run `migrate-to-mx-networkd`, read its output, compare the preview with
`ip -br addr` and `ip -d link show type vlan`.
3. `reboot`. The generator runs before udev, the `.link` files rename the
devices at coldplug, and networkd owns the addresses from the start. A
reboot is needed anyway: renaming only happens on device *add* — a
running interface is not renamed.
4. After the reboot, verify: `ip -br addr`, `ip -d link show type vlan`,
`networkctl status`.

To go back — also at the next boot, not live, for the same reason in
reverse (`ip addr add` on an address networkd still holds fails, and the
units fight over the interface):

1. Remove the tag from `/etc/hostconfig` on the distmaster and push it.
2. `hostconfig --populate-node`
3. `systemctl disable systemd-networkd.service`
4. `reboot` — the old units run again; the generator removes its files.

## Deliberate non-goals

* **No `apply` subcommand.** Applying the configuration is
`networkctl reload`. A second implementation of "make the kernel match the
files" is exactly the divergence this change is supposed to remove.
* **No new file format.** If `/etc/mxvlans` needs to grow a field one day,
that is a separate discussion; this change must be a no-op for the people
who edit it.
* **No IPv6.** The current scripts do not configure any, so neither does this.
`.network` files are the obvious place to add it later.

## Known gaps

* `learn` assumes kernel names of the form `ethN`, i.e. `net.ifnames=0`, which
is what mxnetctl assumed too. On a machine with predictable names
(`enp1s0f0`) it will find nothing and say so.
* Renaming still only happens at device add. Changing a name in
`/etc/local/mxnet` needs a reboot (or `udevadm trigger` plus taking the
interface down), same as before.
* `/etc/mxvlans` is read for the local host only, matched on the short host
name, as mxvlanctl does.
119 changes: 119 additions & 0 deletions mx-networkd/migrate-to-mx-networkd.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#! /bin/bash

# migrate-to-mx-networkd - switch this machine from network.service/mxnetctl/
# mxvlanctl to systemd-networkd fed by the mx-networkd generator.
#
# The switchover happens at the NEXT BOOT, never live: network.service
# deletes the primary address on stop, so flipping services on a running
# machine over SSH locks you out (see mx-networkd/README.md, Migration).
# This script therefore never starts, stops or reloads anything -- it only
# validates the configuration, checks the hostconfig tag, refreshes
# /node/tags/ and enables systemd-networkd for the next boot. It is
# idempotent: run it as often as you like, over SSH too.
#
# The single switch is the hostconfig tag 'mxnetworkgenerator':
# - with the tag: mx-networkd generates, systemd-networkd runs,
# network.service/mxvlan.service are skipped by their
# ConditionPathExists=!/node/tags/mxnetworkgenerator
# - without the tag: mx-networkd writes nothing, systemd-networkd is
# skipped by its drop-in condition, the old units run
#
# Written by OnkelClaude (AI) on behalf of Boris Bergenroth.

TAG=mxnetworkgenerator
TAGFILE=/node/tags/$TAG
DROPIN=/etc/systemd/system/systemd-networkd.service.d/mx-network-generator.conf

die() {
echo "$0: error: $*" >&2
exit 1
}

step() {
echo
echo "== $*"
}

[ "$(id -u)" = 0 ] || die "must be run as root"

step "checking prerequisites"

[ -x /usr/sbin/mx-networkd ] \
|| die "/usr/sbin/mx-networkd is not installed - update mxtools first"
[ -x /usr/lib/systemd/system-generators/mx-networkd-generator ] \
|| die "generator /usr/lib/systemd/system-generators/mx-networkd-generator is not installed - update mxtools first"
[ -x /usr/sbin/hostconfig ] \
|| die "/usr/sbin/hostconfig is not installed"
[ -e /etc/local/mxhost.conf ] \
|| die "/etc/local/mxhost.conf does not exist - this machine does not use the MarIuX network configuration"
[ -e /etc/local/mxnet ] \
|| die "/etc/local/mxnet does not exist - run 'mx-networkd learn' (or mxnetctl) first"
[ -e "$DROPIN" ] \
|| die "$DROPIN is missing - update mxtools first"
systemctl cat systemd-networkd.service >/dev/null 2>&1 \
|| die "systemd-networkd.service does not exist on this machine"
grep -q 'ConditionPathExists=!'"$TAGFILE" /etc/systemd/system/network.service 2>/dev/null \
|| die "/etc/systemd/system/network.service is not conditioned on the tag yet - update mxtools first"
echo "ok"

step "validating the network configuration (mx-networkd check)"
/usr/sbin/mx-networkd check \
|| die "configuration problems found - fix them first; nothing was changed"

step "preview of the generated configuration (nothing is applied)"
/usr/sbin/mx-networkd generate --dry-run
echo
echo "Compare with the running state: ip -br addr ; ip -d link show type vlan"

step "checking hostconfig tag '$TAG'"
if /usr/sbin/hostconfig "$TAG"; then
echo "tag '$TAG' is set for this host in /etc/hostconfig"
else
cat >&2 <<__EOF__

The tag '$TAG' is NOT set for this host in /etc/hostconfig.
/node/tags/ is rebuilt from /etc/hostconfig at every boot, so the tag has
to come from there to survive. Add it on the distmaster:

$(uname -n | cut -d. -f1) tag $TAG

push /etc/hostconfig to this machine, then run this script again.

Nothing was changed.
__EOF__
exit 1
fi

step "refreshing /node/tags/ (hostconfig --populate-node)"
/usr/sbin/hostconfig --populate-node \
|| die "hostconfig --populate-node failed"
[ -e "$TAGFILE" ] \
|| die "$TAGFILE did not appear after populate-node"
echo "ok: $TAGFILE"

step "enabling systemd-networkd.service for the next boot (not starting it)"
systemctl enable systemd-networkd.service \
|| die "systemctl enable systemd-networkd.service failed"

cat <<__EOF__

== done - this machine switches to mx-networkd at the next boot

Nothing has changed on the running system: network.service keeps the
addresses until shutdown. network.service and mxvlan.service stay enabled;
from the next boot on they are skipped by their tag condition, and
systemd-networkd takes over. Reboot when ready, then verify with:

ip -br addr
ip -d link show type vlan
networkctl status

Rollback (also reboot-based, do NOT flip services on a running machine):

1. remove the '$TAG' tag for this host from /etc/hostconfig
on the distmaster and push the file
2. hostconfig --populate-node
3. systemctl disable systemd-networkd.service
4. reboot

__EOF__
Loading