Close sessions accepted during listener shutdown

This commit is contained in:
layue13 committed 2026-09-25 18:21:35 +08:00
1 parent 90e52ea8b6
commit b4ddd3c068
2 files changed
+58 -2

No files matched your search

@@ -160,8 +160,8 @@ public final class ProxySessionListener implements Players {
.childOption(io.netty.channel.ChannelOption.TCP_NODELAY, true) .childOption(io.netty.channel.ChannelOption.TCP_NODELAY, true)
.childHandler(new ChannelInitializer<SocketChannel>() { .childHandler(new ChannelInitializer<SocketChannel>() {
@Override protected void initChannel(SocketChannel channel) { @Override protected void initChannel(SocketChannel channel) {
Session session = new Session(ProxySessionListener.this, channel); Session session = registerSession(channel);
allSessions.add(session); if (session == null) return;
channel.pipeline().addLast("minecraft-frame-decoder", new dev.strataproxy.core.protocol.MinecraftFrameDecoder( channel.pipeline().addLast("minecraft-frame-decoder", new dev.strataproxy.core.protocol.MinecraftFrameDecoder(
dev.strataproxy.core.protocol.ProtocolProfile.minecraft1710(), true, dev.strataproxy.core.protocol.ProtocolProfile.minecraft1710(), true,
dev.strataproxy.core.protocol.ProtocolProfile.MAX_LOGIN_FRAME_BYTES)); dev.strataproxy.core.protocol.ProtocolProfile.MAX_LOGIN_FRAME_BYTES));
@@ -188,6 +188,17 @@ public final class ProxySessionListener implements Players {
return result; return result;
} }
/** Serializes admission with close() so every accepted session is either closed or rejected. */
private synchronized Session registerSession(SocketChannel channel) {
if (closed) {
channel.close();
return null;
}
Session session = new Session(this, channel);
allSessions.add(session);
return session;
}
@Override public Optional<PlayerView> find(PlayerIdentity identity) { @Override public Optional<PlayerView> find(PlayerIdentity identity) {
Objects.requireNonNull(identity, "identity"); Objects.requireNonNull(identity, "identity");
Session session = sessionsByPlayerId.get(identity.playerId()); Session session = sessionsByPlayerId.get(identity.playerId());
@@ -27,16 +27,20 @@ import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.net.SocketException;
import java.net.URI; import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.KeyFactory; import java.security.KeyFactory;
import java.security.spec.X509EncodedKeySpec; import java.security.spec.X509EncodedKeySpec;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import javax.crypto.Cipher; import javax.crypto.Cipher;
@@ -48,6 +52,47 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
final class ProxySessionListenerTest { final class ProxySessionListenerTest {
@Test
void shutdownWhileAcceptingClientsClosesEveryRegisteredSession() throws Exception {
var listener = new ProxySessionListener(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0),
new InMemoryBackendCatalog());
listener.setPlacement(player -> CompletableFuture.completedFuture(Optional.empty()));
var clients = Collections.synchronizedList(new ArrayList<Socket>());
var firstConnected = new CountDownLatch(1);
CompletableFuture<Void> connector = null;
try {
int port = ((InetSocketAddress) listener.start().toCompletableFuture()
.get(5, TimeUnit.SECONDS).localAddress()).getPort();
connector = CompletableFuture.runAsync(() -> {
for (int i = 0; i < 64; i++) {
try {
Socket client = new Socket(InetAddress.getLoopbackAddress(), port);
clients.add(client);
firstConnected.countDown();
} catch (IOException listenerClosed) {
return;
}
}
});
assertTrue(firstConnected.await(5, TimeUnit.SECONDS));
listener.close().toCompletableFuture().get(15, TimeUnit.SECONDS);
connector.get(5, TimeUnit.SECONDS);
assertTrue(listener.allSessions().isEmpty(), "shutdown left an accepted session registered");
for (Socket client : clients) {
client.setSoTimeout(2000);
try {
assertEquals(-1, client.getInputStream().read());
} catch (SocketException reset) {
// A peer close with unread login bytes can surface as a TCP reset on Windows.
}
}
} finally {
listener.close().toCompletableFuture().get(15, TimeUnit.SECONDS);
if (connector != null) connector.get(5, TimeUnit.SECONDS);
for (Socket client : clients) client.close();
}
}
@Test @Test
void rejectsOversizedFrontendFrameBeforeAuthentication() throws Exception { void rejectsOversizedFrontendFrameBeforeAuthentication() throws Exception {
var listener = new ProxySessionListener(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), var listener = new ProxySessionListener(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0),