Nursing Code

Wake on LAN in Rust


I recently invested in a Mac Mini M1 as a secondary machine and have been enjoying using it with VS-Code Remote.

As a developer, the Apple Silicon experience isn't quite ready for full time use while minor incompatibilities are still getting ironed out.

Using VS-Code Remote has allowed me to remain very productive whilst experimenting with the new platform.

I do a lot of frontend development and with all the build tooling that comes along with it, I find my laptop only allows for around 1.5 hours of heavy frontend development when on battery.

Moving a large chunk of that workload to a different machine on my local network hugely extends the time I can spend working outside without having to run extension cords everywhere.

I eventually ran into an issue where I wanted to be able to remotely wake the Mac Mini and started reading up about Wake on LAN and of course, as a nerd, I wanted to build my own tool.

Wake on LAN

Happily, there's a thing called Wake On LAN which allows you to send a message to hardware that supports this feature.

Generally speaking it only working on local networks, which is fine for my purposes.

The way we trigger the behaviour is to send a 'magic packet'.

Magic Packet

The magic packet is a broadcast frame which contains 6 bytes each of which will be the number 255, followed by the MAC address of the target machine, repeated 16 times.

Commonly, 255 would be referred to as FF in hexadecimal, we can convert this to bytes.

Given an example MAC address of 00:1A:BB:CD:EE:61 we can convert that to bytes too.

The final payload we're looking to produce looks like this.

We're going to send this packet via UDP broadcast across the local network.

Building with Rust

The minimal UX I want to provide is a command line application called wol which will take one argument, the MAC address of the target. The MAC address will be supplied in the common 00:11:22:33:44::55 style and will be converted to bytes internally.

That can be achieved fairly simply in Rust with an iterator and using an external library for parsing hex values. We could have done this ourselves, but the hex library helps out with error handling

fn string_mac_to_bytes(incoming: &str) -> Vec<u8> {
    incoming
        .split(":")
        .flat_map(|pair| hex::decode(pair).expect("MAC address contains unexpected characters"))
        .collect()
}

Take the incoming &str and split in on :, then we flat_map using hex::decode. The reason for flat mapping is each time the closure is invoked, it returns a Vec<u8> and we want to finish with a flat vector, not a nested one. Finally collect() runs the conversion and returns the result.

Next, we can build out the packet.

let mac_bytes = string_mac_to_bytes(string_address);
let mut magic_packet: Vec<u8> = vec![0xFF; 6];

for _ in 0..16 {
    magic_packet.extend_from_slice(&mac_bytes[..]);
}

We initialize magic_packet as a mutable Vec<u8> with the 6 entries, each of which is 0xFF (255).

Then using an iterator, we extend the Vec with the bytes of the MAC address, 16 times.

We end up with something like this

[255, 255, 255, 255, 255, 255, 100, 11, 215, 170, 119, 7, 100, 11, 215, 170, 119, 7, 100, 11, 215, 170, 119, 7, 100, 11, 215, 170, 119, 7, 100, 11, 215, 170, 119, 7, 100, 11, 215, 170, 119, 7, 100, 11, 215, 170, 119, 7, 100, 11, 215, 170, 119, 7, 100, 11, 215, 170, 119, 7, 100, 11, 215, 170, 119, 7, 100, 11, 215, 170, 119, 7, 100, 11, 215, 170, 119, 7, 100, 11, 215, 170, 119, 7, 100, 11, 215, 170, 119, 7, 100, 11, 215, 170, 119, 7, 100, 11, 215, 170, 119, 7]

Finally we're ready to send the message.

let socket = UdpSocket::bind("0.0.0.0:0").expect("Couldn't bind to local address");
socket.set_broadcast(true)?;

match socket.send_to(&magic_packet, "255.255.255.255:9") {
    Ok(_) => {
        println!("Magic packet sent to MAC: {}", string_address);
    }
    Err(msg) => {
        println!("Failed to send {:?}", msg);
    }
}

We create a UDP socket bound to the given address, then we set_broadcast(true), which (from the docs)

When enabled, this socket is allowed to send packets to a broadcast address.

Finally we ask the socket to send the &magic_packet to the address 255.255.255.255:9. Port 9 is useful because it targets the discard protocol and we don't expect to get a response anyway.

Then we pattern match on the result and write an appropriate message to the console.

Source

The full source code is hosted here. There's a little more ceremony used to parse the arguments provided by the user, but it's not super relevant to this post.