国行索尼电视安装 YouTube

2024-01-07 ⏳16.5分钟(6.6千字)

2024年元旦假期总共休了五天,完成了很多拖了很久的小心愿。其中之是让家里的索尼电视国行版用上了 YouTube。孩子们从此再也不需要忍受电脑的小屏幕了。讲真在国内环境下,要想能在电视上用 YouTube,还真得花一番功夫。今天把自己的方案梳理出来,分享给大家。虽然我家是索尼电视,但本文的大部分内容都跟电视机型无关,请放心食用。

Sony 电视上的 YouTube 画面

中转节点

众所周知,国内的网络无法直连 Google 的所有服务,YouTube 便是其中之一。要想正常观看油管,就需要一台国外的中转节点,该节点在国内能直接访问。这个节点俗称梯子。国内的电脑跟中转节点之间的通信需要加密,如此 GFW 就没法识别用户要访问什么服务,自然就无法阻断。整个网络拓扑如下图所示:

                             |x| GFW
                             |x|
PC/TV <--> Home Router <-----~~~-----> Proxy <--> YouTube
                             |x|
                             |x|

所以本文的先决条件是要有一台海外中转节点。你可以自己买一台海外的虚拟云主机,也叫 VPS,年付价格一百元到几百元不等,主机配置越高、网络质量越好就越贵。一般使用的话,一百块的 VPS 应该够了。买到 VPS 之后还需要自己安装中转软件。如果不想折腾,也可以直接买现成的中转服务。这类服务人称机场,通常是按流量收费,大约十块钱能买几十吉的流量。机场一般会有很多中转节点,某节点无法访问时可以随意切换,这方面比自己买 VPS 要强。但另一方面,机场用的都是很常见的加密通信协议,而且流量特征比较明显,所以每到严打时间都会有很多机场被封锁,这是一个劣势。除了钞能力,我们还可以白嫖海外的免费资源。比如 Oracle Cloud 就提供永久免费云主机,大家可以自己注册1看看。我用的就是 Oracle 的免费云主机。

路由器

光有中转节点还不够,你还得搞一台稍微高级一点的路由器。高级的意思是你的路由器支持安装第三方软件。这里说说为什么普通的路由器用不了。回顾前面的拓扑图,从电脑发出去的数据会先加密,然后再通过家里的路由器发到互联网上。加密数据可以安全通过 GFW 到达中转节点 Proxy。中转节点收到后将其解密,然后再发给 YouTube。YouTube 回复的数据内容到达 Proxy 节点后会被加密,然后传给家里的路由器并转发给电脑。所以我们需要在 GFW 的两侧,也就是电脑端和中转端分别安装加解密通信所用的软件,也就是常说的翻墙软件。

对于 PC 电脑来说,装个软件希松平常。但对于电视来说就没那么容易了。虽然也有软件支持电视上的 Android TV 系统,但即便装上了,操作起来也多有不便。最好的办法就是把翻墙软件装到家用路由器上,连到路由器上的所有设备就都能访问外网了。这就是我们需要一款稍微高级的路由器的原因。

这类路由器有很多选择。有玩家用 R4S 做旁路由,也有的玩家使用基于 OpenWRT 的系统。还有一些路由器支持刷 OpenWRT 固件。这本身是一个很大的话题,无法在一篇文章中展开讨论。如果你的路由器支持 SSH 访问,而且有空余的存储空间,那就可以使用我今天要讲的方案。我用的一款比较老的设备,华硕的 RT-AC1900P。虽然说老,但也支持上传自定义程序并设置开机自动运行,已经够用了。

VPN

