네트워킹 스택
SafeC는 std/net/에 완전한 임베디드 네트워킹 스택을 제공합니다. 모든 모듈은 힙 할당 없이 고정 크기 바이트 버퍼인 PacketBuf 위에서 직접 동작합니다.
#include <std/net/net.h> // master header: pulls in all modules below
계층 지도
┌─────────────────────────────────┐
│ 애플리케이션 (dns, dhcp) │
├─────────────────────────────────┤
│ 전송 (tcp, udp) │
├─────────────────────────────────┤
│ 네트워크 (ipv4, ipv6) │
├─────────────────────────────────┤
│ 링크 (ethernet, arp) │
├─────────────────────────────────┤
│ PacketBuf (원시 바이트 버퍼) │
└─────────────────────────────────┘
net-core
#include <std/net/net_core.h>
PacketBuf — 기본 패킷 컨테이너:
struct PacketBuf {
unsigned char data[NET_MTU]; // 1514 bytes
unsigned long len;
?&unsigned char at(unsigned long off); // the byte at off; null past NET_MTU
void reset(); // zero the buffer and set len = 0
}
NetIf — 추상 네트워크 인터페이스입니다. 자체 rx/tx 버퍼를 갖지 않습니다: 각 프레임을 여러분 자신의 struct PacketBuf에 만들고(아래의 udp_frame/eth_build/… 사용) 그것을 tx()에 전달합니다. transmit 메서드를 가진 드라이버 타입(NetDriver) 위에 NetIf::new로 생성한 다음 mac과 주소를 채웁니다.
struct NetIf {
unsigned char mac[6]; // hardware MAC address
unsigned int ip4; // assigned IPv4 (numeric form, see net_ip4)
unsigned int gateway;
unsigned int netmask;
// An interface whose tx() calls drv.transmit(frame); drv must outlive it.
generic<T: NetDriver>
static struct NetIf new(&T drv);
int tx(&PacketBuf pkt); // the driver's transmit(pkt.data, pkt.len); 0 on success
};
네트워크 드라이버: NetDriver
NetIf::new는 단일 기능 트레이트 하나로 바운드됩니다:
| 트레이트 | 요구 사항 | 사용처 |
|---|---|---|
NetDriver |
int transmit([]const unsigned char frame): 프레임 하나를 보내며 성공 시 0 |
NetIf::new |
std에는 드라이버가 포함되어 있지 않습니다. 타입은 별도 선언 없이 이 메서드를 가지기만 하면 준수합니다. NetIf::new<T>는 컴파일 타임에 T::transmit로 인터페이스의 송신 항목을 만들고 드라이버를 타입 소거된 형태로 저장하므로, 하나의 스택이 서로 다른 드라이버 위의 인터페이스를 운용할 수 있습니다. transmit이 없는 타입을 넘기면 NetIf::new 호출 지점에서 오류가 납니다. (블록 장치도 BlockDriver로 같은 방식을 따릅니다.)
struct Enc28j60 {
int cs_pin;
unsigned long sent;
int transmit([]const unsigned char frame);
};
int Enc28j60::transmit([]const unsigned char frame) {
// ... push frame.len bytes of `frame` to the chip over SPI ...
self.sent = self.sent + 1UL;
return 0;
}
static struct Enc28j60 g_nic;
void net_setup() {
struct NetIf eth0 = std::NetIf::new(&g_nic);
eth0.ip4 = std::net_ip4(192, 168, 1, 50);
}
바이트 순서 유틸리티:
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);
주소 헬퍼:
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"
상수: NET_MTU(1514, 프리앰블/FCS를 뺀 이더넷 프레임), NET_MAC_LEN(6), NET_IP4_LEN(4).
ethernet
#include <std/net/ethernet.h>
#define ETH_TYPE_IPV4 0x0800
#define ETH_TYPE_ARP 0x0806
#define ETH_TYPE_IPV6 0x86DD
#define ETH_HDR_LEN 14 // dst 6바이트 + src 6바이트 + ethertype 2바이트
struct EthernetHdr {
unsigned char dst[6];
unsigned char src[6];
unsigned short ethertype; // 파싱 후에는 호스트 바이트 순서
};
// 패킷에서 Ethernet 헤더를 파싱한다 (성공 시 0, 너무 짧으면 -1을 반환).
int eth_parse(&PacketBuf pkt, &EthernetHdr hdr_out);
// 오프셋 0에 Ethernet 헤더를 기록한다; pkt.len을 ETH_HDR_LEN으로 설정한다.
void eth_build(&PacketBuf pkt, const unsigned char dst[6],
const unsigned char src[6], unsigned short ethertype);
arp
#include <std/net/arp.h>
ArpTable은 최대 16개 항목(ARP_TABLE_SIZE)을 담습니다 — entries[i].ip4 == 0은 슬롯이 비어 있음을 뜻합니다; 별도의 valid 플래그나 항목 개수 필드는 없습니다.
#define ARP_TABLE_SIZE 16
#define ARP_OP_REQUEST 1
#define ARP_OP_REPLY 2
struct ArpEntry {
unsigned int ip4; // 네트워크 바이트 순서의 IPv4 (0 = 비어 있음)
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
void evict(unsigned int ip4);
void clear();
};
// pkt에 ARP 요청/응답 패킷을 만든다 (Ethernet 헤더 이후부터 시작 --
// pkt.len은 이미 ETH_HDR_LEN을 포함하고 있어야 함).
void arp_build_packet(&PacketBuf pkt,
unsigned short op,
const unsigned char sha[6], unsigned int spa,
const unsigned char tha[6], unsigned int tpa);
// pkt.data + offset 위치의 ARP 패킷을 파싱한다. 성공하면 (0을 반환하며)
// op/sha/spa/tha/tpa를 채우고, 길이가 잘못됐으면 -1을 반환한다.
int arp_parse_packet(&PacketBuf pkt, unsigned long offset,
&unsigned short op,
unsigned char sha[6], &unsigned int spa,
unsigned char tha[6], &unsigned int tpa);
ipv4
#include <std/net/ipv4.h>
#define IPV4_HDR_LEN 20 // 최소 크기, 옵션 없음
#define IP_PROTO_ICMP 1
#define IP_PROTO_TCP 6
#define IP_PROTO_UDP 17
struct Ipv4Hdr {
unsigned char ihl; // 32비트 워드 단위 헤더 길이 (보통 5)
unsigned char dscp;
unsigned short total_len; // 호스트 바이트 순서
unsigned short id;
unsigned short frag_off; // 호스트 바이트 순서 (플래그 포함)
unsigned char ttl;
unsigned char proto;
unsigned short checksum; // 파싱 후에는 0 (검증되지 않음)
unsigned int src; // 네트워크 바이트 순서
unsigned int dst; // 네트워크 바이트 순서
};
unsigned short ip_checksum([]const unsigned char data);
// pkt의 바이트 오프셋 'offset'에서 IPv4 헤더를 파싱한다.
int ipv4_parse(&PacketBuf pkt, unsigned long offset, &Ipv4Hdr hdr_out);
// 'offset'에 IPv4 헤더를 기록한다; 체크섬을 계산해 채운다.
// 첫 페이로드 바이트의 바이트 오프셋을 반환한다.
unsigned long ipv4_build(&PacketBuf pkt, unsigned long offset,
unsigned char proto, unsigned int src, unsigned int dst,
unsigned short payload_len);
ipv6
#include <std/net/ipv6.h>
#define IPV6_HDR_LEN 40 // 고정 헤더 길이
#define IPV6_ADDR_LEN 16 // IPv6 주소의 바이트 수
struct Ipv6Addr {
unsigned char bytes[16];
};
struct Ipv6Hdr {
unsigned int ver_tc_fl; // 버전(4비트) + 트래픽 클래스(8비트) + 플로우 레이블(20비트)
unsigned short payload_len; // 페이로드 길이 (고정 헤더 이후)
unsigned char next_hdr; // IPv4의 proto 필드와 동일한 번호 체계
unsigned char hop_limit;
struct Ipv6Addr src;
struct Ipv6Addr dst;
};
// 주소 유틸리티 -- 모두 '&Ipv6Addr' 참조로 주소를 받는다.
int ipv6_addr_eq(const &Ipv6Addr a, const &Ipv6Addr b);
int ipv6_addr_is_unspecified(const &Ipv6Addr a);
int ipv6_addr_is_loopback(const &Ipv6Addr a); // ::1
int ipv6_addr_is_link_local(const &Ipv6Addr a); // fe80::/10
void ipv6_addr_str(const &Ipv6Addr addr, &char buf); // 40바이트 buf
// pkt의 바이트 오프셋 'offset'에서 IPv6 헤더를 파싱한다.
int ipv6_parse(&PacketBuf pkt, unsigned long offset, &Ipv6Hdr hdr_out);
// 'offset'에 IPv6 헤더를 기록한다. 첫 페이로드 바이트의 바이트 오프셋을 반환한다.
unsigned long ipv6_build(&PacketBuf pkt, unsigned long offset,
unsigned char next_hdr, unsigned char hop_limit,
const &Ipv6Addr src, const &Ipv6Addr dst,
unsigned short payload_len);
// 전체 프레임: Ethernet + IPv6 헤더, 호출자는 반환된 오프셋에 페이로드를
// 기록한다. 첫 페이로드 바이트의 바이트 오프셋을 반환한다.
unsigned long ipv6_frame(&PacketBuf pkt,
const unsigned char eth_src[6],
const unsigned char eth_dst[6],
unsigned char next_hdr,
const &Ipv6Addr src,
const &Ipv6Addr dst,
unsigned short payload_len);
udp
#include <std/net/udp.h>
#define UDP_HDR_LEN 8
struct UdpHdr {
unsigned short src_port; // 호스트 바이트 순서
unsigned short dst_port; // 호스트 바이트 순서
unsigned short length; // UDP 데이터그램 전체 길이 (헤더 + 페이로드)
unsigned short checksum;
};
// pkt의 바이트 오프셋 'offset'에서 UDP 헤더를 파싱한다.
int udp_parse(&PacketBuf pkt, unsigned long offset, &UdpHdr hdr_out);
// 'offset'에 UDP 헤더를 기록한다; payload_len은 헤더 이후 데이터의
// 바이트 수다 (체크섬은 0으로 남겨둠 -- IPv4에서는 선택 사항). 첫 페이로드
// 바이트의 바이트 오프셋을 반환한다.
unsigned long udp_build(&PacketBuf pkt, unsigned long offset,
unsigned short src_port, unsigned short dst_port,
unsigned short payload_len);
// 전체 프레임: Ethernet + IPv4 + UDP 헤더; pkt는 먼저 리셋된다. 호출자가
// 페이로드를 기록해야 할 바이트 오프셋을 반환한다 -- 전체 호출/기록/tx
// 순서는 아래의 종단 간 예제를 참고하라.
unsigned long udp_frame(&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 <std/net/tcp.h>
TcpConn은 RFC 793의 완전한 11개 상태 TCP 상태 기계를 구현합니다. 자체 iface/NetIf 필드는 갖지 않습니다 — build_segment/recv는 각각 필요한 MAC 주소나 패킷 오프셋을 호출마다 직접 받습니다. 위의 NetIf::tx()와 동일한 "호출자가 PacketBuf를 소유한다"는 형태입니다:
// 연결 상태
#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; // 다음에 보낼 시퀀스 번호
unsigned int snd_una; // 아직 확인되지 않은 가장 오래된 시퀀스 번호
unsigned int rcv_nxt; // 다음에 받을 것으로 기대되는 시퀀스 번호
unsigned int rcv_wnd; // 광고된 수신 윈도우
unsigned char rx_buf[2048];
unsigned long rx_len;
unsigned char tx_buf[2048];
unsigned long tx_len;
// 수신한 패킷을 처리한다 (pkt 안의 TCP 헤더 오프셋부터 시작).
// 상태가 바뀌었으면 1, 아니면 0을 반환한다.
int recv(&PacketBuf pkt, unsigned long tcp_offset);
// 보낼 페이로드 데이터를 큐에 넣는다. 받아들여진 바이트 수를 반환한다 (가득 찼으면 0).
unsigned long send([]const unsigned char data);
// 다음 송신 세그먼트를 pkt에 만든다; 세그먼트가 만들어졌으면 1을 반환한다.
int build_segment(&PacketBuf pkt,
const unsigned char eth_src[6],
const unsigned char eth_dst[6]);
int rx_ready() const; // 수신 대기 중인 데이터가 있는가?
// rx_buf에서 최대 'len' 바이트를 'out'으로 소비한다; 복사된 바이트 수를 반환한다.
unsigned long read([]unsigned char out);
// 능동 오픈을 시작한다 (SYN 전송).
void connect(unsigned int remote_ip, unsigned short remote_port,
unsigned int local_ip, unsigned short local_port,
unsigned int isn);
void close() consume; // FIN 전송
}
// pkt의 바이트 오프셋 'offset'에서 TCP 헤더를 파싱한다. 성공 시 0, 잘렸거나
// 데이터 오프셋이 5워드 미만이거나 패킷을 넘으면 -1.
int tcp_parse(&PacketBuf pkt, unsigned long offset, &TcpHdr hdr_out);
unsigned short tcp_checksum(unsigned int src_ip, unsigned int dst_ip, []const unsigned char tcp_seg); // pseudo-header + segment
상수: TCP_HDR_MIN_LEN(20); 플래그 TCP_FIN(0x01), TCP_SYN(0x02), TCP_RST(0x04), TCP_PSH(0x08), TCP_ACK(0x10), TCP_URG(0x20); 연결별 버퍼 TCP_RX_BUF와 TCP_TX_BUF(각 2048바이트).
dns
#include <std/net/dns.h>
#define DNS_PORT 53
#define DNS_MAX_NAME 255
#define DNS_MAX_MSG 512
// 'name'에 대한 DNS A 레코드 질의를 pkt에 만든다. 'txid'는 (내부에서
// 생성되는 것이 아니라) 호출자가 직접 선택한다 -- 응답과 직접 대조해서 확인하라.
unsigned short dns_query(&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);
// pkt(UDP 페이로드 오프셋에서 시작)의 DNS 응답을 파싱한다. 성공하면 첫
// A 레코드로 ip4_out을 채우고 1을 반환한다; 파싱 오류, A 레코드 없음,
// 또는 expected_txid와 일치하지 않는 txid인 경우 0을 반환한다.
int dns_parse_reply(&PacketBuf pkt, unsigned long udp_payload_offset,
unsigned short expected_txid,
&unsigned int ip4_out);
dhcp
#include <std/net/dhcp.h>
#define DHCP_SERVER_PORT 67
#define DHCP_CLIENT_PORT 68
// DHCP 클라이언트 상태 기계
#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; // 제안/할당된 IPv4 (네트워크 순서)
unsigned int server_ip; // DHCP 서버 IPv4 (네트워크 순서)
unsigned int gateway; // 기본 게이트웨이 (네트워크 순서)
unsigned int subnet_mask; // 서브넷 마스크 (네트워크 순서)
unsigned int dns; // DNS 서버 (네트워크 순서)
unsigned int lease_secs; // 임대 기간 (초)
};
struct DhcpClient {
int state;
unsigned int xid; // 트랜잭션 ID
struct DhcpLease lease;
unsigned char mac[6];
// DHCPDISCOVER 패킷을 만든다.
void discover(&PacketBuf pkt, const unsigned char eth_dst[6]);
// 'server_ip'로부터 'offered_ip'에 대한 DHCPREQUEST를 만든다.
void request(&PacketBuf pkt, const unsigned char eth_dst[6],
unsigned int offered_ip, unsigned int server_ip);
// 들어오는 DHCP 응답을 파싱한다; ACK/OFFER면 lease를 채운다. 반환값은
// DHCP_OFFER, DHCP_ACK, DHCP_NAK, 또는 인식하지 못하면 0이다.
int parse_reply(&PacketBuf pkt, unsigned long udp_payload_offset);
int is_bound() const; // 클라이언트가 유효한 임대를 갖고 있으면 1
}
NetIf* iface 필드도, 인자 없는 discover()/request()도 없음
DhcpClient는 NetIf에 대한 참조를 갖고 있지 않습니다 — TcpConn처럼,
호출자가 제공한 &PacketBuf에 프레임을 만들며, discover()/
request()는 저장된 인터페이스에서 읽어오는 대신 대상 MAC(그리고
request()의 경우 제안받은 IP/서버 IP)을 명시적인 인자로 받습니다.
종단 간 예제와 같은 방식으로 패킷을 전송합니다 — 패킷을 만든 다음
직접 iface.tx(&pkt)를 호출하면 됩니다.
상수: 포트 DHCP_SERVER_PORT(67) / DHCP_CLIENT_PORT(68), DHCP_MAGIC_COOKIE(0x63825363); 메시지 타입 DHCP_DISCOVER(1), DHCP_OFFER(2), DHCP_REQUEST(3), DHCP_DECLINE(4), DHCP_ACK(5), DHCP_NAK(6), DHCP_RELEASE(7), DHCP_INFORM(8); 클라이언트 상태 DHCP_STATE_IDLE, DHCP_STATE_SELECTING, DHCP_STATE_REQUESTING, DHCP_STATE_BOUND.
종단 간 예제
udp_frame은 PacketBuf에 Ethernet + IPv4 + UDP 헤더를 만들고 페이로드가
들어갈 바이트 오프셋을 반환합니다(페이로드 영역을 포괄하도록 pkt.len도
미리 설정합니다) — 직접 그 위치에 페이로드 바이트를 기록한 다음
iface.tx(...)에 패킷을 넘깁니다. 실제 컴파일/실행으로 검증됨:
#include <std/net/net_core.h>
#include <std/net/ethernet.h>
#include <std/net/ipv4.h>
#include <std/net/udp.h>
extern int printf(const char* fmt, ...);
// A real driver would hand these bytes to hardware/a socket; this one just
// reports how many bytes it was asked to send.
struct LoopbackNic {
unsigned long frames;
int transmit([]const unsigned char frame); // NetDriver
};
int LoopbackNic::transmit([]const unsigned char frame) {
self.frames = self.frames + 1UL;
unsafe { printf("driver: sending %lu bytes\n", frame.len); }
return 0;
}
static struct LoopbackNic g_nic;
int main() {
struct NetIf iface = std::NetIf::new(&g_nic);
unsafe {
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);
unsigned char dst_mac[6];
unsafe {
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);
struct PacketBuf pkt;
unsigned long offset = std::udp_frame(&pkt, iface.mac, dst_mac,
src_ip, dst_ip, 1234, 5678, 5);
const &char payload = "hello";
unsigned long i = 0UL;
while (i < 5UL) {
match (pkt.at(offset + i)) {
case some(b): unsafe { *b = (unsigned char)payload[i]; }
case null: { }
}
i = i + 1UL;
}
int rc = iface.tx(&pkt); // transmit via the driver
unsafe { printf("tx rc=%d\n", rc); }
// driver: sending 47 bytes (14 eth + 20 ipv4 + 8 udp + 5 payload)
// tx rc=0
return 0;
}