需求

安全提示

最近不知道又是哪位老师被诈骗了,信息办又来了一次风险提示。犹记自从更新了统一身份认证后,第一次启用加强认证,是某位领导被盗号,给院里的老师发送了诈骗邮件。这还是形策老师分享的信息。不过加强认证似乎在寒暑假和法定节假日一直是开启的。

这次似乎更加严峻,因为访问信息系统都需要 VPN。什么是信息系统呢?指的是 1 系统吗?这样不仅登录需要加强认证,而且还需要在校内(或者连接 VPN)才能访问到 Web 页面。

那可不太妙。

因为我现在的各种自动化脚本(排课助手通知公告备份)都只解决了统一身份认证的「加强认证」问题,本质上还是在校外(更准确些,国外)访问的 1 系统。如果必须连接 VPN 才能访问 1 系统,那我的脚本便失效了。

因此,本文要解决的问题是,在服务器中如何通过 MotionPro 连接校园 VPN。

问题分析

在解决问题之前,先对问题进行深入的分析,补充一些背景知识。

在互联网中,我们经常输入类似于 1.tongji.edu.cn 这样的域名来获得 Web 服务。域名对人类很友好,方便记忆。但是网络中的路由器、交换机等设备不认识域名,它们更喜欢数字。于是,就需要通过 DNS 服务,把域名转变为 IP 地址。这样交换机才知道该如何对包进行转发。

你可能会想,点分十进制对机器好像也不是很友好。嗯,是这样的。点分十进制只是一种表示方式,还是方便人来阅读。实际上机器内部的表示方式是 32 位的二进制数。

DNS 服务是由服务器提供的,许多公司和组织都提供 DNS 服务,比如 Google 的 8.8.8.8 等等。网络是一个层次结构,DNS 也是如此。.cn 这个顶级域可以提供其范围内域名的 DNS 服务;而小到一个公司、学校,它们也有自己的 DNS 服务器。

举个例子,.cn 的 DNS 服务器可以提供 example.cn4399.cnedu.cn 这样域名的 DNS 查询服务;而同济的 DNS 服务器可以提供 1.tongji.edu.cnwww.tongji.edu.cn 这样域名的 DNS 服务。是否注意到了层次结构?域名和英文的习惯是一样的,更大范围的标识符在后。

现在我们清楚了,输入一个域名,只有通过 DNS 服务转换为 IP 才能够访问。

之前,1 系统的服务器具有公网 IP,而且这个 IP 和 1.tongji.edu.cn 的对应关系也公开为人所知。现在的情况是,服务器的公网 IP 禁用了,或者学校的 DNS 服务器不再宣传 1.tongji.edu.cn 的 DNS 记录。

这就导致问题的出现,因为只有同济才知道 1.tongji.edu.cn 到底对应什么 IP,这是学校内部的结构,只有学校自己清楚。如果它不告诉外界,那外界就也不知道该如何访问到该域名对应的 IP 了。换言之,外网无法访问到 1 系统的服务。

你可能会想,既然它曾经公开过,一传十、十传百,那大家就知道这一对应关系。合理,但事实上,DNS 是有有效期的,过了有效期,曾经公开的记录便失效了。而消息源又不再宣传这一消息,久而久之,大家就都忘了某个 DNS 记录了。

所以,这串分析告诉我们,为什么外网会无法访问 1 系统的服务。因为不知道域名和 IP 的对应关系。

那,为什么内网可以呢?因为同济内网也有 DNS 服务器呀,当你在校园网环境下,不妨用 nslookup 看一下,默认的 DNS 服务器是同济自己的喔。而同济自己的 DNS 服务器会把 1.tongji.edu.cn 映射为一个内网 IP。这样在内网就能够访问 1 系统的服务了。

换言之,就算你在校园网可能访问不通外网,如百度、Github 这些网站,但内网的服务是可以访问的。这就好比你家宽带欠费了,访问不了百度,但不影响连接路由器的管理员界面(192.168.1.1)、打印机的 Web 接口,或者摄像头等内网设备。

