Project RC

一种标准阿西的设计与实现。

QDP:一种以 QQ 消息为传输介质的通信协议

创建于
分类:Dev
标签:QDPUDP通信协议QQ酷QCQHTTP

QDP(QQ-based Datagram Protocol)是一种基于 QQ 消息的传输协议,可用于通过 QQ 传输任意二进制数据,并自动对数据报进行分片发送。

协议实现见 richardchien/qdp

动机

上面的描述听起来很诡异,但确实是这样。

起因是做 CQHTTP 插件的时候想基于 酷Q 写一个新的 QQ 客户端,然后想到可以通过 QQ 消息发送一些 QQ 自己的客户端不支持的内容,比如 Markdown,再由这个第三方客户端来进行解析和显示,进而意识到其实可以通过 Base64 等编码来使任意二进制数据的传输称为可能。

和朋友讨论的过程中也意识到其实这个东西理论上可以用来做隧道代理(虽然速度会非常非常慢)。

这个想法在脑海里搁置了很久之后,终于闲 xian 来 de 无 dan 事 teng 来设计了一下协议的具体内容并做了简单实现。

思路

首先,QQ 消息只能发送文本内容,而我们想实现任意二进制的传输,因此需要把二进制转换成字符串,既然是概念验证,肯定就选最简单的 Base64 了。

然后,考虑到发送的二进制内容可能非常大,编码之后可能会超出 QQ 单条消息的允许大小,经过简单测试,发现一条消息可以发送 4500 个 ASCII 字符,也就是说,一条消息最大能发送 4500 字节,这里我们记 QQ 消息的 MTU 为 4500 B。

这个 MTU,实际上不能全部拿来传输数据,因为我们要能够确保知道这条消息是 QDP 协议的数据,还需要一些标记手段,这里通过在 Base64 编码之后的消息开头加上魔术前缀来实现,例如 <<<42>>>~,只有消息以这个前缀开头,接收方才会去尝试解包它之后的 Base64 内容。于是消息内容最终长这样:

<<<42>>>~elkAATA5xexIZWxsbywgMTIzIQ==

再考虑 QQ 机器人发送消息的频率不能无限高,实际上这个频率只能很低,否则很容易被封号,经过和相关机器人开发者的讨论,这个频率应该能够达到最高 10 msg/s 左右,但这只是发送频率,实际接收方接收消息的速度会更慢。

另外,QQ 消息有一个特点是,消息之间有可能乱序,甚至有可能丢失,但只要是收到的消息,几乎不可能出错,类比计算机网络中现有的 TCP、UDP 等协议的话,QDP 协议是不需要校验和的,因为只存在乱序和丢包,不存在内容出错。

由于这只是概念验证,于是不打算实现像 TCP 那样的可靠传输,实际上 QQ 消息在一定程度上已经非常可靠了,虽然说有可能出现乱序,但在网络条件比较好的测试环境应该还是可以忽略不计。

UDP 协议使用源 IP、源端口、目标 IP 和目标端口来确定数据报的发送和接收方,但 QQ 上没有 IP,因此这里使用源 QQ、源端口、目标 QQ 和目标端口来确定发送和接收方。于是,模仿 UDP 协议的首部,QDP 的首部需要包含源端口和目标端口。而 QDP 数据报最终发送时,需要经过类似于 IP 分片的处理,也就是把 QDP 数据报当做数据,切成多块之后再各自加首部,形成新的大小在 MTU 允许范围内的数据报,本来想把这一层称为「QIP」,但实现的时候发现没必要这么复杂,所以这个概念就不要了,这个切分之后的分片,我们称之为「QDP 分片」。如果模仿 IP 协议的话,「QDP 分片」的首部需要包含源 QQ、目标 QQ、数据报 ID(用于重组分片)、分片偏移,但这里我们进行简化,因为我们通过 QQ 收发消息,收发消息的时候都能够知道自己和对方的 QQ 号,不像真实的网络那么复杂,因此实际上分片中是不需要放源和目标 QQ 号的。而 IP 的分片偏移在实现上也似乎有点繁琐,因此 QDP 分片直接使用序号了。

