Skip to content

Commit

Permalink
un support ice multiple components
Browse files Browse the repository at this point in the history
  • Loading branch information
shinyoshiaki committed Jan 1, 2024
1 parent d71258a commit 661def0
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 95 deletions.
109 changes: 14 additions & 95 deletions packages/ice/src/ice.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { randomBytes } from "crypto";
import { isIPv4 } from "net";
import os from "os";

import debug from "debug";
import { Uint64BE } from "int64-buffer";
import * as nodeIp from "ip";

import isEqual from "lodash/isEqual";
import range from "lodash/range";
import PCancelable from "p-cancelable";
import { Event } from "rx.mini";
import timers from "timers/promises";
Expand All @@ -20,7 +19,7 @@ import { Message, parseMessage } from "./stun/message";
import { StunProtocol } from "./stun/protocol";
import { createTurnEndpoint } from "./turn/protocol";
import { Address, Protocol } from "./types/model";
import { normalizeFamilyNodeV18 } from "./utils";
import { getHostAddresses, normalizeFamilyNodeV18 } from "./utils";

const log = debug("werift-ice : packages/ice/src/ice.ts : log");

Expand All @@ -38,15 +37,11 @@ export class Connection {
useIpv6: boolean;
options: IceOptions;
remoteCandidatesEnd = false;
/**コンポーネントはデータストリームの一部です. データストリームには複数のコンポーネントが必要な場合があり、
* データストリーム全体が機能するには、それぞれが機能する必要があります.
* RTP / RTCPデータストリームの場合、RTPとRTCPが同じポートで多重化されていない限り、データストリームごとに2つのコンポーネントがあります.
* 1つはRTP用、もう1つはRTCP用です. コンポーネントには候補ペアがあり、他のコンポーネントでは使用できません. */
_components: Set<number>;
_localCandidatesEnd = false;
_tieBreaker: bigint = BigInt(new Uint64BE(randomBytes(64)).toString());
state: IceState = "new";
dnsLookup?: DnsLookup;
restarted = false;

readonly onData = new Event<[Buffer, number]>();
readonly stateChanged = new Event<[IceState]>();
Expand All @@ -71,13 +66,11 @@ export class Connection {
...defaultOptions,
...options,
};
const { components, stunServer, turnServer, useIpv4, useIpv6 } =
this.options;
const { stunServer, turnServer, useIpv4, useIpv6 } = this.options;
this.stunServer = validateAddress(stunServer);
this.turnServer = validateAddress(turnServer);
this.useIpv4 = useIpv4;
this.useIpv6 = useIpv6;
this._components = new Set(range(1, components + 1));
}