这很合理呀!因为毕竟用户所需的服务就在内网里,干嘛大费周章地绕着外网走一圈呢?直接在内网解决就好了。

1
2
3
4
5
6
7
8
9
10
11
C:\Users\summe>nslookup
默认服务器: dnscache1.tongji.edu.cn
Address: 202.120.190.208

> 1.tongji.edu.cn
服务器: dnscache1.tongji.edu.cn
Address: 202.120.190.208

非权威应答:
名称: 1.tongji.edu.cn
Address: 192.168.130.211

问题解决

设备信息

服务器是腾讯云的一台轻量应用服务器。版本是 Ubuntu 24.04 LTS

软件安装

同济 VPN 的支持软件是 MotionPro,我不太清楚其他的 VPN 软件能否连接上。

在官网下载对应版本的 .sh 文件后,直接运行 sudo ./MotionPro_Linux_Ubuntu_x64.sh 安装软件。

VPN 连接

运行 MotionPro --help 前,需要先启用 vpnd,直接 sudo vpnd。之后运行帮助命令,打印的帮助信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ubuntu@VM-4-11-ubuntu:~$ MotionPro --help
Usage: /opt/MotionPro/vpn_cmdline
-s, --stop stop vpn
-h, --host AG host (Required). If no port is given, the default port is 443. If referencing the shared virtual site, please add the alias name at the end. (i.e.hostname:port / alias)
-u, --user username for login (Required)
-p, --passwd password for login (Required)
-k, --cookie connect to vpn with session cookie
-m, --method aaa method for login; if there is only one method, this will choose the default method
-c, --count Reconnect count, "inf" means reconnect all the time (Default: 0, means: no reconnect)
-i, --interval interval between l3vpn reconnect attempts (Default: 0 sec, means: immediately; max: 3600 sec)
-e, --enable enable proxy
-t, --proxyhost Proxy host (Required if proxy enabled), if no port is given the default port is 0.(i.e. post:port)
-n, --proxyuser Proxy username (Required if proxy enabled), if the user wants to assign a domain, please add the domain before the username. (i.e.domain\username)
-w, --proxypwd Proxy password (Required if proxy enabled)
-a, --status display vpn status
-x, --validcode connect to vpn with session validcode
-f, --certfile certificate file path for login (pfx format)
-d, --certpass certificate file password
-v, --capath ca path, to verify server cert
-l, --loglevel log level: debug|info|warn|error
-q, --quiet quiet mode
-g, --syslog write log to syslog
-b, --bandwidth download spped limit,upload speed limit(unit is kbps)
-j, --pid browser pid
-z, --break disconnect vpn but do not logout

这样看来,连接到学校 VPN 最简单的方式就是 MotionPro -h vpn.tongji.cn -u 2365472 -p baka。测试一下,确实成功了,但问题是这会导致当前的 SSH 连接断开。

为什么会这样呢?因为启用 VPN 后,默认是全局模式,接管所有流量。SSH 的流量也走 VPN,但 VPN 服务的提供者不知道该怎样把流量转发到 PC,于是连接便断开了,只能通过重启服务器恢复连接。

将本机 IP 写入路由表

一个很基本的想法是,把本机 IP 写入路由表,这样服务器就知道该通过原来的路径连接 PC,而不是走 VPN。理想很丰满,实际上,VPN 会覆盖路由表,导致写入的记录被覆盖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
default via 10.3.4.1 dev eth0 proto dhcp src 10.3.4.11 metric 100
10.3.4.0/22 dev eth0 proto kernel scope link src 10.3.4.11 metric 100
10.3.4.1 dev eth0 proto dhcp scope link src 10.3.4.11 metric 100
111.187.0.0/16 via 10.3.4.11 dev eth0 # 本机 IP,手动添加
183.60.82.98 via 10.3.4.1 dev eth0 proto dhcp src 10.3.4.11 metric 100
183.60.83.19 via 10.3.4.1 dev eth0 proto dhcp src 10.3.4.11 metric 100

