零入门kubernetes网络实战-22->基于tun设备实现在用户空间可以ping通外部节点(golang版本)

news2024/11/28 8:28:26

《零入门kubernetes网络实战》视频专栏地址
https://www.ixigua.com/7193641905282875942

本篇文章视频地址(稍后上传)


本篇文章主要是想做一个测试:
实现的目的是

  • 希望在宿主机-1上,在用户空间里使用ping命令发起ping请求,产生的icmp类型的数据包经过tun类型的虚拟网络设备转发,能够ping通宿主机-2上的对外物理网卡eth0;
  • 不仅能够发送请求,还能接收到反馈信息;
  • 实现的效果跟普通的ping请求效果一样;数据包有去有回。

1、原理图

实现原理图,如下图所示:

在这里插入图片描述

该图主要分为两下两部分:

  • 上面部分是宿主机-1里的流程图,
  • 下面部署是宿主机-2里的流通图。

我们要实现的目的是:

  • 在宿主机-1上,发起ping命令请求,使用虚拟网络设备tun19发起;
  • 希望能够ping通宿主机-2上的对外的物理网卡eth0;

分在两条线:

  • 黑色实线是ping请求命令数据包走的路线
  • 黑色虚线是ping反馈数据包走的路线

在宿主机-1里,又分了用户空间和内核空间两部分。

tun-driver就是咱们要编写的程序;

  • 该程序会创建虚拟网络设备tun19,并配置了IP信息。
  • 该程序内部,存在三个组件吧:
    • icmpConn,表明,这是传输icmp类型数据包的链接,类似于tcp链接,udp链接。这是icmp链接。
    • imcpTotun,表明,消息是从icmp链接里读取,将读取的消息发送到tun里,即发送到/dev/net/tun文件描述符里
    • tunToimcp,表明,消息是从文件描述符/dev/net/tun里读取的,将读取的消息发送到icmp链接里的

到此为止,我们介绍了我们要实现的目的是什么,介绍了原理图,数据包的走向,以及我们实现的程序都做了哪些事情;

接下来,看一下我们的代码:

2、方案说明

本次测试,我打算采用两种试验方式;

  • 方案一:先单独创建tun虚拟网络设备(tun19),然后,在编写tun-driver程序
  • 方案二:将创建tun虚拟网络设备的程序(tun19),集成到tun-driver程序里。(其实,就是在方案一的基础上,代码整合了一下)

想把我学习tun设备的过程,给大家复盘一下,

代码不是一开始就写正确的,总得有个调试的过程。

刚好方案一,遇到了一些问题。给大家分享一下。

当然,你可以直接跳过方案一,直接看方案二。

3、方案一:

3.1、创建tun设备的代码

package main

import (
	"github.com/vishvananda/netlink"
)

const tunName = "tun19"

func main() {
	la := netlink.LinkAttrs{
		Name:  tunName,
		Index: 8,
		MTU:   1500,
	}
	tun := netlink.Tuntap{
		LinkAttrs: la,
		Mode:      netlink.TUNTAP_MODE_TUN,
	}

	l, err := netlink.LinkByName(tunName)
	if err == nil {
		// 先将tun虚拟网络设备Down掉
		netlink.LinkSetDown(l)

		// 将tun虚拟网络设备删掉
		netlink.LinkDel(l)
	}

	// 每次创建新的tun设备
	err = netlink.LinkAdd(&tun)
	if err != nil {
		panic(err)
	}

	l, err = netlink.LinkByName(tunName)

	ip, err := netlink.ParseIPNet("10.244.2.2/24")
	addr := &netlink.Addr{IPNet: ip, Label: ""}
	if err = netlink.AddrAdd(l, addr); err != nil {
		panic(err)
	}

	err = netlink.LinkSetUp(l)
	if err != nil {
		panic(err)
	}
}

这段代码,就是前文介绍的。

在本地编译,上传到测试服务器上:

build:
	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go

scp:
	scp main root@10.211.55.122:/root

all:
	make build && make scp

执行

make all

登录到远程服务器上

ip a s | grep eth0

ip link sh tun19

./main

ip link sh tun19

ip a sh tun19

在这里插入图片描述

3.2、tun-driver 代码原理介绍

3.2.1、golang代码

package main

import (
	"bytes"
	"encoding/binary"
	"flag"
	"fmt"
	"golang.org/x/net/icmp"
	"golang.org/x/net/ipv4"
	"net"
	"os"
	"syscall"
	"time"
	"unsafe"
)

const (
	tunDevice  = "/dev/net/tun"
	ifnameSize = 16
)

type ifreqFlags struct {
	IfrnName  [ifnameSize]byte
	IfruFlags uint16
}

func ioctl(fd int, request, argp uintptr) error {
	_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), request, argp)
	if errno != 0 {
		fmt.Errorf("ioctl failed with '%s'\n", errno)
		return fmt.Errorf("ioctl failed with '%s'", errno)
	}
	return nil
}

func fromZeroTerm(s []byte) string {
	return string(bytes.TrimRight(s, "\000"))
}

