Fixing Wake-on-LAN on the Linux atlantic Driver
Summary: Diagnose and fix a one-line kernel bug that prevents Wake-on-LAN from S5 (full poweroff) on Aquantia/Marvell AQC-series NICs, then deploy the patched driver via DKMS so it survives kernel updates automatically.
Example Values Used in This Tutorial
| Key | Value |
|---|---|
| Motherboard | ASUS PROART X870E |
| 10GbE NIC | Marvell AQR113C |
| 10GbE driver | atlantic |
| 10GbE interface | enp14s0 |
| 10GbE MAC address | 60:cf:84:8a:7c:3d |
| OS | Linux Mint 22.3 (Ubuntu 24.04 base) |
| Kernel | 6.8.0-111-generic |
| DKMS package name | atlantic-wol-fix |
| DKMS version | 1.0 |
| DKMS source directory | /usr/src/atlantic-wol-fix-1.0/ |
0. Prerequisites
- An Aquantia or Marvell NIC using the
atlanticdriver (AQC107, AQC108, AQC111, AQC112, AQC113, or AQR113C) - A Linux distro based on Ubuntu 22.04 or 24.04, or any distro that supports DKMS
ethtoolinstalled- A second machine on the same network to send the magic packet
- BIOS settings: ErP disabled, PCIe wake enabled, Restore AC Power Loss set to "Power On"
1. The symptoms
The hardware: an ASUS PROART X870E motherboard with two onboard NICs:
- Marvell AQR113C — 10GbE, driver
atlantic, MAC60:cf:84:8a:7c:3d - Intel I226-V — 2.5GbE, driver
igc, MACbc:fc:e7:b2:77:1c
Wake-on-LAN was already working on Windows on the same hardware. On Linux Mint 22.3 (Ubuntu 24.04 base, kernel 6.8.0-111-generic) it claimed to be configured:
sudo ethtool enp14s0 | grep Wake-onCode language: Shell Session (shell)
Supports Wake-on: pg
Wake-on: g
But on sudo poweroff, the magic packet fired from another machine never woke the system. The PSU LED was off, the NIC's link LED was off, and the magic packet was dropping into a black hole.
Things ruled out before touching code:
- Magic packet arrived on the wire (
tcpdumpfrom the firewall confirmed it) - BIOS settings correct: ErP off, Restore AC Power Loss = "Power On", PCIe wake enabled
- No
tlpinstalled (which can disable WOL on shutdown) - ACPI PCI wakeup enabled
- Switch supports lower-speed link maintenance
So it was clearly software. Two relevant GitHub issues existed: Aquantia/AQtion #70 and #54. Both open. Both unanswered. The trail looked cold.
2. The smoking gun
Issue #54 contained a single observation that flipped the whole problem:
"if I have not loaded it [the atlantic driver], then my board can wake on LAN normally. But if I load it and set the interface up, then my board can not wake on LAN after poweroff."
That changes everything. It means:
- The NIC's firmware/PHY natively supports WOL — it's the default state
atlanticis actively breaking it during shutdown — not failing to enable it, actively unsetting it
So the bug isn't somewhere we need to add code. It's somewhere we need to not break what already works. That narrows the investigation to one specific place: the driver's shutdown path.
3. Reading the source
Pull the in-tree atlantic driver from upstream:
mkdir -p ~/atlantic && cd ~/atlantic
git clone --depth 1 --filter=blob:none --sparse https://github.com/torvalds/linux.git
cd linux
git sparse-checkout set drivers/net/ethernet/aquantia/atlanticCode language: Shell Session (shell)
The PCI-layer entry points live in aq_pci_func.c. The shutdown handler is short:
static void aq_pci_shutdown(struct pci_dev *pdev)
{
struct aq_nic_s *self = pci_get_drvdata(pdev);
aq_nic_shutdown(self);
pci_disable_device(pdev);
if (system_state == SYSTEM_POWER_OFF) {
pci_wake_from_d3(pdev, false);
pci_set_power_state(pdev, PCI_D3hot);
}
}Code language: Arduino (arduino)
aq_nic_shutdown() eventually calls aq_nic_set_power(), which programs the NIC firmware to listen for magic packets when WOL is enabled. That part is correct.
But then pci_wake_from_d3(pdev, false) clears the PCI PME_En bit. PME_En is the standard PCIe mechanism for a device in D3 to signal a wake event to the platform. Even if the firmware detects a magic packet, the PCI subsystem won't relay it — because the driver just told PCI "do not wake from D3."
The two halves of the driver are working against each other:
- Firmware: "I'll wake the system if I see a magic packet."
- PCI layer: "Don't wake the system."
Net result: nothing wakes.
For comparison, look at the suspend path (aq_suspend_common), which handles S3 sleep and works for WOL on the same hardware:
static int aq_suspend_common(struct device *dev)
{
struct aq_nic_s *nic = pci_get_drvdata(to_pci_dev(dev));
/* ... */
aq_nic_deinit(nic, !nic->aq_hw->aq_nic_cfg->wol);
aq_nic_set_power(nic);
/* ... */
}Code language: Arduino (arduino)
There is no pci_wake_from_d3(pdev, false) call here. The kernel's PM core manages wake configuration based on device_may_wakeup(). That's why S3 wake works and S5 wake doesn't.
4. The fix
A single-line change — pass the configured WOL state through instead of unconditionally false:
--- a/drivers/net/ethernet/aquantia/atlantic/aq_pci_func.c
+++ b/drivers/net/ethernet/aquantia/atlantic/aq_pci_func.c
@@ -374,7 +374,7 @@ static void aq_pci_shutdown(struct pci_dev *pdev)
pci_disable_device(pdev);
if (system_state == SYSTEM_POWER_OFF) {
- pci_wake_from_d3(pdev, false);
+ pci_wake_from_d3(pdev, self->aq_hw->aq_nic_cfg->wol);
pci_set_power_state(pdev, PCI_D3hot);
}
}Code language: Diff (diff)
The struct path self->aq_hw->aq_nic_cfg->wol is the same one used elsewhere in the driver (e.g. line 1503 of aq_nic.c), so it's a known working accessor. When WOL is enabled, this preserves the PCI PME_En bit so the firmware's detected magic packet can actually generate a wake event. When WOL is disabled, the call becomes pci_wake_from_d3(pdev, false) — identical to the original behavior.
5. Building it as a DKMS module
Building the patched module out-of-tree means no kernel rebuild and automatic re-application across kernel updates.
Source layout: /usr/src/atlantic-wol-fix-1.0/ containing the patched atlantic source, an out-of-tree Makefile, and a dkms.conf.
The out-of-tree Makefile (the in-tree one uses obj-$(CONFIG_AQTION) which won't work standalone):
obj-m += atlantic.o
ccflags-y += -I$(src)
atlantic-objs := aq_main.o aq_nic.o aq_pci_func.o aq_vec.o aq_ring.o \
aq_hw_utils.o aq_ethtool.o aq_drvinfo.o aq_filters.o aq_phy.o \
hw_atl/hw_atl_a0.o hw_atl/hw_atl_b0.o hw_atl/hw_atl_utils.o \
hw_atl/hw_atl_utils_fw2x.o hw_atl/hw_atl_llh.o \
hw_atl2/hw_atl2.o hw_atl2/hw_atl2_utils.o hw_atl2/hw_atl2_utils_fw.o \
hw_atl2/hw_atl2_llh.o macsec/macsec_api.o
atlantic-$(CONFIG_MACSEC) += aq_macsec.o
atlantic-$(CONFIG_PTP_1588_CLOCK) += aq_ptp.o
KDIR ?= /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(CURDIR) modules
clean:
$(MAKE) -C $(KDIR) M=$(CURDIR) cleanCode language: Makefile (makefile)
The dkms.conf:
The crucial detail is DEST_MODULE_LOCATION[0]="/updates/dkms". This places the module under /lib/modules/<kver>/updates/dkms/atlantic.ko, which depmod ranks higher than the in-tree /lib/modules/<kver>/kernel/drivers/.../atlantic.ko. The kernel loads the patched module automatically — no blacklist or modprobe override needed.
Install the build dependencies and register the module with DKMS:
sudo apt install -y dkms build-essential linux-headers-$(uname -r)
sudo dkms add -m atlantic-wol-fix -v 1.0
sudo dkms build -m atlantic-wol-fix -v 1.0
sudo dkms install -m atlantic-wol-fix -v 1.0 --force
sudo depmod -aCode language: Shell Session (shell)
After reboot, confirm the patched module is loaded:
modinfo atlantic | grep filenameCode language: Shell Session (shell)
filename: /lib/modules/6.8.0-111-generic/updates/dkms/atlantic.ko.zst
6. The actual test
Arm WOL and confirm it is set:
sudo ethtool -s enp14s0 wol g
ethtool enp14s0 | grep Wake-onCode language: Shell Session (shell)
Supports Wake-on: pg
Wake-on: g
Power off the machine:
sudo poweroffCode language: Shell Session (shell)
From another machine on the same network, send the magic packet:
wakeonlan 60:cf:84:8a:7c:3dCode language: Shell Session (shell)
Result: the machine powers on. PSU fans spin, the link LED comes back, and SSH is available within ~30 seconds. A feature that should have worked all along, finally working.
7. Making WOL persistent via NetworkManager
The ethtool -s command sets WOL for the current session only. NetworkManager can reset it on profile reactivation or across reboots. To make the setting survive permanently, configure it in the connection profile directly.
Find the active connection profile for the 10GbE interface:
nmcli -f NAME,DEVICE,TYPE connection show --activeCode language: Shell Session (shell)
Set Wake-on-LAN to magic packet mode, substituting the profile name from the NAME column:
sudo nmcli connection modify "<profile-name>" 802-3-ethernet.wake-on-lan magicCode language: Shell Session (shell)
Apply the change without dropping the link:
sudo nmcli device reapply enp14s0Code language: Shell Session (shell)
Note:
reapplyis safe over SSH. If it fails on an older NetworkManager version, useconnection downthenconnection upinstead — but not over SSH unless you have another way back in.
Verify:
nmcli connection show "<profile-name>" | grep -i wake-on-lan
sudo ethtool enp14s0 | grep -E "Wake-on|Link detected"Code language: Shell Session (shell)
Expected output:
802-3-ethernet.wake-on-lan: magic Wake-on: g Link detected: yesWith the atlantic DKMS patch installed and WOL set in the connection profile, no extra systemd service is required to re-arm WOL on each boot.
8. Why this bug survived for years
A few observations on how something this small avoided being caught:
- Ownership transition. The atlantic driver was handed off from Aquantia to Marvell. The original Aquantia GitHub repo is essentially abandoned, and issues sit open for months with no response.
- No regression test. Testing WOL from S5 in CI requires actually powering off and back on real hardware. Most kernel network test suites focus on packet flows, not power management.
- Suspend works, shutdown doesn't. S3 (suspend-to-RAM) WOL works correctly because that path doesn't have the bug. Many users probably never notice — they suspend instead of fully shutting down.
- Confusing user-visible state.
ethtoolhappily reportsWake-on: g. Everything says it's armed. There is no failure indicator at any layer until you actually test it. - The fix looks risky. Passing
woltopci_wake_from_d3()might appear to affect users with WOL disabled. It doesn't — whenwolis0the call is identical to the original. A casual reviewer could miss that immediately.
The diagnostic that made this tractable was Issue #54's observation: "works without the driver loaded, breaks once the driver runs." That single sentence pointed straight at the shutdown teardown path. Without it, finding this would have meant reading a lot more code.
Lesson for future driver bug hunts: read every issue on the relevant repo, even the closed or abandoned ones. Sometimes a user already did the bisection in a comment from two years ago.
9. Patch status and open questions
The one-line fix has been submitted to the Linux netdev mailing list. You can follow the review thread here:
https://lore.kernel.org/netdev/[email protected]/
We're excited to see it land upstream — once accepted, every atlantic user gets the fix automatically in any kernel that includes it, no DKMS required.
Note: Wake-on-LAN behavior on the Intel I226-V (
igcdriver) on this same board is still under investigation. Results and findings will be shared once testing is complete.
Summary
You traced a Wake-on-LAN failure on the atlantic driver to a single line in aq_pci_func.c that unconditionally cleared the PCI PME_En bit on shutdown, counteracting the firmware's own WOL configuration. The one-line fix passes the configured WOL state to pci_wake_from_d3() so the PME_En bit is only cleared when WOL is actually disabled. The patched driver is deployed as a DKMS module, placed under /updates/dkms/ so it takes precedence over the in-tree version and rebuilds automatically across kernel updates.
- Source file changed:
drivers/net/ethernet/aquantia/atlantic/aq_pci_func.c - Key insight: the suspend path (
aq_suspend_common) already handled this correctly — the shutdown path (aq_pci_shutdown) did not - DKMS module placement in
/updates/dkms/is what ensures the patched module wins without any blacklist entry
