Skip to content

Give a WebAssembly engine sockets

You can run an engine compiled to WebAssembly, written in C, C++, Go or Rust against Node’s net and dgram, unchanged on FKN sockets. This page covers taking the two modules as options, wiring the socket calls the compiled code makes back into JavaScript, copying every buffer you read, and reading a socket’s addresses once the relay has answered.

The relay is the server that holds the real socket at the far end of net and dgram. At the end, every socket call the engine makes reaches the relay, every read is a copy the engine owns, and every connection it accepts carries usable addresses. What the recipe costs:

  • Uses connect and createServer from @fkn/lib/net, createSocket from @fkn/lib/dgram
  • Needs nothing installed
  • Proven by four integrations

The first step carries the [Build] badge and the rest carry [Page], both defined on recipes. engine.ts is the engine’s host file, the JavaScript that loads the compiled module and answers its socket calls, and app.ts is the app that hands it the modules. The engine usually runs in a worker, and the optional step says what that adds.

An engine that imports @fkn/lib/net itself ties every host to the library, and to one copy of it. Take net and dgram as options instead. The app passes the library’s modules, a test passes Node’s, and the engine’s package declares @fkn/lib as an optional peer dependency, optional: true under peerDependenciesMeta, and imports nothing from it. The host file names the calls it makes and the members it reads on what they return:

engine.ts
export type
type Socket = {
readonly remoteAddress?: string;
readonly remotePort?: number;
readonly localAddress?: string;
readonly localPort?: number;
write: (chunk: Uint8Array) => boolean;
end: () => unknown;
destroy: () => unknown;
on: (event: string, listener: (...args: any[]) => void) => unknown;
}
Socket
= {
readonly
remoteAddress?: string | undefined
remoteAddress
?: string
readonly
remotePort?: number | undefined
remotePort
?: number
readonly
localAddress?: string | undefined
localAddress
?: string
readonly
localPort?: number | undefined
localPort
?: number
write: (chunk: Uint8Array) => boolean
write
: (
chunk: Uint8Array<ArrayBufferLike>
chunk
:
interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>

A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Uint8Array
) => boolean
end: () => unknown
end
: () => unknown
destroy: () => unknown
destroy
: () => unknown
on: (event: string, listener: (...args: any[]) => void) => unknown
on
: (
event: string
event
: string,
listener: (...args: any[]) => void
listener
: (...
args: any[]
args
: any[]) => void) => unknown
}
export type
type Datagram = {
bind: (port: number, address: string, listener?: () => void) => unknown;
send: (chunk: Uint8Array, port: number, address: string, callback?: (error: Error | null, bytes: number) => void) => void;
address: () => {
address: string;
port: number;
};
close: (callback?: () => void) => unknown;
on: (event: string, listener: (...args: any[]) => void) => unknown;
}
Datagram
= {
bind: (port: number, address: string, listener?: () => void) => unknown
bind
: (
port: number
port
: number,
address: string
address
: string,
listener: (() => void) | undefined
listener
?: () => void) => unknown
send: (chunk: Uint8Array, port: number, address: string, callback?: (error: Error | null, bytes: number) => void) => void
send
: (
chunk: Uint8Array<ArrayBufferLike>
chunk
:
interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>

A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Uint8Array
,
port: number
port
: number,
address: string
address
: string,
callback: ((error: Error | null, bytes: number) => void) | undefined
callback
?: (
error: Error | null
error
:
interface Error
Error
| null,
bytes: number
bytes
: number) => void) => void
address: () => {
address: string;
port: number;
}
address
: () => {
address: string
address
: string,
port: number
port
: number }
close: (callback?: () => void) => unknown
close
: (
callback: (() => void) | undefined
callback
?: () => void) => unknown
on: (event: string, listener: (...args: any[]) => void) => unknown
on
: (
event: string
event
: string,
listener: (...args: any[]) => void
listener
: (...
args: any[]
args
: any[]) => void) => unknown
}
export type
type Modules = {
net: {
connect: (options: {
host: string;
port: number;
}, listener?: () => void) => Socket;
};
dgram: {
createSocket: (type: "udp4" | "udp6") => Datagram;
};
}
Modules
= {
net: {
connect: (options: {
host: string;
port: number;
}, listener?: () => void) => Socket;
}
net
: {
connect: (options: {
host: string;
port: number;
}, listener?: () => void) => Socket
connect
: (
options: {
host: string;
port: number;
}
options
: {
host: string
host
: string,
port: number
port
: number },
listener: (() => void) | undefined
listener
?: () => void) =>
type Socket = {
readonly remoteAddress?: string;
readonly remotePort?: number;
readonly localAddress?: string;
readonly localPort?: number;
write: (chunk: Uint8Array) => boolean;
end: () => unknown;
destroy: () => unknown;
on: (event: string, listener: (...args: any[]) => void) => unknown;
}
Socket
}
dgram: {
createSocket: (type: "udp4" | "udp6") => Datagram;
}
dgram
: {
createSocket: (type: "udp4" | "udp6") => Datagram
createSocket
: (
type: "udp4" | "udp6"
type
: 'udp4' | 'udp6') =>
type Datagram = {
bind: (port: number, address: string, listener?: () => void) => unknown;
send: (chunk: Uint8Array, port: number, address: string, callback?: (error: Error | null, bytes: number) => void) => void;
address: () => {
address: string;
port: number;
};
close: (callback?: () => void) => unknown;
on: (event: string, listener: (...args: any[]) => void) => unknown;
}
Datagram
}
}
export const
const createEngine: ({ net, dgram }: Modules) => {
connect: (host: string, port: number) => number;
open: (type: "udp4" | "udp6") => Datagram;
}
createEngine
= ({
net: {
connect: (options: {
host: string;
port: number;
}, listener?: () => void) => Socket;
}
net
,
dgram: {
createSocket: (type: "udp4" | "udp6") => Datagram;
}
dgram
}:
type Modules = {
net: {
connect: (options: {
host: string;
port: number;
}, listener?: () => void) => Socket;
};
dgram: {
createSocket: (type: "udp4" | "udp6") => Datagram;
};
}
Modules
) => {
const
const sockets: Map<number, Socket>
sockets
= new
var Map: MapConstructor
new <number, Socket>(iterable?: Iterable<readonly [number, Socket]> | null | undefined) => Map<number, Socket> (+3 overloads)
Map
<number,
type Socket = {
readonly remoteAddress?: string;
readonly remotePort?: number;
readonly localAddress?: string;
readonly localPort?: number;
write: (chunk: Uint8Array) => boolean;
end: () => unknown;
destroy: () => unknown;
on: (event: string, listener: (...args: any[]) => void) => unknown;
}
Socket
>() // the number the compiled code holds on one side, the socket on the other
let
let next: number
next
= 1
return {
connect: (host: string, port: number) => number
connect
: (
host: string
host
: string,
port: number
port
: number) => {
const
const handle: number
handle
=
let next: number
next
++
const sockets: Map<number, Socket>
sockets
.
Map<number, Socket>.set(key: number, value: Socket): Map<number, Socket>

Adds a new element with a specified key and value to the Map. If an element with the same key already exists, the element will be updated.

set
(
const handle: number
handle
,
net: {
connect: (options: {
host: string;
port: number;
}, listener?: () => void) => Socket;
}
net
.
connect: (options: {
host: string;
port: number;
}, listener?: () => void) => Socket
connect
({
host: string
host
,
port: number
port
})) // the same call on node:net and on @fkn/lib/net
return
const handle: number
handle
},
open: (type: "udp4" | "udp6") => Datagram
open
: (
type: "udp4" | "udp6"
type
: 'udp4' | 'udp6') =>
dgram: {
createSocket: (type: "udp4" | "udp6") => Datagram;
}
dgram
.
createSocket: (type: "udp4" | "udp6") => Datagram
createSocket
(
type: "udp4" | "udp6"
type
),
}
}

