GVPE.PROTOCOL(7)

πŸ¦‹ GVPE.PROTOCOL(7) β€” The GNU-VPE Protocols πŸ¦‹

okay bestie, welcome to the manual page for the GNU Virtual Private Ethernet protocols. This is GVPE.PROTOCOL(7) and it's a two-parter, so buckle up πŸ’–


✨ Overview ✨

GVPE can make use of a number of protocols. One of them is the GNU VPE protocol, which is the one doing the authenticate-the-tunnels and send-the-encrypted-data-packets jobs. That protocol gets described in way more detail in the second part of this document, iykyk.

The first part of this document β€” aka what you're reading rn β€” describes the transport protocols that GVPE uses to actually send its data packets over the network. 🌈


πŸš€ PART 1: Transport protocols

GVPE offers a wide range of transport protocols you can use to interchange data between nodes. The way that these protocols differ from each other is in their overhead, speed, reliability, and robustness. Like, they're all valid, they just have very different vibes.

The following sections describe each transport protocol in more detail. They are sorted by overhead/efficiency, with the most efficient transport listed first β€” so the first ones are the main characters πŸ‘‘


🌸 RAW IP

This protocol is literally the best choice, performance-wise, because the minimum overhead per packet is only 38 bytes. That's it. That's the whole overhead. Gorgeous.

It works by sending the VPN payload using raw IP frames (using the protocol set by ip-proto).

But bestie, using raw IP frames has a drawback: sooo many firewalls block "unknown" protocols. So this transport only works if you have full IP connectivity between nodes. No full IP connectivity, no raw IP, periodt. 😭

πŸŽ€ ICMP

This protocol offers very low overhead (minimum 42 bytes), and it can sometimes tunnel through firewalls when other protocols can not. Slay.

It works by prepending an ICMP header with type icmp-type and a code of 255. The default icmp-type is echo-reply, so the resulting packets look like echo replies, which looks rather strange to network administrators. Like, imagine being a sysadmin and seeing that. πŸ’€

This transport should only be used if other transports (i.e. raw IP) are not available or undesirable (due to their overhead).


🧚 UDP

This is a good general choice for the transport protocol, because UDP packets tunnel well through most firewalls and routers, and the overhead per packet is moderate (minimum 58 bytes). It's giving reliable-and-chill.

It should be used if RAW IP is not available. 🫢


πŸ™ˆ TCP

This protocol is a very bad choice, and I'm not even being dramatic. Reasons:

  • it not only has high overhead (more than 60 bytes)
  • but the transport also retries on its own, which leads to congestion when the link has moderate packet loss (as both the TCP transport and the tunneled traffic will retry, increasing congestion more and more)
  • it also has high latency
  • and it's quite inefficient

It's only useful when tunneling through firewalls that block better protocols. If a node doesn't have direct internet access but a HTTP proxy that supports the CONNECT method, it can be used to tunnel through a web proxy. For this to work, the tcp-port should be 443 (https), as most proxies do not allow connections to other ports.

It is an abuse of the usage a proxy was designed for, so make sure you are allowed to use it for GVPE. Don't be that girl. Ask first. πŸ’…

This protocol also has server and client sides. If the tcp-port is set to zero, other nodes cannot connect to this node directly. If the tcp-port is non-zero, the node can act both as a client as well as a server.


πŸ’€ DNS

WARNING: Parsing and generating DNS packets is rather tricky. The code almost certainly contains buffer overflows and other, likely exploitable, bugs. You have been warned. 🚨

This is the worst choice of transport protocol with respect to:

  • overhead (overhead can be 2-3 times higher than the transferred data)
  • latency (which can be many seconds)

Some DNS servers might not be prepared to handle the traffic and drop or corrupt packets. The client also has to constantly poll the server for data, so the client will constantly create traffic even if it doesn't need to transport packets. Not it just vibing there burning bandwidth for no reason 😭

In addition, the same problems as the TCP transport also plague this protocol. So yeah, it's got the TCP problems stacked on top of everything else.

