Skip to content

Commit 65d123f

Browse files
myftijaTrigger.dev RepoOps
authored andcommitted
feat(supervisor): reserve configured source ports on run pods
The supervisor can now set the `net.ipv4.ip_local_reserved_ports` sysctl on every run pod it creates, via the new optional `KUBERNETES_POD_RESERVED_SOURCE_PORTS` (a comma-separated list of ports or `low-high` ranges, validated at startup), so the kernel never picks those ports as ephemeral source ports. Unset leaves run pods unchanged. The sysctl is namespaced and on the Kubernetes safe sysctl list since 1.27 (kernel 3.16+), so no kubelet allowlist is needed on 1.27+ clusters. Mono-RevId: fdd8c6b08a68c0a669c650fd51499a7a2efe24d1
1 parent 23730b0 commit 65d123f

7 files changed

Lines changed: 140 additions & 6 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: supervisor
3+
type: feature
4+
---
5+
6+
Set `KUBERNETES_POD_RESERVED_SOURCE_PORTS` on the supervisor (for example `50080,50081`) to stop runs using those ports for outbound connections, which is useful to avoid port clashes.

‎apps/supervisor/src/env.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
BoolEnv,
88
NodeLabelValue,
99
OrgPlacementOverrides,
10+
ReservedPorts,
1011
Tolerations,
1112
} from "./envUtil.js";
1213

@@ -280,6 +281,7 @@ export const Env = z
280281
.default("node-24-plus"),
281282
KUBERNETES_RUNNER_SECURITY_CONTEXT: z.enum(["off", "baseline", "restricted"]).default("off"),
282283
KUBERNETES_RUNNER_RUN_AS_USER: z.coerce.number().int().min(1).default(1000),
284+
KUBERNETES_POD_RESERVED_SOURCE_PORTS: ReservedPorts,
283285

284286
// Pod DNS config: override the cluster default ndots to `KUBERNETES_POD_DNS_NDOTS`.
285287
// Default k8s ndots is 5: any name with fewer than 5 dots (e.g. `api.example.com`, 2 dots) is first walked

‎apps/supervisor/src/envUtil.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
AdditionalEnvVars,
55
NodeLabelValue,
66
OrgPlacementOverrides,
7+
ReservedPorts,
78
Tolerations,
89
} from "./envUtil.js";
910

@@ -113,6 +114,27 @@ describe("NodeLabelValue", () => {
113114
});
114115
});
115116

