Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tun/Tap设备基本原理 #9

Open
ICKelin opened this issue Mar 27, 2018 · 3 comments
Open

Tun/Tap设备基本原理 #9

ICKelin opened this issue Mar 27, 2018 · 3 comments

Comments

@ICKelin
Copy link
Owner

ICKelin commented Mar 27, 2018

接触过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实现当中会有一个协议的解码,根据目的地址来做转发。

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

@stone-98
Copy link

stone-98 commented Mar 3, 2022

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

@ICKelin
Copy link
Owner Author

ICKelin commented Mar 13, 2022

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

@stone-98
Copy link

stone-98 commented Mar 13, 2022

@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的原因吧,能给我一点思路嘛?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants