Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sim TOML Reference

A simulation is defined by one TOML file. That file describes what topology to use, what binaries to run, and the sequence of steps to execute. This page covers every field.


File layout

[[extends]]          # optional: inherit from a shared defaults file
file = "..."

[matrix]             # optional: generate multiple sims via Cartesian product
topo = ["1to1", "1to3"]

[sim]                # simulation metadata
name     = "..."
topology = "..."

[[binary]]           # optional: binary definitions (repeatable)
...

[[prepare]]          # optional: prebuild configuration (repeatable)
...

[[step-template]]    # optional: reusable single-step templates (repeatable)
...

[[step-group]]       # optional: reusable multi-step groups (repeatable)
...

[[step]]             # the actual steps to execute (repeatable)
...

Inline topology tables ([[router]], [device.*], [region.*]) can also appear directly in the sim file instead of referencing an external topology.

An optional [matrix] table generates multiple simulations from one file via Cartesian product expansion. See [matrix] below.


[matrix]

Defines axes whose Cartesian product generates multiple simulation variants from a single TOML file. Each axis is an array of string values. Placeholders of the form ${matrix.<key>} in string values throughout the file are replaced with the corresponding axis value for each variant.

Files without a [matrix] table produce exactly one simulation, unchanged.

Basic usage

[matrix]
topo = ["1to1", "1to3", "1to5"]

[sim]
name     = "iroh-${matrix.topo}-baseline"
topology = "${matrix.topo}-public"

This produces three simulations. ${matrix.topo} is replaced with each value in order.

Multi-axis expansion

Multiple axes produce the Cartesian product of all values:

[matrix]
topo = ["1to1", "1to3"]
cond = ["baseline", "impaired"]

This produces four simulations (2 x 2). Each combination of topo and cond generates one variant.

Params

When a matrix axis needs more than one substitution value per variant, use [matrix.params.<axis>]. Each key in the params table corresponds to an axis value and maps to a table of additional placeholder values:

[matrix]
cond = ["baseline", "impaired"]

[matrix.params.cond]
baseline = { latency = "0", rate = "0", impaired = "false" }
impaired = { latency = "200", rate = "4000", impaired = "true" }

When cond = "impaired", the placeholders resolve as follows: ${matrix.cond} becomes impaired, ${matrix.latency} becomes 200, ${matrix.rate} becomes 4000, and ${matrix.impaired} becomes true. Param keys are flattened into the ${matrix.*} namespace alongside the axis value itself.

All param values are strings. Fields that expect numbers (like latency_ms in link conditions) accept both native TOML numbers and string representations, so latency_ms = "200" and latency_ms = 200 are equivalent.

Conditional steps with when

Steps can include a when field to conditionally skip execution based on a matrix variable. A step with when = "false" is skipped; any other value (or no when field) means the step runs normally:

[[step]]
when      = "${matrix.impaired}"
action    = "set-link-condition"
device    = "fetcher"
condition = { latency_ms = "${matrix.latency}", rate_kbit = "${matrix.rate}" }

In the baseline variant (impaired = "false"), this step is skipped. In the impaired variant (impaired = "true"), it runs and applies the condition.

Interaction with extends

Matrix expansion runs after extends are loaded. An [[extends]] file can contribute templates, groups, and binaries; the [matrix] table in the main sim file then expands the merged result.


[[extends]]

Pulls in definitions from another TOML file. The loaded file can contribute [[binary]], [[prepare]], [[step-template]], and [[step-group]] entries. The sim file’s own declarations always win on name collision. Multiple [[extends]] blocks are supported and processed in order.

KeyTypeDescription
filestringPath to the shared file. Searched relative to the sim file, then one directory up, then the working directory.

Example:

[[extends]]
file = "iroh-defaults.toml"

[sim]

KeyTypeDescription
namestringIdentifier used in output filenames and the report header.
topologystringName of a topology file to load from the topos/ directory next to the sim file. Overrides any topology from [[extends]].

[[binary]]

Declares a named binary that steps can reference as ${binary.<name>}. Exactly one source field is required (or mode can be set explicitly).

KeyTypeDescription
namestringReference key. Used as ${binary.relay}, ${binary.transfer}, etc.
modestringSource mode: "path", "fetch", "build", or "target". Inferred from other fields when omitted.
pathstringLocal path to a prebuilt binary or source directory. Prefix target: to resolve relative to the Cargo target directory.
urlstringDownload URL. Supports .tar.gz archives; the binary is extracted automatically.
repostringGit repository URL. Must pair with example or bin.
commitstringBranch, tag, or SHA for repo source. Defaults to "main".
examplestringBuild with cargo --example <name>. Works with repo (build mode) or mode = "target".
binstringBuild with cargo --bin <name>. Works with repo (build mode) or mode = "target".
featuresarrayCargo feature list to enable when building.
all-featuresbooleanBuild with --all-features.

