在 iOS 上实现跳板机 SSH 连接

在 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 私钥。

核心作用:

  1. 安全存储私钥:私钥保存在内存中,而不是磁盘文件
  2. 自动签名:当需要 SSH 认证时,agent 自动使用私钥进行签名
  3. 避免重复输入密码:如果私钥有密码保护,只需输入一次解锁 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

原因

  1. 没有系统级 SSH agent

    • iOS 没有 ssh-agent 命令
    • 无法运行后台 agent 进程
  2. 应用沙盒限制

    • 应用无法访问系统级 agent
    • 无法与其他应用共享 agent
  3. 安全模型

    • 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 需要两次传输 直接通过隧道传输
终端交互 嵌套终端,体验差 单一终端,体验好

为什么选择端口转发?

  1. 安全性更高:私钥始终在你的 iOS 设备上,跳板机只是转发 TCP 数据包
  2. 性能更好:单次 SSH 握手,开销更小
  3. 用户体验更好:单一终端,体验流畅
  4. SFTP/SCP 支持更好:直接通过隧道传输
  5. 符合 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));

关键理解

  1. SSH 通道是逻辑概念:在 SSH 会话内部创建的数据通道
  2. TCP 隧道是物理概念:数据通过 SSH 连接传输,看起来像直接 TCP 连接
  3. 封装和解封装:你的 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)送到最终目的地(目标服务器)。


总结

关键要点

  1. PC 上可以使用 Agent Forwarding:私钥不离开本地,认证请求通过跳板机转发
  2. iOS 不支持 Agent Forwarding:没有系统级 SSH agent,应用沙盒限制
  3. iOS 上的替代方案:端口转发优于链式连接
    • 安全性:私钥不离开设备
    • 性能:单次 SSH 握手
    • 体验:单一终端,流畅交互
  4. 端口转发原理:通过 SSH 通道建立 TCP 隧道
    • SSH 通道:在 SSH 会话内部创建的逻辑通道
    • TCP 隧道:数据通过 SSH 连接传输,看起来像直接 TCP 连接
    • 封装和解封装:TCP 数据被封装在 SSH 数据包中传输

实际应用

在 iOS 上实现跳板机功能,应该采用端口转发方式,通过 libssh2_channel_direct_tcpip_ex API 建立 TCP 隧道,这样可以在保持安全性的同时,提供良好的用户体验。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容