2 * Copyright (c) 2023 PANTHEON.tech s.r.o. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.netconf.callhome.server.ssh;
10 import static org.mockito.ArgumentMatchers.any;
11 import static org.mockito.ArgumentMatchers.eq;
12 import static org.mockito.ArgumentMatchers.nullable;
13 import static org.mockito.Mockito.doReturn;
14 import static org.mockito.Mockito.timeout;
15 import static org.mockito.Mockito.times;
16 import static org.mockito.Mockito.verify;
17 import static org.opendaylight.netconf.api.TransportConstants.SSH_SUBSYSTEM;
19 import com.google.common.util.concurrent.SettableFuture;
20 import java.net.InetAddress;
21 import java.net.InetSocketAddress;
22 import java.net.ServerSocket;
23 import java.security.KeyPair;
24 import java.security.KeyPairGenerator;
25 import java.security.SecureRandom;
26 import java.security.spec.RSAKeyGenParameterSpec;
27 import java.util.List;
28 import java.util.Optional;
30 import java.util.concurrent.TimeUnit;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.junit.jupiter.api.Test;
33 import org.junit.jupiter.api.extension.ExtendWith;
34 import org.mockito.Mock;
35 import org.mockito.junit.jupiter.MockitoExtension;
36 import org.opendaylight.netconf.callhome.server.CallHomeStatusRecorder;
37 import org.opendaylight.netconf.callhome.server.ssh.CallHomeSshAuthSettings.DefaultAuthSettings;
38 import org.opendaylight.netconf.client.NetconfClientSession;
39 import org.opendaylight.netconf.client.NetconfClientSessionListener;
40 import org.opendaylight.netconf.client.NetconfClientSessionNegotiatorFactory;
41 import org.opendaylight.netconf.common.impl.DefaultNetconfTimer;
42 import org.opendaylight.netconf.server.NetconfServerSession;
43 import org.opendaylight.netconf.server.NetconfServerSessionNegotiatorFactory;
44 import org.opendaylight.netconf.server.ServerTransportInitializer;
45 import org.opendaylight.netconf.server.api.monitoring.NetconfMonitoringService;
46 import org.opendaylight.netconf.server.api.monitoring.SessionListener;
47 import org.opendaylight.netconf.server.impl.DefaultSessionIdProvider;
48 import org.opendaylight.netconf.server.osgi.AggregatedNetconfOperationServiceFactory;
49 import org.opendaylight.netconf.shaded.sshd.client.session.ClientSession;
50 import org.opendaylight.netconf.shaded.sshd.common.keyprovider.KeyPairProvider;
51 import org.opendaylight.netconf.shaded.sshd.server.auth.password.PasswordAuthenticator;
52 import org.opendaylight.netconf.shaded.sshd.server.auth.password.UserAuthPasswordFactory;
53 import org.opendaylight.netconf.shaded.sshd.server.auth.pubkey.PublickeyAuthenticator;
54 import org.opendaylight.netconf.shaded.sshd.server.auth.pubkey.UserAuthPublicKeyFactory;
55 import org.opendaylight.netconf.transport.ssh.SSHServer;
56 import org.opendaylight.netconf.transport.ssh.SSHTransportStackFactory;
57 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host;
58 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IetfInetUtil;
59 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber;
60 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev231228.netconf.client.initiate.stack.grouping.transport.ssh.ssh.TcpClientParametersBuilder;
61 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.Capabilities;
62 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.CapabilitiesBuilder;
63 import org.opendaylight.yangtools.yang.common.Uint16;
65 @ExtendWith(MockitoExtension.class)
66 public class CallHomeSshServerTest {
67 private static final long TIMEOUT = 5000L;
68 private static final Capabilities EMPTY_CAPABILITIES = new CapabilitiesBuilder().setCapability(Set.of()).build();
69 private static final String USERNAME = "username";
70 private static final String PASSWORD = "pa$$W0rd";
73 private NetconfMonitoringService monitoringService;
75 private SessionListener serverSessionListener;
77 private NetconfClientSessionListener clientSessionListener;
79 private CallHomeStatusRecorder statusRecorder;
82 void integrationTest() throws Exception {
85 final var serverKeys = generateKeyPair();
86 final var client1Keys = generateKeyPair();
87 final var client2Keys = generateKeyPair();
88 final var client3Keys = generateKeyPair();
89 final var client4Keys = generateKeyPair();
92 final var authProvider = (CallHomeSshAuthProvider) (remoteAddress, publicKey) -> {
93 // identify client 2 by password (invalid)
94 if (client2Keys.getPublic().equals(publicKey)) {
95 return new DefaultAuthSettings("client2-id", USERNAME, Set.of("invalid-password"), null);
97 // identify client 3 by password (valid)
98 if (client3Keys.getPublic().equals(publicKey)) {
99 return new DefaultAuthSettings("client3-id", USERNAME, Set.of(PASSWORD), null);
101 // identify client 4 by public key
102 if (client4Keys.getPublic().equals(publicKey)) {
103 return new DefaultAuthSettings("client4-id", USERNAME, null, Set.of(serverKeys));
105 // client 1 is not identified
108 // client side authenticators
109 final PasswordAuthenticator passwordAuthenticator =
110 (username, password, session) -> USERNAME.equals(username) && PASSWORD.equals(password);
111 final PublickeyAuthenticator publicKeyAuthenticator =
112 (username, publicKey, session) -> serverKeys.getPublic().equals(publicKey);
114 // Netconf layer for clients
115 doReturn(serverSessionListener).when(monitoringService).getSessionListener();
116 doReturn(EMPTY_CAPABILITIES).when(monitoringService).getCapabilities();
118 final var timer = new DefaultNetconfTimer();
120 final var negotiatorFactory = NetconfServerSessionNegotiatorFactory.builder()
122 .setAggregatedOpService(new AggregatedNetconfOperationServiceFactory())
123 .setIdProvider(new DefaultSessionIdProvider())
124 .setConnectionTimeoutMillis(TIMEOUT)
125 .setMonitoringService(monitoringService)
127 final var netconfTransportListener = new ServerTransportInitializer(negotiatorFactory);
129 // tcp layer for clients
130 final var sshTransportFactory = new SSHTransportStackFactory("call-home-test-client", 0);
131 final var serverPort = serverPort();
132 final var tcpConnectParams = new TcpClientParametersBuilder()
133 .setRemoteAddress(new Host(IetfInetUtil.ipAddressFor(InetAddress.getLoopbackAddress())))
134 .setRemotePort(new PortNumber(Uint16.valueOf(serverPort))).build();
136 // Session context manager
137 final var contextManager = new CallHomeSshSessionContextManager() {
138 // inject netconf session listener
140 public CallHomeSshSessionContext createContext(final String id, final ClientSession clientSession) {
141 return new CallHomeSshSessionContext(id, clientSession.getRemoteAddress(), clientSession,
142 clientSessionListener, SettableFuture.create());
146 // start Call-Home server
147 final var server = CallHomeSshServer.builder()
148 .withPort(serverPort)
149 .withAuthProvider(authProvider)
150 .withSessionContextManager(contextManager)
151 .withStatusRecorder(statusRecorder)
152 .withNegotiationFactory(new NetconfClientSessionNegotiatorFactory(timer, Optional.empty(), TIMEOUT,
153 NetconfClientSessionNegotiatorFactory.DEFAULT_CLIENT_CAPABILITIES))
156 SSHServer client1 = null;
157 SSHServer client2 = null;
158 SSHServer client3 = null;
159 SSHServer client4 = null;
162 // client 1: rejected due to public key is not identified
163 client1 = sshTransportFactory.connectServer(SSH_SUBSYSTEM, netconfTransportListener, tcpConnectParams,
164 null, factoryMgr -> {
165 factoryMgr.setKeyPairProvider(KeyPairProvider.wrap(client1Keys));
166 factoryMgr.setPasswordAuthenticator(passwordAuthenticator);
167 factoryMgr.setUserAuthFactories(List.of(new UserAuthPasswordFactory()));
168 }).get(TIMEOUT, TimeUnit.MILLISECONDS);
169 // verify unknown key reported
170 verify(statusRecorder, timeout(TIMEOUT).times(1))
171 .reportUnknown(any(InetSocketAddress.class), eq(client1Keys.getPublic()));
173 // client 2: rejected due to auth failure (wrong password)
174 client2 = sshTransportFactory.connectServer(SSH_SUBSYSTEM, netconfTransportListener, tcpConnectParams,
175 null, factoryMgr -> {
176 factoryMgr.setKeyPairProvider(KeyPairProvider.wrap(client2Keys));
177 factoryMgr.setPasswordAuthenticator(passwordAuthenticator);
178 factoryMgr.setUserAuthFactories(List.of(new UserAuthPasswordFactory()));
179 }).get(TIMEOUT, TimeUnit.MILLISECONDS);
180 // verify auth failure reported for known key
181 verify(statusRecorder, timeout(TIMEOUT).times(1)).reportFailedAuth("client2-id");
183 // client 3: success with password auth
184 client3 = sshTransportFactory.connectServer(SSH_SUBSYSTEM, netconfTransportListener, tcpConnectParams,
185 null, factoryMgr -> {
186 factoryMgr.setKeyPairProvider(KeyPairProvider.wrap(client3Keys));
187 factoryMgr.setPasswordAuthenticator(passwordAuthenticator);
188 factoryMgr.setUserAuthFactories(List.of(new UserAuthPasswordFactory()));
189 }).get(TIMEOUT, TimeUnit.MILLISECONDS);
190 // verify netconf sessions established
191 verify(clientSessionListener, timeout(TIMEOUT).times(1)).onSessionUp(any(NetconfClientSession.class));
192 verify(serverSessionListener, timeout(TIMEOUT).times(1)).onSessionUp(any(NetconfServerSession.class));
193 verify(statusRecorder, times(1)).reportSuccess("client3-id");
195 // client 4: success with public key auth
196 client4 = sshTransportFactory.connectServer(SSH_SUBSYSTEM, netconfTransportListener, tcpConnectParams,
197 null, factoryMgr -> {
198 factoryMgr.setKeyPairProvider(KeyPairProvider.wrap(client4Keys));
199 final var pkFactory = new UserAuthPublicKeyFactory();
200 pkFactory.setSignatureFactories(factoryMgr.getSignatureFactories());
201 factoryMgr.setPublickeyAuthenticator(publicKeyAuthenticator);
202 factoryMgr.setUserAuthFactories(List.of(pkFactory));
203 }).get(TIMEOUT, TimeUnit.MILLISECONDS);
204 // verify netconf sessions established
205 verify(clientSessionListener, timeout(TIMEOUT).times(2)).onSessionUp(any(NetconfClientSession.class));
206 verify(serverSessionListener, timeout(TIMEOUT).times(2)).onSessionUp(any(NetconfServerSession.class));
207 verify(statusRecorder, times(1)).reportSuccess("client4-id");
211 shutdownClient(client1);
212 shutdownClient(client2);
213 shutdownClient(client3);
214 shutdownClient(client4);
218 // verify disconnect reported
219 verify(serverSessionListener, timeout(TIMEOUT).times(2)).onSessionDown(any(NetconfServerSession.class));
220 verify(clientSessionListener, timeout(TIMEOUT).times(2))
221 .onSessionDown(any(NetconfClientSession.class), nullable(Exception.class));
222 verify(statusRecorder, times(1)).reportDisconnected("client3-id");
223 verify(statusRecorder, times(1)).reportDisconnected("client4-id");
226 private static void shutdownClient(final @Nullable SSHServer client) throws Exception {
227 if (client != null) {
228 client.shutdown().get(TIMEOUT, TimeUnit.MILLISECONDS);
232 private static int serverPort() throws Exception {
233 try (var socket = new ServerSocket(0)) {
234 return socket.getLocalPort();
238 private static KeyPair generateKeyPair() throws Exception {
239 final var keyPairGenerator = KeyPairGenerator.getInstance("RSA");
240 keyPairGenerator.initialize(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4), new SecureRandom());
241 return keyPairGenerator.generateKeyPair();