Skip to content

RNTP network disk

RNTP (Rourex Network Talk Protocol) v1 exports a RAM volume from one R2 instance to multiple R2 or Linux clients over TCP. The server formats a fresh export using R2's FAT formatter: 2 MiB uses FAT12, larger supported sizes use FAT16. The client mounts the volume at /mnt/net after validating the handshake and boot sector. The usual file, directory, range-I/O and filesystem-size syscalls work there. Memento's Files window recognizes mount type 6 as a writable netdisk; the C, C++ and Go SDKs expose the same mount constant.

Build and connect

make build_rntp builds the service in the sibling r2_app/c/rntp directory against r2_app/c/libcr2.a and stages iso/bin/rntp.elf. make build_iso also builds/stages it. Rebuild both kernels and the boot archive/ISO to include syscall 0x47 and the service. The executable is available from /mnt/tar/bin and /mnt/iso/bin.

To build or test it directly, run make -C ../r2_app/c/rntp build or make -C ../r2_app/c/rntp test. The app owns both server/client code and the portable protocol tests; the kernel owns the bridge and guest integration tests. Kernel build targets accept R2_APP=/path/to/r2_app for another checkout.

On the server, after ETH has configured its address:

bg rntp serve 16

On the client, using the server's IPv4 address:

bg rntp connect 192.0.2.1
mount
cd /mnt/net
write HELLO hello-from-r2
read HELLO.TXT

The default TCP port is 7780. A custom port is the last argument: rntp serve 2 7781 and rntp connect 192.0.2.1 7781. Export sizes are whole MiB, from 2 through 64, subject to available user heap memory. The client chooses a separate local TCP port. Put either command in INIT.RC after bg eth when the peer address and role are known. A connection failure prints an error; there is no automatic reconnect loop.

For two guests on an isolated Ethernet link, start ETH with distinct addresses first: bg eth --ip 192.0.2.1 --mask 255.255.255.0 on the server and bg eth --ip 192.0.2.2 --mask 255.255.255.0 on the client.

Linux FUSE mount

Install a C compiler, pkg-config, libfuse3's development package and fusermount3. Fedora names the development package fuse3-devel; Debian/Ubuntu use libfuse3-dev. Build and mount from r2_app/c/rntp:

make linux # or: make build_rntp_linux from r2_main
mkdir -p ~/rntp
./rntp-fuse 192.0.2.1 ~/rntp -f
# Another shell:
cp picture.bmp ~/rntp/PICTURE.BMP
ls ~/rntp
fusermount3 -u ~/rntp

Use -p 7781 for another TCP port. Omit -f to run in the background. FUSE options follow the mountpoint; -o allow_other uses the host's normal FUSE permission configuration. Each Linux instance and R2 instance sees the same files, directories, renames and deletions. The mount connects to the server; it does not format the existing export.

The Linux client implements FAT12/16 in userspace and participates in RNTP's operation locks. It supports reads/writes, atomic append per FUSE request, create, mkdir, unlink/rmdir, rename (including replacement and NOREPLACE), truncate, DOS modification timestamps and statfs. READ_BATCH prefetches up to 32 sectors; writes coalesce into 32-sector batches. Metadata and data use separate bounded caches, both invalidated after a foreign revision change. Kernel page/attribute/name caches and writeback are disabled to observe peer changes; each write waits for commit. statfs uses committed server STAT data.

Names are case-insensitive FAT 8.3 names; Linux rejects longer names rather than truncating them. This mount has FAT's fixed permissions and UTC DOS times with two-second precision; symlinks, Unix ownership/mode changes, long names, distributed advisory locks and mmap are unsupported. Deleted or remotely renamed open files do not retain POSIX open-file lifetime semantics. Use separate files or application coordination for longer editing sessions: RNTP locks individual filesystem operations, not an application's entire edit. fsync acknowledges the RAM export and cannot make it survive a server reboot.

Lifetime and failures

The server supports up to seven connected clients sharing the same filesystem. It never mounts its export locally. Updated clients use shared sessions and hold a server lock for each FAT operation, including reads. Each commit changes the volume revision when sectors were written; the next client invalidates its metadata cache before accessing that revision. A second daemon on the same R2 still cannot claim its one /mnt/net mount.

