INET ドメインソケットの状態をCプログラムで取得する

Linux上で動作するプログラムの中で現在のTCPセッションの状態を取得する処理をC言語で書く必要がありました.

シェルスクリプトなら ss netstat を呼ぶことになりますが, C 言語でTCPセッション、あるいは現在の INET ドメインソケットの状態を知る方法を調べてみました.

/proc/net/tcp

現在のTCPセッションの状態を /proc/net/tcp から取得することができます.

/proc/net/tcp は以下の様な形式となります.

netstat の出力はこの情報をもとにしているようです.

テキストベースのデータですので、sed, awk などのスクリプト言語で処理するには都合が良いですが, CあるいはC++で処理するにはちょっと面倒です.

他の方法が無いかと調べました.

sock_diag

NETLINK netlink_familyNETLINK_SOCK_DIAG を選択することによって様々なプロトコル・ファミリーのソケット情報をカーネルから得ることができます.

形式は以下の通り.

#include <sys/socket.h>
#include <linux/sock_diag.h>
#include <linux/unix_diag.h> /* for UNIX domain sockets */
#include <linux/inet_diag.h> /* for IPv4 and IPv6 sockets */

diag_socket = socket(AF_NETLINK, socket_type, NETLINK_SOCK_DIAG);

NETLINK_SOCK_DIAG の man page, sock_diag(7) にその詳細が述べられてます.

sock_diag(7) EXAMPLES には UNIX ドメイン (AF_UNIX) ソケットの情報を取得するサンプルコードが掲載されてます.

INET ドメイン (AF_INET) ソケットの情報を取得するサンプルコードが無かったので、AF_UNIX ドメインソケットのサンプルコードをベースに書いてみました.

INET ドメインソケットの状態を取得するコード

例としてすべての TCP の INET ドメインソケットの状態を取得するサンプルプログラムです.

#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <linux/sock_diag.h>
#include <linux/inet_diag.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>

static int
send_query (int fd)
{
  struct sockaddr_nl nladdr = {
    .nl_family = AF_NETLINK
  };
  struct
  {
    struct nlmsghdr nlh;
    struct inet_diag_req_v2 idr;
  } req = {
    .nlh = {
	    .nlmsg_len = sizeof (req),
	    .nlmsg_type = SOCK_DIAG_BY_FAMILY,
	    .nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP},
    .idr = {
	    .sdiag_family = AF_INET,
            .sdiag_protocol = IPPROTO_TCP,
	    .idiag_ext = 0,
	    .pad = 0,
	    .idiag_states = UINT32_MAX,
	    .id = {},
	    }
    };
  struct iovec iov = {
    .iov_base = &req,
    .iov_len = sizeof (req)
  };
  struct msghdr msg = {
    .msg_name = (void *) &nladdr,
    .msg_namelen = sizeof (nladdr),
    .msg_iov = &iov,
    .msg_iovlen = 1
  };

  for (;;)
    {
      if (sendmsg (fd, &msg, 0) < 0)
	{
	  if (errno == EINTR)
	    continue;

	  perror ("sendmsg");
	  return -1;
	}
      return 0;
    }
}

static int
print_diag (const struct inet_diag_msg *diag, unsigned int len)
{
  char src_addr [32];
  char dst_addr [32];

  if (len < NLMSG_LENGTH (sizeof (*diag)))
    {
      fputs ("short response\n", stderr);
      return -1;
    }
  if (diag->idiag_family != AF_INET)
    {
      fprintf (stderr, "unexpected family %u\n", diag->idiag_family);
      return -1;
    }

  inet_ntop(AF_INET, &(diag->id.idiag_src), src_addr, sizeof (src_addr));
  inet_ntop(AF_INET, &(diag->id.idiag_dst), dst_addr, sizeof (dst_addr));

  printf ("src: %s, sport: %d, dst: %s, dport:%d, state:%d\n",
	  src_addr, ntohs (diag->id.idiag_sport), dst_addr, ntohs (diag->id.idiag_dport), diag->idiag_state);

  return 0;
}

static int
receive_responses (int fd)
{
  long buf[8192 / sizeof (long)];
  struct sockaddr_nl nladdr = {
    .nl_family = AF_NETLINK
  };
  struct iovec iov = {
    .iov_base = buf,
    .iov_len = sizeof (buf)
  };
  int flags = 0;

  for (;;)
    {
      struct msghdr msg = {
	.msg_name = (void *) &nladdr,
	.msg_namelen = sizeof (nladdr),
	.msg_iov = &iov,
	.msg_iovlen = 1
      };

      ssize_t ret = recvmsg (fd, &msg, flags);
      if (ret < 0)
	{
	  if (errno == EINTR)
	    continue;

	  perror ("recvmsg");
	  return -1;
	}
      if (ret == 0)
	return 0;

      const struct nlmsghdr *h = (struct nlmsghdr *) buf;
      if (!NLMSG_OK (h, ret))
	{
	  fputs ("!NLMSG_OK\n", stderr);
	  return -1;
	}

      for (; NLMSG_OK (h, ret); h = NLMSG_NEXT (h, ret))
	{
	  if (h->nlmsg_type == NLMSG_DONE)
	    return 0;
	  if (h->nlmsg_type == NLMSG_ERROR)
	    {
	      const struct nlmsgerr *err = NLMSG_DATA (h);

	      if (h->nlmsg_len < NLMSG_LENGTH (sizeof (*err)))
		{
		  fputs ("NLMSG_ERROR\n", stderr);
		}
	      else
		{
		  errno = -err->error;
		  perror ("NLMSG_ERROR");
		}
	      return -1;
	    }

	  if (h->nlmsg_type != SOCK_DIAG_BY_FAMILY)
	    {
	      fprintf (stderr, "unexpected nlmsg_type %u\n",
		       (unsigned) h->nlmsg_type);
	      return -1;
	    }

	  if (print_diag (NLMSG_DATA (h), h->nlmsg_len))
	    return -1;
	}
    }
}

