From 55005ef73daa7cea9bdaeb278e873fa3120814a9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sat, 17 Oct 2026 16:42:55 -0500 Subject: [PATCH] Resolve host names in net.connect net.connect and net.createConnection dialed only IP literals and "localhost"; any other host name failed with getaddrinfo ENOTFOUND without a lookup, so names served by /etc/hosts or a container's DNS (such as Docker service names) did not connect. The dial now resolves the name with getaddrinfo using Node's hints, orders the addresses as Node's autoSelectFamily does, and tries them in order. Fixes #1156 Co-authored-by: Quim T. <26749475+espetro@users.noreply.github.com> --- .../corpus/5970-net-connect-host-name.ts.json | 6 + packages/runtime/src/scr_net.c | 121 ++++++++++++++++-- tests/corpus/5970-net-connect-host-name.ts | 29 +++++ 3 files changed, 145 insertions(+), 11 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5970-net-connect-host-name.ts.json create mode 100644 tests/corpus/5970-net-connect-host-name.ts diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5970-net-connect-host-name.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5970-net-connect-host-name.ts.json new file mode 100644 index 000000000..8e6e036ba --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5970-net-connect-host-name.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5970-net-connect-host-name.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index 8b8cd81b6..64f5dee02 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -527,6 +527,8 @@ struct ScrNetSocket { bool dial_deferred; ScrStr **dial_ips; size_t dial_n, dial_i; + /* a host name the dial resolves first (+1; NULL for IP literals) */ + ScrStr *dial_host; /* the in-flight dial chain's LAST failure — surfaced (moved into * pending_err) only when the list exhausts; a later success drops it */ ScrStr *dial_err; @@ -662,6 +664,7 @@ void scr_net_sock_release(ScrNetSocket *s) { for (size_t i = 0; i < s->dial_n; i++) scr_str_release(s->dial_ips[i]); free(s->dial_ips); scr_str_release(s->dial_err); + scr_str_release(s->dial_host); free(s->wbuf); free(s->rbuf); if (s->pipe_dst) scr_net_sock_release(s->pipe_dst); @@ -2023,9 +2026,9 @@ static void scr_net_server_accept(ScrNetServer *srv) { /* ── the client ──────────────────────────────────────────────────────── */ -/* connect(port, host): numeric IPv4/IPv6 hosts and "localhost" (pinned to - * 127.0.0.1 — the documented divergence from Node's family autoselect). - * The connect callback registers as once('connect'). Never throws: every +/* connect(port, host): numeric IPv4/IPv6 hosts, "localhost" (pinned to + * 127.0.0.1 — the documented divergence from Node's family autoselect), + * and host names resolved through getaddrinfo. The connect callback registers as once('connect'). Never throws: every * failure is the async 'error' event, like Node. */ ScrStr *scr_net_blocking_lookup(ScrStr *host /*borrowed*/) { @@ -2056,12 +2059,112 @@ ScrStr *scr_net_blocking_lookup(ScrStr *host /*borrowed*/) { return scr_str_new(ip, strlen(ip)); } +/* Records a host connect target: IP literals dial as given, "localhost" + * is pinned to 127.0.0.1 (the documented divergence from Node's family + * autoselect), and any other name is resolved when the dial starts. */ +static void scr_net_sock_set_peer(ScrNetSocket *s, ScrStr *host /*borrowed, nullable*/) { + const char *h = host && host->len > 0 ? host->data : "localhost"; + if (strcmp(h, "localhost") == 0) h = "127.0.0.1"; + snprintf(s->peer_ip, sizeof s->peer_ip, "%s", h); + struct in_addr a4; + struct in6_addr a6; + if (inet_pton(AF_INET, h, &a4) != 1 && inet_pton(AF_INET6, h, &a6) != 1) + s->dial_host = scr_str_retain(host); +} + +/* Resolves dial_host the way Node's lookupAndConnect does: getaddrinfo + * for stream sockets of any family, with AI_ADDRCONFIG except on + * Windows. The answers are deduplicated and ordered like Node's + * autoSelectFamily list (families alternate, starting with the first + * answer's), then dialed in order through the address chain: a failed + * connect tries the next address, and the last failure is the 'error'. + * The attempt timer that starts the next address while one is still + * pending is not modeled. A failed lookup is Node's deferred + * "getaddrinfo " error. */ +static void scr_net_sock_dial_host(ScrNetSocket *s) { + ScrStr *host = s->dial_host; + s->dial_host = NULL; + struct addrinfo hints; + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; +#ifndef _WIN32 + hints.ai_flags = AI_ADDRCONFIG; +#endif + struct addrinfo *res = NULL; + int rc = getaddrinfo(host->data, NULL, &hints, &res); + size_t n = 0; + for (struct addrinfo *ai = rc == 0 ? res : NULL; ai != NULL; ai = ai->ai_next) n++; + ScrStr **ips = n > 0 ? calloc(n, sizeof *ips) : NULL; + if (n > 0 && !ips) scr_net_oom(); + /* by family: [0] is the first answer's family, [1] the other */ + size_t count[2] = { 0, 0 }; + ScrStr **fam[2] = { NULL, NULL }; + int first = AF_UNSPEC; + for (struct addrinfo *ai = rc == 0 ? res : NULL; ai != NULL; ai = ai->ai_next) { + char ip[INET6_ADDRSTRLEN]; + const char *out = NULL; + if (ai->ai_family == AF_INET) { + out = inet_ntop(AF_INET, &((struct sockaddr_in *)ai->ai_addr)->sin_addr, ip, sizeof ip); + } else if (ai->ai_family == AF_INET6) { + out = inet_ntop(AF_INET6, &((struct sockaddr_in6 *)ai->ai_addr)->sin6_addr, ip, sizeof ip); + } + if (out == NULL) continue; + if (first == AF_UNSPEC) first = ai->ai_family; + size_t f = ai->ai_family == first ? 0 : 1; + if (fam[f] == NULL) { + fam[f] = calloc(n, sizeof *fam[f]); + if (!fam[f]) scr_net_oom(); + } + size_t len = strlen(ip); + bool seen = false; + for (size_t i = 0; i < count[f] && !seen; i++) + seen = fam[f][i]->len == len && memcmp(fam[f][i]->data, ip, len) == 0; + if (!seen) fam[f][count[f]++] = scr_str_new(ip, len); + } + if (res) freeaddrinfo(res); + size_t total = 0; + for (size_t i = 0; i < count[0] || i < count[1]; i++) { + if (i < count[0]) ips[total++] = fam[0][i]; + if (i < count[1]) ips[total++] = fam[1][i]; + } + free(fam[0]); + free(fam[1]); + if (total == 0) { + bool notfound = rc == 0 || rc == EAI_NONAME +#ifdef EAI_NODATA + || rc == EAI_NODATA +#endif + ; + const char *code = notfound ? "ENOTFOUND" : rc == EAI_AGAIN ? "EAI_AGAIN" : "EAI_FAIL"; + char msg[320]; + snprintf(msg, sizeof msg, "getaddrinfo %s %.*s", code, + (int)(host->len < 255 ? host->len : 255), host->data); + free(ips); + scr_str_release(host); + s->pending_err = scr_str_new(msg, strlen(msg)); + s->had_error = true; + s->emit_close = true; + s->connecting = false; + return; + } + scr_str_release(host); + s->dial_ips = ips; + s->dial_n = total; + s->dial_i = 0; + scr_net_sock_dial_next(s); +} + /* The dial itself (peer_ip/peer_port already set): the sockaddr arms, * socket(), and the nonblocking connect(). Shared by the immediate dial * (scr_net_connect) and the DEFERRED one (the http agent's maxSockets * queue — the socket exists, buffers writes, and dials when a slot * frees). */ static void scr_net_sock_dial_peer(ScrNetSocket *s) { + if (s->dial_host != NULL) { + scr_net_sock_dial_host(s); + return; + } const char *h = s->peer_ip; struct sockaddr_in a4; struct sockaddr_in6 a6; @@ -2081,8 +2184,8 @@ static void scr_net_sock_dial_peer(ScrNetSocket *s) { salen = sizeof a6; } if (!sa) { - /* Non-numeric host: no resolver in this slice. Node's shape for a - * failed lookup is ENOTFOUND; deliver it deferred. */ + /* not an IP literal and not resolved: Node's shape for a failed + * lookup is ENOTFOUND; deliver it deferred. */ char msg[160]; snprintf(msg, sizeof msg, "getaddrinfo ENOTFOUND %s", h); s->pending_err = scr_str_new(msg, strlen(msg)); @@ -2142,9 +2245,7 @@ ScrNetSocket *scr_net_connect(double port, ScrStr *host /*borrowed, nullable*/, ScrNetSocket *s = scr_net_sock_new(); if (cb) scr_net_ls_add(&s->conn_ls, cb, NULL, true); s->peer_port = (int)port; - const char *h = host && host->len > 0 ? host->data : "localhost"; - if (strcmp(h, "localhost") == 0) h = "127.0.0.1"; - snprintf(s->peer_ip, sizeof s->peer_ip, "%s", h); + scr_net_sock_set_peer(s, host); if (!scr_net_poller_init()) { fputs("scriptc: event poller init failed\n", stderr); abort(); @@ -2283,9 +2384,7 @@ void scr_net_connect_opts_chk(const ScrDyn *opts, const ScrStr *fence) { ScrNetSocket *scr_net_connect_deferred(double port, ScrStr *host /*borrowed, nullable*/) { ScrNetSocket *s = scr_net_sock_new(); s->peer_port = (int)port; - const char *h = host && host->len > 0 ? host->data : "localhost"; - if (strcmp(h, "localhost") == 0) h = "127.0.0.1"; - snprintf(s->peer_ip, sizeof s->peer_ip, "%s", h); + scr_net_sock_set_peer(s, host); if (!scr_net_poller_init()) { fputs("scriptc: event poller init failed\n", stderr); abort(); diff --git a/tests/corpus/5970-net-connect-host-name.ts b/tests/corpus/5970-net-connect-host-name.ts new file mode 100644 index 000000000..4db67fbbb --- /dev/null +++ b/tests/corpus/5970-net-connect-host-name.ts @@ -0,0 +1,29 @@ +// net.connect resolves a host name through the system resolver before it +// dials. "LOCALHOST" is not the "localhost" literal, so it goes through +// getaddrinfo and the hosts file, as a container service name does. +import * as net from "node:net"; + +const server = net.createServer((socket) => { + socket.on("data", (chunk: Buffer) => socket.end("echo: " + chunk.toString())); +}); + +server.listen(0, () => { + const { port } = server.address() as net.AddressInfo; + const client = net.connect(port, "LOCALHOST", () => { + console.log("connected", net.isIP(client.remoteAddress ?? "") !== 0); + client.write("hello"); + }); + client.on("data", (chunk: Buffer) => console.log(chunk.toString())); + client.on("close", () => { + console.log("client closed"); + const options = net.createConnection({ port, host: "LOCALHOST" }); + options.on("connect", () => { + console.log("options form connected"); + options.end(); + }); + options.on("close", () => { + console.log("options form closed"); + server.close(() => console.log("server closed")); + }); + }); +});