Its only use is to tunnel through firewalls that do not allow direct internet access. Similar to using a HTTP proxy (as the TCP transport does), it uses a local DNS server/forwarder (given by the dns-forw-host configuration value) as a proxy to send and receive data as a client, and an NS record pointing to the GVPE server (as given by the dns-hostname directive).

The only good side of this protocol is that it can tunnel through most firewalls mostly undetected, iff the local DNS server/forwarder is sane (which is true for most routers, wireless LAN gateways and nameservers). πŸ‘‘

Fine-tuning needs to be done by editing src/vpn_dns.C directly. Yes bestie, straight into the source. Good luck. 🌸

πŸ’– PART 2: The GNU VPE protocol πŸ’–

okay bestie, real talk: this section is not finished yet 😭 but the protocol itself is stable β€” well, until somebody finds a bug in the cryptography, and if that happens this whole description is probably getting completely rewritten. so like, take it as a vibe overview, not a sworn affidavit. πŸ’…


🧚 Anatomy of a VPN packet 🧚

the exact layout and field lengths of a VPN packet? decided at compile time and it does not budge. and the same structure gets used for every transport protocol, whether that's RAWIP or TCP. one shape, all the vibes. ✨

 +------+------+--------+------+
 | HMAC | TYPE | SRCDST | DATA |
 +------+------+--------+------+

the HMAC field is in every packet, even when it's not being used (like in auth request packets) β€” in that case it's just set to all zeroes. and the MAC itself is always calculated over the TYPE, SRCDST and DATA fields, no matter what. consistency queen. πŸ‘‘

TYPE is a single byte and it tells you the whole purpose of the packet β€” like RESET, COMPRESSED/UNCOMPRESSED DATA, PING, AUTH REQUEST/RESPONSE, CONNECT REQUEST/INFO, etc.

SRCDST is a three byte field holding the source and destination node IDs, 12 bits each.

the DATA portion is different for every packet type, obviously, and it's the only part that can be encrypted. data packets get extra fields, like so:

 +------+------+--------+-------+------+
 | HMAC | TYPE | SRCDST | SEQNO | DATA |
 +------+------+--------+-------+------+

SEQNO is a 32-bit sequence number. it's negotiated at every single connection initialization and starts at some random 31 bit value. GVPE currently runs a sliding window of 512 packets/sequence numbers to catch reordering, duplication and replay attacks. πŸ•΅οΈβ€β™€οΈ

encryption happens on SEQNO+DATA in CTR mode, with the IV generated from the seqno β€” for AES that's seqno || seqno || seqno || (u32)0 β€” which guarantees uniqueness for a given key. no repeats, no notes. πŸŽ€


πŸ” The authentication/key exchange protocol πŸ”

before any nodes can swap packets, they have to establish that the other side is actually them, plus agree on a key. every node holds a private RSA key and the public RSA keys of all the other nodes. it's giving mutual trust issues, but make it cryptography. πŸ’€

when a node wants to connect to another node, it sends an RSA-OEAP-encrypted challenge and an ECDH (curve25519) key. the other node replies with its own ECDH key plus a HKDF of the challenge and both ECDH keys β€” that's the proof of identity. 🫢

and the remote node engages in exactly the same protocol right back. once both nodes have exchanged their challenge and verified the response, they calculate a cipher key and a HMAC key, and then it's data packet o'clock. 🌸

in detail, the challenge consists of:

RSA-OAEP (SEQNO MAC CIPHER SALT EXTRA-AUTH) ECDH1

so basically: it encrypts (with the public key of the remote node) an initial sequence number for data packets, key material for the HMAC key, key material for the cipher key, a salt used by the HKDF (more on that below) and some extra random bytes that go unused except for authentication. and it also sends the public key of a curve25519 exchange. multitasking icon. ✨

the remote node decrypts that RSA data, generates its own ECDH key (ECDH2), and replies with:

HKDF-Expand (HKDF-Extract (ECDH2, RSA), ECDH1, AUTH_DIGEST_SIZE) ECDH2

so it extracts from the decrypted RSA challenge using its own ECDH key as salt, then expands using the requesting node's ECDH1 key. that resulting hash comes back as proof the node could actually decrypt the RSA challenge data, bundled together with the ECDH key. πŸ’…

