Letzte Woche gab es einen interessanten Fall von Paketverlust, der sich in kein bisher bekanntes Schema passen wollte. Normalerweise hat man bei Leitungsstörungen (DSL) eine bestimmte Rate an defekten Paketen. Und dann gibt es Bandbreitenbegrenzungen, die schlagartig zu erhöhtem Paketverlust führen. In diesem Fall stieg aber die nutzbare Bandbreite weiter an, nur der Paketverlust steigt massiv.

Überraschung

Auf einer Kundenleitung trat ein erhöhter Paketverlust auf, sobald mehr Last auf die Leitung geschoben wurde.

Ein zentraler Server liefert über diese Leitung an Endgeräte Daten aus. Er misst die Bandbreiten und Paketverluste in aggregierter Form.

packet-loss-lacp-asymmectric-praxis

Man sieht deutlich, dass bis knapp 2Gbps die Paketverluste nahezu konstant sind, was auf sporadische Fehler auf einer ungeschirmten Leitung hindeutet. Die meisten Endgeräte befinden sich hinter DSL Anschlüssen, das passt also ganz gut.

Der Rest der Graphik, der danach folgt, ist allerdings seltsam: Die Kurve knickt ziemlich plötzlich ab und steigt dann fast linear an.

Was kann das sein?

Fehlersuche

Die erste Annahme ist selbstverständlich eine Bandbreitenbegrenzung. Allerdings sollte dann die Bandbreite auch nicht weiter ansteigen.

Testweise habe ich mal eine solche Leitung bei 2Gbps per QoS-Policy limitiert und dann durch gemessen:

packet-loss-bandwidthlimit

Sobald die Ziel-Bandbreite erreicht wird, steigt der Paketverlust an. Aber es geht nicht mehr durch die Leitung durch als konfiguriert wurde.

Eine einfache QoS-Policy ist es also nicht.

Beim genauen Ablaufen des Leitungsweges stellt sich heraus, dass der Kunde folgendes gebaut hat.

packet-loss-netzplan

Interessanterweise treten die Probleme nur auf, wenn das LACP-Bündel aus Leitung 3 und Leitung 4 im Spiel sind. Werden die Strecken 5, 6 oder 7 anders zugeführt, so treten dort bei gleichem Lastverhalten keine Probleme auf.

Dafür gibt es keine gute Erklärung, weil die Bandbreite mit 2x10Gbps mehr als ausreichend ist, um die Daten durch zu schieben. Trotzdem muss der Fehler an einem der an diesem Tunnel beteiligten Switche liegen.

Eine explizite Messung über diese Strecke 2-(3,4) mit 8 Gbps ergab einen Paketverlust von fast 50%. Damit ist die Eingrenzung auf diese Strecke definitiv. Offenbar gibt eine der beiden Leitungen 3 oder 4 unter Last den Geist auf.

Bei einer gründlichen Untersuchung der Konfiguration stellte sich heraus, dass auf dem zentralen, linken Switch eine QoS-Konfiguration vorliegt, die auf Leitung 4 aktiv ist. Auf Leitung 3 fehlt diese QoS-Einstellung. Diese QoS-Policy definiert eine maximale Bandbreite von 1Gbps für den betroffenen Datenverkehr.

Nach Deaktivierung der Policy läuft die explizite Messung problemlos durch.

Erklärung

Das LACP-Bündel verteilt ziemlich gleichmäßig die Datenpakete auf die beiden Leitungen. Die QoS-Policy schmeißt dann alle Pakete, die das Gigabit überschreiten einfach weg.

Das lässt sich einfach modellieren und mit den Messwerten vergleichen:

packet-loss-lacp-asymmectric-theorie

Bei kleinen Bandbreiten ist der Abknick-Effekt sowohl im Modell als auch in der Messung in deutlicher Übereinstimmung.

Aber auch bei 8 Gbps Durchsatz ist der Paketverlust im Modell über 40%, was der realen Messung sehr nahe kommt. Das Modell passt also für kleine und große Werte sehr gut.

Nachdem wieder mal Kritik an xz hoch gekocht ist, habe ich mich zuerst gefreut, dass ich auf zunehmend auf zstd setze. Dann gab es aber Nachfragen, wie sich den zstd so schlagen würde. Besonders im Hinblick auf den Hutter Prize. Also habe ich mal gemessen.

Der Hutter Prize

Der Informationsgehalt natürlicher (englischer) Sprache liegt bei einem Bit pro Zeichen. Soweit behauptet das die Shannon-Theorie.

Aber wie weit kann man wirklich an das Limit kommen?

Der Hutter Prize stellt vereinfacht gesagt folgende Aufgabe:

  • Gegeben ist eine 100MByte Auszug der englischen Wikipedia in XML.
  • Nimmt man 1bit/Zeichen als Limit an, so müsste sich dieser Auszug auf 12,5 MByte packen lassen.
  • Damit die Aktion fair bleibt und das Kompressionsprogramm nicht selbst den ganzen Text enthält, zählt die Größe des Dekomprimierprogrammes mit.
  • Als Qualifikation sind weniger als 16MByte zu schaffen.
  • Es gibt eine Zeitbeschränkungen von 10h.
  • Da es inkrementelle Fortschritte gibt, wird der Preis auch nur inkrementell ausgezahlt.

Es steht natürlich nicht zu erwarten, dass allgemeine Kompressionsprogramme so dicht heran kommen. Stattdessen ist das theoretische Limit nur dann zu erreichen, wenn man den Kontext der englischsprachigen Welt bereits kennt.

Genauer gesagt, sollte die Dekompression eher wie eine freie Rede unter Zuhilfenahme eines Stichpunktzettels (das Komprimat) ablaufen. Es läuft also auf eine Entwicklung einer partiell begabten künstlichen Intelligenz hinaus, die den Kontext es Stichpunktzettels richtig zu deuten weiß.

In einer solchen Liga spielen die handelsüblichen Programme  natürlich nicht.

Messungen

Ich habe das Testfile des Hutter Prizes als Grundlage eigener Messungen genommen. Es entspricht in etwa den Datenbank-Dumps oder den Webseiten typischer CMS-Syteme und damit einem unserer Backup-Szenarien.

Gemessen wurden:

  • Kompressionsrate als Anteil der Dateneinsparung. 20% heißt also die 100MByte Datei ist 20% (20MByte) kleiner, die komprimierte Datei hat also 80Mbyte. Größer sind besser.
  • Die Zeit zum Komprimieren ist vor allem eine praktische Eigenschaft. Schnelle Algorithmen belasten das System weniger. Kleiner ist besser.
  • Speicherbedarf beim Komprimieren ist analog. Insbesondere sollte vermieden werden, dass die Maschine swappen muss. Kleiner ist besser.
  • Die Zeit zum Entpacken ist im Recovery-Fall wichtig. Wenn man an Backups ran muss, ist der Zeitdruck meist schon groß. Wenn man dann noch auf's entpacken warten muss, kann das die Nerven völlig zerrütten. Kleiner ist besser.
  • Speicherbedarf beim Entpacken ist analog. Ausgepackt wird im Notfall auf stark belasteten Maschinen evtl. sogar parallel. Kleiner ist besser.

zstd läuft hier in zwei Modi: Einmal mit extra Dictionary (-D) und einmal normal. Das Dictionary wurde mit einer Auswahl einiger tausend page-Elemente aus der Originaldatei trainiert. Auf welche Art und mit wie vielen Elementen trainiert wird, macht keinen praktischen Unterschied.

Die Kompressionsoptionen laufen von 1 (schneller) bis zu hohen Werten (kompakter). Die Optionen sind nicht vollständig angegeben.

Programm Rate Zeit (comp) MB (comp) Zeit (decomp) MB (decomp)
gzip -1 58% 2,64 3 1,13 3
gzip -9 64% 8,14 5 1,02 3
bzip2 -1 67% 12,24 5 4,11 3
bzip2 -9 71% 12,86 27 4,91 16
zstd -3 64% 1,96 12 0,51 8
zstd -9 68% 9,51 45 0,64 12
zstd -19 72% 78,83 231 0,46 36
zstd -22 75% 126,17 2948 0,62 385
zstd -D 64% 2,12 16 0,48 9
zstd -D -9 68% 11,67 78 0,45 13
zstd -D -19 72% 81,88 426 0,45 37
zstd -D -22 75% 128,38 5508 0,63 386
xz 74% 98,70 376 2,40 36
xz -9 75% 125,93 2688 2,16 260
xz -9e 75% 134,20 2688 2,47 260
Shannon 88%  
sha256   0,44  
sha1   0,21  

