From 6a4abf7e500f2a4f33feb7646212e583fe26187a Mon Sep 17 00:00:00 2001 From: hstyi Date: Sun, 30 Mar 2025 16:34:51 +0800 Subject: [PATCH] fix: SSH proxy not working in jump hosts (#435) --- src/main/kotlin/app/termora/SshClients.kt | 158 +++++++++++++++++++--- src/test/resources/sshd/Dockerfile | 5 +- 2 files changed, 141 insertions(+), 22 deletions(-) diff --git a/src/main/kotlin/app/termora/SshClients.kt b/src/main/kotlin/app/termora/SshClients.kt index 5dfb8fd..3658e70 100644 --- a/src/main/kotlin/app/termora/SshClients.kt +++ b/src/main/kotlin/app/termora/SshClients.kt @@ -18,6 +18,7 @@ import org.apache.sshd.client.channel.ClientChannelEvent import org.apache.sshd.client.config.hosts.HostConfigEntry import org.apache.sshd.client.config.hosts.HostConfigEntryResolver import org.apache.sshd.client.config.hosts.KnownHostEntry +import org.apache.sshd.client.future.ConnectFuture import org.apache.sshd.client.kex.DHGClient import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier import org.apache.sshd.client.keyverifier.ModifiedServerKeyAcceptor @@ -29,7 +30,13 @@ import org.apache.sshd.common.SshException import org.apache.sshd.common.channel.PtyChannelConfiguration import org.apache.sshd.common.config.keys.KeyRandomArt import org.apache.sshd.common.config.keys.KeyUtils +import org.apache.sshd.common.future.CloseFuture +import org.apache.sshd.common.future.SshFutureListener import org.apache.sshd.common.global.KeepAliveHandler +import org.apache.sshd.common.io.IoConnectFuture +import org.apache.sshd.common.io.IoConnector +import org.apache.sshd.common.io.IoServiceEventListener +import org.apache.sshd.common.io.IoSession import org.apache.sshd.common.kex.BuiltinDHFactories import org.apache.sshd.common.keyprovider.KeyIdentityProvider import org.apache.sshd.common.util.net.SshdSocketAddress @@ -199,6 +206,10 @@ object SshClients { entry.username = host.username entry.hostName = host.host entry.setProperty("Middleware", middleware.toString()) + entry.setProperty("Host", host.id) + + // 设置代理 +// configureProxy(entry, host, client) // ssh-agent if (host.authentication.type == AuthenticationType.SSHAgent) { @@ -285,11 +296,12 @@ object SshClients { fun openClient(host: Host): SshClient { val builder = ClientBuilder.builder() builder.globalRequestHandlers(listOf(KeepAliveHandler.INSTANCE)) - .factory { JGitSshClient() } + .factory { MyJGitSshClient() } val keyExchangeFactories = ClientBuilder.setUpDefaultKeyExchanges(true).toMutableList() // https://github.com/TermoraDev/termora/issues/123 + @Suppress("DEPRECATION") keyExchangeFactories.addAll( listOf( DHGClient.newFactory(BuiltinDHFactories.dhg1), @@ -345,26 +357,6 @@ object SshClients { sshClient.setKeyPasswordProviderFactory { IdentityPasswordProvider(CredentialsProvider.getDefault()) } - if (host.proxy.type != ProxyType.No) { - sshClient.setProxyDatabase { - if (host.proxy.authenticationType == AuthenticationType.No) ProxyData( - Proxy( - if (host.proxy.type == ProxyType.SOCKS5) Proxy.Type.SOCKS else Proxy.Type.HTTP, - InetSocketAddress(host.proxy.host, host.proxy.port) - ) - ) - else - ProxyData( - Proxy( - if (host.proxy.type == ProxyType.SOCKS5) Proxy.Type.SOCKS else Proxy.Type.HTTP, - InetSocketAddress(host.proxy.host, host.proxy.port) - ), - host.proxy.username, - host.proxy.password.toCharArray(), - ) - } - } - sshClient.start() return sshClient } @@ -497,5 +489,129 @@ object SshClients { } } + + @Suppress("UNCHECKED_CAST") + private class MyJGitSshClient : JGitSshClient() { + companion object { + private val HOST_CONFIG_ENTRY: AttributeRepository.AttributeKey by lazy { + JGitSshClient::class.java.getDeclaredField("HOST_CONFIG_ENTRY").apply { isAccessible = true } + .get(null) as AttributeRepository.AttributeKey + } + } + + override fun createConnector(): IoConnector { + return MyIoConnector(this, super.createConnector()) + } + + /** + * 加上 synchronized ,因为默认代理是全局的(需要在连接时动态修改),所以这里需要一个个连接 + */ + override fun connect( + hostConfig: HostConfigEntry?, + context: AttributeRepository?, + localAddress: SocketAddress? + ): ConnectFuture { + synchronized(this) { + return super.connect(hostConfig, context, localAddress) + } + } + + private class MyIoConnector(private val sshClient: SshClient, private val ioConnector: IoConnector) : + IoConnector { + override fun close(immediately: Boolean): CloseFuture { + return ioConnector.close(immediately) + } + + override fun addCloseFutureListener(listener: SshFutureListener?) { + return ioConnector.addCloseFutureListener(listener) + } + + override fun removeCloseFutureListener(listener: SshFutureListener?) { + return ioConnector.removeCloseFutureListener(listener) + } + + override fun isClosed(): Boolean { + return ioConnector.isClosed + } + + override fun isClosing(): Boolean { + return ioConnector.isClosing + } + + override fun getIoServiceEventListener(): IoServiceEventListener { + return ioConnector.ioServiceEventListener + } + + override fun setIoServiceEventListener(listener: IoServiceEventListener?) { + return ioConnector.setIoServiceEventListener(listener) + } + + override fun getManagedSessions(): MutableMap { + return ioConnector.managedSessions + } + + override fun connect( + targetAddress: SocketAddress, + context: AttributeRepository?, + localAddress: SocketAddress? + ): IoConnectFuture { + var tAddress = targetAddress + val entry = context?.getAttribute(HOST_CONFIG_ENTRY) + if (entry != null) { + val host = hostManager.getHost(entry.getProperty("Host") ?: StringUtils.EMPTY) + if (host != null) { + tAddress = configureProxy(host, tAddress) + } + } + + val proxyConnector = sshClient.clientProxyConnector + val future = ioConnector.connect(tAddress, context, localAddress) + + // 代理是一次性的 + // 如果 tAddress != targetAddress 为 true 那么表示进行代理了 + if (proxyConnector != null && tAddress != targetAddress) { + future.addListener { + if (it.isDone) { + if (sshClient.clientProxyConnector == proxyConnector) { + sshClient.clientProxyConnector = null + } + } + } + } + + return future + } + + private fun configureProxy(host: Host, targetAddress: SocketAddress): SocketAddress { + if (host.proxy.type == ProxyType.No) return targetAddress + if (targetAddress.toString().contains(SshdSocketAddress.LOCALHOST_IPV4)) return targetAddress + + val proxyData = ProxyData( + if (host.proxy.type == ProxyType.HTTP) { + Proxy(Proxy.Type.HTTP, InetSocketAddress(host.proxy.host, host.proxy.port)) + } else { + Proxy(Proxy.Type.SOCKS, InetSocketAddress(host.proxy.host, host.proxy.port)) + }, + host.proxy.username.ifBlank { null }, + if (host.proxy.password.isBlank()) null else host.proxy.password.toCharArray(), + ) + + // 反射调用 + val configureProxy = JGitSshClient::class.java.getDeclaredMethod( + "configureProxy", + ProxyData::class.java, + InetSocketAddress::class.java + ) + configureProxy.isAccessible = true + val address = configureProxy.invoke(sshClient, proxyData, InetSocketAddress(host.host, host.port)) + if (address is InetSocketAddress) { + return address + } + + return targetAddress + } + + } + } } diff --git a/src/test/resources/sshd/Dockerfile b/src/test/resources/sshd/Dockerfile index 12aa185..9e4e2fb 100644 --- a/src/test/resources/sshd/Dockerfile +++ b/src/test/resources/sshd/Dockerfile @@ -3,4 +3,7 @@ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk update && apk add wget gcc g++ git make zsh htop stress-ng inetutils-telnet xclock xorg-server xinit && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \ && tar -xf lrzsz-0.12.20.tar.gz && cd lrzsz-0.12.20 && ./configure && make && make install \ && ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz - +RUN sed -i 's/#AllowAgentForwarding yes/AllowAgentForwarding yes/g' /etc/ssh/sshd_config +RUN sed -i 's/AllowTcpForwarding no/AllowTcpForwarding yes/g' /etc/ssh/sshd_config +RUN sed -i 's/GatewayPorts no/GatewayPorts yes/g' /etc/ssh/sshd_config +RUN sed -i 's/X11Forwarding no/X11Forwarding yes/g' /etc/ssh/sshd_config