func OpenTun(name string) (*os.File, string, error) {
	tun, err := os.OpenFile(tunDevice, os.O_RDWR, 0)
	if err != nil {
		fmt.Printf("OpenTun Failed! err:%v", err.Error())
		return nil, "", err
	}
	var ifr ifreqFlags
	copy(ifr.IfrnName[:len(ifr.IfrnName)-1], []byte(name+"\000"))
	ifr.IfruFlags = syscall.IFF_TUN | syscall.IFF_NO_PI

	err = ioctl(int(tun.Fd()), syscall.TUNSETIFF, uintptr(unsafe.Pointer(&ifr)))
	if err != nil {
		fmt.Printf("OpenTun Failed! err:%v\n", err.Error())
		return nil, "", err
	}

	ifName := fromZeroTerm(ifr.IfrnName[:ifnameSize])
	return tun, ifName, nil
}

var tunName string

func checkSum(data []byte) uint16 {
	var (
		sum    uint32
		length int = len(data)
		index  int
	)
	for length > 1 {
		sum += uint32(data[index])<<8 + uint32(data[index+1])
		index += 2
		length -= 2
	}
	if length > 0 {
		sum += uint32(data[index])
	}
	sum += sum >> 16

	return uint16(^sum)
}

func main() {
	flag.StringVar(&tunName, "tunName", "tun19", "Use -tunName xxx")
	flag.Parse()

	var err error

	tunFile, _, err := OpenTun(tunName)
	if err != nil {
		fmt.Printf("ICMP Listen Packet Failed! err:%v\n", err.Error())
		return
	}
	defer tunFile.Close()

	icmpconn, _ := icmp.ListenPacket("ip4:icmp", "0.0.0.0")

	defer icmpconn.Close()
	var srcCh = make(chan string, 1)

	go tunToicmp(icmpconn, tunFile, srcCh)

	go icmpToTun(icmpconn, tunFile, srcCh)

	time.Sleep(time.Hour)
}

func tunToicmp(icmpconn *icmp.PacketConn, tunFile *os.File, srcCh chan string) {
	var srcIP string
	packet := make([]byte, 1024*64)
	size := 0
	var err error
	for {
		if size, err = tunFile.Read(packet); err != nil {
			return
		}
		fmt.Printf("Msg Length: %d\n", binary.BigEndian.Uint16(packet[2:4]))
		fmt.Printf("Msg Protocol: %d (1=ICMP, 6=TCP, 17=UDP)\tsize:%d\n", packet[9], size)
		b := packet[:size]
		srcIP = GetSrcIP(b)
		srcCh <- srcIP
		dstIP := GetDstIP(b)
		fmt.Printf("Msg srcIP: %s\tdstIP:%v\n", srcIP, dstIP)

		var raddr = net.IPAddr{IP: net.ParseIP(dstIP)}

		b = b[20:size]

		if size, err = icmpconn.WriteTo(b, &raddr); err != nil {
			fmt.Println(err.Error())
			return
		}
		fmt.Printf("Send ICMP Packet Success OK! size:%d\n", size)
	}
}

func icmpToTun(icmpconn *icmp.PacketConn, tunFile *os.File, srcCh chan string) {

	var sb = make([]byte, 1024*64)
	var addr net.Addr
	var size int
	var err error

	for {
		if size, addr, err = icmpconn.ReadFrom(sb); err != nil {
			continue
		}

		srcIP := <-srcCh

		ipHeader := createIPv4Header(net.ParseIP(addr.String()), net.ParseIP(srcIP), os.Getpid())
		iphb, err := ipHeader.Marshal()
		if err != nil {
			continue
		}
		fmt.Printf("Reply MSG Length: %d\n", binary.BigEndian.Uint16(iphb[2:4]))
		fmt.Printf("Reply MSG Protocol: %d (1=ICMP, 6=TCP, 17=UDP)\n", iphb[9])
		dstIP := GetDstIP(iphb)
		fmt.Printf("Reply src IP: %s\tdstIP:%v\n", addr, dstIP)

		var tunb = make([]byte, 84)
		tunb = append(iphb, sb[:size]...)

		size, err = tunFile.Write(tunb)
		if err != nil {
			continue
		}
		fmt.Printf("Reply MSG To Tun OK! size:%d\n", size)
	}
}

func createIPv4Header(src, dst net.IP, id int) *ipv4.Header {

	iph := &ipv4.Header{
		Version:  ipv4.Version,
		Len:      ipv4.HeaderLen,
		TOS:      0x00,
		TotalLen: ipv4.HeaderLen + 64,
		ID:       id,
		Flags:    ipv4.DontFragment,
		FragOff:  0,
		TTL:      64,
		Protocol: 1,
		Checksum: 0,
		Src:      src,
		Dst:      dst,
	}

	h, _ := iph.Marshal()

	iph.Checksum = int(checkSum(h))

	return iph
}

func IsIPv4(packet []byte) bool {
	flag := packet[0] >> 4
	return flag == 4
}

func GetIPv4Src(packet []byte) net.IP {
	return net.IPv4(packet[12], packet[13], packet[14], packet[15])
}

func GetIPv4Dst(packet []byte) net.IP {
	return net.IPv4(packet[16], packet[17], packet[18], packet[19])
}

func GetSrcIP(packet []byte) string {
	key := ""
	if IsIPv4(packet) && len(packet) >= 20 {
		key = GetIPv4Src(packet).To4().String()
	}

	return key
}

