引言:当"你是谁"变成最危险的问题
在Signal里,你的身份是手机号;在Matrix里,你的身份是域名下的用户名;在P2P网络中,你的身份是公钥。几乎所有通讯协议都在某个层面回答了"你是谁"这个问题------而SimpleX Chat的回答是:这个问题本身就不该存在。
SimpleX Chat 是目前唯一一个不存储任何用户标识符 的通讯协议。没有手机号、没有用户名、没有邮箱、甚至没有持久化的公钥身份。它的 GitHub 仓库(simplex-chat/simplex-chat(https://github.com/simplex-chat/simplex-chat))和消息代理(simplex-chat/simplexmq(https://github.com/simplex-chat/simplexmq))共同实现了一套完全基于一次性单向队列的通讯架构。
本文将深入源码层面,拆解 SimpleX 如何通过 3 层队列架构 + Haskell STM 并发模型实现这一目标。
一、核心思想:用队列地址替代用户 ID
传统通讯协议构建的是"用户到用户"的图:
text
Alice(手机号:138xxxx) ────> Server ────> Bob(用户名:@bob)
任何能接触到服务端的人都能绘制出完整的社交图谱。这就是元数据泄露------很多时候,知道"谁在和谁通话"比通话内容本身更有价值。
SimpleX 的做法是把这张图拆解为资源到资源的单向管道:
text
Alice的发送队列 ──> SMP Server A ──> Bob的接收队列
Alice的接收队列 <── SMP Server B <── Bob的发送队列
每个队列都是一次性的、可随时销毁和替换的。 服务端只知道自己管理了若干队列,但不知道哪个队列属于哪个用户,更不知道哪些队列构成了一个"对话"。
下面是源码中 SMP 协议的核心类型定义(来自 simplexmq/src/Simplex/Messaging/Protocol.hs):
haskell
-- SMP 协议的命令只有 10 个
data Command (p :: Party) where
-- 接收方命令
NEW :: Command Recipient -- 创建队列
SUB :: Command Recipient -- 订阅队列
KEY :: Command Recipient -- 绑定密钥
ACK :: Command Recipient -- 确认消息
OFF :: Command Recipient -- 暂停队列
DEL :: Command Recipient -- 删除队列
-- 发送方命令
SEND :: Command Sender -- 发送消息
PING :: Command Sender -- 心跳检测
-- 通知
Ntf :: Command Recipient -- 接收通知
-- 服务器响应只有 8 种
data Response where
IDS :: Response -- 队列ID返回
OK :: Response -- 操作确认
ERR :: Response -- 错误
MSG :: Response -- 消息投递
-- ...
10 条命令、8 种响应------这就是整个传输层协议的完整词汇表。极简的程度堪比 Redis 的 RESP 协议。这种极简设计本身就是安全特性:功能越少,攻击面越小。
二、3 层队列架构详解
SimpleX 的架构分为三个递进层次,每一层都建立在下一层之上:
第 1 层:SMP(SimpleX Messaging Protocol)------单向哑管道
这是最底层的传输协议。SMP 服务器的设计哲学是:做一个一无所知的"哑管道"。
关键设计约束:
- 队列是**单向的**(simplex),消息只能从发送方流入、从接收方流出
- 服务器之间**绝不通信**------不存在联邦协议
- 每个队列由一对 Ed448 密钥保护:接收方密钥和发送方密钥
- 访问授权通过**签名验证**实现,而非用户认证
haskell
-- SMP 服务器的核心状态:只存储队列,不存储用户
data ServerState = ServerState
{ queues :: TVar (Map QueueId QueueState) -- 队列映射表
, msgQueue :: TQueue ServerMessage -- 内部消息通道
, subscribers :: TVar (Map QueueId Subscriber) -- 活跃订阅者
}
-- 注意:没有任何 User 类型,没有任何身份表
第 2 层:SMP Agent------双队列组合器
SMP Agent 负责将两个单向队列组合成一个双向连接。这是关键的一层抽象:
haskell
-- Agent 将两个 simplex 队列组合为一个 duplex 连接
data DuplexConnection = DuplexConnection
{ sendQueue :: SMPQueueInfo -- 我的发送队列(对方的接收队列)
, receiveQueue :: SMPQueueInfo -- 我的接收队列(对方的发送队列)
, encryptKey :: PublicKey -- 端到端加密公钥
, verifyKey :: PublicKey -- 签名验证公钥
}
-- Agent 管理所有连接
data AgentState = AgentState
{ connections :: TVar (Map ConnId DuplexConnection)
, msgQueue :: TQueue AgentMessage
, e2eKeys :: TVar (Map ConnId E2EKeyPair)
}
-- 仍然没有任何用户 ID 字段
Agent 的核心职责:
- **队列生命周期管理**:创建、轮换、销毁队列
- **消息加密/解密**:在每个连接上维护 Double Ratchet 状态
- **重连与故障转移**:当某个 SMP 服务器不可用时,透明切换到备用服务器
第 3 层:Chat Client------用户界面与高层协议
这是用户直接交互的终端层。它负责:
- 联系人管理(将 `ConnId` 映射为人可读的显示名------仅本地)
- 群组逻辑(通过多个 `DuplexConnection` 实现)
- 文件传输、语音通话等高层功能
三、Haskell STM:无锁并发的核心引擎
SimpleX 选择 Haskell 的一个关键原因是其软件事务内存(STM) 模型。源码中大量使用 TVar、TMVar、TQueue 和 TChan:
haskell
-- 简化版 SMP 消息投递流程(可运行示例)
import Control.Concurrent.STM
import Control.Concurrent
import qualified Data.Map.Strict as M
import Data.ByteString.Char8 as B
-- | 模拟 SMP 消息队列
data SMPQueue = SMPQueue
{ qId :: Int
, messages :: TVar [B.ByteString] -- STM 保护的消息列表
}
-- | 投递消息到队列(发送方视角)
sendMessage :: SMPQueue -> B.ByteString -> STM ()
sendMessage q msg = do
msgs <- readTVar (messages q)
writeTVar (messages q) (msgs ++ [msg])
-- | 拉取并清空消息(接收方视角)
receiveMessages :: SMPQueue -> IO [B.ByteString]
receiveMessages q = atomically $ do
msgs <- readTVar (messages q)
writeTVar (messages q) []
pure msgs
-- | 演示:两个线程同时操作,STM 保证一致性
main :: IO ()
main = do
-- 创建队列(等效于 SMP NEW 命令)
q <- atomically $ do
let q = SMPQueue 42 (error "TVar not initialized")
msgs <- newTVar []
pure $ q { messages = msgs }
-- Alice 线程:持续发送消息
_ <- forkIO $ replicateM_ 5 $ atomically $ do
sendMessage q "Hello from Alice"
sendMessage q "How are you?"
-- Bob 线程:延迟后接收消息
_ <- forkIO $ do
threadDelay 100000 -- 等待 100ms
msgs <- receiveMessages q
putStrLn $ "Bob 收到的消息条数: " ++ show (Prelude.length msgs)
mapM_ (B.putStrLn . B.append " -> ") msgs
threadDelay 500000 -- 等待所有线程完成
putStrLn "✓ 演示完成:没有用户 ID,只有队列操作"
这段代码展示了 SimpleX 架构中最核心的概念:发送方和接收方通过队列交互,彼此不需要知道对方身份,队列本身也不携带任何身份信息。 STM 保证了并发安全------多个发送方可以同时写入而不会出现竞态条件。
四、加密的洋葱:4 层防护体系
SimpleX 的加密并非只有一层 E2E,而是一个4 层洋葱模型:
| 层 | 算法 | 防护目标 |
|---|---|---|
| 传输层 | TLS 1.3(双向认证) | 防中间人篡改 TCP 流 |
| 队列层 | NaCl crypto_box(Curve25519) | 防止 SMP 服务器读取消息内容 |
| 端到端层 | Double Ratchet(Curve448 + X3DH) | 前向安全性 + 后向安全性 |
| 抗关联层 | 额外服务端→接收方加密 | 防止流量关联分析 |
关键洞察:第 2 层(队列加密)和第 4 层(抗关联加密)是 SimpleX 区别于 Signal 等传统 E2E 协议的关键。Signal 只有传输层和端到端层------服务端能看到"Alice 向 Bob 发送了一条消息",只是读不了内容。SimpleX 的服务端连"谁向谁发送了消息"都不知道。
五、队列生命周期:一次对话的全流程
text
1. [Bob 创建接收队列]
Bob → SMP Server: NEW
SMP Server → Bob: queueId=0x3F2A, recipientKey=Kb1, senderKey=Kb2
2. [Bob 将队列地址分享给 Alice](通过二维码/链接,带外传输)
地址内容: smp://server-hash::queueId=0x3F2A::encryptKey=Kb2
3. [Alice 发送消息]
Alice → SMP Server: SEND queueId=0x3F2A, "Hello"
Alice 用 Kb2 签名以证明自己是合法发送方
4. [Bob 接收消息]
Bob → SMP Server: SUB queueId=0x3F2A
SMP Server → Bob: MSG "Hello"
Bob → SMP Server: ACK (删除服务端消息副本)
5. [队列轮换------每 N 条消息后]
Bob 创建新队列 queueId=0x7D1E
通过现有加密通道将新地址发送给 Alice
旧队列被删除
每一步服务端都只知道孤立的队列操作,不知道这 5 步构成了一个对话。
六、为什么这很重要
2025 年 Signal 被迫关闭瑞典服务器的事件证明了一件事:只要服务端存有用户标识符,无论加密多强,元数据泄露的风险就永远存在。 瑞典政府要求 Signal 在消息中植入后门被拒后,试图通过分析元数据来获取通讯图谱------这正是 SimpleX 从架构层面解决的问题。
SimpleX 的选择是激进的:放弃全局身份,换取绝对的元数据隐私。 代价是用户体验上的小幅妥协(需要通过链接/二维码添加联系人),换来的是技术上可证明的隐私保证------即使 SMP 服务器被完全攻破,攻击者也拿不到社交图谱,因为图谱根本不存在于服务端。
从源码设计的角度看,SimpleX 的价值不仅在于它做了什么,更在于它坚决不做什么:
- 不定义 `User` 类型
- 不存储任何标识符
- 不让服务器之间通信
- 不持久化消息(收到 ACK 即删除)
这种"通过不设计来设计"的思路,是所有注重隐私的系统都应该学习的范式。
参考资料
- SimpleX Chat 主仓库(https://github.com/simplex-chat/simplex-chat)
- SimpleXMQ 消息代理(https://github.com/simplex-chat/simplexmq)
- SimpleX 协议白皮书(https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md)
- DeepWiki - SimpleX 核心架构(https://deepwiki.com/simplex-chat/simplex-chat/2-core-architecture)