Files
NXST/docs/PROTOCOL.md
T
DragonSpirit ba53a464fe
CI / Build NRO (push) Failing after 39s
CI / Format check (push) Successful in 13s
CI / Layering check (push) Successful in 1s
finish refactor, add docs and CI
2026-04-27 03:19:42 +03:00

3.3 KiB

NXST Wire Protocol

Overview

NXST uses two sockets per transfer session:

Socket Purpose
UDP multicast Receiver advertisement (discovery)
TCP File data stream

Both sides must be on the same local network segment. The sender (Transfer mode) initiates; the receiver (Receive mode) listens.


Discovery — UDP Multicast

Parameter Value
Group 239.0.0.1
Port 8081
Direction sender → receiver

Flow:

  1. Receiver joins multicast group and binds 0.0.0.0:8081.
  2. Sender sends "DISCOVER_SERVER" (15 bytes, no null terminator) to 239.0.0.1:8081.
  3. Receiver replies "SERVER_HERE" (11 bytes) to the sender's source address.
  4. Sender extracts the receiver's IP from the reply source and closes the UDP socket.

Sender polls in 100 ms slices for up to 3 seconds. Cancel is checked each slice.


File Transfer — TCP

Parameter Value
Port 8080
Direction sender connects → receiver listens
Buffer size 65 536 bytes (proto::BUF_SIZE)

Connection:

  1. Receiver listens on 0.0.0.0:8080 (started concurrently with multicast listener).
  2. Sender connects after receiving "SERVER_HERE".

Wire layout — one file:

┌─────────────────────────────────┐
│  filename_len  │  uint32_t LE   │  4 bytes
├─────────────────────────────────┤
│  filename      │  filename_len  │  bytes, no null terminator
│                │  bytes         │
├─────────────────────────────────┤
│  file_size     │  uint64_t LE   │  8 bytes
├─────────────────────────────────┤
│  file_data     │  file_size     │  bytes
│                │  bytes         │
└─────────────────────────────────┘

Files are sent sequentially. The stream ends with a sentinel frame:

filename_len == 0   (proto::EOF_SENTINEL)

No filename or file_size field follows the sentinel.

Constraints:

  • filename_len > proto::MAX_FILENAME (4 096) is treated as a protocol error; the receiver aborts.
  • Filenames are full paths as produced by io::backup (e.g. /switch/NXST/<title>/<user>/...).
  • On the receiver, the username path component is rewritten to match the local user's nickname before writing to disk.

Post-Transfer

After the TCP stream closes (sentinel received), the receiver calls io::restore:

  1. Mounts the title's save filesystem.
  2. Clears existing save data.
  3. Copies received files into save:/.
  4. Commits via fsdevCommitDevice("save").

The sender creates a local backup via io::backup before connecting, so the sender's own save is never at risk.


Cancellation

Either side can cancel at any time by closing the relevant socket (shutdown + close). The other side's blocking read/write will return an error and the transfer loop exits cleanly. The receiver's accept thread sets receiver_state.done = true regardless of how the connection ends, so the UI poll loop always terminates.