Legacy HELLO clients retain an exclusive lease and cannot coexist with shared sessions. Updated R2 clients fall back to an exclusive connection when either peer's kernel/service lacks shared support. Linux requires the shared extension. The server allocates an undo image equal to the export size, plus a sector bitmap, so a 64 MiB export needs roughly 128 MiB for its image and undo storage.

The server's allocated image survives client disconnects; a reconnect mounts its existing FAT volume without formatting it. Stop the client with kill using the task ID from ts; the kernel releases its registration and removes /mnt/net. Reconnect by starting rntp connect again. The server releases a vanished client's lease within ten seconds; it may report BUSY until then.

Data is volatile: stopping/restarting the server or rebooting it loses the export. A write response means the sector has reached the server's RAM. RNTP v1 has no durable backing or durable flush operation. It trusts peers on its network and does not authenticate or encrypt connections.

The client sends heartbeats every two seconds. A failed exchange ends after five seconds and unmounts the disk; the kernel has a ten-second block-I/O deadline even if the client daemon stops servicing requests. Reads and writes report the calling syscall's normal error value (FilesystemError, or u64::MAX for range I/O/directory listing). A failed FAT sector stops further writes in that filesystem operation. Shared sessions keep an undo copy of each changed sector until UNLOCK commits the operation. ABORT, socket reset or lease expiry restores all those sectors before another client can acquire the lock. Killing an R2 process inside an operation disconnects its daemon registration so the server can roll back its writes. A lost commit reply can still mean that the operation committed; clients do not automatically replay writes. Legacy exclusive clients retain their previous unjournaled sector-write behavior.

Mount sizes and free space (syscall 0x40, the shell's mount and Memento's Files window) read a connection-scoped kernel cache. They never wait for TCP or acquire the netdisk's filesystem operation lock. The server counts free clusters directly in its RAM image. The client fetches the initial snapshot before mounting and refreshes it every two seconds using STAT as its heartbeat, including during transfers. Free space can briefly lag a write or deletion; disconnect and a new attachment clear the previous cache. Both peers need the updated RNTP service for the STAT extension. Legacy attach without a published snapshot reports unavailable usage immediately rather than scanning the peer.

Whole FAT operations are serialized while their network requests run. The kernel keeps interrupts and scheduling active during waits. Connection epochs and request IDs prevent late completions from satisfying a later request; attach is refused until an old filesystem operation has unwound. Process death cancels an outstanding request and releases operation ownership.

The client caches up to 192 boot, primary FAT and root-directory sectors (96 KiB of sector data). File data bypasses this cache, so a bulk transfer cannot evict the FAT. Reads reuse acknowledged metadata. Disconnects, failed responses and interrupted filesystem operations invalidate it, including reconnects to the same server generation. Shared clients also invalidate FAT/root metadata whenever LOCK returns a different volume revision. The shared boot sector is immutable, so its validated geometry remains cached.

When CAPS advertises READ_BATCH, R2 downloads fetch up to 32 consecutive sectors (16 KiB) per TCP exchange. Bridge operation 12 publishes the acknowledged batch into a separate kernel read buffer; the remaining sector reads in that FAT operation avoid daemon wakeups and network round trips. The buffer is discarded on operation end, writes, failures, disconnects and foreign revision changes. Metadata-only reads stop before the data area. The daemon uses its own bounded read buffer on older kernels; servers without READ_BATCH retain sector reads. Image loading and other range reads use this path without changing syscall 0x39 or the fmimage job ABI. Update the client kernel and both RNTP apps for the full optimization.

After CAPS negotiation, the kernel stages up to 32 distinct sector writes within the current filesystem operation. Repeated FAT edits and a cluster's zeroing followed by its data write coalesce into their final sector contents. Reads by that operation see its staged contents. The write set flushes when full, before an uncached remote read, and before the outermost operation ends. Every filesystem syscall waits for these acknowledgments before returning; there is no background write-behind. Failed flushes return the syscall's normal error and unmount the export. No unacknowledged sectors enter the shared metadata cache or survive a disconnected/interrupted operation.

Memento copies and moves files in a separate fmcopy.elf process, built with the desktop and staged in iso/bin by make build_iso. The worker performs replacement, range I/O, source deletion for moves, and the final directory listings. The Files window only polls progress and paints cached listings. Escape requests cancellation between chunks; closing the window also cancels the worker and retains its shared state until it stops. A cancelled or failed copy can leave a partial destination; the source is removed only after a complete successful move. The desktop stops outstanding workers on shutdown.

