diff --git a/.github/workflows/jvm-ci.yml b/.github/workflows/jvm-ci.yml
index 0233ed772a..1ea8b8ec42 100644
--- a/.github/workflows/jvm-ci.yml
+++ b/.github/workflows/jvm-ci.yml
@@ -53,7 +53,7 @@ jobs:
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
- images: ghostchu/peerbanhelper
+ images: ghostchu/peerbanhelper-snapshot
tags: |
type=ref,event=branch
type=ref,event=tag
@@ -68,7 +68,7 @@ jobs:
with:
context: .
file: ./Dockerfile
- push: false
+ push: true
platforms: |
linux/amd64
linux/arm64/v8
diff --git a/Dockerfile b/Dockerfile
index 954a810901..23c9952a35 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,21 +1,15 @@
-FROM eclipse-temurin:17.0.10_7-jre
-
-LABEL MAINTAINER="https://github.com/PBH-BTN/PeerBanHelper"
+FROM --platform=$BUILDPLATFORM maven:3.9.6-eclipse-temurin-17 as build
-ENV TZ=UTC PUID=0 PGID=0
-
-RUN set -ex && \
- export DEBIAN_FRONTEND=noninteractive && \
- apt update -y && \
- apt install -y gosu dumb-init && \
- apt autoremove -y && \
- apt clean && \
- rm -rf /var/lib/apt/lists/*
+ADD . /build
+WORKDIR /build
+RUN sh setup-webui.sh && mvn -B clean package --file pom.xml
+FROM eclipse-temurin:17.0.10_7-jre
+LABEL MAINTAINER="https://github.com/PBH-BTN/PeerBanHelper"
+USER 0
+ENV TZ=UTC
WORKDIR /app
-
-COPY --chmod=755 ./docker-entrypoint.sh /app/
-
-COPY ./target/PeerBanHelper.jar /app/
-
-ENTRYPOINT [ "/app/docker-entrypoint.sh" ]
\ No newline at end of file
+VOLUME /tmp
+COPY --from=build build/target/PeerBanHelper.jar /app/PeerBanHelper.jar
+ENV PATH "${JAVA_HOME}/bin:${PATH}"
+ENTRYPOINT ["java","-Xmx256M","-XX:+UseSerialGC","-jar","PeerBanHelper.jar"]
\ No newline at end of file
diff --git a/README.md b/README.md
index 176b86a5ea..e86a5d0dd2 100644
--- a/README.md
+++ b/README.md
@@ -224,6 +224,41 @@ PeerBanHelper 主要由以下几个功能模块组成:
+### 多拨侦测
+
+专业PCDN用户会在一台PCDN服务器上接入多条宽带,以此提升上传带宽,称为多拨。
+这类用户的刷下载工具一般也较为复杂,会利用多条宽带不同的出口IP分散流量,对抗基于下载进度的吸血检测。
+此模块对多拨下载现象进行侦测,发现同一网段集中下载同一种子,即予以全部封禁。
+目前已知可能误伤的情况:小ISP的骨干网出口在同一网段,造成多拨假象。如果种子涉及的BT网络主体在大陆以外,请谨慎使用。
+
+
+
+查看示例配置文件
+
+```yaml
+ multi-dialing-blocker:
+ enabled: false
+ # 子网掩码长度
+ # IP地址前多少位相同的视为同一个子网,位数越少范围越大,一般不需要修改
+ subnet-mask-length: 24
+ # 对于同小区IPv6地址应该取多少位掩码没有调查过,64位是不会误杀的保险值
+ subnet-mask-v6-length: 64
+ # 容许同一网段下载同一种子的IP数量,正整数
+ # 防止DHCP重新分配IP、碰巧有同一小区的用户下载同一种子等导致的误判
+ tolerate-num: 3
+ # 缓存持续时间(秒)
+ # 所有连接过的peer会记入缓存,DHCP服务会定期重新分配IP,缓存时间过长会导致误杀
+ cache-lifespan: 86400
+ # 是否追猎
+ # 如果某IP已判定为多拨,无视缓存时间限制继续搜寻其同伙
+ keep-hunting: true
+ # 追猎持续时间(秒)
+ # 和cache-lifspan作用相似,对被猎杀IP的缓存持续时间,keep-hunting为true时有效
+ keep-hunting-time: 2592000
+```
+
+
+
## 添加下载器
PeerBanHelper 能够连接多个支持的下载器,并共享 IP 黑名单。但每个下载器只能被一个 PeerBanHelper 添加,多个 PBH 会导致操作 IP 黑名单时出现冲突。
diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh
deleted file mode 100644
index 6de856ae5a..0000000000
--- a/docker-entrypoint.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/sh
-# shellcheck shell=sh
-
-chown -R "${PUID}":"${PGID}" /app
-
-exec gosu "${PUID}":"${PGID}" dumb-init java -Xmx256M -XX:+UseSerialGC -jar PeerBanHelper.jar
diff --git a/pom.xml b/pom.xml
index cafd65dd4d..45ecf68604 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,13 +6,12 @@
com.ghostchu.peerbanhelper
peerbanhelper
- 3.3.7
+ 3.4.0
PeerBanHelper
- 17
- 17
+ 17
UTF-8
com.ghostchu.peerbanhelper.Main
yyyyMMdd-HHmmss
@@ -47,7 +46,7 @@
shade
- ${name}
+ ${project.name}
true
false
false
@@ -204,7 +203,9 @@
shade
- ${name}-SNAPSHOT-${maven.build.timestamp}-${git.commit.id.abbrev}
+
+ ${project.name}-SNAPSHOT-${maven.build.timestamp}-${git.commit.id.abbrev}
+
@@ -310,6 +311,15 @@
flatlaf
${flatlafVersion}
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.10.2
+ test
+
+
+
diff --git a/setup-webui.sh b/setup-webui.sh
index 5086fb127e..575fdee615 100755
--- a/setup-webui.sh
+++ b/setup-webui.sh
@@ -3,5 +3,5 @@ current_wd=$(pwd)
cd "$(dirname $0)/src/main/resources"
rm -rf static
git clone --depth 1 --branch gh-pages "https://github.com/PBH-BTN/pbh-fe.git" static
-echo "WebUI Version: $(git rev-parse --short HEAD)"
+cd static && echo "WebUI Version: $(git rev-parse --short HEAD)"
cd $current_wd
\ No newline at end of file
diff --git a/src/main/java/com/ghostchu/peerbanhelper/PeerBanHelperServer.java b/src/main/java/com/ghostchu/peerbanhelper/PeerBanHelperServer.java
index b5f37a8170..062227ff79 100644
--- a/src/main/java/com/ghostchu/peerbanhelper/PeerBanHelperServer.java
+++ b/src/main/java/com/ghostchu/peerbanhelper/PeerBanHelperServer.java
@@ -344,6 +344,7 @@ private void registerModules() {
moduleManager.register(new PeerIdBlacklist(this, profile));
moduleManager.register(new ClientNameBlacklist(this, profile));
moduleManager.register(new ProgressCheatBlocker(this, profile));
+ moduleManager.register(new MultiDialingBlocker(this, profile));
//moduleManager.register(new ActiveProbing(this, profile));
moduleManager.register(new AutoRangeBan(this, profile));
moduleManager.register(new BtnNetworkOnline(this, profile));
@@ -523,18 +524,22 @@ public void banPeer(@NotNull PeerAddress peer, @NotNull BanMetadata banMetadata,
BAN_LIST.put(peer, banMetadata);
metrics.recordPeerBan(peer, banMetadata);
banListInvoker.forEach(i -> i.add(peer, banMetadata));
- CompletableFuture.runAsync(() -> {
- try {
- InetAddress address = InetAddress.getByName(peer.getAddress().toString());
- if (!address.getCanonicalHostName().equals(peer.getIp())) {
- banMetadata.setReverseLookup(address.getCanonicalHostName());
- } else {
+ if (mainConfig.getBoolean("lookup.dns-reverse-lookup")) {
+ CompletableFuture.runAsync(() -> {
+ try {
+ InetAddress address = InetAddress.getByName(peer.getAddress().toString());
+ if (!address.getCanonicalHostName().equals(peer.getIp())) {
+ banMetadata.setReverseLookup(address.getCanonicalHostName());
+ } else {
+ banMetadata.setReverseLookup("N/A");
+ }
+ } catch (UnknownHostException ignored) {
banMetadata.setReverseLookup("N/A");
}
- } catch (UnknownHostException ignored) {
- banMetadata.setReverseLookup("N/A");
- }
- }, generalExecutor);
+ }, generalExecutor);
+ } else {
+ banMetadata.setReverseLookup("N/A");
+ }
Main.getEventBus().post(new PeerBanEvent(peer, banMetadata, torrentObj, peerObj));
}
diff --git a/src/main/java/com/ghostchu/peerbanhelper/config/MainConfigUpdateScript.java b/src/main/java/com/ghostchu/peerbanhelper/config/MainConfigUpdateScript.java
index 3732546cfd..015edc42e4 100644
--- a/src/main/java/com/ghostchu/peerbanhelper/config/MainConfigUpdateScript.java
+++ b/src/main/java/com/ghostchu/peerbanhelper/config/MainConfigUpdateScript.java
@@ -10,6 +10,11 @@ public MainConfigUpdateScript(YamlConfiguration conf) {
this.conf = conf;
}
+ @UpdateScript(version = 5)
+ public void optionForDnsReverseLookup() {
+ conf.set("lookup.dns-reverse-lookup", false);
+ }
+
@UpdateScript(version = 4)
public void defTurnOffIncrementBans() {
ConfigurationSection section = conf.getConfigurationSection("client");
diff --git a/src/main/java/com/ghostchu/peerbanhelper/config/ProfileUpdateScript.java b/src/main/java/com/ghostchu/peerbanhelper/config/ProfileUpdateScript.java
index 014b81f813..94c58d0ee8 100644
--- a/src/main/java/com/ghostchu/peerbanhelper/config/ProfileUpdateScript.java
+++ b/src/main/java/com/ghostchu/peerbanhelper/config/ProfileUpdateScript.java
@@ -18,6 +18,17 @@ public ProfileUpdateScript(YamlConfiguration conf) {
this.conf = conf;
}
+ @UpdateScript(version = 3)
+ public void multiDialingBlocker() {
+ conf.set("module.multi-dialing-blocker.enabled", false);
+ conf.set("module.multi-dialing-blocker.subnet-mask-length", 24);
+ conf.set("module.multi-dialing-blocker.subnet-mask-v6-length", 64);
+ conf.set("module.multi-dialing-blocker.tolerate-num", 3);
+ conf.set("module.multi-dialing-blocker.cache-lifespan", 86400);
+ conf.set("module.multi-dialing-blocker.keep-hunting", true);
+ conf.set("module.multi-dialing-blocker.keep-hunting-time", 2592000);
+ }
+
@UpdateScript(version = 2)
public void newRuleSyntax() {
List peerId = conf.getStringList("module.peer-id-blacklist.exclude-peer-id");
diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/DatabaseHelper.java b/src/main/java/com/ghostchu/peerbanhelper/database/DatabaseHelper.java
index c8bea3eb95..59a8d030b9 100644
--- a/src/main/java/com/ghostchu/peerbanhelper/database/DatabaseHelper.java
+++ b/src/main/java/com/ghostchu/peerbanhelper/database/DatabaseHelper.java
@@ -74,24 +74,10 @@ public String getMetadata(String key) throws SQLException {
}
}
- public long queryBanLogsCount(Date from, Date to) throws SQLException {
+ public long queryBanLogsCount() throws SQLException {
try (Connection connection = manager.getConnection()) {
- PreparedStatement ps;
- if (from == null && to == null) {
- ps = connection.prepareStatement("SELECT COUNT(*) AS count FROM ban_logs");
- } else {
- if (from == null || to == null) {
- throw new IllegalArgumentException("from or null cannot be null if any provided");
- } else {
- ps = connection.prepareStatement("SELECT COUNT(*) AS count FROM ban_logs");
- }
- }
- if (from != null) {
- ps.setDate(1, from);
- ps.setDate(2, to);
- }
@Cleanup
- ResultSet set = ps.executeQuery();
+ ResultSet set = connection.createStatement().executeQuery("SELECT COUNT(*) AS count FROM ban_logs");
return set.getLong("count");
}
}
diff --git a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/swing/SwingGuiImpl.java b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/swing/SwingGuiImpl.java
index f39480baf3..0371b6d6b0 100644
--- a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/swing/SwingGuiImpl.java
+++ b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/swing/SwingGuiImpl.java
@@ -70,11 +70,6 @@ public void sync() {
private void onColorThemeChanged() {
OsThemeDetector detector = OsThemeDetector.getDetector();
boolean isDarkThemeUsed = detector.isDark();
- try {
- setColorTheme(Class.forName(UIManager.getSystemLookAndFeelClassName()));
- } catch (ClassNotFoundException e) {
- throw new RuntimeException(e);
- }
if (isDarkThemeUsed) {
setColorTheme(FlatDarculaLaf.class);
} else {
diff --git a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/swing/SwingLoggerAppender.java b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/swing/SwingLoggerAppender.java
index 6e9745a118..aa74f20c66 100644
--- a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/swing/SwingLoggerAppender.java
+++ b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/swing/SwingLoggerAppender.java
@@ -60,19 +60,14 @@ public void append(LogEvent event) {
{
for (JTextArea textArea : textAreas) {
try {
- if (textArea != null) {
- if (textArea.getText().isEmpty()) {
- textArea.setText(message);
- } else {
- textArea.append("\n" + message);
- if (maxLines > 0 & textArea.getLineCount() > maxLines + 1) {
- int endIdx = textArea.getDocument().getText(0, textArea.getDocument().getLength()).indexOf("\n");
- textArea.getDocument().remove(0, endIdx + 1);
- }
- }
- String content = textArea.getText();
- textArea.setText(content.substring(0, content.length() - 1));
+ textArea.append(message);
+ int linesToCut = (textArea.getLineCount() - maxLines) + (maxLines / 2);
+ linesToCut = Math.min(linesToCut, textArea.getLineCount());
+ if (linesToCut > 0) {
+ int posOfLastLineToTrunk = textArea.getLineEndOffset(linesToCut - 1);
+ textArea.replaceRange("", 0, posOfLastLineToTrunk);
}
+ textArea.setCaretPosition(textArea.getDocument().getLength());
} catch (Throwable throwable) {
throwable.printStackTrace();
}
diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/MultiDialingBlocker.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/MultiDialingBlocker.java
new file mode 100644
index 0000000000..b202705663
--- /dev/null
+++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/MultiDialingBlocker.java
@@ -0,0 +1,198 @@
+package com.ghostchu.peerbanhelper.module.impl.rule;
+
+import com.ghostchu.peerbanhelper.PeerBanHelperServer;
+import com.ghostchu.peerbanhelper.module.AbstractFeatureModule;
+import com.ghostchu.peerbanhelper.module.AbstractRuleFeatureModule;
+import com.ghostchu.peerbanhelper.module.BanResult;
+import com.ghostchu.peerbanhelper.module.PeerAction;
+import com.ghostchu.peerbanhelper.peer.Peer;
+import com.ghostchu.peerbanhelper.text.Lang;
+import com.ghostchu.peerbanhelper.torrent.Torrent;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import inet.ipaddr.IPAddress;
+import lombok.extern.slf4j.Slf4j;
+import org.bspfsystems.yamlconfiguration.file.YamlConfiguration;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 同一网段集中下载同一个种子视为多拨,因为多拨和PCDN强相关所以可以直接封禁
+ */
+@Slf4j
+public class MultiDialingBlocker extends AbstractRuleFeatureModule {
+ // 计算缓存容量
+ private static final int TORRENT_PEER_MAX_NUM = 1024;
+ private static final int PEER_MAX_NUM_PER_SUBNET = 16;
+
+ private int subnetMaskLength;
+ private int subnetMaskV6Length;
+ private int tolerateNum;
+ private long cacheLifespan;
+ private boolean keepHunting;
+ private long keepHuntingTime;
+
+ public MultiDialingBlocker(PeerBanHelperServer server, YamlConfiguration profile) {
+ super(server, profile);
+ }
+
+ @Override
+ public @NotNull String getName() {
+ return "Multi Dialing Blocker";
+ }
+
+ @Override
+ public @NotNull String getConfigName() {
+ return "multi-dialing-blocker";
+ }
+
+ @Override
+ public boolean isCheckCacheable() {
+ return false;
+ }
+
+ @Override
+ public boolean needCheckHandshake() {
+ return false;
+ }
+
+ @Override
+ public boolean isConfigurable() {
+ return true;
+ }
+
+ @Override
+ public void onEnable() {
+ reloadConfig();
+ }
+
+ @Override
+ public void onDisable() {
+
+ }
+
+ private void reloadConfig() {
+ subnetMaskLength = getConfig().getInt("subnet-mask-length");
+ subnetMaskV6Length = getConfig().getInt("subnet-mask-v6-length");
+ tolerateNum = getConfig().getInt("tolerate-num");
+ cacheLifespan = getConfig().getInt("cache-lifespan") * 1000L;
+ keepHunting = getConfig().getBoolean("keep-hunting");
+ keepHuntingTime = getConfig().getInt("keep-hunting-time") * 1000L;
+
+ cache = CacheBuilder.newBuilder().
+ expireAfterWrite(cacheLifespan, TimeUnit.MILLISECONDS).
+ maximumSize(TORRENT_PEER_MAX_NUM).
+ build();
+ // 内层维护子网下的peer列表,外层回收不再使用的列表
+ // 外层按最后访问时间过期即可,若子网的列表还在被访问,说明还有属于该子网的peer在连接
+ subnetCounter = CacheBuilder.newBuilder().
+ expireAfterAccess(cacheLifespan, TimeUnit.MILLISECONDS).
+ maximumSize(TORRENT_PEER_MAX_NUM).
+ build();
+ huntingList = CacheBuilder.newBuilder().
+ expireAfterWrite(keepHuntingTime, TimeUnit.MILLISECONDS).
+ maximumSize(TORRENT_PEER_MAX_NUM).
+ build();
+ }
+
+ @Override
+ public @NotNull BanResult shouldBanPeer(
+ @NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) {
+ String torrentName = torrent.getName();
+ String torrentId = torrent.getId();
+ IPAddress peerAddress = peer.getAddress().getAddress();
+ String peerIpStr = peerAddress.toString();
+ IPAddress peerSubnet = peerAddress.isIPv4() ?
+ peerAddress.toPrefixBlock(subnetMaskLength) : peerAddress.toPrefixBlock(subnetMaskV6Length);
+
+ try {
+ long currentTimestamp = System.currentTimeMillis();
+
+ String torrentIpStr = torrentId + '@' + peerIpStr;
+ cache.put(torrentIpStr, currentTimestamp);
+
+ String torrentSubnetStr = torrentId + '@' + peerSubnet;
+ Cache subnetPeers = subnetCounter.get(torrentSubnetStr, this::genPeerGroup);
+ subnetPeers.put(peerIpStr, currentTimestamp);
+
+ if (subnetPeers.size() > tolerateNum) {
+ // 落库
+ huntingList.put(torrentSubnetStr, currentTimestamp);
+ // 返回当前IP即可,其他IP会在下一周期被封禁
+ return new BanResult(this, PeerAction.BAN, "Multi-dialing download detected",
+ String.format(Lang.MODULE_MDB_MULTI_DIALING_DETECTED,
+ torrentName, peerSubnet, peerIpStr));
+ }
+
+ if (keepHunting) {
+ recoverHuntingList();
+ try {
+ long huntingTimestamp = huntingList.get(torrentSubnetStr, () -> 0L);
+ if (huntingTimestamp > 0) {
+ if (currentTimestamp - huntingTimestamp < keepHuntingTime) {
+ // 落库
+ huntingList.put(torrentSubnetStr, currentTimestamp);
+ return new BanResult(this, PeerAction.BAN, "Multi-dialing hunting",
+ String.format(Lang.MODULE_MDB_MULTI_DIALING_HUNTING_TRIGGERED,
+ torrentName, peerSubnet, peerIpStr));
+ }
+ else {
+ huntingList.invalidate(torrentSubnetStr);
+ }
+ }
+ } catch (ExecutionException ignored) {}
+ }
+ }
+ catch (Exception e) {
+ log.error("shouldBanPeer exception", e);
+ }
+
+ return new BanResult(this, PeerAction.NO_ACTION, "N/A",
+ String.format(Lang.MODULE_MDB_MULTI_DIALING_NOT_DETECTED, torrentName));
+ }
+
+ // 是否已从数据库恢复追猎名单,持久化用的,目前没用
+ private static volatile boolean cacheRecovered = false;
+ // 所有peer的连接记录 torrentId+ip : createTime
+ private static Cache cache;
+ // 按子网统计的连接记录 torrentId+subnet : peerGroup
+ // 需要统计同一子网下的peer数量,Cache不支持size(),所以需要自己维护
+ private static Cache> subnetCounter;
+ // 追猎名单 torrentId+subnet : createTime
+ private static Cache huntingList;
+
+ private Cache genPeerGroup() {
+ return CacheBuilder.newBuilder().
+ expireAfterAccess(cacheLifespan, TimeUnit.MILLISECONDS).
+ maximumSize(PEER_MAX_NUM_PER_SUBNET).
+ build();
+ }
+
+ /**
+ * 将追猎名单恢复到内存中
+ * 持久化先不搞了
+ */
+ private void recoverHuntingList() {
+ if (cacheRecovered) return;
+ synchronized (MultiDialingBlocker.class) {
+ if (cacheRecovered) return;
+
+ // 根据配置删除超过限制时间的追猎记录
+ // 加载追猎名单
+
+ cacheRecovered = true;
+ }
+ }
+
+ public record HuntingTarget (
+ String hashSubnet,
+ long createTime
+ ){
+ }
+}
+
+
diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/ProgressCheatBlocker.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/ProgressCheatBlocker.java
index ff223fdf80..e53586483f 100644
--- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/ProgressCheatBlocker.java
+++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/ProgressCheatBlocker.java
@@ -80,57 +80,64 @@ private void reloadConfig() {
@Override
public @NotNull BanResult shouldBanPeer(@NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) {
- final long uploaded = peer.getUploaded();
- final long torrentSize = torrent.getSize();
- if (torrentSize <= 0) {
- return new BanResult(this, PeerAction.NO_ACTION, "N/A", Lang.MODULE_PCB_SKIP_UNKNOWN_SIZE_TORRENT);
- }
- if (torrentSize < torrentMinimumSize) {
- return new BanResult(this, PeerAction.NO_ACTION, "N/A", "Skip due the torrent size");
- }
- final double actualProgress = (double) uploaded / torrentSize;
- final double clientProgress = peer.getProgress();
- // uploaded = -1 代表客户端不支持统计此 Peer 总上传量
- if (uploaded != -1 && blockExcessiveClients && (uploaded > torrentSize)) {
- // 下载过量,检查
- long maxAllowedExcessiveThreshold = (long) (torrentSize * excessiveThreshold);
- if (uploaded > maxAllowedExcessiveThreshold) {
- return new BanResult(this, PeerAction.BAN, "Max allowed excessive threshold: " + maxAllowedExcessiveThreshold, String.format(Lang.MODULE_PCB_EXCESSIVE_DOWNLOAD, torrentSize, uploaded, maxAllowedExcessiveThreshold));
+ // 从缓存取数据
+ List lastRecordedProgress = progressRecorder.getIfPresent(peer.getAddress().getIp());
+ if (lastRecordedProgress == null) lastRecordedProgress = new ArrayList<>();
+ ClientTask clientTask = new ClientTask(torrent.getId(), 0d, 0L);
+ for (ClientTask recordedProgress : lastRecordedProgress) {
+ if (recordedProgress.getTorrentId().equals(torrent.getId())) {
+ clientTask = recordedProgress;
+ break;
}
}
-
- if (actualProgress - clientProgress <= 0) {
- return new BanResult(this, PeerAction.NO_ACTION, "N/A", String.format(Lang.MODULE_PCB_PEER_MORE_THAN_LOCAL_SKIP, percent(clientProgress), percent(actualProgress)));
- }
-
- double difference = Math.abs(actualProgress - clientProgress);
- if (difference > maximumDifference) {
- return new BanResult(this, PeerAction.BAN, "Over max Difference: " + difference, String.format(Lang.MODULE_PCB_PEER_BAN_INCORRECT_PROGRESS, percent(clientProgress), percent(actualProgress), percent(difference)));
- }
-
- double rewindAllow = rewindMaximumDifference;
- if (rewindAllow > 0) {
- List lastRecordedProgress = progressRecorder.getIfPresent(peer.getAddress().getIp());
- if (lastRecordedProgress == null) lastRecordedProgress = new ArrayList<>();
- ClientTask clientTask = new ClientTask(torrent.getId(), 0d);
- for (ClientTask recordedProgress : lastRecordedProgress) {
- if (recordedProgress.getTorrentId().equals(torrent.getId())) {
- clientTask = recordedProgress;
- break;
+ // 获取真实已上传量
+ final long actualUploaded = peer.getUploaded() > clientTask.getUploaded() ? peer.getUploaded() : clientTask.getUploaded() + peer.getUploaded();
+ try {
+ final long torrentSize = torrent.getSize();
+ // 过滤
+ if (torrentSize <= 0) {
+ return new BanResult(this, PeerAction.NO_ACTION, "N/A", Lang.MODULE_PCB_SKIP_UNKNOWN_SIZE_TORRENT);
+ }
+ if (torrentSize < torrentMinimumSize) {
+ return new BanResult(this, PeerAction.NO_ACTION, "N/A", "Skip due the torrent size");
+ }
+ // 计算进度信息
+ final double actualProgress = (double) actualUploaded / torrentSize; // 实际进度
+ final double clientProgress = peer.getProgress(); // 客户端汇报进度
+ // actualUploaded = -1 代表客户端不支持统计此 Peer 总上传量
+ if (actualUploaded != -1 && blockExcessiveClients && (actualUploaded > torrentSize)) {
+ // 下载过量,检查
+ long maxAllowedExcessiveThreshold = (long) (torrentSize * excessiveThreshold);
+ if (actualUploaded > maxAllowedExcessiveThreshold) {
+ return new BanResult(this, PeerAction.BAN, "Max allowed excessive threshold: " + maxAllowedExcessiveThreshold, String.format(Lang.MODULE_PCB_EXCESSIVE_DOWNLOAD, torrentSize, actualUploaded, maxAllowedExcessiveThreshold));
}
}
- double lastRecord = clientTask.getProgress();
- clientTask.setProgress(clientProgress);
+ // 如果客户端报告自己进度更多,则跳过检查
+ if (actualProgress - clientProgress <= 0) {
+ return new BanResult(this, PeerAction.NO_ACTION, "N/A", String.format(Lang.MODULE_PCB_PEER_MORE_THAN_LOCAL_SKIP, percent(clientProgress), percent(actualProgress)));
+ }
+ // 计算进度差异
+ double difference = Math.abs(actualProgress - clientProgress);
+ if (difference > maximumDifference) {
+ return new BanResult(this, PeerAction.BAN, "Over max Difference: " + difference, String.format(Lang.MODULE_PCB_PEER_BAN_INCORRECT_PROGRESS, percent(clientProgress), percent(actualProgress), percent(difference)));
+ }
+ if (rewindMaximumDifference > 0) {
+ double lastRecord = clientTask.getLastReportProgress();
+ double rewind = lastRecord - peer.getProgress();
+ boolean ban = rewind > rewindMaximumDifference;
+ return new BanResult(this, ban ? PeerAction.BAN : PeerAction.NO_ACTION, "RewindAllow: " + rewindMaximumDifference, String.format(Lang.MODULE_PCB_PEER_BAN_REWIND, percent(clientProgress), percent(actualProgress), percent(lastRecord), percent(rewind), percent(rewindMaximumDifference)));
+ }
+ return new BanResult(this, PeerAction.NO_ACTION, "N/A", String.format(Lang.MODULE_PCB_PEER_BAN_INCORRECT_PROGRESS, percent(clientProgress), percent(actualProgress), percent(difference)));
+ } finally {
+ // 无论如何都写入缓存,同步更改
+ clientTask.setUploaded(actualUploaded);
+ clientTask.setLastReportProgress(peer.getProgress());
progressRecorder.put(peer.getAddress().getIp(), lastRecordedProgress);
- double rewind = lastRecord - peer.getProgress();
- boolean ban = rewind > rewindAllow;
- return new BanResult(this, ban ? PeerAction.BAN : PeerAction.NO_ACTION, "RewindAllow: " + rewindAllow, String.format(Lang.MODULE_PCB_PEER_BAN_REWIND, percent(clientProgress), percent(actualProgress), percent(lastRecord), percent(rewind), percent(rewindAllow)));
}
- return new BanResult(this, PeerAction.NO_ACTION, "N/A", String.format(Lang.MODULE_PCB_PEER_BAN_INCORRECT_PROGRESS, percent(clientProgress), percent(actualProgress), percent(difference)));
}
- private String percent(double d){
- return (d*100)+"%";
+ private String percent(double d) {
+ return (d * 100) + "%";
}
@@ -138,7 +145,8 @@ private String percent(double d){
@Data
static class ClientTask {
private String torrentId;
- private Double progress;
+ private Double lastReportProgress;
+ private long uploaded;
}
}
diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHBanLogs.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHBanLogs.java
index b9f5c5c853..b2689a8b93 100644
--- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHBanLogs.java
+++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHBanLogs.java
@@ -48,7 +48,7 @@ public NanoHTTPD.Response handle(NanoHTTPD.IHTTPSession session) {
map.put("pageIndex", pageIndex);
map.put("pageSize", pageSize);
map.put("results", db.queryBanLogs(null, null, pageIndex, pageSize));
- map.put("total", db.queryBanLogsCount(null,null));
+ map.put("total", db.queryBanLogsCount());
return HTTPUtil.cors(NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, "application/json", JsonUtil.prettyPrinting().toJson(map)));
} catch (SQLException e) {
log.error(Lang.WEB_BANLOGS_INTERNAL_ERROR, e);
diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHMetrics.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHMetrics.java
index 53a9a90f88..811e50726b 100644
--- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHMetrics.java
+++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHMetrics.java
@@ -41,6 +41,7 @@ public NanoHTTPD.Response handle(NanoHTTPD.IHTTPSession session) {
map.put("checkCounter", metrics.getCheckCounter());
map.put("peerBanCounter", metrics.getPeerBanCounter());
map.put("peerUnbanCounter", metrics.getPeerUnbanCounter());
+ map.put("banlistCounter", getServer().getBannedPeers().size());
return HTTPUtil.cors(NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, "application/json", JsonUtil.prettyPrinting().toJson(map)));
}
diff --git a/src/main/java/com/ghostchu/peerbanhelper/text/Lang.java b/src/main/java/com/ghostchu/peerbanhelper/text/Lang.java
index ea42645d3f..f4599d0da1 100644
--- a/src/main/java/com/ghostchu/peerbanhelper/text/Lang.java
+++ b/src/main/java/com/ghostchu/peerbanhelper/text/Lang.java
@@ -37,6 +37,9 @@ public class Lang {
public static String MODULE_AP_TCP_TEST_PORT_FAIL = "TCP 探测目标失败: %s";
public static String MODULE_AP_EXECUTE_EXCEPTION = "烘焙缓存时出错,请将下面的错误日志发送给开发者以协助修复此错误";
public static final String MODULE_AP_SSL_CONTEXT_FAILURE = "初始化 SSLContext 时出错";
+ public static final String MODULE_MDB_MULTI_DIALING_NOT_DETECTED = "未发现多拨下载,种子名称:{}";
+ public static final String MODULE_MDB_MULTI_DIALING_DETECTED = "发现多拨下载,请持续关注,种子名称:{},子网:{},触发IP:{}";
+ public static final String MODULE_MDB_MULTI_DIALING_HUNTING_TRIGGERED = "触发多拨追猎名单,种子名称:{},子网:{},触发IP:{}";
public static final String DOWNLOADER_QB_LOGIN_FAILED = "登录到 {} 失败:{} - {}: \n{}";
public static final String DOWNLOADER_QB_FAILED_REQUEST_TORRENT_LIST = "请求 Torrents 列表失败 - %d - %s";
public static final String DOWNLOADER_QB_FAILED_REQUEST_PEERS_LIST_IN_TORRENT = "请求 Torrent 的 Peers 列表失败 - %d - %s";
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index dab8a3a788..4609814b27 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -1,4 +1,4 @@
-config-version: 4
+config-version: 5
# 客户端设置
client:
# 名字,可以自己起,会在日志中显示,只能由字母数字横线组成,数字不能打头
@@ -49,6 +49,9 @@ logger:
# 是否隐藏 [完成] 已检查 XX 的 X 个活跃 Torrent 和 X 个对等体 的日志消息?
# 在 DSM 的 ContainerManager 上有助于大幅度减少日志数量,并仅记录有价值的封禁等日志条目
hide-finish-log: false
+lookup:
+ # 启用 DNS 反查,能够通过 IP 反查域名,但可能增加你所使用 DNS 服务器的压力,并可能导致 DNS 服务器对你采取降低服务质量的措施
+ dns-reverse-lookup: false
# 线程控制
threads:
# 全局检查线程池并发等级
diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml
index 13b1e0112b..90abd3b659 100644
--- a/src/main/resources/log4j2.xml
+++ b/src/main/resources/log4j2.xml
@@ -13,7 +13,7 @@
-
+
[%d{HH:mm:ss}] [%t/%level]: %msg%n
diff --git a/src/main/resources/profile.yml b/src/main/resources/profile.yml
index 30bfa81939..dfeb21c9ed 100644
--- a/src/main/resources/profile.yml
+++ b/src/main/resources/profile.yml
@@ -164,4 +164,24 @@ module:
ipv6: 64 # /64 = ISP 通常分配给家宽用户的前缀长度
# 启用来自 BTN 网络的规则
btn:
- enabled: true
\ No newline at end of file
+ enabled: true
+ # 多拨封禁(实验性功能)
+ multi-dialing-blocker:
+ enabled: false
+ # 子网掩码长度
+ # IP地址前多少位相同的视为同一个子网,位数越少范围越大,一般不需要修改
+ subnet-mask-length: 24
+ # 对于同小区IPv6地址应该取多少位掩码没有调查过,64位是不会误杀的保险值
+ subnet-mask-v6-length: 64
+ # 容许同一网段下载同一种子的IP数量,正整数
+ # 防止DHCP重新分配IP、碰巧有同一小区的用户下载同一种子等导致的误判
+ tolerate-num: 3
+ # 缓存持续时间(秒)
+ # 所有连接过的peer会记入缓存,DHCP服务会定期重新分配IP,缓存时间过长会导致误杀
+ cache-lifespan: 86400
+ # 是否追猎
+ # 如果某IP已判定为多拨,无视缓存时间限制继续搜寻其同伙
+ keep-hunting: true
+ # 追猎持续时间(秒)
+ # keep-hunting为true时有效,和cache-lifspan相似,对被猎杀IP的缓存持续时间
+ keep-hunting-time: 2592000
\ No newline at end of file