diff --git a/README.md b/README.md index 16629a0..25228fa 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ usage: h2tunnel [options] commands: client server - + client options: --crt Path to certificate file (.crt) --key Path to private key file (.key) @@ -61,14 +61,14 @@ client options: server options: --crt Path to certificate file (.crt) --key Path to private key file (.key) - --tunnel-listen-ip IP for the tunnel server to bind on (default: 0.0.0.0) + --tunnel-listen-ip IP for the tunnel server to bind on (default: ::0) --tunnel-listen-port Port for the tunnel server to listen on - --proxy-listen-ip IP for the remote TCP proxy server to bind on (default: 0.0.0.0) + --proxy-listen-ip IP for the remote TCP proxy server to bind on (default: ::0) --proxy-listen-port Port for the remote TCP proxy server to listen on - -The tunnel and proxy servers will bind to 0.0.0.0 by default which will make them publically available. This requires + +The tunnel and proxy servers will bind to ::0 by default which will make them publically available. This requires superuser permissions on Linux. You can change this setting to bind to a specific network interface, e.g. a VPN, but -this is advanced usage. +this is advanced usage. Note that on most operating systems, binding to ::0 will also bind to 0.0.0.0. ``` Generate `h2tunnel.key` and `h2tunnel.crt` files using `openssl` command: @@ -83,7 +83,7 @@ On your server (mysite.example.com), we will be listening for tunnel connections proxy on port 80. Make sure these are open in your firewall. ```bash -# sudo is required to bind to 0.0.0.0, which is necessary for public access +# sudo is required to bind to ::0, which is necessary for public access sudo h2tunnel server \ --crt h2tunnel.crt \ --key h2tunnel.key \ @@ -169,9 +169,9 @@ const server = new TunnelServer({ logger: (line) => console.log(line), // optional key: `-----BEGIN PRIVATE KEY----- ...`, cert: `-----BEGIN CERTIFICATE----- ...`, - tunnelListenIp: "0.0.0.0", // optional + tunnelListenIp: "::0", // optional tunnelListenPort: 15001, - proxyListenIp: "0.0.0.0", // optional + proxyListenIp: "::0", // optional proxyListenPort: 80, }); diff --git a/src/cli.ts b/src/cli.ts index 9526c13..58ec544 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -93,9 +93,9 @@ server options: --${"proxy-listen-ip" satisfies Param} IP for the remote TCP proxy server to bind on (default: ${DEFAULT_LISTEN_IP}) --${"proxy-listen-port" satisfies Param} Port for the remote TCP proxy server to listen on -The tunnel and proxy servers will bind to 0.0.0.0 by default which will make them publically available. This requires +The tunnel and proxy servers will bind to ::0 by default which will make them publically available. This requires superuser permissions on Linux. You can change this setting to bind to a specific network interface, e.g. a VPN, but -this is advanced usage. +this is advanced usage. Note that on most operating systems, binding to ::0 will also bind to 0.0.0.0. `; if (positionals.length === 0) { diff --git a/src/h2tunnel.test.ts b/src/h2tunnel.test.ts index 610470a..a7e1563 100644 --- a/src/h2tunnel.test.ts +++ b/src/h2tunnel.test.ts @@ -8,14 +8,17 @@ import { import net from "node:net"; // localhost echo server -const LOCAL_PORT = 14000; +const LOCAL_PORT = 15000; +// localhost echo server passed through network emulator +const LOCAL2_PORT = 15007; + +// remote public echo server forwarded by h2tunnel +const PROXY_PORT = 15004; -// remote public HTTP1 server -const PROXY_PORT = 14004; -const PROXY_TEST_PORT = 14007; // remote TLS server for establishing a tunnel -const TUNNEL_PORT = 14005; -const TUNNEL_TEST_PORT = 14008; +const TUNNEL_PORT = 15005; +// remote TLS server for establishing a tunnel passed through network emulator +const TUNNEL2_PORT = 15008; // Reduce this to make tests faster const TIME_MULTIPLIER = 0.1; @@ -50,9 +53,9 @@ const serverOptions: ServerOptions = { logger: getLogger("server", 32), key: CLIENT_KEY, cert: CLIENT_CRT, - tunnelListenIp: "127.0.0.1", + tunnelListenIp: "::1", tunnelListenPort: TUNNEL_PORT, - proxyListenIp: "127.0.0.1", + proxyListenIp: "::1", proxyListenPort: PROXY_PORT, }; @@ -60,14 +63,14 @@ const clientOptions: ClientOptions = { logger: getLogger("client", 33), key: CLIENT_KEY, cert: CLIENT_CRT, - tunnelHost: "localhost", + tunnelHost: "::1", tunnelPort: TUNNEL_PORT, - originHost: "localhost", + originHost: "::1", originPort: LOCAL_PORT, tunnelRestartTimeout: 500 * TIME_MULTIPLIER, }; -type Conn = { browserSocket: net.Socket; localhostSocket: net.Socket }; +type Conn = { browserSocket: net.Socket; originSocket: net.Socket }; async function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms * TIME_MULTIPLIER)); @@ -110,12 +113,19 @@ async function createBadTlsClient(port: number): Promise<() => Promise> { }); } +interface NetworkEmulatorParams { + listenHost: string; + listenPort: number; + forwardHost: string; + forwardPort: number; +} + class NetworkEmulator { incomingSocket: net.Socket | null = null; outgoingSocket: net.Socket | null = null; + constructor( - readonly forwardPort: number, - readonly listenPort: number, + readonly params: NetworkEmulatorParams, readonly server = net.createServer({ allowHalfOpen: true }), readonly logger = getLogger("network", 31), readonly abortController = new AbortController(), @@ -126,8 +136,8 @@ class NetworkEmulator { this.server.on("connection", (incomingSocket: net.Socket) => { this.incomingSocket = incomingSocket; const outgoingSocket = net.createConnection({ - host: "127.0.0.1", - port: this.forwardPort, + host: this.params.forwardHost, + port: this.params.forwardPort, allowHalfOpen: true, }); this.outgoingSocket = outgoingSocket; @@ -138,7 +148,7 @@ class NetworkEmulator { outgoingSocket.pipe(incomingSocket); }); this.server.on("listening", () => resolve()); - this.server.listen(this.listenPort, "127.0.0.1"); + this.server.listen(this.params.listenPort, this.params.listenHost); }); } async stopAndWaitUntilClosed() { @@ -147,24 +157,30 @@ class NetworkEmulator { } } -class EchoServer { - readonly server: net.Server; +interface EchoServerParams { + originListenHost: string; + originListenPort: number; + proxyHost: string; + proxyPort: number; +} + +class EchoTester { readonly dataReceived: Map = new Map(); private i = 0; + constructor( - readonly port: number, - public proxyPort = port, - readonly loggerLocalhost = getLogger("localhost", 35), + readonly params: EchoServerParams, + readonly loggerOrigin = getLogger("origin", 35), readonly loggerBrowser = getLogger("browser", 36), + readonly server = net.createServer({ allowHalfOpen: true }), ) { - const server = net.createServer({ allowHalfOpen: true }); - server.on("connection", (socket) => { - loggerLocalhost({ localhost: "connection" }); + this.server.on("connection", (socket) => { + loggerOrigin({ localhost: "connection" }); socket.on("error", (err) => { - loggerLocalhost({ localhost: "error", err }); + loggerOrigin({ localhost: "error", err }); }); socket.on("data", (data) => { - this.loggerLocalhost({ + this.loggerOrigin({ echoServerData: data.toString(), socketWritableEnded: socket.writableEnded, }); @@ -175,14 +191,12 @@ class EchoServer { }); // Make sure other end stays half-open long enough to receive the last byte socket.on("end", async () => { - loggerLocalhost({ localhost: "received FIN" }); + loggerOrigin({ localhost: "received FIN" }); await sleep(50); - loggerLocalhost({ localhost: "sending last byte and FIN" }); + loggerOrigin({ localhost: "sending last byte and FIN" }); socket.end("z"); }); }); - - this.server = server; } appendData(socket: net.Socket, data: Buffer): void { @@ -200,10 +214,10 @@ class EchoServer { } async startAndWaitUntilReady() { - this.server.listen(this.port); + this.server.listen(this.params.originListenPort); await new Promise((resolve) => this.server.on("listening", () => { - this.loggerLocalhost({ localhost: "listening" }); + this.loggerOrigin({ localhost: "listening" }); resolve(); }), ); @@ -216,7 +230,8 @@ class EchoServer { createClientSocket(): net.Socket { this.loggerBrowser({ browser: "connecting" }); const socket = net.createConnection({ - port: this.proxyPort, + host: this.params.proxyHost, + port: this.params.proxyPort, allowHalfOpen: true, }); socket.on("data", (chunk) => { @@ -234,7 +249,10 @@ class EchoServer { async expectEconn() { return new Promise((resolve, reject) => { - const socket = net.createConnection(this.proxyPort); + const socket = net.createConnection( + this.params.proxyPort, + this.params.proxyHost, + ); socket.on("error", () => {}); socket.on("close", (hadError) => { if (hadError) { @@ -249,7 +267,7 @@ class EchoServer { async expectPingPongAndClose(t: TestContext) { const conn = await this.createConn(t); await new Promise((resolve) => { - conn.localhostSocket.once("data", (pong) => { + conn.originSocket.once("data", (pong) => { t.assert.strictEqual(pong.toString(), "a"); // Send FIN from client and wait for FIN back conn.browserSocket.end(); @@ -275,7 +293,7 @@ class EchoServer { }); await sleep(100); const localhostSocket = this.getSocketByPrefix(id); - return { browserSocket, localhostSocket }; + return { browserSocket, originSocket: localhostSocket }; } async testConn( @@ -289,7 +307,7 @@ class EchoServer { const conn = await this.createConn(t); for (let i = 0; i < numBytes; i++) { await new Promise((resolve) => { - conn.localhostSocket.once("data", (pong) => { + conn.originSocket.once("data", (pong) => { t.assert.strictEqual(pong.toString(), "a"); resolve(); }); @@ -300,8 +318,8 @@ class EchoServer { const [socket1, socket2] = by === "browser" - ? [conn.browserSocket, conn.localhostSocket] - : [conn.localhostSocket, conn.browserSocket]; + ? [conn.browserSocket, conn.originSocket] + : [conn.originSocket, conn.browserSocket]; if (term === "FIN") { t.assert.strictEqual(socket2.readyState, "open"); @@ -367,65 +385,120 @@ class EchoServer { } } -await test.only( +async function withClientAndServer( + clientOverrides: Partial, + serverOverrides: Partial, + func: (client: TunnelClient, server: TunnelServer) => Promise, +) { + const server = new TunnelServer({ ...serverOptions, ...serverOverrides }); + server.start(); + await server.waitUntilListening(); + const client = new TunnelClient({ ...clientOptions, ...clientOverrides }); + client.start(); + await client.waitUntilConnected(); + await server.waitUntilConnected(); + + await func(client, server); + + await client.stop(); + await server.stop(); +} + +async function runTests(t: TestContext, params: EchoServerParams) { + for (const term of ["FIN", "RST"] satisfies ("FIN" | "RST")[]) { + for (const by of ["browser", "localhost"] satisfies ( + | "browser" + | "localhost" + )[]) { + console.log( + `clean termination by ${by} ${term} on ${params.proxyHost}:${params.proxyPort}`, + ); + const echoServer = new EchoTester(params); + await echoServer.startAndWaitUntilReady(); + // Test single + await echoServer.testConn(t, 1, term, by, 0); + await echoServer.testConn(t, 4, term, by, 0); + // Test double simultaneous + await Promise.all([ + echoServer.testConn(t, 3, term, by, 0), + echoServer.testConn(t, 3, term, by, 0), + ]); + // Test triple delayed + await Promise.all([ + echoServer.testConn(t, 4, term, by, 0), + echoServer.testConn(t, 4, term, by, 10), + echoServer.testConn(t, 4, term, by, 100), + ]); + await echoServer.stopAndWaitUntilClosed(); + } + } +} + +await test( "basic connection and termination", { timeout: 10000 }, async (t) => { - const net = new NetworkEmulator(LOCAL_PORT, PROXY_TEST_PORT); - const server = new TunnelServer(serverOptions); - const client = new TunnelClient(clientOptions); - server.start(); - client.start(); - await server.waitUntilListening(); - await client.waitUntilConnected(); - await server.waitUntilConnected(); - await net.startAndWaitUntilReady(); - for (const term of ["FIN", "RST"] satisfies ("FIN" | "RST")[]) { - for (const by of ["browser", "localhost"] satisfies ( - | "browser" - | "localhost" - )[]) { - for (const proxyPort of [LOCAL_PORT, PROXY_TEST_PORT, PROXY_PORT]) { - // if (term !== "RST" || by !== "browser" || proxyPort !== PROXY_PORT) { - // continue; - // } - // clean termination by browser RST on 14004 - console.log(`clean termination by ${by} ${term} on ${proxyPort}`); - const echoServer = new EchoServer(LOCAL_PORT, proxyPort); - await echoServer.startAndWaitUntilReady(); - // Test single - await echoServer.testConn(t, 1, term, by, 0); - await echoServer.testConn(t, 4, term, by, 0); - // Test double simultaneous - await Promise.all([ - echoServer.testConn(t, 3, term, by, 0), - echoServer.testConn(t, 3, term, by, 0), - ]); - // Test triple delayed - await Promise.all([ - echoServer.testConn(t, 4, term, by, 0), - echoServer.testConn(t, 4, term, by, 10), - echoServer.testConn(t, 4, term, by, 100), - ]); - await echoServer.stopAndWaitUntilClosed(); - } - } - } + for (const localIp of ["127.0.0.1", "::1"]) { + // Run EchoServer tests without proxy or tunnel + await runTests(t, { + originListenHost: localIp, + originListenPort: LOCAL_PORT, + proxyHost: localIp, + proxyPort: LOCAL_PORT, + }); - await net.stopAndWaitUntilClosed(); - await client.stop(); - await server.stop(); + // Test NetworkEmulator using EchoServer + const net = new NetworkEmulator({ + listenHost: localIp, + listenPort: LOCAL2_PORT, + forwardHost: localIp, + forwardPort: LOCAL_PORT, + }); + await net.startAndWaitUntilReady(); + await runTests(t, { + originListenHost: localIp, + originListenPort: LOCAL_PORT, + proxyHost: localIp, + proxyPort: LOCAL2_PORT, + }); + await net.stopAndWaitUntilClosed(); + + // Test EchoServer through default tunnel + await withClientAndServer( + { + originHost: localIp, + tunnelHost: localIp, + }, + { + tunnelListenIp: localIp, + proxyListenIp: localIp, + }, + async () => { + await runTests(t, { + originListenHost: localIp, + originListenPort: LOCAL_PORT, + proxyHost: localIp, + proxyPort: PROXY_PORT, + }); + }, + ); + } }, ); -await test("happy-path", { timeout: 5000 }, async (t) => { - const echo = new EchoServer(LOCAL_PORT, PROXY_PORT); +await test.only("happy-path", { timeout: 5000 }, async (t) => { + const echo = new EchoTester({ + originListenHost: "::1", + originListenPort: LOCAL_PORT, + proxyHost: "::1", + proxyPort: PROXY_PORT, + }); await echo.startAndWaitUntilReady(); const server = new TunnelServer(serverOptions); const client = new TunnelClient({ ...clientOptions, - tunnelPort: TUNNEL_TEST_PORT, + tunnelPort: TUNNEL2_PORT, }); server.start(); @@ -433,7 +506,12 @@ await test("happy-path", { timeout: 5000 }, async (t) => { await echo.expectEconn(); await server.waitUntilListening(); - const net = new NetworkEmulator(TUNNEL_PORT, TUNNEL_TEST_PORT); + const net = new NetworkEmulator({ + listenHost: "::1", + listenPort: TUNNEL2_PORT, + forwardHost: "::1", + forwardPort: TUNNEL_PORT, + }); await net.startAndWaitUntilReady(); client.start(); @@ -442,6 +520,7 @@ await test("happy-path", { timeout: 5000 }, async (t) => { // Wait until client is connected and test 200 await client.waitUntilConnected(); + // Make two simultaneous slow requests await Promise.all([ echo.expectPingPongAndClose(t), @@ -478,9 +557,9 @@ await test("happy-path", { timeout: 5000 }, async (t) => { await client.waitUntilConnected(); await echo.expectPingPongAndClose(t); - // Break tunnel during a request but before response headers could be sent + // Break tunnel during a request const promise1 = echo.expectEconn(); - await sleep(10); + await sleep(5); net.incomingSocket!.resetAndDestroy(); net.outgoingSocket!.resetAndDestroy(); await sleep(10); @@ -493,7 +572,12 @@ await test("happy-path", { timeout: 5000 }, async (t) => { }); await test("garbage-to-client", { timeout: 5000 }, async (t: TestContext) => { - const echoServer = new EchoServer(LOCAL_PORT, PROXY_PORT); + const echoServer = new EchoTester({ + originListenHost: "::1", + originListenPort: LOCAL_PORT, + proxyHost: "::1", + proxyPort: PROXY_PORT, + }); await echoServer.startAndWaitUntilReady(); const stopBadServer = await createBadTlsServer(TUNNEL_PORT); const client = new TunnelClient(clientOptions); @@ -518,7 +602,12 @@ await test("garbage-to-client", { timeout: 5000 }, async (t: TestContext) => { }); await test("garbage-to-server", { timeout: 5000 }, async (t: TestContext) => { - const echoServer = new EchoServer(LOCAL_PORT, PROXY_PORT); + const echoServer = new EchoTester({ + originListenHost: "::1", + originListenPort: LOCAL_PORT, + proxyHost: "::1", + proxyPort: PROXY_PORT, + }); await echoServer.startAndWaitUntilReady(); const server = new TunnelServer(serverOptions); server.start(); diff --git a/src/h2tunnel.ts b/src/h2tunnel.ts index 19f6f1a..b78e3ef 100644 --- a/src/h2tunnel.ts +++ b/src/h2tunnel.ts @@ -12,8 +12,9 @@ export interface CommonOptions { cert: string; } -export const DEFAULT_LISTEN_IP = "0.0.0.0"; +export const DEFAULT_LISTEN_IP = "::0"; export const DEFAULT_ORIGIN_HOST = "localhost"; +export const MUX_SERVER_HOST = "127.0.0.1"; export interface ServerOptions extends CommonOptions { tunnelListenIp?: string; @@ -37,10 +38,11 @@ export abstract class AbstractTunnel< M extends net.Server, > extends events.EventEmitter> { state: TunnelState = "stopped"; - aborted: boolean; + aborted: boolean = false; + connected: boolean = false; tunnelSocket: tls.TLSSocket | null = null; muxSocket: net.Socket | null = null; - h2session: S | null = null; + h2Session: S | null = null; abstract init(): void; abstract isListening(): boolean; protected constructor( @@ -54,7 +56,10 @@ export abstract class AbstractTunnel< this.log({ muxServer: "drop", options }); }); muxServer.on("listening", () => { - this.log({ muxServer: "listening", port: this.muxServerPort }); + this.log({ + muxServer: "listening", + ...(muxServer.address() as net.AddressInfo), + }); this.updateHook(); }); muxServer.on("error", (err) => { @@ -71,14 +76,15 @@ export abstract class AbstractTunnel< } setH2Session(session: S) { - this.h2session = session; - this.h2session.on("close", () => { - this.log({ h2session: "close" }); - this.h2session = null; + this.h2Session = session; + this.h2Session.on("close", () => { + this.log({ h2Session: "close" }); + this.h2Session = null; + this.connected = false; this.updateHook(); }); - this.h2session.on("error", (err) => { - this.log({ h2session: "error", err }); + this.h2Session.on("error", (err) => { + this.log({ h2Session: "error", err }); }); } @@ -113,8 +119,9 @@ export abstract class AbstractTunnel< this.tunnelSocket.on("close", () => { this.log({ tunnelSocket: "close", error: socket.errored }); this.muxSocket?.destroy(); - // Make sure error cascades to all active HTTP2 streams - this.h2session?.destroy(socket.errored ? new Error() : undefined); + // Regardless of why the tunnel closes, if there are ongoing HTTP2 streams, they have to be destroyed with an error + this.h2Session?.destroy(new Error()); + this.connected = false; this.updateHook(); }); this.linkSocketsIfNecessary(); @@ -175,7 +182,7 @@ export abstract class AbstractTunnel< let state: TunnelState; if (this.aborted) { state = this.isStopped() ? "stopped" : "stopping"; - } else if (this.h2session && this.tunnelSocket?.readyState === "open") { + } else if (this.connected) { state = "connected"; } else if (this.isListening()) { if (this.muxServer.listening) { @@ -183,7 +190,7 @@ export abstract class AbstractTunnel< state = "listening"; } else { this.log({ muxServer: "starting" }); - this.muxServer.listen(); // Let the OS pick a port + this.muxServer.listen(0, MUX_SERVER_HOST); // Let the OS pick a port state = "stopped"; } } else { @@ -205,7 +212,6 @@ export abstract class AbstractTunnel< isStopped() { return ( - !this.h2session && !this.muxServer.listening && !this.muxSocket && (!this.tunnelSocket || this.tunnelSocket.closed) @@ -216,7 +222,7 @@ export abstract class AbstractTunnel< this.log({ aborting: true }); this.muxSocket?.destroy(); this.tunnelSocket?.destroy(); - this.h2session?.destroy(); + this.h2Session?.destroy(); this.muxServer?.close(); this.updateHook(); } @@ -262,19 +268,22 @@ export class TunnelServer extends AbstractTunnel< }); proxyServer.on("connection", (socket: net.Socket) => { this.log({ proxyServer: "connection" }); - if (!this.h2session || this.h2session.destroyed) { + if (!this.connected) { socket.resetAndDestroy(); } else { this.addDemuxSocket( socket, - this.h2session.request({ + this.h2Session!.request({ [http2.constants.HTTP2_HEADER_METHOD]: "POST", }), ); } }); proxyServer.on("listening", () => { - this.log({ proxyServer: "listening" }); + this.log({ + proxyServer: "listening", + ...(proxyServer.address() as net.AddressInfo), + }); this.updateHook(); }); proxyServer.on("close", () => { @@ -286,7 +295,10 @@ export class TunnelServer extends AbstractTunnel< this.log({ tunnelServer: "drop", options }); }); tunnelServer.on("listening", () => { - this.log({ tunnelServer: "listening" }); + this.log({ + tunnelServer: "listening", + ...(tunnelServer.address() as net.AddressInfo), + }); this.updateHook(); }); tunnelServer.on("close", () => { @@ -338,14 +350,17 @@ export class TunnelServer extends AbstractTunnel< } init() { - if (this.tunnelSocket && !this.tunnelSocket.closed && !this.h2session) { - this.log({ muxSession: "starting" }); - const session = http2.connect(`http://localhost:${this.muxServerPort}`); - session.on("connect", () => { - this.log({ h2session: "connect" }); + if (this.tunnelSocket && !this.tunnelSocket.closed && !this.h2Session) { + this.log({ h2Session: "starting" }); + const h2Session = http2.connect( + `http://${MUX_SERVER_HOST}:${this.muxServerPort}`, + ); + h2Session.on("connect", () => { + this.log({ h2Session: "connect" }); + this.connected = true; this.updateHook(); }); - this.setH2Session(session); + this.setH2Session(h2Session); } } @@ -363,20 +378,25 @@ export class TunnelClient extends AbstractTunnel< constructor(readonly options: ClientOptions) { super(options.logger, http2.createServer()); - this.muxServer.on("session", (session: http2.ServerHttp2Session) => { + this.muxServer.on("session", (h2Session) => { this.log({ muxServer: "session" }); - this.setH2Session(session); - session.on("stream", (stream: http2.ServerHttp2Stream) => { - this.addDemuxSocket( - net.createConnection({ - host: this.options.originHost ?? DEFAULT_ORIGIN_HOST, - port: this.options.originPort, - allowHalfOpen: true, - }), - stream, - ); + h2Session.on("remoteSettings", () => { + this.log({ h2Session: "remoteSettings" }); + this.connected = true; + this.updateHook(); }); - this.updateHook(); + this.setH2Session(h2Session); + }); + this.muxServer.on("stream", (stream: http2.ServerHttp2Stream) => { + this.log({ muxServer: "stream" }); + this.addDemuxSocket( + net.createConnection({ + host: this.options.originHost ?? DEFAULT_ORIGIN_HOST, + port: this.options.originPort, + allowHalfOpen: true, + }), + stream, + ); }); } @@ -413,7 +433,7 @@ export class TunnelClient extends AbstractTunnel< init() { if (!this.muxSocket) { const muxSocket = net.createConnection({ - host: "localhost", + host: MUX_SERVER_HOST, port: this.muxServerPort, }); muxSocket.on("connect", () => {