15 Commits
Author SHA1 Message Date
Dylan Keir e20f2df589 Merge pull request #128 from lewisgibson/htpdate-socket
Close the HTPDate socket and time out its connection
2026-10-01 23:21:57 +01:00
Dylan Keir b2fdd4dc4e Merge pull request #127 from lewisgibson/spigot-version-and-timestamp
Fix Spigot version lookup and parse handshake timestamps as long
2026-10-01 23:21:17 +01:00
Dylan Keir d348727586 Merge pull request #126 from lewisgibson/cidr-matching-fixes
Fix CIDR mask validation and address matching
2026-10-01 23:20:13 +01:00
Dylan Keir b5df9d9427 Merge pull request #125 from lewisgibson/cidr-validator-thread-safe
Make the CIDR validator cache thread safe
2026-10-01 23:18:17 +01:00
Dylan Keir 2cf6b46c5d Merge pull request #123 from lewisgibson/folia-supported
Mark the plugin as Folia supported
2026-10-01 23:16:43 +01:00
Lewis Gibson a608a4c168 Close the HTPDate socket and time out its connection
The socket was never closed and had no connect or read timeout, so an unreachable or stalled google.com left a ForkJoinPool common pool thread blocked indefinitely. Both now give up after 10 seconds, and when google.com cannot be reached the server's own clock is used, as before.
2026-10-01 19:33:31 +01:00
Lewis Gibson f6030d24d6 Use the Spigot API to read the server version
Server#getMinecraftVersion is Paper only, so with prefer-protocollib enabled on Spigot onEnable failed with a NoSuchMethodError, which the surrounding catch (Exception) does not handle (#107). The fallback to the Paper handler for ProtocolLib 5.2.1 and below now also requires the Paper handshake event, since Spigot cannot load that handler.
2026-10-01 19:32:22 +01:00
Lewis Gibson 6f1820cb6d Parse the handshake timestamp as a long
Unix time in seconds no longer fits in an int after 19 January 2038, at which point every signed handshake would be rejected with a NumberFormatException.
2026-10-01 19:32:21 +01:00
Lewis Gibson 62658147a3 Add CIDR matcher tests for mask edges and address families 2026-10-01 19:31:16 +01:00
Lewis Gibson 3172f51728 Reject CIDR masks that are longer than the address
A mask such as 203.0.113.0/33 was accepted and then threw an ArrayIndexOutOfBoundsException from match() for the address it names, and a negative mask matched on a meaningless prefix. Reject both when the entry is loaded, where invalid entries are already skipped.
2026-10-01 19:31:15 +01:00
Lewis Gibson ef77cd7bcd Fix CIDR matching for IPv6 prefixes of 32 bits
A mask of 32 was always treated as an exact address match, which is only right for IPv4. An IPv6 entry such as 2001:db8::/32 matched nothing but 2001:db8::. Compare the mask against the address length instead.
2026-10-01 19:31:14 +01:00
Lewis Gibson 5bed462a2a Fix CIDR entries without a mask being dropped
The mask was always read from the split entry, so a bare address such as 203.0.113.7 threw an ArrayIndexOutOfBoundsException and was ignored with a warning. Also reject empty entries, which would otherwise resolve to the loopback address.
2026-10-01 19:31:13 +01:00
Lewis Gibson ef7b7c6510 Make the CIDR validator cache thread safe
validate() is called from a network thread per handshake and added to a plain HashSet, which is not safe to modify concurrently.
2026-10-01 19:30:11 +01:00
Lewis Gibson 82d05ea316 Mark the plugin as Folia supported
The Paper handshake path (PlayerHandshakeEvent) uses no scheduler and no world access.
2026-10-01 19:28:53 +01:00
Dylan Keir b316629e04 Bump version to 2.8.1 & fix issues with ProtocolLib on 1.21 2024-08-04 13:06:24 +01:00
11 changed files with 147 additions and 27 deletions

No files matched your search

