Add net.ipv6_pin_gateway (off by default): make the HOST's upstream
link-local default gateway a permanent neighbour entry, so the kernel stops
probing it.
Why: on a host whose provider router answers neighbour solicitations
unreliably, the host's neighbour for its default nexthop drops to INCOMPLETE,
and the host then answers every forwarded packet — every container's IPv6
included — with "address unreachable" until it recovers. The container keeps
its address and can ping the bridge, but nothing off-link works,
intermittently; the host's own IPv6 drops at the same moments. Captured
on-bridge: the host (the bridge's global address) returning ICMP6
destination-unreachable for the container's packets, correlated with the
gateway neighbour going REACHABLE -> DELAY -> PROBE -> INCOMPLETE.
Applied by RewireAllIPv6, so `vps ipv6-reapply` (the boot unit) and
`vps install` re-apply it, and `vps config set net.ipv6_pin_gateway true`
applies it immediately. It only touches the host's own link-local default
gateway, and only while that gateway is resolved: the MAC is read from the
live neighbour table by the new `vps ip6 neigh-pin` root helper
(`neigh-unpin` drops the entry again when the key is turned off). Off by
default because a permanent entry is never re-resolved — a gateway that
later moves to another MAC would be missed.
Also drop the bridge-MAC pin from 02d55d2 (and lx.NetworkGet with it):
measured on a live host, Incus keeps a managed bridge's MAC stable across
two reboots even with bridge.hwaddr unset and no stored MAC, so that pin
guarded a failure that does not occur.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Every container pins the bridge's MAC as its static fe80::1 gateway
neighbor — the guest cannot resolve the gateway by NDP, because
security.ipv6_filtering drops its neighbor solicitation (it is sourced from
the guest link-local, which is outside the routed /112 the source filter
allows). Incus does not persist a managed bridge's MAC, so a host reboot
recreated the bridge with a new one and every guest pinned to the old MAC
kept its address but lost all IPv6; the boot-time reapply only heals a
container that is already running. bridge.hwaddr is now pinned to the MAC
in use, so the guest pins never go stale.
Also render one NDP rule per block: a reinstall passed the container's block
as `fresh` while its account row already existed, emitting the rule twice.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- allow net.ipv6_extra_prefix when net.ipv6_mode is none (IPv4-only)
- deliver standalone /64 to containers via a routed NIC (eth0) alongside bridged private IPv4 (eth1)
- bind <block>::1/64 on-link inside guest with default route fe80::1 (no local route, enabling SLAAC and internal subnet delegation)
- in-tree NDP responder (vps-ipv6.service) covers the extra /64 blocks on ext_if in none mode
- admin overview, user creation, batch creation, quota dialog, and IPv6 management page support none mode
- add unit tests covering none mode allocation, middle slice (/56 in /48), and admin UI
A container that owns a whole /64 had only its primary address reachable: the
guest makes the whole /112 local, but the host could no longer hand packets for
any other address in it to the container, so every suffix but ::1 went silent.
The cause was the shape v1.11.0 chose to get the /64 routed. Incus programs a
declared route as `via <ipv6.address>`, and the kernel refuses such a route when
the gateway is itself reached by a gateway route — which the account's own /112
route is ("RTNETLINK answers: No route to host"). Dropping ipv6.address made
both routes direct (`dev`), and a direct route means the host resolves every
address by neighbour discovery on the bridge, where the guest answers only for
the addresses it has bound.
Keep ipv6.address and order the list instead: `<block>/64,<account /112>`. Incus
installs a NIC's routes in the order declared, so the /64 is in the table before
the /112 route exists and both end up as via-address routes. The /112 reaches
the container's MAC again (every address in it answers, as it did before this
feature), and the whole /64 lands on the container's MAC too — which is what
lets a customer route sub-prefixes out of it.
- blockRoutes puts the /64 first, with the reasoning and a test pinning the order
- applyExtraRoutes declares the address again instead of removing it
- EnsureBlockRoutes no longer truncates a block owner's declaration to the /112
- share/transfer build instances with the address again (they had lost it too)
- lx.EnsureDeviceOptionsDrop is gone: nothing needs to remove a device key now
- docs/ipv6.md describes the shape and why each part of it is load-bearing
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- a container without a whole /64 now gets exactly the NIC it has always had
(ipv6.address + the /112 in ipv6.routes, the /112 route via that address).
Only a /64 owner uses the routes-only form, so an upgraded host is
unchanged for everything that does not touch the new key.
- drop the `vps extra64` command: `vps quota <name> --extra64` assigns a block
instead, next to the quotas it belongs with (the panel's quota dialog is the
other entry point). Nothing else about quota changed.
- docs updated for the merged flag.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- admin → IPv6 (prefix mode): the extra-prefix editor, a capacity readout
(total / kept for this host / assigned / free) and the assigned blocks with
their owners — only the assigned ones, a /48 has 65536.
- create and batch forms carry a ticked-by-default "Assign a whole /64 (N
left)" checkbox, disabled once the pool is empty; The quota dialog can hand
one to an existing container and shows an existing block read-only, since it
can never be taken back there (only deleting the account releases it).
- the user panel lists the block next to the IPv6 address, plainly — nothing
advertises it as something to ask for.
- docs: the configuration.md key row/example/notes, a new ipv6.md section on
what a container gets and the two wiring traps above, and an architecture.md
pointer.
- check-ipv6-support.sh now probes a random address anywhere inside the
candidate prefix instead of a host part of its first /64, so a delegation
shorter than /64 is actually verified (the first /64 is usually the one the
host already holds an address in, and proves nothing).
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
A prefix-mode container's /112 block index was sha256(username)[0:4], so the
suffix of its address was a global constant for a given name: "alice" got the
same block on every host running this panel. An address therefore identified
its owner to anyone who knew the scheme and could enumerate names — the
opposite of what the random container hostname is for.
The index now lives in users.ipv6_index (schema v19) and is random:
- accounts that predate the column are seeded with the value they have been
using, so no address moves and no container is touched;
- vps add picks a free index and retries internally, with the UNIQUE index on
the column as the backstop, so a name can no longer be refused for a reason
as opaque as "your address is taken" (the old checkIPv6BlockCollision). A
block containing the bridge gateway is skipped the same way.
- deleting an account and recreating the same name yields a fresh random index,
and so does a transfer: the value is per-host and stays out of the metadata.
Add launches a container before creating its account row, so within that window
the block can no longer be looked up by name: WireIPv6 and
ConfigureContainerIPv6 now take the block/address the caller already picked.
Blocks are also only meaningful in prefix mode, which the accessors now say.
Verified on a live prefix-mode host: a user created before the change kept her
address (panel and inside the container), a new user came up on a random block
end to end (guest address, ndppd rule, Incus route), deleting and recreating a
name produced a new random block, and a V4-only host still creates users with
no index at all.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
The in-tree NDP responder relies on being the only NDP proxy on the host.
main.go already disabled ndppd.service at install; npd6 (an alternative
distro NDP proxy daemon) was left running, so a host that happened to have
npd6 installed would answer Neighbor Solicitations with a competing
link-local-source NA and break the global-address fix.
- main.go: also `systemctl disable --now npd6.service` at install
- uninstall.sh: also stop/disable npd6 in the IPv6 cleanup path
- docs: note both daemons are disabled (ipv6-ndp-responder.md, ipv6.md)
- net.ipv6_pool is now operator-editable (was immutable): accept bare
addresses or explicit /128 (any other prefix length rejected), stored
normalized to the bare form.
- New admin "IPv6 Pool" page (visible in pool mode): multi-line
batch-add box, list with per-address state (free / used + owner), and
remove for free addresses (assigned ones are refused — a user keeps
its address for life).
- Adding addresses re-applies the host plumbing so any address the
provider bound on the external interface is detached immediately.
- mgr: AddPoolIPv6s / RemovePoolIPv6 / PoolList / PoolEntries with tests.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Empirically this is what makes discrete whitelist addresses work:
- The provider binds every address on the host's NIC at boot; while the
host holds an address the kernel treats it as LOCAL and drops the
container's packets using it as a source (source-address validation),
so outbound v6 silently fails. Pool mode now removes each pool address
from the external interface (vps ip6 addr-del), on add/reinstall and
every ipv6-reapply/boot — self-healing after a reboot re-binds them.
- Containers get TWO NICs: eth0 = nictype routed on the external
interface with ipv6.address=<pool /128> (Incus programs the /128 route
+ proxy_ndp + fe80::1 gateway), eth1 = bridged on incusbr0 for the
shared IPv4. systemd-networkd binds the /128 statically on eth0 and
DHCPv4s on eth1.
- The manual bridge /128-route + proxy_ndp plumbing (SetupIPv6BridgePool,
WireIPv6Pool route/neigh) is gone — Incus programs everything.
Verified end-to-end on the whitelist host: container reaches out over v6
(curl -6 returns its own /128) and v4 (eth1 NAT) simultaneously, external
Globalping hits the container /128 with 0% loss, delete releases the
address and a new container reuses it.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Pool mode adds a create-user dropdown (auto first-free / a specific free
address / no IPv6) and an IPv6 column in the user table. Docs cover the
two modes, the installer flow, and the empirically verified per-address
routing.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
The Incus line is a new project, not an upgrade path — remove every
piece of cross-version compatibility:
- version gate: ver.Blocked/MinAdoptable/SemVerLT, blockUnadoptable,
cmdNoteVersion, the installed_version/uninstalled_version config
fields and registry entries, install.sh old-binary detection and the
uninstall note-version call.
- DB migrations for pre-0.3 schemas: migrateAuditTargetDrop,
migrateUserColumns, migrateDomainColumns, migrateCPU and their tests.
- legacy admin-pass migration from config.yaml into the DB.
- duplicate static sudoers file (configs/sudoers.d/vps) — the Go
ensureSudoers embedded copy is the only source.
- docs swept of version-gate and 0.2.x/0.3.x language.
Also rename the bridge lxdbr0 -> incusbr0 across code, scripts, config
and docs, and adapt NIC isolation to Incus 7.0 validation: ipv6_filtering
is only applied when the eth0 device actually carries an IPv6 address or
route (Incus rejects it on a v6-less device, unlike LXD).
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
The panel now talks to Incus over its Unix socket REST API
(/var/lib/incus/unix.socket, group incus-admin) exclusively:
- internal/lx: exec moved from the `lxc` CLI to the API websocket
transport (Exec/ExecSH/RunInitScript); EnsureImage pulls a
remote-qualified fallback (images:debian/13) before Launch since the
API cannot auto-fetch it inside the create call; per-container df
probe in mgr/batch.go uses the same API path.
- cfg: config key renamed lxd.* -> incus.* (image, image_fallback,
pool, bridge, socket); DefaultSocket is the Debian-package path.
- mgr: container disk probe and image handling updated; comment sweep
from LXD to Incus.
- deps: add gorilla/websocket for the exec transport.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>