Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 31 additions & 0 deletions packages/@webex/plugin-meetings/src/meeting/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,7 @@ export default class Meeting extends StatelessWebexPlugin {
private rtcMetrics?: RtcMetrics;
private uploadLogsTimer?: ReturnType<typeof setTimeout>;
private logUploadIntervalIndex: number;
private mediaServerIp: string;

/**
* @param {Object} attrs
Expand Down Expand Up @@ -1598,6 +1599,15 @@ export default class Meeting extends StatelessWebexPlugin {
* @memberof Meeting
*/
this.#isoLocalClientMeetingJoinTime = undefined;

/**
* IP Address of the remote media server
* @instance
* @type {string}
* @private
* @memberof Meeting
*/
this.mediaServerIp = undefined;
}

/**
Expand Down Expand Up @@ -6333,6 +6343,11 @@ export default class Meeting extends StatelessWebexPlugin {
? MeetingsUtil.getMediaServer(roapMessage.sdp)
: undefined;

const mediaServerIp =
roapMessage.messageType === 'ANSWER'
? MeetingsUtil.getMediaServerIp(roapMessage.sdp)
: undefined;

if (this.isMultistream && mediaServer && mediaServer !== 'homer') {
throw new MultistreamNotSupportedError(
`Client asked for multistream backend (Homer), but got ${mediaServer} instead`
Expand All @@ -6343,6 +6358,10 @@ export default class Meeting extends StatelessWebexPlugin {
if (mediaServer) {
this.mediaProperties.webrtcMediaConnection.mediaServer = mediaServer;
}

if (this.isMultistream && mediaServerIp) {
this.mediaServerIp = mediaServerIp;
}
};

/**
Expand Down Expand Up @@ -7768,6 +7787,11 @@ export default class Meeting extends StatelessWebexPlugin {
const reachabilityStats = await this.webex.meetings.reachability.getReachabilityMetrics();
const iceCandidateErrors = Object.fromEntries(this.iceCandidateErrors);

// @ts-ignore
const isSubnetReachable = this.webex.meetings.reachability.isSubnetReachable(
this.mediaServerIp
);

Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS, {
correlation_id: this.correlationId,
locus_id: this.locusUrl.split('/').pop(),
Expand All @@ -7777,6 +7801,7 @@ export default class Meeting extends StatelessWebexPlugin {
isMultistream: this.isMultistream,
retriedWithTurnServer: this.addMediaData.retriedWithTurnServer,
isJoinWithMediaRetry: this.joinWithMediaRetryInfo.isRetry,
isSubnetReachable,
...reachabilityStats,
...iceCandidateErrors,
iceCandidatesCount: this.iceCandidatesCount,
Expand Down Expand Up @@ -7806,6 +7831,11 @@ export default class Meeting extends StatelessWebexPlugin {

const iceCandidateErrors = Object.fromEntries(this.iceCandidateErrors);

// @ts-ignore
const isSubnetReachable = this.webex.meetings.reachability.isSubnetReachable(
this.mediaServerIp
);

Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
correlation_id: this.correlationId,
locus_id: this.locusUrl.split('/').pop(),
Expand Down Expand Up @@ -7835,6 +7865,7 @@ export default class Meeting extends StatelessWebexPlugin {
this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
'unknown',
...reachabilityMetrics,
isSubnetReachable,
...iceCandidateErrors,
iceCandidatesCount: this.iceCandidatesCount,
});
Expand Down
18 changes: 18 additions & 0 deletions packages/@webex/plugin-meetings/src/meetings/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,24 @@ MeetingsUtil.getMediaServer = (sdp) => {
return mediaServer;
};

MeetingsUtil.getMediaServerIp = (sdp) => {
let mediaServerIp;

// Attempt to collect the media server from the roap message.
try {
mediaServerIp = sdp
.split('\r\n')
.find((line) => line.startsWith('o='))
.match(/o=\S+ \d+ \d+ IN IP4 ([\d.]+)/)?.[1]
.toLowerCase()
.trim();
} catch {
mediaServerIp = undefined;
}

return mediaServerIp;
};

MeetingsUtil.checkForCorrelationId = (deviceUrl, locus) => {
let devices = [];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export class ClusterReachability extends EventsScope {
private srflxIceCandidates: RTCIceCandidate[] = [];
public readonly isVideoMesh: boolean;
public readonly name;
public readonly reachedSubnets: Set<string> = new Set();

/**
* Constructor for ClusterReachability
Expand Down Expand Up @@ -234,27 +235,13 @@ export class ClusterReachability extends EventsScope {
*/
private registerIceGatheringStateChangeListener() {
this.pc.onicegatheringstatechange = () => {
const {COMPLETE} = ICE_GATHERING_STATE;

if (this.pc.iceConnectionState === COMPLETE) {
if (this.pc.iceGatheringState === ICE_GATHERING_STATE.COMPLETE) {
this.closePeerConnection();
this.finishReachabilityCheck();
}
};
}

/**
* Checks if we have the results for all the protocols (UDP and TCP)
*
* @returns {boolean} true if we have all results, false otherwise
*/
private haveWeGotAllResults(): boolean {
return ['udp', 'tcp', 'xtls'].every(
(protocol) =>
this.result[protocol].result === 'reachable' || this.result[protocol].result === 'untested'
);
}

/**
* Saves the latency in the result for the given protocol and marks it as reachable,
* emits the "resultReady" event if this is the first result for that protocol,
Expand All @@ -264,9 +251,15 @@ export class ClusterReachability extends EventsScope {
* @param {string} protocol
* @param {number} latency
* @param {string|null} [publicIp]
* @param {string|null} [serverIp]
* @returns {void}
*/
private saveResult(protocol: 'udp' | 'tcp' | 'xtls', latency: number, publicIp?: string | null) {
private saveResult(
protocol: 'udp' | 'tcp' | 'xtls',
latency: number,
publicIp?: string | null,
serverIp?: string | null
) {
const result = this.result[protocol];

if (result.latencyInMilliseconds === undefined) {
Expand Down Expand Up @@ -294,6 +287,10 @@ export class ClusterReachability extends EventsScope {
} else {
this.addPublicIP(protocol, publicIp);
}

if (serverIp) {
this.reachedSubnets.add(serverIp);
}
}

/**
Expand Down Expand Up @@ -351,21 +348,25 @@ export class ClusterReachability extends EventsScope {

if (e.candidate) {
if (e.candidate.type === CANDIDATE_TYPES.SERVER_REFLEXIVE) {
this.saveResult('udp', latencyInMilliseconds, e.candidate.address);
let serverIp = null;
if ('url' in e.candidate) {
const stunServerUrlRegex = /stun:([\d.]+):\d+/;

const match = (e.candidate as any).url.match(stunServerUrlRegex);
if (match) {
// eslint-disable-next-line prefer-destructuring
serverIp = match[1];
}
}

this.saveResult('udp', latencyInMilliseconds, e.candidate.address, serverIp);
Comment on lines +356 to +362
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: use ternary operator

Suggested change
if (match) {
// eslint-disable-next-line prefer-destructuring
serverIp = match[1];
}
}
this.saveResult('udp', latencyInMilliseconds, e.candidate.address, serverIp);
const match = (e.candidate as any).url.match(regex);
match && ([, serverIp] = match);
this.saveResult('udp', latencyInMilliseconds, e.candidate.address, serverIp);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this line is good readable.. match && ([, serverIp] = match);


this.determineNatType(e.candidate);
}

if (e.candidate.type === CANDIDATE_TYPES.RELAY) {
const protocol = e.candidate.port === TURN_TLS_PORT ? 'xtls' : 'tcp';
this.saveResult(protocol, latencyInMilliseconds);
// we don't add public IP for TCP, because in the case of relay candidates
// e.candidate.address is the TURN server address, not the client's public IP
}

if (this.haveWeGotAllResults()) {
this.closePeerConnection();
this.finishReachabilityCheck();
this.saveResult(protocol, latencyInMilliseconds, null, e.candidate.address);
}
}
};
Expand Down
54 changes: 54 additions & 0 deletions packages/@webex/plugin-meetings/src/reachability/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,60 @@ export default class Reachability extends EventsScope {
}
}

/**
* Checks if the given subnet is reachable
* @param {string} mediaServerIp - media server ip
* @returns {boolean | null} true if reachable, false if not reachable, null if mediaServerIp is not provided
* @public
* @memberof Reachability
*/
public isSubnetReachable(mediaServerIp?: string): boolean | null {
if (!mediaServerIp) {
LoggerProxy.logger.error(`Reachability:index#isSubnetReachable --> mediaServerIp is null`);

return null;
}

const subnetFirstOctet = mediaServerIp.split('.')[0];

LoggerProxy.logger.info(
`Reachability:index#isSubnetReachable --> Looking for subnet: ${subnetFirstOctet}.X.X.X`
);

const matchingReachedClusters = Object.values(this.clusterReachability).reduce(
(acc, cluster) => {
const reachedSubnetsArray = Array.from(cluster.reachedSubnets);

let logMessage = `Reachability:index#isSubnetReachable --> Cluster ${cluster.name} reached [`;
for (let i = 0; i < reachedSubnetsArray.length; i += 1) {
const subnet = reachedSubnetsArray[i];
const reachedSubnetFirstOctet = subnet.split('.')[0];

if (subnetFirstOctet === reachedSubnetFirstOctet) {
acc.add(cluster.name);
}

logMessage += `${subnet}`;
if (i < reachedSubnetsArray.length - 1) {
logMessage += ',';
}
}
logMessage += `]`;

LoggerProxy.logger.info(logMessage);

return acc;
},
new Set<string>()
);

LoggerProxy.logger.info(
`Reachability:index#isSubnetReachable --> Found ${matchingReachedClusters.size} clusters that use the subnet ${subnetFirstOctet}.X.X.X`
);

return matchingReachedClusters.size > 0;
}

/**
* Gets a list of media clusters from the backend and performs reachability checks on all the clusters
* @param {string} trigger - explains the reason for starting reachability
Expand Down
Loading
Loading