Mode inference: When mode is omitted, it is inferred: path"path"; url"fetch"; repo, example, or bin"build". Use mode = "target" explicitly to reference a pre-built artifact in the Cargo target directory by example or bin name (skips building).


[[prepare]]

Declares binaries to prebuild from the project workspace before execution. Multiple entries are supported; each produces release-mode artifacts.

KeyTypeDescription
modestringPreparation mode. Currently only "build" (the default).
examplesarrayExample names to build with cargo build --example.
binsarrayBinary names to build with cargo build --bin.
featuresarrayCargo feature list to enable.
all-featuresbooleanBuild with --all-features.

[[step-template]]

A named, reusable step definition. Contains the same fields as a [[step]] plus a name. Referenced with use = "<name>" in a step; the call-site fields are merged on top before the step executes.

[[step-template]]
name   = "transfer-fetcher"
action = "spawn"
parser = "ndjson"
cmd    = ["${binary.transfer}", "--output", "json", "fetch"]
[step-template.captures.size]
match = { kind = "DownloadComplete" }
pick  = ".size"
[step-template.results]
down_bytes = ".size"

Call site:

[[step]]
use    = "transfer-fetcher"
id     = "fetcher"
device = "fetcher"
args   = ["${provider.endpoint_id}"]

The call site’s id, device, timeout, args, env, requires, captures, and results fields are merged into the template. args is appended to the template’s cmd. env is merged (call site wins on collision). captures is merged (call site wins). results replaces entirely if supplied.


[[step-group]]

A named sequence of steps that expands inline wherever use = "<group-name>" appears. Groups support variable substitution for parameterization.

KeyTypeDescription
namestringGroup identifier.
[[step-group.step]]arrayOrdered step definitions.

The call site uses a [[step]] with use and vars:

[[step]]
use  = "relay-setup"
vars = { device = "relay" }

Inside group steps, ${group.<key>} is substituted with the caller-supplied value before the steps execute. This substitution happens at expansion time (before runtime), so a two-stage pattern is used for nested references:

# In the group step:
content = "cert_path = \"${${group.device}-cert.cert_pem_path}\""
# After group expansion (e.g. device="relay"):
#   -> cert_path = "${relay-cert.cert_pem_path}"
# Then resolved at runtime as a capture reference.

Group steps can themselves use use = "<step-template-name>" to inherit from a template. Groups cannot nest other groups.


[[step]]

Common fields

These fields apply to most or all step types.

KeyTypeDescription
actionstringStep type. See the sections below for valid values. Defaults to "run" when cmd is present.
idstringStep identifier. Required for spawn, gen-certs, gen-file. Referenced as ${id.capture_name} in later steps.
usestringTemplate or group name. When referencing a group, only vars is used from this entry; all other fields come from the group.
varstableGroup substitution variables. Only meaningful when use references a [[step-group]].
devicestringName of the network namespace to run the command in.
envtableExtra environment variables, merged with any template env.
requiresarray of stringsCapture keys to wait for before this step starts. Format: "step_id.capture_name". Blocks until all are resolved.
whenstringConditional guard. If "false", the step is skipped. Any other value or absent means run. Typically set via ${matrix.*} substitution.

Counted device expansion

When a step targets a device that has count > 1 in the topology, the step is automatically expanded into N copies. Each copy’s device and id fields are suffixed with -0, -1, …, -N-1. For example, a step with device = "peer" against a topology with [device.peer] count = 3 produces three steps targeting peer-0, peer-1, and peer-2.

wait-for steps are similarly expanded when their id matches a counted device name.


action = "run"

Runs a command and waits for it to exit before moving to the next step.

KeyTypeDefaultDescription
cmdarrayrequiredCommand and arguments. Supports ${binary.<n>}, $NETSIM_IP_<device>, ${id.capture}.
argsarraynoneAppended to the template’s cmd. Does not replace it.
parserstring"text"Output parser. See parsers.
capturestablenoneNamed captures. See [captures].
resultstablenoneNormalized result fields. See [results].

action = "spawn"

Starts a process in the background. A later wait-for step waits for it to exit.

