一、前言
网卡
也称网络适配器
,是电脑与局域网进行相互连接的设备,在 OSI 七层模型中,工作在物理层
和数据链路层
,其作用可以简单描述为:
将本机的数据封装成帧,通过网线发送到网络上去;
接收网络上其他设别传过来的帧,将其重新组合成数据,向上层传输到本机的应用程序中。
这里的网卡指的是真实的网卡,是一个真实的物理设备。今天我们要了解的是一个叫虚拟网卡
的东西。
在当前的云计算时代,虚拟机和容器的盛行离不开网络管理设备,即虚拟网络设备
,或者说是虚拟网卡
。虚拟网卡有以下好处:
对用户来说,虚拟网卡和真实网卡几乎没有区别。我们对虚拟网卡的操作不会影响到真实的网卡,不会影响到本机网络;
虚拟网卡的数据可以直接从用户态读取和写入,这样方便我们在用户态进行一些额外的操作(比如截包、修改后再发送出去)
Linux
系统中有众多的虚拟网络设备,如TUN/TAP
设备、VETH
设备、Bridge
设备、Bond
设备、VLAN
设备、MACVTAP
设备等。这里我们只关注TUN/TAP
设备。
tap/tun
是Linux内核2.4.x
版本之后实现的虚拟网络设备,不同于物理网卡靠硬件网路板卡实现,tap/tun
虚拟网卡完全由软件来实现,功能和硬件实现完全没有差别,它们都属于网络设备,都可以配置IP,都归Linux网络设备管理模块统一管理。
二、理解 tun/tap 数据传输过程
TUN
设备是一种虚拟网络设备,通过此设备,程序可以方便地模拟网络行为。TUN
模拟的是一个三层设备(OSI 模型的第三层:网络层,即IP 层),也就是说,通过它可以处理来自网络层的数据,更通俗一点的说,通过它,通过它我们可以处理IP
数据包。
先看一下正常情况下的物理设备是如何工作的:
这里的
ethx
表示的就是一台主机的真实的网卡接口,一般一台主机只会有一块网卡,像一些特殊的设备,比如路由器,有多少个口就有多少块网卡。
我们先看一下 ifconfig 命令的输出:
$ ifconfig
...
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
options=400<CHANNEL_IO>
ether ac:bc:32:96:86:01
inet6 fe80::456:7cb8:3dc5:2722%en0 prefixlen 64 secured scopeid 0x4
inet 10.0.0.176 netmask 0xffffff00 broadcast 10.0.0.255
nd6 options=201<PERFORMNUD,DAD>
media: autoselect
status: active
可以看到etho这个网卡接口分配到的IP地址是10.0.0.176
,这是一块物理网卡,它的两端分别是 内核协议栈 和 外面的网络,从物理层收到的数据,会被转发给内核进而通过某种接口被应用层的用户程序读到;应用程序要想和网络中的另一个进程进行数据通信,会先将数据发送给内核,然后被网卡发送出去。
接下来我们看一看tun/tap
设备的工作方式:
上图中应用层有两个应用程序,而 网络协议栈 和 网络设备(eth0 和 tun0)
都位于内核层,对于 socket,可以这么理解:socket
就像是一组接口
(interface),它将更复杂的TCP/IP
协议簇隐藏在 socket 接口后面,只对用户暴露更简单的接口,就像操作系统隐藏了底层的硬件操作细节而只对用户程序暴露接口一样,它是应用层
与TCP/IP协议簇
通信的中间软件抽象层。
tun0 就是一个 tun/tap 虚拟设备,从上图中就可以看出它和物理设备eth0
的区别:虽然它们的一端都是连着网络协议栈,但是eth0
另一端连接的是物理网络,而tun0
另一端连接的是一个应用层程序
,这样协议栈发送给tun0
的数据包就可以被这个应用程序读取到,此时这个应用程序可以对数据包进行一些自定义的修改(比如封装成 UDP),然后又通过网络协议栈发送出去——这就是目前大多数代理
的工作原理。
假如 eth0 的IP地址是10.0.0.176
,而tun0
配的IP为192.168.1.2
。上图是一个典型的使用tun/tap
进行VPN工作的原理,发送给192.168.1.0/24
的数据通过 应用程序B
这个隧道
处理(隐藏一些信息)之后,利用真实的物理设备 10.0.0.176 转发给目的地址(假如为49.233.198.76
),从而实现VPN
。我们看下每一个流程:
1.Application A
是一个普通的应用程序,通过Socket A
发送了一个数据包,这个数据包的目的地址是192.168.1.2
;
Socket A
将这个数据包丢给网络协议栈;协议栈根据数据包的目的地址,匹配本地路由规则,得知这个数据包应该由
tun0
出去,于是将数据包丢给了tun0
;tun0
收到数据包之后,发现另一端被Application B
打开,于是又将数据包丢给了Application B
;Application B
收到数据包之后,解包,做一些特殊的处理,然后构造一个新的数据包,将原来的数据嵌入新的数据包中,最后通过Socket B
将数据包转发出去,这个时候新数据包的源地址就变成了eth0
的地址,而目的地址就变成了真正想发送的主机的地址,比如49.233.198.76
;
6.Socket B
将这个数据包丢给网络协议栈;协议栈根据本地路由得知,这个数据包应该从
eth0
发送出去,于是将数据包丢给eth0
;
8.eth0
通过物理网络将这个数据包发送出去
简单来说,tun/tap
设备的用处是将协议栈中的部分数据包转发给用户空间的特殊应用程序,给用户空间的程序一个处理数据包的机会,比较常用的场景是数据压缩
、加密
等,比如VPN
。
三、使用 Golang 实现一个简易 VPN
先看客户端的实现:
package main
import (
"encoding/binary"
"net"
"os"
"os/signal"
"syscall"
"github.com/fatih/color"
"github.com/songgao/water"
flag "github.com/spf13/pflag"
)
/*
数据传输过程:
用户数据,如ping --> 协议栈conn --> IfaceWrite --> IfaceRead --> 协议栈conn --> 网线
*/
var (
serviceAddress = flag.String("addr", "10.0.0.245:9621", "service address")
tunName = flag.String("dev", "", "local tun device name")
)
func main() {
flag.Parse()
// create tun/tap interface
iface, err := water.New(water.Config{
DeviceType: water.TUN,
PlatformSpecificParams: water.PlatformSpecificParams{
Name: *tunName,
},
})
if err != nil {
color.Red("create tun device failed,error: %v", err)
return
}
// connect to server
conn, err := net.Dial("tcp", *serviceAddress)
if err != nil {
color.Red("connect to server failed,error: %v", err)
return
}
//
go IfaceRead(iface, conn)
go IfaceWrite(iface, conn)
sig := make(chan os.Signal, 3)
signal.Notify(sig, syscall.SIGINT, syscall.SIGABRT, syscall.SIGHUP)
<-sig
}
/*
IfaceRead 从 tun 设备读取数据
*/
func IfaceRead(iface *water.Interface, conn net.Conn) {
packet := make([]byte, 2048)
for {
// 不断从 tun 设备读取数据
n, err := iface.Read(packet)
if err != nil {
color.Red("READ: read from tun failed")
break
}
// 在这里你可以对拿到的数据包做一些数据,比如加密。这里只对其进行简单的打印
color.Cyan("get data from tun: %v", packet[:n])
// 通过物理连接,将处理后的数据包发送给目的服务器
err = forwardServer(conn, packet[:n])
if err != nil {
color.Red("forward to server failed")
}
}
}
/*
IfaceWrite 从物理连接中读取数据,然后通过 tun 将数据发送给 IfaceRead
*/
func IfaceWrite(iface *water.Interface, conn net.Conn) {
packet := make([]byte, 2048)
for {
// 从物理请求中读取数据
nr, err := conn.Read(packet)
if err != nil {
color.Red("WRITE: read from tun failed")
break
}
// 将处理后的数据通过 tun 发送给 IfaceRead
_, err = iface.Write(packet[4:nr])
if err != nil {
color.Red("WRITE: write to tun failed")
}
}
}
// forwardServer 通过物理连接发送一个包
func forwardServer(conn 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 := conn.Write(output)
if er != nil {
err = er
}
left -= nw
}
return err
}
再看服务端的实现:
package main
import (
"io"
"net"
"github.com/fatih/color"
)
var clients = make([]net.Conn, 0)
func main() {
listener, err := net.Listen("tcp", ":9621")
if err != nil {
color.Red("listen failed,error: %v", err)
return
}
color.Cyan("server start...")
for {
// 对客户端的每一个连接,都起一个 go 协程去处理
conn, err := listener.Accept()
if err != nil {
color.Red("tcp accept failed,error: %v", err)
break
}
clients = append(clients, conn)
color.Cyan("accept tun client")
go handleClient(conn)
}
}
func handleClient(conn net.Conn) {
defer conn.Close()
buff := make([]byte, 65536)
for {
n, err := conn.Read(buff)
if err != nil {
if err != io.EOF {
color.Red("read from client failed")
}
break
}
// broadcast data to all clients
for _, c := range clients {
if c.RemoteAddr().String() != conn.RemoteAddr().String() {
c.Write(buff[:n])
}
}
}
}
步骤如下:
分别启动客户端和服务端,服务端成功打印accept tun client,服务端和客户端的网络是互通的。
给客户端的tun10网卡设置IP
sudo ip addr add 192.168.100.1/24 dev tun10
给客户端的网卡tun10的状态设为up
sudo ip link set tun10 up
设置客户端tun10网卡路由,将192.168.100.1/24网段的请求转发到192.168.100.1
sudo route add -net 192.168.100.0/24 gw 192.168.100.1 tun10 sudo route add default gw 192.168.100.1 tun10 # 设置默认网关
然后ping167.179.89.136,使用dumtcp查看服务端的包,发现没有和客户端交互的包
附上我对VPN的理解
四、参考
```
评论区