Sans-IO: The secret to effective Rust for network services
Firezone utilizes connlib, a Rust library for managing network connections and WireGuard tunnels sans-IO. This approach enhances testing, customization, and functionality assurance, promoting efficient and flexible network services development.
Read original articleAt Firezone, a connectivity library named connlib is used to manage network connections and WireGuard tunnels securely. The library is designed with a sans-IO approach in Rust, emphasizing state machines over direct socket interactions. This design choice allows for better testing, customization, and assurance of functionality. The post discusses the benefits of sans-IO design in Rust for network services, highlighting its compatibility with async models and addressing challenges like the "function colouring" debate. By abstracting away IO operations and introducing abstractions like Transmit and event loops, developers can create efficient and flexible network services. The article also delves into implementing a STUN binding protocol using sans-IO principles, showcasing how to handle input, transmit data, and abstract time for protocol operations. Overall, the sans-IO approach in Rust enables cleaner, more modular, and testable network service implementations.
Related
Interface Upgrades in Go (2014)
The article delves into Go's interface upgrades, showcasing their role in encapsulation and decoupling. It emphasizes optimizing performance through wider interface casting, with examples from io and net/http libraries. It warns about complexities and advises cautious usage.
Remembering the LAN (2020)
The article discusses the shift from simple LAN setups in the 1990s to complex modern internet programming. It highlights DIY solutions for small businesses and envisions a future merging traditional LAN environments with modern technologies.
SquirrelFS: Using the Rust compiler to check file-system crash consistency
The paper introduces SquirrelFS, a crash-safe file system using Rust's typestate pattern for compile-time operation order enforcement. Synchronous Soft Updates ensure crash safety by maintaining metadata update order. SquirrelFS offers correctness guarantees without separate proofs, quickly verifying crash consistency during compilation. Comparative evaluations show SquirrelFS performs similarly or better than NOVA and WineFS.
Download Accelerator – Async Rust Edition
This post explores creating a download accelerator with async Rust, emphasizing its advantages over traditional methods. It demonstrates improved file uploads to Amazon S3 and provides code for parallel downloads.
The Inconceivable Types of Rust: How to Make Self-Borrows Safe
The article addresses Rust's limitations on self-borrows, proposing solutions like named lifetimes and inconceivable types to improve support for async functions. Enhancing Rust's type system is crucial for advanced features.
The biggest productivity boost to my rust embedded firmware development was when I could stop manually implementing state machines and marshalling all local variables into custom state after custom state between each I/O operation snd let rust do that for me by using async/await syntax!
That’s, after all, what async desugars to in rust: an automatic state machine that saves values across I/O (await) points for you.
What got me thinking about this was the whole fn coloring discussion, and a happy accident on my part. I had been writing a VT100 library and was doing my head in trying to unit test it. The problem was that I was essentially `parser::new(stdin())`. During the 3rd or 4th rewrite I changed the parser to `parser::push(data)` without really thinking about what I was doing. I then realized that Rust was punishing me for using an enterprise OOPism anti-pattern I have since been calling "encapsulation infatuation." I now see it everywhere (not just in I/O) and the havoc it wreaks.
The irony is that this solution is taught pre-tertiary education (and again early tertiary). The simplest description of a computer is a machine that takes input, processes/transforms data, and produces output. This is relevant to the fn coloring discussion because only input and output need to be concerned with it, and the meat-and-potatoes is usually data transformation.
Again, this is patently obvious - but if you consider the size of the fn coloring "controversy;" we've clearly all been missing/forgetting it because many of us have become hard-wired to start solving problems by encapsulation first (the functional folks probably feel mighty smug at this point).
Rust has seriously been a journey of more unlearning than learning for me. Great pattern, I am going to adopt it.
Edit: code in question: https://codeberg.org/jcdickinson/termkit/src/branch/main/src...
But besides that it's pretty convenient. Let's say you have a ws_handler channel, you just send your data through that and there is a dedicated handler somewhere that may or may not send that message if it's able to.
The idea of separating logic from execution is a whole thing, well trodden by the Haskell ecosystem.
[EDIT] Also, they didn't mention how they encapsulated the `tokio::select!` call that shows up when they need to do time-related things -- are they just carrying around a `tokio::Runtime` that they use to make the loop code async without requiring the outside code to be async?
[EDIT2] Maybe they weren't trying to show an encapsulated library doing that, but rather to show that the outside application can use the binding in an async context...
I would have been more interested in seeing how they could implement an encapsulated function in the sans-IO style that had to do something like wait on an action or a timer -- or maybe the answer they're expecting there is just busy-waiting, or carrying your own async runtime instance (that can essentially do the busy waiting for you, with something like block_in_place.
I got half way through this article feeling like this pattern was extremely familiar after spending time down inside rust-libp2p. Seems like that wasn't a coincidence!
Firezone looks amazing, connect all the things!
Has anyone tried to combine async and sans-io? At least morally, I ought to be able to write an async function that awaits sans-io-aware helpers, and the whole thing should be able to be compiled down to a state machine inside a struct with a nice sans-io interface that is easily callable by non-async code.
I’ve never tried this, but the main issues I would forsee would be getting decent ergonomics and dealing with Pin.
The most interesting thing I learned from the article is that cloudflare runs a public stun server. But even that isn't helpful because the 'good' and 'useful' version of the STUN protocol is the first version of the protocol which supports 'change requests' -- a feature that allows for NAT enumeration. Later versions of the STUN protocol removed that feature thanks to the 'helpful suggestions' of Cisco engineers who contributed to the spec.
Am I missing something?
Why would you want this in a client? It's not like a client needs to manage tens of thousands of connections. Unless it's doing a DDOS job.
I mean, this is basically what the IO monad and monadic programming in Haskell end up pushing Haskell programmers to do.
- the event loop
- the state machine of data states that occur
But async rust is already a state machine, so the stun binding could be expressed as a 3 line async function that is fairly close to sans-io (if you don't consider relying on abstractions like Stream and Sink to be IO).
async fn stun(
server: SocketAddr,
mut socket: impl Sink<(BindingRequest, SocketAddr), Error = color_eyre::Report>
+ Stream<Item = Result<(BindingResponse, SocketAddr), color_eyre::Report>>
+ Unpin
+ Send
+ 'static,
) -> Result<SocketAddr> {
socket.send((BindingRequest, server)).await?;
let (message, _server) = socket.next().await.ok_or_eyre("No response")??;
Ok(message.address)
}
If you look at how the underlying async primitives are implemented, they look pretty similar to what you;ve implemented. sink.send is just a future for Option<SomeMessage>, a future is just something that can be polled at some later point, which is exactly equivalent to your event loop constructing the StunBinding and then calling poll_transmit to get the next message. And the same goes with the stream.next call, it's the same as setting up a state machine that only proceeds when there is a next item that is being fed to it. The Tokio runtime is your event loop, but just generalized.Restated simply: stun function above returns a future that that combines the same methods you have with a contract about how that interacts with a standard async event loop.
The above is testable without hitting the network. Just construct the test Stream / Sink yourself. It also easily composes to add timeouts etc. To make it work with the network instead pass in a UdpFramed (and implement codecs to convert the messages to / from bytes).
Adding timeout can be either composed from the outside caller if it's a timeout imposed by the application, or inside the function if it's a timeout you want to configure on the call. This can be tested using tokio test-utils and pausing / advancing the time in your tests.
---
The problem with the approach suggested in the article is that it splits the flow (event loop) and logic (statemachine) from places where the flow is the logic (send a stun binding request, get an answer).
Yes, there's arguments to be made about not wanting to use async await, but when you effectively create your own custom copy of async await, just without the syntactic sugar, and without the various benefits (threading, composability, ...), it's worth considering whether you could use async instead.
Related
Interface Upgrades in Go (2014)
The article delves into Go's interface upgrades, showcasing their role in encapsulation and decoupling. It emphasizes optimizing performance through wider interface casting, with examples from io and net/http libraries. It warns about complexities and advises cautious usage.
Remembering the LAN (2020)
The article discusses the shift from simple LAN setups in the 1990s to complex modern internet programming. It highlights DIY solutions for small businesses and envisions a future merging traditional LAN environments with modern technologies.
SquirrelFS: Using the Rust compiler to check file-system crash consistency
The paper introduces SquirrelFS, a crash-safe file system using Rust's typestate pattern for compile-time operation order enforcement. Synchronous Soft Updates ensure crash safety by maintaining metadata update order. SquirrelFS offers correctness guarantees without separate proofs, quickly verifying crash consistency during compilation. Comparative evaluations show SquirrelFS performs similarly or better than NOVA and WineFS.
Download Accelerator – Async Rust Edition
This post explores creating a download accelerator with async Rust, emphasizing its advantages over traditional methods. It demonstrates improved file uploads to Amazon S3 and provides code for parallel downloads.
The Inconceivable Types of Rust: How to Make Self-Borrows Safe
The article addresses Rust's limitations on self-borrows, proposing solutions like named lifetimes and inconceivable types to improve support for async functions. Enhancing Rust's type system is crucial for advanced features.