New transport implementation makes NetconfServerDispatcher outdated.
NetconfServerFactory interface and implementation introduced
as replacement. Netconf subsystem factory added to serve netconf
over ssh.
JIRA: NETCONF-1106
Change-Id: Id74cf3511c1129710b231869c089b4339333d7e8
Signed-off-by: Ruslan Kashapov <ruslan.kashapov@pantheon.tech>
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
<groupId>io.netty</groupId>
<artifactId>netty-codec</artifactId>
</dependency>
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-buffer</artifactId>
+ </dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-common</artifactId>
<groupId>org.opendaylight.yangtools</groupId>
<artifactId>yang-common</artifactId>
</dependency>
- <dependency>
- <groupId>org.opendaylight.yangtools</groupId>
- <artifactId>yang-model-api</artifactId>
- </dependency>
<dependency>
<groupId>org.opendaylight.mdsal.binding.model.ietf</groupId>
<artifactId>rfc6991-ietf-inet-types</artifactId>
<artifactId>netconf-netty-util</artifactId>
</dependency>
<dependency>
- <groupId>com.guicedee.services</groupId>
- <artifactId>javax.inject</artifactId>
- <optional>true</optional>
- </dependency>
- <dependency>
- <groupId>org.osgi</groupId>
- <artifactId>org.osgi.service.component.annotations</artifactId>
+ <groupId>org.opendaylight.netconf</groupId>
+ <artifactId>shaded-sshd</artifactId>
</dependency>
<dependency>
<groupId>org.opendaylight.netconf</groupId>
<groupId>org.opendaylight.netconf</groupId>
<artifactId>transport-ssh</artifactId>
</dependency>
- <!-- Test dependencies -->
<dependency>
- <groupId>io.netty</groupId>
- <artifactId>netty-buffer</artifactId>
- <scope>test</scope>
+ <groupId>com.guicedee.services</groupId>
+ <artifactId>javax.inject</artifactId>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.service.component.annotations</artifactId>
</dependency>
+
+ <!-- Test dependencies -->
<dependency>
<groupId>org.opendaylight.yangtools</groupId>
<artifactId>mockito-configuration</artifactId>
<groupId>org.opendaylight.netconf</groupId>
<artifactId>netconf-test-util</artifactId>
</dependency>
- <dependency>
- <groupId>org.opendaylight.netconf</groupId>
- <artifactId>shaded-sshd</artifactId>
- <scope>test</scope>
- </dependency>
<dependency>
<groupId>org.xmlunit</groupId>
<artifactId>xmlunit-legacy</artifactId>
import org.opendaylight.netconf.nettyutil.AbstractNetconfDispatcher;
import org.opendaylight.netconf.server.api.NetconfServerDispatcher;
+@Deprecated(since = "7.0.0", forRemoval = true)
public class NetconfServerDispatcherImpl extends AbstractNetconfDispatcher<NetconfServerSession,
NetconfServerSessionListener> implements NetconfServerDispatcher {
private final ServerChannelInitializer initializer;
--- /dev/null
+/*
+ * Copyright (c) 2023 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.server;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.EventLoopGroup;
+import io.netty.util.concurrent.DefaultPromise;
+import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.concurrent.GlobalEventExecutor;
+import java.util.List;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.netconf.server.api.NetconfServerFactory;
+import org.opendaylight.netconf.shaded.sshd.server.ServerFactoryManager;
+import org.opendaylight.netconf.shaded.sshd.server.SshServer;
+import org.opendaylight.netconf.transport.api.TransportChannel;
+import org.opendaylight.netconf.transport.api.TransportChannelListener;
+import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
+import org.opendaylight.netconf.transport.ssh.SSHServer;
+import org.opendaylight.netconf.transport.ssh.ServerFactoryManagerConfigurator;
+import org.opendaylight.netconf.transport.tcp.NettyTransportSupport;
+import org.opendaylight.netconf.transport.tcp.TCPServer;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.SshServerGrouping;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.server.rev230417.TcpServerGrouping;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class NetconfServerFactoryImpl implements NetconfServerFactory {
+ private static final Logger LOG = LoggerFactory.getLogger(NetconfServerFactoryImpl.class);
+ private static final TransportChannelListener EMPTY_LISTENER = new ChannelInitializerListener(null, null);
+
+ private final EventLoopGroup parentGroup;
+ private final EventLoopGroup workerGroup;
+ private final ServerChannelInitializer channelInitializer;
+ private final TransportChannelListener transportChannelListener;
+
+ public NetconfServerFactoryImpl(final ServerChannelInitializer channelInitializer, final EventLoopGroup bossGroup,
+ final EventLoopGroup workerGroup) {
+ this(channelInitializer, bossGroup, workerGroup, GlobalEventExecutor.INSTANCE);
+ }
+
+ public NetconfServerFactoryImpl(final ServerChannelInitializer channelInitializer,
+ final EventLoopGroup parentGroup, final EventLoopGroup workerGroup, final EventExecutor executor) {
+ this.parentGroup = requireNonNull(parentGroup);
+ this.workerGroup = requireNonNull(workerGroup);
+ this.channelInitializer = channelInitializer;
+ transportChannelListener = new ChannelInitializerListener(channelInitializer, executor);
+ }
+
+ @NonNull protected ServerBootstrap createBootstrap() {
+ return NettyTransportSupport.newServerBootstrap().group(parentGroup, workerGroup);
+ }
+
+ @Override
+ public ListenableFuture<TCPServer> createTcpServer(final TcpServerGrouping params)
+ throws UnsupportedConfigurationException {
+ return TCPServer.listen(transportChannelListener, createBootstrap(), params);
+ }
+
+ @Override
+ public ListenableFuture<SSHServer> createSshServer(final TcpServerGrouping tcpParams,
+ final SshServerGrouping sshParams) throws UnsupportedConfigurationException {
+ return SSHServer.listen(EMPTY_LISTENER, createBootstrap(), tcpParams, sshParams);
+ }
+
+ @Override
+ public ListenableFuture<SSHServer> createSshServer(final TcpServerGrouping tcpParams,
+ final SshServerGrouping sshParams, final ServerFactoryManagerConfigurator configurator)
+ throws UnsupportedConfigurationException {
+ return SSHServer.listen(EMPTY_LISTENER, createBootstrap(), tcpParams, sshParams,
+ new FactoryManagerConfigurator(channelInitializer, configurator));
+ }
+
+ private record ChannelInitializerListener(
+ @Nullable ServerChannelInitializer channelInitializer,
+ @Nullable EventExecutor executor) implements TransportChannelListener {
+ @Override
+ public void onTransportChannelEstablished(final TransportChannel channel) {
+ LOG.debug("Transport channel {} established", channel);
+ if (channelInitializer != null && executor != null) {
+ channelInitializer.initialize(channel.channel(), new DefaultPromise<>(executor));
+ }
+ }
+
+ @Override
+ public void onTransportChannelFailed(final Throwable cause) {
+ LOG.error("Transport channel failed", cause);
+ }
+ }
+
+ private record FactoryManagerConfigurator(
+ @NonNull ServerChannelInitializer channelInitializer,
+ @Nullable ServerFactoryManagerConfigurator configurator) implements ServerFactoryManagerConfigurator {
+ FactoryManagerConfigurator {
+ requireNonNull(channelInitializer);
+ }
+
+ @Override
+ public void configureServerFactoryManager(final ServerFactoryManager factoryManager)
+ throws UnsupportedConfigurationException {
+ if (configurator != null) {
+ configurator.configureServerFactoryManager(factoryManager);
+ }
+ if (factoryManager instanceof SshServer server) {
+ server.setSubsystemFactories(List.of(new NetconfSubsystemFactory(channelInitializer)));
+ }
+ }
+ }
+}
final NetconfSessionListenerFactory<NetconfServerSessionListener> defunctSessionListenerFactory,
final Channel channel, final Promise<NetconfServerSession> promise) {
final var sessionId = idProvider.getNextSessionId();
+ final var socketAddress = channel.parent() == null ? null : channel.parent().localAddress();
return new NetconfServerSessionNegotiator(createHelloMessage(sessionId, monitoringService), sessionId, promise,
- channel, timer, getListener(sessionId, channel.parent().localAddress()),
- connectionTimeoutMillis, maximumIncomingChunkSize);
+ channel, timer, getListener(sessionId, socketAddress), connectionTimeoutMillis, maximumIncomingChunkSize);
}
private NetconfServerSessionListener getListener(final SessionIdType sessionId, final SocketAddress socketAddress) {
--- /dev/null
+/*
+ * Copyright (c) 2023 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.server;
+
+import static java.util.Objects.requireNonNull;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.util.concurrent.DefaultPromise;
+import io.netty.util.concurrent.GlobalEventExecutor;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import org.opendaylight.netconf.api.messages.NetconfHelloMessageAdditionalHeader;
+import org.opendaylight.netconf.shaded.sshd.common.io.IoInputStream;
+import org.opendaylight.netconf.shaded.sshd.common.io.IoOutputStream;
+import org.opendaylight.netconf.shaded.sshd.common.util.buffer.ByteArrayBuffer;
+import org.opendaylight.netconf.shaded.sshd.server.channel.ChannelDataReceiver;
+import org.opendaylight.netconf.shaded.sshd.server.channel.ChannelSession;
+import org.opendaylight.netconf.shaded.sshd.server.channel.ChannelSessionAware;
+import org.opendaylight.netconf.shaded.sshd.server.command.AbstractCommandSupport;
+import org.opendaylight.netconf.shaded.sshd.server.command.AsyncCommand;
+import org.opendaylight.netconf.shaded.sshd.server.command.Command;
+import org.opendaylight.netconf.shaded.sshd.server.subsystem.SubsystemFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class NetconfSubsystemFactory implements SubsystemFactory {
+ private static final String NETCONF = "netconf";
+
+ private final ServerChannelInitializer channelInitializer;
+
+ public NetconfSubsystemFactory(final ServerChannelInitializer channelInitializer) {
+ this.channelInitializer = requireNonNull(channelInitializer);
+ }
+
+ @Override
+ public String getName() {
+ return NETCONF;
+ }
+
+ @Override
+ public Command createSubsystem(ChannelSession channel) throws IOException {
+ return new NetconfSubsystem(channelInitializer);
+ }
+
+ private static class NetconfSubsystem extends AbstractCommandSupport implements AsyncCommand, ChannelSessionAware {
+ private static final Logger LOG = LoggerFactory.getLogger(NetconfSubsystem.class);
+
+ private final ServerChannelInitializer channelInitializer;
+ private Channel innerChannel;
+ private IoOutputStream ioOutputStream;
+ private ChannelSession channelSession;
+
+ NetconfSubsystem(final ServerChannelInitializer channelInitializer) {
+ super(NETCONF, null);
+ this.channelInitializer = channelInitializer;
+ }
+
+ @Override
+ public void setIoInputStream(final IoInputStream in) {
+ // not used
+ }
+
+ @Override
+ public void setIoOutputStream(final IoOutputStream out) {
+ this.ioOutputStream = out;
+ }
+
+ @Override
+ public void setIoErrorStream(final IoOutputStream err) {
+ // not used
+ }
+
+ @Override
+ public void setChannelSession(final ChannelSession channelSession) {
+ this.channelSession = channelSession;
+ }
+
+ @Override
+ public void run() {
+
+ /*
+ * While NETCONF protocol handlers are designed to operate over Netty channel,
+ * the inner channel is used to serve NETCONF over SSH.
+ */
+
+ final var embeddedChannel = new EmbeddedChannel() {
+ @Override
+ protected void handleOutboundMessage(final Object msg) {
+ if (msg instanceof ByteBuf byteBuf) {
+ // redirect channel outgoing packets to output stream linked to transport
+ final byte[] bytes = new byte[byteBuf.readableBytes()];
+ byteBuf.readBytes(bytes);
+ try {
+ ioOutputStream.writeBuffer(new ByteArrayBuffer(bytes))
+ .addListener(future -> {
+ if (future.isWritten()) {
+ byteBuf.release(); // report outbound message being handled
+ } else if (future.getException() != null) {
+ LOG.debug("Error writing buffer", future.getException());
+ }
+ });
+ } catch (IOException e) {
+ LOG.error("Error writing buffer", e);
+ }
+ } else {
+ // non-ByteBuf messages are persisted within channel for subsequent handling
+ super.handleOutboundMessage(msg);
+ }
+ }
+ };
+
+ this.innerChannel = embeddedChannel;
+
+ // inbound packets handler
+ channelSession.setDataReceiver(new ChannelDataReceiver() {
+ @Override
+ public int data(ChannelSession channel, byte[] buf, int start, int len) throws IOException {
+ embeddedChannel.writeInbound(Unpooled.copiedBuffer(buf, start, len));
+ return len;
+ }
+
+ @Override
+ public void close() throws IOException {
+ embeddedChannel.close();
+ }
+ });
+
+ // inner channel termination handler
+ embeddedChannel.pipeline().addFirst(
+ new ChannelInboundHandlerAdapter() {
+ @Override
+ public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+ onExit(0);
+ }
+ }
+ );
+
+ // NETCONF handlers
+ channelInitializer.initialize(embeddedChannel, new DefaultPromise<>(GlobalEventExecutor.INSTANCE));
+ // trigger negotiation flow
+ embeddedChannel.pipeline().fireChannelActive();
+ // set additional info for netconf session
+ embeddedChannel.writeInbound(Unpooled.wrappedBuffer(getHelloAdditionalMessageBytes()));
+ }
+
+ @Override
+ protected void onExit(int exitValue, String exitMessage) {
+ super.onExit(exitValue, exitMessage);
+ if (innerChannel != null) {
+ innerChannel.close();
+ }
+ }
+
+ private byte[] getHelloAdditionalMessageBytes() {
+ final var session = getServerSession();
+ final var address = (InetSocketAddress) session.getClientAddress();
+ final var header = new NetconfHelloMessageAdditionalHeader(
+ session.getUsername(),
+ address.getAddress().getHostAddress(),
+ String.valueOf(address.getPort()),
+ "ssh", "client").toFormattedString();
+ return header.getBytes(StandardCharsets.UTF_8);
+ }
+ }
+}
import io.netty.channel.local.LocalAddress;
import java.net.InetSocketAddress;
+/**
+ * Basic interface for Netconf server dispatcher.
+ * @deprecated Due to design change. Use {@link NetconfServerFactory} to instantiate Netconf server.
+ */
+@Deprecated(since = "7.0.0", forRemoval = true)
public interface NetconfServerDispatcher {
ChannelFuture createServer(InetSocketAddress address);
--- /dev/null
+/*
+ * Copyright (c) 2023 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.server.api;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
+import org.opendaylight.netconf.transport.ssh.SSHServer;
+import org.opendaylight.netconf.transport.ssh.ServerFactoryManagerConfigurator;
+import org.opendaylight.netconf.transport.tcp.TCPServer;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.SshServerGrouping;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.server.rev230417.TcpServerGrouping;
+
+/**
+ * Basic interface for Netconf server factory.
+ */
+public interface NetconfServerFactory {
+ /**
+ * Build Netconf server operating over TCP transport.
+ *
+ * @param params - TCP transport configuration
+ * @return server instance as future
+ * @throws UnsupportedConfigurationException if server cannot be started using given configuration
+ * @throws NullPointerException if params is null
+ */
+ ListenableFuture<TCPServer> createTcpServer(TcpServerGrouping params) throws UnsupportedConfigurationException;
+
+ /**
+ * Build SSH Netconf server.
+ *
+ * @param tcpParams TCP transport configuration
+ * @param sshParams SSH overlay configuration
+ * @return server instance as future
+ * @throws UnsupportedConfigurationException if server cannot be started using given configuration
+ * @throws NullPointerException if either tcpParams or sshParams is null
+ */
+ ListenableFuture<SSHServer> createSshServer(TcpServerGrouping tcpParams, SshServerGrouping sshParams)
+ throws UnsupportedConfigurationException;
+
+ /**
+ * Build SSH Netconf server with integration support.
+ *
+ * @param tcpParams TCP transport configuration
+ * @param sshParams SSH overlay configuration
+ * @param configurator explicit server factory configurator (if defined sshParams became optional)
+ * @return server instance as future
+ * @throws UnsupportedConfigurationException if server cannot be started using given configuration
+ * @throws NullPointerException if tcpParams is null
+ * @throws IllegalArgumentException if both sshParams and configurator are null, at least one is expected
+ */
+ ListenableFuture<SSHServer> createSshServer(TcpServerGrouping tcpParams, SshServerGrouping sshParams,
+ ServerFactoryManagerConfigurator configurator) throws UnsupportedConfigurationException;
+}
--- /dev/null
+/*
+ * Copyright (c) 2023 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.server;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import io.netty.channel.Channel;
+import io.netty.channel.EventLoopGroup;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.security.KeyPairGenerator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.opendaylight.netconf.server.api.NetconfServerFactory;
+import org.opendaylight.netconf.shaded.sshd.server.auth.password.UserAuthPasswordFactory;
+import org.opendaylight.netconf.shaded.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
+import org.opendaylight.netconf.transport.api.TransportChannel;
+import org.opendaylight.netconf.transport.api.TransportChannelListener;
+import org.opendaylight.netconf.transport.ssh.SSHClient;
+import org.opendaylight.netconf.transport.ssh.SSHServer;
+import org.opendaylight.netconf.transport.tcp.NettyTransportSupport;
+import org.opendaylight.netconf.transport.tcp.TCPClient;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.iana.crypt.hash.rev140806.CryptHash;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.RsaPrivateKeyFormat;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.SubjectPublicKeyInfoFormat;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.asymmetric.key.pair.grouping._private.key.type.CleartextPrivateKeyBuilder;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.crypto.types.rev230417.password.grouping.password.type.CleartextPasswordBuilder;
+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.keystore.rev230417.inline.or.keystore.asymmetric.key.grouping.inline.or.keystore.InlineBuilder;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.keystore.rev230417.inline.or.keystore.asymmetric.key.grouping.inline.or.keystore.inline.InlineDefinitionBuilder;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev230417.netconf.client.initiate.stack.grouping.transport.ssh.ssh.SshClientParameters;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev230417.netconf.client.initiate.stack.grouping.transport.ssh.ssh.SshClientParametersBuilder;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev230417.netconf.client.initiate.stack.grouping.transport.ssh.ssh.TcpClientParametersBuilder;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.server.rev230417.netconf.server.listen.stack.grouping.transport.ssh.ssh.SshServerParametersBuilder;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.server.rev230417.netconf.server.listen.stack.grouping.transport.ssh.ssh.TcpServerParametersBuilder;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.ClientIdentityBuilder;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev230417.ssh.client.grouping.client.identity.PasswordBuilder;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.ClientAuthenticationBuilder;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.ServerIdentity;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.ServerIdentityBuilder;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.client.authentication.UsersBuilder;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.client.authentication.users.UserBuilder;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.server.identity.HostKeyBuilder;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417.ssh.server.grouping.server.identity.host.key.host.key.type.PublicKeyBuilder;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.client.rev230417.TcpClientGrouping;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.server.rev230417.TcpServerGrouping;
+import org.opendaylight.yangtools.yang.common.Uint16;
+
+@ExtendWith(MockitoExtension.class)
+class NetconfServerFactoryImplTest {
+
+ private static final String USERNAME = "username";
+ private static final String PASSWORD = "pa$$w0rd";
+ private static final String RSA = "RSA";
+
+ private static EventLoopGroup parentGroup;
+ private static EventLoopGroup workerGroup;
+ private static EventLoopGroup clientGroup;
+ @Mock
+ private ServerChannelInitializer serverChannelInitializer;
+ @Mock
+ private TransportChannelListener clientListener;
+
+ private NetconfServerFactory factory;
+ private TcpServerGrouping tcpServerParams;
+ private TcpClientGrouping tcpClientParams;
+
+ @BeforeAll
+ static void beforeAll() {
+ parentGroup = NettyTransportSupport.newEventLoopGroup("parent");
+ workerGroup = NettyTransportSupport.newEventLoopGroup("worker");
+ clientGroup = NettyTransportSupport.newEventLoopGroup("client");
+ }
+
+ @AfterAll
+ static void afterAll() {
+ parentGroup.shutdownGracefully();
+ workerGroup.shutdownGracefully();
+ clientGroup.shutdownGracefully();
+ }
+
+ @BeforeEach
+ void beforeEach() throws Exception {
+ factory = new NetconfServerFactoryImpl(serverChannelInitializer, parentGroup, workerGroup);
+
+ // create temp socket to get available port for test
+ final var socket = new ServerSocket(0);
+ final var address = IetfInetUtil.ipAddressFor(InetAddress.getLoopbackAddress());
+ final var port = new PortNumber(Uint16.valueOf(socket.getLocalPort()));
+ socket.close();
+
+ tcpServerParams = new TcpServerParametersBuilder().setLocalAddress(address).setLocalPort(port).build();
+ tcpClientParams =
+ new TcpClientParametersBuilder().setRemoteAddress(new Host(address)).setRemotePort(port).build();
+ }
+
+ @Test
+ void tcpServer() throws Exception {
+ final var server = factory.createTcpServer(tcpServerParams).get(1, TimeUnit.SECONDS);
+ try {
+ final var client = TCPClient.connect(clientListener,
+ NettyTransportSupport.newBootstrap().group(clientGroup), tcpClientParams).get(1, TimeUnit.SECONDS);
+ try {
+ verify(serverChannelInitializer, timeout(1000L)).initialize(any(Channel.class), any());
+ verify(clientListener, timeout(1000L)).onTransportChannelEstablished(any(TransportChannel.class));
+ } finally {
+ client.shutdown().get(1, TimeUnit.SECONDS);
+ }
+ } finally {
+ server.shutdown().get(1, TimeUnit.SECONDS);
+ }
+ }
+
+ @Test
+ void sshServer() throws Exception {
+ final var user = new UserBuilder().setName(USERNAME).setPassword(new CryptHash("$0$" + PASSWORD)).build();
+ final var sshServerConfig = new SshServerParametersBuilder()
+ .setServerIdentity(buildSshServerIdentityWithKeyPair())
+ .setClientAuthentication(
+ new ClientAuthenticationBuilder().setUsers(
+ new UsersBuilder().setUser(Map.of(user.key(), user)).build()
+ ).build()
+ ).build();
+
+ assertSshServer(factory.createSshServer(tcpServerParams, sshServerConfig), sshClientParams());
+ }
+
+ @Test
+ void sshServerExtInitializer() throws Exception {
+ assertSshServer(factory.createSshServer(tcpServerParams, null, factoryManager -> {
+ factoryManager.setUserAuthFactories(List.of(new UserAuthPasswordFactory()));
+ factoryManager.setPasswordAuthenticator((username, password, session) -> true);
+ factoryManager.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());
+ }), sshClientParams());
+ }
+
+ void assertSshServer(final ListenableFuture<SSHServer> serverFuture, final SshClientParameters sshClientParams)
+ throws Exception {
+ final var server = serverFuture.get(2, TimeUnit.SECONDS);
+ try {
+ final var client = SSHClient.connect(clientListener,
+ NettyTransportSupport.newBootstrap().group(clientGroup), tcpClientParams, sshClientParams)
+ .get(2, TimeUnit.SECONDS);
+ try {
+ // FIXME commented line requires netconf client to trigger netconf subsystem initialization on server
+ // verify(serverChannelInitializer, timeout(10_000L)).initialize(any(Channel.class), any());
+ verify(clientListener, timeout(10_000L)).onTransportChannelEstablished(any(TransportChannel.class));
+ } finally {
+ client.shutdown().get(2, TimeUnit.SECONDS);
+ }
+ } finally {
+ server.shutdown().get(2, TimeUnit.SECONDS);
+ }
+ }
+
+ private static ServerIdentity buildSshServerIdentityWithKeyPair() throws Exception {
+ final var keyPair = KeyPairGenerator.getInstance(RSA).generateKeyPair();
+ final var inlineDef = new InlineDefinitionBuilder()
+ .setPublicKeyFormat(SubjectPublicKeyInfoFormat.VALUE)
+ .setPublicKey(keyPair.getPublic().getEncoded())
+ .setPrivateKeyFormat(RsaPrivateKeyFormat.VALUE)
+ .setPrivateKeyType(
+ new CleartextPrivateKeyBuilder().setCleartextPrivateKey(
+ keyPair.getPrivate().getEncoded()
+ ).build()
+ ).build();
+ final var inline = new InlineBuilder().setInlineDefinition(inlineDef).build();
+ var publicKey = new PublicKeyBuilder().setPublicKey(
+ new org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.server.rev230417
+ .ssh.server.grouping.server.identity.host.key.host.key.type._public.key
+ .PublicKeyBuilder().setInlineOrKeystore(inline).build()
+ ).build();
+ return new ServerIdentityBuilder().setHostKey(
+ List.of(new HostKeyBuilder().setName("test-name").setHostKeyType(publicKey).build())
+ ).build();
+ }
+
+ private static SshClientParameters sshClientParams() {
+ return new SshClientParametersBuilder().setClientIdentity(
+ new ClientIdentityBuilder().setUsername(USERNAME).setPassword(
+ new PasswordBuilder().setPasswordType(
+ new CleartextPasswordBuilder().setCleartextPassword(PASSWORD).build()
+ ).build()
+ ).build()
+ ).build();
+ }
+}