syd/landlock/
net.rs

1use std::mem::zeroed;
2
3use crate::landlock::{
4    compat::private::OptionCompatLevelMut, uapi, Access, AddRuleError, AddRulesError, CompatError,
5    CompatLevel, CompatResult, CompatState, Compatible, HandleAccessError, HandleAccessesError,
6    PrivateAccess, PrivateRule, Rule, Ruleset, RulesetCreated, TailoredCompatLevel, TryCompat, ABI,
7};
8
9crate::landlock::access::bitflags_type! {
10    /// Network access right.
11    ///
12    /// Each variant of `AccessNet` is an [access right](https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#access-rights)
13    /// for the network.
14    ///
15    /// # Example
16    ///
17    /// ```ignore
18    /// use syd::landlock::{ABI, Access, AccessNet};
19    ///
20    /// let bind = AccessNet::BindTcp;
21    ///
22    /// let bind_set: AccessNet = bind.into();
23    ///
24    /// let bind_connect = AccessNet::BindTcp | AccessNet::ConnectTcp;
25    ///
26    /// let net_v4 = AccessNet::from_all(ABI::V4);
27    ///
28    /// assert_eq!(bind_connect, net_v4);
29    /// ```
30    pub struct AccessNet: u64 {
31        /// Bind to a TCP port.
32        const BindTcp = uapi::LANDLOCK_ACCESS_NET_BIND_TCP as u64;
33        /// Connect to a TCP port.
34        const ConnectTcp = uapi::LANDLOCK_ACCESS_NET_CONNECT_TCP as u64;
35    }
36}
37
38impl TailoredCompatLevel for AccessNet {}
39
40/// # Warning
41///
42/// If `ABI <= ABI::V3`, `AccessNet::from_all()` returns an empty `AccessNet`, which
43/// makes `Ruleset::handle_access(AccessNet::from_all(ABI::V3))` return an error.
44impl Access for AccessNet {
45    fn from_all(abi: ABI) -> Self {
46        match abi {
47            ABI::Unsupported | ABI::V1 | ABI::V2 | ABI::V3 => AccessNet::EMPTY,
48            ABI::V4 | ABI::V5 | ABI::V6 => AccessNet::BindTcp | AccessNet::ConnectTcp,
49        }
50    }
51}
52
53impl PrivateAccess for AccessNet {
54    fn is_empty(self) -> bool {
55        AccessNet::is_empty(&self)
56    }
57
58    fn ruleset_handle_access(
59        ruleset: &mut Ruleset,
60        access: Self,
61    ) -> Result<(), HandleAccessesError> {
62        // We need to record the requested accesses for PrivateRule::check_consistency().
63        ruleset.requested_handled_net |= access;
64        ruleset.actual_handled_net |= match access
65            .try_compat(
66                ruleset.compat.abi(),
67                ruleset.compat.level,
68                &mut ruleset.compat.state,
69            )
70            .map_err(HandleAccessError::Compat)?
71        {
72            Some(a) => a,
73            None => return Ok(()),
74        };
75        Ok(())
76    }
77
78    fn into_add_rules_error(error: AddRuleError<Self>) -> AddRulesError {
79        AddRulesError::Net(error)
80    }
81
82    fn into_handle_accesses_error(error: HandleAccessError<Self>) -> HandleAccessesError {
83        HandleAccessesError::Net(error)
84    }
85}
86
87/// Landlock rule for a network port.
88///
89/// # Example
90///
91/// ```
92/// use syd::landlock::{AccessNet, NetPort};
93///
94/// fn bind_http() -> NetPort {
95///     NetPort::new(80, AccessNet::BindTcp)
96/// }
97/// ```
98#[cfg_attr(test, derive(Debug))]
99pub struct NetPort {
100    attr: uapi::landlock_net_port_attr,
101    // Only 16-bit port make sense for now.
102    port: u16,
103    allowed_access: AccessNet,
104    compat_level: Option<CompatLevel>,
105}
106
107// If we need support for 32 or 64 ports, we'll add a new_32() or a new_64() method returning a
108// Result with a potential overflow error.
109impl NetPort {
110    /// Creates a new TCP port rule.
111    ///
112    /// As defined by the Linux ABI, `port` with a value of `0` means that TCP bindings will be
113    /// allowed for a port range defined by `/proc/sys/net/ipv4/ip_local_port_range`.
114    pub fn new<A>(port: u16, access: A) -> Self
115    where
116        A: Into<AccessNet>,
117    {
118        NetPort {
119            // Invalid access-rights until as_ptr() is called.
120            attr: unsafe { zeroed() },
121            port,
122            allowed_access: access.into(),
123            compat_level: None,
124        }
125    }
126}
127
128impl Rule<AccessNet> for NetPort {}
129
130impl PrivateRule<AccessNet> for NetPort {
131    const TYPE_ID: uapi::landlock_rule_type = uapi::landlock_rule_type_LANDLOCK_RULE_NET_PORT;
132
133    fn as_ptr(&mut self) -> *const libc::c_void {
134        self.attr.port = self.port as u64;
135        self.attr.allowed_access = self.allowed_access.bits();
136        &self.attr as *const _ as _
137    }
138
139    fn check_consistency(&self, ruleset: &RulesetCreated) -> Result<(), AddRulesError> {
140        // Checks that this rule doesn't contain a superset of the access-rights handled by the
141        // ruleset.  This check is about requested access-rights but not actual access-rights.
142        // Indeed, we want to get a deterministic behavior, i.e. not based on the running kernel
143        // (which is handled by Ruleset and RulesetCreated).
144        if ruleset.requested_handled_net.contains(self.allowed_access) {
145            Ok(())
146        } else {
147            Err(AddRuleError::UnhandledAccess {
148                access: self.allowed_access,
149                incompatible: self.allowed_access & !ruleset.requested_handled_net,
150            }
151            .into())
152        }
153    }
154}
155
156#[test]
157fn net_port_check_consistency() {
158    use crate::*;
159
160    let bind = AccessNet::BindTcp;
161    let bind_connect = bind | AccessNet::ConnectTcp;
162
163    assert!(matches!(
164        Ruleset::from(ABI::Unsupported)
165            .handle_access(bind)
166            .unwrap()
167            .create()
168            .unwrap()
169            .add_rule(NetPort::new(1, bind_connect))
170            .unwrap_err(),
171        RulesetError::AddRules(AddRulesError::Net(AddRuleError::UnhandledAccess { access, incompatible }))
172            if access == bind_connect && incompatible == AccessNet::ConnectTcp
173    ));
174}
175
176impl TryCompat<AccessNet> for NetPort {
177    fn try_compat_children<L>(
178        mut self,
179        abi: ABI,
180        parent_level: L,
181        compat_state: &mut CompatState,
182    ) -> Result<Option<Self>, CompatError<AccessNet>>
183    where
184        L: Into<CompatLevel>,
185    {
186        // Checks with our own compatibility level, if any.
187        self.allowed_access = match self.allowed_access.try_compat(
188            abi,
189            self.tailored_compat_level(parent_level),
190            compat_state,
191        )? {
192            Some(a) => a,
193            None => return Ok(None),
194        };
195        Ok(Some(self))
196    }
197
198    fn try_compat_inner(
199        &mut self,
200        _abi: ABI,
201    ) -> Result<CompatResult<AccessNet>, CompatError<AccessNet>> {
202        Ok(CompatResult::Full)
203    }
204}
205
206impl OptionCompatLevelMut for NetPort {
207    fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
208        &mut self.compat_level
209    }
210}
211
212impl OptionCompatLevelMut for &mut NetPort {
213    fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
214        &mut self.compat_level
215    }
216}
217
218impl Compatible for NetPort {}
219
220impl Compatible for &mut NetPort {}