《零入门kubernetes网络实战》视频专栏地址
https://www.ixigua.com/7193641905282875942
本篇文章视频地址(稍后上传)
本篇文章是在上一篇文章的基础之上,重点介绍
- 以太网帧、
- ICMP、
- UDP、
- TCP、
- ARP、
- IP
协议的封装过程,
并用golang解析
- 以太网帧、
- ICMP、
- UDP、
- TCP、
- ARP、
- IP
协议的报文头结构,以及对应的数据内容
用到的测试代码是
上一篇文章中的测试用例1中的代码。
代码占用的篇幅量太大,不再这里重新展示了。
1、本篇文章的核心点 |
本篇文章的核心点:
- 把ICMP、ARP、IP、TCP、UDP协议的封装过程进行展示
- 使用golang对ICMP、ARP、IP、TCP、UDP协议的报文头进行解析
- 通过tcpdump命令进行抓包,使用wireshark进行抓包分析
- 跟golang解析的报文头进行对比,检测一下我们解析的报文头是否有问题
- 不会介绍ARP、ICMP、IP、TCP、UDP协议的原理。
2、测试环境介绍 |
两台虚拟机
- centos7.5 10.211.55.122
- centos7.5 10.211.55.123
3、OSI七层模型介绍 |
4、网络信息传输模型图 |
客户端发送数据到服务器端的完整路线
5、帧 |
5.1、什么叫做帧 |
在数据链路传输的数据包叫做帧。
也就是说
数据链路层以帧为单位进行传输和处理数据。
在数据链路层里,将位组合成字节,并将字节组合成帧。
5.2、帧结构分类 |
至少存在
- 以太网帧
- PPP帧
6、以太网帧介绍 |
6.1、以太网帧结构 6.1.1、封装过程流程图 |
怎么看这个图呢?
- 从上往下看
- 从IP层到数据链路层,增加了哪些字段
- 从数据链路层到物理层,增加哪些字段
- 插入8个字节,目前不用关心。
- 这8个字节,表明开始读取以太网帧了。也就是起始位置。
6.1.2、以太网帧结构 |
看上面图中第2行,从左到右看
- 目的MAC地址,
- 占用6个字节
- 接收方设备的硬件地址
- 源MAC地址
- 占用6个字节
- 发送方设备的硬件地址
- 类型
- 占用2个字节
- 表示封装的数据的类型。如:
- 0x0800代表IP协议帧
- 0x0806代表ARP协议帧
- 0x8864代表PPPoE
- 0x86dd代表IPv6
- 其他协议栈可以参考网址https://www.pianshen.com/article/5115861078/
- 注意:
- 没有TCP,UDP,ICMP。
- 也就说,不能将TCP,UDP,ICMP数据直接封装到以太网帧里。
- 必须先封装放到IP协议栈里。
- 数据
- 被封装的具体内容。
- 占用46~1500字节
- 如果不足46字节时,会进行填充到46字节
- 最大占用1500字节。
- 最大值也叫做最大传输单元(MTU)
- 内容的类型是有要求的
- 在上面"类型"里已经说明了
- FCS
- 帧校验序列
6.2、以太网帧报文头解析介绍 |
代码在前一篇文章中测试用例1的
etherheader.go文件中
先看一下main.go文件中的tapToUDP函数
读取原理,前面介绍tun设备时,已经介绍过了。
这里又简单的说明了一次。
然后,打开etherheader.go文件
7、IP报文介绍 |
7.1、IP报文封装过程 |
什么是封装呢?
简单的说,就是在上一层的基础上又添加一些东西而已。
添加的东西都有一定的规则。
上一层到下一层,添加了哪些属性。
7.2、IP报文详细结构如下:包括IP报文头结构+IP报文数据 |
从上面的图中,可以看出来
一个IP报文包括2部分构成
- IP报文头
- 固定部分占用20个字节
- 可变长部分占用0~40个字节
- 具体数据部分
一个IP报文,包括IP报文头+数据部分,最多可以占用65535个字节
首先注意一点:
IP报文的存储结构并不是如上图的样子,是个长方形。
IP报文是存储一个切片里。
只是IP报文的属性比较多,这里为了表达清楚,而如此绘画的。
前5行,每行占用4个字节。
按照图中的顺序存储的属性。
先版本号,然后头部长度,以此类推。
IP报文头结构的具体属性解释,可以参考下面的文章
https://wenku.baidu.com/view/f4559240f6335a8102d276a20029bd64783e62f1.html
7.3、使用golang来解析IP报文 |
7.3.1、对前篇文章测试用例1中的代码,进行更新 |
从上图中,可以看出来,
IP头部是由两部分构成的:
第一部分是固定部分,固定占用20个字节。
第二部分是可选部分,最少占用0个字节,最多占用40个字节。
因此,我们需要对测试用例1中的client包下的main.go文件以及ipv4header.go文件进行更新。
7.3.1.1、更新main.go文件中的tapToUDP函数 |
tapToUDP函数的最新内容如下:(直接copy即可)
func tapToUDP(udpConn *net.UDPConn, tapFile *os.File) {
packet := make([]byte, 1024*64)
size := 0
var err error
for {
if size, err = tapFile.Read(packet); err != nil {
return
}
te := MACType(packet[:size])
printMACHeader(packet[:size])
if strings.EqualFold(fmt.Sprintf("%x", te), "0800") {
b := packet[14:size]
printIPv4Header(b)
hl := getIPv4HeaderLen(b)
if b[9] == 1 {
icmpPacket := b[hl:]
printICMPHeader(icmpPacket)
}
if b[9] == 6 {
tcpPacket := b[hl:]
printTCPHeader(tcpPacket)
}
if b[9] == 17 {
udpPacket := b[hl:]
printUDPHeader(udpPacket)
}
}
if strings.EqualFold(fmt.Sprintf("%x", te), "0806") {
b := packet[14:size]
printARPHeader(b)
}
rAddr, err := net.ResolveUDPAddr("udp", remoteAddr)
if err != nil {
log.Fatalln("failed to get udp socket:", err)
return
}
if size, err = udpConn.WriteTo(packet[:size], rAddr); err != nil {
fmt.Println(err.Error())
return
}
fmt.Printf("tapToUDP--->Write Msg To UDP Conn OK! size:%d\n", size)
}
}
7.3.1.2、更新ipv4header.go文件中,新增一个函数getIPv4HeaderLen |
在ipv4header.go文件中,新增加一个函数
func getIPv4HeaderLen(packet []byte) int {
header := packet[0]
headerLen := header & 0x0f * 4
hl, _ := strconv.Atoi(fmt.Sprintf("%d", headerLen))
return hl
}
7.4、对ipv4header.go文件中的部分代码进行说明 |
在测试用例1中的ipv4header.go文件里
7.4.1、printVersionIPv4函数说明 |
该函数打印的是IP协议的版本号,
IP协议有两个版本号:
- IPv4
- IPv6
上面的图中,已经说明版本号占用的是第一个字节的前4个bit;
第一个字节,即packet[0]
而函数printVersionIPv4的参数类型是字节切片
只能按照字节获取
1个字节等于8个bit
那么,如何获取前4个bit的值呢?
直接将前4个字节向右移动4位即可,即packet[0]>>4
7.4.2、printHeaderLenIPv4函数说明 |
该函数是打印一个IP报文的头部长度
包括:固定长度+可选项长度
上面图中,已经说明,头部长度在第1个字节的后4个bit存储。
即:
- 第1个字节用packet[0]来获取
- 只有后4个bit有用,即packet[0]&0x0f
- 0x0f,转换成二进制是0b00001111;
- 0跟任何数相与的话,结果都是0
- 即,将前4位置0
- 即,前4位bit的数无意义了
packet[0]&0x0f*4?为什么最后乘以4呢?
头部长度占用4bit位,这里我们假设用每个bit位代表4个字节
那么,用二进制表示,4bit最小值是0b0000
4bit最大值是0b1111,即15
转换成字节数是15*4=60个字节
即,头部长度最多占用60个字节。
7.4.3、printAllLenIPv4函数说明 |
该函数是打印一个IP报文的长度;这里的长度包括IP头部长度+具体数据长度
uint16(packet[2])<<8|uint16(packet[3]) 什么意思呢?
首先,观察IP报文结构图中,显示IP报文总长度占用的第3个字节packet[2],以及第4个字节packet[3];
在golang中byte是uint8的别名,取值范围是0~255
假设当前IP报文的总长度是1000,已经超过byte的最大存储值255了, 一个字节已经存储不下了,
此时,可以使用两个字节进行存储。
将1000转换为二进制0000001111101000,可以
- 将前8个bit,即00000011存储到packet[2]里,
- 将后8个bit,即11101000存储到packet[3]里
这样的话,就可以存储起来了。
现在,过程反过来了;如何通过读取packet[2], packet[3]的值来获得1000?
我们需要将packet[2],packet[3]转换成二进制,并且将packet[2]放到packet[3]前面,因此,涉及到packet[2]的移位操作;
因为packet[2]的类型byte,一共才占用8位,因此,如果直接读取packet[2]的值,向左移动8的话,会导致数据丢失的;
因此,需要先将packet[2]的值,强制转换为uint16,再向左移动8位;
即,00000011->0000000000000011->0000001100000000
uint16类型不能uint8类型进行异或操作,位数不对。
因此,也必须将packet[3]强制转换为uint16
最终变为uint16(packet[2])<<8|uint16(packet[3])
7.4.4、printDstIPv4函数说明 |
获取IP的两种方式
8、TCP报文 |
8.1、TCP数据段封装过程 |
TCP报文头同样有两部分组成:
- 固定部分,占20个字节
- 可变长部分,占0~40个字节
8.2、TCP报文头的结构如下 |
每个属性的具体含义,可以参考下面的网址:
https://blog.csdn.net/marywang56/article/details/76151064
8.3、使用golang解析TCP报文头部结构 |
8.3.1、分析main.go文件中的tapToUDP函数 |
测试用例1的client包下的main.go文件里
8.3.2、分析tcpheader.go文件 |
主要分析一下
TCP报文头中的CWR、ACK、FIN这三个属性,如何获取进行分析;以及如何获取TCP报文中的数据部分进行简单分析一下。
其他属性的获取方式,跟以前很类似,不再过多介绍了。
8.3.2.1、printTCPFlagFIN函数说明 |
8.3.2.2、printTCPFlagCWR函数说明 |
CWR占用的是packet[13]字节中的第1个bit位,需要将剩余的属性全部置为0,
即,packet[13]&0x80
为什么还要向右移动7位呢?
8.3.2.3、printTCPFlagACK函数说明 |
8.3.2.4、printTCPData函数说明 |
8.4、测试 |
提供一个简单的tcp测试用例,抓包分析,
然后跟Wireshark软件进行对比分析
查看,我们golang解析的是否正确
8.4.1、启动helloworld级别的点对点VPN服务 |
将前一篇文章中的测试用例1启动即可。
前面文章中,已经说明了启动方式。
8.4.2、TCP测试用例 |
8.4.2.1、TCP服务器端代码 |
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
const (
ip = "10.244.3.3"
port = 9898
)
func main() {
l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", ip, port))
if err != nil {
fmt.Println("listen error:", err)
os.Exit(1)
}
defer l.Close()
fmt.Printf("listening on :%v", fmt.Sprintf("%s:%d", ip, port))
for {
conn, err := l.Accept()
if err != nil {
fmt.Println("accept error:", err)
os.Exit(1)
}
fmt.Printf("message %s->%s\n", conn.RemoteAddr(), conn.LocalAddr())
go handleRequest(conn)
}
}
func handleRequest(conn net.Conn) {
ip := conn.RemoteAddr().String()
defer func() {
fmt.Println("disconnect:" + ip)
conn.Close()
}()
reader := bufio.NewReader(conn)
writer := bufio.NewWriter(conn)
for {
b, _, err := reader.ReadLine()
if err != nil {
return
}
writer.Write([]byte(strings.ToUpper(string(b))))
writer.Write([]byte("\n"))
writer.Flush()
}
}
TCP服务器端,只是将接收到的内容,转换为大写而已。
反馈给客户端。
8.4.2.2、TCP客户端代码 |
package main
import (
"bufio"
"fmt"
"net"
"os"
"sync"
)
const (
ip = "10.244.3.3"
port = 9898
)
func main() {
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", ip, port))
if err != nil {
fmt.Println("connect error", err)
os.Exit(1)
}
defer conn.Close()
var wg sync.WaitGroup
wg.Add(2)
go handleWrite(conn, &wg)
go handleRead(conn, &wg)
wg.Wait()
}
func handleWrite(conn net.Conn, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 1; i++ {
writer := bufio.NewWriter(conn)
writer.Write([]byte("hello456"))
writer.Write([]byte("\n"))
writer.Flush()
}
fmt.Println("write done")
}
func handleRead(conn net.Conn, wg *sync.WaitGroup) {
defer wg.Done()
reader := bufio.NewReader(conn)
for i := 0; i < 1; i++ {
line, _, err := reader.ReadLine()
if err != nil {
fmt.Println("read error", err)
return
}
fmt.Printf("Read Msg From Tcp Server:%v\n", string(line))
}
fmt.Println("read done")
}
注意:
tcp客户端请求内容是
"hello456"
在使用golang进行解析时,查看一下是否是"hello456"
8.4.2.3、本地编译,上传到10.211.55.122,10.211.55.123节点上去 |
Makefile参考内容如下
build:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o tcpclient ./client/main.go
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o tcpserver ./server/main.go
scp:
scp tcpclient root@10.211.55.122:/root
scp tcpserver root@10.211.55.123:/root
all:
make build && make scp
在本地执行
make all
即可
8.4.2.4、登陆到10.211.55.122节点上, 进行抓包 |
tcpdump -nn -i tap99 -w tcp.pcap
8.4.2.5、登陆到10.211.55.123节点上,启动TCP服务器端服务 |
./tcpserver
8.4.2.6、登陆到10.211.55.122节点上,启动TCP客户端服务 |
./tcpclient
日志显示,已经将请求的内容,转换为了大写,说明确实是服务器端反馈的。
8.4.2.7、将抓包结果上传到wireshark里查看 |
8.4.2.8、在10.211.55.122节点上查看一下vpn客户端日志 并跟 Wireshark里解析的数据包进行对比 |
9、UDP报文介绍 |
9.1、UDP报文封装过程介绍 |
UDP报文的封装过程,跟TCP是一样的。
只是UDP的报文结构跟TCP不同。
9.2、UDP报文结构介绍 |
很明显,UDP的报文结构比TCP简单多了。
UDP报文头固定占用8个字节。
源端口
- 源端口:
- 主机的应用程序使用的端口号。
- 目的端口:
- 目的主机的应用程序使用的端口号。
- 总长度:
- 是指UDP头部和UDP数据的字节长度。
- 因为UDP头 部长度为8字节,所以该字段的最小值为8。
- 也就是说,数据部分可以为空
- 校验和:
- 检测UDP数据报在传输中是否有错,有错则丢弃。
9.3、使用golang解析UDP报文头部结构 |
9.3.1、main.go文件里 |
先看一下前一篇文章中的测试用例1中的客户端代码main.go文件里:
9.3.2、udpheader.go文件里 |
解析原理,跟以前的一样。不再过多解析了。
9.4、测试 |
提供一个简单的udp测试用例,抓包分析,
然后跟Wireshark软件进行对比分析
查看,我们golang解析的是否正确
9.4.1、启动helloworld级别的点对点VPN服务 |
将前一篇文章中的测试用例1启动即可。
前面文章中,已经说明了启动方式。
如果已经启动了,忽略即可。
9.4.2、UDP测试用例 |
其实,tcp测试用例,udp测试用例的逻辑不用心。
我们只是抓包分析。
9.4.2.1、UDP服务器端代码 |
package main
import (
"fmt"
"net"
"time"
)
const ip = "10.244.3.3"
func main() {
udpAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", ip, 8989))
if err != nil {
fmt.Println("Err resolve UDP address: ", err)
return
}
serverConn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
fmt.Println("ListenUDP error: ", err)
return
}
var ticker = time.Tick(time.Second * 2) // 每隔2秒钟发送一个数据
for {
for _ = range ticker {
var buff [512]byte
n, rAddr, err := serverConn.ReadFromUDP(buff[0:])
if err != nil {
fmt.Println("Read error: ", err)
break
}
fmt.Println("Read from client: ", string(buff[:n]))
// 如果使用Write,本地测试时客户端接收不到信息
serverConn.WriteToUDP([]byte("Hello client"), rAddr)
}
}
}
9.4.2.2、UDP客户端代码 |
package main
import (
"fmt"
"net"
)
const ip = "10.244.3.3"
func main() {
udpAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", ip, 8989))
if err != nil {
fmt.Println("Err resolve UDP address: ", err)
return
}
conn, err := net.DialUDP("udp", nil, udpAddr)
if err != nil {
fmt.Println("Dial UDP error: ", err)
return
}
size, err := conn.Write([]byte("Hello UDP Server"))
if err != nil {
fmt.Printf("Send Msg Failed! error: %v\n", err.Error())
return
}
fmt.Printf("Send Msg OK!size:%d\n", size)
var buff [512]byte
n, err := conn.Read(buff[0:])
if err != nil {
fmt.Println("ERR: ", err)
}
fmt.Printf("Read from server: %v\tlen(msg):%v\n", string(buff[:n]), n)
//for {
// conn.Write([]byte("Hello server"))
// var buff [512]byte
// n, err := conn.Read(buff[0:])
// if err != nil {
// fmt.Println("ERR: ", err)
// break
// }
// fmt.Println("Read from server: ", string(buff[:n]))
//}
}
9.4.2.3、本地编译,上传到10.211.55.122,10.211.55.123节点上去 |
Makefile参考内容,如下(一定要改成自己的)
build:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o udpclient ./client/main.go
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o udpserver ./server/main.go
scp:
scp udpclient root@10.211.55.122:/root
scp udpserver root@10.211.55.123:/root
all:
make build && make scp
9.4.2.4、登陆到10.211.55.122节点上, 进行抓包 |
tcpdump -nn -i tap99 -w udp.pcap
9.4.2.5、登陆到10.211.55.123节点上,启动UDP服务器端服务 |
./udpserver
9.4.2.6、登陆到10.211.55.122节点上,启动UDP客户端服务 |
./udpclient
9.4.2.7、将抓包结果上传到wireshark里查看 |
9.4.2.8、在10.211.55.122节点上查看一下vpn客户端日志 并跟 Wireshark里解析的数据包进行对比 |
10、ICMP报文 |
10.1、ICMP封包过程介绍 |
10.2、ICMP报文结构介绍 |
ICMP报文存在多个类型,不同的类型,ICMP头部属性并不相同。
这里画的是TYPE为8的情况下。
ICMP报文的类型,可以参考下面的文章
https://www.51cto.com/article/207507.html
http://t.zoukankan.com/linfeng-learning-p-12511225.html
https://blog.csdn.net/cixieku3433/article/details/100353399/
10.3、使用golang解析UDP报文头部结构 |
10.3.1、main.go文件里 |
10.3.2、icmpheader.go文件里 |
10.4、测试 |
本次测试的ICMP协议,因此,直接使用ping命令即可,
同样,使用Wireshark软件对ICMP报文进行分析
查看,我们使用golang解析的ICMP报文是否正确
10.4.1、启动helloworld级别的点对点VPN服务 |
将前一篇文章中的测试用例1启动即可。
前面文章中,已经说明了启动方式。
如果已经启动了,忽略即可。
10.4.2、登录到10.211.55.122客户端上 |
10.4.2.1、设置抓包命令 |
tcpdump -nn -i tap99 -w icmp.pcap
10.4.2.2、使用ping命令创建ICMP数据包 |
在10.211.55.122节点上ping 10.211.55.123节点上的tap99虚拟网络设备
ping -c 1 10.244.3.3 -I tap99
10.4.2.3、将抓包结果上传到wireshark里查看 |
10.4.2.4、在10.211.55.122节点上查看一下vpn客户端日志 并跟 Wireshark里解析的数据包进行对比 |
11、ARP报文 |
11.1、ARP报文封装过程 |
注意:
ARP报文并非是使用IP协议进行封装的。
而是,直接封装到以太网帧里。
11.2、ARP报文结构介绍 |
ARP报文跟其他协议报文还是有很大的区别的。
注意到了没,ARP报文是没有头部结构这一说的。
ARP报文的属性,以及占用的字节都已经固定好了。
11.3、使用golang解析arp报文结构 |
11.3.1、main.go文件里 |
11.3.2、arpheader.go文件里 |
不再介绍了,原理跟解析其他报文完全一样。
很清晰。
11.4、测试 |
本次测试的arp协议;
根据上面的ARP报文原理,我们已经知道为什么会产生了ARP报文了,
因此,我们可以继续使用ping来测试。
11.4.1、启动helloworld级别的点对点VPN服务 |
将前一篇文章中的测试用例1启动即可。
前面文章中,已经说明了启动方式。
如果已经启动了,忽略即可。
11.4.2、登录到10.211.55.122客户端上 |
11.4.2.1、设置抓包命令 |
tcpdump -nn -i tap99 -w icmp.pcap
11.4.2.2、使用ping命令创建ICMP数据包 |
在10.211.55.122节点上ping 10.211.55.123节点上的tap99虚拟网络设备
ping -c 1 10.244.3.3 -I tap99
11.4.2.3、将抓包结果上传到wireshark里查看 |
11.4.2.4、在10.211.55.122节点上查看一下vpn客户端日志 并跟 Wireshark里解析的数据包进行对比 |
12、总结 |
本篇文章主要是介绍了一下ICMP、TCP、UDP、ARP协议的封装过程,以及如何通过golang去解析报文结构。
TCP、UDP、ICMP协议的整体封装过程是一样的;都是封装在IP协议里;再将IP协议封装到以太网帧中。
而ARP协议是直接封装到以太网帧中的。
13、参考文档 |
<<零入门kubernetes网络实战>>技术专栏之文章目录