登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  linux

systemd socket 激活服务的依赖与监听配置

来源:17golang原创

时间:2026-09-28 22:51:10 229浏览 收藏

systemd socket 激活的核心不是“先启动服务再监听”,而是由 .socket unit 先创建监听对象,连接到来时再启动 .service,并把文件描述符交给守护进程。默认情况下,demo-api.socket 会激活同名的 demo-api.service;名称不同时用 Service= 指定。目标程序必须支持 systemd 文件描述符传递,不能仍然自行绑定同一个地址。

官方文档:https://systemd.io/

最小配置原则
  • 本机调用优先使用 Unix socket,并通过 SocketUser、SocketGroup 和 SocketMode 限制访问。
  • 常驻多连接服务通常使用 Accept=no,接收监听文件描述符;Accept=yes 则为每个连接创建模板实例。
  • socket unit 会自动排在被激活服务之前,但若服务禁止脱离 socket 单独启动,还要明确写出依赖。

保护资产:监听面和继承的文件描述符

配置前先明确四类资产:监听地址、systemd 持有的监听文件描述符、服务进程权限,以及允许连接的客户端身份。socket activation 只是改变“谁负责 bind 和 listen”,不会自动替你完成访问控制。

授权客户端、权限门、systemd socket、内核监听套接字与受限服务的安全边界
图1:systemd socket 激活的安全边界结构图。Unix socket 权限限制本机客户端,监听文件描述符由 systemd 交给受限服务;这是原创静态说明图。

Unix socket 的路径、属主、属组和模式直接决定本机哪些进程可以连接。TCP socket 则由绑定地址、防火墙和上游网络策略共同决定暴露面。服务接收到的文件描述符已经处于监听状态,程序应通过 sd_listen_fds() 或对应语言的 systemd activation 库识别它,而不是再次占用端口或路径。

攻击路径:最常见的五个配置缺口

  • 监听范围过大:本来只需本机 IPC,却把 ListenStream=8080 或通配地址暴露到外部接口。
  • Unix socket 权限过宽:SocketMode=0666 让所有本机用户都能访问高权限服务。
  • 服务可绕过 socket 单独启动:管理员或依赖单元直接启动 service 后,程序回退为自行绑定,形成第二套未经审计的监听配置。
  • 误用 Accept 模式:Accept=yes 为每个连接创建实例,连接洪泛会放大进程创建成本;高吞吐守护进程通常应使用 Accept=no。
  • 描述符识别错误:程序忽略 LISTEN_FDS、假设固定 FD 数量,或多 socket 时不使用名称区分,可能监听错协议端点。

还要避免把 socket activation 与网络就绪混为一谈。监听本地 Unix socket 不需要等待网络上线;绑定特定网络设备时,BindToDevice= 会带来设备单元相关依赖。无依据地添加 After=network-online.target 会增加启动等待,却不能修正程序不支持描述符传递的问题。

风险分级:先处理暴露与权限,再处理可用性

配置问题主要影响优先级判断依据
管理接口监听所有网络地址远程未授权访问面扩大高实际监听地址与业务需求不一致
Unix socket 对所有用户可写本机低权限进程可调用服务高模式、属组和客户端身份不匹配
service 可脱离 socket 自行监听策略绕过、端口冲突中高手工启动服务后仍产生另一监听对象
Accept=yes 用于高频短连接资源放大、激活抖动中每连接实例数与请求量同步增长
缺少显式 fd 名称多监听端点被程序混淆中程序按顺序猜测文件描述符用途

风险排序应基于实际暴露范围和服务权限,而不是只看 unit 是否能启动。一个运行正常但监听面过宽的配置,比一个因名称拼错而无法激活的配置更危险。

防护控制:把依赖和监听配置写清楚

下面使用本机 Unix socket 和 Accept=no。它适合一个进程统一接受多连接的常驻服务。Service= 虽然与同名默认值一致,但显式写出后更便于审查。

systemd socket、service、sockets target、挂载单元和 Accept 模式的依赖关系
图2:socket unit 与 service unit 的依赖关系图。systemd 自动建立部分顺序依赖,主动启动服务时仍应明确 socket 需求;这是原创静态说明图。
# /etc/systemd/system/demo-api.socket
[Unit]
Description=Demo API activation socket

[Socket]
# 本机 IPC 使用 Unix socket,避免无意暴露 TCP 端口。
ListenStream=/run/demo-api.sock
Accept=no
Service=demo-api.service
FileDescriptorName=api

# 只有 root 和 demo-client 组可以连接。
SocketUser=root
SocketGroup=demo-client
SocketMode=0660
RemoveOnStop=yes

[Install]
# 开机启用监听 unit,而不是强制服务常驻。
WantedBy=sockets.target

Accept=no 时,systemd 把监听 socket 交给一个 service;程序自行调用 accept() 处理后续连接。Accept=yes 时,每个连接会创建一个 demo-api@.service 模板实例,交付的是已连接 socket。两种模式所需程序模型不同,不能只改一个布尔值。

