Samba VFS 模块开发实践:实现 vfs_lastwriter

前言

在完成对 Samba 框架的整体学习,并能够描述各模块功能之后,我开始尝试进行功能扩展。

Samba 提供了灵活的 VFS 模块机制,开发者可以在文件操作调用链中增加处理逻辑,再将操作传递给后续模块,最终由默认实现调用操作系统接口。

本次参考现有 vfs_* 模块的实现方式,新增一个名为 vfs_lastwriter 的插件,用于记录通过 Samba 修改文件的用户信息。

选择这个功能作为入门实践,主要是因为它的目标明确、实现范围较小,能够帮助我快速熟悉以下开发流程:

  • Samba VFS 模块的基本结构;
  • VFS 操作拦截与调用链传递;
  • 模块构建与安装;
  • 文件扩展属性的读写;
  • 黑盒测试与运行验证。

一、功能目标与实现思路

1.1 功能目标

文件治理系统需要查询:

最后是谁通过 Samba 修改了某个文件?

本模块将用户信息写入文件扩展属性:

user.samba.last_writer

这样,查询方不需要检索历史日志,可以直接读取文件上的扩展属性。

当前实现是在被标记为修改过的文件句柄关闭前,记录对应连接的 Unix 用户名。因此,它更准确地表示“最近执行记录操作的修改句柄所属用户”。在多用户并发写入时,关闭顺序不一定等于最后一次写入的顺序。

1.2 实现思路

模块拦截 VFS 的 close 操作,在文件描述符仍然有效时:

  1. 判断当前文件句柄是否被标记为修改过;
  2. 排除目录、路径引用句柄和命名流;
  3. 读取扩展属性名称配置;
  4. 获取当前连接的 Unix 用户名;
  5. 写入文件扩展属性;
  6. 调用下一层 VFS 的 close,完成关闭操作。

简化后的调用链如下:

SMB 文件关闭流程
        ↓
vfs_lastwriter:执行 lastwriter_close()
        ↓
SMB_VFS_NEXT_CLOSE()
        ↓
后续 VFS 模块(如果存在)
        ↓
vfs_default.c
        ↓
Linux 系统调用
        ↓
内核 VFS 与具体文件系统

需要注意,Samba VFS 是用户态的模块接口,不等同于 Linux 内核中的 VFS。

二、配置共享目录

smb.conf 中为测试共享增加以下配置:

[devshare]
    vfs objects = lastwriter
    lastwriter:xattr name = user.samba.last_writer

配置含义:

配置项 说明
vfs objects = lastwriter 为当前共享加载 lastwriter 模块
lastwriter:xattr name 指定保存用户信息的扩展属性名称

如果没有配置 lastwriter:xattr name,模块默认使用:

user.samba.last_writer

上述内容只是与模块相关的配置片段,共享路径、访问权限和访客映射等配置需要结合实际测试环境设置。如果共享已经加载其他 VFS 模块,应保留原有模块,并评估模块排列顺序。

三、主要开发步骤

3.1 编写测试脚本

创建:

learning/test_lastwriter.sh

测试脚本主要验证:

  1. 能够通过 SMB 上传文件;
  2. 文件在服务端物理目录中存在;
  3. 文件存在扩展属性 user.samba.last_writer
  4. 当前测试环境下,属性值为 nobody

脚本如下:

#!/usr/bin/env bash

set -u

ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)

SMBCLIENT=/debug/prefix/bin/smbclient
SHARE_DIR=/tmp/kylin-samba-fresh/share
CLIENT_DIR=/tmp/kylin-samba-fresh/client

REMOTE_FILE="lastwriter-$$.txt"
PAYLOAD="$CLIENT_DIR/$REMOTE_FILE.input"
XATTR_NAME="user.samba.last_writer"

. "$ROOT/src/testprogs/blackbox/subunit.sh"

failed=0

mkdir -p "$CLIENT_DIR"
printf 'lastwriter module test\n' > "$PAYLOAD"

cleanup()
{
    rm -f \
        "$PAYLOAD" \
        "$SHARE_DIR/$REMOTE_FILE"
}

trap cleanup EXIT

upload_file()
{
    "$SMBCLIENT" \
        //127.0.0.1/devshare \
        -p 445 \
        -N \
        -m SMB3 \
        -c "put $PAYLOAD $REMOTE_FILE"
}

physical_file_exists()
{
    test -f "$SHARE_DIR/$REMOTE_FILE"
}

last_writer_is_nobody()
{
    python3 - \
        "$SHARE_DIR/$REMOTE_FILE" \
        "$XATTR_NAME" <<'PY'
import os
import sys

path = sys.argv[1]
name = sys.argv[2]

try:
    value = os.getxattr(path, name).decode()
except OSError as error:
    print(f"unable to read {name}: {error}")
    raise SystemExit(1)

if value != "nobody":
    print(f"expected nobody, got {value!r}")
    raise SystemExit(1)
PY
}

