Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugin-meetings): reachability- add detection of symmetric NAT #4156

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
13cc78c
feat: add detection of nat type
k-wasniowski Mar 18, 2025
c481be7
feat: add unit-tests
k-wasniowski Mar 18, 2025
e6bf1d2
fix: remove dead code
k-wasniowski Mar 18, 2025
957bb88
Merge branch 'next' into feat-add-detection-of-symmetric-nat
k-wasniowski Mar 18, 2025
efcd021
Merge branch 'next' into feat-add-detection-of-symmetric-nat
k-wasniowski Mar 19, 2025
e13ff55
Merge branch 'next' into feat-add-detection-of-symmetric-nat
k-wasniowski Mar 19, 2025
07945db
Merge branch 'next' into feat-add-detection-of-symmetric-nat
k-wasniowski Mar 20, 2025
9847b90
Merge branch 'next' into feat-add-detection-of-symmetric-nat
k-wasniowski Mar 21, 2025
1869338
Merge branch 'next' into feat-add-detection-of-symmetric-nat
k-wasniowski Mar 22, 2025
678dd52
Merge branch 'next' into feat-add-detection-of-symmetric-nat
k-wasniowski Mar 24, 2025
786e6b7
Merge branch 'next' into feat-add-detection-of-symmetric-nat
k-wasniowski Mar 25, 2025
362db91
Merge branch 'feat-add-detection-of-symmetric-nat' of https://github.…
k-wasniowski Mar 25, 2025
a65e2a9
fix: remove natType from the cluster
k-wasniowski Mar 27, 2025
d04883f
Merge branch 'next' into feat-add-detection-of-symmetric-nat
k-wasniowski Mar 27, 2025
e7a5e04
Merge branch 'feat-add-detection-of-symmetric-nat' of https://github.…
k-wasniowski Mar 27, 2025
c7e0440
fix: removed failure values
k-wasniowski Mar 27, 2025
9b61bde
fix: unit tests correction
k-wasniowski Mar 27, 2025
b539bce
Merge branch 'next' into feat-add-detection-of-symmetric-nat
k-wasniowski Mar 27, 2025
2fac087
Merge branch 'next' into feat-add-detection-of-symmetric-nat
k-wasniowski Mar 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {convertStunUrlToTurn, convertStunUrlToTurnTls} from './util';
import EventsScope from '../common/events/events-scope';

import {CONNECTION_STATE, Enum, ICE_GATHERING_STATE} from '../constants';
import {ClusterReachabilityResult} from './reachability.types';
import {ClusterReachabilityResult, NatType} from './reachability.types';