有了设备和中转节点,接下来就得说说加解密所用的协议和软件。翻墙的协议可以说是五花八门,百家争鸣。这也让新人眼花缭乱,无从下手。要说最简单的还是 VPN,而且现在操作系统默认支持很多 VPN 协议,比如 IPSec。只不过它们的特征都很明显,很多都被 GFW 封锁了。要想不被封,最好的办法就是使用小众协议。用的人越少就越安全。最小众的莫过于自己实现一套 VPN 协议。自己实现 VPN 当然要用到 tun 设备了,主流操作系统都支持。另外还需要一门编程语言,我选的是 Go。选它不光是因为它是我近几年的主力编程语言,更重要的是 Go 语言内置交叉编译功能,可以在普通 PC 上直接编译能够在路由器上运行的程序,非常方便。

基本原理

用 Go 语言实现自己的 VPN 可谓是非常方便,有兴趣的朋友可以阅读我的另一篇文章2。核心原理参考下图:

       Node 1 1.1.1.1                         Node 2 2.2.2.2
+--------------------+                 +--------------------+
| +---+ <----- +---+ |     UDP/TCP     | +---+ <----- +---+ |
| |TUN|        |APP|<------------------->|APP|        |TUN| |
| +---+ -----> +---+ |                 | +---+ -----> +---+ |
+--------------------+                 +--------------------+
  10.0.0.1                                         10.0.0.2

这里假设有两台电脑 Node 1 和 Node 2,它们都有公网 IP 地址,分别是 1.1.1.1 和 2.2.2.2。它们可以通过公网互相访问。我们通过 tun 设备在两台设备上各创建一块虚拟网卡,它们的 IP 地址分别是 10.0.0.1 和 10.0.0.2。TUN 设备需要由应用动态创建。创建后我们可以像物理网卡一样给它们设置 IP 地址。但不同的是,每一个 TUN 网卡对应用程序来说就像是一个普通文件,程序可以从中读取和写入数据。但这不是普通的文件,如果要写入数据,则应该写入符合 IP 报文格式的数据。操作系统收到后会把该数据转发给内核的网络栈处理,处理过程跟物理网卡收到网络包完全一样。同样的,如果系统的其他部分尝试通过 TUN 虚拟网卡发送数据,那么相应的报文就会被应用程序读取到。程序读取之后可以加密,然后再通过机器上的物理网卡发给外网。

举个完整的例子。假设 Node 1 上的程序 A 相给 Node 2 上的程序 B 发送数据。程序 B 在 IP 地址 10.0.0.2 上接收网络数据。那么 A 会通过 Node 1 发送目标地址为 10.0.0.2 的 IP 报文。显然该报文需要通过 TUN 发出,也就是使用 10.0.0.1 地址发出。Node 1 的操作系统发现数据是从 10.0.0.1 发出的,所以所数据转交给了 TUN 设备对应的应用 APP。 APP 从 TUN 设备读取 A 要发给 B 的报文后完成加密,然后通过 Node 1 的物理网卡发送给 Node 2,这回是通过网络传输了。可能是 UDP 协议,也可能是 TCP 协议。Node 2 上的 APP 收到后完成解密,然后将其写么 TUN 设备。这时 Node 2 的系统就仿佛从网络上收到一个从 10.0.0.1 发给 10.0.0.2 的数据包,然后就转发给了 B 程序。从 A 和 B 的角度来看,数据确实是从 10.0.0.1 发给 10.0.0.2 的。但从 Node 1 和 Node 2 的角度来看,数据实际是从 1.1.1.1 发给 2.2.2.2 的。因为从 10.0.0.1 到 10.0.0.2 并没有真正的网络连接,所以称之为虚拟专用网,也就是 VPN。

DTUN

以上就是使用 TUN 设备实现 VPN 的基本思想。最核心的就是通过 TUN 设备跟系统的网络协议栈完成交互。除此之外,就是数据的加密和解密。这部分又是非常复杂的主题。之所以复杂,就是因为 VPN 系统需要尽可能地保护传输的数据不被外界破解。用到的主要思想是非对称密钥交换以及定时更新密钥,所以比较复杂。对于这部分功能,业界有成熟的标准,最常用的 TLS 系统,还有 IPSec 等都是标准化的协议。从来没有人会建议从零开始自己设计并实现一套加解密协议。因为很容易出现安全漏洞。基于这个思想,我早先利用 DTLS 协议,也就是 TLS 协议的 UDP 版设计了 DTUN3 项目。这是一个开箱即用的的 VPN 实现,使用 TLS 加密 UDP 数据来承载 VPN 通信。但是我用的路由器芯片太差,不支持 AES 硬件加速,VPN 的流量一大 CPU 就飙起来,没法使用。

