抓狂!明明开了系统代理,为啥CMD和PowerShell还是上不了网?深扒Windows代理机制的坑
昨晚本来打算早点睡的,结果刚躺下,手机就震动个不停。看了一眼,是咱们群里的小王发来的私信,一连串的表情包加感叹号,隔着屏幕都能感觉到他的崩溃。
事情挺简单,也挺典型。这哥们最近在捣鼓那个啥“科学上网”工具,想在服务器上拉个Docker镜像,或者是用脚本去请求个外部API。他在Windows设置里把代理开得好好的,浏览器访问Google那是嗖嗖的快,一点毛病没有。可是一切换到CMD或者PowerShell里,立马就瞎火,curl 一下超时,wget 一下拒绝连接。
他当时那个郁闷啊,跟我抱怨说:“这Windows是不是针对我?设置里都配好了,怎么到了命令行就成了聋子的耳朵——摆设?”
其实吧,这事儿真不能怪Windows针对谁,这是很多刚入坑运维或者开发的小伙伴经常遇到的经典“玄学”问题。咱们平时在图形界面点点点,习惯了那种“所见即所得”的爽快,殊不知Windows底层的网络机制其实是个“双轨制”。
今天咱们就趁着这个机会,好好扒一扒这里面的门道。别看这是个代理问题,里面涉及的底层逻辑,如果你没搞清楚,以后遇到更复杂的网络故障,一样得抓瞎。
浏览器能通,为啥命令行不通?
咱们得先把这个事儿理清楚。很多人有个误区,觉得我在“设置 -> 网络和 Internet -> 代理”里面填了个地址,这电脑上所有的流量就应该乖乖地走那个通道出去。
其实不是的。
Windows这系统,历史悠久,身上背着不少历史包袱。它的网络架构里,其实并存着两套主要的机制。一套是咱们老百姓常用的,叫 WinINET 代理设置;另一套,是更底层、更“极客”一点的 WinHTTP。
你在Windows设置界面里配的那个代理,它实际上修改的是 WinINET 的配置。这玩意儿是给谁用的呢?主要是给浏览器用的,给那些跑在用户态的应用程序用的。比如你打开Edge、Chrome,或者是一些现代化的桌面软件,它们启动的时候,会自动去读WinINET的配置,“哦,原来用户设了代理,那我把请求发给代理服务器吧”。所以你的浏览器能通,是因为它听话,它主动去适配了这个配置。
但是,问题来了。CMD和PowerShell,这俩货虽然长得黑乎乎的挺像黑客工具,但它们本质上只是命令行解释器。当你在这个黑框框里敲下 curl 或者 Invoke-WebRequest 的时候,它们默认是不会去读那个WinINET的配置的。
这就好比你跟家里的管家(浏览器)说:“以后有人找我,先经过你筛选一下。” 管家听懂了,照做了。但是你忘了跟那个负责跑腿的快递员(CMD/PowerShell)说这事儿。快递员还是按照老规矩,直接去敲客户的门,结果客户(目标网站)在国外,他哪能敲得开?直接就被大防火墙给拦回来了。
所以,CMD和PowerShell默认情况下,它是“直连”模式。它不知道你设了代理,它也不想管这闲事,它就觉得:“我有网关,我有DNS,我直接发请求不就完了吗?” 结果就是,请求发出去,石沉大海。
那个让人误解的 curl 命令
这里还得插一句,特别容易误导人的一个点。
现在的Windows 10、Windows 11,居然自带 curl 命令了。以前咱们在Windows上用curl,那都是得自己去下载个exe,扔到System32里,或者配个环境变量。现在好了,微软良心发现,原生支持了。
但是!这个坑就在于,PowerShell里有个别名机制。
你在PowerShell里敲 curl,你以为你用的是那个大名鼎鼎的Linux下的curl工具吗?并不是。在PowerShell里,curl 其实是 Invoke-WebRequest 这个Cmdlet的别名。这就导致了一个很搞笑的现象:你在网上搜教程,人家Linux下是 curl -x socks5://...,你原封不动复制到PowerShell里跑,报错!参数不对!
因为 Invoke-WebRequest 它是.NET Framework封装出来的东西,它的逻辑跟原生的curl.exe完全不是一码事。
你要是真想在PowerShell里用原生的curl,得敲 curl.exe,或者把别名去掉。这一步搞不清楚,很多人配代理能配一天,最后把自己配懵了。
我之前有个同事,就是死活搞不定这个,还在群里骂微软垃圾,说curl都不兼容。后来我让他敲个 Get-Alias curl 看一眼,他才恍然大悟,原来是“李鬼”遇上了“李逵”。
既然默认不走,咱们怎么让它走?
知道了原因,解决办法其实就呼之欲出了。既然CMD和PowerShell不会主动去读WinINET的配置,那咱们就得想办法把代理配置“喂”到它们嘴里。
一般来说,有三种路子,咱们一种一种掰开了说。
方法一:环境变量大法,简单粗暴
这是最通用、最省事的办法。不管是CMD还是PowerShell,不管是curl还是git,绝大多数命令行工具,都会去读系统的环境变量。特别是 HTTP_PROXY 和 HTTPS_PROXY 这两个变量。
咱们可以在当前的终端窗口里,临时设置一下。
如果你是CMD:
set HTTP_PROXY=http://127.0.0.1:7890
set HTTPS_PROXY=http://127.0.0.1:7890如果你是PowerShell:
$env:HTTP_PROXY = "http://127.0.0.1:7890"
$env:HTTPS_PROXY = "http://127.0.0.1:7890"这里的 127.0.0.1:7890 是你代理软件监听的地址和端口,这个每个人用的软件不一样,端口也不一样,得自己去看一眼。
设置完这一步,你再试试敲命令。你会发现,原本不通的请求,现在“刷”的一下就通了。
为啥?因为当你在终端里设了环境变量,当前这个窗口里启动的所有子进程,都能看到这两个变量。像Python的pip、Node的npm、Git、curl这些工具,它们在发起请求前,都会先去看看有没有这几个变量。如果有,它们就会乖乖地把请求发到这个代理地址去。
这个方法的好处是“即插即用”,坏处是“一次性”的。你把这个窗口关了,下次再打开,这设置就没了。如果你想永久生效,那就得去系统环境变量里,把这俩变量加进去。但我一般不建议这么做,因为有些时候,你访问内网系统的时候,如果设了全局代理,反而会报错,还得回来删,挺麻烦的。
方法二:命令行参数,精准打击
如果你不想设环境变量,或者只是偶尔需要走代理,那可以在命令里直接带参数。
如果你用的是PowerShell的 Invoke-WebRequest(也就是那个 curl 别名),它有个 -Proxy 参数:
Invoke-WebRequest -Uri "https://www.google.com" -Proxy "http://127.0.0.1:7890"如果你用的是原生的 curl.exe,那就更熟悉了,跟Linux下一样:
curl.exe -x "http://127.0.0.1:7890" https://www.google.com这种办法虽然稍微繁琐点,每次都要敲一长串地址,但是它胜在精准。不会影响其他的命令,也不会污染环境变量。对于那种只需要测试一下连通性的场景,这招最管用。
方法三:netsh winhttp,系统层面的“硬核”设置
刚才咱们说了,CMD和PowerShell不走系统代理,是因为它们没读WinINET。那有没有办法让它们读呢?或者说,有没有一种更底层的代理设置?
这就得请出 netsh 这个神器了。Windows有一个叫 WinHTTP 的底层组件,很多系统服务(比如Windows Update)和命令行工具,其实是看这个组件的脸色的。
我们可以通过管理员权限的CMD或PowerShell,执行下面这条命令:
netsh winhttp set proxy 127.0.0.1:7890这命令一敲,相当于给系统的底层网络栈打了个补丁。你可以理解为,你在系统的大门口直接安了个安检机,不管是谁,只要是从WinHTTP这块出门的,统统都得走这个代理。
设完之后,你可以敲 netsh winhttp show proxy 看一眼状态。
但是,这个方法有个坑。有些老旧的程序,或者那些写死了直连逻辑的程序,它可能还是不走代理。而且,这个设置是全局的,如果你哪天把代理软件关了,忘了把这个设置撤回来,那你这台机器可能就彻底上不了网了,甚至连局域网都可能访问不了。因为它会死板地把所有请求都往那个已经关门的代理地址发。
取消设置的命令是:netsh winhttp reset proxy。这个命令得记好了,关键时刻能救命。
别忘了那个“Ping”的坑
说到这儿,我还得提一个特别容易让人产生自我怀疑的事儿。
很多时候,咱们配完代理,习惯性地想测试一下,于是敲了个 ping www.google.com。
结果呢?大概率还是不通。
这时候你心态可能就崩了:“我都设了环境变量了,我也设了netsh了,怎么还是不通?是不是代理软件坏了?”
其实不是。这里有个非常底层的网络知识:Ping命令走的是ICMP协议,而绝大多数的代理软件,走的是HTTP/HTTPS或者SOCKS协议。
代理,说白了,是应用层的事儿。你把你的HTTP请求封装一下,发给代理服务器,代理服务器帮你去跟目标服务器握手,再把数据拿回来给你。
而Ping,它是网络层的东西,它发的是ICMP包。这个包没法被封装成HTTP请求。所以,你配了代理,Ping不通是正常的。Ping它根本就不支持代理!它就像是一辆不收费的救护车,不走普通车道(代理通道),它有自己的专用道,但可惜,防火墙把这条专用道给堵死了。
所以,测试代理有没有通,千万别用Ping。得用 curl,或者直接用浏览器访问,或者用 Test-NetConnection (PowerShell自带的高级命令,可以指定端口)。
比如:
Test-NetConnection www.google.com -Port 443这个命令如果能通,说明TCP连接是通的,代理大概率是没问题的。
实战中的那些“妖魔鬼怪”
在实际的生产环境里,情况比这还要复杂。
有时候你会发现,明明环境变量设了,curl也能通了,但是用 git clone 拉代码的时候,还是报错 Failed to connect to github.com port 443。
这时候你得琢磨琢磨,Git这货是不是有它自己的小九九?
确实,Git很多时候不走系统环境变量,它有自己的配置命令:
git config --global http.proxy http://127.0.0.1:7890你看,这就是Windows生态的“百花齐放”(或者说“群魔乱舞”)。每个工具都有自己的脾气,都有自己的配置方式。NPM有 .npmrc,Pip有 pip.conf,Docker还得去改daemon.json。
这就是为什么很多运维小伙伴觉得心累的原因。你以为你掌握了原理,结果换个工具,它又不按套路出牌。
但万变不离其宗。只要你搞清楚了“WinINET是给浏览器用的”、“环境变量是给大多数CLI工具用的”、“WinHTTP是给系统服务用的”这三点,遇到问题,你就知道该往哪个方向去排查了。
比如,你在CMD里跑不通,先看环境变量;环境变量设了还不通,看是不是工具本身有配置文件覆盖了环境变量;再不行,抓包看看。
抓包也是个好习惯。装个Wireshark,或者用微软自带的 netsh trace start 抓个包看看。你会发现,当你没设代理的时候,你的机器在疯狂地往目标IP发SYN包,然后收不到ACK;当你设了环境变量后,你的机器开始往 127.0.0.1:7890 发数据了,这就说明代理配置生效了。
总结一下
今天聊得有点碎,主要是想把Windows这个代理机制给揉碎了讲清楚。咱们最后再捋一捋:
- 系统设置的代理是给浏览器用的,那是WinINET,CMD和PowerShell这俩“老实人”默认是不认的。
- 想让命令行走代理,最简单的办法是设环境变量
HTTP_PROXY和HTTPS_PROXY。 - Ping不通别慌,那是ICMP协议,跟HTTP代理不是一路人。
- 工具特例,像Git、NPM这种,有时候得单独配,环境变量不一定好使。
- netsh winhttp 是把双刃剑,能解决很多系统级代理问题,但也容易把网搞挂,慎用。
运维这活儿,很多时候就是在跟这些“默认行为”做斗争。系统觉得默认这样最安全、最快,但咱们实际场景里,往往需要咱们手动去干预。把这些细节搞懂了,以后再遇到这种“浏览器能通,命令行不通”的玄学问题,你就能淡定地端起杯子,喝口水,然后轻蔑一笑:“小样,又是环境变量没设对吧。”
希望这篇文章能帮到像小王那样在深夜里抓狂的小伙伴。如果你觉得这篇文章帮你理清了思路,哪怕只是让你少走了一点点弯路,那我这大半夜敲的这几千字也就值了。
路漫漫其修远兮,运维的路上坑多,但咱们填坑的过程,不就是成长的足迹嘛。
公众号:运维躬行录
个人博客:躬行笔记
如果你觉得这篇文章对你有帮助,或者想看更多这种接地气的运维实战干货,别忘了点个关注,顺手转发给身边同样在“踩坑”的兄弟们。咱们下期见,在这个充满Bug的世界里,咱们一起“躬行”实践,寻找真理!