The wireless layer
LDN is the Switch’s local wireless; Pia, Nintendo’s peer-to-peer middleware, runs above it.
The two secrets
The LDN passphrase (16-64 bytes, used verbatim) authenticates the 802.11 association. The 16-byte Pia game key derives the session key for every datagram.
| title | LDN passphrase | Pia game key |
|---|---|---|
| Brilliant Diamond / Shining Pearl | WirelessStrongCryptoKey2021 (27 bytes) | from cryptoKeyDataSeed (BDSP) |
| Sword / Shield, Scarlet / Violet, Legends Arceus | W3GoSMEn7RIIUQ89rzqBHGhGferRNb7K18ZBq2aNuj8Us9RO9Q9JYyGOZlLy8MYL (64 bytes) | p1frXqxmeCZWFv0X |
Discovery
tools/ldn/ldn_scan.py reads any advertisement with prod.keys alone. A title sees only advertisements of its own LDN protocol, so a host copies it (HostTransport(protocol=...)).
| title | protocol | advertisement format |
|---|---|---|
the GBA app, comm id 0x01006fa0233f8000 | 3 (AES-GCM, master_key_12) | 3 |
Sword Mystery Gift screen, 0x0100abf008968000 | 1 (AES-CTR, master_key_00) | 2 |
Legends Arceus local trade, 0x01001f5010dfa000 | 1 | 4 |
Beacons go out at 11 Mbit/s DSSS, advertisement action frames at HT MCS 3 (OFDM); a DSSS-only receiver misses the advertisements.
The advertisement’s application data
Pia 5 (Shining Pearl):
+0x00 4 network id random per session
+0x04 4 CRC32 of the user password 0 without a password
+0x08 1 system communication version
+0x09 1 header size 16
+0x0c 4 session parameter seeds the Pia session key
+0x10 application data
A capture decrypts only with its own session’s advertisement.
Pia 6.16 to 6.41 derive the session key and network id from the SSID:
+0x00 2 system property data size 0x5C
+0x02 1 system communication version 21 for 6.16-6.30, 22 for 6.39-6.41
+0x03 2 application communication version
+0x05 16 user password 0xFE-padded, AES-128-GCM under the game key, tag dropped
+0x15 1 is player limit enabled
+0x16 1 number of players
+0x17 4 player name size
+0x1b 1 player name encoding 1 = UTF-8, 2 = UTF-16
+0x1c 64 player name
+0x5c application data
The password’s IV is key[1] key[8] key[7] key[2] (Arceus 1.1.1 0x6fac70, the setter); the game key alone reads it.
Hosting for an emulator
An emulator in ldn_mitm mode associates over port 11452 and runs Pia over the LAN, with no radio and no prod.keys. pokeldn/ldn/ldn_mitm.py joins; ldn_mitm_host.py hosts through IpHostTransport (NEEDS_RADIO = False).
UDP console -> host:11452 Scan header only
UDP host -> console ScanResp NetworkInfo, 0x480
TCP console -> host:11452 Connect NodeInfo, 0x40
TCP host -> console SyncNetwork NetworkInfo with the console seated, held open
nn::ldn::NetworkInfo, 0x480 bytes:
+0x000 NetworkId IntentId 0x10 (u64 localCommunicationId, u16, u16 sceneId, u32) then SessionId 0x10
+0x020 CommonNetworkInfo MAC 6, Ssid 0x22 (length byte then 0x21), s16 channel, u8 linkLevel, u8 networkType, u32
+0x050 LdnNetworkInfo SecurityParameter 0x10, u16 securityMode, u8 acceptPolicy, ...
+0x066 u8 nodeCountMax, u8 nodeCount
+0x068 NodeInfo[8], 0x40 each: u32 IPv4 LE, MAC 6, u8 nodeId,
u8 isConnected, 0x20 name at 0x0C, u16 localCommunicationVersion at 0x2E
+0x26A u16 advertiseDataSize, then 0x180 bytes of advertise data
+0x478 u64 authenticationId
Node 0’s address must be a reachable LAN address: the console sends Pia there. A node’s MAC derives from its address (02:00:ac:10:56:01 for 172.16.86.1). The game’s node carries 88 at +0x2E. FireRed’s scan filter compares localCommunicationId and networkType only.
The 16-byte session id is NetworkId.SessionId, the hex Ssid text, and the plaintext of the Pia session key AES(game_key).encrypt(ssid); network id is crc32(ssid[1:16]) [pokeldn/ldn/crypto.py:114]. Trap: a mismatch associates, then the console silently drops every datagram.
A station’s broadcasts
A joined console sends broadcast and multicast with no DS bits (CCMP group key, key id 1) and unicast to-DS (pairwise key, key id 0). FireRed opens with an ARP for the host; a host that does not answer it is deauthenticated with reason 3. A standard access point drops these frames; the ESP32 forwards them whole.
Channels
LDN also allows 5 GHz channels 36-48, which the ESP32 cannot reach; FireRed/LeafGreen uses 2.4 GHz only. A board hears a host on neighbouring channels too; joining there is dropped at the host’s next advertisement, so the scan reports the busiest channel:
POKELDN_RADIO=esp32:auto ./.venv/bin/python tools/ldn/ldn_scan.py --keys PROD_KEYS --dwell 2.5