上一集中我们讲到了wpcap.dll的go封装方法,对于linux系统下libpcap的go封装采用的是常用的cgo方式,想了解的可以看看pcap文件夹中的pcap_unix.go。
我们得到了wpcap.dll的go调用,就可以利用它来进行列举所有网络设备,例如以下代码
package main
import (
"fmt"
"github.com/google/gopacket/pcap"
"log"
)
// 得到所有的(网络)设备
devices, err := pcap.FindAllDevs()
if err != nil {
log.Fatal(err)
}
// 打印设备信息
fmt.Println("Devices found:")
for _, device := range devices {
fmt.Println("\nName: ", device.Name)
fmt.Println("Description: ", device.Description)
fmt.Println("Devices addresses: ", device.Description)
for _, address := range device.Addresses {
fmt.Println("- IP address: ", address.IP)
fmt.Println("- Subnet mask: ", address.Netmask)
}
}
也可以抓取某个网络设备的数据包,例如以下代码
package main
import (
"fmt"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
"log"
_ "strings"
"time"
)
var (
device string = "你的网络设备名"
snapshotLen int32 = 1024
promiscuous bool = false
err error
timeout time.Duration = 30 * time.Second
handle *pcap.Handle
)
func main() {
// Open device
handle, err = pcap.OpenLive(device, snapshotLen, promiscuous, timeout)
if err != nil {
log.Fatal(err)
}
defer handle.Close()
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
for packet := range packetSource.Packets() {
printPacketInfo(packet)
}
}
func printPacketInfo(packet gopacket.Packet) {
// Let's see if the packet is an ethernet packet
// 判断数据包是否为以太网数据包,可解析出源mac地址、目的mac地址、以太网类型(如ip类型)等
ethernetLayer := packet.Layer(layers.LayerTypeEthernet)
if ethernetLayer != nil {
fmt.Println("Ethernet layer detected.")
ethernetPacket, _ := ethernetLayer.(*layers.Ethernet)
fmt.Println("Source MAC: ", ethernetPacket.SrcMAC)
fmt.Println("Destination MAC: ", ethernetPacket.DstMAC)
// Ethernet type is typically IPv4 but could be ARP or other
fmt.Println("Ethernet type: ", ethernetPacket.EthernetType)
fmt.Println()
}
// Let's see if the packet is IP (even though the ether type told us)
// 判断数据包是否为IP数据包,可解析出源ip、目的ip、协议号等
ipLayer := packet.Layer(layers.LayerTypeIPv4)
if ipLayer != nil {
fmt.Println("IPv4 layer detected.")
ip, _ := ipLayer.(*layers.IPv4)
// IP layer variables:
// Version (Either 4 or 6)
// IHL (IP Header Length in 32-bit words)
// TOS, Length, Id, Flags, FragOffset, TTL, Protocol (TCP?),
// Checksum, SrcIP, DstIP
fmt.Printf("From %s to %s\n", ip.SrcIP, ip.DstIP)
fmt.Println("Protocol: ", ip.Protocol)
fmt.Println()
}
// Let's see if the packet is TCP
// 判断数据包是否为TCP数据包,可解析源端口、目的端口、seq序列号、tcp标志位等
tcpLayer := packet.Layer(layers.LayerTypeTCP)
if tcpLayer != nil {
fmt.Println("TCP layer detected.")
tcp, _ := tcpLayer.(*layers.TCP)
// TCP layer variables:
// SrcPort, DstPort, Seq, Ack, DataOffset, Window, Checksum, Urgent
// Bool flags: FIN, SYN, RST, PSH, ACK, URG, ECE, CWR, NS
fmt.Printf("From port %d to %d\n", tcp.SrcPort, tcp.DstPort)
fmt.Println("Sequence number: ", tcp.Seq)
fmt.Println()
}
// Iterate over all layers, printing out each layer type
fmt.Println("All packet layers:")
for _, layer := range packet.Layers() {
fmt.Println("- ", layer.LayerType())
}
///.......................................................
// Check for errors
// 判断layer是否存在错误
if err := packet.ErrorLayer(); err != nil {
fmt.Println("Error decoding some part of the packet:", err)
}
}
我们可以看到上述代码中,需要对抓取到的网络数据包进行解包操作,每个数据包就像是洋葱一样由各层协议层层封装而成。如果你愿意一层一层一层地剥开它的心,哈哈哈
gopacket库对于每个协议的解包(解析)操作都对应一个go语言文件,放在了layers文件夹中。
本集我们讲解的内容:gopacket库中对于数据包packet的抽象以及对于layers中对于常见协议的封装操作。
我们先来聚焦一下packet.go。gopacket库中使用Packet来表示一个一个的数据包。这里是gopacket库抽象的Packet接口,
type Packet interface {
Functions for outputting the packet as a human-readable string:
------------------------------------------------------------------
// String returns a human-readable string representation of the packet.// It uses LayerString on each layer to output the layer.String() string
// Dump returns a verbose human-readable string representation of the packet,// including a hex dump of all layers. It uses LayerDump on each layer to// output the layer.
Dump() string
Functions for accessing arbitrary packet layers:
------------------------------------------------------------------
// Layers returns all layers in this packet, computing them as necessaryLayers() []Layer
// Layer returns the first layer in this packet of the given type, or nilLayer(LayerType) Layer
// LayerClass returns the first layer in this packet of the given class,// or nil.
LayerClass(LayerClass) Layer
Functions for accessing specific types of packet layers. These functions
return the first layer of each type found within the packet.
------------------------------------------------------------------
// 返回第一个链路层
// LinkLayer returns the first link layer in the packet
LinkLayer() LinkLayer
// 返回第一个网络层
// NetworkLayer returns the first network layer in the packet
NetworkLayer() NetworkLayer
// 返回第一个传输层
// TransportLayer returns the first transport layer in the packet
TransportLayer() TransportLayer
// 返回第一个应用层
// ApplicationLayer returns the first application layer in the packet
ApplicationLayer() ApplicationLayer
// ErrorLayer is particularly useful, since it returns nil if the packet// was fully decoded successfully, and non-nil if an error was encountered
// in decoding and the packet was only partially decoded. Thus, its output
// can be used to determine if the entire packet was able to be decoded.
ErrorLayer() ErrorLayer
Functions for accessing data specific to the packet:
------------------------------------------------------------------
// Data returns the set of bytes that make up this entire packet.
Data() []byte
// Metadata returns packet metadata associated with this packet.
Metadata() *PacketMetadata
}
packet结构中可以看到有几个layer类型,LinkLayer,NetworkLayer ,TransportLayer,ApplicationLayer ,ErrorLayer。我们从这几个名称可以猜出(除去ErrorLayer这个表示错误的layer,其他刚好四个),其对应的应该是TCP/IP体系结构。
有了数据包对应的结构,接下来就是对于相应数据包每一层协议封装相应操作。
请看源码中layers->ethernet.go
ethernet.go是gopacket库根据ethernet协议对ethernet数据包包装的各种操作函数,里面不涉及执行顺序,所以我建议我们先看一下代码里面具体类型结构
type Ethernet struct {
BaseLayer
// 源物理地址 目的物理地址
SrcMAC, DstMAC net.HardwareAddr
// 以太网类型
EthernetType EthernetType
// Length is only set if a length field exists within this header. Ethernet
// headers follow two different standards, one that uses an EthernetType, the
// other which defines a length the follows with a LLC header (802.3). If the// former is the case, we set EthernetType and Length stays 0. In the latter// case, we set Length and EthernetType = EthernetTypeLLC.
//数据包长度
Length uint16
}
这里补充一点以太网协议相关知识
以太网是一种产生较早,使用相当广泛的局域网技术。最初是由Xerox(施乐)公司创建(大概是1973年诞生)并由Xerox、 Intel和DEC公司联合开发的基带局域网规范,后来被电气与电子工程师协会( IEEE)所采纳作为802.3的标准。
目前以太网根据速度等级分类大概分为:标准以太网(10Mbit/s),快速以太网(100Mbit/s),千兆以太网(1000Mbit/s),以及更快的万兆以太网(10Gbit/s)
以太网协议(Ethernet Protocol)按照七层(OSI)网络模型来划分的话属于数据链路层的协议,
常用的以太网MAC帧格式有两种标准,一种是DIX Ethernet V2标准(即以太网V2标准),另一种是IEEE的802.3标准。这里只介绍使用得最多的以太网V2的MAC帧格式(如下图)。
![[Pasted image 20240126142219.png]]
图中假定网络层使用的是IP协议。实际上使用其他的协议也是可以的。IEEE 802.3标准规定的MAC帧格式与上面所讲的以太网V2 MAC帧格式的区别就是两个地方。
第一,IEEE 802.3规定的MAC帧的第三个字段是“长度/类型”。当这个字段值大于0x0600时(相当于十进制的1536),就表示“类型”。这样的帧和以太网V2 MAC帧完全一样。只有当这个字段值小于0x0600时才表示“长度”,即MAC帧的数据部分长度。显然,在这种情况下,若数据字段的长度与长度字段的值不一致,则该帧为无效的MAC帧。实际上,前面我们已经讲过,由于以太网采用了曼彻斯特编码,长度字段并无实际意义。第二,当“长度/类型”字段值小于0x0600时,数据字段必须装入上面的逻辑链路控制LLC子层的LLC帧。
由于现在广泛使用的局域网只有以太网,因此LLC帧已经失去了原来的意义(见本章3.3.1节第1小节“以太网的两个标准”)。现在市场上流行的都是以太网V2的MAC帧,但大家也常常把它称为IEEE 802.3标准的MAC帧。------《计算机网络第七版 谢希仁》
图中所示的目的地址和源地址实际上就是MAC地址分别占了6字节,而 类型 则是用来标识上一层所使用的协议类型,如IP协议(0x0800),ARP(0x0806)等。FCS字段是帧校验字段,即Frame Check Sequence,用来保存CRC(循环冗余校验)校验值。
以太网协议中规定最小的以太网数据包长度应该是64字节,最大长度1518字节,除去目的地址、源地址(各6字节),类型(2字节),FEC字段(4字节),数据字段长度应该在46~1500字节。当数据字段的长度小于46字节时,MAC子层就会在数据字段的后面加入一个整数字节的填充字段,以保证以太网的MAC帧长不小于64字节。
OK,有了上面的知识补充,我们可以继续阅读ethernet.go源码了。
首先是对于以太网协议的解码操作,so look at function DecodeFromBytes():
func (eth *Ethernet) DecodeFromBytes(data []byte, df gopacket.DecodeFeedback) error {
if len(data) < 14 {
return errors.New("Ethernet packet too small")
}
eth.DstMAC = net.HardwareAddr(data[0:6])
eth.SrcMAC = net.HardwareAddr(data[6:12])
eth.EthernetType = EthernetType(binary.BigEndian.Uint16(data[12:14]))
eth.BaseLayer = BaseLayer{data[:14], data[14:]}
eth.Length = 0
if eth.EthernetType < 0x0600 {
eth.Length = uint16(eth.EthernetType)
eth.EthernetType = EthernetTypeLLC
if cmp := len(eth.Payload) - int(eth.Length); cmp < 0 {
df.SetTruncated()
} else if cmp > 0 {
// Strip off bytes at the end, since we have too many bytes
eth.Payload = eth.Payload[:len(eth.Payload)-cmp]
}
// fmt.Println(eth)
}
return nil
}
首先当 len(data)<14时 即目的地址、源地址、类型三个字段不完整,如果长度够继续解析,
eth.DstMAC = net.HardwareAddr(data[0:6])
eth.SrcMAC = net.HardwareAddr(data[6:12])
eth.EthernetType = EthernetType(binary.BigEndian.Uint16(data[12:14]))
eth.BaseLayer = BaseLayer{data[:14], data[14:]}
eth.Length = 0
目的地址、源地址、类型三个字段解析到变量中,然后将前14个字节和14个字节之后的数据复制到baselayer中。
// BaseLayer is a convenience struct which implements the LayerData and// LayerPayload functions of the Layer interface.
type BaseLayer struct {
// Contents is the set of bytes that make up this layer. IE: for an// Ethernet packet, this would be the set of bytes making up the// Ethernet frame.
// 可以理解为封装头
Contents []byte
// Payload is the set of bytes contained by (but not part of) this// Layer. Again, to take Ethernet as an example, this would be the// set of bytes encapsulated by the Ethernet protocol.
// 数据内容
Payload []byte
}
然后是对于EthernetType进行一个判断,这个if语句是用来判断该数据packet采用的协议是以太网v2还是IEEE 802.3,依据见 补充 IEEE 802.3
if eth.EthernetType < 0x0600 {
// 从第三个字段获取以太网数据长度信息
eth.Length = uint16(eth.EthernetType)
// 设置以太网协议类型
eth.EthernetType = EthernetTypeLLC
// 比较获取的以太网数据长度信息是否和实际的数据长度一样,小则设置截断,大则按照获取的以太网数据长度信息重置Payload
if cmp := len(eth.Payload) - int(eth.Length); cmp < 0 {
df.SetTruncated()
} else if cmp > 0 {
// Strip off bytes at the end, since we have too many bytes
eth.Payload = eth.Payload[:len(eth.Payload)-cmp]
}
// fmt.Println(eth)
}
本文件代码中DecodeFromBytes是对于数据包的解包操作,而 SerializeTo 则是对于数据封装成数据包的操作 ,其他的没什么可讲的,基本是为了实现其他接口而写的,自己看吧。
基本上layers文件夹中每一个文件名都对应着一个协议,基本上操作和ethernet.go一样。
本集总结:
获取了抓取到的数据包后,需要对包进行解析,gopacket库的layers中封装了一系列对于各种协议的操作。我们知道了对应协议的格式内容也可以自己来编写类似的操作。