Skip to content

Commit

Permalink
Add service-loaded extension points for channel initialization (#13565)
Browse files Browse the repository at this point in the history
**Motivation:**

Netty is used by many libraries and frameworks that (reasonably) hide
the fact that they use Netty under the covers. When Netty is hidden, it
is hard, or sometimes impossible, to modify the channel pipelines,
attributes, or options, from the outside. It might not even be clear if
a framework or library makes use of Netty at all. However, we sometimes
want to apply some changes, or make some checks, to most or all channels
that are initialized by Netty, regardless of the framework or library
that is using Netty in the given case.

Some examples of use-cases are:
- Web application firewalls.
- Server-side request forgery filters.
- Intrusion detection.
- Metrics gathering.

To address these use-cases in a way that don't require integrators to
somehow find every Netty usage in a process, we introduce a
service-loaded extension point that hooks into the channel
initialization process.

**Modification:**

A new abstract class, `ChannelInitializerExtension`, is added.
Implementations of this interface can be found and service-loaded at
runtime by `AbstractBootstrap`, with the help of the
`ChannelInitializerExtensions` utility class.

The `ChannelInitializerExtension` class offers three callback methods,
one for the initialization of each of the different "kinds" of channels:
- Server listener channels.
- Server child channels.
- Client channels.

The extensions are disabled by default for security reasons, and require
opt in by setting the `io.netty.bootstrap.extensions` system property to
`serviceload`.
To see what extensions are available, the system property can be set to
`log`, and we will load and log (at INFO level) what extensions are
available, but not actually run them.

**Result:**

We have extension points for use cases that are cutting-across different
Netty uses, and require low-level Netty access. This makes it relatively
easy to implement such use cases without requiring deep access to the
Netty internals of arbitrary libraries and frameworks, and without even
needing to know which libraries and frameworks are actually using Netty.
The only restriction to this, is the use of shading with
class-relocation, which in each case would require their own
`META-INF/services` file. Thankfully, most libraries and frameworks that
shade Netty, also offer non-shading versions.
  • Loading branch information
chrisvest committed Nov 2, 2023
1 parent 5a98214 commit d2a7264
Show file tree
Hide file tree
Showing 13 changed files with 463 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFactory;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
Expand All @@ -32,11 +30,7 @@
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.resolver.AddressResolver;
import io.netty.resolver.AddressResolverGroup;
import io.netty.resolver.InetSocketAddressResolver;
import io.netty.resolver.dns.DnsNameResolver;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.FutureListener;
import io.netty.util.concurrent.GenericFutureListener;
Expand All @@ -62,8 +56,6 @@
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URL;
import java.security.SecureRandom;
import java.security.cert.CertificateEncodingException;
Expand Down Expand Up @@ -142,7 +134,7 @@ public void run() {
path = "/";
} else {
if (uri.getQuery() != null) {
path = path + "?" + uri.getQuery();
path = path + '?' + uri.getQuery();
}
}

Expand Down Expand Up @@ -194,6 +186,7 @@ private static Promise<OCSPResp> query(final EventLoop eventLoop, final ByteBuf
.group(ioTransport.eventLoop())
.option(ChannelOption.TCP_NODELAY, true)
.channelFactory(ioTransport.socketChannel())
.attr(OcspServerCertificateValidator.OCSP_PIPELINE_ATTRIBUTE, Boolean.TRUE)
.handler(new Initializer(responsePromise));

dnsNameResolver.resolve(host).addListener(new FutureListener<InetAddress>() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import io.netty.handler.ssl.SslHandshakeCompletionEvent;
import io.netty.resolver.dns.DnsNameResolver;
import io.netty.resolver.dns.DnsNameResolverBuilder;
import io.netty.util.AttributeKey;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import io.netty.util.concurrent.Promise;
Expand All @@ -41,6 +42,11 @@
* will perform certificate validation using OCSP over HTTP/1.1 with the server's certificate issuer OCSP responder.
*/
public class OcspServerCertificateValidator extends ChannelInboundHandlerAdapter {
/**
* An attribute used to mark all channels created by the {@link OcspServerCertificateValidator}.
*/
public static final AttributeKey<Boolean> OCSP_PIPELINE_ATTRIBUTE =
AttributeKey.newInstance("io.netty.handler.ssl.ocsp.pipeline");

private final boolean closeAndThrowIfNotValid;
private final boolean validateNonce;
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1560,7 +1560,7 @@
<nativeImage.handlerMetadataGroupId>${project.groupId}</nativeImage.handlerMetadataGroupId>
<nativeimage.handlerMetadataArtifactId>${project.artifactId}</nativeimage.handlerMetadataArtifactId>
</systemPropertyVariables>
<argLine>${argLine.common} ${argLine.printGC} ${argLine.alpnAgent} ${argLine.leak} ${argLine.coverage} ${argLine.noUnsafe} ${argLine.jni} ${argLine.java9} ${argLine.javaProperties}</argLine>
<argLine>${argLine.common} ${argLine.printGC} ${argLine.alpnAgent} ${argLine.leak} ${argLine.coverage} ${argLine.noUnsafe} ${argLine.jni} ${argLine.java9} ${argLine.javaProperties} -Dio.netty.bootstrap.extensions=serviceload</argLine>
<properties>
<property>
<name>listener</name>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import io.netty.resolver.HostsFileEntriesResolver;
import io.netty.resolver.InetNameResolver;
import io.netty.resolver.ResolvedAddressTypes;
import io.netty.util.AttributeKey;
import io.netty.util.NetUtil;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.EventExecutor;
Expand Down Expand Up @@ -92,6 +93,11 @@
* A DNS-based {@link InetNameResolver}.
*/
public class DnsNameResolver extends InetNameResolver {
/**
* An attribute used to mark all channels created by the {@link DnsNameResolver}.
*/
public static final AttributeKey<Boolean> DNS_PIPELINE_ATTRIBUTE =
AttributeKey.newInstance("io.netty.resolver.dns.pipeline");

private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsNameResolver.class);
private static final String LOCALHOST = "localhost";
Expand Down Expand Up @@ -458,6 +464,7 @@ public DnsNameResolver(
socketBootstrap.option(ChannelOption.SO_REUSEADDR, true)
.group(executor())
.channelFactory(socketChannelFactory)
.attr(DNS_PIPELINE_ATTRIBUTE, Boolean.TRUE)
.handler(TCP_ENCODER);
}
switch (this.resolvedAddressTypes) {
Expand Down Expand Up @@ -498,9 +505,10 @@ public DnsNameResolver(
inflightLookups = null;
}

Bootstrap b = new Bootstrap();
b.group(executor());
b.channelFactory(channelFactory);
Bootstrap b = new Bootstrap()
.group(executor())
.channelFactory(channelFactory)
.attr(DNS_PIPELINE_ATTRIBUTE, Boolean.TRUE);
this.channelReadyPromise = executor().newPromise();
final DnsResponseHandler responseHandler =
new DnsResponseHandler(channelReadyPromise);
Expand Down
24 changes: 24 additions & 0 deletions transport/src/main/java/io/netty/bootstrap/AbstractBootstrap.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
Expand Down Expand Up @@ -66,6 +67,7 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext
private final Map<ChannelOption<?>, Object> options = new LinkedHashMap<ChannelOption<?>, Object>();
private final Map<AttributeKey<?>, Object> attrs = new ConcurrentHashMap<AttributeKey<?>, Object>();
private volatile ChannelHandler handler;
private volatile ClassLoader extensionsClassLoader;

AbstractBootstrap() {
// Disallow extending from a different package.
Expand All @@ -80,6 +82,7 @@ public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C ext
options.putAll(bootstrap.options);
}
attrs.putAll(bootstrap.attrs);
extensionsClassLoader = bootstrap.extensionsClassLoader;
}

/**
Expand Down Expand Up @@ -196,6 +199,19 @@ public <T> B attr(AttributeKey<T> key, T value) {
return self();
}

/**
* Load {@link ChannelInitializerExtension}s using the given class loader.
* <p>
* By default, the extensions will be loaded by the same class loader that loaded this bootstrap class.
*
* @param classLoader The class loader to use for loading {@link ChannelInitializerExtension}s.
* @return This bootstrap.
*/
public B extensionsClassLoader(ClassLoader classLoader) {
extensionsClassLoader = classLoader;
return self();
}

/**
* Validate all the parameters. Sub-classes may override this, but should
* call the super method in that case.
Expand Down Expand Up @@ -343,6 +359,14 @@ final ChannelFuture initAndRegister() {

abstract void init(Channel channel) throws Exception;

Collection<ChannelInitializerExtension> getInitializerExtensions() {
ClassLoader loader = extensionsClassLoader;
if (loader == null) {
loader = getClass().getClassLoader();
}
return ChannelInitializerExtensions.getExtensions().extensions(loader);
}

private static void doBind0(
final ChannelFuture regFuture, final Channel channel,
final SocketAddress localAddress, final ChannelPromise promise) {
Expand Down
16 changes: 13 additions & 3 deletions transport/src/main/java/io/netty/bootstrap/Bootstrap.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
import io.netty.channel.EventLoop;
import io.netty.channel.EventLoopGroup;
import io.netty.resolver.AddressResolver;
import io.netty.resolver.AddressResolverGroup;
import io.netty.resolver.DefaultAddressResolverGroup;
import io.netty.resolver.NameResolver;
import io.netty.resolver.AddressResolverGroup;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.FutureListener;
import io.netty.util.internal.ObjectUtil;
Expand All @@ -35,6 +35,7 @@
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Collection;

/**
* A {@link Bootstrap} that makes it easy to bootstrap a {@link Channel} to use
Expand Down Expand Up @@ -70,9 +71,8 @@ private Bootstrap(Bootstrap bootstrap) {
*
* @see io.netty.resolver.DefaultAddressResolverGroup
*/
@SuppressWarnings("unchecked")
public Bootstrap resolver(AddressResolverGroup<?> resolver) {
this.externalResolver = resolver == null ? null : new ExternalAddressResolver(resolver);
externalResolver = resolver == null ? null : new ExternalAddressResolver(resolver);
disableResolver = false;
return this;
}
Expand Down Expand Up @@ -277,6 +277,16 @@ void init(Channel channel) {

setChannelOptions(channel, newOptionsArray(), logger);
setAttributes(channel, newAttributesArray());
Collection<ChannelInitializerExtension> extensions = getInitializerExtensions();
if (!extensions.isEmpty()) {
for (ChannelInitializerExtension extension : extensions) {
try {
extension.postInitializeClientChannel(channel);
} catch (Exception e) {
logger.warn("Exception thrown from postInitializeClientChannel", e);
}
}
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright 2023 The Netty Project
*
* The Netty Project licenses this file to you 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:
*
* https://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 io.netty.bootstrap;

import io.netty.channel.Channel;
import io.netty.channel.ServerChannel;

/**
* A channel initializer extension make it possible to enforce rules and apply modifications across multiple,
* disconnected uses of Netty within the same JVM process.
* <p>
* For instance, application-level firewall rules can be injected into all uses of Netty within an application,
* without making changes to such uses that are otherwise outside the purview of the application code,
* such as 3rd-party libraries.
* <p>
* Channel initializer extensions are <em>not</em> enabled by default, because of their power to influence Netty
* pipelines across libraries, frameworks, and use-cases.
* Extensions must be explicitly enabled by setting the {@value #EXTENSIONS_SYSTEM_PROPERTY} to {@code serviceload}.
* <p>
* All channel initializer extensions that are available on the classpath will be
* {@linkplain java.util.ServiceLoader#load(Class) service-loaded} and used by all {@link AbstractBootstrap} subclasses.
* <p>
* Note that this feature will not work for Netty uses that are shaded <em>and relocated</em> into other libraries.
* The classes in a relocated Netty library are technically distinct and incompatible types. This means the
* service-loader in non-relocated Netty will not see types from a relocated Netty, and vice versa.
*/
public abstract class ChannelInitializerExtension {
/**
* The name of the system property that control initializer extensions.
* <p>
* These extensions can potentially be a security liability, so they are disabled by default.
* <p>
* To enable the extensions, application operators can explicitly opt in by setting this system property to the
* value {@code serviceload}. This will enable all the extensions that are available through the service loader
* mechanism.
* <p>
* To load and log (at INFO level) all available extensions without actually running them, set this system property
* to the value {@code log}.
*/
public static final String EXTENSIONS_SYSTEM_PROPERTY = "io.netty.bootstrap.extensions";

/**
* Get the "priority" of this extension. If multiple extensions are avilable, then they will be called in their
* priority order, from lowest to highest.
* <p>
* Implementers are encouraged to pick a number between {@code -100.0} and {@code 100.0}, where extensions that have
* no particular opinion on their priority are encouraged to return {@code 0.0}.
* <p>
* Extensions with lower priority will get called first, while extensions with greater priority may be able to
* observe the effects of extensions with lesser priority.
* <p>
* Note that if multiple extensions have the same priority, then their relative order will be unpredictable.
* As such, implementations should always take into consideration that other extensions might be called before
* or after them.
* <p>
* Override this method to specify your own priority.
* The default implementation just returns {@code 0}.
*
* @return The priority.
*/
public double priority() {
return 0;
}

/**
* Called by {@link Bootstrap} after the initialization of the given client channel.
* <p>
* The method is allowed to modify the handlers in the pipeline, the channel attributes, or the channel options.
* The method must refrain from doing any I/O, or from closing the channel.
* <p>
* Override this method to add your own callback logic.
* The default implementation does nothing.
*
* @param channel The channel that was initialized.
*/
public void postInitializeClientChannel(Channel channel) {
}

/**
* Called by {@link ServerBootstrap} after the initialization of the given server listener channel.
* The listener channel is responsible for invoking the {@code accept(2)} system call,
* and for producing child channels.
* <p>
* The method is allowed to modify the handlers in the pipeline, the channel attributes, or the channel options.
* The method must refrain from doing any I/O, or from closing the channel.
* <p>
* Override this method to add your own callback logic.
* The default implementation does nothing.
*
* @param channel The channel that was initialized.
*/
public void postInitializeServerListenerChannel(ServerChannel channel) {
}

/**
* Called by {@link ServerBootstrap} after the initialization of the given child channel.
* A child channel is a newly established connection from a client to the server.
* <p>
* The method is allowed to modify the handlers in the pipeline, the channel attributes, or the channel options.
* The method must refrain from doing any I/O, or from closing the channel.
* <p>
* Override this method to add your own callback logic.
* The default implementation does nothing.
*
* @param channel The channel that was initialized.
*/
public void postInitializeServerChildChannel(Channel channel) {
}
}
Loading

0 comments on commit d2a7264

Please sign in to comment.