.sock 的魔力:为何现代基础设施依赖 Unix Domain Sockets
Source: Dev.to
/var/run/docker.sock – 用于控制 Docker。
agent.sock – 被 SPIRE 用来对工作负载进行身份验证。
这不仅仅是“因为它快”。还有一个决定性的安全原因:由操作系统内核提供的绝对身份保证。
在本文中我们将:
- 解释套接字的真实本质以及它们与 TCP 的关键区别。
- 用 Go 进行实验,直接从内核中提取 连接对等方的身份(PID/UID)。
套接字 vs. 文件
很多人被教导说“套接字被当作文件来处理”。当你用 ls -l 列出它们时,它们确实看起来像文件:
srw-rw---- 1 root docker 0 Jan 1 12:00 /var/run/docker.sock
文件大小始终为 0,因为“数据”并未存储在磁盘上;它仅仅是内核内存中通信端点(一个窗口)的地址簿条目。
Data‑flow Comparison
| Feature | TCP (INET) | UNIX Domain Socket (UDS) |
|---|---|---|
| Addressing | IP:Port (e.g., 127.0.0.1:8080) | 文件路径 (e.g., /tmp/app.sock) |
| Scope | 跨网络(远程) | 仅同一主机(本地) |
| Overhead | 高 – 协议头、校验和、路由表查找 | 极低 – 仅内存拷贝 |
| Access control | 防火墙(iptables)、TLS | 文件权限(chmod/chown) |
| Identity | 源 IP(可伪造) | PID/UID/GID(内核保证) |
- TCP – 即使是本地通信,数据也要经过完整的网络栈(数据包封装、校验和计算、路由)。
- UDS – 完全绕过网络栈;通信通过在内核内部拷贝缓冲区实现,因而 延迟极低。
在 TCP 中可以看到源 IP 地址,但 没有可靠的方法 知道是哪一个进程发起的连接(IP 可以被伪造)。
使用 UDS 时,服务器可以通过 SO_PEERCRED 套接字选项让内核 返回对等方的凭证。由于这些信息直接来源于内核内部表,客户端无法伪造。这也是 Zero‑Trust 系统(如 SPIRE)采用 UDS 的原因。
⚠️ Note:
SO_PEERCRED是 Linux 专有特性。它 不适用于 macOS 或 Windows。
演示:使用 Go 提取 PID/UID
下面是一个最小的服务器示例,它会打印对等方的 PID、UID 和 GID。
// main.go
package main
import (
"fmt"
"net"
"os"
"syscall"
)
func main() {
const socketPath = "/tmp/test.sock"
// Remove any stale socket file.
_ = os.Remove(socketPath)
// Start a UDS listener.
l, err := net.Listen("unix", socketPath)
if err != nil {
panic(err)
}
defer l.Close()
// Make the socket world‑writable for the experiment.
_ = os.Chmod(socketPath, 0o777)
fmt.Println("🕵️ Server is listening on", socketPath)
fmt.Println("waiting for connection...")
for {
conn, err := l.Accept()
if err != nil {
fmt.Println("accept error:", err)
continue
}
go handleConnection(conn)
}
}
func handleConnection(c net.Conn) {
defer c.Close()
// Convert net.Conn to *net.UnixConn to obtain the underlying file descriptor.
unixConn, ok := c.(*net.UnixConn)
if !ok {
fmt.Println("Not a unix connection")
return
}
file, err := unixConn.File()
if err != nil {
fmt.Println("Failed to get file descriptor:", err)
return
}
defer file.Close()
fd := int(file.Fd())
// Query the kernel for peer credentials.
ucred, err := syscall.GetsockoptUcred(fd, syscall.SOL_SOCKET, syscall.SO_PEERCRED)
if err != nil {
fmt.Println("Failed to get credentials:", err)
return
}
// Display the results.
fmt.Printf("\n[🚨 DETECTED]\n")
fmt.Printf(" - Connected by PID : %d\n", ucred.Pid)
fmt.Printf(" - User ID (UID) : %d\n", ucred.Uid)
fmt.Printf(" - Group ID (GID) : %d\n", ucred.Gid)
_, _ = c.Write([]byte("Identity Verified. closing.\n"))
}
运行演示(Linux / Docker)
# 1️⃣ Save the code above as main.go
# 2️⃣ Start a Linux container with Go installed.
docker run -it --rm -v "$PWD":/app -w /app golang:1.25 bash
# Inside the container:
# ── Start the server in the background
go run main.go &
# ── Connect with netcat (nc)
echo | sh -c 'echo "Client PID: $$"; exec nc -U /tmp/test.sock'
预期输出(服务器端)
🕵️ Server is listening on /tmp/test.sock
waiting for connection...
[🚨 DETECTED]
- Connected by PID : 757
- User ID (UID) : 0
- Group ID (GID) : 0
显示的 PID(示例中的 757)与执行连接的 nc 进程相匹配——这证明了内核能够保证对等方的身份。
Real‑World Use: SPIRE & Docker
- 访问
docker.sock实际上相当于 root 级别,因为 Docker 守护进程是以 root 身份运行的。 - SPIRE 的 agent 在其 Unix 套接字上使用
SO_PEERCRED来验证请求确实来自预期的工作负载(例如 Pod)。
SPIRE 做的不仅仅是读取 PID:
- 调用
watcher.IsAlive()来防御 PID 重用攻击(进程死亡后,其 PID 被重新分配,攻击者尝试冒充)。 - 将 PID 传递给 Workload Attestor 插件(Docker、Kubernetes 等)。这些插件会把 PID 转换为更丰富的属性,如容器 ID、Pod 标签等。
在 Windows 上,SPIRE 使用 命名管道 并采用类似的机制:即使底层操作系统不同,内核仍然保证客户端的身份。
要点
.sock(Unix 域套接字)不是“旧技术”——它提供高性能 以及 内核验证的身份。SO_PEERCRED为你提供一个可信的 PID/UID/GID 对,客户端无法伪造。- 这种能力是云原生环境中许多 Zero‑Trust(零信任)设计的基石(Docker、SPIRE 等)。
随意尝试上面的代码,并请记住,安全保证仅在支持 SO_PEERCRED 的 Linux 上有效。祝玩得开心!
性能
绕过网络栈。
最强安全
在内核层面的身份保证(Peer Cred)。
简单访问控制
利用文件系统权限。
结合这些特性,UDS 仍然是现代容器基础设施中支持 “last one mile of host communication” 的关键组件,在该环境中需要零信任安全。