End-to-end encrypted relays for OCapN

本文为原文前 6,000 字符的节选翻译,完整内容请查看原文。

End-to-end encrypted relays for OCapN

OCapN 的端到端加密中继

Jessica Tallon — October 15, 2026 Jessica Tallon — 2026年10月15日

Creating a direct connection between any two computers (not just from client to server) over the internet is notoriously difficult. Many home networks are behind NATs which prevent new inbound connections. Sometimes these NATs are from the ISP (carrier-grade NATs) which are impossible to work around with port forwarding and/or UPnP. While hole punching techniques can break through NATs, it’s far from guaranteed. Thus, we need something that works in the general case: relays. 在互联网上建立任意两台计算机(不仅仅是客户端到服务器)之间的直接连接是出了名的困难。许多家庭网络位于 NAT 之后,这会阻止新的入站连接。有时这些 NAT 来自 ISP(运营商级 NAT),无法通过端口转发和/或 UPnP 来解决。虽然打洞技术可以穿透 NAT,但远不能保证成功。因此,我们需要一种在一般情况下都能工作的方法:中继。

Relays are servers, accessible over the public internet, that act as a proxy between two computers that cannot directly connect. In Goblins 0.12 we introduced a federated relay named “prelay”. While OCapN traffic is encrypted in transit, the prelay crucially lacks end-to-end encryption (E2EE). E2EE secures all the messages so they can only be read by their intended recipient, preventing intemediatory parties (like the relay) from tampering with or reading them. Due to the lack of E2EE, the relay was named “prelay” (pre-relay) to dissuade its use. 中继是可通过公共互联网访问的服务器,充当两台无法直接连接的计算机之间的代理。在 Goblins 0.12 中,我们引入了一个名为“prelay”的联邦中继。虽然 OCapN 流量在传输过程中是加密的,但 prelay 关键性地缺乏端到端加密 (E2EE)。E2EE 对所有消息进行保护,使其只能由预期的接收者读取,从而防止中间方(如中继)篡改或读取它们。由于缺乏 E2EE,该中继被命名为“prelay”(预中继),以劝阻人们使用它。

Thanks to a grant from NLnet, we’re happy to announce that the next release of Goblins will include a federated, E2EE relay implementation! Having a relay that we could recommend has become even more of a critical part of the OCapN story as we’ve developed Hoot, our Scheme to WebAssembly compiler. The web is a challenging environment in which to build peer-to-peer applications. Most of the patterns and available APIs are designed around client-server architectures and centralized certificate authorities for security. While relays are unsatisfying compared to making direct connections, they enable peer-to-peer applications to communicate in these hostile environments. 感谢 NLnet 的资助,我们很高兴地宣布,Goblins 的下一个版本将包含一个联邦式的 E2EE 中继实现!随着我们开发出 Scheme 到 WebAssembly 的编译器 Hoot,拥有一个我们可以推荐的中继已成为 OCapN 故事中更为关键的一部分。Web 是构建点对点应用程序的挑战性环境。大多数模式和可用 API 都是围绕客户端-服务器架构和用于安全的集中式证书颁发机构设计的。虽然与直接连接相比,中继并不令人满意,但它们使点对点应用程序能够在这些敌对环境中进行通信。

In addition to implementing the E2EE relay for Goblins, our NLnet grant also includes writing a specification for the OCapN group so that the relay can be broadly supported by other OCapN implementations. 除了为 Goblins 实现 E2EE 中继外,我们的 NLnet 资助还包括为 OCapN 小组编写规范,以便该中继能够得到其他 OCapN 实现的广泛支持。

Design 设计

We wanted OCapN relays to have the following qualities: Stateless: no storage of user data; easy to replace or migrate Federated: reduces the power any one relay holds over the wider network End-to-end encrypted: prevents man-in-the-middle and replay attacks; perfect forward secrecy 我们希望 OCapN 中继具备以下品质:无状态:不存储用户数据;易于替换或迁移。联邦化:减少任何单个中继对更广泛网络的影响力。端到端加密:防止中间人攻击和重放攻击;具备完善的前向安全性。

Our existing “prelay” already has the first two properties. To get E2EE, we chose to build upon the Noise Protocol. Noise is actually a well-established framework of different patterns and ciphers. This allows applications to pick the pattern which is most suitable to their needs. It’s also a protocol family which has widespread use (WireGuard, WhatsApp, etc.) and has been scrutinised a great deal. None of us at Spritely are cryptographers by training, so using an off-the-shelf protocol also ensures we’re avoiding any mistakes in protocol design. 我们现有的“prelay”已经具备了前两个属性。为了实现 E2EE,我们选择基于 Noise 协议进行构建。Noise 实际上是一个由不同模式和密码组成的成熟框架。这允许应用程序选择最适合其需求的模式。它也是一个被广泛使用的协议族(WireGuard、WhatsApp 等),并经过了大量的审查。我们 Spritely 的成员都不是受过专业训练的密码学家,因此使用现成的协议也能确保我们避免协议设计中的任何错误。

So how can we use Noise with OCapN? When connecting to an OCapN peer, there is a Locator which encodes three components about the remote peer: Designator: self-authenticating identity (usually a public key) Transport: a symbol identifying the kind of netlayer it is (such as onion) Hints: a hashmap of routing data (such as IP, port, etc.) Each netlayer decides what goes into these fields, and the union of both the designator and transport should uniquely identify the peer. 那么我们如何将 Noise 与 OCapN 一起使用呢?当连接到 OCapN 对等点时,有一个定位器 (Locator),它编码了关于远程对等点的三个组件:标识符 (Designator):自认证身份(通常是公钥);传输 (Transport):标识其网络层类型的符号(例如 onion);提示 (Hints):路由数据的哈希映射(例如 IP、端口等)。每个网络层决定这些字段的内容,标识符和传输的结合应该能唯一地识别该对等点。

Noise uses two kinds of keys: static: a key that doesn’t change between sessions ephemeral: a key that is generated fresh for each session An important step each netlayer needs to consider is something called peer authentication, which verifies that a peer is who they say they are. OCapN requires that there exists at most one connection between any two peers at a given time. Ensuring there is only ever one session between any two peers is essential to allow for object identity comparison and preventing unnecessary 3rd party handoffs. An OCapN implementation will enforce this by reusing connections or closing new inbound connections if one already exists. Without peer authentication, a malicious peer could impersonate another peer causing a denial of service to the peer whose identity was stolen. Noise 使用两种密钥:静态密钥:在会话之间不改变的密钥;临时密钥:为每个会话新生成的密钥。每个网络层需要考虑的一个重要步骤是所谓的对等点认证,它验证对等点是否是其声称的身份。OCapN 要求在任何给定时间,任意两个对等点之间最多存在一个连接。确保任意两个对等点之间始终只有一个会话,对于允许对象身份比较和防止不必要的第三方移交至关重要。OCapN 实现将通过重用连接或在已有连接存在时关闭新的入站连接来强制执行此操作。如果没有对等点认证,恶意对等点可能会冒充另一个对等点,从而对身份被盗用的对等点造成拒绝服务。

As described above, the designator is a public key, which allows the peer to prove their identity by using its private key. Noise has a pattern for this known as XK, where the initiator knows the key of the responder and will verify it. The X is referring to the initiator’s static key being transmitted to the responder. The K is referring to the initiator already knowing the responder’s public key. The public key of a peer is encoded and placed in the designator for the initiator to use. 如上所述,标识符是一个公钥,它允许对等点通过使用其私钥来证明自己的身份。Noise 有一种为此设计的模式,称为 XK,其中发起者知道响应者的密钥并会对其进行验证。X 指的是发起者的静态密钥被传输给响应者。K 指的是发起者已经知道响应者的公钥。对等点的公钥被编码并放置在标识符中供发起者使用。

Noise also allows the use of different key generation algorithms, cipher suites, and hashes. This is great as we needed to select something which has implementations both in commonly available cryptographic libraries and within the browser (for Hoot). X25519 keys are well supported in all those situations, as are AESGCM cipher suites and SHA256 hashes. These are all available in GnuTLS (what we’re using on the Guile VM) and in the SubtleCrypto API (that we’re using on the web). Noise 还允许使用不同的密钥生成算法、密码套件和哈希。这非常好,因为我们需要选择在常用加密库和浏览器中(对于 Hoot)都有实现的东西。X25519 密钥在所有这些情况下都得到了很好的支持,AESGCM 密码套件和 SHA256 哈希也是如此。这些都可以在 GnuTLS(我们在 Guile VM 上使用的)和 SubtleCrypto API(我们在 Web 上使用的)中找到。

Guile Noise library Guile Noise 库

Of course, to use Noise we need an implementation. Noise is popular enough that there’s quite a few implementations for different languages. Unfortunately, Guile is not one of them, so that meant writing our own Noise library. The Guile Noise library we’ve written doesn’t require users to know the intricate workings of Noise, but Noise is interesting and so let’s take a brief look at how it works and my implementation experience. The Noise specification is actually pretty approachable and is built around three conceptual objects: CipherState: holds a key and a hash; provides encryption, decryption, and rekeying SymmetricState: holds a chaining key and hash output; uses CipherState for encryption, as well as deriving new keys and hashes HandshakeState: holds state for the initial handshake and handles reading and writing of handshake messages The Noise protocol starts with a hand 当然,要使用 Noise,我们需要一个实现。Noise 非常流行,以至于有许多针对不同语言的实现。不幸的是,Guile 不在其中,这意味着我们需要编写自己的 Noise 库。我们编写的 Guile Noise 库不需要用户了解 Noise 的复杂工作原理,但 Noise 本身很有趣,所以让我们简要了解一下它是如何工作的以及我的实现经验。Noise 规范实际上非常易于理解,并且围绕三个概念对象构建:CipherState:保存密钥和哈希;提供加密、解密和重新生成密钥的功能。SymmetricState:保存链式密钥和哈希输出;使用 CipherState 进行加密,以及派生新的密钥和哈希。HandshakeState:保存初始握手的状态,并处理握手消息的读取和写入。Noise 协议从握手开始。