Articles

Debugging war story: the mystery of NXDOMAIN

het volgende blogbericht beschrijft een debugavontuur op Cloudflare ‘ s Mesos-gebaseerde cluster. Dit interne cluster wordt voornamelijk gebruikt om logbestandinformatie te verwerken, zodat Cloudflare-klanten analytics hebben en voor onze systemen die aanvallen detecteren en daarop reageren.

het ondervonden probleem had geen enkel effect op onze klanten, maar had wel ingenieurs die hun hoofd krabden…

het probleem

op een gegeven moment in een van onze cluster begonnen we fouten als deze te zien (een NXDOMEIN voor een bestaand domein op onze interne DNS):

lookup some.existing.internal.host on 10.1.0.9:53: no such host

dit leek erg vreemd, omdat het domein inderdaad bestond. Het was een van onze interne domeinen! Ingenieurs hadden gezegd dat ze dit gedrag hadden gezien, dus besloten we dieper te onderzoeken. Queries triggering deze fout waren gevarieerd en varieerden van dynamische SRV-records beheerd door mesos-dns externe domeinen bekeken vanuit het cluster.

onze eerste naïeve poging was om het volgende in een lus uit te voeren:

while true; do dig some.existing.internal.host > /tmp/dig.txt || break; done

dit een tijdje op één server uitvoeren reproduceerde het probleem niet: alle lookups waren succesvol. Toen namen we onze service logs voor een dag en deden een grep voor “no such host” en soortgelijke berichten. Fouten gebeurden sporadisch. Er zaten uren tussen fouten en geen duidelijk patroon dat ons tot een conclusie kon leiden. Ons onderzoek verwierp de mogelijkheid dat de fout lag in Go, die we gebruiken voor veel van onze diensten, omdat fouten waren afkomstig van Java diensten ook.

in het konijnenhol

we draaiden ongebonden op één IP op een paar machines voor onze cluster DNS-resolver. BGP is dan verantwoordelijk voor het aankondigen van interne routes van de machines naar de router. We besloten om te proberen om een patroon te vinden door het verzenden van veel verzoeken van verschillende machines en het opnemen van fouten. Hier is hoe ons load testing programma eruit zag in het begin:

package mainimport ("flag""fmt""net""os""time")func main() {n := flag.String("n", "", "domain to look up")p := flag.Duration("p", time.Millisecond*10, "pause between lookups")flag.Parse()if *n == "" {flag.PrintDefaults()os.Exit(1)}for {_, err := net.LookupHost(*n)if err != nil {fmt.Println("error:", err)}time.Sleep(*p)}}

we draaien net.LookupHost in een lus met kleine pauzes en logfouten; dat is het. Het verpakken van deze in een Docker container en lopen op Marathon was een voor de hand liggende keuze voor ons, want dat is hoe we lopen andere diensten toch. Logs worden verscheept naar Kafka en dan naar Kibana, waar we ze kunnen analyseren. Het draaien van dit programma op 65 machines die elke 50ms opzoeken toont de volgende foutverdeling (van hoog naar laag) over hosts:

we zagen geen sterke correlatie met racks of specifieke machines. Fouten gebeurden op veel hosts, maar niet op alle van hen en in verschillende tijd windows fouten gebeuren op verschillende machines. De tijd op de X-as en het aantal fouten op de Y-as lieten het volgende zien:

om te zien of een bepaalde DNS-recursor gek was geworden, stopten we alle laadgeneratoren op reguliere machines en startten we het gereedschap voor het genereren van belasting op de recursors zelf. Er waren geen fouten in een paar uur, wat erop wees dat ongebonden perfect gezond was.

we begonnen te vermoeden dat pakketverlies het probleem was, maar waarom zou “geen dergelijke host” optreden? Het zou alleen moeten gebeuren als een NXDOMAIN-fout in een DNS-antwoord zit, maar onze theorie was dat antwoorden helemaal niet terugkomen.

de ontbrekende

om de hypothese te testen dat het verliezen van pakketten kan leiden tot een “no such host” fout, hebben we eerst geprobeerd het blokkeren van uitgaand verkeer op poort 53:

sudo iptables -A OUTPUT -p udp --dport 53 -j DROP

in dit geval, dig en soortgelijke tools gewoon time-out, maar geef “no such host”niet terug:

; <<>> DiG 9.9.5-9+deb8u3-Debian <<>> cloudflare.com;; global options: +cmd;; connection timed out; no servers could be reached

Go is een beetje slimmer en vertelt je meer over wat er aan de hand is, maar geeft ook niet “no such host” terug:

error: lookup cloudflare.com on 10.1.0.9:53: write udp 10.1.14.20:47442->10.1.0.9:53: write: operation not permitted

omdat de Linux kernel de afzender vertelt dat het pakketten liet vallen, moesten we de nameserver wijzen naar een zwart gat in het netwerk dat niets doet met pakketten om pakketverlies na te bootsen. Nog steeds geen geluk:

om het netwerk de schuld te blijven geven moesten we onze aannames op een of andere manier ondersteunen, dus hebben we informatie over de timing toegevoegd aan onze lookups:

s := time.Now()_, err := net.LookupHost(*n)e := time.Now().Sub(s).Seconds()if err != nil { log.Printf("error after %.4fs: %s", e, err)} else if e > 1 { log.Printf("success after %.4fs", e)}

om eerlijk te zijn, begonnen we met fouten in de timing en hebben we later succes timing toegevoegd. Fouten gebeurden na 10s, relatief veel succesvolle reacties kwamen na 5s. Het ziet eruit als pakketverlies, maar vertelt ons nog steeds niet waarom “geen dergelijke host” gebeurt.

