How to Build an I2P App with SAM: Easier Than You Think

SAM: An App Working Through I2P Is Easier Than It Seems

Invisible Internet Project (I2P) is a leader among technologies for private information transfer. Its complete decentralization and independence make the I2P network architecturally complex, but unique in its class. This article addresses the question: can a programmer with little knowledge of cryptography and networking write an app that works through I2P?

Modern I2P router implementations support the SAM (Simple Anonymous Messaging) API protocol, which allows external applications to communicate over I2P using just a few simple commands. In this article, we’ll cover the bare minimum you need to start your own experiments.

A Brief Retrospective

I2P’s history began in the early 2000s. Good software development practices were not widely spread in the era of VHS tapes. At that time, it was hard to imagine modern concepts for interacting with external applications through a convenient interface (API).

Initially, the network’s software client only provided a proxy to the outside world. A proxy, simply put, is an intermediary that receives a request (like “example.i2p”) and processes it. For the external application, all internal logic is completely hidden, and the process feels like using a regular network.

To the uninitiated, this might not seem like a problem—“there’s a proxy, do whatever you want!” And that’s partly true: client-server apps will work as usual—websites will open, IRC chats too. But what about peer-to-peer technologies, or dynamically creating new user identifiers so all apps aren’t surfing the network as the same user?

The I2P router has the I2CP (I2P Control Protocol), designed to logically separate the anonymous network router from external applications. However, things weren’t as rosy as the developers hoped: I2CP is a very bulky and complex protocol. For an external app to do anything meaningful, it would need to include more than half the router’s code. Imagine needing to speak conversational Chinese just to shop online!

This integration complexity explains why the original Java router is overloaded with extra apps like BitTorrent and Email, turning the I2P router from a small utility into a monolithic mammoth.

The current SAM API protocol was preceded by BOB (Basic Open Bridge). When I2P globally switched to stronger cryptography (moving away from SHA1), the flagship Java router’s developers decided to leave BOB behind and not teach it the new standards. Maintaining two similar protocols with limited resources just wasn’t practical.

Note: In the C++ router implementation (i2pd), BOB is still a current protocol alongside SAM. The PurpleI2P team keeps it updated as needed. If you’re interested, you can check it out.

Getting Started

There are several libraries for different programming languages that provide ready-made functions and methods for interacting with the I2P router via SAM. You can easily find the official list on the documentation page. If you write your own library, consider adding it to the list.

Let’s look at the basics of working with SAM using the terminal as a general example. We’ll use the netcat utility and the i2pd router (the Java router works similarly). The old telnet utility won’t work for connecting to SAM because it uses two end-of-line characters (\r\n), while SAM expects just \n. The \r character isn’t part of the protocol and will cause an error, stopping the process.

In i2pd, SAM is usually enabled by default. To check, open the web interface (http://127.0.0.1:7070), where the main page shows the status of services. If SAM is inactive, enable it in the config file with sam.enabled = true. By default, it’s available at 127.0.0.1:7656.

Hello Hidden World

When connecting to the socket that the SAM service listens on, you need to initiate a handshake by sending HELLO VERSION. The router will reply with its SAM protocol version. If you see RESULT=OK, the handshake was successful. You can also specify a version requirement: HELLO VERSION MIN=3.0 MAX=3.3. This is useful if you need features that older routers don’t support. In practice, version 3.0 is enough for most tasks.

When creating a session, you must specify a nickname (ID) for later use, as well as keys that will serve as the cryptographic identifier for the new hidden destination. The main requirement for the nickname is no spaces. As for the keys, if you specify TRANSIENT, random keys will be generated and returned as a single block of base64-encoded data containing both public and private keys. Since the sizes of the public and private key parts are known in advance, it’s easy to decode and separate them programmatically (for example, to share the public key with someone else). For clarity and easier use of SAM via the terminal, let’s generate keys with a separate command before creating the session.

The DEST GENERATE command can be used as is, or with an explicit signature type. By default, SIGNATURE_TYPE=0 (deprecated); now it’s recommended to use 7, but for simplicity, we’ll stick with the default. You don’t need to know about signature types for basic I2P use.

After receiving the command, the router returns the keys, clearly separating the public (PUB) and private (PRIV) parts. This is convenient for simple keyword-based parsing.

Now, let’s create a session. SAM supports three session types: STREAM (for TCP), DATAGRAM (for sending and receiving UDP packets), and RAW (a UDP variant). The most basic and illustrative is TCP, used for file and message transfers, so we’ll create a STREAM session as an example.

SESSION CREATE STYLE=STREAM ID=HabraHabr DESTINATION=PRIVATE_KEY_BLOCK

The STYLE parameter sets the session type (here, STREAM), DESTINATION takes the private keys (the PRIV block from the generated keys), and ID is the session nickname (in this example: HabraHabr).

After a few seconds, the router will confirm the session was created. The session will live as long as the socket that created it exists. You must interact with the session through new connections to SAM. This means the control socket isn’t used directly, but must remain open as long as you need the session.

In a new terminal window, connect to SAM again and perform the standard handshake. There are two ways to use the existing “HabraHabr” session: initiate a connection to another hidden node as this session (if you know its address), or connect to a local stream to wait for incoming connections. To receive messages sent from a second session, connect to the stream with:

STREAM ACCEPT ID=HabraHabr

That’s it for the first session. Open another terminal window, connect to SAM, and create a new session “Novosibirsk,” this time specifying TRANSIENT for the DESTINATION parameter instead of generating keys separately. The process is otherwise the same.

To connect to the previous “HabraHabr” session, open yet another terminal window, connect to SAM, and enter:

STREAM CONNECT ID=Novosibirsk DESTINATION=PUBLIC_KEYS_OF_HabraHabr_SESSION

When a message comes in, the socket receives a block of public keys from the connected user, which serves as their internal network identifier. In this session, you don’t need this info, but it can be useful if you want to initiate a connection to that user later.

This is how two endpoints connect. Even though they’re on the same I2P router, the connection actually passes through several computers worldwide, with each direction using a separate chain of transit nodes. Sometimes you’ll notice this when messages sent from one terminal window arrive quickly, but take longer in the other direction.

What’s shown here as a primitive text chat is, in essence, a direct connection between two users through the hidden network. Such P2P (peer-to-peer) connections can be used for communication over various protocols without a server intermediary, while keeping both parties’ identities confidential.

When one user disconnects, the other will also be disconnected from their session. The other party’s actions don’t affect your local session, but you’ll need to reconnect from a new socket each time to catch a new incoming connection. In other words, each connection is single-use. In the terminal example, this means you need a new terminal window for each connection, each connected to SAM in listening mode.

This is especially important in practice: you must create a new connection to the local session every time the existing one accepts an incoming connection. You can also have multiple listening sockets at once, which prevents connection refusals for users you didn’t have time to set up a new listener for.

Summary

This article demonstrates the basic principles your app can use directly, or via library wrappers for these commands. If there’s enough interest, we’ll cover SAM in more detail in the future.

Leave a Reply