1use std::net::Ipv4Addr;
7
8use isoloom_spec::model::{Machine, Spec};
9
10use crate::addressing::{address as machine_address, cidr};
11
12pub const NAME: &str = "isoloom-router";
15
16#[must_use]
19pub fn plain(spec: &Spec, network: &str) -> bool {
20 spec.networks.get(network).is_none_or(|n| n.gateway.is_none())
21}
22
23#[must_use]
25pub fn needed(spec: &Spec) -> bool {
26 spec.reach.iter().any(|r| plain(spec, &r.from) && plain(spec, &r.to))
27}
28
29#[must_use]
31pub fn routed(spec: &Spec) -> bool {
32 needed(spec) || spec.networks.values().any(|n| n.gateway.is_some())
33}
34
35pub fn networks(spec: &Spec) -> impl Iterator<Item = &String> {
37 spec.networks.keys().filter(|n| plain(spec, n))
38}
39
40#[must_use]
42pub fn address(spec: &Spec, network: &str) -> Ipv4Addr {
43 cidr(spec, network).router()
44}
45
46#[must_use]
48pub fn is_gateway(spec: &Spec, name: &str) -> bool {
49 spec.networks.values().any(|n| n.gateway.as_deref() == Some(name))
50}
51
52#[must_use]
55pub fn default_gateway(spec: &Spec, name: &str, m: &Machine) -> Option<Ipv4Addr> {
56 m.networks
57 .keys()
58 .find(|n| spec.networks.get(*n).and_then(|net| net.gateway.as_deref()).is_some_and(|g| g != name))
59 .map(|n| cidr(spec, n).gateway())
60}
61
62#[must_use]
66pub fn routes(spec: &Spec, name: &str, m: &Machine) -> Vec<(String, Ipv4Addr)> {
67 let via_router = needed(spec)
68 .then(|| m.networks.keys().find(|n| plain(spec, n)))
69 .flatten()
70 .map(|n| address(spec, n));
71 spec.networks
72 .iter()
73 .filter(|(n, _)| !m.networks.contains_key(*n))
74 .filter_map(|(_, net)| {
75 let via = match &net.gateway {
76 Some(gw) if gw != name => {
77 let g = spec.machines.get(gw)?;
78 m.networks
79 .keys()
80 .find_map(|shared| g.networks.get(shared).map(|octet| machine_address(spec, shared, *octet)))
81 }
82 Some(_) => None,
83 None => via_router,
84 }?;
85 Some((net.cidr.clone(), via))
86 })
87 .collect()
88}
89
90#[must_use]
93pub fn offline(spec: &Spec, name: &str, m: &Machine) -> bool {
94 !m.networks.keys().any(|n| spec.networks.get(n).is_some_and(|net| net.internet)) && default_gateway(spec, name, m).is_none()
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::testing;
101
102 const LAB: &str = "version: 1
104name: t
105networks:
106 front: { cidr: 10.1.0.0/24 }
107 back: { cidr: 10.2.0.0/24, internet: false }
108 inside: { cidr: 10.3.0.0/24, gateway: fw, internet: false }
109reach: [{ from: front, to: back }]
110machines:
111 web: { networks: { front: 10 }, app: { image: nginx } }
112 db: { networks: { back: 20 }, app: { image: postgres } }
113 fw: { networks: { front: 1, inside: 1 }, app: { image: alpine } }
114 pc: { networks: { inside: 30 }, app: { image: alpine } }
115";
116
117 #[test]
118 fn the_router_joins_the_networks_no_machine_routes() {
119 let spec = testing::spec(LAB);
120 assert!(needed(&spec) && routed(&spec));
121 assert_eq!(networks(&spec).collect::<Vec<_>>(), ["front", "back"]);
122 assert!(plain(&spec, "front") && !plain(&spec, "inside"));
123 assert_eq!(address(&spec, "back"), Ipv4Addr::new(10, 2, 0, 254));
124 }
125
126 #[test]
127 fn a_gateway_routes_its_network_and_is_the_default_route_of_its_machines() {
128 let spec = testing::spec(LAB);
129 assert!(is_gateway(&spec, "fw") && !is_gateway(&spec, "web"));
130 let pc = &spec.machines["pc"];
131 assert_eq!(default_gateway(&spec, "pc", pc), Some(Ipv4Addr::new(10, 3, 0, 1)));
132 assert_eq!(default_gateway(&spec, "fw", &spec.machines["fw"]), None, "a gateway isn't its own");
133 assert!(!offline(&spec, "pc", pc), "its gateway decides");
134 assert!(offline(&spec, "db", &spec.machines["db"]));
135 }
136
137 #[test]
138 fn routes_go_through_the_router_or_the_gateway_on_a_shared_network() {
139 let spec = testing::spec(LAB);
140 assert_eq!(
141 routes(&spec, "web", &spec.machines["web"]),
142 [
143 ("10.2.0.0/24".to_owned(), Ipv4Addr::new(10, 1, 0, 254)),
144 ("10.3.0.0/24".to_owned(), Ipv4Addr::new(10, 1, 0, 1))
145 ]
146 );
147 assert_eq!(
148 routes(&spec, "fw", &spec.machines["fw"]),
149 [("10.2.0.0/24".to_owned(), Ipv4Addr::new(10, 1, 0, 254))]
150 );
151 assert_eq!(
152 routes(&spec, "pc", &spec.machines["pc"]),
153 [],
154 "nothing it shares a network with routes them: its default route does"
155 );
156 }
157
158 #[test]
159 fn a_gateway_routes_without_the_router_and_reach_into_its_network_needs_none() {
160 let spec = testing::spec(
161 "version: 1\nname: t\nnetworks: { a: { cidr: 10.1.0.0/24 }, inside: { cidr: 10.3.0.0/24, gateway: fw } }\nreach: [{ from: a, to: inside }]\nmachines:\n fw: { networks: { a: 1, inside: 1 }, app: { image: x } }\n",
162 );
163 assert!(routed(&spec) && !needed(&spec));
164 assert_eq!(networks(&spec).collect::<Vec<_>>(), ["a"]);
165 }
166
167 #[test]
168 fn a_machine_named_the_gateway_of_a_network_it_isnt_on_routes_nothing_through_itself() {
169 let spec = testing::spec(
171 "version: 1\nname: t\nnetworks: { a: { cidr: 10.1.0.0/24 }, inside: { cidr: 10.3.0.0/24, gateway: fw } }\nmachines:\n fw: { networks: { a: 1 }, app: { image: x } }\n",
172 );
173 assert_eq!(routes(&spec, "fw", &spec.machines["fw"]), []);
174 }
175
176 #[test]
177 fn without_reach_or_gateways_nothing_is_routed() {
178 let spec = testing::spec(
179 "version: 1\nname: t\nnetworks: { a: { cidr: 10.1.0.0/24 }, b: { cidr: 10.2.0.0/24 } }\nmachines:\n m: { networks: { a: 10 }, app: { image: x } }\n",
180 );
181 assert!(!needed(&spec) && !routed(&spec));
182 assert_eq!(routes(&spec, "m", &spec.machines["m"]), []);
183 }
184}