fix: SSH proxy not working in jump hosts (#435)

This commit is contained in:
hstyi
2025-03-30 16:34:51 +08:00
committed by GitHub
parent e2a6cceafd
commit 6a4abf7e50
2 changed files with 141 additions and 22 deletions

View File

@@ -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.HostConfigEntry
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver import org.apache.sshd.client.config.hosts.HostConfigEntryResolver
import org.apache.sshd.client.config.hosts.KnownHostEntry 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.kex.DHGClient
import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier
import org.apache.sshd.client.keyverifier.ModifiedServerKeyAcceptor 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.channel.PtyChannelConfiguration
import org.apache.sshd.common.config.keys.KeyRandomArt import org.apache.sshd.common.config.keys.KeyRandomArt
import org.apache.sshd.common.config.keys.KeyUtils 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.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.kex.BuiltinDHFactories
import org.apache.sshd.common.keyprovider.KeyIdentityProvider import org.apache.sshd.common.keyprovider.KeyIdentityProvider
import org.apache.sshd.common.util.net.SshdSocketAddress import org.apache.sshd.common.util.net.SshdSocketAddress
@@ -199,6 +206,10 @@ object SshClients {
entry.username = host.username entry.username = host.username
entry.hostName = host.host entry.hostName = host.host
entry.setProperty("Middleware", middleware.toString()) entry.setProperty("Middleware", middleware.toString())
entry.setProperty("Host", host.id)
// 设置代理
// configureProxy(entry, host, client)
// ssh-agent // ssh-agent
if (host.authentication.type == AuthenticationType.SSHAgent) { if (host.authentication.type == AuthenticationType.SSHAgent) {
@@ -285,11 +296,12 @@ object SshClients {
fun openClient(host: Host): SshClient { fun openClient(host: Host): SshClient {
val builder = ClientBuilder.builder() val builder = ClientBuilder.builder()
builder.globalRequestHandlers(listOf(KeepAliveHandler.INSTANCE)) builder.globalRequestHandlers(listOf(KeepAliveHandler.INSTANCE))
.factory { JGitSshClient() } .factory { MyJGitSshClient() }
val keyExchangeFactories = ClientBuilder.setUpDefaultKeyExchanges(true).toMutableList() val keyExchangeFactories = ClientBuilder.setUpDefaultKeyExchanges(true).toMutableList()
// https://github.com/TermoraDev/termora/issues/123 // https://github.com/TermoraDev/termora/issues/123
@Suppress("DEPRECATION")
keyExchangeFactories.addAll( keyExchangeFactories.addAll(
listOf( listOf(
DHGClient.newFactory(BuiltinDHFactories.dhg1), DHGClient.newFactory(BuiltinDHFactories.dhg1),
@@ -345,26 +357,6 @@ object SshClients {
sshClient.setKeyPasswordProviderFactory { IdentityPasswordProvider(CredentialsProvider.getDefault()) } 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() sshClient.start()
return sshClient return sshClient
} }
@@ -497,5 +489,129 @@ object SshClients {
} }
} }
@Suppress("UNCHECKED_CAST")
private class MyJGitSshClient : JGitSshClient() {
companion object {
private val HOST_CONFIG_ENTRY: AttributeRepository.AttributeKey<HostConfigEntry> by lazy {
JGitSshClient::class.java.getDeclaredField("HOST_CONFIG_ENTRY").apply { isAccessible = true }
.get(null) as AttributeRepository.AttributeKey<HostConfigEntry>
}
}
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<CloseFuture>?) {
return ioConnector.addCloseFutureListener(listener)
}
override fun removeCloseFutureListener(listener: SshFutureListener<CloseFuture>?) {
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<Long, IoSession> {
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
}
}
}
} }

View File

@@ -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 \ && 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 \ && 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 && 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