feat(device-edge): open bounded tracker ingress

This commit is contained in:
Codex
2026-08-12 15:10:27 +03:00
parent 6fd172ecc5
commit 606872edcc
9 changed files with 717 additions and 3 deletions
@@ -36,11 +36,12 @@ if (
"relay",
"core-channel",
"tailscale-retirement",
"tracker-ingress",
].includes(phase)
|| !/^[A-Za-z0-9._-]{1,96}$/.test(patchId || "")
) {
throw new Error(
"usage: build-device-edge-vps-artifact.mjs <foundation|runtime-reconciliation|backhaul|relay|core-channel|tailscale-retirement> <patch-id>",
"usage: build-device-edge-vps-artifact.mjs <foundation|runtime-reconciliation|backhaul|relay|core-channel|tailscale-retirement|tracker-ingress> <patch-id>",
);
}
@@ -96,6 +97,19 @@ const entriesByPhase = {
"tailscale-retirement": [
"deployment/device-edge-vps-tailscale-retirement-v1.json",
],
"tracker-ingress": [
"packages/device-adapter-runtime/package.json",
"packages/device-adapter-runtime/src",
"packages/device-adapter-catalog/package.json",
"packages/device-adapter-catalog/src",
"packages/arusnavi-b2-adapter/package.json",
"packages/arusnavi-b2-adapter/src",
"services/device-gateway/src/runtime.mjs",
"vps/edge-process/device-edge-runtime.mjs",
"vps/config/nftables-tracker-ingress.conf",
"vps/systemd/nodedc-device-edge-runtime.service",
"deployment/device-edge-vps-tracker-ingress-v1.json",
],
};
const entries = entriesByPhase[phase];
const ignoredBasenames = new Set([".DS_Store", ".git", "node_modules"]);
@@ -157,6 +171,8 @@ try {
? "tcp/9921"
: ["core-channel", "tailscale-retirement"].includes(phase)
? "tcp/443-mtls-only"
: phase === "tracker-ingress"
? "tcp/443-mtls+tcp/9921-telemetry"
: "disabled",
commandTransport: "disabled",
gelios: "untouched",
@@ -314,6 +330,41 @@ async function assertBoundary() {
}
}
}
if (phase === "tracker-ingress") {
for (const required of [
'"predecessorPatch": "device-edge-vps-tailscale-retirement-20260812-011"',
'"runtimeComposition": "single-process-core-channel-plus-universal-device-gateway"',
'"trackerIngress": "enabled:allowlisted-adapters-only"',
'"acknowledgementBoundary": "tracker-ack-only-after-core-durable-acceptance"',
'"initialAdapterProfile": "arusnavi.b2.internal.v1"',
"createDeviceGatewayRuntime",
"DEVICE_ADAPTER_CATALOG.registry",
"onDiscovery: (signal) => channel.submitDiscovery(signal)",
"onMessage: (message) => channel.submitAdapterMessage(message)",
"tcp dport 9921",
"User=nodedc-channel",
"MemoryMax=192M",
"MemorySwapMax=0",
"CPUQuota=75%",
"TasksMax=128",
"LimitNOFILE=1024",
]) {
if (!combined.includes(required)) {
throw new Error(`tracker_ingress_boundary_missing:${required}`);
}
}
for (const forbidden of [
"LocalForward",
"tailscale-userspace",
"DEVICE_EDGE_RELAY_UPSTREAM",
'commandTransport": "enabled',
"device.dc.ru",
]) {
if (combined.includes(forbidden)) {
throw new Error(`tracker_ingress_boundary_violation:${forbidden}`);
}
}
}
}
function canonicalTarScript() {
+171 -2
View File
@@ -106,6 +106,12 @@ CORE_CHANNEL_ACCEPTED_PATCH = "device-edge-vps-core-channel-20260812-010"
CORE_CHANNEL_ACCEPTED_SHA256 = (
"c8ef3c4bb45850cad32e881eba081bc4c891c2886e5500d02cb94616d82353f3"
)
TAILSCALE_RETIREMENT_ACCEPTED_PATCH = (
"device-edge-vps-tailscale-retirement-20260812-011"
)
TAILSCALE_RETIREMENT_ACCEPTED_SHA256 = (
"e7b61ec9c83122fa5631467010eff871b98935746df6a1326b6ff6bb9713d877"
)
FOUNDATION_ENTRIES = (
"vps/config/00-nodedc-b2-vps.conf",
@@ -143,6 +149,19 @@ CORE_CHANNEL_ENTRIES = (
TAILSCALE_RETIREMENT_ENTRIES = (
"deployment/device-edge-vps-tailscale-retirement-v1.json",
)
TRACKER_INGRESS_ENTRIES = (
"packages/device-adapter-runtime/package.json",
"packages/device-adapter-runtime/src",
"packages/device-adapter-catalog/package.json",
"packages/device-adapter-catalog/src",
"packages/arusnavi-b2-adapter/package.json",
"packages/arusnavi-b2-adapter/src",
"services/device-gateway/src/runtime.mjs",
"vps/edge-process/device-edge-runtime.mjs",
"vps/config/nftables-tracker-ingress.conf",
"vps/systemd/nodedc-device-edge-runtime.service",
"deployment/device-edge-vps-tracker-ingress-v1.json",
)
PHASE_ENTRIES = {
"foundation": FOUNDATION_ENTRIES,
@@ -151,6 +170,7 @@ PHASE_ENTRIES = {
"relay": RELAY_ENTRIES,
"core-channel": CORE_CHANNEL_ENTRIES,
"tailscale-retirement": TAILSCALE_RETIREMENT_ENTRIES,
"tracker-ingress": TRACKER_INGRESS_ENTRIES,
}
SUPERSEDED_TRANSPORT_PHASES = frozenset({"backhaul", "relay"})
@@ -218,6 +238,30 @@ PHASE_FILE_SHA256 = {
"deployment/device-edge-vps-tailscale-retirement-v1.json":
"bbe11e8cf4103f44ae7888b4d3f3dde7015eeaef8ea5a7c4c00427a7b85f8e33",
},
"tracker-ingress": {
"packages/device-adapter-runtime/package.json":
"293ee6010f255c511df40b34cf8019fcd0d1b06babe30761c2a842c7da7af778",
"packages/device-adapter-runtime/src/index.mjs":
"199fcdd775d1ea15ddc9f85ab24fc2c165bb7757008de26749f1b7e84481dfeb",
"packages/device-adapter-catalog/package.json":
"60dbfc551ca49719e0851755058c159968411c490621eac0bf96ca76bf8b78d1",
"packages/device-adapter-catalog/src/index.mjs":
"614749e763c0624bf0de7905343632d3edb4e026316f427a2f52ce12c4225dca",
"packages/arusnavi-b2-adapter/package.json":
"136fc997f5d6757ba81d370f03037e0a93fd4750dc624def1af593e7ba8b9098",
"packages/arusnavi-b2-adapter/src/index.mjs":
"c5d95391438dfff1f1c12396b2e919effca9003a4a4a3eb3b828be42790f6173",
"services/device-gateway/src/runtime.mjs":
"bf0697674ed150d5e043e19e31fa99a242bb5dcc7d184e7adaccd883a22c5e5e",
"vps/edge-process/device-edge-runtime.mjs":
"7616e894e55b2579de7c2652589baf3cd3804dc0ec8af69661eadac1098979e9",
"vps/config/nftables-tracker-ingress.conf":
"6a6a76a02e5908118104a9a54a9e41b5a7e04476418efdb1485e39f0ee7c995c",
"vps/systemd/nodedc-device-edge-runtime.service":
"7e639efb66eee891da85b2976353715d942bd8aa381ddc1383446debf279817b",
"deployment/device-edge-vps-tracker-ingress-v1.json":
"faecfa0317ad0e3d102c3636c0547b65257b930e9b48f34d8679c00ceb3d323b",
},
}
# Exact immutable baselines from terminally accepted predecessor artifacts.
@@ -617,6 +661,19 @@ def current_phase_preflight(phase: str):
return {
"predecessor": "failed-core-channel-001-rollback-runtime-mode-drift",
}
if phase == "tracker-ingress":
retirement_record = applied_phase_record("tailscale-retirement")
if (
retirement_record.get("patch")
!= TAILSCALE_RETIREMENT_ACCEPTED_PATCH
or retirement_record.get("sha256")
!= TAILSCALE_RETIREMENT_ACCEPTED_SHA256
):
die("VPS tracker ingress Tailscale retirement predecessor mismatch")
validate_tailscale_retirement_runtime()
if (LIVE_ROOT / TRACKER_INGRESS_ENTRIES[-1]).exists():
die("VPS tracker ingress target path already exists")
return {"predecessor": "accepted-tailscale-retirement-011"}
validate_foundation_runtime(
require_running_tailnet=phase in {"backhaul", "relay"},
expected_key_user=BACKHAUL_USER if phase == "relay" else SERVICE_USER,
@@ -724,6 +781,8 @@ def backup_targets_for_phase(phase: str):
TAILSCALE_BIN.parent,
TRUST_ROOT,
]
if phase == "tracker-ingress":
return common + [CHANNEL_UNIT, NFTABLES_CONFIG]
return common + [RELAY_UNIT, NFTABLES_CONFIG]
@@ -1194,6 +1253,18 @@ def apply_tailscale_retirement(_payload: Path):
validate_tailscale_retirement_runtime()
def apply_tracker_ingress(_payload: Path):
install_file(
LIVE_ROOT / TRACKER_INGRESS_ENTRIES[9],
CHANNEL_UNIT,
0o644,
)
apply_nftables(LIVE_ROOT / TRACKER_INGRESS_ENTRIES[8])
systemctl("daemon-reload")
systemctl("restart", "nodedc-device-edge-channel.service")
validate_tracker_ingress_runtime()
def sshd_effective():
return run(["/usr/sbin/sshd", "-T"]).stdout.lower()
@@ -1462,6 +1533,93 @@ def validate_tailscale_retirement_runtime():
return health
def validate_tracker_ingress_runtime():
source_file_state("foundation")
source_file_state("core-channel")
source_file_state("tailscale-retirement")
source_file_state("tracker-ingress")
retirement_record = applied_phase_record("tailscale-retirement")
if (
retirement_record.get("patch") != TAILSCALE_RETIREMENT_ACCEPTED_PATCH
or retirement_record.get("sha256")
!= TAILSCALE_RETIREMENT_ACCEPTED_SHA256
):
die("tracker ingress retirement identity mismatch")
if run([str(NODE_BIN), "--version"]).stdout.strip() != f"v{NODE_VERSION}":
die("tracker ingress Node version mismatch")
for path in (
TAILSCALE_UNIT,
TAILSCALE_SOCKET,
TAILSCALE_STATE.parent,
TAILSCALE_BIN.parent,
TRUST_ROOT,
):
if path.exists() or path.is_symlink():
die(f"tracker ingress superseded trust boundary present: {path}")
if service_active("nodedc-b2-tailscaled.service"):
die("tracker ingress Tailscale service became active")
if service_active("nodedc-b2-backhaul.service"):
die("tracker ingress frozen backhaul service became active")
if service_active("nodedc-b2-relay.service"):
die("tracker ingress frozen relay service became active")
assert_management_key()
assert_channel_trust(require_runtime_owner=True)
if not service_active("nodedc-device-edge-channel.service"):
die("VPS Device Edge runtime is not active")
health = core_channel_health(require_accepted=True)
expected = {
"ok": True,
"service": "nodedc-device-edge-runtime",
"channel": "accepted",
"trackerIngress": "telemetry-ingest",
"commandTransport": "disabled",
}
for key, value in expected.items():
if health.get(key) != value:
die(f"VPS tracker ingress health contract mismatch: {key}")
tracker = health.get("tracker") or {}
for key, value in {
"adapter": "arusnavi-b2",
"protocolProfile": "arusnavi.b2.internal.v1",
"framing": "verified-read-only",
"publicIngress": "telemetry-ingest",
"commandTransport": "disabled",
}.items():
if tracker.get(key) != value:
die(f"VPS tracker adapter health contract mismatch: {key}")
for port in (22, CHANNEL_PUBLIC_PORT, 9921):
if not port_is_open(PUBLIC_IPV4, port, timeout=5):
die(f"VPS required public listener is unavailable: {port}")
if not port_is_open("127.0.0.1", 18221, timeout=5):
die("VPS tracker adapter health listener is unavailable")
if port_is_open("127.0.0.1", 1055):
die("VPS retired Tailscale SOCKS listener returned")
nft = run(["/usr/sbin/nft", "list", "table", "inet", "nodedc_b2_vps"]).stdout
for required in ("policy drop", "tcp dport 22", "tcp dport 443", "tcp dport 9921"):
if required not in nft:
die(f"VPS tracker ingress firewall contract mismatch: {required}")
unit = run([
"/usr/bin/systemctl",
"show",
"nodedc-device-edge-channel.service",
"--property=User,Group,NoNewPrivileges,CapabilityBoundingSet,AmbientCapabilities,MemoryMax,MemorySwapMax,CPUQuotaPerSecUSec,TasksMax,LimitNOFILE",
]).stdout
for required in (
"User=nodedc-channel",
"Group=nodedc-channel",
"NoNewPrivileges=yes",
"CapabilityBoundingSet=cap_net_bind_service",
"AmbientCapabilities=cap_net_bind_service",
"MemoryMax=201326592",
"MemorySwapMax=0",
"TasksMax=128",
"LimitNOFILE=1024",
):
if required not in unit:
die(f"VPS tracker ingress resource boundary mismatch: {required}")
return health
def validate_relay_runtime():
validate_backhaul_runtime()
source_file_state("relay")
@@ -1621,7 +1779,7 @@ def plan_artifact(artifact_argument: str):
print("services=nodedc-device-edge-channel")
print(f"channel_runtime_identity={CHANNEL_USER}:host-local-private-key")
print("peer_trust=preprovisioned-pinned-self-signed-core-certificate+fingerprint")
else:
elif phase == "tailscale-retirement":
print("public_core_channel=preserved:155.212.211.15:443/tcp:tls13-mtls-h2")
print(f"health=preserved:127.0.0.1:{CHANNEL_HEALTH_PORT}")
print("tailscale=stop+disable+destroy-local-runtime-state")
@@ -1631,6 +1789,15 @@ def plan_artifact(artifact_argument: str):
print("tracker_tcp_9921=closed")
print("services=preserved:nodedc-device-edge-channel")
print("external_tailnet_machine_cleanup=required-after-deploy-ok")
else:
print("public_core_channel=preserved:155.212.211.15:443/tcp:tls13-mtls-h2")
print(f"health=127.0.0.1:{CHANNEL_HEALTH_PORT}:combined-edge-runtime")
print("public_b2_ingress=155.212.211.15:9921/tcp:telemetry-only")
print("tracker_adapter=arusnavi-b2:profile=arusnavi.b2.internal.v1")
print("tracker_ack=after-core-durable-acceptance-only")
print("runtime_composition=single-non-root-process:core-channel+universal-gateway")
print("tailscale=preserved:absent")
print("services=recreate:nodedc-device-edge-channel")
print("command_transport=disabled")
print("gelios=untouched")
print("dns=unchanged")
@@ -1666,8 +1833,10 @@ def apply_artifact(artifact_argument: str):
apply_relay(loaded["payload"])
elif loaded["phase"] == "core-channel":
apply_core_channel(loaded["payload"])
else:
elif loaded["phase"] == "tailscale-retirement":
apply_tailscale_retirement(loaded["payload"])
else:
apply_tracker_ingress(loaded["payload"])
archived = archive_artifact(loaded["artifact"], APPLIED_ROOT)
record = {
@@ -99,6 +99,7 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
"relay",
"core-channel",
"tailscale-retirement",
"tracker-ingress",
):
with self.subTest(phase=phase), tempfile.TemporaryDirectory(
prefix=f"nodedc-vps-{phase}-"
@@ -178,6 +179,7 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
"relay",
"core-channel",
"tailscale-retirement",
"tracker-ingress",
):
result = self.build(
inbox,
@@ -361,6 +363,45 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
self.assertIn("command_transport=disabled", rendered)
self.assertIn("gelios=untouched", rendered)
def test_tracker_ingress_plan_is_single_process_bounded_and_command_free(self):
with tempfile.TemporaryDirectory(prefix="nodedc-vps-ingress-plan-") as directory:
inbox = Path(directory) / "inbox"
inbox.mkdir()
result = self.build(
inbox,
"tracker-ingress",
"device-edge-vps-tracker-ingress-plan-001",
)
self.assertEqual(result.returncode, 0, result.stderr)
artifact = Path(json.loads(result.stdout)["artifact"])
old_inbox = RUNNER.INBOX_ROOT
RUNNER.INBOX_ROOT = inbox
try:
with patch.object(RUNNER, "assert_root"), patch.object(
RUNNER,
"preflight",
return_value={"predecessor": "accepted-tailscale-retirement-011"},
), patch("builtins.print") as output:
RUNNER.plan_artifact(str(artifact))
finally:
RUNNER.INBOX_ROOT = old_inbox
rendered = "\n".join(
" ".join(str(arg) for arg in call.args)
for call in output.call_args_list
)
self.assertIn("phase=tracker-ingress", rendered)
self.assertIn("predecessor=accepted-tailscale-retirement-011", rendered)
self.assertIn(
"public_b2_ingress=155.212.211.15:9921/tcp:telemetry-only",
rendered,
)
self.assertIn("tracker_adapter=arusnavi-b2", rendered)
self.assertIn("tracker_ack=after-core-durable-acceptance-only", rendered)
self.assertIn("runtime_composition=single-non-root-process", rendered)
self.assertIn("tailscale=preserved:absent", rendered)
self.assertIn("command_transport=disabled", rendered)
self.assertIn("gelios=untouched", rendered)
def test_publish_payload_preserves_unselected_executable_modes(self):
with tempfile.TemporaryDirectory(prefix="nodedc-vps-publish-scope-") as directory:
root = Path(directory)
@@ -435,6 +476,7 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
foundation = (source_root / "vps/config/nftables-foundation.conf").read_text()
relay = (source_root / "vps/config/nftables-relay.conf").read_text()
channel = (source_root / "vps/config/nftables-core-channel.conf").read_text()
tracker = (source_root / "vps/config/nftables-tracker-ingress.conf").read_text()
sshd = (source_root / "vps/config/00-nodedc-b2-vps.conf").read_text()
backhaul = (source_root / "vps/config/backhaul_ssh_config").read_text()
tailscale_unit = (
@@ -447,6 +489,9 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
channel_unit = (
source_root / "vps/systemd/nodedc-device-edge-channel.service"
).read_text()
tracker_unit = (
source_root / "vps/systemd/nodedc-device-edge-runtime.service"
).read_text()
self.assertIn("policy drop", foundation)
self.assertIn("tcp dport 22", foundation)
@@ -454,6 +499,8 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
self.assertIn("tcp dport 9921", relay)
self.assertIn("tcp dport 443", channel)
self.assertNotIn("tcp dport 9921", channel)
self.assertIn("tcp dport 443", tracker)
self.assertIn("tcp dport 9921", tracker)
self.assertIn("PasswordAuthentication no", sshd)
self.assertIn("AllowTcpForwarding no", sshd)
self.assertIn("StrictHostKeyChecking yes", backhaul)
@@ -490,6 +537,17 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
self.assertIn("LimitNOFILE=1024", channel_unit)
self.assertNotIn("LocalForward", channel_unit)
self.assertNotIn("DEVICE_EDGE_RELAY_UPSTREAM", channel_unit)
self.assertIn("User=nodedc-channel", tracker_unit)
self.assertIn("vps/edge-process/device-edge-runtime.mjs", tracker_unit)
self.assertIn("DEVICE_GATEWAY_TCP_PORT=9921", tracker_unit)
self.assertIn("DEVICE_GATEWAY_MAX_SESSIONS=128", tracker_unit)
self.assertIn("MemoryMax=192M", tracker_unit)
self.assertIn("MemorySwapMax=0", tracker_unit)
self.assertIn("CPUQuota=75%", tracker_unit)
self.assertIn("TasksMax=128", tracker_unit)
self.assertIn("LimitNOFILE=1024", tracker_unit)
self.assertNotIn("LocalForward", tracker_unit)
self.assertNotIn("DEVICE_EDGE_RELAY_UPSTREAM", tracker_unit)
def test_runner_has_registered_rollback_and_no_generic_latest(self):
source = RUNNER_PATH.read_text(encoding="utf-8")