MoreRSS

site iconLala | 荒岛修改

一个应用分享、教程类的博客,主要是那些需要自部署的。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Lala | 荒岛的 RSS 预览

Debian 11 EOL更新系统并修复PHP高危漏洞

2026-10-21 23:58:27

今天PHP爆了两个高危漏洞:CVE-2026-109618、CVE-2026-109619,本站受影响,第一时间维护并着手更新。顺带把之前的一些问题一并解决了,记录下过程用于备忘。

我这个破站运行在Debian 11,因为一些历史遗留问题一直没更新到新系统,Debian 11已经停止支持了,官方不再为其提供安全更新,所以这不是一个好的示范,如果有能力应该还是升级系统。但好在我的PHP是通过sury的仓库安装的,不得不说sury真的是一个特别敬业的软件包维护者,他几乎与PHP上游同一时间发布了修复漏洞后的PHP软件包,包括向后移植的旧版本。

执行apt dist-upgrade报错,原因是Debian 11已经EOL了,Debian官方会把仓库归档,虽然Hetzner的系统模板会配置一个自家的镜像源,但这个镜像源也是404的状态:

Ign:1 http://security.debian.org/debian-security bullseye-security/main amd64 dpkg amd64 1.20.14
Ign:2 http://security.debian.org/debian-security bullseye-security/main amd64 libc-bin amd64 2.31-13+deb11u14
Err:1 http://mirror.hetzner.com/debian/security bullseye-security/main amd64 dpkg amd64 1.20.14
  404  Not Found [IP: 2a04:4e42:6e::644 80]
Err:2 http://mirror.hetzner.com/debian/security bullseye-security/main amd64 libc-bin amd64 2.31-13+deb11u14
  404  Not Found [IP: 2a04:4e42:6e::644 80]

解决办法就是编辑sources.list:

nano /etc/apt/sources.list

将Debian源统一替换为以下归档配置:

deb [check-valid-until=no] https://archive.debian.org/debian bullseye main contrib non-free
deb [check-valid-until=no] https://archive.debian.org/debian bullseye-updates main contrib non-free
deb [check-valid-until=no] https://archive.debian.org/debian-security bullseye-security main contrib non-free

这些对应Debian官方的更新归档和安全归档。Hetzner的镜像源可以不做修改,因为两边的优先级都是500,如果不放心可以将其注释掉以禁用。

Hetzner镜像源:

nano /etc/apt/sources.list.d/hetzner-mirror.list

注释掉:

# deb http://mirror.hetzner.com/debian/packages bullseye main contrib non-free
# deb http://mirror.hetzner.com/debian/packages bullseye-updates main contrib non-free

Hetzner安全更新镜像源:

nano /etc/apt/sources.list.d/hetzner-security-updates.list

注释掉:

# deb http://mirror.hetzner.com/debian/security bullseye-security main contrib non-free

第二个问题是Sury PHP仓库的签名密钥过期:

下载并安装最新的官方密钥包:

curl -sSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb
dpkg -i /tmp/debsuryorg-archive-keyring.deb

编辑Sury PHP仓库配置文件:

nano /etc/apt/sources.list.d/php.list

改用新密钥路径:

deb [signed-by=/usr/share/keyrings/debsuryorg-archive-keyring.gpg] https://packages.sury.org/php/ bullseye main

现在就可以更新系统了:

apt update
apt dist-upgrade

更新的过程中又报错了,CrowdSec的安装配置未完成,看样子单纯是网络问题:

Hub 规则下载返回 503
        ↓
CrowdSec 安装后脚本失败,配置流程中断
        ↓
本地 API(LAPI)未能恢复服务
        ↓
bouncer 连接 127.0.0.1:8088 被拒绝
        ↓
bouncer 启动失败并反复重试

重试CrowdSec规则更新,理想结果,第4条命令不再报错,第5条命令没有输出:

cscli hub update
cscli hub upgrade
dpkg --configure crowdsec
dpkg --configure -a
dpkg --audit

至此系统更新完成。在这次系统更新完成的时候Sury仓库还没有发布新的软件包更新,所以我是等到发布后,又更新了一遍:

apt update
apt dist-upgrade

收到了新的更新:

根据Sury的更新记录,7.4.33-31包含这两个CVE的回移修复,更新记录明确写着:

* Backported from 8.2.35
  + [CVE-2026-109618, CVE-2026-109619]: Avoid state corruption via
    bailout.

更新完成后重启PHP-FPM:

systemctl restart php7.4-fpm

