feat(device-plane): harden edge channel rotation

This commit is contained in:
Codex
2026-08-11 19:37:32 +03:00
parent 50b9179fe4
commit 1124c15216
7 changed files with 365 additions and 34 deletions
@@ -8,6 +8,7 @@ import {
createChannelEnvelopeDecoder,
encodeChannelEnvelope,
nextReconnectDelay,
normalizeCertificateIdentities,
normalizeCertificateFingerprint,
} from "../../../packages/device-edge-channel-contract/src/index.mjs";
import {
@@ -80,6 +81,10 @@ export function createDeviceGatewayCoreChannelClient(options = {}) {
channel: state?.ready ? "accepted" : state ? "connecting" : "absent",
edgeRegistrationId: state?.registration?.edgeRegistrationId ?? null,
channelGeneration: state?.channelGeneration ?? null,
edgeTrustGeneration: state?.observedEdgeIdentity?.generationRef ?? null,
edgeCertificateFingerprint:
state?.observedEdgeIdentity?.fingerprint ?? null,
activeTrackerSessionChains: state?.sessionChains.size ?? 0,
connectionAttempts: totalConnectionAttempts,
channelsAccepted: totalChannelsAccepted,
reconnects: totalReconnects,
@@ -117,7 +122,9 @@ export function createDeviceGatewayCoreChannelClient(options = {}) {
direction: "edge-to-core",
maxEnvelopeBytes: config.maxEnvelopeBytes,
}),
sessionChains: new Map(),
channelGeneration: null,
observedEdgeIdentity: null,
edgeSequence: 0,
coreSequence: 0,
lastEdgeActivityAt: config.clock(),
@@ -220,6 +227,8 @@ export function createDeviceGatewayCoreChannelClient(options = {}) {
if (
envelope.payload?.status !== "ready"
|| envelope.payload?.transport !== "http2-mtls"
|| envelope.payload?.trustGeneration
!== connection.observedEdgeIdentity?.generationRef
|| envelope.payload?.commandTransport !== "disabled"
) {
throw new Error("device_gateway_core_channel_hello_invalid");
@@ -242,17 +251,34 @@ export function createDeviceGatewayCoreChannelClient(options = {}) {
return;
}
if (envelope.messageKind === "channel.heartbeat") return;
if (envelope.messageKind === "discovery.observed") {
await acceptDiscovery(connection, envelope);
return;
}
if (envelope.messageKind === "adapter.message") {
await acceptAdapterMessage(connection, envelope);
if (["discovery.observed", "adapter.message"].includes(envelope.messageKind)) {
scheduleTrackerEvent(connection, envelope);
return;
}
throw new Error("device_gateway_core_edge_message_unhandled");
}
function scheduleTrackerEvent(connection, envelope) {
if (envelope.trackerSessionId === CHANNEL_TRACKER_SESSION_ID) {
throw new Error("device_gateway_core_tracker_session_invalid");
}
const previous = connection.sessionChains.get(envelope.trackerSessionId);
if (!previous && connection.sessionChains.size >= 128) {
throw new Error("device_gateway_core_tracker_session_limit_reached");
}
const work = (previous ?? Promise.resolve())
.then(() => envelope.messageKind === "discovery.observed"
? acceptDiscovery(connection, envelope)
: acceptAdapterMessage(connection, envelope))
.catch((error) => failConnection(connection, error))
.finally(() => {
if (connection.sessionChains.get(envelope.trackerSessionId) === work) {
connection.sessionChains.delete(envelope.trackerSessionId);
}
});
connection.sessionChains.set(envelope.trackerSessionId, work);
}
async function acceptDiscovery(connection, envelope) {
try {
const signal = normalizeDiscoverySignal(envelope.payload?.signal);
@@ -352,9 +378,13 @@ export function createDeviceGatewayCoreChannelClient(options = {}) {
const observed = normalizeCertificateFingerprint(
socket.getPeerCertificate()?.fingerprint256,
);
if (observed !== connection.registration.certificateFingerprint) {
const identity = connection.registration.certificateIdentities.find(
(candidate) => candidate.fingerprint === observed,
);
if (!identity) {
throw new Error("device_gateway_core_edge_identity_mismatch");
}
connection.observedEdgeIdentity = identity;
}
function failConnection(connection, error) {
@@ -369,6 +399,7 @@ export function createDeviceGatewayCoreChannelClient(options = {}) {
connection.closed = true;
clearInterval(connection.heartbeatTimer);
connection.heartbeatTimer = null;
connection.sessionChains.clear();
try {
connection.request?.close();
} catch {}
@@ -520,8 +551,8 @@ function normalizeRegistration(value) {
),
endpoint: endpoint.toString(),
servername,
certificateFingerprint: normalizeCertificateFingerprint(
value.certificateFingerprint,
certificateIdentities: normalizeCertificateIdentities(
value.certificateIdentities,
),
lifecycleState: value.lifecycleState,
});