Skip to content
Open
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
6 changes: 6 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@ See docs/process.md for more on how version tagging works.
process, or pthreads required. Supports incoming and outgoing TCP, UDP, IPv6,
and `-pthread` with `PROXY_TO_PTHREAD`. Uses the public node APIs where
available, falling back to `tcp_wrap`/`udp_wrap` on older Node.js. (#27080)
- Under `-sNODERAWSOCKETS`, `getaddrinfo` now performs real name resolution:
`/etc/hosts` entries (read through the emscripten FS) resolve synchronously,
and other hostnames resolve via `node:dns`, blocking the caller where its
stack can wait (a proxied pthread, `ASYNCIFY`/`JSPI`) and returning
`EAI_AGAIN` otherwise. Results may now be a linked list, which `freeaddrinfo`
frees in full.
- The following symbols are no longer included in `INCOMING_MODULE_JS_API`
by default:
- GL_MAX_TEXTURE_IMAGE_UNITS
Expand Down
81 changes: 78 additions & 3 deletions src/lib/libcore.js
Original file line number Diff line number Diff line change
Expand Up @@ -1007,9 +1007,17 @@ addToLibrary({
return inetPton4(DNS.lookup_name(nameString));
},

getaddrinfo__deps: ['$DNS', '$inetPton4', '$inetNtop4', '$inetPton6', '$inetNtop6', '$writeSockaddr', 'malloc', 'htonl'],
getaddrinfo__proxy: 'sync',
getaddrinfo: (node, service, hint, out) => {
// Returns an EAI_* code (0 on success, having written the addrinfo list to
// *out), or - under NODERAWSOCKETS, for a hostname needing a real DNS lookup -
// a thunk producing a Promise of that code, for getaddrinfo to wait on where
// the calling stack can suspend.
$doGetAddrInfo__internal: true,
$doGetAddrInfo__deps: ['$DNS', '$inetPton4', '$inetNtop4', '$inetPton6', '$inetNtop6', '$writeSockaddr', 'malloc', 'htonl',
#if NODERAWSOCKETS
'$nodeSockHelpers',
#endif
],
$doGetAddrInfo: (node, service, hint, out) => {
// Note getaddrinfo currently only returns a single addrinfo with ai_next defaulting to NULL. When NULL
// hints are specified or ai_family set to AF_UNSPEC or ai_socktype or ai_protocol set to 0 then we
// really should provide a linked list of suitable addrinfo values.
Expand Down Expand Up @@ -1055,6 +1063,23 @@ addToLibrary({
return ai;
}

#if NODERAWSOCKETS
// Chain one addrinfo per {family, addr} entry, returning the head.
function allocaddrinfos(entries) {
var head = 0, prev = 0;
for (var entry of entries) {
var ai = allocaddrinfo(entry.family, type, proto, null, entry.addr, port);
if (prev) {
{{{ makeSetValue('prev', C_STRUCTS.addrinfo.ai_next, 'ai', '*') }}};
} else {
head = ai;
}
prev = ai;
}
return head;
}
#endif

if (hint) {
flags = {{{ makeGetValue('hint', C_STRUCTS.addrinfo.ai_flags, 'i32') }}};
family = {{{ makeGetValue('hint', C_STRUCTS.addrinfo.ai_family, 'i32') }}};
Expand Down Expand Up @@ -1167,6 +1192,21 @@ addToLibrary({
//
// try as a hostname
//
#if NODERAWSOCKETS
// /etc/hosts first (read through emscripten's FS), then a real node:dns
// lookup, which is asynchronous: hand the caller a thunk to wait on.
var hosts = nodeSockHelpers.readHosts(node).filter((e) =>
family === {{{ cDefs.AF_UNSPEC }}} || e.family === family);
if (hosts.length) {
{{{ makeSetValue('out', '0', 'allocaddrinfos(hosts)', '*') }}};
return 0;
}
return () => nodeSockHelpers.lookupHost(node, family).then((entries) => {
if (typeof entries == 'number') return entries;
{{{ makeSetValue('out', '0', 'allocaddrinfos(entries)', '*') }}};
return 0;
});
#else
// resolve the hostname to a temporary fake address
node = DNS.lookup_name(node);
addr = inetPton4(node);
Expand All @@ -1178,6 +1218,41 @@ addToLibrary({
ai = allocaddrinfo(family, type, proto, null, addr, port);
{{{ makeSetValue('out', '0', 'ai', '*') }}};
return 0;
#endif
},

getaddrinfo__deps: ['$doGetAddrInfo',
#if NODERAWSOCKETS && ASYNCIFY
'$Asyncify',
#endif
],
getaddrinfo__proxy: 'sync',
#if NODERAWSOCKETS && (PTHREADS || ASYNCIFY)
// A hostname needing a real DNS lookup blocks by returning a Promise, which
// a proxied pthread awaits (PROXY_SYNC_ASYNC) and ASYNCIFY/JSPI suspends on.
// Every other outcome still returns synchronously.
getaddrinfo__async: true,
#endif
getaddrinfo: (node, service, hint, out) => {
var ret = doGetAddrInfo(node, service, hint, out);
#if NODERAWSOCKETS
if (typeof ret == 'function') {
#if PTHREADS
if (PThread.currentProxiedOperationCallerThread) return ret();
#endif
#if ASYNCIFY
return Asyncify.handleAsync(ret);
#else
// No stack that can wait on the lookup (the event-loop thread itself).
return {{{ cDefs.EAI_AGAIN }}};
#endif
}
#if PTHREADS
// A sync-proxied caller awaits a thenable even for an immediate result.
if (PThread.currentProxiedOperationCallerThread) return Promise.resolve(ret);
#endif
#endif
return ret;
},

getnameinfo__deps: ['$DNS', '$readSockaddr', '$stringToUTF8'],
Expand Down
58 changes: 57 additions & 1 deletion src/lib/libsockfs_node.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ null;

var NodeSockFSLibrary = {
// Node plumbing shared by the interface methods below.
$nodeSockHelpers__deps: ['$SOCKFS', '$ERRNO_CODES'],
$nodeSockHelpers__deps: ['$SOCKFS', '$ERRNO_CODES', '$FS', '$inetPton4', '$inetPton6'],
$nodeSockHelpers: {
// node builtins, resolved once each. getBuiltinModule works in both
// CommonJS and ESM output, with require as the fallback.
Expand All @@ -69,6 +69,62 @@ var NodeSockFSLibrary = {
getDgram() {
return nodeSockHelpers.dgramModule ??= (process.getBuiltinModule || require)('dgram');
},
getDns() {
return nodeSockHelpers.dnsModule ??= (process.getBuiltinModule || require)('dns');
},
// Address entries for `name` in /etc/hosts, read fresh through emscripten's
// FS on each call so a MEMFS or mounted file is honored as written. A
// missing file is simply empty.
readHosts(name) {
var out = [];
var text;
try {
text = FS.readFile('/etc/hosts', { encoding: 'utf8' });
} catch (e) {
return out;
}
for (var line of text.split('\n')) {
var hash = line.indexOf('#');
if (hash !== -1) line = line.slice(0, hash);
var parts = line.split(/\s+/).filter((p) => p.length);
if (parts.length < 2 || !parts.slice(1).includes(name)) continue;
var addr = inetPton4(parts[0]);
if (addr !== null) {
out.push({ family: {{{ cDefs.AF_INET }}}, addr });
} else if ((addr = inetPton6(parts[0])) !== null) {
out.push({ family: {{{ cDefs.AF_INET6 }}}, addr });
}
}
return out;
},
// Resolve a hostname via node:dns for `family` (AF_UNSPEC for both).
// Resolves to a list of {family, addr} entries, or an EAI_* code: node:dns
// surfaces either getaddrinfo EAI_* names or libuv codes, of which the
// transient ones map to EAI_AGAIN and the rest to "name not found".
lookupHost(name, family) {
var opts = { all: true };
if (family === {{{ cDefs.AF_INET }}}) opts.family = 4;
else if (family === {{{ cDefs.AF_INET6 }}}) opts.family = 6;
return new Promise((resolve) => {
nodeSockHelpers.getDns().lookup(name, opts, (err, addresses) => {
if (err) {
switch (err.code) {
case 'EAI_AGAIN':
case 'ETIMEDOUT':
case 'ESERVFAIL':
case 'EREFUSED':
return resolve({{{ cDefs.EAI_AGAIN }}});
default:
return resolve({{{ cDefs.EAI_NONAME }}});
}
}
if (!addresses.length) return resolve({{{ cDefs.EAI_NONAME }}});
resolve(addresses.map((a) => a.family === 6 ?
{ family: {{{ cDefs.AF_INET6 }}}, addr: inetPton6(a.address) } :
{ family: {{{ cDefs.AF_INET }}}, addr: inetPton4(a.address) }));
});
});
},
// True when node:dgram exposes both synchronous bindSync and connectSync
// (a recent addition), letting UDP run entirely on the public API. A runtime
// missing either falls back to the private udp_wrap handle, which provides
Expand Down
3 changes: 2 additions & 1 deletion src/struct_info.json
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,8 @@
"NI_NAMEREQD",
"EAI_NONAME",
"EAI_SOCKTYPE",
"EAI_BADFLAGS"
"EAI_BADFLAGS",
"EAI_AGAIN"
],
"structs": {
"addrinfo": [
Expand Down
1 change: 1 addition & 0 deletions src/struct_info_generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"EADV": 122,
"EAFNOSUPPORT": 5,
"EAGAIN": 6,
"EAI_AGAIN": -3,
"EAI_BADFLAGS": -1,
"EAI_FAMILY": -6,
"EAI_NONAME": -2,
Expand Down
1 change: 1 addition & 0 deletions src/struct_info_generated_wasm64.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"EADV": 122,
"EAFNOSUPPORT": 5,
"EAGAIN": 6,
"EAI_AGAIN": -3,
"EAI_BADFLAGS": -1,
"EAI_FAMILY": -6,
"EAI_NONAME": -2,
Expand Down
13 changes: 8 additions & 5 deletions system/lib/libc/musl/src/network/freeaddrinfo.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
void freeaddrinfo(struct addrinfo *p)
{
#if __EMSCRIPTEN__
// Emscripten's usage of this structure is very simple: we always allocate
// ai_addr, and do not use the linked list aspect at all. There is also no
// aliasing with aibuf.
free(p->ai_addr);
free(p);
// Emscripten allocates each node and its ai_addr separately (no aibuf
// block, no aliasing), so walk the list freeing both.
while (p) {
struct addrinfo *next = p->ai_next;
free(p->ai_addr);
free(p);
p = next;
}
#else
size_t cnt;
for (cnt=1; p->ai_next; cnt++, p=p->ai_next);
Expand Down
4 changes: 2 additions & 2 deletions test/codesize/test_codesize_hello_dylink_all.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"a.out.js": 270584,
"a.out.js": 270610,
"a.out.nodebug.wasm": 588233,
"total": 858817,
"total": 858843,
"sent": [
"IMG_Init",
"IMG_Load",
Expand Down
112 changes: 112 additions & 0 deletions test/sockets/test_dns.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2026 The Emscripten Authors. All rights reserved.
* Emscripten is available under two separate licenses, the MIT license and the
* University of Illinois/NCSA Open Source License. Both these licenses can be
* found in the LICENSE file.
*
* getaddrinfo() under -sNODERAWSOCKETS: numeric addresses and /etc/hosts
* entries (read through emscripten's FS) resolve synchronously, a name with
* several addresses comes back as a linked list, and any other hostname goes
* to node:dns. That lookup is asynchronous, so it blocks where the calling
* stack can wait (a proxied pthread, JSPI) and is EAI_AGAIN where it cannot
* (built with -DNO_WAIT).
*/

#include <arpa/inet.h>
#include <assert.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>

static struct addrinfo* lookup(const char* name, int family, int expect) {
struct addrinfo hints = {0};
hints.ai_family = family;
hints.ai_socktype = SOCK_STREAM;
struct addrinfo* res = NULL;
int err = getaddrinfo(name, "80", &hints, &res);
if (err != expect) {
printf("getaddrinfo(%s) = %d, expected %d\n", name, err, expect);
exit(1);
}
return res;
}

static int count_v4(struct addrinfo* res, const char* addr) {
int n = 0;
for (struct addrinfo* ai = res; ai; ai = ai->ai_next) {
assert(ai->ai_socktype == SOCK_STREAM);
assert(ai->ai_protocol == IPPROTO_TCP);
if (ai->ai_family != AF_INET) continue;
struct sockaddr_in* sin = (struct sockaddr_in*)ai->ai_addr;
assert(ai->ai_addrlen == sizeof(*sin));
assert(ntohs(sin->sin_port) == 80);
if (sin->sin_addr.s_addr == inet_addr(addr)) n++;
}
return n;
}

int main(void) {
mkdir("/etc", 0777);
FILE* f = fopen("/etc/hosts", "w");
assert(f);
fputs("# test hosts\n"
"10.1.2.3 statichost.test alias.test\n"
"192.0.2.1 multi.test\n"
"fe80::1 multi.test\n"
"192.0.2.2 multi.test # trailing comment\n",
f);
fclose(f);

struct addrinfo* res = lookup("10.9.8.7", AF_UNSPEC, 0);
assert(count_v4(res, "10.9.8.7") == 1 && !res->ai_next);
freeaddrinfo(res);

res = lookup("alias.test", AF_INET, 0);
assert(count_v4(res, "10.1.2.3") == 1 && !res->ai_next);
freeaddrinfo(res);

// Only the matching family, still as a list.
res = lookup("multi.test", AF_INET, 0);
assert(count_v4(res, "192.0.2.1") == 1);
assert(count_v4(res, "192.0.2.2") == 1);
assert(res->ai_next && !res->ai_next->ai_next);
freeaddrinfo(res);

// AF_UNSPEC includes the IPv6 entry.
res = lookup("multi.test", AF_UNSPEC, 0);
int n = 0, v6 = 0;
for (struct addrinfo* ai = res; ai; ai = ai->ai_next) {
n++;
if (ai->ai_family == AF_INET6) {
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)ai->ai_addr;
assert(ai->ai_addrlen == sizeof(*sin6));
assert(sin6->sin6_addr.s6_addr[0] == 0xfe && sin6->sin6_addr.s6_addr[15] == 1);
v6++;
}
}
assert(n == 3 && v6 == 1);
freeaddrinfo(res);

// Not in /etc/hosts: a real node:dns lookup.
#ifdef NO_WAIT
lookup("localhost", AF_INET, EAI_AGAIN);
#else
res = lookup("localhost", AF_INET, 0);
assert(count_v4(res, "127.0.0.1") == 1);
freeaddrinfo(res);

struct addrinfo hints = {0};
hints.ai_family = AF_INET;
res = NULL;
int err = getaddrinfo("nonexistent.invalid", NULL, &hints, &res);
assert(err == EAI_NONAME || err == EAI_AGAIN);
assert(!res);
#endif

printf("done\n");
return 0;
}
17 changes: 17 additions & 0 deletions test/test_sockets_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,23 @@ def test_noderawsockets_udp_ipv6(self):
self.skipTest('no IPv6 loopback available')
self.do_runf('sockets/test_udp_ipv6.c', 'done\n', cflags=['-sNODERAWSOCKETS'])

def test_noderawsockets_dns(self):
# getaddrinfo() resolves numeric addresses and /etc/hosts entries (read via
# emscripten's FS) synchronously, as a linked list. A real hostname needs a
# node:dns lookup, and with no stack able to wait on it is EAI_AGAIN.
self.do_runf('sockets/test_dns.c', 'done\n', cflags=['-sNODERAWSOCKETS', '-DNO_WAIT'])

def test_noderawsockets_dns_blocking(self):
# A real hostname blocks on the node:dns lookup: main() is proxied to a
# worker, which awaits the resolution through the sync proxy.
self.do_runf('sockets/test_dns.c', 'done\n',
cflags=['-sNODERAWSOCKETS', '-pthread', '-sPROXY_TO_PTHREAD', '-sEXIT_RUNTIME'])

@requires_jspi_node
def test_noderawsockets_dns_blocking_jspi(self):
# Same, but getaddrinfo() suspends the wasm stack under JSPI.
self.do_runf('sockets/test_dns.c', 'done\n', cflags=['-sNODERAWSOCKETS', '-sEXIT_RUNTIME'])

def test_noderawsockets_epoll_socket_blocking(self):
# A blocking epoll_wait() on a socket is woken by an incoming datagram
# through the unified readiness wait-queue (the SOCKFS.emit bridge), with
Expand Down
Loading