Skip to content

Commit 4defb73

Browse files
LL-LINAias00zqr10159
authored
[feature] support SSH proxy jump connections (apache#3138)
Co-authored-by: aias00 <[email protected]> Co-authored-by: Logic <[email protected]>
1 parent f8e85ec commit 4defb73

File tree

16 files changed

+2310
-7
lines changed

16 files changed

+2310
-7
lines changed

hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/ssh/SshCollectImpl.java

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,12 @@ public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
7979
long startTime = System.currentTimeMillis();
8080
SshProtocol sshProtocol = metrics.getSsh();
8181
boolean reuseConnection = Boolean.parseBoolean(sshProtocol.getReuseConnection());
82+
boolean useProxy = Boolean.parseBoolean(sshProtocol.getUseProxy());
8283
int timeout = CollectUtil.getTimeout(sshProtocol.getTimeout(), DEFAULT_TIMEOUT);
8384
ClientChannel channel = null;
8485
ClientSession clientSession = null;
8586
try {
86-
clientSession = getConnectSession(sshProtocol, timeout, reuseConnection);
87+
clientSession = getConnectSession(sshProtocol, timeout, reuseConnection, useProxy);
8788
if (CommonSshBlacklist.isCommandBlacklisted(sshProtocol.getScript())) {
8889
builder.setCode(CollectRep.Code.FAIL);
8990
builder.setMsg("The command is blacklisted: " + sshProtocol.getScript());
@@ -156,7 +157,7 @@ public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
156157
log.error(e.getMessage(), e);
157158
}
158159
}
159-
if (clientSession != null && !reuseConnection) {
160+
if (clientSession != null && !reuseConnection && !useProxy) {
160161
try {
161162
clientSession.close();
162163
} catch (Exception e) {
@@ -280,11 +281,8 @@ private void removeConnectSessionCache(SshProtocol sshProtocol) {
280281
connectionCommonCache.removeCache(identifier);
281282
}
282283

283-
private ClientSession getConnectSession(SshProtocol sshProtocol, int timeout, boolean reuseConnection)
284+
private ClientSession getConnectSession(SshProtocol sshProtocol, int timeout, boolean reuseConnection, boolean useProxy)
284285
throws IOException, GeneralSecurityException {
285-
return SshHelper.getConnectSession(
286-
sshProtocol.getHost(), sshProtocol.getPort(), sshProtocol.getUsername(), sshProtocol.getPassword(),
287-
sshProtocol.getPrivateKey(), sshProtocol.getPrivateKeyPassphrase(), timeout, reuseConnection
288-
);
286+
return SshHelper.getConnectSession(sshProtocol, timeout, reuseConnection, useProxy);
289287
}
290288
}

hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/collect/common/ssh/SshHelper.java

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,18 @@
1717

1818
package org.apache.hertzbeat.collector.collect.common.ssh;
1919

20+
import java.io.InputStream;
21+
import java.security.KeyPair;
22+
import java.util.List;
2023
import lombok.extern.slf4j.Slf4j;
2124
import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection;
2225
import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier;
2326
import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache;
2427
import org.apache.hertzbeat.collector.collect.common.cache.SshConnect;
2528
import org.apache.hertzbeat.collector.util.PrivateKeyUtils;
29+
import org.apache.hertzbeat.common.entity.job.protocol.SshProtocol;
2630
import org.apache.sshd.client.SshClient;
31+
import org.apache.sshd.client.config.hosts.HostConfigEntry;
2732
import org.apache.sshd.client.session.ClientSession;
2833
import org.apache.sshd.common.config.keys.FilePasswordProvider;
2934
import org.apache.sshd.common.util.security.SecurityUtils;
@@ -101,4 +106,101 @@ public static ClientSession getConnectSession(String host, String port, String u
101106
return clientSession;
102107
}
103108

109+
public static ClientSession getConnectSession(SshProtocol sshProtocol, int timeout, boolean reuseConnection, boolean useProxy)
110+
throws IOException, GeneralSecurityException {
111+
CacheIdentifier identifier = CacheIdentifier.builder()
112+
.ip(sshProtocol.getHost()).port(sshProtocol.getPort())
113+
.username(sshProtocol.getUsername()).password(sshProtocol.getPassword())
114+
.build();
115+
ClientSession clientSession = null;
116+
// When using ProxyJump, force connection reuse:
117+
// Apache MINA SSHD will pass the proxy password error to the target host in proxy scenarios, causing the first connection to fail.
118+
// Reusing connections can skip duplicate authentication and avoid this problem.
119+
if (reuseConnection || useProxy) {
120+
Optional<AbstractConnection<?>> cacheOption = CONNECTION_COMMON_CACHE.getCache(identifier, true);
121+
if (cacheOption.isPresent()) {
122+
SshConnect sshConnect = (SshConnect) cacheOption.get();
123+
clientSession = sshConnect.getConnection();
124+
try {
125+
if (clientSession == null || clientSession.isClosed() || clientSession.isClosing()) {
126+
clientSession = null;
127+
CONNECTION_COMMON_CACHE.removeCache(identifier);
128+
}
129+
} catch (Exception e) {
130+
log.warn(e.getMessage());
131+
clientSession = null;
132+
CONNECTION_COMMON_CACHE.removeCache(identifier);
133+
}
134+
}
135+
if (clientSession != null) {
136+
return clientSession;
137+
}
138+
}
139+
SshClient sshClient = CommonSshClient.getSshClient();
140+
HostConfigEntry proxyConfig = new HostConfigEntry();
141+
if (useProxy && StringUtils.hasText(sshProtocol.getProxyHost())) {
142+
String proxySpec = String.format("%s@%s:%d", sshProtocol.getProxyUsername(), sshProtocol.getProxyHost(), Integer.parseInt(sshProtocol.getProxyPort()));
143+
proxyConfig.setHostName(sshProtocol.getHost());
144+
proxyConfig.setHost(sshProtocol.getHost());
145+
proxyConfig.setPort(Integer.parseInt(sshProtocol.getPort()));
146+
proxyConfig.setUsername(sshProtocol.getUsername());
147+
proxyConfig.setProxyJump(proxySpec);
148+
149+
// Apache SSHD requires the password for the proxy to be preloaded into the sshClient instance before connecting
150+
if (StringUtils.hasText(sshProtocol.getProxyPassword())) {
151+
sshClient.addPasswordIdentity(sshProtocol.getProxyPassword());
152+
log.debug("Loaded proxy server password authentication: {}@{}", sshProtocol.getProxyUsername(), sshProtocol.getProxyHost());
153+
}
154+
if (StringUtils.hasText(sshProtocol.getProxyPrivateKey())) {
155+
proxyConfig.setIdentities(List.of(sshProtocol.getProxyPrivateKey()));
156+
log.debug("Proxy private key loaded into HostConfigEntry");
157+
}
158+
}
159+
160+
if (useProxy && StringUtils.hasText(sshProtocol.getProxyHost())) {
161+
try {
162+
clientSession = sshClient.connect(proxyConfig)
163+
.verify(timeout, TimeUnit.MILLISECONDS).getSession();
164+
}
165+
finally {
166+
sshClient.removePasswordIdentity(sshProtocol.getProxyPassword());
167+
}
168+
} else {
169+
clientSession = sshClient.connect(sshProtocol.getUsername(), sshProtocol.getHost(), Integer.parseInt(sshProtocol.getPort()))
170+
.verify(timeout, TimeUnit.MILLISECONDS).getSession();
171+
}
172+
173+
if (StringUtils.hasText(sshProtocol.getPassword())) {
174+
clientSession.addPasswordIdentity(sshProtocol.getPassword());
175+
} else if (StringUtils.hasText(sshProtocol.getPrivateKey())) {
176+
var resourceKey = PrivateKeyUtils.writePrivateKey(sshProtocol.getHost(), sshProtocol.getPrivateKey());
177+
try (InputStream keyStream = new FileInputStream(resourceKey)) {
178+
FilePasswordProvider passwordProvider = (session, resource, index) -> {
179+
if (StringUtils.hasText(sshProtocol.getPrivateKeyPassphrase())) {
180+
return sshProtocol.getPrivateKeyPassphrase();
181+
}
182+
return null;
183+
};
184+
Iterable<KeyPair> keyPairs = SecurityUtils.loadKeyPairIdentities(null, () -> resourceKey, keyStream, passwordProvider);
185+
if (keyPairs != null) {
186+
keyPairs.forEach(clientSession::addPublicKeyIdentity);
187+
} else {
188+
log.error("Failed to load private key pairs from: {}", resourceKey);
189+
}
190+
} catch (IOException e) {
191+
log.error("Error reading private key file: {}", e.getMessage());
192+
}
193+
} // else auth with localhost private public key certificates
194+
195+
// auth
196+
if (!clientSession.auth().verify(timeout, TimeUnit.MILLISECONDS).isSuccess()) {
197+
clientSession.close();
198+
throw new IllegalArgumentException("ssh auth failed.");
199+
}
200+
if (reuseConnection || useProxy) {
201+
SshConnect sshConnect = new SshConnect(clientSession);
202+
CONNECTION_COMMON_CACHE.addCache(identifier, sshConnect);
203+
}
204+
return clientSession;
205+
}
104206
}

hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/SshProtocol.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,34 @@ public class SshProtocol implements CommonRequestProtocol, Protocol {
8080
* Response data parsing mode:oneRow, multiRow
8181
*/
8282
private String parseType;
83+
84+
/**
85+
* IP ADDRESS OR DOMAIN NAME OF THE PEER PROXY HOST
86+
*/
87+
private String proxyHost;
88+
89+
/**
90+
* Peer proxy host port
91+
*/
92+
private String proxyPort;
93+
94+
/**
95+
* Proxy UserName
96+
*/
97+
private String proxyUsername;
98+
99+
/**
100+
* Proxy Password (optional)
101+
*/
102+
private String proxyPassword;
103+
104+
/**
105+
* flag of use proxy
106+
*/
107+
private String useProxy = "false";
108+
109+
/**
110+
* Proxy private key (optional)
111+
*/
112+
private String proxyPrivateKey;
83113
}

0 commit comments

Comments
 (0)