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.Nullable;
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 @Nullable TransportChannel underlayOf(final Long sessionId) {
150 return underlays.get(sessionId);
153 final void deleteSession(final Long sessionId) {
154 sessions.remove(sessionId);
155 // auth failure, close underlay if any
156 completeUnderlay(sessionId, underlay -> underlay.channel().close());
159 final void completeUnderlay(final Long sessionId, final Consumer<TransportChannel> action) {
160 final var removed = underlays.remove(sessionId);
161 if (removed != null) {
162 action.accept(removed);
166 static final Long sessionId(final Session session) {
167 return session.getIoSession().getId();
171 Collection<Session> getSessions() {
172 return sessions.values();