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
@@ -0,0 +1,69 @@
{
"schemaVersion": "nodedc.device-edge-vps.tracker-ingress.v1",
"mode": "provider-neutral-allowlisted-adapter-ingress-over-accepted-core-channel",
"status": "active-tracker-ingress",
"authority": "DCPLATFORM-21/DCPLATFORM-76/ADR-0001",
"component": "device-edge-vps",
"phase": "tracker-ingress",
"runtimeHost": "koffyvngij",
"predecessorPatch": "device-edge-vps-tailscale-retirement-20260812-011",
"predecessorArtifactSha256": "e7b61ec9c83122fa5631467010eff871b98935746df6a1326b6ff6bb9713d877",
"runtimeUser": "nodedc-channel",
"runtimeService": "nodedc-device-edge-channel.service",
"runtimeComposition": "single-process-core-channel-plus-universal-device-gateway",
"publicIngress": "tcp/443-mtls-core-channel+tcp/9921-tracker-telemetry",
"trackerIngress": "enabled:allowlisted-adapters-only",
"initialAdapterProfile": "arusnavi.b2.internal.v1",
"acknowledgementBoundary": "tracker-ack-only-after-core-durable-acceptance",
"health": "127.0.0.1:18222",
"adapterHealth": "127.0.0.1:18221",
"rawDeviceTcp9921": "public-telemetry-ingest",
"commandTransport": "disabled",
"gelios": "untouched",
"tailscale": "absent",
"dataBoundary": "no-vps-database-no-business-logic-no-synology-route",
"resourceCeilings": {
"memory": "192M",
"swap": "0",
"cpu": "75%",
"tasks": 128,
"openFiles": 1024,
"sessions": 128,
"sessionsPerAddress": 16,
"connectionsPerMinutePerAddress": 60,
"sessionBufferBytes": 65536,
"aggregateBufferBytes": 33554432
},
"preserved": [
"management-ssh-key",
"accepted-node-runtime",
"accepted-core-channel-trust-and-registration",
"retired-tailnet-boundary",
"gelios-production-path"
],
"forbidden": [
"vps-initiated-synology-connection",
"generic-tcp-forwarding",
"tailscale-runtime",
"docker",
"public-health",
"vps-database",
"vps-business-logic",
"unregistered-adapter",
"device-command"
],
"acceptance": [
"exact-tailscale-retirement-011-predecessor",
"single-non-root-edge-process",
"core-channel-remains-accepted",
"public-tracker-tcp-9921-listening",
"adapter-profile-allowlisted",
"bounded-session-and-buffer-limits",
"tracker-ack-after-core-acceptance",
"tailscale-remains-absent",
"no-vps-to-synology-route",
"command-transport-disabled",
"gelios-untouched"
],
"rollback": "close-9921-restore-exact-tailscale-retirement-011-source-unit-firewall-and-accepted-core-channel-runtime"
}
@@ -0,0 +1,32 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import {
normalizeTrackerIngressConfiguration,
} from "../../../vps/edge-process/device-edge-runtime.mjs";
test("VPS tracker ingress resolves one allowlisted profile within bounded limits", () => {
const config = normalizeTrackerIngressConfiguration({}, "edge:moscow-vps-1");
assert.equal(config.protocolProfileRef, "arusnavi.b2.internal.v1");
assert.equal(config.tcpHost, "0.0.0.0");
assert.equal(config.tcpPort, 9921);
assert.equal(config.maxConcurrentSessions, 128);
assert.equal(config.maxSessionsPerAddress, 16);
assert.equal(config.maxConnectionsPerMinutePerAddress, 60);
assert.equal(config.maxAggregateBufferedBytes, 32 * 1024 * 1024);
});
test("VPS tracker ingress rejects hidden bind and non-allowlisted adapters", () => {
assert.throws(() => normalizeTrackerIngressConfiguration({
DEVICE_GATEWAY_TCP_HOST: "127.0.0.1",
}, "edge:moscow-vps-1"), /public_host_invalid/);
assert.throws(() => normalizeTrackerIngressConfiguration({
DEVICE_GATEWAY_PROTOCOL_PROFILE_REF: "vendor.unknown.v1",
}, "edge:moscow-vps-1"), /profile_not_allowlisted/);
assert.throws(() => normalizeTrackerIngressConfiguration({
DEVICE_GATEWAY_MAX_SESSIONS: "129",
}, "edge:moscow-vps-1"), /session_limit_invalid/);
});
@@ -139,6 +139,9 @@ export function createDeviceGatewayRuntime(options = {}) {
}, },
status() { status() {
return { return {
adapter: config.adapter?.adapterRef ?? "disabled",
protocolProfile: config.profile?.profileRef ?? "disabled",
framing: config.profile?.framing?.status ?? "disabled",
activeSessions: sessions.size, activeSessions: sessions.size,
totalAccepted, totalAccepted,
totalRejected, totalRejected,
@@ -0,0 +1,27 @@
#!/usr/sbin/nft -f
flush ruleset
table inet nodedc_b2_vps {
chain input {
type filter hook input priority -10; policy drop;
iifname "lo" accept
ct state invalid drop
ct state established,related accept
ip protocol icmp accept
ip6 nexthdr ipv6-icmp accept
tcp dport 22 ct state new limit rate 30/minute burst 60 packets accept
tcp dport 443 ct state new limit rate 120/minute burst 120 packets accept
tcp dport 9921 ct state new limit rate 600/minute burst 128 packets accept
}
chain forward {
type filter hook forward priority -10; policy drop;
}
chain output {
type filter hook output priority -10; policy accept;
}
}
@@ -0,0 +1,243 @@
import { createServer } from "node:http";
import { pathToFileURL } from "node:url";
import {
DEVICE_ADAPTER_CATALOG,
} from "../../packages/device-adapter-catalog/src/index.mjs";
import {
createDeviceEdgeChannelServer,
} from "../../services/device-edge-channel/src/runtime.mjs";
import {
readRuntimeConfiguration,
} from "../../services/device-edge-channel/src/server.mjs";
import {
createDeviceGatewayRuntime,
} from "../../services/device-gateway/src/runtime.mjs";
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
await main();
}
export async function main(environment = process.env) {
const base = await readRuntimeConfiguration(environment);
const tracker = normalizeTrackerIngressConfiguration(
environment,
base.channel.edgeRegistrationId,
);
const channel = createDeviceEdgeChannelServer(base.channel);
const gateway = createDeviceGatewayRuntime({
listenEnabled: true,
publicIngressEnabled: true,
coreChannelAuthenticated: true,
adapterRegistry: DEVICE_ADAPTER_CATALOG.registry,
protocolProfileRef: tracker.protocolProfileRef,
edgeRef: tracker.edgeRef,
healthHost: tracker.healthHost,
healthPort: tracker.healthPort,
tcpHost: tracker.tcpHost,
tcpPort: tracker.tcpPort,
maxBufferedBytes: tracker.maxBufferedBytes,
maxAggregateBufferedBytes: tracker.maxAggregateBufferedBytes,
maxConcurrentSessions: tracker.maxConcurrentSessions,
maxSessionsPerAddress: tracker.maxSessionsPerAddress,
maxConnectionsPerMinutePerAddress:
tracker.maxConnectionsPerMinutePerAddress,
maxTrackedSourceAddresses: tracker.maxTrackedSourceAddresses,
sessionTimeoutMs: tracker.sessionTimeoutMs,
onDiscovery: (signal) => channel.submitDiscovery(signal),
onMessage: (message) => channel.submitAdapterMessage(message),
});
const health = createCombinedHealthServer(channel, gateway, base.health);
let stopping = false;
try {
await channel.start();
await gateway.start();
await listen(health, base.health.port, base.health.host);
} catch (error) {
await Promise.allSettled([
gateway.stop(),
channel.stop(),
closeServer(health),
]);
throw error;
}
console.log(JSON.stringify({
event: "device_edge_runtime_started",
channel: `${base.channel.host}:${base.channel.port}`,
health: `${base.health.host}:${base.health.port}`,
trackerIngress: `${tracker.tcpHost}:${tracker.tcpPort}`,
adapterProfile: tracker.protocolProfileRef,
edgeRegistrationId: base.channel.edgeRegistrationId,
channelGeneration: base.channel.channelGeneration,
trustGeneration: base.channel.trustGeneration,
commandTransport: "disabled",
}));
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
async function shutdown() {
if (stopping) return;
stopping = true;
await Promise.allSettled([
gateway.stop(),
channel.stop(),
closeServer(health),
]);
process.exit(0);
}
}
export function normalizeTrackerIngressConfiguration(environment = {}, edgeRef) {
return Object.freeze({
edgeRef: normalizeRef(edgeRef, "device_edge_runtime_edge_ref_invalid"),
protocolProfileRef: normalizeProfileRef(
environment.DEVICE_GATEWAY_PROTOCOL_PROFILE_REF
?? DEVICE_ADAPTER_CATALOG.defaultProfileRef,
),
healthHost: normalizeLoopbackHost(
environment.DEVICE_GATEWAY_HEALTH_HOST ?? "127.0.0.1",
),
healthPort: normalizePort(environment.DEVICE_GATEWAY_HEALTH_PORT, 18221),
tcpHost: normalizePublicHost(
environment.DEVICE_GATEWAY_TCP_HOST ?? "0.0.0.0",
),
tcpPort: normalizePort(environment.DEVICE_GATEWAY_TCP_PORT, 9921),
maxBufferedBytes: normalizeInteger(
environment.DEVICE_GATEWAY_MAX_BUFFERED_BYTES,
64 * 1024,
1024,
256 * 1024,
"device_edge_runtime_session_buffer_invalid",
),
maxAggregateBufferedBytes: normalizeInteger(
environment.DEVICE_GATEWAY_MAX_AGGREGATE_BUFFERED_BYTES,
32 * 1024 * 1024,
1024,
32 * 1024 * 1024,
"device_edge_runtime_aggregate_buffer_invalid",
),
maxConcurrentSessions: normalizeInteger(
environment.DEVICE_GATEWAY_MAX_SESSIONS,
128,
1,
128,
"device_edge_runtime_session_limit_invalid",
),
maxSessionsPerAddress: normalizeInteger(
environment.DEVICE_GATEWAY_MAX_SESSIONS_PER_ADDRESS,
16,
1,
16,
"device_edge_runtime_address_session_limit_invalid",
),
maxConnectionsPerMinutePerAddress: normalizeInteger(
environment.DEVICE_GATEWAY_MAX_CONNECTIONS_PER_MINUTE_PER_ADDRESS,
60,
1,
60,
"device_edge_runtime_address_rate_limit_invalid",
),
maxTrackedSourceAddresses: normalizeInteger(
environment.DEVICE_GATEWAY_MAX_TRACKED_SOURCE_ADDRESSES,
2048,
1,
2048,
"device_edge_runtime_source_tracking_limit_invalid",
),
sessionTimeoutMs: normalizeInteger(
environment.DEVICE_GATEWAY_SESSION_TIMEOUT_MS,
10_000,
100,
60_000,
"device_edge_runtime_session_timeout_invalid",
),
});
}
function createCombinedHealthServer(channel, gateway, healthConfig) {
return createServer((request, response) => {
response.setHeader("Content-Type", "application/json; charset=utf-8");
response.setHeader("Cache-Control", "no-store");
response.setHeader("X-Content-Type-Options", "nosniff");
if (request.method !== "GET" || request.url !== "/healthz") {
response.statusCode = 404;
response.end(JSON.stringify({ ok: false, error: "not_found" }));
return;
}
response.statusCode = 200;
response.end(JSON.stringify({
ok: true,
service: "nodedc-device-edge-runtime",
health: `${healthConfig.host}:${healthConfig.port}`,
...channel.status(),
trackerIngress: "telemetry-ingest",
tracker: gateway.status(),
commandTransport: "disabled",
}));
});
}
function normalizeRef(value, errorCode) {
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) {
throw new TypeError(errorCode);
}
return value;
}
function normalizeProfileRef(value) {
if (typeof value !== "string" || !/^[a-z][a-z0-9._-]{2,127}$/.test(value)) {
throw new TypeError("device_edge_runtime_profile_ref_invalid");
}
DEVICE_ADAPTER_CATALOG.registry.resolveProfile(value);
return value;
}
function normalizeLoopbackHost(value) {
if (!["127.0.0.1", "::1"].includes(value)) {
throw new TypeError("device_edge_runtime_health_host_invalid");
}
return value;
}
function normalizePublicHost(value) {
if (!["0.0.0.0", "::"].includes(value)) {
throw new TypeError("device_edge_runtime_public_host_invalid");
}
return value;
}
function normalizePort(value, fallback) {
return normalizeInteger(
value,
fallback,
1,
65_535,
"device_edge_runtime_port_invalid",
);
}
function normalizeInteger(value, fallback, minimum, maximum, errorCode) {
const normalized = Number(value ?? fallback);
if (!Number.isSafeInteger(normalized) || normalized < minimum || normalized > maximum) {
throw new TypeError(errorCode);
}
return normalized;
}
function listen(server, port, host) {
return new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(port, host, () => {
server.off("error", reject);
resolve();
});
});
}
function closeServer(server) {
if (!server.listening) return Promise.resolve();
return new Promise((resolve) => server.close(() => resolve()));
}
@@ -0,0 +1,62 @@
[Unit]
Description=NODE.DC provider-neutral Device Edge runtime
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=nodedc-channel
Group=nodedc-channel
ExecStart=/opt/nodedc-b2-vps/runtime/node/bin/node /opt/nodedc-b2-vps/vps/edge-process/device-edge-runtime.mjs
Environment=DEVICE_EDGE_CHANNEL_HOST=0.0.0.0
Environment=DEVICE_EDGE_CHANNEL_PORT=443
Environment=DEVICE_EDGE_CHANNEL_HEALTH_HOST=127.0.0.1
Environment=DEVICE_EDGE_CHANNEL_HEALTH_PORT=18222
Environment=DEVICE_EDGE_CHANNEL_CONFIG_FILE=/var/lib/nodedc-b2-vps/channel-trust/runtime.json
Environment=DEVICE_EDGE_CHANNEL_KEY_FILE=/var/lib/nodedc-b2-vps/channel-trust/edge-private-key.pem
Environment=DEVICE_EDGE_CHANNEL_CERTIFICATE_FILE=/var/lib/nodedc-b2-vps/channel-trust/edge-certificate.pem
Environment=DEVICE_EDGE_CHANNEL_CORE_TRUST_FILE=/var/lib/nodedc-b2-vps/channel-trust/core-certificate.pem
Environment=DEVICE_GATEWAY_PROTOCOL_PROFILE_REF=arusnavi.b2.internal.v1
Environment=DEVICE_GATEWAY_HEALTH_HOST=127.0.0.1
Environment=DEVICE_GATEWAY_HEALTH_PORT=18221
Environment=DEVICE_GATEWAY_TCP_HOST=0.0.0.0
Environment=DEVICE_GATEWAY_TCP_PORT=9921
Environment=DEVICE_GATEWAY_MAX_BUFFERED_BYTES=65536
Environment=DEVICE_GATEWAY_MAX_AGGREGATE_BUFFERED_BYTES=33554432
Environment=DEVICE_GATEWAY_MAX_SESSIONS=128
Environment=DEVICE_GATEWAY_MAX_SESSIONS_PER_ADDRESS=16
Environment=DEVICE_GATEWAY_MAX_CONNECTIONS_PER_MINUTE_PER_ADDRESS=60
Environment=DEVICE_GATEWAY_MAX_TRACKED_SOURCE_ADDRESSES=2048
Environment=DEVICE_GATEWAY_SESSION_TIMEOUT_MS=10000
Restart=always
RestartSec=2
TimeoutStartSec=20
TimeoutStopSec=15
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
RestrictSUIDSGID=yes
RestrictRealtime=yes
LockPersonality=yes
MemoryDenyWriteExecute=no
SystemCallArchitectures=native
RestrictAddressFamilies=AF_INET AF_INET6
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
UMask=0077
MemoryMax=192M
MemorySwapMax=0
CPUQuota=75%
TasksMax=128
LimitNOFILE=1024
[Install]
WantedBy=multi-user.target
@@ -36,11 +36,12 @@ if (
"relay", "relay",
"core-channel", "core-channel",
"tailscale-retirement", "tailscale-retirement",
"tracker-ingress",
].includes(phase) ].includes(phase)
|| !/^[A-Za-z0-9._-]{1,96}$/.test(patchId || "") || !/^[A-Za-z0-9._-]{1,96}$/.test(patchId || "")
) { ) {
throw new Error( 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": [ "tailscale-retirement": [
"deployment/device-edge-vps-tailscale-retirement-v1.json", "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 entries = entriesByPhase[phase];
const ignoredBasenames = new Set([".DS_Store", ".git", "node_modules"]); const ignoredBasenames = new Set([".DS_Store", ".git", "node_modules"]);
@@ -157,6 +171,8 @@ try {
? "tcp/9921" ? "tcp/9921"
: ["core-channel", "tailscale-retirement"].includes(phase) : ["core-channel", "tailscale-retirement"].includes(phase)
? "tcp/443-mtls-only" ? "tcp/443-mtls-only"
: phase === "tracker-ingress"
? "tcp/443-mtls+tcp/9921-telemetry"
: "disabled", : "disabled",
commandTransport: "disabled", commandTransport: "disabled",
gelios: "untouched", 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() { 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 = ( CORE_CHANNEL_ACCEPTED_SHA256 = (
"c8ef3c4bb45850cad32e881eba081bc4c891c2886e5500d02cb94616d82353f3" "c8ef3c4bb45850cad32e881eba081bc4c891c2886e5500d02cb94616d82353f3"
) )
TAILSCALE_RETIREMENT_ACCEPTED_PATCH = (
"device-edge-vps-tailscale-retirement-20260812-011"
)
TAILSCALE_RETIREMENT_ACCEPTED_SHA256 = (
"e7b61ec9c83122fa5631467010eff871b98935746df6a1326b6ff6bb9713d877"
)
FOUNDATION_ENTRIES = ( FOUNDATION_ENTRIES = (
"vps/config/00-nodedc-b2-vps.conf", "vps/config/00-nodedc-b2-vps.conf",
@@ -143,6 +149,19 @@ CORE_CHANNEL_ENTRIES = (
TAILSCALE_RETIREMENT_ENTRIES = ( TAILSCALE_RETIREMENT_ENTRIES = (
"deployment/device-edge-vps-tailscale-retirement-v1.json", "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 = { PHASE_ENTRIES = {
"foundation": FOUNDATION_ENTRIES, "foundation": FOUNDATION_ENTRIES,
@@ -151,6 +170,7 @@ PHASE_ENTRIES = {
"relay": RELAY_ENTRIES, "relay": RELAY_ENTRIES,
"core-channel": CORE_CHANNEL_ENTRIES, "core-channel": CORE_CHANNEL_ENTRIES,
"tailscale-retirement": TAILSCALE_RETIREMENT_ENTRIES, "tailscale-retirement": TAILSCALE_RETIREMENT_ENTRIES,
"tracker-ingress": TRACKER_INGRESS_ENTRIES,
} }
SUPERSEDED_TRANSPORT_PHASES = frozenset({"backhaul", "relay"}) SUPERSEDED_TRANSPORT_PHASES = frozenset({"backhaul", "relay"})
@@ -218,6 +238,30 @@ PHASE_FILE_SHA256 = {
"deployment/device-edge-vps-tailscale-retirement-v1.json": "deployment/device-edge-vps-tailscale-retirement-v1.json":
"bbe11e8cf4103f44ae7888b4d3f3dde7015eeaef8ea5a7c4c00427a7b85f8e33", "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. # Exact immutable baselines from terminally accepted predecessor artifacts.
@@ -617,6 +661,19 @@ def current_phase_preflight(phase: str):
return { return {
"predecessor": "failed-core-channel-001-rollback-runtime-mode-drift", "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( validate_foundation_runtime(
require_running_tailnet=phase in {"backhaul", "relay"}, require_running_tailnet=phase in {"backhaul", "relay"},
expected_key_user=BACKHAUL_USER if phase == "relay" else SERVICE_USER, 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, TAILSCALE_BIN.parent,
TRUST_ROOT, TRUST_ROOT,
] ]
if phase == "tracker-ingress":
return common + [CHANNEL_UNIT, NFTABLES_CONFIG]
return common + [RELAY_UNIT, NFTABLES_CONFIG] return common + [RELAY_UNIT, NFTABLES_CONFIG]
@@ -1194,6 +1253,18 @@ def apply_tailscale_retirement(_payload: Path):
validate_tailscale_retirement_runtime() 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(): def sshd_effective():
return run(["/usr/sbin/sshd", "-T"]).stdout.lower() return run(["/usr/sbin/sshd", "-T"]).stdout.lower()
@@ -1462,6 +1533,93 @@ def validate_tailscale_retirement_runtime():
return health 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(): def validate_relay_runtime():
validate_backhaul_runtime() validate_backhaul_runtime()
source_file_state("relay") source_file_state("relay")
@@ -1621,7 +1779,7 @@ def plan_artifact(artifact_argument: str):
print("services=nodedc-device-edge-channel") print("services=nodedc-device-edge-channel")
print(f"channel_runtime_identity={CHANNEL_USER}:host-local-private-key") print(f"channel_runtime_identity={CHANNEL_USER}:host-local-private-key")
print("peer_trust=preprovisioned-pinned-self-signed-core-certificate+fingerprint") 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("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(f"health=preserved:127.0.0.1:{CHANNEL_HEALTH_PORT}")
print("tailscale=stop+disable+destroy-local-runtime-state") print("tailscale=stop+disable+destroy-local-runtime-state")
@@ -1631,6 +1789,15 @@ def plan_artifact(artifact_argument: str):
print("tracker_tcp_9921=closed") print("tracker_tcp_9921=closed")
print("services=preserved:nodedc-device-edge-channel") print("services=preserved:nodedc-device-edge-channel")
print("external_tailnet_machine_cleanup=required-after-deploy-ok") 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("command_transport=disabled")
print("gelios=untouched") print("gelios=untouched")
print("dns=unchanged") print("dns=unchanged")
@@ -1666,8 +1833,10 @@ def apply_artifact(artifact_argument: str):
apply_relay(loaded["payload"]) apply_relay(loaded["payload"])
elif loaded["phase"] == "core-channel": elif loaded["phase"] == "core-channel":
apply_core_channel(loaded["payload"]) apply_core_channel(loaded["payload"])
else: elif loaded["phase"] == "tailscale-retirement":
apply_tailscale_retirement(loaded["payload"]) apply_tailscale_retirement(loaded["payload"])
else:
apply_tracker_ingress(loaded["payload"])
archived = archive_artifact(loaded["artifact"], APPLIED_ROOT) archived = archive_artifact(loaded["artifact"], APPLIED_ROOT)
record = { record = {
@@ -99,6 +99,7 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
"relay", "relay",
"core-channel", "core-channel",
"tailscale-retirement", "tailscale-retirement",
"tracker-ingress",
): ):
with self.subTest(phase=phase), tempfile.TemporaryDirectory( with self.subTest(phase=phase), tempfile.TemporaryDirectory(
prefix=f"nodedc-vps-{phase}-" prefix=f"nodedc-vps-{phase}-"
@@ -178,6 +179,7 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
"relay", "relay",
"core-channel", "core-channel",
"tailscale-retirement", "tailscale-retirement",
"tracker-ingress",
): ):
result = self.build( result = self.build(
inbox, inbox,
@@ -361,6 +363,45 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
self.assertIn("command_transport=disabled", rendered) self.assertIn("command_transport=disabled", rendered)
self.assertIn("gelios=untouched", 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): def test_publish_payload_preserves_unselected_executable_modes(self):
with tempfile.TemporaryDirectory(prefix="nodedc-vps-publish-scope-") as directory: with tempfile.TemporaryDirectory(prefix="nodedc-vps-publish-scope-") as directory:
root = Path(directory) root = Path(directory)
@@ -435,6 +476,7 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
foundation = (source_root / "vps/config/nftables-foundation.conf").read_text() foundation = (source_root / "vps/config/nftables-foundation.conf").read_text()
relay = (source_root / "vps/config/nftables-relay.conf").read_text() relay = (source_root / "vps/config/nftables-relay.conf").read_text()
channel = (source_root / "vps/config/nftables-core-channel.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() sshd = (source_root / "vps/config/00-nodedc-b2-vps.conf").read_text()
backhaul = (source_root / "vps/config/backhaul_ssh_config").read_text() backhaul = (source_root / "vps/config/backhaul_ssh_config").read_text()
tailscale_unit = ( tailscale_unit = (
@@ -447,6 +489,9 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
channel_unit = ( channel_unit = (
source_root / "vps/systemd/nodedc-device-edge-channel.service" source_root / "vps/systemd/nodedc-device-edge-channel.service"
).read_text() ).read_text()
tracker_unit = (
source_root / "vps/systemd/nodedc-device-edge-runtime.service"
).read_text()
self.assertIn("policy drop", foundation) self.assertIn("policy drop", foundation)
self.assertIn("tcp dport 22", 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 9921", relay)
self.assertIn("tcp dport 443", channel) self.assertIn("tcp dport 443", channel)
self.assertNotIn("tcp dport 9921", 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("PasswordAuthentication no", sshd)
self.assertIn("AllowTcpForwarding no", sshd) self.assertIn("AllowTcpForwarding no", sshd)
self.assertIn("StrictHostKeyChecking yes", backhaul) self.assertIn("StrictHostKeyChecking yes", backhaul)
@@ -490,6 +537,17 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
self.assertIn("LimitNOFILE=1024", channel_unit) self.assertIn("LimitNOFILE=1024", channel_unit)
self.assertNotIn("LocalForward", channel_unit) self.assertNotIn("LocalForward", channel_unit)
self.assertNotIn("DEVICE_EDGE_RELAY_UPSTREAM", 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): def test_runner_has_registered_rollback_and_no_generic_latest(self):
source = RUNNER_PATH.read_text(encoding="utf-8") source = RUNNER_PATH.read_text(encoding="utf-8")