一个本地服务间通信的接口,白天好好的,第二天运维把 socket 文件的权限从 777 改成 660,换成某个组的账号就连不上了。
改权限这个动作是对的,问题在于它管的是什么。很多人以为 socket 文件的权限位和普通文件一样决定「谁能连」,实际上它受目录权限约束、受 umask 影响,而对另一类地址则完全不起作用。
这几条边界在手册里都写着,只是分散在不同段落。
AF_UNIX 到底是什么?
手册的 NAME 段一句话:unix 是用于本地进程间通信的 socket。
DESCRIPTION 的第一段解释了地址形式:AF_UNIX(也叫 AF_LOCAL)socket 家族用于在同一台机器上高效地通信。传统上,Unix 域 socket 要么是未命名的,要么绑定到一个文件系统路径名(标记为 socket 类型)。Linux 还支持一个与文件系统无关的抽象命名空间。
这里要说清一件事:这本手册没有任何一句说 AF_UNIX 比 TCP 回环快,也没有「不经过网络协议栈」「零拷贝」这类表述。它只说了 "efficiently"。
所以「本地通信用 unix socket 比 127.0.0.1 快」这句话,属于工程上的观察,不是手册结论。方向大概率对,但不该被引成规范原文------尤其当你要在方案评审里用它做决策依据时。
三种 socket 类型,语义和你想的不一样?
手册列的合法类型有三个,其中第二个最反直觉:
SOCK_STREAM, for a stream-oriented socket; SOCK_DGRAM, for a datagram-oriented socket that preserves message boundaries (as on most UNIX implementations, UNIX domain datagram sockets are always reliable and don't reorder datagrams); and (since Linux 2.6.4) SOCK_SEQPACKET, for a sequenced-packet socket that is connection-oriented, preserves message boundaries, and delivers messages in the order that they were sent.
SOCK_DGRAM 在 Unix 域上总是可靠的,而且不会乱序。这和对 UDP 的印象完全相反------名字里都有 DGRAM,行为是两套。
| 类型 | 面向连接 | 保留消息边界 | 可靠性与顺序 | 像什么 |
|---|---|---|---|---|
| SOCK_STREAM | 是 | 否 | 可靠、有序 | TCP |
| SOCK_DGRAM | 否 | 是 | 总是可靠、不乱序 | 不像 UDP |
| SOCK_SEQPACKET | 是 | 是 | 可靠、有序 | 介于两者之间 |
| 任意类型 + OOB | --- | --- | 不支持带外数据 | 没有对应 |
第四行是另一处边界:Unix 域 socket 不支持带外数据的传输 ,MSG_OOB 在这里没有意义。手册在 DESCRIPTION 里直接写了两遍,一处泛指,一处专门指流式 socket。
如果你的代码是从 TCP 迁过来的,而里面用了 MSG_OOB 或者依赖「流里能插优先级数据」,迁到 unix socket 时这部分会失效。它不会报错得很明显,通常是数据根本收不到。
抽象地址和路径名地址,权限差在哪?
这是开头那个故障的答案。
地址结构体里 sun_path 是一个 108 字节的数组(Linux 上),手册区分了三种地址:路径名的、未命名的、以及抽象的。
抽象地址的判定方式很特别:sun_path[0] 是一个空字节(\0),就说明它是抽象的。名字由该地址结构长度覆盖的后续字节给出,名字里的空字节没有特殊含义。而且这个名字与文件系统路径名没有任何关系。
真正的差别在这段:
Socket permissions have no meaning for abstract sockets: the process umask(2) has no effect when binding an abstract socket, and changing the ownership and permissions of the object (via fchown(2) and fchmod(2)) has no effect on the accessibility of the socket.
抽象 socket 上,权限没有意义。umask 不起作用,改属主和权限也不影响谁能连。还有一句:抽象 socket 在所有打开引用都关闭后会自动消失。
对照路径名 socket 那一节,规则完全是另一套:
- 新建 socket 时,属主和属组按常规规则设定;socket 文件除了被 umask 关掉的位,其余权限全部打开;
- 在 Linux 上,连接一个流式 socket 对象需要该 socket 的写权限;
- 路径名 socket 遵循它所在目录的权限------目录没有写和执行权限时,创建 socket 会失败;
- 绑定一个文件名会创建一个文件系统对象,不再需要时必须由调用方自己
unlink。
所以开头那次改权限,改的确实是有效的那一层------因为服务用的是路径名 socket。而如果换成抽象地址,同样的 660 什么也不会改变,因为它根本不看。
这也解释了一类相反的困惑:有人把权限改成 777 仍然连不上,因为瓶颈在父目录的搜索权限,不在 socket 文件本身。
顺带说一个可移植性细节:sun_path 在 Linux 上是 108 字节,但手册提醒,写可移植程序时要记得有些实现只有 92 字节。路径太长时,不同系统的表现不一致。
SO_PEERCRED 为什么不能做实时认证?
这是个很容易踩的坑,因为它看起来正好是「拿到对端身份」的接口。
手册对 SO_PEERCRED 的描述是:这是一个只读 socket 选项,返回连接到本 socket 的对端进程凭据 。而返回的凭据,是调用 connect、listen 或 socketpair 那一刻生效的凭据。
关键就在于「那一刻」。
如果对端进程在连接之后调用了 setuid 降低了权限,或者改变了有效 UID,SO_PEERCRED 给你的仍然是连接建立时的旧值。它是一张快照,不是实时查询。
对认证场景,这意味着两件事:不能靠它判断「对端当前有没有降权」,也不能靠它做「连接期间权限变更后自动断开」这类策略。
需要实时的凭据传递,手册给的是另一条路:SCM_CREDENTIALS。它把凭据作为辅助数据随消息传递,并且有内核校验------发送方必须是发送方的真实 UID、有效 UID 或保存的设置用户 ID(除非有 CAP_SETUID),内核会检查发送方指定的值。
还有配套的 SO_PASSCRED:开启之后,每条后续收到的消息里都会带上发送方凭据。如果不显式指定 SCM_CREDENTIALS,默认包含发送方的 PID、真实 UID 和真实 GID。
一个是连接时的快照,一个是每条消息都可以重新给出的凭据,用途完全不同。
SCM_RIGHTS 传的是「文件」吗?
不是。手册专门纠正了这个说法。
SCM_RIGHTS 用来在进程之间发送一组打开的文件描述符。数据部分是一个整数数组。手册说,这个操作通常被称为「传递文件描述符」,但更准确地说,传递的是对一个打开的文件描述的引用;在接收进程里,用的很可能是另一个不同的文件描述符编号。
从语义上讲,这个操作等价于把文件描述符 dup 到另一个进程的文件描述符表里。
所以三件事要记住:
- 接收方拿到的 fd 号通常和发送方不同,别写死;
- 传过去的是共享的打开文件描述,文件偏移、状态标志是共用的;
- 数组有上限:内核常量
SCM_MAX_FD定义了长度上限,值是 253(Linux 2.6.38 之前是 255),超了sendmsg会以EINVAL失败。
还有一个坑在接收侧:如果接收辅助数据的缓冲区太小或者根本没给,辅助数据会被截断或丢弃,超出的那些文件描述符会在接收进程里被自动关闭。这种失败是静默的,你只会在后面用的时候发现 fd 少了几个。
那条最容易被忽略的规则是什么?
在流式 socket 上发送辅助数据,必须同时发送至少一个字节的真实数据。
手册写了两遍:在 Linux 上,这是成功通过流式 Unix 域 socket 发送辅助数据的必要条件;而通过数据报 socket 发送时,Linux 上不需要附带真实数据,但可移植的程序也应该带至少一个字节。
所以「只传 fd、不传内容」这种写法在流式 socket 上会失败。常见的绕法是发一个占位字节,接收侧读一个字节后忽略。
这也意味着,任何「辅助数据 + 零长度正文」的协议设计,在跨实现时都要重新验证一遍。
该选路径名还是抽象地址?
| 你的需求 | 建议 | 理由 |
|---|---|---|
| 需要按用户或组限制访问 | 路径名 | 权限和目录权限都生效 |
| 需要防止误连到别的实例 | 路径名 + 唯一路径 | 抽象名字也可能撞名 |
| 不想留下残留文件 | 抽象地址 | 引用全关自动消失 |
| 需要跨系统可移植 | 路径名 | 抽象命名空间是非可移植的 Linux 扩展 |
| 需要在容器间共享 | 路径名 + 挂载目录 | 抽象命名空间不跨 netns |
最后一行常被忽略:抽象 socket 落在网络命名空间里,容器有自己的 netns,宿主机上的抽象地址在容器里看不到。要做跨容器通信,只能走路径名加共享挂载。
清理这件事也要区分。路径名 socket 需要自己 unlink,而且有个经典问题:进程被 kill -9 时来不及清理,留下一个「文件还在但没有监听者」的 socket,下次启动 bind 会因为文件已存在而失败。
这些差别我列成一张对照表放在团队文档里,几个项目的部署说明各要一份。重复排版的部分用 AI墨衍 推过去,具体填什么值还得按各自的拓扑来。抽象地址没有这个烦恼,但它也有代价------调试时 ls 看不到它。
怎么验证权限到底卡在哪一层?
按从外到内的顺序查,不要跳步。
bash
# 1. 看 socket 文件本身的权限和属主
ls -l /run/app.sock
# 2. 看父目录的搜索权限(这一层最常被漏)
namei -l /run/app.sock
# 3. 用对端账号实际连一次,别用 curl 之类的工具猜
sudo -u other-user python3 -c \
'import socket; s=socket.socket(socket.AF_UNIX); s.connect("/run/app.sock")'
namei -l 会把路径上每一级目录的权限列出来,比 ls 直观。第二级报错通常是目录缺搜索位,而不是 socket 文件本身。
第三条的价值在于「用真实身份验证」。用 root 测永远是通的,用当前登录用户测只能证明自己这个身份可以。
本文引用的原文出自 Linux 手册页 unix(7) 的 DESCRIPTION、Address format 小节(pathname / unnamed / abstract 三种)、Pathname socket ownership and permissions 小节、Abstract sockets 小节,以及 SCM_RIGHTS、SCM_CREDENTIALS、SO_PEERCRED、SO_PASSCRED 各条。资料整理自 man-pages 项目文本。