Subsystems in GNUnet and using them in GNUnet++

I'm back working on GNUnet++ after months of not knowing what to do with it! Now I've a fun project that I'm working on. In the mean time I have fixed and added a lot of things to GNUnet++. I think it's a good time to write a blog post about what each GNUnet subsystem does and how to use it in GNUnet++. Fire-hose style.

This post also assumes you are going to use the coroutine API mainly. Since 1) it's what I use and 2) callbacks are not fun to layer on top each other. Also I'll be explaining the more commonly used parts. There's a lot of stuff GNUnet offers that I don't know about or don't use.

Before we start. I'm planing on release the first version of GNUnet++ soon. Tidying up the code and writing tests. Should be there in a month or so.

Starting GNUnet++

GNUnet++ is written in C++20 with full coroutine support. You need GCC 12 or Clang 14 to compile it. The entry point is gnunetpp::start. It calls a coroutine where you can do your GNUnet stuff. It's a blocking call. It will return when the GNUnet service shuts down. You can use gnunetpp::shutdown to shut down the service.

cppcoro::task<> service(const GNUNET_CONFIGURATION_Handle* cfg)
    // Your GNUnet code goes here

int main()


Technically the scheduler is not a subsystem in GNUnet. But it's in it's own namespace used almost in every application. So GNUnet++ follows this conversion. There's 3 functions of interest. scheduler::queue, scheduler::runEvery and scheduler::runLater. queue queues a task to be run by the GNUnet main thread after all current tasks finish. runLater delays the function to run for a certin mount of time. Mostly used to act as a timeout timer. Finally runEvery runs every certain amount of time.

