✧ 君と集まって星座になれたら ✦ ROKUTOSEI
11095 字
55 分钟
基于 qBittorrent EE、PeerBanHelper 与 OpenList 的个人追番 Seedbox 部署指南
更新于 2026-07-31

本文先发布于 NodeSeek 与 Github

NodeSeek、GitHub 文章不保证再更新,可能过时

请以本文或者为主

背景#

在此之前的很长一段时间里,我一直使用 Windows 本机运行 qBEE 追番。日常使用倒也没什么大问题,只是偶尔会系统卡顿一下(尤其在我播放音频的时候尤为明显),而且网络也似乎时不时被运营商 Q,多少有些不便。再考虑到长期全天做种对 SSD 有读写负担,索性迁移到 VPS 上

前阵子我买了某芬兰厂家的普通 Seedbox(无 root 权限的普通盒子),但没想到短时间内就经历了两次服务宕机,发工单也没真人,都是 AI 敷衍回复。

虽然社区也已经有了一键搭建脚本,但它们大多面向 PT 刷流场景,又或者里面预装很多东西等等之类的,于是我便想着,为什么我不自己搞一台大盘机做 Seedbox 呢?

但最近硬盘内存涨价,连带服务器也水涨船高,便宜的大盘机基本都处于售罄状态,更何况是版权宽松的芬兰、荷兰和卢森堡等地的机器,特别某些热门商家,即便补货也是被脚本秒了。要么去继续蹲,要么溢价收……

在我各种搜索之后,(虽然得到的案例不多)知道了我的需求貌似处于一个中间地带,被 DMCA 投诉的对象一般是热门美剧、英剧、好莱坞大片或主流流媒体平台(Ne*flix、Di*ney+、H*O等)的独家剧集,而某 Mi*an、某 N*a 上面这类动漫种子则通常都是经由字幕组重烧录编码,文件哈希已经改变(除了 Raw 生肉搬运、部分由 Ne*flix 发行的动漫),版权方很难追溯。那么我就想,是不是不一定需要找版权宽松地区的机器?这样选择范围变广了,再考虑到我需要通过 OpenList 在线观看,那在预算有限的情况下,优先选择美西地区的大盘机……

因此,我决定铤而走险,试试在 DMCA 最严格的 US 尝试 BT 下载

以本文记录我个人的操作流程,供日后参考

适用人群#

本文面向以 RSS 订阅追番为主、希望将 BT 下载与日常电脑分离的个人用户,适合以下读者:

  • 了解 Linux 基础操作
  • 拥有一台 VPS(2C2G 或 1C2G 起步)和一个个人域名
  • 希望设备简单解耦
CAUTION

本文不适合 PT 刷流或冲上传量的玩家

文中配置追求保守、不被投诉,需要自动刷流请参考其他专门教程

完成后将获得#

跟随本文完成全部部署后,日常工作流如下:

  • 种子自动下载:在 qBEE 中配置 RSS 订阅,番剧更新后自动下载到 VPS (限于篇幅,本文不教如何使用 RSS 订阅种子)
  • 多端在线观看:在任意设备的浏览器打开 https://openlist.yourdomain.com,直接在线播放已下载的番剧
  • 本地备份(可选):通过 Air2、SFTP、Rclone 等工具将文件拖回 Windows 本地保存

零、 准备工作#

在开始之前,请做好以下准备:

  1. 一个个人域名:用于反代访问
  2. 一个通过支付验证的 Cloudflare 账号:将域名托管至 Cloudflare
  3. 一台 VPS:拥有 root 且建议 2GB RAM 起步
NOTE

为避免纠纷,本文不提供任何 VPS 商家推荐或购买指导,请自行选择

本文中的 your_vps_ip、yourname、yourdomain.com 均为占位符,实际操作时请替换为你的真实 IP、用户名和域名


一、 系统基础防护#

1.1 系统基础优化#

1.1.1 清除厂家模板#

使用 root 账密进行 ssh 登录

PowerShell 7
ssh root@192.0.2.100 # 改为你的 IP,回车后输入密码

部分服务商的系统模板可能包含首次开机脚本,建议清除,没有则跳过

Terminal window
sudo crontab -e

进入编辑界面后,寻找类似 @reboot /admin/firstbootkvm yes 的行,将其删除并保存

1.1.2 开启 BBR 网络加速#

把系统自带的 BBR 拥塞控制开启,能直接拉升机器的公网吞吐量:

Terminal window
echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf
sysctl -p

1.1.3 增大 Swap(可选)#

对于 2GB RAM 的 VPS,qBEE 和 PBH 同时运行时内存吃紧,适当增大 Swap 可提供缓冲:

Terminal window
# 查看当前 Swap
sudo swapon --show
# 创建 2G Swap 文件(可根据磁盘余量调整大小)
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
# 写入 fstab 使其重启后自动挂载
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

降低 swappiness 让系统优先使用物理内存:

Terminal window
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
swappiness行为
0不用 swap(如果 RAM 充足,可以完全不用)
10物理内存接近耗尽时才动用 swap(推荐)
20较保守的折中值
60系统默认,较早换出到 swap

数值越低,系统越倾向保留物理内存,避免磁盘 I/O;但同时 OOM 风险略增。常规场景设为 10 即可

1.1.4 切换默认 Shell 为 Bash#

Debian 12 Template 默认将 /bin/sh 指向 dash,不是 bash。本文所有脚本均以 #!/bin/bash 声明,不受影响,但个别系统脚本和交互环境可能行为异常(比如在 PowerShell 7 中主机名无彩色、部分 sh 脚本语法报错)。

将默认 Shell 切换为 bash:

方法一(标准交互方式):

Terminal window
# 查看当前指向
ls -l /bin/sh
# 重新配置 dash,在弹出的对话框中选择 <No>(即“不使用 dash 作为默认 sh”)
sudo dpkg-reconfigure dash
# 确认已切换
ls -l /bin/sh

执行后应看到 /bin/sh -> bash。

如果执行后没有弹出任何交互窗口(直接返回),可能是系统处于非交互模式,请改用方法二:

Terminal window
# 设置 debconf 选项为“不使用 dash”
echo "dash dash/sh boolean false" | sudo debconf-set-selections
# 手动链接到 bash
sudo ln -sf bash /bin/sh
# 确认切换
ls -l /bin/sh

方法二同时更新了 debconf 数据库,因此后续 dash 包升级时不会自动覆盖该链接。如果交互失败,直接采用方法二即可,无需纠结弹窗。

1.2 SSH 密钥登录#

1.2.1 生成密钥对#

在本地电脑(本文以 Windows 为例)打开 PowerShell 7,生成一把新密钥对:

PowerShell 7
cd ~
ssh-keygen -t ed25519 -C "随便取名比如seedbox" -f ~/.ssh/id_ed25519_seedbox

回车后,会提示你设置密码(passphrase),如果嫌麻烦可以直接连按两次回车跳过即可

1.2.2 上传公钥至服务器#

密钥生成后,在本地 PowerShell 中运行以下命令,将公钥上传到服务器(注意将 your_vps_ip 替换为真实 IP)

PowerShell 7
$pubKey = Get-Content "$HOME\.ssh\id_ed25519_seedbox.pub"
$pubKey | ssh root@your_vps_ip "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

1.2.3 加固 SSH 配置#

然后,回到 VPS SSH 窗口,编辑 SSH 配置文件:

Terminal window
nano /etc/ssh/sshd_config

修改或取消注释以下行:

Port 43210 # 修改为任意高位端口
PasswordAuthentication no # 禁用密码登录
PubkeyAuthentication yes # 允许密钥登录

