普通视图

发现新文章,点击刷新页面。
昨天以前Mythsman

Ghost 解决 jsdelivr 资源加载慢的问题

作者 mythsman
2023年1月27日 22:58

背景

用了很久的自建 Ghost 博客系统不知道从哪个版本开始,页面加载速度忽然变慢了很多。看了下加载的资源,发现多了很多走 jsdelivr cdn 的资源,加载速度竟然长达半分钟。。。

本来选择自建博客系统的重要目的之一就是为了页面加载速度可控,尽量避免加载不可靠、容易被墙的第三方资源。结果没想到 Ghost 官方又在核心模块里引用了第三方的 CDN。

不过还好 Ghost 项目本身的配置化做的还是不错的,大年初六上班摸个鱼的时间解决了一下。

解决

仔细看了下,新加入的走 CDN 的资源主要是 会员系统(portal)+评论系统(comments)+页面搜索 (sodo-search),因此在某次支持这些系统的更新前都是没问题的。不过考虑到目前的主题已经集成了这些系统,所以这些功能也不能禁用掉。

参考 Ghost Forum 的这篇讨论,可以通过在 config.[env].json 中修改配置,将 url 等替换成 self-hosted 的版本。不过这里的讨论中提到的配置来源并不清晰,在源码中搜索了一番发现了端倪(这里的 /var/lib/ghost 目录是我这 docker 里的 ghost 安装目录):

root@1f7b379a87f4:/var/lib/ghost/current# grep -r 'cdn.jsdelivr.net/ghost' *
core/shared/config/defaults.json:        "url": "https://cdn.jsdelivr.net/ghost/portal@~{version}/umd/portal.min.js",
core/shared/config/defaults.json:        "url": "https://cdn.jsdelivr.net/ghost/sodo-search@~{version}/umd/sodo-search.min.js",
core/shared/config/defaults.json:        "styles": "https://cdn.jsdelivr.net/ghost/sodo-search@~{version}/umd/main.css",
core/shared/config/defaults.json:        "url": "https://cdn.jsdelivr.net/ghost/comments-ui@~{version}/umd/comments-ui.min.js",
core/shared/config/defaults.json:        "styles": "https://cdn.jsdelivr.net/ghost/comments-ui@~{version}/umd/main.css",

可见这些配置都来源于 core/shared/config/defaults.json 这个文件,涉及到 CDN 的相关配置如下:

{

    ...

    "portal": {
        "url": "https://cdn.jsdelivr.net/ghost/portal@~{version}/umd/portal.min.js",
        "version": "2.23"
    },
    "sodoSearch": {
        "url": "https://cdn.jsdelivr.net/ghost/sodo-search@~{version}/umd/sodo-search.min.js",
        "styles": "https://cdn.jsdelivr.net/ghost/sodo-search@~{version}/umd/main.css",
        "version": "1.1"
    },
    "comments": {
        "url": "https://cdn.jsdelivr.net/ghost/comments-ui@~{version}/umd/comments-ui.min.js",
        "styles": "https://cdn.jsdelivr.net/ghost/comments-ui@~{version}/umd/main.css",
        "version": "0.12"
    },
    
    ...

 }

为了自托管这些文件,我们首先将这些文件下载到静态文件夹下,然后在想办法将 config.[env].json 配置好即可。

不过问题来了,静态文件夹有哪些?如果想当然的放在 /var/lib/ghost/content/public 下,肯定是要吃瘪的。正解应该是要看下 /var/lib/ghost/current/core/frontend/web/site.js 这里启动 express 的地方:

module.exports = function setupSiteApp(routerConfig) {
    debug('Site setup start', routerConfig);

    const siteApp = express('site');

    //...
    
    // Serve sitemap.xsl file
    siteApp.use(mw.servePublicFile('static', 'sitemap.xsl', 'text/xsl', config.get('caching:sitemapXSL:maxAge')));

    // Serve stylesheets for default templates
    siteApp.use(mw.servePublicFile('static', 'public/ghost.css', 'text/css', config.get('caching:publicAssets:maxAge')));
    siteApp.use(mw.servePublicFile('static', 'public/ghost.min.css', 'text/css', config.get('caching:publicAssets:maxAge')));

    // Card assets
    siteApp.use(mw.servePublicFile('built', 'public/cards.min.css', 'text/css', config.get('caching:publicAssets:maxAge')));
    siteApp.use(mw.servePublicFile('built', 'public/cards.min.js', 'application/javascript', config.get('caching:publicAssets:maxAge')));

    // Comment counts
    siteApp.use(mw.servePublicFile('built', 'public/comment-counts.min.js', 'application/javascript', config.get('caching:publicAssets:maxAge')));

    // Member attribution
    siteApp.use(mw.servePublicFile('built', 'public/member-attribution.min.js', 'application/javascript', config.get('caching:publicAssets:maxAge')));

    // Serve site images using the storage adapter
    siteApp.use(STATIC_IMAGE_URL_PREFIX, mw.handleImageSizes, storage.getStorage('images').serve());
    // Serve site media using the storage adapter
    siteApp.use(STATIC_MEDIA_URL_PREFIX, storage.getStorage('media').serve());
    // Serve site files using the storage adapter
    siteApp.use(STATIC_FILES_URL_PREFIX, storage.getStorage('files').serve());
    
    //...
};

显然,这里 public 文件夹下的文件都是单独配置绑定的,不是整个文件夹的绑定。额外添加文件的话是不会映射到外部路径上的。这样一来,我们只能绑定在 images , media , files 这些路径下。再 check 下这些具体的变量,就会得知这些路径映射到的外部路径:

root@1f7b379a87f4:/var/lib/ghost/current# grep -r 'STATIC_.*_URL_PREFIX' *
...
node_modules/@tryghost/constants/index.js:    STATIC_IMAGES_URL_PREFIX: 'content/images',
node_modules/@tryghost/constants/index.js:    STATIC_MEDIA_URL_PREFIX: 'content/media',
node_modules/@tryghost/constants/index.js:    STATIC_FILES_URL_PREFIX: 'content/files'

我这里就选择放在 files 文件夹下,这里新建一个 self-host 文件夹:

root@1f7b379a87f4:/var/lib/ghost/content/files/self-host# ls
comments-ui-0.12.css  comments-ui-0.12.min.js  portal-2.23.min.js  sodo-search-1.1.css  sodo-search-1.1.min.js

由于我是通过 docker-compose 部署,相比于修改 config 文件,直接通过环境变量配置更为方便,变量名跟 config 中的 json 格式一一对应,这里不得不夸奖下 Ghost 的配置自动映射做的挺方便:

version: "3.8"
services:
  ghost:
  image: ghost:5.27
  container_name: "ghost"
  environment:
    ...
    portal__url: /content/files/self-host/portal-2.23.min.js
    sodoSearch__url: /content/files/self-host/sodo-search-1.1.min.js
    sodoSearch__styles: /content/files/self-host/sodo-search-1.1.css
    comments__url: /content/files/self-host/comments-ui-0.12.min.js
    comments__styles: /content/files/self-host/comments-ui-0.12.css
  ...

效果

首次加载速度直接从 30s 优化到了 300ms ,优化效果十分感人。。。

更新(2024.4)

考虑到 Ghost 经常会更新,每次都复制文件怪麻烦的,于是搭建了一个简单的 jsdelivr 代理 :

https://cdn.mythsman.com/jsdelivr/{path_to_jsdelivr}

这样只要在 docker-compose 做如下配置就好:

ghost:
    image: ghost:5.69.3
    container_name: "ghost"
    restart: always
    depends_on:
        - "mariadb"
    environment:
        # ...
        portal__url: https://cdn.mythsman.com/jsdelivr/ghost/portal@~2.37/umd/portal.min.js
        sodoSearch__url: https://cdn.mythsman.com/jsdelivr/ghost/sodo-search@~1.1/umd/sodo-search.min.js
        sodoSearch__styles: https://cdn.mythsman.com/jsdelivr/ghost/sodo-search@~1.1/umd/main.css
        comments__url: https://cdn.mythsman.com/jsdelivr/ghost/comments-ui@~0.16/umd/comments-ui.min.js
        # ...
    volumes:
        - ./ghost/:/var/lib/ghost/content
    expose:
        - "2368"

OpenWrt+R66s 软路由入门尝鲜

作者 mythsman
2023年1月11日 15:46

背景

随着家里电子设备越来越多,客户端维度的科学上网配置已经逐渐支持不了日常需求了:

  1. 设备数量越来越多;配置不过来,且容易触发机场的客户端连接上限。
  2. 设备类型越来越多;安卓端、IPhone端、IPAD端、Mac端、树莓派端、PC端、Kindle端,需要的客户端各不相同,配置的思维负担太大,甚至有些类型的设备没有靠谱的客户端。
  3. PAC规则越来越多;自己通常会有一个主要用的PAC规则,但是当换设备后,就需要重新配置,非常麻烦。

于是趁着工作之闲,研究了一下软路由,一站式解决了家庭上网问题。

硬件选购

市场上的可自由刷机的路由器还是很多的,不过对于入门级屌丝租客党来说,明白自己到底需要什么是很重要的:

  1. 不需要 WIFI 功能。移动光猫自带 WIFI 功能,软路由只需专注做网关服务器即可。后续如果对 WIFI 有更高要求再另买专用的 WIFI。所谓专业的东西做专业的事。
  2. 不需要 HDMI 功能。带 HDMI 功能的基本都是定位于迷你主机或者电视盒子的,其实做路由器根本用不到。
  3. 网口并不需要太多。考虑到家庭设备都是用光猫自带的WIFI连接,因此不需要很多网口。最低一个 LAN 口其实就足矣。
  4. 性能并不需要太高。毕竟租房的时候办的也就是200M带宽,网线再快也没用。同时家里也没搞什么视频工作站,内网也基本没啥带宽。况且性能高了的话、耗电、散热、占地都是问题。
  5. 芯片尽可能新一点。芯片类产品买新不买旧,就算不太在乎性能,也得考虑性价比,少买老古董不做垃圾佬。
  6. 尽量支持卡刷。线刷基本都需要去官方文档上查流程,比较麻烦。而卡刷的流程几乎都是一样的,拔卡写镜像开机即可。

基于上述原则,类似小米AX6000之类的 WIFI 路由器基本不会考虑了;类似J4125之类的性能较强、耗电较高的基于 X86 的路由器基本也不会考虑了;类似 R2s ,N1盒子之类的老东西也不考虑了。

搜寻了一番,发现电犀牛(FastRhino)的 R66s 刚好符合我的基本要求。

  • 接口和大小刚合适。两个2.5G网口、两个USB3.0口、使用SD卡、无HDMI口,因此十分小巧。
  • 产品和芯片比较新。产品是2022年5月发布,瑞芯微(Rockchip)RK3568芯片。
  • 性价比较高。2G内存+32G卡配置到手320¥不到。

相比一些从电视盒子改来的玩意、或者羞羞答答才放开刷机功能的WIFI路由器、或者是仗着原装OpenWrt割韭菜的路由器来说,这个产品还是比较大胆和良心的。

