Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Received SSH_MSG_UNIMPLEMENTED while exchanging keys with KeepAlive Enabled #810

Closed
exceptionfactory opened this issue Sep 14, 2022 · 1 comment

Comments

@exceptionfactory
Copy link
Contributor

Starting several SSH connections across multiple threads can result in an SSH server sending SSH_MSG_UNIMPLEMENTED during key exchange as shown in the follow error log:

ERROR net.schmizz.sshj.transport.TransportImpl - Dying because - Received SSH_MSG_UNIMPLEMENTED while exchanging keys
net.schmizz.sshj.transport.TransportException: Received SSH_MSG_UNIMPLEMENTED while exchanging keys
	at net.schmizz.sshj.transport.TransportImpl.gotUnimplemented(TransportImpl.java:565)
	at net.schmizz.sshj.transport.TransportImpl.handle(TransportImpl.java:495)
	at net.schmizz.sshj.transport.Decoder.decode(Decoder.java:113)
	at net.schmizz.sshj.transport.Decoder.received(Decoder.java:200)
	at net.schmizz.sshj.transport.Reader.run(Reader.java:60)

This appears to occur more frequently in SSHJ 0.33.0 and 0.34.0 with SSH KeepAlive enabled. These versions include changes in pull request #752, which start the KeepAlive Thread in SSHClient.onConnect().

The underlying problem appears to be related to the timing of sending and receiving SSH_MSG_KEXINIT packets. Sending an SSH_MSG_KEXINIT packet can be initiated from different locations. The SSHClient.onConnect() method calls Transport.doKex(), which calls KeyExchanger.startKex(). KexExchange.handle() can also call KeyExchanger.startKex() when receiving an SSH_MSG_KEXINIT packet. This can cause problems with multiple connections due to different threads calling KeyExchanger.startKex() at different times.

When an OpenSSH server receives an unexpected SSH_MSG_KEXINIT packet, it sends an SSH_MSG_UNIMPLEMENTED packet and logs the following error:

dispatch_protocol_error: type 20 seq 3 [preauth]

The atmoz/sftp Docker image provides a standard OpenSSH server for testing, but this issue should be reproducible with any recent version of OpenSSH server.

The nature of the problem makes it difficult to reproduce in a unit test, but the following class is capable of causing the error in the process of initiating multiple connection attempts using a pool of threads. The class attempts up to 1000 connections using a pool of four threads. It is not necessary to authenticate to the SSH server since the problem occurs during initial connection key exchange. The class relies of slf4j-simple and Java System properties for console logging. The connect method calls System.exit() at the first error encountered.

import net.schmizz.keepalive.KeepAliveProvider;
import net.schmizz.sshj.DefaultConfig;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.transport.verification.PromiscuousVerifier;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class SshConnectCommand {

    static {
        System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "DEBUG");
        System.setProperty("org.slf4j.simpleLogger.showDateTime", "true");
        System.setProperty("org.slf4j.simpleLogger.dateTimeFormat", "yyyy-MM-dd HH:mm:ss.SSS");
    }

    private static final String SSH_SERVER = "127.0.0.1";
    private static final int SSH_PORT = 2222;

    private static final int KEEP_ALIVE_INTERVAL = 5;
    private static final int POOL_SIZE = 4;
    private static final int CONNECTIONS = 1000;
    private static final int EXECUTION_TIME = 10;

    private static final Logger logger = LoggerFactory.getLogger(SshConnectCommand.class);

    public static void main(String[] args) throws Exception {
        final ExecutorService executorService = Executors.newFixedThreadPool(POOL_SIZE);
        for (int i = 1; i <= CONNECTIONS; i++) {
            final int counter = i;
            executorService.execute(() -> {
                connect(counter);
            });
        }

        executorService.awaitTermination(EXECUTION_TIME, TimeUnit.SECONDS);
        executorService.shutdown();

        logger.info("Completed Connections [{}]", CONNECTIONS);
    }

    private static void connect(int counter) {
        try (final SSHClient sshClient = createClient()) {
            logger.info("Connection Starting [{}]", counter);
            sshClient.connect(SSH_SERVER, SSH_PORT);
        } catch (final Exception e) {
            logger.error("Connection Failed [{}]", counter, e);
            System.exit(1);
        } finally {
            logger.info("Connection Completed [{}]", counter);
        }
    }

    private static SSHClient createClient() {
        final DefaultConfig config = new DefaultConfig();
        config.setKeepAliveProvider(KeepAliveProvider.KEEP_ALIVE);
        final SSHClient sshClient = new SSHClient(config);

        sshClient.getConnection().getKeepAlive().setKeepAliveInterval(KEEP_ALIVE_INTERVAL);
        sshClient.addHostKeyVerifier(new PromiscuousVerifier());
        return sshClient;
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants