Add transport-{api,tcp} 24/102124/59
authorRobert Varga <robert.varga@pantheon.tech>
Wed, 10 Aug 2022 18:23:21 +0000 (20:23 +0200)
committerRobert Varga <nite@hq.sk>
Thu, 20 Oct 2022 14:36:02 +0000 (14:36 +0000)
NETCONF (and potentially RESTCONF) has rather well-defined concepts
how it works on the transport layer. Furthermore there are quite mature
YANG models covering the configuration of both NETCONF server and
NETCONF client.

This patch introduces the transport-level APIs to establish a transport
stack and its interactions with protocol negotiation and then the
messages layer. A baseline implementation based on plain TCP is also
provided.

JIRA: NETCONF-590
Change-Id: I29c207a2036e1c54ea1f6d7fe48f7eb42a3a4a7d
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
29 files changed:
artifacts/pom.xml
pom.xml
transport/pom.xml [new file with mode: 0644]
transport/transport-api/pom.xml [new file with mode: 0644]
transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/AbstractOverlayTransportChannel.java [new file with mode: 0644]
transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/AbstractOverlayTransportStack.java [new file with mode: 0644]
transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/AbstractTransportStack.java [new file with mode: 0644]
transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/TransportChannel.java [new file with mode: 0644]
transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/TransportChannelListener.java [new file with mode: 0644]
transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/TransportStack.java [new file with mode: 0644]
transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/UnsupportedConfigurationException.java [new file with mode: 0644]
transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/package-info.java [new file with mode: 0644]
transport/transport-tcp/pom.xml [new file with mode: 0644]
transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/AbstractNettyImpl.java [new file with mode: 0644]
transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/EpollNettyImpl.java [new file with mode: 0644]
transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/IetfTcpClientFeatureProvider.java [new file with mode: 0644]
transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/IetfTcpCommonFeatureProvider.java [new file with mode: 0644]
transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/IetfTcpServerFeatureProvider.java [new file with mode: 0644]
transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/NettyTransportSupport.java [new file with mode: 0644]
transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/NioNettyImpl.java [new file with mode: 0644]
transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/TCPClient.java [new file with mode: 0644]
transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/TCPServer.java [new file with mode: 0644]
transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/TCPTransportChannel.java [new file with mode: 0644]
transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/TCPTransportStack.java [new file with mode: 0644]
transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/package-info.java [new file with mode: 0644]
transport/transport-tcp/src/main/yang/ietf-tcp-client@2022-05-24.yang [new file with mode: 0644]
transport/transport-tcp/src/main/yang/ietf-tcp-common@2022-05-24.yang [new file with mode: 0644]
transport/transport-tcp/src/main/yang/ietf-tcp-server@2022-05-24.yang [new file with mode: 0644]
transport/transport-tcp/src/test/java/org/opendaylight/netconf/transport/tcp/TCPClientServerTest.java [new file with mode: 0644]

index c1b1bd53026dece064da926a305cd16a66c7faa7..9d4c4f0985a5a92bb6803f86a208ea9965c3c52c 100644 (file)
                 <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>
diff --git a/pom.xml b/pom.xml
index bfd0b81f306fc3afb9673b874a6a8cb42cd429d0..99e3e0779f74daedfe5c7674836b95003ccec799 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -35,6 +35,9 @@
         <module>karaf</module>
         <module>karaf-static</module>
         <module>model</module>
+        <module>transport</module>
+
+        <!-- Legacy layout -->
         <module>netconf</module>
         <module>restconf</module>
     </modules>
