ax-net 0.13.1

Unified network stack for TGOSKits (ArceOS, StarryOS, Axvisor)
//! Shared socket options and protocol wake registration.
//!
//! Protocol-specific sockets embed `GeneralOptions` for common POSIX socket
//! state such as nonblocking mode, reuse-address, timeouts, socket identity, and
//! device binding. Keeping these fields here avoids duplicating subtly
//! different getsockopt/setsockopt behavior in TCP, UDP, raw, Unix, and vsock
//! transports.
//!
//! Blocking, timeout, and signal semantics belong to the consuming OS.  This
//! module only stores the corresponding socket policy and registers protocol
//! wake sources.

use core::{
    sync::atomic::{AtomicBool, AtomicI32, AtomicU8, AtomicU32, AtomicU64, Ordering},
    task::Waker,
    time::Duration,
};

use crate::{
    NetError, NetResult,
    config::{DeviceBinding, InterfaceId},
    get_service, interface_by_id,
    options::{Configurable, GetSocketOption, SetSocketOption},
};

const SO_PRIORITY_UNPRIVILEGED_MAX: i32 = 6;
const IP_TOS_ECN_MASK: u8 = 0x03;

/// Linux IP_PMTUDISC_WANT: use per-route path-MTU discovery. Default for a fresh
/// socket, echoed back by getsockopt(IP_MTU_DISCOVER).
const IP_PMTUDISC_WANT: u8 = 1;
/// Highest valid IP_PMTUDISC_* mode Linux accepts (IP_PMTUDISC_OMIT).
const IP_PMTUDISC_MAX: u8 = 5;

/// General options for all sockets.
pub(crate) struct GeneralOptions {
    /// Whether the socket is non-blocking.
    nonblock: AtomicBool,
    /// Whether the socket should reuse the address.
    reuse_address: AtomicBool,
    /// Whether the socket should reuse the port (SO_REUSEPORT).
    reuse_port: AtomicBool,

    /// Per-socket send timeout in nanoseconds; zero means no timeout.
    send_timeout_nanos: AtomicU64,
    /// Per-socket receive timeout in nanoseconds; zero means no timeout.
    recv_timeout_nanos: AtomicU64,

    /// Bound interface id encoded as zero for "not bound".
    bound_if: AtomicU32,

    /// IP_TOS value used by protocol sockets when marking outgoing packets.
    ip_tos: AtomicU8,
    /// IP_MTU_DISCOVER mode (IP_PMTUDISC_*). Stored for Linux ABI compatibility;
    /// smoltcp does not model path-MTU discovery, so it has no wire effect.
    ip_mtu_discover: AtomicU8,
    /// Whether recvmsg should report IPv4 TOS as IP_TOS ancillary data.
    recv_tos: AtomicBool,
    /// Whether recvmsg should report IPv6 traffic class as IPV6_TCLASS ancillary data.
    recv_traffic_class: AtomicBool,
    /// SO_PRIORITY value. ax-net stores it for Linux compatibility; packet
    /// queue scheduling is not modeled yet.
    priority: AtomicI32,

    /// Socket type: SOCK_STREAM (1), SOCK_DGRAM (2), SOCK_RAW (3).
    socket_type: AtomicI32,
    /// Socket domain: AF_INET (2), AF_UNIX (1), AF_VSOCK (40).
    domain: i32,
    /// IP protocol: IPPROTO_TCP (6), IPPROTO_UDP (17), IPPROTO_ICMP (1), etc.
    protocol: i32,
}
impl GeneralOptions {
    /// Create new GeneralOptions. `socket_type` is the SOCK_* constant
    /// (e.g. SOCK_STREAM=1, SOCK_DGRAM=2, SOCK_RAW=3).
    /// `domain` is the AF_* constant (e.g. AF_INET=2, AF_UNIX=1, AF_VSOCK=40).
    /// `protocol` is the IPPROTO_* constant (e.g. IPPROTO_TCP=6, IPPROTO_UDP=17, IPPROTO_ICMP=1).
    pub fn new(socket_type: i32, domain: i32, protocol: i32) -> Self {
        Self {
            nonblock: AtomicBool::new(false),
            reuse_address: AtomicBool::new(false),
            reuse_port: AtomicBool::new(false),

            send_timeout_nanos: AtomicU64::new(0),
            recv_timeout_nanos: AtomicU64::new(0),

            bound_if: AtomicU32::new(0),

            ip_tos: AtomicU8::new(0),
            ip_mtu_discover: AtomicU8::new(IP_PMTUDISC_WANT),
            recv_tos: AtomicBool::new(false),
            recv_traffic_class: AtomicBool::new(false),
            priority: AtomicI32::new(0),

            socket_type: AtomicI32::new(socket_type),
            domain,
            protocol,
        }
    }

    /// Returns whether this socket is in non-blocking mode.
    pub fn nonblocking(&self) -> bool {
        self.nonblock.load(Ordering::Relaxed)
    }

