当有故障的应用程序bind()
使用 TCP 套接字调用某个端口P
但没有跟随 时listen()
,该端口P
不会在开放端口中列出,即netstat
或ss
或ls /proc/net/tcp
不显示它,但该端口被占用,并且没有其他应用程序可以使用它。有没有合理的方法来找到这样的应用程序和这样的端口?
答案1
在出现更合适的东西之前,这里有一个答案,它尝试以绝对非工业的方式查找bind(2)
在 TCP 套接字上使用的进程,但随后既没有listen(2)
也没有connect(2)
,并且还可以显示绑定的 TCP 地址是什么。
需要getfattr
attr
在大多数发行版中指定的包中找到加上内核 >= 3.7过滤掉非 TCP 套接字,并最小化安装gdb
(例如在 Debian 上:gdb-minimal
)。不需要开发环境。应该运行为根用户(否则它只会找到相同用户的信息,但这甚至无法跨容器工作)。请参阅最后的注意事项。
成分:
第一个 shell 脚本模仿了部分功能
lsof
,但仅适用于这种特定情况。在所有进程中搜索套接字 FD。对于具有属性TCP
或的套接字TCPv6
(可用作文件元属性,system.sockprotoname
使用getfattr
,正如发现的lsof
那样会使用getxattr(2)
以这种方式至少显示它是一个 TCP 套接字),检查是否(袜子文件系统伪文件系统的)inode 可以在各自的网络命名空间中找到tcp
或tcp6
进程文件,如果没有,则将 pid、fd 和 inode 显示为候选 3-uple。仅此脚本就可以找到并列出“有缺陷”的进程。findbadtcpprocs.sh
:#!/bin/sh find /proc -mindepth 1 -maxdepth 1 -name '[1-9]*' | xargs -I{} find {}/fd -follow -type s 2>/dev/null | while read procfd; do type=$(getfattr --absolute --only-values -L -n system.sockprotoname $procfd | tr '\0' '\n') if [ "$type" = "TCP" -o "$type" = "TCPv6" ]; then inode=$(stat -L -c %i $procfd) pid=$(echo $procfd | cut -d/ -f3) if awk '$10 == inode { exit 1 }' inode=$inode /proc/$pid/net/tcp /proc/$pid/net/tcp6; then fd=$(echo $procfd | cut -d/ -f5) echo $pid $fd $inode fi fi done
该脚本可以独立使用,仅查找候选进程,而无需附加信息。
然后是一个
gdb
必须赋予正确的脚本FD信息。它附加在候选进程上,并将(首先分配一些内存)运行getsockname(2)
,显示绑定的套接字(并释放分配的资源)并释放进程。getsockname.gdb
:set $malloc=(void *(*)(long long)) malloc set $ntohs=(unsigned short(*)(unsigned short)) ntohs p $malloc(64) p $malloc(4) set *(long *)$2=64 p (int) getsockname($fd,$1,$2) set logging file /dev/stdout set logging on if *((short *) $1) == 2 set $ip=(unsigned char *) ($1+4) printf "%hu.%hu.%hu.%hu",$ip[0],$ip[1],$ip[2],$ip[3] else if *((short *) $1) == 10 set $ip6=(unsigned short *) ($1+8) printf "[%hx:%hx:%hx:%hx:%hx:%hx:%hx:%hx]",$ntohs($ip6[0]),$ntohs($ip6[1]),$ntohs($ip6[2]),$ntohs($ip6[3]),$ntohs($ip6[4]),$ntohs($ip6[5]),$ntohs($ip6[6]),$ntohs($ip6[7]) end end printf ":%hu\n",$ntohs(*(unsigned short *)($1+2)) set logging off call (void) free($2) call (void) free($1) quit
最后,胶水脚本使用之前的两个脚本以方便操作。它将避免无用地附加到共享同一套接字的多个进程(或线程)。
result.sh
:#!/bin/sh oldinode=-1 ./findbadtcpprocs.sh | sort -s -n -k 3 | while read pid fd inode; do printf '%d\t%d\t%d\t' $pid $fd $inode if [ $inode -ne $oldinode ]; then socketname=$(gdb -batch-silent -p $pid -ex 'set $fd'=$fd -x ./getsockname.gdb 2>/dev/null) || socketname=FAIL oldinode=$inode fi printf '%s\n' "$socketname" done
只需运行此命令即可提供所有内容:
chmod a+rx findbadtcpprocs.sh result.sh ./result.sh
作为奖励,C 源代码中的简单重现器将使用相同的 TCP 套接字创建两个进程,而无需使用
listen(2)
它。用法:gcc -o badtcpbind badtcpbind.c
和./badtcpbind 5555
badtcpbind.c
:#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <strings.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char *argv[]) { int sockfd; struct sockaddr_in myaddr; if (argc < 2) { exit(2); } if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket"); exit(1); } bzero(&myaddr, sizeof myaddr); myaddr.sin_family = AF_INET; myaddr.sin_addr.s_addr = INADDR_ANY; myaddr.sin_port = htons(atoi(argv[1])); if (bind(sockfd, (struct sockaddr *) &myaddr, sizeof myaddr) < 0) { perror("bind"); exit(1); } #if 0 listen(sockfd,5); #endif fork(); sleep(9999); }
例子:
# ./badtcpbind 5555 &
[1] 330845
# ./result.sh
108762 20 303507 0.0.0.0:0
330845 3 586443 0.0.0.0:5555
330846 3 586443 0.0.0.0:5555
(是的,由于某种未知的原因,libvirtd
这里出现一个进程来创建一个 TCP 套接字,但该套接字未被使用并被捕获在结果的第一行中)。
注意事项:
也许应该使用比 shell 更好的语言,以提高可读性和效率。
当然比 更有活力
lsof
。以此处完成的方式附加到正在运行的进程存在问题:
- 不适用于静态链接的二进制文件(
malloc()
此时函数或某些符号定义不可用)。 - 由于没有可用的调试信息,大多数函数都有明确的作用域,并且如果不进行更改,这可能无法在所有环境中运行(在AMD64内核 5.10.x 的架构,在 Debian bullseye、Debian 10 和 CentOS 7 用户空间上)。
- 同样,与通常的 glibc 相比,与其他 libc 链接的二进制文件可能无法按原样工作。
- 具有侵入性,可能会导致脆弱的(尤其是多线程)应用程序崩溃。检查未完成(例如:
malloc(3)
或getsockname(2)
的失败)。
- 不适用于静态链接的二进制文件(
最后一个脚本考虑袜子文件系统inode 是全局(而不是每个网络命名空间)唯一的,我没有尝试证明这一点,但使脚本更简单。