setRemoteParams({
Expand Down Expand Up @@ -117,15 +110,8 @@ export class Connection {
);
}

for (const component of this._components) {
const candidates = await this.getComponentCandidates(
component,
address,
5,
cb,
);
this.localCandidates = [...this.localCandidates, ...candidates];
}
const candidates = await this.getComponentCandidates(address, 5, cb);
this.localCandidates = [...this.localCandidates, ...candidates];

this._localCandidatesEnd = true;
this.promiseGatherCandidates.execute();
Expand All @@ -134,7 +120,6 @@ export class Connection {
}

private async getComponentCandidates(
component: number,
addresses: string[],
timeout = 5,
cb?: (candidate: Candidate) => void,
Expand Down Expand Up @@ -164,9 +149,9 @@ export class Connection {

protocol.localCandidate = new Candidate(
candidateFoundation("host", "udp", candidateAddress[0]),
component,
1,
"udp",
candidatePriority(component, "host"),
candidatePriority(1, "host"),
candidateAddress[0],
candidateAddress[1],
"host",
Expand Down Expand Up @@ -231,9 +216,9 @@ export class Connection {

protocol.localCandidate = new Candidate(
candidateFoundation("relay", "udp", candidateAddress[0]),
component,
1,
"udp",
candidatePriority(component, "relay"),
candidatePriority(1, "relay"),
candidateAddress[0],
candidateAddress[1],
"relay",
Expand Down Expand Up @@ -330,9 +315,7 @@ export class Connection {

private unfreezeInitial() {
// # unfreeze first pair for the first component
const firstPair = this.checkList.find(
(pair) => pair.component === Math.min(...[...this._components]),
);
const [firstPair] = this.checkList;
if (!firstPair) return;
if (firstPair.state === CandidatePairState.FROZEN) {
this.setPairState(firstPair, CandidatePairState.WAITING);
Expand Down Expand Up @@ -499,7 +482,6 @@ export class Connection {
// """

if (!remoteCandidate) {
this.pruneComponents();
this.remoteCandidatesEnd = true;
return;
}
Expand Down Expand Up @@ -666,23 +648,13 @@ export class Connection {
}
this.remoteCandidates.push(remoteCandidate);
}
this.pruneComponents();

this.remoteCandidatesEnd = true;
}
get remoteCandidates() {
return this._remoteCandidates;
}

private pruneComponents() {
const seenComponents = new Set(
this.remoteCandidates.map((v) => v.component),
);
const missingComponents = [...difference(this._components, seenComponents)];
if (missingComponents.length > 0) {
this._components = seenComponents;
}
}

private sortCheckList() {
sortCandidatePairs(this.checkList, this.iceControlling);
}
Expand Down Expand Up @@ -747,7 +719,7 @@ export class Connection {
// Once there is at least one nominated pair in the valid list for
// every component of at least one media stream and the state of the
// check list is Running:
if (this.nominatedKeys.length === this._components.size) {
if (this.nominatedKeys.length === 1) {
if (!this.checkListDone) {
log("ICE completed");
this.checkListState.put(new Promise((r) => r(ICE_COMPLETED)));
Expand Down Expand Up @@ -1059,7 +1031,6 @@ export enum CandidatePairState {
type IceState = "disconnected" | "closed" | "completed" | "new" | "connected";

export interface IceOptions {
components: number;
stunServer?: Address;
turnServer?: Address;
turnUsername?: string;
Expand All @@ -1081,7 +1052,6 @@ export interface IceOptions {
}

const defaultOptions: IceOptions = {
components: 1,
useIpv4: true,
useIpv6: true,
};
Expand Down Expand Up @@ -1127,57 +1097,6 @@ export function candidatePairPriority(
return (1 << 32) * Math.min(G, D) + 2 * Math.max(G, D) + (G > D ? 1 : 0);
}

function isAutoconfigurationAddress(info: os.NetworkInterfaceInfo) {
return (
normalizeFamilyNodeV18(info.family) === 4 &&
info.address?.startsWith("169.254.")
);
}

function nodeIpAddress(family: number): string[] {
// https://chromium.googlesource.com/external/webrtc/+/master/rtc_base/network.cc#236
const costlyNetworks = ["ipsec", "tun", "utun", "tap"];
const banNetworks = ["vmnet", "veth"];

const interfaces = os.networkInterfaces();

const all = Object.keys(interfaces)
.map((nic) => {
for (const word of [...costlyNetworks, ...banNetworks]) {
if (nic.startsWith(word)) {
return {
nic,
addresses: [],
};
}
}
const addresses = interfaces[nic]!.filter(
(details) =>
normalizeFamilyNodeV18(details.family) === family &&
!nodeIp.isLoopback(details.address) &&
!isAutoconfigurationAddress(details),
);
return {
nic,
addresses: addresses.map((address) => address.address),
};
})
.filter((address) => !!address);

// os.networkInterfaces doesn't actually return addresses in a good order.
// have seen instances where en0 (ethernet) is after en1 (wlan), etc.
// eth0 > eth1
all.sort((a, b) => a.nic.localeCompare(b.nic));
return Object.values(all).flatMap((entry) => entry.addresses);
}

export function getHostAddresses(useIpv4: boolean, useIpv6: boolean) {
const address: string[] = [];
if (useIpv4) address.push(...nodeIpAddress(4));
if (useIpv6) address.push(...nodeIpAddress(6));
return address;
}

export async function serverReflexiveCandidate(
protocol: Protocol,
stunServer: Address,
Expand Down
53 changes: 53 additions & 0 deletions packages/ice/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { InterfaceAddresses } from "../../common/src/network";
import * as nodeIp from "ip";
import os from "os";
import { Connection, serverReflexiveCandidate } from "./ice";
import { StunProtocol } from "./stun/protocol";
import { Address } from "./types/model";
Expand Down Expand Up @@ -36,3 +38,54 @@ export function normalizeFamilyNodeV18(family: string | number): 4 | 6 {

return family as 4 | 6;
}

function isAutoconfigurationAddress(info: os.NetworkInterfaceInfo) {
return (
normalizeFamilyNodeV18(info.family) === 4 &&
info.address?.startsWith("169.254.")
);
}

function nodeIpAddress(family: number): string[] {
// https://chromium.googlesource.com/external/webrtc/+/master/rtc_base/network.cc#236
const costlyNetworks = ["ipsec", "tun", "utun", "tap"];
const banNetworks = ["vmnet", "veth"];

const interfaces = os.networkInterfaces();

const all = Object.keys(interfaces)
.map((nic) => {
for (const word of [...costlyNetworks, ...banNetworks]) {
if (nic.startsWith(word)) {
return {
nic,
addresses: [],
};
}
}
const addresses = interfaces[nic]!.filter(
(details) =>
normalizeFamilyNodeV18(details.family) === family &&
!nodeIp.isLoopback(details.address) &&
!isAutoconfigurationAddress(details),
);
return {
nic,
addresses: addresses.map((address) => address.address),
};
})
.filter((address) => !!address);

// os.networkInterfaces doesn't actually return addresses in a good order.
// have seen instances where en0 (ethernet) is after en1 (wlan), etc.
// eth0 > eth1
all.sort((a, b) => a.nic.localeCompare(b.nic));
return Object.values(all).flatMap((entry) => entry.addresses);
}

export function getHostAddresses(useIpv4: boolean, useIpv6: boolean) {
const address: string[] = [];
if (useIpv4) address.push(...nodeIpAddress(4));
if (useIpv6) address.push(...nodeIpAddress(6));
return address;
}

0 comments on commit 661def0

Please sign in to comment.