UTUN

鉴于此,我又连夜设计了 UTUN4 项目。等等,不是原则是不要自己设计加密协议吗?确实不建议。但仔细想了一下我的应用场景,只要不被 GFW 直接识别就行,不需要太高的加密强度。而且 VPN 传出的网络层数据,而实际的流量在应用层还会用 TLS 再加密一遍,所以 VPN 不需要高强度的加密算法。所以 UTUN 项目只是将数据包做了简单的异或加密处理,接收方收到后用想同的密钥再做一遍异或处理就完成解密了。同样基于上面的考虑,收发双方约定好密钥,不需要协议,也没必要动态更新。鉴权则采用了取巧的办法。每个 IP 报文都有 checksum 字段,是根据 IP 头部各字段动态计算的。因为我们会对 IP 报文做异或,也就是加密。解密之后,重新计算 IP 报文的 checksum 值。除非有人猜到了共享密钥,不然算出来的 checksum 肯定不匹配。以此实现鉴权。

另外,UTUN 还受 WireGuard 启发,改为完全无状态设计,收发双方可以随意重启。而且 UTUN 使用 UDP 传输 VPN 数据。这里可能有人会说 ISP 会对 UDP 限流。不过就我目前实测发现,好像不会对跨国 UDP 流量做 QoS。对于 Oracle Cloud 来说,反而 UDP 流量比 TCP 流量传输速度更快也更稳定。这可能能跟 UDP 用的少有关系吧。现在很多 VPN 协议使用 TCP 承载流量,这种设计其实问题不小,具体可参考我的另一篇文章5

扯了一通理论,现在具体说说 UTUN 项目需要怎么用。

首先下载源码:

git clone https://github.com/taoso/utun.git

然后编译可执行文件。如果是要给路由器运行,则需要确认路由器的 CPU 架构和版本。像我的 RT-AC1900P 用的是 ARMv7 指令集,但没有硬件浮点数单元。在 Go1.22 之前,我们只能交叉编译成 ARMv5 指令集。因为当前 Go 要求只要没有硬件浮点单元只能退回到 v5 指令:

cd utun/cmd/utun
GOOS=linux GOARCH=arm GOARM=5 go build

到了 Go1.22 就开始支持 AMRv7 + 软件浮点数系统,可以如下编译:

GOOS=linux GOARCH=arm GOARM=7,softfloat ~/Downloads/go1.22/go/bin/go build

Go1.22 还没有正式发布,我是下的 RC 版。使用 ARMv7 编译性能会有不小的提升6

然后使用 openssl 生成随机密钥。密钥长度多多益善,不会影响转发性能:

openssl rand -hex 8

最后在两端启动 UTUN。我们把中转节点称为服务端,把家里的路由器称为客户端。因为服务端需要监听固定 UDP 端口,等待客户端传入数据。因为要操作 tun 设备,所以需要使用 root 账号运行。

# 服务端启动命令
utun -listen :4430 -key 20f984485905a099

# 客户端启动命令
utun -connect 2.2.2.2:4430 -key 20f984485905a099

这时两边都会生成类似 tun0 的虚拟网卡。在 mac 上,虚拟网卡通常以 utun 开关。服务端会监听 UDP 协议的 4430 端口,注意要让防火墙放行。我们还需要给 tun 网卡设置 IP 地址。

