Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a017643
OF-2643: Share one rule for the domains that S2S peers may address
viv Oct 26, 2026
a8adb0d
OF-2643: Extract the refusal of a stream into StanzaHandler.refuseStream
viv Oct 26, 2026
a78fd24
OF-2643: Optionally refuse S2S streams that address an unserviced domain
viv Oct 26, 2026
b3a99b0
OF-2643: Validate the S2S stream header that follows STARTTLS
viv Oct 26, 2026
268c4c5
OF-2643: Refuse a stream for an unknown host from our own domain
viv Oct 26, 2026
37cddae
OF-2643: Refuse from our own domain whenever the host is not ours
viv Oct 27, 2026
fbe6fef
OF-2643: Ignore an invalid trunked domain instead of failing
viv Oct 27, 2026
db92e3a
OF-2643: Clear test properties after the new test classes
viv Oct 27, 2026
6969239
OF-2643: Normalise the trunked domains once per configuration change
viv Oct 27, 2026
5b16182
OF-2643: Continue dialback with the normalised form of the domain
viv Oct 27, 2026
434c991
OF-2643: Describe host validation in the trunking guide
viv Oct 27, 2026
573c726
OF-2643: Refer to the local XMPP service rather than to this server
guusdk Oct 28, 2026
7452993
OF-2643: Describe the reason for every assertion in the new tests
guusdk Oct 28, 2026
410c251
OF-2643: Add the Dutch description of xmpp.server.validate.host
guusdk Oct 28, 2026
a3f15af
OF-2643: Define xmpp.client.validate.host as a SystemProperty
guusdk Oct 28, 2026
d8196c3
OF-2643: Allow the empty string when refusing a stream for an unknown…
guusdk Oct 28, 2026
e9ff7b1
OF-2643: Reference the label length limit that makes the test domain …
guusdk Oct 28, 2026
e1ace8e
OF-2643: Give the current reason for accepting a stream header withou…
guusdk Oct 28, 2026
9ddb549
OF-2643: Verify that a stream header without 'to' after STARTTLS is a…
guusdk Oct 28, 2026
ebf3073
OF-2643: Return the normalised domain from ServerDialback#validateRem…
guusdk Oct 28, 2026
510a300
OF-2643: Restore the namespace-refusal tests from OF-3425
viv Oct 29, 2026
d94c989
OF-2643: Test the normalised-domain callers and the client host-valid…
viv Oct 30, 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
4 changes: 4 additions & 0 deletions documentation/trunking-guide.html
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ <h2>Setup</h2>
To enable the trunking functionality in Openfire, the property <code>xmpp.gateway.enabled</code> needs to be set to <code>true</code>, and the property <code>xmpp.gateway.domains</code> needs to be populated with a (comma-separated) list of XMPP domain names of remote XMPP domains that the Openfire server will forward data to.
</p>

<p>
When the property <code>xmpp.server.validate.host</code> is enabled, Openfire refuses inbound server-to-server connections that address a domain it does not service. The domains listed in <code>xmpp.gateway.domains</code> count as serviced, so connections to them are still accepted while trunking is enabled. Subdomains of those domains do not count: list each domain that is to be trunked.
</p>

<section id="setup-example">

