1use std::net::Ipv4Addr;
21
22use isoloom_plan::addressing::cidr;
23use isoloom_spec::images::is_windows;
24use isoloom_spec::model::{Machine, Spec};
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28#[non_exhaustive]
29pub struct Vlan {
30 pub network: String,
32 pub id: u16,
34 pub octet: u8,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40#[non_exhaustive]
41pub struct Trunk {
42 pub lan: String,
44 pub machine: String,
46 pub vlans: Vec<Vlan>,
48}
49
50#[must_use]
52pub fn containers(spec: &Spec) -> Vec<Trunk> {
53 of_machines(spec, |m| m.docker.is_some())
54}
55
56#[must_use]
58pub fn vms(spec: &Spec) -> Vec<Trunk> {
59 of_machines(spec, |m| m.vm.as_ref().is_some_and(|vm| !is_windows(&vm.os)))
60}
61
62fn of_machines(spec: &Spec, kept: impl Fn(&Machine) -> bool) -> Vec<Trunk> {
64 let mut out: Vec<Trunk> = Vec::new();
65 for (name, m) in spec.machines.iter().filter(|(_, m)| kept(m)) {
66 for (net, octet) in &m.networks {
67 let Some((lan, id)) = spec.networks.get(net).and_then(|n| n.vlan.as_ref()) else {
68 continue;
69 };
70 let vlan = Vlan {
71 network: net.clone(),
72 id: *id,
73 octet: *octet,
74 };
75 match out.iter_mut().find(|t| &t.machine == name && &t.lan == lan) {
76 Some(t) => t.vlans.push(vlan),
77 None => out.push(Trunk {
78 lan: lan.clone(),
79 machine: name.clone(),
80 vlans: vec![vlan],
81 }),
82 }
83 }
84 }
85 out.retain(|t| t.vlans.len() >= 2);
86 out
87}
88
89pub fn of<'a>(trunks: &'a [Trunk], machine: &str) -> impl Iterator<Item = &'a Trunk> {
91 trunks.iter().filter(move |t| t.machine == machine)
92}
93
94#[must_use]
96pub fn carried(trunks: &[Trunk], machine: &str, network: &str) -> bool {
97 of(trunks, machine).any(|t| t.vlans.iter().any(|v| v.network == network))
98}
99
100#[must_use]
102pub fn link_network(t: &Trunk) -> String {
103 format!("isoloom-trunk-{}-{}", t.lan, t.machine)
104}
105
106#[must_use]
108pub fn switch_name(lan: &str) -> String {
109 format!("isoloom-switch-{lan}")
110}
111
112#[must_use]
115pub fn interface(lan: &str) -> String {
116 lan.chars().take(10).collect::<String>().trim_end_matches('-').to_owned()
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum End {
122 Machine,
124 Switch,
126}
127
128#[must_use]
131pub fn mac(t: &Trunk, end: End) -> String {
132 let hash = format!("{}/{}", t.lan, t.machine)
133 .bytes()
134 .fold(2_166_136_261_u32, |hash, byte| (hash ^ u32::from(byte)).wrapping_mul(16_777_619));
135 let [first, second, third, last] = hash.to_be_bytes();
136 let side = match end {
137 End::Machine => 0,
138 End::Switch => 1,
139 };
140 format!("02:1e:{first:02x}:{second:02x}:{third:02x}:{:02x}", (last & 0xfe) | side)
141}
142
143fn find_by_mac(mac: &str) -> String {
145 format!("IF=$(ip -o link | awk 'tolower($0) ~ /{mac}/ {{print $2}}' | cut -d@ -f1 | tr -d : | head -n 1)")
146}
147
148fn find_by_address(addr: Ipv4Addr) -> String {
150 format!(
151 "IF=$(ip -o -4 addr show | awk '$4 ~ /^{}\\//{{print $2}}' | head -n 1)",
152 addr.to_string().replace('.', "\\.")
153 )
154}
155
156#[must_use]
158pub fn switch_address(spec: &Spec, network: &str) -> Ipv4Addr {
159 cidr(spec, network).controller()
160}
161
162#[must_use]
164pub fn switch_script(spec: &Spec, trunks: &[&Trunk]) -> String {
165 let mut bridged: Vec<&Vlan> = Vec::new();
166 for v in trunks.iter().flat_map(|t| &t.vlans) {
167 if !bridged.iter().any(|b| b.id == v.id) {
168 bridged.push(v);
169 }
170 }
171 let bridge_lines = bridged.iter().map(|v| {
172 format!(
173 "{} && ip addr flush dev $IF && ip link add br{id} type bridge && ip link set $IF master br{id} && ip link set br{id} up && ",
174 find_by_address(switch_address(spec, &v.network)),
175 id = v.id
176 )
177 });
178 let trunk_lines = trunks.iter().map(|t| {
179 let tagged: String = t
180 .vlans
181 .iter()
182 .map(|v| {
183 format!(
184 "ip link add link $IF name $IF.{id} type vlan id {id} && ip link set $IF.{id} master br{id} && ip link set $IF.{id} up && ",
185 id = v.id
186 )
187 })
188 .collect::<Vec<_>>()
189 .concat();
190 format!(
191 "{} && ip addr flush dev $IF && ip link set $IF up && {tagged}",
192 find_by_mac(&mac(t, End::Switch))
193 )
194 });
195 bridge_lines
196 .chain(trunk_lines)
197 .chain(std::iter::once("exec sleep infinity".to_owned()))
198 .collect()
199}
200
201#[must_use]
203pub fn switch_ready(trunks: &[&Trunk]) -> String {
204 let mut ids: Vec<u16> = trunks.iter().flat_map(|t| t.vlans.iter().map(|v| v.id)).collect();
205 ids.sort_unstable();
206 ids.dedup();
207 ids.iter()
208 .map(|id| format!("ip link show br{id} | grep -q 'state UP'"))
209 .collect::<Vec<_>>()
210 .join(" && ")
211}
212
213#[must_use]
217pub fn machine_commands(spec: &Spec, m: &Machine, t: &Trunk, address: impl Fn(&str, u8) -> Ipv4Addr, gateway: impl Fn(&str) -> Ipv4Addr) -> Vec<String> {
218 let name = interface(&t.lan);
219 let mut cmds = vec![format!(
220 "{} && ip addr flush dev $IF && ip link set $IF down && ip link set $IF name {name} && ip link set {name} up",
221 find_by_mac(&mac(t, End::Machine))
222 )];
223 for v in &t.vlans {
224 cmds.push(format!(
225 "ip link add link {name} name {name}.{id} type vlan id {id} && ip addr add {}/{} dev {name}.{id} && ip link set {name}.{id} up",
226 address(&v.network, v.octet),
227 cidr(spec, &v.network).len,
228 id = v.id
229 ));
230 }
231 let internet = |n: &str| spec.networks.get(n).is_some_and(|net| net.internet);
232 let other_way_out = m.networks.keys().any(|n| !t.vlans.iter().any(|v| &v.network == n) && internet(n));
233 if !other_way_out && let Some(v) = t.vlans.iter().find(|v| internet(&v.network)) {
234 cmds.push(format!("ip route replace default via {}", gateway(&v.network)));
235 }
236 cmds
237}
238
239#[must_use]
241pub fn machine_ready(t: &Trunk) -> String {
242 let name = interface(&t.lan);
243 t.vlans
244 .iter()
245 .map(|v| format!("ip link show {name}.{} | grep -q 'UP'", v.id))
246 .collect::<Vec<_>>()
247 .join(" && ")
248}
249
250fn vm_peer(lan: &str) -> String {
252 format!("{}-s", lan.chars().take(8).collect::<String>().trim_end_matches('-'))
253}
254
255#[must_use]
257pub fn vm_commands(spec: &Spec, t: &Trunk, address: impl Fn(&str, u8) -> Ipv4Addr) -> Vec<String> {
258 let name = interface(&t.lan);
259 let peer = vm_peer(&t.lan);
260 let mut steps = vec![
261 format!("ip link add {name} type veth peer name {peer}"),
262 format!("ip link set {name} up"),
263 format!("ip link set {peer} up"),
264 ];
265 for v in &t.vlans {
266 let (id, addr) = (v.id, address(&v.network, v.octet));
267 let len = cidr(spec, &v.network).len;
268 let local = format!("02:1f:{}", addr.octets().map(|o| format!("{o:02x}")).join(":"));
270 steps.push(format!(
271 "{} && MAC=$(cat \"/sys/class/net/$IF/address\") && ip addr flush dev \"$IF\" && ip link set \"$IF\" address {local}",
272 find_by_address(addr)
273 ));
274 steps.push(format!(
275 "ip link add br{id} type bridge && ip link set \"$IF\" master br{id} && ip link set br{id} up"
276 ));
277 steps.push(format!(
278 "ip link add link {peer} name {peer}.{id} type vlan id {id} && ip link set {peer}.{id} master br{id} && ip link set {peer}.{id} up"
279 ));
280 steps.push(format!(
281 "ip link add link {name} name {name}.{id} address \"$MAC\" type vlan id {id} && ip addr add {addr}/{len} dev {name}.{id} && ip link set {name}.{id} up"
282 ));
283 }
284 vec![format!("ip link show {name} >/dev/null 2>&1 || {{ {}; }}", steps.join(" && "))]
285}
286
287#[cfg(test)]
288mod tests;