Bit-flip i store nedlastinger: GRO-feil i gatewayens nettverksdriver

Korrupte store nedlastinger bak en prosumer-gateway skyldtes Generic Receive Offload (GRO) i gatewayens nettverksdriver, og ble fikset med ett ethtool-kall. HTTPS-overføringer døde etter 12 til 15 MB med SSL record-feil. HTTP-overføringer fikk riktig størrelse, men feil innhold, med ulik hash for hvert forsøk. Små filer og strupede overføringer gikk fint, og LAN-trafikk var upåvirket. Alle verter bak gatewayen var rammet, så feilen lå i den felles veien ut mot internett.

Symptomet

HTTPS-testen mot et raskt endepunkt feilet hver gang. curl avsluttet med exit 56 og en OpenSSL-feil:

$ curl -4sL -o /dev/null https://speed.example/100mb
curl: (56) OpenSSL SSL_read: error:0A000119:SSL routines::decryption failed or bad record mac


Mot samme endepunkt, strupet til 1 MB/s, kom 30 av 30 MB fram hver gang. Feilen var altså hastighetsavhengig. Over HTTP ga samme feil riktig antall bytes, men ulik hash:

$ for i in 1 2; do curl -4s http://mirror/20mb.bin | md5sum; done
9017c3... -
2bef4a... - # samme fil, samme storrelse, ulik hash


Diagnosen


En strupet nedlasting ga en ren referansekopi uten ekstra utstyr. Byte for byte mot de raske kopiene viste den et systematisk mønster:

  • Enkeltbyte-feil, 1 til 3 bytes per 20 MB.
  • Alltid samme bit som nulles: bit 2, altså 0x04. 0x16 blir 0x12, 0x3d blir 0x39, 0x77 blir 0x73, 0x8e blir 0x8a.
  • Tre av fire treff på samme offset innenfor segmentet. Det tyder på en systematisk defekt bit-bane og ikke på sviktende minne.

Den korrupte HTTP-payloaden kom fram til klienten med gyldig TCP-sjekksum. Sjekksummen var altså regnet ut på nytt etter at biten ble endret. Det peker på en komponent som behandler pakkene og regner ut sjekksummer på veien. En feil på linja ville gitt ugyldig sjekksum. TLS-record-MAC-en oppdager endringen, og forbindelsen brytes. HTTP har ingen slik kontroll, og feilen havner i fila. En gyldig TCP-sjekksum utelukker derfor ikke korrupt payload.

Diagram: bit-flip skjer i gatewayens GRO-koalescering
Payloaden er ren fram til gatewayen. GRO-koalesceringen nuller bit 2 i enkeltbytes ved høy hastighet.

Gatewayen eller linja

Omstart av både gateway og fibersentral endret ingenting. En deterministisk feil som overlever omstart, er enten defekt maskinvare eller en programvarefeil. For å teste linja alene ble en liten enkeltkortmaskin koblet direkte til bridge-porten på fibersentralen, utenom gatewayen, med et testskript som kjørte på egen hånd:

  • Utenom gatewayen, samme fiber og samme offentlige IP: HTTP 0 av 12 korrupte, HTTPS 2 av 2 store nedlastinger komplette.
  • Samme maskin samme kveld, gjennom gatewayen: HTTP korrupt igjen, HTTPS brutt.

Fiber og fibersentral var dermed utelukket, og feilen lå i gatewayen. Test utenom utstyret du mistenker før du kjøper nytt: feilen så ut som defekt maskinvare.

Rotårsaken er GRO

Bit 2-signaturen og hastighetsavhengigheten pekte mot Generic Receive Offload (GRO). GRO slår sammen flere innkommende TCP-segmenter til én stor buffer før de sendes videre opp i stacken. Det sparer CPU ved høy pakkerate. Sammenslåingen og ny utregning av sjekksum skjer i en driverbane som hadde en feil i denne firmwaregrenen. Ved høy pakkerate ble en byte av og til endret under sammenslåingen, og sjekksummen ble regnet ut over den korrupte payloaden. Ved lav pakkerate slås det sammen færre segmenter, og derfor var strupede overføringer alltid rene.

Fiksen er å slå av GRO på WAN-grensesnittet:

ethtool -K eth8 gro off


Etterpå var 6 av 6 store HTTPS-nedlastinger komplette og 0 av 30 HTTP-overføringer korrupte. Rett før var tallene 1 av 3 og 2 av 10. Hastigheten var uendret. To firmwareoppgraderinger hadde ikke fjernet feilen.

Gjør fiksen varig

ethtool-innstillingen forsvinner ved omstart. En systemd-unit setter den etter at nettet er oppe:

[Unit]
Description=Disable GRO on WAN (driver bit-flip workaround)
After=network.target

[Service]
Type=oneshot
ExecStart=/sbin/ethtool -K eth8 gro off

[Install]
WantedBy=multi-user.target


Firmwareoppgraderinger på slike bokser overskriver ofte slike enheter. En cron-jobb på en annen maskin sjekker derfor innstillingen over SSH hvert 30. minutt, setter den på nytt hvis den mangler, og varsler når den har måttet gjøre det:

*/30 * * * * /opt/udmp_gro_watchdog.sh   # sjekk 'ethtool -k eth8 | grep generic-receive-offload'