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}