GNS — the GNU Name System
The GNU Name System (GNS) is a decentralized database that enables users to securely resolve names to values. Names can be used to identify other users (for example, in social networking), or network services (for example, VPN services running at a peer in GNUnet, or purely IP-based services on the Internet). Users interact with GNS by typing in a hostname that ends in a top-level domain that is configured in the “GNS” section, matches an identity of the user or ends in a Base32-encoded public key.
Videos giving an overview of most of the GNS and the motivations behind it is available here and here. The remainder of this chapter targets developers that are familiar with high level concepts of GNS as presented in these talks.
Todo
Link to videos and GNS talks?
GNS-aware applications should use the GNS resolver to obtain the respective records that are stored under that name in GNS. Each record consists of a type, value, expiration time and flags.
The type specifies the format of the value. Types below 65536 correspond to DNS record types, larger values are used for GNS-specific records. Applications can define new GNS record types by reserving a number and implementing a plugin (which mostly needs to convert the binary value representation to a human-readable text format and vice-versa). The expiration time specifies how long the record is to be valid. The GNS API ensures that applications are only given non-expired values. The flags are typically irrelevant for applications, as GNS uses them internally to control visibility and validity of records.
Records are stored along with a signature. The signature is generated using the private key of the authoritative zone. This allows any GNS resolver to verify the correctness of a name-value mapping.
Internally, GNS uses the NAMECACHE to cache information obtained from other users, the NAMESTORE to store information specific to the local users, and the DHT to exchange data between users. A plugin API is used to enable applications to define new GNS record types.
libgnunetgns libgnunetgns ————
The GNS API itself is extremely simple. Clients first connect to the GNS
service using GNUNET_GNS_connect
. They can then perform lookups
using GNUNET_GNS_lookup
or cancel pending lookups using
GNUNET_GNS_lookup_cancel
. Once finished, clients disconnect using
GNUNET_GNS_disconnect
.
Looking up records
GNUNET_GNS_lookup
takes a number of arguments:
- handle This is simply the GNS connection handle from
GNUNET_GNS_connect
.- name The client needs to specify the name to
be resolved. This can be any valid DNS or GNS hostname.
- zone The client
needs to specify the public key of the GNS zone against which the resolution should be done. Note that a key must be provided, the client should look up plausible values using its configuration, the identity service and by attempting to interpret the TLD as a base32-encoded public key.
- type This is the desired GNS or DNS record type
to look for. While all records for the given name will be returned, this can be important if the client wants to resolve record types that themselves delegate resolution, such as CNAME, PKEY or GNS2DNS. Resolving a record of any of these types will only work if the respective record type is specified in the request, as the GNS resolver will otherwise follow the delegation and return the records from the respective destination, instead of the delegating record.
- only_cached This argument should typically be set to
GNUNET_NO
. Setting it toGNUNET_YES
disables resolution via the overlay network.- shorten_zone_key If GNS encounters new names during resolution,
their respective zones can automatically be learned and added to the "shorten zone". If this is desired, clients must pass the private key of the shorten zone. If NULL is passed, shortening is disabled.
- proc This argument identifies
the function to call with the result. It is given proc_cls, the number of records found (possibly zero) and the array of the records as arguments. proc will only be called once. After proc,> has been called, the lookup must no longer be canceled.
proc_cls The closure for proc.
Accessing the records
The libgnunetgnsrecord
library provides an API to manipulate the GNS
record array that is given to proc. In particular, it offers functions
such as converting record values to human-readable strings (and back).
However, most libgnunetgnsrecord
functions are not interesting to
GNS client applications.
For DNS records, the libgnunetdnsparser
library provides functions
for parsing (and serializing) common types of DNS records.
Creating records
Creating GNS records is typically done by building the respective record
information (possibly with the help of libgnunetgnsrecord
and
libgnunetdnsparser
) and then using the libgnunetnamestore
to
publish the information. The GNS API is not involved in this operation.
Future work
In the future, we want to expand libgnunetgns
to allow applications
to observe shortening operations performed during GNS resolution, for
example so that users can receive visual feedback when this happens.
libgnunetgnsrecord
The libgnunetgnsrecord
library is used to manipulate GNS records (in
plaintext or in their encrypted format). Applications mostly interact
with libgnunetgnsrecord
by using the functions to convert GNS record
values to strings or vice-versa, or to lookup a GNS record type number
by name (or vice-versa). The library also provides various other
functions that are mostly used internally within GNS, such as converting
keys to names, checking for expiration, encrypting GNS records to GNS
blocks, verifying GNS block signatures and decrypting GNS records from
GNS blocks.
We will now discuss the four commonly used functions of the
API. libgnunetgnsrecord
does not perform these operations itself,
but instead uses plugins to perform the operation. GNUnet includes
plugins to support common DNS record types as well as standard GNS
record types.
Value handling
GNUNET_GNSRECORD_value_to_string
can be used to convert the (binary)
representation of a GNS record value to a human readable, 0-terminated
UTF-8 string. NULL is returned if the specified record type is not
supported by any available plugin.
GNUNET_GNSRECORD_string_to_value
can be used to try to convert a
human readable string to the respective (binary) representation of a GNS
record value.
Type handling
GNUNET_GNSRECORD_typename_to_number
can be used to obtain the
numeric value associated with a given typename. For example, given the
typename "A" (for DNS A reocrds), the function will return the number
1. A list of common DNS record types is
here. Note
that not all DNS record types are supported by GNUnet GNSRECORD plugins
at this time.
GNUNET_GNSRECORD_number_to_typename
can be used to obtain the
typename associated with a given numeric value. For example, given the
type number 1, the function will return the typename "A".
GNS plugins
Adding a new GNS record type typically involves writing (or extending) a
GNSRECORD plugin. The plugin needs to implement the
gnunet_gnsrecord_plugin.h
API which provides basic functions that
are needed by GNSRECORD to convert typenames and values of the
respective record type to strings (and back). These gnsrecord plugins
are typically implemented within their respective subsystems. Examples
for such plugins can be found in the GNSRECORD, GNS and CONVERSATION
subsystems.
The libgnunetgnsrecord
library is then used to locate, load and
query the appropriate gnsrecord plugin. Which plugin is appropriate is
determined by the record type (which is just a 32-bit integer). The
libgnunetgnsrecord
library loads all block plugins that are
installed at the local peer and forwards the application request to the
plugins. If the record type is not supported by the plugin, it should
simply return an error code.
The central functions of the block APIs (plugin and main library) are the same four functions for converting between values and strings, and typenames and numbers documented in the previous subsection.
The GNS Client-Service Protocol
The GNS client-service protocol consists of two simple messages, the
LOOKUP
message and the LOOKUP_RESULT
. Each LOOKUP
message
contains a unique 32-bit identifier, which will be included in the
corresponding response. Thus, clients can send many lookup requests in
parallel and receive responses out-of-order. A LOOKUP
request also
includes the public key of the GNS zone, the desired record type and
fields specifying whether shortening is enabled or networking is
disabled. Finally, the LOOKUP
message includes the name to be
resolved.
The response includes the number of records and the records themselves
in the format created by GNUNET_GNSRECORD_records_serialize
. They
can thus be deserialized using GNUNET_GNSRECORD_records_deserialize
.
Hijacking the DNS-Traffic using gnunet-service-dns
This section documents how the gnunet-service-dns (and the
gnunet-helper-dns) intercepts DNS queries from the local system. This is
merely one method for how we can obtain GNS queries. It is also possible
to change resolv.conf
to point to a machine running
gnunet-dns2gns
or to modify libc’s name system switch (NSS)
configuration to include a GNS resolution plugin. The method described
in this chapter is more of a last-ditch catch-all approach.
gnunet-service-dns
enables intercepting DNS traffic using policy
based routing. We MARK every outgoing DNS-packet if it was not sent by
our application. Using a second routing table in the Linux kernel these
marked packets are then routed through our virtual network interface and
can thus be captured unchanged.
Our application then reads the query and decides how to handle it. If
the query can be addressed via GNS, it is passed to
gnunet-service-gns
and resolved internally using GNS. In the future,
a reverse query for an address of the configured virtual network could
be answered with records kept about previous forward queries. Queries
that are not hijacked by some application using the DNS service will be
sent to the original recipient. The answer to the query will always be
sent back through the virtual interface with the original nameserver as
source address.
Network Setup Details
The DNS interceptor adds the following rules to the Linux kernel:
iptables -t mangle -I OUTPUT 1 -p udp --sport $LOCALPORT --dport 53 \
-j ACCEPT iptables -t mangle -I OUTPUT 2 -p udp --dport 53 -j MARK \
--set-mark 3 ip rule add fwmark 3 table2 ip route add default via \
$VIRTUALDNS table2
Todo
FIXME: Rewrite to reflect display which is no longer content by line due to the < 74 characters limit.
Line 1 makes sure that all packets coming from a port our application
opened beforehand ($LOCALPORT
) will be routed normally. Line 2 marks
every other packet to a DNS-Server with mark 3 (chosen arbitrarily). The
third line adds a routing policy based on this mark 3 via the routing
table.
Importing DNS Zones into GNS
This section discusses the challenges and problems faced when writing the Ascension tool. It also takes a look at possible improvements in the future.
Consider the following diagram that shows the workflow of Ascension:
Further the interaction between components of GNUnet are shown in the diagram below:
DNS Conversion .. _Conversions-between-DNS-and-GNS:
Conversions between DNS and GNS
The differences between the two name systems lies in the details and is not always transparent. For instance an SRV record is converted to a BOX record which is unique to GNS.
This is done by converting to a BOX record from an existing SRV record:
# SRV
# _service._proto.name. TTL class SRV priority weight port target
_sip._tcp.example.com. 14000 IN SRV 0 0 5060 www.example.com.
# BOX
# TTL BOX flags port protocol recordtype priority weight port target
14000 BOX n 5060 6 33 0 0 5060 www.example.com
Other records that need to undergo such transformation is the MX record type, as well as the SOA record type.
Transformation of a SOA record into GNS works as described in the following example. Very important to note are the rname and mname keys.
# BIND syntax for a clean SOA record
IN SOA master.example.com. hostmaster.example.com. (
2017030300 ; serial
3600 ; refresh
1800 ; retry
604800 ; expire
600 ) ; ttl
# Recordline for adding the record
$ gnunet-namestore -z example.com -a -n -t SOA -V \
rname=master.example.com mname=hostmaster.example.com \
2017030300,3600,1800,604800,600 -e 7200s
The transformation of MX records is done in a simple way.
# mail.example.com. 3600 IN MX 10 mail.example.com.
$ gnunet-namestore -z example.com -n mail -R 3600 MX n 10,mail
Finally, one of the biggest struggling points were the NS records that are found in top level domain zones. The intended behaviour for those is to add GNS2DNS records for those so that gnunet-gns can resolve records for those domains on its own. Those require the values from DNS GLUE records, provided they are within the same zone.
The following two examples show one record with a GLUE record and the other one does not have a GLUE record. This takes place in the ‘com’ TLD.
# ns1.example.com 86400 IN A 127.0.0.1
# example.com 86400 IN NS ns1.example.com.
$ gnunet-namestore -z com -n example -R 86400 GNS2DNS n \
example.com@127.0.0.1
# example.com 86400 IN NS ns1.example.org.
$ gnunet-namestore -z com -n example -R 86400 GNS2DNS n \
example.com@ns1.example.org
As you can see, one of the GNS2DNS records has an IP address listed and the other one a DNS name. For the first one there is a GLUE record to do the translation directly and the second one will issue another DNS query to figure out the IP of ns1.example.org.
A solution was found by creating a hierarchical zone structure in GNS and linking the zones using PKEY records to one another. This allows the resolution of the name servers to work within GNS while not taking control over unwanted zones.
Currently the following record types are supported:
A
AAAA
CNAME
MX
NS
SRV
TXT
This is not due to technical limitations but rather a practical ones. The problem occurs with DNSSEC enabled DNS zones. As records within those zones are signed periodically, and every new signature is an update to the zone, there are many revisions of zones. This results in a problem with bigger zones as there are lots of records that have been signed again but no major changes. Also trying to add records that are unknown that require a different format take time as they cause a CLI call of the namestore. Furthermore certain record types need transformation into a GNS compatible format which, depending on the record type, takes more time.
Further a blacklist was added to drop for instance DNSSEC related records. Also if a record type is neither in the white list nor the blacklist it is considered as a loss of data and a message is shown to the user. This helps with transparency and also with contributing, as the not supported record types can then be added accordingly.
DNS Zone Size
Another very big problem exists with very large zones. When migrating a small zone the delay between adding of records and their expiry is negligible. However when working with big zones that easily have more than a few million records this delay becomes a problem.
Records will start to expire well before the zone has finished migrating. This is usually not a problem but can cause a high CPU load when a peer is restarted and the records have expired.
A good solution has not been found yet. One of the idea that floated around was that the records should be added with the s (shadow) flag to keep the records resolvable even if they expired. However this would introduce the problem of how to detect if a record has been removed from the zone and would require deletion of said record(s).
Another problem that still persists is how to refresh records. Expired records are still displayed when calling gnunet-namestore but do not resolve with gnunet-gns. Zonemaster will sign the expired records again and make sure that the records are still valid. With a recent change this was fixed as gnunet-gns to improve the suffix lookup which allows for a fast lookup even with thousands of local egos.
Currently the pace of adding records in general is around 10 records per second. Crypto is the upper limit for adding of records. The performance of your machine can be tested with the perf_crypto_* tools. There is still a big discrepancy between the pace of Ascension and the theoretical limit.
A performance metric for measuring improvements has not yet been implemented in Ascension.
Performance
The performance when migrating a zone using the Ascension tool is limited by a handful of factors. First of all ascension is written in Python3 and calls the CLI tools of GNUnet. This is comparable to a fork and exec call which costs a few CPU cycles. Furthermore all the records that are added to the same label are signed using the zones private key. This signing operation is very resource heavy and was optimized during development by adding the ‘-R’ (Recordline) option to gnunet-namestore which allows to specify multiple records using the CLI tool. Assuming that in a TLD zone every domain has at least two name servers this halves the amount of signatures needed.
Another improvement that could be made is with the addition of multiple threads or using asynchronous subprocesses when opening the GNUnet CLI tools. This could be implemented by simply creating more workers in the program but performance improvements were not tested.
Ascension was tested using different hardware and database backends. Performance differences between SQLite and postgresql are marginal and almost non existent. What did make a huge impact on record adding performance was the storage medium. On a traditional mechanical hard drive adding of records were slow compared to a solid state disk.
In conclusion there are many bottlenecks still around in the program, namely the single threaded implementation and inefficient, sequential calls of gnunet-namestore. In the future a solution that uses the C API would be cleaner and better.
Registering names using the FCFS daemon
This section describes FCFSD, a daemon used to associate names with PKEY records following a “First Come, First Served” policy. This policy means that a certain name can not be registered again if someone registered it already.
The daemon can be started by using gnunet-namestore-fcfsd
, which
will start a simple HTTP server on localhost, using a port specified by
the HTTPORT
value in its configuration.
Communication is performed by sending GET or POST requests to specific paths (“endpoints”), as described in the following sections.
The daemon will always respond with data structured using the JSON format. The fields to be expected will be listed for each endpoint.
The only exceptions are for the “root” endpoint (i.e. /
) which will
return a HTML document, and two other HTML documents which will be
served when certain errors are encountered, like when requesting an
unknown endpoint.
FCFSD GET requests .. _Obtaining-information-from-the-daemon:
Obtaining information from the daemon
To query the daemon, a GET request must be sent to these endpoints, placing parameters in the address as per the HTTP specification, like so:
GET /endpoint?param1=value¶m2=value
Each endpoint will be described using its name (/endpoint
in the
example above), followed by the name of each parameter (like param1
and param2
.)
- Endpoint: /search :bolditalic:`name`
This endpoint is used to query about the state of <name>, that is, whether it is available for registration or not.
The response JSON will contain two fields:
error
free
error
can be either the string"true"
or the string"false"
: when"true"
, it means there was an error within the daemon and the name could not be searched at all.free
can be either the string"true"
or the string"false"
: when"true"
, the requested name can be registered.
FCFSD POST requests .. _Submitting-data-to-the-daemon:
Submitting data to the daemon
To send data to the daemon, a POST request must be sent to these endpoints, placing the data to submit in the body of the request, structured using the JSON format, like so:
POST /endpoint
Content-Type: application/json
...
{"param1": value1, "param2": value2, ...}
Each endpoint will be described using its name (/endpoint
in the
example above), followed by the name of each JSON field (like param1
and param2
.)
- Endpoint: /register :bolditalic:`name key`
This endpoint is used to register a new association between <name> and <key>.
For this operation to succeed, both <NAME> and <KEY> must not be registered already.
The response JSON will contain two fields:
error
message
error
can be either the string"true"
or the string"false"
: when"true"
, it means the name could not be registered. Clients can get the reason of the failure from the HTTP response code or from themessage
field.message
is a string which can be used by clients to let users know the result of the operation. It might be localized to the daemon operator’s locale.
Customizing the HTML output
In some situations, the daemon will serve HTML documents instead of JSON
values. It is possible to configure the daemon to serve custom documents
instead of the ones provided with GNUnet, by setting the HTMLDIR
value in its configuration to a directory path.
Within the provided path, the daemon will search for these three files:
fcfsd-index.html
fcfsd-notfound.html
fcfsd-forbidden.html
The fcfsd-index.html
file is the daemon’s “homepage”: operators
might want to provide information about the service here, or provide a
form with which it is possible to register a name.
The fcfsd-notfound.html
file is used primarily to let users know
they tried to access an unknown endpoint.
The fcfsd-forbidden.html
file is served to users when they try to
access an endpoint they should not access. For example, sending an
invalid request might result in this page being served.