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 本地保存
零、 准备工作
在开始之前,请做好以下准备:
- 一个个人域名:用于反代访问
- 一个通过支付验证的 Cloudflare 账号:将域名托管至 Cloudflare
- 一台 VPS:拥有 root 且建议 2GB RAM 起步
NOTE为避免纠纷,本文不提供任何 VPS 商家推荐或购买指导,请自行选择
本文中的
your_vps_ip、yourname、yourdomain.com均为占位符,实际操作时请替换为你的真实 IP、用户名和域名
一、 系统基础防护
1.1 系统基础优化
1.1.1 清除厂家模板
使用 root 账密进行 ssh 登录
ssh root@192.0.2.100 # 改为你的 IP,回车后输入密码部分服务商的系统模板可能包含首次开机脚本,建议清除,没有则跳过
sudo crontab -e进入编辑界面后,寻找类似 @reboot /admin/firstbootkvm yes 的行,将其删除并保存
1.1.2 开启 BBR 网络加速
把系统自带的 BBR 拥塞控制开启,能直接拉升机器的公网吞吐量:
echo "net.core.default_qdisc=fq" >> /etc/sysctl.confecho "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.confsysctl -p1.1.3 增大 Swap(可选)
对于 2GB RAM 的 VPS,qBEE 和 PBH 同时运行时内存吃紧,适当增大 Swap 可提供缓冲:
# 查看当前 Swapsudo swapon --show
# 创建 2G Swap 文件(可根据磁盘余量调整大小)sudo fallocate -l 2G /swapfilesudo chmod 600 /swapfilesudo mkswap /swapfilesudo swapon /swapfile
# 写入 fstab 使其重启后自动挂载echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab降低 swappiness 让系统优先使用物理内存:
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.confsudo 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:
方法一(标准交互方式):
# 查看当前指向ls -l /bin/sh
# 重新配置 dash,在弹出的对话框中选择 <No>(即“不使用 dash 作为默认 sh”)sudo dpkg-reconfigure dash
# 确认已切换ls -l /bin/sh执行后应看到 /bin/sh -> bash。
如果执行后没有弹出任何交互窗口(直接返回),可能是系统处于非交互模式,请改用方法二:
# 设置 debconf 选项为“不使用 dash”echo "dash dash/sh boolean false" | sudo debconf-set-selections
# 手动链接到 bashsudo ln -sf bash /bin/sh
# 确认切换ls -l /bin/sh方法二同时更新了 debconf 数据库,因此后续 dash 包升级时不会自动覆盖该链接。如果交互失败,直接采用方法二即可,无需纠结弹窗。
1.2 SSH 密钥登录
1.2.1 生成密钥对
在本地电脑(本文以 Windows 为例)打开 PowerShell 7,生成一把新密钥对:
cd ~ssh-keygen -t ed25519 -C "随便取名比如seedbox" -f ~/.ssh/id_ed25519_seedbox回车后,会提示你设置密码(passphrase),如果嫌麻烦可以直接连按两次回车跳过即可
1.2.2 上传公钥至服务器
密钥生成后,在本地 PowerShell 中运行以下命令,将公钥上传到服务器(注意将 your_vps_ip 替换为真实 IP)
$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 配置文件:
nano /etc/ssh/sshd_config修改或取消注释以下行:
Port 43210 # 修改为任意高位端口PasswordAuthentication no # 禁用密码登录PubkeyAuthentication yes # 允许密钥登录顺便查看是否存在这一行 Include /etc/ssh/sshd_config.d/*.conf
如有,在保存退出对 /etc/ssh/sshd_config 的编辑后,输入:
ls -R /etc/ssh/sshd_config.d/如果看到输出结果是 50-cloud-init.conf,则继续输入:
sudo nano /etc/ssh/sshd_config.d/50-cloud-init.conf将里面的 PasswordAuthentication yes 改为 PasswordAuthentication no
重启 SSH 服务使变更生效:
sudo systemctl restart sshd注意,重启服务后,先别关闭当前的 SSH 终端窗口,先开一个新的终端窗口,测试一下能不能用新端口和密钥成功连上,如果连不上,在原来的窗口里把配置改回来
1.2.4 测试连接
从本地 Windows 测试连接的命令是:
cd ~/.sshssh -i ./id_ed25519_seedbox -p 43210 root@your_vps_ip如果私钥设置了 passphrase,在连接时会提示输入;如果没有设置,则直接登录
1.2.5 配置本地快捷登录
如果希望后续不用每次都输路径和端口,可以在本地添加一个配置块
在 Windows 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 3Ctrl+S 保存后,即可用以下命令登录:
ssh seedbox1.3 创建新用户
为贯彻最小权限原则,我们不使用 root 登录,创建一个专用用户来运行服务和管理文件
TIP以下步骤仍在当前的 root SSH 会话中执行,完成后将切换到新用户
1.3.1 创建用户并设置登录密码
# 创建用户 yourname(UID 1000,带 home 目录)useradd -m -u 1000 yourname
# 设置密码(必须设置,否则无法使用 sudo)passwd yournameNOTE系统会提示你输入两次密码,虽然我们主要用密钥登录,但
sudo默认需要用户有密码,否则会报错user is not in the sudoers file或no password set
1.3.2 将用户加入 sudo 组
usermod -aG sudo yourname1.3.3 配置免密 sudo (可选、谨慎!)
CAUTION跳过密码验证会降低安全性,仅建议在个人独占、无敏感数据的 VPS 上使用
如果你希望执行 sudo 时不用输密码,编辑 sudoers 文件:
visudo在文件末尾添加一行:
yourname ALL=(ALL) NOPASSWD: ALLCtrl+O 、回车、 Ctrl+X 保存退出
TIP比较推荐的方法是保留密码验证,只对特定命令免密
比如
sudo systemctl restart qbittorrent本教程为简化流程采用全局免密
1.3.4 将公钥复制给新用户
在本地 PowerShell 中运行(注意将 your_vps_ip 替换为真实 IP):
$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
sudo apt update && sudo apt install fail2ban -y创建自定义配置,注意将 port 改为你之前设置的 SSH 端口:
sudo nano /etc/fail2ban/jail.local写入:
[sshd]enabled = trueport = 43210filter = sshdlogpath = /var/log/auth.logmaxretry = 3findtime = 600bantime = 8640010 分钟内输错 3 次密码,封禁该 IP 24 小时
启动并设置开机自启:
sudo systemctl enable --now fail2ban1.5 部署防火墙
本文采用以下设计:
- 防火墙规则(iptables):长期稳定,部署一次即可,后续无需变动
- 黑名单数据(ipset):定期更新,独立脚本执行,不影响现有防火墙规则
防火墙规则 (deploy_firewall.sh) → 执行一次即可 ↓ ipset 集合 ↓ASN 黑名单 (update_blacklists.sh) → 每周自动更新P2P 黑名单 (update_blacklists.sh) → 每周自动更新1.5.1 禁用 UFW
如果你之前启用过 UFW,先关掉:
sudo ufw disablesudo systemctl stop ufwsudo systemctl disable ufw1.5.2 创建防火墙部署脚本
防火墙部署脚本只需执行一次(或修改端口配置后重新执行):
sudo nano /usr/local/bin/deploy_firewall.sh粘贴以下脚本:
NOTE注意修改开头的用户配置区:
#!/bin/bash# ============================================================================# 部署防火墙规则,本脚本只需执行一次## 修改配置后重新执行即可,脚本会自动覆盖旧规则# ============================================================================
# ===================== 用户配置区 =====================
SSH_PORT=43210
# BT 监听端口(TCP 与 UDP 通常相同)BT_TCP_PORT=54321BT_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; }
# 检查 IPv6check_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-persistentinstall_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 "$@"赋予执行权限并运行:
sudo chmod +x /usr/local/bin/deploy_firewall.shsudo /usr/local/bin/deploy_firewall.sh脚本会:
- 创建 ipset 集合(
bad_asn_v4、bad_p2p_v4、bad_asn_v6) - 清空原有 iptables 规则并部署新规则
- 安装
iptables-persistent和ipset-persistent并保存规则(重启后自动恢复)
NOTEiptables 的匹配逻辑是 “自上而下,首次匹配即停止”。
本脚本将黑名单 DROP 规则写在开放端口 ACCEPT 规则的前面:命中黑名单的请求在前半部分就会被直接丢弃;只有未命中黑名单的正常请求,才会顺次走到后半部分被 443 / BT 端口 ACCEPT 放行。
此脚本只部署防火墙规则,不会下载黑名单数据。此时 ipset 集合为空,所有合法的 443 和 BT 请求均可正常访问。
1.5.3 创建黑名单更新脚本
黑名单更新脚本负责下载 ASN/P2P 数据并更新 ipset 集合,不修改防火墙规则:
sudo nano /usr/local/bin/update_blacklists.sh粘贴以下脚本:
#!/bin/bash# ============================================================================# 更新 ASN & P2P 黑名单# ============================================================================
# 需要屏蔽的 ASNASNS=( "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"; }
# 检查 IPv6check_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, ipaddressfor 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 "$@"赋予执行权限:
sudo chmod +x /usr/local/bin/update_blacklists.sh脚本说明:
- 原子更新:创建新集合 → 导入数据 →
ipset swap切换 → 销毁旧集合,切换耗时毫秒级,更新期间不会出现空集合窗口 - 缓存:将解析后的 CIDR 列表存入
/var/lib/seedbox-firewall/,下次运行时对比 SHA256,数据未变化则跳过更新 - 容错:下载失败不销毁现有集合,部分源失败不影响已成功的更新
1.5.4 首次加载黑名单
sudo /usr/local/bin/update_blacklists.sh首次运行会下载并导入所有 ASN 和 P2P 黑名单(目前约 47+ 万条),耗时取决于网络和 VPS 性能
NOTE由于脚本采用原子 swap 更新,首次运行时
deploy_firewall.sh创建的集合可能为空,需要等待导入完成后才会有数据后续每周运行时,当前集合始终保持有效,不受更新过程影响
1.5.5 设置自动更新
P2P 黑名单更新频率较低,建议每周执行一次:
sudo crontab -e末尾添加一行(每周日凌晨 4 点):
0 4 * * 0 /usr/local/bin/update_blacklists.sh > /var/log/blacklist-update.log 2>&1如果你希望更高频率,改为每日:
0 4 * * * /usr/local/bin/update_blacklists.sh > /var/log/blacklist-update.log 2>&1NOTE该 cron 任务只更新 ipset 数据,不修改防火墙规则、不会中断网络连接
1.5.6 验证规则
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 destination1 0 0 ACCEPT 0 -- * * 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED2 0 0 ACCEPT 0 -- lo * 0.0.0.0/0 0.0.0.0/03 0 0 DROP 0 -- * * 0.0.0.0/0 0.0.0.0/0 match-set bad_asn_v4 src4 0 0 DROP 0 -- * * 0.0.0.0/0 0.0.0.0/0 match-set bad_p2p_v4 src5 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:432106 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:807 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:4438 0 0 ACCEPT 6 -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:543219 0 0 ACCEPT 17 -- * * 0.0.0.0/0 0.0.0.0/0 udp dpt:54321NOTEiptables 规则链的匹配逻辑是 “自上而下,首次匹配即停止”。
bad_asn_v4和bad_p2p_v4规则为全局 DROP,位列所有开放端口(SSH / 80 / 443 / BT)规则的前面。命中黑名单的请求会在规则 3/4 直接被拦截丢弃,不会触及后续的端口放行规则;只有未命中黑名单的正常请求才能顺次匹配到后面的端口 ACCEPT 规则。
验证 ipset 集合数据量:
sudo ipset list bad_asn_v4 | grep "Number of entries"sudo ipset list bad_p2p_v4 | grep "Number of entries"预期输出示例:
Number of entries: 3421Number of entries: 468200二、 安装应用与反代网关
所有业务应用均在普通用户下运行,应用启动后,统一由 Caddy 接管公网流量
2.1 目录初始化
以普通用户(如 yourname)登录,执行:
# 数据落盘区mkdir -p ~/downloads/animemkdir -p ~/downloads/animetempmkdir -p ~/downloads/bd_archive2.2 安装并配置 qBittorrent EE
2.2.1 安装 qBEE
# 安装依赖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-noxsudo chmod +x /usr/local/bin/qbittorrent-nox
# 清理安装包rm qbittorrent-enhanced-nox_x86_64-linux-musl_static.zip2.2.2 编写服务
sudo nano /etc/systemd/system/qbittorrent.service将以下内容粘贴进去:
[Unit]Description=qBittorrent Command Line ClientAfter=network.target
[Service]Type=execUser=yourname # 改为你的用户名Group=yourname # 改为你的用户名UMask=002ExecStart=/usr/local/bin/qbittorrent-noxRestart=on-failure
[Install]WantedBy=multi-user.targetTIP
UMask=002能确保 qBEE 下载的文件,默认权限是 775 (目录)和 664 (文件)对于后续 OpenList 读取或者 rclone 拖回本地都极其友好,不会卡权限
保存并退出(Ctrl+O 回车,Ctrl+X)后,拉起服务:
sudo systemctl enable --now qbittorrent然后停止以生成配置文件:
sudo systemctl stop qbittorrentnano 打开 ~/.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 用:
# 创建 Java 25 目录sudo mkdir -p /opt/java25
# 下载 Java 25wget -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.gz2.3.2 下载并解压 PBH
让程序和数据都待在之前创建的 yourname 专属目录里:
# 创建并进入专属配置目录mkdir -p /home/yourname/seedbox/config/pbhcd /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.zip2.3.3 编写 Systemd 守护进程
sudo nano /etc/systemd/system/peerbanhelper.service将下面的内容粘贴进去(强制以 yourname 用户运行,并指定使用我们刚准备好的 Java 25):
[Unit]Description=PeerBanHelper DaemonAfter=network.target
[Service]Type=simpleUser=yourname # 改为你的用户名Group=yourname # 改为你的用户名WorkingDirectory=/home/yourname/seedbox/config/pbh/PeerBanHelperExecStart=/opt/java25/bin/java -jar /home/yourname/seedbox/config/pbh/PeerBanHelper/PeerBanHelper.jarRestart=on-failureLimitNOFILE=65536
[Install]WantedBy=multi-user.target保存并退出
2.4 安装并配置 OpenList (流媒体面板)
- 二进制部署:
# 1. 创建 OpenList 专属工作目录mkdir -p /home/yourname/seedbox/config/openlistcd /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.gzsudo mv openlist /usr/local/bin/openlistsudo chmod +x /usr/local/bin/openlist
# 4. 清理安装包rm openlist-linux-amd64.tar.gz- Systemd 守护:
编写守护进程
sudo nano /etc/systemd/system/openlist.service将以下内容粘贴进去:
[Unit]Description=OpenList Web File ManagerAfter=network.target
[Service]Type=simpleUser=yourname # 改为你的用户名Group=yourname # 改为你的用户名WorkingDirectory=/home/yourname/seedbox/config/openlistExecStart=/usr/local/bin/openlist serverRestart=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 记录:
qbee-> 指向 Seedbox 的公网 IPopenlist-> 指向 Seedbox 的公网 IPpbh-> 指向 Seedbox 的公网 IP
NOTE这三个域名的“代理状态”全部请保持为 小灰云(仅限 DNS)
后续 OpenList 传输大流量视频时直连跑满带宽,不走 Cloudflare CDN,以免违反 TOS 导致封号
2.5.2 安装 Caddy
使用 Caddy 官方针对 Debian 的软件源进行原生安装:
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 updatesudo apt install caddy -y
# 创建 Caddy 专属日志目录并赋予 caddy 用户权限sudo mkdir -p /var/log/caddysudo chown -R caddy:caddy /var/log/caddysudo chmod 755 /var/log/caddy2.5.3 配置 Caddyfile 路由
清空再打开 Caddyfile:
sudo > /etc/caddy/Caddyfilesudo nano /etc/caddy/Caddyfile将下面这段配置粘贴进去:
# 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 服务:
TIPCaddy 会在首次启动时自动向 Let’s Encrypt 申请免费 SSL 证书
为三个子域名启用 HTTPS,无需任何额外配置
sudo systemctl enable --now caddy2.6 限制 Systemd Journal 日志大小
Caddy 的访问日志已经在 Caddyfile 中配置了原生内置的日志滚动机制(roll_size、roll_keep 等参数),无需额外配置系统的 logrotate 脚本。
但是 Systemd 的日志(journald)默认仅按系统空闲空间比例限制大小,需要手动限制最大占用空间。
TIP如果不需要保留访问日志,或者磁盘空间十分有限,可以直接将 Caddyfile 中的整个
log { ... }代码块移除。
Systemd 的日志(journald)默认只按体积轮转但不设硬性上限,需要手动限制最大占用空间。
sudo nano /etc/systemd/journald.conf找到并取消注释、修改以下两行(若不存在则手动添加):
SystemMaxUse=100MMaxFileSec=1week保存后重启服务,使新配置生效:
sudo systemctl restart systemd-journald三、启动并配置应用
基础设施已就绪,现在可以正式启动 qBEE 和 PBH :
3.1 启动 qBEE
# 启动 qBEEsudo systemctl start qbittorrent
# 查看并记录首次启动的随机密码sudo journalctl -u qbittorrent | grep -i password此时,在本地 Windows 的浏览器中进入 https://qbee.yourdomain.com
初始账号为 admin,使用刚才抓取的随机密码登录进去后
首先去 Tools -> Options -> Web UI 修改随机初始密码与账户名(不推荐继续使用 admin)
假设修改为:
user: yournamepassword: 12345678 # 仅作教程示例,请使用强密码!保存后,按 F5 刷新网址重新登陆一遍即可
接着,根据个人喜好进行设置,VPS 配置较低的用户可参考文末的【附:qBEE 参考配置】
默认保存路径(完成时):/home/yourname/downloads/anime默认下载路径(未完成的种子):/home/yourname/downloads/animetemp3.2 启动 PBH
sudo systemctl enable --now peerbanhelpersudo 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(必须大写)
NOTEPBH 已通过行为检测(BTN 规则、进度判定、吸血识别)自动封禁恶意 Peer,一般无需额外按地理范围封禁。开启后会切断中文社区做种者的连接,可能影响做种效率,酌情使用
3.3 启动 OpenList
# 拉起并设置开机自启sudo systemctl daemon-reloadsudo 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,友情推荐一个关联小工具:
Waiting for api.github.com...Android 端则推荐 VLC 或 MX Player
对于内嵌软字幕的 MKV,安卓端目前仅测试了 VLC 支持解析;
同时,在视频页面直接点击 VLC 图标,VLC 无法解析视频链接,可以尝试点击视频页面的安卓图标按钮,然后再选择 VLC
对于外挂字幕,目前没找到自动加载的办法,只能单独下载字幕文件到本地,再通过播放器手动选择字幕
四、 进阶可选配置
4.1 更换 WebUI 为 VueTorrent
如果看腻了 qBEE 的默认样式,可尝试更换为 VueTorrent
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 当前种子列表中
安装依赖并编写脚本:
sudo apt install -y jqcd ~ && mkdir -p script && cd script && nano qb_cleaner.sh填入以下内容(注意将 QB_PASS 和 PORT 改为你的实际值):
#!/bin/bashQB_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 1fi
# 登录 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 1fi
# 扫描过期的目录/文件,跳过仍在做种的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" fidone
rm -f "$COOKIE_FILE"赋予执行权限:
chmod +x ~/script/qb_cleaner.sh接着每日清晨 8 点自动执行:
crontab -e写入:
0 8 * * * /home/yourname/script/qb_cleaner.sh >> ~/script/qb_cleaner.log 2>&14.3 优化线路跳转(Mihomo 代理转发)
假设,你的 Seedbox 在 LA,但是线路很差,但是恰好你又有另一台 LA 优化线路的机,并且更巧还搭建好了魔法,那么可利用本地 Mihomo 客户端将优化节点作为 SSH 跳板
如果你的 mihomo rules 最后是 Match, Proxy,那当你访问 Seedbox 时,大概率会被自动捕获并走 proxy,但我们可以简单加一个 tunnel 字段,使得 mihomo 不必每次都去匹配 rules:
- 在 Mihomo 配置文件中添加顶级字段
tunnels:
tunnels: # 类型,监听本地端口,目标地址与端口,出站节点/节点组 # 将目标地址与端口改为你seedbox的ip与ssh端口 - tcp,127.0.0.1:33333,your_vps_ip:43210,魔法节点/魔法节点策略组- 接着在本地添加 ssh config 配置块:
notepad $HOME/.ssh/configHost 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,依次发送以下指令:
/newbot创建新 Bot- 输入 Bot 显示名称(任意)
- 输入 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 创建通知脚本
sudo apt install -y bccd ~/script && nano tg_notify.sh#!/bin/bash
# 配置你的 token 和 IDBOT_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/&/\&/g; s/</\</g; s/>/\>/g')SAFE_PATH_RAW=$(echo "$SAVE_PATH" | sed 's/&/\&/g; s/</\</g; s/>/\>/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赋予执行权限:
chmod +x ~/script/tg_notify.sh4.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 在云端创建隧道
- 登录你的 Cloudflare 账号,进入 Zero Trust 面板
- . Zero Trust → Networks(网络) → Connectors(连接器) → Create a tunnel(创建隧道)
- 隧道类型 → 选择
Cloudflared - 命名
Seedbox-Main(这里的命名是隧道的显示名称,随便取) → Save tunnel(保存隧道)
4.5.2 在 Seedbox 上安装连接器
- 在面板的下一步里,选择环境为 Debian -> 64-bit
- 在这里会给出三个安装命令,复制粘贴执行前面两个即可
- 把这整些命令复制到 Seedbox 的 SSH 终端里运行逐个执行即可
- 运行完毕后,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,加入这段:
notepad $HOME/.ssh/configHost 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 的多个连接:关
- 上传连接策略:最快上传
附:本文提及端口速查
| 端口 | 协议 | 用途 | 在何处配置 |
|---|---|---|---|
| 43210 | TCP | SSH | sshd_config / 防火墙脚本 |
| 80 | TCP | HTTP(Caddy 反代) | 防火墙脚本 |
| 443 | TCP | HTTPS(Caddy) | Caddy 自动 / 防火墙脚本 |
| 8080 | TCP | qBEE WebUI | qBittorrent 默认 |
| 54321 | TCP/UDP | BT 监听 | qBittorrent / 防火墙脚本 |
| 9898 | TCP | PBH WebUI | PBH 默认 |
| 5244 | TCP | OpenList WebUI | OpenList 默认 |