静看光阴荏苒
不管不顾不问不说也不念

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

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

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

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

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

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

名称类型内容注释
relayA8.9.6.4#后端(中继)服务
blossom-armadaA8.9.6.4#媒体存储服务
avA8.9.6.4#音视频通话服务
armadaA8.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://chad@chadwick.site/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去中心化的意思就是有一堆中心化的中继,以至于你不知道哪个是中心。。。

赞(0)
未经允许不得转载:荒岛 » Armada:基于Nostr的开源端到端加密聊天应用
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

分享创造快乐

广告合作资源投稿