The panel now fronts user domains with HAProxy 3.4 LTS (official performance
packages) instead of Traefik: about 100 MB of RSS and 200 MB of disk saved for
a feature set that never used more than Host routing on :80 and SNI passthrough
on :443.
- hpx: publish the proxy configuration as immutable GENERATIONS. Every domain
mutation renders the whole set from the DB, validates it with the pinned
`haproxy -c`, and only then repoints the `current` symlink, so a rejected
candidate leaves the running proxy untouched and a stale domain can never
survive into a later generation. Routing is carried by two map files
(Host -> backend, SNI -> backend): per-domain frontend rules cannot work from
a second file because HAProxy rejects duplicate section names since 3.3 and
attaches bare directives to the section opened last.
- mgr: republish and reload (SIGUSR2, i.e. `systemctl reload`) after every
domain change, so a change takes effect without dropping established HTTP or
TLS connections. Rollback semantics (DB first, then publish) are unchanged.
- cfg,db: `net.traefik` is renamed to `net.haproxy` with identical values. A
pre-rename config.yaml key and the legacy VPSMGR_TRAEFIK env var are still
read (new name wins), and schema migration v18 moves the settings mirror from
`traefik` to `haproxy`, keeping a newer value when both rows exist.
- 30-haproxy.sh: install the pinned branch from the vendor repository and
migrate a Traefik host in one run — install and validate HAProxy first, then
remove Traefik's binary, unit, service account and dynamic rules (only its
config file is kept, inert). No fallback keeps both proxies alive.
- upgrade-haproxy.sh: follow the pinned branch to its newest patch, with a
configuration check and a graceful reload; `install.sh --update` runs it.
- tests: generation publish/rollback, map rendering, config-key and settings
migrations, plus real-binary tests (skipped unless
VPSMGR_TEST_HAPROXY_BIN is set) that run the generated configuration through
`haproxy -c` and start it in a network namespace.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Reinstall/upgrade with incus.image set to anything other than the
default vpsmgr/debian-sshd now skips the Debian 13 sshd image build —
a custom image operator does not launch the stock one, so pulling
Debian 13 and rebuilding it is wasted work (even when the default
image is missing).
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
A plain ./install.sh downloads the prebuilt RELEASE binary, which always
comes from main regardless of the checked-out branch. If this is a real
git checkout on a non-main branch, prompt for confirmation before
continuing. Tarball / one-click pipe installs have no .git directory and
skip the check. The branch is read directly from .git/HEAD so git(1) is
not required; a detached HEAD is treated as non-main.
A plain ./install.sh downloads the prebuilt RELEASE binary, which always
comes from main regardless of the checked-out branch. If this is a real
git checkout on a non-main branch, prompt for confirmation before
continuing. Tarball / one-click pipe installs have no .git directory and
skip the check. The branch is read directly from .git/HEAD so git(1) is
not required; a detached HEAD is treated as non-main.
Some upstream IPv6 providers reject NDP replies whose source is a
link-local address (what ndppd and Linux proxy_ndp emit) and require the
advertised global address. Replace the distro ndppd prefix-mode data
plane with an in-tree raw AF_PACKET responder whose NA source is the
target's global address, re-reading a panel-writable rules file while it
runs so vps add/del never restarts it.
- add internal/ndp: minimal NDP responder (NS->NA, DAD handling, correct
ICMPv6 checksum/hop-limit/flags); only answers for target blocks inside
the operator's routed prefix (confines the root responder's trust surface)
- main: ipv6-proxy unit (prefix mode) + ipv6-proxy command + ethernetMAC
pre-check at install so a broken ext_if never hot-loops under Restart=always
- ipv6.go: write ndppd.conf atomically (temp+rename), drop all ndppd daemon
management and its sudoers grants
- mgr/routed_ipv6.go: configure guest v6 before publishing the NDP rule;
pin fe80::1 gateway neighbor, UseGateway=false, local /112 route +
vpsmgr-ipv6.service helper in the guest
- 50-image.sh/80-debian-dev-image.sh: bake UseGateway=false into [IPv6AcceptRA]
- VPSMGR_STORAGE now accepts btrfs alongside zfs and dir. On a btrfs root the
pool is created as a native subvolume at /var/lib/incus/storage-pools/vpsmgr
(Incus only allows that source path and requires it NOT to pre-exist); on any
other host it becomes a sparse loop-file pool sized exactly like ZFS.
- install.sh asks for an explicit beta confirmation up front when the btrfs
backend is selected (default yes — on a btrfs host there is no ZFS fallback).
Non-interactive installs proceed; non-btrfs/default-zfs installs are
unchanged.
- btrfs swap: on a btrfs root the swapfile is created with
`btrfs filesystem mkswapfile` (btrfs-progs >= 6.1), falling back to
truncate + chattr +C + dd for older tools — the generic fallocate+mkswap path
fails on btrfs (swap must be NODATACOW and preallocated). Non-btrfs hosts
keep the existing fallocate path.
- 00-check.sh installs btrfs-progs and skips the ZFS DKMS/kernel-header
toolchain for btrfs and dir alike; 10-incus.sh verifies the final driver.
- Panel Go code is untouched: container management is storage-driver-
transparent, and lx.EnsureZFSRemoveSnapshots is a no-op off ZFS.
- Docs (architecture, configuration, READMEs, AGENTS) updated for the new
backend, including snapshot-restore semantics (btrfs keeps later snapshots).
Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
Add net.slot_range, a shrinkable container slot range (read as the IPv4
last octet a new container may take; idx = octet-1) that bounds how many
containers can be created and, consequently, how much of the user-port
block 10000-29999 a host reserves. Default 2-201 = 200 slots; must always
be a sub-range of 2-201 (only-shrink rule). Affects ONLY new containers —
existing ones keep their slot/ports.
- cfg: add net.slot_range with strict ParseSlotRange validation
(exactly one '-', both ends integers in 2..201, lo<=hi), SlotCount/
SlotIdxBounds helpers, and VPSMGR_SLOT_RANGE env handling in FillAuto.
Register net.slot_range as operator / next-add in the config registry.
- db/mgr: NextFreeIdx now picks from a caller-supplied [idxMin,idxMax]
window instead of the fixed 1..cfg.MaxUsers; mgr.Add feeds it from the
configured range. Existing users outside a narrowed range keep their
slots.
- admin panel: capacity readout follows SlotCount() instead of a fixed
200 (honestly shows over-capacity when pre-existing users fall outside
a shrunken range).
- installer: 00-checklist asks the slot range right after the subnet
octet (env/adoption/non-interactive branches, validated). The
port-occupancy check now scans only the range's user-port span rather
than the whole 10000-29999.
- 80/443 handling: if either is already bound by a non-vpsmgr process,
the install no longer fails — it proceeds with Traefik installed but
DISABLED (net.traefik false, no autostart). 00-check writes a marker
that install.sh turns into VPSMGR_TRAEFIK=0 for 30-traefik and the
panel. A belt-and-suspenders guard skips the force-off when our own
traefik.service is already active (update/adoption). SSH 30000-31999
and the user-port span remain hard failures.
- traefik: bump pinned version to 3.7.12 and rewrite 30-traefik's
enable/disable decision to respect the effective net.traefik (forced
env > adopted config > default), so a disabled traefik is never
briefly started.
- docs: README(s), config example, architecture and configuration docs
updated; tests added for slot-range parsing and the FillAuto env vars.
Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
- prompt now shows [<RAM> GiB/y/N] with the auto size computed from
host memory; answering y creates a swap file of RAM × 1:1
- custom sizes in GiB are still accepted, N/Enter aborts as before
Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
Move the swap decision to the start of install.sh (before 00-ip-ask).
Swap is now mandatory: if absent, ask the user for a size in GiB
(default decline aborts with a bilingual note pointing at swap files
or systemd-zram-generator). The old optional swap block in
00-check.sh is removed.
- install.sh accepts --update (alongside --local-build): it forces a
fresh download of the latest prebuilt release over an existing binary.
A plain re-run still skips an installed binary (idempotent adopt).
- Conservative by design: on download/checksum failure the installed
binary is kept and a warning is printed — never a surprise local
rebuild over an unknown checkout.
- install_prebuilt now stops vps.service before replacing the binary
(ETXTBSY on a live panel), and only after the download has verified,
so a failed download never takes the panel down.
- docs: AGENTS.md invariant, architecture.md install lifecycle,
README.zh-CN usage line.
- New installs use a dir storage pool when VPSMGR_STORAGE=dir (default
zfs). A failed ZFS pool still aborts — the switch is explicit, never
an automatic fallback.
- 00-check.sh skips zfsutils-linux, the DKMS module build and the zfs
boot units entirely in dir mode (no kernel module needed).
- 10-incus.sh creates/adopts a dir pool without requiring zpool; zfs
mode still refuses any non-ZFS pool, fresh or adopted.
- uninstall.sh is unchanged: incus storage delete handles both drivers
and the zpool destroy is already guarded by 'zpool list'.
- docs: architecture.md and AGENTS.md document the switch.
00-ip-ask.sh now runs the unchanged check-ipv6-support.sh. Whole prefix
verified -> classic prefix mode. Not verified but >1 global address on
the NIC -> list them, keep the first for the host, ask the user to
confirm the rest as the pool. Declined or no addresses -> pure IPv4.
ndppd is only installed in prefix mode; pool mode uses kernel proxy_ndp.
vps ip6 gains neigh-add-proxy for the per-address wiring.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Security review finding P2-9 + the extra requirement in the review doc:
- P2-9: fixed /tmp paths were used by install steps — a local attacker
could pre-create the file (symlink follow on cat/tee/curl -o) and
/tmp/vpsmgr-install.out carried the one-time admin password in a world
guessable location. 10-incus.sh preseed now uses mktemp; 30-traefik.sh
downloads into a private mktemp dir with an EXIT trap; 40-panel.sh
writes the install transcript to /etc/vpsmgr/.last-install.out (0600,
root-only) instead of /tmp, and install.sh reads it from there.
- Extra: before sizing a loop-file zpool, 10-incus.sh now reclaims space
when / has less than 15 GiB free: apt clean, apt list purge, autoremove,
journal vacuum (3d), /tmp + /var/tmp, stale kernels (keep 2 newest,
linux-image-* only), pip/go build caches — then re-checks free space and
refuses to build a pool that would leave < 3 GiB free worst-case.
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>
- 10-incus.sh replaces 10-lxd.sh: adds the Zabbly incus lts-7.0 apt
repo (key fingerprint verified), installs the incus package, creates
the vpsmgr pool (ZFS/spare/loop-file, dir fallback) and lxdbr0 via
`incus admin init --preseed`, and keeps dns.mode=none.
- 00-check.sh targets Debian 12/13 + Ubuntu 22.04/24.04/26.04, drops
snapd, checks incus instead of the lxd snap.
- 50/60-image scripts use `incus` (was `lxc`).
- Service naming unified to vps.*: vps.service (panel),
vps-nft.service, vps-ipv6.service. `systemctl restart vps` is now the
one command operators need. unit After= incus.service.
- uninstall.sh stops vps.* units and removes the Incus package on
--purge.
- sudoers whitelist for the unprivileged panel (configs/sudoers.d/vps).
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>