By default, DNS queries are sent over UDP by most systems. UDP is an easy to understand protocol because each packet stands by itself and at least as far as UDP is concerned, the payload is contained within a single packet. In Firezone, we receive all DNS traffic on the TUN device as IP packets. Processing the UDP packets is trivial as each query is contained within a single IP packet. For TCP, we first need to assemble the TCP stream before we can read the entire query. In case a DNS query is not for a Firezone DNS resource, we want to forward it to the specified upstream resolver, either directly from the system or - in case the specified upstream resolver is an IP resource - through the tunnel as an IP packet. Specifically, the forwarding of UDP DNS packets through the tunnel currently happens like this: IP packet -> read UDP payload -> parse DNS query -> mangle original destination IP to new upstream -> send through tunnel For TCP DNS queries, it is not quite as easy as we have to decode the incoming TCP stream first before we can parse the DNS query. Thus, when we want to then forward the query, we need to open our own TCP stream to the upstream resolver and encode the DNS query onto that stream, sending each IP packet from the TCP client through the tunnel. The difference in these designs makes several code paths in connlib hard to follow. Therefore - and despite the simplicity of DNS over UDP - we already created our own "Layer 3 UDP DNS"-client. This PR now integrates this client into the tunnel. Using this new client, we can simplify the processing of UDP DNS queries because we never have to "go back" to the original IP packet. Instead, when a DNS query needs to be forwarded to an usptream resolver through the tunnel, we simply tell the Layer 3 UDP DNS client to make a new DNS query. The processing of the resulting IP packet then happens in a different place, right next to where we also process the IP packets of the TCP DNS client. That simplifications unlocks further refactorings where we now only process DNS queries in a single place and the transport we received it over is a simple function parameter with the control flow for both of them being identical. Related: #4668
Rust development guide
Firezone uses Rust for all data plane components. This directory contains the Linux and Windows clients, and low-level networking implementations related to STUN/TURN.
We target the last stable release of Rust using rust-toolchain.toml.
If you are using rustup, that is automatically handled for you.
Otherwise, ensure you have the latest stable version of Rust installed.
Reading Client logs
The Client logs are written as JSONL for machine-readability.
To make them more human-friendly, pipe them through jq like this:
cd path/to/logs # e.g. `$HOME/.cache/dev.firezone.client/data/logs` on Linux
cat *.log | jq -r '"\(.time) \(.severity) \(.message)"'
Resulting in, e.g.
2024-04-01T18:25:47.237661392Z INFO started log
2024-04-01T18:25:47.238193266Z INFO GIT_VERSION = 1.0.0-pre.11-35-gcc0d43531
2024-04-01T18:25:48.295243016Z INFO No token / actor_name on disk, starting in signed-out state
2024-04-01T18:25:48.295360641Z INFO null
Benchmarking on Linux
The recommended way for benchmarking any of the Rust components is Linux' perf utility.
For example, to attach to a running application, do:
- Ensure the binary you are profiling is compiled with the
releaseprofile. sudo perf record -g --freq 10000 --pid $(pgrep <your-binary>).- Run the speed test or whatever load-inducing task you want to measure.
sudo perf script > profile.perf- Open profiler.firefox.com and load
profile.perf
Instead of attaching to a process with --pid, you can also specify the path to executable directly.
That is useful if you want to capture perf data for a test or a micro-benchmark.