<h3>Full example</h3>
Expand Down
2 changes: 2 additions & 0 deletions i18n/src/main/resources/openfire_i18n.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1342,10 +1342,12 @@ system_property.xmpp.client.idle.ping=Set to true to ping idle clients, otherwis
system_property.xmpp.client.limits.advertisement.disabled=Disables the advertisement of server limits in the XMPP stream features (XEP-0478) on client connections.
system_property.xmpp.client.version-query.enabled=Send a version request query to clients when they connect.
system_property.xmpp.client.version-query.delay=After this amount of time has passed since a new client connection has been accepted, a version request is being sent to the peer.
system_property.xmpp.client.validate.host=When enabled, a client stream that addresses a domain other than the XMPP domain is refused with a host-unknown stream error. A stream that does not address a domain is accepted.
system_property.xmpp.gateway.enabled=Controls if Openfire's 'trunking' or 'gateway' functionality is enabled. This feature allows Openfire to act as a gateway to transfer data between other XMPP domains.
system_property.xmpp.gateway.domains=A collection of XMPP domain names for which Openfire will provide trunking functionality. For each domain in this collection, Openfire will accept data from other domains, and forward that data to that domain.
system_property.xmpp.server.rewrite.replace-missing-to=If the server receives a message or IQ stanza with no 'to' attribute, set the 'to' attribute to the bare JID representation of the 'from' attribute value.
system_property.xmpp.server.incoming.skip-jid-validation=Controls if JIDs that are in the addresses of stanzas supplied by remote domains are validated.
system_property.xmpp.server.validate.host=When enabled, an inbound server-to-server stream that addresses a domain that is not serviced by the local XMPP service is refused with a host-unknown stream error. Serviced domains are the XMPP domain, its subdomains that have a component, and domains configured for trunking. A stream that does not address a domain is accepted.
system_property.xmpp.server.outgoing.max.threads=Maximum amount of threads in the thread pool that is used to establish outbound server-to-server connections. Surplus connections will be created on the calling thread, possibly / gracefully slowing down other operations considerably.
system_property.xmpp.server.outgoing.min.threads=Minimum amount of threads in the thread pool that is used to establish outbound server-to-server connections
system_property.xmpp.server.outgoing.threads-timeout=Amount of time after which idle, surplus threads are removed from the thread pool that is used to establish outbound server-to-server connections.
Expand Down
2 changes: 2 additions & 0 deletions i18n/src/main/resources/openfire_i18n_nl.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1240,10 +1240,12 @@ system_property.xmpp.client.idle.ping=Zet op 'true' om inactieve sessies te ping
system_property.xmpp.client.limits.advertisement.disabled=Schakelt het adverteren van server-limieten in XMPP stream features (XEP-0478) uit voor client-connecties.
system_property.xmpp.client.version-query.enabled=Probeer de versie van een client op te vragen, zodra er een nieuwe verbinding wordt gemaakt.
system_property.xmpp.client.version-query.delay=De hoeveelheid tijd die verstreken moet zijn nadat een nieuwe client-connectie is opgezet, voordat de client wordt gevraagd naar haar software versie.
system_property.xmpp.client.validate.host=Indien ingeschakeld, wordt een client-stream die een ander domein dan het XMPP-domein adresseert, geweigerd met een host-unknown stream error. Een stream die geen domein adresseert, wordt geaccepteerd.
system_property.xmpp.gateway.enabled=Bepaald of Openfire's 'trunking' of 'gateway' functionaliteit beschikbaar is. Deze functionaliteit stelt Openfire in staat om als gateway te dienen die data tussen andere XMPP domeinen kan verplaatsen.
system_property.xmpp.gateway.domains=Een collectie XMPP domeinnamen waar Openfire trunkingfunctionaliteit voor zal bieden. Openfire zal voor elk domein in deze collectie data accepteren van andere domeinen, om die data vervolgens door te sturen naar dat domein.
system_property.xmpp.server.rewrite.replace-missing-to=Als de server een IQ of message stanza ontvangt zonder 'to' attribuut, zet dan de waarde van het 'to' attribuut op de bare JID representatie van het 'from' attribuut.
system_property.xmpp.server.incoming.skip-jid-validation=Definieert of JIDs die de addressen zijn van stanzas die door externe domeinen worden aangeleverd, worden gevalideerd.
system_property.xmpp.server.validate.host=Indien ingeschakeld, wordt een inkomende server-naar-server stream die een domein adresseert dat niet door de lokale XMPP-dienst wordt bediend, geweigerd met een host-unknown stream error. Bediende domeinen zijn het XMPP-domein, de subdomeinen daarvan die een component hebben, en domeinen die voor trunking zijn geconfigureerd. Een stream die geen domein adresseert, wordt geaccepteerd.
system_property.xmpp.server.outgoing.max.threads=Maximum aantal threads in de threadpool die gebruikt wordt om server-naar-server verbindingen op te zetten. Als meer connecties tegelijkertijd worden aangevraagd, dan zal de aanvragende thread de connectie opzetten. Dit kan een vertragend effect hebben.
system_property.xmpp.server.outgoing.min.threads=Minimum aantal threads in de threadpool die gebruikt wordt om server-naar-server verbindingen op te zetten.
system_property.xmpp.server.outgoing.threads-timeout=Tijd waarna inactieve en overbodige threads worden verwijderd van de threadpool die gebruikt wordt om server-naar-server verbindingen op te zetten.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.csi.CsiManager;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.SystemProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmlpull.v1.XmlPullParser;
Expand All @@ -47,6 +47,18 @@ public class ClientStanzaHandler extends StanzaHandler {

private static final Logger Log = LoggerFactory.getLogger(ClientStanzaHandler.class);

/**
* Controls if the 'to' attribute of stream headers sent by clients is validated. When enabled, a stream that names
* a domain other than the XMPP domain of the local XMPP service is refused with a host-unknown stream error.
*
* @see #isHostUnknown(String)
*/
public static final SystemProperty<Boolean> VALIDATE_HOST = SystemProperty.Builder.ofType(Boolean.class)
.setKey("xmpp.client.validate.host")
.setDefaultValue(false)
.setDynamic(true)
.build();

public ClientStanzaHandler(PacketRouter router, Connection connection) {
super(router, connection);
}
Expand All @@ -68,7 +80,7 @@ protected Namespace getNamespace() {

@Override
protected boolean validateHost() {
return JiveGlobals.getBooleanProperty("xmpp.client.validate.host",false);
return VALIDATE_HOST.getValue();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.xmlpull.v1.XmlPullParserException;
import org.xmpp.packet.*;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

Expand Down Expand Up @@ -71,6 +72,47 @@ public class ServerStanzaHandler extends StanzaHandler {
.setDynamic(true)
.build();

/**
* Controls if the 'to' attribute of stream headers sent by remote servers is validated. When enabled, a stream that
* names a domain that is not serviced by the local XMPP service (the service that this Openfire instance provides,
* alone or together with the other nodes of its cluster) is refused with a host-unknown stream error.
*
* @see #isLocallyServiced(String)
*/
public static final SystemProperty<Boolean> VALIDATE_HOST = SystemProperty.Builder.ofType(Boolean.class)
.setKey("xmpp.server.validate.host")
.setDefaultValue(false)
.setDynamic(true)
.build();

/**
* Checks if a domain that a remote server addresses is serviced by the local XMPP service. That is the case for the
* XMPP domain of the service, for subdomains of it that have a component route, and for domains for which trunking
* is enabled.
*
* @param domain the domain to check.
* @return true if the domain is serviced by the local XMPP service, otherwise false (including when the value is not a valid domain name).
*/
public static boolean isLocallyServiced(@Nonnull final String domain) {
final String requested;
try {
requested = JID.domainprep(domain);
} catch (final IllegalArgumentException e) {
Log.debug("Value '{}' is not a valid domain name.", domain, e);
return false;
}

final XMPPServer server = XMPPServer.getInstance();
final String xmppDomain = server.getServerInfo().getXMPPDomain();
if (requested.equals(xmppDomain)) {
return true;
}
if (requested.endsWith("." + xmppDomain) && server.getRoutingTable().hasComponentRoute(new JID(requested))) {
return true;
}
return Trunking.isTrunkingEnabledFor(requested);
}

private final boolean directTLS;

public ServerStanzaHandler(PacketRouter router, Connection connection, boolean directTLS) {
Expand Down Expand Up @@ -103,8 +145,14 @@ Namespace getNamespace() {

@Override
boolean validateHost() {
// Hosts are not currently validated for S2S
return false;
return VALIDATE_HOST.getValue();
}

@Override
protected boolean isHostUnknown(String host) {
// OF-2643: Accept a missing 'to', as RFC 6120 section 4.7.2 asks to be liberal towards peers that omit it. The
// stream is then treated as addressing the XMPP domain (see LocalIncomingServerSession#createSession).
return host != null && !isLocallyServiced(host);
}

@Override
Expand Down Expand Up @@ -175,6 +223,16 @@ protected void tlsNegotiated(XmlPullParser xpp) throws XmlPullParserException, I
for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG;) {
eventType = xpp.next();
}

// The stream header that was sent before TLS was negotiated has been validated already, but its values are to
// be discarded. Validate the 'to' attribute of the stream header that was sent after TLS was negotiated.
final String host = xpp.getAttributeValue("", "to");
if (validateHost() && isHostUnknown(host)) {
Log.warn("Refusing stream that was opened after TLS was negotiated, as it addresses a host that is not serviced: '{}'. Closing connection: {}", host, connection);
refuseStream(xpp, host, new StreamError(StreamError.Condition.host_unknown, "Incorrect hostname in stream header: " + host));
return;
}

this.domainPair = new DomainPair(XMPPServer.getInstance().getServerInfo().getXMPPDomain(), xpp.getAttributeValue("", "from"));

// Discard session data obtained from unencrypted initiating stream
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -933,24 +933,44 @@ protected void createSession(XmlPullParser xpp) throws XmlPullParserException, I
catch (final StreamErrorException ex) {
Log.warn("Failed to create a session, as the stream opened by the peer has a problem: {} - '{}' (a full stack trace is logged on debug level). Closing connection: {}. session is now: {}", ex.getStreamError().getCondition(), ex.getStreamError().getText(), connection, session);
Log.debug("Failed to create a session.", ex);
final Element stream = DocumentHelper.createElement(QName.get("stream", "stream", "http://etherx.jabber.org/streams"));
final Document document = DocumentHelper.createDocument(stream);
document.setXMLEncoding(StandardCharsets.UTF_8.toString());
// The refused header need not declare a content namespace. When it does not, the error is answered in the
// namespace of this handler, rather than copying the absent namespace (which would fail to build the error).
final String contentNamespace = xpp.getNamespace(null);
stream.add(Namespace.get(contentNamespace != null ? contentNamespace : getNamespace().getURI()));
stream.addAttribute("from", host);
stream.addAttribute("id", STREAM_ID_FACTORY.createStreamID().getID());
stream.addAttribute("version", "1.0");
stream.add(ex.getStreamError().getElement());
refuseStream(xpp, host, ex.getStreamError());
}
}

// Deliver stanza
connection.deliverRawText(StringUtils.asUnclosedStream(document));
/**
* Responds to a stream header that cannot be accepted by opening a stream that carries the provided stream error,
* and then closes the underlying connection.
*
* The stream is opened from the host that the peer addressed, unless that host is missing or is not serviced (as
* determined by {@link #isHostUnknown(String)}), whatever the reason for the refusal: then it is opened from the XMPP
* domain of the local XMPP service. In that case, RFC 6120 section 4.9.1.3 allows either an authoritative name for
* the receiving entity or the empty string; the XMPP domain is such an authoritative name.
*
* @param xpp a parser positioned on the stream header that is refused.
* @param host the value of the 'to' attribute of the refused stream header (can be null).
* @param error the stream error to send to the peer.
*/
protected void refuseStream(XmlPullParser xpp, String host, StreamError error) {
final String from = host == null || isHostUnknown(host)
? XMPPServer.getInstance().getServerInfo().getXMPPDomain()
: host;
final Element stream = DocumentHelper.createElement(QName.get("stream", "stream", "http://etherx.jabber.org/streams"));
final Document document = DocumentHelper.createDocument(stream);
document.setXMLEncoding(StandardCharsets.UTF_8.toString());
// The refused header need not declare a content namespace. When it does not, the error is answered in the
// namespace of this handler, rather than copying the absent namespace (which would fail to build the error).
final String contentNamespace = xpp.getNamespace(null);
stream.add(Namespace.get(contentNamespace != null ? contentNamespace : getNamespace().getURI()));
stream.addAttribute("from", from);
stream.addAttribute("id", STREAM_ID_FACTORY.createStreamID().getID());
stream.addAttribute("version", "1.0");
stream.add(error.getElement());

// Deliver stanza
connection.deliverRawText(StringUtils.asUnclosedStream(document));

// Close the underlying connection
connection.close();
}
// Close the underlying connection
connection.close();
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Ignite Realtime Foundation. All rights reserved.
* Copyright (C) 2022-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.
Expand All @@ -17,11 +17,15 @@
package org.jivesoftware.openfire.net;

import org.jivesoftware.util.SystemProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;

import javax.annotation.Nonnull;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
* Configuration of Openfire's 'gateway' or 'trunking' functionality, which allows Openfire to act as a gateway to
Expand All @@ -32,6 +36,8 @@
*/
public class Trunking
{
private static final Logger Log = LoggerFactory.getLogger(Trunking.class);

/**
* Enables or disables the trunking functionality that allows Openfire to act as a gateway to transfer data between
* other XMPP domains.
Expand Down Expand Up @@ -61,7 +67,44 @@ public class Trunking
*/
public static boolean isTrunkingEnabledFor(@Nonnull final String domain)
{
return ENABLED.getValue() && ALLOWABLE_DOMAINS.getValue().stream().anyMatch(v -> JID.domainprep(v).equals(JID.domainprep(domain)));
if (!ENABLED.getValue()) {
return false;
}
return normalisedDomains().contains(JID.domainprep(domain));
}

/**
* The configured trunking domains, and their normalised form. Recomputed only when the configuration changes.
*/
private record NormalisedDomains(List<String> configured, Set<String> normalised) {}

private static volatile NormalisedDomains normalisedDomains = new NormalisedDomains(List.of(), Set.of());

/**
* Returns the normalised form of the domains that are configured for trunking. A configured value that is not a
* valid domain name is ignored, so that it does not prevent the other configured domains from being recognised.
* The configuration is normalised again only when it changes, so that an invalid value is reported once per change
* rather than on every check.
*
* @return the normalised domains.
*/
private static Set<String> normalisedDomains()
{
final List<String> configured = ALLOWABLE_DOMAINS.getValue();
NormalisedDomains current = normalisedDomains;
if (!current.configured().equals(configured)) {
final Set<String> normalised = new HashSet<>();
for (final String value : configured) {
try {
normalised.add(JID.domainprep(value));
} catch (final IllegalArgumentException e) {
Log.warn("Ignoring a value of {} that is not a valid domain name: '{}'", ALLOWABLE_DOMAINS.getKey(), value);
}
}
current = new NormalisedDomains(List.copyOf(configured), Set.copyOf(normalised));
normalisedDomains = current;
}
return current.normalised();
}
}

Loading
Loading