KeyTypeDefaultDescription
cmdarrayrequiredCommand and arguments.
argsarraynoneAppended to the template’s cmd.
parserstring"text"Output parser. See parsers.
ready_afterdurationnoneHow long to wait after spawning before the next step runs. Useful when a process needs startup time but doesn’t print a ready signal.
capturestablenoneNamed captures. See [captures].
resultstablenoneNormalized result fields. Collected when the process exits.

action = "wait-for"

Waits for a spawned process to exit. Collects its captures and results.

KeyTypeDefaultDescription
idstringrequiredID of a previously spawned step.
timeoutduration"300s"How long to wait before failing.

action = "wait"

Sleeps for a fixed duration.

KeyTypeDescription
durationdurationRequired. How long to sleep.

Applies link impairment (rate limit, loss, latency) to a device interface using tc netem and tc tbf. Pass null / omit condition to clear impairment.

KeyTypeDescription
devicestringTarget device.
interfacestringInterface name (e.g. "eth0"). Defaults to the device’s first interface.
conditionstring or tablePreset name or a custom table. See link conditions. Alias: impair.

Brings a device interface up or down.

KeyTypeDescription
devicestringTarget device.
interfacestringInterface name.

action = "set-default-route"

Switches the default route on a device to a given interface. Useful for simulating path changes.

KeyTypeDescription
devicestringTarget device.
tostringInterface to set as the new default route.

action = "gen-certs"

Generates a self-signed TLS certificate and key using rcgen. The outputs are written to {work_dir}/certs/{id}/ and also stored as captures.

KeyTypeDefaultDescription
idstringrequiredStep ID, prefixes the output captures.
devicestringnoneDevice whose IP is automatically added to the Subject Alternative Names.
cnstring"patchbay"Certificate Common Name.
sanarray of strings[device_ip]Additional SANs. $NETSIM_IP_<device> variables are expanded. Values that parse as IP addresses become IP SANs; others become DNS SANs.

Output captures: {id}.cert_pem_path, {id}.key_pem_path.


action = "gen-file"

Writes an interpolated string to disk and records the path as a capture. Useful for generating config files that reference captures from earlier steps.

KeyTypeDescription
idstringRequired.
contentstringRequired. ${...} tokens are interpolated; blocks on unresolved capture references.

Output capture: {id}.path.

The file is written to {work_dir}/files/{id}/content.


action = "assert"

Checks one or more assertion expressions. All must pass; the sim fails on the first that doesn’t.

KeyTypeDescription
checkstringSingle assertion expression.
checksarray of stringsMultiple expressions; equivalent to multiple check fields.

Expression syntax:

step_id.capture_name operator rhs

The LHS must be a capture key in the form step_id.capture_name. The value used is the most recent one recorded for that capture.

OperatorPasses when
== rhsExact string match.
!= rhsNot an exact match.
contains rhsrhs is a substring of the capture value.
matches rhsrhs is a Rust regex that matches the capture value.
>= rhsBoth sides parsed as numbers; LHS is greater or equal.

Examples:

[[step]]
action = "assert"
checks = [
  "fetcher.conn_type contains Direct",
  "fetcher.size matches [0-9]+",
  "iperf-run.bps != 0",
  "ping-check.avg_rtt >= 50",
]

Parsers

Set on run or spawn steps with parser = "...".

ValueWhen it firesWhat it can do
"text"Streaming, per lineregex captures only.
"ndjson"Streaming, per lineregex captures, plus match/pick on JSON lines.
"json"After process exitspick on the single JSON document. No per-line matching.

[captures]

Defined as sub-tables of a run or spawn step:

[[step]]
action = "run"
id     = "iperf"
parser = "json"
cmd    = ["iperf3", "-J", ...]
[step.captures.bytes]
pick = ".end.sum_received.bytes"
[step.captures.seconds]
pick = ".end.sum_received.seconds"

Or on a template:

[[step-template]]
name = "transfer-provider"
...
[step-template.captures.endpoint_id]
match = { kind = "EndpointBound" }
pick  = ".endpoint_id"
KeyTypeDefaultDescription
pipestring"stdout"Which output stream to read: "stdout" or "stderr".
regexstringnoneRegex applied to the raw text line. Group 1 is captured if present, otherwise the full match. Works with all parsers.
matchtablenoneKey=value guards on a parsed JSON object. All keys must match. Requires pick. Only valid with "ndjson" or "json" parser.
pickstringnoneDot-path into the parsed JSON value, e.g. ".endpoint_id" or ".end.sum_received.bytes". Requires "ndjson" or "json" parser.

