ssh妙用

SSH 端口转发的四种用法:本地转发、远程转发、动态转发(SOCKS5)与反向动态转发,用于访问内网服务、把本机服务暴露给外网、以及让两台互相看不见的机器借道服务器共享出口。

ssh端口转发(用于设置代理)

ssh -D [本地监听IP]:[本地端口] [用户名]@[远程服务器IP]

作用: 在本地建立一个 SOCKS 代理服务器

动态转发: 与固定端口转发(只能访问特定 IP 和端口)不同,“动态”意味着你可以通过这个隧道访问远程服务器能触达的任何地址。

虽然SSH代理本身是高度加密的,但是防火墙能识别出协议是ssh,如果你用它来浏览网页或看视频,会产生长时间、高带宽的 SSH 流量。这种“不正常”的行为很容易触发防火墙的封锁。所以如果为了安全需要,最好配合stunnel 进行的加密隧道传输。

严格来说,这通常被归类为 加密代理(Encrypted Proxy) 而非传统意义上的 VPN,但在广义的“科学上网”语境下,它们常被当作 VPN 的替代方案。

SSH 单独使用: 流量特征 = [TCP] -> [SSH 加密数据](容易被识别)。

SSH + Stunnel: 流量特征 = [TCP] -> [TLS/SSL 握手] -> [看似 HTTPS 的加密数据]

Windows 批处理脚本 (Auto-Reconnect)

你可以创建一个 .bat 文件(例如 ssh_proxy.bat),将以下代码复制进去。即使连接断开,它也会每隔 30 秒自动尝试重新连接(使用 SSH 密钥对登录!):

@echo off
:loop
echo [%date% %time%] 正在尝试建立 SSH 隧道...

:: 如果你已经配置了密钥登录(推荐):
ssh -D 127.0.0.1:18080 -N -o ServerAliveInterval=30 -o ServerAliveCountMax=3 root@vps.example.com
(如果在config文件配置了其他的参数)
ssh -N vps.example.com

:: 如果连接断开,程序会执行到这里
echo [%date% %time%] 连接已断开或失败,30秒后重试...
timeout /t 30
goto loop

-o ServerAliveInterval=30: 每隔 30 秒向服务器发送一个“心跳”包。

-o ServerAliveCountMax=3: 如果连续 3 次没收到回应,才认为连接断开。

可以设置在本机config文件里面

Host vps.example.com
    User root
    Port 22
    ServerAliveInterval 30
    ServerAliveCountMax 3
    DynamicForward 127.0.0.1:18080

1. 默认行为:静默等待

ServerAliveInterval: 默认值是 0

  • 这意味着客户端永远不会主动向服务器发送“心跳”包。

ServerAliveCountMax: 默认值通常是 3

  • 但因为 Interval 是 0,这个计数器实际上处于失效状态。

不设置会导致什么后果?

A. 被防火墙/路由器踢掉(死连接)

这是最常见的情况。很多网络设备(如家用路由器、公司防火墙或云服务商的负载均衡器)为了节省资源,会自动切断长时间没有数据传输的 TCP 连接。

B. 无法及时察觉断线

如果远程服务器意外重启或网络瞬间中断:

现象: SSH 客户端会卡在那里,既不报错也不退出,可能要等几十分钟(触发 TCP 自身的 KeepAlive 超时)才会提示 Connection timed out

附上配置密钥对登录的全过程:

1. 在本地 Windows 生成密钥对

在你的 C:\Users\maluyao 路径下的命令行执行以下命令:

ssh-keygen -t ed25519

-t ed25519: 这是目前最推荐的算法,比传统的 RSA 更快且更安全。

按回车确认: 提示保存路径和密码(Passphrase)时,全部直接按回车(不设置密码,这样重连时才不需要人工干预)。

生成结果: 你会在 C:\Users\maluyao\.ssh\ 文件夹下看到两个文件:

  • id_ed25519: 你的私钥(绝对不能给别人)。
  • id_ed25519.pub: 你的公钥

2. 将公钥上传到远程服务器

你需要把公钥的内容告诉服务器,以便它能识别你的电脑。

复制公钥内容: 用记事本打开 C:\Users\maluyao\.ssh\id_ed25519.pub,复制里面那串以 ssh-ed25519 开头的长字符串。

