Secure, anonymous peer-to-peer chat over I2P
This page documents the I2PChat wire protocol, connection lifecycle, and key internals for anyone building a compatible client or contributing to the codebase.
Every packet on an established I2P stream follows a fixed-length-header format:
Offset Size Description
0 4 Payload length in bytes (hex ASCII), e.g. "0032" = 50 bytes
This length excludes these 4 bytes.
4 4 Tag (ASCII) — identifies the message or command type
8 N Payload (tag-dependent format)
Packets are reassembled from the raw I2P stream by PacketManager, which accumulates bytes in a buffer and extracts complete packets when enough data arrives (checkIfOnePacketIsComplete). All I/O beyond the initial handshake uses this framing.
When a new I2P stream is established (either outbound via STREAM CONNECT or inbound via STREAM ACCEPT), the peer that initiated the connection sends the first application-level message:
CHATSYSTEM\t0.7\tCallerNickname\n
The receiver parses this in CProtocol::slotInputUnknown:
CHATSYSTEM\t — normal chat protocolCHATSYSTEMFILETRANSFER\t — file transfer protocolAfter the handshake, all traffic switches to the framed binary packet format (4-byte hex length + 4-byte tag + payload).
Tags are 4-character ASCII strings. Two namespaces exist:
| Tag | Enum | Direction | Description |
|---|---|---|---|
| 1000 | PING | bidirectional | Keepalive ping |
| 1001 | GET_PROTOCOLVERSION | request | Query peer's protocol version |
| 1002 | GET_MAX_PROTOCOLVERSION_FILETRANSFER | request | Query max supported file transfer version |
| 1003 | GET_MIN_PROTOCOLVERSION_FILETRANSFER | request | Query min supported file transfer version |
| 1004 | GET_CLIENTVERSION | request | Query peer's client version string |
| 1005 | GET_CLIENTNAME | request | Query peer's client name |
| 1006 | GET_USER_ONLINESTATUS | request | Query peer's online status |
| 1007 | GET_USER_INFOS | request | Query peer's profile info |
| 1008 | GET_AVATARIMAGE | request | Query peer's avatar image |
| Tag | Enum | Description |
|---|---|---|
| 0000 | ECHO_OF_PING | Ping reply |
| 0001 | ANSWER_OF_GET_PROTOCOLVERSION | Reply: protocol version string |
| 0002 | ANSWER_OF_GET_CLIENTVERSION | Reply: client version string |
| 0003 | CHATMESSAGE | UTF-8 chat message payload |
| 0004 | ANSWER_OF_GET_CLIENTNAME | Reply: client name |
| 0005 | USER_ONLINESTATUS_ONLINE | Status update: online |
| 0006 | USER_ONLINESTATUS_OFFLINE | Status update: offline |
| 0007 | USER_ONLINESTATUS_AWAY | Status update: away |
| 0008 | USER_ONLINESTATUS_INVISIBLE | Status update: invisible |
| 0009 | ANSWER_OF_GET_MAX_PROTOCOLVERSION_FILETRANSFER | Reply: max file transfer protocol version |
| 0010 | ANSWER_OF_GET_MIN_PROTOCOLVERSION_FILETRANSFER | Reply: min file transfer protocol version |
| 0011 | USER_INFO_NICKNAME | Profile: nickname |
| 0012 | USER_INFO_GENDER | Profile: gender |
| 0013 | USER_INFO_AGE | Profile: age |
| 0014 | USER_INFO_INTERESTS | Profile: interests |
| 0015 | USER_BLOCK_INVISIBLE | Blocked (invisible to peer) |
| 0016 | USER_BLOCK_NORMAL | Blocked (visible but blocked) |
| 0017 | ANSWER_OF_GET_AVATARIMAGE_IMAGE | Reply: avatar image data |
| 0018 | AVATARIMAGE_CHANGED | Notification: avatar changed |
| 0019 | FILE_OFFER | File transfer offer |
| 0020 | FILE_OFFER_ACCEPTED | File transfer offer accepted |
| 0021 | FILE_OFFER_REJECTED | File transfer offer rejected |
| 0022 | USER_ONLINESTATUS_WANTTOCHAT | Status update: want to chat |
| 0023 | USER_ONLINESTATUS_DONT_DISTURB | Status update: do not disturb |
Tags are dispatched in CProtocol::slotInputKnown by comparing the 4-char tag string.
CHATMESSAGE (0003): Payload is UTF-8 encoded chat text. No additional framing or metadata — the tag alone identifies it as a chat message.
Profile fields (0011–0014): Payload is a UTF-8 string containing the field value (nickname, gender, age string, interests text).
Status updates (0005–0008, 0022, 0023): Payload is empty — the tag itself encodes the status. The receiver sets the peer's online status based solely on which tag arrived.
Block notifications (0015, 0016): Payload is empty. Tag 0015 means the peer has blocked you invisibly; 0016 means you're blocked but visible.
Avatar (0017, 0018): Tag 0017 payload is the full image byte data. Tag 0018 is a notification with no payload — the receiver should fetch the new avatar with GET_AVATARIMAGE.
File transfer (0019–0021): Payload is a JSON or structured string identifying the file offer, acceptance, or rejection (handled by CFileTransferManager).
Command replies (0001, 0002, 0004, 0009, 0010): Payload is a UTF-8 string containing the requested value (version number, client name, etc.).
File transfers use a separate I2P stream (not the primary chat stream). The flow:
0019 (FILE_OFFER) on the chat streamCHATSYSTEMFILETRANSFER\t0.3\n<size_in_bytes>\n<filename>File transfer protocol version is independently negotiated (currently 0.3). Both peers must agree on a common version range before transfer begins.
User selects "Online"
→ SAM session created (HELLO + SESSION CREATE)
→ For each known contact: CI2PStream::doConnect(dest)
→ STREAM CONNECT (SAM steals the socket)
→ Send CHATSYSTEM\t0.7\tNickname\n
→ Receiver parses handshake, sends back profile info
→ PacketManager framing takes over
CI2PStream in accept mode receives data → STREAM STATUS (SAM notification of inbound stream) → Read first line → parse CHATSYSTEM header → If unknown contact: authorization request dialog → If known: accept, reply with profile, enter framed mode
When the SAM connection drops, SessionController auto-reconnects after a 20-second delay. All streams are recreated. Unsent messages stored in UnsetMessages.dat are delivered on reconnection. The protocol state for each user (protocol version, capabilities) is re-queried.
Explicit setOnlineStatus(OFFLINE) calls stopCore() and tears down the SAM session cleanly. SAM disconnect without explicit offline keeps the user list intact — streams are recreated on reconnection.
Each CUser tracks its peer's online status. I2PChat sends unsolicited status updates whenever the local user changes status, and responds to GET_USER_ONLINESTATUS (1006) queries. The peer's status is stored as an enum and displayed via the roster UI.
States: OFFLINE, ONLINE, AWAY, INVISIBLE, WANTTOCHAT, DONT_DISTURB. Each has a dedicated tag (0005–0008, 0022, 0023).
To implement a client interoperable with I2PChat:
HELLO VERSION MIN=3.1 MAX=3.3 + SESSION CREATE ID=... STYLE=STREAM DESTINATION=TRANSIENT)NAMING LOOKUP NAME=<b32>STREAM CONNECT ID=... DESTINATION=<b64> (socket stealing)CHATSYSTEM\t0.7\tYourNickname\nGET_AVATARIMAGE (1008) — reply comes as ANSWER_OF_GET_AVATARIMAGE_IMAGE (0017) with raw image bytesThe SAM socket-stealing model means each I2P stream occupies its own TCP connection to SAM. After STREAM CONNECT or STREAM ACCEPT, the TCP socket becomes a raw byte pipe to the I2P peer — your framed packets flow directly.
Licensed under GPLv3.