blob: c19d8ed40823ae734d79e8f1f4d90752c9d4379f [file] [log] [blame] [view]
# 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