根据上一段的思路,最终 QDP 数据报(Packet)和 QDP 分片(Fragment)的协议格式设计如下:

QDP Packet
+------------------+------------------+------------+
|      16 bit      |      16 bit      | var length |
|     Src Port     |     Dst Port     |    Data    |
+------------------+------------------+------------+

QDP Fragment
+------------------+-------+----------+------------+
|     16 bit       | 1 bit |  15 bit  | var length |
|    Packet ID     |  MF   | Sequence |    Data    |
+------------------+-------+----------+------------+

其中,QDP Fragment 的 Data 是整个 QDP Packet 的二进制切分之后的一部分,QDP Packet 的 Data 是实际要传输的二进制内容;Packet ID 是对应一个 QDP Packet 的随机数,用于接收方对可能有多个数据报混杂的分片进行重组;MF 标志位就和 IP 数据报的 MF 一个意思,More Fragment,表示当前分片是不是最后一个分片;Sequence 表示当前分片是原始 QDP Packet 的第几个分片,从 1 开始数。

有了协议格式之后,整体的收发流程就比较明确了。

发送方指定目标 QQ、目标端口和数据,QDP 实现程序将源端口、目标端口和数据放入 QDP Packet,按需进行切分,组成一个或若干个 QDP Fragment,然后依次将每个 QDP Fragment 的内容的二进制进行 Base64 编码,加上魔术前缀 <<<42>>>~,发送给目标 QQ。

接收方收到 QQ 消息,判断是否以魔术前缀开头,如果是,将前缀之后的内容进行 Base64 解码,恢复成 QDP Fragment,判断 MF 标志位,如果不是最后一个分片,暂存该分片,如果是最后一个,则根据 Sequence 进行排序重组,如果出现分片丢失的情况,直接丢弃该数据报。成功重组之后得到 QDP Packet,判断其目标端口是否为当前应用监听的端口,如果是,则放入 Socket 对应的接收队列,等待应用程序接收。

实现

上面思路的最后已经详细说明了协议的具体工作流程,因此实现起来就非常简单了,见 richardchien/qdp

上面给出的实现提供了如下接口(很类似于 Python 内置的 UDP Socket 接口):

qdp.init({
    12345678: 'ws://127.0.0.1:6700'
})

sock = qdp.Socket()
await sock.bind((12345678, 9999))
logging.info('QDP socket created')

while True:
    data, addr = await sock.recvfrom()
    text = data.decode('utf-8')
    logging.info(f'Received from {addr[0]}:{addr[1]}, data: {text}')
    text_send = f'Hello, {text}!'
    await sock.sendto(text_send.encode('utf-8'), addr)
    logging.info(f'Sent to {addr[0]}:{addr[1]}, data: {text_send}')

上面的代码给出了一段使用 QDP Socket 实现服务端的例子。qdp.init() 传入 QQ 号和 CQHTTP 的 WebSocket API 地址的映射;sock.bind((12345678, 9999)) 将 Socket 绑定到了 QQ 12345678 的 9999 端口;sock.recvfrom()sock.sendto() 就是非常正常的接收和发送接口了。

客户端部分的代码基本上和服务端类似,和 UDP 的区别是,即使是客户端,也必须要调用 sock.bind() 方法,来绑定到 QQ 号(因为要连接 CQHTTP),端口可以填 None,内部会从 50000 到 65535 随机选取一个可用端口。

另外,由于发送大量数据时需要分很多片,为了限制消息发送频率,顺便给 CQHTTP 写了一个 API 限速功能,可以通过配置文件限制请求的执行速度,测试时使用了 2 msg/s 和 5 msg/s 两种速度。

总结

以上就是 QDP 协议从理论上的概念到实现的过程了,实现之后简单进行了一下测速,发现每秒实际只能传输 2~3 KB 的数据,可以说非常慢了,基本上没啥用,不过作为概念验证已经算是成功了。

参考资料

QDP:一种以 QQ 消息为传输介质的通信协议

在笔记本上使用 Proxmox(或其它 Linux 服务器版)时关闭屏幕

创建于
分类:Ops
标签:LinuxProxmox

