14 Commits
Author SHA1 Message Date
bluebluesodaandCommandCodeBot 70eac5918f feat(ipv6): opt-in permanent pin of the upstream gateway neighbour
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>
2026-10-07 13:19:40 +00:00
bluebluesodaandCommandCodeBot 02d55d2eec fix(ipv6): keep the bridge MAC stable and render one NDP rule per block
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>
2026-10-07 07:49:47 +00:00
bluebluesoda 93fa6bcef9 feat(ipv6): support whole /64 extra prefix in IPv4-only (none) mode
- 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
2026-09-22 15:02:52 +00:00
bluebluesodaandCommandCodeBot 2175b6068c fix(ipv6): deliver the /112 to the container again, and list the /64 first
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>
2026-09-21 14:18:47 +00:00
bluebluesodaandCommandCodeBot b5a9ba940f polish(ipv6): keep the plain NIC shape, fold the /64 into vps quota
- 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>
2026-09-21 08:25:02 +00:00
bluebluesodaandCommandCodeBot 8c728096cc feat(admin): expose the whole-/64 blocks in the panel
- 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>
2026-09-21 07:53:35 +00:00
bluebluesodaandCommandCodeBot b073533155 feat(ipv6): store a random block index instead of deriving it from the username
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>
2026-09-19 04:43:47 +00:00
bluebluesoda e25e729de7 fix(ipv6): disable npd6 alongside ndppd to avoid competing NAs
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)
2026-09-04 08:47:36 +00:00
bluebluesodaandCommandCodeBot c1db56312d feat(panel): editable IPv6 pool with batch-add management page
- 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>
2026-08-15 14:47:27 +00:00
bluebluesodaandCommandCodeBot 790ed11369 fix(pool): routed NIC for per-address IPv6, detach pool addrs from host
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>
2026-08-15 14:47:27 +00:00
bluebluesodaandCommandCodeBot 904e60ef7e feat(panel): IPv6 pool dropdown on create + docs
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>
2026-08-15 14:47:27 +00:00
bluebluesodaandCommandCodeBot 57f4ccb94e chore: drop upgrade/compat scaffolding for the fresh start
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>
2026-08-14 04:05:53 +00:00
bluebluesodaandCommandCodeBot 1663563020 docs: reflect the Incus runtime, vps.* services and privilege drop
- architecture.md: Incus components, unprivileged panel section
  (incus-admin group + sudoers whitelist), vps.* unit names, atomic
  user creation transaction.
- configuration.md: incus.* config keys, /var/lib/incus socket, vps
  service restart.
- development.md: Incus dev-loop instructions, updated layout.
- README(.zh-CN).md: Debian 13/Ubuntu requirements, Incus mention.
- AGENTS.md: Incus invariants (socket, websocket exec, EnsureImage
  before Launch, sudoers whitelist, vps.service naming).

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-08-14 03:33:37 +00:00
bluebluesodaandCommandCodeBot ebc2b1478f feat(incus): port the container runtime from LXD to Incus 7 LTS
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>
2026-08-14 03:33:15 +00:00