Check TCP reachability without any external tools like nc or nmap
Bash ships with a built-in tool for checking whether a TCP port on another host is reachable, through the pseudo file /dev/tcp, with no need for netcat, nmap, or an extra package. For simple reachability checks in monitoring and deployment scripts that is often plenty, but for real security analysis or large network ranges it is the wrong toolset.
Table of Contents
- 1. What /dev/tcp is and what a simple port check is good for
- 2. Checking a single port with /dev/tcp
- 3. Interpreting return values correctly and distinguishing failure causes
- 4. Checking several ports and hosts in a loop
- 5. Timeout handling: why /dev/tcp can block without help
- 6. Limits of pure Bash compared to real port scanners
- 7. When a real port scanner like nmap is needed
- 8. Practical example: a pre-deployment health check in a CI pipeline
- 9. Tool choice at a glance
- 10. Summary
- 11. FAQ
1. What /dev/tcp is and what a simple port check is good for
/dev/tcp is not a real device file on the filesystem, it is a convention that Bash itself interprets whenever a redirection targets a path of the form /dev/tcp/host/port. Instead of opening an actual file, Bash internally establishes a TCP connection to the given host and port and exposes it as a readable or writable file descriptor. This behavior is enabled in many, but not all, Bash builds, it is missing for instance in some minimal distributions that compile Bash without the net-redirections feature.
For a simple reachability check, the kind monitoring scripts often need before a deployment or inside a cron job, that is plenty: the question is usually just whether a connection succeeds at all, not exactly which service is listening or what version it reports. For that binary yes-or-no question, /dev/tcp is the leanest tool available, because no installation is needed and the check fits in a handful of lines of Bash.
2. Checking a single port with /dev/tcp
The basic pattern uses exec to redirect a free file descriptor to the pseudo file, then evaluates the exit code of that redirection. If the connection succeeds, the redirection itself reports success, if it fails, for example because the port is closed and the target host sends an RST reply, Bash reports an error and the exit code is non-zero.
It matters to close the opened descriptor again after the check, otherwise the TCP connection stays open until the script or shell exits, which unnecessarily ties up resources on both ends of the connection when many checks run in sequence.
#!/usr/bin/env bash
set -uo pipefail
HOST="db.internal.example.com"
PORT=5432
if exec 3<>"/dev/tcp/$HOST/$PORT"; then
echo "Port $PORT on $HOST is open"
exec 3<&-
exec 3>&-
else
echo "Port $PORT on $HOST is closed or unreachable"
fi
3. Interpreting return values correctly and distinguishing failure causes
A failed connection attempt through /dev/tcp can have several very different causes that a plain exit code cannot distinguish between: the port could be closed and actively refusing the connection, the host could sit behind a firewall silently dropping packets, or the hostname itself might not resolve at all. For a meaningful monitoring script, a plain yes or no is often not enough.
A more robust script therefore separates DNS resolution from the actual connection check, first verifying with getent hosts or host whether the name resolves at all, and only then attempting the connection through /dev/tcp. That makes it possible to state clearly in the error output whether the problem is the name, the network path, or the target port itself.
#!/usr/bin/env bash
set -uo pipefail
check_port() {
local host="$1" port="$2"
if ! getent hosts "$host" >/dev/null 2>&1; then
echo "DNS resolution failed for $host"
return 2
fi
if exec 3<>"/dev/tcp/$host/$port" 2>/dev/null; then
exec 3<&- 3>&-
return 0
else
echo "Cannot reach $host:$port"
return 1
fi
}
check_port "api.internal.example.com" 443
4. Checking several ports and hosts in a loop
For a simple health check across several services, a loop over a list of host-port pairs is enough, checking each entry individually with the function shown above. It matters that a single failed port does not immediately abort the whole script under set -e, but instead gets collected and summarized at the end, so a deployment script reports all relevant problems at once instead of only the first one.
This kind of script does not replace a dedicated health check endpoint in the application itself, but it is a sensible first step before a deployment, catching basic network and firewall problems early, before the actual application even starts and produces more confusing error messages.
#!/usr/bin/env bash
set -uo pipefail
declare -A targets=(
["db.internal.example.com"]=5432
["cache.internal.example.com"]=6379
["queue.internal.example.com"]=5672
)
failed=0
for host in "${!targets[@]}"; do
port="${targets[$host]}"
if timeout 3 bash -c "exec 3<>/dev/tcp/$host/$port" 2>/dev/null; then
echo "OK $host:$port"
else
echo "FAIL $host:$port"
failed=1
fi
done
exit "$failed"
5. Timeout handling: why /dev/tcp can block without help
/dev/tcp itself has no built-in timeout option. Against an open port, the connection attempt usually responds instantly, against a closed port with an RST reply it also fails quickly, but against a host that silently drops packets, for example behind a restrictive firewall, Bash waits for the operating system's own TCP stack timeout, which depending on the system can range from a few seconds to several minutes.
The external timeout command solves this reliably, hard-terminating the entire Bash subshell call after a fixed number of seconds, regardless of how long the underlying TCP connection attempt itself would have taken. For monitoring scripts that run regularly and need a predictable runtime, combining timeout with /dev/tcp is therefore mandatory, not optional.
6. Limits of pure Bash compared to real port scanners
As handy as /dev/tcp is for individual checks, it remains a very limited tool compared to a dedicated port scanner. Bash can only establish TCP connections, UDP ports cannot be checked through /dev/tcp at all, because UDP as a connectionless protocol produces no handshake result that could be interpreted as success or failure. Banner grabbing, reading the software version of a responding service, and operating system fingerprinting based on TCP stack quirks are both also well outside what a simple Bash redirection can do.
A sequential Bash loop over many ports or many hosts is also noticeably slow, because each check waits for the previous one, while a dedicated scanner opens hundreds of connection attempts in parallel and covers an entire subnet in seconds instead of minutes. For an occasional health check of three or four known services that does not matter, for a scan of an entire network segment pure Bash is simply the wrong tool.
7. When a real port scanner like nmap is needed
Once the question goes beyond a simple yes-or-no reachability check, the task belongs in the hands of a dedicated tool like nmap. That is especially true for security audits, where every open port on a target system needs to be enumerated along with the services behind it and their versions, and for any situation involving UDP services, operating system fingerprinting, or scans across larger IP ranges.
nmap also comes with capabilities that deliberately do not belong in a simple Bash script, for example various scan techniques to work around firewalls, parallel scans with configurable speed, and structured, machine readable output for further processing in security tools. Anyone who regularly needs to check more than a single known port for reachability should consider nmap instead of continuing to extend a home grown Bash script.
8. Practical example: a pre-deployment health check in a CI pipeline
A realistic use case for /dev/tcp is a pre-deployment check in a CI pipeline that verifies, before the actual rollout, whether all required backend services are reachable from the deployment runner. If that check fails, the pipeline aborts immediately with a clear error message, rather than the application failing later at startup with a confusing connection error.
Checks like this should always end with a clear exit code, so the CI pipeline can evaluate the state unambiguously, and the output should have one line per checked target, so on failure it is immediately obvious which service exactly caused the problem, without having to search through the entire pipeline log.
9. Tool choice at a glance
The right choice between pure Bash, netcat and nmap depends on how deep the check needs to go, how many targets need checking, and whether a structured evaluation of the results is required.
| Tool | Protocols | Installation required | Typical use |
|---|---|---|---|
/dev/tcp in Bash |
TCP only | No, Bash builtin | Single reachability check in deployment scripts |
nc (netcat) |
TCP and UDP | Usually yes, but often preinstalled | Quick port check including banner grabbing |
curl with --connect-timeout |
TCP over HTTP(S) | Usually preinstalled | HTTP endpoint reachability rather than raw TCP |
nmap |
TCP, UDP, ICMP and more | Yes, dedicated package | Security audits, network range scans, service detection |
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
Port Checks in Bash: The Essentials at a Glance
Core idea
/dev/tcp/host/port is a Bash internal convention that establishes a real TCP connection on redirection.
Timeout
/dev/tcp has no built-in timeout, the external timeout command is therefore mandatory for reliable scripts.
Limits
No UDP, no banner grabbing, no parallelization: pure Bash is the wrong tool for security audits.
Use case
Ideal for simple reachability checks of a few known ports in monitoring and deployment scripts.