From a59bd7550779e9737443fc93464c0924ffdec579 Mon Sep 17 00:00:00 2001 From: Eduard Tolosa Date: Fri, 23 Oct 2026 15:34:29 -0500 Subject: [PATCH] Document nspawn 2.1.0: published ports over IPv6 - networking: an IPv6 subsection on the proxy unit per machine, the -p forms and where each publishes, the client address a machine sees and the host firewall; firewalld's trusted zone covers the IPv4 side - machines and reference: -p on every IPv4 and IPv6 address, [::1] and [::] forms, nspawn port printing the [::] line - compose: the ps example with the [::] binding, as a 2.1.0 run prints it - hugo.yaml: documented version 2.1.0 --- content/en/docs/compose.md | 2 +- content/en/docs/machines.md | 5 ++-- content/en/docs/networking.md | 51 +++++++++++++++++++++++++++++------ content/en/docs/reference.md | 7 ++--- hugo.yaml | 2 +- 5 files changed, 52 insertions(+), 15 deletions(-) diff --git a/content/en/docs/compose.md b/content/en/docs/compose.md index cdd5303..58fd155 100644 --- a/content/en/docs/compose.md +++ b/content/en/docs/compose.md @@ -62,7 +62,7 @@ $ sudo nspawn compose up -d $ sudo nspawn compose ps NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS shop-db-1 postgres:18-alpine "docker-entrypoint.s…" db 4 seconds ago Up 3 seconds (healthy) -shop-web-1 nginx:alpine "/docker-entrypoint.…" web 3 seconds ago Up Less than a second 0.0.0.0:8080->80/tcp +shop-web-1 nginx:alpine "/docker-entrypoint.…" web 3 seconds ago Up Less than a second 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp $ sudo nspawn compose exec db psql -U postgres -c 'select 1' ?column? diff --git a/content/en/docs/machines.md b/content/en/docs/machines.md index 0ad3871..dd9b1fb 100644 --- a/content/en/docs/machines.md +++ b/content/en/docs/machines.md @@ -66,8 +66,8 @@ variables and the volumes of the last run: fixes its address on one made with `network create`; see [Networking](/docs/networking/). - `-p [IP:]HOST:CONTAINER[/udp]` publishes a port on the host, on every - address or on one, like docker. It needs a bridge network; `-p none` forgets - all published ports. + IPv4 and IPv6 address or on one, like docker. It needs a bridge network; + `-p none` forgets all published ports. - `--entrypoint`, `-e`, `--env-file` and the arguments after `--` change what an **app** image runs; see [Command, entrypoint and environment](#command-entrypoint-and-environment). Boot images refuse them. @@ -909,6 +909,7 @@ $ sudo nspawn wait job 3 $ sudo nspawn port web 80/tcp -> 0.0.0.0:8080 +80/tcp -> [::]:8080 ``` `wait NAME...` waits for machines to stop and prints each one's exit code, and diff --git a/content/en/docs/networking.md b/content/en/docs/networking.md index 5389a92..a3ffd45 100644 --- a/content/en/docs/networking.md +++ b/content/en/docs/networking.md @@ -58,7 +58,8 @@ host's address on the machine's network. On hosts with systemd 258 or newer, machined also lets the host resolve machine names by itself. The bridge carries IPv4 only, so neither the machines nor the bridge get an IPv6 link-local address, and a machine's name leads to its bridge address: -`ping web` from the host answers from `10.99.0.x`. +`ping web` from the host answers from `10.99.0.x`. Published ports answer +over IPv6 all the same; see [IPv6](#ipv6). ## Networks of your own @@ -221,11 +222,11 @@ sudo nspawn network inspect front sudo nspawn start web -p 8080:80 -p 5353:53/udp -p 127.0.0.1:9090:9090 -p 8000-8010:8000-8010 ``` -Each `-p [IP:]HOST:CONTAINER[/udp]` becomes a DNAT entry in the `ip nspawn` -table. Without an address the port is reachable from other hosts, from the -host's own addresses and from `127.0.0.1` (through `route_localnet`, as docker -does without its userland proxy); with one, `127.0.0.1:9090:9090` say, on that -address of the host alone, for a reverse proxy in front. A range, +On IPv4 each `-p [IP:]HOST:CONTAINER[/udp]` becomes a DNAT entry in the `ip +nspawn` table. Without an address the port is reachable from other hosts, from +the host's own addresses and from `127.0.0.1` (through `route_localnet`, as +docker does without its userland proxy); with one, `127.0.0.1:9090:9090` say, on +that address of the host alone, for a reverse proxy in front. A range, `8000-8010:8000-8010`, is one mapping per port, which `inspect` shows one by one. The entries are installed once the machine is registered and removed when it ends, by the unit hooks, so they also go away after a crash or when the @@ -239,12 +240,46 @@ the same ports, and `-p none` forgets them all. Ports need a bridge network (not an internal one); a machine on the host's network listens on the host's ports directly. +### IPv6 + +The machines have IPv4 addresses alone, and nftables cannot translate IPv6 into +IPv4, so a port answers on the host's IPv6 addresses through a proxy, as +docker's userland proxy serves it. `nspawn-proxy-NAME.service`, a transient +unit the machine's hooks start once it runs and stop with it, listens there and +relays TCP connections and UDP datagrams to the machine's address; UDP replies +leave from the address the client wrote to, so HTTP/3 works too. The IP of `-p` +says where a port is published, as with docker: + +| `-p` | Where | +| --- | --- | +| `8080:80` | every IPv4 address and, on a host with IPv6, every IPv6 address | +| `0.0.0.0:8080:80` | every IPv4 address | +| `127.0.0.1:8080:80` | that IPv4 address | +| `[::]:8080:80` | every IPv6 address | +| `[2001:db8::1]:8080:80`, `[::1]:8080:80` | that IPv6 address | + +```text +$ sudo nspawn port web +80/tcp -> 0.0.0.0:8080 +80/tcp -> [::]:8080 +``` + +A link-local address (`fe80::`) cannot be named, as it needs an interface; +`[::]` takes it with the others. The port checks cover the IPv6 side as well, +and when the proxy cannot listen, `start` says so and the IPv4 side works all +the same (`journalctl -u nspawn-proxy-web` says why). A machine sees an IPv6 +client as the bridge's gateway address, not the client's own, as with docker's +proxy. The client comes in through the host's input path, not its forwarding, +so a host firewall has to let the port in, as for any service of the host; +nspawn changes no rule for it. + ## Firewalls - **firewalld**: the bridge is placed in the `trusted` zone at runtime, which - also lets published ports through. The binding does not survive + also lets published ports through on IPv4. The binding does not survive `firewall-cmd --reload`; the next `start` or `nspawn network up` puts it - back. + back. The IPv6 side of a port needs the port open in the zone of the host's + interface, as a service of the host does. - **docker** (in its default iptables mode) and **ufw** set the `FORWARD` policy to `DROP`, which would silence every machine on the bridge. `start` then adds two rules to the `DOCKER-USER` chain, which docker reserves for diff --git a/content/en/docs/reference.md b/content/en/docs/reference.md index 700c864..2576d6f 100644 --- a/content/en/docs/reference.md +++ b/content/en/docs/reference.md @@ -381,7 +381,7 @@ Boots an image as a machine. Every option is remembered for the next start. | --- | --- | | `--network NETWORK` | Network of the machine: `bridge`, the default network (the default); the name of a network made with [network create](#network-create); `veth`, a veth pair configured by systemd-networkd on the host (booted images only); `host`, the host's own network; `none`, no interface but `lo`; or `container:NAME`, the network namespace of that running machine, like `docker run --network container:NAME` (app images only; see [container:NAME](/docs/networking/#containername)). Repeatable for several bridge networks, the first one primary: its address is where published ports lead and its gateway the default route, unless it is internal, in which case the first network that is not has the route. `veth`, `host`, `none` and `container:NAME` go alone. | | `--network-alias [NETWORK=]NAME` | Another name for the machine on its primary network, or on `NETWORK`, like `docker --network-alias`: every member of that network resolves it. Repeatable; `none` forgets them. | -| `-p`, `--publish [IP:]HOST:CONTAINER[/udp]` | Publish a port on the host, like `docker -p`: on every address of the host, or on `IP` alone (`127.0.0.1:8080:80`). `8000-8010:8000-8010` publishes a range, one mapping per port. Repeatable; `none` forgets them all. | +| `-p`, `--publish [IP:]HOST:CONTAINER[/udp]` | Publish a port on the host, like `docker -p`: on every IPv4 and IPv6 address of the host, or on `IP` alone (`127.0.0.1:8080:80`, `[::1]:8080:80`); `0.0.0.0` is every IPv4 address, `[::]` every IPv6 one. `8000-8010:8000-8010` publishes a range, one mapping per port. Repeatable; `none` forgets them all. | | `--ip [NETWORK=]IP` | A fixed IPv4 address on the primary network, or on `NETWORK`: one made with [network create](#network-create), inside its subnet and nobody else's there, like `docker --ip`. Repeatable, once per network. | | `--entrypoint PROGRAM` | Replace the image's entrypoint and drop its cmd, so only the arguments given follow it; an empty string runs the arguments alone. An app left with nothing to run is refused with `no command specified`. App images only. | | `-e`, `--env VAR[=VALUE]` | Environment for the program, `VAR=value` or `VAR` copied from the calling shell. Repeatable; `none` forgets them. App images only. | @@ -623,8 +623,9 @@ nspawn port NAME [PRIVATE_PORT[/PROTO]] ``` Prints the ports a running machine publishes, like `docker port` -(`80/tcp -> 0.0.0.0:8080`), or only those of one port of the machine; one it -does not publish is an error, as docker words it. +(`80/tcp -> 0.0.0.0:8080`, and `80/tcp -> [::]:8080` on a host with IPv6), or +only those of one port of the machine; one it does not publish is an error, as +docker words it. ## cp diff --git a/hugo.yaml b/hugo.yaml index 1232c44..5b54e92 100644 --- a/hugo.yaml +++ b/hugo.yaml @@ -53,7 +53,7 @@ params: privacy_policy: /about/#privacy # Version of nspawn the documentation describes. - version: 2.0.0 + version: 2.1.0 archived_version: false # In-page links to open issues and suggest changes.