Zum Vergleich habe ich die theoretische Grenze und die Verarbeitung der Originaldatei mit zwei Hashverfahren hinzugenommen. Letzteres dient vor allem der Abschätzung, was so beim reinen Durchpumpen der Daten durch die CPU an Arbeit entsteht.

Auswertung

Zuerst zur Kompressionsrate:

image009

Alles über 70% ist großartig, über 60% ist gut. xz und zstd sind also prima, bzip2 kommt nahe ran. Auffällig ist, dass zstd einen großen Bereich an Kompressionsraten abdeckt.

Also schauen wir als nächstes auf die Laufzeiten.

image006

Hier sind die Unterschiede massiv. xz fällt jedoch in allen Varianten aus dem Rahmen.

Also mal nur eine Auswahl der praktisch relevanten Zeiten:

image008

Bei bzip2 überraschen die hohen Entpackzeiten.

Was kann die hohen Packzeiten verursacht haben? In erster Linie könnte die Maschine ins Swappen kommen, wenn der RAM Bedarf die Größe der Testmaschine übersteigt. Das ist tatsächlich ein praktisch relevanter Fall. Also schauen wir mal:

image010

Die Testmaschine hat 2GB RAM, was zum Packen von 100M eigentlich ausreichen sollte. Alles über 1GB ist schlicht indiskutabel. Über Zeiten braucht man da nicht mehr reden.

Auffällig ist, dass zstd mit einem extra Dictionary gar nicht gut fährt. Dieser Modus ist ganz offensichtlich ausschließlich für sehr kleine Dateien wir JSON-Objekte gedacht.

Schauen wir uns noch die relevanten Kandidaten an, die in der großen Übersicht ja gar nicht zu sehen waren. Alles unter 100MByte RAM ist nutzbar:

image007

gzip und bzip2 sind standardmäßig sparsam mit RAM, zstd greift schon gut zu.

Fazit

Ich glaube, dass ich mit zstd gut fahren werde. Die Kompressionsrate ist besser als bei gzip und bzip2, das Einpacken geht schnell und das Auspacken geht doppelt so schnell wie bei den Konkurrenten. Es packt so schnell aus, wie die Prüfsummenberechnung mit sha256 braucht. Das ist beachtlich.

Ich betreibe ja schon etwas länger eine eigne CA. Seit 1998 wechsle ich jährlich die Root-CA Schlüssel, die jeweils zwei Jahre gültig und so überlappend wirksam sind. Und natürlich habe ich dafür einen OCSP Responder selbst schreiben müssen, weil es die Idee erst seit 1999 gibt. Die aktuellen Vorfälle von LetsEncrypt demonstrieren, wie wichtig es ist, sich intensiv mit den Clients zu befassen.

Aufgabestellung

OSCP ist ein Protokoll, bei dem online bei der CA nachgefragt werden kann, ob ein bestimmtes Zertifikat noch gültig ist.

Vor OCSP hatten wir CRLs, also Listen von zurückgerufenen Zertifikaten.

Ich ziehe OSCP der CRL aus mehreren Gründen vor:

  • Eine CRL muss regelmäßig neu erstellt und übertragen werden.
  • Eine CRL muss sämtliche Rückrufe enthalten, während der Gültigkeit der CA-Schlüssel ausgestellt wurden.
  • Damit Clients mit einer (i.d.R. riesigen) CRL etwas anfangen können, müssen sie diese einige Stunden oder Tage cachen, bevor sie eine neue Liste übertragen. In der Zeit bekommen Sie von Zertifikatsrückrufen nichts mit.
  • OCSP ist live, d.h. die Reaktion auf einen Zertifikatsrückruf ist praktisch unmittelbar.
  • OCSP beantwortet nur die konkrete Frage, ob ein bestimmtes Zertifikat einer bestimmten CA jetzt gültig ist.
  • Die (kleine) OCSP-Antwort hat ebenfalls einen (kleineren) Timeout und kann solange gecached werden.
  • OCSP lässt sich seitens des Websservers zentralisieren und mit an die Clients ausliefern. Die Clients müssen dann nicht selbst bei der CA anfragen.

Das Protokoll sieht vor, dass der OCSP Dienst in der Anfrage gesagt bekommt, um welche Seriennummer des Zertifikat es geht und von welcher CA die ausgestellt wurde. Das ist wichtig, wenn man – wie ich – zwei CAs parallel betreibt.

Die Anfrage ist ein ASN.1-Binärblob und kann auf zwei verschiedene Arten gestellt werden:

  • Als POST-Request mit einem binären Blob als Payload.
  • Als GET-Request mit der BASE64-codierten Variante des Blobs.

So weit, so einfach.

Implementierung

Als ich damit anfing, war die einzige verfügbare Software openssl.

Anfangs habe ich diesen als Dienst einfach laufen lassen, aber das ging nicht lange gut.

  • Zum einen kann der Dienst nur mit einer CA arbeiten, ich musste also immer die Neuste nehmen.
  • Anfragen zu Bestandszertifikaten der alten CA wurden mit einem Kenn-ich-nicht-Fehler abgewiesen.
  • Die Software ist im Serverbetrieb instabil, sie dient eigentlich nur dem schnellen Test.

Mit der zunehmenden Validierung von Zertifikatsketten im Browser, wurden OCSP-Fehler zu einem ernsthaften Problem. Also musste es anders gelöst werden.

Als Server dient nun der normale Apache, der ein PHP-Script aufruft, das dann die OSCP Validierung durchführen kann.

<VirtualHost *:8888>
  ServerName ocsp.iks-jena.de
  AcceptPathInfo On
  AllowEncodedSlashes On
  RewriteEngine On
  RewriteRule .* /index.php [L]
</VirtualHost>

In den Zertifikaten wird als OCSP-URL //ocsp.iks-jena.de:8888/ angegeben. Das ist konsistent mit dem Betriebsmodus des openssl Servers.

Anfragen an die URL werden dem PHP-Script vorgeworfen, was für POST-Requests anstandslos klappt.

Problematisch sind die GET-Requests. Die schauen so aus:

"GET //MEMwQTA%2FMD0wOzAJBgUrDgMCGgUABBRFLlbvANe4EFL%2F%2Fg56rSJuL2puEQQULWVsPIwYJRoGyp0azlVnHz5%2FsOoCAgIf HTTP/1.1" 200 2081 "-" "Microsoft-CryptoAPI/6.3"
"GET /MGgwZjA/MD0wOzAJBgUrDgMCGgUABBQmq5X/jwfX2qs7ZfkkUfZHOvrMbAQU6DQvbfzWJ21xpLYn9t9RpDf4gY4CAgICoiMwITAfBgkrBgEFBQcwAQIEEgQQw9lx90TbtLT+H7mqNucYhg== HTTP/1.1" 200 2125 "-" "Mozilla/5.0 (X11; FreeBSD amd64; rv:53.0) Gecko/20100101 Firefox/53.0"

Es gibt aber auch beliebige Mischformen, bei denen der initale Slash doppelt oder einfach ist, die Slashes, Gleichheits- oder Pluszeichen im String literal da stehen oder urlencoded sind.

Es ist dabei nicht einmal sicher gestellt, das eine bestimmte Codierung konsequent eingehalten wird. Proxies neigen bspw. dazu, selbst einige Zeichen zu encodieren. Im Extemfall es sogar auftreten, dass auch normale Buchstaben encodiert werden.

Was tut nun der Apache?

  • Mit AcceptPathInfo regt er sich nicht darüber auf, dass die URL einen nicht existenten Pfad referenziert, denn eine Datei dieses Namens existiert ganz offensichtlich nicht.
  • Mit AllowEncodedSlashes weist der Apache encodierte Slashes nicht mehr ab, die er sonst für einen Angriff hält und die Bearbeitung verweigert.

Im PHP Script kann ich dann schauen, welche CA angefragt wurde und die passende OCSP Antwort für die jeweilige CA bereit stellen.

Das tut einfach schmerzfrei.

Slash me

Nun zur Frage, wie man mit führenden Slashes umgeht. Ab wann ist der Slash Bestandteil der Anfrage und bis wann ist er noch Ergebnis der Anfragekonstruktion der Software.

Die Software bekommt aus dem Zertifikat eine OCSP-URL und muss dort ihren Request anhängen. Einige Software benutzt dazu "%s/%s" und andere benutzt "%s%s". Für beide Varianten gibt es gute Gründe.

In meinem Fall hat die OCSP-URL aber keinen Pfad. Der URI-Standard gibt es aber nicht her, den abschließenden Slash wegzulassen, um das Ende des Hostnamens zu markieren. Also hat meine URL einen finalen syntaktischen Slash, der allerdings semantisch gar nicht existiert.

