syd 3.52.0

rock-solid application kernel
Documentation
// SPDX-License-Identifier: Apache-2.0 OR MIT

use std::mem::zeroed;

use crate::landlock::{
    compat::private::OptionCompatLevelMut, uapi, Access, AddRuleError, AddRulesError, CompatError,
    CompatLevel, CompatResult, CompatState, Compatible, HandleAccessError, HandleAccessesError,
    PrivateAccess, PrivateRule, Rule, Ruleset, RulesetCreated, TailoredCompatLevel, TryCompat, ABI,
};

crate::landlock::access::bitflags_type! {
    /// Network access right.
    ///
    /// Each variant of `AccessNet` is an [access right](https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#access-rights)
    /// for the network.
    ///
    /// # Example
    ///
    /// ```ignore
    /// use syd::landlock::{ABI, Access, AccessNet};
    ///
    /// let bind = AccessNet::BindTcp;
    ///
    /// let bind_set: AccessNet = bind.into();
    ///
    /// let bind_connect = AccessNet::BindTcp | AccessNet::ConnectTcp;
    ///
    /// let net_v4 = AccessNet::from_all(ABI::V4);
    ///
    /// assert_eq!(bind_connect, net_v4);
    /// ```
    pub struct AccessNet: u64 {
        /// Bind to a TCP port.
        const BindTcp = uapi::LANDLOCK_ACCESS_NET_BIND_TCP as u64;
        /// Connect to a TCP port.
        const ConnectTcp = uapi::LANDLOCK_ACCESS_NET_CONNECT_TCP as u64;
    }
}

impl TailoredCompatLevel for AccessNet {}

/// # Warning
///
/// If `ABI <= ABI::V3`, `AccessNet::from_all()` returns an empty `AccessNet`, which
/// makes `Ruleset::handle_access(AccessNet::from_all(ABI::V3))` return an error.
impl Access for AccessNet {
    fn from_all(abi: ABI) -> Self {
        match abi {
            ABI::Unsupported | ABI::V1 | ABI::V2 | ABI::V3 => AccessNet::EMPTY,
            ABI::V4 | ABI::V5 | ABI::V6 | ABI::V7 | ABI::V8 => {
                AccessNet::BindTcp | AccessNet::ConnectTcp
            }
        }
    }
}

impl PrivateAccess for AccessNet {
    fn is_empty(self) -> bool {
        AccessNet::is_empty(&self)
    }

    fn ruleset_handle_access(
        ruleset: &mut Ruleset,
        access: Self,
    ) -> Result<(), HandleAccessesError> {
        // We need to record the requested accesses for PrivateRule::check_consistency().
        ruleset.requested_handled_net |= access;
        ruleset.actual_handled_net |= match access
            .try_compat(
                ruleset.compat.abi(),
                ruleset.compat.level,
                &mut ruleset.compat.state,
            )
            .map_err(HandleAccessError::Compat)?
        {
            Some(a) => a,
            None => return Ok(()),
        };
        Ok(())
    }

    fn into_add_rules_error(error: AddRuleError<Self>) -> AddRulesError {
        AddRulesError::Net(error)
    }

    fn into_handle_accesses_error(error: HandleAccessError<Self>) -> HandleAccessesError {
        HandleAccessesError::Net(error)
    }
}

/// Landlock rule for a network port.
///
/// # Example
///
/// ```
/// use syd::landlock::{AccessNet, NetPort};
///
/// fn bind_http() -> NetPort {
///     NetPort::new(80, AccessNet::BindTcp)
/// }
/// ```
#[derive(Debug)]
pub struct NetPort {
    attr: uapi::landlock_net_port_attr,
    // Only 16-bit port make sense for now.
    port: u16,
    allowed_access: AccessNet,
    compat_level: Option<CompatLevel>,
}

