本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/11903758.html 本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/11899478.html 通常,我们用于调试的计算机无法远程访问位于局域网中的待调试设备。通过 ssh 的端口转发(又称 ssh 隧道)技术,可以实现这种远程调试功能。 下文中,sshc 指 ssh 客户端,sshd 指 ssh 服务器。 1. ssh 端口转发模式简介 ssh 客户端运行于本地机器,它的作用是:登录到目标机器并在目标机器上执行命令。它可以建立一个安全通道,为不安全网络上两个不受信任的主机提供安全的加密通信。X11 连接、任意 TCP 端口和 UNIX 域套接字也可以通过 ssh 安全通道进行转发。 ssh 连接并登录到指定的主机名(用户名可选)。如果指定了命令,命令将在远程主机上执行,而不是在本机 shell 里执行。 1.1 ssh 常用选项简介 ssh 端口转发相关的常用选项如下: -C 请求压缩所有数据(包括 stdin、stdout、stderr 和用于转发的 X11、TCP 和 UNIX 域连接的数据)。压缩算法与 gzip 使用的算法相同,压缩级别由 ssh 协议版本 1 的 CompressionLevel 选项控制。在调制解调器线路和其他慢速连接上采用压缩是可取的,但它会减慢快速网络上的速度。 -f 请求 ssh 在执行命令之前转到后台。如果用户希望 ssh 在后台运行,但 ssh 需要用户提供密码或口令,使用 -f 选项就很有用,在用户输入密码之后,ssh 就会转入后台运行。这个选项隐含了 -n 选项的功能(-n 选项将 stdin 重定向到 /dev/null,从而避免后台进程读 stdin)。在远程站点上启动 X11 程序的推荐方法是使用 "ssh -f host xterm" 。 如果 ExitOnForwardFailure 配置选项设置的是 "yes",则使用 -f 选项启动的 ssh 客户端会等所有的远程端口转发建立成功后才将自己转到后台运行。 -n 将 stdin 重定向到 /dev/null (实际上是为了防止后台进程从stdin读取数据)。当 ssh 在后台运行时必须使用此选项。 一个常见的技巧是使用它在目标机器上运行 X11 程序。例如,ssh -n shadow.cs.hut.fi emacs & 将在 shadows.cs.hut.fi 上启动 emacs 程序。X11 的连接将通过加密通道自动转发。ssh 程序将在后台运行。(如果 ssh 需要请求密码或口令,则此操作无效;参见-f选项。) -N 不执行远程命令。此选项用于只需要端口转发功能时。 -g 允许远程主机连接到本地转发端口。如果用于多路复用连接,则必须在主进程上指定此选项。 -t 强制分配一个伪终端。在目标机上执行任意的基于屏幕的程序时(例如,实现菜单服务),分配伪终端很有用。使用多个 -t 选项则会强制分配终端,即使 ssh 没有本地终端。 -T 禁止分配伪终端。 -L [bind_address:]port:host:hostport -L [bind_address:]port:remote_socket -L local_socket:host:hostport -L local_socket:remote_socket 数据从本机转发到远程。本机上指定 TCP 端口或 UNIX 套接字的连接将被转发到目标机上指定端口或套接字。 上述参数中,bind_address 指本地地址;port 指本地端口;local_socket 指本地 UNIX 套接字;host 指远程主机地址;hostport 指远程端口;remote_socket 指远程 UNIX 套接字。 本地(ssh 客户端)与远程(ssh 服务端)建立一条连接,此连接的形式有四种: 1 2 3 4 本地 [bind_address:]port <====> 远程 host:hostport 本地 [bind_address:]port <====> 远程 remote_socket 本地 local_socket <====> 远程 host:hostport 本地 local_socket <====> 远程 remote_socket 位于本机的 ssh 客户端会分配一个套接字来监听本地 TCP 端口(port),此套接字可绑定本机地址(bind_address, 可选,本机不同网卡具有不同的 IP 地址)或本地 UNIX 套接字(local_socket)。每当一个连接建立于本地端口或本地套接字时,此连接就会通过安全通道进行转发。 也可在配置文件中设置端口转发功能。只有超级用户可以转发特权端口。 默认情况下,本地端口是根据 GatewayPorts 设置选项绑定的。但是,使用显式的bind_address 可将连接绑定到指定地址。bind_address 值是 “localhost”时,表示仅监听本机内部数据[TODO: 待验证],值为空或“*”时,表示监听本机所有网卡的监听端口。 注意:localhost 是个域名,不是地址,它可以被配置为任意的 IP 地址,不过通常情况下都指向 127.0.0.1(ipv4)和 ::1。127.0.0.1 这个地址通常分配给 loopback 接口。loopback 是一个特殊的网络接口(可理解成虚拟网卡),用于本机中各个应用之间的网络交互。 GatewayPorts 说明 (查阅 man sshd_config):指定是否允许远程主机(ssh客户端)连接到本机(ssh服务端)转发端口。默认情况下,sshd(8)将远程端口转发绑定到环回地址,这将阻止其他远程主机连接到本机转发端口。GatewayPorts 也可设置为将将远程端口转发绑定到非环回地址,从而允许其他远程主机连接到本机。GatewayPorts 值“no”,表示强制远程端口转发仅对本机可用;值“yes”,表示强制远程端口转发绑定到通配符地址;值“clientspecified”,表示允许客户端选择转发绑定到的地址。默认是“no”。 -R [bind_address:]port:host:hostport -R [bind_address:]port:local_socket -R remote_socket:host:hostport -R remote_socket:local_socket 此选项在本地机上执行,目标机上指定 TCP 端口或 UNIX 套接字的连接将被转发到本机上指定端口或套接字。 上述参数中,bind_address 指远程地址;port 指远程端口;remote_socket 指远程 UNIX 套接字;host 指本地地址;hostport 指本地端口;local_socket 指本地 UNIX 套接字。 工作原理:位于远程的 ssh 服务端会分配一个套接字来监听 TCP 端口或 UNIX 套接字。当目标机(服务端)上有新的连接建立时,此连接会通过安全通道进行转发,本地机执行当前命令的进程收到此转发的连接后,会在本机内部新建一条 ssh 连接,连接到当前选项中指定的端口或套接字。参 2.3 节分析。 也可在配置文件中设置端口转发功能。只有超级用户可以转发特权端口。 默认情况下,目标机(服务端)上的 TCP 监听套接字只绑定回环接口。也可将目标机上的监听套接字绑定指定的 bind_address 地址。bind_address 值为空或 “*” 时,表示目标机上的监听套接字会监听目标机上的所有网络接口。仅当目标机上 GatewayPorts 设置选项使能时,通过此选项为目标机指定 bind_address 才能绑定成功(参考 sshd_config(5))。 如果 port 参数是 ‘0’,目标机(服务端)可在运行时动态分配监听端口并通知本地机(客户端),如果同时指定了 “-O forward” 选项,则动态分配的监听端口会被打印在标准输出上。 -D [bind_address:]port 指定本地“动态”应用程序级端口转发。它的工作方式是分配一个套接字来监听本地端口(可选绑定指定的 bind_address)。每当连接到此端口时,连接都通过安全通道进行转发,然后使用应用程序协议确定将远程计算机连接到何处。目前支持 SOCKS4 和 SOCKS5 协议,ssh 将充当 SOCKS 服务器。只有 root 用户可以转发特权端口。还可以在配置文件中指定动态端口转发。 IPv6 地址可以通过将地址括在方括号中来指定。只有超级用户可以转发特权端口。默认情况下,本地端口是根据 GatewayPorts 设置选项进行绑定的。但是,可以使用显式的 bind_address 将连接绑定到特定的地址。bind_address 值为 “localhost” 时表示监听端口仅绑定为本地使用,而空地址或 “*” 表示监听所有网络接口的此端口。 1.2 ssh 端口转发模式 ssh 的端口转发有三种模式: 本地:ssh -C -f -N -g -L local_listen_port:remote_host:remote_port agent_user@agent_host 将本地机监听端口 local_listen_port 上的数据转发到远程端口 remote_host:remote_port 远程:ssh -C -f -N -g -R agent_listen_port:local_host:local_port agent_user@agent_host 将代理机监听端口 agent_listen_port 上的数据转发到本地端口 local_host:local_port 动态:ssh -C -f -N -g -D listen_port agent_user@agent_host 2. 利用 ssh 隧道建立远程调试环境 组网环境下设备角色如下: 代理机:把一个具有公网 IP 的中间服务器用作 ssh 代理,将这台代理机称作代理 A(Agent)。 目标机:把待调试的目标机器称作目标机 T(Target)。目标机通常是待调试的设备,处于局域网内,外网无法直接访问内网中的设备。 本地机:把调试用的本地计算机称作本地机 L(Local)。本地机通常也位于局域网内。 ssh隧道组网示意图 L 和 T 无法互相访问,但 L 和 T 都能访问 A。我们将 T 通过 ssh 连接到A,将 L 也通过 ssh 连接到A,A 用于转发数据,这样就能使用本地计算机 L 来访问远端设备 R。 2.1 目标机 T (sshc) 2.1.1 shell 中 T 连接 A 目标机 T 上的 sshc 连接代理机 A 上的 sshd: ssh -T -f -N -g -R :10022:127.0.0.1:22 frank@120.198.45.126 这条命令的作用: 1. 建立一条 ssh 连接,T 上的 ssh 客户端连接到 A 上的 ssh 服务器,A 的 IP 是 120.198.45.126,端口号是 10022,账号是10022; 2. 如果有其他 ssh 客户端连接到了 A 的 10022 端口上,则 A 会将这条连接转发到 T,T 在内部建立新的连接,连接到本机 22 端口。 这条命令在 T 上执行。在 T 连接 A 这条命令里,T 是本地主机(local),A 是远程主机(remote)。 解释一下此命令各选项: -T 不分配伪终端; -f 使 ssh 进程在用户输入密码之后转入后台运行; -N 不执行远程指令,即远程主机(代理机A)不需执行指令,只作端口转发; -g 允许远程主机(代理机A)连接到本地转发端口; -R 将远程主机(代理机A)指定端口上的连接转发到本机端口; frank@120.198.45.126 表示使用远程主机 120.198.45.126 上的用户 frank 来连接远程主机; :10022:127.0.0.1:22 表示本机回环接口(127.0.0.1,也可使用本机其他网络接口的地址,比如以太网 IP 或 WiFi IP)的 22 端口连接到远程主机的 10022 接口,因远程主机 10022 绑定的地址为空,所以远程主机会监听其所有网络接口的 10022 端口。 在目标机 shell 中查看连接是否建立: 1 2 3 root@localhost:~# ps | grep ssh 22850 root 2492 S ssh -T -f -N -g -R :10022:127.0.0.1:22 frank@120.198.45.126 22894 root 3500 S grep ssh 在目标机 shell 中关闭 ssh 连接: kill -9 $(pidof ssh) 此时在目标机 T 和代理机 A 中查看 ssh 连接信息,两端都可以看到 ssh 连接不存在了。 2.1.2 C 代码中 T 连接 A 的处理 C 代码中主要还是调用 2.1.1 节中的命令。但是由 C 代码编译生成的进程无法在命令行和用户进行交互,因此要避免交互问题。 1. 避免首次连接时的 y/n(或yes/no) 询问 如果是首次登录代理机 A,本机(目标机 T)没有 A 的信息,需用用户手动输入 y 之后才能继续。打印如下: 1 2 3 4 5 root@localhost:~# ssh -T -f -N -g -R :10022:127.0.0.1:22 frank@120.198.45.126 Host '120.198.45.126' is not in the trusted hosts file. (ssh-rsa fingerprint md5 86:09:0c:1b:fd:0b:02:8c:29:62:7f:ff:70:1b:64:f5) Do you want to continue connecting? (y/n) 如果 T 上有 A 的信息,可通过执行删除操作:rm ~/.ssh/known_hosts 再进行上述测试。 如果是在 C 代码中执行登录命令,进程在后台自动运行,是无法和用户进行交互的。为了避免交互动作,应该禁止 ssh 发出 y/n 的询问。 如果 ssh 客户端是 dropbear ssh,则添加 -y 参数,如下: ssh -y -T -f -N -g -R :10022:127.0.0.1:22 frank@120.198.45.126 如果 ssh 客户端是 openssh,则添加 -o StrictHostKeyChecking=no 选项,如下: ssh -o "StrictHostKeyChecking no" -y -T -f -N -g -R :10022:127.0.0.1:22 frank@120.198.45.126 2. 避免输入登录密码 避免由用户手动输入登录密码有如下方法: 1) 用 ssh-copy-id 把本地主机的公钥复制到远程主机的authorized_keys文件上,登录不需要输入密码。 2) 用 expect 调用 shell 脚本,向 shell 脚本发送密码。这种方式是模拟键盘输入。 3) 如果是 openssh,则用 sshpass 向 ssh 命令行传递密码。如果是 dropbear,则通过 DROPBEAR_PASSWORD 环境变量向 ssh 命令行传递密码。 我们采用第 3 种方法。 假如代理机 A 上用户 frank 密码是 123456,则最终 C 代码里应执行的指令如下: 1 2 3 4 5 # openssh sshpass -p '123456' ssh -y -T -f -N -g -R :10022:127.0.0.1:22 frank@120.198.45.126 # dropbear DROPBEAR_PASSWORD='123456' ssh -y -T -f -N -g -R :10022:127.0.0.1:22 frank@120.198.45.126 dropbear 无法接收 DROPBEAR_PASSWORD 变量传递密码的处理方法: dropbear 包含 ssh 客户端和 ssh 服务器,体积小巧,常用于嵌入式设备。dropbear ssh 无法接收 sshpass 传入的密码信息。但 dropbear ssh 可以通过环境变量 DROPBEAR_PASSWORD 传入密码信息。openwrt 从某一版开始,通过打补丁的方式禁用了 DROPBEAR_PASSWORD 选项,我们可以找到对应的补丁,开启 DROPBEAR_PASSWORD 选项,再重新编译生成 dropbear。如下: 修改 dropbear patch 文件(如下路径位于 openwrt 源码根目录): vim package/network/services/dropbear/patches/120-openwrt_options.patch 将如下几行删除: 1 2 3 4 5 6 7 8 9 @@ -226,7 +226,7 @@ much traffic. */ * note that it will be provided for all "hidden" client-interactive * style prompts - if you want something more sophisticated, use * SSH_ASKPASS instead. Comment out this var to remove this functionality.*/ -#define DROPBEAR_PASSWORD_ENV "DROPBEAR_PASSWORD" +/*#define DROPBEAR_PASSWORD_ENV "DROPBEAR_PASSWORD"*/ /* Define this (as well as ENABLE_CLI_PASSWORD_AUTH) to allow the use of * a helper program for the ssh client. The helper program should be 重新编译生成 dropbear,并替换设备里已安装的 dropbear。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #define DEFAULT_SSH_AGENT_HOST "120.198.45.126" #define DEFAULT_SSH_AGENT_PORT "10022" #define DEFAULT_SSH_AGENT_USER "ssha_debug" #define DEFAULT_SSH_AGENT_PASSWD "220011ssha" int login_to_ssh_agent(const char *host, const char *port, const char *user, const char *passwd) { // openssh client: // sshpass -p '123456' ssh -y -T -f -N -g -R :10022:127.0.0.1:22 frank@120.198.45.126 // dropbear ssh clent: // DROPBEAR_PASSWORD='123456' ssh -y -T -f -N -g -R :10022:127.0.0.1:22 frank@120.198.45.126 char cmd[256]; snprintf(cmd, sizeof(cmd), "DROPBEAR_PASSWORD='%s' ssh -y -T -f -N -g -R :%s:127.0.0.1:22 %s@%s", (passwd != NULL) ? passwd : DEFAULT_SSH_AGENT_PASSWD, (port != NULL) ? port : DEFAULT_SSH_AGENT_PORT, (user != NULL) ? user : DEFAULT_SSH_AGENT_USER, (host != NULL) ? host : DEFAULT_SSH_AGENT_HOST); printf("login to ssh agent: \n%s\n", cmd); system(cmd); return 0; } 2.2 代理机 A (sshd) 在 /etc/ssh/sshd_config 中添加如下几行后重启 ssh 服务: 1 2 3 GatewayPorts yes UseDNS no GSSAPIAuthentication no 目标机 T 发起连接后,在代理机 A 上查询目标机 T 是否连接成功: sudo netstat -anp | grep 10022 打印形如: 1 2 tcp 0 0 0.0.0.0:10022 0.0.0.0:* LISTEN 8264/sshd: frank tcp6 0 0 :::10022 :::* LISTEN 8264/sshd: frank 上述打印中,8264 就是和目标机 T 保持连接的 sshd 进程号,如需关闭当前连接重新建立一个新的连接,则先在代理机 A 上执行: kill -9 8264 然后再执行 2.1 节的指令,就会建立一次新的代理连接。 为了安全,我们可以专门新建一个用户,仅用于 ssh 端口转发功能,不能在 shell 中使用此用户登录。如下创建一个 ssha_debug 的用户,无 shell 登录权限。然后为此用户创建密码。注意系统中 nologin 文件的位置,不同系统可能路径不同。 1 2 sudo useradd ssha_debug -M -s /usr/sbin/nologin sudo passwd ssha_debug 2.3 本地机 L (sshc) 2.3.1 本地机 L 登录目标机 T 有三种方式: 1. 在本地机 L 上通过 ssh 登录代理机 A,在 A 的 shell 中再登录目标机 T 代理服务器的公网 ip 是 120.198.45.126,内网 ip 是 192.168.1.102。 1) 先使用 ssh(SecureCRT 或 OpenSSH 命令行) 登录上代理服务器的 shell。如果调试机在内网,既可登录代理机的外网 ip,也可登录其内网 ip。 2) 在代理机的 shell 中执行如下命令登录远程设备: ssh -p 10022 root@127.0.0.1 -vvv 注意,此命令中用户 root 及其密码是远程设备上的账户。 如果提示 Host key 认证失败之类的信息,请按提示执行如下命令: ssh-keygen -f "/home/frank/.ssh/known_hosts" -R [127.0.0.1]:10022 也可直接删除当前用户目录下的 .ssh/known_hosts 文件。 然后重新执行登录设备操作。 建议优先使用此方法。 2. 在本地机 L 上使用 ssh 命令登录目标机 T Win 10 系统默认安装有 OpenSSH 客户端。可以在调试机 Windows 命令行中执行: ssh -p 10022 root@120.198.45.126 -vvv 对于本地计算机来说,待调试的设备 ip 地址不可见。本机登录到代理机 120.198.45.126 的转发端口 10022,通过代理机转发功能,本地机能成功登录到远程设备上。注意,此命令中用户 root 及其密码是设备上的账户,不是 SSH 代理服务器上的账户。 如果出现认证失败之类的信息。可删除 C:/Users/当前用户/.ssh/known_hosts 文件,然后再试。 3. 在本地机 L 上使用 SecureCRT 工具登录目标机 T 也可以直接使用 SecureCRT 软件,设置好代理机的 ip(120.198.45.126) 和端口号(10022),填上设备的登录用户和登录密码。 不建议使用此方法。因为连接过程太长或连接失败的话,无法看到错误提示信息。 2.3.2 查看代理机 A 打印信息 在 L 执行登录 T 之前查看打印信息: 1 2 3 frank@SERVER:~$ sudo netstat -anp | grep 10022 tcp 0 0 0.0.0.0:10022 0.0.0.0:* LISTEN 106438/sshd: frank tcp6 0 0 :::10022 :::* LISTEN 106438/sshd: frank 在 L 执行登录 T 之后查看打印信息: 1 2 3 4 frank@SERVER:~$ sudo netstat -anp | grep 10022 tcp 0 0 0.0.0.0:10022 0.0.0.0:* LISTEN 106438/sshd: frank tcp 0 0 192.168.1.102:10022 120.229.163.51:27027 ESTABLISHED 106438/sshd: frank tcp6 0 0 :::10022 :::* LISTEN 106438/sshd: frank 可以看到,上述第二行是 L 执行登录命令后新出现的打印信息。表示新建立了一条 L 到 A 的 ssh 连接。 L 端的外网地址 120.229.163.51:27027 连接到 A 上的 192.168.1.102:10022,L 通常位于局域网内、具有一个内网地址,120.229.163.51 可能是 L 连接的路由器的公网 IP。 这条连接建立后,A 将这条连接转发到 R。 2.3.3 查看目标机 T 打印信息 在 L 执行登录 T 之前查看打印信息: 1 2 3 4 5 root@localhost:~# netstat -anp | grep 22 tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 917/sshd tcp 0 0 192.168.202.140:47989 120.198.45.126:22 ESTABLISHED 9452/ssh tcp 0 0 192.168.202.140:22 192.168.202.100:64737 ESTABLISHED 2041/sshd tcp 0 0 :::22 :::* LISTEN 917/sshd 在 L 执行登录 T 之后查看打印信息: 1 2 3 4 5 6 root@localhost:~# netstat -anp | grep 22 tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 917/sshd tcp 0 0 192.168.202.140:47989 120.198.45.126:22 ESTABLISHED 9452/ssh tcp 0 0 192.168.202.140:51732 192.168.202.140:22 ESTABLISHED 9452/ssh tcp 0 0 192.168.202.140:22 192.168.202.140:51732 ESTABLISHED 9579/sshd tcp 0 0 :::22 :::* LISTEN 917/sshd 可以看到,上述第 3 行和第 4 行是登录之后新增加的打印信息。 第 2 行,表示 T 上的 ssh 客户端连接到了 A 上的 ssh 服务端,进程号是 9452。第 3 行,表示进程 9452 收到了 A 转发来的 ssh 连接后,在本机内部建立新的 ssh 连接,使用 51732 端口号作为 ssh 客户端,连接到本机 22 端口,22 端口是 sshd 端口。第 4 行,表示本机新启动一个 sshd 进程,来接收 sshc 的连接。 这样,L 到 T 的 ssh 通路彻底打通了。A 将来自 L 的连接转发到 R,R 在内部启动了 sshd 来处理来自 L 的请求,通过 A 的代理作用,实现了 L 上的 sshc 和 T 上的 sshd 的交互。 3. 典型使用场景步骤总结 上文已涵盖详细使用方法,但篇幅太长。此处简单总结使用步骤如下: 3.1 在代理机 A 上执行 使用 SecureCRT 登录代理机 A。代理机外网 ip 120.198.45.126,内网 ip 192.168.1.102,端口 22。如果本地机与代理机在同一个局域网里,使用代理机的内网 ip 登录即可。 在代理机 shell 中查看是否有未关闭的 ssh 隧道: sudo netstat -anp | grep 10022 若打印形如: 1 2 tcp 0 0 0.0.0.0:10022 0.0.0.0:* LISTEN 8264/sshd: frank tcp6 0 0 :::10022 :::* LISTEN 8264/sshd: frank 则表示有未关闭的 ssh 隧道连接。执行如下命令可关闭连接。 kill -9 8264 3.2 在目标机 T 上执行 使用远程应用程序接口或者在远程设备 T 上做一些特殊操作,触发 T 执行如下两条指令之一: 1 2 3 4 5 # openssh sshpass -p '123456' ssh -y -T -f -N -g -R :10022:127.0.0.1:22 frank@120.198.45.126 # dropbear DROPBEAR_PASSWORD='123456' ssh -y -T -f -