ts_netstack_smoltcp_core 0.3.3

command-channel-based userspace netstack built on smoltcp (core functionality)
Documentation

ts_netstack_smoltcp_core (netcore)

Command-channel-driven userspace network stack.

This is an opinionated wrapper around [smoltcp] that provides an easier-to-integrate, more-portable API.

Organization

This is the core crate, which just provides a [Netstack] type that contains the required state and logic to drive the contained [smoltcp::iface::Interface] to process commands received over a [Channel]. The ergonomic sockets API around the remote end of that channel is provided by ts_netstack_smoltcp_socket.

The whole thing is integrated along with runtime functionality in ts_netstack_smoltcp.

Motivation

  • Usage of smoltcp sockets requires access to their backing [SocketSet][smoltcp::iface::SocketSet] (buffer storage). This functionally attaches them to the lifetime of that storage, meaning that synchronous access across threads would require passing around Arc<Mutex<SocketSet>> and grabbing locks everywhere that's used. Instead, this crate uses a channel-oriented approach, converting the socket function call API into a set of RPC-style request/response messages.

  • Lack of direct integration with async: the command-channel paradigm means that this is straightforward in our implementation: a task or thread drives the netstack, and sockets enjoy asynchronous semantics by polling on channel sends and receives. Because the channel library we're using ([flume]) supports both sync and async operation, this means that sockets work naturally in sync contexts as well.

  • Lack of features: smoltcp is a minimal core crate -- it doesn't provide TCP accept logic, any commitments re: allocation, a complete polling loop, garbage collection of closed TCP connections, or a way to block until e.g. a TCP connection is established.

Example

extern crate ts_netstack_smoltcp_core as netcore;

use core::net::SocketAddr;
use bytes::Bytes;
use smoltcp::time::Instant;
use smoltcp::phy::Medium;
use netcore::{Response, udp, HasChannel};

fn main() -> Result<(), netcore::Error> {
    // Construct a new netstack:
    let mut stack = netcore::Netstack::new(netcore::Config::default(), Instant::ZERO);

    // Grab a channel through which we can send commands:
    let channel = stack.command_channel();

    // Process the upcoming bind and send commands in the background (request() blocks
    // for a response, hence the thread)
    let thread = std::thread::spawn(move || {
        for i in 0..2 {
            let cmd = stack.wait_for_cmd_blocking(None).unwrap();
            stack.process_one_cmd(cmd);
        }

        stack
    });

    // Send a command to bind a UDP socket:
    let endpoint = SocketAddr::from(([127, 0, 0, 1], 1000));
    let Response::Udp(udp::Response::Bound { handle, local }) = channel.request_blocking(None, udp::Command::Bind {
        endpoint
    })? else {
        unreachable!();
    };
    println!("bound udp socket to {local}");

    // Issue a command to send a UDP packet over the channel:
    channel.request_nonblocking(Some(handle), udp::Command::Send {
        endpoint: SocketAddr::from(([1, 2, 3, 4], 80)),
        buf: Bytes::copy_from_slice(b"hello"),
    })?;
    println!("sent udp packet");

    // Wait for the thread started above to finish processing the two UDP port commands:
    let mut stack = thread.join().unwrap();

    // Pump the netstack to produce the IP packet that needs to be sent out on the network:
    let (end1, end2) = netcore::Pipe::unbounded();
    stack.poll_device_io(Instant::ZERO, &mut netcore::PipeDev {
        pipe: end1,
        medium: Medium::Ip,
        mtu: 1500,
    });

    // Receive the packet from the pipe device:
    let packet = end2.rx.recv().unwrap();
    println!("packet: {packet:?}");

    // Sanity-check that the packet we got back is shaped correctly:
    assert_eq!(packet.len(), smoltcp::wire::IPV4_HEADER_LEN + smoltcp::wire::UDP_HEADER_LEN + b"hello".len());
    assert_eq!(packet[0] >> 4, 4); // ipv4 packet
    assert!(packet.ends_with(b"hello"));

    Ok(())
}

Compare the examples in ts_netstack_smoltcp_socket and ts_netstack_smoltcp (which do the same thing as this example) for an indication of the abstraction that crate provides.