feat(device-edge): add fail-closed ingress admission
This commit is contained in:
@@ -23,7 +23,7 @@ const artifactDir = resolve(
|
||||
|| resolve(scriptDir, "../deploy-artifacts"),
|
||||
);
|
||||
const [
|
||||
patchId = "device-edge-ingress-ipvlan-20260804-001",
|
||||
patchId = "device-edge-admission-gate-20260804-002",
|
||||
...extra
|
||||
] = process.argv.slice(2);
|
||||
|
||||
@@ -38,7 +38,7 @@ const files = [
|
||||
"docker-compose.device-edge.ingress.yml",
|
||||
"services/device-edge-relay/Dockerfile",
|
||||
"services/device-edge-relay/src",
|
||||
"deployment/device-edge-ingress-ipvlan-v1.json",
|
||||
"deployment/device-edge-admission-gate-v1.json",
|
||||
];
|
||||
const ignoredBasenames = new Set([".DS_Store", ".git", "node_modules"]);
|
||||
const descriptor = await assertBoundary();
|
||||
@@ -84,7 +84,7 @@ try {
|
||||
artifact: target,
|
||||
sha256: digest,
|
||||
component: "device-edge",
|
||||
transition: "single-nic-ipvlan-b2-relay-only",
|
||||
transition: "reviewed-ipvlan-b2-relay-admission-gate",
|
||||
entries: files,
|
||||
services: ["device-edge-relay"],
|
||||
preservedRuntime: ["device-edge-backhaul", "tailnet", "Gelios"],
|
||||
@@ -96,6 +96,9 @@ try {
|
||||
ipv4Approval: descriptor.ingressIpv4Approval,
|
||||
tcp: 9921,
|
||||
hostPortPublication: "disabled",
|
||||
sourceAdmission: descriptor.sourceAdmission,
|
||||
maxTrackedSourceAddresses: descriptor.maxTrackedSourceAddresses,
|
||||
maxBytesPerDirection: descriptor.maxBytesPerDirection,
|
||||
lifecycle: "quarantine",
|
||||
commandTransport: "disabled",
|
||||
},
|
||||
@@ -117,7 +120,7 @@ async function assertBoundary() {
|
||||
const descriptor = JSON.parse(await readFile(
|
||||
resolve(
|
||||
sourceRoot,
|
||||
"deployment/device-edge-ingress-ipvlan-v1.json",
|
||||
"deployment/device-edge-admission-gate-v1.json",
|
||||
),
|
||||
"utf8",
|
||||
));
|
||||
@@ -145,6 +148,9 @@ async function assertBoundary() {
|
||||
'DEVICE_EDGE_RELAY_INGRESS_ENABLED: "true"',
|
||||
"DEVICE_EDGE_RELAY_UPSTREAM_HOST: device-edge-backhaul",
|
||||
'DEVICE_EDGE_RELAY_UPSTREAM_PORT: "19921"',
|
||||
"DEVICE_EDGE_RELAY_SOURCE_POLICY: public-ipv4-only",
|
||||
'DEVICE_EDGE_RELAY_MAX_TRACKED_SOURCE_ADDRESSES: "2048"',
|
||||
'DEVICE_EDGE_RELAY_MAX_BYTES_PER_DIRECTION: "262144"',
|
||||
"name: nodedc-device-edge-ingress",
|
||||
"driver: ipvlan",
|
||||
"parent: enp1s0f0",
|
||||
@@ -171,7 +177,7 @@ async function assertBoundary() {
|
||||
}
|
||||
|
||||
const expected = {
|
||||
schemaVersion: "nodedc.device-edge.ingress-ipvlan.v1",
|
||||
schemaVersion: "nodedc.device-edge.admission-gate.v1",
|
||||
mode: "single-nic-ipvlan-b2-relay-only",
|
||||
runtimeHost: "ndcmini12",
|
||||
component: "device-edge",
|
||||
@@ -192,6 +198,9 @@ async function assertBoundary() {
|
||||
hostPortPublication: "disabled",
|
||||
healthPublication: "disabled",
|
||||
privateUpstream: "device-edge-backhaul:19921",
|
||||
sourceAdmission: "public-ipv4-only",
|
||||
maxTrackedSourceAddresses: 2048,
|
||||
maxBytesPerDirection: 262144,
|
||||
protocolInspection: "gateway-owned",
|
||||
identityTrust: "claimed-not-ownership-proof",
|
||||
discoveryLifecycle: "quarantine",
|
||||
@@ -199,7 +208,7 @@ async function assertBoundary() {
|
||||
gelios: "untouched",
|
||||
amneziaHostFullTunnel: "preserved",
|
||||
routerNatFirewall: "separate-manual-gate",
|
||||
rollback: "restore-predecessor-relay-remove-unused-ingress-network",
|
||||
rollback: "restore-reviewed-ipvlan-predecessor-without-network-or-router-mutation",
|
||||
};
|
||||
if (JSON.stringify(descriptor) !== JSON.stringify(expected)) {
|
||||
throw new Error("device_edge_ingress_descriptor_mismatch");
|
||||
|
||||
@@ -60,10 +60,25 @@ ENTRIES = (
|
||||
"docker-compose.device-edge.ingress.yml",
|
||||
"services/device-edge-relay/Dockerfile",
|
||||
"services/device-edge-relay/src",
|
||||
"deployment/device-edge-ingress-ipvlan-v1.json",
|
||||
"deployment/device-edge-admission-gate-v1.json",
|
||||
)
|
||||
|
||||
PAYLOAD_FILE_SHA256 = {
|
||||
"docker-compose.device-edge.yml":
|
||||
"666945ffd9512355e610ecd36a9df96936477315150555def93e0243e8ff1e22",
|
||||
"docker-compose.device-edge.ingress.yml":
|
||||
"11bedfd7fdea749ca1bdb3b35b9c136c86b330f51a9001f0b38c4618f6f96108",
|
||||
"services/device-edge-relay/Dockerfile":
|
||||
"f2f15b7618ac2ab3a1d4dd40041d06d1e9a695edcfc168d8835f9560b4897e70",
|
||||
"services/device-edge-relay/src/runtime.mjs":
|
||||
"21e83678980aa61127bf9f3d77982dd485c4aaae208c43818db7bb1cc150b83a",
|
||||
"services/device-edge-relay/src/server.mjs":
|
||||
"1b99ec944f1d3fbadded045b159f08624e2829620cec39a97f6b4b8cdcd2be22",
|
||||
"deployment/device-edge-admission-gate-v1.json":
|
||||
"e6c1f21ff297b451c42b6746bc2063484874435dfa9f1614410a7cbe84f0ce6f",
|
||||
}
|
||||
|
||||
PREDECESSOR_FILE_SHA256 = {
|
||||
"docker-compose.device-edge.yml":
|
||||
"7f13c11d6d4d541964053c0a8cf791e401947d34c42e0f7c26f9f9df26fa00b5",
|
||||
"docker-compose.device-edge.ingress.yml":
|
||||
@@ -78,20 +93,8 @@ PAYLOAD_FILE_SHA256 = {
|
||||
"b9ce402db0c059a76f07a8d4a34297aff2250fd1c0d1aed9970b3a88f4e75d7f",
|
||||
}
|
||||
|
||||
PREDECESSOR_FILE_SHA256 = {
|
||||
"docker-compose.device-edge.yml":
|
||||
"50dc7366d7db451935dd4d67f76ec8b342b3a94be6d22176976db910981445d0",
|
||||
"services/device-edge-relay/Dockerfile":
|
||||
"f2f15b7618ac2ab3a1d4dd40041d06d1e9a695edcfc168d8835f9560b4897e70",
|
||||
"services/device-edge-relay/src/runtime.mjs":
|
||||
"ae8bf8b55603bab266b6fa6e9bc65c9f310a9d94a54db04e2130704e38622ffc",
|
||||
"services/device-edge-relay/src/server.mjs":
|
||||
"e4b051b74f934bd37322440e6a013fb6774a76607da08f9cc1e844fc109c83c1",
|
||||
}
|
||||
|
||||
PREDECESSOR_ABSENT = {
|
||||
"docker-compose.device-edge.ingress.yml",
|
||||
"deployment/device-edge-ingress-ipvlan-v1.json",
|
||||
"deployment/device-edge-admission-gate-v1.json",
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +139,7 @@ def docker_json(*args):
|
||||
|
||||
def expected_descriptor():
|
||||
return {
|
||||
"schemaVersion": "nodedc.device-edge.ingress-ipvlan.v1",
|
||||
"schemaVersion": "nodedc.device-edge.admission-gate.v1",
|
||||
"mode": "single-nic-ipvlan-b2-relay-only",
|
||||
"runtimeHost": "ndcmini12",
|
||||
"component": COMPONENT,
|
||||
@@ -157,6 +160,9 @@ def expected_descriptor():
|
||||
"hostPortPublication": "disabled",
|
||||
"healthPublication": "disabled",
|
||||
"privateUpstream": "device-edge-backhaul:19921",
|
||||
"sourceAdmission": "public-ipv4-only",
|
||||
"maxTrackedSourceAddresses": 2048,
|
||||
"maxBytesPerDirection": 262144,
|
||||
"protocolInspection": "gateway-owned",
|
||||
"identityTrust": "claimed-not-ownership-proof",
|
||||
"discoveryLifecycle": "quarantine",
|
||||
@@ -164,7 +170,7 @@ def expected_descriptor():
|
||||
"gelios": "untouched",
|
||||
"amneziaHostFullTunnel": "preserved",
|
||||
"routerNatFirewall": "separate-manual-gate",
|
||||
"rollback": "restore-predecessor-relay-remove-unused-ingress-network",
|
||||
"rollback": "restore-reviewed-ipvlan-predecessor-without-network-or-router-mutation",
|
||||
}
|
||||
|
||||
|
||||
@@ -268,7 +274,7 @@ def validate_payload(payload: Path):
|
||||
if actual_files != PAYLOAD_FILE_SHA256:
|
||||
die("Device Edge artifact payload digest set mismatch")
|
||||
descriptor = json.loads(
|
||||
(payload / "deployment/device-edge-ingress-ipvlan-v1.json")
|
||||
(payload / "deployment/device-edge-admission-gate-v1.json")
|
||||
.read_text(encoding="utf-8")
|
||||
)
|
||||
if descriptor != expected_descriptor():
|
||||
@@ -352,18 +358,24 @@ def assert_preserved_runtime(snapshot):
|
||||
def validate_predecessor_runtime():
|
||||
relay = inspect_container(RELAY_CONTAINER)
|
||||
if relay.get("State", {}).get("Status") != "running":
|
||||
die("Device Edge predecessor relay is not running")
|
||||
die("Device Edge IPvlan predecessor relay is not running")
|
||||
if container_health(relay) != "healthy":
|
||||
die("Device Edge predecessor relay is not healthy")
|
||||
die("Device Edge IPvlan predecessor relay is not healthy")
|
||||
environment = set(relay.get("Config", {}).get("Env") or [])
|
||||
if "DEVICE_EDGE_RELAY_INGRESS_ENABLED=false" not in environment:
|
||||
die("Device Edge predecessor ingress is not disabled")
|
||||
required = {
|
||||
"DEVICE_EDGE_RELAY_HEALTH_HOST=127.0.0.1",
|
||||
"DEVICE_EDGE_RELAY_INGRESS_ENABLED=true",
|
||||
"DEVICE_EDGE_RELAY_TCP_HOST=0.0.0.0",
|
||||
"DEVICE_EDGE_RELAY_TCP_PORT=9921",
|
||||
"DEVICE_EDGE_RELAY_UPSTREAM_HOST=device-edge-backhaul",
|
||||
"DEVICE_EDGE_RELAY_UPSTREAM_PORT=19921",
|
||||
}
|
||||
if not required.issubset(environment):
|
||||
die("Device Edge IPvlan predecessor environment mismatch")
|
||||
bindings = relay.get("HostConfig", {}).get("PortBindings") or {}
|
||||
expected = {"18221/tcp": [{"HostIp": "127.0.0.1", "HostPort": "18221"}]}
|
||||
if bindings != expected:
|
||||
die("Device Edge predecessor host publication mismatch")
|
||||
if INGRESS_NETWORK in (relay.get("NetworkSettings", {}).get("Networks") or {}):
|
||||
die("Device Edge predecessor unexpectedly uses ingress network")
|
||||
if bindings not in ({}, None):
|
||||
die("Device Edge IPvlan predecessor host publication mismatch")
|
||||
validate_network_runtime(relay)
|
||||
|
||||
|
||||
def validate_host_network_boundary():
|
||||
@@ -435,10 +447,6 @@ def preflight(manifest, artifact_sha256):
|
||||
validate_predecessor_runtime()
|
||||
preserved = preserved_runtime_snapshot()
|
||||
validate_host_network_boundary()
|
||||
if arp_duplicate_detected(INGRESS_IPV4, INGRESS_PARENT):
|
||||
die("Device Edge ingress IPv4 duplicate detected")
|
||||
if run([DOCKER, "network", "inspect", INGRESS_NETWORK], check=False).returncode == 0:
|
||||
die("Device Edge ingress network already exists")
|
||||
return preserved
|
||||
|
||||
|
||||
@@ -626,6 +634,9 @@ def validate_relay_runtime(preserved):
|
||||
"DEVICE_EDGE_RELAY_TCP_PORT=9921",
|
||||
"DEVICE_EDGE_RELAY_UPSTREAM_HOST=device-edge-backhaul",
|
||||
"DEVICE_EDGE_RELAY_UPSTREAM_PORT=19921",
|
||||
"DEVICE_EDGE_RELAY_SOURCE_POLICY=public-ipv4-only",
|
||||
"DEVICE_EDGE_RELAY_MAX_TRACKED_SOURCE_ADDRESSES=2048",
|
||||
"DEVICE_EDGE_RELAY_MAX_BYTES_PER_DIRECTION=262144",
|
||||
}
|
||||
if not required.issubset(environment):
|
||||
die("Device Edge relay environment mismatch")
|
||||
@@ -648,6 +659,7 @@ def validate_relay_runtime(preserved):
|
||||
"ingress": "relay-only",
|
||||
"protocolInspection": "disabled",
|
||||
"commandTransport": "disabled",
|
||||
"sourceAdmission": "public-ipv4-only",
|
||||
}
|
||||
for key, expected in expected_health.items():
|
||||
if health.get(key) != expected:
|
||||
@@ -688,10 +700,8 @@ def rollback(backup: Path, preserved):
|
||||
"--pull",
|
||||
"never",
|
||||
RELAY_SERVICE,
|
||||
baseline=True,
|
||||
), cwd=LIVE_ROOT, timeout=300, capture=False)
|
||||
wait_healthy(RELAY_CONTAINER)
|
||||
run([DOCKER, "network", "rm", INGRESS_NETWORK], check=False)
|
||||
current_source_state()
|
||||
validate_predecessor_runtime()
|
||||
assert_preserved_runtime(preserved)
|
||||
@@ -725,12 +735,15 @@ def plan_artifact(artifact_argument: str):
|
||||
print("device_edge_host_port_publication=disabled")
|
||||
print("device_edge_health_publication=disabled")
|
||||
print("device_edge_private_upstream=device-edge-backhaul:19921")
|
||||
print("device_edge_source_admission=public-ipv4-only")
|
||||
print("device_edge_source_table_limit=2048")
|
||||
print("device_edge_byte_limit_per_direction=262144")
|
||||
print("device_edge_command_transport=disabled")
|
||||
print("device_edge_discovery_lifecycle=quarantine")
|
||||
print("device_edge_gelios=untouched")
|
||||
print("device_edge_amnezia=preserved:active:host-full-tunnel")
|
||||
print("device_edge_router_nat_firewall=unchanged")
|
||||
print("device_edge_rollback=restore-predecessor-relay-remove-unused-ingress-network")
|
||||
print("device_edge_rollback=restore-reviewed-ipvlan-predecessor-no-router-mutation")
|
||||
print("state=new")
|
||||
print("== files ==")
|
||||
for entry in entries:
|
||||
@@ -826,6 +839,7 @@ def verify_install():
|
||||
print(f"compose={compose_version}")
|
||||
print(f"device_edge_ingress_ipv4={INGRESS_IPV4}")
|
||||
print(f"device_edge_ingress_ipv4_approval={INGRESS_IPV4_APPROVAL}")
|
||||
print("device_edge_source_admission=public-ipv4-only")
|
||||
print("verify-install-ok")
|
||||
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ class DeviceEdgeIngressArtifactTest(unittest.TestCase):
|
||||
) as directory:
|
||||
result = self.build(
|
||||
Path(directory),
|
||||
"device-edge-ingress-ipvlan-20260804-001",
|
||||
"device-edge-admission-gate-20260804-002",
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
built = json.loads(result.stdout)
|
||||
@@ -128,7 +128,7 @@ class DeviceEdgeIngressArtifactTest(unittest.TestCase):
|
||||
inbox.mkdir()
|
||||
result = self.build(
|
||||
inbox,
|
||||
"device-edge-ingress-ipvlan-20260804-002",
|
||||
"device-edge-admission-gate-20260804-003",
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
artifact = Path(json.loads(result.stdout)["artifact"])
|
||||
@@ -150,7 +150,7 @@ class DeviceEdgeIngressArtifactTest(unittest.TestCase):
|
||||
self.assertEqual(digest, hashlib.sha256(artifact.read_bytes()).hexdigest())
|
||||
self.assertEqual(
|
||||
json.loads(
|
||||
(payload / "deployment/device-edge-ingress-ipvlan-v1.json")
|
||||
(payload / "deployment/device-edge-admission-gate-v1.json")
|
||||
.read_text(encoding="utf-8")
|
||||
),
|
||||
RUNNER.expected_descriptor(),
|
||||
|
||||
Reference in New Issue
Block a user