func GetDstIP(packet []byte) string {
	key := ""
	if IsIPv4(packet) && len(packet) >= 20 {
		key = GetIPv4Dst(packet).To4().String()
	}

	return key
}

接下来,对代码进行分析

3.2.2、 main流程分析

在这里插入图片描述

3.2.3、tunToicmp流程分析

3.2.3.1、主要流程说明

第111-133行:内部死循环的执行任务。
那么,任务的主要过程是:
在这里插入图片描述

3.2.3.2、IP报文头结构说明

看一下112行,作用是从/dev/net/tun文件描述符里读取数据到packet切片里

而packet属于字节切片

前面文章已经介绍过了,从/dev/net/tun文件里读取的数据,tun设备默认会给原数据包添加一个IP报文头;

如下形式:
在这里插入图片描述

那么, 接下来的问题就是,如何解析IP报文头?

从IP报文头里获取我们想要的信息,如ICMP数据包的目的地址是哪里?

IP报文头的结构形式,如下所示:

在这里插入图片描述

  • IP报文头一共20个字节,不包括可选项
  • 这20个字节,分为了5行来说明、解释(行与行之间的空白行不算)
  • 一个byte字节=8bit,每一行都是4个字节,32bit
  • 看第一行第一个小框,
  • 版本占用了4bit,当值为4时,即代表IPv4
  • 头部长度占用了4bit,值为4字节的倍数,4bit位的全部是1的话,也就是15,因此,头部长度占用的最大字节数是15*4=60字节;
    • 比方说,二进制0101,占用了4bit,十进制是5,当前IP报文头占用的字节数是5*4=20个字节。
  • 版本和头部长度合起来,就是第0个字节,即packet[0]
  • packet[1],表示服务类型;
  • packet[2], packet[3]:总长
  • packet[4], packet[5]:表示标识
  • packet[6], packet[7]: 表示标记和分段偏移
  • packet[8]: 表示 生命周期
  • packet[9]: 表示 协议(如,ICMP,tcp, udp)
  • packet[10], packet[11]:表示头部校验和
  • packet[12], packet[13], packet[14], packet[15]:表示 源地址,源IP
  • packet[16], packet[17], packet[18], packet[19]:表示 目的地址,目的IP

从IP报文头结构中,我们可以获取到目的地址,即packet[16], packet[17], packet[18], packet[19]

代码中的115行,116行,118行非必须代码,是为给大家演示一下,才写的。

3.2.3.3、通过icmp连接将数据发送到目的地址

  • 看117行:size表示从/dev/net/tun里读取了多少字节,
    • 通过这条语句获取到有效的数据
  • 看119行:首先说明这条语句非必须的。
    • 在后续的版本的中,已经删除了。(代码非一次性写好的)
    • 因为把icmp数据包发送给了另一个宿主机上的eth0,eth0会反馈消息到icmpTotun函数里,
    • 在此函数里,需要将从icmp链接里接收到的数据添加上一个自定义的IP报文头,
    • 写到/dev/net/tun文件里,此时,需要知道srcIP;因此,在这里传输过去的。
    • 但是一般情况下,srcIP就是tun19的IP,是固定死的。因此,没有必要写。

到目前为止,我们已经将数据从tun19设备里发送到了另一台宿主机的对外网卡eth0上了。

3.2.4、icmpTotun流程分析

3.2.4.1、主流程分析

整体看一下这块代码,就是一个for循环,在死循环的执行逻辑;

主要逻辑,如下图所示:

在这里插入图片描述

3.2.4.2、从ICMP链接里读取消息

  • 看143行: 从ICMP链接里读取另一个宿主机eth0反馈的ping的消息,
    • 将读取到的消息存储到sb字节切片里
    • 这个消息肯定是ICMP的回包,就不需要解析。ICMP的回包中,type为0,表明是ICMP的回包。

3.2.4.3、自定义构建IP报文头

什么情况下构建IP报文头和什么情况下不需要构建呢?
从两个方面说,一个是虚拟网卡tun19,一个是/dev/net/tun文件描述符;而且跟数据包的走向有关系;如下:

  • 如果数据包从tun19网卡进入的话,不需要自己添加IP报文头,tun19网卡内部会自动添加的
  • 如果数据包从tun19网卡出去的话,tun19网卡内部会自动删除IP报文头
  • 如果数据包从/dev/net/tun文件描述符里读取数据的话,读取到的数据包里已经包含IP报文头了
  • 如果数据包写入/dev/net/tun文件描述符的话,写入之前需要给数据包添加上IP报文头才才能写入

详细的看代码:

  • 看下147行:就是获取源IP,本条语句非必须,在后续的版本中,已经删除了。
  • 看下149-153行:自定义构建了一个IP报文头:初始化了版本号,协议,源IP,目的IP等等
  • 看154,155行:这两个非必须的,只是为了给大家演示,添加的。可以删除
  • 看156行:获取目的IP,即获取的是tun设备的IP,tun19的IP

3.2.4.4、重新组合数据包

将143行的切片跟150行的自定义IP报文头进行组合,将报文头放到切片的首部;

组成成新的回复数据包。

3.2.4.5、将数据包写入到/dev/net/tun里

将最新组合而成的切片写入到/dev/net/tun文件描述符里。

