Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .github/test-infra/aws/eks/cluster.tf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2025 Defense Unicorns
# Copyright 2025-2026 Defense Unicorns
# SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Defense-Unicorns-Commercial

locals {
Expand Down Expand Up @@ -58,6 +58,18 @@ module "eks" {
source_cluster_security_group = true
}

# The terraform-aws-eks recommended rules only open UDP/53 (CoreDNS) between nodes,
# so cross-node pod-to-pod UDP on any other port is silently dropped at the node SG.
# This must be self-referencing node-to-node traffic, not cluster-SG sourced traffic.
clusterapi_ingress_udp = {
description = "Pod-to-pod UDP across nodes"
protocol = "udp"
from_port = 0
to_port = 65535
type = "ingress"
self = true
}

// This is needed to allow the ELB to communicate with Istio ingress gateways
ingress_443 = {
description = "Allow ELB to Nodes"
Expand Down
7 changes: 6 additions & 1 deletion src/test/app-curl.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,12 @@ spec:
- name: udp-echo-server
image: curlimages/curl
imagePullPolicy: IfNotPresent
command: ["sleep", "3600"]
command:
[
"sh",
"-c",
"printf 'server-mode:nc-loop-w1\n' >> /tmp/udp.meta; while true; do nc -u -l -p 5000 -w 1 >> /tmp/udp.log; done",
]
resources:
limits:
cpu: 50m
Expand Down
161 changes: 102 additions & 59 deletions test/vitest/network.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,49 @@ function isResponseError(curlOutput: { stdout: string; stderr: string }) {
return httpResponseCode < 100 || httpResponseCode > 399;
}

async function clearUdpLog(serverPodName: string): Promise<void> {
await execInPod("curl-ns-udp-server", serverPodName, "udp-echo-server", [
"sh",
"-c",
"> /tmp/udp.log",
]);
}

async function readUdpLog(serverPodName: string): Promise<string> {
const result = await execInPod("curl-ns-udp-server", serverPodName, "udp-echo-server", [
"sh",
"-c",
"cat /tmp/udp.log 2>/dev/null || true",
]);

return result.stdout.trim();
}

async function waitForUdpLog(
serverPodName: string,
expected: string,
timeoutMs = 5000,
intervalMs = 250,
): Promise<string> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const log = await readUdpLog(serverPodName);
if (log === expected) return log;
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
return readUdpLog(serverPodName);
}

function expectUdpPingLog(log: string, message: string) {
const lines = log.trim().split("\n").filter(Boolean);

expect(lines.length > 0, `${message} lines=${JSON.stringify(lines)}`).toBe(true);
expect(
lines.every(line => line === "ping"),
`${message} lines=${JSON.stringify(lines)}`,
).toBe(true);
}

// Check if egress tests should run
const runEgressTests = process.env.EGRESS_TESTS === "true";

Expand All @@ -83,22 +126,37 @@ let curlPodNameEgress2 = "";
let udpServerPodName = "";
let udpClientPodName = "";

// Each getPodName is a separate API list call; on a slow EKS API server, 9+ sequential
// calls can overrun the default 10s hook timeout. Run them in parallel so total time
// tracks the slowest single call, not the sum.
beforeAll(async () => {
// Always fetch these pod names
curlPodName1 = await getPodName("curl-ns-deny-all-1", "app=curl-pkg-deny-all-1");
testAdminApp = await getPodName("test-admin-app", "app=httpbin");
curlPodName6 = await getPodName("curl-ns-remote-ns-1", "app=curl-pkg-remote-ns-egress");
curlPodName8 = await getPodName("curl-ns-kube-api", "app=curl-pkg-kube-api");
curlPodNameEgressAmbient1 = await getPodName("egress-ambient-1", "app=curl");
curlPodNameEgressAmbient2 = await getPodName("egress-ambient-2", "app=curl");
curlPodNameEgressAmbient3 = await getPodName("egress-ambient-2", "app=another-curl");
udpServerPodName = await getPodName("curl-ns-udp-server", "app=udp-echo-server");
udpClientPodName = await getPodName("curl-ns-udp-allow", "app=udp-echo-client");

// Only fetch egress pod names if egress tests will run
[
curlPodName1,
testAdminApp,
curlPodName6,
curlPodName8,
curlPodNameEgressAmbient1,
curlPodNameEgressAmbient2,
curlPodNameEgressAmbient3,
udpServerPodName,
udpClientPodName,
] = await Promise.all([
getPodName("curl-ns-deny-all-1", "app=curl-pkg-deny-all-1"),
getPodName("test-admin-app", "app=httpbin"),
getPodName("curl-ns-remote-ns-1", "app=curl-pkg-remote-ns-egress"),
getPodName("curl-ns-kube-api", "app=curl-pkg-kube-api"),
getPodName("egress-ambient-1", "app=curl"),
getPodName("egress-ambient-2", "app=curl"),
getPodName("egress-ambient-2", "app=another-curl"),
getPodName("curl-ns-udp-server", "app=udp-echo-server"),
getPodName("curl-ns-udp-allow", "app=udp-echo-client"),
]);

if (runEgressTests) {
curlPodNameEgress1 = await getPodName("egress-gw-1", "app=curl");
curlPodNameEgress2 = await getPodName("egress-gw-2", "app=curl");
[curlPodNameEgress1, curlPodNameEgress2] = await Promise.all([
getPodName("egress-gw-1", "app=curl"),
getPodName("egress-gw-2", "app=curl"),
]);
}
});

Expand Down Expand Up @@ -525,58 +583,43 @@ test(
);

test("UDP NetworkPolicy - custom allow and deny", { retry: 2, timeout: 60000 }, async () => {
// Both execInPod calls run concurrently: the server nc blocks waiting for a UDP packet
// and the client sends after a short delay. We check the server's stdout to verify
// whether the packet arrived, with no echo mechanism required.

// Allowed: udp-echo-client (curl-ns-udp-allow) has remoteProtocol: UDP egress to server port 5000.
const [allowedServer] = await Promise.all([
execInPod("curl-ns-udp-server", udpServerPodName, "udp-echo-server", [
"sh",
"-c",
"timeout 3 nc -u -l -p 5000",
]),
(async () => {
// Retry sends at short intervals — UDP gives no feedback if the server isn't listening yet.
for (let i = 0; i < 5; i++) {
await new Promise(r => setTimeout(r, 600));
await execInPod("curl-ns-udp-allow", udpClientPodName, "udp-echo-client", [
"sh",
"-c",
"echo ping | nc -u -w 1 udp-echo-server.curl-ns-udp-server.svc.cluster.local 5000 2>/dev/null || true",
]);
}
})(),
const serverPod = await K8s(kind.Pod).InNamespace("curl-ns-udp-server").Get(udpServerPodName);
const serverPodIP = serverPod.status?.podIP ?? "";
const serverNode = serverPod.spec?.nodeName ?? "";

const clientPod = await K8s(kind.Pod).InNamespace("curl-ns-udp-allow").Get(udpClientPodName);
const clientNode = clientPod.spec?.nodeName ?? "";

const placement = `server-node="${serverNode}" client-node="${clientNode}" server-ip="${serverPodIP}"`;

// Validate the NetworkPolicy behavior via direct pod IP so the test stays focused on UDP
// policy enforcement rather than kube-proxy ClusterIP DNAT timing on EKS.
await clearUdpLog(udpServerPodName);
const allowedSend = await execInPod("curl-ns-udp-allow", udpClientPodName, "udp-echo-client", [
"sh",
"-c",
`for i in 1 2 3; do echo ping | nc -u -w 1 ${serverPodIP} 5000 2>&1; printf ' attempt:%s' "$i"; sleep 0.2; done; echo; echo nc-exit:$?`,
]);

const allowedDebug = `UDP allowed: server stdout="${allowedServer.stdout}"`;
expect(allowedServer.stdout.trim(), allowedDebug).toBe("ping");
const allowedLog = await waitForUdpLog(udpServerPodName, "ping", 5000, 250);
expectUdpPingLog(
allowedLog,
`UDP allowed: log="${allowedLog}" nc="${allowedSend.stdout}" ${placement}`,
);

// Blocked: the client's egress NetworkPolicy (curl-pkg-deny-all-1 has no UDP egress to
// port 5000) is the first enforcement point; the server's ingress NetworkPolicy
// (curl-pkg-udp-server only permits ingress from curl-ns-udp-allow) provides defense-in-depth.
// Either policy alone would block the traffic.
const [deniedServer] = await Promise.all([
execInPod("curl-ns-udp-server", udpServerPodName, "udp-echo-server", [
"sh",
"-c",
"timeout 3 nc -u -l -p 5000",
]),
(async () => {
// Retry sends at short intervals — UDP gives no feedback if the server isn't listening yet.
for (let i = 0; i < 5; i++) {
await new Promise(r => setTimeout(r, 600));
await execInPod("curl-ns-deny-all-1", curlPodName1, "curl-pkg-deny-all-1", [
"sh",
"-c",
"echo ping | nc -u -w 1 udp-echo-server.curl-ns-udp-server.svc.cluster.local 5000 2>/dev/null || true",
]);
}
})(),
await clearUdpLog(udpServerPodName);
const deniedSend = await execInPod("curl-ns-deny-all-1", curlPodName1, "curl-pkg-deny-all-1", [
"sh",
"-c",
`for i in 1 2 3; do echo ping | nc -u -w 1 ${serverPodIP} 5000 2>&1; printf ' attempt:%s' "$i"; sleep 0.2; done; echo; echo nc-exit:$?`,
]);

const deniedDebug = `UDP blocked: server stdout="${deniedServer.stdout}"`;
expect(deniedServer.stdout.trim(), deniedDebug).toBe("");
const deniedLog = await waitForUdpLog(udpServerPodName, "ping", 2000, 250);
expect(deniedLog, `UDP blocked: log="${deniedLog}" nc="${deniedSend.stdout}" ${placement}`).toBe(
"",
);
});

test.concurrent("Keycloak AuthorizationPolicies", async () => {
Expand Down
Loading