Skip to main content

oxirush_sctp/
lib.rs

1//! Async SCTP sockets for [Tokio](https://tokio.rs) on Linux, over the SCTP
2//! of the kernel and its sockets API
3//! ([RFC 6458](https://www.rfc-editor.org/rfc/rfc6458.html)). `send` and
4//! `recv` move one whole message, with its stream and its payload protocol
5//! identifier.
6//!
7//! ```rust
8//! use oxirush_sctp::{NotificationOrData, SendOptions, Socket, SocketToAssociation};
9//!
10//! #[tokio::main(flavor = "current_thread")]
11//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
12//!     // Stream 1, and the payload protocol identifier of NGAP
13//!     let options = SendOptions::new().stream(1).ppid(60);
14//!
15//!     let socket = Socket::new_v4(SocketToAssociation::OneToOne)?;
16//!     socket.bind("127.0.0.1:0".parse()?)?;
17//!     let listener = socket.listen(5)?;
18//!     let address = listener.local_addr()?;
19//!
20//!     let server = async {
21//!         let (association, _peer) = listener.accept().await?;
22//!         if let Some(NotificationOrData::Data(message)) = association.recv().await? {
23//!             let text = String::from_utf8_lossy(&message.payload);
24//!             let (stream, ppid) = (message.stream_id(), message.ppid());
25//!             println!("server: {text:?} on stream {stream:?} with PPID {ppid:?}");
26//!             association.send(b"pong", options).await?;
27//!         }
28//!         std::io::Result::Ok(())
29//!     };
30//!     let client = async {
31//!         let socket = Socket::new_v4(SocketToAssociation::OneToOne)?;
32//!         let (association, _) = socket.connect(address).await?;
33//!         association.send(b"ping", options).await?;
34//!         if let Some(NotificationOrData::Data(message)) = association.recv().await? {
35//!             println!("client: {:?}", String::from_utf8_lossy(&message.payload));
36//!         }
37//!         std::io::Result::Ok(())
38//!     };
39//!     let (server, client) = tokio::join!(server, client);
40//!     Ok(server.and(client)?)
41//! }
42//! ```
43//!
44//! ```text
45//! server: "ping" on stream Some(1) with PPID Some(60)
46//! client: "pong"
47//! ```
48//!
49//! # Tasks
50//!
51//! The [README](https://github.com/linouxis9/oxirush-sctp#readme) has a
52//! program for each, with what it prints.
53//!
54//! | Task | Items |
55//! |------|-------|
56//! | An NGAP association, with a writer and a reader | [`driver::spawn`], [`driver::DriverConfig`], [`driver::Writer::overload`], [`InitParams`] |
57//! | Notifications | [`SocketOptions::subscribe_events`], [`Event`], [`Notification`] |
58//! | The end of an association | [`ConnectedSocket::recv`], [`ConnectedSocket::close`] |
59//! | Multihoming | [`Socket::bindx`], [`Socket::connectx`], [`SocketOptions::set_peer_address_params`] |
60//! | Options and status | [`SocketOptions`], [`ConnectedSocket::status`] |
61//! | One socket for many associations | [`Socket::into_endpoint`], [`OneToManyEndpoint`], [`OneToManyEndpoint::peeloff`] |
62//! | Many messages a call | [`ConnectedSocket::send_many`], [`ConnectedSocket::recv_many`] |
63//!
64//! # Requirements
65//!
66//! - Linux 5.0 or later with SCTP. `sudo modprobe sctp` loads the kernel
67//!   module, which some distributions ship in a separate package of extra
68//!   kernel modules. Without it, creating a socket fails with
69//!   `EPROTONOSUPPORT`, or `ESOCKTNOSUPPORT` for a one-to-many socket.
70//! - A Tokio runtime with the I/O driver (`#[tokio::main]`, or `enable_io` on
71//!   a runtime builder). Sockets register with it when they are created, and
72//!   panic without it.
73//! - Rust 1.88 or later.
74//!
75//! The crate makes the system calls itself, without `libsctp`. Diagnostics go
76//! through [`tracing`](https://crates.io/crates/tracing).
77//!
78//! # Socket types
79//!
80//! | Type | Comes from | Does |
81//! | --- | --- | --- |
82//! | [`Socket`] | `Socket::new_v4`, `Socket::new_v6` | binds, then becomes one of the next three |
83//! | [`Listener`] | `Socket::listen` | accepts associations |
84//! | [`ConnectedSocket`] | `Socket::connect`, `Socket::connectx`, `Listener::accept`, `OneToManyEndpoint::peeloff` | sends and receives on one association |
85//! | [`OneToManyEndpoint`] | `Socket::into_endpoint` | sends and receives on many associations |
86//! | [`SocketOptions`] | `options()` on the four above | reads and sets options |
87//!
88//! Every socket type has `options()`. What is set before `listen` or `connect`
89//! stays, and accepted associations inherit it: subscribe to events before
90//! listening.
91//!
92//! On a one-to-many socket, `connect` starts an association without waiting
93//! for it: subscribe to `Event::Association` to learn whether it came up.
94//! `send_to` sends to an address, and starts an association if there is none.
95//! `peeloff` moves one association to a [`ConnectedSocket`] of its own.
96//!
97//! `bindx` binds several local addresses and `connectx` gives several
98//! addresses of the peer. The addresses of one call share a port, which the
99//! kernel chooses for port 0. Subscribe to `Event::Address` to receive a
100//! `Notification::PeerAddressChange` when a path changes state.
101//! `options().peer_address_params`, `set_peer_address_params` and
102//! `request_heartbeat` read and set the heartbeat and the retransmission limit
103//! of a path.
104//!
105//! # Messages
106//!
107//! - `recv` returns a whole message, however many reads it takes. A dropped
108//!   `recv` future loses nothing: the next call continues the message.
109//! - A received message longer than `options().max_message_size()`, 4 MiB by
110//!   default, is discarded and reported as an `InvalidData` error. The next
111//!   `recv` returns the message after it. A notification is never discarded.
112//! - `recv_into` receives into a buffer of the caller, without an allocation
113//!   when one read gives the message whole.
114//! - `poll_recv`, `poll_recv_into`, `poll_send`, `poll_send_to` and
115//!   `poll_accept` are the calls as polls, for a `Stream` or a future of a
116//!   program's own: they wait for the socket to be readable or writable, and end
117//!   and fail as the async calls do. A one-to-one socket subscribes to its
118//!   association changes for itself, so that the end of its association makes
119//!   it readable, and passes them on only to a caller that subscribes to them.
120//! - `send` waits for room in the send buffer. A dropped `send` future has
121//!   sent nothing.
122//! - `send_many` and `recv_many` move a batch with one `sendmmsg` or
123//!   `recvmmsg`. `send_many` returns how many messages the socket took, a
124//!   prefix of the batch; the kernel puts the messages of an association
125//!   that follow one another in as few packets as it can (`MSG_MORE`), and
126//!   holds none back past the call. `recv_many` returns whole messages, as
127//!   `recv` does. [`driver`] runs them in a task each.
128//! - A message shorter than a packet, sent while the one before it is not
129//!   acknowledged, waits for that acknowledgement, which the peer may delay by
130//!   200 ms: SCTP gathers small messages as TCP does. With
131//!   `options().set_nodelay(true)` it leaves at once. The ping-pong of the
132//!   README takes 200 ms a message without it, and 8.0 µs with it.
133//! - A received message holds the memory of its octets and no more.
134//! - A message longer than the receive buffer of its receiver (`SO_RCVBUF`)
135//!   takes seconds: Linux carried 1 MB in 2.7 s with the default buffers, as
136//!   a program in C does, and now and then stops delivering one that is many
137//!   times longer, nothing more of it readable while the sender sends its
138//!   parts again. `options().set_recv_buffer_size` to the longest message
139//!   avoids both.
140//! - A sent message must fit in the send buffer (`SO_SNDBUF`): a longer one
141//!   is `InvalidInput`, as are an empty one and a stream that the
142//!   association does not have: the error says which.
143//! - `ReceivedData::stream_id`, `ppid` and `assoc_id` read the receive
144//!   information (`SCTP_RCVINFO`) that every socket requests. They return
145//!   `None` after `options().request_rcvinfo(false)`.
146//!
147//! # The end of an association
148//!
149//! On a [`ConnectedSocket`], `recv` returns `None` for an association that was
150//! shut down and an error for one that was aborted or lost:
151//!
152//! ```no_run
153//! # async fn serve(association: oxirush_sctp::ConnectedSocket) -> std::io::Result<()> {
154//! while let Some(received) = association.recv().await? {
155//!     println!("{received:?}");
156//! }
157//! // The peer has closed, or this side has shut the association down
158//! # Ok(())
159//! # }
160//! ```
161//!
162//! Linux reports the end once, to the call that happens to be there. The crate
163//! records it wherever it learns of it, in `recv`, in `send` or in a
164//! notification, and every later call returns it at once: none waits.
165//!
166//! | What happened | `recv` returns, at each call | `send` returns, at each call | Notifications, when subscribed |
167//! | --- | --- | --- | --- |
168//! | The peer closed its socket or shut the association down | what the peer had sent, then `None` | `BrokenPipe` | `Shutdown`, then `AssociationChange` with `ShutdownComplete` |
169//! | The peer aborted, or was dropped with a message that it had not read | `ConnectionReset` | `ConnectionReset` | `AssociationChange` with `CommLost`, before the error |
170//! | The peer was lost: nothing was acknowledged up to the retransmission limit | `TimedOut` | `TimedOut` | `AssociationChange` with `CommLost`, before the error |
171//! | This side called `shutdown(Shutdown::Write)` | what the peer had still sent, then `None` once the peer has answered | `BrokenPipe`, from the call on | `AssociationChange` with `ShutdownComplete` |
172//! | This side called `shutdown(Shutdown::Read)` | `None`, from the call on | sends: the association lives | none |
173//! | This side called `shutdown(Shutdown::Both)` | `None`, from the call on | `BrokenPipe`, from the call on | `AssociationChange` with `ShutdownComplete` |
174//! | `connect` failed | nothing: `connect` returns the error, such as `ConnectionRefused` or `TimedOut`, and has consumed the socket | | |
175//!
176//! Each row is the same whichever call learns of the end first: a `send` that
177//! takes the error of an abort leaves it for `recv`, which first returns what
178//! the kernel still holds of the association. A [`OneToManyEndpoint`] learns of
179//! the end of an association from the notifications only.
180//!
181//! `close(deadline)` on a [`ConnectedSocket`] loses nothing that was sent: it
182//! shuts the association down for writing, reads until the peer has answered
183//! and returns the messages that the peer had still sent. A dropped socket is
184//! closed by Linux, which shuts the association down in the background when
185//! everything that it received was read, and aborts it when a message or a
186//! notification was not: what this side had sent and the peer had not
187//! acknowledged is then lost. With `options().set_linger(Some(Duration::ZERO))`
188//! a drop always aborts, as `abort` does at once. Other durations are refused,
189//! because closing would then block the runtime.
190//!
191//! # Payload protocol identifiers
192//!
193//! A payload protocol identifier is a plain number in every call and in every
194//! value: 60 is NGAP, in `SendOptions::ppid`, `SendInfo::ppid`,
195//! `ReceivedData::ppid`, `RcvInfo::ppid` and `NxtInfo::ppid`. The crate converts
196//! it to the network byte order that the kernel carries.
197//!
198//! # Addresses
199//!
200//! A socket that connects without being bound offers the peer every address
201//! of the host, loopback and the others: the peer may then send to any of
202//! them. `bind` or `bindx` before `connect` to offer what was chosen:
203//!
204//! ```
205//! # #[tokio::main(flavor = "current_thread")]
206//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
207//! use oxirush_sctp::{Socket, SocketToAssociation};
208//! # let listener = Socket::new_v4(SocketToAssociation::OneToOne)?;
209//! # listener.bind("127.0.0.1:0".parse()?)?;
210//! # let listener = listener.listen(1)?;
211//! # let server = listener.local_addr()?;
212//!
213//! let client = Socket::new_v4(SocketToAssociation::OneToOne)?;
214//! client.bind("127.0.0.1:0".parse()?)?;
215//! let (association, _) = client.connect(server).await?;
216//! # let (accepted, _) = listener.accept().await?;
217//! // The peer sees the one address that was bound
218//! assert_eq!(accepted.peer_addrs()?.len(), 1);
219//! # let _ = association;
220//! # Ok(())
221//! # }
222//! ```
223//!
224//! # What it covers
225//!
226//! Sockets of both kinds, with the options of RFC 6458 that the table below
227//! names: each row is a section of the RFC and what gives it here.
228//!
229//! | RFC 6458 | Provided by |
230//! | --- | --- |
231//! | 3.1, 4.1: socket, bind, listen, accept, connect, send, receive, shutdown, close | the socket types; a dropped socket is closed |
232//! | 5.3.4 `SCTP_SNDINFO` | `SendOptions`, `SendInfo` |
233//! | 5.3.5 `SCTP_RCVINFO`, 5.3.6 `SCTP_NXTINFO` | `ReceivedData::rcv_info`, `nxt_info` |
234//! | 6.1.1 `SCTP_ASSOC_CHANGE` | `Notification::AssociationChange` |
235//! | 6.1.2 `SCTP_PEER_ADDR_CHANGE` | `Notification::PeerAddressChange` |
236//! | 6.1.5 `SCTP_SHUTDOWN_EVENT` | `Notification::Shutdown` |
237//! | 6.1.11 `SCTP_SEND_FAILED_EVENT` | `Notification::SendFailure` |
238//! | 6.1: the other notifications | `Notification::Unsupported`, with the number of their type and their octets |
239//! | 6.1.3 `SCTP_REMOTE_ERROR` | `Notification::RemoteError` |
240//! | 6.1.7 `SCTP_PARTIAL_DELIVERY_EVENT` | `Notification::PartialDelivery` |
241//! | 6.1.9 `SCTP_SENDER_DRY_EVENT` | `Notification::SenderDry` |
242//! | 8.1.1 `SCTP_RTOINFO` | `set_rto_info`, `rto_info` |
243//! | 8.1.2 `SCTP_ASSOCINFO` | `set_association_params`, `association_params`: the retransmission limit of an association |
244//! | 8.1.3 `SCTP_INITMSG` | `set_init_params`, `init_params`, `InitParams` |
245//! | 8.1.4 `SO_LINGER` | `set_linger`, off or zero |
246//! | 8.1.5 `SCTP_NODELAY` | `set_nodelay`, `nodelay` |
247//! | 8.1.9 `SCTP_PRIMARY_ADDR` | `set_primary_address` |
248//! | 8.1.19 `SCTP_DELAYED_SACK` | `set_sack_info`, `sack_info` |
249//! | `SO_SNDBUF`, `SO_RCVBUF`, `IP_TOS`, `IPV6_TCLASS` | `set_send_buffer_size`, `set_recv_buffer_size`, `set_dscp` and their getters |
250//! | 8.1.12 `SCTP_PEER_ADDR_PARAMS` | `peer_address_params`, `set_peer_address_params`, `request_heartbeat`: heartbeat and retransmission limit |
251//! | 8.1.28 `SCTP_EVENT` | `subscribe_events`, `unsubscribe_events` |
252//! | 8.1.29 `SCTP_RECVRCVINFO`, 8.1.30 `SCTP_RECVNXTINFO` | `request_rcvinfo`, `request_nxtinfo` |
253//! | 8.1.31 `SCTP_DEFAULT_SNDINFO` | `set_default_sendinfo` |
254//! | 8.2.1 `SCTP_STATUS` | `status` |
255//! | 8.2.2 `SCTP_GET_PEER_ADDR_INFO` | `paths`, `peer_address_info`: the state, window, round trip, timeout and MTU of each path |
256//! | 8.2.6 `SCTP_GET_ASSOC_STATS` | `association_stats`: the retransmitted chunks, gaps, duplicates, packets and SACKs of an association |
257//! | 9.1 `sctp_bindx`, 9.9 `sctp_connectx` | `bindx`, `connectx` |
258//! | 9.2 `sctp_peeloff` | `OneToManyEndpoint::peeloff` |
259//! | 9.3 `sctp_getpaddrs`, 9.5 `sctp_getladdrs` | `peer_addrs`, `local_addrs` |
260//!
261//! # Limits
262//!
263//! | Subject | Limit |
264//! | --- | --- |
265//! | SCTP itself | The protocol runs in the Linux kernel: the crate is not an SCTP implementation. |
266//! | Other systems | Linux only. |
267//! | SCTP over DTLS | The data channels of WebRTC are another stack: not served. |
268//! | Many associations on one socket | On a [`OneToManyEndpoint`], the parts of the messages of several associations come in turns (`SCTP_FRAGMENT_INTERLEAVE` 1) and are assembled apart: a long message of one association holds back none of the others. Each association has one message in assembly at most, of `max_message_size()` at most, until it ends, and all of them together hold `max_assembly()` octets at most, 64 MiB by default: past it the endpoint aborts the association whose message is the longest. The associations share the receive buffer of the socket (Linux's `net.sctp.rcvbuf_policy` 0): one that fills it makes the others wait for room, and their peers send again after a time-out; `net.sctp.rcvbuf_policy` 1, or a [`Listener`], gives each its own. They share its send buffer too (`net.sctp.sndbuf_policy` 0): Linux wakes the socket once for each association when one of them has data acknowledged, and a message to one of 100 associations took 34 µs of CPU on loopback, where one to a single association took 11 (the README's figures, at ea7c778). After `request_rcvinfo(false)` the parts come one message after the other, and a long message holds back the others. |
269//! | The rest of RFC 6458 | Not wrapped: among others authentication, partial reliability and stream reconfiguration. The sockets implement `AsFd` and `AsRawFd` for them. The interfaces that the RFC deprecates are left out on purpose. |
270//!
271//! # Terms
272//!
273//! | Term | Meaning |
274//! | --- | --- |
275//! | SCTP | Stream Control Transmission Protocol (RFC 9260): a transport that carries whole messages, in order within a stream, between two endpoints that may each have several addresses |
276//! | Association | What SCTP has in the place of a TCP connection |
277//! | Stream | One of the numbered sequences of messages of an association. Messages of two streams do not wait for each other |
278//! | PPID | Payload protocol identifier: a number that travels with each message and says what it carries. 60 is NGAP and 18 is S1AP |
279//! | One-to-one socket | A socket with one association, which is used like a TCP socket |
280//! | One-to-many socket | A socket with many associations, which is used like a UDP socket |
281//! | Multihoming | An association between several addresses of each side: when a path fails, another carries it |
282//! | Notification | A message of the kernel about an association, which `recv` returns in the place of data when it is subscribed |
283//!
284//! # Tests
285//!
286//! The tests open SCTP associations over loopback, so the kernel's `sctp`
287//! module must be available. The tests of a connection that times out and
288//! of a peer that is lost need `unshare` and `ip`, and the tests of long
289//! messages need `net.core.wmem_max` and `net.core.rmem_max` large enough;
290//! each skips otherwise.
291//!
292//! `fuzz/` has three `cargo fuzz` targets for what the kernel hands over, which
293//! CI runs for a minute each: a notification of any octets, the control
294//! messages of a `recvmsg`, and the assembly of messages from any sequence of
295//! parts.
296//!
297//! # The OxiRush family
298//!
299//! - [oxirush-utils](https://docs.rs/oxirush-utils): the 3GPP identities, and the JSON tree with its paths that the others share.
300//! - [oxirush-aper](https://docs.rs/oxirush-aper): aligned PER, the runtime of the NGAP and S1AP codecs.
301//! - [oxirush-nas](https://docs.rs/oxirush-nas): the NAS messages of 5GS and EPS.
302//! - [oxirush-security](https://docs.rs/oxirush-security): NAS security, AKA and the derivation of keys.
303//! - [oxirush-ngap](https://docs.rs/oxirush-ngap): NGAP, between a gNB and an AMF.
304//! - [oxirush-s1ap](https://docs.rs/oxirush-s1ap): S1AP, between an eNB and an MME.
305//! - [oxirush-gtp-u](https://docs.rs/oxirush-gtp-u): GTP-U tunnels, with a test UPF.
306//! - [oxirush-sctp](https://docs.rs/oxirush-sctp): SCTP sockets, which carry NGAP and S1AP.
307#![warn(missing_docs)]
308
309#[cfg(not(any(target_os = "linux", target_os = "android")))]
310compile_error!(
311    "oxirush-sctp is for Linux: it calls the SCTP of the Linux kernel through its sockets API"
312);
313
314mod connected_socket;
315pub mod driver;
316mod endpoint;
317mod listener;
318mod options;
319mod socket;
320
321#[doc(inline)]
322pub use socket::Socket;
323
324#[doc(inline)]
325pub use listener::Listener;
326
327#[doc(inline)]
328pub use connected_socket::ConnectedSocket;
329
330#[doc(inline)]
331pub use endpoint::OneToManyEndpoint;
332
333#[doc(inline)]
334pub use options::SocketOptions;
335
336mod internal;
337
338// What `cargo fuzz` drives, which builds the crate with `--cfg fuzzing`: the readers of what
339// the kernel hands over. No other build has this module.
340#[cfg(fuzzing)]
341#[doc(hidden)]
342pub mod fuzzing {
343    pub use crate::internal::fuzzing::*;
344}
345
346mod consts;
347
348mod types;
349
350#[doc(inline)]
351pub use types::{
352    AssocChangeState, AssociationChange, AssociationId, AssociationParams, AssociationStats,
353    BindxFlags, ConnState, ConnStatus, Event, EventSubscriptionError, InitParams, Notification,
354    NotificationOrData, NxtInfo, PartialDelivery, PathState, PeerAddress, PeerAddressChange,
355    PeerAddressParams, PeerAddressState, RcvInfo, ReceivedData, ReceivedInto, RemoteError, RtoInfo,
356    SackInfo, SendData, SendFailure, SendInfo, SendOptions, Shutdown, SocketToAssociation,
357    SubscribeEventAssocId,
358};