Documentation
¶
Overview ¶
Package tap manages the per-host pool of (TAP-device, host-IP, guest-IP, /30-CIDR, vsock-CID) slots that Firecracker microVMs claim at create time. The state lives in SQLite (firecracker_tap_pool table); this package owns the seed + allocation policy and exposes a typed API over the raw store CRUD.
What lives here vs. internal/store:
internal/store: schema, raw CRUD, partial unique index, idempotency primitives. The load-bearing transactional substrate. Touching this triggers pr-review.md §5 (TCP-host-port-pool & L4-bootstrap fragility class) — the indexes are the contract.
this package: policy on top of those primitives. Computing the next /30 from a base CIDR, picking the next vsock CID, naming TAP devices ("fctapN"), validating SeedConfig. No SQL, no transactions of its own — the store is the boundary.
What does NOT live here yet: actual host-side TAP device manipulation (`ip link add fctap0 type tap`, `ip addr add 172.16.0.1/30 dev fctap0`, `ip link set fctap0 up`). The first cut wires only the *allocation bookkeeping* — the act of creating the slot row and reserving the IP. Real `ip` invocations are the runtime driver's job and land in a follow-up file (`tap/host.go`) once Driver.Create needs them. Keeping allocation separate from realization mirrors the host-port pool (allocation in store; bind() in caddy-l4) and lets each step be tested in isolation.
Index ¶
- type Host
- type HostManager
- type Pool
- func (p *Pool) Allocate(ctx context.Context, sandboxID string, now time.Time) (*Slot, error)
- func (p *Pool) Get(ctx context.Context, sandboxID string) (*Slot, error)
- func (p *Pool) Release(ctx context.Context, sandboxID string) error
- func (p *Pool) Seed(ctx context.Context, cfg SeedConfig, now time.Time) error
- func (p *Pool) Stats(ctx context.Context) (Stats, error)
- func (p *Pool) Transfer(ctx context.Context, fromID, toID string, now time.Time) (*Slot, error)
- type SeedConfig
- type Slot
- type Stats
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Host ¶
type Host struct {
// IPCmd is the absolute path to the `ip` binary. Empty defaults to "ip"
// (resolved from $PATH). Operators with a hardened iproute2 install
// (e.g. /usr/sbin/ip not in the daemon's PATH) point this at it.
IPCmd string
// contains filtered or unexported fields
}
Host is the production HostManager. It shells out to /sbin/ip (or whichever IPCmd points at on this host). All methods are concurrency- safe — `ip` does its own kernel-side serialization, and the methods hold no per-host state.
func NewHost ¶
NewHost constructs a Host with the given ip-binary path. Empty path means "use `ip` from $PATH". Most callers leave it at "" because the daemon's launchd/systemd unit already has /usr/sbin on PATH.
func (*Host) Ensure ¶
Ensure creates the TAP device, assigns the host-side /30 IP, brings the link up, and pins the guest's L2 neighbor entry. Each step is idempotent on its own; the composition is idempotent overall because each step recognizes the "already in desired state" output and treats it as success.
Order matters:
- `ip tuntap add` first — we need the device to exist before `ip addr add` can target it.
- `ip addr add` next — putting the IP on first means the link is usable the moment step 3 makes it up. Reverse order would race against the guest's DHCP/static-IP probe.
- `ip link set ... up` — flipping IFF_UP is what the guest's virtio-net device sees.
- `ip neigh replace` last — host-to-guest connects should not depend on ARP discovery during Firecracker's early boot window.
Rollback on partial failure: if step 2 or 3 fails we DO NOT call Remove. The caller is expected to call Remove from its error-cleanup path (the firecracker driver's deferred-release runs it), which keeps the "single owner of cleanup" rule pr-review.md §4 describes. Mixing rollback in here would mean a Destroy after a failed Ensure double-Removes — harmless on idempotent Remove, but makes the failure log noisier.
type HostManager ¶
type HostManager interface {
// Ensure brings the slot's TAP device up. Idempotent: a call against a
// device that already exists with the right address is a no-op (no
// error). A call against a device that exists with the WRONG address
// surfaces an error — that's a state-mismatch the operator needs to
// see, not silently fix, because the wrong address may be in use by
// another sandbox.
Ensure(ctx context.Context, slot Slot) error
// Remove tears the slot's TAP device down. Idempotent: missing device
// returns nil. Used by Destroy and by the rollback-on-Create-failure
// path; both must be safe to retry.
Remove(ctx context.Context, tapName string) error
}
HostManager is the operations the firecracker runtime driver depends on for putting a Slot onto the host's network surface. Declared here as an exported interface so the driver can hold this type without importing internal/network/tap (driver tests would otherwise drag in the SQLite-backed pool just to satisfy the type).
Production implementation is *Host below. Tests inject a fake that records calls.
type Pool ¶
type Pool struct {
// contains filtered or unexported fields
}
Pool is the policy layer. Holds a *store.Store reference; the zero value is not usable.
func New ¶
New returns a Pool. The store must already be open. The Pool does not take ownership; the caller is responsible for the store's lifetime.
func (*Pool) Allocate ¶
Allocate claims a slot for sandboxID. Idempotent (re-Allocate for the same sandbox returns the existing slot, not a new one). Returns store.ErrNoFreeFirecrackerTapSlot when the pool is exhausted — the runtime driver translates that into a 503-ish admission error so the operator sees "pool exhausted" rather than a confusing timeout.
func (*Pool) Get ¶
Get returns the slot owned by sandboxID, or nil. Used by Inspect / Destroy paths. Errors from the underlying store bubble up untouched.
func (*Pool) Seed ¶
Seed lays out PoolSize slots from BaseCIDR. Idempotent: a re-Seed with the same config is a no-op (the underlying ON CONFLICT(tap_name) DO NOTHING in the store path handles this). A re-Seed with a DIFFERENT BaseCIDR or PoolSize will silently keep the original layout for any pre-existing tap_name rows and add the new ones — operators must migrate manually (drop + reseed) if they need to shrink or relocate the pool. This is by design: rewriting an in-use slot would orphan running sandboxes.
type SeedConfig ¶
SeedConfig describes the network plan the pool is laid out from. BaseCIDR is carved into /30 subnets, one per slot; PoolSize is the number of /30s to produce (and therefore the maximum number of concurrent Firecracker sandboxes per host). Both fields are required.
Why /30 specifically: a /30 holds 4 addresses — network, host-side TAP IP, guest IP, broadcast. That's the minimum that lets the host route to the guest. /31 (RFC 3021) would also work but is unsupported by some older `ip` tooling; /30 is the safe default.
type Slot ¶
type Slot struct {
TapName string
CIDR string
HostIP string
GuestIP string
VsockCID uint32
GuestMAC string
}
Slot is the Pool-level view of an allocated network slot. Mirrors store.FirecrackerTapSlot but lives in this package so callers (the Firecracker driver) depend on the policy layer's type, not the store's. SandboxID is always non-empty here — a Slot returned by the pool is always claimed. GuestMAC is an optional host-neighbor override used by snapshot restores whose guest NIC MAC is frozen in snapshot state.
type Stats ¶
type Stats = store.FirecrackerTapPoolStats
Stats reports current occupancy. Used by /healthz and the admission controller — a near-empty pool blocks new Firecracker creates upstream of the failing Allocate call.