Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Socket setup: socket, bind, listen, getsockopt, setsockopt. No io_uring equivalent on Linux 5.15.

  2. Synchronous connected operations: connect, accept4, send, recv, sendmsg, recvmsg, shutdown. All have their io_uring equivalent exposed in Phase 2b.

  3. Utility operations: getsockname, getpeername, socketpair.

Cross-cutting characteristics of the family.

  1. Universal CLOEXEC. All socket FDs created have CLOEXEC by default.

  2. NOSIGNAL by default on send. Synchronous sends disable SIGPIPE by default.

  3. Types shared with io_uring. SocketAddr, MessageFlags, AcceptFlags, etc. are the same types as in Phase 2b.

  4. 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:

CategoryMain functions
Setupsocket, bind, listen
Optionstyped getsockopt/setsockopt (~20 options exposed)
Sync connectionconnect, accept4
Sync I/Osend, recv, sendmsg, recvmsg, shutdown
Utilitiesgetsockname, 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:

  • SocketOptionLevel
  • LingerOption
  • 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).