Modules names one call on each module and the members the engine reads on what they return, by the names Node declares, so node:net and node:dgram satisfy it and the same file runs in a plain Node test with no broker, as testing shows. Add createServer the same way when the engine accepts peers. The app is the one file that imports the library:

app.ts
import * as
import net
net
from '@fkn/lib/net'
import * as
import dgram
dgram
from '@fkn/lib/dgram'
import {
const createEngine: ({ net, dgram }: Modules) => {
connect: (host: string, port: number) => number;
open: (type: "udp4" | "udp6") => Datagram;
}
createEngine
} from './engine'
const
const engine: {
connect: (host: string, port: number) => number;
open: (type: "udp4" | "udp6") => Datagram;
}
engine
=
function createEngine({ net, dgram }: Modules): {
connect: (host: string, port: number) => number;
open: (type: "udp4" | "udp6") => Datagram;
}
createEngine
({
net: {
connect: (options: {
host: string;
port: number;
}, listener?: () => void) => Socket;
}
net
,
dgram: {
createSocket: (type: "udp4" | "udp6") => Datagram;
}
dgram
}) // the library's modules, and the compiled code never learns which
const engine: {
connect: (host: string, port: number) => number;
open: (type: "udp4" | "udp6") => Datagram;
}
engine
.
connect: (host: string, port: number) => number
connect
('67.215.246.10', 6881) // 1, and the socket behind it is dialing through the relay

The engine never learns which module it holds. With the library’s, the bytes travel through the broker, the connection your page holds into FKN, to the relay. The bundle has to shim stream, events and buffer for the library’s module bodies to run, see install and bundle an app that uses sockets.

The compiled code holds a number for each socket and passes it back on every later call, so the host keeps a table from number to socket, and each call becomes one call on the module. On the page, against @fkn/lib/net directly, the three calls an engine makes most read like this:

