feat(device-plane): harden edge channel rotation
This commit is contained in:
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user