Normal view

There are new articles available, click to refresh the page.
Yesterday — 29 May 2026Main stream
Before yesterdayMain stream

类比 systemd 的 macOS launchd 使用笔记

By: prin
2 February 2026 at 10:22

现在各大 Linux 发行版很多都变成 systemd 的形状了,连 Synology DSM 7 都开始用 systemd 了,可以说是 Learn once, operate anywhere。不过有时候还是要在 Mac 机器上部署一些服务,需要用到 launchd/launchctl。

本文以一个熟悉 systemd/systemctl 工具的运维视角,整理了常用的 launchd 相关命令备忘。(说实话我每次都会忘记怎么用,然后得去翻手册……而且网上有时候还会教你用 load/unload 子命令,这些其实都已经标记为过时了,有新的命令替代它们)

list 查看所有

launchctl list# 类比 systemctl list-units

status 查看状态

launchctl print gui/501/uploadserver# -> ~/Library/LaunchAgents/uploadserver.plistlaunchctl print system/com.openssh.sshd# -> /System/Library/LaunchDaemons/ssh.plist# 类比 systemctl status uploadserver.service# -> /etc/systemd/system/uploadserver.service

这里的 system/ gui/501/ 叫做 domain-target,com.openssh.sshduploadserver 叫做 service-name,合起来之后叫做 service-target。service-name 一般就是下面 .plist 配置文件中的 Label。

其中 system/ 顾名思义就是系统级的,system domain,需要 root 权限才能修改。而 gui/501/ user/501/ 则是用户级的 user domain,其中 501 就是用户 uid。gui/<uid>/login/<asid>/ 下的服务只有用户登录了才会运行。

通常来说需要 GUI 运行的放在 gui/<uid>/ 下面,不需要的放在其他下面。不过考虑到 macOS 作为桌面操作系统的特性,其实一股脑都放 gui 下面也没啥问题。

start 启动

launchctl bootstrap gui/501 ~/Library/LaunchAgents/uploadserver.plist# 或者系统级的服务,需要 sudo 运行sudo launchctl bootstrap system /Library/LaunchDaemons/com.example.plist# 类比 systemctl start uploadserver.service# 不需要 systemctl daemon-reload

如果 bootstrap 提示下面的报错,就说明很可能是服务器已经在运行了,或者服务被 disable 掉了:

Bootstrap failed: 5: Input/output errorTry re-running the command as root for richer errors.

stop 停止

launchctl bootout gui/501 ~/Library/LaunchAgents/uploadserver.plist# 或者系统级的服务,需要 sudo 运行sudo bootout bootstrap system /Library/LaunchDaemons/com.example.plist# 除了 .plist 文件路径,也可以用 label 指定launchctl bootout gui/501/uploadserver# 类比 systemctl stop uploadserver.service

如果 bootout 提示下面的报错,就说明很可能是服务本来就没有在运行:

Boot-out failed: 3: No such process

restart 重启

launchctl kickstart -k -p gui/501/uploadserver# service spawned with pid: 17247# 参数:# -k       如果服务已经在运行了,先杀死现有的进程再重启# -p       成功后输出进程的 PID# 类比 systemctl restart uploadserver.service

如果 kickstart 提示下面的报错,就说明服务没有 bootstrap:

Could not find service "uploadserver" in domain for user gui: 501

enable 开机启动

一般来说你的 .plist 里配置了 RunAtLoad 的话,bootstrap 了之后默认就是开机启动的,不需要再 enable 一遍。

不过你也可以通过 disable 主动关闭服务的开机启动,这个是否 enable 的状态是 launchd 自己维护的,不会更改 .plist 文件的内容。使用 disable 禁止开机启动之后,想要恢复开机启动就需要用 enable。

launchctl enable gui/501/uploadserver# 类比 systemctl enable uploadserver.service

disable 禁止开机启动

launchctl disable gui/501/uploadserver# 类比 systemctl disable uploadserver.service

config 配置文件

类似于 systemd 的 Unit File,以 .plist/XML 文件的形式存在:

~/Library/LaunchAgents   Per-user agents provided by the user./Library/LaunchAgents    Per-user agents provided by the administrator./Library/LaunchDaemons   System-wide daemons provided by the administrator./System/Library/LaunchAgents   Per-user agents provided by Apple./System/Library/LaunchDaemons  System-wide daemons provided by Apple.

