VPN 连接监控与 Telegram 实时通知
# 背景
之前部署了 VPN 供个人使用,但一直不知道有没有其他人在使用。每次想查看连接状态都要手动登录服务器执行命令,非常麻烦。
需求很简单:
- 有人连接 VPN 时收到通知
- 有人断开 VPN 时收到通知
- 通知要推送到手机,方便随时查看
本文记录从零到完成 VPN 监控通知系统的完整过程,包括踩过的坑和最终方案。
更新记录:2026-06-20 从 IKEv2 迁移到 Shadowsocks + WebSocket + TLS,监控脚本同步更新。
# 一、方案演进
# 1.1 最初方案:定时检查(IKEv2 时代)
最直观的想法:写个脚本定时检查 VPN 连接数,有变化就发通知。
#!/bin/bash
# 获取当前 VPN 连接数
get_connections() {
ip xfrm state list 2>/dev/null | grep "proto esp" | wc -l
}
current_count=$(get_connections)
actual_count=$((current_count / 2)) # 每个 VPN 连接有 2 个 SA
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
问题:VPN 连接时长如果很短(比如 1-2 分钟),而检查间隔是 1 分钟,很容易漏掉事件。
# 1.2 进阶方案:实时日志监控(IKEv2)
改为实时监控 strongswan 日志,使用 SA ID 去重避免重复通知。
问题:IKEv2 服务已于 2026-06-20 下线,迁移到 Shadowsocks。
# 1.3 当前方案:Shadowsocks 日志监控
Shadowsocks + v2ray-plugin 的日志格式与 IKEv2 不同:
Jun 21 23:07:15 server ssserver[1685735]: 2026/06/21 23:07:15 tcp:203.218.155.130:0 accepted tcp:127.0.0.1:0
1
挑战:
- 每个 WebSocket 连接会产生多条
accepted日志(一个连接多个 TCP 流) - 没有明确的断开事件日志
- 需要用 IP + 时间窗口去重
# 二、最终方案
# 2.1 架构说明
┌─────────────┐ WebSocket + TLS ┌─────────────┐
│ Client │ ───────────────────────▶ │ Caddy │
│ (Shadowsocks) │ (HTTPS) │
└─────────────┘ └──────┬──────┘
│
▼
┌─────────────┐
│ v2ray-plugin│
│ (WS Server)│
└──────┬──────┘
│
▼
┌─────────────┐
│ Shadowsocks │
│ Server │
└─────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2.2 监控脚本
创建 /home/moyin/vpn-monitor/ss-monitor.sh:
#!/bin/bash
# Shadowsocks VPN 实时日志监控 - 连接/断开通知
# 使用超时检测实现断开通知(30分钟无活动)
# 配置
TELEGRAM_CHAT_ID="YOUR_TELEGRAM_CHAT_ID"
STATE_DIR="/tmp/ss-monitor"
LOG_FILE="/var/log/ss-monitor.log"
# 去重时间窗口(秒)- 同一IP短时间内不重复通知连接
DEDUP_WINDOW=60
# 断开超时时间(秒)- 30分钟无活动判定为断开
DISCONNECT_TIMEOUT=1800
# 创建状态目录
mkdir -p "$STATE_DIR"
# 发送 Telegram 通知
send_notify() {
local event="$1"
local ip="$2"
local timestamp="$3"
local msg=""
if [ "$event" = "CONNECTED" ]; then
msg="🔒 Shadowsocks VPN 连接通知
✅ 新设备已连接
📍 IP: ${ip}
🕐 时间: ${timestamp}
🌐 协议: WebSocket + TLS"
elif [ "$event" = "DISCONNECTED" ]; then
msg="🔒 Shadowsocks VPN 连接通知
❌ 设备已断开
📍 IP: ${ip}
🕐 时间: ${timestamp}"
fi
openclaw message send --channel telegram --target "$TELEGRAM_CHAT_ID" --message "$msg" 2>/dev/null
}
# 获取IP状态文件路径
get_state_file() {
echo "$STATE_DIR/${1}.state"
}
# 更新IP最后活动时间
update_activity() {
echo $(date +%s) > "$(get_state_file "$1")"
}
# 检查是否应该发送连接通知(去重)
should_notify_connect() {
local ip="$1"
local current_time=$(date +%s)
local notify_file="$STATE_DIR/${ip}.notify"
# 首次连接
[ ! -f "$(get_state_file "$ip")" ] && return 0
# 检查去重窗口
if [ -f "$notify_file" ]; then
local diff=$((current_time - $(cat "$notify_file")))
[ $diff -lt $DEDUP_WINDOW ] && return 1
fi
return 0
}
# 检查超时断开(后台循环)
check_timeout() {
local current_time=$(date +%s)
for state_file in "$STATE_DIR"/*.state; do
[ -f "$state_file" ] || continue
local ip=$(basename "$state_file" .state)
local last_activity=$(cat "$state_file")
local connected_marker="$STATE_DIR/${ip}.connected"
# 跳过未标记为已连接的IP
[ -f "$connected_marker" ] || continue
local diff=$((current_time - last_activity))
# 超过30分钟无活动,判定为断开
if [ $diff -gt $DISCONNECT_TIMEOUT ]; then
send_notify "DISCONNECTED" "$ip" "$(date '+%Y-%m-%d %H:%M:%S')"
rm -f "$state_file" "$STATE_DIR/${ip}.notify" "$connected_marker"
fi
done
}
# 启动后台超时检查(每分钟检查一次)
while true; do sleep 60; check_timeout; done &
# 清理函数
trap 'kill $(jobs -p) 2>/dev/null; exit 0' SIGTERM SIGINT
# 监控 shadowsocks 日志
journalctl -u shadowsocks -f --no-pager 2>/dev/null | while read -r line; do
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
if echo "$line" | grep -q "accepted"; then
remote_ip=$(echo "$line" | grep -oP 'tcp:\K[0-9.]+(?=:0 accepted)')
if [ -n "$remote_ip" ]; then
update_activity "$remote_ip"
local connected_marker="$STATE_DIR/${remote_ip}.connected"
if [ ! -f "$connected_marker" ] && should_notify_connect "$remote_ip"; then
send_notify "CONNECTED" "$remote_ip" "$timestamp"
date +%s > "$STATE_DIR/${remote_ip}.notify"
touch "$connected_marker"
fi
fi
fi
done
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
119
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
119
# 2.3 Systemd 服务
创建 /etc/systemd/system/ss-monitor.service:
[Unit]
Description=Shadowsocks VPN Real-time Log Monitor
After=network.target shadowsocks.service
Requires=shadowsocks.service
[Service]
Type=simple
ExecStart=/home/moyin/vpn-monitor/ss-monitor.sh
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 2.4 启用服务
chmod +x /home/moyin/vpn-monitor/ss-monitor.sh
systemctl daemon-reload
systemctl enable ss-monitor
systemctl start ss-monitor
systemctl status ss-monitor
1
2
3
4
5
2
3
4
5
# 三、效果展示
连接 VPN 时收到通知:
🔒 Shadowsocks VPN 连接通知
✅ 新设备已连接
📍 IP: 203.218.155.130 (示例 IP)
🕐 时间: 2026-06-22 15:30:45
🌐 协议: WebSocket + TLS
1
2
3
4
5
6
2
3
4
5
6
断开 VPN 时收到通知(30分钟无活动后):
🔒 Shadowsocks VPN 连接通知
❌ 设备已断开
📍 IP: 203.218.155.130 (示例 IP)
🕐 时间: 2026-06-22 16:05:30
1
2
3
4
5
2
3
4
5
# 四、技术要点总结
| 对比项 | IKEv2 方案 | Shadowsocks 方案 |
|---|---|---|
| 日志来源 | journalctl -u strongswan-starter | journalctl -u shadowsocks |
| 连接事件 | IKE_SA.*established | tcp:IP:0 accepted |
| 断开事件 | deleting IKE_SA | 无明确事件,使用超时检测 |
| 去重方式 | SA ID 去重 | IP + 时间窗口去重 |
| 断开判定 | 日志事件 | 30分钟无活动超时 |
| 服务依赖 | strongswan-starter.service | shadowsocks.service |
# 五、工作原理
┌─────────────────────────────────────────────────────────────┐
│ 主进程(日志监控) │
│ journalctl -f → 检测 accepted → 更新IP活动时间 → 发送连接通知 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 后台进程(超时检查) │
│ 每分钟扫描状态文件 → 发现30分钟无活动的IP → 发送断开通知 │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
状态文件存储:
{IP}.state- IP 最后活动时间{IP}.notify- 上次发送通知时间(用于去重){IP}.connected- 已发送连接通知标记
# 六、注意事项
- 去重时间窗口:设置为 60 秒,避免同一 IP 短时间内重复通知
- 断开超时:30 分钟无活动判定为断开,发送断开通知
- 状态文件:存储在
/tmp/ss-monitor/目录,重启后清空 - 后台检查:每分钟扫描一次,检测超时断开的 IP
- 服务依赖:监控服务依赖 shadowsocks.service,确保 Shadowsocks 先启动
# 六、历史方案(已废弃)
点击展开 IKEv2 时代的方案(仅供参考)
# IKEv2 日志监控脚本(已废弃)
#!/bin/bash
# 已废弃 - IKEv2 VPN 实时日志监控
# 监控 strongswan 日志
journalctl -u strongswan-starter -f --no-pager 2>/dev/null | while read -r line; do
# 检测连接
if echo "$line" | grep -q "IKE_SA.*established between"; then
sa_id=$(echo "$line" | grep -oP 'IKE_SA[^[]+\[\K[0-9]+')
remote_ip=$(echo "$line" | grep -oP '\.\.\.([0-9.]+)' | sed 's/\.\.\.//')
# ... 发送通知
fi
# 检测断开
if echo "$line" | grep -q "deleting IKE_SA"; then
# ... 发送通知
fi
done
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 七、参考链接
- Shadowsocks-rust GitHub (opens new window)
- v2ray-plugin GitHub (opens new window)
- OpenClaw 文档 (opens new window)
最后更新:2026-06-22(迁移到 Shadowsocks) 历史版本:2026-06-06(IKEv2 版本)
上次更新: 2026/7/13 15:04:12