Files
vpsmgr/docs/configuration.md
T
bluebluesodaandCommandCodeBot bbc90fbf74 fix(bandwidth): name the right month in the quota label; make the reset-day change explicit
Two problems with `panel.bandwidth_reset_day`, both around its interaction
with the quota period:

- The user panel hardcoded "至次月 N 号", which is wrong whenever the reset day
  is still ahead of today: the period in force started on that day of the
  PREVIOUS month, so it ends on that day of THIS one (reset day 22 on the 16th
  ends on the 22nd of this month). mgr.BandwidthNextReset derives the real
  moment, the page flags whether it lands in this calendar month, and the label
  follows. A test also pins it against BandwidthPeriod, so the label and the
  rollover can never disagree about the boundary.

- Changing the day DISCARDS every user's accumulated bandwidth totals. That is
  not an explicit reset: the counters are keyed by a period derived from the
  day, so moving the day moves the key and the next sampler pass sees a new
  period. It stays that way (it is the period model), but it is no longer
  silent: the key is classified ApplyDestructive, so `vps config set` shows a
  confirmation naming the loss, repeats it on the --no-apply path (where the
  loss lands at the next panel restart instead), and prints it after applying.
  docs/configuration.md explains the mechanism and points at the admin panel's
  per-user "重置流量统计" button for a deliberate reset.

TestApplyBandwidthPeriodChangeDiscardsTotals pins the discard so the warning
cannot quietly become wrong, and TestBandwidthResetDayIsDestructive keeps the
classification from regressing to a plain restart.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-09-16 13:39:39 +00:00

20 KiB
Raw Blame History

Configuration

The config file is at /etc/vpsmgr/config.yaml (auto-generated at install, root-only read/write). The VPSMGR_CONFIG env var points the vps binary at another path; the shell install/uninstall scripts always use /etc/vpsmgr/config.yaml.

The installer is intended for a fresh, dedicated host unless existing services, ports, bridges, UFW, and firewall rules have been checked. For a host that must keep other public services and only needs IPv6 inbound access, install with ./install.sh --disable-v4forward; this requires an explicit confirmation, skips vpsmgr's reserved-port check, writes net.v4_forward=false, and leaves HAProxy installed but stopped.

Editing config.yaml by hand is discouraged. The sanctioned interface is vps config list / vps config set / vps config help, which validate every change and refuse immutable fields. A raw YAML edit still works but is not checked — get the rules from vps config help.

Field matrix (kind + how a change applies)

Every field is classified exactly one way — there is no ambiguity about who may change it or when the change takes effect. vps config help prints the same table; vps config list shows the live values with this annotation.

Key Kind Change applies Notes
panel.listen operator systemctl restart vps a FRESH install picks a random free port in 2000-9999
panel.cert operator restart panel TLS certificate path
panel.key operator restart panel TLS private key path
panel.db operator re-run vps install SQLite database path
panel.public_ip operator re-run vps install NIC IPv4 used by firewall/routing; cert is regenerated
panel.display_ip operator restart panel address shown to users (panel URL / SSH hints); any string without spaces (IP or domain), or empty = fall back to public_ip
panel.session_days operator restart panel login session lifetime (days)
panel.url_path fixed at install — secret prefix of the user panel; settable only while empty (re-enable)
panel.admin_url_path operator restart panel secret prefix of the admin panel; an empty value disables the admin panel (shown as disabled)
panel.show_footer operator restart panel user panel footer with project link and installed/build version; false hides the entire footer
panel.bandwidth_reset_day operator restart panel — discards stored state day of month the monthly bandwidth quota resets, 1-28 (28 keeps February safe); default 1. Changing it wipes every user's accumulated traffic totals — see below
panel.admin_pass_hash managed elsewhere — bcrypt hash of the admin password; stored in the DB, set via vps admin-passwd / web UI
net.subnet fixed at install — container subnet 10.<n>.0.0/24; changing breaks existing containers
net.gateway fixed at install — bridge gateway (derived from subnet)
net.user_ports operator next vps add / reinstall user-port ranges a new container's 100-port block is drawn from; comma-separated inclusive ranges (e.g. 10000-29999 = 200 containers, or 10000-20000, 25000-30000); auto-aligned to whole hundreds; affects new containers only — existing ones keep their ports
net.v4_forward runtime toggle applied immediately false = IPv6-only containers (no SSH/port DNAT, domain proxy disabled, NAT4 outbound kept)
net.haproxy runtime toggle applied immediately false = stop and disable HAProxy; existing domains are retained, but new domains cannot be added. Renamed from net.traefik — same values; an old key is read and rewritten under the new name
net.ext_if operator re-run vps install external NIC (auto-detected from default route)
net.ipv6_subnet operator re-run vps install global IPv6 prefix for pass-through, e.g. 2602:fada:6::/64; empty = disabled (does not remove IPv6 state already applied, see note below)
net.ipv6_mode fixed at install — IPv6 allocation mode: none / prefix (/112 blocks) / pool (per-container address)
net.ipv6_pool operator vps config set needs --apply; admin-panel changes apply immediately pool-mode address list (bare global addresses or /128); editable via the admin panel's IPv6 Pool page
incus.image operator next vps add / reinstall container image alias
incus.image_fallback operator next vps add / reinstall fallback remote image
incus.pool fixed at install — storage pool (backend selected at install via VPSMGR_STORAGE=zfs|btrfs|dir)
incus.bridge fixed at install — managed bridge
incus.socket operator restart panel Incus daemon Unix socket
incus.swap_ratio operator applied immediately swap granted to each container as a multiple of its memory limit (limits.memory.swap = limits.memory × ratio); 0 disables container swap. Setting it re-applies the allowance to all existing containers (no restart)
snapshots.limit operator restart panel max checkpoints a user may keep per container (0 = disable new snapshots). Restoring to an older checkpoint auto-deletes the ones created after it (see below)
snapshots.share operator applied immediately allow users to share a checkpoint behind a code and install from it. Off by default; CLI-only: vps config set snapshots.share true. Stored codes survive while it is off

