# Networking (/docs/guides/vps/networking)

> How VPS instances get addresses today, from one IPv4 range per hosted pool to what runtimes and providers report, and what is not built yet.



A VM is not much use until you know its address. Better Compute keeps networking deliberately small: an address
plan per hosted pool, and whatever the runtime or the provider reports. Software-defined networking beyond bridges,
VLANs and IPAM is a non-goal for v1, as are volumes and live migration.

Where an instance's addresses come from depends on its pool:

| Pool                        | Addresses                                                                                                |
| --------------------------- | -------------------------------------------------------------------------------------------------------- |
| Hosted (`vps-host`)         | One IPv4 lease from the pool's range, taken at placement, plus any addresses the host's runtime reports. |
| Hosted, no range configured | Only what the runtime reports.                                                                           |
| Provider (`provider`)       | What the provider reports for the machine, replaced on every poll.                                       |

## Give a hosted pool a range [#give-a-hosted-pool-a-range]

Pool IPAM is one IPv4 range per hosted pool. You configure it with `setIpPool`, a function exported by
`@better-compute/vps`. There is no API method, CLI command or FleetConfig field for it in this release, so run it
from your own setup code against the compute database:

```ts title="setup-ipam.ts"
import { setIpPool } from '@better-compute/vps';

const range = await compute.context.sql.transaction((tx) =>
  setIpPool(tx, { poolId: 'pool_01jz…', cidr: '10.0.0.0/24', gateway: '10.0.0.1' }),
);
// → { poolId: 'pool_01jz…', cidr: '10.0.0.0/24', gateway: '10.0.0.1',
//     firstHost: '10.0.0.2', lastHost: '10.0.0.254' }
```

<TypeTable
  type="{
  poolId: { type: 'string', description: 'The hosted pool.', required: true },
  cidr: { type: 'string', description: 'The IPv4 network, such as 10.0.0.0/24. IPv6 ranges are refused.', required: true },
  gateway: { type: 'string', description: 'The gateway address. It is never leased, even when it falls inside the range.', required: true },
  firstHost: { type: 'string', description: 'The first address to lease.', default: 'the address after the gateway' },
  lastHost: { type: 'string', description: 'The last address to lease.', default: 'the address before the broadcast address' },
  dns: { type: 'string[]', description: &#x22;DNS servers for the guests (ip_pools.dns, migration vps/0019_ip_pool_dns; 387c22f). Empty: guests keep their image's resolvers.&#x22;, default: '[]' },
}"
/>

`firstHost` and `lastHost` must both lie inside `cidr`, with `firstHost` not after `lastHost`. Anything else,
including a malformed address, is `INVALID_INPUT` naming the pool. Because the default `firstHost` is the address after
the gateway, set it explicitly when the gateway sits at the end of the network (`10.0.0.254` in a `/24`).

Calling `setIpPool` again for the same pool replaces the range. Existing leases are kept as they are, even if they now
fall outside it.

## How leases work [#how-leases-work]

A hosted instance takes its lease in the same transaction that places it, right after capacity claims its resources on
a host (see [instances](/docs/guides/vps/instances)):

* The lease is the lowest free address from `firstHost` to `lastHost`, skipping the gateway.
* An instance holds at most one lease. Placing an instance that already holds one returns the same address.
* `(pool, address)` is the primary key of `ip_leases`, so two instances can never hold the same address. Concurrent
  placements that race for one address retry, up to 8 times, on the next free one.
* When the range is exhausted, placement fails with `CAPACITY_UNAVAILABLE` ("Pool … has no free IP address"). In
  `instances.create` the whole transaction rolls back, host claims included, and the caller gets the error. When the
  reconciler places an instance later (after a host loss), the instance moves to `error` with an `Unschedulable`
  condition.
* A pool without a range leases nothing, and placement succeeds without an address.

The lease is returned when the instance is deleted. After a host loss with `recreateOnHostLoss`, the instance returns
its lease with its allocation and takes a new one, again the lowest free address, when it is placed on another host.

For example, with `cidr: '10.0.0.0/29'` and `gateway: '10.0.0.1'`, the range is `10.0.0.2` to `10.0.0.6`: five
instances get `.2` to `.6` in order, and a sixth create fails with `CAPACITY_UNAVAILABLE` until one of them is deleted.

Operators can see the plan and the leases in the database:

```sql
SELECT cidr, host(gateway) AS gateway, host(first_host) AS first, host(last_host) AS last
  FROM ip_pools WHERE pool_id = $1;

SELECT host(address) AS address, instance_id
  FROM ip_leases WHERE pool_id = $1 ORDER BY address;
```

The v0.3 acceptance run (`tests/vps-churn.test.ts`) checks that after 200 instances churn on 3 hosts there is exactly
one lease per placed instance, none leaked and none shared.

## Addresses on an instance [#addresses-on-an-instance]

`Instance.addresses` starts with the lease, if any. After that:

* **Hosted pools.** Addresses in the host's `StatusReport.instances` (from the guest agent or DHCP leases, CIDR form
  on the wire) are added to the list. Reports only add; the lease stays first.
* **Provider pools.** Each poll replaces the list with the machine's addresses. Hetzner Cloud reports the public IPv4
  address, the public IPv6 network as Hetzner gives it (a `/64`), and private network addresses. Proxmox VE reports
  the guest agent's addresses while the VM runs, without loopback and link-local ones, and nothing without an agent.
  Proxmox clones are configured with `ipconfig0=ip=dhcp`, so the guest takes its address from your cluster network's
  DHCP.

## What reaches the host [#what-reaches-the-host]

A hosted instance's network reaches its host in `InstanceSpec.nics`, in the `instances` section of the
NodeSpec. vps sends one NIC per instance:

| Field         | Value                                                                                                         |
| ------------- | ------------------------------------------------------------------------------------------------------------- |
| `network`     | The host bridge: `br0`, or `vps({ hostNetwork })`                                                             |
| `mac`         | A stable locally administered MAC: `02` followed by the first 10 hex digits of the SHA-256 of the instance id |
| `addresses`   | The IPAM lease in CIDR form (the address with the range's prefix length), or none without a lease             |
| `gateway`     | The range's gateway, for a NIC with a lease (387c22f)                                                         |
| `nameservers` | The range's `dns`, for a NIC with a lease, when it has any (387c22f)                                          |

**The guest's network configuration.** Since 0791fc9 bcd writes a cloud-init `network-config` (version 2) into the
NoCloud seed of `libvirt-kvm` and `cloud-hypervisor` guests when a NIC has static configuration: each such NIC matched
by its MAC, its addresses static, a default route (`0.0.0.0/0`, or `::/0` for an IPv6 gateway) through its gateway, and
its nameservers. A NIC without addresses keeps DHCP, and with no static NIC at all bcd writes no `network-config`, so
the image's own fallback (DHCP on the first NIC) stays. bcd parses every value and quotes it again, so a malformed
lease fails the seed rather than adding YAML to it. A bcx runtime plugin gets each NIC's gateway and nameservers in its
spec.

The NodeSpec encoder keeps NIC order, since it is the guest's interface order. The spec also carries the instance's
`allocationId`, which the host names when it fetches the user data. The FakeDaemon's fake hypervisor creates a tap
per NIC. On a host build with `virsh`, bcd's `libvirt-kvm` runtime (362317c) attaches each NIC as a virtio device on
the named bridge with the spec's MAC, on a tap named `runtime.TapName`, so bcd's nftables rules for the NIC exist
before the tap does. bcd's `cloud-hypervisor` runtime (2598f0a) creates a persistent tap of the same name
(`ip tuntap add`), puts it on the named bridge and brings it up before the VMM starts, then gives it to the VM with
the spec's MAC (933c51b). A guest reboot reopens that same tap, a tap that cannot be attached means the VM does not
start, a NIC without a network is refused, and the taps are deleted when the VM stops or is destroyed. Both have been
tested against fake tools only, not on a real host. A VPS profile can already declare `network: { mode: 'bridge', ipam: 'pool' }`
(see [steps and profiles](/docs/extending/steps-and-profiles)), but `vps()` does not read profiles.

### bcd's nftables tables [#bcds-nftables-tables]

Every instance tap is named `bc<10 hex digits>n<nic>` (`runtime.TapName`), and bcd owns three nftables tables, which
it replaces together in one transaction before any domain starts:

| Table                  | Holds                                                                                                                                                                                                                                                                                                                                                                            |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bridge bcd_instances` | The per-tap rules: anti-spoofing (the NIC's MAC and addresses), the `private` and `none` network policies, sandboxes' SSH-from-the-host-only rule, and the drops every tap gets (DHCP server replies, router advertisements and redirects, the host's own addresses). See [sandbox networks](/docs/guides/vps/sandboxes#network)                                                 |
| `inet bcd_taps`        | A backstop (933c51b): `iifname "bc*" drop` on the input and forward hooks, and `oifname "bc*" drop` on forward and (since b12c217) output. Frames of a tap that is on its bridge reach these hooks as the bridge's, so only a tap off its bridge, which the bridge table cannot see, is dropped here. With no instance NIC on the host the table is deleted and not loaded again |
| `arp bcd_taps`         | The same backstop for ARP, which the inet hooks never see (b12c217): `iifname "bc*" drop` on input and `oifname "bc*" drop` on output. Present exactly when `inet bcd_taps` is                                                                                                                                                                                                   |

A NIC on a bridge whose name starts with `bc` would match every tap rule, so it fails its instance. bcd's 5-second
check counts the rules of all three tables against what it loaded, and under
[privilege separation](/docs/daemon#privilege-separation) the root helper accepts a ruleset that names these tables
and no other.

What is not there at all: IPv6 ranges, more than one range or lease per instance, and an API to manage ranges. Tunnels
give you exec, SSH and single-port TCP access without routing to the instance network; see [tunnels](/docs/guides/access/tunnels).
