Skip to content

Commit

Permalink
Initial real commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ScottESanDiego committed Mar 27, 2021
1 parent 72e0b17 commit 63d3199
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 0 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,21 @@
# virtwold
Wake-on-LAN for libvirt based VMs

## Introduction
This is a daemon which listens for wake-on-LAN ("WOL") packets, and upon spotting one, tries to start the virtual machine with the associated MAC address.

One use-case (my use case) is to have a gaming VM that doesn't need to be running all the time. NVIDIA Gamestream and Moonlight both have the ability to send WOL packets in an attempt to wake an associated system. For "real" hardware, this works great. Unfortunately, for VMs it doesn't really do anything since there's no physical NIC snooping for the WOL packet. This daemon attempts to solve that.

## Mechanics
When started, this daemon will use `libpcap` to make a listener on the specified network interface, listening for packets that look like they might be wake-on-lan. Due to how `pcap` works, the current filter is for UDP sent to the broadcast address with a length of 144 bytes. This seems to generate very low false-positives, doesn't require the NIC to be in promiscuous mode, and overall seems like a decent filter.

Upon receipt of a (probable) WOL packet, the daemon extracts the first MAC address (WOL packets are supposed to repeat the target machine MAC a few times).

With a MAC address in-hand, the program then connects to the local `libvirt` daemon via `/var/run/libvirt/libvirt-sock`, and gets an XML formatted list of every Virtual Machine configured (yuck). An XML query to list all of the MAC addresses in the configured VMs, and compares that with the MAC from the WOL packet. If a match is found, and the VM isn't already running, the daemon asks `libvirtd` to start the associated VM.

## Usage
Usage is pretty staightforward, as the command only needs one argument: the name of the network interface to listen on. Specify this with the `--interface` flag (e.g., `--interface enp44s0`).

The daemon will keep running until killed with a SIGINT (`^c`).

Because this daemon, and wake-on-LAN, operate by MAC addresses, any VMs that are a candidate to be woken must have a hard-coded MAC in their machine configuration.
157 changes: 157 additions & 0 deletions virtwold.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
//
// Virtual Wake-on-LAN
//
// Listens for a WOL magic packet (UDP), then connects to the local libvirt socket and finds a matching VM
// If a matching VM is found, it is started (if not already running)
//
// Assumes the VM has a static MAC configured
// Assumes libvirtd connection is at /var/run/libvirt/libvirt-sock

package main

import (
"errors"
"flag"
"fmt"
"log"
"net"
"strings"
"time"

"github.com/antchfx/xmlquery"
"github.com/digitalocean/go-libvirt"
"github.com/google/gopacket"
"github.com/google/gopacket/pcap"
)

func main() {
var iface string // Interface we'll listen on
var buffer = int32(1600) // Buffer for packets received
var filter = "udp and broadcast and len = 144" // PCAP filter to catch UDP WOL packets

flag.StringVar(&iface, "interface", "", "Network interface name to listen on")
flag.Parse()

if !deviceExists(iface) {
log.Fatalf("Unable to open device: %s", iface)
}

handler, err := pcap.OpenLive(iface, buffer, false, pcap.BlockForever)
if err != nil {
log.Fatalf("failed to open device: %v", err)
}
defer handler.Close()

if err := handler.SetBPFFilter(filter); err != nil {
log.Fatalf("Something in the BPF went wrong!: %v", err)
}

// Handle every packet received, looping forever
source := gopacket.NewPacketSource(handler, handler.LinkType())
for packet := range source.Packets() {
// Called for each packet received
fmt.Printf("Received WOL packet, ")
mac, err := GrabMACAddr(packet)
if err != nil {
log.Fatalf("Error with packet: %v", err)
}
WakeVirtualMachine(mac)
}
}

// Return the first MAC address seen in the WOL packet
func GrabMACAddr(packet gopacket.Packet) (string, error) {
app := packet.ApplicationLayer()
if app != nil {
payload := app.Payload()
mac := fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", payload[12], payload[13], payload[14], payload[15], payload[16], payload[17])
fmt.Printf("found MAC: %s\n", mac)
return mac, nil
}
return "", errors.New("no MAC found in packet")
}

func WakeVirtualMachine(mac string) bool {
// Connect to the local libvirt socket
c, err := net.DialTimeout("unix", "/var/run/libvirt/libvirt-sock", 2*time.Second)
if err != nil {
log.Fatalf("failed to dial libvirt: %v", err)
}

l := libvirt.New(c)
if err := l.Connect(); err != nil {
log.Fatalf("failed to connect: %v", err)
}

// Get a list of all VMs (aka Domains) configured so we can loop through them
flags := libvirt.ConnectListDomainsActive | libvirt.ConnectListDomainsInactive
domains, _, err := l.ConnectListAllDomains(1, flags)
if err != nil {
log.Fatalf("failed to retrieve domains: %v", err)
}

for _, d := range domains {
//fmt.Printf("%d\t%s\t%x\n", d.ID, d.Name, d.UUID)

// Now we get the XML Description for each domain
xmldesc, err := l.DomainGetXMLDesc(d, 0)
if err != nil {
log.Fatalf("failed retrieving interfaces: %v", err)
}

// Feed the XML output into xmlquery
querydoc, err := xmlquery.Parse(strings.NewReader(xmldesc))
if err != nil {
log.Fatalf("Failed to parse XML: %v", err)
}

// Perform an xmlquery to look for the MAC address in the XML
for _, list := range xmlquery.Find(querydoc, "//domain/devices/interface/mac/@address") {
// Use the strings.EqualFold function to do a case-insensitive comparison of MACs
if strings.EqualFold(list.InnerText(), mac) {
active, err := l.DomainIsActive(d)
if err != nil {
log.Fatalf("failed to check domain active state: %v", err)
}

// Check that the system isn't already active
// Silly that "active" is an int, not a bool
if active != 0 {
fmt.Printf("System %s is already awake\n", d.Name)
} else {
fmt.Printf("Waking system: %s at MAC %s\n", d.Name, mac)
if err := l.DomainCreate(d); err != nil {
log.Fatalf("Failed to start domain: %v", err)
}
}
}
}
}

if err := l.Disconnect(); err != nil {
log.Fatalf("failed to disconnect: %v", err)
}

return true
}

// Check if the network device exists
func deviceExists(interfacename string) bool {
if interfacename == "" {
fmt.Printf("No interface to listen on specified\n\n")
flag.PrintDefaults()
return false
}
devices, err := pcap.FindAllDevs()

if err != nil {
log.Panic(err)
}

for _, device := range devices {
if device.Name == interfacename {
return true
}
}
return false
}

0 comments on commit 63d3199

Please sign in to comment.