// If we need support for 32 or 64 ports, we'll add a new_32() or a new_64() method returning a
// Result with a potential overflow error.
impl NetPort {
    /// Creates a new TCP port rule.
    ///
    /// As defined by the Linux ABI, `port` with a value of `0` means that TCP bindings will be
    /// allowed for a port range defined by `/proc/sys/net/ipv4/ip_local_port_range`.
    pub fn new<A>(port: u16, access: A) -> Self
    where
        A: Into<AccessNet>,
    {
        NetPort {
            // Invalid access-rights until as_ptr() is called.
            attr: unsafe { zeroed() },
            port,
            allowed_access: access.into(),
            compat_level: None,
        }
    }
}

impl Rule<AccessNet> for NetPort {}

impl PrivateRule<AccessNet> for NetPort {
    const TYPE_ID: uapi::landlock_rule_type = uapi::landlock_rule_type_LANDLOCK_RULE_NET_PORT;

    fn as_ptr(&mut self) -> *const libc::c_void {
        self.attr.port = self.port as u64;
        self.attr.allowed_access = self.allowed_access.bits();
        &self.attr as *const _ as _
    }

    fn check_consistency(&self, ruleset: &RulesetCreated) -> Result<(), AddRulesError> {
        // Checks that this rule doesn't contain a superset of the access-rights handled by the
        // ruleset.  This check is about requested access-rights but not actual access-rights.
        // Indeed, we want to get a deterministic behavior, i.e. not based on the running kernel
        // (which is handled by Ruleset and RulesetCreated).
        if ruleset.requested_handled_net.contains(self.allowed_access) {
            Ok(())
        } else {
            Err(AddRuleError::UnhandledAccess {
                access: self.allowed_access,
                incompatible: self.allowed_access & !ruleset.requested_handled_net,
            }
            .into())
        }
    }
}

#[test]
fn net_port_check_consistency() {
    use crate::landlock::*;

    let bind = AccessNet::BindTcp;
    let bind_connect = bind | AccessNet::ConnectTcp;

    assert!(matches!(
        Ruleset::from(ABI::Unsupported)
            .handle_access(bind)
            .unwrap()
            .create()
            .unwrap()
            .add_rule(NetPort::new(1, bind_connect))
            .unwrap_err(),
        RulesetError::AddRules(AddRulesError::Net(AddRuleError::UnhandledAccess { access, incompatible }))
            if access == bind_connect && incompatible == AccessNet::ConnectTcp
    ));
}

impl TryCompat<AccessNet> for NetPort {
    fn try_compat_children<L>(
        mut self,
        abi: ABI,
        parent_level: L,
        compat_state: &mut CompatState,
    ) -> Result<Option<Self>, CompatError<AccessNet>>
    where
        L: Into<CompatLevel>,
    {
        // Checks with our own compatibility level, if any.
        self.allowed_access = match self.allowed_access.try_compat(
            abi,
            self.tailored_compat_level(parent_level),
            compat_state,
        )? {
            Some(a) => a,
            None => return Ok(None),
        };
        Ok(Some(self))
    }

    fn try_compat_inner(
        &mut self,
        _abi: ABI,
    ) -> Result<CompatResult<AccessNet>, CompatError<AccessNet>> {
        Ok(CompatResult::Full)
    }
}

impl OptionCompatLevelMut for NetPort {
    fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
        &mut self.compat_level
    }
}

impl OptionCompatLevelMut for &mut NetPort {
    fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
        &mut self.compat_level
    }
}

impl Compatible for NetPort {}

