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
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:
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' }Prop
Type
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
A hosted instance takes its lease in the same transaction that places it, right after capacity claims its resources on a host (see instances):
- The lease is the lowest free address from
firstHosttolastHost, 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 ofip_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"). Ininstances.createthe 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 toerrorwith anUnschedulablecondition. - 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:
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
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 withipconfig0=ip=dhcp, so the guest takes its address from your cluster network's DHCP.
What reaches the host
A hosted instance's network reaches its host in InstanceSpec.nics, in the instances section of the
. 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), but vps() does not read profiles.
bcd's 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 |
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 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.
Better Compute is created by Sean Filimon
Last updated
Providersbeta
Provider accounts, the defineProvider contract, the Hetzner Cloud and Proxmox VE providers, conformance cases, and secret references for credentials.
Bare metalexperimental
Enroll machines by their BMCs, commission them over PXE, deploy signed images and wipe them on release, all driven by one provision state machine.