Wake-on-LAN with Go
Doing stuff remotely is pretty awesome. Nothing is cooler than banging a bunch of keys on a terminal, to result in the machine 3 feet away roar to life. Yes, it is within my reach, but hey where is the fun in that?!?
The process by which one might remotely power-on a machine (on the same LAN) is referred to as Wake-on-LAN (WOL)
. This process involves sending a specific payload over the local area network that a target machine is connected to. This payload is encoded with the MAC Address of the target machine.
It is also possible to wake machines not directly in your LAN, but this topic is out of the scope of this post. There are many security implications of allowing machines to be woken up via a network broadcast, therefore most machines will expose a setting in their BIOS to enable or disable remote power on.
How does it work?
Assuming that the Wake-on-LAN
settings have been enabled in a machine’s BIOS, and said machine has a MAC address of 00:11:22:33:44:55
, we can power this machine on by sending a Magic Packet
encoded with its MAC address.
These packets are not protocol specific, and therefore can be sent using just about any network protocol. However, these are typically sent as a UDP Packet
.
Show me the Magic!
A Magic Packet is defined as any payload that contains the following pattern:
- 6 bytes of
0xFF
- 16 repetitions of the target’s 48-bit MAC address (16 * 6 bytes)
Note that the relevant part of the Magic Packet is 6 + (16 * 6) = 102 bytes. The payload however can be larger, as long as the above pattern can be found.
Time to get coding. Lets define our packet:
// A MacAddress is 6 bytes in a row
type MacAddress [6]byte
// A MagicPacket is constituted of 6 bytes of 0xFF followed by
// 16 groups of the destination MAC address.
type MagicPacket struct {
header [6]byte
payload [16]MacAddress
}
Initializing a Magic Packet
Since the only real “input” to a MagicPacket is a valid MAC address, it should make sense to have a convenience function to create, initialize and inject the MAC address into a new MagicPacket.
First some globals. Here we call out the delims which might separate the bytes of a MAC Address, and a Regex to match for valid MAC Addresses:
// Define globals for MacAddress parsing
var (
delims = ":-"
re_MAC = regexp.MustCompile(`^([0-9a-fA-F]{2}[`+delims+`]){5}([0-9a-fA-F]{2})$`)
)
To convert a MAC Address string into a net.HardwareAddr
, we can use net.ParseMAC()
. Once we have our set of bytes, all we need to do is fill our MagicPacket.
// This function accepts a MAC Address string, and returns a pointer to
// a MagicPacket object. A Magic Packet is a broadcast frame which
// contains 6 bytes of 0xFF followed by 16 repetitions of a given mac address.
func NewMagicPacket(mac string) (*MagicPacket, error) {
var packet MagicPacket
var macAddr MACAddress
// We only support 6 byte MAC addresses
if !re_MAC.MatchString(mac) {
return nil, errors.New("MAC address " + mac + " is not valid.")
}
hwAddr, err := net.ParseMAC(mac)
if err != nil {
return nil, err
}
// Copy bytes from the returned HardwareAddr -> a fixed size MACAddress
for idx := range macAddr {
macAddr[idx] = hwAddr[idx]
}
// Setup the header which is 6 repetitions of 0xFF
for idx := range packet.header {
packet.header[idx] = 0xFF
}
// Setup the payload which is 16 repetitions of the MAC addr
for idx := range packet.payload {
packet.payload[idx] = macAddr
}
return &packet, nil
}
Awesome, now we can do something like this to get a MagicPacket from a MAC Address:
magicPacket, err := NewMagicPacket("00:11:22:33:44:55")
if err == nil {
fmt.Printf("Magic Packet: %v\n", magicPacket)
}
This produces:
Magic Packet: &{[255 255 255 255 255 255] [[0 17 34 51 68 85] [0 17 34 51 68 85] [0 17 34 51 68 85] [0 17 34 51 68 85] [0 17 34 51 68 85] [0 17 34 51 68 85] [0 17 34 51 68 85] [0 17 34 51 68 85] [0 17 34 51 68 85] [0 17 34 51 68 85] [0 17 34 51 68 85] [0 17 34 51 68 85] [0 17 34 51 68 85] [0 17 34 51 68 85] [0 17 34 51 68 85] [0 17 34 51 68 85]]}
Put that in a pipe!
We now have a nicely formed MagicPacket, all we need to do is send this data out as a UDP broadcast. This basically involves converting the MagicPacket into a []byte which we can then feed into a UDP connection that we will form.
// This function accepts a MAC address string, and s
// Function to send a magic packet to a given mac address
func SendMagicPacket(macAddr string) error {
magicPacket, err := NewMagicPacket(macAddr)
if err != nil {
return err
}
// Fill our byte buffer with the bytes in our MagicPacket
var buf bytes.Buffer
binary.Write(&buf, binary.BigEndian, magicPacket)
// Get a UDPAddr to send the broadcast to
udpAddr, err := net.ResolveUDPAddr("udp", "255.255.255.255:9")
if err != nil {
fmt.Printf("Unable to get a UDP address for %s\n", "255.255.255.255:9")
return err
}
// Open a UDP connection, and defer its cleanup
connection, err := net.DialUDP("udp", nil, udpAddr)
if err != nil {
fmt.Printf("Unable to dial UDP address for %s\n", "255.255.255.255:9")
return err
}
defer connection.Close()
// Write the bytes of the MagicPacket to the connection
bytesWritten, err := connection.Write(buf.Bytes())
if err != nil {
fmt.Printf("Unable to write packet to connection\n")
return err
} else if bytesWritten != 102 {
fmt.Printf("Warning: %d bytes written, %d expected!\n", bytesWritten, 102)
}
return nil
}
Wrapping up
We looked at defining a bunch of bytes to form a MagicPacket, then we went about initializing the packet based on a given input MAC address. Then we explored using the net package
to send a bunch of bytes as a UDP broadcast to wake our target machine.
I hope this was somewhat useful. If this was interesting, and you want to check out a more complete, command line version of this utility - take a look at: sabhiram/go-wol
.