用 netcat 分层检查 TCP、HTTP 与 TLS 连接
最后更新:
一个服务连不上,先区分“TCP 连接建不起来”和“连上了但应用不给正确响应”。nc(netcat)既能只尝试建立连接,也能在连接上收发原始数据。本文使用 Debian 上的 netcat-openbsd 语法;BusyBox、传统 netcat 和 Nmap Ncat 的参数并不完全相同,先用 nc -h 核对本机版本。TCP 通了之后,再用 nc 手写 HTTP 请求,用 OpenSSL 和 curl 检查 TLS,一层层往上排除。
先复现一次本机连接
在终端 A 只监听回环地址,避免把实验端口暴露到局域网:
| |
-l(listen,监听):等待客户端连接。不加-l时,nc默认作为客户端,连接后面指定的地址和端口。127.0.0.1:本机 IPv4 回环地址。9009:监听端口。
括号中的英文用于助记,不代表这些短选项都有对应的长选项。本文没有使用 -u(UDP),因此默认使用 TCP;开头的 nc -h(help,帮助)则是查看帮助。
在终端 B 写一行,再对发送方向做半关闭:
| |
printf 'hello\n':输出hello和一个换行。|:把左侧命令的输出交给nc的标准输入。-N:OpenBSD nc 的选项,标准输入读到 EOF(End Of File,输入结束)后关闭 socket 的写入方向,仍可读取对端发送的内容。手册没有给出N的英文全称,可按行为记为 shutdown after EOF。-w 2(wait,等待):把连接建立和连接空闲的超时设为 2 秒,并不保证命令总共只运行 2 秒;监听模式下它也不限制等待客户端的时间。
终端 A 会看到 hello;这个单次监听没有使用 -k,会话结束后退出。Debian 12 容器中,这条发送命令和随后关闭端口时的拒绝连接均已验证。
检查目标端口
| |
-zv 是 -z -v 的合写:
-z(zero-I/O,零应用数据收发助记):只检查能不能建立连接,不发送应用数据;TCP 握手仍然会发包。-v(verbose,详细输出):显示连接成功或失败的诊断信息。-w 2:超时含义同上。
先重新启动终端 A 的监听可观察成功,关闭后可观察 Connection refused。一次成功只证明从该客户端可以和目标地址、端口建立 TCP 连接。拒绝通常表示目标没有监听或中间设备主动拒绝;超时可能是流量被丢弃、目标不可达或回包路径有问题。单凭一个结果不能定位到具体防火墙。也不要先用 -z 检查一次性实验监听,再以为下一条发送命令还能连上同一个监听进程。
端口正确,但监听地址不对
另开一个终端,用 -k 保留监听,便于连续测试。这里显式只监听 IPv4 回环:
| |
-4:强制使用 IPv4。-6:强制使用 IPv6,用于下面的 IPv6 检查。-k(keep listening,保持监听):与-l配合,让nc在一次连接结束后继续接受后续连接。
IPv6 的回环地址是 ::1,对应 IPv4 的 127.0.0.1。
从同一台机器分别检查 IPv4 和 IPv6,再看内核的监听地址:
| |
ss 用来查看 socket 状态。这里的 -ltn 合并了三个选项,后面跟一个过滤表达式:
-l/--listening(listening,监听中):只显示监听中的 socket。-t/--tcp:只看 TCP。-n/--numeric(numeric,数字形式):用数字显示地址和端口,不解析成主机名或服务名。'sport = :9009':只保留本地端口为9009的条目;引号让 shell 把整个表达式作为一个参数传给ss。
注意,ss -l 是筛选已有监听,nc -l 是创建监听。
在同时启用 IPv4 和 IPv6 的 Linux 上,第一条能连,第二条通常被拒绝,ss 只显示 127.0.0.1:9009,没有 [::1]:9009。如果系统禁用了 IPv6,第二条可能直接报告地址族不可用。服务只绑定回环时,宿主之外的客户端也不能借此访问它。遇到“本机能连、另一台机器不能连”,先对照服务端监听地址和客户端实际连接的地址;域名同时有 A、AAAA 记录时,可分别用 nc -4 和 nc -6 验证。容器内外也要分别检查,容器的 127.0.0.1 是它自己的回环。检查结束后用 Ctrl+C 停掉这个监听。
连上之后,发送应用协议要求的数据
如果目标是明文 HTTP,可以先在回环上起一个只服务测试目录的 HTTP 服务。在终端 A 执行,目录中只放测试文件:
| |
HTTP 服务命令中的参数是:
-m http.server(module,模块):运行 Python 自带的 HTTP 服务模块。8000:监听端口。--bind 127.0.0.1(bind,绑定):绑定回环地址,限制为本机访问。--directory "$lab"(directory,目录):指定服务的文件目录。
其余 shell 语法负责创建和清理实验目录,后面的 TLS 实验复用同样的方式:
( ... ):在子 shell 中运行这些命令。set -e:命令失败时停止实验。mktemp -d(directory,目录):创建临时目录,模板中的XXXXXX会替换成随机字符。$PWD、lab=$(...):前者是当前目录,后者把命令输出的新目录路径保存到变量lab中。trap ... EXIT:在子 shell 退出时执行清理命令。rm -rf -- "$lab":递归删除该目录,其中-r表示递归,-f不提示确认,--表示后面都是路径。>:把输出写入文件。"$lab":变量外的双引号避免路径中的空格被拆开。
终端 B 发两条完整的 HEAD 请求。最后的空行结束请求头;Connection: close 便于读完本次响应后关闭连接。
| |
for 循环依次把 / 和 /not-found 赋给 path。请求中的各部分含义是:
%s:printf的占位符,替换成"$path"的值。HEAD:只取响应头,不取响应正文。HTTP/1.1:请求使用的协议版本。Host: localhost:指定要访问的主机。Connection: close:要求服务端在本次响应后关闭连接。\r\n:HTTP 要求的回车加换行。末尾的\r\n\r\n表示请求头结束;如果少了最后的空行,服务端可能继续等待输入。
Debian 12 容器中,第一条收到 HTTP/1.0 200 OK,第二条收到 HTTP/1.0 404 File not found。两条请求都完成了 TCP 和 HTTP 往返,区别在资源路径。如果反向代理返回 404,可能是代理本身生成了响应,不能据此断言请求到达了上游;应按请求走过的每一跳核对状态或日志。完成后在终端 A 按 Ctrl+C 停止测试服务。
TCP 可达,也可能卡在 TLS
下面用 OpenSSL 在回环上起一个 TLS 服务,证书只为这次实验签发,不安装到系统信任库。终端 A 执行,记下打印出的目录:
| |
这里有两条 OpenSSL 命令。第一条 openssl req 生成实验用的私钥和证书,参数分别是:
-x509:直接生成自签名证书,而不是证书签名请求。-newkey rsa:2048:新建一把 2048 位的 RSA 私钥。-nodes:不加密输出的私钥,避免启动服务时交互输入私钥密码;它不表示关闭 TLS 加密。-days 1:证书有效期为 1 天。-subj '/CN=localhost':设置证书主题,其中 CN 是 Common Name(通用名称)。-addext 'subjectAltName=DNS:localhost':添加 SAN(Subject Alternative Name,主题备用名称),声明证书适用于 DNS 名称localhost,供客户端校验。-keyout key.pem、-out cert.pem:分别指定私钥文件和证书文件的输出路径。
第二条 openssl s_server 启动 TLS 测试服务:
-accept 127.0.0.1:9443:指定监听地址和端口。-cert cert.pem:指定服务端使用的证书。-key key.pem:指定服务端使用的私钥。-www:在收到 HTTP 请求后返回内置的状态页面,供后面的 curl 请求使用。-quiet:减少会话和证书信息的输出。
命令末尾的 \ 只是 shell 的续行符,让一条命令可以分多行写。cd "$lab" 进入实验目录,printf '%s\n' "$PWD" 打印该目录的路径。
终端 B 进入打印的目录。-servername 是发给服务端的 SNI,用于选择站点;-verify_hostname 是客户端要求证书匹配的名字。这里保持 SNI 不变,只换校验名称:
| |
openssl s_client 是 TLS 诊断客户端,其余参数的作用是:
-brief:简要显示协商的 TLS 版本、加密套件和证书校验结果。-connect 127.0.0.1:9443:指定实际连接的 IP 和端口。-servername localhost:发送 SNI(Server Name Indication,服务器名称指示),让服务端知道客户端要访问哪个站点;它不负责证书名称校验。-verify_hostname "$name":要求证书适用于变量name中的主机名,循环依次检查localhost和wrong.example。-verify_return_error:遇到证书校验错误就中止连接;否则s_client默认可能在报告校验错误后继续。-CAfile cert.pem:把本次实验的自签名证书作为信任依据,不修改系统信任库。
最后的 </dev/null 是 shell 的输入重定向,让客户端立即读到 EOF,不等待键盘输入。这个例子只检查 TLS 握手和证书,没有发送 HTTP 请求。
nc 能连接,校验 localhost 也能成功,换成 wrong.example 则会报 hostname mismatch 并返回非零状态。这个反例说明 TCP 通了,还要继续检查证书链、名称和客户端的 TLS 设置。不要用“跳过证书校验后能打开”当作证书已修复的证据。
若要排除 DNS 的影响并完成一次 HTTPS 请求,可以用 curl 的 --resolve 给本次请求指定 IP,同时保留 URL 中的主机名。仍在上面的证书目录执行:
| |
这条命令中:
--noproxy localhost:访问localhost时不走代理,避免代理环境变量改变实验路径。--resolve localhost:9443:127.0.0.1:按主机名:端口:IP的格式,为这次调用指定解析结果;URL 中仍保留localhost。--cacert cert.pem:使用指定证书文件作为信任依据,对应前面的 OpenSSL-CAfile。-sS:合写-s(silent,静默)和-S(show error,显示错误),隐藏进度信息,但在请求失败时仍显示错误。-o /dev/null/--output(output,输出):丢弃响应正文,避免内置状态页面干扰观察。-w '%{http_code}\n'/--write-out(write out,写出):传输结束后输出 HTTP 状态码和一个换行。curl 的-w是输出格式选项,与nc -w的超时含义不同。https://localhost:9443/:使用 HTTPS,访问localhost的9443端口和根路径/。
预期输出 200。这个请求既连到指定 IP,又用 localhost 做 SNI 和证书名称校验;把 URL 直接写成 IP,比较的就不是同一次请求了。实际排查虚拟主机时,把示例里的主机名、端口和 IP 换成有权限验证的目标即可。完成后停止测试服务。
上面的方法都依赖 TCP 握手。nc -uz 中的 -u 把传输协议改为 UDP,-z 仍表示探测模式;UDP 没有握手,所以命令没报错也不能证明对端收到了数据,要靠应用响应或抓包验证。
参考:OpenBSD nc(1)、Debian Bookworm nc.openbsd(1)、Linux connect(2)、ss(8)、Python http.server、OpenSSL req、s_server、s_client、curl 参数手册。