Skip to content

Raw TCP relay for guest egress beyond port 80#358

Merged
felixrieseberg merged 1 commit into
mainfrom
claude/quizzical-benz
Apr 11, 2026
Merged

Raw TCP relay for guest egress beyond port 80#358
felixrieseberg merged 1 commit into
mainfrom
claude/quizzical-benz

Conversation

@felixrieseberg
Copy link
Copy Markdown
Owner

What

Let the Win95 guest open real TCP connections to ports other than 80. The fetch network adapter only ever accepted port-80 SYNs (parsing the payload as HTTP and replaying it via fetch()); anything else was reset. This adds a second tcp-connection bus listener in the renderer that bridges every other destination straight to a Node net.Socket — no extra process, no WebSocket/Wisp relay, no new dependencies.

How

  • src/renderer/net/tcp-relay.ts — accept the SYN synchronously (v86 RSTs otherwise), net.connect to conn.psrc:conn.sport, buffer guest bytes until the upstream connects, then pipe both ways. Loopback / link-local / multicast are refused so guest software can't poke host services or cloud metadata; the rest of RFC1918 is intentionally left reachable so LAN FTP/telnet/etc keep working. Ports 80 and 139 stay with the existing fetch and SMB handlers.
  • src/renderer/net/dns-shim.tsnet_device.dns_method is now "doh" so the guest resolves real IPs the relay can dial. Cloudflare can't answer single-label names like windows95 or the NNN.external localhost magic, so this shim wraps global fetch, spots /dns-query POSTs for those names, and answers with the same 192.168.87.1 placeholder the static resolver used to hand out — the port-80 fetch path then takes over via the Host header exactly as before.
  • emulator.tsx — wire both in alongside the SMB hook; set dns_method.
  • debug-harness.tsWIN95_PROBE_RUN / _RUN_AFTER / _RUN_WAIT to type an arbitrary command into Start → Run (used for the e2e test below).

Tested

npm run tsc clean. End-to-end: booted the guest under the probe harness, ran telnet 192.168.0.128 7777 against a host-side echo server — relay opened the socket, server received the typed hi-from-win95 keystrokes, and the guest's Telnet window rendered the server banner (bidirectional). http://windows95 still serves the bundled static site via the DNS shim.

Notes

TLS from the guest will tunnel but modern servers reject Win95-era cipher suites, so 443 connecting ≠ HTTPS working. Plaintext protocols (FTP, telnet, IRC, POP3, gopher) are the realistic win here.

…shim

The fetch network adapter only intercepts port 80 (parsed as HTTP and
replayed via fetch). Everything else was RST'd. This adds a second
tcp-connection bus listener that bridges any other destination port
straight to a Node net.Socket in the renderer — no extra process, no
WebSocket relay, no new dependencies.

- net/tcp-relay.ts: hook v86 tcp-connection, accept SYN synchronously,
  open a real socket to conn.psrc:conn.sport, buffer until connect,
  pipe both directions, tear down on either side closing. Loopback,
  link-local and multicast are refused; the rest of RFC1918 is left
  reachable so the guest can still talk to LAN devices.
- net/dns-shim.ts: dns_method is now "doh" so the guest gets real IPs
  for the relay to dial, but Cloudflare can't answer single-label names
  like "windows95" or the NNN.external localhost magic. The shim wraps
  global fetch, spots /dns-query POSTs for those names, and answers
  with the same 192.168.87.1 placeholder the static resolver used to
  hand out — the port-80 fetch path then takes over via the Host header
  exactly as before.
- emulator.tsx: wire both in next to the SMB hook; set dns_method.
- debug-harness.ts: WIN95_PROBE_RUN/_RUN_AFTER/_RUN_WAIT to type an
  arbitrary command into Start→Run (used to e2e-test the relay with
  telnet against a host echo server).
@felixrieseberg felixrieseberg merged commit 85c4451 into main Apr 11, 2026
8 checks passed
@felixrieseberg felixrieseberg deleted the claude/quizzical-benz branch April 11, 2026 22:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant