a73x

09c1011b

test: pin the reserved port range and the vfkit socket limit to real numbers

a73x   2026-08-23 11:12

Commit message
test: pin the reserved port range and the vfkit socket limit to real numbers

Every exposure test measures ports relative to MinAllocatedHostPort, so
at 80 the allocator hands out privileged ports and the suite is green;
one allocation now has to land between literal 1024 and 32767, where a
host can actually bind without colliding with the kernel's ephemeral
range. vfkit's maxSocketPath restates macOS's sockaddr_un.sun_path, so
Preflight is now driven at 103 bytes (must pass) and 104 (must refuse)
rather than asked whether it agrees with itself.

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)