给吃灰许久的 12 年款 MacBook Pro 装了个 Proxmox 玩玩,发现启动后屏幕一直亮着,提示登录,搜了一圈找到 AskUbuntu 上有个同样的问题,这个答案 完美的解决了问题:

setterm --blank 1

这条命令的效果是,不操作一分钟后自动关闭屏幕。

另外,一直开着盖子,键盘上会落灰,盖上之后发现机子掉线了,也搜了一下,发现 这个答案 完美解决了问题:

修改 /etc/systemd/logind.conf 中的

#HandleLidSwitch=suspend

HandleLidSwitch=ignore

然后

service systemd-logind restart

之后合盖就不会睡眠了。

在笔记本上使用 Proxmox(或其它 Linux 服务器版)时关闭屏幕

使用 Nginx 反向代理 Google 和 Wikipedia

创建于
分类:Ops
标签:NginxGoogleWikipedia反向代理

对于程序员来说,能够通过一些途径上 Google、Wikipedia 等网站是非常必要的,但对于一些初学者来说,上这些网站还是有一些阻碍,因此尝试通过反代来让他们在内网中轻松访问这些网站。

准备

只需要有一台在墙外的 VPS,如果还有一个域名则更好。

安装/重新编译 Nginx

反代 Google 和 Wikipedia 需要 Nginx 安装有 ngx_http_substitutions_filter_module 模块,因此如果之前安装的 Nginx 在编译时没有添加这个模块,是需要重新编译的。

如果没有安装 Nginx,先使用发行版自带的包管理器安装一个 Nginx。

然后通过:

nginx -V

查看 Nginx 版本,例如 nginx version: nginx/1.10.3,然后下载相应版本的 Nginx 源码:

wget http://nginx.org/download/nginx-1.10.3.tar.gz
tar zxf nginx-1.10.3.tar.gz

然后下载 ngx_http_substitutions_filter_module 模块:

git clone https://github.com/yaoweibin/ngx_http_substitutions_filter_module

接着安装编译所必需的包(这里以 Ubuntu 为例):

apt-get update
apt-get install -y libpcre3 libpcre3-dev zlib1g zlib1g-dev openssl libssl-dev libxml2 libxml2-dev libxslt1-dev libgd-dev libgeoip-dev gcc g++ make automake

然后进入 Nginx 源码目录,利用 nginx -V 输出的 configure arguments,在最后加上 --add-module=../ngx_http_substitutions_filter_module 来运行 ./configure 并编译,例如(具体参数需要换成你系统中的):

cd nginx-1.10.3
./configure --with-cc-opt='-g -O2 -fPIE -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2' --with-ld-opt='-Wl,-Bsymbolic-functions -fPIE -pie -Wl,-z,relro -Wl,-z,now' --prefix=/usr/share/nginx --conf-path=/etc/nginx/nginx.conf --http-log-path=/var/log/nginx/access.log --error-log-path=/var/log/nginx/error.log --lock-path=/var/lock/nginx.lock --pid-path=/run/nginx.pid --http-client-body-temp-path=/var/lib/nginx/body --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-proxy-temp-path=/var/lib/nginx/proxy --http-scgi-temp-path=/var/lib/nginx/scgi --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --with-debug --with-pcre-jit --with-ipv6 --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-http_auth_request_module --with-http_addition_module --with-http_dav_module --with-http_geoip_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_image_filter_module --with-http_v2_module --with-http_sub_module --with-http_xslt_module --with-stream --with-stream_ssl_module --with-mail --with-mail_ssl_module --with-threads --add-module=../ngx_http_substitutions_filter_module
make
make install

然后将编译出来的二进制覆盖系统中已安装的:

cp -rf objs/nginx /usr/sbin/nginx

再重启 Nginx:

systemctl stop nginx
systemctl start nginx

使用 ZeroTier 组建虚拟网络

ZeroTier 用于组建一个虚拟的内网,安装和配置非常简单,见 ZeroTier

这一步其实不是必须的,但是在公网发布 Google 的反向代理是极其危险的行为,建议还是仅内部使用。

使用 acme.sh 签发 SSL 证书

