Baidu built a network monitoring toolkit in Go — here is how it works
Baidu’s nettools is a suite of Go network diagnostics built by the company’s physical network black-box monitoring team. The open-source repo ships six binaries: bitflip, bitflip6, baize, lidar, mping, and mping6. They are not all solving the same problem, which is what makes the codebase interesting. One tool focuses on ICMP probing, another on UDP echo-based corruption detection, another on continuous monitoring, and lidar takes a different route entirely with TCP SYN reachability checks.
What grabbed me from a Go perspective is how much low-level networking it packs into a fairly small repo: raw ICMP sockets, Linux hardware timestamping, packet codecs, per-bucket stats, and platform-specific files where the operating system behavior really differs. It reads like a practical example of where Go stops being “backend API language” and starts looking a lot more like systems programming.
What the repo actually contains
The top-level cmd/ directory is clean and explicit:
cmd/mping/main.goandcmd/mping6/main.go— IPv4 and IPv6 mass-ping toolscmd/bitflip/main.goandcmd/bitflip6/main.go— UDP-based packet loss and bit-flip detectioncmd/baize/main.go— config-driven continuous monitoringcmd/lidar/main.go— TCP SYN reachability probing without deploying an agent on the remote side
That structure matters because the real logic lives below it in reusable packages such as ping/, ping6/, sonar/, sonar6/, lidar/, and stat/. The entrypoints stay thin. They parse flags, build config, wire up senders and processors, and then hand off to a focused package. If you like Go projects where main.go is orchestration rather than a dumping ground, this one gets it right.
mping: raw ICMP with timestamp support
The most obviously low-level code sits in ping/pinger.go. mping opens an ip4:icmp packet connection, builds ICMP echo requests, sends them at a controlled rate, and tracks per-target stats. The packet construction uses goscapy, while the socket setup drops down to raw file descriptors when it needs features the higher-level net API does not expose directly.
The part worth studying is the timestamp handling. ping/hardware_timestamp_linux.go enables Linux socket timestamping with SO_TIMESTAMPING, uses golang.org/x/sys/unix for the timestamp config structs, and parses ancillary data from received packets to prefer hardware timestamps when they are available. The companion file ping/hardware_timestamp_other.go is gated by a //go:build !linux constraint and falls back to a simpler path on non-Linux systems.
That split is textbook Go. When behavior differs meaningfully by platform, you do not bury it behind a tangle of runtime checks. You put the Linux-specific code in one file, the fallback in another, and let the build system select the implementation. The official Go docs call these build constraints, and nettools uses them the way they were intended.
One detail I liked: the repo keeps separate ping/ and ping6/ packages instead of trying to abstract IPv4 and IPv6 into one clever layer. For networking code, that restraint usually pays off. The protocol differences stay visible, and the code stays easier to debug.
sonar: the UDP probe engine behind bitflip
If mping is the raw ICMP side of the repo, sonar/ is the UDP probe engine. The bitflip command pulls in sonar/client, sonar/server, and sonar/config. The client sends high-rate UDP probes with a small header plus a known salt pattern in the payload. The server validates what it receives, echoes the packet back, and both sides can then reason about loss and payload corruption.
This is not a protobuf-shaped project, and that is the right choice. sonar/codec/packet.go and sonar/codec/simple.go work directly with packet bytes because the payload format is tiny and performance-sensitive. When a tool is meant to detect loss and corruption, adding an unnecessarily heavy serialization layer would just make the measuring instrument noisier.
The IPv6 version mirrors this as sonar6/. Again, the project chooses duplication over forced generalization, which is a sensible trade when socket handling diverges.
The stats layer is simple and reusable
The stat/ package is one of the cleaner pieces in the repo. It collects sent and received packets into time buckets, computes loss and RTT summaries, and then hands a StatResult to a Sender interface. The default sender is just a logger in stat/sender.go, but the interface is clearly meant to support other sinks. The package comments even call out things like Kafka and ClickHouse as possible implementations.
That design keeps the transport-specific code separate from reporting. ping, sonar, and lidar all record events into stats, but they do not need to know how output is emitted. That is a small design decision that tends to age well.
lidar is not traceroute, and that is important
The original draft overstated what lidar does. In this repo, lidar is not a TTL-based path tracing tool. Baidu’s own design doc describes it as a TCP SYN reachability probe. It sends SYN packets to a target IP and port, then classifies responses as:
SYN-ACKfor reachable and openRSTfor reachable but closed or rejected- timeout for unreachable or dropped
That distinction matters because the implementation follows from it. lidar/scanner.go builds raw TCP SYN packets, rotates source ports to cover more paths, receives replies through goscapy/sendrecv, and feeds the results into the same stats machinery. The design doc also explains the platform split: Linux can use raw sockets plus filtering, while macOS needs a BPF-based receive path. That is a very different shape from traceroute-style TTL probing.
Why Go fits this repo well
There are a few reasons this code feels natural in Go.
First, the deployment model is excellent. These are CLI tools intended to run on many hosts, and Go’s single-binary story is still hard to beat.
Second, the concurrency model is a good fit. The repo uses goroutines for send loops, read loops, and periodic stats processors without making the code unreadable. This is exactly the kind of I/O-heavy workload where Go tends to feel straightforward.
Third, Go gives you enough low-level access to work with sockets directly when you need to. One caveat: nettools still uses the standard library syscall package in several places. The Go project has documented that syscall is frozen and deprecated in favor of golang.org/x/sys, so I would not treat that as a best practice for new code. But for understanding how the tool works today, it is still the path the repo takes in parts of its socket and ioctl handling.
What I would steal from it
If you build infrastructure-heavy Go software, there are a few patterns here worth copying:
- keep
cmd/entrypoints thin and push real behavior into focused packages - use build-constrained files when platform behavior genuinely differs
- keep packet formats explicit and small when you are writing measurement tools
- separate stats collection from output so you can reuse the same core across multiple binaries
nettools is a good reminder that Go is comfortable well below the HTTP layer. If your mental model of the language is still mostly web handlers and JSON structs, this repo is a useful corrective. There is a lot of practical networking code here, and most of it is organized in a way that is easy to learn from.