19 Commits
Author SHA1 Message Date
bluebluesodaandCommandCodeBot 4c93ae4be3 feat(haproxy): replace Traefik with a pinned HAProxy domain proxy
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>
2026-09-16 12:59:13 +00:00
bluebluesodaandCommandCodeBot 48d8cafe2f fix(install): skip 50-image when incus.image is a custom alias
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>
2026-09-04 13:33:12 +00:00
bluebluesoda c9b18ffee5 Merge remote-tracking branch 'origin/dev-ndp'
# Conflicts:
#	src/internal/admin/handlers.go
2026-09-04 08:33:56 +00:00
bluebluesoda 888cf09a95 Update install.sh 2026-09-02 13:02:13 +08:00
bluebluesoda 430d6f77c4 feat(install): warn on a release install from a non-main branch
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.
2026-08-31 23:53:01 +08:00
bluebluesoda 5f154728af feat(install): warn on a release install from a non-main branch
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.
2026-08-31 23:52:51 +08:00
bluebluesoda 062cf9c4f5 feat(ipv6): in-tree NDP responder for global-address NA source
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]
2026-08-31 17:45:24 +08:00
bluebluesoda f79ccb09f0 feat(install): add btrfs storage backend (native subvolume or loop file)
- 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>
2026-08-31 17:00:03 +08:00
bluebluesoda fa151eaee4 feat(install): configurable slot range + auto-off Traefik on 80/443 conflict
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>
2026-08-30 09:36:29 +00:00
bluebluesoda 25758a6590 feat(install): swap prompt offers 1:1 RAM size via y
- 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>
2026-08-30 08:30:53 +00:00
bluebluesoda 97396589f5 feat(install): support disabling IPv4 forwarding
- add --disable-v4forward with explicit confirmation\n- skip reserved-port checks and keep Traefik stopped for IPv6-only installs\n- document dedicated-host requirements and sync config references
2026-08-28 15:07:24 +00:00
bluebluesoda 6d897cc1bb feat(install): enforce swap up front, before IPv6 prompts
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.
2026-08-28 12:36:53 +00:00
bluebluesoda 8ab4d396d9 feat(install): add --update to force prebuilt binary refresh
- 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.
2026-08-16 05:29:46 +00:00
bluebluesoda fb3f8b7cfc feat(install): add VPSMGR_STORAGE=dir opt-in for test boxes
- 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.
2026-08-15 17:33:53 +00:00
bluebluesodaandCommandCodeBot 5a0494df3c feat(install): probe-then-choose IPv6 mode (prefix / pool / v4)
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>
2026-08-15 14:47:27 +00:00
bluebluesodaandCommandCodeBot c9b7e20c85 fix(scripts): private scratch paths; reclaim space before zpool sizing
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>
2026-08-14 08:21:21 +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 c0faea6c8c feat(scripts): install Incus via Zabbly LTS repo; unify services as vps.*
- 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>
2026-08-14 03:33:30 +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