为什么来宾虚拟机里的 PAC 代理用宿主机的 URL 会失败
PAC URL 指向的是一段策略脚本,而不是代理。来宾必须先取到这段脚本,用它自己的地址完成计算,然后才能连接到脚本选出的代理。
「宿主机用这个 PAC 文件,所以我把同一个 URL 也填进了虚拟机」是合理的一句话,却是一个不完整的方案。PAC URL 指向的是一段策略脚本,而不是代理。在来宾既取不到这段脚本、也无法用它自己的地址完成计算、更无法连上脚本指定的任何代理之前,这个 URL 只是设置框里的一串字符。虚拟机里关于代理策略的大部分困惑,都来自把这一行当成了全部配置。
PAC URL 是脚本,不是代理
文件里有一个函数,它接收一个 URL 和一个主机名,返回一段代理描述或者 DIRECT 这个词。URL 里没有任何东西告诉应用该把流量发往哪里;这个决定发生在运行时,在脚本内部,用它向平台索取的数据完成。一个接受 PAC URL 的设置对话框,接受的其实是一个指向决策过程的指针。
正是这个区别,让复制 URL 看起来像是成功了。字段被填上了,设置被保存了,而故障要更晚才出现:脚本取不到,或者脚本返回的代理来宾连不上。
四个步骤各有各的失败方式
在来宾内部,四件事必须按顺序发生,而每一件都有自己的错误特征。来宾取回 PAC 文件。应用用本地信息为所请求的 URL 计算这份脚本。选出的代理被解析并连上。如果代理要求认证,就进行认证。
先证明取回这一步,因为没有它,后面的一切都没有意义。私有服务器上的 PAC 文件,在任何规则能起作用之前,都需要一条通往该服务器的允许路径;一段连不上取不回的脚本,其内部选出的代理也造不出那条路径:
curl -sS -o /dev/null -w '%{http_code}\n' http://pac.corp.example/proxy.pac
getent hosts pac.corp.example
如果这个请求从来宾那里失败、从宿主机那里成功,问题就在来宾通往 PAC 服务器的路径上,而不是脚本内容。如果它返回的是 JavaScript 文件,就进入第二步,弄清楚脚本针对来宾的处境会做出什么决定。
同一段脚本在虚拟机内部会做出不同的判断
PAC 函数可以检查被请求的主机名,以及执行计算的这台机器的信息。建立在客户端自身地址上的规则,例如检查某个接口地址或某个子网归属,看到的将是来宾的地址,而不是物理宿主机的地址。在宿主机一侧的 NAT 之后,这两者是不同的地址,因此同一段脚本可能在虚拟机内部选出另一个代理,或者回退到 DIRECT。
这不是脚本的缺陷。脚本只是在用不同的输入,做它本来被写来做的事;这也解释了那个令人困惑的现象:宿主机走代理,来宾却直连。与其推断,不如复现这个决定:一个接收地址和 URL 的 PAC 评估器,如果你的发行版里就有,一条命令就能给出答案。否则就用真正会投入使用的应用去测,读它的代理日志,而不是靠猜。
不要因为来宾继承了宿主机的代理设置就下结论,毕竟它的流量只是经过了宿主机一侧的地址转换。网络地址转换转发的只是数据包,并不会复制应用配置,而系统代理本身就是应用配置。
根本不读 PAC 的工具
命令行工具是另一道边界,很多时间就丢在这里。curl 不会执行 PAC 文件,它的代理文档说得很清楚。把代理变量指向 PAC 的下载 URL,并不会让这段脚本变成代理端点;工具会试着用 HTTP 去跟一个 JavaScript 文件对话,然后以一种看起来像网络故障的方式失败。
许多包管理器、语言运行时和构建工具也一样,它们只认显式的代理变量或自己的配置。如果你在意的测试是命令行工具,就用 PAC 脚本本会选出的那个具体代理去配置它,并记下你是怎样得出这个选择的,以便下一个人重复。
对于 Linux 上的桌面应用,系统设置必须切到自动模式并且给出 URL;模式还停在手动时保存的 URL 什么也不会做。两者都要检查,而且要在来宾内部检查:
gsettings get org.gnome.system.proxy mode
gsettings get org.gnome.system.proxy autoconfig-url
DIRECT 也是一种决定
PAC 结果为 DIRECT,意味着应用绕过了 HTTP 代理,而不是它逃出了网络。由此建立的连接仍可能走操作系统经过隧道的路由,也可能因为与代理毫无关系的原因失败。反过来,代理结果描述的只是某一个应用的请求,而不是来宾里的每一个进程。
所以要沿着需求写下的方向去验证。如果需求是「这个应用能访问内部站点」,就证明这个应用确实能访问,并记下是哪条代理或哪条路由承载了它。如果需求是「这台虚拟机在公司网络上」,那么一个通过所选代理正常工作的浏览器,只能证明那个浏览器,别的都证明不了。
去修来宾内部的链条,而不是为了通过测试就关掉宿主机的隧道或者放宽转发策略。这条链有四环,跳过其中一环的改动,只能撑到脚本下次更新或者来宾地址发生变化为止。