From 526e3a76029a47a03ff1525b1ef87a4a69fbcf04 Mon Sep 17 00:00:00 2001 From: Kenny Root Date: Wed, 22 Apr 2026 18:18:11 -0700 Subject: [PATCH] Add a way to get the connection addresses The local and remote socket address is used for tracking connectivity in ConnectBot. --- .../java/com/trilead/ssh2/Connection.java | 28 ++++++++++++++++ .../java/com/trilead/ssh2/ConnectionInfo.java | 12 +++++++ .../trilead/ssh2/transport/KexManager.java | 10 ++++++ .../ssh2/transport/TransportManager.java | 21 ++++++++++++ .../java/com/trilead/ssh2/ConnectionTest.java | 33 ++++++++++++++++--- .../ssh2/OpenSSHCompatibilityTest.java | 24 ++++++++++++++ 6 files changed, 123 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/trilead/ssh2/Connection.java b/src/main/java/com/trilead/ssh2/Connection.java index 1cacafcd..0de952a0 100644 --- a/src/main/java/com/trilead/ssh2/Connection.java +++ b/src/main/java/com/trilead/ssh2/Connection.java @@ -1075,6 +1075,34 @@ public synchronized ConnectionInfo getConnectionInfo() throws IOException return tm.getConnectionInfo(1); } + /** + * Returns the local address to which the socket is bound. + * + * @return the local address to which the socket is bound. + * @throws IOException + * in case of any problem. + */ + public synchronized InetSocketAddress getLocalSocketAddress() throws IOException + { + if (tm == null) + throw new IllegalStateException("You need to establish a connection first."); + return tm.getLocalSocketAddress(); + } + + /** + * Returns the address of the endpoint this socket is connected to. + * + * @return the address of the endpoint this socket is connected to. + * @throws IOException + * in case of any problem. + */ + public synchronized InetSocketAddress getRemoteSocketAddress() throws IOException + { + if (tm == null) + throw new IllegalStateException("You need to establish a connection first."); + return tm.getRemoteSocketAddress(); + } + /** * After a successful connect, one has to authenticate oneself. This method * can be used to tell which authentication methods are supported by the diff --git a/src/main/java/com/trilead/ssh2/ConnectionInfo.java b/src/main/java/com/trilead/ssh2/ConnectionInfo.java index d508acf5..230c5778 100644 --- a/src/main/java/com/trilead/ssh2/ConnectionInfo.java +++ b/src/main/java/com/trilead/ssh2/ConnectionInfo.java @@ -1,6 +1,8 @@ package com.trilead.ssh2; +import java.net.InetSocketAddress; + /** * In most cases you probably do not need the information contained in here. * @@ -9,6 +11,16 @@ */ public class ConnectionInfo { + /** + * The address of the local socket. + */ + public InetSocketAddress localSocketAddress; + + /** + * The address of the remote socket. + */ + public InetSocketAddress remoteSocketAddress; + /** * The used key exchange (KEX) algorithm in the latest key exchange. */ diff --git a/src/main/java/com/trilead/ssh2/transport/KexManager.java b/src/main/java/com/trilead/ssh2/transport/KexManager.java index c904d0be..49d07a28 100644 --- a/src/main/java/com/trilead/ssh2/transport/KexManager.java +++ b/src/main/java/com/trilead/ssh2/transport/KexManager.java @@ -729,6 +729,16 @@ public synchronized void handleMessage(byte[] msg, int msglen) throws IOExceptio ConnectionInfo sci = new ConnectionInfo(); + try + { + sci.localSocketAddress = tm.getLocalSocketAddress(); + sci.remoteSocketAddress = tm.getRemoteSocketAddress(); + } + catch (IOException e) + { + /* Ignore, if the connection is closed then it doesn't matter much anyway */ + } + kexCount++; sci.keyExchangeAlgorithm = kxs.np.kex_algo; diff --git a/src/main/java/com/trilead/ssh2/transport/TransportManager.java b/src/main/java/com/trilead/ssh2/transport/TransportManager.java index f7376b58..5cef65d0 100644 --- a/src/main/java/com/trilead/ssh2/transport/TransportManager.java +++ b/src/main/java/com/trilead/ssh2/transport/TransportManager.java @@ -4,6 +4,7 @@ import com.trilead.ssh2.ExtensionInfo; import com.trilead.ssh2.packets.PacketExtInfo; import java.io.IOException; +import java.net.InetSocketAddress; import java.net.Socket; import java.security.SecureRandom; import java.util.Vector; @@ -184,6 +185,26 @@ public int getPort() return port; } + public InetSocketAddress getLocalSocketAddress() throws IOException + { + synchronized (connectionSemaphore) + { + if (sock == null) + throw new IOException("The connection is closed."); + return (InetSocketAddress) sock.getLocalSocketAddress(); + } + } + + public InetSocketAddress getRemoteSocketAddress() throws IOException + { + synchronized (connectionSemaphore) + { + if (sock == null) + throw new IOException("The connection is closed."); + return (InetSocketAddress) sock.getRemoteSocketAddress(); + } + } + public ServerHostKeyVerifier getServerHostKeyVerifier() { return km != null ? km.getServerHostKeyVerifier() : null; diff --git a/src/test/java/com/trilead/ssh2/ConnectionTest.java b/src/test/java/com/trilead/ssh2/ConnectionTest.java index daef4fcc..500270a8 100644 --- a/src/test/java/com/trilead/ssh2/ConnectionTest.java +++ b/src/test/java/com/trilead/ssh2/ConnectionTest.java @@ -149,13 +149,36 @@ public void testSetSecureRandomWithNull() { @Test public void testGetConnectionInfoThrowsWhenNotConnected() { try { - connection.getConnectionInfo(); - fail("Should throw IllegalStateException when not connected"); + connection.getConnectionInfo(); + fail("Should throw IllegalStateException when not connected"); } catch (IllegalStateException e) { - assertTrue(e.getMessage().contains("establish a connection first"), "Exception message should indicate connection required"); + assertTrue(e.getMessage().contains("establish a connection first"), "Exception message should indicate connection required"); } catch (Exception e) { - fail("Should throw IllegalStateException, got: " + - e.getClass().getSimpleName()); + fail("Should throw IllegalStateException, got: " + e.getClass().getSimpleName()); + } +} + +@Test +public void testGetLocalSocketAddressThrowsWhenNotConnected() { + try { + connection.getLocalSocketAddress(); + fail("Should throw IllegalStateException when not connected"); + } catch (IllegalStateException e) { + assertTrue(e.getMessage().contains("establish a connection first"), "Exception message should indicate connection required"); + } catch (Exception e) { + fail("Should throw IllegalStateException, got: " + e.getClass().getSimpleName()); + } +} + +@Test +public void testGetRemoteSocketAddressThrowsWhenNotConnected() { + try { + connection.getRemoteSocketAddress(); + fail("Should throw IllegalStateException when not connected"); + } catch (IllegalStateException e) { + assertTrue(e.getMessage().contains("establish a connection first"), "Exception message should indicate connection required"); + } catch (Exception e) { + fail("Should throw IllegalStateException, got: " + e.getClass().getSimpleName()); } } diff --git a/src/test/java/com/trilead/ssh2/OpenSSHCompatibilityTest.java b/src/test/java/com/trilead/ssh2/OpenSSHCompatibilityTest.java index c436b3ce..37806883 100644 --- a/src/test/java/com/trilead/ssh2/OpenSSHCompatibilityTest.java +++ b/src/test/java/com/trilead/ssh2/OpenSSHCompatibilityTest.java @@ -15,10 +15,12 @@ import org.testcontainers.images.builder.ImageFromDockerfile; import java.io.IOException; +import java.net.InetSocketAddress; import com.trilead.ssh2.crypto.Base64; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; /** @@ -101,6 +103,28 @@ private void canConnectWithPubkey(String keyFilename) throws Exception { } } + @Test + public void testSocketAddress() throws IOException { + try (GenericContainer server = getBaseContainer()) { + server.start(); + try (Connection c = withServer(server)) { + c.connect(verifier); + + InetSocketAddress local = c.getLocalSocketAddress(); + InetSocketAddress remote = c.getRemoteSocketAddress(); + + assertThat(local, notNullValue()); + assertThat(remote, notNullValue()); + assertThat(remote.getHostString(), is(server.getHost())); + assertThat(remote.getPort(), is(server.getMappedPort(22))); + + ConnectionInfo info = c.getConnectionInfo(); + assertThat(info.localSocketAddress.toString(), is(local.toString())); + assertThat(info.remoteSocketAddress.toString(), is(remote.toString())); + } + } + } + @Test public void canConnectWithEd25519() throws Exception { canConnectWithPubkey("ed25519-openssh2-private-key.txt");