Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
14a4337
iris-gui: NET status indicator (internal-network traffic light)
danifunker Jun 18, 2026
678c257
iris-gui: move mouse/keyboard capture status into the control column
danifunker Jun 18, 2026
128f561
iris-gui: fix from-scratch window sizing (crash, clamp, default scale)
danifunker Jun 18, 2026
a3ba148
iris-gui: re-fit window to vm_scale every launch; stop persisting win…
danifunker Jun 18, 2026
82e6e8f
iris-gui: redesign Networking config tab + NAT diagnostics groundwork
danifunker Jun 18, 2026
f05ae61
iris-gui: live NAT subnet apply + host-conflict guard; coherent Check…
danifunker Jun 18, 2026
fd94733
iris: FTP passive-mode ALG for inbound port-forwards
danifunker Jun 18, 2026
900b8c8
docs: Phase 3 plan + suppaftp-emu transport-generic fork prompt
danifunker Jun 18, 2026
0f0c227
iris: robust NAT adoption + "networking off" diagnostic
danifunker Jun 18, 2026
f99b399
iris-gui: gate NFS by platform + macOS install guidance
danifunker Jun 18, 2026
051d144
iris: live port-forward rebind (no restart to add/remove a forward)
danifunker Jun 18, 2026
1446157
docs: plan for in-core NFSv3/UDP server (nfsv3udp.rs)
danifunker Jun 18, 2026
4393b36
iris: NFSv3/UDP server — increment 1: backing-store core
danifunker Jun 18, 2026
1478154
iris: rename nfsv3udp.rs -> nfsudp.rs (server is NFSv2+v3, not v3-only)
danifunker Jun 18, 2026
d64d543
iris: NFS/UDP server — increment 2: XDR + RPC framing
danifunker Jun 18, 2026
cfc286f
iris: NFS/UDP server — increment 3: NFSv3 read procedures
danifunker Jun 18, 2026
963ae1c
iris: NFS/UDP server — increment 4: NFSv3 write procedures + DRC + Nf…
danifunker Jun 18, 2026
2d1d0e2
iris: NFS/UDP server — increment 5: NFSv2 (IRIX 5.3)
danifunker Jun 18, 2026
f455217
iris: NFS/UDP server — increment 6: MOUNT protocol (v1 + v3)
danifunker Jun 18, 2026
06205be
iris: NFS/UDP server — increment 7: wire it into the NAT (no more unfsd)
danifunker Jun 18, 2026
685f534
iris: NFS/UDP server — increment 8: un-gate GUI + drop unfsd config
danifunker Jun 19, 2026
a8160b6
docs: NFS now in-process (no unfsd) — update HELP.md + plan status
danifunker Jun 19, 2026
5638b60
docs: resume/troubleshooting handoff for networking + in-core NFS
danifunker Jun 19, 2026
83ff34f
iris: NFSv2 READDIR must respect count + fit one datagram
danifunker Jun 19, 2026
fe8d449
iris: CHD copy-on-write + "Synchronizing disks" fold-on-exit
danifunker Jun 19, 2026
6015a0b
iris-gui: CHD COW UI, powered-off overlay, capture debounce, NFS shar…
danifunker Jun 19, 2026
a20ca42
removed un-necessary documentation
danifunker Jun 19, 2026
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
48 changes: 16 additions & 32 deletions HELP.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,62 +188,46 @@ grep -v '^#' /etc/inetd.conf | grep -E 'tcp|udp'

## NFS file sharing

