Skip to main content

syd/landlock/
net.rs

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