omdat we nu op een plaats waren waar we wisten welke hosts het meest waarschijnlijk door dit beïnvloed zouden worden, voerden we de volgende twee commando ‘ s parallel uit in twee screen sessies:

while true; do dig cloudflare.com > /tmp/dig.log || break; done; date; sudo killall tcpdumpsudo tcpdump -w /state/wtf.cap port 53

het punt was om een netwerkdump te krijgen met mislukte resolves. Daarin zagen we de volgende queries:

00.00s A cloudflare.com05.00s A cloudflare.com10.00s A cloudflare.com.in.our.internal.domain

twee queries time-out zonder enig antwoord, maar de derde heeft geluk en slaagt. Natuurlijk hebben we geen wolkenflare.com in ons interne domein, dus Unbound rechtmatig geeft NXDOMAIN in antwoord, 10s nadat de lookup werd gestart.

Bingo

laten we eens kijken naar/etc / resolv.conf om meer te begrijpen:

nameserver 10.1.0.9search in.our.internal.domain

met behulp van het zoekwoord kunnen we korte hostnamen gebruiken in plaats van FQDN, waardoor myhost transparant equivalent is aan myhost.in.our.internal.domain.

voor de DNS-resolver betekent dit het volgende: voor elke DNS-query vraag de nameserver 10.1.0.9, als dit mislukt, voeg .in.our.internal.domain toe aan de query en probeer het opnieuw. Het maakt niet uit welke fout optreedt voor de oorspronkelijke DNS-query. Meestal is het NXDOMAIN, maar in ons geval is het een leestijd vanwege pakketverlies.

de volgende gebeurtenissen leken zich voor te moeten doen om een” no such host ” – fout te laten verschijnen:

  1. De oorspronkelijke DNS-verzoek moet worden verloren
  2. De pogingen die worden verzonden na 5 seconden heeft om verloren te gaan
  3. De volgende query voor het interne domein (veroorzaakt door de search optie) heeft om te slagen en terug NXDOMAIN

Aan de andere kant, de naleving van een time-out opgetreden voor DNS-query in plaats van NXDOMAIN, heb je te verliezen vier pakketten verzonden 5 seconden na elkaar (2 voor de oorspronkelijke query en 2 voor de interne versie van uw domein), die een veel kleinere kans. In feite zagen we alleen een NXDOMAIN na 15s en nooit een fout na 20s.

om die aanname te valideren, hebben we een proof-of-concept DNS-server gebouwd die alle aanvragen voor cloudflare.com, maar stuurt een NXDOMEIN voor bestaande domeinen:

package mainimport ("github.com/miekg/dns""log")func main() {server := &dns.Server{Addr: ":53", Net: "udp"}dns.HandleFunc(".", func(w dns.ResponseWriter, r *dns.Msg) {m := &dns.Msg{}m.SetReply(r)for _, q := range r.Question {log.Printf("checking %s", q.Name)if q.Name == "cloudflare.com." {log.Printf("ignoring %s", q.Name)// just ignorereturn}}w.WriteMsg(m)})log.Printf("listening..")if err := server.ListenAndServe(); err != nil {log.Fatalf("error listening: %s", err)}}

uiteindelijk vonden we wat er aan de hand was en hadden we een manier om dat gedrag betrouwbaar te repliceren.

oplossingen

laten we eens nadenken over hoe we onze client kunnen verbeteren om beter om te gaan met deze tijdelijke netwerkproblemen, waardoor het veerkrachtiger wordt. De man page voor resolv.conf vertelt je dat je twee knoppen hebt: de timeout en retries opties. De standaardwaarden zijn respectievelijk 5 en 2.

tenzij u uw DNS-server erg druk houdt, is het zeer onwaarschijnlijk dat het meer dan 1 seconde duurt om te antwoorden. In feite, als je toevallig een netwerk apparaat op de maan, kunt u verwachten dat het antwoord in 3 seconden. Als je nameserver in het volgende rack woont en bereikbaar is via een high-speed netwerk, kun je er veilig vanuit gaan dat als er na 1 seconde geen antwoord is, je DNS server je vraag niet heeft gekregen. Als u wilt minder raar hebben “geen dergelijk domein” fouten die je krabben je hoofd, je kan net zo goed verhogen pogingen. Hoe vaker u het opnieuw probeert met tijdelijk pakketverlies, hoe minder kans op mislukking. Hoe vaker je het opnieuw probeert, hoe groter de kans om sneller te eindigen.

stel je voor dat je echt willekeurig 1% pakketverlies hebt.

  • 2 pogingen, 5S timeout: max 10s wachten voor fout, 0,001% kans op falen
  • 5 pogingen, 1s timeout: max 5s wachten voor fout, 0,000001% kans op falen

in het echte leven zou de distributie anders zijn vanwege het feit dat pakketverlies niet willekeurig is, maar u kunt veel minder verwachten dat DNS antwoordt met dit type verandering.

zoals u weet zijn veel systeembibliotheken die DNS-resolutie bieden, zoals glibc, nscd, systemd-resolved, niet gehard om op het internet te zijn of in een omgeving met pakketverliezen. We hebben de uitdaging van het creëren van een betrouwbare en snelle DNS-resolutie omgeving een aantal keer geconfronteerd als we zijn gegroeid, alleen om later te ontdekken dat de oplossing is niet perfect.

Over naar u

gegeven wat u hebt gelezen in dit artikel over pakketverlies en split-DNS / private-namespace, hoe zou u een snelle en betrouwbare resolutie setup ontwerpen? Welke software zou u gebruiken en waarom? Welke tuning wijzigingen van de standaard configuratie zou u gebruiken?