fix(ports): align user port upper bound strictly inward to block end <= b

The previous normalization logic aligned the upper bound using (b // 100) * 100 + 99,
which rounded down to a block start and then extended to +99. For any input where
the upper bound did not already end in 99 (such as 10000-15000 or 10000-20000),
this spilled over by (99 - (b % 100)) ports past the user's requested range
(e.g. 10000-15000 became 10000-15099).

Ports outside the user's input may be in use or unavailable. Align the upper
bound strictly inward so each allocated block is a subset of the input:

- calculate hi as ((b + 1) / PortsPerUser) * PortsPerUser - 1 (ending in ...99 <= b)
- mirror the calculation in scripts/00-ip-ask.sh normalize_user_ports
- update unit tests in internal/cfg to verify strict inward alignment
- update docs/configuration.md examples and container capacity readouts
This commit is contained in:
bluebluesoda committed 2026-09-22 13:31:49 +00:00
1 parent 8d9156b333
commit 1fdcf3f37b
4 files changed
+26 -24

No files matched your search

+5 -4
View File
@@ -179,13 +179,14 @@ Rules:
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`.
block start and the high end rounds down to a block end (`...99 <= B`), so the
effective range always ends in `...99`, never `...00`, and never spills past
the requested upper bound. For example `10001-29998` becomes `10100-29899`,
and `10000-15000` becomes `10000-14999`.
- 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
`10000-20000, 25000-30000` allows **150**; 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
+3 -3
View File
@@ -315,8 +315,8 @@ PY
# comma-separated) and the container capacity on one line ("canon|cap"). Exit 0
# only when the value yields at least one usable 100-port block. Mirrors
# cfg.ParseUserPorts: low end rounds up to a block start, high end rounds down
# then +99 (never ends in ...00); ranges outside 10000-29999 are clamped, and
# fully-outside ranges contribute nothing.
# to a block end <= b (never ends in ...00, never exceeds user's b); ranges
# outside 10000-29999 are clamped, and fully-outside ranges contribute nothing.
normalize_user_ports(){
python3 - "$1" <<'PY'
import sys
@@ -337,7 +337,7 @@ def norm(s):
if a > b:
continue
lo = ((a + 99) // 100) * 100
hi = (b // 100) * 100 + 99
hi = ((b + 1) // 100) * 100 - 1
if lo > hi:
continue
out.append([lo, hi])
+5 -6
View File
@@ -386,9 +386,8 @@ func LegacySlotRangeToUserPorts(slotRange string) (string, error) {
// - Lo/hi may extend outside [UserPortBase, UserPortMax] — only the overlap
// with the usable domain counts (a range fully outside contributes nothing).
// - Each range is aligned inward to whole hundreds: the low end rounds UP to
// a block start, the high end rounds DOWN to a block start then extends to
// +99 (so the effective range always ends in ...99, never ...00). A range
// narrower than one block is dropped.
// a block start, the high end rounds DOWN to a block end (never exceeding
// b, ending in ...99). A range narrower than one block is dropped.
// - Overlapping/adjacent ranges are merged, so capacity is never double-counted.
// - At least one usable 100-port block must remain, or the value is rejected
// (a range that cannot host even one container is an error).
@@ -421,9 +420,9 @@ func ParseUserPorts(s string) ([]PortRange, error) {
if a > b {
continue
}
// Align inward: lo up to a block start, hi down to a block start then +99.
// Align inward: lo rounds UP to a block start, hi rounds DOWN to a block end <= b.
lo := ((a + PortsPerUser - 1) / PortsPerUser) * PortsPerUser
hi := (b/PortsPerUser)*PortsPerUser + PortsPerUser - 1
hi := ((b + 1) / PortsPerUser) * PortsPerUser - 1
if lo > hi {
continue // narrower than one block
}
@@ -448,7 +447,7 @@ func ParseUserPorts(s string) ([]PortRange, error) {
}
// CanonicalUserPorts renders parsed ranges as their canonical comma-separated
// string form ("10000-20099, 25000-29999"). Returns the raw input unchanged if
// string form ("10000-19999, 25000-29999"). Returns the raw input unchanged if
// it does not parse.
func CanonicalUserPorts(s string) string {
rs, err := ParseUserPorts(s)
+13 -11
View File
@@ -266,17 +266,19 @@ func TestParseUserPorts(t *testing.T) {
canon string // canonical form; "" = must be rejected
}{
{"10000-29999", 200, "10000-29999"}, // default full range
{"10000-20000", 101, "10000-20099"}, // hi rounds down then +99 (never ends in 00)
{"10001-29998", 199, "10100-29999"}, // inward-aligned both ends
{"10000-20000", 100, "10000-19999"}, // hi rounds down to block end <= b (never exceeds b)
{"10000-15000", 50, "10000-14999"}, // boundary test: 15000 rounds down to 14999, not 15099
{"10001-29998", 198, "10100-29899"}, // inward-aligned both ends (29998 rounds down to 29899)
{"10001-29999", 199, "10100-29999"}, // inward-aligned low end only
{"10001-10099", 0, ""}, // narrower than one block
{"10000-10099", 1, "10000-10099"}, // exactly one block
{"10000-20000, 25000-30000", 151, "10000-20099, 25000-29999"}, // discontiguous
{"10000-15000, 14000-20000", 101, "10000-20099"}, // overlapping → merged
{"5000-15000", 51, "10000-15099"}, // lo below domain clamps to 10000
{"10000-20000, 25000-30000", 150, "10000-19999, 25000-29999"}, // discontiguous
{"10000-15000, 14000-20000", 100, "10000-19999"}, // overlapping → merged
{"5000-15000", 50, "10000-14999"}, // lo below domain clamps to 10000
{"20000-40000", 100, "20000-29999"}, // hi above domain clamps to 29999
{"10000-10000", 1, "10000-10099"}, // single port still yields its block
{"10000-10000", 0, ""}, // single port has no whole block
{"5000-6000", 0, ""}, // fully outside domain
{"10000-20000, 21000-21099", 102, "10000-20099, 21000-21099"}, // adjacent not merged (gap 100)
{"10000-20000, 21000-21099", 101, "10000-19999, 21000-21099"}, // adjacent not merged (gap 100)
}
for _, c := range cases {
rs, err := ParseUserPorts(c.in)
@@ -329,8 +331,8 @@ func TestUserPortCount(t *testing.T) {
t.Errorf("default UserPortCount() = %d, want 200", n)
}
cfg.Net.UserPorts = "10000-20000, 25000-30000"
if n := cfg.UserPortCount(); n != 151 {
t.Errorf("UserPortCount() = %d, want 151", n)
if n := cfg.UserPortCount(); n != 150 {
t.Errorf("UserPortCount() = %d, want 150", n)
}
cfg.Net.UserPorts = "garbage"
if n := cfg.UserPortCount(); n != 200 {
@@ -346,8 +348,8 @@ func TestFillAutoUserPortsEnv(t *testing.T) {
if err := c.FillAuto(); err != nil {
t.Fatalf("FillAuto: %v", err)
}
if c.Net.UserPorts != "10000-20099, 25000-29999" {
t.Errorf("user_ports = %q, want canonical 10000-20099, 25000-29999", c.Net.UserPorts)
if c.Net.UserPorts != "10000-19999, 25000-29999" {
t.Errorf("user_ports = %q, want canonical 10000-19999, 25000-29999", c.Net.UserPorts)
}
// A bad env value must fail loudly, not silently default.