Skip to main content

netget/server/ngap/
transport.rs

1//! N2 transport: an SCTP association (TS 38.412 section 7), or a non-standard TCP framing for lab
2//! use on hosts without SCTP. Shared by the AMF (server) and the gNB (client).
3//!
4//! # SCTP, the real one
5//!
6//! The socket is a one-to-one style SCTP socket (`SOCK_STREAM`, `IPPROTO_SCTP`) requested from
7//! the kernel through `socket2`. It is a **probe, not a `cfg`**: on macOS, or a Linux kernel
8//! without the `sctp` module, `socket(2)` fails and the caller gets an error naming SCTP, TS
9//! 38.412 and the lab escape hatch — the `m3ua` precedent. The code below compiles on every unix
10//! and type-checks on the machine that cannot run it.
11//!
12//! An SCTP one-to-one socket presents the TCP socket API, so once it exists it is driven through a
13//! `tokio::net::TcpStream` registered on its descriptor. What TCP's API cannot express is done
14//! with `sendmsg`/`recvmsg` directly:
15//!
16//! - **Every message carries PPID 60** (TS 38.412 section 7), in an `SCTP_SNDRCV` control
17//!   message. Wireshark keys its NGAP dissector on it, and some stacks check it.
18//! - **Non-UE-associated signalling goes on stream 0, UE-associated on stream 1** (TS 38.412
19//!   section 7). The writer decides by decoding the PDU it is asked to send; if the association
20//!   negotiated a single outbound stream, `sendmsg` refuses stream 1 and the message goes on 0.
21//! - **Message boundaries come from `MSG_EOR`**: NGAP has no length field, so a message is exactly
22//!   one SCTP user message. A message longer than [`super::codec::MAX_PDU_LEN`] is drained to its
23//!   end and reported as [`ReadOutcome::TooLarge`]; the association survives because the next
24//!   boundary is still known.
25//!
26//! # `tcp`, the lab one
27//!
28//! **No real gNB or AMF speaks NGAP over TCP.** NGAP has no framing of its own, so the lab
29//! transport prefixes every PDU with a 4-octet big-endian length — a NetGet invention that exists
30//! so the layers above the socket can be exercised on macOS. [`Transport::label`] says so
31//! wherever the transport is named. A declared length over the bound cannot be skipped safely on
32//! a byte stream, so it ends the connection.
33
34use anyhow::{anyhow, Result};
35use std::net::SocketAddr;
36use std::pin::Pin;
37use std::sync::Arc;
38use std::task::{Context, Poll};
39use std::time::Duration;
40use tokio::io::AsyncWrite;
41use tokio::net::{TcpListener, TcpStream};
42
43use super::codec;
44
45/// IANA protocol number for SCTP; named here because `libc` lacks it where the stack is absent.
46pub const IPPROTO_SCTP: i32 = 132;
47/// Octets of the lab transport's length prefix.
48pub const LAB_HEADER_LEN: usize = 4;
49
50/// Which transport an N2 association runs on.
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum Transport {
53    /// TS 38.412 section 7: SCTP, PPID 60.
54    Sctp,
55    /// **Non-standard**: a 4-octet length prefix over TCP, for hosts with no SCTP stack.
56    Tcp,
57}
58
59impl Transport {
60    pub fn parse(value: &str) -> Result<Self> {
61        match value.trim().to_ascii_lowercase().as_str() {
62            "sctp" => Ok(Self::Sctp),
63            "tcp" => Ok(Self::Tcp),
64            other => Err(anyhow!(
65                "transport must be \"sctp\" (what TS 38.412 defines for N2) or \"tcp\" \
66                 (non-standard length-prefixed framing, for local protocol work only), got {other:?}"
67            )),
68        }
69    }
70
71    pub fn from_params(params: Option<&crate::protocol::StartupParams>) -> Result<Self> {
72        match params
73            .map(|p| p.get_optional_string("transport"))
74            .transpose()?
75            .flatten()
76        {
77            Some(s) => Self::parse(&s),
78            None => Ok(Self::Sctp),
79        }
80    }
81
82    pub fn as_str(self) -> &'static str {
83        match self {
84            Self::Sctp => "sctp",
85            Self::Tcp => "tcp",
86        }
87    }
88
89    /// How the transport is named wherever an operator or the model can see it. The TCP wording
90    /// carries its own warning because this string travels into logs, the dashboard and events.
91    pub fn label(self) -> &'static str {
92        match self {
93            Self::Sctp => "sctp (TS 38.412, PPID 60)",
94            Self::Tcp => "tcp (NON-STANDARD length-prefixed lab transport, not N2)",
95        }
96    }
97}
98
99fn no_sctp(role: &str, e: std::io::Error) -> anyhow::Error {
100    anyhow!(
101        "NGAP {role} cannot open an SCTP socket on this host: {e}. NGAP's transport is SCTP \
102         (TS 38.412 section 7, port 38412, PPID 60) and this operating system has no SCTP stack \
103         available to this process — macOS ships neither kernel support nor headers for SCTP, and \
104         a Linux kernel without the sctp module fails the same way (`modprobe sctp`). NetGet \
105         refuses rather than pretend it is speaking N2. Either run on a host with SCTP, or, for \
106         local protocol work only, pass the startup parameter transport=\"tcp\" — a NON-STANDARD \
107         length-prefixed framing that no real gNB or AMF speaks."
108    )
109}
110
111fn sctp_socket(addr: &SocketAddr, role: &str) -> Result<socket2::Socket> {
112    use socket2::{Domain, Protocol, Socket, Type};
113    let domain = if addr.is_ipv4() {
114        Domain::IPV4
115    } else {
116        Domain::IPV6
117    };
118    Socket::new(domain, Type::STREAM, Some(Protocol::from(IPPROTO_SCTP)))
119        .map_err(|e| no_sctp(role, e))
120}
121
122/// Bind the AMF's listening socket.
123///
124/// Public because the SCTP refusal is a feature with its own test: on a host without SCTP this
125/// must fail with a message naming SCTP rather than an errno.
126pub async fn bind_listener(addr: SocketAddr, transport: Transport) -> Result<TcpListener> {
127    match transport {
128        Transport::Tcp => crate::server::socket_helpers::create_reusable_tcp_listener(addr).await,
129        Transport::Sctp => {
130            let socket = sctp_socket(&addr, "AMF")?;
131            socket
132                .set_reuse_address(true)
133                .map_err(|e| anyhow!("NGAP could not set SO_REUSEADDR on the SCTP socket: {e}"))?;
134            socket
135                .bind(&addr.into())
136                .map_err(|e| anyhow!("NGAP could not bind the SCTP socket to {addr}: {e}"))?;
137            socket
138                .listen(128)
139                .map_err(|e| anyhow!("NGAP could not listen on the SCTP socket at {addr}: {e}"))?;
140            socket
141                .set_nonblocking(true)
142                .map_err(|e| anyhow!("NGAP could not make the SCTP socket non-blocking: {e}"))?;
143            let std_listener: std::net::TcpListener = socket.into();
144            TcpListener::from_std(std_listener)
145                .map_err(|e| anyhow!("NGAP could not register the SCTP listener with tokio: {e}"))
146        }
147    }
148}
149
150/// Open an association to an AMF (the gNB side).
151pub async fn connect(addr: SocketAddr, transport: Transport, deadline: Duration) -> Result<Conn> {
152    let stream = match transport {
153        Transport::Tcp => tokio::time::timeout(deadline, TcpStream::connect(addr))
154            .await
155            .map_err(|_| anyhow!("no answer from {addr} within {}s", deadline.as_secs()))??,
156        Transport::Sctp => {
157            let socket = sctp_socket(&addr, "gNB")?;
158            let std_stream = tokio::task::spawn_blocking(move || -> Result<std::net::TcpStream> {
159                socket
160                    .connect_timeout(&addr.into(), deadline)
161                    .map_err(|e| anyhow!("SCTP association to {addr} failed: {e}"))?;
162                socket.set_nonblocking(true)?;
163                Ok(socket.into())
164            })
165            .await
166            .map_err(|e| anyhow!("SCTP connect task failed: {e}"))??;
167            TcpStream::from_std(std_stream)?
168        }
169    };
170    Ok(Conn::new(stream, transport))
171}
172
173/// One established association.
174pub struct Conn {
175    stream: Arc<TcpStream>,
176    transport: Transport,
177}
178
179impl Conn {
180    pub fn new(stream: TcpStream, transport: Transport) -> Self {
181        Conn {
182            stream: Arc::new(stream),
183            transport,
184        }
185    }
186
187    pub fn local_addr(&self) -> std::io::Result<SocketAddr> {
188        self.stream.local_addr()
189    }
190
191    /// The underlying socket, for tests that must put octets on it NetGet's writer would refuse.
192    #[doc(hidden)]
193    pub fn socket(&self) -> socket2::SockRef<'_> {
194        socket2::SockRef::from(&*self.stream)
195    }
196
197    pub fn reader(&self) -> MessageReader {
198        MessageReader {
199            stream: self.stream.clone(),
200            transport: self.transport,
201        }
202    }
203
204    pub fn writer(&self) -> MessageWriter {
205        MessageWriter {
206            stream: self.stream.clone(),
207            transport: self.transport,
208            pending: None,
209        }
210    }
211}
212
213/// The result of waiting for one message.
214#[derive(Debug)]
215pub enum ReadOutcome {
216    Message(Vec<u8>),
217    /// A message over [`codec::MAX_PDU_LEN`], of this many octets (at least). Over SCTP it was
218    /// drained and the association can continue; over the lab transport the framing is lost.
219    TooLarge(usize),
220    Eof,
221    TimedOut,
222    Error(String),
223}
224
225/// Reads whole NGAP messages.
226pub struct MessageReader {
227    stream: Arc<TcpStream>,
228    transport: Transport,
229}
230
231impl MessageReader {
232    /// Wait up to `deadline` for the next message to *start* arriving; a message that has begun
233    /// is then given the same deadline to finish.
234    pub async fn next(&mut self, deadline: Duration) -> ReadOutcome {
235        match tokio::time::timeout(deadline, self.next_inner(deadline)).await {
236            Ok(o) => o,
237            Err(_) => ReadOutcome::TimedOut,
238        }
239    }
240
241    async fn next_inner(&mut self, _deadline: Duration) -> ReadOutcome {
242        match self.transport {
243            Transport::Tcp => {
244                let mut header = [0u8; LAB_HEADER_LEN];
245                match read_exact(&self.stream, &mut header).await {
246                    Ok(true) => {}
247                    Ok(false) => return ReadOutcome::Eof,
248                    Err(e) => return ReadOutcome::Error(e.to_string()),
249                }
250                let len = u32::from_be_bytes(header) as usize;
251                if len > codec::MAX_PDU_LEN {
252                    return ReadOutcome::TooLarge(len);
253                }
254                let mut body = vec![0u8; len];
255                match read_exact(&self.stream, &mut body).await {
256                    Ok(true) => ReadOutcome::Message(body),
257                    Ok(false) => ReadOutcome::Eof,
258                    Err(e) => ReadOutcome::Error(e.to_string()),
259                }
260            }
261            Transport::Sctp => sctp_recv(&self.stream).await,
262        }
263    }
264}
265
266/// Fill `buf` from the stream. `Ok(false)` on a clean EOF before any octet of it arrived.
267async fn read_exact(stream: &TcpStream, buf: &mut [u8]) -> std::io::Result<bool> {
268    let mut got = 0;
269    while got < buf.len() {
270        stream.readable().await?;
271        match stream.try_read(&mut buf[got..]) {
272            Ok(0) if got == 0 => return Ok(false),
273            Ok(0) => {
274                return Err(std::io::Error::new(
275                    std::io::ErrorKind::UnexpectedEof,
276                    "truncated frame",
277                ))
278            }
279            Ok(n) => got += n,
280            Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
281            Err(e) => return Err(e),
282        }
283    }
284    Ok(true)
285}
286
287#[cfg(unix)]
288async fn sctp_recv(stream: &TcpStream) -> ReadOutcome {
289    use std::os::fd::AsRawFd;
290    let fd = stream.as_raw_fd();
291    let mut message: Vec<u8> = Vec::new();
292    let mut chunk = vec![0u8; codec::MAX_PDU_LEN + 1];
293    let mut oversized = false;
294    loop {
295        if let Err(e) = stream.readable().await {
296            return ReadOutcome::Error(e.to_string());
297        }
298        let r = stream.try_io(tokio::io::Interest::READABLE, || {
299            let mut iov = libc::iovec {
300                iov_base: chunk.as_mut_ptr() as *mut libc::c_void,
301                iov_len: chunk.len(),
302            };
303            // SAFETY: msghdr is plain data; every pointer in it refers to `iov`/`chunk`, which
304            // outlive the call.
305            let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
306            msg.msg_iov = &mut iov;
307            msg.msg_iovlen = 1;
308            // SAFETY: fd is the live socket owned by `stream`; msg is initialised above.
309            let n = unsafe { libc::recvmsg(fd, &mut msg, 0) };
310            if n < 0 {
311                Err(std::io::Error::last_os_error())
312            } else {
313                Ok((n as usize, msg.msg_flags))
314            }
315        });
316        let (n, flags) = match r {
317            Ok(v) => v,
318            Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
319            Err(e) => return ReadOutcome::Error(e.to_string()),
320        };
321        if n == 0 {
322            return ReadOutcome::Eof;
323        }
324        // MSG_NOTIFICATION (0x8000 on Linux): none are subscribed to, but never mistake one
325        // for user data.
326        if flags & 0x8000 != 0 {
327            continue;
328        }
329        if !oversized {
330            if message.len() + n > codec::MAX_PDU_LEN {
331                oversized = true;
332            } else {
333                message.extend_from_slice(&chunk[..n]);
334            }
335        }
336        let total = message.len() + if oversized { n } else { 0 };
337        if flags & libc::MSG_EOR != 0 {
338            return if oversized {
339                ReadOutcome::TooLarge(total.max(codec::MAX_PDU_LEN + 1))
340            } else {
341                ReadOutcome::Message(message)
342            };
343        }
344    }
345}
346
347#[cfg(not(unix))]
348async fn sctp_recv(_stream: &TcpStream) -> ReadOutcome {
349    ReadOutcome::Error("SCTP is not available on this platform".into())
350}
351
352/// Writes whole NGAP messages: each `write` is one message.
353///
354/// Implements `AsyncWrite` so the dashboard's generic peer command task can use it, under the
355/// contract that one `write_all` call is one PDU (which is how both that task and NetGet's own
356/// sessions use it).
357pub struct MessageWriter {
358    stream: Arc<TcpStream>,
359    transport: Transport,
360    /// Lab transport: the framed message still being written, and how much of it is out.
361    pending: Option<(Vec<u8>, usize)>,
362}
363
364impl MessageWriter {
365    /// Send one PDU.
366    pub async fn send(&mut self, pdu: &[u8]) -> std::io::Result<()> {
367        use tokio::io::AsyncWriteExt;
368        self.write_all(pdu).await?;
369        self.flush().await
370    }
371
372    /// Half-close: an SCTP SHUTDOWN, or a TCP FIN.
373    pub fn close(&self) {
374        let _ = socket2::SockRef::from(&*self.stream).shutdown(std::net::Shutdown::Write);
375    }
376}
377
378#[cfg(unix)]
379fn sctp_send(stream: &TcpStream, pdu: &[u8], sid: u16) -> std::io::Result<usize> {
380    use std::os::fd::AsRawFd;
381    // struct sctp_sndrcvinfo (linux/sctp.h): stream, ssn, flags, (pad), ppid, context,
382    // timetolive, tsn, cumtsn, assoc_id — 32 octets.
383    #[repr(C)]
384    struct SndRcvInfo {
385        stream: u16,
386        ssn: u16,
387        flags: u16,
388        _pad: u16,
389        ppid: u32,
390        context: u32,
391        timetolive: u32,
392        tsn: u32,
393        cumtsn: u32,
394        assoc_id: i32,
395    }
396    const SCTP_SNDRCV: i32 = 1;
397    let info = SndRcvInfo {
398        stream: sid,
399        ssn: 0,
400        flags: 0,
401        _pad: 0,
402        // The PPID is carried in network byte order (RFC 6458 section 5.3.2).
403        ppid: codec::PPID.to_be(),
404        context: 0,
405        timetolive: 0,
406        tsn: 0,
407        cumtsn: 0,
408        assoc_id: 0,
409    };
410    // SAFETY: CMSG_SPACE is a pure size computation.
411    let space = unsafe { libc::CMSG_SPACE(std::mem::size_of::<SndRcvInfo>() as u32) } as usize;
412    let mut control = vec![0u8; space];
413    let mut iov = libc::iovec {
414        iov_base: pdu.as_ptr() as *mut libc::c_void,
415        iov_len: pdu.len(),
416    };
417    // SAFETY: msghdr is plain data; its pointers refer to `iov` and `control`, which outlive the
418    // sendmsg call, and CMSG_FIRSTHDR/CMSG_DATA stay within `control` because it is CMSG_SPACE
419    // bytes long.
420    unsafe {
421        let mut msg: libc::msghdr = std::mem::zeroed();
422        msg.msg_iov = &mut iov;
423        msg.msg_iovlen = 1;
424        msg.msg_control = control.as_mut_ptr() as *mut libc::c_void;
425        msg.msg_controllen = space as _;
426        let cmsg = libc::CMSG_FIRSTHDR(&msg);
427        (*cmsg).cmsg_level = IPPROTO_SCTP;
428        (*cmsg).cmsg_type = SCTP_SNDRCV;
429        (*cmsg).cmsg_len = libc::CMSG_LEN(std::mem::size_of::<SndRcvInfo>() as u32) as _;
430        std::ptr::copy_nonoverlapping(
431            &info as *const SndRcvInfo as *const u8,
432            libc::CMSG_DATA(cmsg),
433            std::mem::size_of::<SndRcvInfo>(),
434        );
435        let n = libc::sendmsg(stream.as_raw_fd(), &msg, 0);
436        if n < 0 {
437            Err(std::io::Error::last_os_error())
438        } else {
439            Ok(n as usize)
440        }
441    }
442}
443
444#[cfg(not(unix))]
445fn sctp_send(_stream: &TcpStream, _pdu: &[u8], _sid: u16) -> std::io::Result<usize> {
446    Err(std::io::Error::other(
447        "SCTP is not available on this platform",
448    ))
449}
450
451impl AsyncWrite for MessageWriter {
452    fn poll_write(
453        mut self: Pin<&mut Self>,
454        cx: &mut Context<'_>,
455        buf: &[u8],
456    ) -> Poll<std::io::Result<usize>> {
457        if buf.len() > codec::MAX_PDU_LEN {
458            return Poll::Ready(Err(std::io::Error::new(
459                std::io::ErrorKind::InvalidInput,
460                format!(
461                    "NGAP PDU of {} octets exceeds {}",
462                    buf.len(),
463                    codec::MAX_PDU_LEN
464                ),
465            )));
466        }
467        match self.transport {
468            Transport::Sctp => {
469                // TS 38.412 section 7: UE-associated signalling off stream 0.
470                let preferred: u16 = if codec::is_ue_associated(buf) { 1 } else { 0 };
471                loop {
472                    if let Err(e) = std::task::ready!(self.stream.poll_write_ready(cx)) {
473                        return Poll::Ready(Err(e));
474                    }
475                    let stream = self.stream.clone();
476                    let mut result = stream.try_io(tokio::io::Interest::WRITABLE, || {
477                        sctp_send(&stream, buf, preferred)
478                    });
479                    if preferred != 0
480                        && matches!(&result, Err(e) if e.raw_os_error() == Some(libc::EINVAL))
481                    {
482                        // The association negotiated a single outbound stream.
483                        result = stream
484                            .try_io(tokio::io::Interest::WRITABLE, || sctp_send(&stream, buf, 0));
485                    }
486                    match result {
487                        Ok(_) => return Poll::Ready(Ok(buf.len())),
488                        Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
489                        Err(e) => return Poll::Ready(Err(e)),
490                    }
491                }
492            }
493            Transport::Tcp => {
494                if self.pending.is_none() {
495                    let mut framed = Vec::with_capacity(LAB_HEADER_LEN + buf.len());
496                    framed.extend_from_slice(&(buf.len() as u32).to_be_bytes());
497                    framed.extend_from_slice(buf);
498                    self.pending = Some((framed, 0));
499                }
500                loop {
501                    let (data, done) = self.pending.as_ref().expect("set above");
502                    if *done == data.len() {
503                        self.pending = None;
504                        return Poll::Ready(Ok(buf.len()));
505                    }
506                    if let Err(e) = std::task::ready!(self.stream.poll_write_ready(cx)) {
507                        self.pending = None;
508                        return Poll::Ready(Err(e));
509                    }
510                    let remaining = data[*done..].to_vec();
511                    match self.stream.try_write(&remaining) {
512                        Ok(n) => {
513                            if let Some((_, d)) = self.pending.as_mut() {
514                                *d += n;
515                            }
516                        }
517                        Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
518                        Err(e) => {
519                            self.pending = None;
520                            return Poll::Ready(Err(e));
521                        }
522                    }
523                }
524            }
525        }
526    }
527
528    fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
529        Poll::Ready(Ok(()))
530    }
531
532    fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
533        self.close();
534        Poll::Ready(Ok(()))
535    }
536}