This documents various design decisions, also to be read as _lessons learned_. At the time you read this they may, or may not, still be relevant. Apologies if I seem overly critical of any projects - I have huge respect for all of the dependencies I've tried. ## Error type Snafu seems to do the job for an error type. Initially the `Error` type had two 35 byte `UnknownName`s in one of the variants. Removing that reduced the arm thumb binary size by 16kB! The reason appears to be that when the error variant of `Result` is larger than the `Ok` item, it can't be used to construct a struct in-place (it has to write to temporary larger stack space, then copy the item out). That matters a lot for the `sshwire` deserialisation that recursively creates lots of structs. `.trap()` is there as an alternative to `.unwrap()` - if a server handles multiple connections you don't want them all going away panicking if there's some un-thought-of edge case. Each call to `.trap()` seems on the order of perhaps 100 bytes larger than a plain `panic!()` - perhaps there should be a feature to just panic. In debug builds it panics too so it's quick to get a backtrace. ## Serialisation/deserialisation Previously the code used `serde` with a custom `Serializer`/`Deserializer`. That worked fine for serializing and `derive(Deserialize)`, but deserializing enums was a pain. For types like `packets::ChannelRequest` the deserializer had to stash the channel request type string from the `ChannelRequest` struct somewhere, then use that later when deserialising the `ChannelReqType` enum. Other response packets like number 60 `Userauth60` mean completely different things depending on the previous request, but that state is hard to pass through the deserializer. [`serde_state`](https://docs.rs/serde_state/latest/serde_state/) solves some of it, but was still a bit awkward, and didn't work well with manually written `Deserialize` implementations. serde is fairly flexible design, but for our purposes we can use something much simpler - the packet format is not self describing (in most parts), we just take bytes off the wire exactly as we expect them. Eventually I gave up fighting and wrote a new `sshwire_derive` with [virtue](https://github.com/bincode-org/virtue) crate. That is about 10kB smaller (arm thumb) and easier to customise as required. For example `UnknownName` has a simple attribute to indicate that's the variant that should store an unmatched string. It was easier than I expected - the virtue syntax seems a bit verbose but feels more intuitive than `syn` or `quote` crates which are the alternative. I think this is because it's still written in normal Rust, not a new macro language to learn. ## Ring vs RustCrypto Initially the code was written using `ring`, mainly because it already had [`chacha20_poly1305_openssh`](https://docs.rs/ring/latest/ring/aead/chacha20_poly1305_openssh/index.html). That worked great until I tried to build for a `thumbv7em` platform. Some of the code wouldn't build (ARM assembly issues?, possibly fixable), but the bigger problem is there's no way to insert a custom random number generator. Instead switching to RustCrypto crates worked fairly easily, though perhaps a bit messier having to deal with `GenericArray` and `DynamicDigest` and types like that. At the time of writing `curve25519-dalek` etc are a bit behind on dependencies which is messy (the perennial problem of rust dependency traits), but I assume that will sort itself out. Edit: Now using Salty instaed of dalek, it's designed for microcontrollers. ## `Behaviour` (This is outdated, gave up on async `Behaviour` for the time being) At some points in packet handling some custom behaviour from the application is required. For example "is this hostkey valid?", "is this user's password correct?". Those need an immediate response, so `.await` fits well. The problem is that `async_trait` requires `Box`, won't work on `no_std`. The `Behaviour` struct has a `cfg` to switch between async and non-async traits, hiding that from the main code. Eventually `async fn` [should work OK](https://github.com/rust-lang/rust/issues/91611) in static traits on `no_std`, and then it can be unified. ## Async The majority of packet dispatch handling isn't async, it just returns Ready straight away. Because of that we just have a Tokio `Mutex` which occassionally holds the mutex across the `.await` boundary - it should seldom be contended.