即tun19虚拟网卡就收到另一个宿主机eth0回馈的数据包了。

3.3、本地编译,上传到服务器

接下来,在本地环境Mac上编译下,上传到测试服务器上
Makefile内容

build:
	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go

scp:
	scp main root@10.211.55.122:/root

all:
	make build && make scp

执行

make all

在这里插入图片描述

3.4、服务器上测试

分别打开两个shell终端,如下图所示:

  • 左侧的shell终端,执行tun-driver程序
  • 右侧的shell终端,执行ping 10.211.55.123 -I tun19命令

看一下运行结果:

  • 左侧打印了出了信息
  • 右侧缺没有任何信息,然后,ctrl+c终端测试,发现ping一共发送了5个数据包,但是,全部丢失。

在这里插入图片描述

再次,查看一下tun19的网卡情况
在这里插入图片描述

3.5、问题分析

通过测试我们发现执行此命令ping 10.211.55.123 -I tun19的时候
没有反馈结果,
那么,分析的思路是:

  • 先排查虚拟网卡tun19、对外的物理网卡eth0,以及另一个宿主机上的eth0网卡是否收到相应的包
  • 排查tun-driver程序是否有问题,打印日志是否符合要求
  • 排查iptables防火墙规则是否满足条件
    • 是否存在DROP策略
    • 转发规则是否打开了?(经测试,这个参数为0也可以的;默认值为1)
      • more /proc/sys/net/ipv4/ip_forward
      • 查看结果是否为1

好,接下来,先抓包分一下

3.5.1、抓包

通过抓包,来判断一下,网卡是否收到数据;如果没有收到数据肯定有问题。

3.5.1.1、分析tun19网卡是否有问题

针对tun19网卡有两种方式

3.5.1.1.1、方式一:可以查看tun19网卡的统计信息来判断

通过如下命令:

ifconfig -v tun19

查看TX,RX是否有变化

在这里插入图片描述

3.5.1.1.2、方式二:可以针对tun19网卡进行抓包来分析
tcpdump -nn icmp -i tun19

在这里插入图片描述

其实,在测试的初期是只有请求数据包,并没有回复数据包,只是不知道如何回复到当时的场景了。

3.5.1.2、分析10.211.55.122节点上的对外网卡eth0是否收到数据

tcpdump -nn icmp -i eth0

在这里插入图片描述

3.5.1.3、分析10.211.55.123节点上的对外网卡eth0是否收到数据

其实,通过上面的抓包,已经说明了,

123节点上的eth0是可以接收到ping命令的请求的,并且此网卡也对ping命令进行回复。

我们可以抓一下,并且通过wireshark简单看一下:

tcpdump -nn icmp -i eth0 -w icmp.pcap

在这里插入图片描述

icmp.pcap用WireShark打开

  • ping命令请求数据包
    在这里插入图片描述
  • ping命令回复数据包分析
    在这里插入图片描述

整个测试试验中,涉及到了三个网卡:
122节点上的虚拟网卡tun19,eth0,以及123节点上的eth0网卡

通过抓包我们可以看出来,这三个网卡都可以正常接收到数据包,应该是没有问题的

但是,执行ping 10.211.55.123 -I tun19的时候,确实没有返回值

其实,查看tun19网卡的时候,已经说明了,tun19网卡可以正常接收到123节点的反馈的数据包

也就是说,反馈的数据包从tun19网卡出来后,ping没有收到?

是这一段线路出了问题。

数据包从网卡到应用程序之间的路径是由主机防火墙来规定的。

有可能是防火墙规则给限制住了。

因此,接下来,只能查看防火墙了。

3.5.2、iptables防火墙规则分析

通过对tun19虚拟网卡进行抓包,发现:

tun19网卡已经接收到反馈的数据包了,

接下来,看一下:数据包流向图:

在这里插入图片描述

问题如下:
在这里插入图片描述

涉及到两个链:

  • PREROUTING
  • INPUT

涉及到表

  • raw表
  • mangle表
  • nat表
  • filter表

接下来,可以依次查看响应的链。
为了节省篇幅,不再一一展示了。
直接添加日志。
添加日志方式

iptables -nvL -t raw 

iptables -t raw -A PREROUTING -p icmp -j TRACE

iptables -t raw -A OUTPUT -p icmp -j TRACE

iptables -nvL -t raw 

在这里插入图片描述

按照数据包的走向,依次根据表查看规则链

tail -f /var/log/messages | grep raw | grep PREROUTING | grep DST=10.244.2.2

在这里插入图片描述

注意:打印日志可能不会马上显示,可能需要等待10秒左右
tail -f /var/log/messages | grep mangle | grep PREROUTING | grep DST=10.244.2.2

tail -f /var/log/messages | grep nat | grep PREROUTING | grep DST=10.244.2.2

tail -f /var/log/messages | grep mangle | grep INPUT | grep DST=10.244.2.2

tail -f /var/log/messages | grep nat | grep INPUT | grep DST=10.244.2.2

tail -f /var/log/messages | grep filter | grep INPUT | grep DST=10.244.2.2

其实,通过上面的命令去查询的话,也不是很靠谱的。

可能是这样的,一个数据包要不要经过某个表的某个链

