| # Static Typing |
| |
| This document describes and motivates the use of static typing in netstack3. |
| |
| Netstack3 makes heavy use of static typing largely for the same reasons that any |
| program does - to catch bugs at compile time. But there are a number of benefits |
| to static typing that are more specific to our use case that are worth calling |
| out. |
| |
| ## Guaranteeing invariants and preventing denial-of-service attacks |
| |
| Often, there are invariants that we want to uphold which are not as simple as |
| type safety. For example, consider the act of setting the address and subnet on |
| an interface. Here's one way we might write a function to do this: |
| |
| ```rust |
| pub fn set_ip_addr(iface: Interface, addr: IpAddr, network: IpAddr, prefix: u8) { ... } |
| ``` |
| |
| In this example, we'll assume that `IpAddr` is either an IPv4 address or an IPv6 |
| address, just like the standard library's `std::net::IpAddr`. We can observe a |
| number of invariants which the caller must uphold: |
| - `addr` and `network` must be of the same IP version |
| - `prefix` must be a valid prefix length for this IP version (no more than 32 |
| bits for IPv4 or 128 bits for IPv6) |
| - `network` must only have its uppermost `prefix` bits set |
| - `addr` must be an address in the subnet identified by `network` and `prefix` |
| |
| Assuming we wanted to design the interface this way, the canonical way of |
| enforcing these invariants would be a) to document them in a doc comment and, b) |
| to verify them at runtime, panicking if they are not upheld. |
| |
| Now let's assume that `set_ip_addr` is called at the bottom of a long, |
| complicated sequence of function calls that originate with a request from a |
| local application client. How can we ensure that the invariants required by |
| `set_ip_addr` are bubbled all the way up through the application to the point of |
| accepting external input? This is a crucial question because, if we get this |
| wrong, a client of the netstack could pass invalid parameters, and have those |
| parameters make it to `set_ip_addr`, which would panic and crash the entire |
| netstack. |
| |
| To address this problem, we instead write `set_ip_addr` like this: |
| |
| ```rust |
| pub fn set_ip_addr<A: IpAddress>(iface: Interface, addr_subnet: AddrSubnet<A>) { ... } |
| ``` |
| |
| An `AddrSubnet` is a type which guarantees all of the invariants we listed |
| above. Instead of having to worry about documenting the invariants and hoping |
| that the caller upholds them, we can simply assume that, by virtue of the fact |
| that the caller has an `AddrSubnet` value, the invariants *must* be upheld. |
| |
| What's more, `AddrSubnet` has the following constructor: |
| |
| ```rust |
| impl<A: IpAddress> AddrSubnet<A> { |
| pub fn new(addr: A, prefix: u8) -> Option<AddrSubnet<A>> { ... } |
| } |
| ``` |
| |
| The caller must guarantee that `prefix` is valid for the IP version as described |
| above. However, instead of panicking if `prefix` is invalid, `new` simply |
| returns `None`. This forces the programmer to be cognizant of the potential for |
| error even if they didn't read the documentation. If `new` panicked on an |
| invalid `prefix`, we could write the innocuous code: |
| |
| ```rust |
| let (addr, prefix) = read_addr_prefix_from_client(); |
| let addr_subnet = AddrSubnet::new(addr, prefix); |
| ... |
| ``` |
| |
| To someone reading this code, it's not at all clear that there's a |
| denial-of-service waiting to happen if a client passes an invalid `prefix`. |
| Instead, with `new` returning an `Option`, this code becomes: |
| |
| ```rust |
| let (addr, prefix) = read_addr_prefix_from_client(); |
| let addr_subnet = AddrSubnet::new(addr, prefix).unwrap(); |
| ... |
| ``` |
| |
| The `unwrap` makes it clear what's happening, and will hopefully tip off a code |
| reviewer to the issue. |
| |
| There's one final benefit: Function authors are encouraged to take these types |
| as arguments, rather than the raw inputs from which the types are produced. This |
| has the effect of naturally pushing input validation as close to client-provided |
| input as possible, which is exactly where we want it. |
| |
| To summarize, using static typing to uphold invariants gives us the following benefits: |
| - It allows us to express our invariants in code rather than documentation, |
| making it much harder to violate them accidentally |
| - It provides us with types whose existence *proves* that the invariants are |
| upheld, meaning that any function accepting those types as arguments can |
| simply rely on the invariants being maintained by the caller |
| - As a result of the previous property, it becomes both trivial and natural to |
| perform input validation as close to the client as possible, leaving the core |
| of the application to simply operate on already-verified values |