Settings that are NOT in this file

The dynamic CPU limit rule is edited on the admin overview and stored in the DB: it has no config.yaml key and no vps config key, because it is tuned repeatedly and must apply immediately. Saving applies it on the spot (turning it off restores every capped container), and vps install never resets it.

How "fixed at install" is enforced

Fields marked fixed at install (net.subnet, net.gateway, incus.pool, incus.bridge, panel.url_path, net.ipv6_mode) are snapshotted into the DB settings table on the first vps install. Every later vps install and vps serve compares the live config against that snapshot and refuses to run if any of them drifted — vps config set also refuses them up front. The user panel path (panel.url_path) is immutable on purpose: moving it strands every user who bookmarked it. The admin path is NOT immutable — it can be rotated and emptied to disable the admin panel.

Managed files

Files generated by the panel carry a Managed by vpsmgr — generated, do not edit by hand banner and are overwritten on the next write:

File Written by
/etc/vpsmgr/nftables.conf vps install
/etc/vpsmgr/nftables.d/user-<name>.nft vps add / quota / firewall updates
/etc/haproxy/haproxy.cfg install (from configs/haproxy/haproxy.cfg); static entry config, never rewritten by the panel
/etc/haproxy/releases/<gen>/{domains.cfg,maps/*.map,manifest} domain add/update/delete (a new immutable generation)
/etc/haproxy/current domain add/update/delete (symlink to the generation in service)
/etc/ndppd.conf IPv6 pass-through updates
/etc/sysctl.d/99-vpsmgr.conf vps install
systemd units (vps/vps-nft/vps-ipv6, haproxy) vps install

Example

panel:
  listen: ":5231"              # panel listen address — a FRESH install picks a random free port in 2000-9999
  cert: /etc/vpsmgr/panel.crt  # HTTPS certificate
  key: /etc/vpsmgr/panel.key   # private key
  db: /etc/vpsmgr/vpsmgr.db    # SQLite database
  public_ip: AUTO              # NIC IPv4 used by the firewall / routing; on NAT-ing clouds (AWS/Alibaba) this is a private address
  display_ip: AUTO             # address shown to users (panel URL, SSH hints); any string without spaces (IP or domain); auto-fetched from ipv4.ip.sb when public_ip is private; empty = fall back to public_ip
  session_days: 3              # login session lifetime (days)
  bandwidth_reset_day: 1       # day of month the monthly bandwidth quota resets (1-28).
                               # CHANGING IT WIPES every user's accumulated traffic totals.
                               # `vps config set` asks for confirmation.
  url_path: AUTO               # random secret path, the only panel entrance; do not change after first install
  admin_url_path: AUTO         # random secret path of the admin panel; do not change after first install
  admin_pass_hash: AUTO        # bcrypt hash of the admin password — stored in the DB, not in this file

net:
  subnet: "10.115.0.0/24"      # container subnet 10.<n>.0.0/24 — only the second octet is settable, at install
  gateway: "10.115.0.1"
  v4_forward: true             # IPv4 inbound policy (false = IPv6-only containers)
  haproxy: true                # domain reverse proxy (false = stopped and not enabled at boot);
                               # renamed from `traefik` — a pre-rename key is adopted as-is
  ext_if: AUTO                 # external NIC, auto-detected from the default route
  ipv6_mode: ""                # IPv6 allocation mode: none / prefix / pool (fixed at install)
  ipv6_subnet: ""              # optional: global prefix for IPv6 pass-through, e.g. "2602:fada:6::/64"
                               # (/64 or shorter uses the first /64; provider /80 slices like
                               # "2406:da14:1dd2:a807:753a::/80"); empty = disabled (default),
                               # but does not remove IPv6 state already applied
  ipv6_pool: []                # optional (pool mode): the /128 addresses containers are
                               # assigned from, e.g. "2001:db8:1::9c4" (bare global
                               # addresses, no prefix length)

incus:
  image: "vpsmgr/debian-sshd"
  image_fallback: "images:debian/13"
  pool: vpsmgr
  bridge: incusbr0
  socket: "/var/lib/incus/unix.socket"   # Incus daemon Unix socket (REST API)
  swap_ratio: 0.5                        # swap per container as a multiple of its
                                         # memory limit (0 = no swap; 0.5 = a 1 GiB
                                         # container may use 512 MiB host swap);
                                         # applied to all containers immediately on set

snapshots:
  limit: 1                                # 0 disables new snapshots
  share: false                            # allow checkpoint share codes (opt-in:
                                          # install from someone's checkpoint)

Port scheme

The port layout per container is fixed at install and not individually tunable, but the span new containers may use follows net.user_ports:

  • Panel port: random free port in 2000-9999, chosen on a fresh install and stored in panel.listen. Change it in the config only if you know why.
  • SSH port: each container gets one random port in 30000-31999 (TCP, DNAT to container :22). Independent of the user-port ranges. Not reserved at install — each port is probed at vps add time, and a candidate already listened on by another process is skipped for another. Shown as ssh -p <port>.
  • User ports: each container owns a whole-hundred block of 100 ports, drawn at create time from the configured net.user_ports ranges, DNAT to the container (TCP and UDP). Displayed compactly as e.g. 107xx.

User-port ranges (net.user_ports)

net.user_ports names the port ranges a new container's 100-port block may be drawn from, e.g. 10000-29999 (full) or 10000-20000, 25000-30000 (leave a gap for the host). Set it at install (00-ip-ask.sh asks right after the subnet octet) or change it later:

vps config set net.user_ports 10000-20000, 25000-30000

Rules:

  • Comma-separated inclusive ranges A-B (integers, A <= B). Ends may extend outside 10000-29999 — only the overlap with the usable domain counts, and a fully-outside range contributes nothing.
  • Values are auto-aligned to whole hundreds: the low end rounds up to a block start and the high end rounds down then extends to +99, so the effective range always ends in ...99, never ...00. For example 10001-29998 becomes 10100-29999.
  • At least one whole 100-port block must remain (the value is rejected otherwise) — a range that cannot host even one container is an error.
  • Capacity follows the ranges: 10000-29999 allows 200 containers; 10000-20000, 25000-30000 allows 151; and so on. The interactive prompt echoes the computed capacity for confirmation.
  • A narrowed set reserves far fewer host user ports, so an install's port-occupancy check scans only the configured ranges — not the whole 10000-29999. 80/443 is reserved regardless; SSH ports (30000-31999) are not checked at install — each is probed at vps add time and the allocator reassigns on conflict (a live listener on the candidate is skipped).
  • It affects new containers only: editing the ranges never renumbers or removes an existing container (one whose block falls outside the current ranges keeps it). The admin panel's capacity readout reflects the ranges.

Legacy: configs written before net.user_ports used net.slot_range (a range of v4 last octets, e.g. 2-201). On upgrade the installer converts it once to the equivalent user_ports value and writes the new key; the old key stays in the file but is ignored and is no longer an editable config key.

IPv4 inbound policy (v4_forward)

net.v4_forward controls whether containers receive shared IPv4 inbound. Always enabled by default — the installer does not ask (with IPv6 off, IPv4 forwarding is mandatory, as containers would otherwise be unreachable).

  • true (default): containers get the random SSH port + user port block (DNAT), and the domain proxy (HAProxy) is available.
  • false: containers are IPv6-only. No SSH DNAT, no port-block DNAT, and HAProxy is stopped (domains are kept but not served; adding a domain is rejected until re-enabled). Containers still reach IPv4 outbound via the NAT4 masquerade.

Toggle at runtime with vps config set net.v4_forward true|false — the rules are refreshed and HAProxy started/stopped immediately (its boot autostart is disabled along with it, so it cannot come back on reboot). The SSH/user ports stay recorded in the DB, so turning it back on restores everything. The user panel hides IPv4 inbound info and shows "v4 SSH unavailable" while off, and domain-add is blocked while off — the add form is hidden and the handler rejects it (the panel reads the toggle live from the DB, no restart needed).

Domain proxy (net.haproxy)

net.haproxy independently controls the HAProxy domain reverse proxy. It is enabled by default. Set it with vps config set net.haproxy false to stop HAProxy and disable its boot autostart. Existing domain records and the published configuration are kept, but users cannot add new domains while it is disabled. Re-enabling the setting starts HAProxy, restores autostart, and republishes the configuration from the database.

Upgrading from a Traefik-era install. This key used to be called net.traefik. The values are identical (true = running and enabled at boot, false = stopped and disabled), and the rename is handled automatically:

  • a config.yaml carrying traefik: is read as net.haproxy (the new key wins if both are somehow present), and the next save rewrites only the new key;
  • the DB runtime mirror (settings.traefik) is moved to settings.haproxy by schema migration v18, keeping the stored value;
  • the installer still honors VPSMGR_TRAEFIK as a fallback for VPSMGR_HAPROXY (an older checkout exports the old name);
  • install.sh removes the Traefik program, unit and service account and republishes the domains onto HAProxy, keeping only /etc/traefik/traefik.yaml (inert).

There is no rollback to Traefik: HAProxy is the only proxy the panel knows.

Install-time auto-off: if port 80 or 443 is already bound by a non-vpsmgr process during install.sh, the installer does not fail. Instead it proceeds with HAProxy installed but net.haproxy: false (not started, no boot autostart), so a host that already serves 80/443 stays usable. The 00-check.sh port-occupancy scan reports it and installs the proxy disabled; re-enable later with vps config set net.haproxy true.

HAProxy layout and version

The proxy itself is HAProxy 3.4 LTS, installed from the official HAProxy performance packages (haproxy-awslc) for the host's distribution. The panel pins the branch (3.4), not the patch: install.sh and upgrade-haproxy.sh keep the newest patch of that branch, and install.sh --update moves the panel and the proxy together. A branch change is a deliberate migration — update HAPROXY_BRANCH / HAPROXY_REPO_SLUG / HAPROXY_BRANCH_RE in scripts/30-haproxy.sh and upgrade-haproxy.sh, plus cfg.HaproxyBranch, then re-run install.sh.

Path Written by
/etc/haproxy/haproxy.cfg install (static entry config; the unit loads it first and current/domains.cfg second)
/etc/haproxy/current panel — symlink to the generation in service
/etc/haproxy/releases/<gen>/ panel — one immutable generation (three kept)
/etc/haproxy/generation panel — counter of the last published generation
/etc/systemd/system/haproxy.service install (overrides the distribution's unit)

Monthly bandwidth reset day (panel.bandwidth_reset_day)

The quota period runs from the configured day of one month to that day of the next (default 1 = calendar months). The user panel shows which reset applies to the period in force, including which month it falls in — with the reset day set to the 22nd and today the 16th, the period ends on the 22nd of this month, because it started on the 22nd of the previous one.

Changing this setting discards every user's accumulated bandwidth totals. The accumulated counters are keyed by an accounting period derived from this day, so moving the day moves the period key, and the next sampler pass sees "a new period" and starts from zero. Any over-quota throttle is lifted as a side effect, and past totals are not recoverable — setting the day back does not bring them back.

This is why the key is classified as destructive: vps config set panel.bandwidth_reset_day <n> asks for an explicit confirmation describing the loss (and repeats it when the change is saved with --no-apply, since then it lands at the next panel restart instead). To reset ONE user's traffic on purpose, use the admin panel's "重置流量统计" button instead — that is the supported, per-user operation.

Port 25 (SMTP) is always blocked

The host nftables ruleset drops port 25 for all forwarded traffic, both directions, TCP and UDP (containers ⇄ internet, container ⇄ container). This is a permanent anti-spam measure: there is no user toggle, and the rule only goes away when the panel is uninstalled.

Field notes

  • panel.listen, panel.public_ip, net.ext_if only affect display/on how the panel listens.
  • url_path, net.subnet, incus.pool etc. are bound to existing data — do not change them after first install.
  • net.ipv6_subnet must be a global (non-ULA) IPv6 CIDR with an explicit prefix length — a bare address is rejected, never silently assumed /64. The validator accepts a global prefix of /80 or shorter; /48../80 are the documented operational range (see ipv6.md).
  • net.ipv6_mode is fixed at install and immutable (like net.subnet): none (pure IPv4), prefix (classic /112 blocks), pool (per-container address from net.ipv6_pool). Switching modes would renumber containers.
  • Clearing net.ipv6_subnet (empty) only stops vpsmgr from applying IPv6 on the next vps install; it does not remove IPv6 already in place (bridge address, ndppd, routes). Full IPv6 cleanup happens in uninstall.sh.