Skip to content

OliverDrechsler/MADwaS

Repository files navigation

GitHub Action Status Quality Gate Status CodeFactor Known Vulnerabilities

MADwaS

MADwaS stands for Monitor ARP and DNS to Wake a Server. It is a small Python service for home labs and small LAN environments where a server should stay powered off until someone actually needs it.

The script watches for ARP who-has requests and DNS queries for configured hosts. If a sleeping machine is requested, MADwaS checks whether that machine is already reachable. If it is still offline, MADwaS sends a Wake-on-LAN magic packet and can optionally send a notification email.

MADwaS is aimed at setups such as:

  • a NAS that should wake only when a client tries to access it
  • a media server or VDR host (Linux Video Disk Recorder https://tvdr.de) that is not needed 24/7
  • a home lab machine that should become available on demand
  • a Raspberry Pi or other always-on Linux box acting as the wake-up controller

Configured hosts may use IPv4, IPv6, or both in a single monitoring entry.

Table of Contents

Overview

MADwaS is designed for networks where one lightweight, always-on system can observe local traffic and wake a heavier system only when needed.

Typical deployment:

  • A target host such as a NAS, media server, or lab server is asleep most of the time.
  • A Raspberry Pi or another low-power Linux host runs MADwaS continuously.
  • A client on the LAN tries to reach the sleeping host by name or address.
  • MADwaS detects that request and decides whether the host needs to be woken.

This approach is useful when you want:

  • on-demand availability without teaching every client how to send WoL packets
  • lower power usage for machines that are only needed occasionally
  • one central place to manage wake-up behavior and logging
  • dual-stack support for hosts that use both IPv4 and IPv6
  • if you run a local DNS Server (e.g. Pi-Hole) in your local network where you can
    run MADwas (to make use of DNS name query monitoring)

How It Works

  1. MADwaS loads config.yaml or, if it does not exist yet, falls back to config_template.yaml.
  2. It normalizes the configured host data and builds lookup tables for monitored IP addresses and DNS names.
  3. It starts packet sniffing with a narrow BPF filter: arp[6:2] == 1 or udp dst port 53
  4. For each matching packet, MADwaS checks whether the request is relevant. It ignores:
    • traffic generated by the host running MADwaS
    • source IPs listed in blocked_ip
    • requests coming from the monitored host itself
  5. For IPv4 targets, ARP requests and DNS queries can trigger a wake-up path. For IPv6 targets, DNS queries are the trigger and ICMPv6 is used for reachability checks.
  6. If one host entry contains both IPv4 and IPv6 addresses, MADwaS treats them as one logical host. If either configured address answers, the host is considered online.
  7. Valid requests are placed into a worker queue.
  8. The worker thread checks whether the host is already reachable:
    • ICMP echo for IPv4
    • ICMPv6 echo for IPv6
  9. If the host is offline, MADwaS sends the Wake-on-LAN magic packet, optionally sends a notification email, waits for the configured cooldown, and then accepts new wake requests for that host.
  10. Duplicate requests for the same host are suppressed while one wake-up attempt is still being processed.

Code Explanation

The implementation in dns_wol.py is intentionally compact. The main components are:

  • WakeupRequest Represents one queued wake-up job. It carries the triggering source, the target MAC address, the requested DNS name if present, and the normalized set of IP addresses that belong to the host.

  • Configuration Reads the YAML configuration, normalizes IP data, and builds fast lookup maps for packet matching. One configuration entry can describe a single host with multiple IP addresses, including mixed IPv4 and IPv6.

  • __discover_local_ip_addresses() Collects the addresses assigned to the host running MADwaS. This prevents the script from reacting to its own follow-up traffic.

  • __arp_check() and __dns_query_check() Parse and validate sniffed packets. These functions decide whether a packet maps to a monitored host and whether it should create a wake-up request.

  • __add_object_to_thread_queue() Adds new wake-up work only if no other wake-up for the same host is already pending.

  • __check_thread_queue() Runs as a dedicated worker thread. It processes queued requests one by one so that wake-up handling stays simple and predictable.

  • wakeup_monitored_host() Performs the actual decision and action flow: reachability check, WoL send, notification mail, cooldown wait, and pending-state cleanup.

Requirements

MADwaS is targeted at Linux-based always-on nodes in a local network, typically:

  • a Raspberry Pi
  • a small x86 mini PC
  • a home server that stays online all the time
  • a VM or container host with direct visibility into the relevant LAN traffic

You need:

  • Linux with permission to sniff packets and send ICMP and WoL packets
  • Python 3.10 or newer
  • visibility of the ARP or DNS traffic that should trigger wake-ups
  • network reachability to send Wake-on-LAN packets to the target segment
  • optional local SMTP service on localhost if notification emails are wanted

Network visibility notes:

  • On a typical switched LAN, ARP requests are often the most reliable trigger for IPv4 hosts.
  • DNS-based wake-up works best when MADwaS runs on the DNS server, near the DNS path, or on a segment where those queries are visible.
  • IPv6 wake-up handling is DNS-driven. ARP is IPv4-only, so IPv6 hosts are not woken by neighbor discovery traffic.
  • Wi-Fi client isolation, VLAN separation, or routed topologies can prevent the required broadcast or query traffic from being visible.
  • MADwaS is not aimed at arbitrary internet-facing wake-up scenarios. It is intended for trusted local networks where packet visibility is expected.

Configuration

Copy config_template.yaml to config.yaml and adjust it:

version: 1

blocked_ip:
  - "192.168.101.2"
wait_time: 45

monitoring:
  - dns_name: "server.example.local"
    ip: "192.168.101.4"
    ipv6: "2001:db8::50"
    mac: "d4:3d:7e:4a:16:87"
  - dns_name: "server-v6-only.example.local"
    ip: "2001:db8::60"
    mac: "d4:3d:7e:4a:16:87"

from_mail: "madwas@example.local"
to_mail: "admin@example.local"
enable_mail: true

Important fields:

  • blocked_ip: source IPs that must never trigger a wake-up
  • wait_time: cooldown after a successful wake-up attempt
  • monitoring: list of targets to watch
  • dns_name: DNS query name that should trigger wake-up
  • ip: primary target IP address; can be IPv4 or IPv6
  • ipv4: optional explicit IPv4 address for the same host if you prefer not to use ip
  • ipv6: optional IPv6 address for the same host
  • ips: optional list of additional IP addresses for the same host
  • mac: target MAC address used for the magic packet
  • enable_mail: enables or disables notification mail

Example dual-stack host entry:

monitoring:
  - dns_name: "nas.example.local"
    ip: "192.168.101.4"
    ipv6: "2001:db8::50"
    mac: "d4:3d:7e:4a:16:87"

Notes:

  • Use one monitoring entry per host when that host has both IPv4 and IPv6 addresses.
  • ARP matching only applies to the IPv4 address.
  • DNS triggers work for both IPv4 and IPv6 hosts.
  • Reachability checks try all configured addresses for the host and skip Wake-on-LAN if any one of them answers.

Installation

git clone https://github.com/od2017/MADwaS.git
cd MADwaS
python3 -m pip install -r requirements.txt
cp config_template.yaml config.yaml

Then edit config.yaml and start the script with sufficient privileges:

sudo python3 dns_wol.py

Usage

The script reads log_config.yaml for logging configuration and writes logs according to that file. By default the rotating file handler first tries the system log path /var/log/dns_wol.log. If that path is not writable for the current user, it falls back to the project-local dns_wol.log next to dns_wol.py, regardless of the current working directory. The script then listens indefinitely until interrupted.

To force debug logging without editing log_config.yaml, start the script with --debug:

python3 dns_wol.py --debug

This overrides the configured logger and handler levels in memory for that run only.

Systemd Service

The repository includes dns_wol.service as an example unit file. Adjust it for your environment, then install and enable it:

sudo cp dns_wol.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable dns_wol.service
sudo systemctl start dns_wol.service

Testing

Run the unit tests locally with:

python3 -m pytest -q tests/test_dns_wol.py

Run the tests with coverage using:

python3 -m pytest --cov=./ --cov-branch --cov-report=term-missing -q tests/test_dns_wol.py

The test suite currently covers:

  • ignoring locally generated ARP requests
  • ignoring locally generated DNS requests
  • enqueuing valid remote ARP requests
  • enqueuing valid monitored DNS requests
  • deduplicating pending wake-up jobs across IPv4 and IPv6 addresses of the same host
  • collecting local IPv4 and IPv6 addresses correctly

Used by

Contributors

Languages