是根据这个数据包自带的包状态来判断的。

比方说,这个数据包没有涉及到nat功能,可能不会经过nat表,因此,查询nat表时,就不会有响应的日志。

可能raw表,filter会必须经过的。

有点遗憾,感觉没有通过iptables日志完全查出来是什么原因,但至少也进一步定位了问题所在。

数据包从tun19网卡出来后,经过了raw表。

在后续的表中,出了问题。

3.5.3、反向路由校验

在即将放弃的时候,突然想起了一个内核参数net.ipv4.conf.all.rp_filter
尝试的修改了几次,发现OK了!

3.5.3.1、什么是反向路由校验

所谓反向路由校验,

  • 就是在一个网卡收到数据包后,

  • 把源地址和目标地址对调后查找路由出口,

  • 从而得到反身后路由出口。

  • 然后根据反向路由出口进行过滤。

  • 当rp_filter的值为1时,

    • 要求反向路由的出口必须与数据包的入口网卡是同一块,否则就会丢弃数据包。
  • 当rp_filter的值为2时,

    • 要求反向路由必须是可达的,如果反路由不可达,则会丢弃数据包。

3.5.3.2、rp_filter (Reverse Path Filtering)参数介绍

  • 定义了网卡对接收到的数据包进行反向路由验证的规则。
  • 存在三个值:0、1、2,具体含意如下:
    • 0:关闭反向路由校验
    • 1:开启严格的反向路由校验。
      • 对每个进来的数据包,校验其反向路由是否是最佳路由
      • 如果反向路由不是最佳路由,则直接丢弃该数据包。
    • 2:开启松散的反向路由校验。
      • 对每个进来的数据包,校验其源地址是否可达,即反向路由是否能通(通过任意网口),
      • 如果反向路径不通,则直接丢弃该数据包。

默认值应该是1,严格进行反向路由校验

具体可以参考下面的网址
Linux内核参数 rp_filter

3.5.3.3、ping不通的原因可能是

仅仅是猜测:
数据包经过了两个网卡,一个是虚拟网卡tun19, 一个是eth0
当对数据包进行反向路由校验时,从tun19网卡到eth0网卡失败了,它校验的时候,可能没有添加IP报文头。

3.6、设置反向路由校验,重新测试

3.6.1、设置反向路由校验参数

既然问题原因已经找到了,设置参数

sysctl -w net.ipv4.conf.all.rp_filter=2

sysctl net.ipv4.conf.all.rp_filter

在这里插入图片描述

3.6.2、重新测试

ping 10.211.55.123 -I tun19

在这里插入图片描述

再次,查看iptables日志

tail -f /var/log/messages | grep DST=10.244.2.2

在这里插入图片描述

从上面的日志中可以看出来

即使在ping通的情况下,数据包也只经过了raw表,filter表
RPEROUTING链,以及INPUT链。

可能没有涉及到nat、mangle功能,因此没有nat、mangle相关日志。

3.6.3、为什么会存在大量的DUP!呢?

如果你去查百度的话,会查到一堆。

最后,在测试时无意中发现是代码的问题。

在这里插入图片描述

重新测试

在这里插入图片描述

好,到这里我们的试验目标已经完全成功了。

其实,测试用了好长时间。结局还是圆满的。

4、方案二

4.1、golang代码

package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"github.com/vishvananda/netlink"
	"golang.org/x/net/icmp"
	"golang.org/x/net/ipv4"
	"net"
	"os"
	"syscall"
	"time"
	"unsafe"
)

const (
	tunName    = "tun19"
	tunDevice  = "/dev/net/tun"
	ifnameSize = 16
	eth0       = "10.211.55.122"
	tunIP      = "10.244.1.3"
)

type ifreqFlags struct {
	IfrnName  [ifnameSize]byte
	IfruFlags uint16
}

func ioctl(fd int, request, argp uintptr) error {
	_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), request, argp)
	if errno != 0 {
		fmt.Errorf("ioctl failed with '%s'\n", errno)
		return fmt.Errorf("ioctl failed with '%s'", errno)
	}
	return nil
}

func fromZeroTerm(s []byte) string {
	return string(bytes.TrimRight(s, "\000"))
}

func OpenTun(name string) (*os.File, string, error) {
	tun, err := os.OpenFile(tunDevice, os.O_RDWR|syscall.O_NONBLOCK, 0)
	if err != nil {
		fmt.Printf("OpenTun Failed! err:%v", err.Error())
		return nil, "", err
	}
	var ifr ifreqFlags
	copy(ifr.IfrnName[:len(ifr.IfrnName)-1], []byte(name+"\000"))
	ifr.IfruFlags = syscall.IFF_TUN | syscall.IFF_NO_PI

	err = ioctl(int(tun.Fd()), syscall.TUNSETIFF, uintptr(unsafe.Pointer(&ifr)))
	if err != nil {
		fmt.Printf("OpenTun Failed! err:%v\n", err.Error())
		return nil, "", err
	}

	ifName := fromZeroTerm(ifr.IfrnName[:ifnameSize])
	return tun, ifName, nil
}

func checkSum(data []byte) uint16 {
	var (
		sum    uint32
		length int = len(data)
		index  int
	)
	for length > 1 {
		sum += uint32(data[index])<<8 + uint32(data[index+1])
		index += 2
		length -= 2
	}
	if length > 0 {
		sum += uint32(data[index])
	}
	sum += sum >> 16

	return uint16(^sum)
}

func main() {
	fmt.Printf("======>Now----Test----Tun<===1232===\n")
	tunFile, err := createTun()
	if err != nil {
		fmt.Printf("ICMP Listen Packet Failed! err:%v\n", err.Error())
		return
	}
	defer tunFile.Close()

	icmpConn, _ := icmp.ListenPacket("ip4:icmp", eth0)

	defer icmpConn.Close()

	go tunToIcmp(icmpConn, tunFile)

	go icmpToTun(icmpConn, tunFile)

	time.Sleep(time.Hour)
}

func tunToIcmp(icmpconn *icmp.PacketConn, tunFile *os.File) {
	var srcIP string
	packet := make([]byte, 1024*64)
	size := 0
	var err error
	for {
		if size, err = tunFile.Read(packet); err != nil {
			return
		}
		fmt.Printf("Msg Length: %d\n", binary.BigEndian.Uint16(packet[2:4]))
		fmt.Printf("Msg Protocol: %d (1=ICMP, 6=TCP, 17=UDP)\tsize:%d\n", packet[9], size)

		b := packet[:size]
		srcIP = GetSrcIP(b)
		dstIP := GetDstIP(b)
		fmt.Printf("Msg srcIP: %s\tdstIP:%v\n", srcIP, dstIP)

		var raddr = net.IPAddr{IP: net.ParseIP(dstIP)}

		b = b[20:size]

		if size, err = icmpconn.WriteTo(b, &raddr); err != nil {
			fmt.Println(err.Error())
			return
		}
		fmt.Printf("Write Msg To Icmp Conn OK! size:%d\n", size)
	}
}

func icmpToTun(icmpconn *icmp.PacketConn, tunFile *os.File) {
	var sb = make([]byte, 1024*64)
	var addr net.Addr
	var size int
	var err error

	for {
		if size, addr, err = icmpconn.ReadFrom(sb); err != nil {
			continue
		}

		ipHeader := createIPv4Header(net.ParseIP(addr.String()), net.ParseIP(tunIP), os.Getpid())
		iphb, err := ipHeader.Marshal()
		if err != nil {
			continue
		}
		fmt.Printf("Reply MSG Length: %d\n", binary.BigEndian.Uint16(iphb[2:4]))
		fmt.Printf("Reply MSG Protocol: %d (1=ICMP, 6=TCP, 17=UDP)\n", iphb[9])
		dstIP := GetDstIP(iphb)
		fmt.Printf("Reply src IP: %s\tdstIP:%v\n", addr, dstIP)

		var rep = make([]byte, 84)
		rep = append(iphb, sb[:size]...)

		size, err = tunFile.Write(rep)
		if err != nil {
			continue
		}
		fmt.Printf("Write Msg To /dev/net/tun OK! size:%d\ttime:%v\n", size, time.Now())
	}
}

func createIPv4Header(src, dst net.IP, id int) *ipv4.Header {

	iph := &ipv4.Header{
		Version:  ipv4.Version,
		Len:      ipv4.HeaderLen,
		TOS:      0x00,
		TotalLen: ipv4.HeaderLen + 64,
		ID:       id,
		Flags:    ipv4.DontFragment,
		FragOff:  0,
		TTL:      64,
		Protocol: 1,
		Checksum: 0,
		Src:      src,
		Dst:      dst,
	}

	h, _ := iph.Marshal()

	iph.Checksum = int(checkSum(h))

	return iph
}

func IsIPv4(packet []byte) bool {
	flag := packet[0] >> 4
	return flag == 4
}

func GetIPv4Src(packet []byte) net.IP {
	return net.IPv4(packet[12], packet[13], packet[14], packet[15])
}

func GetIPv4Dst(packet []byte) net.IP {
	return net.IPv4(packet[16], packet[17], packet[18], packet[19])
}

func GetSrcIP(packet []byte) string {
	key := ""
	if IsIPv4(packet) && len(packet) >= 20 {
		key = GetIPv4Src(packet).To4().String()
	}

	return key
}

func GetDstIP(packet []byte) string {
	key := ""
	if IsIPv4(packet) && len(packet) >= 20 {
		key = GetIPv4Dst(packet).To4().String()
	}

	return key
}

func createTun() (*os.File, error) {
	err := addTun()
	if err != nil {
		return nil, err
	}

	err = configTun()
	if err != nil {
		return nil, err
	}

	tunFile, _, err := OpenTun(tunName)
	if err != nil {
		return nil, err
	}

	return tunFile, nil
}

func addTun() error {
	la := netlink.LinkAttrs{
		Name:  tunName,
		Index: 8,
		MTU:   1500,
	}
	tun := netlink.Tuntap{
		LinkAttrs: la,
		Mode:      netlink.TUNTAP_MODE_TUN,
	}

	l, err := netlink.LinkByName(tunName)
	if err == nil {
		netlink.LinkSetDown(l)

		netlink.LinkDel(l)
	}

	err = netlink.LinkAdd(&tun)
	if err != nil {
		return err
	}
	return nil
}

func configTun() error {

	l, err := netlink.LinkByName(tunName)
	if err != nil {
		return err
	}

	ip, err := netlink.ParseIPNet(fmt.Sprintf("%s/%d", tunIP, 24))
	if err != nil {
		return err
	}

	addr := &netlink.Addr{IPNet: ip, Label: ""}
	if err = netlink.AddrAdd(l, addr); err != nil {
		return err
	}

	err = netlink.LinkSetUp(l)
	if err != nil {
		return err
	}

	return nil
}

本代码里,已经集成了虚拟网卡tun19的创建,配置了。

直接使用即可。

4.2、本地编译,上传到服务器上

build:
	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o tun-driver main.go

scp:
	scp tun-driver root@10.211.55.122:/root

all:
	make build && make scp

执行

make all

即可

4.3、反向路由规则校验

sysctl -w net.ipv4.conf.all.rp_filter=2

sysctl net.ipv4.conf.all.rp_filter

在这里插入图片描述

4.4、测试

登录到远程服务器10.211.55.122上
root目录下

进行测试

./tun-driver
ping 10.211.55.123 -I tun19 -c 1

在这里插入图片描述

5、总结

本次试验,完成了在用户空间发送请求,经过tun类型的设备,实现请求的跨主机通信。

当然,在上面的测试用例中,你可以将ICMP改成udp来做,只不过在10.211.55.123节点上也需要开启一个udp服务来接收122节点上发送过来的数据包,然后,在将数据包转发给123节点上的eth0

6、参考

https://www.itdaan.com/blog/2017/03/09/e9d4766e2982.html

84字节,是如何计算处理的?(ctrl+f全局搜索一下84, 即可发现)
https://blog.csdn.net/Rong_Toa/article/details/86665176

https://www.freesion.com/search

云原生虚拟化:一文读懂网络虚拟化之 tun/tap 网络设备

Linux虚拟网络设备——tun/tap

TUN/TAP 学习总结(二) —— Linux TUN demo

c语言版本的tun读
https://blog.haohtml.com/archives/31687

如何读取二层,三层,四层数据包

图解:Ping 命令的工作原理


点击 下面 返回 专栏目录

<<零入门kubernetes网络实战>>技术专栏之文章目录

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/402342.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

从FVM上线前的测试网统计报告中看前景,Filecoin将会迎来什么变化?

FEVM将在2023/03/14主网上线&#xff01;在Calibration网络升级正式完成后&#xff0c;Filecoin V18 Hygge升级将于2023年3月14日&#xff08;π日&#xff09;正式上线&#xff01;此次升级将正式为Filecoin网络带来智能合约。基于FVM的可编程性。此次更新升级将释放数据经济的…

Spring Cloud Alibaba Sentinel 集群流量控制

为什么要进行集群流控 假设集群中有 10 台机器&#xff0c;我们给每台机器设置单机限流阈值为 10 QPS&#xff0c;理想情况下整个集群的限流阈值就为 100 QPS。不过实际情况下流量到每台机器可能会不均匀&#xff0c;会导致总量没有到的情况下某些机器就开始限流。因此仅靠单机…

因特网基础

1、因特网的概述 1-1、网络、互联网和因特网 网络&#xff08;Network&#xff09;是由若干节点&#xff08;Node&#xff09;和连接这些节点的链路&#xff08;Link&#xff09;组成。 多个网络还可以通过路由器相互连起来&#xff0c;这样就构成了一个覆盖范围更大的网络&…

【刷题笔记】--双指针--189. 轮转数组

题目&#xff1a; 思路1&#xff1a; 再设一个数组&#xff0c;通过下标的规律&#xff0c;进行更新数组。 关于这个平移的下标规律&#xff1a;%numbersize&#xff1b; 假设数组1&#xff0c;2&#xff0c;3&#xff0c;4&#xff0c;5&#xff0c;6&#xff0c;7 要整体…

MTK平台 Wireless Authentication Denied问题

这个问题一开始的现象是部分station无法连接某台AP。无线抓包后发现station和AP间进行了频繁的Auth认证,即station发送Auth包,ap回复Auth包出现 Expert: Wireless Authentication Denied (13: Requested authentication algorithm not supported)这样错误提示…

Lesson 9.2 随机森林回归器的参数

文章目录一、弱分类器的结构1. 分枝标准与特征重要性2. 调节树结构来控制过拟合二、弱分类器的数量三、弱分类器训练的数据1. 样本的随机抽样2. 特征的随机抽样3. 随机抽样的模式四、弱分类器的其他参数在开始学习之前&#xff0c;先导入我们需要的库。 import numpy as np im…

【项目精选】俄罗斯方块项目(视频+论文+源码)

点击下载源码 俄罗斯方块项目&#xff0c;基本功能包括&#xff1a;游戏主界面显示模块、方块及数据显示模块、方块移动控制模块、游戏界面颜色控制模块、游戏进度、等级控制模块等。本项目结构如下&#xff1a; &#xff08;1&#xff09;游戏主界面显示模块&#xff1a; 显示…

【密码学篇】密码行业标准汇总(GM)

【密码学篇】密码行业标准汇总&#xff08;GM&#xff09; 截止到2023年03月10日&#xff0c;共130个密码行业标准&#xff0c;适用商用密码应用与安全性评估等密码行业&#xff0c;可点击链接预览或下载标准—【蘇小沐】 文章目录【密码学篇】密码行业标准汇总&#xff08;GM…