# 启动 VPN 前(上)启动 VPN 后(下)

1.1.1.1 dev tun0 proto kernel scope link src 111.187.78.177
10.3.4.1 dev eth0 scope link
202.120.189.6 via 10.3.4.1 dev eth0
202.120.189.23 via 10.3.4.1 dev eth0
202.120.189.34 via 10.3.4.1 dev eth0

这样看,思路应该是在 VPN 启用后,恢复原来的路由表,但同时让该走 VPN 的流量走 VPN。这一思路是正确的,但还有一个问题没有解决。

禁用 systemd-resolved 服务

现在面临的问题是,能 ping 通 IP,但是 ping 不通域名。这就是 DNS 服务器的问题了。为什么会这样呢?GPT 认为是系统的 systemd-resolved 和 VPN 的解析服务冲突了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 这是 /etc/resolve.conf
nameserver 202.120.190.208
nameserver 202.120.190.108
# This is /run/systemd/resolve/stub-resolv.conf managed by man:systemd-resolved(8).
# Do not edit.
#
# This file might be symlinked as /etc/resolv.conf. If you're looking at
# /etc/resolv.conf and seeing this text, you have followed the symlink.
#
# This is a dynamic resolv.conf file for connecting local clients to the
# internal DNS stub resolver of systemd-resolved. This file lists all
# configured search domains.
#
# Run "resolvectl status" to see details about the uplink DNS servers
# currently in use.
#
# Third party programs should typically not access this file directly, but only
# through the symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a
# different way, replace this symlink by a static file or a different symlink.
#
# See man:systemd-resolved.service(8) for details about the supported modes of
# operation for /etc/resolv.conf.

nameserver 127.0.0.53
options edns0 trust-ad
search .
1
2
3
4
5
这说明 Ubuntu 的 systemd-resolved 仍在生效,它是一个本地 stub resolver(DNS 代理),拦截了 DNS 请求但没能转发到真正的上游 DNS(被 VPN 改掉或屏蔽了)。

因为 MotionPro 在命令行模式下(非 GUI)一般不会修改 systemd-resolved 的 upstream 配置,只会尝试修改 /etc/resolv.conf。
而 Ubuntu 默认 /etc/resolv.conf 是个符号链接(symlink)→ /run/systemd/resolve/stub-resolv.conf。
所以 MotionPro 写入的 DNS 被 systemd-resolved 吃掉、忽略。

这里我确实不是做出很好的解释,但总而言之,需要在启用 VPN 的 DNS 服务后需要关闭 systemd-resolved

将 DNS 服务器添加到路由表中

即使这样,还是不能解决问题。DNS 解析仍然出错。这次不是多个 DNS 冲突了,而是学校的 DNS 服务器连不上。考察路由表:

1
2
3
4
5
6
7
8
9
10
# 不完全正确的路由表
default via 10.3.4.1 dev eth0
1.1.1.1 dev tun0 proto kernel scope link src 111.187.82.242
10.0.0.0/8 dev tun0 scope link
10.3.4.1 dev eth0 scope link
172.16.0.0/12 dev tun0 scope link
192.168.0.0/16 dev tun0 scope link
202.120.189.6 via 10.3.4.1 dev eth0
202.120.189.23 via 10.3.4.1 dev eth0
202.120.189.34 via 10.3.4.1 dev eth0

学校 DNS 服务器并没有专属的一条记录,走默认网关 eth0,但问题是学校的 DNS 服务器可能设置了只接受内网请求。那,走公网的 eth0 是无法访问到的。需要添加如下记录:

1
2
202.120.190.108 dev tun0 scope link 
202.120.190.208 dev tun0 scope link

你可能奇怪,为什么启用 VPN 就能找到 DNS 服务器呢?观察下表,启用 VPN 后,默认路由被删掉了。在这种条件下,一般来说,隐含的默认路由是 VPN,这样,发往 DNS 服务器的请求也走 VPN,一切正常。