testit "upload lastwriter test file" \
    upload_file ||
    failed=$((failed + 1))

testit "physical file exists" \
    physical_file_exists ||
    failed=$((failed + 1))

testit "last writer xattr is nobody" \
    last_writer_is_nobody ||
    failed=$((failed + 1))

testok "$0" "$failed"

这里通过 Samba 自带的 subunit.sh 组织黑盒测试,并通过退出清理函数删除测试文件。

-N 表示不提示输入密码,并不保证服务端用户一定是 nobody。本测试预期为 nobody,依赖当前共享的访客访问和用户映射配置。

3.2 创建 VFS 模块源文件

新增源文件:

src/source3/modules/vfs_lastwriter.c

参考 vfs_worm.c 的组织方式,将模块划分为三个部分:

组成部分 本模块对应内容 作用
业务函数 lastwriter_close() 处理文件关闭前的附加逻辑
VFS 函数表 lastwriter_fns 注册需要拦截的 VFS 操作
模块初始化入口 vfs_lastwriter_init() 向 Samba 注册模块

首先实现一个最小框架:

拦截文件关闭操作,但暂时不增加其他行为,直接传递到下一层。

VFS 模块最小框架

3.3 注册模块构建目标

为了让新增模块能够编译为 lastwriter.so,需要修改构建配置:

  • wscript_build 中注册构建目标;
  • wscript 中调整模块启用配置。

本次在对应配置中增加 vfs_lastwriter,并将其设为默认启用。

在 wscript_build 中注册模块

在 wscript 中启用模块

至此,源码层面的最小可编译框架已经完成,后续功能在此基础上扩展。

构建配置和 VFS 接口可能随 Samba 版本变化,具体写法应以当前源码树中的现有模块为准。

3.4 编译模块

进入 Samba 源码目录,先进行构建配置:

./configure

然后编译目标模块:

make -j1 vfs_lastwriter

当前阶段优先确保模块能够编译运行,后续再完善构建参数和环境管理。

编译 vfs_lastwriter 模块

3.5 安装模块

将生成的动态库安装到测试环境的 VFS 模块目录:

install -m 0755 \
    bin/modules/vfs/lastwriter.so \
    /debug/prefix/lib/vfs/lastwriter.so

安装后,在对应共享的 smb.conf 配置中启用模块。

四、功能实现细节

4.1 判断文件是否被修改

参考 vfs_commit.cvfs_virusfilter.c,检查:

fsp->fsp_flags.modified

当文件句柄被标记为修改过时,才尝试记录用户信息。

同时跳过以下情况:

fsp->fsp_flags.is_directory
fsp->fsp_flags.is_pathref
is_named_stream(fsp->fsp_name)

对应含义:

判断条件 处理方式
未被标记为修改 不更新扩展属性
目录 不处理
路径引用句柄 不处理
命名流 不处理

modified 标志具体覆盖哪些修改操作,需要结合当前 Samba 版本的源码和测试确认,不能仅凭该字段假定所有内容变更路径都已覆盖。

4.2 读取扩展属性配置

通过以下接口读取共享配置:

xattr_name = lp_parm_const_string(
    SNUM(handle->conn),
    MODULE_NAME,
    "xattr name",
    "user.samba.last_writer");

对应配置:

lastwriter:xattr name = user.samba.last_writer

最后一个参数是默认值。

4.3 获取 Unix 用户名

从连接的会话信息中读取:

handle->conn->session_info->unix_info->unix_name

使用前检查:

  • session_info 是否为空;
  • unix_info 是否为空;
  • unix_name 是否为空或空字符串。

这里记录的是 Unix 用户名,不一定能够唯一对应原始 SMB 登录账号。访客映射等配置可能让多个客户端映射到同一个 Unix 用户。

4.4 在关闭前写入扩展属性

参考 vfs_fake_acls.c,调用:

SMB_VFS_NEXT_FSETXATTR(
    handle,
    fsp,
    xattr_name,
    unix_name,
    strlen(unix_name),
    0);

参数含义:

参数 说明
handle 当前 VFS 模块句柄
fsp 当前文件对象
xattr_name 扩展属性名称
unix_name 要保存的用户名
strlen(unix_name) 属性值长度,不包含字符串结尾的 \0
0 属性不存在时创建,存在时覆盖

写入发生在真正关闭文件之前,确保后续模块仍能使用有效的文件句柄。

4.5 错误处理策略

当前采用“尽力记录、不影响正常关闭”的策略:

  • 扩展属性写入失败时记录日志;
  • 不因为记录失败而跳过文件关闭;
  • 最终返回下一层 close 的结果。

同时,在扩展属性操作前保存 errno,操作后恢复,避免附加操作遗留的错误值干扰后续流程。

