Blog Engineering Kann NO_PROXY standardisiert werden?
Aktualisiert am: October 9, 2024
14 Minuten Lesezeit

Kann NO_PROXY standardisiert werden?

Erfahre, wie GitLab ein Problem gelöst hat, das durch die Unterschiede der Variablen, die nicht von allen Webclients unterstützt werden, entstanden ist.

question-mark-pile.jpg

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:

  1. Ein git push über die Befehlszeile funktionierte
  2. Ü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:

  1. Ruby versucht es zuerst mit der kleingeschriebenen Variante
  2. 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

  1. Verwende die Kleinschreibung.
  2. Nutze durch Kommas getrennte hostname:port Werte.
  3. IP-Adressen sind okay, aber Hostnamen werden nie aufgelöst.
  4. Endungen werden immer zugeordnet (z.B. example.com wird test.example.com zugeordnet).
  5. Wenn Top-Level-Domains abgeglichen werden müssen, solltest du einen vorangestellten Punkt vermeiden (.).
  6. 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:

  1. Bevorzugung von Kleinbuchstaben gegenüber Großbuchstaben bei Variablen (z. B. http_proxy sollte vor HTTP_PROXY gesucht werden).
  2. Verwende durch Kommas getrennte Werte für hostname:port.
    • Jeder Wert kann optionale Leerzeichen enthalten.
  3. Führe niemals DNS-Lookups durch und verwende keine regulären Formeln.
  4. Nutze * um alle Hosts zu verbinden.
  5. Führende Punkte (.) werden entfernt und mit Domain-Suffixen abgeglichen.
  6. Unterstützung des CIDR-Blockabgleichs.
  7. 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.

Wir möchten gern von dir hören

Hat dir dieser Blogbeitrag gefallen oder hast du Fragen oder Feedback? Erstelle ein neues Diskussionsthema im GitLab Community-Forum und tausche deine Eindrücke aus. Teile dein Feedback

Bist du bereit?

Sieh dir an, was dein Team mit einer einheitlichen DevSecOps-Plattform erreichen könnte.

Kostenlose Testversion anfordern

Finde heraus, welcher Tarif für dein Team am besten geeignet ist

Erfahre mehr über die Preise

Erfahre mehr darüber, was GitLab für dein Team tun kann

Sprich mit einem Experten/einer Expertin