揭秘 Bash 源码内置的伪设备
很多人用过 /dev/tcp 做端口探活,但如果你去执行 ls /dev/tcp,系统会告诉你 No such file or directory。
这是一个非常反直觉的底层事实:/dev/tcp 和 /dev/udp 在真实磁盘的 /dev 目录下根本不存在。它们并不是 Linux 内核创建的设备文件,而是 Bash 解释器在解析重定向符号(< 或 >)时,在应用层硬编码拦截并转换成底层系统调用(如 socket()、connect()、dup())的“虚拟语法糖”。
翻开 Bash 源码(redir.c),你会发现 Bash 内部拦截处理的伪设备总共有以下几类:
| 伪设备路径 | 底层实现机制 | 核心工程用途 |
|---|---|---|
/dev/tcp/<host>/<port> |
Bash 应用层拦截,发起真实 TCP Socket 连接 | 无 nc/telnet/curl 时的端口探活、手写原生 HTTP 客户端 |
/dev/udp/<host>/<port> |
Bash 应用层拦截,发送无连接 UDP 数据报 | 发送 Syslog 日志、测试 DNS 端口、发送 NTP 校时请求 |
/dev/fd/<n> |
映射到当前进程的第 n 号文件描述符 |
将内存数据流伪装成“文件路径参数”传给外部命令(进程替换 <(...) 的底层基石) |
/dev/stdin |
等价于 /dev/fd/0(标准输入) |
欺骗那些强制要求传入“输入文件路径”、不支持 - 读取标准输入的工具 |
/dev/stdout & /dev/stderr |
等价于 /dev/fd/1 与 /dev/fd/2 |
在 Docker 容器中,强迫只支持写本地日志文件的软件(如某些配置下的 Nginx/Java 应用)将日志直接吐向控制台流 |
终极实战:零外部依赖,仅用 Bash 实现 HTTP GET 下载
假设你身处一个没有任何下载工具(无 curl、无 wget、无 python)的极简容器内,需要从内网服务器拉取一个初始化脚本,只需利用 /dev/tcp 配合文件描述符 3 即可手搓一个 HTTP 客户端:
#!/bin/bash
HOST="example.com"
PORT=80
PATH_URI="/"
# 1. 使用 timeout 测试端口连通性(防止防火墙丢包导致死等)
timeout 5 bash -c "exec 3<>/dev/tcp/${HOST}/${PORT}" 2>/dev/null || {
echo "连接失败或超时"
exit 1
}
# 2. 在当前 Shell 中正式建立 TCP 连接并绑定到文件描述符 3
exec 3<>/dev/tcp/${HOST}/${PORT}
# 3. 向描述符 3 写入标准的 HTTP/1.1 请求报文(换行符必须是 \r\n)
printf "GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n" "$PATH_URI" "$HOST" >&3
# 4. 从描述符 3 读取服务器返回的全部响应(包含响应头和响应体)
cat <&3
# 5. 关闭连接
exec 3>&-
避坑指南
- 仅限重定向场景:直接执行
cat /dev/tcp/127.0.0.1/80会报错找不到文件。因为cat是独立的外部二进制程序,它去查真实文件系统时找不到/dev/tcp。必须由 Bash 自己先通过<、>或exec完成重定向打开,再把打开后的描述符交给其他命令读取。 - 注意发行版编译选项:极少数精简发行版(如早期部分 Debian/Ubuntu 衍生版)在编译 Bash 时可能会通过
--disable-net-redirections参数禁用网络重定向特性,使用前可在目标环境简单测试验证。
