Samba MSDFS在mDNS自动发现环境下的踩坑记录

目的和现状

已有一台Samba Server server,现在希望新增一台Samba Server newserver,并将原有Share的一部分目录引向新Server,从而减少东西向通信、广播节点数量和客户端配置的风险。

旧服务器部署在同用户隔离的网段,无法广播,网络架构也不建议使用NetBIOS,因此通过avahi-daemon在用户网段广播服务。

新旧server的服务均已经在防火墙上放通,只是由于网段隔离,客户端无法直接用samba自带的体系发现服务,需要通过mDNS。客户端有macOS、Windows、Linux、iOS。

调试

加入如下配置:

[global]
host msdfs = true
[sharename]
msdfs root = true
msdfs proxy = \newserver\sharename

测试发现macOS本身可以看到新的共享,但是点击后提示无法连接。

抓包:

failed to connect

macOS侧的错误原因由于没有安装调试Profile,无法在Console中确认。但点击后,可以在服务端日志中立刻发现这一的错误是这个。起初走了一些弯路,以为是msdfs配置方式不对,改成软链接配置,情况依旧:

Feb 25 20:46:05 server rpcd_classic[3919]: [2025/02/25 20:46:05.961408,  0] ../../source3/printing/printer_list.c:58(get_printer_list_db)
Feb 25 20:46:05 server rpcd_classic[3919]:   get_printer_list_db: Failed to open printer_list.tdb
Feb 25 20:46:15 server smbd[3906]: [2025/02/25 20:46:15.148529,  0] ../../source3/smbd/msdfs.c:120(parse_dfs_path_strict)
Feb 25 20:46:15 server smbd[3906]:   parse_dfs_path_strict: Hostname server._smb._tcp.local is not ours.
Feb 25 20:46:15 server smbd[3906]: [2025/02/25 20:46:15.181912,  0] ../../source3/smbd/msdfs.c:120(parse_dfs_path_strict)
Feb 25 20:46:15 server smbd[3906]:   parse_dfs_path_strict: Hostname server._smb._tcp.local is not ours.
Feb 25 20:46:15 server smbd[3906]: [2025/02/25 20:46:15.215110,  0] ../../source3/smbd/msdfs.c:107(parse_dfs_path_strict)
Feb 25 20:46:15 server smbd[3906]:   parse_dfs_path_strict: can't parse hostname from path \server._smb._tcp.local

注意到日志中samba说一个名字不是自己。检查源码,这里lp开头的函数都是加载配置的参数。从源码中可以看到这里的行为是,对归一化后的服务器名称,检查:

  • 是否是配置的netbios name(全等和equal都测一轮)
  • 是否是配置的netbios aliaes
  • 是否是localhost或本地地址
  • 检查是否是dns名称
  • 检查是否是自己的某个ip
  • 尝试解析域名后,检查是否是自己的某个ip

如果都没有命中,就挂掉。而他这里检查的这个名字,是 ${service_name_configed_in_avahi}._smb._tcp.local ,确实和域控、网络侧配置的域名、主机名都不匹配,这是一个mDNS形式的完整域名。于是尝试在global段加配置:

[global]
netbios aliases = server._smb._tcp.local

此时referral就正常发放了,客户端也开始尝试链接:

can get referral after abuse nbns alias

到这里可以达成的初步结论是,由于Samba服务器没有配置好别名,在收到客户端的IPC请求时,认为请求的目标并不是自己,从而拒绝回复。

不过,这里有一些疑问:macOS可以通过mDNS发现Samba共享服务器,但Windows不可以。这是否代表系统中存在其他问题?

在Windows 10 22H2虚拟机中,将操作系统切换到私有网络模式,点击资源管理器中的网络菜单,除了NBNS和LLMNR外,没有看到其他的服务发现类请求。在具有Hyper V虚拟机的Windows 11 24H2虚拟机中,同上。卸载Hyper V后依然如此。顺便还发现了Windows默认会把所有VLAN的流量剥离标签后聚到一起,6。

查了下资料,看来确实大家都不支持,看来这个特性就是还没做。

另外:这个测试是在macOS下进行的。如果是在Windows/Linux下,效果会有所不同吗?

在Windows下,由于没有mDNS发现,需要手动输入机器名来访问,那么自然不存在这个问题:

windows can certainly get it

可以看到在IPC构建的过程中,就使用了server,这个名字和主机名是一样的,因此后续可以被认出来。在Linux下就不测试了,一方面是现在没有需求,另一方面是我们更想解决下面这个问题:

