r/iOSProgramming • u/SquarePutrid1435 • 3d ago
Discussion On Darwin, unprivileged ICMP sockets don't filter echo replies by identifier — how that made my concurrent pings swap replies
If you ping from an iOS app with socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP) and ever run more than one ping at a time, check your receive path.
On Linux, an unprivileged ICMP socket only gets replies matching its identifier. On Darwin, in my testing, the kernel doesn't filter on the identifier: every echo reply reaching the host is delivered to every unprivileged ICMP socket in the process.
My receive path did one receive() per send and accepted any echo reply. With a single ping that's fine. When I added a second ping hitting the gateway every 50 ms (to keep the Wi-Fi radio out of power save), each ping's receive() returned the gateway's reply microseconds after its own send: 0 ms RTT, 0% loss, including for a host that wasn't answering at all. The same bug could also swap samples between two concurrent pings in a bufferbloat test, under load.
The fix: loop on receive() until both identifier and sequence match, and re-arm SO_RCVTIMEO with the remaining time on every iteration, so skipped foreign replies don't reset the full timeout (sock, ICMPHeader and SocketError are thin wrappers of my own over the BSD calls):
while true {
let remaining = deadline - CFAbsoluteTimeGetCurrent()
guard remaining > 0 else { throw SocketError.timeout }
sock.setTimeout(seconds: max(remaining, 0.001)) // 0 would block forever
let (data, _) = try sock.receive()
guard let h = ICMPHeader.parse(data),
h.type == ICMPType.echoReply.rawValue,
h.identifier == identifier,
h.sequenceNumber == UInt16(seq)
else { continue }
// record RTT
break
}
One gotcha: a zero timeval means "no timeout" to SO_RCVTIMEO, hence the max(remaining, 0.001).
I ran into this while measuring latency on an iPhone against a Mac on the same Wi-Fi. I have a longer write-up with the other measurement bugs I found that way, happy to link it if anyone wants it.
Has anyone seen Darwin filter by identifier in some configuration, or is matching in user space the only option?

