RyuLdn Protocol
RyuLdn Protocol
Section titled “RyuLdn Protocol”This page documents the RyuLdn network protocol used to communicate between the Switch sysmodule and the Ryujinx LDN server.
Overview
Section titled “Overview”RyuLdn is a binary protocol over TCP. All multi-byte integers are little-endian, matching both x86/x64 (server) and ARM (Switch) native byte order.
Critical: The protocol must remain byte-for-byte compatible with the C# server (LdnServer/Network/RyuLdnProtocol.cs). The C# server uses [StructLayout(LayoutKind.Sequential)] without Pack=1, which means int32 fields are aligned on 4-byte boundaries. See Byte Order & Padding for details.
Packet Structure
Section titled “Packet Structure”Every packet starts with a 12-byte LdnHeader:
┌──────────────────────────────────────────────────┐│ LdnHeader (12 bytes) │├──────────┬────────┬─────────┬──────────┬─────────┤│ Magic │ Type │ Version │ Reserved │DataSize ││ 4 bytes │ 1 byte │ 1 byte │ 2 bytes │ 4 bytes ││ 0x4E444C52│ 0-255 │ 1 │ 0x00 │ 0-N │└──────────┴────────┴─────────┴──────────┴─────────┘| Field | Offset | Size | Description |
|-------|--------|------|-------------|
| magic | 0x00 | 4 | Protocol magic: 0x4E444C52 (“RLDN” in LE) |
| type | 0x04 | 1 | Packet type from PacketId enum |
| version | 0x05 | 1 | Protocol version (must be 1) |
| reserved | 0x06 | 2 | Padding to align data_size on 4-byte boundary |
| data_size | 0x08 | 4 | Payload size in bytes (signed int32, may be 0) |
Important: The header is 12 bytes, not 10. The reserved field at offset 0x06 pads data_size onto a 4-byte boundary at offset 0x08. This matches the C# StructLayout(LayoutKind.Sequential, Size=0xA) where Size=0xA only affects marshaling, not the internal struct layout.
Packet Types
Section titled “Packet Types”Session Management (0–1)
Section titled “Session Management (0–1)”| Value | Name | Direction | Description |
|-------|------|-----------|-------------|
| 0 | Initialize | C→S | Client sends session ID and MAC address |
| 1 | Passphrase | C→S | Room passphrase for private room filtering |
Access Point Operations (2–9)
Section titled “Access Point Operations (2–9)”| Value | Name | Direction | Description |
|-------|------|-----------|-------------|
| 2 | CreateAccessPoint | C→S | Create a public network session |
| 3 | CreateAccessPointPrivate | C→S | Create a private (passphrase) session |
| 4 | ExternalProxy | S→C | Configure external proxy mode |
| 5 | ExternalProxyToken | S→C | External proxy authentication token |
| 6 | ExternalProxyState | S→C | External proxy state update |
| 7 | SyncNetwork | S→C | Synchronize network state to clients |
| 8 | Reject | C→S | Host rejects a player |
| 9 | RejectReply | S→C | Server confirms rejection |
Network Discovery (10–16)
Section titled “Network Discovery (10–16)”| Value | Name | Direction | Description |
|-------|------|-----------|-------------|
| 10 | Scan | C→S | Request available networks |
| 11 | ScanReply | S→C | One discovered network |
| 12 | ScanReplyEnd | S→C | End of scan results |
| 13 | Connect | C→S | Join a network |
| 14 | ConnectPrivate | C→S | Join a private network |
| 15 | Connected | S→C | Connection confirmed |
| 16 | Disconnect | Both | Disconnection notice |
Proxy Operations (17–21)
Section titled “Proxy Operations (17–21)”| Value | Name | Direction | Description |
|-------|------|-----------|-------------|
| 17 | ProxyConfig | S→C | Configure proxy settings (LDN subnet) |
| 18 | ProxyConnect | S→C | Incoming P2P connection through proxy |
| 19 | ProxyConnectReply | S→C | Proxy connection result |
| 20 | ProxyData | Both | Game data relay through proxy |
| 21 | ProxyDisconnect | Both | Close proxy connection |
Host Control (22–23)
Section titled “Host Control (22–23)”| Value | Name | Direction | Description |
|-------|------|-----------|-------------|
| 22 | SetAcceptPolicy | C→S | Change accept policy (allow/reject) |
| 23 | SetAdvertiseData | C→S | Update session advertisement data |
Utility (254–255)
Section titled “Utility (254–255)”| Value | Name | Direction | Description |
|-------|------|-----------|-------------|
| 254 | Ping | Both | Keepalive with timestamp |
| 255 | NetworkError | S→C | Error notification |
Packet Signaling
Section titled “Packet Signaling”Not all packets wake WaitForResponse. The following table shows which packet types signal m_response_event:
| Signal event? | Packet types |
|---|---|
| Yes | Connected (15), ScanReply (11), ScanReplyEnd (12), RejectReply (9), ProxyConnectReply (19), NetworkError (255) |
| No | ProxyConfig (17), ProxyData (20), ProxyConnect (18), ExternalProxy (4), SyncNetwork (7), Ping (254) |
This prevents spurious wake-ups — ProxyConfig arriving before Connected no longer triggers a false timeout.
Authentication Flow
Section titled “Authentication Flow”Client Server │ │ │──── Passphrase (1) ─────────────────────▶│ │ (passphrase or empty for public) │ │ │ │──── Initialize (0) ─────────────────────▶│ │ (session_id, mac_address) │ │ │ │◀──── ProxyConfig (17) ──────────────────│ │ (proxy_ip, proxy_subnet_mask) │ │ │ │◀──── Connected (15) ─────────────────────│ │ (NetworkInfo + node assignment) │After Connected, the client periodically sends Ping (254) and receives SyncNetwork (7) updates.
Packet Definitions
Section titled “Packet Definitions”Initialize (0)
Section titled “Initialize (0)”struct InitializeMessage { // 22 bytes (0x16) SessionId id; // 16 bytes: client session ID (zeros = new) MacAddress mac_address; // 6 bytes: client MAC (zeros = assign new)};Passphrase (1)
Section titled “Passphrase (1)”struct PassphraseMessage { // 128 bytes (0x80) uint8_t passphrase[128]; // UTF-8 passphrase, null-padded}; // "Ryujinx-<hex8>" or empty for publicProxyConfig (17)
Section titled “ProxyConfig (17)”struct ProxyConfig { // 8 bytes uint32_t proxy_ip; // LDN subnet IP (e.g., 10.114.0.1 as 0x0A720001) uint32_t proxy_subnet_mask; // Subnet mask (e.g., 0xFFFF0000 for /16)};Connected (15)
Section titled “Connected (15)”Responds with full NetworkInfo (0x480 bytes) after the header. The client’s assigned node is identified by matching GetIpv4Address() against NetworkInfo.ldn.nodes[].ipv4_address.
Scan (10) / ScanReply (11) / ScanReplyEnd (12)
Section titled “Scan (10) / ScanReply (11) / ScanReplyEnd (12)”- Scan: payload is a
ScanFilterFull(0x60 bytes) or empty for wildcards - ScanReply: payload is one
NetworkInfo(0x480 bytes) - ScanReplyEnd: no payload, signals end of scan results
CreateAccessPoint (2) / CreateAccessPointPrivate (3)
Section titled “CreateAccessPoint (2) / CreateAccessPointPrivate (3)”struct CreateAccessPointRequest { // 0xBC bytes (188) SecurityConfig security_config; // 0x44 bytes UserConfig user_config; // 0x30 bytes NetworkConfig network_config; // 0x20 bytes RyuNetworkConfig ryu_network_config;// 0x28 bytes};// Advertise data is appended after this structurestruct CreateAccessPointPrivateRequest { // 0x13C bytes (316) SecurityConfig security_config; // 0x00: 0x44 bytes SecurityParameter security_parameter; // 0x44: 0x20 bytes UserConfig user_config; // 0x64: 0x30 bytes NetworkConfig network_config; // 0x94: 0x20 bytes AddressList address_list; // 0xB4: 0x60 bytes RyuNetworkConfig ryu_network_config; // 0x114: 0x28 bytes};Connect (13) / ConnectPrivate (14)
Section titled “Connect (13) / ConnectPrivate (14)”struct ConnectRequest { // 0x500 bytes (1280) SecurityConfig security_config; // 0x00: 0x44 bytes UserConfig user_config; // 0x44: 0x30 bytes uint32_t local_communication_version; // 0x74: 4 bytes uint32_t option_unknown; // 0x78: 4 bytes uint32_t _padding; // 0x7C: 4 bytes (C# alignment padding) NetworkInfo network_info; // 0x80: 0x480 bytes};static_assert(sizeof(ConnectRequest) == 0x500);Important: The _padding field at 0x7C is required because the C# StructLayout (without Pack=1) aligns NetworkInfo (which contains int64_t local_communication_id in IntentId) to an 8-byte boundary at offset 0x80.
ProxyData (20)
Section titled “ProxyData (20)”struct ProxyDataHeader { // 20 bytes (0x14) ProxyInfo info; // 16 bytes: source/dest addressing uint32_t data_length; // 4 bytes: payload size following header};ProxyInfo contains source/dest IPv4, source/dest port, and protocol type (TCP=6, UDP=17). This is how game traffic (PIA mesh data) is tunneled through the server.
For broadcast UDP packets (PIA mesh discovery), dest_ipv4 is the broadcast address (e.g., 10.114.255.255) and dest_port matches the game’s listening port. The Switch-side RouteIncomingData() must deliver these to all matching proxy sockets on the same port.
ExternalProxy Structures
Section titled “ExternalProxy Structures”struct ExternalProxyConfig { // 0x26 bytes (38) uint8_t proxy_ip[16]; // IP address string (IPv4 or IPv6) uint32_t address_family; // 2 = IPv4, 23 = IPv6 uint16_t proxy_port; // Proxy server port uint8_t token[16]; // Authentication token};
struct ExternalProxyToken { // 0x28 bytes (40) uint32_t virtual_ip; // Assigned virtual IP uint8_t token[16]; // Auth token uint8_t physical_ip[16]; // Client's physical IP uint32_t address_family; // 2 = IPv4, 23 = IPv6};
struct ExternalProxyConnectionState { // 0x08 bytes (8) uint32_t ip_address; // Client IP address uint8_t connected; // 0=disconnected, 1=connected uint8_t _pad[3]; // Pack=4 alignment padding};Host Control Packets
Section titled “Host Control Packets”struct SetAcceptPolicyRequest { // 1 byte uint8_t accept_policy; // AcceptPolicy enum (0-3)};
struct RejectRequest { // 8 bytes uint32_t node_id; // Node ID of player to reject uint32_t disconnect_reason; // DisconnectReason enum value};Utility Packets
Section titled “Utility Packets”struct PingMessage { // 2 bytes uint8_t requester; // 0 = server ping (echo back), 1 = client ping uint8_t id; // Ping ID for matching request/response};
struct NetworkErrorMessage { // 4 bytes uint32_t error_code; // NetworkErrorCode enum value};
struct DisconnectMessage { // 4 bytes uint32_t disconnect_ip; // IP of disconnecting client};Key Structures
Section titled “Key Structures”NetworkInfo (0x480 bytes = 1152 bytes)
Section titled “NetworkInfo (0x480 bytes = 1152 bytes)”The main structure used in ScanReply, Connected, and SyncNetwork packets:
struct NetworkInfo { NetworkId network_id; // 0x000: 32 bytes CommonNetworkInfo common; // 0x020: 48 bytes (0x30) LdnNetworkInfo ldn; // 0x050: 1072 bytes (0x430)};static_assert(sizeof(NetworkInfo) == 0x480);NodeInfo (0x40 bytes = 64 bytes)
Section titled “NodeInfo (0x40 bytes = 64 bytes)”struct NodeInfo { uint32_t ipv4_address; // Network byte order MacAddress mac_address; // 6 bytes uint8_t node_id; // 0 = host, 1-7 = clients uint8_t is_connected; // 1 = connected char user_name[33]; // UTF-8, null-terminated uint8_t reserved1; uint16_t local_communication_version; uint8_t reserved2[16];};Node 0 is always the host. Games use GetIpv4Address() and match it against nodes[].ipv4_address to find their own node index.
LdnNetworkInfo (0x430 bytes = 1072 bytes)
Section titled “LdnNetworkInfo (0x430 bytes = 1072 bytes)”struct LdnNetworkInfo { uint8_t security_parameter[16]; // 0x000 uint16_t security_mode; // 0x010: SecurityMode enum uint8_t station_accept_policy; // 0x012: AcceptPolicy enum uint8_t unknown1; // 0x013 uint16_t reserved1; // 0x014 uint8_t node_count_max; // 0x016 uint8_t node_count; // 0x017 NodeInfo nodes[8]; // 0x018: 8 × 64 = 512 bytes uint16_t reserved2; // 0x218 uint16_t advertise_data_size; // 0x21A uint8_t advertise_data[384]; // 0x21C uint8_t unknown2[140]; // 0x39C uint64_t authentication_id; // 0x428};Byte Order & Padding
Section titled “Byte Order & Padding”IP Addresses — Critical Invariant
Section titled “IP Addresses — Critical Invariant”LDN IPs in NetworkInfo, ProxyConfig, and m_ipv4_address are all in Ryujinx format: big-endian read as uint32 (e.g., 10.114.0.1 = 0x0A720001). BSD sockaddr_in.sin_addr uses network byte order (which is also big-endian). GetAddr() does bswap32, sin_addr is stored in Ryujinx format (no bswap in Bind). Never double-convert.
Struct Layout Padding
Section titled “Struct Layout Padding”The C# server uses [StructLayout(LayoutKind.Sequential)] without Pack=1. This has real consequences:
LdnHeaderis 12 bytes, not 10:reservedat offset 0x06 padsdata_sizeto offset 0x08ConnectRequesthas_paddingat 0x7C to alignNetworkInfoat 0x80 (8-byte boundary forint64_t)ExternalProxyConnectionStatehas 3 bytes of_padafterconnectedfor Pack=4 alignment
Every struct has a static_assert for its expected size. If a static_assert fails, the fix is in the struct layout, not in the assert.
Network Error Codes
Section titled “Network Error Codes”| Value | Name | Description |
|-------|------|-------------|
| 0 | None | No error |
| 1 | VersionMismatch | Protocol version doesn’t match server |
| 2 | InvalidMagic | Invalid protocol magic number |
| 3 | InvalidSessionId | Session ID is invalid or expired |
| 4 | HandshakeTimeout | Handshake didn’t complete in time |
| 5 | AlreadyInitialized | Client already sent Initialize |
| 100 | SessionNotFound | Referenced session doesn’t exist |
| 101 | SessionFull | Session has maximum players |
| 102 | SessionClosed | Session was closed by host |
| 103 | NotInSession | Operation requires being in a session |
| 104 | AlreadyInSession | Already in a session |
| 200 | NetworkNotFound | Requested network doesn’t exist |
| 201 | NetworkFull | Network is at capacity |
| 202 | ConnectionRejected | Host rejected the connection |
| 203 | AuthenticationFailed | Passphrase authentication failed |
| 204 | InvalidRequest | Malformed or invalid request |
| 900 | InternalError | Server internal error |
| 901 | ServiceUnavailable | Service temporarily unavailable |
Enums Reference
Section titled “Enums Reference”|| Enum | Underlying | Values |
||------|------------|--------|
|| AcceptPolicy | uint8_t | AcceptAll=0, RejectAll=1, BlackList=2, WhiteList=3 |
|| SecurityMode | uint16_t | Any=0, Product=1, Debug=2 |
|| NetworkType | uint8_t | None=0, General=1, Ldn=2, All=3 |
|| DisconnectReason | uint32_t | None=0, User=1, SystemRequest=2, DestroyedByHost=3, DestroyedByAdmin=4, Rejected=5, SignalLost=6 |
|| NetworkErrorCode | uint32_t | None=0, VersionMismatch=1, InvalidMagic=2, InvalidSessionId=3, HandshakeTimeout=4, AlreadyInitialized=5, SessionNotFound=100, SessionFull=101, SessionClosed=102, NotInSession=103, AlreadyInSession=104, NetworkNotFound=200, NetworkFull=201, ConnectionRejected=202, AuthenticationFailed=203, InvalidRequest=204, InternalError=900, ServiceUnavailable=901 |
|| ProtocolType | int32_t | See full table below |
ProtocolType (full enum)
Section titled “ProtocolType (full enum)”Maps to System.Net.Sockets.ProtocolType. Only Tcp (6) and Udp (17) are used in practice for PIA traffic.
|| Value | Name | Notes |
||-------|------|-------|
|| -1 | Unknown | |
|| 0 | Unspecified / IP | Same value |
|| 1 | Icmp | |
|| 2 | Igmp | |
|| 3 | Ggp | |
|| 4 | IPv4 | |
|| 6 | Tcp | Used for PIA TCP |
|| 12 | Pup | |
|| 17 | Udp | Used for PIA UDP/broadcast |
|| 22 | Idp | |
|| 41 | IPv6 | |
|| 43 | IPv6RoutingHeader | |
|| 44 | IPv6FragmentHeader | |
|| 50 | IPSecEncapsulatingSecurityPayload | |
|| 51 | IPSecAuthenticationHeader | |
|| 58 | IcmpV6 | |
|| 59 | IPv6NoNextHeader | |
|| 60 | IPv6DestinationOptions | |
|| 77 | ND | |
|| 255 | Raw | |
|| 1000 | Ipx | |
|| 1256 | Spx | |
|| 1257 | SpxII | |
NetworkType size mismatch
Section titled “NetworkType size mismatch”NetworkType is uint8_t in the C++ enum but ScanFilterFull.network_type is uint32_t. This mirrors the C# server where ScanFilter uses uint for NetworkType. Wire format is always 4 bytes for the network_type field.
Additional Structures
Section titled “Additional Structures”SecurityConfig (0x44 bytes = 68 bytes)
Section titled “SecurityConfig (0x44 bytes = 68 bytes)”struct __attribute__((packed)) SecurityConfig { uint16_t security_mode; // SecurityMode enum uint16_t passphrase_size; // Length of passphrase (0-64) uint8_t passphrase[64]; // Passphrase data (null-padded)};SecurityParameter (0x20 bytes = 32 bytes)
Section titled “SecurityParameter (0x20 bytes = 32 bytes)”Random security data for private rooms. Used in CreateAccessPointPrivateRequest and ConnectPrivateRequest.
struct __attribute__((packed)) SecurityParameter { uint8_t data[16]; // Random security data uint8_t session_id[16]; // Session ID};UserConfig (0x30 bytes = 48 bytes)
Section titled “UserConfig (0x30 bytes = 48 bytes)”struct __attribute__((packed)) UserConfig { char user_name[33]; // Player name (UTF-8, null-terminated) uint8_t unknown1[15]; // Unknown/reserved};NetworkConfig (0x20 bytes = 32 bytes)
Section titled “NetworkConfig (0x20 bytes = 32 bytes)”struct __attribute__((packed)) NetworkConfig { IntentId intent_id; uint16_t channel; uint8_t node_count_max; uint8_t reserved1; uint16_t local_communication_version; uint8_t reserved2[10];};RyuNetworkConfig (0x28 bytes = 40 bytes)
Section titled “RyuNetworkConfig (0x28 bytes = 40 bytes)”Extended configuration for Ryujinx-specific features.
struct __attribute__((packed)) RyuNetworkConfig { uint8_t game_version[16]; uint8_t private_ip[16]; // For external proxy LAN detection uint32_t address_family; // AddressFamily enum (2=IPv4, 23=IPv6) uint16_t external_proxy_port; uint16_t internal_proxy_port;};AddressEntry (0x0C bytes = 12 bytes)
Section titled “AddressEntry (0x0C bytes = 12 bytes)”IP/MAC address pair for a node.
struct __attribute__((packed)) AddressEntry { uint32_t ipv4_address; // Node IPv4 address MacAddress mac_address; // Node MAC address uint16_t reserved; // Reserved/padding};AddressList (0x60 bytes = 96 bytes)
Section titled “AddressList (0x60 bytes = 96 bytes)”Array of 8 address entries for CreateAccessPointPrivateRequest.
struct __attribute__((packed)) AddressList { AddressEntry addresses[8];};ScanFilterFull (0x60 bytes = 96 bytes)
Section titled “ScanFilterFull (0x60 bytes = 96 bytes)”Complete filter for network scanning. Note: network_type is uint32_t despite NetworkType being uint8_t — this matches the C# server’s ScanFilter which uses uint for this field.
struct __attribute__((packed)) ScanFilterFull { NetworkId network_id; // 0x00: 32 bytes uint32_t network_type; // 0x20: 4 bytes (uint32, not uint8!) MacAddress mac_address; // 0x24: 6 bytes Ssid ssid; // 0x2A: 34 bytes uint8_t reserved[16]; // 0x4C: 16 bytes uint32_t flag; // 0x5C: 4 bytes};ConnectPrivateRequest (0xBC bytes = 188 bytes)
Section titled “ConnectPrivateRequest (0xBC bytes = 188 bytes)”struct __attribute__((packed)) ConnectPrivateRequest { SecurityConfig security_config; // 0x00: 0x44 bytes SecurityParameter security_parameter; // 0x44: 0x20 bytes UserConfig user_config; // 0x64: 0x30 bytes uint32_t local_communication_version; // 0x94: 4 bytes uint32_t option_unknown; // 0x98: 4 bytes NetworkConfig network_config; // 0x9C: 0x20 bytes};ProxyConnectRequest (0x10 bytes = 16 bytes)
Section titled “ProxyConnectRequest (0x10 bytes = 16 bytes)”struct __attribute__((packed)) ProxyConnectRequest { ProxyInfo info; // Connection addressing info};ProxyConnectResponse (0x10 bytes = 16 bytes)
Section titled “ProxyConnectResponse (0x10 bytes = 16 bytes)”struct __attribute__((packed)) ProxyConnectResponse { ProxyInfo info; // Connection addressing info};ProxyDisconnectMessage (0x14 bytes = 20 bytes)
Section titled “ProxyDisconnectMessage (0x14 bytes = 20 bytes)”struct __attribute__((packed)) ProxyDisconnectMessage { ProxyInfo info; // Connection that was closed int32_t disconnect_reason; // Reason for disconnection};ProxyInfo IP byte order
Section titled “ProxyInfo IP byte order”ProxyInfo.source_ipv4 and ProxyInfo.dest_ipv4 use the same Ryujinx format as all other LDN IPs (big-endian as uint32). See Byte Order & Padding.