09c1011b
test: pin the reserved port range and the vfkit socket limit to real numbers
a73x 2026-08-23 11:12
Commit message
internal/agent/vfkit/vfkit_test.go
| Old | New | ||
|---|---|---|---|
| @@ -139,7 +139,62 @@ func TestPreflightAcceptsTheDefaultStateDirectory(t *testing.T) { | |||
| 139 | assert.NoError(t, p.Preflight(context.Background())) | 139 | assert.NoError(t, p.Preflight(context.Background())) |
| 140 | // The shipped default is /var/lib/eitri-agent; a VM's socket under it is | 140 | // The shipped default is /var/lib/eitri-agent; a VM's socket under it is |
| 141 | // 68 bytes, so the guard must not be so tight that normal use trips it. | 141 | // 68 bytes, so the guard must not be so tight that normal use trips it. |
| 142 | assert.Less(t, len("/var/lib/eitri-agent/vms/"+strings.Repeat("0", vmIDLen)+"/vfkit.sock"), maxSocketPath) | 142 | assert.Less(t, len("/var/lib/eitri-agent/vms/"+strings.Repeat("0", vmIDLen)+"/vfkit.sock"), sunPathBytes, |
| 143 | "the shipped default --state-dir must leave room for a VM's control socket inside sockaddr_un") | ||
| 144 | } | ||
| 145 | |||
| 146 | // sunPathBytes is macOS's sockaddr_un.sun_path, terminator included. It is a | ||
| 147 | // literal here because it is the OS's number, not eitri's: maxSocketPath only | ||
| 148 | // restates it, so a Preflight boundary asserted against maxSocketPath moves | ||
| 149 | // whenever the constant does while the real bind keeps failing at 104. | ||
| 150 | const sunPathBytes = 104 | ||
| 151 | |||
| 152 | // provWithSocketPathLen builds a Provisioner whose state directory is padded so | ||
| 153 | // that a VM's control socket path is exactly want bytes long. | ||
| 154 | func provWithSocketPathLen(t *testing.T, want int) *Provisioner { | ||
| 155 | t.Helper() | ||
| 156 | root, err := os.MkdirTemp("", "e") | ||
| 157 | require.NoError(t, err) | ||
| 158 | t.Cleanup(func() { _ = os.RemoveAll(root) }) | ||
| 159 | |||
| 160 | probe, err := state.Open(root) | ||
| 161 | require.NoError(t, err) | ||
| 162 | base := len(New(probe, "vfkit", nil).sockPath(strings.Repeat("0", vmIDLen))) | ||
| 163 | pad := want - base - 1 // -1 for the separator filepath.Join adds | ||
| 164 | require.Positive(t, pad, "TMPDIR is already longer than the target path; run with a shorter TMPDIR") | ||
| 165 | |||
| 166 | dir := filepath.Join(root, strings.Repeat("d", pad)) | ||
| 167 | require.NoError(t, os.MkdirAll(dir, 0o700)) | ||
| 168 | st, err := state.Open(dir) | ||
| 169 | require.NoError(t, err) | ||
| 170 | p := New(st, "vfkit", nil) | ||
| 171 | p.lookPath = func(file string) (string, error) { return file, nil } | ||
| 172 | require.Len(t, p.sockPath(strings.Repeat("0", vmIDLen)), want, "harness must hit the target length exactly") | ||
| 173 | return p | ||
| 174 | } | ||
| 175 | |||
| 176 | // TestPreflightAcceptsTheLongestSocketPathThatBinds and its sibling below pin | ||
| 177 | // the guard on the byte, from both sides. Between them they hold maxSocketPath | ||
| 178 | // to macOS's number rather than to whatever it currently says. | ||
| 179 | func TestPreflightAcceptsTheLongestSocketPathThatBinds(t *testing.T) { | ||
| 180 | p := provWithSocketPathLen(t, sunPathBytes-1) | ||
| 181 | |||
| 182 | assert.NoError(t, p.Preflight(context.Background()), | ||
| 183 | "103 bytes plus the NUL terminator is exactly sockaddr_un.sun_path (104), so this path binds — refusing it turns a "+ | ||
| 184 | "working --state-dir away and sends an operator hunting for a shorter one that was never needed") | ||
| 185 | } | ||
| 186 | |||
| 187 | func TestPreflightRefusesASocketPathOneByteOverSunPath(t *testing.T) { | ||
| 188 | p := provWithSocketPathLen(t, sunPathBytes) | ||
| 189 | |||
| 190 | err := p.Preflight(context.Background()) | ||
| 191 | |||
| 192 | require.Error(t, err, | ||
| 193 | "104 bytes does not fit sockaddr_un.sun_path, which is 104 INCLUDING the terminator: macOS refuses the bind, and "+ | ||
| 194 | "raising maxSocketPath does not raise sun_path — it only moves the refusal to vfkit, after the image download, "+ | ||
| 195 | "as a sentence about URIs") | ||
| 196 | assert.True(t, isPermanent(err), "a state directory does not get shorter on retry") | ||
| 197 | assert.Contains(t, err.Error(), "--state-dir", "the refusal must name the fix") | ||
| 143 | } | 198 | } |
| 144 | 199 | ||
| 145 | // fakeRunner records the commands it is asked to run and answers from a script | 200 | // fakeRunner records the commands it is asked to run and answers from a script |
internal/server/store/exposures_test.go
| Old | New | ||
|---|---|---|---|
| @@ -33,6 +33,31 @@ func TestCreateExposureAllocatesFromTheReservedRange(t *testing.T) { | |||
| 33 | assert.NotEmpty(t, e.ID) | 33 | assert.NotEmpty(t, e.ID) |
| 34 | } | 34 | } |
| 35 | 35 | ||
| 36 | // TestReservedRangeSitsWhereAHostCanBind pins the two numbers the reserved | ||
| 37 | // range is built from, against literals rather than against themselves: every | ||
| 38 | // other exposure test measures ports relative to MinAllocatedHostPort, so the | ||
| 39 | // base is free to move anywhere and the suite stays green. | ||
| 40 | func TestReservedRangeSitsWhereAHostCanBind(t *testing.T) { | ||
| 41 | s := newStore(t) | ||
| 42 | h := enrollHost(t, s) | ||
| 43 | vm := makeExposureVM(t, s, h, "web-1") | ||
| 44 | |||
| 45 | e, err := s.CreateExposure(vm.ID, 8080, 0, "tcp") | ||
| 46 | require.NoError(t, err) | ||
| 47 | |||
| 48 | assert.GreaterOrEqual(t, e.HostPort, int64(1024), | ||
| 49 | "an auto-allocated host port must land above the privileged range: below 1024 the agent's bind needs root and "+ | ||
| 50 | "lands on ports the host's own sshd, DNS or web server already own, so eitri would hand a tenant's guest the "+ | ||
| 51 | "host's front door and the bind would fail — or worse, succeed first") | ||
| 52 | assert.LessOrEqual(t, e.HostPort, int64(32767), | ||
| 53 | "and below 32768, where Linux's default ip_local_port_range begins: a reserved port inside the kernel's ephemeral "+ | ||
| 54 | "range is one the kernel will also hand to an outbound socket, so the exposure's listener fails to bind "+ | ||
| 55 | "intermittently, on a schedule nobody can reproduce") | ||
| 56 | assert.LessOrEqual(t, int64(MaxAllocatedHostPort), int64(32767), | ||
| 57 | "the top of the reserved range answers to the same kernel: 30000-32767 is the documented span (docs/mcp.md), and "+ | ||
| 58 | "it stops one short of the ephemeral floor") | ||
| 59 | } | ||
| 60 | |||
| 36 | func TestCreateExposureAllocatesTheLowestFreePort(t *testing.T) { | 61 | func TestCreateExposureAllocatesTheLowestFreePort(t *testing.T) { |
| 37 | s := newStore(t) | 62 | s := newStore(t) |
| 38 | h := enrollHost(t, s) | 63 | h := enrollHost(t, s) |