No description
  • Zig 95.5%
  • Nix 3.8%
  • Python 0.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jeffrey C. Ollie bb12599ba5
All checks were successful
test / test (push) Successful in 10m45s
test / vm (push) Successful in 1m36s
test / docs (push) Successful in 6m18s
Move to Zig 0.17.0 from nixpkgs-unstable
The released 0.17.0 is zig_0_17 in nixpkgs-unstable, so the devshell takes
it as it is and the patched test runner goes: 0.17 fixed the type error it
existed for. The dependencies move to their 0.17 releases -- zig-asn1, z46,
zig-macaddress, zig-std-crypto-ext and zig-smi, all 0.2.0 -- and
build.zig.zon.nix is regenerated with `zon2nix --17`.

The port: b.args is gone for addPassthruArgs, findProgram takes an options
struct and no longer fails, the docs server takes the emitted directory
rather than an install path read while configuring, and array
multiplication, which the language dropped, becomes @splat -- the one
string repeated in tests/fuzz.zig is a splat of two-byte arrays bit-cast
end to end. `zig fmt` rewrites @intFromEnum to @backingInt.

A test is handed the environment now, as std.testing.environ, so the
real-device tests read it from there instead of parsing
/proc/self/environ, and stop being Linux-only.

The test binaries are compiled with LLVM. The self-hosted backend emits no
coverage instrumentation, and with LLVM `zig build fuzz --fuzz` runs with
coverage feedback instead of stopping at an empty table of program
counters. The loop in tools/fuzz.zig stays, as the run that is the same on
every machine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AJfjkPKSZVrYrkseQ1arKH
2026-10-10 10:32:22 -05:00
.forgejo/workflows A trap receiver, fuzz targets for v3, and the virtual machine test 2026-09-12 12:08:21 -05:00
LICENSES The SNMPv1 and v2c wire format 2026-09-12 10:28:29 -05:00
src Move to Zig 0.17.0 from nixpkgs-unstable 2026-10-10 10:32:22 -05:00
tests Move to Zig 0.17.0 from nixpkgs-unstable 2026-10-10 10:32:22 -05:00
tools Move to Zig 0.17.0 from nixpkgs-unstable 2026-10-10 10:32:22 -05:00
.gitignore The SNMPv1 and v2c wire format 2026-09-12 10:28:29 -05:00
build.zig Move to Zig 0.17.0 from nixpkgs-unstable 2026-10-10 10:32:22 -05:00
build.zig.zon Move to Zig 0.17.0 from nixpkgs-unstable 2026-10-10 10:32:22 -05:00
build.zig.zon.nix Move to Zig 0.17.0 from nixpkgs-unstable 2026-10-10 10:32:22 -05:00
flake.lock Move to Zig 0.17.0 from nixpkgs-unstable 2026-10-10 10:32:22 -05:00
flake.nix Move to Zig 0.17.0 from nixpkgs-unstable 2026-10-10 10:32:22 -05:00
package.nix Move to Zig 0.17.0 from nixpkgs-unstable 2026-10-10 10:32:22 -05:00
README.md Move to Zig 0.17.0 from nixpkgs-unstable 2026-10-10 10:32:22 -05:00
REUSE.toml 3DES privacy, and the Cisco commands the device tests need 2026-09-12 13:27:23 -05:00

zig-snmp

An SNMP manager for Zig: versions 1, 2c and 3 on the wire, with command line tools named after net-snmp's.

The API documentation is generated from the doc comments, which carry most of the explanation, and is published at https://jeff.jcollie.page/zig-snmp/.

$ git clone https://git.jcollie.dev/jeff/zig-snmp.git
$ cd zig-snmp
$ nix develop -c zig build test

It is written for Zig 0.17. The last release for Zig 0.16 is v0.1.0, and the zig-0.16 branch holds it.

Where this lives

Two homes, with the same history in both.

What is here

