Aug 20,
2017

WiFi Woes Redux

Another glitch I found out after upgrading to Debian Stretch was that my desktop's wireless NIC has started using the Chinese regulatory domain instead of guessing the correct country code where I'm living or simply using the value that was specified in the WiFi settings. For the record, it's a TP-LINK TL-WN881ND based on the Atheros AR9287 chipset.

I'm not sure if it's a bug in the central regulatory domain agent daemon (CRDA) that is shipped with Debian or something broken in this card's firmware. I'm leaning on the latter as the notebook is working as expected and the chipsets aren't the same. Anyway, because of international regulations about radio interference, it seems I can't nonchalantly set the correct country code of my network card, in fact according to the documentation1:

"The kernel ignores regulatory domains sent to it if it does not expect them."

Yup, it works as advertised.

Trying to launch, for example COUNTRY=US crda from the command line, it fails and return -7 as exit code. By the way, is the same exit code that I've seen in the kernel logs2 where instead the crda is invoked from udev. In some ways the kernel isn't able to find which regulatory domain to set or doesn't agree with the value configured in /etc/default/crda and it applies the value stored in the card's EEPROM.

Now, I think there's a documented procedure to override a regulatory domain that I've found described in the kernel.org's Wiki; it consists of creating and installing local crypto keys to sign a modified regulatory domains DB and it looked quite convoluted and boring. To be honest I didn't read the entire wall of text.

As I will explain later, after peeking in the card's firmware I've found out that it's present the China ISO/IEC 3166 country code on it even if the card was destined for the European market3, so I asked myself if it wouldn't be easier to simply alter two nimble bytes with the desired country code and called it a day. In other words, wouldn't be faster to have a chance to brick the wireless NIC trying to fix an allegedly broken firmware, maybe using a third party unmaintained tool of unknown quality?

Interestingly enough, even if not exactly a case of the Betteridge's law of headlines, the answer in this case is yes, with only a small hitch that I eventually solved.

After scouring the web I indeed found an abandoned utility on code.google.com called iwleeprom that allegedly had the ability to flash the card's firmware and after briefly examining the available documentation, a single terse man page, it really looked of unknown quality as it mentioned only Intel wireless card models.

With iwleeprom I was able to dump the information stored in the card's EEPROM, like capabilities, channels, MAC address and what I was looking for, the regulatory domain. In this particular case it reported the value 0x809c. Ignoring the 0x8000 flag4, the value is 156 in decimal that corresponds to the China country code in the ISO/IEC 3166 list5.

Bingo!

Now it's only a matter of dumping the firmware, modifying it, calculating a new checksum, and writing it back on the card's EEPROM.

Regarding the small hitch I mentioned above, iwleeprom when dumping the firmware it calculates its checksum and reports the value stored on the card: the issue here is the two didn't matched. I suppose I can chalk this one as another thing that doesn't give confidence in the tool.

Undeterred I nonetheless tried to flash a modified firmware with the iwleeprom's calculated checksum and the obvious result was a soft brick. As I feared the wireless NIC refused to work with the applied changes.

Well, after looking at the kernel's ath9k sources, I managed to fix the checksum calculation; fortunately the change was trivial. The patch consists in commenting a single line in the iwleeprom's ath9k_eeprom_crc_calc function:

static bool ath9k_eeprom_crc_calc(struct pcidev *dev, uint16_t *crcp)
{
    uint16_t crc = 0, data;
    int i;

    if (!short_eeprom_size)
        return false;
    printf("Calculating EEPROM CRC");

    for (i=0; i<short_eeprom_size; i+=2) {
        if (2 == i) continue;
        if (!dev->ops->eeprom_read16(dev, short_eeprom_base + i, &data)) {
            printf(" !ERROR!\n");
            return false;
        }
        crc ^= data;
        if (!(i & 0xFFC0)) printf(".");
    }
    //crc ^= 0xFFFF; <-- The offending line
    if (crcp)
        *crcp = crc;
    printf("\n");
    return true;
}

The final exclusive OR with the 0xffff constant isn't required for this chipset.

For the modification part the interesting bits, pun intended, start at location 0x0102 with two bytes for the checksum in little-endian format (in this case 0x9187) followed by the two bytes of the country code at 0x0108 (in this case 0x809c) again in little-endian format:

00000100  d7 02 87 91 04 e0 16 00  9c 80 1f 00 07 00 13 37
00000110  d0 0d 03 03 00 00 00 00  00 00 00 1b 09 00 05 01
00000120  fb fa fa 00 00 00 00 00  00 00 00 00 00 00 00 00
00000130  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00

So after fixing the checksum calculation the plan is:

  1. dumping the firmware with sudo ./iwleeprom -d 0000:03:00.0 -o /tmp/ar9287.bin passing the correct PCI slot address with the -d option

  2. modifying the country code

  3. launching ./iwleeprom -n -i /tmp/ar9287-mod.bin -s to calculate a new checksum passing the -n option to operate only on files

  4. updating the checksum

  5. flashing the modified firmware to the card's EEPROM with sudo ./iwleeprom -d 0000:03:00.0 -i /tmp/ar9287.bin

  6. ...

  7. profit

Yay, success!

I can assure the concerned reader that no wireless NIC was harmed performing the above procedure, in fact the mentioned card is now happily connected with my access point using the expected country regulatory domain.

As a final note, I'm not encouraging the reader to tinker with his WiFi hardware, this is merely a reference to be used by my future self. Messing with radio stuff is serious business, especially with interference, fines aren't a remote possibility.


  1. quoting from the crda(8) man page ↩︎

  2. in the kernel logs the exit code is 249, the decimal value of -7 in two's complement ↩︎

  3. in my opinion they should have put a generic "world" regulatory domain instead of a specific country code ↩︎

  4. according to the Atheros documentation it's the COUNTRY_ERD_FLAG ↩︎

  5. the inquisitive reader can find the complete list on the Wikipedia page ↩︎