最近在学习Linux 内核二三层转发的知识,同时也在学习GO编程,于是有了如何通过Go实现创建TUN/TAP接口,并实现收发包的想法,通过查阅相关资料及不断调试终于实现,在此分享给大家,希望能共同学习交流。
TUN/TAP 设备一端连着操作系统协议栈,另一端连着用户空间的程序:用户空间程序 --- tap0
&tun0
--- TCP/IP 协议栈 --- ping,如下图所示:
TUN 工作在三层,无 MAC 地址,无法加入网桥;TAP 工作在二层,更接近物理网卡;
TUN 设备常用于 VPN 场景;TAP 设备常用于虚拟机网卡;
GO下实现创建TAP/TUN接口及收发包,主要依赖如下库:
water:是一个用于TUN/TAP接口的原生库
gopacket:是GO语言中用于处理网络数据包的一个库,它提供了一种方便的方式来解析和构建网络数据包,支持多种网络协议的解析。gopacket还包含许多子包用于提供额外的功能,包括 :layers、pcap、pfring、afpacket、tcpassembly;
gopacket/pcap:用于读取和分析网络流量,可以从具体线路上(OpenLive)读取,也可从具体的pcap文件(OpenOffline)中读取。
gopacket/layers:用于将报文解码为具体协议的片段。
具体使用可参考官方文档使用说明:GO 库使用说明手册
package main
import (
"log"
"github.com/songgao/water"
"net"
"github.com/google/gopacket"
"github.com/google/gopacket/pcap"
"github.com/google/gopacket/layers"
"os/exec"
"fmt"
)
func main() {
/* 依据配置信息创建并生成相应接口 */
config := water.Config{
/* 设置接口类型 */
DeviceType : water.TAP,
}
/* 设置接口名 */
config.Name = "tap0"
/* 依据配置信息创建并生成相应接口 */
iface, err := water.New(config)
if err != nil {
log.Fatal(err)
}
/* 通exec库,构建linux 命令 */
cmd := exec.Command("ip", "addr", "add", "10.1.1.200/24", "dev", "tap0")
/* 命令执行,为接口绑定IP */
_ = cmd.Run()
/* 设置接口状态为up */
cmd = exec.Command("ip", "link", "set", "tap0", "up")
_ = cmd.Run()
/* 构建SMAC, SIP 由于回复ARP */
sourceMACAddr, _:= net.ParseMAC("00:00:00:00:00:01")
sourceIPAddr := net.ParseIP("10.1.1.200")
/* 通pcap库捕获tap0接口上的报文,打开tap0接口 */
handle, err := pcap.OpenLive("tap0", 1600, true, pcap.BlockForever)
if err != nil {
log.Fatal(err)
}
/* 构建包接收源 */
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
/* 接收数据包 */
for packet := range packetSource.Packets() {
/* 将数据报文解码为ARP报文 */
arpLayer := packet.Layer(layers.LayerTypeARP)
if arpLayer != nil {
/* 强制转换并获取ARP头部信息 */
arpPacket, _ := arpLayer.(*layers.ARP)
/* 判断是否为ARP请求 */
if arpPacket.Operation == layers.ARPRequest {
// 输出 ARP 报文信息
fmt.Println("ARP Operation:", arpPacket.Operation)
fmt.Println("ARP Source Hardware Address:", arpPacket.SourceHwAddress)
fmt.Println("ARP Source Protocol Address:", arpPacket.SourceProtAddress)
fmt.Println("ARP Target Hardware Address:", arpPacket.DstHwAddress)
fmt.Println("ARP Target Protocol Address:", arpPacket.DstProtAddress)
fmt.Println("--------------------------")
/* 构造arpReply 头部信息 */
arpReply := &layers.ARP{
AddrType: layers.LinkTypeEthernet,
Protocol: layers.EthernetTypeIPv4,
HwAddressSize: 6,
ProtAddressSize: 4,
Operation: layers.ARPReply,
SourceHwAddress: sourceMACAddr,
SourceProtAddress: sourceIPAddr.To4(),
DstHwAddress: arpPacket.SourceHwAddress,
DstProtAddress: arpPacket.SourceProtAddress,
}
/* 构建以太网头部信息, ARP属于三层协议,报文格式却是二层报文 */
ethernetLayer := &layers.Ethernet{
SrcMAC: sourceMACAddr,
DstMAC: arpPacket.SourceHwAddress,
EthernetType: layers.EthernetTypeARP,
}
/* 构建并通过tun0接口发送数据包 */
frame1 := gopacket.NewSerializeBuffer()
gopacket.SerializeLayers(frame1, gopacket.SerializeOptions{}, ethernetLayer, arpReply)
_, err = iface.Write(frame1.Bytes())
}
continue
}
/* 将数据报文解码为ICMPv4报文 */
icmpLayer := packet.Layer(layers.LayerTypeICMPv4)
if icmpLayer != nil {
icmpPacket, _ := icmpLayer.(*layers.ICMPv4)
// 输出 ICMP 报文信息
fmt.Printf("ICMP Type: %d\n", icmpPacket.TypeCode.Type())
fmt.Printf("ICMP Code: %d\n", icmpPacket.TypeCode.Code())
fmt.Printf("ICMP Checksum: %d\n", icmpPacket.Checksum)
fmt.Printf("ICMP Payload: %v\n", icmpPacket.Payload)
fmt.Println("--------------------------")
/* 依据icmp的 type ,code判断是否为 icmp请求报文 */
if icmpPacket.TypeCode.String() == "EchoRequest" {
/* 构造 icmpreply 报文头部信息 */
icmpReplyPacket := &layers.ICMPv4{
TypeCode: layers.ICMPv4TypeEchoReply,
Id: icmpPacket.Id,
Seq: icmpPacket.Seq,
}
/* icmpLayer只获取了报文的三层及以上信息,不需设置以太网头部信息 */
ipPacket := packet.Layer(layers.LayerTypeIPv4).(*layers.IPv4)
ipPacket.DstIP, ipPacket.SrcIP = ipPacket.SrcIP, ipPacket.DstIP
fmt.Printf("ICMP SIP: %v\n", ipPacket.SrcIP)
fmt.Printf("ICMP DIP: %v\n", ipPacket.DstIP)
/* tap口工作在二层,需要构建以太网头部信息 */
ethernetPacket := packet.Layer(layers.LayerTypeEthernet).(*layers.Ethernet)
ethernetPacket.DstMAC, ethernetPacket.SrcMAC = ethernetPacket.SrcMAC, ethernetPacket.DstMAC
/* 构造并发送icmp reply报文 */
frame2 := gopacket.NewSerializeBuffer()
gopacket.SerializeLayers(frame2, gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
},ethernetPacket, ipPacket, icmpReplyPacket, gopacket.Payload(icmpPacket.Payload))
_, err = iface.Write(frame2.Bytes())
}
continue
}
}
}
注意:该程序只适用于Linux系统。
1)在shell窗口执行./tap程序
2)在另一个shell窗口通过ping -c3 -i2 10.1.1.200
3)shell1可以接收到ARP,ICMP请求报文,并构造回复ARP,ICMP响应报文
shell1效果如下:
shell2 ping报文有回复,且arp -a可以查看到对应的arp表信息,如下所示:
package main
/* 导入依赖包 */
import (
"fmt"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
"github.com/songgao/water"
"log"
"os/exec"
)
func main() {
/* 通过water库配置 tun接口信息 */
config := water.Config{
/* 设置接口类型 */
DeviceType: water.TUN,
}
/* 设置接口名 */
config.Name = "tun0"
/* 依据配置信息创建并生成相应接口 */
iface, err := water.New(config)
if err != nil {
log.Fatal(err)
}
/* 通exec库,构建linux 命令 */
cmd := exec.Command("ip", "addr", "add", "10.1.1.100/24", "dev", "tun0")
/* 命令执行,为接口绑定IP */
_ = cmd.Run()
/* 设置接口状态为up */
cmd = exec.Command("ip", "link", "set", "tun0", "up")
_ = cmd.Run()
/* 通pcap库捕获tun0接口上的报文,打开tun0接口 */
handle, err := pcap.OpenLive("tun0", 1600, true, pcap.BlockForever)
if err != nil {
log.Fatal(err)
}
/* 构建包接收源 */
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
/* 接收数据包 */
for packet := range packetSource.Packets() {
/* 将数据报文解码为ICMPv4报文 */
icmpLayer := packet.Layer(layers.LayerTypeICMPv4)
if icmpLayer != nil {
icmpPacket, _ := icmpLayer.(*layers.ICMPv4)
// 输出 ICMP 报文信息
fmt.Printf("ICMP Type: %d\n", icmpPacket.TypeCode.Type())
fmt.Printf("ICMP Code: %d\n", icmpPacket.TypeCode.Code())
fmt.Printf("ICMP Checksum: %d\n", icmpPacket.Checksum)
fmt.Printf("ICMP Payload: %v\n", icmpPacket.Payload)
fmt.Println("--------------------------")
/* 依据icmp的 type ,code判断是否为 icmp请求报文 */
if icmpPacket.TypeCode.String() == "EchoRequest" {
/* 构造 icmpreply 报文头部信息 */
icmpReplyPacket := &layers.ICMPv4{
TypeCode: layers.ICMPv4TypeEchoReply,
Id: icmpPacket.Id,
Seq: icmpPacket.Seq,
}
/* icmpLayer只获取了报文的三层及以上信息,不需设置以太网头部信息 */
ipPacket := packet.Layer(layers.LayerTypeIPv4).(*layers.IPv4)
ipPacket.DstIP, ipPacket.SrcIP = ipPacket.SrcIP, ipPacket.DstIP
fmt.Printf("ICMP SIP: %v\n", ipPacket.SrcIP)
fmt.Printf("ICMP DIP: %v\n", ipPacket.DstIP)
/* 构造并发送icmp reply报文 */
frame2 := gopacket.NewSerializeBuffer()
gopacket.SerializeLayers(frame2, gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
}, ipPacket, icmpReplyPacket, gopacket.Payload(icmpPacket.Payload))
_, err = iface.Write(frame2.Bytes())
}
}
}
}
注意:该程序只适用于Linux系统。
1)在shell窗口执行./tun程序
2)在另一个shell窗口通过ping -c3 -i2 10.1.1.200
3)shell1可以接收到ICMP请求报文,并构造回复ICMP响应报文
shell1效果如图所示:
shell2效果如图所示:
Linux 网络设备- TUN/TAP