Module What it is
snmp.protocol The wire format: every value type, variable binding, PDU and message, encoded and decoded, both directions. No I/O anywhere in it, which is what lets the whole of it be fuzzed with no socket.
snmp.Client A socket and the operations over one: get, getNext, getBulk, set and walk. The only file that does any I/O, and it takes its Io as a parameter rather than naming an implementation.
snmp.Receiver A notification sink: traps and informs, with the acknowledgement an inform needs.
snmp.usm The User-based Security Model: key derivation, the six authentication protocols, and DES, 3DES and AES-CFB privacy at all three key sizes. No I/O, so the whole of it is testable against the RFC's published vectors.

It is built on two libraries of its own, both extracted rather than vendored:

  • zig-asn1 — the Basic Encoding Rules and the OBJECT IDENTIFIER type.
  • zig-std-crypto-ext — the ciphers and modes std.crypto leaves out, three of which SNMPv3 privacy needs: DES, AES-192, and CFB.

and on z46 for IP addresses and zig-macaddress for the PhysAddress textual convention.

State

SNMPv1, v2c and v3 all work against real agents. What is here:

  • The whole wire format. Every value type including the application-tagged ones (Counter32, Gauge32, TimeTicks, IpAddress, Opaque, Counter64) and the SNMPv2 exception markers; variable bindings and lists; all eight PDUs in their three shapes; the community-based message wrapper.

  • A UDP client with retries, a per-attempt deadline, and request-id matching that discards a late reply rather than answering with the previous question's answer.

  • A walk, over GetNext or GetBulk, that ends on endOfMibView, on leaving the subtree, or on a v1 agent's noSuchName — and refuses to loop when an agent fails to advance.

  • Four command line tools: snmpget, snmpwalk, snmpbulkwalk and snmpset, which produce byte-identical output to net-snmp's given the same arguments, the same MIBs and the same agent. Verified against snmpd and against a Cisco Catalyst.

    The MIBs need saying because net-snmp has a compiled-in MIB directory and loads it whether or not -M is given, where these tools load nothing unless told --- so the two agree on identical arguments only once they have been told the same thing. MIBDIRS pointed at the same directory, or MIBS= emptied on net-snmp's side, is what makes the comparison a comparison. Both cases are in the virtual machine test.

  • SNMPv3 with USM: engine discovery, the time window, all six authentication protocols (MD5, SHA-1 and RFC 7860's four SHA-2 ones) and five privacy protocols (DES-CBC, 3DES-EDE-CBC, and AES-CFB at 128, 192 and 256 bits). Verified against net-snmp and against Cisco IOS — two implementations that wrote their key derivation separately from each other and from this.

  • A trap and inform receiver, for all three versions. Traps are fire and forget; informs are acknowledged, which is what stops the sender retrying. Under v3 the receiver authenticates and decrypts notifications, and answers engine discovery — a v3 inform cannot be sent until it does, since the receiver is the authoritative engine for one. Driven by net-snmp's own snmptrap in the tests: the one place our decoder is checked against net-snmp's encoder rather than the other way round.

  • Five command line tools, the fifth being snmptrapd.

  • MIB support, through zig-smi: -M and -m spelled as net-snmp spells them, and both falling back to the MIBDIRS and MIBS environment variables, so the two programs can be run with one exported variable and identical arguments. An OID may be written any way a person would write one, and is printed in any of the four styles net-snmp's -O flags select:

    $ export MIBDIRS=/path/to/mibs
    $ snmpget -v2c -c public localhost sysDescr.0            # or SNMPv2-MIB::sysDescr.0,
    SNMPv2-MIB::sysDescr.0 = STRING: Linux ...               # or .1.3.6.1.2.1.1.1.0
    $ snmpwalk -Of -v2c -c public localhost system | head -1
    .iso.org.dod.internet.mgmt.mib-2.system.sysDescr.0 = STRING: Linux ...
    

    -m restricts to the modules named and whatever they import, which is what net-snmp's does: -m IF-MIB against its own MIB directory loads five modules rather than sixty-five, so an OID that only HOST-RESOURCES-MIB names comes back numeric from both programs.

    What it costs, measured against net-snmp on the same files, ReleaseSafe against its release build:

    time peak memory
    net-snmp's 65 base modules 0.06s 10 MB
    Cisco's 1,627 plus those 65, 80 MB of source 0.67s 392 MB
    net-snmp, the same 1,692 1.84s 75 MB

    So: faster than net-snmp and five times hungrier. The memory is the arena holding every module's source and parsed form, which is the shape this takes and not a leak; pointing -M at a whole vendor corpus is the extreme case, and -m naming the two modules actually wanted is both quicker and smaller.

    Values are rendered through what the MIBs say about them too, which is most of what makes a walk readable: an enumeration's label, a UNITS clause, and the DISPLAY-HINT of whatever textual convention the syntax names.

    $ snmpwalk -v2c -c public localhost ifPhysAddress | head -1
    IF-MIB::ifPhysAddress.2 = STRING: 18:c0:4d:95:93:86      # PhysAddress, "1x:"
    $ snmpget -v2c -c public localhost ifOperStatus.2 hrStorageAllocationUnits.35
    IF-MIB::ifOperStatus.2 = INTEGER: up(1)
    HOST-RESOURCES-MIB::hrStorageAllocationUnits.35 = INTEGER: 4096 Bytes
    

    A walk of the whole of mib-2 against net-snmp's own snmpd --- 8,461 varbinds --- is byte-identical to net-snmp's, apart from the eight values that genuinely moved between the two runs: the uptime, the system clock, and the agent's own packet counters, which the two walks themselves increment.

    With no -M and no MIBDIRS, nothing is loaded and everything prints numerically --- which is exactly what these tools did before MIB support existed, so no script that predates it changes behaviour.

Which oracle covers what is not uniform, and worth knowing before relying on any of it:

net-snmp Cisco IOS
v1, v2c, and the v3 message layer yes yes
MD5 and SHA-1 authentication yes yes
SHA-224/256/384/512 (RFC 7860) yes no — the device here has no SHA-2
DES and AES-128 privacy yes yes
3DES, AES-192, AES-256 privacy no — its CLI refuses them yes

3DES, AES-192 and AES-256 come from drafts that expired without becoming RFCs — draft-reeder-snmpv3-usm-3desede and draft-blumenthal-aes-usm — and net-snmp 5.9's command line refuses all three, so Cisco firmware is the only thing they have been checked against. The SHA-2 authentication protocols are in the same position for the opposite reason: standardised, implemented by net-snmp, and not by the device here.

All three need more localized key than their hash produces — 3DES wants 32 octets and MD5 gives 16 — so all three go through the same chained key extension, which is defined in the first of those drafts and which net-snmp's library implements as netsnmp_extend_kul.

Why net-snmp is everywhere in the tests

Correctness here is settled by agreement with the reference implementation rather than by reading the RFCs alone, because the RFCs leave room and the deployed base does not. Four tiers, cheapest first:

  1. Unit tests beside what they cover.
  2. Captured fixtures — net-snmp's own bytes, checked in, asserted by decode-then-re-encode identity. That sidesteps the random request-id entirely, since the id comes from the captured bytes rather than from us.
  3. A real agent, in an ordinary test. snmpd runs unprivileged on a loopback high port given SNMP_PERSISTENT_DIR, so tests/oracle.zig spawns one rather than booting a virtual machine. It answers v1, v2c and v3 with every auth and privacy protocol from there.
  4. A NixOS guest, for only what genuinely needs privilege — the NixOS module's own snmpd, and a receiver on port 162 with net-snmp's snmptrap aimed at it with no :port, so the default is under test on both sides. Run by nix flake check, by hand and in the workflow alike.

There is a fifth, opt-in and never in CI: a real Cisco Catalyst. tests/device-cisco.md has the IOS commands to configure a device for it and to remove the configuration again. It is the only oracle in the set that is another vendor's implementation rather than net-snmp wearing two hats, and it has already earned its place by showing three things snmpd never will — a sysDescr containing newlines, so one varbind is not one line of output; sparse interface indices; and ifHCInOctets returning a Counter64 under v2c and noSuchName under v1 from the same agent at the same OID. See tests/device.zig for how it is gated.

Licence

MIT. See LICENSES/MIT.txt.