app.ts
import * as
import net
net
from '@fkn/lib/net'
const
const sockets: Map<number, net.Socket>
sockets
= new
var Map: MapConstructor
new <number, net.Socket>(iterable?: Iterable<readonly [number, net.Socket]> | null | undefined) => Map<number, net.Socket> (+3 overloads)
Map
<number,
import net
net
.
class Socket
export Socket
Socket
>() // the number the compiled code holds on one side, the socket on the other
let
let next: number
next
= 1
const
const connect: (host: string, port: number) => number
connect
= (
host: string
host
: string,
port: number
port
: number) => {
const
const handle: number
handle
=
let next: number
next
++
const
const socket: net.Socket
socket
=
import net
net
.
function connect(options: SocketConnectOpts, connectionListener?: () => void): net.Socket
export connect
connect
({
TcpSocketConnectOpts.host?: string | undefined
host
,
TcpSocketConnectOpts.port: number
port
})
const socket: net.Socket
socket
.
Stream.Duplex.on<"error">(eventName: "error", listener: (err: Error) => void): net.Socket (+1 overload)

Adds the listener function to the end of the listeners array for the event named eventName. No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times.

server.on('connection', (stream) => {
console.log('someone connected!');
});

Returns a reference to the EventEmitter, so that calls can be chained.

By default, event listeners are invoked in the order they are added. The emitter.prependListener() method can be used as an alternative to add the event listener to the beginning of the listeners array.

import { EventEmitter } from 'node:events';
const myEE = new EventEmitter();
myEE.on('foo', () => console.log('a'));
myEE.prependListener('foo', () => console.log('b'));
myEE.emit('foo');
// Prints:
// b
// a

@sincev0.1.101

@parameventName The name of the event.

@paramlistener The callback function

on
('error', (
error: Error
error
:
interface Error
Error
) =>
error: Error
error
.
Error.message: string
message
) // a refused dial lands here, and nothing else fires
const sockets: Map<number, net.Socket>
sockets
.
Map<number, Socket$2>.set(key: number, value: net.Socket): Map<number, net.Socket>

Adds a new element with a specified key and value to the Map. If an element with the same key already exists, the element will be updated.

set
(
const handle: number
handle
,
const socket: net.Socket
socket
)
return
const handle: number
handle
}
const
const write: (handle: number, bytes: Uint8Array) => boolean
write
= (
handle: number
handle
: number,
bytes: Uint8Array<ArrayBufferLike>
bytes
:
interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>

A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Uint8Array
) =>
const sockets: Map<number, net.Socket>
sockets
.
Map<number, Socket$2>.get(key: number): net.Socket | undefined

Returns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map.

@returnsReturns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.

get
(
handle: number
handle
)?.
Socket$2.write(buffer: Uint8Array | string, cb?: (err?: Error) => void): boolean (+1 overload)

The writable.write() method writes some data to the stream, and calls the supplied callback once the data has been fully handled. If an error occurs, the callback will be called with the error as its first argument. The callback is called asynchronously and before 'error' is emitted.

The return value is true if the internal buffer is less than the highWaterMark configured when the stream was created after admitting chunk. If false is returned, further attempts to write data to the stream should stop until the 'drain' event is emitted.

While a stream is not draining, calls to write() will buffer chunk, and return false. Once all currently buffered chunks are drained (accepted for delivery by the operating system), the 'drain' event will be emitted. Once write() returns false, do not write more chunks until the 'drain' event is emitted. While calling write() on a stream that is not draining is allowed, Node.js will buffer all written chunks until maximum memory usage occurs, at which point it will abort unconditionally. Even before it aborts, high memory usage will cause poor garbage collector performance and high RSS (which is not typically released back to the system, even after the memory is no longer required). Since TCP sockets may never drain if the remote peer does not read the data, writing a socket that is not draining may lead to a remotely exploitable vulnerability.

Writing data while the stream is not draining is particularly problematic for a Transform, because the Transform streams are paused by default until they are piped or a 'data' or 'readable' event handler is added.

If the data to be written can be generated or fetched on demand, it is recommended to encapsulate the logic into a Readable and use

pipe

. However, if calling write() is preferred, it is possible to respect backpressure and avoid memory issues using the 'drain' event:

function write(data, cb) {
if (!stream.write(data)) {
stream.once('drain', cb);
} else {
process.nextTick(cb);
}
}
// Wait for cb to be called before doing any other write.
write('hello', () => {
console.log('Write completed, do more writes now.');
});

A Writable stream in object mode will always ignore the encoding argument.

Writes data to the stream, with an explicit encoding for string data.