通常来说我们只会用到前三个。Daemon 和 Agent 的区别:

  • Daemon:系统级、单个实例服务多个用户、不应显示 UI、没有用户登录也会运行、可以以 root 权限运行。
  • Agent:每个用户各自运行实例、直接与用户交互、用户不登录则不会运行、以普通用户权限运行。

这里以 ~/Library/LaunchAgents/uploadserver.plist 为例:

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict>    <key>KeepAlive</key>    <true/>    <key>Label</key>    <string>uploadserver</string>    <key>LimitLoadToSessionType</key>    <array>        <string>Aqua</string>        <string>Background</string>        <string>LoginWindow</string>        <string>StandardIO</string>        <string>System</string>    </array>    <key>ProcessType</key>    <string>Background</string>    <key>ProgramArguments</key>    <array>        <string>/opt/homebrew/bin/uvx</string>        <string>uploadserver</string>        <string>8888</string>        <string>-d</string>        <string>/tmp/upload</string>    </array>    <key>RunAtLoad</key>    <true/>    <key>EnvironmentVariables</key>    <dict>        <key>FOO</key>        <string>BAR</string>    </dict>    <key>StandardErrorPath</key>    <string>/tmp/uploadserver.log</string>    <key>StandardOutPath</key>    <string>/tmp/uploadserver.log</string>    <key>TimeOut</key>    <integer>5</integer></dict></plist>

类比 /etc/systemd/system/uploadserver.service

[Unit]Description=Upload ServerWants=network-online.targetAfter=network-online.target[Service]ExecStart=/root/.local/bin/uvx uploadserver 8888 -d /tmp/uploadRestart=alwaysRestartSec=5Environment="FOO=BAR"StandardOutput=file:/tmp/uploadserver.logStandardError=file:/tmp/uploadserver.log[Install]WantedBy=multi-user.target

具体怎么写就不多说了,大部分开机自启 + 守护进程需求用上面的模板就够了,有其他需求的直接看文档。

名字基本可以随便取。另外注意有多个命令行参数的话,要拆成多个 <string>

不再推荐使用的 Legacy 命令

现在很多教程里还在用 load/unload,不过在 macOS 10.11 之后,官方手册中已经不再推荐使用这些老的命令,应该使用上面的命令替代。

另外一点需要注意的是,load/unload 通过执行 launchctl 命令时的权限来决定 domain。如果使用 sudo launchctl 执行的,算作 system domain,否则算作 user domain。之前被坑过一次……💩

launchctl load ~/Library/LaunchAgents/uploadserver.plist# 应该替换为 launchctl bootstrap gui/501 ~/Library/LaunchAgents/uploadserver.plistlaunchctl load -w ~/Library/LaunchAgents/uploadserver.plist# 相当于先 enable 再 bootstraplaunchctl unload ~/Library/LaunchAgents/uploadserver.plist# 应该替换为 launchctl bootout gui/501 ~/Library/LaunchAgents/uploadserver.plistsudo launchctl load /Library/LaunchDaemons/com.example.plist# 应该替换为 sudo launchctl bootstrap system /Library/LaunchDaemons/com.example.plistsudo launchctl unload /Library/LaunchDaemons/com.example.plist# 应该替换为 sudo bootout bootstrap system /Library/LaunchDaemons/com.example.plist

可视化管理工具

推荐 Lingon X,有点像以前 Windows 上的那种开机进程管理,可以查看当前系统上有哪些 Agent/Deamon,以及它们的配置,挺方便的。

小尾巴

我记得以前 macOS 设置页面里的启动项管理很垃圾,只能显示部分启动项,但实际上程序想要开机自启有很多种方法:

  • LoginItems (Helper App with SMLoginItemSetEnabled)
  • LaunchAgents (~/Library/LaunchAgents or /Library/LaunchAgents)
  • LaunchDaemons (/Library/LaunchDaemons)
  • 直接拖动到设置页面「登录时打开」列表中

搞得有些流氓软件开机自启了你都不知道,比之 Windows 还不如。当时我还准备写篇博客,在不安装第三方清理软件的情况下如何删除这些启动项,但后来不了了之了。

不过好在后来的 macOS 更新把这块的管理补上了,现在在设置页面就可以直接管理上面那些方式添加的启动项,挺好。

禁止 Synology/QNAP NAS 通过 ssh 密码登录

By: prin
2 February 2026 at 00:00

