2 * Copyright (c) 2022 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.transport.ssh;
10 import com.google.common.annotations.VisibleForTesting;
11 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
12 import java.io.IOException;
13 import java.util.Collection;
15 import java.util.concurrent.ConcurrentHashMap;
16 import java.util.function.Consumer;
17 import org.eclipse.jdt.annotation.NonNull;
18 import org.opendaylight.netconf.shaded.sshd.common.FactoryManager;
19 import org.opendaylight.netconf.shaded.sshd.common.SshConstants;
20 import org.opendaylight.netconf.shaded.sshd.common.io.IoHandler;
21 import org.opendaylight.netconf.shaded.sshd.common.session.Session;
22 import org.opendaylight.netconf.shaded.sshd.common.session.SessionListener;
23 import org.opendaylight.netconf.transport.api.AbstractOverlayTransportStack;
24 import org.opendaylight.netconf.transport.api.TransportChannel;
25 import org.opendaylight.netconf.transport.api.TransportChannelListener;
26 import org.opendaylight.netconf.transport.api.TransportStack;
27 import org.slf4j.Logger;
28 import org.slf4j.LoggerFactory;
31 * An SSH {@link TransportStack}. Instances of this class are built indirectly. The setup of the Netty channel is quite
32 * weird. We start off with whatever the underlay sets up.
35 * We then add {@link TransportIoSession#getHandler()}, which routes data between the socket and
36 * {@link TransportSshClient} (or {@link TransportSshServer}) -- forming the "bottom half" of the channel.
39 * The "upper half" of the channel is formed once the corresponding SSH subsystem is established, via
40 * {@link TransportClientSubsystem}, which installs a {@link OutboundChannelHandler}. These two work together:
42 * <li>TransportClientSubsystem pumps bytes inbound from the subsystem towards the tail of the channel pipeline</li>
43 * <li>OutboundChannelHandler pumps bytes outbound from the tail of channel pipeline into the subsystem</li>
46 public abstract sealed class SSHTransportStack extends AbstractOverlayTransportStack<SSHTransportChannel>
47 implements SessionListener permits SSHClient, SSHServer {
48 private static final Logger LOG = LoggerFactory.getLogger(SSHTransportStack.class);
50 // Underlay TransportChannels which do not have an open subsystem
51 private final Map<Long, TransportChannel> underlays = new ConcurrentHashMap<>();
52 private final Map<Long, Session> sessions = new ConcurrentHashMap<>();
53 private final TransportIoService ioService;
55 @SuppressFBWarnings(value = "MC_OVERRIDABLE_METHOD_CALL_IN_CONSTRUCTOR", justification = """
56 SessionListener has default implementations which we do not care about. We have all subclasses in this package
57 and neither of them has additional state""")
58 SSHTransportStack(final TransportChannelListener listener, final FactoryManager factoryManager,
59 final IoHandler handler) {
61 ioService = new TransportIoService(factoryManager, handler);
62 factoryManager.addSessionListener(this);
66 protected void onUnderlayChannelEstablished(final TransportChannel underlayChannel) {
67 LOG.debug("Underlay establishing, attaching SSH to {}", underlayChannel);
68 // Acquire underlying channel, create a TransportIoSession and attach its handler to this channel -- which takes
69 // care of routing bytes between the underlay channel and SSHD's network-facing side.
70 final var channel = underlayChannel.channel();
71 final var ioSession = ioService.createSession(channel.localAddress());
72 channel.pipeline().addLast(ioSession.getHandler());
74 // we now have an attached underlay, but it needs further processing before we expose it to the end user
75 underlays.put(ioSession.getId(), underlayChannel);
78 // SessionListener integration. Responsible for observing authentication-related events, orchestrating both client
79 // and server interactions.
81 // The state machine is responsible for driving TransportChannel
84 // At some point we should keep this in an encapsulated state object, but we have specializations, so we keep this
85 // here at the cost of not modeling the solution domain correctly.
88 public final void sessionCreated(final Session session) {
89 sessions.put(sessionId(session), session);
93 public final void sessionException(final Session session, final Throwable throwable) {
94 final var sessionId = sessionId(session);
95 LOG.warn("Session {} encountered an error", sessionId, throwable);
96 deleteSession(sessionId);
100 public final void sessionDisconnect(final Session session, final int reason, final String msg,
101 final String language, final boolean initiator) {
102 final var sessionId = sessionId(session);
103 LOG.debug("Session {} disconnected: {}", sessionId, SshConstants.getDisconnectReasonName(reason));
104 deleteSession(sessionId);
108 public final void sessionClosed(final Session session) {
109 final var sessionId = sessionId(session);
110 LOG.debug("Session {} closed", sessionId);
111 deleteSession(sessionId);
115 public final void sessionEvent(final Session session, final Event event) {
116 final var sessionId = sessionId(session);
118 case KeyEstablished -> {
119 LOG.debug("New key established on session {}", sessionId);
121 onKeyEstablished(session);
122 } catch (IOException e) {
123 LOG.error("Post-key step failed on session {}", sessionId, e);
124 deleteSession(sessionId);
127 case Authenticated -> {
128 LOG.debug("Authentication on session {} successful", sessionId);
130 onAuthenticated(session);
131 } catch (IOException e) {
132 LOG.error("Post-authentication step failed on session {}", sessionId, e);
133 deleteSession(sessionId);
136 case KexCompleted -> {
137 LOG.debug("Key exchange completed on session {}", sessionId);
140 LOG.debug("Ignoring event {} on session {}", event, sessionId);
145 abstract void onKeyEstablished(Session session) throws IOException;
147 abstract void onAuthenticated(Session session) throws IOException;
149 final @NonNull TransportChannel getUnderlayOf(final Long sessionId) throws IOException {
150 final var ret = underlays.get(sessionId);
152 throw new IOException("Cannot find underlay for " + sessionId);
157 final void deleteSession(final Long sessionId) {
158 sessions.remove(sessionId);
159 // auth failure, close underlay if any
160 completeUnderlay(sessionId, underlay -> underlay.channel().close());
163 // FIXME: this should be an assertion, the channel should just be there
164 final void transportEstablished(final Long sessionId) {
165 completeUnderlay(sessionId, underlay -> {
166 LOG.debug("Established transport on session {}", sessionId);
167 addTransportChannel(new SSHTransportChannel(underlay));
171 private void completeUnderlay(final Long sessionId, final Consumer<TransportChannel> action) {
172 final var removed = underlays.remove(sessionId);
173 if (removed != null) {
174 action.accept(removed);
178 static final Long sessionId(final Session session) {
179 return session.getIoSession().getId();
183 Collection<Session> getSessions() {
184 return sessions.values();