    /// Returns whether SO_REUSEADDR-style bind reuse is enabled.
    pub fn reuse_address(&self) -> bool {
        self.reuse_address.load(Ordering::Relaxed)
    }

    /// Returns whether SO_REUSEPORT is enabled.
    ///
    /// Under a single-core smoltcp stack there is one accept queue per
    /// endpoint, so port reuse degrades to the same rebind allowance as
    /// SO_REUSEADDR rather than fanning connections across a socket group.
    pub fn reuse_port(&self) -> bool {
        self.reuse_port.load(Ordering::Relaxed)
    }

    /// Updates the interface binding used by route selection.
    pub fn set_device_binding(&self, binding: DeviceBinding) {
        self.bound_if.store(
            binding.bound_if.map_or(0, InterfaceId::get),
            Ordering::Release,
        );
    }

    /// Returns the current interface binding.
    pub fn device_binding(&self) -> DeviceBinding {
        let raw = self.bound_if.load(Ordering::Acquire);
        DeviceBinding {
            bound_if: (raw != 0).then_some(InterfaceId::new(raw)),
        }
    }

    /// Returns the IPv4 TOS / IPv6 traffic-class byte configured on this socket.
    pub fn ip_tos(&self) -> u8 {
        self.ip_tos.load(Ordering::Relaxed)
    }

    /// Updates the IPv4 TOS / IPv6 traffic-class byte configured on this socket.
    pub fn set_ip_tos(&self, tos: u8) {
        self.ip_tos.store(tos & !IP_TOS_ECN_MASK, Ordering::Relaxed);
    }

    /// Returns the IP_MTU_DISCOVER (IP_PMTUDISC_*) mode configured on this socket.
    pub fn ip_mtu_discover(&self) -> u8 {
        self.ip_mtu_discover.load(Ordering::Relaxed)
    }

    /// Updates the IP_MTU_DISCOVER mode. Rejects modes Linux does not define so a
    /// probing client sees the same EINVAL, then stores the mode for readback.
    pub fn set_ip_mtu_discover(&self, mode: u8) -> NetResult<()> {
        if mode > IP_PMTUDISC_MAX {
            return Err(NetError::InvalidInput);
        }
        self.ip_mtu_discover.store(mode, Ordering::Relaxed);
        Ok(())
    }

    /// Returns whether IPv4 TOS ancillary data is enabled for receive calls.
    pub fn recv_tos(&self) -> bool {
        self.recv_tos.load(Ordering::Relaxed)
    }

    /// Updates whether IPv4 TOS ancillary data is enabled for receive calls.
    pub fn set_recv_tos(&self, enabled: bool) {
        self.recv_tos.store(enabled, Ordering::Relaxed);
    }

    /// Returns whether IPv6 traffic-class ancillary data is enabled for receive calls.
    pub fn recv_traffic_class(&self) -> bool {
        self.recv_traffic_class.load(Ordering::Relaxed)
    }

    /// Updates whether IPv6 traffic-class ancillary data is enabled for receive calls.
    pub fn set_recv_traffic_class(&self, enabled: bool) {
        self.recv_traffic_class.store(enabled, Ordering::Relaxed);
    }

    /// Returns the Linux SO_PRIORITY value configured on this socket.
    pub fn priority(&self) -> i32 {
        self.priority.load(Ordering::Relaxed)
    }

    /// Updates SO_PRIORITY using Linux's ordinary unprivileged range.
    pub fn set_priority(&self, priority: i32) -> NetResult<()> {
        if !(0..=SO_PRIORITY_UNPRIVILEGED_MAX).contains(&priority) {
            return Err(NetError::OperationNotPermitted);
        }
        self.priority.store(priority, Ordering::Relaxed);
        Ok(())
    }

    /// Publishes protocol work and registers any protocol deadline for this
    /// socket. Queue IRQs independently schedule their exact poll group.
    pub fn register_waker(&self, waker: &Waker) {
        get_service().register_waker(self.device_binding(), waker);
        crate::request_poll();
    }
}
impl Configurable for GeneralOptions {
    fn get_option_inner(&self, option: &mut GetSocketOption) -> NetResult<bool> {
        use GetSocketOption as O;
        match option {
            O::Error(error) => {
                // TODO(mivik): actual logic
                **error = 0;
            }
            O::NonBlocking(nonblock) => {
                **nonblock = self.nonblocking();
            }
            O::ReuseAddress(reuse) => {
                **reuse = self.reuse_address();
            }
            O::ReusePort(reuse) => {
                **reuse = self.reuse_port();
            }
            O::SendTimeout(timeout) => {
                **timeout = Duration::from_nanos(self.send_timeout_nanos.load(Ordering::Relaxed));
            }
            O::ReceiveTimeout(timeout) => {
                **timeout = Duration::from_nanos(self.recv_timeout_nanos.load(Ordering::Relaxed));
            }
            O::RecvErr(val) => {
                **val = false;
            }
            O::IpTos(tos) => {
                **tos = self.ip_tos.load(Ordering::Relaxed);
            }
            O::IpMtuDiscover(mode) => {
                **mode = self.ip_mtu_discover();
            }
            O::RecvTos(enabled) => {
                **enabled = self.recv_tos();
            }
            O::RecvTrafficClass(enabled) => {
                **enabled = self.recv_traffic_class();
            }
            O::Priority(priority) => {
                **priority = self.priority();
            }
            O::SocketType(t) => {
                **t = self.socket_type.load(Ordering::Relaxed);
            }
            O::SocketProtocol(proto) => {
                **proto = self.protocol;
            }
            O::SocketDomain(domain) => {
                **domain = self.domain;
            }
            O::BindToDevice(binding) => {
                **binding = self.device_binding().bound_if;
            }
            _ => return Ok(false),
        }
        Ok(true)
    }

