如何通过已SSH连接的主机B中转实现主机A到主机C的文件传输及命令执行(基于Paramiko)
如何通过已SSH连接的主机B中转实现主机A到主机C的文件传输及命令执行(基于Paramiko)
我完全理解你的处境——受网络拓扑限制,hostA只能访问hostB,而hostC只有hostB能碰到,现在已经搞定了A到B的文件传输和命令执行,卡在了B到C的中转环节。用Paramiko实现这个其实有两种很实用的方案,我来一步步帮你解决,顺便把命令执行和文件传输的问题都搞定:
核心思路说明
你现在的代码是在hostA上运行的,要访问hostC,本质是要通过hostB作为SSH跳板。这里有两种主流实现方式:
- 方案1(推荐):直接通过hostB代理连接hostC:用Paramiko的TCP通道代理功能,在hostA上直接建立到hostC的SSH连接,所有操作(命令执行、文件传输)都像直接连C一样,可控性更强。
- 方案2:在hostB上执行SSH/SCP命令中转:在hostB的SSH会话里,通过执行
ssh或scp命令来操作hostC,适合快速临时场景,但密码处理相对麻烦。
方案1:通过HostB代理直接连接HostC(推荐)
我先修正你现有代码的小问题,然后添加代理连接hostC的逻辑,同时实现命令执行和文件传输:
完整修改后的代码
import os import paramiko def main(): try: # 第一步:连接到hostB ssh_client_b = connect_to_host("100.100.100.100", "hostB", "hostB123") execute_command_over_ssh(ssh_client_b, "uname") execute_command_over_ssh(ssh_client_b, "ls /home/hostB/") # 从hostA传输文件到hostB src_path_on_A = "/home/hostA/repos/repo1/" dest_path_on_B = "/home/hostB/repos/" transfer_file_or_directory(ssh_client_b, src_path_on_A, dest_path_on_B) # 第二步:通过hostB作为代理,连接到hostC(核心逻辑) ssh_client_c = connect_to_host( host_name="200.200.200.200", user_name="hostC", password="hostC123", proxy_ssh_client=ssh_client_b # 传入hostB的连接作为跳板 ) # 在hostC上执行命令并获取结果 execute_command_over_ssh(ssh_client_c, "uname") execute_command_over_ssh(ssh_client_c, "ls /home/hostC/") # 方式1:在hostB上执行scp命令,将B上的文件传到C(符合你先存B再转C的需求) # 注意:若要免密执行,需在hostB上配置到hostC的SSH密钥对,替代sshpass scp_command = f"sshpass -p 'hostC123' scp -r {dest_path_on_B}/repo1/ hostC@200.200.200.200:/home/hostC/repos/" execute_command_over_ssh(ssh_client_b, scp_command) # 方式2(更高效):直接从A通过B代理传文件到C(跳过B的留存) # transfer_file_or_directory(ssh_client_c, src_path_on_A, "/home/hostC/repos/") # 关闭连接 ssh_client_c.close() ssh_client_b.close() except Exception as e: print(f"Error occurred: {str(e)}") finally: print("All SSH connections closed") def connect_to_host(host_name, user_name, password, proxy_ssh_client=None): """ 建立SSH连接,支持通过已有SSH连接作为代理(跳板) :param proxy_ssh_client: 已连接的SSHClient对象(如hostB的连接) """ ssh_client = paramiko.SSHClient() ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: if proxy_ssh_client: # 建立TCP代理通道:将hostA的流量通过hostB中转到hostC的22端口 sock = proxy_ssh_client.get_transport().open_channel( "direct-tcpip", (host_name, 22), (proxy_ssh_client.get_transport().getpeername()[0], 0) ) # 通过代理通道连接hostC ssh_client.connect( hostname=host_name, username=user_name, password=password, sock=sock ) else: # 直接连接目标主机 ssh_client.connect( hostname=host_name, username=user_name, password=password, port=22 ) print(f"Successfully connected to {host_name}") return ssh_client except Exception as e: print(f"Failed to connect to {host_name}: {str(e)}") ssh_client.close() raise def execute_command_over_ssh(ssh_client, command): """在SSH连接上执行命令,返回并打印输出/错误信息""" try: stdin, stdout, stderr = ssh_client.exec_command(command) output = stdout.read().decode('utf-8').strip() error = stderr.read().decode('utf-8').strip() print(f"=== Command: `{command}` ===") if output: print(f"Output:\n{output}") if error: print(f"Error:\n{error}") return output, error except Exception as e: print(f"Failed to execute command `{command}`: {str(e)}") raise def transfer_file_or_directory(ssh_client, local_path, remote_path): """递归传输文件或目录到远程主机""" try: sftp = ssh_client.open_sftp() # 确保远程目标目录存在 try: sftp.stat(remote_path) except FileNotFoundError: sftp.mkdir(remote_path, mode=0o755) if os.path.isfile(local_path): remote_file = os.path.join(remote_path, os.path.basename(local_path)) sftp.put(local_path, remote_file) print(f"Transferred file: {local_path} -> {remote_file}") elif os.path.isdir(local_path): _transfer_dir_recursive(sftp, local_path, os.path.join(remote_path, os.path.basename(local_path))) print(f"Transferred directory: {local_path} -> {remote_path}") else: raise ValueError(f"Invalid path: {local_path} is neither file nor directory") sftp.close() except Exception as e: print(f"Transfer failed: {str(e)}") raise def _transfer_dir_recursive(sftp, local_dir, remote_dir): """递归传输目录的辅助函数""" try: sftp.stat(remote_dir) except FileNotFoundError: sftp.mkdir(remote_dir, mode=0o755) for item in os.listdir(local_dir): local_item = os.path.join(local_dir, item) remote_item = os.path.join(remote_dir, item) if os.path.isfile(local_item): sftp.put(local_item, remote_item) elif os.path.isdir(local_item): _transfer_dir_recursive(sftp, local_item, remote_item) if __name__ == "__main__": main()
关键部分解释
带代理的SSH连接:
我修改了connect_to_host函数,增加了proxy_ssh_client参数。当传入已连接的ssh_client_b时,会通过它建立一个直接TCP通道,把hostA的SSH流量通过hostB中转到hostC的22端口,实现“通过hostB连hostC”的效果,完全不需要在hostB上额外配置。HostC的命令执行:
优化后的execute_command_over_ssh函数不仅能执行命令,还能捕获标准输出和错误输出,比你原来的函数更健壮,能帮你快速定位命令执行的问题。B到C的文件传输:
提供了两种符合需求的方式:- 方式一:在hostB上执行
scp命令,把已存到B的文件传到C,完全匹配你“先存B再转C”的需求; - 方式二:直接从A通过B代理传文件到C,省去中间在B的存储步骤,更高效。
- 方式一:在hostB上执行
方案2:在HostB上执行SSH/SCP命令中转(快速临时方案)
如果你不想修改太多Paramiko连接逻辑,也可以直接在hostB的SSH会话里执行原生SSH命令操作hostC:
示例代码片段
# 在hostB上执行命令,登录hostC并执行uname ssh_cmd = "sshpass -p 'hostC123' ssh hostC@200.200.200.200 'uname'" execute_command_over_ssh(ssh_client_b, ssh_cmd) # 在hostB上执行scp命令,把文件传到hostC scp_cmd = "sshpass -p 'hostC123' scp -r /home/hostB/repos/repo1/ hostC@200.200.200.200:/home/hostC/repos/" execute_command_over_ssh(ssh_client_b, scp_cmd)
注意事项
- 安全提示:
sshpass会明文暴露密码,推荐在hostB上配置到hostC的SSH密钥对(执行ssh-keygen生成密钥,将公钥传到hostC的~/.ssh/authorized_keys),这样就可以去掉sshpass部分,直接用ssh hostC@xxx command。 - 输出处理:这种方式需要自己解析命令输出,可控性不如方案1。
额外提示
- 错误处理:代码中添加了完整的异常捕获,能及时发现连接、命令执行、文件传输中的问题;
- SSH密钥替代密码:所有用明文密码的地方都推荐换成SSH密钥对,既安全又避免硬编码密码(Paramiko支持通过
key_filename参数加载密钥文件); - 大文件传输:传输大文件时,可以给
sftp.put添加callback参数实现进度条,方便监控传输状态。
内容来源于stack exchange
相关产品推荐
相关产品推荐

