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