blob: 5268fb9eaf41421c4bcb397205c6dd9e6cde2ebd [file] [log] [blame] [view]
# Dual-stack sockets
The Netstack3 core crate exposes IPv4 and IPv6 UDP and TCP sockets to bindings,
which uses them to provide a POSIX-compatible socket API. For IPv6 sockets, that
means supporting communication with IPv6 hosts, and also IPv4 hosts via
[IPv4-mapped IPv6] addressing.
## What does "dual-stack" mean?
IETF [RFC 3493] specifies how the POSIX socket API was extended to support
IPv6 sockets. IPv6 sockets that do not have the `IPV6_V6ONLY` option enabled
have "dual-stack" behavior: they can be used to communicate with IPv4 hosts or
with IPv6 hosts.
Not all network socket protocols are capable of dual-stack behavior. Netstack3
supports dual-stack operation only for TCP and UDP, but not for ICMP or raw IP
sockets.
## How dual-stack behavior is implemented
Netstack3 core exposes socket functionality to bindings via functions that are
templated over the IP version. These are implemented internally by dispatching
to handler and context traits that are implemented generically for both IP
versions. Even though only IPv6 sockets are capable of dual-stack operation,
preserving the IP-generic implementation of socket protocol functionality is
useful for reducing code duplication.
To implement behavior generically, we move the question of whether an
`[IP version, socket type]` supports dual-stack operation into the type system.
That lets us use Rust's exhaustive enum matching to ensure that all possible
cases are handled. There are a couple techniques that make this possible:
1. Exposing the variability of dual-stack/non-dual-stack support as an enum from
a `dual_stack_context` method provided by a context trait. This allows us to
write IP-generic socket handler code that matches on the result of the method
and handles both cases.
2. Provide dual-stack-specific and non-dual-stack-specific behavior behind
traits. Instances of types that implement these traits are returned in the
enum variants from the `dual_stack_context` method. This means that socket
handler code has access to additional functionality in the `match` branches
for dual-stack-capable and non-dual-stack-capable sockets.
3. Use uninstantiable types for associated types that will never be constructed.
The `dual_stack_context` trait method from above returns a notional
`Either<Self::DualStackContext, Self::NotDualStackContext>` which can contain
an instance of one or the other associated types. For a given
`[IP version, socket type]`, the choice is fixed and knowable at compile time
- only one of these types is ever instantiated, so the other can be
uninstantiable! That allows the compiler to optimize out branches in
monomorphized code, making the abstraction free in terms of performance. It's
also [trivial][UnreachableExt] to implement trait methods on uninstantiable
types (assuming the trait methods take `self`, `&self` or `&mut self`).
Putting these together, we get something like the following:
```
use core::convert::Infallible as Never;
trait StateContext<I: IpExt> {
type DualStackContext: DualStackStateContext<I>;
type SingleStackContext: SingleStackStateContext<I>;
fn dual_stack_context(&mut self)
-> Either<&mut Self::DualStackContext, &mut Self::SingleStackContext>;
}
trait DualStackStateContext<I: IpExt> { /* ... */ }
trait SingleStackStateContext<I: IpExt> { /* ... */ }
impl StateContext<Ipv4> for &SyncCtx {
type DualStackContext = Never; // uninstantiable
type SingleStackContext = Self;
fn dual_stack_context(&mut self)
-> Either<&mut Self::DualStackContext, &mut Self::SingleStackContext> {
Either::Right(self)
}
}
impl StateContext<Ipv6> for &SyncCtx {
type DualStackContext = Self;
type SingleStackContext = Never; // uninstantiable
fn dual_stack_context(&mut self)
-> Either<&mut Self::DualStackContext, &mut Self::SingleStackContext> {
Either::Left(self)
}
}
// Real implementations that access state.
impl DualStackStateContext<Ipv6> for &SyncCtx { /* ... */ }
impl SingleStackStateContext<Ipv4> for &SyncCtx { /* ... */ }
// Implementations on uninstantiable types with unreachable method impls.
impl<I: IpExt> DualStackStateContext<I> for Never { /* ... */ }
impl<I: IpExt> SingleStackStateContext<I> for Never { /* ... */ }
```
## Holding dual-stack state
Some socket state types are different for a particular dual-stack-capable
protocol (UDP or TCP) depending on the IP version. For example, the state for a
bound IPv4 socket should hold only an `Option<Specified<Ipv4Addr>>` as the bound
address. An IPv6 socket, however, can be bound to either an assigned IPv6
address or an IPv4-mapped IPv6 address (or to the unspecified address), and the
type for storing the address should reflect the additional possibility.
The tricky part is that this bound address will comprise one field in an
IP-generic "bound state" type. We can model the IP variability by declaring an
associated type on an IP extension trait and using that in IP-generic code. So a
bound socket state might look like this:
```
struct BoundSocketState<I: IpExt> {
bound_addr: I::BoundAddr
}
trait IpExt: Ip {
type BoundAddr;
}
impl IpExt for Ipv4 { type BoundAddr = Option<Specified<Ipv4Addr>>; }
impl IpExt for Ipv6 { type BoundAddr = DualStackBoundAddr<Ipv6>; }
```
This allows us to store the state but not to easily access it, since IP-generic
code sees only `I::BoundAddr`, not the possible concrete types. To enable access
to the concrete types, we provide IP-generic code access to infallible
conversion routines via the same dual-stack and non-dual-stack context traits
from earlier. This lets IP-generic code convert IP-generic types like
`I::BoundAddr` into their dual-stack or non-dual-stack versions. Since the
conversions are implemented on the same types returned by the
`dual_stack_context` trait method, it's only possible to perform a conversion
once the question of dual-stack or non-dual-stack operation is already known.
Extending the above example:
```
trait DualStackContext<I: IpExt> {
fn to_dual_stack_bound_addr(&self, addr: I::BoundAddr)
-> DualStackBoundAddr<I>;
}
trait SingleStackContext<I: IpExt> {
fn to_single_stack_bound_addr(&self, addr: I::BoundAddr)
-> Option<Specified<I::Addr>>;
}
```
These conversion routes are trivially implementable for uninstantiable types
(see above) and should be effectively pass-through methods on concrete
dual-stack and non-dual-stack context trait implementations:
```
impl DualStackContext<Ipv6> for &SyncCtx {
fn to_dual_stack_bound_addr(&self, addr: Ipv6::BoundAddr)
-> DualStackBoundAddr<Ipv6> {
// Ipv6::BoundAddr is definitionally DualStackBoundAddr<Ipv6>.
addr
}
}
impl SingleStackContext<Ipv4> for &SyncCtx {
fn to_single_stack_bound_addr(&self, addr: Ipv4::BoundAddr)
-> Option<SpecifiedAddr<Ipv4Addr>> {
// Like above, Ipv4::BoundAddr is definitionally the output type.
addr
}
}
```
[IPv4-mapped IPv6]: https://en.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses
[RFC 3493]: https://datatracker.ietf.org/doc/html/rfc3493
[`core::convert::Infallible`]: https://doc.rust-lang.org/std/convert/enum.Infallible.html
[UnreachableExt]: https://fuchsia-docs.firebaseapp.com/rust/explicit/trait.UnreachableExt.html