Skip to main content

isoloom_plan/
ipv6.rs

1//! IPv6: each network's IPv6 block, each machine's IPv6 address (derived from its octet, or
2//! left to the target), the router's IPv6 addresses and the IPv6 routes through it. Pure
3//! functions of a validated spec, beside their IPv4 counterparts in `addressing` and `routing`.
4
5use std::fmt;
6use std::net::Ipv6Addr;
7
8use indexmap::IndexMap;
9use isoloom_spec::model::{Ipv6Addresses, Machine, Spec};
10use isoloom_spec::validate::Cidr6;
11
12use crate::addressing::cidr;
13use crate::requirements::Requirement;
14use crate::routing;
15
16/// A network's IPv6 side, as the target lays it out.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Ipv6Plan {
19    /// Its /64 block.
20    pub cidr: String,
21    /// The host's address on it (Docker's bridge, the hypervisor): `::1`.
22    pub gateway: Ipv6Addr,
23    /// Whether machines get the address derived from their octet, or the target's.
24    pub addresses: Ipv6Addresses,
25    /// Whether the network carries no IPv4.
26    pub only: bool,
27}
28
29/// An IPv6 route to a block a machine isn't on.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct Route6 {
32    /// The block.
33    pub cidr: String,
34    /// The next hop: the router's IPv6 address on a network the machine is on.
35    pub via: Ipv6Addr,
36}
37
38/// A network's IPv6 block, when it has one that parses.
39#[must_use]
40pub fn block(spec: &Spec, network: &str) -> Option<Cidr6> {
41    spec.networks.get(network)?.ipv6.as_ref().and_then(|v| Cidr6::parse(&v.cidr))
42}
43
44/// A network's IPv6 side, when it has one.
45#[must_use]
46pub fn network(spec: &Spec, network: &str) -> Option<Ipv6Plan> {
47    let v6 = spec.networks.get(network)?.ipv6.as_ref()?;
48    let c = Cidr6::parse(&v6.cidr)?;
49    Some(Ipv6Plan {
50        cidr: c.to_string(),
51        gateway: c.gateway(),
52        addresses: v6.addresses,
53        only: v6.only,
54    })
55}
56
57/// Whether the network carries no IPv4.
58#[must_use]
59pub fn only(spec: &Spec, network: &str) -> bool {
60    spec.networks.get(network).and_then(|n| n.ipv6.as_ref()).is_some_and(|v| v.only)
61}
62
63/// The IPv6 address a machine at `octet` has on a network, when the network is IPv6 and its
64/// addresses are derived (with `auto`, the target picks it).
65#[must_use]
66pub fn address(spec: &Spec, network: &str, octet: u8) -> Option<Ipv6Addr> {
67    let v6 = spec.networks.get(network)?.ipv6.as_ref()?;
68    if v6.addresses != Ipv6Addresses::Static {
69        return None;
70    }
71    block(spec, network).map(|c| c.host(octet))
72}
73
74/// A machine's derived IPv6 addresses, by network, in its order.
75#[must_use]
76pub fn addresses(spec: &Spec, m: &Machine) -> IndexMap<String, Ipv6Addr> {
77    m.networks.iter().filter_map(|(n, o)| Some((n.clone(), address(spec, n, *o)?))).collect()
78}
79
80/// The router's IPv6 address on a network it routes: the one its IPv4 octet gives (`::254` in
81/// a /24), fixed whatever the network's `addresses`.
82#[must_use]
83pub fn router_address(spec: &Spec, network: &str) -> Option<Ipv6Addr> {
84    let octet = cidr(spec, network).router().octets()[3];
85    block(spec, network).map(|c| c.host(octet))
86}
87
88/// The router's IPv6 addresses, on each IPv6 network it routes (empty without a router).
89#[must_use]
90pub fn router(spec: &Spec) -> IndexMap<String, Ipv6Addr> {
91    if !routing::needed(spec) {
92        return IndexMap::new();
93    }
94    routing::networks(spec).filter_map(|n| Some((n.clone(), router_address(spec, n)?))).collect()
95}
96
97/// A machine's IPv6 routes: each IPv6 block it isn't on, through the router's IPv6 address on
98/// its first IPv6 network the router is on. Gateway networks carry no IPv6 (validated), so the
99/// router is the only next hop.
100#[must_use]
101pub fn routes(spec: &Spec, m: &Machine) -> Vec<Route6> {
102    let router = router(spec);
103    let Some(via) = m.networks.keys().find_map(|n| router.get(n)) else {
104        return Vec::new();
105    };
106    router
107        .keys()
108        .filter(|n| !m.networks.contains_key(*n))
109        .filter_map(|n| {
110            Some(Route6 {
111                cidr: block(spec, n)?.to_string(),
112                via: *via,
113            })
114        })
115        .collect()
116}
117
118/// A cloud's network routes between its subnets, IPv6 included, and has no Isoloom router:
119/// the IPv6 step keeps the addresses and drops the routes.
120pub(crate) fn cloud_routes(steps: &mut [crate::lifecycle::Step]) {
121    for s in steps {
122        if let crate::lifecycle::Step::Ipv6 { routes, .. } = s {
123            routes.clear();
124        }
125    }
126}
127
128/// Why a target without IPv6-only networks refuses network `network`.
129pub(crate) fn no_ipv6_only(f: &mut fmt::Formatter<'_>, network: &str) -> fmt::Result {
130    write!(
131        f,
132        "network `{network}` is IPv6-only (`ipv6.only`): this target needs IPv4 on every network (its router, its set-up, its transport)"
133    )
134}
135
136/// Why a target that hands out no IPv6 addresses refuses network `network`'s `auto` ones.
137pub(crate) fn no_auto_addresses(f: &mut fmt::Formatter<'_>, network: &str) -> fmt::Result {
138    write!(
139        f,
140        "network `{network}` leaves IPv6 addresses to the target (`ipv6.addresses: auto`), and nothing on this target hands them out: use `static`"
141    )
142}
143
144/// What the IPv6 networks ask of a backend: IPv6 at all, IPv6-only networks, addresses the
145/// target hands out. At most one of each kind, naming the first network that asks.
146#[must_use]
147pub fn requirements(spec: &Spec) -> Vec<Requirement> {
148    let first = |pick: fn(&isoloom_spec::model::Ipv6) -> bool| {
149        spec.networks
150            .iter()
151            .find(|(_, n)| n.ipv6.as_ref().is_some_and(pick))
152            .map(|(name, _)| name.clone())
153    };
154    let mut out = Vec::new();
155    if let Some(network) = first(|_| true) {
156        out.push(Requirement::Ipv6 { network });
157    }
158    if let Some(network) = first(|v| v.only) {
159        out.push(Requirement::Ipv6Only { network });
160    }
161    if let Some(network) = first(|v| v.addresses == Ipv6Addresses::Auto) {
162        out.push(Requirement::Ipv6Auto { network });
163    }
164    out
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use crate::lifecycle::Step;
171
172    fn spec() -> Spec {
173        crate::testing::spec(
174            "version: 1
175name: v6
176networks:
177  a: { cidr: 10.1.0.0/24, ipv6: { cidr: \"fd00:1::/64\" } }
178  b: { cidr: 10.2.0.0/24, ipv6: { cidr: \"fd00:2::/64\", addresses: auto } }
179  c: { cidr: 10.3.0.0/24 }
180reach: [{ from: a, to: b }]
181machines:
182  x: { networks: { a: 10 }, app: { image: alpine } }
183  y: { networks: { b: 20, c: 5 }, app: { image: alpine } }
184",
185        )
186    }
187
188    #[test]
189    fn only_an_ipv6_only_network_carries_no_ipv4() {
190        let s = crate::testing::spec(
191            "version: 1\nname: v6\nnetworks:\n  a: { cidr: 10.1.0.0/24, ipv6: { cidr: \"fd00:1::/64\", only: true } }\n  b: { cidr: 10.2.0.0/24, ipv6: { cidr: \"fd00:2::/64\" } }\nmachines: {}\n",
192        );
193        assert!(only(&s, "a") && !only(&s, "b") && !only(&s, "nowhere"));
194        let plan = network(&s, "a").expect("an IPv6 side");
195        assert_eq!(
196            (plan.cidr.as_str(), plan.gateway.to_string(), plan.only),
197            ("fd00:1::/64", "fd00:1::1".to_owned(), true)
198        );
199        assert_eq!(network(&spec(), "c"), None);
200        let kinds: Vec<Requirement> = requirements(&s);
201        assert_eq!(
202            kinds,
203            [Requirement::Ipv6 { network: "a".into() }, Requirement::Ipv6Only { network: "a".into() }]
204        );
205        assert_eq!(requirements(&spec()).last(), Some(&Requirement::Ipv6Auto { network: "b".into() }));
206    }
207
208    #[test]
209    fn a_cloud_keeps_the_addresses_and_drops_the_routes() {
210        let mut steps = vec![
211            Step::Ipv6 {
212                addresses: Vec::new(),
213                routes: routes(&spec(), &spec().machines["x"]),
214            },
215            Step::Ready,
216        ];
217        cloud_routes(&mut steps);
218        assert_eq!(
219            steps,
220            [
221                Step::Ipv6 {
222                    addresses: Vec::new(),
223                    routes: Vec::new()
224                },
225                Step::Ready
226            ]
227        );
228    }
229
230    #[test]
231    fn derived_addresses_only_where_static() {
232        let s = spec();
233        assert_eq!(addresses(&s, &s.machines["x"])["a"].to_string(), "fd00:1::10");
234        assert!(addresses(&s, &s.machines["y"]).is_empty());
235    }
236
237    #[test]
238    fn routes_go_through_the_router_on_ipv6_networks() {
239        let s = spec();
240        assert_eq!(router(&s)["b"].to_string(), "fd00:2::254");
241        let r = routes(&s, &s.machines["x"]);
242        assert_eq!(
243            r,
244            vec![Route6 {
245                cidr: "fd00:2::/64".into(),
246                via: "fd00:1::254".parse().unwrap()
247            }]
248        );
249        assert_eq!(routes(&s, &s.machines["y"])[0].cidr, "fd00:1::/64");
250    }
251}