Skip to content

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

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions