在 iOS 上实现跳板机 SSH 连接 :从 Agent Forwarding 到 Port Forwarding
1. 跳板机典型架构
跳板机(Jump Host / Bastion Host)是一台中间服务器,用于:
- 保护内网服务器,不允许直接 SSH 连接
- 集中管理用户访问权限
- 记录所有访问日志
典型架构:
外网设备 → 跳板机(可 SSH 直连)→ 内网服务器(只能通过跳板机访问)
实际场景:
你的电脑 → 跳板机(192.168.56.5)→ 目标服务器(192.168.57.3)
2. 理解在 PC 上的跳板机连接流程
方式 1:手动两步连接
步骤 1:SSH 登录跳板机
$ ssh u3@192.168.56.5
Last login: Sun Sep 8 19:51:48 2019 from 192.168.56.1
u3@h3:~$
步骤 2:从跳板机登录到目标服务器
u3@h3:~$ ssh -o "PasswordAuthentication no" u2@192.168.57.3
u2@192.168.57.3: Permission denied (publickey,password).
问题分析:
虽然 public key 已拷贝到目标机器,但此时在跳板机上,跳板机没有你电脑的 private key,因此认证失败。
为什么不把 private key 拷贝到跳板机?
不行,因为 private key 一旦拷贝到跳板机,其他能登录跳板机的人就可能拿到你的 private key,存在安全风险。
方式 2:使用 Agent Forwarding
SSH Agent 是什么?
SSH Agent 是一个后台程序,用于在内存中安全地保存和管理 SSH 私钥。
核心作用:
- 安全存储私钥:私钥保存在内存中,而不是磁盘文件
- 自动签名:当需要 SSH 认证时,agent 自动使用私钥进行签名
- 避免重复输入密码:如果私钥有密码保护,只需输入一次解锁 agent
Agent Forwarding 工作原理:
你的电脑 跳板机 目标服务器
| | |
| SSH连接(带-A) | |
|------------->| |
| | SSH连接 |
| |--------------->|
| | | 需要私钥认证
| |<----------------|
| | 转发认证请求 |
|<--------------| |
| 使用本地agent签名 |
|-------------->| |
| | 转发签名结果 |
| |--------------->|
| | | 认证成功
使用步骤:
# 1. 开启 ssh-agent,然后将 private key 添加到 ssh-agent 中
$ eval $(ssh-agent)
Agent pid 8350
$ ssh-add
Identity added: /home/yt/.ssh/id_rsa (yt@arch)
# 2. SSH 登录到跳板机(加上 -A 参数,表示开启 agent forwarding)
$ ssh -A u3@192.168.56.5
# 3. 从跳板机登录目标机器(自动使用本地 agent)
u3@h3:~$ ssh u2@192.168.57.3
Last login: Sun Sep 8 20:45:03 2019 from 192.168.57.4
u2@h2:~$
关键点:
- 跳板机上没有你的私钥
- 认证请求被转发回你的电脑
- 你的电脑上的 agent 使用私钥签名
- 签名结果再转发回目标服务器
方式 3:使用 ProxyJump(-J 参数)
这是更简单的方式,用一条命令就可以直接成功:
$ ssh -J u3@192.168.56.5 u2@192.168.57.3
Last login: Sun Sep 8 21:09:13 2019 from 192.168.57.4
u2@h2:~$
该命令中的 -J 参数是用来指定跳板机的,该命令执行后,SSH 会帮我们先登录跳板机,然后再登录目标机器,一切都是自动的。
使用 -J 参数的优势:
用 -J 参数指定跳板机还有一个好处就是在使用 scp 拷贝文件时更加方便:
$ scp -J u3@192.168.56.5 abc.txt u2@192.168.57.3:/home/u2/
abc.txt
完美!
3. iOS 不支持 Agent Forwarding
原因
-
没有系统级 SSH agent
- iOS 没有
ssh-agent命令 - 无法运行后台 agent 进程
- iOS 没有
-
应用沙盒限制
- 应用无法访问系统级 agent
- 无法与其他应用共享 agent
-
安全模型
- iOS 的安全模型不允许应用间共享敏感信息
- 每个应用只能管理自己的私钥
总结
由于 iOS 的限制,SSH 客户端在 iOS 上无法使用 Agent Forwarding 功能。因此需要寻找替代方案。
4. iOS 上的替代方案
方式 1:链式连接(Chained Connection)
工作原理:
你的设备 → 跳板机(SSH连接)→ 在跳板机上执行 ssh 命令 → 目标服务器
实现方式:
// 伪代码
// 1. 连接到跳板机
let jumpClient = SSHClient(host: "jumpHost", ...)
jumpClient.connect(...)
// 2. 在跳板机上执行 SSH 命令连接目标服务器
jumpClient.executeCommand("ssh user@targetHost")
特点:
- ✅ 实现简单,只需执行命令
- ❌ 需要在跳板机上执行 SSH 命令
- ❌ 跳板机需要安装 SSH 客户端
- ❌ 跳板机上需要私钥(不安全)
- ❌ 建立两个独立的 SSH 会话
- ❌ 终端嵌套,用户体验差
方式 2:端口转发(Port Forwarding)- 推荐
工作原理:
你的设备 → 跳板机(SSH连接)→ 通过 SSH 通道建立 TCP 隧道 → 目标服务器
实现方式:
// 伪代码
// 1. 连接到跳板机
let jumpClient = SSHClient(host: "jumpHost", ...)
jumpClient.connect(...)
// 2. 在跳板机上建立到目标服务器的 TCP 通道
let channel = libssh2_channel_direct_tcpip_ex(
jumpClient.session.rawSession,
"targetHost", // 目标服务器
22, // 目标端口
"127.0.0.1", // 源地址
0 // 源端口
)
// 3. 通过这个通道建立到目标服务器的 SSH 连接
let targetClient = SSHClient(channel: channel)
targetClient.connect(...)
特点:
- ✅ 跳板机不需要 SSH 客户端
- ✅ 跳板机上不需要私钥
- ✅ 私钥始终在你的设备上(更安全)
- ✅ 单一 SSH 会话,性能更好
- ✅ 终端体验流畅
- ❌ 需要底层 API 支持
核心区别对比
| 特性 | 链式连接 | 端口转发 |
|---|---|---|
| 跳板机需要 | SSH 客户端 + 私钥/密码 | 只需网络连接 |
| 连接数量 | 2 个独立 SSH 会话 | 1 个 SSH 会话(含隧道) |
| 安全性 | 私钥可能暴露在跳板机 | 私钥只在你的设备上 |
| 性能 | 两次 SSH 握手,开销较大 | 一次 SSH 握手,开销较小 |
| 实现复杂度 | 简单(执行命令) | 需要底层 API |
| SFTP/SCP | 需要两次传输 | 直接通过隧道传输 |
| 终端交互 | 嵌套终端,体验差 | 单一终端,体验好 |
为什么选择端口转发?
- 安全性更高:私钥始终在你的 iOS 设备上,跳板机只是转发 TCP 数据包
- 性能更好:单次 SSH 握手,开销更小
- 用户体验更好:单一终端,体验流畅
- SFTP/SCP 支持更好:直接通过隧道传输
- 符合 SSH 标准协议:端口转发是 SSH 协议的标准功能(RFC 4254)
5. 实际测试案例
场景设置
- 跳板机:
root@101.43.226.17 - 目标用户:
jumpuser@101.43.226.17 - 目标:在本地电脑上通过端口转发登录
jumpuser
测试命令
方法 1:使用 ProxyJump(-J 参数)- 最简单(推荐)
# 直接通过跳板机连接目标用户
ssh -J root@101.43.226.17 jumpuser@101.43.226.17
# 测试执行命令
ssh -J root@101.43.226.17 jumpuser@101.43.226.17 'whoami'
# 测试 SFTP
sftp -J root@101.43.226.17 jumpuser@101.43.226.17
# 测试 SCP
scp -J root@101.43.226.17 test.txt jumpuser@101.43.226.17:/tmp/
方法 2:使用 ProxyCommand - 最灵活
# 使用 ProxyCommand 通过跳板机连接
ssh -o ProxyCommand="ssh -W %h:%p root@101.43.226.17" jumpuser@101.43.226.17
# 测试执行命令
ssh -o ProxyCommand="ssh -W %h:%p root@101.43.226.17" jumpuser@101.43.226.17 'pwd'
方法 3:使用本地端口转发(-L)- 分步操作
步骤 1:建立本地端口转发(保持终端打开)
# 在第一个终端窗口执行
ssh -L 2222:localhost:22 root@101.43.226.17
步骤 2:通过本地端口连接目标用户(新开终端)
# 在第二个终端窗口执行
ssh -p 2222 jumpuser@localhost
方法 4:使用 SSH Config 文件 - 最优雅
步骤 1:编辑 SSH 配置文件
nano ~/.ssh/config
步骤 2:添加配置
# 跳板机配置
Host jump-host
HostName 101.43.226.17
User root
Port 22
IdentityFile ~/.ssh/id_rsa
# 目标服务器配置(通过跳板机)
Host target-user
HostName 101.43.226.17
User jumpuser
Port 22
ProxyJump jump-host
IdentityFile ~/.ssh/id_rsa
步骤 3:使用别名连接
# 直接使用别名连接
ssh target-user
验证端口转发是否工作
# 使用详细模式查看连接过程
ssh -vvv -J root@101.43.226.17 jumpuser@101.43.226.17
# 查看本地端口监听(使用方法 3 时)
netstat -an | grep 2222
# 测试连接时间
time ssh -J root@101.43.226.17 jumpuser@101.43.226.17 'echo "Connection test"'
6. 理解"通过 SSH 通道建立 TCP 隧道"
SSH 通道(Channel)是什么?
SSH 连接建立后,可以在同一个 SSH 会话中创建多个通道(Channel),每个通道用于不同的目的:
SSH 会话(Session)
├── Channel 1: Shell(终端)
├── Channel 2: SFTP(文件传输)
├── Channel 3: SCP(文件复制)
└── Channel 4: Direct TCP/IP(端口转发)← 这就是我们要用的
TCP 隧道是什么?
TCP 隧道是在一个已建立的连接上"承载"另一个 TCP 连接的技术。数据包被封装在现有连接中传输。
原始数据包:
[TCP Header][Data]
通过隧道传输:
[SSH Header][TCP Header][Data]
如何通过 SSH 通道建立 TCP 隧道?
步骤 1:建立 SSH 连接到跳板机
你的设备 跳板机
| |
|--- SSH 握手 ---------->|
|<-- SSH 握手确认 -------|
| |
|--- 认证请求 ---------->|
|<-- 认证成功 -----------|
| |
| [SSH 会话已建立] |
步骤 2:在 SSH 会话中创建 Direct TCP/IP 通道
你的设备 跳板机
| |
| [SSH 会话] |
| |
|--- 创建通道请求 ------->|
| 类型: direct-tcpip |
| 目标: target:22 |
|<-- 通道创建成功 -------|
| |
| [SSH 通道已建立] |
关键点:
- 这个通道是在 SSH 会话内部创建的
- 通道类型是
direct-tcpip(直接 TCP/IP) - 告诉跳板机:请连接到
target:22
步骤 3:通过通道传输 TCP 数据
你的设备 跳板机 目标服务器
| | |
| [SSH 会话] | |
| └─ [TCP 通道] | |
| | |
|--- SSH 数据包 -------->| |
| [封装了 TCP 数据] | |
| |--- TCP 连接 --->|
| | 到 target:22 |
| | |
| |<-- TCP 响应 ----|
|<-- SSH 数据包 ---------| |
| [封装了 TCP 响应] | |
关键点:
- 你的设备发送的 TCP 数据被封装在 SSH 数据包中
- 跳板机收到后,解封装,然后转发到目标服务器
- 目标服务器的响应也通过相同路径返回
详细数据流示例
假设你要执行命令 ls -la:
步骤 1:你的设备发送命令
你的设备:
1. 创建 TCP 数据包:[TCP Header][ls -la\n]
2. 封装到 SSH 通道:[SSH Channel Header][TCP Header][ls -la\n]
3. 封装到 SSH 会话:[SSH Session Header][SSH Channel Header][TCP Header][ls -la\n]
4. 发送到跳板机
步骤 2:跳板机处理
跳板机:
1. 接收 SSH 数据包
2. 解密 SSH 会话层:[SSH Channel Header][TCP Header][ls -la\n]
3. 识别是 direct-tcpip 通道
4. 提取 TCP 数据:[TCP Header][ls -la\n]
5. 转发到目标服务器(通过普通 TCP 连接)
步骤 3:目标服务器响应
目标服务器:
1. 接收 TCP 数据包:[TCP Header][ls -la\n]
2. 执行命令
3. 返回结果:[TCP Header][file1\nfile2\n...]
4. 发送到跳板机
步骤 4:跳板机转发响应
跳板机:
1. 接收目标服务器的 TCP 响应
2. 封装到 SSH 通道:[SSH Channel Header][TCP Header][file1\nfile2\n...]
3. 封装到 SSH 会话:[SSH Session Header][SSH Channel Header][TCP Header][file1\nfile2\n...]
4. 发送到你的设备
步骤 5:你的设备接收
你的设备:
1. 接收 SSH 数据包
2. 解密 SSH 会话层:[SSH Channel Header][TCP Header][file1\nfile2\n...]
3. 提取通道数据:[TCP Header][file1\nfile2\n...]
4. 提取 TCP 数据:[file1\nfile2\n...]
5. 显示结果
用代码理解
// 1. 建立 SSH 会话(你已经完成)
LIBSSH2_SESSION *session = libssh2_session_init();
libssh2_session_handshake(session, socket_fd);
libssh2_userauth_publickey(session, username, publickey, privatekey);
// 2. 创建 Direct TCP/IP 通道(这就是"建立 TCP 隧道")
LIBSSH2_CHANNEL *channel = libssh2_channel_direct_tcpip_ex(
session, // SSH 会话
"target-server.com", // 目标服务器地址
22, // 目标端口
"127.0.0.1", // 源地址(跳板机本地)
0 // 源端口(自动分配)
);
// 3. 现在 channel 就是一个到目标服务器的 TCP 连接
// 你可以通过这个 channel 发送和接收数据
libssh2_channel_write(channel, "ls -la\n", 7);
char buffer[1024];
libssh2_channel_read(channel, buffer, sizeof(buffer));
关键理解
- SSH 通道是逻辑概念:在 SSH 会话内部创建的数据通道
- TCP 隧道是物理概念:数据通过 SSH 连接传输,看起来像直接 TCP 连接
- 封装和解封装:你的 TCP 数据被封装在 SSH 数据包中传输
图示总结
┌─────────────────────────────────────────────────────────┐
│ 你的设备 │
│ │
│ ┌──────────────┐ │
│ │ SSH Client │ │
│ │ │ │
│ │ ┌──────────┐ │ │
│ │ │ Channel │ │ ← Direct TCP/IP Channel │
│ │ │ (TCP) │ │ │
│ │ └──────────┘ │ │
│ └──────┬───────┘ │
│ │ SSH Session (加密) │
└─────────┼───────────────────────────────────────────────┘
│
▼
┌─────────┼───────────────────────────────────────────────┐
│ │ 跳板机 │
│ │ │
│ ┌──────▼───────┐ │
│ │ SSH Server │ │
│ │ │ │
│ │ ┌──────────┐ │ │
│ │ │ Channel │ │ ← 接收通道请求,建立 TCP 连接 │
│ │ │ Handler │ │ │
│ │ └────┬─────┘ │ │
│ └──────┼───────┘ │
│ │ TCP Connection (明文) │
└─────────┼───────────────────────────────────────────────┘
│
▼
┌─────────┼───────────────────────────────────────────────┐
│ │ 目标服务器 │
│ │ │
│ ┌──────▼───────┐ │
│ │ SSH Server │ │
│ │ │ │
│ │ 接收 TCP 连接 │ │
│ │ 处理 SSH 协议 │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
类比理解
想象一个快递包裹:
- SSH 会话 = 快递公司的运输网络(加密、安全)
- TCP 通道 = 包裹(里面装着你的数据)
- 跳板机 = 中转站(打开包裹,取出内容,转发到最终目的地)
你的数据(TCP 包)被装在 SSH 包裹中,通过加密的运输网络(SSH 会话)送到中转站(跳板机),中转站打开包裹,取出数据,然后通过普通快递(TCP)送到最终目的地(目标服务器)。
总结
关键要点
- PC 上可以使用 Agent Forwarding:私钥不离开本地,认证请求通过跳板机转发
- iOS 不支持 Agent Forwarding:没有系统级 SSH agent,应用沙盒限制
- iOS 上的替代方案:端口转发优于链式连接
- 安全性:私钥不离开设备
- 性能:单次 SSH 握手
- 体验:单一终端,流畅交互
- 端口转发原理:通过 SSH 通道建立 TCP 隧道
- SSH 通道:在 SSH 会话内部创建的逻辑通道
- TCP 隧道:数据通过 SSH 连接传输,看起来像直接 TCP 连接
- 封装和解封装:TCP 数据被封装在 SSH 数据包中传输
实际应用
在 iOS 上实现跳板机功能,应该采用端口转发方式,通过 libssh2_channel_direct_tcpip_ex API 建立 TCP 隧道,这样可以在保持安全性的同时,提供良好的用户体验。