Tun/Tap设备基本原理
ICKelin opened this issue · 3 comments
接触过VPN相关技术的基本都会接触过虚拟网卡,tun,tap等字眼,因为大部分vpn都或多或少使用有类似技术。本文会对tun/tap设备的基本原理进行说明,并且对其如何应用在vpn上进行了分析,最后提供一个简单的tun的vpn的实现代码。
TUN/TAP设备的基本原理
首先需要明确一点,tun和tap是两种类型的虚拟设备,其一大区别是从tun设备读取数据,你将能够拿到三层包,从tap网卡获取数据,你将能拿到二层包。
在了解虚拟网卡之前,应该先简单了解下真实网卡是如何进行工作的。
首先,网卡介于物理网络和内核协议栈之间,接受协议栈外出的数据并将数据往物理网络发出,同时,也接受外部数据并交付给内核协议栈进行处理。(在这里先将内核协议栈当成一个整体,一个黑盒来看待。)
了解物理网卡所处的位置以及网络数据包的流动之后,再看看虚拟网卡有什么不一样的地方。
从最直观的使用来看,用户是可以直接读写虚拟网卡的,也就是说,从内核协议栈发出的数据在选定以虚拟网卡发出之后,数据将会被用户层程序直接读取,这点与物理网卡不一样,物理网卡直接就往外发。虚拟网卡告知用户程序数据可读。
在写方面,用户进程往虚拟网卡写数据会直接从网卡写出去
一图胜千言:
TUN/TAP与VPN
了解了TUN与TAB的基本原理之后,可以明确的知道,用户层通过虚拟网卡具备有读写二层,三层数据包的能力,这种读写与原始套接字还不一样,原始套套接字做的事旁路拷贝,这个是直接截取数据包到用户层,用户层自己处理。
有了这类技术底子之后,再看看vpn,很多人一提到vpn就想到翻墙,vpn并不等于翻墙,vpn的一个目的是为不同地区模拟出一个局域网环境,让A地区的员工能够像访问局域网一样访问位于总部B的服务器或者其他比如打印机,这是vpn。
一图胜千言:
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的原因吧,能给我一点思路嘛?