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
39 changes: 38 additions & 1 deletion packages/preview3-shim/lib/nodejs/sockets/address.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Adapted from preview2-shim/lib/io/worker-sockets.js
import { networkInterfaces } from "node:os";

// TODO(tandr): switch to generated types
export const IP_ADDRESS_FAMILY = {
Expand Down Expand Up @@ -47,7 +48,7 @@ export const ipv4ToTuple = (ipv4) => {

export const isMulticastIpAddress = ({ tag, val: { address } }) =>
(tag === "ipv4" && address[0] >= 0xe0 && address[0] <= 0xef) ||
(tag === "ipv6" && address[0] === 0xff);
(tag === "ipv6" && (address[0] & 0xff00) === 0xff00);

export const isIpv4MappedAddress = ({ tag, val: { address } }) =>
tag === "ipv6" &&
Expand All @@ -66,6 +67,18 @@ export const isBroadcastIpAddress = ({ tag, val: { address } }) =>
export const isUnicastIpAddress = (a) =>
!isMulticastIpAddress(a) && !isBroadcastIpAddress(a) && !isWildcardIpAddress(a);

export const isLoopbackIpAddress = ({ tag, val: { address } }) =>
(tag === "ipv4" && address[0] === 127) ||
(tag === "ipv6" && address.slice(0, 7).every((segment) => segment === 0) && address[7] === 1);

export const ipAddressConflict = (a, b) =>
a.tag === b.tag &&
Number(a.val.port) === Number(b.val.port) &&
Number(a.val.port) !== 0 &&
(isWildcardIpAddress(a) ||
isWildcardIpAddress(b) ||
a.val.address.every((segment, idx) => segment === b.val.address[idx]));

/**
* Serialize a socket‐address to text.
* @param {{tag:string,val:{address:number[]}}} ipAddr
Expand Down Expand Up @@ -107,3 +120,27 @@ export const makeIpAddress = (family, host, port) => {
? { tag: "ipv4", val: base }
: { tag: "ipv6", val: { ...base, flowInfo: 0, scopeId: 0 } };
};

/**
* Determine whether an IP address can be used as a local bind address.
* @param {{tag:'ipv4'|'ipv6',val:{address:number[]}}} ipAddress
* @returns {boolean}
*/
export const isBindableIpAddress = (ipAddress) => {
if (isWildcardIpAddress(ipAddress)) {
return true;
}

const expectedFamily = ipAddress.tag === "ipv4" ? "IPv4" : "IPv6";
const expectedAddress = ipAddress.val.address;
return Object.values(networkInterfaces())
.flat()
.some((iface) => {
if (iface?.family !== expectedFamily) {
return false;
}
const ifaceAddress =
ipAddress.tag === "ipv4" ? ipv4ToTuple(iface.address) : ipv6ToTuple(iface.address);
return expectedAddress.every((segment, idx) => segment === ifaceAddress[idx]);
});
};
3 changes: 3 additions & 0 deletions packages/preview3-shim/lib/nodejs/sockets/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ export class SocketError extends Error {
if (tag === "other") {
val = err.code;
}
} else if (err && typeof err.message === "string" && ERROR_MAP[err.message]) {
tag = ERROR_MAP[err.message];
message = err.message;
} else {
tag = "other";
message = err?.message;
Expand Down
22 changes: 15 additions & 7 deletions packages/preview3-shim/lib/nodejs/sockets/tcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
isUnicastIpAddress,
isMulticastIpAddress,
isIpv4MappedAddress,
isBindableIpAddress,
IP_ADDRESS_FAMILY,
} from "./address.js";

Expand Down Expand Up @@ -77,11 +78,8 @@ export class TcpSocket {
keepAliveInterval: 1_000_000_000n,
keepAliveCount: 10,
hopLimit: 1,

// For sendBufferSize and receiveBufferSize we can at least
// use the system defaults, but still we can't support setting them.
receiveBufferSize: undefined,
sendBufferSize: undefined,
receiveBufferSize: 65_536n,
sendBufferSize: 65_536n,
};

/**
Expand Down Expand Up @@ -151,6 +149,9 @@ export class TcpSocket {
if (!this.#isValidLocalAddress(localAddress)) {
throw new SocketError("invalid-argument");
}
if (!isBindableIpAddress(localAddress)) {
throw new SocketError("address-not-bindable");
}
try {
worker().runSync({
op: "tcp-bind",
Expand Down Expand Up @@ -224,10 +225,12 @@ export class TcpSocket {
}
try {
// Convert incoming connections from worker to TcpSocket instances
const listener = this;
const transform = new TransformStream({
transform({ family, socketId }, controller) {
const socket = TcpSocket._create(token(), family, socketId);
socket.#state = STATE.CONNECTED;
socket.#options = { ...listener.#options };
controller.enqueue(socket);
},
});
Expand Down Expand Up @@ -422,7 +425,11 @@ export class TcpSocket {
throw new SocketError("invalid-argument");
}

if (this.#state === STATE.CONNECTING || this.#state === STATE.CONNECTED) {
if (
this.#state === STATE.CONNECTING ||
this.#state === STATE.CONNECTED ||
this.#state === STATE.LISTENING
) {
throw new SocketError("not-supported");
}

Expand Down Expand Up @@ -518,6 +525,7 @@ export class TcpSocket {
}

if (value === this.#options.keepAliveIdleTime || !this.#options.keepAliveEnabled) {
this.#options.keepAliveIdleTime = value;
return;
}

Expand Down Expand Up @@ -744,7 +752,7 @@ export class TcpSocket {
#isValidLocalAddress(localAddress) {
return (
this.#family === localAddress.tag &&
isUnicastIpAddress(localAddress) &&
(isUnicastIpAddress(localAddress) || isWildcardIpAddress(localAddress)) &&
!isIpv4MappedAddress(localAddress)
);
}
Expand Down
48 changes: 37 additions & 11 deletions packages/preview3-shim/lib/nodejs/sockets/udp.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { ResourceWorker } from "../workers/resource-worker.js";
import { SocketError } from "./error.js";
import { isWildcardIpAddress, isUnicastIpAddress, IP_ADDRESS_FAMILY } from "./address.js";
import {
isWildcardIpAddress,
isUnicastIpAddress,
isIpv4MappedAddress,
isBindableIpAddress,
IP_ADDRESS_FAMILY,
} from "./address.js";

let WORKER = null;
function worker() {
Expand Down Expand Up @@ -55,8 +61,8 @@ export class UdpSocket {
#remote = null;
#options = {
hopLimit: 1,
receiveBufferSize: undefined,
sendBufferSize: undefined,
receiveBufferSize: 65_536n,
sendBufferSize: 65_536n,
};

/**
Expand Down Expand Up @@ -123,9 +129,16 @@ export class UdpSocket {
if (this.#state !== STATE.UNBOUND) {
throw new SocketError("invalid-state");
}
if (localAddress.tag !== this.#family) {
if (
localAddress.tag !== this.#family ||
(!isUnicastIpAddress(localAddress) && !isWildcardIpAddress(localAddress)) ||
isIpv4MappedAddress(localAddress)
) {
throw new SocketError("invalid-argument");
}
if (!isBindableIpAddress(localAddress)) {
throw new SocketError("address-not-bindable");
}

try {
worker().runSync({
Expand Down Expand Up @@ -153,14 +166,12 @@ export class UdpSocket {
* @throws {SocketError} for other errors, payload.tag maps the system error
*/
connect(remoteAddress) {
if (this.#state === STATE.CONNECTED) {
throw new SocketError("invalid-state");
}
if (
remoteAddress.tag !== this.#family ||
remoteAddress.val.port === 0 ||
isWildcardIpAddress(remoteAddress) ||
!isUnicastIpAddress(remoteAddress)
!isUnicastIpAddress(remoteAddress) ||
isIpv4MappedAddress(remoteAddress)
) {
throw new SocketError("invalid-argument");
}
Expand Down Expand Up @@ -221,7 +232,7 @@ export class UdpSocket {
* @throws {SocketError} for other errors, payload.tag maps the system error
*/
async send(data, remoteAddress = null) {
if (this.#state === STATE.UNBOUND || this.#state === STATE.CLOSED) {
if (this.#state === STATE.CLOSED) {
throw new SocketError("invalid-state");
}

Expand All @@ -230,7 +241,14 @@ export class UdpSocket {
}

const addr = remoteAddress ?? this.#remote;
if (!addr || addr.val.port === 0 || addr.tag !== this.#family) {
if (
!addr ||
addr.val.port === 0 ||
addr.tag !== this.#family ||
isWildcardIpAddress(addr) ||
isIpv4MappedAddress(addr) ||
!isUnicastIpAddress(addr)
) {
throw new SocketError("invalid-argument");
}

Expand All @@ -244,6 +262,7 @@ export class UdpSocket {
throw new SocketError("invalid-argument");
}

const wasUnbound = this.#state === STATE.UNBOUND;
try {
await worker().run(
{
Expand All @@ -254,6 +273,9 @@ export class UdpSocket {
},
[data.buffer],
);
if (wasUnbound) {
this.#state = STATE.BOUND;
}
} catch (e) {
throw SocketError.from(e);
}
Expand All @@ -280,7 +302,7 @@ export class UdpSocket {
op: "udp-receive",
socketId: this.#socketId,
});
return { data: new Uint8Array(data), addr: remoteAddress };
return [new Uint8Array(data), remoteAddress];
} catch (e) {
throw SocketError.from(e);
}
Expand Down Expand Up @@ -372,6 +394,10 @@ export class UdpSocket {
}

this.#options.hopLimit = value;
if (this.#state === STATE.UNBOUND) {
return;
}

try {
worker().runSync({
op: "udp-set-unicast-hop-limit",
Expand Down
40 changes: 35 additions & 5 deletions packages/preview3-shim/lib/nodejs/workers/tcp-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@ import { pipeline } from "stream/promises";
import { once } from "node:events";

import { Router } from "../workers/resource-worker.js";
import { serializeIpAddress, makeIpAddress } from "../sockets/address.js";
import { serializeIpAddress, makeIpAddress, ipAddressConflict } from "../sockets/address.js";
import { SocketError } from "../sockets/error.js";

import process from "node:process";
const { TCP, constants: TCPConstants } = process.binding("tcp_wrap");

// Socket instances stored by ID
const sockets = new Map();
// Unique IDs for sockets and servers
// Unique IDs for sockets
let NEXT_SOCKET_ID = 0n;
let NEXT_SERVER_ID = 0n;

// Handle worker messages
Router()
Expand Down Expand Up @@ -48,6 +47,7 @@ function handleTcpCreate({ family }) {
tcp: null,
server: null,
backlog: 128,
localAddress: null,
});

return { socketId };
Expand All @@ -61,6 +61,17 @@ async function handleTcpBind({ socketId, localAddress }) {

const { handle, family } = socket;

const hasConflict = [...sockets].some(
([id, { localAddress: boundAddress }]) =>
id !== socketId && boundAddress && ipAddressConflict(boundAddress, localAddress),
);

if (hasConflict) {
const err = new Error("EADDRINUSE");
err.code = "EADDRINUSE";
throw err;
}

await new Promise((resolve, reject) => {
let code;
if (family === "ipv6") {
Expand All @@ -75,6 +86,13 @@ async function handleTcpBind({ socketId, localAddress }) {
resolve();
}
});

const out = {};
const code = handle.getsockname(out);
if (code !== 0) {
throw SocketError.from(-code);
}
socket.localAddress = makeIpAddress(out.family.toLowerCase(), out.address, out.port);
}

// Connect a socket to remote address
Expand Down Expand Up @@ -119,9 +137,21 @@ async function handleTcpListen({ socketId, stream }) {

await Promise.race([onListening, onError]);

const addr = server.address();
if (addr && typeof addr === "object") {
socket.localAddress = makeIpAddress(family, addr.address, addr.port);
}

server.on("connection", (conn) => {
const id = NEXT_SERVER_ID++;
sockets.set(id, { handle: conn._handle, family, backlog, tcp: conn });
const id = NEXT_SOCKET_ID++;
sockets.set(id, {
handle: conn._handle,
family,
backlog,
tcp: conn,
server: null,
localAddress: makeIpAddress(family, conn.localAddress, conn.localPort),
});
writer.write({ family, socketId: id });
});

Expand Down
Loading
Loading