如果需要通过域名访问反代站点,则需要先准备好 SSL 证书,现在 Let's Encrypt 已经提供免费的证书,可通过 acme.sh 比较简单地签发。

具体签发方法见 acme.sh 的 文档

配置反向代理

添加一个 Nginx 站点配置文件,并启用:

touch /etc/nginx/sites-available/google-wikipedia
ln -s /etc/nginx/sites-available/google-wikipedia /etc/nginx/sites-enabled/

然后编辑 /etc/nginx/sites-available/google-wikipedia,内容如下:

注意:

  • <google.domain><wikipedia.domain> 等需要改成你自己的域名,不包含 https://,如果你没有域名,而是打算直接使用 IP,则需要把下面配置中放在 443 端口中的配置移动到 80 端口配置中,并且无法使用 HTTPS

  • <ip> 需要改成你的 VPS 在 ZeroTier 网络中的 IP,如果你不使用 ZeroTier,则需要改成你需要监听的 IP,或直接连同端口前的冒号一起删除

upstream www.google.com {
    # 这里的 IP 是写此文时可用的 IP,具体需要使用 nslookup 来查看
    server 172.217.0.4:443 weight=1;
    server 172.217.1.36:443 weight=1;
    server 216.58.193.196:443 weight=1;
    server 216.58.194.196:443 weight=1;
    server 216.58.195.196:443 weight=1;
    server 216.58.216.4:443 weight=1;
    server 216.58.216.36:443 weight=1;
    server 216.58.219.36:443 weight=1;
    server 172.217.6.36:443 weight=1;
}

server {
    listen <ip>:80;
    server_name <google.domain> <wikipedia.domain> <m.wikipedia.domain> <upload.wikipedia.domain>;
    return 301 https://$host$request_uri;
}

server {
    listen <ip>:443 ssl;
    server_name <google.domain>;
    resolver 8.8.8.8;

    ssl on;
    ssl_certificate /root/.acme.sh/<google.domain>/fullchain.cer;
    ssl_certificate_key /root/.acme.sh/<google.domain>/<google.domain>.key;

    # 如果服务跑在公网,则可能需要解除下面的注释,以确保不会被搜索引擎收录,以及下面维基百科的 server 配置也需要添加下面这些
    #if ($http_user_agent ~* "qihoobot|Baiduspider|Googlebot|Googlebot-Mobile|Googlebot-Image|Mediapartners-Google|Adsbot-Google|Feedfetcher-Google|Yahoo! Slurp|Yahoo! Slurp China|YoudaoBot|Sosospider|Sogou spider|Sogou web spider|MSNBot|ia_archiver|Tomato Bot") {
    #    return 403;
    #}

    access_log  off;
    error_log   on;
    error_log  /var/log/nginx/google-proxy-error.log;

    location / {
        proxy_redirect off;
        proxy_cookie_domain google.com <google.domain>;
        proxy_pass https://www.google.com;
        proxy_connect_timeout 60s;
        proxy_read_timeout 5400s;
        proxy_send_timeout 5400s;

        proxy_set_header Host "www.google.com";
        proxy_set_header User-Agent $http_user_agent;
        proxy_set_header Referer https://www.google.com;
        proxy_set_header Accept-Encoding "";
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header Accept-Language "zh-CN";
        proxy_set_header Cookie "PREF=ID=047808f19f6de346:U=0f62f33dd8549d11:FF=2:LD=en-US:NW=1:TM=1325338577:LM=1332142444:GM=1:SG=2:S=rE0SyJh2W1IQ-Maw";

        subs_filter https://www.google.com.hk https://<google.domain>;
        subs_filter https://www.google.com https://<google.domain>;
        subs_filter //zh.wikipedia.org //<wikipedia.domain>;

        sub_filter_once off;
    }
}

server {
    listen <ip>:443 ssl;
    server_name <upload.wikipedia.domain>;

    ssl on;
    ssl_certificate /root/.acme.sh/<wikipedia.domain>/fullchain.cer;
    ssl_certificate_key /root/.acme.sh/<wikipedia.domain>/<wikipedia.domain>.key;

    access_log  off;
    error_log   on;
    error_log  /var/log/nginx/wikipedia-proxy-error.log;

    location / {
        proxy_pass https://upload.wikimedia.org;
    }
}