登录服务器并写入: 先最后一次手动输入密码登录到你的 Ubuntu 服务器,然后执行以下命令:

# 创建 .ssh 目录(如果不存在)
mkdir -p ~/.ssh
# 将公钥写入授权文件
echo "这里粘贴你刚才复制的公钥内容" >> ~/.ssh/authorized_keys
# 设置权限(非常重要,权限不对会导致无法登陆)
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

3. 修改本地 config 文件(终极优化)

现在你可以把所有的连接参数固化到 C:\Users\maluyao\.ssh\config 文件中:

Host myproxy
    HostName vps.example.com
    User root
    Port 22
    # 自动重连心跳设置
    ServerAliveInterval 30
    ServerAliveCountMax 3
    # 密钥路径
    IdentityFile ~/.ssh/id_ed25519
    # 自动开启动态代理
    DynamicForward 127.0.0.1:18080

4. 验证效果

现在,你在命令行只需要输入:

ssh -N myproxy

它将不再询问密码,直接建立隧道。

Stunnel (TLS/SSL Tunnel)

用它来配合 SSH,主要是为了解决 SSH 流量特征过于明显的问题:

流量伪装: Stunnel 能够将任何 TCP 流量(比如 SSH 流量)封装在 标准的 TLS/SSL 隧道 中。

混淆: 这样在网络防火墙看来,你的流量就像是在访问一个普通的 HTTPS 网站,从而躲避对 SSH 协议的深度数据包检测(DPI)。

要使用 Stunnel 来给 SSH 隧道穿上“HTTPS 伪装衣”,你需要分别在服务端(VPS)和客户端(你的电脑)上进行配置。

第一步:服务端配置

1、安装 Stunnel:

bash
# Ubuntu/Debian
sudo apt install stunnel4
# CentOS
sudo yum install stunnel

2、生成 SSL 证书: Stunnel 需要证书来加密。为了方便,可以生成自签名证书:

openssl req -new -x509 -days 3650 -nodes -out /etc/stunnel/stunnel.pem -keyout /etc/stunnel/stunnel.pem

3、创建配置文件: 新建并编辑 /etc/stunnel/stunnel.conf

ini
    cert = /etc/stunnel/stunnel.pem
    setuid = stunnel4
    setgid = stunnel4
    pid = /var/run/stunnel4/stunnel.pid

    [ssh]
    accept = 443
    connect = 127.0.0.1:22
*   accept = 443: Stunnel 监听的对外端口(伪装成 HTTPS)。
*   connect = 127.0.0.1:22: 收到流量后转发给本地的 SSH 服务。

4、启动服务:

bash
sudo systemctl start stunnel4
第二步:客户端配置

如果你用的是 Windows,可以去官网下载 Stunnel 安装包;macOS 可以使用 brew install stunnel

1、编辑客户端配置文件 (stunnel.conf):

ini
    client = yes
    [ssh]
    accept = 127.0.0.1:10000
    connect = [你的服务器IP]:443
*   accept = 127.0.0.1:10000: 在你本地机器上开一个“入口”端口。
*   connect = [你的服务器IP]:443: 连接到服务器上 Stunnel 监听的 443 端口。

2、启动客户端 Stunnel。

第三步:通过“隧道中的隧道”建立 SSH 代理

现在,你的本地端口 10000 已经通过 TLS 加密直连到了服务器的 SSH 服务。你只需要把之前的 SSH 命令目标改为本地的这个“入口”即可:

bash
ssh -D 1080 -N -f -p 10000 user@127.0.0.1

为什么是 127.0.0.1 因为此时本地的 10000 端口就等同于服务器的 SSH 端口。流量路径如下:

  1. 你的浏览器 -> SOCKS5: 1080
  2. SSH 客户端 -> 转发到本地 10000
  3. Stunnel 客户端 -> 将数据包装成 TLS (HTTPS) -> 发往服务器 443
  4. Stunnel 服务端 -> 解开 TLS -> 发往服务器本地 22 (SSH)

ssh本地转发

ssh -L [本地端口]:[目标IP]:[目标端口] user@server

作用:远程服务器上的某个端口,“映射”到你本地电脑的某个端口

场景: 比如远程服务器(内网)里跑着一个数据库(端口 3306),你的电脑连不上。你可以通过 SSH 把它映射到你本地的 3306。

原理: 访问本地 localhost:3306 = 访问远程 server:3306

ssh -L [本地绑定地址:]本地端口:目标地址:目标端口 用户名@SSH服务器
当你使用SSH密钥来连接远程服务器时,使用-i参数指定
-i /path/to/key.pem

只想做端口转发,不需要进入vps,在后面加上-N参数

SSH 远程转发

ssh -R [服务器端口]:[目标IP]:[目标端口] user@server

作用:你本地的某个端口,“反向映射”到远程服务器的某个端口

场景: 内网穿透。比如你在寝室(内网)电脑跑了一个网站,想让身在外地的朋友通过你的服务器 IP 访问到你这台电脑。

必须在 VPS 的 /etc/ssh/sshd_config 中控制哪些用户允许反向隧道:

# 确保以下配置开启
GatewayPorts yes
AllowTcpForwarding yes

重新启动 SSH 服务:

sudo systemctl restart sshd

调试

检查端口+kill进程

image-20250808003218685

SSH 长时间空闲被踢

ssh -N -R 8868:localhost:8868 -o ServerAliveInterval=60 root@relay.example.com

如果本机没有开ipv6会出现问题,因为Windows主机优先把localhost解析为ipv6地址

Windows 上 localhost 默认优先解析为 IPv6(::1), 而你的 Nginx 只监听了 IPv4(127.0.0.1), SSH 反向隧道实际连到了 ::1:443,结果直接被系统关闭。

C:\Users\maluyao>ssh -N -T -R 0.0.0.0:8867:localhost:443 root@relay.example.com
失败!!!

C:\Users\maluyao>ssh -N -T -R 8867:127.0.0.1:443 root@relay.example.com
成功!!!

这里的 localhost 在 Windows 上经常会优先解析到 IPv6 的 ::1(而不是 127.0.0.1)。 你的本机 Nginx 虽然能用 https://127.0.0.1:443 访问,但很可能 没有在 ::1:443 上监听(没开 IPv6 listen)。

真正的问题是:Windows 上用 localhost 做转发目标时,实际走到 IPv6 ::1,而本机服务只在 IPv4 上可用

实战一:让 B 借 A 的网络出去(反向动态转发)

A、B 两台机器互相看不见(各在一层 NAT 后面),但都能连到同一台有公网 IP 的服务器。想让 B 上的程序从 A 的出口发出去 —— 比如只有 A 那条线路能访问某个站点,而人在 B 上。服务器只当汇合点,上面不用装任何东西,两条命令就够。

A 上(把 A 变成出口,在服务器上开一个 SOCKS 端口):

bash
ssh -N -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -R 127.0.0.1:11080 -p 22022 root@relay.example.com

B 上(把服务器上那个端口拉到本地):

bash
ssh -N -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -L 127.0.0.1:1080:127.0.0.1:11080 -p 22022 root@relay.example.com

之后 B 上的程序填 socks5://127.0.0.1:1080,流量路径是 B → 服务器 → A → 目标站点,出口 IP 是 A 的。

验证(在 B 上跑,应该回显 A 的公网 IP):

bash
curl -x socks5h://127.0.0.1:1080 https://ifconfig.me

关键是 A 那条 -R 后面不写目标地址

前面那节的 -R 8868:localhost:8868 是把服务器的 8868 钉死在本机的 8868 上,一个端口只通到一个目标。而 -R 127.0.0.1:11080 只给了端口、没给目标,ssh 就会在服务器那侧扮演一个 SOCKS4/5 代理,由 A 按客户端每次的要求现连目标。这叫反向动态转发,等于把 -D 的方向反过来:-D 是「我出去的流量走服务器」,它是「服务器那侧出去的流量走我」。

写法服务器上的 11080 通向哪
-R 11080:127.0.0.1:80死钉在 A 的 80 端口
-R 11080A 上的一个 SOCKS 代理,目标由调用方每次现说

这是 OpenSSH 7.6(2017 年)才加的能力,A 上先 ssh -V 确认一遍版本。

两端都绑 127.0.0.1

-R 127.0.0.1:11080-L 127.0.0.1:1080:… 最前面那一截是绑定地址。服务器侧默认本来就只绑回环,写出来只是把意图说明白:这个 11080 只有服务器自己连得上,公网扫不到,也不需要动 sshd 里的 GatewayPorts —— 那一项是「允许 -R 绑到 0.0.0.0」的开关,前面那节为了让朋友从外网访问才开的,这里正好用不上。B 上的 1080 同理只服务本机,不会变成一个谁都能用的开放代理。

