10 Commits
Author SHA1 Message Date
bluebluesoda cf5c6af181 feat(panel): make the panel name configurable (panel.title)
- the login page heading and the panel header (and their <title>) now use the
  configured name instead of the hard-coded "VPS Manager"
- blank falls back to "VPS Manager", so existing config files are unchanged;
  clearing the key resets it
- registry validation keeps it a single line of at most 60 characters; applied
  on panel restart like the other panel.* display settings
- documented in the config example and the configuration reference
2026-09-27 12:27:41 +00:00
bluebluesodaandCommandCodeBot 02e590c974 fix(install): make oneclick.sh work via bash <(curl ...)
When piped through /dev/fd the script's $0 is not a filesystem path, so
sourcing scripts/lib-download.sh failed ("... /dev/fd/scripts/lib-download.sh:
No such file or directory"). Inline dl_content() instead so the gitless
bootstrap can still retry the repo tarball download.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-09-24 13:21:26 +00:00
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
bluebluesoda 69f79187c8 feat(ports): replace net.slot_range with net.user_ports; probe SSH ports at add time
net.slot_range required operators to reason about IPv4 last-octet slots to
control the host's reserved port span. Replace it with net.user_ports, a
direct comma-separated set of user-port ranges (default 10000-29999) that a
new container's whole-hundred block is drawn from, auto-aligned to whole
hundreds and merged. Only-new-users effect; existing containers keep their
ports.

- cfg: ParseUserPorts/CanonicalUserPorts/UserPortRanges/UserPortCount/
  UserPortBlockStarts; legacy slot_range is migrated once on Load and the
  old key stays in the file, ignored, no longer editable.
- db: migration v13 adds a UNIQUE index on users.start_port (port blocks are
  no longer derived from idx, so uniqueness must be enforced directly);
  UsedStartPorts() feeds the allocator.
- mgr: port blocks are allocated independently of the IPv4 idx via
  allocUserPortBlock; admin capacity readout follows UserPortCount.
- installer: 00-ip-ask.sh asks for port ranges (default = no confirm, a
  changed value echoes capacity and asks Y/n) and converts legacy slot_range;
  00-check.sh scans only the effective user-port ranges.
- ssh: allocSSHPort now probes the host (bind test) before picking a port,
  reassigning on conflict; the install no longer hard-fails on an occupied
  30000-31999 port.
2026-09-04 08:32:13 +00: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 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
bluebluesodaandCommandCodeBot 76e99e0fcf feat(config): allow any space-free string for panel.display_ip
display_ip is purely cosmetic (panel URL, SSH hints) and never used in
rules, so accept IPv6 addresses, domain names, or any string without
spaces instead of requiring a parseable IP.

- registry: validate only that the value contains no spaces
- tests: domain accepted, space-containing value rejected
- docs/examples: drop the "public IPv4" wording

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-08-20 16:05:19 +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