Using FUSE to map network statistics to directories

2025-06-10

使用 FUSE 将网络统计信息映射到目录

简介

FUSE 是一款优秀的 Linux 工具,普通用户可以轻松实现文件系统并在用户空间运行。让我们来体验一下它,实现一个可以显示网络统计数据的简单文件系统!我们将使用procfs一个文件系统来获取系统统计数据。
为了方便编写,我们将使用 GO 语言。
您可以立即尝试,下载代码库,从源代码构建,然后将文件系统挂载到如下目录:

$ git clone https://github.com/r4dx/netstatfs
$ cd netstatfs
$ go build
$ mkdir m
$ ./netstatfs --mount=m&
$ ls m
Enter fullscreen mode Exit fullscreen mode

m文件夹将包含目录——每个目录代表一个进程,每个文件夹内将包含文件——每个文件代表一个套接字连接。
现在,既然我们知道了目标,让我们仔细看看我们将要使用的工具。

保险丝

FUSE,即用户空间文件系统 (Filesystem in Userspace),是一个软件接口,允许用户创建自己的文件系统,而无需编写驱动程序。要访问 FUSE 中的文件或目录,程序首先需要向位于用户空间的 FUSE 库发送请求。然后,该库会通过 FUSE 设备文件向内核发送自己的请求。内核会将请求转发到用户空间的文件系统实现(我们将要实现它!)。

当然,这只是对 FUSE 的一个非常简短的描述。您可以通过访问以下资源来深入了解 FUSE 的详细信息:

netstat 是如何做到的?

除了 FUSE 之外,我们的项目还使用了 proc 文件系统,这是一个 Linux 虚拟文件系统,提供有关系统及其上运行的进程的信息。我们将使用 procfs 来检索网络统计信息。值得注意的是,Linux netstat 命令使用 procfs 来检索网络统计信息。它的具体操作
如下

  • 它打开/proc/目录并读取其中的所有数字ID。这些ID代表当前在系统上运行的进程。
  • 对于每个进程,它会读取/proc/{processId}/fd/
  • 它解析套接字文件 ID 的链接,因此它们被包装成以下格式:socket:[socketInode][0000]:socketInode
  • 然后将 socketInode 到其对应进程的映射存储在pgr_hash哈希图中。
  • 最后,它读取 /proc/net/{tcp,tcp6,udp,udp6,igmp,igmp6,unix} 中包含 inode 字段的网络协议文件。然后,该字段用于从 pgr_hash 哈希表中检索进程。

值得注意的是,功能类似的 ss 命令使用的是 netlink 协议。更多信息,请参阅以下链接:https://man7.org/linux/man-pages/man7/sock_diag.7.html

深入代码

现在让我们仔细看看代码并逐步讨论它:

netstatfs.go使用该库实现了一个文件系统bazil.org/fuse。该文件系统公开了系统上运行的进程的网络连接信息。

main函数首先通过解析命令行参数来设置文件系统的挂载点。然后,它通过调用 创建与 FUSE 库的连接fuse.Mount。该NewNetstatfs函数创建 struct 的新实例Netstatfs,该实例充当文件系统的根节点。最后,该函数通过调用 将 Netstatfs 实例用作 FUSE 文件系统fs.Serve

