Skip to main content

arcbox_vm_driver/
net.rs

1//! The second port in this crate: what a VM's NIC is attached to.
2//!
3//! [`GuestNetwork`] plans and builds the host side of a guest interface —
4//! an address, a TAP or equivalent, filters — and hands the driver a
5//! [`NicSpec`] to boot with. The Linux TAP adapter, the platform's IPv6
6//! dataplane, and the trivial MAC-minting networks behind the VZ/HV drivers
7//! all sit behind it. Cleanup after a VM is a two-step, token-guarded
8//! protocol ([`NetworkReconcile`]) because host forwarding state outlives
9//! the VM and must be confirmed gone before an address is reused.
10
11use std::net::IpAddr;
12
13use async_trait::async_trait;
14use serde::{Deserialize, Serialize};
15
16use crate::error::Result;
17use crate::spec::{MacAddr, NicSpec, VmId};
18
19/// Plans, builds, and tears down the host side of guest NICs.
20#[async_trait]
21pub trait GuestNetwork: Send + Sync {
22    /// Reserves an address and plans host resources for `vm`; nothing is
23    /// created on the host yet.
24    async fn reserve(&self, vm: &VmId, policy: NetworkPolicy) -> Result<NetworkLease>;
25
26    /// Creates the host side (TAP, filters) for `lease` and returns the NIC
27    /// the driver boots with.
28    async fn activate(&self, lease: &NetworkLease, mode: AttachMode) -> Result<NicSpec>;
29
30    /// Re-registers a lease whose guest is still running, and re-establishes
31    /// whatever host state died with the previous process.
32    ///
33    /// The third way a lease becomes live: a host that restarts while its
34    /// guests keep running replays its durable leases through this, then
35    /// declares the replay done ([`NetworkReconcile::replay_complete`]).
36    /// Unlike [`GuestNetwork::activate`] it must not create or re-address
37    /// the guest's interface — the link is live and carrying traffic.
38    ///
39    /// `mode` is what the lease was activated as, which the caller knows
40    /// and the network does not: it decides whether the guest holds the
41    /// pool address itself or the fixed invariant one.
42    ///
43    /// Not idempotent, unlike [`GuestNetwork::quarantine`]: a lease that is
44    /// already live, or whose address another live lease holds, is an
45    /// error. Two records claiming one address is an inconsistent replay,
46    /// and adopting either of them is a guess.
47    ///
48    /// Fallible on purpose, and the failure is not fatal to the caller: an
49    /// adapter that cannot re-establish says so, and the owner falls back
50    /// to tearing the VM down — the behavior it had before adoption
51    /// existed. Hence no default body: an adapter that silently claimed
52    /// adoption worked would leave a live guest unreachable, which is
53    /// strictly worse than that fallback.
54    async fn adopt(&self, lease: &NetworkLease, mode: AttachMode) -> Result<()>;
55
56    /// Tears the host side down but keeps the address reserved until the
57    /// cleanup protocol confirms host forwarding state is gone. Idempotent.
58    async fn quarantine(&self, lease: NetworkLease) -> Result<()>;
59
60    /// Returns the address to the pool outright, skipping quarantine.
61    async fn release(&self, lease: NetworkLease) -> Result<()>;
62
63    /// Withholds `address` from this process's pool without creating,
64    /// claiming or touching any host state.
65    ///
66    /// For durable state a restart could not read: nothing can name that
67    /// record's lease through this port, so nothing can adopt, quarantine
68    /// or release it — yet its guest may still be running on that address
69    /// with its interface up. Handing the address to the next VM would
70    /// take that interface out from under the guest, since
71    /// [`GuestNetwork::activate`] replaces a device of the same name, and
72    /// forwarding rules installed for the previous occupant outlive the
73    /// process that installed them. So the address is held for this
74    /// process's lifetime instead.
75    ///
76    /// Deliberately neither a lease nor a quarantine: it cannot be listed
77    /// by [`NetworkReconcile::pending_cleanups`], finalized, or released,
78    /// because nothing can prove whose it is. An adapter whose pool does
79    /// not contain `address` has nothing to withhold and does nothing.
80    ///
81    /// Infallible and idempotent, unlike everything above it: this is what
82    /// a caller reaches for once every richer answer has already failed,
83    /// and "could not hold it" would leave that caller nothing to do.
84    fn hold_address(&self, address: IpAddr);
85
86    /// The network as the guest sees it under `mode`: what goes on the
87    /// kernel command line or into a net-reconfigure command.
88    ///
89    /// The mode is the caller's, not the lease's: a lease is reserved
90    /// before anyone knows how it will be attached, and the same lease
91    /// reads differently once activated as [`AttachMode::LegacySnapshot`]
92    /// (the guest owns the pool address) than as [`AttachMode::Invariant`]
93    /// (the address is the host's, translated per interface). Pass what
94    /// [`GuestNetwork::activate`] was given.
95    fn identity(&self, lease: &NetworkLease, mode: AttachMode) -> NetworkIdentity;
96
97    /// How host-side forwarding reaches this guest: the address DNAT must
98    /// target, and any packet mark the datapath needs to see.
99    ///
100    /// Decided by the datapath that actually applied to this lease, not by
101    /// the policy that was asked for — an adapter that falls back at
102    /// activation time reports the fallback's answer here. It is therefore
103    /// not derivable from [`GuestNetwork::identity`] plus the lease: a
104    /// network that gives every guest the same address translated per
105    /// interface has the host target the *lease's* address while the guest
106    /// holds the fixed one, which is the exact inverse of what those two
107    /// values say.
108    ///
109    /// A read, like [`GuestNetwork::identity`]: what applied to a lease is
110    /// state the network already holds by the time anyone can ask.
111    fn host_ingress(&self, lease: &NetworkLease) -> Result<HostIngress>;
112
113    /// The cleanup-token protocol and startup sweep, when the network keeps
114    /// a quarantine ledger.
115    fn reconcile(&self) -> Option<&dyn NetworkReconcile> {
116        None
117    }
118}
119
120/// Where host-side forwarding sends packets destined for one guest.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122#[non_exhaustive]
123pub enum HostIngress {
124    /// DNAT to the lease's own (pool) address; the datapath translates
125    /// onwards.
126    PoolAddress,
127    /// DNAT to the guest's address, marking packets with `fwmark` so the
128    /// datapath's policy routing picks them up.
129    GuestAddress {
130        /// The mark the forwarding rule must set.
131        fwmark: u32,
132    },
133}
134
135/// What kind of connectivity a VM gets.
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137pub struct NetworkPolicy {
138    /// The connectivity mode.
139    pub mode: NetworkMode,
140}
141
142/// Connectivity modes a network may offer.
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
144#[serde(rename_all = "snake_case")]
145#[non_exhaustive]
146pub enum NetworkMode {
147    /// Host and guest can talk; nothing beyond the host.
148    Isolated,
149    /// Egress through the host's address.
150    Nat,
151    /// The guest appears on the host's link.
152    Bridged,
153}
154
155/// How the host side is attached, for networks that keep the legacy shape.
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum AttachMode {
158    /// Fixed guest identity with per-interface translation host-side: every
159    /// fresh boot and every invariant-snapshot restore.
160    Invariant,
161    /// The interface carries the pool address itself: restores of
162    /// checkpoints taken before the invariant identity existed.
163    LegacySnapshot,
164}
165
166/// A reserved address and the host resources planned for one VM.
167///
168/// Serialized into the VM's durable record so cleanup can be replayed after
169/// a crash; `cleanup_token` is what the two-step cleanup protocol checks.
170///
171/// Deliberately plain — no `#[non_exhaustive]`, because a runtime builds
172/// one from a record it read back and needs every field. What that costs
173/// is that the field *names* are an on-disk contract: a field added here
174/// must carry `#[serde(default)]` so a record written before it still
175/// loads, and nothing may key on the order they are declared in.
176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
177pub struct NetworkLease {
178    /// The VM this lease belongs to.
179    pub vm: VmId,
180    /// The guest's address.
181    pub ip: IpAddr,
182    /// The subnet prefix length.
183    pub prefix_len: u8,
184    /// The gateway the guest routes through.
185    pub gateway: IpAddr,
186    /// The guest's MAC address.
187    pub mac: MacAddr,
188    /// Opaque; presented to [`NetworkReconcile::finalize_cleanup`] to prove
189    /// the caller is finishing *this* generation of the VM's cleanup.
190    pub cleanup_token: String,
191}
192
193/// The network as the guest sees it.
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub struct NetworkIdentity {
196    /// The guest's address.
197    pub ip: IpAddr,
198    /// The subnet prefix length.
199    pub prefix_len: u8,
200    /// The gateway the guest routes through.
201    pub gateway: IpAddr,
202    /// Resolvers the guest should use.
203    pub dns: Vec<IpAddr>,
204    /// The guest's MAC address.
205    pub mac: MacAddr,
206}
207
208/// The quarantine ledger's cleanup protocol.
209///
210/// Two flows share the token discipline. Per VM: after `quarantine`, the
211/// host side (forwarding rules, listeners) is cleaned by someone else, who
212/// then `validate`s and `finalize`s with the lease's token; only then does
213/// the address return to the pool. At startup: the network sweeps what a
214/// previous process left behind and, when there is anything, mints a
215/// startup token that gates the same validate/finalize pair before the
216/// network declares itself settled.
217#[async_trait]
218pub trait NetworkReconcile: Send + Sync {
219    /// VMs whose leases are quarantined, with the token each finalization
220    /// must present.
221    ///
222    /// Fallible because a durable ledger is read back from disk: an entry
223    /// whose id is not a [`VmId`] is one this protocol could never name,
224    /// list, or finalize, and its address would stay out of the pool
225    /// forever. The caller sees that as an error rather than as a shorter
226    /// list.
227    async fn pending_cleanups(&self) -> Result<Vec<(VmId, String)>>;
228
229    /// Checks that `token` names `vm`'s pending cleanup generation, and
230    /// hands back the lease it names.
231    ///
232    /// The lease is the answer, not an acknowledgement: the host cleanup
233    /// this gates is keyed by the address that generation held, and after
234    /// a restart the only place that address survives is the ledger the
235    /// network is reading here.
236    async fn validate_cleanup(&self, vm: &VmId, token: &str) -> Result<NetworkLease>;
237
238    /// Ends `vm`'s quarantine and returns its address to the pool.
239    async fn finalize_cleanup(&self, vm: &VmId, token: &str) -> Result<()>;
240
241    /// The token for the startup sweep's host-side cleanup, while one is
242    /// pending.
243    async fn startup_cleanup_token(&self) -> Option<String>;
244
245    /// Checks that `token` names the pending startup cleanup.
246    async fn validate_startup_cleanup(&self, token: &str) -> Result<()>;
247
248    /// Marks the startup sweep's host-side cleanup done.
249    async fn finalize_startup_cleanup(&self, token: &str) -> Result<()>;
250
251    /// Waits until no startup cleanup is pending.
252    async fn wait_startup_cleanup_complete(&self);
253
254    /// The owner has finished replaying its durable state into this ledger.
255    /// Until it has, the startup sweep cannot be finalized: a host must not
256    /// declare a previous process's forwarding state gone while the owner is
257    /// still discovering what it left behind.
258    fn replay_complete(&self);
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn lease_round_trips_through_json() {
267        let lease = NetworkLease {
268            vm: VmId::new("vm-1").unwrap(),
269            ip: "10.200.0.2".parse().unwrap(),
270            prefix_len: 16,
271            gateway: "10.200.0.1".parse().unwrap(),
272            mac: "02:fa:ce:00:00:02".parse().unwrap(),
273            cleanup_token: "gen-1".into(),
274        };
275        let json = serde_json::to_string(&lease).unwrap();
276        assert_eq!(serde_json::from_str::<NetworkLease>(&json).unwrap(), lease);
277        assert!(json.contains("\"02:fa:ce:00:00:02\""));
278        assert!(json.contains("\"10.200.0.2\""));
279    }
280}