Networking Stack
SafeC provides a complete embedded networking stack in std/net/. All modules work directly on a PacketBuf — a fixed-size byte buffer — with no heap allocation required.
#include "net/net.h" // master header: pulls in all modules belowLayer Map
┌─────────────────────────────────┐
│ Application (dns, dhcp) │
├─────────────────────────────────┤
│ Transport (tcp, udp) │
├─────────────────────────────────┤
│ Network (ipv4, ipv6) │
├─────────────────────────────────┤
│ Link (ethernet, arp) │
├─────────────────────────────────┤
│ PacketBuf (raw byte buffer) │
└─────────────────────────────────┘net-core
#include "net/net_core.h"PacketBuf — the fundamental packet container:
struct PacketBuf {
unsigned char data[NET_MTU]; // 1514 bytes
unsigned long len;
void* at(unsigned long off); // pointer into data at offset
void reset(); // zero the buffer and set len = 0
}NetIf — an abstract network interface handle. It has no rx/tx PacketBuf fields of its own — the caller owns its own PacketBuf(s) and passes one to tx() explicitly:
struct NetIf {
unsigned char mac[6]; // hardware MAC address
unsigned int ip4; // assigned IPv4, network byte order
unsigned int gateway; // default gateway IPv4, network byte order
unsigned int netmask; // subnet mask, network byte order
void* tx_fn; // driver send callback, cast before calling --
// fn(void* iface_ctx, unsigned char* data, unsigned long len) -> int
void* iface_ctx; // opaque driver context, passed as tx_fn's first argument
// Transmit 'pkt'. Casts tx_fn and calls it as
// tx_fn(iface_ctx, pkt.data, pkt.len). Returns 0 on success, -1 if tx_fn is unset.
int tx(&stack PacketBuf pkt);
}tx_fn is void*, not a typed function pointer
Assigning to it and implementing a driver both go through an explicit cast — iface.tx_fn = (void*)my_driver_send; to set it (inside unsafe {}), and tx()'s own implementation casts it back to fn int(void*, unsigned char*, unsigned long) before calling. There's no PacketBuf rx/PacketBuf tx field to build a frame into directly — build into your own local struct PacketBuf pkt (via udp_frame/eth_build/etc., below) and pass &pkt to iface.tx(...), as the end-to-end example does.
Byte-order utilities:
unsigned short net_htons(unsigned short x);
unsigned int net_htonl(unsigned int x);
unsigned short net_ntohs(unsigned short x);
unsigned int net_ntohl(unsigned int x);Address helpers:
unsigned int net_ip4(unsigned char a, unsigned char b,
unsigned char c, unsigned char d);
void net_ip4_str(unsigned int ip, char* out); // "192.168.1.1\0"
void net_mac_str(const unsigned char mac[6], char* out); // "aa:bb:cc:dd:ee:ff\0"ethernet
#include "net/ethernet.h"#define ETH_TYPE_IPV4 0x0800
#define ETH_TYPE_ARP 0x0806
#define ETH_TYPE_IPV6 0x86DD
#define ETH_HDR_LEN 14 // 6 dst + 6 src + 2 ethertype
struct EthernetHdr {
unsigned char dst[6];
unsigned char src[6];
unsigned short ethertype; // host byte order after parse
};
// Parse Ethernet header from packet (returns 0 on success, -1 if too short).
int eth_parse(&stack PacketBuf pkt, &stack EthernetHdr hdr_out);
// Write Ethernet header into packet at offset 0; sets pkt.len to ETH_HDR_LEN.
void eth_build(&stack PacketBuf pkt, const unsigned char dst[6],
const unsigned char src[6], unsigned short ethertype);arp
#include "net/arp.h"ArpTable holds up to 16 entries (ARP_TABLE_SIZE) — entries[i].ip4 == 0 marks a slot empty; there's no separate valid flag or entry count field.
#define ARP_TABLE_SIZE 16
#define ARP_OP_REQUEST 1
#define ARP_OP_REPLY 2
struct ArpEntry {
unsigned int ip4; // IPv4 in network byte order (0 = empty)
unsigned char mac[6];
};
struct ArpTable {
struct ArpEntry entries[16];
void update(unsigned int ip4, const unsigned char mac[6]);
int lookup(unsigned int ip4, unsigned char mac_out[6]) const; // 1 on hit
void evict(unsigned int ip4);
void clear();
};
// Build an ARP request/reply packet into pkt (starting after the Ethernet
// header -- pkt.len must already include ETH_HDR_LEN).
void arp_build_packet(&stack PacketBuf pkt,
unsigned short op,
const unsigned char sha[6], unsigned int spa,
const unsigned char tha[6], unsigned int tpa);
// Parse an ARP packet at pkt.data + offset. Fills op/sha/spa/tha/tpa on
// success (returns 0), -1 on bad length.
int arp_parse_packet(&stack PacketBuf pkt, unsigned long offset,
&stack unsigned short op,
unsigned char sha[6], &stack unsigned int spa,
unsigned char tha[6], &stack unsigned int tpa);ipv4
#include "net/ipv4.h"#define IPV4_HDR_LEN 20 // minimum, no options
#define IP_PROTO_ICMP 1
#define IP_PROTO_TCP 6
#define IP_PROTO_UDP 17
struct Ipv4Hdr {
unsigned char ihl; // header length in 32-bit words (usually 5)
unsigned char dscp;
unsigned short total_len; // host byte order
unsigned short id;
unsigned short frag_off; // host byte order (includes flags)
unsigned char ttl;
unsigned char proto;
unsigned short checksum; // 0 after parse (not verified)
unsigned int src; // network byte order
unsigned int dst; // network byte order
};
unsigned short ip_checksum(const unsigned char* data, unsigned long len);
// Parse IPv4 header from pkt at byte offset 'offset'.
int ipv4_parse(&stack PacketBuf pkt, unsigned long offset, &stack Ipv4Hdr hdr_out);
// Write IPv4 header at 'offset'; computes/fills the checksum.
// Returns byte offset of the first payload byte.
unsigned long ipv4_build(&stack PacketBuf pkt, unsigned long offset,
unsigned char proto, unsigned int src, unsigned int dst,
unsigned short payload_len);ipv6
#include "net/ipv6.h"#define IPV6_HDR_LEN 40 // fixed header length
#define IPV6_ADDR_LEN 16 // bytes in an IPv6 address
struct Ipv6Addr {
unsigned char bytes[16];
};
struct Ipv6Hdr {
unsigned int ver_tc_fl; // version(4) + traffic class(8) + flow label(20)
unsigned short payload_len; // length of payload (after the fixed header)
unsigned char next_hdr; // same numbering as IPv4's proto field
unsigned char hop_limit;
struct Ipv6Addr src;
struct Ipv6Addr dst;
};
// Address utilities -- all take addresses by '&stack Ipv6Addr' reference.
int ipv6_addr_eq(const &stack Ipv6Addr a, const &stack Ipv6Addr b);
int ipv6_addr_is_unspecified(const &stack Ipv6Addr a);
int ipv6_addr_is_loopback(const &stack Ipv6Addr a); // ::1
int ipv6_addr_is_link_local(const &stack Ipv6Addr a); // fe80::/10
void ipv6_addr_str(const &stack Ipv6Addr addr, char* buf); // 40-byte buf
// Parse IPv6 header from pkt at byte offset 'offset'.
int ipv6_parse(&stack PacketBuf pkt, unsigned long offset, &stack Ipv6Hdr hdr_out);
// Write IPv6 header at 'offset'. Returns byte offset of the first payload byte.
unsigned long ipv6_build(&stack PacketBuf pkt, unsigned long offset,
unsigned char next_hdr, unsigned char hop_limit,
const &stack Ipv6Addr src, const &stack Ipv6Addr dst,
unsigned short payload_len);
// Full frame: Ethernet + IPv6 header, caller writes the payload at the
// returned offset. Returns byte offset of the first payload byte.
unsigned long ipv6_frame(&stack PacketBuf pkt,
const unsigned char eth_src[6],
const unsigned char eth_dst[6],
unsigned char next_hdr,
const &stack Ipv6Addr src,
const &stack Ipv6Addr dst,
unsigned short payload_len);udp
#include "net/udp.h"#define UDP_HDR_LEN 8
struct UdpHdr {
unsigned short src_port; // host byte order
unsigned short dst_port; // host byte order
unsigned short length; // total UDP datagram length (header + payload)
unsigned short checksum;
};
// Parse UDP header from pkt at byte offset 'offset'.
int udp_parse(&stack PacketBuf pkt, unsigned long offset, &stack UdpHdr hdr_out);
// Write UDP header at 'offset'; payload_len = bytes of data after the
// header (checksum is left 0 -- optional in IPv4). Returns byte offset of
// the first payload byte.
unsigned long udp_build(&stack PacketBuf pkt, unsigned long offset,
unsigned short src_port, unsigned short dst_port,
unsigned short payload_len);
// Full frame: Ethernet + IPv4 + UDP headers; pkt is reset first. Returns
// byte offset where the caller should write the payload -- see the
// End-to-End Example below for the full call/write/tx sequence.
unsigned long udp_frame(&stack PacketBuf pkt,
const unsigned char eth_src[6],
const unsigned char eth_dst[6],
unsigned int ip_src, unsigned int ip_dst,
unsigned short src_port, unsigned short dst_port,
unsigned short payload_len);tcp
#include "net/tcp.h"TcpConn implements the full 11-state RFC 793 TCP state machine. It has no iface/NetIf field — build_segment/recv each take the MAC addresses or packet offset they need directly, per-call, the same "caller owns the PacketBuf" shape as NetIf::tx() above:
// Connection states
#define TCP_CLOSED 0
#define TCP_LISTEN 1
#define TCP_SYN_SENT 2
#define TCP_SYN_RECEIVED 3
#define TCP_ESTABLISHED 4
#define TCP_FIN_WAIT1 5
#define TCP_FIN_WAIT2 6
#define TCP_CLOSE_WAIT 7
#define TCP_CLOSING 8
#define TCP_LAST_ACK 9
#define TCP_TIME_WAIT 10
struct TcpConn {
int state;
unsigned int local_ip;
unsigned int remote_ip;
unsigned short local_port;
unsigned short remote_port;
unsigned int snd_nxt; // next sequence number to send
unsigned int snd_una; // oldest unacknowledged sequence number
unsigned int rcv_nxt; // next expected receive sequence number
unsigned int rcv_wnd; // receive window advertised
unsigned char rx_buf[2048];
unsigned long rx_len;
unsigned char tx_buf[2048];
unsigned long tx_len;
// Feed a received packet (starting at TCP header offset in pkt).
// Returns 1 if state changed, 0 otherwise.
int recv(&stack PacketBuf pkt, unsigned long tcp_offset);
// Enqueue payload data to send. Returns bytes accepted (0 if full).
unsigned long send(const unsigned char* data, unsigned long len);
// Build next outgoing segment into pkt; returns 1 if a segment was produced.
int build_segment(&stack PacketBuf pkt,
const unsigned char eth_src[6],
const unsigned char eth_dst[6]);
int rx_ready() const; // is there received data waiting?
// Consume up to 'len' bytes from rx_buf into 'out'; returns bytes copied.
unsigned long read(unsigned char* out, unsigned long len);
// Initiate active open (send SYN).
void connect(unsigned int remote_ip, unsigned short remote_port,
unsigned int local_ip, unsigned short local_port,
unsigned int isn);
void close(); // send FIN
}
// Parse TCP header from pkt at byte offset 'offset'.
int tcp_parse(&stack PacketBuf pkt, unsigned long offset, &stack TcpHdr hdr_out);
unsigned short tcp_checksum(unsigned int src_ip, unsigned int dst_ip,
void* segment, unsigned long len);dns
#include "net/dns.h"#define DNS_PORT 53
#define DNS_MAX_NAME 255
#define DNS_MAX_MSG 512
// Build a DNS A-record query for 'name' into pkt. 'txid' is caller-chosen
// (not generated internally) -- match it against the reply yourself.
unsigned short dns_query(&stack PacketBuf pkt,
const unsigned char eth_src[6],
const unsigned char eth_dst[6],
unsigned int ip_src, unsigned int ip_dns,
unsigned short src_port,
const char* name,
unsigned short txid);
// Parse a DNS reply in pkt (starting at the UDP payload offset). On
// success fills ip4_out with the first A record and returns 1; returns 0
// on a parse error, no A record, or a txid that doesn't match expected_txid.
int dns_parse_reply(&stack PacketBuf pkt, unsigned long udp_payload_offset,
unsigned short expected_txid,
&stack unsigned int ip4_out);dhcp
#include "net/dhcp.h"#define DHCP_SERVER_PORT 67
#define DHCP_CLIENT_PORT 68
// DHCP client state machine
#define DHCP_STATE_IDLE 0
#define DHCP_STATE_SELECTING 1
#define DHCP_STATE_REQUESTING 2
#define DHCP_STATE_BOUND 3
struct DhcpLease {
unsigned int your_ip; // offered/assigned IPv4 (network order)
unsigned int server_ip; // DHCP server IPv4 (network order)
unsigned int gateway; // default gateway (network order)
unsigned int subnet_mask; // subnet mask (network order)
unsigned int dns; // DNS server (network order)
unsigned int lease_secs; // lease duration in seconds
};
struct DhcpClient {
int state;
unsigned int xid; // transaction ID
struct DhcpLease lease;
unsigned char mac[6];
// Build a DHCPDISCOVER packet.
void discover(&stack PacketBuf pkt, const unsigned char eth_dst[6]);
// Build a DHCPREQUEST for 'offered_ip' from 'server_ip'.
void request(&stack PacketBuf pkt, const unsigned char eth_dst[6],
unsigned int offered_ip, unsigned int server_ip);
// Parse an incoming DHCP reply; fills lease on ACK/OFFER. Returns
// DHCP_OFFER, DHCP_ACK, DHCP_NAK, or 0 on unrecognised.
int parse_reply(&stack PacketBuf pkt, unsigned long udp_payload_offset);
int is_bound() const; // 1 if the client has a valid lease
}No NetIf* iface field, no zero-argument discover()/request()
DhcpClient doesn't hold a reference to a NetIf — like TcpConn, it builds into a caller-supplied &stack PacketBuf, and discover()/ request() take the destination MAC (and, for request(), the offered/ server IPs) as explicit arguments rather than reading them from a stored interface. Transmit the packet the same way the End-to-End Example does: build it, then call iface.tx(&pkt) yourself.
End-to-End Example
udp_frame builds the Ethernet + IPv4 + UDP headers into a PacketBuf and returns the byte offset where the payload belongs (it also pre-sets pkt.len to cover the payload region) — write your payload bytes there yourself, then hand the packet to iface.tx(...). Verified against a real compile/run:
#include <std/net/net_core.sc>
#include <std/net/ethernet.sc>
#include <std/net/ipv4.sc>
#include <std/net/udp.sc>
// A real driver would hand these bytes to hardware/a socket; this one just
// reports how many bytes it was asked to send.
int my_driver_send(void* ctx, unsigned char* data, unsigned long len) {
printf("driver: sending %lu bytes\n", len);
return 0;
}
int main() {
struct NetIf iface;
iface.mac[0] = 0xAA; iface.mac[1] = 0xBB; iface.mac[2] = 0xCC;
iface.mac[3] = 0xDD; iface.mac[4] = 0xEE; iface.mac[5] = 0xFF;
iface.ip4 = std::net_ip4(192, 168, 1, 10);
unsafe { iface.tx_fn = (void*)my_driver_send; }
iface.iface_ctx = (void*)0; // no driver context needed here
unsigned char dst_mac[6];
dst_mac[0] = 0xFF; dst_mac[1] = 0xFF; dst_mac[2] = 0xFF;
dst_mac[3] = 0xFF; dst_mac[4] = 0xFF; dst_mac[5] = 0xFF;
unsigned int src_ip = std::net_ip4(192, 168, 1, 10);
unsigned int dst_ip = std::net_ip4(192, 168, 1, 255);
unsigned char payload[5];
payload[0] = 'h'; payload[1] = 'e'; payload[2] = 'l';
payload[3] = 'l'; payload[4] = 'o';
struct PacketBuf pkt;
unsigned long offset = std::udp_frame(&pkt, iface.mac, dst_mac,
src_ip, dst_ip,
1234, 5678, 5);
unsafe {
unsigned char* d = (unsigned char*)pkt.at(offset);
int i = 0;
while (i < 5) { d[i] = payload[i]; i = i + 1; }
}
int rc = iface.tx(&pkt); // transmit via driver
printf("tx rc=%d\n", rc);
// driver: sending 47 bytes (14 eth + 20 ipv4 + 8 udp + 5 payload)
// tx rc=0
return 0;
}