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.topology.callhome;
10 import static java.util.Objects.requireNonNull;
12 import com.google.common.annotations.VisibleForTesting;
13 import java.net.InetAddress;
14 import java.net.SocketAddress;
15 import java.security.PublicKey;
16 import java.util.List;
17 import java.util.concurrent.ExecutionException;
18 import java.util.concurrent.TimeUnit;
19 import java.util.concurrent.TimeoutException;
20 import org.eclipse.jdt.annotation.NonNull;
21 import org.opendaylight.netconf.api.TransportConstants;
22 import org.opendaylight.netconf.client.NetconfClientSessionNegotiatorFactory;
23 import org.opendaylight.netconf.shaded.sshd.client.auth.password.UserAuthPasswordFactory;
24 import org.opendaylight.netconf.shaded.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
25 import org.opendaylight.netconf.shaded.sshd.client.session.ClientSession;
26 import org.opendaylight.netconf.shaded.sshd.common.session.Session;
27 import org.opendaylight.netconf.shaded.sshd.common.session.SessionListener;
28 import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
29 import org.opendaylight.netconf.transport.ssh.ClientFactoryManagerConfigurator;
30 import org.opendaylight.netconf.transport.ssh.SSHClient;
31 import org.opendaylight.netconf.transport.ssh.SSHTransportStackFactory;
32 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IetfInetUtil;
33 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber;
34 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev240208.netconf.client.initiate.stack.grouping.transport.ssh.ssh.SshClientParametersBuilder;
35 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.client.rev240208.netconf.client.listen.stack.grouping.transport.ssh.ssh.TcpServerParametersBuilder;
36 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.ssh.client.rev240208.ssh.client.grouping.ClientIdentityBuilder;
37 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.server.rev240208.TcpServerGrouping;
38 import org.opendaylight.yangtools.yang.common.Uint16;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
42 public final class CallHomeSshServer implements AutoCloseable {
43 private static final Logger LOG = LoggerFactory.getLogger(CallHomeSshServer.class);
44 private static final long DEFAULT_TIMEOUT_MILLIS = 10000L;
45 private static final int DEFAULT_PORT = 4334;
47 private final CallHomeSshAuthProvider authProvider;
48 private final CallHomeStatusRecorder statusRecorder;
49 private final CallHomeSshSessionContextManager contextManager;
50 private final SSHClient client;
53 CallHomeSshServer(final TcpServerGrouping tcpServerParams,
54 final SSHTransportStackFactory transportStackFactory,
55 final NetconfClientSessionNegotiatorFactory negotiatorFactory,
56 final CallHomeSshSessionContextManager contextManager,
57 final CallHomeSshAuthProvider authProvider,
58 final CallHomeStatusRecorder statusRecorder) {
59 this.authProvider = requireNonNull(authProvider);
60 this.statusRecorder = requireNonNull(statusRecorder);
61 this.contextManager = requireNonNull(contextManager);
64 final var transportChannelListener =
65 new CallHomeTransportChannelListener(negotiatorFactory, contextManager, statusRecorder);
67 // SSH transport layer configuration
68 // NB actual username will be assigned dynamically but predefined one is required for transport initialization
69 final var sshClientParams = new SshClientParametersBuilder().setClientIdentity(
70 new ClientIdentityBuilder().setUsername("ignored").build()).build();
71 final ClientFactoryManagerConfigurator configurator = factoryMgr -> {
72 factoryMgr.setServerKeyVerifier(this::verifyServerKey);
73 factoryMgr.addSessionListener(createSessionListener());
74 // supported auth factories
75 factoryMgr.setUserAuthFactories(List.of(new UserAuthPasswordFactory(), new UserAuthPublicKeyFactory()));
78 client = transportStackFactory.listenClient(TransportConstants.SSH_SUBSYSTEM, transportChannelListener,
79 tcpServerParams, sshClientParams, configurator).get(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
80 } catch (UnsupportedConfigurationException | InterruptedException | ExecutionException | TimeoutException e) {
81 throw new IllegalStateException("Could not start SSH Call-Home server", e);
85 private SessionListener createSessionListener() {
86 return new SessionListener() {
88 public void sessionClosed(final Session session) {
89 if (session instanceof ClientSession clientSession) {
90 final var context = contextManager.findBySshSession(clientSession);
91 if (context != null) {
92 contextManager.remove(context.id());
93 if (!clientSession.isAuthenticated()) {
94 // threat unauthenticated session closure as authentication failure
95 // in case there was context object created for the session
96 statusRecorder.reportFailedAuth(context.id());
97 } else if (context.settableFuture().isDone()) {
98 // disconnected after netconf session established
99 statusRecorder.reportDisconnected(context.id());
107 private boolean verifyServerKey(final ClientSession clientSession, final SocketAddress remoteAddress,
108 final PublicKey serverKey) {
109 final CallHomeSshAuthSettings authSettings = authProvider.provideAuth(remoteAddress, serverKey);
110 if (authSettings == null) {
111 // no auth for server key
112 statusRecorder.reportUnknown(remoteAddress, serverKey);
113 LOG.info("No auth settings found. Connection from {} rejected.", remoteAddress);
116 if (contextManager.exists(authSettings.id())) {
117 LOG.info("Session context with same id {} already exists. Connection from {} rejected.",
118 authSettings.id(), remoteAddress);
121 final var context = contextManager.createContext(authSettings.id(), clientSession);
122 if (context == null) {
123 // if there is an issue creating context then the cause expected to be
124 // logged within overridden createContext() method
127 contextManager.register(context);
129 // Session context is ok, apply auth settings to current session
130 authSettings.applyTo(clientSession);
131 LOG.debug("Session context is created for SSH session: {}", context);
136 public void close() throws Exception {
137 contextManager.close();
138 client.shutdown().get();
141 public static Builder builder() {
142 return new Builder();
145 public static final class Builder {
146 private InetAddress address;
147 private int port = DEFAULT_PORT;
148 private SSHTransportStackFactory transportStackFactory;
149 private NetconfClientSessionNegotiatorFactory negotiationFactory;
150 private CallHomeSshAuthProvider authProvider;
151 private CallHomeSshSessionContextManager contextManager;
152 private CallHomeStatusRecorder statusRecorder;
158 public @NonNull CallHomeSshServer build() {
159 return new CallHomeSshServer(
160 toServerParams(address, port),
161 transportStackFactory == null ? defaultTransportStackFactory() : transportStackFactory,
163 contextManager == null ? new CallHomeSshSessionContextManager() : contextManager,
164 authProvider, statusRecorder);
167 public Builder withAuthProvider(final CallHomeSshAuthProvider newAuthProvider) {
168 authProvider = newAuthProvider;
172 public Builder withSessionContextManager(final CallHomeSshSessionContextManager newContextManager) {
173 contextManager = newContextManager;
177 public Builder withStatusRecorder(final CallHomeStatusRecorder newStatusRecorder) {
178 statusRecorder = newStatusRecorder;
182 public Builder withAddress(final InetAddress newAddress) {
183 address = newAddress;
187 public Builder withPort(final int newPort) {
192 public Builder withTransportStackFactory(final SSHTransportStackFactory newTransportStackFactory) {
193 transportStackFactory = newTransportStackFactory;
197 public Builder withNegotiationFactory(final NetconfClientSessionNegotiatorFactory newNegotiationFactory) {
198 negotiationFactory = newNegotiationFactory;
203 private static TcpServerGrouping toServerParams(final InetAddress address, final int port) {
204 final var ipAddress = IetfInetUtil.ipAddressFor(
205 address == null ? InetAddress.getLoopbackAddress() : address);
206 final var portNumber = new PortNumber(Uint16.valueOf(port < 0 ? DEFAULT_PORT : port));
207 return new TcpServerParametersBuilder().setLocalAddress(ipAddress).setLocalPort(portNumber).build();
210 private static SSHTransportStackFactory defaultTransportStackFactory() {
211 return new SSHTransportStackFactory("ssh-call-home-server", 0);