moon-boofuzz

    Deterministic protocol fuzzing core with native TCP/UDP execution and portable JSONL replay

    fuzzing
    protocol
    boofuzz
    testing
    Download zip
    Author
    Version
    0.1.2
    License
    GPL-2.0-only
    Last updated
    14 hours ago
    Downloads
    8

    #moon-boofuzz

    MoonBit 协议模糊测试核心:定义协议、生成单字段变异、执行前置会话、通过 TCP/UDP 发送、记录结果并重放。

    采用 boofuzz 固定版本 518c13904fc32e7f2cc88c9dec934e509062953e 的明确功能子集。核心支持 Wasm 与 Native,网络和文件 I/O 支持 Windows/Linux Native。已发布到 mooncakes.io(页面显示最新版本)。

    #开始使用

    安装 MoonBit。CI 的 Windows 作业使用 MSVC 编译 C 桥,Linux 使用 GCC;本地推荐 llvm-mingw,也支持 Visual Studio MSVC。当前验证工具链为 moon 0.1.20260920、moonc v0.10.14。

    作为库使用:在自己的模块中执行 moon add GuoXBQ-Q/moon-boofuzz,然后在包的 moon.pkg 里 import "GuoXBQ-Q/moon-boofuzz"(纯逻辑根包在 Wasm 下即可使用,无需 C 工具链;网络与文件 I/O 子包仅支持 Native)。

    #库 API 快速示例

    以下示例由 moon test 执行。根包测试使用 @moon_boofuzz 别名;在自己的项目中,moon add GuoXBQ-Q/moon-boofuzz 后按所用别名导入即可。

    #命名请求与惰性变异

    CompiledRequest 用于新协议模型。正常渲染和变异枚举分别调用 render() 与 cases();用 next() 逐个获取载荷,避免先建立完整用例数组。

    ///|
    test "named request quick start" {
    let request = @moon_boofuzz.CompiledRequest::compile(
    @moon_boofuzz.Block::new("packet", [
    Leaf("prefix", @moon_boofuzz.Field::simple(b"PING ", [])),
    Leaf("value", @moon_boofuzz.Field::simple(b"ok", [b"", b"\x00\xff"])),
    ]),
    )
    assert_eq(request.render(), b"PING ok")
    let cases = request.cases(limit=1)
    assert_eq(cases.next().map(case => case.payload), Some(b"PING "))
    assert_eq(cases.next(), None)
    assert_eq(cases.state(), Limited)
    let resumed = request.cases(start=cases.position())
    assert_eq(resumed.next().map(case => case.payload), Some(b"PING \x00\xff"))
    }

    #Simple and Group

    Field::simple(default, candidates) preserves every explicit candidate. Field::group(values, default_value=...) selects the first value by default, then removes only the first matching default from mutation candidates. Both snapshot input arrays; fuzzable=false yields zero cases. Indices are zero based and out-of-range access returns None. Use these fields as named Leaf nodes in CompiledRequest; the legacy flat Request remains available.

    ///|
    test "group field" {
    let field = @moon_boofuzz.Field::group([b"GET", b"POST", b"GET"])
    assert_eq(field.default_value(), b"GET")
    assert_eq(field.num_mutations(), 2)
    assert_eq(field.mutation(0), Some(b"POST"))
    assert_eq(field.mutation(1), Some(b"GET"))
    }

    ///|
    test "minimal byte request" {
    let request = @moon_boofuzz.Request::new([
    @moon_boofuzz.Static(b"PING "),
    @moon_boofuzz.Choice(b"ok", [b"", b"long"]),
    ])
    assert_eq(request.render(), b"PING ok")
    assert_eq(request.mutations(), [b"PING ", b"PING long"])
    }

    字段路径包含请求名,例如 packet.value。会话图、网络执行及回调的使用分别见 docs/SESSION.md、docs/RUNNER.md 和 docs/MONITORS.md。

    从源码运行完整 CLI:

    git clone https://github.com/GuoXBQ-Q/moon-boofuzz.git cd moon-boofuzz moon update moon check --deny-warn moon build moon test --deny-warn moon build --target native moon test --target native --deny-warn moon run --target native cmd/boofuzz -- generate examples/offline.json --limit 3

    全部自动验收场景使用临时文件、回环地址和临时端口,无需另启服务:

    moon test --target native --deny-warn -p cmd/boofuzz

    Windows 用户可使用 llvm-mingw(把 bin 加入 PATH)或 Visual Studio MSVC;CI 的 Windows 作业由 MoonBit 默认选择 MSVC。仅安装 MoonBit 可以运行纯核心 Wasm 检查;网络 CLI 还需要 C 工具链。运行 moon update 是为独立开发脚本初始化包索引,正常使用 CLI 不需要 Python。

    第一次体验建议先执行上面的自动场景测试,再运行离线 generate,最后连接自己的服务。

    #CLI 流程

    命令输入与用途主要选项
    generateJSON 协议定义 → 变异载荷 JSONL,不连接目标--limit N、--start N、--id ID(需 --combinatorial false)、--combinatorial true|false、--max-depth N(组合深度上限);组合爆破默认开启(与上游 CLI 一致)
    runJSON 协议定义 → 逐例执行并保存实际流量必填 --output FILE;可选 --limit N(缺省无上限,跑完为止)、--combinatorial true|false、--max-depth N、--start N、--end N、--sleep-between-ms N(用例间隔)、--text-dump true|false(逐例实时日志)、--db FILE(缺省自动写 boofuzz-results/run-<UTC时间戳>.db,与上游一致常开)、--record-passes N、--csv-out FILE、--web-port N(缺省 26000,与上游一致常开;0 为随机空闲端口)、--target-cmd CMD
    open结果库/JSONL → 本地只读 Web 视图--ui-port N(默认 26000)
    convertWeb 页面:粘贴原始 HTTP 报文 → 勾选分段并选择变异原语(字符串库/整数/二进制/随机/候选值)→ 自动生成协议定义 JSON(校验、用例数、载荷预览、复制/下载)--ui-port N(默认 26001)
    reportJSONL 执行记录 → 分类计数、失败身份和行号--max-bytes N
    replayJSONL 执行记录 → 按身份重放保存的字节必填 --id ID,可选成对的 --host HOST --port PORT、--max-bytes N

    查看帮助:moon run --target native cmd/boofuzz -- --help。所有命令均从项目根目录执行。

    examples/tcp.json、udp.json 和 stateful.json 默认指向 127.0.0.1:9000。前两者需要兼容的测试目标;stateful.json 可配合仓库自带的 cmd/stateful_target 直接运行,详见 多报文手工测试。

    moon run --target native cmd/boofuzz -- run examples/tcp.json --output _build/tcp-cases.jsonl moon run --target native cmd/boofuzz -- report _build/tcp-cases.jsonl moon run --target native cmd/boofuzz -- replay _build/tcp-cases.jsonl --id '["packet"]/v1:packet.data:0'

    与上游一致,run 每次都会在 boofuzz-results/ 下生成一份 SQLite 结果库(run-<UTC时间戳>.db),并在默认端口 26000 启动实时 Web UI(端口被占时自动顺延);进程在输出摘要后退出,不等待交互。组合爆破默认开启,按 id 续跑需显式 --combinatorial false。

    从 report 复制实际 case_id。重放可用 --host HOST --port PORT 显式覆盖目标,始终发送记录中的字节,不重新生成变异。响应无需与原记录完全相同。

    示例的具体含义:

    • offline.json:保留 PING 前缀,依次生成空值、00ff 二进制值和 long。payload_hex 是完整请求,prefix_hex 是会话前置请求。
    • tcp.json:向 127.0.0.1:9000 发送 00ff、414141 两个用例,每例等待 2 字节响应,接收超时 100 ms。目标不回复时会记录超时。
    • udp.json:发送空报文及 00ff,每例接收一个 UDP 报文。
    • stateful.json:每个新连接重新发送 HELLO、AUTH test,再发送变异 DATA;只变异末端 query,每步等待响应。可直接运行 多报文手工测试 中的本地靶子和命令。
    • http.json:变异 HTTP 请求行(URI)与主体,可指向任意 HTTP 服务,或配合 自带 httpd 靶子。
    • httpd.json、httpd_lab.json:面向自带 httpd 靶子的动词组变异与实验室场景(拒绝状态码、崩溃注入),见 HTTPD.md。
    • http_get_full.json、http_post_full.json:头部丰富的完整 GET/POST 请求,变异点覆盖方法(GET/HEAD、POST/PUT)、URI 显式候选和字符串库头部。两者各有等价的 MoonBit API 版本 examples/http_get_full、examples/http_post_full,支持离线生成与在线执行两种模式。

    value_hex 定义正常值,values_hex 定义显式变异候选;默认值用于普通渲染和前置请求,不会自动额外插入变异序列。Group 会从候选中只移除一次默认值。自动变异可使用 integer、bytes 或 text 字段。

    运行前应按协议配置响应边界:TCP 用 none、fixed 或 until,UDP 用 none 或 datagram。完整 JSON 写法见 协议定义说明。

    generate 输出 generated_case JSONL 及生成汇总;run 逐例写记录;report 按结果分类并给出失败行号和身份。每次运行使用新日志文件,避免追加相同身份后产生歧义。report 遇到损坏尾行仍输出之前的完整记录汇总,并以状态码 2 退出。replay 拒绝损坏文件;重放结果失败返回 1,配置或文件错误返回 2。

    limited 表示达到配置的数量上限,并不表示一定还有未生成的用例。run 成功写完记录时返回 0,即使其中存在超时或其他失败用例;判断目标结果应查看 report,不能只看 run 的退出码。生成结果与执行记录是两种不同格式,replay 的输入应来自 run --output。

    #文档导航

    需求文档
    编写 JSON 协议、字段与读取策略DEFINITIONS.md
    用 Web 页面把 HTTP 报文转成定义CONVERT.md
    运行本地 HTTP fuzz 靶子(httpd)HTTPD.md
    用 MoonBit 代码编写 fuzz 脚本原生 MoonBit 完整示例、CODE.md、可执行 API 示例、MODEL.md
    配置前置路径和执行器SESSION.md、RUNNER.md
    响应检查、故障通知和恢复回调MONITORS.md
    理解日志、重放和退出码RECORDS.md、REPLAY.md、REPORT.md
    核对验收、兼容边界与许可ACCEPTANCE.md、UPSTREAM.md

    #支持的核心

    • Simple、Group、8/16/32/64 位整数、二进制 Bytes、UTF-8 字符串和分隔符变异。
    • HTTP 报文转换器:粘贴原始请求,勾选要 fuzz 的分段并为每段选择变异原语(字符串库/整数/二进制/随机/显式候选),自动生成并校验协议定义 JSON(见 CONVERT.md)。
    • 命名嵌套块、条件块、重复、对齐、长度字段(二进制或 ascii 十进制渲染,后者对应上游 Content-Length 模式)及 CRC32。
    • 惰性单字段枚举、稳定身份、起始位置、数量限制与停止状态。
    • DAG 会话路径;每例重新连接并执行默认前置请求,仅变异末端目标。
    • TCP 完整发送与无响应/固定长度/分隔符读取;UDP 保留报文边界和空报文。
    • 生命周期回调、响应检查、故障通知与目标恢复;监视器检测的目标崩溃(如 --target-cmd 进程监视器)以 MonitorFailed 计入失败并触发恢复,普通回调异常只记录不计数;拨号失败默认无限重试(阈值/超时可配,放弃即停)。
    • 版本化 JSON 定义、JSONL 记录、按保存字节重放及分类报告。

    旧 Static、Choice 和平面 Request 行为保留。Choice 是原项目显式候选 API,不冒充上游 Group。新 API 示例见 README.mbt.md,JSON 格式见 DEFINITIONS.md。

    #边界

    默认单请求 1 MiB、接收 64 KiB;用例数不设默认上限(与上游一致,跑到用例全部耗尽为止,--limit N 或 case_limit 可显式设限,达到上限返回 limited 状态)。直接渲染超限返回明确错误;变异流跳过超长候选并保留其原始序号,不截断载荷。动态变异在分配前检查长度。

    连接、发送和接收超时默认各 5 秒。系统主机名解析发生在套接字连接计时前;需要严格连接总时限时使用 IPv4。网络异常只表示传输/响应故障,不直接判定目标崩溃;目标崩溃由监视器存活检测判定并以 MonitorFailed 计入失败。

    已实现的补充能力:IPv6 双栈、File 传输、CSV 导出、SQLite 结果库(与上游一致常开,自动落 boofuzz-results/run-<UTC时间戳>.db)、--record-passes 写入节流、Web UI(默认 26000 常开)与 open 子命令、UDP 服务端模式与广播。

    仍不包含:Python s_* DSL、pedrpc 远程监视器、调试器与崩溃地址分析、curses TUI、TLS、串口、Unix 域 socket、Raw L2/L3 原始帧、多播、覆盖率引导、并行执行、TCP 服务端模式、restart_interval 周期性重启。String 支持动态 UTF-8 子集,不暴露上游按字符截断的 size/max_len;Bytes 填充限单字节。详细兼容边界与评估结论见 UPSTREAM.md、PLAN.md。

    #验证与发布准备

    GitHub Actions 覆盖 Windows(MSVC)、Linux(GCC + ASan)与 Wasm/Native。moon run scripts/verify.mbtx 执行本地完整检查;GCC/Clang 下可运行 moon run scripts/asan.mbtx。

    moon package --list 审查源码包内容,moon package 生成待发布源码包。发布前清单见 ACCEPTANCE.md。报名申报书仍由本人撰写,本项目不代填或提交。

    #许可与来源

    本项目保持 GPL-2.0-only,见 LICENSE。移植来源、差分样本生成方式、已知差异及工具链许可注意事项见 UPSTREAM.md。本项目不是 boofuzz 官方版本。

    ModelError

    pub suberror ModelError {
    Invalid(String)
    Limit(String)
    } derive(
    Debug
    )

    Block

    pub struct Block {
    name : String
    children : Array[Node]
    condition : Condition?
    alignment : (Int, Bytes)?
    group : String?
    }

    Block::aligned

    fn Block::aligned(self : Block, modulus : Int, pattern? : Bytes) -> Block raise ModelError

    Upstream intentionally appends a full modulus even when already aligned.

    Block::new

    fn Block::new(name : String, children : Array[Node]) -> Block

    Block::when

    fn Block::when(self : Block, condition : Condition) -> Block

    Block::with_group

    fn Block::with_group(self : Block, target : String) -> Block

    Link the block to a Group field; its candidates multiply with the block's own mutation cases like upstream Block(group=...) (boofuzz/blocks/block.py mutations, 518c139). The target is a full field path.

    CaseSeq

    pub enum CaseSeq {
    Plain(Int)
    Sequence(Array[CaseSeq])
    Product(Int, CaseSeq)
    }

    Lazy case sequence tree. Upstream enumerates a block as its plain child mutations followed by group products replaying that same sequence once per group candidate (boofuzz/blocks/block.py mutations, 518c139).

    CaseStream

    pub struct CaseStream {
    request : CompiledRequest
    limit : Int
    skip : Int
    ordinal : Int
    emitted : Int
    state : EnumerationState
    stack : Array[StreamFrame]
    combinatorial : Bool
    units : Array[StreamUnit]
    max_depth : Int?
    depth : Int
    depth_emitted : Int
    frames : Array[CombFrame]
    vars : Map[String, Bytes]?
    }

    CaseStream::next

    fn CaseStream::next(self : CaseStream) -> MutationCase? raise ModelError

    CaseStream::position

    fn CaseStream::position(self : CaseStream) -> Int

    The next raw candidate position, including oversized skipped candidates.

    CaseStream::remaining_skip

    fn CaseStream::remaining_skip(self : CaseStream) -> Int

    Cases not yet consumed from the start offset; nonzero after a stream exhausts before reaching it.

    CaseStream::state

    CaseStream::stop

    fn CaseStream::stop(self : CaseStream) -> Unit

    ChecksumAlgorithm

    pub(all) enum ChecksumAlgorithm {
    Crc32
    Crc32c
    Adler32
    Md5
    Sha1
    } derive(Eq,
    Debug
    )

    Checksum algorithms supported by checksum nodes. MD5/SHA-1 render with upstream's 32-bit word swap when the node endianness is big (boofuzz/blocks/checksum.py:171-189, 518c139).

    ChecksumAlgorithm::equal

    ChecksumAlgorithm::not_equal

    fn ChecksumAlgorithm::not_equal(x : ChecksumAlgorithm, y : ChecksumAlgorithm) -> Bool

    ChecksumSpec

    pub struct ChecksumSpec {
    target : String
    endian : Endian
    mutations : Array[Bytes]
    algorithm : ChecksumAlgorithm
    }

    CombFrame

    pub struct CombFrame {
    unit_index : Int
    incoming : Map[Int, Bool]
    acc : Map[Int, Bool]
    parts : Array[(Int, Int, Bool)]
    }

    CompiledRequest

    pub struct CompiledRequest {
    name : String
    entries : Array[Entry]
    paths : Map[String, Int]
    max_bytes : Int
    seq : CaseSeq
    }

    CompiledRequest::cases

    fn CompiledRequest::cases(self : CompiledRequest, start? : Int, limit? : Int, vars? : Map[String, Bytes]?) -> CaseStream raise ModelError

    CompiledRequest::cases_with_variables

    fn CompiledRequest::cases_with_variables(self : CompiledRequest, vars : Map[String, Bytes]?, start? : Int, limit? : Int) -> CaseStream raise ModelError

    Enumerate combinatorial cases: depth 1..max_depth (indefinite when max_depth is absent, stopping at the first depth without a valid case), combining that many units of the base sequence, deduplicated exactly like upstream. Group products take part as whole units.

    CompiledRequest::combinatorial_cases

    fn CompiledRequest::combinatorial_cases(self : CompiledRequest, start? : Int, limit? : Int, max_depth? : Int) -> CaseStream raise ModelError

    Enumerate combinatorial cases: depth 1..max_depth (indefinite when max_depth is absent, stopping at the first depth without a valid case), combining that many units of the base sequence, deduplicated exactly like upstream. Group products take part as whole units.

    CompiledRequest::combinatorial_cases_with_variables

    fn CompiledRequest::combinatorial_cases_with_variables(self : CompiledRequest, vars : Map[String, Bytes]?, start? : Int, limit? : Int, max_depth? : Int) -> CaseStream raise ModelError

    CompiledRequest::compile

    fn CompiledRequest::compile(root : Block, max_bytes? : Int) -> CompiledRequest raise ModelError

    Validate and snapshot a named protocol tree. Limits count encoded wire bytes.

    CompiledRequest::count_at

    fn CompiledRequest::count_at(self : CompiledRequest, path : String) -> Int raise ModelError

    Number of sequential cases contributed by the entry at path (the entry's own candidate count; group product replays are excluded).

    CompiledRequest::from_request

    fn CompiledRequest::from_request(name : String, request : Request, max_bytes? : Int) -> CompiledRequest raise ModelError

    Legacy flat fields receive stable names field0, field1, ... under name.

    CompiledRequest::name

    fn CompiledRequest::name(self : CompiledRequest) -> String

    CompiledRequest::parent_path

    fn CompiledRequest::parent_path(self : CompiledRequest, path : String) -> String? raise ModelError

    CompiledRequest::paths

    fn CompiledRequest::paths(self : CompiledRequest) -> Array[String]

    CompiledRequest::raw_mutation_count

    fn CompiledRequest::raw_mutation_count(self : CompiledRequest) -> Int64

    Case positions of the sequential stream, including positions inside group product replays.

    CompiledRequest::raw_prefix_before

    fn CompiledRequest::raw_prefix_before(self : CompiledRequest, path : String) -> Int64 raise ModelError

    Raw sequential case count emitted before the entry at path renders its first plain case, walking the same tree the stream enumerates: unlike count_at this includes group product replays of preceding grouped blocks, so the result is a real stream position. Returns -1 when the entry emits no plain cases at all (disabled or non-mutating entry).

    CompiledRequest::render

    fn CompiledRequest::render(self : CompiledRequest) -> Bytes raise ModelError

    CompiledRequest::render_case

    fn CompiledRequest::render_case(self : CompiledRequest, parts : Array[(String, Int)], vars : Map[String, Bytes]?) -> Bytes raise ModelError

    Render a mutation case (parts from MutationCase::parts) with session variables, so edge-callback variable writes are visible at send time.

    CompiledRequest::render_path

    fn CompiledRequest::render_path(self : CompiledRequest, path : String) -> Bytes raise ModelError

    CompiledRequest::render_with

    fn CompiledRequest::render_with(self : CompiledRequest, vars : Map[String, Bytes]?) -> Bytes raise ModelError

    Render with session variables; inside a running case dynamic fields resolve strictly, matching upstream node.render(mutation_context).

    Condition

    pub(all) enum Condition {
    Equal(String, Bytes)
    NotEqual(String, Bytes)
    OneOf(String, Array[Bytes])
    NotOneOf(String, Array[Bytes])
    Greater(String, Bytes)
    GreaterEqual(String, Bytes)
    Less(String, Bytes)
    LessEqual(String, Bytes)
    }

    Block visibility conditions, following boofuzz/blocks/block.py _do_dependencies_allow_render at 518c13904fc32e7f2cc88c9dec934e509062953e. The ordering variants keep upstream's operand order: upstream evaluates dep_value OP dependent_value (the configured threshold on the left), so Greater renders when the current field value is LESS than the configured bytes, not greater. Preserved for definition parity.

    Endian

    pub(all) enum Endian {
    Little
    Big
    } derive(Eq,
    Debug
    )

    Endian::equal

    fn Endian::equal(Endian, Endian) -> Bool

    Endian::not_equal

    fn Endian::not_equal(x : Endian, y : Endian) -> Bool

    Endian::to_repr

    Entry

    type Entry

    EnumerationState

    pub(all) enum EnumerationState {
    Running
    Exhausted
    Limited
    Stopped
    } derive(Eq,
    Debug
    )

    EnumerationState::equal

    EnumerationState::not_equal

    fn EnumerationState::not_equal(x : EnumerationState, y : EnumerationState) -> Bool

    Field

    pub struct Field {
    value : Bytes
    count : Int
    candidate : (Int) -> Bytes
    candidate_length : (Int) -> Int64
    }

    An immutable field definition with indexed, on-demand mutation generation.

    Field::binary

    fn Field::binary(value : Bytes, size? : Int, max_len? : Int, padding? : Bytes, fuzzable? : Bool, fuzz_values? : Array[Bytes]) -> Field raise ModelError

    Binary mutation candidates are materialized individually on indexed access. Size/max_len adjust mutations only, matching upstream; default stays verbatim. Padding is restricted to exactly one byte.

    Field::default_value

    fn Field::default_value(self : Field) -> Bytes

    Field::delimiter

    fn Field::delimiter(value : String, fuzzable? : Bool, fuzz_values? : Array[Bytes]) -> Field

    Field::float

    fn Field::float(default_value? : Double, s_format? : String, f_min? : Double, f_max? : Double, max_mutations? : Int, seed? : Int64, encode_as_ieee_754? : Bool, endian? : Endian, fuzzable? : Bool, fuzz_values? : Array[Bytes]) -> Field raise ModelError

    Float candidates: the first mutation is the default value formatted with s_format, the rest draw random.uniform(f_min, f_max) from the seeded MT19937 stream; adjacent duplicates are dropped exactly like upstream, where the uniform draw is consumed before the dedup check so the remaining sequence matches upstream's generator. The candidate count is the number of formatted values actually produced, whereas upstream num_mutations keeps claiming max_mutations.

    Field::from_lines

    fn Field::from_lines(default_value? : Bytes, lines? : Array[Bytes], max_len? : Int, fuzzable? : Bool, fuzz_values? : Array[Bytes]) -> Field raise ModelError

    Cycle through bad values, one per provided line. Empty lines are dropped like upstream's filter(None, ...); max_len > 0 keeps only shorter lines. Upstream collapses the filtered library through a Python set ONLY when at least one line exceeds max_len (from_file.py:43-47), so the port applies dedup under the same condition — keeping first-occurrence order where the set would scramble it (documented deviation).

    Field::group

    fn Field::group(values : Array[Bytes], default_value? : Bytes, fuzzable? : Bool, fuzz_values? : Array[Bytes]) -> Field raise ModelError

    Group removes only the first occurrence of its default from candidates.

    Field::integer

    fn Field::integer(value : UInt64, width? : Int, endian? : Endian, fuzzable? : Bool, fuzz_values? : Array[Bytes]) -> Field raise ModelError

    Unsigned binary integers with upstream's sorted, deduplicated +/-10 boundaries. Width is in bits. ASCII, full_range and custom max_num are not supported.

    Field::mutation

    fn Field::mutation(self : Field, index : Int) -> Bytes?

    Field::num_mutations

    fn Field::num_mutations(self : Field) -> Int

    Field::random_data

    fn Field::random_data(default_value? : Bytes, min_length? : Int, max_length? : Int, max_mutations? : Int, step? : Int, fuzzable? : Bool, fuzz_values? : Array[Bytes]) -> Field raise ModelError

    Random data candidates from upstream's deterministic Random(0) stream. Step mode fixes each length at min_length + i*step without consuming the generator; otherwise each candidate draws one randint for its length and one randint(0, 255) per byte. Candidate identity is stable, so the upstream count quirk (mutations loop over get_num_mutations, which also counts fuzz_values) is intentionally not reproduced: the random sequence has exactly max_mutations entries and user fuzz_values are appended after.

    Field::simple

    fn Field::simple(value : Bytes, values : Array[Bytes], fuzzable? : Bool, fuzz_values? : Array[Bytes]) -> Field

    Field::text

    fn Field::text(value : String, fuzzable? : Bool, fuzz_values? : Array[Bytes]) -> Field

    UTF-8 bad-string library, variable repeats and deterministic long strings. Only dynamic size is supported. Enforce wire-byte limits at request rendering.

    MutationCase

    pub(all) struct MutationCase {
    id : String
    request_name : String
    field_path : String
    mutation_index : Int
    ordinal : Int
    payload : Bytes
    extra : Array[(String, Int)]
    } derive(Eq,
    Debug
    )

    MutationCase::equal

    MutationCase::not_equal

    fn MutationCase::not_equal(x : MutationCase, y : MutationCase) -> Bool

    MutationCase::parts

    fn MutationCase::parts(self : MutationCase) -> Array[(String, Int)]

    All mutated (path, mutation index) pairs of this case, outermost first.

    Node

    pub(all) enum Node {
    Leaf(String, Field)
    Nested(Block)
    Repeat(String, String, Int, Int, Int, String?)
    Sized(String, SizeSpec)
    Checksummed(String, ChecksumSpec)
    Mirrored(String, String)
    Dynamic(String, String, Field)
    }

    Named protocol items. Names are one path segment and cannot contain dots.

    Node::checksum

    fn Node::checksum(name : String, target : String, endian? : Endian, mutations? : Array[UInt], fuzzable? : Bool, algorithm? : ChecksumAlgorithm) -> Node raise ModelError

    Node::dynamic

    fn Node::dynamic(name : String, variable : String, field : Field) -> Node

    Bind a field's default value to a session variable. Inside a test case the variable must exist (upstream raises on a missing ProtocolSession variable); outside, the field's own value renders. Mutations derive from the fallback value, matching upstream's mutation seeding.

    Node::mirror

    fn Node::mirror(name : String, target : String) -> Node

    Node::repeat

    fn Node::repeat(name : String, target : String, min? : Int, max? : Int, step? : Int, variable? : String) -> Node

    Repeat appends copies of a preceding named block; normal output is empty.

    Node::size

    fn Node::size(name : String, target : String, length? : Int, endian? : Endian, offset? : Int, inclusive? : Bool, ascii? : Bool, mutations? : Array[UInt64], fuzzable? : Bool) -> Node raise ModelError

    Primitive

    pub(all) enum Primitive {
    Static(Bytes)
    Choice(Bytes, Array[Bytes])
    } derive(
    Debug
    )

    Explicit byte fields. Automatic boundary mutations are planned.

    Primitive::default_value

    fn Primitive::default_value(self : Primitive) -> Bytes

    Return the normal wire value.

    Primitive::mutations

    fn Primitive::mutations(self : Primitive) -> Array[Bytes]

    Explicit alternatives in caller order; duplicates are preserved.

    Request

    pub struct Request {
    fields : Array[Primitive]
    }

    A flat request. Nested blocks and computed fields are planned.

    Request::mutations

    fn Request::mutations(self : Request) -> Array[Bytes]

    Generate one-field-at-a-time cases in field and alternative order. Materializes all cases; intended for small explicit mutation sets.

    Request::new

    fn Request::new(fields : Array[Primitive]) -> Request

    Snapshot caller-owned field definitions.

    Request::render

    fn Request::render(self : Request) -> Bytes

    Render the normal request without text decoding.

    SessionGraph

    pub struct SessionGraph {
    requests : Array[CompiledRequest]
    edges : Array[Array[Int]]
    names : Map[String, Int]
    edge_callbacks : Map[(Int, Int), (StepContext) -> Bytes?]
    }

    SessionGraph::add

    fn SessionGraph::add(self : SessionGraph, request : CompiledRequest) -> Unit raise ModelError

    SessionGraph::connect

    fn SessionGraph::connect(self : SessionGraph, from : String, to : String, callback? : (StepContext) -> Bytes?) -> Unit raise ModelError

    SessionGraph::new

    SessionGraph::paths

    fn SessionGraph::paths(self : SessionGraph, targets? : Array[String], max_paths? : Int) -> Array[SessionPath] raise ModelError

    Roots follow node insertion order; outgoing edges follow connection order.

    SessionPath

    pub struct SessionPath {
    requests : Array[CompiledRequest]
    transitions : Array[(StepContext) -> Bytes??]
    }

    SessionPath::cases

    fn SessionPath::cases(self : SessionPath, start? : Int, limit? : Int, variables? : Map[String, Bytes]?) -> CaseStream raise ModelError

    Only the terminal request mutates; prefixes are rendered from their defaults.

    SessionPath::cases_with_variables

    fn SessionPath::cases_with_variables(self : SessionPath, vars : Map[String, Bytes]?, start? : Int, limit? : Int) -> CaseStream raise ModelError

    SessionPath::combinatorial_cases_with_variables

    fn SessionPath::combinatorial_cases_with_variables(self : SessionPath, vars : Map[String, Bytes]?, start? : Int, limit? : Int, max_depth? : Int) -> CaseStream raise ModelError

    SessionPath::names

    fn SessionPath::names(self : SessionPath) -> Array[String]

    SessionPath::prefix

    fn SessionPath::prefix(self : SessionPath) -> Array[Bytes] raise ModelError

    SessionPath::requests

    fn SessionPath::requests(self : SessionPath) -> Array[CompiledRequest]

    SessionPath::target

    SizeSpec

    pub struct SizeSpec {
    target : String
    length : Int
    endian : Endian
    offset : Int
    inclusive : Bool
    ascii : Bool
    mutations : Array[Bytes]
    }

    StepContext

    pub(all) struct StepContext {
    variables : Map[String, Bytes]
    received : Array[Bytes]
    request : String
    }

    Context handed to an edge callback: the mutable session-variable map for the running case, bytes received from earlier steps, and the destination request name. Mirrors upstream's ProtocolSession (protocol_session.py, 518c139); callbacks write variables and may return replacement data.

    StreamFrame

    pub struct StreamFrame {
    seq : CaseSeq
    child : Int
    pass : Int
    walking : Bool
    }

    One stack frame of the lazy case-sequence walker. Sequence frames track the next child, plain frames the next mutation index, product frames the current group pass.

    StreamUnit

    pub struct StreamUnit {
    parts : Array[(Int, Int, Bool)]
    child : Int
    }

    A pre-enumerated unit of the base case sequence for combinatorial enumeration. is_group marks parts contributed by a product's group, which upstream adds without consulting the skip set. child is the availability-checked leaf entry.

    TextCandidate

    type TextCandidate derive(Eq)

    TextCandidate::equal

    TextCandidate::not_equal

    fn TextCandidate::not_equal(x : TextCandidate, y : TextCandidate) -> Bool

    adler32

    fn adler32(bytes : Bytes) -> UInt

    Adler-32 (RFC 1950): two 16-bit sums modulo 65521, matching zlib.

    crc32

    fn crc32(bytes : Bytes) -> UInt

    Reflected IEEE CRC32, initial/final XOR 0xffffffff, polynomial 0xedb88320.

    crc32c

    fn crc32c(bytes : Bytes) -> UInt

    CRC-32C (Castagnoli, poly 0x82F63B78 reflected), initial/final XOR 0xffffffff — the algorithm behind upstream's optional crc32c dependency.

    ipv4_checksum

    fn ipv4_checksum(header : Bytes) -> UInt

    IPv4 header checksum over the header bytes with the checksum field zero; the result is already complemented (0xb861 for the RFC 1071 example).

    md5

    fn md5(message : Bytes) -> Bytes

    MD5 digest (16 bytes) of the input.

    sha1

    fn sha1(message : Bytes) -> Bytes

    SHA-1 digest (20 bytes) of the input.

    udp_checksum

    fn udp_checksum(source : Bytes, destination : Bytes, udp_bytes : Bytes) -> UInt

    UDP checksum over the pseudo-header plus the UDP header and payload. source/destination are the 4-byte IPv4 addresses of the pseudo header; length is the UDP length field (header + payload). The complemented sum is computed over the same layout as upstream helpers.udp_checksum; an all-zero result is transmitted as 0xffff.