This required a mid-sized refactor of the relay's eventloop. The idea is that we can use [`mio`](https://docs.rs/mio/latest/mio/) to do the actual IO handling instead of `tokio`. `tokio` depends on `mio` internally but doesn't expose its primitives. Most importantly, we don't get access to the API where we can dynamically register file descriptors to watch for readiness. In order to avoid allocations on the relaying hotpath, we need to listen on a dynamic number of sockets: 1. Our client-facing socket on port 3478 2. All sockets allocated by clients `mio` is the building block of the async tokio runtime, hence it does not provide an async primitives. Instead, it blocks the current thread that it is running on and feeds you events that you need to deal with. We still need our `tokio` runtime to register timers and for communication with the portal. To integrate the two, we spawn a dedicated thread for `mio::Poll` and communicate with it via channels within the `Sockets` abstraction. Thus, the `Eventloop` itself has no idea that `mio` is used for all the network communication. Whenever `mio` sends us an event that a socket is ready, we try to read from that specific socket. We must read from this socket until it returns `WouldBlock` at which point we move on to the next event. We only register for read-readiness. If a socket is not ready for writing, we just drop the packet. With this design in place, we can now have a single buffer that we read incoming packets into and dispatch it to `Server`, depending on which port is what received on. A future refactoring could maybe even unify these functions and let the `Server` deal with the ports internally. Resolves: #4366.
relay
This crate houses a minimalistic STUN & TURN server.
Features
We aim to support the following feature set:
- STUN binding requests
- TURN allocate requests
- TURN refresh requests
- TURN channel bind requests
- TURN channel data requests
Relaying of data through other means such as DATA frames is not supported.
Building
You can build the relay using: cargo build --release --bin firezone-relay
You should then find a binary in target/release/firezone-relay.
Running
The Firezone Relay supports Linux only. To run the Relay binary on your Linux host:
- Generate a new Relay token from the "Relays" section of the admin portal and save it in your secrets manager.
- Ensure the
FIREZONE_TOKEN=<relay_token>environment variable is set securely in your Relay's shell environment. The Relay expects this variable at startup. - Now, you can start the Firezone Relay with:
firezone-relay
To view more advanced configuration options pass the --help flag:
firezone-relay --help
Ports
The relay listens on port 3478. This is the standard port for STUN/TURN and
not configurable. Additionally, the relay needs to have access to the port range
49152 - 65535 for the allocations.
Portal Connection
When given a token, the relay will connect to the Firezone portal and wait for
an init message before commencing relay operations.
Design
The relay is designed in a sans-IO fashion, meaning the core components do not cause side effects but operate as pure, synchronous state machines. They take in data and emit commands: wake me at this point in time, send these bytes to this peer, etc.
This allows us to very easily unit-test all kinds of scenarios because all inputs are simple values.
The main server runs in a single task and spawns one additional task for each allocation. Incoming data that needs to be relayed is forwarded to the main task where it gets authenticated and relayed on success.