# 开启网卡并设置 MTU
ip link set up mtu 1400 dev tun0
# 服务端添加 IP 地址
ip addr add 10.1.1.1 peer 10.1.1.2 dev tun0
# 客户端添加 IP 地址
ip addr add 10.1.1.2 peer 10.1.1.1 dev tun0

隧道的一端地址是 10.1.1.1,另一端是 10.1.1.2。我们在一端 ping 另一端应该能通。到这里,就这设置好了 VPN。

路由分流

有了 VPN 还不够,我们还得做路由分流。简单来说我们需要让被封锁的网段流量走 VPN,正常的流量走默认网关。但怎么区分流量是个问题。网上也有很多做法。最常规的列出中国所有的 IP 网段,把它们加入到路由表,让这些网段的流量走运营商的默认网关;其它流量走 VPN。但这种办法太粗暴,因为中国的网段碎片化严重,IPv4 网段加起来有三千五百多条。一口气全灌入路由表的话对我的老爷机是个考验。ashi009 想出了一套路由合并算法,可以把路由条目减少到一千条多一点7。虽然效果惊人,但还是太多了。

在这里我提出另外一种解决思路。中国的的 IP 网段由 APNIC 分配,整个池子很小,但要申请的国家很多,中间还多次补充,碎片化再所难免。但美国就不样了,它所处的 ARIN 拥有足量的 IP 资源,而且只需要给美国、加拿大和墨西哥三个国家分配,不应该有很多碎片才对。

我查了 IANA 的分配列表8找出 ARIN 管理的网段,总共才 111 条。加上欧洲区 RIPE 管理的网段 43 条,总共也才 154 条。这比 ashi009 的方案又小了一个数量级。所以最终方案是把 ARIN 和 RIPE 管理的 154 个网段加入路由表,让相关流量都走 VPN;其他流量走 ISP 的默认网关。这算是最简单的处理方案了。但这个办法缺点也很明显,没有包含日本等非欧美地区的网段。我查了一下,日本分配的网段也得几千条,比中国的少不了多少。有兴趣的朋友可以用 ashi009 的办法聚合试试,看能减少到什么量级。不过对我个人而言,我的 VPS 在美国,用来访问日本的资源有点勉强,而且我也很少消费日本的内容;对于澳洲、印度等地区更是鲜少访问。这些区域被屏蔽的网站也相对较少。只处理欧美路由算是性价比很高的方案了。

添加路由的时候一定要注意,要先给你的中转节点加一条路由。假设你的中转节点 IP 地址是 2.2.2.2,你的 PPPoE 网卡是 ppp0。你需要先添加如下路由:

ip route add 2.2.2.2 dev ppp0

它的意思是目标地址为 2.2.2.2 的流量都走 ppp0 设备,确保你的 VPN 数据畅通。如果不加这条规则,那么 2.2.2.2 会被包含在 ARIN 的网段中,相关数据会发给 tun 设备,这样 VPN 数据就发不出去了。对于该网段的其他地址,还是要走 VPN:

ip route add 2.0.0.0/8 dev tun0

路由器会自动选择最精确的路由规则转发数据。

在家里的路由器添加分流路由表之后,你会发现还是不能 ping 通谷歌的 IP 地址。这是因为在中转节点没有开启 NAT 和 IP 转发。

NAT 和 IP 转发

所谓 NAT 就是网络地址转换。我们前面把发到 2.0.0.0/8 网段的数据都转发给 tun0 设备。 tun0 设备通过 UTUN 程序最终将报文转给中转节点。中转节点收到的报文比较奇怪,它的目标地址在 2.0.0.0/8 网段,以本地路由规看,需要通过自己的物理网卡转发;但它的源地址是我们家里的局域网地址,可能是 192.168.1.100,中转节点在转发之前需要将该地址改写成自己的公网地址,不然就发不出去。这就是 NAT 要做的事。

在 Linux 系统中,NAT 非常简单:

iptables -t nat -A POSTROUTING -o enp0s6 -j MASQUERADE