server {
    listen <ip>:443 ssl;
    server_name <wikipedia.domain>;

    ssl on;
    ssl_certificate /root/.acme.sh/<wikipedia.domain>/fullchain.cer;
    ssl_certificate_key /root/.acme.sh/<wikipedia.domain>/<wikipedia.domain>.key;

    access_log  off;
    error_log   on;
    error_log  /var/log/nginx/wikipedia-proxy-error.log;

    location / {
        proxy_cookie_domain zh.wikipedia.org <wikipedia.domain>;
        proxy_pass https://zh.wikipedia.org;
        proxy_redirect https://zh.wikipedia.org/ https://<wikipedia.domain>/;
        proxy_redirect https://zh.m.wikipedia.org/ https://<m.wikipedia.domain>/;
        proxy_connect_timeout 60s;
        proxy_read_timeout 5400s;
        proxy_send_timeout 5400s;

        proxy_set_header Host "zh.wikipedia.org";
        proxy_set_header User-Agent $http_user_agent;
        proxy_set_header Referer https://zh.wikipedia.org;
        proxy_set_header Accept-Encoding "";
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header Accept-Language "zh-CN";

        subs_filter_types text/css text/html text/xml text/javascript application/javascript application/json;
        subs_filter //zh.wikipedia.org //<wikipedia.domain>;
        subs_filter upload.wikimedia.org <upload.wikipedia.domain>;
    }
}

上面示例中并没有配置 m.wikipedia.domain,实际上和 wikipedia.domain 是几乎一样的。

配置完成后通过 nginx -t 检查是否有错误,如果没错,通过 systemctl restart nginx 重启 Nginx 即可生效。

参考资料

使用 Nginx 反向代理 Google 和 Wikipedia

VeriPress 主题集合

创建于
分类:Misc
标签:VeriPressBlogThemeCollection

目前 VeriPress 已经有了好几个官方和第三方主题,为了便于查找,这里做一个集合列表。

主题名 预览/截图 简介
default Preview 为博客设计的默认主题
clean-doc Preview 为文档设计的默认主题
fenki - 为 Wiki 设计的默认主题
richardchien/veripress-theme-r - 一个简洁的纯色博客主题
richardchien/veripress-theme-light Preview 一个风格小清新的博客主题,样式比较轻快
txperl/Story-for-VeriPress Preview 一个适合写作与阅读的博客主题
momocow/veripress-theme-suka Preview Modern, powerful and simple theme ported from Hexo.

以上的主题均可以直接通过:

$ veripress theme install 主题名

来安装,并可通过:

$ veripress theme update 主题名

来更新。

VeriPress 主题集合

使用位置模拟器进行步道乐跑的跑步打卡

创建于 更新于
分类:Misc

这学期学校突然非常贴 zhi 心 zhang 地引入了「步道乐跑」这一软件来完成大学生阳光体育的政策要求(以前是去体育场刷卡),简单了解了一下发现这个软件非常臭名昭著——随机打卡点、只使用 GPS 定位导致路线记录不准、对较新版本 Android 系统兼容性不好等。

对于这种强迫我们在危险的大马路上跑步、还经常需要停下来低头看手机的黑恶势力软件,乖乖听话是不可能的,于是花了点时间研究了一下另类方法。

准备

首先你需要准备一部已经 root 的 Android 手机,具体怎么 root 这里就不说了。

注意,如果你没有接触过 root,那么需要小心谨慎地详细了解一下,因为 root 操作比较危险,会让手机变得不安全,尽量在备用机上操作。

我一开始尝试了在夜神模拟器里面运行,结果步道乐跑会检测蓝牙状态,夜神模拟器不支持蓝牙,于是放弃了,不过 Genymotion 这种是有可能可以的,因为它也模拟了蓝牙等传感器。

目前发现前几天还可以用的情况下,某天乐跑突然又说检测到模拟器了,推测是通过热更新增加了对 root 的检测,换用 Magisk 进行 root 之后,通过 Magisk Hide 功能对乐跑隐藏了 root,似乎解决了,不过并不十分确定是因为检测 root。

