You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2019/02/28 03:05:48 UTC

[mina-sshd] 02/04: [SSHD-782] Added session level heartbeat via SSH_MSG_IGNORE

This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a commit to branch SSHD-901
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit dcdb1467c81b5b8773cc3a7e6df04c55622a05a8
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Tue Feb 26 18:25:06 2019 +0200

    [SSHD-782] Added session level heartbeat via SSH_MSG_IGNORE
---
 CHANGES.md                                         |   2 +
 docs/client-setup.md                               |  35 +++++++
 docs/internals.md                                  |   7 +-
 docs/server-setup.md                               |  13 +++
 .../sshd/cli/client/SshClientCliSupport.java       |  25 ++++-
 .../sshd/cli/server/SshServerCliSupport.java       |  17 +++-
 .../org/apache/sshd/cli/server/SshServerMain.java  |   2 +
 .../org/apache/sshd/common/PropertyResolver.java   |   7 ++
 .../apache/sshd/common/PropertyResolverUtils.java  |   9 +-
 .../apache/sshd/common/session/SessionContext.java |   3 +-
 .../common/session/SessionHeartbeatController.java |  86 +++++++++++++++++
 .../client/config/SshClientConfigFileReader.java   |   6 ++
 .../client/session/ClientConnectionService.java    |  22 ++---
 .../sshd/client/session/ClientUserAuthService.java |  11 ++-
 .../org/apache/sshd/common/FactoryManager.java     |   3 +-
 .../main/java/org/apache/sshd/common/Service.java  |  12 ++-
 .../sshd/common/session/ConnectionService.java     |   1 +
 .../session/helpers/AbstractConnectionService.java | 107 ++++++++++++++++++---
 .../server/config/SshServerConfigFileReader.java   |   3 +
 .../sshd/server/session/ServerUserAuthService.java |  26 +++--
 .../sshd/deprecated/ClientUserAuthServiceOld.java  |   9 ++
 21 files changed, 352 insertions(+), 54 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index fa3f7c6..a9f5839 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -38,6 +38,8 @@ then the internal `DefaultClientKexExtensionHandler` is used.
 
 ## Behavioral changes and enhancements
 
