네트워킹 스택
SafeC는 std/net/에 완전한 임베디드 네트워킹 스택을 제공합니다. 모든 모듈은 힙 할당 없이 고정 크기 바이트 버퍼인 PacketBuf 위에서 직접 동작합니다.
#include "net/net.h" // 마스터 헤더: 아래의 모든 모듈을 가져온다계층 지도
┌─────────────────────────────────┐
│ 애플리케이션 (dns, dhcp) │
├─────────────────────────────────┤
│ 전송 (tcp, udp) │
├─────────────────────────────────┤
│ 네트워크 (ipv4, ipv6) │
├─────────────────────────────────┤
│ 링크 (ethernet, arp) │
├─────────────────────────────────┤
│ PacketBuf (원시 바이트 버퍼) │
└─────────────────────────────────┘net-core
#include "net/net_core.h"PacketBuf — 기본 패킷 컨테이너:
struct PacketBuf {
unsigned char data[NET_MTU]; // 1514 바이트
unsigned long len;
void* at(unsigned long off); // data 내 오프셋 위치를 가리키는 포인터
void reset(); // 버퍼를 0으로 채우고 len = 0으로 설정
}NetIf — 추상 네트워크 인터페이스 핸들입니다. 자체 rx/tx PacketBuf 필드를 갖지 않습니다 — 호출자가 자신의 PacketBuf를 소유하고 그것을 tx()에 명시적으로 전달합니다:
struct NetIf {
unsigned char mac[6]; // 하드웨어 MAC 주소
unsigned int ip4; // 할당된 IPv4, 네트워크 바이트 순서
unsigned int gateway; // 기본 게이트웨이 IPv4, 네트워크 바이트 순서
unsigned int netmask; // 서브넷 마스크, 네트워크 바이트 순서
void* tx_fn; // 드라이버 송신 콜백, 호출 전에 캐스트 --
// fn(void* iface_ctx, unsigned char* data, unsigned long len) -> int
void* iface_ctx; // 불투명 드라이버 컨텍스트, tx_fn의 첫 번째 인자로 전달됨
// 'pkt'를 전송한다. tx_fn을 캐스트한 뒤
// tx_fn(iface_ctx, pkt.data, pkt.len)로 호출한다. 성공 시 0, tx_fn이 설정되지 않았으면 -1을 반환한다.
int tx(&stack PacketBuf pkt);
}tx_fn은 타입이 지정된 함수 포인터가 아니라 void*입니다
값을 대입하는 것과 드라이버를 구현하는 것 둘 다 명시적 캐스트를 거칩니다 — 설정할 때는 iface.tx_fn = (void*)my_driver_send;(unsafe {} 안에서), tx() 자체의 구현은 호출 전에 이를 다시 fn int(void*, unsigned char*, unsigned long)로 캐스트합니다. 프레임을 직접 만들어 넣을 PacketBuf rx/PacketBuf tx 필드는 없습니다 — 아래의 예제가 하는 것처럼, udp_frame/eth_build/등을 통해 여러분 자신의 로컬 struct PacketBuf pkt에 프레임을 만들고 &pkt를 iface.tx(...)에 전달하세요.
바이트 순서 유틸리티:
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"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 // 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(&stack PacketBuf pkt, &stack EthernetHdr hdr_out);
// 오프셋 0에 Ethernet 헤더를 기록한다; pkt.len을 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은 최대 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(&stack 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(&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 // 최소 크기, 옵션 없음
#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, unsigned long len);
// pkt의 바이트 오프셋 'offset'에서 IPv4 헤더를 파싱한다.
int ipv4_parse(&stack PacketBuf pkt, unsigned long offset, &stack Ipv4Hdr hdr_out);
// 'offset'에 IPv4 헤더를 기록한다; 체크섬을 계산해 채운다.
// 첫 페이로드 바이트의 바이트 오프셋을 반환한다.
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 // 고정 헤더 길이
#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;
};
// 주소 유틸리티 -- 모두 '&stack Ipv6Addr' 참조로 주소를 받는다.
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바이트 buf
// pkt의 바이트 오프셋 'offset'에서 IPv6 헤더를 파싱한다.
int ipv6_parse(&stack PacketBuf pkt, unsigned long offset, &stack Ipv6Hdr hdr_out);
// 'offset'에 IPv6 헤더를 기록한다. 첫 페이로드 바이트의 바이트 오프셋을 반환한다.
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);
// 전체 프레임: Ethernet + IPv6 헤더, 호출자는 반환된 오프셋에 페이로드를
// 기록한다. 첫 페이로드 바이트의 바이트 오프셋을 반환한다.
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; // 호스트 바이트 순서
unsigned short dst_port; // 호스트 바이트 순서
unsigned short length; // UDP 데이터그램 전체 길이 (헤더 + 페이로드)
unsigned short checksum;
};
// pkt의 바이트 오프셋 'offset'에서 UDP 헤더를 파싱한다.
int udp_parse(&stack PacketBuf pkt, unsigned long offset, &stack UdpHdr hdr_out);
// 'offset'에 UDP 헤더를 기록한다; payload_len은 헤더 이후 데이터의
// 바이트 수다 (체크섬은 0으로 남겨둠 -- IPv4에서는 선택 사항). 첫 페이로드
// 바이트의 바이트 오프셋을 반환한다.
unsigned long udp_build(&stack 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(&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은 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(&stack PacketBuf pkt, unsigned long tcp_offset);
// 보낼 페이로드 데이터를 큐에 넣는다. 받아들여진 바이트 수를 반환한다 (가득 찼으면 0).
unsigned long send(const unsigned char* data, unsigned long len);
// 다음 송신 세그먼트를 pkt에 만든다; 세그먼트가 만들어졌으면 1을 반환한다.
int build_segment(&stack 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, unsigned long len);
// 능동 오픈을 시작한다 (SYN 전송).
void connect(unsigned int remote_ip, unsigned short remote_port,
unsigned int local_ip, unsigned short local_port,
unsigned int isn);
void close(); // FIN 전송
}
// pkt의 바이트 오프셋 'offset'에서 TCP 헤더를 파싱한다.
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
// 'name'에 대한 DNS A 레코드 질의를 pkt에 만든다. 'txid'는 (내부에서
// 생성되는 것이 아니라) 호출자가 직접 선택한다 -- 응답과 직접 대조해서 확인하라.
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);
// pkt(UDP 페이로드 오프셋에서 시작)의 DNS 응답을 파싱한다. 성공하면 첫
// A 레코드로 ip4_out을 채우고 1을 반환한다; 파싱 오류, A 레코드 없음,
// 또는 expected_txid와 일치하지 않는 txid인 경우 0을 반환한다.
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 클라이언트 상태 기계
#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(&stack PacketBuf pkt, const unsigned char eth_dst[6]);
// 'server_ip'로부터 'offered_ip'에 대한 DHCPREQUEST를 만든다.
void request(&stack 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(&stack PacketBuf pkt, unsigned long udp_payload_offset);
int is_bound() const; // 클라이언트가 유효한 임대를 갖고 있으면 1
}NetIf* iface 필드도, 인자 없는 discover()/request()도 없음
DhcpClient는 NetIf에 대한 참조를 갖고 있지 않습니다 — TcpConn처럼, 호출자가 제공한 &stack PacketBuf에 프레임을 만들며, discover()/ request()는 저장된 인터페이스에서 읽어오는 대신 대상 MAC(그리고 request()의 경우 제안받은 IP/서버 IP)을 명시적인 인자로 받습니다. 종단 간 예제와 같은 방식으로 패킷을 전송하세요 — 패킷을 만든 다음 직접 iface.tx(&pkt)를 호출하면 됩니다.
종단 간 예제
udp_frame은 PacketBuf에 Ethernet + IPv4 + UDP 헤더를 만들고 페이로드가 들어갈 바이트 오프셋을 반환합니다(페이로드 영역을 포괄하도록 pkt.len도 미리 설정합니다) — 직접 그 위치에 페이로드 바이트를 기록한 다음 iface.tx(...)에 패킷을 넘기세요. 실제 컴파일/실행으로 검증됨:
#include <std/net/net_core.sc>
#include <std/net/ethernet.sc>
#include <std/net/ipv4.sc>
#include <std/net/udp.sc>
// 실제 드라이버라면 이 바이트들을 하드웨어나 소켓에 넘기겠지만, 여기서는
// 몇 바이트를 보내라고 요청받았는지만 알려준다.
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; // 여기서는 드라이버 컨텍스트가 필요 없음
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); // 드라이버를 통해 전송
printf("tx rc=%d\n", rc);
// driver: sending 47 bytes (eth 14 + ipv4 20 + udp 8 + payload 5)
// tx rc=0
return 0;
}