Making our own ping
When I launched my Patreon, I vowed to explain how computers work. But in 2019, computers rarely work in isolation. So let's take the time to write a few articles about how computers talk to each other.
Series overview
When I launched my Patreon, I vowed to explain how computers work. But in 2019, computers rarely work in isolation. So let's take the time to write a few articles about how computers talk to each other.
The history of network protocols and standards is long and complicated. Starting with a comprehensive review would prove quite tedious, especially if such a review was done in isolation from modern use.
manySo, how does ping.exe
actually send a ping? It seems unrealistic that
ping.exe
itself implements all the protocols involved in sending a ping.
So it must be calling some sort of library. Also, since it ends up
talking to the outside world via a NIC (network interface controller),
the kernel is probably involved at some point.
In reading files the hard way - part 2, we learned about dynamic libraries (like libc), and the Linux kernel, and how syscalls allowed us to ask the Linux kernel to do our bidding. For this series, we're going to have to look at the Windows equivalents.
It's time to make sup
, our own take on ping
, use the Win32 APIs to send
an ICMP echo. Earlier we discovered that Windows's ping.exe
used
IcmpSendEcho2Ex
. But for our purposes, the simpler IcmpSendEcho
will do
just fine.
As we mentioned earlier, it's provided by IPHLPAPI.dll
, and its C
declaration is:
IPHLPAPI_DLL_LINKAGE DWORD IcmpSendEcho( , , , , , , , );
It's refactor time!
Our complete program is now about a hundred lines, counting blank lines (see the end of part 3 for a complete listing).
While this is pretty good for a zero-dependency project (save for
pretty-hex
), we can do better.
First off, concerns are mixed up. In the same file, we:
- Expose
LoadLibraryA
/GetProcAddress
- Expose the Win32 ICMP API
We've just spent a lot of time abstracting over LoadLibrary, but we still have all the gory details of the Win32 ICMP API straight in our main.rs file! That won't do.
This time will be much quicker, since we already learned about carefully designing an API, hiding the low-level bits and so on.
Let's add an icmp
module to our program. Actually, we've been dealing with
an all this time, it also sounds like it could use its own package:
Our ping API is simple, but it's also very limited:
pub fn ping(dest: ipv4::Addr) -> Result<(), String> // called as: ping(ipv4::Addr([8, 8, 8, 8])).unwrap();
It doesn't allow specifying the TTL (time to live) of packets, it doesn't allow specifying the timeout, it doesn't let one specify the data to send along, and it doesn't give us any kind of information on the reply.
Let's set aside our sup
project for a while.
Don't get me wrong - it's a perfectly fine project, and, were we simply rewriting "ping" for Windows in Rust, we could (almost) stop there.
We're currently using the operating system's facility to speak ICMP, which is great for a bunch of reasons: we can be sure that whatever flaws there are in the implementation, all "native" Windows programs suffer from it as well.
Okay, I lied.
I'm deciding - right this instant - that using wmic is cheating too. Oh, it was fair game when we were learning about Windows, but we're past that now.
We know there's IPv4 routing tables, and we know network interfaces have indices (yes, they do change when you disable/enable one, so ill-timed configuration changes may make our program blow up).
Now that we've found the best way to find the "default network interface"... what can we do with that interface?
Well, listen for network traffic of course!
use rawsock::open_best_library; use std::time::Instant; fn main() -> Result<(), Error> { let lib = open_best_library()?; let iface_name = format!(r#"\Device\NPF_{}"# netinfodefault_nic_guid? iface = libiface_name? start = iface |packet| startelapsed packetlen ?
Before we move on to parsing more of our raw packets, I want to take some time to improve our error handling strategy.
Currently, the ersatz
codebase contains a mix of Result<T, E>
, and some
methods that panic, like unwrap()
and expect()
.
We also have a custom Error
enum that lets us return rawsock errors, IO errors,
or Win32 errors:
pub Rawsockrawsock IOstdio Win32
Hello and welcome to Part 11 of this series, wherein we finally use some of the code I prototyped way back when I was planning this series.
Where are we standing?
Let's review the progress we've made in the first 10 parts: first, we've started thinking about what it takes for computers to communicate. Then, we've followed a rough outline of the various standards and protocols that have emerged since the 1970s.
In the last part, we've finally parsed some IPv4 packets. We even found a way to filter only IPv4 packets that contain ICMP packets.
There's one thing we haven't done though, and that's verify their checksum. Folks could be sending us invalid IPv4 packets and we'd be parsing them like a fool!
This series is getting quite long, so let's jump right into it.
Alright. ALRIGHT. I know, we're all excited, but let's think about what we're doing again.
So we've managed to look at real network traffic and parse it completely. We've also taken some ICMP packets, parsed them, and then serialized them right back and we got the exact same result.
So I know what you're thinking - let's just move our way down the stack again - stuff that ICMP packet in an IP packet, then in an Ethernet frame, and then serialize the whole thing.
So. Serializing IPv4 packets. Easy? Well, not exactly.
IPv4 was annoying to parse, because we had 3-bit integers, and 13-bit integers, and who knows what else. Serializing it is going to be exactly the same.
Right now, we don't have a way to serialize that.
Let's take the version
and ihl
fields, both of which are supposed
to take 4 bits, together making a byte. We could serialize them like this:
This series is complete.