解决家庭宽带443端口封禁问题

2021-07-20 ⏳5.9分钟(2.3千字) 🕸️

现在的运营商会主动封禁 80 和 443 端口,防止用户使用家庭宽带对外提供 WEB 服务。我们理论上可以使用非标准端口(比如 8080 或者 4430)。但不管你用哪个,只要哪天运营商注意到你,加一条规则就可以让你所有的链接都失效(URL 中有端口号)。这才是端口封禁最大威力。所以大家没法用家庭宽带建站,哪怕是一个简单的博客也不行。就我观察,这个事情有了转机。机会就在 HTTP/3 协议。今天就跟大家介绍一下如何利用 HTTP/3 协议最新的标准来突破端口封禁问题。

先简单说一下 HTTP/3 协议。HTTP 协议从诞生一直到 HTTP/2,都是依赖 TCP 传输数据。TCP 协议提供面向连接的可靠数据传输能力。但 TCP 有一个小缺点,所有的数据都是按顺序发送,按顺序确认。简单来说,前面的数据如果没有收到对方的确认包,后面的数据就得一直等待。HTTP/2 引入了流的概念,可以在一个 TCP 连接上同时发起多个 HTTP 请求。但在 TCP 层,所有的数据还是要排队发送,这就大大弱化了流的功能。这个问题英文叫作 Head-of-line blocking(HOL blocking) ,有兴趣的同学可以继续谷歌。

HTTP/3 从某种程度上就是为了解决 HOL 问题。其实也不是 HTTP/3 解决的,而是 HTTP/3 底层的 QUIC 协议解决的。QUIC 跟 TCP 类似可以实现数据的可靠传输。但是 QUIC 使用 UDP 协议,不同的逻辑流不会相互影响,底层数据在发送的时候不需要排队。这就解决了 HOL 问题。QUIC 还提供比 TCP 更快的连接握手机制,内置 TLC 加密等等,本文不再展开。后面会写一篇完整介绍 QUIC 协议的文章,敬请期待。

到现在,我们讲到了一个重点,HTTP/3 使用 UDP 通信,HTTP/1 和 HTTP/2 使用 TCP 通信。那么问题来了,当我们在浏览器输入网址按回车的时候,浏览器怎么确定要用 HTTP 协议的哪个版本来通信呢?浏览器要怎么确定是用 TCP 还是 UDP 呢?

其实在引入 HTTP/2 就有这个问题了。但 HTTP/2 跟 HTTP/1 都使用 TCP 连接,问题还不是很大。HTTP/2 也强制使用 TLS 加密(理论上也支持明文通信,但浏览器都没有实现)。TLS 协议本身支持所谓的 ALPN 功能,全称叫应用层协议协商。也就是说,如果浏览器支持 HTTP/2 协议,它会在创建 TLS 连接的时候给服务器发送 h2 标识,如果服务也支持,则双方就会继续使用 HTTP/2 通信;如果不支持,则降级回 HTTP/1 协议。因为都是用 TCP + TLS,这种降级不会带来性能问题上。

到了 HTTP/3 就不一样了。HTTP/3 依赖 QUIC,使用 UDP 通信。如果浏览器认为服务端支持 HTTP/3,那就会开始创建 QUIC 会话。那要用 UDP 的哪个端口呢?先用默认 443 端口吧。但现在 HTTP/3 的标准化还没正式发布,肯定很多站点不支持 HTTP/3 协议。那创建 QUIC 会话肯定会超时。这个时候浏览器再尝试建立 TCP 连接使用 HTTP/2 通信就有点太晚了。不能忍。

怎么办呢?现阶段只能假设网站默认支持 HTTP/2 或者 HTTP/1 了。也就是说,浏览器一上来还是要发起 HTTP/1 或 HTTP/2 请求。如果网站还支持 HTTP/3,那么应该在返回响应信息的时候附带上 UDP 端口信息。这就要用到 Alt-Svc 这个头信息(由rfc7838定义)。一个典型的 Alt-Svc 头是这样的:

alt-svc: h3-28=":443"; ma=86400, h3=":443"; ma=86400

h3-28h3 表示 HTTP/3 服务,-28 表示 HTTP/3 标准草案编号,正式发布之后就不需要了。等号后面会指定 HTTP/3 服务的主机和端口。:443 表示使用使用 UDP 的 443 端口。后面的 ma 表示 max age,浏览器会根据这个值缓存 Alt-Svc 信息。等下一次需要发起 HTTP 请求的时候,浏览器就开始使用 HTTP/3 协议通信了。

这里同样有两个重点:第一,在发起 HTTP/3 通信之前至少要发起一次 HTTP/2 通信(后面可能会缓存 Alt-Svc 信息);第二,HTTP/3 的端口是动态的,需要根据服务端返回的信息动态确定。

