Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 2 additions & 45 deletions .github/scripts/provision-kvm-host.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@
# the node)
# - DEBIAN_FRONTEND=noninteractive (install.md is written for an interactive
# operator session; CI has no tty to prompt on)
# - [network] is always written here (install.md presents it as an optional
# section) since the runner's default route interface is detected fresh
# each run and integration coverage wants egress networking exercised
set -euo pipefail

export DEBIAN_FRONTEND=noninteractive
Expand All @@ -34,20 +31,12 @@ sudo udevadm trigger --name-match=kvm

sudo apt-get update
sudo apt-get install -y \
coreutils e2fsprogs iproute2 libc-bin lvm2 nftables skopeo thin-provisioning-tools util-linux \
coreutils e2fsprogs libc-bin lvm2 skopeo thin-provisioning-tools util-linux \
"linux-modules-extra-$(uname -r)"

# -a is load-bearing: without it modprobe reads the 2nd+ names as module
# PARAMETERS of the first, and the load fails.
#
# nf_tables + nf_nat back host-init's `hyper` nftables table and its
# masquerade/SNAT/DNAT rules. A fresh GitHub-hosted runner does not have them
# loaded, and the netfilter family does not always autoload on first `nft` use,
# so `nft add table ip hyper` fails (empty stderr, non-zero exit) and the node
# refuses to start. Load the base explicitly, as we do for the dm modules; the
# nat expression sub-modules (nft_chain_nat, nft_masq) autoload once the base is
# present and a rule is issued.
sudo modprobe -av dm_snapshot dm_thin_pool loop nf_tables nf_nat
sudo modprobe -av dm_snapshot dm_thin_pool loop
targets="$(sudo dmsetup targets)"
echo "dmsetup targets: ${targets}"
grep -q thin-pool <<<"${targets}" || { echo "ERROR: thin-pool dm target missing" >&2; exit 1; }
Expand All @@ -59,33 +48,6 @@ command -v thin_dump >/dev/null || { echo "ERROR: thin_dump missing (thin-provis
sudo install -o root -g root -m 0755 "$(readlink -f "$(command -v thin_dump)")" /usr/local/sbin/thin_dump
[ ! -L /usr/local/sbin/thin_dump ] || { echo "ERROR: /usr/local/sbin/thin_dump is still a symlink" >&2; exit 1; }

# host-init (run by the node at start) asserts ip_forward=1 rather than
# setting it, so provisioning must turn it on and persist it across reboots.
sudo sysctl -w net.ipv4.ip_forward=1
echo 'net.ipv4.ip_forward=1' \
| sudo tee /etc/sysctl.d/99-hyper-ip-forward.conf >/dev/null

# GitHub-hosted runners ship Docker, which sets the iptables `filter` FORWARD
# policy to DROP and only admits its own bridge traffic. Hyper's guest egress is
# *forwarded* (per-VM netns veth -> uplink), so Docker's drop silently eats
# every packet the guest sends — the guest configures its NIC fine but nothing
# ever returns. Admit the clone pool (172.31.0.0/16, the helper's default) both
# directions via DOCKER-USER, which Docker evaluates before its own drops; the
# `hyper` nftables forward chain still enforces guest isolation. Fall back to an
# accepting FORWARD policy on a host without Docker's chain.
clone_pool="172.31.0.0/16"
if sudo iptables -L DOCKER-USER >/dev/null 2>&1; then
sudo iptables -I DOCKER-USER -s "${clone_pool}" -j ACCEPT
sudo iptables -I DOCKER-USER -d "${clone_pool}" -j ACCEPT
else
sudo iptables -P FORWARD ACCEPT
fi

# The default-route interface is the uplink guests NAT egress through.
uplink="$(ip route show default | awk '{print $5; exit}')"
[ -n "${uplink}" ] || { echo "ERROR: could not detect default-route uplink interface" >&2; exit 1; }
echo "detected uplink: ${uplink}"

sudo mkdir -p /etc/hyper
sudo tee /etc/hyper/config.toml >/dev/null <<EOF
work_dir = "/srv/hyper"
Expand All @@ -104,11 +66,6 @@ cgroup = "hyper"
# load is still decaying, and nothing else runs here worth protecting.
[budget]
cpu_max_load = 4.0

# Enables per-VM egress NAT. The nft table itself is owned by host-init
# (created idempotently at node start), not by this script.
[network]
uplink = "${uplink}"
EOF
sudo chown root:root /etc/hyper/config.toml
sudo chmod 0644 /etc/hyper/config.toml
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
value is now rejected with `INVALID_ARGUMENT`; `ListVms` is now paginated
(`page_size` / `page_token` / `next_page_token`).

### Removed
- **Breaking:** VM networking (per-VM netns + veth + TAP + nftables NAT egress,
and the guest `/etc/resolv.conf` from `hyper.resolver=`) has been removed
pending a ground-up rewrite. VMs boot with no NIC, a node no longer requires
or reads a `[network]` config table (an existing one is ignored), and the
`Hyper.SuidHelper.Network`, `Hyper.Node.FireVMM.Net` and `Hyper.Cfg.Network`
modules and the setuid helper's `network` subcommands remain as interfaces
but return/raise unimplemented.

## [0.1.0]

First public release: a distributed orchestrator for Firecracker microVMs on the
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ integrating Hyper.
call it from any language you already use.
⚠️ The gRPC API is unauthenticated and off by default — bind it to a trusted network or front it with an authenticating proxy. See [the gRPC guide](docs/grpc.md).

> **VM networking** is being rewritten from the ground up and is currently
> unavailable: VMs boot with no NIC and have no network egress.

## Docs

Full docs on getting started, as well as useful diagrams are available on
Expand Down
27 changes: 4 additions & 23 deletions docs/cookbook/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,29 +110,10 @@ uid_gid_range = [900000, 999999]

## VM Networking Configuration

**Required.** Every VM gets NAT'd egress out through a physical host interface,
and a node refuses to start without a `[network]` table (see the
[install guide](install.md#vm-networking-required) for the host prerequisites:
`iproute2`/`nftables` and `net.ipv4.ip_forward=1`). `config.toml`-only — the
setuid helper reads the same `uplink` and `clone_pool` to build each VM's netns,
veth pair, and NAT rules, so it cannot live in `config.exs`.

### `config.toml`

```toml
[network]
# **required**. The physical uplink interface guest egress is NAT'd out through.
# On a single-uplink host, the default-route NIC is the usual choice:
# ip route show default | awk '{print $5; exit}'
uplink = "eth0"

# optional -- defaults shown. The IPv4 CIDR the per-VM /30s are carved from.
clone_pool = "172.31.0.0/16"

# optional -- defaults shown. DNS resolver handed to guests via the kernel
# cmdline (the guest agent writes it to /etc/resolv.conf).
resolver = "1.1.1.1"
```
VM networking is being rewritten from the ground up and is currently
unavailable: VMs boot with no NIC, and a node no longer requires or reads a
`[network]` table. An existing `[network]` table in `config.toml` is accepted
but ignored (see the [install guide](install.md#vm-networking)).

## gRPC Configuration

Expand Down
69 changes: 5 additions & 64 deletions docs/cookbook/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,71 +193,12 @@ cgroup = "hyper"
For more details on configuring and tuning Hyper, we suggest you see the
[configuration guide](config.md).

### VM Networking (required)
### VM networking

VM networking is **mandatory**: a node refuses to start without a `[network]`
table in `/etc/hyper/config.toml`, and every VM gets NAT'd egress out through a
physical interface on the host. There is no opt-out — a node that fails this
preflight will not boot (`{:error, :network_not_configured}`, a missing uplink,
or IPv4 forwarding off). The `hyper` nft table itself is created (and owned) by
`host-init`, which runs automatically at node start — you never create nft rules
by hand. You must prepare the host first:

Install `iproute2` and `nftables`:

```sh
sudo apt install -y iproute2 nftables # dnf install -y iproute nftables
```

Enable IPv4 forwarding. `host-init` only *asserts* this is on — it refuses to
proceed rather than setting it for you:

```sh
sudo sysctl -w net.ipv4.ip_forward=1
```

> #### Persistent Config {: .warning}
>
> Loading this via `sysctl -w` is ephemeral and resets on reboot. Persist it:
>
> ```sh
> echo 'net.ipv4.ip_forward=1' \
> | sudo tee /etc/sysctl.d/99-hyper-ip-forward.conf
> ```

> #### Conflicting FORWARD firewall {: .warning}
>
> Guest egress is *forwarded* (per-VM netns veth → uplink). If the host already
> runs a firewall that defaults the `filter` FORWARD chain to DROP — Docker does
> this, and so do some hardened base images — those drops silently eat every
> guest packet: the guest configures its NIC correctly but nothing ever returns.
> Admit the clone pool through the firewall's user hook (Docker evaluates
> `DOCKER-USER` before its own drops); the `hyper` nft forward chain still
> enforces guest isolation:
>
> ```sh
> sudo iptables -I DOCKER-USER -s 172.31.0.0/16 -j ACCEPT
> sudo iptables -I DOCKER-USER -d 172.31.0.0/16 -j ACCEPT
> ```
>
> On a host without Docker, ensure nothing else sets the FORWARD policy to DROP.

Then set `uplink` to the physical NIC guests should NAT out through — the
default-route interface is a reasonable choice on a single-uplink host:

```sh
ip route show default | awk '{print $5; exit}'
```

```toml
[network]
# **required**. The physical uplink interface guests NAT egress through.
uplink = "eth0"

# optional, defaults shown
clone_pool = "172.31.0.0/16"
resolver = "1.1.1.1"
```
VM networking is being rewritten from the ground up and is currently
unavailable: VMs boot with no NIC, so guests have no egress. No `[network]`
table, `nftables`, `iproute2` or IPv4 forwarding is needed on the host. An
existing `[network]` table in `/etc/hyper/config.toml` is accepted but ignored.

### Cgroups

Expand Down
164 changes: 164 additions & 0 deletions docs/cookbook/networking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Networking

This document serves to document and illustrate networking in Hyper. This
document assumes you are relatively familiar with high-level networking
concepts. I have done my best to provide useful links for new and unfamiliar
terms.

## Goals

TODO

## Architecture Overview

```mermaid
flowchart TD
subgraph host-netns
subgraph if0
ipv4["ipv4 108.....54/23"]
ipv6["ipv6 2001:...:1a00/64"]
end

nat0{{"nat0"}}

veth1["vm-veth-1 10.200.0.1/30"]
veth2["vm-veth-2 10.200.0.5/30"]
end

subgraph host-vm1-netns
uplink1["uplink 10.200.0.2/30"]
nat1{{"vm1-nat"}}
tap1["tap0 172.16.0.1/30"]
end

subgraph host-vm2-netns
uplink2["uplink 10.200.2.2/30"]
nat2{{"vm2-nat"}}
tap2["tap0 172.16.0.1/30"]
end

subgraph vm1
subgraph vm1-netns
eth1["eth0 172.16.0.2/30"]
end
end

subgraph vm2
subgraph vm2-netns
eth2["eth0 172.16.0.2/30"]
end
end

eth1 --> tap1 --> nat1 --> uplink1 --> veth1 --> nat0
eth2 --> tap2 --> nat2 --> uplink2 --> veth2 --> nat0
nat0 --> ipv4
```

### VM Namespaces

Hyper allocates a single [network
namespace](https://man7.org/linux/man-pages/man7/network_namespaces.7.html) per
guest VM. The host has its own _host namespace_.

Namespaces serve to provide isolation per VMs. Namespaces prevent access to the
host's network devices as well as services running on `localhost` for
compromised firecracker instances.

## Outgoing IPv4

```mermaid
flowchart TD
subgraph host-netns
subgraph if0
ipv4["ipv4 108.....54/23"]
end

nat0{{"nat0"}}

veth1["vm-veth-1 10.200.0.1/30"]
veth2["vm-veth-2 10.200.0.5/30"]
end

subgraph host-vm1-netns
uplink1["uplink 10.200.0.2/30"]
nat1{{"vm1-nat"}}
tap1["tap0 172.16.0.1/30"]
end

subgraph host-vm2-netns
uplink2["uplink 10.200.2.2/30"]
nat2{{"vm2-nat"}}
tap2["tap0 172.16.0.1/30"]
end

subgraph vm1
subgraph vm1-netns
eth1["eth0 172.16.0.2/30"]
end
end

subgraph vm2
subgraph vm2-netns
eth2["eth0 172.16.0.2/30"]
end
end

eth1 --> tap1 --> nat1 --> uplink1 --> veth1 --> nat0
eth2 --> tap2 --> nat2 --> uplink2 --> veth2 --> nat0
nat0 --> ipv4
```

For outgoing [IPv4](https://en.wikipedia.org/wiki/IPv4) traffic, Hyper
allocates a single host
[NAT](https://en.wikipedia.org/wiki/Network_address_translation) per hypervisor
node in the host namespace. Hyper uses
[`nftables`](https://en.wikipedia.org/wiki/Nftables) to configure the NAT.

Namespaces are bridged through
[veth](https://man7.org/linux/man-pages/man4/veth.4.html) devices. Alongside
creating a namespace per VM, Hyper creates an IPv4 veth pair. One side of the
pair goes in the host namespace, and the other goes in the VM's namespace.

The veth domain is specified to be `10.200.0.0/16`. This allows for `16,384`
possible veth pairs. Each host side of the veth pair gets a `10.200.X.1+4*Y`
address, while the namespace side gets a `10.200.X.2+4*Y` address. For example:

| VM | Subnet | vm-veth | uplink |
|-------|-------------------|----------------|----------------|
| 1 | 10.200.0.0/30 | 10.200.0.1 | 10.200.0.2 |
| 2 | 10.200.0.4/30 | 10.200.0.5 | 10.200.0.6 |
| 64 | 10.200.1.0/30 | 10.200.1.1 | 10.200.0.2 |
| 16383 | 10.200.255.252/30 | 10.200.255.253 | 10.200.255.254 |

> #### Unavailable addresses {: .info}
>
> Please note that the `10.200.X.0+4*Y/30` and `100.200.X.3+4*Y/30` addresses
> are unavailable for use. The former represents the subnet address and the
> latter is the broadcast address, as per IP spec.
>
> These addresses are effectively useless.

An additional NAT is added in the VM's namespace with the express goal of
ensuring that the VM-local IP remains stable across VM pauses and restarts.

> #### Stable IPs {: .info}
>
> Stable IPs are useful. VMs may be configured to host services on a stable IP
> and if the IP changes from under the VM user, the user may find such behavior
> unexpected.

Finally, a [TAP](https://en.wikipedia.org/wiki/TUN/TAP) network device is used
to bridge into the firecracker VM. This is the only way we can add interfaces
into firecracker.

## FAQ

This section aims to answer some commonly-asked questions.

### Why not reverse HTTP proxy?

One of my goals with Hyper is to be able to support arbitrary
protocols---`ssh`, `quic`, `tcp`, `udp` and pretty much anything on top of
[IP](https://en.wikipedia.org/wiki/Internet_Protocol). If Hyper used a reverse
HTTP proxy, we would only be able to support HTTP and websockets, which implies
all the aforemnetioned protocols would not be supported.
Loading
Loading