Featured image of post Google Play 商店的国内 CDN:从密码学入门到分流策略优化

Google Play 商店的国内 CDN:从密码学入门到分流策略优化

一个被封锁的服务竟然还能有国内 CDN,挺神奇的。

改用自己的 Mihomo 覆写规则后,手机从 Google Play 上下载应用就会一直转圈圈,而换用机场的规则就没问题,非常奇怪。遂调取 Mihomo 内核日志查看。

1
2
3
4
5
play-lh.googleusercontent.com:443 match RuleSet(cdn_domainset) using 静态资源[🇭🇰 香港 07]
play.googleapis.com:443 match GeoSite(GFW) using 节点选择[🇭🇰 香港 07]
play-lh.googleusercontent.com:443 match RuleSet(cdn_domainset) using 静态资源[🇭🇰 香港 07]
play-fe.googleapis.com:443 match GeoSite(GFW) using 节点选择[🇭🇰 香港 07]
services.googleapis.cn:443 match GeoSite(CN) using 全球直连[DIRECT]

(以上日志已精简)

国行手机内置的 Google Play 服务使用services.googleapis.cn而非services.googleapis.com域名提供服务,而这个域名在大多数分流规则中都是直连,对,问题就出在这里,修改规则把这个域名分流到代理就万事大吉了!

……万事大吉了,吗?

1
2
3
rr4---sn-j5o7dn7s.xn--ngstr-lra8j.com:443 match Match using 漏网之鱼[🇭🇰 香港 07]
rr2---sn-j5o7dn7s.xn--ngstr-lra8j.com:443 match Match using 漏网之鱼[🇭🇰 香港 07]
rr1---sn-j5o76n7z.xn--ngstr-lra8j.com:443 match Match using 漏网之鱼[🇭🇰 香港 07]

等等,为什么每次从 Play 商店下载应用都会出现这些奇怪的域名?在网上查找一番资料后,新世界的大门就此打开。

与 Google 截然相反却又异曲同工的 Ångströ

看到 xn–ngstr-lra8j.com,熟悉域名的同学肯定知道,这是 PunyCode 编码,这串字符解码后就是 ångströ.com。Anders Jonas Ångström 是一位瑞典物理学家,他是光谱学的奠基人。1埃斯特朗(Ångström)是为纪念他而以他的名字命名的长度单位,$ 1 Å = 10^{-10} m = \frac{1}{10} nm $,通常用于描述非常短的距离,比如原子和分子尺寸或光的波长。其代表极端小的量级,尤其是在物理学和化学中用于精细测量。

虽然 Google 的名字并不是直接取自「Googol」,但是它的灵感来源于该词。「Googol」是一个数学术语,表达$10^{100}$,即1后面跟着100个零,是一个极其庞大的数字,常用来表示计算机科学中涉及的巨大数字或信息量。

在命名哲学上,Google 与Ångströ 这对看似分处光谱两端的科技概念,却展现出耐人寻味的对称美学。前者源于数学概念「Googol」的创造性改写,后者则脱胎于物理单位「Ångström」的意象重构。这两个经过艺术化变奏的称谓,恰似科技文明的双螺旋:Google 之称承载着驾驭浩如星海的数字宇宙的雄心,Ångströ 之谓则暗喻着雕琢精微的原子级技术图景。当这两个经过创造性变形的专业术语在硅谷相遇,恰好构成了一组完美的认知坐标——既指向人类处理信息的尺度极限,也昭示着科技文明同时向宏观与微观进军的壮阔征程。

入门密码学:Google 基础设施域名的规律2

OK,enough。现在让我们把视角转回 Google 的基础设施。先来看一些完整的连接域名:

1
2
3
rr4---sn-j5o7dn7s.xn--ngstr-lra8j.com
rr1---sn-j5o76n7z.xn--ngstr-lra8j.com
rr5---sn-i3b7knzs.xn--ngstr-lra8j.com

这些看似随机的字符串,实际上是经过一些简单的密码学加密后的结果。让我们拆解其中的核心部件。

城市名称的转换规则

rr1---sn-j5o76n7z为例,关键的信息段在sn-后面的 8 位:r1---sn-[123][45][6][78]。前三位,也就是本例中的j5o是城市名称,由该城市主要机场的 IATA 码通过一定的密码学变换转换而来。

首先构建一张 5 * 7 的数字字母表:

1
2
3
4
5
6
7
行0: 1 0 2 3 4
行1: 5 6 7 8 9
行2: a b c d e
行3: f g h i j
行4: k l m n o
行5: p q r s t
行6: u v w x y

逆时针旋转这张表,即从新表格的左下角开始,依次按照原表格「从左到右,从上到下」的顺序,把原表的数据在新表上按照「从下到上,从左到右」的顺序誊抄。

旋转完成后,可以得到下表:

1
2
3
4
5
6
7
8
9
| 6 d k r y |
| --------- |
| 5 c j q x |
| 4 b i p w |
| 3 a h o v |
| 2 9 g n u |
| 0 8 f m t |
| --------- |
| 1 7 e l s | 

表格中间用线条框出来的 5*5 的部分就是最终的密码表,一共有 25 个字符,「从左到右,从上到下」依次代表字母a到字母y。例如,上海虹桥国际机场的 IATA 代码为sha,我们可以通过这张表得到加密后的密文:

  • s在字母表中是第 19 个字母,「从左到右,从上到下」依次在密码表中数到第 19 个字符,也就是n
  • h在字母表中是第 8 个字母,「从左到右,从上到下」依次在密码表中数到第 8 个字符,也就是i
  • a在字母表中是第 1 个字母,「从左到右,从上到下」依次在密码表中数到第 1 个字符,也就是5

加密后的密文就是ni5

反之亦然,回到一开始的城市名称j5o,从我们刚刚得到的密码表中反推出原文:

  • j按照「从左到右,从上到下」的顺序是第 3 个字符,也就是c
  • 5按照「从左到右,从上到下」的顺序是第 1 个字符,也就是a
  • o按照「从左到右,从上到下」的顺序是第 14 个字母,也就是n

翻译完成的明文是can,也就是广州白云国际机场的 IATA 码,显然,我们连接到的服务器位于广州。

要是谷歌有某个服务器位于苏黎世,而苏黎世机场的 IATA 码是zrh,有一个字母z,可是我们的密码表只有ay啊?别担心,Google 当然也考虑到了这个问题,z对应密码表中的1

将这套规则用代码表示如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
table = "5cjqx4bipw3ahov29gnu08fmt1"
def iata2cipher(iata):
    global table
    iata = iata.upper()
    cipher = ""
    for element in iata:
        index = ord(element) - 65
        cipher += table[index]
    return cipher
    
def cipher2iata(cipher):
    global table
    cipher = cipher.lower()
    iata = ""
    for element in cipher:
        index = table.find(element)
        iata += chr(65 + index)
    return iata
    
# print(iata2cipher(input("IATA:")))
# print(cipher2iata(input("Cipher:")))

服务器组编号

[45]和[78]位,如767z,表示服务器组(接入点)编号,由下表第一列(已经用分隔线隔开)包含的字符组成,7elsz6dkry分别代表0123456789。所以,76的明文是057z的明文是04

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  | 1 2 3 4 5 6
7 | 8 9 a b c d
e | f g h i j k
l | m n o p q r
s | t u v w x y
z | 0 1 2 3 4 5
6 | 7 8 9 a b c
d | e f g h i j
k | l m n o p q
r | s t u v w x
y | z

以 Python 表示如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
codeTable = "7elsz6dkry"
def cipher2code(cipher):
    global codeTable
    cipher = cipher.lower()
    codeString = ""
    for element in cipher:
        index = codeTable.find(element)
        codeString += chr(index + 48)
    return codeString

# print(cipher2code(input("Cipher:")))

同样要将数字加密为密文同理,这里不再赘述。

支持协议

[6]位表示网络协议信息:

  • n:IPv6(地址段 0x000-0x3FF)
  • u:IPv6(地址段 0x400-0x7FF)
  • m:仅支持 IPv4

例如:

  • a5mekn7r,IPv6 前缀:2607:f8b0:4007:a::/64,IPv4 前缀:74.125.103.0/24
  • a5m7zu7r,IPv6 前缀:2607:f8b0:4007:407::/64,IPv4 前缀:74.125.215.0/24
  • a5mekm76,仅支持 IPv4,前缀:208.117.242.0/24

正确的分流处理

了解了 Google 基础设施域名的加密规律后,我们可以根据这些信息实现更精准的分流策略。目前已知 Google 在中国大陆的 CDN 节点主要分布在北京(2x3)、上海(ni5)和广州(j5o),对应的域名特征可以通过正则表达式识别:

1
2
rules:
  - DOMAIN-REGEX,^r+[0-9]+(---|\.)sn-(2x3|ni5|j5o)\w{5}\.xn--ngstr-lra8j\.com$,DIRECT

这条规则会匹配所有形如 rr1---sn-j5o76n7z.xn--ngstr-lra8j.com 的国内 CDN 域名,并将其标记为直连。而对于其他未被覆盖的 Google 域名(如 play.googleapis.com 等),仍遵循原有的代理规则。

事实上,GeoSite 数据库的上游 v2fly/domain-list-community 已针对 Google Play 的国内 CDN 节点进行了优化3。只需在 Mihomo 配置中启用 GEOSITE,GOOGLE-PLAY@CN 规则,即可自动实现国内 CDN 直连与海外域名代理的智能分流:

1
2
3
rules:
  - GEOSITE,GOOGLE-PLAY@CN,全球直连
  - GEOSITE,GOOGLE,代理选择
Licensed under CC BY-NC-SA 4.0