IRIS can export a host directory to IRIX over NFS using
[unfs3](https://github.com/unfs3/unfs3) as the NFS server. The emulator
handles portmap (port 111) internally and NATs NFS/mountd traffic to a
localhost unfsd instance — no root privileges required.
IRIS exports a host directory to IRIX over NFS using a **built-in, pure-Rust NFS
server** (`src/nfsudp.rs`). It runs entirely inside the NAT — the emulator
answers portmap (port 111) and the MOUNT/NFS RPC itself and injects the replies
as virtual-network frames. **Nothing to install** (no external `unfsd`), **no
host sockets**, and it works the same on Linux, macOS, and Windows. The only
host interaction is reading/writing files in the folder you export.

### Requirements

Install unfs3 and make sure `unfsd` is in your `PATH`:

```bash
# Debian / Ubuntu
apt install unfs3

# Or build from source
git clone https://github.com/unfs3/unfs3 && cd unfs3 && ./autogen.sh && ./configure && make
```
It speaks **NFSv2 (IRIX 5.3)** and **NFSv3 (IRIX 6.x)** and answers whichever the
guest mounts with.

### Configuration

Add an `[nfs]` section to `iris.toml`:

```toml
[nfs]
shared_dir = "./shared" # directory to export (resolved to absolute path at startup)
# unfsd = "unfsd" # path to unfsd binary [default: unfsd]
# nfs_host_port = 12049 # host port for NFS [default: 12049]
# mountd_host_port = 11234 # host port for mountd [default: 11234]
shared_dir = "./shared" # directory to export (created on demand)
# version = "auto" # "auto" (default), "v2", or "v3"
```

Or use the command-line flag to enable it without editing the config:
Or enable it from the command line:

```bash
iris --nfs-dir /path/to/share
iris --nfs-dir /path/to/share --nfs-port 12049 --mountd-port 11234 --unfsd /usr/sbin/unfsd
```

IRIS will start unfsd automatically on launch and kill it on exit. The shared
directory must exist before starting the emulator.
(The GUI exposes the same under Configuration → Networking → NFS share.)

### Mounting from IRIX

The emulator prints the export path at startup:

```
iris: unfsd started (pid 1234) nfs=127.0.0.1:12049 mountd=127.0.0.1:11234 dir=/absolute/path/to/shared
```

From IRIX, mount using that absolute path:
The export is a single root, so mount it as `/`:

```
# mkdir /shared
# mount 192.168.0.1:/absolute/path/to/shared /shared
# mount 192.168.0.1:/ /shared
# ls /shared
```

NFS version 3 is used by default; IRIX will fall back to version 2 automatically if needed.
Use the gateway address shown in the GUI (it tracks your NAT subnet). The server
fakes uid/gid/mode so the export behaves the same regardless of the host OS.

### Checking network status from the monitor

Expand Down
295 changes: 295 additions & 0 deletions docs/cow-chd-sync-plan.md

Large diffs are not rendered by default.

183 changes: 183 additions & 0 deletions docs/networking-tab-redesign.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Networking tab redesign — design & plan

Status: **Phase 0 + 1 done** (subnet logic + Networking tab UI landed, unit-tested,
builds clean on default and `appstore` features; not yet committed). Phases 2
(FTP ALG) and 3 (in-app file bridge) pending. Captures the design agreed for the iris-gui
**Networking** config tab and the two new backend features it pulls in. Mirrors
the `docs/cow-chd-sync-plan.md` convention so the work survives across sessions.

## Goal

A regular user with **no networking knowledge** opens the Networking tab, accepts
the defaults, and it *just works*. Power users can still build odd subnets, port
forwards, and file-sharing setups. Everything must stay inside the Mac App Store
sandbox (no new entitlements; no reliance on spawning external binaries).

## Background — how the backend constrains the UI

- `nat_subnet` is stored as a **single CIDR string** (e.g. `192.168.0.0/24`).
The two new UI controls (network + mask) just recompose into that one string.
- `iris::config::parse_nat_subnet` requires the **network address** (host bits
zero), rejects prefix `> /30`, and always derives **gateway = network + 1**,
**Indy `ec0` = network + 2**. So the host octet typed by the user is never the
actual host — it must be `.0`, and both endpoints fall out of the subnet.
- `net.rs` already runs a real userspace TCP state machine *toward the guest*
(`NatTcpEntry`: `server_seq` / `client_seq` / `client_win` / `retransmit`), and
the port-forward path (`TcpFwdPending`) bridges a peer to the guest by injecting
segments. Today that peer is always a host `TcpStream`. The **in-app file
bridge** swaps that peer for an in-process protocol engine.
- NFS works by spawning an **external `unfsd`** (`src/main.rs`), which a sandboxed
MAS app generally can't exec — so NFS likely doesn't function in the App Store
build (open item below).

## Decisions

### A. Private-network controls (replace the single CIDR text field)
- **Network address** dropdown: *first-free 192.168.x* (default) / first-free
172.16.x / first-free 10.x / **Custom…** (free-typed, snap-to-boundary + warn).
- **Subnet mask** dropdown — prefixes **8, 12, 16, 22, 24, 25, 26** (default
**/24**) / **Custom…** (type bits → show mask). `/8 /12 /16` are the native
sizes of the three RFC1918 blocks. Custom still reaches `/30`.
- **Live derived line**: gateway (net+1), Indy `ec0` (net+2), usable host range,
broadcast, and conflict ✓/⚠.
- **`if-addrs`** dependency powers *first-free* selection and *overlap* warnings
by reading the host's own interface addresses (no entitlement; does not trigger
the macOS 15 Local Network prompt — that's for talking *to* LAN peers).
- **Sanity tiers / override dialog**:
- **Hard** (engine can't represent): prefix `0` or `> /30`, malformed → blocked.
- **Off-boundary** (e.g. `192.168.40.0/16`): snap to the real network
(`192.168.0.0/16`) + a small grey note. Widening the mask is the common cause.
- **Soft** (parses but unwise): not RFC1918, or overlaps a host network →
confirmation dialog *"This networking configuration does not appear to be
valid, please double-check…"* → **[Override Sanity Checks]** / **[Cancel]**
(Cancel reverts to the previous good value). Big masks (`/8 /12`) overlap
Docker/VPN ranges far more often, so this earns its keep.
- Defaults are RFC1918 + /24 + conflict-checked, so a normal user **never**
sees the dialog.
- The **troubleshooting** (`netfix`) dialog surfaces the Indy's *required* address
(`ExpectedNet`); the config tab shows the host/gateway side.

### B. Port forwards
- `+ Add forward ▼` menu: **Telnet** (2323→23) / **FTP** (2121→21) / **Custom…**.
- Pre-filled rows use **unprivileged host ports** (>1024) — required to stay in
the sandbox without root.

### C. FTP ALG (in `net.rs`) — *kept*
- Watches a forwarded FTP control stream, rewrites `PASV`/`PORT`, and opens the
data port dynamically so the FTP **port-forward** works for a user's *own
external* FTP client. Uses the existing `network.server` entitlement.

### D. In-app NAT-side file bridge — *the big one*
- The app is already a host on the virtual network (`gateway_ip`). An in-app
client originates from a NAT address straight to the guest's daemon over the
emulated SEEQ8003 — **no NAT traversal, no host sockets, no new entitlement**,
and it reads/writes only user-selected local files (already entitled). This is
the cleanest App Store file-sharing story and fills the NFS gap there.
- **Abstract the TCP-peer seam**: generalize the forward-path peer from a host
`TcpStream` to an in-process stream trait, reusing the `NatTcpEntry` core.
- Ship **FTP-passive client first** (app opens both control + data connections to
guest `ftpd` — no reverse channel), with **rcp/rsh behind the same seam later**
(adds a guest-dials-back stderr channel + `.rhosts` trust).
- **UI**: folder picker (Browse + MRU), file list with push/pull, credentials for
FTP.
- **Guest auto-provision** via the existing `netfix` serial path: enable the
daemon / confirm trust or account, so the user only clicks Browse. The
troubleshooting dialog and the bridge become one "set up sharing" flow.

### E. NFS
- Add an explanatory blurb + a **live-generated** mount command (gateway + folder
fill in automatically).
- Keep NFS for the **notarized DMG** build; the App Store build relies on the
in-app bridge. Final gating pending the unfsd-in-sandbox investigation.

## Entitlements / App Store
- **No new entitlements.** Port-forwards are already justified under
`com.apple.security.network.server`; the FTP ALG reuses it. The in-app bridge
adds **zero** socket surface. `if-addrs`/`getifaddrs` needs none.
- Unprivileged host ports keep forwards legal in the sandbox.

## Build order

| Phase | What | Risk | Status |
|---|---|---|---|
| **0** | Pure subnet/conflict logic (`iris-gui/src/netplan.rs`) + `if-addrs`: parse/compose CIDR, mask presets, first-free, conflict, sanity tiers, snap, derived addrs — 15 unit tests. No UI. | low | **done** |
| **1** | Networking tab UI on Phase 0: network preset combo, mask combo (8/12/16/22/24/25/26 + custom bits), CIDR field, derived line + conflict ✓/⚠, override modal (Cancel / Use suggested / Override), Add-forward menu (Telnet/FTP/Custom), NFS blurb + live mount cmd, troubleshooting-window Indy address. Network edits now mark cfg dirty. | low | **done** |
| **2** | FTP ALG in `net.rs`: rewrite passive-mode `227` replies on an inbound FTP control forward, bind a localhost data listener, register a transient (FIFO-bounded) data forward. Pure parse/rewrite unit-tested. | med | **done** |
| **3** | In-app file bridge — see decision below. | high | pending |

### Phase 3 plan (decided 2026-06-18)
Architecture **B** (pure NAT-side client, no host sockets). FTP reuses suppaftp's
protocol code via a **transport-generic fork** rather than a hand-rolled client —
spec in `docs/suppaftp-emu-fork-prompt.md` (make suppaftp's sync client generic
over an `FtpConnector` trait; default `TcpConnector` preserves behavior; passive
first; TLS only on the TCP path). With a virtual-net connector **no ALG/PASV
rewrite is needed** for the bridge (we reach the guest's PASV address directly on
the virtual net; the Phase 2 ALG stays only for the external-client forward).

Two tracks:
- **suppaftp-emu fork** — separate crate/repo, driven by the prompt (user kicks
this off). IRIS links it by path/git.
- **IRIS foundation** (resumes once the crate API exists): (1) `NatEngine`
in-process TCP-peer seam — generalize `TcpFwdPending`/`NatTcpEntry`'s
`stream: TcpStream` to `{ HostSocket(TcpStream), InProc(..) }`; (2)
`VirtualTcpStream: Read+Write` + a `VirtualConnector` impl of the crate's
`FtpConnector`; (3) **dual-pane** UI (rfd Browse + MRU local, FTP `LIST`/`CWD`
for the Indy); (4) rcp (hand-rolled — no crate exists); (5) guest daemon
auto-provision via the `netfix` serial path.

Status: paused after delivering the fork prompt; IRIS side not started.

### Phase 2 implementation notes / limits
- Works because the NAT *relays application bytes* between an OS host socket and
the userspace guest-side TCP, so the length-changing `227` rewrite needs no
seq/ack surgery (the host stack re-sequences). `client_seq` still advances by
the original payload length.
- Inbound FTP control connection identified by `server_ip == gateway && client_port == 21`.
- **Handles classic passive mode only** (`PASV` → `227`). Not handled yet:
active mode (`PORT`), extended passive (`EPSV`/`229`), or a `227` split across
TCP segments (no control-stream reassembly — fine for IRIX ftpd, which sends it
in one segment).
- Data forwards are transient, FIFO-capped at 16, and truncated on reset /
live-subnet-apply. **Unverified on a real boot** — needs a manual FTP transfer.

### Phase 1 implementation notes
- `cfg.nat_subnet` stays the raw-CIDR source of truth; preset/mask controls
rewrite it (snapped via `to_cidr`), the CIDR field stays partial-typing-safe.
- Config-editor tab edits did **not** previously mark the config dirty (only the
Memory/SCSI quick-menus did). The Networking tab now reports `changed` up via
`TabOutcome` so its edits autosave; other tabs are unchanged (pre-existing gap
left for a separate fix).
- The override modal fires only on a *committed* soft-invalid subnet (preset/mask
pick or CIDR field losing focus), never on live keystrokes.

Phase 1 depends on 0. Phases 2 and 3 both touch `net.rs`; sequence them. Phase 3
is the largest.

## Open items
- **unfsd-in-sandbox**: confirm whether `unfsd` is bundled/signed and actually
launches under the App Store sandbox. Drives whether the NFS panel is compiled
out under `feature = "appstore"`.

## Verbiage (approved)

**NAT intro:** "IRIS gives the Indy its own private NAT network — the same trick
your home router uses. The Indy reaches the internet through IRIS, but nothing on
your real network can see it. Pick a subnet that doesn't overlap a network your
computer already uses (Wi-Fi, Ethernet, VPN, Docker…) — if it does, IRIS flags it
below, since an overlap can cut the Indy off from the internet."

**Port forwards helper:** "A port forward maps a port on your computer to a port
on the Indy, so host tools can reach guest services (log in, copy files…).
Inbound only, and it works once the guest is up on the NAT subnet. None exist by
default."

**NFS blurb:** "The Indy speaks NFS natively — the easiest, batteries-included way
to move files between your computer and the emulated machine. IRIS runs the NFS
server for you, backed by the folder you pick below; there's nothing to install
and no NFS know-how required. Pick a folder, boot the Indy, then mount it:
`mkdir /shared` / `mount <gateway>:<shared-dir> /shared`. Your files appear at
`/shared` on the Indy. The host address and path above update automatically to
match your subnet and folder."

**Override dialog:** "This networking configuration does not appear to be valid,
please double-check…" — buttons **[Override Sanity Checks]** / **[Cancel]**.
Loading
Loading