diff --git a/transport/pom.xml b/transport/pom.xml
new file mode 100644 (file)
index 0000000..472435a
--- /dev/null
@@ -0,0 +1,35 @@
+<?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>
diff --git a/transport/transport-api/pom.xml b/transport/transport-api/pom.xml
new file mode 100644 (file)
index 0000000..5e5b9e1
--- /dev/null
@@ -0,0 +1,38 @@
+<?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>
diff --git a/transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/AbstractOverlayTransportChannel.java b/transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/AbstractOverlayTransportChannel.java
new file mode 100644 (file)
index 0000000..de7d1dd
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * 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);
+    }
+}
diff --git a/transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/AbstractOverlayTransportStack.java b/transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/AbstractOverlayTransportStack.java
new file mode 100644 (file)
index 0000000..59eca14
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * 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());
+    }
+}
diff --git a/transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/AbstractTransportStack.java b/transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/AbstractTransportStack.java
new file mode 100644 (file)
index 0000000..7234b37
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+ * 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);
+        }
+    }
+}
diff --git a/transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/TransportChannel.java b/transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/TransportChannel.java
new file mode 100644 (file)
index 0000000..343d197
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * 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());
+    }
+}
diff --git a/transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/TransportChannelListener.java b/transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/TransportChannelListener.java
new file mode 100644 (file)
index 0000000..759becd
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * 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);
+}
diff --git a/transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/TransportStack.java b/transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/TransportStack.java
new file mode 100644 (file)
index 0000000..1d22130
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * 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();
+}
diff --git a/transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/UnsupportedConfigurationException.java b/transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/UnsupportedConfigurationException.java
new file mode 100644 (file)
index 0000000..d25c8e3
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * 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);
+    }
+}
diff --git a/transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/package-info.java b/transport/transport-api/src/main/java/org/opendaylight/netconf/transport/api/package-info.java
new file mode 100644 (file)
index 0000000..a399d79
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * 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
diff --git a/transport/transport-tcp/pom.xml b/transport/transport-tcp/pom.xml
new file mode 100644 (file)
index 0000000..347a300
--- /dev/null
@@ -0,0 +1,70 @@
+<?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>
diff --git a/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/AbstractNettyImpl.java b/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/AbstractNettyImpl.java
new file mode 100644 (file)
index 0000000..6e29fe5
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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
diff --git a/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/EpollNettyImpl.java b/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/EpollNettyImpl.java
new file mode 100644 (file)
index 0000000..66a7dd2
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * 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
diff --git a/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/IetfTcpClientFeatureProvider.java b/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/IetfTcpClientFeatureProvider.java
new file mode 100644 (file)
index 0000000..c31dbe2
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * 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);
+    }
+}
diff --git a/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/IetfTcpCommonFeatureProvider.java b/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/IetfTcpCommonFeatureProvider.java
new file mode 100644 (file)
index 0000000..bcdf2bb
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * 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);
+    }
+}
diff --git a/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/IetfTcpServerFeatureProvider.java b/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/IetfTcpServerFeatureProvider.java
new file mode 100644 (file)
index 0000000..322708f
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * 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();
+    }
+}
diff --git a/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/NettyTransportSupport.java b/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/NettyTransportSupport.java
new file mode 100644 (file)
index 0000000..995e6a6
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * 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");
+        }
+    }
+}
diff --git a/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/NioNettyImpl.java b/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/NioNettyImpl.java
new file mode 100644 (file)
index 0000000..58777c6
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * 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
diff --git a/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/TCPClient.java b/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/TCPClient.java
new file mode 100644 (file)
index 0000000..5a872aa
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * 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
diff --git a/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/TCPServer.java b/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/TCPServer.java
new file mode 100644 (file)
index 0000000..a665420
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * 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);
+    }
+}
diff --git a/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/TCPTransportChannel.java b/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/TCPTransportChannel.java
new file mode 100644 (file)
index 0000000..636a03b
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * 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;
+    }
+}
diff --git a/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/TCPTransportStack.java b/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/TCPTransportStack.java
new file mode 100644 (file)
index 0000000..1bf28ba
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * 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);
+    }
+}
diff --git a/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/package-info.java b/transport/transport-tcp/src/main/java/org/opendaylight/netconf/transport/tcp/package-info.java
new file mode 100644 (file)
index 0000000..306d220
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * 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;
diff --git a/transport/transport-tcp/src/main/yang/ietf-tcp-client@2022-05-24.yang b/transport/transport-tcp/src/main/yang/ietf-tcp-client@2022-05-24.yang
new file mode 100644 (file)
index 0000000..4426353
--- /dev/null
@@ -0,0 +1,316 @@
+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.";
+      }
+    }
+  }
+}
diff --git a/transport/transport-tcp/src/main/yang/ietf-tcp-common@2022-05-24.yang b/transport/transport-tcp/src/main/yang/ietf-tcp-common@2022-05-24.yang
new file mode 100644 (file)
index 0000000..e9a927d
--- /dev/null
@@ -0,0 +1,115 @@
+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
+
+}
diff --git a/transport/transport-tcp/src/main/yang/ietf-tcp-server@2022-05-24.yang b/transport/transport-tcp/src/main/yang/ietf-tcp-server@2022-05-24.yang
new file mode 100644 (file)
index 0000000..b465dfe
--- /dev/null
@@ -0,0 +1,114 @@
+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.";
+      }
+    }
+  }
+}
diff --git a/transport/transport-tcp/src/test/java/org/opendaylight/netconf/transport/tcp/TCPClientServerTest.java b/transport/transport-tcp/src/test/java/org/opendaylight/netconf/transport/tcp/TCPClientServerTest.java
new file mode 100644 (file)
index 0000000..54767e6
--- /dev/null
@@ -0,0 +1,130 @@
+/*
+ * 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);
+        }
+    }
+}