lneto Userspace Networking Library

repository·main·Indexed 41 days ago

https://github.com/soypat/lneto

A heapless, zero-allocation userspace networking library for embedded systems and zero-OS environments. It provides a complete networking stack including Ethernet, IPv4, IPv6, TCP, UDP, DNS, DHCP, and NTP, designed to operate without OS dependencies or internal goroutines. Includes httφ (httphi), a transport-agnostic, heapless HTTP/1.1 router for microcontrollers.

Tokens
25.9K
Snippets
58
Records
96
Agent score
52%

What's inside lneto

  1. What is httφ (httphi)?

    main

    httφ is a heapless HTTP/1.1 router designed for environments where per-request allocations are unacceptable, such as microcontrollers. Unlike the standard net/http package which allocates a Request, header map, response buffer, and goroutine for every request, httφ performs these allocations once during the Router.Configure phase.

    Key characteristics:

    • Zero allocation during serving: Memory usage is fixed at configuration time and does not grow with load.
    • Fixed resource limits: If no free exchanges are available, the Handle method refuses the connection rather than allocating more memory.
    • Transport agnostic: The Handle method accepts an io.ReadWriteCloser, allowing it to run over raw sockets, lneto TCP stacks, or test pipes without requiring an OS listener or clock.
    • Parsing: Uses httpraw for parsing.
  2. Overview of lneto userspace networking

    main

    lneto is a userspace networking library designed for zero-OS environments, embedded systems, and lightweight alternatives to gVisor. It provides a complete networking stack (Ethernet, IPv4, TCP, UDP, etc.) with a focus on being heapless, zero-allocation, and having a minimal memory footprint.

    Key characteristics:

    • Zero OS dependency: Performs no time.Sleep or time.Now calls unless explicitly configured via a user-provided HAL.
    • Zero scheduling requirement: Does not use goroutines or channels internally; it can be integrated into an existing event loop.
    • Heapless processing: Designed for high performance and low memory usage (e.g., an entire Ethernet+IPv4+UDP+DHCP+DNS+NTP stack fits in ~2kB RAM).
    • Minimal dependencies: Uses only the Go standard library (e.g., bytes, errors, io).
  3. RMII Overview and Signal Definitions

    main

    RMII (Reduced Media Independent Interface) is a simplified interface between a MAC and a PHY, reducing the pin count from 16 (MII) to 7-8 pins.

    Signal Mapping (PHY Perspective):

    • TX_EN (Input): Transmit enable. MAC asserts this when presenting valid dibits.
    • TXD[1:0] (Input): Transmit data. 2-bit dibit per clock cycle.
    • CRS_DV (Output): Combined Carrier Sense / Receive Data Valid signal.
    • RXD[1:0] (Output): Receive data. 2-bit dibit per clock cycle.
    • RX_ER (Output): Receive error indicator (optional).
    • REF_CLK (Input or Output): 50 MHz reference clock.

    Timing Requirement: All RMII signals are synchronous to the rising edge of the 50 MHz REF_CLK. The PHY updates RXD[1:0], CRS_DV, and RX_ER shortly after the rising edge; the MAC should sample these on the next rising edge to capture the previous cycle's value.

  4. Understand the StackNode abstraction

    main

    The StackNode interface is the core abstraction implemented by networking stack nodes and the stack itself. It defines how data is encapsulated, demultiplexed, and identified within the stack.

    Implementations of StackNode are used to build up the layers of the networking stack (e.g., moving from Ethernet to IPv4 to TCP).

    type StackNode interface {
        // Encapsulate receives a buffer the receiver must fill with data.
        // The receiver's start byte is at carrierData[offsetToFrame].
        Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (int, error)
        // Demux receives a buffer the receiver must decode and pass on to corresponding child StackNode(s).
        // The receiver's start byte is at carrierData[offsetToFrame].
        Demux(carrierData []byte, offsetToFrame int) error
        // LocalPort returns the port of the node if applicable or zero. Used for UDP/TCP nodes.
        LocalPort() uint16
        // Protocol returns the protocol of this node if applicable or zero. Usually either an ethernet.Type (EtherType) or lneto.IPProto (IP Protocol number).
        Protocol() uint64
        // ConnectionID returns a pointer to the connection ID of the StackNode.
        // A change in the ID means the node is no longer valid and should be discarded.
        // A change in the ID could mean the connection was closed by the user or that the node will not send nor receive any more data over said connection ID.
        ConnectionID() *uint64
    }
  5. Understand the RMII Rx0/Rx1/CRS_DV Interface

    main

    The RMII receive path uses RXD[1:0] for data and CRS_DV as a combined carrier sense and data valid signal.

    Signal Behaviors

    • CRS_DV: Combines Carrier Sense (CRS) and Data Valid (DV).
      • CRS: Asserted when the medium is non-idle. Loss of carrier deasserts CRS_DV synchronously to REF_CLK.
      • DV: Indicates data is valid. Note that RXD[1:0] is considered valid once CRS_DV is asserted, but may be asynchronous to REF_CLK initially (appearing as 0b00 until decoding occurs).
      • End of packet toggle: If the carrier ends before the PHY's elasticity buffer is empty, CRS_DV may toggle at a nibble rate (HIGH on the second dibit, LOW on the first dibit of each nibble) to indicate carrier loss while data is still valid.
    • RXD[1:0]: Transitions synchronously to REF_CLK. It transfers two bits (a dibit) per clock period while CRS_DV is asserted. When idle (CRS_DV low), it remains 0b00.
    • RX_ER: A synchronous signal to REF_CLK. It is asserted for one or more periods to indicate a frame error (e.g., 4B/5B invalid code, bad SFD, signal amplitude issues, or FIFO overflow). If RX_ER is not checkable, the MAC should rely on CRC validation.
    • RX_DV (Optional): Some PHYs provide a dedicated RX_DV signal. It is asserted with the first recovered data (preamble) and deasserted after the final dibit. Full-duplex MACs can use this to avoid deriving DV from CRS_DV.
  6. Avoid host TCP stack interference when running xcurl

    main

    Running xcurl directly on a standard Linux interface (e.g., wlan0 or eth0) causes xcurl to share the host's IP and MAC address. If the host kernel sees incoming packets for that IP that it doesn't own via a socket, it may send a TCP RST, interrupting the xcurl flow.

    To avoid this, use one of the following preferred methods:

    • Use the -ihttp flag to connect to an httptap interface.
    • Use a TAP interface.
    • Use a network namespace.
    • Use an otherwise isolated interface/IP.
  7. Configure RMII Clock Source Modes

    main

    RMII requires a continuous 50 MHz reference clock. There are two primary modes for managing this clock:

    1. Clock Source Mode (PHY is clock source)

    In this mode, the PHY generates the 50 MHz clock for the MAC.

    • Setup: The PHY takes a 25 MHz crystal/oscillator on its XI/XO pins and uses an internal PLL to multiply it to 50 MHz.
    • Output: The PHY outputs the 50 MHz signal on the REF_CLK pin to the MAC.
    • Examples: KSZ8081RNA (default), LAN8720A (with nINTSEL pin low at reset).

    2. Clock Follower Mode (MAC is clock source)

    In this mode, an external clock source drives both the PHY and the MAC.

    • Setup: An external 50 MHz oscillator feeds both the PHY (XI pin) and the MAC.
    • PHY Connection: The PHY REF_CLK pin is either left unconnected or used as an input.
    • Example: DP83826 in RMII follower mode.

    Typical Clock Specifications:

    • Frequency: 50 MHz ±50 ppm
    • Duty cycle: 40-60%
    • Jitter: <150 ps peak-to-peak
  8. Configure unbounded goroutines in httphi

    main
    While httphi is designed for fixed resource allocation, you can achieve an 'unbounded' behavior (similar to standard net/http) by setting FixedNumGoroutines to -1 in your configuration. This will result in one goroutine and one exchange being allocated per connection.
  9. Handle 10M vs 100M Link Mode Speed Differences in RMII

    main

    The RMII interface handles data rates differently depending on the link speed:

    • 100 Mbps: Delivers one dibit per REF_CLK cycle (50 MHz clock $\times$ 2 bits = 100 Mbps).
    • 10 Mbps: Each dibit is repeated 10 times across 10 REF_CLK cycles to maintain the bit rate.

    When implementing a receiver, your logic must account for this 10x repetition in 10M mode to avoid sampling the same dibit ten times as ten different pieces of data.

    100M: one dibit per clock
    REF_CLK:   _|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|
    RXD[1:0]:  |D0|D1|D2|D3|D4|D5|D6|D7|
    
    10M: each dibit held for 10 clocks
    REF_CLK:   _|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_ ... (×10 per dibit)
    RXD[1:0]:  |D0|D0|D0|D0|D0|D0|D0|D0|D0|D0|D1|D1|D1|...
  10. Implement the Practical RMII Rx Sequence

    main

    To correctly receive frames via the RMII interface, follow this sequence:

    1. Monitor CRS_DV: Watch for the signal to assert.
    2. Detect SFD: Wait for the Start of Frame Delimiter (SFD) pattern, which consists of dibits ending in 11 (representing the 0xD5 byte).
    3. Sample Data: Sample RXD[1:0] on every REF_CLK rising edge while DV is asserted (note that CRS_DV may toggle if the carrier drops before the buffer is empty).
    4. Error Checking: Ensure RX_ER was not asserted during the frame. Some PHYs may emit invalid data on RX[1:0] after an error.
    5. Reassemble Bytes: Assemble the received dibits into bytes using LSB first order.
    6. Validate: Perform a CRC32 check on the assembled frame to ensure integrity.
    1. Monitor CRS_DV for assertion
    2. Wait for SFD pattern: dibits ending in "11" (0xD5 byte)
    3. Sample RXD[1:0] on each REF_CLK rising while DV asserted (CRS_DV may toggle for DV assert)
    4. Check RX_ER was never asserted during frame.
    5. Assemble dibits into bytes (LSB first, same as Tx)
    6. Validate CRC32 on assembled frame
  11. Implement a heapless HTTP server with httphi

    main

    To use httphi, you define your routes using a httphi.MuxSlice, configure a httphi.Router with fixed resource limits, and then manually pass connections (implementing io.ReadWriteCloser) to the router's Handle method.

    Note that accepting connections is the responsibility of the caller. If router.Handle(conn) returns an error, the connection should be closed immediately. This error typically indicates the router has refused the connection because it has reached its pre-allocated capacity.

    var mux httphi.MuxSlice
    mux.Handle("GET /", func(ex *httphi.Exchange) {
    	ex.WriteBody([]byte("hello world"))
    })
    
    var router httphi.Router
    // numWorkers, memoryPerConn, and mux.MaxPathValues() are configuration parameters
    cfg := httphi.DefaultRouterConfig(numWorkers, memoryPerConn, mux.MaxPathValues())
    err := router.Configure(&mux, cfg)
    if err != nil {
    	log.Fatal(err)
    }
    
    for {
    	conn, err := listener.Accept() // Accepting is the caller's job.
    	if err != nil {
    		log.Fatal(err)
    	}
    	if err = router.Handle(conn); err != nil {
    		conn.Close() // Refused: never blocks, never queues unboundedly.
    	}
    }
  12. Quick run xcurl over httptap

    main

    To run the xcurl example using the httptap interface (which avoids the need for root privileges on the xcurl client), follow these steps in two separate terminal sessions:

    1. Start the server (requires root):
      go build ./examples/httptap && sudo ./httpap
    2. Run the client (non-root):
      go run ./examples/xcurl -host google.com -ihttp
    go build ./examples/httptap && sudo ./httpap
    
    go run ./examples/xcurl -host google.com -ihttp