Skip to content

Cannot split a server and client across separate exec() calls in one VM #88

@NathanFlurry

Description

@NathanFlurry

Summary

A single NodeRuntime VM cannot run a long-lived server in one exec() and reach it from a separate exec(). Concurrent exec() processes in the same VM do not share a reachable loopback, so a client started in a second exec() cannot connect to a server started in the first.

This came up while documenting the dev-servers use case: the natural API a user reaches for is "run the server script, then run the client script," and it does not work today.

Observed behavior

Two ways to split a server and client:

  1. Sequentially (await rt.exec(server) then await rt.exec(client)): impossible. exec() is run-to-completion, so the server exec() never returns (it blocks until the guest process exits), and you never reach the client call.

  2. Concurrently (fire the server exec() without awaiting, then await a separate client exec()): the client cannot reach the server. In a repro, the client polled http://127.0.0.1:3000/ 40x over ~4s and got no-response, even though the server process was still alive (it was killed by dispose() with exit 143). The two exec() processes ran concurrently but did not share a reachable loopback.

Repro

import { NodeRuntime } from "secure-exec";

const rt = await NodeRuntime.create({ permissions: { network: "allow" } });

// Server in exec #1 (do not await; never exits on its own).
rt.exec(`
  import http from "node:http";
  const app = http.createServer((req, res) => { res.writeHead(200); res.end("hi"); });
  await new Promise(r => app.listen(3000, "127.0.0.1", r));
  await new Promise(() => {});
`, { timeout: 20000 });

// Client in exec #2 (separate process, same VM).
const client = await rt.exec(`
  let out = "no-response";
  for (let i = 0; i < 40; i++) {
    try { const r = await fetch("http://127.0.0.1:3000/"); out = r.status + " " + await r.text(); break; }
    catch { await new Promise(r => setTimeout(r, 100)); }
  }
  console.log("client got:", out);
`, { timeout: 12000 });

console.log(client.stdout); // "client got: no-response"
await rt.dispose();

Expected

Two exec() calls in the same VM should be able to talk to each other over the VM's loopback (one VM = one shared network namespace), so a server in one exec() is reachable from a client in another. At minimum the limitation should be intentional and documented.

Notes / workarounds

  • Today the working pattern is to keep the server and its request loop inside a single exec() (same process = shared loopback), which is what the dev-servers doc example does.
  • The host-to-guest path exists at the wire-protocol level via vmFetch (addresses a running guest server by port and proxies a host request into it through the kernel socket table), but it is not yet surfaced on the public NodeRuntime API.

Worth deciding: should concurrent exec() processes in a VM share loopback (fix), or is per-exec() network isolation intentional (document + point users to vmFetch)?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions