Skip to content

配置一台新的 VPS

本文档用于在 Ubuntu VPS 上完成 Shadowsocks、simple-obfs、WARP、UFW、Fail2ban 和基础流量监控配置。

1. 更新系统软件包

bash
apt update
apt upgrade -y

2. 安装 Shadowsocks 服务端

Ubuntu 官方仓库提供 shadowsocks-libev

bash
apt install -y shadowsocks-libev

3. 安装 simple-obfs

simple-obfs 用于为 Shadowsocks 流量增加混淆层:

bash
apt install -y simple-obfs

4. 配置 Shadowsocks 服务端

编辑 /etc/shadowsocks-libev/config.json

bash
vi /etc/shadowsocks-libev/config.json

写入以下配置:

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-poly1305aes-256-gcm
  • plugin_opts: 混淆模式,可选 httptls

5. 启用 Shadowsocks 服务

bash
systemctl enable --now shadowsocks-libev

6. 检查服务状态

bash
systemctl status shadowsocks-libev

状态应为 active (running)

7. 放行 Shadowsocks 端口

已启用 UFW 时,放行 Shadowsocks 端口:

bash
ufw allow 12096/tcp
ufw allow 12096/udp

8. 配置 WARP

WARP 为可选项。参考 这篇文章

bash
# 自动配置 WARP WireGuard 双栈全局网络(所有出站流量走 WARP 网络)
bash <(curl -fsSL git.io/warp.sh) d

9. 安装并配置 UFW

UFW 用于统一管理入站访问策略:

bash
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 verbose
  • allow OpenSSH: 放行 SSH 服务
  • default deny incoming: 默认拒绝所有入站连接
  • default allow outgoing: 默认允许出站连接

如需开放 Web 端口:

bash
ufw allow 80/tcp
ufw allow 443/tcp

如需在 UFW 中封禁来源地址:

bash
# 封禁单个 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

解封规则:

bash
# 按完整规则删除
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 用于自动封禁重复失败登录来源:

bash
apt install -y fail2ban
systemctl enable --now fail2ban

使用本地覆盖配置:

bash
cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
vi /etc/fail2ban/jail.local

写入基础配置:

ini
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5

[sshd]
enabled = true
port = ssh
logpath = /var/log/auth.log
backend = systemd
  • bantime: 封禁时长
  • findtime: 统计窗口
  • maxretry: 触发封禁的失败次数

重启并检查状态:

bash
systemctl restart fail2ban
fail2ban-client status
fail2ban-client status sshd

SSH 使用非默认端口时,将 port = ssh 改为实际端口号。

11. 清理硬盘空间

查看当前占用:

bash
df -h
du -sh /var/cache/apt /var/log 2>/dev/null

执行常规清理:

bash
apt autoremove -y
apt autoclean
apt clean
  • apt autoremove: 删除无用依赖
  • apt autoclean: 清理过期软件包缓存
  • apt clean: 清空本地软件包缓存

需要进一步检查临时目录时:

bash
du -sh /tmp /var/tmp /var/log/* 2>/dev/null

启用 Docker 时,可额外清理未使用资源:

bash
docker system prune -a

执行前确认不存在仍需保留的镜像或容器。

12. 配置流量监控

本节目标包括两部分:

  • 统计网卡总流量,确认异常发生时间
  • 在流量异常后回查对应进程、端口和远端地址

推荐工具组合如下:

  • vnstat: 长期记录网卡总流量
  • tcplife-bpfcc: 记录 TCP 会话摘要
  • ssconntracknethogs: 用于实时回查当前连接和进程

Linux 默认不会长期保存按进程统计的历史流量记录,因此本节方案对 TCP 的回查能力强于 UDP。对于 TCP,可通过 tcplife-bpfcc 保留连接结束后的会话摘要;对于 UDP,先定位服务端口,再在异常持续期间结合 ss -uapnnethogs 和服务日志判断具体进程。

12.1 安装与配置

安装工具并识别公网网卡。下文示例使用 eth0,实际名称以系统输出为准:

bash
add-apt-repository universe
apt update
apt install -y vnstat nethogs conntrack bpfcc-tools

# 查看默认出网接口
ip route get 1.1.1.1

启动 vnstat

bash
systemctl enable --now vnstat
vnstat --iflist

接口未自动识别时手动添加:

bash
vnstat --add -i eth0
systemctl restart vnstat

journald 配置为持久化存储,确保系统重启后仍可保留 tcplife 日志。以下示例将日志保留策略设置为 30 天或 1G,以先到者为准:

bash
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

常用查询命令:

bash
vnstat -i eth0
vnstat -h -i eth0
vnstat -d -i eth0
vnstat -m -i eth0
  • vnstat -h -i eth0: 按小时查看流量
  • vnstat -d -i eth0: 按天查看流量
  • vnstat -m -i eth0: 按月查看流量

tcplife-bpfcc 配置为常驻服务,并将会话摘要写入 journald

bash
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 改为按本地端口过滤。例如跟踪 22120963001730019

bash
ExecStart=/bin/bash -lc 'exec /usr/sbin/tcplife-bpfcc -w -L 22,12096,30017,30019'

修改服务文件后重新加载并重启:

bash
systemctl daemon-reload
systemctl restart tcplife-log

状态检查:

bash
systemctl status vnstat
systemctl status tcplife-log

回查依赖 journald 持久保存这些日志。以上示例配置下,日志在重启后仍会保留,并按 30 天或 1G 上限自动清理。

12.2 异常流量回查

异常流量回查可按以下顺序执行:

  1. 使用 vnstat 确定异常时间窗口
  2. 导出对应时间段的 tcplife 日志,或直接从 journald 读取
  3. 使用汇总脚本定位异常远端地址、进程和重点服务端口
  4. 异常仍在持续时,再使用 ssnethogsconntrack 检查当前状态

确定时间窗口:

bash
vnstat -h -i eth0
vnstat -d -i eth0

查看指定时间段日志:

bash
journalctl -u tcplife-log \
  --since "2026-04-14 00:00" \
  --until "2026-04-14 03:00" \
  -o short-iso --no-pager

导出到文件:

bash
journalctl -u tcplife-log \
  --since "2026-04-14 00:00" \
  -o short-iso --no-pager > tcp.log

tcp.log 样例中,127.0.0.1/::1 回环连接和 PID=0 记录占比很高,默认统计时应排除;否则 obfs-server -> ss-server 的本地转发流量会淹没真实外部连接。下面的脚本默认排除这两类记录,并同时支持直接分析导出的 tcp.log 文件或直接读取 journald

bash
#!/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

推荐用法:

bash
# 直接分析导出的日志文件
./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: 单连接收发流量

异常仍在持续时,可继续查看当前状态:

bash
# 当前高流量进程
nethogs eth0

# 当前 TCP 连接与监听端口
ss -lntup
ss -ntup

# 当前 UDP 连接
ss -uapn

# 连接跟踪表
conntrack -L | less

已定位到重点端口时,可直接筛选:

bash
ss -lntup | grep -E ':22|:12096|:30017|:30019'
ss -ntup  | grep -E ':22|:12096|:30017|:30019'
ss -uapn  | grep -E ':12096'

回查判断顺序如下:

  1. vnstat 确认异常时间窗口
  2. tcplife 日志和汇总脚本确认异常远端地址、进程和本地端口
  3. ss 确认当前监听端口和未断开的连接
  4. nethogs 确认异常是否仍在持续
  5. UDP 场景再结合 ss -uapnconntrack 和服务日志进一步确认

限制如下:

  • vnstat 只能统计网卡总量,不能直接区分端口或进程
  • tcplife-bpfcc 主要覆盖 TCP,并在连接结束时输出摘要
  • 长连接场景仍需结合 ssnethogs
  • UDP 的历史归因能力弱于 TCP

基于 MIT 许可发布

加载中...