配置一台新的 VPS
本文档用于在 Ubuntu VPS 上完成 Shadowsocks、simple-obfs、WARP、UFW、Fail2ban 和基础流量监控配置。
1. 更新系统软件包
apt update
apt upgrade -y2. 安装 Shadowsocks 服务端
Ubuntu 官方仓库提供 shadowsocks-libev:
apt install -y shadowsocks-libev3. 安装 simple-obfs
simple-obfs 用于为 Shadowsocks 流量增加混淆层:
apt install -y simple-obfs4. 配置 Shadowsocks 服务端
编辑 /etc/shadowsocks-libev/config.json:
vi /etc/shadowsocks-libev/config.json写入以下配置:
{
"server": ["::0", "0.0.0.0"],
"mode": "tcp_and_udp",
"server_port": 12096,
"local_port": 1080,
"method": "chacha20-ietf-poly1305",
"password": "your_password",
"plugin": "obfs-server",
"plugin_opts": "obfs=http"
}server_port: 服务端口password: 访问密码method: 加密算法,推荐chacha20-ietf-poly1305或aes-256-gcmplugin_opts: 混淆模式,可选http或tls
5. 启用 Shadowsocks 服务
systemctl enable --now shadowsocks-libev6. 检查服务状态
systemctl status shadowsocks-libev状态应为 active (running)。
7. 放行 Shadowsocks 端口
已启用 UFW 时,放行 Shadowsocks 端口:
ufw allow 12096/tcp
ufw allow 12096/udp8. 配置 WARP
WARP 为可选项。参考 这篇文章:
# 自动配置 WARP WireGuard 双栈全局网络(所有出站流量走 WARP 网络)
bash <(curl -fsSL git.io/warp.sh) d9. 安装并配置 UFW
UFW 用于统一管理入站访问策略:
apt install -y ufw
# 先放行 SSH,避免远程连接中断
ufw allow OpenSSH
# 放行 Shadowsocks 端口
ufw allow 12096/tcp
ufw allow 12096/udp
# 默认策略
ufw default deny incoming
ufw default allow outgoing
# 启用并查看状态
ufw enable
ufw status verboseallow OpenSSH: 放行 SSH 服务default deny incoming: 默认拒绝所有入站连接default allow outgoing: 默认允许出站连接
如需开放 Web 端口:
ufw allow 80/tcp
ufw allow 443/tcp如需在 UFW 中封禁来源地址:
# 封禁单个 IP
ufw deny from 203.0.113.10
# 仅封禁该 IP 访问某个端口
ufw deny from 203.0.113.10 to any port 22 proto tcp
# 封禁网段
ufw deny from 203.0.113.0/24
# 封禁 IPv6 网段
ufw deny from 2001:db8:1234::/48
# 查看带编号的规则
ufw status numbered解封规则:
# 按完整规则删除
ufw delete deny from 203.0.113.10
ufw delete deny from 203.0.113.10 to any port 22 proto tcp
ufw delete deny from 203.0.113.0/24
ufw delete deny from 2001:db8:1234::/48
# 或按编号删除
ufw status numbered
ufw delete 3需要显式返回拒绝响应时,可将 deny 改为 reject。
10. 安装并配置 Fail2ban
Fail2ban 用于自动封禁重复失败登录来源:
apt install -y fail2ban
systemctl enable --now fail2ban使用本地覆盖配置:
cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
vi /etc/fail2ban/jail.local写入基础配置:
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
[sshd]
enabled = true
port = ssh
logpath = /var/log/auth.log
backend = systemdbantime: 封禁时长findtime: 统计窗口maxretry: 触发封禁的失败次数
重启并检查状态:
systemctl restart fail2ban
fail2ban-client status
fail2ban-client status sshdSSH 使用非默认端口时,将 port = ssh 改为实际端口号。
11. 清理硬盘空间
查看当前占用:
df -h
du -sh /var/cache/apt /var/log 2>/dev/null执行常规清理:
apt autoremove -y
apt autoclean
apt cleanapt autoremove: 删除无用依赖apt autoclean: 清理过期软件包缓存apt clean: 清空本地软件包缓存
需要进一步检查临时目录时:
du -sh /tmp /var/tmp /var/log/* 2>/dev/null启用 Docker 时,可额外清理未使用资源:
docker system prune -a执行前确认不存在仍需保留的镜像或容器。
12. 配置流量监控
本节目标包括两部分:
- 统计网卡总流量,确认异常发生时间
- 在流量异常后回查对应进程、端口和远端地址
推荐工具组合如下:
vnstat: 长期记录网卡总流量tcplife-bpfcc: 记录 TCP 会话摘要ss、conntrack、nethogs: 用于实时回查当前连接和进程
Linux 默认不会长期保存按进程统计的历史流量记录,因此本节方案对 TCP 的回查能力强于 UDP。对于 TCP,可通过 tcplife-bpfcc 保留连接结束后的会话摘要;对于 UDP,先定位服务端口,再在异常持续期间结合 ss -uapn、nethogs 和服务日志判断具体进程。
12.1 安装与配置
安装工具并识别公网网卡。下文示例使用 eth0,实际名称以系统输出为准:
add-apt-repository universe
apt update
apt install -y vnstat nethogs conntrack bpfcc-tools
# 查看默认出网接口
ip route get 1.1.1.1启动 vnstat:
systemctl enable --now vnstat
vnstat --iflist接口未自动识别时手动添加:
vnstat --add -i eth0
systemctl restart vnstat将 journald 配置为持久化存储,确保系统重启后仍可保留 tcplife 日志。以下示例将日志保留策略设置为 30 天或 1G,以先到者为准:
mkdir -p /etc/systemd/journald.conf.d /var/log/journal
cat >/etc/systemd/journald.conf.d/persistent.conf <<'EOF'
[Journal]
Storage=persistent
SystemMaxUse=1G
MaxRetentionSec=30day
EOF
systemctl restart systemd-journald
journalctl --flush常用查询命令:
vnstat -i eth0
vnstat -h -i eth0
vnstat -d -i eth0
vnstat -m -i eth0vnstat -h -i eth0: 按小时查看流量vnstat -d -i eth0: 按天查看流量vnstat -m -i eth0: 按月查看流量
将 tcplife-bpfcc 配置为常驻服务,并将会话摘要写入 journald:
cat >/etc/systemd/system/tcplife-log.service <<'EOF'
[Unit]
Description=Log TCP session summaries for traffic backtrace
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/bin/bash -lc 'exec /usr/sbin/tcplife-bpfcc -w'
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now tcplife-log
journalctl -u tcplife-log -n 20 --no-pager仅需跟踪固定服务端口时,可将 ExecStart 改为按本地端口过滤。例如跟踪 22、12096、30017、30019:
ExecStart=/bin/bash -lc 'exec /usr/sbin/tcplife-bpfcc -w -L 22,12096,30017,30019'修改服务文件后重新加载并重启:
systemctl daemon-reload
systemctl restart tcplife-log状态检查:
systemctl status vnstat
systemctl status tcplife-log回查依赖 journald 持久保存这些日志。以上示例配置下,日志在重启后仍会保留,并按 30 天或 1G 上限自动清理。
12.2 异常流量回查
异常流量回查可按以下顺序执行:
- 使用
vnstat确定异常时间窗口 - 导出对应时间段的
tcplife日志,或直接从journald读取 - 使用汇总脚本定位异常远端地址、进程和重点服务端口
- 异常仍在持续时,再使用
ss、nethogs、conntrack检查当前状态
确定时间窗口:
vnstat -h -i eth0
vnstat -d -i eth0查看指定时间段日志:
journalctl -u tcplife-log \
--since "2026-04-14 00:00" \
--until "2026-04-14 03:00" \
-o short-iso --no-pager导出到文件:
journalctl -u tcplife-log \
--since "2026-04-14 00:00" \
-o short-iso --no-pager > tcp.logtcp.log 样例中,127.0.0.1/::1 回环连接和 PID=0 记录占比很高,默认统计时应排除;否则 obfs-server -> ss-server 的本地转发流量会淹没真实外部连接。下面的脚本默认排除这两类记录,并同时支持直接分析导出的 tcp.log 文件或直接读取 journald:
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
用法:
./analyze_tcplife.sh -f tcp.log [-n 20] [-p 22,12096] [--min-kb 10] [--include-loopback] [--include-pid0]
./analyze_tcplife.sh [-u tcplife-log] [-s "1 hour ago"] [-t now] [-n 20] [-p 22,12096] [--min-kb 10] [--include-loopback] [--include-pid0]
USAGE
}
INPUT_FILE=""
SINCE="1 hour ago"
UNTIL="now"
UNIT="tcplife-log"
TOPN="${TOPN:-15}"
WATCH_PORTS="${WATCH_PORTS:-}"
MIN_KB="${MIN_KB:-1}"
INCLUDE_LOOPBACK=0
INCLUDE_PID0=0
while [[ $# -gt 0 ]]; do
case "$1" in
-f|--file) INPUT_FILE="$2"; shift 2 ;;
-s|--since) SINCE="$2"; shift 2 ;;
-t|--until) UNTIL="$2"; shift 2 ;;
-u|--unit) UNIT="$2"; shift 2 ;;
-n|--top) TOPN="$2"; shift 2 ;;
-p|--ports) WATCH_PORTS="$2"; shift 2 ;;
--min-kb) MIN_KB="$2"; shift 2 ;;
--include-loopback) INCLUDE_LOOPBACK=1; shift ;;
--include-pid0) INCLUDE_PID0=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown arg: $1" >&2; usage >&2; exit 1 ;;
esac
done
RAW_FILE="$(mktemp)"
ROW_FILE="$(mktemp)"
trap 'rm -f "$RAW_FILE" "$ROW_FILE"' EXIT
if [[ -n "$INPUT_FILE" ]]; then
cat "$INPUT_FILE" > "$RAW_FILE"
SOURCE_DESC="file: $INPUT_FILE"
else
journalctl -u "$UNIT" --since "$SINCE" --until "$UNTIL" -o short-iso --no-pager > "$RAW_FILE"
SOURCE_DESC="journalctl -u $UNIT --since $SINCE --until $UNTIL"
fi
awk -v include_loopback="$INCLUDE_LOOPBACK" \
-v include_pid0="$INCLUDE_PID0" \
-v watch_ports="$WATCH_PORTS" \
-v min_kb="$MIN_KB" '
function is_loopback(addr) {
return addr == "127.0.0.1" || addr == "::1"
}
BEGIN {
split(watch_ports, tmp, ",")
for (i in tmp) if (tmp[i] != "") watch[tmp[i]] = 1
}
{
line = $0
sub(/.*\]: /, "", line)
if (line !~ /^[0-9]+[[:space:]]+/) next
n = split(line, a, /[[:space:]]+/)
if (n < 10) next
pid = a[1] + 0
comm = a[2]
ipver = a[3]
laddr = a[4]
lport = a[5]
raddr = a[6]
rport = a[7]
tx = a[8] + 0
rx = a[9] + 0
ms = a[10] + 0
total = tx + rx
if (!include_pid0 && pid == 0) next
if (!include_loopback && (is_loopback(laddr) || is_loopback(raddr))) next
if (total < min_kb) next
watched = ((watch_ports != "" && (lport in watch)) ? 1 : 0)
printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%.0f\t%.0f\t%.0f\t%.2f\t%d\n", \
pid, comm, laddr, lport, raddr, rport, ipver, total, tx, rx, ms, watched
}
' "$RAW_FILE" > "$ROW_FILE"
if [[ ! -s "$ROW_FILE" ]]; then
echo "未找到符合条件的 tcplife 日志。"
exit 0
fi
human_size() {
awk -v kb="$1" '
function fmt(v, unit, i, u) {
split("KB MB GB TB PB", u, " ")
i = 1
while (v >= 1024 && i < 5) {
v /= 1024
i++
}
if (v >= 100 || i == 1) {
printf "%.0f %s", v, u[i]
} else if (v >= 10) {
printf "%.1f %s", v, u[i]
} else {
printf "%.2f %s", v, u[i]
}
}
BEGIN { fmt(kb + 0) }'
}
print_table() {
local title="$1"
local script="$2"
echo "[$title]"
local out
out="$(awk -F'\t' "$script" "$ROW_FILE" | sort -nr | sed -n "1,${TOPN}p")"
if [[ -z "$out" ]]; then
echo "(empty)"
else
printf "%s\n" "$out" | awk -F'\t' '
function human(kb, i, u) {
split("KB MB GB TB PB", u, " ")
i = 1
while (kb >= 1024 && i < 5) {
kb /= 1024
i++
}
if (kb >= 100 || i == 1) {
return sprintf("%.0f %s", kb, u[i])
} else if (kb >= 10) {
return sprintf("%.1f %s", kb, u[i])
}
return sprintf("%.2f %s", kb, u[i])
}
{printf "%12s %s\n", human($1), $2}'
fi
echo
}
ROWS="$(wc -l < "$ROW_FILE")"
TOTAL_KB="$(awk -F'\t' '{s += $8} END {printf "%.0f", s + 0}' "$ROW_FILE")"
TOTAL_TX_KB="$(awk -F'\t' '{s += $9} END {printf "%.0f", s + 0}' "$ROW_FILE")"
TOTAL_RX_KB="$(awk -F'\t' '{s += $10} END {printf "%.0f", s + 0}' "$ROW_FILE")"
printf "Source: %s\n" "$SOURCE_DESC"
printf "Rows: %s\n" "$ROWS"
printf "Total: %s (TX %s / RX %s)\n" \
"$(human_size "$TOTAL_KB")" \
"$(human_size "$TOTAL_TX_KB")" \
"$(human_size "$TOTAL_RX_KB")"
printf "Filters: min_kb=%s, loopback=%s, pid0=%s\n\n" \
"$MIN_KB" \
"$([[ "$INCLUDE_LOOPBACK" -eq 1 ]] && echo include || echo exclude)" \
"$([[ "$INCLUDE_PID0" -eq 1 ]] && echo include || echo exclude)"
print_table "Top remote IP" \
'{sum[$5] += $8} END {for (k in sum) printf "%.0f\t%s\n", sum[k], k}'
print_table "Top process" \
'{sum[$2] += $8} END {for (k in sum) printf "%.0f\t%s\n", sum[k], k}'
print_table "Top process + remote IP" \
'{k=$2 "|" $5; sum[k] += $8} END {for (k in sum) printf "%.0f\t%s\n", sum[k], k}'
print_table "Top remote endpoint" \
'{k=$5 ":" $6; sum[k] += $8} END {for (k in sum) printf "%.0f\t%s\n", sum[k], k}'
if [[ -n "$WATCH_PORTS" ]]; then
print_table "Top watched local port" \
'$12 == 1 {sum[$4] += $8} END {for (k in sum) printf "%.0f\t%s\n", sum[k], k}'
print_table "Top watched port + remote IP" \
'$12 == 1 {k=$4 "|" $5; sum[k] += $8} END {for (k in sum) printf "%.0f\t%s\n", sum[k], k}'
print_table "Top watched port + process" \
'$12 == 1 {k=$4 "|" $2; sum[k] += $8} END {for (k in sum) printf "%.0f\t%s\n", sum[k], k}'
fi推荐用法:
# 直接分析导出的日志文件
./analyze_tcplife.sh -f tcp.log -p 22,12096
# 直接读取 journald
./analyze_tcplife.sh -s "2026-04-14 00:00" -t "2026-04-14 03:00" -p 22,12096输出关注点:
Top remote IP: 远端地址汇总Top process: 进程汇总Top process + remote IP: 进程与远端地址组合Top remote endpoint: 远端地址与端口组合Top watched local port: 重点服务端口汇总Top watched port + remote IP: 重点服务端口与远端地址组合
tcplife-bpfcc 关键字段如下:
COMM/PID: 进程名和进程号LPORT: 本地端口RADDR/RPORT: 远端地址和端口TX_KB/RX_KB: 单连接收发流量
异常仍在持续时,可继续查看当前状态:
# 当前高流量进程
nethogs eth0
# 当前 TCP 连接与监听端口
ss -lntup
ss -ntup
# 当前 UDP 连接
ss -uapn
# 连接跟踪表
conntrack -L | less已定位到重点端口时,可直接筛选:
ss -lntup | grep -E ':22|:12096|:30017|:30019'
ss -ntup | grep -E ':22|:12096|:30017|:30019'
ss -uapn | grep -E ':12096'回查判断顺序如下:
vnstat确认异常时间窗口tcplife日志和汇总脚本确认异常远端地址、进程和本地端口ss确认当前监听端口和未断开的连接nethogs确认异常是否仍在持续- UDP 场景再结合
ss -uapn、conntrack和服务日志进一步确认
限制如下:
vnstat只能统计网卡总量,不能直接区分端口或进程tcplife-bpfcc主要覆盖 TCP,并在连接结束时输出摘要- 长连接场景仍需结合
ss和nethogs - UDP 的历史归因能力弱于 TCP