Modern Linux Tools Part 5: Network Diagnostics With gping, trippy and xh
This is Part 5 of the Modern Linux Tools series. Part 4 covered navigation and history. This part covers the network tools: gping against ping, trippy against mtr and traceroute, and xh against curl.
Same rules as before. A clean Debian 13 VM with 2 vCPUs and 2 GB of RAM, everything installed from the Debian repositories, and the old tool run beside the new one.
sudo apt install gping trippy xh mtr-tiny traceroute iputils-ping bind9-dnsutils
The versions are gping 1.19.0, trippy 0.12.2, xh 0.24.0, mtr-tiny 0.95 and traceroute 2.1.6. The trippy binary is called trip.
Two tools I planned to cover, bandwhich and doggo, are not in the Debian repositories. I left them out of this part rather than install unpackaged binaries on a test machine. This series is partly about what you can get with a plain apt install, and these two do not qualify.
gping against ping
ping prints one line per reply. gping draws a live graph of round-trip time and prints a statistics line per host. It can plot several hosts on the same graph, which is the main reason to use it.
I ran it inside tmux and captured the pane after nine seconds, pinging two public resolvers at once:
1.1.1.1 (1.1.1.1) last 11.1ms min 10.3ms max 14.8ms avg 11.377 jtr 104µs p95 14.8ms t/o 0
8.8.8.8 (8.8.8.8) last 9.13ms min 8.41ms max 11.9ms avg 9.106 jtr 81µs p95 11.3ms t/o 0
15.033ms│
│ ⢠ ⡄
13.789ms│ ⢸ ⡇
│ ⢸⡆ ⣷
12.545ms│ ⢸⡇ ⢀⢿
11.301ms│ ⢀ ⡸⡀ ⡸⢸⢸ ⢀⠞⡄ ⢸ ⡇⢀⢠⢆ ⣄
│⡰⠁⠉⠁⠉⠒⠃⢸⡆⢇⢸ ⠉⠙⣄⠏⠉⢹⡸⡇⠑⠁⠁ ⠉⠈⠂
10.057ms│ ⡜⡇⠘⡇ ⠘ ⠇⣧
8.813ms│⠒⠢⠤⠃⠣⠋⠁⠁⠈⢣⠖⠉⠎⠑⠁⠁⠁⠙⠹ ⣧⠳⠁ ⠁
7.569ms│
└─────────────────────────────────────
Each host gets its last, minimum, maximum and average time, jitter, the 95th percentile and a timeout count. ping gives you the minimum, average, maximum and deviation only when you stop it.
My verdict: gping is useful for one job, comparing two or more targets by eye. Two resolvers, a gateway and a remote host, or before and after a change. It is not a replacement for ping in scripts, because the output is a full-screen interface. Use ping -c 4 for scripts and for anything you paste into a ticket.
trippy against mtr and traceroute
trip does what mtr does: it sends probes with increasing TTLs and keeps a running table of every hop. It adds a full-screen interface, several output modes, and TCP and UDP probing.
It worked without sudo
I expected to need root for raw sockets. On the Debian package trip ran as a normal user with no sudo and no changes. The status bar showed [IPv4/ICMP] [privileged]. Its --unprivileged option, on the other hand, failed on this system:
$ trip --unprivileged 1.1.1.1
error: unprivileged mode not supported on this platform
I did not investigate why the privileged mode works for a normal user here, so do not assume it works the same on another distribution.
The flag that bites
In mtr, -c is the number of cycles. In trip, -c is the config file. The cycle count is -C:
trip -m markdown -C 3 -t 6 1.1.1.1 # 3 cycles, max TTL 6, markdown report
If you are used to mtr -c 10, you will get an error about a missing config file instead of a report. The -m option picks the output mode: pretty, markdown, csv, json and others. The -t option is the maximum TTL.
The TUI
Running trip 1.1.1.1 with no mode opens the interface. This is a capture after eight cycles, with my own hops anonymised:
Target: 192.168.11.154 (modern-tools) -> 1.1.1.1 (one.one.one.one)
Status: Running, discovered 6 hops
# Host Loss% Snd Recv Last Avg Best Wrst StDev
1 _gateway 0.0% 8 8 1.1 1.0 0.7 1.7 0.3
2 <isp-router> 50.0% 8 4 261.3 262.0 253.8 275.1 7.5
3 <isp-upstream> 12.5% 8 7 10.0 9.9 9.6 10.2 0.2
4 141.101.71.166 12.5% 8 7 13.8 17.8 9.5 42.7 10.5
5 141.101.71.174 12.5% 8 7 10.8 10.7 10.2 12.2 0.6
6 No response 100.0% 8 0
Machine-readable output
The CSV and JSON modes make trip easy to script. The same target in CSV, two cycles:
Target,TargetIp,Hop,IPs,Addrs,Loss%,Snt,Recv,Last,Avg,Best,Wrst,StdDev
1.1.1.1,1.1.1.1,1,192.168.11.1,192.168.11.1,0.00,2,2,0.7,0.76,0.7,0.8,0.02
1.1.1.1,1.1.1.1,2,<isp-router>,<isp-router>,0.00,2,2,252.1,246.40,240.7,252.1,5.67
1.1.1.1,1.1.1.1,3,<isp-upstream>,<isp-upstream>,0.00,2,2,9.1,8.97,8.9,9.1,0.10
1.1.1.1,1.1.1.1,4,141.101.71.166,141.101.71.166,0.00,2,2,10.5,10.26,10.0,10.5,0.23
The JSON mode has a hops array. Each hop carries ttl, hosts, loss_pct, sent, recv, last, avg, best, worst, stddev and several jitter fields (jitter, javg, jmax, jinta).
TCP probes
Many networks drop ICMP and UDP probes but pass traffic to port 443. trip can probe with TCP:
trip -p tcp -P 443 -m csv -C 3 1.1.1.1
In that mode some hops answered with several different addresses in the same cycle. That is equal-cost multipath routing: the network sends packets to the same destination by different links, and each probe may take a different one. mtr has -T -P 443 for the same job.
The AS lookup
trip -z looks up the autonomous system for each hop. With the default system resolver it failed:
error: AS lookup not supported by resolver `system`, use `-r`
Choosing an explicit resolver with -r cloudflare is the way around that. I only verified the error and the hint, so check the result on your own machine.
What all three tools showed about hop 2
This is the most useful result of the whole test. Hop 2 in my path, my ISP’s first router, showed round trips of around 250 ms. Every hop after it showed around 10 ms. That cannot be true: packets that reach hop 5 in 10 ms cannot have passed through a router that took 250 ms.
mtr, traceroute and trip all agreed, and traceroute showed * at hop 2. The explanation is that a router forwards traffic in hardware but answers ICMP messages about it, such as the time-exceeded replies traceroute relies on, with its control-plane CPU, and often gives that work low priority. The delay and the loss on hop 2 are in the router’s replies, not in the traffic going through it. The rule for reading any of these tools: loss or latency on one hop that does not continue to later hops is not a fault. Look at the final hop, and at where a problem starts and stays.
My verdict: trip is a better mtr if you want the interface, the output modes and TCP probing in one tool, and it worked on Debian with no setup. If you already have mtr in your fingers, -C against -c is the only thing that will trip you up. Both are more useful than traceroute for anything beyond a single quick check, because a table that keeps updating shows the variation that one pass hides.
xh against curl
xh is a Rust tool that follows the HTTPie interface. It sends HTTP requests with a shorter syntax and prints readable output.
For a test target I ran a small Python HTTP server on the VM that echoes back the method, headers and body. No traffic left the machine.
Sending JSON
The curl way needs the content type, the method and a quoted JSON string:
curl -s -X POST -H 'Content-Type: application/json' \
-d '{"name":"web01","port":8080,"tags":["a","b"]}' http://127.0.0.1:8099/api
The xh way:
xh POST 127.0.0.1:8099/api name=web01 port:=8080 tags:='["a","b"]'
name=web01 is a string field. port:=8080 is a raw JSON value, so it arrives as a number. The server saw the same body from both tools:
curl: "body": "{\"name\":\"web01\",\"port\":8080,\"tags\":[\"a\",\"b\"]}"
xh: "body": "{\"name\":\"web01\",\"port\":8080,\"tags\":[\"a\",\"b\"]}"
The two tools sent different headers. curl sent Accept: */*. xh sent Accept: application/json, */*;q=0.5 and a Content-Type: application/json it chose itself. The user agent was curl/8.14.1 against xh/0.24.0.
Query parameters and offline mode
== adds a query parameter and does the URL encoding:
$ xh -b GET 127.0.0.1:8099/search q==debian page==2
{"method": "GET", "path": "/search?q=debian&page=2", ...}
--offline builds the request and prints it without sending, which is a good way to check what you are about to send:
$ xh --offline POST 127.0.0.1:8099/api name=web01 port:=8080
POST /api HTTP/1.1
Accept-Encoding: gzip, deflate, br, zstd
User-Agent: xh/0.24.0
Connection: keep-alive
Accept: application/json, */*;q=0.5
Content-Type: application/json
Content-Length: 28
Host: 127.0.0.1:8099
{"name":"web01","port":8080}
Terminal against pipe
On a terminal xh prints the response headers and a coloured, indented body. When you pipe it, it prints the body only, and -p hb forces headers and body back:
$ xh -p hb GET 127.0.0.1:8099/x | cat
HTTP/1.0 200 OK
Server: BaseHTTP/0.6 Python/3.13.5
Content-Type: application/json
Content-Length: 67
{"method": "GET", "path": "/x", "accept": "*/*", "ua": "xh/0.24.0"}
The same applies to colour. Piped output has no colour codes unless you ask for them with --pretty=all, and when you do, the escape codes are in the stream. That is the behaviour you want from a tool that is used both by people and by scripts.
One more trap. xh reads standard input as the request body when stdin is not a terminal, which is how you pipe a file into a request. In a script or a CI job that is easy to hit by accident, so add --ignore-stdin. I used it in every test here.
Speed
For 50 requests to the local server, run with hyperfine:
| Command | Mean time |
|---|---|
curl -s http://127.0.0.1:8099/x | 8.8 ms |
xh --ignore-stdin -b http://127.0.0.1:8099/x | 40.3 ms |
curl is about 4.6 times faster per request here. The difference is start-up cost, and it does not matter when you run one request by hand. It matters when a script calls the tool hundreds of times.
My verdict: xh is nicer for exploring an API by hand, because the JSON syntax is shorter, you get type-aware fields, and you can preview a request with --offline. curl is the tool for scripts: it is on every machine, faster to start, and it does not read stdin by default. I use xh at the terminal and curl in anything I commit.
Summary
| Tool | Replaces | Verdict |
|---|---|---|
| gping | ping | Good for comparing hosts by eye, not for scripts |
| trippy | mtr, traceroute | Better mtr, worked without sudo on Debian, mind -C |
| xh | curl | Nicer by hand, curl for scripts |
Whichever traceroute tool you use, remember hop 2. A router answering slowly is not the same as a router forwarding slowly.
The next part covers data and git: jq and gojq, delta, lazygit and tldr.