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:180801. 默认行为:静默等待
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_keys3. 修改本地 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:180804. 验证效果
现在,你在命令行只需要输入:
ssh -N myproxy它将不再询问密码,直接建立隧道。
Stunnel (TLS/SSL Tunnel)
用它来配合 SSH,主要是为了解决 SSH 流量特征过于明显的问题:
流量伪装: Stunnel 能够将任何 TCP 流量(比如 SSH 流量)封装在 标准的 TLS/SSL 隧道 中。
混淆: 这样在网络防火墙看来,你的流量就像是在访问一个普通的 HTTPS 网站,从而躲避对 SSH 协议的深度数据包检测(DPI)。
要使用 Stunnel 来给 SSH 隧道穿上“HTTPS 伪装衣”,你需要分别在服务端(VPS)和客户端(你的电脑)上进行配置。
第一步:服务端配置
1、安装 Stunnel:
# Ubuntu/Debian
sudo apt install stunnel4
# CentOS
sudo yum install stunnel2、生成 SSL 证书: Stunnel 需要证书来加密。为了方便,可以生成自签名证书:
openssl req -new -x509 -days 3650 -nodes -out /etc/stunnel/stunnel.pem -keyout /etc/stunnel/stunnel.pem3、创建配置文件:
新建并编辑 /etc/stunnel/stunnel.conf:
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、启动服务:
sudo systemctl start stunnel4第二步:客户端配置
如果你用的是 Windows,可以去官网下载 Stunnel 安装包;macOS 可以使用 brew install stunnel。
1、编辑客户端配置文件 (stunnel.conf):
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 命令目标改为本地的这个“入口”即可:
ssh -D 1080 -N -f -p 10000 user@127.0.0.1为什么是 127.0.0.1? 因为此时本地的 10000 端口就等同于服务器的 SSH 端口。流量路径如下:
- 你的浏览器 ->
SOCKS5: 1080 - SSH 客户端 -> 转发到本地
10000 - Stunnel 客户端 -> 将数据包装成 TLS (HTTPS) -> 发往服务器
443 - 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进程

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 端口):
ssh -N -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -R 127.0.0.1:11080 -p 22022 root@relay.example.comB 上(把服务器上那个端口拉到本地):
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):
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 11080 | A 上的一个 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,让服务器早点发现对面没了、把座位腾出来。
⚠ ServerAliveInterval 和 ClientAliveInterval 是两侧各一个,名字很容易看串:前者写在客户端(客户端去探服务器,前面讲过),后者写在服务器的 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 服务(microsocks、danted 之类)监听 1080,再把 A 那条改回死钉的写法:
ssh -N -o ExitOnForwardFailure=yes -R 127.0.0.1:11080:127.0.0.1:1080 -p 22022 root@relay.example.comB 那条一个字都不用改 —— 对 B 来说,服务器的 11080 后面是谁在当 SOCKS 无所谓。
实战二:只绑 127.0.0.1 的后台,本地怎么打开
服务器上的管理后台、数据库、监控面板这类东西,正确做法是只监听 127.0.0.1,要么靠 nginx 反代出去,要么干脆不对外。临时想看一眼它的原始界面(比如反代那一层改过路径、或者正在排查反代本身),不用去改防火墙和配置文件:
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 指的是服务器自己。把后面那两段换成内网地址,同一条命令就能连到内网的另一台机器(由服务器代你去连):
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 不进隧道 |
评论
评论加载中……