int
main (void)
{
  int fd = socket (AF_NETLINK, SOCK_RAW, NETLINK_SOCK_DIAG);
  if (fd < 0)
    {
      perror ("socket");
      return 1;
    }
  int ret = send_query (fd) || receive_responses (fd);
  close (fd);
  return ret;
}

上記プログラムの実行結果はこのようになります.

TCPのセッション情報 idiag_states の値は以下の様に定義されてます.

enum
{
  TCP_ESTABLISHED = 1,
  TCP_SYN_SENT,
  TCP_SYN_RECV,
  TCP_FIN_WAIT1,
  TCP_FIN_WAIT2,
  TCP_TIME_WAIT,
  TCP_CLOSE,
  TCP_CLOSE_WAIT,
  TCP_LAST_ACK,
  TCP_LISTEN,
  TCP_CLOSING   /* now a valid state */
};

取得したIPアドレス、ポート番号はネットワークバイトオーダーになっているため、ホスト上で取り扱う際にそのマシンのバイトオーダーに変換が必要です.

任意のTCP状態のソケット状態を取得する方法

例えば TCP LISTEN 状態のソケット情報のみ取得したい場合、構造体 struct inet_diag_req_v2 idiag_states に取得したい状態を設定します.

struct inet_diag_req_v2 {
	__u8	sdiag_family;
	__u8	sdiag_protocol;
	__u8	idiag_ext;
	__u8	pad;
	__u32	idiag_states;
	struct inet_diag_sockid id;
};

ただ、取得したソケット情報を表示する idag_states と異なり、TCP状態の値を設定するのではなく、TCP状態に該当するビットをセットする点に注意が必要です.

sock_diag(7) には以下の様に書かれています.

This is a bit mask that defines a filter of socket states.
Only those sockets whose states are in this mask will be reported.
Ignored when querying for an individual socket.

idag_statesへの設定値は以下の Linux kernel code を参考になります.

enum {
	TCPF_ESTABLISHED = (1 << TCP_ESTABLISHED),
	TCPF_SYN_SENT	 = (1 << TCP_SYN_SENT),
	TCPF_SYN_RECV	 = (1 << TCP_SYN_RECV),
	TCPF_FIN_WAIT1	 = (1 << TCP_FIN_WAIT1),
	TCPF_FIN_WAIT2	 = (1 << TCP_FIN_WAIT2),
	TCPF_TIME_WAIT	 = (1 << TCP_TIME_WAIT),
	TCPF_CLOSE	 = (1 << TCP_CLOSE),
	TCPF_CLOSE_WAIT	 = (1 << TCP_CLOSE_WAIT),
	TCPF_LAST_ACK	 = (1 << TCP_LAST_ACK),
	TCPF_LISTEN	 = (1 << TCP_LISTEN),
	TCPF_CLOSING	 = (1 << TCP_CLOSING),
	TCPF_NEW_SYN_RECV = (1 << TCP_NEW_SYN_RECV),
};

任意のポート, アドレスのソケット状態を取得する方法

例えば dest port 12345 に関連したソケット情報のみ取得したい場合、構造体 struct inet_diag_req_v2 struct inet_diag_sockid id に取得したい状態を設定します.

    .idr = {
	    .sdiag_family = AF_INET,
            .sdiag_protocol = IPPROTO_TCP,
	    .idiag_ext = 0,
	    .pad = 0,
	    .idiag_states = UINT32_MAX,
	    .id = {
		   .idiag_dport = htons (12345),
		   },
	    }
    };

struct inet_diag_sockid id は以下のような構造です.

/* Socket identity */
struct inet_diag_sockid {
	__be16	idiag_sport;
	__be16	idiag_dport;
	__be32	idiag_src[4];
	__be32	idiag_dst[4];
	__u32	idiag_if;
	__u32	idiag_cookie[2];
#define INET_DIAG_NOCOOKIE (~0U)
};

上記の構造体の設定値はネットワークバイトオーダーとなる点に注意です.

NETLINK 関連情報が少ない

今回の NETLINK_SOCK_DIAG に限らず, NETLINK に関する情報が少ないです.

私の知る限り、ある程度まとまった情報が読める本はこれかなぁ、と.

それにもまして日本語で読める情報はさらに少ないのが現状は以前から述べてます.

日本語でNETLINKの情報が読める本はこれ以外知りません…

そのような状況に対して少しでも情報を提供できればと…少しずつ書いていきたいと思ってます.

2022.6.12 追伸

先に書いたサンプルプログラムを GitHub で公開しました.

微力ながら若いソフトウェア・エンジニアの方々のお役に立てればと…

では.

“INET ドメインソケットの状態をCプログラムで取得する” への1件の返信

コメントは受け付けていません。