クラシックプロトコル
UDP、TCP、Ethernet、IPv4 はインターネットの基盤となるプロトコルです。いずれも小さくて仕様が明確であり、wirespec の機能を端的に示す良い題材でもあります。wirespec を初めて使うなら、まずここから読むのがおすすめです。
UDP
User Datagram Protocol は最もシンプルな固定ヘッダを持ちます。4 つの 16 ビットフィールド、長さの制約、そしてヘッダ値から算出されるサイズの可変長ペイロードで構成されます。
module net.udp
@endian big
packet UdpDatagram {
src_port: u16,
dst_port: u16,
length: u16,
checksum: u16,
require length >= 8,
data: bytes[length: length - 8],
}使われている機能:
| 機能 | 該当箇所 |
|---|---|
packet 定義 | packet UdpDatagram { ... } |
| ビッグエンディアンのモジュールデフォルト | @endian big |
u16 スカラーフィールド | src_port, dst_port, length, checksum |
| 実行時バリデーション | require length >= 8 |
| 長さプレフィックス付きバイト列 | bytes[length: length - 8] |
| 式中の算術演算 | bytes spec 内の length - 8 |
require 節はパース時に評価され、条件を満たさなければ WIRESPEC_ERR_CONSTRAINT を返します。式 length - 8 は、length フィールドから固定ヘッダサイズ(8 バイト)を差し引いてペイロードのバイト数を求めます。
コンパイル:
wirespec compile examples/net/udp.wspec -o build/生成される C API:
wirespec_result_t net_udp_udp_datagram_parse(
const uint8_t *buf, size_t len,
net_udp_udp_datagram_t *out, size_t *consumed);
wirespec_result_t net_udp_udp_datagram_serialize(
const net_udp_udp_datagram_t *in,
uint8_t *buf, size_t cap, size_t *written);data フィールドは wirespec_bytes_t(ポインタ + 長さ)になり、元のバッファをゼロコピーで参照します。
TCP
TCP は UDP より複雑なヘッダ構造を持ちます。4 ビットのデータオフセット、8 個の個別フラグビット、可変長のオプション領域があり、wirespec の bits[N] 型と bit 型が活躍します。
module net.tcp
@endian big
packet TcpSegment {
src_port: u16,
dst_port: u16,
seq_num: u32,
ack_num: u32,
data_offset: bits[4],
reserved: bits[4],
cwr: bit,
ece: bit,
urg: bit,
ack: bit,
psh: bit,
rst: bit,
syn: bit,
fin: bit,
window: u16,
checksum: u16,
urgent_pointer: u16,
require data_offset >= 5,
options: bytes[length: data_offset * 4 - 20],
}使われている機能:
| 機能 | 該当箇所 |
|---|---|
bits[N] サブバイトフィールド | data_offset: bits[4], reserved: bits[4] |
bit(1 ビットフラグ) | cwr, ece, urg, ack, psh, rst, syn, fin |
| ビットグループの自動グループ化 | 8 個のフラグビットがワイヤ上で 1 バイトにパック |
u32 スカラー | seq_num, ack_num |
| 計算されたバイト長 | bytes[length: data_offset * 4 - 20] |
ビットグループの自動グループ化。 バイト境界に揃う連続した bits[N]/bit フィールドは、コンパイラが自動的にグループ化します。TCP の定義では、data_offset: bits[4] と reserved: bits[4] で 1 バイト、8 個のフラグで 1 バイトにまとまります。コンパイラはグループごとに 1 回の読み取りを生成し、シフト & マスクで個別フィールドを抽出します。手動のビット操作は不要です。
データオフセットフィールドはヘッダ長を 32 ビットワード単位でエンコードしており、最小値は 5(= 20 バイト)です。オプション領域は data_offset * 4 - 20 バイトを占め、オプションがなければゼロバイトになります。
コンパイル:
wirespec compile examples/net/tcp.wspec -o build/生成される C 構造体(抜粋):
typedef struct {
uint16_t src_port;
uint16_t dst_port;
uint32_t seq_num;
uint32_t ack_num;
uint8_t data_offset; /* bits[4] */
uint8_t reserved; /* bits[4] */
uint8_t cwr; /* bit */
uint8_t ece;
uint8_t urg;
uint8_t ack;
uint8_t psh;
uint8_t rst;
uint8_t syn;
uint8_t fin;
uint16_t window;
uint16_t checksum;
uint16_t urgent_pointer;
wirespec_bytes_t options;
} net_tcp_segment_t;Ethernet
Ethernet フレームは 6 バイト固定長の MAC アドレスフィールドが 2 つと、入力バッファの末尾まで広がるペイロードで構成されます。ゼロコピーの bytes[remaining] を使う好例です。
module net.ethernet
@endian big
packet EthernetFrame {
dst_mac: bytes[6],
src_mac: bytes[6],
ether_type: u16,
payload: bytes[remaining],
}使われている機能:
| 機能 | 該当箇所 |
|---|---|
| 固定長バイト列 | bytes[6] -- 指定バイト数をゼロコピーで参照 |
bytes[remaining] | payload がバッファの残り全体を消費 |
| シンプルなフラットパケット | 条件分岐や複雑な式は不要 |
bytes[remaining] はターミナルフィールドです。スコープ内の最後のワイヤフィールドでなければならず、コンパイラがこれを静的に検証します。後続にワイヤフィールドがあるとコンパイルエラーになります。
dst_mac、src_mac、payload はいずれも C では wirespec_bytes_t({ const uint8_t *ptr; size_t len; })にマップされ、元のバッファを直接指します。コピーは発生しません。
コンパイル:
wirespec compile examples/net/ethernet.wspec -o build/IPv4
IPv4 はさまざまなビット幅の bits[N] フィールドを使い、@checksum(internet) アノテーションで RFC 1071 チェックサムの自動検証・自動計算を実現します。
module ip.v4
@endian big
packet IPv4Header {
version: bits[4],
ihl: bits[4],
dscp: bits[6],
ecn: bits[2],
total_length: u16,
identification: u16,
flags: bits[3],
fragment_offset: bits[13],
ttl: u8,
protocol: u8,
@checksum(internet)
header_checksum: u16,
src_addr: u32,
dst_addr: u32,
}使われている機能:
| 機能 | 該当箇所 |
|---|---|
各種ビット幅の bits[N] | 4, 6, 2, 3, 13 ビットフィールド |
| ビットグループの自動グループ化 | version+ihl -> 1 バイト、dscp+ecn -> 1 バイト、flags+fragment_offset -> 2 バイト |
@checksum(internet) | header_checksum に RFC 1071 チェックサムを自動適用 |
@checksum(internet) の動作:
- パース時: ヘッダ全体の 1 の補数和を計算し、結果が 0xFFFF でなければ
WIRESPEC_ERR_CHECKSUMを返します。 - シリアライズ時: チェックサムフィールドをゼロクリアしてからヘッダ全体のチェックサムを計算し、結果を書き戻します。手動でのフィールド設定は不要です。
RFC 791 の規定に準拠しており、実際の IPv4 パケットで検証済みです。
コンパイル:
wirespec compile examples/ip/ipv4.wspec -o build/チェックサム検証付きパース:
ip_v4_ipv4_header_t hdr;
size_t consumed;
wirespec_result_t r = ip_v4_ipv4_header_parse(buf, len, &hdr, &consumed);
if (r == WIRESPEC_ERR_CHECKSUM) {
/* ヘッダチェックサム不正 -- パケット破棄 */
}自動チェックサム付きシリアライズ:
ip_v4_ipv4_header_t hdr = {
.version = 4,
.ihl = 5,
.protocol = 17, /* UDP */
/* ... 他のフィールド ... */
};
uint8_t buf[20];
size_t written;
ip_v4_ipv4_header_serialize(&hdr, buf, sizeof(buf), &written);
/* buf には正しいチェックサムが設定された有効な IPv4 ヘッダが格納される */