diff --git a/packages/preview2-shim/lib/io/worker-thread.js b/packages/preview2-shim/lib/io/worker-thread.js index d235d882a..758a82459 100644 --- a/packages/preview2-shim/lib/io/worker-thread.js +++ b/packages/preview2-shim/lib/io/worker-thread.js @@ -75,7 +75,9 @@ import { } from "./calls.js"; import { createUdpSocket } from "./worker-socket-udp.js"; -const symbolSocketUdpIpUnspecified = Symbol.symbolSocketUdpIpUnspecified ?? Symbol.for("symbolSocketUdpIpUnspecified"); +const symbolSocketUdpIpUnspecified = + Symbol.symbolSocketUdpIpUnspecified ?? + Symbol.for("symbolSocketUdpIpUnspecified"); let streamCnt = 0, pollCnt = 0; @@ -127,8 +129,7 @@ function streamError(streamId, stream, err) { * @returns {{ stream: NodeJS.ReadableStream | NodeJS.WritableStream, flushPromise: Promise | null }} */ export function getStreamOrThrow(streamId) { - if (!streamId) - throw new Error('Internal error: no stream id provided'); + if (!streamId) throw new Error("Internal error: no stream id provided"); const stream = unfinishedStreams.get(streamId); // not in unfinished streams <=> closed if (!stream) throw { tag: "closed" }; @@ -148,7 +149,9 @@ export function getSocketOrThrow(socketId) { } export function getSocketByPort(port) { - return Array.from(openedSockets.values()).find((socket) => socket.address().port === port); + return Array.from(openedSockets.values()).find( + (socket) => socket.address().port === port + ); } export function getBoundSockets(socketId) { @@ -158,7 +161,9 @@ export function getBoundSockets(socketId) { } export function dequeueReceivedSocketDatagram(socketInfo, maxResults) { - const dgrams = queuedReceivedSocketDatagrams.get(`PORT:${socketInfo.port}`).splice(0, Number(maxResults)); + const dgrams = queuedReceivedSocketDatagrams + .get(`PORT:${socketInfo.port}`) + .splice(0, Number(maxResults)); return dgrams; } export function enqueueReceivedSocketDatagram(socketInfo, { data, rinfo }) { @@ -292,7 +297,8 @@ function handle(call, id, payload) { // We need to cache the original bound IP type and fix rinfo.address when receiving datagrams (see below) // See https://github.com/WebAssembly/wasi-sockets/issues/86 socket[symbolSocketUdpIpUnspecified] = { - isUnspecified: localAddress === "0.0.0.0" || localAddress === "0:0:0:0:0:0:0:0", + isUnspecified: + localAddress === "0.0.0.0" || localAddress === "0:0:0:0:0:0:0:0", localAddress, }; @@ -314,7 +320,8 @@ function handle(call, id, payload) { if (remoteSocket[symbolSocketUdpIpUnspecified].isUnspecified) { // cache original bound address - rinfo._address = remoteSocket[symbolSocketUdpIpUnspecified].localAddress; + rinfo._address = + remoteSocket[symbolSocketUdpIpUnspecified].localAddress; } const receiverSocket = { diff --git a/packages/preview2-shim/lib/nodejs/sockets.js b/packages/preview2-shim/lib/nodejs/sockets.js index 2874e82f6..eeba80cec 100644 --- a/packages/preview2-shim/lib/nodejs/sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets.js @@ -1,11 +1,25 @@ -import { WasiSockets } from "./sockets/wasi-sockets.js"; - -export const { - ipNameLookup, - instanceNetwork, - network, - tcpCreateSocket, - udpCreateSocket, - tcp, - udp, -} = new WasiSockets(); \ No newline at end of file +import { WasiSockets, denyDnsLookup, denyTcp, denyUdp } from "./sockets/wasi-sockets.js"; + +export function _denyDnsLookup() { + denyDnsLookup(sockets); +} + +export function _denyTcp() { + denyTcp(sockets); +} + +export function _denyUdp() { + denyUdp(sockets); +} + +const sockets = new WasiSockets(); + +export const { + ipNameLookup, + instanceNetwork, + network, + tcpCreateSocket, + udpCreateSocket, + tcp, + udp, +} = sockets; diff --git a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js index 4b6129c2f..386c7db68 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js @@ -60,6 +60,7 @@ const globalBoundAddresses = new Map(); // TODO: implement would-block exceptions // TODO: implement concurrency-conflict exceptions export class TcpSocketImpl { + #allowed; id = 1; /** @type {TCP.TCPConstants.SOCKET} */ #socket = null; /** @type {Network} */ network = null; @@ -206,6 +207,8 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is already bound. (EINVAL) */ startBind(network, localAddress) { + if (!this.allowed()) + throw 'access-denied'; try { assert(this[symbolSocketState].isBound, "invalid-state", "The socket is already bound"); @@ -292,6 +295,8 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) */ startConnect(network, remoteAddress) { + if (!this.allowed()) + throw 'access-denied'; const host = serializeIpAddress(remoteAddress); const ipFamily = `ipv${isIP(host)}`; try { @@ -396,6 +401,8 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is already in the Listener state. */ startListen() { + if (!this.allowed()) + throw 'access-denied'; try { assert(this[symbolSocketState].lastErrorState !== null, "invalid-state"); assert(this[symbolSocketState].isBound === false, "invalid-state"); @@ -447,6 +454,8 @@ export class TcpSocketImpl { * @throws {new-socket-limit} The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) */ accept() { + if (!this.allowed()) + throw 'access-denied'; this[symbolOperations].accept++; try { diff --git a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js index 6fb6db53d..157531746 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js @@ -280,6 +280,8 @@ export class UdpSocket { * @throws {invalid-state} The socket is already bound. (EINVAL) */ startBind(network, localAddress) { + if (!this.allowed()) + throw 'access-denied'; try { assert(this[symbolSocketState].isBound, "invalid-state", "The socket is already bound"); diff --git a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js index 49c601d9f..43ec2c39b 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js @@ -120,6 +120,9 @@ export const IpAddressFamily = { }; export class WasiSockets { + #allowDnsLookup = true; + #allowTcp = true; + #allowUdp = true; networkCnt = 1; socketCnt = 1; @@ -155,6 +158,9 @@ export class WasiSockets { super(addressFamily, TcpSocket, net.socketCnt++); net.tcpSockets.set(this.id, this); } + allowed () { + return net.#allowTcp; + } } this.tcp = { @@ -196,9 +202,12 @@ export class WasiSockets { try { const id = net.socketCnt++; - const updSocket = udpSocketImplCreate(addressFamily, id); - net.udpSockets.set(id, updSocket); - return updSocket; + const udpSocket = udpSocketImplCreate(addressFamily, id); + udpSocket.allowed = () => { + return net.#allowUdp; + }; + net.udpSockets.set(id, udpSocket); + return udpSocket; } catch (err) { console.log("udp socket create error", { err, @@ -301,13 +310,34 @@ export class WasiSockets { * @throws {invalid-argument} `name` is a syntactically invalid domain name or IP address. */ resolveAddresses(network, name) { + if (!net.#allowDnsLookup) + throw 'permanent-resolver-failure'; // TODO: bind to network return resolveAddressStreamCreate(name); }, }; } + + static _denyDnsLookup (sockets) { + sockets.#allowDnsLookup = false; + } + static _denyTcp (sockets) { + sockets.#allowTcp = false; + } + static _denyUdp (sockets) { + sockets.#allowUdp = false; + } } +export const denyDnsLookup = WasiSockets._denyDnsLookup; +delete WasiSockets._denyDnsLookup; + +export const denyTcp = WasiSockets._denyTcp; +delete WasiSockets._denyTcp; + +export const denyUdp = WasiSockets._denyUdp; +delete WasiSockets._denyUdp; + function convertResolveAddressError(err) { switch (err.code) { default: diff --git a/tests/generated/cli_no_ip_name_lookup.rs b/tests/generated/cli_no_ip_name_lookup.rs index bd2ba0d37..db8f83bcb 100644 --- a/tests/generated/cli_no_ip_name_lookup.rs +++ b/tests/generated/cli_no_ip_name_lookup.rs @@ -10,7 +10,7 @@ fn cli_no_ip_name_lookup() -> anyhow::Result<()> { let wasi_file = "./tests/rundir/cli_no_ip_name_lookup.component.wasm"; let _ = fs::remove_dir_all("./tests/rundir/cli_no_ip_name_lookup"); - let cmd = cmd!(sh, "node ./src/jco.js run --jco-dir ./tests/rundir/cli_no_ip_name_lookup --jco-import ./tests/virtualenvs/base.js {wasi_file} hello this '' 'is an argument' 'with 🚩 emoji'"); + let cmd = cmd!(sh, "node ./src/jco.js run --jco-dir ./tests/rundir/cli_no_ip_name_lookup --jco-import ./tests/virtualenvs/deny-dns.js {wasi_file} hello this '' 'is an argument' 'with 🚩 emoji'"); cmd.run()?; Ok(()) diff --git a/tests/generated/cli_no_tcp.rs b/tests/generated/cli_no_tcp.rs index 670a8b639..c386d484c 100644 --- a/tests/generated/cli_no_tcp.rs +++ b/tests/generated/cli_no_tcp.rs @@ -10,7 +10,7 @@ fn cli_no_tcp() -> anyhow::Result<()> { let wasi_file = "./tests/rundir/cli_no_tcp.component.wasm"; let _ = fs::remove_dir_all("./tests/rundir/cli_no_tcp"); - let cmd = cmd!(sh, "node ./src/jco.js run --jco-dir ./tests/rundir/cli_no_tcp --jco-import ./tests/virtualenvs/base.js {wasi_file} hello this '' 'is an argument' 'with 🚩 emoji'"); + let cmd = cmd!(sh, "node ./src/jco.js run --jco-dir ./tests/rundir/cli_no_tcp --jco-import ./tests/virtualenvs/deny-tcp.js {wasi_file} hello this '' 'is an argument' 'with 🚩 emoji'"); cmd.run()?; Ok(()) diff --git a/tests/generated/cli_no_udp.rs b/tests/generated/cli_no_udp.rs index e916278f3..cb95a5986 100644 --- a/tests/generated/cli_no_udp.rs +++ b/tests/generated/cli_no_udp.rs @@ -10,7 +10,7 @@ fn cli_no_udp() -> anyhow::Result<()> { let wasi_file = "./tests/rundir/cli_no_udp.component.wasm"; let _ = fs::remove_dir_all("./tests/rundir/cli_no_udp"); - let cmd = cmd!(sh, "node ./src/jco.js run --jco-dir ./tests/rundir/cli_no_udp --jco-import ./tests/virtualenvs/base.js {wasi_file} hello this '' 'is an argument' 'with 🚩 emoji'"); + let cmd = cmd!(sh, "node ./src/jco.js run --jco-dir ./tests/rundir/cli_no_udp --jco-import ./tests/virtualenvs/deny-udp.js {wasi_file} hello this '' 'is an argument' 'with 🚩 emoji'"); cmd.run()?; Ok(()) diff --git a/tests/virtualenvs/deny-dns.js b/tests/virtualenvs/deny-dns.js new file mode 100644 index 000000000..9b558e346 --- /dev/null +++ b/tests/virtualenvs/deny-dns.js @@ -0,0 +1,3 @@ +import { _denyDnsLookup } from '@bytecodealliance/preview2-shim/sockets'; + +_denyDnsLookup(); diff --git a/tests/virtualenvs/deny-tcp.js b/tests/virtualenvs/deny-tcp.js new file mode 100644 index 000000000..642d08bbd --- /dev/null +++ b/tests/virtualenvs/deny-tcp.js @@ -0,0 +1,3 @@ +import { _denyTcp } from '@bytecodealliance/preview2-shim/sockets'; + +_denyTcp(); diff --git a/tests/virtualenvs/deny-udp.js b/tests/virtualenvs/deny-udp.js new file mode 100644 index 000000000..d891c7690 --- /dev/null +++ b/tests/virtualenvs/deny-udp.js @@ -0,0 +1,3 @@ +import { _denyUdp } from '@bytecodealliance/preview2-shim/sockets'; + +_denyUdp(); diff --git a/xtask/src/generate/tests.rs b/xtask/src/generate/tests.rs index 098682a0f..ac6b4eab2 100644 --- a/xtask/src/generate/tests.rs +++ b/xtask/src/generate/tests.rs @@ -86,6 +86,9 @@ fn generate_test(test_name: &str) -> String { "cli_file_append" => "bar-jabberwock", "proxy_handler" => "server-api-proxy", "proxy_echo" | "proxy_hash" => "server-api-proxy-streaming", + "cli_no_ip_name_lookup" => "deny-dns", + "cli_no_tcp" => "deny-tcp", + "cli_no_udp" => "deny-udp", _ => { if test_name.starts_with("preview1") { "scratch"