diff --git a/src/backend/libc/net/syscalls.rs b/src/backend/libc/net/syscalls.rs index 75d63e1b3..97b862033 100644 --- a/src/backend/libc/net/syscalls.rs +++ b/src/backend/libc/net/syscalls.rs @@ -248,6 +248,19 @@ pub(crate) fn connect_unix(sockfd: BorrowedFd<'_>, addr: &SocketAddrUnix) -> io: } } +#[cfg(not(any(target_os = "redox", target_os = "wasi")))] +pub(crate) fn connect_unspec(sockfd: BorrowedFd<'_>) -> io::Result<()> { + debug_assert_eq!(c::AF_UNSPEC, 0); + let addr = MaybeUninit::::zeroed(); + unsafe { + ret(c::connect( + borrowed_fd(sockfd), + as_ptr(&addr).cast(), + size_of::() as c::socklen_t, + )) + } +} + #[cfg(not(any(target_os = "redox", target_os = "wasi")))] pub(crate) fn listen(sockfd: BorrowedFd<'_>, backlog: c::c_int) -> io::Result<()> { unsafe { ret(c::listen(borrowed_fd(sockfd), backlog)) } diff --git a/src/backend/linux_raw/net/syscalls.rs b/src/backend/linux_raw/net/syscalls.rs index 908cf773f..f513f1261 100644 --- a/src/backend/linux_raw/net/syscalls.rs +++ b/src/backend/linux_raw/net/syscalls.rs @@ -904,6 +904,34 @@ pub(crate) fn connect_unix(fd: BorrowedFd<'_>, addr: &SocketAddrUnix) -> io::Res } } +#[inline] +pub(crate) fn connect_unspec(fd: BorrowedFd<'_>) -> io::Result<()> { + debug_assert_eq!(c::AF_UNSPEC, 0); + let addr = MaybeUninit::::zeroed(); + + #[cfg(not(target_arch = "x86"))] + unsafe { + ret(syscall_readonly!( + __NR_connect, + fd, + by_ref(&addr), + size_of::() + )) + } + #[cfg(target_arch = "x86")] + unsafe { + ret(syscall_readonly!( + __NR_socketcall, + x86_sys(SYS_CONNECT), + slice_just_addr::, _>(&[ + fd.into(), + by_ref(&addr), + size_of::(), + ]) + )) + } +} + #[inline] pub(crate) fn listen(fd: BorrowedFd<'_>, backlog: c::c_int) -> io::Result<()> { #[cfg(not(target_arch = "x86"))] diff --git a/src/net/socket.rs b/src/net/socket.rs index 8727ca53b..ca70d61f5 100644 --- a/src/net/socket.rs +++ b/src/net/socket.rs @@ -453,6 +453,41 @@ pub fn connect_unix(sockfd: Fd, addr: &SocketAddrUnix) -> io::Result<( backend::net::syscalls::connect_unix(sockfd.as_fd(), addr) } +/// `connect(sockfd, {.sa_family = AF_UNSPEC}, sizeof(struct sockaddr))` +/// — Dissolve the socket's association. +/// +/// On UDP sockets, BSD platforms report AFNOSUPPORT or INVAL even if the disconnect was successful. +/// +/// # References +/// - [Beej's Guide to Network Programming] +/// - [POSIX] +/// - [Linux] +/// - [Apple] +/// - [Winsock2] +/// - [FreeBSD] +/// - [NetBSD] +/// - [OpenBSD] +/// - [DragonFly BSD] +/// - [illumos] +/// - [glibc] +/// +/// [Beej's Guide to Network Programming]: https://beej.us/guide/bgnet/html/split/system-calls-or-bust.html#connect +/// [POSIX]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/connect.html +/// [Linux]: https://man7.org/linux/man-pages/man2/connect.2.html +/// [Apple]: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/connect.2.html +/// [Winsock2]: https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-connect +/// [FreeBSD]: https://man.freebsd.org/cgi/man.cgi?query=connect&sektion=2 +/// [NetBSD]: https://man.netbsd.org/connect.2 +/// [OpenBSD]: https://man.openbsd.org/connect.2 +/// [DragonFly BSD]: https://man.dragonflybsd.org/?command=connect§ion=2 +/// [illumos]: https://illumos.org/man/3SOCKET/connect +/// [glibc]: https://www.gnu.org/software/libc/manual/html_node/Connecting.html +#[inline] +#[doc(alias = "connect")] +pub fn connect_unspec(sockfd: Fd) -> io::Result<()> { + backend::net::syscalls::connect_unspec(sockfd.as_fd()) +} + /// `listen(fd, backlog)`—Enables listening for incoming connections. /// /// # References diff --git a/tests/net/connect_bind_send.rs b/tests/net/connect_bind_send.rs index 1622f3daf..95e68526e 100644 --- a/tests/net/connect_bind_send.rs +++ b/tests/net/connect_bind_send.rs @@ -194,6 +194,94 @@ fn net_v6_connect() { assert_eq!(request, &response[..n]); } +/// Test `connect_unspec`. +#[test] +fn net_v4_connect_unspec() { + const SOME_PORT: u16 = 47; + let localhost_addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, SOME_PORT); + + let socket = rustix::net::socket(AddressFamily::INET, SocketType::DGRAM, None).unwrap(); + + rustix::net::connect_v4(&socket, &localhost_addr).expect("connect_v4"); + assert_eq!(getsockname_v4(&socket).unwrap().ip(), &Ipv4Addr::LOCALHOST); + assert_eq!(getpeername_v4(&socket).unwrap(), localhost_addr); + + match rustix::net::connect_unspec(&socket) { + // BSD platforms return an error even if the socket was disconnected successfully. + #[cfg(bsd)] + Err(rustix::io::Errno::INVAL | rustix::io::Errno::AFNOSUPPORT) => {} + r => r.expect("connect_unspec"), + } + assert_eq!( + getsockname_v4(&socket).unwrap().ip(), + &Ipv4Addr::UNSPECIFIED + ); + assert_eq!(getpeername_v4(&socket), Err(rustix::io::Errno::NOTCONN)); + + rustix::net::connect_v4(&socket, &localhost_addr).expect("connect_v4"); + assert_eq!(getsockname_v4(&socket).unwrap().ip(), &Ipv4Addr::LOCALHOST); + assert_eq!(getpeername_v4(&socket).unwrap(), localhost_addr); + + fn getsockname_v4(sockfd: Fd) -> rustix::io::Result { + match rustix::net::getsockname(sockfd)? { + SocketAddrAny::V4(addr_v4) => Ok(addr_v4), + _ => Err(rustix::io::Errno::AFNOSUPPORT), + } + } + + fn getpeername_v4(sockfd: Fd) -> rustix::io::Result { + match rustix::net::getpeername(sockfd)? { + Some(SocketAddrAny::V4(addr_v4)) => Ok(addr_v4), + None => Err(rustix::io::Errno::NOTCONN), + _ => Err(rustix::io::Errno::AFNOSUPPORT), + } + } +} + +/// Test `connect_unspec`. +#[test] +fn net_v6_connect_unspec() { + const SOME_PORT: u16 = 47; + let localhost_addr = SocketAddrV6::new(Ipv6Addr::LOCALHOST, SOME_PORT, 0, 0); + + let socket = rustix::net::socket(AddressFamily::INET6, SocketType::DGRAM, None).unwrap(); + + rustix::net::connect_v6(&socket, &localhost_addr).expect("connect_v6"); + assert_eq!(getsockname_v6(&socket).unwrap().ip(), &Ipv6Addr::LOCALHOST); + assert_eq!(getpeername_v6(&socket).unwrap(), localhost_addr); + + match rustix::net::connect_unspec(&socket) { + // BSD platforms return an error even if the socket was disconnected successfully. + #[cfg(bsd)] + Err(rustix::io::Errno::INVAL | rustix::io::Errno::AFNOSUPPORT) => {} + r => r.expect("connect_unspec"), + } + assert_eq!( + getsockname_v6(&socket).unwrap().ip(), + &Ipv6Addr::UNSPECIFIED + ); + assert_eq!(getpeername_v6(&socket), Err(rustix::io::Errno::NOTCONN)); + + rustix::net::connect_v6(&socket, &localhost_addr).expect("connect_v6"); + assert_eq!(getsockname_v6(&socket).unwrap().ip(), &Ipv6Addr::LOCALHOST); + assert_eq!(getpeername_v6(&socket).unwrap(), localhost_addr); + + fn getsockname_v6(sockfd: Fd) -> rustix::io::Result { + match rustix::net::getsockname(sockfd)? { + SocketAddrAny::V6(addr_v6) => Ok(addr_v6), + _ => Err(rustix::io::Errno::AFNOSUPPORT), + } + } + + fn getpeername_v6(sockfd: Fd) -> rustix::io::Result { + match rustix::net::getpeername(sockfd)? { + Some(SocketAddrAny::V6(addr_v6)) => Ok(addr_v6), + None => Err(rustix::io::Errno::NOTCONN), + _ => Err(rustix::io::Errno::AFNOSUPPORT), + } + } +} + /// Test `bind_any`. #[test] fn net_v4_bind_any() {