到这里,我们发现了 HTTP/3 与老版本协议的一个根本区别,它的端口是动态变化的,需要通过 Alt-Svc 来发现。这就为我们突破端口封锁提供了一种可能。说白了,HTTP/3 可以工作在任意端口,你怎么封?

但有心的读者会注意一点,发起 HTTP/3 通信之前至少要发起一次 HTTP/2 通信。问题又绕回来了。HTTP/2 使用 443 端口,已经被运营商屏蔽了。所以到现在为止 HTTP/3 动态端口的优势根本发挥不出来。也是因为发起 HTTP/3 之前需要发起一次 HTTP/2 调用,这也增加了网页首次打开的耗时。为此,人们又开始研究新的解决办法。这个办法就是 DNS SVCB/HTTPS 记录。我之前写过一篇 DNS SVCB/HTTPS 记录介绍,请移步阅读。下面假设大家对 SVCB 有基础的认识。

首先,我们需要一个纯 HTTP/3 服务器。为了演示,我用 go 写了一段程序:

package main

import (
        "flag"
        "net/http"
        "github.com/lucas-clemente/quic-go/http3"
)

var root, cert, key, addr string

func init() {
        flag.StringVar(&root, "root", "", "static server root")
        flag.StringVar(&cert, "cert", "", "cert file path")
        flag.StringVar(&key, "key", "", "key file path")
        flag.StringVar(&addr, "addr", "", "listen addr")
}

func main() {
        flag.Parse()
        fs := http.FileServer(http.Dir(root))
        http3.ListenAndServeQUIC(addr, cert, key, fs)
}

编译后执行

./h3 -cert 证书文件 -key 私钥文件 -root 根目录 -addr :4430

程序会在 UDP 的 4430 端口等待 HTTP/3 请求。

证书可以通过 letsencrypt 签发。我用的域名是 h3.taoshu.in,通过路由器的 DDNS 配置(家用宽带的公网IP会变)。因为 443 端口被封,必须用 DNS 的方式来验证域名所有权。我用的是lego(Go语言开发,方便交叉编译到路由器运行)。我的 DNS 解析平台是 cloudflare。执行下面的命令:

CLOUDFLARE_DNS_API_TOKEN=DNS令牌 ./lego \
  --email 邮箱 \
  --dns cloudflare \
  --domains '*.taoshu.in' run

最终生成如下文件:

ls .lego/certificates/|grep _
_.taoshu.in.crt
_.taoshu.in.issuer.crt
_.taoshu.in.json
_.taoshu.in.key

_.taoshu.in.crt 就是证书文件,_.taoshu.in.key就是私钥文件。最终启动 HTTP/3 服务器:

./h3 \
	-cert .lego/certificates/_.taoshu.in.crt \
	-key .lego/certificates/_.taoshu.in.key \
	-root web -addr :4430

然后需要设计 DNS HTTPS 记录了。我用的是 cloudflare,对新标准支持的非常友好,可以直接在界面设置 HTTPS 记录。最终添加的 HTTPS 记录如下:

h3.taoshu.in. 7200 IN HTTPS 1 . alpn=h3,h3-29 port=4430

对应 cloudflare 的界面输入框分别是:

这里的 Target 设成了.,意思是 h3.taoshu.in 的服务器域名就是 h3.taoshu.in 本身。其实也可以指向其他域名(功能类似 CNAME),在此不展开讨论了。

Value 中的 port 指定了端口为 4430。

一切就绪,现在需要验证一下 HTTP/3 服务是否正常。经过我测试,Firefox 浏览器支持的最好,但需要修改几个配置(iOS 15 的 Safari 也是支持的,但要打开 HTTP/3 实验特性)。在地址栏输入about:config按回车。将以下配置改为true:

然后开启 DNS over HTTPS(DoH),服务商选默认的 cloudflare 就行。如果无法访问,也可以选自定义,输入

https://doh.pub/dns-query

最后,在地址栏输入 https://h3.taoshu.in:4430 ,如果你看到hello h3,那就成功了一大半了。然后可以在地址栏输入https://h3.taoshu.in,应该也能看到hello h3。注意,这次没有端口号

好了,总结一下技术要点:

有了 DoH + HTTPS 记录这套组合拳,我们可以在任何端口提供 HTTP/3 服务。运营商再也没法封锁了。但后面政府肯定会对 DNS 服务特别是 DNS over HTTPS 加强监管,弄不好也得搞备案制。但无论如何,这是一个巨大的进步。

另外,IETF已经收到了DNS SVCB标准的发布请求,估计今年就会正式发布。HTTP/3 + DNS SVCB 会很快普及,其他浏览器也会逐渐支持。期待。