banger/internal/model/vm_name_test.go
Thales Maciel caa6a2b996
model: validate VM names as DNS labels at CLI + daemon
A VM name flows into five places that all have narrower grammars
than "arbitrary string":

  - the guest's /etc/hostname  (vm_disk.patchRootOverlay)
  - the guest's /etc/hosts      (same)
  - the <name>.vm DNS record    (vmdns.RecordName)
  - the kernel command line     (system.BuildBootArgs*)
  - VM-dir file-path fragments  (layout.VMsDir/<id>, etc.)

Nothing in the chain was validating the input. A name with
whitespace, newline, dot, slash, colon, or = would produce broken
hostnames, weird DNS labels, smuggled kernel cmdline tokens, or
(in the worst case) surprising traversal through the on-disk
layout. Not host shell injection — we already avoid shelling out
with the raw name — but a real correctness and supportability bug.

New: model.ValidateVMName. Rules:

  - 1..63 chars (DNS label max per RFC 1123; also a comfortable
    /etc/hostname cap)
  - lowercase ASCII letters, digits, '-' only
  - no leading or trailing '-'
  - no normalization — the name is the user-visible identifier
    (store key, `ssh <name>.vm`, `vm show`); silently rewriting
    "MyVM" → "myvm" would hand the user back something different
    than they typed

Called from two places:

  - internal/cli/commands_vm.go vmCreateParamsFromFlags — rejects
    bad `--name` values before any RPC. Empty name still passes
    through so the daemon can generate one.
  - internal/daemon/vm_create.go reserveVM — defense in depth for
    any non-CLI RPC caller (SDK, direct JSON over the socket).

Tests:

  - internal/model/vm_name_test.go — exhaustive character-class
    matrix (space, newline, tab, dot, slash, colon, equals, quote,
    control chars, unicode letters, uppercase, leading/trailing
    hyphen, over-length, max-length-exact, digits-only).
  - internal/cli TestVMCreateParamsFromFlagsRejectsInvalidName —
    CLI wire-through + empty-name passthrough.
  - internal/daemon TestReserveVMRejectsInvalidName — daemon
    defense-in-depth (including `box/../evil` path-traversal).
  - scripts/smoke.sh — end-to-end rejection + no-leaked-row
    assertion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 14:06:40 -03:00

68 lines
2.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package model
import (
"strings"
"testing"
)
func TestValidateVMName(t *testing.T) {
cases := []struct {
name string
input string
wantOK bool
wantErrSub string
}{
// Happy path.
{"simple", "mybox", true, ""},
{"with-hyphen", "my-box", true, ""},
{"digits", "box-123", true, ""},
{"digits-only", "1234", true, ""},
{"single-char", "a", true, ""},
{"max length", strings.Repeat("a", MaxVMNameLen), true, ""},
{"namegen style", "ace-fox", true, ""},
// Empty / length.
{"empty", "", false, "required"},
{"over max length", strings.Repeat("a", MaxVMNameLen+1), false, "max is"},
// Hyphen position.
{"leading hyphen", "-box", false, "cannot start or end with '-'"},
{"trailing hyphen", "box-", false, "cannot start or end with '-'"},
{"lone hyphen", "-", false, "cannot start or end with '-'"},
// Character class.
{"uppercase", "MyBox", false, "invalid character"},
{"space", "my box", false, "invalid character"},
{"newline", "my\nbox", false, "invalid character"},
{"tab", "my\tbox", false, "invalid character"},
{"dot", "my.box", false, "invalid character"},
{"dot-vm suffix", "box.vm", false, "invalid character"},
{"slash", "my/box", false, "invalid character"},
{"underscore", "my_box", false, "invalid character"},
{"at sign", "user@box", false, "invalid character"},
{"colon (kernel cmdline separator)", "my:box", false, "invalid character"},
{"equals (kernel cmdline)", "a=b", false, "invalid character"},
{"quote", "my\"box", false, "invalid character"},
{"unicode letter", "box-α", false, "invalid character"},
{"leading space", " box", false, "invalid character"},
{"trailing space", "box ", false, "invalid character"},
{"control char NUL", "my\x00box", false, "invalid character"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := ValidateVMName(tc.input)
if tc.wantOK {
if err != nil {
t.Fatalf("ValidateVMName(%q) = %v, want nil", tc.input, err)
}
return
}
if err == nil {
t.Fatalf("ValidateVMName(%q) = nil, want error containing %q", tc.input, tc.wantErrSub)
}
if !strings.Contains(err.Error(), tc.wantErrSub) {
t.Fatalf("ValidateVMName(%q) = %v, want error containing %q", tc.input, err, tc.wantErrSub)
}
})
}
}