所谓专用网络(Private Network),本质上不过是物理上局域网概念的延伸,将分属两地的局域网络联通,构建一个更大的逻辑局域网,并保证通信的私密性和独占性。最质朴的物理解决方案——在两个局域网络之间拉一个网线;不过距离越远,成本越高,还得考虑信号衰减等问题。相对而言租用运营商的线路就比较实际。但终究物理上的解决方案相较于使用互联网还是过于高昂,如何通过互联网构建一个逻辑上的专用网络?虚拟专用网络(Virtual Private Network, VPN) 提供了答案。

要构建 VPN ,最基本的要求是两地局域网络互通。即如下图中的设备 172.16.1.1 要像在同一个局域网内,对内网 IP (分属不同的局域网)为 172.20.1.2 的设备发起并建立连接。但从基本的网络协议栈来看,IP 报文根本无法到达 B 区域的局域网。得益于分层的协议栈,TCP/IP 的五个层次,上下层次间通过接口和服务来通信,每个层次仅仅承载数据(payload)而不关心数据的格式和内容。理论上任何层次的数据都可以由其它任何层次或者它当前的层次所承载,也就出现了很多 XX over YY 的网络模型。在 A 区域将 IP 报文通过 YY 协议封装,到 B 区域再解包,像是从局域网内收到 IP 报文一样,发送给目的主机,就像是 IP 报文穿越了一条隧道。

+-------------------------------------------------------+
|                       A 地区                          |
| +------------+  +------------+       +--------------+ |
| | 172.16.1.1 |  | 172.16.1.2 |  .... | 172.16.39.60 | |
| +------+-----+  +------+-----+       +-------+------+ |
|        |               |                     |        |
|        +---------------+-----+---------------+        |
|                              |                        |
|                         +----+----+                   |
|                         |   NAT   |                   |
|                         +----^----+                   |
                               |
                               |
    +--------------------------v------------------+
    |    INTERNET                     INTERNET    |
    +--------------------------^------------------+
                               |
|                              |                        |
|                         +----v----+                   |
|                         |   NAT   |                   |
|                         +----+----+                   |
|                              |                        |
|        +---------------+-----+---------------+        |
|        |               |                     |        |
| +------------+  +------------+       +--------------+ |
| | 172.20.1.1 |  | 172.20.1.2 |  .... | 172.20.39.60 | |
| +------+-----+  +------+-----+       +-------+------+ |
|                       B 地区                          |
+-------------------------------------------------------+

XX over YY

网络协议栈分明的层次,为 XX over YY 建立了基础,可划分 3 个大类。

  • 最常规的形式,例如 TCP over IP, IP over Ethernet, IP over PPP ,上层协议数据由下层协议承载,也就是普通的 TCP/IP 网络模型。
  • 第二种是同层协议数据的承载,例如 PPPoE,由链路层协议 Ethernet 承载 PPP (Point to Point Protocol, 点对点协议) 数据。
  • 第三种,是上层协议承载下层协议数据,IP over TCP 等就属于这一类。

由于网络协议栈在发送数据时,逐层封装数据并交由下层协议进一步处理,第三类 XX over YY 没有直接得到 Linux 内核的支持。如何获得下层数据,并交给上层协议重新封装并发送呢?

pcap 可以深入网络协议栈,监听并抓取目标网络包,但这个操作将全局性地影响网络 IO 效率;用 netfilter 对网络包进行过滤和转发,应该也是一种解决之道;近年来逐渐壮大的 ebpf 应该也可以被用来抓取网络包。不过,典型的方案还是通过 tun/tap 等虚拟网卡设备来获取下层数据并重新封装发送。

虚拟网卡在逻辑上与物理网卡完全等同,核心的不同点在于,物理网卡真的会把数据顺着网线、无线网络等发送/接收。而虚拟网卡的发送/接收操作只是把数据存放到内存或从内存中读取数据。

简易实现 TCP 隧道

虚拟网卡天然地提供了抓取数据的方案。从 TUN 设备中读取到的将是网络层的报文,从 TAP 设备读到的是链路层的数据帧。将 TUN 设备得到的 IP 报文,通过已经建立的 TCP 连接发送到远端,就达成了联通两地局域网的目标。而这个 TCP 连接,也被认为是一个 TCP 隧道,为需要传送的 IP 报文隐藏途径的复杂网络,提供端到端的递送。