五、完整模块代码

/*
 * Store the last Unix user that modified a file.
 */

#include "includes.h"
#include "smbd/smbd.h"
#include "system/filesys.h"

#define MODULE_NAME "lastwriter"

static int lastwriter_close(vfs_handle_struct *handle,
                            files_struct *fsp)
{
    const char *xattr_name = NULL;
    const char *unix_name = NULL;

    int ret;
    int saved_errno;
    int xattr_errno;

    if (!fsp->fsp_flags.modified ||
        fsp->fsp_flags.is_directory ||
        fsp->fsp_flags.is_pathref ||
        is_named_stream(fsp->fsp_name)) {
        goto close_file;
    }

    xattr_name = lp_parm_const_string(
        SNUM(handle->conn),
        MODULE_NAME,
        "xattr name",
        "user.samba.last_writer");

    if (handle->conn->session_info == NULL ||
        handle->conn->session_info->unix_info == NULL) {
        goto close_file;
    }

    unix_name =
        handle->conn->session_info->unix_info->unix_name;

    if (unix_name == NULL || unix_name[0] == '\0') {
        goto close_file;
    }

    if (xattr_name == NULL || xattr_name[0] == '\0') {
        goto close_file;
    }

    saved_errno = errno;

    ret = SMB_VFS_NEXT_FSETXATTR(
        handle,
        fsp,
        xattr_name,
        unix_name,
        strlen(unix_name),
        0);

    if (ret == -1) {
        xattr_errno = errno;

        DEBUG(1, ("lastwriter: failed to set %s on %s: %s\n",
                  xattr_name,
                  fsp->fsp_name->base_name,
                  strerror(xattr_errno)));
    } else {
        DEBUG(10, ("lastwriter: file=%s, user=%s, xattr=%s\n",
                   fsp->fsp_name->base_name,
                   unix_name,
                   xattr_name));
    }

    errno = saved_errno;

close_file:
    return SMB_VFS_NEXT_CLOSE(handle, fsp);
}

static struct vfs_fn_pointers lastwriter_fns = {
    .close_fn = lastwriter_close,
};

static_decl_vfs;

NTSTATUS vfs_lastwriter_init(TALLOC_CTX *ctx)
{
    return smb_register_vfs(
        SMB_VFS_INTERFACE_VERSION,
        MODULE_NAME,
        &lastwriter_fns);
}

六、编译与运行验证

6.1 安装编译后的模块

install -m 0755 \
    /workspace/kylin-samba/src/bin/modules/vfs/lastwriter.so \
    /debug/prefix/lib/vfs/lastwriter.so

模块应使用与测试 smbd 匹配的源码和构建环境生成,避免 VFS 接口或动态库版本不匹配。

6.2 启动测试服务

设置测试环境的动态库搜索路径:

export LD_LIBRARY_PATH=/debug/prefix/lib:/debug/prefix/lib/private:/usr/lib64

以前台模式启动 smbd

/debug/prefix/sbin/smbd \
    -F \
    --no-process-group \
    --debug-stdout \
    -d 2 \
    -s /debug/etc/smb.conf \
    -p 445

主要参数:

参数 说明
-F 前台运行
--no-process-group 不创建新的进程组
--debug-stdout 将调试信息输出到标准输出
-d 2 设置调试级别
-s 指定配置文件
-p 445 指定监听端口

启动前应确认测试环境中的 445 端口没有被其他 Samba 服务占用。模块中的成功日志使用 DEBUG(10),如果需要观察这条日志,应在受控测试环境中提高调试级别。

6.3 执行测试脚本

在另一个终端中执行:

./learning/test_lastwriter.sh

如果脚本没有执行权限:

chmod +x learning/test_lastwriter.sh
./learning/test_lastwriter.sh

6.4 验证结果

本次测试验证了以下流程:

SMB 上传文件
    ↓
服务端文件存在
    ↓
文件关闭前写入扩展属性
    ↓
读取 user.samba.last_writer
    ↓
属性值为 nobody

当前测试配置下,用户信息被正确记录为 nobody,验证通过。

vfs_lastwriter 黑盒测试结果

七、总结

本次实践完成了一个最小可用的 Samba VFS 扩展模块,串联了以下流程:

编写黑盒测试
    ↓
搭建 VFS 模块框架
    ↓
注册构建目标
    ↓
编译和安装动态库
    ↓
配置共享并加载模块
    ↓
拦截文件关闭操作
    ↓
写入扩展属性
    ↓
执行运行验证

通过这次实践,我对 Samba VFS 调用链、文件句柄生命周期、扩展属性操作以及模块构建方式有了更具体的认识。

目前我刚开始学习 Linux 系统编程,本文记录的是一次入门实践。如有理解不准确或实现不完善的地方,欢迎批评指正,我也会继续补充测试、完善边界处理,并持续学习。