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 操作,在文件描述符仍然有效时:
- 判断当前文件句柄是否被标记为修改过;
- 排除目录、路径引用句柄和命名流;
- 读取扩展属性名称配置;
- 获取当前连接的 Unix 用户名;
- 写入文件扩展属性;
- 调用下一层 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
测试脚本主要验证:
- 能够通过 SMB 上传文件;
- 文件在服务端物理目录中存在;
- 文件存在扩展属性
user.samba.last_writer; - 当前测试环境下,属性值为
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 注册模块 |
首先实现一个最小框架:
拦截文件关闭操作,但暂时不增加其他行为,直接传递到下一层。

3.3 注册模块构建目标
为了让新增模块能够编译为 lastwriter.so,需要修改构建配置:
- 在
wscript_build中注册构建目标; - 在
wscript中调整模块启用配置。
本次在对应配置中增加 vfs_lastwriter,并将其设为默认启用。


至此,源码层面的最小可编译框架已经完成,后续功能在此基础上扩展。
构建配置和 VFS 接口可能随 Samba 版本变化,具体写法应以当前源码树中的现有模块为准。
3.4 编译模块
进入 Samba 源码目录,先进行构建配置:
./configure
然后编译目标模块:
make -j1 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.c 和 vfs_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,验证通过。

七、总结
本次实践完成了一个最小可用的 Samba VFS 扩展模块,串联了以下流程:
编写黑盒测试
↓
搭建 VFS 模块框架
↓
注册构建目标
↓
编译和安装动态库
↓
配置共享并加载模块
↓
拦截文件关闭操作
↓
写入扩展属性
↓
执行运行验证
通过这次实践,我对 Samba VFS 调用链、文件句柄生命周期、扩展属性操作以及模块构建方式有了更具体的认识。
目前我刚开始学习 Linux 系统编程,本文记录的是一次入门实践。如有理解不准确或实现不完善的地方,欢迎批评指正,我也会继续补充测试、完善边界处理,并持续学习。