VPN 原理以及示例

2022-11-27 ⏳2.6分钟(1.0千字)

该篇会从服务端以及客户端(iOS & Android)的角度来实践下 VPN 的工作原理.

准备工作

我写该篇的时候已经假设读者有如下知识:

  1. 理解 TCP 并能轻松编程
  2. 理解 TUN/TAP 设备
  3. 对 iOS 开发有了解, 对 NetworkExtension 有一定了解.

VPN 介绍

假设读者知道 OSI 七层模型或者tcp 和udp 五层模型

vpn 工作在网络层数据链路层, 对应的实现分别为 tun 设备tap 设备, 这是跟 proxy (工作在应用层) 不一样的地方. vpn 只负责把流量扔到另一块网卡, 对传输层 tcp/udp 流量不敏感.

代理大家应该都比较能容易理解, 比如 A 需要发一个 http 请求, 委托给了 B, 然后 B 请求后把相应返回给 A, 形成闭环.

而 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, 相当于包了一层.

server 端示例

注意在 macOS 下跟 linux 的命令有些区别, 暂不赘述, 示例代码可以在两个系统下运行, 但是会一些区别

github 代码示例

开启流量转发

流量转发起到了什么作用?

假设服务端创建一个 tun 设备 utun0, 我们这时需要把 utun0 的流量通过 en0 转发出去, 然后回来的流量从 en0 回到 utun0, 即这里的转发是指 utun0 和 en0 的操作. (注意, 这里假设 en0 是你的主机出口网卡, utun0 是你拉起来的 tun 设备, 它在不同的操作系统上可能是其他名字.)

详细内容可以看这里

#!/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
# 开启流量转发
sudo sysctl -w net.inet.ip.forwarding=1
iptables -t nat -A POSTROUTING -j MASQUERADE

代码示例

  1. 首先我们需要创建一个 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 interfaceutun3

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 设备, 到此我们把系统准备工作基本做齐了.

  1. 我们现在还有一个比较关键的问题,如何把 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)
}
  1. 把取出来的 packet 吐给客户端

其实这里就更简单了, 我们可以通过 TCP/UDP 协议吐给客户端, 这里用 tcp 作为示例.

// 这里就是简单创建一个 tcp 连接
ln, err := net.ListenTCP("tcp", laddr)

client, err := ln.AcceptTCP()

Client

这里以 iOS 作为示例, macOS 客户端与之类似, Android 大家可以看源代码, 思路类似, 只有简单的 api 使用的问题.

准备工作

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 客户端进行详细的讲解.