once both nodes have done this to each other, they calculate the shared ECDH secret plus cipher and HMAC keys for the session β€” and heads up, each node generates two cipher and HMAC keys, one for sending and one for receiving.

the HMAC key for sending gets generated like this:

HMAC_KEY = HKDF-Expand (HKDF-Extract (REMOTE_SALT, MAC ECDH_SECRET), info, HMAC_MD_SIZE)

it extracts from MAC and ECDH_SECRET using the remote SALT, then expands using a static info string.

the cipher key? same exact way, except it uses the CIPHER part of the original challenge instead.

the whole point of this process is to authenticate each node to the other while exchanging keys using both RSA and ECDH β€” and ECDH is the one bringing the perfect forward secrecy to the party. πŸ”₯

ngl, the protocol has been overdesigned wherever that was possible without adding implementation complexity, all in an attempt to protect against implementation or protocol failures. for example: if the ECDH challenge turned out to be flawed, perfect forward secrecy would be lost, but the data would likely still be protected. same energy β€” standard algorithms and implementations get used wherever possible. no cap. 🧚


πŸ” Retrying πŸ”

when there's no response to an auth request, the node sends auth requests in bursts with an exponential back-off. after a while it resorts to PING packets, which are tiny (8 bytes + protocol header) and lightweight (no RSA operations required, thank u). and a node that gets ping requests from an unconnected peer will respond by trying to create a connection. polite. 🫢

on top of the exponential back-off there's a global rate-limit on a per-IP base. it allows long bursts but caps total packet rate to something like one control packet every ten seconds β€” which is there to avoid accidental floods from protocol problems (like an RSA key file mismatch between two nodes, oops πŸ™ˆ).

the intervals between retries are limited by the max-retry configuration value. a node with connect = always will always retry; a node with connect = ondemand will only try (and re-try) to connect for as long as there are packets in the queue β€” which usually limits the retry period to max-ttl seconds.

and sending packets over the VPN resets the retry intervals too, so as long as somebody's trying to send packets to a given node, GVPE will try to connect every few seconds. persistent bestie behavior. πŸ’œ


πŸ—ΊοΈ Routing and Protocol translation πŸ—ΊοΈ

the GVPE routing algorithm is easy, because there isn't much routing to speak of: when routing packets to another node, GVPE tries these options in order:

  1. if the two nodes should be able to reach each other directly (common protocol, port known), then GVPE sends the packet directly to the other node.
  2. if that isn't possible (say, because the node doesn't have a hostname or a known port), but the nodes speak a common protocol and a router is available, then GVPE asks a router to "mediate" between both nodes (see below πŸ‘‡).
  3. if a direct connection isn't possible (no common protocols) or is forbidden (deny-direct) and there are any routers, then GVPE tries to send packets to the router with the highest priority that's already connected and is able (as specified by the config file) to connect directly to the target node.
  4. if no such router exists, then GVPE just sends the packet to the node with the highest priority available.
  5. and failing all of that? the packet gets dropped. πŸ’€

a host can usually declare itself unreachable directly by setting its port number(s) to zero. it can declare other hosts unreachable by using a config-file that disables all protocols for those other hosts. another option is to disable all protocols on that host in the other config files. many paths, one goal. ✨

if two hosts can't connect to each other because their IP address(es) aren't known (dial-up hosts, we see you), one side will send a mediated connection request to a router β€” and routers must be configured to act as routers! β€” which then sends both the originating and the destination host a connection info request with protocol information and the IP address of the other host (if known). both hosts will then try to establish a direct connection to the other peer, which is usually possible even when both hosts are behind a NAT gateway. πŸŽ€

routing via other nodes works because the SRCDST field is not encrypted, so the router can just forward the packet to the destination host. and since each host uses its own private key, the router can't decrypt or encrypt packets at all β€” it's just a simple router and protocol translator doing its little job. main character energy from a side character. πŸ‘‘


that's the whole page bestie, go build a VPN and slay πŸ’–

this was GVPE.PROTOCOL(7), version 2.25, dated 2015-10-31 β€” thank u for coming to my ted talk, mwah πŸŽ€βœ¨

πŸ“¦ where this page comes from