@matter/protocol 0.16.8-alpha.0-20260123-dff2cae52 → 0.16.8-alpha.0-20260125-38e62bc3e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/action/client/ClientInteraction.d.ts +4 -4
- package/dist/cjs/action/client/ClientInteraction.d.ts.map +1 -1
- package/dist/cjs/action/client/ClientInteraction.js +48 -6
- package/dist/cjs/action/client/ClientInteraction.js.map +1 -1
- package/dist/cjs/action/client/QueuedClientInteraction.d.ts +0 -1
- package/dist/cjs/action/client/QueuedClientInteraction.d.ts.map +1 -1
- package/dist/cjs/action/client/QueuedClientInteraction.js +0 -1
- package/dist/cjs/action/client/QueuedClientInteraction.js.map +1 -1
- package/dist/cjs/action/client/subscription/ClientSubscriptionHandler.d.ts.map +1 -1
- package/dist/cjs/action/client/subscription/ClientSubscriptionHandler.js +5 -2
- package/dist/cjs/action/client/subscription/ClientSubscriptionHandler.js.map +1 -1
- package/dist/cjs/action/server/AttributeWriteResponse.d.ts +1 -1
- package/dist/cjs/action/server/AttributeWriteResponse.d.ts.map +1 -1
- package/dist/cjs/action/server/AttributeWriteResponse.js +0 -6
- package/dist/cjs/action/server/AttributeWriteResponse.js.map +1 -1
- package/dist/cjs/action/server/DataResponse.d.ts +5 -0
- package/dist/cjs/action/server/DataResponse.d.ts.map +1 -1
- package/dist/cjs/action/server/DataResponse.js +7 -0
- package/dist/cjs/action/server/DataResponse.js.map +1 -1
- package/dist/cjs/action/server/ServerInteraction.js.map +1 -1
- package/dist/cjs/interaction/InteractionMessenger.d.ts +30 -30
- package/dist/cjs/interaction/InteractionMessenger.d.ts.map +1 -1
- package/dist/cjs/interaction/InteractionMessenger.js +81 -12
- package/dist/cjs/interaction/InteractionMessenger.js.map +1 -1
- package/dist/cjs/mdns/MdnsClient.d.ts.map +1 -1
- package/dist/cjs/mdns/MdnsClient.js +157 -100
- package/dist/cjs/mdns/MdnsClient.js.map +1 -1
- package/dist/cjs/mdns/MdnsServer.d.ts +2 -0
- package/dist/cjs/mdns/MdnsServer.d.ts.map +1 -1
- package/dist/cjs/mdns/MdnsServer.js +45 -5
- package/dist/cjs/mdns/MdnsServer.js.map +1 -1
- package/dist/cjs/peer/ControllerCommissioningFlow.d.ts.map +1 -1
- package/dist/cjs/peer/ControllerCommissioningFlow.js +3 -1
- package/dist/cjs/peer/ControllerCommissioningFlow.js.map +1 -1
- package/dist/esm/action/client/ClientInteraction.d.ts +4 -4
- package/dist/esm/action/client/ClientInteraction.d.ts.map +1 -1
- package/dist/esm/action/client/ClientInteraction.js +49 -6
- package/dist/esm/action/client/ClientInteraction.js.map +1 -1
- package/dist/esm/action/client/QueuedClientInteraction.d.ts +0 -1
- package/dist/esm/action/client/QueuedClientInteraction.d.ts.map +1 -1
- package/dist/esm/action/client/QueuedClientInteraction.js +0 -1
- package/dist/esm/action/client/QueuedClientInteraction.js.map +1 -1
- package/dist/esm/action/client/subscription/ClientSubscriptionHandler.d.ts.map +1 -1
- package/dist/esm/action/client/subscription/ClientSubscriptionHandler.js +5 -2
- package/dist/esm/action/client/subscription/ClientSubscriptionHandler.js.map +1 -1
- package/dist/esm/action/server/AttributeWriteResponse.d.ts +1 -1
- package/dist/esm/action/server/AttributeWriteResponse.d.ts.map +1 -1
- package/dist/esm/action/server/AttributeWriteResponse.js +0 -6
- package/dist/esm/action/server/AttributeWriteResponse.js.map +1 -1
- package/dist/esm/action/server/DataResponse.d.ts +5 -0
- package/dist/esm/action/server/DataResponse.d.ts.map +1 -1
- package/dist/esm/action/server/DataResponse.js +7 -0
- package/dist/esm/action/server/DataResponse.js.map +1 -1
- package/dist/esm/action/server/ServerInteraction.js.map +1 -1
- package/dist/esm/interaction/InteractionMessenger.d.ts +30 -30
- package/dist/esm/interaction/InteractionMessenger.d.ts.map +1 -1
- package/dist/esm/interaction/InteractionMessenger.js +82 -12
- package/dist/esm/interaction/InteractionMessenger.js.map +1 -1
- package/dist/esm/mdns/MdnsClient.d.ts.map +1 -1
- package/dist/esm/mdns/MdnsClient.js +157 -100
- package/dist/esm/mdns/MdnsClient.js.map +1 -1
- package/dist/esm/mdns/MdnsServer.d.ts +2 -0
- package/dist/esm/mdns/MdnsServer.d.ts.map +1 -1
- package/dist/esm/mdns/MdnsServer.js +45 -5
- package/dist/esm/mdns/MdnsServer.js.map +1 -1
- package/dist/esm/peer/ControllerCommissioningFlow.d.ts.map +1 -1
- package/dist/esm/peer/ControllerCommissioningFlow.js +3 -1
- package/dist/esm/peer/ControllerCommissioningFlow.js.map +1 -1
- package/package.json +6 -6
- package/src/action/client/ClientInteraction.ts +62 -6
- package/src/action/client/QueuedClientInteraction.ts +0 -1
- package/src/action/client/subscription/ClientSubscriptionHandler.ts +5 -2
- package/src/action/server/AttributeWriteResponse.ts +4 -16
- package/src/action/server/DataResponse.ts +8 -0
- package/src/action/server/ServerInteraction.ts +2 -2
- package/src/interaction/InteractionMessenger.ts +113 -15
- package/src/mdns/MdnsClient.ts +207 -102
- package/src/mdns/MdnsServer.ts +79 -6
- package/src/peer/ControllerCommissioningFlow.ts +5 -1
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
TlvDataVersionFilter,
|
|
31
31
|
TlvInvokeRequest,
|
|
32
32
|
TlvInvokeResponse,
|
|
33
|
+
TlvInvokeResponseForSend,
|
|
33
34
|
TlvReadRequest,
|
|
34
35
|
TlvSchema,
|
|
35
36
|
TlvStatusResponse,
|
|
@@ -76,6 +77,7 @@ export type SubscribeRequest = TypeFromSchema<typeof TlvSubscribeRequest>;
|
|
|
76
77
|
export type SubscribeResponse = TypeFromSchema<typeof TlvSubscribeResponse>;
|
|
77
78
|
export type InvokeRequest = TypeFromSchema<typeof TlvInvokeRequest>;
|
|
78
79
|
export type InvokeResponse = TypeFromSchema<typeof TlvInvokeResponse>;
|
|
80
|
+
export type InvokeResponseForSend = TypeFromSchema<typeof TlvInvokeResponseForSend>;
|
|
79
81
|
export type TimedRequest = TypeFromSchema<typeof TlvTimedRequest>;
|
|
80
82
|
export type WriteRequest = TypeFromSchema<typeof TlvWriteRequest>;
|
|
81
83
|
export type WriteResponse = TypeFromSchema<typeof TlvWriteResponse>;
|
|
@@ -211,7 +213,12 @@ export interface InteractionRecipient {
|
|
|
211
213
|
request: ReadRequest,
|
|
212
214
|
message: Message,
|
|
213
215
|
): Promise<{ dataReport: DataReport; payload?: DataReportPayloadIterator }>;
|
|
214
|
-
handleWriteRequest(
|
|
216
|
+
handleWriteRequest(
|
|
217
|
+
exchange: MessageExchange,
|
|
218
|
+
request: WriteRequest,
|
|
219
|
+
messenger: InteractionServerMessenger,
|
|
220
|
+
message: Message,
|
|
221
|
+
): Promise<void>;
|
|
215
222
|
handleSubscribeRequest(
|
|
216
223
|
exchange: MessageExchange,
|
|
217
224
|
request: SubscribeRequest,
|
|
@@ -262,11 +269,8 @@ export class InteractionServerMessenger extends InteractionMessenger {
|
|
|
262
269
|
}
|
|
263
270
|
case MessageType.WriteRequest: {
|
|
264
271
|
const writeRequest = TlvWriteRequest.decode(message.payload);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (!suppressResponse && !isGroupSession) {
|
|
268
|
-
await this.send(MessageType.WriteResponse, TlvWriteResponse.encode(writeResponse));
|
|
269
|
-
}
|
|
272
|
+
await recipient.handleWriteRequest(this.exchange, writeRequest, this, message);
|
|
273
|
+
// response is sent by the handler
|
|
270
274
|
break;
|
|
271
275
|
}
|
|
272
276
|
case MessageType.SubscribeRequest: {
|
|
@@ -278,7 +282,7 @@ export class InteractionServerMessenger extends InteractionMessenger {
|
|
|
278
282
|
}
|
|
279
283
|
const subscribeRequest = TlvSubscribeRequest.decode(message.payload);
|
|
280
284
|
await recipient.handleSubscribeRequest(this.exchange, subscribeRequest, this, message);
|
|
281
|
-
// response is sent by handler
|
|
285
|
+
// response is sent by the handler
|
|
282
286
|
break;
|
|
283
287
|
}
|
|
284
288
|
case MessageType.InvokeRequest: {
|
|
@@ -797,6 +801,60 @@ export class InteractionServerMessenger extends InteractionMessenger {
|
|
|
797
801
|
}
|
|
798
802
|
}
|
|
799
803
|
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Send a WriteResponse message.
|
|
807
|
+
*/
|
|
808
|
+
async sendWriteResponse(response: WriteResponse, options?: { logContext?: string }) {
|
|
809
|
+
await this.send(MessageType.WriteResponse, TlvWriteResponse.encode(response), {
|
|
810
|
+
logContext: options?.logContext ? { for: options.logContext } : undefined,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Wait for and decode the next WriteRequest message (for chunked writes).
|
|
816
|
+
*/
|
|
817
|
+
async readNextWriteRequest(): Promise<{ writeRequest: WriteRequest; message: Message }> {
|
|
818
|
+
const message = await this.nextMessage(MessageType.WriteRequest, undefined, "WriteRequest-chunk");
|
|
819
|
+
return {
|
|
820
|
+
writeRequest: TlvWriteRequest.decode(message.payload),
|
|
821
|
+
message,
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Send an intermediate InvokeResponse chunk with moreChunkedMessages=true and wait for Status.Success.
|
|
827
|
+
* Returns true if a client acknowledged, and we should continue, false if a client terminated the chunked series.
|
|
828
|
+
*/
|
|
829
|
+
async sendInvokeResponseChunk(response: InvokeResponseForSend): Promise<boolean> {
|
|
830
|
+
await this.send(
|
|
831
|
+
MessageType.InvokeResponse,
|
|
832
|
+
TlvInvokeResponseForSend.encode({
|
|
833
|
+
...response,
|
|
834
|
+
moreChunkedMessages: true,
|
|
835
|
+
}),
|
|
836
|
+
{
|
|
837
|
+
logContext: { for: "InvokeResponse-chunk" },
|
|
838
|
+
},
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
try {
|
|
842
|
+
await this.waitForSuccess("InvokeResponse-chunk");
|
|
843
|
+
return true; // Continue sending chunks
|
|
844
|
+
} catch (error) {
|
|
845
|
+
// Any non-success status or error terminate further transmission of InvokeResponseMessages,
|
|
846
|
+
// close the exchange, and consider the Invoke Interaction completed.
|
|
847
|
+
logger.debug("Chunked invoke response terminated by client", error);
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Send the final InvokeResponse (without moreChunkedMessages flag).
|
|
854
|
+
*/
|
|
855
|
+
async sendInvokeResponse(response: InvokeResponseForSend) {
|
|
856
|
+
await this.send(MessageType.InvokeResponse, TlvInvokeResponseForSend.encode(response));
|
|
857
|
+
}
|
|
800
858
|
}
|
|
801
859
|
|
|
802
860
|
export class IncomingInteractionClientMessenger extends InteractionMessenger {
|
|
@@ -972,7 +1030,14 @@ export class InteractionClientMessenger extends IncomingInteractionClientMesseng
|
|
|
972
1030
|
await this.send(MessageType.SubscribeRequest, request);
|
|
973
1031
|
}
|
|
974
1032
|
|
|
975
|
-
|
|
1033
|
+
/**
|
|
1034
|
+
* Send an invoke command and handle chunked responses.
|
|
1035
|
+
* Returns a combined InvokeResponse with all responses from all chunks, or undefined if suppressResponse
|
|
1036
|
+
*/
|
|
1037
|
+
async sendInvokeCommand(
|
|
1038
|
+
invokeRequest: InvokeRequest,
|
|
1039
|
+
expectedProcessingTime?: Duration,
|
|
1040
|
+
): Promise<InvokeResponse | undefined> {
|
|
976
1041
|
if (invokeRequest.suppressResponse) {
|
|
977
1042
|
await this.requestWithSuppressedResponse(
|
|
978
1043
|
MessageType.InvokeRequest,
|
|
@@ -980,16 +1045,49 @@ export class InteractionClientMessenger extends IncomingInteractionClientMesseng
|
|
|
980
1045
|
invokeRequest,
|
|
981
1046
|
expectedProcessingTime,
|
|
982
1047
|
);
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1048
|
+
return undefined;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Send invoke request
|
|
1052
|
+
await this.send(MessageType.InvokeRequest, TlvInvokeRequest.encode(invokeRequest), {
|
|
1053
|
+
expectAckOnly: false,
|
|
1054
|
+
expectedProcessingTime,
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
// Receive and accumulate responses from potentially multiple chunks
|
|
1058
|
+
const allInvokeResponses: InvokeResponse["invokeResponses"] = [];
|
|
1059
|
+
let finalResponse: InvokeResponse | undefined;
|
|
1060
|
+
|
|
1061
|
+
while (true) {
|
|
1062
|
+
const responseMessage = await this.nextMessage(
|
|
987
1063
|
MessageType.InvokeResponse,
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
expectedProcessingTime,
|
|
1064
|
+
{ expectedProcessingTime },
|
|
1065
|
+
"InvokeResponse",
|
|
991
1066
|
);
|
|
1067
|
+
const response = TlvInvokeResponse.decode(responseMessage.payload);
|
|
1068
|
+
|
|
1069
|
+
// Accumulate responses from this chunk
|
|
1070
|
+
if (response.invokeResponses) {
|
|
1071
|
+
allInvokeResponses.push(...response.invokeResponses);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Check if more chunks are coming
|
|
1075
|
+
if (response.moreChunkedMessages) {
|
|
1076
|
+
await this.sendStatus(Status.Success, {
|
|
1077
|
+
multipleMessageInteraction: true,
|
|
1078
|
+
logContext: { for: "InvokeResponse-chunk" },
|
|
1079
|
+
});
|
|
1080
|
+
} else {
|
|
1081
|
+
// This is the final chunk
|
|
1082
|
+
finalResponse = {
|
|
1083
|
+
...response,
|
|
1084
|
+
invokeResponses: allInvokeResponses,
|
|
1085
|
+
};
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
992
1088
|
}
|
|
1089
|
+
|
|
1090
|
+
return finalResponse;
|
|
993
1091
|
}
|
|
994
1092
|
|
|
995
1093
|
async sendWriteCommand(writeRequest: WriteRequest) {
|
package/src/mdns/MdnsClient.ts
CHANGED
|
@@ -63,6 +63,19 @@ const logger = Logger.get("MdnsClient");
|
|
|
63
63
|
|
|
64
64
|
const MDNS_EXPIRY_GRACE_PERIOD_FACTOR = 1.05;
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Protection window for out-of-order goodbye packets (RFC 6762).
|
|
68
|
+
* If a record was discovered within this window, ignore TTL=0 goodbye packets
|
|
69
|
+
* as they likely arrived out of order (goodbye sent before an announcement but received after).
|
|
70
|
+
*/
|
|
71
|
+
const GOODBYE_PROTECTION_WINDOW = Millis(1000);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Minimum TTL for PTR records to prevent DoS attacks with very short TTLs.
|
|
75
|
+
* Value based on python-zeroconf/bonjour implementation.
|
|
76
|
+
*/
|
|
77
|
+
const RECORD_MIN_TTL = Seconds(15);
|
|
78
|
+
|
|
66
79
|
type MatterServerRecordWithExpire = ServerAddressUdp & AddressLifespan;
|
|
67
80
|
|
|
68
81
|
/** Type for commissionable Device records including Lifespan details. */
|
|
@@ -891,6 +904,13 @@ export class MdnsClient implements Scanner {
|
|
|
891
904
|
answersList.forEach(answers =>
|
|
892
905
|
answers.forEach(answer => {
|
|
893
906
|
const { name, recordType } = answer;
|
|
907
|
+
|
|
908
|
+
// Enforce minimum TTL for records to prevent DoS attacks with very short TTLs
|
|
909
|
+
// But don't modify TTL=0 goodbye packets - those need to be processed for record removal
|
|
910
|
+
if (answer.ttl > 0 && answer.ttl < RECORD_MIN_TTL) {
|
|
911
|
+
answer = { ...answer, ttl: RECORD_MIN_TTL };
|
|
912
|
+
}
|
|
913
|
+
|
|
894
914
|
if (name.endsWith(MATTER_SERVICE_QNAME)) {
|
|
895
915
|
structuredAnswers.operational = structuredAnswers.operational ?? {};
|
|
896
916
|
structuredAnswers.operational[recordType] = structuredAnswers.operational[recordType] ?? [];
|
|
@@ -926,8 +946,32 @@ export class MdnsClient implements Scanner {
|
|
|
926
946
|
return structuredAnswers;
|
|
927
947
|
}
|
|
928
948
|
|
|
949
|
+
/**
|
|
950
|
+
* Merge a record into a map with goodbye protection.
|
|
951
|
+
* Returns true if the record was processed (added or deleted), false if skipped due to protection.
|
|
952
|
+
*/
|
|
953
|
+
#mergeRecordWithGoodbyeProtection(
|
|
954
|
+
targetMap: Map<string, AnyDnsRecordWithExpiry>,
|
|
955
|
+
key: string,
|
|
956
|
+
record: AnyDnsRecordWithExpiry,
|
|
957
|
+
now: number,
|
|
958
|
+
): void {
|
|
959
|
+
const existingRecord = targetMap.get(key);
|
|
960
|
+
if (!existingRecord || existingRecord.discoveredAt < record.discoveredAt) {
|
|
961
|
+
if (record.ttl === 0) {
|
|
962
|
+
// Apply goodbye protection - ignore if the existing record is young
|
|
963
|
+
if (existingRecord && now - existingRecord.discoveredAt < GOODBYE_PROTECTION_WINDOW) {
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
targetMap.delete(key);
|
|
967
|
+
} else {
|
|
968
|
+
targetMap.set(key, record);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
929
973
|
#combineStructuredAnswers(...answersList: StructuredDnsAnswers[]): StructuredDnsAnswers {
|
|
930
|
-
// Special type for easier combination of answers
|
|
974
|
+
// Special type for an easier combination of answers
|
|
931
975
|
const combinedAnswers: {
|
|
932
976
|
operational?: Record<number, Map<string, AnyDnsRecordWithExpiry>>;
|
|
933
977
|
commissionable?: Record<number, Map<string, AnyDnsRecordWithExpiry>>;
|
|
@@ -935,7 +979,9 @@ export class MdnsClient implements Scanner {
|
|
|
935
979
|
addressesV6?: Record<string, Map<string, AnyDnsRecordWithExpiry>>;
|
|
936
980
|
} = {};
|
|
937
981
|
|
|
982
|
+
const now = Time.nowMs;
|
|
938
983
|
for (const answers of answersList) {
|
|
984
|
+
// Process operational records
|
|
939
985
|
if (answers.operational) {
|
|
940
986
|
combinedAnswers.operational = combinedAnswers.operational ?? {};
|
|
941
987
|
for (const [recordType, records] of Object.entries(answers.operational) as unknown as [
|
|
@@ -943,18 +989,18 @@ export class MdnsClient implements Scanner {
|
|
|
943
989
|
AnyDnsRecordWithExpiry[],
|
|
944
990
|
][]) {
|
|
945
991
|
combinedAnswers.operational[recordType] = combinedAnswers.operational[recordType] ?? new Map();
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
}
|
|
955
|
-
});
|
|
992
|
+
for (const record of records) {
|
|
993
|
+
this.#mergeRecordWithGoodbyeProtection(
|
|
994
|
+
combinedAnswers.operational[recordType],
|
|
995
|
+
record.name,
|
|
996
|
+
record,
|
|
997
|
+
now,
|
|
998
|
+
);
|
|
999
|
+
}
|
|
956
1000
|
}
|
|
957
1001
|
}
|
|
1002
|
+
|
|
1003
|
+
// Process commissionable records
|
|
958
1004
|
if (answers.commissionable) {
|
|
959
1005
|
combinedAnswers.commissionable = combinedAnswers.commissionable ?? {};
|
|
960
1006
|
for (const [recordType, records] of Object.entries(answers.commissionable) as unknown as [
|
|
@@ -963,18 +1009,18 @@ export class MdnsClient implements Scanner {
|
|
|
963
1009
|
][]) {
|
|
964
1010
|
combinedAnswers.commissionable[recordType] =
|
|
965
1011
|
combinedAnswers.commissionable[recordType] ?? new Map();
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
}
|
|
975
|
-
});
|
|
1012
|
+
for (const record of records) {
|
|
1013
|
+
this.#mergeRecordWithGoodbyeProtection(
|
|
1014
|
+
combinedAnswers.commissionable[recordType],
|
|
1015
|
+
record.name,
|
|
1016
|
+
record,
|
|
1017
|
+
now,
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
976
1020
|
}
|
|
977
1021
|
}
|
|
1022
|
+
|
|
1023
|
+
// Process IPv6 addresses
|
|
978
1024
|
if (answers.addressesV6) {
|
|
979
1025
|
combinedAnswers.addressesV6 = combinedAnswers.addressesV6 ?? {};
|
|
980
1026
|
for (const [name, records] of Object.entries(answers.addressesV6) as unknown as [
|
|
@@ -982,18 +1028,18 @@ export class MdnsClient implements Scanner {
|
|
|
982
1028
|
Map<string, AnyDnsRecordWithExpiry>,
|
|
983
1029
|
][]) {
|
|
984
1030
|
combinedAnswers.addressesV6[name] = combinedAnswers.addressesV6[name] ?? new Map();
|
|
985
|
-
records.
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
}
|
|
994
|
-
});
|
|
1031
|
+
for (const record of records.values()) {
|
|
1032
|
+
this.#mergeRecordWithGoodbyeProtection(
|
|
1033
|
+
combinedAnswers.addressesV6[name],
|
|
1034
|
+
record.value,
|
|
1035
|
+
record,
|
|
1036
|
+
now,
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
995
1039
|
}
|
|
996
1040
|
}
|
|
1041
|
+
|
|
1042
|
+
// Process IPv4 addresses
|
|
997
1043
|
if (this.#socket.supportsIpv4 && answers.addressesV4) {
|
|
998
1044
|
combinedAnswers.addressesV4 = combinedAnswers.addressesV4 ?? {};
|
|
999
1045
|
for (const [name, records] of Object.entries(answers.addressesV4) as unknown as [
|
|
@@ -1001,16 +1047,14 @@ export class MdnsClient implements Scanner {
|
|
|
1001
1047
|
Map<string, AnyDnsRecordWithExpiry>,
|
|
1002
1048
|
][]) {
|
|
1003
1049
|
combinedAnswers.addressesV4[name] = combinedAnswers.addressesV4[name] ?? new Map();
|
|
1004
|
-
records.
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
}
|
|
1013
|
-
});
|
|
1050
|
+
for (const record of records.values()) {
|
|
1051
|
+
this.#mergeRecordWithGoodbyeProtection(
|
|
1052
|
+
combinedAnswers.addressesV4[name],
|
|
1053
|
+
record.value,
|
|
1054
|
+
record,
|
|
1055
|
+
now,
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1014
1058
|
}
|
|
1015
1059
|
}
|
|
1016
1060
|
}
|
|
@@ -1065,43 +1109,60 @@ export class MdnsClient implements Scanner {
|
|
|
1065
1109
|
}
|
|
1066
1110
|
|
|
1067
1111
|
/**
|
|
1068
|
-
* Update
|
|
1112
|
+
* Update IP address records in a target map with goodbye protection.
|
|
1113
|
+
* Returns true if any records were updated.
|
|
1069
1114
|
*/
|
|
1070
|
-
#
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1115
|
+
#updateIpAddressRecords(
|
|
1116
|
+
sourceAddresses: Record<string, Map<string, AnyDnsRecordWithExpiry>> | undefined,
|
|
1117
|
+
targetAddresses: Record<string, Map<string, AnyDnsRecordWithExpiry>> | undefined,
|
|
1118
|
+
now: number,
|
|
1119
|
+
): boolean {
|
|
1120
|
+
if (!sourceAddresses || !targetAddresses) {
|
|
1121
|
+
return false;
|
|
1074
1122
|
}
|
|
1075
1123
|
let updated = false;
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
if (record.ttl === 0) {
|
|
1081
|
-
interfaceRecords.addressesV6[target].delete(ip);
|
|
1082
|
-
} else {
|
|
1083
|
-
interfaceRecords.addressesV6[target].set(ip, record);
|
|
1084
|
-
}
|
|
1085
|
-
updated = true;
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1124
|
+
for (const [target, ipAddresses] of Object.entries(sourceAddresses)) {
|
|
1125
|
+
const targetMap = targetAddresses[target];
|
|
1126
|
+
if (targetMap === undefined) {
|
|
1127
|
+
continue;
|
|
1088
1128
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
if (
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
interfaceRecords.addressesV4[target].set(ip, record);
|
|
1129
|
+
for (const [ip, record] of Object.entries(ipAddresses)) {
|
|
1130
|
+
if (record.ttl === 0) {
|
|
1131
|
+
const existingRecord = targetMap.get(ip);
|
|
1132
|
+
if (existingRecord) {
|
|
1133
|
+
const recordAge = now - existingRecord.discoveredAt;
|
|
1134
|
+
if (recordAge < GOODBYE_PROTECTION_WINDOW) {
|
|
1135
|
+
// Record was recently added - ignore goodbye (likely out-of-order packet)
|
|
1136
|
+
continue;
|
|
1098
1137
|
}
|
|
1138
|
+
targetMap.delete(ip);
|
|
1099
1139
|
updated = true;
|
|
1100
1140
|
}
|
|
1141
|
+
} else {
|
|
1142
|
+
targetMap.set(ip, record);
|
|
1143
|
+
updated = true;
|
|
1101
1144
|
}
|
|
1102
1145
|
}
|
|
1103
1146
|
}
|
|
1104
|
-
|
|
1147
|
+
return updated;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Update the discovered matter relevant IP records with the new data from the DNS message.
|
|
1152
|
+
*/
|
|
1153
|
+
#updateIpRecords(answers: StructuredDnsAnswers, netInterface: string) {
|
|
1154
|
+
const interfaceRecords = this.#discoveredIpRecords.get(netInterface);
|
|
1155
|
+
if (interfaceRecords === undefined) {
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
const now = Time.nowMs;
|
|
1159
|
+
|
|
1160
|
+
const updatedV6 = this.#updateIpAddressRecords(answers.addressesV6, interfaceRecords.addressesV6, now);
|
|
1161
|
+
const updatedV4 =
|
|
1162
|
+
this.#socket.supportsIpv4 &&
|
|
1163
|
+
this.#updateIpAddressRecords(answers.addressesV4, interfaceRecords.addressesV4, now);
|
|
1164
|
+
|
|
1165
|
+
if (updatedV6 || updatedV4) {
|
|
1105
1166
|
this.#discoveredIpRecords.set(netInterface, interfaceRecords);
|
|
1106
1167
|
}
|
|
1107
1168
|
}
|
|
@@ -1197,20 +1258,37 @@ export class MdnsClient implements Scanner {
|
|
|
1197
1258
|
);
|
|
1198
1259
|
}
|
|
1199
1260
|
|
|
1261
|
+
/**
|
|
1262
|
+
* Handle goodbye (TTL=0) for an operational device record with protection against out-of-order packets.
|
|
1263
|
+
* Returns true if the goodbye was processed (record deleted or protected), false if no action needed.
|
|
1264
|
+
*/
|
|
1265
|
+
#handleOperationalDeviceGoodbye(matterName: string, netInterface: string, now: number): boolean {
|
|
1266
|
+
const existingRecord = this.#operationalDeviceRecords.get(matterName);
|
|
1267
|
+
if (!existingRecord) {
|
|
1268
|
+
return false;
|
|
1269
|
+
}
|
|
1270
|
+
const recordAge = now - existingRecord.discoveredAt;
|
|
1271
|
+
if (recordAge < GOODBYE_PROTECTION_WINDOW) {
|
|
1272
|
+
// Record was recently added - ignore goodbye (likely out-of-order packet)
|
|
1273
|
+
return true;
|
|
1274
|
+
}
|
|
1275
|
+
logger.debug(
|
|
1276
|
+
`Removing operational device ${matterName} from cache (interface ${netInterface}) because of ttl=0`,
|
|
1277
|
+
);
|
|
1278
|
+
this.#operationalDeviceRecords.delete(matterName);
|
|
1279
|
+
return true;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1200
1282
|
#handleOperationalTxtRecord(record: DnsRecord<any>, netInterface: string) {
|
|
1201
1283
|
const { name: matterName, value, ttl } = record as DnsRecord<string[]>;
|
|
1202
|
-
const
|
|
1284
|
+
const now = Time.nowMs;
|
|
1203
1285
|
|
|
1204
|
-
// we got
|
|
1286
|
+
// we got expiry info, so we can remove the record if we know it already and are done
|
|
1205
1287
|
if (ttl === 0) {
|
|
1206
|
-
|
|
1207
|
-
logger.debug(
|
|
1208
|
-
`Removing operational device ${matterName} from cache (interface ${netInterface}) because of ttl=0`,
|
|
1209
|
-
);
|
|
1210
|
-
this.#operationalDeviceRecords.delete(matterName);
|
|
1211
|
-
}
|
|
1288
|
+
this.#handleOperationalDeviceGoodbye(matterName, netInterface, now);
|
|
1212
1289
|
return;
|
|
1213
1290
|
}
|
|
1291
|
+
const discoveredAt = now;
|
|
1214
1292
|
if (!Array.isArray(value)) return;
|
|
1215
1293
|
|
|
1216
1294
|
// Existing records are always updated if relevant, but no new are added if they are not matching the criteria
|
|
@@ -1256,14 +1334,11 @@ export class MdnsClient implements Scanner {
|
|
|
1256
1334
|
value: { target, port },
|
|
1257
1335
|
} = record;
|
|
1258
1336
|
|
|
1337
|
+
const now = Time.nowMs;
|
|
1338
|
+
|
|
1259
1339
|
// We got device expiry info, so we can remove the record if we know it already and are done
|
|
1260
1340
|
if (ttl === 0) {
|
|
1261
|
-
|
|
1262
|
-
logger.debug(
|
|
1263
|
-
`Removing operational device ${matterName} from cache (interface ${netInterface}) because of ttl=0`,
|
|
1264
|
-
);
|
|
1265
|
-
this.#operationalDeviceRecords.delete(matterName);
|
|
1266
|
-
}
|
|
1341
|
+
this.#handleOperationalDeviceGoodbye(matterName, netInterface, now);
|
|
1267
1342
|
return;
|
|
1268
1343
|
}
|
|
1269
1344
|
|
|
@@ -1276,7 +1351,7 @@ export class MdnsClient implements Scanner {
|
|
|
1276
1351
|
return;
|
|
1277
1352
|
}
|
|
1278
1353
|
|
|
1279
|
-
const discoveredAt =
|
|
1354
|
+
const discoveredAt = now;
|
|
1280
1355
|
const device = this.#operationalDeviceRecords.get(matterName) ?? {
|
|
1281
1356
|
deviceIdentifier: matterName,
|
|
1282
1357
|
addresses: new Map<string, MatterServerRecordWithExpire>(),
|
|
@@ -1288,10 +1363,18 @@ export class MdnsClient implements Scanner {
|
|
|
1288
1363
|
if (ips.length > 0) {
|
|
1289
1364
|
for (const { value: ip, ttl } of ips) {
|
|
1290
1365
|
if (ttl === 0) {
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1366
|
+
const existingAddress = addresses.get(ip);
|
|
1367
|
+
if (existingAddress) {
|
|
1368
|
+
const addressAge = now - existingAddress.discoveredAt;
|
|
1369
|
+
if (addressAge < GOODBYE_PROTECTION_WINDOW) {
|
|
1370
|
+
// Address was recently added - ignore goodbye (likely out-of-order packet)
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
logger.debug(
|
|
1374
|
+
`Removing IP ${ip} for operational device ${matterName} from cache (interface ${netInterface}) because of ttl=0`,
|
|
1375
|
+
);
|
|
1376
|
+
addresses.delete(ip);
|
|
1377
|
+
}
|
|
1295
1378
|
continue;
|
|
1296
1379
|
}
|
|
1297
1380
|
const address = addresses.get(ip) ?? ({ ip, port, type: "udp" } as MatterServerRecordWithExpire);
|
|
@@ -1321,6 +1404,25 @@ export class MdnsClient implements Scanner {
|
|
|
1321
1404
|
return;
|
|
1322
1405
|
}
|
|
1323
1406
|
|
|
1407
|
+
/**
|
|
1408
|
+
* Handle goodbye (TTL=0) for a commissionable device record with protection against out-of-order packets.
|
|
1409
|
+
* Returns true if the goodbye should be skipped (record is protected), false if processed or no record exists.
|
|
1410
|
+
*/
|
|
1411
|
+
#handleCommissionableDeviceGoodbye(name: string, netInterface: string, now: number): boolean {
|
|
1412
|
+
const existingRecord = this.#commissionableDeviceRecords.get(name);
|
|
1413
|
+
if (!existingRecord) {
|
|
1414
|
+
return false;
|
|
1415
|
+
}
|
|
1416
|
+
const recordAge = now - existingRecord.discoveredAt;
|
|
1417
|
+
if (recordAge < GOODBYE_PROTECTION_WINDOW) {
|
|
1418
|
+
// Record was recently added - ignore goodbye (likely out-of-order packet)
|
|
1419
|
+
return true;
|
|
1420
|
+
}
|
|
1421
|
+
logger.debug(`Removing commissionable device ${name} from cache (interface ${netInterface}) because of ttl=0`);
|
|
1422
|
+
this.#commissionableDeviceRecords.delete(name);
|
|
1423
|
+
return false;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1324
1426
|
#handleCommissionableRecords(
|
|
1325
1427
|
answers: StructuredDnsAnswers,
|
|
1326
1428
|
formerAnswers: StructuredDnsAnswers,
|
|
@@ -1341,17 +1443,14 @@ export class MdnsClient implements Scanner {
|
|
|
1341
1443
|
|
|
1342
1444
|
const queryMissingDataForInstances = new Set<string>();
|
|
1343
1445
|
|
|
1446
|
+
const now = Time.nowMs;
|
|
1447
|
+
|
|
1344
1448
|
// First process the TXT records
|
|
1345
1449
|
const txtRecords = commissionableRecords[DnsRecordType.TXT] ?? [];
|
|
1346
1450
|
for (const record of txtRecords) {
|
|
1347
1451
|
const { name, ttl } = record;
|
|
1348
1452
|
if (ttl === 0) {
|
|
1349
|
-
|
|
1350
|
-
logger.debug(
|
|
1351
|
-
`Removing commissionable device ${name} from cache (interface ${netInterface}) because of ttl=0`,
|
|
1352
|
-
);
|
|
1353
|
-
this.#commissionableDeviceRecords.delete(name);
|
|
1354
|
-
}
|
|
1453
|
+
this.#handleCommissionableDeviceGoodbye(name, netInterface, now);
|
|
1355
1454
|
continue;
|
|
1356
1455
|
}
|
|
1357
1456
|
const txtRecord = this.#parseCommissionableTxtRecord(record);
|
|
@@ -1392,10 +1491,8 @@ export class MdnsClient implements Scanner {
|
|
|
1392
1491
|
ttl,
|
|
1393
1492
|
} = record as DnsRecord<SrvRecordValue>;
|
|
1394
1493
|
if (ttl === 0) {
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
);
|
|
1398
|
-
this.#commissionableDeviceRecords.delete(record.name);
|
|
1494
|
+
// Handle goodbye - either deletes or protects the record
|
|
1495
|
+
this.#handleCommissionableDeviceGoodbye(record.name, netInterface, now);
|
|
1399
1496
|
continue;
|
|
1400
1497
|
}
|
|
1401
1498
|
|
|
@@ -1405,15 +1502,23 @@ export class MdnsClient implements Scanner {
|
|
|
1405
1502
|
if (ips.length > 0) {
|
|
1406
1503
|
for (const { value: ip, ttl } of ips) {
|
|
1407
1504
|
if (ttl === 0) {
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1505
|
+
const existingAddress = storedRecord.addresses.get(ip);
|
|
1506
|
+
if (existingAddress) {
|
|
1507
|
+
const addressAge = now - existingAddress.discoveredAt;
|
|
1508
|
+
if (addressAge < GOODBYE_PROTECTION_WINDOW) {
|
|
1509
|
+
// Address was recently added - ignore goodbye (likely out-of-order packet)
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
1512
|
+
logger.debug(
|
|
1513
|
+
`Removing IP ${ip} for commissionable device ${record.name} from cache (interface ${netInterface}) because of ttl=0`,
|
|
1514
|
+
);
|
|
1515
|
+
storedRecord.addresses.delete(ip);
|
|
1516
|
+
}
|
|
1412
1517
|
continue;
|
|
1413
1518
|
}
|
|
1414
1519
|
const matterServer =
|
|
1415
1520
|
storedRecord.addresses.get(ip) ?? ({ ip, port, type: "udp" } as MatterServerRecordWithExpire);
|
|
1416
|
-
matterServer.discoveredAt =
|
|
1521
|
+
matterServer.discoveredAt = now;
|
|
1417
1522
|
matterServer.ttl = ttl;
|
|
1418
1523
|
|
|
1419
1524
|
storedRecord.addresses.set(ip, matterServer);
|