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

twolf pushed a commit to branch dev_3.0
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit 7ec8197e9751cde2ecaaf27532532ab1ad42ace6
Author: Thomas Wolf <tw...@apache.org>
AuthorDate: Tue Apr 22 20:05:28 2025 +0200

    Simplify CoreModuleProperties.SEND_IMMEDIATE_KEXINIT
    
    Setting this property to false should not only delay sending the
    client's initial KEX_INIT until has received the server. It should
    delay the whole KEX, including determining the client's proposal,
    until that point, so that the client can adapt its own proposal
    based on what the server sent (ident and server proposal).
    
    So remove the DelayKexFilter, and just don't do anything in
    KexFilter.startKex() if the property is false and it's the very first
    call. (Well, we send a null message through the filter chain to tell
    lower filters that they should initialize. In particular, this triggers
    the IdentFilter to send the client's SSH protocol version string.)
    
    Add a test to verify that we do have the server's ident and proposal
    when the client finally builds its own proposal.
---
 docs/technical/filters.md                          |   7 +-
 .../common/session/filters/DelayKexInitFilter.java | 132 ---------------------
 .../common/session/filters/SshTransportFilter.java |   4 -
 .../sshd/common/session/filters/kex/KexFilter.java |  27 +++--
 .../java/org/apache/sshd/client/ClientTest.java    |  28 +++++
 5 files changed, 50 insertions(+), 148 deletions(-)

diff --git a/docs/technical/filters.md b/docs/technical/filters.md
index 28123d50f..9d4899371 100644
--- a/docs/technical/filters.md
+++ b/docs/technical/filters.md
@@ -150,7 +150,7 @@ chain should not be modified anymore. Adding filters to or 
removing filters from
 are coming in or going out would be very tricky to get right without race 
conditions.
 
 If there are filters that should not be active after some time, they can set 
their incoming or outgoing
-handlers to `null` (use an `AtomicReference`).- An example would be the 
aforementioned proxy filter: once the
+handlers to `null` (use an `AtomicReference`). An example would be the 
aforementioned proxy filter: once the
 proxy tunnel is established (or the HAproxy protocol header has been 
received), such a proxy filter will
 not do anything anymore but pass on messages. Such a filter can then set its 
handlers to `null`. The filter
 chain will skip any filter that has a `null` handler for the direction a 
particular message is going.
@@ -180,9 +180,8 @@ server's messages:
 * `CoreModuleProperties.SEND_IMMEDIATE_IDENTIFICATION`: by default `true`. If 
`false`, the client sends its SSH
 protocol version only after it has received the server's. This is implemented 
transparently in `IdentFilter`.
 * `CoreModuleProperties.SEND_IMMEDIATE_KEXINIT`: by default `true`. If 
`false`, the client sends its `SSH_MSG_KEXINIT`
-only after it has received the server's. This is handled in a special 
`DelayKexFilter` that sits below the
-`KexFilter` and that is omitted on purpose from the above diagrams since it is 
a really special special case
-and including it would only have confused the presentation.
+only after it has received the server's. The client thus has a chance to adapt 
its own proposal depending on
+the server's ident and key exchange proposal.
 
 These two options were introduced long ago to handle connecting to old SSH 
servers. In SSH version 1, the
 server sent its protocol version string first, and the client always came 
second. In SSH version 2, the
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/session/filters/DelayKexInitFilter.java
 
b/sshd-core/src/main/java/org/apache/sshd/common/session/filters/DelayKexInitFilter.java
deleted file mode 100644
index b63e028ff..000000000
--- 
a/sshd-core/src/main/java/org/apache/sshd/common/session/filters/DelayKexInitFilter.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * 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.filters;
-
-import java.io.IOException;
-import java.util.Objects;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-
-import org.apache.sshd.common.SshConstants;
-import org.apache.sshd.common.filter.BufferInputHandler;
-import org.apache.sshd.common.filter.InputHandler;
-import org.apache.sshd.common.filter.IoFilter;
-import org.apache.sshd.common.filter.OutputHandler;
-import org.apache.sshd.common.io.DefaultIoWriteFuture;
-import org.apache.sshd.common.io.IoWriteFuture;
-import org.apache.sshd.common.session.Session;
-import org.apache.sshd.common.util.buffer.Buffer;
-import org.apache.sshd.core.CoreModuleProperties;
-
-/**
- * A filter implementing "delayed KEX-INIT" where the client waits for the 
server's initial KEX-INIT to arrive first
- * before sending its own.
- */
-public class DelayKexInitFilter extends IoFilter {
-
-    private AtomicBoolean isFirst = new AtomicBoolean(true);
-
-    private final DefaultIoWriteFuture initReceived = new 
DefaultIoWriteFuture(this, null);
-
-    private final AtomicReference<InputHandler> input = new 
AtomicReference<>();
-
-    private final AtomicReference<OutputHandler> output = new 
AtomicReference<>();
-
-    private Session session;
-
-    public DelayKexInitFilter() {
-        input.set(new KexInputHandler());
-        output.set(new KexOutputHandler());
-    }
-
-    @Override
-    public InputHandler in() {
-        return input.get();
-    }
-
-    @Override
-    public OutputHandler out() {
-        return output.get();
-    }
-
-    public void setSession(Session session) {
-        this.session = Objects.requireNonNull(session);
-    }
-
-    private class KexInputHandler implements BufferInputHandler {
-
-        KexInputHandler() {
-            super();
-        }
-
-        @Override
-        public void handleMessage(Buffer message) throws Exception {
-            if (input.get() != null) {
-                int cmd = message.rawByte(message.rpos());
-                if (cmd == SshConstants.SSH_MSG_KEXINIT) {
-                    initReceived.setValue(Boolean.TRUE);
-                    input.set(null);
-                }
-            }
-            owner().passOn(message);
-        }
-
-    }
-
-    private class KexOutputHandler implements OutputHandler {
-
-        KexOutputHandler() {
-            super();
-        }
-
-        @Override
-        public IoWriteFuture send(int cmd, Buffer message) throws IOException {
-            if (cmd != SshConstants.SSH_MSG_KEXINIT || output.get() == null || 
message == null) {
-                return owner().send(cmd, message);
-            }
-            boolean first = isFirst.getAndSet(false);
-            if (!first || session.isServerSession()
-                    || 
CoreModuleProperties.SEND_IMMEDIATE_KEXINIT.getRequired(session).booleanValue())
 {
-                return owner().send(cmd, message).addListener(f -> 
output.set(null));
-            }
-            // We're a client, and we delay sending the initial KEX-INIT until 
we have received the peer's KEX-INIT
-            IoWriteFuture initial = owner().send(-1, null);
-            DefaultIoWriteFuture result = new 
DefaultIoWriteFuture(KexOutputHandler.this, null);
-            initial.addListener(init -> {
-                Throwable t = init.getException();
-                if (t != null) {
-                    result.setValue(t);
-                    return;
-                }
-                initReceived.addListener(f -> {
-                    try {
-                        owner().send(cmd, message).addListener(g -> {
-                            output.set(null);
-                            result.setValue(g.isWritten() ? Boolean.TRUE : 
g.getException());
-                        });
-                    } catch (IOException e) {
-                        result.setValue(e);
-                    }
-                });
-            });
-            return result;
-        }
-
-    }
-}
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/session/filters/SshTransportFilter.java
 
b/sshd-core/src/main/java/org/apache/sshd/common/session/filters/SshTransportFilter.java
index 2223b29a8..273b00b5f 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/common/session/filters/SshTransportFilter.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/common/session/filters/SshTransportFilter.java
@@ -80,10 +80,6 @@ public class SshTransportFilter extends IoFilter {
 
         filters.addLast(new PacketLoggingFilter(session, cryptFilter));
 
-        DelayKexInitFilter delayKexFilter = new DelayKexInitFilter();
-        delayKexFilter.setSession(session);
-        filters.addLast(delayKexFilter);
-
         filters.addLast(new InjectIgnoreFilter(session, random));
 
         kexFilter = new KexFilter(session, random, cryptFilter, 
compressionFilter, events, proposer, checker);
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/session/filters/kex/KexFilter.java
 
b/sshd-core/src/main/java/org/apache/sshd/common/session/filters/kex/KexFilter.java
index 4be26057e..e41cdabee 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/common/session/filters/kex/KexFilter.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/common/session/filters/kex/KexFilter.java
@@ -33,6 +33,7 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.stream.Collectors;
@@ -144,6 +145,8 @@ public class KexFilter extends IoFilter {
 
     private final CopyOnWriteArrayList<KexListener> listeners = new 
CopyOnWriteArrayList<>();
 
+    private final AtomicBoolean firstKex = new AtomicBoolean();
+
     private final AtomicReference<KexState> kexState = new 
AtomicReference<>(KexState.DONE);
 
     private final AtomicReference<DefaultKeyExchangeFuture> kexFuture = new 
AtomicReference<>();
@@ -278,19 +281,18 @@ public class KexFilter extends IoFilter {
     }
 
     public Map<KexProposalOption, String> getNegotiated() {
-        return Collections.unmodifiableMap(negotiated.get());
+        Map<KexProposalOption, String> result = negotiated.get();
+        return result == null ? null : Collections.unmodifiableMap(result);
     }
 
     public Map<KexProposalOption, String> getClientProposal() {
-        return session.isServerSession()
-                ? Collections.unmodifiableMap(peerProposal.get())
-                : Collections.unmodifiableMap(myProposal.get());
+        Map<KexProposalOption, String> result = session.isServerSession() ? 
peerProposal.get() : myProposal.get();
+        return result == null ? null : Collections.unmodifiableMap(result);
     }
 
     public Map<KexProposalOption, String> getServerProposal() {
-        return session.isServerSession()
-                ? Collections.unmodifiableMap(myProposal.get())
-                : Collections.unmodifiableMap(peerProposal.get());
+        Map<KexProposalOption, String> result = session.isServerSession() ? 
myProposal.get() : peerProposal.get();
+        return result == null ? null : Collections.unmodifiableMap(result);
     }
 
     public void setClientIdent(String ident) {
@@ -1061,6 +1063,16 @@ public class KexFilter extends IoFilter {
     }
 
     public KeyExchangeFuture startKex() throws Exception {
+        DefaultKeyExchangeFuture result = new 
DefaultKeyExchangeFuture(session.toString(), session.getFutureLock());
+        if (firstKex.compareAndSet(false, true) && !session.isServerSession()
+                && 
!CoreModuleProperties.SEND_IMMEDIATE_KEXINIT.getRequired(session).booleanValue())
 {
+            // We're a client, and we're supposed to wait for the server's 
proposal. Just trigger sending the SSH
+            // protocol ident, but don't do anything else. When we get the 
server's KEX-INIT, we'll evaluate and send
+            // our proposal, then do the negotiation.
+            owner().send(-1, null);
+            result.setValue(Boolean.FALSE);
+            return result;
+        }
         boolean start = output.updateState(() -> {
             if (kexState.compareAndSet(KexState.DONE, KexState.INIT)) {
                 output.initNewKeyExchange();
@@ -1068,7 +1080,6 @@ public class KexFilter extends IoFilter {
             }
             return false;
         });
-        DefaultKeyExchangeFuture result = new 
DefaultKeyExchangeFuture(session.toString(), session.getFutureLock());
         if (start) {
             listeners.forEach(listener -> listener.event(true));
             kexFuture.set(result);
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java 
b/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java
index f82eb40c4..d2dfa3ea7 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java
@@ -101,6 +101,8 @@ import org.apache.sshd.common.io.IoOutputStream;
 import org.apache.sshd.common.io.IoReadFuture;
 import org.apache.sshd.common.io.IoSession;
 import org.apache.sshd.common.io.IoWriteFuture;
+import org.apache.sshd.common.kex.KexProposalOption;
+import org.apache.sshd.common.kex.extension.DefaultClientKexExtensionHandler;
 import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
 import org.apache.sshd.common.session.ConnectionService;
@@ -743,6 +745,32 @@ public class ClientTest extends BaseTestSupport {
         assertNull(clientSessionHolder.get(), "Session closure not signalled");
     }
 
+    @Test
+    void delayedKexInit() throws Exception {
+        CoreModuleProperties.SEND_IMMEDIATE_KEXINIT.set(client, false);
+        AtomicReference<String> serverId = new AtomicReference<>();
+        AtomicReference<Map<KexProposalOption, String>> serverProposal = new 
AtomicReference<>();
+        client.setKexExtensionHandler(new DefaultClientKexExtensionHandler() {
+
+            @Override
+            public void handleKexInitProposal(Session session, boolean 
initiator, Map<KexProposalOption, String> proposal)
+                    throws Exception {
+                serverId.set(session.getServerVersion());
+                serverProposal.set(session.getServerKexProposals());
+                super.handleKexInitProposal(session, initiator, proposal);
+            }
+        });
+        client.start();
+        try (ClientSession session = client.connect("user1", TEST_LOCALHOST, 
port).verify(CONNECT_TIMEOUT).getSession()) {
+            session.addPasswordIdentity("user1");
+            session.auth().verify(AUTH_TIMEOUT);
+        } finally {
+            client.stop();
+        }
+        assertNotNull(serverId.get());
+        assertNotNull(serverProposal.get());
+    }
+
     @Test
     void client() throws Exception {
         client.start();

Reply via email to