固件选择

OpenWrt官方固件自然也是可以的,不过考虑到国人的常用需求,一般都是用国内维护的固件。常见的固件有:

  1. Lean 的 LEDE 固件(官网)。
  2. 易有云的 iStoreOS 固件(官网)。
  3. Flippy 的固件(官网)。

个人感觉这些固件其实都大同小异,主要区别还是上游的 OpenWrt 版本不同、Linux 内核版本不同、默认自带的软件包不同(尤其是 iStoreOS 自带了很多私货应用)。其实 R66s 出厂自带的镜像也是够用的(其实就是 Lean 的固件),只是出于政策原因,没有自带科学上网工具。这些东西动动小手自己装下也行,不过为了方便大家开箱即用,直接写到固件里用起来更方便而已。

如果需要自己刷机,其实也不复杂。用读卡器插入PC,用 rufus 工具直接写入指定镜像即可。

这里还遇到了有一个有意思的点,原装的 32G SD 卡插进来之后,Windows系统的文件管理器只识别出了 64MB。原本以为是遇到了奸商,但是用磁盘管理工具一看的确是 32G 的盘。后来研究后才发现是因为这个是系统引导盘,因此有时只会识别系统主分区的大小,有时甚至不会被识别为外部存储,不影响使用。

正常刷完后,SD 卡分区大致如下。只使用了主分区,还有大量未分配的区域,如有需要等启动后再新增分区即可。

网络设计

相关背景知识可以先看一下油管 jack stone的家庭网络设计,讲的挺好,颇有老司机风范。

考虑到我这里光猫自带的WIFI也够用,但是光猫后台没有admin权限,很多东西不好改。并且出于社恐也不想打电话找宽带师傅把光猫的路由模式改桥接模式。因此我还是采用了 光猫做主路由+R66s做旁路由 的方案:

  1. 软路由和光猫以 LAN口-LAN口相连。
  2. 整个网络中只使用光猫的网段和光猫的DHCP服务。
  3. 光猫不做任何配置,由接入的客户端手动配置网关和 DNS 到软路由。

网络配置

对于新手小白来说,这一步最容易踩坑。尤其是当接线和配置不对时,连后台都打不开,这是最让人沮丧的。我个人也是尝试了很多姿势才了解其中的弯弯绕。

错误姿势:软路由LAN口直接接光猫LAN口

唉?前面不是说软路由和光猫就是要LAN口相连么?没错,但那是指的配置完成后的连接方式。

在设备出厂、或者刷了新固件后,说明书或文档上都会写清楚当前固件默认配置的IP。例如 R66s 刷的 Lean 固件默认 IP 就是 192.168.100.1。这个配置的意思是,当前设备的 LAN 口配置的是 192.168.100.1 的静态 IP。

而我这边光猫的网段是 192.168.1.0/24 ,软路由的静态 IP 不在这个网段内。这就导致连接光猫的PC设备和软路由是无法连接的,自然也就访问不到后台了。

错误姿势:软路由WAN口直接接光猫LAN口

既然软路由配置静态IP不行,那是不是直接用 WAN 口和光猫的 LAN 口相连就行了?毕竟 WAN 口的默认配置是 DHCP 协议,肯定可以直接加到光猫的网段中。

理论上说当然可以,但是实际上还是要看固件的配置。因为出于安全考虑,很多固件的默认防火墙配置是只允许从 LAN 口访问后台,不允许从 WAN 口访问。

如果我记得不错的话,iStore OS 默认是允许从 WAN 口访问的,而 Lean 固件默认是不允许从 WAN 口访问的。

正确姿势:软路由LAN口先和电脑直连配置好后再接光猫LAN口

为了避免上面的各种问题,最稳妥的方法还是电脑端直接和光猫LAN口相连(或者通过交换机相连),这样就避免了各种上面的幺蛾子。如果一不小心光猫的静态IP还和光猫的IP冲突了,直接断开光猫即可。

首次连接到软路由后台后,按照旁路由的配置思路,需要在 网络-> 接口 -> LAN 中进行如下配置:

  1. 配置协议为静态地址。
  2. 配置 IPV4 地址为当前光猫网段下的一个未被占用的IP地址。
  3. 配置子网掩码为光猫的掩码。
  4. 配置 IPV4 网关为光猫的 IP 地址。
  5. 配置 DNS 服务器为光猫的 IP 地址。
  6. 关闭 DHCP 开关。

配置完成后,当前页面应当就加载不出来了,这时候将软路由和光猫 LAN-LAN 相连,然后通过这里配置的静态IP就又可访问了。

OpenWrt重要路径

OpenWrt 基于 Linux,但是一些重要的配置和Linux 还是有些区别的。重点要关注下面路径下的文件:

  1. /etc/config 这里记录了各个应用的通用配置。操作这里的文件和在 luci 页面中操作是等价的。
  2. /etc/init.d 这里记录了各个应用的启动命令,类似 service 或是 systemctl 的功能。
  3. /usr/lib/lua/luci/ 这里记录了 luci 的页面信息,用于操作 /etc/config 的配置。

配置 SSR-PLUS

Lean 国际版固件自带,或者直接在软件包下搜索安装下面两个包,配置上自己准备好的机场,就能开心使用了。

luci-app-ssr-plus
luci-i18n-ssr-plus-zh-cn

由于 SSR 会深入影响 DNS 解析,路由转发等基础功能。因此一般来说,SSR可以算是家庭网络的最大不稳定因素。所以这里有必要简单了解下 SSR-PLUS 的工作原理,方便定位网络问题。

单纯的 SSR 其实就是一个基于 libshadowsocks 的加密 socks 代理。不过仅此而已肯定不够,毕竟更重要的问题是“选择哪些流量走代理,哪些流量不走代理“。因此我理解 SSR-PLUS 就是 OpenWrt 下 SSR 结合了 iptables + ipset + dnsmasq 的组合工具。

SSR-PLUS 在启动时会加载两个外部订阅的数据。这些配置可以在 /etc/ssrplus 下看到:

  1. 国内IP列表。里面记录了部署在国内不会被墙的IP网段。
  2. GFW 列表。里面记录了被墙的域名。

国内IP列表比较简单,就是在 ipset 里添加一个 china 的路由规则集,方便后续进行 iptables 设置。而GFW 列表除了设置 ipset 之外,还要进行 DNS 选路。

进行 DNS 选路

众所周知,DNS 是明文报文,无良运营商可以很方便的进行 DNS 污染,导致直接解析到错误 IP,更别提后续的连接了。

一种简单的解决方案是将所有 DNS 请求都走 SSR 的 socks 代理,但这样毕竟效率较低。因此实践中常用的办法就是:

  1. 本地启动一个走 SSR 代理的 DNS 服务。可以用 dns2tcp 或者 dns2socks 之类的工具。通常暴露的是 5335 端口。
  2. 根据预先配置的 GFW 列表,告知本地的 dnsmasq 服务,对 GFW 列表内的域名走加密通道,对GFW列表外的域名走普通通道。SSR-PLUS 对 dnsmasq 的配置可以参见 /tmp/dnsmasq.d/dnsmasq-ssrplus.d

这样就能做到安全的 DNS 了。

实际用起来发现dns总是容易挂。。。这里需要加一个定时监测脚本拉一下:

ps -ef|grep /usr/sbin/dnsmasq |grep -qv grep

dns_alive=$?

if [ $dns_alive -ne 0 ]
then
  echo 'dnsmasq down !!!'
  /etc/init.d/dnsmasq restart
  echo 'dnsmasq restarting !!!'
fi

配置 ipset 规则

我们知道 GFW 列表只记录域名,而在用 iptables 对流量路由转发时只能拿到 IP。因此 dnsmasq 在解析 GFW 后,还会将解析出来的 IP 加入到一个名为 gfwlist 的 ipset 规则。

root@OpenWrt:~# ipset list gfwlist
Name: gfwlist
Type: hash:net
Revision: 6
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 56232
References: 2
Number of entries: 1686
Members:
20.198.162.78
65.21.236.8
142.250.186.54
203.77.190.0
54.148.90.231
....

这样一来,再配合 iptables 的 NAT 的 PREROUTING 设置,就能做到针对 GFW 内的域名进行代理转发:

root@OpenWrt:~# iptables -L -t nat
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
SS_SPEC_WAN_AC  tcp  --  anywhere             anywhere             /* _SS_SPEC_RULE_ */
...

Chain SS_SPEC_WAN_AC (2 references)
SS_SPEC_WAN_FW  all  --  anywhere             anywhere             match-set gfwlist dst
...

Chain SS_SPEC_WAN_FW (4 references)
target     prot opt source               destination         
RETURN     all  --  anywhere             0.0.0.0/8           
RETURN     all  --  anywhere             10.0.0.0/8          
RETURN     all  --  anywhere             127.0.0.0/8         
RETURN     all  --  anywhere             169.254.0.0/16      
RETURN     all  --  anywhere             172.16.0.0/12       
RETURN     all  --  anywhere             192.168.0.0/16      
RETURN     all  --  anywhere             0.0.0.224.in-addr.arpa/4 
RETURN     all  --  anywhere             240.0.0.0/4         
REDIRECT   tcp  --  anywhere             anywhere             redir ports 1234

GFW模式和绕过中国大陆IP模式的区别

有了上面的基础,我们就明白了其实这两种模式的区别也很简单。看下两种模式下的 iptables 表就清楚了。

GFW模式

Chain SS_SPEC_WAN_AC (2 references)                 
target     prot opt source               destination                  
RETURN     all  --  anywhere             anywhere             match-set whitelist dst
SS_SPEC_WAN_FW  all  --  anywhere             anywhere             match-set blacklist dst
RETURN     all  --  anywhere             anywhere             match-set bplan src         
SS_SPEC_WAN_FW  all  --  anywhere             anywhere             match-set fplan src    
RETURN     tcp  --  anywhere             36.156.102.177       tcp dpt:!domain              
RETURN     all  --  anywhere             anywhere             match-set china dst         
SS_SPEC_WAN_FW  all  --  anywhere             anywhere             match-set gfwlist dst  
                                                               

简要的工作流程是:

  1. 判断目的地址是否在白名单,如果是则不转发。
  2. 判断目的地址是否在黑名单,如果是则直接转发。
  3. 判断目的地址是否是大陆IP,如果是则不转发。
  4. 判断目的地址是否在GFW列表中,如果是则直接转发。
  5. 最后默认不转发

绕过中国大陆IP模式

Chain SS_SPEC_WAN_AC (2 references)                 
target     prot opt source               destination                     
RETURN     all  --  anywhere             anywhere             match-set whitelist dst
SS_SPEC_WAN_FW  all  --  anywhere             anywhere             match-set blacklist dst
RETURN     all  --  anywhere             anywhere             match-set bplan src         
SS_SPEC_WAN_FW  all  --  anywhere             anywhere             match-set fplan src    
RETURN     tcp  --  anywhere             36.156.102.177       tcp dpt:!domain              
RETURN     all  --  anywhere             anywhere             match-set ss_spec_wan_ac dst
RETURN     all  --  anywhere             anywhere             match-set china dst         
SS_SPEC_WAN_FW  all  --  anywhere             anywhere 

简要的工作流程是:

  1. 判断目的地址是否在白名单,如果是则不转发。
  2. 判断目的地址是否在黑名单,如果是则直接转发。
  3. 判断目的地址是否是大陆IP,如果是则不转发。
  4. 最后默认转发

大白话总结就是:

  1. GFW 列表模式是:默认不翻墙,除非你在GFW列表中配置。
  2. 绕过大陆IP模式是:默认翻墙,除非你在国内。

那么具体家里选哪个更合适呢?我个人建议选 GFW 列表模式。虽然绕过大陆IP模式看起来方便,但是:

  1. 考虑到正常大部分流量还是不希望转发的,任何网站只有在GFW列表中、或者你配置的黑名单中才会翻墙。这样你会明确的知道哪些是翻的,哪些是不翻的。
  2. 中国大陆IP列表其实并不完善,很多运营商会偷摸摸使用一些保留IP作为局域网IP(例如类似 224.0.0.68 这类地址)。这就导致这些局域网IP会被中国大陆IP列表当成是海外IP进行转发。这显然就会导致难以排查的网络问题。(尤其是使用 N2N 之类的 P2P VPN 工具时)

配置 socat

不知道为啥 OpenWrt 自带的端口转发功能总是不能正常 work,尤其是转发到内网的其他 IP 时。这时候就需要 socat 来进行额外的端口转发功能。

安装下面的包即可:

luci-i18n-socat-zh-cn
luci-app-socat

然后就可以在 网络 -> Socat 里愉快的进行端口转发了。

配置 n2n

使用 v3 版的 n2n 需要先更新下 opkg 的源,否则拉不到适配 v3 版本的 luci-app 。虽然可以直接改 /etc/config/n2n 配置文件,不过毕竟不太优雅。

不知道为啥固件自带的源长这样,luci 的版本都跟其他的不一样。。。

src/gz openwrt_base https://downloads.immortalwrt.org/releases/21.02.1/packages/aarch64_generic/base
src/gz openwrt_luci src/gz openwrt_luci https://downloads.immortalwrt.org/releases/packages-18.06-k5.4/aarch64_generic/luci
src/gz openwrt_packages https://downloads.immortalwrt.org/releases/21.02.1/packages/aarch64_generic/packages
src/gz openwrt_routing https://downloads.immortalwrt.org/releases/21.02.1/packages/aarch64_generic/routing
src/gz openwrt_telephony https://downloads.immortalwrt.org/releases/21.02.1/packages/aarch64_generic/telephony

改成一样即可:

src/gz openwrt_base https://downloads.immortalwrt.org/releases/21.02.1/packages/aarch64_generic/base
src/gz openwrt_luci https://downloads.immortalwrt.org/releases/21.02.1/packages/aarch64_generic/luci
src/gz openwrt_packages https://downloads.immortalwrt.org/releases/21.02.1/packages/aarch64_generic/packages
src/gz openwrt_routing https://downloads.immortalwrt.org/releases/21.02.1/packages/aarch64_generic/routing
src/gz openwrt_telephony https://downloads.immortalwrt.org/releases/21.02.1/packages/aarch64_generic/telephony

配置好 n2n 客户端后,再在服务端配置好 nginx,挂上 ssl 域名 ,就能从远端访问家里的 OpenWrt 配置后台了。

当然,由于有些 luci 组件是需要额外暴露端口的,因此直挂 80 端口的 nginx 的话,实时监控、tty终端之类的功能还是没法用的。

运行一段时间后发现 n2n 的客户端 edge 经常时不时的挂掉,logread 看了下日志:

# logread |grep n2n
Thu Mar 30 19:24:48 2023 daemon.info n2n-edge[21183]: 30/Mar/2023 11:24:48 [n2n.c:38] ERROR: Unable to create socket [No file descriptors available][-1]
Thu Mar 30 19:24:48 2023 daemon.info n2n-edge[21183]: 30/Mar/2023 11:24:48 [edge_utils.c:273] ERROR: failed to bind main UDP port 0
...

看起来似乎是进程打开的最大句柄数超过了上限,确认了一下发现 ulimit -a 里看到的限制是 1024 ,实际 ls -lA /proc/{pid}/fd|wc -l  看到的正好也是 1024 (ls 的 -lA 参数忽略了 . 目录和 .. 目录,如果直接 ls -la 则是 1026)。。。

解决方式是改 ulimit 增大全局配置,或者直接修改 /etc/init.d/n2n 配置文件,增加进程的配额。而我选择后者(参考 openwrt 配置):

  procd_set_param limits core="unlimited"
  procd_set_param limits nofile="10240"
  procd_set_param limits nproc="10240"

(更新)发现上面的配置没用, cat /proc/{pid}/limits 发现 nofile 的配置并没有生效。于是自己写了一个脚本,当 fd 超过限制就自动重启下 n2n,放在 cron 里定期执行:

n2n_pid=`ps -ef|grep /usr/bin/n2n-edge |grep -v grep |awk '{print $1}'`

if [ ! $n2n_pid ]
then 
  echo 'n2n not started'
  exit 1
fi

fd_cnt=`ls /proc/${n2n_pid}/fd |wc -l`

if [ ${fd_cnt} -ge 1024 ]
then
  /etc/init.d/n2n restart
fi

配置 Dropbear

Dropbear 可以理解是 OpenWrt 下的 sshd 。为了安全起见,dropbear 默认只对 lan 口过来的请求开放 ssh 连接。因此在配置好 n2n 之后,也是无法直接访问这个服务的。这里需要先关闭下 dropbear 对 lan 口的绑定:

root@OpenWrt:/etc/config# cat /etc/config/dropbear 
config dropbear
        option PasswordAuth 'on'
        option RootPasswordAuth 'on'
        option Port         '22'
#        option Interface    'lan'
#       option BannerFile   '/etc/banner

配置好后 restart 即可生效。

root@OpenWrt:/etc/config# /etc/init.d/dropbear restart

配置各个客户端

软路由配置好了后先不要急着连接设备。建议先重启几次路由器,确保配置正常生效,且重启不丢失。确认完成后,各个需要连接的客户端再手动配置下:

  1. 关闭 DHCP,IP 地址设置为原先自动分配的IP即可。
  2. 将网关/路由器设置为软路由IP。
  3. 将子网掩码设置为和主路由一致。
  4. 将 DNS 设置为软路由IP。

正常情况下,配置好后即可感受科学的魅力,不过偶尔也可能出现一些奇怪的坑。此时可以先尝试:

  1. 关闭 IPV6。SSR 不支持 IPV6 ,如果一不小心走了 IPV6,则基本上是翻不出去的。
  2. 重启设备。像 IPhone 这类的设备似乎配置完后要重启一下,否则容易报诸如 SSL Error 之类的错。

爬虫浏览器的Cloudflare五秒盾处理

作者 mythsman
2022年12月12日 21:59

背景

近期业务有不少涉及到国外的网站,本以为经受了和国内大量卷王公司对抗的考验之后,处理国外业务应该是降维打击才对。结果本地测试的时候的确很OK,但是一上线就发现全面飘红,多个不同业务同时出了 Cloudflare 著名的五秒盾

根据 Cloudflare 的说明,只要是使用 Cloudflare 的域名解析服务,默认都会自动“享受” 他家的一系列防护服务(尤其是 Javascript 防护)。具体特征就是会在/cdn-cgi/challenge-platform/ 下嵌入风控逻辑。他们会收集浏览器的一系列指纹,再根据后台大数据对比,计算出当前环境的可疑概率以进行风控。

原则上出现这个也无妨,毕竟五秒盾也只防第一次登录。就算 hCaptcha 不好自动化处理,先人肉处理下,后续复用统一份 Cookie ,只要控制好规模和频率,基本能满足业务需要。但是吊诡的在于:无论验证多少次,永远不会跳转回正常网页。(鬼知道我点了多少次小海豚和大熊猫)

一般来说,对于网页爬虫而言,遇到强力风控时,只要不是极其重要的业务,我们一般都是考虑从业务上更换策略,尽量不正面硬刚。但是用本地浏览器在匿名模式下访问时、在和线上使用相同代理IP的前提下,虽然也出了五秒盾,但是人肉点也是能正常滑过的。这说明他的风控策略本身应当并不强,或者说这看起来不像是一种反爬策略,而更像是一种 Bug 或是某种安全策略,并不像是无法解决的问题。于是就尝试处理了一下,果然发现了一些意料之外的坑。

环境

我们的工作环境是基于 cef 定制的有头浏览器。通过 cef 框架 hook 浏览器页面的 LoadStart, LoadEnd, LoadError 等事件,注入用于页面操作的纯 JS 脚本,再通过代理抓包获取数据。同时定制一些集群管理、任务管控、链路追踪等逻辑。内核基于的是 87.1.12+g03f9336+chromium-87.0.4280.88 的 Linux 版本,默认的 Chromium 启动参数大致有如下这些:

--disable-dev-shm-usage
--no-first-run
--disable-web-security
--disable-site-isolation-trials
--lang=zh-CN
--no-sandbox
--single-process
--disable-http2
--disable-gpu
--disable-sync
--allow-running-insecure-content
--ignore-certificate-errors
--proxy-server=localhost:8080
--user-agent="some user agent"

解决

解决的思路比较清晰,既然本地环境可以正常过风控、线上的环境不行,我们就先想办法在本地搭建类似线上的环境,先想办法复现出问题,再控制变量将二者环境参数不断接近,直到找到临界的问题点。

测试的时候尤其需要注意两点:

  1. 如果配置了用户目录或者缓存目录,一定要记得每次测试前都要删一下,防止影响结论。
  2. 每一次测试都要重复多次,否则很容易由于各种偶然原因影响结论。

这里省略枯燥的探索过程,直接记录多次尝试后的结论:(实测,以下配置缺一不可)

关闭 --single-process 配置

默认 Chromium 是多进程模式的。这里的多进程模式可以有两层理解:

  1. 窗口进程、渲染进程、GPU进程、工具进程分开;
  2. 不同站点页面进程分开;

进程分开的好处就是可以更方便的进程安全控制,但是代价就是整体内存占用会略高。

因此我们线上默认选择了单进程模式,本意是为了节省进程数从而节省内存,让单个机器能同时跑更多的任务。当然,这里说的单进程并不是只有一个进程,而是只有 zygote 一种进程。(参考 #single-process-mode

例如,打开百度,如果采用单进程模式,只会有四个进程:

myths@ubuntu:~$ ps -ef|grep myproject|grep -v myproject
myths      27250    3785 10 15:27 pts/1    00:00:05 /home/myths/Projects/myproject --url=https://www.baidu.com --single-process
myths      27253   27250  0 15:27 pts/1    00:00:00 /home/myths/Projects/myproject --type=zygote --no-zygote-sandbox --lang=en-US --user-data-dir=/home/myths/.config/cef_user_data
myths      27254   27250  0 15:27 pts/1    00:00:00 /home/myths/Projects/myproject --type=zygote --lang=en-US --user-data-dir=/home/myths/.config/cef_user_data
myths      27256   27254  0 15:27 pts/1    00:00:00 /home/myths/Projects/myproject --type=zygote --lang=en-US --user-data-dir=/home/myths/.config/cef_user_data

而如果使用默认的多进程模式,则会有八个进程:

myths@ubuntu:~$ ps -ef|grep myproject -v myproject
myths      27351    3785  9 15:29 pts/1    00:00:00 /home/myths/Projects/myproject --url=https://www.baidu.com
myths      27354   27351  0 15:29 pts/1    00:00:00 /home/myths/Projects/myproject --type=zygote --no-zygote-sandbox --lang=en-US --user-data-dir=/home/myths/.config/cef_user_data
myths      27355   27351  0 15:29 pts/1    00:00:00 /home/myths/Projects/myproject --type=zygote --lang=en-US --user-data-dir=/home/myths/.config/cef_user_data
myths      27357   27355  0 15:29 pts/1    00:00:00 /home/myths/Projects/myproject --type=zygote --lang=en-US --user-data-dir=/home/myths/.config/cef_user_data
myths      27378   27351  4 15:29 pts/1    00:00:00 /home/myths/Projects/myproject --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --lang=en-US --user-data-dir=/home/myths/.config/cef_user_data --shared-files=v8_context_snapshot_data:100 --field-trial-handle=0,i,4491148827631734197,7878804601012403413,131072 --disable-features=BackForwardCache
myths      27394   27357  0 15:29 pts/1    00:00:00 /home/myths/Projects/myproject --type=renderer --user-data-dir=/home/myths/.config/cef_user_data --first-renderer-process --lang=en-US --num-raster-threads=1 --renderer-client-id=5 --time-ticks-at-unix-epoch=-1669691342091293 --launch-time-ticks=102038976468 --shared-files=v8_context_snapshot_data:100 --field-trial-handle=0,i,4491148827631734197,7878804601012403413,131072 --disable-features=BackForwardCache
myths      27395   27357 35 15:29 pts/1    00:00:02 /home/myths/Projects/myproject --type=renderer --user-data-dir=/home/myths/.config/cef_user_data --lang=en-US --num-raster-threads=1 --renderer-client-id=4 --time-ticks-at-unix-epoch=-1669691342091293 --launch-time-ticks=102038982131 --shared-files=v8_context_snapshot_data:100 --field-trial-handle=0,i,4491148827631734197,7878804601012403413,131072 --disable-features=BackForwardCache
myths      27420   27354  2 15:29 pts/1    00:00:00 /home/myths/Projects/myproject --type=gpu-process --lang=en-US --user-data-dir=/home/myths/.config/cef_user_data --gpu-preferences=WAAAAAAAAAAgAAAIAAAAAAAAAAAAAAAAAABgAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAABAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAIAAAAAAAAAA== --use-gl=angle --use-angle=swiftshader-webgl --shared-files --field-trial-handle=0,i,4491148827631734197,7878804601012403413,131072 --disable-features=BackForwardCache

使用单进程模式看似很美好,但是问题就是失去了多进程间的隔离,很多安全策略就失效了,浏览器的安全性就会下降很多。同时单进程模式下似乎也更容易出 bug,我在测试的时候发现单进程模式下的浏览器的确很容易出现一些 DeadLock 和 Coredump。

Cloudflare 或许通过某种方式测试出了单进程模式下安全性的问题,也或许是触发了单进程模式下的一些 Bug,导致风控页面无法跳转,具体方式还不清楚。

关闭--disable-site-isolation-trials 配置

添加这个配置的本意是方便进行跨域请求,但是据说这个需要跟 --disable-web-security--user-data-dir 一起使用才能生效(参考 StackOverflow)。但是实际上我测试下来,不加这个配置也能进行跨域访问,不太明白。

Cloudflare 似乎也能检测到这个参数引入后带来的问题,导致风控校验不过。原因也不明。

Chromium 内核版本尽量用较新的版本

众所周知,UserAgent 信息一定会被拿来进行大数据计算和风控识别,因此这个值的设置要尤其注意:

  1. UserAgent 里带的浏览器内核版本要尽量的新。长期不升级的浏览器显然是有问题的,况且内核较老的浏览器容易有未被修复的安全漏洞,可能会通不过检测。
  2. UserAgent 的值尽量和真实的内核版本一致。毕竟 UserAgent 声明的配置有时是会和其他配置互相应证的,如果不改全,很可能会被认为是恶意修改。不过,如果是 Linux 机器,还是建议不要用 Linux 的 UA,毕竟在 Linux 下刷网页的,更像是爬虫,更容易被风控。实测用 Mac 的 UA 会更好。

不要对所有页面都注入脚本

风控页面本身会加载很多的 Iframe 用于跑验证脚本、出验证码之类的,这些东西都会触发页面加载事件。通过 cdp 我们可以直接看到这些页面:

Just a moment...
https://cf-assets.hcaptcha.com/captcha/v1/6fdad99/static/hcaptcha.html#frame=challenge&id=04nubo7q8lgo&host=www.wethrift.com&sentry=true&reportapi=https%3A%2F%2Faccounts.hcaptcha.com&recaptchacompat=off&custom=
false&endpoint=https%3A%2F%2Fcloudflare.hcaptcha.com&hl=zh&assethost=https%3A%2F%2Fcf-assets.hcaptcha.com&imghost=https%3A%2F%2Fcf-imgs.hcaptcha.com&tplinks=on&sitekey=03196e24-ce02-40fc-aa86-4d6130e1c97a&theme=light&origin=https%3A%2F%2Fwww.wethrift.com
https://cf-assets.hcaptcha.com/captcha/v1/6fdad99/static/hcaptcha.html#frame=checkbox&id=04nubo7q8lgo&host=www.wethrift.com&sentry=true&reportapi=https%3A%2F%2Faccounts.hcaptcha.com&recaptchacompat=off&custom=false&endpoint=https%3A%2F%2Fcloudflare.hcaptcha.com&hl=zh&assethost=https%3A%2F%2Fcf-assets.hcaptcha.com&imghost=https%3A%2F%2Fcf-imgs.hcaptcha.com&tplinks=on&sitekey=03196e24-ce02-40fc-aa86-4d6130e1c97a&theme=light&origin=https%3A%2F%2Fwww.wethrift.com
https://cf-assets.hcaptcha.com/captcha/v1/6fdad99/static/hcaptcha.html#frame=challenge&id=12ks6yz7fbwp&host=www.wethrift.com&sentry=true&reportapi=https%3A%2F%2Faccounts.hcaptcha.com&recaptchacompat=off&custom=false&endpoint=https%3A%2F%2Fcloudflare.hcaptcha.com&hl=zh&assethost=https%3A%2F%2Fcf-assets.hcaptcha.com&imghost=https%3A%2F%2Fcf-imgs.hcaptcha.com&tplinks=on&sitekey=03196e24-ce02-40fc-aa86-4d6130e1c97a&theme=light&origin=https%3A%2F%2Fwww.wethrift.com
https://cf-assets.hcaptcha.com/captcha/v1/6fdad99/static/hcaptcha.html#frame=checkbox&id=12ks6yz7fbwp&host=www.wethrift.com&sentry=true&reportapi=https%3A%2F%2Faccounts.hcaptcha.com&recaptchacompat=off&custom=false&endpoint=https%3A%2F%2Fcloudflare.hcaptcha.com&hl=zh&assethos
t=https%3A%2F%2Fcf-assets.hcaptcha.com&imghost=https%3A%2F%2Fcf-imgs.hcaptcha.com&tplinks=on&sitekey=03196e24-ce02-40fc-aa86-4d6130e1c97a&theme=light&origin=https%3A%2F%2Fwww.wethrift.com

在对目标页面加载脚本时,尽量不要对那些校验用的中间页面进行 hook。原先我们有一个对所有加载的页面都打印一下日志的兜底脚本(没有做任何其他事情),本以为没啥影响,结果测试了很久发现,这个兜底脚本的存在极大影响了页面加载的成功率。具体原因未知,但是理论上的确是有办法能够检测到。

尽量提高页面加载速度

Did you know some signs of bot malware on your computer are computer crashes, slow Internet, and a slow computer?

大数据表明,越慢的网络环境越有可能是危险的。各种 Hook 处理、多层代理、抓包解包等都会影响网络响应速度,因此一定要想办法提高。

目前我们采用的抓包代理技术比较老旧,解包太慢了,导致一加抓包代理,页面打开的成功率就骤降。尤其是清除缓存后的浏览器首次打开时,由于缓存被干掉了,资源一起加载就会非常慢。因此我们在已有的抓包代理的基础上又套了一层静态资源缓存,实现对静态资源 Cache-Control 的处理,减少因清除缓存带来的速度变慢的问题。

实测后发现,加了静态资源缓存后,页面打开的成功率也有了明显提升。

改用指纹浏览器

上面的方法虽然基本上能让滑块或者验证码出来,但是仍然解决不了一直验证的问题。测试过很多浏览器之后发现,即使是本地正常的浏览器、正常的IP,很大概率也会出验证的。

但是偶然间发现了一款 GoLogin 指纹浏览器,在不做任何额外配置时、竟然能几乎100%通过 cloudflare 的盾,非常神奇(多次重试均可通过✅);但是在我加了一堆自以为是的配置之后,却又出验证了。看来爬虫浏览器改用指纹浏览器应当是一大趋势。不过目前市面上这类的浏览器基本都是闭源收费的,且价格一般都比较昂贵,只能说多多关注了。。。

有趣的东西

基于V8引擎特性的内核版本判断

通过 JS 校验 V8 引擎在不同 Chromium 内核版本下的特性,可以用来大致判断真实内核状态。如果和 UserAgent 声明的版本不一样,则会是一个非常负面的评价。

根据 V8 的版本说明文档,V8 的版本和 Chromium 版本是一一对应的:

说的很清楚,将 Chromium 的大版本号除以10,即得到了 V8 的大版本号。从 chrome://version 中也可以应证这一点(106.0.5249.103 -> 10.6.194.17):

Google Chrome 106.0.5249.103 (Official Build) (x86_64)
Revision 182570408a1f25ab2731ef5f283b918df9b9f956-refs/branch-heads/5249_91@{#6}
OS macOS Version 12.6 (Build 21G115)
JavaScript V8 10.6.194.17
User Agent Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36
Command Line /Applications/Google Chrome.app/Contents/MacOS/Google Chrome --flag-switches-begin --flag-switches-end
Executable Path /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

有了这个对应关系,我们就可以通过测试 V8 语言在不同版本下的语言特性,来推断当前 Chromium 内核的版本。我们可以在 V8 的 Features 页查看到 V8 的 Javascript api 各种新特性,选择其中一个希望测试的特性,再去 V8 的 Blog 页查看其对应的版本,从而对 Chromium 内核版本进行甄别。

例如,我们发现 V8 在 9.3 版本新增了一个 Object.hasOwn 方法,那么在 Chromium 内核高于 93 的机器上,下面的 Javascript 就会正常执行:

Object.hasOwn.toString()
'function hasOwn() { [native code] }'

而在内核低于 93 的机器上,下面的 Javascript 就会报错:

Object.hasOwn.toString()
Uncaught TypeError: Cannot read property 'toString' of undefined
at <anonymous>:1:15

通过测试足够多的特征,我们就可以很轻松的精确判断当前浏览器实际的内核版本。

当然,这里直接通过 toString() 来判断函数是否存在的方法比较粗暴,很容易绕过。实际上可以多设计一些测试用例,甚至可以在服务端随机生成测试用例,再将客户端返回的结果与服务端的进行对比,这样即使通过数据抓包、JS 逆向,也挺难处理分析的。

hCaptcha基于指纹和行为打分

和 reCatpcha 类似,hCaptcha 在提供验证服务的同时,也会给后端服务返回当前校验结果的“打分”,用于评判当前的用户时真人或是机器的概率。因此并不是说点对了验过码图片就能100%保证通过风控。

一般来说,这类验证码服务几乎都会通过 JS 收集浏览器的基本信息(尤其是 window.screen 和 window.navigor 下的所有参数),以及用户的行为信息(鼠标的移动序列),传回 hCaptcha 服务端;服务端经过大数据计算后返回类似如下接口的数据给业务后端,用于进行风控判断,这样业务后端就可以根据这个 score 来控制风控强度:

{
"success": true|false,     // is the passcode valid, and does it meet security criteria you specified, e.g. sitekey?
"challenge_ts": timestamp, // timestamp of the challenge (ISO format yyyy-MM-dd'T'HH:mm:ssZZ)
"hostname": string,        // the hostname of the site where the challenge was solved
"credit": true|false,      // optional: whether the response will be credited
"error-codes": [...]       // optional: any error codes
"score": float,            // ENTERPRISE feature: a score denoting malicious activity.
"score_reason": [...]      // ENTERPRISE feature: reason(s) for score.
}

本来我也想测试下自己的定制浏览器的实际得分,奈何这个 score 只对企业级用户开放,普通开发者是拿不到的。

寻找了一圈倒是发现 reCaptcha 有类似的 demo ,不过仔细一看,他返回的 score 也只是个 demo ,不具参考意义。

PDF字体乱码问题分析

作者 mythsman
2022年10月27日 18:03

背景

最近身体有恙,于是就想起来整理下这几年体检的电子报告汇总看看。结果发现在善诊平台下载的体检报告在电脑上看直接乱码了。不过这种字符集的问题怎么能难得了本码农呢,所以就尝试起各种办法进行恢复,结果发现这果然是个深坑。。。

问题

本来好好的一个 PDF 报告,结果打开后画风是这样的:

我本地的环境是 Macbook,尝试了用 Chrome 打开、自带的 Preview 工具打开,都是同样的结果。除了 ASCII 字符能展示,其他所有的中文信息都没有,但是 PDF 中的所有图片都能正常展示。这么看大概率还是中文字体的问题。

分析

EXIF信息

不管如何,首先肯定要看一下这个 PDF 本身带的 EXIF 信息,寻找一些分析线索。这里 mac 下可以直接在 Preview 里看,不过我更喜欢用命令行工具 exiftool 看。

$ brew install exiftool
$ exiftool -a -G1:2 report.pdf
[ExifTool]      ExifTool Version Number         : 12.42
[System:Other]  File Name                       : report.pdf
[System:Other]  Directory                       : .
[System:Other]  File Size                       : 522 kB
[System:Other]  File Permissions                : -rw-r--r--
[System:Time]   File Modification Date/Time     : 2022:10:27 11:06:00+08:00
[System:Time]   File Access Date/Time           : 2022:10:27 14:14:04+08:00
[System:Time]   File Inode Change Date/Time     : 2022:10:27 15:34:02+08:00
[File:Other]    File Type                       : PDF
[File:Other]    File Type Extension             : pdf
[File:Other]    MIME Type                       : application/pdf
[PDF:Document]  PDF Version                     : 1.7
[PDF:Document]  Linearized                      : No
[PDF:Document]  PDF Version                     : 1.7
[PDF:Document]  Tagged PDF                      : Yes
[PDF:Document]  Page Count                      : 11
[PDF:Document]  Page Mode                       : UseNone
[PDF:Document]  Title                           :
[PDF:Document]  Subject                         : FastReport PDF export
[PDF:Document]  Keywords                        : 宋体, Bold, -, SimSun;宋体, -, SimSun;微软雅黑, -, Microsoft, YaHei;微软雅黑, Bold, -, Microsoft, YaHei, Negreta;宋体, Bold, -, SimSun;宋体, -, SimSun;微软雅黑, -, Microsoft, YaHei;微软雅黑, Bold, -, Microsoft, YaHei, Negreta;
[PDF:Document]  Creator                         :
[PDF:Document]  Producer                        :
[PDF:Author]    Author                          : FastReport
[PDF:Time]      Create Date                     : 2021:02:24 15:35:03
[PDF:Time]      Modify Date                     : 2021:02:24 15:35:03

划一下重点:

  1. Keywords 字段似乎展示了他使用的字体信息,看起来都是微软字体。
  2. Author 和 Subject 字段似乎展示了生成这个 PDF 的工具:FastReport

众所周知 Mac 是不带微软字体的,那么这个问题似乎就是微软字体导致的。

微软字体替换

既然是 Mac 找不到微软字体,那我就把微软字体安装到本地应该就行了吧。于是我就找了个 Windows 电脑,跑去 C:\Windows\Fonts 下把所有的 ttf , ttc 文件拷贝到 MacBook 的 ~/Library/Fonts 下,作为当前用户的字体。

当然,这些字体看起来比较多,但是重要的还是 sim (中易系列)和 msyh (微软雅黑系列) 打头的这几个。

C:\Windows\Fonts> dir|findstr sim
2019/10/15  21:46        10,578,152 simfang.ttf
2019/10/15  21:46         9,753,388 simhei.ttf
2019/10/15  21:46        11,787,328 simkai.ttf
2019/12/07  17:08        18,214,472 simsun.ttc
2019/12/07  17:08        17,064,180 simsunb.ttf

C:\Windows\Fonts> dir|findstr msyh
2019/12/07  17:08        19,647,736 msyh.ttc
2019/12/07  17:09        16,829,116 msyhbd.ttc
2019/12/07  17:09        12,139,380 msyhl.ttc

可是,安装好后发现并不起作用,Chrome 和 Preview 还是不能用,感觉这就不太合理了。于是我把 PDF 拷回 Windows 下试试,发现果然连 Windows 自己都不能打开。。。

思考了一下,既然 PDF 格式主要是 Adobe 公司主导的,是不是他家的官方软件就能搞定呢?

Adobe Acrobat 的尝试

看网上有人说 Adobe Acrobat 有个 Chrome 插件,据说能解决很多乱码问题。于是就抱着试试看的心情装了一个。

果然没什么卵用。

考虑到 Adobe Acrobat 是收费的,为了这个小问题直接买有点不合适,又懒得找破解,于是就去 PDD 花 9¥ 买了个破解版。

软件是童叟无欺的,不过还是没什么卵用,依然预览起来都是乱码。而且还报了一个乱码的错,似乎是在说找不到一些字体(这些字体的名字是乱码的)。一番搜寻,发现有人在 Google Group 里提到:

通常对于字体的识别方式是先在文档内部寻找内嵌字体文件,如果没有字体文件,那么就根据文档所使用的字体名称在用户本地查找 相应的字体,最后使用替代机制——利用本地的默认字体来显示。
很不幸的是,一般的默认字体都是不识别非 acsii 字符的,所以就会出现各种乱码和字体很丑的 情况。

再结合报错时的一大堆乱码,那么我有理由进一步怀疑:如果文件的文本字符集有问题,而字体名又是非纯 ASCII 码的中文名,那么文本字符集的问题就会导致字体名的解析问题,进而就会导致字体解析的问题。但是究竟具体要怎么做才能修正字符集呢?不知道。

忽然想起来 PDF 传到电脑前明明在手机上预览过,于是我把目光转到了手机。。。

WPS 的逆袭

把 PDF 发回我的小米手机,发现果然能成功打开。

既然这样,那我是不是可以在手机上对 PDF 进行编辑,将有问题的字体切换成好使的通用字体,再重新保存呢?

小米默认采用的是 WPS 的组件,下载 WPS 后尝试编辑发现竟然又要冲会员。不能忍,于是下载了一个不要钱的 福昕PDF编辑器。结果福昕编辑器竟然又不能打开了。

难道说只能用 WPS 打开?于是我又在 Mac 上下载了 WPS,果然能打开了。。。

绕了一圈,能否正确打开这个 PDF 这件事跟平台没有任何关系,只跟软件有关。不得不说 FastReport 这个工具生成的 PDF 是真的流氓。

可是 WPS 的 PDF 编辑功能需要开通超级会员。没办法,只能又去 PDD 花了 2.9¥ 买了一个 ⭐️WPS 一天超级会员试用版⭐️

但是问题又来了,PDF 的编辑非常麻烦,每个文本框都是独立的,不能一次全选所有文本然后统一修改字体,只能一个框一个框的选。。。

忽然想到,为啥不先把 PDF 转成 Word,利用 Word 快速修改字体,然后再保存成 PDF 呢?

于是尝试了 WPS 的 PDF 转 Word 功能,按照刚才的思路,导出 Word 之后将所有文字的字体统一改成了 Arial ,再导出回 PDF。

终于成功生成了在各个平台都能正常使用的 PDF 。

iLovePDF

问题解决了,但是为了用一个小功能就要给流氓软件冲会员,实在不能忍。思考一番忽然明白了,我需要的其实只是一个“高兼容性的PDF转Word“工具——iLovePDF

有免费工具用了,谁还愿意交智商税呢?

将你的 PDF 转换为 DOC 和 DOCX 文件,以便于编辑。
将你的 PDF 转换为可编辑的 Office DOC 和 DOCX 文件。转换准确率近乎100%。
iLovePDF - 提供在线处理PDF文件的多种工具

利用 Redsocks 解决透明代理的远程抓包问题

作者 mythsman
2022年5月9日 22:10

背景

最近在做一些远程设备的抓包能力建设。具体来说是设备(基于 Docker 的 Redroid 云手机)在服务器上,抓包工具在本地( Mitmproxy , Charles, Fiddler 等类似工具),希望通过某种方法将远程设备上的流量打到本地的抓包工具上,并且流量通过本地的 IP 出到公网。

这样做的目的主要有两个:

  1. 可以做到抓包工具和待抓包设备的分离。既能利用上强大的第三方抓包工具,又无需本地部署待抓包的设备。毕竟如果在远程服务器上部署抓包工具,操作起来就不那么方便,甚至可能还需要自行开发管控界面;如果在本地部署待抓包设备,很可能会遇到例如芯片架构、操作系统、环境依赖、系统资源消耗等问题。
  2. 可以方便进行 IP 出口的调整。在调试一些不可言说的功能时,服务器上部署的设备很可能是走一些代理 IP 池,有时候这些 IP 池本身可能有点问题,导致远程设备被封。这时候如果我们能够将设备的流量导到本地的正常 IP 出公网,可能会更方便我们验证是 IP 问题还是其他的设备问题。

计划

为了打通远程设备本地抓包的这套链路,我们需要考虑如下技术点:

  1. 远程设备需要能够安装本地抓包工具的证书。
  2. 远程设备的流量需要通过某种内网穿透能力打到本地的代理工具上。
  3. 远程设备的流量需要保证不遗漏地进行转发。

由于我们的远程设备是有 root 权限的云手机,因此证书安装并不难。只要将本地的证书通过 openssl 命令转换成指定格式的证书文件,传到服务器上,在云手机启动时 bind 到 /system/etc/security/cacerts/  目录下即可。

同理,由于远程设备是云手机,通过暴露 adb 的 tcpip 端口,我们可以用本地的 adb 客户端进行连接,再通过 adb reverse 就可以构建一个云手机访问本机代理端口的信道。

而要保证云手机的流量(这里特指 HTTP/HTTPS 流量)不遗漏的进行转发,我们就不能采用配置全局正向代理的方法( adb shell settings put global http_proxy xxxx  ),因为个别 app 可以配置强制 NO_PROXY 不走系统代理。一个简单的方法是通过云手机自带的 iptables 工具进行转发,将云手机中所有目的端口为 80/443 的流量转发到 adb reverse 命令转发过来的、映射到本地抓包工具的代理端口即可。

理想的架构图如下:

问题

架构图谁都会画,但是真正实操起来才发现有一堆坑。这套流程对 HTTP 请求的确是有用的,透明代理的工具无论是使用 Charles 还是 Mitmproxy 等中间人代理工具都能正常抓到包。但是对与 HTTPS 的流量则都出现了问题:

  • Charles 会报 invalid first line in request 的错。
  • Mitmproxy 会报 Could not resolve original destination 的错。

当然,个别代理工具可能不支持解 HTTPS ,出现问题可以理解。但是各种代理工具都不能抓,那显然应当是流程上出了问题。经过实验我们发现,使用正向代理或非 HTTPS 的透明代理再加上端口转发都是能通的,唯一不能通的是 HTTPS 的透明代理模式。那么我们就需要先辨析一下这些模式的区别。

分析

正向代理

正向代理是由客户端主动发起,主动将流量打给一个代理服务器,由代理服务器代替请求的过程。下图主要展示正向代理过程中 IP 报文的变化:

  • Alice 代表发起请求的客户端
  • Flank 代表代理服务器(Forward Proxy)
  • Bob 代表客户端需要请求到的服务器

透明代理

透明代理是客户端本身无感知的,由路由转发工具强行进行流量转发(Linux 下可以用 iptables ,Windows 下可以用 netsh ,Mac 下可以用 pfctl)。下图主要展示透明代理过程中 IP 报文的变化:

  • Alice 代表发起请求的客户端
  • Ivan 代表转发流量的路由工具(Iptables之类的工具)
  • Tom 代表透明代理服务器(Transparent Proxy)
  • Bob 代表客户端需要请求到的服务器

在透明代理模式下,路由工具会非常暴力地将客户端发来的包的目的地址直接改为透明代理服务器,这会导致当数据包到了透明代理服务器中时,代理服务器是无法直接获取客户端真正想要到达的服务器地址。而正向代理服务器则不同,客户端会明确告知代理服务器他想访问谁。

那么透明代理服务器要如何在报文中获得真实的目的地址呢?这时候就需要分情况讨论了。

HTTP

我们知道 HTTP 报文是纯明文,就像一个没有封口的信封。只要打开来看就会发现,HTTP 请求报文会在 Header 中带上一个 Host 头表明当前的信期望到达的地方。透明代理服务器可以非常方便地解析到这个信息,从而知道报文需要被发送到的目的地址(Bob)。

HTTPS

HTTPS 这里就比较尴尬了。我们知道 HTTPS 在第四层有一个 TLS 加密层,如果想和 HTTP 一样从 Header 中获取 Host 头的话,则需要先进行 TLS 解密;但是,如果想进行 TLS 解密,则必须和实际的服务器进行 TLS 握手;可是你都不知道实际的服务器在哪,如何握手呢?这竟然变成了一个鸡生蛋还是蛋生鸡的问题。

那么问题最终会怎么解决呢?目光还得回到路由工具。

既然报文是你路由工具传给透明代理的,那显然路由工具这边是记录了报文原先实际需要访问的目的地址的,我直接请求你不就好了么?在 Linux 下,我们有一个用户工具 conntrack 可以展示当前网络连接的链路追踪信息:

$ sudo conntrack -L
tcp      6 60 TIME_WAIT src=192.168.32.1 dst=192.168.32.3 sport=40298 dport=8080 src=192.168.32.3 dst=192.168.32.1 sport=8080 dport=40298 [ASSURED] mark=0 use=1
tcp      6 56 TIME_WAIT src=192.168.32.1 dst=192.168.32.7 sport=54398 dport=2368 src=192.168.32.7 dst=192.168.32.1 sport=2368 dport=54398 [ASSURED] mark=0 use=1
tcp      6 60 TIME_WAIT src=192.168.32.5 dst=172.17.0.1 sport=52992 dport=5001 src=172.17.0.1 dst=192.168.32.5 sport=5001 dport=52992 [ASSURED] mark=0 use=1
tcp      6 79 TIME_WAIT src=10.0.0.4 dst=168.63.129.16 sport=39414 dport=80 src=168.63.129.16 dst=10.0.0.4 sport=80 dport=39414 [ASSURED] mark=0 use=1

而透明代理程序则可以通过 getsockopt 等方法直接向内核查询 socket 的链路信息:

static int getdestaddr(int fd, struct sockaddr_storage *destaddr)
{
    socklen_t socklen = sizeof(*destaddr);
    int error         = 0;

    error = getsockopt(fd, SOL_IPV6, IP6T_SO_ORIGINAL_DST, destaddr, &socklen);
    if (error) { // Didn't find a proper way to detect IP version.
        error = getsockopt(fd, SOL_IP, SO_ORIGINAL_DST, destaddr, &socklen);
        if (error) {
            return -1;
        }
    }
    return 0;
}

这样一来,透明代理服务就能在 TLS 握手前就能拿到真实的目的 IP 了。

然而有了 IP 就够了么?并不是。路由工具中我们只能拿到目的 IP ,但是并没有域名!现代服务端的网关层基本都需要通过域名来进行转发。如何解决这个问题呢?这就引入了 SNI (Server Name Indication)头。在 TLS 握手阶段,客户端会在握手报文里额外增加 SNI 信息(这个已经是TLS标准了,但是有些客户端可能没有加),这样服务端或者透明代理服务器就能获取到实际的域名了。

最后我们再来看下 Mitmproxy 文档中提供的透明代理的流程图:

解决

了解了代理服务器的工作细节,我们再来尝试回答下开头的问题。为什么把远端的 HTTPS 流量通过路由工具+端口映射转发到本地的透明代理服务器中会报错呢?

答案已经很明显了,那就是本地的透明代理工具无法查询到远端服务器中的路由链路信息,导致无法获取真实的目的地址。(也就是为什么 mitmproxy 会报 Could not resolve original destination的原因。

解决这个问题的思路也很清晰,既然希望走透明代理的流量无法跨主机,那我们就将透明代理服务部署在本机,然后转换成正向代理出去即可

具体操作中,由于 mitmproxy 默认不支持在透明代理模式下再配置一个上游正向代理,因此我们可以选择简单魔改一下代码,在透明代理模式下的出口处请求另一个正向代理。

如果懒得改代码,我找到了一个 Redsocks 工具。这个工具可以直接作为透明代理服务器,并将流量转发给一个上游的正向代理。Ubuntu 下可以直接用 apt 安装,配置一下 /etc/redsocks.conf 就能直接使用。

如此,架构图修改成下图即可:

稳定性

方案上线后,原先预计 redsocks 可能会对系统稳定性造成影响,后来发现这一块还好,反倒是由于 mitmproxy 从透明代理模式切到正向代理/上行代理模式,导致 mitmproxy 经常 crash,参见 issue ,略坑。

参考资料

Mitmproxy Docs - Transparent HTTPS

如何使用透明代理抓 HTTPS

Istio的流量劫持和Linux下透明代理实现

快手抓包问题分析

作者 mythsman
2022年2月21日 14:10

背景

不知从什么版本后,对快手进行简单抓包似乎“不可行”了。表现就是使用常规的 HTTP 正向代理抓包工具(charlesmitmproxyfiddler 之类)并且把自签名证书种到系统证书里后,数据依然能刷出来,也能抓到一些零星的报文,但是关键出数据的那些接口报文都没有。

一般来说,常规方法无法抓安卓应用的 https 包,通常有以下几种可能:

  1. 证书信任问题。在 Android 7 以上,应用会默认不信任用户证书,只信任系统证书,如果配置不得当则是抓不到包的。
  2. 应用配置了 SSL pinning,强制只信任自己的证书。这样一来由于客户端不信任我们种的证书,因此也无法抓包。
  3. 应用使用了纯 TCP 传输私有协议(通常也会套上一层 TLS)。由于甚至都不是 HTTP 协议,当然就抓不到包了。
  4. 应用使用 WebSocket 长链接,将不同的接口封装在这个长链接里。在 WebSocket 里承载的协议一般是用某种自定义方式来模拟 http 请求,因此也难以抓包。
  5. 应用使用了基于 UDP 的 http3.0协议等。大部分代理工具目前还不支持 quic 等协议,所以这样一般是抓不到包的。
  6. 应用配置了 Proxy.NO_PROXY ,强制不走系统代理。这样http流量就绕过我们配置的代理,自然抓不到包。

当前的现象是数据能刷出来,那就说明并不是证书信任相关的问题。接下来就需要验证它究竟是使用了什么样的传输方式,对症下药。

最稳妥的验证方式当然是白盒测试看源码,不过快手反编译的代码看起来也费劲,于是考虑直接当成黑盒来测试看看。以下验证方式均以 快手 8.2.31.17191 版本为例。

分析

环境部署

准备自签名CA证书

需要在 Linux 主机上使用 openssl 工具生成一波证书。当然,这一步可以忽略,直接使用mitmproxy生成的证书。只是手动配置一下能够加深一下对 openssl 的理解。

# 生成RSA私钥 cert.key 
openssl genrsa -out cert.key 2048

# 生成有效期为15年的X509证书 cert.crt(DN信息随便填填似乎都行,但是不能全空)
openssl req -new -x509 -key cert.key -out -days 5480 cert.crt

# 私钥和证书放一起,构成PEM格式的 cert.pem
cat cert.key cert.crt > cert.pem

# 将之前生成的pem证书作为mitmproxy使用的根证书
cp cert.pem ~/.mitmproxy/mitmproxy-ca.pem

# 以普通正向代理模式启动在8000端口,并配合curl简单验证证书可用性
mitmproxy -p 8000 

# 能正常访问没有SSL相关报错,就说明之前生成的自签名证书是OK的
curl -x localhost:8000 --cacert ~/.mitmproxy/mitmproxy-ca.pem https://www.baidu.com

# 从证书文件中计算出用于放置在安卓中的文件hash名,假设结果为 a5176621 
openssl x509 -subject_hash_old -in cert.crt -noout

# 将cert.crt复制出一个上述hash结果的新文件
cp cert.crt a5176621.0

值得注意的是,不要尝试使用 mitmproxy --certs 来配置证书,这种方式只能配置 leaf 证书,而不能配置根 CA 证书。因此还是老老实实的把根证书放在默认路径下。

准备设备

为了方便测试,我在 arm 服务器上使用 redroid 准备了一台安卓虚拟机。

docker run -itd --rm --memory-swappiness=0 \
--privileged --name redroid \
--mount type=bind,source=/home/myths/exp/a5176621.0,target=/system/etc/security/cacerts/a5176621.0 \
-p 5555:5555 \
redroid/redroid:11.0.0-arm64 \
redroid.width=720 \
redroid.height=1280 \
redroid.fps=15 \
redroid.gpu.mode=guest

其中 /home/myths/exp/a5176621.0 替换成实际文件所在路径。

然后在arm主机上用 adb 连接安卓的 tcpip 端口,下载并安装快手 8.2.31.17191 版本。

# 在arm服务器上用adb连接安卓虚拟机
adb connect localhost:5555

# 安装快手
adb install kuaishou.apk

为了方便远程操作,需要在本地机器上连接 arm 服务器上的安卓虚拟机,并用scrcpy操作。

# 在本地主机上连接远程arm服务器上的安卓虚拟机
adb connect <ip for arm server>:5555

# 启动scrcpy
scrcpy

到这一步骤时,可以检测安卓中的网络应该都已经是通的了。

复现抓包问题

先尝试使用传统正向代理的方式进行抓包。

# 在arm服务器上用正向代理启动mitmproxy
mitmproxy -p 8000

# 对安卓设置正向代理,其中 172.17.0.1 为安卓下访问arm主机的ip
adb shell settings put global http_proxy 172.17.0.1:8000

设置完成后,观察手机的网络状况,现象如下:

  1. 使用浏览器访问普通网站,返回均正常,mitmproxy也能抓到包。
  2. 刷快手推荐页,返回也正常,但是mitmproxy只能抓到一些静态资源,无法抓到接口。

Ban掉不走代理的所有流量

有数据但是抓不到包,怀疑应当是有些流量漏掉了,于是尝试把这些流量Ban掉看看效果。

# 首先需要打开内核的 ip_forward 功能
echo 1 > /proc/sys/net/ipv4/ip_forward

# 依然在arm服务器上用正向代理启动mitmproxy
mitmproxy -p 8000

# 继续在手机上配置http代理,其中172.17.0.1为安卓下访问arm主机的ip
adb shell settings put global http_proxy 172.17.0.1:8000

# 在 arm 服务器上配置iptables,将来源于安卓虚拟机但目的地不是arm服务器的流量重定向到一个无用端口。
# 其中 172.17.0.12 位安卓虚拟机的ip,1234为一个无用端口。
sudo iptables -t nat -I PREROUTING -p tcp -s 172.17.0.12 ! -d 172.17.0.1 -j REDIRECT --to-ports 1234
# 如果上述操作报了 Couldn't load match `owner':No such file or directory 的错,那么需要在 arm 服务器上启动下相关模块。
sudo modprobe ipt_owner

# 记得测试完成后,将上面这个记录干掉
iptables -t nat -D PREROUTING 1

设置完成后,观察手机的网络状况,现象如下:

  1. 使用浏览器访问普通网站,返回均正常,mitmproxy也能抓到包。
  2. 刷快手推荐页,显示“无网络连接“。

说明这里的确是有流量漏了,没有走正向代理,但是依然出了外网。

Ban掉不走代理的443/80流量

那么这些流量到底是私有的四层TCP流量、还是没走正向代理的80/443流量呢?因此尝试把非80/443的流量放开试试。

# 依然在arm服务器上用正向代理启动mitmproxy
mitmproxy -p 8000

# 继续在手机上配置http代理
adb shell settings put global http_proxy 172.17.0.1:8000

# 在 arm 服务器上配置iptables,将来源于安卓虚拟机但目的端口不是80/443的流量重定向到一个无用端口。
# 其中 172.17.0.12 位安卓虚拟机的ip,1234为一个无用端口。
sudo iptables -t nat -I PREROUTING -p tcp -s 172.17.0.12  --dport 80 -j REDIRECT --to-ports 1234
sudo iptables -t nat -I PREROUTING -p tcp -s 172.17.0.12  --dport 443 -j REDIRECT --to-ports 1234

# 记得测试完成后,将上面这两条记录干掉
iptables -t nat -D PREROUTING 1
iptables -t nat -D PREROUTING 1

设置完成后,观察手机的网络状况,现象如下:

  1. 使用浏览器访问普通网站,返回均正常,mitmproxy也能抓到包。
  2. 刷快手推荐页,依然显示“无网络连接“。

这就说明,控制快手推荐页的流量并不是所谓私有流量,而就是走的80/443端口,只是没有走正向代理。

改用透明代理模式

既然七层的代理配置会被忽略,那就尝试使用四层的透明代理,将流量强制转到透明代理服务器上即可。

# 在arm服务器上以透明代理模式启动mitmproxy
mitmproxy -p 8000 -m transparent

# 将手机上的http代理移除
adb shell settings put global http_proxy :0

# 在arm服务器上配置将来源于安卓虚拟机的的80/443流量直接路由到mitmproxy
# 其中 172.17.0.12 位安卓虚拟机的ip
sudo iptables -t nat -I PREROUTING -p tcp  -s 172.17.0.12 --dport 80 -j REDIRECT --to-ports 8000
sudo iptables -t nat -I PREROUTING -p tcp  -s 172.17.0.12 --dport 443 -j REDIRECT --to-ports 8000

# 记得测试完成后,将上面这两个记录干掉
iptables -t nat -D PREROUTING 1
iptables -t nat -D PREROUTING 1

设置完成后,观察手机的网络状况,现象如下:

  1. 使用浏览器访问普通网站,返回均正常,mitmproxy也能抓到包。
  2. 刷快手推荐页,能成功刷出,并且mitmproxy也抓到了包。

总结

目前看来,快手并非像很多网上传的那样,大多数接口都走了 kquic 协议导致无法抓包。其实很多接口只是做了一个禁止走系统代理的设置,简单使用透明代理模式进行抓包即可轻松绕过。当然,不排除有些关键接口也做了SSL pinning、走私有协议之类的。。。

参考

https://docs.mitmproxy.org/stable/concepts-certificates/

https://stackoverflow.com/questions/56830858

https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/-builder/proxy/

树莓派与1024x600分辨率屏幕的适配问题

作者 mythsman
2021年12月27日 21:41

问题

为了方便操作,买了一个 7英寸的 LCD 触摸屏,结果发现屏幕是 1024x600 的分辨率,系统总是会认为是 1024x768 的分辨率,导致了两个后果:

  1. 分辨率不准。由于实际展示的高度比渲染的要低,导致屏幕下方的东西是看不到的。
  2. 触摸板不准。虽然图像渲染会超出屏幕,但是触摸板似乎却做了缩放,触摸的地方越往下鼠标跟手的位置就越不准。

问了下亚博智能的技术客服,按照他的建议修改了 /boot/config.txt 中的 hdmi_cvt 等相关参数,重启后依然不行。接着他们建议直接刷入他们配置好的 armhf 镜像。由于我需要使用 arm64 ,因此个我肯定是不能干的。于是就自己琢磨了下,大半天才终于搞定。

踩坑

弯路稍微提一嘴吧,不然对不起大半天踩的坑。

为了解决分辨率问题,研究了树莓派官网HDMI配置的一大堆参数,经过一下午的重启后,结果还是不行。

为了解决触摸板不准的问题,找到了 xinput_calibrator 这个屏幕校准的工具,结果由于屏幕太矮,下面的两个检查点根本点不到,太坑。

解决

这个问题可以理解是驱动问题,应当还是要直接找到生产厂商,整到驱动相关的文档。

经过多方查找,发现我的这个板子是 微雪的7inch HDMI LCD ,找到官方文档后,发现了驱动的 github 地址:https://github.com/waveshare/LCD-show.git

代码clone下来后,似乎是要执行其中的 ./LCD7-1024x600-show 脚本。不过慢着,先看下脚本做了啥:

i=1
for lines in `cat ./boot/mark`
do
  case ${i} in
  1) var1=${lines};;
  esac
  i=`expr ${i} + 1`
done
#echo "$var1"

j=$(uname -n)
if test "$var1" = "0" -a "$1" != "lite" -a "$2" != "lite" ;then

if test "$j" != "retropie" ;then
sudo apt-get install xserver-xorg-input-evdev
sudo cp -rf /usr/share/X11/xorg.conf.d/10-evdev.conf /usr/share/X11/xorg.conf.d/45-evdev.conf
fi

if test "$j" = "retropie" ;then
sudo cp -rf ./nes /home/pi/RetroPie/roms/
fi

echo "1" > ./boot/mark
sudo dpkg -i -B ./xinput-calibrator_0.7.5-1_armhf.deb

sudo apt-get install cmake -y
cd ./rpi-fbcp/build/
sudo cmake ..
sudo make
sudo install fbcp /usr/local/bin/fbcp
cd ../../
sudo mkdir -p /usr/share/X11/xorg.conf.d

elif test "$var1" = "0" -a "$1" = "lite" ;then
echo "1" > ./boot/mark
echo "No touch driver installled"
echo "No fbcp driver installled"
sudo mkdir -p /usr/share/X11/xorg.conf.d

fi

sudo cp -rf ./etc/rc.local /etc/rc.local

if test "$1" = "0" -o "$#" = "0" -o "$2" = "0" ; then
sudo cp -rf ./etc/X11/xorg.conf.d/99-calibration.conf-7-1024x600  /usr/share/X11/xorg.conf.d/99-calibration.conf
sudo cp ./boot/config-7-1024x600.txt /boot/config.txt
echo "LCD configure 0"
elif test "$1" = "lite" -a  "$#" = "1"; then
sudo cp -rf ./etc/X11/xorg.conf.d/99-calibration.conf-7-1024x600  /usr/share/X11/xorg.conf.d/99-calibration.conf
sudo cp ./boot/config-7-1024x600.txt /boot/config.txt
echo "LCD configure 0"
elif test "$1" = "90" -o "$2" = "90" ; then
sudo cp -rf ./etc/X11/xorg.conf.d/99-calibration.conf-7-1024x600-90  /usr/share/X11/xorg.conf.d/99-calibration.conf
sudo cp ./boot/config-7-1024x600.txt-90 /boot/config.txt
echo "LCD configure 90"
elif test "$1" = "180" -o "$2" = "180" ;then
sudo cp -rf ./etc/X11/xorg.conf.d/99-calibration.conf-7-1024x600-180  /usr/share/X11/xorg.conf.d/99-calibration.conf
sudo cp ./boot/config-7-1024x600.txt-180 /boot/config.txt
echo "LCD configure 180"
elif test "$1" = "270" -o "$2" = "270" ;then
sudo cp -rf ./etc/X11/xorg.conf.d/99-calibration.conf-7-1024x600-270  /usr/share/X11/xorg.conf.d/99-calibration.conf
sudo cp ./boot/config-7-1024x600.txt-270 /boot/config.txt
echo "LCD configure 270"
fi



sudo cp -rf ./usr/share/X11/xorg.conf.d/99-fbturbo.conf-HDMI /usr/share/X11/xorg.conf.d/99-fbturbo.conf 
if test "$j" = "kali" ;then
sudo cp ./usr/share/X11/xorg.conf.d/99-fbturbo.conf-kali-HDMI /usr/share/X11/xorg.conf.d/99-fbturbo.conf
fi

if [ -b /dev/mmcblk0p7 ]; then
sudo cp ./cmdline.txt-noobs /boot/cmdline.txt
else
sudo cp ./cmdline.txt /boot/
fi

sudo cp ./inittab /etc/

if test "$#" = "0" -o "$1" = "0" -o "$1" = "90"  -o "$1" = "180" -o "$1" = "270" -o "$2" = "0" -o "$2" = "90"  -o "$2" = "180" -o "$2" = "270" -o "$1" = "lite" -o "$2" = "lite"; then
sudo reboot
echo "reboot now"
else
echo "Invalid parameter,Usage:LCD7-1024x600-show [0] [90] [180] [270] [lite]"
fi

需要注意的坑是这里有个 sudo dpkg -i -B ./xinput-calibrator_0.7.5-1_armhf.deb 操作。不懂为啥他要把 xinput-calibrator 的 armhf 包写死在项目里。如果系统镜像是 arm64 的话,这个包是用不了的,还会报错。解决方法是,把这行直接改成 sudo apt install xinput-calibrator -y 即可。

其他操作看起来都没有坑,比如用 apt 安装依赖、往 /usr/share/X11/xorg.conf.d/ 下扔配置文件、往 /etc 下扔 inittab 文件、修改 /boot/config.txt 配置文件、修改 /boot/cmdline.txt 启动参数等等,似乎都是些业务逻辑。

但其实这里的配置修改的地方简直太暴力了,都是直接用项目里的配置完整替换掉原始文件,也不做任何备份。万一我自己加了一些配置,这里就完全被覆盖掉了,找都找不回来。。。所以建议在刷完干净的镜像后就执行这个命令,不要急着配置其他的东西。并且记得把这两个文件手动备份下,万一出了问题也方便改回去。。。

树莓派4B使用arm64系统踩坑

作者 mythsman
2021年12月27日 21:39

背景

由于工作原因,需要一台 arm64 的服务器测试一些功能。但是目前这个点没法快速采购到腾讯云或者百度云的arm服务器(这俩公司的arm服务器好像都只是在内测阶段,据说得2022年初才能 Release)。想了一圈发现树莓派似乎正好有 arm64 的cpu,于是去官网确认了下 Specification

Broadcom BCM2711, Quad core Cortex-A72 (ARM v8) 64-bit SoC @ 1.5GHz

看起来挺好的,于是在亚博智能店里买了个 4B ,但是买到后跑起来发现竟然不是 arm64 的,大呼上当。不过仔细一研究发现原来是原装的 Raspberry os 镜像使用的是 armhf 内核(即支持hard float 的32位 arm )。而树莓派官方其实在 2020 年已经发布了支持 arm64 内核的镜像了,参见这个 Reddit 讨论。同时也发现 arm64 的系统相比 armhf 其实也更能发挥64位cpu的能力。

既然如此,那就搞起来吧。

方案

网上冲浪了一把,发现基本有俩套解决方案:

  1. 修改 /boot/config.txt 的 arm_64bit 配置(参考官方配置文档)。
  2. 重新刷入 arm64 位的镜像。

方案1看起来更方便,方案2看起来更稳妥,于是我挨个试了下。为了验证系统架构的正确性,我也准备了几个测试方法:

  1. uname -a
  2. cat /proc/cpuinfo
  3. getconf LONG_BIT
  4. file /bin/bash
  5. cat /proc/version
  6. dpkg --print-architecture

验证

原始环境配置

首先看一下原版基于 raspios_armhf-2020-05-28 的镜像下的配置情况。

pi@raspberrypi:~ $ uname -a
Linux raspberrypi 5.10.63-v7l+ #1496 SMP Wed Dec 1 15:58:56 GMT 2021 armv7l GNU/Linux
pi@raspberrypi:~ $ cat /proc/cpuinfo
processor : 0
model name : ARMv7 Processor rev 3 (v7l)
BogoMIPS : 108.00
Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
processor : 1
model name : ARMv7 Processor rev 3 (v7l)
BogoMIPS : 108.00
Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
processor : 2
model name : ARMv7 Processor rev 3 (v7l)
BogoMIPS : 108.00
Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
processor : 3
model name : ARMv7 Processor rev 3 (v7l)
BogoMIPS : 108.00
Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
Hardware : BCM2711
Revision : d03114
Serial  : 10000000b6d307b2
Model  : Raspberry Pi 4 Model B Rev 1.4
pi@raspberrypi:~ $ getconf LONG_BIT
32
pi@raspberrypi:~ $ file /bin/bash
/bin/bash: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0, BuildID[sha1]=3e5e2847bbc51da2ab313bc53d4bdcff0faf2462, stripped
pi@raspberrypi:~ $ cat /proc/version
Linux version 5.10.63-v7l+ (dom@buildbot) (arm-linux-gnueabihf-gcc-8 (Ubuntu/Linaro 8.4.0-3ubuntu1) 8.4.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #1496 SMP Wed Dec 1 15:58:56 GMT 2021
pi@raspberrypi:~ $ dpkg --print-architecture
armhf

基本符合预期,唯一让我一开始比较意外的就是 /proc/cpuinfo 里的 Model Name 是 ARMv7 Processor rev 3 (v7l),和官方声称的 ARM v8 并不一致。后来才明白原来这个值也是跟系统内核有关的,有时并不能真正准确的表达真是硬件的属性。这个镜像下的系统的确是32位arm无疑了。

修改环境配置

参照官网配置文档,将 /boot/config.txt 中的 arm64_bit 置位1,重启后重新进行下检测。

pi@raspberrypi:~ $ uname -a
Linux raspberrypi 5.10.63-v8+ #1496 SMP PREEMPT Wed Dec 1 15:59:46 GMT 2021 aarch64 GNU/Linux
pi@raspberrypi:~ $ cat /proc/cpuinfo
processor : 0
BogoMIPS : 108.00
Features : fp asimd evtstrm crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
processor : 1
BogoMIPS : 108.00
Features : fp asimd evtstrm crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
processor : 2
BogoMIPS : 108.00
Features : fp asimd evtstrm crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
processor : 3
BogoMIPS : 108.00
Features : fp asimd evtstrm crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
Hardware : BCM2835
Revision : d03114
Serial  : 10000000b6d307b2
Model  : Raspberry Pi 4 Model B Rev 1.4
pi@raspberrypi:~ $ getconf LONG_BIT
32
pi@raspberrypi:~ $ file /bin/bash
/bin/bash: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0, BuildID[sha1]=3e5e2847bbc51da2ab313bc53d4bdcff0faf2462, stripped
pi@raspberrypi:~ $ cat /proc/version
Linux version 5.10.63-v8+ (dom@buildbot) (aarch64-linux-gnu-gcc-8 (Ubuntu/Linaro 8.4.0-3ubuntu1) 8.4.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #1496 SMP PREEMPT Wed Dec 1 15:59:46 GMT 2021
pi@raspberrypi:~ $ dpkg --print-architecture
armhf

结果有点出乎意料。

通过内核版本查看的cpu架构基本都能正确展示出 aarch64 的关键字,但是其实系统中除了内核之外的其他软件仍然是 32 位的版本(毕竟只是更新了内核)。而且 dpkg 默认使用的软件架构仍然是 32 位的。这意味着当前系统其实有点四不像,既有64位的东西,又有32位的东西,不用想就知道以后100%会踩坑。

同时注意到,刷了新内核之后,cpuinfo 展示硬件信息为 Hardware : BCM2835 ,而原来的硬件信息却是Hardware : BCM2711 。在 gadgetversus 里对比了一下两种芯片:

虽然 2835>2711,但其实是变成了个老版本,真的是闹不明白。。。后来在官方论坛上瞅了瞅,才发现树莓派的软件总监说这好像是因为实际上存在多个芯片类型,但是为了一些上下游兼容的逻辑,就展示了一个错的。。。

顺带吃一个瓜:

mmuaa同学在博客里分享了他的方法。pingwei同学照着弄死机了,也写了个博客,然后还去mmuaa同学的博客下面喷粪,笑死了。

重新刷入镜像

四不像的系统肯定是不要用的,于是就去官网下了Imager,又去清华镜像站里下了 最新的基于 buster (ubuntu 18.04 , debian 10) 的arm64位镜像,烧进TF卡里。装机成功后验证了下数据。

pi@raspberrypi:~ $ uname -a
Linux raspberrypi 5.10.17-v8+ #1414 SMP PREEMPT Fri Apr 30 13:23:25 BST 2021 aarch64 GNU/Linux
pi@raspberrypi:~ $ cat /proc/cpuinfo
processor : 0
BogoMIPS : 108.00
Features : fp asimd evtstrm crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
processor : 1
BogoMIPS : 108.00
Features : fp asimd evtstrm crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
processor : 2
BogoMIPS : 108.00
Features : fp asimd evtstrm crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
processor : 3
BogoMIPS : 108.00
Features : fp asimd evtstrm crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
Hardware : BCM2835
Revision : d03114
Serial  : 10000000b6d307b2
Model  : Raspberry Pi 4 Model B Rev 1.4
pi@raspberrypi:~ $ getconf LONG_BIT
64
pi@raspberrypi:~ $  file /bin/bash
/bin/bash: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, BuildID[sha1]=b11533bde88bb45ef2891fbf3ad86c1869ed3a41, stripped
pi@raspberrypi:~ $ cat /proc/version
Linux version 5.10.17-v8+ (dom@buildbot) (aarch64-linux-gnu-gcc-8 (Ubuntu/Linaro 8.4.0-3ubuntu1) 8.4.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #1414 SMP PREEMPT Fri Apr 30 13:23:25 BST 2021
pi@raspberrypi:~ $ dpkg --print-architecture
arm64

这下终于统一都是 arm64 的了,虽然 cpuinfo 还是显示的是老的芯片信息(BCM2835),不过毕竟没啥太大影响。

剩下需要注意的就是,在配置新的apt源的时候,/etc/apt/sources.list 直接配置 debian 10 的镜像即可,不要用 raspberrypi 的镜像了;/etc/apt/sources.list.d/raspi.list 里再配置下 raspberrypi 的特定源即可。

❌
❌