VPN 和 代理 的工作原理

1. 导语

日常会遇到一些问题:

  • tunnelblick 和 surge一起使用的时候会不会冲突?
  • tunnelblick 的原理是怎样的?
  • surge的工作原理是怎样的?
  • 使用tunnelblick 时访问网站很慢,怎么解决?

2. 分析

  • tunnelblick 是openvpn的一个客户端,属于VPN
  • surge 是ss的客户端,属于代理
  • 所以问题就变成了:VPN 和 代理 的工作原理是怎样的?

3. 原理

3.1. VPN工作原理

3.1.1. 虚拟网卡

OpenVPN的运行原理其实很简单,其核心机制就是在OpenVPN服务器和客户端所在的计算机上都安装一个虚拟网卡(又称虚拟网络适配器),并获得一个对应的虚拟IP地址。OpenVPN的服务器和多个客户端就可以通过虚拟网卡,使用这些虚拟IP进行相互访问了。其中,OpenVPN服务器起到一个路由和控制的作用(相当于一个虚拟的路由器)。

在本地运行ifconfig可以看到所有的网卡

红圈中代表的意思是 utun3 这个虚拟网卡
IP是 172.31.69.66
网关是 172.31.69.66
子网掩码 0xffffff00

1

3.1.2. 路由表

2

进行网络访问的时候,如果确定该走哪个网卡呢?
通过本地的路由表来确定。
通过以下命令查看路由表:
netstat -r

3

3.2. 代理工作原理

4
以代理软件surge为例:

要想使 Surge 实现后续的转发、修改和截获等功能,首先需要 Surge 对网络连接进行接管。

在 macOS 和 iOS 下,要想使程序发出的网络连接被另一个程序所接管,而不是直接将数据发送到物理网卡,有以下三种方式:

配置代理

如果系统配置了代理服务器,那么程序在执行网络请求的时候,就不会直接连接目标服务器,而是产生一个发向代理服务器的连接。利用这个特性,可以在本地启动一个代理服务,并配置系统代理为 127.0.0.1 (即本机)的一个端口,这样就可以接管网络请求。

  • 但是,这种方式要求程序自身支持代理机制,系统的代理设置只是告知程序应该使用代理,需要程序自己完成代理的后续逻辑。好在,对于绝大部分带用户界面的程序,由于开发时使用了系统的高层网络框架(Cocoa/Cocoa Touch),开发者不需要进行任何额外的工作就可以支持代理。
  • 而对于命令行程序,由于使用的是 POSIX 接口进行网络请求,该接口并没有对代理服务器提供内嵌支持,所以需要开发者自己完成对代理服务器的支持,这导致各种命令行程序对代理的支持情况和具体行为并不统一。同时由于大部分命令行程序并没有为 macOS 进行特殊处理,所以不会理会系统配置里的代理服务器设置。大部分命令行程序需要通过环境变量 https_proxy 和 http_proxy 去配置代理,还有一部分需要通过修改配置文件进行配置。
  • 还有少量程序由于完全缺乏代理服务器的支持,无法通过这种方式去接管网络连接。

虚拟网卡(Virtual Network Interface,简写为 VIF)

主流操作系统几乎都存在 TUN 和 TAP 两种虚拟网卡接口,原本是为了提供对 VPN 的支持。通过在系统中建立虚拟网卡并配置全局路由表,可以接管所有的网络请求。

  • 这种方式对应程序来说是无感知的,所以并不需要程序主动提供支持,几乎所有程序都可以被这种方式接管网络请求。除非程序主动指定了物理网卡,绕过了默认的虚拟网卡。

Socket Filter

这是 macOS 的一项内核特性,可以通过注入一个 Kernel Extension(kext)对所有 socket 调用进行 hook,以此接管请求。

  • 除系统自身的一些程序外,这种方式可以强制接管系统中所有程序的所有网络请求。如 Proxifier 和 Little Snitch 就使用了这种方式接管网络。

三种方式对比

这三种方式各有优劣:

  1. 方法 1 性能最优,对系统侵入性最小,无奈有部分程序不支持。
  2. 方法 2 性能略低,因为截取到的流量是 IP 层的数据包,需要有一个 TCP 协议栈进行重组装,造成了额外的性能开销。
  3. 方式 3 最暴力,对系统侵入性高,Kernel Extension 有可能造成整个系统的不稳定,Apple 已确认在未来的 macOS 中将取消对 Socket Filter 的支持。

