One's-complement arithmetic, performed by a router that is only trying to tell you your packet expired. A hop returns a sum, hence the name.
A router that drops a packet on TTL zero may reply with ICMP Time Exceeded,
quoting the original IPv4 header and the first eight bytes of payload. The ICMP
checksum is computed over all of it, so part of its input is yours to choose.
Send IPv4 protocol 253 with the payload 0xF4FF || a || b || c and everything
you did not choose cancels out: a valid IPv4 header already sums to 0xFFFF,
and 0xF4FF absorbs the Time Exceeded type and code. What comes back in the
checksum field is ~(a ⊞ b ⊞ c).
That single probe is the whole machine. Addition, subtraction, and multiplication are built by sending replies back in as the next operands. The router never multiplies anything; it only ever sums the words in your payload and complements the result.
As arithmetic this is gloriously impractical, since every probe spends a round trip on work a CPU finishes in one instruction. The point is that the router computes anything for you at all, as a byproduct of an error message it was obliged to send.
Linux, Go 1.22+, and cap_net_raw:
go install github.com/kuzand/hopsum@latest
sudo setcap cap_net_raw+ep "$(go env GOPATH)/bin/hopsum"Or from a clone:
go build -o hopsum . && sudo setcap cap_net_raw+ep ./hopsumEvery build writes a new file with no capabilities, so setcap has to follow
each one.
| Command | Probes | Result |
|---|---|---|
checksum a [b [c]] |
1 | ~(a ⊞ b ⊞ c), the reply as it arrives |
negate a |
1 | ~a, the same primitive with b and c left at zero |
verify a [b [c]] expected |
1 | exits 0 if the reply matches |
add a [b [c]] |
2 | a ⊞ b ⊞ c, the second probe cancelling the first complement |
sub a b |
2 | a ⊟ b, computed as a ⊞ ~b |
mul a b |
2 per step | a * b by double-and-add |
Operands are 16-bit words, and the payload holds three of them. Where a command
takes several, a lone operand may instead be up to 48 bits and is cut into words
from the low end, so checksum 0x0064012c and checksum 0x0064 0x012c are the
same probe. Leading zeros are free, since 0 is the additive identity here, and
add on one wide operand folds it back down to 16 bits.
With -x, the operands of checksum, verify and add are read as a message
instead of as numbers: up to six hex octets, written together or separated
however is convenient. A trailing odd octet is padded on the right with a zero
byte, as RFC 1071 requires, so
the three bytes 12 34 56 sum as 0x1234 ⊞ 0x5600. The pad exists only for
the checksum and is never sent.
$ ./hopsum -x checksum 12 34 56
Target: 10.128.0.1
Probes: 1
Result: 0x97cb (38859)
That is the real Internet checksum of those three bytes, computed by a router that was only trying to report an expired packet.
For mul, a step is one double or one add, so the cost scales with the bit
length and the popcount of the multiplier. Both factors are known locally once a
reply is back, so it multiplies by whichever one is cheaper.
$ ./hopsum mul 12 5
Target: 10.128.0.1
Probes: 6
Result: 0x003c (60)
| Flag | Default | Meaning |
|---|---|---|
-dst |
1.1.1.1 |
where the packet is aimed; the TTL should expire long before it |
-ttl |
1 |
IPv4 TTL, where 1 is usually your own gateway |
-q |
off | print only the decimal result, for pipes |
-x |
off | read operands as hex octets, padded per RFC 1071, not as numbers |
-timeout |
3s |
how long to wait for one reply |
-pace |
500ms |
minimum gap between sends |
-retries |
3 |
resends when a reply goes missing |
Operands are decimal or 0x hex. - reads one word from stdin, so with -q the
probes compose:
./hopsum -q add 100 200 | ./hopsum -q mul - 5The arithmetic is the checksum's own, which means modulo 65535 rather than
2^16, with 0x0000 and 0xFFFF both behaving as zero. A product that lands on
a multiple of 65535 comes back as 0xFFFF.
The whole trick rests on the IPv4 header checksum cancelling itself. IPv6 has no such field, so none of this carries over.
Whether anything answers is painfully path-dependent. TTL 1 is normally your own
gateway and usually works, but past that, carrier NAT and tunnelled IPv4 often
mean nothing comes back at all. Run traceroute over the path you intend to use
before blaming the tool, and if it dies just past the gateway, a VPN will usually
revive it by moving TTL expiry onto routers that do answer.
You also need a hop that quotes your eight bytes without rewriting them, and Time Exceeded is widely rate-limited, which is why sends are paced and missed replies retried rather than reported as failures.
probe.go- raw sockets, the singleProbe, and Time Exceeded parsingcalc.go- add / sub / mul, built by chaining probesmain.go- the CLI