虽然仅仅一个隧道,没办法被称作 VPN 。局域网络虽然打通,但私密性需要加解密组件保证,独占性需要依靠认证模块来实现。不过,仅从概念验证来说,理解 TCP 隧道也足以一窥 VPN 技术。

server 侧配置

  1. 编译 VPN 模块 gcc -o vpn vpn.c (代码见参考资料 2)
  2. 利用 Netcat 作为网络模块,打开监听端口
mkfifo /tmp/fifo; cat /tmp/fifo | ./vpn | nc -l 33960 > /tmp/fifo
  1. 配置设备 IP 与 iptables
ip addr add 10.1.1.1/24 dev tun0; ip link set dev tun0 up;
iptables -t nat -A POSTROUTING -s 10.1.1.0/24 -o eth0 -j MASQUERADE

client 侧配置

  1. 编译 VPN 模块 gcc -o vpn vpn.c
  2. 利用 Netcat 作为网络模块,打开监听端口
mkfifo /tmp/fifo; cat /tmp/fifo | ./vpn | nc -l 33960 > /tmp/fifo
  1. 配置设备 IP 与 iptables
ip addr add 10.1.1.2/24 dev tun0; ip link set dev tun0 up
# 配置需要经过 TCP tunnel 的网段
ip route add xxx.xxx.xxx.xxx/xx dev tun proto kernel scope link src 10.1.1.2

参考资料

[1]. 虚拟网卡 TUN/TAP 驱动程序设计原理 [2]. 简易 VPN 程序 (clinet/server 均相同)

#include <sys/select.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <linux/if_tun.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>

#define TRUE 1
#define BUF_SIZE 4096
#define TUN_PATH "/dev/net/tun"

char buf[BUF_SIZE];

int openTun() {
    int fd = open(TUN_PATH, O_RDWR);
    if (fd == -1) {
        fprintf(stderr, "open device [tun] failed. cause: %s(%d)\n", strerror(errno), errno);
        exit(EXIT_FAILURE);
    }
    return fd;
}

int rw(int rfd, int wfd) {
    int rcount, wcount, wtcount;
    do {
        // read from rfd
        rcount = read(rfd, &buf, BUF_SIZE);
        if (rcount == -1) {
            fprintf(stderr, "read from %d failed. cause: %s(%d)\n", rfd, strerror(errno), errno);
            exit(EXIT_FAILURE);
        }

        wcount = 0;
        // write to wfd
        while (wcount != rcount) {
            wtcount = write(wfd, &buf+wcount, rcount-wcount);
            if (wtcount == -1) {
                fprintf(stderr, "write to %d failed. cause: %s(%d)\n", wfd, strerror(errno), errno);
                exit(EXIT_FAILURE);
            }
            wcount += wtcount;
        }
    } while (rcount == BUF_SIZE);
}

int main(int args, char **argv) {
    int fd = openTun();

    fd_set rfds;
    FD_ZERO(&rfds);

    struct timeval tv;

    struct ifreq ifr;
    memset(&ifr, 0, sizeof(ifr));
    ifr.ifr_flags |= IFF_NO_PI;
    ifr.ifr_flags |= IFF_TUN;
    snprintf(ifr.ifr_name, IFNAMSIZ, "%s", "tun0");
    ioctl(fd, TUNSETIFF, (void *)&ifr);

    while (TRUE) {

        /* Wait up to one seconds. */
        tv.tv_sec = 100;
        tv.tv_usec = 0;

        // Watch tun to see when it has input.
        FD_SET(fd, &rfds);
        // Watch stdin (fd 0) to see when it has input.
        FD_SET(0, &rfds);

        int retval = select(1024, &rfds, NULL, NULL, &tv);
        if (retval == -1) {
            fprintf(stderr, "select() failed. cause: %s(%d)\n", strerror(errno), errno);
        } else if (retval > 0) {
            // check which FD_ISSET(x, &rfds) is true, read data and write to
            if (FD_ISSET(0, &rfds)) {
                // read from stdin and write to tun
                // fprintf(stderr, "stdin has bytes\n");
                rw(0, fd);
            }
            if (FD_ISSET(fd, &rfds)) {
                // read from tun and write to stdout
                // fprintf(stderr, "tun0 has bytes\n");
                rw(fd, 1);
            }
        } else {
            // no data available, just timeout
            fprintf(stderr, "timeout...\n");
        }
    }
}