Thanks to my sponsors: Michal Hošna, Radu Matei, Tom Forbes, Sawyer Knoblich, Jake Demarest-Mays, Jelle Besseling, Chris Sims, Nicolas Riebesel, Peter Shih, Chris Emery, Paul Schuberth, Daniel Strittmatter, Andy Gocke, Mason Ginter, René Ribaud, Jean Manguy, Xirvik Servers, Romain Kelifa, Marty Penner, Luiz Irber and 230 more
Binding C APIs with variable-length structs and UTF-16
👋 This page was last updated ~5 years ago. Just so you know.
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).
We also know how to call Win32 APIs directly, and we know that WMI really is more geared toward system administrators than it is toward developers.
There's no way a query language is the canonical way to retrieve that kind of information. Also, we've gone slightly over our crate budget.
Let's fix that right now.
$ cargo rm maplit serde wmi
Removing maplit from dependencies
Removing serde from dependencies
Removing wmi from dependencies
Ahhh. deep sigh. That's better.
(Note that you'll need to remove any code referencing those, especially wmi
,
thanks to kwas on GitHub
for remarking that).
A quick search for "win32 API ipv4 routing tables" returns the GetIpForwardTable
function as a second result:
IPHLPAPI_DLL_LINKAGE DWORD GetIpForwardTable(
PMIB_IPFORWARDTABLE pIpForwardTable,
PULONG pdwSize,
BOOL bOrder
);
It lives in... well, would you look at that, IPHLPAPI.dll
. That sounds familiar!
It looks like it returns the full table (MIB_IPFORWARDTABLE
), which itself has rows
(MIB_IPFORWARDROW
), which in turn contain.. the destination!
typedef struct _MIB_IPFORWARDROW {
DWORD dwForwardDest;
DWORD dwForwardMask;
DWORD dwForwardPolicy;
DWORD dwForwardNextHop;
IF_INDEX dwForwardIfIndex;
// (other fields cut)
} MIB_IPFORWARDROW, *PMIB_IPFORWARDROW;
I also spy an "IfIndex" in there, and pray that "If" stands for "Network Interface".
Before we truly leave WMI behind, let's make a small C program just to confirm that we can indeed use those Win32 APIs to get the information we want.
We'll keep it real simple. First we'll need to include some headers:
#include <windows.h>
#include <iphlpapi.h>
#include <ipmib.h>
#include <stdio.h>
int main() {
// C crimes go here
return 0;
}
Then we'll call GetIpForwardTable
once, with a NULL
buffer, just so we can query
how much size it actually needs.
// in main
DWORD err;
ULONG table_size = 0;
err = GetIpForwardTable(NULL, &table_size, FALSE);
if (err == ERROR_INSUFFICIENT_BUFFER) {
// good, continue
} else {
fprintf(stderr, "GetIpForwardTable returned: %x\n", err);
return 1;
}
printf("table_size = %d\n", table_size);
Let's give it a go.
> cl.exe getiface.c /link iphlpapi.lib
Microsoft (R) C/C++ Optimizing Compiler Version 19.23.28105.4 for x64
Copyright (C) Microsoft Corporation. All rights reserved.
getiface.c
Microsoft (R) Incremental Linker Version 14.23.28105.4
Copyright (C) Microsoft Corporation. All rights reserved.
/out:getiface.exe
iphlpapi.lib
getiface.obj
> .\getiface.exe
table_size = 2196
Cool bear's hot tip
The above commands are run from "x64 Native Tools Command Prompt for VS 2019", which we installed waaaaaaaaay back in Part 2.
Not shown above: starting PowerShell first, for nicer coloring, completion, and
Ctrl+L
support (to clear the terminal). Typing cls
is only fun the first
couple thousand times.
Seems legit. Let's allocate enough space and call it a second time.
// still in main
PMIB_IPFORWARDTABLE table = calloc(table_size, 1);
err = GetIpForwardTable(table, &table_size, FALSE);
if (err == NO_ERROR) {
// good
} else {
fprintf(stderr, "GetIpForwardTable (second) returned: %x\n", err);
return 1;
}
printf("Num entries: %d\n", table->dwNumEntries);
Note that we're using calloc
because it zeroes the allocated memory for us.
Is it necessary? I'm not looking it up - you look it up. I'd look it up if it was performance-critical, which it isn't, so I'd rather be on the safe side.
> .\getiface.exe
Num entries: 21
Alright, that seems realistic. 21 routing rules ought to be enough for everyone.
Now, we need to find the routing rule that has 0.0.0.0
as a destination.
As we've found out earlier, IPv4 addresses are typically "just a DWORD
".
It follows that - no matter what endianness we're into - the address 0.0.0.0
should be just 0
.
Let's find it:
// forever in main
int ifaceIndex = -1;
for (int i = 0; i < table->dwNumEntries; i++) {
if (0 == table->table[i].dwForwardDest) {
ifaceIndex = table->table[i].dwForwardIfIndex;
break;
}
}
if (ifaceIndex == -1) {
fprintf(stderr, "Default interface not found");
return 1;
}
printf("Default interface index = %d\n", ifaceIndex);
And the winner is...
> .\getiface.exe
Num entries: 21
Default interface index = 5
Number 5! Just like in rehearsal.
Next up, we need to find the GUID of the network interface.
It just so happens that IPHLPAPI.dll
contains a GetInterfaceInfo
function, which I think will do nicely.
Its signature is similar:
IPHLPAPI_DLL_LINKAGE DWORD GetInterfaceInfo(
PIP_INTERFACE_INFO pIfTable,
PULONG dwOutBufLen
);
Before using it, let's check what data it actually returns, IP_INTERFACE_INFO
:
typedef struct _IP_INTERFACE_INFO {
LONG NumAdapters;
IP_ADAPTER_INDEX_MAP Adapter[1];
} IP_INTERFACE_INFO, *PIP_INTERFACE_INFO;
Okay, okay, so we'll get all adapters, very good. What's in IP_ADAPTER_INDEX_MAP
?
typedef struct _IP_ADAPTER_INDEX_MAP {
ULONG Index;
WCHAR Name[MAX_ADAPTER_NAME];
} IP_ADAPTER_INDEX_MAP, *PIP_ADAPTER_INDEX_MAP;
Ah. Well, at this point we can only hope that Index
is, in fact, the same
index that GetIpForwardTable
was talking about.
I don't see a GUID anywhere, though, do you?
Remarks
The
IP_ADAPTER_INDEX_MAP
structure is specific to network adapters with IPv4 enabled.An adapter index may change when the adapter is disabled and then enabled, or under other circumstances, and should not be considered persistent.
On Windows Vista and later, the Name member of the
IP_ADAPTER_INDEX_MAP
structure may be a Unicode string of the GUID for the network interface (the string begins with the'{'
character).
Ah, there it is.
Well this all sounds fine, let's proceed:
// no one can escape the main
err = GetInterfaceInfo(NULL, &table_size);
if (err == ERROR_INSUFFICIENT_BUFFER) {
// good, continue
} else {
fprintf(stderr, "GetInterfaceInfo returned: %x\n", err);
return 1;
}
PIP_INTERFACE_INFO ifaces = calloc(table_size, 1);
err = GetInterfaceInfo(ifaces, &table_size);
if (err == NO_ERROR) {
// good
} else {
fprintf(stderr, "GetInterfaceInfo (second) returned: %x\n", err);
return 1;
}
printf("Listed %d ifaces\n", ifaces->NumAdapters);
for (int i = 0; i < ifaces->NumAdapters; i++) {
if (ifaces->Adapter[i].Index == ifaceIndex) {
printf("Found it! Name is: %S", ifaces->Adapter[i].Name);
}
}
> .\getiface.exe
Num entries: 21
Default interface index = 5
Listed 7 ifaces
Found it! Name is: \DEVICE\TCPIP_{0E89380B-814A-48FC-86C4-5C51B8040CB2}
Success! There is indeed a GUID in there, and it is indeed the same we've gotten through WMI.
Now we just need to quiet sob bind these two functions.
Hello IPHLPAPI my old friend
Luckily, we've made a pretty good LoadLibrary wrapper earlier in the series, so my vote is that we just use that.
In fact, let's just copy the package entirely from sup
(our ping clone) into ersatz
(our userland re-implementation of various network protocols).
Let's also make a separate module just for getting the default interface GUID.
We'll call it netinfo
.
// in `src/main.rs`
mod loadlibrary;
mod netinfo;
fn main() -> Result<(), Error> {
let interface_name = format!(r#"\Device\NPF_{}"#, netinfo::default_nic_guid()?);
let lib = open_best_library()?;
lib.open_interface(&interface_name)?;
println!("Interface opened!");
Ok(())
}
// not shown: copying `src/loadlibrary.rs` from `sup` to `ersatz`
// in `src/netinfo.rs`
use crate::error::Error;
pub fn default_nic_guid() -> Result<String, Error> {
unimplemented!()
}
Now, in Part 6 we made a nice macro for
binding Win32 DLLs, but it's stuck in the sup
project - and it hardcoded
IPHLPAPI.dll
, so it's not like it was reusable.
So let's make it reusable!
We'll want the once_cell
crate for this:
$ cargo add once_cell
(cut)
// in `src/loadlibrary.rs`, at the bottom
#[macro_export]
macro_rules! bind {
// new: `library $lib:expr;`
(library $lib:expr; $(fn $name:ident($($arg:ident: $type:ty),*) -> $ret:ty;)*) => {
struct Functions {
$(pub $name: extern "stdcall" fn ($($arg: $type),*) -> $ret),*
}
static FUNCTIONS: once_cell::sync::Lazy<Functions> =
once_cell::sync::Lazy::new(|| {
let lib = crate::loadlibrary::Library::new($lib).unwrap();
Functions {
$($name: unsafe { lib.get_proc(stringify!($name)).unwrap() }),*
}
});
$(
#[inline(always)]
pub fn $name($($arg: $type),*) -> $ret {
(FUNCTIONS.$name)($($arg),*)
}
)*
};
}
And use it from netinfo
. The functions we really want are a tad complicated,
so let's just make sure we've copied all the right bits by calling a simple
function we know about:
// in `src/netinfo.rs`
#![allow(non_snake_case)]
use crate::error::Error;
pub fn default_nic_guid() -> Result<String, Error> {
let icmp_file = IcmpCreateFile();
dbg!(icmp_file);
unimplemented!()
}
crate::bind! {
library "IPHLPAPI.dll";
fn IcmpCreateFile() -> *const std::ffi::c_void;
}
Give it a go:
$ cargo run --quiet
[src\netinfo.rs:6] icmp_file = 0x0000020f44bfc190
thread 'main' panicked at 'not yet implemented', src\netinfo.rs:8:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
Fantastic!
Variable-length structs
Now let's try to bind the actual functions. The hard part is not really the functions, but the data structures themselves.
The C declaration for MIB_IPFORWARDTABLE
reads as follows:
typedef struct _MIB_IPFORWARDTABLE {
DWORD dwNumEntries;
MIB_IPFORWARDROW table[ANY_SIZE];
} MIB_IPFORWARDTABLE, *PMIB_IPFORWARDTABLE;
Can we express that in Rust? Of course we can!
This is perfectly valid:
// we've learned earlier in this series about different
// struct layouts - we definitely want C representation here.
#[repr(C)]
#[derive(Debug)]
pub struct IpForwardTable {
num_entries: u32,
entries: [IpForwardRow],
}
#[repr(C)]
#[derive(Debug)]
pub struct IpForwardRow {}
crate::bind! {
library "IPHLPAPI.dll";
fn GetIpForwardTable(table: *mut IpForwardTable, size: *mut u32, order: bool) -> u32;
}
However, if we try to allocate it on the stack:
pub fn default_nic_guid() -> Result<String, Error> {
let table: IpForwardTable;
unimplemented!()
}
Then we start running into problems:
$ cargo check --quiet
error[E0277]: the size for values of type `[netinfo::IpForwardRow]` cannot be known at compilation time
--> src\netinfo.rs:5:9
|
5 | let table: IpForwardTable;
| ^^^^^ doesn't have a size known at compile-time
|
= help: within `netinfo::IpForwardTable`, the trait `std::marker::Sized` is not implemented for `[netinfo::IpForwardRow]`
= note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
= note: required because it appears within the type `netinfo::IpForwardTable`
= note: all local variables must have a statically known size
= help: unsized locals are gated as an unstable feature
We accidentally ended up with a DST - a Dynamically-Sized Type.
At the time of this writing (Rust 1.38.0 is stable), DSTs are kinda awkward
to work with. I won't go into details, largely because I don't fully understand
them, but the gist is that - as usual, rustc is right. We cannot allocate an
IpForwardTable
on the stack because we don't know what its size is.
It may have zero entries, or one, or twenty-one, and there is nothing in our code that lets the compiler know how many entries it has. And even if we do specify an array of entries, rustc isn't happy.
My understanding is that, for that particular usecase (variable-length C structs), DSTs are "unfinished". There are pre-RFCs (Requests For Comments) floating around, but nothing quite usable yet. The situation might be better in unstable, but we're going to stick with stable for now.
So, for the time being, we're going to pretend that our entries
member is a fixed-size
array of size 1:
#[repr(C)]
#[derive(Debug)]
pub struct IpForwardTable {
num_entries: u32,
entries: [IpForwardRow; 1],
}
And then this works great:
pub fn default_nic_guid() -> Result<String, Error> {
let mut table: IpForwardTable;
let mut size = std::mem::size_of_val(&table) as u32;
GetIpForwardTable(&mut table, &mut size, false);
unimplemented!()
}
Wait, no, it doesn't:
$ cargo check --quiet
error[E0381]: borrow of possibly uninitialized variable: `table`
--> src\netinfo.rs:6:42
|
6 | let mut size = std::mem::size_of_val(&table) as u32;
| ^^^^^^ use of possibly uninitialized `table`
Fair enough. We haven't initialized table. But we don't want to initialize table -
I'm pretty sure GetIpForwardTable
should initialize table.
Can we use MaybeUninit
, as we learned in Part 3 ?
use std::mem::{self, MaybeUninit};
pub fn default_nic_guid() -> Result<String, Error> {
let mut table: MaybeUninit<IpForwardTable> = MaybeUninit::uninit();
let mut size = mem::size_of_val(&table) as u32;
GetIpForwardTable(table.as_mut_ptr(), &mut size, false);
let table = unsafe { table.assume_init() };
dbg!(&table);
std::process::exit(0);
}
$ cargo run --quiet
[src\netinfo.rs:13] &table = IpForwardTable {
num_entries: 3355314237,
entries: [
IpForwardRow,
],
}
Uhhh... something's up. I promise I haven't added 3355314216 network
interfaces to my computer. Lucky we derived Debug
, so it's easy to
see something's going wrong!
For starters, we haven't checked GetIpForwardTable
's return value.
Win32 error codes are typically just DWORD
, so let's make an Error
variant for them in our error type:
// in `src/error.rs`
pub enum Error {
// omitted: Rawsock, IO variants
Win32(u32),
}
// omitted: Rawsock, IO impls
impl From<u32> for Error {
fn from(e: u32) -> Self {
Self::Win32(e)
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
// omitted: Rawsock, IO match arms
Self::Win32(e) => write!(f, "Win32 error code {} (0x{:x})", e, e),
}
}
}
And then use that if we get a non-zero value returned from GetIpForwardTable
:
// in `src/netinfo.rs`
// in `default_nic_guid()`
let ret = GetIpForwardTable(table.as_mut_ptr(), &mut size, false);
if ret != 0 {
return Err(Error::Win32(ret));
}
$ cargo run --quiet
Error: Win32 error code 122 (0x7a)
Ah. Well then. Let's look up that error code (a quick !msdocs system error codes
search
on DuckDuckGo will do)
ERROR_INSUFFICIENT_BUFFER
122 (0x7A)
The data area passed to a system call is too small.
Oh hey, we know that one - we were actually expecting it.
Now we just, uh, make it larger and try again, and uhh
const ERROR_INSUFFICIENT_BUFFER: u32 = 122;
pub fn default_nic_guid() -> Result<String, Error> {
let mut table: MaybeUninit<IpForwardTable> = MaybeUninit::uninit();
let mut size = mem::size_of_val(&table) as u32;
match GetIpForwardTable(table.as_mut_ptr(), &mut size, false) {
ERROR_INSUFFICIENT_BUFFER => {
// good, continue
}
ret => return Err(Error::Win32(ret)),
}
// TODO: how the heck do we make `table` bigger?
// ❓❓❓
unimplemented!()
}
The short answer is: we can't. We done goofed. Consequences will never be the same. I'm also pretty sure we've been reported to the cyber police.
How about we flip the problem around. Instead of starting with an
IpForwardTable
table and trying to make it bigger, how about we start with
"raw memory" and try to get an IpForwardTable
later, mh?
So, how does one go about allocating raw memory in Rust?
Well, there's std::alloc::alloc
:
use std::alloc::{alloc, dealloc, Layout};
unsafe {
let layout = Layout::new::<u16>();
let ptr = alloc(layout);
*(ptr as *mut u16) = 42;
assert_eq!(*(ptr as *mut u16), 42);
dealloc(ptr, layout);
}
I'm uhhh not super excited about this. We don't really want to have the responsibility
to dealloc
by hand (and keep track of the layout!).
How about we use a good old vec instead? That'll be nice.
We'll do it just like in our C version: first call GetIpForwardTable
with a null pointer,
expecting ERROR_INSUFFICIENT_BUFFER
, then allocate the right amount of
memory, and call it again, expecting a zero return code.
Vec
has as_mut_ptr
, which works great for us. We just need to transmute it a bit.
use std::{mem, ptr};
pub fn default_nic_guid() -> Result<String, Error> {
let mut size = 0;
match GetIpForwardTable(ptr::null_mut(), &mut size, false) {
ERROR_INSUFFICIENT_BUFFER => {} // good, continue
ret => return Err(Error::Win32(ret)),
}
let mut v = vec![0u8; size as usize];
match GetIpForwardTable(unsafe { mem::transmute(v.as_mut_ptr()) }, &mut size, false) {
0 => {} // good, continue
ret => return Err(Error::Win32(ret)),
}
let table: &IpForwardTable = unsafe { mem::transmute(v.as_ptr()) };
dbg!(table.num_entries);
unimplemented!()
}
Does it work?
$ cargo run --quiet
[src\netinfo.rs:20] table.num_entries = 21
Pheww.
That code is, uh, whew, it's something though. Remember how Rust is memory-safe?
Yeah, that's gone now. We forsook that.
In our code, table
is just a view into v
(the byte vector that actually
holds the data), but as far as types and lifetimes (which are mostly types) are concerned,
there's nothing linking them, so for example, we can do this:
let table: &IpForwardTable = unsafe { mem::transmute(v.as_ptr()) };
drop(v);
dbg!(table.num_entries);
And kaboom:
$ cargo run --quiet
[src\netinfo.rs:21] table.num_entries = 2248212816
Ah, nothing like the smell of an uninitialized memory read in the morning.
So, this code isn't exactly.. good. If we're not extremely careful, bad things can - nay, will - happen. We'll have to get everyone at the unsafe working group to review our code, and I'm sure they have better things to do.
Plus, we're going to have to do that again, with GetInterfaceInfo
, and what
are the chances we get it right twice in a row?
A generic "variable length struct" container
Let's use more types.
We want to keep together:
- our
Vec<u8>
which contains the actual data - the pointer/reference which is a view into that data, with the layout we want
We'd also like, if possible, to have some facility to call Win32 functions twice - once with a null buffer, the second time with a buffer of exactly the right size.
Let's get started:
// in `src/main.rs`
mod vls; // for "variable-length struct"
// in `src/vls.rs`
pub struct VLS<T> {
v: Vec<u8>,
}
Good start! We definitely want our VLS
struct to be generic, since we're going
to be using it with IpForwardTable
, and later, IpInterfaceInfo
.
rustc isn't thrilled though:
$ cargo check --quiet
error[E0392]: parameter `T` is never used
--> src\vls.rs:1:16
|
1 | pub struct VLS<T> {
| ^ unused parameter
|
= help: consider removing `T` or using a marker such as `std::marker::PhantomData`
Alright, alright, who am I to argue:
use std::marker::PhantomData;
pub struct VLS<T> {
v: Vec<u8>,
_phantom: PhantomData<T>,
}
This compiles. Good.
Now, we want VLS<T>
to behave like T
, mostly. Just so we're able to
access T
's fields, and methods.
So we want to implement std::ops::Deref
:
use std::{marker::PhantomData, mem, ops::Deref};
// cut: struct VLS
impl<T> Deref for VLS<T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { mem::transmute(self.v.as_ptr()) }
}
}
Okay. Is this any better than before? Let's find out.
If we got it right, this should compile:
use crate::vls::VLS;
fn get_vls<T>() -> VLS<T> {
unimplemented!()
}
pub fn default_nic_guid() -> Result<String, Error> {
let vls = get_vls::<IpForwardTable>();
dbg!(vls.num_entries);
// etc.
}
(And it does).
And this should not compile:
pub fn default_nic_guid() -> Result<String, Error> {
// allocate...
let vls = get_vls::<IpForwardTable>();
// deref to &IpForwardTable...
let table: &IpForwardTable = &*vls;
// deallocate
drop(vls);
// woops, that's illegal
dbg!(table);
}
And it doesn't!
$ cargo check --quiet
error[E0505]: cannot move out of `vls` because it is borrowed
--> src\netinfo.rs:16:10
|
14 | let table: &IpForwardTable = &*vls;
| --- borrow of `vls` occurs here
15 | // deallocate
16 | drop(vls);
| ^^^ move out of `vls` occurs here
17 | // woops, that's illegal
18 | dbg!(table);
| ----- borrow later used here
So, when using our VLS
type, at least lifetimes are taken care of. VLS
owns the data, and when we deref it to whatever C-repr struct, we get a
reference whose lifetime must not outlive the VLS
instance.
Now for the actual calls. We want to be able to call any Win32 function, but they all work rather the same way:
- First call with a null pointer, should get
ERROR_INSUFFICIENT_BUFFER
- Second call with a memory block of the right size, should get 0
So, we want our VLS::new
signature to look something like this:
use crate::error::Error;
impl<T> VLS<T> {
pub fn new<F>(f: F) -> Result<Self, Error>
where
F: Fn(*mut T, *mut u32) -> u32,
{
unimplemented!()
}
}
So that we can use it like this:
pub fn default_nic_guid() -> Result<String, Error> {
let table = VLS::new(|ptr, size| GetIpForwardTable(ptr, size, false))?;
dbg!(table.num_entries);
unimplemented!()
}
This all compiles, but obviously will panic at runtime, so it's time
to implement new
for real.
Bear with me for a second:
// in `src/vls.rs`
use std::ptr;
const ERROR_INSUFFICIENT_BUFFER: u32 = 122;
impl<T> VLS<T> {
pub fn new<F>(f: F) -> Result<Self, Error>
where
F: Fn(*mut T, *mut u32) -> u32,
{
let mut size = 0;
match f(ptr::null_mut(), &mut size) {
ERROR_INSUFFICIENT_BUFFER => {} // good
ret => return Err(Error::Win32(ret)),
};
let mut v = vec![0u8; size as usize];
match f(unsafe { mem::transmute(v.as_mut_ptr()) }, &mut size) {
0 => {} // good
ret => return Err(Error::Win32(ret)),
};
Ok(Self {
v,
_phantom: PhantomData::default(),
})
}
}
Not that bad.
Does it work?
$ cargo run --quiet
[src\netinfo.rs:8] table.num_entries = 21
Yay! Feels pretty good.
We sorta.. forgot one part though. How do we access the actual entries?
We can just implement a method on IpForwardTable
:
// in `src/netinfo.rs`
use std::slice;
impl IpForwardTable {
fn entries(&self) -> &[IpForwardRow] {
// just like we'd do in C: take the address of the first element.
// note: this will blow up if `self.num_entries` is incorrect.
unsafe { slice::from_raw_parts(&self.entries[0], self.num_entries as usize) }
}
}
pub fn default_nic_guid() -> Result<String, Error> {
let table = VLS::new(|ptr, size| GetIpForwardTable(ptr, size, false))?;
dbg!(table.num_entries);
dbg!(table.entries()); // new!
unimplemented!()
}
$ cargo run --quiet
[src\netinfo.rs:8] table.num_entries = 21
[src\netinfo.rs:9] table.entries() = [
IpForwardRow,
IpForwardRow,
IpForwardRow,
IpForwardRow,
(cut: 17 more entries)
]
Ok, ok, ok. That's ok, we're ok. Everyone's ok.
Those rows look a.. little bit empty though.
#[repr(C)]
#[derive(Debug)]
pub struct IpForwardRow {}
Oh right.
Well, the C declaration for MIB_IPFORWARDROW
looks like this:
typedef struct _MIB_IPFORWARDROW {
DWORD dwForwardDest;
DWORD dwForwardMask;
DWORD dwForwardPolicy;
DWORD dwForwardNextHop;
IF_INDEX dwForwardIfIndex;
union {
DWORD dwForwardType; // Old field name uses DWORD type.
MIB_IPFORWARD_TYPE ForwardType; // New field name uses enum type.
};
union {
DWORD dwForwardProto; // Old field name uses DWORD type.
MIB_IPFORWARD_PROTO ForwardProto; // New field name uses enum type.
};
DWORD dwForwardAge;
DWORD dwForwardNextHopAS;
DWORD dwForwardMetric1;
DWORD dwForwardMetric2;
DWORD dwForwardMetric3;
DWORD dwForwardMetric4;
DWORD dwForwardMetric5;
} MIB_IPFORWARDROW, *PMIB_IPFORWARDROW;
So let's, shall we:
#[repr(C)]
#[derive(Debug)]
pub struct IpForwardRow {
dest: u32,
mask: u32,
policy: u32,
next_hop: u32,
if_index: u32,
_other_fields: [u32; 9],
}
We're mostly interested in dest
and if_index
, so I kinda.. slapped all
the other fields in an u32
array, just so that IpForwardRow
has the
correct size.
If we didn't do that, the first entry would match, but none of the others would. Remember: we're in full control of how the data in memory gets interpreted, and that is a terrifying amount of power:
That's why _other_fields
:
Does it work? Let's take a second to use custom_debug_derive
so we can skip
_other_fields
in our Debug
impl:
$ cargo add custom_debug_derive
Adding custom_debug_derive v0.1.7 to dependencies
use custom_debug_derive::Debug;
#[repr(C)]
#[derive(Debug)]
pub struct IpForwardRow {
dest: u32,
mask: u32,
policy: u32,
next_hop: u32,
if_index: u32,
#[debug(skip)]
_other_fields: [u32; 9],
}
$ cargo run --quiet
[src\netinfo.rs:9] table.num_entries = 21
[src\netinfo.rs:10] table.entries() = [
IpForwardRow {
dest: 0,
mask: 0,
policy: 0,
next_hop: 4261521600,
if_index: 5,
},
IpForwardRow {
dest: 127,
mask: 255,
policy: 0,
next_hop: 16777343,
if_index: 1,
},
IpForwardRow {
dest: 16777343,
mask: 4294967295,
policy: 0,
next_hop: 16777343,
if_index: 1,
},
(cut: 18 more entries)
Alright, alright, that sounds good.
Printing IPv4 addresses as u32
isn't the best though.
We've already made an IPv4 address type earlier in sup
, let's add it
to ersatz
in a new module:
// in `src/main.rs`
mod ipv4;
// in `src/ipv4.rs`
use std::fmt;
// these will come in handy later
#[derive(PartialEq, Eq, Clone, Copy)]
pub struct Addr(pub [u8; 4]);
impl fmt::Display for Addr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let [a, b, c, d] = self.0;
write!(f, "{}.{}.{}.{}", a, b, c, d)
}
}
impl fmt::Debug for Addr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self)
}
}
// in `src/netinfo.rs`
use crate::ipv4;
#[repr(C)]
#[derive(Debug)]
pub struct IpForwardRow {
dest: ipv4::Addr,
mask: ipv4::Addr,
policy: u32,
next_hop: ipv4::Addr,
if_index: u32,
#[debug(skip)]
_other_fields: [u32; 9],
}
Let's try it out:
$ cargo run --quiet
[src\netinfo.rs:10] table.num_entries = 21
[src\netinfo.rs:11] table.entries() = [
IpForwardRow {
dest: 0.0.0.0,
mask: 0.0.0.0,
policy: 0,
next_hop: 192.168.1.254,
if_index: 5,
},
IpForwardRow {
dest: 127.0.0.0,
mask: 255.0.0.0,
policy: 0,
next_hop: 127.0.0.1,
if_index: 1,
},
IpForwardRow {
dest: 127.0.0.1,
mask: 255.255.255.255,
policy: 0,
next_hop: 127.0.0.1,
if_index: 1,
},
(cut: 18 more entries)
Fantastic! We can even see the route we need, with dest
0.0.0.0
and if_index
5
.
// in `src/netinfo.rs`
pub fn default_nic_guid() -> Result<String, Error> {
let table = VLS::new(|ptr, size| GetIpForwardTable(ptr, size, false))?;
let entry = table
.entries()
.iter()
.find(|r| r.dest == ipv4::Addr([0, 0, 0, 0]))
.expect("should have default interface");
dbg!(entry);
unimplemented!()
}
$ cargo run --quiet
[src\netinfo.rs:15] entry = IpForwardRow {
dest: 0.0.0.0,
mask: 0.0.0.0,
policy: 0,
next_hop: 192.168.1.254,
if_index: 5,
}
Cool!
Now for the other call.
The declaration for GetInterfaceInfo
is easy enough:
crate::bind! {
library "IPHLPAPI.dll";
fn GetIpForwardTable(table: *mut IpForwardTable, size: *mut u32, order: bool) -> u32;
// new!
fn GetInterfaceInfo(info: *mut IpInterfaceInfo, size: *mut u32) -> u32;
}
So is IP_INTERFACE_INFO
:
typedef struct _IP_INTERFACE_INFO {
LONG NumAdapters;
IP_ADAPTER_INDEX_MAP Adapter[1];
} IP_INTERFACE_INFO,*PIP_INTERFACE_INFO;
Interestingly, the size of Adapter
here is explicitly set to 1
. I guess
those C compilers don't do a lot of checking huh.
#[repr(C)]
#[derive(Debug)]
pub struct IpInterfaceInfo {
num_adapters: u32,
adapter: [IpAdapterIndexMap; 1],
}
Then we get to IP_ADAPTER_INDEX_MAP
:
typedef struct _IP_ADAPTER_INDEX_MAP {
ULONG Index;
WCHAR Name[MAX_ADAPTER_NAME];
} IP_ADAPTER_INDEX_MAP, *PIP_ADAPTER_INDEX_MAP;
Uh oh, WCHAR
. We avoided dealing with UTF-16 back in Part 2, but now there's no way around it.
We know WCHAR
is basically an u16
, so we can define a newtype like so:
use std::fmt;
pub struct IpAdapterName([u16; 128]);
// Implementing `Display` gives us `.to_string()`
impl fmt::Display for IpAdapterName {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// we assume Windows gave us valid UTF-16
let s = String::from_utf16_lossy(&self.0[..]);
// since the name is fixed-size at 128, we want
// to trim any extra null WCHAR(s) at the end.
write!(f, "{}", s.trim_end_matches("\0"))
}
}
// Implementing `Debug` is necessary so we can derive `Debug`
// for `IpAdapterIndexMap`.
impl fmt::Debug for IpAdapterName {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// just use our `Display` implementation with UFCS:
// https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax
fmt::Display::fmt(self, f)
}
}
And shove it in IpAdapterIndexMap
#[repr(C)]
#[derive(Debug)]
pub struct IpAdapterIndexMap {
index: u32,
name: IpAdapterName,
}
With all that done, we can call GetInterfaceInfo
:
// in `src/netinfo.rs`
// in default_nic_guid()
let ifaces = VLS::new(|ptr, size| GetInterfaceInfo(ptr, size))?;
dbg!(ifaces.num_adapters);
$ cargo run --quiet
(...)
[src\netinfo.rs:19] ifaces.num_adapters = 7
Okay! Now for an adapters
method, same technique as IpForwardTable::entries
:
impl IpInterfaceInfo {
fn adapters(&self) -> &[IpAdapterIndexMap] {
unsafe { slice::from_raw_parts(&self.adapter[0], self.num_adapters as usize) }
}
}
..and for us to actually use it:
// in default_nic_guid()
let ifaces = VLS::new(|ptr, size| GetInterfaceInfo(ptr, size))?;
let iface = ifaces
.adapters()
.iter()
.find(|r| r.index == entry.if_index)
.expect("default interface should exist");
dbg!(iface);
$ cargo run --quiet
(...)
[src\netinfo.rs:24] iface = IpAdapterIndexMap {
index: 5,
name: \DEVICE\TCPIP_{0E89380B-814A-48FC-86C4-5C51B8040CB2},
}
Success!
We only want to return the GUID, so let's grab it:
// in default_nic_guid()
let name = iface.name.to_string();
let guid_start = name.find("{").expect("interface name should have a guid");
let guid = &name[guid_start..];
Ok(guid.to_string())
Finally, here's our complete netinfo.rs
, just shy of 100 lines (counting
blank lines):
#![allow(non_snake_case)]
use crate::error::Error;
use crate::ipv4;
use crate::vls::VLS;
use custom_debug_derive::*;
use std::fmt;
use std::slice;
pub fn default_nic_guid() -> Result<String, Error> {
let table = VLS::new(|ptr, size| GetIpForwardTable(ptr, size, false))?;
let entry = table
.entries()
.iter()
.find(|r| r.dest == ipv4::Addr([0, 0, 0, 0]))
.expect("should have default interface");
dbg!(entry);
let ifaces = VLS::new(|ptr, size| GetInterfaceInfo(ptr, size))?;
let iface = ifaces
.adapters()
.iter()
.find(|r| r.index == entry.if_index)
.expect("default interface should exist");
dbg!(iface);
let name = iface.name.to_string();
let guid_start = name.find("{").expect("interface name should have a guid");
let guid = &name[guid_start..];
Ok(guid.to_string())
}
#[repr(C)]
#[derive(Debug)]
pub struct IpForwardTable {
num_entries: u32,
entries: [IpForwardRow; 1],
}
impl IpForwardTable {
fn entries(&self) -> &[IpForwardRow] {
unsafe { slice::from_raw_parts(&self.entries[0], self.num_entries as usize) }
}
}
#[repr(C)]
#[derive(Debug)]
pub struct IpForwardRow {
dest: ipv4::Addr,
mask: ipv4::Addr,
policy: u32,
next_hop: ipv4::Addr,
if_index: u32,
#[debug(skip)]
_other_fields: [u32; 9],
}
#[repr(C)]
#[derive(Debug)]
pub struct IpInterfaceInfo {
num_adapters: u32,
adapter: [IpAdapterIndexMap; 1],
}
impl IpInterfaceInfo {
fn adapters(&self) -> &[IpAdapterIndexMap] {
unsafe { slice::from_raw_parts(&self.adapter[0], self.num_adapters as usize) }
}
}
#[repr(C)]
#[derive(Debug)]
pub struct IpAdapterIndexMap {
index: u32,
name: IpAdapterName,
}
pub struct IpAdapterName([u16; 128]);
impl fmt::Display for IpAdapterName {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = String::from_utf16_lossy(&self.0[..]);
write!(f, "{}", s.trim_end_matches("\0"))
}
}
impl fmt::Debug for IpAdapterName {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
crate::bind! {
library "IPHLPAPI.dll";
fn GetIpForwardTable(table: *mut IpForwardTable, size: *mut u32, order: bool) -> u32;
fn GetInterfaceInfo(info: *mut IpInterfaceInfo, size: *mut u32) -> u32;
}
Here's another article just for you:
Understanding Rust futures by going way too deep
So! Rust futures! Easy peasy lemon squeezy. Until it's not. So let's do the easy thing, and then instead of waiting for the hard thing to sneak up on us, we'll go for it intentionally.
Cool bear's hot tip
That's all-around solid life advice.
Choo choo here comes the easy part 🚂💨
We make a new project: