@matter/protocol 0.12.0-alpha.0-20241219-af74a6a14 → 0.12.0-alpha.0-20241220-c66941cd7
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/certificate/CertificateAuthority.js +2 -2
- package/dist/cjs/certificate/CertificateAuthority.js.map +1 -1
- package/dist/cjs/interaction/InteractionMessenger.d.ts +1 -1
- package/dist/cjs/interaction/InteractionMessenger.d.ts.map +1 -1
- package/dist/cjs/interaction/InteractionMessenger.js +7 -6
- package/dist/cjs/interaction/InteractionMessenger.js.map +1 -1
- package/dist/cjs/mdns/MdnsScanner.d.ts +2 -4
- package/dist/cjs/mdns/MdnsScanner.d.ts.map +1 -1
- package/dist/cjs/mdns/MdnsScanner.js +130 -51
- package/dist/cjs/mdns/MdnsScanner.js.map +2 -2
- package/dist/cjs/mdns/MdnsServer.d.ts +1 -1
- package/dist/cjs/mdns/MdnsServer.d.ts.map +1 -1
- package/dist/cjs/mdns/MdnsServer.js +20 -8
- package/dist/cjs/mdns/MdnsServer.js.map +1 -1
- package/dist/cjs/peer/ControllerDiscovery.js +1 -1
- package/dist/cjs/peer/ControllerDiscovery.js.map +1 -1
- package/dist/cjs/peer/PeerSet.d.ts.map +1 -1
- package/dist/cjs/peer/PeerSet.js +27 -10
- package/dist/cjs/peer/PeerSet.js.map +2 -2
- package/dist/cjs/protocol/MessageExchange.d.ts +5 -0
- package/dist/cjs/protocol/MessageExchange.d.ts.map +1 -1
- package/dist/cjs/protocol/MessageExchange.js.map +1 -1
- package/dist/esm/certificate/CertificateAuthority.js +2 -2
- package/dist/esm/certificate/CertificateAuthority.js.map +1 -1
- package/dist/esm/interaction/InteractionMessenger.d.ts +1 -1
- package/dist/esm/interaction/InteractionMessenger.d.ts.map +1 -1
- package/dist/esm/interaction/InteractionMessenger.js +7 -6
- package/dist/esm/interaction/InteractionMessenger.js.map +1 -1
- package/dist/esm/mdns/MdnsScanner.d.ts +2 -4
- package/dist/esm/mdns/MdnsScanner.d.ts.map +1 -1
- package/dist/esm/mdns/MdnsScanner.js +130 -51
- package/dist/esm/mdns/MdnsScanner.js.map +2 -2
- package/dist/esm/mdns/MdnsServer.d.ts +1 -1
- package/dist/esm/mdns/MdnsServer.d.ts.map +1 -1
- package/dist/esm/mdns/MdnsServer.js +20 -8
- package/dist/esm/mdns/MdnsServer.js.map +1 -1
- package/dist/esm/peer/ControllerDiscovery.js +1 -1
- package/dist/esm/peer/ControllerDiscovery.js.map +1 -1
- package/dist/esm/peer/PeerSet.d.ts.map +1 -1
- package/dist/esm/peer/PeerSet.js +27 -10
- package/dist/esm/peer/PeerSet.js.map +2 -2
- package/dist/esm/protocol/MessageExchange.d.ts +5 -0
- package/dist/esm/protocol/MessageExchange.d.ts.map +1 -1
- package/dist/esm/protocol/MessageExchange.js.map +1 -1
- package/package.json +6 -6
- package/src/certificate/CertificateAuthority.ts +2 -2
- package/src/interaction/InteractionMessenger.ts +9 -5
- package/src/mdns/MdnsScanner.ts +190 -59
- package/src/mdns/MdnsServer.ts +24 -8
- package/src/peer/ControllerDiscovery.ts +1 -1
- package/src/peer/PeerSet.ts +34 -11
- package/src/protocol/MessageExchange.ts +6 -0
package/src/mdns/MdnsScanner.ts
CHANGED
|
@@ -70,6 +70,12 @@ type OperationalDeviceRecordWithExpire = Omit<OperationalDevice, "addresses"> &
|
|
|
70
70
|
addresses: Map<string, MatterServerRecordWithExpire>; // Override addresses type to include expiration
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
type StructuredDnsAnswers = {
|
|
74
|
+
operational?: Record<number, DnsRecord<any>[]>; // Operational Matter device records by recordType
|
|
75
|
+
commissionable?: Record<number, DnsRecord<any>[]>; // Commissionable Matter device records by recordType
|
|
76
|
+
addresses?: Record<string, DnsRecord<any>[]>; // IP Address records by name
|
|
77
|
+
};
|
|
78
|
+
|
|
73
79
|
/** The initial number of seconds between two announcements. MDNS specs require 1-2 seconds, so lets use the middle. */
|
|
74
80
|
const START_ANNOUNCE_INTERVAL_SECONDS = 1.5;
|
|
75
81
|
|
|
@@ -96,7 +102,7 @@ export class MdnsScanner implements Scanner {
|
|
|
96
102
|
);
|
|
97
103
|
}
|
|
98
104
|
|
|
99
|
-
readonly #activeAnnounceQueries = new Map<string, { queries: DnsQuery[]; answers:
|
|
105
|
+
readonly #activeAnnounceQueries = new Map<string, { queries: DnsQuery[]; answers: StructuredDnsAnswers }>();
|
|
100
106
|
#queryTimer?: Timer;
|
|
101
107
|
#nextAnnounceIntervalSeconds = START_ANNOUNCE_INTERVAL_SECONDS;
|
|
102
108
|
|
|
@@ -108,6 +114,7 @@ export class MdnsScanner implements Scanner {
|
|
|
108
114
|
resolver: () => void;
|
|
109
115
|
timer?: Timer;
|
|
110
116
|
resolveOnUpdatedRecords: boolean;
|
|
117
|
+
cancelResolver?: (value: void) => void;
|
|
111
118
|
}
|
|
112
119
|
>();
|
|
113
120
|
readonly #periodicTimer: Timer;
|
|
@@ -141,7 +148,9 @@ export class MdnsScanner implements Scanner {
|
|
|
141
148
|
this.#queryTimer?.stop();
|
|
142
149
|
const allQueries = Array.from(this.#activeAnnounceQueries.values());
|
|
143
150
|
const queries = allQueries.flatMap(({ queries }) => queries);
|
|
144
|
-
const answers = allQueries.flatMap(({ answers }) =>
|
|
151
|
+
const answers = allQueries.flatMap(({ answers }) =>
|
|
152
|
+
Object.values(answers).flatMap(answer => Object.values(answer).flatMap(records => records)),
|
|
153
|
+
);
|
|
145
154
|
|
|
146
155
|
this.#queryTimer = Time.getTimer("MDNS discovery", this.#nextAnnounceIntervalSeconds * 1000, () =>
|
|
147
156
|
this.#sendQueries(),
|
|
@@ -210,7 +219,7 @@ export class MdnsScanner implements Scanner {
|
|
|
210
219
|
* Set new DnsQuery records to the list of active queries to discover devices in the network and start sending them
|
|
211
220
|
* out. When entry already exists the query is overwritten and answers are always added.
|
|
212
221
|
*/
|
|
213
|
-
#setQueryRecords(queryId: string, queries: DnsQuery[], answers:
|
|
222
|
+
#setQueryRecords(queryId: string, queries: DnsQuery[], answers: StructuredDnsAnswers = {}) {
|
|
214
223
|
const activeExistingQuery = this.#activeAnnounceQueries.get(queryId);
|
|
215
224
|
if (activeExistingQuery) {
|
|
216
225
|
const { queries: existingQueries } = activeExistingQuery;
|
|
@@ -231,7 +240,7 @@ export class MdnsScanner implements Scanner {
|
|
|
231
240
|
return;
|
|
232
241
|
}
|
|
233
242
|
queries = [...newQueries, ...existingQueries];
|
|
234
|
-
answers =
|
|
243
|
+
answers = this.#combineStructuredAnswers(activeExistingQuery.answers, answers);
|
|
235
244
|
}
|
|
236
245
|
this.#activeAnnounceQueries.set(queryId, { queries, answers });
|
|
237
246
|
logger.debug(`Set ${queries.length} query records for query ${queryId}: ${Logger.toJSON(queries)}`);
|
|
@@ -241,7 +250,9 @@ export class MdnsScanner implements Scanner {
|
|
|
241
250
|
}
|
|
242
251
|
|
|
243
252
|
#getActiveQueryEarlierAnswers() {
|
|
244
|
-
return
|
|
253
|
+
return this.#combineStructuredAnswers(
|
|
254
|
+
...[...this.#activeAnnounceQueries.values()].map(({ answers }) => answers),
|
|
255
|
+
);
|
|
245
256
|
}
|
|
246
257
|
|
|
247
258
|
/**
|
|
@@ -314,13 +325,21 @@ export class MdnsScanner implements Scanner {
|
|
|
314
325
|
* Registers a deferred promise for a specific queryId together with a timeout and return the promise.
|
|
315
326
|
* The promise will be resolved when the timer runs out latest.
|
|
316
327
|
*/
|
|
317
|
-
async #registerWaiterPromise(
|
|
328
|
+
async #registerWaiterPromise(
|
|
329
|
+
queryId: string,
|
|
330
|
+
timeoutSeconds?: number,
|
|
331
|
+
resolveOnUpdatedRecords = true,
|
|
332
|
+
cancelResolver?: (value: void) => void,
|
|
333
|
+
) {
|
|
318
334
|
const { promise, resolver } = createPromise<void>();
|
|
319
335
|
const timer =
|
|
320
336
|
timeoutSeconds !== undefined
|
|
321
|
-
? Time.getTimer("MDNS timeout", timeoutSeconds * 1000, () =>
|
|
337
|
+
? Time.getTimer("MDNS timeout", timeoutSeconds * 1000, () => {
|
|
338
|
+
cancelResolver?.();
|
|
339
|
+
this.#finishWaiter(queryId, true);
|
|
340
|
+
}).start()
|
|
322
341
|
: undefined;
|
|
323
|
-
this.#recordWaiters.set(queryId, { resolver, timer, resolveOnUpdatedRecords });
|
|
342
|
+
this.#recordWaiters.set(queryId, { resolver, timer, resolveOnUpdatedRecords, cancelResolver });
|
|
324
343
|
logger.debug(
|
|
325
344
|
`Registered waiter for query ${queryId} with ${
|
|
326
345
|
timeoutSeconds !== undefined ? `timeout ${timeoutSeconds} seconds` : "no timeout"
|
|
@@ -399,6 +418,9 @@ export class MdnsScanner implements Scanner {
|
|
|
399
418
|
|
|
400
419
|
cancelCommissionableDeviceDiscovery(identifier: CommissionableDeviceIdentifiers, resolvePromise = true) {
|
|
401
420
|
const queryId = this.#buildCommissionableQueryIdentifier(identifier);
|
|
421
|
+
const { cancelResolver } = this.#recordWaiters.get(queryId) ?? {};
|
|
422
|
+
// Mark as cancelled to not loop further in discovery, if cancel resolver is used
|
|
423
|
+
cancelResolver?.();
|
|
402
424
|
this.#finishWaiter(queryId, resolvePromise);
|
|
403
425
|
}
|
|
404
426
|
|
|
@@ -613,10 +635,8 @@ export class MdnsScanner implements Scanner {
|
|
|
613
635
|
}
|
|
614
636
|
|
|
615
637
|
/**
|
|
616
|
-
* Discovers commissionable devices based on a defined identifier and returns the first found entries.
|
|
617
|
-
*
|
|
618
|
-
* @param callback
|
|
619
|
-
* @param timeoutSeconds
|
|
638
|
+
* Discovers commissionable devices based on a defined identifier and returns the first found entries.
|
|
639
|
+
* If an own cancelSignal promise is used the discovery can only be cancelled via this signal!
|
|
620
640
|
*/
|
|
621
641
|
async findCommissionableDevicesContinuously(
|
|
622
642
|
identifier: CommissionableDeviceIdentifiers,
|
|
@@ -625,17 +645,26 @@ export class MdnsScanner implements Scanner {
|
|
|
625
645
|
cancelSignal?: Promise<void>,
|
|
626
646
|
): Promise<CommissionableDevice[]> {
|
|
627
647
|
const discoveredDevices = new Set<string>();
|
|
628
|
-
const now = Time.nowMs();
|
|
629
648
|
|
|
630
|
-
const discoveryEndTime = timeoutSeconds ?
|
|
649
|
+
const discoveryEndTime = timeoutSeconds ? Time.nowMs() + timeoutSeconds * 1000 : undefined;
|
|
631
650
|
const queryId = this.#buildCommissionableQueryIdentifier(identifier);
|
|
632
651
|
this.#setQueryRecords(queryId, this.#getCommissionableQueryRecords(identifier));
|
|
633
652
|
|
|
653
|
+
let queryResolver: ((value: void) => void) | undefined;
|
|
654
|
+
if (cancelSignal === undefined) {
|
|
655
|
+
const { promise, resolver } = createPromise<void>();
|
|
656
|
+
cancelSignal = promise;
|
|
657
|
+
queryResolver = resolver;
|
|
658
|
+
}
|
|
659
|
+
|
|
634
660
|
let canceled = false;
|
|
635
661
|
cancelSignal?.then(
|
|
636
662
|
() => {
|
|
637
663
|
canceled = true;
|
|
638
|
-
|
|
664
|
+
if (queryResolver === undefined) {
|
|
665
|
+
// Always finish when cancelSignal parameter was used, else cancelling is done separately
|
|
666
|
+
this.#finishWaiter(queryId, true);
|
|
667
|
+
}
|
|
639
668
|
},
|
|
640
669
|
cause => {
|
|
641
670
|
logger.error("Unexpected error canceling commissioning", cause);
|
|
@@ -653,12 +682,12 @@ export class MdnsScanner implements Scanner {
|
|
|
653
682
|
|
|
654
683
|
let remainingTime;
|
|
655
684
|
if (discoveryEndTime !== undefined) {
|
|
656
|
-
|
|
685
|
+
remainingTime = discoveryEndTime - Time.nowMs();
|
|
657
686
|
if (remainingTime <= 0) {
|
|
658
687
|
break;
|
|
659
688
|
}
|
|
660
689
|
}
|
|
661
|
-
await this.#registerWaiterPromise(queryId, remainingTime, false);
|
|
690
|
+
await this.#registerWaiterPromise(queryId, remainingTime, false, queryResolver);
|
|
662
691
|
}
|
|
663
692
|
return this.#getCommissionableDeviceRecords(identifier);
|
|
664
693
|
}
|
|
@@ -681,6 +710,99 @@ export class MdnsScanner implements Scanner {
|
|
|
681
710
|
);
|
|
682
711
|
}
|
|
683
712
|
|
|
713
|
+
/** Converts the discovery data into a structured format for performant access. */
|
|
714
|
+
#structureAnswers(...answersList: DnsRecord<any>[][]): StructuredDnsAnswers {
|
|
715
|
+
const structuredAnswers: StructuredDnsAnswers = {};
|
|
716
|
+
|
|
717
|
+
answersList.forEach(answers =>
|
|
718
|
+
answers.forEach(answer => {
|
|
719
|
+
const { name, recordType } = answer;
|
|
720
|
+
if (name.endsWith(MATTER_SERVICE_QNAME)) {
|
|
721
|
+
structuredAnswers.operational = structuredAnswers.operational ?? {};
|
|
722
|
+
structuredAnswers.operational[recordType] = structuredAnswers.operational[recordType] ?? [];
|
|
723
|
+
structuredAnswers.operational[recordType].push(answer);
|
|
724
|
+
} else if (name.endsWith(MATTER_COMMISSION_SERVICE_QNAME)) {
|
|
725
|
+
structuredAnswers.commissionable = structuredAnswers.commissionable ?? {};
|
|
726
|
+
structuredAnswers.commissionable[recordType] = structuredAnswers.commissionable[recordType] ?? [];
|
|
727
|
+
structuredAnswers.commissionable[recordType].push(answer);
|
|
728
|
+
} else if (recordType === DnsRecordType.A || recordType === DnsRecordType.AAAA) {
|
|
729
|
+
structuredAnswers.addresses = structuredAnswers.addresses ?? {};
|
|
730
|
+
structuredAnswers.addresses[name] = structuredAnswers.addresses[name] ?? [];
|
|
731
|
+
structuredAnswers.addresses[name].push(answer);
|
|
732
|
+
}
|
|
733
|
+
}),
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
return structuredAnswers;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
#combineStructuredAnswers(...answersList: StructuredDnsAnswers[]): StructuredDnsAnswers {
|
|
740
|
+
const combinedAnswers: {
|
|
741
|
+
operational?: Record<number, Map<string, DnsRecord<any>>>;
|
|
742
|
+
commissionable?: Record<number, Map<string, DnsRecord<any>>>;
|
|
743
|
+
addresses?: Record<string, DnsRecord<any>[]>;
|
|
744
|
+
} = {};
|
|
745
|
+
|
|
746
|
+
for (const answers of answersList) {
|
|
747
|
+
if (answers.operational) {
|
|
748
|
+
combinedAnswers.operational = combinedAnswers.operational ?? {};
|
|
749
|
+
for (const [recordType, records] of Object.entries(answers.operational) as unknown as [
|
|
750
|
+
number,
|
|
751
|
+
DnsRecord<any>[],
|
|
752
|
+
][]) {
|
|
753
|
+
combinedAnswers.operational[recordType] = combinedAnswers.operational[recordType] ?? new Map();
|
|
754
|
+
records.forEach(record => {
|
|
755
|
+
combinedAnswers.operational![recordType].set(record.name, record);
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (answers.commissionable) {
|
|
760
|
+
combinedAnswers.commissionable = combinedAnswers.commissionable ?? {};
|
|
761
|
+
for (const [recordType, records] of Object.entries(answers.commissionable) as unknown as [
|
|
762
|
+
number,
|
|
763
|
+
DnsRecord<any>[],
|
|
764
|
+
][]) {
|
|
765
|
+
combinedAnswers.commissionable[recordType] =
|
|
766
|
+
combinedAnswers.commissionable[recordType] ?? new Map();
|
|
767
|
+
records.forEach(record => {
|
|
768
|
+
combinedAnswers.commissionable![recordType].set(record.name, record);
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (answers.addresses) {
|
|
773
|
+
combinedAnswers.addresses = combinedAnswers.addresses ?? {};
|
|
774
|
+
for (const [name, records] of Object.entries(answers.addresses) as unknown as [
|
|
775
|
+
string,
|
|
776
|
+
DnsRecord<any>[],
|
|
777
|
+
][]) {
|
|
778
|
+
combinedAnswers.addresses[name] = combinedAnswers.addresses[name] ?? [];
|
|
779
|
+
// IP address consolidation happens when they are selected for a device
|
|
780
|
+
combinedAnswers.addresses[name].push(...records);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return {
|
|
786
|
+
operational: combinedAnswers.operational
|
|
787
|
+
? Object.fromEntries(
|
|
788
|
+
Object.entries(combinedAnswers.operational).map(([recordType, records]) => [
|
|
789
|
+
recordType,
|
|
790
|
+
Array.from(records.values()),
|
|
791
|
+
]),
|
|
792
|
+
)
|
|
793
|
+
: undefined,
|
|
794
|
+
commissionable: combinedAnswers.commissionable
|
|
795
|
+
? Object.fromEntries(
|
|
796
|
+
Object.entries(combinedAnswers.commissionable).map(([recordType, records]) => [
|
|
797
|
+
recordType,
|
|
798
|
+
Array.from(records.values()),
|
|
799
|
+
]),
|
|
800
|
+
)
|
|
801
|
+
: undefined,
|
|
802
|
+
addresses: combinedAnswers.addresses,
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
684
806
|
/**
|
|
685
807
|
* Main method to handle all incoming DNS messages.
|
|
686
808
|
* It will parse the message and check if it contains relevant discovery records.
|
|
@@ -692,58 +814,62 @@ export class MdnsScanner implements Scanner {
|
|
|
692
814
|
if (message.messageType !== DnsMessageType.Response && message.messageType !== DnsMessageType.TruncatedResponse)
|
|
693
815
|
return;
|
|
694
816
|
|
|
695
|
-
const answers = [...message.answers, ...message.additionalRecords];
|
|
817
|
+
const answers = this.#structureAnswers([...message.answers, ...message.additionalRecords]);
|
|
696
818
|
|
|
819
|
+
const formerAnswers = this.#getActiveQueryEarlierAnswers();
|
|
697
820
|
// Check if we got operational discovery records and handle them
|
|
698
|
-
|
|
821
|
+
this.#handleOperationalRecords(answers, formerAnswers, netInterface);
|
|
699
822
|
|
|
700
823
|
// Else check if we got commissionable discovery records and handle them
|
|
701
|
-
this.#handleCommissionableRecords(answers,
|
|
824
|
+
this.#handleCommissionableRecords(answers, formerAnswers, netInterface);
|
|
702
825
|
}
|
|
703
826
|
|
|
704
827
|
#handleIpRecords(
|
|
705
|
-
answers:
|
|
828
|
+
answers: StructuredDnsAnswers[],
|
|
706
829
|
target: string,
|
|
707
830
|
netInterface: string,
|
|
708
831
|
): { value: string; ttl: number }[] {
|
|
709
|
-
const ipRecords = answers
|
|
710
|
-
(
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
})
|
|
832
|
+
const ipRecords = answers
|
|
833
|
+
.flatMap(answer => answer.addresses?.[target] ?? [])
|
|
834
|
+
.filter(
|
|
835
|
+
({ recordType }) =>
|
|
836
|
+
recordType === DnsRecordType.AAAA || (recordType === DnsRecordType.A && this.#enableIpv4),
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
// If an IP is included multiple times we only keep the latest record
|
|
840
|
+
const collectedIps = new Map<string, { value: string; ttl: number }>();
|
|
841
|
+
ipRecords.forEach(record => {
|
|
842
|
+
const { value, ttl } = record as DnsRecord<string>;
|
|
843
|
+
if (value.startsWith("fe80::")) {
|
|
844
|
+
collectedIps.set(value, { value: `${value}%${netInterface}`, ttl });
|
|
845
|
+
} else {
|
|
846
|
+
collectedIps.set(value, { value, ttl });
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
return [...collectedIps.values()];
|
|
718
850
|
}
|
|
719
851
|
|
|
720
|
-
#handleOperationalRecords(
|
|
721
|
-
|
|
852
|
+
#handleOperationalRecords(
|
|
853
|
+
answers: StructuredDnsAnswers,
|
|
854
|
+
formerAnswers: StructuredDnsAnswers,
|
|
855
|
+
netInterface: string,
|
|
856
|
+
) {
|
|
722
857
|
// Does the message contain data for an operational service?
|
|
723
|
-
|
|
724
|
-
({ name, recordType }) => recordType === DnsRecordType.TXT && name.endsWith(MATTER_SERVICE_QNAME),
|
|
725
|
-
);
|
|
726
|
-
if (operationalTxtRecords.length) {
|
|
727
|
-
operationalTxtRecords.forEach(record => this.#handleOperationalTxtRecord(record, netInterface));
|
|
728
|
-
recordsHandled = true;
|
|
729
|
-
}
|
|
858
|
+
if (!answers.operational) return;
|
|
730
859
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
);
|
|
860
|
+
const operationalTxtRecords = answers.operational[DnsRecordType.TXT] ?? [];
|
|
861
|
+
operationalTxtRecords.forEach(record => this.#handleOperationalTxtRecord(record, netInterface));
|
|
862
|
+
|
|
863
|
+
let operationalSrvRecords = answers.operational[DnsRecordType.SRV] ?? [];
|
|
864
|
+
if (!operationalSrvRecords.length && formerAnswers.operational) {
|
|
865
|
+
operationalSrvRecords = formerAnswers.operational[DnsRecordType.SRV] ?? [];
|
|
738
866
|
}
|
|
739
867
|
|
|
740
868
|
if (operationalSrvRecords.length) {
|
|
741
869
|
operationalSrvRecords.forEach(record =>
|
|
742
870
|
this.#handleOperationalSrvRecord(record, answers, formerAnswers, netInterface),
|
|
743
871
|
);
|
|
744
|
-
recordsHandled = true;
|
|
745
872
|
}
|
|
746
|
-
return recordsHandled;
|
|
747
873
|
}
|
|
748
874
|
|
|
749
875
|
#handleOperationalTxtRecord(record: DnsRecord<any>, netInterface: string) {
|
|
@@ -791,8 +917,8 @@ export class MdnsScanner implements Scanner {
|
|
|
791
917
|
|
|
792
918
|
#handleOperationalSrvRecord(
|
|
793
919
|
record: DnsRecord<any>,
|
|
794
|
-
answers:
|
|
795
|
-
formerAnswers:
|
|
920
|
+
answers: StructuredDnsAnswers,
|
|
921
|
+
formerAnswers: StructuredDnsAnswers,
|
|
796
922
|
netInterface: string,
|
|
797
923
|
) {
|
|
798
924
|
const {
|
|
@@ -813,7 +939,7 @@ export class MdnsScanner implements Scanner {
|
|
|
813
939
|
return true;
|
|
814
940
|
}
|
|
815
941
|
|
|
816
|
-
const ips = this.#handleIpRecords([
|
|
942
|
+
const ips = this.#handleIpRecords([formerAnswers, answers], target, netInterface);
|
|
817
943
|
const deviceExisted = this.#operationalDeviceRecords.has(matterName);
|
|
818
944
|
const device = this.#operationalDeviceRecords.get(matterName) ?? {
|
|
819
945
|
deviceIdentifier: matterName,
|
|
@@ -861,18 +987,23 @@ export class MdnsScanner implements Scanner {
|
|
|
861
987
|
return true;
|
|
862
988
|
}
|
|
863
989
|
|
|
864
|
-
#handleCommissionableRecords(
|
|
990
|
+
#handleCommissionableRecords(
|
|
991
|
+
answers: StructuredDnsAnswers,
|
|
992
|
+
formerAnswers: StructuredDnsAnswers,
|
|
993
|
+
netInterface: string,
|
|
994
|
+
) {
|
|
865
995
|
// Does the message contain a SRV record for an operational service we are interested in?
|
|
866
|
-
let commissionableRecords = answers.
|
|
867
|
-
if (!commissionableRecords.length) {
|
|
868
|
-
commissionableRecords = formerAnswers.
|
|
869
|
-
if (!commissionableRecords.length)
|
|
996
|
+
let commissionableRecords = answers.commissionable ?? {};
|
|
997
|
+
if (!commissionableRecords[DnsRecordType.SRV]?.length && !commissionableRecords[DnsRecordType.TXT]?.length) {
|
|
998
|
+
commissionableRecords = formerAnswers.commissionable ?? {};
|
|
999
|
+
if (!commissionableRecords[DnsRecordType.SRV]?.length && !commissionableRecords[DnsRecordType.TXT]?.length)
|
|
1000
|
+
return;
|
|
870
1001
|
}
|
|
871
1002
|
|
|
872
1003
|
const queryMissingDataForInstances = new Set<string>();
|
|
873
1004
|
|
|
874
1005
|
// First process the TXT records
|
|
875
|
-
const txtRecords = commissionableRecords.
|
|
1006
|
+
const txtRecords = commissionableRecords[DnsRecordType.TXT] ?? [];
|
|
876
1007
|
for (const record of txtRecords) {
|
|
877
1008
|
const { name, ttl } = record;
|
|
878
1009
|
if (ttl === 0) {
|
|
@@ -916,7 +1047,7 @@ export class MdnsScanner implements Scanner {
|
|
|
916
1047
|
}
|
|
917
1048
|
|
|
918
1049
|
// We got SRV records for the instance ID, so we know the host name now and can collect the IP addresses
|
|
919
|
-
const srvRecords = commissionableRecords.
|
|
1050
|
+
const srvRecords = commissionableRecords[DnsRecordType.SRV] ?? [];
|
|
920
1051
|
for (const record of srvRecords) {
|
|
921
1052
|
const storedRecord = this.#commissionableDeviceRecords.get(record.name);
|
|
922
1053
|
if (storedRecord === undefined) continue;
|
|
@@ -934,7 +1065,7 @@ export class MdnsScanner implements Scanner {
|
|
|
934
1065
|
|
|
935
1066
|
const recordExisting = storedRecord.addresses.size > 0;
|
|
936
1067
|
|
|
937
|
-
const ips = this.#handleIpRecords([
|
|
1068
|
+
const ips = this.#handleIpRecords([formerAnswers, answers], target, netInterface);
|
|
938
1069
|
if (ips.length > 0) {
|
|
939
1070
|
for (const { value: ip, ttl } of ips) {
|
|
940
1071
|
if (ttl === 0) {
|
package/src/mdns/MdnsServer.ts
CHANGED
|
@@ -61,6 +61,7 @@ export class MdnsServer {
|
|
|
61
61
|
15 * 60 * 1000 /* 15mn - also matches maximum commissioning window time. */,
|
|
62
62
|
);
|
|
63
63
|
readonly #recordLastSentAsMulticastAnswer = new Map<string, number>();
|
|
64
|
+
readonly #recordLastSentAsUnicastAnswer = new Map<string, number>();
|
|
64
65
|
|
|
65
66
|
readonly #network: Network;
|
|
66
67
|
readonly #multicastServer: UdpMulticastServer;
|
|
@@ -75,8 +76,8 @@ export class MdnsServer {
|
|
|
75
76
|
this.#netInterface = netInterface;
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
buildDnsRecordKey(record: DnsRecord<any>, netInterface?: string) {
|
|
79
|
-
return `${record.name}-${record.recordClass}-${record.recordType}-${netInterface}`;
|
|
79
|
+
buildDnsRecordKey(record: DnsRecord<any>, netInterface?: string, unicastTarget?: string) {
|
|
80
|
+
return `${record.name}-${record.recordClass}-${record.recordType}-${netInterface}-${unicastTarget}`;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
buildTypePortKey(type: AnnouncementType, port: number) {
|
|
@@ -128,30 +129,42 @@ export class MdnsServer {
|
|
|
128
129
|
|
|
129
130
|
const now = Time.nowMs();
|
|
130
131
|
let uniCastResponse = queries.filter(query => !query.uniCastResponse).length === 0;
|
|
131
|
-
const
|
|
132
|
+
const answersTimeSinceLastSent = answers.map(answer => ({
|
|
132
133
|
timeSinceLastMultiCast:
|
|
133
134
|
now -
|
|
134
135
|
(this.#recordLastSentAsMulticastAnswer.get(this.buildDnsRecordKey(answer, netInterface)) ?? 0),
|
|
136
|
+
timeSinceLastUniCast:
|
|
137
|
+
now -
|
|
138
|
+
(this.#recordLastSentAsUnicastAnswer.get(this.buildDnsRecordKey(answer, netInterface, remoteIp)) ??
|
|
139
|
+
0),
|
|
135
140
|
ttl: answer.ttl,
|
|
136
141
|
}));
|
|
137
142
|
if (
|
|
138
143
|
uniCastResponse &&
|
|
139
|
-
|
|
144
|
+
answersTimeSinceLastSent.some(
|
|
140
145
|
({ timeSinceLastMultiCast, ttl }) => timeSinceLastMultiCast > (ttl / 4) * 1000,
|
|
141
|
-
)
|
|
146
|
+
)
|
|
142
147
|
) {
|
|
143
148
|
// If the query is for unicast response, still send as multicast if they were last sent as multicast longer then 1/4 of their ttl
|
|
144
149
|
uniCastResponse = false;
|
|
145
150
|
}
|
|
146
151
|
if (!uniCastResponse) {
|
|
147
|
-
answers = answers.filter(
|
|
148
|
-
(_, index) => answersTimeSinceLastMultiCast[index].timeSinceLastMultiCast > 1000,
|
|
149
|
-
);
|
|
152
|
+
answers = answers.filter((_, index) => answersTimeSinceLastSent[index].timeSinceLastMultiCast > 1000);
|
|
150
153
|
if (answers.length === 0) continue; // Nothing to send
|
|
151
154
|
|
|
152
155
|
answers.forEach(answer =>
|
|
153
156
|
this.#recordLastSentAsMulticastAnswer.set(this.buildDnsRecordKey(answer, netInterface), now),
|
|
154
157
|
);
|
|
158
|
+
} else {
|
|
159
|
+
answers = answers.filter((_, index) => answersTimeSinceLastSent[index].timeSinceLastUniCast > 1000);
|
|
160
|
+
if (answers.length === 0) continue; // Nothing to send
|
|
161
|
+
|
|
162
|
+
answers.forEach(answer =>
|
|
163
|
+
this.#recordLastSentAsUnicastAnswer.set(
|
|
164
|
+
this.buildDnsRecordKey(answer, netInterface, remoteIp),
|
|
165
|
+
now,
|
|
166
|
+
),
|
|
167
|
+
);
|
|
155
168
|
}
|
|
156
169
|
|
|
157
170
|
this.#sendRecords(
|
|
@@ -290,6 +303,7 @@ export class MdnsServer {
|
|
|
290
303
|
);
|
|
291
304
|
await this.#records.clear();
|
|
292
305
|
this.#recordLastSentAsMulticastAnswer.clear();
|
|
306
|
+
this.#recordLastSentAsUnicastAnswer.clear();
|
|
293
307
|
}
|
|
294
308
|
|
|
295
309
|
async setRecordsGenerator(
|
|
@@ -299,12 +313,14 @@ export class MdnsServer {
|
|
|
299
313
|
) {
|
|
300
314
|
await this.#records.clear();
|
|
301
315
|
this.#recordLastSentAsMulticastAnswer.clear();
|
|
316
|
+
this.#recordLastSentAsUnicastAnswer.clear();
|
|
302
317
|
this.#recordsGenerator.set(this.buildTypePortKey(type, hostPort), generator);
|
|
303
318
|
}
|
|
304
319
|
|
|
305
320
|
async close() {
|
|
306
321
|
await this.#records.close();
|
|
307
322
|
this.#recordLastSentAsMulticastAnswer.clear();
|
|
323
|
+
this.#recordLastSentAsUnicastAnswer.clear();
|
|
308
324
|
await this.#multicastServer.close();
|
|
309
325
|
}
|
|
310
326
|
|
|
@@ -193,7 +193,7 @@ export class ControllerDiscovery {
|
|
|
193
193
|
while (true) {
|
|
194
194
|
logger.debug(
|
|
195
195
|
`Server addresses to try: ${Array.from(addresses)
|
|
196
|
-
.map(([addressString, { device }]) => `${device?.DN
|
|
196
|
+
.map(([addressString, { device }]) => `${addressString}${device?.DN ? ` (${device.DN})` : ""}`)
|
|
197
197
|
.join(",")}`,
|
|
198
198
|
);
|
|
199
199
|
|
package/src/peer/PeerSet.ts
CHANGED
|
@@ -82,7 +82,8 @@ export interface DiscoveryOptions {
|
|
|
82
82
|
interface RunningDiscovery {
|
|
83
83
|
type: NodeDiscoveryType;
|
|
84
84
|
promises?: (() => Promise<MessageChannel>)[];
|
|
85
|
-
|
|
85
|
+
stopTimerFunc?: (() => void) | undefined;
|
|
86
|
+
mdnsScanner?: MdnsScanner;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
/**
|
|
@@ -356,9 +357,8 @@ export class PeerSet implements ImmutableSet<OperationalPeer>, ObservableSet<Ope
|
|
|
356
357
|
}
|
|
357
358
|
|
|
358
359
|
async close() {
|
|
359
|
-
const mdnsScanner
|
|
360
|
-
|
|
361
|
-
timer?.stop();
|
|
360
|
+
for (const [address, { stopTimerFunc, mdnsScanner }] of this.#runningPeerDiscoveries.entries()) {
|
|
361
|
+
stopTimerFunc?.();
|
|
362
362
|
|
|
363
363
|
// This ends discovery without triggering promises
|
|
364
364
|
mdnsScanner?.cancelOperationalDeviceDiscovery(this.#sessions.fabricFor(address), address.nodeId, false);
|
|
@@ -469,21 +469,26 @@ export class PeerSet implements ImmutableSet<OperationalPeer>, ObservableSet<Ope
|
|
|
469
469
|
|
|
470
470
|
const discoveryPromises = new Array<() => Promise<MessageChannel>>();
|
|
471
471
|
let reconnectionPollingTimer: Timer | undefined;
|
|
472
|
+
let stopTimerFunc: (() => void) | undefined;
|
|
472
473
|
|
|
473
|
-
|
|
474
|
+
const lastOperationalAddress = this.#getLastOperationalAddress(address);
|
|
475
|
+
if (lastOperationalAddress !== undefined) {
|
|
474
476
|
// Additionally to general discovery we also try to poll the formerly known operational address
|
|
475
477
|
if (requestedDiscoveryType === NodeDiscoveryType.FullDiscovery) {
|
|
476
478
|
const { promise, resolver, rejecter } = createPromise<MessageChannel>();
|
|
477
479
|
|
|
480
|
+
logger.debug(
|
|
481
|
+
`Starting reconnection polling for ${serverAddressToString(lastOperationalAddress)} (Interval ${RECONNECTION_POLLING_INTERVAL_MS / 1000}s)`,
|
|
482
|
+
);
|
|
478
483
|
reconnectionPollingTimer = Time.getPeriodicTimer(
|
|
479
484
|
"Controller reconnect",
|
|
480
485
|
RECONNECTION_POLLING_INTERVAL_MS,
|
|
481
486
|
async () => {
|
|
482
487
|
try {
|
|
483
|
-
logger.debug(`Polling for device at ${serverAddressToString(
|
|
488
|
+
logger.debug(`Polling for device at ${serverAddressToString(lastOperationalAddress)} ...`);
|
|
484
489
|
const result = await this.#reconnectKnownAddress(
|
|
485
490
|
address,
|
|
486
|
-
|
|
491
|
+
lastOperationalAddress,
|
|
487
492
|
discoveryData,
|
|
488
493
|
);
|
|
489
494
|
if (result !== undefined && reconnectionPollingTimer?.isRunning) {
|
|
@@ -509,6 +514,11 @@ export class PeerSet implements ImmutableSet<OperationalPeer>, ObservableSet<Ope
|
|
|
509
514
|
},
|
|
510
515
|
).start();
|
|
511
516
|
|
|
517
|
+
stopTimerFunc = () => {
|
|
518
|
+
reconnectionPollingTimer?.stop();
|
|
519
|
+
reconnectionPollingTimer = undefined;
|
|
520
|
+
rejecter(new NoResponseTimeoutError("Reconnection polling cancelled"));
|
|
521
|
+
};
|
|
512
522
|
discoveryPromises.push(() => promise);
|
|
513
523
|
}
|
|
514
524
|
}
|
|
@@ -521,8 +531,8 @@ export class PeerSet implements ImmutableSet<OperationalPeer>, ObservableSet<Ope
|
|
|
521
531
|
timeoutSeconds,
|
|
522
532
|
timeoutSeconds === undefined,
|
|
523
533
|
);
|
|
524
|
-
const {
|
|
525
|
-
|
|
534
|
+
const { stopTimerFunc } = this.#runningPeerDiscoveries.get(address) ?? {};
|
|
535
|
+
stopTimerFunc?.();
|
|
526
536
|
this.#runningPeerDiscoveries.delete(address);
|
|
527
537
|
|
|
528
538
|
const { result } = await ControllerDiscovery.iterateServerAddresses(
|
|
@@ -551,10 +561,13 @@ export class PeerSet implements ImmutableSet<OperationalPeer>, ObservableSet<Ope
|
|
|
551
561
|
this.#runningPeerDiscoveries.set(address, {
|
|
552
562
|
type: requestedDiscoveryType,
|
|
553
563
|
promises: discoveryPromises,
|
|
554
|
-
|
|
564
|
+
stopTimerFunc,
|
|
565
|
+
mdnsScanner,
|
|
555
566
|
});
|
|
556
567
|
|
|
557
|
-
return await anyPromise(discoveryPromises).finally(() =>
|
|
568
|
+
return await anyPromise(discoveryPromises).finally(() => {
|
|
569
|
+
this.#runningPeerDiscoveries.delete(address);
|
|
570
|
+
});
|
|
558
571
|
}
|
|
559
572
|
|
|
560
573
|
async #reconnectKnownAddress(
|
|
@@ -713,6 +726,16 @@ export class PeerSet implements ImmutableSet<OperationalPeer>, ObservableSet<Ope
|
|
|
713
726
|
};
|
|
714
727
|
}
|
|
715
728
|
await this.#store.updatePeer(peer);
|
|
729
|
+
|
|
730
|
+
// If we got a new channel and have a running discovery we can end it
|
|
731
|
+
if (this.#runningPeerDiscoveries.has(address)) {
|
|
732
|
+
logger.info(`Found ${address} during discovery, cancel discovery.`);
|
|
733
|
+
// We are currently discovering this node, so we need to update the discovery data
|
|
734
|
+
const { mdnsScanner } = this.#runningPeerDiscoveries.get(address) ?? {};
|
|
735
|
+
|
|
736
|
+
// This ends discovery and triggers the promises
|
|
737
|
+
mdnsScanner?.cancelOperationalDeviceDiscovery(this.#sessions.fabricFor(address), address.nodeId, true);
|
|
738
|
+
}
|
|
716
739
|
}
|
|
717
740
|
|
|
718
741
|
#getLastOperationalAddress(address: PeerAddress) {
|
|
@@ -49,6 +49,12 @@ export type ExchangeSendOptions = {
|
|
|
49
49
|
*/
|
|
50
50
|
expectAckOnly?: boolean;
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* If the message is part of a multiple message interaction, this flag indicates that it is not allowed
|
|
54
|
+
* to establish a new exchange
|
|
55
|
+
*/
|
|
56
|
+
multipleMessageInteraction?: boolean;
|
|
57
|
+
|
|
52
58
|
/**
|
|
53
59
|
* Defined an expected processing time by the responder for the message. This is used to calculate the final
|
|
54
60
|
* timeout for responses together with the normal retransmission logic when MRP is used.
|