write
(
bytes: Uint8Array<ArrayBufferLike>
bytes
) ?? false
const
const close: (handle: number) => void
close
= (
handle: number
handle
: number) => {
const sockets: Map<number, net.Socket>
sockets
.
Map<number, Socket$2>.get(key: number): net.Socket | undefined

Returns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map.

@returnsReturns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.

get
(
handle: number
handle
)?.
Stream.Readable.destroy(error?: Error): net.Socket

Destroy the stream. Optionally emit an 'error' event, and emit a 'close' event (unless emitClose is set to false). After this call, the readable stream will release any internal resources and subsequent calls to push() will be ignored.

Once destroy() has been called any further calls will be a no-op and no further errors except from _destroy() may be emitted as 'error'.

Implementors should not override this method, but instead implement readable._destroy().

@sincev8.0.0

@paramerror Error which will be passed as payload in 'error' event

destroy
() // tears the socket down
const sockets: Map<number, net.Socket>
sockets
.
Map<number, Socket$2>.delete(key: number): boolean

@returnstrue if an element in the Map existed and has been removed, or false if the element does not exist.

delete
(
handle: number
handle
)
}
const
const handle: number
handle
=
const connect: (host: string, port: number) => number
connect
('67.215.246.10', 6881) // 1
const sockets: Map<number, net.Socket>
sockets
.
Map<number, Socket$2>.get(key: number): net.Socket | undefined

Returns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map.

@returnsReturns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.

get
(
const handle: number
handle
) instanceof
import net
net
.
class Socket
export Socket
Socket
// true, a live socket before the relay has answered
const write: (handle: number, bytes: Uint8Array) => boolean
write
(
const handle: number
handle
, new
var Uint8Array: Uint8ArrayConstructor
new (elements: Iterable<number>) => Uint8Array<ArrayBuffer> (+6 overloads)
Uint8Array
([19, 66, 105, 116])) // true, and the bytes go out once the relay has answered

connect hands back the Socket at once and dials through the relay in the background, so the number maps to a live socket before the relay has answered, and a write in that window is queued rather than refused. The error listener goes on the line after connect. A refused dial emits error and nothing else, no close and no listener call, and an error with no listener throws. What arrives there is a message: getaddrinfo ENOTFOUND <hostname> when the name has no answer, tcp connect to <address>:<port> timed out after 12000ms when the relay does not acknowledge within 12,000 ms, and the operating system’s own text when the peer refused.

None of those carries an errno, so an engine that maps error.errno to its own table is mapping its own fallback, as testing explains.

An engine written for Node may hand connect a path or a file descriptor, and the library throws FKN WebVPN does not support IPC connections for the path and FKN WebVPN does not support file descriptors for the descriptor at the call, so pass { host, port } and nothing else. The whole surface is on TCP and UDP sockets.

The compiled code reads from its own linear memory, so every byte a socket delivers is copied into that memory at some point, and the question is only when. A data chunk arrives as a Buffer, a view over memory the library allocated, and an engine keeps it in a queue until its next tick drains it. Copy it in the listener, before the listener returns, so the queue holds memory the engine owns:

app.ts
import * as
import net
net
from '@fkn/lib/net'
const
const inbox: Uint8Array<ArrayBufferLike>[]
inbox
:
interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>

A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Uint8Array
[] = [] // what the compiled code's read drains, in arrival order
const
const socket: net.Socket
socket
=
import net
net
.
function connect(options: SocketConnectOpts, connectionListener?: () => void): net.Socket
export connect
connect
({
TcpSocketConnectOpts.host?: string | undefined
host
: '67.215.246.10',
TcpSocketConnectOpts.port: number
port
: 6881 })
const socket: net.Socket
socket
.
Stream.Duplex.on<"error">(eventName: "error", listener: (err: Error) => void): net.Socket (+1 overload)

Adds the listener function to the end of the listeners array for the event named eventName. No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times.

server.on('connection', (stream) => {
console.log('someone connected!');
});

Returns a reference to the EventEmitter, so that calls can be chained.

By default, event listeners are invoked in the order they are added. The emitter.prependListener() method can be used as an alternative to add the event listener to the beginning of the listeners array.

import { EventEmitter } from 'node:events';
const myEE = new EventEmitter();
myEE.on('foo', () => console.log('a'));
myEE.prependListener('foo', () => console.log('b'));
myEE.emit('foo');
// Prints:
// b
// a

@sincev0.1.101

@parameventName The name of the event.

@paramlistener The callback function

on
('error', (
error: Error
error
:
interface Error
Error
) =>
error: Error
error
.
Error.message: string
message
)
const socket: net.Socket
socket
.
Stream.Duplex.on<"data">(eventName: "data", listener: (chunk: any) => void): net.Socket (+1 overload)

Adds the listener function to the end of the listeners array for the event named eventName. No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times.

server.on('connection', (stream) => {
console.log('someone connected!');
});

Returns a reference to the EventEmitter, so that calls can be chained.

By default, event listeners are invoked in the order they are added. The emitter.prependListener() method can be used as an alternative to add the event listener to the beginning of the listeners array.