【洛谷 P1044】[NOIP2003 普及组] 栈 题解(递归+记忆化搜索)

[NOIP2003 普及组] 栈 题目背景 栈是计算机中经典的数据结构&#xff0c;简单的说&#xff0c;栈就是限制在一端进行插入删除操作的线性表。 栈有两种最重要的操作&#xff0c;即 pop&#xff08;从栈顶弹出一个元素&#xff09;和 push&#xff08;将一个元素进栈&#xff…

常见数量关系分析

考点一相遇追及问题&#xff08;一&#xff09;直线型1. 单次相遇&#xff1a;相距两地&#xff0c;同时出发&#xff0c;相向而行。2. 单次追及&#xff1a;同时出发&#xff0c;同向而行。3. 直线型相遇追及问题公式总结&#xff08;二&#xff09;环线型环线型相遇追及问题公…

弹性存储-块存储和文件存储部分

存储通用知识 存储架构发展历程&#xff1a;直连存储-》存储网络-〉分布式存储/云存储 块存储、文件存储及对象存储使用场景 块存储、文件存储及对象存储性能对比 块存储及文件存储-块存储介绍 块存储EBS&#xff08;Elastic Block Storage&#xff09;是为了云服务器提…

CleanMyMac4.20新版本核心功能介绍

CleanMyMac4.20是Mac清理工具&#xff0c;具有很多功能。如‬删除大量不可见的缓存文件&#xff0c;可以批量删除未使用的DMG、不完整的下载以及其余的旧包。 与 CleanMyMac 3相比&#xff0c;新版本 UI设计焕然一新&#xff0c;采用了完全不同的风格。 CleanMyMac X4.20全新版…

在ubuntu上搭建SSH和FTP和NFS和TFTP

一、SSH服务搭建使用如下命令安装 SSH 服务&#xff1b;ssh 的配置文件为/etc/ssh/sshd_config&#xff0c;使用默认配置即可。sudo apt-get install openssh-server开启 SSH 服务以后我们就可以在 Windwos 下使用终端软件登陆到 Ubuntu&#xff0c;比如使用 Mobaxterm。二、FT…

六零导航页(LyLme Spage)导航网站源码

六零导航页 (LyLme Spage)前端基于D.Young的 5IUX搜索 &#xff0c;后台使用笔下光年的Light Year Admin模板开发&#xff0c;包含多种搜索引擎&#xff0c;致力于简洁高效无广告的上网导航和搜索入口&#xff0c;沉淀最具价值链接&#xff0c;全站无商业推广&#xff0c;简约而…

滤波算法 | 无迹卡尔曼滤波(UKF)算法及其Python实现

文章目录简介UKF滤波1. 概述和流程2. Python代码第一个版本a. KF滤波b. UKF滤波第一个版本简介 上一篇文章&#xff0c;我们介绍了UKF滤波公式及其MATLAB代码。在做视觉测量的过程中&#xff0c;基于OpenCV的开发包比较多&#xff0c;因此我们将UKF的MATLAB代码转到python中&a…

运维视角:rabbitmq教程(四)工作模式

今天这篇文章&#xff0c;通过python代码来测试rabbitmq交换机以及队列的工作模式&#xff0c;以此更加透彻的理解它的工作方式 一、简单模式 1、测试代码 生产者代码&#xff1a; import pikauser_info pika.PlainCredentials(admin, admin) connection pika.BlockingCon…

ICG-Hydrazide,吲哚菁绿-酰肼,ICG-HZ结构式,溶于二氯甲烷等部分有机溶剂,

ICG-Hydrazide,吲哚菁绿-酰肼 中文名称&#xff1a;吲哚菁绿-酰肼 英文名称&#xff1a;ICG-Hydrazide 英文别名&#xff1a;ICG-HZ 性状&#xff1a;粉末或固体 溶剂&#xff1a;溶于二氯甲烷等部分有机溶剂 稳定性&#xff1a;-20℃密封保存、置阴凉干燥处、防潮 分子…

vue上实现左右关联滚动

先看效果&#xff1a; 代码&#xff1a; <template><div class"container"><!-- 左侧fixed导航区域 --><div class"left"><divv-for"item in leftList":key"item.id"class"left_item":class&…

Angular学习之ControlValueAccessor接口详解

ControlValueAccessor 是什么&#xff1f;为什么需要使用 &#xff1f;下面本篇文章就来带大家了解Angular中的ControlValueAccessor组件接口&#xff0c;希望对大家有所帮助&#xff01; ControlValueAccessor 是什么&#xff1f; 简单来说ControlValueAccessor是一个接口&am…

【Linux 网络编程2】应用层协议--http;序列化和反序列化,get和post请求传参的区别,cookie和sesion,编写一个简单的http

目录 1.序列化和反序列化 2.HTTP协议 3.编写一个简单的http 3.2.简单的http的使用 3.3.get和post请求传参的区别 4.http的状态码分类 5.cookie和sesion 1.序列化和反序列化 1.1.序列化和反序列化的优势 序列化将结构体转化为长字符串&#xff0c;便于传输&#xff1b;反序…