Layer 0 Spec — net Family
Technical specification — Version 1.0
Family overview
The air-sys-syscall::net module exposes the synchronous socket primitives. In accordance with Decision 2 of ADR-022, the operations exist alongside their io_uring equivalents, with shared types and conventions.
Family scope.
Three categories of operations:
-
Socket setup:
socket,bind,listen,getsockopt,setsockopt. No io_uring equivalent on Linux 5.15. -
Synchronous connected operations:
connect,accept4,send,recv,sendmsg,recvmsg,shutdown. All have their io_uring equivalent exposed in Phase 2b. -
Utility operations:
getsockname,getpeername,socketpair.
Cross-cutting characteristics of the family.
-
Universal CLOEXEC. All socket FDs created have CLOEXEC by default.
-
NOSIGNAL by default on send. Synchronous sends disable SIGPIPE by default.
-
Types shared with io_uring.
SocketAddr,MessageFlags,AcceptFlags, etc. are the same types as in Phase 2b. -
Family validation. The functions validate the consistency between the socket family and the address used.
Subsection 1: Socket setup
socket
#![allow(unused)]
fn main() {
pub fn socket(
domain: SocketDomain,
ty: SocketType,
protocol: i32,
) -> Result<OwnedFd, Errno>;
}
The Air wrapper uses SOCK_CLOEXEC systematically.
bind
#![allow(unused)]
fn main() {
pub fn bind(sock: BorrowedFd<'_>, address: &SocketAddr) -> Result<(), Errno>;
}
For path-based Unix sockets, the correct pattern is to attempt unlinkat before bind, ignoring ENOENT. An Air helper in layer 1 (bind_unix_socket_replacing) will make this pattern easier.
listen
#![allow(unused)]
fn main() {
pub fn listen(sock: BorrowedFd<'_>, backlog: u32) -> Result<(), Errno>;
}
getsockopt and setsockopt
API typed per option (cf. convention 3 of ADR-021). Rather than a generic wrapper, Air exposes one function per common option:
#![allow(unused)]
fn main() {
pub fn get_so_keepalive(sock: BorrowedFd<'_>) -> Result<bool, Errno>;
pub fn set_so_keepalive(sock: BorrowedFd<'_>, enable: bool) -> Result<(), Errno>;
pub fn get_so_reuseaddr(sock: BorrowedFd<'_>) -> Result<bool, Errno>;
pub fn set_so_reuseaddr(sock: BorrowedFd<'_>, enable: bool) -> Result<(), Errno>;
pub fn get_so_reuseport(sock: BorrowedFd<'_>) -> Result<bool, Errno>;
pub fn set_so_reuseport(sock: BorrowedFd<'_>, enable: bool) -> Result<(), Errno>;
pub fn get_tcp_nodelay(sock: BorrowedFd<'_>) -> Result<bool, Errno>;
pub fn set_tcp_nodelay(sock: BorrowedFd<'_>, enable: bool) -> Result<(), Errno>;
pub fn get_so_rcvbuf(sock: BorrowedFd<'_>) -> Result<u32, Errno>;
pub fn set_so_rcvbuf(sock: BorrowedFd<'_>, size: u32) -> Result<(), Errno>;
pub fn get_so_sndbuf(sock: BorrowedFd<'_>) -> Result<u32, Errno>;
pub fn set_so_sndbuf(sock: BorrowedFd<'_>, size: u32) -> Result<(), Errno>;
pub fn get_so_error(sock: BorrowedFd<'_>) -> Result<Option<Errno>, Errno>;
pub fn get_so_peercred(sock: BorrowedFd<'_>) -> Result<UnixCredentials, Errno>;
pub fn get_so_linger(sock: BorrowedFd<'_>) -> Result<LingerOption, Errno>;
pub fn set_so_linger(sock: BorrowedFd<'_>, linger: LingerOption) -> Result<(), Errno>;
pub enum LingerOption {
Disabled,
Enabled(Duration),
}
pub fn set_so_bindtodevice(sock: BorrowedFd<'_>, ifname: &CStr) -> Result<(), Errno>;
pub fn get_so_bindtodevice(sock: BorrowedFd<'_>) -> Result<CString, Errno>;
// TCP-specific
pub fn set_tcp_keepidle(sock: BorrowedFd<'_>, seconds: u32) -> Result<(), Errno>;
pub fn set_tcp_keepintvl(sock: BorrowedFd<'_>, seconds: u32) -> Result<(), Errno>;
pub fn set_tcp_keepcnt(sock: BorrowedFd<'_>, count: u32) -> Result<(), Errno>;
// IP-specific
pub fn set_ip_ttl(sock: BorrowedFd<'_>, ttl: u8) -> Result<(), Errno>;
pub fn set_ipv6_v6only(sock: BorrowedFd<'_>, v6only: bool) -> Result<(), Errno>;
}
If a developer needs an option not covered, there are two paths: an RFC to add the wrapper function, or the raw API in air-sys-syscall::net::raw.
Subsection 2: Synchronous operations
connect
#![allow(unused)]
fn main() {
pub fn connect(sock: BorrowedFd<'_>, address: &SocketAddr) -> Result<(), Errno>;
}
For non-blocking sockets, returns EINPROGRESS immediately.
accept4
#![allow(unused)]
fn main() {
pub fn accept4(
listener: BorrowedFd<'_>,
flags: AcceptFlags,
) -> Result<AcceptResult, Errno>;
}
AcceptFlags::CLOEXEC enabled by default. accept4 is preferred over accept because it allows passing CLOEXEC atomically.
send, recv, sendmsg, recvmsg, shutdown
#![allow(unused)]
fn main() {
pub fn send(
sock: BorrowedFd<'_>,
buf: &[u8],
flags: MessageFlags,
) -> Result<usize, Errno>;
pub fn recv(
sock: BorrowedFd<'_>,
buf: &mut [u8],
flags: MessageFlags,
) -> Result<usize, Errno>;
pub fn sendmsg(
sock: BorrowedFd<'_>,
request: &SendMessageRequest,
) -> Result<SendMessageResult, Errno>;
pub fn recvmsg(
sock: BorrowedFd<'_>,
request: &mut ReceiveMessageRequest,
) -> Result<ReceiveMessageResult, Errno>;
pub fn shutdown(
sock: BorrowedFd<'_>,
mode: ShutdownMode,
) -> Result<(), Errno>;
}
Semantic difference with io_uring.
The synchronous versions take references (&[u8], &mut [u8]) rather than owned values. No ownership transfer is needed since the operation is synchronous.
NOSIGNAL by default. As in io_uring, MessageFlags::NOSIGNAL is enabled by default in the synchronous send wrapper.
FD passing via sendmsg/recvmsg. Identical to Phase 2b: received FDs auto-wrapped into OwnedFd, typed ancillary data.
Subsection 3: Utility operations
getsockname and getpeername
#![allow(unused)]
fn main() {
pub fn getsockname(sock: BorrowedFd<'_>) -> Result<SocketAddr, Errno>;
pub fn getpeername(sock: BorrowedFd<'_>) -> Result<SocketAddr, Errno>;
}
socketpair
#![allow(unused)]
fn main() {
pub fn socketpair(
domain: SocketDomain,
ty: SocketType,
protocol: i32,
) -> Result<(OwnedFd, OwnedFd), Errno>;
}
Only Unix sockets support socketpair on Linux. CLOEXEC enabled by default on both FDs.
net family summary
Functions exposed:
| Category | Main functions |
|---|---|
| Setup | socket, bind, listen |
| Options | typed getsockopt/setsockopt (~20 options exposed) |
| Sync connection | connect, accept4 |
| Sync I/O | send, recv, sendmsg, recvmsg, shutdown |
| Utilities | getsockname, getpeername, socketpair |
Total: ~30 main public functions.
Non-wrapped syscalls (listed in UNSUPPORTED.md):
accept(without the 4): replaced by accept4.sendto,recvfrom: covered via sendmsg/recvmsg.sendmmsg,recvmmsg: batched operations, to be evaluated for future addition.
Types added to air-sys-types
Mainly reuse of types already introduced in Phase 2b (SocketAddr, MessageFlags, etc.). A few additions:
SocketOptionLevelLingerOption- Various constants for the options
That is ~3 additional types.
Underlying decisions that emerged in the net family
1. API typed per option for setsockopt/getsockopt.
Strict application of convention 3 of ADR-021. The cost in code volume is offset by the safety and per-option documentation.
2. No non-accept4 accept wrapper.
accept4 is strictly superior.
3. NOSIGNAL by default.
Consistent with io_uring. SIGPIPE is almost always undesirable.
4. socketpair returns a tuple.
The idiomatic Rust choice.
Document license: MPL 2.0
Status: Technical specification of the air-sys-syscall::net module (layer 0).