自建IPFS网关如何处理版权投诉
我自己跑的一个公网 IPFS 网关,集中收到了一大批版权投诉。一条一条核对、再在网关上单独屏蔽,既费时间,同一份内容以后再被投诉还得重做一遍。网关自己并不保存内容,也不适合逐条当审核方,所以改成跟官方的 Bad Bits Denylist:每天整份拉取这份名单,命中的 CID 直接拒绝提供;首页放一个入口,把投诉人送到官方举报表单。名单更新后新投诉会自动生效,不用再维护一份自己的黑名单。
这里用的网关是 Rainbow。它只做检索和转发,不 pin、不长期保存内容。名单放在数据目录的 denylists 里,进程启动时加载。命中后返回 410 Gone,正文会写明这个 CID 被拦截。
名单里不是明文 CID。每一行是对 CID 再做一次 SHA-256 之后的十六进制,前面加两个斜杠,所以不能拿 CID 去文件里搜索。文件头类似这样:
name: "badbits"
description: "copyright violations, DMCA, GDPR, Code of Conduct or malware reported to Protocol Labs"
author: "Protocol Labs"
---
//00003f3fdeb4f3ee55e2c601ca263b1c8eef866e7f8e018a9f8ce03e7aea4d8a2026-09-19 这一份大约 50 万行、33MB。
为什么不直接订阅 URL
Rainbow 支持 RAINBOW_DENYLISTS,填一个 URL 就会自己下载并追更新。但它按 append-only 处理,只去拉文件末尾多出来的字节。
Bad Bits 是排序后的列表,新条目会插到中间,不是只在末尾追加。用订阅会漏掉插在中间的记录,这个问题在 rainbow#126 里还开着。
另外,Rainbow 运行时只允许往现有名单末尾追加。整文件替换、或者新放一个名单文件,都要先停掉进程。所以这里改成每天完整下载,内容变了再停容器、换文件、重新启动。
每天更新名单
脚本放在 /data/go-ipfs-proxy/update-badbits-denylist.sh。下载后做三步检查:文件非空、首行是 name:、体积不少于 1MB。和现有文件一致就直接退出,避免无意义重启。有变化才 docker compose stop,替换后再 up -d。容器里的用户是 uid 1000,换文件后要把属主改成 1000:1000。
#!/usr/bin/env bash
# 每天从 badbits.dwebops.pub 拉取完整 denylist。
# 该列表会在中间插入新条目,不是 append-only,不能用 RAINBOW_DENYLISTS 订阅。
# rainbow 要求更换 denylist 文件时先停容器。
set -euo pipefail
URL="https://badbits.dwebops.pub/badbits.deny"
DEST_DIR="/data/go-ipfs-proxy/data/rainbow/denylists"
DEST="${DEST_DIR}/badbits.deny"
TMP="${DEST}.tmp.$$"
COMPOSE_FILE="/data/go-ipfs-proxy/docker-compose.yml"
COMPOSE_DIR="/data/go-ipfs-proxy"
cleanup() {
rm -f "${TMP}"
}
trap cleanup EXIT
mkdir -p "${DEST_DIR}"
echo "[rainbow-badbits] 开始下载 ${URL}"
curl -fsSL --retry 3 --retry-delay 5 --connect-timeout 30 --max-time 300 \
-A "rainbow-badbits-updater/1.0" \
-o "${TMP}" "${URL}"
if [[ ! -s "${TMP}" ]]; then
echo "[rainbow-badbits] 下载结果为空" >&2
exit 1
fi
if ! head -n 1 "${TMP}" | grep -q 'name:'; then
echo "[rainbow-badbits] 文件格式异常,首行不是 name:" >&2
exit 1
fi
SIZE=$(wc -c < "${TMP}")
if [[ "${SIZE}" -lt 1000000 ]]; then
echo "[rainbow-badbits] 文件过小: ${SIZE} 字节" >&2
exit 1
fi
chown 1000:1000 "${TMP}"
chmod 644 "${TMP}"
if [[ -f "${DEST}" ]] && cmp -s "${TMP}" "${DEST}"; then
echo "[rainbow-badbits] 内容未变化 (${SIZE} 字节),跳过重启"
exit 0
fi
echo "[rainbow-badbits] 列表有更新 (${SIZE} 字节),停 rainbow 后替换并启动"
docker compose -f "${COMPOSE_FILE}" --project-directory "${COMPOSE_DIR}" stop rainbow
mv -f "${TMP}" "${DEST}"
chown 1000:1000 "${DEST}"
docker compose -f "${COMPOSE_FILE}" --project-directory "${COMPOSE_DIR}" up -d rainbow
echo "[rainbow-badbits] 完成"用 systemd timer 每天跑一次,定在服务器本地时间 04:00,再随机延迟最多 5 分钟,避免整点一起打官方源。Persistent=true 保证关机错过的那次开机后补跑。
/etc/systemd/system/rainbow-badbits-denylist.service:
[Unit]
Description=Update rainbow badbits denylist and reload container
After=docker.service network-online.target
Wants=network-online.target
Requires=docker.service
[Service]
Type=oneshot
ExecStart=/data/go-ipfs-proxy/update-badbits-denylist.sh
Nice=10
TimeoutStartSec=10min/etc/systemd/system/rainbow-badbits-denylist.timer:
[Unit]
Description=Daily update of rainbow badbits denylist
[Timer]
OnCalendar=*-*-* 04:00:00
Persistent=true
RandomizedDelaySec=5min
[Install]
WantedBy=timers.target启用:
chmod +x /data/go-ipfs-proxy/update-badbits-denylist.sh
systemctl daemon-reload
systemctl enable --now rainbow-badbits-denylist.timer内容没变时日志类似:内容未变化 (34017386 字节),跳过重启。
Rainbow 这边不用设 RAINBOW_DENYLISTS。数据目录挂到 /data/rainbow,名单放在里面的 denylists/badbits.deny,启动时会自己读。
网关首页放投诉入口
Rainbow 自带的根路径页面没有投诉入口。Caddy 只把 / 换成静态页,/ipfs/、/ipns/ 继续反代到本机 127.0.0.1:9090。
/etc/caddy/sites-enabled/ipfsscan-gateway.caddy:
http://gateway.img2ipfs.com http://gateway.ipfsscan.io http://cdn.img2ipfs.com http://cdn.ipfsscan.io {
encode gzip zstd
@home path /
handle @home {
root * /data/ipfs-gateway-landing/public
rewrite * /index.html
file_server
}
handle {
reverse_proxy 127.0.0.1:9090
}
}静态页 /data/ipfs-gateway-landing/public/index.html 只放一块说明,链接指向官方举报表单 https://ipfs.fyi/report-abuse。邮件也可以发到 [email protected]。审核和写入名单由 Protocol Labs 的 Bad Bits 处理,网关不自己增删条目。投诉人提交之后,最多等到第二天名单更新、Rainbow 重启,对应 CID 才会在这个网关上被拦住。
<div class="abuse-box">
<strong>Report Abuse / Copyright Infringement</strong>
<p>
IPFSScan uses the IPFS Bad Bits denylist to block reported content.
Copyright, malware, phishing, and other abuse reports can be submitted
through the Bad Bits reporting system.
</p>
<a href="https://ipfs.fyi/report-abuse" target="_blank" rel="noopener noreferrer">
Submit Abuse Report →
</a>
</div>改完执行 caddy reload。访问 http://gateway.ipfsscan.io/ 应返回这段静态页,而不是 Rainbow 默认首页。
确认名单生效
官方留了两个测试 CID。
应该被拦截的:
bafkreicx5e3uuh5qqimy3copvblqebitoguqzqsnnqeq3ogz77aiwceopycurl -I http://127.0.0.1:9090/ipfs/bafkreicx5e3uuh5qqimy3copvblqebitoguqzqsnnqeq3ogz77aiwceopy正常是 HTTP/1.1 410 Gone,正文里有 is blocked and cannot be provided。这个 CID 是故意留在名单里的无害测试内容,能打开说明名单没加载上。
不应该被拦截的空目录:
bafybeiczsscdsbs7ffqz55asqdf3smv6klcw3gofszvwlyarci47bgf354这个必须能打开。它要是也 410,就是拦多了。不带结尾斜杠时 Rainbow 会 301 到带斜杠的目录页,带上斜杠后是 200。