Symptom: Built-in WiFi card is completely dead. rfkill says Hard blocked: yes.
No physical switch exists. Toggling rfkill does nothing. WiFi works fine under Windows.
This guide explains what causes the problem, how it was diagnosed, and how to fix it permanently — on both Gentoo Linux (OpenRC) and Debian 12 (systemd).
Clone the repo and run the installer as root. It detects your init system (OpenRC or systemd) and installs the right files automatically.
git clone https://github.com/huppiflupp/rt2570-rfkill-fix.git
cd rt2570-rfkill-fix
sudo ./install.shNo git? Download all files with curl instead:
mkdir rt2570-rfkill-fix && cd rt2570-rfkill-fix
for f in rt2570-unlock-rfkill rt2570-rfkill.service rt2570-rfkill.rules rt2570-rfkill.start install.sh; do
curl -sO https://raw.githubusercontent.com/huppiflupp/rt2570-rfkill-fix/main/$f
done
chmod +x install.sh
sudo ./install.shTo uninstall:
sudo ./install.sh uninstallThe manual step-by-step instructions are in section 6 if you prefer not to use the installer.
- What is rfkill and what does "hard blocked" mean?
- The hardware
- How the problem was found
- What is actually wrong
- The fix — one command
- Making the fix survive a reboot
- Files in this repository
- Technical deep-dive
Modern computers have a system called rfkill that can turn wireless radios (WiFi, Bluetooth, mobile data) on and off in software. Think of it as a master power switch for radio signals.
rfkill has two kinds of blocks:
| Block type | What sets it | Can software clear it? |
|---|---|---|
| Soft block | The operating system | Yes — rfkill unblock wifi |
| Hard block | A hardware signal from the chip itself | Normally no |
When rfkill list shows:
1: phy0: Wireless LAN
Soft blocked: no
Hard blocked: yes
…it means the WiFi chip itself is telling Linux "I am disabled". Linux trusts this and refuses to use the radio. This is by design — hard blocks are meant to honour physical kill switches (like on old laptops).
The problem here is that the chip is lying. It is asserting the "I am disabled" signal not because of a physical switch, but because a tiny register inside the chip was never set correctly by the Linux driver.
- WiFi chip: Ralink RT2570 (USB, built-in, not a dongle)
- USB IDs: vendor
148f, product2570 - Linux driver:
rt2500usb - Kernel interface name:
wlp0s29f7u6u6(may differ on your system)
The RT2570 is connected internally to a USB port on the motherboard — it looks like an external USB device to the operating system, but there is no dongle.
rfkill list allOutput showed Hard blocked: yes with hard_block_reasons: 0x1, which means
the chip itself is asserting a physical kill signal — not a user setting.
The motherboard's GPIO (general-purpose input/output) pins were checked. These are tiny programmable signals used to control hardware. On this system the CPU chipset is an Intel ICH6, which exposes its GPIO registers at a known address.
All ICH6 GPIO output pins were read and the ones that were LOW (off) were identified as candidates for the kill signal. After toggling them, the rfkill state did not change — so the kill was not coming from the motherboard.
The driver file rt2500usb.c contains this function:
static int rt2500usb_rfkill_poll(struct rt2x00_dev *rt2x00dev)
{
u16 reg;
reg = rt2500usb_register_read(rt2x00dev, MAC_CSR19);
return rt2x00_get_field16(reg, MAC_CSR19_VAL7);
}This function runs periodically to decide whether to report a hard block. It reads a register called MAC_CSR19 from the chip and checks bit 7.
- If bit 7 is
1→ radio is enabled - If bit 7 is
0→ hard block is reported
Using the kernel's debug filesystem (debugfs), the current value of MAC_CSR19
was read directly from the chip:
MAC_CSR19 = 0xFE00
In binary: 1111 1110 0000 0000
- Bits 9–15 (
1111111) = GPIO pins 1–7 are configured as outputs - Bits 0–7 (
00000000) = all output values are zero (LOW)
Bit 7 is 0 → the driver reports "hard blocked". The GPIO7 output is driving
LOW, which the rfkill poll reads as "radio disabled".
The Windows driver sets GPIO7 to output-HIGH during startup. The Linux driver never does this — it sets up other GPIO pins but leaves GPIO7 at zero.
Because Linux never sets bit 7 high, the chip always appears to be in the hardware-kill state.
Inside the RT2570 chip there is a register called MAC_CSR19. It controls eight GPIO (general-purpose input/output) pins on the chip itself.
GPIO7 (bit 7 of MAC_CSR19) is used as the hardware radio kill signal.
When this bit is 0, the Linux driver believes a hardware kill switch is
asserting "radio OFF". When it is 1, the radio is allowed to run.
The Linux rt2500usb driver initialises GPIO7 as an output but never sets
it high. So bit 7 stays at 0 forever, and rfkill permanently reports
Hard blocked: yes.
The Windows driver sets bit 7 high at startup, which is why WiFi works in Windows.
This is a bug in the Linux driver. The fix is to write the correct value
to MAC_CSR19 so that bit 7 is 1.
The register address in the chip is 0x0426. The value we need to write is
0xFE80:
| Bits | Value | Meaning |
|---|---|---|
| 15–9 | 1111111 |
GPIO 1–7 configured as outputs (unchanged) |
| 8 | 0 |
GPIO 0 configured as input (unchanged) |
| 7 | 1 |
GPIO7 output HIGH — releases the kill block |
| 6–0 | 0000000 |
Other GPIO values (unchanged) |
The Linux kernel exposes a debug interface that lets you read and write chip registers directly. To use it, find the correct path on your system:
ls /sys/kernel/debug/ieee80211/You will see something like phy0 or phy1. Use whichever one exists. Then:
# Replace phy0 with the correct name on your system
REG=/sys/kernel/debug/ieee80211/phy0/rt2500usb/register
echo 19 > $REG/csr_offset
echo 0xFE80 > $REG/csr_valueWhy 19? The register address
0x0426minus the chip's base address0x0400equals0x26= 38 bytes = 19 two-byte words. The debug interface uses word numbering, not byte addresses.
After running these two commands, check the result:
rfkill list allYou should see:
1: phy0: Wireless LAN
Soft blocked: no
Hard blocked: no
WiFi is now unblocked. You can bring the interface up normally.
The register is reset every time the system boots, so the fix needs to run automatically at startup. There are two methods depending on your Linux distribution.
OpenRC is the init system used by Gentoo Linux. It has a simple mechanism
called local.d — any script placed in /etc/local.d/ with a .start
extension is run automatically at boot.
Install the unlock script:
cp rt2570-unlock-rfkill /usr/local/sbin/
chmod +x /usr/local/sbin/rt2570-unlock-rfkillInstall the boot hook:
cp rt2570-rfkill.start /etc/local.d/
chmod +x /etc/local.d/rt2570-rfkill.startVerify the local service is active:
rc-update show | grep localYou should see local listed under the default runlevel. If not:
rc-update add local defaultThat is all. On the next boot the script will run automatically, find the debugfs path, write the correct register value, and release the hard block before your network manager tries to connect.
Debian 12 uses systemd as its init system and udev to load kernel
modules. Because the rt2500usb module is loaded by udev rather than by
systemd-modules-load.service, a plain systemd service that runs at
multi-user.target starts before the USB driver has bound to the device.
The fix is a udev rule that triggers the service at exactly the right
moment — when the RT2570 USB device is bound to its driver.
Install the dependency:
sudo apt install python3-usbThe unlock script is written in Python and sends the register write directly
over USB — no CONFIG_RT2X00_DEBUG or debugfs access required. Debian's stock
kernel has that option disabled, so the debugfs path never appears.
Install the unlock script:
sudo cp rt2570-unlock-rfkill /usr/local/sbin/
sudo chmod +x /usr/local/sbin/rt2570-unlock-rfkillInstall the udev rule:
sudo cp rt2570-rfkill.rules /etc/udev/rules.d/
sudo udevadm control --reload-rulesInstall the systemd service:
sudo cp rt2570-rfkill.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable rt2570-rfkill.serviceThe service is enabled so systemd knows about it, but on Debian 12 it is started by the udev rule, not by the normal boot target order. The
enablestep just prevents systemd from complaining about an unknown unit.
Test it immediately without rebooting:
# Simulate what udev does at boot: rebind the USB device
echo 0 | sudo tee /sys/bus/usb/devices/<BUS>-<PORT>/authorized
echo 1 | sudo tee /sys/bus/usb/devices/<BUS>-<PORT>/authorizedReplace <BUS>-<PORT> with your device's path — find it with:
lsusb -d 148f:2570
# e.g. "Bus 001 Device 003" → path is likely 1-6 or similar
ls /sys/bus/usb/devices/ | xargs -I{} sh -c \
'grep -q 148f $(ls /sys/bus/usb/devices/{}/idVendor 2>/dev/null) && echo {}'Or simply reboot to confirm. After the reboot:
sudo systemctl status rt2570-rfkill.service
rfkill list allYou should see the service as active (exited) and Hard blocked: no.
| File | What it does |
|---|---|
rt2570-unlock-rfkill |
The unlock script (Python). Sends a USB vendor control transfer to write 0xFE80 to MAC_CSR19. Does not require CONFIG_RT2X00_DEBUG or debugfs. Requires python3-usb. |
rt2570-rfkill.start |
Boot hook for Gentoo / OpenRC. Place in /etc/local.d/. |
rt2570-rfkill.service |
systemd service unit for Debian 12. Place in /etc/systemd/system/. |
rt2570-rfkill.rules |
udev rule for Debian 12. Triggers the service when the RT2570 USB device binds. Place in /etc/udev/rules.d/. |
install.sh |
Installer script. Detects OpenRC or systemd and installs the correct files. Run as root: sudo ./install.sh. Supports uninstall too. |
This section is for readers who want to understand the full diagnostic process.
The RT2570 is a USB 802.11g WiFi chip from Ralink Technology (now part of MediaTek). It connects to the host computer via USB and communicates using vendor-specific USB control transfers — essentially USB messages with a custom format that the Linux driver knows how to send and receive.
The driver sends USB control transfers with these parameters:
- bmRequestType
0x40(vendor request, device-to-host for writes) - bRequest
6(USB_MULTI_WRITE) for register writes - bRequest
7(USB_MULTI_READ) for register reads - wIndex = register byte address (e.g.
0x0426for MAC_CSR19) - data = 2-byte value to write
The rt2500usb driver registers an rfkill poll function with the kernel:
static int rt2500usb_rfkill_poll(struct rt2x00_dev *rt2x00dev)
{
u16 reg = rt2500usb_register_read(rt2x00dev, MAC_CSR19);
return rt2x00_get_field16(reg, MAC_CSR19_VAL7); // bit 7
}The kernel calls this function periodically. If it returns 0, the kernel
marks the radio as hard-blocked. If it returns 1, the block is cleared.
The rt2x00 driver framework (which rt2500usb is built on) exposes a
debug interface under:
/sys/kernel/debug/ieee80211/<phy>/rt2500usb/register/
Writing a word index to csr_offset and a value to csr_value causes the
kernel to send the corresponding USB control transfer to the chip — exactly
as if the driver itself had written the register.
The word index is computed as:
index = (byte_address - CSR_REG_BASE) / word_size
= (0x0426 - 0x0400) / 2
= 19
On Gentoo (OpenRC), /etc/local.d/ scripts run late in the boot sequence,
after all drivers have been loaded and USB devices have been enumerated. The
timing is naturally correct.
On Debian 12, systemd services with After=systemd-modules-load.service
start before udev has finished binding drivers to USB devices. The
rt2500usb module is loaded by udev on device detection, not by
systemd-modules-load.service. This means the unlock script must not run
until the USB device is actually bound.
Debian's stock 6.1 kernel has CONFIG_RT2X00_DEBUG disabled. The original
fix relied on writing to:
/sys/kernel/debug/ieee80211/phy0/rt2500usb/register/
But that directory is only created by the driver when CONFIG_RT2X00_DEBUG
is compiled in. On Debian 12 the directory is empty and no register files
exist.
The replacement approach sends the identical USB vendor control transfer that
the kernel driver uses internally, directly from Python using python3-usb:
dev.ctrl_transfer(0x40, 0x06, 0, 0x0426, struct.pack('<H', 0xFE80))0x40— bmRequestType: vendor request, host-to-device0x06— bRequest: USB_MULTI_WRITE (rt2500usb vendor command)0— wValue: unused0x0426— wIndex: MAC_CSR19 byte address0xFE80— data: the register value (little-endian)
This does not require detaching the kernel driver and works while rt2500usb
is running normally.
The udev rule rt2570-rfkill.rules uses ACTION=="bind" on the RT2570's
USB IDs to fire the service at the correct moment:
ACTION=="bind", SUBSYSTEM=="usb", ATTR{idVendor}=="148f", ATTR{idProduct}=="2570", \
TAG+="systemd", ENV{SYSTEMD_WANTS}+="rt2570-rfkill.service"
TAG+="systemd" tells udevd to track this device as a systemd device unit.
ENV{SYSTEMD_WANTS} causes systemd to start rt2570-rfkill.service as a
dependency of that device unit — which happens immediately after the driver
probe() function returns and the bind event is sent.
Ndiswrapper loads the Windows driver binary into the Linux kernel to control hardware that has no Linux driver. In this case:
- The RT2570 already has a Linux driver (
rt2500usb) — the driver works, it just initialises one register wrong. - Ndiswrapper is incompatible with modern Linux kernels (4.x and above) on most hardware.
- Ndiswrapper requires NDIS (Windows driver model) support, which has been removed from most distributions.
The correct fix is to patch the one register value, not replace the driver.
Early in the diagnosis, the Intel ICH6 chipset GPIO registers were inspected.
The ICH6 has a GPIO base address at 0x0480 (read from PCI config space of
device 00:1f.0, offset 0x48). The GPIO level register (GP_LVL) at
0x048C showed two output pins held LOW (GPIO19 and GPIO23). These were
toggled HIGH, but the rfkill state did not change. The kill signal originates
inside the RT2570 chip, not from external motherboard GPIO lines.
The system's ACPI DSDT (firmware table that describes hardware to the OS)
defines named fields for the ICH6 GPIO pins (GO10–GO1C, GO20–GO2B)
but never calls any method that uses them. This confirmed that the firmware
does not control the WiFi kill — the Windows driver handles it directly by
writing to the chip register over USB.
Tested on: Gentoo Linux, kernel 6.12.58, Pentium 4 / ICH6, RT2570 USB WiFi (148f:2570), driver rt2500usb 2.3.0
Debian 12 fix (udev rule + Python USB) verified on kernel 6.1.0-42-amd64 / systemd 252