i2pchat

Secure, anonymous peer-to-peer chat over I2P

View the Project on GitHub

Developer Guide

This page documents the I2PChat wire protocol, connection lifecycle, and key internals for anyone building a compatible client or contributing to the codebase.


Wire Protocol — Packet Format

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.

Connection Handshake

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:

After the handshake, all traffic switches to the framed binary packet format (4-byte hex length + 4-byte tag + payload).

Protocol Tags

Tags are 4-character ASCII strings. Two namespaces exist:

Commands (1000–1008)

TagEnumDirectionDescription
1000PINGbidirectionalKeepalive ping
1001GET_PROTOCOLVERSIONrequestQuery peer's protocol version
1002GET_MAX_PROTOCOLVERSION_FILETRANSFERrequestQuery max supported file transfer version
1003GET_MIN_PROTOCOLVERSION_FILETRANSFERrequestQuery min supported file transfer version
1004GET_CLIENTVERSIONrequestQuery peer's client version string
1005GET_CLIENTNAMErequestQuery peer's client name
1006GET_USER_ONLINESTATUSrequestQuery peer's online status
1007GET_USER_INFOSrequestQuery peer's profile info
1008GET_AVATARIMAGErequestQuery peer's avatar image

Messages (0000–0022)

TagEnumDescription
0000ECHO_OF_PINGPing reply
0001ANSWER_OF_GET_PROTOCOLVERSIONReply: protocol version string
0002ANSWER_OF_GET_CLIENTVERSIONReply: client version string
0003CHATMESSAGEUTF-8 chat message payload
0004ANSWER_OF_GET_CLIENTNAMEReply: client name
0005USER_ONLINESTATUS_ONLINEStatus update: online
0006USER_ONLINESTATUS_OFFLINEStatus update: offline
0007USER_ONLINESTATUS_AWAYStatus update: away
0008USER_ONLINESTATUS_INVISIBLEStatus update: invisible
0009ANSWER_OF_GET_MAX_PROTOCOLVERSION_FILETRANSFERReply: max file transfer protocol version
0010ANSWER_OF_GET_MIN_PROTOCOLVERSION_FILETRANSFERReply: min file transfer protocol version
0011USER_INFO_NICKNAMEProfile: nickname
0012USER_INFO_GENDERProfile: gender
0013USER_INFO_AGEProfile: age
0014USER_INFO_INTERESTSProfile: interests
0015USER_BLOCK_INVISIBLEBlocked (invisible to peer)
0016USER_BLOCK_NORMALBlocked (visible but blocked)
0017ANSWER_OF_GET_AVATARIMAGE_IMAGEReply: avatar image data
0018AVATARIMAGE_CHANGEDNotification: avatar changed
0019FILE_OFFERFile transfer offer
0020FILE_OFFER_ACCEPTEDFile transfer offer accepted
0021FILE_OFFER_REJECTEDFile transfer offer rejected
0022USER_ONLINESTATUS_WANTTOCHATStatus update: want to chat
0023USER_ONLINESTATUS_DONT_DISTURBStatus update: do not disturb

Tags are dispatched in CProtocol::slotInputKnown by comparing the 4-char tag string.

Payload Formats

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

File transfers use a separate I2P stream (not the primary chat stream). The flow:

  1. Sender offers a file via tag 0019 (FILE_OFFER) on the chat stream
  2. Receiver accepts (0020) or rejects (0021) on the chat stream
  3. If accepted, the sender opens a new I2P stream to the receiver
  4. The new stream starts with the handshake: CHATSYSTEMFILETRANSFER\t0.3\n<size_in_bytes>\n<filename>
  5. File data follows as raw stream bytes (no packet framing)

File transfer protocol version is independently negotiated (currently 0.3). Both peers must agree on a common version range before transfer begins.

Connection Lifecycle

Outbound (you initiate)

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

Inbound (contact initiates)

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

Offline / Reconnect

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.

Online State Machine

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

Building a Compatible Client

To implement a client interoperable with I2PChat:

  1. Open a SAM v3 STREAM session (HELLO VERSION MIN=3.1 MAX=3.3 + SESSION CREATE ID=... STYLE=STREAM DESTINATION=TRANSIENT)
  2. Resolve contact destinations via NAMING LOOKUP NAME=<b32>
  3. For each outbound connection: STREAM CONNECT ID=... DESTINATION=<b64> (socket stealing)
  4. Send the handshake: CHATSYSTEM\t0.7\tYourNickname\n
  5. Parse incoming handshake to get the peer's name and protocol version
  6. Send frames using the 4-byte hex length + 4-byte tag + payload format
  7. Respond to commands (1000–1008) promptly — especially PING (1000) → ECHO (0000)
  8. Send status updates as unsolicited messages (tags 0005–0008)
  9. For avatar support, fetch via GET_AVATARIMAGE (1008) — reply comes as ANSWER_OF_GET_AVATARIMAGE_IMAGE (0017) with raw image bytes

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


License

Licensed under GPLv3.