ICKelin / article

读书笔记,博客文章
MIT License
116 stars 18 forks source link

Tun/Tap设备基本原理 #9

Open ICKelin opened 6 years ago

ICKelin commented 6 years ago

接触过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 commented 2 years ago

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

ICKelin commented 2 years ago

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

stone-98 commented 2 years ago

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

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