1use 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#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Ipv6Plan {
19 pub cidr: String,
21 pub gateway: Ipv6Addr,
23 pub addresses: Ipv6Addresses,
25 pub only: bool,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct Route6 {
32 pub cidr: String,
34 pub via: Ipv6Addr,
36}
37
38#[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#[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#[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#[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#[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#[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#[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#[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
118pub(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
128pub(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
136pub(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#[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}