<version>${project.version}</version>
</dependency>
+ <!-- NETCONF Transport API and implementations-->
+ <dependency>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>transport-api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>transport-tcp</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
<!-- YANG models -->
<!-- RFC5277 NETCONF Event Notifications -->
<dependency>
<module>karaf</module>
<module>karaf-static</module>
<module>model</module>
+ <module>transport</module>
+
+ <!-- Legacy layout -->
<module>netconf</module>
<module>restconf</module>
</modules>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- vi: set et smarttab sw=4 tabstop=4: -->
+<!--
+ Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+
+ This program and the accompanying materials are made available under the
+ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ and is available at http://www.eclipse.org/legal/epl-v10.html
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.opendaylight.odlparent</groupId>
+ <artifactId>odlparent-lite</artifactId>
+ <version>11.0.1</version>
+ <relativePath/>
+ </parent>
+
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>transport-aggregator</artifactId>
+ <version>4.0.3-SNAPSHOT</version>
+ <packaging>pom</packaging>
+ <name>${project.artifactId}</name>
+
+ <properties>
+ <maven.deploy.skip>true</maven.deploy.skip>
+ <maven.install.skip>true</maven.install.skip>
+ </properties>
+
+ <modules>
+ <module>transport-api</module>
+ <module>transport-tcp</module>
+ </modules>
+</project>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- vi: set et smarttab sw=4 tabstop=4: -->
+<!--
+ Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+
+ This program and the accompanying materials are made available under the
+ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ and is available at http://www.eclipse.org/legal/epl-v10.html
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>netconf-parent</artifactId>
+ <version>4.0.3-SNAPSHOT</version>
+ <relativePath>../../parent</relativePath>
+ </parent>
+
+ <artifactId>transport-api</artifactId>
+ <name>${project.artifactId}</name>
+ <packaging>bundle</packaging>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-transport</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>yang-common</artifactId>
+ </dependency>
+ </dependencies>
+</project>
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.api;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.MoreObjects.ToStringHelper;
+import io.netty.channel.Channel;
+
+/**
+ * Abstract base class for {@link TransportChannel}s overlaid on another {@link TransportChannel}.
+ */
+public abstract class AbstractOverlayTransportChannel extends TransportChannel {
+ private final TransportChannel underlay;
+
+ protected AbstractOverlayTransportChannel(final TransportChannel tcp) {
+ underlay = requireNonNull(tcp);
+ }
+
+ @Override
+ public final Channel channel() {
+ return underlay.channel();
+ }
+
+ @Override
+ protected ToStringHelper addToStringAttributes(final ToStringHelper helper) {
+ return helper.add("underlay", underlay);
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.api;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yangtools.yang.common.Empty;
+
+/**
+ * Abstract base class for {@link TransportStack}s overlaid on a different stack.
+ */
+public abstract class AbstractOverlayTransportStack<C extends TransportChannel> extends AbstractTransportStack<C> {
+ private final @NonNull TransportChannelListener asListener = new TransportChannelListener() {
+ @Override
+ public void onTransportChannelFailed(final Throwable cause) {
+ notifyTransportChannelFailed(cause);
+ }
+
+ @Override
+ public void onTransportChannelEstablished(final TransportChannel channel) {
+ onUnderlayChannelEstablished(channel);
+ }
+ };
+
+ private volatile TransportStack underlay = null;
+
+ protected AbstractOverlayTransportStack(final TransportChannelListener listener) {
+ super(listener);
+ }
+
+ @Override
+ protected final ListenableFuture<Empty> startShutdown() {
+ return underlay.shutdown();
+ }
+
+ protected final @NonNull TransportChannelListener asListener() {
+ return asListener;
+ }
+
+ protected abstract void onUnderlayChannelEstablished(@NonNull TransportChannel underlayChannel);
+
+ final void setUnderlay(final TransportStack underlay) {
+ this.underlay = requireNonNull(underlay);
+ }
+
+ protected static final <T extends AbstractOverlayTransportStack<?>> @NonNull ListenableFuture<T> transformUnderlay(
+ final T stack, final ListenableFuture<? extends TransportStack> tcpFuture) {
+ return Futures.transform(tcpFuture, tcpStack -> {
+ stack.setUnderlay(tcpStack);
+ return stack;
+ }, MoreExecutors.directExecutor());
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.api;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.MoreObjects.ToStringHelper;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
+import io.netty.util.concurrent.Future;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+import org.checkerframework.checker.lock.qual.GuardedBy;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yangtools.yang.common.Empty;
+
+/**
+ * Convenience base class for {@link TransportStack} implementations. It mediates atomic idempotence of
+ * {@link #shutdown()}.
+ *
+ * @param <C> associated {@link TransportChannel} type
+ */
+public abstract class AbstractTransportStack<C extends TransportChannel> implements TransportStack {
+ private final @NonNull TransportChannelListener listener;
+
+ /**
+ * Polymorphic state. It can be in one of four states:
+ * <ol>
+ * <li>{@code null} when there is no attached transport channel,</li>
+ * <li>a {@link TransportChannel} instance when there is exactly one attached transport channel,</li>
+ * <li>a {@link Set} holding multiple attached transport channels,</li>
+ * <li>a {@link ListenableFuture} completing when shutdown is complete
+ * </ol>
+ */
+ @GuardedBy("this")
+ private Object state;
+
+ protected AbstractTransportStack(final TransportChannelListener listener) {
+ this.listener = requireNonNull(listener);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public final ListenableFuture<Empty> shutdown() {
+ final SettableFuture<Empty> future;
+ final Set<TransportChannel> channels;
+
+ synchronized (this) {
+ var local = state;
+ if (local instanceof ListenableFuture) {
+ // Already shutting down, no-op
+ return (ListenableFuture<Empty>) local;
+ }
+
+ if (local == null) {
+ channels = Set.of();
+ } else if (local instanceof Set) {
+ channels = (Set<TransportChannel>) local;
+ } else if (local instanceof TransportChannel tc) {
+ channels = Set.of(tc);
+ } else {
+ throw new IllegalStateException("Unexpected state " + local);
+ }
+ state = future = SettableFuture.create();
+ }
+
+ final var futures = new ArrayList<ListenableFuture<?>>(channels.size() + 1);
+ futures.add(startShutdown());
+ for (var channel : channels) {
+ futures.add(toListenableFuture(channel.channel().close()));
+ }
+ Futures.whenAllComplete(futures).run(() -> future.set(Empty.value()), MoreExecutors.directExecutor());
+ return future;
+ }
+
+ protected abstract @NonNull ListenableFuture<Empty> startShutdown();
+
+ protected final void addTransportChannel(final @NonNull C channel) {
+ // Careful stepping here so we do not invoke callbacks while holding a lock...
+ final var ch = channel.channel();
+ if (add(channel)) {
+ // The channel is tracked in state, make sure we remove it when it goes away. Invoke the user listener only
+ // after that, so our listener fires first (and the user does not have a chance to close() before that).
+ ch.closeFuture().addListener(ignored -> remove(channel));
+ listener.onTransportChannelEstablished(channel);
+ } else {
+ // We are already shutting down, just close the channel
+ ch.close();
+ }
+ }
+
+ protected final void notifyTransportChannelFailed(final @NonNull Throwable cause) {
+ listener.onTransportChannelFailed(cause);
+ }
+
+ @Override
+ public final String toString() {
+ return addToStringAttributes(MoreObjects.toStringHelper(this).omitNullValues()).toString();
+ }
+
+ protected synchronized ToStringHelper addToStringAttributes(final ToStringHelper helper) {
+ return helper.add("listener", listener).add("state", state);
+ }
+
+ protected static final @NonNull ListenableFuture<Empty> toListenableFuture(final Future<?> nettyFuture) {
+ final var ret = SettableFuture.<Empty>create();
+ nettyFuture.addListener(future -> {
+ final var cause = future.cause();
+ if (cause != null) {
+ ret.setException(cause);
+ } else {
+ ret.set(Empty.value());
+ }
+ });
+ return ret;
+ }
+
+ @SuppressWarnings("unchecked")
+ private synchronized boolean add(final @NonNull TransportChannel channel) {
+ final var local = state;
+ if (local instanceof ListenableFuture) {
+ // Already shutting down
+ return false;
+ }
+ if (local == null) {
+ // First session, simple
+ state = channel;
+ } else if (local instanceof Set) {
+ ((Set<TransportChannel>) local).add(channel);
+ } else if (local instanceof TransportChannel tc) {
+ final var set = new HashSet<TransportChannel>(4);
+ set.add(tc);
+ set.add(channel);
+ state = set;
+ } else {
+ throw new IllegalStateException("Unhandled state " + local);
+ }
+ return true;
+ }
+
+ private synchronized void remove(final @NonNull TransportChannel channel) {
+ final var local = state;
+ if (local == null || local instanceof ListenableFuture) {
+ // No recorded channel or we are already shutting down
+ return;
+ }
+
+ if (local.equals(channel)) {
+ // Single channel, go back to null
+ state = null;
+ } else if (local instanceof Set<?> set) {
+ // Multiple channels, let's just remove it. Note this does not collapse the set if there's just one
+ // remaining session -- the chances are we will go back to more than one soon
+ set.remove(channel);
+ } else {
+ throw new IllegalStateException("Unhandled state " + local);
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.api;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.MoreObjects.ToStringHelper;
+import io.netty.channel.Channel;
+import org.eclipse.jdt.annotation.NonNull;
+
+/**
+ * A transport-level session. This concept is bound to a {@link Channel} for now, so as to enforce type-safety. It acts
+ * as a meeting point between a logical NETCONF session and the underlying transport.
+ */
+public abstract class TransportChannel {
+ /**
+ * Return the underlying Netty channel.
+ *
+ * @return Netty channel
+ */
+ public abstract @NonNull Channel channel();
+
+ @Override
+ public final int hashCode() {
+ return super.hashCode();
+ }
+
+ @Override
+ public final boolean equals(final Object obj) {
+ return super.equals(obj);
+ }
+
+ @Override
+ public final String toString() {
+ return addToStringAttributes(MoreObjects.toStringHelper(this).omitNullValues()).toString();
+ }
+
+ protected ToStringHelper addToStringAttributes(final ToStringHelper helper) {
+ return helper.add("channel", channel());
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.api;
+
+import org.eclipse.jdt.annotation.NonNull;
+
+/**
+ * Transport-level channel event listener.
+ */
+public interface TransportChannelListener {
+ /**
+ * Invoked when a {@link TransportChannel} is established. Implementations of this method are expected to attach
+ * to validate the channel and connect it to the messages layer.
+ *
+ * @param channel Established channel
+ */
+ void onTransportChannelEstablished(@NonNull TransportChannel channel);
+
+ /**
+ * Invoked when a {@link TransportChannel} could not be established. Implementations of this method are expected
+ * to react to this failure at least by logging it.
+ *
+ * @param cause Failure cause
+ */
+ void onTransportChannelFailed(@NonNull Throwable cause);
+}
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.api;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.yangtools.yang.common.Empty;
+
+/**
+ * A wiring of multiple transport components which provides resolution of {@link TransportChannel}s. There are generally
+ * two ways to provide a stack:
+ * <ul>
+ * <li>a listen stack, used for normal NETCONF servers and Call-Home clients, and</li>
+ * <li>a connect stack, used for normal NETCONF clients and Call-Home servers</li>
+ * </ul>.
+ */
+@NonNullByDefault
+public interface TransportStack {
+ /**
+ * Initiate shutdown of this stack, terminating all underlying transport sessions. Implementations of this method
+ * are required to be idempotent, returning the same future.
+ *
+ * @return a {@link ListenableFuture} which completes when all resources have been released.
+ */
+ ListenableFuture<Empty> shutdown();
+}
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.api;
+
+import java.io.Serial;
+
+/**
+ * Exception thrown when an unsupported configuration is supplied to a transport implementation.
+ */
+public final class UnsupportedConfigurationException extends Exception {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ public UnsupportedConfigurationException(final String message) {
+ super(message);
+ }
+
+ public UnsupportedConfigurationException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+/**
+ * NETCONF Secure Transport layer interfaces. These cover establishment of a the persistent connection and managing its
+ * lifecycle. See <a href="https://www.rfc-editor.org/rfc/rfc6241#page-9">RFC6241 Figure 1</a> for the architectural
+ * placement in the NETCONF stack.
+ *
+ * <p>
+ * In traditional NETCONF operation NETCONF server listens for TCP connections and NETCONF clients connect to it. In
+ * <a href="https://www.rfc-editor.org/rfc/rfc8071#section-2">Call Home</a>, the TCP layer operates the other way
+ * around, i.e. NETCONF clients listen for TCP connections and NETCONF servers connect to it. Once the TCP session is
+ * established, though, it is the client which initiates both transport-level and NETCONF-level handshakes.
+ *
+ * <p>
+ * Both scenarios are handled by a single set of interfaces:
+ * <ul>
+ * <li>a {@link TransportStack} represents a transport instance -- a server or a client in either liste or connect
+ * mode.</li>
+ * <li>a {@link TransportChannel} represents a transport session -- there can be multiple concurrent
+ * {@code TransportChannel}s associated with a listening client or server, but only one associated with a
+ * connecting client or server.</li>
+ * <li>a {@link TransportChannelListener} is the primary callback mechanism towards client code. Its methods are
+ * invoked when a {@link TransportChannel} is established successfully or a failure to establish a channel is
+ * encountered.</li>
+ * </ul>
+ */
+package org.opendaylight.netconf.transport.api;
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- vi: set et smarttab sw=4 tabstop=4: -->
+<!--
+ Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+
+ This program and the accompanying materials are made available under the
+ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ and is available at http://www.eclipse.org/legal/epl-v10.html
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>netconf-parent</artifactId>
+ <version>4.0.3-SNAPSHOT</version>
+ <relativePath>../../parent</relativePath>
+ </parent>
+
+ <artifactId>transport-tcp</artifactId>
+ <name>${project.artifactId}</name>
+ <packaging>bundle</packaging>
+ <description>NETCONF TCP transport</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-common</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-transport</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-transport-classes-epoll</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kohsuke.metainf-services</groupId>
+ <artifactId>metainf-services</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.yangtools</groupId>
+ <artifactId>yang-common</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.mdsal.binding.model.ietf</groupId>
+ <artifactId>rfc6991-ietf-inet-types</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.netconf.model</groupId>
+ <artifactId>draft-ietf-netconf-crypto-types</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>transport-api</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-transport-native-epoll</artifactId>
+ <classifier>linux-x86_64</classifier>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.tcp;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.socket.ServerSocketChannel;
+import io.netty.channel.socket.SocketChannel;
+import java.util.concurrent.ThreadFactory;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.common.rev220524.tcp.common.grouping.Keepalives;
+
+/**
+ * Wrapper around a particular Netty transport implementation.
+ */
+@NonNullByDefault
+abstract sealed class AbstractNettyImpl permits EpollNettyImpl, NioNettyImpl {
+
+ abstract Class<? extends SocketChannel> channelClass();
+
+ abstract Class<? extends ServerSocketChannel> serverChannelClass();
+
+ abstract EventLoopGroup newEventLoopGroup(int numThreads, ThreadFactory threadFactory);
+
+ abstract boolean supportsKeepalives();
+
+ abstract void configureKeepalives(Bootstrap bootstrap, Keepalives keepalives);
+
+ abstract void configureKeepalives(ServerBootstrap bootstrap, Keepalives keepalives);
+
+ @Override
+ public abstract String toString();
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.tcp;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.epoll.EpollChannelOption;
+import io.netty.channel.epoll.EpollEventLoopGroup;
+import io.netty.channel.epoll.EpollServerSocketChannel;
+import io.netty.channel.epoll.EpollSocketChannel;
+import java.util.concurrent.ThreadFactory;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.common.rev220524.tcp.common.grouping.Keepalives;
+
+@NonNullByDefault
+final class EpollNettyImpl extends AbstractNettyImpl {
+ @Override
+ Class<EpollSocketChannel> channelClass() {
+ return EpollSocketChannel.class;
+ }
+
+ @Override
+ Class<EpollServerSocketChannel> serverChannelClass() {
+ return EpollServerSocketChannel.class;
+ }
+
+ @Override
+ EventLoopGroup newEventLoopGroup(final int numThreads, final ThreadFactory threadFactory) {
+ return new EpollEventLoopGroup(numThreads, threadFactory);
+ }
+
+ @Override
+ boolean supportsKeepalives() {
+ return true;
+ }
+
+ @Override
+ void configureKeepalives(final Bootstrap bootstrap, final Keepalives keepalives) {
+ bootstrap
+ .option(ChannelOption.SO_KEEPALIVE, Boolean.TRUE)
+ .option(EpollChannelOption.TCP_KEEPIDLE, keepalives.requireIdleTime().toJava())
+ .option(EpollChannelOption.TCP_KEEPCNT, keepalives.requireMaxProbes().toJava())
+ .option(EpollChannelOption.TCP_KEEPINTVL, keepalives.requireProbeInterval().toJava());
+ }
+
+ @Override
+ void configureKeepalives(final ServerBootstrap bootstrap, final Keepalives keepalives) {
+ bootstrap
+ .childOption(ChannelOption.SO_KEEPALIVE, Boolean.TRUE)
+ .childOption(EpollChannelOption.TCP_KEEPIDLE, keepalives.requireIdleTime().toJava())
+ .childOption(EpollChannelOption.TCP_KEEPCNT, keepalives.requireMaxProbes().toJava())
+ .childOption(EpollChannelOption.TCP_KEEPINTVL, keepalives.requireProbeInterval().toJava());
+ }
+
+ @Override
+ public String toString() {
+ return "epoll(2)";
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.tcp;
+
+import java.util.Set;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.kohsuke.MetaInfServices;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.client.rev220524.IetfTcpClientData;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.client.rev220524.LocalBindingSupported;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.client.rev220524.TcpClientKeepalives;
+import org.opendaylight.yangtools.yang.binding.YangFeature;
+import org.opendaylight.yangtools.yang.binding.YangFeatureProvider;
+
+/**
+ * Baseline features supported from {@code ietf-tcp-client.yang}.
+ */
+@MetaInfServices
+@NonNullByDefault
+public final class IetfTcpClientFeatureProvider implements YangFeatureProvider<IetfTcpClientData> {
+ @Override
+ public Class<IetfTcpClientData> boundModule() {
+ return IetfTcpClientData.class;
+ }
+
+ @Override
+ public Set<? extends YangFeature<?, IetfTcpClientData>> supportedFeatures() {
+ return NettyTransportSupport.keepalivesSupported()
+ ? Set.of(LocalBindingSupported.VALUE, TcpClientKeepalives.VALUE) : Set.of(LocalBindingSupported.VALUE);
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.tcp;
+
+import java.util.Set;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.kohsuke.MetaInfServices;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.common.rev220524.IetfTcpCommonData;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.common.rev220524.KeepalivesSupported;
+import org.opendaylight.yangtools.yang.binding.YangFeature;
+import org.opendaylight.yangtools.yang.binding.YangFeatureProvider;
+
+/**
+ * Baseline features supported from {@code ietf-tcp-common.yang}.
+ */
+@MetaInfServices
+@NonNullByDefault
+public final class IetfTcpCommonFeatureProvider implements YangFeatureProvider<IetfTcpCommonData> {
+ @Override
+ public Class<IetfTcpCommonData> boundModule() {
+ return IetfTcpCommonData.class;
+ }
+
+ @Override
+ public Set<? extends YangFeature<?, IetfTcpCommonData>> supportedFeatures() {
+ return Set.of(KeepalivesSupported.VALUE);
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.tcp;
+
+import java.util.Set;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.kohsuke.MetaInfServices;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.server.rev220524.IetfTcpServerData;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.server.rev220524.TcpServerKeepalives;
+import org.opendaylight.yangtools.yang.binding.YangFeature;
+import org.opendaylight.yangtools.yang.binding.YangFeatureProvider;
+
+/**
+ * Baseline features supported from {@code ietf-tcp-server.yang}.
+ */
+@MetaInfServices
+@NonNullByDefault
+public final class IetfTcpServerFeatureProvider implements YangFeatureProvider<IetfTcpServerData> {
+ @Override
+ public Class<IetfTcpServerData> boundModule() {
+ return IetfTcpServerData.class;
+ }
+
+ @Override
+ public Set<? extends YangFeature<?, IetfTcpServerData>> supportedFeatures() {
+ return NettyTransportSupport.keepalivesSupported() ? Set.of(TcpServerKeepalives.VALUE) : Set.of();
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.tcp;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import io.netty.bootstrap.Bootstrap;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.epoll.Epoll;
+import io.netty.channel.socket.ServerSocketChannel;
+import io.netty.channel.socket.SocketChannel;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.common.rev220524.tcp.common.grouping.Keepalives;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility class providing indirection between various Netty transport implementations. An implementation is chosen
+ * based on run-time characteristics -- either {@code epoll(2)}-based or {@code java.nio}-based one.
+ */
+@NonNullByDefault
+public final class NettyTransportSupport {
+ private static final Logger LOG = LoggerFactory.getLogger(NettyTransportSupport.class);
+ private static final AbstractNettyImpl IMPL = Epoll.isAvailable() ? new EpollNettyImpl() : new NioNettyImpl();
+
+ static {
+ LOG.info("Netty transport backed by {}", IMPL);
+ }
+
+ private NettyTransportSupport() {
+ // Hidden on purpose
+ }
+
+ /**
+ * Return a new {@link Bootstrap} instance. The bootstrap has its {@link Bootstrap#channel(Class)} already
+ * initialized to the backing implementation's {@link SocketChannel} class.
+ *
+ * @return A new Bootstrap
+ */
+ public static Bootstrap newBootstrap() {
+ return new Bootstrap().channel(IMPL.channelClass());
+ }
+
+ /**
+ * Return a new {@link Bootstrap} instance. The bootstrap has its {@link ServerBootstrap#channel(Class)} already
+ * initialized to the backing implementation's {@link ServerSocketChannel} class.
+ *
+ * @return A new ServerBootstrap
+ */
+ public static ServerBootstrap newServerBootstrap() {
+ return new ServerBootstrap().channel(IMPL.serverChannelClass());
+ }
+
+ /**
+ * Create a new {@link EventLoopGroup} supporting the backing implementation with default number of threads. The
+ * default is twice the number of available processors, or controlled through the {@code io.netty.eventLoopThreads}
+ * system property.
+ *
+ * @param name Thread naming prefix
+ * @return An EventLoopGroup
+ */
+ public static EventLoopGroup newEventLoopGroup(final String name) {
+ return newEventLoopGroup(name, 0);
+ }
+
+ /**
+ * Create a new {@link EventLoopGroup} supporting the backing implementation with specified (or default) number of
+ * threads.
+ *
+ * @param name Thread naming prefix
+ * @param numThreads Number of threads in the group, must be non-negative. Zero selects the default.
+ * @return An EventLoopGroup
+ */
+ public static EventLoopGroup newEventLoopGroup(final String name, final int numThreads) {
+ return IMPL.newEventLoopGroup(numThreads, new ThreadFactoryBuilder()
+ .setNameFormat(requireNonNull(name) + "-%d")
+ .setUncaughtExceptionHandler(
+ (thread, ex) -> LOG.error("Thread terminated due to uncaught exception: {}", thread.getName(), ex))
+ .build());
+ }
+
+ static boolean keepalivesSupported() {
+ return IMPL.supportsKeepalives();
+ }
+
+ static void configureKeepalives(final Bootstrap bootstrap, final @Nullable Keepalives keepalives)
+ throws UnsupportedConfigurationException {
+ if (keepalives != null) {
+ checkKeepalivesSupported();
+ IMPL.configureKeepalives(bootstrap, keepalives);
+ }
+ }
+
+ static void configureKeepalives(final ServerBootstrap bootstrap, final @Nullable Keepalives keepalives)
+ throws UnsupportedConfigurationException {
+ if (keepalives != null) {
+ checkKeepalivesSupported();
+ IMPL.configureKeepalives(bootstrap, keepalives);
+ }
+ }
+
+ private static void checkKeepalivesSupported() throws UnsupportedConfigurationException {
+ if (!keepalivesSupported()) {
+ throw new UnsupportedConfigurationException("Keepalives are not supported");
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.tcp;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioChannelOption;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import java.util.Map;
+import java.util.concurrent.ThreadFactory;
+import jdk.net.ExtendedSocketOptions;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.common.rev220524.tcp.common.grouping.Keepalives;
+
+@NonNullByDefault
+final class NioNettyImpl extends AbstractNettyImpl {
+ private static final ChannelOption<Integer> TCP_KEEPIDLE = NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPIDLE);
+ private static final ChannelOption<Integer> TCP_KEEPCNT = NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPCOUNT);
+ private static final ChannelOption<Integer> TCP_KEEPINTVL =
+ NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPINTERVAL);
+
+ private final boolean supportsKeepalives;
+
+ NioNettyImpl() {
+ final var ch = new NioSocketChannel();
+ try {
+ supportsKeepalives = ch.config().setOptions(Map.of(
+ ChannelOption.SO_KEEPALIVE, Boolean.TRUE, TCP_KEEPIDLE, 7200, TCP_KEEPCNT, 3, TCP_KEEPINTVL, 5));
+ } finally {
+ ch.close();
+ }
+ }
+
+ @Override
+ Class<NioSocketChannel> channelClass() {
+ return NioSocketChannel.class;
+ }
+
+ @Override
+ Class<NioServerSocketChannel> serverChannelClass() {
+ return NioServerSocketChannel.class;
+ }
+
+ @Override
+ EventLoopGroup newEventLoopGroup(final int numThreads, final ThreadFactory threadFactory) {
+ return new NioEventLoopGroup(numThreads, threadFactory);
+ }
+
+ @Override
+ boolean supportsKeepalives() {
+ return supportsKeepalives;
+ }
+
+ @Override
+ void configureKeepalives(final Bootstrap bootstrap, final Keepalives keepalives) {
+ bootstrap
+ .option(ChannelOption.SO_KEEPALIVE, Boolean.TRUE)
+ .option(TCP_KEEPIDLE, keepalives.requireIdleTime().toJava())
+ .option(TCP_KEEPCNT, keepalives.requireMaxProbes().toJava())
+ .option(TCP_KEEPINTVL, keepalives.requireProbeInterval().toJava());
+ }
+
+ @Override
+ void configureKeepalives(final ServerBootstrap bootstrap, final Keepalives keepalives) {
+ bootstrap
+ .childOption(ChannelOption.SO_KEEPALIVE, Boolean.TRUE)
+ .childOption(TCP_KEEPIDLE, keepalives.requireIdleTime().toJava())
+ .childOption(TCP_KEEPCNT, keepalives.requireMaxProbes().toJava())
+ .childOption(TCP_KEEPINTVL, keepalives.requireProbeInterval().toJava());
+ }
+
+ @Override
+ public String toString() {
+ return "java.nio";
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.tcp;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandler.Sharable;
+import io.netty.channel.ChannelInitializer;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.netconf.transport.api.TransportChannelListener;
+import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.client.rev220524.TcpClientGrouping;
+import org.opendaylight.yangtools.yang.common.Empty;
+
+/**
+ * A {@link TCPTransportStack} acting as a TCP client.
+ */
+public final class TCPClient extends TCPTransportStack {
+ @Sharable
+ private static final class ConnectChannelInitializer extends ChannelInitializer<Channel> {
+ static final ConnectChannelInitializer INSTANCE = new ConnectChannelInitializer();
+
+ @Override
+ protected void initChannel(final Channel ch) {
+ // No-op
+ }
+ }
+
+ private static final @NonNull ListenableFuture<Empty> SHUTDOWN_FUTURE = Futures.immediateFuture(Empty.value());
+
+ private TCPClient(final TransportChannelListener listener) {
+ super(listener);
+ }
+
+ /**
+ * Attempt to establish a {@link TCPClient} by connecting to a remote address.
+ *
+ * @param listener {@link TransportChannelListener} to notify when the session is established
+ * @param bootstrap Client {@link Bootstrap} to use for the underlying Netty channel
+ * @param connectParams Connection parameters
+ * @return A future
+ * @throws UnsupportedConfigurationException when {@code connectParams} contains an unsupported options
+ * @throws NullPointerException if any argument is {@code null}
+ */
+ public static @NonNull ListenableFuture<TCPClient> connect(final TransportChannelListener listener,
+ final Bootstrap bootstrap, final TcpClientGrouping connectParams)
+ throws UnsupportedConfigurationException {
+ NettyTransportSupport.configureKeepalives(bootstrap, connectParams.getKeepalives());
+
+ final var ret = SettableFuture.<TCPClient>create();
+ bootstrap
+ .handler(ConnectChannelInitializer.INSTANCE)
+ .connect(
+ socketAddressOf(connectParams.requireRemoteAddress(), connectParams.requireRemotePort()),
+ socketAddressOf(connectParams.getLocalAddress(), connectParams.getLocalPort()))
+ .addListener((ChannelFutureListener) future -> {
+ if (future.isSuccess()) {
+ // Order of operations is important here: the stack should be visible before the underlying channel
+ final var stack = new TCPClient(listener);
+ ret.set(stack);
+ stack.addTransportChannel(new TCPTransportChannel(future.channel()));
+ } else {
+ ret.setException(future.cause());
+ }
+ });
+ return ret;
+ }
+
+ @Override
+ protected ListenableFuture<Empty> startShutdown() {
+ return SHUTDOWN_FUTURE;
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.tcp;
+
+import static com.google.common.base.Verify.verifyNotNull;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandler.Sharable;
+import io.netty.channel.ChannelInitializer;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.netconf.transport.api.TransportChannelListener;
+import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.server.rev220524.TcpServerGrouping;
+import org.opendaylight.yangtools.yang.common.Empty;
+
+/**
+ * A {@link TCPTransportStack} acting as a TCP server.
+ */
+public final class TCPServer extends TCPTransportStack {
+ @Sharable
+ private static final class ListenChannelInitializer extends ChannelInitializer<Channel> {
+ private final TCPServer stack;
+
+ ListenChannelInitializer(final TCPServer stack) {
+ this.stack = requireNonNull(stack);
+ }
+
+ @Override
+ protected void initChannel(final Channel ch) {
+ verifyNotNull(stack, "Stack not initialized while handling channel %s", ch)
+ .addTransportChannel(new TCPTransportChannel(ch));
+ }
+ }
+
+ private volatile Channel listenChannel;
+
+ private TCPServer(final TransportChannelListener listener) {
+ super(listener);
+ }
+
+ /**
+ * Attempt to establish a {@link TCPClient} by connecting to a remote address.
+ *
+ * @param listener {@link TransportChannelListener} to notify when the session is established
+ * @param bootstrap {@link ServerBootstrap} to use for the underlying Netty server channel
+ * @param listenParams Listening parameters
+ * @return A future
+ * @throws UnsupportedConfigurationException when {@code listenParams} contains an unsupported options
+ * @throws NullPointerException if any argument is {@code null}
+ */
+ public static @NonNull ListenableFuture<TCPServer> listen(final TransportChannelListener listener,
+ final ServerBootstrap bootstrap, final TcpServerGrouping listenParams)
+ throws UnsupportedConfigurationException {
+ NettyTransportSupport.configureKeepalives(bootstrap, listenParams.getKeepalives());
+
+ final var ret = SettableFuture.<TCPServer>create();
+ final var stack = new TCPServer(listener);
+ final var initializer = new ListenChannelInitializer(stack);
+ bootstrap
+ .childHandler(initializer)
+ .bind(socketAddressOf(listenParams.requireLocalAddress(), listenParams.requireLocalPort()))
+ .addListener((ChannelFutureListener) future -> {
+ if (future.isSuccess()) {
+ stack.setListenChannel(future.channel());
+ ret.set(stack);
+ } else {
+ ret.setException(future.cause());
+ }
+ });
+ return ret;
+ }
+
+ @Override
+ protected ListenableFuture<Empty> startShutdown() {
+ return toListenableFuture(listenChannel().close());
+ }
+
+ @VisibleForTesting
+ @NonNull Channel listenChannel() {
+ return verifyNotNull(listenChannel, "Channel not initialized");
+ }
+
+ private void setListenChannel(final Channel listenChannel) {
+ this.listenChannel = requireNonNull(listenChannel);
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.tcp;
+
+import static java.util.Objects.requireNonNull;
+
+import io.netty.channel.Channel;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.netconf.transport.api.TransportChannel;
+
+/**
+ * A TCP {@link TransportChannel}.
+ */
+@NonNullByDefault
+public final class TCPTransportChannel extends TransportChannel {
+ private final Channel channel;
+
+ TCPTransportChannel(final Channel channel) {
+ this.channel = requireNonNull(channel);
+ }
+
+ @Override
+ public Channel channel() {
+ return channel;
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.tcp;
+
+import java.net.InetSocketAddress;
+import org.opendaylight.netconf.transport.api.AbstractTransportStack;
+import org.opendaylight.netconf.transport.api.TransportChannelListener;
+import org.opendaylight.netconf.transport.api.TransportStack;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IetfInetUtil;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IpAddress;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber;
+
+/**
+ * Base class for TCP-based {@link TransportStack}s.
+ */
+public abstract sealed class TCPTransportStack extends AbstractTransportStack<TCPTransportChannel>
+ permits TCPClient, TCPServer {
+ TCPTransportStack(final TransportChannelListener listener) {
+ super(listener);
+ }
+
+ static final InetSocketAddress socketAddressOf(final Host host, final PortNumber port) {
+ final var addr = host.getIpAddress();
+ return addr != null ? socketAddressOf(addr, port)
+ : InetSocketAddress.createUnresolved(host.getDomainName().getValue(), port.getValue().toJava());
+ }
+
+ static final InetSocketAddress socketAddressOf(final IpAddress addr, final PortNumber port) {
+ final int portNum = port == null ? 0 : port.getValue().toJava();
+ if (addr == null) {
+ return port == null ? null : new InetSocketAddress(portNum);
+ }
+ return new InetSocketAddress(IetfInetUtil.INSTANCE.inetAddressFor(addr), portNum);
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+/**
+ * TCP transport layer. This does not constitute a {@code NETCONF Secure Transport}, but rather captures common aspects
+ * of TCP-based secure transports, such as SSH and TLS. Configuration follows
+ * <a href="https://datatracker.ietf.org/doc/html/draft-ietf-netconf-tcp-client-server-13">
+ * draft-ietf-netconf-tcp-client-server</a>.
+ *
+ * <p>
+ * While this is strictly not a secure transport, it provides a TransportStack and thus can be used as is, for example
+ * for testing purposes.
+ *
+ * <p>
+ * The three primary entry points into this package are {@link NettyTransportSupport}, {@link TCPClient} and
+ * {@link TCPServer}.
+ */
+package org.opendaylight.netconf.transport.tcp;
--- /dev/null
+module ietf-tcp-client {
+ yang-version 1.1;
+ namespace "urn:ietf:params:xml:ns:yang:ietf-tcp-client";
+ prefix tcpc;
+
+ import ietf-inet-types {
+ prefix inet;
+ reference
+ "RFC 6991: Common YANG Data Types";
+ }
+
+ import ietf-crypto-types {
+ prefix ct;
+ reference
+ "RFC AAAA: YANG Data Types and Groupings for Cryptography";
+ }
+
+ import ietf-tcp-common {
+ prefix tcpcmn;
+ reference
+ "RFC DDDD: YANG Groupings for TCP Clients and TCP Servers";
+ }
+
+ organization
+ "IETF NETCONF (Network Configuration) Working Group and the
+ IETF TCP Maintenance and Minor Extensions (TCPM) Working Group";
+
+ contact
+ "WG Web: https://datatracker.ietf.org/wg/netconf
+ https://datatracker.ietf.org/wg/tcpm
+ WG List: NETCONF WG list <mailto:netconf@ietf.org>
+ TCPM WG list <mailto:tcpm@ietf.org>
+ Authors: Kent Watsen <mailto:kent+ietf@watsen.net>
+ Michael Scharf
+ <mailto:michael.scharf@hs-esslingen.de>";
+
+ description
+ "This module defines reusable groupings for TCP clients that
+ can be used as a basis for specific TCP client instances.
+
+ Copyright (c) 2022 IETF Trust and the persons identified
+ as authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with
+ or without modification, is permitted pursuant to, and
+ subject to the license terms contained in, the Revised
+ BSD License set forth in Section 4.c of the IETF Trust's
+ Legal Provisions Relating to IETF Documents
+ (https://trustee.ietf.org/license-info).
+
+ This version of this YANG module is part of RFC DDDD
+ (https://www.rfc-editor.org/info/rfcDDDD); see the RFC
+ itself for full legal notices.
+
+ The key words 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL',
+ 'SHALL NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED',
+ 'NOT RECOMMENDED', 'MAY', and 'OPTIONAL' in this document
+ are to be interpreted as described in BCP 14 (RFC 2119)
+ (RFC 8174) when, and only when, they appear in all
+ capitals, as shown here.";
+
+ revision 2022-05-24 {
+ description
+ "Initial version";
+ reference
+ "RFC DDDD: YANG Groupings for TCP Clients and TCP Servers";
+ }
+
+ // Features
+
+ feature local-binding-supported {
+ description
+ "Indicates that the server supports configuring local
+ bindings (i.e., the local address and local port) for
+ TCP clients.";
+ }
+
+ feature tcp-client-keepalives {
+ description
+ "Per socket TCP keepalive parameters are configurable for
+ TCP clients on the server implementing this feature.";
+ }
+
+ feature proxy-connect {
+ description
+ "Proxy connection configuration is configurable for
+ TCP clients on the server implementing this feature.";
+ }
+
+ feature socks5-gss-api {
+ description
+ "Indicates that the server supports authenticating
+ using GSSAPI when initiating TCP connections via
+ and SOCKS Version 5 proxy server.";
+ reference
+ "RFC 1928: SOCKS Protocol Version 5";
+ }
+
+ feature socks5-username-password {
+ description
+ "Indicates that the server supports authenticating using
+ username/password when initiating TCP connections via
+ and SOCKS Version 5 proxy server.";
+ reference
+ "RFC 1928: SOCKS Protocol Version 5";
+ }
+
+ // Groupings
+
+ grouping tcp-client-grouping {
+ description
+ "A reusable grouping for configuring a TCP client.
+
+ Note that this grouping uses fairly typical descendant
+ node names such that a stack of 'uses' statements will
+ have name conflicts. It is intended that the consuming
+ data model will resolve the issue (e.g., by wrapping
+ the 'uses' statement in a container called
+ 'tcp-client-parameters'). This model purposely does
+ not do this itself so as to provide maximum flexibility
+ to consuming models.";
+
+ leaf remote-address {
+ type inet:host;
+ mandatory true;
+ description
+ "The IP address or hostname of the remote peer to
+ establish a connection with. If a domain name is
+ configured, then the DNS resolution should happen on
+ each connection attempt. If the DNS resolution
+ results in multiple IP addresses, the IP addresses
+ are tried according to local preference order until
+ a connection has been established or until all IP
+ addresses have failed.";
+ }
+ leaf remote-port {
+ type inet:port-number;
+ default "0";
+ description
+ "The IP port number for the remote peer to establish a
+ connection with. An invalid default value (0) is used
+ (instead of 'mandatory true') so that as application
+ level data model may 'refine' it with an application
+ specific default port number value.";
+ }
+ leaf local-address {
+ if-feature "local-binding-supported";
+ type inet:ip-address;
+ description
+ "The local IP address/interface (VRF?) to bind to for when
+ connecting to the remote peer. INADDR_ANY ('0.0.0.0') or
+ INADDR6_ANY ('0:0:0:0:0:0:0:0' a.k.a. '::') MAY be used to
+ explicitly indicate the implicit default, that the server
+ can bind to any IPv4 or IPv6 addresses, respectively.";
+ }
+ leaf local-port {
+ if-feature "local-binding-supported";
+ type inet:port-number;
+ default "0";
+ description
+ "The local IP port number to bind to for when connecting
+ to the remote peer. The port number '0', which is the
+ default value, indicates that any available local port
+ number may be used.";
+ }
+ container proxy-server {
+ if-feature "proxy-connect";
+ presence
+ "Indicates that a proxy connection has been configured.
+ Present so that the mandatory descendant nodes do not
+ imply that this node must be configured.";
+ choice proxy-type {
+ mandatory true;
+ description
+ "Selects a proxy connection protocol.";
+ case socks4 {
+ container socks4-parameters {
+ leaf remote-address {
+ type inet:ip-address;
+ mandatory true;
+ description
+ "The IP address of the proxy server.";
+ }
+ leaf remote-port {
+ type inet:port-number;
+ default "1080";
+ description
+ "The IP port number for the proxy server.";
+ }
+ description
+ "Parameters for connecting to a TCP-based proxy
+ server using the SOCKS4 protocol.";
+ reference
+ "SOCKS, Proceedings: 1992 Usenix Security Symposium.";
+ }
+ }
+ case socks4a {
+ container socks4a-parameters {
+ leaf remote-address {
+ type inet:host;
+ mandatory true;
+ description
+ "The IP address or hostname of the proxy server.";
+ }
+ leaf remote-port {
+ type inet:port-number;
+ default "1080";
+ description
+ "The IP port number for the proxy server.";
+ }
+ description
+ "Parameters for connecting to a TCP-based proxy
+ server using the SOCKS4a protocol.";
+ reference
+ "SOCKS Proceedings:
+ 1992 Usenix Security Symposium.
+ OpenSSH message:
+ SOCKS 4A: A Simple Extension to SOCKS 4 Protocol
+ https://www.openssh.com/txt/socks4a.protocol";
+ }
+ }
+ case socks5 {
+ container socks5-parameters {
+ leaf remote-address {
+ type inet:host;
+ mandatory true;
+ description
+ "The IP address or hostname of the proxy server.";
+ }
+ leaf remote-port {
+ type inet:port-number;
+ default "1080";
+ description
+ "The IP port number for the proxy server.";
+ }
+ container authentication-parameters {
+ presence
+ "Indicates that an authentication mechanism
+ has been configured. Present so that the
+ mandatory descendant nodes do not imply that
+ this node must be configured.";
+ description
+ "A container for SOCKS Version 5 authentication
+ mechanisms.
+
+ A complete list of methods is defined at:
+ https://www.iana.org/assignments/socks-methods
+ /socks-methods.xhtml.";
+ reference
+ "RFC 1928: SOCKS Protocol Version 5";
+ choice auth-type {
+ mandatory true;
+ description
+ "A choice amongst supported SOCKS Version 5
+ authentication mechanisms.";
+ case gss-api {
+ if-feature "socks5-gss-api";
+ container gss-api {
+ description
+ "Contains GSS-API configuration. Defines
+ as an empty container to enable specific
+ GSS-API configuration to be augmented in
+ by future modules.";
+ reference
+ "RFC 1928: SOCKS Protocol Version 5
+ RFC 2743: Generic Security Service
+ Application Program Interface
+ Version 2, Update 1";
+ }
+ }
+ case username-password {
+ if-feature "socks5-username-password";
+ container username-password {
+ leaf username {
+ type string;
+ mandatory true;
+ description
+ "The 'username' value to use for client
+ identification.";
+ }
+ uses ct:password-grouping {
+ description
+ "The password to be used for client
+ authentication.";
+ }
+ description
+ "Contains Username/Password configuration.";
+ reference
+ "RFC 1929: Username/Password Authentication
+ for SOCKS V5";
+ }
+ }
+ }
+ }
+ description
+ "Parameters for connecting to a TCP-based proxy server
+ using the SOCKS5 protocol.";
+ reference
+ "RFC 1928: SOCKS Protocol Version 5";
+ }
+ }
+ }
+ description
+ "Proxy server settings.";
+ }
+
+ uses tcpcmn:tcp-common-grouping {
+ augment "keepalives" {
+ if-feature "tcp-client-keepalives";
+ description
+ "Add an if-feature statement so that implementations
+ can choose to support TCP client keepalives.";
+ }
+ }
+ }
+}
--- /dev/null
+module ietf-tcp-common {
+ yang-version 1.1;
+ namespace "urn:ietf:params:xml:ns:yang:ietf-tcp-common";
+ prefix tcpcmn;
+
+ organization
+ "IETF NETCONF (Network Configuration) Working Group and the
+ IETF TCP Maintenance and Minor Extensions (TCPM) Working Group";
+
+ contact
+ "WG Web: https://datatracker.ietf.org/wg/netconf
+ https://datatracker.ietf.org/wg/tcpm
+ WG List: NETCONF WG list <mailto:netconf@ietf.org>
+ TCPM WG list <mailto:tcpm@ietf.org>
+ Authors: Kent Watsen <mailto:kent+ietf@watsen.net>
+ Michael Scharf
+ <mailto:michael.scharf@hs-esslingen.de>";
+
+ description
+ "This module defines reusable groupings for TCP commons that
+ can be used as a basis for specific TCP common instances.
+
+ Copyright (c) 2022 IETF Trust and the persons identified
+ as authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with
+ or without modification, is permitted pursuant to, and
+ subject to the license terms contained in, the Revised
+ BSD License set forth in Section 4.c of the IETF Trust's
+ Legal Provisions Relating to IETF Documents
+ (https://trustee.ietf.org/license-info).
+
+ This version of this YANG module is part of RFC DDDD
+ (https://www.rfc-editor.org/info/rfcDDDD); see the RFC
+ itself for full legal notices.
+
+ The key words 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL',
+ 'SHALL NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED',
+ 'NOT RECOMMENDED', 'MAY', and 'OPTIONAL' in this document
+ are to be interpreted as described in BCP 14 (RFC 2119)
+ (RFC 8174) when, and only when, they appear in all
+ capitals, as shown here.";
+
+ revision 2022-05-24 {
+ description
+ "Initial version";
+ reference
+ "RFC DDDD: YANG Groupings for TCP Clients and TCP Servers";
+ }
+
+ // Features
+
+ feature keepalives-supported {
+ description
+ "Indicates that keepalives are supported.";
+ }
+
+ // Groupings
+
+ grouping tcp-common-grouping {
+ description
+ "A reusable grouping for configuring TCP parameters common
+ to TCP connections as well as the operating system as a
+ whole.";
+ container keepalives {
+ if-feature "keepalives-supported";
+ presence
+ "Indicates that keepalives are enabled. This statement is
+ present so the mandatory descendant nodes do not imply that
+ this node must be configured.";
+ description
+ "Configures the keep-alive policy, to proactively test the
+ aliveness of the TCP peer. An unresponsive TCP peer is
+ dropped after approximately (idle-time + max-probes
+ * probe-interval) seconds.";
+ leaf idle-time {
+ type uint16 {
+ range "1..max";
+ }
+ units "seconds";
+ mandatory true;
+ description
+ "Sets the amount of time after which if no data has been
+ received from the TCP peer, a TCP-level probe message
+ will be sent to test the aliveness of the TCP peer.
+ Two hours (7200 seconds) is safe value, per RFC 1122.";
+ reference
+ "RFC 1122:
+ Requirements for Internet Hosts -- Communication Layers";
+ }
+ leaf max-probes {
+ type uint16 {
+ range "1..max";
+ }
+ mandatory true;
+ description
+ "Sets the maximum number of sequential keep-alive probes
+ that can fail to obtain a response from the TCP peer
+ before assuming the TCP peer is no longer alive.";
+ }
+ leaf probe-interval {
+ type uint16 {
+ range "1..max";
+ }
+ units "seconds";
+ mandatory true;
+ description
+ "Sets the time interval between failed probes. The interval
+ SHOULD be significantly longer than one second in order to
+ avoid harm on a congested link.";
+ }
+ } // container keepalives
+ } // grouping tcp-common-grouping
+
+}
--- /dev/null
+module ietf-tcp-server {
+ yang-version 1.1;
+ namespace "urn:ietf:params:xml:ns:yang:ietf-tcp-server";
+ prefix tcps;
+
+ import ietf-inet-types {
+ prefix inet;
+ reference
+ "RFC 6991: Common YANG Data Types";
+ }
+
+ import ietf-tcp-common {
+ prefix tcpcmn;
+ reference
+ "RFC DDDD: YANG Groupings for TCP Clients and TCP Servers";
+ }
+
+ organization
+ "IETF NETCONF (Network Configuration) Working Group and the
+ IETF TCP Maintenance and Minor Extensions (TCPM) Working Group";
+
+ contact
+ "WG Web: https://datatracker.ietf.org/wg/netconf
+ https://datatracker.ietf.org/wg/tcpm
+ WG List: NETCONF WG list <mailto:netconf@ietf.org>
+ TCPM WG list <mailto:tcpm@ietf.org>
+ Authors: Kent Watsen <mailto:kent+ietf@watsen.net>
+ Michael Scharf
+ <mailto:michael.scharf@hs-esslingen.de>";
+
+ description
+ "This module defines reusable groupings for TCP servers that
+ can be used as a basis for specific TCP server instances.
+
+ Copyright (c) 2022 IETF Trust and the persons identified
+ as authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with
+ or without modification, is permitted pursuant to, and
+ subject to the license terms contained in, the Revised
+ BSD License set forth in Section 4.c of the IETF Trust's
+ Legal Provisions Relating to IETF Documents
+ (https://trustee.ietf.org/license-info).
+
+ This version of this YANG module is part of RFC DDDD
+ (https://www.rfc-editor.org/info/rfcDDDD); see the RFC
+ itself for full legal notices.
+
+ The key words 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL',
+ 'SHALL NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED',
+ 'NOT RECOMMENDED', 'MAY', and 'OPTIONAL' in this document
+ are to be interpreted as described in BCP 14 (RFC 2119)
+ (RFC 8174) when, and only when, they appear in all
+ capitals, as shown here.";
+
+ revision 2022-05-24 {
+ description
+ "Initial version";
+ reference
+ "RFC DDDD: YANG Groupings for TCP Clients and TCP Servers";
+ }
+
+ // Features
+
+ feature tcp-server-keepalives {
+ description
+ "Per socket TCP keepalive parameters are configurable for
+ TCP servers on the server implementing this feature.";
+ }
+
+ // Groupings
+
+ grouping tcp-server-grouping {
+ description
+ "A reusable grouping for configuring a TCP server.
+
+ Note that this grouping uses fairly typical descendant
+ node names such that a stack of 'uses' statements will
+ have name conflicts. It is intended that the consuming
+ data model will resolve the issue (e.g., by wrapping
+ the 'uses' statement in a container called
+ 'tcp-server-parameters'). This model purposely does
+ not do this itself so as to provide maximum flexibility
+ to consuming models.";
+ leaf local-address {
+ type inet:ip-address;
+ mandatory true;
+ description
+ "The local IP address to listen on for incoming
+ TCP client connections. INADDR_ANY (0.0.0.0) or
+ INADDR6_ANY (0:0:0:0:0:0:0:0 a.k.a. ::) MUST be
+ used when the server is to listen on all IPv4 or
+ IPv6 addresses, respectively.";
+ }
+ leaf local-port {
+ type inet:port-number;
+ default "0";
+ description
+ "The local port number to listen on for incoming TCP
+ client connections. An invalid default value (0)
+ is used (instead of 'mandatory true') so that an
+ application level data model may 'refine' it with
+ an application specific default port number value.";
+ }
+ uses tcpcmn:tcp-common-grouping {
+ augment "keepalives" {
+ if-feature "tcp-server-keepalives";
+ description
+ "Add an if-feature statement so that implementations
+ can choose to support TCP server keepalives.";
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.tcp;
+
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.endsWith;
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.socket.ServerSocketChannel;
+import java.net.InetAddress;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.opendaylight.netconf.transport.api.TransportChannel;
+import org.opendaylight.netconf.transport.api.TransportChannelListener;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IetfInetUtil;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.client.rev220524.TcpClientGrouping;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.server.rev220524.TcpServerGrouping;
+import org.opendaylight.yangtools.yang.common.Uint16;
+
+@ExtendWith(MockitoExtension.class)
+public class TCPClientServerTest {
+ @Mock
+ private TcpClientGrouping clientGrouping;
+ @Mock
+ private TransportChannelListener clientListener;
+ @Mock
+ private TcpServerGrouping serverGrouping;
+ @Mock
+ private TransportChannelListener serverListener;
+
+ private static EventLoopGroup group;
+
+ @BeforeAll
+ public static void beforeAll() {
+ group = NettyTransportSupport.newEventLoopGroup("IntegrationTest");
+ }
+
+ @AfterAll
+ public static void afterAll() {
+ group.shutdownGracefully();
+ group = null;
+ }
+
+ @Test
+ public void integrationTest() throws Exception {
+ // localhost address, so we do not leak things around
+ final var loopbackAddr = IetfInetUtil.INSTANCE.ipAddressFor(InetAddress.getLoopbackAddress());
+
+ // Server-side config
+ doReturn(loopbackAddr).when(serverGrouping).getLocalAddress();
+ doCallRealMethod().when(serverGrouping).requireLocalAddress();
+ // note: this lets the server pick any port, we do not care
+ doReturn(new PortNumber(Uint16.ZERO)).when(serverGrouping).getLocalPort();
+ doCallRealMethod().when(serverGrouping).requireLocalPort();
+
+ // Spin up the server and acquire its port
+ final var server = TCPServer.listen(serverListener, NettyTransportSupport.newServerBootstrap().group(group),
+ serverGrouping).get(2, TimeUnit.SECONDS);
+ try {
+ assertEquals("TCPServer{listener=serverListener}", server.toString());
+
+ final var serverChannel = server.listenChannel();
+ assertInstanceOf(ServerSocketChannel.class, serverChannel);
+ final var serverPort = new PortNumber(
+ Uint16.valueOf(((ServerSocketChannel) serverChannel).localAddress().getPort()));
+
+ // Client-side config
+ doReturn(new Host(loopbackAddr)).when(clientGrouping).getRemoteAddress();
+ doCallRealMethod().when(clientGrouping).requireRemoteAddress();
+ doReturn(serverPort).when(clientGrouping).getRemotePort();
+ doCallRealMethod().when(clientGrouping).requireRemotePort();
+
+ // Capture client and server channels
+ final var serverCaptor = ArgumentCaptor.forClass(TransportChannel.class);
+ doNothing().when(serverListener).onTransportChannelEstablished(serverCaptor.capture());
+ final var clientCaptor = ArgumentCaptor.forClass(TransportChannel.class);
+ doNothing().when(clientListener).onTransportChannelEstablished(clientCaptor.capture());
+
+ final var client = TCPClient.connect(clientListener, NettyTransportSupport.newBootstrap().group(group),
+ clientGrouping).get(2, TimeUnit.SECONDS);
+ try {
+ assertThat(client.toString(), allOf(
+ startsWith("TCPClient{listener=clientListener, state=TCPTransportChannel{channel=[id: 0x"),
+ endsWith(":" + serverPort.getValue() + "]}}")));
+
+ verify(serverListener, timeout(500)).onTransportChannelEstablished(any());
+ final var serverTransports = serverCaptor.getAllValues();
+ assertEquals(1, serverTransports.size());
+ assertThat(serverTransports.get(0).toString(), allOf(
+ startsWith("TCPTransportChannel{channel=[id: "),
+ containsString(":" + serverPort.getValue() + " - R:")));
+
+ verify(clientListener, timeout(500)).onTransportChannelEstablished(any());
+ final var clientTransports = clientCaptor.getAllValues();
+ assertEquals(1, clientTransports.size());
+ assertThat(clientTransports.get(0).toString(), allOf(
+ startsWith("TCPTransportChannel{channel=[id: "),
+ endsWith(":" + serverPort.getValue() + "]}")));
+ } finally {
+ client.shutdown().get(2, TimeUnit.SECONDS);
+ }
+ } finally {
+ server.shutdown().get(2, TimeUnit.SECONDS);
+ }
+ }
+}