手动VPN
手动模拟实现VPN
本项目旨在模拟VPN效果的一种实现,分为客户端和服务端两个程序,客户端程序是一个安卓APP,用Android Studio作为开发IDE,服务端程序是一个运行于Linux系统上的C语言程序,用Clion作为开发IDE。
了解Android Studio,就不得不了解Ktolin,早年间安卓开发虽然用的是Java,但是现在已经流行用Kotlin,``Ktolin`是一种有别于Java语法的基于JVM的编程语言,它的官网:Kotlin Programming Language (kotlinlang.org),可从此官网页面Kotlin command-line compiler | Kotlin Documentation (kotlinlang.org)获取命令行编译器,下载解压后如下

bin目录包含kotlinc脚本,此脚本可以用于编译kotlin程序,可将此路径添加到PATH环境变量

直接运行kotlinc会进入一个交互式环境,从输出中可知,kotlin编译器依赖于jvm,可以推断,kotlin编译器是用java或其它能编译成字节码的语言写的

也可以将代码从源文件编译执行

另一个需要了解的东西是Gradle,一个Java项目构建管理工具,不同于Java Web开发的Maven,安卓开发一般用它管理项目
Android Studio能从虚拟机运行构建后的APP,也能USB连接真机运行,真机需要开启开发者模式,并且允许USB调试,此外,还需确保电脑防火墙没有阻止adb通信,否则都会造成检测不到真机的情况。bulid菜单提供了项目构建相关操作,构建输出一般在app/build/outputs目录下
安卓的VpnService类提供了在安卓系统上建立vpn的能力,新建一个继承此类的类后,核心代码如下所示
1 | private fun setupVpn() { |
为了防止VPN程序自身的网络流量被路由,可以使用如下代码
1 | val connectivityManager:ConnectivityManager =getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager |
拿到vpnInterface后,可以用它获取输入输出流,输入流每次读取到的是一个IP数据包。需要注意的是,IP包的源地址会被替换成配置的IP地址,如上例10.0.0.2,写入IP包时,目的地址保持一致即可。接下来的逻辑就是,不断从输入流读取IP数据包,然后不断从输出流写入IP数据包,实现网络联通。至于读出来的IP包怎么用,完全由VPN客户端程序自己决定。
通常的处理方式是,将IP包通过某种方式发送到另一台服务器进程上,这种方式可以是使用原始的tcp字节流配合自定义的的应用层协议,也可以是采用ws这种不需要处理消息边界的协议,而且一般是加密通信,在数据传输开始之前,一般会协商此次连接客户端的虚拟IP。远端进程收到IP包后,通过某种方式写入到某个网络接口,并且监听这个网络接口关于写入的数据包的回包,读取并且发送回本地进程,本地进程收到数据包后,再写入它的vpn输出流
上述内容相关参考资料:
Linux系统上可以使用TUN/TAP机制创建一个虚拟网络接口,TUN设备工作在网络层,处理的是IP包数据,TAP设备工作在链路层,那么可以处理以太网帧数据。写入虚拟网络接口的数据也会被操作系统协议栈处理,当开启了ip转发后,可以被路由到其他真实网络接口,从而将数据包发送到真实网络上。为了能接收回包以实现双向通信,一般还要搞一个源地址转换的NAT,可以通过iptables实现。
C语言创建tun设备的核心代码如下,使用icotl系统调用在程序运行期间生成tun设备,并进行相关配置
1 | int create_tun() { |
iptables配置
1 | iptables -t nat -A POSTROUTING -o ens33 -s 10.0.0.0/24 -j MASQUERADE #开启网络地址转换 |
此命令也可以用来查看路由表,而且非常直观形象
1 | netstat -rn |
查看ip转发状态,0表示关闭,1表示启用
1 | cat /proc/sys/net/ipv4/ip_forward |
永久开启,编辑此配置文件
1 | nano /etc/sysctl/conf |
添加
1 | net.ipv4.ip_forward=1 |
应用更改
1 | sysctl -p |
最好重启确保一定生效
Linux系统关于TUN/TAP的虚拟网卡技术,相关参考资料
在C语言中,获取了tun设备的文件描述符后,就可以用read和write系统调用读取和写入数据了
在Windows系统上,可以使用Hyper-V创建虚拟网络接口,Hyper-V是微软提供的一个内置虚拟化工具,使用流程如下
启用 Hyper-V 功能
- 打开 “控制面板” > “程序” > “启用或关闭 Windows 功能”。
- 勾选 “Hyper-V” 选项,然后点击 “确定”。
- 系统会安装必要的组件并提示重启。
创建虚拟交换机
- 打开 “Hyper-V 管理器”。
- 在右侧的操作栏中,点击 “虚拟交换机管理器”。
- 选择 “新建虚拟网络交换机”。
- 选择 “内部” 作为交换机类型,然后点击 “创建虚拟交换机”。
- 为交换机命名,并确保选择了 “内部网络” 选项。
- 点击 “确定” 保存设置。
为虚拟交换机配置 NAT
打开 PowerShell,使用管理员权限运行。
创建一个新的 NAT 网络:
1
New-NetIPAddress -IPAddress 192.168.100.1 -PrefixLength 24 -InterfaceAlias "vEthernet (你的交换机名称)"
设置 NAT 配置:
1
New-NetNat -Name "MyNAT" -InternalIPInterfaceAddressPrefix "192.168.100.0/24"
上述流程完毕后,你会看到系统多了一个虚拟网络接口,并且ip地址是192.168.100.1,此虚拟网络接口能处理以太网帧数据,并且具有NAT功能,虚拟网络的网关就是本机,凡是发到本机此网络接口MAC地址的IP包都会经过NAT处理以与外部网络通信。但是要想在此网络接收到回包,还需要一个还需要有一个虚拟的ARP行为,以便网关根据ip找到mac地址
为方便,Windows系统上服务端程序的实现可以用Python语言配合scapy+websockets库,scapy是用于向网络接口发送和监听网络数据包的库,也可以解析原始二进制包数据。websockets是用于创建ws服务器的库。
顺便提一下,关于类似scapy的库Java有pcap4j,但用起来不如scapy方便,更主要的是,它貌似不能自动计算IP包头部和TCP包头部的校验和。关于tcp的checksum问题,需要注意的是,当网络地址转换后要相应地重新计算checksum,否则不正确的checksum的包会被丢弃,这点非常重要,尤其是当你在程序中要手动实现NAT时,关于如何计算checksum,见:http://bruce690813.blogspot.com/2017/09/tcpip-checksum.html,Java代码如下
1 | public static int calculateChecksum(byte[] header) { |
TCP包是一定需要校验和的,包括下层的IP包也要有。
关于校验和通过抓包发现一个神奇现象
linux系统发的tcp包,ip头和tcp头的校验和都有,且计算方法就是上述的计算方法,这种是可以通过验证的
windows系统发的tcp包,ip头校验和可能为0,tcp头校验和有,但是不是通过上述的计算方法计算的(如果按上述计算方法计算,则通不过验证),不知道是怎么计算的,但是也能通过验证
这里所说的通过验证是指tcp的syn报文能收到对端的syn/ack回应
你可能会想,既然都有直接往网络接口发送和接收数据包的库了?为什么还要额外创建虚拟网络接口,而不是直接将其发送到真实的网络接口呢?这主要是为了避免手动处理NAT问题,手动NAT非常复杂(对于我这种菜鸟而言)且性能、稳定性、可靠性都不好,我在之前的尝试中把NAT想得太简单了,以为只是简单地替换ip和端口,实践才知道,有各种问题,所以不得不采用现成的能避免我解决NAT的方案。
