Sky Watch

联邦宇宙历险记

介绍

最近马一龙老板在 Twitter 搞了一系列事情,炒红了「联邦宇宙」这个概念,很多推友都申请了帐号。我一开始是拒绝的,因为我不怎么看好这个东西。分布式服务里用户最多的当属 email,可以说是失败的典型了⸺email 现在的状态可以说和「分布式」一点关系都没有,我认为这是所有分布式服务足够流行以后的必然归宿。但是后来马老板的政策过于光怪陆离,基本上是逼着我开始考虑其他平台了。

「联邦宇宙」这个词听起来神神叨叨的,其实它就是指网上有一票服务器,它们都使用同一个协议互相发消息,这个消息可以是一段文本(micro-blog),可以是图片(类 instagram 服务),也可以是视频(Peertube),而这个协议就是 ActivityPub。如果你注意看这个链接就会发现⋯⋯没错,这是个 W3C recommendation~~ 既然有协议,就要有实现这个协议的软件,其中最流行的当然就是现在烂大街的 Mastodon。本着「流行的一定不是最好的」的原则,我选择了比较小众的 Pleroma⋯⋯我在家里有自己的服务器硬件和环境,其中开了数个虚拟机,所以我直接就装在其中一台虚拟机上了。Pleroma 有官方编译好的二进制版本,可以直接运行,没有其它运行上的依赖,所以架起来十分简单,直接跟着官方文档走就可以了。我的虚拟机用的是 Arch Linux,所以用了 AUR(后来我自己升级到了 2.5)。整个安装过程用 Ansible 自动化,目前为止还比较无痛。

坑,好大一个

但是目前 Pleroma 有个惊天神坑,就是它对于「用户 ID 域名和服务域名不一样」这件事的支持。这要从 WebFinger 说起,WebFinger 的逻辑是这样的:你有个域名,很短很好看,你想用这个域名作为你 ID 的一部分,我们暂且把这个叫做你的公开域名。但是你想挂很多 HTTP 服务,每个服务都有自己的入口,一般这种情况有两种做法,

  • 所有的服务都挂在公开域名下面,但是在不同的路径下,比如 blog 在 /blog 下,Wiki 在 /wiki 下,等等。这种方法有很大的局限性,比如有些服务可能就不支持挂在非根路径下面;如果在根路径下挂服务还要保证 URI 不能重。

  • 在公开域名下开子域名,把服务挂在子域名下,在 HTTP 服务器里给每个子域名开虚拟服务器。这是比较好的做法。这里暂且把这个子域名叫做内部域名。

我家里的网络本来就在一个内部域名下面,所以我肯定是选择第二种方法,更何况 Pleroma 压根就不支持第一种方法。那么问题来了:内部域名很长,我就想用公开域名作为 ID,但是在内部域名下面架服务,怎么办呢?这里就需要一种机制,当外面看到我的 ID,认为我的服务在公开域名下,但是公开域名里有个东西告诉对方,我的服务其实是在内部域名下,让对方去那找。WebFinger 就是这样一种机制。Pleroma 用到的 WebFinger 其实是两个协议:

  • Web Host Metadata(RFC6415),这个协议说的是当外面想访问一个域名下的某个资源的时候,这个域名可以告诉对方关于这个资源的元信息。协议规定支持这个协议的程序在访问一个资源的时候,会先访问 /.well-known/host-meta 这个 URI,如果服务也支持这个协议,应该返回一个 XRD 文档。

  • WebFinger(RFC7033),这个协议说的是如何在一个服务里找一个人。

Host-meta

在 Pleroma 的语境下,这两个协议的用法是这样的: 每个人有自己的 ID,公开域名是这个 ID 的一部分。这个 ID 就是这个服务里的一个资源。所以如果有人想找到我,这个人应该先访问 host-meta,注意这时外面并不知道内部域名的存在,所以访问的是公开域名下的 /.well-known/host-meta。这时应该返回下面这个 XRD 文档:

