wpg (web-packet-generator) is a Rust packet generator for network verification, driven from a web GUI. It lets you assemble arbitrary packets, including deep OSI-family protocols such as IS-IS and CLNP, and transmit them at high speed. Transmission backends range from pcap/Npcap up to AF_PACKET, AF_XDP and DPDK. It ships as a single binary for Windows, macOS and Linux. Use is limited to lab and verification environments.
A .wpg.yaml file describing a packet as a stack of layers (ethernet2, ipv4, udp, …), their field values, variables and generators. Templates are what you save, share and send. See the template schema.
Library
The template directories wpg loads, ./templates by default. The bundled library has examples, protocols, scenarios, malformed and interop.
Field generator
A field value that changes from frame to frame: a range, a list, random values, a counter, values from a file or an expression. One template can expand to many frames (streams).
Builder
The GUI screen that edits a template layer by layer and previews the hex dump and field tree in the browser.
Job
One run of templates on an interface with a rate (pps, bps or % of line rate), a count or a duration. Jobs show live statistics.
Backend
How frames leave the machine: afpacket, afxdp or dpdk on Linux, pcap (libpcap / Npcap / BPF) everywhere, pcapfile to write a pcapng instead of sending (dry run), and null for benchmarks.
wpg doctor
The self-diagnosis that checks privileges, interfaces, kernel features and libraries, and prints the fix for each failed check.
The same binary is both the server (wpg serve, the web UI and REST API) and a command-line tool (wpg send, wpg render, wpg template lint, …); see the CLI reference.
wpg is a single wpg executable with the web UI embedded. Release binaries are not published yet, so every way below builds it from source; they differ in where it is built and how it runs.
Run wpg on a Linux lab host (CML2, EVE-NG, a VM), at high rates, as a service
Deploying with scripts/deploy.sh: a wizard that builds wpg on the host over SSH and installs a systemd unit. Start it from Windows (scripts\deploy.bat), Linux or macOS.
SSH access with sudo to the host
Run wpg on a Linux host without installing a Rust toolchain
rustup (scripts/setup.sh or setup.ps1 installs the rest); Npcap on Windows
Notes for the choice:
Rates. Windows sends through Npcap only and is meant for the GUI at low to medium rates. macOS sends through BPF. For high rates (AF_PACKET, AF_XDP, DPDK) use Linux.
Docker Desktop on macOS and Windows runs containers in a VM, so frames never reach your physical NIC. Use Docker on Linux hosts only.
No privileges yet? Every path can render templates to pcapng files and run the web UI without raw-socket rights; only sending needs Permissions.
Supported: Ubuntu 20.04 or later (glibc 2.31) and comparable distributions. Release binaries are not published yet, so you build from source; the result is a single wpg executable with the web UI embedded.
# Rust toolchain installer (rustup.rs) and a C toolchain for linking
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
sudo apt install build-essential pkg-config git
git clone https://github.com/SeiyaFunaokaJP/web-packet-generator.git
cd web-packet-generator
./scripts/setup.sh # add --just to also install the just task runner
scripts/setup.sh installs the Rust toolchain pinned in rust-toolchain.toml with the WASM target used by the web UI, and a prebuilt trunk (the web UI bundler) at the version pinned in scripts/versions.env. It ends by printing the next command.
Linux sends through AF_PACKET, which needs no extra library. Only the optional libpcap backend (--features pcap) needs sudo apt install libpcap-dev. tshark (package tshark) is optional; the tutorials use it to inspect generated captures.
The script builds the web UI with trunk, then the wpg binary with cargo, and prints ==> done: target/release/wpg. Add --features pcap, --features afxdp or --dpdk for optional backends. Without trunk the script still builds the binary but skips the web UI.
./target/release/wpg serve # or: just run for development with hot reload
By default wpg listens on 127.0.0.1:8080 and loads the templates under ./templates. Open http://127.0.0.1:8080 in a browser; the next page, First login, covers the password and the first screens. To reach it from another machine, set a password and use --bind 0.0.0.0:8080 (or bind in wpg.toml, written by wpg config init); see Configuration. For a permanent installation on a lab host, scripts/deploy.sh copies the binary, applies setcap and installs a systemd unit; see CML2 notes.
Supported: macOS 12 or later. Release binaries are not published yet, so you build from source; the result is a single wpg executable with the web UI embedded. macOS ships libpcap, so no extra library is needed: scripts/build.sh (and just release) build the libpcap backend (pcap feature) automatically on macOS, because it is the only way wpg can send there (--no-pcap leaves it out). Release archives include it too.
xcode-select --install # compiler and linker
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
brew install wireshark # optional: tshark, and ChmodBPF (see below)
git clone https://github.com/SeiyaFunaokaJP/web-packet-generator.git
cd web-packet-generator
./scripts/setup.sh # add --just to also install the just task runner
scripts/setup.sh installs the Rust toolchain pinned in rust-toolchain.toml with the WASM target, and a prebuilt trunk at the version pinned in scripts/versions.env.
Sending goes through /dev/bpf*. Either run wpg with sudo, or make the devices accessible: installing Wireshark with its ChmodBPF helper does this for members of the access_bpf group. wpg doctor prints the exact hint for your machine; see Permissions.
wpg listens on 127.0.0.1:8080 by default and loads the templates under ./templates. Open http://127.0.0.1:8080 in a browser and continue with First login.
Release archives (wpg-<version>-aarch64-apple-darwin.tar.gz for Apple silicon, ...-x86_64-apple-darwin.tar.gz for Intel) are not signed or notarised. A file downloaded with a browser gets the com.apple.quarantine attribute, and Gatekeeper then refuses to run it (““wpg” cannot be opened because Apple cannot check it for malicious software” or “… is damaged”). Building from source as above avoids this completely.
Check the download first, then remove the attribute from the extracted files:
shasum -a 256 -c wpg-0.2.0-aarch64-apple-darwin.tar.gz.sha256 # or compare with SHA256SUMS
tar -xzf wpg-0.2.0-aarch64-apple-darwin.tar.gz
xattr -dr com.apple.quarantine wpg-0.2.0-aarch64-apple-darwin
./wpg-0.2.0-aarch64-apple-darwin/wpg --version
Alternatives:
Download with curl -LO instead of a browser: curl does not set the quarantine attribute.
Try to run it once, then allow it in System Settings → Privacy & Security (“Open Anyway”); macOS asks again on the next start and then remembers it.
The Finder shortcut (Control-click → Open) was removed in macOS 15 and never applied to command-line tools; use xattr or System Settings.
Do not disable Gatekeeper globally (spctl --master-disable). Remove the attribute again after every upgrade, since each new download is quarantined. For running wpg at boot, see Running as a service.
Supported: Windows 10 21H2 or later. Release binaries are not published yet, so you build from source. Transmission uses Npcap only and is intended for GUI use at low to medium rates; use Linux for high rates. Commands below are for PowerShell.
Installing wpg on a Linux lab host from Windows? You do not need to build anything on Windows. Get the repository (git clone, or “Download ZIP” on GitHub and unpack it), then double-click scripts\deploy.bat. A wizard asks for the host, the SSH user and password, the sudo password, the admin password of the web UI and the interfaces to send on, builds wpg on the host and installs it as a service. Only the OpenSSH client that ships with Windows 10 / 11 is needed. See Deploying.
Install rustup (choose the default MSVC toolchain; it asks for the Visual Studio Build Tools “Desktop development with C++” workload if it is missing).
Install Npcap (the default options are fine; the WinPcap API-compatible mode is not needed). Sending needs it.
Get the repository and run the setup script:
git clone https://github.com/SeiyaFunaokaJP/web-packet-generator.git
cd web-packet-generator
.\scripts\setup.ps1 # add -Just to also install the just task runner
It installs the Rust toolchain pinned in rust-toolchain.toml with the WASM target, a prebuilt trunk and the Npcap SDK (unpacked to C:\Npcap-SDK) at the versions pinned in scripts\versions.env, and lists anything still missing (the C++ build tools, Npcap). If PowerShell refuses to run scripts, start it with powershell -ExecutionPolicy Bypass -File .\scripts\setup.ps1.
The SDK is needed to link the Npcap backend (pcap feature), the only way wpg can send on Windows. scripts\build.ps1 also finds an SDK you unpacked yourself in C:\npcap-sdk-<version> (the name of the zip; the newest version wins), in the directory named by the environment variable NPCAP_SDK, or through LIBPCAP_LIBDIR set to its Lib\x64 directory. Without the SDK the build still succeeds, but the resulting wpg.exe cannot send and build.ps1 warns about it.
Check that wpg doctor reports Npcap as installed (npcap) and the Npcap backend as built in (backend.pcap). If backend.pcap fails, build.ps1 did not find the Npcap SDK: run .\scripts\setup.ps1 (step 1) and build again. With Npcap’s default install an ordinary PowerShell can send (caps.raw_socket is ok); if Npcap was installed with “Restrict Npcap driver’s access to Administrators only”, caps.raw_socket fails and you must open PowerShell as Administrator for sending. See Permissions.
wpg listens on 127.0.0.1:8080 by default and loads the templates under .\templates. Open http://127.0.0.1:8080 in a browser and continue with First login. Windows Firewall may ask whether to allow wpg.exe; allow private networks only if you bind to a non-local address.
Use the interface name printed by wpg ifaces (the bare {GUID}; \Device\NPF_{GUID} as printed by tshark -D works too). The Npcap loopback adapter (\Device\NPF_Loopback) does not accept Ethernet frames (PacketSendPacket failed ... (50)); to try sending without touching a real network, use a host-only virtual adapter such as Hyper-V’s vEthernet (Default Switch) or a VMware host-only adapter, and watch it with wpg capture -i <interface> --backend pcap --include-outgoing or tshark. Only send on networks you own or are authorised to test. Continue with the ARP sweep tutorial.
Release archives (wpg-<version>-x86_64-pc-windows-msvc.zip) contain wpg.exe with the web UI and the Npcap backend, the sample templates and packaging\windows\wpg-service.ps1. They are not code-signed:
# Verify, then clear the "downloaded from the Internet" mark before extracting
(Get-FileHash .\wpg-0.2.0-x86_64-pc-windows-msvc.zip -Algorithm SHA256).Hash # compare with SHA256SUMS
Unblock-File .\wpg-0.2.0-x86_64-pc-windows-msvc.zip
Expand-Archive .\wpg-0.2.0-x86_64-pc-windows-msvc.zip -DestinationPath "C:\Program Files\wpg"
If SmartScreen still shows “Windows protected your PC”, choose More info → Run anyway once.
Npcap is not bundled (its licence does not allow redistribution with the free edition). wpg.exe loads wpcap.dll only when the pcap backend is first used (it is linked with /DELAYLOAD) and adds System32\Npcap to the DLL search path itself, so it starts without Npcap and works with either Npcap install mode. Both wpg doctor and scripts\build.ps1 check for it:
wpg doctor fails the npcap check when Npcap is missing and warns when Npcap looks installed but wpcap.dll is in neither System32\Npcap nor System32. The fix line links to https://npcap.com/#download.
Sending without Npcap stops with Npcap is not installed; install it from https://npcap.com/#download and run wpg doctor instead of crashing.
wpg doctor also fails backend.pcap when this wpg.exe was built without the Npcap backend, and tells you to unpack the SDK and run scripts\build.ps1 again.
When Npcap was installed in administrator-only mode, caps.raw_socket fails in an unelevated terminal; the fix is an Administrator terminal or reinstalling Npcap without that option.
scripts\build.ps1 prints a warning before building, since a build without Npcap succeeds but cannot send, and another one when it builds without the Npcap SDK.
The image builds wpg and its web UI inside the container, so the host needs Docker and nothing else: no Rust toolchain, no trunk. Use a Linux host: Docker Desktop on macOS and Windows runs containers in a VM, and frames sent there never reach your physical NIC.
This page is the short path. Build options, proxies, every setting and the limits are in Docker.
git clone https://github.com/SeiyaFunaokaJP/web-packet-generator.git
cd web-packet-generator
WPG_PASSWORD='change me' WPG_ALLOWED_IFACES='["eth1"]' docker compose up -d --build
WPG_PASSWORD is required: the server listens on every address of the host, so it refuses to start without a password. Pick your own; it is the administrator password of the web UI.
WPG_ALLOWED_IFACES lists the interfaces jobs may send on. Use the name of your lab interface (ip -br link lists them); leave it out to allow all.
The first build compiles everything and takes a while. Later starts reuse the image.
The container uses the host network and gets only CAP_NET_RAW and CAP_NET_ADMIN, so it can send on the host’s interfaces without running wpg as root on the host.
docker compose ps # STATUS shows "healthy" once the server answers
docker compose exec wpg wpg doctor # raw sockets and interfaces, as the container sees them
Open http://127.0.0.1:8080 on the host (or http://<host>:8080 from another machine) and log in with the password. First login walks through the screens.
The bundled templates are in /usr/share/wpg/templates inside the container and appear in the Templates screen. From the command line, render one to a file first (nothing is sent), then send it:
/var/lib/wpg is the wpg-data volume: the database, your own templates and files such as the pcapng above survive restarts and image upgrades. Only send on networks you own or are authorised to test. Continue with the ARP sweep tutorial, using docker compose exec wpg wpg ... where the tutorial runs wpg ....
docker compose logs -f wpg # follow the log
docker compose down # stop; the wpg-data volume is kept
git pull && WPG_PASSWORD='change me' docker compose up -d --build # upgrade
docker compose down -v # stop and delete the wpg-data volume
To keep the settings out of your shell history, put them in a .env file next to docker-compose.yml (WPG_PASSWORD=..., WPG_ALLOWED_IFACES=..., WPG_BIND=...). The repository ignores .env in both git and the Docker build context, so the password is neither committed nor copied into the image.
Sending raw frames is a privileged operation and the requirement differs per OS. At startup and on demand (wpg doctor) wpg runs a self-diagnosis and shows OS-specific guidance. Administrators can also restrict allowed interfaces and the maximum rate.
This is the hint wpg doctor prints, with the real path of the running binary. If a backend is refused with EPERM/EACCES, the error says:
AF_PACKET needs CAP_NET_RAW: run as root or grant it with `sudo setcap cap_net_raw,cap_net_admin+ep $(command -v wpg)`
File capabilities are lost when the binary is replaced and are ignored on nosuid mounts; under systemd use AmbientCapabilities=CAP_NET_RAW CAP_NET_ADMIN. See CML2 notes.
Needs read/write access to /dev/bpf*. Run with sudo, or make the devices accessible (for example with Wireshark’s ChmodBPF). The hint printed by wpg doctor is sudo /path/to/wpg … (or make /dev/bpf* accessible, e.g. via Wireshark's ChmodBPF).
Needs Npcap (either install mode; wpg finds wpcap.dll in System32\Npcap itself) and, only when Npcap was installed in administrator-only mode, an Administrator terminal. wpg doctor reads that setting (the AdminOnly value under HKLM\SYSTEM\CurrentControlSet\Services\npcap\Parameters): unelevated with the default install, caps.raw_socket is ok (npcap_admin_only=no); with administrator-only mode it fails with the hint Run from an elevated (Administrator) terminal, or reinstall Npcap ... without "Restrict Npcap driver's access to Administrators only"; when the setting cannot be read it is a warning. A missing Npcap points to https://npcap.com/#download.
wpg doctor prints one line per check with a stable id in brackets; wpg doctor --json gives the same data for scripts. The exit status is non-zero when a check fails.
The password can come from any of these (highest priority first; run wpg serve --help to see which ones your build supports):
wpg serve --password … or --password-file <file>
the WPG_PASSWORD environment variable
[auth] password_hash / password_file in wpg.toml (an argon2id hash, written by wpg config set-password)
the setup screen shown on the first visit, if none of the above is set
While no password exists wpg accepts connections from the local machine only and warns at startup; do not bind it to a non-local address until a password is set. Passwords are stored as argon2id hashes, sessions use HttpOnly, SameSite=Strict cookies, and repeated failed logins are rate limited. For access over a network, put TLS in front (--tls, a certificate in wpg.toml, or a reverse proxy).
After logging in you land on the dashboard: interface state, running jobs and recent statistics. Then:
Templates: the bundled library (examples, protocols, scenarios, malformed) is listed here, searchable by tag (protocol:ipv4, layer:l3, scenario, malformed). Open one, or duplicate it to make your own.
Builder: stack layers, edit fields (Auto/Manual, generators) and watch the hex dump and field tree update. An empty builder offers common stacks such as “Ethernet II / IPv4 / UDP”, and every field shows an input prefilled with its default (MAC address, IP address, …). Typing makes it a manual value; ↺ goes back to the default.
By default the builder only makes packets that follow the protocol definitions. Computed fields such as lengths and checksums stay locked, and out-of-spec values (a length or checksum that does not match, a value outside its constraints, …) are shown as errors under the field and in the toolbar, and block saving and creating a job.
For malformed test traffic, turn on Allow malformed packets in the toolbar: computed fields get an “Override” button and out-of-spec values are only warnings. The switch is remembered per browser and turns on by itself when you open a template that is already out of spec (such as the malformed ones).
Jobs: pick templates, an interface, a rate and a count, and start. Write to a file instead of sending makes a dry run (a pcapng file). Burst, workers and the verification signature are under Advanced. A finished job can be started again with Run again, or copied into the form with Edit as new job.
Settings: language (Japanese, English, Simplified Chinese, Traditional Chinese, Korean, German, French, Spanish), theme, template library paths.
The language menu and the theme toggle are in the header of every screen.
A stream of ARP requests (“who has 10.0.0.x?”) that walks every host address of a /22, and then the same sweep repeated inside several VLANs. ARP sweeps are the classic way to populate an ARP cache, to check that a switch floods broadcasts correctly, and to exercise a router’s ARP glean rate limiting.
Build wpg once (see Install on Linux, macOS or Windows) and run the commands from the repository root. The examples call the binary wpg; use ./target/release/wpg (or target\release\wpg.exe) if it is not on your PATH. tshark is optional but makes the checks below more convincing.
Only send on networks you own or are authorised to test; see the legal notice.
layers:
- protocol: ethernet2
fields:
dst: ff:ff:ff:ff:ff:ff
src: ${src_mac}
- protocol: arp
fields:
oper: request
sha: ${src_mac}
spa: ${src_ip}
tha: 00:00:00:00:00:00
generators:
tpa: ${first}-${last} # shorthand for a range generator
Two things do the work. generators.tpa varies the ARP target protocol address across the stream, and ${...} placeholders are template variables (declared in the variables: list with defaults) that you can override at run time with --var name=value. ethertype in the Ethernet layer is not written: it is Auto and becomes 0x0806 because the next layer is ARP.
A CIDR generator yields the host addresses only, so a /22 gives 1022 frames (10.0.0.1 to 10.0.3.254). Render it to a pcapng file without touching the network:
$ wpg render templates/scenarios/arp-sweep-slash22.wpg.yaml -o /tmp/sweep.pcapng
Wrote 1022 packets to /tmp/sweep.pcapng.
Point the same template at another subnet without editing it:
With two generators you have to say how they combine. zip advances both in lockstep, product is the Cartesian product, and nested is a product with an explicit order: here the VLAN is the slow loop, so all 254 addresses are asked in VLAN 100, then in VLAN 101, and so on: 4 x 254 = 1016 frames. Restrict it to two VLANs with a list:
$ wpg render templates/scenarios/vlan-arp-sweep.wpg.yaml -o /tmp/vlan-sweep.pcapng --var vids=100,200
Wrote 508 packets to /tmp/vlan-sweep.pcapng.
First rehearse with a backend that writes a file (for pcapfile the -i argument is the output path) or discards the frames (null):
$ wpg send templates/scenarios/arp-sweep-slash22.wpg.yaml --backend null -i lo --rate 100kpps
Sending on lo via null: rate 100000pps, burst 1, 1022 packets, 1 worker(s), plan PreRender (...)
state finished (stream exhausted)
packets 1022
bytes 61320
Then send for real on your lab interface. Raw sending needs privileges (see Permissions); run wpg ifaces to pick the interface and wpg doctor if the backend is refused:
The command runs the stream once (1016 frames at 2000 frames per second is about half a second) and prints a summary. Add --count 10000 to loop the sweep, or --duration 30s to run for a fixed time.
On the device under test, look at the ARP or neighbour table (ip neigh, show ip arp) and at the MAC table of the switch (the sender MAC 02:00:00:00:00:01 should appear on the port and in the VLANs you used).
On a Linux host in the same segment, tcpdump -e -n -i eth1 arp shows the requests with their VLAN tags.
The sweep uses a made-up sender address (10.0.0.250). Change src_mac and src_ip with --var so that replies come back to an address you can observe.
A stream of 100,000 TCP SYN segments in an 802.1Q VLAN, each with a random source address, source port and initial sequence number, and the usual SYN options (MSS, SACK-permitted, window scale). It is the standard way to load a firewall, load balancer or connection-tracking table from many “clients” while you watch the device’s SYN handling. wpg is stateless: it never completes a handshake and does not answer SYN-ACKs.
Use it only against equipment in a lab that you own or are authorised to test.
Build wpg and run from the repository root (see Install on Linux). The template is templates/scenarios/vlan-syn-flood.wpg.yaml; a smaller relative is templates/examples/vlan-tcp-syn.wpg.yaml.
Nothing else is written. IPv4 ihl, total_length, protocol and header_checksum, TCP data_offset and checksum (with the IPv4 pseudo header), and the dot1q/Ethernet EtherTypes are all Auto and are recomputed for every frame. The TCP options region is padded to a 32-bit boundary automatically.
Options are typed.mss: 1460, sack_permitted, ws: 7, nop and timestamps: { tsval: 1000, tsecr: 0 } are the built-in names; anything else can be written as { type: 99, data: "01 02" }.
stream.seed makes random reproducible. The same seed produces the same 100,000 frames on every run and on every machine.
tx.rate and tx.burst are only defaults; wpg send --rate overrides them.
Wireshark reports no bad checksum. (Seq=0 is the relative sequence number Wireshark displays; the raw values are random.) tshark -V -c 1 shows the 802.1Q header with ID: 100.
Use --var src_mac=… and --var dst_mac=… to set the MAC addresses; the next hop’s MAC on the sending interface is what the device under test expects as the destination.
The stream has a fixed length (stream.count: 100000). wpg send pre-renders small streams into memory (limit 256 MiB by default) and expands large ones on the fly; force either with --plan prerender or --plan onthefly.
Start gently and climb: --ramp-up 10s increases the rate linearly from zero, which is kind to devices that fall over at the first burst. See Load and rate control.
Time: about 25 minutes. Privileges: none until Step 6.
This page uses the IPv6 stack to practise two ideas: several layers whose type, length and checksum fields are derived from each other, and a link-layer multicast address computed from a network-layer one. The OSI stack without IP (IS-IS over LLC) is covered in Bringing up an IS-IS adjacency with Hellos.
Build wpg and run from the repository root. Only send on lab networks you own or are authorised to test: forged RAs can disrupt every host on a segment.
ethernet2.ethertype = 0x86dd because the next layer is ipv6.
ipv6.next_header = 58 because the next layer is icmpv6, and payload_length counts everything after the 40-octet header.
icmpv6.type = 135 because the next layer is ndp_ns. The ICMPv6 message is split in two layers on purpose: icmpv6 holds type, code and checksum, and the body layer (ndp_ns, ndp_na, ndp_rs, ndp_ra, ndp_redirect, icmpv6_echo) selects the type.
icmpv6.checksum covers the IPv6 pseudo header, the ICMPv6 header and the whole body, so it changes whenever any field below it changes.
A Neighbor Solicitation for 2001:db8::2 goes to the solicited-node multicast group ff02::1:ff00:2 (ff02::1:ff plus the low 24 bits of the target) and, on Ethernet, to the multicast MAC 33:33: plus the low 32 bits of the IPv6 destination: 33:33:ff:00:00:02. wpg does not derive one address from the other, so if you change target you must change ipv6.dst and ethernet2.dst too (or send to the all-nodes group ff02::1 / 33:33:00:00:00:01 as the sweep does).
Also note hop_limit: 255. Receivers drop NDP messages that arrive with any other value, so leaving the default of 64 produces packets that look right in Wireshark and do nothing.
NDP options (options) are written as hex. 01 01 02 00 00 00 00 01 is type 1 (source link-layer address), length 1 (in 8-octet units), then the six-octet MAC. The Router Advertisement template chains three options:
Because the options are raw octets you can also build deliberately odd ones: a zero-length option, an option that runs past the end of the packet, or an unknown type.
$ wpg render templates/scenarios/ndp-ns-sweep.wpg.yaml -o /tmp/ns-sweep.pcapng
Wrote 256 packets to /tmp/ns-sweep.pcapng.
Since the destination is the all-nodes group, every host on the segment sees all 256 solicitations and answers only for the addresses it owns. A real host would send each one to that target’s own solicited-node group; the sweep trades that for a single, simple template.
ra-flood.wpg.yaml shows random generators for the source MAC (random_mac with a fixed OUI) and the source link-local address, both reproducible via stream.seed.
(Privileges: see Permissions.) On the target, ip -6 neigh, ndisc6 output or show ipv6 neighbors show what was learned, and tcpdump -n -i eth1 icmp6 on another host shows the solicitations and any advertisements.
For a type that has no body protocol yet (for example MLD), set icmpv6.type explicitly and follow it with a raw layer that contains the four octets after the checksum and then the message. The same trick lets you send IPv6 extension-header chains, which have their own samples: templates/protocols/l3/ipv6-hopopts, ipv6-dstopts, ipv6-routing, ipv6-frag and ipv6-ext-chain.
Time: about 20 minutes. Privileges: none (the null and pcapfile backends are used throughout).
The skills below (mass generation, rate, duration, ramp-up, workers) work with any template. For an example that loads a real server and counts its answers, see Load-testing a DHCP server.
Commands that push a template at a chosen rate for a chosen amount of time, read the summary that comes back, and stay inside the limits of a lab. The example templates are the MAC flood (templates/scenarios/mac-flood.wpg.yaml, 100,000 frames with random source MACs) and the SYN flood (templates/scenarios/vlan-syn-flood.wpg.yaml).
Build wpg and run from the repository root. Real transmission needs privileges and a lab interface; this tutorial uses --backend null (frames are built and counted but not sent), so you can run every command anywhere. Replace --backend null -i lo with -i eth1 to send for real.
Only load devices you own or are authorised to test.
The stopping condition and the speed come from the template’s tx: block and can be overridden on the command line:
Option
Meaning
Examples
--count N
stop after N frames (the stream wraps around if N is longer than the stream)
--count 50000
--duration T
stop after T seconds/minutes
--duration 10s, --duration 2m
--rate R
target rate
250kpps, 1Mpps, 1Gbps, 50%, ifg=10us, unlimited
--burst B
frames sent back to back per rate-control step
--burst 32
If both count and duration are set, whichever comes first ends the run. Rates in bps count Layer-1 bytes (preamble, FCS and inter-frame gap included, as on the wire); % needs a real interface with a known link speed (it fails on lo).
$ wpg send templates/scenarios/udp-qos-matrix.wpg.yaml --backend null -i lo --rate 250kpps --count 50000
Sending on lo via null: rate 250000pps, burst 1, 50000 packets, 1 worker(s), plan PreRender (...)
state finished (count reached)
backend null
packets 50000
bytes 10100000
dropped 0
elapsed 0.200 s
average 250.00 kpps 404.00 Mbps (L2) 452.00 Mbps (L1)
accuracy target 250.00 kpps achieved 100.00% max jitter 454.12µs mean jitter 202ns
(The numbers on your machine will differ.) Read the last two lines first: average is what was actually sent, and accuracy tells you how close the pacing got to the target. A large dropped means the backend could not keep up.
Devices that fall over on the first burst are easier to characterise if the rate climbs. --ramp-up increases the rate linearly from zero to the target over the given time:
$ wpg send templates/scenarios/vlan-syn-flood.wpg.yaml --backend null -i lo --rate 20kpps --ramp-up 1s --duration 3s
[ 3.0 s] 50023 pkts 16.67 kpps 9.34 Mbps dropped 0 rate 100.0%
state finished (duration elapsed)
packets 50023
elapsed 3.000 s
average 16.67 kpps 9.34 Mbps (L2) 12.54 Mbps (L1)
accuracy target 20.00 kpps achieved 100.04% max jitter 3.196862ms mean jitter 115.686µs
The average (16.67 kpps) is below the 20 kpps target because the first second was the ramp.
--start-at waits before the first frame: a delay (+10s) or an RFC 3339 time (2026-09-29T12:00:00Z). It is handy for starting two senders together or for giving yourself time to start a capture:
$ wpg send templates/scenarios/vlan-syn-flood.wpg.yaml --backend null -i lo --start-at +2s --count 10
Scheduled start at +2s (in 2.0 s); Ctrl-C cancels.
A single worker is limited by one CPU core. --workers N starts N transmit threads, each with its own backend instance and its own share of the stream and of the rate; --pin-cores 2,3 pins them to cores (Linux):
unlimited on the null backend measures how fast frames can be built; it is not a network figure. For the afpacket backend also see --batch, --ring-depth, --qdisc-bypass and --no-tx-ring in wpg send --help, and Performance tuning.
--plan auto (default) pre-renders the stream into memory when it is small (256 MiB threshold) so the transmit loop only copies bytes, and expands it on the fly when it is larger. Force one with --plan prerender or --plan onthefly. Random generators take their values from stream.seed, so set one when you need a reproducible stream.
Set an absolute upper bound for everyone with [tx] max_rate in wpg.toml (for example max_rate = "100kpps") and restrict interfaces with allowed_ifaces; see Configuration.
Ctrl-C cancels a run.
Use a rehearsal backend (null, or pcapfile with -i out.pcapng) before every new template.
A round trip: take a capture (here one produced by wpg so the commands are reproducible, but any Ethernet pcap or pcapng works), convert it into an editable template, and render it back to the same bytes. Along the way wpg recognises which fields vary between packets and turns them into generators.
wpg import reads pcap (both byte orders, microsecond and nanosecond) and pcapng, Ethernet link type only, and assumes frames carry no FCS. It can also read pasted hex.
Fields are in human notation (192.168.1.10, request, ethernet), and fields that wpg can recompute were put back to auto only where doing so reproduces the captured bytes. A capture with a wrong checksum or length keeps that field as a manual value, so deliberately malformed traffic survives the trip.
Generator inference. All 254 frames have the same layer stack, and arp.tpa changes by +1 each time, so it became a range generator (an exact one). Other patterns are recognised too: a repeating cycle becomes list, a small set of values becomes an approximate list, and anything else becomes a random generator over the observed minimum and maximum (wpg warns when the result is approximate). Use --no-detect-generators to keep only the first packet’s values.
The 18 zero octets after ARP are the Ethernet padding that was in the capture. The importer keeps bytes it cannot attribute to a protocol in a raw layer so the frame is reproduced exactly. You may delete that layer: with packet.pad_to_min (the default) wpg pads the frame itself.
Because it is now a template, you can change anything:
Turn constants into variables: replace 192.168.1.10 by ${src_ip} and add variables: [{ name: src_ip, default: 192.168.1.10 }], then run wpg render … --var src_ip=10.9.8.7.
Widen the sweep: change the range to 192.168.0.1-192.168.7.254.
Set oper: reply to turn the capture into replies.
Add packet: { fcs: bad } to see how a device treats a bad FCS.
For captures that start with something other than Ethernet II, use --first-protocol. Frames that use a protocol wpg does not know are kept as raw bytes after the last layer it could decode, so they still replay.
A stream of DHCPDISCOVERs from many simulated clients, and a count of the DHCPOFFERs the server sends back, to find the rate the server can keep up with. Every frame changes the source MAC and chaddr (the client hardware address) together and uses a random transaction ID (xid). The server is dnsmasq running in a network namespace on the same Linux host.
wpg is stateless. It does not answer an OFFER with a DHCPREQUEST, so DORA (Discover / Offer / Request / Ack) never completes and no lease is created. What you measure is how fast the server handles DISCOVERs, not pool exhaustion (Step 6).
Only use this against DHCP servers you own or are authorised to test. A burst of DISCOVERs on a production segment can stop real clients from getting an address.
Build wpg and run from the repository root (Installing on Linux). The template is templates/scenarios/dhcp-discover-flood.wpg.yaml; a single-frame example is templates/protocols/app/dhcpv4-discover.wpg.yaml. From Step 4 you need Linux, iproute2, dnsmasq (sudo apt install dnsmasq-base) and root.
Source MACs are sequential.range works on MAC addresses too and counts up from 02:00:5e:00:00:01. The 02 first octet marks a locally administered address that cannot clash with real hardware. --var oui=… changes the upper three octets.
chaddr copies the source MAC through an expression. In an expr generator, $0.src means “this frame’s value of the src generator in layer 0 (ethernet2)”. Servers usually identify a client by chaddr, so the two must not drift apart. The expression syntax is in Template schema.
xid is random per frame.rand(min, max) is derived from the frame index, so the same stream.seed gives the same values on every run.
Fields you do not write are Auto or defaults. IPv4 length and checksum and UDP length and checksum (with the pseudo-header) are Auto; DHCP htype / hlen, the zero-filled sname and file, and the magic cookie come from defaults.
--count sets the number of clients. Past the 10,000 of stream.count the MAC sequence starts over, so for more clients add a second stream with a different --var oui=…:
$ wpg render templates/scenarios/dhcp-discover-flood.wpg.yaml -o /tmp/dhcp-b.pcapng --var oui=02:00:5f
Wrote 10000 packets to /tmp/dhcp-b.pcapng.
The source MACs in this file run from 02:00:5f:00:00:01 to 02:00:5f:00:27:10 (10000 = 0x2710).
Start dnsmasq in a namespace wpgdhcp, connected to wpg by a veth pair (dh0 ⇔ dhs0), with a 50-address pool:
sudo ip netns add wpgdhcp
sudo ip link add dh0 type veth peer name dhs0
sudo ip link set dhs0 netns wpgdhcp
sudo ip link set dh0 up
sudo ip netns exec wpgdhcp ip link set lo up
sudo ip netns exec wpgdhcp ip link set dhs0 up
sudo ip netns exec wpgdhcp ip addr add 10.98.0.1/24 dev dhs0
sudo ip netns exec wpgdhcp dnsmasq --no-daemon --port=0 --interface=dhs0 --bind-interfaces \
--dhcp-range=10.98.0.10,10.98.0.59,10m --dhcp-authoritative --dhcp-leasefile=/tmp/wpg-leases \
--log-dhcp --log-facility=/tmp/wpg-dnsmasq.log &
--port=0 turns DNS off. To test a real DHCP server or router, use the lab interface that reaches it instead of dh0.
Capturing on dh0 via afpacket, filter udp src port 67 (Ctrl-C stops)
# 0 16:14:33.696984775 342 B ethernet2 / ipv4 / udp / dhcpv4
# 1 16:14:33.703225873 342 B ethernet2 / ipv4 / udp / dhcpv4
state finished (duration elapsed)
...
matched 100 pkts 34200 bytes
saved 100 pkts to /tmp/offers.pcapng
$ tshark -r /tmp/offers.pcapng -c 3
1 0.000000 10.98.0.1 → 255.255.255.255 DHCP 342 DHCP Offer - Transaction ID 0xc4c98940
2 0.006241 10.98.0.1 → 255.255.255.255 DHCP 342 DHCP Offer - Transaction ID 0xef7b0650
3 0.009784 10.98.0.1 → 255.255.255.255 DHCP 342 DHCP Offer - Transaction ID 0x330355d7
Each OFFER carries the xid of its DISCOVER (Step 2). The broadcast flag is set, so the OFFERs go to 255.255.255.255.
Your first run will probably look very slow. By default dnsmasq sends an ICMP Echo to an address before offering it, to make sure it is unused. That costs about 3 seconds per DISCOVER, so a 3-second capture counts a single OFFER (the log /tmp/wpg-dnsmasq.log shows one DHCPOFFER every 3 seconds). The result above is with dnsmasq restarted with --no-ping. Finding limits like this one, set by how the server is built, is one of the points of this kind of test.
Now raise the rate to find where the OFFERs stop keeping up:
for r in 1kpps 5kpps 20kpps; do
sudo wpg capture -i dh0 -f "udp src port 67" --duration 4s --show 0 > /tmp/cap-$r.txt &
sudo wpg send templates/scenarios/dhcp-discover-flood.wpg.yaml -i dh0 --backend afpacket --count 2000 --rate $r
wait; grep '^matched' /tmp/cap-$r.txt
done
Results in the development container (--no-ping); yours depend on the host and the server:
Send rate
DISCOVERs
OFFERs
Answered
1 kpps
2000
2000
100%
5 kpps
2000
2000
100%
20 kpps
2000
1175
59%
The rate at which the answered share starts to fall is a rough measure of the server’s DISCOVER capacity. The unanswered DISCOVERs were dropped in the server’s socket receive buffer. On real equipment, DHCP snooping or CoPP (control-plane policing) may drop them first, so check the switch or router counters too.
An OFFER does not reserve the address. The pool has 50 addresses, yet all 100 DISCOVERs were answered, with 50 distinct addresses offered (tshark -r /tmp/offers.pcapng -T fields -e dhcp.ip.your | sort -u | wc -l). dnsmasq, like most servers, creates the lease on REQUEST / ACK, so DISCOVERs alone do not exhaust a pool. The lease file (/tmp/wpg-leases) stays empty.
Tests that create leases are out of scope for wpg. Answering an OFFER with a REQUEST is stateful, which is a non-goal. For pool exhaustion or lease-holding tests use a DHCP client emulator such as perfdhcp.
Through a relay: start from templates/protocols/app/dhcpv4-relay.wpg.yaml (unicast, with giaddr and option 82). Set the destination to the server’s address to test a server behind a router.
For DHCPv6 there are templates/protocols/app/dhcpv6-solicit.wpg.yaml and dhcpv6-relay-forward.wpg.yaml.
Clean up:
sudo pkill -f 'dnsmasq --no-daemon --port=0 --interface=dhs0'
sudo ip netns del wpgdhcp # removes dh0 as well
Use --ramp-up 30s with the target --rate to raise the rate linearly and find where the answered share starts to fall. See Load and rate control.
In the GUI, open this scenario from the Templates page, send it from a job, and count the OFFERs on the Receive page (/rx) with the filter udp src port 67 (Receive and verification).
An IS-IS Level 2 LAN Hello (IIH), sent to FRR’s isisd, and the peer’s view of the adjacency going from Initializing to Up. The template is the interop check templates/interop/isis-l2-lan-hello-up.wpg.yaml. It is an OSI stack without IP, with LLC (0xfe) and IS-IS directly on Ethernet, and it shows the PDU length, padding up to the MTU, and the two-way check through the IS Neighbors TLV.
wpg is stateless. Keep sending Hellos and the adjacency stays up, but wpg does not flood LSPs or act as the DIS.
Only use this against routers you own or are authorised to test. A fake adjacency in a production IS-IS domain can affect route computation.
Build wpg and run from the repository root (Installing on Linux). From Step 3 you need Linux, iproute2, FRR (sudo apt install frr; the script starts the daemons itself, so the frr service can stay stopped) and root. The IS-IS PDUs and TLVs are listed in IS-IS (P2).
layers:
- protocol: dot3
fields:
dst: "01:80:c2:00:00:15" # AllL2ISs
src: ${src_mac}
length: auto
- protocol: llc
- protocol: isis_l2_lan_hello
fields:
circuit_type: l2
source_id: ${system_id}
holding_time: 30
priority: 0 # wpg does not flood LSPs, so let the peer be the DIS
lan_id: "0000 0000 0001 01" # overwritten by the DIS's LAN ID once the peer elects itself
# Manual PDU length: Padding TLVs fill the PDU up to it (MTU 1500 - LLC 3).
pdu_length: 1497
tlvs:
- { protocols_supported: [ipv4] }
- { area_addresses: [{ address: "${area}" }] }
- { ip_interface_addresses: ["${ip}"] }
- { is_neighbors: ["${peer_mac}"] }
tx:
count: 40 # two minutes of Hellos
rate: ifg=3s
Things worth knowing:
IS-IS does not use IP. With llc on top of dot3 (802.3 with a length field), DSAP / SSAP become 0xfe (OSI) automatically, and the IS-IS NLPID 0x83 comes from the PDU layer. 01:80:c2:00:00:15 is the all-Level-2-ISs address (AllL2ISs).
A Manual pdu_length adds padding. Left Auto, the PDU is as long as its TLVs. With a value, Padding TLVs (type 8) are added until the PDU is exactly that long. 1497 is the Ethernet MTU of 1500 minus the 3-octet LLC header. A Hello that arrives at that size tells the peer that the MTUs match.
is_neighbors is the two-way check. A LAN adjacency only comes Up once a router sees its own MAC (SNPA) in the other side’s Hello. Set peer_mac to the MAC of the peer’s interface.
priority: 0 makes wpg lose the DIS (designated IS) election, because it does not do the DIS’s work (CSNPs, pseudonode LSP).
The System ID (source_id) is 6 octets; area addresses use NSAP notation (49.0001).
(The -V output is abridged.) The frame is 1514 octets (MTU 1500 plus the 14-octet Ethernet header). The 1497-octet PDU is the 27-octet header, 23 octets for the four TLVs above, and six Padding TLVs (five of 255 octets and one of 160, each plus 2 octets of type and length: 1447).
scripts/interop/frr-netns.sh starts FRR in a namespace wpgfrr connected to wpg by a veth pair (wpg0 ⇔ frr0). FRR’s IS-IS is net 49.0001.0000.0000.0002.00, is-type level-2-only. With --keep the lab stays up after the interop checks.
$ cargo build --release -p wpg-cli
$ sudo scripts/interop/frr-netns.sh --keep target/release/wpg
...
PASS IS-IS L2 adjacency Up
...
lab kept: ip netns exec wpgfrr vtysh -N wpg; tear down with scripts/interop/frr-netns.sh --down
The script’s own check has already brought the adjacency up once. Wait until it expires, one holding time (30 s) after the last Hello, before you go on:
$ sudo ip netns exec wpgfrr vtysh -N wpg -c "show isis neighbor"
Area 1:
System Id Interface L State Holdtime SNPA
Note both MACs (veth MACs change every time the pair is created):
SRC=$(cat /sys/class/net/wpg0/address)
PEER=$(sudo ip netns exec wpgfrr cat /sys/class/net/frr0/address)
FRR sends its own Hello every 3 seconds. Receive one with wpg capture to check that wpg’s parser reads it:
$ sudo wpg capture -i wpg0 -f "ether dst 01:80:c2:00:00:15" --count 1 --show 1
Capturing on wpg0 via afpacket, filter ether dst 01:80:c2:00:00:15 (Ctrl-C stops)
# 0 16:10:42.763122508 1514 B dot3 / llc / isis_l2_lan_hello
state finished (count reached)
...
FRR’s Hello is also padded to 1514 octets and parses as dot3 / llc / isis_l2_lan_hello. Save it with -w /tmp/frr.pcapng to turn it into a template (pcap to template replay).
First send one Hello without the peer’s MAC. peer_mac keeps its default 02:00:00:00:00:02, so to FRR this is a neighbor that does not know it yet:
$ sudo wpg send templates/interop/isis-l2-lan-hello-up.wpg.yaml -i wpg0 --backend afpacket \
--var src_mac=$SRC --count 1
$ sudo ip netns exec wpgfrr vtysh -N wpg -c "show isis neighbor"
Area 1:
System Id Interface L State Holdtime SNPA
0000.0000.0001 frr0 2 Initializing 29 a63f.1a8d.7239
FRR has found wpg, but without the two-way check the adjacency stays Initializing. Now send with FRR’s MAC in peer_mac:
$ sudo wpg send templates/interop/isis-l2-lan-hello-up.wpg.yaml -i wpg0 --backend afpacket \
--var src_mac=$SRC --var peer_mac=$PEER --count 2 --rate 1pps
$ sudo ip netns exec wpgfrr vtysh -N wpg -c "show isis neighbor"
Area 1:
System Id Interface L State Holdtime SNPA
0000.0000.0001 frr0 2 Up 30 a63f.1a8d.7239
$ sudo ip netns exec wpgfrr vtysh -N wpg -c "show isis neighbor detail"
Area 1:
0000.0000.0001
Interface: frr0, Level: 2, State: Up, Expires in 30s
Adjacency flaps: 1, Last: 1s ago
Circuit type: L2, Speaks: IPv4
SNPA: a63f.1a8d.7239, LAN id: 0000.0000.0001.01
LAN Priority: 0, unknown DIS state, DIS flaps: 0, Last: 2s ago
Area Address(es):
49.0001
IPv4 Address(es):
10.99.0.1
FRR sees the template’s TLVs as written: the area, the IPv4 address, priority 0 and the 30-second holding time. To keep the adjacency, send Hellos more often than the holding time. The template’s default (without --count) is 40 Hellos 3 seconds apart, two minutes in all. Stop sending and the adjacency is gone 30 seconds later.
A different area: sending with --var area=49.0002 --var "system_id=0000 0000 0003" still brings the adjacency Up. Only Level 1 adjacencies need a matching area address; Level 2 adjacencies cross areas (ISO 10589). To try Level 1, change isis_l2_lan_hello to isis_l1_lan_hello and the destination to 01:80:c2:00:00:14 (AllL1ISs) in the template, and set FRR to is-type level-1.
MTU mismatch: put pdu_length back to Auto (delete the line) and the Hello is short and unpadded. FRR does not require padded Hellos from its neighbors by default, so the adjacency still comes up, but implementations that insist on padding will not form it. Use this to compare devices.
Authentication: templates/protocols/routing/isis-l2-lan-hello.wpg.yaml carries clear-text authentication (TLV 10). wpg does not compute HMAC-MD5 values; enter them by hand.
Other PDUs: samples of LSPs, CSNPs, PSNPs and point-to-point Hellos are in templates/protocols/routing/isis-*.wpg.yaml. The LSP checksum (Fletcher) is Auto.
The procedure and configuration examples for real routers (IOS-XR, Junos) and routers on CML2 are in Interop checks.
The IPv6 counterpart of the same ideas (several layers whose type, length and checksum fields are derived from each other) is IPv6 and Neighbor Discovery.
This chapter is generated from the protocol definition IR of wpg-protocols (the catalog served at GET /api/protocols), so it always matches the code: 183 protocols. Each page lists the fields in wire order with their type, default, Auto rule and description, the lower layers that lead to the protocol, the next-layer table, the constraints checked after building and the bundled sample templates. The hand-written protocol notes explain how the protocols are reached, which fields you set by hand and the approximations that remain. After changing a definition, regenerate with just docs-gen.
Protocol number of the next layer (see Next layer)
EtherType of the next layer (Auto from the layer above; 0x88b5 = local experimental with no known next layer). A value up to 0x05ff is an IEEE 802.3 length.
Event flag: 1 for an event PDU sent at once after a QL change, 0 for the once-a-second information PDU.
reserved
uint(3)
0
reserved2
uint(24)
0
tlvs
tlv — rest of the PDU
2 items
TLVs (8-bit type, 16-bit length including the 3-octet header, Auto): QL TLV with the SSM code, Extended QL TLV with the enhanced SSM code, SyncE clock identity and cascaded clock counts. Octets after them (padding) are kept as one raw item.
MAID / MEG ID, 48 octets: MD name format and name, short MA name format, length and name, zero padded (Y.1731: no MD name, format 32 ICC-based, length 13).
tx_fcf
uint(32)
0
Frame counter (loss measurement).
rx_fcb
uint(32)
0
Frame counter (loss measurement).
tx_fcb
uint(32)
0
Frame counter (loss measurement).
reserved
uint(32)
0
tlvs
tlv — up to and including the first item of type 0, or the end of the PDU
1 items
TLVs (8-bit type, 16-bit length of the value, Auto): Sender ID, Port Status, Data, Interface Status, Reply Ingress / Egress, LTM / LTR Egress Identifier, Organization-Specific, Test, closed by the End TLV (type 0).
tlv — up to and including the first item of type 0, or the end of the PDU
2 items
Information TLVs (8-bit type, 8-bit length including the header, Auto): Local / Remote Information, Organization Specific, closed by the End TLV (type 0).
tlv — up to and including the first item of type 0, or the end of the PDU
2 items
Link event TLVs (8-bit type, 8-bit length including the header, Auto): errored symbol period, errored frame, errored frame period, errored frame seconds summary, Organization Specific, closed by the End TLV (type 0).
Protocol number of the next layer (see Next layer)
Protocol number of the header following this extension header (Auto).
hdr_ext_len
uint(8)
Length of the header in 64-bit words (-1)
Length of this extension header in 8-octet units, not counting the first 8 octets (Auto).
routing_type
enum(8)
4 (segment_routing)
Routing header type (e.g. 2 = Mobile IPv6, 4 = SRH).
segments_left
uint(8)
1
Segments Left: index (in the segment list) of the next segment to visit; 0 at the last one.
last_entry
uint(8)
Number of items of segment_list (-1)
Last Entry: index of the last element of the segment list, i.e. number of segments - 1 (Auto).
flags
uint(8)
0
Flags (no flag defined by RFC 8754; 0).
tag
uint(16)
0
Tag: marks packets as part of a class or group (0 = unused).
segment_list
list<ipv6> — last_entry * 16 + 16 octets
2 items
Segment list (SIDs) in reverse path order: element 0 is the last segment, the last element the first. While Segments Left is non-zero, upper-layer checksums use element 0 as the destination.
tlvs
tlv — up to offset hdr_ext_len * 8 + 8 of the group
Optional TLVs (8-bit type, 8-bit length): Pad1, PadN, HMAC (the HMAC value is Manual). The header is then padded with Pad1 octets to a multiple of 8 (Auto).
padding
padding(8)
Zero padding (Pad1) of the extension header to a multiple of 8 octets (Auto).
ISO 8473 Fletcher checksum of the header (Auto). 0 = checksum not used.
destination
nsap
49.0001.0000.0000.0002.00
Destination NSAP address.
source
nsap
49.0001.0000.0000.0001.00
Source NSAP address.
segmentation
list<struct> — sp items
[]
Segmentation part (data unit identifier, segment offset, total length), present when SP is set. The total length is not computed.
options
tlv — up to offset length_indicator of the group
[]
Options (8-bit code / 8-bit length) up to the length indicator: security, QoS maintenance, priority, source routing, recording of route, padding, reason for discard.
Length of the fixed header in octets (constant for each PDU type: LAN Hello / LSP 27, P2P Hello 20, CSNP 33, PSNP 17).
version_ext
uint(8)
1
Version / protocol ID extension (1).
id_length
uint(8)
0
System ID length (0 = 6 octets).
reserved
uint(3)
0
pdu_type
enum(5)
18 (l1_lsp)
PDU type (15/16 LAN Hello L1/L2, 17 P2P Hello, 18/20 LSP L1/L2, 24/25 CSNP, 26/27 PSNP).
version
uint(8)
1
Version (1).
reserved2
uint(8)
0
max_area_addresses
uint(8)
0
Maximum number of area addresses (0 = 3).
pdu_length
uint(16)
Length of the header and payload in octets
Length of the whole PDU (Auto). On a Hello, a Manual value pads the PDU to that length with Padding TLVs (e.g. 1497 for an MTU of 1500).
remaining_lifetime
uint(16)
1199
Remaining lifetime of the LSP in seconds (not covered by the checksum).
lsp_id
bytes(8)
00 00 00 00 00 01 00 00
LSP ID: system ID, pseudonode ID and fragment number.
sequence_number
uint(32)
1
LSP sequence number.
checksum
uint(16)
Fletcher checksum over the header and everything after it; irpd, header_length, version_ext, id_length, reserved, pdu_type, version, reserved2, max_area_addresses, pdu_length, remaining_lifetime counted as zero
Fletcher checksum from the LSP ID to the end of the PDU (Auto).
partition_repair
uint(1)
0
P bit: partition repair supported.
attached
flags(4)
0x0
ATT bits: attached to another area (default / delay / expense / error metric).
overload
uint(1)
0
OL bit: LSP database overload.
is_type
enum(2)
1 (l1)
IS type: 1 = L1, 3 = L1/L2.
tlvs
tlv — rest of the PDU
6 items
TLVs (8-bit code / 8-bit length) and their sub-TLVs.
Length of the fixed header in octets (constant for each PDU type: LAN Hello / LSP 27, P2P Hello 20, CSNP 33, PSNP 17).
version_ext
uint(8)
1
Version / protocol ID extension (1).
id_length
uint(8)
0
System ID length (0 = 6 octets).
reserved
uint(3)
0
pdu_type
enum(5)
20 (l2_lsp)
PDU type (15/16 LAN Hello L1/L2, 17 P2P Hello, 18/20 LSP L1/L2, 24/25 CSNP, 26/27 PSNP).
version
uint(8)
1
Version (1).
reserved2
uint(8)
0
max_area_addresses
uint(8)
0
Maximum number of area addresses (0 = 3).
pdu_length
uint(16)
Length of the header and payload in octets
Length of the whole PDU (Auto). On a Hello, a Manual value pads the PDU to that length with Padding TLVs (e.g. 1497 for an MTU of 1500).
remaining_lifetime
uint(16)
1199
Remaining lifetime of the LSP in seconds (not covered by the checksum).
lsp_id
bytes(8)
00 00 00 00 00 01 00 00
LSP ID: system ID, pseudonode ID and fragment number.
sequence_number
uint(32)
1
LSP sequence number.
checksum
uint(16)
Fletcher checksum over the header and everything after it; irpd, header_length, version_ext, id_length, reserved, pdu_type, version, reserved2, max_area_addresses, pdu_length, remaining_lifetime counted as zero
Fletcher checksum from the LSP ID to the end of the PDU (Auto).
partition_repair
uint(1)
0
P bit: partition repair supported.
attached
flags(4)
0x0
ATT bits: attached to another area (default / delay / expense / error metric).
overload
uint(1)
0
OL bit: LSP database overload.
is_type
enum(2)
3 (l1_l2)
IS type: 1 = L1, 3 = L1/L2.
tlvs
tlv — rest of the PDU
6 items
TLVs (8-bit code / 8-bit length) and their sub-TLVs.
The next layer is selected by dst_port; when the field is Auto the builder writes the value of the protocol stacked above. When decoding, src_port is looked up too if the first field matches no entry.
The next layer is selected by dst_port; when the field is Auto the builder writes the value of the protocol stacked above. When decoding, src_port is looked up too if the first field matches no entry.
Diagnostic code: the reason of the last state change of the local session.
state
enum(2)
1 (down)
Session state: 0 AdminDown, 1 Down, 2 Init, 3 Up.
p
uint(1)
0
P bit: Poll (a parameter change, the peer answers with F).
f
uint(1)
0
F bit: Final (answer to a Poll).
c
uint(1)
0
C bit: Control Plane Independent.
a
uint(1)
Number of items of authentication
A bit: authentication present (Auto: set when there is an authentication section).
d
uint(1)
0
D bit: Demand mode.
m
uint(1)
0
M bit: Multipoint (0).
detect_mult
uint(8)
3
Detect time multiplier.
length
uint(8)
Length of the header in octets
Length of the control packet in octets, including the authentication section (Auto).
my_discriminator
uint(32)
1
My discriminator: a non-zero value unique to the session on the sender.
your_discriminator
uint(32)
0
Your discriminator: the peer’s discriminator (0 while unknown).
desired_min_tx_interval
uint(32)
1000000
Desired minimum TX interval in microseconds.
required_min_rx_interval
uint(32)
1000000
Required minimum RX interval in microseconds.
required_min_echo_rx_interval
uint(32)
0
Required minimum echo RX interval in microseconds (0 = no echo).
authentication
tlv — length - 24 octets
Authentication section (8-bit type, 8-bit length including the header): simple password, keyed MD5 / SHA1 and their meticulous variants. The digest / hash is Manual.
AVPs (32-bit code, V / M / P flags, 24-bit length without the padding, optional Vendor-ID, data padded to 4 octets): base protocol and Credit-Control AVPs, grouped AVPs one level deep. Lengths, padding and the V flag are Auto.
minorVersionPTP (0 in IEEE 1588-2008, 1 in IEEE 1588-2019).
version
uint(4)
2
versionPTP (2).
message_length
uint(16)
Length of the header and payload in octets
Length of the whole message in octets, including the TLVs (Auto).
domain_number
uint(8)
0
Domain number.
minor_sdo_id
uint(8)
0
minorSdoId (reserved, 0, in IEEE 1588-2008).
flags
flags(16)
0x0
Flag field: alternateMaster, twoStep, unicast, the profile specific and security bits in the first octet; leap61, leap59, currentUtcOffsetValid, ptpTimescale, timeTraceable, frequencyTraceable and synchronizationUncertain in the second.
correction_field
int(64)
0
Correction in nanoseconds multiplied by 2^16 (signed).
message_type_specific
uint(32)
0
Message type specific field (reserved, 0, in IEEE 1588-2008).
clock_identity
uint(64)
144116287553929217
Clock identity of the source port (EUI-64, e.g. a MAC address with ff:fe in the middle).
port_number
uint(16)
1
Port number of the source port.
sequence_id
uint(16)
0
Sequence ID; a Follow_Up / Delay_Resp / Pdelay_Resp copies the one of the message it answers.
control_field
enum(8)
0 (sync)
Control field (IEEE 1588 version 1 compatibility): 0 Sync, 1 Delay_Req, 2 Follow_Up, 3 Delay_Resp, 4 Management, 5 other messages.
log_message_interval
int(8)
0
Log2 of the message interval in seconds (signed; 0x7f when not applicable).
data
bytes — message_length - 34 octets
00 00 00 00 00 00 00 00 00 00
Rest of the message after the common header, as octets (up to the message length).
minorVersionPTP (0 in IEEE 1588-2008, 1 in IEEE 1588-2019).
version
uint(4)
2
versionPTP (2).
message_length
uint(16)
Length of the header and payload in octets
Length of the whole message in octets, including the TLVs (Auto).
domain_number
uint(8)
0
Domain number.
minor_sdo_id
uint(8)
0
minorSdoId (reserved, 0, in IEEE 1588-2008).
flags
flags(16)
0x0
Flag field: alternateMaster, twoStep, unicast, the profile specific and security bits in the first octet; leap61, leap59, currentUtcOffsetValid, ptpTimescale, timeTraceable, frequencyTraceable and synchronizationUncertain in the second.
correction_field
int(64)
0
Correction in nanoseconds multiplied by 2^16 (signed).
message_type_specific
uint(32)
0
Message type specific field (reserved, 0, in IEEE 1588-2008).
clock_identity
uint(64)
144116287553929217
Clock identity of the source port (EUI-64, e.g. a MAC address with ff:fe in the middle).
port_number
uint(16)
1
Port number of the source port.
sequence_id
uint(16)
0
Sequence ID; a Follow_Up / Delay_Resp / Pdelay_Resp copies the one of the message it answers.
control_field
enum(8)
0 (sync)
Control field (IEEE 1588 version 1 compatibility): 0 Sync, 1 Delay_Req, 2 Follow_Up, 3 Delay_Resp, 4 Management, 5 other messages.
log_message_interval
int(8)
0
Log2 of the message interval in seconds (signed; 0x7f when not applicable).
origin_timestamp_seconds
uint(48)
0
Timestamp: seconds (48 bits).
origin_timestamp_nanoseconds
uint(32)
0
Timestamp: nanoseconds (32 bits).
tlvs
tlv — message_length - 44 octets
[]
TLVs (16-bit type, 16-bit length of the value, Auto): Management, Management Error Status, Organization Extension, unicast negotiation (Request / Grant / Cancel / Acknowledge Cancel), Path Trace, Alternate Time Offset Indicator. They end at the message length.
minorVersionPTP (0 in IEEE 1588-2008, 1 in IEEE 1588-2019).
version
uint(4)
2
versionPTP (2).
message_length
uint(16)
Length of the header and payload in octets
Length of the whole message in octets, including the TLVs (Auto).
domain_number
uint(8)
0
Domain number.
minor_sdo_id
uint(8)
0
minorSdoId (reserved, 0, in IEEE 1588-2008).
flags
flags(16)
0x0
Flag field: alternateMaster, twoStep, unicast, the profile specific and security bits in the first octet; leap61, leap59, currentUtcOffsetValid, ptpTimescale, timeTraceable, frequencyTraceable and synchronizationUncertain in the second.
correction_field
int(64)
0
Correction in nanoseconds multiplied by 2^16 (signed).
message_type_specific
uint(32)
0
Message type specific field (reserved, 0, in IEEE 1588-2008).
clock_identity
uint(64)
144116287553929217
Clock identity of the source port (EUI-64, e.g. a MAC address with ff:fe in the middle).
port_number
uint(16)
1
Port number of the source port.
sequence_id
uint(16)
0
Sequence ID; a Follow_Up / Delay_Resp / Pdelay_Resp copies the one of the message it answers.
control_field
enum(8)
1 (delay_req)
Control field (IEEE 1588 version 1 compatibility): 0 Sync, 1 Delay_Req, 2 Follow_Up, 3 Delay_Resp, 4 Management, 5 other messages.
log_message_interval
int(8)
127
Log2 of the message interval in seconds (signed; 0x7f when not applicable).
origin_timestamp_seconds
uint(48)
0
Timestamp: seconds (48 bits).
origin_timestamp_nanoseconds
uint(32)
0
Timestamp: nanoseconds (32 bits).
tlvs
tlv — message_length - 44 octets
[]
TLVs (16-bit type, 16-bit length of the value, Auto): Management, Management Error Status, Organization Extension, unicast negotiation (Request / Grant / Cancel / Acknowledge Cancel), Path Trace, Alternate Time Offset Indicator. They end at the message length.
minorVersionPTP (0 in IEEE 1588-2008, 1 in IEEE 1588-2019).
version
uint(4)
2
versionPTP (2).
message_length
uint(16)
Length of the header and payload in octets
Length of the whole message in octets, including the TLVs (Auto).
domain_number
uint(8)
0
Domain number.
minor_sdo_id
uint(8)
0
minorSdoId (reserved, 0, in IEEE 1588-2008).
flags
flags(16)
0x0
Flag field: alternateMaster, twoStep, unicast, the profile specific and security bits in the first octet; leap61, leap59, currentUtcOffsetValid, ptpTimescale, timeTraceable, frequencyTraceable and synchronizationUncertain in the second.
correction_field
int(64)
0
Correction in nanoseconds multiplied by 2^16 (signed).
message_type_specific
uint(32)
0
Message type specific field (reserved, 0, in IEEE 1588-2008).
clock_identity
uint(64)
144116287553929217
Clock identity of the source port (EUI-64, e.g. a MAC address with ff:fe in the middle).
port_number
uint(16)
1
Port number of the source port.
sequence_id
uint(16)
0
Sequence ID; a Follow_Up / Delay_Resp / Pdelay_Resp copies the one of the message it answers.
control_field
enum(8)
5 (other)
Control field (IEEE 1588 version 1 compatibility): 0 Sync, 1 Delay_Req, 2 Follow_Up, 3 Delay_Resp, 4 Management, 5 other messages.
log_message_interval
int(8)
127
Log2 of the message interval in seconds (signed; 0x7f when not applicable).
origin_timestamp_seconds
uint(48)
0
Timestamp: seconds (48 bits).
origin_timestamp_nanoseconds
uint(32)
0
Timestamp: nanoseconds (32 bits).
reserved
bytes(10)
00 00 00 00 00 00 00 00 00 00
tlvs
tlv — message_length - 54 octets
[]
TLVs (16-bit type, 16-bit length of the value, Auto): Management, Management Error Status, Organization Extension, unicast negotiation (Request / Grant / Cancel / Acknowledge Cancel), Path Trace, Alternate Time Offset Indicator. They end at the message length.
minorVersionPTP (0 in IEEE 1588-2008, 1 in IEEE 1588-2019).
version
uint(4)
2
versionPTP (2).
message_length
uint(16)
Length of the header and payload in octets
Length of the whole message in octets, including the TLVs (Auto).
domain_number
uint(8)
0
Domain number.
minor_sdo_id
uint(8)
0
minorSdoId (reserved, 0, in IEEE 1588-2008).
flags
flags(16)
0x0
Flag field: alternateMaster, twoStep, unicast, the profile specific and security bits in the first octet; leap61, leap59, currentUtcOffsetValid, ptpTimescale, timeTraceable, frequencyTraceable and synchronizationUncertain in the second.
correction_field
int(64)
0
Correction in nanoseconds multiplied by 2^16 (signed).
message_type_specific
uint(32)
0
Message type specific field (reserved, 0, in IEEE 1588-2008).
clock_identity
uint(64)
144116287553929217
Clock identity of the source port (EUI-64, e.g. a MAC address with ff:fe in the middle).
port_number
uint(16)
1
Port number of the source port.
sequence_id
uint(16)
0
Sequence ID; a Follow_Up / Delay_Resp / Pdelay_Resp copies the one of the message it answers.
control_field
enum(8)
5 (other)
Control field (IEEE 1588 version 1 compatibility): 0 Sync, 1 Delay_Req, 2 Follow_Up, 3 Delay_Resp, 4 Management, 5 other messages.
log_message_interval
int(8)
127
Log2 of the message interval in seconds (signed; 0x7f when not applicable).
request_receipt_timestamp_seconds
uint(48)
0
Timestamp: seconds (48 bits).
request_receipt_timestamp_nanoseconds
uint(32)
0
Timestamp: nanoseconds (32 bits).
requesting_port_clock_identity
uint(64)
144116287553929218
Port identity: clock identity (all ones = every clock).
requesting_port_port_number
uint(16)
1
Port identity: port number (0xffff = every port).
tlvs
tlv — message_length - 54 octets
[]
TLVs (16-bit type, 16-bit length of the value, Auto): Management, Management Error Status, Organization Extension, unicast negotiation (Request / Grant / Cancel / Acknowledge Cancel), Path Trace, Alternate Time Offset Indicator. They end at the message length.
minorVersionPTP (0 in IEEE 1588-2008, 1 in IEEE 1588-2019).
version
uint(4)
2
versionPTP (2).
message_length
uint(16)
Length of the header and payload in octets
Length of the whole message in octets, including the TLVs (Auto).
domain_number
uint(8)
0
Domain number.
minor_sdo_id
uint(8)
0
minorSdoId (reserved, 0, in IEEE 1588-2008).
flags
flags(16)
0x0
Flag field: alternateMaster, twoStep, unicast, the profile specific and security bits in the first octet; leap61, leap59, currentUtcOffsetValid, ptpTimescale, timeTraceable, frequencyTraceable and synchronizationUncertain in the second.
correction_field
int(64)
0
Correction in nanoseconds multiplied by 2^16 (signed).
message_type_specific
uint(32)
0
Message type specific field (reserved, 0, in IEEE 1588-2008).
clock_identity
uint(64)
144116287553929217
Clock identity of the source port (EUI-64, e.g. a MAC address with ff:fe in the middle).
port_number
uint(16)
1
Port number of the source port.
sequence_id
uint(16)
0
Sequence ID; a Follow_Up / Delay_Resp / Pdelay_Resp copies the one of the message it answers.
control_field
enum(8)
2 (follow_up)
Control field (IEEE 1588 version 1 compatibility): 0 Sync, 1 Delay_Req, 2 Follow_Up, 3 Delay_Resp, 4 Management, 5 other messages.
log_message_interval
int(8)
0
Log2 of the message interval in seconds (signed; 0x7f when not applicable).
precise_origin_timestamp_seconds
uint(48)
0
Timestamp: seconds (48 bits).
precise_origin_timestamp_nanoseconds
uint(32)
0
Timestamp: nanoseconds (32 bits).
tlvs
tlv — message_length - 44 octets
[]
TLVs (16-bit type, 16-bit length of the value, Auto): Management, Management Error Status, Organization Extension, unicast negotiation (Request / Grant / Cancel / Acknowledge Cancel), Path Trace, Alternate Time Offset Indicator. They end at the message length.
minorVersionPTP (0 in IEEE 1588-2008, 1 in IEEE 1588-2019).
version
uint(4)
2
versionPTP (2).
message_length
uint(16)
Length of the header and payload in octets
Length of the whole message in octets, including the TLVs (Auto).
domain_number
uint(8)
0
Domain number.
minor_sdo_id
uint(8)
0
minorSdoId (reserved, 0, in IEEE 1588-2008).
flags
flags(16)
0x0
Flag field: alternateMaster, twoStep, unicast, the profile specific and security bits in the first octet; leap61, leap59, currentUtcOffsetValid, ptpTimescale, timeTraceable, frequencyTraceable and synchronizationUncertain in the second.
correction_field
int(64)
0
Correction in nanoseconds multiplied by 2^16 (signed).
message_type_specific
uint(32)
0
Message type specific field (reserved, 0, in IEEE 1588-2008).
clock_identity
uint(64)
144116287553929217
Clock identity of the source port (EUI-64, e.g. a MAC address with ff:fe in the middle).
port_number
uint(16)
1
Port number of the source port.
sequence_id
uint(16)
0
Sequence ID; a Follow_Up / Delay_Resp / Pdelay_Resp copies the one of the message it answers.
control_field
enum(8)
3 (delay_resp)
Control field (IEEE 1588 version 1 compatibility): 0 Sync, 1 Delay_Req, 2 Follow_Up, 3 Delay_Resp, 4 Management, 5 other messages.
log_message_interval
int(8)
0
Log2 of the message interval in seconds (signed; 0x7f when not applicable).
receive_timestamp_seconds
uint(48)
0
Timestamp: seconds (48 bits).
receive_timestamp_nanoseconds
uint(32)
0
Timestamp: nanoseconds (32 bits).
requesting_port_clock_identity
uint(64)
144116287553929218
Port identity: clock identity (all ones = every clock).
requesting_port_port_number
uint(16)
1
Port identity: port number (0xffff = every port).
tlvs
tlv — message_length - 54 octets
[]
TLVs (16-bit type, 16-bit length of the value, Auto): Management, Management Error Status, Organization Extension, unicast negotiation (Request / Grant / Cancel / Acknowledge Cancel), Path Trace, Alternate Time Offset Indicator. They end at the message length.
minorVersionPTP (0 in IEEE 1588-2008, 1 in IEEE 1588-2019).
version
uint(4)
2
versionPTP (2).
message_length
uint(16)
Length of the header and payload in octets
Length of the whole message in octets, including the TLVs (Auto).
domain_number
uint(8)
0
Domain number.
minor_sdo_id
uint(8)
0
minorSdoId (reserved, 0, in IEEE 1588-2008).
flags
flags(16)
0x0
Flag field: alternateMaster, twoStep, unicast, the profile specific and security bits in the first octet; leap61, leap59, currentUtcOffsetValid, ptpTimescale, timeTraceable, frequencyTraceable and synchronizationUncertain in the second.
correction_field
int(64)
0
Correction in nanoseconds multiplied by 2^16 (signed).
message_type_specific
uint(32)
0
Message type specific field (reserved, 0, in IEEE 1588-2008).
clock_identity
uint(64)
144116287553929217
Clock identity of the source port (EUI-64, e.g. a MAC address with ff:fe in the middle).
port_number
uint(16)
1
Port number of the source port.
sequence_id
uint(16)
0
Sequence ID; a Follow_Up / Delay_Resp / Pdelay_Resp copies the one of the message it answers.
control_field
enum(8)
5 (other)
Control field (IEEE 1588 version 1 compatibility): 0 Sync, 1 Delay_Req, 2 Follow_Up, 3 Delay_Resp, 4 Management, 5 other messages.
log_message_interval
int(8)
127
Log2 of the message interval in seconds (signed; 0x7f when not applicable).
response_origin_timestamp_seconds
uint(48)
0
Timestamp: seconds (48 bits).
response_origin_timestamp_nanoseconds
uint(32)
0
Timestamp: nanoseconds (32 bits).
requesting_port_clock_identity
uint(64)
144116287553929218
Port identity: clock identity (all ones = every clock).
requesting_port_port_number
uint(16)
1
Port identity: port number (0xffff = every port).
tlvs
tlv — message_length - 54 octets
[]
TLVs (16-bit type, 16-bit length of the value, Auto): Management, Management Error Status, Organization Extension, unicast negotiation (Request / Grant / Cancel / Acknowledge Cancel), Path Trace, Alternate Time Offset Indicator. They end at the message length.
minorVersionPTP (0 in IEEE 1588-2008, 1 in IEEE 1588-2019).
version
uint(4)
2
versionPTP (2).
message_length
uint(16)
Length of the header and payload in octets
Length of the whole message in octets, including the TLVs (Auto).
domain_number
uint(8)
0
Domain number.
minor_sdo_id
uint(8)
0
minorSdoId (reserved, 0, in IEEE 1588-2008).
flags
flags(16)
0x0
Flag field: alternateMaster, twoStep, unicast, the profile specific and security bits in the first octet; leap61, leap59, currentUtcOffsetValid, ptpTimescale, timeTraceable, frequencyTraceable and synchronizationUncertain in the second.
correction_field
int(64)
0
Correction in nanoseconds multiplied by 2^16 (signed).
message_type_specific
uint(32)
0
Message type specific field (reserved, 0, in IEEE 1588-2008).
clock_identity
uint(64)
144116287553929217
Clock identity of the source port (EUI-64, e.g. a MAC address with ff:fe in the middle).
port_number
uint(16)
1
Port number of the source port.
sequence_id
uint(16)
0
Sequence ID; a Follow_Up / Delay_Resp / Pdelay_Resp copies the one of the message it answers.
control_field
enum(8)
5 (other)
Control field (IEEE 1588 version 1 compatibility): 0 Sync, 1 Delay_Req, 2 Follow_Up, 3 Delay_Resp, 4 Management, 5 other messages.
log_message_interval
int(8)
1
Log2 of the message interval in seconds (signed; 0x7f when not applicable).
Number of boundary clocks between the grandmaster and the sender.
time_source
enum(8)
160 (internal_oscillator)
Time source of the grandmaster: atomic clock, GPS, terrestrial radio, PTP, NTP, hand set, other, internal oscillator.
tlvs
tlv — message_length - 64 octets
[]
TLVs (16-bit type, 16-bit length of the value, Auto): Management, Management Error Status, Organization Extension, unicast negotiation (Request / Grant / Cancel / Acknowledge Cancel), Path Trace, Alternate Time Offset Indicator. They end at the message length.
minorVersionPTP (0 in IEEE 1588-2008, 1 in IEEE 1588-2019).
version
uint(4)
2
versionPTP (2).
message_length
uint(16)
Length of the header and payload in octets
Length of the whole message in octets, including the TLVs (Auto).
domain_number
uint(8)
0
Domain number.
minor_sdo_id
uint(8)
0
minorSdoId (reserved, 0, in IEEE 1588-2008).
flags
flags(16)
0x0
Flag field: alternateMaster, twoStep, unicast, the profile specific and security bits in the first octet; leap61, leap59, currentUtcOffsetValid, ptpTimescale, timeTraceable, frequencyTraceable and synchronizationUncertain in the second.
correction_field
int(64)
0
Correction in nanoseconds multiplied by 2^16 (signed).
message_type_specific
uint(32)
0
Message type specific field (reserved, 0, in IEEE 1588-2008).
clock_identity
uint(64)
144116287553929217
Clock identity of the source port (EUI-64, e.g. a MAC address with ff:fe in the middle).
port_number
uint(16)
1
Port number of the source port.
sequence_id
uint(16)
0
Sequence ID; a Follow_Up / Delay_Resp / Pdelay_Resp copies the one of the message it answers.
control_field
enum(8)
5 (other)
Control field (IEEE 1588 version 1 compatibility): 0 Sync, 1 Delay_Req, 2 Follow_Up, 3 Delay_Resp, 4 Management, 5 other messages.
log_message_interval
int(8)
127
Log2 of the message interval in seconds (signed; 0x7f when not applicable).
target_port_clock_identity
uint(64)
18446744073709551615
Port identity: clock identity (all ones = every clock).
target_port_port_number
uint(16)
65535
Port identity: port number (0xffff = every port).
tlvs
tlv — message_length - 44 octets
1 items
TLVs (16-bit type, 16-bit length of the value, Auto): Management, Management Error Status, Organization Extension, unicast negotiation (Request / Grant / Cancel / Acknowledge Cancel), Path Trace, Alternate Time Offset Indicator. They end at the message length.
minorVersionPTP (0 in IEEE 1588-2008, 1 in IEEE 1588-2019).
version
uint(4)
2
versionPTP (2).
message_length
uint(16)
Length of the header and payload in octets
Length of the whole message in octets, including the TLVs (Auto).
domain_number
uint(8)
0
Domain number.
minor_sdo_id
uint(8)
0
minorSdoId (reserved, 0, in IEEE 1588-2008).
flags
flags(16)
0x0
Flag field: alternateMaster, twoStep, unicast, the profile specific and security bits in the first octet; leap61, leap59, currentUtcOffsetValid, ptpTimescale, timeTraceable, frequencyTraceable and synchronizationUncertain in the second.
correction_field
int(64)
0
Correction in nanoseconds multiplied by 2^16 (signed).
message_type_specific
uint(32)
0
Message type specific field (reserved, 0, in IEEE 1588-2008).
clock_identity
uint(64)
144116287553929217
Clock identity of the source port (EUI-64, e.g. a MAC address with ff:fe in the middle).
port_number
uint(16)
1
Port number of the source port.
sequence_id
uint(16)
0
Sequence ID; a Follow_Up / Delay_Resp / Pdelay_Resp copies the one of the message it answers.
control_field
enum(8)
4 (management)
Control field (IEEE 1588 version 1 compatibility): 0 Sync, 1 Delay_Req, 2 Follow_Up, 3 Delay_Resp, 4 Management, 5 other messages.
log_message_interval
int(8)
127
Log2 of the message interval in seconds (signed; 0x7f when not applicable).
target_port_clock_identity
uint(64)
18446744073709551615
Port identity: clock identity (all ones = every clock).
TLVs (16-bit type, 16-bit length of the value, Auto): Management, Management Error Status, Organization Extension, unicast negotiation (Request / Grant / Cancel / Acknowledge Cancel), Path Trace, Alternate Time Offset Indicator. They end at the message length.
How the payload is generated: fixed (data as given), pattern (data repeated to length), random (length octets from seed) or counter (data with a per-packet counter embedded).
data
bytes — rest of the PDU
""
Payload octets; in fixed mode cut or zero-extended to length when length is non-zero, in pattern mode the repeated unit (empty = 00 01 02 …).
length
uint(32)
0
Payload length in octets; 0 keeps the length of data (fixed / counter mode).
seed
uint(64)
0
Seed of the pseudo-random generator in random mode.
counter_offset
uint(32)
0
Octet offset of the embedded counter in counter mode.
counter_width
enum(8)
4 (u32)
Width of the embedded counter: 4 or 8 octets, big endian.
counter_value
uint(64)
0
Counter value written per packet by the stream expander (counter mode).
The protocol reference is the field listing generated from the protocol definition IR, one page per protocol. This chapter is the hand-written supplement to it for the P1 and P2 protocols (M6, M7); each page ends with links to the generated pages of the protocols it covers. For each group it covers:
how a protocol is reached from the layer below (demultiplexing on decode)
which fields are Auto and which you must set by hand (Manual)
the approximations and limits that remain because of what the engine can express
the bundled sample templates (templates/protocols/)
Per-field descriptions (ja/en) come with the GUI field editor and GET /api/protocols. The primary source for the limits is the module docs in crates/wpg-protocols/src/; where this chapter disagrees, the code wins.
LDP PDUs, messages and TLVs (FEC, labels, Hello / session parameters), RSVP-TE Path / Resv and other messages with their objects (ERO / RRO, SESSION_ATTRIBUTE, IntServ), BFD control packets with authentication (Micro-BFD included)
Header + body split: protocols whose common header type selects the body layout (OSPF, BGP, IGMP, PIM, ICMPv6 for MLD) are stacked as two layers, the header and the body (for example ospfv2 → ospfv2_hello). The header type is Auto from the next layer, so usually you only pick the body layer.
Re-parse: where one number or port carries several layouts (VRRPv2/v3, HSRPv1/v2, DHCPv6 relay, SNMP Trap, the TFTP opcodes), decoding starts with a representative definition and re-decodes as another one based on a version or type value. When building, pick the definition you want directly.
First-nibble demux: the payload under the bottom MPLS label, GTP-U and the packet inside PIM Register are told apart by their first 4 bits (4 → IPv4, 6 → IPv6).
Source-port fallback: UDP and TCP demultiplex on the destination port and fall back to the source port, so the reply direction (DNS responses, the passive side of BGP, …) decodes too.
Opaque regions: parts that cannot be typed are kept as octets. Give them as hex when building; decoding shows them as octets too.
Every sample is linted and rendered by crates/wpg-template/tests/samples.rs. The P1 golden byte tests live in crates/wpg-protocols/tests/p1_*.rs; run them with WPG_TSHARK=1 and they also check that tshark dissects the frames (the protocol is recognised and nothing is flagged malformed).
Notes on LLDP / CDP / STP / LACP and Marker / EAPOL / PPPoE / PPP. Definitions are in crates/wpg-protocols/src/l2control.rs, samples in templates/protocols/l2/.
LLDP TLV lengths are not Auto. An LLDP TLV header is a 7-bit type plus a 9-bit length, and the engine’s TLV type only handles whole-octet widths. tlvs is therefore a list of { type: uint(7), length: uint(9), value } structs and length is Manual. When you change a value, change its length too. The trailing End TLV (type 0) is an explicit item as well.
CDP checksum: the standard Internet checksum is computed. Cisco’s implementation sign-extends the trailing octet of an odd-length PDU, so for odd-length PDUs the value differs from real devices. Keep the TLVs at an even total length, or set the checksum by hand.
STP types the IEEE 802.1D Configuration BPDU (35 octets) exactly. The RSTP / MSTP extension (Version 1 Length, Version 3 Length, MSTI configuration records) is an opaque trailer. For an RST BPDU set protocol_version and bpdu_type and put the Version 1 Length (00) in trailer; for an MST BPDU give the rest of the MST part as hex as well.
Bridge and root identifiers are split into the 2-octet priority (including the system-id extension) and the 6-octet MAC. Timers are in 1/256-second units (max_age 20 s = 0x1400).
The EAPOL body (the EAP packet for type 0) is an opaque body.
Notes on VXLAN / GRE / NVGRE / MPLS / MPLS-in-UDP / L2TPv2 and v3 / GTP-U / Geneve / IP-in-IP. Definitions are in crates/wpg-protocols/src/encap.rs, samples in templates/protocols/encap/.
GRE checksum / key / sequence number fields exist only when their flag bit is set, and the engine has no presence-conditional fields. gre is the 4-octet base (flags + protocol type) only; setting a flag does not add the field. For NVGRE, the common keyed case, use the dedicated nvgre (Key bit on by default, vsid + flow_id). nvgre is build-only: a received packet decodes as gre, and the 4 key octets are misread as the start of the inner frame. For other options, put the values in a raw layer right after gre and set the flags by hand.
MPLS pseudowires: below the bottom label the payload is told apart by its first nibble (4 → IPv4, 6 → IPv6). A control word or Ethernet pseudowire has no reliable marker and decodes as raw; when building, stack ethernet2 explicitly.
GTP-U optional fields (sequence number, N-PDU number, extension headers, present per the E/S/PN flags) are not modelled.
Geneve options have a header with 3 flag bits and a 5-bit length, narrower than an octet, so they are an opaque options region sized by opt_len.
L2TPv2 / v3 header fields depend heavily on the message kind (control / data) and flag bits, so the body is opaque octets.
Notes on OSPFv2 / OSPFv3 / BGP-4 / RIPv1 and v2 / RIPng / EIGRP. Definitions are in crates/wpg-protocols/src/routing.rs, samples in templates/protocols/routing/. OSPFv2 Opaque LSAs / Segment Routing and the BGP multiprotocol NLRI (IPv6, VPN, EVPN) are described on the OSPF Opaque / SR, BGP MP / VPN / EVPN page.
OSPF header type, packet_length and checksum. The OSPFv3 checksum includes the IPv6 pseudo header and is exact.
LSA header ls_type, length and ls_checksum (Fletcher, LS age excluded; exact for any age).
Router-LSA num_links.
BGP header length and type; OPEN opt_params_length; UPDATE withdrawn_routes_length and total_path_attribute_length; the Extended Length flag of path attributes longer than 255 octets.
OSPFv2 authentication: the checksum leaves out the 64-bit authentication field, so it is exact for null and simple-password authentication. With cryptographic authentication (AuType 2) set the checksum to 0 by hand (in the builder, turn on “Allow malformed packets” to override it). The cryptographic-auth trailer after the packet is not modelled.
One typed LSA per LSU: there is no repeated-layer construct, so no further LSA can follow a typed LSA body.
LS types sharing a body: OSPFv2 types 3 / 4 and 5 / 7, and OSPFv3 0x4005 / 0x2007, have the same body layout, so Auto announces the first type. For the other one set ls_type by hand (a next-protocol mismatch warning results, but the packet can be sent).
OSPFv2 TOS entries (router-link num_tos > 0, summary / external TOS metrics) and the OSPFv3 prefix lists are not typed; they stay opaque prefixes / address_prefix octets.
BGP routes (NLRI / withdrawn routes) are lists of {length, prefix} (length in bits, ceil(length / 8) prefix octets). A hex string is still accepted as verbatim octets.
BGP path attributes are a TLV with a 16-bit type (flags << 8 | type code); the length is 8 bits, or 16 bits with the Extended Length flag (0x10), which is set automatically above 255 octets and kept when given in an explicit type. AS_PATH / AGGREGATOR use 4-octet AS numbers (RFC 6793). Extended optional parameters (RFC 9072) are not supported.
Only the first BGP message in a segment is decoded.
EIGRP route TLVs end in a variable-length destination (ceil(prefix_length / 8) octets), kept as destination octets running to the end of the TLV value; a multi-destination TLV stays in that one field.
VRRPv2 and v3 share IP protocol 112 and differ only in the version nibble. Decoding starts as vrrpv3 and re-decodes as vrrpv2 when version is 2. When building, pick the definition you want.
VRRPv3 address family follows the IP version of the enclosing packet, but a field cannot be sized from a lower layer, so there are two definitions: vrrpv3 (IPv4) and vrrpv3_ipv6 (IPv6).
HSRPv1 and v2 share UDP 1985. The first octet is the version (0) in v1 and the first TLV type (1, Group State) in v2, so decoding starts as hsrp and re-decodes as hsrpv2 for 1. The 16-octet HSRPv2 virtual address is IPv4 (zero padded) or IPv6 depending on ip_version, so it is opaque bytes(16).
GLBP is undocumented; only the Hello TLV (IPv4 virtual address) is typed. Request/Response and Authentication TLVs keep their value octets.
Only clear-text authentication is typed: VRRPv2 auth_data (8 octets), HSRPv1 auth_data and the HSRPv2 Text Authentication TLV (both default to cisco). The HSRPv2 MD5 authentication TLV stays value octets, and no digest is computed.
IPv4 protocol 2, type 0x11 with at least 8 octets left
igmpv3-query.wpg.yaml
igmp + igmpv3_report
IGMPv3 report
IPv4 protocol 2, type 0x22
igmpv3-report.wpg.yaml
icmpv6 + mld
MLDv1 (query, report, done)
ICMPv6 type 130 / 131 / 132
mldv1-query.wpg.yaml
icmpv6 + mldv2_query
MLDv2 query
ICMPv6 type 130 with at least 24 octets left
mldv2-query.wpg.yaml
icmpv6 + mldv2_report
MLDv2 report
ICMPv6 type 143
mldv2-report.wpg.yaml
pim + pim_hello / pim_join_prune
PIM-SM v2
IPv4 protocol 103
pim-hello.wpg.yaml, pim-join-prune.wpg.yaml
pim_register
PIM Register (standalone)
chosen when building (decodes as pim)
pim-register.wpg.yaml
Like ICMPv6, IGMP and PIM are two layers, a common header (type, checksum) and a body; the header type is Auto from the body. MLD bodies stack on icmpv6.
IGMP / PIM header type and checksum. The MLD checksum is computed by icmpv6, including the IPv6 pseudo header.
Counts: num_sources of IGMPv3 / MLDv2 queries, num_group_records / num_records of reports, num_sources inside a group record, and PIM Join/Prune num_groups / num_joined / num_pruned. Adding or removing list elements keeps them right.
pim_register.checksum: covers only the 8 octets of PIM header + Register flags (RFC 7761).
v2 and v3 queries differ only in length: with at least 8 (IGMPv3) or 24 (MLDv2) octets left, a query decodes as the v3 / v2 query body, otherwise as igmpv2 / mld.
PIM Register’s checksum does not cover the encapsulated packet, so pim_register is a standalone definition that repeats the PIM header. IP protocol 103 decodes as pim, so the body of a received Register decodes as raw. The inner packet is IPv4 or IPv6 by its first nibble.
PIM over IPv6 needs the IPv6 pseudo header in its checksum, but a checksum kind cannot depend on the lower layer, so pim is reached from IPv4 only and uses the plain Internet checksum.
PIM encoded addresses are typed for IPv4 (address family 1) only.
IGMP / MLD need the Router Alert option; the samples put it in the ipv4options / the ipv6_hopopts layer.
Notes on DHCPv4 / DHCPv6 / DNS / NTP / SNMP / Syslog / RADIUS / TFTP. Definitions are in crates/wpg-protocols/src/app.rs, samples in templates/protocols/app/.
All of them are demultiplexed on the UDP port: the destination port, else the source port, so replies from a well-known port to an ephemeral one (DNS / SNMP / RADIUS responses, …) decode too.
DNSqdcount / ancount / nscount / arcount. The sections are opaque, so set the counts to match their contents.
SNMPcommunity_length (update it when you change the community string).
RADIUSauthenticator (Response Authenticator) and the Message-Authenticator attribute. Both are MD5 / HMAC-MD5 with the shared secret, and there is no keyed-digest auto rule. An Access-Request’s Request Authenticator is random, so any 16 octets will do.
DNS: names are length-prefixed labels ended by a zero octet, and each section is sized by a header count. The engine’s list type has neither a stop-on-terminator nor a count-driven length, so only the 12-octet header is typed and the sections (question, answer, authority, additional) are one opaque sections region. The default is an example.com A/IN question. Write compression pointers as octets too.
SNMP is BER-encoded ASN.1, modelled as a fixed layout of pre-encoded tag / length / value triples. Only the BER short-form length (contents below 128 octets) is supported; long-form lengths (0x81 nn, 0x82 nn nn) are not. Constructed tags nest to a fixed depth (varbind list → varbind → value). The PDU type is only known inside the PDU, so snmp is followed by snmp_pdu by default, which re-decodes as snmp_trap for the v1 Trap tag (0xa4).
DHCPv6 Relay-Forward / Relay-Reply (msg-type 12 / 13) have a different fixed layout from client / server messages, so decoding starts as dhcpv6 and re-decodes as dhcpv6_relay. The message inside the Relay Message option is typed one level deep only.
TFTP: the RRQ / WRQ file name, mode and options and the error message are typed as NUL-terminated strings. DATA / ACK / ERROR / OACK split into tftp_data and so on via re-parse on the opcode. The transfer itself runs between two ephemeral ports (TIDs), so packets not on port 69 decode as raw after UDP.
NTP control (mode 6) and private (mode 7) messages have their own layouts and are out of scope. Extension fields and the MAC are an opaque extensions.
Syslog is free text, a single opaque message. Give the string as is, in RFC 5424 or RFC 3164 format.
Notes on the OSI network-layer encapsulation shared by CLNP / ES-IS / IS-IS, and on the NSAP address type (M7). The definitions are in crates/wpg-protocols/src/osi.rs; the sample is templates/protocols/l2/dot3-llc-osi.wpg.yaml.
No “next protocol” number precedes an OSI PDU: its own first octet, the NLPID (ISO/IEC TR 9577), says what it is (0x81 CLNP, 0x82 ES-IS, 0x83 IS-IS). llc and ppp therefore peek at the first 8 payload bits (Binding::peek) to pick the next layer. The dedicated definitions are registered in that peek table: CLNP (0x81) and ES-IS (0x82), see CLNP / ES-IS, and IS-IS (0x83), see IS-IS. Other NLPIDs (for example 0x00, the inactive subset of ISO 8473), and PDUs too short for their definition, decode as the generic osi PDU (the NLPID plus the rest as opaque octets).
When building, stack osi or a dedicated PDU (clnp, esis_ish, isis_l1_lan_hello, …) on top of llc; DSAP / SSAP become 0xfe automatically. The PPP protocol likewise becomes 0x0023 automatically.
An nsap value is the address itself (1..=20 octets). On the wire it is preceded by a 1-octet length indicator, as is usual in OSI; the builder writes it and the parser reads it.
Notation: hex such as 49.0001.1921.6800.1001.00 (. and spaces are ignored, a 0x prefix is allowed).
The GUI field editor splits the NSAP being typed into AFI / area / system ID (6 octets) / NSEL (addresses shorter than 8 octets are not split).
A range generator steps the system ID (for example 49.0001.1921.6800.1001.00-49.0001.1921.6800.1004.00).
A PDU with NLPID 0x81 / 0x82 / 0x83 always decodes as CLNP / ES-IS / IS-IS (when long enough), so a generic osi layer carrying one of them reads back as the dedicated protocol.
Notes on CLNP (ISO 8473-1) and ES-IS (ISO 9542) (M7). The definitions are in crates/wpg-protocols/src/clnp.rs, the samples in templates/protocols/l3/clnp-*.wpg.yaml and templates/protocols/routing/esis-*.wpg.yaml. Both run directly over the OSI encapsulation.
dot3 / llc / clnp / payload # DT carrying a transport PDU
dot3 / llc / clnp / raw # ER carrying the discarded header
dot3 / llc / esis_esh # to 09:00:2b:00:00:05 (AllIntermediateSystems)
dot3 / llc / esis_ish # to 09:00:2b:00:00:04 (AllEndSystems)
ppp / clnp / payload # PPP protocol 0x0023
The LLC DSAP / SSAP (0xfe) and the PPP protocol (0x0023) are Auto. When parsing, the NLPID 0x81 selects clnp and 0x82 the generic esis, whose pdu_type then re-decodes the layer as the dedicated PDU; unknown ES-IS types stay esis. PDUs shorter than the fixed part (CLNP 11 octets, ES-IS 9) decode as the generic osi.
The clnp layer is the header only. Its data (DT: the transport PDU; ER: the header of the discarded PDU; ERQ / ERP: the echoed PDU) is the next layer, typically payload or raw.
Header length in octets (CLNP: up to the end of the options; ES-IS: the whole PDU)
segment_length (CLNP)
Length of the whole PDU (header + data)
checksum
ISO 8473 Fletcher checksum of the header (CLNP) / of the PDU (ES-IS). A Manual 0 means “checksum not used”; it is sent as is and reported as differing from Auto (a computed checksum is never 0)
sp (CLNP)
1 when the segmentation part has an element, else 0
number_of_source_addresses (ESH)
Number of source_addresses
bsnpa_length (RD)
Size of bsnpa
The CLNP segmentation part is a list of at most one {data_unit_identifier, segment_offset, total_length}; total_length (the length of the unsegmented PDU) is not computed:
Options are {code, length, value} items after the addresses (and the segmentation part); they end at the length_indicator (Length::UntilOffset), so whatever follows the header is the next layer even though the addresses have variable size.
Code
Name (name)
Value fields
In
0xc5
security
value (octets)
CLNP, ES-IS
0xc3
qos_maintenance
value (octets)
CLNP, ES-IS
0xcd
priority
value
CLNP, ES-IS
0xc8 / 0xcb
source_routing / recording_of_route
route_type (partial / complete), next_address, addresses: list of {address}
Notes on IS-IS (ISO/IEC 10589, RFC 1195 and the TLV RFCs below) (M7). The definitions are in crates/wpg-protocols/src/isis.rs, the samples in templates/protocols/routing/isis-*.wpg.yaml. IS-IS runs directly over the OSI encapsulation.
Each PDU is a single layer: the 8-octet common header, the PDU’s fixed fields and the TLVs.
dot3 / llc / isis_l1_lan_hello # to 01:80:c2:00:00:14 (AllL1ISs); L2: ...:15
dot3 / llc / isis_p2p_hello # to 09:00:2b:00:00:05 (AllISs) on Ethernet
ppp / isis_l2_lsp # PPP protocol 0x0023
The LLC DSAP / SSAP (0xfe) and the PPP protocol (0x0023) are Auto. When parsing, the NLPID 0x83 selects the generic isis, whose pdu_type then re-decodes the layer as the dedicated PDU; unknown PDU types stay isis.
ISO 8473 Fletcher checksum from the LSP ID to the end of the PDU. The fields before the LSP ID (common header, PDU length, remaining lifetime) are excluded, so it stays correct for any lifetime
Hello padding
When pdu_length of a Hello is Manual, Padding TLVs (type 8, up to 255 octets each) are appended until the PDU is exactly that long, e.g. pdu_length: 1497 for an Ethernet MTU of 1500 (minus the 3-octet LLC header). On Auto no padding is added. If the missing size cannot be made of TLVs (one octet), a warning is shown
sub_tlvs_length (extended IS / IP / IPv6 reachability)
Size of the sub_tlvs that follow
sub_tlvs_present (S bit, TLV 135 / 236)
1 when the entry has a sub_tlv_block, else 0
header_length is a constant per PDU type (27 / 20 / 33 / 17) and has a default, not an Auto rule; a warning appears when it is changed.
Router capability (242): SR capabilities (2), SR algorithms (19).
prefix holds ceil(prefix_length / 8) octets; give the significant octets only (192.0.2.0/24 is prefix_length: 24, prefix: "c00002"). The optional sub-TLVs of a prefix are written as a one-element sub_tlv_block list:
System IDs are 6 octets (id_length 0); other ID lengths are not modelled.
TE bandwidths are the raw 32-bit IEEE floating point values. SIDs / labels and the SR capability ranges are octets (a 3-octet label or a 4-octet index).
The HMAC-MD5 authentication value (and the cryptographic authentication of RFC 5310) is not computed.
TLV 240 is modelled in its 15-octet form; the shorter forms (1 and 5 octets) read back as data.
The sctp layer is the common header (src_port, dst_port, verification_tag, checksum) and every chunk of the packet in chunks. Several chunks can be bundled; there is no layer after sctp. The user data of DATA / I-DATA is the user_data octets of the chunk; it is not demultiplexed on the payload protocol identifier.
Chunks are items of 8-bit type, 8-bit flags and 16-bit length; the length counts the 4-octet chunk header but not the zero padding to a multiple of 4 octets, which is always added. Every chunk has a flags field (written between the type and the length):
- protocol: sctp
fields:
verification_tag: 0x3a5c7e91
chunks:
- sack:
cumulative_tsn_ack: 100
gap_ack_blocks: [{ start: 2, end: 3 }]
- name: data # `data` is a reserved item key: use {name, fields}
fields:
flags: b|e
tsn: 101
user_data: "68 65 6c 6c 6f"
- abort: { flags: t }
Parameters (INIT, INIT ACK, HEARTBEAT, ASCONF, RE-CONFIG) and error causes (ABORT, ERROR) are items of 16-bit type and 16-bit length, padded to 4 octets. As RFC 9260 §3.2 requires, the padding of the last parameter / cause is not counted in the chunk length: the chunk padding covers it.
A received chunk whose length also counts the padding of its last parameter (some implementations do) decodes the same, with an empty item after that parameter that keeps the padding where it was.
No association state: TSNs, stream sequence numbers and tags are values (use generators to vary them).
Unknown chunk types decode with their flags as the first data octet; the “action” bits of the type are not interpreted.
Notes on the routing protocol extensions of M7: OSPFv2 Opaque LSAs (RFC 5250) with Traffic Engineering (RFC 3630), Router Information (RFC 7770), Extended Prefix / Extended Link (RFC 7684) and Segment Routing (RFC 8665), and the BGP multiprotocol NLRI (RFC 4760) for IPv4 / IPv6 unicast, VPN-IPv4 / VPN-IPv6 (RFC 4364 / RFC 4659) and EVPN (RFC 7432 / RFC 9136). The definitions are in crates/wpg-protocols/src/ospf_opaque.rs and crates/wpg-protocols/src/bgp_mp.rs; the basics of OSPF and BGP are on the routing protocols page.
ospfv2_opaque_lsa is the LSA header with the Link State ID split into opaque_type (8 bits, Auto from the body layer) and opaque_id (24 bits). ls_type gives the flooding scope: 9 link-local, 10 area (default), 11 AS. The LSA length and the Fletcher ls_checksum are Auto as for the other LSAs. When parsing, an LSA of type 9 / 10 / 11 is first read as ospfv2_lsa and then re-decoded as ospfv2_opaque_lsa; an unknown opaque type is followed by a raw layer.
Sample
Contents
ospfv2-lsu-te-lsa
TE LSA: Link TLV with link type / ID, interface addresses, TE metric, bandwidths, admin group
ospfv2-lsu-ri-sr
Router Information LSA: capabilities, hostname, SR-Algorithm, SRGB (SID/Label Range), SRLB
ospfv2-lsu-ext-prefix-sr
Extended Prefix LSA: 192.0.2.1/32 with a node Prefix SID (index 1)
ospfv2-lsu-ext-link-sr
Extended Link LSA: a point-to-point link with an Adj-SID (label 24000)
Each body has one field, tlvs. TLVs and sub-TLVs have a 16-bit type and a 16-bit length counting the value only; the value is padded with zeros to a multiple of 4 octets (not counted in the length). Lengths and padding are Auto.
A SID is 3 octets (a label in the low 20 bits, V and L flags set) or 4 octets (an index, V and L clear). Every sub-TLV that carries one exists in both forms; pick the item by name (label or index field) and the parser tells them apart by the value length:
MP_REACH_NLRI (code 14) and MP_UNREACH_NLRI (code 15) have a layout that depends on the AFI / SAFI after them. Each modelled family is its own path attribute item; the parser picks it from the first three value octets (AFI, SAFI):
Family
AFI / SAFI
Items
Next hop
NLRI element
IPv4 unicast
1 / 1
mp_reach_ipv4_unicast, mp_unreach_ipv4_unicast
list of IPv4 addresses
{length, prefix}
IPv6 unicast
2 / 1
mp_reach_ipv6_unicast, mp_unreach_ipv6_unicast
list of IPv6 addresses (global, link-local)
{length, prefix}
VPN-IPv4
1 / 128
mp_reach_vpnv4, mp_unreach_vpnv4
list of {rd, address}
{length, label, exp, bos, rd, prefix}
VPN-IPv6
2 / 128
mp_reach_vpnv6, mp_unreach_vpnv6
list of {rd, address}
same
EVPN
25 / 70
mp_reach_evpn, mp_unreach_evpn
octets (IPv4 or IPv6 address)
EVPN route items
Other families use the generic mp_reach_nlri / mp_unreach_nlri with the next hop and NLRI as octets. afi / safi default to the family; next_hop_length is Auto.
Prefixes: length is in bits and prefix has ceil(length / 8) octets. The UPDATE withdrawn_routes and nlri are prefix lists too, and both UPDATE length fields are Auto.
VPN prefixes: length counts the label (24 bits), the route distinguisher (64 bits) and the prefix bits, so a /24 is 112. One label is modelled (bos: 1); a withdrawal usually carries label: 0x80000, bos: 0 (the 0x800000 compatibility value).
Route distinguishers are 8 octets: 0000 fde9 00000064 is 65001:100 (type 0), 0001 c0000201 0001 is 192.0.2.1:1 (type 1).
EVPN routes (8-bit type, 8-bit length): ethernet_ad (1), mac_ip (2), inclusive_multicast (3), ethernet_segment (4), ip_prefix (5, IPv4, 34 octets) and ipv6_prefix (5, IPv6, 58 octets). ESIs are 10 octets. Label fields are the 3-octet wire value: label << 4 | bos for MPLS, the VNI for VXLAN (RFC 8365). IP addresses with a length field (ip_length in bits) are octets.
The PMSI Tunnel attribute (pmsi_tunnel, code 22) is typed; route targets are extended_communities values (0x0002fde900000064 = RT 65001:100).
A path attribute whose value exceeds 255 octets gets the Extended Length flag (0x10) and a 16-bit length automatically. To send the long form for a short attribute (as FRR and GoBGP do for MP_REACH_NLRI), give the item an explicit type:
OSPFv3 Extended LSAs (RFC 8362) and OSPFv3 Segment Routing (RFC 8666) are not modelled.
The OSPF TE GMPLS sub-TLVs (RFC 4203) and the RI TLVs other than those listed stay raw items.
BGP ADD-PATH path identifiers (RFC 7911), label stacks of more than one label (RFC 8277) and the BGP Prefix-SID attribute (RFC 8669) are not modelled. Write such NLRI as octets with the generic mp_reach_nlri / mp_unreach_nlri items; when parsing such NLRI, the octets still rebuild identically but the decoded fields do not line up with them.
Several BGP messages in one segment, and several typed LSAs in one LSU, are not supported (see the routing protocols page).
Notes on LDP (RFC 5036), RSVP-TE (RFC 2205 / RFC 3209) and BFD (RFC 5880 / 5881 / 5883, Micro-BFD RFC 7130) (M7). The definitions live in crates/wpg-protocols/src/{ldp,rsvp,bfd}.rs, the samples in templates/protocols/mpls/ and templates/protocols/oam/.
LDP over IPv6 (RFC 7552) is ipv6 / udp|tcp / ldp. UDP / TCP destination ports are not Auto: set 646, 3784, … (decoding also recognises the source port). The BFD echo port 3785 is not decoded: its contents only mean something to the sender.
Sample
Contents
ldp-hello
Link Hello to 224.0.0.2 (GTSM bit, IPv4 transport address)
ldp-targeted-hello
Targeted Hello for extended discovery (T / R bits, configuration sequence number)
ldp-initialization
Initialization (Common Session Parameters) and KeepAlive in one PDU
ldp-label-mapping
Address and two Label Mappings (prefix FECs, generic labels including implicit null 3)
ldp-label-withdraw
Label Withdraw with the wildcard FEC and a Notification (Shutdown)
The ldp layer is the PDU header (version, pdu_length, lsr_id, label_space) and every message of the PDU (messages). Messages end at the PDU length, so a further PDU in the same TCP segment becomes the next layer (raw).
Messages and TLVs are items of the same shape: a 16-bit type (the U bit, and for TLVs the F bit, are its top bits), a 16-bit length of the value only, and the value. Every message starts with message_id, followed by tlvs.
status (e, f, status_code, message_id, message_type)
0x0400
label_mapping
0x0400
common_hello_parameters (hold_time, t, r, g (GTSM, RFC 6720))
0x0401
label_request
0x0401 / 0x0403
ipv4_transport_address / ipv6_transport_address
0x0402
label_withdraw
0x0402
configuration_sequence_number
0x0403
label_release
0x0500
common_session_parameters (protocol_version, keepalive_time, a, d, pv_lim, max_pdu_length, receiver_lsr_id, receiver_label_space)
0x0404
label_abort_request
0x0600
label_request_message_id
The FEC TLV is decoded as fec_wildcard or fec from the first value octet (the element type), the Address List as IPv4 or IPv6 from the first two (the address family). Other types (ATM / Frame Relay labels and session parameters, vendor TLVs, …) are kept as { type: N, data: "..." }.
The rsvp layer is the common header (version, flags, msg_type, checksum, send_ttl, length) and every object of the message (objects). An object is a 16-bit length (counting the 4-octet header) followed by the Class-Num and C-Type, read together as one 16-bit type (class_num << 8 | c_type; wpg-core TlvDef::len_first). Item names combine the class and the C-Type.
EXPLICIT_ROUTE subobjects are ipv4_prefix, ipv6_prefix, unnumbered_interface and as_number; the names ending in _loose have the L bit (loose hop). RECORD_ROUTE subobjects are ipv4_address, ipv6_address, label and unnumbered_interface (each with flags). ADSPEC, POLICY_DATA, INTEGRITY and other objects keep their value octets.
The token bucket of SENDER_TSPEC / FLOWSPEC (token_bucket_rate, token_bucket_size, peak_data_rate) is given as IEEE 754 single-precision bit patterns in 32-bit integers (1,000,000 octets/s = 0x49742400, infinity = 0x7f800000). The default is 125,000 octets/s (1 Mbit/s).
Path, PathTear and ResvConf are normally sent with the IP Router Alert option (ipv4options: [router_alert: 0]).
The bfd layer is the mandatory section and the optional authentication section (authentication). The flags are the 1-bit fields p, f, c, a, d and m. Authentication types are simple password (simple_password: key_id, password), keyed_md5 / meticulous_keyed_md5 (key_id, sequence_number, a 16-octet digest) and keyed_sha1 / meticulous_keyed_sha1 (a 20-octet hash).
Building blocks for the SRv6 Segment Routing Header (RFC 8754) and SR-MPLS (RFC 8660) (M7). The definitions are in crates/wpg-protocols/src/ipv6.rs (ipv6_srh) and encap.rs (mpls); samples are in templates/protocols/srv6/, templates/protocols/mpls/sr-mpls-entropy and templates/scenarios/sr-mpls-prefix-sid-sweep.
An IPv6 Routing header (next header 43) decodes as ipv6_srh when its routing type (third octet of the header) is 4 and as the generic ipv6_route otherwise. IPv6 next header 143 (Ethernet) maps to ethernet2.
Sample
Contents
srv6-srh-udp
IPv6/UDP with a three-segment SRH (two left); the UDP checksum is computed toward the last segment
srv6-h-encaps-ipv4
IPv4/UDP encapsulated in outer IPv6 + SRH (End then End.DT4) for an L3VPN
srv6-end-dx2-hmac
An Ethernet frame (ARP) over an SRH with an HMAC TLV toward End.DX2 (L2VPN)
sr-mpls-entropy
Prefix SID (SRGB + index as variables), adjacency SID, ELI + entropy label
sr-mpls-prefix-sid-sweep (scenario)
Sweeps the prefix SID index over 1-100 with a per-packet entropy label
Length in 8-octet units, not counting the first 8 octets
routing_type
default 4
segments_left
Manual (default 1)
Index of the next segment to visit
last_entry
Auto
Number of segments - 1
flags / tag
Manual
segment_list
Manual
List of IPv6 addresses in reverse path order: element 0 is the last segment
tlvs
Manual
Pad1 / PadN / HMAC TLVs; the trailing Pad1 padding is Auto
Write segment_list in reverse path order. The IPv6 destination of a packet leaving the source is segment_list[segments_left] (the active segment); set ipv6.dst to it by hand (the IPv6 destination is not Auto).
While segments_left is non-zero, the pseudo header of UDP / TCP / ICMPv6 checksums uses the final destination segment_list[0] instead of ipv6.dst (RFC 8200 §8.1). wpg handles this automatically, including when a generator changes segments_left (fast patch path). With segments_left = 0 it uses ipv6.dst. The generic ipv6_route (types 0 / 2 / 3) keeps its addresses as data octets and does not do this replacement.
After the TLVs, the header is filled with zero octets (= Pad1) up to an 8-octet boundary automatically. The HMAC is not computed (compute it externally for your key and coverage and paste it in).
SR-MPLS SIDs are ordinary MPLS labels, so a stack is built from mpls layers.
Reserved label names: label accepts names for the reserved labels 0-15: ipv4_explicit_null (0), router_alert (1), ipv6_explicit_null (2), implicit_null (3), entropy_label_indicator (7), gal (13), oam_alert (14), extension (15). Other labels are numbers.
Prefix SIDs: label = SRGB base + SID index. With the expr generator and variables, "${srgb_base} + ${sid_index}" follows the routers’ SRGB by changing only the variables (see sr-mpls-entropy). Add idx to reach many egress nodes in turn (see sr-mpls-prefix-sid-sweep).
Entropy labels (RFC 6790): put the entropy label right after an entropy_label_indicator (7) label, with TTL 0.
Notes on PTP (IEEE 1588-2008), SyncE ESMC (ITU-T G.8264), Ethernet CFM / OAM (IEEE 802.1Q clause 21 = 802.1ag, ITU-T Y.1731) with the ERPS R-APS messages (ITU-T G.8032) and EFM OAM (IEEE 802.3 clause 57 = 802.3ah) (M7). The definitions live in crates/wpg-protocols/src/{ptp,esmc,cfm,efm_oam}.rs, the samples in templates/protocols/timing/ and templates/protocols/oam/.
The Slow Protocols EtherType 0x8809 is demultiplexed by its first octet, the subtype: 1 LACP, 2 Marker, 3 OAM, 0x0a ESMC (l2control::slow_protocols_peek; other subtypes decode as raw). Marker PDUs now decode as marker instead of lacp. UDP destination ports are not Auto: set 319 for Sync / Delay_Req / Pdelay_Req / Pdelay_Resp and 320 for the other messages.
Sample
Contents
ptp-sync-two-step-l2
Two-step Sync over Ethernet, 8 per second, incrementing sequence ID
ptp-follow-up-l2
Follow_Up with the precise origin timestamp and a correction
PTP, CFM and 802.3ah OAM follow the IS-IS pattern: one protocol per message (PTP), OpCode (CFM) or code (OAM), plus a generic layer (ptp, cfm, efm_oam: the common header and the rest as data octets). The parser decodes the generic layer first and re-decodes it as the dedicated one from message_type / opcode / code; values without a dedicated definition (CFM Linear APS, MCC, EXM / VSM, …) stay generic. When building, every dedicated protocol sets the EtherType / subtype by itself.
The 34-octet common header has major_sdo_id (transportSpecific), message_type, minor_version, version, message_length (Auto), domain_number, minor_sdo_id, flags (16 bits: two_step is bit 9, unicast bit 10, ptp_timescale bit 3, …), correction_field (signed, nanoseconds × 2^16), message_type_specific, the source port identity (clock_identity, port_number), sequence_id, control_field and log_message_interval (signed). The defaults of control_field and log_message_interval follow the message (0x7f where not applicable).
Every message ends with tlvs (16-bit type, 16-bit length of the value, Auto). They end at message_length, so the zero padding of a short Ethernet frame is not read as TLVs.
esmc is the OSSP header (subtype 0x0a, itu_oui 00-19-a7, itu_subtype 1), version (1), event_flag and the TLVs (8-bit type, 16-bit length including the 3-octet header, Auto): ql (ssm_code: prc, ssu_a, ssu_b, sec, dnu, and the option 2 codes) and extended_ql (enhanced_ssm_code, synce_clock_identity, flags (mixed EEC / eEEC, partial chain), cascaded_eeecs, cascaded_eecs). The padding after the TLVs decodes as one raw item.
The common header is md_level, version, opcode, the flags and first_tlv_offset; the flags are split per OpCode (CCM: rdi, period; LTM / LTR: use_fdb_only, fwd_yes, terminal_mep; AIS / LCK / CSF: period, CSF type; others: flags). first_tlv_offset is constant per OpCode (a warning when changed). The TLVs end with the End TLV (end, type 0); the Ethernet padding after it is padding, not more End TLVs (wpg-core Length::UntilTlv).
TLVs (8-bit type, 16-bit length of the value, Auto): end (0), sender_id (1: chassis_id_length Auto, chassis_id_subtype, chassis_id, management_address octets), port_status (2), data_tlv (3: value), interface_status (4), reply_ingress / reply_egress (5 / 6: action, MAC, port ID octets), ltm_egress_identifier (7), ltr_egress_identifier (8), organization_specific (31), test (32: pattern_type, test_pattern).
R-APS (cfm_raps, OpCode 40) defaults to version 1 (G.8032v2), first_tlv_offset 32 and R-APS(NR, RB). Send it to 01:19:a7:00:00:<ring ID>, usually on the R-APS VLAN at MEL 7.
PTP: the authentication / security TLVs (IEEE 1588-2019 annex P), the data fields of the Management TLVs per management ID, PTP over other transports (annexes C, E-G except IPv6 / Ethernet), state machines and timestamping on transmit.
CFM: the MAID / MEG ID is the 48 octets as sent (not split into name formats), the CRC-32 of the Test TLV is not computed; Linear APS, MCC, EXM / EXR and VSM / VSR have no dedicated definition (generic cfm).
802.3ah OAM: variable descriptors / containers are octets; the OAM discovery state machine.
Notes on MACsec (IEEE 802.1AE, structure only), GTPv2-C (3GPP TS 29.274), Diameter (RFC 6733, Credit-Control RFC 4006), SIP (RFC 3261) and RTP / RTCP (RFC 3550) (M7). The definitions live in crates/wpg-protocols/src/{macsec,gtpv2,diameter,sip,rtp}.rs, the samples in templates/protocols/l2/macsec.wpg.yaml, templates/protocols/mobile/ and templates/protocols/voip/.
The macsec layer is the SecTAG and the protected part of the frame:
Field
Contents
v, es, sc, scb, e, c
TCI bits. sc is Auto: set when sci holds an element
an
Association number (2 bits)
sl
Short length (6 bits): the secure data length when it is below 48 octets, otherwise 0. Manual
pn
Packet number (32 bits)
sci
[] or [{ system_identifier, port_identifier }]
secure_data
Opaque octets: what you write is sent, nothing is encrypted
icv
16 opaque octets: not computed
When parsing, the last 16 octets of the frame are the ICV and everything between the SecTAG and the ICV is the secure data. A frame shorter than the Ethernet minimum is padded after the ICV (as on the wire); parsing it back keeps the padding inside the secure data / ICV. Integrity-only frames (E = C = 0) are not decoded further: the user data stays in secure_data.
The gtpv2 layer is the header (version 2, p, t, mp, message_type, message_length, teid, sequence_number, message_priority) and the IEs (ies).
t is Auto from teid: teid: [0x1001] sends a TEID, teid: [] (Echo) leaves it out.
message_length is Auto (the octets after the first four). The IEs end at that length, so a piggybacked message (p: 1) is the next gtpv2 layer.
An IE is an 8-bit type, a 16-bit length of the IE data, and a spare / instance octet that the length does not count (wpg-core TlvDef::uncounted_octets). Every typed IE has the spare and instance fields first.
fteid / fteid_ipv6 / fteid_ipv4v6 (v4, v6, interface_type, teid, ipv4, ipv6), by the IE length
93
bearer_context (ies: the IEs above, one level deep)
94 / 95
charging_id, charging_characteristics
99
pdn_type
114 / 127 / 128
ue_time_zone, apn_restriction, selection_mode
255
private_extension (enterprise_id, value)
Other IEs are kept as { type: N, data: "..." } (the data starts with the instance octet). GTPv1-C (version 1, also on UDP 2123) is not modelled: such a message decodes as gtpv2 with a version warning.
The diameter layer is the header (version, message_length (Auto), the r / p / e / t flags, command_code, application_id, hop_by_hop, end_to_end) and the AVPs (avps). The AVPs end at the message length, so a second message in the same TCP segment is the next layer (raw).
An AVP is a 32-bit code, the flags octet (between the code and the length: wpg-core TlvDef::head_octets), a 24-bit length counting the header and data but not the padding (Auto), the Vendor-ID when the V flag is set, and the data padded to 4 octets (Auto). Every typed AVP has the fields v (Auto from vendor_id), m, p, reserved, vendor_id ([] or [10415]), then its data:
Vendor-specific AVPs (3GPP and others) whose code is not listed are kept as { type: N, data: "..." } with the flags octet, the Vendor-ID and the value in data. Diameter over SCTP is built by putting the message octets into the user data of an SCTP DATA chunk; SCTP does not decode its user data (see SCTP).
The sip layer is text. start_line and every element of headers are CRLF-terminated lines (wpg-core Length::Until): write them without the CRLF, the builder adds it, and the empty line after the headers. body is the rest of the message (an SDP offer / answer, …).
Content-Length is an ordinary header line and stays Manual: keep it in step with the body (the samples do). A message without the empty line does not decode as sip.
There is no well-known RTP port: the session ports come from SDP. Put the rtp / rtcp_* layer after udp explicitly; when parsing, only the IANA rtp-avp / rtcp-avp ports 5004 / 5005 select RTP / RTCP (tshark needs -d udp.port==N,rtp as well).
rtp is the fixed header, csrc (count cc Auto) and header_extension ([] or [{ profile, data }]: X bit and the length in words Auto, data is a list of 32-bit words; profile 0xBEDE is RFC 8285 one-byte elements). The media payload is the next layer (payload / raw). The padding bit and the padding octets are Manual.
RTCP is one layer per packet, so a compound packet is a stack of RTCP layers. The count (report_count / source_count) and length (32-bit words minus one) are Auto; every packet ends at its length.
Proprietary or in-house protocols that are not built in can be added as YAML files, without writing Rust. They become the same definition IR as the built-ins (wpg_core::ProtocolDef), so building (including Auto lengths and checksums), parsing (wpg import, the tree of the capture page), generators and the GUI builder and preview all work with them.
--protocols-dir <DIR> (repeatable) or the WPG_PROTOCOLS_DIR environment variable
Web UI
After login it fetches the server’s definitions from GET /api/protocols/extensions and adds them to the in-browser catalog (again after a reload)
The *.yaml / *.yml files directly in each directory are loaded in file name order (subdirectories are not read). On an invalid definition the command stops with exit status 2 and prints the file and the reason. wpg config validate checks protocols.dirs too.
wpg serve reads the directories at startup. After adding, changing or removing files, reload them without a restart:
Web UI: Settings → User-defined protocols → Reload.
API: POST /api/protocols/reload (admin session and X-CSRF-Token, like other changes). The answer lists the directories and the files now in use; connected browsers get a protocols_changed event and fetch the definitions again.
The same directories are read again (a changed [protocols] dirs in wpg.toml needs a restart). If a file does not load, the answer is 422 invalid_protocols with details.file / details.reason, and the protocols in use stay as they were. Jobs that are already running keep the definitions they started with.
Every distinct set of definitions that is loaded stays in memory until the server exits (running jobs may still use it); loading a set that was loaded before reuses it. This is small, but restart now and then if you reload very often during development.
Lists the protocols: id, name, layer and source (built-in, or the file of a user-defined one from --protocols-dir)
wpg protocols show <id> [--json]
Prints a definition as an extension file (YAML), a starting point for a new protocol; --json prints the definition IR
wpg protocols check <file|dir>...
Checks extension files without loading them anywhere; exit status 1 on errors
check reads each file, then merges them with the built-in protocols and the other files of --protocols-dir (so files may refer to each other). Errors name the file and the place in it, for example:
my.yaml: protocols[0].fields[2]: missing field `bits` at line 12 column 9
acme.yaml: bind[0]: `udp.dst_port` value 4789 is already bound to `vxlan`
To start from an existing protocol:
wpg protocols show udp > protocols.d/my-proto.yaml
# change `id` (and `name`), remove the `description_key` lines, edit the fields
wpg protocols check protocols.d/my-proto.yaml
A file has protocols (new protocol definitions) and bind (entries added to the next-layer tables of existing protocols); either may be omitted. The repository has an example in examples/protocols.d/acme-telemetry.yaml.
protocols:
- id: acme_tlm # unique id (the name used as `protocol:` in templates)
name: ACME Telemetry # display name
layer: l7 # palette class: l2 / l3 / l4 / l7 / payload
fields: # in wire order
- name: version
type: { kind: uint, bits: 4 }
default: { uint: 1 }
- name: msg_type
type:
kind: enum
bits: 4
variants:
- { value: 1, name: report }
- { value: 2, name: alarm }
default: { uint: 1 }
- name: length
type: { kind: uint, bits: 16 }
auto: { kind: length, of: total } # octets of the header and what follows
- name: readings
type: { kind: bytes, len: { kind: to_end } }
bind:
- under: udp # lower protocol
value: 47000 # value of its selector field (UDP: dst_port, and src_port when parsing)
protocol: acme_tlm
Each definition has the shape of the JSON returned by GET /api/protocols/{id}; wpg protocols show <id> prints an existing protocol in this format as a starting point.
A template is a YAML file (.wpg.yaml; JSON with .wpg.json is equivalent) that describes one stream: a layer stack, optional generators that vary fields, and a transmit profile. The machine-readable definition is docs/schema/template.schema.json; print it with wpg template schema. Unknown keys are errors, so typos are caught. Check a file with wpg template lint FILE.
Template arguments. Each entry has name, optional default (string, number or bool) and description. Use ${name} anywhere inside a field value or generator. Override at run time with --var name=value (wpg send, wpg render), or in the GUI. A variable with no default and no override is an error, and so is a ${…} that is not declared. Declared but unused variables give a warning.
Optional keys constrain the value. Without them any text is accepted:
Key
Meaning
type
uint, int, bool, string, ipv4, ipv6, mac or bytes; values use the same notation as fields (0x0800, 0200.0000.0001)
choices
the allowed values
min, max
inclusive range, for uint and int
required
a value must be given at run time; the variable has no default
wpg template lint checks the default and the choices. A run-time value that does not fit (--var, or vars in the job and render API) is a template error that names the variable and what was expected: the CLI exits with status 3 and the server answers 422 invalid_template. A bool accepts true, false, 1 and 0 and is substituted as 1 or 0. The job form in the GUI shows a matching input for each variable.
Fields you write are Manual; fields you omit (or set to auto) keep their default or are computed (lengths, checksums, next-protocol numbers). Values use human notation:
Type
Examples
integer
64, 0x0800, 0b101
MAC / IPv4 / IPv6
02:00:00:00:00:01, 192.0.2.1, 2001:db8::1 (quote MACs made only of digits, such as "00:11:22:33:44:55", because YAML can read them as numbers)
bytes
"00 01 02 03" (always quoted); text as '"text"' with \r, \n, \t escapes
text line (SIP lines, CRLF-terminated)
start_line: '"INVITE sip:bob@example.com SIP/2.0"' (the terminator is added automatically)
enum
a number or a variant name: ethertype: arp, oper: request, type: echo_request
nlri: "18 c6 33 64" (a quoted hex string instead of the list; written by the import for content it cannot decode)
Deliberately wrong values are accepted: a Manual length or checksum is used as written, and wpg template lint reports the mismatch as a warning (diag-*). That is how the samples in templates/malformed/ are made.
An expr generator computes an integer for every packet. The result is written into the field like any other value, so it can fill numbers, IPv4 / IPv6 addresses and MAC addresses.
Element
Meaning
12, 0x1f, 0b101
literals (decimal, hexadecimal, binary)
idx
the packet index in the stream (0, 1, 2, …)
$field, $<layer>.field
this packet’s value of another generated field; <layer> is written as in stream.order ($0.src and $ethernet2.src both name the src generator of the first layer; $ipv4[1].src is the second ipv4)
+ - * / %, << >>, & ^ |, unary - ~
C-like operators and precedence; integer arithmetic (128-bit, wrapping)
hash(x), crc(x)
FNV-1a 64 / CRC-32 of x (as 8 big-endian octets): spreads sequential values
rand(min, max)
a random value between min and max, fixed per packet index and stream.seed
min(a, ...), max(a, ...)
smallest / largest argument
A reference can only name a field that has a generator in the same template; fixed field values are not visible (put them in a variable and write ${name} instead, which is substituted before the expression is parsed). An expr can read another expr only if that one comes earlier in the template. Examples from the bundled library:
generators:
chaddr: { kind: expr, expr: "$0.src" } # copy the Ethernet source MAC (dhcp-discover-flood)
xid: { kind: expr, expr: "rand(0, 0xffffffff)" } # a reproducible random transaction ID
label: { kind: expr, expr: "${srgb_base} + ${first_index} + idx" } # SR-MPLS prefix SIDs (sr-mpls-prefix-sid-sweep)
How generators combine and how long the stream is.
Key
Meaning
mode
zip (default, lockstep), product (last varies fastest) or nested
order
for nested: fields from slowest to fastest as <layer>.<field>, where <layer> is a 0-based index, a protocol id unique in the stack (dot1q.vid, arp.tpa), or <protocol>[<n>] for the n-th (0-based) occurrence of a protocol that appears more than once (ipv4[1].src is the inner IPv4 of a tunnel)
count
number of packets; default is tx.count, else the natural length of the generators
seed
seed for random generators; makes the stream reproducible
Unbounded generators (a counter without wrap) need a count and cannot be in the inner position of product or nested.
wpg send, wpg render, wpg template show and wpg bench --template change a template for one run without editing the file:
Option
Effect
--vars-file FILE
Variable values from a YAML or JSON map (dst_ip: 10.1.2.3).
--var NAME=VALUE
One variable value (repeatable). Wins over --vars-file, which wins over the template’s default.
--set LAYER.FIELD=VALUE
Fixes a field to a value, written as in fields:. The field becomes Manual and loses any generator.
--gen LAYER.FIELD=GEN
Gives a field a generator and drops its fixed value.
LAYER is a protocol id (its first layer: ipv4), the n-th layer of a protocol counting from 0 (ipv4[1] is the inner header of IP-in-IP) or a 0-based layer index (2). FIELD is a field name from the protocol reference. An unknown layer or field stops with exit status 3 and lists the template’s layers or the closest field names. Every variable in --vars-file must be declared in the template, as with --var.
GEN is one of:
Shorthand
Generator
range:A..B, range:A..B/STEP
range from A to B inclusive
list:a,b,c
list
random, random:MIN..MAX
random
counter, counter:START, counter:START/STEP
counter
expr:EXPR
expr
anything else
the shorthand of generators:: 10.0.0.1-10.0.0.9, 10.0.0.0/24, 1,5,9, 00:11:22:*
payload is not a wire protocol; its fields control generation: mode (fixed, pattern, random, counter), data, length, seed, and for counter the counter_offset, counter_width (u32 or u64) and counter_value (usually fed by a counter generator). raw places bytes as given.
templates/ is loaded as a library by default and is checked by crates/wpg-template/tests/samples.rs:
Folder
Contents
examples/
five small starting points
protocols/l2, l3, l4, payload, frame
one or more samples for every protocol in the catalog, plus jumbo, minimum-size, runt and FCS cases
scenarios/
ready-made tests: ARP sweeps, SYN flood and port scan, QinQ, NDP and RA, ping and TTL sweeps, QoS matrix, MTU probe, frame-size sweep, MAC flood, SR-MPLS prefix SID sweep, DHCP DISCOVER load
interop/
interop checks against real devices and FRR: OSPFv2 2-Way, RIPv2 routes, IS-IS adjacency Up, VRRPv3 preemption, PIM neighbor, LDP discovery, BFD session Up (interop tag; procedure in the operations guide, “Interop checks”)
malformed/
deliberately broken packets (bad checksums, wrong lengths, truncation, bad FCS); they lint with warnings by design and are tagged malformed
Generated from the JSON Schema of the template format (wpg template schema); the descriptions are the doc comments of the code (English). Template is the document root.
TLV: a list of items, each a bare variant name (nop), a one-key map
({ mss: 1460 }, { timestamps: { tsval: 1, tsecr: 0 } }) or the explicit form
{ type: 99, data: "01 02" } / { name: mss, length: 4, fields: { value: 1 } };
list: a list of scalars, or of maps for lists of field groups.
For nested: fields from the slowest to the fastest, as <layer>.<field> where <layer> is a 0-based index, a protocol id (ipv4.src) or the n-th occurrence of a protocol (ipv4[1].src).
Manually set fields in human notation (192.168.1.1, 0x0800, syn|ack, ${src_ip}). Unlisted fields use their default or Auto value; the literal auto also leaves a field Auto. TLV / list fields take structured values.
wpg-server serves a REST (JSON) API under /api and pushes live statistics, job state changes and logs over a WebSocket (GET /api/ws). This page is generated from the OpenAPI document of the server (utoipa). A running server serves the same document at /api/openapi.json with Swagger UI at /api/docs/, and a copy is kept in docs/schema/openapi.json. Log in with POST /api/auth/login (or POST /api/auth/setup on first run); the wpg_session cookie then authenticates every request, and POST / PUT / DELETE must also send the CSRF token of GET /api/auth/status in the X-CSRF-Token header. Errors are a JSON object error with code, message (in the language of Accept-Language) and details. The endpoint and schema descriptions below come from the code.
Logs in (rate limited per client address: 5 failures a minute, then an exponentially growing lock-out up to 15 minutes). Requires Content-Type: application/json.
First-run setup: sets the admin password and logs in. Only while no password is configured, and only from a loopback client unless the server runs with --allow-insecure. Requires Content-Type: application/json.
The user-defined protocol files in use ([protocols] dirs, --protocols-dir; loaded at startup or by POST /api/protocols/reload): their definitions and bindings, so that the web UI can add them to its in-browser catalog. Empty without extensions.
Reads the user-defined protocol directories of the server again ([protocols] dirs, --protocols-dir) and switches the catalog to what they hold now; WebSocket clients get a protocols_changed event. Jobs that are already running keep the catalog they started with. On an error nothing changes. Admin only.
The full definition (wpg_core::ProtocolDef): fields with types, defaults, auto computations and bindings. Every description_key gets a localised description next to it.
Opens a terminal session (WebSocket). Send connect first; see TerminalClientMessage / TerminalServerMessage. Remote output arrives as binary frames, keystrokes may be sent as binary frames or input messages.
Logs in (rate limited per client address: 5 failures a minute, then an exponentially growing lock-out up to 15 minutes). Requires Content-Type: application/json.
First-run setup: sets the admin password and logs in. Only while no password is configured, and only from a loopback client unless the server runs with --allow-insecure. Requires Content-Type: application/json.
The user-defined protocol files in use ([protocols] dirs, --protocols-dir; loaded at startup or by POST /api/protocols/reload): their definitions and bindings, so that the web UI can add them to its in-browser catalog. Empty without extensions.
Reads the user-defined protocol directories of the server again ([protocols] dirs, --protocols-dir) and switches the catalog to what they hold now; WebSocket clients get a protocols_changed event. Jobs that are already running keep the catalog they started with. On an error nothing changes. Admin only.
The full definition (wpg_core::ProtocolDef): fields with types, defaults, auto computations and bindings. Every description_key gets a localised description next to it.
Opens a terminal session (WebSocket). Send connect first; see TerminalClientMessage / TerminalServerMessage. Remote output arrives as binary frames, keystrokes may be sent as binary frames or input messages.
Rate: 1000pps, 1Mpps, 10Gbps, 50%, ifg=10us, unlimited (default: the first template’s tx.rate, then tx.max_rate, else unlimited).
signature
boolean
Stamp every frame with a receive-verification signature (magic + stream id + sequence number + TX time over the last 24 payload octets; see /api/captures with verify).
start_at
string | null
Scheduled start: RFC 3339 or +10s.
stream_id
integer | null
Signature stream id of the first stream (stream i gets stream_id + i; default 0).
SSH jump hosts (ProxyJump), nearest first: the server logs in to each in turn and reaches the next one (and finally host) through a direct-tcpip channel. Works for SSH and Telnet targets.
passphrase
string | null
Passphrase of private_key.
password
string | null
SSH password (also answers a single keyboard-interactive password prompt).
Progress: resolving, connecting, authenticating; with jump hosts,
jumping (opening the tunnel from host onwards) and the other phases
carry the host they concern.
Key
Type
Required
Description
host
string | null
The host of this phase (sessions with jump hosts).
Object instance: name (sysName.0, IF-MIB::ifAdminStatus.2) or dotted OID.
type
string | null
auto (default: from the MIB), or i / integer, u / unsigned, c / counter32, C / counter64, t / timeticks, a / ipaddress, o / oid, s / string, x / hex, d / decimal, n / null, opaque.
value
string
Value text (enum labels such as down and object names are accepted).
The wpg binary runs both the server (wpg serve) and the headless tools, so it suits CI and ssh sessions too. For example, wpg send tpl.wpg.yaml -i eth0 --rate 1Mpps transmits a template. This page is generated from the command definitions: each block is the verbatim --help output of the current build. Run-time messages follow LANG / LC_ALL; the --help text itself is English.
Web Packet Generator
Usage: wpg [OPTIONS] <COMMAND>
Commands:
serve Start the web UI / API server
send Send a template headlessly
render Render a template to a pcapng file without transmitting
import Convert a pcap/pcapng capture into a template
template Template tools (lint, list, migrate, export, import)
ifaces List network interfaces usable for transmission
doctor Diagnose privileges, drivers, hugepages and Npcap
bench Measure transmit performance
capture Receive: counters, the first frames, pcapng output, signature verification
config Configuration file tools (init, show, validate) and API tokens
remote Drive a running server with an API token (jobs, templates)
protocols Protocol tools (list, show, check user-defined protocol files)
help Print this message or the help of the given subcommand(s)
Options:
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
-h, --help
Print help
-V, --version
Print version
Start the web UI / API server
Usage: wpg serve [OPTIONS]
Options:
--bind <BIND>
Address to listen on (default from the config: 127.0.0.1:8080). Without a password only loopback addresses are accepted (see --allow-insecure)
[env: WPG_BIND]
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
--password <PASSWORD>
Admin password (plain text or argon2 hash). Visible in the process list: prefer --password-file, `WPG_PASSWORD` or `wpg config set-password`
--password-file <FILE>
File holding the admin password (plain text or argon2 hash)
--tls
Serve HTTPS; without `[server.tls]` a self-signed certificate is generated into `<data_dir>/tls/`
--allow-insecure
Listen on a non-loopback address even though no password is configured (anyone reaching the server can then set it on the setup screen)
--dev
Development mode: allow CORS from the Trunk dev server (:8081)
--library-path <DIR>
Template library directory (repeatable; replaces `templates.library_paths`)
--data-dir <DIR>
Data directory (user templates, history)
-h, --help
Print help
-V, --version
Print version
Sends a template. --plan chooses between expanding all frames up front (prerender) and building them while sending (onthefly). --json prints the final report on stdout.
Send a template headlessly
Usage: wpg send [OPTIONS] <TEMPLATE>
Arguments:
<TEMPLATE>
Template file (`.wpg.yaml`)
Options:
-i, --iface <IFACE>
Interface to transmit on
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--backend <BACKEND>
Transmit backend (null, pcapfile, pcap, afpacket, afxdp, dpdk)
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
--count <COUNT>
Number of packets
--duration <DURATION>
Duration limit, e.g. `10s`, `2m`
--rate <RATE>
Rate, e.g. `1Mpps`, `10Gbps`, `50%`, `ifg=10us`, `unlimited`
--burst <BURST>
Frames sent back to back per rate-control grant
--wait <MODE>
How the rate control waits: hybrid (sleep, then spin), sleep, spin or busy-poll (spin and service the backend)
--clock <CLOCK>
Rate-control clock: auto (TSC when invariant), tsc or monotonic
[default: auto]
--ramp-up <DURATION>
Ramp the rate up linearly from 0 over this time, e.g. `5s`
--start-at <TIME>
Start at this time: RFC 3339 (`2026-09-29T12:00:00Z`) or a delay (`+10s`)
--plan <PLAN>
Expansion strategy: auto, prerender or onthefly
[default: auto]
--prerender-max-bytes <BYTES>
`auto` pre-renders only streams up to this many bytes (default: `tx.prerender_max_bytes`, 256 MiB)
--prerender-max-packets <N>
`auto` pre-renders only streams up to this many packets (default: `tx.prerender_max_packets`, 100M)
--json
Print the final report as JSON on stdout
--var <NAME=VALUE>
Template variable override (`name=value`), repeatable; wins over --vars-file
--vars-file <FILE>
YAML or JSON map of template variable values
--set <LAYER.FIELD=VALUE>
Set a field to a fixed value, repeatable. LAYER is a protocol id, `proto[n]` or a layer index, e.g. `ipv4.ttl=1`
--gen <LAYER.FIELD=GEN>
Vary a field with a generator, repeatable, e.g. `udp.src_port=range:1000..1999`, `ipv4.dst=list:10.0.0.1,10.0.0.2`, `ipv4.id=random`
--signature
Stamp every frame with a receive-verification signature (magic, stream id, sequence number, TX time over the last 24 payload octets; see `wpg capture --verify`)
--stream-id <ID>
Signature stream id (with `--signature`)
[default: 0]
--batch <N>
Frames per `send_batch` call
--ring-depth <N>
TX ring depth in frames (`afpacket`)
--ring-bytes <BYTES>
Memory of one TX ring / UMEM per worker in bytes (`afpacket`, `afxdp`; default: `tx.ring_bytes`, 16 MiB)
--pin-core <CORE>
Pin the TX thread to this CPU core (Linux; the first worker)
--pin-cores <CORES>
Pin worker i to the i-th core of this list, e.g. `1,2` (Linux)
--workers <N>
TX worker threads, each with its own backend instance (afpacket: socket and ring) and share of the stream and rate
[default: 1]
--qdisc-bypass
Bypass the qdisc layer (`afpacket`, `PACKET_QDISC_BYPASS`)
--no-tx-ring
Use `sendmmsg` instead of the TPACKET_V3 TX ring (`afpacket`)
--auto-pin
Pin the workers to cores on the NIC's NUMA node, core 0 last (Linux; ignored when --pin-cores is given)
--queue <N>
First hardware queue (`afxdp`, `dpdk`; worker i uses queue N+i)
--xdp-mode <MODE>
AF_XDP bind mode: auto (zero-copy, else copy), copy or zerocopy
[default: auto]
--busy-poll
Busy polling: `afxdp` sockets busy-poll the driver (kernel 5.11+) and `wpg send` paces with `--wait busy-poll` unless --wait says otherwise
--eal-args <ARGS>
DPDK EAL arguments (`dpdk`), e.g. "-a 0000:00:04.0 --file-prefix wpg" (split on whitespace; wpg adds `-l <first CPU>` and `--in-memory` unless given)
-h, --help
Print help
-V, --version
Print version
Writes the frames of a template to a pcapng file without transmitting.
Render a template to a pcapng file without transmitting
Usage: wpg render [OPTIONS] --output <OUTPUT> <TEMPLATE>
Arguments:
<TEMPLATE>
Template file
Options:
-o, --output <OUTPUT>
Output pcapng file (`-` writes it to standard output)
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
--count <COUNT>
Number of packets
--var <NAME=VALUE>
Template variable override (`name=value`), repeatable; wins over --vars-file
--vars-file <FILE>
YAML or JSON map of template variable values
--set <LAYER.FIELD=VALUE>
Set a field to a fixed value, repeatable. LAYER is a protocol id, `proto[n]` or a layer index, e.g. `ipv4.ttl=1`
--gen <LAYER.FIELD=GEN>
Vary a field with a generator, repeatable, e.g. `udp.src_port=range:1000..1999`, `ipv4.dst=list:10.0.0.1,10.0.0.2`, `ipv4.id=random`
--json
Print the result (packets, bytes, path) as JSON on stdout
-h, --help
Print help
-V, --version
Print version
Convert a pcap/pcapng capture into a template
Usage: wpg import [OPTIONS] <INPUT>
Arguments:
<INPUT>
Input: a `.pcap` / `.pcapng` capture, a hex dump (`.hex`, see `--hex`) or `-` for standard input
Options:
-o, --output <OUTPUT>
Output template (`-` prints YAML on standard output)
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
--per-packet <DIR>
Write one template per packet into this directory instead of one merged template
--first-protocol <PROTOCOL>
Protocol the frames start with
[default: ethernet2]
--max-packets <N>
Read at most this many packets
--no-detect-generators
Do not infer generators (range / list / random) from the packets
--hex
Treat the input as pasted hex (Wireshark hex stream, spaced bytes, hexdump)
-h, --help
Print help
-V, --version
Print version
Template tools. lint exits with status 1 on errors; export and import pack templates into, and unpack them from, .wpgbundle archives.
Template tools (lint, list, migrate, export, import)
Usage: wpg template [OPTIONS] <COMMAND>
Commands:
lint Check templates for errors and warnings (exit status 3 on errors)
list List the templates of a directory (default: `templates/`)
show Print the built packet (hex dump and layout tree) of a template
schema Print the JSON Schema of the template format
export Pack templates (files or ids) into a `.wpgbundle` archive
migrate Upgrade templates to the current schema version
import Extract the templates of a `.wpgbundle` archive into a directory
help Print this message or the help of the given subcommand(s)
Options:
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
-h, --help
Print help
-V, --version
Print version
Check templates for errors and warnings (exit status 3 on errors)
Usage: wpg template lint [OPTIONS] <FILES>...
Arguments:
<FILES>...
Template files
Options:
--json
Print the issues as JSON on stdout
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
-h, --help
Print help
-V, --version
Print version
List the templates of a directory (default: `templates/`)
Usage: wpg template list [OPTIONS] [DIR]
Arguments:
[DIR]
Directory to scan recursively
Options:
--json
Print the list as JSON on stdout
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
-h, --help
Print help
-V, --version
Print version
Print the built packet (hex dump and layout tree) of a template
Usage: wpg template show [OPTIONS] <FILE>
Arguments:
<FILE>
Template file
Options:
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--var <NAME=VALUE>
Template variable override (`name=value`), repeatable; wins over --vars-file
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
--vars-file <FILE>
YAML or JSON map of template variable values
--set <LAYER.FIELD=VALUE>
Set a field to a fixed value, repeatable. LAYER is a protocol id, `proto[n]` or a layer index, e.g. `ipv4.ttl=1`
--gen <LAYER.FIELD=GEN>
Vary a field with a generator, repeatable, e.g. `udp.src_port=range:1000..1999`, `ipv4.dst=list:10.0.0.1,10.0.0.2`, `ipv4.id=random`
--json
Print the packet (hex, layers, fields) as JSON on stdout
-h, --help
Print help
-V, --version
Print version
Print the JSON Schema of the template format
Usage: wpg template schema [OPTIONS]
Options:
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
-h, --help
Print help
-V, --version
Print version
Pack templates (files or ids) into a `.wpgbundle` archive
Usage: wpg template export [OPTIONS] --output <OUTPUT> <TEMPLATES>...
Arguments:
<TEMPLATES>...
Template files, or ids (file stem / name slug) looked up in `--dir`
Options:
-o, --output <OUTPUT>
Output bundle
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--attach <FILE>
Attach a capture (pcap / pcapng) or other file, repeatable
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
--dir <DIR>
Template library used to resolve ids (default: `templates/`)
-h, --help
Print help
-V, --version
Print version
Upgrade templates to the current schema version.
Directories are scanned recursively. The original of a rewritten file is kept as `<file>.bak`; the new file loses its comments. Files already at the current version are not touched.
Usage: wpg template migrate [OPTIONS] <PATHS>...
Arguments:
<PATHS>...
Template files or directories
Options:
--check
Only report outdated templates (exit status 1 if any needs migrating)
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
--no-backup
Do not keep a `.bak` copy of migrated files
-h, --help
Print help (see a summary with '-h')
-V, --version
Print version
Extract the templates of a `.wpgbundle` archive into a directory
Usage: wpg template import [OPTIONS] --output <OUTPUT> <BUNDLE>
Arguments:
<BUNDLE>
The bundle
Options:
-o, --output <OUTPUT>
Output directory
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
--force
Overwrite existing files
-h, --help
Print help
-V, --version
Print version
Lists interfaces with MAC, addresses, MTU, link state and driver.
List network interfaces usable for transmission
Usage: wpg ifaces [OPTIONS]
Options:
--json
Output JSON
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
-h, --help
Print help
-V, --version
Print version
Runs the environment checks described in Permissions; the exit status is non-zero when a check fails.
Diagnose privileges, drivers, hugepages and Npcap
Usage: wpg doctor [OPTIONS]
Options:
--json
Output JSON
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
-h, --help
Print help
-V, --version
Print version
Unlimited-rate transmission of a synthetic frame (EtherType 0x88b5), or of a template with --template. See docs/perf.md for baselines. --json output fields: backend, detail, iface, frame_size, batch_size, packets, dropped, pps, bps (L2), l1_bps, elapsed.
Measure transmit performance
Usage: wpg bench [OPTIONS]
Options:
--backend <BACKEND>
Backend to benchmark
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
-i, --iface <IFACE>
Interface (for real backends)
--duration <DURATION>
Duration in seconds
[default: 5]
--frame-size <FRAME_SIZE>
Frame size in bytes (without FCS)
[default: 64]
--template <FILE>
Benchmark on-the-fly generation of this template instead of a fixed frame
--var <NAME=VALUE>
Template variable override (`name=value`), repeatable; wins over --vars-file
--vars-file <FILE>
YAML or JSON map of template variable values
--set <LAYER.FIELD=VALUE>
Set a field to a fixed value, repeatable. LAYER is a protocol id, `proto[n]` or a layer index, e.g. `ipv4.ttl=1`
--gen <LAYER.FIELD=GEN>
Vary a field with a generator, repeatable, e.g. `udp.src_port=range:1000..1999`, `ipv4.dst=list:10.0.0.1,10.0.0.2`, `ipv4.id=random`
--json
Output JSON
--batch <N>
Frames per `send_batch` call
--ring-depth <N>
TX ring depth in frames (`afpacket`)
--ring-bytes <BYTES>
Memory of one TX ring / UMEM per worker in bytes (`afpacket`, `afxdp`; default: `tx.ring_bytes`, 16 MiB)
--pin-core <CORE>
Pin the TX thread to this CPU core (Linux; the first worker)
--pin-cores <CORES>
Pin worker i to the i-th core of this list, e.g. `1,2` (Linux)
--workers <N>
TX worker threads, each with its own backend instance (afpacket: socket and ring) and share of the stream and rate
[default: 1]
--qdisc-bypass
Bypass the qdisc layer (`afpacket`, `PACKET_QDISC_BYPASS`)
--no-tx-ring
Use `sendmmsg` instead of the TPACKET_V3 TX ring (`afpacket`)
--auto-pin
Pin the workers to cores on the NIC's NUMA node, core 0 last (Linux; ignored when --pin-cores is given)
--queue <N>
First hardware queue (`afxdp`, `dpdk`; worker i uses queue N+i)
--xdp-mode <MODE>
AF_XDP bind mode: auto (zero-copy, else copy), copy or zerocopy
[default: auto]
--busy-poll
Busy polling: `afxdp` sockets busy-poll the driver (kernel 5.11+) and `wpg send` paces with `--wait busy-poll` unless --wait says otherwise
--eal-args <ARGS>
DPDK EAL arguments (`dpdk`), e.g. "-a 0000:00:04.0 --file-prefix wpg" (split on whitespace; wpg adds `-l <first CPU>` and `--in-memory` unless given)
-h, --help
Print help
-V, --version
Print version
Receive: counters, the first frames, pcapng output, signature verification
Usage: wpg capture [OPTIONS]
Options:
-i, --iface <IFACE>
Interface to receive on (`any` = every interface with `afpacket`); for the `pcapfile` backend the capture file to replay
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--backend <BACKEND>
Receive backend: afpacket (Linux), pcap (feature `pcap`), pcapfile, null
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
-f, --filter <EXPR>
tcpdump-style filter, e.g. `udp port 5001 and wpg` (default: every frame)
-c, --count <COUNT>
Stop after this many matching frames
--duration <DURATION>
Stop after this long, e.g. `10s`
--show <N>
Print the first N matching frames
[default: 5]
--hex
Also print a hex dump of the shown frames
-w, --write <FILE>
Write the matching frames to this pcapng file
--verify
Check the signatures stamped by `wpg send --signature` (loss / order / latency)
--promisc
Promiscuous mode
--include-outgoing
Also count frames this host transmits
--json
Machine-readable output
-h, --help
Print help
-V, --version
Print version
Configuration file tools (init, show, validate) and API tokens
Usage: wpg config [OPTIONS] <COMMAND>
Commands:
init Write a fully commented example `wpg.toml`
show Print the effective configuration (file + environment) as TOML, secrets redacted
validate Check the configuration; exit status 1 on errors
set-password Prompt for the admin password and store its argon2id hash as `auth.password_hash` (the file is created from the example if missing)
token API tokens for scripts and `wpg remote` (create, list, revoke), stored in the server's database
help Print this message or the help of the given subcommand(s)
Options:
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
-h, --help
Print help
-V, --version
Print version
Write a fully commented example `wpg.toml`
Usage: wpg config init [OPTIONS]
Options:
--path <FILE>
Output file
[default: wpg.toml]
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
--force
Overwrite an existing file
-h, --help
Print help
-V, --version
Print version
Check the configuration; exit status 1 on errors
Usage: wpg config validate [OPTIONS]
Options:
--config <FILE>
Configuration file (default: `wpg.toml` if present)
[env: WPG_CONFIG]
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
-h, --help
Print help
-V, --version
Print version
Prompt for the admin password and store its argon2id hash as `auth.password_hash` (the file is created from the example if missing)
Usage: wpg config set-password [OPTIONS]
Options:
--config <FILE>
Configuration file to update
[env: WPG_CONFIG]
[default: wpg.toml]
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--stdin
Read the password from the first line of standard input (no prompt)
-h, --help
Print help
-V, --version
Print version
Manages API tokens directly in the server’s database (<data_dir>/wpg.sqlite), also while the server runs. create prints only the secret on standard output.
API tokens for scripts and `wpg remote` (create, list, revoke), stored in the server's database
Usage: wpg config token [OPTIONS] <COMMAND>
Commands:
create Create a token and print its secret (shown only once)
list List the tokens (never their secrets)
revoke Revoke (delete) a token by id
help Print this message or the help of the given subcommand(s)
Options:
--data-dir <DIR>
Data directory holding `wpg.sqlite` (default: `data_dir` of the configuration)
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
-h, --help
Print help
-V, --version
Print version
Create a token and print its secret (shown only once)
Usage: wpg config token create [OPTIONS] <NAME>
Arguments:
<NAME>
Name, to recognise the token later
Options:
--data-dir <DIR>
Data directory holding `wpg.sqlite` (default: `data_dir` of the configuration)
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--user <USER>
User the token acts as (with that user's role)
[default: admin]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
--expires-days <DAYS>
Expire after this many days (default: never)
--json
Print the token and its secret as JSON
-h, --help
Print help
-V, --version
Print version
List the tokens (never their secrets)
Usage: wpg config token list [OPTIONS]
Options:
--data-dir <DIR>
Data directory holding `wpg.sqlite` (default: `data_dir` of the configuration)
--json
Print JSON
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
-h, --help
Print help
-V, --version
Print version
Revoke (delete) a token by id
Usage: wpg config token revoke [OPTIONS] <ID>
Arguments:
<ID>
Token id (see `wpg config token list`)
Options:
--data-dir <DIR>
Data directory holding `wpg.sqlite` (default: `data_dir` of the configuration)
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
-h, --help
Print help
-V, --version
Print version
Drives a running server through its API with an API token; see Authentication and TLS. Exit status: 0 success, 1 the job failed (jobs wait, jobs start --wait), 2 any other error.
Drive a running server with an API token (jobs, templates)
Usage: wpg remote [OPTIONS] <COMMAND>
Commands:
jobs Transmit jobs: list, start, stop, wait
templates The server's template library: list, push
help Print this message or the help of the given subcommand(s)
Options:
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--server <URL>
Server URL
[env: WPG_SERVER]
[default: http://127.0.0.1:8080]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
--token <TOKEN>
API token (`wpg_...`). Visible in the process list: prefer `WPG_TOKEN`
[env: WPG_TOKEN]
--insecure
Accept any TLS certificate (for servers with a self-signed certificate)
--json
Print the server's JSON responses
-h, --help
Print help
-V, --version
Print version
Transmit jobs: list, start, stop, wait
Usage: wpg remote jobs [OPTIONS] <COMMAND>
Commands:
list List the server's jobs (running and recently finished)
start Start a job from a library template id or a local template file; prints the job id
stop Stop a job
wait Wait until a job ends; exit status 1 if it failed
help Print this message or the help of the given subcommand(s)
Options:
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--server <URL>
Server URL
[env: WPG_SERVER]
[default: http://127.0.0.1:8080]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
--token <TOKEN>
API token (`wpg_...`). Visible in the process list: prefer `WPG_TOKEN`
[env: WPG_TOKEN]
--insecure
Accept any TLS certificate (for servers with a self-signed certificate)
--json
Print the server's JSON responses
-h, --help
Print help
-V, --version
Print version
List the server's jobs (running and recently finished)
Usage: wpg remote jobs list [OPTIONS]
Options:
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--server <URL>
Server URL
[env: WPG_SERVER]
[default: http://127.0.0.1:8080]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
--token <TOKEN>
API token (`wpg_...`). Visible in the process list: prefer `WPG_TOKEN`
[env: WPG_TOKEN]
--insecure
Accept any TLS certificate (for servers with a self-signed certificate)
--json
Print the server's JSON responses
-h, --help
Print help
-V, --version
Print version
Start a job from a library template id or a local template file; prints the job id
Usage: wpg remote jobs start [OPTIONS] <TEMPLATE>
Arguments:
<TEMPLATE>
Template id on the server, or a local `.wpg.yaml` file sent with the job
Options:
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--server <URL>
Server URL
[env: WPG_SERVER]
[default: http://127.0.0.1:8080]
--var <NAME=VALUE>
Template variable value (repeatable)
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
--rate <RATE>
Rate, e.g. `1000pps`, `1Mpps`, `10Gbps`, `unlimited`
--token <TOKEN>
API token (`wpg_...`). Visible in the process list: prefer `WPG_TOKEN`
[env: WPG_TOKEN]
--count <COUNT>
Number of packets
--insecure
Accept any TLS certificate (for servers with a self-signed certificate)
--duration <DURATION>
Duration limit, e.g. `10s`, `2m`
--json
Print the server's JSON responses
-i, --iface <IFACE>
Interface on the server
--backend <BACKEND>
Transmit backend (default: the server's)
--name <NAME>
Job name
--wait
Wait until the job ends (like `wpg remote jobs wait`)
-h, --help
Print help
-V, --version
Print version
Stop a job
Usage: wpg remote jobs stop [OPTIONS] <ID>
Arguments:
<ID>
Job id
Options:
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--server <URL>
Server URL
[env: WPG_SERVER]
[default: http://127.0.0.1:8080]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
--token <TOKEN>
API token (`wpg_...`). Visible in the process list: prefer `WPG_TOKEN`
[env: WPG_TOKEN]
--insecure
Accept any TLS certificate (for servers with a self-signed certificate)
--json
Print the server's JSON responses
-h, --help
Print help
-V, --version
Print version
Wait until a job ends; exit status 1 if it failed
Usage: wpg remote jobs wait [OPTIONS] <ID>
Arguments:
<ID>
Job id
Options:
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--server <URL>
Server URL
[env: WPG_SERVER]
[default: http://127.0.0.1:8080]
--timeout <SECS>
Give up after this many seconds (exit status 2)
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
--token <TOKEN>
API token (`wpg_...`). Visible in the process list: prefer `WPG_TOKEN`
[env: WPG_TOKEN]
--insecure
Accept any TLS certificate (for servers with a self-signed certificate)
--json
Print the server's JSON responses
-h, --help
Print help
-V, --version
Print version
The server's template library: list, push
Usage: wpg remote templates [OPTIONS] <COMMAND>
Commands:
list List the templates on the server
push Upload a template file to the server's user templates; prints its id
help Print this message or the help of the given subcommand(s)
Options:
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--server <URL>
Server URL
[env: WPG_SERVER]
[default: http://127.0.0.1:8080]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
--token <TOKEN>
API token (`wpg_...`). Visible in the process list: prefer `WPG_TOKEN`
[env: WPG_TOKEN]
--insecure
Accept any TLS certificate (for servers with a self-signed certificate)
--json
Print the server's JSON responses
-h, --help
Print help
-V, --version
Print version
List the templates on the server
Usage: wpg remote templates list [OPTIONS]
Options:
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--server <URL>
Server URL
[env: WPG_SERVER]
[default: http://127.0.0.1:8080]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
--token <TOKEN>
API token (`wpg_...`). Visible in the process list: prefer `WPG_TOKEN`
[env: WPG_TOKEN]
--insecure
Accept any TLS certificate (for servers with a self-signed certificate)
--json
Print the server's JSON responses
-h, --help
Print help
-V, --version
Print version
Upload a template file to the server's user templates; prints its id
Usage: wpg remote templates push [OPTIONS] <FILE>
Arguments:
<FILE>
Template file (`.wpg.yaml`)
Options:
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--server <URL>
Server URL
[env: WPG_SERVER]
[default: http://127.0.0.1:8080]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
--token <TOKEN>
API token (`wpg_...`). Visible in the process list: prefer `WPG_TOKEN`
[env: WPG_TOKEN]
--insecure
Accept any TLS certificate (for servers with a self-signed certificate)
--json
Print the server's JSON responses
-h, --help
Print help
-V, --version
Print version
Protocol tools (list, show, check user-defined protocol files)
Usage: wpg protocols [OPTIONS] <COMMAND>
Commands:
list List the protocols: built-in ones and those of `--protocols-dir`
show Print a protocol definition, as an extension file to start a new protocol from (change its `id` first)
check Check user-defined protocol files (exit status 1 on errors)
help Print this message or the help of the given subcommand(s)
Options:
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
-h, --help
Print help
-V, --version
Print version
List the protocols: built-in ones and those of `--protocols-dir`
Usage: wpg protocols list [OPTIONS]
Options:
--json
Output JSON
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
-h, --help
Print help
-V, --version
Print version
Print a protocol definition, as an extension file to start a new protocol from (change its `id` first)
Usage: wpg protocols show [OPTIONS] <ID>
Arguments:
<ID>
Protocol id (`udp`, `ipv4`, ...)
Options:
--json
Output the definition as JSON instead of YAML
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
-h, --help
Print help
-V, --version
Print version
Check user-defined protocol files (exit status 1 on errors).
The files are checked together, with the built-in protocols and the other files of `--protocols-dir`.
Usage: wpg protocols check [OPTIONS] <PATHS>...
Arguments:
<PATHS>...
Extension files (`*.yaml`) or directories holding them
Options:
--protocols-dir <DIR>
Directory of user-defined protocol files (`*.yaml`, repeatable; added to `[protocols] dirs` of the configuration)
[env: WPG_PROTOCOLS_DIR]
--config <FILE>
Configuration file (default: `wpg.toml` in the working directory, if any). `wpg send` also takes its default backend and rate limit from `[tx]`
[env: WPG_CONFIG]
-h, --help
Print help (see a summary with '-h')
-V, --version
Print version
The server reads wpg.toml from the working directory, or the file given with --config / WPG_CONFIG. Precedence, lowest first: built-in defaults, the file, environment variables WPG_<SECTION>__<KEY> (__ separates nesting levels, for example WPG_SERVER__BIND=0.0.0.0:8080 or WPG_TX__ALLOWED_IFACES='["eth1"]'), then command line options such as wpg serve --bind. Unknown keys are errors. wpg config init writes the commented example below, wpg config show prints the effective configuration, wpg config validate checks it and wpg config set-password stores the admin password hash. The admin password itself is not a key: wpg serve --password, --password-file and WPG_PASSWORD take precedence over auth.password_hash and auth.password_file. This page is generated from the configuration types.
Directory for server state: user templates, favourites, the SQLite database (users, sessions, job history), dry-run captures and the self-signed TLS certificate.
Directories of user-defined protocol files (*.yaml: new protocol definitions and bindings under existing protocols), loaded at startup in file name order. wpg serve --protocols-dir adds more.
SNMP manager tool (Get / Walk / Set / traps, MIB upload).
Key
Type
Default
Environment variable
Description
enabled
boolean
true
WPG_SNMP__ENABLED
Enables the SNMP tool screen and the /api/snmp endpoints.
mib_dir
string
""
WPG_SNMP__MIB_DIR
Directory of uploaded MIB files. Empty: <data_dir>/mibs.
trap_listen
string
""
WPG_SNMP__TRAP_LISTEN
Address the trap / inform receiver listens on from startup, e.g. 0.0.0.0:162 (ports below 1024 need root or CAP_NET_BIND_SERVICE). Empty: not started at boot (it can be started from the UI).
Interfaces jobs may send on; empty = all. The null backend and dry runs are not restricted.
default_backend
string
WPG_TX__DEFAULT_BACKEND
Backend used when a job names none (null, pcapfile, afpacket, pcap, afxdp, dpdk); unset = afpacket on Linux, else pcap, else null.
dpdk_eal_args
string[]
[]
WPG_TX__DPDK_EAL_ARGS
DPDK EAL arguments for the dpdk backend, e.g. ["-a", "0000:00:04.0"]. wpg adds -l <first CPU> and --in-memory unless given. EAL starts once per process with the first dpdk job; changing them needs a restart.
max_rate
string
WPG_TX__MAX_RATE
Highest rate a job may request (100kpps, 1Gbps); jobs without a rate are sent at this rate.
prerender_max_bytes
integer
268435456
WPG_TX__PRERENDER_MAX_BYTES
Pre-render a stream (build every frame before sending) only when packets × frame length is at most this many bytes; larger streams are built on the fly. 0 never pre-renders unless --plan prerender asks for it. Up to 1 TiB; wpg send --prerender-max-bytes overrides.
prerender_max_packets
integer
100000000
WPG_TX__PRERENDER_MAX_PACKETS
Pre-render a stream only when it has at most this many packets. wpg send --prerender-max-packets overrides.
ring_bytes
integer
16777216
WPG_TX__RING_BYTES
Memory of one afpacket TX ring or afxdp UMEM per worker, in bytes (1 MiB–1 GiB); the ring depth is reduced to fit. The memory is locked: raise ulimit -l for large values. --ring-bytes overrides.
Every active line equals the built-in default, so the file changes nothing until edited; the commented lines show the optional keys.
# wpg.toml — Web Packet Generator configuration.
#
# Precedence (lowest first): built-in defaults < this file < environment variables
# (WPG_<SECTION>__<KEY>, e.g. WPG_SERVER__BIND=0.0.0.0:8080 or
# WPG_TX__ALLOWED_IFACES='["eth1"]') < command line options (wpg serve --bind ...).
# Unknown keys are rejected. Check a file with `wpg config validate`, print the
# effective configuration with `wpg config show`.
# Server state: user templates, favourites, job history, dry-run captures.
# Default: the OS data directory (Linux: ~/.local/share/wpg).
# data_dir = "/var/lib/wpg"
[server]
# Listen address. Without a password keep it on localhost.
bind = "127.0.0.1:8080"
# Public URL when running behind a reverse proxy.
# base_url = "https://wpg.example.net"
# Serve HTTPS with a self-signed certificate generated into <data_dir>/tls/
# (same as `wpg serve --tls`). Ignored when [server.tls] is set.
self_signed_tls = false
# Most packets one preview request may render (1-100000).
preview_max = 256
# Finished jobs listed on the Jobs screen (kept in memory, 1-100000) and kept in
# the database (1-10000000; older ones are deleted).
job_history = 100
job_history_db = 1000
# TLS certificate chain and private key (PEM) to serve HTTPS with.
# [server.tls]
# cert = "/etc/wpg/cert.pem"
# key = "/etc/wpg/key.pem"
[auth]
# Admin password. Without one (and without a user created on the first-run
# setup screen) the server only listens on localhost. Sources, highest first:
# `wpg serve --password`, `--password-file`, $WPG_PASSWORD, password_hash,
# password_file, the setup screen.
# argon2id hash of the admin password (`wpg config set-password` writes it):
# password_hash = "$argon2id$v=19$m=19456,t=2,p=1$..."
# Or a file holding the hash or the plain-text password:
# password_file = "/etc/wpg/password"
# Session lifetime in seconds (extended by every request).
session_ttl_secs = 86400
[templates]
# Read-only template libraries (Git checkouts etc.), scanned recursively for
# *.wpg.yaml / *.wpg.json and reloaded when files change.
library_paths = ["./templates"]
# Writable directory for templates created or imported in the GUI.
# Default: <data_dir>/templates
# user_dir = "/var/lib/wpg/templates"
[protocols]
# Directories of user-defined protocol files (*.yaml), loaded at startup.
# Changes need a restart.
# dirs = ["/etc/wpg/protocols.d"]
[tx]
# Interfaces jobs may send on; empty = all. The null backend and dry runs are
# not restricted.
allowed_ifaces = []
# Highest rate a job may request, e.g. "100kpps" or "1Gbps". Jobs without a rate
# are sent at this rate.
# max_rate = "100kpps"
# Backend when a job names none: null, pcapfile, afpacket, pcap, afxdp, dpdk.
# Default: afpacket on Linux, else pcap, else null.
# default_backend = "afpacket"
# DPDK EAL arguments for the dpdk backend (wpg adds "-l <first CPU>" and
# "--in-memory" unless given). EAL starts with the first dpdk job; changing them
# needs a restart.
# dpdk_eal_args = ["-a", "0000:00:04.0"]
# Streams are pre-rendered (every frame built before sending) when they have at
# most prerender_max_packets packets and packets * frame length is at most
# prerender_max_bytes (default 256 MiB, up to 1 TiB); larger ones are built on
# the fly. `wpg send --prerender-max-bytes / --prerender-max-packets` override.
prerender_max_bytes = 268435456
prerender_max_packets = 100000000
# Memory of one afpacket TX ring / afxdp UMEM per worker in bytes (1 MiB-1 GiB,
# default 16 MiB); --ring-depth is reduced to fit. Locked memory: raise
# `ulimit -l` / LimitMEMLOCK for large values.
ring_bytes = 16777216
[snmp]
# SNMP tool screen and API (Get / Walk / Set / traps, MIB upload).
enabled = true
# Trap / inform receiver started at boot, e.g. "0.0.0.0:162" (ports below 1024
# need root or CAP_NET_BIND_SERVICE). Empty: not started at boot (it can be
# started from the UI).
trap_listen = ""
# Directory of uploaded MIB files. Empty: <data_dir>/mibs
mib_dir = ""
[dhcp]
# DHCP screen and API (DHCPv4 / DHCPv6 server, Linux only). Pools and options
# are edited on the screen and kept in <data_dir>/dhcp.json.
enabled = true
# Interface the server answers on, e.g. "eth1". Empty: chosen on the screen
# among tx.allowed_ifaces. Ports 67 / 547 need root or CAP_NET_BIND_SERVICE.
iface = ""
[log]
# Filter directives (RUST_LOG overrides this), e.g. "info" or "wpg_tx=debug,info".
level = "info"
# "pretty" or "json".
format = "pretty"
# Also write to this file (its directory is created).
# file = "/var/log/wpg/wpg.log"
# Rotation of the file: "never", "hourly" or "daily".
rotation = "never"
# Keep at most this many rotated files.
# max_files = 7
# Colour the console output when it is a terminal.
ansi = true
[terminal]
enabled = false # Web terminal (SSH / Telnet)
allowed_targets = [] # "host", "host:port" or CIDR; empty = any target
record_sessions = true # keep session logs under <data_dir>/terminal-logs
idle_timeout_secs = 1800
max_sessions = 8
These scripts install wpg on a Linux host (Ubuntu 20.04 to 24.04, Debian; first target: an Ubuntu node on CML2) over SSH. From Windows use scripts\deploy.bat (a wrapper around scripts\deploy.ps1), from Linux / macOS scripts/deploy.sh. Both run the same host-side scripts (scripts/lib/*.sh), so the result is the same.
There are no arguments to remember. Started without arguments, a wizard asks for everything it needs, one question at a time, in English or Japanese (from the system language). Every question has a default in [ ] that Enter accepts.
It can run unattended too. With the answer file the wizard offers to save at the end, the same deploy runs without any question (scheduled upgrades, several hosts, CI); see Unattended deploy.
Requirements: the OpenSSH client (ssh, scp, ssh-keygen) and tar.exe that ship with Windows 10 1809 and later / Windows 11. If the OpenSSH client is missing, add it under Settings > Apps > Optional features. Rust and Git are not needed on Windows (with Git, the source is packed with git archive).
A first run (Windows PowerShell 5.1, English system). Input at ******** is not shown.
wpg deploy wizard (Enter accepts the default in [ ]; Ctrl+C aborts)
Target host (IP address or name): 192.0.2.10
SSH port [22]:
SSH user [ubuntu]: cisco
SSH authentication:
1) Password (used once to install a deploy key)
2) An existing private key file
Choose a number [1]:
[1] Connecting to cisco@192.0.2.10
SSH password (cisco@192.0.2.10): ********
Created the deploy key C:\Users\you\.ssh\wpg_deploy_ed25519
Installing the public key into ~/.ssh/authorized_keys on the host (the password is used this once).
Key login works; no password needed next time.
OS: Ubuntu 24.04.1 LTS (x86_64) sudo: password wpg: -
How to get the wpg binary:
1) Build from source on the host (recommended; needs internet there; 10-30 min the first time)
2) Upload a Linux binary file you already have
3) Download from a GitHub Release (no release yet)
Choose a number [1]:
Source code:
1) Upload this checkout (C:\work\web-packet-generator)
2) git clone from GitHub
Choose a number [1]:
Install as a systemd service (starts at boot)? [Y/n]:
Web UI listen address:port (0.0.0.0 = all) [0.0.0.0:8080]:
Interfaces jobs may send on (tx.allowed_ifaces):
1) ens2 UP 192.0.2.10/24 <- management (this SSH session)
2) ens3 UP
3) ens4 UP
Numbers, comma separated (e.g. 2,3; all = no restriction) [2,3]:
Enable the web terminal? (newer wpg only) [y/N]:
Enable the SNMP trap receiver (UDP 162)? (newer wpg only) [y/N]: y
Interface the DHCP server answers on (empty / - = chosen on the screen; newer wpg only): ens4
Static addresses for ens4 (address/len, comma separated, e.g. 192.168.100.1/24,2001:db8:1::1/64; empty / - = none): 192.168.100.1/24
sudo password (Enter = same as the SSH password):
wpg admin password (Web UI login): ********
Repeat it: ********
Summary
Host : cisco@192.0.2.10 (port 22)
Binary : build on the host (this checkout: C:\work\web-packet-generator)
Service : systemd (wpg.service)
Listen : 0.0.0.0:8080
TX interfaces : ens3,ens4
Admin password : set
Web terminal : off
SNMP traps : on (UDP 162)
DHCP server : ens4 192.168.100.1/24
Go ahead? [Y/n]:
Save these answers for next time? (no passwords) [Y/n]:
Saved the answers to C:\Users\you\AppData\Roaming\wpg\deploy.conf (no passwords).
log: C:\Users\you\AppData\Local\wpg\deploy-logs\deploy-20261027-101500.log
[2/6] Uploading to cisco@192.0.2.10:/tmp/wpg-deploy.Xa81Qe
[3/6] Installing the build dependencies (apt)
-> apt-get install build-essential pkg-config libpcap-dev curl git ca-certificates tar
[4/6] Building on the host (10-30 minutes the first time)
-> installing rustup (https://rustup.rs) into ~/.cargo
-> installed trunk 0.22.1 (prebuilt)
...
-> built wpg 0.1.0
[5/6] Installing (sudo)
-> installed wpg 0.1.0 to /usr/local/bin/wpg
-> setcap cap_net_raw,cap_net_admin,cap_net_bind_service+eip /usr/local/bin/wpg
-> systemd unit wpg.service (re)started
[6/6] Health check (http://127.0.0.1:8080/api/health)
{"status":"ok","version":"0.1.0",...}
Done. Open in a browser:
http://192.0.2.10:8080/
The deploy.sh wizard asks the same questions (its binary menu also offers “Build on this machine and upload”, and it asks for the sudo password right after connecting).
The answers (never the passwords) are saved to %APPDATA%\wpg\deploy.conf on Windows and ~/.config/wpg/deploy.conf on Linux / macOS. The next run starts with “Use the previous settings?”; Enter repeats the last deployment and only the passwords are asked (the SSH password is not needed any more, the key works). After a successful deploy the wizard offers to save a file that runs the same deploy without questions (Unattended deploy).
When wpg is installed and a wpg.prev exists, a menu comes first:
wpg is installed (wpg 0.1.0). What do you want to do?
1) Install / upgrade
2) Roll back (restore the previous binary wpg.prev)
3) Quit
Almost always: the binary matches the host’s glibc.
Internet access from the host (apt, rustup, crates.io, GitHub); 2 GiB of memory or swap; about 5 GiB of free disk.
Upload a Linux binary
You already built one on another Linux machine.
A wpg ELF file (Windows’ wpg.exe does not work). A binary built against a newer glibc than the host’s does not run.
Download a GitHub Release
Once releases are published.
Internet access from your machine. There are no releases yet (the wizard says “no release yet”).
Build on this machine (deploy.sh only)
Your machine is Linux on the same CPU architecture.
Rust and trunk (the original deploy.sh behaviour).
Building on the host:
As root, installs build-essential, pkg-config, libpcap-dev, curl, git and ca-certificates with apt-get (on dnf-based distributions the equivalent packages, best effort).
As the SSH user, installs rustup (stable, minimal profile, ~/.cargo), the wasm32-unknown-unknown target and trunk (prebuilt; cargo install trunk when that fails).
Puts the source into ~/wpg-build/src: by default this checkout, uploaded (git archive when Git is available, including uncommitted changes of tracked files but not untracked files; otherwise tar.exe without target and the like), or a git clone from GitHub at a branch / tag.
Runs scripts/build.sh --release. ~/wpg-build/target is kept, so later builds are incremental. The number of parallel jobs is lowered on hosts with little memory.
Connects over SSH (installing the key if needed) and reads facts about the host (OS, CPU, sudo, interfaces, installed wpg, internet access).
Uploads to a temporary directory on the host (/tmp/wpg-deploy.*, removed at the end).
(When building on the host) installs the dependencies and builds.
Installs (scripts/lib/deploy-remote.sh as root): the binary goes to /usr/local/bin/wpg (the previous one stays as wpg.prev) with setcap cap_net_raw,cap_net_admin,cap_net_bind_service+eip (installing libcap2-bin first if setcap is missing and apt-get exists). For the service it creates the wpg system user, /etc/wpg/wpg.toml (only if missing), /etc/wpg/wpg.env and /etc/wpg/password, and (re)starts wpg.service. If ufw / firewalld is active and the port is not open, it prints how to open it (the firewall is never changed).
Prints wpg doctor from the host and waits for GET /api/health (30 s; shows the last journal lines if the server does not answer).
Prints the URL to open.
Steps are numbered ([3/6]) and coloured, and everything also goes to a log file (Windows: %LOCALAPPDATA%\wpg\deploy-logs\, deploy.sh wizard: ~/.local/state/wpg/). On a failure the scripts name the failing step, show the last lines of output and suggest a fix.
# Build, upload, setcap only (run `wpg send` / `wpg serve` yourself on the host)
scripts/deploy.sh ubuntu@192.0.2.10
# Install as a service that listens on all addresses, allows sending only on ens3
scripts/deploy.sh ubuntu@192.0.2.10 --service --iface ens3 --password-file ./admin-password
# Build from source on the host and install (the password is asked once, a key is installed)
scripts/deploy.sh ubuntu@192.0.2.10 --ask-password --sudo-password --build-on-host --service --iface ens3
# The same through the task runner
just deploy ubuntu@192.0.2.10
# Show what would run, without touching anything
scripts/deploy.sh ubuntu@192.0.2.10 --service --dry-run
--password-file takes a local file whose first line is the admin password (plain text) or its argon2id hash; it lands on the host as /etc/wpg/password (mode 0640, group wpg). Without it, and without a password already on the host (/etc/wpg/password, password_hash / password_file in /etc/wpg/wpg.toml, or an existing user database), the script refuses to start the server on a non-loopback address: use --bind 127.0.0.1:8080 plus an SSH tunnel (ssh -L 8080:127.0.0.1:8080 ubuntu@192.0.2.10), or --allow-insecure on an isolated lab network.
Option
Meaning
(none), --interactive
The wizard.
--iface <name>
Interface jobs may send on (repeatable); written as WPG_TX__ALLOWED_IFACES. Leave the management interface out.
--service
Install / update wpg.service and restart it.
--no-service
Do not install the service (overrides service=1 of --config).
--bind <addr:port>
Listen address of the service (default 0.0.0.0:8080).
--password-file <file>
Admin password (or argon2 hash) for the service.
--allow-insecure
Start without a password on a non-loopback address (adds a drop-in running wpg serve --allow-insecure).
--binary <file>
Deploy this binary instead of target/release/wpg.
--target <triple>
Build for another target, e.g. aarch64-unknown-linux-gnu (needs a cross linker).
--build-on-host
Build from source on the host: --source local (default: upload this checkout) or --source git (--git-url, --git-ref; default: GitHub, main).
--release-tag <tag>
Use the Linux binary of a GitHub Release (latest for the newest).
--prefix <dir>
Install prefix on the host (default /usr/local).
--port <n>, --ssh-opt <opt>
SSH port and extra -o options (e.g. --ssh-opt IdentityFile=~/.ssh/lab).
--identity <file>
SSH private key to use.
--ask-password
Ask for the SSH password once and install the deploy key ~/.ssh/wpg_deploy_ed25519 on the host.
--sudo-password
Ask for the sudo password up front and pass it to sudo -S on stdin (default: sudo asks on the SSH terminal).
--terminal, --snmp-trap
Enable the web terminal / the SNMP trap receiver (see below).
--dhcp-iface <name>
Pin the interface the DHCP server answers on (WPG_DHCP__IFACE in /etc/wpg/wpg.env; with --service). See DHCP server.
--dhcp-iface-addr <addr/len>
Static addresses for --dhcp-iface (repeatable or comma separated, e.g. 192.168.100.1/24,2001:db8:1::1/64): written to /etc/netplan/90-wpg-<iface>.yaml and applied with netplan apply. Refused for an interface carrying the default route.
--no-templates
Do not copy the sample templates to /usr/local/share/wpg/templates.
--no-healthcheck
Skip the health check.
--rollback
Swap /usr/local/bin/wpg with wpg.prev and restart the service. Running it twice undoes it.
Everything the wizard asks can also be passed as parameters. With -HostName the wizard does not start (only missing passwords are asked; with -Yes nothing is asked and a missing one fails at once).
# Build on the host and install the service, without questions (sudo password from WPG_DEPLOY_SUDO_PASSWORD, ...)
.\scripts\deploy.ps1 -HostName 192.0.2.10 -User cisco -Iface ens3,ens4 -Yes
# Use a Linux binary you already have
.\scripts\deploy.ps1 -HostName 192.0.2.10 -User cisco -BinarySource file -Binary .\wpg
# Build the v0.2.0 tag from GitHub
.\scripts\deploy.ps1 -HostName 192.0.2.10 -Source git -GitRef v0.2.0
# Roll back
.\scripts\deploy.ps1 -HostName 192.0.2.10 -User cisco -Rollback
Parameter
Meaning
-HostName, -User, -Port, -Identity
Host, SSH user (default ubuntu), port, existing private key.
-SshPassword, -SudoPassword, -AdminPassword
SecureStrings (e.g. (Read-Host -AsSecureString)); asked when needed if omitted.
-BinarySource build|file|release
Build on the host (default), the file -Binary, or a GitHub Release (-ReleaseTag).
-Source local|git, -GitUrl, -GitRef
Source to build.
-Bind, -Iface
Listen address, interfaces jobs may send on.
-NoService
Do not install the service (installed by default).
-Terminal, -SnmpTrap
Web terminal / SNMP trap receiver.
-DhcpIface, -DhcpIfaceAddr
DHCP server interface and static addresses (as --dhcp-iface / --dhcp-iface-addr).
-Rollback, -NoTemplates, -NoHealthcheck
As in deploy.sh.
-ProfilePath, -Lang ja|en, -LogFile
Answers file, message language, log file.
-Interactive, -Yes, -DryRun
Start the wizard with the parameters as defaults / never ask anything (no confirmation, no password) / only connect and check, without building or installing.
-Prefix, -AllowInsecure
Install prefix on the host, start without a password (as in deploy.sh).
Use the wizard when you deploy by hand. To repeat a known setup (scheduled upgrades, several hosts, CI), use an answer file plus --yes: the scripts then run to the end without a single question.
At the end of a successful wizard run the scripts ask:
Save a file to run this deploy unattended (automatically) from now on? (no passwords) [Y/n]:
File [C:\Users\you\AppData\Roaming\wpg\deploy-192.0.2.10.conf]:
Save the sudo password encrypted? (Windows DPAPI: only this Windows user on this PC can decrypt it) [Y/n]:
Saved C:\Users\you\AppData\Roaming\wpg\deploy-192.0.2.10.conf (no plain-text passwords).
From now on this one line runs the same deploy without questions:
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\work\web-packet-generator\scripts\deploy.ps1" -ConfigFile "C:\Users\you\AppData\Roaming\wpg\deploy-192.0.2.10.conf" -Yes -SkipIfCurrent
Scheduled run, e.g. Task Scheduler every Sunday at 3:17 (logs stay in %LOCALAPPDATA%\wpg\deploy-logs):
schtasks /Create /TN "wpg deploy 192.0.2.10" /SC WEEKLY /D SUN /ST 03:17 /TR "..."
Copy the printed line as it is (deploy.sh prints scripts/deploy.sh --config ~/.config/wpg/deploy-192.0.2.10.conf --yes --skip-if-current and a cron line). SSH uses the deploy key the wizard installed, so no SSH password is needed. When sudo needs a password, Windows can keep it DPAPI-encrypted (the question above); on Linux / macOS use an environment variable or a password file (see below).
scripts/deploy.sh --config deploy.wpg.conf --yes # Linux / macOS
scripts/deploy.sh --config deploy.wpg.conf --check-config # only show the settings (no connection)
.\scripts\deploy.ps1 -ConfigFile .\deploy.wpg.conf -Yes # Windows
Relative paths in the file (binary_file, identity, *_password_file, …) are relative to the answer file. Command-line options win over the file (--set key=value / -Set "key=value" overrides any key). An unknown key is an error (exit code 1), so typos do not go unnoticed.
Key
Default
Meaning
host
(required)
Target host; user@host works too.
user
ubuntu
SSH user.
ssh_port (port)
22
SSH port.
identity
(none)
SSH private key. Without it: the deploy key (wpg_deploy_ed25519), then ssh-agent / your default keys.
binary_source
build
build (on the host), file (upload binary_file), release (GitHub Release); deploy.sh also has local (build on this machine).
binary_file
Linux binary for binary_source=file.
source
local
Source for build: local (upload this checkout) or git.
git_url, git_ref
GitHub, main
Repository and branch / tag / commit for source=git.
release_tag
latest
Tag for binary_source=release.
bind
0.0.0.0:8080
Listen address.
ifaces
(all)
Interfaces jobs may send on (comma separated).
service
1
Install the systemd service.
terminal, snmp_trap
0
Web terminal / SNMP trap receiver.
dhcp_iface, dhcp_iface_addr
empty
DHCP server interface and static addresses (comma separated).
templates, healthcheck
1
Copy the sample templates / run the health check.
allow_insecure
0
Listen on a non-loopback address without a password.
Searched top to bottom. In deploy.ps1 the SecureString parameters (-SshPassword, …) come first.
The SSH password is only used when no key works, and then installs the deploy key (not needed next time). Without a sudo password the SSH password is tried. NOPASSWD sudo or a root login need none.
The admin password is only required for a first install that listens on a non-loopback address (0.0.0.0:8080, …). On later runs leaving it out keeps the current one (setting it rewrites it every time).
The scripts never write a password to a file in plain text. A DPAPI file is the output of ConvertFrom-SecureString (no key): only the Windows user who made it can decrypt it, on that PC. Run the scheduled task as that user.
A missing secret fails at once without asking: the message names the variable / key to set, and the exit code is 2 (SSH / sudo) or 1 (admin password). Even without --yes / -Yes nothing is asked when the standard input is not a terminal (cron, a pipe, -NonInteractive): the scripts say to add --yes and exit with 1.
Only the result goes to the standard output, as JSON (the log goes to the standard error and the log file). One host or several, the shape is the same:
A host’s status is ok, skipped (already current, nothing done), dry-run, checked, aborted or failed. On a failure failed_step (config, connect, sudo, binary, deps, build, upload, install, health) and message say what went wrong.
Every deploy records what it installed with which settings in /usr/local/share/wpg/deploy.stamp on the host (source id, settings, SHA-256 of the binary, time; no passwords). With --skip-if-current the scripts connect, and when all of the following match they stop without building or installing anything (exit code 0, statusskipped), without even using the sudo password:
The same source id: for source=git the commit the branch / tag points to (resolved with git ls-remote), for source=local the commit of this checkout (plus a hash of uncommitted changes), for binary_source=file the file’s SHA-256, for release the URL of the release archive.
The same settings (bind, ifaces, service, terminal, snmp_trap, dhcp_iface, dhcp_iface_addr, templates, allow_insecure, prefix).
The installed binary is the recorded one (after a rollback or a manual replacement the scripts deploy again).
With a service: wpg.service runs and answers the health check.
wpg --version carries no commit, hence the record file. When the source cannot be identified (no Git, the ref does not resolve) the scripts warn and deploy as usual. Re-installing an identical binary no longer overwrites wpg.prev, so the previous version stays available for a rollback.
Together with --yes the scripts connect, read the host facts, check that every needed secret is there (the sudo password is really tried) and print the commands they would run. Nothing on the host changes (not even the key installation). Good for checking a setup from CI. To check only the answer file, without connecting, use --check-config / -CheckConfig.
[host <name>] sections in the answer file deploy to one host each. Keys before the first section apply to all hosts; a section’s own values win (without host= the section name is the host name).
The hosts can also come from a separate list (--targets / -Targets: one host per line, [user@]host[:port] followed by optional key=value pairs; relative paths are relative to that file):
Each host runs in its own process (--parallel / -Parallel sets how many at a time, default 1); per-host logs stay in ~/.local/state/wpg/deploy-<time>/ (Windows: %LOCALAPPDATA%\wpg\deploy-logs\deploy-<time>\). At the end a table is printed, and the exit code is 6 if any host failed.
Result
NAME HOST STATUS EXIT VERSION DETAIL
r1 cisco@192.0.2.11 ok 0 0.2.0 git:5d0c1e2...
r2 cisco@192.0.2.12 skipped 0 0.2.0 git:5d0c1e2...
r3 cisco@192.0.2.13 failed 2 cannot connect to cisco@192.0.2.13 (port 22): ssh: connect to host ...
2 succeeded, 1 failed
Paste the schtasks line the wizard prints into a command prompt. By hand it looks like this (add /RU <user> /RP to run while logged off; with a DPAPI file, run the task as the user who created the file):
When sudo needs a password, put sudo_password_file= (a file you create, chmod 600) into the answer file, or give the deploy user NOPASSWD on the host (environment variables are awkward in cron).
Keep the SSH key and the sudo password in repository secrets. Putting the host’s public key into known_hosts avoids accepting it blindly on the first connection.
The SSH password is used once, to install a key. The scripts create a deploy key (~/.ssh/wpg_deploy_ed25519, on Windows %USERPROFILE%\.ssh\wpg_deploy_ed25519, no passphrase) and append its public key to ~/.ssh/authorized_keys on the host. The password is passed by sshpass (deploy.sh, when installed) or a temporary SSH_ASKPASS helper (OpenSSH 8.4 and later; the password travels in an environment variable, never in a file, and the helper is deleted right away); if neither works, ssh itself asks for the password once more. To revoke the key, delete the wpg-deploy@... line from ~/.ssh/authorized_keys on the host.
The sudo and admin passwords go through the standard input of ssh (sudo -S, cat > /tmp/wpg-deploy.*/password): they never appear on a command line, in the process list or on the local disk. The saved answers file contains no passwords.
The host key of a new host is accepted automatically (StrictHostKeyChecking=accept-new; a changed key is still refused). Outside a lab, connect once with ssh first and check the fingerprint.
The admin password is stored on the host as /etc/wpg/password (0640 root:wpg) and hashed by wpg at startup.
Unattended runs read passwords from the environment, from files you create, or on Windows from DPAPI-encrypted files. The scripts never store a password in plain text (Unattended deploy).
Keep the management interface out of the TX interfaces (the wizard’s default does): test traffic on it can cut your SSH session.
Only into a wpg.tomlcreated by this deployment. An existing wpg.toml is never changed; the lines to add and sudo systemctl restart wpg are printed instead.
An older wpg refuses to start with keys it does not know, so they are only written when wpg config validate of the installed binary accepts them; otherwise a warning is printed and they are skipped.
With trap_listen set, a drop-in (/etc/systemd/system/wpg.service.d/bind-service.conf, CAP_NET_BIND_SERVICE) lets the service bind the privileged UDP port 162. With ufw active, sudo ufw allow 162/udp is suggested.
Data directory: database, user templates, TLS certificate
no
~/wpg-build/, ~/.cargo/, ~/.rustup/ (SSH user)
Source, build cache and Rust toolchain when building on the host
when building
Environment variables win over wpg.toml: change the wpg.env keys through the deploy options and everything else in wpg.toml. See Running as a service for the unit itself.
--dpdk only prepares the host. The dpdk backend also needs a binary built with --features dpdk (scripts/build.sh --dpdk; together with --build-on-host the host builds with --features dpdk), the NIC bound to vfio-pci, and EAL arguments in tx.dpdk_eal_args. See DPDK / AF_XDP setup.
With --dpdk the script:
reserves --hugepages 2 MiB hugepages (default 1024 = 2 GiB) now and at boot (/etc/sysctl.d/80-wpg-hugepages.conf), and mounts hugetlbfs on /dev/hugepages if nothing is mounted;
loads vfio-pci now and at boot (/etc/modules-load.d/wpg-dpdk.conf);
warns when there are no IOMMU groups (typical for CML2 VMs without a virtual IOMMU).
It does not bind NICs to vfio-pci: that takes the interface away from the kernel and can cut your SSH session. Use dpdk-devbind.py by hand; see DPDK / AF_XDP setup.
Running the same deployment again (in the wizard: Enter at “Use the previous settings?”) upgrades: the new binary replaces the old one, the service restarts, configuration and data stay. See Upgrading. If the new version misbehaves, choose “Roll back” in the wizard’s menu, or:
Check address, port and user; try ssh -p 22 user@host by hand. On CML2 the node needs the management link (External Connector).
could not install the public key
Wrong password, or password authentication is disabled on the server (PasswordAuthentication no). Choose an existing key file, or install a key on the host yourself.
key login fails
Public key authentication is disabled on the server, or ~/.ssh is too permissive (chmod 700 ~/.ssh; chmod 600 ~/.ssh/authorized_keys).
sudo failed
Wrong sudo password, or the user is not in the sudo group (sudo usermod -aG sudo <user>).
Failure in the dependency step
Check that the host reaches the internet (behind a proxy: configure it in /etc/apt/apt.conf.d/ and ~/.bashrc) and that apt is not locked by automatic updates; retry after a few minutes.
killed / signal: 9 in the build step
Out of memory: add swap (sudo fallocate -l 4G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile) and run again.
The build keeps failing
Run rm -rf ~/wpg-build on the host and deploy again.
the uploaded binary does not run on this host
The binary does not match the host’s glibc / CPU; build on the host instead.
Health check fails
sudo journalctl -u wpg -n 50 on the host; sudo ss -ltnp for a port already in use. Without an admin password the server does not start on a non-loopback address.
The browser cannot connect
Check the firewall (sudo ufw allow 8080/tcp), the CML2 external connection, and the listen address (127.0.0.1 needs an SSH tunnel).
does not support [terminal] / [snmp]
The installed wpg predates the feature; deploy a newer source.
Garbled Japanese (Windows)
If you edited deploy.ps1, save it as UTF-8 with BOM.
deploy.sh was run against an Ubuntu 24.04 container (sshd, a minimal systemctl stand-in instead of systemd): the wizard (key installation with the password through both sshpass and SSH_ASKPASS, the sudo password, the interface selection, saving and reusing the answers, the rollback menu), building on the host (this checkout uploaded, rustup and trunk installed, through to the health check), and writing [terminal] / [snmp] into a new wpg.toml (with a stub standing in for a binary that knows the keys) while leaving an existing wpg.toml alone. The earlier checks in a container running systemd (installation, refusal without a password, --service with the health check, --rollback, --dpdk) still stand. The unattended mode was checked with deploy.sh against the same Ubuntu 24.04 container (two sshds, each in its own mount namespace separating /usr/local, /etc/wpg and /var/lib/wpg): a first install from an answer file with --yes (the deploy key installed with the SSH password from the environment, sudo and admin passwords from the environment, the sudo password also from a file), failing at once without asking when a secret is missing (no key and no SSH password: 2, sudo password missing / wrong: 2, no admin password on a first install: 1, no --yes with a non-terminal stdin: 1), --skip-if-current (skipped when nothing changed, even without a sudo password; deployed again after a new binary, changed settings or a rollback), --dry-run, --check-config, the --json output (validated with python3 -m json.tool), several hosts (three sections, one unreachable: per-host results in the table and the JSON, exit code 6; --parallel 2; --targets), and saving the unattended file at the end of the wizard plus a rerun with it. deploy.ps1 / deploy.bat have not been run on Windows (CI only parses them and checks the PowerShell 5.1 syntax; the unattended parts are untested on Windows too). Not tested on CML2 nodes or real hardware yet.
Samples live under packaging/ in the repository and in every release archive. All of them run wpg serve with the configuration in a fixed place (WPG_CONFIG) and a fixed data directory (WPG_DATA_DIR). Create the configuration first with wpg config init --path <file> and set a password with wpg config set-password --config <file>; a server that listens on a non-loopback address refuses to start without one.
Runs as the unprivileged user wpg. Raw sockets come from AmbientCapabilities=CAP_NET_RAW CAP_NET_ADMIN, so the binary needs no setcap for the service (file capabilities are ignored under NoNewPrivileges=yes anyway).
StateDirectory=wpg creates /var/lib/wpg owned by wpg; ProtectSystem=strict makes the rest of the file system read-only for the server.
EnvironmentFile=-/etc/wpg/wpg.env for per-host WPG_* overrides (deploy.sh writes it).
LimitMEMLOCK=infinity for the AF_XDP backend (UMEM is charged to RLIMIT_MEMLOCK on kernels before 5.11).
RestrictAddressFamilies allows AF_PACKET and AF_XDP besides IP and Unix sockets.
Change the listen address with bind in wpg.toml or WPG_SERVER__BIND= in wpg.env, then sudo systemctl restart wpg.
packaging/launchd/io.github.seiyafunaokajp.wpg.plist is a system daemon (it runs as root, which /dev/bpf* needs unless ChmodBPF is set up, see Permissions):
Stop and remove it with sudo launchctl bootout system/io.github.seiyafunaokajp.wpg. KeepAlive restarts the server when it exits with an error. Remove the quarantine attribute from a downloaded binary first, or launchd cannot start it (see Install on macOS).
wpg.exe is a console program and does not talk the Service Control Manager protocol. A plain sc.exe create wpg binPath= "C:\...\wpg.exe serve" therefore registers a service that fails to start with error 1053. Use one of these instead.
packaging/windows/wpg-service.ps1 registers a task that runs wpg.exe serve as SYSTEM at boot and restarts it every minute if it stops. From an elevated PowerShell:
It creates C:\ProgramData\wpg\wpg.toml (with wpg config init) when missing and keeps the data in C:\ProgramData\wpg\data. A directory it creates is open to SYSTEM and Administrators only: every user may read new folders under C:\ProgramData, and these hold the password hash, the database and the TLS key (existing directories keep their permissions). From an elevated PowerShell, set the password with & "C:\Program Files\wpg\wpg.exe" config set-password --config C:\ProgramData\wpg\wpg.toml before binding to a non-loopback address. The script warns when Npcap is missing.
Npcap must be installed in either case (wpg doctor checks it). Allow wpg.exe through Windows Defender Firewall only on private networks, and only if it listens on a non-loopback address.
The systemd unit was started by scripts/deploy.sh in an Ubuntu 24.04 container running systemd (the server ran as wpg with only CAP_NET_RAW and CAP_NET_ADMIN and answered /api/health); systemd-analyze verify accepts it. The release workflow installs the launchd daemon from the Apple silicon archive on a macOS 14 runner and the scheduled task from the Windows archive on a Windows Server runner as described above, checks /api/health, then stops and removes them; on Windows it also checks that BUILTIN\Users cannot read the configuration directory (see “What CI checks” in Release process). Both jobs pass. Not covered: a reboot (the boot trigger and RunAtLoad themselves), the NSSM / WinSW wrappers, and sending through Npcap on Windows.
The repository has a multi-stage Dockerfile and a docker-compose.yml. The build stage (rust:1-bookworm) downloads Trunk, builds the web UI and the release binary; the runtime image (debian:bookworm-slim) holds only wpg, curl (for the health check) and the sample templates in /usr/share/wpg/templates.
docker build -t wpg .
docker build -t wpg --build-arg FEATURES=pcap . # with the libpcap backend
The build needs Internet access: crates.io, the Trunk release on GitHub, and the Tailwind CSS and wasm-bindgen tools that Trunk downloads. Behind a TLS-intercepting proxy, pass the proxy’s CA certificate (PEM) as a build secret and the proxy address as the standard build arguments. The certificate is trusted in the build stage only and does not end up in the runtime image:
If Docker Hub rate-limits anonymous pulls, the base images can be taken from another registry with --build-arg RUST_IMAGE=mirror.gcr.io/library/rust:1-bookworm --build-arg RUNTIME_IMAGE=mirror.gcr.io/library/debian:bookworm-slim.
With --network host this is the host’s port. Use 127.0.0.1:8080 to keep it local.
WPG_PASSWORD
none
Required for a non-loopback bind: the server refuses to start otherwise. A plain password or an argon2id hash. WPG_AUTH__PASSWORD_FILE=/run/secrets/... works with Docker secrets.
WPG_DATA_DIR
/var/lib/wpg (volume)
Database, user templates, TLS certificate.
WPG_TEMPLATES__LIBRARY_PATHS
["/usr/share/wpg/templates"]
Mount your own library and add its path.
WPG_TX__ALLOWED_IFACES
all
Interfaces jobs may send on.
Other settings: any WPG_<SECTION>__<KEY> variable, or mount a wpg.toml and set WPG_CONFIG. See the configuration reference.
The process runs as root inside the container, but with only CAP_NET_RAW and CAP_NET_ADMIN. A non-root user would need file capabilities on the binary, which fail to execute when the container’s capability set lacks one of them, so the image keeps it simple.
Use Linux hosts. Docker Desktop on macOS and Windows runs containers inside a VM: --network host there is the VM’s network, so frames never reach your physical NIC.
wpg doctor inside the container reports the container’s view (capabilities, interfaces). The image is built without the dpdk feature (it would need the DPDK libraries in both stages); hugepages and vfio-pci for DPDK are prepared on the host in any case.
Without --network host (the default bridge network with -p 8080:8080) the GUI works, but jobs can only send on the container’s own eth0.
Checked in October 2026 (Docker 29.8, Linux 6.18 host, image without extra features):
docker build completed (Trunk web UI and cargo build --release --locked); the runtime image is about 170 MB on disk (44 MB compressed).
docker run --network host --cap-drop ALL --cap-add NET_RAW --cap-add NET_ADMIN -e WPG_PASSWORD=...: the health check turned healthy, /api/health and the web UI answered, and wpg doctor inside the container reported raw sockets with CAP_NET_RAW / CAP_NET_ADMIN.
wpg send with the afpacket backend on lo inside the container sent 100 frames, and wpg capture in the same container received them.
--build-arg FEATURES=pcap: the image ships libpcap0.8, wpg doctor finds it, and wpg send --backend pcap sent 50 frames on lo.
docker compose up -d --no-build: the service turned healthy, login with WPG_PASSWORD succeeded (204) and a wrong password was refused (401). Without WPG_PASSWORD, compose refuses to start with the message from the compose file.
The development sandbox only allows HTTPS through its proxy, so apt-get there had to be pointed at https://deb.debian.org with a throw-away copy of the Dockerfile; the Dockerfile in the repository was otherwise used unchanged. Docker Desktop (macOS / Windows) was not checked.
An upgrade replaces the wpg binary and restarts the server. Configuration, the database and templates stay where they are; each of them carries a version and is migrated forward automatically or with one command.
Read the release notes on the GitHub Release page.
Back up the configuration and the data directory. The database is SQLite in WAL mode, so stop the server first (or copy with sqlite3 wpg.sqlite ".backup wpg.backup.sqlite"):
Users, sessions and job history. The schema version is stored in the schema_version table. On start, the server applies the missing migrations in order, each in its own transaction. Nothing to do by hand. A database written by a newer wpg is refused with “database schema version N is newer than this build supports”, so a downgrade needs the backup taken before the upgrade.
Unknown keys are errors, so a typo is never silently ignored. New versions only add keys with defaults, so an old wpg.toml keeps working. After a downgrade, keys that the old version does not know make it refuse to start; wpg config validate names them. Compare your file with wpg config init --path /tmp/wpg.toml to see new keys and their documentation.
Every template starts with schema: <n>. When a template is loaded (library, user templates, bundles, the API), wpg upgrades older versions in memory; nothing is rewritten. Templates saved from the GUI are written with the current version. A template with a newerschema: than the binary supports is rejected (“unsupported template schema”), as is one without schema:.
To rewrite files on disk (for example a library kept in Git), use wpg template migrate:
wpg template migrate --check templates/ # list outdated files; exit status 1 if any
wpg template migrate templates/ # rewrite them, keeping <file>.bak
wpg template migrate --no-backup a.wpg.yaml # no .bak copy
Directories are scanned recursively for *.wpg.yaml / *.wpg.json. Rewritten files lose their YAML comments and get the canonical key order, so review the diff. Files already at the current version are not touched.
The current template schema is 1, the only version so far, so there is nothing to migrate yet. The mechanism exists so that a future format change does not break saved templates.
A change that old templates cannot be read with (renamed or moved keys, changed meaning) needs a new schema version:
Increase SCHEMA_VERSION in crates/wpg-template/src/lib.rs.
Append a step to MIGRATIONS: a fn(serde_yaml::Value) -> Result<serde_yaml::Value, TemplateError> that turns a version n document into version n + 1, including setting schema: n + 1. MIGRATIONS[n - 1] upgrades n; steps run in sequence, so each one only knows its neighbour.
Add a test that loads a version n fixture and compares it with the expected version n + 1 template, and keep the fixture.
Update the sample templates (wpg template migrate templates/), regenerate docs/schema/template.schema.json (wpg template schema), and update the template schema reference in both languages.
Mention the change in the release notes.
Additive changes (a new optional key with a default) do not need a new version.
Database changes work the same way: append an SQL migration to MIGRATIONS in crates/wpg-server/src/db.rs; never edit one that has been released.
The afxdp backend is implemented (M8) but optional: it is only available in builds with --features afxdp (cargo build --release -p wpg-cli --features afxdp). Copy mode has been verified on lo and veth; zero-copy has not been verified on real (physical) NICs. The dpdk backend (M8) is optional too (--features dpdk) and has only been verified on DPDK virtual devices (net_null, net_ring, net_pcap); physical NICs and CML2’s virtio PMD have not been tried yet.
The afxdp backend is a minimal implementation on the kernel ABI (<linux/if_xdp.h>, bpf(2)) without libbpf or libxdp, so it adds no build dependencies. Each worker (--workers) has:
one AF_XDP socket bound to one NIC queue; worker i uses queue --queue + i (from 0 by default);
a UMEM (the frame area, at most 16 MiB) whose pages are allocated on the NIC’s NUMA node (mbind);
a TX ring and a completion ring. Frames are copied into UMEM chunks, their descriptors are queued on the TX ring, and the kernel is woken with sendto only when it asks for it (XDP_USE_NEED_WAKEUP).
always copy mode (XDP_COPY); works on any interface including lo and veth
zerocopy
zero-copy (XDP_ZEROCOPY) only; an error if unavailable
Many drivers (i40e, ice, ixgbe, igc, …) only create the XDP rings that zero-copy transmission uses while an XDP program is attached. auto and zerocopy therefore attach a two-instruction XDP_PASS program in driver mode before the zero-copy bind (a bpf_link, detached when the backend closes; an XDP program that is already attached is used as is). If the program cannot be loaded, the driver refuses the zero-copy bind, or the first wake-up fails, the backend falls back to copy mode. The backend line of wpg bench / wpg send shows the outcome (AF_XDP zero-copy / AF_XDP copy mode, with the reason after a fallback).
Caution: some drivers rebuild their queues when an XDP program is attached or detached, which briefly takes the link down. On a production interface use --xdp-mode copy, or check the effect beforehand.
Before Linux 5.11 the UMEM is charged to RLIMIT_MEMLOCK. wpg raises the soft limit to the hard limit; if that is not enough set ulimit -l unlimited (LimitMEMLOCK=infinity in systemd).
Only one AF_XDP socket can be bound to a queue. Configure as many channels as queues you use; with too few queues wpg fails with an ethtool -L hint.
ethtool -l ens3 # current and maximum channels
sudo ethtool -L ens3 combined 4
wpg bench --backend afxdp -i ens3 --workers 4 --auto-pin
The maximum frame length is the UMEM chunk size (2048 B when the MTU is 2030 or less, else 4096 B). Longer frames are counted as dropped (multi-buffer AF_XDP, needed for jumbo frames, is not used).
AF_XDP copy mode works on every network device; zero-copy needs driver support. The table lists the main mainline Linux drivers with AF_XDP zero-copy and the approximate kernel version that added it. None of them has been verified with wpg on real hardware. Distribution kernels may differ because of backports, so always check that wpg bench reports zero-copy.
Driver
NIC
Zero-copy since (approx.)
Verified with wpg
i40e
Intel X710 / XL710 / XXV710
4.20
no
ixgbe
Intel 82599 / X520 / X540 / X550
4.20
no
mlx5_core
NVIDIA (Mellanox) ConnectX-4 Lx and later
5.3
no
ice
Intel E810
5.5
no
stmmac
Synopsys DWMAC (embedded SoCs)
5.13
no
igc
Intel I225 / I226
5.14
no
virtio_net
virtio-net in KVM / CML2
recent kernels (6.x); copy mode before
no
veth
containers / netns
none (native XDP yes, sending in copy mode)
copy mode verified (4 queues, 2 workers)
lo
loopback
none (copy mode)
copy mode verified
Verified (2026-10, Linux 6.18 container, root): copy-mode transmission on lo and veth with the frames arriving at a receiver; the auto fallback (on veth the XDP_PASS program attaches in driver mode, the zero-copy bind is refused with EOPNOTSUPP, and copy mode is used); two workers on two veth queues. Unit tests: cargo test -p wpg-tx --features afxdp (the veth test runs only with WPG_AFXDP_VETH=vxa,vxb). For performance see docs/perf.md.
The dpdk backend sends through a DPDK poll-mode driver (PMD). The NIC is taken away from the kernel and driven by wpg’s own threads, with no system call per batch. It is optional: build with --features dpdk (needs the DPDK development package and libclang, see Building).
cargo build --release -p wpg-cli --features dpdk
Verified so far (2026-10, Ubuntu 24.04 with DPDK 23.11.4, in a container without hugepages or a NIC): EAL with --no-huge, the net_null (wpg bench, 2 workers), net_ring and net_pcap virtual devices. A wpg send of a template through net_pcap wrote the same frames as the pcapfile backend. The unit tests run in CI against DPDK 24.11 and 23.11. Physical NICs (vfio-pci), hugepages and the virtio PMD on CML2 have not been tried.
EAL (DPDK’s environment layer) is started once per wpg process, by the first dpdk job, with your EAL arguments plus two defaults you can override: -l <first CPU wpg may use> (the main lcore only; wpg’s TX workers are its own threads and register with DPDK as non-EAL lcores) and --in-memory (no files under /var/run/dpdk; --no-shconf instead when you pass --no-huge). EAL cannot be restarted inside a process: if it fails, or you need other arguments, restart wpg serve (wpg send / wpg bench start a new process every time).
The interface is a DPDK port: its number (0), its device name (the PCI address of a bound NIC, 0000:00:04.0), or a virtual device. Virtual devices may carry devargs and are added on first use: net_tap0,iface=dtap0 (a kernel TAP interface, handy for testing), net_pcap0,tx_pcap=/tmp/out.pcap (writes a pcap file), net_null0 (discards frames, for benchmarks). An unknown name fails with a list of the ports EAL found.
Workers (--workers N) share one port: it is configured with --queue + N TX queues, worker i sends on TX queue --queue + i. Only one job can use a port at a time. The port is stopped when the job ends.
Buffers: one mbuf pool per port on the NIC’s NUMA socket, sized from --ring-depth (TX descriptors per queue, clamped to the driver’s limits) and --batch. Each frame is copied into an mbuf (2048 octets of data, so longer frames are counted as dropped; jumbo frames are not supported). When the TX ring is full the backend retries for up to 10 ms, then counts the rest as dropped. MBUF_FAST_FREE is turned on when the driver offers it.
The backend line of wpg send / wpg bench shows the DPDK version, port, driver, queue, descriptors, pool, NUMA socket, link state and the full EAL arguments.
scripts/deploy.sh <host> --dpdk [--hugepages N] does the first two steps (hugepages now and at boot, hugetlbfs, vfio-pci, an IOMMU check) but leaves binding the NIC to you; see Deploying with scripts/deploy.sh.
Hugepages (wpg doctor check hugepages, a warning when none). The pool for one port with 4 workers and the default --ring-depth 4096 takes about 70 MiB, plus what the driver allocates:
echo 1024 | sudo tee /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
grep Huge /proc/meminfo
IOMMU and vfio: on bare metal enable the IOMMU (intel_iommu=on iommu=pt or amd_iommu=on on the kernel command line) and sudo modprobe vfio-pci.
Bind the NIC. The interface disappears from the kernel (and from wpg ifaces), so never bind the interface that carries your ssh session:
dpdk-devbind.py --status # PCI addresses and current drivers
sudo ip link set ens4 down
sudo dpdk-devbind.py --bind=vfio-pci 0000:00:04.0
# back to the kernel: sudo dpdk-devbind.py --bind=virtio-pci 0000:00:04.0 (or ixgbe, i40e, ...)
NVIDIA / Mellanox ConnectX (mlx5) is the exception: it stays on its kernel driver and DPDK uses it through rdma-core; do not rebind it.
Privileges: run as root. vfio as an ordinary user needs /dev/vfio/* permissions, CAP_IPC_LOCK / a large RLIMIT_MEMLOCK and access to the hugepage mount, and is not covered here.
wpg send / wpg bench take them with --eal-args (one string, split on spaces); wpg serve takes them from tx.dpdk_eal_args in wpg.toml (Configuration) or WPG_TX__DPDK_EAL_ARGS. The useful ones:
Argument
Meaning
-a 0000:00:04.0
Only probe this PCI device (repeatable). Without it, EAL takes every NIC bound to a DPDK driver
-l 2
Main lcore (default: first CPU wpg may use). Pin the TX workers with --pin-cores / --auto-pin as usual
--socket-mem 512 / -m 512
Hugepage memory to reserve up front (MiB)
--vdev net_tap0,iface=dtap0
Create a virtual device at start (or name it as the interface, see above)
--file-prefix wpg
Use shared runtime files instead of --in-memory (for dpdk-proc-info and the telemetry socket)
--log-level lib.eal:debug
More EAL logging when something fails
--no-huge -m 256
Run without hugepages (virtual devices only; physical NICs need hugepages)
sudo wpg bench --backend dpdk -i 0000:00:04.0 --eal-args "-a 0000:00:04.0" --workers 2 --auto-pin
sudo wpg send t.wpg.yaml --backend dpdk -i 0000:00:04.0 --eal-args "-a 0000:00:04.0" --rate 1Mpps
# Without a NIC: a TAP interface you can watch with tcpdump (needs root).
sudo wpg send t.wpg.yaml --backend dpdk -i net_tap0,iface=dtap0 --count 10 &
sudo tcpdump -ni dtap0
On a CML2 Ubuntu node the data interfaces are virtio-net devices, which DPDK drives with the virtio PMD (net_virtio, part of the standard DPDK packages):
The guest normally has no IOMMU, so vfio-pci refuses to bind. Use vfio’s no-IOMMU mode (echo 1 | sudo tee /sys/module/vfio/parameters/enable_unsafe_noiommu_mode before binding; “unsafe” because DMA is not isolated, acceptable in a lab VM) or uio_pci_generic.
Keep the management interface (usually ens2) on the kernel; bind only a data interface.
The virtio PMD usually offers as many TX queues as the node has vCPUs / virtio queue pairs; --workers above that fails with the queue count in the message.
Throughput is bounded by the host’s vhost path and the virtual link, not by DPDK. Compare with afpacket on the same link before relying on DPDK; on CML2 the gain is often small.
The NIC’s MAC address does not matter for sending: wpg sends the source MAC in the template. Promiscuous mode is not needed to transmit.
Frames up to 2048 octets; no multi-segment mbufs, no TX checksum or segmentation offloads (wpg builds complete frames).
One job per port at a time; EAL arguments are fixed for the life of the process.
Shared DPDK libraries only (PMDs are loaded as plugins).
Expected performance (TODO section 2.2, not measured on hardware): AF_XDP about several Mpps per core, DPDK 10 Mpps or more per core on a physical NIC. Record your numbers with the checklist in docs/perf.md.
The bindings live in the dpdk-sys crate (ADR-0004). With its link feature (turned on by wpg-tx’s dpdk feature) the build script finds DPDK with pkg-config (libdpdk, 23.11 or newer; other versions than 24.11 / 23.11 build with a warning), generates the bindings with bindgen (EAL, ethdev, mempool / mbuf, lcore, the RTE_* constants) and compiles a small C shim with DPDK’s own cflags for the parts of the fast path that are static inline in the headers (wpg_rte_eth_tx_burst, wpg_rte_pktmbuf_alloc, wpg_rte_errno, …). Without the feature the crate is empty, so ordinary builds need no DPDK.
# Debian 13 (DPDK 24.11) / Ubuntu 24.04 (DPDK 23.11)
sudo apt install dpdk-dev libclang-dev pkg-config
cargo test -p dpdk-sys --features link # bindings + EAL smoke test on a net_ring port
cargo build -p wpg-tx --features dpdk # wpg-tx with the bindings linked
For DPDK built from source (meson / ninja), point pkg-config at it and make the shared libraries findable:
DPDK 24.11 refuses to load PMD plugins from a directory with a world-writable parent (such as /tmp), so install it under /opt or /usr/local. The smoke test runs EAL with --no-huge --no-pci on a net_ring virtual port, so it needs neither hugepages, a NIC nor root; CI runs it against both LTS releases (job dpdk). Only DPDK’s shared libraries are supported (PMDs are loaded as plugins). The backend’s own tests (cargo test -p wpg-tx --features dpdk --lib dpdk) also run without hugepages, a NIC or root, on net_ring / net_pcap / net_null devices.
The first deployment target is an Ubuntu node in Cisco CML2 with a virtio-net interface, where the afpacket backend is the main path. This page covers what to set up and what to watch for.
Verification status. Everything about wpg’s own behaviour (doctor checks, error messages, flags) is taken from the code and from wpg run in a container. No measurement has been made on CML2 yet. All performance numbers in docs/perf.md come from the lo interface and the null backend, so they only bound the afpacket path from above. The CML2-specific statements below (bridge behaviour, MAC filtering) are general Linux/KVM knowledge to be confirmed on your CML2 version; the M2 on-CML2 measurement is still open.
An Ubuntu node (22.04 or later, kernel 5.15+) with at least one connected virtio-net interface. wpg doctor requires kernel 3.2 or newer for TPACKET_V3 and 5.10 or newer for AF_XDP.
The wpg binary built with --features pcap if you also want the pcap backend (needs libpcap0.8). afpacket needs no library.
Use two or more vCPUs. Sending is one busy thread; leave a core for the OS.
The smallest setup is one Ubuntu node running wpg and the node under test. To receive and verify as well (Receive and verification), add a second Ubuntu node on the far side.
Management link: connect the Ubuntu node’s first interface (ens2) to an External Connector. Use it only for copying the binary (scripts/deploy.sh), ssh and the web UI, never for test traffic.
Data links: connect the next interfaces (ens3, ens4, …) to the device under test. An interface has no carrier until the nodes on both ends of the link are started.
Node size: 2 or more vCPUs (N + 1 or more for --workers N) and 2 GiB of memory or more. Add the hugepages for DPDK (2 GiB for the default 1024 pages).
Initial configuration: in the Ubuntu node’s Config (cloud-init user-data), add your ssh key and, if you want them, ethtool, tcpdump and libpcap0.8:
Deploy: run scripts/deploy.sh from your workstation against the management address (Deploying with scripts/deploy.sh). Restrict sending to the data interfaces with --iface:
Web UI: either set a password and listen on 0.0.0.0:8080, or install the service with --bind 127.0.0.1:8080 and open it through an ssh tunnel (ssh -L 8080:127.0.0.1:8080 ubuntu@192.0.2.10) (Authentication and TLS).
This is the hint wpg doctor prints (with the real path of the running binary). When the backend itself is refused it prints: AF_PACKET needs CAP_NET_RAW: run as root or grant it with `sudo setcap cap_net_raw,cap_net_admin+ep $(command -v wpg)` . The capability is lost when the file is replaced, so re-run setcap after every copy (scripts/deploy.sh does this). For systemd, use AmbientCapabilities=CAP_NET_RAW CAP_NET_ADMIN instead of file capabilities. Capabilities are ignored on filesystems mounted nosuid.
$ wpg ifaces
NAME IDX MAC IPV4 IPV6 MTU STATE SPEED DRIVER
lo 1 - 127.0.0.1 - 65536 up, loopback - -
ifb0 2 66:00:97:e0:50:14 - - 1500 down - -
ifb1 3 62:7f:e1:7b:e9:80 - - 1500 down - -
eth0 4 02:fc:00:00:00:01 192.0.2.2 - 1400 up - virtio_net
(The listing above is from a development container, not CML2.) Pick the interface that is up, has driver virtio_net, and is wired to the link you want to test. Do not send on the management interface that carries your ssh session. Interface names on CML2 Ubuntu nodes are usually ens2, ens3 and so on, one per node interface in the order of the CML2 topology; confirm with the MAC shown in the CML2 UI. Note the MTU column, it decides the maximum frame size (see below). An interface must be up with carrier; an unconnected CML2 interface stays without carrier.
$ wpg doctor
wpg doctor: environment check
✓ Raw sockets are available [caps.raw_socket]
euid/elevated=true CAP_NET_RAW=yes CAP_NET_ADMIN=yes bpf_writable=n/a
✓ At least one usable network interface is up [iface.any_up]
eth0
✓ Kernel supports AF_XDP (5.10 or newer) [kernel.af_xdp]
kernel 6.18.44-fc-v49 (need >= 5.10.0)
✓ Kernel supports TPACKET_V3 [kernel.tpacket_v3]
kernel 6.18.44-fc-v49 (need >= 3.2.0)
! No hugepages configured (only needed for the DPDK backend) [hugepages]
0 x 2048 kB (0 free), 0 x 1048576 kB (0 free)
→ Reserve hugepages and bind the NIC to DPDK: echo 1024 | sudo tee /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages ; sudo dpdk-devbind.py --status (then: --bind=vfio-pci <PCI address>)
- Not applicable on this operating system [npcap]
- Not applicable on this operating system [backend.pcap]
✓ libpcap is available [pcap_lib]
/usr/lib/x86_64-linux-gnu/libpcap.so
5 ok, 1 warning(s), 0 failure(s)
For afpacket only caps.raw_socket (fail if missing), iface.any_up and kernel.tpacket_v3 matter. The hugepages warning is expected unless you use DPDK. wpg doctor --json gives the same list for scripts. Then run a smoke test that leaves the host untouched:
wpg only transmits, and its afpacket socket is created with protocol 0, so it receives nothing and does not need promiscuous mode on the sending node. The restrictions are on the path:
Internal links between CML2 nodes are virtual links implemented by the CML2 host. Frames whose source MAC is not the one the node interface was created with may be filtered or the link may learn it inconsistently. If you generate frames with spoofed source MACs (ARP sweeps, DHCP load with many client MACs) and the peer never sees them, test first with the interface’s real MAC as source, then consider that the CML2 link may be dropping the rest. This has to be confirmed on your CML2 version.
External connector (bridge to the CML2 host network or to a physical NIC): the host bridge and any upstream switch may apply port security or MAC learning limits. High-rate multi-MAC traffic can flood the CAM table of a physical switch; use only on an isolated segment.
The receiving side (a CML2 router, or a second Ubuntu node running tcpdump) needs promiscuous mode only if the destination MAC is not its own: sudo ip link set ens3 promisc on.
Use of wpg is limited to lab networks you are authorised to test; see the legal notice.
The afpacket ring sizes its slots from the interface MTU (capped at 9216); frames larger than a slot go out through a second plain socket. The frame length you send is Ethernet header plus payload without FCS, so a 1514-byte frame needs MTU 1500. The example above shows an MTU of 1400 on a virtio interface: a 1514-byte frame will then be rejected by the kernel (EMSGSIZE) and counted as dropped. Check wpg ifaces, keep template frames at MTU + 14, or raise the MTU on both ends (sudo ip link set ens3 mtu 9000). Jumbo frames also need every CML2 link and the peer node to allow them, which depends on the CML2 version and node type.
Unmeasured on CML2. What we have is the lo baseline in docs/perf.md: about 1.4–1.9 Mpps for 64-byte frames from one thread, bounded by the loopback receive path. On virtio-net the limit is the guest-to-host path: the virtio TX queue, the vhost thread on the CML2 host and the virtual link, so expect a value below the lo figure and strongly dependent on the host. Tuning switches worth trying, in order: --qdisc-bypass, larger --batch, --pin-core, and comparing --no-tx-ring. Record your results using the checklist in docs/perf.md.
--qdisc-bypass sets PACKET_QDISC_BYPASS so frames skip the qdisc layer. On lo it improved 64-byte throughput by about 30%. The consequences: no tc shaping, priority or netem on that interface applies to the traffic, packets are not counted by the qdisc statistics (tc -s qdisc), and if the device queue is full the frame is dropped (the kernel returns ENOBUFS) instead of waiting in the qdisc. wpg retries for a short time and then counts the frames as dropped. Leave it off when you rely on netem for impairments.
Offloads change what the receiver shows, not what wpg sent. With GRO or LRO enabled, tcpdump on a Linux receiver can display merged super-frames larger than the MTU for TCP streams; disable it for accurate captures: sudo ethtool -K ens3 gro off lro off. Checksum offload on the sending node does not matter for wpg, because wpg builds the full frame including checksums itself (bad checksums you asked for are sent as is). On virtio guests, captures on the sender may show “incorrect checksum” only for packets the kernel’s own stack produced, not for wpg frames. Use ethtool -k to see the current settings.
The default; start here. Needs nothing extra and coexists with the management interface
CAP_NET_RAW (above)
afxdp
Worth comparing when afpacket tops out. virtio-net zero-copy needs a recent kernel; mostly you get copy mode, and the gap to afpacket is small
built with --features afxdp, kernel 5.10+, enough queues (ethtool -L) (AF_XDP)
dpdk
Takes the interface away from the kernel with the virtio PMD. The host’s vhost path is the limit, so the gain depends on the host
built with --features dpdk, hugepages, vfio no-IOMMU mode or uio_pci_generic (CML2 and virtio)
pcap
Only to follow the same procedure as on other OSes
built with --features pcap, libpcap0.8
With any backend, check the frames with --backend pcapfile first and start with a low --rate. To compare performance, run wpg bench with each backend on the same link (Performance tuning) and record the results with the checklist in docs/perf.md.
The templates in templates/interop/ check that a peer router accepts packets built by wpg. wpg is a stateless sender (no TCP sessions, no database synchronization), so each scenario checks one state change you can observe on the peer.
Verification status: the seven scenarios below were checked against FRR 8.4.4 with scripts/interop/frr-netns.sh (network namespace + veth, afpacket backend) on 2026-10-12, all PASS. The IOS-XR / Junos configuration examples and show commands come from general knowledge and have not been checked on real devices or CML2 yet. Please report differences in an issue.
OSPFv2 Hellos listing the peer’s router ID (priority 0), every 10 s
Neighbor 2-Way/DROther (never Full)
ripv2-response-route
RIPv2 Responses with 198.51.100.0/24 (metric 1) and 203.0.113.0/25 (metric 5), every 30 s
R routes in the RIB (metric + 1)
isis-l2-lan-hello-up
L2 LAN IIHs listing the peer’s MAC in the IS Neighbors TLV (priority 0, padded to the MTU), every 3 s
L2 adjacency Up
vrrpv3-preempt
VRRPv3 Advertisements with priority 200 (VRID 5), every 1 s
The peer is Backup; about 3.6 s after wpg stops it is Master again
pim-hello-neighbor
PIMv2 Hellos (105 s holdtime, DR priority 0), every 30 s
wpg in the PIM neighbor list
ldp-link-hello
LDP link Hellos (224.0.0.2, 15 s hold time), every 5 s
A Link entry in LDP discovery (no TCP session)
bfd-session-up
BFD control packets in state Down, then in state Up with the peer’s discriminator, every 300 ms
BFD session Up; Down one detection time (about 0.9 s) after wpg stops
By default wpg is 10.99.0.1 and the peer 10.99.0.2/24; change them with --var (see each template’s variables). Passing --var src_mac=<MAC of the sending interface> makes the peer’s tables easier to read.
BFD takes two steps: send a few packets with --var state=down so the peer moves to Init, read the peer’s local discriminator, then keep sending state Up with --var peer_discriminator=<value>.
sudo apt install frr iproute2 # the frr service does not need to run
cargo build -p wpg-cli
sudo scripts/interop/frr-netns.sh # runs every scenario and prints PASS / FAIL
The script creates the namespace wpgfrr, starts the FRR daemons (zebra, ospfd, ripd, isisd, vrrpd, pimd, ldpd, bfdd) directly behind a veth pair (wpg0 ⇔ frr0), sends each template with wpg send --backend afpacket and checks the vtysh output. It cleans up on exit; with --keep the lab stays up and sudo ip netns exec wpgfrr vtysh -N wpg opens a shell in it (--down removes it).
Example output:
PASS OSPFv2 neighbor 2-Way
PASS RIPv2 routes installed
PASS IS-IS L2 adjacency Up
PASS VRRPv3 peer is Backup
PASS PIM neighbor
PASS LDP link discovery
PASS BFD session Up
The FRR configuration applied by the script is below. FRR’s VRRP needs a macvlan vrrp4-2-5 carrying the virtual MAC 00:00:5e:00:01:05, which the script creates.
router ospf
ospf router-id 10.99.0.2
network 10.99.0.0/24 area 0
router rip
version 2
network 10.99.0.0/24
router isis 1
net 49.0001.0000.0000.0002.00
is-type level-2-only
interface frr0
ip router isis 1
ip pim
vrrp 5 version 3
vrrp 5 priority 100
vrrp 5 ip 10.99.0.254
mpls ldp
router-id 10.99.0.2
address-family ipv4
discovery transport-address 10.99.0.2
interface frr0
bfd
peer 10.99.0.1 interface frr0
receive-interval 300
transmit-interval 300
Connect the sending interface of the Ubuntu node running wpg and the router’s interface to the same segment (prepare the node as in CML2 notes first).
Apply the configuration below to the router (adjust interface names).
For each scenario run wpg send templates/interop/<name>.wpg.yaml -i <iface> --backend afpacket --var src_mac=<MAC> ... and look at the state with the show commands. IS-IS needs --var peer_mac=<router MAC>; BFD needs the two steps above.
set interfaces ge-0/0/0 unit 0 family inet address 10.99.0.2/24 vrrp-group 5 virtual-address 10.99.0.254 priority 100
set interfaces ge-0/0/0 unit 0 family iso
set interfaces ge-0/0/0 unit 0 family mpls
set interfaces lo0 unit 0 family iso address 49.0001.0000.0000.0002.00
set protocols vrrp version-3
set protocols ospf area 0 interface ge-0/0/0.0
set protocols rip group wpg neighbor ge-0/0/0.0
set protocols isis interface ge-0/0/0.0 level 1 disable
set protocols pim interface ge-0/0/0.0
set protocols mpls interface ge-0/0/0.0
set protocols ldp interface ge-0/0/0.0
set routing-options static route 192.0.2.0/24 next-hop 10.99.0.1 bfd-liveness-detection minimum-interval 300 multiplier 3
No stateful procedures (a non-goal): OSPF never reaches Full (wpg stays a priority-0 DROther), there are no LDP / BGP TCP sessions, no IS-IS LSP flooding and no automatic BFD state machine. BGP needs TCP and is not covered.
RIP routes time out 180 s after the last Response; keep sending to keep them.
IPv6 scenarios (OSPFv3, RIPng, MLD, VRRPv3 for IPv6) are not included because IPv6 was disabled in the environment used for verification.
The source MAC of the VRRP template is the virtual MAC 00:00:5e:00:01:<VRID>; change src together with the VRID.
Measure first with wpg bench and compare against the baseline in docs/perf.md (measured on lo, null and a veth / dummy interface on WSL2, not on real NICs; afpacket reached 2-3 Mpps on one core on WSL2). All flags below exist on both wpg send and wpg bench unless noted; see the CLI reference.
Change one thing at a time and measure each step with wpg bench (64-byte frames, about --duration 10 seconds).
Know the ceiling: wpg bench --backend null measures frame generation without any transmit path. If a real interface gets close to it, wpg itself is the limit.
afpacket basics: --qdisc-bypass (unless you need tc), --batch 64 / 128 / 256 / 512, and a comparison with --no-tx-ring.
Pin cores: --pin-core (one worker) or --auto-pin. Keep the thread on the NIC’s NUMA node with its interrupts, and set the governor to performance (see “IRQ affinity and CPU governor” below).
Add workers: --workers 2, 4, … up to the number of queues, until the gain stops.
Change the backend: if that is still not enough, try afxdp (--xdp-mode, --busy-poll), then dpdk (DPDK and AF_XDP setup). On virtual NICs (CML2 and the like) the gap is often small; see CML2 notes.
Tighten the rate: run wpg send at the target rate, read the achieved rate and lateness in the final report, and adjust --wait / --clock (see “Rate accuracy” below).
Record it: keep the environment (CPU, NIC and driver, kernel, command, wpg bench --json output) with the checklist in docs/perf.md.
--pin-core <CORE> pins the transmit thread to one CPU core (Linux). This stops the scheduler from migrating the thread and keeps caches warm. Choose a core that is not handling the NIC’s interrupts, and preferably on the same NUMA node as the NIC (lscpu, /sys/class/net/<if>/device/numa_node). Keep the core free of other work (isolcpus or taskset for the rest).
--batch <N>: frames handed to the backend per send_batch call. Larger batches mean fewer system calls (default 64 for wpg send, 256 for wpg bench). Too large a batch delays the rate limiter’s pacing granularity. Try 64, 128, 256, 512.
--ring-depth <N>: number of frame slots in the afpacket TX ring (default 4096). The ring (and the afxdp UMEM) is capped at --ring-bytes / tx.ring_bytes in total (default 16 MiB), so on lo (16 KiB slots) the effective depth is 1024. Larger rings absorb bursts; they do not raise the sustained rate.
--burst <N> (wpg send): frames sent back to back per rate-control grant.
These defaults suit most hosts; raise them only for the cases below. Each has a wpg.toml key (see the configuration reference) and, for wpg send, a command-line override.
tx.prerender_max_bytes (256 MiB) and tx.prerender_max_packets (100M), --prerender-max-bytes / --prerender-max-packets: streams within both limits are built in full before sending (--plan auto); larger ones are built on the fly. Pre-rendered streams reach higher rates for templates whose fields cannot be patched in place, so raise the limits when such a stream just misses them and the host has the memory to spare. Lower them on small hosts that run several jobs at once.
tx.ring_bytes (16 MiB), --ring-bytes: memory per worker for the afpacket TX ring or the afxdp UMEM. Raise it together with --ring-depth when jumbo frames or long bursts need a deeper ring than 16 MiB holds. The memory is locked, so raise ulimit -l (systemd: LimitMEMLOCK) to match.
server.preview_max (256): most packets one preview request may render. Raise it to look further into a stream in one request; each request renders that many packets on the server.
server.job_history (100) and server.job_history_db (1000): finished jobs listed on the Jobs screen and kept in the database.
The default afpacket path is the TPACKET_V3 TX ring: frames are copied into shared slots and a single send() per batch triggers transmission. --no-tx-ring uses sendmmsg instead (also the automatic fallback when the ring cannot be set up). On lo the two are within a few percent of each other (ring 1.44, sendmmsg 1.50 Mpps in release); on other drivers the result can differ, so measure both. The bench header prints which mode is active.
The rate limiter (wpg send) is a token bucket. --wait chooses how it waits:
--wait
Behaviour
CPU
hybrid (default)
sleep while the deadline is far away, busy-spin for the last 50 microseconds
a little, proportional to waiting
sleep
sleep only
lowest
spin
busy-spin only
a full core
busy-poll
spin and service the backend meanwhile (afxdp: reap the completion ring, wake the driver); the default with --busy-poll
a full core
Precision work (M8):
The TX thread’s timer slack is lowered from 50 to 1 microsecond (PR_SET_TIMERSLACK, Linux, for the modes that sleep).
Oversleep is learned: a moving average of how late sleep actually woke up is subtracted from the next sleep (the rest is spun away).
The clock is the TSC (--clock auto, the default): on x86_64 with an invariant TSC (CPUID, or the Linux flags constant_tsc and nonstop_tsc) the pacer reads rdtsc directly, with the frequency calibrated once against Instant. A read costs a few ns, so each spin iteration is shorter and the lateness about halves. --clock monotonic restores Instant; where the TSC is not usable the pacer falls back automatically.
Measurements in a container on lo (including a before/after comparison) are in docs/perf.md, section “Rate control precision”. In short: hybrid at 1 kpps went from 74.5 to 10-15 microseconds mean lateness, sleep from 62-121 to 2-44 microseconds; the median lateness of the spinning modes is about 0.1 microsecond (0.03-0.06 with the TSC). The maximum (milliseconds) comes from the shared host taking the vCPU away and no wait mode removes it; for precision combine --pin-core / --auto-pin with isolcpus.
When the interval between frames is shorter than the wait resolution, the limiter grants several frames at once (--burst), so the average rate follows the target while individual spacing does not. ifg=<duration> forces a burst of 1. If the achieved rate is lower than requested, the path is the limit: a catch-up burst is capped (10 ms) so it never floods after a stall. The final report of wpg send includes the achieved ratio and the lateness (max, mean); --json also has p50 / p99 (p50_jitter, p99_jitter) and whether the TSC was used (tsc).
--workers N starts N TX workers. Each has its own thread and backend instance (afpacket: socket and TX ring; afxdp: socket and UMEM; dpdk: a TX queue of the shared port) and gets 1/N of the stream and of the rate (--rate 1Mpps --workers 4 is 250 kpps per worker). Counters are summed and the final report lists every worker. pcap and pcapfile are limited to one worker.
Queue assignment:
afxdp: worker i binds NIC queue --queue + i. Create the queues with ethtool -L <if> combined N (DPDK / AF_XDP).
dpdk: the port is configured with --queue + N TX queues and worker i sends on TX queue --queue + i; the mbuf pool lives on the NIC’s NUMA socket (DPDK).
afpacket: the queue cannot be chosen directly; the kernel derives it from the sending CPU. With XPS configured (/sys/class/net/<if>/queues/tx-<n>/xps_cpus) it follows that map, otherwise with --qdisc-bypass it is “CPU number mod queue count”. Pinning the workers to different cores spreads them over different queues.
# 4 workers on a 4-queue NIC, pinned automatically to cores on the NIC's NUMA node
wpg bench --backend afxdp -i ens3 --workers 4 --auto-pin
wpg send tpl.wpg.yaml -i ens3 --backend afpacket --qdisc-bypass --workers 4 --pin-cores 2,3,4,5
--auto-pin pins the workers, in order, to cores on the NIC’s NUMA node (/sys/class/net/<if>/device/numa_node). Only cores the process may use (taskset / cgroup cpuset) are chosen, and core 0 (which tends to collect interrupts and housekeeping) comes last. Without NUMA information (virtual NICs, single-node machines) the allowed cores are used in order. --pin-cores takes precedence.
Workers allocate their frame buffers after pinning themselves, so Linux’s first-touch policy puts that memory on the core’s node.
The afxdp UMEM is allocated on the NIC’s node (mbind(MPOL_PREFERRED)); the backend line shows NUMA node N.
Keep the (TX completion) interrupts on cores of the same node too (see “IRQ affinity” below).
Spread or pin NIC interrupts: cat /proc/interrupts | grep <if>, echo <mask> | sudo tee /proc/irq/<n>/smp_affinity; stop irqbalance when pinning by hand. Keep the TX thread and the TX-completion IRQ on the same NUMA node (not necessarily the same core).
Use the performance governor: sudo cpupower frequency-set -g performance. In a VM the governor is controlled by the host; ask the CML2 host admin.
Disable deep C-states if latency spikes appear; disable SMT sibling load on the pinned core.
wpg has one admin account (admin). Its password comes from the first of these sources that is set:
wpg serve --password <PASSWORD>. Other users can see it in the process list, so use it only for quick tests.
wpg serve --password-file <FILE>. The file holds the plain-text password or an argon2 hash.
The environment variable WPG_PASSWORD, holding the plain-text password or an argon2 hash.
auth.password_hash in wpg.toml. wpg config set-password prompts for the password and writes this key. Use --stdin in scripts.
auth.password_file in wpg.toml, holding the plain-text password or an argon2 hash.
The first-run setup screen, when none of the above is set.
wpg hashes plain-text passwords with argon2id at startup (19 MiB, 2 passes; the OWASP recommendation) and stores the hash in <data_dir>/wpg.sqlite. If a configured password changes, the next start ends all existing sessions.
every API endpoint except /api/health, /api/version, /api/auth/* and /api/docs answers 401 setup_required;
wpg serve refuses to listen on a non-loopback address and exits with status 2. Pass --allow-insecure to override this. wpg then logs a loud warning, and anyone who can reach the server can set the password;
the setup screen accepts requests only from the machine itself (loopback), unless the server runs with --allow-insecure.
wpg serve # 127.0.0.1:8080: open it and set the password
wpg config set-password # or: store a hash in wpg.toml first
wpg serve --bind 0.0.0.0:8080 --tls # now any address is fine
After a login, the wpg_session cookie identifies the session. It holds a random 256-bit token and is set with HttpOnly; SameSite=Strict, plus Secure over HTTPS. The database stores only the token’s SHA-256.
Sessions last auth.session_ttl_secs (default 24 h). Every request extends the session.
POST, PUT and DELETE requests must send the session’s CSRF token in X-CSRF-Token. The server returns the token in GET /api/auth/status and in the readable wpg_csrf cookie. Login and setup take no CSRF token, but they accept only Content-Type: application/json.
WebSocket connections (/api/ws, and /api/terminal/ws of the web terminal) need the session cookie. The server rejects them when their Origin names another host. See Web terminal for the terminal’s safeguards.
API: GET /api/auth/status, POST /api/auth/setup {password, confirm}, POST /api/auth/login {password, username?}, POST /api/auth/logout. See the API reference.
Scripts, CI jobs and wpg remote authenticate with an API token instead of a session:
# on the server (also while it runs); prints only the token
wpg config token create ci --expires-days 90
# or on the web UI: Settings → API tokens
export WPG_SERVER=https://wpg.example.net:8080 WPG_TOKEN=wpg_...
wpg remote jobs start examples-arp-sweep --backend null --count 100 --wait
curl -H "Authorization: Bearer $WPG_TOKEN" "$WPG_SERVER/api/jobs"
A token is wpg_ followed by 64 hex digits (256 random bits). wpg shows it once, when you create it. The database stores only its SHA-256, and the first 12 characters (wpg_ + 8 hex digits) to tell tokens apart. A fast hash is enough because the token is random and long; argon2 would cost every API request about 19 MiB and tens of milliseconds.
A request with Authorization: Bearer <token> acts as the user who created the token, with that user’s role. There are no finer scopes. It needs no CSRF token and gets no cookies.
When a request carries an Authorization header, wpg ignores the session cookie. A wrong, expired or revoked token answers 401 unauthorized. Failures count against the login rate limit of the client address.
A token can expire after 1 to 3650 days, or never. It survives password changes. Revoking it (web UI, wpg config token revoke <id>, DELETE /api/tokens/{id}) deletes it; the next request with it fails.
Tokens cannot manage tokens: /api/tokens answers 403 forbidden to bearer requests. Create and revoke tokens with a session (the web UI) or with wpg config token on the server. Admins see and revoke every user’s tokens; other users see only their own.
The audit log records token creation, revocation and rejected tokens. wpg records each token’s last use, at most once a minute.
API: GET /api/tokens, POST /api/tokens {name, expires_in_days?} (returns {token, secret}), DELETE /api/tokens/{id}.
wpg remote checks the server certificate. For a server with a self-signed certificate, pass --insecure.
wpg allows 5 failed logins per minute per client address. After that, the client is locked out for 1 minute. The lock-out doubles each time it happens again, up to 15 minutes. While a client is locked out, logins answer 429 rate_limited with details.retry_after_secs and a Retry-After header.
wpg records every attempt in the login_attempts table and keeps it for 30 days. Behind a reverse proxy, all clients share the proxy’s address.
Logins, failed logins, logouts, the setup, API token creation, revocation and rejection, and every job action (create, start, pause, resume, stop, delete) are logged with the wpg::audit target, the user and the client address. To log only these events, set RUST_LOG=wpg::audit=info.
wpg serve --tls, or server.self_signed_tls = true, generates a self-signed ECDSA certificate into <data_dir>/tls/ and reuses it on later starts. wpg renews it about a month before it expires; certificates are valid for 800 days. It covers localhost, 127.0.0.1, ::1, the bind address and the host name. wpg prints the certificate’s SHA-256 fingerprint at startup, so you can compare it with the one your browser shows.
To use your own certificate, set [server.tls] cert = "...", key = "..." (PEM files) in wpg.toml. This takes precedence over the self-signed certificate.
The server does not answer plain HTTP on a TLS port and does not redirect to HTTPS.
Behind a reverse proxy (nginx, Caddy), keep wpg on 127.0.0.1 and let the proxy handle TLS. Forward the WebSocket upgrade for /api/ws.
Every response carries a Content Security Policy (default-src 'self'; scripts only from the server plus 'wasm-unsafe-eval' and the hashes of the inline bootstrap script; inline styles; connect-src 'self' ws: wss:; frame-ancestors 'none'), plus X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer and a restrictive Permissions-Policy. The Swagger UI under /api/docs gets a relaxed policy that allows its inline scripts.
The wpg UI can open SSH and Telnet sessions to the devices under test. While sending packets you can watch the counters and logs of the peer without a separate terminal program (Tera Term, PuTTY) or jump host. The wpg server opens the connections, so it also works in labs where your PC cannot reach the devices directly (CML2, EVE-NG, closed hardware labs). The design is recorded in ADR-0011.
Seconds without input or output before a session is closed; 0 = never
max_sessions
8
Simultaneous sessions
Environment variables work as well (WPG_TERMINAL__ENABLED=true, WPG_TERMINAL__ALLOWED_TARGETS='["10.0.0.0/8"]'). Check the file with wpg config validate; it warns when the terminal is enabled with an empty allowed_targets on a non-loopback address.
Host name entries match the name the user typed. Address and CIDR entries match the addresses the name resolves to, and the session only connects to the addresses that matched. The checked address is the one dialled, so changing DNS afterwards cannot bypass the list.
In the connection form choose the protocol (SSH / Telnet), host, port (default 22 / 23) and, for SSH, the user name.
Choose the SSH authentication:
Password: log in with the password. Leave it empty to get the server’s prompts (keyboard-interactive, password) in a dialog. A single hidden keyboard-interactive prompt (“Password:”, common on network devices) is answered with the password you entered.
Private key: paste or load an OpenSSH, PEM or PuTTY .ppk key; passphrases are supported.
Connect opens a new tab. Several tabs can be open at once and they stay connected while you use other pages.
Save profile stores the protocol, host, port, user name, authentication method and log options in the browser (localStorage). Passwords, keys and passphrases are never stored.
For a host it has not seen before, wpg shows the host key algorithm and its SHA-256 fingerprint (SHA256:...). Compare it with the real value before you continue, for example:
run ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub on the host itself;
run ssh-keyscan -p 22 host | ssh-keygen -lf - from a machine with a trusted path to the host;
on network devices, convert the public key the device shows (show crypto key mypubkey rsa on Cisco IOS, …) to OpenSSH format and pass it to ssh-keygen -lf; some devices print MD5 or hexadecimal fingerprints, so compare like with like.
Then choose:
Trust and save: store the key in the server’s <data_dir>/terminal/known_hosts (OpenSSH known_hosts format) and connect. Later connections are silent.
Connect once: connect without storing the key (useful for lab devices that are rebuilt often).
Cancel: do not connect.
When a host presents a key that differs from the stored one, the connection is refused (possible man-in-the-middle) and both keys are shown. If the change is expected (device replaced, keys regenerated), remove the stored key under Known hosts at the bottom of the page and connect again to confirm the new key. You can also pre-seed known_hosts with lines from ssh-keyscan.
Under Jump hosts (ProxyJump) in the connection form, press Add jump host and enter the SSH servers to go through, nearest first (up to 4). It works like OpenSSH’s ssh -J ops@bastion,ops@inner admin@r1: one click reaches the device behind them, without typing ssh on each hop.
The wpg server only connects to jump host 1 itself. Jump host 2 and the device are reached through a direct-tcpip channel opened on the previous jump host (the mechanism behind OpenSSH’s -W / ProxyJump), so AllowTcpForwarding must be enabled in the jump hosts’ sshd.
Each hop’s host key is confirmed on first contact, exactly as for a direct connection. The dialog and the login prompts name the hop they belong to.
Each hop logs in with its own password or private key. An empty password is asked for at that hop. Keys and passwords are not stored in profiles (the jump hosts’ host, port and user are).
The final target can be SSH or Telnet, so a terminal server’s Telnet ports behind a bastion work too.
When something fails, the error names the jump host it happened at (for example “at jump host 10.0.0.1:22”).
The audit log and GET /api/terminal/status record the jump hosts (via).
Allowlist: allowed_targets applies to every hop, jump hosts included. The first jump host is resolved by the wpg server and checked like a direct target. Later hops and the final target are resolved by the jump host, so a host name is matched against name entries (r1.lab, *.lab.example) and an IP address against address / CIDR entries; a target given by name is not admitted by a CIDR entry.
The Telnet client only answers the server’s option requests (ECHO, SGA, NAWS window size, terminal type XTERM-256COLOR, BINARY); it never starts a negotiation, so raw TCP console ports of terminal servers (CML2, EVE-NG consoles) never see stray bytes. Enter is sent as CR NUL as the NVT rules require.
Select (copied automatically by default), Ctrl+Shift+C, Ctrl+Insert
Paste
Right / middle click, Ctrl+Shift+V, Shift+Insert, Ctrl+V
Find
The search box of the toolbar (Enter: next, Shift+Enter: previous)
Pasting several lines shows the line count and a preview first (can be switched off). With a pause per line each line is followed by that many milliseconds, for consoles with small input buffers when pasting configurations. Send file sends a text file after the same confirmation.
Browsers only allow the clipboard API on HTTPS or localhost. Over plain HTTP from another host, right-click paste cannot read the clipboard: use Ctrl+V or Shift+Insert (copying still works).
Font size, scrollback lines, copy on select, right-click paste, paste confirmation and the pause per line are in Terminal settings and stored in the browser.
With record_sessions = true, “Record a session log on the server” (on by default) writes what the remote side sends to <data_dir>/terminal-logs/, named like 20261027-101500_ssh_r1.lab_22_ab12cd34.log (UTC time, protocol, host, port, session id).
Only the remote side’s output is recorded, never your keystrokes (input that is not echoed, such as passwords, does not end up in the log).
By default escape sequences are removed (Tera Term’s “plain text”); without stripping, cat replays the log with colours.
Start log / Stop log in the toolbar control recording during a session; restarting creates a new file.
A file stops growing at 256 MiB (the session continues).
The Session logs list downloads and deletes logs; a log that is being written cannot be deleted.
Logs are never deleted automatically; manage retention yourself. Files are created with mode 0600.
Without server-side logs, Save scrollback saves the terminal’s scrollback from the browser as a text file.
With the web terminal enabled the wpg server becomes a jump host into the test network:
Restrict the targets: list only the lab ranges in allowed_targets. Left empty, everyone who can log in to wpg can reach any host the server can reach, management networks and internal servers included.
Use HTTPS: passwords and private keys travel from the browser to the wpg server. Away from localhost use wpg serve --tls or a certificate (Authentication and TLS).
No stored secrets: wpg never writes passwords, keys or passphrases to disk; they stay in memory while the connection is being set up. The browser tab keeps what you typed in memory for reconnecting until the tab is closed.
Audit trail: session start, end and refusals are logged with target wpg::audit (wpg user, client address, target, login name, end reason, bytes in / out, log file). RUST_LOG=wpg::audit=info prints only audit events. Storing / removing host keys and deleting logs are logged as well.
Access: only logged-in users; the WebSocket only accepts same-origin connections (including a reverse proxy configured in server.base_url). Users with the viewer role (P1) cannot open sessions.
Idle timeout and session limit: abandoned sessions are closed after idle_timeout_secs; max_sessions caps simultaneous sessions.
Legacy algorithms: to reach old network equipment, SHA-1 key exchange and CBC ciphers are offered with the lowest priority; they are only used when the device offers nothing better.
Configuration (enabled, allowlist) and running sessions
GET
/api/terminal/ws
Session WebSocket (403 terminal_disabled when disabled)
GET
/api/terminal/logs
Session logs
GET / DELETE
/api/terminal/logs/{name}
Download / delete a log
GET
/api/terminal/known-hosts
Stored host keys
DELETE
/api/terminal/known-hosts/{host}/{port}
Remove a host key
On the WebSocket, send {"type":"connect","protocol":"ssh","host":"r1","username":"admin","password":"...","cols":120,"rows":40} first. The server answers with JSON state, host_key, auth_prompt, connected, record, error and closed messages and sends the terminal output as binary frames; keystrokes go back as binary frames (or {"type":"input","data":"..."}). See TerminalClientMessage / TerminalServerMessage in the API reference.
Receives through an AF_PACKET raw socket; -i any means every interface. Needs CAP_NET_RAW (Permissions). Frames this host transmits (PACKET_OUTGOING) are not counted by default, so a frame sent and received on lo counts once; --include-outgoing counts them too. Kernel drops (PACKET_STATISTICS) are shown as dropped.
pcap
all
libpcap / Npcap, only in builds with --features pcap. Excluding outgoing frames (pcap_setdirection) does not work on Windows.
pcapfile
all
Replays a pcap / pcapng file as if its frames had just been received, with their recorded timestamps. Give the file path instead of an interface. Useful without a NIC and for offline verification of a capture taken elsewhere. Replay is instant, so pps / bps are processing speeds.
Filters are a subset of tcpdump / BPF, evaluated in user space the same way on every backend. An empty filter matches every frame. Keywords are case-insensitive.
Syntax
Meaning
and / &&, or / ||, not / !, ( )
Logic (precedence not > and > or)
ether [src|dst] host <MAC>, ether src <MAC>
Source / destination MAC (00:11:22:33:44:55, 0011.2233.4455)
ether proto <number|ip|ip6|arp|vlan|mpls|lldp|pppoe>
EtherType (tag types count too)
broadcast, multicast (optionally after ether)
All-ones / multicast destination MAC
vlan [<id>]
Has an 802.1Q / 802.1ad tag (with an id: any tag with that id)
mpls
MPLS (0x8847 / 0x8848)
ip, ip6, arp, rarp
L3 type
tcp, udp, sctp, icmp, icmp6
Transport type
ip proto <number|name>, ip6 proto …, proto …
IP protocol number (names such as ospf, gre, pim, vrrp work too)
With wpg send --signature (job option "signature": true, the checkbox under Advanced in the web UI’s new-job form) every frame gets a signature written over its last 24 octets:
Offset
Size
Content
0
4
magic 57 50 47 01 ("WPG" + version 1)
4
4
stream id (big endian)
8
8
sequence number within the stream (from 0)
16
8
TX time (nanoseconds since the Unix epoch)
Only payload (or Ethernet padding) is overwritten. The sender reads the Ethernet / 802.1Q / 802.1ad / IPv4 / IPv6 (extension headers) / UDP / TCP / ICMP / ICMPv6 headers; a frame with fewer than 24 octets after them is sent unchanged and counted as unsigned in the statistics. Give the template a payload of at least 24 octets when using signatures.
UDP / TCP / ICMP / ICMPv6 checksums are updated incrementally (RFC 1624), so receivers do not see bad checksums. An IPv4 UDP checksum of 0 (not computed) stays 0. Other checksums (OSPF, GRE, …) are not updated.
The stream id is --stream-id (default 0) plus the stream index; the streams of a mixed job get distinct ids.
Sequence numbers count per stream from 0 across all workers. The TX time is taken once per batch.
The receiver looks for the magic within the last 92 octets of a frame, so Ethernet padding or an FCS added on the way do not hide it.
With --verify (verify in the API / web UI) the signatures of the matching frames are aggregated per stream:
Field
Meaning
received
Frames received with that stream id
seq first–last
Lowest / highest sequence number received
lost
(highest − lowest + 1) − distinct sequence numbers received. An estimate: frames before the lowest one (sent before the capture started) and frames still in flight at the time of the snapshot are not counted.
reordered
Frames that arrived after a higher sequence number
duplicates
Sequence numbers received twice within the last 65536 sequence numbers (older repeats count as reordered)
latency min / avg / max
Receive time − TX time
jitter
Mean absolute difference between consecutive latencies
Latency is only meaningful when sender and receiver share a clock (same host, or hosts synchronised with PTP / NTP); negative values mean the clocks disagree. It includes the kernel network stack and batching.
Start wpg capture -i eth1 --verify -f "udp and wpg" on the receiver first, then wpg send … --signature on the sender. For latency, synchronise both clocks (PTP, …); loss and ordering do not need synchronised clocks.
On the “Receive” page choose the backend, interface, filter (checked while you type), packet / time limits, how many leading frames to keep, a file name to save to and whether to verify signatures, then start. While it runs, counters and rates update live over the WebSocket, with the per-stream verification results and the list of the first N frames (hex and decoded view of the selected one). A saved pcapng file can be downloaded once the capture has ended.
For the pcapfile backend the input file and the save file are file names under captures/ in the server’s data directory (where job dry runs are written too).
Filters are evaluated in user space and afpacket calls recvfrom once per frame, so receiving more than a few hundred thousand pps is out of scope; at high rates compare the receive counters with NIC or switch statistics.
Saved pcapng files have microsecond timestamps.
Full capture features (ring buffers, file rotation, kernel BPF) are out of scope.
Besides generating packets, wpg contains an SNMP manager (the “SNMP” screen of the web UI; API /api/snmp). Reading device counters while you load a link, or checking that a trap is sent, no longer needs a second tool. See ADR-0010 for the design.
Get / GetNext / GetBulk / Walk / Set (SNMPv1 / v2c / v3)
SNMPv3 USM: noAuthNoPriv / authNoPriv / authPriv, authentication MD5 / SHA-1 / SHA-224 / SHA-256 / SHA-384 / SHA-512, privacy DES / AES-128 / AES-192 / AES-256; the agent’s engine ID is discovered automatically
sending and receiving traps / informs
MIB upload, and readable values using names, enumerations, display hints and units
pcap / pcapng recording of the exchanged SNMP datagrams
Fill in the target (host, port, version; the community for v1 / v2c; user, security level, protocols and pass phrases for v3). “Save profile” keeps a target in the browser. Communities and pass phrases are never stored, neither in profiles nor on the server.
Type object names or OIDs; names from the loaded MIBs are completed (ifDescr.1, IF-MIB::ifTable, sysUpTime.0, 1.3.6.1.2.1.1). Separate several with spaces. “Pick from the MIB tree” opens the tree (whole, or one module) with check boxes that add OIDs to the field and remove them; scalars get .0.
Press Get / GetNext / GetBulk / Walk. What each one does is listed below the buttons:
Get: the value of the given instances: sysDescr.0 for a scalar, ifDescr.1 with the INDEX for a table column. sysDescr or ifTable alone return noSuchObject (the result says so).
GetNext: the one instance after each OID.
GetBulk (v2c / v3): up to “Max repetitions” instances after each OID in one request. It does not stop at the end of the subtree, so values outside the requested OIDs are included.
Walk: the whole subtree of each OID, in the order given. It uses GetBulk on v2c / v3 and GetNext on v1 and stops at the end of the subtree, at endOfMibView or at an OID that does not increase. An instance such as sysDescr.0 gives the Get result. “Walk limit” counts all OIDs together.
The result names the OIDs that were requested.
Results are shown as name / type / value, formatted with the MIBs:
Raw
Shown
ifOperStatus 1
up(1)
MacAddress (1x:)
00:1b:21:aa:bb:cc
DateAndTime
2026-10-27,13:5:9.4,+9:0
TimeTicks 12345678
1d 10:17:36.78 (12345678)
hrMemorySize 8048
8048 KBytes (UNITS)
OID value
IF-MIB::ifIndex
“Raw values” shows the values without MIB knowledge. When a walk contains a conceptual table (ifTable, …) it is also shown as rows × columns keyed by its INDEX (“Table view”). Values outside the tables (ifNumber.0 when walking interfaces, …) are listed above them.
“Auto refresh” repeats the last read operation at the chosen interval. Counter32 / Counter64 values get a per-second rate from the previous poll and a sparkline (Counter32 wraps are handled).
Set takes rows of OID / type / value. Type auto takes the type from the MIB and accepts enumeration labels such as down; for objects no MIB knows, choose a type (net-snmp’s iucCtaosxdn). Agent errors (notWritable, wrongType, …) are shown with the result.
Receive: choose the listen address (default 0.0.0.0:162), the accepted communities (empty = any) and v3 users, then “Start”. Notifications appear at once (WebSocket snmp_trap events) with the trap OID and bindings named by the MIBs. Informs are acknowledged automatically. Messages with a wrong community or failed authentication are listed too, with the error.
Send: with the target of the Operations tab, send a trap OID (linkDown, …) and bindings (one per line: “object value” or “object type value”). On v1 the notification is converted to a Trap-PDU (RFC 3584). “Inform” waits for the acknowledgement. For v3 traps wpg is the authoritative engine.
Drop MIB files (.mib.txt.my) on the upload area or use “Choose files”. They are stored under <data_dir>/mibs (setting snmp.mib_dir) and loaded at once. The module list shows the load state:
loaded: everything resolved
loaded with problems: some definitions do not resolve or an imported module is missing; the missing modules are named, upload them too (vendor MIBs usually need common modules such as CISCO-SMI)
not usable: nothing resolved
The quirks of vendor MIBs (comment banners, unknown macros, missing imports, broken definitions) become warnings; the rest stays usable. Uploaded modules can be deleted (with every module of the same file). Bundled modules cannot be deleted, but uploading a newer version with the same name replaces them.
The MIB tree shows descriptions, syntax, access, enumerations, INDEX and more. “Module to show” narrows it to one uploaded (or bundled) MIB module: only the objects that module defines, starting at its top nodes instead of 1.3.6.1. The magnifier button in the module list does the same. “Use in a request” copies the name to the Operations tab (scalars with .0); the Get (scalars only) / GetNext / Walk buttons switch to the Operations tab, run the request at once and scroll to the result.
With “Record pcap” on, every SNMP datagram wpg sends or receives (requests, responses, traps, informs) is recorded; download it as .pcap or .pcapng and open it in Wireshark. The datagrams travel over ordinary UDP sockets, so the Ethernet / IP / UDP headers are synthesised with the real addresses and ports and placeholder MACs (02:00:00:00:00:01 for wpg, 02:00:00:00:00:02 for the peer). At most 100,000 frames are kept in memory.
[snmp]
enabled = true # SNMP tool screen and API
trap_listen = "" # e.g. "0.0.0.0:162"; empty = trap receiver not started at boot (can be started from the UI)
mib_dir = "" # default <data_dir>/mibs
With trap_listen set, the receiver starts at boot (accepting any community and no v3 users; restart it from the screen to change that).
Otherwise listen on e.g. 0.0.0.0:1162 and point the devices’ trap destination port there. A start without the permission fails with an explanatory error.
The receiver does not enforce the SNMPv3 time window (notInTimeWindow) so that late messages are still shown; noAuthNoPriv v3 messages are accepted from any user name.
v3 pass phrases need at least 8 characters (RFC 3414’s recommendation, enforced by most devices).
The MIB parser looks up names and types; it is not a strict SMI validator.
wpg includes a DHCP server for test networks (the “DHCP” screen of the web UI; API /api/dhcp). Connect the wpg host directly to the device under test (router, CPE, host) and hand it addresses and prefixes. The design is in ADR-0012 (Japanese).
--dhcp-iface ens3 writes WPG_DHCP__IFACE=ens3 into /etc/wpg/wpg.env; the screen can no longer change the interface. The service gets a CAP_NET_BIND_SERVICE drop-in (ports 67 / 547).
--dhcp-iface-addr gives that interface static addresses: it writes /etc/netplan/90-wpg-ens3.yaml and runs netplan apply (DHCP client and RA acceptance are turned off on it). An interface carrying the default route is never touched. To undo, delete the file and run sudo netplan apply.
From Windows: deploy.ps1 -DhcpIface ens3 -DhcpIfaceAddr 192.168.100.1/24. In answer files: dhcp_iface= / dhcp_iface_addr=.
It can also be set in wpg.toml:
[dhcp]
enabled = true
iface = "ens3"
With an empty iface, the screen offers the interfaces in tx.allowed_ifaces.
The interface, the DHCPv4 server identifier, the DHCPv6 server DUID, the addresses wpg added, and packet counters. “Save and start” in the header saves the settings and starts the server (restarts it when running); “Stop” stops it. A server running when wpg stops starts again with wpg.
Interface addresses (static IP): addresses such as 192.168.100.1/24, 2001:db8:1::1/64 are added to the interface when the server starts (the link is brought up too) and the ones wpg added are removed when it stops. Use --dhcp-iface-addr at deploy time for addresses that survive a reboot. The first IPv4 address becomes the DHCPv4 server identifier (the “server identifier” field overrides it).
DHCPv4: range start / end, subnet mask, lease time (T1 / T2 are 1/2 and 7/8 of it), routers, DNS servers, domain name, NTP servers. Lists are separated by commas or spaces. Reservations are one MAC IP per line (the address may be outside the range).
DHCPv6: “Addresses (IA_NA)” hands out one address of the range per IA; “Prefix delegation (IA_PD)” cuts the pool prefix (e.g. 2001:db8:100:: / 40) into prefixes of the delegated length (e.g. 56). Also the preferred and valid lifetimes (T1 / T2 are 1/2 and 4/5 of the preferred lifetime), DNS servers, domain search list, the ADVERTISE preference and Rapid Commit.
Router advertisements: hosts only use DHCPv6 when an RA sets the M flag (addresses) or the O flag (other configuration). Turn on “Send router advertisements” when no router on the link sends them. M is set when IA_NA is on, O always. An on-link prefix adds a Prefix Information option, with the A flag when “SLAAC” is checked. A router lifetime of 0 does not make wpg a default router.
Bound, offered and declined (DECLINE: reported in use) leases. Deleting one only removes wpg’s record (the client keeps the address until it renews). Expired leases disappear on their own.
Requests and replies (DISCOVER → OFFER, SOLICIT → ADVERTISE, RAs sent, …) and why nothing was sent (pool exhausted, another server selected, unknown server identifier), newest first.
The systemd service installed by deploy.sh has all of them (the bind-service.conf drop-in is added with --dhcp-iface or when an SNMP trap listener is configured). When running wpg by hand: sudo setcap cap_net_raw,cap_net_admin,cap_net_bind_service+eip /usr/local/bin/wpg.
The repository is a Cargo workspace under crates/. User-facing strings live in locales/, sample templates in templates/, documentation in docs/ and the end-to-end tests in e2e/, all beside crates/.
Receive-side counters and simple verification (v1: minimal)
wpg-sys
OS facts: network interfaces, privileges, kernel, hugepages, Npcap / libpcap detection, the doctor checks
wpg-log
Logging setup: a tracing subscriber (pretty / JSON), rolling log files and a broadcast sink for live streaming
wpg-server
axum HTTP / WebSocket server with the web UI embedded
wpg-cli
The command line itself (serve, send, render, import, template, doctor, …); builds the wpg binary
wpg-web
Leptos (CSR) web UI, compiled to WASM with Trunk
✓
dpdk-sys
Raw FFI to DPDK (24.11 / 23.11 LTS), generated with bindgen; the inline fast path (rte_eth_tx_burst, rte_pktmbuf_alloc, …) and rte_errno are wrapped by a C shim (wpg_ prefix). Empty unless the link feature is on (enabled by wpg-tx’s dpdk). ADR-0004
wpg-testutil
Test helpers: pcap / pcapng reader and writer, hex helpers, golden-file assertions (not published)
A further wpg-golden-tests crate holds the golden-pcap byte checks and the tshark cross-check (cargo test -p wpg-golden-tests).
wpg-core, wpg-protocols, wpg-i18n and wpg-web must also build for the browser (wasm32-unknown-unknown) for the web UI. Do not use file I/O, threads or OS-specific crates in these four. CI and just check-wasm enforce it with:
Transmission and OS facts (wpg-tx, wpg-sys) are native-only. When you add build/parse logic, first check whether it belongs in the OS-independent wpg-core.
Core protocols are defined with a Rust macro (crates/wpg-protocols/src/) and compile to the IR. From the IR the project generates building, parsing, the GUI editor and the reference docs. A new protocol needs a definition, Fluent strings, a sample template, a golden pcap and, where possible, a tshark cross-check. This keeps the cost of each added protocol constant.
Define it. Add the protocol with the macro to the right module in crates/wpg-protocols/src/ (for example app.rs for an application-layer protocol) and register it in the catalog. Tag each field with a Fluent key via desc = "key". Before reaching for an approximation, check the building blocks below.
Add Fluent strings. Add the protocol name and every field’s desc key to every language’s locales/*/main.ftl (see Adding i18n strings).
Add a sample template. Put one or more .wpg.yaml files under templates/protocols/<group>/, with ja / en descriptions and protocol: / layer: tags. The samples.rs tests check that every protocol has a sample and that well-formed samples parse back to the same stack.
Regenerate the golden files. Rebuild tests/golden/*.hex / *.pcap with just golden-update and review the diff. Never edit the .hex / .pcap by hand.
Cross-check. If tshark is available, compare against Wireshark with WPG_TSHARK=1 cargo test -p wpg-golden-tests -- --ignored.
Update the notes. When approximations remain or the encapsulation is unusual, add matching ja / en notes under docs/{ja,en}/src/protocols/.
Regenerate the reference. Rebuild the protocol reference and the other generated pages with just docs-gen and commit them (see “Reference docs” below).
The protocol reference is generated from the definitions: run just docs-gen (cargo run -p wpg-cli -- docs gen) and commit the result. New protocols, fields, bindings and sample templates are picked up automatically; the field descriptions come from the desc = "..." Fluent keys (ja / en). Hand-written notes go in docs/{ja,en}/src/protocols/: mention the protocol ids in backticks (`isis_l1_lsp`) and the generator links them both ways. The test generated_docs_are_up_to_date (wpg-cli) fails while the committed pages are stale.
User-facing text — in the server, the CLI and the web UI, error messages included — is never hard-coded; it is a Fluent key. Strings are handled by the wpg-i18n crate, which embeds the Fluent files locales/<lang>/main.ftl with include_str! (no file I/O, so it builds for the browser). Eight languages are supported: Japanese (ja), English (en), Simplified Chinese (zh-CN), Traditional Chinese (zh-TW), Korean (ko), German (de), French (fr) and Spanish (es).
The manual (mdBook), the references generated by wpg docs gen and the descriptions of the bundled templates (meta.descriptions) exist in Japanese and English only. When a template has no description in the UI language, the English one is shown.
Add one line with the same key to everylocales/*/main.ftl (all eight languages). English is the base and the fallback. Keep placeholders ({ $name }) and backquoted code and ids untranslated.
Reference it from code: t when there are no arguments, t_args when there are.
#![allow(unused)]
fn main() {
use wpg_i18n::{t, t_args, FluentArgs, Locale};
let s = t(locale, "error-not-found");
let mut args = FluentArgs::new();
args.set("packets", 1022);
args.set("elapsed", "0.5s");
let s = t_args(locale, "send-done", &args);
}
A missing key falls back to English, then to the key name itself. The { $name } placeholder names are a contract with the caller — do not rename them in a translation.
The .ftl files of all languages must hold the same set of keys. A wpg-i18n unit test checks this and fails if a language lacks a key or has a key English does not have.
cargo test -p wpg-i18n
This is the “i18n parity lint” of the specification (docs/spec/11-risks.md); there is no separate wpg subcommand for it. The same test run also checks that every field-description key of the protocol catalog exists, and that no key is dead (no_unused_keys: a key that no source file mentions must be removed; keys built at run time from a prefix, docgen-* and rtcp-*, are exempt). In CI it runs as part of cargo test --workspace; the GUI end-to-end tests (just e2e, e2e/README.md) additionally switch the language in the browser.
The desc = "key" attached to a field in a protocol definition (wpg-protocols) is a Fluent key. When you add a protocol, add its desc keys to the .ftl files of every language (see “Adding a protocol” in CONTRIBUTING.md and Adding a protocol).
Diagnostic (diag-*) and lint (lint-*) messages work the same way. Their argument names ({ $field } and so on) are a contract with wpg-core’s diag::keys.
Create locales/<tag>/main.ftl and translate every key (<tag> is a BCP 47 language tag, e.g. pt-BR).
Add a variant to wpg-i18n’s Locale and add it to Locale::ALL, as_str(), source() and from_langid (detection from Accept-Language or LANG); see the doc comment in crates/wpg-i18n/src/lib.rs.
Add the language-name key ui-lang-<tag> (the language’s name in itself, e.g. ui-lang-pt-br = Português (Brasil)) to every .ftl, and map it in locale_label_key in crates/wpg-web/src/i18n.rs. The language menus of the header and of the settings page list it automatically.
Run cargo test -p wpg-i18n -p wpg-web to confirm no key is missing.
The language is picked from the saved preference, else the browser’s navigator.languages in the web UI; from Accept-Language in the API; and from LC_ALL / LC_MESSAGES / LANG in the CLI. Chinese is split by script and region: zh-Hant, zh-TW, zh-HK and zh-MO give Traditional Chinese, any other zh Simplified Chinese. Unsupported languages get English.
The workspace has a single version ([workspace.package] version in Cargo.toml; each crate uses version.workspace = true). It follows Semantic Versioning; breaking changes are possible until v1.0.
Before tagging, make sure all of the following are green. On top of the usual pre-push checks (CLAUDE.md) there are protocol, security and documentation gates.
# Pre-push checks
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace --no-fail-fast
cargo check -p wpg-core -p wpg-protocols -p wpg-i18n --target wasm32-unknown-unknown
# Protocols (golden pcaps and the tshark cross-check)
cargo test -p wpg-golden-tests
WPG_TSHARK=1 cargo test -p wpg-golden-tests -- --ignored
# Security (just audit / just deny)
cargo audit
cargo deny check
The v1.0 release gate (ROADMAP.md, M12) additionally requires:
All P0 + P1 protocol golden pcaps and tshark cross-checks green.
Build, start and pcap-backend transmission confirmed on all three OSes.
The web UI end-to-end tests (just e2e) green, and zero i18n gaps in every language (cargo test -p wpg-i18n).
The screenshots and the builder GIF in docs/{ja,en}/src/images/ regenerated with just screenshots when the UI changed (review them before committing).
Documentation complete in ja and en, with docs/build.sh passing.
Move the Unreleased entries of CHANGELOG.md into a section for the new version (## [X.Y.Z] - YYYY-MM-DD), adding anything since the last tag from the Conventional Commits. Call out upgrade notes (template schema, database or configuration changes, see Upgrading).
Bump the workspace version in Cargo.toml (also the version of the internal wpg-* entries in [workspace.dependencies]) and update Cargo.lock (cargo check).
Commit as release: vX.Y.Z and create an annotated tag vX.Y.Z (git tag -a vX.Y.Z -m "wpg X.Y.Z").
Push the tag (git push origin vX.Y.Z). .github/workflows/release.yml builds the archives, writes SHA256SUMS and creates the GitHub Release with generated notes; edit the notes afterwards. Tags with a hyphen (v1.0.0-rc.1) become pre-releases. scripts/release.sh fails when the tag does not match the workspace version.
A manual run of the workflow (workflow_dispatch), or a pull request that touches the packaging (scripts/release.sh, scripts/smoke-archive.sh, scripts/build.ps1, packaging/, the Docker files, the workflow itself, or the Npcap loading code), builds and tests the archives and keeps them as workflow artifacts without publishing anything; use it to test the pipeline.
Releases are built by .github/workflows/release.yml. Each target is built on its own runner by scripts/release.sh and checked by scripts/smoke-archive.sh; further jobs test the archives on their OS (below), and a final job publishes a GitHub Release with all archives and a SHA256SUMS file once every other job has passed. The release builds use a pinned trunk version (set in the workflow), so re-running a tag builds the same web UI.
Built against glibc 2.35, so it runs on Ubuntu 22.04 / Debian 12 and newer. Sends with afpacket, so no libpcap is needed
wpg-<ver>-aarch64-apple-darwin.tar.gz
macos-14
pcap
Apple silicon. libpcap ships with macOS
wpg-<ver>-x86_64-apple-darwin.tar.gz
macos-14 (cross)
pcap
Intel Macs
wpg-<ver>-x86_64-pc-windows-msvc.zip
windows-latest
pcap
Starts without Npcap; sending needs Npcap
Each archive holds wpg / wpg.exe (with the web UI embedded), README.md, README.ja.md, CHANGELOG.md, both licences, templates/ and packaging/ (systemd, launchd and Windows service samples). macOS and Windows can only send through libpcap / Npcap, so those binaries are built with the pcap feature. For Windows, release.sh downloads the Npcap SDK (version and SHA-256 pinned in the script, cached under target/) to link wpcap.lib; the SDK is not shipped. wpg.exe delay-loads wpcap.dll (crates/wpg-cli/build.rs), so it starts, and wpg doctor runs, on machines without Npcap. The binaries are not signed or notarised; see the macOS and Windows install pages for what users have to do.
scripts/smoke-archive.sh: the .sha256 file, the archive contents, wpg --version, wpg doctor --json, wpg render, and wpg serve answering /api/health with the embedded UI (contents only for the cross-built Intel macOS archive)
macOS (launchd, pcap)
Installs the Apple silicon archive and the launchd daemon as in Running as a service, checks /api/health, removes it; then sends 16 frames with --backend pcap on a feth pair and receives them with wpg capture --backend pcap
Windows (scheduled task, no Npcap)
wpg doctor fails the npcap check, wpg send --backend pcap stops with the “Npcap is not installed” message instead of crashing, and packaging\windows\wpg-service.ps1 installs, starts (checked through /api/health), stops and removes the task, with C:\ProgramData\wpg and wpg.toml not readable by BUILTIN\Users
Windows (build.ps1)
scripts\build.ps1 with the Npcap SDK in C:\Npcap-SDK (so it adds pcap), then runs the result
Docker (compose)
docker compose up --build --wait, /api/health and wpg doctor in the container
Npcap cannot be installed unattended on the runners (silent installation is limited to the OEM edition), so sending through Npcap is not covered by CI; check it by hand on a Windows machine before a release.
scripts/release.sh # host target, into dist/
scripts/release.sh --target x86_64-apple-darwin # cross target (needs its linker / SDK)
scripts/release.sh --skip-frontend --features pcap # reuse crates/wpg-web/dist
scripts/smoke-archive.sh dist/wpg-*.tar.gz # or: just smoke-archive <archive>
scripts/release.sh --checksums dist # SHA256SUMS of everything in dist/
It needs trunk (cargo install trunk --locked) unless --skip-frontend is given, and refuses to package without crates/wpg-web/dist/index.html so no release ships without the web UI. On Windows run it from Git Bash (it uses 7z or zip).
The roadmap planned cargo-dist. The pipeline uses a small script instead because wpg needs the Trunk front-end build before cargo build (the UI is embedded by rust-embed), and the archives carry templates and service files beside the binary. cargo-dist can do both through custom build-setup steps and include, but its generated workflow has to be regenerated whenever cargo-dist is upgraded. release.sh is the same script locally and in CI. Installers (shell / PowerShell / Homebrew / MSI) are not produced; revisit cargo-dist if they are wanted.
Not done yet. macOS users have to remove the quarantine attribute (Install on macOS); Windows SmartScreen may warn about the unknown publisher (Install on Windows). Signing and notarisation need an Apple Developer ID and a code-signing certificate, which the project does not have.
When something goes wrong, run wpg doctor first. It diagnoses privileges, interfaces, the kernel, hugepages and Npcap / libpcap one line at a time, and prints OS-specific fix commands. wpg doctor --json returns the same for scripts, and the exit status is non-zero when a check fails.
Raw-socket privilege: CAP_NET_RAW / root on Linux, BPF access on macOS, an elevated terminal on Windows only when Npcap is in administrator-only mode. See Permissions
iface.any_up
warn
A non-loopback interface is up with carrier. Check the cable / link
kernel.af_xdp
warn
Kernel 5.10+ for the AF_XDP backend
kernel.tpacket_v3
warn
Kernel 3.2+; without it afpacket falls back to sendmmsg
hugepages
warn
Hugepages configured; needed only for the DPDK backend
npcap
fail / warn (Windows only)
Npcap installed and wpcap.dll present (System32\Npcap or System32); skipped on other OSes
backend.pcap
fail (Windows, macOS)
This wpg was built with the pcap backend (pcap feature), the only way to send on Windows and macOS. On Windows a build without the Npcap SDK fails here: unpack the SDK to C:\Npcap-SDK and run scripts\build.ps1 again (see Install on Windows); on macOS run scripts/build.sh again. Skipped on Linux
pcap_lib
warn
libpcap found (needed for the pcap backend); skipped on Windows and when the backend is not built in
webui.embedded
warn
The web UI was built before wpg; otherwise wpg serve shows a placeholder page. Run scripts/build.sh (scripts\build.ps1 on Windows) again
Each result is ok, warn, fail or skip (not applicable on this OS).
Raw-socket transmission needs privilege. Run wpg doctor and apply the command it prints on the caps.raw_socket line (on Linux, something like sudo setcap cap_net_raw,cap_net_admin+ep $(command -v wpg)). A file capability is lost when the binary is replaced. See Permissions.
When average is below the target and dropped is large, the backend cannot keep up. Try raising --burst, using --qdisc-bypass with afpacket, or adding --workers (one worker per core). See Performance tuning.
--rate 50% needs a real interface whose link speed is known. On lo it reports Cannot determine the link speed. Give an absolute rate such as 1Gbps.
unlimited on the null backend measures how fast frames are built, not a network figure.
Dry-run with --backend null (build and count only) or --backend pcapfile -i out.pcapng (write to a file). wpg render FILE -o out.pcapng also writes a pcapng without transmitting.
wpg template lint FILE reports unknown fields, out-of-range values, malformed generators and length / checksum mismatches. Deliberately broken templates (templates/malformed/) stay at the warning level (diag-*) and can still be sent.
The tshark cross-check of the golden pcaps is WPG_TSHARK=1 cargo test -p wpg-golden-tests -- --ignored. For your own capture, tshark -r out.pcapng -V shows, for instance, that checksums read [correct].
The language is set by the settings screen in the GUI, by the LANG / LC_MESSAGES / LC_ALL environment variables (POSIX precedence) for the CLI, and by the Accept-Language header for the API. Untranslated strings fall back to English. To add keys, see Adding i18n strings.
If your problem is not here, report it in an issue with the output of wpg doctor --json. Entries are added as issues come in.
wpg is a tool for verifying and testing networks. Use it only against networks you own, or that you have explicit authorization to test.
The tool sends arbitrary frames statelessly and at high speed, and can generate traffic that disrupts a live network — ARP / ND sweeps, SYN / MAC floods, malformed frames. Directing such traffic at networks or devices you do not control, without authorization, is illegal in many jurisdictions. You are solely responsible for how you use it.
Always dry-run a new template with a non-transmitting backend (--backend null, or pcapfile with -i out.pcapng) before sending it.
Test in a lab isolated from production networks (a virtual environment such as CML2 is useful; see CML2 notes).
The administrator settings [tx] max_rate and allowed_ifaces can cap the usable rate and interfaces (Configuration).
Changes intended to turn wpg into an attack tool are not accepted (see “Responsible use” in CONTRIBUTING.md).
This software is dual-licensed under MIT or Apache-2.0, at your option. Contributions are, unless stated otherwise, taken to be offered under the same dual license (Developer Certificate of Origin; sign off with git commit -s).
Dependency crates keep their own licenses. The set and the license policy are checked with cargo deny check (deny.toml).
The web terminal page bundles xterm.js and its fit / web-links / search add-ons (all MIT licensed). The copyright notices and license text are in crates/wpg-web/vendor/xterm/LICENSE and are served by wpg as /xterm/LICENSE.
Npcap, used for transmission on Windows, is not bundled with this software; you obtain it from npcap.com and install it under its own terms. Redistribution may require a separate license.