Skip to main content

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}