With "ndjson", every matching line updates the capture value. With "json", the capture is set once from the parsed document. With "text", only regex matching is available.

The latest value is available for interpolation as ${step_id.capture_name}.


[results]

Maps well-known output fields to capture references, so the report and UI can show normalized throughput and latency comparisons across steps and runs.

[step.results]
duration   = "iperf-run.seconds"
down_bytes = "iperf-run.bytes"
latency_ms = "ping-check.avg_rtt"

Inside a [[step-template]], the shorthand .capture_name (leading dot, no step ID) refers to the template step’s own captures. It gets rewritten to {id}.capture_name when the template is expanded:

[step-template.results]
duration   = ".duration"    # becomes "fetcher.duration" when id="fetcher"
down_bytes = ".size"
FieldTypeDescription
durationstringCapture key for the duration of the transfer or test (microseconds as integer, or seconds as float).
up_bytesstringCapture key for bytes sent (upload).
down_bytesstringCapture key for bytes received (download).
latency_msstringCapture key for round-trip or one-way latency in milliseconds.

Throughput (down_bytes / duration) is computed in the UI. Unset fields are omitted from the output.


Used by the set-link-condition step (condition / impair field) and by device interface impair fields in the topology.

Presets:

ValueLatencyJitterLossRate limit
"lan"0 ms0 ms0 %unlimited
"wifi"5 ms2 ms0.1 %unlimited
"wifi-bad"40 ms15 ms2 %20 Mbit
"mobile-4g"25 ms8 ms0.5 %unlimited
"mobile-3g"100 ms30 ms2 %2 Mbit
"satellite"40 ms7 ms1 %unlimited
"satellite-geo"300 ms20 ms0.5 %25 Mbit

Custom table:

impair = { latency_ms = 100, jitter_ms = 10, loss_pct = 0.5, rate_kbit = 10000 }

All numeric fields also accept string representations (latency_ms = "100" is equivalent to latency_ms = 100). This enables matrix variable substitution in link condition tables.

FieldTypeDefaultDescription
rate_kbitu320Rate limit in kbit/s (0 = unlimited).
loss_pctf320.0Packet loss percentage (0.0–100.0).
latency_msu320One-way latency in milliseconds.
jitter_msu320Jitter in milliseconds (uniform ±jitter around latency).
reorder_pctf320.0Packet reordering percentage.
duplicate_pctf320.0Packet duplication percentage.
corrupt_pctf320.0Bit-error corruption percentage.

Variable interpolation

Supported in cmd, args, env values, content (gen-file), and san (gen-certs).

PatternResolves to
${binary.<name>}Resolved filesystem path to the named binary.
$NETSIM_IP_<DEVICE>IP address of the device (name uppercased, non-alphanumeric characters replaced with _).
${step_id.capture_name}Latest value of the named capture. Blocks until the capture resolves.
${matrix.<key>}Matrix axis value or param. Substituted at load time before deserialization. See [matrix].

Duration format

Durations are strings of the form "<n>s", "<n>ms", or "<n>m".

Examples: "30s", "500ms", "2m", "300s".


Output files

For each invocation of patchbay run, a timestamped run directory is created under the work root (default .patchbay-work/):

.patchbay-work/
  latest -> sim-YYMMDD-HHMMSS  # symlink to most recent run
  sim-YYMMDD-HHMMSS/           # run root
    manifest.json               # run-level metadata and sim summaries
    progress.json               # live progress (updated during execution)
    combined-results.json       # aggregated results across all sims
    combined-results.md         # human-readable combined summary
    <sim-name>/                 # per-sim subdirectory
      sim.json                  # sim-level summary (status, setup, errors)
      results.json              # captures and normalized results
      results.md                # human-readable results table
      events.jsonl              # lab lifecycle events
      nodes/
        <device>/
          stdout.log
          stderr.log
      files/                    # gen-file outputs
        <id>/content
      certs/                    # gen-certs outputs
        <id>/cert.pem
        <id>/key.pem

results.json structure:

{
  "sim": "iperf-baseline",
  "steps": [
    {
      "id": "iperf-run",
      "duration": "10.05",
      "down_bytes": "1234567890",
      "latency_ms": null,
      "up_bytes": null
    }
  ]
}

Topology files

A topology file (in topos/) defines the network graph: routers with optional NAT, and devices with their interfaces and gateways.

# A datacenter router (no NAT)
[[router]]
name = "dc"