# /etc/systemd/system/demo-api.service
[Unit]
Description=Demo socket-activated API

# 如果服务不允许脱离 socket 单独运行,明确建立强依赖。
Requires=demo-api.socket
After=demo-api.socket

[Service]
Type=simple
User=demo-api
Group=demo-api
ExecStart=/usr/local/libexec/demo-api

# 服务被手工启动时也拉起并等待指定 socket。
Sockets=demo-api.socket

# 这些限制适用于仅需本机 Unix socket 的示例服务。
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
RestrictAddressFamilies=AF_UNIX

systemd 会自动给 socket 增加相对于被激活 service 的 Before= 顺序关系。文件系统路径 socket 还会自动依赖访问该路径所需的挂载单元。默认依赖通常已足够,不要随意设置 DefaultDependencies=no;它只适合早期启动或关机阶段的特殊 socket。

Sockets= 会让服务通过 Wants= 和 After= 拉入 socket。示例又加了 Requires=,是因为这里的程序没有“自行绑定”的备用模式,socket 启动失败时服务也不应继续。若程序确实支持无 systemd 环境运行,可以放宽这个约束,但应把两种启动方式分别测试和审计。

审计记录:确认监听对象与激活关系

部署前先做 unit 静态解析,再重新加载配置并只启用 socket。以下命令块中的路径和单元名与上面的示例一致:

# 检查 unit 语法、未知指令和依赖引用,不启动服务。
sudo systemd-analyze verify \
  /etc/systemd/system/demo-api.socket \
  /etc/systemd/system/demo-api.service

# 重新加载 unit,并让 socket 立即监听且随系统启用。
sudo systemctl daemon-reload
sudo systemctl enable --now demo-api.socket

# 查看监听配置、触发目标和当前状态。
systemctl show demo-api.socket \
  --property=Listen \
  --property=Triggers \
  --property=ActiveState \
  --property=SubState

# 核对 Unix socket 节点和内核监听状态。
test -S /run/demo-api.sock
ss -xlpn | grep '/run/demo-api.sock'

刚启动 socket 时,service 可以仍是 inactive;这是按需激活的正常状态。使用业务协议客户端连接后,再检查 service 是否进入 active。不要用浏览器或任意文本客户端探测二进制协议,以免把协议错误误判为激活故障。

# 触发业务客户端后,检查服务状态与两类 unit 的同一时间窗日志。
systemctl status demo-api.service --no-pager
journalctl \
  -u demo-api.socket \
  -u demo-api.service \
  --since '-10 minutes' \
  --output=short-iso

# 核对 service 主动启动时是否会拉入目标 socket。
systemctl show demo-api.service \
  --property=Wants \
  --property=Requires \
  --property=After

审计日志至少保留 unit 版本、监听地址、socket 模式、服务用户、激活失败原因和部署批次。不要记录连接载荷、令牌或业务密钥。

验证清单:上线、停用与回滚

  • 启动 socket 后监听节点存在,service 在首个连接前无需常驻。
  • 授权组客户端可以连接,非授权用户被文件系统权限拒绝。
  • 连接到来后只激活预期 service,没有生成额外监听端口。
  • 停止 service 而保留 socket 后,新连接能重新激活服务。
  • 停止 socket 后节点按配置移除,服务不会悄悄自行绑定同一路径。
  • 程序能按 LISTEN_FDS 与 LISTEN_FDNAMES 识别描述符;多 socket 时不依赖猜测顺序。

需要回滚时,先停止新 socket,再恢复旧服务 unit 和原监听配置,执行 daemon-reload 后启动旧服务。不要让新旧两个监听者同时争用地址。

# 先停止激活入口和新服务,避免回滚期间继续接收连接。
sudo systemctl stop demo-api.socket demo-api.service

# 恢复已版本化的旧 unit 后重新加载,再启动旧服务。
sudo systemctl daemon-reload
sudo systemctl start demo-api.service

为什么只启用 socket,不启用 service?

按需模式由 socket 保持监听并在流量到来时激活服务。若同时把 service 设为开机常驻,就失去了延迟启动的主要意义,但仍可继续使用 systemd 提供的监听文件描述符。

Service= 可以省略吗?

同名 unit 可以省略,例如 demo-api.socket 默认对应 demo-api.service。名称不一致或需要显式审计映射时写出更清楚。

Accept=yes 为什么需要模板 service?

因为每个连接都要创建独立实例,systemd 需要从 name@.service 模板生成实例;Accept=no 则只启动单个普通 service。

程序完全不支持 LISTEN_FDS 怎么办?

不能只靠 unit 文件强行实现原生 socket activation。应先确认程序是否支持 systemd、inetd 风格或专用文件描述符参数;没有接收机制时继续使用程序自己的监听方式,或修改程序后再迁移。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>