The Image window also loads in the background using fmimage.elf, staged alongside Memento. It checks Jug's /mnt/tmp/jug/FMIMAGE.ELF first, then /mnt/tar/bin/fmimage.elf, /mnt/iso/bin/fmimage.elf, and finally a floppy-root copy at /mnt/fat/FMIMAGE.ELF. Explicit local paths keep a netdisk working directory from delaying the launch. File-size lookup, bounded reads and PNG/JPEG/GIF/BMP decoding run in that process. The window shows loading progress, continues handling input during a slow or stalled netdisk read, and closes with Escape or Back. Its shared job stays alive until a cancelled loader has stopped. Decoded pixels belong to the desktop and remain valid after the loader exits; desktop shutdown stops pending loaders before releasing their shared memory. Loading progress repaints at most every 200 ms, while heartbeat/input polling continues; phase changes and completion repaint immediately.

Wire protocol

Each message has a 40-byte header followed by its declared payload. All multibyte wire fields are big endian; no host pointers appear on the wire.

Offset Bytes Meaning
0 4 ASCII RNTP
4 1 Version, 1
5 1 Opcode; responses set bit 0x80
6 2 Status: 0 okay, 1 busy, 2 invalid, 3 stale generation
8 4 Payload length: Declared payload size, at most 16,644 bytes
12 4 Reserved, zero
16 8 Nonzero request ID, echoed in the response
24 8 Sector LBA; HELLO response carries export capacity in sectors
32 8 Export generation; zero in initial HELLO

Opcodes are 1 HELLO, 2 READ, 3 WRITE, 4 PING, 5 STAT, 6 CAPS, 7 WRITE_BATCH, 8 HELLO_SHARED, 9 LOCK, 10 UNLOCK, 11 READ_BATCH and 12 ABORT. HELLO and HELLO_SHARED use zero generation/LBA and no payload; replies contain sector zero, export capacity and its connection generation. HELLO claims an exclusive export; HELLO_SHARED joins the shared filesystem. Every later request carries that generation. READ returns one sector; WRITE carries one sector and returns no payload. PING has no payload. Shared READ, WRITE and batch operations require the requesting session to hold LOCK.

CAPS uses LBA zero and no payload; its reply is an eight-byte capability mask: 0x01 WRITE_BATCH, 0x02 shared sessions and 0x04 READ_BATCH. WRITE_BATCH uses LBA zero and count: u32, then 1–32 records of lba: u64, data[512]. The maximum payload is 16,644 bytes. The server validates all records before changing any sector. Its empty reply acknowledges the complete batch, whose writes remain undoable until UNLOCK. Shared writes to sector zero are refused. READ_BATCH carries a four-byte count (1–32), starts at the header's LBA and returns count * 512 consecutive bytes. Both Linux and R2 clients use it for bounded read-ahead.

LOCK, UNLOCK and ABORT use LBA zero and no payload. Successful replies carry an eight-byte, nonzero volume revision. LOCK returns BUSY while another operation holds it; that response keeps the connection open and clients retry within their bounded deadline. UNLOCK commits staged changes and increments the revision only if sectors were written. ABORT restores the undo sectors without changing the revision. Disconnect and lease expiry also abort. A revision describes committed disk contents; it differs from the export generation, which stays unchanged across commits and client reconnects.

STAT uses LBA zero and no payload; its eight-byte reply is the free-space count in bytes from the last committed image. The server counts clusters in RAM when committing changes. Heartbeats and other clients never see free space from an unfinished transaction.

The service preserves partial headers, split payloads and coalesced messages. It sends multiple segments within the peer's receive window and negotiates an MSS of 1,460 bytes for the 1,500-byte Ethernet MTU. RNTP provides a 32 KiB TCP receive buffer without changing libcr2's public socket layout. The kernel preserves its actual remaining window in each wire header; it previously overrode the application with a constant 1,024 bytes. TCP construction carries SYN options through IPv4/Ethernet and accepts full MTU packets. Other apps retain plain SYNs, their existing buffers and a 1,370-byte sending limit unless they opt in with tcp_set_mss(). Draining a full socket buffer sends a window update, so a peer can resume without waiting for another application write. RNTP also enables tcp_set_local_packet_crafting() to build IPv4/TCP headers and checksums in userspace, removing two packet-builder syscalls per segment. Raw Ethernet transmission still goes through the kernel. This works on older netdisk kernels; applications using kernel packet construction rebuild a plain SYN and retain the old sending limit if that kernel ignores SYN options. Bursts are paced at eight segments per daemon turn. Cumulative acknowledgments advance the send window, and unacknowledged bytes retransmit at their original TCP sequence numbers after 250 ms, so lost segments cannot resubmit an application write. Application requests remain serialized; write batching amortizes that round trip across many sectors. Kernel block waits sleep until the next timer tick, allowing the transport daemon and UI to run without repeatedly polling all devices in a busy loop.

