ssh妙用
SSH 端口转发的四种用法:本地转发、远程转发、动态转发(SOCKS5)与反向动态转发,用于访问内网服务、把本机服务暴露给外网、让两台互相看不见的机器借道服务器共享出口、以及把内网机器变成公网可用的 SOCKS 代理。
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 无所谓。
实战二:把内网机器变成一个公网 SOCKS 代理
实战一里 B 也得连一条 ssh,把端口拉回自己本地。换个需求:内网里有台机器 C(外面连不进来,但它能连到服务器),想让它当代理出口,而用代理的人什么都不装 —— 在浏览器、Clash 或者系统代理里填「服务器地址 + 端口 + SOCKS5」就能用。
和实战一只差一处:服务器上那个 SOCKS 端口不能再只绑 127.0.0.1,得对公网开放。
服务器上(一次性):
# /etc/ssh/sshd_config 里加一行(已有 GatewayPorts 就改它)
GatewayPorts clientspecifiedsystemctl restart ssh # Ubuntu 上服务名是 ssh,别的发行版多是 sshd然后去云厂商的防火墙 / 安全组放行 TCP 11080 入站,来源范围见下面「两道门」。
C 上:
ssh -N -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -R 0.0.0.0:11080 -p 22022 root@relay.example.com使用方的代理设置:
| 项 | 填什么 |
|---|---|
| 类型 | SOCKS5 |
| 地址 | relay.example.com 或服务器公网 IP |
| 端口 | 11080 |
| 用户名 / 密码 | 不填(加了门二才填) |
流量路径是 使用方 → 服务器:11080 → ssh 隧道 → C → 目标站点,出口 IP 是 C 的。
验证分两步,先在服务器上看端口绑在哪:
ss -tlnp | grep 11080要看到 0.0.0.0:11080。如果是 127.0.0.1:11080,说明 GatewayPorts 没生效(sshd 没重启,或者配置文件后面有个 Match 块把它盖掉了)。⚠ 这种情况 ssh 不会报错,ExitOnForwardFailure 也拦不住:sshd 只是把 0.0.0.0 这个要求默默改成了回环,转发本身是建成了的。
再找一台外网机器:
curl -x socks5h://relay.example.com:11080 https://ifconfig.me回显的应该是 C 的出口 IP。
为什么是 clientspecified 而不是 yes
GatewayPorts 管的是 -R 开出来的端口绑在哪。三个值的区别:
GatewayPorts | -R 11080 | -R 0.0.0.0:11080 |
|---|---|---|
no(默认) | 回环 | 回环(客户端的要求被忽略) |
yes | 公网 | 公网 |
clientspecified | 回环 | 公网 |
yes 是把服务器上所有 -R 都强制绑到公网 —— 包括实战一那个本该只有服务器自己连得上的 11080。前面「SSH 远程转发」那节开的就是 yes,服务器上要是已经这么配了,这条命令照样能用,但实战一那个端口也一起暴露了。clientspecified 让每条命令自己说了算:写了 0.0.0.0 才开到公网,不写还是回环。
⚠ 这样开出来的是一个谁都能用的代理
ssh 自带的 SOCKS 不支持认证。公网上一直有人在扫开放的 SOCKS 端口,扫到就拿去发垃圾邮件、撞库、刷接口 —— 出口 IP 是 C 的,也就是你内网那条线路的,最后找上门的是你。下面两道门至少关一道。
门一:只放行认识的 IP。最省事,使用方 IP 固定时首选。安全组里来源范围只填对方的公网 IP(203.0.113.7/32 这种),不填 0.0.0.0/0。没有安全组就在服务器上用 iptables:
iptables -I INPUT -p tcp --dport 11080 -j DROP
iptables -I INPUT -p tcp --dport 11080 -s 203.0.113.7 -j ACCEPT-I 是插到最前面,所以顺序是反的:先插 DROP、再插 ACCEPT,ACCEPT 才会排在 DROP 上面。这两条重启就丢,长期用得装 iptables-persistent 存下来。
门二:加用户名密码。使用方 IP 不固定(手机、经常换网络)时只能走这条。在 C 上跑一个支持认证的 SOCKS 服务,ssh 那条退回实战一末尾的死钉写法:
# C 上,只听本机,外面直接连不到它
microsocks -i 127.0.0.1 -p 1080 -u alice -P '换成一个长密码'ssh -N -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -R 0.0.0.0:11080:127.0.0.1:1080 -p 22022 root@relay.example.com使用方在 SOCKS5 设置里多填用户名和密码。microsocks 在 Debian / Ubuntu 上 apt install microsocks 就有。注意 SOCKS5 的用户名密码是明文传的:挡得住扫描器,挡不住链路上有人偷看;要加密就让使用方走实战一那条 ssh。
C 手里那把钥匙最好只能干这一件事
上面的命令是拿 root 连服务器,而 C 是一台常年开着、挂在内网的机器,它的私钥等于服务器的 root。给它单独一把钥匙,在服务器的 authorized_keys 里把这把钥匙锁死:
restrict,port-forwarding,permitlisten="0.0.0.0:11080" ssh-ed25519 AAAA…(C 的公钥)restrict 关掉 shell、伪终端和所有转发,port-forwarding 再把转发开回来,permitlisten 限定只能开这一个端口(OpenSSH 7.8 起)。这把钥匙泄露了,别人能做的也就是占用 11080。因为没有 shell,命令里的 -N 这时是必需的。
常驻:systemd 拉着
C 是台服务器,前台挂一条 ssh 不现实。写个 unit,掉线自己重连:
# /etc/systemd/system/socks-relay.service
[Unit]
Description=Reverse SOCKS via relay
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/bin/ssh -N -o BatchMode=yes -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -i /root/.ssh/relay -R 0.0.0.0:11080 -p 22022 root@relay.example.com
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetsystemctl daemon-reload && systemctl enable --now socks-relay两个细节:BatchMode=yes 让它在要输密码、要确认主机指纹时直接失败退出,而不是卡在一个没人看得见的提示上;也正因为这样,先用 root 手动连一次把服务器指纹存进 /root/.ssh/known_hosts,不然它会一直起不来。重连时旧连接没被回收、端口还占着的问题和实战一一样,服务器上配 ClientAliveInterval 30 能缩短这段空窗。
用了门二的话,microsocks 也照这个样子写一个 unit,ssh 那个 unit 的 ExecStart 换成死钉写法。
性能预期
所有使用方的所有连接都挤在 C 到服务器的一条 TCP 连接里。那条线上丢一个包,所有人一起卡(队头阻塞),带宽上限是 C 和服务器两段里较窄的那段。几个人日常用没问题,拿来跑大流量下载就会明显发涩。
实战三:只绑 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 或 clientspecified,见实战二) |
GatewayPorts clientspecified(服务器侧) | -R 写了 0.0.0.0 才绑公网,不写仍是回环 | 默认 no:客户端要 0.0.0.0 也被默默改成回环,不报错 |
permitlisten="0.0.0.0:11080"(authorized_keys) | 这把钥匙只准开这一个 -R 端口 | 钥匙能开任意端口,配上 root 还能登 shell |
ClientAliveInterval(服务器侧) | 服务器主动探客户端还在不在 | 死连接可能挂很久,一直占着 -R 那个端口 |
curl -x socks5h://… | 让 curl 走这个 SOCKS 代理,域名由代理解析 | 写 socks5:// 就是本地解析域名,DNS 不进隧道 |
评论
评论区加载中
正在获取评论,请稍候。