1
2
3
4
5
6
# 启用 VPN 后的路由表
1.1.1.1 dev tun0 proto kernel scope link src 111.187.78.177
10.3.4.1 dev eth0 scope link
202.120.189.6 via 10.3.4.1 dev eth0
202.120.189.23 via 10.3.4.1 dev eth0
202.120.189.34 via 10.3.4.1 dev eth0

加强认证

下一个要解决的问题是加强认证。在 2025 年 10 月 9 日,在校外连接 VPN 确实启用了短信验证(不可邮箱验证)。而且在 Linux 的命令行界面下也是如此。启动 MotionPro 后,会唤起一行输入提示,请求用户输入手机接收到的验证码。

这个解决方案也比较简单,只需要在脚本中设置一个判断字段。如果 VPN 提示输入验证码,则请求用户输入验证码;否则直接连接。

1
2
3
ubuntu@VM-4-11-ubuntu:~$ sudo MotionPro -u 2365472 -p baka -h vpn.tongji.cn
验证码已经发送至123*****456
Verification Code:

只不过有个需要注意的坑是,使用 expect 来接收用户输入时,不能使用 expect_user 来接收用户输入,因为似乎在脚本中使用 EOF 会改变 expect 的标准输入流位置,因此必须使用 tty 来接收输入。

你可能会说,夏凌夏凌,这样好麻烦!为嘛要用一个脚本来实现?我直接在命令行启动 VPN 不就好了。有时候走太远了就忘了初心,因为直接启动 VPN 会导致 SSH 连接断开呀。

完整脚本

启用 VPN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#!/bin/bash
set -e

# === Step 1. 保存当前默认网关和出口网卡 ===
GW=$(ip route | grep default | awk '{print $3}')
IFACE=$(ip route | grep default | awk '{print $5}')
echo "[INFO] 当前默认网关: $GW"
echo "[INFO] 当前出口网卡: $IFACE"

# === Step 2. 启动 VPN ===
echo "[INFO] 启动 MotionPro VPN..."
if pgrep -x "vpnd" > /dev/null; then
echo "[INFO] vpnd 已在运行中,跳过启动。"
else
echo "[INFO] 启动 vpnd..."
sudo vpnd
sleep 2
fi

echo "[INFO] 启动 MotionPro 客户端并处理可能的短信验证码..."

# 使用 expect 处理交互式输入
sudo apt-get install -y expect >/dev/null 2>&1 || true

echo "[INFO] 启动 MotionPro 客户端(以 root 权限)..."
expect <<'EOF'
spawn sudo MotionPro -h vpn.tongji.cn -u 2365472 -p baka
set timeout 30

proc prompt_read {prompt} {
# open the controlling tty for read/write
set chan [open /dev/tty {RDWR}]
# print prompt without newline
puts -nonewline $chan $prompt
flush $chan

# disable echo on the tty while reading (so typed code is not shown)
exec sh -c {stty -echo < /dev/tty}
gets $chan line
exec sh -c {stty echo < /dev/tty}
puts $chan "" ;# newline after user typed (clean output)
close $chan
return $line
}