import { EventEmitter } from 'node:events';
const myEE = new EventEmitter();
myEE.on('foo', () => console.log('a'));
myEE.prependListener('foo', () => console.log('b'));
myEE.emit('foo');
// Prints:
// b
// a

@sincev0.1.101

@parameventName The name of the event.

@paramlistener The callback function

on
('data', (
chunk: Buffer<ArrayBufferLike>
chunk
:
interface Buffer<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>
Buffer
) => {
const
const copy: Uint8Array<ArrayBuffer>
copy
= new
var Uint8Array: Uint8ArrayConstructor
new (elements: Iterable<number>) => Uint8Array<ArrayBuffer> (+6 overloads)
Uint8Array
(
chunk: Buffer<ArrayBufferLike>
chunk
) // the bytes, in memory of their own
const copy: Uint8Array<ArrayBuffer>
copy
.
Uint8Array<ArrayBuffer>.byteLength: number

The length in bytes of the array.

byteLength
===
chunk: Buffer<ArrayBufferLike>
chunk
.
Uint8Array<ArrayBufferLike>.byteLength: number

The length in bytes of the array.

byteLength
// true
const copy: Uint8Array<ArrayBuffer>
copy
.
Uint8Array<ArrayBuffer>.buffer: ArrayBuffer

The ArrayBuffer instance referenced by the array.

buffer
===
chunk: Buffer<ArrayBufferLike>
chunk
.
Uint8Array<ArrayBufferLike>.buffer: ArrayBufferLike

The ArrayBuffer instance referenced by the array.

buffer
// false, so the next data event cannot reach it
const inbox: Uint8Array<ArrayBufferLike>[]
inbox
.
Array<Uint8Array<ArrayBufferLike>>.push(...items: Uint8Array<ArrayBufferLike>[]): number

Appends new elements to the end of an array, and returns the new length of the array.

@paramitems New elements to add to the array.

push
(
const copy: Uint8Array<ArrayBuffer>
copy
)
})

new Uint8Array(chunk) copies. chunk.slice() does not: on a Buffer, slice is an alias of subarray and returns a view over the same memory, and so does Buffer.from(chunk.buffer). Neither the library nor the shape above promises what happens to the memory behind a chunk once your listener returns, and the failure when it changes reads as a peer’s fault rather than the socket’s: a piece that hashes wrong, then a peer dropped for data that arrived intact. One integration traced exactly that failure to chunks it had kept rather than copied.

The same rule holds for a datagram. message hands you a Buffer and an rinfo with the sender’s address and port, and the engine’s receive queue takes a copy of the bytes and the two fields it answers with:

app.ts
import * as
import dgram
dgram
from '@fkn/lib/dgram'
const
const inbox: {
bytes: Uint8Array;
address: string;
port: number;
}[]
inbox
: {
bytes: Uint8Array<ArrayBufferLike>
bytes
:
interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>

A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Uint8Array
,
address: string
address
: string,
port: number
port
: number }[] = [] // what the compiled code's recvfrom drains
const
const socket: dgram.Socket
socket
=
import dgram
dgram
.
function createSocket(options: SocketType | SocketOptions, callback?: (msg: Buffer, rinfo: RemoteInfo) => void): dgram.Socket
export createSocket
createSocket
('udp4')
const socket: dgram.Socket
socket
.
NodeJS.EventEmitter<any>.on<string | symbol>(eventName: string | symbol, listener: (...args: any[]) => void): dgram.Socket

Adds the listener function to the end of the listeners array for the event named eventName. No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times.

server.on('connection', (stream) => {
console.log('someone connected!');
});

Returns a reference to the EventEmitter, so that calls can be chained.

By default, event listeners are invoked in the order they are added. The emitter.prependListener() method can be used as an alternative to add the event listener to the beginning of the listeners array.

import { EventEmitter } from 'node:events';
const myEE = new EventEmitter();
myEE.on('foo', () => console.log('a'));
myEE.prependListener('foo', () => console.log('b'));
myEE.emit('foo');
// Prints:
// b
// a

@sincev0.1.101

@parameventName The name of the event.

@paramlistener The callback function

on
('error', (
error: Error
error
:
interface Error
Error
) =>
error: Error
error
.
Error.message: string
message
) // a refused bind lands here, and listening never fires
const socket: dgram.Socket
socket
.
NodeJS.EventEmitter<any>.on<string | symbol>(eventName: string | symbol, listener: (...args: any[]) => void): dgram.Socket

Adds the listener function to the end of the listeners array for the event named eventName. No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times.

server.on('connection', (stream) => {
console.log('someone connected!');
});

Returns a reference to the EventEmitter, so that calls can be chained.

By default, event listeners are invoked in the order they are added. The emitter.prependListener() method can be used as an alternative to add the event listener to the beginning of the listeners array.