Eine anders konstruierte OCSP-URL könnte lauten http://ocsp.example:12345/ocsp.asp?req=. Hier soll offensichtlich kein Slash angefügt werden.

Noch anders wäre der Fall bei http://ocsp.example:54321/cgi-bin/ocsp.pl. Hier wird erwartet, dass der Slash literal (und nicht encoded) eingefügt wird, weil sonst das Script nicht gefunden werden kann.

Das eigentliche Problem liegt also darin begründet, dass der OCSP Standard leichtsinnig eine Codierung gewählt hat, die reservierte Zeichen im benutzten Protokoll generieren kann. Kurz gesagt: Die Norm ist fehlerhaft.

Aber sie ist draußen und wir müssen damit leben.

Die entscheidende Frage ist also, wie man entscheiden kann, wo die realen Daten anfangen. Dazu muss ins Protokoll geschaut werden.

Es liegt immer eine ASN.1 Seqzenz vor:

OCSPRequest     ::=     SEQUENCE {
 tbsRequest                  TBSRequest,
 optionalSignature   [0]     EXPLICIT Signature OPTIONAL
} 
TBSRequest      ::=     SEQUENCE {
 version             [0]     EXPLICIT Version DEFAULT v1,
 requestorName       [1]     EXPLICIT GeneralName OPTIONAL,
 requestList                 SEQUENCE OF Request,
 requestExtensions   [2]     EXPLICIT Extensions OPTIONAL
}

Das erste ASN.1 Byte für diesen Anfang ist also 0x30 (SEQUENCE). Die Dekodierung eines Slashes aus Base64 heraus ist mindestens 0xfc.