expect {
"验证码已经发送至" {
send_user "\n\[INFO\] 检测到需要短信验证码认证。\n"
expect "Verification Code:"
set code [prompt_read "\[ACTION\]请输入验证码:"]
send "$code\r"
exp_continue
}
"login successfully" {
send_user "\n\[SUCCESS\] VPN 登录成功。\n"
timeout {
send_user "\n\[ERROR\] 登录超时,请检查网络或账号信息。\n"
}
}
EOF

# 等待 VPN 建立
sleep 5

# === Step 3 修复 DNS 解析问题 ===
echo "[INFO] 修复 DNS 解析配置..."
# 关闭 systemd-resolved 以避免与 MotionPro 冲突
sudo systemctl stop systemd-resolved 2>/dev/null
sudo systemctl disable systemd-resolved 2>/dev/null

VPN_DNS=$(resolvectl dns tun0 2>/dev/null | awk '{print $3}' | tr '\n' ' ')
if [ -z "$VPN_DNS" ]; then
# 若获取失败则回退为同济默认 DNS
VPN_DNS="202.120.190.208 202.120.190.108"
fi

sudo unlink /etc/resolv.conf 2>/dev/null
for dns in $VPN_DNS; do
echo "nameserver $dns" | sudo tee -a /etc/resolv.conf >/dev/null
done

echo "[INFO] DNS 修复完成,当前配置如下:"
cat /etc/resolv.conf

# === Step 4. 打印路由表用于调试 ===
echo "[DEBUG] 当前路由表(VPN启动后):"
ip route

# === Step 5. 恢复公网默认路由 ===
echo "[INFO] 恢复公网默认路由..."
sudo ip route del default || true
sudo ip route add default via $GW dev $IFACE

# === Step 6. 为校内常见网段添加路由到VPN ===
echo "[INFO] 添加内网路由到VPN(假设 VPN 接口为 tun0 或 ppp0)..."
VPN_IFACE=$(ip link show | grep -E "tun|ppp" | awk -F: '{print $2}' | head -n 1 | tr -d ' ')
if [ -z "$VPN_IFACE" ]; then
echo "[WARN] 未检测到 VPN 接口,请检查 MotionPro 状态。"
else
sudo ip route add 10.0.0.0/8 dev $VPN_IFACE || true
sudo ip route add 172.16.0.0/12 dev $VPN_IFACE || true
sudo ip route add 192.168.0.0/16 dev $VPN_IFACE || true

# 把 VPN DNS IP 单独路由到 VPN 接口
for dns in $VPN_DNS; do
sudo ip route add $dns/32 dev $VPN_IFACE || true
done

echo "[INFO] 内网路由已配置到 $VPN_IFACE"
echo "[INFO] VPN DNS 路由已添加"
fi

# === Step 7. 输出最终路由表 ===
echo "[INFO] === 最终路由表 ==="
ip route

echo "[SUCCESS] VPN 启动完成,SSH 路由已保留。"
wait $VPN_PID

终止 VPN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/bin/bash
# ==========================================
# MotionPro VPN 停止脚本(恢复网络环境)
# ==========================================

echo "[INFO] 尝试停止 MotionPro VPN 连接..."

# 1 停止 MotionPro VPN 客户端
sudo MotionPro --stop 2>/dev/null && echo "[OK] MotionPro 已停止。" || echo "[WARN] MotionPro 未在运行或已关闭。"

# 2 停止 vpnd 后台服务(如果存在)
if pgrep vpnd >/dev/null 2>&1; then
echo "[INFO] 检测到 vpnd 正在运行,尝试停止..."
sudo pkill vpnd && echo "[OK] vpnd 已停止。"
else
echo "[INFO] vpnd 未运行。"
fi

# 3 恢复默认系统 DNS 设置
echo "[INFO] 恢复系统默认 DNS 配置..."
sudo rm -f /etc/resolv.conf
sudo ln -s /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
sudo systemctl restart systemd-resolved
echo "[OK] DNS 已恢复为 systemd-resolved 管理模式。"

# 4 删除 VPN 残留路由(可选)
# tun0 / ppp0 都可能是 VPN 设备,逐一清理
for dev in tun0 ppp0; do
if ip link show $dev >/dev/null 2>&1; then
echo "[INFO] 删除 VPN 设备相关路由 ($dev)..."
sudo ip route flush dev $dev
sudo ip link set $dev down
fi
done

# 5 检查网络是否恢复
echo "[INFO] 检查网络连通性..."
if ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1; then
echo "[SUCCESS] 网络恢复正常。"
else
echo "[ERROR] 网络仍不可达,请检查默认网关或 DHCP。"
fi

echo "[DONE] VPN 停止与网络恢复完成。"