# SSH 反向隧道实战:让 内网VM 经过代理使用本机网络(HTTP 代理 + npm/dnf 配置)

📅 发布时间:2026/7/31 18:46:49
# SSH 反向隧道实战:让 内网VM 经过代理使用本机网络(HTTP 代理 + npm/dnf 配置) 适用场景企业开发环境中内网主机常需访问 npm 仓库、PyPI、系统软件源等公共依赖仓库来安装依赖。本文用SSH 反向端口转发把一台开发机上的 HTTP 代理转发到内网主机本地127.0.0.1让VM 经过代理使用本机网络去访问这些软件源再配置npm/dnf等工具走该代理并用systemd --user托管实现断线自愈与开机自启。本文是一篇网络工程实践笔记核心一句话VM 经过代理可以使用本机的网络。聚焦SSH 反向隧道、HTTP 代理与包管理器代理配置三个技术点。一、环境与原理1.1 环境信息与变量对照下面以一次真实部署为例。复用时把表中的值替换成你自己的即可。角色含义本文示例值VM需访问软件源的内网主机10.44.57.177VM 登录可密钥免密 SSH 登录 rootssh root10.44.57.177本机开发机已配置可访问软件源用户xzyang本机内网网卡与 VM 互通的网卡enp1s0172.30.210.143/24本机出口网卡默认路由出口可访问软件源wlp2s0192.168.13.96/23本机 node 路径nvm 安装的 node/home/xzyang/.nvm/versions/node/v20.20.2/bin/node前置条件本机可访问目标软件源、本机与 VM 互通、本机可用密钥免密 SSH 登录 VM。1.2 两种代理思路对比为主机配置代理访问软件源通常有两类做法方案层级优点缺点NAT 路由本机开 ip_forward MASQUERADE主机把网关指向本机网络层全协议通任意 TCP 都行需本机 sudo要改主机默认路由配错可能失联需评估所在网络是否允许此类路由HTTP 代理本文应用层零改路由、可自愈、本机基本不需 root、可逐工具配置只代理 HTTP/HTTPS/FTP非代理感知程序不走本文场景只需要npm/dnf/curl等命令行工具拉取依赖应用层代理足矣且更安全可控故选 HTTP 代理。⚠️注意HTTP 代理工作在应用层只代理 HTTP/HTTPS/FTP 协议。ping(ICMP) 等不走 HTTP 代理属正常现象。如需全协议见文末“替代方案”。1.3 整体架构与数据流VM 上的程序curl/npm/dnf ... │ 读取 http_proxyhttp://127.0.0.1:8080 ▼ VM 127.0.0.1:8080 ← SSH 反向隧道在 VM 上的监听口 │ 经 SSH 加密隧道回到本机 ▼ 本机 127.0.0.1:8888 ← node HTTP 代理proxy.js │ 由本机路由表决定出口 ▼ WiFi(wlp2s0) ──────────────────────► 公共软件源几个关键点DNS 在本机代理侧解析VM 发起 HTTPS 请求时由本机的代理去解析域名VM 无需自行解析远端域名。内网直连不受影响把内网网段写入no_proxy10.x / 172.30.x / 169.254.x等内网流量直接走直连、不经过代理。VM 与本机不在同一子网中间隔着路由器但两端互可达SSH 反向隧道就建立在这条可达路径上。1.4 核心思路SSH 反向隧道是什么普通 SSH 本地转发ssh -L 8080:目标:端口是“把远端端口搬到我本地”。而反向转发ssh -R 8080:本机:8888 rootVM则相反从本机发起连接到 VM并在VM 侧开一个127.0.0.1:8080监听口所有到该端口的流量经 SSH 隧道回到本机、再转给本机的127.0.0.1:8888。于是VM 上的程序只要把代理指向http://127.0.0.1:8080流量就自动经 SSH 加密隧道送到本机的 HTTP 代理再由本机转发到目标软件源。整个过程不需要 VM 能主动连本机的任何端口——隧道是由本机主动建立的。二、步骤总览本机写一个零依赖的 node HTTP 代理监听127.0.0.1:8888。本机用 systemd --user 托管“代理”和“SSH 反向隧道”两个服务实现断线自愈 开机自启。VM放开 sshd 的端口转发权限默认AllowTcpForwarding no隧道建不起来。VM配置代理环境变量/etc/profile.d/etc/environment。VM单独配置 npm 代理npm 不读环境变量。VM单独配置 dnf/yum 代理dnf/yum 不读环境变量。三、操作步骤步骤 1本机创建 HTTP 代理程序mkdir-p~/vm-proxy创建~/vm-proxy/proxy.jsconstnetrequire(net);consthttprequire(http);constPORT8888;constHOST127.0.0.1;functionts(){returnnewDate().toISOString();}constserverhttp.createServer((req,res){// 普通 HTTP 代理请求绝对 URI转发到目标try{consttargetnewURL(req.url);constheaders{...req.headers};deleteheaders[proxy-connection];deleteheaders[connection];constupstreamhttp.request({hostname:target.hostname,port:target.port||80,path:target.pathnametarget.search,method:req.method,headers,},(upRes){res.writeHead(upRes.statusCode,upRes.headers);upRes.pipe(res);});upstream.on(error,(e){console.error([${ts()}] HTTP${req.method}${req.url}-${e.message});try{res.writeHead(502,{Content-Type:text/plain});res.end(Bad Gateway: e.message);}catch(_){}});req.on(error,()upstream.destroy());req.pipe(upstream);}catch(e){try{res.writeHead(400);res.end(Bad request: e.message);}catch(_){}}});// CONNECT 方法HTTPS 隧道server.on(connect,(req,clientSocket,head){consttargetreq.url;constidxtarget.lastIndexOf(:);consthostidx-1?target.slice(0,idx):target;constportidx-1?parseInt(target.slice(idx1),10):443;constupstreamnet.connect({host,port,timeout:30000},(){clientSocket.write(HTTP/1.1 200 Connection Established\r\n\r\n);if(headhead.length)upstream.write(head);upstream.pipe(clientSocket);clientSocket.pipe(upstream);});upstream.on(timeout,()upstream.destroy());upstream.on(error,(e){console.error([${ts()}] CONNECT${target}-${e.message});clientSocket.end(HTTP/1.1 502 Bad Gateway\r\n\r\n);});clientSocket.on(error,()upstream.destroy());});server.on(error,(e)console.error([${ts()}] server error:${e.message}));server.listen(PORT,HOST,(){console.log([${ts()}] HTTP forward proxy listening on${HOST}:${PORT});}); 说明HTTP 代理需要同时处理两种请求——普通 HTTP客户端发绝对 URI代理转发和 HTTPS客户端发CONNECT代理建一条到目标的 TCP 隧道并双向透传。上面代码分别用createServer和connect事件实现零第三方依赖。先自测代理本身可用不涉及 VMnode~/vm-proxy/proxy.js# 后台跑起来curl-sxhttp://127.0.0.1:8888-o/dev/null-wbaidu HTTP %{http_code}\nhttps://www.baidu.comcurl-sxhttp://127.0.0.1:8888-o/dev/null-wnpm HTTP %{http_code}\nhttps://registry.npmjs.org# 期望: baidu HTTP 200 / npm HTTP 200kill%1# 验证完关掉交给 systemd 管理步骤 2本机用 systemd --user 托管先确认linger已开启保证开机自启、不依赖你是否登录loginctl show-user$USER|grepLinger# 期望 Lingeryes若 nosudo loginctl enable-linger $USER创建~/.config/systemd/user/vm-proxy.service跑代理[Unit] DescriptionHTTP forward proxy for VM (node, listens 127.0.0.1:8888) Afternetwork-online.target Wantsnetwork-online.target [Service] Typesimple ExecStart/home/xzyang/.nvm/versions/node/v20.20.2/bin/node /home/xzyang/vm-proxy/proxy.js Restartalways RestartSec3 StandardOutputappend:/home/xzyang/vm-proxy/proxy.log StandardErrorappend:/home/xzyang/vm-proxy/proxy.log [Install] WantedBydefault.target创建~/.config/systemd/user/vm-proxy-tunnel.service跑 SSH 反向隧道依赖代理服务[Unit] DescriptionSSH reverse tunnel to VM (VM 127.0.0.1:8080 - local 8888) Aftervm-proxy.service network-online.target Requiresvm-proxy.service Wantsnetwork-online.target [Service] Typesimple ExecStart/usr/bin/ssh -N -o ServerAliveInterval30 -o ServerAliveCountMax3 -o ExitOnForwardFailureyes -o StrictHostKeyCheckingaccept-new -R 8080:127.0.0.1:8888 root10.44.57.177 Restartalways RestartSec5 StandardOutputappend:/home/xzyang/vm-proxy/proxy.log StandardErrorappend:/home/xzyang/vm-proxy/proxy.log [Install] WantedBydefault.target 关键参数解释-R 8080:127.0.0.1:8888在 VM 侧开127.0.0.1:8080转发到本机8888。ExitOnForwardFailureyes端口转发建立失败就让 ssh 退出配合Restartalways会不断重试直到成功。ServerAliveInterval30ServerAliveCountMax3隧道断线检测断开后 ssh 退出→systemd 自动重启。Requiresvm-proxy.service隧道依赖代理服务保证代理先起。加载并启用systemctl--userdaemon-reload systemctl--userenable--nowvm-proxy.service vm-proxy-tunnel.service systemctl--userstatus vm-proxy.service vm-proxy-tunnel.service# 两者应 active(running)ss-ltn|grep8888# 本机 8888 监听中顺手放一个管理脚本~/vm-proxy/vm-proxy.shcat~/vm-proxy/vm-proxy.shSCRIPT #!/usr/bin/env bash set -euo pipefail PROXY_SVCvm-proxy.service TUNNEL_SVCvm-proxy-tunnel.service VM10.44.57.177 start() { systemctl --user start $PROXY_SVC $TUNNEL_SVC; } stop() { systemctl --user stop $TUNNEL_SVC $PROXY_SVC; } restart() { systemctl --user restart $PROXY_SVC $TUNNEL_SVC; } is_active(){ systemctl --user is-active --quiet $1; } status() { for s in $PROXY_SVC $TUNNEL_SVC; do is_active $s echo ✓ $s 运行中 || echo ✗ $s 未运行; done ss -ltn 2/dev/null | grep -q :8888\b echo ✓ 本机 8888 监听中 || echo ✗ 本机 8888 未监听 if timeout 6 ssh -o StrictHostKeyCheckingaccept-new root$VM bash -lc curl -sI --max-time 5 https://www.baidu.com | head -1 2/dev/null | grep -q 200; then echo ✓ VM 经代理可访问软件源 else echo ✗ VM 经代理访问失败 fi } logs(){ systemctl --user status $PROXY_SVC $TUNNEL_SVC --no-pager; echo proxy.log 末尾 ; tail -n 20 /home/xzyang/vm-proxy/proxy.log; } case ${1:-} in start) start; echo 已启动 ;; stop) stop; echo 已停止 ;; restart) restart;echo 已重启 ;; status) status ;; logs) logs ;; *) echo 用法: $0 {start|stop|restart|status|logs} (委托 systemctl --user) ;; esac SCRIPTchmodx ~/vm-proxy/vm-proxy.sh步骤 3VM 放开 sshd 端口转发这一步是最大的坑。VM 的sshd_config默认常常是AllowTcpForwarding no此时隧道会报Error: remote port forwarding failed for listen port 8080且换任何端口都失败。必须改为yessshroot10.44.57.177# 在 VM 上cp/etc/ssh/sshd_config /etc/ssh/sshd_config.baksed-is/^\s*AllowTcpForwarding\s*no/AllowTcpForwarding yes/I/etc/ssh/sshd_config# 若该行被注释或不存在直接追加grep-qi^\s*AllowTcpForwarding/etc/ssh/sshd_config||echoAllowTcpForwarding yes/etc/ssh/sshd_config sshd-t# 语法校验无输出即 OKsystemctl reload sshd# reload 不断现有连接grep-iAllowTcpForwarding /etc/ssh/sshd_config回到本机重启隧道并确认 VM 侧已出现监听systemctl--userrestart vm-proxy-tunnel.servicesshroot10.44.57.177ss -ltn | grep 8080# 应看到 127.0.0.1:8080 监听步骤 4VM 配置代理环境变量/etc/profile.d/proxy.sh登录/交互 shell 生效sshroot10.44.57.177cat /etc/profile.d/proxy.sh EOF # 通过本机 SSH 反向隧道代理访问软件源 export http_proxyhttp://127.0.0.1:8080 export https_proxyhttp://127.0.0.1:8080 export ftp_proxyhttp://127.0.0.1:8080 export ALL_PROXYhttp://127.0.0.1:8080 export no_proxylocalhost,127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,172.30.0.0/16,192.168.0.0/16,169.254.0.0/16 export NO_PROXY$no_proxy EOF chmod 644 /etc/profile.d/proxy.sh/etc/environmentPAM 读取覆盖非登录 shell / cron / 系统服务sshroot10.44.57.177cat /etc/environment EOF http_proxyhttp://127.0.0.1:8080 https_proxyhttp://127.0.0.1:8080 ftp_proxyhttp://127.0.0.1:8080 ALL_PROXYhttp://127.0.0.1:8080 no_proxylocalhost,127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,172.30.0.0/16,192.168.0.0/16,169.254.0.0/16 NO_PROXYlocalhost,127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,172.30.0.0/16,192.168.0.0/16,169.254.0.0/16 EOFno_proxy里把内网网段都排除掉确保 VM 访问内网服务、云元数据169.254.169.254等仍走直连不被代理误伤。步骤 5VM 配置 npm 代理npm 不读http_proxy环境变量必须单独配置sshroot10.44.57.177 npm config set proxy http://127.0.0.1:8080 npm config set https-proxy http://127.0.0.1:8080 npm config set noproxy localhost,127.0.0.1,10.0.0.0/8,172.30.0.0/16 npm config get proxy; npm config get https-proxy; npm config get noproxy 步骤 6VM 配置 dnf/yum 代理dnf/yum 同样不读http_proxy环境变量要在[main]段写proxysshroot10.44.57.177 mkdir -p /etc/dnf for f in /etc/dnf/dnf.conf /etc/yum.conf; do touch $f if grep -q ^\[main\] $f; then sed -i /^\[main\]/a proxyhttp://127.0.0.1:8080 $f else printf [main]\nproxyhttp://127.0.0.1:8080\n $f fi grep -iE ^\[main\]|proxy $f done 四、验证# 本机服务状态bash~/vm-proxy/vm-proxy.sh status# VM逐项验证用登录 shell确保加载了代理环境sshroot10.44.57.177bash -lc curl -sI --max-time 12 https://www.baidu.com | head -1 curl -sI --max-time 12 https://registry.npmjs.org | head -1 npm view lodash version 21 | tail -1 dnf repolist curl -s --max-time 6 -o /dev/null -w \metadata %{http_code}\n\ http://169.254.169.254/latest/meta-data/ 实测输出节选HTTP/1.1 200 Connection Established ← curl 访问百度通 4.18.1 ← npm 成功拉到包元数据 repo id repo name v11-adv-os Kylin Linux Advanced Server 11 - Os ← dnf 走代理列出仓库 v11-adv-updates Kylin Linux Advanced Server 11 - Updates metadata HTTP 200 ← 内网云元数据直连未被代理影响curl/npm/dnf全部成功内网直连也未受影响方案完成。五、日常管理bash~/vm-proxy/vm-proxy.sh{start|stop|restart|status|logs}# 或直接操作 systemdsystemctl--user{start,stop,restart,status}vm-proxy.service vm-proxy-tunnel.service journalctl--user-uvm-proxy-tunnel.service-f# 看隧道日志tail-f~/vm-proxy/proxy.log# 看代理日志六、踩坑记录重点现象原因解决remote port forwarding failed for listen port 8080换任何端口都失败VMsshd_config为AllowTcpForwarding no改为yessshd -t校验后systemctl reload sshdVMcurl/npm都通了唯独dnf/yum不通dnf/yum不读http_proxy环境变量在dnf.conf/yum.conf的[main]段写proxyVMnpm install超时npm不读http_proxy环境变量npm config set proxy / https-proxyVMping不通HTTP 代理工作在应用层不代理 ICMP属正常如需 ping 见“替代方案”curl某站点返回000该目标本机本身访问不了代理只能转发到本机能到达的目标换可达目标验证在脚本里后台跑nohup cmd 父进程退出后子进程被一起带走 / 工具调用卡死子进程继承了会话的输出管道用setsid完全脱离或直接交给 systemd 托管推荐重启本机后隧道没了进程未做开机自启用 systemdenablelingeryes七、局限与替代方案局限HTTP 代理只代理 HTTP/HTTPS/FTPping(ICMP) 等不走代理属正常。非代理感知的程序、任意裸 TCP 连接不走代理。VM 的代理是常开的依赖本机两个 systemd 服务在线服务挂了 VM 的curl/npm/dnf会超时systemd 会自动重启。隧道在 VM 上只监听127.0.0.1:8080仅 VM 本机可用不对外暴露。代理只能转发到本机本身能到达的目标本机访问不了的目标VM 也访问不了。替代方案网络层 NAT 路由全协议若需要任意 TCP 全协议可用可改用 NAT 路由方案本机当 VM 网关 MASQUERADE 伪装# 本机需 sudosudosysctl-wnet.ipv4.ip_forward1echonet.ipv4.ip_forward1|sudotee/etc/sysctl.d/99-ipforward.confsudosysctl--systemsudoiptables-tnat-APOSTROUTING-s10.44.57.177-owlp2s0-jMASQUERADEsudoiptables-AFORWARD-mconntrack--ctstateESTABLISHED,RELATED-jACCEPTsudoiptables-AFORWARD-s10.44.57.177-jACCEPT# VM把默认网关指到本机内网网卡 IP内网仍走原网关iprouteadd172.30.210.143/32 via10.44.57.254# 先有到本机的路由iproute replace default via172.30.210.143# 默认走本机注意该方案会改主机默认路由配错可能失联建议先用ip route add试、配错可ip route del还原、需评估所在网络是否允许此类路由、且需本机 sudo 权限。仅命令行工具拉取依赖时优先用本文的 HTTP 代理方案。八、还原彻底拆除# 1) 本机停服务并禁用systemctl--userdisable--nowvm-proxy.service vm-proxy-tunnel.servicerm-f~/.config/systemd/user/vm-proxy.service ~/.config/systemd/user/vm-proxy-tunnel.service systemctl--userdaemon-reloadrm-rf~/vm-proxy# 2) VM 还原sshroot10.44.57.177 rm -f /etc/profile.d/proxy.sh sed -i /_proxy/d /etc/environment npm config delete proxy; npm config delete https-proxy; npm config delete noproxy sed -i /^proxy/d /etc/dnf/dnf.conf /etc/yum.conf sed -i s/^\s*AllowTcpForwarding\s*yes/AllowTcpForwarding no/I /etc/ssh/sshd_config sshd -t systemctl reload sshd 九、总结用SSH 反向隧道把本机的 HTTP 代理端口“搬”到 VM 本地VM 程序走127.0.0.1:8080即经过代理使用本机网络去访问目标软件源无需 VM 能主动连本机。三个关键配置别漏VM 的AllowTcpForwarding yes、npm 的npm config set proxy、dnf/yum 的[main] proxy——这三个是各自独立、互不读取环境变量的“坑点”。用 systemd --user linger托管实现断线自愈与开机自启长期稳定。本方案是应用层代理ping不通属正常如需全协议见 NAT 替代方案。一句话总结VM 经过代理可以使用本机的网络。如果本文对你有帮助欢迎点赞收藏有问题欢迎评论区交流。