wasi_net (wasm v0.1.0)

View Source

The network capability.

Write a grant to say which addresses and ports an instance may reach, and which it may accept on. It is evaluated here, in one place, with no sockets in sight, so you can test the decision directly rather than through a syscall.

#{net => #{connect     => [{tcp, ~"10.0.0.0/8", {8000, 8099}}],
           listen      => [{tcp, ~"127.0.0.1", 8080}],
           resolve     => allow,
           max_sockets => 32,
           timeout     => 30000}}

Write a rule as {Proto, Addr, Port}. Proto is tcp or udp. Addr is an IP tuple, a binary address, or a binary CIDR. Port is an integer, {Lo, Hi}, or any.

Leave out net and there is no network at all. Leave out connect and there is no outbound, leave out listen and there is no inbound, so #{net => #{}} grants nothing and is refused exactly as if you had left the key out.

You name addresses, never names

You cannot write a rule that says "example.com". A name would have to be resolved to be checked and resolved again to be used, and the two answers can differ. Naming addresses removes that window rather than narrowing it: sock_connect checks the literal address it was handed and passes the same parsed tuple to gen_tcp, so nothing between the check and the syscall can move the target. sock_getaddrinfo is a separate capability, and its answers get no authority from having been resolved.

IPv4-mapped IPv6

::ffff:127.0.0.1 reaches the same host as 127.0.0.1 on every stack, so a matcher comparing tuples would let it past a 127.0.0.0/8 grant. Every address is normalised out of the ::ffff:0:0/96 block before matching, and the normalised form is what the caller then connects to, so the address checked and the address used are the same one.

IPv4-compatible addresses (::1.2.3.4, the deprecated block) are not normalised. ::0.0.0.1 and ::1 are the same address, so normalising the block would make loopback ambiguous. They stay IPv6 and need an IPv6 rule.

What a rule does not do

Nothing is denied implicitly. ~"0.0.0.0/0" really does include link-local and cloud metadata addresses, and this module will not second-guess you. Name what you mean. See docs/security.md.

Summary

Types

A concrete peer, as the syscall layer knows it.

A parsed grant. none is no network at all.

What a socket call is asking to do.

Functions

Ask whether the grant permits this endpoint, for this kind of use.

The endpoints the host should open a listening socket on.

Parse the net value from a capability configuration.

How many sockets an instance may hold open at once.

Fold an IPv4-mapped IPv6 address onto the IPv4 address it reaches.

Parse a textual address. It takes everything inet:parse_address/1 does, and normalises the result.

Is sock_getaddrinfo granted?

How long a blocking socket operation may wait.

Types

endpoint()

-nominal endpoint() :: {tcp | udp, inet:ip_address(), 0..65535}.

A concrete peer, as the syscall layer knows it.

grant()

-nominal grant() ::
             none |
             #{connect := [rule()],
               listen := [rule()],
               resolve := boolean(),
               max_sockets := pos_integer(),
               timeout := timeout()}.

A parsed grant. none is no network at all.

kind()

-nominal kind() :: connect | listen.

What a socket call is asking to do.

Functions

allows/3

-spec allows(kind(), endpoint(), grant()) -> boolean().

Ask whether the grant permits this endpoint, for this kind of use.

connect and listen are separate lists and neither implies the other. An accepted connection is not checked against connect: inbound is not outbound, and you granted the listener it arrived on already.

bindable/1

-spec bindable(grant()) -> [endpoint()].

The endpoints the host should open a listening socket on.

Only a rule naming one address and one port describes something bindable. A CIDR or a port range is permission for the module to bind within it, which is the extension's business, not a socket the host can open on its behalf.

grant/1

-spec grant(term()) -> grant().

Parse the net value from a capability configuration.

It raises {bad_net_grant, Term} on anything it cannot read. This runs where the import map is built, in your own process, because a typo in a CIDR is a configuration error and you should hear about it as one rather than meeting it later as a refused connection.

max_sockets/1

-spec max_sockets(grant()) -> non_neg_integer().

How many sockets an instance may hold open at once.

normalise/1

-spec normalise(inet:ip_address()) -> inet:ip_address().

Fold an IPv4-mapped IPv6 address onto the IPv4 address it reaches.

You get everything else back unchanged, including the IPv4-compatible block: ::0.0.0.1 and ::1 are the same address, so folding that block would make loopback ambiguous.

parse/1

-spec parse(binary() | string()) -> {ok, inet:ip_address()} | error.

Parse a textual address. It takes everything inet:parse_address/1 does, and normalises the result.

resolves/1

-spec resolves(grant()) -> boolean().

Is sock_getaddrinfo granted?

timeout/1

-spec timeout(grant()) -> timeout().

How long a blocking socket operation may wait.