Kernel bridge: syscall 0x47

Argument 1 is the operation, Argument 2 a buffer or zero, Argument 3 (RCX) the format buffer length or zero. Set RCX explicitly for every call. The native x86 C layouts are defined in r2_app/c/rntp/bridge.h.

Operation Buffer Result
0 Zero, length zero ABI tag 0x52324e44
1 NetDiskAttach, 528 bytes Attach and mount
2 NetDiskRequest, 536 bytes 1 request copied, 0 no request
3 NetDiskRequest, 536 bytes Complete the matching request
4 Zero, length zero Detach/unmount; owner only while attached
5 Shared heap image, length 512–64 MiB, a multiple of 512 Format a new FAT12/16 image
6 NetDiskUsageUpdate, 16 bytes Publish cached free space; owner/generation must match
7 NetDiskAttachInfo, 536 bytes Attach and mount with an initial usage snapshot
8 NetDiskAttachInfo, 536 bytes Attach with usage and operation-scoped write buffering
9 NetDiskBatch, 16,656 bytes, aligned to 8 Poll a write batch: 1 copied, 0 none
10 NetDiskBatchReply, 16 bytes Acknowledge matching batch ID and count
11 NetDiskAttachInfo, 536 bytes Attach with batching and shared operation locks/revision invalidation
12 NetDiskReadAheadReply, 16,408 bytes, aligned to 8 Complete a READ and publish 1–32 acknowledged sectors
13 Zero, length zero Supported read-ahead sector count, 32

NetDiskAttach contains sectors: u64, generation: u64, boot[512]. NetDiskRequest contains id: u64, lba: u64, op: u32, status: u32, data[512]. Bridge operations return Okay (0), Busy (0xfa), InvalidInput (0xfc) or FilesystemError (0xfd), except the query/poll results above. A nonzero completion status fails the operation. Only the registered client daemon can poll/complete. Formatting modifies the supplied image; it does not attach or mount it. The mount type reported by 0x2c and 0x40 is 6 (netdisk); format is 1 FAT12 or 2 FAT16.

NetDiskUsageUpdate contains generation: u64, free_bytes: u64. NetDiskAttachInfo appends free_bytes: u64 to NetDiskAttach. Geometry and format come from the validated boot sector; free bytes must be cluster-aligned and fit within the data area. Operation 7 initializes the cache atomically with the mount. An operation-1 attach has no known free space until operation 6.

NetDiskBatch is id: u64, count: u32, reserved: u32, followed by 32 native {lba: u64, data[512]} records; only the first count are valid. Reserved is zero. NetDiskBatchReply contains id: u64, count: u32, status: u32. Only the attached daemon can poll/complete. Operation 8 enables batching atomically with attachment; operations 1 and 7 keep their legacy behavior. Operation 11 also makes the kernel submit wire opcodes LOCK, UNLOCK and ABORT through the existing 536-byte request bridge. Successful control completions put the big-endian revision in the first eight data bytes, without caching it as a disk sector. Local nesting acquires/releases the server lock only at the outermost filesystem operation.

NetDiskReadAheadReply contains id: u64, lba: u64, count: u32, status: u32 and data[32][512]; only the first count sectors are read. Operation 12 requires a delivered pending READ with matching ID/LBA, valid count and bounds. The kernel copies directly from the checked daemon buffer into static storage without placing a 16 KiB value on the kernel stack. Operation 13 is a capability query; an older kernel returns InvalidInput and the daemon uses sector completions. Wire READ_BATCH replies must match the requested byte count before any sectors enter the read buffer.

Verification

make test includes real FAT formatter/filesystem tests and bridge ownership and failure tests, then runs r2_app/c/rntp's wire codec/server-handler tests. For actual R2 guests, after building the service and kernel:

python3 tests/netdisk/qemu_pair.py --size 2
python3 tests/netdisk/qemu_pair.py --read-benchmark --no-loss --copy-kib 512 --size 16 --require-read-throughput --output /tmp/r2-read-speed
python3 tests/netdisk/qemu_pair.py --size 16 --output /tmp/r2-netdisk-fat16
python3 tests/netdisk/qemu_pair.py --shared --size 2 --output /tmp/r2-netdisk-shared12
python3 tests/netdisk/qemu_pair.py --shared --size 16 --output /tmp/r2-netdisk-shared16
make -C ../r2_app/c/rntp test-linux test-fuse
python3 tests/netdisk/qemu_linux.py --size 2
python3 tests/netdisk/qemu_linux.py --mixed --size 16 --output /tmp/r2-netdisk-linux16
make -C ../r2_app/cpp/memento-hello copytest
python3 tests/netdisk/qemu_pair.py --copy --output /tmp/r2-netdisk-copy
python3 tests/netdisk/qemu_pair.py --desktop --size 16 --output /tmp/r2-netdisk-desktop
python3 tests/netdisk/qemu_pair.py --image-viewer --size 16 --output /tmp/r2-netdisk-image
python3 tests/netdisk/qemu_pair.py --image-viewer --image-loader-jug --size 16 --output /tmp/r2-netdisk-image-jug
python3 tests/netdisk/qemu_pair.py --copy --require-throughput --batch-failure --output /tmp/r2-netdisk-throughput
python3 tests/netdisk/qemu_pair.py --copy-only --no-loss --copy-kib 1024 --require-window --require-mss --output /tmp/r2-netdisk-speed

The fixture uses a private loopback Ethernet relay and its own temporary boot ISO. It checks file bytes across clusters, unaligned range reads, directories, rename, local mount ownership, client process cleanup, reconnect without format, lost READ/WRITE TCP segments and a failed read after losing the server. It also times 100 mount/usage queries with a live and a stalled link, and checks that background free-space refresh observes file and directory allocations. --copy also checks tar-to-netdisk bytes, worker directory snapshots, cancellation and polling gaps. --desktop boots Memento, starts a copy from its Files window and checks that UI heartbeats and Escape input continue during the transfer. --copy-only runs the worker test after connecting. --image-viewer opens a 750 KiB BMP from netdisk in actual Memento, verifies UI heartbeats while loading and that pixels survive the loader's exit, then stalls the connection and checks that Escape closes the window promptly. --image-loader-jug installs the helper in Jug's download directory and omits it from the boot bin directory to catch hard-coded launch paths. --require-throughput requires at least 32 KiB/s for the copy fixture; --rntp-elf selects another service binary for before/after measurements. --kernel selects the matching kernel. --copy-kib accepts 128 (default), 512 or 1024. --no-loss is limited to --copy-only sustained-throughput measurements; the regular tests inject loss. --require-window and --require-mss inspect real Ethernet frames and require 32 KiB windows and 1,460-byte segments, rather than checking only the userspace packet request. --batch-failure verifies that a disconnected peer fails a buffered write at its final flush, including the syscall error and unmount. The app's tests also cover large TCP windows, partial acknowledgments, socket reuse, and validation of a malformed final batch record without partial application.

--shared boots one server and two R2 clients, writes files concurrently and checks both clients' views after foreign changes. qemu_linux.py mounts two actual Linux FUSE clients against the R2 server using a private QEMU user-mode network port. --mixed also mounts an R2 client and verifies bidirectional file exchange and foreign revision refresh with Linux. Host test-linux covers concurrent TCP clients, out-of-space rollback and disconnect rollback; test-fuse creates and removes two temporary mounts for both FAT kinds and runs fsck.fat -n when available.

--read-benchmark writes a known fixture and downloads it in fmimage's 16 KiB chunks, validating every byte. --require-read-throughput requires 128 KiB/s in this QEMU regression. A 512 KiB FAT16 comparison measured 39 KiB/s with sector RPCs and 182 KiB/s with batched reads; these are emulator measurements, not a prediction for physical NICs. The RTL8139 ring boundary regression also covers a full received frame, including CRC, whose header is at the end of the ring; its overflow allocation must accommodate the driver's accepted frame size rather than only an IP packet.