// data for the Events.resultReady event
export type ResultEventData = {
Expand All @@ -22,9 +22,14 @@ export type ClientMediaIpsUpdatedEventData = {
clientMediaIPs: string[];
};

export type NatTypeUpdatedEventData = {
natType: NatType;
};

export const Events = {
resultReady: 'resultReady', // emitted when a cluster is reached successfully using specific protocol
clientMediaIpsUpdated: 'clientMediaIpsUpdated', // emitted when more public IPs are found after resultReady was already sent for a given protocol
natTypeUpdated: 'natTypeUpdated', // emitted when NAT type is determined
} as const;

export type Events = Enum<typeof Events>;
Expand All @@ -41,6 +46,7 @@ export class ClusterReachability extends EventsScope {
private pc?: RTCPeerConnection;
private defer: Defer; // this defer is resolved once reachability checks for this cluster are completed
private startTimestamp: number;
private srflxIceCandidates: RTCIceCandidate[] = [];
public readonly isVideoMesh: boolean;
public readonly name;

Expand Down Expand Up @@ -290,6 +296,44 @@ export class ClusterReachability extends EventsScope {
}
}

/**
* Determines NAT Type.
*
* @param {RTCIceCandidate} candidate
* @returns {void}
*/
private determineNatType(candidate: RTCIceCandidate) {
this.srflxIceCandidates.push(candidate);

if (this.srflxIceCandidates.length > 1) {
const portsFound: Record<string, Set<number>> = {};

this.srflxIceCandidates.forEach((c) => {
const key = `${c.address}:${c.relatedPort}`;
if (!portsFound[key]) {
portsFound[key] = new Set();
}
portsFound[key].add(c.port);
});

Object.entries(portsFound).forEach(([, ports]) => {
if (ports.size > 1) {
// Found candidates with the same address and relatedPort, but different ports
this.emit(
{
file: 'clusterReachability',
function: 'determineNatType',
},
Events.natTypeUpdated,
{
natType: NatType.SymmetricNat,
}
);
}
});
}
}

/**
* Registers a listener for the icecandidate event
*
Expand All @@ -308,6 +352,8 @@ export class ClusterReachability extends EventsScope {
if (e.candidate) {
if (e.candidate.type === CANDIDATE_TYPES.SERVER_REFLEXIVE) {
this.saveResult('udp', latencyInMilliseconds, e.candidate.address);

this.determineNatType(e.candidate);
}

if (e.candidate.type === CANDIDATE_TYPES.RELAY) {
Expand Down
11 changes: 11 additions & 0 deletions packages/@webex/plugin-meetings/src/reachability/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ import {
ReachabilityResultsForBackend,
TransportResultForBackend,
GetClustersTrigger,
NatType,
} from './reachability.types';
import {
ClientMediaIpsUpdatedEventData,
ClusterReachability,
Events,
NatTypeUpdatedEventData,
ResultEventData,
} from './clusterReachability';
import EventsScope from '../common/events/events-scope';
Expand Down Expand Up @@ -64,6 +66,7 @@ export default class Reachability extends EventsScope {
resultsCount = {videoMesh: {udp: 0}, public: {udp: 0, tcp: 0, xtls: 0}};
startTime = undefined;
totalDuration = undefined;
natType = NatType.Unknown;

protected lastTrigger?: string;

Expand Down Expand Up @@ -309,6 +312,7 @@ export default class Reachability extends EventsScope {
reachability_vmn_tcp_failed: 0,
reachability_vmn_xtls_success: 0,
reachability_vmn_xtls_failed: 0,
natType: this.natType,
};

const updateStats = (clusterType: 'public' | 'vmn', result: ClusterReachabilityResult) => {
Expand Down Expand Up @@ -967,6 +971,13 @@ export default class Reachability extends EventsScope {
}
);

this.clusterReachability[key].on(
Events.natTypeUpdated,
async (data: NatTypeUpdatedEventData) => {
this.natType = data.natType;
}
);

this.clusterReachability[key].start(); // not awaiting on purpose
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export type TransportResult = {
clientMediaIPs?: string[];
};

export enum NatType {
Unknown = 'unknown',
SymmetricNat = 'symmetric-nat',
}

// reachability result for a specific media cluster
export type ClusterReachabilityResult = {
udp: TransportResult;
Expand All @@ -27,6 +32,7 @@ export type ReachabilityMetrics = {
reachability_vmn_tcp_failed: number;
reachability_vmn_xtls_success: number;
reachability_vmn_xtls_failed: number;
natType: NatType;
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,27 @@ import {
ResultEventData,
Events,
ClientMediaIpsUpdatedEventData,
NatTypeUpdatedEventData,
} from '@webex/plugin-meetings/src/reachability/clusterReachability'; // replace with actual path
import { NatType } from 'packages/@webex/plugin-meetings/dist/reachability/reachability.types';

describe('ClusterReachability', () => {
let previousRTCPeerConnection;
let clusterReachability;
let fakePeerConnection;
let gatherIceCandidatesSpy;

const emittedEvents: Record<Events, (ResultEventData | ClientMediaIpsUpdatedEventData)[]> = {
const emittedEvents: Record<Events, (ResultEventData | ClientMediaIpsUpdatedEventData | NatTypeUpdatedEventData)[]> = {
[Events.resultReady]: [],
[Events.clientMediaIpsUpdated]: [],
[Events.natTypeUpdated]: [],
};
const FAKE_OFFER = {type: 'offer', sdp: 'fake sdp'};

const resetEmittedEvents = () => {
emittedEvents[Events.resultReady].length = 0;
emittedEvents[Events.clientMediaIpsUpdated].length = 0;
emittedEvents[Events.natTypeUpdated].length = 0;
};
beforeEach(() => {
fakePeerConnection = {
Expand Down Expand Up @@ -56,6 +60,10 @@ describe('ClusterReachability', () => {
clusterReachability.on(Events.clientMediaIpsUpdated, (data: ClientMediaIpsUpdatedEventData) => {
emittedEvents[Events.clientMediaIpsUpdated].push(data);
});

clusterReachability.on(Events.natTypeUpdated, (data: NatTypeUpdatedEventData) => {
emittedEvents[Events.natTypeUpdated].push(data);
});
});

afterEach(() => {
Expand Down Expand Up @@ -440,5 +448,43 @@ describe('ClusterReachability', () => {
xtls: {result: 'reachable', latencyInMilliseconds: 40},
});
});

it('determines correctly if symmetric-nat is detected', async () => {
const promise = clusterReachability.start();

// generate candidates with duplicate addresses
await clock.tickAsync(10);
fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp1', relatedPort: 3478, port: 1000}});

// check events emitted: there shouldn't be any natTypeUpdated emitted
assert.equal(emittedEvents[Events.natTypeUpdated].length, 0);

await clock.tickAsync(10);
fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp1', relatedPort: 3478, port: 2000}});

// should emit natTypeUpdated event
assert.equal(emittedEvents[Events.natTypeUpdated].length, 1);
assert.deepEqual(emittedEvents[Events.natTypeUpdated][0], {
natType: 'symmetric-nat',
});

// send also a relay candidate so that the reachability check finishes
fakePeerConnection.onicecandidate({candidate: {type: 'relay', address: 'someTurnRelayIp'}});
fakePeerConnection.onicecandidate({
candidate: {type: 'relay', address: 'someTurnRelayIp', port: 443},
});

await promise;

assert.deepEqual(clusterReachability.getResult(), {
udp: {
result: 'reachable',
latencyInMilliseconds: 10,
clientMediaIPs: ['somePublicIp1'],
},
tcp: {result: 'reachable', latencyInMilliseconds: 20},
xtls: {result: 'reachable', latencyInMilliseconds: 20},
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2156,6 +2156,7 @@ describe('getReachabilityMetrics', () => {
reachability_vmn_tcp_failed: 0,
reachability_vmn_xtls_success: 0,
reachability_vmn_xtls_failed: 0,
natType: 'unknown'
});
});

Expand Down Expand Up @@ -2223,6 +2224,7 @@ describe('getReachabilityMetrics', () => {
reachability_vmn_tcp_failed: 1,
reachability_vmn_xtls_success: 0,
reachability_vmn_xtls_failed: 0,
natType: 'unknown'
}
);
});
Expand Down Expand Up @@ -2284,6 +2286,7 @@ describe('getReachabilityMetrics', () => {
reachability_vmn_tcp_failed: 0,
reachability_vmn_xtls_success: 0,
reachability_vmn_xtls_failed: 0,
natType: 'unknown'
}
);
});
Expand Down Expand Up @@ -2345,6 +2348,7 @@ describe('getReachabilityMetrics', () => {
reachability_vmn_tcp_failed: 3,
reachability_vmn_xtls_success: 1,
reachability_vmn_xtls_failed: 1,
natType: 'unknown'
}
);
});
Expand Down