//
// Syd: rock-solid application kernel
// src/kernel/net/bind.rs: bind(2) handler
//
// Copyright (c) 2023, 2024, 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    net::IpAddr,
    os::fd::{AsRawFd, OwnedFd},
};

use ipnet::IpNet;
use libseccomp::ScmpNotifResp;
use nix::{
    errno::Errno,
    sys::socket::{getsockname, AddressFamily, SockaddrLike, SockaddrStorage},
};

use crate::{
    cookie::{safe_bind, safe_fchdir, safe_umask},
    info,
    lookup::CanonicalPath,
    path::XPathBuf,
    proc::proc_umask,
    req::UNotifyEventRequest,
    sandbox::{Action, AddressPattern, Capability, CidrRule},
    wildmatch::MatchMethod,
};

pub(crate) fn handle_bind(
    fd: OwnedFd,
    addr: (SockaddrStorage, SockaddrStorage),
    root: Option<CanonicalPath>,
    request: &UNotifyEventRequest,
    allow_safe_bind: bool,
) -> Result<ScmpNotifResp, Errno> {
    let (addr, argaddr) = addr;
    if addr.as_unix_addr().and_then(|a| a.path()).is_some() {
        let req = request.scmpreq;
        let mask = proc_umask(req.pid())?;

        // SAFETY:
        // 1. Honour directory for too long sockets.
        //    Note, the current working directory is per-thread here.
        // 2. We cannot resolve symlinks in root or we risk TOCTOU!
        #[expect(clippy::disallowed_methods)]
        let dirfd = root.as_ref().unwrap().dir.as_ref().unwrap();
        safe_fchdir(dirfd)?;

        // SAFETY: Honour process' umask.
        // Note, the umask is per-thread here.
        safe_umask(mask);

        // SAFETY: bind() does not work through dangling
        // symbolic links even with SO_REUSEADDR. When called
        // with a dangling symlink as argument, bind() fails
        // with EADDRINUSE unlike creat() which is going to
        // attempt to create the symlink target. Hence basename
        // in addr here is not vulnerable to TOCTOU.
        safe_bind(&fd, &addr)?;
    } else if let Some(addr) = addr.as_sockaddr_in6() {
        // SAFETY: addr is an IPV6 socket.
        safe_bind(&fd, addr)?;
    } else if let Some(addr) = addr.as_sockaddr_in() {
        // SAFETY: addr is an IPV4 socket.
        safe_bind(&fd, addr)?;
    } else if let Some(addr) = addr.as_alg_addr() {
        // SAFETY: addr is an Algorithm socket.
        safe_bind(&fd, addr)?;
    } else if let Some(addr) = addr.as_link_addr() {
        // SAFETY: addr is a Link socket.
        safe_bind(&fd, addr)?;
    } else if let Some(addr) = addr.as_netlink_addr() {
        // SAFETY: addr is a Netlink socket.
        safe_bind(&fd, addr)?;
    } else if let Some(addr) = addr.as_vsock_addr() {
        // SAFETY: addr is a Vsock socket.
        safe_bind(&fd, addr)?;
    } else if let Some(addr) = addr.as_unix_addr() {
        // SAFETY: addr is a UNIX abstract socket.
        safe_bind(&fd, addr)?;
    } else {
        // SAFETY: addr is not a UNIX domain socket.
        safe_bind(&fd, &addr)?;
    }

    // Handle allow_safe_bind and bind_map.
    // Ignore errors as bind has already succeeded.
    //
    // Configure sandbox:
    // Remove and re-add the address so repeated binds to the
    // same address cannot overflow the vector.
    #[expect(clippy::cognitive_complexity)]
    let _result = (|fd: OwnedFd, request: &UNotifyEventRequest| -> Result<(), Errno> {
        let (addr, port) = match addr.family() {
            Some(AddressFamily::Unix) => {
                let addr = addr.as_unix_addr().ok_or(Errno::EINVAL)?;
                let unix = match (addr.path(), addr.as_abstract()) {
                    (Some(_), _) => {
                        // Case 1: UNIX domain socket
                        //
                        // SAFETY: addr.path()=Some asserts root is Some.
                        #[expect(clippy::disallowed_methods)]
                        let unix = root.unwrap().take();

                        // Handle bind_map after successful bind for UNIX sockets.
                        // We ignore errors because there's nothing we can do about them.
                        // We use original address structure for path for getsockname(2).
                        let _ = request.add_unix(
                            &fd,
                            request.scmpreq.pid(),
                            argaddr.as_unix_addr(),
                            None,
                        );
                        drop(fd); // Close our copy of the socket.

                        if !allow_safe_bind {
                            return Ok(());
                        }

                        unix
                    }
                    (_, Some(path)) => {
                        // Case 2: UNIX abstract socket
                        //
                        // SAFETY: Prefix UNIX abstract sockets with `@' before access check.
                        let mut unix = XPathBuf::from("@");
                        let null = memchr::memchr(0, path).unwrap_or(path.len());
                        unix.append_bytes(&path[..null]);

                        // Handle bind_map after successful bind for UNIX sockets.
                        // We ignore errors because there's nothing we can do
                        // about them.
                        // BindMap is only used for SO_PEERCRED for UNIX abstract sockets.
                        let _ = request.add_unix(&fd, request.scmpreq.pid(), None, None);
                        drop(fd); // Close our copy of the socket.

                        if !allow_safe_bind {
                            return Ok(());
                        }

                        unix
                    }
                    _ => {
                        // Case 3: unnamed UNIX socket.
                        let unix = if addr.len() as usize == size_of::<libc::sa_family_t>() {
                            // Autobind on abstract UNIX socket.
                            getsockname::<SockaddrStorage>(fd.as_raw_fd())?
                                .as_unix_addr()
                                .ok_or(Errno::EINVAL)?
                                .as_abstract()
                                .map(|path| {
                                    // SAFETY: Prefix UNIX abstract sockets with `@' before access check.
                                    let mut unix = XPathBuf::from("@");
                                    let null = memchr::memchr(0, path).unwrap_or(path.len());
                                    unix.append_bytes(&path[..null]);
                                    unix
                                })
                                .ok_or(Errno::EINVAL)?
                        } else {
                            // Use dummy path `!unnamed' for unnamed UNIX sockets.
                            XPathBuf::from("!unnamed")
                        };

                        // Handle bind_map after successful bind for UNIX sockets.
                        // We ignore errors because there's nothing we can do
                        // about them.
                        // BindMap is only used for SO_PEERCRED for UNIX abstract sockets.
                        let _ = request.add_unix(&fd, request.scmpreq.pid(), None, None);
                        drop(fd); // Close our copy of the socket.

                        if !allow_safe_bind {
                            return Ok(());
                        }

                        unix
                    }
                };

                info!("ctx": "bind", "op": "allow_safe_bind",
                    "sys": "bind", "pid": request.scmpreq.pid().as_raw(), "unix": &unix,
                    "msg": format!("add rule `allow/net/connect+{unix}' after bind"));
                let mut sandbox = request.get_mut_sandbox();
                let acl = sandbox.get_acl_mut(Capability::CAP_NET_CONNECT);
                if let Some(idx) = acl.iter().position(|(p, m, a)| {
                    *m == MatchMethod::Literal && *a == Action::Allow && p.is_equal(unix.as_bytes())
                }) {
                    acl.remove(idx);
                }
                return acl.push_front((unix, MatchMethod::Literal, Action::Allow));
            }
            Some(AddressFamily::Inet) => {
                if !allow_safe_bind {
                    return Ok(());
                }

                let addr = addr.as_sockaddr_in().ok_or(Errno::EINVAL)?;
                let mut port = addr.port();
                let addr = IpNet::new_assert(IpAddr::V4(addr.ip()), 32);

                if port == 0 {
                    port = getsockname::<SockaddrStorage>(fd.as_raw_fd())?
                        .as_sockaddr_in()
                        .ok_or(Errno::EINVAL)?
                        .port();
                }
                drop(fd); // Close our copy of the socket.

                (addr, port)
            }
            Some(AddressFamily::Inet6) => {
                if !allow_safe_bind {
                    return Ok(());
                }

                let addr = addr.as_sockaddr_in6().ok_or(Errno::EINVAL)?;
                let mut port = addr.port();
                let addr = addr.ip();
                let addr = if let Some(addr) = addr.to_ipv4_mapped() {
                    IpNet::new_assert(IpAddr::V4(addr), 32)
                } else {
                    IpNet::new_assert(IpAddr::V6(addr), 128)
                };

                if port == 0 {
                    port = getsockname::<SockaddrStorage>(fd.as_raw_fd())?
                        .as_sockaddr_in6()
                        .ok_or(Errno::EINVAL)?
                        .port();
                }
                drop(fd); // Close our copy of the socket.

                (addr, port)
            }
            _ => return Ok(()),
        };

        let addr = AddressPattern {
            addr,
            port: Some(port..=port),
        };
        info!("ctx": "bind", "op": "allow_safe_bind",
            "sys": "bind", "pid": request.scmpreq.pid().as_raw(), "rule": &addr,
            "msg": format!("add rule `allow/net/connect+{addr}' after bind"));

        let rule = CidrRule {
            act: Action::Allow,
            cap: Capability::CAP_NET_CONNECT,
            pat: addr,
        };

        let mut sandbox = request.get_mut_sandbox();
        if let Some(idx) = sandbox.cidr_rules.iter().position(|r| *r == rule) {
            sandbox.cidr_rules.remove(idx);
        }
        sandbox.cidr_rules.push_front(rule)?;

        // 1. The sandbox lock will be released on drop here.
        // 2. The socket fd will be closed on drop here.
        Ok(())
    })(fd, request);

    Ok(request.return_syscall(0))
}
