isoloom_spec/model/network.rs
1//! Networks: address blocks, their Docker and traffic-control settings, VLANs, and the
2//! reach between them.
3
4use indexmap::IndexMap;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8use super::Ipv6;
9pub use super::bridged::Bridged;
10
11/// A network: an address block, whether it reaches the internet, and what routes it.
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
13#[serde(deny_unknown_fields)]
14pub struct Network {
15 /// The network's private IPv4 block (inside 10.0.0.0/8, 172.16.0.0/12 or 192.168.0.0/16),
16 /// a /24 to a /29, e.g. `10.20.0.0/24`. A LAN split into `vlans` may be larger, from /8.
17 pub cidr: String,
18 /// Whether machines on it may reach the internet (default true).
19 #[serde(default = "yes")]
20 pub internet: bool,
21 /// A machine of the environment that routes this network (an edge firewall): it takes the
22 /// gateway address, and its own configuration decides what crosses it. Isoloom adds no
23 /// router of its own here, so `reach` and `internet` on this network become expected
24 /// behavior for the checks to verify.
25 #[serde(default)]
26 pub gateway: Option<String>,
27 /// How the Docker target lays this network out, when it differs from the other targets.
28 #[serde(default)]
29 #[schemars(skip)] // lowering fills it from `overrides:`; a spec never writes it here.
30 pub docker: Option<NetworkDocker>,
31 /// The VLANs this LAN is split into, by id (1-4094), each with its own block inside the
32 /// LAN's. Machines join one as `<lan>.vlan<id>` (`office.vlan10`); a `reach` rule naming
33 /// the LAN covers all of its VLANs. Each VLAN becomes a network of its own,
34 /// `<lan>-vlan<id>`; the LAN itself stays a network only when a machine joins it directly.
35 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
36 pub vlans: IndexMap<u16, Vlan>,
37 /// On a LAN split into `vlans`: a switch appliance of the environment (`docker.appliance:
38 /// cisco-iol-l2` or `cisco-vios-l2`) that switches it, instead of Isoloom's own Linux switch.
39 /// Its ports are derived, so the machine lists no `networks`: an access port per VLAN, then
40 /// a trunk per machine on several of the LAN's VLANs. Docker targets only.
41 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub switch: Option<String>,
43 /// Set by parsing on the networks a LAN's VLANs become: that LAN and the VLAN's id, so a
44 /// target can carry them on one 802.1Q segment (a machine on several of them gets a trunk).
45 #[serde(skip)]
46 #[schemars(skip)]
47 pub vlan: Option<(String, u16)>,
48 /// Link impairment on traffic entering this network through Isoloom's router (a slow or
49 /// lossy link): delay, jitter, loss, rate. Needs the router on the network (a `reach` rule
50 /// touching it, no `gateway` machine).
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub tc: Option<Tc>,
53 /// The network's IPv6 block: dual-stack beside `cidr`, or IPv6-only (`only: true`).
54 /// Machines get an IPv6 address on it (derived from their octet, or handed out by the
55 /// target) and routes to the other IPv6 blocks the `reach` rules open.
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub ipv6: Option<Ipv6>,
58 /// An existing network, by its name on the target (a Docker network, a Proxmox bridge, an
59 /// ESXi port group): the machines join it, and Isoloom never creates or deletes it. `cidr`
60 /// is its block, and the machines keep fixed addresses in it.
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 #[schemars(skip)] // lowering fills it from `overrides:`; a spec never writes it here.
63 pub external: Option<String>,
64 /// Bridged to an interface of the host, so the machines sit on the host's own LAN (fixed
65 /// addresses in `cidr`, or DHCP from the LAN).
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub bridged: Option<Bridged>,
68 /// The largest packet the network carries, in bytes (576 to 9216): smaller to mimic a
69 /// tunnel or a VPN link, jumbo frames (9000) for a storage network. A LAN's VLANs carry
70 /// the LAN's. Default: the target's own (1500 on most).
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub mtu: Option<u16>,
73}
74
75impl Network {
76 /// Whether the network is outside the environment (`external` or `bridged`): Isoloom
77 /// doesn't make it, route it or put its own machines on it.
78 #[must_use]
79 pub fn is_outside(&self) -> bool {
80 self.external.is_some() || self.bridged.is_some()
81 }
82
83 /// Whether its machines take their address from the LAN's DHCP server.
84 #[must_use]
85 pub fn is_dhcp(&self) -> bool {
86 self.bridged.as_ref().is_some_and(|b| b.dhcp)
87 }
88}
89
90/// Link impairment (Linux netem on the router's interface into the network).
91#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
92#[serde(deny_unknown_fields)]
93pub struct Tc {
94 /// The one-way delay added to every packet entering the network, e.g. `50ms`, `1s`.
95 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub delay: Option<String>,
97 /// Random variation of the delay, e.g. `5ms` (needs `delay`).
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub jitter: Option<String>,
100 /// The share of packets entering the network that are dropped at random, in percent
101 /// (`0.5` = 0.5%).
102 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub loss: Option<f64>,
104 /// The bandwidth cap on traffic entering the network, as `tc` writes a rate, e.g. `10mbit`,
105 /// `512kbit`.
106 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub rate: Option<String>,
108}
109
110impl Tc {
111 /// The netem parameters, as `tc qdisc ... netem` takes them.
112 #[must_use]
113 pub fn netem(&self) -> String {
114 let mut parts = Vec::new();
115 if let Some(d) = &self.delay {
116 parts.push(format!("delay {d}"));
117 if let Some(j) = &self.jitter {
118 parts.push(j.clone());
119 }
120 }
121 if let Some(l) = self.loss {
122 parts.push(format!("loss {l}%"));
123 }
124 if let Some(r) = &self.rate {
125 parts.push(format!("rate {r}"));
126 }
127 parts.join(" ")
128 }
129}
130
131/// A VLAN of a LAN (see [`Network::vlans`]).
132#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
133#[serde(deny_unknown_fields)]
134pub struct Vlan {
135 /// Its IPv4 block, inside the LAN's (e.g. 10.10.10.0/24 in 10.10.0.0/16).
136 pub cidr: String,
137 /// Whether machines on it may reach the internet (default: as the LAN).
138 #[serde(default)]
139 pub internet: Option<bool>,
140}
141
142/// A network on the Docker target.
143#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
144#[serde(deny_unknown_fields)]
145pub struct NetworkDocker {
146 /// The block to use on Docker instead of `cidr` (same size, inside 10.0.0.0/8). Without
147 /// it, a network outside 10.0.0.0/8 is moved into 10.0.0.0/8 automatically, since Docker's
148 /// own pools, Docker Desktop and home networks use 172.16.0.0/12 and 192.168.0.0/16.
149 pub cidr: String,
150}
151
152fn yes() -> bool {
153 true
154}
155
156/// Traffic allowed from one network to another (one way: answers come back), on some ports or
157/// all of them.
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
159#[serde(deny_unknown_fields)]
160pub struct Reach {
161 /// The network the connections start from: its machines may open them.
162 pub from: String,
163 /// The network those connections may reach: its machines accept them, on `ports`.
164 pub to: String,
165 /// The destination ports allowed. Empty (the default): every port.
166 #[serde(default)]
167 pub ports: Vec<u16>,
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn an_external_or_a_bridged_network_is_outside_the_environment() -> Result<(), serde_yaml_ng::Error> {
176 let outside = |yaml| serde_yaml_ng::from_str::<Network>(yaml).map(|n| n.is_outside());
177 assert!(!outside("{ cidr: 10.0.0.0/24 }")?);
178 assert!(outside("{ cidr: 10.0.0.0/24, external: corp }")?);
179 assert!(outside("{ cidr: 10.0.0.0/24, bridged: { interface: eth0 } }")?);
180 Ok(())
181 }
182
183 #[test]
184 fn netem_takes_the_delay_its_jitter_the_loss_and_the_rate() {
185 let tc = Tc {
186 delay: Some("50ms".into()),
187 jitter: Some("5ms".into()),
188 loss: Some(0.5),
189 rate: Some("10mbit".into()),
190 };
191 assert_eq!(tc.netem(), "delay 50ms 5ms loss 0.5% rate 10mbit");
192 let jitter_alone = Tc {
193 jitter: Some("5ms".into()),
194 ..Tc::default()
195 };
196 assert_eq!(jitter_alone.netem(), "", "a jitter goes with a delay");
197 }
198}