ICKelin/article

Tun/Tap设备基本原理

ICKelin opened this issue · 3 comments

接触过VPN相关技术的基本都会接触过虚拟网卡,tun,tap等字眼,因为大部分vpn都或多或少使用有类似技术。本文会对tun/tap设备的基本原理进行说明,并且对其如何应用在vpn上进行了分析,最后提供一个简单的tun的vpn的实现代码。

TUN/TAP设备的基本原理

首先需要明确一点,tun和tap是两种类型的虚拟设备,其一大区别是从tun设备读取数据,你将能够拿到三层包,从tap网卡获取数据,你将能拿到二层包。

在了解虚拟网卡之前,应该先简单了解下真实网卡是如何进行工作的。
首先,网卡介于物理网络和内核协议栈之间,接受协议栈外出的数据并将数据往物理网络发出,同时,也接受外部数据并交付给内核协议栈进行处理。(在这里先将内核协议栈当成一个整体,一个黑盒来看待。)

了解物理网卡所处的位置以及网络数据包的流动之后,再看看虚拟网卡有什么不一样的地方。
从最直观的使用来看,用户是可以直接读写虚拟网卡的,也就是说,从内核协议栈发出的数据在选定以虚拟网卡发出之后,数据将会被用户层程序直接读取,这点与物理网卡不一样,物理网卡直接就往外发。虚拟网卡告知用户程序数据可读。

在写方面,用户进程往虚拟网卡写数据会直接从网卡写出去
一图胜千言:

image

TUN/TAP与VPN

了解了TUN与TAB的基本原理之后,可以明确的知道,用户层通过虚拟网卡具备有读写二层,三层数据包的能力,这种读写与原始套接字还不一样,原始套套接字做的事旁路拷贝,这个是直接截取数据包到用户层,用户层自己处理。

有了这类技术底子之后,再看看vpn,很多人一提到vpn就想到翻墙,vpn并不等于翻墙,vpn的一个目的是为不同地区模拟出一个局域网环境,让A地区的员工能够像访问局域网一样访问位于总部B的服务器或者其他比如打印机,这是vpn。

一图胜千言:

image

ping经过内核协议栈,路由选择从虚拟网卡发出

虚拟网卡的另外一端,也就是用户进程,将这一ping包读取出来

将ping的payload通过真实网卡发出,经过一系列的传输,到达目的主机,

目的主机收到数据包之后,将其写入虚拟网卡。

Ping reply返回类似,上图的左右两端是等价的,能够收发数据包。

为了方便说明这一原理,编写一个简单的基于tun设备的vpn——gtun

gtun客户端:

package main

import (
	"encoding/binary"
	"flag"
	"net"
	"os"
	"os/signal"
	"syscall"

	"github.com/ICKelin/glog"
	"github.com/songgao/water"
)

var (
	psrv = flag.String("s", "120.25.214.63:9621", "srv address")
	pdev = flag.String("dev", "gtun", "local tun device name")
)

func main() {
	flag.Parse()

	cfg := water.Config{
		DeviceType: water.TUN,
	}
	cfg.Name = *pdev
	ifce, err := water.New(cfg)

	if err != nil {
		glog.ERROR(err)
		return
	}

	conn, err := ConServer(*psrv)
	if err != nil {
		glog.ERROR(err)
		return
	}

	go IfaceRead(ifce, conn)
	go IfaceWrite(ifce, conn)

	sig := make(chan os.Signal, 3)
	signal.Notify(sig, syscall.SIGINT, syscall.SIGABRT, syscall.SIGHUP)
	<-sig
}

func ConServer(srv string) (conn net.Conn, err error) {
	conn, err = net.Dial("tcp", srv)
	if err != nil {
		return nil, err
	}
	return conn, err
}

func IfaceRead(ifce *water.Interface, conn net.Conn) {
	packet := make([]byte, 2048)
	for {
		n, err := ifce.Read(packet)
		if err != nil {
			glog.ERROR(err)
			break
		}

		err = ForwardSrv(conn, packet[:n])
		if err != nil {
			glog.ERROR(err)
		}
	}
}

func IfaceWrite(ifce *water.Interface, conn net.Conn) {
	packet := make([]byte, 2000)
	for {
		nr, err := conn.Read(packet)
		if err != nil {
			glog.ERROR(err)
			break
		}

		_, err = ifce.Write(packet[4:nr])
		if err != nil {
			glog.ERROR(err)
		}
	}
}

func ForwardSrv(srvcon net.Conn, buff []byte) (err error) {
	output := make([]byte, 0)
	bsize := make([]byte, 4)
	binary.BigEndian.PutUint32(bsize, uint32(len(buff)))

	output = append(output, bsize...)
	output = append(output, buff...)

	left := len(output)
	for left > 0 {
		nw, er := srvcon.Write(output)
		if err != nil {
			err = er
		}

		left -= nw
	}

	return err
}

gtun_srv,中间转发服务

package main

import (
	"io"
	"net"

	"github.com/ICKelin/glog"
)

var client = make([]net.Conn, 0)

func main() {
	listener, err := net.Listen("tcp", ":9621")
	if err != nil {
		glog.ERROR(err)
		return
	}
	for {
		conn, err := listener.Accept()
		if err != nil {
			glog.ERROR(err)
			break
		}

		client = append(client, conn)
		glog.INFO("accept gtun client")
		go HandleClient(conn)
	}
}

func HandleClient(conn net.Conn) {
	defer conn.Close()

	buff := make([]byte, 65536)
	for {
		nr, err := conn.Read(buff)
		if err != nil {
			if err != io.EOF {
				glog.ERROR(err)
			}
			break
		}

		// broadcast
		for _, c := range client {
			if c.RemoteAddr().String() != conn.RemoteAddr().String() {
				c.Write(buff[:nr])
			}
		}
	}
}

这里示例程序为了简化Demo,中间转发服务器将收到的数据包广播给所有的客户端,具体gtun实现当中会有一个协议的解码,根据目的地址来做转发。

后续将会往路由选择方面靠拢,逐步将内核协议栈这一黑盒慢慢打开。

@ICKelin 讲的很通俗易懂,点个👍,但是有问题想请教一下,我在两台ubuntu服务器下分别启动服务端以及客户端的程序,并且设置好了客户端的虚拟网卡路由,然后我ping服务端的网段,流量已经成功转发到服务端,但是ping请求一直阻塞,并没有获得响应,能解答一下嘛?

@stone-98 可以抓包看看,我猜测可能是你有一条iptables命令没加上
iptables -t nat -I POSTROUTING -j MASQUERADE

@ICKelin 还是没有成功,但是我使用抓包查看发现请求并没有转发到服务端
客户端ip:116.62.129.179
服务端ip:167.179.89.137
我的步骤如下:

  • 分别启动客户端和服务端,服务端成功打印accept gtun client,服务端和客户端的网络是互通的。
  • 给客户端的gtun网卡设置IP
sudo ip addr add 167.179.89.136/24 dev gtun
  • 给客户端的网卡gtun的状态设为up
sudo ip link set gtun up
  • 设置客户端gtun网卡路由,将167.179.89.0/24网段的请求转发到167.179.89.137
route add -net 167.179.89.0/24 gw 167.179.89.137 gtun
  • 然后ping167.179.89.136,使用dumtcp查看服务端的包,发现没有和客户端交互的包

这是我大致遇到的问题,我之前的描述有误其实流量并没有转发到服务端,所以应该不是iptables的原因吧,能给我一点思路嘛?