Linux
如何找到執行bind()但不執行listen()的應用程序/埠?
當有故障的應用程序
bind()
使用 TCP 套接字呼叫某個埠P
但沒有跟上 時listen()
,該埠P
不在打開的埠中列出,即netstat
或ss
不ls /proc/net/tcp
顯示,但該埠已被佔用,其他應用程序無法使用該埠。是否有合理的方法可以找到此類應用程序和此類埠?
在提供更合適的東西之前,這裡有一個答案,它嘗試以絕對非工業的方式查找
bind(2)
在 TCP 套接字上使用的程序,但隨後既沒有listen(2)
也沒有connect(2)
,也可以顯示綁定的 TCP 地址是什麼。需要在大多數發行版
getfattr
中命名的包中找到,attr
加上kernel >= 3.7以過濾掉非 TCP 套接字,以及最小的安裝gdb
(例如在 Debian: 上gdb-minimal
)。不需要開發環境。應該以root使用者身份執行(否則它只會找到相同使用者的資訊,但這甚至無法跨容器工作)。請參閱最後的注意事項。成分:
- 第一個 shell 腳本模擬了一部分會
lsof
做的事情,但僅適用於這種特定情況。在所有程序中搜尋套接字 FD。對於具有屬性TCP
或的套接字TCPv6
(可作為文件元屬性system.sockprotoname
使用getfattr
,如發現的lsof
那樣getxattr(2)
以至少顯示它是 TCP 套接字的方式使用),檢查是否可以找到(sockfs偽文件系統的)inode在它們各自的網路命名空間tcp
或tcp6
proc文件中,如果沒有,則將 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()
函式或某些符號定義不可用)。- 由於沒有可用的調試資訊,大多數功能都明確限定了範圍,並且如果不進行更改,這可能無法在所有環境中執行(在核心 5.10.x的amd64架構上、在 Debian Bullseye、Debian 10 和 CentOS 7 使用者空間上進行了測試)。
- 同樣,與通常 glibc 之外的其他 libc 連結的二進製文件也可能無法按原樣工作。
- 是侵入性的,可能會使脆弱的(尤其是多執行緒的)應用程序崩潰。檢查未完成(例如:
malloc(3)
’s 或getsockname(2)
’ 失敗)。最後一個腳本認為sockfs inode 是全域(而不是每個網路命名空間)唯一的,我沒有試圖證明這一點,但使腳本更簡單。