用 SSH Tunnel 做内网穿透 & 服务器翻墙等骚操作

三葉Leaves
三葉Leaves Author

本文专注于解决下面这类问题:

A 能 SSH 到 B

但是

A 访问不了 X
或者
B 访问不了 Y

我用其解决了下面两个之前来看很麻烦的问题:

  1. 处在中国大陆环境的 Linux Server 访问不了 ChatGPT 等外网,而我们又不想给远程服务器配梯子
  2. 你已经通过 DDNS / Tailscale 等手段成功 SSH 到你家处在 NAT 后面的一台内网服务器,而你想访问内网里网关设备(如路由器)的管理页面。但是这个路由器是硬路由,并不能安装 Tailscale。而中国大陆地区的 ISP 又封了你家的 80 / 443 端口。这时候,可以借助你已经能访问的服务器,去触及目标内网环境的路由器(或者任何一台其他机子)

基础知识

今天这个场景下要学 SSH 命令的 3 个参数:

-L, -R, -D

一个比较好记的办法是:

L = Listen Local

R = Listen Remote

语法:

1
2
3
ssh -L LOCAL_PORT:TARGET_HOST:TARGET_PORT SSH_SERVER

ssh -R REMOTE_PORT:TARGET_HOST:TARGET_PORT SSH_SERVER

由于我们一般执行这个命令的时候不需要 shell 和终端,所以还可以加上 -NT 两个参数:

功能 命令 核心用途
Local Forward -L 借远端访问某个目标
Remote Forward -R 让远端借本机访问某个目标
Dynamic Forward -D 借远端建立 SOCKS Proxy
ProxyJump -J SSH 经跳板连接另一台 SSH
No command -N 只建立 tunnel
No TTY -T 不创建终端
Background -f SSH 进入后台
Keepalive ServerAliveInterval 保持 tunnel
Exit on failure ExitOnForwardFailure yes 转发失败直接退出

这里看不懂没关系,下面两个场景可以有直观的感受。

最后一个 -D 参数可以利用 socks 动态代理,有点 VPN 那味了,这就不需要手动指定要搬哪个 TCP 连接。但是今天的两个场景都用不上。

场景 A:用你的电脑帮你的远程服务器翻墙

用 AI 运维远程服务器的场景越来越多,Codex (ChatGPT Desktop APP) 就支持 Remote SSH 功能。

但是问题在于,这需要服务器能访问外网。

更别提还有一堆 curl Github 地址下载脚本,以及 Docker / npm 仓库的需要外网访问能力的问题了。

巧就巧在,在需要这些功能的场景下,通常你恰好会通过 SSH 访问机子。那如果直接用 SSH 帮助服务器翻墙,岂不美哉。

下面是我的配置:

~/.ssh/config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Host server server-pt
HostName <domin>
User root
Port 22
IdentityFile <path>
IdentitiesOnly yes

ForwardAgent no
Compression yes

ServerAliveInterval 30
ServerAliveCountMax 3

# 不复用普通 SSH 连接,避免与 cmux / 独立 tunnel 相互影响
ControlMaster no
ControlPath none
ControlPersist no

Host server
RequestTTY auto

Host server-pt
RequestTTY no

# 将服务器本机 17890 转发到 SSH 客户端机器的 7890 HTTP 代理
RemoteForward 127.0.0.1:17890 127.0.0.1:7890

# 如果远端 17890 无法成功监听,直接让 SSH 失败
ExitOnForwardFailure yes

这其实等价于:

1
2
3
ssh -NT \
-R 127.0.0.1:17890:127.0.0.1:7890 \
your-server

最终效果是让远程服务器监听它的 17890 端口,然后把本机的 7890 端口(Clash 常见代理端口)作为其出口。
正因如此,我们还需要把服务器上的 HTTP PROXY 环境变量设置到 17890。通常来说, export 几个环境变量就行了。但是为了更加优雅、自动化一点,我们可以这样做:

1
2
# 在远程服务器上
vim /root/.bashrc

然后添加如下内容在末尾:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# ---------------------------------------------------------------------------
# Non-interactive shells
#
# SSH automation and remote tools may invoke Bash non-interactively.
# Do not print banners or status messages in those sessions.
# ---------------------------------------------------------------------------

case $- in
*i*) ;;
*) return ;;
esac


# ---------------------------------------------------------------------------
# Standard interactive shell settings
# ---------------------------------------------------------------------------

HISTCONTROL=ignoreboth
shopt -s histappend

HISTSIZE=1000
HISTFILESIZE=2000

shopt -s checkwinsize

if [ -x /usr/bin/dircolors ]; then
test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"

alias ls='ls --color=auto'
alias grep='grep --color=auto'
alias fgrep='fgrep --color=auto'
alias egrep='egrep --color=auto'
fi

alias ll='ls -alF'
alias la='ls -A'
alias l='ls -CF'


# ---------------------------------------------------------------------------
# Reverse proxy tunnel
# ---------------------------------------------------------------------------

PROXY_HOST="127.0.0.1"
PROXY_PORT="17890"
PROXY_URL="http://${PROXY_HOST}:${PROXY_PORT}"

PROXY_NO_PROXY="127.0.0.1,localhost,::1"


_proxy_unset() {
unset HTTP_PROXY
unset HTTPS_PROXY
unset http_proxy
unset https_proxy
unset ALL_PROXY
unset all_proxy
unset NO_PROXY
unset no_proxy
}


