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 | 1 month ago | |
README.md | 1 month ago | |
byteorder.cpp | 1 month ago | |
byteorder.h | 1 month ago | |
compat.h | 1 month ago | |
i2psam.cpp | 1 month ago | |
i2psam.h | 1 month ago | |
identity.cpp | 1 month ago | |
identity.h | 1 month ago | |
samserver.cpp | 1 month ago | |
samserver.h | 1 month ago | |
sha256.cpp | 1 month ago | |
sha256.h | 1 month ago |
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.
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.
#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();
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.
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);
There are two ways to accept connections: forward on incoming requests, or process directly.
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.
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.
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
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.