跳到主要内容

网络层

主机 (Host) 是一个 WebSocket 服务器,监听来自终端的传入连接。

终端 (Terminal) 是连接到主机的 WebSocket 客户端。

本文介绍了主机和终端是如何建立连接的,以及终端和终端之间是如何交换信息的。

主机通常可以通过互联网访问,而终端通常位于本地网络中,无法从互联网访问。因此,终端可以连接到主机并通过主机相互传输消息。

终端是对等的,可以被视为一个 P2P 网络。

主机是一个傻组件,仅在终端之间转发消息。而终端是智能组件,可以处理消息。

创建主机

您可以使用我们提供的主机集群服务 (wss://hosts.ntnl.io),或自行部署一个主机集群 (@yuants/app-hosts)。

您需要先生成一个 ED25519 密钥对,然后使用私钥签名一个空字符串,得到签名。将公钥和签名拼接到主机连接 URL 中,即可创建一个主机 URL。

主机 URL 形如:

wss://hosts.ntnl.io/?public_key=84KD3d2vGLnYo5ars7eNG7KxZwWai2snuJBMFU8kbeg5&signature=3NP1aeyn88Lj7xJRoaDvzpfcLP8mMEp7CgMyXoQXS6MH5cpnpy62GkdeCkZhSwUdv4EJh8RX761e633cc5QtqeYw

其中,public_key 是主机的公钥;signature 是主机的签名,是用主机私钥对 空字符串 加签名得到的结果。

您需要用这个 URL 来连接到主机。主机集群收到此公钥和签名后,可以验证签名是否与公钥匹配,从而允许您连接到主机。

加入主机的终端并不一定需要知道主机的私钥,终端从主机的创建者处获得主机的连接 URL 即可。

如果私钥意外泄漏,主机的创建者可以随时创建一个新的主机密钥对,将所有服务迁移到新的主机上。

注意

切勿将主机 URL 随意泄露给他人。知道主机令牌的任何人都可以连接到主机并向终端发送消息。

这可能会导致严重的安全问题并损失您的资金和秘密。

连接主机

如果您已经有了主机 URL,您可以使用 Web GUI、@yuants/protocolTerminal 类或者任意其他 WebSocket 客户端连接到主机。

终端在连接主机时,需要在主机 URL 后面带上终端 ID,形如:

wss://hosts.ntnl.io/?public_key=84KD3d2vGLnYo5ars7eNG7KxZwWai2snuJBMFU8kbeg5&signature=3NP1aeyn88Lj7xJRoaDvzpfcLP8mMEp7CgMyXoQXS6MH5cpnpy62GkdeCkZhSwUdv4EJh8RX761e633cc5QtqeYw&terminal_id=haha

其中,terminal_id 是终端的 ID。它在主机中应该是唯一的。这类似于一个 IP 地址。

安全性

讨论安全性之前,首先要明确:我们是谁?我们要防谁?

Yuan 中,所有有意义的数据都是由终端生成的。我们要站在终端的角度思考安全性。

  1. 网络中间人攻击。例如,网络提供商,黑客等会不会窃听我的消息?
  2. 软件供应链攻击。例如,开发者会不会在代码中植入后门?
  3. 来自主机集群的攻击。例如 hosts.ntnl.io 会不会监听我的消息?
  4. 来自主机创建者的攻击。例如,主机的创建者会不会对我不利?
  5. 来自主机中的其他终端的攻击。例如,主机中的其他终端会不会伪造自身的身份,诱导我将隐私数据发送给他们?

如何防止网络中间人攻击

使用 TLS 证书,将主机 URL 改为 wss:// 协议。不要使用 ws:// 协议。

更多资料可以参考 维基百科 - 传输层安全性协议

如何防止软件供应链攻击

我们的代码是开源的,受到开源社区的监督,您可以参与审查源代码。我们不会在代码中做任何损害用户利益的事情。

另外,您可以检查代码包的哈希值,确保您下载的代码包没有被篡改。现代 npm 包管理器已经支持了这一功能,会在安装时自动检查哈希值。

如何防止来自主机集群的攻击

来自主机集群的攻击本质上类似于网络中间人攻击。主机集群由于是网络提供商,它有能力窃听、篡改通过它的消息。

但是,由于主机内部的终端通常都是互相认识的一伙人,他们可以采取预共享密钥的方式,保证主机集群不会窃听消息。

如果终端知道主机私钥,这意味着终端是由主机创建者部署的,或者受到了创建者完整的授权的,那么终端可以直接通过这个私钥派生出一个 AES-GCM 对称密钥,然后将消息加密后发送,由于主机集群不知道私钥,也就无法知道其派生的对称密钥,所以无法解密消息。

如果终端不知道主机的私钥,它可以在部署时,要求主机创建者提供额外的预共享对称密钥。这样仍然可以保证不泄露私钥的情况下,终端可以防御主机集群的攻击。

顺便一提,如果主机集群没有尽责地隔离主机之间的通信,使得另一个主机得以接收您的消息、或者向您发送消息,那也等同于来自主机集群的攻击的情形。我们可以少讨论一种情形,即来自其他主机的攻击。

如何防止来自主机创建者的攻击

好问题,这是一个难题。但你为什么要冒险加入这个主机呢?这是您自己的选择。

为什么不自己成为一个主机创建者呢?您可以自己创建一个主机。这样您就不用担心主机创建者的攻击了。

如何防止来自主机中的其他终端的攻击

更严格的信任问题是,假设您来到了一个主机,您信任这个主机的创建者,但这个主机中有一些其他终端是您不信任的,您担心他们会伪造自己的身份,诱导您将隐私数据发送给他们。

现在的问题是,如何与主机创建者的终端安全地通讯,而让其他的参与者无法获取隐私数据?

经典地,我们假设存在 Alice, Bob, Eve 三个终端。

您是 Alice;Bob 拥有主机私钥;Eve 是恶意终端。

  1. Eve 有能力 100% 窃听、并篡改主机内的消息,甚至主机集群也和 Eve 是一伙的,目的是诱导 Alice 将隐私数据发送给自己。
  2. Bob 拥有主机私钥,遵守协议,不存在泄漏 Alice 的隐私数据的动机,但不事先认识 Alice,无法提前将任何信息单独告知 Alice。
  3. Alice 想要将隐私数据发送给 Bob,且不想将隐私数据发送给 Eve。

怎么做?其实用 X25519 密钥交换算法就可以解决这个问题。

  1. Alice 生成一个一次性的随机 X25519 密钥对 (Pub_A, Sec_A),将其中的 Pub_A 发送给对方终端。要求对方终端也生成一个一次性随机的 X25519 密钥对 (Pub_B, Sec_B),然后使用 主机私钥 对 "Pub_A+Pub_B" 进行签名。然后将签名连同 Pub_B 一同返回。
  2. Alice 可以验证签名,看 "Pub_A+Pub_B" 和主机的公钥是否匹配,如果不匹配,说明对方终端不是 Bob,重新生成密钥对,重试。
  3. Alice 使用得到的密钥 Pub_B 和自己的 Sec_A 计算得到共享密钥 Shared_A_B,用这个密钥加密隐私数据,然后发送给对方终端。
  4. 如果对方终端是 Bob,他可以使用自己的 Sec_B 和 Alice 的 Pub_A 计算得到相同的 Shared_A_B,用这个密钥解密消息。

如果对方终端其实是 Eve 呢?

  1. 如果 Eve 获得了 "Pub_A",由于没有主机私钥,无法直接对任何消息加签名,无法通过 Alice 的验证。
  2. Eve 只能考虑向 Bob 骗取一个正确消息的签名。假设 Bob 生成的 X25519 密钥对是 (Pub_B, Sec_B)。能被 Alice 接受的签名的消息只有 "Pub_A+?" 的格式。但是 Bob 返回的签名格式一定是 "?+Pub_B" 的格式。两者的交集仅有一个元素: "Pub_A+Pub_B",所以 Eve 无法篡改消息,Alice 一定会获得 Pub_B,Bob 一定会获得 Pub_A。当然,Eve 会获得 Pub_A, Pub_B,但是这并没有什么卵用。
  3. 根据 X25519 密钥交换算法的数学原理,Alice 和 Bob 一定会得到相同的 Shared_A_B,而可怜的 Eve 无法获得这个密钥,自然没有办法解读用 Shared_A_B 加密后的数据。
  4. Eve 最多只能无能狂怒地破坏网络,阻挠 Alice 和 Bob 的正常通讯,但是无法窃取隐私数据。
什么场景会用到这么复杂的加密机制?

这适合于一些访客场景,例如,主机内的一部分终端是官方的,而另一部分终端是访客的。

访客终端可以通过一些方式验证其他终端的身份是否为官方终端。

当然,访客终端可以访问主机内的所有服务,因此还是需要谨慎对待。

小结

安全性和效率不可得兼。

更严格的安全性意味着更多的验证计算,势必会导致沟通效率下降。

提高效率意味着牺牲一部分安全性,这同时意味着省钱。

  1. 如果您的所有终端都部署在局域网中,您不需要用 TLS 来防御网络中间人;
  2. 如果您自己部署了主机集群,或者信任主机集群提供者,就不需要防范主机集群窃取消息。
  3. 如果您加入了有陌生人的主机,您才需要使用 X25519 密钥交换算法,额外增加逐消息加密的机制来保护您的隐私。

最佳实践是,隐私权限的划分与主机的划分是同构的。主机的职责是单一的。

  1. 请将仅有您知晓的秘密,部署到仅有您本人能访问的个人主机中。
  2. 团队共享的秘密,可以部署到仅有团队成员能访问的公共主机中。
  3. 使用 Portal 终端,将您的服务授权给其他主机,以便他人访问。
  4. 向陌生人提供服务的情况,创建一个新的主机,要求陌生人使用密钥交换算法验证你的身份,然后进行服务。
如何跨主机授权?

一个进程可以创建多个终端,以同时连接多个主机。 当您想与其他人共享数据,但不想告知其全部秘密时,这非常有用。 主机非常轻量级,您可以随时为某种目的去创建一个主机。 后续我们会单独介绍如何利用 @yuants/app-portal 有限地进行信息共享。

终端信息

终端在连接到主机时应声明其终端信息。终端信息是一个 JSON 对象:

export interface ITerminalInfo {
terminal_id: string;
// 其他字段省略
}
  • terminal_id 是终端的 ID。它在主机中应该是唯一的。这类似于一个 IP 地址。
  • 终端信息通常包含终端的服务信息。例如,终端可以声明它提供账户信息服务。
  • 终端信息用于服务模式层,我们将在后面介绍。
  • 主机负责及时向所有终端广播终端信息。

消息结构

所有消息都是 JSON 编码的,并具有以下结构:

export interface ITerminalMessage {
source_terminal_id: string;
target_terminal_id: string;

// 其他字段用于服务模式层,稍后介绍。
}
  • source_terminal_id 是发送者的终端 ID。
  • target_terminal_id 是接收者的终端 ID。
  • 主机会读取 target_terminal_id 并将消息转发到目标终端。
  • 其他字段用于服务模式层,我们将在后面介绍。

优化

以下优化并不是协议必需的,但是实现如下优化可以提升系统的性能。建议实现。

优化:P2P 直连

我们可以使用 WebRTC 在两个终端之间建立 P2P 连接。WebRTC 是一种点对点技术,允许终端直接交换消息。它是我们目的的完美选择。无需通过主机传输消息。它更快,流量成本更低。当建立 P2P 连接时,主机将停止在两个终端之间转发消息。实际上,终端将不再向主机发送消息。主机的操作从未改变。但是,如果 P2P 连接中断,主机将恢复在两个终端之间转发消息。

主机还将在两个终端之间转发 ICE 候选(提议和应答)。因此,两个终端可以建立 P2P 连接。主机既是 STUN 服务器又是 TURN 服务器。

对等连接是隐式建立的。当一个终端向另一个终端发送消息时,终端将检查是否与目标终端有 P2P 连接。如果没有,终端将尝试与目标终端建立 P2P 连接。如果建立了 P2P 连接,终端将通过对等连接发送消息。如果 P2P 连接中断,终端将恢复通过主机发送消息。

优化:本地环回

如果消息的目标终端指向终端自身,那么此消息应当不通过网络发送,而是直接发送到终端自身的消息通道。

这有利于终端订阅自身提供的频道或者消费自身的服务,促进设计解耦。