mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
Correctly implementing asynchronous IO is notoriously hard. In order to not drop packets in the process, one has to ensure a given socket is ready to accept packets, buffer them if it is not case, suspend everything else until the socket is ready and then continue. Until now, we did this because it was the only option to run the UDP sockets on the same thread as the actual packet processing. That in turn was motivated by wanting to pass around references of the received packets for processing. Rust's borrow-checker does not allow to pass references between threads which forced us to have the sockets on the same thread as the packet processing. Like we already did in other places in `connlib`, this can be solved through the use of buffer pools. Using a buffer pool, we can use heap allocations to store the received packets without having to make a new allocation every time we read new packets. Instead, we can have a dedicated thread that is connected to `connlib`'s packet processing thread via two channels (one for inbound and one for outbound packets). These channels are bounded, which ensures backpressure is maintained in case one of the two threads lags behind. These bounds also mean that we have at most N buffers from the buffer pool in-flight (where N is the capacity of the channel). Within those dedicated threads, we can then use `async/await` notation to suspend the entire task when a socket isn't ready for sending. Resolves: #8000