117+
describe("ReservedPorts", () => {
118+
it("is unset when empty", () => {
119+
for (const value of [undefined, "", " "]) {
120+
expect(ReservedPorts.parse(value)).toBeUndefined();
121+
}
122+
});
123+
124+
it("accepts ports and ranges, stripping whitespace the kernel would reject", () => {
125+
expect(ReservedPorts.parse("50080,50081")).toBe("50080,50081");
126+
expect(ReservedPorts.parse(" 50080 , 50000-50100 ")).toBe("50080,50000-50100");
127+
expect(ReservedPorts.parse("0,65535,8080-8080")).toBe("0,65535,8080-8080");
128+
});
129+
130+
it.each(["50080,", ",50080", "50080 50081", "65536", "50100-50000", "08080", "0x50", "-1", "a"])(
131+
"refuses %j, so a bad value stops the supervisor from starting",
132+
(value) => {
133+
expect(ReservedPorts.safeParse(value).success).toBe(false);
134+
}
135+
);
136+
});
137+
116138
describe("Tolerations", () => {
117139
it("should parse key=value entries as Equal", () => {
118140
expect(Tolerations.parse("dedicated=runs:NoSchedule")).toEqual([

‎apps/supervisor/src/envUtil.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,36 @@ export const Tolerations = z.string().transform((val, ctx) => {
146146
});
147147
});
148148

149+
// The kernel rejects whitespace between entries and reads a leading zero as octal.
150+
const PORT_OR_RANGE = /^(0|[1-9]\d{0,4})(?:-(0|[1-9]\d{0,4}))?$/;
151+
152+
export const ReservedPorts = z
153+
.string()
154+
.optional()
155+
.transform((val, ctx) => {
156+
if (val === undefined || val.trim() === "") {
157+
return undefined;
158+
}
159+
160+
const entries = val.split(",").map((entry) => entry.trim());
161+
162+
for (const entry of entries) {
163+
const match = PORT_OR_RANGE.exec(entry);
164+
const low = Number(match?.[1]);
165+
const high = Number(match?.[2] ?? match?.[1]);
166+
167+
if (!match || high > 65535 || low > high) {
168+
ctx.addIssue({
169+
code: z.ZodIssueCode.custom,
170+
message: `Invalid reserved port "${entry}". Must be a port or an ascending low-high range, within 0-65535.`,
171+
});
172+
return z.NEVER;
173+
}
174+
}
175+
176+
return entries.join(",");
177+
});
178+
149179
/**
150180
* Scalar values are coerced: YAML/JSON easily produce `true` or `3` where a label
151181
* value is meant, and Kubernetes label values are always strings. An empty value

‎apps/supervisor/src/workloadManager/kubernetes.test.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
nodetypeNodeSelector,
44
runPodTolerations,
55
runnerSecurityContext,
6+
withReservedSourcePorts,
67
withRunnerSeccompProfile,
78
withNodeSelector,
89
} from "./kubernetesPodSpec.js";
@@ -155,6 +156,51 @@ describe("withRunnerSeccompProfile", () => {
155156
});
156157
});
157158

159+
describe("withReservedSourcePorts", () => {
160+
const reserved = { name: "net.ipv4.ip_local_reserved_ports", value: "50080,50081" };
161+
162+
it("leaves the spec untouched when unset", () => {
163+
for (const ports of [undefined, ""]) {
164+
expect(withReservedSourcePorts(basePodSpec, ports)).toBe(basePodSpec);
165+
}
166+
});
167+
168+
it("adds the sysctl and keeps the rest of the security context", () => {
169+
expect(withReservedSourcePorts(basePodSpec, "50080,50081")).toEqual({
170+
...basePodSpec,
171+
securityContext: { ...basePodSpec.securityContext, sysctls: [reserved] },
172+
});
173+
});
174+
175+
it("adds a security context when the spec has none", () => {
176+
expect(withReservedSourcePorts({ restartPolicy: "Never" }, "50080,50081")).toEqual({
177+
restartPolicy: "Never",
178+
securityContext: { sysctls: [reserved] },
179+
});
180+
});
181+
182+
it("preserves existing sysctls", () => {
183+
const existing = { name: "net.ipv4.ip_local_port_range", value: "32768 60999" };
184+
const podSpec = { ...basePodSpec, securityContext: { sysctls: [existing] } };
185+
186+
expect(withReservedSourcePorts(podSpec, "50080,50081").securityContext?.sysctls).toEqual([
187+
existing,
188+
reserved,
189+
]);
190+
});
191+
192+
it("replaces an existing value rather than duplicating the name", () => {
193+
const podSpec = {
194+
...basePodSpec,
195+
securityContext: { sysctls: [{ ...reserved, value: "40000" }] },
196+
};
197+
198+
expect(withReservedSourcePorts(podSpec, "50080,50081").securityContext?.sysctls).toEqual([
199+
reserved,
200+
]);
201+
});
202+
});
203+
158204
describe("runnerSecurityContext", () => {
159205
it("sets nothing when off", () => {
160206
expect(runnerSecurityContext("off", 1000, "node-24")).toBeUndefined();

‎apps/supervisor/src/workloadManager/kubernetes.ts‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
nodetypeNodeSelector,
1919
runPodTolerations,
2020
runnerSecurityContext,
21+
withReservedSourcePorts,
2122
withRunnerSeccompProfile,
2223
withNodeSelector,
2324
} from "./kubernetesPodSpec.js";
@@ -137,12 +138,15 @@ export class KubernetesWorkloadManager implements WorkloadManager {
137138
);
138139
}
139140
}
140-
const podSpec = withRunnerSeccompProfile(basePodSpec, {
141-
profilePath: env.KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH,
142-
runtimes: env.KUBERNETES_RUNNER_SECCOMP_PROFILE_RUNTIMES,
143-
runtime: opts.runtime,
144-
checkpointsEnabled: this.opts.checkpointsEnabled,
145-
});
141+
const podSpec = withReservedSourcePorts(
142+
withRunnerSeccompProfile(basePodSpec, {
143+
profilePath: env.KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH,
144+
runtimes: env.KUBERNETES_RUNNER_SECCOMP_PROFILE_RUNTIMES,
145+
runtime: opts.runtime,
146+
checkpointsEnabled: this.opts.checkpointsEnabled,
147+
}),
148+
env.KUBERNETES_POD_RESERVED_SOURCE_PORTS
149+
);
146150

147151
await this.k8s.core.createNamespacedPod({
148152
namespace: this.namespace,

‎apps/supervisor/src/workloadManager/kubernetesPodSpec.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,30 @@ export function withRunnerSeccompProfile(
9797
};
9898
}
9999

100+
const RESERVED_PORTS_SYSCTL = "net.ipv4.ip_local_reserved_ports";
101+
102+
export function withReservedSourcePorts(
103+
podSpec: Omit<k8s.V1PodSpec, "containers">,
104+
ports: string | undefined
105+
): Omit<k8s.V1PodSpec, "containers"> {
106+
if (!ports) {
107+
return podSpec;
108+
}
109+
110+
return {
111+
...podSpec,
112+
securityContext: {
113+
...podSpec.securityContext,
114+
sysctls: [
115+
...(podSpec.securityContext?.sysctls ?? []).filter(
116+
(sysctl) => sysctl.name !== RESERVED_PORTS_SYSCTL
117+
),
118+
{ name: RESERVED_PORTS_SYSCTL, value: ports },
119+
],
120+
},
121+
};
122+
}
123+
100124
const BUN_RUN_AS_USER = 1001;
101125

102126
/**

0 commit comments

Comments
 (0)