Drop-in TLS- and DTLS-secured implementations of the JDK NIO channel API – SocketChannel, ServerSocketChannel, DatagramChannel, and Selector wrappers powered by SSLEngine, with zero dependencies.
Secure NIO lets existing NIO code speak TLS with minimal changes:
channels are decorated rather than replaced, all SSLEngine mechanics (handshaking, encryption, decryption, delegated tasks) are handled internally,
and both blocking and non-blocking selector-driven I/O are supported.
The library has no runtime dependencies.
| Class | Description |
|---|---|
SecureSocketChannel |
A SocketChannel decorator that transparently encrypts writes and decrypts reads. Non-I/O operations are delegated to the underlying raw channel. |
SecureServerSocketChannel |
A ServerSocketChannel decorator that produces SecureSocketChannel instances (with server-mode SSLEngines from a supplied SSLContext) for each accepted connection. |
SecureDatagramChannel |
A DatagramChannel decorator that transparently encrypts and decrypts datagrams with DTLS. Unicast, connected mode only – one DTLS session per peer pair; multicast is not supported. |
DelegatingSelectorProvider |
A SelectorProvider that opens Selectors capable of registering the TLS/DTLS-wrapped channels above, transparently unwrapping them to the raw channel on registration. |
Add the dependency to your pom.xml:
<dependency>
<groupId>io.github.rhusar.securenio</groupId>
<artifactId>secure-nio</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>SSLContext sslContext = ...; // initialized with your key material
ExecutorService taskExecutor = Executors.newCachedThreadPool(); // runs SSLEngine delegated tasks
ServerSocketChannel rawServer = ServerSocketChannel.open();
SecureServerSocketChannel server = new SecureServerSocketChannel(rawServer, sslContext, taskExecutor);
server.bind(new InetSocketAddress(8443));
SocketChannel channel = server.accept(); // returns a SecureSocketChannelSocketChannel rawChannel = SocketChannel.open();
SSLEngine engine = sslContext.createSSLEngine();
engine.setUseClientMode(true);
SecureSocketChannel channel = new SecureSocketChannel(rawChannel, engine, taskExecutor);
channel.connect(new InetSocketAddress("example.com", 8443));Once connected, use read(ByteBuffer) and write(ByteBuffer) as with any SocketChannel – the TLS handshake is driven transparently as a side effect of reading and writing.
Secure channels cannot be registered directly with a system selector (because the JDK's SelectorImpl requires its own internal channel types).
Use DelegatingSelectorProvider, which unwraps the secure channel on registration:
DelegatingSelectorProvider provider = new DelegatingSelectorProvider();
Selector selector = provider.openSelector();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);Alternatively, register the raw channel with a regular selector and attach the secure channel to the key:
SelectionKey key = channel.delegate().register(selector, SelectionKey.OP_READ);
key.attach(channel);See TLSLoopbackTestCase for a complete end-to-end example:
a non-blocking client and server exchanging data over TLS.
SecureDatagramChannel secures unicast UDP with DTLS 1.2 using the JDK's DTLS SSLEngine.
The channel operates in connected mode only:
the DTLS session is bound to a single peer, so the underlying channel must be connected before any secure I/O.
SSLContext sslContext = SSLContext.getInstance("DTLS");
sslContext.init(keyManagers, trustManagers, null);
DatagramChannel rawChannel = DatagramChannel.open();
rawChannel.bind(new InetSocketAddress(0));
rawChannel.connect(peerAddress);
SSLEngine engine = sslContext.createSSLEngine();
engine.setUseClientMode(true); // or false on the accepting side
SecureDatagramChannel channel = new SecureDatagramChannel(rawChannel, engine, taskExecutor);Datagram semantics are preserved:
- each
write(ByteBuffer)produces one DTLS-protected datagram, - each
read(ByteBuffer)delivers the plaintext of at most one received datagram, - and plaintext exceeding the caller's buffer is discarded just as with a plain
DatagramChannel. The handshake is driven transparently as a side effect of reading and writing; - while it is in progress, writes consume nothing and simply advance the handshake.
Two DTLS-specific caveats:
- Retransmission – UDP loses packets and the JDK's DTLS engine has no timer,
so a lost handshake flight must be re-sent by the application:
when no progress is observed within a select timeout while
isHandshaking()returnstrue, callretransmit()to re-send the last flight. Theclose_notifyalert sent onclose()is likewise a single datagram delivered best-effort. - Path MTU: To avoid IP fragmentation, constrain the datagram size via
SSLParameters.setMaximumPacketSize(...)(for example1432for Ethernet-sized MTUs) on the engine before constructing the channel.
See DTLSLoopbackTestCase for a complete end-to-end example, including handshake recovery from packet loss.
SecureDatagramChannel is unicast-only, by design and permanently.
A DTLS session always secures a conversation between exactly two endpoints:
the handshake verifies who the other side is and derives keys that only those two hold.
There is no meaningful way to map that onto a multicast group, where an arbitrary and changing set of receivers must decrypt the same datagram –
that requires a group key management protocol (see RFC 3740 and RFC 5374), which is out of scope for this library and cannot be expressed through the JDK's SSLEngine.
Thus:
join(InetAddress, NetworkInterface)andjoin(InetAddress, NetworkInterface, InetAddress)always throwUnsupportedOperationException,disconnect()likewise throws – close the channel instead,- the
IP_MULTICAST_*socket options are delegated to the raw channel and remain settable, but have no effect – no group can ever be joined.
If you need to protect multicast traffic, secure the payload at the application layer – for example with a pre-shared group key – rather than at the transport layer.
mvn clean verifyRequires Maven (or use Maven Wrapper scripting) and JDK 17 or higher. The build generates a self-signed test keystore automatically for testing, so no manual setup is required.
Continuous integration runs on GitHub Actions.
JaCoCo coverage is enabled automatically on JDK 25 (the latest LTS) and writes an HTML report to impl/target/site/jacoco/index.html.
To disable it explicitly on JDK 25:
# either deactivate the profile
mvn clean verify -P '!jacoco'
# or leave the profile active and skip the plugin
mvn clean verify -Djacoco.skip=trueCoverage reports for main are published alongside the Javadoc at
rhusar.github.io/secure-nio/coverage/main.
Contributions are welcome – see CONTRIBUTING.md for build, style, and sign-off requirements.
Please report bugs and feature requests via GitHub Issues.