The SAMv3 (I2P router API) library in C++ to build peer-to-peer connections between users in the I2P network

John Doe c50eea694d dead declaration removed 1 month ago
codec 87f5a12b59 first commit 1 month ago
README.md c32097ee28 Readme typo 1 month ago
byteorder.cpp 87f5a12b59 first commit 1 month ago
byteorder.h c50eea694d dead declaration removed 1 month ago
compat.h 87f5a12b59 first commit 1 month ago
i2psam.cpp 2edba36b24 remove legacy defines 1 month ago
i2psam.h 2edba36b24 remove legacy defines 1 month ago
identity.cpp 22a3c010aa commented code removed 1 month ago
identity.h 87f5a12b59 first commit 1 month ago
samserver.cpp 3e670cff5f automatic connection handling 1 month ago
samserver.h 3e670cff5f automatic connection handling 1 month ago
sha256.cpp 87f5a12b59 first commit 1 month ago
sha256.h 87f5a12b59 first commit 1 month ago

README.md

Samty

The SAMv3 (I2P router API) library in C++ to build peer-to-peer connections between users in da I2P network. TCP streams support only.

Written just for fun by acetone in 2024 without guarantees of stable performance. But it's working well.

Features

  • Automatic connection handling;
  • Configuring the length of tunnels;
  • Configuring the quantity of tunnels;
  • Configuring the length variance of tunnels (сhanging the length of a random tunnel);
  • Supports encrypted leaseset to keep your server point private;
  • Disabling leaseset publishing (for client sessions);
  • Calculation of intra-network address *.b32.i2p;
  • Ability to save generated keys and reuse them to save a static address.

Quick start

To create a session, Samty::Configuration is used:

struct Configuration
{
    std::string nickname = "Samty";
    std::string SAMHost = "127.0.0.1";
    uint16_t SAMPort = 7656;
    uint8_t inboundLength = 3;      // Possible correct values: 0-8
    uint8_t inboundQuantity = 3;    // Possible correct values: 1-16
    uint8_t inboundVariance = 0;    // Possible correct values: 0-3
    uint8_t outboundLength = 3;     // Possible correct values: 0-8
    uint8_t outboundQuantity = 3;   // Possible correct values: 1-16
    uint8_t outboundVariance = 0;   // Possible correct values: 0-3

    // https://geti2p.net/spec/encryptedleaseset
    bool encryptedLeaseSet = false;

    // Cannot initiate a connection from the outside if false
    bool publishLeaseSet = true;    

    // Using known keys will allow the static address to be used
    std::string destination = SAM_DEFAULT_DESTINATION; 
};

Intuitive session management. Nickname is used for nice display in the I2P router control panel.

1. Simple access to hidden web server

#include "samty/i2psam.h"
#include "samty/identity.h"

// ...

Samty::Configuration cfg;
cfg.publishLeaseSet = false;

cfg.publishLeaseSet installed to false because it is a client session that does not expect external connections.

Samty::StreamSession s(cfg);
const auto ident = s.getMyDestination();
if (!ident.isOk())
{
    // "Failed to connect to SAM" or something like this
    std::cerr << s.errorString() << std::endl;
    return 1;
}

std::cout << "My address: " << ident.dest32() << std::endl;

auto lookupResult = s.namingLookup("acetone.i2p");
if (!lookupResult.isOk)
{
    std::cerr << "Lookup failed" << std::endl;
    return 1;
}

Naming lookup needed for short *.i2p names. Full b32 destinations returned without any operations.

auto connResult = s.connect(lookupResult.value, false);
if (!connResult.isOk)
{
    std::cerr << "Connection failed" << std::endl;
    return 1;
}

auto conn = connResult.value.get();

conn in this context is a socket wrapper that is ready to read and write!

conn->write("GET / HTTP/1.0\r\nAccept: */*\r\nConnection: close\r\nHost: zhopa\r\n\r\n");

std::string reply = conn->readString(); 
// or conn->readBinary(), return std::vector<uint8_t>

while (!reply.empty())
{
    std::cout << reply;
    reply = conn->readString();
}

conn->close();

2. Session with an encrypted leaseset

Encrypted leaseset prevents a passive observer who holds a floodfill* from establishing a connection to your session. This requires knowing a special address that is not published.

*floodfill is a bulletin board where server addresses are posted for public retrieval.

Samty::Configuration cfg;
cfg.encryptedLeaseSet = true;

// Creating a session as in the example above

std::cout << "My special address: " << ident.dest33() << std::endl;

The special address is called "b33", but is not different from a normal address by eye. It must be communicated to the other party so that it can connect.

3. Address saving

By default, a random key is generated. For server needs, it is often necessary to have a permanent address so that clients know who to connect to. This is accomplished by using single keys.