这里的 -t nat 表示操作 nat 这张规则表。-A POSTROUTING -o enp0s6 表示在完成路由选择之后,如果确定数据要通过网卡enp0s6发送,-j MASQUERADE 表示执行 NAT 处理。也就是发送前修改 IP 报文的源地址并记录对应关系,等收到回包后还得再把回包的目标地址改为刚才被覆盖的源地址。这整个过程由 MASQUERADE 链来处理。

开启 NAT 之后你会发现还是 ping 不通。那是因为 Linux 默认只会处理发给自己的数据包。如果是要转发给别人,需要单独开启转发功能。我用的是 Ubuntu,可以修改如下配置:

/etc/sysctl.d/99-sysctl.conf

# 开启 IPv4 转发
net.ipv4.ip_forward=1
# 开启 IPv6 转发
net.ipv6.conf.all.forwarding=1

执行sysctl --system重新加载,到这里应该就能 ping 通 YouTube 的 IP 地址了。但这仅限于在路由器上。如果你是在另一台局域网设备上 ping,就会发现还是不通。假设你的局域网地址是 192.168.1.100。当你 ping 8.8.8.8 时,数据包会经过路由器转发给中转节点。中转节点在 NAT 之后再转发给谷歌的服务器。然后对端返回查询结果。结果报文发给中转节点,中转节点将目标地址改写为 192.168.1.100。然后它发现现不知道将这个包发给谁,于是就丢掉了。为此,我们需要在中转节点加上如下路由:

ip route add 192.168.1.0.24 dev tun0

这是告诉中转节点说 192.168.1.0/24 这个网段可以通过 tun0 也就是 VPN 来访问。于是上面的查询结果会通过 tun0 转发给家里的路由器,路由器再转发给局域网设备。从而完成全部通信过程。

到这里,网络层的问题全部都解决了。但是不要高兴的太早,因为这时你还是访问不了油管。因为在你面前还有一座大山 DNS 污染。

DNS 分流

GFW 有多种屏蔽方式,最著名的当属 DNS 污染,也叫 DNS 劫持。DNS 协议默认使用明文 UDP 通信,运营商或者 GFW 可以轻易冒充服务器提前回复查询结果。比如你想通过 8.8.8.8 查询 youtube.com 的 IP 地址。当 DNS 查询发出之后,位于国内的某台服务器会抢先回复一个错误的 IP,这个 IP 通常在国内无法直接访问,从而实现封锁目的。

解决 DNS 污染也有不同的办法。最简单的当属 DoH,也就是 DNS over HTTPS。说白了就是通过 HTTPS 协议访问国外的 DNS 服务器查询 DNS 记录。因为是整个过程由 TLS 加密,中间不可能有人抢答,这就杜绝了 DNS 劫持。不过这个办法要求只能访问国外的 DNS 解析服务,国内的服务还是会被污染,HTTPS 加密的只是你的电脑到解析服务器这一段的流量。而且国外的 DoH 基本上是见光死,能用的也不多。

另外一个办法是使用普通的 DNS,但查询流量走 VPN,这样中间人同样无法劫持。假设我们的解析服务是 8.8.8.8。我们可以加这样一条路由:

ip route add 8.8.8.8 dev tun0

这查所有的 DNS 查询都会通过 tun0 设备发给国外的 8.8.8.8 服务器。到此,我们终于可以 ping 通 youtube.com 了。也就是说,到了这一步,我们总算能正常访问油管服务。

但这种办法有个副作用,就是有时候访问像苹果之类的国际化服务会很慢。因为 DNS 查询走了 VPN,也就是通过国外的服务来解析,回复的自然是国外的 IP 地址。而很多服务根据用户的 IP 网段动态确定 CDN 服务节点,在 DNS 查询时反回就近的 IP 地址。而我们用了国外的解析服务,拿到的自然就是国外的 CDN 节点,在国内访问肯定快不了。

