使用 FUSE 将网络统计信息映射到目录
简介
FUSE 是一款优秀的 Linux 工具,普通用户可以轻松实现文件系统并在用户空间运行。让我们来体验一下它,实现一个可以显示网络统计数据的简单文件系统!我们将使用procfs
一个文件系统来获取系统统计数据。
为了方便编写,我们将使用 GO 语言。
您可以立即尝试,下载代码库,从源代码构建,然后将文件系统挂载到如下目录:
$ git clone https://github.com/r4dx/netstatfs
$ cd netstatfs
$ go build
$ mkdir m
$ ./netstatfs --mount=m&
$ ls m
m
文件夹将包含目录——每个目录代表一个进程,每个文件夹内将包含文件——每个文件代表一个套接字连接。
现在,既然我们知道了目标,让我们仔细看看我们将要使用的工具。
保险丝
FUSE,即用户空间文件系统 (Filesystem in Userspace),是一个软件接口,允许用户创建自己的文件系统,而无需编写驱动程序。要访问 FUSE 中的文件或目录,程序首先需要向位于用户空间的 FUSE 库发送请求。然后,该库会通过 FUSE 设备文件向内核发送自己的请求。内核会将请求转发到用户空间的文件系统实现(我们将要实现它!)。
当然,这只是对 FUSE 的一个非常简短的描述。您可以通过访问以下资源来深入了解 FUSE 的详细信息:
- FUSE 官方网站提供有关该技术、其设计和工作方式的详细信息:https://github.com/libfuse/libfuse
- 维基百科上有关 FUSE 的页面涵盖了其历史、设计和实现:https://en.wikipedia.org/wiki/Filesystem_in_Userspace
- Linux 上有关 FUSE 的手册页,提供有关在 Linux 环境中使用该工具的见解:https://man7.org/linux/man-pages/man4/fuse.4.html
- 网上还有几个关于 FUSE 的分步教程:** https://www.ibm.com/developerworks/library/l-fuse/index.html ** https://www.cs.nmsu.edu/~pfeiffer/fuse-tutorial/html/index.html
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)
}
}
我们来看看Netstatfs
结构:
type Netstatfs struct {
ProcessProvider ProcessProvider
SocketProvider SocketProvider
FileIdProvider FileIdProvider
RootINode uint64
}
这些都是不言自明的 - 请注意,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
}
查看ReadAllDir
RootDir 结构的方法 - 它用于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
}
每个进程目录都作为该结构体的一个实例实现ProcessDir
。该ReadDirAll
方法返回与该进程关联的网络套接字。文件名的格式为<socket local address>:<socket local port>_<socket remote address>:<socket remote port>
。
type ProcessDir struct {
Root *Netstatfs
Process Process
INode uint64
}
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
}
请注意,通过实现该方法ProcessDir
进行连接。RootDir
Lookup
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
}
现在,我们已经了解了 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
}
结果是给定进程的所有套接字的列表,其中包含有关每个套接字的详细信息。
结论
希望以上内容能帮助您快速了解 FUSE 的强大功能。如果您进一步阅读源代码,您会发现,我们甚至可以毫不费力地使用 PCAP 库来实现读取每个“文件”(在本例中为套接字)内容的功能。
鏂囩珷鏉ユ簮锛�https://dev.to/r4dx/using-fuse-to-map-network-statistics-to-directories-3c1b