Samty allows you to save the generated key and then load it, obtaining the former *.b32.i2p address.

How to get a full set of keys to save:

Samty::StreamSession s(cfg);
const auto ident = s.getMyDestination();
if (!ident.isOk())
{
    std::cerr << s.errorString() << std::endl;
    return 1;
}

const std::string myIdent = ident.fullBase64();

And how reuse it:

const std::string myIdent = readFromFileBlahBlahBlah();

Samty::Configuration cfg;
cfg.destination = myIdent;

You can save the key in any way you like. The key can be saved in text form, or in binary form (the standard way of storing keys in *.dat files).

You can use library functions to encode to base64 or decode to binary format to save to a file (I2P uses a non-standard base64 with two characters replaced):

#include "samty/codec/base64_i2p.hpp"

// ...

const std::vector<uint8_t> binary = cppcodec_samty::base64_i2p::decode(base64)
const std::string base64 = cppcodec_samty::base64_i2p::encode(binary);

4. Low level accept incoming connection

There are two ways to accept connections: forward on incoming requests, or process directly.

Forwarding

Samty::StreamSession s(cfg);
const auto ident = s.getMyDestination();
if (!ident.isOk())
{
    std::cerr << s.errorString() << std::endl;
    return 1;
}

const auto forwardResult = s.forward("127.0.0.1", 8080, true);
if (!forwardResult.isOk)
{
    std::cerr << "Forwarding failed" << std::endl;
    return 1;
}

std::cout << "Connect to " << ident.dest32() << std::endl;

while (true)
{
    sleep(1);
}

//

s.stopForwardingAll();

true in parameters of call s.forward("127.0.0.1", 8080, true) means silent mode - I2P service commands will not be written to the socket, because the external web server will not understand them and will not be able to process them.

Directly

Samty::StreamSession s(cfg);
const auto ident = s.getMyDestination();
if (!ident.isOk())
{
    std::cerr << s.errorString() << std::endl;
    return 1;
}

const auto acceptResult = s.accept(false);
if (!acceptResult.isOk)
{
    std::cerr << "Accept failed" << std::endl;
    return 1;
}

std::cout << "Connect to " << ident.dest32() << std::endl;

const auto conn = acceptResult.value.get();

while (conn->isOk())
{
    std::cout << conn->readString() << std::endl;
}

If silent mode disabled (s.accept(false)), when a new connection is made, the second party certificate (public keys that form the b32 address) is written to the socket, ends with \n. This allows the second party's *.b32.i2p address to be determined.

The library allows you to wait for an incoming connection to get the address of the incoming node (use only with silence mode turned off!).

const auto conn = acceptResult.value.get();

std::string b32 = conn->readDestination();
if (b32.empty())
{
    std::cerr << "Socket failed" << std::endl;
    return 1;
}
std::cout << "New connection from " << b32 << std::endl;

A direct connection blocks the receiving socket. After a connection has occurred, a new stream (i.e. receiving socket) must be opened for the next connections. Sockets are disposable - once a connection is closed, it cannot be used again.

5. Automatic connection handling

The library includes the Samty::SAMServer class, which provides callback and new accept() after each new connection.

Full example:

#include "samty/samserver.h"

void callback(const std::string& b32, std::unique_ptr<Samty::I2pSocket> socket)
{
    uint8_t byte;
    std::vector<uint8_t> buffer;
    int lineCounter = 0;

    while (socket->isOk())
    {
        if (!socket->readBytes(&byte, 1, 5 /*timeout in seconds*/))
        {
            std::cerr << socket->errorString() << std::endl;
            socket->close();
            break;
        }

        if (byte == '\n')
        {
            ++lineCounter;
        }

        buffer.push_back(byte);
    }

    std::cout << buffer.size() << " bytes (" << std::to_string(lineCounter) << " lines) readed from " << b32 << std::endl;
}

int main(int argc, char **argv)
{
    Samty::Configuration cfg;

    Samty::SAMServer server(cfg, callback);
    std::cout << "Server address: " << server.myAddress() << std::endl;
    std::cout << "Server started: " << server.start() << std::endl;

    while (true)
    {
        sleep(1);
    }
}

Possible output:

Server address: e3a62hwjvea7c3edzzuozi4vjbjiqobku263tnhkomxt77l3fdjq.b32.i2p
Server started: 1
Read timed out
216 bytes (8 lines) readed from gy2wxtfji4t65fo2n4x3uqbr6evg34k36xdlxjy3xctv7waethhq.b32.i2p

License

I don't know anything about copyright and I just don't give a fuck. Copy, use, break or fix mistakes. I don't care. I guess it's called the public domain.