diff --git a/sshd-common/src/main/java/org/apache/sshd/common/SshConstants.java b/sshd-common/src/main/java/org/apache/sshd/common/SshConstants.java index 05d201995..63d2c29e1 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/SshConstants.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/SshConstants.java @@ -95,6 +95,9 @@ public final class SshConstants { public static final byte SSH_MSG_CHANNEL_SUCCESS = 99; public static final byte SSH_MSG_CHANNEL_FAILURE = 100; + public static final byte SSH_MSG_PING = (byte) 192; + public static final byte SSH_MSG_PONG = (byte) 193; + // // Disconnect error codes // diff --git a/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/KexExtensions.java b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/KexExtensions.java index f275227e1..904c39e94 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/KexExtensions.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/KexExtensions.java @@ -40,6 +40,7 @@ import org.apache.sshd.common.kex.extension.parser.DelayCompression; import org.apache.sshd.common.kex.extension.parser.Elevation; import org.apache.sshd.common.kex.extension.parser.NoFlowControl; +import org.apache.sshd.common.kex.extension.parser.PingPong; import org.apache.sshd.common.kex.extension.parser.ServerSignatureAlgorithms; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.MapEntryUtils; @@ -84,7 +85,8 @@ public final class KexExtensions { ServerSignatureAlgorithms.INSTANCE, NoFlowControl.INSTANCE, Elevation.INSTANCE, - DelayCompression.INSTANCE) + DelayCompression.INSTANCE, + PingPong.INSTANCE) .collect(Collectors.toMap( NamedResource::getName, Function.identity(), MapEntryUtils.throwingMerger(), () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER))); diff --git a/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/PingPong.java b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/PingPong.java new file mode 100644 index 000000000..4e64029c6 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/kex/extension/parser/PingPong.java @@ -0,0 +1,74 @@ +/* + * 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.kex.extension.parser; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.apache.sshd.common.util.buffer.Buffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Apache MINA SSHD Project + * @see OpenSSH PROTOCOL + */ +public class PingPong extends AbstractKexExtensionParser { + public static final String NAME = "ping@openssh.com"; + + public static final PingPong INSTANCE = new PingPong(); + + private static final Logger LOG = LoggerFactory.getLogger(PingPong.class); + + public PingPong() { + super(NAME); + } + + @Override + public Integer parseExtension(Buffer buffer) throws IOException { + return parseExtension(buffer.array(), buffer.rpos(), buffer.available()); + } + + @Override + public Integer parseExtension(byte[] data, int off, int len) throws IOException { + if (len <= 0) { + if (LOG.isDebugEnabled()) { + LOG.debug("Inconsistent KEX extension {} received; no data (len={})", NAME, len); + } + return null; + } + String value = new String(data, off, len, StandardCharsets.UTF_8); + try { + Integer result = Integer.valueOf(Integer.parseUnsignedInt(value)); + LOG.info("Server announced support for {} version {}", NAME, result); + return result; + } catch (NumberFormatException e) { + if (LOG.isDebugEnabled()) { + LOG.debug("Cannot parse KEX extension {} version {}", NAME, value); + } + } + return null; + } + + @Override + protected void encode(Integer version, Buffer buffer) throws IOException { + buffer.putString(version.toString()); + } +} diff --git a/sshd-core/src/main/java/org/apache/sshd/client/channel/PtyCapableChannelSession.java b/sshd-core/src/main/java/org/apache/sshd/client/channel/PtyCapableChannelSession.java index 459356ab5..b4cf7ba3b 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/channel/PtyCapableChannelSession.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/channel/PtyCapableChannelSession.java @@ -18,15 +18,24 @@ */ package org.apache.sshd.client.channel; +import java.io.EOFException; import java.io.IOException; import java.util.Collections; +import java.util.Locale; import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.sshd.common.FactoryManager; import org.apache.sshd.common.SshConstants; import org.apache.sshd.common.channel.PtyChannelConfiguration; import org.apache.sshd.common.channel.PtyChannelConfigurationHolder; import org.apache.sshd.common.channel.PtyChannelConfigurationMutator; import org.apache.sshd.common.channel.PtyMode; +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.util.GenericUtils; import org.apache.sshd.common.util.MapEntryUtils; @@ -34,6 +43,9 @@ import org.apache.sshd.common.util.buffer.ByteArrayBuffer; import org.apache.sshd.core.CoreModuleProperties; +import static org.apache.sshd.common.SshConstants.SSH_MSG_PING; +import static org.apache.sshd.core.CoreModuleProperties.OBFUSCATE_KEYSTROKE_TIMING; + /** *

* Serves as the base channel session for executing remote commands - including a full shell. Note: all the @@ -80,9 +92,12 @@ * @author Apache MINA SSHD Project */ public class PtyCapableChannelSession extends ChannelSession implements PtyChannelConfigurationMutator { + private static final String PING_MESSAGE = "PING!"; private boolean agentForwarding; private boolean usePty; + private int obfuscate; private final PtyChannelConfiguration config; + private final AtomicReference> chaffFuture = new AtomicReference<>(); public PtyCapableChannelSession(boolean usePty, PtyChannelConfigurationHolder configHolder, Map env) { this.usePty = usePty; @@ -267,8 +282,62 @@ protected void doOpenPty() throws IOException { modes.putByte(PtyMode.TTY_OP_END); buffer.putBytes(modes.getCompactData()); writePacket(buffer); + + String obf + = OBFUSCATE_KEYSTROKE_TIMING.get(getSession()).orElse(Boolean.FALSE.toString()).toLowerCase(Locale.ENGLISH); + if (obf.equals("yes") || obf.equals("true")) { + obfuscate = 20; + } else if (obf.equals("no") || obf.equals("false")) { + obfuscate = 0; + } else if (obf.matches("interval:[0-9]{1,5}")) { + obfuscate = Integer.parseInt(obf.substring("interval:".length())); + } else { + log.warn("doOpenPty({}) unrecognized value {} for property {}", this, obf, + OBFUSCATE_KEYSTROKE_TIMING.getName()); + } } sendEnvVariables(session); } + + @Override + public IoWriteFuture writePacket(Buffer buffer) throws IOException { + if (obfuscate > 0 && buffer.available() < 256) { + log.info("Sending: "); + if (mayWrite()) { + Session s = getSession(); + return s.writePacket(buffer); + } + if (log.isDebugEnabled()) { + log.debug("writePacket({}) Discarding output packet because channel state={}", this, state); + } + return AbstractIoWriteFuture.fulfilled(toString(), new EOFException("Channel is being closed")); + } else { + return super.writePacket(buffer); + } + } + + protected void scheduleChaff() { + FactoryManager manager = getSession().getFactoryManager(); + ScheduledExecutorService service = manager.getScheduledExecutorService(); + long delay = 1024 + manager.getRandomFactory().get().random(2048); + ScheduledFuture future = service.schedule(this::sendChaff, delay, TimeUnit.MILLISECONDS); + future = this.chaffFuture.getAndSet(future); + if (future != null) { + future.cancel(false); + } + } + + protected void sendChaff() { + try { + Buffer buf = getSession().createBuffer(SSH_MSG_PING, PING_MESSAGE.length() + Integer.SIZE); + buf.putString(PING_MESSAGE); + getSession().writePacket(buf); + } catch (IOException e) { + if (log.isDebugEnabled()) { + log.debug("Error sending chaff message", e); + } + } + } + } diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/DefaultClientKexExtensionHandler.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/DefaultClientKexExtensionHandler.java index f70128e40..89f73f153 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/DefaultClientKexExtensionHandler.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/DefaultClientKexExtensionHandler.java @@ -30,6 +30,7 @@ import org.apache.sshd.common.AttributeRepository.AttributeKey; import org.apache.sshd.common.NamedFactory; import org.apache.sshd.common.kex.extension.parser.HostBoundPubkeyAuthentication; +import org.apache.sshd.common.kex.extension.parser.PingPong; import org.apache.sshd.common.kex.extension.parser.ServerSignatureAlgorithms; import org.apache.sshd.common.session.Session; import org.apache.sshd.common.signature.Signature; @@ -58,6 +59,11 @@ public class DefaultClientKexExtensionHandler extends AbstractLoggingBean implem */ public static final AttributeKey HOSTBOUND_AUTHENTICATION = new AttributeKey<>(); + /** + * Session {@link AttributeKey} storing the version if the server supports ping-pong requests. + */ + public static final AttributeKey PING_PONG = new AttributeKey<>(); + public DefaultClientKexExtensionHandler() { super(); } @@ -88,6 +94,21 @@ public boolean handleKexExtensionRequest( } else { session.setAttribute(HOSTBOUND_AUTHENTICATION, version); } + } else if (PingPong.NAME.equals(name)) { + Integer version = PingPong.INSTANCE.parseExtension(data); + if (version == null) { + if (log.isDebugEnabled()) { + log.debug("handleKexExtensionRequest({}) : ignoring unknown {} extension", session, + PingPong.NAME); + } + } else if (version != 0) { + if (log.isDebugEnabled()) { + log.debug("handleKexExtensionRequest({}) : ignoring unknown {} version {}", session, + PingPong.NAME, version); + } + } else { + session.setAttribute(PING_PONG, version); + } } return true; } diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/DefaultServerKexExtensionHandler.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/DefaultServerKexExtensionHandler.java index 7d5924622..993eacece 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/DefaultServerKexExtensionHandler.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/DefaultServerKexExtensionHandler.java @@ -27,6 +27,7 @@ import org.apache.sshd.common.AttributeRepository.AttributeKey; import org.apache.sshd.common.kex.KexProposalOption; +import org.apache.sshd.common.kex.extension.parser.PingPong; import org.apache.sshd.common.kex.extension.parser.ServerSignatureAlgorithms; import org.apache.sshd.common.session.Session; import org.apache.sshd.common.util.GenericUtils; @@ -157,5 +158,7 @@ public void collectExtensions(Session session, KexPhase phase, BiConsumer OBFUSCATE_KEYSTROKE_TIMING + = Property.string("obfuscate-keystroke-timing", Boolean.FALSE.toString()); + private CoreModuleProperties() { throw new UnsupportedOperationException("No instance"); }