背景
好消息: 我校数据结构课程有 OJ 辣!
坏消息: OJ 搭在校内, 然而现在疫情在上网课 :(
要想访问校内网, 最正常的方法是启动学校发的 VPN, 直接回内网. 然而我校使用的 VPN 是臭名昭著的深信服, 因而我并不愿意让它安装我的 Windows 上. 幸好, 有大佬制作了深信服 VPN 的 Docker 版. 但是不知道为什么, 这个 Docker 版跟 WSL 不是很搭, 不管怎么调都是登录成功, 访问失败. 按常理来讲, 我那时应该开一个虚拟机, 然后在虚拟机上跑这个的. 但是那时候突发奇想, 能不能换种思路, 我不回校园网, 让服务走出校园网?
事实证明, 这是一个大坑, 这个想法 ruin 了我一整个上午, 人都炸毛了. 自疫情开学以来我就没有这么愤怒过 (无能狂怒.jpg). 不过在祭献了一个上午之后, 下午的配置过程出奇地顺利, 最后我还是成功配好了一套看起来非常扭曲的服务.
总结一下需求: 在校外访问 ak.origami404.top
就能访问到校内的 10.1.1.1
上跑着的服务, 并且既能收包也能发包. 此服务为一前后端分离的 Web 项目, 前端运行在 9000
端口, 后端运行在 9001
端口. 我拥有域名所对应的服务器的 root 权限, 且拥有域名所有权; 但此校内服务非我所有. 学校提供深信服的 VPN.
出于安全考虑, 上面提到的所有地址与端口信息都不是真实的, 但是场景是真的.
那么开始折腾吧!
问题一: 如何使用 VPN?
正如上文提到的, 我校使用深信服 VPN. 根据 docker 版深信服的配置说明, 我首先写出了下面的 docker-compose.yml
:
version: '3'
services:
easyconn:
image: hagb/docker-easyconnect:cli
restart: always
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
environment:
- EC_VER=7.6.3
- CLI_OPTS="-d <vpnaddress> -u <username> -p <password>"
然后就死活登录不上了…
经过一阵疑神疑鬼的检查(包括进容器里查 iptables), 最后才怀疑到是我的密码里包含的特殊字符可能被转义了. 因为我其实不太会 yml
的语法, 我觉得我是配不好这个 yml+bash 的多重转义了. 所幸天无绝人之路, 从学长的口中我知道了深信服 VPN 会直接把地址/用户名/密码等信息全部保存到一个文件里供下次使用, 所以可以直接在终端运行手动输入密码, 然后将文件挂载出来保存, 然后再拿保存起来的文件挂载进正式的环境里用.
具体方法是:
$ touch easyconn
$ docker run --device /dev/net/tun --cap-add NET_ADMIN -ti -v "$PWD/easyconn:/root/.easyconn" -e EC_VER=7.6.3 hagb/docker-easyconnect:cli
在挂载之前, 首先要创建起这个文件. 因为 Docker 的挂载在本地和容器内的该路径都不存在的情况下, 会选择在本地这个路径创建一个 文件夹, 然后把文件夹挂进去. 这样当容器里的 easy-connect 想创建配置文件的时候就创建不出来了. 所以要先在本机这个路径创建出一个文件, 然后 Docker 才会把这个路径当成文件挂载进去.
接下来容器应该会询问你的 VPN 地址, 用户名跟密码. 输入后看见 login successfully
就可以直接停止容器了. 随后可以使用下面的 docker-compose 启动一个长期跑的容器:
version: '3'
services:
easyconn:
image: hagb/docker-easyconnect:cli
restart: always
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
environment:
- EC_VER=7.6.3
volumes:
- ./easyconn:/root/.easyconn
问题二: 如何让反向代理的上游经过 socks5 代理?
这个问题可难搜了. 因为 reverse proxy
跟 socks/http proxy
这两个关键字天然犯冲, 直接去搜多半是搜到一堆跟你讲正向代理与反向代理区别的文章. 在经过一阵挣扎疯狂替换搜索词搜索后, 终于被我找到了一个 GitHub Repo: rsocks. 这个 python 写的小脚本可以作为一个反向代理, 并且经过某个 socks5 代理再连接上游.
问题三: 如何处理写死的 baseUrl
?
当我兴冲冲地打开 ak.origami404.top
, 看到登录界面的时候, 我那叫一个激动啊. 然而当我尝试登录的时候, 发现根本登录不上去…
一开 F12, 发现这个 OJ 项目是一个前后端分离的项目, 在传过来的前端 JS 文件里, 写死了一个后端地址: http://10.1.1.1:9001
, 这下麻烦了.
一开始, 我想找找有没有能够拦截并修改收到的 HTML/JS 文件内容的浏览器扩展, 我倒是找到了一个叫 GooReplacer 的, 但是看起来不大好用. 于是我就开始折腾 MITM 了. 等到晚上我水群的时候, 群友给我发了一个 JsProxy. 这个跑在浏览器里, 利用 ServiceWorker 的玩意看起来比我配的服务器端 MITM 靠谱多了, 但是由于我已经配好了, 也没有去尝试这个. 如果读者有兴趣的话可以尝试一下.
那么, MITM 是什么呢? MITM, Man-In-The-Middle, 中间人攻击, 是一种常见的攻击手段, 详见维基百科. 在这里我选择了 mitmproxy 作为一个 “中间人服务器”. 选它的主要原因是其支持 Python 脚本, 并且可以对外作为一个反向代理服务器使用.
首先来写一个 mitmproxy addon:
import mitmproxy.http
from mitmproxy import ctx
# 辅助变量: 需要修改的 HTTP 响应的 MIME 类型
target_mime = {
'text/html', 'text/javascript',
'text/css', 'application/json',
'application/javascript',
}
# 辅助变量: 需要替换的字符串
replacements = {
b'http://10.1.1.1:9000': b'https://ak.origami404.top',
b'http://10.1.1.1:9001': b'https://ak.origami404.top/api',
}
# 辅助函数: 根据 MIME 头判断是否应该修改这个响应
def is_proper_mime(ct: str) -> bool:
# 有些框架的 MIME 类型会类似这样: "text/css; charset=utf-8"
# 所以需要做下面的处理
return ct.split(';')[0] in target_mime
# Addon 的主体部分, 定义了当发生特点事件时动作
class Interceptor:
# 当 mitmproxy 拦截到一个响应时
def response(self, flow: mitmproxy.http.HTTPFlow):
# 获得响应本身
response = flow.response
if not response:
return
# 根据响应的 MIME 头先筛选一次, 防止脚本对很多 jpg/png 等二进制文件也做替换
if ct := response.headers['Content-Type']:
if is_proper_mime(ct) and response.content:
# 依次修改想要修改的字符串
for old, new in replacements.items():
response.content = response.content.replace(old, new)
# 注册 Interceptor
addons = [ Interceptor() ]
将脚本保存为 run.py
并且通过环境变量 UPSTREAM
指定上游服务器, 随后使用命令即可运行这个脚本. 这样做的主要是为了方便将这个 mitmproxy 做成 Docker 镜像.
mitmdump --mode "reverse:${UPSTREAM}" -s /run.py
HTTPS 与重写 URL
既然是要把校内的服务暴露到公网, 怎么着都得开一个 HTTPS 吧. 并且由于前后端跨域问题, 我选择手动把 https://ak.origami404.top/api/*
重写到 http://10.1.1.1:9001/*
, 其他的请求都直接发送到 http://10.1.1.1:9000
. 由于之前我的服务器上就跑着一个 Caddy-docker-proxy, 所以我只需要在各个容器的 labels
里写上对应的规则就行了.
加起来
最终, 我的全套流程大概如下:
+-----------+ +-----------+ +------------------+ +-----------+
| easy | | rsocks | |mitmproxy-backend | | Caddy |
| connect | | | | | | |
| | | 10001+--->| 8080+--->| provide |
| | | | | change | | HTTPS |
| | | | | baseUrl | | and |
| | | reverse | +------------------+ | rewrite |
| | | proxy | | URL |
| | | though | | |
| 1080+--->| socks5 | | 80|
| | | proxy | | 443|
| | | | +------------------+ | |
| | | | |mitmproxy-frontend| | |
| | | | | | | |
| | | 10002+--->| 8080+--->| |
|VPN->socks5| | | | change | | |
| | | | | baseUrl | | |
+-----------+ +-----------+ +------------------+ +-----------+
具体的配置文件详见这个仓库: WiredSetup.
使用体验
意外地还行. 我其实对这一套配置没有一点信心, 但出乎我意料的是, 它运行得很好 – 除了偶尔会断连之外(虽然我觉得断连应该都是深信服的问题). 学校内网里的大部分服务都是 HTTP (毕竟内网里申不到 HTTPS 证书), 所以有了这套设施, 可以把学校里绝大部分服务都搬到外网来了. 我也可以安心等学校安全中心给我打电话了(大雾).