Architecture
I2PChat is a Qt/C++ instant messaging application that operates over the I2P anonymous network using the SAM v3 protocol bridge. It provides encrypted, decentralized chat with file transfer capabilities.
Directory Layout
I2PChat.pro — Qt project file
src/backend/ — Core logic: network, protocol, user management
src/gui/ — Qt Widgets UI layer
docs/ — Local documentation
site/ — GitHub Pages website
www/ — Web profile assets (b32.i2p profiles)
qss/ — Qt stylesheets
sounds/ — Notification sound files
templates/ — HTML templates
packaging/ — Packaging & platform helpers
src/backend — Layer Overview
Session Layer (I2P Rendezvous)
| File | Role |
SessionController | Single SAM session lifecycle — connects to SAM, creates a STREAM session, handles hello handshake, session create, dest generate, naming lookup, and auto-reconnect on disconnect. One per application. |
I2PStream | One per logical connection. Wraps a QTcpSocket to SAM, performs STREAM CONNECT (outgoing) or STREAM ACCEPT (incoming), handles the SAM v3 handshake, reassembles fragmented SAM data, fires signDataReceived. |
Stream Management Layer
| File | Role |
ConnectionManager | Owns the SessionController and all CI2PStream objects. Creates/destroys streams, manages stream ID allocation, dispatches incoming streams to CCore. |
I2PSamMessageAnalyser | Parses SAM v3 protocol text lines into SAM_MESSAGE structs. |
Core / Application Logic
| File | Role |
Core | Central orchestrator. Owns ConnectionManager, UserManager, CProtocol, CFileTransferManager, CSoundManager. Manages the online state machine. |
Protocol | Wire protocol for I2PChat peers. Handles slotInputUnknown (initial CHATSYSTEM handshake) and slotInputKnown (parsed known packets). |
PacketManager | Per-stream reassembly buffer. Slices complete packets by the 4-byte hex length header and emits signAPacketIsComplete. |
User Management
| File | Role |
User | Per-contact model: nickname, I2P destination, protocol version, chat message history, unsent message queue, online state machine. |
UserManager | Persisted user list (users.config), CRUD, sorting. |
UserBlockManager | Block list persistence and lookup. |
UnsentChatMessageStorage | Offline message queue persisted to disk. |
File Transfer
| File | Role |
FileTransferManager | Manages active send/receive transfers, protocol version guard. |
FileTransferSend | Outgoing file transfer — streams file chunks over a dedicated I2P stream. |
FileTransferReceive | Incoming file transfer — reassembles chunks, writes to disk. |
HttpOverStreamObject | HTTP parsing over I2P stream (for web profile). |
Utilities
| File | Role |
Base | Base64/Base32 encoding/decoding. |
ChatmessageChanger | Text transformation pipeline (emoticons, formatting). |
DebugMessageManager | In-memory ring buffer of debug messages. |
SoundManager | Audio notification dispatcher. |
TextEmotionChanger | Smiley/emoticon substitution. |
src/gui — UI Layer
| File | Role |
form_Main | Main window: user list, online status combo, tray icon, context menus. |
form_chatwidget | Chat window per contact: message display (HTML), send field, emoticon/color/font controls. |
form_settingsgui | Settings dialog (7 tabs). |
form_about | About dialog. |
form_DebugMessages | Debug log viewer. |
form_fileReceive | Incoming file transfer dialog. |
form_fileSend | Outgoing file transfer dialog. |
form_newUser | Add-contact dialog. |
form_rename | Rename-contact dialog. |
form_TopicSubscribe | (Disabled) Topic subscription. |
form_UserSearch | (Disabled) User search. |
Data Flow — Connection Establishment
Outgoing (you connect to a known contact)
User clicks "Online"
→ form_Main::onlineComboBoxChanged()
→ CCore::setOnlineStatus(ONLINE)
[if OFFLINE] → mNextOnlineStatus=ONLINE, mCurrentOnlineStatus=TRYTOCONNECT
→ CCore::init()
→ CConnectionManager::doCreateSession(STREAM, privkey, opts)
→ CSessionController::doConnect() // TCP to SAM
→ slotConnected() → HELLO VERSION...
→ slotReadFromSocket()
→ HELLO_REPLAY OK → doSessionCreate()
→ SESSION STATUS OK → signSessionStreamStatusOK(true)
→ CCore::slotStreamControllerStatusOK(true)
→ createStreamObjectsForAllUsers()
→ createStreamObjectForUser(user)
→ CI2PStream::doConnect(dest) // per-contact
→ SAM handshake → STREAM CONNECT
→ STREAM STATUS OK → signStreamStatusReceived(OK)
→ CCore::slotStreamStatusReceived
→ CProtocol::newConnectionChat(ID)
→ send "CHATSYSTEM\t0.6\n" over SAM stream
→ remote side receives FIRSTPACKET, processes slotInputUnknown
→ remote sends back version, client info → slotInputKnown replies
Incoming (a contact connects to you)
CI2PStream (ACCEPT mode) receives data
→ slotReadFromSocket() → STREAM STATUS + DESTINATION
→ signModeAcceptIncomingStream(ID)
→ CConnectionManager::slotModeAcceptIncomingStream()
→ stream moved from StreamIncomingListener → allStreams
→ emit signIncomingStream(stream)
→ CCore::slotIncomingStream()
→ connect stream → signDataReceived → mProtocol → slotInputUnknown
→ remaining buffered data sent as signDataReceived
→ CProtocol::slotInputUnknown()
→ detects "CHATSYSTEM\t" → parses version
→ if ID<0: CProtocol::newConnectionChat(ID) // send our FIRSTPACKET back
→ adds/updates user via UserManager
→ setStreamTypeToKnown() → creates PacketManager
→ subsequent data routed through PacketManager → slotInputKnown
Online State Machine
setOnlineStatus(ONLINE/AWAY/...)
USEROFFLINE —————————————————————————→ USERTRYTOCONNECT
↑ ↓
session OK ↓
↓
USERTRYTOCONNECT ——→ mNextOnlineStatus
↓
session ↓
created ↓
↓
USERONLINE / USERAWAY /
USERWANTTOCHAT / USERINVISIBLE /
USERDONT_DISTURB
↓
←—————— setOnlineStatus(OFFLINE) ←——————————————└————————————←
stopCore()
Note: stopCore() is only called on explicit offline — not on SAM disconnect.
Session loss sets mCurrentOnlineStatus = USEROFFLINE without cleaning up streams.
SAM Protocol Integration
I2PChat communicates with the local I2P router over TCP via the SAM v3 bridge (127.0.0.1:7656). All SAM I/O is managed by SessionController and CI2PStream.
SAM v3 Handshake Flow
I2PChat → SAM: HELLO VERSION MIN=3.1 MAX=3.3
SAM → I2PChat: HELLO REPLY RESULT=OK VERSION=3.3
I2PChat → SAM: SESSION CREATE ID=i2pchat STYLE=STREAM DESTINATION=TRANSIENT
[SIGNATURE_TYPE=EdDSA_SHA512_Ed25519]
SAM → I2PChat: SESSION STATUS RESULT=OK DESTINATION=<base64>
Key Management
- TRANSIENT destinations — server generates an ephemeral keypair; not persisted. Used when the user enables "Transient identity" in settings.
- Persistent (base64 private key) — a previously-generated key is sent back to SAM on reconnect, restoring the same I2P identity. Stored in
~/.i2pchat/.
- Signature types:
EdDSA_SHA512_Ed25519 (EdDSA, default for new profiles) and DSA_SHA1 (legacy).
Socket Stealing (v3)
After SESSION CREATE, the control socket is still a command channel. When STREAM CONNECT or STREAM ACCEPT executes, SAM marks the handler as stolen and the TCP connection becomes a transparent bidirectional pipe to the I2P peer. From I2PChat's perspective, the QTcpSocket is now a raw stream.
Naming Lookup
I2PChat → SAM: NAMING LOOKUP NAME=<b32|hostname>
SAM → I2PChat: NAMING REPLY RESULT=OK NAME=<name> VALUE=<base64>
Used to resolve .b32.i2p addresses and hostnames to full I2P destinations.
Full SAM v3 protocol reference: https://i2p.net/en/docs/api/samv3/
Limits
| Item | Value |
| SAM HELLO timeout | 75 s |
| Stream send size | 1–32768 bytes per message |
| Stale session expiry | 10 min |
| Control socket timeout | 60 s (first cmd), 3 min (subsequent) |
Protocol Packet Format
Offset Size Description
0 4 Packet length in bytes (hex ASCII), excludes these 4 bytes
4 4 Tag (ASCII) — identifies command or message type
8 N Payload (tag-dependent)
| Tag | Meaning |
| 0000–0002 | Response: echo of ping, client version, client name |
| 0003 | Chat message |
| 0004–0008 | Online status updates |
| 0009–0010 | Protocol version, max file transfer protocol |
| 0011–0014 | User info (nick, gender, age, interests) |
| 0015–0016 | Block notifications |
| 0017–0018 | Min file transfer protocol, avatar image |
| 0019 | Avatar changed notification |
| 1000–1008 | Commands: PING, GET_CLIENTVERSION/NAME/USER_ONLINESTATUS etc. |
License
Licensed under GPLv3.