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