顺便查看是否存在这一行 Include /etc/ssh/sshd_config.d/*.conf

如有,在保存退出对 /etc/ssh/sshd_config 的编辑后,输入:

Terminal window
ls -R /etc/ssh/sshd_config.d/

如果看到输出结果是 50-cloud-init.conf,则继续输入:

Terminal window
sudo nano /etc/ssh/sshd_config.d/50-cloud-init.conf

将里面的 PasswordAuthentication yes 改为 PasswordAuthentication no

重启 SSH 服务使变更生效:

Terminal window
sudo systemctl restart sshd

注意,重启服务后,先别关闭当前的 SSH 终端窗口,先开一个新的终端窗口,测试一下能不能用新端口和密钥成功连上,如果连不上,在原来的窗口里把配置改回来

1.2.4 测试连接#

从本地 Windows 测试连接的命令是:

PowerShell 7
cd ~/.ssh
ssh -i ./id_ed25519_seedbox -p 43210 root@your_vps_ip

如果私钥设置了 passphrase,在连接时会提示输入;如果没有设置,则直接登录

1.2.5 配置本地快捷登录#

如果希望后续不用每次都输路径和端口,可以在本地添加一个配置块

在 Windows Powershell 7 输入:

PowerShell 7
notepad $HOME/.ssh/config

回车后会打开一个记事本编辑器,输入以下内容:

Host seedbox
HostName your_vps_ip
User root
Port 43210
IdentityFile ~/.ssh/id_ed25519_seedbox
IdentitiesOnly yes
ServerAliveInterval 30
ServerAliveCountMax 3

Ctrl+S 保存后,即可用以下命令登录:

PowerShell 7
ssh seedbox

1.3 创建新用户#

为贯彻最小权限原则,我们不使用 root 登录,创建一个专用用户来运行服务和管理文件

TIP

以下步骤仍在当前的 root SSH 会话中执行,完成后将切换到新用户

1.3.1 创建用户并设置登录密码#

Terminal window
# 创建用户 yourname(UID 1000,带 home 目录)
useradd -m -u 1000 yourname
# 设置密码(必须设置,否则无法使用 sudo)
passwd yourname
NOTE

系统会提示你输入两次密码,虽然我们主要用密钥登录,但 sudo 默认需要用户有密码,否则会报错 user is not in the sudoers file 或 no password set

1.3.2 将用户加入 sudo 组#

Terminal window
usermod -aG sudo yourname

1.3.3 配置免密 sudo (可选、谨慎!)#

CAUTION

跳过密码验证会降低安全性,仅建议在个人独占、无敏感数据的 VPS 上使用

如果你希望执行 sudo 时不用输密码,编辑 sudoers 文件:

Terminal window
visudo

在文件末尾添加一行:

yourname ALL=(ALL) NOPASSWD: ALL

Ctrl+O 、回车、 Ctrl+X 保存退出

TIP

比较推荐的方法是保留密码验证,只对特定命令免密

比如 sudo systemctl restart qbittorrent

本教程为简化流程采用全局免密

1.3.4 将公钥复制给新用户#

在本地 PowerShell 中运行(注意将 your_vps_ip 替换为真实 IP):

PowerShell 7
$pubKey = Get-Content "$HOME\.ssh\id_ed25519_seedbox.pub"
$pubKey | ssh seedbox "mkdir -p /home/yourname/.ssh && chmod 700 /home/yourname/.ssh && cat >> /home/yourname/.ssh/authorized_keys && chmod 600 /home/yourname/.ssh/authorized_keys && chown -R yourname:yourname /home/yourname/.ssh"

1.3.5 更新本地 SSH config#

打开你本地的 $HOME/.ssh/config,追加新用户的配置块:

Host seedbox-yourname
HostName your_vps_ip
User yourname
Port 43210
IdentityFile ~/.ssh/id_ed25519_seedbox
IdentitiesOnly yes
ServerAliveInterval 30
ServerAliveCountMax 3

保存后,即可用 ssh seedbox-yourname 登录,再通过 sudo 执行管理命令


1.4 部署 Fail2ban#

虽然已经禁用了密码登录,但公网上的端口探测依然会消耗系统资源

Fail2ban 可以动态封禁那些反复试探的 IP

Terminal window
sudo apt update && sudo apt install fail2ban -y

创建自定义配置,注意将 port 改为你之前设置的 SSH 端口:

Terminal window
sudo nano /etc/fail2ban/jail.local

写入:

[sshd]
enabled = true
port = 43210
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
findtime = 600
bantime = 86400

10 分钟内输错 3 次密码,封禁该 IP 24 小时

启动并设置开机自启:

Terminal window
sudo systemctl enable --now fail2ban

1.5 部署防火墙#

本文采用以下设计:

  • 防火墙规则(iptables):长期稳定,部署一次即可,后续无需变动
  • 黑名单数据(ipset):定期更新,独立脚本执行,不影响现有防火墙规则
防火墙规则 (deploy_firewall.sh) → 执行一次即可
↓
ipset 集合
↓
ASN 黑名单 (update_blacklists.sh) → 每周自动更新
P2P 黑名单 (update_blacklists.sh) → 每周自动更新

1.5.1 禁用 UFW#

如果你之前启用过 UFW,先关掉:

Terminal window
sudo ufw disable
sudo systemctl stop ufw
sudo systemctl disable ufw

1.5.2 创建防火墙部署脚本#

防火墙部署脚本只需执行一次(或修改端口配置后重新执行):

Terminal window
sudo nano /usr/local/bin/deploy_firewall.sh

粘贴以下脚本:

NOTE

注意修改开头的用户配置区:

deploy_firewall.sh
#!/bin/bash
# ============================================================================
# 部署防火墙规则,本脚本只需执行一次
#
# 修改配置后重新执行即可,脚本会自动覆盖旧规则
# ============================================================================
# ===================== 用户配置区 =====================
SSH_PORT=43210
# BT 监听端口(TCP 与 UDP 通常相同)
BT_TCP_PORT=54321
BT_UDP_PORT=54321
# Web 端口(80 用于 HTTP,443 用于 HTTPS)
# 如需放行其他端口请在此添加
WEB_PORTS=(80 443)
# 是否启用 IPv6(1=启用,0=禁用)
ENABLE_IPV6=1
# 邻居网段全局阻断(设为 "-" 表示不启用)
# 示例: NEIGHBOR_V4="10.0.0.0/8"
NEIGHBOR_V4="-"
NEIGHBOR_V6="-"
export PATH="/usr/sbin:/sbin:$PATH"
# ===================== 日志与颜色 =====================
RED='\033[1;31m'
GREEN='\033[1;32m'
YELLOW='\033[1;33m'
BLUE='\033[1;34m'
CYAN='\033[1;36m'
NC='\033[0m'
log() { echo -e "${BLUE}[INFO]${NC} $1"; }
ok() { echo -e "${GREEN}[OK]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
err() { echo -e "${RED}[ERR]${NC} $1"; }
die() { err "$1"; exit 1; }
# 检查 IPv6
check_v6() {
if [ "$ENABLE_IPV6" != "1" ]; then echo 0; return; fi
if [ -f /proc/net/if_inet6 ] && [ "$(wc -l < /proc/net/if_inet6)" -gt 0 ]; then
command -v ip6tables &>/dev/null && echo 1 || echo 0
else
echo 0
fi
}
# 依赖检查
check_deps() {
local missing=()
for cmd in ipset iptables; do
command -v "$cmd" &>/dev/null || missing+=("$cmd")
done
if [ "${#missing[@]}" -gt 0 ]; then
err "Missing dependencies: ${missing[*]}"
err "Run: apt install -y ipset iptables"
exit 1
fi
ok "Dependencies satisfied"
if [ "$HAS_V6" = "1" ] && ! command -v ip6tables &>/dev/null; then
warn "ip6tables not found, disabling IPv6"
HAS_V6=0
fi
}
# 安装 iptables-persistent 与 ipset-persistent
install_persistent() {
if dpkg -l iptables-persistent 2>/dev/null | grep -q '^ii' && dpkg -l ipset-persistent 2>/dev/null | grep -q '^ii'; then
return 0
fi
log "Installing iptables-persistent & ipset-persistent..."
echo iptables-persistent iptables-persistent/autosave_v4 boolean true \
| debconf-set-selections 2>/dev/null
echo iptables-persistent iptables-persistent/autosave_v6 boolean true \
| debconf-set-selections 2>/dev/null
DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent ipset-persistent &>/dev/null \
|| warn "Could not install persistent packages, will fallback to manual save"
}
# 创建 ipset 集合
create_ipsets() {
ipset create bad_asn_v4 hash:net family inet \
hashsize 4096 maxelem 500000 -exist 2>/dev/null
ipset create bad_p2p_v4 hash:net family inet \
hashsize 65536 maxelem 1000000 -exist 2>/dev/null
if [ "$HAS_V6" = "1" ]; then
ipset create bad_asn_v6 hash:net family inet6 \
hashsize 4096 maxelem 500000 -exist 2>/dev/null
fi
ok "ipset collections created"
}
# 部署 iptables 规则
deploy_v4() {
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
iptables -F
iptables -X
# 基础放行
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
# 全局阻断:邻居网段
if [ "$NEIGHBOR_V4" != "-" ]; then
iptables -A INPUT -s "$NEIGHBOR_V4" -j DROP
ok "Neighbor v4 blocked globally: $NEIGHBOR_V4"
fi
# 全局阻断:ASN & P2P 黑名单
iptables -A INPUT -m set --match-set bad_asn_v4 src -j DROP
iptables -A INPUT -m set --match-set bad_p2p_v4 src -j DROP
ok "Global blacklist: DROP(ASN) -> DROP(P2P)"
# SSH
iptables -A INPUT -p tcp --dport "$SSH_PORT" -j ACCEPT
ok "Allow TCP port $SSH_PORT (SSH)"
# Web (80, 443)
for port in "${WEB_PORTS[@]}"; do
iptables -A INPUT -p tcp --dport "$port" -j ACCEPT
ok "Allow TCP port $port (Web)"
done
# BT
iptables -A INPUT -p tcp --dport "$BT_TCP_PORT" -j ACCEPT
ok "Allow TCP port $BT_TCP_PORT (BT)"
iptables -A INPUT -p udp --dport "$BT_UDP_PORT" -j ACCEPT
ok "Allow UDP port $BT_UDP_PORT (BT DHT)"
}
# 部署 ip6tables 规则
deploy_v6() {
ip6tables -P INPUT DROP
ip6tables -P FORWARD DROP
ip6tables -P OUTPUT ACCEPT
ip6tables -F
ip6tables -X
ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
ip6tables -A INPUT -i lo -j ACCEPT
# 全局阻断:邻居网段
if [ "$NEIGHBOR_V6" != "-" ]; then
ip6tables -A INPUT -s "$NEIGHBOR_V6" -j DROP
ok "Neighbor v6 blocked globally: $NEIGHBOR_V6"
fi
# 全局阻断:ASN 黑名单
ip6tables -A INPUT -m set --match-set bad_asn_v6 src -j DROP
ok "Global blacklist: DROP(ASN)"
# SSH
ip6tables -A INPUT -p tcp --dport "$SSH_PORT" -j ACCEPT
# Web (80, 443)
for port in "${WEB_PORTS[@]}"; do
ip6tables -A INPUT -p tcp --dport "$port" -j ACCEPT
done
# BT
ip6tables -A INPUT -p tcp --dport "$BT_TCP_PORT" -j ACCEPT
ip6tables -A INPUT -p udp --dport "$BT_UDP_PORT" -j ACCEPT
}
# 持久化
persist() {
if command -v netfilter-persistent &>/dev/null; then
netfilter-persistent save 2>/dev/null
ok "Rules saved via netfilter-persistent"
else
mkdir -p /etc/iptables
ipset save > /etc/iptables/ipsets 2>/dev/null
iptables-save > /etc/iptables/rules.v4
[ "$HAS_V6" = "1" ] && ip6tables-save > /etc/iptables/rules.v6
ok "Rules saved to /etc/iptables/"
warn "Consider installing iptables-persistent for automatic restore on boot"
fi
}
# 打印当前规则
print_rules() {
echo ""
echo -e "${CYAN}=== IPv4 INPUT Rules ===${NC}"
iptables -L INPUT -v -n --line-numbers 2>/dev/null | head -20
if [ "$HAS_V6" = "1" ]; then
echo ""
echo -e "${CYAN}=== IPv6 INPUT Rules ===${NC}"
ip6tables -L INPUT -v -n --line-numbers 2>/dev/null | head -15
fi
}
# 主流程
main() {
if [ "$(id -u)" != "0" ]; then
die "This script must be run as root"
fi
echo ""
echo -e "${CYAN}========================================${NC}"
echo -e "${GREEN} Firewall Deployment (One-Time Setup)${NC}"
echo -e "${CYAN}========================================${NC}"
echo ""
HAS_V6=$(check_v6)
[ "$HAS_V6" = "1" ] && ok "IPv6 enabled" || warn "IPv6 disabled"
check_deps
create_ipsets
echo ""
log "Deploying IPv4 rules..."
deploy_v4
if [ "$HAS_V6" = "1" ]; then
echo ""
log "Deploying IPv6 rules..."
deploy_v6
fi
echo ""
log "Persisting rules..."
install_persistent
persist
print_rules
echo ""
echo -e "${GREEN}Firewall deployed.${NC}"
echo -e "${YELLOW}Next: run update_blacklists.sh to load ASN/P2P blacklists.${NC}"
echo ""
}
main "$@"

赋予执行权限并运行:

Terminal window
sudo chmod +x /usr/local/bin/deploy_firewall.sh
sudo /usr/local/bin/deploy_firewall.sh

脚本会:

  1. 创建 ipset 集合(bad_asn_v4、bad_p2p_v4、bad_asn_v6)
  2. 清空原有 iptables 规则并部署新规则
  3. 安装 iptables-persistent 和 ipset-persistent 并保存规则(重启后自动恢复)
NOTE

iptables 的匹配逻辑是 “自上而下,首次匹配即停止”。

本脚本将黑名单 DROP 规则写在开放端口 ACCEPT 规则的前面:命中黑名单的请求在前半部分就会被直接丢弃;只有未命中黑名单的正常请求,才会顺次走到后半部分被 443 / BT 端口 ACCEPT 放行。

此脚本只部署防火墙规则,不会下载黑名单数据。此时 ipset 集合为空,所有合法的 443 和 BT 请求均可正常访问。

1.5.3 创建黑名单更新脚本#

黑名单更新脚本负责下载 ASN/P2P 数据并更新 ipset 集合,不修改防火墙规则:

Terminal window
sudo nano /usr/local/bin/update_blacklists.sh

粘贴以下脚本:

update_blacklists.sh
#!/bin/bash
# ============================================================================
# 更新 ASN & P2P 黑名单
# ============================================================================
# 需要屏蔽的 ASN
ASNS=(
"14061|DigitalOcean"
"16509|AWS"
"15169|Google_Cloud"
"8075|Azure"
"20473|Vultr"
"31898|Oracle"
)
# P2P 黑名单数据源
declare -A P2P_SOURCES=(
["Naunter_Mega"]="https://raw.githubusercontent.com/Naunter/BT_BlockLists/master/bt_blocklists.gz"
["mxdpeep_Comprehensive"]="https://raw.githubusercontent.com/mxdpeep/p2p-blocklist-creator/master/blocklist.p2p"
["eMule_Security"]="http://upd.emule-security.org/ipfilter.zip"
)
# 缓存目录
CACHE_DIR="/var/lib/seedbox-firewall"
export PATH="/usr/sbin:/sbin:$PATH"
set -o pipefail
# ===================== 日志与颜色 =====================
RED='\033[1;31m'
GREEN='\033[1;32m'
YELLOW='\033[1;33m'
BLUE='\033[1;34m'
CYAN='\033[1;36m'
NC='\033[0m'
log() { echo -e "${BLUE}[INFO]${NC} $1"; }
ok() { echo -e "${GREEN}[OK]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
err() { echo -e "${RED}[ERR]${NC} $1"; }
die() { err "$1"; exit 1; }
ln() { echo "------------------------------"; }
fmt() { printf "%'d" "$1"; }
# 检查 IPv6
check_v6() {
if [ -f /proc/net/if_inet6 ] && [ "$(wc -l < /proc/net/if_inet6)" -gt 0 ]; then
command -v ip6tables &>/dev/null && echo 1 || echo 0
else
echo 0
fi
}
# 依赖检查
check_deps() {
local missing=()
for cmd in curl gunzip unzip awk python3 ipset whois md5sum sha256sum; do
command -v "$cmd" &>/dev/null || missing+=("$cmd")
done
if [ "${#missing[@]}" -gt 0 ]; then
die "Missing: ${missing[*]}. Run: apt install -y curl gzip unzip gawk python3 ipset iproute2 whois"
fi
# 检查 ipsets 是否存在
if ! ipset list bad_asn_v4 &>/dev/null || ! ipset list bad_p2p_v4 &>/dev/null; then
die "ipsets not found. Run deploy_firewall.sh first."
fi
}
# ===================== 缓存函数 =====================
init_cache() {
mkdir -p "$CACHE_DIR" 2>/dev/null || { die "Cannot create $CACHE_DIR"; }
}
get_cached_hash() {
local name="$1"
local hashfile="${CACHE_DIR}/${name}.sha256"
[ -f "$hashfile" ] && cat "$hashfile" || echo ""
}
set_cached_hash() {
local name="$1" hash="$2"
echo "$hash" > "${CACHE_DIR}/${name}.sha256"
}
is_changed() {
local name="$1" cidr_file="$2"
if [ ! -s "$cidr_file" ]; then
return 0 # 空文件视为有变化(或错误)
fi
local new_hash=$(sha256sum "$cidr_file" | awk '{print $1}')
local old_hash=$(get_cached_hash "$name")
if [ "$new_hash" = "$old_hash" ] && [ -n "$old_hash" ]; then
return 1 # 未变化
fi
return 0 # 有变化
}
update_cache() {
local name="$1" cidr_file="$2"
local new_hash=$(sha256sum "$cidr_file" | awk '{print $1}')
set_cached_hash "$name" "$new_hash"
}
# ===================== ASN 更新 =====================
update_asn() {
log "Fetching ASN routes from RADb..."
local work="/tmp/asn-update-$$"
mkdir -p "$work"
local v4_cidr="${CACHE_DIR}/asn_v4.cidr"
local v6_cidr="${CACHE_DIR}/asn_v6.cidr"
local v4_tmp="${work}/asn_v4.cidr"
local v6_tmp="${work}/asn_v6.cidr"
true > "$v4_tmp"
[ "$HAS_V6" = "1" ] && true > "$v6_tmp"
local v4_total=0 v6_total=0
for ITEM in "${ASNS[@]}"; do
ASN="${ITEM%%|*}"
NAME="${ITEM##*|}"
local whois_out="${work}/whois_${ASN}.txt"
if ! timeout 20 whois -h whois.radb.net -- "-i origin AS${ASN}" > "$whois_out" 2>/dev/null; then
warn "AS${ASN} ${NAME}: whois timeout or failed"
rm -f "$whois_out"
continue
fi
# 提取 v4 路由
local v4_count=$(awk '/^route:/ {print $2}' "$whois_out" | grep -v '^$' | tee -a "$v4_tmp" | wc -l)
v4_total=$((v4_total + v4_count))
if [ "$HAS_V6" = "1" ]; then
local v6_count=$(awk '/^route6:/ {print $2}' "$whois_out" | grep -v '^$' | tee -a "$v6_tmp" | wc -l)
v6_total=$((v6_total + v6_count))
fi
log " AS${ASN} ${NAME}: v4=${v4_count} v6=${HAS_V6:+"$v6_count"}"
rm -f "$whois_out"
done
# 更新 v4
local v4_updated=0
if [ "$v4_total" -lt 50 ]; then
warn "ASN v4 too few entries ($v4_total), keeping existing set"
elif is_changed "asn_v4" "$v4_tmp"; then
log "ASN v4 changed, swapping ipset..."
local new_set="bad_asn_v4_new"
ipset create "$new_set" hash:net family inet hashsize 4096 maxelem 500000 -exist 2>/dev/null
{
echo "create $new_set hash:net family inet hashsize 4096 maxelem 500000 -exist"
awk '{print "add '"$new_set"' " $0}' "$v4_tmp"
} | ipset restore -exist 2>/dev/null
ipset swap "$new_set" bad_asn_v4
ipset destroy "$new_set" 2>/dev/null
cp "$v4_tmp" "$v4_cidr"
update_cache "asn_v4" "$v4_tmp"
v4_updated=1
ok "ASN v4 updated: $(fmt $(wc -l < "$v4_cidr")) entries"
else
ok "ASN v4 unchanged ($(fmt $v4_total) entries), skipped"
fi
# 更新 v6
if [ "$HAS_V6" = "1" ]; then
if [ "$v6_total" -lt 50 ]; then
warn "ASN v6 too few entries ($v6_total), keeping existing set"
elif is_changed "asn_v6" "$v6_tmp"; then
log "ASN v6 changed, swapping ipset..."
local new_set="bad_asn_v6_new"
ipset create "$new_set" hash:net family inet6 hashsize 4096 maxelem 500000 -exist 2>/dev/null
{
echo "create $new_set hash:net family inet6 hashsize 4096 maxelem 500000 -exist"
awk '{print "add '"$new_set"' " $0}' "$v6_tmp"
} | ipset restore -exist 2>/dev/null
ipset swap "$new_set" bad_asn_v6
ipset destroy "$new_set" 2>/dev/null
cp "$v6_tmp" "$v6_cidr"
update_cache "asn_v6" "$v6_tmp"
ok "ASN v6 updated: $(fmt $(wc -l < "$v6_cidr")) entries"
else
ok "ASN v6 unchanged ($(fmt $v6_total) entries), skipped"
fi
fi
rm -rf "$work"
}
# ===================== P2P 更新 =====================
download_p2p_source() {
local name="$1" url="$2" out="$3"
printf " %-25s " "${name}..."
if ! curl -sL --connect-timeout 15 --max-time 120 --retry 3 --retry-delay 2 \
"$url" -o "$out"; then
echo -e "${RED}FAIL${NC}"
return 1
fi
# Auto-detect compression and decompress
if command -v file &>/dev/null; then
if file "$out" | grep -q "gzip compressed"; then
mv "$out" "${out}.gz"
gunzip -c "${out}.gz" > "$out" 2>/dev/null
rm -f "${out}.gz"
elif file "$out" | grep -q "Zip archive"; then
mv "$out" "${out}.zip"
unzip -p "${out}.zip" > "$out" 2>/dev/null
rm -f "${out}.zip"
fi
fi
if [ -s "$out" ]; then
echo -e "${GREEN}OK${NC} ($(fmt $(wc -l < "$out")) lines)"
return 0
fi
echo -e "${RED}FAIL${NC}"
return 1
}
update_p2p() {
log "Fetching P2P blocklists..."
local work="/tmp/p2p-update-$$"
mkdir -p "$work/cache"
local raw="${work}/raw.tmp"
local cidr_out="${CACHE_DIR}/p2p_v4.cidr"
local cidr_tmp="${work}/p2p_v4.cidr"
> "$raw"
local ok_count=0 fail_count=0
for name in "${!P2P_SOURCES[@]}"; do
local url="${P2P_SOURCES[$name]}"
local cache_file="${work}/cache/$(echo "$url" | md5sum | cut -d' ' -f1).dat"
if download_p2p_source "$name" "$url" "$cache_file"; then
cat "$cache_file" >> "$raw"
((ok_count++))
else
((fail_count++))
fi
done
if [ "$ok_count" -eq 0 ]; then
err "All P2P sources failed, keeping existing ipset"
rm -rf "$work"
return 1
fi
log "P2P raw lines: $(fmt $(wc -l < "$raw")) | ${ok_count} OK, ${fail_count} failed"
# 解析 IP 范围(支持 range 和 CIDR 格式)
local processed="${work}/processed.tmp"
awk '
function ip2dec(ip) {
split(ip, a, ".")
return (a[1]*16777216) + (a[2]*65536) + (a[3]*256) + a[4]
}
{
content = $0
if ($0 ~ /^[A-Za-z0-9_]+:/) {
split($0, p, ":")
content = p[2]
}
# IP-range: 1.2.3.4-5.6.7.8
if (match(content, /[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+-[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)) {
split(substr(content, RSTART, RLENGTH), ips, "-")
s = ip2dec(ips[1]); e = ip2dec(ips[2])
if (s <= e) print s "," e
}
# CIDR: 1.2.3.0/24
else if (match(content, /[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\/[0-9]+/)) {
split(substr(content, RSTART, RLENGTH), parts, "/")
s = ip2dec(parts[1])
e = s + (2 ^ (32 - parts[2])) - 1
print s "," e
}
}' "$raw" | sort -t',' -k1,1n > "$processed"
local range_count=$(wc -l < "$processed")
# 合并连续范围,转换为 CIDR
awk -F',' '
function dec2ip(d) {
return sprintf("%d.%d.%d.%d",
int(d/16777216)%256, int(d/65536)%256, int(d/256)%256, d%256)
}
NR==1 { s=$1; e=$2; next }
$1<=e+1 { if($2>e) e=$2; next }
{ printf "%s %s\n", dec2ip(s), dec2ip(e); s=$1; e=$2 }
END { if(NR>0) printf "%s %s\n", dec2ip(s), dec2ip(e) }
' "$processed" | sort -u | python3 -c "
import sys, ipaddress
for line in sys.stdin:
start, end = line.strip().split()
for net in ipaddress.summarize_address_range(
ipaddress.IPv4Address(start), ipaddress.IPv4Address(end)
):
print(net)
" > "$cidr_tmp"
local final_count=$(wc -l < "$cidr_tmp")
local savings=$((range_count - final_count))
local pct=$((range_count > 0 ? (savings * 100 / range_count) : 0))
log "P2P merged: $(fmt $final_count) CIDR entries (${pct}% reduction)"
# 缓存检查 + 原子更新
if is_changed "p2p_v4" "$cidr_tmp"; then
log "P2P data changed, swapping ipset..."
local new_set="bad_p2p_v4_new"
ipset create "$new_set" hash:net family inet hashsize 65536 maxelem 1000000 -exist 2>/dev/null
{
echo "create $new_set hash:net family inet hashsize 65536 maxelem 1000000 -exist"
awk '{print "add '"$new_set"' " $0}' "$cidr_tmp"
} | ipset restore -exist 2>/dev/null
ipset swap "$new_set" bad_p2p_v4
ipset destroy "$new_set" 2>/dev/null
cp "$cidr_tmp" "$cidr_out"
update_cache "p2p_v4" "$cidr_tmp"
ok "P2P v4 updated: $(fmt $final_count) entries"
else
ok "P2P v4 unchanged ($(fmt $final_count) entries), skipped"
fi
rm -rf "$work"
}
# ===================== 打印统计 =====================
print_summary() {
echo ""
ln
local asn_v4=$(ipset list bad_asn_v4 2>/dev/null | grep "Number of entries" | awk '{print $NF}')
local p2p_v4=$(ipset list bad_p2p_v4 2>/dev/null | grep "Number of entries" | awk '{print $NF}')
echo " ASN v4: ${asn_v4:-0} entries"
echo " P2P v4: ${p2p_v4:-0} entries"
if [ "$HAS_V6" = "1" ]; then
local asn_v6=$(ipset list bad_asn_v6 2>/dev/null | grep "Number of entries" | awk '{print $NF}')
echo " ASN v6: ${asn_v6:-0} entries"
fi
ln
}
# ===================== 主流程 =====================
main() {
if [ "$(id -u)" != "0" ]; then
die "This script must be run as root"
fi
echo ""
echo -e "${CYAN}========================================${NC}"
echo -e "${GREEN} Blacklist Update${NC}"
echo -e "${CYAN}========================================${NC}"
echo ""
HAS_V6=$(check_v6)
[ "$HAS_V6" = "1" ] && ok "IPv6 enabled" || warn "IPv6 disabled"
check_deps
init_cache
echo ""
log "Updating ASN blacklists..."
update_asn
echo ""
log "Updating P2P blacklists..."
update_p2p
print_summary
echo -e "${GREEN}Blacklist update complete.${NC}"
echo ""
}
main "$@"

赋予执行权限:

Terminal window
sudo chmod +x /usr/local/bin/update_blacklists.sh

脚本说明:

  • 原子更新:创建新集合 → 导入数据 → ipset swap 切换 → 销毁旧集合,切换耗时毫秒级,更新期间不会出现空集合窗口
  • 缓存:将解析后的 CIDR 列表存入 /var/lib/seedbox-firewall/,下次运行时对比 SHA256,数据未变化则跳过更新
  • 容错:下载失败不销毁现有集合,部分源失败不影响已成功的更新

1.5.4 首次加载黑名单#

Terminal window
sudo /usr/local/bin/update_blacklists.sh

首次运行会下载并导入所有 ASN 和 P2P 黑名单(目前约 47+ 万条),耗时取决于网络和 VPS 性能

NOTE

由于脚本采用原子 swap 更新,首次运行时 deploy_firewall.sh 创建的集合可能为空,需要等待导入完成后才会有数据

后续每周运行时,当前集合始终保持有效,不受更新过程影响

1.5.5 设置自动更新#

P2P 黑名单更新频率较低,建议每周执行一次:

Terminal window
sudo crontab -e

末尾添加一行(每周日凌晨 4 点):

Terminal window
0 4 * * 0 /usr/local/bin/update_blacklists.sh > /var/log/blacklist-update.log 2>&1

如果你希望更高频率,改为每日:

Terminal window
0 4 * * * /usr/local/bin/update_blacklists.sh > /var/log/blacklist-update.log 2>&1
NOTE

该 cron 任务只更新 ipset 数据,不修改防火墙规则、不会中断网络连接

1.5.6 验证规则#

Terminal window
sudo iptables -L INPUT -v -n --line-numbers

预期输出类似(bad_asn_v4 与 bad_p2p_v4 为全局 DROP,排在端口放行规则之前):

Chain INPUT (policy DROP)
num pkts bytes target prot opt in out source destination
1 0 0 ACCEPT 0 -- * * 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
2 0 0 ACCEPT 0 -- lo * 0.0.0.0/0 0.0.0.0/0
3 0 0 DROP 0 -- * * 0.0.0.0/0 0.0.0.0/0 match-set bad_asn_v4 src
4 0 0 DROP 0 -- * * 0.0.0.0/0 0.0.0.0/0 match-set bad_p2p_v4 src
5 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:43210
6 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:80
7 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:443
8 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:54321
9 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:54321
NOTE

iptables 规则链的匹配逻辑是 “自上而下,首次匹配即停止”。

bad_asn_v4 和 bad_p2p_v4 规则为全局 DROP,位列所有开放端口(SSH / 80 / 443 / BT)规则的前面。

命中黑名单的请求会在规则 3/4 直接被拦截丢弃,不会触及后续的端口放行规则;只有未命中黑名单的正常请求才能顺次匹配到后面的端口 ACCEPT 规则。

验证 ipset 集合数据量:

Terminal window
sudo ipset list bad_asn_v4 | grep "Number of entries"
sudo ipset list bad_p2p_v4 | grep "Number of entries"

预期输出示例:

Number of entries: 3421
Number of entries: 468200

二、 安装应用与反代网关#

所有业务应用均在普通用户下运行,应用启动后,统一由 Caddy 接管公网流量

2.1 目录初始化#

以普通用户(如 yourname)登录,执行:

Terminal window
# 数据落盘区
mkdir -p ~/downloads/anime
mkdir -p ~/downloads/animetemp
mkdir -p ~/downloads/bd_archive

2.2 安装并配置 qBittorrent EE#

2.2.1 安装 qBEE#

Terminal window
# 安装依赖
sudo apt install -y unzip
# 下载静态编译包(一般选择 x86_64 架构即可)
wget https://github.com/c0re100/qBittorrent-Enhanced-Edition/releases/latest/download/qbittorrent-enhanced-nox_x86_64-linux-musl_static.zip
# 解压
unzip qbittorrent-enhanced-nox_x86_64-linux-musl_static.zip
# 将二进制文件移动到本机软件目录并赋予执行权限
sudo mv qbittorrent-nox /usr/local/bin/qbittorrent-nox
sudo chmod +x /usr/local/bin/qbittorrent-nox
# 清理安装包
rm qbittorrent-enhanced-nox_x86_64-linux-musl_static.zip

2.2.2 编写服务#

Terminal window
sudo nano /etc/systemd/system/qbittorrent.service

将以下内容粘贴进去:

[Unit]
Description=qBittorrent Command Line Client
After=network.target
[Service]
Type=exec
User=yourname # 改为你的用户名
Group=yourname # 改为你的用户名
UMask=002
ExecStart=/usr/local/bin/qbittorrent-nox
Restart=on-failure
[Install]
WantedBy=multi-user.target
TIP

UMask=002 能确保 qBEE 下载的文件,默认权限是 775 (目录)和 664 (文件)

对于后续 OpenList 读取或者 rclone 拖回本地都极其友好,不会卡权限

保存并退出(Ctrl+O 回车,Ctrl+X)后,拉起服务:

Terminal window
sudo systemctl enable --now qbittorrent

然后停止以生成配置文件:

Terminal window
sudo systemctl stop qbittorrent

nano 打开 ~/.config/qBittorrent/qBittorrent.conf,在 [Preferences] 区域添加/修改:

WebUI\Address=127.0.0.1

此操作将面板死锁在本地环回地址

顺便将其中的Session\Port=54321 记下来,该端口就是上面提到的 BT 监听端口

2.3 安装并配置 PeerBanHelper (PBH)#

接下来保持 qBEE 不启动,继续配置 PBH

2.3.1 配置 Java 25 运行环境#

目前PBH 最新版需要 Java 25,先下载一个纯净绿色的 Java 25 放到 /opt 下专门给 PBH 用:

Terminal window
# 创建 Java 25 目录
sudo mkdir -p /opt/java25
# 下载 Java 25
wget -O jdk25.tar.gz "https://api.adoptium.net/v3/binary/latest/25/ga/linux/x64/jdk/hotspot/normal/eclipse"
# 解压到专属目录
sudo tar -xzf jdk25.tar.gz -C /opt/java25 --strip-components=1
# 删除压缩包
rm jdk25.tar.gz

2.3.2 下载并解压 PBH#

让程序和数据都待在之前创建的 yourname 专属目录里:

Terminal window
# 创建并进入专属配置目录
mkdir -p /home/yourname/seedbox/config/pbh
cd /home/yourname/seedbox/config/pbh
# 下载安装包
wget https://github.com/PBH-BTN/PeerBanHelper/releases/download/v9.3.14/PeerBanHelper_9.3.14.zip
# 解压
unzip PeerBanHelper_9.3.14.zip
# 删掉安装包
rm PeerBanHelper_9.3.14.zip

2.3.3 编写 Systemd 守护进程#

Terminal window
sudo nano /etc/systemd/system/peerbanhelper.service

将下面的内容粘贴进去(强制以 yourname 用户运行,并指定使用我们刚准备好的 Java 25):

[Unit]
Description=PeerBanHelper Daemon
After=network.target
[Service]
Type=simple
User=yourname # 改为你的用户名
Group=yourname # 改为你的用户名
WorkingDirectory=/home/yourname/seedbox/config/pbh/PeerBanHelper
ExecStart=/opt/java25/bin/java -jar /home/yourname/seedbox/config/pbh/PeerBanHelper/PeerBanHelper.jar
Restart=on-failure
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target

保存并退出

2.4 安装并配置 OpenList (流媒体面板)#

  1. 二进制部署:
Terminal window
# 1. 创建 OpenList 专属工作目录
mkdir -p /home/yourname/seedbox/config/openlist
cd /home/yourname/seedbox/config/openlist
# 2. 下载 OpenList 最新的 linux-amd64 包
wget https://github.com/OpenListTeam/OpenList/releases/latest/download/openlist-linux-amd64.tar.gz
# 3. 解压并赋予权限
tar -zxf openlist-linux-amd64.tar.gz
sudo mv openlist /usr/local/bin/openlist
sudo chmod +x /usr/local/bin/openlist
# 4. 清理安装包
rm openlist-linux-amd64.tar.gz
  1. Systemd 守护:

编写守护进程

Terminal window
sudo nano /etc/systemd/system/openlist.service

将以下内容粘贴进去:

[Unit]
Description=OpenList Web File Manager
After=network.target
[Service]
Type=simple
User=yourname # 改为你的用户名
Group=yourname # 改为你的用户名
WorkingDirectory=/home/yourname/seedbox/config/openlist
ExecStart=/usr/local/bin/openlist server
Restart=on-failure
[Install]
WantedBy=multi-user.target

保存并退出

2.5 部署 Caddy 网关统一反代#

各面板现已隐藏在 127.0.0.1,使用 Caddy 将其安全发布并自动维护 HTTPS 证书

总共需要在 Cloudflare 上配置三个子域名

2.5.1 Cloudflare DNS 设置#

登录 Cloudflare 控制台,为你的主域名 <yourdomain.com> 增添三个 A 记录:

  1. qbee -> 指向 Seedbox 的公网 IP
  2. openlist -> 指向 Seedbox 的公网 IP
  3. pbh -> 指向 Seedbox 的公网 IP
NOTE

这三个域名的“代理状态”全部请保持为 小灰云(仅限 DNS)

后续 OpenList 传输大流量视频时直连跑满带宽,不走 Cloudflare CDN,以免违反 TOS 导致封号

2.5.2 安装 Caddy#

使用 Caddy 官方针对 Debian 的软件源进行原生安装:

Terminal window
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
# 导入 GPG 密钥
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
# 添加官方软件源
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
# 更新并安装
sudo apt update
sudo apt install caddy -y
# 创建 Caddy 专属日志目录并赋予 caddy 用户权限
sudo mkdir -p /var/log/caddy
sudo chown -R caddy:caddy /var/log/caddy
sudo chmod 755 /var/log/caddy

2.5.3 配置 Caddyfile 路由#

清空再打开 Caddyfile:

Terminal window
sudo > /etc/caddy/Caddyfile
sudo nano /etc/caddy/Caddyfile

将下面这段配置粘贴进去:

Caddyfile-template
# 1. qBittorrent Enhanced Edition 面板
qbee.yourdomain.com {
reverse_proxy localhost:8080
log {
output file /var/log/caddy/qbee_access.log {
roll_size 10mib
roll_keep 7
roll_keep_for 168h
}
}
}
# 2. OpenList 媒体看番面板
openlist.yourdomain.com {
reverse_proxy localhost:5244
log {
output file /var/log/caddy/openlist_access.log {
roll_size 10mib
roll_keep 7
roll_keep_for 168h
}
}
}
# 3. PeerBanHelper 管理面板
pbh.yourdomain.com {
reverse_proxy localhost:9898
log {
output file /var/log/caddy/pbh_access.log {
roll_size 10mib
roll_keep 7
roll_keep_for 168h
}
}
}

保存并退出,然后启动 Caddy 服务:

TIP

Caddy 会在首次启动时自动向 Let’s Encrypt 申请免费 SSL 证书

为三个子域名启用 HTTPS,无需任何额外配置

Terminal window
sudo systemctl enable --now caddy

2.6 限制 Systemd Journal 日志大小#

Caddy 的访问日志已经在 Caddyfile 中配置了原生内置的日志滚动机制(roll_size、roll_keep 等参数),无需额外配置系统的 logrotate 脚本。

但是 Systemd 的日志(journald)默认仅按系统空闲空间比例限制大小,需要手动限制最大占用空间。

TIP

如果不需要保留访问日志,或者磁盘空间十分有限,可以直接将 Caddyfile 中的整个 log { ... } 代码块移除。

Systemd 的日志(journald)默认只按体积轮转但不设硬性上限,需要手动限制最大占用空间。

Terminal window
sudo nano /etc/systemd/journald.conf

找到并取消注释、修改以下两行(若不存在则手动添加):

/etc/systemd/journald.conf
SystemMaxUse=100M
MaxFileSec=1week

保存后重启服务,使新配置生效:

Terminal window
sudo systemctl restart systemd-journald

三、启动并配置应用#

基础设施已就绪,现在可以正式启动 qBEE 和 PBH :

3.1 启动 qBEE#

Terminal window
# 启动 qBEE
sudo systemctl start qbittorrent
# 查看并记录首次启动的随机密码
sudo journalctl -u qbittorrent | grep -i password

此时,在本地 Windows 的浏览器中进入 https://qbee.yourdomain.com

初始账号为 admin,使用刚才抓取的随机密码登录进去后

首先去 Tools -> Options -> Web UI 修改随机初始密码与账户名(不推荐继续使用 admin)

假设修改为:

user: yourname
password: 12345678 # 仅作教程示例,请使用强密码!

保存后,按 F5 刷新网址重新登陆一遍即可

接着,根据个人喜好进行设置,VPS 配置较低的用户可参考文末的【附:qBEE 参考配置】

默认保存路径(完成时):/home/yourname/downloads/anime
默认下载路径(未完成的种子):/home/yourname/downloads/animetemp

3.2 启动 PBH#

Terminal window
sudo systemctl enable --now peerbanhelper
sudo systemctl status peerbanhelper

在本地 Windows 的浏览器中直接输入 https://pbh.yourdomain.com

登录 token 为随机生成,建议使用浏览器密码管理器保存

接下来跟随界面指引,创建 BTN 账号或登录已有账号(输入已有的 App ID 和 Secret)

接着连接下载器,填下以下信息(未提及的保持默认)

类型:qBittorrentEE
名称:随便
地址:http://127.0.0.1:8080
用户名:yourname
密码:12345678 # 仅作教程示例,请使用强密码!

接着前往 PBH 的设置页->基础设置->WebUI,将前缀(Prefix)改为https://pbh.yourdomain.com

因为我们使用了 Caddy 反代,这里需要修改为外部可以访问的域名,否则 PBH 无法把封禁列表传给 qBEE

(可选)继续往下滑,调整封禁日志保留时间默认存半年的日志,建议改成 15 天或 30 天即可

(可选)屏蔽 CN IP

去年发现热门种子容易被国内用户刷下行以平衡上行,可按需在 PBH 中屏蔽所有来自 CN 的 IP:设置 → 规则订阅 → 地区 → 新增 → 输入 CN(必须大写)

NOTE

PBH 已通过行为检测(BTN 规则、进度判定、吸血识别)自动封禁恶意 Peer,一般无需额外按地理范围封禁。开启后会切断中文社区做种者的连接,可能影响做种效率,酌情使用

3.3 启动 OpenList#

Terminal window
# 拉起并设置开机自启
sudo systemctl daemon-reload
sudo systemctl enable --now openlist
# 进入 OpenList 工作目录并设定管理员密码(复杂强密码需要用单引号包裹)
cd /home/yourname/seedbox/config/openlist
/usr/local/bin/openlist admin set '1111111111111' # 此处仅示例,实际请改为强密码!

在 Windows 浏览器打开 https://openlist.yourdomain.com

登录后,在 管理 -> 用户,找到 guest 账号,点击编辑并勾选 停用,禁止游客登陆;

接着在个人资料将初始用户名 admin 修改为 yourname;

再接着挂载由 qBEE 下好的视频: 存储 -> 添加

驱动:本机存储
挂载路径:/动漫
根文件夹路径: /home/yourname/downloads/anime

点击最底部的 添加 即可保存

最后加固一下,设置 -> 站点,将底部的 Robots.txt 改为

User-agent: *
Disallow: /
NOTE

浏览器中 OpenList 暂不支持解析 MKV 容器的内嵌软字幕

遇到已经内嵌软字幕的文件播放却无字幕的情况,请尝试点击视频页面下方小按钮跳转到外部播放器即可

PC 端推荐 MPV,友情推荐一个关联小工具:

outlook84
/
mpv-handler-openlist
Waiting for api.github.com...
00K
0K
0K
Waiting...

Android 端则推荐 VLC 或 MX Player

对于内嵌软字幕的 MKV,安卓端目前仅测试了 VLC 支持解析;

同时,在视频页面直接点击 VLC 图标,VLC 无法解析视频链接,可以尝试点击视频页面的安卓图标按钮,然后再选择 VLC

对于外挂字幕,目前没找到自动加载的办法,只能单独下载字幕文件到本地,再通过播放器手动选择字幕

详情可查看:OpenList 播放内嵌软字幕与外挂字幕视频的小技巧


四、 进阶可选配置#

4.1 更换 WebUI 为 VueTorrent#

如果看腻了 qBEE 的默认样式,可尝试更换为 VueTorrent

VueTorrent
/
VueTorrent
Waiting for api.github.com...
00K
0K
0K
Waiting...
Terminal window
cd ~
# 下载构建产物
wget https://github.com/VueTorrent/VueTorrent/releases/latest/download/vuetorrent.zip
# 解压
unzip vuetorrent.zip
# 清理
rm vuetorrent.zip

接着登录 qBEE WebUI,进入 设置 -> WebUI ->

启用备用 WebUI,输入:

/home/yourname/vuetorrent

完成后刷新网页即可

CAUTION

若路径填写错误,WebUI 会返回「不支持的文件类型」且无法再进入页面

此时通过 SSH 编辑 nano ~/.config/qBittorrent/qBittorrent.conf

将 WebUI\AlternativeUIEnabled=true 改为 false 即可恢复默认界面

或者将 WebUI\RootFolder= 改为正确的 VueTorrent 路径即可恢复正常

4.2 自动清理过期番剧#

qBEE 下载的番剧会长期占用磁盘空间,用一个脚本定期扫描 anime 目录,将超过 120 天且未在 qBEE 中做种的旧番自动删除,防止磁盘被撑爆

NOTE

触发条件:文件最后修改时间距今超过 EXPIRE_DAYS(默认 120 天),且该文件名不在 qBEE 当前种子列表中

安装依赖并编写脚本:

Terminal window
sudo apt install -y jq
cd ~ && mkdir -p script && cd script && nano qb_cleaner.sh

填入以下内容(注意将 QB_PASS 和 PORT 改为你的实际值):

#!/bin/bash
QB_USER="yourname"
QB_PASS='你的qB WebUI密码'
# qBittorrent WebUI 端口(默认 8080),非 BT 监听端口
PORT="8080"
API_URL="http://127.0.0.1:${PORT}/api/v2"
COOKIE_FILE="/tmp/qb_cookies_cleaner.txt"
ANIME_DIR="/home/yourname/downloads/anime"
BD_ARCHIVE_DIR="/home/yourname/downloads/bd_archive"
EXPIRE_DAYS=120
# 检查 jq 是否安装
if ! command -v jq &>/dev/null; then
echo "[ERROR] jq is not installed. Run: sudo apt install -y jq"
exit 1
fi
# 登录 qB API 并获取所有种子名称
curl -s -c "$COOKIE_FILE" \
--data-urlencode "username=$QB_USER" \
--data-urlencode "password=$QB_PASS" \
"${API_URL}/auth/login" > /dev/null
ACTIVE_NAMES=$(curl -s -b "$COOKIE_FILE" \
"${API_URL}/torrents/info" | jq -r '.[] | .name' 2>/dev/null)
# 若获取做种列表失败或为空则直接退出,防止误删做种文件
if [ -z "$ACTIVE_NAMES" ]; then
echo "[ERROR] Failed to fetch active torrent list or list is empty. Aborting cleanup for safety."
rm -f "$COOKIE_FILE"
exit 1
fi
# 扫描过期的目录/文件,跳过仍在做种的
find "$ANIME_DIR" -mindepth 1 -maxdepth 1 -mtime +$EXPIRE_DAYS | while read -r ITEM; do
if [[ "$ITEM" == *"$BD_ARCHIVE_DIR"* ]]; then continue; fi
BASE_NAME=$(basename "$ITEM")
if echo "$ACTIVE_NAMES" | grep -F -x -q "$BASE_NAME"; then
continue
else
rm -rf "$ITEM"
fi
done
rm -f "$COOKIE_FILE"

赋予执行权限:

Terminal window
chmod +x ~/script/qb_cleaner.sh

接着每日清晨 8 点自动执行:

Terminal window
crontab -e

写入:

0 8 * * * /home/yourname/script/qb_cleaner.sh >> ~/script/qb_cleaner.log 2>&1

4.3 优化线路跳转(Mihomo 代理转发)#

假设,你的 Seedbox 在 LA,但是线路很差,但是恰好你又有另一台 LA 优化线路的机,并且更巧还搭建好了魔法,那么可利用本地 Mihomo 客户端将优化节点作为 SSH 跳板

如果你的 mihomo rules 最后是 Match, Proxy,那当你访问 Seedbox 时,大概率会被自动捕获并走 proxy,但我们可以简单加一个 tunnel 字段,使得 mihomo 不必每次都去匹配 rules:

  1. 在 Mihomo 配置文件中添加顶级字段 tunnels:
tunnels:
# 类型,监听本地端口,目标地址与端口,出站节点/节点组
# 将目标地址与端口改为你seedbox的ip与ssh端口
- tcp,127.0.0.1:33333,your_vps_ip:43210,魔法节点/魔法节点策略组
  1. 接着在本地添加 ssh config 配置块:
PowerShell 7
notepad $HOME/.ssh/config
Host seedbox-mihomo
HostName 127.0.0.1
Port 33333
User yourname
IdentityFile ~/.ssh/id_ed25519_seedbox

倘若顺利,当你执行 ssh seedbox-mihomo 时,mihomo 日志显示的连接类型应为 Tunnel

此方法需要 mihomo 保持启动状态,更多玩法自行研究吧

4.4 配置下载通知(Telegram Bot)#

在本地 Windows 上我习惯使用下载完成后发送电子邮件通知,但 VPS 环境下配置 SMTP 既繁琐又不安全,因此我使用 Telegram Bot 进行通知

前置条件:拥有一个 Telegram 账号

4.4.1 创建 Telegram Bot#

在 Telegram 中搜索 @BotFather,依次发送以下指令:

  1. /newbot 创建新 Bot
  2. 输入 Bot 显示名称(任意)
  3. 输入 Bot 用户名(必须以 bot 结尾)

完成后 BotFather 会返回一段消息,其中包含:

Use this token to access the HTTP API:
1234567890:BBA2XXjn4YjgHmPqdvd-nK7AbuvCkuSVPzs

妥善保存此 token,持有它即可控制你的 Bot

然后在 TG 搜索你刚创建的 Bot 用户名,将其添加到聊天列表

4.4.2 获取 Chat ID#

第三方 TG 客户端可以直接看到自己的 ID

往你的 Bot 发送一条任意消息(如 hello),然后在浏览器访问以下网址(替换 TOKEN):

https://api.telegram.org/bot<TOKEN>/getUpdates

返回的 JSON 中 "chat":{"id":123123123} 即为你的 Chat ID,记录下来

4.4.3 创建通知脚本#

Terminal window
sudo apt install -y bc
cd ~/script && nano tg_notify.sh
#!/bin/bash
# 配置你的 token 和 ID
BOT_TOKEN="YOUR_BOT_TOKEN"
CHAT_ID="YOUR_CHAT_ID"
# ==========================================
# 接收 qB 传过来的参数
TORRENT_NAME="${1:-"Unknown Torrent"}"
SIZE_BYTES="${2:-0}"
SAVE_PATH="${3:-"Unknown Path"}"
SAFE_NAME=$(echo "$TORRENT_NAME" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')
SAFE_PATH_RAW=$(echo "$SAVE_PATH" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')
SIZE_MB=$((SIZE_BYTES / 1024 / 1024))
if [ "$SIZE_MB" -ge 1024 ]; then
SIZE_GB=$(echo "scale=1; $SIZE_MB / 1024" | bc)
SIZE_DISPLAY="${SIZE_GB} GB"
else
SIZE_DISPLAY="${SIZE_MB} MB"
fi
USERNAME=$(whoami)
SAVE_PATH_OBFUSCATED=$(echo "$SAFE_PATH_RAW" | sed "s|${USERNAME}|<span class=\"tg-spoiler\">${USERNAME}</span>|g")
MESSAGE="🎉 <b>Seedbox 种子下载完成!</b>
🎬 <b>名称</b>: ${SAFE_NAME}
💾 <b>大小</b>: ${SIZE_DISPLAY}
📁 <b>路径</b>: ${SAVE_PATH_OBFUSCATED}"
curl -s --max-time 10 -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
-d chat_id="${CHAT_ID}" \
--data-urlencode text="${MESSAGE}" \
-d parse_mode="HTML" > /dev/null

赋予执行权限:

Terminal window
chmod +x ~/script/tg_notify.sh

4.4.4 绑定到 qBEE#

打开 qBEE WebUI → 设置 → 下载,勾选 种子下载完成时运行外部程序,填入:

/home/yourname/script/tg_notify.sh "%N" "%Z" "%D"

%N = 种子名称,%Z = 文件大小(字节),%D = 保存路径

4.4.5 预期效果#

当种子下载完成时,你将在 Telegram 收到:

🎉 Seedbox 下载完成
🎬 名称:[FLsnow][Star-Detective_Precure][20][1080p]
💾 大小:977 MB
📁 路径:/home/yourname/downloads/anime/07-名侦探光之美少女

4.5 部署零信任隧道#

利用 Cloudflare Tunnel 实现无端口的隐蔽内网穿透

4.5.1 在云端创建隧道#

  1. 登录你的 Cloudflare 账号,进入 Zero Trust 面板
  2. . Zero Trust → Networks(网络) → Connectors(连接器) → Create a tunnel(创建隧道)
  3. 隧道类型 → 选择 Cloudflared
  4. 命名 Seedbox-Main(这里的命名是隧道的显示名称,随便取) → Save tunnel(保存隧道)

4.5.2 在 Seedbox 上安装连接器#

  1. 在面板的下一步里,选择环境为 Debian -> 64-bit
  2. 在这里会给出三个安装命令,复制粘贴执行前面两个即可
  3. 把这整些命令复制到 Seedbox 的 SSH 终端里运行逐个执行即可
  4. 运行完毕后,CF 网页端底部会弹出一个绿色的 Connected(已连接)状态,点击 Next

4.5.3 在网页端配置 SSH 路由#

字段值
Subdomain(子域名)<YOUR_SSH_SUBDOMAIN>(子域名前缀,可自己取,比如seedbox-ssh)
Domain(域)<YOUR_DOMAIN> (下拉选择你已托管的域名)
Path(路径)留空
Type(类型)SSH
URL填入 localhost:你的非标 SSH 端口 (比如 localhost:43210)

点击 Save tunnel即可

4.5.4 增加 SSH 配置块#

依然是打开 Windows 的 ~/.ssh/config,加入这段:

PowerShell 7
notepad $HOME/.ssh/config
Host seedbox-cf
HostName seedbox-ssh.yourdomain.com
User yourname
IdentityFile ~/.ssh/id_ed25519_seedbox
IdentitiesOnly yes
ProxyCommand cloudflared access ssh --hostname %h

下次可以使用 ssh seedbox-cf 通过隧道访问 SSH

至此,Seedbox 部署完毕

附:qBEE 参考配置#

适用于 2GB RAM VPS 的保守配置,以下为本人实际使用参数,请根据自身情况调整,切勿照抄

本人 VPS 带宽 10 Gbps,位于美西洛杉矶

长时间占用上传通道可能给邻居或宿主机带来负担,违反 TOS 导致封号

再加上 US 并非版权宽松地区,低调限速以规避部分投诉风险

限速#

  • 全局上传限速:500 Mbps
  • 启用备用限速
    • 备用限速时段:16:00 – 02:00 (UTC+8)
    • 备用上传速度:80 Mbps
    • 备用下载速度:300 Mbps

连接数#

  • 全局最大连接数:300
  • 单种最大连接数:50
  • 全局上传槽:30
  • 单种上传槽:5

队列#

  • 活动下载:5
  • 活动上传:15
  • 活动总数:20
  • 启用种子队列:开

做种限制#

  • 分享率达到:1.5
  • 做种时间达到:7200 分钟
  • 非活跃做种时间达到:7200 分钟

当分享率 ≥1.5 或 做种满 5 天 或 非活跃 5 天后 自动移除

BitTorrent#

  • DHT:开
  • PeX:开
  • LSD:关
  • 加密模式:优先加密

这里额外说一下加密模式

选项中文libtorrent 策略行为适用场景
Allow encryption允许加密 / 优先加密pe_enabled尝试加密,对方不支持则降级明文兼容性好,日常用
Require encryption强制加密pe_forced强制加密,拒绝明文连接应对 ISP 限速
Disable encryption禁用加密pe_disabled禁用加密,仅明文连接某些极其古老的 tracker/peer

高级 — 内存#

  • RAM 使用限制:384 MiB

高级 — 线程#

  • 异步 I/O 线程:2
  • 哈希校验线程:1

高级 — 磁盘#

  • 文件池大小:50
  • 校验时内存增量:32 MiB
  • 预分配文件:关
  • 磁盘缓存:64 MiB
  • 磁盘缓存过期时间:60 秒
  • 磁盘队列大小:1024 KiB
  • 读缓存模式:启用系统缓存
  • 写缓存模式:启用系统缓存

高级 — 网络与 Peer#

  • 每秒传出连接数:10
  • Socket Backlog:30
  • μTP-TCP 混合模式:优先使用 TCP
  • 单个 Peer 最大未完成请求:250
  • 允许来自同一 IP 的多个连接:关
  • 上传连接策略:最快上传

附:本文提及端口速查#

端口协议用途在何处配置
43210TCPSSHsshd_config / 防火墙脚本
80TCPHTTP(Caddy 反代)防火墙脚本
443TCPHTTPS(Caddy)Caddy 自动 / 防火墙脚本
8080TCPqBEE WebUIqBittorrent 默认
54321TCP/UDPBT 监听qBittorrent / 防火墙脚本
9898TCPPBH WebUIPBH 默认
5244TCPOpenList WebUIOpenList 默认
Rokutosei
Rokutosei ✦ 六等星
“君と集まって星座になれたら”
基于 qBittorrent EE、PeerBanHelper 与 OpenList 的个人追番 Seedbox 部署指南
https://blog.rokutosei.com/posts/seedbox/deployment/
作者
Rokutosei
发布于
2026-07-11
修改于
2026-07-31
许可协议
CC BY-NC-SA 4.0
✧ 文章目录 ✦
1
背景
2
适用人群
3
完成后将获得
4
零、 准备工作
5
一、 系统基础防护
1.1 系统基础优化
1.2 SSH 密钥登录
1.3 创建新用户
1.4 部署 Fail2ban
1.5 部署防火墙
6
二、 安装应用与反代网关
2.1 目录初始化
2.2 安装并配置 qBittorrent EE
2.3 安装并配置 PeerBanHelper (PBH)
2.4 安装并配置 OpenList (流媒体面板)
2.5 部署 Caddy 网关统一反代
2.6 限制 Systemd Journal 日志大小
7
三、启动并配置应用
3.1 启动 qBEE
3.2 启动 PBH
3.3 启动 OpenList
8
四、 进阶可选配置
4.1 更换 WebUI 为 VueTorrent
4.2 自动清理过期番剧
4.3 优化线路跳转(Mihomo 代理转发)
4.4 配置下载通知(Telegram Bot)
4.5 部署零信任隧道
9
附:qBEE 参考配置
限速
连接数
队列
做种限制
BitTorrent
高级 — 内存
高级 — 线程
高级 — 磁盘
高级 — 网络与 Peer
10
附:本文提及端口速查