<?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
  <Link rel="lrdd" template="https://internal.domain/.well-known/webfinger?resource={uri}" type="application/xrd+xml" />
</XRD>

简单地说这个文档的意思是你要找人的话就去这里看 WebFinger,其它的事我不管。LRDD link 是另一个 RFC,里面的 {uri} 就是要找的人的 ID(包含公有域名)。

注意这个请求发生在公有域名,所以 Pleroma 是看不到的。所以这时有两种方法:

  • 把这个请求重定向到内部域名,让 Pleroma 处理,这个 Pleroma 是支持的。

  • 这个文档是不用变的,所以可以手动在公有域名的目录里创建这个文件,或者把文件内容直接写在 HTTP 服务器的配置里。

对面在收到这个文档以后应该去访问里面提供的这个 URL,也就是 WebFinger 请求。

WebFinger

这时对面其实就已经知道了服务是在内部域名下。//internal.domain/.well-known/webfinger 这个 URL 完全是 Pleroma 负责的,回应里会包括用户的 profile URL(当然是内部域名)。如果 Pleroma 的版本是 2.4 或更老,这里还有一个二阶神坑。前面说到 host-meta 的回应里有 webfinger 的 URL,其中包括一个 {uri} 变量。对面看到这个以后应该把这个 {uri} 替换为用户的 ID,然后去访问 URL。所以整个 WebFinger URL 是 https://internal.domain/.well-known/webfinger?resource=name@public.domain。但是在 2.4 或更老的版本里,这个 URL 会返回 404,因为 Pleroma 期望对面会把这个 {uri} 替换为使用内部域名的 ID⋯⋯这根本就不对,也没有其它实现会这样干。所以当时我在 Apache proxy 里有几个重写规则来处理这个事:

RewriteCond %{REQUEST_URI} "/.well-known/webfinger"
RewriteCond %{QUERY_STRING} "resource=acct:(.*)@public.domain"
RewriteRule ^ "http://127.0.0.1:1234/.well-known/webfinger?resource=acct:%1@internal.domain**?**" [P,L]

RewriteCond %{REQUEST_URI} "/.well-known/webfinger"
RewriteCond %{QUERY_STRING} "resource=acct%3A(.*)%40public.domain"
RewriteRule ^ "http://127.0.0.1:1234/.well-known/webfinger?resource=acct:%1@internal.domain**?**" [P,L]

在 2.5 版里已经修复了这个 bug。

到此为止,整个查询用户 ID 的流程就完成了,Pleroma 对这个流程的实现是没有问题的。但是我觉得官方根本就没有想明白为什么要来这么一套复杂的逻辑⸺是为了可以让用户声称自己的 ID 就是在公开域名之下,别人看到的 ID 和别人搜索这个用户时使用的 ID 都应该使用公开域名!而你在 Pleroma 和 HTTP 服务器里设置好这一切以后(官方甚至专门有文档教你设置),Pleroma 依然傻了吧唧在网站上显示用户的内部域名 ID,别人用公开域名也找不到你。这样 WebFinger 的意义何在?

解决办法

理论上这个坑是可以填的,我想的一个办法就是把公有域名下的 HTTP 服务器当作 proxy,这样外面看来服务就在公开域名之下,完全看不到内部域名。但是这个方法对我不可行,因为我的公开域名解析到外面的 VPS,内部域名解析到家里,中间的网络延迟不可忽略。当然另一个方法就是抛弃 Pleroma,使用其它实现,比如

  • Mastodon,其实我一开始是准备架这个的,但是我折腾了一个星期都没架起来,总是有各种错误,弃疗。

  • Misskey,JavaScript 写的,不考虑。

  • GoToSocial,有可能会换到这个,但是似乎功能不多,尚处于开发的早期阶段。

除此以外我也只能等官方修了。等我其它玩具项目写完以后可能会自己写一个 ActivityPub 实现。

