From ab54d8bf440399f1c492630be598997876202ddf Mon Sep 17 00:00:00 2001
From: Finn
Date: Tue, 20 Jan 2026 15:04:30 +0100
Subject: [PATCH] Rewritten NetworkSystem
---
pom.xml | 2 +-
.../unlegitlibrary/bank/CreditCard.java | 10 +-
.../network/system/client/NetworkClient.java | 1143 +++++++++--------
.../receive/C_PacketReceivedEvent.java | 55 +-
.../receive/C_PacketReceivedFailedEvent.java | 73 +-
.../receive/C_UnknownObjectReceivedEvent.java | 60 +-
.../packets/send/C_PacketSendEvent.java | 65 +-
.../packets/send/C_PacketSendFailedEvent.java | 72 +-
.../state/connect/ClientConnectedEvent.java | 44 +-
.../connect/ClientFullyConnectedEvent.java | 49 +-
.../disconnect/ClientDisconnectedEvent.java | 44 +-
.../ClientFullyDisconnectedEvent.java | 43 +-
.../network/system/packets/Packet.java | 56 +-
.../network/system/packets/PacketCodec.java | 123 ++
.../network/system/packets/PacketHandler.java | 71 -
.../system/packets/PacketRegistry.java | 78 ++
.../system/packets/impl/ClientIDPacket.java | 58 -
.../packets/impl/ConnectionIdPacket.java | 70 +
.../impl/ProtocolRequirementsPacket.java | 81 ++
.../system/packets/impl/UdpBindAckPacket.java | 71 +
.../system/packets/impl/UdpBindPacket.java | 67 +-
.../system/packets/impl/UdpHelloPacket.java | 44 +
.../system/server/ClientConnection.java | 363 ++++++
.../system/server/ConnectionHandler.java | 275 ----
.../network/system/server/NetworkServer.java | 909 ++++++-------
.../system/server/NetworkServerUdpHooks.java | 88 --
.../system/server/ServerProtocolMode.java | 69 +
.../receive/S_PacketReceivedEvent.java | 57 +-
.../receive/S_PacketReceivedFailedEvent.java | 74 +-
.../receive/S_UnknownObjectReceivedEvent.java | 62 +-
.../packets/send/S_PacketSendEvent.java | 57 +-
.../packets/send/S_PacketSendFailedEvent.java | 74 +-
.../ClientConnectionConnectedEvent.java | 59 +
.../ClientConnectionFullyConnectedEvent.java | 60 +
.../ConnectionHandlerConnectedEvent.java | 57 -
.../ConnectionHandlerFullyConnectedEvent.java | 61 -
.../ClientConnectionDisconnectedEvent.java | 53 +
...lientConnectionFullyDisconnectedEvent.java | 55 +
.../ConnectionHandlerDisconnectedEvent.java | 51 -
...nnectionHandlerFullyDisconnectedEvent.java | 55 -
.../incoming/TCPIncomingConnectionEvent.java | 17 +-
.../incoming/UDPIncomingConnectionEvent.java | 25 +-
.../network/system/udp/UdpPacketCodec.java | 88 --
.../network/system/utils/ClientAuthMode.java | 30 +
.../network/system/utils/ClientID.java | 63 -
.../system/{udp => utils}/DtlsEndpoint.java | 168 ++-
.../network/system/utils/Endpoint.java | 145 +++
.../network/system/utils/NetworkProtocol.java | 34 +
.../network/system/utils/Transport.java | 28 -
.../network/system/utils/TransportPolicy.java | 76 --
50 files changed, 3072 insertions(+), 2460 deletions(-)
create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketCodec.java
delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketHandler.java
create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketRegistry.java
delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ClientIDPacket.java
create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ConnectionIdPacket.java
create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ProtocolRequirementsPacket.java
create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpBindAckPacket.java
create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpHelloPacket.java
create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ClientConnection.java
delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ConnectionHandler.java
delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/NetworkServerUdpHooks.java
create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ServerProtocolMode.java
create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ClientConnectionConnectedEvent.java
create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ClientConnectionFullyConnectedEvent.java
delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ConnectionHandlerConnectedEvent.java
delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ConnectionHandlerFullyConnectedEvent.java
create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ClientConnectionDisconnectedEvent.java
create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ClientConnectionFullyDisconnectedEvent.java
delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ConnectionHandlerDisconnectedEvent.java
delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ConnectionHandlerFullyDisconnectedEvent.java
delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/udp/UdpPacketCodec.java
create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/ClientAuthMode.java
delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/ClientID.java
rename src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/{udp => utils}/DtlsEndpoint.java (63%)
create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/Endpoint.java
create mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/NetworkProtocol.java
delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/Transport.java
delete mode 100644 src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/TransportPolicy.java
diff --git a/pom.xml b/pom.xml
index f8b6b7c..f787edd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
dev.unlegitdqrk
unlegitlibrary
- 1.7.1
+ 1.7.2
https://unlegitdqrk.dev/
Just a big library
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/bank/CreditCard.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/bank/CreditCard.java
index 149ea47..a6d3629 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/bank/CreditCard.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/bank/CreditCard.java
@@ -19,14 +19,16 @@ public class CreditCard {
private final Random random;
public CreditCard(CardBrand cardBrand) {
- this.cardBrand = cardBrand;
- this.random = new Random();
- this.cardNumber = generateCardNumber();
+ this(cardBrand, new Random());
}
public CreditCard(CardBrand cardBrand, int seed) {
+ this(cardBrand, new Random(seed));
+ }
+
+ public CreditCard(CardBrand cardBrand, Random random) {
this.cardBrand = cardBrand;
- this.random = new Random(seed);
+ this.random = random;
this.cardNumber = generateCardNumber();
}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/NetworkClient.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/NetworkClient.java
index 3d76fb0..7c47b6d 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/NetworkClient.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/NetworkClient.java
@@ -2,7 +2,7 @@
* Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
*
* You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
+ * You have to give Credits to the Author in your project and link this GitHub site: https://github.com/UnlegitDqrk
* See LICENSE-File if exists
*/
@@ -11,674 +11,713 @@ package dev.unlegitdqrk.unlegitlibrary.network.system.client;
import dev.unlegitdqrk.unlegitlibrary.event.EventManager;
import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.receive.C_PacketReceivedEvent;
import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.receive.C_PacketReceivedFailedEvent;
-import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.receive.C_UnknownObjectReceivedEvent;
import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.send.C_PacketSendEvent;
import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.send.C_PacketSendFailedEvent;
import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.connect.ClientConnectedEvent;
-import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.disconnect.ClientDisconnectedEvent;
import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.connect.ClientFullyConnectedEvent;
+import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.disconnect.ClientDisconnectedEvent;
import dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.disconnect.ClientFullyDisconnectedEvent;
+import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketCodec;
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
-import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
-import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.ClientIDPacket;
+import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.ConnectionIdPacket;
+import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.ProtocolRequirementsPacket;
+import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.UdpBindAckPacket;
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.UdpBindPacket;
-import dev.unlegitdqrk.unlegitlibrary.network.system.udp.DtlsEndpoint;
-import dev.unlegitdqrk.unlegitlibrary.network.system.udp.UdpPacketCodec;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.ClientID;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
-import dev.unlegitdqrk.unlegitlibrary.network.utils.PemUtils;
-import dev.unlegitdqrk.unlegitlibrary.utils.DefaultMethodsOverrider;
-import dev.unlegitdqrk.unlegitlibrary.utils.Logger;
+import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.UdpHelloPacket;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.ClientAuthMode;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.DtlsEndpoint;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Endpoint;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
-import javax.net.ssl.*;
-import java.io.*;
-import java.net.ConnectException;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
import java.net.InetSocketAddress;
-import java.net.Proxy;
-import java.net.Socket;
+import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
-import java.security.KeyStore;
-import java.security.cert.CertificateFactory;
+import java.util.EnumSet;
import java.util.Objects;
+import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
/**
- * Hybrid client supporting TCP (TLS) and UDP (DTLS).
+ * Secure client supporting TCP/TLS and UDP/DTLS (independently).
*
- * Your chosen policy: BOTH transports must be connected simultaneously.
+ * Connectivity rules:
+ *
+ * - {@link #isConnected()} becomes {@code true} only after:
+ *
+ * - the server-assigned {@link #connectionId()} is present,
+ * - the server-required protocol set (via {@link ProtocolRequirementsPacket}) was received, and
+ * - all server-required protocols are connected/ready.
+ *
+ *
+ * - UDP is considered ready only after:
+ *
+ * - DTLS handshake is complete, and
+ * - server acknowledged bind via {@link UdpBindAckPacket}, or UDP-only bootstrap assigned the id.
+ *
+ *
+ *
*/
public final class NetworkClient {
- private final String host;
- private final int tcpPort;
- private final int udpPort;
+ private final Endpoint tcpEndpoint;
+ private final Endpoint udpEndpoint;
+ private final EnumSet enabledProtocols;
- private final PacketHandler packetHandler;
- private final EventManager eventManager;
- private final Logger logger;
-
- private final int timeout;
+ private final PacketCodec codec;
private final SSLSocketFactory tlsSocketFactory;
- private final SSLParameters tlsParameters;
+ private final SSLParameters tlsParameters; // optional
private final SSLContext dtlsContext;
- private final Proxy proxy;
- private final AtomicBoolean fullyConnectedEventFired = new AtomicBoolean(false);
- private SSLSocket tcpSocket;
- private ObjectOutputStream tcpOut;
- private ObjectInputStream tcpIn;
- private DatagramChannel udpChannel;
- private DtlsEndpoint dtlsEndpoint;
- private InetSocketAddress udpRemote;
- private volatile ClientID clientId;
- private volatile Thread tcpReceiveThread;
+
+ private final int timeoutMillis;
+ private final int mtu;
+
+ private final EventManager eventManager;
+
+ private final AtomicBoolean lifecycleUp = new AtomicBoolean(false);
+
+ private volatile boolean tcpConnected;
+ private volatile boolean udpConnected; // DTLS handshake ok
+ private volatile boolean udpBound; // server acknowledged bind (or UDP-only hello assigned)
+ private volatile boolean wasFullyConnected;
+
+ private volatile SSLSocket tcpSocket;
+ private volatile ObjectOutputStream tcpOut;
+ private volatile ObjectInputStream tcpIn;
+ private volatile Thread tcpThread;
+
+ private volatile DatagramChannel udpChannel;
+ private volatile DtlsEndpoint dtlsEndpoint;
+ private volatile InetSocketAddress udpRemote;
private volatile Thread udpThread;
- public NetworkClient(
- String host,
- int tcpPort,
- int udpPort,
- PacketHandler packetHandler,
- EventManager eventManager,
- Logger logger,
- int timeout,
+ private volatile UUID connectionId;
+
+ private volatile EnumSet serverRequiredProtocols; // received from server
+
+ private NetworkClient(
+ Endpoint tcpEndpoint,
+ Endpoint udpEndpoint,
+ EnumSet enabledProtocols,
+ PacketCodec codec,
SSLSocketFactory tlsSocketFactory,
SSLParameters tlsParameters,
SSLContext dtlsContext,
- Proxy proxy
+ int timeoutMillis,
+ int mtu,
+ EventManager eventManager
) {
- this.host = Objects.requireNonNull(host, "host");
- this.tcpPort = tcpPort;
- this.udpPort = udpPort;
+ this.tcpEndpoint = Objects.requireNonNull(tcpEndpoint, "tcpEndpoint");
+ this.udpEndpoint = Objects.requireNonNull(udpEndpoint, "udpEndpoint");
+ this.enabledProtocols = EnumSet.copyOf(Objects.requireNonNull(enabledProtocols, "enabledProtocols"));
- this.packetHandler = Objects.requireNonNull(packetHandler, "packetHandler");
- this.eventManager = Objects.requireNonNull(eventManager, "eventManager");
- this.logger = logger;
-
- this.timeout = timeout;
- this.tlsSocketFactory = tlsSocketFactory;
+ this.codec = Objects.requireNonNull(codec, "codec");
+ this.tlsSocketFactory = Objects.requireNonNull(tlsSocketFactory, "tlsSocketFactory");
this.tlsParameters = tlsParameters;
- this.dtlsContext = dtlsContext;
- this.proxy = proxy;
+ this.dtlsContext = Objects.requireNonNull(dtlsContext, "dtlsContext");
- this.packetHandler.setClientInstance(this);
- this.packetHandler.registerPacket(new ClientIDPacket());
- this.packetHandler.registerPacket(new UdpBindPacket());
+ this.timeoutMillis = timeoutMillis;
+ this.mtu = mtu;
+
+ this.eventManager = Objects.requireNonNull(eventManager, "eventManager");
}
- public EventManager getEventManager() {
- return eventManager;
- }
+ /**
+ * Connects the client.
+ *
+ * If UDP is enabled and TCP is disabled, the client sends {@link UdpHelloPacket} to obtain
+ * a {@link ConnectionIdPacket} + {@link ProtocolRequirementsPacket} over UDP.
+ *
+ * @return true if started, false if already started
+ * @throws Exception on connection/handshake errors
+ */
+ public synchronized boolean connect() throws Exception {
+ if (lifecycleUp.get()) return false;
- public ClientID getClientId() {
- return clientId;
- }
+ // Reset state
+ connectionId = null;
+ serverRequiredProtocols = null;
- public void setClientId(ClientID clientId) {
- if (this.clientId == null) {
- this.clientId = Objects.requireNonNull(clientId, "clientId");
- }
- }
+ tcpConnected = false;
+ udpConnected = false;
+ udpBound = false;
+ wasFullyConnected = false;
- public boolean isTcpConnected() {
- Thread t = tcpReceiveThread;
- return tcpSocket != null && tcpSocket.isConnected() && !tcpSocket.isClosed()
- && t != null && t.isAlive() && !t.isInterrupted();
- }
-
- public boolean isUdpConnected() {
- return dtlsEndpoint != null && udpChannel != null && udpChannel.isOpen();
- }
-
- public boolean isFullyConnected() {
- return isTcpConnected() && isUdpConnected() && clientId != null;
- }
-
- public synchronized boolean connect() throws ConnectException {
- if (isFullyConnected()) return false;
-
- fullyConnectedEventFired.set(false);
+ // Mark lifecycle up early so checkFullyConnectedState can succeed during connect.
+ lifecycleUp.set(true);
try {
- connectTcp();
- eventManager.executeEvent(new ClientConnectedEvent(this, Transport.TCP));
+ if (enabledProtocols.contains(NetworkProtocol.TCP)) {
+ connectTcpTls();
+ }
- waitForClientId();
+ if (enabledProtocols.contains(NetworkProtocol.UDP)) {
+ connectUdpDtls();
- connectDtls();
- // UDP is considered connected after DTLS handshake is complete (bind follows immediately)
- eventManager.executeEvent(new ClientConnectedEvent(this, Transport.UDP));
+ // UDP-only bootstrap: ask server to create/assign an id over UDP.
+ if (!enabledProtocols.contains(NetworkProtocol.TCP)) {
+ sendPacket(new UdpHelloPacket(), NetworkProtocol.UDP);
+ }
+ }
- bindUdpToClientId();
-
- fireFullyConnectedIfReady();
return true;
- } catch (ConnectException e) {
- disconnect();
- throw e;
} catch (Exception e) {
disconnect();
- throw new ConnectException("Failed to connect: " + e.getMessage());
- }
- }
-
- private void connectTcp() throws Exception {
- if (tlsSocketFactory == null) throw new ConnectException("TLS socket factory not set.");
-
- if (proxy != null) {
- Socket raw = new Socket(proxy);
- raw.connect(new InetSocketAddress(host, tcpPort), timeout);
- tcpSocket = (SSLSocket) tlsSocketFactory.createSocket(raw, host, tcpPort, true);
- } else {
- tcpSocket = (SSLSocket) tlsSocketFactory.createSocket(host, tcpPort);
- }
-
- if (tlsParameters != null) {
- tcpSocket.setSSLParameters(tlsParameters);
- } else {
- SSLParameters p = tcpSocket.getSSLParameters();
- p.setProtocols(new String[]{"TLSv1.3"});
- tcpSocket.setSSLParameters(p);
- }
-
- tcpSocket.setTcpNoDelay(true);
- tcpSocket.setSoTimeout(timeout);
- tcpSocket.startHandshake();
-
- tcpOut = new ObjectOutputStream(tcpSocket.getOutputStream());
- tcpIn = new ObjectInputStream(tcpSocket.getInputStream());
-
- tcpReceiveThread = new Thread(this::tcpReceive, "NetworkClient-TCP-Receive");
- tcpReceiveThread.start();
- }
-
- private void waitForClientId() throws ConnectException {
- long start = System.currentTimeMillis();
- while (clientId == null) {
- if (!isTcpConnected()) throw new ConnectException("TCP disconnected before ClientID was assigned.");
- if (System.currentTimeMillis() - start > timeout)
- throw new ConnectException("Timed out waiting for ClientID over TCP.");
- Thread.onSpinWait();
- }
- }
-
- private void connectDtls() throws Exception {
- if (dtlsContext == null) throw new ConnectException("DTLS context not set.");
-
- udpRemote = new InetSocketAddress(host, udpPort);
-
- udpChannel = DatagramChannel.open();
- udpChannel.configureBlocking(false);
- udpChannel.connect(udpRemote);
-
- dtlsEndpoint = new DtlsEndpoint(
- udpChannel,
- dtlsContext,
- true,
- 1400,
- timeout,
- this::onDtlsApplicationData
- );
-
- dtlsEndpoint.handshake(udpRemote);
-
- udpThread = new Thread(this::udpLoop, "NetworkClient-UDP-DTLS");
- udpThread.start();
- }
-
- private void bindUdpToClientId() throws IOException, ClassNotFoundException {
- sendPacket(new UdpBindPacket(clientId), Transport.UDP);
- }
-
- public boolean sendPacket(Packet packet, Transport transport) throws IOException, ClassNotFoundException {
- Objects.requireNonNull(packet, "packet");
- Objects.requireNonNull(transport, "transport");
-
- return switch (transport) {
- case TCP -> sendTcp(packet);
- case UDP -> sendUdp(packet);
- };
- }
-
- private boolean sendTcp(Packet packet) throws IOException, ClassNotFoundException {
- if (!isTcpConnected()) return false;
-
- boolean sent = packetHandler.sendPacket(packet, tcpOut);
- if (sent) eventManager.executeEvent(new C_PacketSendEvent(this, packet, Transport.TCP));
- else eventManager.executeEvent(new C_PacketSendFailedEvent(this, packet, Transport.TCP));
- return sent;
- }
-
- private boolean sendUdp(Packet packet) throws IOException, ClassNotFoundException {
- if (dtlsEndpoint == null || udpRemote == null) return false;
-
- ByteBuffer encoded = UdpPacketCodec.encode(packetHandler, packet);
- dtlsEndpoint.sendApplication(udpRemote, encoded);
-
- eventManager.executeEvent(new C_PacketSendEvent(this, packet, Transport.UDP));
- return true;
- }
-
- private void udpLoop() {
- try {
- while (udpChannel != null && udpChannel.isOpen() && !Thread.currentThread().isInterrupted()) {
- DtlsEndpoint endpoint = dtlsEndpoint;
- if (endpoint != null) endpoint.poll();
- Thread.onSpinWait();
- }
- } catch (Exception ignored) {
- disconnect();
- }
- }
-
- private void onDtlsApplicationData(java.net.SocketAddress remote, ByteBuffer data) {
- try {
- Packet decoded = UdpPacketCodec.decodeAndHandle(packetHandler, data);
- if (decoded == null) {
- eventManager.executeEvent(new C_UnknownObjectReceivedEvent(this, data, Transport.UDP));
- return;
- }
- eventManager.executeEvent(new C_PacketReceivedEvent(this, decoded, Transport.UDP));
- } catch (Exception ignored) {
- }
- }
-
- private void tcpReceive() {
- try {
- while (isTcpConnected()) {
- Object received = tcpIn.readObject();
- handleTcpReceived(received);
- }
- } catch (Exception ignored) {
- disconnect();
- }
- }
-
- private void handleTcpReceived(Object received) throws IOException, ClassNotFoundException {
- if (received instanceof Integer id) {
- if (packetHandler.isPacketIDRegistered(id)) {
- Packet packet = packetHandler.getPacketByID(id);
- boolean handled = packetHandler.handlePacket(id, packet, tcpIn);
-
- if (handled) eventManager.executeEvent(new C_PacketReceivedEvent(this, packet, Transport.TCP));
- else eventManager.executeEvent(new C_PacketReceivedFailedEvent(this, packet, Transport.TCP));
- } else {
- eventManager.executeEvent(new C_UnknownObjectReceivedEvent(this, received, Transport.TCP));
- }
- } else {
- eventManager.executeEvent(new C_UnknownObjectReceivedEvent(this, received, Transport.TCP));
- }
-
- fireFullyConnectedIfReady();
- }
-
- private void fireFullyConnectedIfReady() {
- if (isFullyConnected() && fullyConnectedEventFired.compareAndSet(false, true)) {
- eventManager.executeEvent(new ClientFullyConnectedEvent(this, new Transport[]{Transport.TCP, Transport.UDP}));
+ throw e;
}
}
+ /**
+ * Disconnects the client and stops background threads.
+ *
+ * @return true if a disconnect happened, false if it was already down
+ */
public synchronized boolean disconnect() {
- boolean wasTcpConnected = isTcpConnected();
- boolean wasUdpConnected = isUdpConnected();
- boolean wasFullyConnected = isFullyConnected();
+ boolean wasUp = lifecycleUp.getAndSet(false);
- if (!wasTcpConnected && !wasUdpConnected) {
- cleanup();
- return false;
+ Thread t1 = tcpThread;
+ Thread t2 = udpThread;
+ if (t1 != null) t1.interrupt();
+ if (t2 != null) t2.interrupt();
+
+ try {
+ if (tcpOut != null) tcpOut.close();
+ } catch (Exception ignored) {
+ }
+ try {
+ if (tcpIn != null) tcpIn.close();
+ } catch (Exception ignored) {
+ }
+ try {
+ if (tcpSocket != null) tcpSocket.close();
+ } catch (Exception ignored) {
}
try {
- Thread t1 = tcpReceiveThread;
- Thread t2 = udpThread;
- if (t1 != null) t1.interrupt();
- if (t2 != null) t2.interrupt();
-
- if (tcpOut != null) tcpOut.close();
- if (tcpIn != null) tcpIn.close();
- if (tcpSocket != null) tcpSocket.close();
-
if (udpChannel != null) udpChannel.close();
- } catch (IOException ignored) {
+ } catch (Exception ignored) {
}
- // Transport-specific disconnect events
- if (wasUdpConnected) {
- eventManager.executeEvent(new ClientDisconnectedEvent(this, Transport.UDP));
- }
- if (wasTcpConnected) {
- eventManager.executeEvent(new ClientDisconnectedEvent(this, Transport.TCP));
- }
+ boolean tcpWas = tcpConnected;
+ boolean udpWas = udpConnected;
- cleanup();
+ tcpConnected = false;
+ udpConnected = false;
+ udpBound = false;
- if (wasFullyConnected) {
- eventManager.executeEvent(new ClientFullyDisconnectedEvent(this, new Transport[]{Transport.TCP, Transport.UDP}));
- }
-
- return true;
- }
-
- private void cleanup() {
tcpSocket = null;
tcpOut = null;
tcpIn = null;
+ tcpThread = null;
udpChannel = null;
dtlsEndpoint = null;
udpRemote = null;
-
- tcpReceiveThread = null;
udpThread = null;
- clientId = null;
- fullyConnectedEventFired.set(false);
+ connectionId = null;
+ serverRequiredProtocols = null;
+
+ if (tcpWas) {
+ fireDisconnected(NetworkProtocol.TCP);
+ }
+ if (udpWas) {
+ fireDisconnected(NetworkProtocol.UDP);
+ }
+
+ if (wasFullyConnected) {
+ wasFullyConnected = false;
+ eventManager.executeEvent(new ClientFullyDisconnectedEvent(this, requiredProtocolsForClientView()));
+ }
+
+ return wasUp;
}
- private void logInfo(String msg) {
- if (logger != null) logger.info(msg);
- else System.out.println(msg);
+ /**
+ * Returns the server-assigned connection id, if already received.
+ *
+ * @return connection id or null
+ */
+ public UUID connectionId() {
+ return connectionId;
+ }
+
+ /**
+ * Returns the protocols the client enabled locally.
+ *
+ * @return enabled protocols
+ */
+ public EnumSet enabledProtocols() {
+ return EnumSet.copyOf(enabledProtocols);
+ }
+
+ /**
+ * Returns the server-required protocol set if received, otherwise null.
+ *
+ * @return required protocols from server or null
+ */
+ public EnumSet serverRequiredProtocols() {
+ EnumSet r = serverRequiredProtocols;
+ return r == null ? null : EnumSet.copyOf(r);
+ }
+
+ /**
+ * Returns a snapshot of currently connected protocols (ready for app traffic).
+ *
+ * @return connected protocols
+ */
+ public EnumSet connectedProtocols() {
+ EnumSet set = EnumSet.noneOf(NetworkProtocol.class);
+ if (isTcpConnected()) set.add(NetworkProtocol.TCP);
+ if (isUdpConnected()) set.add(NetworkProtocol.UDP);
+ return set;
+ }
+
+ /**
+ * Returns true if the client is fully connected:
+ *
+ * - lifecycle is up
+ * - connection id assigned
+ * - server-required protocols received
+ * - all server-required protocols are connected/ready
+ *
+ *
+ * @return true if fully connected
+ */
+ public boolean isConnected() {
+ if (!lifecycleUp.get()) return false;
+ if (connectionId == null) return false;
+ if (serverRequiredProtocols == null) return false;
+
+ EnumSet required = EnumSet.copyOf(serverRequiredProtocols);
+
+ boolean tcpOk = !required.contains(NetworkProtocol.TCP) || isTcpConnected();
+ boolean udpOk = !required.contains(NetworkProtocol.UDP) || isUdpConnected();
+
+ return tcpOk && udpOk;
+ }
+
+
+ /**
+ * Returns true if TCP is up.
+ *
+ * @return true if TCP connected
+ */
+ public boolean isTcpConnected() {
+ return tcpConnected;
+ }
+
+ /**
+ * Returns true if UDP is up AND bound/acknowledged AND client id is set.
+ *
+ * @return true if UDP is ready for application traffic
+ */
+ public boolean isUdpConnected() {
+ return udpConnected && udpBound && connectionId != null;
+ }
+
+ /**
+ * Sends a packet via TCP/TLS or UDP/DTLS.
+ *
+ * Note: UDP packets can be sent before UDP is "bound" because the bind/hello flow itself
+ * is transported via UDP.
+ *
+ * @param packet packet
+ * @param protocol protocol to use
+ * @return true if sent, false if not possible (not up / protocol not enabled / not ready)
+ * @throws Exception on errors
+ */
+ public boolean sendPacket(Packet packet, NetworkProtocol protocol) throws Exception {
+ Objects.requireNonNull(packet, "packet");
+ Objects.requireNonNull(protocol, "protocol");
+
+ if (!lifecycleUp.get()) return false;
+ if (!enabledProtocols.contains(protocol)) return false;
+
+ try {
+ boolean sent = switch (protocol) {
+ case TCP -> sendTcp(packet);
+ case UDP -> sendUdp(packet);
+ };
+
+ if (sent) {
+ eventManager.executeEvent(new C_PacketSendEvent(this, packet, protocol));
+ } else {
+ eventManager.executeEvent(new C_PacketSendFailedEvent(
+ this, packet, protocol, new IllegalStateException("Packet not sent")
+ ));
+ }
+
+ return sent;
+ } catch (Exception e) {
+ eventManager.executeEvent(new C_PacketSendFailedEvent(this, packet, protocol, e));
+ throw e;
+ }
+ }
+
+ private void connectTcpTls() throws Exception {
+ InetSocketAddress addr = tcpEndpoint.resolveBest();
+
+ SSLSocket socket = (SSLSocket) tlsSocketFactory.createSocket(addr.getAddress(), addr.getPort());
+ socket.setTcpNoDelay(true);
+ socket.setSoTimeout(timeoutMillis);
+
+ if (tlsParameters != null) {
+ socket.setSSLParameters(tlsParameters);
+ } else {
+ SSLParameters p = socket.getSSLParameters();
+ p.setProtocols(new String[]{"TLSv1.3"});
+ socket.setSSLParameters(p);
+ }
+
+ socket.startHandshake();
+
+ ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
+ ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
+
+ this.tcpSocket = socket;
+ this.tcpOut = out;
+ this.tcpIn = in;
+
+ this.tcpConnected = true;
+ eventManager.executeEvent(new ClientConnectedEvent(this, NetworkProtocol.TCP));
+ checkFullyConnectedState();
+
+ this.tcpThread = new Thread(this::tcpReceiveLoop, "SecureNetworkClient-TCP-Receive");
+ this.tcpThread.start();
+ }
+
+ private void connectUdpDtls() throws Exception {
+ InetSocketAddress addr = udpEndpoint.resolveBest();
+
+ DatagramChannel ch = DatagramChannel.open();
+ ch.configureBlocking(false);
+ ch.connect(addr);
+
+ DtlsEndpoint endpoint = new DtlsEndpoint(
+ ch,
+ dtlsContext,
+ true,
+ mtu,
+ timeoutMillis,
+ ClientAuthMode.OPTIONAL, // ignored for clientMode=true
+ this::onDtlsApplicationData
+ );
+
+ endpoint.handshake(addr);
+
+ this.udpChannel = ch;
+ this.dtlsEndpoint = endpoint;
+ this.udpRemote = addr;
+
+ this.udpConnected = true;
+ eventManager.executeEvent(new ClientConnectedEvent(this, NetworkProtocol.UDP));
+ checkFullyConnectedState();
+
+ this.udpThread = new Thread(this::udpPollLoop, "SecureNetworkClient-UDP-DTLS");
+ this.udpThread.start();
+ }
+
+ private boolean sendTcp(Packet packet) throws Exception {
+ if (!tcpConnected) return false;
+
+ ObjectOutputStream out = tcpOut;
+ SSLSocket s = tcpSocket;
+ if (out == null || s == null || s.isClosed()) return false;
+
+ codec.sendToStream(packet, out);
+ return true;
+ }
+
+ private boolean sendUdp(Packet packet) throws Exception {
+ if (!udpConnected) return false;
+
+ DtlsEndpoint endpoint = dtlsEndpoint;
+ InetSocketAddress remote = udpRemote;
+ if (endpoint == null || remote == null) return false;
+
+ ByteBuffer buf = codec.encodeToBuffer(packet);
+ endpoint.sendApplication(remote, buf);
+ return true;
+ }
+
+ private void tcpReceiveLoop() {
+ try {
+ while (!Thread.currentThread().isInterrupted() && tcpConnected) {
+ SSLSocket s = tcpSocket;
+ ObjectInputStream in = tcpIn;
+ if (s == null || in == null || s.isClosed()) break;
+
+ Packet packet = codec.receiveFromStream(in);
+ if (packet == null) continue;
+
+ if (processServerControlPacket(packet, NetworkProtocol.TCP)) {
+ continue;
+ }
+
+ eventManager.executeEvent(new C_PacketReceivedEvent(this, packet, NetworkProtocol.TCP));
+ }
+ } catch (Exception e) {
+ eventManager.executeEvent(new C_PacketReceivedFailedEvent(this, null, NetworkProtocol.TCP, e));
+ } finally {
+ boolean tcpWas = tcpConnected;
+ tcpConnected = false;
+
+ if (tcpWas) {
+ fireDisconnected(NetworkProtocol.TCP);
+ }
+
+ checkFullyDisconnectedIfNeeded();
+ }
+ }
+
+ private void udpPollLoop() {
+ try {
+ while (!Thread.currentThread().isInterrupted() && udpConnected) {
+ DatagramChannel ch = udpChannel;
+ DtlsEndpoint endpoint = dtlsEndpoint;
+ if (ch == null || endpoint == null || !ch.isOpen()) break;
+
+ endpoint.poll();
+ Thread.onSpinWait();
+ }
+ } catch (Exception e) {
+ eventManager.executeEvent(new C_PacketReceivedFailedEvent(this, null, NetworkProtocol.UDP, e));
+ } finally {
+ boolean udpWas = udpConnected;
+
+ udpConnected = false;
+ udpBound = false;
+
+ if (udpWas) {
+ fireDisconnected(NetworkProtocol.UDP);
+ }
+
+ checkFullyDisconnectedIfNeeded();
+ }
+ }
+
+ private void onDtlsApplicationData(SocketAddress remote, ByteBuffer data) {
+ try {
+ Packet packet = codec.decodeFromBuffer(data);
+
+ if (processServerControlPacket(packet, NetworkProtocol.UDP)) {
+ return;
+ }
+
+ eventManager.executeEvent(new C_PacketReceivedEvent(this, packet, NetworkProtocol.UDP));
+ } catch (Exception e) {
+ eventManager.executeEvent(new C_PacketReceivedFailedEvent(this, null, NetworkProtocol.UDP, e));
+ }
+ }
+
+ /**
+ * Processes connection/bootstrap/control packets that affect connectivity state.
+ *
+ * @param packet received packet
+ * @param sourceProtocol source protocol
+ * @return true if handled (should not be forwarded as application packet)
+ */
+ private boolean processServerControlPacket(Packet packet, NetworkProtocol sourceProtocol) {
+ if (packet instanceof ConnectionIdPacket idPacket) {
+ this.connectionId = idPacket.connectionId();
+
+ // UDP-only bootstrap: once server assigned an id via UDP, the remote is already bound.
+ if (sourceProtocol == NetworkProtocol.UDP && !enabledProtocols.contains(NetworkProtocol.TCP)) {
+ this.udpBound = true;
+ }
+
+ checkFullyConnectedState();
+
+ // If TCP delivered the id and UDP is enabled, attempt bind now.
+ if (sourceProtocol == NetworkProtocol.TCP && enabledProtocols.contains(NetworkProtocol.UDP)) {
+ tryAutoBindUdp();
+ }
+
+ return true;
+ }
+
+ if (packet instanceof ProtocolRequirementsPacket reqPacket) {
+ this.serverRequiredProtocols = EnumSet.copyOf(reqPacket.requiredProtocols());
+ checkFullyConnectedState();
+ return true;
+ }
+
+ if (packet instanceof UdpBindAckPacket ackPacket) {
+ UUID id = connectionId;
+ if (id != null && id.equals(ackPacket.connectionId())) {
+ this.udpBound = true;
+ checkFullyConnectedState();
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ private void tryAutoBindUdp() {
+ UUID id = connectionId;
+ if (id == null) return;
+ if (!udpConnected) return;
+
+ try {
+ // Bind request goes over UDP/DTLS. Server responds with UdpBindAckPacket.
+ sendPacket(new UdpBindPacket(id), NetworkProtocol.UDP);
+ } catch (Exception e) {
+ eventManager.executeEvent(new C_PacketSendFailedEvent(this, new UdpBindPacket(id), NetworkProtocol.UDP, e));
+ }
+ }
+
+ private void fireDisconnected(NetworkProtocol protocol) {
+ eventManager.executeEvent(new ClientDisconnectedEvent(this, protocol));
+ }
+
+ private EnumSet requiredProtocolsForClientView() {
+ EnumSet r = serverRequiredProtocols;
+ if (r != null) return EnumSet.copyOf(r);
+
+ // Fallback for UI/debug/events only; does NOT influence isConnected().
+ return EnumSet.copyOf(enabledProtocols);
+ }
+
+ private void checkFullyConnectedState() {
+ if (wasFullyConnected) return;
+
+ if (!isConnected()) return;
+
+ wasFullyConnected = true;
+ eventManager.executeEvent(new ClientFullyConnectedEvent(this, requiredProtocolsForClientView()));
+ }
+
+ private void checkFullyDisconnectedIfNeeded() {
+ if (!wasFullyConnected) return;
+
+ if (isConnected()) return;
+
+ wasFullyConnected = false;
+ eventManager.executeEvent(new ClientFullyDisconnectedEvent(this, requiredProtocolsForClientView()));
}
/**
* Builder for {@link NetworkClient}.
*/
- public static final class ClientBuilder extends DefaultMethodsOverrider {
+ public static final class Builder {
- private String host;
- private int tcpPort;
- private int udpPort;
-
- private PacketHandler packetHandler;
- private EventManager eventManager;
- private Logger logger;
-
- private int timeout = 5000;
+ private Endpoint tcpEndpoint;
+ private Endpoint udpEndpoint;
+ private EnumSet enabledProtocols = EnumSet.of(NetworkProtocol.TCP, NetworkProtocol.UDP);
+ private PacketCodec codec;
private SSLSocketFactory tlsSocketFactory;
private SSLParameters tlsParameters;
private SSLContext dtlsContext;
- private File caFolder;
- private File clientCertFolder;
- private File clientKeyFolder;
+ private int timeoutMillis = 5000;
+ private int mtu = 1400;
- private Proxy proxy;
+ private EventManager eventManager;
- /**
- * Creates a TLS socket factory for TCP using PEM certificates/keys.
- *
- * @param caFolder folder containing CA PEM files
- * @param clientCertFolder folder containing client certificate files
- * @param clientKeyFolder folder containing client key files
- * @return TLS socket factory
- * @throws Exception on any certificate/key errors
- */
- public static SSLSocketFactory createTLSSocketFactory(File caFolder, File clientCertFolder, File clientKeyFolder) throws Exception {
- Objects.requireNonNull(caFolder, "caFolder");
- Objects.requireNonNull(clientCertFolder, "clientCertFolder");
- Objects.requireNonNull(clientKeyFolder, "clientKeyFolder");
+ public Builder setTcpEndpoint(Endpoint endpoint) {
+ this.tcpEndpoint = endpoint;
+ return this;
+ }
- // Trust store (CAs)
- KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
- trustStore.load(null, null);
+ public Builder setUdpEndpoint(Endpoint endpoint) {
+ this.udpEndpoint = endpoint;
+ return this;
+ }
- int caIndex = 1;
- File[] caFiles = caFolder.listFiles((f) -> f.isFile() && f.getName().endsWith(".pem"));
- if (caFiles != null) {
- for (File caFile : caFiles) {
- try (FileInputStream fis = new FileInputStream(caFile)) {
- CertificateFactory cf = CertificateFactory.getInstance("X.509");
- java.security.cert.Certificate caCert = cf.generateCertificate(fis);
- trustStore.setCertificateEntry("ca" + (caIndex++), caCert);
- }
- }
+ public Builder setEnabledProtocols(EnumSet protocols) {
+ this.enabledProtocols = EnumSet.copyOf(Objects.requireNonNull(protocols, "protocols"));
+ if (this.enabledProtocols.isEmpty()) {
+ throw new IllegalArgumentException("enabledProtocols must not be empty");
}
-
- TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
- tmf.init(trustStore);
-
- // Key store (client cert + key)
- KeyStore keyStore = KeyStore.getInstance("PKCS12");
- keyStore.load(null, null);
-
- int clientIndex = 1;
- File[] certFiles = clientCertFolder.listFiles((f) -> f.isFile() && (f.getName().endsWith(".crt") || f.getName().endsWith(".pem")));
- if (certFiles != null) {
- for (File certFile : certFiles) {
- String baseName = certFile.getName()
- .replace(".crt", "")
- .replace(".pem", "");
- File keyFile = new File(clientKeyFolder, baseName + ".key");
- if (!keyFile.exists()) continue;
-
- java.security.PrivateKey key = PemUtils.loadPrivateKey(keyFile);
- java.security.cert.Certificate cert = PemUtils.loadCertificate(certFile);
-
- keyStore.setKeyEntry("client" + (clientIndex++), key, null, new java.security.cert.Certificate[]{cert});
- }
- }
-
- KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
- kmf.init(keyStore, null);
-
- SSLContext tls = SSLContext.getInstance("TLSv1.3");
- tls.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
- return tls.getSocketFactory();
- }
-
- /**
- * Creates a DTLS context using the same key/trust setup as TLS.
- *
- * @param caFolder folder containing CA PEM files
- * @param clientCertFolder folder containing client certificate files
- * @param clientKeyFolder folder containing client key files
- * @return DTLS SSL context
- * @throws Exception on any certificate/key errors
- */
- public static SSLContext createDTLSContext(File caFolder, File clientCertFolder, File clientKeyFolder) throws Exception {
- Objects.requireNonNull(caFolder, "caFolder");
- Objects.requireNonNull(clientCertFolder, "clientCertFolder");
- Objects.requireNonNull(clientKeyFolder, "clientKeyFolder");
-
- // Trust store
- KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
- trustStore.load(null, null);
-
- int caIndex = 1;
- File[] caFiles = caFolder.listFiles((f) -> f.isFile() && f.getName().endsWith(".pem"));
- if (caFiles != null) {
- for (File caFile : caFiles) {
- try (FileInputStream fis = new FileInputStream(caFile)) {
- CertificateFactory cf = CertificateFactory.getInstance("X.509");
- java.security.cert.Certificate caCert = cf.generateCertificate(fis);
- trustStore.setCertificateEntry("ca" + (caIndex++), caCert);
- }
- }
- }
-
- TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
- tmf.init(trustStore);
-
- // Key store
- KeyStore keyStore = KeyStore.getInstance("PKCS12");
- keyStore.load(null, null);
-
- int clientIndex = 1;
- File[] certFiles = clientCertFolder.listFiles((f) -> f.isFile() && (f.getName().endsWith(".crt") || f.getName().endsWith(".pem")));
- if (certFiles != null) {
- for (File certFile : certFiles) {
- String baseName = certFile.getName()
- .replace(".crt", "")
- .replace(".pem", "");
- File keyFile = new File(clientKeyFolder, baseName + ".key");
- if (!keyFile.exists()) continue;
-
- java.security.PrivateKey key = PemUtils.loadPrivateKey(keyFile);
- java.security.cert.Certificate cert = PemUtils.loadCertificate(certFile);
-
- keyStore.setKeyEntry("client" + (clientIndex++), key, null, new java.security.cert.Certificate[]{cert});
- }
- }
-
- KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
- kmf.init(keyStore, null);
-
- SSLContext dtls = SSLContext.getInstance("DTLS");
- dtls.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
- return dtls;
- }
-
- /**
- * Sets server host.
- */
- public ClientBuilder setHost(String host) {
- this.host = host;
return this;
}
- /**
- * Sets TCP port.
- */
- public ClientBuilder setTcpPort(int tcpPort) {
- this.tcpPort = tcpPort;
+ public Builder setCodec(PacketCodec codec) {
+ this.codec = codec;
return this;
}
- /**
- * Sets UDP port.
- */
- public ClientBuilder setUdpPort(int udpPort) {
- this.udpPort = udpPort;
- return this;
- }
-
- /**
- * Sets packet handler.
- */
- public ClientBuilder setPacketHandler(PacketHandler handler) {
- this.packetHandler = handler;
- return this;
- }
-
- /**
- * Sets event manager.
- */
- public ClientBuilder setEventManager(EventManager manager) {
- this.eventManager = manager;
- return this;
- }
-
- /**
- * Sets logger.
- */
- public ClientBuilder setLogger(Logger logger) {
- this.logger = logger;
- return this;
- }
-
- /**
- * Sets timeout in millis.
- */
- public ClientBuilder setTimeout(int timeoutMillis) {
- this.timeout = timeoutMillis;
- return this;
- }
-
- /**
- * Sets TLS socket factory explicitly.
- */
- public ClientBuilder setTLSSocketFactory(SSLSocketFactory factory) {
+ public Builder setTlsSocketFactory(SSLSocketFactory factory) {
this.tlsSocketFactory = factory;
return this;
}
- /**
- * Sets TLS parameters (optional).
- */
- public ClientBuilder setTLSParameters(SSLParameters params) {
- this.tlsParameters = params;
+ public Builder setTlsParameters(SSLParameters parameters) {
+ this.tlsParameters = parameters;
return this;
}
- /**
- * Sets DTLS context explicitly.
- */
- public ClientBuilder setDTLSContext(SSLContext dtlsContext) {
- this.dtlsContext = dtlsContext;
+ public Builder setDtlsContext(SSLContext context) {
+ this.dtlsContext = context;
return this;
}
- /**
- * Sets root CA folder for auto-creating TLS/DTLS when factories/contexts are not provided.
- */
- public ClientBuilder setRootCAFolder(File folder) {
- this.caFolder = folder;
+ public Builder setTimeoutMillis(int timeoutMillis) {
+ this.timeoutMillis = timeoutMillis;
return this;
}
- /**
- * Sets client certificate + key folders for auto-creating TLS/DTLS.
- */
- public ClientBuilder setClientCertificatesFolder(File clientCertFolder, File clientKeyFolder) {
- this.clientCertFolder = clientCertFolder;
- this.clientKeyFolder = clientKeyFolder;
+ public Builder setMtu(int mtu) {
+ this.mtu = mtu;
return this;
}
- /**
- * Sets optional proxy.
- */
- public ClientBuilder setProxy(Proxy proxy) {
- this.proxy = proxy;
+ public Builder setEventManager(EventManager eventManager) {
+ this.eventManager = eventManager;
return this;
}
- /**
- * Builds the client.
- *
- * @return client
- */
public NetworkClient build() {
- if (host == null) throw new IllegalStateException("Host not set");
- if (tcpPort <= 0) throw new IllegalStateException("TCP port not set");
- if (udpPort <= 0) throw new IllegalStateException("UDP port not set");
- if (packetHandler == null) throw new IllegalStateException("PacketHandler not set");
- if (eventManager == null) throw new IllegalStateException("EventManager not set");
+ if (codec == null) throw new IllegalStateException("codec not set");
+ if (eventManager == null) throw new IllegalStateException("eventManager not set");
- // Auto-create TLS/DTLS from folders if not explicitly provided
- if ((tlsSocketFactory == null || dtlsContext == null) && caFolder != null) {
- if (clientCertFolder == null || clientKeyFolder == null) {
- throw new IllegalStateException("Client cert/key folders must be set when using CA folder auto-config.");
- }
- try {
- if (tlsSocketFactory == null) {
- tlsSocketFactory = createTLSSocketFactory(caFolder, clientCertFolder, clientKeyFolder);
- }
- if (dtlsContext == null) {
- dtlsContext = createDTLSContext(caFolder, clientCertFolder, clientKeyFolder);
- }
- } catch (Exception e) {
- throw new RuntimeException("Failed to create TLS/DTLS client configuration", e);
- }
+ if (enabledProtocols.contains(NetworkProtocol.TCP)) {
+ if (tcpEndpoint == null) throw new IllegalStateException("tcpEndpoint not set (TCP enabled)");
+ if (tlsSocketFactory == null) throw new IllegalStateException("tlsSocketFactory not set (TLS mandatory)");
}
- if (tlsSocketFactory == null) throw new IllegalStateException("TLS socket factory missing");
- if (dtlsContext == null) throw new IllegalStateException("DTLS context missing");
+ if (enabledProtocols.contains(NetworkProtocol.UDP)) {
+ if (udpEndpoint == null) throw new IllegalStateException("udpEndpoint not set (UDP enabled)");
+ if (dtlsContext == null) throw new IllegalStateException("dtlsContext not set (DTLS mandatory for UDP)");
+ }
+
+ if (timeoutMillis <= 0) throw new IllegalStateException("timeoutMillis must be > 0");
+ if (mtu < 256) throw new IllegalStateException("mtu too small");
+
+ // Placeholders are never used when protocol is disabled, but must be non-null for constructor invariants.
+ Endpoint tcpEp = tcpEndpoint != null ? tcpEndpoint : new Endpoint("127.0.0.1", 1);
+ Endpoint udpEp = udpEndpoint != null ? udpEndpoint : new Endpoint("127.0.0.1", 1);
+ SSLContext dtls = dtlsContext != null ? dtlsContext : emptyDtls();
return new NetworkClient(
- host,
- tcpPort,
- udpPort,
- packetHandler,
- eventManager,
- logger,
- timeout,
- tlsSocketFactory,
+ tcpEp,
+ udpEp,
+ enabledProtocols,
+ codec,
+ tlsSocketFactory != null ? tlsSocketFactory : (SSLSocketFactory) SSLSocketFactory.getDefault(),
tlsParameters,
- dtlsContext,
- proxy
+ dtls,
+ timeoutMillis,
+ mtu,
+ eventManager
);
}
+
+ private static SSLContext emptyDtls() {
+ try {
+ return SSLContext.getInstance("DTLS");
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to create DTLS context", e);
+ }
+ }
}
}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_PacketReceivedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_PacketReceivedEvent.java
index 6e24468..47e3da8 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_PacketReceivedEvent.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_PacketReceivedEvent.java
@@ -6,52 +6,61 @@
* See LICENSE-File if exists
*/
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
package dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.receive;
import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
import dev.unlegitdqrk.unlegitlibrary.network.system.client.NetworkClient;
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
+
+import java.util.Objects;
/**
- * Fired when a packet was received by the client on a specific transport.
+ * Fired when a packet was received by the client on a specific protocol.
*/
public final class C_PacketReceivedEvent extends Event {
- private final NetworkClient networkClient;
+ private final NetworkClient client;
private final Packet packet;
- private final Transport transport;
+ private final NetworkProtocol protocol;
/**
* Creates a new packet received event.
*
- * @param networkClient client instance
- * @param packet received packet
- * @param transport transport the packet was received on
+ * @param client client instance
+ * @param packet received packet
+ * @param protocol protocol the packet was received on
*/
- public C_PacketReceivedEvent(NetworkClient networkClient, Packet packet, Transport transport) {
- this.networkClient = networkClient;
- this.packet = packet;
- this.transport = transport;
+ public C_PacketReceivedEvent(NetworkClient client, Packet packet, NetworkProtocol protocol) {
+ this.client = Objects.requireNonNull(client, "client");
+ this.packet = Objects.requireNonNull(packet, "packet");
+ this.protocol = Objects.requireNonNull(protocol, "protocol");
}
- public NetworkClient getNetworkClient() {
- return networkClient;
+ /**
+ * Returns the client instance.
+ *
+ * @return client
+ */
+ public NetworkClient getClient() {
+ return client;
}
+ /**
+ * Returns the received packet.
+ *
+ * @return packet
+ */
public Packet getPacket() {
return packet;
}
- public Transport getTransport() {
- return transport;
+ /**
+ * Returns the protocol the packet was received on.
+ *
+ * @return protocol
+ */
+ public NetworkProtocol getProtocol() {
+ return protocol;
}
}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_PacketReceivedFailedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_PacketReceivedFailedEvent.java
index 4a3326c..61aad33 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_PacketReceivedFailedEvent.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_PacketReceivedFailedEvent.java
@@ -6,52 +6,79 @@
* See LICENSE-File if exists
*/
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
package dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.receive;
import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
import dev.unlegitdqrk.unlegitlibrary.network.system.client.NetworkClient;
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
+
+import java.util.Objects;
/**
- * Fired when a packet receive failed on the client on a specific transport.
+ * Fired when receiving or decoding a packet failed on the client
+ * for a specific protocol.
*/
public final class C_PacketReceivedFailedEvent extends Event {
- private final NetworkClient networkClient;
+ private final NetworkClient client;
private final Packet packet;
- private final Transport transport;
+ private final NetworkProtocol protocol;
+ private final Exception error;
/**
* Creates a new packet receive failed event.
*
- * @param networkClient client instance
- * @param packet packet that failed to be read/handled
- * @param transport transport the packet was received on
+ * @param client client instance
+ * @param packet packet that failed to be processed (may be null if undecodable)
+ * @param protocol protocol the failure happened on
+ * @param error root cause
*/
- public C_PacketReceivedFailedEvent(NetworkClient networkClient, Packet packet, Transport transport) {
- this.networkClient = networkClient;
- this.packet = packet;
- this.transport = transport;
+ public C_PacketReceivedFailedEvent(
+ NetworkClient client,
+ Packet packet,
+ NetworkProtocol protocol,
+ Exception error
+ ) {
+ this.client = Objects.requireNonNull(client, "client");
+ this.packet = packet; // may be null
+ this.protocol = Objects.requireNonNull(protocol, "protocol");
+ this.error = Objects.requireNonNull(error, "error");
}
- public NetworkClient getNetworkClient() {
- return networkClient;
+ /**
+ * Returns the client instance.
+ *
+ * @return client
+ */
+ public NetworkClient getClient() {
+ return client;
}
+ /**
+ * Returns the packet that failed to be processed, if available.
+ *
+ * @return packet or null
+ */
public Packet getPacket() {
return packet;
}
- public Transport getTransport() {
- return transport;
+ /**
+ * Returns the protocol the failure occurred on.
+ *
+ * @return protocol
+ */
+ public NetworkProtocol getProtocol() {
+ return protocol;
+ }
+
+ /**
+ * Returns the underlying error.
+ *
+ * @return error
+ */
+ public Exception getError() {
+ return error;
}
}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_UnknownObjectReceivedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_UnknownObjectReceivedEvent.java
index f748218..33e83f9 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_UnknownObjectReceivedEvent.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/receive/C_UnknownObjectReceivedEvent.java
@@ -6,51 +6,65 @@
* See LICENSE-File if exists
*/
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
package dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.receive;
import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
import dev.unlegitdqrk.unlegitlibrary.network.system.client.NetworkClient;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
+import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
+
+import java.util.Objects;
/**
- * Fired when an unknown object was received on a specific transport.
+ * Fired when an unknown (non-packet) object was received on a specific protocol.
+ *
+ * Note: In v2 the default transport is {@link dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketCodec}
+ * based and typically produces {@link Packet}.
+ * This event exists for backwards compatibility and for custom codecs/handlers.
*/
public final class C_UnknownObjectReceivedEvent extends Event {
- private final NetworkClient networkClient;
+ private final NetworkClient client;
private final Object received;
- private final Transport transport;
+ private final NetworkProtocol protocol;
/**
* Creates a new unknown object received event.
*
- * @param networkClient client instance
- * @param received received object
- * @param transport transport the object was received on
+ * @param client client instance
+ * @param received received object
+ * @param protocol protocol the object was received on
*/
- public C_UnknownObjectReceivedEvent(NetworkClient networkClient, Object received, Transport transport) {
- this.networkClient = networkClient;
- this.received = received;
- this.transport = transport;
+ public C_UnknownObjectReceivedEvent(NetworkClient client, Object received, NetworkProtocol protocol) {
+ this.client = Objects.requireNonNull(client, "client");
+ this.received = Objects.requireNonNull(received, "received");
+ this.protocol = Objects.requireNonNull(protocol, "protocol");
}
- public NetworkClient getNetworkClient() {
- return networkClient;
+ /**
+ * Returns the client instance.
+ *
+ * @return client
+ */
+ public NetworkClient getClient() {
+ return client;
}
+ /**
+ * Returns the received object.
+ *
+ * @return received object
+ */
public Object getReceived() {
return received;
}
- public Transport getTransport() {
- return transport;
+ /**
+ * Returns the protocol the object was received on.
+ *
+ * @return protocol
+ */
+ public NetworkProtocol getProtocol() {
+ return protocol;
}
}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/send/C_PacketSendEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/send/C_PacketSendEvent.java
index a433072..7867627 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/send/C_PacketSendEvent.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/send/C_PacketSendEvent.java
@@ -6,52 +6,71 @@
* See LICENSE-File if exists
*/
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
package dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.send;
import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
import dev.unlegitdqrk.unlegitlibrary.network.system.client.NetworkClient;
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
+
+import java.util.Objects;
/**
- * Fired when a packet was sent by the client on a specific transport.
+ * Fired when a packet was successfully sent by the client on a specific protocol.
+ *
+ * v2 mapping:
+ *
+ * - {@link NetworkProtocol#TCP} → TLS/TCP
+ * - {@link NetworkProtocol#UDP} → DTLS/UDP
+ *
*/
public final class C_PacketSendEvent extends Event {
- private final NetworkClient networkClient;
+ private final NetworkClient client;
private final Packet packet;
- private final Transport transport;
+ private final NetworkProtocol protocol;
/**
* Creates a new packet send event.
*
- * @param networkClient client instance
- * @param packet sent packet
- * @param transport used transport
+ * @param client client instance
+ * @param packet sent packet
+ * @param protocol used protocol
*/
- public C_PacketSendEvent(NetworkClient networkClient, Packet packet, Transport transport) {
- this.networkClient = networkClient;
- this.packet = packet;
- this.transport = transport;
+ public C_PacketSendEvent(
+ NetworkClient client,
+ Packet packet,
+ NetworkProtocol protocol
+ ) {
+ this.client = Objects.requireNonNull(client, "client");
+ this.packet = Objects.requireNonNull(packet, "packet");
+ this.protocol = Objects.requireNonNull(protocol, "protocol");
}
- public NetworkClient getNetworkClient() {
- return networkClient;
+ /**
+ * Returns the client.
+ *
+ * @return client
+ */
+ public NetworkClient getClient() {
+ return client;
}
+ /**
+ * Returns the sent packet.
+ *
+ * @return packet
+ */
public Packet getPacket() {
return packet;
}
- public Transport getTransport() {
- return transport;
+ /**
+ * Returns the protocol used.
+ *
+ * @return protocol
+ */
+ public NetworkProtocol getProtocol() {
+ return protocol;
}
}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/send/C_PacketSendFailedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/send/C_PacketSendFailedEvent.java
index ee63090..e415816 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/send/C_PacketSendFailedEvent.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/packets/send/C_PacketSendFailedEvent.java
@@ -6,52 +6,78 @@
* See LICENSE-File if exists
*/
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
package dev.unlegitdqrk.unlegitlibrary.network.system.client.events.packets.send;
import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
import dev.unlegitdqrk.unlegitlibrary.network.system.client.NetworkClient;
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
+
+import java.util.Objects;
/**
- * Fired when a packet send failed on the client on a specific transport.
+ * Fired when a packet send failed on the client on a specific protocol.
*/
public final class C_PacketSendFailedEvent extends Event {
- private final NetworkClient networkClient;
+ private final NetworkClient client;
private final Packet packet;
- private final Transport transport;
+ private final NetworkProtocol protocol;
+ private final Exception error;
/**
* Creates a new packet send failed event.
*
- * @param networkClient client instance
- * @param packet packet that failed to be sent
- * @param transport intended transport
+ * @param client client instance
+ * @param packet packet that failed to be sent
+ * @param protocol intended protocol
+ * @param error root cause
*/
- public C_PacketSendFailedEvent(NetworkClient networkClient, Packet packet, Transport transport) {
- this.networkClient = networkClient;
- this.packet = packet;
- this.transport = transport;
+ public C_PacketSendFailedEvent(
+ NetworkClient client,
+ Packet packet,
+ NetworkProtocol protocol,
+ Exception error
+ ) {
+ this.client = Objects.requireNonNull(client, "client");
+ this.packet = Objects.requireNonNull(packet, "packet");
+ this.protocol = Objects.requireNonNull(protocol, "protocol");
+ this.error = Objects.requireNonNull(error, "error");
}
- public NetworkClient getNetworkClient() {
- return networkClient;
+ /**
+ * Returns the client.
+ *
+ * @return client
+ */
+ public NetworkClient getClient() {
+ return client;
}
+ /**
+ * Returns the packet that failed to be sent.
+ *
+ * @return packet
+ */
public Packet getPacket() {
return packet;
}
- public Transport getTransport() {
- return transport;
+ /**
+ * Returns the intended protocol.
+ *
+ * @return protocol
+ */
+ public NetworkProtocol getProtocol() {
+ return protocol;
+ }
+
+ /**
+ * Returns the underlying error.
+ *
+ * @return error
+ */
+ public Exception getError() {
+ return error;
}
}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/connect/ClientConnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/connect/ClientConnectedEvent.java
index d0d0643..db290f1 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/connect/ClientConnectedEvent.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/connect/ClientConnectedEvent.java
@@ -6,53 +6,37 @@
* See LICENSE-File if exists
*/
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
package dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.connect;
import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
import dev.unlegitdqrk.unlegitlibrary.network.system.client.NetworkClient;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
import java.util.Objects;
/**
- * Fired when the client established a specific transport connection.
+ * Fired when the client established a specific protocol connection.
*
- * This event is transport-specific:
+ *
Protocol-specific:
*
- * - {@link Transport#TCP}: after TLS handshake + TCP streams are ready
- * - {@link Transport#UDP}: after DTLS handshake + UDP bind was initiated
+ * - {@link NetworkProtocol#TCP}: after TLS handshake + TCP streams are ready
+ * - {@link NetworkProtocol#UDP}: after DTLS handshake (channel/engine ready)
*
*/
public final class ClientConnectedEvent extends Event {
private final NetworkClient client;
- private final Transport transport;
+ private final NetworkProtocol protocol;
/**
* Creates a new client connected event.
*
- * @param client client instance
- * @param transport connected transport
+ * @param client client instance
+ * @param protocol connected protocol
*/
- public ClientConnectedEvent(NetworkClient client, Transport transport) {
+ public ClientConnectedEvent(NetworkClient client, NetworkProtocol protocol) {
this.client = Objects.requireNonNull(client, "client");
- this.transport = Objects.requireNonNull(transport, "transport");
+ this.protocol = Objects.requireNonNull(protocol, "protocol");
}
/**
@@ -65,11 +49,11 @@ public final class ClientConnectedEvent extends Event {
}
/**
- * Returns the transport that was connected.
+ * Returns the protocol that was connected.
*
- * @return transport
+ * @return protocol
*/
- public Transport getTransport() {
- return transport;
+ public NetworkProtocol getProtocol() {
+ return protocol;
}
}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/connect/ClientFullyConnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/connect/ClientFullyConnectedEvent.java
index d836019..660691c 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/connect/ClientFullyConnectedEvent.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/connect/ClientFullyConnectedEvent.java
@@ -6,47 +6,38 @@
* See LICENSE-File if exists
*/
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
package dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.connect;
import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
import dev.unlegitdqrk.unlegitlibrary.network.system.client.NetworkClient;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
+
+import java.util.EnumSet;
+import java.util.Objects;
/**
- * Fired when the client has fully established the configured transport policy.
+ * Fired when the client satisfies its enabled protocol requirements.
*
- * For your setup this means: TCP (TLS) connected, ClientID received and UDP (DTLS) bound.
+ * In v2 this typically means:
+ *
+ * - TCP is connected (TLS ok)
+ * - and if UDP is enabled: DTLS is established and the bind flow is completed
+ *
*/
public final class ClientFullyConnectedEvent extends Event {
private final NetworkClient client;
- private final Transport[] requiredTransports;
+ private final EnumSet requiredProtocols;
/**
* Creates a new event.
*
- * @param client client instance
- * @param requiredTransports transports that are required and now satisfied
+ * @param client client instance
+ * @param requiredProtocols protocols that are required and now satisfied
*/
- public ClientFullyConnectedEvent(NetworkClient client, Transport[] requiredTransports) {
- this.client = client;
- this.requiredTransports = requiredTransports;
+ public ClientFullyConnectedEvent(NetworkClient client, EnumSet requiredProtocols) {
+ this.client = Objects.requireNonNull(client, "client");
+ this.requiredProtocols = EnumSet.copyOf(Objects.requireNonNull(requiredProtocols, "requiredProtocols"));
}
/**
@@ -59,11 +50,11 @@ public final class ClientFullyConnectedEvent extends Event {
}
/**
- * Returns the required transports that are now established.
+ * Returns the required protocols that are now established.
*
- * @return required transports
+ * @return required protocols (copy)
*/
- public Transport[] getRequiredTransports() {
- return requiredTransports;
+ public EnumSet getRequiredProtocols() {
+ return EnumSet.copyOf(requiredProtocols);
}
}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/disconnect/ClientDisconnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/disconnect/ClientDisconnectedEvent.java
index cac7034..59bb34b 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/disconnect/ClientDisconnectedEvent.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/disconnect/ClientDisconnectedEvent.java
@@ -6,53 +6,37 @@
* See LICENSE-File if exists
*/
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
package dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.disconnect;
import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
import dev.unlegitdqrk.unlegitlibrary.network.system.client.NetworkClient;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
import java.util.Objects;
/**
- * Fired when the client lost/closed a specific transport connection.
+ * Fired when the client lost/closed a specific protocol connection.
*
- * This event is transport-specific:
+ *
Protocol-specific:
*
- * - {@link Transport#TCP}: TCP/TLS socket closed or receive loop ended
- * - {@link Transport#UDP}: UDP channel/DTLS endpoint closed
+ * - {@link NetworkProtocol#TCP}: TCP/TLS socket closed or receive loop ended
+ * - {@link NetworkProtocol#UDP}: UDP channel/DTLS endpoint closed
*
*/
public final class ClientDisconnectedEvent extends Event {
private final NetworkClient client;
- private final Transport transport;
+ private final NetworkProtocol protocol;
/**
* Creates a new client disconnected event.
*
- * @param client client instance
- * @param transport disconnected transport
+ * @param client client instance
+ * @param protocol disconnected protocol
*/
- public ClientDisconnectedEvent(NetworkClient client, Transport transport) {
+ public ClientDisconnectedEvent(NetworkClient client, NetworkProtocol protocol) {
this.client = Objects.requireNonNull(client, "client");
- this.transport = Objects.requireNonNull(transport, "transport");
+ this.protocol = Objects.requireNonNull(protocol, "protocol");
}
/**
@@ -65,11 +49,11 @@ public final class ClientDisconnectedEvent extends Event {
}
/**
- * Returns the transport that was disconnected.
+ * Returns the protocol that was disconnected.
*
- * @return transport
+ * @return protocol
*/
- public Transport getTransport() {
- return transport;
+ public NetworkProtocol getProtocol() {
+ return protocol;
}
}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/disconnect/ClientFullyDisconnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/disconnect/ClientFullyDisconnectedEvent.java
index 278530a..b78e0b2 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/disconnect/ClientFullyDisconnectedEvent.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/client/events/state/disconnect/ClientFullyDisconnectedEvent.java
@@ -6,51 +6,32 @@
* See LICENSE-File if exists
*/
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
package dev.unlegitdqrk.unlegitlibrary.network.system.client.events.state.disconnect;
import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
import dev.unlegitdqrk.unlegitlibrary.network.system.client.NetworkClient;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
+import java.util.EnumSet;
import java.util.Objects;
/**
- * Fired when the client was fully connected and then became fully disconnected.
- *
- * For your policy (TCP+UDP required) this means:
- * previously: TCP+UDP connected and ClientID present,
- * now: not fully connected anymore (both transports are not satisfying the requirement).
+ * Fired when the client previously satisfied its required protocols and then stopped satisfying them.
*/
public final class ClientFullyDisconnectedEvent extends Event {
private final NetworkClient client;
- private final Transport[] requiredTransports;
+ private final EnumSet requiredProtocols;
/**
* Creates a new fully disconnected event.
*
- * @param client client instance
- * @param requiredTransports required transports according to policy
+ * @param client client instance
+ * @param requiredProtocols protocols that were required for full connectivity
*/
- public ClientFullyDisconnectedEvent(NetworkClient client, Transport[] requiredTransports) {
+ public ClientFullyDisconnectedEvent(NetworkClient client, EnumSet requiredProtocols) {
this.client = Objects.requireNonNull(client, "client");
- this.requiredTransports = Objects.requireNonNull(requiredTransports, "requiredTransports");
+ this.requiredProtocols = EnumSet.copyOf(Objects.requireNonNull(requiredProtocols, "requiredProtocols"));
}
/**
@@ -63,11 +44,11 @@ public final class ClientFullyDisconnectedEvent extends Event {
}
/**
- * Returns transports that were required for full connectivity.
+ * Returns protocols that were required for full connectivity.
*
- * @return required transports
+ * @return required protocols (copy)
*/
- public Transport[] getRequiredTransports() {
- return requiredTransports;
+ public EnumSet getRequiredProtocols() {
+ return EnumSet.copyOf(requiredProtocols);
}
}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/Packet.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/Packet.java
index a8b9de5..a226c86 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/Packet.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/Packet.java
@@ -1,22 +1,62 @@
+/*
+ * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
+ *
+ * You are unauthorized to remove this copyright.
+ * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
+ * See LICENSE-File if exists
+ */
+
package dev.unlegitdqrk.unlegitlibrary.network.system.packets;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
+/**
+ * Base type for packets used by the v2 secure network system.
+ *
+ * Packets are encoded as:
+ *
+ * [int packetId][payload bytes...]
+ *
+ * Payload is written/read by the packet itself through Java serialization streams.
+ */
public abstract class Packet {
- private final int id;
+ private final int packetId;
- public Packet(int id) {
- this.id = id;
+ /**
+ * Creates a new packet.
+ *
+ * @param packetId unique packet id
+ */
+ protected Packet(int packetId) {
+ this.packetId = packetId;
}
- public final int getPacketID() {
- return id;
+ /**
+ * Returns the packet id.
+ *
+ * @return id
+ */
+ public final int packetId() {
+ return packetId;
}
- public abstract void write(PacketHandler packetHandler, ObjectOutputStream outputStream) throws IOException, ClassNotFoundException;
+ /**
+ * Writes payload fields to the stream.
+ *
+ * @param out output stream
+ * @throws IOException on I/O errors
+ */
+ public abstract void write(ObjectOutputStream out) throws IOException;
- public abstract void read(PacketHandler packetHandler, ObjectInputStream outputStream) throws IOException, ClassNotFoundException;
-}
\ No newline at end of file
+ /**
+ * Reads payload fields from the stream.
+ *
+ * @param in input stream
+ * @throws IOException on I/O errors
+ * @throws ClassNotFoundException on deserialization errors
+ */
+ public abstract void read(ObjectInputStream in) throws IOException, ClassNotFoundException;
+}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketCodec.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketCodec.java
new file mode 100644
index 0000000..830a29b
--- /dev/null
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketCodec.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
+ *
+ * You are unauthorized to remove this copyright.
+ * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
+ * See LICENSE-File if exists
+ */
+
+package dev.unlegitdqrk.unlegitlibrary.network.system.packets;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+/**
+ * Encodes and decodes {@link Packet} instances for TCP (stream) and UDP (datagram).
+ *
+ * Wire format (transport-agnostic):
+ *
+ * [int packetId][ObjectStream payload...]
+ *
+ */
+public final class PacketCodec {
+
+ private final PacketRegistry registry;
+
+ /**
+ * Creates a new codec.
+ *
+ * @param registry packet registry
+ */
+ public PacketCodec(PacketRegistry registry) {
+ this.registry = Objects.requireNonNull(registry, "registry");
+ }
+
+ /**
+ * Encodes a packet into a byte buffer suitable for UDP/DTLS sending.
+ *
+ * @param packet packet to encode
+ * @return encoded buffer (position=0, limit=length)
+ * @throws IOException on serialization errors
+ */
+ public ByteBuffer encodeToBuffer(Packet packet) throws IOException {
+ Objects.requireNonNull(packet, "packet");
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(256);
+ try (DataOutputStream dos = new DataOutputStream(baos)) {
+ dos.writeInt(packet.packetId());
+ try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
+ packet.write(oos);
+ oos.flush();
+ }
+ }
+ return ByteBuffer.wrap(baos.toByteArray());
+ }
+
+ /**
+ * Decodes a packet from a received datagram buffer.
+ *
+ * @param buffer received buffer (position..limit)
+ * @return decoded packet
+ * @throws IOException on I/O errors
+ * @throws ClassNotFoundException on deserialization errors
+ */
+ public Packet decodeFromBuffer(ByteBuffer buffer) throws IOException, ClassNotFoundException {
+ Objects.requireNonNull(buffer, "buffer");
+
+ ByteArrayInputStream bais =
+ new ByteArrayInputStream(buffer.array(), buffer.position(), buffer.remaining());
+
+ int packetId;
+ try (DataInputStream dis = new DataInputStream(bais)) {
+ packetId = dis.readInt();
+ }
+
+ Packet packet = registry.create(packetId);
+
+ try (ObjectInputStream ois = new ObjectInputStream(bais)) {
+ packet.read(ois);
+ }
+
+ return packet;
+ }
+
+ /**
+ * Sends a packet over a TCP/TLS stream.
+ *
+ * @param packet packet
+ * @param out output stream
+ * @throws IOException on I/O errors
+ */
+ public void sendToStream(Packet packet, ObjectOutputStream out) throws IOException {
+ Objects.requireNonNull(packet, "packet");
+ Objects.requireNonNull(out, "out");
+
+ out.writeInt(packet.packetId());
+ packet.write(out);
+ out.flush();
+ }
+
+ /**
+ * Receives a packet from a TCP/TLS stream.
+ *
+ * @param in input stream
+ * @return decoded packet
+ * @throws IOException on I/O errors
+ * @throws ClassNotFoundException on deserialization errors
+ */
+ public Packet receiveFromStream(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ Objects.requireNonNull(in, "in");
+
+ int packetId = in.readInt();
+ Packet packet = registry.create(packetId);
+ packet.read(in);
+ return packet;
+ }
+}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketHandler.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketHandler.java
deleted file mode 100644
index 5214013..0000000
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketHandler.java
+++ /dev/null
@@ -1,71 +0,0 @@
-package dev.unlegitdqrk.unlegitlibrary.network.system.packets;
-
-import dev.unlegitdqrk.unlegitlibrary.network.system.client.NetworkClient;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.NetworkServer;
-import dev.unlegitdqrk.unlegitlibrary.utils.DefaultMethodsOverrider;
-
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.util.HashMap;
-import java.util.Map;
-
-public final class PacketHandler extends DefaultMethodsOverrider {
-
- private final Map packets = new HashMap<>();
-
- private NetworkClient clientInstance;
- private NetworkServer serverInstance;
-
- public NetworkClient getClientInstance() {
- return clientInstance;
- }
-
- public void setClientInstance(NetworkClient clientInstance) {
- if (this.clientInstance == null) this.clientInstance = clientInstance;
- }
-
- public NetworkServer getServerInstance() {
- return serverInstance;
- }
-
- public void setServerInstance(NetworkServer serverInstance) {
- if (this.serverInstance == null) this.serverInstance = serverInstance;
- }
-
- public boolean isPacketIDRegistered(int id) {
- return packets.containsKey(id);
- }
-
- public Packet getPacketByID(int id) {
- return packets.get(id);
- }
-
- public boolean registerPacket(Packet packet) {
- int id = packet.getPacketID();
-
- if (isPacketIDRegistered(id)) return false;
-
- packets.put(id, packet);
- return true;
- }
-
- public boolean handlePacket(int id, Packet packet, ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
- if (!isPacketIDRegistered(id) || (packet != null && id != packet.getPacketID()) || (packet != null && !isPacketIDRegistered(packet.getPacketID())))
- return false;
-
- packet.read(this, inputStream);
- return true;
- }
-
- public boolean sendPacket(Packet packet, ObjectOutputStream outputStream) throws IOException, ClassNotFoundException {
- int id = packet.getPacketID();
- if (!isPacketIDRegistered(id)) return false;
-
- outputStream.writeObject(id);
- packet.write(this, outputStream);
- outputStream.flush();
-
- return true;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketRegistry.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketRegistry.java
new file mode 100644
index 0000000..bbd0b33
--- /dev/null
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/PacketRegistry.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
+ *
+ * You are unauthorized to remove this copyright.
+ * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
+ * See LICENSE-File if exists
+ */
+
+package dev.unlegitdqrk.unlegitlibrary.network.system.packets;
+
+import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.*;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Supplier;
+
+/**
+ * Thread-safe registry that maps packet ids to factories creating new packet instances.
+ *
+ * This avoids reusing the same packet instance across threads/connections.
+ */
+public final class PacketRegistry {
+
+ private final Map> factories = new ConcurrentHashMap<>();
+
+ public PacketRegistry() {
+ register(ConnectionIdPacket::new);
+ register(ProtocolRequirementsPacket::new);
+ register(UdpBindAckPacket::new);
+ register(UdpBindPacket::new);
+ register(UdpHelloPacket::new);
+ }
+
+ /**
+ * Registers a packet factory for the given id.
+ *
+ * @param factory instance factory
+ * @return true if newly registered, false if id already present
+ */
+ public boolean register(Supplier extends Packet> factory) {
+ Objects.requireNonNull(factory, "factory");
+ return factories.putIfAbsent(factory.get().packetId(), factory) == null;
+ }
+
+ /**
+ * Returns true if a packet id is registered.
+ *
+ * @param packetId id
+ * @return true if registered
+ */
+ public boolean isRegistered(int packetId) {
+ return factories.containsKey(packetId);
+ }
+
+ /**
+ * Creates a new packet instance for the given id.
+ *
+ * @param packetId id
+ * @return new packet instance
+ * @throws IllegalArgumentException if id is not registered or factory returns null
+ */
+ public Packet create(int packetId) {
+ Supplier extends Packet> supplier = factories.get(packetId);
+ if (supplier == null) {
+ throw new IllegalArgumentException("Unknown packetId: " + packetId);
+ }
+ Packet p = supplier.get();
+ if (p == null) {
+ throw new IllegalArgumentException("Packet factory returned null for packetId: " + packetId);
+ }
+ if (p.packetId() != packetId) {
+ throw new IllegalArgumentException("Packet factory produced mismatching id: expected="
+ + packetId + ", got=" + p.packetId());
+ }
+ return p;
+ }
+}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ClientIDPacket.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ClientIDPacket.java
deleted file mode 100644
index 07eb3cc..0000000
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ClientIDPacket.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
-package dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl;
-
-import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
-import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.ClientID;
-
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.util.UUID;
-
-/**
- * Transfers the server-assigned {@link ClientID} to the client.
- */
-public final class ClientIDPacket extends Packet {
-
- private ClientID clientId;
-
- public ClientIDPacket() {
- super(0);
- }
-
- public ClientIDPacket(ClientID clientId) {
- this();
- this.clientId = clientId;
- }
-
- public ClientID getClientId() {
- return clientId;
- }
-
- @Override
- public void write(PacketHandler packetHandler, ObjectOutputStream outputStream) throws IOException {
- if (clientId == null) throw new IOException("ClientIDPacket.clientId is null");
- UUID uuid = clientId.uuid();
- outputStream.writeLong(uuid.getMostSignificantBits());
- outputStream.writeLong(uuid.getLeastSignificantBits());
- }
-
- @Override
- public void read(PacketHandler packetHandler, ObjectInputStream inputStream) throws IOException {
- long msb = inputStream.readLong();
- long lsb = inputStream.readLong();
- this.clientId = new ClientID(new UUID(msb, lsb));
-
- if (packetHandler.getClientInstance() != null) {
- packetHandler.getClientInstance().setClientId(this.clientId);
- }
- }
-}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ConnectionIdPacket.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ConnectionIdPacket.java
new file mode 100644
index 0000000..af0ad2d
--- /dev/null
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ConnectionIdPacket.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
+ *
+ * You are unauthorized to remove this copyright.
+ * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
+ * See LICENSE-File if exists
+ */
+
+package dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl;
+
+import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * Server -> Client packet that assigns a server-side connection id.
+ *
+ * This id is used to bind the DTLS/UDP endpoint to the already authenticated TCP/TLS connection
+ * via {@link UdpBindPacket}.
+ */
+public final class ConnectionIdPacket extends Packet {
+
+ private UUID connectionId;
+
+ /**
+ * Creates an empty packet instance for decoding.
+ */
+ public ConnectionIdPacket() {
+ super(1);
+ }
+
+ /**
+ * Creates a packet instance with a connection id for sending.
+ *
+ * @param connectionId connection id (must not be null)
+ */
+ public ConnectionIdPacket(UUID connectionId) {
+ this();
+ this.connectionId = Objects.requireNonNull(connectionId, "connectionId");
+ }
+
+ /**
+ * Returns the assigned connection id.
+ *
+ * @return connection id
+ */
+ public UUID connectionId() {
+ return connectionId;
+ }
+
+ @Override
+ public void write(ObjectOutputStream out) throws IOException {
+ if (connectionId == null) {
+ throw new IOException("ConnectionIdPacket.connectionId is null");
+ }
+ out.writeLong(connectionId.getMostSignificantBits());
+ out.writeLong(connectionId.getLeastSignificantBits());
+ }
+
+ @Override
+ public void read(ObjectInputStream in) throws IOException {
+ long msb = in.readLong();
+ long lsb = in.readLong();
+ this.connectionId = new UUID(msb, lsb);
+ }
+}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ProtocolRequirementsPacket.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ProtocolRequirementsPacket.java
new file mode 100644
index 0000000..7d6a0d9
--- /dev/null
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/ProtocolRequirementsPacket.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
+ *
+ * You are unauthorized to remove this copyright.
+ * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
+ * See LICENSE-File if exists
+ */
+
+package dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl;
+
+import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.EnumSet;
+import java.util.Objects;
+
+/**
+ * Server -> client packet containing the server-required protocol set.
+ *
+ * This packet allows the client to evaluate "fully connected" based on the server policy.
+ */
+public final class ProtocolRequirementsPacket extends Packet {
+
+ private EnumSet requiredProtocols = EnumSet.noneOf(NetworkProtocol.class);
+
+ /**
+ * Constructs an empty packet for deserialization.
+ */
+ public ProtocolRequirementsPacket() {
+ super(2);
+ }
+
+ /**
+ * Constructs the packet.
+ *
+ * @param requiredProtocols required protocol set
+ */
+ public ProtocolRequirementsPacket(EnumSet requiredProtocols) {
+ super(2);
+ this.requiredProtocols = EnumSet.copyOf(Objects.requireNonNull(requiredProtocols, "requiredProtocols"));
+ }
+
+ /**
+ * Returns the required protocol set.
+ *
+ * @return required protocols
+ */
+ public EnumSet requiredProtocols() {
+ return EnumSet.copyOf(requiredProtocols);
+ }
+
+ @Override
+ public void write(ObjectOutputStream out) throws IOException {
+ Objects.requireNonNull(out, "out");
+ out.writeInt(toMask(requiredProtocols));
+ }
+
+ @Override
+ public void read(ObjectInputStream in) throws IOException {
+ Objects.requireNonNull(in, "in");
+ int mask = in.readInt();
+ this.requiredProtocols = fromMask(mask);
+ }
+
+ private static int toMask(EnumSet set) {
+ int mask = 0;
+ if (set.contains(NetworkProtocol.TCP)) mask |= 1;
+ if (set.contains(NetworkProtocol.UDP)) mask |= 2;
+ return mask;
+ }
+
+ private static EnumSet fromMask(int mask) {
+ EnumSet set = EnumSet.noneOf(NetworkProtocol.class);
+ if ((mask & 1) != 0) set.add(NetworkProtocol.TCP);
+ if ((mask & 2) != 0) set.add(NetworkProtocol.UDP);
+ return set;
+ }
+}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpBindAckPacket.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpBindAckPacket.java
new file mode 100644
index 0000000..610728b
--- /dev/null
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpBindAckPacket.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
+ *
+ * You are unauthorized to remove this copyright.
+ * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
+ * See LICENSE-File if exists
+ */
+
+package dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl;
+
+import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * Server -> client acknowledgement for {@link UdpBindPacket}.
+ *
+ * The client must only consider UDP "bound/ready" after receiving this ACK with the matching connection id.
+ *
+ * IMPORTANT: Ensure {@link #PACKET_ID} does not conflict with your other packet ids and
+ * register it in {@code PacketRegistry}.
+ */
+public final class UdpBindAckPacket extends Packet {
+
+ private UUID connectionId;
+
+ /**
+ * Constructs an empty packet for deserialization.
+ */
+ public UdpBindAckPacket() {
+ super(3);
+ }
+
+ /**
+ * Constructs the ACK packet.
+ *
+ * @param connectionId connection id being acknowledged
+ */
+ public UdpBindAckPacket(UUID connectionId) {
+ super(3);
+ this.connectionId = Objects.requireNonNull(connectionId, "connectionId");
+ }
+
+ /**
+ * Returns the acknowledged connection id.
+ *
+ * @return connection id
+ */
+ public UUID connectionId() {
+ return connectionId;
+ }
+
+ @Override
+ public void write(ObjectOutputStream out) throws IOException {
+ Objects.requireNonNull(out, "out");
+ out.writeLong(connectionId.getMostSignificantBits());
+ out.writeLong(connectionId.getLeastSignificantBits());
+ }
+
+ @Override
+ public void read(ObjectInputStream in) throws IOException {
+ Objects.requireNonNull(in, "in");
+ long msb = in.readLong();
+ long lsb = in.readLong();
+ this.connectionId = new UUID(msb, lsb);
+ }
+}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpBindPacket.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpBindPacket.java
index 9bb5113..6bea518 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpBindPacket.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpBindPacket.java
@@ -9,54 +9,67 @@
package dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl;
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
-import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.ClientID;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
+import java.util.Objects;
import java.util.UUID;
/**
- * Binds a DTLS/UDP endpoint to the already assigned {@link ClientID}.
+ * Client -> Server packet sent over UDP/DTLS to bind the DTLS remote address to an existing TCP/TLS connection.
*
- * Flow: TCP connect -> receive ClientID -> DTLS handshake -> send UdpBindPacket(clientId)
+ * Flow:
+ *
+ * - Client connects via TCP/TLS
+ * - Server sends {@link ConnectionIdPacket} over TCP
+ * - Client completes DTLS handshake and sends {@link UdpBindPacket} over UDP/DTLS with the same connection id
+ * - Server attaches UDP remote to the matching {@code SecureConnection}
+ *
*/
public final class UdpBindPacket extends Packet {
- public static final int PACKET_ID = 1;
-
- private ClientID clientId;
+ private UUID connectionId;
+ /**
+ * Creates an empty packet instance for decoding.
+ */
public UdpBindPacket() {
- super(PACKET_ID);
+ super(4);
}
- public UdpBindPacket(ClientID clientId) {
+ /**
+ * Creates a bind packet for sending.
+ *
+ * @param connectionId server-assigned connection id (must not be null)
+ */
+ public UdpBindPacket(UUID connectionId) {
this();
- this.clientId = clientId;
+ this.connectionId = Objects.requireNonNull(connectionId, "connectionId");
}
- public ClientID getClientId() {
- return clientId;
+ /**
+ * Returns the connection id this UDP endpoint wants to bind to.
+ *
+ * @return connection id
+ */
+ public UUID connectionId() {
+ return connectionId;
}
@Override
- public void write(PacketHandler packetHandler, ObjectOutputStream outputStream) throws IOException {
- if (clientId == null) throw new IOException("UdpBindPacket.clientId is null");
- UUID uuid = clientId.uuid();
- outputStream.writeLong(uuid.getMostSignificantBits());
- outputStream.writeLong(uuid.getLeastSignificantBits());
- }
-
- @Override
- public void read(PacketHandler packetHandler, ObjectInputStream inputStream) throws IOException {
- long msb = inputStream.readLong();
- long lsb = inputStream.readLong();
- this.clientId = new ClientID(new UUID(msb, lsb));
- // Server-side binding is handled by UDP receiver, because it knows the remote address.
- if (packetHandler.getClientInstance() != null) {
- packetHandler.getClientInstance().setClientId(this.clientId);
+ public void write(ObjectOutputStream out) throws IOException {
+ if (connectionId == null) {
+ throw new IOException("UdpBindPacket.connectionId is null");
}
+ out.writeLong(connectionId.getMostSignificantBits());
+ out.writeLong(connectionId.getLeastSignificantBits());
+ }
+
+ @Override
+ public void read(ObjectInputStream in) throws IOException {
+ long msb = in.readLong();
+ long lsb = in.readLong();
+ this.connectionId = new UUID(msb, lsb);
}
}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpHelloPacket.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpHelloPacket.java
new file mode 100644
index 0000000..659c662
--- /dev/null
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/packets/impl/UdpHelloPacket.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
+ *
+ * You are unauthorized to remove this copyright.
+ * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
+ * See LICENSE-File if exists
+ */
+
+package dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl;
+
+import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.Objects;
+
+/**
+ * Client -> server packet used for UDP-only bootstrap.
+ *
+ * The client sends this packet over UDP/DTLS to request the server to create/assign a connection id
+ * and return bootstrap information (e.g. ConnectionIdPacket + ProtocolRequirementsPacket) over UDP.
+ */
+public final class UdpHelloPacket extends Packet {
+
+ /**
+ * Constructs an empty packet for serialization/deserialization.
+ */
+ public UdpHelloPacket() {
+ super(5);
+ }
+
+ @Override
+ public void write(ObjectOutputStream out) throws IOException {
+ Objects.requireNonNull(out, "out");
+ // No payload.
+ }
+
+ @Override
+ public void read(ObjectInputStream in) throws IOException {
+ Objects.requireNonNull(in, "in");
+ // No payload.
+ }
+}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ClientConnection.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ClientConnection.java
new file mode 100644
index 0000000..b692fb7
--- /dev/null
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ClientConnection.java
@@ -0,0 +1,363 @@
+/*
+ * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
+ *
+ * You are unauthorized to remove this copyright.
+ * You have to give Credits to the Author in your project and link this GitHub site: https://github.com/UnlegitDqrk
+ * See LICENSE-File if exists
+ */
+
+/*
+ * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
+ *
+ * You are unauthorized to remove this copyright.
+ * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
+ * See LICENSE-File if exists
+ */
+
+package dev.unlegitdqrk.unlegitlibrary.network.system.server;
+
+import dev.unlegitdqrk.unlegitlibrary.event.EventManager;
+import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketCodec;
+import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
+import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.send.S_PacketSendEvent;
+import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.send.S_PacketSendFailedEvent;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.DtlsEndpoint;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
+
+import javax.net.ssl.SSLSocket;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.net.SocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.DatagramChannel;
+import java.util.EnumSet;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Represents one connected client on the server side, with optional TCP/TLS and/or UDP/DTLS channels.
+ *
+ * Connection usability is defined by {@link ServerProtocolMode#required()}.
+ */
+public final class ClientConnection {
+
+ private final UUID connectionId;
+ private final PacketCodec codec;
+ private final ServerProtocolMode protocolMode;
+ private final EventManager eventManager;
+
+ // TCP (TLS) - optional
+ private final SSLSocket tcpSocket;
+ private final ObjectOutputStream tcpOut;
+ private final ObjectInputStream tcpIn;
+
+ // UDP (DTLS) - optional
+ private volatile SocketAddress udpRemote;
+ private volatile DtlsEndpoint.DtlsSession udpSession;
+ private volatile DtlsEndpoint dtlsEndpoint;
+ private volatile DatagramChannel udpChannel;
+ private volatile boolean udpBound;
+
+ private final AtomicBoolean closed = new AtomicBoolean(false);
+
+ /**
+ * Creates a new connection with a predefined connection id.
+ *
+ * @param connectionId server-side unique connection id
+ * @param codec packet codec
+ * @param protocolMode server protocol mode (supported/required)
+ * @param eventManager event manager used to dispatch server events
+ * @param tcpSocket TLS socket (nullable for UDP-only)
+ * @param tcpOut TCP output stream (nullable for UDP-only)
+ * @param tcpIn TCP input stream (nullable for UDP-only)
+ */
+ public ClientConnection(
+ UUID connectionId,
+ PacketCodec codec,
+ ServerProtocolMode protocolMode,
+ EventManager eventManager,
+ SSLSocket tcpSocket,
+ ObjectOutputStream tcpOut,
+ ObjectInputStream tcpIn
+ ) {
+ this.connectionId = Objects.requireNonNull(connectionId, "connectionId");
+ this.codec = Objects.requireNonNull(codec, "codec");
+ this.protocolMode = Objects.requireNonNull(protocolMode, "protocolMode");
+ this.eventManager = Objects.requireNonNull(eventManager, "eventManager");
+ this.tcpSocket = tcpSocket;
+ this.tcpOut = tcpOut;
+ this.tcpIn = tcpIn;
+ }
+
+ /**
+ * Creates a new TCP-accepted connection (TLS).
+ *
+ * @param codec packet codec
+ * @param protocolMode server protocol mode (supported/required)
+ * @param eventManager event manager used to dispatch server events
+ * @param tcpSocket TLS socket
+ * @param tcpOut TCP output stream
+ * @param tcpIn TCP input stream
+ * @return connection
+ */
+ public static ClientConnection tcpAccepted(
+ PacketCodec codec,
+ ServerProtocolMode protocolMode,
+ EventManager eventManager,
+ SSLSocket tcpSocket,
+ ObjectOutputStream tcpOut,
+ ObjectInputStream tcpIn
+ ) {
+ return new ClientConnection(
+ UUID.randomUUID(),
+ codec,
+ protocolMode,
+ eventManager,
+ Objects.requireNonNull(tcpSocket, "tcpSocket"),
+ Objects.requireNonNull(tcpOut, "tcpOut"),
+ Objects.requireNonNull(tcpIn, "tcpIn")
+ );
+ }
+
+ /**
+ * Creates a new UDP-only connection.
+ *
+ * @param codec packet codec
+ * @param protocolMode server protocol mode (supported/required)
+ * @param eventManager event manager used to dispatch server events
+ * @param connectionId pre-generated connection id
+ * @return connection
+ */
+ public static ClientConnection udpOnly(
+ PacketCodec codec,
+ ServerProtocolMode protocolMode,
+ EventManager eventManager,
+ UUID connectionId
+ ) {
+ return new ClientConnection(
+ Objects.requireNonNull(connectionId, "connectionId"),
+ codec,
+ protocolMode,
+ eventManager,
+ null,
+ null,
+ null
+ );
+ }
+
+ /**
+ * Returns a server-side unique connection id.
+ *
+ * @return connection id
+ */
+ public UUID connectionId() {
+ return connectionId;
+ }
+
+ /**
+ * Returns required protocols snapshot (server policy snapshot).
+ *
+ * @return required protocols
+ */
+ public EnumSet requiredProtocols() {
+ return protocolMode.required();
+ }
+
+ /**
+ * Returns true if the TCP/TLS channel is currently connected.
+ *
+ * @return true if TCP connected
+ */
+ public boolean isTcpConnected() {
+ if (closed.get()) return false;
+ SSLSocket s = tcpSocket;
+ return s != null && !s.isClosed();
+ }
+
+ /**
+ * Returns true if the UDP/DTLS channel is currently connected and bound.
+ *
+ * @return true if UDP connected
+ */
+ public boolean isUdpConnected() {
+ if (closed.get()) return false;
+ if (!udpBound) return false;
+ if (udpRemote == null || dtlsEndpoint == null || udpSession == null) return false;
+ return udpSession.isHandshakeComplete();
+ }
+
+ /**
+ * Returns true if this connection satisfies the server required protocol set.
+ *
+ * @return true if fully connected according to {@link ServerProtocolMode#required()}
+ */
+ public boolean isFullyConnected() {
+ EnumSet required = protocolMode.required();
+ boolean tcpOk = !required.contains(NetworkProtocol.TCP) || isTcpConnected();
+ boolean udpOk = !required.contains(NetworkProtocol.UDP) || isUdpConnected();
+ return tcpOk && udpOk;
+ }
+
+ /**
+ * Returns the currently bound UDP remote, or null.
+ *
+ * @return udp remote
+ */
+ public SocketAddress udpRemote() {
+ return udpRemote;
+ }
+
+ /**
+ * Sets whether UDP is bound/acknowledged.
+ *
+ * @param udpBound bound flag
+ */
+ public void setUdpBound(boolean udpBound) {
+ this.udpBound = udpBound;
+ }
+
+ /**
+ * Attaches the DTLS side (after bind/hello flow).
+ *
+ * @param udpRemote remote address
+ * @param udpSession DTLS session
+ * @param dtlsEndpoint DTLS endpoint
+ * @param udpChannel UDP channel (server channel)
+ */
+ public void attachUdp(
+ SocketAddress udpRemote,
+ DtlsEndpoint.DtlsSession udpSession,
+ DtlsEndpoint dtlsEndpoint,
+ DatagramChannel udpChannel
+ ) {
+ this.udpRemote = Objects.requireNonNull(udpRemote, "udpRemote");
+ this.udpSession = Objects.requireNonNull(udpSession, "udpSession");
+ this.dtlsEndpoint = Objects.requireNonNull(dtlsEndpoint, "dtlsEndpoint");
+ this.udpChannel = Objects.requireNonNull(udpChannel, "udpChannel");
+ }
+
+ /**
+ * Sends a packet via the selected protocol (TCP/TLS or UDP/DTLS).
+ *
+ * Emits:
+ *
+ * - {@link S_PacketSendEvent} on success
+ * - {@link S_PacketSendFailedEvent} on failure
+ *
+ *
+ * @param packet packet to send
+ * @param protocol protocol to use
+ * @return true if sent, false if not possible/unsupported/not ready/closed
+ * @throws IOException on I/O errors
+ */
+ public boolean sendPacket(Packet packet, NetworkProtocol protocol) throws IOException {
+ Objects.requireNonNull(packet, "packet");
+ Objects.requireNonNull(protocol, "protocol");
+
+ if (closed.get()) return false;
+
+ try {
+ boolean ok = switch (protocol) {
+ case TCP -> sendTcp(packet);
+ case UDP -> sendUdp(packet);
+ };
+
+ if (ok) {
+ eventManager.executeEvent(new S_PacketSendEvent(packet, this, protocol));
+ } else {
+ eventManager.executeEvent(new S_PacketSendFailedEvent(
+ packet, this, protocol, new IllegalStateException("Packet not sent")
+ ));
+ }
+
+ return ok;
+ } catch (IOException | RuntimeException e) {
+ eventManager.executeEvent(new S_PacketSendFailedEvent(packet, this, protocol, e));
+ throw e;
+ }
+ }
+
+ /**
+ * Receives the next packet from the TCP stream.
+ *
+ * @return packet
+ * @throws IOException on I/O errors
+ * @throws ClassNotFoundException on deserialization errors
+ */
+ public Packet receiveTcp() throws IOException, ClassNotFoundException {
+ if (closed.get()) throw new IOException("Connection closed");
+ if (tcpIn == null) throw new IOException("TCP not available for this connection");
+ return codec.receiveFromStream(tcpIn);
+ }
+
+ /**
+ * Returns the TCP output stream (nullable).
+ *
+ * @return tcp output stream or null
+ */
+ public ObjectOutputStream tcpOut() {
+ return tcpOut;
+ }
+
+ /**
+ * Returns the TCP input stream (nullable).
+ *
+ * @return tcp input stream or null
+ */
+ public ObjectInputStream tcpIn() {
+ return tcpIn;
+ }
+
+ /**
+ * Closes this connection (TCP socket if present). UDP association is cleared.
+ *
+ * @throws IOException on close errors
+ */
+ public void close() throws IOException {
+ if (!closed.compareAndSet(false, true)) return;
+
+ try {
+ if (tcpOut != null) tcpOut.close();
+ } catch (Exception ignored) {
+ }
+ try {
+ if (tcpIn != null) tcpIn.close();
+ } catch (Exception ignored) {
+ }
+ try {
+ if (tcpSocket != null) tcpSocket.close();
+ } catch (Exception ignored) {
+ }
+
+ udpRemote = null;
+ udpSession = null;
+ dtlsEndpoint = null;
+ udpChannel = null;
+ udpBound = false;
+ }
+
+ private boolean sendTcp(Packet packet) throws IOException {
+ if (!isTcpConnected()) return false;
+ if (tcpOut == null) return false;
+ codec.sendToStream(packet, tcpOut);
+ return true;
+ }
+
+ private boolean sendUdp(Packet packet) throws IOException {
+ if (closed.get()) return false;
+
+ SocketAddress remote = udpRemote;
+ DtlsEndpoint endpoint = dtlsEndpoint;
+ if (remote == null || endpoint == null) return false;
+
+ // Allow sending bind/hello/ack even if udpBound==false (handshake must still be complete).
+ DtlsEndpoint.DtlsSession s = udpSession;
+ if (s == null || !s.isHandshakeComplete()) return false;
+
+ ByteBuffer buf = codec.encodeToBuffer(packet);
+ endpoint.sendApplication(remote, buf);
+ return true;
+ }
+}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ConnectionHandler.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ConnectionHandler.java
deleted file mode 100644
index 0e4f296..0000000
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ConnectionHandler.java
+++ /dev/null
@@ -1,275 +0,0 @@
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
-package dev.unlegitdqrk.unlegitlibrary.network.system.server;
-
-import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
-import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.ClientIDPacket;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.receive.S_PacketReceivedEvent;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.receive.S_PacketReceivedFailedEvent;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.receive.S_UnknownObjectReceivedEvent;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.send.S_PacketSendEvent;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.send.S_PacketSendFailedEvent;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.connect.ConnectionHandlerConnectedEvent;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.disconnect.ConnectionHandlerDisconnectedEvent;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.disconnect.ConnectionHandlerFullyDisconnectedEvent;
-import dev.unlegitdqrk.unlegitlibrary.network.system.udp.DtlsEndpoint;
-import dev.unlegitdqrk.unlegitlibrary.network.system.udp.UdpPacketCodec;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.ClientID;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
-
-import javax.net.ssl.SSLSocket;
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.net.SocketAddress;
-import java.net.SocketException;
-import java.nio.ByteBuffer;
-import java.util.Objects;
-
-/**
- * Handles one connected client socket and dispatches received packets.
- *
- * Supports TCP (TLS) and UDP (DTLS). UDP is bound after TCP identity assignment.
- */
-public final class ConnectionHandler {
-
- private final NetworkServer server;
- private final ClientID clientId;
- private final Thread receiveThread;
- private SSLSocket socket;
- private ObjectOutputStream outputStream;
- private ObjectInputStream inputStream;
- private volatile SocketAddress udpRemoteAddress;
- private volatile DtlsEndpoint.DtlsSession udpSession;
- private volatile DtlsEndpoint dtlsEndpoint;
-
- /**
- * Creates a new connection handler for an already accepted SSL socket.
- *
- * @param server owning server
- * @param socket ssl socket
- * @param clientId server-assigned client identity
- * @throws IOException if stream creation fails
- * @throws ClassNotFoundException if packet serialization fails
- */
- public ConnectionHandler(NetworkServer server, SSLSocket socket, ClientID clientId) throws IOException, ClassNotFoundException {
- this.server = Objects.requireNonNull(server, "server");
- this.socket = Objects.requireNonNull(socket, "socket");
- this.clientId = Objects.requireNonNull(clientId, "clientId");
-
- this.outputStream = new ObjectOutputStream(socket.getOutputStream());
- this.inputStream = new ObjectInputStream(socket.getInputStream());
-
- this.receiveThread = new Thread(this::receive, "ConnectionHandler-TCP-Receive-" + clientId.uuid());
- this.receiveThread.start();
-
- // Send identity via TCP first.
- sendPacket(new ClientIDPacket(this.clientId), Transport.TCP);
-
- // Transport-specific connect event (TCP)
- server.getEventManager().executeEvent(new ConnectionHandlerConnectedEvent(this, Transport.TCP));
- }
-
- /**
- * Attaches a DTLS/UDP remote endpoint to this already TCP-authenticated connection.
- *
- * @param remote remote UDP address
- * @param session DTLS session for the remote
- * @param endpoint DTLS endpoint
- */
- public void attachUdp(SocketAddress remote, DtlsEndpoint.DtlsSession session, DtlsEndpoint endpoint) {
- this.udpRemoteAddress = Objects.requireNonNull(remote, "remote");
- this.udpSession = Objects.requireNonNull(session, "session");
- this.dtlsEndpoint = Objects.requireNonNull(endpoint, "endpoint");
-
- // Transport-specific connect event (UDP)
- server.getEventManager().executeEvent(new ConnectionHandlerConnectedEvent(this, Transport.UDP));
- }
-
- public SocketAddress getUdpRemoteAddress() {
- return udpRemoteAddress;
- }
-
- public boolean isUdpAttached() {
- return udpRemoteAddress != null && udpSession != null && udpSession.isHandshakeComplete();
- }
-
- public boolean isFullyConnected() {
- boolean tcpOk = isConnected();
- boolean udpOk = isUdpAttached();
-
- if (server.getTransportPolicy().required().contains(Transport.TCP) && !tcpOk) return false;
- return !server.getTransportPolicy().required().contains(Transport.UDP) || udpOk;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) return true;
- if (!(obj instanceof ConnectionHandler other)) return false;
- return clientId.equals(other.clientId);
- }
-
- @Override
- public int hashCode() {
- return clientId.hashCode();
- }
-
- public SSLSocket getSocket() {
- return socket;
- }
-
- public ObjectOutputStream getOutputStream() {
- return outputStream;
- }
-
- public ObjectInputStream getInputStream() {
- return inputStream;
- }
-
- public NetworkServer getServer() {
- return server;
- }
-
- public ClientID getClientId() {
- return clientId;
- }
-
- public boolean isConnected() {
- return socket != null && socket.isConnected() && !socket.isClosed()
- && receiveThread.isAlive() && !receiveThread.isInterrupted();
- }
-
- public synchronized boolean disconnect() {
- boolean wasTcpConnected = isConnected();
- boolean wasUdpConnected = isUdpAttached();
- boolean wasFullyConnected = isFullyConnected();
-
- if (!wasTcpConnected && !wasUdpConnected) {
- // still cleanup
- cleanup();
- return false;
- }
-
- try {
- receiveThread.interrupt();
-
- if (outputStream != null) outputStream.close();
- if (inputStream != null) inputStream.close();
- if (socket != null) socket.close();
- } catch (IOException ignored) {
- }
-
- // Emit transport-specific disconnect events
- if (wasUdpConnected) {
- server.getEventManager().executeEvent(new ConnectionHandlerDisconnectedEvent(this, Transport.UDP));
- }
- if (wasTcpConnected) {
- server.getEventManager().executeEvent(new ConnectionHandlerDisconnectedEvent(this, Transport.TCP));
- }
-
- cleanup();
-
- if (wasFullyConnected) {
- server.getEventManager().executeEvent(new ConnectionHandlerFullyDisconnectedEvent(
- this,
- server.getTransportPolicy().required().toArray(new Transport[0])
- ));
- }
-
- return true;
- }
-
- private void cleanup() {
- socket = null;
- outputStream = null;
- inputStream = null;
-
- udpRemoteAddress = null;
- udpSession = null;
- dtlsEndpoint = null;
-
- server.getConnectionHandlers().remove(this);
- }
-
- /**
- * Sends a packet via the selected transport.
- *
- * @param packet packet to send
- * @param transport target transport
- * @return true if sent, false if not possible
- * @throws IOException on I/O errors
- * @throws ClassNotFoundException on serialization errors
- */
- public boolean sendPacket(Packet packet, Transport transport) throws IOException, ClassNotFoundException {
- Objects.requireNonNull(packet, "packet");
- Objects.requireNonNull(transport, "transport");
-
- if (!server.getTransportPolicy().supported().contains(transport)) {
- return false;
- }
-
- return switch (transport) {
- case TCP -> sendTcp(packet);
- case UDP -> sendUdp(packet);
- };
- }
-
- private boolean sendTcp(Packet packet) throws IOException, ClassNotFoundException {
- if (!isConnected()) return false;
-
- boolean sent = server.getPacketHandler().sendPacket(packet, outputStream);
- if (sent) server.getEventManager().executeEvent(new S_PacketSendEvent(packet, this, Transport.TCP));
- else server.getEventManager().executeEvent(new S_PacketSendFailedEvent(packet, this, Transport.TCP));
- return sent;
- }
-
- private boolean sendUdp(Packet packet) throws IOException, ClassNotFoundException {
- if (!isUdpAttached()) return false;
-
- DtlsEndpoint endpoint = dtlsEndpoint;
- if (endpoint == null) return false;
-
- ByteBuffer encoded = UdpPacketCodec.encode(server.getPacketHandler(), packet);
- endpoint.sendApplication(udpRemoteAddress, encoded);
-
- server.getEventManager().executeEvent(new S_PacketSendEvent(packet, this, Transport.UDP));
- return true;
- }
-
- private void receive() {
- while (isConnected()) {
- try {
- Object received = inputStream.readObject();
-
- if (received instanceof Integer id) {
- if (server.getPacketHandler().isPacketIDRegistered(id)) {
- Packet packet = server.getPacketHandler().getPacketByID(id);
-
- boolean ok = server.getPacketHandler().handlePacket(id, packet, inputStream);
- if (ok)
- server.getEventManager().executeEvent(new S_PacketReceivedEvent(this, packet, Transport.TCP));
- else
- server.getEventManager().executeEvent(new S_PacketReceivedFailedEvent(this, packet, Transport.TCP));
- } else {
- server.getEventManager().executeEvent(new S_UnknownObjectReceivedEvent(received, this, Transport.TCP));
- }
- } else {
- server.getEventManager().executeEvent(new S_UnknownObjectReceivedEvent(received, this, Transport.TCP));
- }
- } catch (SocketException se) {
- disconnect();
- } catch (Exception ex) {
- if (server.getLogger() != null) {
- server.getLogger().exception("Receive thread exception for client " + clientId, ex);
- }
- disconnect();
- }
- }
- }
-}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/NetworkServer.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/NetworkServer.java
index 1b323be..a649da6 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/NetworkServer.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/NetworkServer.java
@@ -9,331 +9,329 @@
package dev.unlegitdqrk.unlegitlibrary.network.system.server;
import dev.unlegitdqrk.unlegitlibrary.event.EventManager;
+import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketCodec;
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
-import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
-import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.ClientIDPacket;
+import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.ConnectionIdPacket;
+import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.ProtocolRequirementsPacket;
+import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.UdpBindAckPacket;
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.UdpBindPacket;
+import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.UdpHelloPacket;
import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.receive.S_PacketReceivedEvent;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.receive.S_UnknownObjectReceivedEvent;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.connect.ConnectionHandlerFullyConnectedEvent;
+import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.receive.S_PacketReceivedFailedEvent;
+import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.connect.ClientConnectionConnectedEvent;
+import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.connect.ClientConnectionFullyConnectedEvent;
+import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.disconnect.ClientConnectionDisconnectedEvent;
+import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.disconnect.ClientConnectionFullyDisconnectedEvent;
import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.incoming.TCPIncomingConnectionEvent;
import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.incoming.UDPIncomingConnectionEvent;
-import dev.unlegitdqrk.unlegitlibrary.network.system.udp.DtlsEndpoint;
-import dev.unlegitdqrk.unlegitlibrary.network.system.udp.UdpPacketCodec;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.ClientID;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.TransportPolicy;
-import dev.unlegitdqrk.unlegitlibrary.network.utils.PemUtils;
-import dev.unlegitdqrk.unlegitlibrary.utils.DefaultMethodsOverrider;
-import dev.unlegitdqrk.unlegitlibrary.utils.Logger;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.ClientAuthMode;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.DtlsEndpoint;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
-import javax.net.ssl.*;
-import java.io.File;
+import javax.net.ServerSocketFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLServerSocketFactory;
+import javax.net.ssl.SSLSocket;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
import java.net.InetSocketAddress;
-import java.net.Socket;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
-import java.security.KeyStore;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
- * Hybrid server supporting TCP (TLS) and UDP (DTLS) transports.
+ * Secure server supporting TCP/TLS and UDP/DTLS.
*
- * Supports policy combinations:
- *
- * - TCP only
- * - UDP only
- * - both supported
- * - both required simultaneously
- *
+ * Listeners are started based on {@link ServerProtocolMode#required()}.
+ * Fully-connected is evaluated based on {@link ServerProtocolMode#required()}.
*/
public final class NetworkServer {
+ private final ServerProtocolMode protocolMode;
+ private final ClientAuthMode clientAuthMode;
+
+ private final PacketCodec codec;
+ private final EventManager eventManager;
+
private final int tcpPort;
private final int udpPort;
- private final PacketHandler packetHandler;
- private final EventManager eventManager;
- private final Logger logger;
+ private final int timeoutMillis;
+ private final int mtu;
- private final int timeout;
private final SSLServerSocketFactory tlsServerSocketFactory;
+ private final SSLParameters tlsParameters; // optional
private final SSLContext dtlsContext;
- private final TransportPolicy transportPolicy;
- private final boolean requireClientCert;
- private final List connectionHandlers = Collections.synchronizedList(new ArrayList<>());
- private final Map handlerByClientId = new ConcurrentHashMap<>();
- private SSLServerSocket tcpServerSocket;
- private DatagramChannel udpChannel;
- private DtlsEndpoint dtlsEndpoint;
+ private volatile SSLServerSocket tcpServerSocket;
+ private volatile DatagramChannel udpChannel;
+ private volatile DtlsEndpoint dtlsEndpoint;
+
private volatile Thread tcpAcceptThread;
private volatile Thread udpThread;
+ private final Map connectionsById = new ConcurrentHashMap<>();
+ private final Map connectionsByUdpRemote = new ConcurrentHashMap<>();
+ private final List connections = java.util.Collections.synchronizedList(new ArrayList<>());
+
private NetworkServer(
+ ServerProtocolMode protocolMode,
+ ClientAuthMode clientAuthMode,
+ PacketCodec codec,
+ EventManager eventManager,
int tcpPort,
int udpPort,
- PacketHandler packetHandler,
- EventManager eventManager,
- Logger logger,
- int timeout,
- SSLServerSocketFactory tlsFactory,
- SSLContext dtlsContext,
- TransportPolicy transportPolicy,
- boolean requireClientCert
+ int timeoutMillis,
+ int mtu,
+ SSLServerSocketFactory tlsServerSocketFactory,
+ SSLParameters tlsParameters,
+ SSLContext dtlsContext
) {
+ this.protocolMode = Objects.requireNonNull(protocolMode, "protocolMode");
+ this.clientAuthMode = Objects.requireNonNull(clientAuthMode, "clientAuthMode");
+ this.codec = Objects.requireNonNull(codec, "codec");
+ this.eventManager = Objects.requireNonNull(eventManager, "eventManager");
this.tcpPort = tcpPort;
this.udpPort = udpPort;
-
- this.packetHandler = Objects.requireNonNull(packetHandler, "packetHandler");
- this.eventManager = Objects.requireNonNull(eventManager, "eventManager");
- this.logger = logger;
-
- this.timeout = timeout;
- this.tlsServerSocketFactory = tlsFactory;
- this.dtlsContext = dtlsContext;
-
- this.transportPolicy = Objects.requireNonNull(transportPolicy, "transportPolicy");
- this.requireClientCert = requireClientCert;
-
- this.packetHandler.setServerInstance(this);
- this.packetHandler.registerPacket(new ClientIDPacket());
- this.packetHandler.registerPacket(new UdpBindPacket());
+ this.timeoutMillis = timeoutMillis;
+ this.mtu = mtu;
+ this.tlsServerSocketFactory = Objects.requireNonNull(tlsServerSocketFactory, "tlsServerSocketFactory");
+ this.tlsParameters = tlsParameters;
+ this.dtlsContext = Objects.requireNonNull(dtlsContext, "dtlsContext");
}
/**
- * Returns the configured transport policy.
+ * Starts the server.
*
- * @return policy
+ * @return true if started, false if already started
+ * @throws Exception on startup errors
*/
- public TransportPolicy getTransportPolicy() {
- return transportPolicy;
- }
+ public synchronized boolean start() throws Exception {
+ if (tcpAcceptThread != null || udpThread != null) return false;
- /**
- * Returns the TCP port (TLS).
- *
- * @return tcp port
- */
- public int getTcpPort() {
- return tcpPort;
- }
+ // FIX: only required exists -> listener start is based on required()
+ EnumSet required = protocolMode.required();
- /**
- * Returns the UDP port (DTLS).
- *
- * @return udp port
- */
- public int getUdpPort() {
- return udpPort;
- }
-
- /**
- * Returns packet handler.
- *
- * @return packet handler
- */
- public PacketHandler getPacketHandler() {
- return packetHandler;
- }
-
- /**
- * Returns event manager.
- *
- * @return event manager
- */
- public EventManager getEventManager() {
- return eventManager;
- }
-
- /**
- * Returns logger.
- *
- * @return logger (may be null)
- */
- public Logger getLogger() {
- return logger;
- }
-
- /**
- * Returns current DTLS endpoint instance (may be null if UDP not supported or not started).
- *
- * @return dtls endpoint
- */
- public DtlsEndpoint getDtlsEndpoint() {
- return dtlsEndpoint;
- }
-
- /**
- * Returns live connection handlers.
- *
- * @return list of handlers
- */
- public List getConnectionHandlers() {
- return connectionHandlers;
- }
-
- /**
- * Looks up a handler by client id.
- *
- * @param clientId client id
- * @return handler or null
- */
- public ConnectionHandler getConnectionHandlerByClientId(ClientID clientId) {
- return handlerByClientId.get(clientId);
- }
-
- /**
- * Starts the server according to the configured transport policy.
- *
- * @return true if started successfully
- */
- public synchronized boolean start() {
- try {
- if (transportPolicy.supported().contains(Transport.TCP)) {
- if (tlsServerSocketFactory == null) {
- throw new IllegalStateException("TLS ServerSocketFactory missing (TCP supported).");
- }
- if (tcpPort <= 0) {
- throw new IllegalStateException("TCP port not set (TCP supported).");
- }
-
- tcpServerSocket = (SSLServerSocket) tlsServerSocketFactory.createServerSocket(tcpPort);
- tcpServerSocket.setNeedClientAuth(requireClientCert);
- tcpServerSocket.setSoTimeout(timeout);
- tcpServerSocket.setEnabledProtocols(new String[]{"TLSv1.3"});
-
- tcpAcceptThread = new Thread(this::acceptTcpLoop, "NetworkServer-TCP-Acceptor");
- tcpAcceptThread.start();
- }
-
- if (transportPolicy.supported().contains(Transport.UDP)) {
- if (dtlsContext == null) {
- throw new IllegalStateException("DTLS SSLContext missing (UDP supported).");
- }
- if (udpPort <= 0) {
- throw new IllegalStateException("UDP port not set (UDP supported).");
- }
-
- udpChannel = DatagramChannel.open();
- udpChannel.bind(new InetSocketAddress(udpPort));
- udpChannel.configureBlocking(false);
-
- dtlsEndpoint = new DtlsEndpoint(
- udpChannel,
- dtlsContext,
- false,
- 1400,
- timeout,
- this::onDtlsApplicationData
- );
-
- udpThread = new Thread(this::udpLoop, "NetworkServer-UDP-DTLS");
- udpThread.start();
- }
-
- logInfo("Server started. tcp=" + tcpPort + ", udp=" + udpPort
- + ", supported=" + transportPolicy.supported()
- + ", required=" + transportPolicy.required());
- return true;
- } catch (Exception e) {
- logException("Failed to start", e);
- stop();
- return false;
+ if (required.contains(NetworkProtocol.TCP)) {
+ if (tcpPort <= 0) throw new IllegalStateException("tcpPort not set (TCP required)");
+ startTcp();
}
+
+ if (required.contains(NetworkProtocol.UDP)) {
+ if (udpPort <= 0) throw new IllegalStateException("udpPort not set (UDP required)");
+ startUdp();
+ }
+
+ return true;
}
- /**
- * Stops the server and disconnects clients.
- *
- * @return true if stopped successfully
- */
+
public synchronized boolean stop() {
- for (ConnectionHandler h : new ArrayList<>(connectionHandlers)) {
+ boolean wasRunning = (tcpAcceptThread != null) || (udpThread != null);
+
+ Thread t1 = tcpAcceptThread;
+ Thread t2 = udpThread;
+ if (t1 != null) t1.interrupt();
+ if (t2 != null) t2.interrupt();
+
+ tcpAcceptThread = null;
+ udpThread = null;
+
+ try {
+ if (tcpServerSocket != null) tcpServerSocket.close();
+ } catch (Exception ignored) {
+ } finally {
+ tcpServerSocket = null;
+ }
+
+ try {
+ if (udpChannel != null) udpChannel.close();
+ } catch (Exception ignored) {
+ } finally {
+ udpChannel = null;
+ dtlsEndpoint = null;
+ }
+
+ EnumSet required = protocolMode.required();
+
+ for (ClientConnection c : new ArrayList<>(connections)) {
+ boolean wasFully = isFullyConnected(c);
+
+ boolean tcpWas = isConnected(c, NetworkProtocol.TCP);
+ boolean udpWas = isConnected(c, NetworkProtocol.UDP);
+
+ SocketAddress udpRemote = c.udpRemote();
+
try {
- h.disconnect();
+ c.close();
} catch (Exception ignored) {
}
+
+ if (udpRemote != null) {
+ connectionsByUdpRemote.remove(udpRemote);
+ }
+
+ if (tcpWas) {
+ eventManager.executeEvent(new ClientConnectionDisconnectedEvent(c, NetworkProtocol.TCP));
+ }
+ if (udpWas) {
+ eventManager.executeEvent(new ClientConnectionDisconnectedEvent(c, NetworkProtocol.UDP));
+ }
+ if (wasFully) {
+ eventManager.executeEvent(new ClientConnectionFullyDisconnectedEvent(c, required));
+ }
}
- try {
- if (tcpAcceptThread != null) tcpAcceptThread.interrupt();
- if (udpThread != null) udpThread.interrupt();
+ connections.clear();
+ connectionsById.clear();
+ connectionsByUdpRemote.clear();
- if (tcpServerSocket != null) tcpServerSocket.close();
- tcpServerSocket = null;
+ return wasRunning;
+ }
- if (udpChannel != null) udpChannel.close();
- udpChannel = null;
+ public ServerProtocolMode protocolMode() {
+ return protocolMode;
+ }
- dtlsEndpoint = null;
+ public boolean isConnected(ClientConnection connection, NetworkProtocol protocol) {
+ Objects.requireNonNull(connection, "connection");
+ Objects.requireNonNull(protocol, "protocol");
- handlerByClientId.clear();
- connectionHandlers.clear();
+ return switch (protocol) {
+ case TCP -> connection.isTcpConnected();
+ case UDP -> connection.isUdpConnected();
+ };
+ }
- logInfo("Server stopped");
- return true;
- } catch (Exception e) {
- logException("Failed to stop", e);
- return false;
+ public boolean isFullyConnected(ClientConnection connection) {
+ Objects.requireNonNull(connection, "connection");
+ return connection.isFullyConnected();
+ }
+
+ public List connections() {
+ synchronized (connections) {
+ return List.copyOf(connections);
}
}
- private void acceptTcpLoop() {
+ private void startTcp() throws Exception {
+ SSLServerSocket serverSocket = (SSLServerSocket) tlsServerSocketFactory.createServerSocket();
+ serverSocket.bind(new InetSocketAddress(tcpPort));
+ serverSocket.setSoTimeout(timeoutMillis);
+
+ if (clientAuthMode == ClientAuthMode.REQUIRED) {
+ serverSocket.setNeedClientAuth(true);
+ } else {
+ serverSocket.setWantClientAuth(true);
+ }
+
+ if (tlsParameters != null) {
+ serverSocket.setSSLParameters(tlsParameters);
+ } else {
+ serverSocket.setEnabledProtocols(new String[]{"TLSv1.3"});
+ }
+
+ this.tcpServerSocket = serverSocket;
+
+ this.tcpAcceptThread = new Thread(this::tcpAcceptLoop, "SecureNetworkServer-TCP-Accept");
+ this.tcpAcceptThread.start();
+ }
+
+ private void startUdp() throws Exception {
+ DatagramChannel ch = DatagramChannel.open();
+ ch.bind(new InetSocketAddress(udpPort));
+ ch.configureBlocking(false);
+
+ DtlsEndpoint endpoint = new DtlsEndpoint(
+ ch,
+ dtlsContext,
+ false,
+ mtu,
+ timeoutMillis,
+ clientAuthMode,
+ this::onDtlsApplicationData
+ );
+
+ this.udpChannel = ch;
+ this.dtlsEndpoint = endpoint;
+
+ this.udpThread = new Thread(this::udpLoop, "SecureNetworkServer-UDP-DTLS");
+ this.udpThread.start();
+ }
+
+ private void tcpAcceptLoop() {
try {
while (tcpServerSocket != null && !tcpServerSocket.isClosed() && !Thread.currentThread().isInterrupted()) {
- Socket socket = tcpServerSocket.accept();
- if (!(socket instanceof SSLSocket ssl)) {
- socket.close();
- continue;
- }
+ SSLSocket socket = (SSLSocket) tcpServerSocket.accept();
+ socket.setTcpNoDelay(true);
+ socket.setSoTimeout(timeoutMillis);
- ssl.setTcpNoDelay(true);
- ssl.setSoTimeout(timeout);
-
- try {
- ssl.startHandshake();
- } catch (Exception handshakeEx) {
- logException("TLS handshake failed", handshakeEx);
- try {
- ssl.close();
- } catch (Exception ignored) {
- }
- continue;
- }
-
- TCPIncomingConnectionEvent event = new TCPIncomingConnectionEvent(this, ssl);
- eventManager.executeEvent(event);
- if (event.isCancelled()) {
- try {
- ssl.close();
- } catch (Exception ignored) {
- }
+ TCPIncomingConnectionEvent incoming = new TCPIncomingConnectionEvent(this, socket);
+ eventManager.executeEvent(incoming);
+ if (incoming.isCancelled()) {
+ safeClose(socket);
continue;
}
try {
- ClientID clientId = ClientID.random();
- ConnectionHandler handler = new ConnectionHandler(this, ssl, clientId);
- connectionHandlers.add(handler);
- handlerByClientId.put(clientId, handler);
-
- // NOTE:
- // TCP connect event is fired inside ConnectionHandler constructor (Transport.TCP).
- // FullyConnected will be fired later after UDP bind (if policy requires UDP).
- } catch (Exception ex) {
- try {
- ssl.close();
- } catch (Exception ignored) {
- }
+ socket.startHandshake();
+ } catch (Exception e) {
+ safeClose(socket);
+ continue;
}
+
+ try {
+ ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
+ ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
+
+ ClientConnection connection = ClientConnection.tcpAccepted(codec, protocolMode, eventManager, socket, out, in);
+
+ connections.add(connection);
+ connectionsById.put(connection.connectionId(), connection);
+
+ eventManager.executeEvent(new ClientConnectionConnectedEvent(connection, NetworkProtocol.TCP));
+
+ codec.sendToStream(new ConnectionIdPacket(connection.connectionId()), connection.tcpOut());
+ codec.sendToStream(new ProtocolRequirementsPacket(protocolMode.required()), connection.tcpOut());
+
+ if (isFullyConnected(connection)) {
+ eventManager.executeEvent(new ClientConnectionFullyConnectedEvent(connection, protocolMode.required()));
+ }
+
+ Thread rx = new Thread(() -> tcpReceiveLoop(connection),
+ "SecureNetworkServer-TCP-Rx-" + connection.connectionId());
+ rx.start();
+ } catch (Exception e) {
+ safeClose(socket);
+ }
+ }
+ } catch (Exception ignored) {
+ }
+ }
+
+ private void tcpReceiveLoop(ClientConnection connection) {
+ boolean wasFully = isFullyConnected(connection);
+
+ try {
+ while (!Thread.currentThread().isInterrupted() && connection.isTcpConnected()) {
+ Packet packet = codec.receiveFromStream(connection.tcpIn());
+ if (packet == null) continue;
+
+ eventManager.executeEvent(new S_PacketReceivedEvent(connection, packet, NetworkProtocol.TCP));
}
} catch (Exception e) {
- if (!Thread.currentThread().isInterrupted()) {
- logException("TCP accept loop failed", e);
- }
+ eventManager.executeEvent(new S_PacketReceivedFailedEvent(connection, null, NetworkProtocol.TCP, e));
+ } finally {
+ cleanupConnection(connection, wasFully);
}
}
@@ -341,300 +339,249 @@ public final class NetworkServer {
try {
while (udpChannel != null && udpChannel.isOpen() && !Thread.currentThread().isInterrupted()) {
DtlsEndpoint endpoint = dtlsEndpoint;
- if (endpoint != null) {
- endpoint.poll();
- }
+ if (endpoint != null) endpoint.poll();
Thread.onSpinWait();
}
- } catch (Exception e) {
- if (!Thread.currentThread().isInterrupted()) {
- logException("UDP loop failed", e);
- }
+ } catch (Exception ignored) {
}
}
private void onDtlsApplicationData(SocketAddress remote, ByteBuffer data) {
+ UDPIncomingConnectionEvent incoming = new UDPIncomingConnectionEvent(this, remote, data.asReadOnlyBuffer());
+ eventManager.executeEvent(incoming);
+ if (incoming.isCancelled()) {
+ return;
+ }
+
try {
- UDPIncomingConnectionEvent event =
- new UDPIncomingConnectionEvent(this, remote, data);
- eventManager.executeEvent(event);
- if (event.isCancelled()) {
+ Packet packet = codec.decodeFromBuffer(data);
+
+ if (packet instanceof UdpHelloPacket) {
+ handleUdpHello(remote);
return;
}
- Packet decoded = UdpPacketCodec.decodeAndHandle(packetHandler, data);
- if (decoded == null) {
- // No handler associated here; keep null for handler
- eventManager.executeEvent(new S_UnknownObjectReceivedEvent(data, null, Transport.UDP));
+ if (packet instanceof UdpBindPacket bind) {
+ handleUdpBind(remote, bind);
return;
}
- // Bind flow: DTLS is up, now bind remote <-> clientId using UdpBindPacket(ClientID)
- if (decoded instanceof UdpBindPacket bind) {
- ConnectionHandler handler = handlerByClientId.get(bind.getClientId());
- if (handler == null) {
- // Unknown/expired client id - ignore
- return;
- }
-
- boolean wasFullyConnected = handler.isFullyConnected();
-
- // attachUdp fires ConnectionHandlerConnectedEvent(Transport.UDP) internally
- DtlsEndpoint endpoint = dtlsEndpoint;
- if (endpoint == null) {
- return;
- }
- handler.attachUdp(remote, endpoint.session(remote), endpoint);
-
- boolean nowFullyConnected = handler.isFullyConnected();
- if (!wasFullyConnected && nowFullyConnected) {
- eventManager.executeEvent(new ConnectionHandlerFullyConnectedEvent(
- handler,
- transportPolicy.required().toArray(new Transport[0])
- ));
- }
- return;
- }
-
- // Route other UDP packets by already bound remote address
- ConnectionHandler bound = findHandlerByUdpRemote(remote);
- if (bound != null) {
- eventManager.executeEvent(new S_PacketReceivedEvent(bound, decoded, Transport.UDP));
+ ClientConnection conn = connectionsByUdpRemote.get(remote);
+ if (conn != null) {
+ eventManager.executeEvent(new S_PacketReceivedEvent(conn, packet, NetworkProtocol.UDP));
}
} catch (Exception ignored) {
- // best effort: drop
}
}
- /**
- * Finds a handler by its bound UDP remote address.
- *
- * @param remote remote address
- * @return handler or null
- */
- public ConnectionHandler findHandlerByUdpRemote(SocketAddress remote) {
- synchronized (connectionHandlers) {
- for (ConnectionHandler h : connectionHandlers) {
- if (remote != null && remote.equals(h.getUdpRemoteAddress())) {
- return h;
- }
+ private void handleUdpHello(SocketAddress remote) {
+ DtlsEndpoint endpoint = dtlsEndpoint;
+ DatagramChannel ch = udpChannel;
+ if (endpoint == null || ch == null) return;
+
+ try {
+ endpoint.handshake(remote);
+
+ UUID id = UUID.randomUUID();
+ ClientConnection conn = ClientConnection.udpOnly(codec, protocolMode, eventManager, id);
+
+ boolean wasFullyBefore = isFullyConnected(conn);
+
+ conn.attachUdp(remote, endpoint.session(remote), endpoint, ch);
+ conn.setUdpBound(true);
+
+ connections.add(conn);
+ connectionsById.put(conn.connectionId(), conn);
+ connectionsByUdpRemote.put(remote, conn);
+
+ eventManager.executeEvent(new ClientConnectionConnectedEvent(conn, NetworkProtocol.UDP));
+
+ conn.sendPacket(new ConnectionIdPacket(conn.connectionId()), NetworkProtocol.UDP);
+ conn.sendPacket(new ProtocolRequirementsPacket(protocolMode.required()), NetworkProtocol.UDP);
+
+ boolean fullyNow = isFullyConnected(conn);
+ if (!wasFullyBefore && fullyNow) {
+ eventManager.executeEvent(new ClientConnectionFullyConnectedEvent(conn, protocolMode.required()));
}
- }
- return null;
- }
-
- private void logInfo(String msg) {
- if (logger != null) logger.log(msg);
- else System.out.println(msg);
- }
-
- private void logException(String msg, Exception e) {
- if (logger != null) logger.exception(msg, e);
- else {
- System.err.println(msg + ": " + e.getMessage());
- e.printStackTrace();
+ } catch (Exception ignored) {
}
}
- // --- Builder ---
- public static class ServerBuilder extends DefaultMethodsOverrider {
+ private void handleUdpBind(SocketAddress remote, UdpBindPacket bind) {
+ ClientConnection conn = connectionsById.get(bind.connectionId());
+ if (conn == null) return;
+
+ DtlsEndpoint endpoint = dtlsEndpoint;
+ DatagramChannel ch = udpChannel;
+ if (endpoint == null || ch == null) return;
+
+ boolean wasFullyBefore = isFullyConnected(conn);
+
+ try {
+ endpoint.handshake(remote);
+
+ conn.attachUdp(remote, endpoint.session(remote), endpoint, ch);
+ conn.setUdpBound(true);
+
+ connectionsByUdpRemote.put(remote, conn);
+
+ eventManager.executeEvent(new ClientConnectionConnectedEvent(conn, NetworkProtocol.UDP));
+
+ conn.sendPacket(new UdpBindAckPacket(conn.connectionId()), NetworkProtocol.UDP);
+
+ boolean fullyNow = isFullyConnected(conn);
+ if (!wasFullyBefore && fullyNow) {
+ eventManager.executeEvent(new ClientConnectionFullyConnectedEvent(conn, protocolMode.required()));
+ }
+ } catch (Exception ignored) {
+ }
+ }
+
+ private void cleanupConnection(ClientConnection connection, boolean wasFully) {
+ boolean tcpWas = connection.isTcpConnected();
+ boolean udpWas = connection.isUdpConnected();
+
+ SocketAddress udpRemote = connection.udpRemote();
+
+ try {
+ connection.close();
+ } catch (Exception ignored) {
+ }
+
+ connections.remove(connection);
+ connectionsById.remove(connection.connectionId());
+
+ if (udpRemote != null) {
+ connectionsByUdpRemote.remove(udpRemote);
+ }
+
+ if (tcpWas) {
+ eventManager.executeEvent(new ClientConnectionDisconnectedEvent(connection, NetworkProtocol.TCP));
+ }
+ if (udpWas) {
+ eventManager.executeEvent(new ClientConnectionDisconnectedEvent(connection, NetworkProtocol.UDP));
+ }
+ if (wasFully) {
+ eventManager.executeEvent(new ClientConnectionFullyDisconnectedEvent(connection, protocolMode.required()));
+ }
+ }
+
+ private static void safeClose(SSLSocket socket) {
+ try {
+ socket.close();
+ } catch (Exception ignored) {
+ }
+ }
+
+ public static final class Builder {
+
+ private ServerProtocolMode protocolMode = ServerProtocolMode.bothRequired();
+ private ClientAuthMode clientAuthMode = ClientAuthMode.OPTIONAL;
+
+ private PacketCodec codec;
+ private EventManager eventManager;
private int tcpPort;
private int udpPort;
- private PacketHandler packetHandler;
- private EventManager eventManager;
- private Logger logger;
+ private int timeoutMillis = 5000;
+ private int mtu = 1400;
- private int timeout = 5000;
-
- private SSLServerSocketFactory tlsFactory;
+ private SSLServerSocketFactory tlsServerSocketFactory;
+ private SSLParameters tlsParameters;
private SSLContext dtlsContext;
- private TransportPolicy transportPolicy = TransportPolicy.bothRequired();
- private boolean requireClientCert;
-
- private File caFolder;
- private File serverCertFile;
- private File serverKeyFile;
-
- /**
- * Creates a TLS (TCP) server socket factory using your PEM setup.
- *
- * @param caFolder root CA folder
- * @param serverCert server certificate (PEM)
- * @param serverKey server key (PEM)
- * @return TLS server socket factory
- * @throws Exception if configuration fails
- */
- public static SSLServerSocketFactory createTLSServerSocketFactory(File caFolder, File serverCert, File serverKey) throws Exception {
- KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
- trustStore.load(null, null);
-
- int caIndex = 1;
- File[] caFiles = caFolder.listFiles((f) -> f.getName().endsWith(".pem"));
- if (caFiles != null) {
- for (File caFile : caFiles) {
- java.security.cert.Certificate cert = PemUtils.loadCertificate(caFile);
- trustStore.setCertificateEntry("ca" + (caIndex++), cert);
- }
- }
-
- TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
- tmf.init(trustStore);
-
- KeyStore keyStore = KeyStore.getInstance("PKCS12");
- keyStore.load(null, null);
-
- java.security.PrivateKey key = PemUtils.loadPrivateKey(serverKey);
- java.security.cert.Certificate cert = PemUtils.loadCertificate(serverCert);
- keyStore.setKeyEntry("server", key, null, new java.security.cert.Certificate[]{cert});
-
- KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
- kmf.init(keyStore, null);
-
- SSLContext tls = SSLContext.getInstance("TLSv1.3");
- tls.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
-
- return tls.getServerSocketFactory();
- }
-
- /**
- * Creates a DTLS SSLContext using the same key/trust setup.
- *
- * @param caFolder root CA folder
- * @param serverCert server certificate (PEM)
- * @param serverKey server key (PEM)
- * @return DTLS context
- * @throws Exception if configuration fails
- */
- public static SSLContext createDTLSContext(File caFolder, File serverCert, File serverKey) throws Exception {
- KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
- trustStore.load(null, null);
-
- int caIndex = 1;
- File[] caFiles = caFolder.listFiles((f) -> f.getName().endsWith(".pem"));
- if (caFiles != null) {
- for (File caFile : caFiles) {
- java.security.cert.Certificate cert = PemUtils.loadCertificate(caFile);
- trustStore.setCertificateEntry("ca" + (caIndex++), cert);
- }
- }
-
- TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
- tmf.init(trustStore);
-
- KeyStore keyStore = KeyStore.getInstance("PKCS12");
- keyStore.load(null, null);
-
- java.security.PrivateKey key = PemUtils.loadPrivateKey(serverKey);
- java.security.cert.Certificate cert = PemUtils.loadCertificate(serverCert);
- keyStore.setKeyEntry("server", key, null, new java.security.cert.Certificate[]{cert});
-
- KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
- kmf.init(keyStore, null);
-
- return DtlsEndpoint.createDtlsContext(kmf.getKeyManagers(), tmf.getTrustManagers());
- }
-
- public ServerBuilder setTcpPort(int port) {
- this.tcpPort = port;
+ public Builder setProtocolMode(ServerProtocolMode protocolMode) {
+ this.protocolMode = protocolMode;
return this;
}
- public ServerBuilder setUdpPort(int port) {
- this.udpPort = port;
+ public Builder setClientAuthMode(ClientAuthMode clientAuthMode) {
+ this.clientAuthMode = clientAuthMode;
return this;
}
- public ServerBuilder setPacketHandler(PacketHandler handler) {
- this.packetHandler = handler;
+ public Builder setCodec(PacketCodec codec) {
+ this.codec = codec;
return this;
}
- public ServerBuilder setEventManager(EventManager manager) {
- this.eventManager = manager;
+ public Builder setEventManager(EventManager eventManager) {
+ this.eventManager = eventManager;
return this;
}
- public ServerBuilder setLogger(Logger logger) {
- this.logger = logger;
+ public Builder setTcpPort(int tcpPort) {
+ this.tcpPort = tcpPort;
return this;
}
- public ServerBuilder setTimeout(int timeoutMillis) {
- this.timeout = timeoutMillis;
+ public Builder setUdpPort(int udpPort) {
+ this.udpPort = udpPort;
return this;
}
- public ServerBuilder setTransportPolicy(TransportPolicy policy) {
- this.transportPolicy = policy;
+ public Builder setTimeoutMillis(int timeoutMillis) {
+ this.timeoutMillis = timeoutMillis;
return this;
}
- public ServerBuilder setRequireClientCertificate(boolean requireClientCertificate) {
- this.requireClientCert = requireClientCertificate;
+ public Builder setMtu(int mtu) {
+ this.mtu = mtu;
return this;
}
- public ServerBuilder setRootCAFolder(File folder) {
- this.caFolder = folder;
+ public Builder setTlsServerSocketFactory(SSLServerSocketFactory tlsServerSocketFactory) {
+ this.tlsServerSocketFactory = tlsServerSocketFactory;
return this;
}
- public ServerBuilder setServerCertificate(File certFile, File keyFile) {
- this.serverCertFile = certFile;
- this.serverKeyFile = keyFile;
+ public Builder setTlsParameters(SSLParameters tlsParameters) {
+ this.tlsParameters = tlsParameters;
return this;
}
- public ServerBuilder setTLSServerSocketFactory(SSLServerSocketFactory factory) {
- this.tlsFactory = factory;
- return this;
- }
-
- public ServerBuilder setDTLSContext(SSLContext dtlsContext) {
+ public Builder setDtlsContext(SSLContext dtlsContext) {
this.dtlsContext = dtlsContext;
return this;
}
public NetworkServer build() {
- if (packetHandler == null) throw new IllegalStateException("PacketHandler not set");
- if (eventManager == null) throw new IllegalStateException("EventManager not set");
- if (transportPolicy == null) throw new IllegalStateException("TransportPolicy not set");
+ if (codec == null) throw new IllegalStateException("codec not set");
+ if (eventManager == null) throw new IllegalStateException("eventManager not set");
+ if (protocolMode == null) throw new IllegalStateException("protocolMode not set");
+ if (clientAuthMode == null) throw new IllegalStateException("clientAuthMode not set");
- if (transportPolicy.supported().contains(Transport.TCP) && tcpPort <= 0) {
- throw new IllegalStateException("TCP port not set");
- }
- if (transportPolicy.supported().contains(Transport.UDP) && udpPort <= 0) {
- throw new IllegalStateException("UDP port not set");
- }
+ SSLServerSocketFactory tcpFactory = tlsServerSocketFactory != null
+ ? tlsServerSocketFactory
+ : (SSLServerSocketFactory) ServerSocketFactory.getDefault();
- if ((tlsFactory == null || dtlsContext == null) && caFolder != null && serverCertFile != null && serverKeyFile != null) {
- try {
- if (tlsFactory == null && transportPolicy.supported().contains(Transport.TCP)) {
- tlsFactory = createTLSServerSocketFactory(caFolder, serverCertFile, serverKeyFile);
- }
- if (dtlsContext == null && transportPolicy.supported().contains(Transport.UDP)) {
- dtlsContext = createDTLSContext(caFolder, serverCertFile, serverKeyFile);
- }
- } catch (Exception e) {
- throw new RuntimeException("Failed to create TLS/DTLS configuration", e);
- }
- }
+ SSLContext dtls = dtlsContext != null ? dtlsContext : emptyDtls();
+
+ if (timeoutMillis <= 0) throw new IllegalStateException("timeoutMillis must be > 0");
+ if (mtu < 256) throw new IllegalStateException("mtu too small");
return new NetworkServer(
+ protocolMode,
+ clientAuthMode,
+ codec,
+ eventManager,
tcpPort,
udpPort,
- packetHandler,
- eventManager,
- logger,
- timeout,
- tlsFactory,
- dtlsContext,
- transportPolicy,
- requireClientCert
+ timeoutMillis,
+ mtu,
+ tcpFactory,
+ tlsParameters,
+ dtls
);
}
+
+ private static SSLContext emptyDtls() {
+ try {
+ return SSLContext.getInstance("DTLS");
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to create DTLS context", e);
+ }
+ }
}
}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/NetworkServerUdpHooks.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/NetworkServerUdpHooks.java
deleted file mode 100644
index 05e889c..0000000
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/NetworkServerUdpHooks.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
-package dev.unlegitdqrk.unlegitlibrary.network.system.server;
-
-import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
-import dev.unlegitdqrk.unlegitlibrary.network.system.packets.impl.UdpBindPacket;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.connect.ConnectionHandlerFullyConnectedEvent;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.receive.S_PacketReceivedEvent;
-import dev.unlegitdqrk.unlegitlibrary.network.system.udp.DtlsEndpoint;
-import dev.unlegitdqrk.unlegitlibrary.network.system.udp.UdpPacketCodec;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
-
-import java.net.SocketAddress;
-import java.nio.ByteBuffer;
-import java.util.Objects;
-
-/**
- * Helper methods for server-side DTLS/UDP handling.
- *
- * Drop this into your NetworkServer class (replace your existing onDtlsApplicationData).
- */
-final class NetworkServerUdpHooks {
-
- private NetworkServerUdpHooks() {
- }
-
- /**
- * Processes decrypted DTLS application data.
- *
- * @param server server
- * @param remote remote address
- * @param data decrypted data
- */
- static void onDtlsApplicationData(NetworkServer server, SocketAddress remote, ByteBuffer data) {
- Objects.requireNonNull(server, "server");
- Objects.requireNonNull(remote, "remote");
- Objects.requireNonNull(data, "data");
-
- try {
- Packet decoded = UdpPacketCodec.decodeAndHandle(server.getPacketHandler(), data);
- if (decoded == null) {
- return;
- }
-
- if (decoded instanceof UdpBindPacket bind) {
- // Bind remote to existing TCP handler
- ConnectionHandler handler = server.getConnectionHandlerByClientId(bind.getClientId());
- if (handler == null) {
- return;
- }
- if (!handler.isConnected()) {
- return;
- }
-
- DtlsEndpoint endpoint = server.getDtlsEndpoint();
- if (endpoint == null) {
- return;
- }
-
- boolean wasFullyConnected = handler.isFullyConnected();
- handler.attachUdp(remote, endpoint.session(remote), endpoint);
- boolean nowFullyConnected = handler.isFullyConnected();
-
- if (!wasFullyConnected && nowFullyConnected) {
- server.getEventManager().executeEvent(
- new ConnectionHandlerFullyConnectedEvent(handler, server.getTransportPolicy().required().toArray(new Transport[0]))
- );
- }
-
- return;
- }
-
- // For normal UDP packets: route to the bound handler (if any)
- ConnectionHandler bound = server.findHandlerByUdpRemote(remote);
- if (bound != null) {
- server.getEventManager().executeEvent(new S_PacketReceivedEvent(bound, decoded, Transport.UDP));
- }
- } catch (Exception ignored) {
- // best effort: drop
- }
- }
-}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ServerProtocolMode.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ServerProtocolMode.java
new file mode 100644
index 0000000..d9c818f
--- /dev/null
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/ServerProtocolMode.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
+ *
+ * You are unauthorized to remove this copyright.
+ * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
+ * See LICENSE-File if exists
+ */
+
+package dev.unlegitdqrk.unlegitlibrary.network.system.server;
+
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
+
+import java.util.EnumSet;
+import java.util.Objects;
+
+/**
+ * Defines which network protocols a server requires for a connection to be considered usable.
+ *
+ * Note: This model exposes only {@link #required()}.
+ * Therefore, "optional protocols" are not representable here. If you need optional transports,
+ * the model must include a separate supported-set.
+ */
+public final class ServerProtocolMode {
+
+ private final EnumSet required;
+
+ private ServerProtocolMode(EnumSet required) {
+ this.required = EnumSet.copyOf(Objects.requireNonNull(required, "required"));
+ if (this.required.isEmpty()) {
+ throw new IllegalArgumentException("required must not be empty");
+ }
+ }
+
+ /**
+ * Returns required protocols.
+ *
+ * @return required protocols
+ */
+ public EnumSet required() {
+ return EnumSet.copyOf(required);
+ }
+
+ /**
+ * Server requires TCP only.
+ *
+ * @return mode
+ */
+ public static ServerProtocolMode tcpOnly() {
+ return new ServerProtocolMode(EnumSet.of(NetworkProtocol.TCP));
+ }
+
+ /**
+ * Server requires UDP only.
+ *
+ * @return mode
+ */
+ public static ServerProtocolMode udpOnly() {
+ return new ServerProtocolMode(EnumSet.of(NetworkProtocol.UDP));
+ }
+
+ /**
+ * Server requires both TCP and UDP.
+ *
+ * @return mode
+ */
+ public static ServerProtocolMode bothRequired() {
+ return new ServerProtocolMode(EnumSet.of(NetworkProtocol.TCP, NetworkProtocol.UDP));
+ }
+}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_PacketReceivedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_PacketReceivedEvent.java
index f633eef..45bf602 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_PacketReceivedEvent.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_PacketReceivedEvent.java
@@ -6,52 +6,61 @@
* See LICENSE-File if exists
*/
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.receive;
import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.ConnectionHandler;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
+import dev.unlegitdqrk.unlegitlibrary.network.system.server.ClientConnection;
+
+import java.util.Objects;
/**
- * Fired when a packet was received on the server on a specific transport.
+ * Fired when a packet was received on the server on a specific protocol.
*/
public final class S_PacketReceivedEvent extends Event {
- private final ConnectionHandler connectionHandler;
+ private final ClientConnection connection;
private final Packet packet;
- private final Transport transport;
+ private final NetworkProtocol protocol;
/**
* Creates a new packet received event.
*
- * @param connectionHandler handler
- * @param packet packet
- * @param transport transport used
+ * @param connection connection
+ * @param packet packet
+ * @param protocol protocol used
*/
- public S_PacketReceivedEvent(ConnectionHandler connectionHandler, Packet packet, Transport transport) {
- this.connectionHandler = connectionHandler;
- this.packet = packet;
- this.transport = transport;
+ public S_PacketReceivedEvent(ClientConnection connection, Packet packet, NetworkProtocol protocol) {
+ this.connection = Objects.requireNonNull(connection, "connection");
+ this.packet = Objects.requireNonNull(packet, "packet");
+ this.protocol = Objects.requireNonNull(protocol, "protocol");
}
- public ConnectionHandler getConnectionHandler() {
- return connectionHandler;
+ /**
+ * Returns the connection.
+ *
+ * @return connection
+ */
+ public ClientConnection getConnection() {
+ return connection;
}
+ /**
+ * Returns the packet.
+ *
+ * @return packet
+ */
public Packet getPacket() {
return packet;
}
- public Transport getTransport() {
- return transport;
+ /**
+ * Returns the protocol the packet was received on.
+ *
+ * @return protocol
+ */
+ public NetworkProtocol getProtocol() {
+ return protocol;
}
}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_PacketReceivedFailedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_PacketReceivedFailedEvent.java
index 1e0d292..3a8653a 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_PacketReceivedFailedEvent.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_PacketReceivedFailedEvent.java
@@ -6,52 +6,78 @@
* See LICENSE-File if exists
*/
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.receive;
import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.ConnectionHandler;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
+import dev.unlegitdqrk.unlegitlibrary.network.system.server.ClientConnection;
+
+import java.util.Objects;
/**
- * Fired when a packet receive failed on the server on a specific transport.
+ * Fired when receiving or decoding a packet failed on the server for a specific protocol.
*/
public final class S_PacketReceivedFailedEvent extends Event {
- private final ConnectionHandler connectionHandler;
+ private final ClientConnection connection;
private final Packet packet;
- private final Transport transport;
+ private final NetworkProtocol protocol;
+ private final Exception error;
/**
* Creates a new packet receive failed event.
*
- * @param connectionHandler handler
- * @param packet packet
- * @param transport transport used
+ * @param connection connection (may be null if not attributable)
+ * @param packet packet that failed to be processed (may be null if undecodable)
+ * @param protocol protocol the failure happened on
+ * @param error root cause
*/
- public S_PacketReceivedFailedEvent(ConnectionHandler connectionHandler, Packet packet, Transport transport) {
- this.connectionHandler = connectionHandler;
- this.packet = packet;
- this.transport = transport;
+ public S_PacketReceivedFailedEvent(
+ ClientConnection connection,
+ Packet packet,
+ NetworkProtocol protocol,
+ Exception error
+ ) {
+ this.connection = connection; // may be null
+ this.packet = packet; // may be null
+ this.protocol = Objects.requireNonNull(protocol, "protocol");
+ this.error = Objects.requireNonNull(error, "error");
}
- public ConnectionHandler getConnectionHandler() {
- return connectionHandler;
+ /**
+ * Returns the connection, if attributable.
+ *
+ * @return connection or null
+ */
+ public ClientConnection getConnection() {
+ return connection;
}
+ /**
+ * Returns the packet that failed to be processed, if available.
+ *
+ * @return packet or null
+ */
public Packet getPacket() {
return packet;
}
- public Transport getTransport() {
- return transport;
+ /**
+ * Returns the protocol the failure occurred on.
+ *
+ * @return protocol
+ */
+ public NetworkProtocol getProtocol() {
+ return protocol;
+ }
+
+ /**
+ * Returns the underlying error.
+ *
+ * @return error
+ */
+ public Exception getError() {
+ return error;
}
}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_UnknownObjectReceivedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_UnknownObjectReceivedEvent.java
index 2d3f4e3..5b89393 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_UnknownObjectReceivedEvent.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/receive/S_UnknownObjectReceivedEvent.java
@@ -6,51 +6,65 @@
* See LICENSE-File if exists
*/
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.receive;
import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.ConnectionHandler;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
+import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
+import dev.unlegitdqrk.unlegitlibrary.network.system.server.ClientConnection;
+
+import java.util.Objects;
/**
- * Fired when an unknown object was received on a specific transport.
+ * Fired when an unknown (non-packet) object was received on a specific protocol.
+ *
+ * In v2 the default transport uses {@link dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketCodec}
+ * and usually yields {@link Packet}. This event is kept
+ * for custom/legacy decoding paths.
*/
public final class S_UnknownObjectReceivedEvent extends Event {
private final Object received;
- private final ConnectionHandler connectionHandler;
- private final Transport transport;
+ private final ClientConnection connection;
+ private final NetworkProtocol protocol;
/**
* Creates a new event.
*
- * @param received received object
- * @param connectionHandler handler
- * @param transport transport
+ * @param received received object
+ * @param connection connection (may be null if not attributable)
+ * @param protocol protocol
*/
- public S_UnknownObjectReceivedEvent(Object received, ConnectionHandler connectionHandler, Transport transport) {
- this.received = received;
- this.connectionHandler = connectionHandler;
- this.transport = transport;
+ public S_UnknownObjectReceivedEvent(Object received, ClientConnection connection, NetworkProtocol protocol) {
+ this.received = Objects.requireNonNull(received, "received");
+ this.connection = connection; // may be null
+ this.protocol = Objects.requireNonNull(protocol, "protocol");
}
- public ConnectionHandler getConnectionHandler() {
- return connectionHandler;
+ /**
+ * Returns the connection, if attributable.
+ *
+ * @return connection or null
+ */
+ public ClientConnection getConnection() {
+ return connection;
}
+ /**
+ * Returns the received object.
+ *
+ * @return received object
+ */
public Object getReceived() {
return received;
}
- public Transport getTransport() {
- return transport;
+ /**
+ * Returns the protocol.
+ *
+ * @return protocol
+ */
+ public NetworkProtocol getProtocol() {
+ return protocol;
}
}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/send/S_PacketSendEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/send/S_PacketSendEvent.java
index 206bb6d..3799aef 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/send/S_PacketSendEvent.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/send/S_PacketSendEvent.java
@@ -6,52 +6,61 @@
* See LICENSE-File if exists
*/
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.send;
import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.ConnectionHandler;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
+import dev.unlegitdqrk.unlegitlibrary.network.system.server.ClientConnection;
+
+import java.util.Objects;
/**
- * Fired when a packet was sent by the server on a specific transport.
+ * Fired when a packet was successfully sent by the server on a specific protocol.
*/
public final class S_PacketSendEvent extends Event {
private final Packet packet;
- private final ConnectionHandler connectionHandler;
- private final Transport transport;
+ private final ClientConnection connection;
+ private final NetworkProtocol protocol;
/**
* Creates a new packet send event.
*
- * @param packet packet
- * @param connectionHandler handler
- * @param transport transport used
+ * @param packet packet
+ * @param connection connection
+ * @param protocol protocol used
*/
- public S_PacketSendEvent(Packet packet, ConnectionHandler connectionHandler, Transport transport) {
- this.packet = packet;
- this.connectionHandler = connectionHandler;
- this.transport = transport;
+ public S_PacketSendEvent(Packet packet, ClientConnection connection, NetworkProtocol protocol) {
+ this.packet = Objects.requireNonNull(packet, "packet");
+ this.connection = Objects.requireNonNull(connection, "connection");
+ this.protocol = Objects.requireNonNull(protocol, "protocol");
}
- public ConnectionHandler getConnectionHandler() {
- return connectionHandler;
+ /**
+ * Returns the connection.
+ *
+ * @return connection
+ */
+ public ClientConnection getConnection() {
+ return connection;
}
+ /**
+ * Returns the packet.
+ *
+ * @return packet
+ */
public Packet getPacket() {
return packet;
}
- public Transport getTransport() {
- return transport;
+ /**
+ * Returns the protocol used.
+ *
+ * @return protocol
+ */
+ public NetworkProtocol getProtocol() {
+ return protocol;
}
}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/send/S_PacketSendFailedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/send/S_PacketSendFailedEvent.java
index c239a92..e85bdb4 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/send/S_PacketSendFailedEvent.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/packets/send/S_PacketSendFailedEvent.java
@@ -6,52 +6,78 @@
* See LICENSE-File if exists
*/
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.packets.send;
import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.ConnectionHandler;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
+import dev.unlegitdqrk.unlegitlibrary.network.system.server.ClientConnection;
+
+import java.util.Objects;
/**
- * Fired when a packet send failed on the server on a specific transport.
+ * Fired when a packet send failed on the server on a specific protocol.
*/
public final class S_PacketSendFailedEvent extends Event {
private final Packet packet;
- private final ConnectionHandler connectionHandler;
- private final Transport transport;
+ private final ClientConnection connection;
+ private final NetworkProtocol protocol;
+ private final Exception error;
/**
* Creates a new packet send failed event.
*
- * @param packet packet
- * @param connectionHandler handler
- * @param transport intended transport
+ * @param packet packet that failed to be sent
+ * @param connection connection
+ * @param protocol intended protocol
+ * @param error root cause
*/
- public S_PacketSendFailedEvent(Packet packet, ConnectionHandler connectionHandler, Transport transport) {
- this.packet = packet;
- this.connectionHandler = connectionHandler;
- this.transport = transport;
+ public S_PacketSendFailedEvent(
+ Packet packet,
+ ClientConnection connection,
+ NetworkProtocol protocol,
+ Exception error
+ ) {
+ this.packet = Objects.requireNonNull(packet, "packet");
+ this.connection = Objects.requireNonNull(connection, "connection");
+ this.protocol = Objects.requireNonNull(protocol, "protocol");
+ this.error = Objects.requireNonNull(error, "error");
}
- public ConnectionHandler getConnectionHandler() {
- return connectionHandler;
+ /**
+ * Returns the connection.
+ *
+ * @return connection
+ */
+ public ClientConnection getConnection() {
+ return connection;
}
+ /**
+ * Returns the packet.
+ *
+ * @return packet
+ */
public Packet getPacket() {
return packet;
}
- public Transport getTransport() {
- return transport;
+ /**
+ * Returns the intended protocol.
+ *
+ * @return protocol
+ */
+ public NetworkProtocol getProtocol() {
+ return protocol;
+ }
+
+ /**
+ * Returns the underlying error.
+ *
+ * @return error
+ */
+ public Exception getError() {
+ return error;
}
}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ClientConnectionConnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ClientConnectionConnectedEvent.java
new file mode 100644
index 0000000..8e97c9b
--- /dev/null
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ClientConnectionConnectedEvent.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
+ *
+ * You are unauthorized to remove this copyright.
+ * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
+ * See LICENSE-File if exists
+ */
+
+package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.connect;
+
+import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
+import dev.unlegitdqrk.unlegitlibrary.network.system.server.ClientConnection;
+
+import java.util.Objects;
+
+/**
+ * Fired when a specific protocol becomes connected for a server-side connection.
+ *
+ * Protocol-specific:
+ *
+ * - {@link NetworkProtocol#TCP}: connection created after TLS handshake
+ * - {@link NetworkProtocol#UDP}: connection received valid UDP bind and attached DTLS session
+ *
+ */
+public final class ClientConnectionConnectedEvent extends Event {
+
+ private final ClientConnection connection;
+ private final NetworkProtocol protocol;
+
+ /**
+ * Creates a new connection connected event.
+ *
+ * @param connection connection
+ * @param protocol connected protocol
+ */
+ public ClientConnectionConnectedEvent(ClientConnection connection, NetworkProtocol protocol) {
+ this.connection = Objects.requireNonNull(connection, "connection");
+ this.protocol = Objects.requireNonNull(protocol, "protocol");
+ }
+
+ /**
+ * Returns the connection.
+ *
+ * @return connection
+ */
+ public ClientConnection getConnection() {
+ return connection;
+ }
+
+ /**
+ * Returns the protocol that was connected.
+ *
+ * @return protocol
+ */
+ public NetworkProtocol getProtocol() {
+ return protocol;
+ }
+}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ClientConnectionFullyConnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ClientConnectionFullyConnectedEvent.java
new file mode 100644
index 0000000..88e317b
--- /dev/null
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ClientConnectionFullyConnectedEvent.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
+ *
+ * You are unauthorized to remove this copyright.
+ * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
+ * See LICENSE-File if exists
+ */
+
+package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.connect;
+
+import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
+import dev.unlegitdqrk.unlegitlibrary.network.system.server.ClientConnection;
+
+import java.util.EnumSet;
+import java.util.Objects;
+
+/**
+ * Fired when a connection satisfies the server protocol requirements.
+ *
+ * In v2 this typically means:
+ *
+ * - TCP is connected (TLS ok)
+ * - and if UDP is enabled: DTLS is established and the bind flow is completed
+ *
+ */
+public final class ClientConnectionFullyConnectedEvent extends Event {
+
+ private final ClientConnection connection;
+ private final EnumSet requiredProtocols;
+
+ /**
+ * Creates a new event.
+ *
+ * @param connection connection
+ * @param requiredProtocols required protocols now established
+ */
+ public ClientConnectionFullyConnectedEvent(ClientConnection connection, EnumSet requiredProtocols) {
+ this.connection = Objects.requireNonNull(connection, "connection");
+ this.requiredProtocols = EnumSet.copyOf(Objects.requireNonNull(requiredProtocols, "requiredProtocols"));
+ }
+
+ /**
+ * Returns the connection.
+ *
+ * @return connection
+ */
+ public ClientConnection getConnection() {
+ return connection;
+ }
+
+ /**
+ * Returns the required protocols that are now established.
+ *
+ * @return required protocols (copy)
+ */
+ public EnumSet getRequiredProtocols() {
+ return EnumSet.copyOf(requiredProtocols);
+ }
+}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ConnectionHandlerConnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ConnectionHandlerConnectedEvent.java
deleted file mode 100644
index 928c278..0000000
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ConnectionHandlerConnectedEvent.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
-package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.connect;
-
-import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.ConnectionHandler;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
-
-import java.util.Objects;
-
-/**
- * Fired when a specific transport becomes connected for a handler.
- *
- * Transport-specific:
- *
- * - {@link Transport#TCP}: handler created after TLS handshake
- * - {@link Transport#UDP}: handler received valid UDP bind and attached DTLS session
- *
- */
-public final class ConnectionHandlerConnectedEvent extends Event {
-
- private final ConnectionHandler connectionHandler;
- private final Transport transport;
-
- /**
- * Creates a new handler connected event.
- *
- * @param connectionHandler handler
- * @param transport connected transport
- */
- public ConnectionHandlerConnectedEvent(ConnectionHandler connectionHandler, Transport transport) {
- this.connectionHandler = Objects.requireNonNull(connectionHandler, "connectionHandler");
- this.transport = Objects.requireNonNull(transport, "transport");
- }
-
- public ConnectionHandler getConnectionHandler() {
- return connectionHandler;
- }
-
- public Transport getTransport() {
- return transport;
- }
-}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ConnectionHandlerFullyConnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ConnectionHandlerFullyConnectedEvent.java
deleted file mode 100644
index 7e43e98..0000000
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/connect/ConnectionHandlerFullyConnectedEvent.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
-package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.connect;
-
-import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.ConnectionHandler;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
-
-/**
- * Fired when a connection handler satisfies the server transport policy.
- *
- * For your setup this means: TCP connected and UDP (DTLS) bound.
- */
-public final class ConnectionHandlerFullyConnectedEvent extends Event {
-
- private final ConnectionHandler connectionHandler;
- private final Transport[] requiredTransports;
-
- /**
- * Creates a new event.
- *
- * @param connectionHandler handler
- * @param requiredTransports required transports now established
- */
- public ConnectionHandlerFullyConnectedEvent(ConnectionHandler connectionHandler, Transport[] requiredTransports) {
- this.connectionHandler = connectionHandler;
- this.requiredTransports = requiredTransports;
- }
-
- /**
- * Returns the handler.
- *
- * @return handler
- */
- public ConnectionHandler getConnectionHandler() {
- return connectionHandler;
- }
-
- /**
- * Returns the required transports that are now established.
- *
- * @return transports
- */
- public Transport[] getRequiredTransports() {
- return requiredTransports;
- }
-}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ClientConnectionDisconnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ClientConnectionDisconnectedEvent.java
new file mode 100644
index 0000000..b6dad28
--- /dev/null
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ClientConnectionDisconnectedEvent.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
+ *
+ * You are unauthorized to remove this copyright.
+ * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
+ * See LICENSE-File if exists
+ */
+
+package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.disconnect;
+
+import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
+import dev.unlegitdqrk.unlegitlibrary.network.system.server.ClientConnection;
+
+import java.util.Objects;
+
+/**
+ * Fired when a specific protocol becomes disconnected for a server-side connection.
+ */
+public final class ClientConnectionDisconnectedEvent extends Event {
+
+ private final ClientConnection connection;
+ private final NetworkProtocol protocol;
+
+ /**
+ * Creates a new connection disconnected event.
+ *
+ * @param connection connection
+ * @param protocol disconnected protocol
+ */
+ public ClientConnectionDisconnectedEvent(ClientConnection connection, NetworkProtocol protocol) {
+ this.connection = Objects.requireNonNull(connection, "connection");
+ this.protocol = Objects.requireNonNull(protocol, "protocol");
+ }
+
+ /**
+ * Returns the connection.
+ *
+ * @return connection
+ */
+ public ClientConnection getConnection() {
+ return connection;
+ }
+
+ /**
+ * Returns the protocol that was disconnected.
+ *
+ * @return protocol
+ */
+ public NetworkProtocol getProtocol() {
+ return protocol;
+ }
+}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ClientConnectionFullyDisconnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ClientConnectionFullyDisconnectedEvent.java
new file mode 100644
index 0000000..c32a590
--- /dev/null
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ClientConnectionFullyDisconnectedEvent.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
+ *
+ * You are unauthorized to remove this copyright.
+ * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
+ * See LICENSE-File if exists
+ */
+
+package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.disconnect;
+
+import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
+import dev.unlegitdqrk.unlegitlibrary.network.system.server.ClientConnection;
+
+import java.util.EnumSet;
+import java.util.Objects;
+
+/**
+ * Fired when a connection was fully connected and then became fully disconnected
+ * (i.e., it no longer satisfies the required protocols).
+ */
+public final class ClientConnectionFullyDisconnectedEvent extends Event {
+
+ private final ClientConnection connection;
+ private final EnumSet requiredProtocols;
+
+ /**
+ * Creates a new fully disconnected event.
+ *
+ * @param connection connection
+ * @param requiredProtocols required protocols according to policy
+ */
+ public ClientConnectionFullyDisconnectedEvent(ClientConnection connection, EnumSet requiredProtocols) {
+ this.connection = Objects.requireNonNull(connection, "connection");
+ this.requiredProtocols = EnumSet.copyOf(Objects.requireNonNull(requiredProtocols, "requiredProtocols"));
+ }
+
+ /**
+ * Returns the connection.
+ *
+ * @return connection
+ */
+ public ClientConnection getConnection() {
+ return connection;
+ }
+
+ /**
+ * Returns the protocols that were required for full connectivity.
+ *
+ * @return required protocols (copy)
+ */
+ public EnumSet getRequiredProtocols() {
+ return EnumSet.copyOf(requiredProtocols);
+ }
+}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ConnectionHandlerDisconnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ConnectionHandlerDisconnectedEvent.java
deleted file mode 100644
index c7d2286..0000000
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ConnectionHandlerDisconnectedEvent.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
-package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.disconnect;
-
-import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.ConnectionHandler;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
-
-import java.util.Objects;
-
-/**
- * Fired when a specific transport becomes disconnected for a handler.
- */
-public final class ConnectionHandlerDisconnectedEvent extends Event {
-
- private final ConnectionHandler connectionHandler;
- private final Transport transport;
-
- /**
- * Creates a new handler disconnected event.
- *
- * @param connectionHandler handler
- * @param transport disconnected transport
- */
- public ConnectionHandlerDisconnectedEvent(ConnectionHandler connectionHandler, Transport transport) {
- this.connectionHandler = Objects.requireNonNull(connectionHandler, "connectionHandler");
- this.transport = Objects.requireNonNull(transport, "transport");
- }
-
- public ConnectionHandler getConnectionHandler() {
- return connectionHandler;
- }
-
- public Transport getTransport() {
- return transport;
- }
-}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ConnectionHandlerFullyDisconnectedEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ConnectionHandlerFullyDisconnectedEvent.java
deleted file mode 100644
index 1790c71..0000000
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/disconnect/ConnectionHandlerFullyDisconnectedEvent.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
-package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.disconnect;
-
-import dev.unlegitdqrk.unlegitlibrary.event.impl.Event;
-import dev.unlegitdqrk.unlegitlibrary.network.system.server.ConnectionHandler;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
-
-import java.util.Objects;
-
-/**
- * Fired when a handler was fully connected and then became fully disconnected.
- *
- * For your policy (TCP+UDP required) this means:
- * previously: TCP connected and UDP attached,
- * now: requirements are no longer satisfied.
- */
-public final class ConnectionHandlerFullyDisconnectedEvent extends Event {
-
- private final ConnectionHandler connectionHandler;
- private final Transport[] requiredTransports;
-
- /**
- * Creates a new fully disconnected event.
- *
- * @param connectionHandler handler
- * @param requiredTransports required transports according to policy
- */
- public ConnectionHandlerFullyDisconnectedEvent(ConnectionHandler connectionHandler, Transport[] requiredTransports) {
- this.connectionHandler = Objects.requireNonNull(connectionHandler, "connectionHandler");
- this.requiredTransports = Objects.requireNonNull(requiredTransports, "requiredTransports");
- }
-
- public ConnectionHandler getConnectionHandler() {
- return connectionHandler;
- }
-
- public Transport[] getRequiredTransports() {
- return requiredTransports;
- }
-}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/incoming/TCPIncomingConnectionEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/incoming/TCPIncomingConnectionEvent.java
index 03d16e8..d262e11 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/incoming/TCPIncomingConnectionEvent.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/incoming/TCPIncomingConnectionEvent.java
@@ -10,22 +10,21 @@ package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.incomi
import dev.unlegitdqrk.unlegitlibrary.event.impl.CancellableEvent;
import dev.unlegitdqrk.unlegitlibrary.network.system.server.NetworkServer;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
import javax.net.ssl.SSLSocket;
import java.util.Objects;
/**
- * Fired when an incoming connection attempt reaches the server.
+ * Fired when an incoming TCP/TLS connection attempt reaches the server.
*
- * Currently this event is emitted for TCP/TLS accept only (because UDP/DTLS is bound later via {@code UdpBindPacket}).
- * The {@link #getTransport()} field exists to keep the event model transport-aware and future-proof.
+ * This event is emitted for TCP/TLS accept only. UDP/DTLS is bound later via the bind flow.
*/
public final class TCPIncomingConnectionEvent extends CancellableEvent {
private final NetworkServer server;
private final SSLSocket socket;
- private final Transport transport = Transport.TCP;
+ private final NetworkProtocol protocol = NetworkProtocol.TCP;
/**
* Creates a new incoming connection event.
@@ -57,11 +56,11 @@ public final class TCPIncomingConnectionEvent extends CancellableEvent {
}
/**
- * Returns the transport associated with this incoming connection.
+ * Returns the protocol associated with this incoming connection.
*
- * @return transport
+ * @return {@link NetworkProtocol#TCP}
*/
- public Transport getTransport() {
- return transport;
+ public NetworkProtocol getProtocol() {
+ return protocol;
}
}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/incoming/UDPIncomingConnectionEvent.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/incoming/UDPIncomingConnectionEvent.java
index a9c8744..a828eb5 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/incoming/UDPIncomingConnectionEvent.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/server/events/state/incoming/UDPIncomingConnectionEvent.java
@@ -10,21 +10,20 @@ package dev.unlegitdqrk.unlegitlibrary.network.system.server.events.state.incomi
import dev.unlegitdqrk.unlegitlibrary.event.impl.CancellableEvent;
import dev.unlegitdqrk.unlegitlibrary.network.system.server.NetworkServer;
-import dev.unlegitdqrk.unlegitlibrary.network.system.utils.Transport;
+import dev.unlegitdqrk.unlegitlibrary.network.system.utils.NetworkProtocol;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.util.Objects;
/**
- * Fired when an incoming UDP/DTLS datagram is received by the server
- * before it is bound to a {@link dev.unlegitdqrk.unlegitlibrary.network.system.server.ConnectionHandler}.
+ * Fired when an incoming UDP datagram is received by the server before it is bound to a connection.
*
* This event allows inspection or rejection of:
*
* - DTLS handshake traffic
- * - {@code UdpBindPacket}
- * - Any unbound UDP packet
+ * - bind packets
+ * - any unbound UDP packet
*
*
* If cancelled, the datagram is silently dropped.
@@ -34,14 +33,14 @@ public final class UDPIncomingConnectionEvent extends CancellableEvent {
private final NetworkServer server;
private final SocketAddress remoteAddress;
private final ByteBuffer rawData;
- private final Transport transport = Transport.UDP;
+ private final NetworkProtocol protocol = NetworkProtocol.UDP;
/**
- * Creates a new incoming UDP connection/datagram event.
+ * Creates a new incoming UDP datagram event.
*
* @param server server instance
* @param remoteAddress remote UDP address
- * @param rawData raw received datagram (read-only duplicate recommended)
+ * @param rawData raw received datagram (a read-only copy will be stored)
*/
public UDPIncomingConnectionEvent(
NetworkServer server,
@@ -74,7 +73,7 @@ public final class UDPIncomingConnectionEvent extends CancellableEvent {
/**
* Returns the raw UDP datagram payload.
*
- * The buffer is read-only and positioned at the start of the payload.
+ * The buffer is read-only.
*
* @return raw datagram data
*/
@@ -83,11 +82,11 @@ public final class UDPIncomingConnectionEvent extends CancellableEvent {
}
/**
- * Returns the transport type of this incoming connection.
+ * Returns the protocol type of this incoming datagram.
*
- * @return {@link Transport#UDP}
+ * @return {@link NetworkProtocol#UDP}
*/
- public Transport getTransport() {
- return transport;
+ public NetworkProtocol getProtocol() {
+ return protocol;
}
}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/udp/UdpPacketCodec.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/udp/UdpPacketCodec.java
deleted file mode 100644
index 143b720..0000000
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/udp/UdpPacketCodec.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
-package dev.unlegitdqrk.unlegitlibrary.network.system.udp;
-
-import dev.unlegitdqrk.unlegitlibrary.network.system.packets.Packet;
-import dev.unlegitdqrk.unlegitlibrary.network.system.packets.PacketHandler;
-
-import java.io.*;
-import java.nio.ByteBuffer;
-
-/**
- * Encodes/decodes packets for UDP transport.
- *
- * Format:
- *
- * [int packetId][ObjectOutputStream payload bytes...]
- *
- */
-public final class UdpPacketCodec {
-
- private UdpPacketCodec() {
- }
-
- /**
- * Encodes a packet into a byte buffer ready for sending.
- *
- * @param handler packet handler
- * @param packet packet
- * @return encoded buffer (position=0, limit=length)
- * @throws IOException on I/O errors
- * @throws ClassNotFoundException on serialization errors
- */
- public static ByteBuffer encode(PacketHandler handler, Packet packet) throws IOException, ClassNotFoundException {
- ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
-
- try (DataOutputStream dos = new DataOutputStream(baos)) {
- dos.writeInt(packet.getPacketID());
-
- // Keep your existing Packet API (ObjectOutputStream)
- try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
- packet.write(handler, oos);
- oos.flush();
- }
- }
-
- byte[] bytes = baos.toByteArray();
- return ByteBuffer.wrap(bytes);
- }
-
- /**
- * Decodes a packet id and lets the handler read the payload into a packet instance.
- *
- * @param handler packet handler
- * @param datagram datagram buffer (position=0, limit=length)
- * @return decoded packet instance (already read/filled), or {@code null} if unknown id
- * @throws IOException on errors
- * @throws ClassNotFoundException on errors
- */
- public static Packet decodeAndHandle(PacketHandler handler, ByteBuffer datagram) throws IOException, ClassNotFoundException {
- ByteArrayInputStream bais = new ByteArrayInputStream(datagram.array(), datagram.position(), datagram.remaining());
-
- int id;
- try (DataInputStream dis = new DataInputStream(bais)) {
- id = dis.readInt();
- }
-
- if (!handler.isPacketIDRegistered(id)) {
- return null;
- }
-
- Packet packet = handler.getPacketByID(id);
-
- // Now decode remaining bytes with ObjectInputStream
- int payloadOffset = 4;
- ByteArrayInputStream payload = new ByteArrayInputStream(datagram.array(), datagram.position() + payloadOffset, datagram.remaining() - payloadOffset);
-
- try (ObjectInputStream ois = new ObjectInputStream(payload)) {
- boolean ok = handler.handlePacket(id, packet, ois);
- return packet;
- }
- }
-}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/ClientAuthMode.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/ClientAuthMode.java
new file mode 100644
index 0000000..9a7789a
--- /dev/null
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/ClientAuthMode.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
+ *
+ * You are unauthorized to remove this copyright.
+ * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
+ * See LICENSE-File if exists
+ */
+
+package dev.unlegitdqrk.unlegitlibrary.network.system.utils;
+
+/**
+ * Defines whether the server requires client certificates during the TLS handshake.
+ *
+ * {@link #REQUIRED} enforces mutual TLS (mTLS): clients must present a certificate.
+ *
+ * {@link #OPTIONAL} allows clients without a certificate to connect (server will request a certificate,
+ * but does not fail the handshake if none is provided).
+ */
+public enum ClientAuthMode {
+
+ /**
+ * Client certificate is mandatory (mTLS).
+ */
+ REQUIRED,
+
+ /**
+ * Client certificate is optional.
+ */
+ OPTIONAL
+}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/ClientID.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/ClientID.java
deleted file mode 100644
index 64628d8..0000000
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/ClientID.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
-package dev.unlegitdqrk.unlegitlibrary.network.system.utils;
-
-import java.io.Serial;
-import java.io.Serializable;
-import java.util.Objects;
-import java.util.UUID;
-
-/**
- * Immutable identifier for a client (similar to a Minecraft player's UUID).
- */
-public record ClientID(UUID uuid) implements Serializable {
-
- @Serial
- private static final long serialVersionUID = 1L;
-
- /**
- * Creates a new {@link ClientID}.
- *
- * @param uuid backing UUID (must not be {@code null})
- */
- public ClientID(UUID uuid) {
- this.uuid = Objects.requireNonNull(uuid, "uuid");
- }
-
- /**
- * Generates a random {@link ClientID}.
- *
- * @return random client id
- */
- public static ClientID random() {
- return new ClientID(UUID.randomUUID());
- }
-
- /**
- * Returns backing UUID.
- *
- * @return UUID
- */
- @Override
- public UUID uuid() {
- return uuid;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) return true;
- if (!(obj instanceof ClientID other)) return false;
- return uuid.equals(other.uuid);
- }
-
- @Override
- public String toString() {
- return "ClientID{uuid=" + uuid + "}";
- }
-}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/udp/DtlsEndpoint.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/DtlsEndpoint.java
similarity index 63%
rename from src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/udp/DtlsEndpoint.java
rename to src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/DtlsEndpoint.java
index 241269d..8742bc6 100644
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/udp/DtlsEndpoint.java
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/DtlsEndpoint.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
+ *
+ * You are unauthorized to remove this copyright.
+ * You have to give Credits to the Author in your project and link this GitHub site: https://github.com/UnlegitDqrk
+ * See LICENSE-File if exists
+ */
+
+/*
+ * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
+ *
+ * You are unauthorized to remove this copyright.
+ * You have to give Credits to the Author in your project and link this GitHub site: https://github.com/UnlegitDqrk
+ * See LICENSE-File if exists
+ */
+
/*
* Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
*
@@ -6,47 +22,69 @@
* See LICENSE-File if exists
*/
-package dev.unlegitdqrk.unlegitlibrary.network.system.udp;
+package dev.unlegitdqrk.unlegitlibrary.network.system.utils;
-import javax.net.ssl.*;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.TrustManager;
import java.io.IOException;
+import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.security.SecureRandom;
+import java.security.cert.Certificate;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
- * Minimal DTLS endpoint using {@link SSLEngine} over {@link DatagramChannel}.
+ * Minimal DTLS endpoint using {@link SSLEngine} over a datagram transport.
*
* This implementation is designed for "best effort" UDP and focuses on:
*
* - DTLS handshake via SSLEngine
* - wrap/unwrap application data into datagrams
- * - per remote address sessions
+ * - per-remote address sessions
*
+ *
+ * Server mode note: A newly created DTLS session must enter handshake
+ * ({@link SSLEngine#beginHandshake()}) immediately, otherwise initial client handshake datagrams
+ * may not be processed and the client may time out.
+ *
+ * Client certificate policy (server-side):
+ * For {@link ClientAuthMode#REQUIRED} the presence of a peer certificate is enforced after handshake
+ * completion (via {@link SSLEngine#getSession()} and {@code getPeerCertificates()}), because
+ * {@link SSLParameters#setNeedClientAuth(boolean)} is not reliably enforced for {@link SSLEngine} across providers.
*/
public final class DtlsEndpoint {
- private final DatagramChannel channel;
+ private static final ByteBuffer EMPTY = ByteBuffer.allocate(0);
+
+ private final java.nio.channels.DatagramChannel channel;
private final SSLContext sslContext;
private final boolean clientMode;
private final int mtu;
private final int timeoutMillis;
private final ApplicationDataHandler appHandler;
+ private final ClientAuthMode clientAuthMode;
+
private final Map sessions = new ConcurrentHashMap<>();
/**
* Creates a DTLS endpoint.
*
- * @param channel underlying datagram channel (bound for server, unbound or bound for client)
- * @param sslContext DTLS SSL context (created with "DTLS")
- * @param clientMode true for client sessions, false for server sessions
- * @param mtu maximum datagram size
- * @param timeoutMillis handshake/read timeout for polling
- * @param appHandler application data handler
+ * @param channel underlying datagram channel (bound for server, connected or unconnected for client)
+ * @param sslContext DTLS SSL context (created with "DTLS")
+ * @param clientMode true for client sessions, false for server sessions
+ * @param mtu maximum datagram size
+ * @param timeoutMillis handshake/read timeout for polling
+ * @param clientAuthMode server-side client auth policy (OPTIONAL/REQUIRED). Ignored for clientMode=true.
+ * @param appHandler application data handler
*/
public DtlsEndpoint(
DatagramChannel channel,
@@ -54,21 +92,24 @@ public final class DtlsEndpoint {
boolean clientMode,
int mtu,
int timeoutMillis,
+ ClientAuthMode clientAuthMode,
ApplicationDataHandler appHandler
- ) {
+ )
+ {
this.channel = Objects.requireNonNull(channel, "channel");
this.sslContext = Objects.requireNonNull(sslContext, "sslContext");
this.clientMode = clientMode;
this.mtu = mtu;
this.timeoutMillis = timeoutMillis;
+ this.clientAuthMode = Objects.requireNonNull(clientAuthMode, "clientAuthMode");
this.appHandler = Objects.requireNonNull(appHandler, "appHandler");
}
/**
- * Creates a DTLS SSLContext from an existing key+trust configuration.
+ * Creates a DTLS {@link SSLContext} from an existing key+trust configuration.
*
- * @param keyManagers key managers
- * @param trustManagers trust managers
+ * @param keyManagers key managers (nullable)
+ * @param trustManagers trust managers (nullable)
* @return DTLS SSL context
* @throws Exception on errors
*/
@@ -86,9 +127,17 @@ public final class DtlsEndpoint {
* @throws SSLException if engine creation fails
*/
public DtlsSession session(SocketAddress remote) throws SSLException {
+ Objects.requireNonNull(remote, "remote");
return sessions.computeIfAbsent(remote, r -> {
try {
- return new DtlsSession(createEngine(r), r, mtu);
+ SSLEngine engine = createEngine(r);
+
+ // Critical: server-side sessions must enter handshake immediately.
+ if (!clientMode) {
+ engine.beginHandshake();
+ }
+
+ return new DtlsSession(engine, r, mtu);
} catch (SSLException e) {
throw new RuntimeException(e);
}
@@ -98,9 +147,6 @@ public final class DtlsEndpoint {
/**
* Performs a DTLS handshake for a remote address.
*
- * Client: call this after creating a session and before sending app data.
- * Server: call this once you detect a new remote (first datagrams arrive) to complete handshake.
- *
* @param remote remote address
* @throws IOException on I/O error
* @throws SSLException on TLS error
@@ -109,7 +155,10 @@ public final class DtlsEndpoint {
DtlsSession s = session(remote);
if (s.isHandshakeComplete()) return;
- s.engine().beginHandshake();
+ if (clientMode) {
+ s.engine().beginHandshake();
+ }
+
SSLEngineResult.HandshakeStatus hs = s.engine().getHandshakeStatus();
ByteBuffer netIn = ByteBuffer.allocate(mtu);
@@ -126,9 +175,14 @@ public final class DtlsEndpoint {
switch (hs) {
case NEED_WRAP -> {
netOut.clear();
- SSLEngineResult r = s.engine().wrap(ByteBuffer.allocate(0), netOut);
+
+ SSLEngineResult r = s.engine().wrap(EMPTY, netOut);
hs = r.getHandshakeStatus();
+ if (r.getStatus() == SSLEngineResult.Status.CLOSED) {
+ throw new SSLException("DTLS engine closed during handshake (wrap)");
+ }
+
netOut.flip();
if (netOut.hasRemaining()) {
channel.send(netOut, remote);
@@ -138,24 +192,23 @@ public final class DtlsEndpoint {
netIn.clear();
SocketAddress from = channel.receive(netIn);
if (from == null) {
- // best effort: keep looping until timeout
continue;
}
if (!from.equals(remote)) {
- // ignore other peers here; their sessions will be handled elsewhere
continue;
}
netIn.flip();
+ app.clear();
+
SSLEngineResult r = s.engine().unwrap(netIn, app);
hs = r.getHandshakeStatus();
if (r.getStatus() == SSLEngineResult.Status.BUFFER_UNDERFLOW) {
- // wait for more datagrams
continue;
}
if (r.getStatus() == SSLEngineResult.Status.CLOSED) {
- throw new SSLException("DTLS engine closed during handshake");
+ throw new SSLException("DTLS engine closed during handshake (unwrap)");
}
}
case NEED_TASK -> {
@@ -165,7 +218,10 @@ public final class DtlsEndpoint {
}
hs = s.engine().getHandshakeStatus();
}
- case FINISHED, NOT_HANDSHAKING -> s.setHandshakeComplete(true);
+ case FINISHED, NOT_HANDSHAKING -> {
+ s.setHandshakeComplete(true);
+ enforceClientAuthIfRequired(s);
+ }
}
}
}
@@ -179,6 +235,9 @@ public final class DtlsEndpoint {
* @throws SSLException on TLS errors
*/
public void sendApplication(SocketAddress remote, ByteBuffer applicationData) throws IOException, SSLException {
+ Objects.requireNonNull(remote, "remote");
+ Objects.requireNonNull(applicationData, "applicationData");
+
DtlsSession s = session(remote);
if (!s.isHandshakeComplete()) {
handshake(remote);
@@ -202,8 +261,6 @@ public final class DtlsEndpoint {
/**
* Polls UDP, unwraps DTLS records and dispatches decrypted application data.
*
- * Run this in a dedicated thread for server and client.
- *
* @throws IOException on I/O errors
*/
public void poll() throws IOException {
@@ -224,27 +281,35 @@ public final class DtlsEndpoint {
ByteBuffer app = ByteBuffer.allocate(s.engine().getSession().getApplicationBufferSize());
try {
+ app.clear();
SSLEngineResult r = s.engine().unwrap(netIn, app);
if (r.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_TASK) {
Runnable task;
- while ((task = s.engine().getDelegatedTask()) != null) task.run();
+ while ((task = s.engine().getDelegatedTask()) != null) {
+ task.run();
+ }
}
if (r.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_WRAP) {
- // Respond to DTLS handshake flights if needed
ByteBuffer netOut = ByteBuffer.allocate(mtu);
netOut.clear();
- SSLEngineResult wr = s.engine().wrap(ByteBuffer.allocate(0), netOut);
+
+ SSLEngineResult wr = s.engine().wrap(EMPTY, netOut);
if (wr.getStatus() != SSLEngineResult.Status.CLOSED) {
netOut.flip();
- if (netOut.hasRemaining()) channel.send(netOut, from);
+ if (netOut.hasRemaining()) {
+ channel.send(netOut, from);
+ }
}
}
if (r.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.FINISHED
|| r.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) {
- s.setHandshakeComplete(true);
+ if (!s.isHandshakeComplete()) {
+ s.setHandshakeComplete(true);
+ enforceClientAuthIfRequired(s);
+ }
}
if (r.getStatus() == SSLEngineResult.Status.CLOSED) {
@@ -258,24 +323,49 @@ public final class DtlsEndpoint {
appHandler.onApplicationData(from, app);
}
} catch (SSLException ignored) {
- // best effort: invalid record / handshake mismatch -> drop
+ // Best effort: invalid record / handshake mismatch -> drop.
} finally {
netIn.clear();
}
}
}
+ private void enforceClientAuthIfRequired(DtlsSession s) throws SSLException {
+ if (clientMode) return;
+ if (clientAuthMode != ClientAuthMode.REQUIRED) return;
+
+ // If REQUIRED, ensure the peer presented a certificate.
+ try {
+ Certificate[] peer = s.engine().getSession().getPeerCertificates();
+ if (peer == null || peer.length == 0) {
+ sessions.remove(s.remote());
+ throw new SSLException("Client certificate required but not provided");
+ }
+ } catch (javax.net.ssl.SSLPeerUnverifiedException e) {
+ sessions.remove(s.remote());
+ throw new SSLException("Client certificate required but peer unverified", e);
+ }
+ }
+
private SSLEngine createEngine(SocketAddress remote) throws SSLException {
- SSLEngine engine = sslContext.createSSLEngine();
+ final SSLEngine engine;
+
+ if (remote instanceof InetSocketAddress isa) {
+ // Host string may be an IP or hostname. Both are acceptable for SSLEngine identity.
+ engine = sslContext.createSSLEngine(isa.getHostString(), isa.getPort());
+ } else {
+ engine = sslContext.createSSLEngine();
+ }
+
engine.setUseClientMode(clientMode);
SSLParameters p = engine.getSSLParameters();
- // Prefer DTLSv1.2 (widest support in JSSE DTLS). If your environment supports DTLSv1.3, you can extend this.
p.setProtocols(new String[]{"DTLSv1.2"});
- engine.setSSLParameters(p);
- // For DTLS it's recommended to set a secure random
- engine.getSSLParameters();
+ // Do NOT rely on setNeedClientAuth/setWantClientAuth for SSLEngine enforcement.
+ // Enforce REQUIRED via post-handshake peer certificate check.
+
+ engine.setSSLParameters(p);
return engine;
}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/Endpoint.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/Endpoint.java
new file mode 100644
index 0000000..f6b8213
--- /dev/null
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/Endpoint.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
+ *
+ * You are unauthorized to remove this copyright.
+ * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
+ * See LICENSE-File if exists
+ */
+
+package dev.unlegitdqrk.unlegitlibrary.network.system.utils;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a network endpoint that may be defined by hostname or IP literal.
+ *
+ * Fully supports IPv4 and IPv6 (including bracketed IPv6 literals).
+ * Resolution is deterministic and configurable via {@link IpPreference}.
+ */
+public final class Endpoint {
+
+ private final String host;
+ private final int port;
+ private final IpPreference ipPreference;
+
+ /**
+ * Creates a new endpoint.
+ *
+ * @param host hostname or IP literal
+ * @param port port (1–65535)
+ * @param ipPreference IP selection preference
+ */
+ public Endpoint(String host, int port, IpPreference ipPreference) {
+ this.host = normalizeHost(host);
+ this.port = validatePort(port);
+ this.ipPreference = Objects.requireNonNull(ipPreference, "ipPreference");
+ }
+
+ /**
+ * Creates a new endpoint with {@link IpPreference#PREFER_IPV6}.
+ *
+ * @param host hostname or IP literal
+ * @param port port
+ */
+ public Endpoint(String host, int port) {
+ this(host, port, IpPreference.PREFER_IPV6);
+ }
+
+ /**
+ * Returns the host or IP literal.
+ *
+ * @return host
+ */
+ public String host() {
+ return host;
+ }
+
+ /**
+ * Returns the port.
+ *
+ * @return port
+ */
+ public int port() {
+ return port;
+ }
+
+ /**
+ * Resolves all addresses for this endpoint.
+ *
+ * @return resolved socket addresses
+ * @throws UnknownHostException if resolution fails
+ */
+ public List resolveAll() throws UnknownHostException {
+ InetAddress[] addresses = InetAddress.getAllByName(host);
+ if (addresses.length == 0) {
+ throw new UnknownHostException("No addresses resolved for " + host);
+ }
+
+ List result = new ArrayList<>(addresses.length);
+ for (InetAddress a : addresses) {
+ result.add(new InetSocketAddress(a, port));
+ }
+
+ result.sort(Comparator.comparingInt(this::score));
+ return result;
+ }
+
+ /**
+ * Resolves the best address according to {@link IpPreference}.
+ *
+ * @return best socket address
+ * @throws UnknownHostException if resolution fails
+ */
+ public InetSocketAddress resolveBest() throws UnknownHostException {
+ return resolveAll().get(0);
+ }
+
+ private int score(InetSocketAddress addr) {
+ boolean ipv6 = addr.getAddress() instanceof java.net.Inet6Address;
+ boolean ipv4 = addr.getAddress() instanceof java.net.Inet4Address;
+
+ return switch (ipPreference) {
+ case IPV6_ONLY -> ipv6 ? 0 : 100;
+ case IPV4_ONLY -> ipv4 ? 0 : 100;
+ case PREFER_IPV6 -> ipv6 ? 0 : 10;
+ case PREFER_IPV4 -> ipv4 ? 0 : 10;
+ case ANY -> 0;
+ };
+ }
+
+ private static String normalizeHost(String host) {
+ Objects.requireNonNull(host, "host");
+ String h = host.trim();
+ if (h.isEmpty()) throw new IllegalArgumentException("host must not be empty");
+
+ // Remove IPv6 brackets if present
+ if (h.startsWith("[") && h.endsWith("]") && h.length() > 2) {
+ h = h.substring(1, h.length() - 1).trim();
+ }
+ return h;
+ }
+
+ private static int validatePort(int port) {
+ if (port <= 0 || port > 65535) {
+ throw new IllegalArgumentException("port out of range: " + port);
+ }
+ return port;
+ }
+
+ /**
+ * Controls IP family selection when resolving hostnames.
+ */
+ public enum IpPreference {
+ ANY,
+ PREFER_IPV6,
+ PREFER_IPV4,
+ IPV6_ONLY,
+ IPV4_ONLY
+ }
+}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/NetworkProtocol.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/NetworkProtocol.java
new file mode 100644
index 0000000..96accf6
--- /dev/null
+++ b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/NetworkProtocol.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
+ *
+ * You are unauthorized to remove this copyright.
+ * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
+ * See LICENSE-File if exists
+ */
+
+package dev.unlegitdqrk.unlegitlibrary.network.system.utils;
+
+import dev.unlegitdqrk.unlegitlibrary.network.system.server.ServerProtocolMode;
+
+/**
+ * Supported network protocols for packet transport.
+ *
+ * This enum is used:
+ *
+ * - by client and server to declare supported/enabled protocols
+ * - when sending packets to explicitly choose the transport
+ * - by {@link ServerProtocolMode} to define server capabilities
+ *
+ */
+public enum NetworkProtocol {
+
+ /**
+ * TCP transport secured via TLS.
+ */
+ TCP,
+
+ /**
+ * UDP transport secured via DTLS.
+ */
+ UDP
+}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/Transport.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/Transport.java
deleted file mode 100644
index e344e58..0000000
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/Transport.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
-package dev.unlegitdqrk.unlegitlibrary.network.system.utils;
-
-import java.util.EnumSet;
-
-/**
- * Supported transport mechanisms for packet sending.
- */
-public enum Transport {
- TCP,
- UDP;
-
- /**
- * Returns a default transport set for a typical hybrid server/client.
- *
- * @return TCP + UDP set
- */
- public static EnumSet both() {
- return EnumSet.of(TCP, UDP);
- }
-}
diff --git a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/TransportPolicy.java b/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/TransportPolicy.java
deleted file mode 100644
index 152bc4e..0000000
--- a/src/main/java/dev/unlegitdqrk/unlegitlibrary/network/system/utils/TransportPolicy.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2026 UnlegitDqrk - All Rights Reserved
- *
- * You are unauthorized to remove this copyright.
- * You have to give Credits to the Author in your project and link this GitHub site: https://unlegitdqrk.dev/
- * See LICENSE-File if exists
- */
-
-package dev.unlegitdqrk.unlegitlibrary.network.system.utils;
-
-import java.util.EnumSet;
-
-/**
- * Defines which transports a server supports and which are mandatory to be connected.
- */
-public record TransportPolicy(EnumSet supported, EnumSet required) {
-
- /**
- * Creates a new transport policy.
- *
- * @param supported transports supported by the server
- * @param required transports required for a connection to be considered "fully connected"
- */
- public TransportPolicy(EnumSet supported, EnumSet required) {
- this.supported = EnumSet.copyOf(supported);
- this.required = EnumSet.copyOf(required);
-
- if (!this.supported.containsAll(this.required)) {
- throw new IllegalArgumentException("Required transports must be a subset of supported transports.");
- }
- if (this.supported.isEmpty()) {
- throw new IllegalArgumentException("Supported transports cannot be empty.");
- }
- }
-
- /**
- * Convenience: TCP only.
- */
- public static TransportPolicy tcpOnly() {
- return new TransportPolicy(EnumSet.of(Transport.TCP), EnumSet.of(Transport.TCP));
- }
-
- /**
- * Convenience: UDP only.
- */
- public static TransportPolicy udpOnly() {
- return new TransportPolicy(EnumSet.of(Transport.UDP), EnumSet.of(Transport.UDP));
- }
-
- /**
- * Convenience: supports TCP+UDP and requires BOTH simultaneously (your chosen setting).
- */
- public static TransportPolicy bothRequired() {
- return new TransportPolicy(EnumSet.of(Transport.TCP, Transport.UDP), EnumSet.of(Transport.TCP, Transport.UDP));
- }
-
- /**
- * Supported transports.
- *
- * @return supported set
- */
- @Override
- public EnumSet supported() {
- return EnumSet.copyOf(supported);
- }
-
- /**
- * Required transports.
- *
- * @return required set
- */
- @Override
- public EnumSet required() {
- return EnumSet.copyOf(required);
- }
-}