24 KiB
Development and testing
See CONTRIBUTING.md for commit format, git hooks, pull request checklist, and CI overview.
Development environment
Requirements:
- Go 1.26.6 or later (see scripts/ci/dev-tools.env and mise.toml)
- Make and/or Task (either works for common workflows)
- revive and staticcheck (make bootstrap or task bootstrap)
- shellcheck and yamllint for git hooks (optional system packages)
- Python 3 for crossref vector generation (optional)
Clone the repository. Dependencies are in vendor/. No network fetch is needed for ordinary builds.
Setup (Make, Task, or both):
make bootstrap # or: task bootstrap
make doctor # or: task doctor
make hooks-install # or: task hooks:install
On some Linux distributions the Task binary is named go-task. Add alias task='go-task' if needed. Run task --list or make help for available targets.
Optional: mise (mise install) or the Dev Container (.devcontainer/).
Build automation reference
Makefile and Taskfile both set GOFLAGS=-mod=vendor, GOPROXY=off, and GOSUMDB=off. Prefer Make, Task, or plain go as you like. Equivalents for the common targets:
| Make | Task | Manual / notes |
|---|---|---|
make / make all / make build |
task build |
mkdir -p bin && CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/reticulum-go ./cmd/reticulum-go |
make install |
task install |
Installs binary, legacy tool symlinks, and man pages under PREFIX (supports DESTDIR) |
make install-man |
Man pages only | |
make install-service |
INIT=auto|systemd|openrc|runit|dinit|all. Sample config under /var/lib/reticulum-go |
|
make uninstall |
Removes installed binary, symlinks, and man pages | |
make package-deb / package-rpm / package-arch |
nfpm packages into dist/ |
|
make clean |
task clean |
go clean and remove build artifacts |
make test |
task test |
go test -v ./... (project wrapper may use testsummary) |
make test-short |
task test-short |
go test -short -v ./... |
make test-race |
CGO_ENABLED=1 go test -race -v ./... |
|
make test-services |
scripts/ci/test-services-docker.sh |
|
make test-install-script |
scripts/ci/test-install.sh |
|
make test-self-check |
scripts/ci/run-self-check.sh |
|
make test-self-check-{386,arm,riscv64,ppc64le,ppc64} |
qemu-user self-check (qemu-user-static) |
|
make coverage |
coverage profile and HTML report | |
make bench |
task bench |
go test -run=^$ -bench=. -benchmem ./... |
make fmt |
task fmt |
go fmt ./... |
make vet |
task vet |
go vet ./... |
make lint |
task lint |
revive with revive.toml |
make staticcheck |
task staticcheck |
staticcheck on core packages |
make vulncheck |
task vulncheck |
govulncheck |
make check |
task check |
fmt/vet/lint/staticcheck/short tests/vulncheck (gosec via Make) |
make prepush |
task prepush |
fmt-check, vet, lint, test-short |
make deps |
task deps |
module download/verify (clears offline proxy for the fetch) |
make run |
task run |
go run ./cmd/reticulum-go |
make debug |
go build -o bin/reticulum-go ./cmd/reticulum-go |
|
make build-linux / build-windows / build-darwin / build-all |
matching task build-* |
Cross-compile release targets |
make build-windows-legacy |
task build-windows-legacy |
go-legacy-win7 (GO_LEGACY_WIN7) |
make build-windows-xp |
task build-windows-xp |
go-legacy-winxp (GO_LEGACY_WINXP) |
make build-librns |
task build-librns |
CGO_ENABLED=1 go build -buildmode=c-shared -o bin/librns.so ./cmd/librns |
make test-wasm |
task test-wasm |
js/wasm package tests (Node exec helper) |
task build-wasm |
GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o bin/reticulum-go.wasm ./cmd/reticulum-wasm |
|
make microvm-up / microvm-stop |
matching microvm tasks | Firecracker guest. See microvm |
make tree-rsm-verify / tree-rsm-sign |
matching tasks | Source tree .rsm inventory. See SECURITY.md |
make hooks-install |
task hooks:install |
Tracked git hooks |
make doctor / bootstrap / changelog-preview |
matching tasks | Dev tool pins and CHANGELOG preview |
Code quality commands
make fmt && make vet && make lint && make check
# or
task fmt
task vet
task lint
task staticcheck
task test-short
task vulncheck
task prepush # fast path before git push
task check # full local check suite
task ci # same static checks as CI lint job
make check runs fmt, vet, lint, staticcheck, test-short, vulncheck, and gosec.
Linting
revive with project config:
revive -config revive.toml -formatter friendly ./pkg/* ./cmd/* ./internal/*
staticcheck:
staticcheck -tests=false ./pkg/... ./cmd/... ./internal/... ./tests/...
Changelog preview
Preview unreleased notes from conventional commits:
task changelog-preview
Uses cliff.toml and git-cliff (installed via go run when not on PATH).
Race detector
make test-race
Transport and interface packages include dedicated race and deadlock regression tests (interface_stress_race_test.go, pipe_race_test.go, pipe_deadlock_test.go).
Coverage
make coverage
Opens HTML coverage from coverage.out.
Benchmarks
make bench
task test-bench-gate
Loopback link throughput smoke (RNS Speedtest-style liveness floor):
task test-link-speed
reticulum-go speedtest
Nightly sim-heavy also runs test-link-speed.
Project layout for contributors
| Path | Purpose |
|---|---|
| pkg/ | Public library code |
| cmd/ | Binaries and e2e tests |
| internal/ | Daemon-only code |
| tests/crossref/ | Python vector parity |
| tests/interop/ | Live Go/Python tests |
| scripts/ci/ | CI install and release scripts |
| .github/workflows/ | GitHub Actions |
Follow existing naming, error wrapping, and SPDX headers in each file.
Testing layers
| Layer | How to run | What it covers |
|---|---|---|
| Unit | task test / task test-short | Package *_test.go |
| Property | task test-property | *_pbt_test.go and embedded quad4/pbt / testing/quick |
| Mutation | task test-mutation | gomutant on cryptography, packet, announce, destination, identity, ifac, backbone, interfaces |
| Chaos | task test-chaos / task test-soak / task test-soak-protect | TestSimChaos* / TestLinkChaos* / TestIfaceChaos* plus soak, including dos_protection flood soak |
| Oracle | task test-oracle | Crossref vectors, health TransportOracle deltas, adversarial corpus |
| Smoke | task test-binary-smoke, binding smokes | Binary --version/--help, CLI dump via Main, librns smoke examples |
| Acceptance | task test-acceptance | librns SCAFFOLD minimum, control API acceptance |
| E2E | task test-e2e | Daemon reload, UDP path e2e, transport TestE2E_* |
| Black box | task test-blackbox | CLI Main / rgodump surface, control API HTTP acceptance |
| Interop | RUN_LIVE_INTEROP=1 go test ./tests/interop/... | Live Go↔Python (optional locally), including dos_protection live suites |
Unit tests
Standard go test in each package. Run all:
go test -v ./...
Short mode skips long tests:
go test -short -v ./...
Property-based tests
Files named *_pbt_test.go use quad4/pbt for generative testing (cryptography, packet, buffer, rate, resource, announce, link, identity). Some properties still live beside unit tests in the same package.
task test-property
Mutation tests
In-repo tools/gomutant flips same-width operators and re-runs package tests.
task test-mutation
Default packages: cryptography, packet, announce, destination, identity, ifac, backbone, interfaces. Override with MUTATION_PACKAGES / MUTATION_THRESHOLD.
Fuzz tests
Files named *_fuzz_test.go cover packet, link, ifac, blackhole, discovery, health counters (pkg/health), pipe HDLC framing (pkg/interfaces/pipe_fuzz_test.go), and librns (pkg/librns).
Chaos and fault injection
Seeded loss, reorder, corruption, and flap tests across layers:
| Prefix | Package | Focus |
|---|---|---|
| TestSimChaos* | pkg/transport | Multi-node path and announce under delay/loss/corrupt/reorder/flap (health oracle deltas on corrupt) |
| TestLinkChaos* | pkg/link | Establish, packet under loss/reorder, resource under capped drop, mid-session flap, goroutine budget |
| TestIfaceChaos* | pkg/interfaces | TCP HDLC corrupt/reorder, Local corrupt resync, Pipe respawn, Backbone reconnect |
task test-chaos
task test-soak
task test-soak-protect
Chaos suites are in-process Go only (sim pipes / HDLC fixtures). Live Go↔Python HDLC loss, reorder, corrupt, and mid-session flap live under tests/interop/ (TestLiveInteropHDLC*, RUN_LIVE_INTEROP=1). Unpack hop-gate, HT2 truncation, oversize, and Pack/Unpack byte-identity vs Python RNS.Packet live in unpack_live_test.go. Healthy-path cross-stack coverage also lives under tests/interop/.
dos_protection tests
pkg/protect and interface hooks cover false positives, false negatives, auto learn/persist/relearn, and live sockets.
| Suite | Location | Focus |
|---|---|---|
| Unit / FP-FN | pkg/protect/false_positive_test.go | Quiet traffic, bursty legit, multi-iface isolation, oscillation, poisoned warmup, drift without false block |
| Snapshot / bitrate / priority | pkg/protect/snapshot_test.go, bitrate_test.go, packetclass.go | Status snapshot, scaled floors, prefer-keep shedding |
| Harm / FP oracles | pkg/protect/harm_oracle_test.go | Quiet mesh, slow radio, bulk link after quiet learn, path requests during announce floods, massive discovery bursts, peer isolation vs resource streams, auto-learning flood contract |
| Transport handler shed | pkg/transport/protect_test.go | Semaphore-full always sheds in detect, prevent, and auto learning |
| Slow findings | pkg/rnsutil/slow_protect_test.go | dos_armed_trips and cool-down findings from RPC protect block |
| Sandbox soft-fail | pkg/sandbox/warn_test.go | Rate-limited stdout when Landlock or seccomp soft-fails |
| Replay traces | pkg/protect/replay_trace_test.go | Mesh-like timelines, flood inject and recovery, auto learn then block |
| Property / fuzz | pkg/protect/property_test.go, fuzz_test.go | Detect never blocks, prevent after threshold, mode round-trip |
| Soak | task test-soak-protect | Bounded flood heap and goroutine budgets |
| Live UDP/TCP | pkg/interfaces/protect_*_live_test.go | Real loopback sockets, optional non-loopback NIC, TCP accept storms |
| Live interop | tests/interop/dos_protect_live_test.go | RUN_LIVE_INTEROP=1: quiet budget, UDP flood shed, auto learn on live UDP, transport path, public mesh peer dials |
go test ./pkg/protect -short
go test ./pkg/interfaces -run 'LiveUDP|LiveTCP|Protect'
task test-soak-protect
RUN_LIVE_INTEROP=1 go test ./tests/interop -run 'DoSProtect|DoSProtection'
FreeBSD, OpenBSD, and Haiku CI jobs run go test -short ./pkg/protect/ and transport protect tests in addition to pkg/sandbox and self-check. The test-extra soak matrix runs task test-soak-protect.
Config and threat scope: Configuration, Security.
Test oracles
| Oracle | Location |
|---|---|
| Python crossref vectors | tests/crossref/ |
| Health counter deltas | pkg/health.TransportOracle / OracleSnapshot.Delta |
| Handshake / adversarial frames | pkg/packet/testdata/ |
| IFAC goldens | pkg/ifac unit tests |
| Sim path/hop asserts | pkg/transport/sim_assertions_test.go |
task test-oracle
Smoke, acceptance, e2e, black box
task test-binary-smoke
task test-acceptance
task test-e2e
task test-blackbox
Notable additions: pkg/cli dump smoke via Main, pkg/librns.TestAcceptanceScaffoldMinimum, pkg/node.TestUDPPathE2E.
Crossref tests
Location: tests/crossref/
Purpose: byte-level parity with Python reference output.
Workflow:
./tests/crossref/run_crossref.sh generate # requires Python reference
./tests/crossref/run_crossref.sh test
./tests/crossref/run_crossref.sh all
Vectors are JSON with format version 5 (generate_vectors.py). The reference tree is fetched via rngit in run_crossref.sh.
Coverage includes identity, HKDF, HMAC, packet wire, announces, encryption, links, resources, channel envelopes, buffers, path requests.
Handshake decode trees for porters also live in pkg/packet/testdata/handshake_vectors.json (see packet-debug.md).
Packet debug tools
- Wireshark Lua dissector: tools/wireshark/rns.lua
- reticulum-go dump / rgodump for hex and pcap
- reticulum-go snapshot / rgosnap for path and health JSON
- Timeline convention: interop-timeline.md
go test ./pkg/packet/ -run 'TestHandshakeVector|TestTshark|TestDecode|TestPCAP'
go test ./pkg/health/ -run TestDrop
go test ./pkg/cli/ -run TestRunDump
Interop tests
Location: tests/interop/
Live tests pair a Go process with Python helpers under tests/interop/py/. HDLC burst (TestLiveInteropHDLCBurst*) and Unpack hop-gate (TestLiveInteropUnpackOracleMatchesPython) cover stream coalescing and refuse-invalid parity.
Enable:
RUN_LIVE_INTEROP=1 go test -v ./tests/interop/...
Optional Python interpreter (prefer a venv or pipx install with rns==1.5.2):
sh scripts/ci/setup-venv-pip.sh 'rns==1.5.2'
PYTHON_INTEROP=.venv/bin/python RUN_LIVE_INTEROP=1 go test -v ./tests/interop/...
With pipx (pipx install rns), the interop harness and self-check also auto-detect ~/.local/share/pipx/venvs/rns/bin/python when PYTHON_INTEROP is unset.
Debug harness
NomadNet relay, pageserver, and shared helpers use tests/interop/harness/.
| Variable | Behavior |
|---|---|
| INTEROP_ARTIFACTS=1 | Always keep artifact dirs (default: only when the test fails) |
| INTEROP_EVENTS=1 | Force event logging (also on when artifacts are enabled) |
| INTEROP_ARTIFACT_ROOT | Parent directory for durable artifact folders |
On failure (or with INTEROP_ARTIFACTS=1) the test logs the artifact path and the last events. Typical files:
| File | Contents |
|---|---|
| events.jsonl | One JSON object per line (ts, src, event, kind, detail) |
| stderr.txt | Captured Python stderr (human RNS logs plus INTEROP_EVENT lines) |
| env.json | Selected interop env keys |
Python peers emit INTEROP_EVENT {...} on stderr via tests/interop/py/interop_events.py. Stdout tokens such as READY and REQUEST_OK are unchanged.
Example:
INTEROP_ARTIFACTS=1 INTEROP_ARTIFACT_ROOT=/tmp/rns-interop \
RUN_LIVE_INTEROP=1 go test -v ./tests/interop/ -run 'TestLiveNomadNetLinkThroughGoRelay|TestLiveInteropPythonPageServerLargePageRequest'
| Test file | Topic |
|---|---|
| link_live_test.go | Link sessions, resources, binary burst echo, Go→Python request |
| channel_buffer_live_test.go | Channel messages and buffer streams |
| rncp_blackhole_live_test.go | rncp file transfer and blackhole LINKIDENTIFY |
| auto_live_test.go | AutoInterface |
| ifac_live_test.go | IFAC |
| transport_path_live_test.go | Path requests |
| transport_relay_live_test.go | Transport relay |
| backbone_live_test.go | Backbone |
| quic_live_test.go | QUIC Go-Go echo (no Python peer) |
| pipe_live_test.go | PipeInterface with Python echo |
| modem73_live_test.go | Modem73Interface (fake KISS/control, optional modem73 binary audio loop via MODEM73_BIN + pactl) |
| sdr_live_test.go | SDRInterface mock exchange (RUN_LIVE_SDR=1) |
| shared_rpc_live_test.go | Shared-instance RPC (TCP and Unix on Linux) |
| pageserver_live_test.go | Pageserver example |
| nomadnet_crawl_live_test.go | Nomadnet crawl |
| nomadnet_relay_live_test.go | NomadNet through Go mesh relay |
| path_cp_live_test.go | path and rgocp utilities |
| i2p_live_test.go | I2PInterface Go and Python (needs SAM) |
| directory_outgoing_live_test.go | Public directory clearnet TCP peers |
Package-specific live tests
| Package | Env var |
|---|---|
| pkg/i2p, pkg/interfaces I2P | RUN_LIVE_I2P=1 (optional I2P_SAM_ADDRESS, I2P_DIRECTORY_URL) |
| pkg/blackhole | RUN_PY_INTEROP=1 |
| pkg/discovery | RUN_PY_INTEROP=1 |
| pkg/lxstamper | RUN_PY_INTEROP=1 (LXStamper) |
End-to-end daemon tests
cmd/reticulum-go/ contains controlapi_e2e_test.go, reload_e2e_test.go, and related tests.
Host self-check
reticulum-go self-check is a host OS preflight. It validates that platform features work on the machine under test (crypto, identity file backend, sandbox, securemem, loopback interfaces, daemon with sandbox, shared-instance RPC, and on Unix a SIGHUP config/interface reload).
make test-self-check
# or
task test-self-check
# or
./bin/reticulum-go self-check --json --full
Flags:
| Flag | Behavior |
|---|---|
| --json | Machine-readable report |
| --quick | Core and platform only (no loopback or daemon) |
| --full | Also probe QUIC, HTTPS, VSOCK, Pipe, and Serial |
| --interop | Optional external tools (crossref vectors, Python RNS, binding CLIs) |
| --strict | Treat warnings as failures |
| --binary PATH | Binary used for CLI and daemon checks |
Environment:
| Variable | Behavior |
|---|---|
| RETICULUM_SELF_CHECK=1 | Used by CI wrappers that invoke the same checklist |
| RETICULUM_SELF_CHECK_INTEROP=1 | Enables the interop tier |
| RETICULUM_TEST_KEYRING=1 | Require Linux keyring round-trip (fail if unavailable) |
Exit code is non-zero on any fail result. With --strict, warnings also fail.
Daemon checks include Control API health with sandbox enabled, shared-instance GetInterfaceStats RPC, and (except Windows and FreeBSD CapEnter) SIGHUP reload of a UDP interface.
CI runs self-check on Linux (amd64 and arm64), macOS, Windows, FreeBSD, and OpenBSD. Extra Linux arches (386, arm GOARM=6, riscv64, ppc64le, ppc64) run via qemu-user-static (task test-self-check-386, test-self-check-arm, test-self-check-riscv64, test-self-check-ppc64le, test-self-check-ppc64). Android emulator self-check is a separate workflow (selfcheck-android.yml) on schedule or workflow_dispatch.
NetBSD is not in CI. Run reticulum-go self-check manually on that host.
Static footgun scan
reticulum-go zen scans Go sources (and optional Python with -python) for path and link anti-patterns: RequestPath or HasPath loops, Establish before AwaitPath, link use without callbacks, announce bursts, and legacy 15 second timeouts. It is a developer tool, not a runtime check. No daemon is required.
make build
./bin/reticulum-go zen ./...
./bin/reticulum-go zen -list-rules
./bin/reticulum-go zen -fix ./pkg/myapp/...
-fix applies only safe edits (checking RequestPath errors in functions that return error). Warnings without -fix exit non-zero so CI can gate on the scan.
Package tests live in pkg/zenfix/. Full flag and rule reference: CLI utilities.
Vendoring
Third-party source is committed under vendor/. Ordinary builds and tests use that tree with GOFLAGS=-mod=vendor and GOPROXY=off (set by the Makefile and Taskfile). That keeps air-gapped builds reliable, makes dependency upgrades reviewable as diffs, and matches CI.
Versions and checksums remain in go.mod / go.sum. Scripts that install standalone CLI tools (revive, gosec, and similar) temporarily clear those env flags to fetch the tool binary. Project code itself always compiles from vendor/.
Refresh after dependency changes:
task vendor-sync
# or
make deps
task vendor-sync requires LIBS_ROOT pointing at the Reticulum-Go-Projects sibling tree for replace directives. Commit go.mod, go.sum, and vendor/ after refresh.
Day-to-day clones only need vendor/ to build offline. Sibling checkouts are only required when re-vendoring first-party libraries. examples/wasm and examples/pageserver keep their own go.mod / vendor/ trees. Docker configs under docker/ copy those folders for offline image builds.
CI overview
GitHub Actions workflows in .github/workflows/:
| Workflow | Role |
|---|---|
| ci.yml | Build, test, reproducibility, OS self-check (Linux amd64 v1+v3, i686/386, arm64, arm, riscv64, ppc64le, ppc64, mips, s390x, plus Windows, macOS, *BSD, Solaris, illumos, AIX, Android arm64, wasm, and qemu-user self-check for 386/arm/riscv64/ppc64le/ppc64) |
| selfcheck-android.yml | Android emulator self-check (nightly / manual) |
| security.yml | Gosec, govulncheck, Trivy, SBOM dispatch |
| codeql.yml | CodeQL for Go, JS/TS, Python, and Actions workflows |
| dependency-review.yml | PR dependency and advisory gate |
| publish.yml | Tagged releases, cosign attestations |
CI uses Go 1.26.5 via actions/setup-go in .github/actions/setup-ci with GOTOOLCHAIN=local and vendored modules. Actions are SHA-pinned. Dependabot opens weekly PRs for Action bumps (.github/dependabot.yml).
Cross-compilation
make build-linux
make build-windows
make build-darwin
make build-all
make build-linux always emits linux-amd64 v1 and v3 together, plus linux-386 and linux-i686. make build-all covers every CGO-free GOOS/GOARCH pair that compiles (see scripts/build-release-targets.sh).
Legacy Windows uses go-legacy-win7 (make build-windows-legacy).
WebAssembly development
task build-wasm
make test-wasm
# or
task test-wasm
Manual WASM binary:
mkdir -p bin
GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o bin/reticulum-go.wasm ./cmd/reticulum-wasm
See Embedding and WebAssembly.
librns shared library
make build-librns
# or: task build-librns
make -C bindings/c/examples/smoke
./bindings/c/examples/smoke/librns-smoke
Needs a C toolchain and CGO. Daemon builds stay CGO_ENABLED=0. See librns.
Odin bindings
task build-librns
task test-odin
Requires the Odin compiler on PATH (CI installs a pinned monthly release via scripts/ci/setup-odin.sh, job Odin bindings). Package lives under bindings/odin. See librns.
Zig bindings
task build-librns
task test-zig
Requires Zig 0.16.0 or later on PATH (CI installs a pinned release via scripts/ci/setup-zig.sh, job Zig bindings). Package lives under bindings/zig. See librns.
C++ bindings
task build-librns
task test-cpp
Requires CMake and a C++17 compiler on PATH (CI job C++ bindings). Package lives under bindings/cpp. See librns.
Dart bindings
task build-librns
task test-dart
Requires the Dart SDK on PATH (CI pins 3.11.4, job Dart bindings) and CGO for librns.so FFI smoke tests. Package and examples live under bindings/dart. See librns Dart FFI and Control API.
Rust bindings
task build-librns
task test-rust
Requires cargo on PATH (CI job Rust bindings). Package and examples live under bindings/rust.
Python bindings
task build-librns
task test-python
Requires python3 on PATH (CI job Python bindings). Package and examples live under bindings/python.
Lua bindings
task build-librns
task test-lua
Requires LuaJIT on PATH (CI job Lua bindings). Package and examples live under bindings/lua.
Swift bindings
task build-librns
task test-swift
Requires swift on PATH (CI pins 6.0.3 via scripts/ci/setup-swift.sh, job Swift bindings). Package and examples live under bindings/swift.
Java bindings
task build-librns
task test-java
Requires javac on PATH (CI uses Temurin 17, job Java bindings). Package and examples live under bindings/java. JNA is fetched on first build.
Kotlin bindings
task build-librns
task test-kotlin
Requires kotlinc and javac on PATH (CI pins Kotlin 2.1.10 via scripts/ci/setup-kotlin.sh, job Kotlin bindings). Package and examples live under bindings/kotlin and depend on bindings/java.
C ABI examples
task build-librns
task test-c
Builds and runs bindings/c/examples (smoke plus page-fetch/pageserver compile). Binding CI jobs also run make -C bindings/ examples.
Adding a change safely
- Write or extend unit tests in the affected package
- If wire format changes, update crossref vectors and Python reference together
- Run make check locally
- For protocol behavior, add or extend interop test when feasible
- Update Compatibility and COMPATIBILITY.md if parity status changes
Related documents
- Security for CI scan detail
- Package map
- Examples