    fn set_option_inner(&self, option: SetSocketOption) -> NetResult<bool> {
        use SetSocketOption as O;

        match option {
            O::NonBlocking(nonblock) => {
                self.nonblock.store(*nonblock, Ordering::Relaxed);
            }
            O::ReuseAddress(reuse) => {
                self.reuse_address.store(*reuse, Ordering::Relaxed);
            }
            O::ReusePort(reuse) => {
                self.reuse_port.store(*reuse, Ordering::Relaxed);
            }
            O::SendTimeout(timeout) => {
                self.send_timeout_nanos
                    .store(timeout.as_nanos() as u64, Ordering::Relaxed);
            }
            O::ReceiveTimeout(timeout) => {
                self.recv_timeout_nanos
                    .store(timeout.as_nanos() as u64, Ordering::Relaxed);
            }
            O::SendBuffer(_) | O::ReceiveBuffer(_) => {
                // TODO(mivik): implement buffer size options
            }
            O::BindToDevice(interface_id) => {
                if let Some(id) = *interface_id
                    && interface_by_id(id).is_none()
                {
                    return Err(NetError::NoSuchDevice);
                }
                self.set_device_binding(DeviceBinding {
                    bound_if: *interface_id,
                });
            }
            O::RecvErr(_) => {
                // TODO: Retrieve ICMP errors via errqueue
            }
            O::IpTos(tos) => {
                self.set_ip_tos(*tos);
            }
            O::IpMtuDiscover(mode) => {
                self.set_ip_mtu_discover(*mode)?;
            }
            O::RecvTos(enabled) => {
                self.set_recv_tos(*enabled);
            }
            O::RecvTrafficClass(enabled) => {
                self.set_recv_traffic_class(*enabled);
            }
            O::Priority(priority) => {
                self.set_priority(*priority)?;
            }
            O::SocketType(_) | O::SocketProtocol(_) | O::SocketDomain(_) => {
                // Read-only options
                return Err(NetError::ProtocolOptionUnsupported);
            }
            _ => return Ok(false),
        }
        Ok(true)
    }
}

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

    #[test]
    fn reuse_address_and_reuse_port_are_independent_flags() {
        let options = GeneralOptions::new(1, 2, 6);

        assert!(!options.reuse_address());
        assert!(!options.reuse_port());

        options
            .set_option(SetSocketOption::ReusePort(&true))
            .unwrap();
        assert!(options.reuse_port());
        assert!(!options.reuse_address());

        let mut reuse_port = false;
        options
            .get_option(GetSocketOption::ReusePort(&mut reuse_port))
            .unwrap();
        assert!(reuse_port);

        options
            .set_option(SetSocketOption::ReusePort(&false))
            .unwrap();
        assert!(!options.reuse_port());
    }

    #[test]
    fn socket_priority_matches_unprivileged_linux_range() {
        let options = GeneralOptions::new(1, 2, 6);

        assert_eq!(options.priority(), 0);
        options.set_priority(6).unwrap();
        assert_eq!(options.priority(), 6);

        assert_eq!(
            options.set_priority(7).unwrap_err(),
            NetError::OperationNotPermitted
        );
        assert_eq!(
            options.set_priority(-1).unwrap_err(),
            NetError::OperationNotPermitted
        );
        assert_eq!(options.priority(), 6);
    }

    #[test]
    fn ip_tos_storage_masks_user_controlled_ecn_bits() {
        let options = GeneralOptions::new(1, 2, 6);

        options.set_ip_tos(0x2e);
        assert_eq!(options.ip_tos(), 0x2c);

        options.set_ip_tos(0xff);
        assert_eq!(options.ip_tos(), 0xfc);
    }

    #[test]
    fn receive_qos_metadata_toggles_are_independent() {
        let options = GeneralOptions::new(2, 2, 17);

        assert!(!options.recv_tos());
        assert!(!options.recv_traffic_class());

        options.set_recv_tos(true);
        assert!(options.recv_tos());
        assert!(!options.recv_traffic_class());

        options.set_recv_traffic_class(true);
        assert!(options.recv_tos());
        assert!(options.recv_traffic_class());

        options.set_recv_tos(false);
        assert!(!options.recv_tos());
        assert!(options.recv_traffic_class());
    }
}