一些想法

本文最开始说了,我对联邦宇宙这个东西是不看好的。这里的「不看好」并不是说这个东西不好用(虽然现在确实没有很好用⋯⋯),而是我认为所有这种 federated 分布式服务最后都会是两个结局中的一个⸺要么用的人很多,成为事实上的中心化系统;要么没什么人用,成为少数 geek 的玩具。前者的例子是 email,后者的例子⋯⋯是所有其它分布式服务。这个趋势在我看来是必然的,用户多起来以后必然就会有大公司想来分一杯羹,它会搞出一套特别好用(至少是看起来好用)的实现吸引用户。而这些用户里必然只有极少数是真正关心「分布式」这件事的,大部分人都只是想找个方便且稳定的服务,以及别人用什么自己就用什么。大公司的用户多起来以后就要留住用户,这里的方法之一就是以安全为名强行把标准拉高,然后拒绝与不够标准的实现(也就是其它所有实现)互通。

这套伎俩不仅用在了 email 上,还用在另一个受众更广的协议/实现上,那就是 HTTP 和浏览器,具体来说就是拔高 SSL 的标准,并且强行在浏览器里引入证书。

不知道你知道不知道,现在的浏览器都认为如果你的 SSL 证书有效期多于一年,那就是不安全的。Excuse you?? 我自己在家里的网络上,外面又访问不到,给自己开一个有效期十年的证书,不行吗??你算什么东西?告诉我我自己的证书不安全??关你屁事啊??这也太把自己当人了吧??要是真想安全的话你咋不要求每个 session 都新开个证书啊??

另外在浏览器(和操作系统)里默认自带一套证书这件事,实在是太过虚伪。大家都知道 SSL 证书的信任关系是树状的,最后总会追溯到少数几个根证书上。这里的逻辑是这些根证书是由一些比较「权威」的公司颁发的,其它人如果想要证书的话,要向这些公司(以及他们信任的其它机构)申请,证明你是你,你妈是你妈,你祖宗十八代是你祖宗十八代,这些公司验证过后,就会颁发证书。问题是这种破事你们几个公司之间玩玩就算了,不就是花钱买信任么?不新鲜。你信任他,那是你的事,跟我没关系,但是你把这些证书放到浏览器里,让所有用户默认信任,这算什么道理?我又不认识你们,凭什么信任你们?你们又没给我钱对不对?这也太把自己当人了吧?我认为这里正确的做法是浏览器默认不信任任何证书,但是能访问使用证书的网站(当然要是真想安全的话应该连访问都不让访问),地址栏里放个叹号(说真的,反正又没人 care,放了就放了),让用户选择信任哪些证书。

当然有人说了,这些大公司都是有一定公信力的,他们旗下的证书都是他们验证过的,确实它是它,它妈是它妈,值得信赖。那请问 Let’s Encrypt 是怎么回事?我开 Let’s Encrypt 的时候怎么没人验证我是我我妈是我妈?Let’s Encrypt 的 HTTP 验证本身就是不安全的,怎么不说了?所以就不要假模假式地搞这些事了,何必呢对不对~~

扯远了,回到分布式服务这件事上。这里的矛盾其实是少数 geek 和大多数小白之间的人民内部矛盾。少数 geek 的需求是分布式,去中心化,这就必然意味着鼓励每个人(或者至少每个小团体)搭建自己的服务,这和其它大部分人的需求是完全相反的。此外还有另一个问题,就是分布式的协议设计和实现都比中心化的要复杂,所以对社区的要求更高。我忘了是在哪里看到的了,Matrix 的设计者和 Signal 的大佬(Jabber 的大佬?)吵架,Signal 大佬说你们这样是不行的,分布式太难做了,你们成功不了。Matrix 大佬回答说这就是为什么我们要做。

当然现在 Matrix 的实现和客户端确实没几个好用的就是了,就像 Fediverse 一样。