ExitOnForwardFailure=yes:转发没建成就当场退出

默认行为很坑:转发建立失败时 ssh 只打一行警告,然后照样把连接建起来。于是你看到一个连上了的 ssh 和一个根本不通的代理,排查方向从一开始就是错的。加上这一项就是「端口没占上就直接退出」。

A 断线重连时最容易撞上它:服务器上旧的那条连接可能还没被回收,11080 还占着。要么等几十秒再连,要么在服务器的 sshd_config 里加 ClientAliveInterval 30,让服务器早点发现对面没了、把座位腾出来。

ServerAliveIntervalClientAliveInterval两侧各一个,名字很容易看串:前者写在客户端(客户端去探服务器,前面讲过),后者写在服务器的 sshd_config 里(服务器去探客户端)。想让服务器早点回收死连接,只有后者管用。

只有 TCP,没有 UDP

socks5h:// 里那个 h 的意思是「域名交给代理端解析」。写成 socks5:// 的话 B 会先在本地把域名解析掉、只把 IP 交给代理 —— DNS 那一跳没进隧道,解析到的是 B 这边就近的结果,出口是 A 但拿到的地址不是 A 看到的,容易走偏。另外 SOCKS 隧道只转发 TCP,UDP 一律不过。

两条都是前台长跑

要放后台就各加 -f,但掉线了不会自己回来;长期挂着用 systemd 或 autossh 拉着,Windows 上就是前面那个 .bat 循环。

如果 A 的 ssh 太老,没有反向动态转发

退路是在 A 上另跑一个真的 SOCKS 服务(microsocksdanted 之类)监听 1080,再把 A 那条改回死钉的写法:

bash
ssh -N -o ExitOnForwardFailure=yes -R 127.0.0.1:11080:127.0.0.1:1080 -p 22022 root@relay.example.com

B 那条一个字都不用改 —— 对 B 来说,服务器的 11080 后面是谁在当 SOCKS 无所谓。

实战二:只绑 127.0.0.1 的后台,本地怎么打开

服务器上的管理后台、数据库、监控面板这类东西,正确做法是只监听 127.0.0.1,要么靠 nginx 反代出去,要么干脆不对外。临时想看一眼它的原始界面(比如反代那一层改过路径、或者正在排查反代本身),不用去改防火墙和配置文件:

bash
ssh -N -L 127.0.0.1:3000:127.0.0.1:3000 -p 22022 root@relay.example.com

本地浏览器开 http://127.0.0.1:3000 就是它。冒号分出来的四段是「本地绑哪 : 本地端口 : 去连哪 : 目标端口」,而第三段那个目标地址是在服务器上解析、由服务器发起连接的,所以这里的 127.0.0.1 指的是服务器自己。把后面那两段换成内网地址,同一条命令就能连到内网的另一台机器(由服务器代你去连):

bash
ssh -N -L 127.0.0.1:3306:10.0.0.5:3306 -p 22022 root@relay.example.com

参数速查:上面这几条里新出现的

参数干什么不写会怎样
-N只转发、不开远端 shell多出一个交互 shell,误按 exit 隧道就断
-T不分配伪终端-N 时基本用不上它,只在「让远端跑一条命令但不给终端」时才需要
-o ExitOnForwardFailure=yes转发没建成就退出只警告一行、连接照样建立,得到一条连上了却不通的隧道
-R 127.0.0.1:11080(不写目标)反向动态转发,出口在 A老版本 ssh 会报转发格式错误
最前面那截绑定地址端口只对本机开放-L 默认就是本机;-R 服务器侧默认也只绑回环,要绑 0.0.0.0 才需要 GatewayPorts yes
ClientAliveInterval(服务器侧)服务器主动探客户端还在不在死连接可能挂很久,一直占着 -R 那个端口
curl -x socks5h://…让 curl 走这个 SOCKS 代理,域名由代理解析socks5:// 就是本地解析域名,DNS 不进隧道

评论

评论加载中……

登录后再评论

注册要用邮箱收个验证码,只为确认邮箱能收信,不会拿去做别的。账号设置