import { EventEmitter } from 'node:events';
const myEE = new EventEmitter();
myEE.on('foo', () => console.log('a'));
myEE.prependListener('foo', () => console.log('b'));
myEE.emit('foo');
// Prints:
// b
// a

@sincev0.1.101

@parameventName The name of the event.

@paramlistener The callback function

on
('message', (
message: Buffer<ArrayBufferLike>
message
:
interface Buffer<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>
Buffer
,
rinfo: any
rinfo
) => {
const inbox: {
bytes: Uint8Array;
address: string;
port: number;
}[]
inbox
.
Array<{ bytes: Uint8Array; address: string; port: number; }>.push(...items: {
bytes: Uint8Array;
address: string;
port: number;
}[]): number

Appends new elements to the end of an array, and returns the new length of the array.

@paramitems New elements to add to the array.

push
({
bytes: Uint8Array<ArrayBufferLike>
bytes
: new
var Uint8Array: Uint8ArrayConstructor
new (elements: Iterable<number>) => Uint8Array<ArrayBuffer> (+6 overloads)
Uint8Array
(
message: Buffer<ArrayBufferLike>
message
),
address: string
address
:
rinfo: any
rinfo
.
any
address
,
port: number
port
:
rinfo: any
rinfo
.
any
port
}) // a copy of the bytes, and the two fields recvfrom answers with
})
const socket: dgram.Socket
socket
.
Socket.bind(port?: number | undefined, address?: string | undefined, callback?: (() => void) | undefined): dgram.Socket (+3 overloads)

For UDP sockets, causes the dgram.Socket to listen for datagram messages on a named port and optional address. If port is not specified or is 0, the operating system will attempt to bind to a random port. If address is not specified, the operating system will attempt to listen on all addresses. Once binding is complete, a 'listening' event is emitted and the optional callback function is called.

Specifying both a 'listening' event listener and passing a callback to the socket.bind() method is not harmful but not very useful.

A bound datagram socket keeps the Node.js process running to receive datagram messages.

If binding fails, an 'error' event is generated. In rare case (e.g. attempting to bind with a closed socket), an Error may be thrown.

Example of a UDP server listening on port 41234:

