HTTP 代理、SSH 隧道还是 VPN:选对层级
代理、SSH 隧道和 VPN 承载的流量各不相同。按任务所需的协议与域名解析来匹配层级,然后验证路径。
问三位工程师如何访问内部服务,你会得到三种设计:Web 代理、SSH 隧道或完整的 VPN。三者都能让远程服务触手可及,也都能让流量看起来来自别处。差异要到后面才显现,当实际使用的协议并不是所选工具假设的那一种。一个能通过浏览器快速检查的方案,常常在促成这次请求的那个应用上失败。
凭两个事实来决定:哪些协议必须穿过,以及谁能解析涉及的名字。其余都是偏好。
代理只搬运它理解的东西
Web 代理会解析它为自身而构建的协议。客户端交给它一个请求,它把请求继续转交,所以没有代理设置的应用程序根本不参与。对 HTTPS,客户端需要一条不透明的通道,代理通过 CONNECT 提供它,而不是读取请求。
SSH 动态转发会打开一个说 SOCKS 的本地端口,该端口把 TCP 流转发到客户端指定的任何目的地。建立在 TCP 之上的协议通常能通过它工作。UDP 不行,除非某个工具显式加了支持。本地转发更窄:一个本地端口映射到一个固定主机和端口,只服务一个已知服务,仅此而已。
ssh -D 1080 user@bastion
ssh -L 8443:fileserver.internal:443 user@bastionVPN 搬运的是数据包。客户端路由进隧道的一切都会穿过它,包括 UDP 和 ICMP,所以它适合的是网络层面的需求,而不是单个连接。
总得有人解析名字
当目标名字只存在于远端网络的 DNS 里,执行查询的那台机器必须能看到那个区域。Web 代理自己做查询,所以即使笔记本电脑完全解析不了这个名字,内部名字也能经由它工作。SOCKS 也可以表现得一样,但 只有当客户端把名字传过去,而不是连到它已经解析好的地址时。这个选择在客户端,不在服务器。
curl 清楚展示了这种分裂:一个标志把主机名交给代理,另一个先在本地解析。本地解析时,仅内部可用的名字在任何隧道被触及之前就失败了,而错误却把责任推给代理解析,其实那是 DNS 的决定。
VPN 走第三条路,把私有解析器和搜索域与路由一起安装。这很方便,也制造了一个熟悉的错觉:一条路由存在而解析器不应答,看起来就像隧道死了,哪怕数据包正到达另一端。
当网络只允许一个出口
许多企业网络只允许通过经过认证的 Web 代理向外连接。OpenVPN 和 SSH 都能在那里跑起来,前者把自己的传输穿过代理,后者通过一条运行小型 CONNECT 辅助程序的代理命令。
建立在 TCP 上的隧道能扛住这个约束,而基于 UDP 的 VPN 不行,这解释了大量在家里能通、在办公室却失败的配置。第二项代价容易被忽略:一条被装在经过代理的 TCP 连接里的 TCP 隧道,现在有两层在重传,一旦丢包,这个组合就会以任何单层都不会出现的方式卡住。
把中间人串成链,与选择层级是不同的问题。增加第二个代理或 Tor 会改变谁观察到什么,而在链中的位置决定了哪一跳看到密文。
顺序比跳数更重要。外层跳看到的是加密流,内层跳看到的则是它承载的明文,所以一条链的私密程度取决于它最不小心的那一环。在各跳之间混用协议并不会自动改善这一点。它多半只是让故障模式更难推理。
伪造地址不在选项之内
用别人的源地址构造数据包毫无用处:回应会发往真正的所有者,而大多数网络会直接丢弃这些流量。中间人是必需的,恰恰因为源地址无法凭空捏造,这也是三种设计共同的诚实理由。
它也界定了三者各自能主张什么。看起来源自某个特定地址,问的是该地址在何处被合法分配、谁能路由它,而不是哪个工具能伪造报头。如果某个设计假设相反,这个假设会在第一个过滤它的运营商那里破产。把假设写下来,故障到来时才认得出来。
按协议和名字来选
当单个 Web 客户端需要单个 Web 服务且它有代理设置时,用代理。当几个已知的 TCP 服务就够、且已有 SSH 访问时,用 SSH 隧道。当多个应用、非 TCP 流量、私有 DNS 或可复用的路径属于需求的一部分时,用 VPN。
然后用真实客户端和真实名字去验证。通过代理加载一个页面,不会告诉你任何关于 UDP 的事;让一个工具跑通 SOCKS,也不会告诉你任何关于系统解析器的事。去读进程绑定了哪个本地地址、哪个解析器做了应答。这就是一条能用的路径和一条碰巧好用过的路径之间的差别。