解决这个问题就需要另外一套机制,DNS 解析分流。跟前面的 IP 网段分流类似,对于域名也搞一个列表,国内域名用国内的 DNS 解析服务,国外的域名用国外的。这部分需要用到 dnsmasq-china-list9 项目。我们前面说的所谓高级路由器大多使用 dnsmasq 做 DNS 解析。dnsmasq-china-list 项目整理了国内使用的域名列表,并生成 dnsmasq 配置。dnsmasq 加载之后对于这部分域名会自动使用国内的 DNS 服务来解析,从而解决国内访问部分服务很慢的问题。

具体到我的 RT-AC1900P,因为我用的是 Merlin 固件。它支持在 /jffs/configs 位置添加 dnsmasq.conf.add 配置,dnsmasq 启动的时候会读取。配置内容如下:

conf-dir=/tmp/mnt/sda1/dnsmasq-china-list/,*.conf

意思是让 dnsmasq 从 /tmp/mnt/sda1/dnsmasq-china-list 目录加载所有 conf 结尾的文件。这些文件就是我从 dnsmasq-china-list 项目下载的。

Merlin 固件还有一个小特点。如果你自己指定了 DNS 服务,也就是说不用 ISP 推送的服务,它会为指定的服务添加一条路由,让所有的 DNS 流量默认走运营商的默认网关。

8.8.8.8 via 112.243.xxx.x dev ppp0  metric 1
8.8.4.4 via 112.243.xxx.x dev ppp0  metric 1

这些路由规则优先级更高,会导致 DNS 查询流量不走 VPN。但我注意到它们的 metric 为 1。如果我们自己加入新的路由,对应的 metric 为 0,优先级更高。

ip route add 8.8.8.8 dev ppp0  metric 1
ip route add 8.8.4.4 dev ppp0  metric 1

这样就解决了 DNS 分流问题。到此为止,我们把所有的网络问题都搞定了。理论上你家里所有的设备都能正常访问欧美区的所有网络服务。

安装 APK

最后的一关是给电视安装 YouTube 软件。跟前面的九九八十一难相比,这一步是最简单的。你可以到 APKMirror 等网站下载 YouTube 的 apk,保存到优盘上,然后插到电视,再通过应用助手完成安装。YouTube 不需要谷歌三件套就能正常使用,但不装的话无法登录。好在我家主要给孩子学英语用,不登录也没关系。后面会再研究一下怎么安装 Google Play 框架。

网上有资料说可以用用 Emotn Store 安装软件。其实我就是 Emotn Store 装的 YouTube。但 Emotn Store 时不时会跳出不允许中国大陆使用。但通过我前面的设置应该识别成美国才对。这个问题也没搞清楚。后面有时间再研究吧。

另外我装了 Netflix,但我的 Sony TV 型号是 XR-65X91K,Netflix 打开会报错。网上有资料说是因为太新了,还没加入到 Netflix 的认证列表,不知是真是假。

小结

最后来个总结。本文先介绍了翻墙的一般原理。然后简述了如何用 Go 语言配合 tun 设备实现简单的 VPN 协议。接着利用 ARIN 和 RIPE 网段信息实现路由分流,最后利用 dnsmasq 实现 DNS 分流。通过这一番折腾,总算实现在索尼国行电视上看 YouTube 这一日益增长的文化需求。希望本文对你能有所启发,也欢迎大家留言讨论。


  1. ../free-oracle-cloud.html↩︎

  2. ../go/simple-vpn.html↩︎

  3. https://github.com/taoso/dtun/↩︎

  4. https://github.com/taoso/utun/↩︎

  5. ../vpn-over-tcp.html↩︎

  6. https://github.com/golang/go/issues/61588↩︎

  7. https://www.tumblr.com/ashi009/36581070478/翻墙-vpn-本地路由表的优化↩︎

  8. https://www.iana.org/assignments/ipv4-address-space/ipv4-address-space.xhtml↩︎

  9. https://github.com/felixonmars/dnsmasq-china-list↩︎