Normal view

There are new articles available, click to refresh the page.
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

Hugo博客更新笔记:Shortcode、RSS与FOUC

By: Juby
28 December 2025 at 21:30

行内Shortcode去除空格

初衷是不再在博客的内文插入html标签,从而关闭Hugo的unsafe模式。一番整理后,只剩一处<ruby>标签不舍得删,虽不知何时会再用到,还是为它写了一个简单的shortcode。Hugo的shortcode是块元素,插入行内时会产生空格,{{- -}}的写法也消除不掉。我是强迫症,非要苛求这一个像素的完美,然而ChatGPT提供的诸多方法无一生效。久不用搜索引擎,一搜就找到了解法,AI竟然不知,也是奇怪。

去掉所有空格的方法很简单,在末尾加{{- "" -}}即可,示例如下:

<ruby>{{ .Get 0 }}<rt>{{ .Get 1 }}</rt></ruby>{{- "" -}}

RSS输出保留HTML标签

这是另一个ChatGPT未能帮我解决的问题,它提供的方法要么不能防止html标签转义,要么会导致XML校验报错。正确的写法就在Hugo的官方示例中,AI竟然也没学到,令人诧异。

官方写法如下,如果不这么写,Inoreader阅读器将无法正确排版。

{{ .Content | transform.XMLEscape | safeHTML }}

页面切换闪屏

给博客新加了一个暗色主题后,切换页面开始出现闪屏现象,本地运行没有问题,发布之后却有问题。试了AI提供的各种方法均无效果,准备放弃之际,无意中看到植入博客的rocket-loader.min.js脚本,罪魁祸首原来是它,导致资源的加载顺序有问题从而闪屏。这是域名迁移到Cloudflare后打开的功能,当时并未细查有何用处,而AI也没有想到这一点,似乎不太应该。

在平台之外,留一片属于自己的空间:读 Be A Property Owner And Not A Renter On The Internet 一文有感

By: Justin
20 April 2025 at 11:33

文章讨论了互联网平台的集中化带来的问题,如用户被锁定和内容控制权的丧失。建议用户成为“房东”,拥有自己的域名和服务器,建立个人网站,维护自我内容的独立性。强调真正拥有数字资产的重要性,以实现自由和灵活性。

在平台之外,留一片属于自己的空间:读 Be A Property Owner And Not A Renter On The Internet 一文有感最先出现在Justin写字的地方

[限免] Coursera Plus免费订阅一年,快冲!

By: Justin
30 September 2024 at 09:29

今天通过墨西哥 VPN 注册的用户可免费获得一年Coursera Plus会员,享受7,000多门课程的无限访问。该促销活动有时间限制,只需在注册过程中使用VPN,完成后可正常使用。需注意,在订阅结束前取消以避免收费。

[限免] Coursera Plus免费订阅一年,快冲!最先出现在Justin写字的地方

RSS简单科普

By: Justin
9 September 2024 at 09:26

RSS是一种基于XML的内容发布和订阅格式,用于在互联网上分享和同步网站内容。用户可通过RSS阅读器订阅网站的最新内容,从而在一个界面中查看多个网站的更新。尽管其主流性已降低,但RSS仍然在信息自主选择和隐私保护方面展现独特价值,为用户提供高效的信息获取方式。

RSS简单科普最先出现在Justin写字的地方

Fidder: 用Telegram管理RSS订阅源

By: Justin
8 September 2024 at 18:17

Fidder是一款强大的Telegram RSS订阅机器人,让用户可以通过Telegram平台管理和接收更新,无需额外应用程序。只需搜索并添加@FidderBot,用户即可轻松使用指令添加、管理和接收RSS订阅源更新。这种便捷方式让用户可以第一时间获取站点新文章。

Fidder: 用Telegram管理RSS订阅源最先出现在Justin写字的地方

ChatGPT on macOS客户端app正式面向所有用户开放

By: Justin
27 June 2024 at 09:21

OpenAI宣布了适用于 macOS 的 ChatGPT 客户端app正式面向所有用户开放。该应用专为 macOS 系统设计,支持快捷键呼出和多种内容形式的交互。目前仅适用于配备 Apple Silicon(M1 或更高版本)的 macOS 14+,但计划在今年晚些时候登陆 Windows。

ChatGPT on macOS客户端app正式面向所有用户开放最先出现在Justin写字的地方

FUJISTYLE: 富士相机用户的专属app -富士色彩配方、边框水印、胶片模拟

By: Justin
4 June 2024 at 09:34

FUJISTYLE是专为富士相机用户设计的app,可保存富士色彩配方笔记、识别JPEG原图色彩配方、查看照片EXIF信息等。用户可免费使用基础功能,而Pro会员享有更多专属功能,并有年度或月度订阅选项。app的目标是提升用户拍摄体验并提供额外功能。

FUJISTYLE: 富士相机用户的专属app -富士色彩配方、边框水印、胶片模拟最先出现在Justin写字的地方

ChatGPT on macOS客户端app正式面向所有用户开放

By: Justin
27 June 2024 at 09:21
OpenAI宣布了适用于 macOS 的 ChatGPT 客户端app正式面向所有用户开放。该应用专为 macOS 系统设计,支持快捷键呼出和多种内容形式的交互。目前仅适用于配备 Apple Silicon(M1 或更高版本)的 macOS 14+,但计划在今年晚些时候登陆 Windows。

FUJISTYLE: 富士相机用户的专属app -富士色彩配方、边框水印、胶片模拟

By: Justin
4 June 2024 at 09:34
FUJISTYLE是专为富士相机用户设计的app,可保存富士色彩配方笔记、识别JPEG原图色彩配方、查看照片EXIF信息等。用户可免费使用基础功能,而Pro会员享有更多专属功能,并有年度或月度订阅选项。app的目标是提升用户拍摄体验并提供额外功能。

❌
❌