From 8dfe3eddefa790c1278bf6a7b8889da6da3963fd Mon Sep 17 00:00:00 2001 From: Ruslan Kashapov Date: Mon, 3 Jul 2023 17:10:19 +0300 Subject: [PATCH] Switch netconf-server to transport-api 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 Signed-off-by: Robert Varga --- protocol/netconf-server/pom.xml | 35 ++- .../server/NetconfServerDispatcherImpl.java | 1 + .../server/NetconfServerFactoryImpl.java | 117 ++++++++++ ...NetconfServerSessionNegotiatorFactory.java | 4 +- .../server/NetconfSubsystemFactory.java | 176 +++++++++++++++ .../server/api/NetconfServerDispatcher.java | 5 + .../server/api/NetconfServerFactory.java | 57 +++++ .../server/NetconfServerFactoryImplTest.java | 205 ++++++++++++++++++ 8 files changed, 578 insertions(+), 22 deletions(-) create mode 100644 protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/NetconfServerFactoryImpl.java create mode 100644 protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/NetconfSubsystemFactory.java create mode 100644 protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/api/NetconfServerFactory.java create mode 100644 protocol/netconf-server/src/test/java/org/opendaylight/netconf/server/NetconfServerFactoryImplTest.java diff --git a/protocol/netconf-server/pom.xml b/protocol/netconf-server/pom.xml index b577064fd4..543a73e943 100644 --- a/protocol/netconf-server/pom.xml +++ b/protocol/netconf-server/pom.xml @@ -29,6 +29,10 @@ io.netty netty-codec + + io.netty + netty-buffer + io.netty netty-common @@ -45,10 +49,6 @@ org.opendaylight.yangtools yang-common - - org.opendaylight.yangtools - yang-model-api - org.opendaylight.mdsal.binding.model.ietf rfc6991-ietf-inet-types @@ -94,13 +94,8 @@ netconf-netty-util - com.guicedee.services - javax.inject - true - - - org.osgi - org.osgi.service.component.annotations + org.opendaylight.netconf + shaded-sshd org.opendaylight.netconf @@ -114,12 +109,17 @@ org.opendaylight.netconf transport-ssh - - io.netty - netty-buffer - test + com.guicedee.services + javax.inject + true + + + org.osgi + org.osgi.service.component.annotations + + org.opendaylight.yangtools mockito-configuration @@ -139,11 +139,6 @@ org.opendaylight.netconf netconf-test-util - - org.opendaylight.netconf - shaded-sshd - test - org.xmlunit xmlunit-legacy diff --git a/protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/NetconfServerDispatcherImpl.java b/protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/NetconfServerDispatcherImpl.java index 1caee81d39..c1d16e9fd8 100644 --- a/protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/NetconfServerDispatcherImpl.java +++ b/protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/NetconfServerDispatcherImpl.java @@ -15,6 +15,7 @@ import java.net.InetSocketAddress; 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 implements NetconfServerDispatcher { private final ServerChannelInitializer initializer; diff --git a/protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/NetconfServerFactoryImpl.java b/protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/NetconfServerFactoryImpl.java new file mode 100644 index 0000000000..c58f0560d1 --- /dev/null +++ b/protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/NetconfServerFactoryImpl.java @@ -0,0 +1,117 @@ +/* + * 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 createTcpServer(final TcpServerGrouping params) + throws UnsupportedConfigurationException { + return TCPServer.listen(transportChannelListener, createBootstrap(), params); + } + + @Override + public ListenableFuture createSshServer(final TcpServerGrouping tcpParams, + final SshServerGrouping sshParams) throws UnsupportedConfigurationException { + return SSHServer.listen(EMPTY_LISTENER, createBootstrap(), tcpParams, sshParams); + } + + @Override + public ListenableFuture 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))); + } + } + } +} diff --git a/protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/NetconfServerSessionNegotiatorFactory.java b/protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/NetconfServerSessionNegotiatorFactory.java index 6a5fde3d18..2e998ada9f 100644 --- a/protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/NetconfServerSessionNegotiatorFactory.java +++ b/protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/NetconfServerSessionNegotiatorFactory.java @@ -108,10 +108,10 @@ public class NetconfServerSessionNegotiatorFactory final NetconfSessionListenerFactory defunctSessionListenerFactory, final Channel channel, final Promise 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) { diff --git a/protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/NetconfSubsystemFactory.java b/protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/NetconfSubsystemFactory.java new file mode 100644 index 0000000000..1e6ac1f436 --- /dev/null +++ b/protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/NetconfSubsystemFactory.java @@ -0,0 +1,176 @@ +/* + * 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); + } + } +} diff --git a/protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/api/NetconfServerDispatcher.java b/protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/api/NetconfServerDispatcher.java index 9153fe9461..bb1c0ebd4e 100644 --- a/protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/api/NetconfServerDispatcher.java +++ b/protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/api/NetconfServerDispatcher.java @@ -11,6 +11,11 @@ import io.netty.channel.ChannelFuture; 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); diff --git a/protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/api/NetconfServerFactory.java b/protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/api/NetconfServerFactory.java new file mode 100644 index 0000000000..1e7de53abf --- /dev/null +++ b/protocol/netconf-server/src/main/java/org/opendaylight/netconf/server/api/NetconfServerFactory.java @@ -0,0 +1,57 @@ +/* + * 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 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 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 createSshServer(TcpServerGrouping tcpParams, SshServerGrouping sshParams, + ServerFactoryManagerConfigurator configurator) throws UnsupportedConfigurationException; +} diff --git a/protocol/netconf-server/src/test/java/org/opendaylight/netconf/server/NetconfServerFactoryImplTest.java b/protocol/netconf-server/src/test/java/org/opendaylight/netconf/server/NetconfServerFactoryImplTest.java new file mode 100644 index 0000000000..041951e75c --- /dev/null +++ b/protocol/netconf-server/src/test/java/org/opendaylight/netconf/server/NetconfServerFactoryImplTest.java @@ -0,0 +1,205 @@ +/* + * 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 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(); + } +} -- 2.36.6