+* [SSHD-782](https://issues.apache.org/jira/browse/SSHD-882) - Added session level heartbeat mechanism via `SSH_MSG_IGNORE`.
+
 * [SSHD-882](https://issues.apache.org/jira/browse/SSHD-882) - Provide hooks to allow users to register a consumer
 for STDERR data sent via the `ChannelSession` - especially for the SFTP subsystem.
 
diff --git a/docs/client-setup.md b/docs/client-setup.md
index fbdd284..4b30dad 100644
--- a/docs/client-setup.md
+++ b/docs/client-setup.md
@@ -154,6 +154,41 @@ sessions depends on the actual changed configuration. Here is how a typical usag
 
 ```
 
+## Keeping the session alive while no traffic
+
+The client-side implementation has a 2 builtin mechanisms for maintaining the session alive as far as the **server** is concerned
+regardless of the user's own traffic:
+
+* Sending `SSH_MSG_IGNORE` messages every once in a while.
+
+    This mechanism is along the lines of [PUTTY null packets configuration](https://patrickmn.com/aside/how-to-keep-alive-ssh-sessions/).
+    It generates small [`SSH_MSG_IGNORE`](https://tools.ietf.org/html/rfc4253#section-11.2) messages. The way to set this mechanism
+    up is via the `setSessionHeartbeat` API.
+
+    *Note:* the same effect can also be achieved by setting the relevant properties documented in `SessionHeartbeatController`, but
+    it is highly recommended to use the API - unless one needs to control these properties **externally** via `-Dxxx` JVM options.
+
+* Sending `keepalive@...` [global requests](https://tools.ietf.org/html/rfc4254#section-4).
+
+    The feature is controlled via the `ClientFactoryManager#HEARTBEAT_REQUEST` and `HEARTBEAT_INTERVAL` properties - see the relevant
+    documentation for these features. The simplest way to activate this feature is to set the `HEARTBEAT_INTERVAL` property value
+    to the **milliseconds** value of the requested heartbeat interval.
+
+**Note(s):**
+
+* Both options are disabled by default - they need to be activated explicitly.
+
+* Both options can be activated either on the `SshClient` (for **global** setup) and/or
+the `ClientSession` (for specific session configuration).
+
+* The `keepalive@,,,,` mechanism **supersedes** the `SSH_MSG_IGNORE` one if both activated.
+
+* When using the CLI, these options can be configured using the following `-o key=value` properties:
+
+    * `ClientAliveInterval` - if positive the defines the heartbeat interval in **seconds**.
+
+    * `ClientAliveUseNullPackets` - *true* if use the `SSH_MSG_IGNORE` mechanism, *false* if use global request (default).
+
 ## Running a command or opening a shell
 
 When running a command or opening a shell, there is an extra concern regarding the PTY configuration and/or the
diff --git a/docs/internals.md b/docs/internals.md
index 88e480f..23c7284 100644
--- a/docs/internals.md
+++ b/docs/internals.md
@@ -240,22 +240,17 @@ the handler may choose to build and send the response within its own code, in wh
 
 * `exit-signal`, `exit-status` - As described in [RFC4254 section 6.10](https://tools.ietf.org/html/rfc4254#section-6.10)
 
-
 * `*@putty.projects.tartarus.org` - As described in [Appendix F: SSH-2 names specified for PuTTY](http://tartarus.org/~simon/putty-snapshots/htmldoc/AppendixF.html)
 
-
 * `hostkeys-prove-00@openssh.com`, `hostkeys-00@openssh.com` - As described in [OpenSSH protocol - section 2.5](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL)
 
-
 * `tcpip-forward`, `cancel-tcpip-forward` - As described in [RFC4254 section 7](https://tools.ietf.org/html/rfc4254#section-7)
 
-
-* `keepalive@*` - Used by many implementations (including this one) to "ping" the peer and make sure the connection is still alive.
+* `keepalive@*` - Used by many client implementations (including this one) to "ping" the server and keep/make sure the connection is still alive.
 In this context, the SSHD code allows the user to configure both the frequency and content of the heartbeat request (including whether
 to send this request at all) via the `ClientFactoryManager`-s `HEARTBEAT_INTERVAL`, `HEARTBEAT_REQUEST` and `DEFAULT_KEEP_ALIVE_HEARTBEAT_STRING`
 configuration properties.
 
-
 * `no-more-sessions@*` - As described in [OpenSSH protocol section 2.2](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL).
 In this context, the code consults the `ServerFactoryManagder.MAX_CONCURRENT_SESSIONS` server-side configuration property in order to
 decide whether to accept a successfully authenticated session.
diff --git a/docs/server-setup.md b/docs/server-setup.md
index fc25267..3992c63 100644
--- a/docs/server-setup.md
+++ b/docs/server-setup.md
@@ -137,3 +137,16 @@ configurations (except the port) can still be *overridden* while the server is r
 only **new** clients that connect to the server after the change will be affected - with the exception of the negotiation
 options (keys, macs, ciphers, etc...) which take effect the next time keys are re-exchanged, that can affect live sessions
 and not only new ones.
+
+## Providing server-side heartbeat
+
+The server can generate [`SSH_MSG_IGNORE`](https://tools.ietf.org/html/rfc4253#section-11.2) messages towards its
+client sessions in order to make sure that the client does not time out on waiting for traffic if no user generated
+data is available. By default, this feature is **disabled** - however it can be enabled by invoking the `setSessionHeartbeat`
+API either on the server (for **global** setting) or a specific session (for targeted control of the feature).
+
+*Note:* the same effect can also be achieved by setting the relevant properties documented in `SessionHeartbeatController`, but
+it is highly recommended to use the API - unless one needs to control these properties **externally** via `-Dxxx` JVM options.
+
+If one is using the SSHD CLI code, then these options are controlled via `-o ServerAliveInterval=NNNN` where the value is
+the requested **global** interval in **seconds**. *Note*: any non-positive value is treated as if the feature is disabled.
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
index 926015f..ae83310 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
@@ -37,6 +37,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
 import java.util.logging.ConsoleHandler;
 import java.util.logging.Formatter;
 import java.util.logging.Handler;
@@ -81,6 +82,7 @@ import org.apache.sshd.common.kex.extension.KexExtensionHandler;
 import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
 import org.apache.sshd.common.mac.BuiltinMacs;
 import org.apache.sshd.common.mac.Mac;
+import org.apache.sshd.common.session.SessionHeartbeatController.HeartbeatType;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.OsUtils;
 import org.apache.sshd.common.util.ValidateUtils;
@@ -376,9 +378,30 @@ public abstract class SshClientCliSupport extends CliSupport {
         return ptyModes;
     }
 
+    public static void setupClientHeartbeat(
+            SshClient client, Map<String, ?> options, PrintStream stdout) {
+        long interval = PropertyResolverUtils.getLongProperty(
+            options, SshClientConfigFileReader.CLIENT_LIVECHECK_INTERVAL_PROP, SshClientConfigFileReader.DEFAULT_ALIVE_INTERVAL);
+        if (interval <= 0L) {
+            return;
+        }
+
+        if (PropertyResolverUtils.getBooleanProperty(
+                options, SshClientConfigFileReader.CLIENT_LIVECHECK_USE_NULLS, SshClientConfigFileReader.DEFAULT_LIVECHECK_USE_NULLS)) {
+            PropertyResolverUtils.updateProperty(
+                client, ClientFactoryManager.HEARTBEAT_INTERVAL, TimeUnit.SECONDS.toMillis(interval));
+            stdout.println("Using SSH_MSG_IGNORE heartbeat every " + interval + " seconds");
+        } else {
+            client.setSessionHeartbeat(HeartbeatType.IGNORE, TimeUnit.SECONDS, interval);
+            stdout.println("Using global request heartbeat every " + interval + " seconds");
+        }
+    }
+
     public static SshClient setupDefaultClient(
             Map<String, ?> options, Level level, PrintStream stdout, PrintStream stderr, String... args) {
-        return setupIoServiceFactory(SshClient.setUpDefaultClient(), options, level, stdout, stderr, args);
+        SshClient client = setupIoServiceFactory(SshClient.setUpDefaultClient(), options, level, stdout, stderr, args);
+        setupClientHeartbeat(client, options, stdout);
+        return client;
     }
 
     // returns null if error encountered
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java
index c2dc94f..ad5cdeb 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java
@@ -30,9 +30,11 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.ServiceLoader;
 import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -48,6 +50,7 @@ import org.apache.sshd.common.config.keys.BuiltinIdentities;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
 import org.apache.sshd.common.keyprovider.MappedKeyPairProvider;
+import org.apache.sshd.common.session.SessionHeartbeatController.HeartbeatType;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.io.resource.PathResource;
@@ -68,8 +71,6 @@ import org.apache.sshd.server.subsystem.sftp.SftpEventListener;
 import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
 
 /**
- * TODO Add javadoc
- *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public abstract class SshServerCliSupport extends CliSupport {
@@ -80,6 +81,18 @@ public abstract class SshServerCliSupport extends CliSupport {
         super();
     }
 
+    public static void setupServerHeartbeat(
+            SshServer sshd, Map<String, ?> options, PrintStream stdout) {
+        long interval = PropertyResolverUtils.getLongProperty(
+            options, SshServerConfigFileReader.SERVER_ALIVE_INTERVAL_PROP, SshServerConfigFileReader.DEFAULT_ALIVE_INTERVAL);
+        if (interval <= 0L) {
+            return;
+        }
+
+        sshd.setSessionHeartbeat(HeartbeatType.IGNORE, TimeUnit.SECONDS, interval);
+        stdout.println("Generate server-side SSH_MSG_IGNORE heartbeat every " + interval + " seconds");
+    }
+
     public static KeyPairProvider resolveServerKeys(
             PrintStream stderr, String hostKeyType, int hostKeySize, Collection<String> keyFiles)
                 throws Exception {
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java
index f9a294e..442a9ec 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java
@@ -164,6 +164,8 @@ public class SshServerMain extends SshServerCliSupport {
         Map<String, Object> props = sshd.getProperties();
         props.putAll(options);
 
+        setupServerHeartbeat(sshd, options, System.out);
+
         PropertyResolver resolver = PropertyResolverUtils.toPropertyResolver(options);
         KeyPairProvider hostKeyProvider = resolveServerKeys(System.err, hostKeyType, hostKeySize, keyFiles);
         sshd.setKeyPairProvider(hostKeyProvider);
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/PropertyResolver.java b/sshd-common/src/main/java/org/apache/sshd/common/PropertyResolver.java
index d7f5f19..09363c5 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/PropertyResolver.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/PropertyResolver.java
@@ -19,6 +19,7 @@
 
 package org.apache.sshd.common;
 
+import java.nio.charset.Charset;
 import java.util.Collections;
 import java.util.Map;
 
@@ -121,4 +122,10 @@ public interface PropertyResolver {
     default Object getObject(String name) {
         return PropertyResolverUtils.getObject(this, name);
     }
+
+    default Charset getCharset(String name, Charset defaultValue) {
+        Object value = getObject(name);
+        return (value == null) ? defaultValue : PropertyResolverUtils.toCharset(value);
+    }
+
 }
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/PropertyResolverUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/PropertyResolverUtils.java
index d95278b..d16a4d7 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/PropertyResolverUtils.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/PropertyResolverUtils.java
@@ -187,7 +187,8 @@ public final class PropertyResolverUtils {
      * @throws NoSuchElementException If no matching string name found and
      * <tt>failIfNoMatch</tt> is {@code true}
      */
-    public static <E extends Enum<E>> E toEnum(Class<E> enumType, Object value, boolean failIfNoMatch, Collection<E> available) {
+    public static <E extends Enum<E>> E toEnum(
+            Class<E> enumType, Object value, boolean failIfNoMatch, Collection<E> available) {
         if (value == null) {
             return null;
         } else if (enumType.isInstance(value)) {
@@ -208,7 +209,8 @@ public final class PropertyResolverUtils {
 
             return null;
         } else {
-            throw new IllegalArgumentException("Bad value type for enum conversion: " + value.getClass().getSimpleName());
+            throw new IllegalArgumentException(
+                "Bad value type for enum conversion: " + value.getClass().getSimpleName());
         }
     }
 
@@ -332,7 +334,8 @@ public final class PropertyResolverUtils {
         } else if (value instanceof CharSequence) {
             return parseBoolean(value.toString());
         } else {
-            throw new UnsupportedOperationException("Cannot convert " + value.getClass().getSimpleName() + "[" + value + "] to boolean");
+            throw new UnsupportedOperationException(
+                "Cannot convert " + value.getClass().getSimpleName() + "[" + value + "] to boolean");
         }
     }
 
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/session/SessionContext.java b/sshd-common/src/main/java/org/apache/sshd/common/session/SessionContext.java
index d19dd9c..23d5c60 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/session/SessionContext.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/session/SessionContext.java
@@ -22,7 +22,6 @@ package org.apache.sshd.common.session;
 import java.util.Map;
 
 import org.apache.sshd.common.AttributeStore;
-import org.apache.sshd.common.PropertyResolver;
 import org.apache.sshd.common.auth.UsernameHolder;
 import org.apache.sshd.common.kex.KexProposalOption;
 import org.apache.sshd.common.kex.KexState;
@@ -37,7 +36,7 @@ import org.apache.sshd.common.util.net.ConnectionEndpointsIndicator;
 public interface SessionContext
         extends ConnectionEndpointsIndicator,
                 UsernameHolder,
-                PropertyResolver,
+                SessionHeartbeatController,
                 AttributeStore {
     /**
      * Default prefix expected for the client / server identification string
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/session/SessionHeartbeatController.java b/sshd-common/src/main/java/org/apache/sshd/common/session/SessionHeartbeatController.java
new file mode 100644
index 0000000..ce02b80
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/session/SessionHeartbeatController.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.session;
+
+import java.util.EnumSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.common.PropertyResolver;
+import org.apache.sshd.common.PropertyResolverUtils;
+import org.apache.sshd.common.util.GenericUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface SessionHeartbeatController extends PropertyResolver {
+    enum HeartbeatType {
+        /** No heartbeat */
+        NONE,
+        /** Use {@code SSH_MSG_IGNORE} packets */
+        IGNORE;
+
+        public static final Set<HeartbeatType> VALUES = EnumSet.allOf(HeartbeatType.class);
+
+        public static HeartbeatType fromName(String name) {
+            return GenericUtils.isEmpty(name)
+                 ? null
+                 : VALUES.stream()
+                     .filter(v -> name.equalsIgnoreCase(v.name()))
+                     .findAny()
+                     .orElse(null);
+        }
+    }
+
+    /**
+     * Property used to register the {@link HeartbeatType} - if non-existent
+     * or {@code NONE} then disabled. Same if some unknown string value is
+     * set as the property value.
+     */
+    String SESSION_HEARTBEAT_TYPE = "session-connection-heartbeat-type";
+
+    /** Property used to register the interval for the heartbeat - if not set or non-positive then disabled */
+    String SESSION_HEARTBEAT_INTERVAL = "session-connection-heartbeat-interval";
+
+    default HeartbeatType getSessionHeartbeatType() {
+        Object value = getObject(SESSION_HEARTBEAT_TYPE);
+        return PropertyResolverUtils.toEnum(HeartbeatType.class, value, false, HeartbeatType.VALUES);
+    }
+
+    default long getSessionHeartbeatInterval() {
+        return getLongProperty(SESSION_HEARTBEAT_INTERVAL, 0L);
+    }
+
+    /**
+     * Disables the session heartbeat feature - <B>Note:</B> if heartbeat already
+     * in progress then it may be ignored.
+     */
+    default void disableSessionHeartbeat() {
+        setSessionHeartbeat(HeartbeatType.NONE, TimeUnit.MILLISECONDS, 0L);
+    }
+
+    default void setSessionHeartbeat(HeartbeatType type, TimeUnit unit, long count) {
+        Objects.requireNonNull(type, "No heartbeat type specified");
+        Objects.requireNonNull(unit, "No heartbeat time unit provided");
+        PropertyResolverUtils.updateProperty(this, SESSION_HEARTBEAT_TYPE, type);
+        PropertyResolverUtils.updateProperty(this, SESSION_HEARTBEAT_INTERVAL, TimeUnit.MILLISECONDS.convert(count, unit));
+    }
+}
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/SshClientConfigFileReader.java b/sshd-core/src/main/java/org/apache/sshd/client/config/SshClientConfigFileReader.java
index eb87062..669040d 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/config/SshClientConfigFileReader.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/SshClientConfigFileReader.java
@@ -32,6 +32,12 @@ public final class SshClientConfigFileReader {
     public static final String SENDENV_PROP = "SendEnv";
     public static final String REQUEST_TTY_OPTION = "RequestTTY";
 
+    public static final String CLIENT_LIVECHECK_INTERVAL_PROP = "ClientAliveInterval";
+    public static final long DEFAULT_ALIVE_INTERVAL = 0L;
+
+    public static final String CLIENT_LIVECHECK_USE_NULLS = "ClientAliveUseNullPackets";
+    public static final boolean DEFAULT_LIVECHECK_USE_NULLS = false;
+
     private SshClientConfigFileReader() {
         throw new UnsupportedOperationException("No instance allowed");
     }
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientConnectionService.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientConnectionService.java
index 06c6000..bd5873c 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientConnectionService.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientConnectionService.java
@@ -30,6 +30,7 @@ import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.io.AbstractIoWriteFuture;
 import org.apache.sshd.common.io.IoWriteFuture;
+import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.session.helpers.AbstractConnectionService;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.buffer.Buffer;
@@ -51,9 +52,9 @@ public class ClientConnectionService
     public ClientConnectionService(AbstractClientSession s) throws SshException {
         super(s);
 
-        heartbeatRequest = s.getStringProperty(
+        heartbeatRequest = this.getStringProperty(
             ClientFactoryManager.HEARTBEAT_REQUEST, ClientFactoryManager.DEFAULT_KEEP_ALIVE_HEARTBEAT_STRING);
-        heartbeatInterval = s.getLongProperty(
+        heartbeatInterval = this.getLongProperty(
             ClientFactoryManager.HEARTBEAT_INTERVAL, ClientFactoryManager.DEFAULT_HEARTBEAT_INTERVAL);
     }
 
@@ -115,20 +116,22 @@ public class ClientConnectionService
             return super.sendHeartBeat();
         }
 
-        ClientSession session = getClientSession();
+        Session session = getSession();
         try {
             Buffer buf = session.createBuffer(
                 SshConstants.SSH_MSG_GLOBAL_REQUEST, heartbeatRequest.length() + Byte.SIZE);
             buf.putString(heartbeatRequest);
             buf.putBoolean(false);
+
             IoWriteFuture future = session.writePacket(buf);
             future.addListener(this::futureDone);
+            heartbeatCount.incrementAndGet();
             return future;
-        } catch (IOException | RuntimeException e) {
+        } catch (IOException | RuntimeException | Error e) {
             session.exceptionCaught(e);
             if (log.isDebugEnabled()) {
-                log.debug("sendHeartBeat({}) failed ({}) to send request={}: {}",
-                    session, e.getClass().getSimpleName(), heartbeatRequest, e.getMessage());
+                log.debug("sendHeartBeat({}) failed ({}) to send heartbeat #{} request={}: {}",
+                    session, e.getClass().getSimpleName(), heartbeatCount, heartbeatRequest, e.getMessage());
             }
             if (log.isTraceEnabled()) {
                 log.trace("sendHeartBeat(" + session + ") exception details", e);
@@ -142,13 +145,6 @@ public class ClientConnectionService
         }
     }
 
-    protected void futureDone(IoWriteFuture future) {
-        Throwable t = future.getException();
-        if (t != null) {
-            getSession().exceptionCaught(t);
-        }
-    }
-
     @Override
     public AgentForwardSupport getAgentForwardSupport() {
         throw new IllegalStateException("Server side operation");
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientUserAuthService.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientUserAuthService.java
index ea426cb..e45fed7 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientUserAuthService.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientUserAuthService.java
@@ -23,7 +23,9 @@ import java.io.InterruptedIOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicReference;
 
 import org.apache.sshd.client.ClientAuthenticationManager;
@@ -58,6 +60,7 @@ public class ClientUserAuthService
      * isSuccess -> authenticated, else if isDone -> server waiting for user auth, else authenticating.
      */
     private final AtomicReference<AuthFuture> authFutureHolder = new AtomicReference<>();
+    private final Map<String, Object> properties = new ConcurrentHashMap<>();
 
     private final ClientSessionImpl clientSession;
     private final List<String> clientMethods;
@@ -69,7 +72,8 @@ public class ClientUserAuthService
     private int currentMethod;
 
     public ClientUserAuthService(Session s) {
-        clientSession = ValidateUtils.checkInstanceOf(s, ClientSessionImpl.class, "Client side service used on server side: %s", s);
+        clientSession = ValidateUtils.checkInstanceOf(
+            s, ClientSessionImpl.class, "Client side service used on server side: %s", s);
         authFactories = ValidateUtils.checkNotNullAndNotEmpty(
             clientSession.getUserAuthFactories(), "No user auth factories for %s", s);
         clientMethods = new ArrayList<>();
@@ -116,6 +120,11 @@ public class ClientUserAuthService
     }
 
     @Override
+    public Map<String, Object> getProperties() {
+        return properties;
+    }
+
+    @Override
     public void start() {
         // ignored
     }
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java b/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java
index d6bb13e..a6c599c 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java
@@ -38,6 +38,7 @@ import org.apache.sshd.common.random.Random;
 import org.apache.sshd.common.session.ConnectionService;
 import org.apache.sshd.common.session.ReservedSessionMessagesManager;
 import org.apache.sshd.common.session.SessionDisconnectHandlerManager;
+import org.apache.sshd.common.session.SessionHeartbeatController;
 import org.apache.sshd.common.session.SessionListenerManager;
 import org.apache.sshd.common.session.UnknownChannelReferenceHandlerManager;
 import org.apache.sshd.server.forward.AgentForwardingFilter;
@@ -62,7 +63,7 @@ public interface FactoryManager
                 PortForwardingEventListenerManager,
                 IoServiceEventListenerManager,
                 AttributeStore,
-                PropertyResolver {
+                SessionHeartbeatController {
 
     /**
      * Key used to retrieve the value of the channel window size in the
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/Service.java b/sshd-core/src/main/java/org/apache/sshd/common/Service.java
index f6caeb0..69426e9 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/Service.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/Service.java
@@ -22,15 +22,19 @@ import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.util.buffer.Buffer;
 
 /**
- * See RFC 4253 [SSH-TRANS] and the SSH_MSG_SERVICE_REQUEST packet. Examples include ssh-userauth
- * and ssh-connection but developers are also free to implement their own custom service.
+ * See RFC 4253 [SSH-TRANS] and the SSH_MSG_SERVICE_REQUEST packet. Examples include &quot;ssh-userauth&quot;
+ * and &quot;ssh-connection&quot; but developers are also free to implement their own custom service.
  *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public interface Service extends Closeable {
+public interface Service extends PropertyResolver, Closeable {
     Session getSession();
 
-    // TODO: this is specific to clients
+    @Override
+    default PropertyResolver getParentPropertyResolver() {
+        return getSession();
+    }
+
     void start();
 
     /**
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/ConnectionService.java b/sshd-core/src/main/java/org/apache/sshd/common/session/ConnectionService.java
index ac977fa..ace0584 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/ConnectionService.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/ConnectionService.java
@@ -35,6 +35,7 @@ import org.apache.sshd.server.x11.X11ForwardSupport;
  */
 public interface ConnectionService
         extends Service,
+        SessionHeartbeatController,
         UnknownChannelReferenceHandlerManager,
         PortForwardingEventListenerManager,
         PortForwardingEventListenerManagerHolder {
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractConnectionService.java b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractConnectionService.java
index 4aacf7d..2a0d585 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractConnectionService.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractConnectionService.java
@@ -26,9 +26,12 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.IntUnaryOperator;
 
@@ -89,6 +92,9 @@ public abstract class AbstractConnectionService
      */
     public static final IntUnaryOperator RESPONSE_BUFFER_GROWTH_FACTOR = Int2IntFunction.add(Byte.SIZE);
 
+    /** Used in {@code SSH_MSH_IGNORE} messages for the keep-alive mechanism */
+    public static final String DEFAULT_SESSION_IGNORE_HEARTBEAT_STRING = "ignore@sshd.apache.org";
+
     /**
      * Map of channels keyed by the identifier
      */
@@ -97,6 +103,7 @@ public abstract class AbstractConnectionService
      * Next channel identifier
      */
     protected final AtomicInteger nextChannelId = new AtomicInteger(0);
+    protected final AtomicLong heartbeatCount = new AtomicLong(0L);
 
     private ScheduledFuture<?> heartBeat;
 
@@ -106,6 +113,7 @@ public abstract class AbstractConnectionService
     private final AtomicBoolean allowMoreSessions = new AtomicBoolean(true);
     private final Collection<PortForwardingEventListener> listeners = new CopyOnWriteArraySet<>();
     private final Collection<PortForwardingEventListenerManager> managersHolder = new CopyOnWriteArraySet<>();
+    private final Map<String, Object> properties = new ConcurrentHashMap<>();
     private final PortForwardingEventListener listenerProxy;
     private final AbstractSession sessionInstance;
     private UnknownChannelReferenceHandler unknownChannelReferenceHandler;
@@ -117,6 +125,11 @@ public abstract class AbstractConnectionService
     }
 
     @Override
+    public Map<String, Object> getProperties() {
+        return properties;
+    }
+
+    @Override
     public PortForwardingEventListener getPortForwardingEventListenerProxy() {
         return listenerProxy;
     }
@@ -175,12 +188,28 @@ public abstract class AbstractConnectionService
 
     @Override
     public void start() {
-        startHeartBeat();
+        heartBeat = startHeartBeat();
     }
 
     protected synchronized ScheduledFuture<?> startHeartBeat() {
-        // TODO SSHD-782
-        return null;
+        stopHeartBeat();    // make sure any existing heartbeat is stopped
+
+        HeartbeatType heartbeatType = getSessionHeartbeatType();
+        long interval = getSessionHeartbeatInterval();
+        Session session = getSession();
+        boolean debugEnabled = log.isDebugEnabled();
+        if (debugEnabled) {
+            log.debug("startHeartbeat({}) heartbeat type={}, interval={}", session, heartbeatType, interval);
+        }
+
+        if ((heartbeatType == null) || (heartbeatType == HeartbeatType.NONE) || (interval <= 0)) {
+            return null;
+        }
+
+        FactoryManager manager = session.getFactoryManager();
+        ScheduledExecutorService service = manager.getScheduledExecutorService();
+        return service.scheduleAtFixedRate(
+            this::sendHeartBeat, interval, interval, TimeUnit.MILLISECONDS);
     }
 
     /**
@@ -190,8 +219,51 @@ public abstract class AbstractConnectionService
      * message write completion - {@code null} if no heartbeat sent
      */
     protected IoWriteFuture sendHeartBeat() {
-        // TODO SSHD-782
-        return null;
+        HeartbeatType heartbeatType = getSessionHeartbeatType();
+        long interval = getSessionHeartbeatInterval();
+        Session session = getSession();
+        boolean traceEnabled = log.isTraceEnabled();
+        if (traceEnabled) {
+            log.trace("sendHeartbeat({}) heartbeat type={}, interval={}",
+                session, heartbeatType, interval);
+        }
+
+        if ((heartbeatType == null) || (heartbeatType == HeartbeatType.NONE) || (interval <= 0)) {
+            return null;
+        }
+
+        try {
+            Buffer buffer = session.createBuffer(
+                SshConstants.SSH_MSG_IGNORE, DEFAULT_SESSION_IGNORE_HEARTBEAT_STRING.length() + Byte.SIZE);
+            buffer.putString(DEFAULT_SESSION_IGNORE_HEARTBEAT_STRING);
+            IoWriteFuture future = session.writePacket(buffer);
+            future.addListener(this::futureDone);
+            return future;
+        } catch (IOException | RuntimeException | Error e) {
+            session.exceptionCaught(e);
+            if (log.isDebugEnabled()) {
+                log.debug("sendHeartBeat({}) failed ({}) to send heartbeat #{} request={}: {}",
+                    session, e.getClass().getSimpleName(), heartbeatCount, heartbeatType, e.getMessage());
+            }
+            if (log.isTraceEnabled()) {
+                log.trace("sendHeartBeat(" + session + ") exception details", e);
+            }
+
+            return new AbstractIoWriteFuture(DEFAULT_SESSION_IGNORE_HEARTBEAT_STRING, null) {
+                {
+                    setValue(e);
+                }
+            };
+        }
+
+    }
+
+    protected void futureDone(IoWriteFuture future) {
+        Throwable t = future.getException();
+        if (t != null) {
+            Session session = getSession();
+            session.exceptionCaught(t);
+        }
     }
 
     protected synchronized void stopHeartBeat() {
@@ -229,7 +301,8 @@ public abstract class AbstractConnectionService
                 return forwarder;
             }
 
-            forwarder = ValidateUtils.checkNotNull(createForwardingFilter(session), "No forwarder created for %s", session);
+            forwarder = ValidateUtils.checkNotNull(
+                createForwardingFilter(session), "No forwarder created for %s", session);
             forwarderHolder.set(forwarder);
         }
 
@@ -267,7 +340,8 @@ public abstract class AbstractConnectionService
                 return x11Support;
             }
 
-            x11Support = ValidateUtils.checkNotNull(createX11ForwardSupport(session), "No X11 forwarder created for %s", session);
+            x11Support = ValidateUtils.checkNotNull(
+                createX11ForwardSupport(session), "No X11 forwarder created for %s", session);
             x11ForwardHolder.set(x11Support);
         }
 
@@ -291,7 +365,8 @@ public abstract class AbstractConnectionService
                 return agentForward;
             }
 
-            agentForward = ValidateUtils.checkNotNull(createAgentForwardSupport(session), "No agent forward created for %s", session);
+            agentForward = ValidateUtils.checkNotNull(
+                createAgentForwardSupport(session), "No agent forward created for %s", session);
             agentForwardHolder.set(agentForward);
         }
 
@@ -349,7 +424,8 @@ public abstract class AbstractConnectionService
     }
 
     protected void handleChannelRegistrationFailure(Channel channel, int channelId) throws IOException {
-        RuntimeException reason = new IllegalStateException("Channel id=" + channelId + " not registered because session is being closed: " + this);
+        RuntimeException reason = new IllegalStateException(
+            "Channel id=" + channelId + " not registered because session is being closed: " + this);
         AbstractChannel notifier =
             ValidateUtils.checkInstanceOf(channel, AbstractChannel.class, "Non abstract channel for id=%d", channelId);
         notifier.signalChannelClosed(reason);
@@ -678,7 +754,8 @@ public abstract class AbstractConnectionService
         Channel channel = NamedFactory.create(manager.getChannelFactories(), type);
         if (channel == null) {
             // TODO add language tag configurable control
-            sendChannelOpenFailure(buffer, sender, SshConstants.SSH_OPEN_UNKNOWN_CHANNEL_TYPE, "Unsupported channel type: " + type, "");
+            sendChannelOpenFailure(buffer, sender,
+                SshConstants.SSH_OPEN_UNKNOWN_CHANNEL_TYPE, "Unsupported channel type: " + type, "");
             return;
         }
 
@@ -719,14 +796,16 @@ public abstract class AbstractConnectionService
             } catch (IOException e) {
                 if (debugEnabled) {
                     log.debug("operationComplete({}) {}: {}",
-                              AbstractConnectionService.this, e.getClass().getSimpleName(), e.getMessage());
+                          AbstractConnectionService.this, e.getClass().getSimpleName(), e.getMessage());
                 }
                 session.exceptionCaught(e);
             }
         });
     }
 
-    protected IoWriteFuture sendChannelOpenFailure(Buffer buffer, int sender, int reasonCode, String message, String lang) throws IOException {
+    protected IoWriteFuture sendChannelOpenFailure(
+            Buffer buffer, int sender, int reasonCode, String message, String lang)
+                throws IOException {
         if (log.isDebugEnabled()) {
             log.debug("sendChannelOpenFailure({}) sender={}, reason={}, lang={}, message='{}'",
                   this, sender, SshConstants.getOpenErrorCodeName(reasonCode), lang, message);
@@ -797,7 +876,9 @@ public abstract class AbstractConnectionService
         return sendGlobalResponse(buffer, req, RequestHandler.Result.Unsupported, wantReply);
     }
 
-    protected IoWriteFuture sendGlobalResponse(Buffer buffer, String req, RequestHandler.Result result, boolean wantReply) throws IOException {
+    protected IoWriteFuture sendGlobalResponse(
+            Buffer buffer, String req, RequestHandler.Result result, boolean wantReply)
+                throws IOException {
         if (log.isDebugEnabled()) {
             log.debug("sendGlobalResponse({})[{}] result={}, want-reply={}", this, req, result, wantReply);
         }
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/config/SshServerConfigFileReader.java b/sshd-core/src/main/java/org/apache/sshd/server/config/SshServerConfigFileReader.java
index bf4a958..eead6b8 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/config/SshServerConfigFileReader.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/config/SshServerConfigFileReader.java
@@ -59,6 +59,9 @@ public final class SshServerConfigFileReader {
     public static final String VISUAL_HOST_KEY = "VisualHostKey";
     public static final String DEFAULT_VISUAL_HOST_KEY = "no";
 
+    public static final String SERVER_ALIVE_INTERVAL_PROP = "ServerAliveInterval";
+    public static final long DEFAULT_ALIVE_INTERVAL = 0L;
+
     private SshServerConfigFileReader() {
         throw new UnsupportedOperationException("No instance allowed");
     }
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerUserAuthService.java b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerUserAuthService.java
index cf8cb4d..b6a1e55 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerUserAuthService.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerUserAuthService.java
@@ -33,7 +33,9 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.Collectors;
 
@@ -65,8 +67,9 @@ import org.apache.sshd.server.auth.WelcomeBannerPhase;
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public class ServerUserAuthService extends AbstractCloseable implements Service, ServerSessionHolder {
-    private final ServerSession serverSession;
     private final AtomicBoolean welcomeSent = new AtomicBoolean(false);
+    private final Map<String, Object> properties = new ConcurrentHashMap<>();
+    private final ServerSession serverSession;
     private final WelcomeBannerPhase welcomePhase;
     private List<NamedFactory<UserAuth>> userAuthFactories;
     private List<List<String>> authMethods;
@@ -86,10 +89,11 @@ public class ServerUserAuthService extends AbstractCloseable implements Service,
             throw new SshException("Session already authenticated");
         }
 
-        Object phase = PropertyResolverUtils.getObject(s, ServerAuthenticationManager.WELCOME_BANNER_PHASE);
-        phase = PropertyResolverUtils.toEnum(WelcomeBannerPhase.class, phase, true, WelcomeBannerPhase.VALUES);
+        Object phase = this.getObject(ServerAuthenticationManager.WELCOME_BANNER_PHASE);
+        phase = PropertyResolverUtils.toEnum(WelcomeBannerPhase.class, phase, false, WelcomeBannerPhase.VALUES);
         welcomePhase = (phase == null) ? ServerAuthenticationManager.DEFAULT_BANNER_PHASE : (WelcomeBannerPhase) phase;
-        maxAuthRequests = s.getIntProperty(ServerAuthenticationManager.MAX_AUTH_REQUESTS, ServerAuthenticationManager.DEFAULT_MAX_AUTH_REQUESTS);
+        maxAuthRequests = this.getIntProperty(
+            ServerAuthenticationManager.MAX_AUTH_REQUESTS, ServerAuthenticationManager.DEFAULT_MAX_AUTH_REQUESTS);
 
         List<NamedFactory<UserAuth>> factories = ValidateUtils.checkNotNullAndNotEmpty(
             serverSession.getUserAuthFactories(), "No user auth factories for %s", s);
@@ -97,7 +101,7 @@ public class ServerUserAuthService extends AbstractCloseable implements Service,
         // Get authentication methods
         authMethods = new ArrayList<>();
 
-        String mths = s.getString(ServerAuthenticationManager.AUTH_METHODS);
+        String mths = this.getString(ServerAuthenticationManager.AUTH_METHODS);
         if (GenericUtils.isEmpty(mths)) {
             for (NamedFactory<UserAuth> uaf : factories) {
                 authMethods.add(new ArrayList<>(Collections.singletonList(uaf.getName())));
@@ -149,6 +153,11 @@ public class ServerUserAuthService extends AbstractCloseable implements Service,
     }
 
     @Override
+    public Map<String, Object> getProperties() {
+        return properties;
+    }
+
+    @Override
     public synchronized void process(int cmd, Buffer buffer) throws Exception {
         Boolean authed = Boolean.FALSE;
         ServerSession session = getServerSession();
@@ -448,7 +457,7 @@ public class ServerUserAuthService extends AbstractCloseable implements Service,
             return null;
         }
 
-        String lang = PropertyResolverUtils.getStringProperty(session,
+        String lang = this.getStringProperty(
             ServerAuthenticationManager.WELCOME_BANNER_LANGUAGE,
             ServerAuthenticationManager.DEFAULT_WELCOME_BANNER_LANGUAGE);
         Buffer buffer = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_BANNER,
@@ -464,7 +473,7 @@ public class ServerUserAuthService extends AbstractCloseable implements Service,
     }
 
     protected String resolveWelcomeBanner(ServerSession session) throws IOException {
-        Object bannerValue = session.getObject(ServerAuthenticationManager.WELCOME_BANNER);
+        Object bannerValue = this.getObject(ServerAuthenticationManager.WELCOME_BANNER);
         if (bannerValue == null) {
             return null;
         }
@@ -523,7 +532,8 @@ public class ServerUserAuthService extends AbstractCloseable implements Service,
         }
 
         if (bannerValue instanceof URL) {
-            Charset cs = PropertyResolverUtils.getCharset(session, ServerAuthenticationManager.WELCOME_BANNER_CHARSET, Charset.defaultCharset());
+            Charset cs = this.getCharset(
+                ServerAuthenticationManager.WELCOME_BANNER_CHARSET, Charset.defaultCharset());
             return loadWelcomeBanner(session, (URL) bannerValue, cs);
         }
 
diff --git a/sshd-core/src/test/java/org/apache/sshd/deprecated/ClientUserAuthServiceOld.java b/sshd-core/src/test/java/org/apache/sshd/deprecated/ClientUserAuthServiceOld.java
index 44d8c70..9b4b963 100644
--- a/sshd-core/src/test/java/org/apache/sshd/deprecated/ClientUserAuthServiceOld.java
+++ b/sshd-core/src/test/java/org/apache/sshd/deprecated/ClientUserAuthServiceOld.java
@@ -19,6 +19,8 @@
 package org.apache.sshd.deprecated;
 
 import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
 import org.apache.sshd.client.auth.keyboard.UserInteraction;
 import org.apache.sshd.client.future.AuthFuture;
@@ -54,6 +56,8 @@ public class ClientUserAuthServiceOld extends AbstractCloseable implements Servi
         }
     }
 
+    private final Map<String, Object> properties = new ConcurrentHashMap<>();
+
     /**
      * When !authFuture.isDone() the current authentication
      */
@@ -84,6 +88,11 @@ public class ClientUserAuthServiceOld extends AbstractCloseable implements Servi
     }
 
     @Override
+    public Map<String, Object> getProperties() {
+        return properties;
+    }
+
+    @Override
     public void start() {
         synchronized (lock) {
             log.debug("accepted");