go-freebsd-netgraph

Go bindings for FreeBSD's netgraph(4) — control sockets, token-matched control messages, the generic node operations, hook data and ASCII conversion. Every receive has a deadline.

Gonetgraph · ngctl · libnetgraph · networkingfreebsd from gosource on github

Why

Build and inspect netgraph graphs from Go — mkpeer, connect, name, rmhook, shutdown, list nodes, hooks and types, read and write frames on a hook — without running ngctl(8) and parsing what it prints.

It binds netgraph as a subsystem, not as a catalogue of node types. A node type’s own control messages are that type’s business: the package carries their payloads as opaque bytes and lets you marshal them, the same level of abstraction libnetgraph itself offers.

What it guarantees

Each of these is a failure that happened for real while building ng_policy on raw libnetgraph. They’re in the library so nobody has to rediscover them.

  • Replies are matched to requests by token. A control socket hands you whatever message is next in its queue, not the answer to the call you just made, and a late reply to an abandoned request looks identical on the wire. Call sends a fresh token and reads until a reply matches, discarding anything stale. When this bit for real it looked like “the node stopped responding” — which pointed at the kernel, which was fine.
  • No receive blocks past its deadline. Every receiving call takes a time.Duration and returns ErrTimeout at it. SO_RCVTIMEO is re-armed before every read and the remaining time recomputed, so discarding a stale message can’t extend the deadline. The package declines to use NgSendMsg, NgSendAsciiMsg, NgRecvAsciiMsg and NgAllocRecvMsg, which either reach a poll(..., INFTIM) or receive without checking the token.
  • Response buffers are sized to the socket. A control socket’s receive space is 20480 bytes by default (net.graph.recvspace), and a legitimate response can fill it. A smaller buffer doesn’t overflow — the kernel truncates the datagram — but the arglen in the surviving header still describes the whole response, so a caller that trusts it reads out of bounds. Buffers are sized from the sysctl at Open, and a mismatch returns ErrTruncated rather than a short slice.
  • Quietly: names, hooks, types and paths are validated in Go, so you get a specific message instead of a bare EINVAL; errors carry the operation and path and map onto ErrNotFound, ErrExists, ErrStale and friends; and the maximum control path is 32 bytes, not the NG_PATHSIZ 512 the header invites you to assume.

What it can’t guarantee

The deadline covers receives, not sends — and that’s the kernel’s limit, not the package’s. ngc_send queues the item and then sleeps with no timeout and no PCATCH, so the sleep is neither bounded nor interruptible by a signal, and SO_SNDTIMEO can’t reach it. Delivery is synchronous when the target node is idle, which is why nothing here has been seen to block; but control messages are writers unless they carry NGM_READONLY, a writer needs a completely idle node, and queueing is easy to hit under contention. No userland netgraph client on FreeBSD can promise otherwise.

The ASCII brace trap

mkpeer { type="hole" ourhook="a" peerhook="b" } works. mkpeer hole a b — exactly what you type at ngctl — comes back as a bare invalid argument.

The kernel’s NGM_ASCII2BINARY runs ng_parse, whose struct parser demands an opening brace. ngctl accepts the positional form only because it never sends it: it fills a struct ngm_mkpeer in userland and sends the binary message. The positional form is ngctl’s command-line syntax, not netgraph’s ASCII syntax, and nothing about either says so. The package documents it and examples/mkpeer demonstrates it live.

Safety

netgraph is live networking: calls take effect immediately on the running kernel, with no dry run and nothing to roll back. Two traps in particular:

  • A leaked node outlives the process. Nodes are kernel objects; a program that exits without tearing down what it built leaves it running, and the next run fails with EEXIST and looks like a flaky test rather than yesterday’s mess. The examples clean up on every path out, and the README shows a captured run of what happens when they don’t.
  • ng_hole and ng_echo self-destruct when their last hook goes, so closing your socket looks like it tidied up after you — until a node has a second hook. Shut nodes down explicitly.

Testing

Unit tests need no kernel and no root. Integration tests build and destroy real graphs as root; afterwards ngctl list should show only what was there before. The three examples — list, mkpeer, data — ship with output captured from real runs. Off FreeBSD the package still builds, with every call returning ErrNotSupported.

The Makefile sets GOTOOLCHAIN=local on every go invocation. go.mod requires Go 1.26, and an older go doesn’t refuse it — since 1.21 it silently downloads a matching toolchain and builds with that. Better to fail and say so.

Status

v0.1.0. Tested on FreeBSD 14.3-RELEASE-p3 amd64 only; 13.2 and later should work but haven’t been run.

s, err := netgraph.Open("") // anonymous socket node; Close tears it down
if err != nil {
	return err
}
defer s.Close()

// Binary: arguments marshalled in Go.
err = s.MkPeer(".:", "hole", "a", "left")

// ASCII: parsed by the kernel's ng_parse. The braces are mandatory.
_, err = s.CallAscii(".:", `mkpeer { type="hole" ourhook="b" peerhook="left" }`, 2*time.Second)
$ git clone https://github.com/zombocoder/go-freebsd-netgraph
← all projects