Surge 主要使用方法 1 接管网络请求。方法 2 作为补充,接管不支持代理的程序。

  • 对于 Surge iOS 版本,开启后会将自身注册为代理服务器,同时使用 Network Extension 接口建立了 TUN 虚拟网卡。
  • 对于 Surge Mac 版本,开启「设置为系统代理」选项会将自身注册为代理服务器(即方法 1),开启「增强模式」选项将会建立虚拟网卡(即方法 2)。

以上的说明针对的是 Surge 对本机程序的接管。当使用 Surge 接管另一个设备的网络请求时:

  • 由于 iOS 的系统限制,只能靠使用方法 1 作为代理服务器去接管另一个设备的请求。(修改目标设备的代理服务器设置)
  • Surge Mac 除了使用方法 1 外,也可以靠方法 2 接管另一个设备的请求。(修改目标设备的默认路由设置)

4. 附录

4.1. 子网掩码含义

5

举例说明:

192.168.1.0/26
等价于
192.168.1.0 netmask 0xffffffc0

代表

  • IP地址的 前26位固定的,后面6位不固定
  • 192.168.1.0 ~ 192.168.1.63

4.2. IP、子网掩码、网关作用?

  • 计算:
    • a: 源IP 和 子网掩码 做与运算
    • b: 目标IP 和 子网掩码 做与运算
  • 比较:
    • 如果 a 和 b 相同,说明属于同一网络,直接通信
    • 否则 转发给网关

举例说明:

例1:A电脑IP地址为192.168.1.1,子网掩码为255.255.255.0;B电脑IP地址为192.168.1.2,子网掩码为255.255.225.0。大家都知道这二台电脑在同一网段,相互能PING通。
例2:A电脑的IP地址为192.168.1.1,子网掩码为255.255.255.0;B电脑的IP地址为192.168.2.1,子网掩码为255.255.0.0。大家分析一下二台电脑能相互PING通吗?
分析:这个问题需要大家理解子网掩码在网络通讯时的作用。不能简单的认为A电脑处在192.168.1.0网段,B电脑处在192.168.0.0网段,所以不能PING通。正确的分析应该如下:
⑴ 每台电脑事先会把自己IP和自己的子网掩码进行“与”操作,得到自己的网段号,如A电脑处在192.168.1.0网段,B电脑处在192.168.0.0网段。
⑵ B电脑向A电脑发数据包时,会把A电脑的IP与B电脑的子网掩码进行“与”操作,得到网络号是192.168.0.0,B电脑会认为A电脑与自己在同一网段,所以数据包会顺利发出。
⑶ A电脑由于与B电脑在同一网段,肯定能收到B电脑发出的数据包,由于PING操作要求A电脑回应一个响应包。这样A电脑会把B电脑的IP与A电脑的子网掩码进行“与”操作,得到网络号192.168.2.0,A电脑发现网络号与自己所处的192.168.1.0不在同一网段,由于A电脑目前没有设置默认网关,所以对该数据包将进行丢弃操作,结果B电脑当然就无法收到A电脑的回应包,所以B电脑上会显示“Request timed out”,即网络超时。
⑷ 如果在A电脑上去PING B电脑,根据前面的分析,A电脑会认为B电脑与A电脑不在网段,而A电脑又没有设置默认网关,所以会显示“Destination host unreachable”,即目标主机不可达。

6

5. 参考

  1. -->符号的意思

  2. 多个网卡该用哪个?

  3. 查看路由表

  4. surge工作原理

  5. 理解子网掩码

  6. 子网掩码的作用

/** * RECOMMENDED CONFIGURATION VARIABLES: EDIT AND UNCOMMENT THE SECTION BELOW TO INSERT DYNAMIC VALUES FROM YOUR PLATFORM OR CMS. * LEARN WHY DEFINING THESE VARIABLES IS IMPORTANT: https://disqus.com/admin/universalcode/#configuration-variables*/ /* var disqus_config = function () { this.page.url = PAGE_URL; // Replace PAGE_URL with your page's canonical URL variable this.page.identifier = PAGE_IDENTIFIER; // Replace PAGE_IDENTIFIER with your page's unique identifier variable }; */ (function() { // DON'T EDIT BELOW THIS LINE var d = document, s = d.createElement('script'); s.src = 'https://chenzz.disqus.com/embed.js'; s.setAttribute('data-timestamp', +new Date()); (d.head || d.body).appendChild(s); })();