func main() {
    mountpoint := flag.String("mount", "", "mountpoint for FS")
    flag.Parse()
    conn, err := fuse.Mount(*mountpoint, fuse.FSName("netstatfs"))
    if err != nil {
     panic(err)
    }
    defer conn.Close()
    netstatfs, err := NewNetstatfs()
    if err != nil {
     panic(err)
    }
    err = fs.Serve(conn, &netstatfs)
    if err != nil {
     panic(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

我们来看看Netstatfs结构:

type Netstatfs struct {
    ProcessProvider ProcessProvider
    SocketProvider  SocketProvider
    FileIdProvider  FileIdProvider
    RootINode       uint64
}
Enter fullscreen mode Exit fullscreen mode

这些都是不言自明的 - 请注意,FileIdProvider为文件系统中的每个节点提供唯一的文件 ID,并且RootINode是根节点的 inode 编号。

方法Root返回Netstatfs文件系统的根目录。根目录作为RootDir结构体的一个实例实现,该结构体具有返回其属性和目录内容的方法。根目录的内容是系统上运行的进程的名称,每个名称的格式为<process ID>_<process name>

func (me Netstatfs) Root() (fs.Node, error) {
    return RootDir{Root: &me}, nil
}

type RootDir struct {
    Root *Netstatfs
}
Enter fullscreen mode Exit fullscreen mode

查看ReadAllDirRootDir 结构的方法 - 它用于ProcessProvider获取正在运行的进程列表并将它们表示为文件系统中的目录:

func (me RootDir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
    processes, err := (*me.Root).ProcessProvider.GetProcesses()
    if err != nil {
        return nil, err
    }
    result := make([]fuse.Dirent, len(processes))
    for i, process := range processes {
        fn := processNameToFileName(
            process.Id, process.Name)
        inode, err := (*me.Root).FileIdProvider.GetByProcessId(process.Id)
        if err != nil {
            return nil, err
        }

        result[i] = fuse.Dirent{Inode: inode,
            Name: fn,
            Type: fuse.DT_Dir}
    }
    return result, nil
}
Enter fullscreen mode Exit fullscreen mode

每个进程目录都作为该结构体的一个实例实现ProcessDir。该ReadDirAll方法返回与该进程关联的网络套接字。文件名的格式为<socket local address>:<socket local port>_<socket remote address>:<socket remote port>

type ProcessDir struct {
    Root    *Netstatfs
    Process Process
    INode   uint64
}
Enter fullscreen mode Exit fullscreen mode
func (me ProcessDir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
    sockets, err := (*me.Root).SocketProvider.GetSockets(me.Process.Id)
    if err != nil {
        log.Printf("Couldn't get sockets for process=%d: %s\n", me.Process.Id, err)
        return nil, err
    }
    result := make([]fuse.Dirent, len(sockets))
    for i, socket := range sockets {
        inode, err := (*me.Root).FileIdProvider.GetBySocketId(me.Process.Id, socket.Id)
        if err != nil {
            log.Printf("Couldn't get fileid for process=%d, socket=%d: %s", me.Process.Id, socket.Id, err)
            return nil, err
        }
        result[i] = fuse.Dirent{Inode: inode,
            Name: socketToFileName(socket),
            Type: fuse.DT_File}
    }
    return result, nil
}
Enter fullscreen mode Exit fullscreen mode

请注意,通过实现该方法ProcessDir进行连接RootDirLookup

func (me RootDir) Lookup(ctx context.Context, name string) (fs.Node, error) {
    id, err := fileNameToProcessId(name)
    if err != nil {
        return nil, err
    }
    process, err := me.Root.ProcessProvider.GetProcessById(id)
    if err != nil {
        return nil, err
    }
    inode, err := (*me.Root).FileIdProvider.GetByProcessId(id)
    if err != nil {
        return nil, err
    }
    return ProcessDir{Root: me.Root,
        Process: process,
        INode:   inode}, nil
}
Enter fullscreen mode Exit fullscreen mode

现在,我们已经了解了 FUSE 的工作原理,接下来我们来快速看一下如何获取进程的打开套接字列表。
基本上就是这样GetSockets的:

func (me procfsSocketProvider) getProcessSocketInternal(processId uint, socketId uint64) (ProcessSocket, error) {
    file := strconv.FormatUint(uint64(processId), 10) + "/fd/" + strconv.FormatUint(socketId, 10)

    resolved, err := me.procfs.Readlink(file)
    if err != nil {
     return ProcessSocket{}, err
    }
    inode, err := me.getINodeIfSocket(resolved)
    if err != nil {
     return ProcessSocket{}, err
    }
    return ProcessSocket{INode: inode, ProcessId: processId,
     Id: socketId, SocketInfo: SocketInfo{}}, nil
}

func (me procfsSocketProvider) GetSockets(processId uint) ([]ProcessSocket, error) {
    base := strconv.Itoa(int(processId)) + "/fd/"

    files, err := me.procfs.Readdirnames(base)
    if err != nil {
     return nil, err
    }
    result := make([]ProcessSocket, 0)
    for _, file := range files {
     fd, err := strconv.ParseUint(file, 10, 64)
     if err != nil {
         continue
     }
     socket, err := me.getProcessSocketInternal(processId, fd)
     if err != nil {
         continue
     }
     result = append(result, socket)
    }
    err = me.fillSocketInfo(result)
    if err != nil {
     return nil, err
    }

    return result, nil
}
func (me procfsSocketProvider) getINodeIfSocket(src string) (uint64, error) {
    matches := me.socketINodeRe.FindStringSubmatchIndex(src)
    outStr := string(me.socketINodeRe.ExpandString([]byte{}, "$inode", src, matches))
    res, err := strconv.ParseUint(outStr, 10, 64)
    if err != nil {
     return 0, err
    }
    return res, nil
}

Enter fullscreen mode Exit fullscreen mode

结果是给定进程的所有套接字的列表,其中包含有关每个套接字的详细信息。

结论

希望以上内容能帮助您快速了解 FUSE 的强大功能。如果您进一步阅读源代码,您会发现,我们甚至可以毫不费力地使用 PCAP 库来实现读取每个“文件”(在本例中为套接字)内容的功能。

鏂囩珷鏉ユ簮锛�https://dev.to/r4dx/using-fuse-to-map-network-statistics-to-directories-3c1b
PREV
使用 pcap-filter 语法进行网络过滤
NEXT
如何将容器融入你的日常工作