对于有使用旧版本PHP需求的,我非常推荐使用Sury的仓库,该仓库作者本身就一直是Debian/Ubuntu官方PHP软件包的维护者,也是得益于他的细心且负责任的维护才让我们的一些老旧程序(垃圾程序)能够安全的运行,再次感谢Sury!另外,从这个事件也能够看出来CrowdSec在这块就是个玩具级别,漏洞它是拦不了的(waf不能第一时间出vpath),阻碍正常的系统更新是有一套。(bushi

Debian13部署DeepSeek Harness用于远程开发

2026-10-20 21:06:07

记录下DeepSeek Harness安装、反向代理、接入ChatGPT订阅、MCP配置等。

机器配置:OVH-KS-LE-B
操作系统:OVH官方Debian 13模板,无桌面环境
登录方式:SSH密钥
默认用户:debian

这个系统模板不允许SSH密码登录,也不允许用root账号登录SSH,用默认的debian用户登录上去后,创建自己喜欢的新用户:

sudo adduser imlala

按提示设置密码,姓名等信息可以直接回车跳过,系统会自动创建。之后赋予sudo权限:

sudo usermod -aG sudo imlala

复用同一把私钥,就不需要重新生成密钥了:

sudo install -d -m 700 -o imlala -g imlala /home/imlala/.ssh
sudo install -m 600 -o imlala -g imlala \
  /home/debian/.ssh/authorized_keys \
  /home/imlala/.ssh/authorized_keys

建议退出终端重新使用新的用户登录,而不是使用su等命令切换,主要是验证一下你的SSH密钥是否配置正确。登录成功后,旧的这个debian用户如不需要可以删掉。

安装xdg-user-dirs:

sudo apt install xdg-user-dirs

执行如下命令进行设置,如果你的系统是带桌面环境的(KDE、GNOME等),一般情况下可以省略这些步骤:

mkdir -p "$HOME/Documents"
xdg-user-dirs-update --set DOCUMENTS "$HOME/Documents"
xdg-user-dir DOCUMENTS

这个是为了解决DeepSeek Harness在首次启动时无法创建默认工作区的问题。服务器系统普遍过于精简且不带桌面环境,会缺少这个包和相关设置。

安装mise开发环境与多语言版本管理工具:

sudo apt install -y extrepo
sudo extrepo enable mise
sudo apt update
sudo apt install -y mise

激活mise:

echo 'eval "$(mise activate bash)"' >> ~/.bashrc
source ~/.bashrc

使用mise安装Node.js和pnpm:

mise use -g --pin node@24 pnpm@10

-g:全局安装,--pin:固定精确版本,这里额外装一个pnpm包管理器是因为DeepSeek Harness内部安装插件需要用到。经常搞前端开发的肯定也知道pnpm本身就是另一个包管理器,如果你想直接用pnpm安装DeepSeek Harness也是可以的,只是后续编写systemd配置文件的时候会麻烦一点,你可能需要配置它自己的全局命令目录和PATH,而用Node.js自带的npm的话就没这些问题。

所以这里我还是图简单就用npm安装DeepSeek Harness:

npm install -g @deepseek-ai/[email protected]

接下来为DeepSeek Harness创建systemd用户服务,注意这里是用户服务,创建用户服务需要使用这个目录:

~/.config/systemd/user

新建的用户默认不一定有这个目录,如果没有得先创建:

mkdir -p ~/.config/systemd/user

创建DeepSeek Harness的systemd用户服务配置文件:

nano ~/.config/systemd/user/dsh.service

写入如下内容:

[Unit]
Description=DSH Web

[Service]
Type=simple
WorkingDirectory=%h
ExecStart=/usr/bin/mise exec -- dsh web --no-open --host 127.0.0.1 --port 3080 --public-url https://dsh.example.com/ --trusted-host dsh.example.com
Restart=on-failure
RestartSec=5
TimeoutStopSec=30

[Install]
WantedBy=default.target

注意修改域名:

--public-url #设置外部访问地址。
--trusted-host #允许该域名通过DSH的Host校验。

启动并设置开机自启:

systemctl --user enable --now dsh

systemd用户服务在你退出登录后(终端、桌面等)会自动停止,执行下面的命令启用用户服务在退出登录后继续运行的能力:

sudo loginctl enable-linger imlala

接下来配置反向代理,安装NGINX、Certbot、htpasswd:

sudo apt install -y nginx python3-certbot-nginx apache2-utils

DeepSeek Harness的Web UI自带令牌验证,如果不放心可以再加一个basic auth,设置账号密码:

sudo htpasswd -cB /etc/nginx/dsh.htpasswd admin
sudo chown root:www-data /etc/nginx/dsh.htpasswd
sudo chmod 640 /etc/nginx/dsh.htpasswd

新建NGINX站点配置文件:

sudo nano /etc/nginx/sites-available/dsh

写入如下内容:

map $http_upgrade $dsh_connection_upgrade {
    default upgrade;
    ''      close;
}

# 不记录查询参数,避免启动链接中的token写入访问日志
log_format dsh_no_query
    '$remote_addr - $remote_user [$time_local] '
    '"$request_method $uri $server_protocol" $status $body_bytes_sent';

server {
    listen 80;
    listen [::]:80;
    server_name dsh.example.com;

    access_log /var/log/nginx/dsh.access.log dsh_no_query;
    error_log  /var/log/nginx/dsh.error.log;

    client_max_body_size 0;

    auth_basic "DSH";
    auth_basic_user_file /etc/nginx/dsh.htpasswd;

    location / {
        proxy_pass http://127.0.0.1:3080;
        proxy_http_version 1.1;

        # 保留浏览器访问的域名和端口
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $dsh_connection_upgrade;

        # Basic 认证由 Nginx 完成,不把该密码头传给 DSH
        proxy_set_header Authorization "";

        # HTTPS 在 Nginx 终止,为后端 Cookie 添加 Secure
        proxy_cookie_flags ~ secure;

        # 支持流式响应和长连接
        proxy_buffering off;
        proxy_cache off;
        proxy_connect_timeout 10s;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}

启用站点:

sudo ln -s /etc/nginx/sites-available/dsh /etc/nginx/sites-enabled/dsh

检查配置、签发证书:

sudo nginx -t
sudo certbot --nginx

查看DeepSeek Harness的Web UI访问链接:

sudo journalctl _SYSTEMD_USER_UNIT=dsh.service -f

给DeepSeek Harness接入浏览器自动化工具,这里我采用Playwright MCP + 无头Chromium的方式。无头浏览器在服务器后台运行,可以打开网页、点击、填写表单和截图等操作,无需安装桌面,只是服务器系统没有桌面普遍会缺少中文字体,安装一个中文字体可以避免网页截图中文字显示成方块。

安装Chromium和中文字体:

sudo apt update
sudo apt install -y chromium chromium-sandbox fonts-noto-cjk

添加MCP配置:

nano ~/.dsh/profiles/web/cordis.patch.yml

把下面内容作为一个新的列表项追加进去,保留原有配置。如果文件只有[],则用下面内容替换:

- insert:
    - id: mcp-playwright
      name: '@deepseek-ai/dsh-mcp-client'
      config:
        serverName: playwright
        transport: stdio
        command: npx
        args:
          - '-y'
          - '@playwright/mcp@latest'
          - '--headless'
          - '--executable-path'
          - '/usr/bin/chromium'
        toolCallTimeoutMs: 120000
        failOnStartupError: false

把ChatGPT / Codex订阅直接接入DeepSeek Harness,我推荐用这个dsh-codex-subscription插件:

dsh plugin --profile web add [email protected]

DeepSeek Harness有热更新能力,安装完成后无需重启,打开Web UI找到设置-Codex订阅-添加账号-设备代码登录。不要使用浏览器登录,这个在我们当前的环境是无法登录成功的,我们是部署在服务器上供远程访问使用,这里的浏览器登录是使用xdg-open打开本地浏览器,也就是服务器上的浏览器。

用ChatGPT Plus订阅,建议把这个subagent(子智能体)的功能给关了,不然你5小时的额度跑个稍微大一点的任务,20分钟就能给你干光:

注意不是上面的Codex子智能体,是子智能体,在里面按图设置:

DeepSeek Harness位于反向代理之后可能会遇到这个问题:#7206

安装下面的插件可解决,如该插件不兼容最新版可以在网上搜索一下,有很多同类替代品:

dsh plugin --profile web add @exagone313/[email protected]
systemctl --user restart dsh

更新大骚货:

npm install -g @deepseek-ai/dsh@目标版本号

不想用大骚货了?卸载并删除所有相关数据:

systemctl --user disable --now dsh.service
npm uninstall -g @deepseek-ai/dsh
rm -rf -- "$HOME/.dsh"
rm ~/.config/systemd/user/dsh.service
systemctl --user daemon-reload

补充记录一点内容,之前查看用户systemd服务日志的命令是:

sudo journalctl _SYSTEMD_USER_UNIT=dsh.service -f

这其实是不太合理的,按道理来说应该使用这个命令看,因为这是用户自己的服务:

journalctl --user -u dsh.service

用户自己的服务日志还需要用sudo去提权才能看这不是倒反天罡了,我这个系统当时是有点问题,用上面的命令报错:

Hint: You are currently not seeing messages from the system.
      Users in groups 'adm', 'systemd-journal' can see all messages.
      Pass -q to turn off this notice.
No journal files were opened due to insufficient permissions.

和AI周旋了一下解决了,根本原因是系统把所有journal日志都存内存里面了,上面这个提示具有迷惑性,看似是权限问题。先用这个命令检查现有配置:

systemd-analyze cat-config systemd/journald.conf

如下:

# /etc/systemd/journald.conf
#  This file is part of systemd.
#
#  systemd is free software; you can redistribute it and/or modify it under the
#  terms of the GNU Lesser General Public License as published by the Free
#  Software Foundation; either version 2.1 of the License, or (at your option)
#  any later version.
#
# Entries in this file show the compile time defaults. Local configuration
# should be created by either modifying this file (or a copy of it placed in
# /etc/ if the original file is shipped in /usr/), or by creating "drop-ins" in
# the /etc/systemd/journald.conf.d/ directory. The latter is generally
# recommended. Defaults can be restored by simply deleting the main
# configuration file and all drop-ins located in /etc/.
#
# Use 'systemd-analyze cat-config systemd/journald.conf' to display the full config.
#
# See journald.conf(5) for details.

[Journal]
#Storage=auto
#Compress=yes
#Seal=yes
#SplitMode=uid
#SyncIntervalSec=5m
#RateLimitIntervalSec=30s
#RateLimitBurst=10000
#SystemMaxUse=
#SystemKeepFree=
#SystemMaxFileSize=
#SystemMaxFiles=100
#RuntimeMaxUse=
#RuntimeKeepFree=
#RuntimeMaxFileSize=
#RuntimeMaxFiles=100
#MaxRetentionSec=0
#MaxFileSec=1month
#ForwardToSyslog=no
#ForwardToKMsg=no
#ForwardToConsole=no
#ForwardToWall=yes
#TTYPath=/dev/console
#MaxLevelStore=debug
#MaxLevelSyslog=debug
#MaxLevelKMsg=notice
#MaxLevelConsole=info
#MaxLevelWall=emerg
#MaxLevelSocket=debug
#LineMax=48K
#ReadKMsg=yes
#Audit=yes

# /usr/lib/systemd/journald.conf.d/syslog.conf
# Undo upstream commit 46b131574fdd7d77 for now. For details see
#  http://lists.freedesktop.org/archives/systemd-devel/2014-November/025550.html

[Journal]
ForwardToSyslog=yes

说明:

Storage=auto:存在/var/log/journal就持久化,否则只存内存。
SplitMode=uid:已经默认按用户拆分,无需另外开启。
ForwardToSyslog=yes:只是转发日志,与这个读取权限问题无关。

确认这个目录是否存在:/var/log/journal

ls -ld /var/log/journal /run/log/journal

/var/log/journal不存在这就解释得通了,内存日志不支持按用户拆分:

创建目录并切换到持久化即可:

sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo journalctl --flush

现在就可以不加sudo用journalctl命令查看用户自己的systemd服务日志了。

还有刚才的ForwardToSyslog=yes这个设置,意思是向syslog接口转发日志,但系统根本没有安装rsyslog软件包,也就是说系统重启后:启动日志、内核日志、服务日志都会消失。而现在已启用持久化,systemd收集到的启动日志、内核日志、服务日志,都可以用journalctl查看,保存下来的记录也能跨重启查询。

内核、systemd服务等日志
          ↓
       journald
          ├─ 内存日志:/run/log/journal → 重启丢失
          │
          └─ syslog 接口 → rsyslog
                              ↓
                    /var/log/syslog 等磁盘文件
                              ↓
                         重启后仍保留

它但凡给装个rsyslog也不会这么离谱。所以说这就是为什么在用VPS或者独立服务器的时候,能挂载iso安装就用iso安装,不能用iso就自己用preseed重装,商家的系统模板质量真的参差不齐。。。

PaperPhone:一款微信风格的端对端加密即时通讯应用

2026-10-12 00:10:20

PaperPhone这项目是我朋友一直在开发的,这里帮他推荐一下。项目主要分为两个版本:Plus、Lite,Plus比Lite多了音视频会议、朋友圈等功能。

项目的亮点是端到端加密,且使用Tor接入服务,服务器部署不需要准备域名、TLS证书或NGNIX。

这里我部署一个Lite版本的服务端,安装Docker:

apt update
apt install curl
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh

克隆存储库:

cd /opt
git clone https://github.com/619dev/PaperPhoneLite.git
cd PaperPhoneLite/

复制一份配置文件:

cp server/.env.example .env

编辑env配置文件:

nano .env

至少设置以下内容:

JWT_SECRET
DB_PASS
MYSQL_ROOT_PASSWORD
REDIS_PASS

如果你不想手动配置,上述配置全部都可以使用以下命令生成:

openssl rand -hex 32

启动:

docker compose up -d

查看Tor域名:

cat logs/onion-address.log

选择你的客户端,输入刚才查看到的Tor域名,连接服务器以开始使用,其中iOS客户端已经在AppStore上架:

https://github.com/619dev/ppl-android
https://github.com/619dev/ppl-ios
https://github.com/619dev/ppl-mac
https://github.com/619dev/ppl-win

Windows压缩卷扩容Btrfs

2026-08-29 15:03:38

我是双系统Windows10和openSUSE风滚草,openSUSE这边的文件系统是Btrfs,因为磁盘快满了所以我就想着扩容一下,这是我之前遇到的一个问题,今天有时间顺带记录一下扩容办法。

Btrfs内置了类似LVM的多盘组合功能,简单点说就是可以将多个盘的容量(或者分区)合成一个使用。这应该是Btrfs为数不多的优点了。。。

我当初是在一块硬盘上面分了100GB安装了openSUSE,这块硬盘对应我Windows系统内的D盘。我现在的操作步骤是先在Windows的磁盘管理使用压缩卷,把D盘的空间腾一部分出来,这里我压缩50GB:

成功后这里会显示未分配的空间48.83GB:

不要给这个空间创建新加卷什么的,直接重启到openSUSE,先找到在Windows里的这块D盘,这里对应的是/dev/sda,我这机器盘子有点乱,有nvme有hdd还有外挂硬盘= =:

给D盘(/dev/sda)创建新分区,这里我用cfdisk,这是我认为在Linux下创建分区最简单的一款工具了:

cfdisk /dev/sda

选中刚才从Windows压缩出来的这48.83GB:

完成后,最后一步就是把/dev/sda5加入到btrfs:

sudo btrfs device add /dev/sda5 /

检查状态,可以看到/dev/sda5已经存在于未附加里面了,btrfs后续会自动按需分配这些空间:

这个方法也适用于直接加新硬盘,你在机器上装一块新盘,也可以这么操作,反正都是加到一个存储池里面。

5分钟部署一个可供Netcatty同步的WebDAV服务

2026-08-29 14:49:06

Netcatty是一款现代化的跨平台SSH客户端和终端管理器,最近尝试切换到这个SSH客户端使用,用了也有一段时间了,确实挺好用的,开源免费还支持多端同步,为了使用它的同步功能,我自建了一个WebDAV服务,本文记录下WebDAV服务部署和Netcatty同步配置。

安装Docker:

apt update
apt install curl nginx python3-certbot-nginx
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh

创建目录和compose文件:

mkdir -p /opt/webdav/data/netcatty && cd /opt/webdav && nano docker-compose.yml

写入如下内容:

name: webdav

services:
  webdav:
    container_name: webdav
    image: ghcr.io/hacdias/webdav:latest
    ports:
     - 127.0.0.1:6065:6065
    restart: always
    volumes:
      - ./data:/data
      - ./config.yml:/config.yml:ro

新建一个配置文件:

nano config.yml

写入如下内容,注意将password改为一个高强度的密码:

address: 0.0.0.0
port: 6065
tls: false
prefix: /
debug: false
noSniff: false
behindProxy: true
directory: /data
permissions: CRUD

log:
  format: console
  colors: true
  outputs:
    - stderr
users:
  - username: netcatty
    password: hidden
    directory: /data/netcatty

启动:

docker compose up -d

请务必配置TLS反向代理,以保护basic auth的账号密码防止被中间人获取。

nano /etc/nginx/sites-available/webdav

写入如下内容:

server {
    listen 80;
    listen [::]:80;
    server_name webdav.example.com;
    client_max_body_size 0;

    location / {
        proxy_pass http://127.0.0.1:6065;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_redirect off;

        set $dest $http_destination;
        if ($http_destination ~ "^https://webdav.example.com/(?(.+))") {
            set $dest /$path;
        }
        proxy_set_header Destination $dest;
    }
}

启用站点:

ln -s /etc/nginx/sites-available/webdav /etc/nginx/sites-enabled/webdav

签发证书:

certbot --nginx

下面简单说一下Netcatty的同步配置,我主要用Windows10和openSUSE风滚草,两边设置同一个主密码,然后配置刚部署的WebDAV服务连接信息正常情况下就能用了。Windows这边没啥问题开箱即用,主要是openSUSE这边,我用的是KDE桌面,配置好了后同步报错:

这里说的钥匙串指的就是系统的安全存储,然后用这个命令看了一下当前系统用的是哪种安全存储:

busctl --user list | grep -Ei 'kwallet|secrets'

不知道为啥明明是KDE桌面却使用的是gnome-keyring:

正常情况下应该使用KDE钱包才对,然后我找到系统设置-KDE密码库:

1.在这里点击“启用KDE密码库子系统”

2.勾选使用KWallet密码库作为密码服务接口。

3.如果没有默认密码库,就点击“新建”创建一个密码库。

配置好了后光注销不行必须重启系统。再执行之前的命令验证一遍:

busctl --user list | grep -Ei 'kwallet|secrets'

正常的话应该显示:

回到Netcatty检查一下就正常了,也可以同步了:

有点抽象的是,遇到这个问题的时候openSUSE这边的Netcatty不知道怎么回事把我云端(WebDAV服务器)的数据给覆盖了。。正常情况下我Windows这边假设同步到云端有3个主机,那么openSUSE这边也应该同步获取这3个主机才对,但是openSUSE直接把云端的3个主机给删了,这导致Windows这边再去同步云端的数据把我Windows这边的3个主机也搞没了。不过还好当时我主要是测试还没正式使用,在解决KDE的问题后,双端已经能够正常同步了,只是我觉得Netcatty在处理这种意外情况的时候表现有点不佳。

Armada:基于Nostr的开源端到端加密聊天应用

2026-08-21 17:10:22

本文仅记录Armada的部署过程,有关具体的技术细节(如Nostr、Concord),这里只简单介绍一下,如果你对Armada背后的这些技术感兴趣,可以Google或者AI探索一下。

Nostr是一个简单、开放的去中心化网络协议,旨在创建一个抗审查的全球化社交网络。而Concord是基于Nostr网络构建的开源通信协议,用于私密、加密的群聊和社区。

Armada主要分为:前端 + 后端(Nostr中继)+ 语音服务(Livekit)+ Blossom服务(媒体存储服务)

关于自托管Armada,其实可以根据自己的需要来选择是否要托管上述所有的服务,如果你接受将数据保存到别人的服务器上,那么可以只托管一个前端。我们这里做事做全套,本文会把所有的服务都搭建起来。如果你选择和本文一样自建全部服务,服务器的内存最好有8GB,主要是Docker镜像在构建过程中需要较多内存以及运行时的OpenSearch需要很多内存。当然你也可以选择更轻量的开源中继服务实现,这里就不多说了。

准备工作,一个域名添加如下解析记录:

名称 类型 内容 注释
relay A 8.9.6.4 #后端(中继)服务
blossom-armada A 8.9.6.4 #媒体存储服务
av A 8.9.6.4 #音视频通话服务
armada A 8.9.6.4 #前端服务

安装Docker等需要用到的软件包:

apt update
apt install curl nginx python3-certbot-nginx git
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh

因Armada前端代码仓库托管在Nostr网络上,其地址以nostr://开头,Git本身无法识别这些地址,我们需要安装ngit,这是一个Git插件:

wget https://github.com/DanConwayDev/ngit-cli/releases/download/v2.6.3/ngit-v2.6.3-x86_64-unknown-linux-gnu.2.17.tar.gz
tar -xzvf ngit-v2.6.3-x86_64-unknown-linux-gnu.2.17.tar.gz -C /usr/local/bin/

验证安装:

ngit --version
git-remote-nostr --version

安装一下nak这个CLI工具,后续会用到:

wget https://github.com/fiatjaf/nak/releases/download/v0.20.6/nak-v0.20.6-linux-amd64
chmod +x nak-v0.20.6-linux-amd64
mv nak-v0.20.6-linux-amd64 /usr/local/bin/nak

验证安装:

nak --version

Nostr虽然是去中心化的,但是你的数据最终会保存到中继服务器和Blossom服务器,中继服务器负责传递和存储文本等结构化事件数据,而Blossom服务器专门负责大文件(如图片、视频、音频)的存储与分发。我们这里先把最关键且最核心的中继服务搭起来。其实Github上有非常多的开源Nostr中继服务实现,你并不一定要使用和我一样的中继服务实现,只是为了与Armada完美结合,这里我使用Ditto Relay,因为它支持NIP-50 / NIP-42。

cd /opt
git clone https://gitlab.com/soapbox-pub/ditto-relay.git
cd ditto-relay/

复制一份配置文件:

cp .env.example .env

编辑配置文件:

nano .env

这里只列出特别重要以及我修改过的内容:

PORT="13131"
RELAY_URL="wss://relay.example.com/"
NOSTR_NSEC="nsec..."
IP_HEADER="X-Real-IP"

1.修改RELAY_URL为你自己的域名。

2.NOSTR_NSEC填写中继的私钥,私钥可以使用之前安装的nak工具生成:

nak key generate

生成出来的内容还需要进行转换,因为Ditto Relay只支持nsec...这种格式,而nak默认生成出来的是十六进制私钥,例如你生成的值是:

fca5f6b1f172d57dbd89f3f6bb21aac93d828256082665bfa470301e1c20e549

转换为nsec:

nak encode nsec fca5f6b1f172d57dbd89f3f6bb21aac93d828256082665bfa470301e1c20e549

最终的值就是:

nsec1ljjldv03wt2hm0vf70mtkgd2ey7c9qjkpqnxt0aywqcpu8pqu4ystz0zpt

[可选]这里额外记录一下如何通过私钥获取对应的公钥,首先使用你的十六进制私钥获取:

nak key public fca5f6b1f172d57dbd89f3f6bb21aac93d828256082665bfa470301e1c20e549

对应的公钥就是:

8671a5f886f16deeda11ddd998c7fe61cdf9fc2589d5e954caa0852f9931a5bc

将公钥转换为npub...的格式:

nak encode npub 8671a5f886f16deeda11ddd998c7fe61cdf9fc2589d5e954caa0852f9931a5bc

对应的npub公钥就是:

npub1sec6t7yx79k7aks3mhve33l7v8xlnlp93827j4x25zzjlxf35k7qve74ml

3.我们使用NGINX反向代理Ditto Relay,所以IP_HEADER请务必配置为X-Real-IP或者X-Forwarded-For。

启动Ditto Relay,请注意官方没有发布预构建的镜像,目前是在本地构建镜像再启动:

docker compose up -d

Ditto Relay反向代理配置:

nano /etc/nginx/sites-available/ditto-relay

写入如下内容,请注意WebSocket是必须要启用的,Nostr的中继完全依靠WebSocket通信:

server {
    listen 80;
    server_name relay.example.com;
    client_max_body_size 0;

    location / {
        proxy_pass http://127.0.0.1:13131;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

启用站点:

ln -s /etc/nginx/sites-available/ditto-relay /etc/nginx/sites-enabled/ditto-relay

签发证书:

certbot --nginx

访问你的中继域名,应该可以看到如图所示的页面:

查看日志:

docker compose logs -f

如有类似输出,则说明中继服务一切正常:

relay-1  | {"level":"info","msg":"index_created","index":"nostr-events"}
relay-1  | {"level":"info","msg":"opensearch_connected","node":"http://opensearch:9200"}
relay-1  | {"level":"info","msg":"indexer_worker_started"}
relay-1  | {"level":"info","msg":"protocol_pool_started","workers":1}
relay-1  | {"level":"info","msg":"protocol_worker_started"}
relay-1  | {"level":"info","msg":"started","port":13131,"log_level":"info","protocol_workers":1}
relay-1  | {"level":"info","msg":"trends_scheduled","interval_ms":900000}
relay-1  | {"level":"info","msg":"bg_worker_started"}

现在我们来部署Blossom服务,与中继服务一样,Github上也有相当多的开源Blossom服务实现,你并不一定要使用和我一样的实现,这里我选择hzrd149开源的Blossom服务实现,因为该存储库的作者hzrd149正是Blossom协议规范的提出者与核心设计者。克隆blossom-server存储库到本地:

cd /opt
git clone https://github.com/hzrd149/blossom-server.git
cd blossom-server/

复制一份配置文件:

cp config.example.yml config.yml

编辑配置文件:

nano config.yml

这里只列出我修改过的内容:

port: 3005
host: 0.0.0.0
publicDomain: "blossom-armada.example.com"

storage:
  rules:
    - type: "*"
      expiration: 100 year

dashboard:
  enabled: true
  username: admin
  password: "89641937"
  lookupRelays:
    - wss://relay.example.com

1.默认的3000端口改为3005,因我的服务器3000端口被别的程序占用了。

2.publicDomain改为你自己的域名。

3.默认情况下blossom-server不永久保存用户上传的文件,我这里改为永久保存,配置成100 year是因为该程序目前有BUG,按正常情况下来说将rules这里配置成[]就可以,但实测这会导致用户无法上传任何类型的文件。

4.务必修改管理员密码,并将中继地址lookupRelays改为你自己的。

编辑一下compose文件:

nano docker-compose.yml

将端口映射改为仅监听在本地:

services:
  blossom:
    build: .
    ports:
      - "127.0.0.1:3005:3005"

启动blossom-server,该项目同样没有提供预构建的镜像,首次启动会先在本地构建:

docker compose up -d --build

blossom-server反向代理配置:

nano /etc/nginx/sites-available/blossom-server

写入如下内容:

server {
    listen 80;
    server_name blossom-armada.example.com;
    client_max_body_size 0;

    location / {
        proxy_pass http://127.0.0.1:3005;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

启用站点:

ln -s /etc/nginx/sites-available/blossom-server /etc/nginx/sites-enabled/blossom-server

签发证书:

certbot --nginx

现在我们来部署前端,前端就是一个纯静态的网站,仓库内提供了一个Dockerfile构建脚本,该脚本会构建网站并使用NGINX在80端口上提供服务。让我们先克隆仓库:

cd /opt
git clone nostr://soapbox.pub/relay.ngit.dev/armada
cd armada

配置是在构建时作为Docker构建参数嵌入的,新建一个compose文件:

nano docker-compose.yml

写入如下内容:

services:
  kirara-chat:
    image: moyu-chat:latest
    container_name: moyu-chat
    restart: always
    build:
      context: . # 以当前Git根目录作为构建上下文
      args:
        VITE_APP_NAME: "MoYu Chat"
        VITE_APP_RELAYS: "wss://relay.example.com"
        VITE_SEARCH_RELAYS: "wss://relay.example.com"
        VITE_APP_BLOSSOM_SERVERS: "https://blossom-armada.example.com"
        VITE_CONCORD_AV_SERVERS: "https://av.example.com"
    ports:
      - "127.0.0.1:8085:80"

1.完整的构建时参数(环境变量)见:Configuration (build-time env)

2.构建时的参数,如果修改了这些内容,必须重新构建,否则新配置是不会生效的。

3.该镜像使用NGINX在80端口提供服务,但我们使用反向代理,所以端口映射这里配置的端口为8085。

构建并启动前端:

docker compose up -d --build

前端反向代理配置:

nano /etc/nginx/sites-available/armada

写入如下内容:

server {
    listen 80;
    server_name armada.example.com;
    client_max_body_size 0;

    location / {
        proxy_pass http://127.0.0.1:8085;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

启用站点:

ln -s /etc/nginx/sites-available/armada /etc/nginx/sites-enabled/armada

签发证书:

certbot --nginx

现在我们来部署语音服务器,这也是最麻烦的一步了,因为该项目的默认设置没有考虑到将所有服务部署在同一台服务器内的场景,这导致我们需要修改一些配置才能使其与之前部署的这些服务共存。如果你愿意单独将语音服务部署在另外一台服务器内,那没什么好说的,官方的配置直接就能跑起来,但我还是更倾向于将一个完整的项目部署在同一台服务器内。

克隆语音服务的存储库:

git clone nostr://[email protected]/relay.ngit.dev/armada-av
cd armada-av/deploy/single

复制一份配置文件:

cp .env.example .env

编辑.env:

nano .env

需要修改的内容如下:

AV_DOMAIN=av.example.com
LIVEKIT_API_KEY=
LIVEKIT_API_SECRET=

1.AV_DOMAIN请与之前在构建前端时的配置VITE_CONCORD_AV_SERVERS保持一致

2.LIVEKIT_API_KEY / LIVEKIT_API_SECRET使用如下命令生成:

docker run --rm livekit/livekit-server generate-keys

编辑compose文件:

nano docker-compose.yml

将Caddy服务注释或者删除掉,以防止Caddy抢占主机NGINX的80 / 443端口,另外一定要将livekit的镜像TAG从v1.8改为latest,v1.8已经无法与Armada的前端一起正常工作了,这是我踩过的一个坑,可能是作者忘了修改:

# One box: the broker, one LiveKit node, and TLS. `cp .env.example .env`, edit,
# then `docker compose up -d`.
#
# All three services share the host network namespace: media is latency-
# sensitive UDP at thousands of packets per second, and Docker's userland
# proxy adds per-packet cost, so this is the configuration LiveKit itself
# recommends.

services:
  livekit:
    image: livekit/livekit-server:latest
    network_mode: host
    restart: unless-stopped
    command: --config /etc/livekit.yaml
    volumes:
      - ./livekit.yaml:/etc/livekit.yaml:ro
    environment:
      LIVEKIT_KEYS: "${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}"

  broker:
    build:
      context: ../..
    network_mode: host
    restart: unless-stopped
    depends_on:
      - livekit
    environment:
      PORT: "8086"
      # Loopback only. All three services share the host's network namespace,
      # so without this the broker answers on every interface — and with
      # TRUST_PROXY on, a caller reaching it directly could forge
      # X-Forwarded-For and rotate its rate-limit key at will. Caddy is the
      # only way in.
      HOST: "127.0.0.1"
      # What clients sign their grants against — the public name, not the
      # container's. A mismatch here rejects every request (CORD-07 §2 `u` tag).
      PUBLIC_ORIGIN: "https://${AV_DOMAIN}"
      # What clients are told to connect to. A BARE origin: the LiveKit SDK
      # appends /rtc itself, and a url that already carries it breaks signaling.
      LIVEKIT_URL: "wss://${AV_DOMAIN}"
      # Health probes take the loopback shortcut rather than going out and back
      # through TLS.
      LIVEKIT_API_URL: "http://127.0.0.1:7880"
      LIVEKIT_API_KEY: "${LIVEKIT_API_KEY}"
      LIVEKIT_API_SECRET: "${LIVEKIT_API_SECRET}"
      # Caddy sets X-Forwarded-For, and it is the only thing in front of us. The
      # broker reads the client IP from the rightmost entry (the peer Caddy saw),
      # so a client cannot spoof its rate-limit key by stuffing the header.
      TRUST_PROXY: "1"
      TOKEN_TTL_SECONDS: "${TOKEN_TTL_SECONDS:-21600}"
      RATE_LIMIT_PER_MINUTE: "${RATE_LIMIT_PER_MINUTE:-60}"

#  caddy:
#    image: caddy:2-alpine
#    network_mode: host
#    restart: unless-stopped
#    volumes:
#      - ./Caddyfile:/etc/caddy/Caddyfile:ro
#      - caddy_data:/data
#      - caddy_config:/config
#    environment:
#      AV_DOMAIN: "${AV_DOMAIN}"

#volumes:
#  caddy_data:
#  caddy_config:

启动:

docker compose up -d --build

该项目必须使用反向代理,现在改为用主机的NGINX反向代理:

nano /etc/nginx/sites-available/armada-av

写入如下内容:

server {
    listen 80;
    server_name av.example.com;
    client_max_body_size 0;

    location /rtc {
        proxy_pass http://127.0.0.1:7880;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    }

    location ~ ^/\.well-known/concord/av {
        proxy_pass http://127.0.0.1:8086;
    }

    location / {
        default_type text/plain;
        return 200 "armada-av";
    }
}

启用站点:

ln -s /etc/nginx/sites-available/armada-av /etc/nginx/sites-enabled/armada-av

签发证书:

certbot --nginx

到这里整个项目就全部部署完成了,该项目目前还在大力开发中(大量使用AI)基本每天发布一个新版本,如果没有重大的破坏性更改,那么要更新的话基本上就是拉新代码、重新构建:

git pull
docker compose up -d --build

接下来简单说一下如何使用Armada,因为Nostr中继的特殊性,你自建了中继,并不代表你的数据只存在于你自建的中继内,如果你没有预先设置好,你的数据可能会发往多个公共的中继。当然这是Nostr设计之初的意图:去中心化。且Armada本身就是端到端加密的,即便你把数据写到其它的中继了,中继服务器也看不到明文。

别说中继了,就是你通过Armada上传到blossom-server的大部分图片、视频、音频等文件都是加密的,相当于blossom-server服务器管理员知道你上传了一张图片文件,但看不到你的图片是什么内容。所以安全这块不用担心。

但我想的是完全掌控自己的数据,我不需要其它中继参与进来,我想在Nostr上建一个封闭的圈子,所有的数据只存在于我自己的中继服务器和blossom-server服务器内。所以下面的配置可能不适用于每个人。

首次访问Armada会显示下图内容,点加入:

如果你没有账户就点击创建账户按钮,你也可以使用nak再生成一个私钥,这里所谓的账户其实就是一串nsec格式的私钥:

生成私钥:

妥善保管你的私钥,这是你账户唯一的登录凭证:

登录后,找到社区中继,把这些中继全部删除,然后添加自己的:

或者在创建群组的时候,这里只使用自己的中继,那么这个群组就只存在于你自己的中继上:

还有一个用于DM(私聊)的中继,这里默认有一个relay.armada.buzz(Armada官方的中继)删掉:

请注意的上面配置仅生效于你自己的账号,别人使用你自建的Armada客户端还是遵循Armada默认的配置,啥意思呢?比如另外一个人用你的Armada客户端给你发消息,Armada默认的DM配置中有一个relay.armada.buzz,这是Armada官方运营的中继,他没删除这个中继的话,他发给你的消息就会存储到relay.armada.buzz这个中继。目前这个设置没办法全局生效,这算是一个小问题吧。啥时候官方能够支持在构建镜像的时候指定这个配置就好了。

只使用自己的中继,如何确保别人能够找到我?给我发消息?配置一下NIP-65就行了,把自己的中继加上去,点保存:

这会把你当前使用的中继列表发送给purplepag.es,一个存储用户NIP-65列表的公共“黄页”中继,类似于通讯录。别人(别的客户端)就可以通过查询purplepag.es知道你的专属中继地址,从而正确地读取你的动态并向你发送消息。除了purplepag.es外,Armada默认还会将列表发给user.kindpag.es和relay.nos.social。这个行为可以在构建镜像的时候使用VITE_NIP65_DISCOVERY_RELAYS指定要发送的中继,但我觉得没有必要去改动。

除了中继,还有媒体服务,确保使用的是自己的:

音视频通话我也试了一下,群组里面可以正常用,私聊不行,应该是BUG,之前用自建的服务器是点了双方都没反应,现在是点了之后对方没反应,过一会儿提示no answer就自动挂断了。。。不过有点神奇的是,大概一个月以前私聊可以用官方的服务器,现在连官方的服务器都不行了。。。

这应该是目前Nostr生态环境中比较成熟的聊天项目了,除了上述的私聊音视频通话有点问题外,没发现明显的BUG,E2EE特别牛逼,群聊甚至发送的图片都是加密的。硬要说缺点的话就是使用起来有点门槛,得先了解什么是Nostr,尤其是各种NIP。这让我想起了某人的一句话:Nostr去中心化的意思就是有一堆中心化的中继,以至于你不知道哪个是中心。。。