地方エンジニアの学習日記

興味ある技術の雑なメモだったりを書いてくブログ。たまに日記とガジェット紹介。

【Linux】lsofの使わずにポート開いてるプロセスを特定

lsofの仕組み?

Linuxでプロセスがオープンしてるlsofコマンドだが仕組みを知らずに使ってきた。 せっかくだから調べてみるも情報があまりないので自分用にまとめ。

lsofコマンドとは

プロセスが開いているファイルを表示するコマンドです。 GitHub上でもソースが公開されているし比較的欲しい情報が手に入りやすいやつです。

github.com

サーバプログラム作成

任意のポートで待ち受けるプロセスを生成するためのプログラムを作成。 実行時にpid.ppid,portを出力しポートに対して,ソケット生成、bind,listen,acceptをします。

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define QUEUELIMIT 5

int print_pid(unsigned short srvport) {
    pid_t pid, ppid;
    pid = getpid();
    ppid = getppid();
    fprintf(stdout, "pid=%d\nppid=%d\nport=%d\n", pid, ppid,srvport);
    return 0;
}

int main(int argc, char* argv[]) {

    int servSock;
    int clitSock;
    struct sockaddr_in servSockAddr;
    struct sockaddr_in clitSockAddr;
    unsigned short servPort;
    unsigned int clitLen;

    if ( argc != 2) {
        fprintf(stderr, "argument count mismatch error.\n");
        exit(EXIT_FAILURE);
    }

    if ((servPort = (unsigned short) atoi(argv[1])) == 0) {
        fprintf(stderr, "invalid port number.\n");
        exit(EXIT_FAILURE);
    }

    if ((servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0 ){
        perror("socket() failed.");
        exit(EXIT_FAILURE);
    }

    print_pid(servPort);
    memset(&servSockAddr, 0, sizeof(servSockAddr));
    servSockAddr.sin_family      = AF_INET;
    servSockAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servSockAddr.sin_port        = htons(servPort);

    if (bind(servSock, (struct sockaddr *) &servSockAddr, sizeof(servSockAddr) ) < 0 ) {
        perror("bind() failed.");
        exit(1);
    }

    if (listen(servSock, QUEUELIMIT) < 0) {
        perror("listen() failed.");
        exit(1);
    }

    while(1) {
        clitLen = sizeof(clitSockAddr);
        if ((clitSock = accept(servSock, (struct sockaddr *) &clitSockAddr, &clitLen)) < 0) {
            perror("accept() failed.");
            exit(1);
        }

        printf("connected from %s.\n", inet_ntoa(clitSockAddr.sin_addr));
      close(clitSock);
    }

    return 0;
}

ソケット確認

プログラムを実行したら指定したポート番号でソケットファイルを探しに行きます。 ソケットのinodeを取得するためには「/proc/net/tcp」を見ていきます。

(procファイルシステムについてはぜひ下記を。。。!)

qiita.com

tcpファイルのヘッダ部分はこんな感じです。

sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode

sl : ソケットのカーネルハッシュスロットの値 local_address : ローカルアドレスとポート(16進数なので注意) rem_address : リモートアドレスとポート st : ソケットの内部状態 tx_queue : カーネルメモリを消費している 送信サイズ rx_queue : カーネルメモリを消費している 送信サイズ inode : inode

で、このヘッダの中で今回使用するのがlocal_addressとinode。 local_addressでポート番号を使ってinodeを特定していく。

# 今回は9000番ポートを想定してるので9000->0x2328
[root@cnt08 net]# cat /proc/net/tcp | grep ":2328"
   3: 00000000:2328 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 4298572 1 00000000555aa095 100 0 0 10 0

使用しているinodeは4298572であることが特定できました。 inode4298572をopenしているプロセスを次はprocファイルシステムのfdをもとに探していきます。

[root@cnt08 net]# ls -dl /proc/[1-9]*/fd/* | grep 4298572
lrwx------. 1 root   root   64 10月 21 22:18 /proc/21872/fd/3 -> socket:[4298572]

ここで見えるのがpid21872です lsofコマンドで見てみると

[root@cnt08 net]# lsof -i:9000
COMMAND   PID USER   FD   TYPE  DEVICE SIZE/OFF NODE NAME
a.out   22100 root    3u  IPv4 4301160      0t0  TCP *:cslistener (LISTEN)

同じ結果です。

github.com

lsofのそのへんの話はドキュメントにもありました。

lsofっぽいものを雑に実装

ポート番号からプロセスの特定の仕組みは分かったのでpythonで雑に実装

#!/usr/bin/python3

import os
import sys
import re

TCP_PROC_FILE="/proc/net/tcp"

def get_inode(hx):
    """
    渡されたポートを使用しているソケットファイルを特定しinodeを返却する
    """
    inode_num = 0
    with open(TCP_PROC_FILE, mode="r") as fd:
        for line in fd:
            ll = line.strip().split()[1].split(":")
            if len(ll) == 1:
                continue
            if hx == int(ll[1], 16):
                inode_num = line.strip().split()[9]
    return inode_num if inode_num else None


def get_dir_list(path):
    """
    渡されたパス配下のディレクトリをリストで返す
    """
    files = os.listdir(path)
    return [f for f in files if os.path.isdir(os.path.join(path, f))]


def get_hardlink_list(proc_dir_list, inode_num):
    for path in proc_dir_list:
        for files in os.listdir(f"/proc/{path}/fd"):
            try:
                r = os.readlink(f"/proc/{path}/fd/{files}")
                if "socket" in r and r[8:-1] == inode_num:
                    print(path, r, r[8:-1])
            except FileNotFoundError as e:
                # Todo : エラー処理
                pass


def main(argv):
    pattern = "[0-9]*$"
    inode_num = get_inode(int(argv[1]))
    if inode_num is None:
        print("but port")
        sys.exit(1)

    path = "/proc/"
    proc_dir_list = [f for f in get_dir_list(path) if re.match(pattern, f)]
    l = get_hardlink_list(proc_dir_list, inode_num)


if __name__ == "__main__":
    main(sys.argv)

1プロセスが1ポートを使ってるだけだと面白みもないので上で書いたサーバプログラムをマルチプロセスにして 2プロセスで9000番ポートをLISTEしている状態を作ります。

[root@cnt08 ~]# lsof -i:9000
COMMAND   PID USER   FD   TYPE  DEVICE SIZE/OFF NODE NAME
a.out   24491 root    3u  IPv4 4439589      0t0  TCP *:cslistener (LISTEN)
a.out   24492 root    3u  IPv4 4439589      0t0  TCP *:cslistener (LISTEN)

この状態で上のpythonを実行。第一フィールドがPIDなので一致してます。

[root@cnt08 ~]# python3 a.py 9000
24491 socket:[4439589] 4439589
24492 socket:[4439589] 4439589

特に面白みも無いのでkill機能をつけて見ました。 今後も改良すればちょっと使えそうな気がしてきた。