impl Compatible for &mut NetPort {}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::landlock::*;

    #[test]
    fn test_access_net_from_all_1() {
        assert_eq!(AccessNet::from_all(ABI::Unsupported), AccessNet::EMPTY);
    }

    #[test]
    fn test_access_net_from_all_2() {
        assert_eq!(AccessNet::from_all(ABI::V1), AccessNet::EMPTY);
    }

    #[test]
    fn test_access_net_from_all_3() {
        assert_eq!(AccessNet::from_all(ABI::V2), AccessNet::EMPTY);
    }

    #[test]
    fn test_access_net_from_all_4() {
        assert_eq!(AccessNet::from_all(ABI::V3), AccessNet::EMPTY);
    }

    #[test]
    fn test_access_net_from_all_5() {
        let expected = AccessNet::BindTcp | AccessNet::ConnectTcp;
        assert_eq!(AccessNet::from_all(ABI::V4), expected);
    }

    #[test]
    fn test_access_net_from_all_6() {
        let expected = AccessNet::BindTcp | AccessNet::ConnectTcp;
        assert_eq!(AccessNet::from_all(ABI::V5), expected);
    }

    #[test]
    fn test_access_net_from_all_7() {
        let expected = AccessNet::BindTcp | AccessNet::ConnectTcp;
        assert_eq!(AccessNet::from_all(ABI::V6), expected);
    }

    #[test]
    fn test_access_net_from_all_8() {
        let expected = AccessNet::BindTcp | AccessNet::ConnectTcp;
        assert_eq!(AccessNet::from_all(ABI::V7), expected);
    }

    #[test]
    fn test_is_empty_1() {
        assert!(PrivateAccess::is_empty(AccessNet::EMPTY));
    }

    #[test]
    fn test_is_empty_2() {
        assert!(!PrivateAccess::is_empty(AccessNet::BindTcp));
    }

    #[test]
    fn test_is_empty_3() {
        assert!(!PrivateAccess::is_empty(
            AccessNet::BindTcp | AccessNet::ConnectTcp
        ));
    }

    #[test]
    fn test_into_add_rules_error_1() {
        let err = AddRuleError::UnhandledAccess {
            access: AccessNet::BindTcp,
            incompatible: AccessNet::BindTcp,
        };
        assert!(matches!(
            AccessNet::into_add_rules_error(err),
            AddRulesError::Net(AddRuleError::UnhandledAccess { .. })
        ));
    }

    #[test]
    fn test_into_handle_accesses_error_1() {
        let err = HandleAccessError::Compat(CompatError::Access(AccessError::Empty));
        assert!(matches!(
            AccessNet::into_handle_accesses_error(err),
            HandleAccessesError::Net(HandleAccessError::Compat(CompatError::Access(
                AccessError::Empty
            )))
        ));
    }

    #[test]
    fn test_net_port_new_1() {
        let port = NetPort::new(80, AccessNet::BindTcp);
        assert_eq!(port.port, 80);
        assert_eq!(port.allowed_access, AccessNet::BindTcp);
        assert!(port.compat_level.is_none());
    }

    #[test]
    fn test_net_port_new_2() {
        let port = NetPort::new(0, AccessNet::BindTcp | AccessNet::ConnectTcp);
        assert_eq!(port.port, 0);
        assert_eq!(
            port.allowed_access,
            AccessNet::BindTcp | AccessNet::ConnectTcp
        );
    }

    #[test]
    fn test_option_compat_level_mut_1() {
        let mut port = NetPort::new(443, AccessNet::BindTcp);
        assert!(port.as_option_compat_level_mut().is_none());
        *port.as_option_compat_level_mut() = Some(CompatLevel::BestEffort);
        assert_eq!(
            *port.as_option_compat_level_mut(),
            Some(CompatLevel::BestEffort)
        );
    }

    #[test]
    fn test_option_compat_level_mut_2() {
        let mut port = NetPort::new(443, AccessNet::BindTcp);
        let port_ref = &mut port;
        assert!(port_ref.as_option_compat_level_mut().is_none());
        *port_ref.as_option_compat_level_mut() = Some(CompatLevel::HardRequirement);
        assert_eq!(
            *port_ref.as_option_compat_level_mut(),
            Some(CompatLevel::HardRequirement)
        );
    }

    #[test]
    fn test_try_compat_inner_1() {
        let mut port = NetPort::new(80, AccessNet::BindTcp);
        let result = port.try_compat_inner(ABI::V4);
        assert!(matches!(result, Ok(CompatResult::Full)));
    }

    #[test]
    fn test_try_compat_inner_2() {
        let mut port = NetPort::new(80, AccessNet::ConnectTcp);
        let result = port.try_compat_inner(ABI::Unsupported);
        assert!(matches!(result, Ok(CompatResult::Full)));
    }
}