import dgram from 'node:dgram';
const server = dgram.createSocket('udp4');
server.on('error', (err) => {
console.error(`server error:\n${err.stack}`);
server.close();
});
server.on('message', (msg, rinfo) => {
console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`);
});
server.on('listening', () => {
const address = server.address();
console.log(`server listening ${address.address}:${address.port}`);
});
server.bind(41234);
// Prints: server listening 0.0.0.0:41234

bind
(0, '0.0.0.0', () => {
const socket: dgram.Socket
socket
.
Socket.address(): AddressInfo

Returns an object containing the address information for a socket. For UDP sockets, this object will contain address, family, and port properties.

This method throws EBADF if called on an unbound socket.

@sincev0.1.99

address
().
AddressInfo.port: number
port
// the port the relay granted
const socket: dgram.Socket
socket
.
Socket.send(msg: string | readonly any[] | Uint8Array, port?: number | undefined, address?: string | undefined, callback?: ((error: Error | null, bytes: number) => void) | undefined): void (+6 overloads)

Broadcasts a datagram on the socket. For connectionless sockets, the destination port and address must be specified. Connected sockets, on the other hand, will use their associated remote endpoint, so the port and address arguments must not be set.

The msg argument contains the message to be sent. Depending on its type, different behavior can apply. If msg is a Buffer, any TypedArray or a DataView, the offset and length specify the offset within the Buffer where the message begins and the number of bytes in the message, respectively. If msg is a String, then it is automatically converted to a Buffer with 'utf8' encoding. With messages that contain multi-byte characters, offset and length will be calculated with respect to byte length and not the character position. If msg is an array, offset and length must not be specified.

The address argument is a string. If the value of address is a host name, DNS will be used to resolve the address of the host. If address is not provided or otherwise nullish, '127.0.0.1' (for udp4 sockets) or '::1' (for udp6 sockets) will be used by default.

If the socket has not been previously bound with a call to bind, the socket is assigned a random port number and is bound to the "all interfaces" address ('0.0.0.0' for udp4 sockets, '::0' for udp6 sockets.)

An optional callback function may be specified to as a way of reporting DNS errors or for determining when it is safe to reuse the buf object. DNS lookups delay the time to send for at least one tick of the Node.js event loop.

The only way to know for sure that the datagram has been sent is by using a callback. If an error occurs and a callback is given, the error will be passed as the first argument to the callback. If a callback is not given, the error is emitted as an 'error' event on the socket object.

Offset and length are optional but both must be set if either are used. They are supported only when the first argument is a Buffer, a TypedArray, or a DataView.

This method throws ERR_SOCKET_BAD_PORT if called on an unbound socket.

Example of sending a UDP packet to a port on localhost;

import dgram from 'node:dgram';
import { Buffer } from 'node:buffer';
const message = Buffer.from('Some bytes');
const client = dgram.createSocket('udp4');
client.send(message, 41234, 'localhost', (err) => {
client.close();
});

Example of sending a UDP packet composed of multiple buffers to a port on127.0.0.1;

import dgram from 'node:dgram';
import { Buffer } from 'node:buffer';
const buf1 = Buffer.from('Some ');
const buf2 = Buffer.from('bytes');
const client = dgram.createSocket('udp4');
client.send([buf1, buf2], 41234, (err) => {
client.close();
});

Sending multiple buffers might be faster or slower depending on the application and operating system. Run benchmarks to determine the optimal strategy on a case-by-case basis. Generally speaking, however, sending multiple buffers is faster.

Example of sending a UDP packet using a socket connected to a port on localhost:

import dgram from 'node:dgram';
import { Buffer } from 'node:buffer';
const message = Buffer.from('Some bytes');
const client = dgram.createSocket('udp4');
client.connect(41234, 'localhost', (err) => {
client.send(message, (err) => {
client.close();
});
});

send
(new
var Uint8Array: Uint8ArrayConstructor
new (elements: Iterable<number>) => Uint8Array<ArrayBuffer> (+6 overloads)
Uint8Array
([0, 0, 4, 23]), 6881, '67.215.246.10', (
error: Error | null
error
,
bytes: number
bytes
) =>
bytes: number
bytes
) // 4, handed to the transport
})

rinfo.size equals message.length, and rinfo.family is 'IPv4' or 'IPv6'. The send callback reports bytes handed to the transport, not delivered. The rest of the datagram surface is on UDP.

The compiled code asks for a socket’s addresses whenever it likes, often before the relay has answered. Node answers undefined for an address it does not have yet. The library throws Socket is not connected from each of remoteAddress, remotePort, localAddress and localPort until the relay has answered, and answers from the connect event on. A socket the server accepted can name its peer inside the connection handler, because the library publishes its addresses before it emits. Read each getter inside a guard of its own, and answer the engine’s not-connected code on a throw:

app.ts
import * as
import net
net
from '@fkn/lib/net'
// what the compiled code's getpeername reads, one property at a time, with a throw answered as not connected yet
const
const peerOf: (socket: net.Socket) => {
address: string;
port: number;
} | null
peerOf
= (
socket: net.Socket
socket
:
import net
net
.
class Socket
export Socket
Socket
) => {
const
const read: <T>(get: () => T) => T | undefined
read
= <
function (type parameter) T in <T>(get: () => T): T | undefined
T
>(
get: () => T
get
: () =>
function (type parameter) T in <T>(get: () => T): T | undefined
T
) => { try { return
get: () => T
get
() } catch { return
var undefined
undefined
} }
const
const address: string | undefined
address
=
const read: <string>(get: () => string) => string | undefined
read
(() =>
socket: net.Socket
socket
.
Socket$2.remoteAddress: string

The string representation of the remote IP address. For example,'74.125.127.100' or '2001:4860:a005::68'. Value may be undefined if the socket is destroyed (for example, if the client disconnected).

@sincev0.5.10

remoteAddress
)
const
const port: number | undefined
port
=
const read: <number>(get: () => number) => number | undefined
read
(() =>
socket: net.Socket
socket
.
Socket$2.remotePort: number

The numeric representation of the remote port. For example, 80 or 21. Value may be undefined if the socket is destroyed (for example, if the client disconnected).

@sincev0.5.10

remotePort
)
return
const address: string | undefined
address
===
var undefined
undefined
||
const port: number | undefined
port
===
var undefined
undefined
? null : {
address: string
address
,
port: number
port
}
}
const
const socket: net.Socket
socket
=
import net
net
.
function connect(options: SocketConnectOpts, connectionListener?: () => void): net.Socket
export connect
connect
({
TcpSocketConnectOpts.host?: string | undefined
host
: '67.215.246.10',
TcpSocketConnectOpts.port: number
port
: 6881 })
const socket: net.Socket
socket
.
Stream.Duplex.on<"error">(eventName: "error", listener: (err: Error) => void): net.Socket (+1 overload)

Adds the listener function to the end of the listeners array for the event named eventName. No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times.

server.on('connection', (stream) => {
console.log('someone connected!');
});

Returns a reference to the EventEmitter, so that calls can be chained.

By default, event listeners are invoked in the order they are added. The emitter.prependListener() method can be used as an alternative to add the event listener to the beginning of the listeners array.

import { EventEmitter } from 'node:events';
const myEE = new EventEmitter();
myEE.on('foo', () => console.log('a'));
myEE.prependListener('foo', () => console.log('b'));
myEE.emit('foo');
// Prints:
// b
// a

@sincev0.1.101

@parameventName The name of the event.

@paramlistener The callback function

on
('error', (
error: Error
error
:
interface Error
Error
) =>
error: Error
error
.
Error.message: string
message
)
const peerOf: (socket: net.Socket) => {
address: string;
port: number;
} | null
peerOf
(
const socket: net.Socket
socket
) // null, each getter threw Socket is not connected
const socket: net.Socket
socket
.
Stream.Duplex.on(eventName: string | symbol, listener: (...args: any[]) => void): net.Socket (+1 overload)

Adds the listener function to the end of the listeners array for the event named eventName. No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times.

server.on('connection', (stream) => {
console.log('someone connected!');
});

Returns a reference to the EventEmitter, so that calls can be chained.

By default, event listeners are invoked in the order they are added. The emitter.prependListener() method can be used as an alternative to add the event listener to the beginning of the listeners array.

import { EventEmitter } from 'node:events';
const myEE = new EventEmitter();
myEE.on('foo', () => console.log('a'));
myEE.prependListener('foo', () => console.log('b'));
myEE.emit('foo');
// Prints:
// b
// a

@sincev0.1.101

@parameventName The name of the event.

@paramlistener The callback function

on
('connect', () => {
const peerOf: (socket: net.Socket) => {
address: string;
port: number;
} | null
peerOf
(
const socket: net.Socket
socket
) // { address: '67.215.246.10', port: 6881 }
})
const
const server: net.Server
server
=
import net
net
.
function createServer(options?: ServerOpts | ((socket: net.Socket) => void), connectionListener?: (socket: net.Socket) => void): net.Server
export createServer
createServer
((
peer: net.Socket
peer
) => {
const peerOf: (socket: net.Socket) => {
address: string;
port: number;
} | null
peerOf
(
peer: net.Socket
peer
) // the peer's address and port, readable inside the handler
})
const server: net.Server
server
.
NodeJS.EventEmitter<any>.on<string | symbol>(eventName: string | symbol, listener: (...args: any[]) => void): net.Server

Adds the listener function to the end of the listeners array for the event named eventName. No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times.

server.on('connection', (stream) => {
console.log('someone connected!');
});

Returns a reference to the EventEmitter, so that calls can be chained.

By default, event listeners are invoked in the order they are added. The emitter.prependListener() method can be used as an alternative to add the event listener to the beginning of the listeners array.

import { EventEmitter } from 'node:events';
const myEE = new EventEmitter();
myEE.on('foo', () => console.log('a'));
myEE.prependListener('foo', () => console.log('b'));
myEE.emit('foo');
// Prints:
// b
// a

@sincev0.1.101

@parameventName The name of the event.

@paramlistener The callback function

on
('error', (
error: Error
error
:
interface Error
Error
) =>
error: Error
error
.
Error.message: string
message
) // a refused bind lands here, and listening never fires
const server: net.Server
server
.
Server$1.listen(port?: number | undefined, hostname?: string | undefined, backlog?: number | undefined, listeningListener?: (() => void) | undefined): net.Server (+9 overloads)

Starts a TCP listener through WebVPN. Port and host forms are supported. IPC paths and existing Node handles are not supported and reject at runtime.

listen
(0, '0.0.0.0')

peerOf answers null until the relay has answered and the addresses from then on, so the engine’s own lookup returns its not-connected code instead of an exception inside the compiled code. An accepted socket answers at once. Hand a socket to the engine only once it can name its peer, since an engine that cannot name a peer it accepted may drop it without a report. The listener itself is on hold an inbound port.

Send the engine two chunks on one TCP connection, or two datagrams, and read them back through the engine’s own read call. Each read matches what the peer sent, byte for byte, and the first still matches after the second has arrived. A dial to a public address runs the engine’s connect callback with the peer’s address on the socket, and an accepted peer reaches the accept path with one too. When that is not what you see:

  1. Data that is right on the first read and wrong on the second. A chunk was kept as a view rather than a copy, in the listener or in the engine’s own memory. Copy in the data and message listeners, and copy again into linear memory when the compiled code reads.
  2. The engine throws on a fresh connection. An address getter was read before the relay had answered and threw Socket is not connected: on a socket the engine dialed that is any read before connect fires, while an accepted socket can name its peer inside the connection handler.
  3. A write fails with Socket not connected, on the write callback and then on error. The engine wrote on a socket whose connect() was never called, so its number was handed out before the socket was dialed.
  4. Nothing arrives and nothing fails, then after 8,000 ms error carries a BrokerUnreachableError. The engine runs in a worker nobody relayed, see run sockets in a worker.

Every other message a socket can end on has its row on every error.

Most integrations this recipe is drawn from run the engine on a worker thread, relayed from the page with await relayWorker(worker, { unregisterSignal }) so its socket calls reach the broker, and the host file above runs there unchanged, see run sockets in a worker.

An engine that accepts peers reserves its port on UDP and TCP together before it starts, keeps it across a relay session that drops, and reports a refusal as a state rather than an exception, see hold an inbound port.