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