Damit ist es unmöglich, dass ein Slash den Anfang eines base64 kodierten OSCP-Requests darstellen kann. Im Gegenteil, das erste Zeichen einer ASN.1 Sequenz in Base64 ist zwingend ein M (genau genommen kann der Base64 Teil nur mit MA bis MP anfangen.

Die praktische Lösung des Problems ist also einfach:

  • Man dekodiert alles, was nach URLEncode aussieht.
  • Man entfernt alle führenden Slashes.
  • Man dekodiert den Rest mit Base64.

Fazit

Alles, was man tun muss, ist ins Logfile schauen. Das Internet existiert nur wegen eines Grundsatzes: Be conservative in what you do, be liberal in what you accept from others.

Ich hatte gerade im Lab mit DirectAccess gespielt. Da bietet es sich an, auch mal zu schauen, was passiert, wenn DNSSEC dazu kommt. Das Ergebnis ist eine faustdicke Überraschung.

Neue Zone

Als ersten lege ich eine neue Zone sec.adatum.com mit DNSSEC an. Das ist einfach: Einfach an der Zone im Kontextmenü DNSSEC auswählen. Es bietet sich an, die Trust Anchors innerhalb der Domain gleich mit zu verteilen.

Diese Zone bekommt einen WWW Eintrag, der auf den Webserver im Lab verweist. Und das tut dann auch.

Die Antwort besteht aus drei Teilen:

PS dns> Resolve-DnsName -DnssecOk -Name www.sec.adatum.com

Name                           Type   TTL   Section    NameHost
----                           ----   ---   -------    --------
www.sec.adatum.com             CNAME  3600  Answer     www.adatum.com

Name        : www.sec.adatum.com
QueryType   : RRSIG
TTL         : 3600
Section     : Answer
TypeCovered : CNAME
Algorithm   : 8
LabelCount  : 4
OriginalTtl : 3600
Expiration  : 5/19/2017 3:41:34 PM
Signed      : 5/19/2017 8:41:34 AM
Signer      : sec.adatum.com
Signature   : {39, 75, 194, 190...}

Name       : www.adatum.com
QueryType  : A
TTL        : 3600
Section    : Answer
IP4Address : 172.16.0.10

Auch auf dem Client, der irgendwo remote steht, tut es anstandslos. Hier gibt es aber nur zwei Teile in der Antwort:

PS client>  Resolve-DnsName -DnssecOk -Name www.sec.adatum.com

Name                           Type   TTL   Section    NameHost
----                           ----   ---   -------    --------
www.sec.adatum.com             CNAME  600   Answer     www.adatum.com

Name       : www.adatum.com
QueryType  : AAAA
TTL        : 600
Section    : Answer
IP6Address : fd68:d6bf:56b6:7777::ac10:a

Man sieht schön die Wirkung von DNS64 und das Ergebnis überzeugt:

Pinging www.adatum.com [fd68:d6bf:56b6:7777::ac10:a] with 32 bytes of data:
Reply from fd68:d6bf:56b6:7777::ac10:a: time=1ms
Reply from fd68:d6bf:56b6:7777::ac10:a: time=1ms

Allerdings wurde keine DNSSEC Validierung durchgeführt, wie man an dem Fehlen der RRSIG Antwort bemerkt haben könnte.

Validierung

Die Validierung von DNSSEC kann man per Gruppenrichtlinie erzwingen.

Technisch gesehen wird hier per Policy Based DNS, die Validierung pro Domain eingefordert. Die Trust-Anchors wurden ja schon verteilt.

Nach Verteilung der Gruppenrichtlinie auf den Client stellt sich die Situation ganz anders dar:

PS client>  Resolve-DnsName -DnssecOk -Name www.sec.adatum.com
Resolve-DnsName : www.sec.adatum.com : Unsecured DNS packet

Konsequenterweise funktioniert dann auch nichts mehr:

C:\> ping www.sec.adatum.com
Ping request could not find host www.sec.adatum.com. Please check the name and try again.

DNSSEC hat also erfolgreich DNS64 erkannt und die Modifikation verhindert!

Und IPv6?

Nehmen wir einen IPv6 Host im lokalen Netz an:

PS dns> Resolve-DnsName -DnssecOk -Name test.sec.adatum.com -type aaaa

Name                                           Type   TTL   Section    IPAddress
----                                           ----   ---   -------    ---------
test.sec.adatum.com                            AAAA   3600  Answer     2001:db8::1

Name        : test.sec.adatum.com
QueryType   : RRSIG
TTL         : 3600
Section     : Answer
TypeCovered : AAAA
Algorithm   : 8
LabelCount  : 4
OriginalTtl : 3600
Expiration  : 5/19/2017 4:02:43 PM
Signed      : 5/19/2017 9:02:43 AM
Signer      : sec.adatum.com
Signature   : {162, 99, 191, 91...}

Die Antwort besteht aus zwei Teilen, dem AAAA und dem RRSIG. Das tut. Nun zur Sicherheit noch einen nicht existierenden Namen.

PS dns> Resolve-DnsName -DnssecOk -Name test3.sec.adatum.com
Resolve-DnsName : test3.sec.adatum.com : DNS name does not exist

Aber tut es auch auf dem Client? Schließlich muss ja keine Umschreibung durch DNS64 stattfinden.

PS client>  Resolve-DnsName -DnssecOk -Name test.sec.adatum.com

Huch? Das tut nicht? Es fehlt auch komplett eine Antwort! Ist das denn von DNSSEC bestätigt?

PS client>  Resolve-DnsName -DnssecOk -Name test3.sec.adatum.com
Resolve-DnsName : test3.sec.adatum.com : Unsecured DNS packet

Nein, der DirectAccess-Resolver macht DNSSEC kaputt.

Richtig kaputt.

Über DirectAccess hatte ich schon einiges erzählt, und natürlich auch ausprobiert. Was ich aber noch nie angesehen habe, war DirectAccess ohne IPv6 im inneren oder äußeren Netz, also total legacy. In der Schulung habe ich die Gelegenheit dazu und möchte die Ergebnisse nicht vorenthalten.

IPv4 only Lab

Nachdem der DirectAccess Client in die weite Welt verschwunden ist, und er voller Verzweiflung nach Hause telefoniert hat, bietet sich folgendes Bild.

C:\> ipconfig
Ethernet adapter Ethernet 2:
   Connection-specific DNS Suffix  . :
   Link-local IPv6 Address . . . . . : fe80::a19f:cc85:1320:8c4%5
   IPv4 Address. . . . . . . . . . . : 131.107.0.2
   Subnet Mask . . . . . . . . . . . : 255.255.0.0
   Default Gateway . . . . . . . . . :
Tunnel adapter iphttpsinterface:
   Connection-specific DNS Suffix  . :
   IPv6 Address. . . . . . . . . . . : 2002:836b:c8:1000:1934:30a2:9e84:cdf4
   Temporary IPv6 Address. . . . . . : 2002:836b:c8:1000:f037:3b8d:cabc:bfcb
   Link-local IPv6 Address . . . . . : fe80::1934:30a2:9e84:cdf4%24
   Default Gateway . . . . . . . . . :

Es gibt also ein Interface im Internet mit öffentlichen Adressen ohne IPv6 Versorgung. Und dann gibt es den Tunnel nach Hause. Da sowohl Client als auch der DirectAccess Server öffentliche IPs haben, wurde der Tunnel mit 6to4-Adressen aufgebaut.

In den Schulungsunterlagen steht Notice the IP address for Tunnel Adapter is IPHTTPSInterface starting with 2002. This is an IP-HTTPS address. (Fehler sind so übernommen)

Nein, Microsoft! Es ist eine 6to4 Adresse. Offenbar habt Ihr wirklich keine praktische Erfahrungen mit IPv6.

Die zugehörige Routingtabelle zeigt:

c:\> route print -6
===========================================================================
Interface List
  5...00 15 5d 64 4d 4d ......Microsoft Hyper-V Network Adapter #2
  1...........................Software Loopback Interface 1
 24...00 00 00 00 00 00 00 e0 iphttpsinterface
===========================================================================
IPv6 Route Table
===========================================================================
Active Routes:
 If Metric Network Destination      Gateway
  1    331 ::1/128                  On-link
 24   4171 2002::/16                fe80::d92f:34d4:3add:e715
 24    331 2002:836b:c8::/48        fe80::d92f:34d4:3add:e715
 24    331 2002:836b:c8::/64        fe80::d92f:34d4:3add:e715
 24    331 2002:836b:c8:1::/64      fe80::d92f:34d4:3add:e715
 24    331 2002:836b:c8:5::/64      fe80::d92f:34d4:3add:e715
 24    331 2002:836b:c8:1000::/64   On-link
 24    331 2002:836b:c8:1000:1934:30a2:9e84:cdf4/128
                                    On-link
 24    331 2002:836b:c8:1000:f037:3b8d:cabc:bfcb/128
                                    On-link
 24    331 fd68:d6bf:56b6:7777::/96 fe80::d92f:34d4:3add:e715
  5    271 fe80::/64                On-link
 24    331 fe80::/64                On-link
 24    331 fe80::1934:30a2:9e84:cdf4/128
                                    On-link
  5    271 fe80::a19f:cc85:1320:8c4/128
                                    On-link
  1    331 ff00::/8                 On-link
  5    271 ff00::/8                 On-link
 24    331 ff00::/8                 On-link
===========================================================================
Persistent Routes:
  None

Das Routingziel ist eine Link-Local-Adresse, wie es sich für IPv6 gehört. Prima! Und die Adresse ist auch über den Tunnel erreichbar.

C:\>netsh int ipv6 sho nei 24
Internet Address                              Physical Address   Type
--------------------------------------------  -----------------  -----------
2002:836b:c8:1000:d92f:34d4:3add:e715                            Reachable (Router)
fe80::d92f:34d4:3add:e715                                        Reachable (Router)

Spielt man nun mit Browser und Explorer im Netz rum, gibt es offene Verbindungen:

C:\> netstat -n
Active Connections
  Proto  Local Address          Foreign Address        State
  TCP    131.107.0.2:49782      131.107.0.200:443      ESTABLISHED
  TCP    [2002:836b:c8:1000:f037:3b8d:cabc:bfcb]:49785  [fd68:d6bf:56b6:7777::ac10:c8]:80  ESTABLISHED
  TCP    [2002:836b:c8:1000:f037:3b8d:cabc:bfcb]:61893  [fd68:d6bf:56b6:7777::ac10:b]:80  ESTABLISHED

Die Verbindungen sind also offenbar über IPv6 zu einem ULA-Ziel (bäh!).

Das DirectAccess-Gateway macht noch NAT64, bettet also die IPv4 Adressen der LAN-Geräte ins IPv6 ein. Sieht man deutlich.

Dazu muss das Gateway offenbar auch DNS64 machen, um die DNS Antworten umzubiegen. Das sieht man leicht:

c:\>ipconfig /displaydns
Windows IP Configuration
    directaccess-webprobehost.adatum.com
    ----------------------------------------
    Record Name . . . . . : directaccess-WebProbeHost.Adatum.com
    Record Type . . . . . : 28
    Time To Live  . . . . : 213
    Data Length . . . . . : 16
    Section . . . . . . . : Answer
    AAAA Record . . . . . : fd68:d6bf:56b6:7777::ac10:c8

Aber warum macht der Client das? Der hat doch einen ganz anderen DNS Server?

C:\>netsh name show eff
DNS Effective Name Resolution Policy Table Settings

Settings for .Adatum.com
----------------------------------------------------------------------
DirectAccess (Certification Authority)  :
DirectAccess (IPsec)                    : disabled
DirectAccess (DNS Servers)              : 2002:836b:c8:3333::1
DirectAccess (Proxy Settings)           : Bypass Proxy

Settings for DirectAccess-NLS.Adatum.com
----------------------------------------------------------------------
DirectAccess (Certification Authority)  :
DirectAccess (IPsec)                    : disabled
DirectAccess (DNS Servers)              :
DirectAccess (Proxy Settings)           : Use default browser settings

Der Client hat also eine Policy für die Namensauflösung, die bei bestimmten Domains einen anderen DNS Server befragt. Genau, das Gateway.

C:\> ping lon-svr1.adatum.com
Pinging lon-svr1.adatum.com [fd68:d6bf:56b6:7777::ac10:b] with 32 bytes of data:
Reply from fd68:d6bf:56b6:7777::ac10:b: time=4ms
Reply from fd68:d6bf:56b6:7777::ac10:b: time=10ms
Reply from fd68:d6bf:56b6:7777::ac10:b: time=1ms
Ping statistics for fd68:d6bf:56b6:7777::ac10:b:
    Packets: Sent = 3, Received = 3, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 1ms, Maximum = 10ms, Average = 5ms

Allerdings funktioniert das nur, wenn diese Policy Regeln auch von der Applikation berücksichtigt werden:

C:\> nslookup lon-svr1.adatum.com
Server:  UnKnown
Address:  131.107.0.100

DNS request timed out.
    timeout was 2 seconds.
*** Request to UnKnown timed-out

Fragt man den richtigen Server, gibt es ganz andere Antworten:

C:\> nslookup lon-svr1.adatum.com 2002:836b:c8:3333::1
Server:  UnKnown
Address:  2002:836b:c8:3333::1

Non-authoritative answer:
Name:    lon-svr1.adatum.com
Addresses:  fd68:d6bf:56b6:7777::ac10:b
          172.16.0.11

All das ist mir all die Jahre verborgen geblieben, weil ich IPv6 ausgerollt hatte.

Ich vermisse nichts.

Was IPv6 ändert

Wird auf dem Außeninterface natives IPv6 eingesetzt, verschwinden sofort die 2002-er 6to4 Adressen. Dazu ist anzumerken:

  • Das im Lab die Kommunikation geklappt hatte, liegt daran, dass Microsoft das 2002:836b:c8:1000::/64 auf dem Interface betreibt.
  • Um mit 6to4 Adressen zu kommunizieren, muss man sich auf externe 6to4 Gateways verlassen. Dies kommt ins Spiel, weil der DNS Server 2002:836b:c8:3333::1 angesprochen wird.
  • Da beide Hosts über den IPv4-HTTPS-Tunnel IPv6 im externen Routing komplett umgehen, benötigen sie keine 6to4 Router im Internet.
  • Da beide Server extern öffentliche IPv4 Adressen haben, wird überhaupt 6to4 benutzt. Steht ein Gerät hinter NAT wird es auf Teredo zurück fallen.
  • Teredo funktioniert nicht mit allen NAT-Typen, speziell gibt es Probleme, wenn die öffentliche NAT-IP nicht stabil ist (z.B. bei Carrier Grade NAT oder Large Scale NAT)
  • Mit RFC 6540 ist IPv6 auf den Außeninterfaces Pflicht für die ISPs.

Wird IPv6 im LAN eingesetzt, verschwindet sofort das fc00::/7 ULA Netz. Stattdessen bekommen die Clients die gleiche IP, wie sie auch im LAN hätten. Das hat folgende Implikationen:

  • Der Client ist intern, wie extern unter der gleichen IP ansprechbar.
  • Die Fernwartung wird damit im Unternehmen unabhängig vom Aufenthaltsort des Clients.
  • Da Server und Clients IPv6 einsetzen, entfällt DNS64 und NAT64.
  • Applikationen können mit allen benötigten Ports Verbindungen in alle benötigten Richtungen ausführen. Es ist nicht notwendig im DirectAccess-Server NAT-Helper pro Protokoll zu haben.
  • Auf diese Weise funktionieren Protokolle wie Telefonie, Videokonferenzen, FTP, etc. pp. einfach auch extern.
  • Fremdapplikationen, die ihre eigene Namensauflösung fahren, funktionieren problemlos, weil sich nur das Routing zum DNS Server ändert. Es ist nicht länger notwendig, sich Domainabhängig an unterschiedliche DNS Server zu wenden.

Man sollte sich also wirklich überlegen, ob man weiter auf IPv6 verzichten will.

Aktuell lerne ich, was so alles bei Microsoft seit Windows NT neues dazu gekommen ist. Es ist der Updatekurs für Server 2016. Aktuell sind die Neuerungen im DNS dran. Wer bind kennt, weiss, wo die Worte her kommen. Allerdings ist das Zonenkonzept komplett unbenutzbar.

Aufgabe

Erstellen Sie für eine existierende Domain Einträge, die von einem anderen Netz aus andere Adressen zurück liefern.

Diese Aufgabe ist eine klassische Split-Brain Situation und kommt in der Praxis immer dann vor, wenn interne Server nach extern über NAT sichtbar gemacht werden sollen. Bei IPv6 gäbe es das Problem nicht, da würde eine Firewallfreigabe reichen.

Zuerst schauen wir von einem externes Gerät auf die DNS Auflösung:

PS extern> nslookup www.adatum.com
Address:  172.16.10.10

Non-authoritative answer:
Name:    www.adatum.com
Address:  172.16.0.10

Und nun ändern wir das auf dem DNS Server. Zuerst wird festgelegt, dass es einen neuen Scope (Bind sagt dazu View) überhaupt gibt.

PS dns> Add-DnsServerZoneScope -ZoneName adatum.com  -Name Trey

Innerhalb dieses Scopes soll es einen anderen DNS-Eintrag geben als in der Originalzone.

PS dns> Add-DnsServerResourceRecordA -ZoneName adatum.com -Name www -IPv4Address 1.2.3.4 -ZoneScope Trey

Als nächstes wird ein IP-Netz als Auswahlkriterium festgelegt.

 PS dns> Add-DnsServerClientSubnet -Name TreyNet -IPv4Subnet 172.16.10.0/24

Beides zusammen definiert nun das anzuwendende Abfrageregelwerk.

PS dns> Add-DnsServerQueryResolutionPolicy -Name SplitBrain -Action ALLOW -ClientSubnet "eq,TreyNet" -ZoneScope "Trey,1" -ZoneName adatum.com

Fragt man nun von extern den Namen erneut ab, klappt es wie gewünscht.

PS ext> Clear-DnsServerCache
Confirm
This will delete all the cached records on the server and might impact performance, do you want to continue?
[Y] Yes  [N] No  [S] Suspend  [?] Help (default is "Y"): y
PS ext> Clear-DnsClientCache
PS ext> nslookup www.adatum.com
Server:  UnKnown
Address:  172.16.10.10

Non-authoritative answer:
Name:    www.adatum.com
Address:  1.2.3.4

Prima! Übung beendet.

Was wirklich passiert

Fragt man von extern mal andere Namen der Zone ab, so gibt es ausschließlich Fehlermeldungen:

*** UnKnown can't find lon-srv3.adatum.com: Non-existent domain

Ganz offenbar hat Microsoft mehr gemacht, als diesen einen Record zu überschreiben. Aber was genau?

Zuerst einmal fällt auf, dass es keinerlei Möglichkeit gibt, die Einträge des Scopes in der graphischen Oberfläche zu sehen. Dort schaut für den Administrator alles aus wie immer.

ms-dns-scope1

Man kann klicken, wo auch immer man will. Die eingerichteten Scopes und Policies bleiben unsichtbar. Das wird die Fehlersuche oder gar den operativen Betrieb sicher deutlich erschweren.

Also gehen wir per Powershell auf die Suche:

PS dns> Get-DnsServerZoneScope -ZoneName adatum.com
ZoneScope            FileName            
---------            --------            
Adatum.com                               
Trey                                    

Man muss also die Zone kennen, für die man die Scopes abfragen kann. Eine generelle Liste aller Scopes muss man sich zusammen bauen.

PS dns> Get-DnsServerQueryResolutionPolicy

Überraschung! Die generelle Liste aller Policies ist leer. Auch hier muss man wieder konkret fragen.

PS dns> Get-DnsServerQueryResolutionPolicy -ZoneName adatum.com
Name       ProcessingOrder IsEnabled Action
----       --------------- --------- ------
SplitBrain 1               True      Allow

Und was steht nun in der Policy genau drin? Das ist leider nicht auf diese Weise herauszubekommen!

Aber man kann wenigstens seine Kriterien auflisten.

PS dns> Get-DnsServerClientSubnet
Name    IPV4Subnet       IPV6Subnet
----    ----------       ----------
TreyNet {172.16.10.0/24}          

Nun steht der fehlersuchende Administrator belämmert da, weil er nicht heraus bekommt, was die Policy genau tut.

Aber man kann ja mal raten:

PS dns> Get-DnsServerResourceRecord -ZoneName adatum.com
HostName                  RecordType Type       Timestamp            TimeToLive      RecordData                      
--------                  ---------- ----       ---------            ----------      ----------                      
@                         A          1          5/17/2017 1:00:00 AM 00:10:00        172.16.0.10                     
@                         NS         2          0                    01:00:00        lon-dc1.adatum.com.             
@                         SOA        6          0                    01:00:00        [46][lon-dc1.adatum.com.][hos...
_msdcs                    NS         2          0                    01:00:00        lon-dc1.adatum.com.             
_gc._tcp.Default-First... SRV        33         5/17/2017 1:00:00 AM 00:10:00        [0][100][3268][LON-DC1.Adatum...
_kerberos._tcp.Default... SRV        33         5/17/2017 1:00:00 AM 00:10:00        [0][100][88][LON-DC1.Adatum.c...
_ldap._tcp.Default-Fir... SRV        33         5/17/2017 1:00:00 AM 00:10:00        [0][100][389][LON-DC1.Adatum....
_gc._tcp                  SRV        33         5/17/2017 1:00:00 AM 00:10:00        [0][100][3268][LON-DC1.Adatum...
_kerberos._tcp            SRV        33         5/17/2017 1:00:00 AM 00:10:00        [0][100][88][LON-DC1.Adatum.c...
_kpasswd._tcp             SRV        33         5/17/2017 1:00:00 AM 00:10:00        [0][100][464][LON-DC1.Adatum....
_ldap._tcp                SRV        33         5/17/2017 1:00:00 AM 00:10:00        [0][100][389][LON-DC1.Adatum....
_kerberos._udp            SRV        33         5/17/2017 1:00:00 AM 00:10:00        [0][100][88][LON-DC1.Adatum.c...
_kpasswd._udp             SRV        33         5/17/2017 1:00:00 AM 00:10:00        [0][100][464][LON-DC1.Adatum....
DomainDnsZones            A          1          5/17/2017 1:00:00 AM 00:10:00        172.16.0.10                     
_ldap._tcp.Default-Fir... SRV        33         5/17/2017 1:00:00 AM 00:10:00        [0][100][389][LON-DC1.Adatum....
_ldap._tcp.DomainDnsZones SRV        33         5/17/2017 1:00:00 AM 00:10:00        [0][100][389][LON-DC1.Adatum....
ForestDnsZones            A          1          5/17/2017 1:00:00 AM 00:10:00        172.16.0.10                     
_ldap._tcp.Default-Fir... SRV        33         5/17/2017 1:00:00 AM 00:10:00        [0][100][389][LON-DC1.Adatum....
_ldap._tcp.ForestDnsZones SRV        33         5/17/2017 1:00:00 AM 00:10:00        [0][100][389][LON-DC1.Adatum....
LON-CL1                   A          1          5/17/2017 1:00:00 AM 00:20:00        172.16.0.40                     
LON-CL2                   A          1          11/6/2016 2:00:00 AM 00:20:00        172.16.0.41                     
lon-dc1                   A          1          0                    01:00:00        172.16.0.10                     
LON-HOST1                 A          1          11/6/2016 9:00:00 AM 00:20:00        172.16.0.161                    
LON-HOST2                 A          1          11/6/2016 9:00:00 AM 00:20:00        172.16.0.162                    
LON-NVHOST3               A          1          11/6/2016 10:00:0... 00:20:00        172.16.0.163                    
LON-NVHOST4               A          1          11/6/2016 10:00:0... 00:20:00        172.16.0.164                    
LON-RTR                   A          1          11/6/2016 3:00:00 AM 00:20:00        172.16.0.200                    
LON-SVR1                  A          1          5/17/2017 1:00:00 AM 00:20:00        172.16.0.11                     
LON-SVR2                  A          1          5/17/2017 1:00:00 AM 00:20:00        172.16.0.12                     
LON-SVR3                  A          1          11/6/2016 2:00:00 AM 00:20:00        172.16.0.13                     
www                       A          1          0                    01:00:00        172.16.0.10                    

Das ist doch schon mal großartig! Die Zoneninhalte werden aufgelistet.

Dann kann man bestimmt auch die Zoneninhalte aus sich des anderen Scopes ausgeben, oder?

PS dns> Get-DnsServerResourceRecord -ZoneName adatum.com -ZoneScope Trey
HostName                  RecordType Type       Timestamp            TimeToLive      RecordData                      
--------                  ---------- ----       ---------            ----------      ----------                      
@                         NS         2          0                    01:00:00        lon-dc1.adatum.com.             
@                         SOA        6          0                    01:00:00        [2][lon-dc1.adatum.com.][host...
www                       A          1          0                    01:00:00        1.2.3.4                       

Ach, schau an! Es gibt also pro Scope eine komplett neue Zone.

Probleme

Die Konsequenzen sind offensichtlich: Wenn sich an der originalen Zone etwas ändert, muss der Administrator per PowerShell die entsprechenden Einträge in dem anderen Scope der Zone manuell nachpflegen. Er hat dafür keinerlei graphische Unterstützung.

Es ist sehr wahrscheinlich, dass diese Konfiguration innerhalb kürzester Zeit zu massiven Problemen führt.

Selbst für den erfahrenen Admin wird es schwierig herauszubekommen, was genau schief läuft, weil die Debuggingmöglichkeiten extrem eingeschränkt sind. Es ist mir bspw. nicht gelungen, die Konfiguration der Policies auszulesen.

PS C:\Users\Administrator> Get-DnsServerQueryResolutionPolicy -ZoneName "adatum.com" | Format-List *

Action                : Allow
AppliesOn             : QueryProcessing
Condition             : And
Content               : {DnsServerPolicyContent}
Criteria              : {DnsServerPolicyCriteria}
IsEnabled             : True
Level                 : Zone
Name                  : SplitBrain
ProcessingOrder       : 1
ZoneName              : adatum.com
PSComputerName        : 
CimClass              : root/Microsoft/Windows/DNS:DnsServerPolicy
CimInstanceProperties : {Action, AppliesOn, Condition, Content...}
CimSystemProperties   : Microsoft.Management.Infrastructure.CimSystemProperties

Wie man sieht, gibt es keine nutzbaren Details, selbst wenn man in die Tiefe geht. Ich erspare mir die ergebnislosen Versuche, die Felder Content und Criteria weiter auseinander zu dröseln.

Lösung

Aber wie geht es nun richtig?

Das Queryprocessing wird durch die Policies beeinflusst. Leider gibt es in dem betreffenden Scope nur den manuellen Eintrag in der Zone und sonst keine weiteren Einträge. Man darf also nur dann in den Scope dieser Zone wechseln, wenn man genau weiß, das diese konkrete Anfrage vorliegt.

Zuerst einmal wird die alte Policy entfernt.

PS dns> Remove-DnsServerQueryResolutionPolicy -Name SplitBrain -ZoneName adatum.com

Soweit so einfach. Und dann wird ganz überspezifisch festgelegt, was passieren soll:

PS dns> Add-DnsServerQueryResolutionPolicy -name SplitHost -ZoneName adatum.com -Fqdn "eq,www.adatum.com" -Action ALLOW -ClientSubnet "eq,TreyNet" -ZoneScope "Trey,1"

Damit wird nur dann in diesen Scope dieser Zone geschaut, wenn auch der Record genau dieser Zone von genau dem betroffenen Subnetz angefragt wird.

Und dann sollen die restlichen Einträge in dem anderen Scope der gleichen Zone abgefragt werden.

PS dns> Add-DnsServerQueryResolutionPolicy -name AllowOthers -ZoneName adatum.com -Action ALLOW -ClientSubnet "eq,TreyNet" -ZoneScope "adatum.com,2"

Die Nummer 2 gibt an, dass diese Regel als zweite auszuführen ist.

Und nun klappt es von extern auch:

PS ext> nslookup www.adatum.com
Server:  UnKnown
Address:  172.16.10.10

Non-authoritative answer:
Name:    www.adatum.com
Address:  1.2.3.4

PS ext> nslookup lon-svr3.adatum.com
Server:  UnKnown
Address:  172.16.10.10

Non-authoritative answer:
Name:    lon-svr3.adatum.com
Address:  172.16.0.13

Aber bitte, Kinder! Nicht zuhause nachmachen!

Beim Kunden gab es eine Fehlermeldung, die besagt, dass Google einen Fehler gemacht hat und deswegen die Webseite https://www.google.de/ nicht aufgerufen werden kann. Als Begründung wird HSTS angegeben.

google-hsts

Die erste Vermutung war, dass der Kunden http://www.google.de eingeben hat (oder nur google, und der Browser hat den Rest ergänzt).

Dank HSTS müsste der Browser dann wissen, dass er eigentlich auf die https-Seite wechseln muss. Er dürfte die HTTP-Anfrage also gar nicht stellen. Vielleicht ging der HTTPS Request aber schief und der Browser hat dann doch HTTP probiert?

Was passiert denn, wenn man über HTTP mit Google spricht? Leitet der einen sofort zurück auf HTTPS?

$ telnet www.google.com 80
Trying 2a00:1450:4005:803::2004...
Connected to www.google.com.
Escape character is '^]'.
HEAD / HTTP/1.0
Host: www.google.com

HTTP/1.0 302 Found
Cache-Control: private
Content-Type: text/html; charset=UTF-8
Referrer-Policy: no-referrer
Location: http://www.google.de/?gfe_rd=cr&ei=ZFYUWaDyJuuA8QeYv7XQCg
Content-Length: 258
Date: Thu, 11 May 2017 12:17:40 GMT

Connection closed by foreign host.

Ok, kein HTTPS, sondern nur eine Weiterleitung auf google.de:

$ telnet www.google.de 80
Trying 2a00:1450:4005:803::2003...
Connected to www.google.de.
Escape character is '^]'.
HEAD /?gfe_rd=cr&ei=ZFYUWaDyJuuA8QeYv7XQCg HTTP/1.0
Host: www.google.com

HTTP/1.0 302 Found
Location: http://www.google.de/?gfe_rd=cr&ei=ZFYUWaDyJuuA8QeYv7XQCg&gws_rd=cr
Cache-Control: private
Content-Type: text/html; charset=UTF-8
P3P: CP="This is not a P3P policy! See https://www.google.com/support/accounts/answer/151657?hl=en for more info."
Date: Thu, 11 May 2017 12:18:57 GMT
Server: gws
Content-Length: 272
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Set-Cookie: NID=103=... expires=Fri, 10-Nov-2017 12:18:57 GMT; path=/; domain=.google.com; HttpOnly

Connection closed by foreign host.

Also auch hier keine Weiterleitungen nach HTTPS.

Nungut, HSTS wird ja nur über HTTPS gesetzt. Also prüfen wir das:

$ openssl s_client -connect www.google.de:443
CONNECTED(00000003)
Certificate chain
 0 s:/C=US/ST=California/L=Mountain View/O=Google Inc/CN=google.com
   i:/C=US/O=Google Inc/CN=Google Internet Authority G2
 1 s:/C=US/O=Google Inc/CN=Google Internet Authority G2
   i:/C=US/O=GeoTrust Inc./CN=GeoTrust Global CA
 2 s:/C=US/O=GeoTrust Inc./CN=GeoTrust Global CA
   i:/C=US/O=Equifax/OU=Equifax Secure Certificate Authority
---
Server certificate
-----BEGIN CERTIFICATE-----
... 174 lines of base64 - are you serious, Google?
... 510 DNS entries in SubjectAltName - are you kidding?
... 262 wildcard DNS entries - are you dumb?
... 8342 bytes raw data, that are 6 IP packets - Hello, McGoogle?
-----END CERTIFICATE-----
subject=/C=US/ST=California/L=Mountain View/O=Google Inc/CN=google.com
issuer=/C=US/O=Google Inc/CN=Google Internet Authority G2
---
No client certificate CA names sent
---
SSL handshake has read 10426 bytes and written 453 bytes
---
New, TLSv1/SSLv3, Cipher is AES128-SHA
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
---
HEAD / HTTP/1.0
Host: www.google.de

HTTP/1.0 200 OK
Date: Thu, 11 May 2017 13:18:36 GMT
Expires: -1
Cache-Control: private, max-age=0
Content-Type: text/html; charset=ISO-8859-1
P3P: CP="This is not a P3P policy! See https://www.google.com/support/accounts/answer/151657?hl=en for more info."
Server: gws
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Set-Cookie: NID=103=G0eBNw3q5-lpQWdYv8tsf0HpTpAyPeQ1THNMYhN0FOl7Jm2WP0au1U2jRj0a1P9TxLVMCTq4vrqM-GKRrcBy94zj4nk--kO9NCstSjynErLMBzZRBf_mytJ8HOJm_uSp; expires=Fri, 10-Nov-2017 13:18:36 GMT; path=/; domain=.google.de; HttpOnly
Alt-Svc: quic=":443"; ma=2592000; v="37,36,35"
Accept-Ranges: none
Vary: Accept-Encoding

Bitte? Da wird ja gar kein HSTS-Header gesetzt!

Dann muss Google irgendwann früher man einen HSTS-Header gesetzt haben und dieser gilt auf dem Kundensystem noch bis zum Erreichen seines Ablaufdatums. Und tatsächlich Google hat letzten August mit HSTS experimentiert!

Inzwischen sind die Header aber weg, denn Google hat die HSTS-Regeln in Chrome hart einprogrammiert. Alle anderen Browser fallen dabei hinten runter.

Der HSTS Header, der hier noch zum Tragen kommt, ist offenbar der, der im Client gecached wurde, denn HSTS Header sollen mindestens ein halbes Jahr gültig sein.

All das erklärt nicht die Fehlermeldung, denn HTTP ist gar nicht im Spiel. Auch in der Adressleiste des Browsers findet sich die korrekte https-URL. Dann bleibt nur noch anzunehmen, dass im HTTPS etwas schief gelaufen ist.

Leider hat der Kunde keine weiteren Informationen geliefert, insbesondere hat er nicht auf Erweitert geklickt.

Aber das hätte er tun sollen, denn erst dann erscheint die korrekte Fehlermeldung, die besagt, das das Zertifikat nicht passt. Und damit findet man schnell den eigentlichen Verursacher des Problems. Denn der Fall ist mehrfach bekannt.

Grund ist ein Amok laufender Antivirus, der versucht HTTPS Verbindungen aufzubrechen. Das Feature ist zwar abgeschaltet, aber alle paar Monate schaltet es ein Update der Software von selbst wieder an.

Zusammenfassung

Das Weglassen von HSTS durch Google ermöglicht es der Antiviren-Software, unbemerkt HTTPS-Verbindungen aufzubrechen. Diese Eingriffe der Antivirensoftware wird bei Webseiten, die HSTS einsetzen, von Firefox erkannt und gemeldet. Allerdings ist die Fehlermeldung derartig irreführend, dass die Antivirus-Software nicht in Verdacht geraten kann.

Google und Firefox helfen so den Antivirus-Herstellern, mit ihrer Software HTTPS zu belauschen.

Eine regelmäßiger Passwortwechsel ist bei mir üblich, nur klappte er dieses Mal nicht. Seit einer Woche gelingt es mir nicht, mich mit dem neuen Passwort anzumelden. Es ist und bleibt beim alten Passwort.

Störungen in der Normalität

Ein Passwortwechsel ist einfach. Man ruf als Nutzer das Kommando passwd auf.

[lutz@thunder ~]$ passwd 
Changing local password for lutz
Old Password: AltesPassword
New Password: NeuesPassword
Retype New Password: NeuesPassword
Password changed.

Soweit so erinfach. Nur ein Problem gibt es: Es funktioniert nicht.

Alle weiteren Anmeldungen werden ausschließlich mit dem alten Password befriedigt. Am einfachsten sieht man das, indem man passwd nochmal aufruft.

[lutz@thunder ~]$ passwd 
Changing local password for lutz
Old Password: NeuesPassword
passwd: sorry

Auch Reboot hilft nicht. Die betreffenden Dateien in /etc/ werden aber aktualisiert:

-rw-------  1 root  wheel      2657 11 Mai  10:43 master.passwd
-rw-r--r--  1 root  wheel     40960 11 Mai  10:43 pwd.db
-rw-------  1 root  wheel     40960 11 Mai  10:43 spwd.db
-rw-r--r--  1 root  wheel      2266 11 Mai  10:43 passwd

Auch der Versuch, von root aus das Nutzerpassword zu überschreiben zeigt keinen ernsthaften Erfolg. Zwar ändern sich die Einträge in der master.passwd, und auch der Inhalt der *pwd.db zeigt binäre Modifikationen. Die Authentisierung bleibt jedoch beim alten Password.

Fehlersuche

Auf welche Daten greift eigentlich passwd zurück? Wo holt der die alten Passworte wieder raus?

[lutz@thunder ~]$ truss -o login.log -f -a passwd
Changing local password for lutz
Old Password: NeuesPassword
passwd: sorry
[lutz@thunder ~]$ grep open login.log 
  802: openat(AT_FDCWD,"/etc/libmap.conf",O_CLOEXEC,00) = 3 (0x3)
  802: open("/usr/local/etc/libmap.d",O_NONBLOCK|O_DIRECTORY|O_CLOEXEC,0165) = 3 (0x3)
  802: openat(AT_FDCWD,"/var/run/ld-elf.so.hints",O_CLOEXEC,00) = 3 (0x3)
  802: openat(AT_FDCWD,"/usr/lib/libpam.so.6",O_CLOEXEC|O_VERIFY,00) = 3 (0x3)
  802: openat(AT_FDCWD,"/lib/libc.so.7",O_CLOEXEC|O_VERIFY,00) = 3 (0x3)
  802: open("/etc/nsswitch.conf",O_CLOEXEC,0666) = 3 (0x3)
  802: open("/etc/pwd.db",O_CLOEXEC,00)          = 3 (0x3)
  802: open("/etc/pam.d/passwd",O_RDONLY,0666)   = 3 (0x3)
  802: openat(AT_FDCWD,"/usr/lib/pam_unix.so.6",O_RDONLY,00) = 4 (0x4)
  802: openat(AT_FDCWD,"/lib/libutil.so.9",O_CLOEXEC|O_VERIFY,00) = 5 (0x5)
  802: openat(AT_FDCWD,"/lib/libcrypt.so.5",O_CLOEXEC|O_VERIFY,00) = 5 (0x5)
  802: openat(AT_FDCWD,"/usr/lib/libypclnt.so.4",O_CLOEXEC|O_VERIFY,00) = 5 (0x5)
  802: open("/etc/pam.d/other",O_RDONLY,0666)    = 3 (0x3)
  802: openat(AT_FDCWD,"/usr/lib/pam_opie.so.6",O_RDONLY,00) = 4 (0x4)
  802: openat(AT_FDCWD,"/usr/lib/libopie.so.8",O_CLOEXEC|O_VERIFY,00) = 5 (0x5)
  802: openat(AT_FDCWD,"/lib/libmd.so.6",O_CLOEXEC|O_VERIFY,00) = 5 (0x5)
  802: openat(AT_FDCWD,"/usr/lib/pam_opieaccess.so.6",O_RDONLY,00) = 4 (0x4)
  802: openat(AT_FDCWD,"/usr/lib/pam_unix.so.6",O_RDONLY,00) = 4 (0x4)
  802: open("/etc/pam.d/other",O_RDONLY,0666)    = 3 (0x3)
  802: openat(AT_FDCWD,"/usr/lib/pam_nologin.so.6",O_RDONLY,00) = 4 (0x4)
  802: openat(AT_FDCWD,"/usr/lib/pam_login_access.so.6",O_RDONLY,00) = 4 (0x4)
  802: openat(AT_FDCWD,"/usr/lib/pam_unix.so.6",O_RDONLY,00) = 4 (0x4)
  802: open("/etc/pam.d/other",O_RDONLY,0666)    = 3 (0x3)
  802: openat(AT_FDCWD,"/usr/lib/pam_permit.so.6",O_RDONLY,00) = 4 (0x4)
  802: open("/etc/pwd.db",O_CLOEXEC,00)          = 3 (0x3)

Man sieht sehr schön den gesamten PAM Ablauf, also nichts überraschendes. Die relevanten Datei ist offenbar ausschließlich /etc/pwd.db.

Eine Neuerzeugung mittels pwd_mkdb bringt ebenfalls keine Änderung. Das ist allerdings schon überraschend.

Beim Durchsuchen der Binärdatei mittels strings zeigt sich, dass die Datei inzwischen viele Kopien der Datensätze enthält. Vielleicht ist das ja die Ursache? Es könnte ja sein, dass auf den falschen Datensatz zurückgegriffen wird. Evtl. sind die Einträge auch mehrfach vorhanden?

Als letzten Test will ich sicher gehen, dass wirklich auf einen falschen Datensatz zurückgegriffen wird. Dazu ersetze ich den Passwordteil mit vipw durch einen *. Der Nutzer bekommt also gar kein Password mehr.

[root@thunder /home/lutz]# fgrep lutz /etc/passwd 
lutz:*:1001:1001:Lutz Donnerhacke,,561:/home/lutz:/usr/local/bin/bash
[root@thunder /home/lutz]# fgrep lutz /etc/master.passwd 
lutz:*:1001:1001::0:0:Lutz Donnerhacke,,561:/home/lutz:/usr/local/bin/bash

Anschließend vergebe ich ein neues Passwort als root:

[root@thunder /home/lutz]# passwd lutz
Changing local password for lutz
New Password: NeuesPassword
Retype New Password: NeuesPassword

Und siehe da, es funktioniert!

Sollte das demnächst wieder auftreten, so werden ich für diese Version einen Bugreport schreiben.

[lutz@thunder ~]$ uname -a
FreeBSD thunder 11.0-RELEASE-p9 FreeBSD 11.0-RELEASE-p9 #0: Tue Apr 11 08:48:40 UTC 2017
     root@amd64-builder.daemonology.net:/usr/obj/usr/src/sys/GENERIC  amd64

Ein Kunde fragt, ob man bestimmten Nutzern per DHCP ein TR069-Server beibringen kann. Schließlich ist die Fernkonfiguration eines Kundenrouters grundsätzlich zustimmungspflichtig. Die Aufgabe besteht also darin, die entsprechende Option in der DHCP Antwort dann zu senden, wenn der richtige Router anfragt.

Standards

Was zu tun ist, steht in den Standards. Diese sind bei broadband-forum.org hinterlegt. Im Grundsatz ist die Idee ganz einfach:

  • Ein Router (CPE) sendet in der DHCP Anfrage eine Option vom Typ Vendor-Class mit dem Inhalt "dslforum.org"
  • Der DHCP Server erkennt die Vendor-Class und sendet eine Vendor-Option mit den benötigten Parametern.
  • Die CPE kontaktiert anhand der Parameter den TR-069 Server (ACS genannt).
  • Der ACS konfiguriert die CPE.

Im Original heißt es dazu:

A CPE identifies itself to the DHCP server as supporting this method by including the string
“dslforum.org” (all lower case) any where in the DHCPv4 Vendor Class Identifier (option 60),
in the DHCPv4 V-I Vendor Class Option (option 124) or in a DHCPv6 Vendor Class (option 16)
vendor-class-data item.

The CPE MAY include either DHCPv4 option 43 or DHCPv4 option 125 (not both) in a DHCPv4
parameter-request-list (option 55) in order to indicate support for and to request option 43
or option 125. If the CPE does not use option 55 in this way, the server can assume that it
supports and requests option 43 (not option 125). Similarly,the CPE MAY include DHCPv6
option 17 in a DHCPv6 Option Request Option (option 6).

The CPE MAY use the values received from the DHCP server in the Vendor Specific Information
(DHCPv4 option 43/ DHCPv4 option 125/ DHCPv6 option 17) to set the corresponding Parameters ...

Soweit so einfach. Der Text enthält ein paar MAYs zuviel, um unmittelbar benutzbar zu erscheinen.

Als erstes fällt auf, dass es mehr als einen Typ von Vendor Optionen gibt: Die alte Form 60/43 und die neue Form 124/125 (für IPv4) und 16/17 (für IPv6). Das ist schon einmal die erste Entscheidung. Aber die ist einfach, weil sich die Parameter der neueren Vendor Optionen im Laufe der letzten paar Jahre einige Male geändert haben. Stabil blieb nur die alte Form.

Als nächstes kommt die Frage, welche Daten wirklich übersendet werden müssen und welche Folge das hat:

If the CPE obtained an ACS URL through DHCP and it cannot reach the ACS, the CPE MUST
use DHCP to rediscover the ACS URL.  The CPE MUST consider the ACS unreachable if it
cannot establish a TCP connection to it for 300 seconds at each of the IP addresses to which
the ACS URL resolves. 

Primär notwendig ist also die ACS-URL und diese muss erreichbar sein. Andernfalls kommt die CPE nicht online.

Die Entscheidung, nicht sämtliche Kundenrouter mit der TR-69 Konfig zu beglücken, ist also allein dadurch gerechtfertigt.

Implementierung

Im Einsatz sind ISC DHCP Server, und diese haben eine clevere Methode Vendor Options zu konfigurieren.

option space ACS;
option ACS.acs_URL code 1 = text;
option ACS.acs_PROVCODE code 2 = text;

Diese Konfiguration sagt dem DHCP Server, wie die interne Struktur der Vendor Option aussieht: Es gibt zwei Untertypen mit den Codes 1 und 2, die jeweils Text enthalten. Beim Generieren der DHCP Antwort wird der DHCP Server somit die entsprechenden Tag-Length-Value Einträge erzeugen und diese dann als großen Blob in als Wert der Option 43 eintragen.

Die Interpretation der Option 43 (Vendor Option) ist der CPE überlassen, die dann hoffentlich weiß, dass hier ACS Konfiguration auszulesen ist. Was nämlich fehlt, ist die Angabe des Herstellers für den die Optionen gelten. Man muss also absolut sicher sein, welcher CPE man welche Vendor Optionen übersendet. Normalerweise erfolgt das dadurch, dass die CPE ja selbst mit der Angabe der Vendor Class in der Anfrage festgelegt hat, wie sie die Vendor Optionen verarbeiten wird.

Als nächstes kann man die Werte im DHCP Server setzen. Die URL ist statisch und der ProviderCode, mit dem sich die CPE melden soll, ist gerätespezifisch.

option ACS.acs_URL "https://acs.dsl.example:12345/path/to/tr69.app";
host xxx { option ACS.acs_PROVCODE "IDENTIFIER"; }

Jetzt weiß der DHCP Server, welche Angaben er welchem Client einzupacken hat. Was er noch nicht weiß, ist, welche Optionen welchen Herstellers er überhaupt einpacken soll. Solange das nicht klar ist, liefert er nichts aus. Bei den neueren Optionen besteht das Problem nicht, weil dort der Hersteller immer mitgesendet wird.

Normalweise löst man das so, dass man die Vendor Class der Anfrage auswertet. Leider senden die CPEs diese Information aber nicht mit. Also setze ich diese Auswahl am host, denn alle anderen sollen die Optionen ja wirklich nicht bekommen.

host xxx { vendor-option-space ACS; }

Nun liefert der ACS an diese CPEs die entsprechenden Optionen aus.

Geht nicht?

Aber halt! Er liefert nicht an alle CPEs. Grund ist die Option 55 (parameter-request-list) in der die CPE festlegt, welche Optionen sie in der Antwort akzeptiert. Mit dem setzen dieser Option schließt sie alle anderen Optionen für die Antwort aus. Und natürlich ist die Vendor Option (43) nicht angefordert worden. Nur dort, wo die Option 55 in der Anfrage fehlte, hat der DHCP Server auch eine Antwort geliefert.

Also muss man generell die Optionsliste erweitern:

option dhcp-parameter-request-list = concat(option dhcp-parameter-request-list,2b);

Eine berechtigte Frage ist, warum man nicht append option parameter-request-list 43 benutzt. Und die Antwort ist so einfach wie verblüffend: Diese Syntax steht nur in der Konfig des DHCP-Client zur Verfügung.

Und es geht immer noch nicht!

An der Stelle habe ich lange gesucht, weil jede Konfigurationsänderung absolut nichts bewirkt hat.

Erst nach Tagen fiel mir eine Zeile an ganz anderer Stelle in die Hände:

option meta-data code 43 = text;

Irgendjemand hatte sich einen neuen Datentyp gebaut, der den Codepunkt 43 benutzt, Damit war das komplette Vendor Option Space Processing des DHCP Servers deaktiviert.

Nach Löschen der Altlast-Zeile tut die Kiste, was sie soll.

Wartungsarbeiten sind notwendig. Auch bei Webseiten, die eigentlich Tag und Nacht verfügbar sein sollten. Nicht alle Betreiber können auf verteilte Systeme setzen, deren interne Redundanz die Arbeiten vor dem Besucher versteckt. Stattdessen zeigt man eine mehr oder minder kreative Wartungsseite an. Die des Duden ist besonders schön.

2017-03-29-092509_990x759_scrot

Gratulation!