src/renderer/smb/README.md
Zero-dependency SMB1/CIFS server that lets Windows 95 (running inside v86) mount a host folder as a network drive. Read-only. ~1500 lines.
| Layer | File | What it does |
|---|---|---|
| Ethernet/IP/UDP | nbns.ts | Taps bus.register("net0-send") for raw frames, parses UDP 137, builds reply frames manually |
| NetBIOS Name Service | nbns.ts | Answers Node Status (0x21) and Name Query (0x20) — Win95 won't try TCP until this resolves |
| TCP 139 hook | index.ts | Monkeypatches adapter.on_tcp_connection (old v86) or registers tcp-connection bus event (new v86) |
| NetBIOS Session | netbios.ts | RFC 1002 framing — 4-byte header, reassembles fragmented TCP |
| SMB1 wire | wire.ts, smb.ts | Little-endian Reader/Writer, header parse/build |
| Commands | server.ts | NEGOTIATE, SESSION_SETUP, TREE_CONNECT, TRANSACTION (RAP), TRANSACTION2, SEARCH, OPEN, READ, CLOSE, etc. |
Win95 offers ["PC NETWORK PROGRAM 1.0", "MICROSOFT NETWORKS 3.0", "DOS LM1.2X002", "DOS LANMAN2.1", "Windows for Workgroups 3.1a", "NT LM 0.12"]. We pick
NT LM 0.12 and send the 17-word NT response (Capabilities=0 — no UNICODE, no
NT_STATUS, no NT_FIND, so the rest of the protocol stays OEM/DOS-error). On any
LANMAN dialect Win95's redirector lists directories via CMD_SEARCH (0x81) whose
13-byte name field hard-caps at 8.3; under NT LM 0.12 it switches to
TRANS2/FIND_FIRST2 and asks for level 0x104 (FILE_BOTH_DIRECTORY_INFO)
regardless of CAP_NT_FIND. We implement that level — the 94-byte fixed prefix
plus OEM long name, ShortName always UTF-16LE per spec. The 13-word LANMAN
response is kept as a fallback for clients that don't offer NT.
Two disk shares plus IPC$. The user share is named after path.basename() of the
mounted folder (sanitized, ≤12 chars). TOOLS is purely synthetic — _MAPZ.BAT,
README.TXT — so the user's listing isn't cluttered. treeConnect routes by
share name to a TID; every path-resolving handler branches on TID so the TOOLS
tree never touches the host fs.
SEARCH "\FOO.TXT" is a stat probe — Win95 wants exactly one entry back. If you
prepend . and .. like you would for \*, Win95 reads the first entry (.,
attr=DIRECTORY) and treats FOO.TXT as a folder. Only prepend dots when the
pattern contains * or ?.
The 13-byte name field must be name\0\0\0..., not name \0. Space-padding
before the null means Win95 sees FOO.BAT (with trailing spaces) and can't
match the .BAT file association.
~N suffixes, not just truncation84 files in a real Downloads folder → most have long names → naive truncation
gives 30 copies of 15_UNDER.PDF. Use Windows-style ~N and keep a per-dir
SFN→real-name map so OPEN can find the actual file. resolve() walks each path
component through the map.
After TREE_CONNECT \\HOST\IPC$, Win95 sends RAP NetShareEnum (func=0, WrLeh/
B13BWz) then NetWkstaGetInfo (func=63, WrLh/zzzBBzz) then NetServerGetInfo
(func=13, WrLh/B16BBDz). The data descriptor tells you the layout:
B16 = 16-byte inline name, z = string pointer (4 bytes into a heap that
follows the struct), B = byte, D = dword. We synthesize the struct from the
descriptor so any info-level Win95 asks for gets a plausible reply.
The injected _MAPZ.BAT showed in listings but Win95 stats before opening,
got ERR_BADFILE, said "cannot find". Hook getVirtual() into QUERY_INFO and
CHECK_DIRECTORY, not just OPEN.
The tcp-connection bus event was added later. The old API is
adapter.on_tcp_connection(packet, tuple) — you must construct TCPConnection
yourself, but it's closure-scoped in Closure-compiled libv86.js. Worse,
.on()/.emit()/events_handlers were dead-code-eliminated; the data callback
is a flat .on_data property.
The trick: shadow adapter.receive with a no-op (own-prop on a prototype method
— must restore via delete, not reassignment), call the original handler
with a fake port-80 SYN, take the TCPConnection it builds, re-aim it at port
139. accept(packet) overwrites all routing fields (sport/dport/hsrc/psrc/seq/
ack), .on_data = handler replaces the HTTP callback.
bus.register("tcp-connection")Clean API. The new code keeps both paths; the bus event is a no-op on old builds.
bus.send doesn't catch listener exceptions. They bubble through ne2k →
port_write8 → wasm. Win95 freezes. The corrupted state then gets saved by
onbeforeunload. Wrap everything that runs in a callback.
../) AND through symlinks: realpathSync
the deepest existing ancestor, re-append the unresolved tail, confirm under
root. Symlinks pointing inside the share still work; symlinks pointing out
return ERR_BADFILE.realpathSync + isDirectory()).test-standalone.ts — 55 protocol tests, full round-trips with real file I/O.
Run: npx ts-node --skip-project --transpile-only --compiler-options '{"module":"commonjs","moduleResolution":"bundler","ignoreDeprecations":"6.0"}' src/renderer/smb/test-standalone.ts