使用成品 NAS,在相对省心的同时,也意味着更低的自由度。很多自定义操作都是在和这些定制化 OS 斗智斗勇。

本文介绍了在 QNAP 威联通 NAS(以及 Synology 群晖 NAS)上关闭 ssh 密码登录,仅允许密钥登录的方法。Synology 相对还好,对 OpenSSH 的魔改不多,和普通 Linux 一样修改 /etc/ssh/sshd_config 就好了。

但是 QNAP 这个 B,每次启动 sshd 服务都会重新生成配置,直接修改上述配置文件是没法持久化保存的。

风险提示

为了避免你被锁在门外,修改自带 sshd 配置之前,最好自己再起一个 ssh 服务,等确认 OK 了之后再关掉。以及最重要的,做好备份!

sudo opkg install dropbearsudo dropbear -F -p 9922 -P /opt/var/run/dropbear.pid -R -w -E

如果你不幸已经进不去系统了,可以这样自救(别问我为什么知道🤣):

  • 从 Web UI 打开 Container Station / Container Manager
  • 通过上传 docker-compose.yml 的方式创建容器(通常方式创建的话是不让挂载特殊路径的)
  • 挂载要修改的路径,比如 /etc/init.d/
  • 启动容器后,选择 打开终端机/连接终端,然后把改坏的文件改回来
services:  rescue-tmp:    image: ubuntu:latest    container_name: rescue-tmp    stdin_open: true    tty: true    command: /bin/bash    volumes:      - /etc/init.d:/work

Synology NAS

改文件之前做好备份!

执行 sudo vim /etc/ssh/sshd_config,修改:

 PubkeyAuthentication yes ChallengeResponseAuthentication no-PasswordAuthentication yes+PasswordAuthentication no

然后 sudo synosystemctl restart sshd.service 重启 sshd 生效,或者在 DSM Web UI 上关闭「启动 SSH 功能」后再打开也可以。

值得一提的是,直接修改 sshd_config 中的 Port,会导致 sshd 监听在两个端口上,一个是 Web UI 上设置的「SSH 端口」,一个是配置文件中配置的端口,而且后者是无法用于登录 Shell 的,会提示 Permission denied, please try again.

研究了一下,应该是因为 sshd 被魔改过,会读取 /etc/synoinfo.conf 文件里的 ssh_port 配置。

sudo lsof -c sshd# COMMAND   PID USER  FD   TYPE  SIZE/OFF    NODE NAME# sshd    28232 root cwd    DIR      4096       2 /# sshd    28232 root rtd    DIR      4096       2 /# sshd    28232 root txt    REG    888480   20645 /usr/bin/sshd# sshd    28232 root   3u  IPv4       0t0     TCP *:2222 (LISTEN)# sshd    28232 root   4u  IPv6       0t0     TCP *:2222 (LISTEN)# sshd    28232 root   5u  IPv4       0t0     TCP *:6622 (LISTEN)# sshd    28232 root   6u  IPv6       0t0     TCP *:6622 (LISTEN)cat /etc/synoinfo.conf | grep -i port# ssh_port="2222"# sftpPort="3322"# rsync_sshd_port="4422"strings /usr/bin/sshd# SYNOServiceSSHPortGet# /etc/ssh/sshd_config# /etc/synoinfo.conf# Permission denied, please try again.

而根据 Reverse Engineering Synology’s OpenSSH 这篇博客里的说法,这个魔改过的 OpenSSH 会校验一些参数,所以无法连接。

不过对于我们的目标「禁用 ssh 密码登录」没什么影响,就这样吧。这些厂商定制化的东西还是少碰,别给整挂了。

QNAP NAS

大的来了(掩鼻)。

先看看 QNAP 的 sshd 用的什么配置文件:

ps aux | grep sshd# 22714 admin     11504 S   /usr/sbin/sshd -f /etc/config/ssh/sshd_config -p 2222cat /etc/config/ssh/sshd_config# Protocol 2# HostKey /etc/ssh/ssh_host_ed25519_key# PermitRootLogin yes# UseDNS no# Subsystem sftp /usr/libexec/sftp-server# AllowTcpForwarding yes# AllowUsers admin prinecho 'PasswordAuthentication no' | sudo tee -a /etc/config/ssh/sshd_config

重启 sshd 后,你会惊喜地发现,刚才改掉的配置文件,诶,又被改回来了😁:

