GitHub

Warning

RNL including its cryptography code is non-audited so far, thus RNL is only intended for real-time games and multimedia applications without processing of any with critical data, but not for serious applications with critical data!

Description

RNL stands for "Realtime Network Library"

RNL is an UDP-based network library for real-time applications and games, inspired by ENet, yojimbo, libgren, and so on.

RNL is designed around common patterns used in real-time games, which are simulation bound, not I/O bound, and completely stateful, so async IO does not make a lot of sense. Thus the RNL core design is single-threaded, not multi-threaded. But you can use multiple TRNLHost instances inside multiple different threads (one to very few instances per one thread), so that you can host multiple network game matches at the same machine, as long as this one machine is strong and fast enough for hosting multiple network game matches at the same time.

And at game client side, the whole network stuff should run, if possible, in an own (also if possible, CPU-core-pinned) thread, for possible few interferences and other similiar problems. (offtopic: the same also applies to the audio thread, unless one likes possible audio buffer underrun issues and so on, when it did not get enough CPU time at the right time points. :-) )

And for larger games with masses of clients in a single game world, you should use several subdivided TRNLHost instances, so that each TRNLHost must handle only few connected clients, in multiple threads and that in turn on multiple physical dedicated servers, which also in turn may communicate with each other to mimic the impression of a single very large game world. At least a single TRNLHost instance is rather designed for typical low client numbers, as these are the typical case for egoshooters, racing games, and so forth. Or in other words for large game worlds with masses of clients: Divide and conquer (for example with partially sector-border overlapping game world sectors for just as an example of an divide-and-conquer concept idea)

Support me

Support me at Patreon

