Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
09630f0
Introduce QUIC C2S listener type and core lifecycle wiring
dwd Apr 4, 2026
34a9b54
Add admin console support for QUIC client listener configuration
dwd Apr 4, 2026
30430d4
Implement QUIC C2S transport bootstrap and pipeline wiring
dwd Apr 4, 2026
21fc6c6
Add regression tests for NettyConnection QUIC address resolution
dwd Apr 4, 2026
764fbf3
Fix QUIC ALPN negotiation for C2S and make ALPN configurable
dwd Apr 4, 2026
c754a2c
Use QUIC remote socket address for session IP resolution
dwd Apr 4, 2026
aabe181
Add QUIC multi-stream session handling and outbound stream routing
dwd Apr 4, 2026
1942edd
Fix QUIC multi-stream bugs: full pipeline on server-initiated streams…
dwd Apr 23, 2026
a271bf1
QUIC minor fixes: consistent concurrency, safe outbound-stream defaul…
dwd Apr 23, 2026
c2eeebf
QUIC aux streams: eliminate client stream-open requirement
dwd Apr 23, 2026
60defc0
QUIC: add transport-parameter startup logging and qlog support
dwd Apr 23, 2026
7e8c839
QUIC: log every inbound stream open and primary/aux classification
dwd Apr 24, 2026
9a43648
QUIC: log connection-level stream credits and stream-limit-changed ev…
dwd Apr 24, 2026
ff643b4
QUIC aux streams: no stream-open exchange on either side
dwd Apr 24, 2026
9295dbc
QUIC: do not advertise/negotiate XEP-0198 stream management over QUIC
dwd Apr 29, 2026
7973bca
QUIC: pin top-level non-stanza writes (deliverRawText) to stream id 0
dwd Apr 29, 2026
b21f0be
QUIC: lower default idle timeout to 600s per XEP-0467 §2 (#8)
dwd Apr 29, 2026
784506a
QUIC admin: expose max-outbound-streams, ALPN and qlog dir in admin p…
dwd Apr 29, 2026
a867247
QUIC: fix stream:error on wrong stream and spurious keepalive timeout…
dwd Apr 29, 2026
e9b612f
QUIC: disable application-level idle timeout; rely on QUIC transport …
dwd Apr 30, 2026
d9e4125
QUIC: remove app-level WriteTimeoutHandler from stream pipelines
dwd May 1, 2026
8f9a56a
QUIC: enable aux-stream sharding by default (max-outbound-streams 0 -…
dwd May 1, 2026
fe24a63
QUIC: prefer reusing unallocated client-initiated streams before open…
dwd May 1, 2026
aca997a
QUIC admin: merge QUIC settings into the Client Connections page as a…
dwd May 1, 2026
be6e9a9
QUIC admin: surface QUIC connection details on session-details page
dwd May 1, 2026
2a2d13a
Replace InsecureQuicTokenHandler with HmacQuicTokenHandler
dwd May 14, 2026
745337f
QUIC migration Phase 1+3: session registry and path-event handling
dwd May 14, 2026
7cb0b09
QUIC migration Phase 2+6: v2 DCID-bound tokens and migration-enabled …
dwd May 14, 2026
15c6d4e
Update plan-quic-migration.md with implementation status
dwd May 14, 2026
e59cccf
Expand QUIC native platform coverage to all five supported targets
dwd May 14, 2026
3fa7afb
Surface Quic.isAvailable() in admin console QUIC settings panel
dwd May 14, 2026
b60bd86
Extend QUIC support to S2S (federation)
dwd May 14, 2026
d1c4208
Add outbound QUIC S2S and admin console settings page
dwd May 14, 2026
0228f61
Add WebTransport/h3 C2S and shared-port ALPN multiplexing for QUIC
dwd May 14, 2026
c9fcd8a
Document ports in Dockerfile EXPOSE
dwd May 15, 2026
852c365
Fix QUIC CID authentication failure in HmacQuicTokenHandler
dwd May 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions i18n/src/main/resources/openfire_i18n.properties
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ tab.server.descr=Click to manage server settings
sidebar.profile-settings.descr=Click to configure user and group profile settings
sidebar.client-connections-settings=Client Connections
sidebar.client-connections-settings.descr=Click to configure client connections settings
sidebar.quic-client-connections-settings=Client Connections (QUIC)
sidebar.quic-client-connections-settings.descr=Click to configure QUIC client connections settings
sidebar.server2server-settings=Server to Server
sidebar.server2server-settings.descr=Click to configure server to server settings
sidebar.external-components-settings=External Components
Expand Down Expand Up @@ -1347,6 +1349,9 @@ system_property.xmpp.ratelimit.newconnections.logging.suppress=The minimum time
system_property.xmpp.ratelimit.newconnections.s2s.enabled=Enables or disables rate limiting for new server-to-server (S2S) connections.
system_property.xmpp.ratelimit.newconnections.s2s.max_burst=The maximum number of new server-to-server connection attempts that can be accepted in a short burst. Allows temporary bursts without violating the sustained rate.
system_property.xmpp.ratelimit.newconnections.s2s.permits_per_second=The sustained rate of new server-to-server connection attempts allowed per second. Applies to all S2S (federation) connection types, which currently is just TCP.
system_property.xmpp.quic.client.idle=How long, in seconds, before idle QUIC client sessions are dropped. Set to -1 to never drop idle sessions.
system_property.xmpp.quic.client.max-streams=Maximum number of QUIC streams that can be associated with a client session.
system_property.xmpp.quic.client.alpn=Comma-separated list of ALPN values accepted by the QUIC C2S listener. The default and recommended value is xmpp-client.
system_property.plugins.upload.enabled=Defines if the admin console can be used to upload plugins.
system_property.plugins.upload.content-type-check.enabled=Determines if the content-type of uploaded plugin files is verified.
system_property.plugins.upload.content-type-check.expected-value=Defines the expected content-type of uploaded plugin files.
Expand Down Expand Up @@ -2482,6 +2487,8 @@ ssl.certificates.store-management.combined-stores.title=Certificate Stores
ssl.certificates.store-management.combined-stores.info=These stores are used for all encrypted communication. Three stores are provided\: one identity store, a trust store for server-based connections, and a trust store for client-based connections.
ssl.certificates.store-management.socket-c2s-stores.title=XMPP Client Stores
ssl.certificates.store-management.socket-c2s-stores.info=These stores are used for regular, TCP-based client-to-server XMPP communication. Two stores are provided\: one identity store and a trust store. Openfire ships with an empty trust store, as in typical environments, certificate-based authentication of clients is not required.
ssl.certificates.store-management.quic-c2s-stores.title=XMPP Client Stores (QUIC)
ssl.certificates.store-management.quic-c2s-stores.info=These stores are used for QUIC-based client-to-server XMPP communication. Two stores are provided\: one identity store and a trust store.
ssl.certificates.store-management.socket-s2s-stores.title=Server Federation Stores
ssl.certificates.store-management.socket-s2s-stores.info=These stores are used for server-to-server XMPP communication, which establishes server federation. Two stores are provided\: one identity store and a trust store. Openfire ships with a trust store filled with certificates of generally accepted certificate authorities.
ssl.certificates.store-management.bosh-c2s-stores.title=Web binding (websocket and BOSH) Stores
Expand Down Expand Up @@ -2543,6 +2550,7 @@ ssl.certificates.truststore.ca-reply=Certificate Authority Reply:

# Truststore Page
ssl.certificates.truststore.c2s-title=Client Truststore
ssl.certificates.truststore.quic-c2s-title=Client QUIC Truststore
ssl.certificates.truststore.s2s-title=Server Truststore
ssl.certificates.truststore.title=Openfire Trust Certificate Store
ssl.certificates.truststore.info=Certificates in this list are used by Openfire to verify the identity of remote clients and servers when encrypted connections are being established. By default, Openfire ships with a number of certificates from commonly trusted Certificate Authorities.
Expand Down Expand Up @@ -3473,6 +3481,8 @@ ports.directtls.desc=Connections established on this port are established using
ports.client_to_server=Client to Server
ports.client_to_server.desc=The standard port for clients to connect to the server.
ports.client_to_server.desc_old_ssl=The port used for clients to connect to the server using the Direct TLS method.
ports.client_to_server_quic=Client to Server (QUIC)
ports.client_to_server_quic.desc=The QUIC port used for clients to connect to the server.
ports.server_to_server=Server to Server
ports.server_to_server.desc=The port used for remote servers to connect to this server.
ports.connection_manager=Connection Manager
Expand Down Expand Up @@ -3654,9 +3664,22 @@ client.connections.settings.ratelimit.max_burst=Maximum burst
client.connections.settings.ratelimit.permits.invalid=Permits per second must be a positive whole number.
client.connections.settings.ratelimit.max_burst.invalid=Maximum burst must be a positive whole number.

# QUIC Client Connections Settings page
quic.client.connections.settings.title=Client Connections Settings (QUIC)
quic.client.connections.settings.confirm.updated=QUIC client connection settings have been updated successfully.
quic.client.connections.settings.info=Use the form below to configure how XMPP clients connect to this server over QUIC.
quic.client.connections.settings.boxtitle=QUIC Client Listener
quic.client.connections.settings.label_enable=Enable QUIC client listener
quic.client.connections.settings.label_idle=Idle timeout
quic.client.connections.settings.label_maxstreams=Maximum streams per session
quic.client.connections.settings.valid.port=Port must be between 1 and 65535.
quic.client.connections.settings.valid.idle=Idle timeout must be -1 or a positive number of seconds.
quic.client.connections.settings.valid.maxstreams=Maximum streams must be a positive whole number.

# Connection type and mode
connection-type.socket-s2s=server-to-server (federation)
connection-type.socket-c2s=client-to-server
connection-type.quic-c2s=client-to-server (QUIC)
connection-type.bosh-c2s=Web binding (websocket & BOSH)
connection-type.webadmin=admin console
connection-type.component=external component
Expand Down
12 changes: 12 additions & 0 deletions xmppserver/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,18 @@
<artifactId>netty-all</artifactId>
<version>${netty.version}</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec-classes-quic</artifactId>
<version>${netty.version}</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec-native-quic</artifactId>
<version>${netty.version}</version>
<classifier>linux-x86_64</classifier>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only works on (certain flavors of) Linux? Can this be made working on Windows? Alternatively, do things crash and burn when loaded on Windows? Should we have some kind of detection mechanism that gracefully informs an administrator if and why QUIC support is (un)available?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, apparently... The underlying library (Quiche) should work fine on Windows etc though. I'll look into getting it built on as many platforms as possible.

<scope>runtime</scope>
</dependency>

<!-- JZLib -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ public interface ConnectionManager {
* they are created.
*/
int DEFAULT_SSL_PORT = 5223;

/**
* The default XMPP over QUIC port for clients.
*/
int DEFAULT_QUIC_PORT = 5224;
/**
* The default XMPP port for external components.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.Channel;
import io.netty.handler.codec.quic.QuicChannel;
import io.netty.handler.codec.compression.JZlibDecoder;
import io.netty.handler.codec.compression.JZlibEncoder;
import io.netty.handler.ssl.SslContext;
Expand Down Expand Up @@ -104,31 +106,52 @@ public SocketAddress getPeer() {

@Override
public byte[] getAddress() throws UnknownHostException {
final SocketAddress remoteAddress = channelHandlerContext.channel().remoteAddress();
if (remoteAddress == null) throw new UnknownHostException();
final InetSocketAddress socketAddress = (InetSocketAddress) remoteAddress;
final InetSocketAddress socketAddress = resolvePeerSocketAddress();
final InetAddress address = socketAddress.getAddress();
if (address == null) {
throw new UnknownHostException();
}
return address.getAddress();
}

@Override
public String getHostAddress() throws UnknownHostException {
final SocketAddress remoteAddress = channelHandlerContext.channel().remoteAddress();
if (remoteAddress == null) throw new UnknownHostException();
final InetSocketAddress socketAddress = (InetSocketAddress) remoteAddress;
final InetSocketAddress socketAddress = resolvePeerSocketAddress();
final InetAddress inetAddress = socketAddress.getAddress();
if (inetAddress == null) {
throw new UnknownHostException();
}
return inetAddress.getHostAddress();
}

@Override
public String getHostName() throws UnknownHostException {
final SocketAddress remoteAddress = channelHandlerContext.channel().remoteAddress();
if (remoteAddress == null) throw new UnknownHostException();
final InetSocketAddress socketAddress = (InetSocketAddress) remoteAddress;
final InetSocketAddress socketAddress = resolvePeerSocketAddress();
final InetAddress inetAddress = socketAddress.getAddress();
if (inetAddress == null) {
throw new UnknownHostException();
}
return inetAddress.getHostName();
}

private InetSocketAddress resolvePeerSocketAddress() throws UnknownHostException {
for (Channel channel = channelHandlerContext.channel(); channel != null; channel = channel.parent()) {
if (channel instanceof QuicChannel quicChannel) {
final SocketAddress remoteSocketAddress = quicChannel.remoteSocketAddress();
if (remoteSocketAddress instanceof InetSocketAddress inetSocketAddress) {
return inetSocketAddress;
}
}

final SocketAddress socketAddress = channel.remoteAddress();
if (socketAddress instanceof InetSocketAddress inetSocketAddress) {
return inetSocketAddress;
}
}

throw new UnknownHostException();
}

@Override
public Certificate[] getLocalCertificates() {
SslHandler sslhandler = (SslHandler) channelHandlerContext.channel().pipeline().get(SSL_HANDLER_NAME);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public static NettyConnectionHandler createConnectionHandler(ConnectionConfigura
return switch (configuration.getType()) {
case SOCKET_S2S -> new NettyServerConnectionHandler(configuration);
case SOCKET_C2S -> new NettyClientConnectionHandler(configuration);
case QUIC_C2S -> new QuicClientConnectionHandler(configuration);
case COMPONENT -> new NettyComponentConnectionHandler(configuration);
case CONNECTION_MANAGER -> new NettyMultiplexerConnectionHandler(configuration);
default ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright (C) 2026 Ignite Realtime Foundation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jivesoftware.openfire.nio;

import io.netty.channel.ChannelHandlerContext;
import org.jivesoftware.openfire.spi.ConnectionConfiguration;

/**
* C2S business-logic handler for QUIC channels.
*/
public class QuicClientConnectionHandler extends NettyClientConnectionHandler
{
public QuicClientConnectionHandler(final ConnectionConfiguration configuration)
{
super(configuration);
}

@Override
NettyConnection createNettyConnection(final ChannelHandlerContext ctx)
{
final NettyConnection connection = super.createNettyConnection(ctx);
connection.setEncrypted(true); // QUIC always runs on top of TLS.
return connection;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ public static TokenBucketRateLimiter getLimiter(final ConnectionType type)
throw new IllegalArgumentException("ConnectionType cannot be null");
}

if (type == ConnectionType.SOCKET_C2S || type == ConnectionType.BOSH_C2S) {
if (type == ConnectionType.SOCKET_C2S || type == ConnectionType.QUIC_C2S || type == ConnectionType.BOSH_C2S) {
return C2S_LIMITER_REF.get();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.List;

public final class ConnectionSettings {

Expand Down Expand Up @@ -85,6 +86,32 @@ public static final class Client {
public static final String MAX_THREADS_SSL = "xmpp.client_ssl.processing.threads";
public static final String MAX_READ_BUFFER_SSL = "xmpp.client_ssl.maxReadBufferSize";
public static final String TLS_ALGORITHM = "xmpp.socket.ssl.algorithm";

public static final String QUIC_SOCKET_ACTIVE = "xmpp.quic.client.active";
public static final String QUIC_PORT = "xmpp.quic.client.port";
public static final String QUIC_MAX_THREADS = "xmpp.quic.client.processing.threads";
public static final String QUIC_MAX_READ_BUFFER = "xmpp.quic.client.maxReadBufferSize";
public static final String QUIC_AUTH_PER_CLIENTCERT_POLICY = "xmpp.quic.client.cert.policy";
public static final SystemProperty<Integer> QUIC_MAX_STREAMS = SystemProperty.Builder.ofType(Integer.class)
.setKey("xmpp.quic.client.max-streams")
.setDefaultValue(1)
.setMinValue(1)
.setDynamic(Boolean.TRUE)
.build();

public static final SystemProperty<Duration> QUIC_IDLE_TIMEOUT_PROPERTY = SystemProperty.Builder.ofType(Duration.class)
.setKey("xmpp.quic.client.idle")
.setDefaultValue(Duration.ofMinutes(6))
.setMinValue(Duration.ofMillis(-1))
.setChronoUnit(ChronoUnit.MILLIS)
.setDynamic(Boolean.TRUE)
.build();

public static final SystemProperty<List<String>> QUIC_ALPN = SystemProperty.Builder.ofType(List.class)
.setKey("xmpp.quic.client.alpn")
.setDefaultValue(List.of("xmpp-client"))
.setDynamic(Boolean.TRUE)
.buildList(String.class);
}

public static final class Server {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public class ConnectionListener
private final String clientAuthPolicyPropertyName;

// The entity that performs the acceptance of new (socket) connections.
private NettyConnectionAcceptor connectionAcceptor;
private ConnectionAcceptor connectionAcceptor;

/**
* A collection of socket acceptor event listeners, invoked when they are being stopped or started.
Expand Down Expand Up @@ -195,15 +195,10 @@ public synchronized void enable( boolean enable )
*/
public synchronized void start()
{
// TODO Start all connection types here, by supplying more connection acceptors other than a Netty-based one.
switch ( getType() )
if (getType() == ConnectionType.BOSH_C2S || getType() == ConnectionType.WEBADMIN)
{
case BOSH_C2S:
case WEBADMIN:
Log.debug( "Not starting a (Netty-based) connection acceptor, as connections of type " + getType() + " depend on another IO technology.");
return;

default:
Log.debug( "Not starting a connection acceptor, as connections of type {} depend on another IO technology.", getType());
return;
}

if ( !isEnabled() )
Expand Down Expand Up @@ -235,7 +230,7 @@ public synchronized void start()
}

Log.debug( "Starting..." );
connectionAcceptor = new NettyConnectionAcceptor(generateConnectionConfiguration());
connectionAcceptor = instantiateConnectionAcceptor(generateConnectionConfiguration());
eventListeners.forEach(eventListener -> {
try {
eventListener.acceptorStarting(connectionAcceptor);
Expand All @@ -247,6 +242,14 @@ public synchronized void start()
Log.info( "Started." );
}

private ConnectionAcceptor instantiateConnectionAcceptor(final ConnectionConfiguration configuration)
{
if (getType() == ConnectionType.QUIC_C2S) {
return new QuicConnectionAcceptor(configuration);
}
return new NettyConnectionAcceptor(configuration);
}

/**
* Generates an immutable ConnectionConfiguration based on the current state.
*
Expand Down Expand Up @@ -1146,4 +1149,3 @@ public interface SocketAcceptorEventListener
void acceptorStopping(final ConnectionAcceptor connectionAcceptor);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public class ConnectionManagerImpl extends BasicModule implements ConnectionMana

private final ConnectionListener clientListener;
private final ConnectionListener clientSslListener;
private final ConnectionListener quicClientListener;
private final ConnectionListener boshListener;
private final ConnectionListener boshSslListener;
private final ConnectionListener serverListener;
Expand Down Expand Up @@ -124,6 +125,20 @@ public ConnectionManagerImpl() throws IOException
certificateStoreManager.getTrustStoreConfiguration( ConnectionType.SOCKET_C2S ),
ConnectionSettings.Client.COMPRESSION_SETTINGS
);
quicClientListener = new ConnectionListener(
ConnectionType.QUIC_C2S,
ConnectionSettings.Client.QUIC_PORT,
DEFAULT_QUIC_PORT,
ConnectionSettings.Client.QUIC_SOCKET_ACTIVE,
ConnectionSettings.Client.QUIC_MAX_THREADS,
ConnectionSettings.Client.QUIC_MAX_READ_BUFFER,
Connection.TLSPolicy.directTLS.name(),
ConnectionSettings.Client.QUIC_AUTH_PER_CLIENTCERT_POLICY,
bindAddress,
certificateStoreManager.getIdentityStoreConfiguration( ConnectionType.QUIC_C2S ),
certificateStoreManager.getTrustStoreConfiguration( ConnectionType.QUIC_C2S ),
null
);
// BOSH / HTTP-bind
boshListener = new ConnectionListener(
ConnectionType.BOSH_C2S,
Expand Down Expand Up @@ -405,6 +420,7 @@ public Set<ConnectionListener> getListeners() {
final Set<ConnectionListener> listeners = new LinkedHashSet<>();
listeners.add( clientListener );
listeners.add( clientSslListener );
listeners.add( quicClientListener );
listeners.add( boshListener );
listeners.add( boshSslListener );
listeners.add( serverListener );
Expand Down Expand Up @@ -441,6 +457,8 @@ public ConnectionListener getListener( ConnectionType type, boolean startInDirec
} else {
return clientListener;
}
case QUIC_C2S:
return quicClientListener;

case BOSH_C2S:
if (startInDirectTlsMode) {
Expand Down Expand Up @@ -497,6 +515,10 @@ public Set<ConnectionListener> getListeners( ConnectionType type )
result.add( clientSslListener );
break;

case QUIC_C2S:
result.add( quicClientListener );
break;

case BOSH_C2S:
result.add( boshListener );
result.add( boshSslListener );
Expand Down
Loading
Loading