Skip to content
Writing

Gaming on the fly with Azure Relay, a Raspberry Pi and Parsec

Waking a desktop at home from anywhere, without a static IP or a single inbound port, using Wake-on-LAN relayed through a Raspberry Pi.

AzureRaspberry PiPythonNetworking

While travelling recently I wanted to reach the desktop at home for gaming, since my laptop is not powerful enough on its own. Two problems stood in the way:

  • Boot the PC remotely
  • Take control of it once it was up

Remote boot

Modern motherboards support Wake-on-LAN. The network card keeps running in a low-power mode even while the machine is shut down, as long as it still has power, and it scans incoming traffic for a magic byte sequence. If an Ethernet frame arrives containing that sequence anywhere in its payload, the card triggers a boot.

That is the mechanism. The remaining problem is getting those bytes onto the home network in the first place.

A Raspberry Pi already running there solved it. It is on 24x7 and sits on the same network as the PC, so if I could reach the Pi remotely, I could have it send the magic packet.

Reaching the Pi

Getting to the Pi is the hard part. A home router leans heavily on NAT and drops unsolicited inbound traffic, and the address the ISP hands out can change at any time unless you pay for a static IP. Even with one, opening a port inbound is its own can of worms.

What is needed is an outbound connection the Pi establishes itself.

There are a few options here. ngrok and Cloudflare Tunnel both do this, though they generally want a custom domain or username and password authentication to be secured properly. Azure Relay is another. It is built on Service Bus and supports Entra ID authentication alongside ordinary connection strings. I already had an Azure subscription, so that is the route I took.

Relay is driven by a CLI tool called azbridge (opens in a new tab). With Relay configured and azbridge running on the Pi, the Pi becomes reachable from outside the network without anything being exposed inbound.

Sending the packet

The last piece is something on the Pi to receive the request and broadcast the magic packet on the local network. A small Flask server does the job, with a /wake endpoint and a /devices endpoint for finding MAC addresses.

With azbridge running on the laptop too, the Relay surfaces as a local address there, so a request to localhost comes out on the Pi:

Shell
curl -X POST 127.1.0.2:1234/wake \
  -H "Content-Type: application/json" \
  -d '{"mac": "REDACTED"}'

The PC’s MAC address comes from Windows network settings.

Remote desktop

The rest is straightforward. Parsec is built for high-bandwidth remote desktop, and it is set to start automatically on boot. My laptop is signed in to the same account, so the machine appears there as soon as it comes up, without the two being on the same local network.

Mermaid
flowchart TB
  subgraph away["Away from home"]
    laptop["Laptop"]
  end

  subgraph cloud["Azure"]
    relay["Relay"]
  end

  subgraph lan["Home LAN: behind NAT"]
    pi["Raspberry Pi"]
    pc["Desktop"]
  end

  laptop -->|"POST /wake"| relay
  pi -.->|"dials out"| relay
  relay -->|"wake request"| pi
  pi ==>|"magic packet"| pc
  laptop <-->|"Parsec"| pc

That is the whole setup. Boot and control a machine at home from anywhere with an internet connection, in a matter of seconds, and with nothing inside the house ever accepting a connection it did not make itself.

Appendix: pywol.py

The Flask server from earlier, in full. Note that it binds to localhost rather than to every interface, so nothing on the home network can reach it directly. Requests only arrive through azbridge, which means Relay handles the authentication before anything gets this far.

Python
import re
import socket
import subprocess

from flask import Flask, jsonify, request

app = Flask(__name__)


def send_magic_packet(mac_address, broadcast_ip="192.168.1.255", port=9):
    mac_address = mac_address.replace(":", "").replace("-", "")
    if len(mac_address) != 12:
        raise ValueError("Invalid MAC address format")

    magic_packet = bytes.fromhex("FF" * 6 + mac_address * 16)

    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
        sock.sendto(magic_packet, (broadcast_ip, port))


@app.route("/wake", methods=["POST"])
def wake():
    data = request.get_json()
    mac = data.get("mac")
    broadcast = data.get("broadcast", "192.168.1.255")
    try:
        send_magic_packet(mac, broadcast)
        return jsonify({"status": "sent", "mac": mac, "broadcast": broadcast})
    except Exception as e:
        return jsonify({"status": "error", "error": str(e)}), 400


@app.route("/devices", methods=["GET"])
def list_devices():
    # Adjust subnet based on your LAN range
    subnet = "192.168.1.0/24"
    try:
        output = subprocess.check_output(["nmap", "-sn", subnet], text=True)
        devices = []

        ip = mac = vendor = None
        for line in output.splitlines():
            if line.startswith("Nmap scan report for"):
                ip = line.split()[-1]
            elif "MAC Address" in line:
                mac_info = re.search(r"MAC Address: ([\w:]+) \((.+)\)", line)
                if mac_info:
                    mac, vendor = mac_info.groups()
                    devices.append({"ip": ip, "mac": mac, "vendor": vendor})
                    ip = mac = vendor = None  # Reset for next device

        return jsonify(devices)
    except subprocess.CalledProcessError as e:
        return jsonify({"status": "error", "output": e.output, "error": str(e)}), 500


if __name__ == "__main__":
    app.run(host="127.0.0.1", port=1234)

Keyboard

Move

j
Scroll down
k
Scroll up
^d
Half page down
^u
Half page up
gg
Top of page
G
Bottom of page

Go

H
Back
L
Forward
gh
Home
gp
Profile

Toggle

t
Terminal
s
Next skin
?
This help

Esc to close