]> git.ipfire.org Git - thirdparty/util-linux.git/commitdiff
lsfd: add TUN.DEVNETNS column
authorMasatake YAMATO <yamato@redhat.com>
Sun, 23 Nov 2025 07:18:48 +0000 (16:18 +0900)
committerMasatake YAMATO <yamato@redhat.com>
Sun, 23 Nov 2025 20:10:56 +0000 (05:10 +0900)
lsfd can show the name of the network device behind a file descriptor
pointing to a tun/tap device.

    # lsfd -Q 'SOURCE == "misc:tun"'
    COMMAND             PID       USER ASSOC  XMODE TYPE   SOURCE MNTID INODE NAME
    qemu-system-x86  846384       qemu    35 rw----  CHR misc:tun    36  1145 iface=vnet21
    pasta.avx2      1837933     yamato     8 rw---m  CHR misc:tun  2143  1145 iface=ens8191
    ...

This feature helps users inspect target processes, containers, and/or
VMs with tools such as tcpdump, wireshark, or ip-link.

However, I found a case where the device name was not sufficient.
pasta (https://passt.top/) provides networking for rootless
containers. It creates a tap device whose name matches the name of a
network device on the host:

    $ ip link show ens8191
    5: ens8191: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...

    $ ethtool -i ens8191 | head -1
    driver: atlantic

    $ podman exec 9fbbed215871 ip link show ens8191
    2: ens8191: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 65520 ...

    $ podman exec 9fbbed215871 ethtool -i ens8191 | head -1
    driver: tun

A name alone is not enough to identify a network device on the system.

With this change, lsfd reports the network namespace to which the
tun/tap device belongs:

    # lsfd -Q 'SOURCE == "misc:tun"' -oCOMMAND,PID,SOURCE,TUN.DEVNETNS,NAME
    COMMAND             PID   SOURCE TUN.DEVNETNS NAME
    qemu-system-x86  846384 misc:tun   4026531840 iface=vnet21 devnetns=4026531840
    pasta.avx2      1837933 misc:tun   4026536354 iface=ens8191 devnetns=4026536354
    ...

This change relies on the TUNGETDEVNETNS ioctl added in:

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=0c3e0e3bb623c3735b8c9ab8aa8332f944f83a9f

Signed-off-by: Masatake YAMATO <yamato@redhat.com>
lsfd-cmd/cdev.c
lsfd-cmd/lsfd.1.adoc
lsfd-cmd/lsfd.c
lsfd-cmd/lsfd.h
tests/expected/lsfd/mkfds-cdev-tun-domestic [moved from tests/expected/lsfd/mkfds-cdev-tun with 100% similarity]
tests/expected/lsfd/mkfds-cdev-tun-domestic-devnetns [new file with mode: 0644]
tests/expected/lsfd/mkfds-cdev-tun-foreign [new file with mode: 0644]
tests/expected/lsfd/mkfds-cdev-tun-foreign-devnetns [new file with mode: 0644]
tests/ts/lsfd/mkfds-cdev-tun

index fd20e38d17ee617bb570143ef27031ea4f81d6f1..f047ef1b47c813d1ca8f94ee08cd2a05f6c82a27 100644 (file)
  */
 
 #include "lsfd.h"
+#include "pidfd-utils.h"
+
+#include <sys/ioctl.h>         /* ioctl */
+#include <linux/if_tun.h>      /* TUNGETDEVNETNS */
 
 static struct list_head miscdevs;
 static struct list_head ttydrvs;
@@ -371,6 +375,7 @@ static struct cdev_ops cdev_misc_ops = {
  */
 struct tundata {
        const char *iff;
+       ino_t devnetns;         /* 0 implies no value given. */
 };
 
 static bool cdev_tun_probe(struct cdev *cdev)
@@ -406,7 +411,12 @@ static char * cdev_tun_get_name(struct cdev *cdev)
        if (tundata == NULL || tundata->iff == NULL)
                return NULL;
 
-       xasprintf(&str, "iface=%s", tundata->iff);
+       if (tundata->devnetns)
+               xasprintf(&str, "iface=%s devnetns=%llu",
+                         tundata->iff, (unsigned long long)tundata->devnetns);
+       else
+               xasprintf(&str, "iface=%s", tundata->iff);
+
        return str;
 }
 
@@ -426,6 +436,12 @@ static bool cdev_tun_fill_column(struct proc *proc  __attribute__((__unused__)),
        case COL_SOURCE:
                *str = xstrdup("misc:tun");
                return true;
+       case COL_TUN_DEVNETNS:
+               if (tundata && tundata->devnetns) {
+                       xasprintf(str, "%llu", (unsigned long long)tundata->devnetns);
+                       return true;
+               }
+               break;
        case COL_TUN_IFACE:
                if (tundata && tundata->iff) {
                        *str = xstrdup(tundata->iff);
@@ -447,6 +463,64 @@ static int cdev_tun_handle_fdinfo(struct cdev *cdev, const char *key, const char
        return 0;
 }
 
+#ifdef TUNGETDEVNETNS
+static int call_with_foreign_fd(pid_t target_pid, int target_fd,
+                               int (*fn)(int, void*), void *data)
+{
+       int pidfd, tfd, r;
+
+       pidfd = pidfd_open(target_pid, 0);
+       if (pidfd < 0)
+               return pidfd;
+
+       tfd = pidfd_getfd(pidfd, target_fd, 0);
+       if (tfd < 0) {
+               close(pidfd);
+               return tfd;
+       }
+
+       r = fn(tfd, data);
+
+       close(tfd);
+       close(pidfd);
+       return r;
+}
+
+static int cdev_tun_get_devnetns(int tfd, void *data)
+{
+
+       struct tundata *tundata = data;
+       int nsfd = ioctl(tfd, TUNGETDEVNETNS);
+       struct stat sb;
+
+       if (nsfd < 0)
+               return -1;
+
+       if (fstat(nsfd, &sb) == 0)
+               tundata->devnetns = sb.st_ino;
+
+       close(nsfd);
+
+       return 0;
+}
+#endif
+
+static void cdev_tun_attach_xinfo(struct cdev *cdev)
+{
+       struct tundata *tundata = cdev->cdev_data;
+
+       if (tundata->iff == NULL)
+               return;
+
+#ifdef TUNGETDEVNETNS
+       {
+               struct file *file = &cdev->file;
+               call_with_foreign_fd(file->proc->pid, file->association,
+                                    cdev_tun_get_devnetns, tundata);
+       }
+#endif
+}
+
 static struct cdev_ops cdev_tun_ops = {
        .parent = &cdev_misc_ops,
        .probe = cdev_tun_probe,
@@ -454,6 +528,7 @@ static struct cdev_ops cdev_tun_ops = {
        .get_name = cdev_tun_get_name,
        .fill_column = cdev_tun_fill_column,
        .handle_fdinfo = cdev_tun_handle_fdinfo,
+       .attach_xinfo = cdev_tun_attach_xinfo,
 };
 
 /*
index 7c20c00de75c5c80c867fa7b24afbbb52b728fd1..195e54d7eb97f92ce593b16f1963715e61c60776 100644 (file)
@@ -307,7 +307,7 @@ inotify:::
 inodes=_INOTIFY.INODES_
 +
 misc:tun:::
-iface=_TUN.IFACE_
+iface=_TUN.IFACE_[ devnetns=_TUN.DEVNETNS_]
 +
 NETLINK:::
 protocol=_NETLINK.PROTOCOL_[ lport=_NETLINK.LPORT_[ group=_NETLINK.GROUPS_]]
@@ -525,6 +525,9 @@ Remaining time.
 PTMX.TTY-INDEX <``number``>::
 TTY index of the counterpart.
 
+TUN.DEVNETNS <``number``>::
+Inode identifying network namespace where the device belongs.
+
 TUN.IFACE <``string``>::
 Network interface behind the tun device.
 
index 64b9a27b3f4bdc40ccfb747052a153934a8e3a9e..18d1c12a3e1469fd5abcf736f15bbcb47dfe6865 100644 (file)
@@ -406,6 +406,9 @@ static const struct colinfo infos[] = {
        [COL_TIMERFD_REMAINING]= { "TIMERFD.REMAINING",
                                   0,   SCOLS_FL_RIGHT, SCOLS_JSON_FLOAT,
                                   N_("remaining time") },
+       [COL_TUN_DEVNETNS]     = { "TUN.DEVNETNS",
+                                  0,   SCOLS_FL_RIGHT, SCOLS_JSON_NUMBER,
+                                  N_("inode identifying network namespace where the device belongs") },
        [COL_TUN_IFACE]        = { "TUN.IFACE",
                                   0,   SCOLS_FL_RIGHT, SCOLS_JSON_STRING,
                                   N_("network interface behind the tun device") },
index 916ad2d76525bf3404e0fab3b5a9266bcc287b5c..ea9bf5e70fc71d35fa3ddc233b1ed78797536e64 100644 (file)
@@ -131,6 +131,7 @@ enum {
        COL_TIMERFD_CLOCKID,
        COL_TIMERFD_INTERVAL,
        COL_TIMERFD_REMAINING,
+       COL_TUN_DEVNETNS,
        COL_TUN_IFACE,
        COL_TYPE,
        COL_UDP_LADDR,
diff --git a/tests/expected/lsfd/mkfds-cdev-tun-domestic-devnetns b/tests/expected/lsfd/mkfds-cdev-tun-domestic-devnetns
new file mode 100644 (file)
index 0000000..291bc7d
--- /dev/null
@@ -0,0 +1 @@
+TUN.DEVNETNS: 0
diff --git a/tests/expected/lsfd/mkfds-cdev-tun-foreign b/tests/expected/lsfd/mkfds-cdev-tun-foreign
new file mode 100644 (file)
index 0000000..5217d79
--- /dev/null
@@ -0,0 +1,4 @@
+3 rw- CHR misc:tun
+ASSOC,MODE,TYPE,SOURCE: 0
+NAME: 0
+TUN.IFACE: 0
diff --git a/tests/expected/lsfd/mkfds-cdev-tun-foreign-devnetns b/tests/expected/lsfd/mkfds-cdev-tun-foreign-devnetns
new file mode 100644 (file)
index 0000000..291bc7d
--- /dev/null
@@ -0,0 +1 @@
+TUN.DEVNETNS: 0
index 028b420013db9f54b20c1eeeea40ecaa47141ffd..1079167fb14ccdda77ef1d4c642f4a26e0ad692c 100755 (executable)
@@ -25,40 +25,65 @@ ts_skip_nonroot
 
 ts_check_test_command "$TS_CMD_LSFD"
 ts_check_test_command "$TS_HELPER_MKFDS"
+ts_check_test_command "$TS_CMD_UNSHARE"
+ts_check_test_command "$TS_CMD_LSNS"
 
 ts_cd "$TS_OUTDIR"
 
 PID=
 FD=3
 IFNAME=
+readonly MYNETNS=$($TS_CMD_LSNS -n -t net -p $$ -oNS)
+
+if [[ -z "$MYNETNS" ]]; then
+    ts_skip "the current netns is unknown"
+fi
 
 cdev_tun_test()
 {
-    local tname=domestic
+    local unshare=$1
+    local tname
+    local devnetns_available
+    local netns
+
+    if [[ -z "$unshare" ]]; then
+       tname=domestic
+       netns=$MYNETNS
+    else
+       tname=foreign
+    fi
 
     ts_init_subtest "$tname"
     {
-       coproc MKFDS { "$TS_HELPER_MKFDS" cdev-tun $FD ; }
+       coproc MKFDS { $unshare "$TS_HELPER_MKFDS" cdev-tun $FD ; }
 
        if read -u ${MKFDS[0]} PID IFNAME; then
            EXPR='(FD == '"$FD"')'
            ${TS_CMD_LSFD} -p "${PID}" -n -o ASSOC,MODE,TYPE,SOURCE -Q "${EXPR}"
            echo 'ASSOC,MODE,TYPE,SOURCE': $?
 
+           if [[ -z "$netns" ]]; then
+               netns=$($TS_CMD_LSNS -n -t net -p $PID -oNS)
+           fi
+
            output=$(${TS_CMD_LSFD} -p "${PID}" -n --raw -o NAME -Q "${EXPR}")
-           if [[ "$output" == "iface=$IFNAME" ]]; then
+           if [[ "$output" =~ "iface=$IFNAME"(\\x20devnetns=$netns)? ]]; then
                echo 'NAME': $?
+               if [[ -n "${BASH_REMATCH[1]}" ]]; then
+                   devnetns_available=yes
+               fi
            else
                echo 'NAME': $?
                echo "expected NAME: iface=$IFNAME"
                echo "output NAME: $output"
+               echo "netns: $netns"
            fi
 
            output=$(${TS_CMD_LSFD} -p "${PID}" -n --raw -o TUN.IFACE -Q "${EXPR}")
            if [[ "$output" == "$IFNAME" ]]; then
                echo 'TUN.IFACE': $?
            else
-               echo 'TUN.IFAEC': $?
+               echo 'TUN.IFACE': $?
                echo "expected TUN.IFACE: $IFNAME"
                echo "output TUN.IFACE: $output"
            fi
@@ -66,6 +91,24 @@ cdev_tun_test()
     } > $TS_OUTPUT 2>&1
     ts_finalize_subtest
 
+    ts_init_subtest "$tname"-devnetns
+    if [[ -z "${devnetns_available}" ]]; then
+       ts_skip_subtest "no method to access devnetns on this platform"
+    else
+       {
+           output=$(${TS_CMD_LSFD} -p "${PID}" -n --raw -o TUN.DEVNETNS -Q "${EXPR}")
+           if [[ "$output" == "$netns" ]]; then
+               echo 'TUN.DEVNETNS': $?
+           else
+               echo 'TUN.DEVNETNS': $?
+               echo "expected TUN.DEVNETNS: $netns"
+               echo "output TUN.DEVNETNS: $output"
+           fi
+       } > $TS_OUTPUT 2>&1
+    fi
+    ts_finalize_subtest
+
+
     if [[ -n "$PID" ]]; then
        echo DONE >&"${MKFDS[1]}"
     fi
@@ -74,5 +117,6 @@ cdev_tun_test()
 }
 
 cdev_tun_test
+cdev_tun_test "$TS_CMD_UNSHARE" --net
 
 ts_finalize