当别人看见我的iPad上跑着像是 Visual Studio Code 的东西的时候,反应大多有两种:要么表示惊讶并问我怎么做到的,要么甩下一句话:
你这样做有什么意义么?
故事是这样的。 我是学数学的,从上大学开始,我买了一个可以用电磁笔写字的iPad,企图用它来打草稿、记笔记。 和其他理科工科的同学们不一样,数学系学生的工作以纸张(或者某种 可以动笔写 的玩意儿)为中心。 如果没有一块儿可以打草稿的地方,就基本上做不了什么实质性的工作 —— 不像隔壁学计算机的同学们,虽然都被归类到 形式科学 的范畴,但是一般只要有地方放键盘,就能开始工作。
按理说血统纯正 (而不是像我一样天天折腾些有的没的) 的数学系学生只要有一张纸一本书,就能人在陋巷而不改其乐;有些人甚至以不依赖任何其他工具为豪,但是我个人认为这是一种有毒的文化。纵使清高如此,有些时候他们也不能免俗,主要是在需要用 $\LaTeX$ 排版东西的时候。
于是我做事的时候画风就成了这样,每次到了图书馆一样一样拿出来摆好,有一种滑稽的仪式感。
我因此开始研究如何在这个iPad上把数学学生唯一的技术刚需(排版)给解决掉,免得每次出门背个全家桶。 这就掉进了一个大坑。在两年的时间里,我固执地尝试了各种各样的奇奇怪怪的解决方案。
最早的尝试
Berkeley Open Computing Facility 和 Emacs
Open Computing Facility 是一个加州大学伯克利分校的学生组织,向全校师生和学生组织提供镜像站、打印、Web Hosting、数据库、高性能计算等服务,还运营了一个机房给同学们用,时不时办个讲座向学生安利 Linux 和开源软件。我在2022年有幸获取了一个他们的 shell 账号,可以自由使用一个公共服务器。
这是我第一次使用真正的 多用户 *nix主机,作为情怀玩家可是high得无法自拔( who
一下出来60多个用户在线上是怎样的体验)。
当时因为疫情肆虐不常去图书馆(图书馆要求戴口罩,但是戴着口罩闷着真的能做事儿?),自己买的小书桌又太小不容易同时放下电脑和平板,就开始打起了全在平板上写作业的主意。
这大概是最简单的在 iPad 上写东西的方案了:找一个装了想要的环境的工具链的远程主机,从 iPad 上随便挑个 ssh 客户端登上去,然后爱用什么编辑器用什么编辑器。 OCF 的主机上不能自己装软件,但是基本上能想到的软件都装好了,这一点就非常古风。
本机上没有找到这个软件。 如有需要,请联系您的系统管理员
恰逢当时特别喜欢用 Emacs(现在想起来小指头还在疼),就装了几个 $\LaTeX$ 插件就开始写数学作业了,支持得还 相当不错。
Emacs 甚至还有个 Server/Client 使用方式,不过有文档的用法仅限于在本机上复用 Emacs session 加快启动速度。
要是真的能拆成两半用,那简直就是古代科技 VS Code Remote Development 了。
Runestone 和 A-Shell
总是需要连接服务器才能工作还是不便的。
这个吐槽也适用于很多在线服务,一些自然有离线属性的应用也需要联网才能用,甚至其计算也是在服务端而非浏览器中完成的。花钱买了黑色高级计算机,最后还得等忙于应付众多用户而不堪重负的服务器完成计算,有点不甘心。 当然,如果这都让我不甘心,那后面简直不甘心了个够。 因为正常的 App 需要交钱上架才能正常使用,Web App 成为了我往 iPad 上“走私”自己写的程序的主要方式。 无他,联网只是为了分发。
更大的问题是, 因为 iPad 作为移动平台会被带着到处乱跑,ssh 这种在 “假设稳定网络连接” 这种上个世纪的假设下设计出来的协议自然有很多抱怨,想做到碎片时间随时使用体验比较差(更别说在自行车上用了)。
ssh: packet_write_wait: Connection to xxx.xxx.xxx.xxx port 22: Broken pipe
虽然 Mosh 做了一些对移动设备接 ssh 的优化,但是 iPad 上支持它的客户端不多。 既然天天有人抱怨 iPad 性能过剩,要是能本地运行工作负载,岂不美哉?
这里不得不提一个剑走偏锋的卓越工作:A-Shell 。 我第一次见吓了一跳:这怎么还能在 iPad 上开原生 shell 运行 clang 的? 林檎公司的下架铁拳不好使了? 仔细一看,这个 clang 的编译目标是 W e b A S M。 好家伙……果然 Web 技术是时代之光。 这个项目以二进制的方式打包了一系列常用的 Unix 工具和语言工具链,运行在一个真的是 iPadOS 提供的 shell 里面。
烫知识:iOS/iPadOS 源自 macOS,macOS 里面 包了个 Unix® 。 所以四舍五入下来正统 Unix 用户竟是……
但是出于众所周知的 AppStore 条款 对能动态执行原生代码的 App 的严格态度,自己编译程序只能编译到 WebASM,A-Shell 提供的 bash 被配置成了可以像执行原生程序一样解释它们。 NodeJS、Python、Lua 等直译的环境基本不受什么影响,写点不依赖外部库的玩意儿还是可行。 (当然,也可以自己打包然后编译进 A-Shell ,不过就需要开始和林檎的应用侧载签名做斗争了)。 最神奇的事情是,A-Shell 打包了半个 TeXLive 套装,需要的时候下完几个 GB 的资源之后即可使用。
TeXLive 动辄几个 GB 的安装包看起来非常幽默,加上 tcl/tk 写的管理程序就更幽默了,在普通 PC 环境里装一次一个小时,给人一种静止时间里的学术慢生活的崇高感,应该刻在光盘里好好品味。
很遗憾,没有 XeTeX,因为作者搞不定它的重新打包。 LuaTeX 也不是不能用,还能写点小插件苦中作乐。
为了用得更舒服,我们还可以去整一个有 GUI 的编辑器。
A-Shell 虽然自带了 Vim (但是没有 Emacs,宗教战争现在开始!)但是没做好 Esc 的重映射 (给 iPad 的键盘一般没有 Esc)所以用起来很痛苦。
我被一期 MacStories 安利了 Runestone 编辑器。
这个新 App 按照 WWDC 2022 的 desktop class iPad app 指南编写,可以无缝和 A-Shell 的工具链合起来用,就像在真的 *nix 系统上一样。
自古以来,iOS 对待 App 数据的方式类似于一个个存储容器,App 被严格限制只能访问自己的存储容器。 这种创新对于后来的移动平台的时代、计算机技术的广泛普及是奠基性的。 然而,这样做的代价是不同 App 之间难以共享数据,这也成为了 iOS/iPadOS 经常挨骂的点。 林檎在不推翻这个框架的前提下想了很多解决办法,比如那个出了名的不直观的分享菜单,以及后来出现的
security-scoped URL
方案。 对于后者,App 会委托 iPadOS 向用户提供一个 File Picker 选中一个需要访问的文件或者目录,iPadOS 拿到用户的许可后返回一个这样的“访问受限” 的 URL 供 App 访问外部资源。 用这个机制把 Runestone 的工作文件夹(甚至可以是某个云盘 App 的同步区域)链接给 A-Shell,就可以愉快地干活了。
Blink Code
Blink 以与 A-Shell 类似的方式 提供了一个限制性的原生 iPadOS shell,并且附赠了功能完整的 ssh 客户端、 mosh 客户端和一系列常用 Unix 工具。 这个 App 比较敏锐的察觉到了魔怔用户们在 iPad 上 hacking 的奇怪需求,做出了各种各样的提升生活质量的设计(首先一个把自己归类为 ssh 客户端的 App 一打开就是一个 shell,这味儿已经太冲了……),其中一个设计成为了真正在 iPad 上好好工作的突破口:它内置了一个 Code 前端。
这个前端支持多种后端(见后文),也可以当个基础的本地编辑器。 因为 Blink 的 security-scoped URL 支持 做得不错,想同步想接 A-Shell 都没问题,还可以接 Working Copy,这下连 Git GUI 都有了。 Blink 允许用户重映射 Esc 键,这样一来 Code 里装个 Vim 插件,就非常舒服了。
(不过 OneDrive 和 Google Drive 的 App 都没有正确支持 security-scoped URL 而无法直接链接进 Blink,这是它们的锅。链接 iCloud 目录实现同步体验非常丝滑)。
Blink + 云端开发环境
如前述,Blink Code可以接上跑在远程机器上的后端,然后直接使用服务器上的完整的开发环境。
众所周知 Visual Studio Code 是拿 Web 技术造的,把它改回 Web App 以提供一些灵活性是自然的想法。 2019年一群脑洞很大的人实践了这个想法并开源了实现,就是后来的 code-server。 这是个开箱即用的单用户 Code 实例,也打包成了 Docker 镜像可以开箱即用。 Gitpod 紧随其后,做了个类似东西并且做成了 Web 服务:用户需要开发环境时 Gitpod 按照模板起一个跑着单用户 Code 的 Docker 容器并 clone 项目,不用了就销毁,来达到自动的、可重现的开发环境配置。 一看这么搞有戏,2021年微软也给出了自己的 demo,这后来大概发展成了 GitHub Codespace,和 Gitpod 想法类似。 2023年初 Blink 也做了自己的类似服务,现在还在 Beta 阶段。 在这些通用环境之外,code-server 和它的类似物还广泛活在各大云服务厂商,特别是做 serverless 服务的厂,的控制台里,提供在特定上下文里定制的开发环境。 这类产品现在被叫作云端开发环境(CDE)。
当时我看到微软的 demo 对此,但是嫌弃浏览器的上下框架挤占原本就不多的屏幕空间,就放弃了在 iPad 上折腾这些(大概当时也是觉得自己这么做太神秘了。有这时间还不如去用 Overleaf 写数学。好呀,你干得好呀!)。 Blink Code 解决了这个问题,比起直接用浏览器访问 web 上的 code,能提供非常棒的全屏体验和一些生活质量提升(ESC!),让这个路线在 iPad 上真正可行了起来。 Blink Code 支持 Gitpod 和 Github Codespace。我去尝试了 Gitpod 因为它支持白嫖,每月50小时。 用起来只需要注册一个账号,创建 Workspace 并指定要 clone 的项目,等容器起来了就可以在浏览器里打开了。
记录这个的URI,拿到 Blink 里执行
code `https://❄️❄️❄️❄️.gitpod.io`
就能在 iPad 上拥有相同的体验了。
使用了一段时间之后发现,Gitpod 每次启动开发环境就要起一个 Docker 容器,个把小时没活动容器就会被销毁,再回来的时候还得等几十秒甚至几分钟让 Docker 重新启动容器。
在这个容器里,只有 /workspace
目录下的东西被持久化,其他东西都会在容器销毁时消失。
Gitpod 提供两种定制开发环境的方式,要么以一个基本上包含所有常用语言工具链和工具的镜像为基础,实在需要什么别的需要自己写一个 配置文件 往里加;
要么就去自己提供镜像(可以是Docker Hub里的,也可以自己写个Dockerfile)。
这样每次容器级每次推倒重建的环境可能对动真格的开发环境来说能避免很多灵异事件和悬案,但是对于我这种臭写 $\LaTeX$ 的人,就显得有点重了。
对于 $\LaTeX$ 环境, Gitpod建议的方式是自己写 Dockerfile 在默认容器上加装 TeXLive,第一次安装之后容器镜像就会缓存下来,免得每次用都要下载安装几个 GB 的 TeXLive。
当然,在我看来即使是动真格的开发环境,用 Docker 这种方式来保证 开发 环境的可复现性,还是太暴力了。 Docker 以一些性能代价同时干了保证可复现性和环境隔离两件事,对于开发环境这种对后者要求不高的情景,用 Docker 就显得有些浪费。 对于这个问题的改进办法,我的评价是关注 NixOS 喵,关注 NixOS 谢谢喵。
自建 code-server
因为对 Gitpod 反复构建和销毁容器造成大量碳排放导致全球变暖的行为感到不安(实际上是老惦记着那50小时有没有超额),我最终还是走上了自建 code-server 这个邪路。
我第一次接触到租服务器建自己的服务是十年前(大约13年14年的样子)试图和别人一起架 MC 模组服务器。 (现在想想,那时的 MC 模组圈子真的成了个孵化器,各种意义上的)。 那时候对我来说拥有个人 VPS 是一个遥不可及的梦想,想架设游戏服务器只能靠全体玩家攒钱或者找赞助才能长久维持。 这么多年过去了,云服务提供商的机房遍地开花,硬件也发展到了能把整个 Twitter 放进独立服务器的时代。高密度的计算和存储硬件,比如单个20 TB 的变态硬盘,和一个封装192核心的变态处理器,使得租 VPS 的成本进一步下降,以至于一些厂家提供了各种各样的免费套餐。
这里面搞慈善搞最大的就是 Oracle 了,不知道是不是他们靠着在计算机行业里混得久、收专利费赚的太多心里过意不去决定回馈一下技术社区。 Oracle 的免费套餐是几家大厂里唯一一个“永久免费”的(别人家一般是给一个体验期然后发一些代金券),可以一分钱不花维持最多6个 VPS,其中两个是性能羸弱的x86微型实例,剩下的是 ARM 机器,也是最离谱的:
我本来还对这个数字心存疑虑,直到看到有人在上面架了一个 GTNH 服务器…… 除了 VPS 之外,Oracle 还送了一些杂七杂八的云服务,比如托管数据库、密钥基础设施、负载均衡、监控服务、对象存储等等。 要拥有这一切只需要登记一张 看起来不可疑 的信用卡。
按照 code-server team 提供的 教程 可以很快在一个 VPS 上起一个 code-server 实例,可以用安装脚本,可以下载 prebuilt 包,也可以起 docker,文档详尽不多赘述。 同步项目文件可以靠 Git,不想虐待 Git 的话也可以装个 Rclone 然后 挂载 喜欢的网盘。
接下来我们需要设法让 iPad 能访问到这个服务。 当初 Naïve 的我为了能赶紧看看成果听个响,直接把默认安装的 code-server 给暴露到公网了…… 默认配置下, code-server 只有一个密码验证,而且 没开HTTPS,让这个密码验证形同虚设。
一定不要在尚未做好安全加固之前将服务暴露到公网,即使只暴露很短的时间! 现代IPv4互联网可恐怖了,到处都有怪物在游荡。
当时我让服务上线之后还 开开心心 去睡觉了,第二天早上起来经人提醒才意识到问题的严重性(code-server 会在 Web 提供运行它的用户的终端,我当时用的用户还是个 sudoer。要是再多放一段时间,我的服务器怕不是就要变成米奇妙♂妙屋了),吓得我把 VPS 销毁重建。谁知道会不会已经挂上木马了呢?
要把这个服务做得足够靠谱,我们需要对它做以下的加固:
- 我们需要一个身份验证机制,保证只授予特定身份的人访问权限;
- 我们需要一个加密机制,保证没人冒用或者窃取合法身份,避免身份验证机制被绕开;
- 如果要将 web app 暴露到公网上,我们需要一个防渗透机制。因为 code-server 本身可能不是一个太安全的 Web 服务器,我们需要在它和公网之间添加一个 反向代理服务器 来对放进来的 HTTP 请求“消毒”。使用反向代理服务器还会提供很多运维上的便利,此处略过。
code-server 本身支持的身份验证机制就只有那个简单的密码验证。 虽然加上反向代理和 HTTPS 之后,理论上按照上面的标准已经可以暴露到公网了,但是这个可以被穷举的静态密码总还是让人不太放心。 code-server 的文档提供了更多 方案,包括
- 使用一个叫 OAuth2 Proxy 的反向代理服务器,在反向代理这一层验证访问者出示的 OAuth token 并决定是否放行。
- 使用 Cloudflare Access。这是另一个互联网大慈善家提供的服务。Cloudflare 提供的主要服务相当于一个跑在边缘网络上的巨型反向代理,而 Access 则在这个层面上提供访问控制。为了不侧漏,边缘网络和 Web App 所在的服务器之间靠 VPN 通信。
- 使用 Pomerium,一个 Cloudflare Access 的开源 self-host 替代。不过边缘网络是没法自己修的,所以只能替代 Access 的功能,不能替代 Cloudflare 打包提供的其他好处。
SSH 隧道
除了刚才提到直接用 HTTPS 加密码,和那三个比较重的外部身份认证的办法,还有一个比较简单粗暴的方式把上面提到的三个加固都搞定了。 Blink 提供了 功能完整 的 ssh 客户端,我们可以通过 ssh 隧道转发几个端口,这样就可以直接在 iPad 上访问服务器上无防护的 code-server。
- 身份认证靠了 ssh 的认证方式(设置成公钥私钥认证);
- 加密靠 ssh 自己的传输加密;
- 从外界视角看,只有一个 22 端口在活动,里面跑着的是久经沙场的 ssh 服务器。
整个过程中 code-server 一直躲在服务器的防火墙后面,没暴露到公网。
实际操作起来,假设 code-server 在服务器上监听 http://localhost:8080
,我们想把它转发到 iPad 上的2333端口,可以开个新 Blink 终端标签页然后执行
ssh -L localhost:2333:localhost:8080 <domain or IP of your server>
# 这里 -L 后面的一长串 gibberish 表示把远端 localhost:8080 的端口映射到 iPad 的 localhost:2333
# 至于为什么指定端口转发的时候要指明主机名(那两个 localhost),我们后面会看到
# Talking about bad notations...
# 表示端口映射就不能换个和表示主机端口号的冒号不同的符号么?
Blink 的设计相比于别的 ssh 客户端软件显得很独特,开端口转发不需要去 App 设置里鼓捣,新开个终端窗口运行 ssh 转发命令就行了……体验非常纯正。
ssh 隧道这个办法简单粗暴又牢靠,让我折腾 Code on iPad 第一次进入了实用阶段。 但是这么做问题很明显,像之前所说的,ssh 在移动设备的情景下水土不服:要么是 iPad 上客户应用程序时间久了被杀了,要么是挪了地方,连接彻底断掉了。 对于前者 Blink 提供了一个很鬼畜的解决方案: 让 Blink 向系统请求位置信息来保证在后台持续运行。 进一步还能设置成如果离开了某个地理位置范围,就自动取消后台运行。 (国内一众做后台保活的流氓 APP:这个我熟!) 对于后者更是没办法了。
我之前用的时候,还遇到了一个很幽默的问题: 回忆前面用的端口转发的命令是怎么工作的:
[local endpoint hostname:]<port>:<remote endpoint hostname>:<port>
在本地端口转发(
-L
)的语境下,远程端点主机名和端口号指定了该把远程的什么主机的什么端口搬到本地来; 本地端点主机名和端口号的作用更为微妙,类似于一个 过滤条件,指定了以什么为目的地的包该被隧道转发走。 我们这里把它指定为localhost
,就能限定只有 iPad 上的程序发的包能被转发走(因为只有本机上的程序才能访问到本机的localhost
),是种安全措施。 其中本地端点监听的主机名可以省略,省略之后等价于写0.0.0.0
,即“什么包都接收”。 我之前在用的时候把本地端点主机名给省了,于是一人开 Code 全局域网围观,而且还是没有密码保护的……
Tailscale
既然都开起 ssh 隧道来了,为什么不用个 专业 的隧道?
相信大家都有被学校或者工作单位提供的 VPN 软件折磨过。 这些工具想达成的目的和我们想用 ssh 隧道达成的目的类似:通过建立一个隧道,把没有物理连接到可信网络的计算机虚拟地连接到可信网络里。 既然鉴权和加密已经在建立加密隧道的时候完成了,可信网络里的资源便可以不加验证直接访问。 这点在先前的例子里不是很明显,实际上示意图里左边的黑色框和右边的 iPad 一起形成了一个可信网络,在此之内的 traffic 便没有更多的加密和鉴权。
然而,近几年 VPN 的开源解决方案的生态相当之差,有的设计的时候完全没考虑移动网络,网络一不稳定就卡 timeout;有的带着一股扑面而来的90年代的未来主义气息,用着类似于 IPSec 这样的古代神造兵装,导致用起来麻烦部署起来更麻烦,时不时还会被 敌意的网络设施 弄得不工作。 除此之外,市面上还充斥着近乎于流氓软件的一些商业解决方案。
好在后来有人看不下去了,按照近几年已经今非昔比的网络技术理念和密码学,做了一个现代化的 VPN 实现,是为 WireGuard(2015)。 对于一些平台(比如 Linux),WireGuard 还提供了内核模块实现,让它的性能接近于直接访问互联网。 另外,WireGuard 设计时考虑到了移动设备的需求,支持了 漫游,这正是我们所急需的。 一家叫 Tailscale 的公司更近一步,以 Wire Guard 为基础额外实现了一个控制平面,实践了所谓的 Zero Trust 安全模型。
Zero Trust?
刚才提到过一般的 VPN 想法是作为物理专用网的延伸,处于不受信任的公共网络的用户可以通过 VPN 隧道连接到内网的安全网关,以它为代理加入到信任网络里以访问内部资源。 因为能出现在内部网络里的主机也只有物理网络和虚拟网络上的主机,这些资源一般不设防。 但是后来人们意识到这样的做的缺点:一是所有的外部流量都,远程访问的主机太多网关就挤爆了(虽然内网拥有的互联网出口可能还剩很多带宽没用上),二是如果一个主机被成功渗透,攻击者便可以在内网同级区域内畅行无阻。 这样的问题在网络服务越来越分布的时候变得明显,毕竟如果服务器散布在各个数据中心里,甚至同一个逻辑上的数据中心实际上由多个机房组成,这事儿就没法办了。 一个解决方案是,放弃靠网络边界来界定内外网,直接认定所有网络都是不可信的,对于每个 traffic 都做权限验证。 此所谓 Zero Trust 安全模型。
关于 Tailscale 是如何工作的,Tailscale 有提供详尽的 介绍,大概想法是控制平面只提供允许互相访问的主机之间的隧道,其余的主机不知道它们的存在。 落到用的时候,基本上只要在服务器和 iPad 上的 Tailscale 程序里登陆同一个账号,就已经入网了,开箱即用体验相当不错。 每一个主机加入 Tailscale 网络之后会被赋予一个虚拟网络内的私有 IP 地址(实际上他们用的是 Carrier Grade NAT 地址),直接访问这个地址即可访问到对应的主机,即使它们可能并未暴露到公网,最后达到的效果像是把所有加入的主机放进了一个安全的局域网。
虽然 Tailscale 运营风格非常开放,基本功能全部可以免费使用,关键组件也都是开源的,但是也有人因为它以 SaaS 的方式提供服务感到不安(万一哪天他们把投资人的钱霍霍完了呢?)。 为了解决这个问题,有人重新实现了 Tailscale 的控制平面服务器,供需要的人使用。是为 Headscale(你们真会起名字……)。 对此 Tailscale 方面还挺滋瓷的 ,并在 Tailscale 客户端里主动对第三方控制平面服务器做了适配。
其实比 Tailscale (2019) 早很多的时候,已经有了一个叫 ZeroTier(2011) 的类似方案,它也允许用户自己架设控制平面服务。 Tailscale 和 ZeroTier 技术路线上略有差异,网上也经常引发宗教战争。 总体上,Tailscale 更注重开箱即用,对于 敌意的 网络环境适应得也更好;而 ZeroTier 支持更复杂的网络的配置。 最近还有一个更清真的解决方案叫 NetMaker(2021),相比于 ZeroTier,除了能 self-host,协议也换成了大家喜闻乐见的 WireGuard(ZeroTier 很可惜没有用 WireGuard 而是用了自己的协议,毕竟 2011 年 WireGuard 还一根毛都没有;Tailscale 因为偷了懒所以各个平台上的 WireGuard 实现都是用户空间的,没利用上 Linux 上可以用的 WireGuard 内核模块)。
说句题外话。 Tailscale写了个 博客 说明了开发这个工具的最初愿景。 计算机网络已经从原来搞 hacking 做实验的理想地方变成了百万漕工衣食所系,堆积了大量诘聱的技术遗产(你看看我写的这些东西……我还自诩在以非常现代的视角在叙述),同时到处都有怪物在游荡。 除了读计算机本科被课程拿枪指着重新实现一点东西,我们基本上已经丧失了自己重新造轮子的勇气(
但是,但是我又没上过这样的课啊!)。 所以看到 Tailscale 提供的可能性之后我非常激动。
Cloudflare Access 和公网访问
上面提到的 Tailscale 方案伴随我完成了2023年上半年大部分的写作。 Tailscale 工作得很棒,3月份的时候坐在60 mph 奔驰的的大巴上,平板连着时断时续的移动网络,还能稳如磐石。甚至在5月份回国期间,还能隔着太平洋有不错的体验(虽然 Vim 插件没法用了。这个 B 架构,为什么每个 keystroke 都要往网上发啊!!!评价为写 NodeJS 写的)。
本来以为这事儿已经算完了。 我想着,如果理理思路的话,像 code-server 这样的只有一个人用的私有服务,非要上公网显得没有必要。 既然我只是把互联网作为通讯和 Web App 的分发工具,那全部走隧道就完全是合理的。 况且我走的还是 后现代安全技术高级隧道。
情况出现改变的时候是在2023年秋季,导师建议我制作一个学术主页。 我一寻思,按我现在手上的几台破 VPS,时不时还被人关一关,完全达不到保证高可用的程度。 (虽然我觉得我的破网页也没什么人感兴趣,但是如果在真的有人感兴趣的时候派不上用场,那可就闹心了)。 于是我查询了一下现在可以免费用的 静态网页托管服务,结论是 GitHub Pages(老熟人啦)、Netlify 和 Cloudflare Pages。 为了兼顾 世界各地 的可访问性,我选择了 Cloudflare Pages。 我选择的静态网页生成器是 Hugo,据说是当下的 State-of-Art™ 生成器(大嘘)。 把网页弄起来我只花了 10 分钟(还是上课开小差弄的),但是后面立即就掉进了 Cloudflare 的深坑无法出去。
为什么会这样呢?因为我第一次被 Serverless 提供的可能性亮瞎了眼。 Cloudflare 提供服务的风格很好玩:他们手上的数据中心和网络设施是真的多,但是不像别的云服务提供商,他们从来不卖 VPS。 我甚至怀疑这么做是有技术路线选择的成分在里面的。
这是怎么和 Code 扯上关系的呢? 之前说到为了把 code-server 暴露到公网,需要满足三个安全要求。 给 code-server 套上 Cloudflare CDN 之后要求2和3就满足了,同时靠着 Cloudflare 在这个框架下提供自己的身份验证服务 Cloudflare Access,1也满足了。
使用 Cloudflare Access 需要首先把 DNS 服务器交给 Cloudflare 管理。 当对一个域名启用 Cloudflare Access 的时候,Cloudflare 会利用自己的管理权,把暂为授权的回话重定向到一个登陆界面,提示用户亮明身份。
在验明身份之后,再把用户重新定向回正确的 Cloudflare 反向代理服务器,用户就能正确访问原来被 lock down 的 Web 服务了。 这里有一个漏洞:因为我自己的服务器和 Cloudflare 的服务器之间隔着公网,那如果攻击者知道了我本来服务器的地址,攻击者可以绕开 Cloudflare Access 访问未授权的资源。 为了解决这个问题,服务器和 Cloudflare 之间的交流是走的隧道,原服务器本身不直接把服务暴露到公网。
这一套东西部署起来,理论上 也相当简单,但是当时 Cloudflare 刚把它的 Tunnel 整合进自己的 Zero Trust 安全平台(哈,又是 Zero Trust,再加上 Federation 和 Serverless,后现代网络技术全家桶了属于是),文档正在更新比较混乱,所以迷惑了很久。 之前 Cloudflare Tunnel 还叫 Argo Tunnel 的时候,配置一个新 tunnel 需要自己在服务器上填一些配置文件。 现在有一个新办法办法,可以把设置都转移到 Cloudflare 的 Web 控制面板上,比较友好。 以前写配置文件的方式为了兼容也还保留。近来(2023年秋冬)网上能查到的说法都是旧办法,相当具有迷惑性。
首先去 Cloudflare Access 的设置面板锁定一个子域名的访问权限。 这件事一定要第一个做,不要打乱顺序! 不然你的 Web App 就在配置访问控制之前被 Cloudflare 高效地 公之于众了。 之后我们需要添加一些访问规则来说明谁能访问这个服务。
Cloudflare Access 处理访问规则的方式有点绕。 当Identity Provider(可能是配置好的第三方IdP,比如上图的 Login with Google,也可以是 Cloudflare 那个开箱即用的 email 验证码)完成用户身份验证的时候,会向 Cloudflare 提供一个由签名担保的键值对表,里面写了一些诸如“我担保,这个用户叫xxx,email是[email protected]“之类的信息。 如果 IdP 用 OpenID Connect的方式和 Access 通信,那这些条目被叫做 OIDC claims。 (其他的协议怎么称呼它我不清楚,但是总得有个类似的东西)。 我们在配置访问权限的时候就是在编写关于这些信息的规则,比如“email地址是xxx的可以放进来”或者“通过某个 IdP 认证的用户都给我放进来”。 值得一提的是,免费的 Cloudflare Access 限制月活跃用户的数量小于50,而 Cloudflare 区分每一个用户身份时选用的唯一标志是 email 地址。 这个限制当然是可以通过管理员配置自建的 IdP 来把 OIDC claims里的 email 全伪造成一样的来绕开, 但是 Cloudflare 侧的很多高级访问规则功能和审计功能都不能用了。
接下来设置 Web App 和 Cloudflare 之间的 tunnel。 Cloudflare 现在推荐新的 tunnel 安装方式,直接在服务器上安装 tunnel 客户端并配置 secret,剩下的配置直接在 Web 端即可搞定。 往服务器上配置 tunnel 要做的事情在创建 tunnel 的时候会直接给你,下面的是个例子:
# Run on the original server.
# Download latest cloudflared
curl -L --output cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
# Install cloudflared
sudo dpkg -i cloudflared.deb
# Install client secret to tunnel client
sudo cloudflared service install ❄️❄️❄️❄️❄️❄️❄️❄️
在 tunnel 创建完成之后,只需要在 Cloudflare 的控制面板里设置把什么主机名什么端口转发到公网上什么域名的什么端口即可,和之前提过的 ssh 端口转发类似。 公网域名设置成刚才设置的被 Access 所管理的域名,这样 Access 和 tunnel 就在一起工作了。 控制面板附带详细的文档,此处不再赘述。
这样以来就是目前(2023年末)我在用的解决方案了。 看起来相比 Tailscale 方案感觉非常高级,也许 Cloudflare CDN 也能给我提供一些静态资源的缓存(存疑),同时有需要的时候也可以很优雅地在公用电脑上使用 Code 服务,比原来相比稍微更灵活了一些。
和当时第一次架设 code-server 的时候一样,我在配置 Cloudflare 公网访问的时候也出过一次严重的安全问题。 那时的我在搞清楚了新旧文档之谜之后配好了服务,松了一口气,然后开始手贱,修改了 tunnel 出口的名字(因为测试的时候取的这些域名里充斥着各种 test、temp 的字样,看着不爽),但是忘了同时修改 Access 所管理的域名。 最要命的是我还去吃饭了,就放着在公网上门户大开摆了好几个小时。 这次 VPS 免于被销毁重装的命运,是因为我一念之差把 code-server 跑 Docker 里了。
快说谢谢 Docker。这也给我上了一课:这种分布式、云端的应用,因为每个组件之间都隔着公网,很容易一个没设置对,高度敏感的资源就被公开了。
匆忙地赶在1月1日结束前写完并发布。 感觉写长文的技术还是需要多练习,现在感觉实在写太慢(这个坑一挖就是半年啊!),写的时候也经常兴奋得晚上睡不着觉(我不理解)。
🔏 Jack Wang @ Madison, WI, CST 2024/01/01 修订: 2024/01/10