void foo1() {
    std::cout << "1" << std::endl;
    scheduler::queue([]{ std::cout << "2" << std::endl; });
    std::cout << "3" << std::endl;
// The function within the lambda will be called after foo1 returns. The output will be:
// 1
// 3
// 2

void foo2() {
    std::cout << "1" << std::endl;
    scheduler::runLater(std::chrono::seconds(1), []{ std::cout << "2" << std::endl; });
    std::cout << "3" << std::endl;
// The function within the lambda will be called after 1 second. The output will be:
// 1
// 3
// (Wait 1 second)
// 2

void foo3() {
    std::cout << "1" << std::endl;
    scheduler::runEvery(std::chrono::seconds(1), []{ std::cout << "2" << std::endl; });
    std::cout << "3" << std::endl;
// The function within the lambda will be called every 1 second. The output will be:
// 1
// 3
// (Wait 1 second)
// 2
// (Wait 1 second)
// 2

Note that each function also returns a TaskID that you can use to cancel the task via scheduler::cancel.

auto id = scheduler::runLater(std::chrono::seconds(1), []{ std::cout << "2" << std::endl; });
// The function within the lambda will not be called.

There's 2 coroutine that is very useful in GNUnet++. scheduler::sleep and scheduler::waitUntilShutdown. sleep is the same as runLater but it's a coroutine. waitUntilShutdown is a coroutine that will suspend until GNUnet shuts down. Useful to block service function until GNUnet shuts down.

task<> foo() {
    std::cout << "1" << std::endl;
    co_await scheduler::sleep(std::chrono::seconds(1));
    std::cout << "2" << std::endl;

task<> service(const GNUNET_CONFIGURATION_Handle* cfg)
    std::cout << "GNUnet is starting" << std::endl;
    co_await scheduler::waitUntilShutdown();
    std::cout << "GNUnet is shutting down" << std::endl;


The Randomized Recursive Routing for Restricted-Route Networks Distributed Hash Table (R5N) is the DHT used in GNUnet. It's not compatible with the BitTorrent DHT (called mainline DHT) nor it is compatible with the OpenDHT also from a GNU project. R5N does stuff that's not possible with the other DHTs. For example. Most DHT implementations assumes all nodes are connected on a flat address space and can freely connect to each other. R5N does not. In fact R5N can run on a mesh network where only peer to peer communication is possible.

Like any DHT, the R5N provides the same put and get functionality.

// Create a DHT service with a hash table size of 1 since we only do 1 operation at a time
dht = std::make_shared<gnunetpp::DHT>(cfg, 1);

// Put a value
co_await dht->put("key", "value");

Retrieving values is a bit more complicated. Since the DHT is a distributed system and people can put different values under the same key. The DHT will return a list of values. The API allows you to iterate over the list of values asynchronously. GNUnet++ uses the almost standardized for co_await syntax in it's expanded form. The get function returns a Lookup object that you can await on to get an iterator. Then increment the iterator to get the next value.

auto lookup = dht->get("key");
for(auto it = co_await lookup.begin(); it != lookup.end(); co_await ++it) {
    std::cout << *it << std::endl;

Advanced options

Some advanced options are helpful to know when using the DHT. First the put function allows you to specify how long the value should be stored in the DHT. The default is 1 hour. However it's merely a hint not an obligation. The DHT can drop your value if you spam a lot of requests or just does not have the resource to save new long term values.

co_await dht->put("key", "value", std::chrono::days(5));

Likewise, there's a timeout for the get function. The default is 10 seconds. Since a DHT search can take a long time. You want to strike a balance between potentially waiting for a long time and not getting enough results. Note that the lifetime of the lookup also depends on the lookup object. Search also ends if the lookup object is destroyed.

// this lookup will run for 20 minutes
auto lookup = dht->get(key, std::chrono::seconds(1200));

// this lookup start and then immediately end
    auto lookup = dht->get(key, std::chrono::seconds(1));
    // lookup is destroyed here, so the search is canceled

File Share

FS is the original motivation in creating GNUnet. FS allows one to upload and download files to the GNUnet network. It's easier to understand what FS does through the CLI first. FS enables people to publish, search and download files on the GNUnet network.

❯ gnunet-search mp3


gnunet-download -o "metallica - Last Caress Green Hell.mp3" gnunet://fs/chk/WA7BANQVKCK7MVJCS72PFA93WW3WX9XWMRGA1A24T47AW93M14GFC7G47HTD6GJRY60DBJP879VMAAPA694BW97K0G8Q4JT7PJNE47G.NGDVRN1YXG53WJT3N8PN8ZD1SFHQ8Y3B7DV5TWVEKHQKSN5A2F1BEY3CFXRFK6489QQNF8VRRJ64BJSM51TX2FJZ7F01TD29TVRV9SG.3413950




With the URLs, you can download the files using gnunet-download.

100% [============================================================]
Downloading `Exodus - cajun hell.mp3' done (203 b/s).

The same action can be done in C++ using GNUnet++.

FS::search(cfg, keywords, [n=0](const std::string_view uri
    , const std::string_view original_file_name) mutable -> bool {
    std::cout << "#" << ++n << ": " << original_file_name << " uri: " << uri << "\n" << std::endl;

    // return true to indicate keep on searching
    return true;
}, 10s /* at most searching for 10s */);

Which prints






Now the FS::download function can be used to download the files. Note that FS is the only subsystem not fully supporting coroutines as there's a chance that the file takes forever to download. In that case, the coroutine will be suspended forever and no clear way to kill it. Thus only the callback version is provided.

    [](FS::DownloadStatus status, const std::string& message, size_t downloaded, size_t total) {
    if(status == gnunetpp::FS::DownloadStatus::Completed) {
        std::cout << "\nFinish downloading." << std::endl;

Now we need a way to publish files to the network. This is done by the publish function. Say you want to publish a file test.txt on the network. You can do it like the following. URI is the normal URI used for download. The namespace_uri is usually empty unless it's an updatible file. Which we will touch on later.

auto [uri, namespace_uri] = co_await FS::publish(cfg, "test.txt");

Now your file is published on the network. If you wish to remove it, call the unindex function. This will remove that file from your local database. It however, will not remove the file from the network. The network only ever forgets a file if all nodes unindex it or the file is expired.

co_await FS::unindex(cfg, "test.txt");

Advanced publishing options

To help other users finding your file via the search function, you can add keywords to your file by appending a vector of strings to the publish function. This way when other user's search terms match your keywords, your file will be returned as a result. Files on FS also have an expiration date. This is 1 year by default. You can change it by passing desired duration. This is a hint to GNUnet. You file may be removed from the network earlier due to all nodes being full. Or later because some node holding your file has excessive capacity.

auto [uri, namespace_uri] = co_await FS::publish(cfg, "test.txt", {"test", "hello", "world"}, std::chrono::years(10));

GNUnet also supports updatible files. To do so, you must publish the file under an Ego (will be touched on in a later section) and provide the expected "next name" for GNUnet to validate that you have the right to update. Also, updating a file does not make the old one invalid. It just allows the publisher to point to the new version.

auto me = getEgo(cfg, "me");
auto [uri, namespace_uri] = co_await FS::publish(cfg, "my_music.flac", {}, std::chrono::years(10), me, "my music version 1", "my music version 2");

auto [uri, namespace_uri] = co_await FS::publish(cfg, "my_music_v2.flac", {}, std::chrono::years(10), me, "my music version 2", "my music version 3");

Once you decided that's the final update. Publish the final version with an empty next name to disable further updates.


This is one of the core components of GNUnet. It's a transport layer that is used to send messages between nodes. It works even if the nodes are visible to each other over the internet. As long as one now on GNUnet can contact both indirectly. CADET will take care of the rest. It's a very powerful system. For detailed information about the APIs in GNUnet++, please refer to my previous blog post.

// Create a CADET server
auto cadet = std::make_shared<gnunetpp::CADET>(cfg);

// CADET "port" could be a string or a 512 bit hash. We also specify the message types we want to receive
// All other messages sent to this port will be dropped
cadet->openPort("default", {GNUNET_MESSAGE_TYPE_CADET_CLI});

cadet->setConnectedCallback([](const CADETChannelPtr& channel) {
    std::cout << "* New connection from " << crypto::to_string(channel->peer()) << std::endl;
cadet->setReceiveCallback([](const CADETChannelPtr& channel, const std::string_view data, uint16_t type) {
    std::cout << "Client says " << data << std::flush;
    channel->send("Hello from the server\n", GNUNET_MESSAGE_TYPE_CADET_CLI);

Running this will give you a server that accepts connections and send a message back to the client. One can verify this by running the gnunet-cadet command line tool. Then manually type some message for the server to receive.

❯ # First, we need to know our peer identity
❯ gnunet-peerinfo -s
❯ # Manually type "hello!" and press enter
❯ gnunet-cadet -s default
Hello from the server

Then the server side shows

Client says hello!

Client follows basically the same API

// Create a CADET client
auto cadet = std::make_shared<gnunetpp::CADET>(cfg);
auto host = crypto::peerIdentity("CCXHBE49GRQVFAVFQ3BXVXPHHD63NG6OYN31R0F59KVDZQA42PAX");
auto channel = cadet->connect(host, "default", {GNUNET_MESSAGE_TYPE_CADET_CLI});
channel->setReceiveCallback([](const std::string_view data, uint16_t type) {
    std::cout << "Server says " << data << std::flush;
channel->send("hello!\n", GNUNET_MESSAGE_TYPE_CADET_CLI);

Connecting to the server will show the following

# Client side
Server says Hello from the server

# Server side
Client says hello!

Noteworthy differences againt TCP/UDP

CADET is a bit different to TCP and UDP. It's like a combination of both. First, you notice that there's no connection event for the client. Simply call connect() and done. This is intentional. CADET although have the concept of connections and can be a reliable transport. It does not perform the TCP 3 way handshake to validate connectivity. It fires a packet at the peer and forgets about it at all! You don't know if the packet arrives or heck even if the host exists. The only thing you can do is to set up a timer and handle timeout if the peer does not reply. This is a very important concept to understand. CADET is not TCP.

Another critical difference. CADET is a datagram protocol. It is not a stream like TCP! The maximum size of a packet is 0xffff - 4 bytes. Where 4 is the CADET header. The API will not accept a packet larger than this. If you need to send a large message, you need to split it up yourself.

With all that said, CADET is ordered and reliable by default. If you want unordered and unreliable, you need to specify that after connecting or receiving a new connection.

auto channel = cadet->connect(....);

// or
cadet->setConnectedCallback([](const CADETChannelPtr& channel) {

Note that these options can be changed on the fly. So you can start with reliable and ordered, then switch to unreliable and unordered later. I don't know why you would want to do that, but it's possible. Also weather the packets will actually be unordered even if unordered is specified is up to the transport layer and network topology. Most GNUnet nodes are connected to each other via TCP. So packets going through the same path will always be ordered.


GNUnet++ provides a sane C++ layer for the crypto APIs in GNUnet. GNUnet has it's own unique set of conventions. Including to but not limited to the following

  • SHA-512 hashes
  • HMAC-SHA-512 by default
  • Custom base32 encoding
  • ECDSA user identities but EdDSA node identities

The GNUnet++ crypto API loosely follows it's own convention (meaning, it's not consistent in order to follow GNUnet's API). Like crypto::className being the deserialization function and crypto::to_string does serialization. The classNmae function does the computation, which is also overloaded to accept std::string_view and a pointer-size pair. For example.

// The peer identity
auto identity = crypto::peerIdentity(....); // your node's private key
auto identity_str = crypto::to_string(identity); // serialize to string
auto identity2 = crypto::peerIdentity(identity_str); // deserialize from string

auto sk = crypto::anonymousKey(); // the key used to sign anonymous messages
auto sk_str = crypto::to_string(sk); // serialize to string
auto pk = crypto::getPublicKey(sk); // the public key
auto pk_str = crypto::to_string(pk); // serialize to string
auto sig = crypto::sign(sk, "hello"); // sign a message
auto valid = crypto::verify(pk, "hello", sig.value()); // verify a message

// This is SHA-512. Underlying type is GNUNET_HashCode
auto hash = crypto::hash("Hello World");
auto hash_str = crypto::to_string_full(hash); // exception! GNUnet by default outputs truncated hashes
auto hash2 = crypto::hashCode("Hello World");


Fun part. On GNUnet there's 2 sets of identity. The node identity and the user identity. The node identity is simply the EdDSA public key of your node. It also serves as your CADET address. On GNUnet, there's a separate set of identities called Egos. Which are akin to users on a computer (node). Each ego has it's own set of keys and can be created through gnunet-identity -C <ego name>.

auto pk = gnunetpp::crypto::myPeerIdentity(cfg).public_key; 
auto sk = gnunetpp::crypto::myPeerPrivateKey(cfg);

// sign and validate a message
auto sig = crypto::sign(sk, "hello"); // sign a message
auto valid = crypto::verify(pk, "hello", sig.value()); // verify a message

Some example use of Egos

// create a new ego
auto identity = std::make_shared<IdentityService>(cfg);
co_await identity->create("my_ego");

// obtain the ego
// Yeah this is a different API. It's just how GNUnet implemented it
auto ego = co_await getEgo("my_ego").value();

// sign and validate a message
auto sig = crypto::sign(*ego.privateKey(), "hello");
auto valid = crypto::verify(*ego.publicKey(), "hello", sig.value());

// delete the ego
co_await identity->remove("my_ego");

In short. The peer identity is the node's identity. It can be used to prove a message is indeed from a node. The ego is a user identity. It should be used to prove a message is from a user. But not necessarily from a node. Like when you want to sign messages under a different name but all working on the same node.

GNS and Namestore

The GNU Name System is a decentralized and zero trust version of DNS. There's the only shipped TLD is

auto gns = std::make_shared<gnunetpp::GNS>(cfg);
// Change "AAAA" to select different record types
// also "ANY" to get all records
auto result = co_await gns->lookup("", 10s, "AAAA");

for(const auto& [record, type] : result) {
    // record is the IP address, TXT, etc.. record stored in GNS
    // and TYPE is a string of the record type. ex. AAAA, CNAME, TXT, etc..
    std::cout << record << std::endl;
// output: 2a07:6b47:100:464::9357:ffdb

That's not all what GNS can do. GNS allows hosting and sharing of namespaces via Ego's public key (so you can have multiple namespaces/domains on the same node). For example, we can lookup the via's public key.

auto result = co_await gns->lookup("www.000G0047M3HN599H57MPXZK4VB59SWK4M9NRD68E1JQFY3RWAHDMKAPN30", 10s, "A");
std::cout << result[0].first << std::endl;
// output:

GNS is the lookup part of DNS-like system. The namestore is the part that allows you to publish your own records and setup local TLDs (there's no global TLD! Everyone sets up their own list of domains they trust) and subdomains. To create a domain, you need an Ego with the same name. Then pass the private key of the Ego to the namestore to add GNS records.

// retrieve an ego that we'll publish records under
auto id = co_await gnunetpp::getEgo(cfg, "my-ego");

// add a TXT record under the ego. GNS is compatible with standard DNS
// record types. So you can use TXT, A, AAAA, etc.. as well as GNS specific
// record types like "PKEY" to store the public key and domain delegation
co_await namestore->store(*id->privateKey(), "test", "HELLO", "TXT"
    , std::chrono::seconds(1200), false);
//                                 ^^^
// IMPORTANT! By default records are private. Set to true to make it public

Now you can lookup the record via GNS or namestore from the command line or programatically.

❯ gnunet-gns -t TXT
Got `TXT' record: HELLO

❯ gnunet-namestore -D -z my-ego -t TXT -n test
        TXT: HELLO (20 m)     PRIVATE

auto gns = std::make_shared<gnunetpp::GNS>(cfg);
auto result = co_await gns->lookup("", 10s, "TXT");
// result[0].first == "HELLO"

auto records = co_await namestore->lookup(*id->privateKey(), "test");
// records[0].value == "HELLO"
// records[0].type == "TXT"

So far, namestore is really just a flat key-value store. To achieve the same hierarchical structure as DNS, you can store a PKEY record type. This record type allows you to delegate a subdomain to another Ego. Or even Egos living on other nodes. For example, we can delegate to another Ego named bob.

// assuming we have an ego named bob
auto bob = co_await getEgo("bob");

// and store something we can delegate
co_await namestore->store(*bob->privateKey(), "www", "", "A");

// now, store a PKEY record under our main ego. This will delegate the subdomain
co_await namestore->store(*id->privateKey(), "bob", crypto::to_string(bob->publicKey())
    , "PKEY", std::chrono::seconds(1200), false);

We can verify the delegation via the command line. Same thing works in code so I won't show it here.

❯ gnunet-gns -t A
Got `A' record:

Finally, records can be removed using the remove function. Note that remove always removes all records under the given name and always succeeds. If there's no record to remove, the result would be the same as if there were records to remove then we removed them. Hence the behavior.

co_await namestore->remove(*id->privateKey(), "test");
co_await namestore->remove(*id->privateKey(), "bob");


Datastore, unlike the name suggesting, is not a permanent data storage. It's better to think it as a cache for long term data. The GNUnet configuration file contains the maximum size of the datastore. When the datastore is full, the datastore will start to evict old and low priority data.

auto datastore = std::make_shared<DataStore>(cfg);

// put a value into the datastore
co_await datastore->put("key", "value");

// retrieve a value from the datastore
auto value = co_await datastore->getOne("key");

// There could be multiple values for a key
auto lookup = co_await datastore->get("key");
for(auto it = co_await lookup.begin(); it != lookup.end(); co_await ++it) {
    auto str = std::string_view(it->, it->value.size());
    std::cout << str << std::endl;

For real, put your critical long term local data in somewhere safe. Like SQLite. The datastore should only be used for storing or caching data with size depending on user usage. GNUnet uses datastore to store local copies of chunks for the File Sharing service.

And... that's almost everything GNUnet++ supports now. The useful part that is. GNUne++ also supports the Network Size Estimator and PeerInfo service. But I haven't found a use for them yet. In any case, I hope this post helps you get started with GNUnet++. And also be helpful to those intrested in GNUnet. If you have any questions, feel free to ask. Contact info can be found on this website/gemini capsule.

Author's profile. Photo taken in VRChat by my friend Tast+
Martin Chang
Systems software, HPC, GPGPU and AI. I mostly write stupid C++ code. Sometimes does AI research. Chronic VRChat addict

I run TLGS, a major search engine on Gemini. Used by Buran by default.

  • marty1885 \at
  • Matrix:
  • Jami: a72b62ac04a958ca57739247aa1ed4fe0d11d2df