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() {
return {
adapter: config.adapter?.adapterRef ?? "disabled",
protocolProfile: config.profile?.profileRef ?? "disabled",
framing: config.profile?.framing?.status ?? "disabled",
activeSessions: sessions.size,
totalAccepted,
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