_proxy_export() {
export HTTP_PROXY="$PROXY_URL"
export HTTPS_PROXY="$PROXY_URL"

export http_proxy="$PROXY_URL"
export https_proxy="$PROXY_URL"

export NO_PROXY="$PROXY_NO_PROXY"
export no_proxy="$PROXY_NO_PROXY"
}


# Check whether something is listening on the reverse tunnel port.
proxy-tunnel-available() {
ss -H -ltn "sport = :${PROXY_PORT}" 2>/dev/null | grep -q .
}


# Check the complete path:
#
# server -> reverse SSH tunnel -> client proxy -> Internet
proxy-working() {
proxy-tunnel-available || return 1

curl \
--proxy "$PROXY_URL" \
--connect-timeout 2 \
--max-time 4 \
--fail \
--silent \
--output /dev/null \
https://www.gstatic.com/generate_204
}


proxy-on() {
if proxy-working; then
_proxy_export
echo "[proxy] ON $PROXY_URL"
return 0
else
_proxy_unset
echo "[proxy] Cannot enable proxy: tunnel or upstream proxy is unavailable"
return 1
fi
}


proxy-off() {
_proxy_unset
echo "[proxy] OFF"
}


proxy-status() {
local tunnel_status="OFF"
local upstream_status="OFF"
local shell_status="OFF"

if proxy-tunnel-available; then
tunnel_status="ON"

if proxy-working; then
upstream_status="ON"
fi
fi

if [[ "${http_proxy:-}" == "$PROXY_URL" ]] &&
[[ "${https_proxy:-}" == "$PROXY_URL" ]]; then
shell_status="ON"
fi

echo "[proxy]"
echo " Tunnel: $tunnel_status"
echo " Upstream: $upstream_status"
echo " Shell: $shell_status"
echo " URL: $PROXY_URL"
}


proxy-check() {
if ! proxy-working; then
echo "[proxy] Proxy is unavailable"
return 1
fi

curl \
--proxy "$PROXY_URL" \
--connect-timeout 3 \
--max-time 8 \
--fail \
--silent \
https://www.cloudflare.com/cdn-cgi/trace \
| grep -E '^(ip|loc|colo)='
}


# ---------------------------------------------------------------------------
# Initialize proxy environment
# ---------------------------------------------------------------------------

if proxy-working; then
_proxy_export
_PROXY_INITIAL_STATUS="ON"
else
_proxy_unset
_PROXY_INITIAL_STATUS="OFF"
fi


# ---------------------------------------------------------------------------
# Interactive SSH login banner
# ---------------------------------------------------------------------------

if [[ -n "${SSH_CONNECTION:-}" ]]; then
read -r SSH_CLIENT_IP \
SSH_CLIENT_PORT \
SSH_SERVER_IP \
SSH_SERVER_PORT <<< "$SSH_CONNECTION"

echo
echo "[ssh]"
printf ' Client: %s:%s -> Server: %s:%s\n' \
"$SSH_CLIENT_IP" \
"$SSH_CLIENT_PORT" \
"$SSH_SERVER_IP" \
"$SSH_SERVER_PORT"

echo

if [[ "$_PROXY_INITIAL_STATUS" == "ON" ]]; then
echo "[ssh]"
echo " Reverse proxy tunnel is healthy"
echo
echo "[proxy] ON $PROXY_URL"
else
echo "[ssh]"
echo " Reverse proxy tunnel is unavailable"
echo
echo "[proxy] OFF"
echo " Direct network will be used"
fi

echo
fi

unset _PROXY_INITIAL_STATUS

这会在你 SSH 到这台机子的时候,自动检查一下 proxy 情况。如果 17890 端口的代理确实能走得通,则配置好环境变量。

如果走不通,则回退到直接连接。另外提供 proxy-off 函数用于手动关闭代理的场景

每一个 SSH 会话的代理都是分离的。

一个有趣的事情是,如果你本地使用了 ssh -R 给远端监听 17890 端口,在这套机制下,你朋友如果此时也 SSH 上了机子,可能会意外的也通过你的电脑获得了代理翻墙的能力。

场景 B:用远程服务器作跳板,帮助你访问其内网环境里的路由器

假设你家的服务器和你家的路由器在同一个局域网,服务器在 192.168.2.1。路由器管理界面的网页 HTTP 一般在 80 端口。这时候,我们在本机执行:

1
ssh -NT -L 8080:192.168.2.1:80 <ssh_server>

然后直接在你的浏览器里访问:

1
http://127.0.0.1:8080/

你会发现你能直接打开你家路由器的管理界面。

这个过程是:

  1. 你的电脑在本地开始监听 8080 端口
  2. 把远程服务器所在的网络环境里的 192.168.2.1:80 作为网络出口
  3. 你访问本机的 8080 端口,然后你的服务器就会去访问它视角下的 192.168.2.1:80,而这自然是你家的路由器管理地址。
  • 标题: 用 SSH Tunnel 做内网穿透 & 服务器翻墙等骚操作
  • 作者: 三葉Leaves
  • 创建于 : 2026-09-23 00:00:00
  • 更新于 : 2026-09-24 15:39:57
  • 链接: https://blog.oksanye.com/dea8c78408ff/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论