bytecodealliance / bytecodealliance/wasmtime

WASI: Fine-grained network policies

Open
#7,681 8 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
18.6k
Forks
1.8k
Avg merge
1d 19h
Merged PRs (30d)
121

Description

#7662 is a good first step towards letting users of the wasmtime library customize network behavior. After that change, library users have two options: either use `inherit_network` which grants access to everything, or use `socket_addr_check` which allows the user to define arbitrarily complex rules. The upside of that last one is also its downside: the user _must_ define everything themself. On the command-line, the available choices are quite limited: everything or nothing at all. Guess which one users will pick :P

I would like to add a middle ground option for both library & CLI users. This option should provide a default "good enough" option for the majority of users, while still allowing to fall back to fully customized control. My objective is to be able to declare network policies based on:
- Domain name
- Initiator (client vs server)
- Protocol (TCP, UDP, HTTP(S))

I tried to capture the gist of it in pseudo code:

```rs
// A single network permission
enum Grant {

// Allow TCP sockets to connect to remote_host/port, optionally using a specific local_interface/port
TcpOutbound {
remote_host: RemoteHostPattern,
remote_port: RemotePortPattern,
local_interface: LocalInterfacePattern, // default = LocalInterfacePattern::Any
local_port: LocalPortPattern, // default = LocalPortPattern::Ephemeral
},

// Allow TCP sockets to listen on a local port, optionally using a specific local_interface too
TcpInbound {
local_port: LocalPortPattern,
local_interface: LocalInterfacePattern, // default = LocalInterfacePattern::Any
},

// Allow UDP sockets to initiate flows to remote_host/port, optionally using a specific local_interface/port
UdpOutbound {
remote_host: RemoteHostPattern,
remote_port: RemotePortPattern,
local_interface: LocalInterfacePattern, // default = LocalInterfacePattern::Any
local_port: LocalPortPattern, // default = LocalPortPattern::Ephemeral
},

// Allow UDP sockets to handle incoming flows on a local port, optionally using a specific local_interface too
UdpInbound {
local_port: LocalPortPattern,
local_interface: LocalInterfacePattern, // default = LocalInterfacePattern::Any
},

// Allows the component to make outgoing HTTP connections
HttpOutbound {
scheme: Http | Https,
host: RemoteHostPattern,
port: RemotePortPattern,
},
}

// "*" -> RemoteHostPattern::Any
// "localhost" -> RemoteHostPattern::Loopback
// "example.com" -> RemoteHostPattern::Domain(DomainPattern::Single("example.com"))
// "*.example.com" -> RemoteHostPattern::Domain(DomainPattern::Wildcard("example.com"))
// "192.0.2.0" -> RemoteHostPattern::Ip(IpRange("192.0.2.0".into())
// "192.0.2.0/24" -> RemoteHostPattern::Ip(IpRange("192.0.2.0/24".into())
enum RemoteHostPattern {
Any,
Loopback, // Effectively `127.0.0.0/8` and `::1`, but without committing to a specific address family.
Ip(IpRange),
Domain(DomainPattern),
}

// "*" -> RemotePortPattern::Range(1..=u16::MAX))
// "0" -> invalid
// "80" -> RemotePortPattern::Range(80..=80))
// "35000-35999" -> RemotePortPattern::Range(35000..=35999))
enum RemotePortPattern {
Range(PortRange),
}

// "*" -> LocalInterfacePattern::Any
// "localhost" -> LocalInterfacePattern::Loopback
// "192.0.2.0" -> LocalInterfacePattern::Ip(IpRange("192.0.2.0".into())
// "::" -> LocalInterfacePattern::Ip(IpRange("::".into())
enum LocalInterfacePattern {
Any, // Effectively `0.0.0.0` and `::`, but without committing to a specific address family.
Loopback, // Effectively `127.0.0.0/8` and `::1`, but without committing to a specific address family.
Ip(IpRange),
}

// "*" -> LocalPortPattern::Range(0..=u16::MAX))
// "0" -> LocalPortPattern::Ephemeral
// "80" -> LocalPortPattern::Range(80..=80))
// "35000-35999" -> LocalPortPattern::Range(35000..=35999))
enum LocalPortPattern {
Ephemeral,
Range(PortRange),
}

type PortRange = RangeInclusive; // Can also represent single ports by storing an identical `start` and `end` port.

struct IpRange(ipnet::IpNet); // Can also represent a single address

struct Domain(String);

enum DomainPattern {
Single(Domain),
Wildcard(Domain), // Allows the domain itself, along with every subdomain.
}

```

## Domain-based policy strategy

THe IP and Port-based patterns above should speak for themselves. The domain name policies might need to explanation: the idea is to hook into `ip-name-lookup::resolve-addresses` to keep track of which IP address belongs to which domain names at runtime:
- In `ip-name-lookup::resolve-addresses`:
- Before making the syscall: validate that any TcpOutbound or UdpOutbound grant exists with a `Any` or matching `Domain` host pattern.
- After making the syscall: if the previous step matched any `Domain`-based grants: register the resolved addresses in `DynamicPolicy::resolved_names` (see below)
- In `tcp-socket::bind`: validate that any TcpOutbound or TcpInbound grant exists with a matching `local_interface` and `local_port`
- In `tcp-socket::connect`, validate that any TcpOutbound grant matches the `local_interface` & `local_port`. Also match the `remote_host` & `remote_port`:
- first by the IP address passed to the connect call. If none found:
- then by all `resolved_names` for that IP.

```rs
pub struct DynamicPolicyConfig { // Shared across many component instances.
grants: Vec,
}

pub struct DynamicPolicy { // Instantiated once per component
// Reference to the "static" rules:
config: Arc,

// Mapping between resolved IP addresses and the queried domain names.
resolved_names: LruCache>,

// (Recently) active UDP flows
udp_flows: LruCache<(/*local*/SocketAddr, /*remote*/SocketAddr), ()>
}
```

## UDP directionality

UDP has no traditional notion of "client" and "server". However, in practice many UDP applications do fit that model. I've modeled the grant types above based on what stateful firewall do. In order to know the directionality (inbound vs outbound) we need to keep track of "who talked first".

## CLI syntax

Inbound syntax:

```
--expose 80 // Grant::TcpInbound(...) & Grant::UdpInbound(...) // Shorthand inspired by Docker
--expose 127.0.0.1:80 // Grant::TcpInbound(..., local_interface: "127.0.0.1") & Grant::UdpInbound(..., local_interface: "127.0.0.1")
--expose udp://127.0.0.1:80 // Grant::UdpInbound(..., local_interface: "127.0.0.1")
```

Outbound syntax:

```
--connect tcp://example.com:80 // Grant::TcpOutbound(...) & Grant::HttpOutbound(...)
--connect udp://192.168.0.1:80 // Grant::TcpOutbound(...)
--connect http://*.example.com/ // Grant::HttpOutbound(...)
--connect https://example.com/ // Grant::HttpOutbound(...)
```

Let me know what you think.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.