Features

  • Mostly fully object oriented code design
  • IPv6 support
  • Cross platform
    • Windows (with FreePascal and Delphi)
    • Linux (with FreePascal)
    • *BSD (with FreePascal)
    • Android (with FreePascal and Delphi)
    • Darwin (MacOS(X) and iOS) (with FreePascal and Delphi)
  • UDP-based protocol
  • Sequencing
  • Channels
    • With following possible free configurable channel types:
      • Reliable ordered
      • Reliable unordered
      • Unreliable ordered
      • Unreliable unordered
  • Reliability
  • Fragmentation and reassembly
  • Aggregation
  • Adaptability
  • Portability
  • Possibility of using a peer-to-peer model or even a mixed peer-to-peer and client/server hybrid model instead only a pure client/server model, and of course also of a classic client/server model
  • Cryptographically secure pseudo-random number generator (CSPRNG)
    • Based on arc4random but with ChaCha20 instead RC4 as the basic building block
    • Multiple sources of entropy (because you should never trust a single source of entropy, as it may have a backdoor)
      • Including usage of the rdseed/rdrand instructions on newer x86 processors as an optional additional quasi-hardware-based entropy source, if these instructions are supported by the current running processor
  • Mutual authentication
    • Based on a Station-to-Station (STS) like protocol, which assumes that the parties have signature keys, which are used to sign messages, thereby providing minification security against man-in-the-middle attacks, unlike the basic plain Diffie-Hellman method without any so such extensions.
    • Long-term private/public keys are ED25519 keys and are used only for signing purposes
  • Forward secrecy using elliptic curve ephemeral Diffie-Hellman (curve 25519)
    • The consequence of this along other facts is that each connection always has new different private and public short-term keys on both sides and therefore also new shared secret short-term keys
    • Short-term private/public keys are X25519 keys and the short-term shared secret key is using only for AEAD-based ciphering purposes
  • Authenticated Encryption with Associated Data (AEAD) packet encryption
    • Based on ChaCha20 as cipher and Poly1305 as cryptographic message authentication code
  • Replay protection of application packet data
    • Based on various protection mechanisms at the connection establishment phase and encrypted packet sequence numbers
  • Delayed connection establishment mechanism as an additional attack surface minification mechanism
  • Connection and authentication tokens (as an optional option, where you should have a separate out-of-band communication channel, for example a HTTPS-based master backend for to generate and handle this stuff) as an additional attack surface minification mechanism against DDoS amplification attacks
    • Connection token are transferred in clear text, so that they are checked in a fast way at the first ever data packet from a connection attempt, without the need to decrypt the connection token first before it is possible to check the token, so in order to save CPU time in this point. This option is primarily for use in against DDoS amplification attacks, which means that the server will not respond straight away if the connection token does not match at the first ever data packet from a connection attempt, and thus DDoS amplification attacks would simply go into the nothing. Consequently, these tokens should only be valid for a short period of time, which also applies to the master backend side of your infrastructure.
    • Authentication tokens are transferred encrypted, after the private/public key exchange, shared secret key generation, etc. were successfully processed. Authentication tokens, in contrast to the connection token, are NOT a countermeasure against DDoS-category attacks, but rather authentication tokens are, as the name suggests, only for separate out-of-band communication channel authentication purposes, in other words, as additional protection against unauthorized connections, where you can check it in more detail on your master backend side of your infrastructure, before the "client" can connect to the real server, where all the real action happens.
  • Connection attempt rate limiter
    • Configurable with two constants, burst and period
    • Relay aware: an address which was declared as a relay is limited on two levels at once, a bucket per client behind it and a ceiling for the relay as a whole. Behind a relay every client arrives under the same address, so a single bucket would let any one of them lock out all the others; the clients are told apart by the relayed port, which the relay assigns per allocation, and the ceiling is what keeps that from being a way around the limit
  • Configurable bandwidth rate limiter
  • Optional virtual network feature (for example for fast network-API-less local loopback solution for singleplayer game matches, which should be still server/client concept based)
  • Network interference simulator (for example for testcases and so on)
    • Configurable simulated packet loss probability (each for incoming and outgoing packets)
    • Configurable simulated latency (each for incoming and outgoing packets)
    • Configurable simulated jitter (each for incoming and outgoing packets)
    • Configurable simulated duplicate packet probability (each for incoming and outgoing packets)
  • Dynamic connection challenge request response difficulty adjustment mechanism
    • Configurable with a factor value
    • Based on history-smoothing-frames-per-second-style determination mechanism, but just instead frames per second, connection attempts per second
  • More compression algorithms as choices
    • Deflate (a zlib bit-stream compatible LZ77 and canonical Huffman hybrid, only fixed-static-canonical-huffman in this implementation here on compressor side, but the decompressor side is full featured)
    • LZBRRC (a LZ77-style compressor together with an entropy range coder backend)
    • BRRC (a pure order 0 entropy range coder)
  • CRC32C instead CRC32 (without C at the end), and CRC32 (IEEE) as well, which STUN needs for its FINGERPRINT attribute
  • Optional handshake transcript binding
    • The signatures of the mutual authentication cover the whole transcript of the handshake, including the fields which travel in clear text, rather than only the two ephemeral keys
    • Three modes, off, allowed and required. Off by default, so that existing code keeps behaving exactly as it did, down to the same bytes on the wire and the same protocol version; only required is fully safe, and allowed exists as a migration step and can be downgraded by an attacker rewriting one clear text version field, which is why it says so here
    • The long term public key the counter side proved possession of is available on every connection and can be pinned
  • Optional single stage certificates for the mutual authentication
    • One public key of an issuing authority in the client's configuration instead of one pinned key per server, which is what pinning cannot do: key rotation, and several servers behind one name. Several authority keys are accepted at once, so that rotating the authority itself is not a flag day either
    • No chain, no ASN.1, no revocation list. Subject, issuer signature and two validity fields, 104 bytes, in a fixed area which is all zero when there is none. It does not carry the subject's public key although that is what it vouches for: the key is already in the payload as the one which signed the handshake, and the signature covers it from there, so there cannot be a second copy disagreeing with the first
    • The subject is opaque and RNL never interprets it. No host name concept, no wildcard matching, no case folding, no parsing; the issuer puts whatever the deployment uses as an identity into it and the client compares byte for byte. That is deliberate: every serious failure of X.509 name handling comes from the library trying to understand names
    • Validity as two counts of minutes since 1 January 2026, zero for no bound. Minutes rather than days because the most useful shape is a short lived certificate issued per session by the same thing that already hands out connection and authentication tokens. The wall clock is set by the application rather than read from the operating system, and a certificate with a validity period is refused outright where there is no clock to hold it against
    • Checked before the client hands over its authentication token, which is the real gain rather than the rotation: the server's identity travels in the packet before the one carrying the token, so a bearer token is only ever given to a server which has proved who it is
    • Entirely opt in. Without a configured certificate and without an authority key, nothing about a handshake changes
  • NAT traversal
    • Candidate gathering: one host candidate per local interface address and socket, one server reflexive candidate per STUN server which answers, and a relayed one if a relay is in front of the network. Priority and foundation as RFC 8445 computes them, plus serialisation, so that the application can carry candidates over whatever signalling it already has
    • STUN client (RFC 8489). FINGERPRINT is included and checked, which is what makes it possible for STUN and RNL to share one socket. Usable during startup, and also while a host is being serviced, in which case the host answers its own binding requests out of its receive path
    • NAT behaviour discovery, split the way RFC 4787 splits it. The mapping behaviour comes from two servers on different hosts, the filtering behaviour from RFC 5780 CHANGE-REQUEST where a server offers it and stays unknown where none does. From the two of them plus the counter side's, the viability of a direct connection can be predicted before it is attempted rather than after a ten second timeout
    • Hole punching. The handshake repetition fans out over every pairing of a local socket with a remote candidate, bounded per round so that a candidate list from the counter side cannot turn this host into a sender of padded datagrams at every address in it. A four byte punch packet opens a mapping without starting a connection, for the side which is being called
    • Two sides connecting to each other at the same time end up with one connection rather than two, resolved by comparing the random salts both sides exchange anyway, without a protocol field for it
    • One socket per named local address instead of one per address family, for a host which wants to decide which interfaces it is reachable on. A peer then keeps the socket its handshake ran over, because an answer has to leave where the question came in
  • TURN relay (RFC 8656) for the case a direct path cannot be had at all
    • As a network decorator, so that host and peer never learn about it
    • Allocate, Refresh, CreatePermission and ChannelBind, long term credentials with both MESSAGE-INTEGRITY and MESSAGE-INTEGRITY-SHA256, realm and nonce handling including the STALE_NONCE retry
    • ChannelData once a channel is bound, which replaces the thirty six bytes of a Send indication around every datagram with four. Channel numbers are let go of when they go quiet and come back into circulation after the delay RFC 8656 requires
    • IPv6 allocations through REQUESTED-ADDRESS-FAMILY, asked for only where it is needed, since the attribute is comprehension required and a relay which does not know it would refuse
    • UDP, TCP or DTLS as the transport to the relay. TCP is there for the network which lets no UDP out at all, which is exactly the case a relay exists for; DTLS for the one which lets datagrams out but wants to read them on the way, and for the relay one does not run oneself
  • DTLS client (RFC 6347 for 1.2, RFC 9147 for 1.3), written for the relay transport above and owning no socket and no clock of its own, so that a handshake can be driven a datagram at a time in a test with the clock held still
    • One cipher suite each, and the same one twice: ECDHE-ECDSA with ChaCha20-Poly1305 for 1.2, TLS_CHACHA20_POLY1305_SHA256 for 1.3. RNL already has that cipher for its own packets, so the record layer is new code and the cryptography under it is not
    • 1.2 is tried first and 1.3 second where the version is left open, because 1.2 is what is actually deployed on relays today. Fragmented handshake messages are reassembled in any order and with overlap, flights are retransmitted with a doubling wait, and 1.3 sends and reads the ACK of RFC 9147 section 7 so that a lost record does not cost a whole flight
    • x25519 and secp256r1 shares both go out, since deployments disagree about which they want, and the key exchange signature is checked against what the certificate actually carries
    • Certificate verification the way RFC 5280 section 6 lays it out: signatures, issuer names, basic constraints, key usage, path length, validity and the host name, over a DER reader which parses no more of X.509 than that comes to. P-256 and P-384 with SHA-256 and SHA-384
    • Or, instead of all of it, the SHA-256 of the certificate's own bytes against a configured value. That is the case a relay one runs oneself is: no authority to appeal to, often no trustworthy clock, and a host name which is whatever DNS says today. Several fingerprints are accepted at once so that replacing a certificate is not a flag day. What pinning does not replace is the reader - the key that checks the handshake signature sits inside the certificate either way
    • Raw public keys (RFC 7250), which is what pinning was reaching for anyway: a certificate that exists only to carry a key, signed by nobody in particular, is a wrapper around the one thing a pin was ever going to look at. Off by default and only offered where a fingerprint is what recognises the peer, since without a certificate there is no chain, no issuer, no validity and no name for anything else to judge - and because the extension changes the ClientHello of every handshake, including those against relays which have never heard of it. Only the server side of it is offered: the other half says what this end could present, and this end presents nothing
  • Hash primitives
    • SHA-512 and BLAKE2B for the handshake, plus SHA-256, SHA-1 and MD5 for what STUN and TURN need of them. SHA-1 and MD5 are only ever used for the credentials of a relay which speaks the older revision, never for anything RNL itself protects, and can be switched off
    • One HMAC implementation for all of them, over a descriptor of the hash rather than a generic type parameter, so that it compiles on FreePascal and Delphi alike
  • Cryptography self test callable at run time, so that a build can prove its own primitives against the published test vectors instead of assuming them
  • Adaptive congestion control, off by default. Regulates on queueing delay as the primary signal with loss as an independent second one, takes the capacity of the path from a windowed maximum of the rate the counter side actually acknowledges, and enforces the result by spreading it out in time rather than by throwing datagrams away. Measured against a simulated bottleneck in four shapes - deep and shallow buffer, each with and without a competing flow - it keeps the standing queue between 8 and 65 ms where the buffer allows up to 1500 ms, without losing a connection. A queue which has already built up is drained by sending as much below the capacity as the length of that queue calls for, so that a shallow one is not charged the price of a deep one. The measurements behind it are readable on their own as well: baseline round trip time, queueing delay above it, delivery rate, and loss per flight
  • Delayed rather than dropped enforcement of an outgoing bandwidth limit, and an optional age bound after which unreliable data is discarded on the way out instead of delivered stale
  • LAN service discovery, as a server which answers browse requests and a client which sends them. Each answer carries up to 255 bytes of metadata which the application chooses, and that metadata is writable while the server is running: a lobby which fills up publishes its new seat count to the next browse request without the discovery thread being torn down and set up again
  • The outgoing bandwidth limit of a host is divided among its peers by a per peer weight instead of being a single budget which whoever is dispatched first empties. The order in which peers are dispatched rotates as well, since a share nobody can reach is not a share
  • And a lot of more stuff . . .

Planned features (a.k.a Todo) in random order of priorities

  • TLS over TCP as a fourth transport to the TURN relay. DTLS covers the network which reads what leaves it but still passes datagrams; TLS over a stream would cover the one which passes nothing but a stream on port 443. Most of what it needs now exists - the same key schedule, the same certificate handling, the same signatures - and what is missing is a record layer over a stream, which is the easier half of the two: no cookie exchange, no epochs, no fragment reassembly, no retransmission, because the stream underneath already does all of it
  • The server side of DTLS. What is there is the client, which is what a TURN transport needs. A server would have to choose suites rather than offer one, hold a certificate and a private key, and answer a cookie exchange - and RNL has no use for it until something wants to be talked to over DTLS rather than to talk over it
  • TODO

General guidelines for code contributors

General guidelines for code contributors

License

zlib License

IRC channel

IRC channel #rnl on Freenode

Thanks

  • Thanks to Lee Salzman for ENet as inspiration for the base API design implementation ideas
  • Thanks to Glenn Fiedler for inspiration for security-oriented implementation ideas
  • Thanks to Sergey Ignatchenko ("No Bugs" Hare) for inspiration also for security-oriented implementation ideas

Read the original on github.com ↗