sudo setsid /etc/init.d/login.sh restartgrep PasswordAuthentication /etc/config/ssh/sshd_config

既然配置文件没法持久化,那就只能从启动脚本下手了。

简单看看 /etc/init.d/login.sh(文件缩进就是这样,我保留了原汁原味):

SSH=/usr/sbin/sshdSSHD_CONF=/etc/config/ssh/sshd_configSSHD_CONF_DEFAULT=/etc/ssh/sshd_configSSH_PORT=`/sbin/getcfg LOGIN "SSH Port" -d 22`update_sshd_config(){ /sbin/mksshdconf ENABLED_SFTP=`/sbin/getcfg LOGIN "SFTP Enable" -u -d TRUE` # ... #Set PermitRootLogin yes OPTION="PermitRootLogin" if [ -z "`grep .*${OPTION}.* ${SSHD_CONF}`" ]; then     echo "${OPTION} yes" > ${SSHD_CONF} else        sed -i "s/^#\s\?${OPTION}\s\?[yesno]\{1,3\}.*/${OPTION} yes/g" ${SSHD_CONF} fi # ...}# ...case "$1" in    start) # for openssh 7.5p1 and later sed -i '/^UsePrivilegeSeparation .*/d' ${SSHD_CONF_DEFAULT} /bin/chmod 0400 /etc/config/shadow* /etc/default_config/shadow update_ssh_client_config if [ `/sbin/getcfg LOGIN "SSH Enable" -u -d FALSE` != FALSE ]; then  echo -n "Starting sshd service: "  generte_ssh_key     if [ ! -f "${SSHD_CONF}" ]; then      /bin/cp -f ${SSHD_CONF_DEFAULT} ${SSHD_CONF}     fi     if [ ! -f "${SSHD_CONF}" ]; then      SSHD_CONF=${SSHD_CONF_DEFAULT}     fi  update_sshd_config        sshd_privilege_separation  /sbin/daemon_mgr sshd start "$SSH -f ${SSHD_CONF} -p $SSH_PORT"  echo "OK"  touch /var/lock/subsys/sshd fi ;; # ...esac

你可能以为有这个 cp -f ${SSHD_CONF_DEFAULT} ${SSHD_CONF},我们就能通过修改 /etc/ssh/sshd_config 来实现配置持久化了 —— 天真!

实测下来发现 /sbin/mksshdconf 这个二进制每次执行都会重置 /etc/config/ssh/sshd_config 的内容,所以你 cp 了也没有用。我也不知道他们为什么要写一个没有意义的 cp 在那里。🤷

我搜了下,这篇博客 里也有类似的吐槽:

For reasons I can’t fathom, the SSH config on a QNAP seems nuts (If anyone knows why it works this way, please let me know, because I must be missing something obvious).

就算抛开这些不谈,你这个强制 PermitRootLogin yes 是何意啊?

所以我们还是只能改脚本,重启 sshd 后生效:

改文件之前做好备份!

--- /etc/init.d/login.sh.bak+++ /etc/init.d/login.sh@@ -83,6 +83,9 @@ update_sshd_config() {  /sbin/mksshdconf+ echo 'PasswordAuthentication no' >> ${SSHD_CONF}+ echo 'ChallengeResponseAuthentication no' >> ${SSHD_CONF}+ sed -i "s/PermitRootLogin yes/PermitRootLogin no/g" ${SSHD_CONF}  ENABLED_SFTP=`/sbin/getcfg LOGIN "SFTP Enable" -u -d TRUE`  if [ "x${ENABLED_SFTP}" = "xTRUE" ]; then

注意:系统更新后这个脚本会被重置,需要重新修改。

Bonus: macOS

其实和大部分 Linux 一样啦,写在这里只是为了顺便记一下:

cat << EOF | sudo tee /etc/ssh/sshd_config.d/10-no-passwords.confPubkeyAuthentication yesPasswordAuthentication noKbdInteractiveAuthentication noEOF

sshd_config.d 里的序号规则是 小的覆盖大的

重启 sshd:

# Linux systemdsystemctl restart ssh.servicesystemctl status ssh.service# macOS launchdlaunchctl kickstart -k -p system/com.openssh.sshdlaunchctl print system/com.openssh.sshd

让 VNC 远程桌面只能通过 ssh 隧道连接(仅监听 127.0.0.1):

sudo defaults write /Library/Preferences/com.apple.RemoteManagement.plist VNCOnlyLocalConnections -bool yesssh -L 5900:127.0.0.1:5900 mac-mini

小尾巴

保不齐这些 NAS 系统后续更新会出什么幺蛾子,可以让 AI 搓一个脚本,放在 Docker 容器里 crontab 定时运行,然后给一个服务器列表,一旦发现任何服务器允许 PasswordAuthentication,就发送通知告警。

修改后的效果:

ssh -o PreferredAuthentications=password,keyboard-interactive prin@192.168.1.10 -p 2222# prin@192.168.1.10: Permission denied (publickey).ssh -o PreferredAuthentications=password,keyboard-interactive prin@192.168.2.10 -p 2222# prin@192.168.2.10: Permission denied (publickey).

修复 Synology NAS 升级后 M.2 存储池兼容性问题

By: prin
1 February 2026 at 00:00

我的主力 NAS 是 Synology 群晖 DS920+,陪了我也快 4 年了。DS920+ 出厂默认是不支持将 M.2 NVMe SSD 作为存储池的,只能作为缓存使用。后来出的 DS923+ 倒是支持创建 M.2 存储池了,但是只能用它自家的兼容性列表中的盘,也不知道套的哪家 OEM,贵得要死,买是不可能买的这辈子不可能买的。

好在有大神写了脚本 Synology_HDD_db,可以直接修改这个「兼容性列表」所在的数据库,把你在用的硬盘型号加进去。系统一读这个数据库发现型号在里面,就可以正常使用了。不过缺点是每次系统更新后,都需要重新执行一遍脚本。

今天看到关于 飞牛 fnOS 0day 漏洞 的帖子,寻思着也给 DS920+ 更新一下系统,毕竟一直都稳定运行着,也没有特意去升级。虽然我没用飞牛,也没有把服务暴露到公网,不过各家 NAS 系统的 CVE 其实都挺多的,以防万一嘛。

结果升级完系统 NAS 就开始疯狂嘀嘀嘀叫,才想起来还有硬盘兼容性这一茬……太久远了,顺手记录一下,免得下次再忘了。


报错界面是这样的:

  • 当前发出哔声的原因:存储空间或 SSD 缓存异常
  • 检测到不支持的硬盘(缓存设备 2)
  • 在存储池 3 中检测到一个或多个硬盘缺失
  • 严重 存储池 3 发生问题
  • 缓存设备 2 - M.2 NVMe SSD 不受当前 DSM 版本支持

synology-beeping

storage-pool-failure

更新硬盘兼容性数据库

这个 M.2 存储池是 2023 年建的,记得当时好像还不需要捣鼓什么兼容性数据库,应该是后来 DSM 7 加上的。

总之就是运行一下 Synology_HDD_db 这个脚本,每次系统更新完都要跑一遍,然后重启设备生效。

# 版本号自己改成最新的wget https://github.com/007revad/Synology_HDD_db/archive/refs/tags/v3.6.119.zip7z x v3.6.119.zipcd Synology_HDD_db-3.6.119sudo -s ./syno_hdd_db.sh -nr --showedits# 用到的脚本参数:# -n, --noupdate        Prevent DSM updating the compatible drive databases# -r, --ram             Disable memory compatibility checking (DSM 7.x only)# -s, --showedits       Show edits made to <model>_host db and db.new file(s)

比如我这里用的就是一条致态的 2TB 作为下载盘,另一条 256GB 的作为 HDD 缓存。

syno-hdd-db

脚本运行以后,就会把你目前插着的硬盘加到 NAS 本地的兼容性列表数据库里了。

ls -al /var/lib/disk-compatibility/ | grep ds920+_host_v7# -rw-r--r--  1 root root 156K Feb  1 02:07 ds920+_host_v7.db# -rw-r--r--  1 root root 148K Jan 19 17:11 ds920+_host_v7.db.bak# -rw-r--r--  1 root root  97K Oct 27  2023 ds920+_host_v7.db.new.bak# -rw-r--r--  1 root root    8 Jan 19 17:11 ds920+_host_v7.release# -rw-r--r--  1 root root    4 Jan 19 17:11 ds920+_host_v7.versioncat /var/lib/disk-compatibility/ds920+_host_v7.db | jq .

所谓数据库,其实就是个大 JSON:

{  "disk_compatbility_info": {    // ...    "ZHITAI TiPlus7100 2TB": {      "ZTA22002": {        "compatibility_interval": [          {            "compatibility": "support",            "not_yet_rolling_status": "support",            "fw_dsm_update_status_notify": false,            "barebone_installable": true,            "barebone_installable_v2": "auto",            "smart_test_ignore": false,            "smart_attr_ignore": false          }        ]      },      // ...    }  },  "nas_model": "ds920+"}

建立 M.2 SSD 存储池

如果是 DS923+,运行完上面的脚本之后应该直接在 DSM Web UI 上创建存储池就可以了。

DS920+ 还是得自己手搓,记录一下用到的命令(之前参考的是 这篇博客):

# 查看当前的 NVMe 设备,找到你要的 SSDls -al /dev/nvme*sudo fdisk -l /dev/nvme0n1# 给 SSD 分区,有多块盘的话小心别弄错了sudo synopartition --part /dev/nvme0n1 12# 看看当前的 RAID 状态,如果已经有 md5 了就新建 md6cat /proc/mdstatsudo mdadm --create /dev/md6 --level=1 --raid-devices=1 --force /dev/nvme0n1p3# 创建文件系统sudo mkfs.btrfs -f /dev/md6sudo mdadm --detail /dev/md6sudo reboot

重启 NAS 后,检查一下:

sudo fdisk -l /dev/nvme0n1# Disk /dev/nvme0n1: 1.9 TiB, 2048408248320 bytes, 4000797360 sectors# Disk model: ZHITAI TiPlus7100 2TB# Units: sectors of 1 * 512 = 512 bytes# Sector size (logical/physical): 512 bytes / 512 bytes# I/O size (minimum/optimal): 512 bytes / 512 bytes# Disklabel type: dos# Disk identifier: 0xae2a9134# Device         Boot   Start        End    Sectors  Size Id Type# /dev/nvme0n1p1          256    4980735    4980480  2.4G fd Linux raid autodetect# /dev/nvme0n1p2      4980736    9175039    4194304    2G fd Linux raid autodetect# /dev/nvme0n1p3      9437184 4000795469 3991358286  1.9T fd Linux raid autodetectsudo mdadm --detail /dev/md6# /dev/md6:#         Version : 1.2#   Creation Time : Fri Jun  2 02:16:39 2023#      Raid Level : raid1#      Array Size : 1995678080 (1903.23 GiB 2043.57 GB)#   Used Dev Size : 1995678080 (1903.23 GiB 2043.57 GB)#    Raid Devices : 1#   Total Devices : 1#     Persistence : Superblock is persistent#     Update Time : Sun Feb  1 03:25:45 2026#           State : clean#  Active Devices : 1# Working Devices : 1#  Failed Devices : 0#   Spare Devices : 0#            Name : localhost:6  (local to host localhost)#            UUID : 2729425e:28782224:04f5d205:be6fcc6c#          Events : 45#     Number   Major   Minor   RaidDevice State#        0     259        3        0      active sync   /dev/nvme0n1p3

然后去 Web UI 的「存储管理器 > 存储空间 > 可用池」,点击「在线重组」就好了。

dsm-online-assemble

小尾巴

顺便吐槽一下,这系统升级完把我所有的容器都停了,也不给我自动重启,我还得一个一个过去 docker compose up -d 拉起来……

下次有空的话聊聊 HomeLab 的安全性问题:不仅仅是这次飞牛 fnOS 的路径穿越漏洞,还有之前影响到多个 self-hosted 软件乃至众多生产环境的 React Server Component RCE 漏洞,claude-relay-service 的鉴权绕过漏洞等等,都说明了把内网服务暴露到公网是极其危险的。很多人图方便喜欢搞什么内网穿透、DDNS、公网 IP、域名访问、DMZ、UPnP,殊不知距离被入侵可能只有一步之遥。

极空间与 QNAP NAS 实现共享 NUT UPS

By: prin
22 December 2025 at 00:00

我有三台 NAS,Synology 群晖、QNAP 威联通和 ZSpace 极空间,前者放在出租屋里作为主力,后两台放在老家作为备份。

因为极空间是 arm64 版本双盘位的,功耗不高,所以和 QNAP 共用一台 UPS 就好了。之前折腾了一下,让极空间作为 Master 通过 USB 连接 UPS,然后 QNAP 作为 Slave 通过 NUT (Network UPS Tools) 局域网连接。

不过因为涉及到系统文件改动,每次极空间系统升级后都需要重新操作一遍,每次我都会忘记要怎么捣鼓……所以写篇博客记录下。

话说 2025 年居然一篇博客都没写,工作太忙了呀,而且总是犯懒。以前上学的时候写博客,不怎么理解那些断更的博主,现在可以体会到那种感觉了……草稿倒是堆了很多,希望明年可以发出来吧!

TL;DR

以 root 权限登录极空间,运行以下命令,然后重启服务即可。

sed -i 's/127.0.0.1/0.0.0.0/g' /zspace/applications/services/nut/config/upsd.confcat >> /zspace/applications/services/nut/config/upsd.users << EOF[admin]        password = 123456        upsmon slave        allowfrom = %EOFcat >> /zspace/applications/services/nut/config/ups.conf << EOF[qnapups]        driver = usbhid-ups        port = auto        desc = "zspace ups for qnap"        lowbatt = 15        pollfreq = 15        offdelay = 60EOF

备注

首先要吐槽的就是 QNAP 这个傻逼系统,我已经不知道吐槽过多少次了。

QNAP 的天才工程师的天才设计:在作为 Network UPS Slave 连接时,用户只能指定「网络 UPS 服务器的 IP 地址」这一个选项,剩下的都是写死的:

  • 用户名 admin
  • 密码 123456
  • 端口 3493
  • 名称 qnapups

只要任何一个不对都连不上。Man! What can I say?

所以如果你想让 QNAP 和极空间共享一台 UPS,有两个选择:

  1. QNAP as Master,极空间 as Slave
  2. 极空间 as Master,QNAP as Slave

不过当时 24 年我折腾的时候,极空间还不支持 NUT Slave 模式。虽然那会儿极空间已经开放了 ssh 和 root 权限(也是这家之前总是被吐槽的一点),但是考虑到如果通过修改系统文件的方式让极空间支持 NUT Slave,可能不稳,毕竟这种定制系统也不知道哪里改了些什么东西。

所以我选择的是极空间通过 USB 物理连接 UPS,启动 NUT Server 作为 Master,QNAP 作为 Slave 客户端连接 NUT Server。

UPS <---{USB Cable}---> ZSpace <---{TCP 3493}---> QNAP                    (NUT Server)              (NUT Client)

P.S. 现在(2025 年 12 月)极空间已经支持 NUT Slave 模式了,各种参数都可以自定义,所以更好的方式应该是上述的第一种。不过我懒得去改接线了,先这样吧。又不是不能用.jpg

zspace-settings

在极空间上的操作

因为 QNAP 的弱智系统只能接受固定的用户名、密码和 UPS 名称,所以需要修改极空间的 NUT Server 配置。

首先要拿到极空间 NAS 的 ssh 和 root 权限,这里就不赘述了。登上去以后:

ls -al /etc/nut# lrwxrwxrwx 1 root root 33 Sep 17  2024 /etc/nut -> /zspace/applications/services/nutcd /zspace/applications/services/nutls -a ./# .       config                     restart.sh         setting.json  ups.conf     upssched# ..      external_ups_setting.json  restart_driver.sh  start.sh      upsd.conf    upssched-cmd.sh# client  nut.conf                   restart_upsd.sh    stop.sh       upsd.users   upssched.conf# conf    realstart.sh               server             u2600_tools   upsmon.conf

然后 cat ./server/nut-server.service 看看配置文件在哪(后续系统版本升级后实际路径可能会变,最好确认一下):

#!/bin/sh#use upsd.confexport NUT_CONFPATH=/zspace/applications/services/nut/config/sbin/upsd -u root

看看配置文件 ./config/upsd.conf

# Network UPS Tools: example upsd configuration file## ... 其他注释省略 ...# =======================================================================# LISTEN <address> [<port>]LISTEN 127.0.0.1 3493LISTEN ::1 3493

把这里的 127.0.0.1 改成 0.0.0.0,允许局域网(QNAP NAS)访问。或者也可以起一个 socat 做转发,都行。有需要的话也可以配一下 ACL,只允许 QNAP IP 访问。

cp /zspace/applications/services/nut/config/upsd.conf ~/upsd.conf.baksed -i 's/127.0.0.1/0.0.0.0/g' /zspace/applications/services/nut/config/upsd.conf

再看看用户配置文件 ./config/upsd.users

# Network UPS Tools: Example upsd.users## ... 其他注释省略 ...# --------------------------------------------------------------------------[upsmaster]        password = sekret        upsmon master        actions = SET        instcmds = ALL[nut]        password = nut        upsmon slave        allowfrom = %

往里面加上 QNAP 需要的用户:

cp /zspace/applications/services/nut/config/upsd.users ~/upsd.users.bakcat >> /zspace/applications/services/nut/config/upsd.users << EOF[admin]        password = 123456        upsmon slave        allowfrom = %EOF

然后再看看 UPS 配置文件 ./config/ups.conf

# Network UPS Tools: example ups.conf## ... 其他注释省略 ...# ------------------------maxretry = 3xretry = 5pollinterval = 15[ups0]        driver = usbhid-ups        port = auto        desc = "nut ups"        lowbatt = 15        pollfreq = 15        offdelay = 60

往里面加上 QNAP 需要的 UPS 名称:

cp /zspace/applications/services/nut/config/ups.conf ~/ups.conf.bakcat >> /zspace/applications/services/nut/config/ups.conf << EOF[qnapups]        driver = usbhid-ups        port = auto        desc = "zspace ups for qnap"        lowbatt = 15        pollfreq = 15        offdelay = 60EOF

最后,重启 NUT 服务:

本来想直接执行 /zspace/applications/services/nut/restart.sh 重启服务的,但似乎缺少某些环境变量,可能不是拿来直接调用的。

可以直接去极空间的网页面板,设置 > UPS 设置,把开关关闭然后再打开就行了。再检查下进程,看到 qnapups 就算成功了。

ps aux | grep ups# root     29716  /usr/share/nut/usbhid-ups -a ups0 -u root# root     29718  /usr/share/nut/usbhid-ups -a qnapups -u root# root     29722  /sbin/upsd -u root

在 QNAP 上的操作

不需要做什么,在网页面板上填入极空间 NAS 的局域网 IP 就能连上了。

qnap-settings

通过 upsc qnapups@192.168.1.4 查看 UPS 信息:

battery.charge: 100battery.charge.low: 10battery.mfr.date: 2001/01/01battery.runtime: 3618battery.runtime.low: 120battery.type: PbAcbattery.voltage: 13.6battery.voltage.nominal: 12.0device.mfr: American Power Conversiondevice.model: Back-UPS BK650M2-CHdevice.type: upsdriver.name: usbhid-ups

顺便看看 QNAP 上的 NUT 服务配置:

ls -al /etc/config# lrwxrwxrwx 1 admin administrators 21 2025-12-31 13:14 /etc/config -> /mnt/HDA_ROOT/.configls -a /mnt/HDA_ROOT/.config/ups# .  ..  ups.conf  upsd.conf  upsd.users  upsmon.conf

配置文件在 /mnt/HDA_ROOT/.config/ups/upsmon.conf

# Network UPS Tools: example upsmon configuration## ... 其他注释省略 ...# --------------------------------------------------------------------------MONITOR qnapups@192.168.1.4 1 admin 123456 slave#MONITOR zspaceups@192.168.1.4 1 upsmaster sekret slave# ... 其他注释省略 ...RUN_AS_USER adminMINSUPPLIES 1SHUTDOWNCMD "/sbin/shutdown -h +0"POLLFREQ 5POLLFREQALERT 5HOSTSYNC 15DEADTIME 15POWERDOWNFLAG /etc/killpowerRBWARNTIME 43200NOCOMMWARNTIME 300FINALDELAY 5

其实我之前也试过直接改这个文件,把 MONITOR 里写死的配置改成极空间的配置。但是试下来发现改完以后虽然服务是跑起来了,但是在 QNAP 的网页面板上不会显示 UPS 信息和已连接到 UPS 设备的图标。也不知道是这系统魔改了什么地方,担心会出现奇怪的问题,遂作罢。

如果你需要将 QNAP 作为 NUT Server,那可以关注 upsd.confupsd.users 文件,网上也有相关教程。

可视化 UPS 监控面板

因为 NUT 的网络协议走的是 TCP 3493,所以就算是异地的 UPS,组建虚拟局域网后也可以互相访问。

这里推荐一个开源的 NUT UPS 管理面板 PeaNUT,用起来挺方便的。

peanut

❌
❌