Wenn du schon einmal einen Web-Proxyserver verwendet hast, bist du wahrscheinlich mit den Umgebungsvariablen http_proxy
oder HTTP_PROXY
vertraut. Weniger bekannt ist möglicherweise die Variable no_proxy
, mit der du bestimmten Datenverkehr für bestimmte Hosts von der Verwendung des Proxys ausschließen kannst. Obwohl HTTP ein gut definierter Standard ist, existiert kein einheitlicher Standard dafür, wie Clients diese Variablen behandeln sollten. Dies führt dazu, dass Webclients diese Variablen auf sehr unterschiedliche Weise unterstützen. Bei einem GitLab-Kunden führten eben diese Unterschiede zu einer wochenlangen Fehlersuche, um herauszufinden, warum bestimmte Dienste nicht mehr kommunizierten.
In diesem Artikel erfährst du, wie wir die Probleme analysiert und gelöst haben.
Verwendung des Proxyservers: Konflikte und Ausnahmen
Die meisten Webclients unterstützen heutzutage die Verbindung zu Proxy-Servern über Umgebungsvariablen (Environment variables):
http_proxy / HTTP_PROXY
https_proxy / HTTPS_PROXY
no_proxy / NO_PROXY
Diese Variablen sagen dem Client, welche URL genutzt werden sollte, um Zugang zu Proxyservern zu erhalten und welche Ausnahmen gemacht werden sollten. Wenn du zum Beispiel einen Proxyserver hast, der auf http://alice.example.com:8080
überwacht wird, könntest du ihn verwenden via:
export http_proxy=http://alice.example.com:8080
Welcher Proxyserver wird verwendet, wenn Bob die Version in Großbuchstaben, HTTP_PROXY
, ebenfalls definiert?
export HTTP_PROXY=http://bob.example.com:8080
Die Antwort ist uneindeutig: Es hängt vom jeweiligen Kontext ab. In einigen Fällen gewinnt der Proxy von Alice, in anderen Fällen gewinnt Bob.
Ausnahmen definieren
Was passiert, wenn du Ausnahmen machen willst? Nehmen wir etwa an, du willst einen Proxyserver für alles außer internal.example.com
und internal2.example.com
verwenden. In diesem Fall kommt die Variable no_proxy
ins Spiel. Dann würdest du no_proxy
wie folgt definieren:
export no_proxy=internal.example.com,internal2.example.com
Was ist, wenn du IP-Adressen ausschließen willst? Kann man Sternchen oder eine no_proxy
-Wildcard verwenden? Kann man CIDR-Blöcke verwenden (z. B. 192.168.1.1/32
)? Auch hier gilt wieder: Es kommt darauf an.
Geschichte der Webclients,wget no_proxy und cURLno_proxy
1994 haben die meisten Webclients CERN's libwww
genutzt, welche http_proxy
und die no_proxy
Umgebungsvariable unterstützt haben. libwww
hat nur die kleingeschriebene Variante von http_proxy
verwendet. Somit war die no_proxy
-Syntax sehr einfach:
no_proxy ist eine mithilfe von Kommas oder Leerzeichen getrennte Liste von Rechner-
oder Domain-Namen mit optionalem :port part. Wenn kein :port
part vorhanden ist, wird sie für alle Ports auf der Domain angewendet.
Beispiel:
no_proxy="cern.ch,some.domain:8001"
Es entstanden neue Clients, die ihre eigenen HTTP-Implementierungen hinzufügten, ohne auf libwww
zu verlinken. Im Januar 1996 veröffentlichte Hrvoje Niksic geturl
, den Vorgänger des heutigen wget
. Einen Monat später fügte geturl
in v1.1 Unterstützung für http_proxy
hinzu. Im Mai 1996 wurde mit geturl
v1.3 die Unterstützung für no_proxy
hinzugefügt. Genau wie libwww
unterstützte geturl
nur die Kleinbuchstabenform.
Im Januar 1998 veröffentlichte Daniel Stenberg curl
v5.1, das die Variablen http_proxy
und no_proxy
unterstützte. Darüber hinaus erlaubte curl
die Großbuchstaben HTTP_PROXY und NO_PROXY. Eine plötzliche Wendung: Im März 2009 wurde mit curl
v7.19.4 die Unterstützung für die Großbuchstabenvariante von HTTP_PROXY aufgrund von Sicherheitsbedenken eingestellt. Während curl HTTP_PROXY ignoriert, funktioniert HTTPS_PROXY jedoch auch heute noch.
Heutzutage werden diese Proxyserver-Variablen je nach verwendeter Sprache oder Tool unterschiedlich gehandhabt.
http_proxy und https_proxy
In der folgenden Tabelle steht jede Zeile für ein unterstütztes Verfahren, während jede Spalte das Werkzeug (z.B. curl) oder die Sprache (z.B. Ruby) enthält, für die es gilt:
curl | wget | Ruby | Python | Go | |
---|---|---|---|---|---|
http_proxy |
Ja | Ja | Ja | Ja | Ja |
HTTP_PROXY |
Nein | Nein | Ja (warning) | Ja (wenn REQUEST_METHOD nicht in env) |
Ja |
https_proxy |
Ja | Ja | Ja | Ja | Ja |
HTTPS_PROXY |
Ja | Nein | Ja | Ja | Ja |
Präzedenzfall | Kleinschreibung | Kleinschreibung | Kleinschreibung | Kleinschreibung | Großschreibung |
Referenz | Quelle | Quelle | Quelle | Quelle | Quelle |
Proxy-Variablen in Python und Go: Der Unterschied zwischen Groß- und Kleinschreibung
Beachte, dass http_proxy
und https_proxy
immer durchgängig unterstützt werden, während HTTP_PROXY
nicht immer unterstützt wird. Python (über urllib) verkompliziert das Bild noch mehr: HTTP_PROXY
kann so lange verwendet werden, wie REQUEST_METHOD
nicht in der Umgebung definiert ist.
Während man erwarten könnte, dass Umgebungsvariablen in Großbuchstaben geschrieben werden, war http_proxy
zuerst da und ist damit also der De-facto-Standard. Im Zweifelsfall sollte man sich für die Kleinschreibung entscheiden, da diese universell unterstützt wird.
Im Gegensatz zu den meisten Implementierungen versucht Go es mit Großbuchstaben, bevor es auf die Kleinschreibung zurückgreift. Wir werden später noch sehen, warum genau diese Vorgehensweise bei einem GitLab-Kunden zu Problemen führte.
no_proxy im gleichen Issue
Einige Benutzer(innen) haben das Fehlen der no_proxy
-Spezifikation in diesem Issue diskutiert. Da no_proxy
eine Ausschlussliste spezifiziert, stellen sich viele Fragen zu ihrem Verhalten. Nehmen wir zum Beispiel an, deine no_proxy
-Konfiguration ist definiert:
export no_proxy=example.com
Bedeutet dies, dass die Domain ein exaktes Match sein muss oder wird subdomain.example.com
auch mit dieser Konfiguration übereinstimmen? Die folgende Tabelle zeigt den Status der verschiedenen Implementierungen. Es stellt sich heraus, dass alle Implementierungen Suffixe korrekt abgleichen, wie in der Zeile “Stimmt mit Suffixen überein” zu sehen ist:
curl | wget | Ruby | Python | Go | |
---|---|---|---|---|---|
no_proxy |
Ja | Ja | Ja | Ja | Ja |
NO_PROXY |
Ja | Nein | Ja | Ja | Ja |
Präzedenzfall | Kleinschreibung | Kleinschreibung | Kleinschreibung | Kleinschreibung | Großschreibung |
Stimmt mit Suffixen überein? | Ja | Ja | Ja | Ja | Ja |
Strips leading . ? |
Ja | Nein | Ja | Ja | Nein |
* Stimmt mit allen Hosts überein? |
Ja | Nein | Nein | Ja | Ja |
Unterstützt Regexe? | Nein | Nein | Nein | Nein | Nein |
Unterstützt CIDR-Blöcke? | Nein | Nein | Ja | Nein | Ja |
Erkennt Loopback-IPs? | Nein | Nein | Nein | Nein | Ja |
Referenz | Quelle | Quelle | Quelle | Quelle | Quelle |
Wenn jedoch ein vorangestellter. in der no_proxy
-Einstellung vorhanden ist, variiert das Verhalten. Zum Beispiel verhalten sich curl
und wget
unterschiedlich. curl
entfernt immer den vorangestellten . und nimmt den Vergleich mit einem Domain-Suffix vor. Dieser Aufruf umgeht den Proxy:
$ env https_proxy=http://non.existent/ no_proxy=.gitlab.com curl https://gitlab.com
<html><body>You are being <a href="https://about.gitlab.com/">redirected</a>.</body></html>
Allerdings entfernt wget
den vorangestellten.
nicht und führt eine exakte String-Übereinstimmung mit einem Hostnamen durch. Infolgedessen versucht wget
, einen Proxy zu verwenden, wenn eine Top-Level-Domain verwendet wird:
$ env https_proxy=http://non.existent/ no_proxy=.gitlab.com wget https://gitlab.com
Resolving non.existent (non.existent)... failed: Name or service not known.
wget: unable to resolve host address 'non.existent'
In keiner der Implementierungen werden reguläre Ausdrücke unterstützt. Die Verwendung von Regexes würde die Angelegenheit zusätzlich verkomplizieren, da es verschiedene Varianten gibt (z. B. PCRE, POSIX usw.). Darüber hinaus führen Regexes zu potenziellen Leistungs- und Sicherheitsproblemen.
In einigen Fällen können Proxys durch das Setzen der no_proxy
-Variable auf * vollständig deaktiviert werden, aber dies ist keine allgemeingültige Regel. Keine Implementierung führt einen DNS-Lookup durch, um einen Hostnamen in eine IP-Adresse aufzulösen, wenn entschieden wird, ob ein Proxy verwendet werden soll. Daher sollten keine IP-Adressen in der no_proxy
-Variable angegeben werden, es sei denn, es wird erwartet, dass die IPs explizit vom Client verwendet werden.
Dasselbe gilt für CIDR-Blöcke wie z. B. 18.240.0.1/24. CIDR-Blöcke funktionieren nur, wenn die Anfrage direkt an eine IP-Adresse gestellt wird. Nur Go und Ruby erlauben die Verwendung von CIDR-Blöcken. Im Gegensatz zu anderen Implementierungen deaktiviert Go sogar automatisch die Verwendung eines Proxys, wenn eine Loopback-IP-Adresse erkannt wird.
Fehlerbehebung bei Proxy-Konfigurationen: Wie unterschiedliche no_proxy-Einstellungen GitLab-Prozesse beeinträchtigen
Wenn die Anwendung in mehreren Sprachen geschrieben ist und hinter einer Unternehmensfirewall mit einem Proxyserver arbeiten muss, solltest du auf diese Unterschiede achten. GitLab besteht zum Beispiel aus einigen in Ruby und Go geschriebenen Diensten. Ein Kunde hat seine Proxy-Konfiguration in etwa wie folgt eingestellt:
HTTP_PROXY: http://proxy.company.com
HTTPS_PROXY: http://proxy.company.com
NO_PROXY: .correct-company.com
Der Kunde meldete das folgende Problem mit GitLab:
- Ein
git push
über die Befehlszeile funktionierte - Über die Web-UI vorgenommene Git-Änderungen schlugen fehl
Unsere Support-Techniker stellten fest, dass aufgrund eines Konfigurationsproblems bei Kubernetes einige veraltete Werte zurückblieben. Der Pod hatte eine Umgebung, die in etwa so aussah:
HTTP_PROXY: http://proxy.company.com
HTTPS_PROXY: http://proxy.company.com
NO_PROXY: .correct-company.com
no_proxy: .wrong-company.com
Die inkonsistenten Definitionen in no_proxy
und NO_PROXY
waren ein Warnsignal, und wir hätten das Problem lösen können, indem wir sie konsistent gemacht oder den falschen Eintrag entfernt hätten. Aber sehen wir uns an, was passiert ist:
- Ruby versucht es zuerst mit der kleingeschriebenen Variante
- Go versucht es zuerst mit der Variante in Großbuchstaben
Infolgedessen hatten in Go geschriebene Dienste wie GitLab Workhorse die richtige Proxy-Konfiguration. Ein git push
von der Befehlszeile aus funktionierte problemlos, da die Go-Dienste diesen Vorgang primär abwickelten:
Parse error on line 2: ...agram autonumber participant C a ----------------------^ Expecting 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', got 'NL'
Der gRPC-Aufruf in Schritt 2 hat nie versucht, den Proxy zu verwenden, da no_proxy
richtig konfiguriert wurde, um eine direkte Verbindung zu Gitaly herzustellen.
Wenn jedoch ein(e) Benutzer(in) eine Änderung in der Bedienoberfläche vornimmt, leitet Gitaly die Anfrage an einen gitaly-ruby
-Service weiter, der in Ruby geschrieben ist. gitaly-ruby
nimmt Änderungen am Repository vor und meldet diese über einen gRPC-Aufruf an seinen übergeordneten Prozess zurück. Wie in Schritt 4 unten zu sehen ist, fand der Reporting-Schritt jedoch nicht statt:
Parse error on line 2: ...agram autonumber participant C a ----------------------^ Expecting 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', got 'NL'
Da gRPC HTTP/2 als Transport verwendet, versuchte gitaly-ruby
einen CONNECT zum Proxy, da es mit der falschen no_proxy
-Einstellung konfiguriert war. Der Proxy lehnte diese HTTP-Anfrage sofort ab, was den Fehler im Web-UI-Push-Case verursachte.
Nachdem wir den Kleinbuchstaben no_proxy
aus der Umgebung entfernt hatten, funktionierte der Push von der Bedienoberfläche wie erwartet, und gitaly-ruby
verband sich direkt mit dem übergeordneten Gitaly-Prozess. Schritt 4 funktionierte, wie im folgenden Diagramm dargestellt:
Parse error on line 2: ...agram autonumber participant C a ----------------------^ Expecting 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', got 'NL'
Eine überraschende Entdeckung mit gRPC
Beachte, dass der Kunde HTTPS_PROXY
auf einen unverschlüsselten HTTP_PROXY
gesetzt hat; beachte, dass http://
anstelle von https://
verwendet wird. Dies ist zwar vom Standpunkt der Sicherheit aus nicht ideal, aber es kann gemacht werden, um zu vermeiden, dass Clients aufgrund von Problemen bei der TLS-Zertifikatsüberprüfung scheitern.
Ironischerweise wäre dieses Problem nicht aufgetreten, wenn ein HTTPS-Proxy angegeben worden wäre. Wenn ein HTTPS-Proxy verwendet wird, ignoriert gRPC diese Einstellung, da HTTPS-Proxys nicht unterstützt werden.
Der kleinste gemeinsame Nenner
Man sollte niemals inkonsistente Werte mit Proxy-Einstellungen in Klein- und Großbuchstaben definieren. Falls du allerdings jemals einen Stack verwalten musst, der in mehreren Sprachen geschrieben ist, solltest du in Erwägung ziehen, HTTP-Proxy-Konfigurationen auf den kleinsten gemeinsamen Nenner zu setzen:
http_proxy
und https_proxy
- Verwende die Kleinschreibung.
HTTP_PROXY
wird nicht immer unterstützt oder empfohlen.- Wenn du unbedingt die Variante mit Großbuchstaben verwenden musst, achte darauf, dass sie denselben Wert hat.
no_proxy
- Verwende die Kleinschreibung.
- Nutze durch Kommas getrennte
hostname:port
Werte. - IP-Adressen sind okay, aber Hostnamen werden nie aufgelöst.
- Endungen werden immer zugeordnet (z.B.
example.com
wirdtest.example.com
zugeordnet). - Wenn Top-Level-Domains abgeglichen werden müssen, solltest du einen vorangestellten Punkt vermeiden (.).
- Vermeide die Verwendung von CIDR-Matching, da dies nur von Go und Ruby unterstützt wird.
Standardisierung von no_proxy
Die Kenntnis des kleinsten gemeinsamen Nenners kann helfen, Probleme zu vermeiden, wenn diese Definitionen für verschiedene Webclients kopiert werden. Aber sollte es für no_proxy
und die anderen Proxy-Einstellungen einen dokumentierten Standard geben und nicht nur eine Ad-hoc-Übereinstimmung? Die folgende Liste kann als Ausgangspunkt für einen Vorschlag dienen:
- Bevorzugung von Kleinbuchstaben gegenüber Großbuchstaben bei Variablen (z. B.
http_proxy
sollte vorHTTP_PROXY
gesucht werden). - Verwende durch Kommas getrennte Werte für
hostname:port
.- Jeder Wert kann optionale Leerzeichen enthalten.
- Führe niemals DNS-Lookups durch und verwende keine regulären Formeln.
- Nutze
*
um alle Hosts zu verbinden. - Führende Punkte (
.
) werden entfernt und mit Domain-Suffixen abgeglichen. - Unterstützung des CIDR-Blockabgleichs.
- Stelle niemals Vermutungen über spezielle IP-Adressen an (z. B. Loopback-IP-Adressen in
no_proxy
).
Fazit
Seit der Veröffentlichung des ersten Web-Proxys sind über 25 Jahre vergangen. Obwohl sich die grundlegenden Mechanismen zur Konfiguration eines Webclients über Umgebungsvariablen (wie z. B. environment no_proxy/env no_proxy) kaum verändert haben, haben sich bei den verschiedenen Implementierungen zahlreiche Feinheiten herausgebildet. Ein Beispiel aus der Praxis zeigt, dass die irrtümliche Definition widersprüchlicher no_proxy
- und NO_PROXY
-Variablen zu stundenlanger Fehlersuche führte, da Ruby und Go diese Einstellungen unterschiedlich auswerten. Das Hervorheben dieser Unterschiede kann helfen, zukünftige Probleme in deinem Produktions-Stack zu vermeiden. Es wäre wünschenswert, dass Webclient-Maintainer das Verhalten standardisieren, um solche Probleme von vornherein auszuschließen.