Skip to content

RyuLdn Protocol

This page documents the RyuLdn network protocol used to communicate between the Switch sysmodule and the Ryujinx LDN server.

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.

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.

| 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 |

| 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 |

| 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 |

| 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 |

| Value | Name | Direction | Description | |-------|------|-----------|-------------| | 22 | SetAcceptPolicy | C→S | Change accept policy (allow/reject) | | 23 | SetAdvertiseData | C→S | Update session advertisement data |

| Value | Name | Direction | Description | |-------|------|-----------|-------------| | 254 | Ping | Both | Keepalive with timestamp | | 255 | NetworkError | S→C | Error notification |

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.

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.

struct InitializeMessage { // 22 bytes (0x16)
SessionId id; // 16 bytes: client session ID (zeros = new)
MacAddress mac_address; // 6 bytes: client MAC (zeros = assign new)
};
struct PassphraseMessage { // 128 bytes (0x80)
uint8_t passphrase[128]; // UTF-8 passphrase, null-padded
}; // "Ryujinx-<hex8>" or empty for public
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)
};

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 structure
struct 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
};
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.

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.

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
};
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
};
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
};

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);
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.

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
};

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.

The C# server uses [StructLayout(LayoutKind.Sequential)] without Pack=1. This has real consequences:

  1. LdnHeader is 12 bytes, not 10: reserved at offset 0x06 pads data_size to offset 0x08
  2. ConnectRequest has _padding at 0x7C to align NetworkInfo at 0x80 (8-byte boundary for int64_t)
  3. ExternalProxyConnectionState has 3 bytes of _pad after connected for 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.

| 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 |

|| 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 |

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 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.

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)
};

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
};
struct __attribute__((packed)) UserConfig {
char user_name[33]; // Player name (UTF-8, null-terminated)
uint8_t unknown1[15]; // Unknown/reserved
};
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];
};

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;
};

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
};

Array of 8 address entries for CreateAccessPointPrivateRequest.

struct __attribute__((packed)) AddressList {
AddressEntry addresses[8];
};

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.source_ipv4 and ProxyInfo.dest_ipv4 use the same Ryujinx format as all other LDN IPs (big-endian as uint32). See Byte Order & Padding.