VPN 原理以及示例
曦子该篇会从服务端以及客户端(iOS & Android)的角度来实践下 VPN 的工作原理.
准备工作
我写该篇的时候已经假设读者有如下知识:
- 理解 TCP 并能轻松编程
- 理解 TUN/TAP 设备
- 对 iOS 开发有了解, 对 NetworkExtension 有一定了解.
VPN 介绍
这里假设读者知道 OSI 七层模型
或者tcp 和udp 五层模型
vpn 工作在网络层
和数据链路层
, 对应的实现分别为 tun 设备
和 tap 设备
, 这是跟 proxy (工作在应用层) 不一样的地方. vpn 只负责把流量扔到另一块网卡, 对传输层 tcp/udp 流量不敏感.
代理大家应该都比较能容易理解, 比如 A 需要发一个 http 请求, 委托给了 B, 然后 B 请求后把相应返回给 A, 形成闭环.
而 vpn 大家可以想象有一根虚拟的网线把客户端跟服务端连接上了. 有同学给我发私信不太理解为什么没有影响, 大家可以理解应用层的数据相当于你要发快递的物品, 到了网络层已经被快递打包成了盒子, 盒子上有收货地址和发货地址, vpn 相当于把这个盒子发给另外一个快递公司,让他帮忙代发快递。
+---------------+ +---------------+
| | | |
| | | |
+-------+-------+ +-------^-------+
| |
+-------v-------+ works here +-------+-------+
|Network Layer <---------------+Network Layer |
|Tun Interface +--------------->Tun Interface |
+-------+-------+ +-------^-------+
| |
+-------v-------+ +-------+-------+
|Data Link Layer| or here |Data Link Layer|
|Tap Interface | |Tap Interface |
+-------+-------+ +-------^-------+
| |
+-------v-------+ +-------+-------+
|Physical Layer | |Physical Layer |
| | | |
+-------+-------+ +-------^-------+
| |
+-------------------------------+
思路
应用层协议是复杂多样的, 如果我们能接管所有的 tcp/udp 流量那岂不是就可以实现游戏加速或者流量的转发?
关键的点在于我们如何把 L3 (本文 L3 指网络层, L2 指数据量路层, L1 是物理层) 的内容(网络分组: packet )扔到另外一个网卡上,
假如操作系统提供 api 让我们接管 L3 的数据, 我们从 client 取出来通过其他通信方式(一般是另一个 tcp/udp 传输随便什么协议)放到服务端的网卡 L3 上, 然后把相应的数据从 server 端的 L3
取出来再放回 client 的 L3 上是不是就可以了呢?
当然可以, 其实这个方法就相当于用网线连接了两个网卡的网口, 区别在于网线连接的时候经过 L3 -> L1 层层降解就是原始数据, 而我们的内容是 L3 的完整 packet, 相当于包了一层.
说白了, 我们要想办法把 L3 的数据从 A 网卡扔到 server 端的 B 网卡里.
server 端示例
注意在 macOS 下跟 linux 的命令有些区别, 暂不赘述, 示例代码可以在类 linux 系统下运行, 但是会一些配置上的区别.
开启流量转发
流量转发起到了什么作用?
假设服务端创建一个 tun 设备 utun0, 我们这时需要把 utun0 的流量通过 en0 转发出去, 然后回来的流量从 en0 回到 utun0, 即这里的转发是指 utun0 和 en0 的操作. (注意, 这里假设 en0 是你的主机出口网卡, utun0 是你拉起来的 tun 设备即虚拟网卡, 它在不同的操作系统上可能是其他名字.)
详细内容可以看这里
- macOS
#!/bin/bash
cat > /usr/local/etc/pf-nat.conf << EOF
nat on en0 from utun3:network to any -> (en0)
EOF
sudo pfctl -d
sudo sysctl -w net.inet.ip.forwarding=1
sudo pfctl -f /usr/local/etc/pf-nat.conf -e
- Linux
# 开启流量转发
sudo sysctl -w net.inet.ip.forwarding=1
iptables -t nat -A POSTROUTING -j MASQUERADE
代码示例
- 首先我们需要创建一个 utun 设备, 比如示例中的
go
代码
// 这里我们创建一个 TUN interface
tuncfg := water.Config{
DeviceType: water.TUN,
}
tun, err = water.New(tuncfg)
// 给 tun 设备配置 mtu 和点对点 ip
args := []string{tun.Name(), hostIP.String(), "pointopoint", clentIP.String(), "up", "mtu", "1500"}
if runtime.GOOS == "darwin" {
args = []string{tun.Name(), hostIP.String(), clentIP.String(), "up", "mtu", "1500"}
}
if err = exec.Command("/sbin/ifconfig", args...).Run(); err != nil {
return err
}
我们在 bash
使用 ifconfig
命令查看后会发现出现一个心的 tun interface
即 utun3
utun2: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1000
inet6 fe80::ce81:b1c:bd2c:69e%utun2 prefixlen 64 scopeid 0xa
nd6 options=201<PERFORMNUD,DAD>
utun3: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1500
inet 10.0.0.1 --> 10.0.0.2 netmask 0xff000000
可以看到我们拉起了一个点对点的 tun 设备, 到此我们把系统准备工作基本做齐了.
- 我们现在还有一个比较关键的问题,如何把 tun 设备里网络层的 packet 取出来.
注意因为流量是源源不断的我们要循环从 tun 设备里读取 packet, 只要有就去读.
// 此处大小应该不大于协商的 mtu 这里默认用的1500
var packets = make(packet.Packet, 65535)
var headerBuf = make([]byte, 4)
for {
packets.Resize(65535)
// 这里从上面创建的 tun 设备里读取网络层(L3)的 packet
n, err := tun.Read(packets)
}
- 把取出来的 packet 吐给客户端
其实这里就更简单了, 我们可以通过 TCP/UDP 协议吐给客户端, 这里用 tcp 作为示例.
// 这里就是简单创建一个 tcp 连接
ln, err := net.ListenTCP("tcp", laddr)
client, err := ln.AcceptTCP()
Client
这里以 iOS 作为示例, macOS 客户端与之类似, Android 大家可以看源代码, 思路类似, 只有简单的 api 使用的问题.
- XVPN:iOS
- XVPN-Android: Android
- macvpn:macOS
准备工作
iOS 的同学需要调通客户端需要付费的开发者账号, 用到的系统库 PacketTunnelProvider
需要一些权限.
相关 api 可以参考 PacketTunnelProvider
代码相关
我们需要 NETunnelProviderManager 来配置 vpn 客户端
lazy var vpnManager: NETunnelProviderManager = {
let manager = NETunnelProviderManager()
let providerProtocol = NETunnelProviderProtocol()
providerProtocol.providerBundleIdentifier = self.tunnelBundleId
providerProtocol.serverAddress = "47.101.191.9"
manager.protocolConfiguration = providerProtocol
manager.localizedDescription = "VPN"
return manager
}()
同样的作为客户端需要按照服务端的协议建立通信, 我们使用的是 TCP:
endpoint = NWHostEndpoint(hostname:server, port: port)
从 tcp 中读取 packet 塞到 iOS 设备的网卡里.
self.tcpConn.readLength(Int(count), completionHandler: { (pdata: Data?, error: Error?) in
guard error == nil, let packet = pdata else {
self.stopVpn()
self.tcpConn?.cancel()
return
}
let protocols = [NSNumber.init(value: AF_INET)]
let res = self.packetFlow.writePackets([packet], withProtocols: protocols)
if res == false {
NSLog("write tun failed")
}
// 同样 我们也是需要反复做该操作
self.tcpToTun()
})
总结
可以看出来 VPN 的工作原理是很简单的, 考虑到很多同学对 iOS 编程并不是很了解, 后面可能会开篇介绍下 NetworkExtension 并对 iOS 客户端进行详细的讲解.