@webex/plugin-meetings 3.8.0-next.49 → 3.8.0-next.50

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.
@@ -49,6 +49,7 @@ export class ClusterReachability extends EventsScope {
49
49
  private srflxIceCandidates: RTCIceCandidate[] = [];
50
50
  public readonly isVideoMesh: boolean;
51
51
  public readonly name;
52
+ public readonly reachedSubnets: Set<string> = new Set();
52
53
 
53
54
  /**
54
55
  * Constructor for ClusterReachability
@@ -234,27 +235,13 @@ export class ClusterReachability extends EventsScope {
234
235
  */
235
236
  private registerIceGatheringStateChangeListener() {
236
237
  this.pc.onicegatheringstatechange = () => {
237
- const {COMPLETE} = ICE_GATHERING_STATE;
238
-
239
- if (this.pc.iceConnectionState === COMPLETE) {
238
+ if (this.pc.iceGatheringState === ICE_GATHERING_STATE.COMPLETE) {
240
239
  this.closePeerConnection();
241
240
  this.finishReachabilityCheck();
242
241
  }
243
242
  };
244
243
  }
245
244
 
246
- /**
247
- * Checks if we have the results for all the protocols (UDP and TCP)
248
- *
249
- * @returns {boolean} true if we have all results, false otherwise
250
- */
251
- private haveWeGotAllResults(): boolean {
252
- return ['udp', 'tcp', 'xtls'].every(
253
- (protocol) =>
254
- this.result[protocol].result === 'reachable' || this.result[protocol].result === 'untested'
255
- );
256
- }
257
-
258
245
  /**
259
246
  * Saves the latency in the result for the given protocol and marks it as reachable,
260
247
  * emits the "resultReady" event if this is the first result for that protocol,
@@ -264,9 +251,15 @@ export class ClusterReachability extends EventsScope {
264
251
  * @param {string} protocol
265
252
  * @param {number} latency
266
253
  * @param {string|null} [publicIp]
254
+ * @param {string|null} [serverIp]
267
255
  * @returns {void}
268
256
  */
269
- private saveResult(protocol: 'udp' | 'tcp' | 'xtls', latency: number, publicIp?: string | null) {
257
+ private saveResult(
258
+ protocol: 'udp' | 'tcp' | 'xtls',
259
+ latency: number,
260
+ publicIp?: string | null,
261
+ serverIp?: string | null
262
+ ) {
270
263
  const result = this.result[protocol];
271
264
 
272
265
  if (result.latencyInMilliseconds === undefined) {
@@ -294,6 +287,10 @@ export class ClusterReachability extends EventsScope {
294
287
  } else {
295
288
  this.addPublicIP(protocol, publicIp);
296
289
  }
290
+
291
+ if (serverIp) {
292
+ this.reachedSubnets.add(serverIp);
293
+ }
297
294
  }
298
295
 
299
296
  /**
@@ -351,21 +348,25 @@ export class ClusterReachability extends EventsScope {
351
348
 
352
349
  if (e.candidate) {
353
350
  if (e.candidate.type === CANDIDATE_TYPES.SERVER_REFLEXIVE) {
354
- this.saveResult('udp', latencyInMilliseconds, e.candidate.address);
351
+ let serverIp = null;
352
+ if ('url' in e.candidate) {
353
+ const stunServerUrlRegex = /stun:([\d.]+):\d+/;
354
+
355
+ const match = (e.candidate as any).url.match(stunServerUrlRegex);
356
+ if (match) {
357
+ // eslint-disable-next-line prefer-destructuring
358
+ serverIp = match[1];
359
+ }
360
+ }
361
+
362
+ this.saveResult('udp', latencyInMilliseconds, e.candidate.address, serverIp);
355
363
 
356
364
  this.determineNatType(e.candidate);
357
365
  }
358
366
 
359
367
  if (e.candidate.type === CANDIDATE_TYPES.RELAY) {
360
368
  const protocol = e.candidate.port === TURN_TLS_PORT ? 'xtls' : 'tcp';
361
- this.saveResult(protocol, latencyInMilliseconds);
362
- // we don't add public IP for TCP, because in the case of relay candidates
363
- // e.candidate.address is the TURN server address, not the client's public IP
364
- }
365
-
366
- if (this.haveWeGotAllResults()) {
367
- this.closePeerConnection();
368
- this.finishReachabilityCheck();
369
+ this.saveResult(protocol, latencyInMilliseconds, null, e.candidate.address);
369
370
  }
370
371
  }
371
372
  };
@@ -138,6 +138,60 @@ export default class Reachability extends EventsScope {
138
138
  }
139
139
  }
140
140
 
141
+ /**
142
+ * Checks if the given subnet is reachable
143
+ * @param {string} mediaServerIp - media server ip
144
+ * @returns {boolean | null} true if reachable, false if not reachable, null if mediaServerIp is not provided
145
+ * @public
146
+ * @memberof Reachability
147
+ */
148
+ public isSubnetReachable(mediaServerIp?: string): boolean | null {
149
+ if (!mediaServerIp) {
150
+ LoggerProxy.logger.error(`Reachability:index#isSubnetReachable --> mediaServerIp is null`);
151
+
152
+ return null;
153
+ }
154
+
155
+ const subnetFirstOctet = mediaServerIp.split('.')[0];
156
+
157
+ LoggerProxy.logger.info(
158
+ `Reachability:index#isSubnetReachable --> Looking for subnet: ${subnetFirstOctet}.X.X.X`
159
+ );
160
+
161
+ const matchingReachedClusters = Object.values(this.clusterReachability).reduce(
162
+ (acc, cluster) => {
163
+ const reachedSubnetsArray = Array.from(cluster.reachedSubnets);
164
+
165
+ let logMessage = `Reachability:index#isSubnetReachable --> Cluster ${cluster.name} reached [`;
166
+ for (let i = 0; i < reachedSubnetsArray.length; i += 1) {
167
+ const subnet = reachedSubnetsArray[i];
168
+ const reachedSubnetFirstOctet = subnet.split('.')[0];
169
+
170
+ if (subnetFirstOctet === reachedSubnetFirstOctet) {
171
+ acc.add(cluster.name);
172
+ }
173
+
174
+ logMessage += `${subnet}`;
175
+ if (i < reachedSubnetsArray.length - 1) {
176
+ logMessage += ',';
177
+ }
178
+ }
179
+ logMessage += `]`;
180
+
181
+ LoggerProxy.logger.info(logMessage);
182
+
183
+ return acc;
184
+ },
185
+ new Set<string>()
186
+ );
187
+
188
+ LoggerProxy.logger.info(
189
+ `Reachability:index#isSubnetReachable --> Found ${matchingReachedClusters.size} clusters that use the subnet ${subnetFirstOctet}.X.X.X`
190
+ );
191
+
192
+ return matchingReachedClusters.size > 0;
193
+ }
194
+
141
195
  /**
142
196
  * Gets a list of media clusters from the backend and performs reachability checks on all the clusters
143
197
  * @param {string} trigger - explains the reason for starting reachability
@@ -253,6 +253,7 @@ describe('plugin-meetings', () => {
253
253
  getReachabilityResults: sinon.stub().resolves(undefined),
254
254
  getReachabilityMetrics: sinon.stub().resolves({}),
255
255
  stopReachability: sinon.stub(),
256
+ isSubnetReachable: sinon.stub().returns(true),
256
257
  };
257
258
  webex.internal.llm.on = sinon.stub();
258
259
  webex.internal.newMetrics.callDiagnosticLatencies = new CallDiagnosticLatencies(
@@ -2128,6 +2129,7 @@ describe('plugin-meetings', () => {
2128
2129
  someReachabilityMetric2: 'some value2',
2129
2130
  }),
2130
2131
  stopReachability: sinon.stub(),
2132
+ isSubnetReachable: sinon.stub().returns(false),
2131
2133
  };
2132
2134
 
2133
2135
  const forceRtcMetricsSend = sinon.stub().resolves();
@@ -2183,6 +2185,7 @@ describe('plugin-meetings', () => {
2183
2185
  someReachabilityMetric1: 'some value1',
2184
2186
  someReachabilityMetric2: 'some value2',
2185
2187
  selectedCandidatePairChanges: 2,
2188
+ isSubnetReachable: false,
2186
2189
  numTransports: 1,
2187
2190
  iceCandidatesCount: 0,
2188
2191
  }
@@ -2229,6 +2232,7 @@ describe('plugin-meetings', () => {
2229
2232
  signalingState: 'unknown',
2230
2233
  connectionState: 'unknown',
2231
2234
  iceConnectionState: 'unknown',
2235
+ isSubnetReachable: true,
2232
2236
  })
2233
2237
  );
2234
2238
 
@@ -2243,6 +2247,7 @@ describe('plugin-meetings', () => {
2243
2247
  someReachabilityMetric1: 'some value1',
2244
2248
  someReachabilityMetric2: 'some value2',
2245
2249
  }),
2250
+ isSubnetReachable: sinon.stub().returns(true),
2246
2251
  };
2247
2252
 
2248
2253
  meeting.waitForRemoteSDPAnswer = sinon.stub().rejects();
@@ -2293,6 +2298,7 @@ describe('plugin-meetings', () => {
2293
2298
  selectedCandidatePairChanges: 2,
2294
2299
  numTransports: 1,
2295
2300
  iceCandidatesCount: 0,
2301
+ isSubnetReachable: true,
2296
2302
  }
2297
2303
  );
2298
2304
  });
@@ -2350,6 +2356,7 @@ describe('plugin-meetings', () => {
2350
2356
  signalingState: 'have-local-offer',
2351
2357
  connectionState: 'connecting',
2352
2358
  iceConnectionState: 'checking',
2359
+ isSubnetReachable: true,
2353
2360
  })
2354
2361
  );
2355
2362
 
@@ -2407,6 +2414,7 @@ describe('plugin-meetings', () => {
2407
2414
  signalingState: 'have-local-offer',
2408
2415
  connectionState: 'connecting',
2409
2416
  iceConnectionState: 'checking',
2417
+ isSubnetReachable: true,
2410
2418
  })
2411
2419
  );
2412
2420
 
@@ -2744,6 +2752,7 @@ describe('plugin-meetings', () => {
2744
2752
  isWebexMediaBackendUnreachable: sinon.stub().resolves(false),
2745
2753
  getReachabilityMetrics: sinon.stub().resolves(),
2746
2754
  stopReachability: sinon.stub(),
2755
+ isSubnetReachable: sinon.stub().returns(true),
2747
2756
  };
2748
2757
  const MOCK_CLIENT_ERROR_CODE = 2004;
2749
2758
  const generateClientErrorCodeForIceFailureStub = sinon
@@ -2923,6 +2932,7 @@ describe('plugin-meetings', () => {
2923
2932
  selectedCandidatePairChanges: 2,
2924
2933
  numTransports: 1,
2925
2934
  iceCandidatesCount: 0,
2935
+ isSubnetReachable: true,
2926
2936
  },
2927
2937
  ]);
2928
2938
 
@@ -2953,6 +2963,7 @@ describe('plugin-meetings', () => {
2953
2963
  .resolves(false),
2954
2964
  getReachabilityMetrics: sinon.stub().resolves({}),
2955
2965
  stopReachability: sinon.stub(),
2966
+ isSubnetReachable: sinon.stub().returns(true),
2956
2967
  };
2957
2968
  const getErrorPayloadForClientErrorCodeStub =
2958
2969
  (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
@@ -3120,6 +3131,7 @@ describe('plugin-meetings', () => {
3120
3131
  retriedWithTurnServer: true,
3121
3132
  isJoinWithMediaRetry: false,
3122
3133
  iceCandidatesCount: 0,
3134
+ isSubnetReachable: true,
3123
3135
  },
3124
3136
  ]);
3125
3137
  meeting.roap.doTurnDiscovery;
@@ -3248,6 +3260,7 @@ describe('plugin-meetings', () => {
3248
3260
  someReachabilityMetric2: 'some value2',
3249
3261
  }),
3250
3262
  stopReachability: sinon.stub(),
3263
+ isSubnetReachable: sinon.stub().returns(true),
3251
3264
  };
3252
3265
  meeting.iceCandidatesCount = 3;
3253
3266
  meeting.iceCandidateErrors.set('701_error', 3);
@@ -3275,6 +3288,7 @@ describe('plugin-meetings', () => {
3275
3288
  iceCandidatesCount: 3,
3276
3289
  '701_error': 3,
3277
3290
  '701_turn_host_lookup_received_error': 1,
3291
+ isSubnetReachable: true,
3278
3292
  }
3279
3293
  );
3280
3294
 
@@ -3337,6 +3351,7 @@ describe('plugin-meetings', () => {
3337
3351
  iceConnectionState: 'unknown',
3338
3352
  selectedCandidatePairChanges: 2,
3339
3353
  numTransports: 1,
3354
+ isSubnetReachable: true,
3340
3355
  iceCandidatesCount: 0,
3341
3356
  }
3342
3357
  );
@@ -3398,6 +3413,7 @@ describe('plugin-meetings', () => {
3398
3413
  numTransports: 1,
3399
3414
  '701_error': 2,
3400
3415
  '701_turn_host_lookup_received_error': 1,
3416
+ isSubnetReachable: true,
3401
3417
  iceCandidatesCount: 0,
3402
3418
  }
3403
3419
  );
@@ -174,59 +174,6 @@ describe('ClusterReachability', () => {
174
174
  assert.deepEqual(emittedEvents[Events.clientMediaIpsUpdated], []);
175
175
  });
176
176
 
177
- it('resolves and has correct result as soon as it finds that all udp, tcp and tls are reachable', async () => {
178
- const promise = clusterReachability.start();
179
-
180
- await clock.tickAsync(100);
181
- fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp'}});
182
-
183
- // check the right events were emitted
184
- assert.equal(emittedEvents[Events.resultReady].length, 1);
185
- assert.deepEqual(emittedEvents[Events.resultReady][0], {
186
- protocol: 'udp',
187
- result: 'reachable',
188
- latencyInMilliseconds: 100,
189
- clientMediaIPs: ['somePublicIp'],
190
- });
191
-
192
- // clientMediaIpsUpdated shouldn't be emitted, because the IP is already passed in the resultReady event
193
- assert.equal(emittedEvents[Events.clientMediaIpsUpdated].length, 0);
194
-
195
- await clock.tickAsync(100);
196
- fakePeerConnection.onicecandidate({candidate: {type: 'relay', address: 'someTurnRelayIp'}});
197
-
198
- // check the right event was emitted
199
- assert.equal(emittedEvents[Events.resultReady].length, 2);
200
- assert.deepEqual(emittedEvents[Events.resultReady][1], {
201
- protocol: 'tcp',
202
- result: 'reachable',
203
- latencyInMilliseconds: 200,
204
- });
205
- assert.equal(emittedEvents[Events.clientMediaIpsUpdated].length, 0);
206
-
207
- await clock.tickAsync(100);
208
- fakePeerConnection.onicecandidate({
209
- candidate: {type: 'relay', address: 'someTurnRelayIp', port: 443},
210
- });
211
-
212
- // check the right event was emitted
213
- assert.equal(emittedEvents[Events.resultReady].length, 3);
214
- assert.deepEqual(emittedEvents[Events.resultReady][2], {
215
- protocol: 'xtls',
216
- result: 'reachable',
217
- latencyInMilliseconds: 300,
218
- });
219
- assert.equal(emittedEvents[Events.clientMediaIpsUpdated].length, 0);
220
-
221
- await promise;
222
-
223
- assert.deepEqual(clusterReachability.getResult(), {
224
- udp: {result: 'reachable', latencyInMilliseconds: 100, clientMediaIPs: ['somePublicIp']},
225
- tcp: {result: 'reachable', latencyInMilliseconds: 200},
226
- xtls: {result: 'reachable', latencyInMilliseconds: 300},
227
- });
228
- });
229
-
230
177
  it('resolves and returns correct results when aborted before it gets any candidates', async () => {
231
178
  const promise = clusterReachability.start();
232
179
 
@@ -275,7 +222,7 @@ describe('ClusterReachability', () => {
275
222
 
276
223
  await testUtils.flushPromises();
277
224
 
278
- fakePeerConnection.iceConnectionState = 'complete';
225
+ fakePeerConnection.iceGatheringState = 'complete';
279
226
  fakePeerConnection.onicegatheringstatechange();
280
227
  await promise;
281
228
 
@@ -293,7 +240,7 @@ describe('ClusterReachability', () => {
293
240
  await clock.tickAsync(30);
294
241
  fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp1'}});
295
242
 
296
- fakePeerConnection.iceConnectionState = 'complete';
243
+ fakePeerConnection.iceGatheringState = 'complete';
297
244
  fakePeerConnection.onicegatheringstatechange();
298
245
  await promise;
299
246
 
@@ -436,6 +383,9 @@ describe('ClusterReachability', () => {
436
383
  candidate: {type: 'relay', address: 'someTurnRelayIp', port: 443},
437
384
  });
438
385
 
386
+ fakePeerConnection.iceGatheringState = 'complete';
387
+ fakePeerConnection.onicegatheringstatechange();
388
+
439
389
  await promise;
440
390
 
441
391
  assert.deepEqual(clusterReachability.getResult(), {
@@ -474,6 +424,10 @@ describe('ClusterReachability', () => {
474
424
  candidate: {type: 'relay', address: 'someTurnRelayIp', port: 443},
475
425
  });
476
426
 
427
+ fakePeerConnection.iceGatheringState = 'complete';
428
+ fakePeerConnection.onicegatheringstatechange();
429
+ await clock.tickAsync(10);
430
+
477
431
  await promise;
478
432
 
479
433
  assert.deepEqual(clusterReachability.getResult(), {
@@ -486,5 +440,37 @@ describe('ClusterReachability', () => {
486
440
  xtls: {result: 'reachable', latencyInMilliseconds: 20},
487
441
  });
488
442
  });
443
+
444
+ it('should gather correctly reached subnets', async () => {
445
+ const promise = clusterReachability.start();
446
+
447
+ await clock.tickAsync(10);
448
+ fakePeerConnection.onicecandidate({candidate: {type: 'srflx', url: 'stun:1.2.3.4:5004'}});
449
+ fakePeerConnection.onicecandidate({candidate: {type: 'srflx', url: 'stun:4.3.2.1:5004'}});
450
+ fakePeerConnection.onicecandidate({candidate: {type: 'relay', address: 'someTurnRelayIp'}});
451
+
452
+ clusterReachability.abort();
453
+ await promise;
454
+
455
+ assert.deepEqual(Array.from(clusterReachability.reachedSubnets), [
456
+ '1.2.3.4',
457
+ '4.3.2.1',
458
+ 'someTurnRelayIp'
459
+ ]);
460
+ });
461
+
462
+ it('should store only unique subnet address', async () => {
463
+ const promise = clusterReachability.start();
464
+
465
+ await clock.tickAsync(10);
466
+ fakePeerConnection.onicecandidate({candidate: {type: 'srflx', url: 'stun:1.2.3.4:5004'}});
467
+ fakePeerConnection.onicecandidate({candidate: {type: 'srflx', url: 'stun:1.2.3.4:9000'}});
468
+ fakePeerConnection.onicecandidate({candidate: {type: 'relay', address: '1.2.3.4'}});
469
+
470
+ clusterReachability.abort();
471
+ await promise;
472
+
473
+ assert.deepEqual(Array.from(clusterReachability.reachedSubnets), ['1.2.3.4']);
474
+ });
489
475
  });
490
476
  });
@@ -2686,3 +2686,38 @@ describe('sendMetric', () => {
2686
2686
  });
2687
2687
  });
2688
2688
  });
2689
+
2690
+ describe('isSubnetReachable', () => {
2691
+ let webex;
2692
+ let reachability;
2693
+
2694
+ beforeEach(() => {
2695
+ webex = new MockWebex();
2696
+ reachability = new TestReachability(webex);
2697
+
2698
+ reachability.setFakeClusterReachability({
2699
+ cluster1: {
2700
+ reachedSubnets: new Set(['1.2.3.4', '2.3.4.5']),
2701
+ },
2702
+ cluster2: {
2703
+ reachedSubnets: new Set(['3.4.5.6', '4.5.6.7']),
2704
+ },
2705
+ });
2706
+ });
2707
+
2708
+ afterEach(() => {
2709
+ sinon.restore();
2710
+ });
2711
+
2712
+ it('returns true if the subnet is reachable', () => {
2713
+ assert(reachability.isSubnetReachable('1.2.3.4'));
2714
+ });
2715
+
2716
+ it(`returns false if the subnet is unreachable`, () => {
2717
+ assert(!reachability.isSubnetReachable('11.2.3.4'));
2718
+ });
2719
+
2720
+ it('returns null if the subnet is not provided', () => {
2721
+ assert.isNull(reachability.isSubnetReachable(undefined));
2722
+ });
2723
+ });