# A home NAT router (endpoint-independent mapping, port-restricted filtering)
[[router]]
name = "lan-client"
nat  = "home"

# A device with one interface behind the DC router
[device.server.eth0]
gateway = "dc"

# A device behind the NAT router
[device.client.eth0]
gateway = "lan-client"

# A device with initial link impairment
[device.sender.eth0]
gateway = "dc"
impair  = { latency_ms = 100 }

# Multiple devices of the same name (count expansion)
[device.fetcher]
count = 10

[device.fetcher.eth0]
gateway = "dc"

Device interface fields:

KeyTypeDescription
gatewaystringRequired. Name of the upstream router.
impairstring or tableInitial link impairment. Accepts the same values as link conditions. Applied after network setup.

Device-level fields:

KeyTypeDefaultDescription
countinteger1Number of instances. Creates {name}-0 through {name}-{N-1}. Steps targeting the base name are automatically expanded.

NAT modes:

ValueBehavior
(absent)No NAT; device has a public IP on the upstream network.
"home"EIM+APDF: same external port for all destinations (port-restricted cone).
"corporate"EDM+APDF: different port per destination (symmetric NAT).
"cgnat"EIM+EIF: carrier-grade NAT, stacks with home NAT.
"cloud-nat"EDM+APDF: symmetric NAT with longer timeouts (AWS/Azure/GCP).
"full-cone"EIM+EIF: any host can reach the mapped port.

Region latency can be added to introduce inter-router delays:

[region.us-west]
latencies = { us-east = 80, eu-central = 140 }

Values are one-way latency in milliseconds. Attach a router to a region with region = "us-west" in the [[router]] table.


Example: minimal iperf sim

[sim]
name     = "iperf-baseline"
topology = "1to1-public"

[[step]]
action      = "spawn"
id          = "iperf-server"
device      = "provider"
cmd         = ["iperf3", "-s", "-1"]
ready_after = "1s"

[[step]]
action = "run"
id     = "iperf-run"
device = "fetcher"
parser = "json"
cmd    = ["iperf3", "-c", "$NETSIM_IP_provider", "-t", "10", "-J"]
[step.captures.bytes]
pick = ".end.sum_received.bytes"
[step.captures.seconds]
pick = ".end.sum_received.seconds"
[step.results]
duration   = "iperf-run.seconds"
down_bytes = "iperf-run.bytes"

[[step]]
action = "wait-for"
id     = "iperf-server"

[[step]]
action = "assert"
checks = [
  "iperf-run.bytes matches [0-9]+",
]

Example: ping with latency capture

[sim]
name = "ping-latency"

[[router]]
name = "dc"

[device.sender.eth0]
gateway = "dc"
impair  = { latency_ms = 100 }

[device.receiver.eth0]
gateway = "dc"

[[step]]
action = "run"
id     = "ping-check"
device = "sender"
cmd    = ["ping", "-c", "3", "$NETSIM_IP_receiver"]
parser = "text"

[step.captures.avg_rtt]
pipe  = "stdout"
regex = "rtt min/avg/max/mdev = [\\d.]+/([\\d.]+)/"

[step.results]
latency_ms = "ping-check.avg_rtt"

Example: iroh transfer with relay (NAT topology)

This uses templates and a step group defined in iroh-defaults.toml.

[[extends]]
file = "iroh-defaults.toml"

[sim]
name     = "iroh-1to1-nat"
topology = "1to1-nat"

# Expands to: gen-certs -> gen-file (relay config) -> spawn relay
[[step]]
use  = "relay-setup"
vars = { device = "relay" }

[[step]]
use      = "transfer-provider"
id       = "provider"
device   = "provider"
requires = ["relay.ready"]
args     = ["--relay-url", "https://$NETSIM_IP_relay:3340"]

[[step]]
use    = "transfer-fetcher"
id     = "fetcher"
device = "fetcher"
args   = ["${provider.endpoint_id}",
          "--relay-url",        "https://$NETSIM_IP_relay:3340",
          "--remote-relay-url", "https://$NETSIM_IP_relay:3340"]

[[step]]
action  = "wait-for"
id      = "fetcher"
timeout = "45s"

[[step]]
action = "assert"
checks = [
  "fetcher.size matches [0-9]+",
]

The relay-setup group (from iroh-defaults.toml) runs gen-certs, writes a relay config file with gen-file, and spawns the relay binary. The relay step captures a ready signal from stderr; provider uses requires = ["relay.ready"] to block until it fires.