+4 -2
View File
@@ -26,7 +26,7 @@ plugins {
// --
// Variables
// --
version = '2.8.0'
version = '2.8.1'
group = 'net.tcpshield.tcpshield'
archivesBaseName = 'TCPShield'
@@ -91,7 +91,9 @@ repositories {
dependencies {
// Bukkit
compileOnly group: 'com.comphenix.protocol', name: 'ProtocolLib', version: '5.1.0'
// compileOnly group: 'com.comphenix.protocol', name: 'ProtocolLib', version: '5.3.0-SNAPSHOT'
compileOnly(files("libs/ProtocolLib.jar")) // Temporary, currently using build #726 from the CI server since the maven repository is out of date.
// Paper - 1.19.4 so we can bump from JDK 8 -> JDK 17, using 1.20.x as a dependency would require bumping from JDK 8 -> JDK 21
compileOnly group: 'io.papermc.paper', name: 'paper-api', version: '1.19.4-R0.1-SNAPSHOT'
@@ -109,7 +109,7 @@ public class TCPShieldPacketHandler {
String hostname = payload[0];
String ipData = payload[1];
int timestamp = Integer.parseInt(payload[2]);
long timestamp = Long.parseLong(payload[2]);
String signature = payload[3];
String[] ipParts;
@@ -39,8 +39,8 @@ public class TCPShieldBukkit extends JavaPlugin implements TCPShieldPlugin {
int minor = Integer.parseInt(protocolLibVersion[1]);
int patch = Integer.parseInt(protocolLibVersion[2]);
String paperVersion = Bukkit.getServer().getMinecraftVersion();
if (major <= 5 && minor <= 2 && patch <= 1 && (paperVersion.equals("1.20.5") || paperVersion.equals("1.20.6"))) {
String minecraftVersion = Bukkit.getBukkitVersion().split("-")[0];
if (BukkitImplProvider.hasPaperEvent() && major <= 5 && minor <= 2 && patch <= 1 && (minecraftVersion.equals("1.20.5") || minecraftVersion.equals("1.20.6"))) {
getLogger().severe("TCPShield is incompatible with ProtocolLib <= 5.2.1 on Paper 1.20.5/1.20.6 due to lack of support from ProtocolLib. Reverting to default Paper handler to prevent issues. This error can be avoided by disabling 'prefer-protocollib' in the config.");
bukkitImpl = new BukkitPaper(this);
} else {
@@ -1,6 +1,6 @@
package net.tcpshield.tcpshield.bukkit.protocollib.handler;
import com.comphenix.protocol.injector.temporary.MinimalInjector;
import com.comphenix.protocol.injector.netty.Injector;
import com.comphenix.protocol.injector.temporary.TemporaryPlayerFactory;
import net.tcpshield.tcpshield.provider.PlayerProvider;
import net.tcpshield.tcpshield.util.ReflectionUtil;
@@ -71,13 +71,12 @@ public class ProtocolLibPlayer implements PlayerProvider {
try {
this.ip = ip.getAddress().getHostAddress();
MinimalInjector ignored = TemporaryPlayerFactory.getInjectorFromPlayer(player);
Object injector = ReflectionUtil.getObjectInPrivateField(ignored, "injector");
Object networkManager = ReflectionUtil.getObjectInPrivateField(injector, "networkManager");
Injector ignored = TemporaryPlayerFactory.getInjectorFromPlayer(player);
Object networkManager = ReflectionUtil.getObjectInPrivateField(ignored, "networkManager");
ReflectionUtil.setFinalField(networkManager, ReflectionUtil.searchFieldByClass(networkManager.getClass(), SocketAddress.class), ip);
Object channel = ReflectionUtil.getObjectInPrivateField(injector, "wrappedChannel");
Object channel = ReflectionUtil.getObjectInPrivateField(ignored, "channel");
ReflectionUtil.setFinalField(channel, ReflectionUtil.getDeclaredField(abstractChannelClass, "remoteAddress"), ip);
} catch (Exception e) {
throw new PlayerManipulationException(e);
@@ -34,25 +34,33 @@ public abstract class CIDRMatcher {
String[] split = cidrMatchString.split("/");
String parsedIPAddress;
if (split.length != 0) {
if (split.length > 1) {
parsedIPAddress = split[0];
this.maskBits = Integer.parseInt(split[1]);
this.simpleCIDR = maskBits == 32;
if (maskBits < 0)
throw new CIDRException("Invalid mask: " + split[1]);
} else {
parsedIPAddress = cidrMatchString;
this.maskBits = -1;
this.simpleCIDR = true;
}
this.maskBytes = simpleCIDR ? -1 : maskBits / 8;
if (parsedIPAddress.isEmpty())
throw new CIDRException("Empty CIDR entry");
try {
cidrAddress = InetAddress.getByName(parsedIPAddress);
} catch (UnknownHostException e) {
throw new CIDRException(e);
}
int addressBits = cidrAddress.getAddress().length * 8;
if (maskBits > addressBits)
throw new CIDRException("Invalid mask: " + maskBits + " is longer than the " + addressBits + " bit address " + parsedIPAddress);
this.simpleCIDR = maskBits == -1 || maskBits == addressBits;
this.maskBytes = simpleCIDR ? -1 : maskBits / 8;
}
/**
@@ -7,6 +7,7 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.net.InetAddress;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* A CIDR validator for TCPShield
@@ -18,7 +19,7 @@ public class CIDRValidator {
private final File ipWhitelistFolder;
private final List<CIDRMatcher> cidrMatchers;
private final Set<String> cache = new HashSet<>(); // Max potential size is equilivent to the amount of whitelisted IP's
private final Set<String> cache = ConcurrentHashMap.newKeySet(); // Max potential size is equilivent to the amount of whitelisted IP's
public CIDRValidator(TCPShieldPlugin plugin) throws CIDRException {
this.plugin = plugin;
@@ -7,6 +7,7 @@ import net.tcpshield.tcpshield.util.validation.timestamp.TimestampValidator;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
@@ -24,6 +25,8 @@ import java.util.stream.Collectors;
*/
public class HTPDateTimestampValidator extends TimestampValidator {
private static final int TIMEOUT = 10000; // In milliseconds
private volatile long htpDateOffset = 0;
public HTPDateTimestampValidator(TCPShieldPlugin plugin) {
@@ -40,20 +43,23 @@ public class HTPDateTimestampValidator extends TimestampValidator {
private void updateHTPDateOffset() throws IOException {
Socket socket = new Socket("google.com", 80);
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress("google.com", 80), TIMEOUT);
socket.setSoTimeout(TIMEOUT);
String payload = "HEAD http://google.com/ HTTP/1.1\r\nHost: google.com\r\nUser-Agent: tcpshield/1.0\r\nPragma: no-cache\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n";
socket.getOutputStream().write(payload.getBytes());
String payload = "HEAD http://google.com/ HTTP/1.1\r\nHost: google.com\r\nUser-Agent: tcpshield/1.0\r\nPragma: no-cache\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n";
socket.getOutputStream().write(payload.getBytes());
long readTime = System.currentTimeMillis(); // assuming server -> client time is negligible
long readTime = System.currentTimeMillis(); // assuming server -> client time is negligible
List<String> response = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))
.lines()
.collect(Collectors.toList());
List<String> response = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))
.lines()
.collect(Collectors.toList());
Date serverDate = parseDate(response);
Date serverDate = parseDate(response);
htpDateOffset = Math.round((serverDate.getTime() - readTime) / 1000.0) * 1000; // the HTTP protocol only returns time in seconds; round offset
htpDateOffset = Math.round((serverDate.getTime() - readTime) / 1000.0) * 1000; // the HTTP protocol only returns time in seconds; round offset
}
}
private Date parseDate(List<String> response) {
+1 -1
View File
@@ -1,5 +1,5 @@
name: TCPShield
version: 2.8.0
version: 2.8.1
main: net.tcpshield.tcpshield.bungee.TCPShieldBungee
author: https://tcpshield.com
softdepends:
+2 -1
View File
@@ -1,5 +1,5 @@
name: TCPShield
version: 2.8.0
version: 2.8.1
main: net.tcpshield.tcpshield.bukkit.TCPShieldBukkit
softdepend:
- ProtocolLib
@@ -7,3 +7,4 @@ softdepend:
author: https://tcpshield.com
description: TCPShield IP parsing capabilities for Velocity
api-version: 1.16
folia-supported: true
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "tcpshield",
"name": "TCPShield",
"version": "2.8.0",
"version": "2.8.1",
"description": "TCPShield IP parsing capabilities for Velocity",
"authors": [
"TCPShield"
@@ -0,0 +1,103 @@
package net.tcpshield.tcpshield.util.validation.cidr;
import net.tcpshield.tcpshield.util.exception.phase.CIDRException;
import org.junit.jupiter.api.Test;
import java.net.InetAddress;
import java.net.UnknownHostException;
import static org.junit.jupiter.api.Assertions.*;
public class CIDRMatcherTest {
private static boolean matches(String cidr, String address) throws UnknownHostException {
return CIDRMatcher.create(cidr).match(InetAddress.getByName(address));
}
@Test
public void bareAddressMatchesOnlyItself() throws UnknownHostException {
assertTrue(matches("203.0.113.7", "203.0.113.7"));
assertFalse(matches("203.0.113.7", "203.0.113.8"));
}
@Test
public void bareIPv6AddressMatchesOnlyItself() throws UnknownHostException {
assertTrue(matches("2001:db8::1", "2001:db8::1"));
assertFalse(matches("2001:db8::1", "2001:db8::2"));
}
@Test
public void emptyEntryIsRejected() {
assertThrows(CIDRException.class, () -> CIDRMatcher.create(""));
}
@Test
public void ipv6PrefixOf32BitsMatchesTheWholePrefix() throws UnknownHostException {
assertTrue(matches("2001:db8::/32", "2001:db8::"));
assertTrue(matches("2001:db8::/32", "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"));
assertFalse(matches("2001:db8::/32", "2001:db9::"));
assertFalse(matches("2001:db8::/32", "2002:db8::"));
}
@Test
public void ipv6PrefixOf128BitsMatchesOnlyItself() throws UnknownHostException {
assertTrue(matches("2001:db8::1/128", "2001:db8::1"));
assertFalse(matches("2001:db8::1/128", "2001:db8::2"));
}
@Test
public void ipv4PrefixOf32BitsMatchesOnlyItself() throws UnknownHostException {
assertTrue(matches("203.0.113.7/32", "203.0.113.7"));
assertFalse(matches("203.0.113.7/32", "203.0.113.8"));
}
@Test
public void masksLongerThanTheAddressAreRejected() {
assertThrows(CIDRException.class, () -> CIDRMatcher.create("203.0.113.0/33"));
assertThrows(CIDRException.class, () -> CIDRMatcher.create("2001:db8::/129"));
}
@Test
public void negativeMasksAreRejected() {
assertThrows(CIDRException.class, () -> CIDRMatcher.create("203.0.113.0/-1"));
}
@Test
public void ipv4MaskEdges() throws UnknownHostException {
assertTrue(matches("0.0.0.0/0", "203.0.113.7"));
assertTrue(matches("128.0.0.0/1", "203.0.113.7"));
assertFalse(matches("128.0.0.0/1", "127.255.255.255"));
assertTrue(matches("203.0.0.0/8", "203.255.255.255"));
assertFalse(matches("203.0.0.0/8", "204.0.0.0"));
assertTrue(matches("203.0.0.0/9", "203.127.255.255"));
assertFalse(matches("203.0.0.0/9", "203.128.0.0"));
assertTrue(matches("203.0.112.0/23", "203.0.113.255"));
assertFalse(matches("203.0.112.0/23", "203.0.114.0"));
assertTrue(matches("203.0.113.0/24", "203.0.113.255"));
assertFalse(matches("203.0.113.0/24", "203.0.112.255"));
assertTrue(matches("203.0.113.128/25", "203.0.113.255"));
assertFalse(matches("203.0.113.128/25", "203.0.113.127"));
assertTrue(matches("203.0.113.6/31", "203.0.113.7"));
assertFalse(matches("203.0.113.6/31", "203.0.113.8"));
}
@Test
public void ipv6MaskEdges() throws UnknownHostException {
assertTrue(matches("::/0", "2001:db8::1"));
assertTrue(matches("8000::/1", "ffff::1"));
assertFalse(matches("8000::/1", "7fff::1"));
assertTrue(matches("2001:db8::/48", "2001:db8:0:ffff::1"));
assertFalse(matches("2001:db8::/48", "2001:db8:1::1"));
assertTrue(matches("2001:db8::/49", "2001:db8:0:7fff::1"));
assertFalse(matches("2001:db8::/49", "2001:db8:0:8000::1"));
assertTrue(matches("2001:db8::/127", "2001:db8::1"));
assertFalse(matches("2001:db8::/127", "2001:db8::2"));
}
@Test
public void addressFamiliesNeverMatchEachOther() throws UnknownHostException {
assertFalse(matches("0.0.0.0/0", "2001:db8::1"));
assertFalse(matches("::/0", "203.0.113.7"));
}
}