use super::Problems;
use super::cidr6::Cidr6;
use crate::model::{Network, Spec};
pub(super) fn check(spec: &Spec, p: &mut Problems) {
let mut blocks: Vec<(&str, Cidr6)> = Vec::new();
for (name, net) in &spec.networks {
let Some(v6) = &net.ipv6 else { continue };
let at = format!("networks.{name}.ipv6");
match Cidr6::parse(&v6.cidr) {
Some(c) => {
if let Some((other, _)) = blocks.iter().find(|(_, b)| b.overlaps(c)) {
p.add(format!("{at}.cidr"), format!("network `{other}` has this block already"));
}
blocks.push((name, c));
}
None => p.add(
format!("{at}.cidr"),
format!("`{}` isn't an IPv6 /64 network address, unique local (fd00:20::/64) or global", v6.cidr),
),
}
if net.gateway.is_some() {
p.add(
&at,
"a network routed by a `gateway` machine carries IPv4 only for now: drop `ipv6` or the `gateway`",
);
}
if v6.only {
only(spec, name, net, p);
}
}
}
fn only(spec: &Spec, name: &str, net: &Network, p: &mut Problems) {
let at = format!("networks.{name}.ipv6.only");
if spec.reach.iter().any(|r| r.from == name || r.to == name) {
p.add(
&at,
format!("`reach` rules go through Isoloom's IPv4 router: drop the rules naming `{name}`, or make it dual-stack (`only: false`)"),
);
}
if net.tc.is_some() {
p.add(&at, "`tc` applies on Isoloom's IPv4 router: drop `tc`, or make the network dual-stack");
}
if net.internet {
p.add(&at, "an IPv6-only network has no way out (Isoloom's NAT is IPv4): set `internet: false`");
}
}
#[cfg(test)]
mod tests {
use crate::testing::{lab, problems};
fn of(network: &str, rest: &str) -> Vec<String> {
problems(&lab(&format!(
" v6: {network}\nmachines:\n web: {{ networks: {{ lan: 10, v6: 10 }}, docker: {{ image: nginx }} }}\n{rest}"
)))
}
#[test]
fn a_dual_stack_network_has_its_own_64() {
assert_eq!(of("{ cidr: 10.1.0.0/24, ipv6: { cidr: 'fd00:20::/64' } }", ""), Vec::<String>::new());
assert_eq!(
of("{ cidr: 10.1.0.0/24, ipv6: { cidr: 'fd00:20::5/64' } }", ""),
["networks.v6.ipv6.cidr: `fd00:20::5/64` isn't an IPv6 /64 network address, unique local (fd00:20::/64) or global"]
);
}
#[test]
fn two_networks_never_share_a_64() {
let found = problems(&lab(
" a: { cidr: 10.1.0.0/24, ipv6: { cidr: 'fd00:20::/64' } }\n b: { cidr: 10.2.0.0/24, ipv6: { cidr: 'fd00:20::/64' } }\nmachines:\n web: { networks: { a: 10, b: 10 }, docker: { image: nginx } }\n",
));
assert_eq!(found, ["networks.b.ipv6.cidr: network `a` has this block already"]);
}
#[test]
fn a_gateway_network_carries_ipv4_only() {
let found = problems(&lab(
" v6: { cidr: 10.1.0.0/24, gateway: gw, ipv6: { cidr: 'fd00:20::/64' } }\nmachines:\n gw: { networks: { lan: 10, v6: 1 }, docker: { image: router } }\n",
));
assert_eq!(
found,
["networks.v6.ipv6: a network routed by a `gateway` machine carries IPv4 only for now: drop `ipv6` or the `gateway`"]
);
}
#[test]
fn an_ipv6_only_network_has_no_reach_tc_or_internet() {
let only = "{ cidr: 10.1.0.0/24, internet: false, ipv6: { cidr: 'fd00:20::/64', only: true } }";
assert_eq!(of(only, ""), Vec::<String>::new());
let reach = "networks.v6.ipv6.only: `reach` rules go through Isoloom's IPv4 router: drop the rules naming `v6`, or make it dual-stack (`only: false`)";
assert_eq!(of(only, "reach: [{ from: v6, to: lan }]"), [reach]);
assert_eq!(of(only, "reach: [{ from: lan, to: v6 }]"), [reach]);
assert_eq!(
of(only, "reach: [{ from: lan, to: lan }]").iter().filter(|p| p.contains("ipv6.only")).count(),
0
);
assert_eq!(
of(
"{ cidr: 10.1.0.0/24, internet: false, tc: { delay: 10ms }, ipv6: { cidr: 'fd00:20::/64', only: true } }",
""
),
["networks.v6.ipv6.only: `tc` applies on Isoloom's IPv4 router: drop `tc`, or make the network dual-stack"]
);
assert_eq!(
of("{ cidr: 10.1.0.0/24, ipv6: { cidr: 'fd00:20::/64', only: true } }", ""),
["networks.v6.ipv6.only: an IPv6-only network has no way out (Isoloom's NAT is IPv4): set `internet: false`"]
);
}
}