Skip to main content

ove/
net.rs

1// Copyright (C) 2026 Kamil Lulko <kamil.lulko@gmail.com>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4//
5// This file is part of oveRTOS.
6
7//! Blocking BSD-style networking primitives for oveRTOS.
8//!
9//! Provides safe wrappers for sockets (TCP and UDP), network interface
10//! management, DNS resolution, and socket addresses. All types use
11//! inline storage and implement `Send + Sync`.
12//!
13//! Each backend uses its native TCP/IP stack:
14//!
15//! | Backend  | Stack         | Notes |
16//! |----------|---------------|-------|
17//! | FreeRTOS | lwIP (vendored) | Bare-metal target; sockets in oveRTOS heap |
18//! | Zephyr   | Zephyr `net`  | Uses Zephyr's POSIX `sockets` shim |
19//! | NuttX    | NuttX net     | Native socket layer |
20//! | POSIX    | host kernel   | Native `socket(2)` via libc |
21//!
22//! ## When to pick `ove::net` (blocking) vs [`crate::async_net`]
23//!
24//! Both stacks coexist as separate Cargo features ([`crate::net`] is
25//! always available; [`crate::async_net`] requires `async-net` +
26//! `CONFIG_OVE_ASYNC_NET=y`), but at build time they are **mutually
27//! exclusive at the transport layer**: lwIP (or Zephyr/NuttX net) and
28//! embassy-net both want to own the MAC + ARP cache + descriptor ring.
29//! Pick **one** stack per build.
30//!
31//! Choose `ove::net` (this module) when:
32//!
33//! - You're writing **synchronous code** in a dedicated networking
34//!   thread (the common bare-metal pattern). One thread, one socket,
35//!   one blocking `recv` per loop iteration.
36//! - You need **TLS + HTTP + MQTT + SNTP + HTTPD** all in one app and
37//!   want production-tested implementations — the [`crate::net_tls`] /
38//!   [`crate::net_http`] / [`crate::net_mqtt`] / [`crate::net_sntp`] /
39//!   [`crate::net_httpd`] sister modules wrap mbedTLS + battle-tested
40//!   C clients that are part of oveRTOS itself.
41//! - You're on a backend where the **native socket layer is mature**
42//!   (Zephyr/NuttX/POSIX). The HAL is doing the heavy lifting; the
43//!   Rust wrapper is just type-safe glue.
44//! - You're **interoperating with existing C code** that already
45//!   opens sockets, calls DNS, etc.
46//!
47//! Choose [`crate::async_net`] (Embassy + embassy-net) when:
48//!
49//! - You're writing **multiple concurrent tasks** that all do network
50//!   I/O — TCP listener + MQTT client + sensor uplink — and don't
51//!   want one thread per task.
52//! - You want **embedded-hal-async / embedded-io-async** trait impls
53//!   so async sensor drivers from crates.io work directly on top of
54//!   your sockets.
55//! - You're on **bare-metal FreeRTOS** (the lwIP path is fine, but
56//!   embassy-net is smaller and gives you the Rust-native API).
57//! - You're using async elsewhere ([`crate::timer::Timer`] →
58//!   `Timer::after_millis().await`, `embedded_io_async::Write`) and
59//!   want a single async story end-to-end.
60//!
61//! ## Protocol layers
62//!
63//! oveRTOS ships C clients for the common app-layer protocols, with
64//! Rust wrappers that hand back RAII-cleaning handles:
65//!
66//! - [`crate::net_tls`] — mbedTLS sessions on top of [`TcpStream`]
67//! - [`crate::net_http`] — HTTP/1.1 client with `get`/`post`/`put`,
68//!   automatic redirect handling, gzip decode
69//! - [`crate::net_mqtt`] — MQTT 3.1.1 client (QoS 0/1, keep-alive,
70//!   optional TLS)
71//! - [`crate::net_sntp`] — SNTP time sync (single-shot or periodic)
72//! - [`crate::net_httpd`] — embedded HTTP server with REST routes +
73//!   WebSocket upgrade
74//!
75//! For the async equivalents, see [`crate::async_net`]'s module docs —
76//! it documents which crates.io community crates pair with embassy-net
77//! ([`rust-mqtt`], [`reqwless`], [`embedded-tls`], `sntpc`, `picoserve`).
78
79use core::fmt;
80
81use crate::bindings;
82use crate::error::{Error, Result};
83
84// SAFETY (module-wide contract for the `unsafe { bindings::ove_*(...) }` FFI
85// calls below): any handle passed to the C API is non-null and refers to a
86// live RTOS object — wrapper constructors establish validity via
87// `Error::from_code`, and `Drop` (or an explicit `deinit`) is the only place
88// a handle is released. Pointer and slice arguments reference caller-owned
89// memory valid for the duration of the call; the C side copies whatever it
90// retains and does not alias them past return (verified against the
91// signatures in `include/ove/*.h`). Blocks that deviate — `transmute`, raw
92// pointer casts from user data, slice reconstruction via `from_raw_parts`,
93// or storing a callback across the FFI boundary — carry their own
94// `// SAFETY:` comment.
95
96// ---------------------------------------------------------------------------
97// Address
98// ---------------------------------------------------------------------------
99
100/// Generic socket address (IPv4 or IPv6).
101///
102/// Wraps [`ove_sockaddr_t`](bindings::ove_sockaddr_t) transparently so it can
103/// be passed directly to the C API without conversion.
104#[derive(Clone, Copy)]
105#[repr(transparent)]
106pub struct Address(bindings::ove_sockaddr_t);
107
108impl Address {
109    /// Create an IPv4 address from four octets and a port number.
110    pub fn ipv4(a: u8, b: u8, c: u8, d: u8, port: u16) -> Self {
111        let mut inner: bindings::ove_sockaddr_t = unsafe { core::mem::zeroed() };
112        unsafe { bindings::ove_sockaddr_ipv4(&mut inner, a, b, c, d, port) };
113        Self(inner)
114    }
115
116    /// Get the port number in host byte order.
117    pub fn port(&self) -> u16 {
118        self.0.port
119    }
120
121    /// Set the port number in host byte order.
122    pub fn set_port(&mut self, port: u16) {
123        self.0.port = port;
124    }
125
126    /// Get the first four bytes of the address (the IPv4 octets).
127    pub fn octets(&self) -> [u8; 4] {
128        [
129            self.0.addr[0],
130            self.0.addr[1],
131            self.0.addr[2],
132            self.0.addr[3],
133        ]
134    }
135
136    /// Get a const pointer to the inner sockaddr for passing to C APIs.
137    pub(crate) fn as_ptr(&self) -> *const bindings::ove_sockaddr_t {
138        &self.0
139    }
140
141    /// Get a mutable pointer to the inner sockaddr for receiving from C APIs.
142    pub(crate) fn as_mut_ptr(&mut self) -> *mut bindings::ove_sockaddr_t {
143        &mut self.0
144    }
145
146    /// Borrow the inner C struct.
147    fn as_inner(&self) -> &bindings::ove_sockaddr_t {
148        &self.0
149    }
150}
151
152impl Default for Address {
153    fn default() -> Self {
154        Self(unsafe { core::mem::zeroed() })
155    }
156}
157
158impl fmt::Debug for Address {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        let o = self.octets();
161        f.debug_struct("Address")
162            .field("ip", &format_args!("{}.{}.{}.{}", o[0], o[1], o[2], o[3]))
163            .field("port", &self.0.port)
164            .finish()
165    }
166}
167
168// ---------------------------------------------------------------------------
169// NetIfConfig
170// ---------------------------------------------------------------------------
171
172/// Builder for network interface configuration.
173///
174/// Wraps [`ove_netif_config_t`](bindings::ove_netif_config_t) and provides a
175/// fluent API for configuring DHCP, static IP, and DNS.
176pub struct NetIfConfig(bindings::ove_netif_config_t);
177
178impl NetIfConfig {
179    /// Create a zeroed configuration (no DHCP, no static IP, no DNS).
180    pub fn new() -> Self {
181        Self(unsafe { core::mem::zeroed() })
182    }
183
184    /// Configure a static IP, subnet mask, and gateway.
185    ///
186    /// Disables DHCP.
187    pub fn static_ip(mut self, ip: Address, mask: Address, gw: Address) -> Self {
188        self.0.use_dhcp = 0;
189        self.0.static_ip = *ip.as_inner();
190        self.0.netmask = *mask.as_inner();
191        self.0.gateway = *gw.as_inner();
192        self
193    }
194
195    /// Enable DHCP for automatic address assignment.
196    pub fn dhcp(mut self) -> Self {
197        self.0.use_dhcp = 1;
198        self
199    }
200
201    /// Set the DNS server address.
202    pub fn dns(mut self, dns: Address) -> Self {
203        self.0.dns = *dns.as_inner();
204        self
205    }
206}
207
208impl Default for NetIfConfig {
209    fn default() -> Self {
210        Self::new()
211    }
212}
213
214// ---------------------------------------------------------------------------
215// NetIf
216// ---------------------------------------------------------------------------
217
218/// RAII wrapper around a network interface.
219///
220/// Uses inline storage so no heap/zero-heap split is needed.
221/// The handle is derived from `&self.storage` at each use to avoid
222/// dangling self-referential pointers after Rust moves the struct.
223pub struct NetIf {
224    storage: bindings::ove_netif_storage_t,
225    init: bool,
226}
227
228impl NetIf {
229    /// Derive the C handle from inline storage.
230    fn handle(&self) -> bindings::ove_netif_t {
231        &self.storage as *const _ as bindings::ove_netif_t
232    }
233
234    /// Initialise a network interface using inline storage.
235    ///
236    /// # Errors
237    /// Returns an error if the underlying RTOS rejects the initialisation.
238    pub fn new() -> Result<Self> {
239        let mut netif = Self {
240            storage: unsafe { core::mem::zeroed() },
241            init: false,
242        };
243        let mut handle: bindings::ove_netif_t = core::ptr::null_mut();
244        let rc = unsafe { bindings::ove_netif_init(&mut handle, &mut netif.storage) };
245        Error::from_code(rc)?;
246        netif.init = true;
247        Ok(netif)
248    }
249
250    /// Bring the interface up with the given configuration.
251    ///
252    /// # Errors
253    /// Returns an error if the RTOS cannot bring the interface up (e.g.
254    /// invalid configuration or hardware failure).
255    pub fn up(&self, cfg: &NetIfConfig) -> Result<()> {
256        let rc = unsafe { bindings::ove_netif_up(self.handle(), &cfg.0) };
257        Error::from_code(rc)
258    }
259
260    /// Tear down the network interface.
261    pub fn down(&self) {
262        unsafe { bindings::ove_netif_down(self.handle()) }
263    }
264
265    /// Query current addresses of the network interface.
266    ///
267    /// Returns `(ip, gateway, netmask)` on success.
268    ///
269    /// # Errors
270    /// Returns an error if the RTOS cannot retrieve the addresses.
271    pub fn get_addr(&self) -> Result<(Address, Address, Address)> {
272        let mut ip = core::mem::MaybeUninit::<bindings::ove_sockaddr_t>::zeroed();
273        let mut gw = core::mem::MaybeUninit::<bindings::ove_sockaddr_t>::zeroed();
274        let mut nm = core::mem::MaybeUninit::<bindings::ove_sockaddr_t>::zeroed();
275        let rc = unsafe {
276            bindings::ove_netif_get_addr(
277                self.handle(),
278                ip.as_mut_ptr(),
279                gw.as_mut_ptr(),
280                nm.as_mut_ptr(),
281            )
282        };
283        Error::from_code(rc)?;
284        unsafe {
285            Ok((
286                Address(ip.assume_init()),
287                Address(gw.assume_init()),
288                Address(nm.assume_init()),
289            ))
290        }
291    }
292}
293
294impl fmt::Debug for NetIf {
295    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296        f.debug_struct("NetIf")
297            .field("handle", &format_args!("{:p}", self.handle()))
298            .finish()
299    }
300}
301
302impl Drop for NetIf {
303    fn drop(&mut self) {
304        if !self.init {
305            return;
306        }
307        unsafe { bindings::ove_netif_deinit(self.handle()) }
308    }
309}
310
311// SAFETY: Wraps a ove handle backed by inline storage. The RTOS net calls are
312// thread-safe. Create/destroy are single-threaded (lifecycle guarantee).
313unsafe impl Send for NetIf {}
314unsafe impl Sync for NetIf {}
315
316// ---------------------------------------------------------------------------
317// TcpStream
318// ---------------------------------------------------------------------------
319
320/// RAII wrapper around a TCP (stream) socket.
321///
322/// Uses inline storage for the socket backend, so no heap/zero-heap split is
323/// needed.  The handle is derived from `&self.storage` at each call to
324/// avoid dangling self-referential pointers after Rust moves the struct.
325pub struct TcpStream {
326    storage: bindings::ove_socket_storage_t,
327    open: bool,
328}
329
330impl TcpStream {
331    /// Derive the C handle from inline storage.
332    pub(crate) fn handle(&self) -> bindings::ove_socket_t {
333        &self.storage as *const _ as bindings::ove_socket_t
334    }
335
336    /// Open a new TCP socket.
337    ///
338    /// # Errors
339    /// Returns an error if the RTOS cannot open the socket.
340    pub fn new() -> Result<Self> {
341        let mut sock = Self {
342            storage: unsafe { core::mem::zeroed() },
343            open: false,
344        };
345        let mut handle: bindings::ove_socket_t = core::ptr::null_mut();
346        let rc = unsafe {
347            bindings::ove_socket_open(
348                &mut handle,
349                &mut sock.storage,
350                bindings::OVE_AF_INET,
351                bindings::OVE_SOCK_STREAM,
352            )
353        };
354        Error::from_code(rc)?;
355        sock.open = true;
356        Ok(sock)
357    }
358
359    /// Connect to a remote address with a timeout.
360    ///
361    /// # Errors
362    /// Returns an error if the connection fails or times out.
363    pub fn connect(&self, addr: &Address, timeout: core::time::Duration) -> Result<()> {
364        let rc = unsafe {
365            bindings::ove_socket_connect(
366                self.handle(),
367                addr.as_ptr(),
368                crate::time::dur_to_ns(timeout),
369            )
370        };
371        Error::from_code(rc)
372    }
373
374    /// Send data on the connected socket.
375    ///
376    /// Returns the number of bytes actually sent.
377    ///
378    /// # Errors
379    /// Returns an error if the send fails.
380    pub fn send(&self, data: &[u8]) -> Result<usize> {
381        let mut sent: usize = 0;
382        let rc = unsafe {
383            bindings::ove_socket_send(
384                self.handle(),
385                data.as_ptr() as *const _,
386                data.len(),
387                &mut sent,
388            )
389        };
390        Error::from_code(rc)?;
391        Ok(sent)
392    }
393
394    /// Receive data from the connected socket.
395    ///
396    /// Returns the number of bytes received.
397    ///
398    /// # Errors
399    /// Returns an error if the receive fails or times out.
400    pub fn recv(&self, buf: &mut [u8], timeout: core::time::Duration) -> Result<usize> {
401        let mut received: usize = 0;
402        let rc = unsafe {
403            bindings::ove_socket_recv(
404                self.handle(),
405                buf.as_mut_ptr() as *mut _,
406                buf.len(),
407                &mut received,
408                crate::time::dur_to_ns(timeout),
409            )
410        };
411        Error::from_code(rc)?;
412        Ok(received)
413    }
414}
415
416impl fmt::Debug for TcpStream {
417    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418        f.debug_struct("TcpStream")
419            .field("handle", &format_args!("{:p}", self.handle()))
420            .field("open", &self.open)
421            .finish_non_exhaustive()
422    }
423}
424
425impl Drop for TcpStream {
426    fn drop(&mut self) {
427        if self.open {
428            unsafe { bindings::ove_socket_close(self.handle()) }
429        }
430    }
431}
432
433// SAFETY: Wraps a ove handle backed by inline storage. The RTOS socket calls
434// are thread-safe. Open/close are single-threaded (lifecycle guarantee).
435unsafe impl Send for TcpStream {}
436unsafe impl Sync for TcpStream {}
437
438// ---------------------------------------------------------------------------
439// UdpSocket
440// ---------------------------------------------------------------------------
441
442/// RAII wrapper around a UDP (datagram) socket.
443///
444/// Uses inline storage for the socket backend, so no heap/zero-heap split is
445/// needed.  The handle is derived from `&self.storage` at each call to
446/// avoid dangling self-referential pointers after Rust moves the struct.
447pub struct UdpSocket {
448    storage: bindings::ove_socket_storage_t,
449    open: bool,
450}
451
452impl UdpSocket {
453    /// Derive the C handle from inline storage.
454    fn handle(&self) -> bindings::ove_socket_t {
455        &self.storage as *const _ as bindings::ove_socket_t
456    }
457
458    /// Open a new UDP socket.
459    ///
460    /// # Errors
461    /// Returns an error if the RTOS cannot open the socket.
462    pub fn new() -> Result<Self> {
463        let mut sock = Self {
464            storage: unsafe { core::mem::zeroed() },
465            open: false,
466        };
467        let mut handle: bindings::ove_socket_t = core::ptr::null_mut();
468        let rc = unsafe {
469            bindings::ove_socket_open(
470                &mut handle,
471                &mut sock.storage,
472                bindings::OVE_AF_INET,
473                bindings::OVE_SOCK_DGRAM,
474            )
475        };
476        Error::from_code(rc)?;
477        sock.open = true;
478        Ok(sock)
479    }
480
481    /// Bind the socket to a local address.
482    ///
483    /// # Errors
484    /// Returns an error if the bind fails (e.g. address already in use).
485    pub fn bind(&self, addr: &Address) -> Result<()> {
486        let rc = unsafe { bindings::ove_socket_bind(self.handle(), addr.as_ptr()) };
487        Error::from_code(rc)
488    }
489
490    /// Send a datagram to a specific destination.
491    ///
492    /// Returns the number of bytes actually sent.
493    ///
494    /// # Errors
495    /// Returns an error if the send fails.
496    pub fn send_to(&self, data: &[u8], dest: &Address) -> Result<usize> {
497        let mut sent: usize = 0;
498        let rc = unsafe {
499            bindings::ove_socket_sendto(
500                self.handle(),
501                data.as_ptr() as *const _,
502                data.len(),
503                &mut sent,
504                dest.as_ptr(),
505            )
506        };
507        Error::from_code(rc)?;
508        Ok(sent)
509    }
510
511    /// Receive a datagram and the sender's address.
512    ///
513    /// Returns the number of bytes received and the source address.
514    ///
515    /// # Errors
516    /// Returns an error if the receive fails or times out.
517    pub fn recv_from(
518        &self,
519        buf: &mut [u8],
520        timeout: core::time::Duration,
521    ) -> Result<(usize, Address)> {
522        let mut received: usize = 0;
523        let mut src = Address::default();
524        let rc = unsafe {
525            bindings::ove_socket_recvfrom(
526                self.handle(),
527                buf.as_mut_ptr() as *mut _,
528                buf.len(),
529                &mut received,
530                src.as_mut_ptr(),
531                crate::time::dur_to_ns(timeout),
532            )
533        };
534        Error::from_code(rc)?;
535        Ok((received, src))
536    }
537}
538
539impl fmt::Debug for UdpSocket {
540    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
541        f.debug_struct("UdpSocket")
542            .field("handle", &format_args!("{:p}", self.handle()))
543            .field("open", &self.open)
544            .finish_non_exhaustive()
545    }
546}
547
548impl Drop for UdpSocket {
549    fn drop(&mut self) {
550        if self.open {
551            unsafe { bindings::ove_socket_close(self.handle()) }
552        }
553    }
554}
555
556// SAFETY: Wraps a ove handle backed by inline storage. The RTOS socket calls
557// are thread-safe. Open/close are single-threaded (lifecycle guarantee).
558unsafe impl Send for UdpSocket {}
559unsafe impl Sync for UdpSocket {}
560
561// ---------------------------------------------------------------------------
562// DNS
563// ---------------------------------------------------------------------------
564
565/// Resolve a hostname to an address.
566///
567/// The `hostname` byte slice must be null-terminated (e.g. `b"example.com\0"`).
568///
569/// # Errors
570/// Returns an error if name resolution fails or times out.
571pub fn dns_resolve(hostname: &[u8], timeout: core::time::Duration) -> Result<Address> {
572    let mut addr = Address::default();
573    let rc = unsafe {
574        bindings::ove_dns_resolve(
575            hostname.as_ptr() as *const _,
576            addr.as_mut_ptr(),
577            crate::time::dur_to_ns(timeout),
578        )
579    };
580    Error::from_code(rc)?;
581    Ok(addr)
582}