安装 Xposed

这里 帖子底下的附件 XposedInstaller_3.1.5.apk(实际版本号可能不同)链接下载 Xposed Installer,然后安装。

打开之后会看到如下红色感叹号:

红色下面是下图:

点「Version 89」(具体版本可能不同),然后弹窗中选「Install」即可安装,安装之后需要重启手机。

完成之后红色感叹号会变成绿色的对号:

此时即为安装成功。

安装大牛助手(位置模拟器)

大牛助手是一款利用 root 权限模拟定位的软件,目前最新版是收费的,但旧的 1.1.7 版本是免费的,虽然它强制更新,但可以用 Xposed 模块来强行关闭弹窗。

1.1.7 版本可以从 这里 下载。

安装之后别急着打开,开了也千万不要更新,直接退出。

然后下载安装 对话框取消 Xposed 模块,安装之后需要在 Xposed Installer 的「模块」中勾选,然后重启手机。

重启之后打开对话框取消模块,勾选「开启模块」和「开启增强模式」,然后在下面的应用列表找到大牛助手,进行如下配置:

然后点保存并重启,之后打开大牛助手应该就不会出现强制升级的弹窗了。

运动模拟器

步道乐跑最让人诟病的就是它只通过 GPS 来确定跑步路线和距离,而没有通过陀螺仪等运动传感器来辅助判断,也正因为这一点,不需要开运动模拟器也可以模拟跑步,不过,为了保险起见,目前步道乐跑后台应该是会检查客户端上传的数据中的运动步数,过低会判断为代步工具嫌疑,这个可以通过安装 Xposed 模块 运动模拟器 来模拟运动步数,具体方法这里就不详述了,和「对话框取消」的安装方法基本上一样。

安装步道乐跑

就在应用商店正常安装即可,不过我安装的是一个旧版本,并不确定新版本中下面的方法是否还有效。

旧版本下载地址:链接: https://pan.baidu.com/s/1GMiGWocpXEJqX8yfypRIIA 提取码: abib

如果你安装的是旧版本,那么需要像上一步那样对步道乐跑也启用对话框取消,否则它也会弹窗强制更新。

防止步道乐跑检测到位置模拟器

步道乐跑会「检测」手机中是否有运动模拟器,经过一番研究,发现实际上它是通过获取手机的应用列表来判断的,当发现 Xposed Installer、大牛等应用,就会提示你要诚信跑步。

好在大牛助手有 app 防检测功能,进入大牛的 app 防检测,添加大牛和 Xposed Installer 两个应用,选择防止步道乐跑检测,即可。

模拟跑步

具体模拟跑步时,首先进入步道乐跑,然后点进跑步界面,当出现地图之后,就会出现第一个打卡点。

然后打开大牛,使用「路线模拟」功能添加两个点,第一个点是你的当前位置(或任意学校内的位置),第二个点是步道乐跑的打卡点,然后在「配置」页面调整速度,根据步道乐跑要求的平均配速(3~9 分钟 1 km),设置速度为 10 km/h 左右,然后保存并开启路线模拟。

这时候大牛就已经开始模拟位置的变化了,切换到步道乐跑,点开始跑步,然后等它打卡成功,之后出现下一个点,回到大牛,停止路线模拟,删掉第一个点,再添加新的打卡点,然后再次开启模拟,再切回步道乐跑。

第三个打卡点以此类推,三个点全部打完就可以结束跑步了。为了避免在大牛那边的操作耗时导致配速不符合要求,可以适当提高大牛的模拟路线的速度,或者在切换到大牛之前先暂停跑步。

最后

其实步道乐跑这软件,或者说大学生阳光体育运动,它们本身的动机是好的,现在大学生天天宅在宿舍里确实需要运动,但这样强制通过跑步的方式来运动,为了避免作弊还一定要在校园内随机打卡地点,还花了很多精力去检查系统环境、检查模拟器,就显得有些病态了,搞得本来就喜欢运动的人也需要百般折腾。

虽然这里给出了破解方法,但还是建议抽出一些时间去真实地做一些运动,毕竟身体健康还是很重要的。

使用位置模拟器进行步道乐跑的跑步打卡