scl_lib/api_objects/
router.rs

1// SPDX-License-Identifier: EUPL-1.2
2use crate::api_objects::{
3    Error, MetaData, Result, SclName, SclObject, SeparationContext, TransitionError,
4};
5use serde::{Deserialize, Serialize};
6use std::net::Ipv4Addr;
7
8#[cfg(feature = "openapi")]
9use aide::OperationIo;
10#[cfg(feature = "openapi")]
11use schemars::JsonSchema;
12
13// TODO encapsulate IPv4 gateway fields into Kind variant or something similar.
14#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
15#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
16#[cfg_attr(feature = "openapi", aide(output))]
17#[serde(rename_all = "camelCase")]
18pub struct Router {
19    #[serde(flatten)]
20    pub metadata: MetaData,
21
22    /// Parent SC of the Router.
23    pub separation_context: SclName,
24
25    /// IPv4 netmask of the `internal_ip`.
26    ///
27    /// Cannot be updated.
28    pub internal_ip_netmask: Ipv4Addr,
29
30    /// User specified IP address that will be assigned to the router specific gateway of the SC network namespace.
31    /// Use this value as a gateway address inside SC VMs.
32    ///
33    /// Cannot be updated.
34    pub internal_ip: Ipv4Addr,
35
36    /// IP address that will be assigned by the SCL API.
37    ///
38    /// Cannot be updated.
39    pub external_ip: Ipv4Addr,
40
41    /// Latest status of the router.
42    ///
43    /// Does not need to be specified by the user when creating (POST),
44    /// as it is initialized with the default values.
45    #[serde(default)]
46    pub status: RouterStatus,
47
48    /// Specifies which TCP ports of the `external_ip` should be forwarded
49    /// to which IP and port combination of the internal network.
50    ///
51    /// Cannot be updated.
52    #[serde(default)]
53    pub forwarded_tcp_ports: Vec<ForwardedPort>,
54
55    /// Specifies which UDP ports of the `external_ip` should be forwarded
56    /// to which IP and port combination of the internal network.
57    ///
58    /// Cannot be updated.
59    #[serde(default)]
60    pub forwarded_udp_ports: Vec<ForwardedPort>,
61}
62
63#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
64#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
65#[cfg_attr(feature = "openapi", aide(output))]
66#[serde(rename_all = "camelCase")]
67pub struct ForwardedPort {
68    pub src_port: u16,
69    pub dst_ip: Ipv4Addr,
70    pub dst_port: u16,
71}
72
73#[derive(Clone, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
74#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
75#[cfg_attr(feature = "openapi", aide(output))]
76#[serde(rename_all = "camelCase")]
77pub enum RouterStatus {
78    /// Nothing has been done.
79    #[default]
80    Pending,
81
82    /// Network device names have been assigned.
83    ///
84    /// Network device names can be vulnerable to name collisions, which must be
85    /// prevented. As all Router related side-effects take place on a single node,
86    /// the L3 network controller is responsible to find and reserve free network
87    /// device names.
88    ///
89    /// Note: Another option would be to limit the length of router names so
90    /// that we can directly derive device names (with some SC specific prefix).
91    ///
92    /// TODO Can we use this with the multi-node setup? Do we need any changes?
93    /// - maybe just leave the strings empty if we dont need them.
94    Assigned(AssignedDeviceNames),
95}
96
97#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
98#[cfg_attr(feature = "openapi", derive(OperationIo, JsonSchema))]
99#[cfg_attr(feature = "openapi", aide(output))]
100#[serde(rename_all = "camelCase")]
101pub struct AssignedDeviceNames {
102    /// Network device name of the veth device in the SC specific netns.
103    pub veth_name_in_sc_netns: String,
104
105    /// Network device name of the veth device in the gateway netns.
106    pub veth_name_in_gateway_netns: String,
107}
108
109impl SclObject for Router {
110    /// Prefix for all [Router]s (`"/routers"`).
111    const PREFIX: &'static str = "/routers";
112
113    fn name(&self) -> &SclName {
114        &self.metadata.name
115    }
116
117    fn separation_context(&self) -> Option<&SclName> {
118        Some(&self.separation_context)
119    }
120
121    fn metadata(&self) -> &MetaData {
122        &self.metadata
123    }
124
125    fn metadata_mut(&mut self) -> &mut MetaData {
126        &mut self.metadata
127    }
128
129    fn referenced_db_keys(&self) -> Vec<String> {
130        vec![SeparationContext::db_key(
131            self.separation_context.as_str(),
132            None,
133        )]
134    }
135
136    fn validate_fields_before_create(&self) -> Result<()> {
137        self.metadata.validate_fields_before_create()?;
138
139        if self.status != RouterStatus::default() {
140            return Err(Error::IllegalInitialValue(
141                "Illegal initial router status!".to_string(),
142            ));
143        }
144
145        // TODO deny certain IP addresses?
146
147        Ok(())
148    }
149
150    fn validate_fields_before_update(
151        current_db_state: &Self,
152        proposed_new_state: &Self,
153    ) -> Result<()> {
154        MetaData::validate_fields_before_regular_update(
155            &current_db_state.metadata,
156            &proposed_new_state.metadata,
157        )?;
158
159        validate_status_transition(&current_db_state.status, &proposed_new_state.status)?;
160
161        // Any additional field must not change.
162        if current_db_state.external_ip != proposed_new_state.external_ip
163            || current_db_state.internal_ip != proposed_new_state.internal_ip
164            || current_db_state.internal_ip_netmask != proposed_new_state.internal_ip_netmask
165            || current_db_state.forwarded_tcp_ports != proposed_new_state.forwarded_tcp_ports
166            || current_db_state.forwarded_udp_ports != proposed_new_state.forwarded_udp_ports
167            || current_db_state.separation_context != proposed_new_state.separation_context
168        {
169            Err(TransitionError::Other("Only status field may be changed!".to_string()).into())
170        } else {
171            Ok(())
172        }
173    }
174}
175
176/// Validates `RouterStatus` transitions including nested state.
177fn validate_status_transition(old_state: &RouterStatus, new_state: &RouterStatus) -> Result<()> {
178    // To enable proper cleanup, an Assigned status must not be changed.
179    use RouterStatus::*;
180    let err = Err(TransitionError::Other("Invalid status transition!".to_string()).into());
181    match (old_state, new_state) {
182        (Assigned(a), Assigned(b)) if a != b => return err,
183        (Assigned(_), Pending) => return err,
184        _ => (),
185    };
186
187    // Deny empty device names.
188    if let Assigned(names) = new_state {
189        if names.veth_name_in_sc_netns.is_empty() || names.veth_name_in_gateway_netns.is_empty() {
190            return Err(TransitionError::Other(
191                "Assigned device names must not be empty".to_string(),
192            )
193            .into());
194        }
195    }
196
197    Ok(())
198}
199
200#[cfg(test)]
201mod test {
202    use crate::api_objects::router::{validate_status_transition, AssignedDeviceNames};
203    use crate::api_objects::test::{
204        detect_invalid_metadata_create_mutations, detect_invalid_metadata_update_mutations,
205        detect_invalid_update_mutations, parse_json_dir,
206    };
207    use crate::api_objects::{ForwardedPort, MetaData, Router, RouterStatus, SclName, SclObject};
208    use std::net::Ipv4Addr;
209    use std::path::Path;
210
211    #[test]
212    fn parse_sample_json() {
213        parse_json_dir::<Router>(Path::new("../test/sample_json/routers"));
214    }
215
216    fn example_router() -> Router {
217        Router {
218            external_ip: "127.0.0.1".parse().unwrap(),
219            internal_ip: "127.0.0.1".parse().unwrap(),
220            internal_ip_netmask: "255.255.255.0".parse().unwrap(),
221            metadata: MetaData::new(SclName::try_from("example").unwrap()),
222            separation_context: SclName::try_from("example").unwrap(),
223            status: Default::default(),
224            forwarded_tcp_ports: Vec::new(),
225            forwarded_udp_ports: Vec::new(),
226        }
227    }
228
229    #[test]
230    fn validate_fields_before_create() {
231        let mut router = example_router();
232        assert!(router.validate_fields_before_create().is_ok());
233        router.status = RouterStatus::Assigned(AssignedDeviceNames {
234            veth_name_in_sc_netns: "sc-veth".into(),
235            veth_name_in_gateway_netns: "gateway-veth".into(),
236        });
237        assert!(router.validate_fields_before_create().is_err());
238    }
239
240    #[test]
241    fn test_validate_status_transition() {
242        use RouterStatus::*;
243
244        let assigned_status = |a: &str, b: &str| {
245            Assigned(AssignedDeviceNames {
246                veth_name_in_sc_netns: a.to_string(),
247                veth_name_in_gateway_netns: b.to_string(),
248            })
249        };
250
251        let bad_transitions = vec![
252            (Pending, assigned_status("", "")),
253            (assigned_status("a", "a"), Pending),
254            (assigned_status("a", "a"), assigned_status("a", "b")),
255            (assigned_status("a", "a"), assigned_status("b", "a")),
256        ];
257
258        for (old_state, new_state) in bad_transitions {
259            assert!(validate_status_transition(&old_state, &new_state).is_err());
260        }
261
262        let good_transitions = vec![
263            (Pending, Pending),
264            (Pending, assigned_status("a", "a")),
265            (assigned_status("a", "a"), assigned_status("a", "a")),
266        ];
267
268        for (old_state, new_state) in good_transitions {
269            assert!(validate_status_transition(&old_state, &new_state).is_ok());
270        }
271    }
272
273    #[test]
274    fn validate_fields_before_update() {
275        let mut current = example_router();
276        let proposed = example_router();
277        assert!(Router::validate_fields_before_update(&current, &proposed).is_ok());
278
279        current.status = RouterStatus::Assigned(AssignedDeviceNames {
280            veth_name_in_sc_netns: "sc-veth".into(),
281            veth_name_in_gateway_netns: "gateway-veth".into(),
282        });
283
284        #[allow(clippy::type_complexity)]
285        let invalid_mutations: Vec<Box<dyn Fn(&mut Router)>> = vec![
286            Box::new(|t| t.separation_context = SclName::try_from("different").unwrap()),
287            Box::new(|t| t.internal_ip = "127.0.0.2".parse().unwrap()),
288            Box::new(|t| t.external_ip = "127.0.0.2".parse().unwrap()),
289            Box::new(|t| t.internal_ip_netmask = "127.0.0.2".parse().unwrap()),
290            Box::new(|t| t.status = RouterStatus::Pending),
291            Box::new(|t| {
292                t.forwarded_tcp_ports = vec![ForwardedPort {
293                    src_port: 8008,
294                    dst_ip: Ipv4Addr::new(192, 168, 30, 10),
295                    dst_port: 22,
296                }]
297            }),
298            Box::new(|t| {
299                t.forwarded_udp_ports = vec![ForwardedPort {
300                    src_port: 7000,
301                    dst_ip: Ipv4Addr::new(192, 168, 30, 10),
302                    dst_port: 22,
303                }]
304            }),
305        ];
306
307        detect_invalid_update_mutations(&current, &proposed, invalid_mutations);
308    }
309
310    #[test]
311    fn detect_invalid_metadata_mutations() {
312        let t = example_router();
313        detect_invalid_metadata_create_mutations(&t);
314        detect_invalid_metadata_update_mutations(&t);
315    }
316}