零入门kubernetes网络实战-22->基于tun设备实现在用户空间可以ping通外部节点(golang版本)
《零入门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 mainimport ("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.goscp:scp main root@10.211.55.122:/rootall:make build && make scp
执行
make all
登录到远程服务器上
ip a s | grep eth0ip link sh tun19./mainip link sh tun19ip a sh tun19
3.2、tun-driver 代码原理介绍 |
3.2.1、golang代码 |
package mainimport ("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]byteIfruFlags 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 ifreqFlagscopy(ifr.IfrnName[:len(ifr.IfrnName)-1], []byte(name+"\000"))ifr.IfruFlags = syscall.IFF_TUN | syscall.IFF_NO_PIerr = 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 stringfunc checkSum(data []byte) uint16 {var (sum uint32length int = len(data)index int)for length > 1 {sum += uint32(data[index])<<8 + uint32(data[index+1])index += 2length -= 2}if length > 0 {sum += uint32(data[index])}sum += sum >> 16return uint16(^sum)
}func main() {flag.StringVar(&tunName, "tunName", "tun19", "Use -tunName xxx")flag.Parse()var err errortunFile, _, 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 stringpacket := make([]byte, 1024*64)size := 0var err errorfor {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 <- srcIPdstIP := 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.Addrvar size intvar err errorfor {if size, addr, err = icmpconn.ReadFrom(sb); err != nil {continue}srcIP := <-srcChipHeader := 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] >> 4return 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.goscp:scp main root@10.211.55.122:/rootall: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 TRACEiptables -t raw -A OUTPUT -p icmp -j TRACEiptables -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.2tail -f /var/log/messages | grep nat | grep PREROUTING | grep DST=10.244.2.2tail -f /var/log/messages | grep mangle | grep INPUT | grep DST=10.244.2.2tail -f /var/log/messages | grep nat | grep INPUT | grep DST=10.244.2.2tail -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=2sysctl 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 mainimport ("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 = 16eth0 = "10.211.55.122"tunIP = "10.244.1.3"
)type ifreqFlags struct {IfrnName [ifnameSize]byteIfruFlags 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 ifreqFlagscopy(ifr.IfrnName[:len(ifr.IfrnName)-1], []byte(name+"\000"))ifr.IfruFlags = syscall.IFF_TUN | syscall.IFF_NO_PIerr = 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 uint32length int = len(data)index int)for length > 1 {sum += uint32(data[index])<<8 + uint32(data[index+1])index += 2length -= 2}if length > 0 {sum += uint32(data[index])}sum += sum >> 16return 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 stringpacket := make([]byte, 1024*64)size := 0var err errorfor {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.Addrvar size intvar err errorfor {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] >> 4return 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.goscp:scp tun-driver root@10.211.55.122:/rootall:make build && make scp
执行
make all
即可
4.3、反向路由规则校验 |
sysctl -w net.ipv4.conf.all.rp_filter=2sysctl 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网络实战>>技术专栏之文章目录
相关文章:

零入门kubernetes网络实战-22->基于tun设备实现在用户空间可以ping通外部节点(golang版本)
《零入门kubernetes网络实战》视频专栏地址 https://www.ixigua.com/7193641905282875942 本篇文章视频地址(稍后上传) 本篇文章主要是想做一个测试: 实现的目的是 希望在宿主机-1上,在用户空间里使用ping命令发起ping请求,产生的icmp类型的…...
web安全——Mybatis防止SQL注入 ssrf漏洞利用 DNS污染同源策略
目录 0x01 Mybatis防止SQL注入 0x02 sqlmap中报错注入判断 0x03 ssrf漏洞利用 0x04 SSRF重绑定 0x05 DNS污染...

smp_init过程解析
当你看到这样的log,会不会很慌张?竟然由CPU没有启动成功,除了什么故障?本文将结合我遇到的一个问题,将启动过程中bringup secondary cpu的过程分析一下。smp_init代码如下:602 void __init smp_init(void) …...
判断推理之逻辑判断
考点一翻译推理(一)题目特征题干或选项中出现多组“逻辑关联词”,如:“如果……那么……”、“只要……就……”、“只有……才……”、“所有”、“有些”、“……且……”、“ ……或……”等典型逻辑关联词。(二&am…...

2023金三银四常见Handler面试总结,附带答案
以下的Handler的面试题都是在面试过程中总结出来比较常见的面试题,现在分享给大家,希望可以帮助你们!1.Handler的实现原理从四个方面看Handler、Message、MessageQueue 和 Looper Handler:负责消息的发送和处理 Message:消息对象,…...

为什么机器人操作系统ROS1、ROS2这么难学
AI回复:ROS1、ROS2是机器人操作系统,涉及到很多复杂的概念和技术,需要有一定的编程和机器人知识基础才能学习。此外,ROS1和ROS2的架构和设计也有很大的不同,需要花费一定的时间和精力去学习和适应。但是,一…...
day01
授课老师 :陶国荣 联系方式 : taogrtedu.cn 授课阶段 : Web前端基础 授课内容 : HTML CSS JavaScript 文章目录一、讲师和课程介绍二、Web前端介绍1. 什么是网页2. 网页的组成3. 网页的优势4. 开发前的准备三、 HTML语法介绍…...
第四十章 linux-并发解决方法五(顺序锁seqlock)
第四十章 linux-并发解决方法四(顺序锁seqlock) 文章目录第四十章 linux-并发解决方法四(顺序锁seqlock)顺序锁的设计思想是,对某一共享数据读取时不加锁,写的时候加锁。为了保证读取的过程中不会因为写入名…...

【SPSS】交叉设计方差分析和协方差分析详细操作教程(附案例实战)
🤵♂️ 个人主页:@艾派森的个人主页 ✍🏻作者简介:Python学习者 🐋 希望大家多多支持,我们一起进步!😄 如果文章对你有帮助的话, 欢迎评论 💬点赞👍🏻 收藏 📂加关注+ 目录 方差分析概述 交叉设计方差分析...
playwright--核心概念和Selector定位
文章目录前言一、浏览器二、浏览器上下文三、页面和框架四、Selectors1、data-test-id selector2、CSS and XPath selector3、text 文本selector4、id定位selector5、Selector 组合定位五、内置Selector前言 Playwright提供了一组API可自动化操作Chromium,Firefox和…...

响应式操作实战案例
Project Reactor 框架 在Spring Boot 项目 Maven 中添加依赖管理。 <dependency><groupId>io.projectreactor</groupId><artifactId>reactor-core</artifactId> </dependency><dependency><groupId>io.projectreactor</g…...

NetApp AFF A900:针对任务关键型应用程序的解决方案
NetApp AFF A900:适用于数据中心的解决方案 AFF A 系列中的 AFF A900 高端 NVMe 闪存存储功能强大、安全可靠、具有故障恢复能力,提供您为任务关键型企业级应用程序提供动力并保持数据始终可用且安全所需的一切。 AFF A900:针对任务关键型应…...

使用Houdini输出四面体网格并输出tetgen格式
我们的目标是从houdini输出生成的四面体,希望是tetgen格式的。 众所周知,houdini是不能直接输出四面体的。 有三方案去解决: 输出点云ply文件,然后利用tetgen生成网格。输出Hounidi内置的.geo格式文件,然后写个脚本…...

组合预测 | MATLAB实现EMD-KPCA-LSTM、EMD-LSTM、LSTM多输入单输出回归预测对比
组合预测 | MATLAB实现EMD-KPCA-LSTM、EMD-LSTM、LSTM多输入单输出回归预测对比 目录 组合预测 | MATLAB实现EMD-KPCA-LSTM、EMD-LSTM、LSTM多输入单输出回归预测对比预测效果基本介绍模型描述程序设计参考资料预测效果 基本介绍 MATLAB实现EMD-KP...

【C语言】操作符详解总结(万字)
操作符详解1. 操作符分类2. 算术操作符3. 移位操作符3.1 整数的二进制是怎么形成的3.2 左移操作符3.3 右移操作符4. 位操作符5. 赋值操作符6. 单目操作符6.1 单目操作符介绍6.2 sizeof 和 数组7. 关系操作符8. 逻辑操作符9. 条件操作符9.1 练习19.2 练习210. 逗号表达式11. 下标…...

mac系统手册(帮助/说明)
文章目录1. mac自带的帮助文档2. Mac使用技巧(提示)2.1 聚焦搜索2.2 截图(录制屏幕)2.3 调出右键菜单2.4 快速查看2.5 翻译2.5.1 词典解释2.5.2 翻译(字、词和句)3. macOS使用手册3.1 在聚焦中进行计算和转…...
VLC播放器Demo(录像,截图等功能),Android播放器Demo可二次开发。
VLC播放器Demo(录像,截图等功能),可二次开发。 GitHub地址:https://github.com/ILoveLin/VlcRecordPlayer GitHub地址:https://github.com/ILoveLin/VlcRecordPlayer GitHub地址:https://github.com/ILoveLin/VlcRecordPlayer …...

WeSpeaker支持C++部署链路
WeSpeaker正式更新C部署链路,推理引擎使用OnnxRuntime,支持从语音中提取Speaker Embedding信息,代码详见WeSpeaker/runtime[1]。 Libtorch和onnx的选择? Speaker Embedding提取任务流程简单,并且声纹模型(如ResNet\E…...

window vscode编辑appsmith源码
前言 本来最开始用的idea打开wsl中的appsmith,卡得一批。最后没办法,用自己的电脑装成ubuntu server,然后vscode的远程开发对appsmith源码进行编辑。如果自己电脑内存16个G或者更大可能打开wsl中的估计会还好,我公司电脑只有8g所…...

操作系统面试题
操作系统一、简介篇1.解释一下什么是操作系统2.操作系统的主要功能3.软件访问硬件的几种方式4.操作系统的主要目的是什么5.为什么Linux系统下的应用程序不能直接在Windows下运行6.什么是用户态和内核态7.用户态和内核态如何切换8.什么是内核二、进程和线程篇1.多处理系统的优势…...

stm32G473的flash模式是单bank还是双bank?
今天突然有人stm32G473的flash模式是单bank还是双bank?由于时间太久,我真忘记了。搜搜发现,还真有人和我一样。见下面的链接:https://shequ.stmicroelectronics.cn/forum.php?modviewthread&tid644563 根据STM32G4系列参考手…...

Xshell远程连接Kali(默认 | 私钥)Note版
前言:xshell远程连接,私钥连接和常规默认连接 任务一 开启ssh服务 service ssh status //查看ssh服务状态 service ssh start //开启ssh服务 update-rc.d ssh enable //开启自启动ssh服务 任务二 修改配置文件 vi /etc/ssh/ssh_config //第一…...
OkHttp 中实现断点续传 demo
在 OkHttp 中实现断点续传主要通过以下步骤完成,核心是利用 HTTP 协议的 Range 请求头指定下载范围: 实现原理 Range 请求头:向服务器请求文件的特定字节范围(如 Range: bytes1024-) 本地文件记录:保存已…...

[10-3]软件I2C读写MPU6050 江协科技学习笔记(16个知识点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
【碎碎念】宝可梦 Mesh GO : 基于MESH网络的口袋妖怪 宝可梦GO游戏自组网系统
目录 游戏说明《宝可梦 Mesh GO》 —— 局域宝可梦探索Pokmon GO 类游戏核心理念应用场景Mesh 特性 宝可梦玩法融合设计游戏构想要素1. 地图探索(基于物理空间 广播范围)2. 野生宝可梦生成与广播3. 对战系统4. 道具与通信5. 延伸玩法 安全性设计 技术选…...

R 语言科研绘图第 55 期 --- 网络图-聚类
在发表科研论文的过程中,科研绘图是必不可少的,一张好看的图形会是文章很大的加分项。 为了便于使用,本系列文章介绍的所有绘图都已收录到了 sciRplot 项目中,获取方式: R 语言科研绘图模板 --- sciRplothttps://mp.…...
机器学习的数学基础:线性模型
线性模型 线性模型的基本形式为: f ( x ) ω T x b f\left(\boldsymbol{x}\right)\boldsymbol{\omega}^\text{T}\boldsymbol{x}b f(x)ωTxb 回归问题 利用最小二乘法,得到 ω \boldsymbol{\omega} ω和 b b b的参数估计$ \boldsymbol{\hat{\omega}}…...
node.js的初步学习
那什么是node.js呢? 和JavaScript又是什么关系呢? node.js 提供了 JavaScript的运行环境。当JavaScript作为后端开发语言来说, 需要在node.js的环境上进行当JavaScript作为前端开发语言来说,需要在浏览器的环境上进行 Node.js 可…...

HTTPS证书一年多少钱?
HTTPS证书作为保障网站数据传输安全的重要工具,成为众多网站运营者的必备选择。然而,面对市场上种类繁多的HTTPS证书,其一年费用究竟是多少,又受哪些因素影响呢? 首先,HTTPS证书通常在PinTrust这样的专业平…...