这个新的别名server._smb._tcp.local,是一个mDNS域名,某种程度上,这其实并不符合我们的预期,是否可以修改这个行为?

一开始我认为,可能存在一个配置项,配置好就可以了。找到macOS Samba客户端的源码 ,读了一下,发现事情比想象的麻烦一点。理论上我应该是对照着协议文本来读,但为了偷懒,而且不想看微软的spec,所以就直接去看源码了。

从源码中可以确认,苹果的Samba库,预期输入就是一个mDNS风格的域名,这一点在单元测试中就可以看到:

/* Try Bonjour name */
error = list_shares_once("smb://smbtest:smbtest@homedepot._smb._tcp.local");
if (error) {
	fprintf(stdout, "homedepot._smb._tcp.local: Share test failed %d\n", error);
	ErrorCnt++;
}

再看samba库对外提供的接口。从mount_smbfs工具的源码中,可以看到,对外提供的接口是:

/*!
 * @function SMBOpenServerEx
 * @abstract Connect to a SMB tree with options
 * @param targetServer A UTF-8 encoded UNC name or SMB URL.
 * @param outConnection Filled in with a SMBHANDLE if the connection is
 * successful.
 * @param options SMB
 * @result Returns an NTSTATUS error code.
 */
SMBCLIENT_EXPORT
NTSTATUS
SMBOpenServerEx(
    const char *targetServer,
    SMBHANDLE * outConnection,
    uint64_t    options)
__OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_NA)
    ;

考虑到在代码库中和单测中都看到了mDNS URL的痕迹,因此可以认为这里无论如何预期传入的 targetServer 都应该是一个形如 smb://service._smb._tcp.local 的URL,那么唯一一个可以控制的地方就是options了。从源码后续对options的解析里,没有明显看到可以剥离后半段主机名的内容。不过,是否可能在内部实现中做了这个行为呢?

想来想去,还是直接去看DFS的客户端实现比较简单。但是在看客户端代码之前,我想首先知道,在出问题的这一轮请求中,什么地方有可能和这个主机名产生关联:

detailed referral packet content

从图里看,有这些可能:

  1. 在具体的ioctl请求中,携带了文件名,代表了DFS本身在host上的标识;
  2. 在smb头部,存在两个id
    1. session id,在会话协商后就会立刻拿到,用来区分不同会话
    2. tree id,在一次成功的tree connect(比如,链接到IPC$)后拿到,用来区分操作哪个共享下的东西

回头看了下服务端的报错,是出在dfs路径解析的。翻看服务器源码后,这里确实是去判断从path中拿到的hostname是否是当前机器的,所以问题应该是出现在客户端拼接DFS Referral请求中的文件名路径部分。客户端构造Referral请求的字符串有两个方法,分别是:

  • createReferralStringFromCString ,本质上就是在传入的字符串前加个斜杠(/)
  • createReferralString,这个逻辑稍微复杂一些,也更像正常的构造思路,是从传入的链接上下文中提取serverName, ct_origshare, mountPath,然后用斜杠拼到一起

到这两个方法的调用,都集中在checkForDfsReferral里,这里都是用第二种方法,从ctx中提取的。一路追踪ctx,可以找到serverName是在lib/smb/parse_url.c:SetServerFromURL中填充的:

/* The serverNameRef should always contain the URL host name or the Bonjour Name */
ctx->serverNameRef = serverNameRef;
if (ctx->serverNameRef == NULL)
    return EINVAL;

这里明示了serverName中会包含完整的Bonjour Name,因此应该是无法更改了。

总结

费了一些力气,得到些许结论:

  • 在没有域的环境下,用Samba+DFS技术搭建共享存储是可行的(当然,权限、凭据同步等问题需要操心下)
  • 截至目前,Windows是不支持从mDNS发现文件共享并链接的,如果需要选择一个技术,从而让所有系统都能自动发现文件共享,基本只能使用NetBIOS
  • 如果使用avahi或其他mDNS技术,并从macOS连接自动发现的共享,那么需要确保mDNS的完整域名($hostname._smb._tcp.local)是Samba服务器的别名之一,从而确保服务器可以认得自己。这个行为无法修改,是固化在双方代码中的行为

当然,在域环境、或者有wins/nbns/llmnr等其他技术的场景下,这里又是另一番风味(💩️)了……