零入门kubernetes网络实战-25->基于tap虚拟网络设备的测试用例以及协议栈封装解析介绍(helloworld级别)

《零入门kubernetes网络实战》视频专栏地址

https://www.ixigua.com/7193641905282875942

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


本篇文章主要是分享一下tap虚拟网络设备。

创建tap网络设备的方式跟tun完全一样,只需要将类型改为tap即可。

这里不再占用篇幅介绍了。

1、本篇文章的核心点

  • tap虚拟网络设备一般用在什么场景下
  • tap虚拟网络设备使用什么文件描述符
  • 简单验证/dev/net/tun文件描述符是否支持并发读写操作?即验证在同一个宿主机上实现多个kvm类型的虚拟机网络通信的关键点

2、tap虚拟网络设备一般用在什么场景下

  • openvpn
  • kvm

3、tap虚拟网络设备使用什么文件描述符

tap跟tun一样,都是将此文件

/dev/net/tun

作为文件描述符的。
通过此文件跟虚拟网络设备tap、tun进行数据交互。

在centos7.5系统下,其他其他并为测试。

4、简单验证/dev/net/tun文件描述符是否支持并发读写操作?即验证在同一个宿主机上实现多个kvm类型的虚拟机网络通信的关键点

在kvm里使用tap设备进行的网络构建,那么,

本小节主要是想测试一下,文件描述符/dev/net/tun是否支持并发的读写操作。

如果支持并发读写操作的话,那么,下面的网络构建是成立的,

即在同一个宿主机上可以创建多个kvm类型的虚拟机。

在这里插入图片描述

在上图中:

  • 在一个宿主机内部,通过物理网卡eth0跟外部网络进行数据通信
  • 在宿主机内部,创建一个虚拟网桥br0
  • 可以创建多个tap虚拟网络设备,
    • 这些tap设备挂载到虚拟网桥上br0
    • 并且这些tap设备都跟宿主机上的/dev/net/tun文件进行绑定,所有tap设备通过此文件进行读写
  • 创建好的kvm类型的虚拟机里可以将数据包写入到宿主机上的/dev/net/tun文件里,
  • 数据一旦到达/dev/net/tunW文件里,就相当于到达了虚拟网络设备tap里了,即到达了虚拟网桥br0

这样的话,kvm类型的虚拟机就可以跟外界进行通信了。

因此,主要是测试/dev/net/tun是否支持并发读写,其他相关操作不再测试了,如将tap设备挂载到虚拟网桥上。

4.1、测试/dev/net/tun支持并发读写的方案

我们创建两个测试用例,两个测试用例里都绑定了同一个宿主机上的/dev/net/tun文件,

运行这两个测试用例,观察是否出现问题,没有问题说明支持并发读写操作:

  • 测试用例1,如何实现?
    • 在前面的文章中,我们使用了tun设备来实现点对点的VPN,在此基础上,我们对其代码进行改造,将tun设备改成tap设备来实现。
  • 测试用例2,如何实现?
    • 将前面文章<<tun设备编程ICMP协议>>里的代码直接拿过来使用即可。

4.2、测试环境

两台虚拟机

  • centos7.5 10.211.55.122
  • centos7.5 10.211.55.123

4.3、测试用例1:基于tap虚拟网络设备实现点对点的VPN

4.3.1、代码结构

在这里插入图片描述

并没有严格意义上的客户端,服务器端

请求由哪个服务发起的,被称为客户端,

被请求服务,称为服务器端。

4.3.2、客户端代码

4.3.2.1、main.go文件

package main

import (
	"bytes"
	"fmt"
	"github.com/vishvananda/netlink"
	"log"
	"net"
	"os"
	"strings"
	"syscall"
	"time"
	"unsafe"
)

const (
	tapName    = "tap99"
	tapDevice  = "/dev/net/tun"
	ifnameSize = 16
	localAddr  = "10.211.55.122:8285"
	remoteAddr = "10.211.55.123:8285"
	tapIP      = "10.244.2.2"
)

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 OpenTap(name string) (*os.File, string, error) {
	tap, err := os.OpenFile(tapDevice, os.O_RDWR|syscall.O_NONBLOCK, 0)
	if err != nil {
		fmt.Printf("OpenTap 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_TAP | syscall.IFF_NO_PI

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

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

func main() {
	fmt.Printf("======>Now----Client----Tap---VPN---UDP<======\n")
	tapFile, err := createTap()
	if err != nil {
		fmt.Printf("ICMP Listen Packet Failed! err:%v\n", err.Error())
		return
	}
	defer tapFile.Close()

	udpConn, err := createUDP()
	if err != nil {
		fmt.Printf("UDP conn Failed! err:%v\n", err.Error())
		return
	}
	defer udpConn.Close()

	go tapToUDP(udpConn, tapFile)

	go udpToTap(udpConn, tapFile)

	time.Sleep(time.Hour)
}

func createUDP() (*net.UDPConn, error) {
	localAddr, err := net.ResolveUDPAddr("udp", localAddr)
	if err != nil {
		log.Fatalln("failed to get udp socket:", err)
		return nil, err
	}
	conn, err := net.ListenUDP("udp", localAddr)
	if err != nil {
		log.Fatalln("failed to listen on udp socket:", err)
		return nil, err
	}

	return conn, nil

}

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)
			if b[9] == 1 {
				icmpPacket := b[20:]
				printICMPHeader(icmpPacket)
			}

			if b[9] == 6 {
				tcpPacket := b[20:]
				printTCPHeader(tcpPacket)
			}

			if b[9] == 17 {
				udpPacket := b[20:]
				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)
	}
}

func udpToTap(udpConn *net.UDPConn, tapFile *os.File) {
	var packet = make([]byte, 1024*64)
	var size int
	var err error
	var addr net.Addr

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

		size, err = tapFile.Write(packet[:size])
		if err != nil {
			continue
		}
		fmt.Printf("udpToTap--->Write Msg To /dev/net/tun OK! size:%d\tsrcIP:%v\n", size, addr)
	}
}

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

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

	tapFile, _, err := OpenTap(tapName)
	if err != nil {
		return nil, err
	}

	return tapFile, nil
}

func addTap() error {
	la := netlink.LinkAttrs{
		Name:  tapName,
		Index: 8,
		MTU:   1500,
	}
	tap := netlink.Tuntap{
		LinkAttrs: la,
		Mode:      netlink.TUNTAP_MODE_TAP,
	}

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

		netlink.LinkDel(l)
	}

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

func configTap() error {

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

	ip, err := netlink.ParseIPNet(fmt.Sprintf("%s/%d", tapIP, 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
}

在这里插入图片描述

在这里插入图片描述

具体下一小节在介绍。

目前,先把/dev/net/tun文件的并发性介绍完。

4.3.2.2、etherheader.go文件

package main

import (
	"fmt"
	"net"
)

func printMACHeader(packet []byte) {
	fmt.Println()
	fmt.Println()
	fmt.Println()
	fmt.Println("----->MAC Header<----")
	printSrcMACEther(packet)
	printDstMACEther(packet)
	printTypeEther(packet)
	fmt.Println("----->MAC Header<--end--")
}

func printDstMACEther(packet []byte) {
	fmt.Printf("Ether Header--->DstMac:%x:%x:%x:%x:%x:%x\n", packet[0], packet[1], packet[2], packet[3], packet[4], packet[5])
}

func printSrcMACEther(packet []byte) {
	fmt.Printf("Ether Header--->SrcMac:%x:%x:%x:%x:%x:%x\n", packet[6], packet[7], packet[8], packet[9], packet[10], packet[11])
}
func printTypeEther(packet []byte) {
	fmt.Printf("Ether Header--->Type:%04x\n", uint16(packet[12])<<8|uint16(packet[13]))
}

func MACDestination(macFrame []byte) net.HardwareAddr {
	return net.HardwareAddr(macFrame[:6])
}

func MACSource(macFrame []byte) net.HardwareAddr {
	return net.HardwareAddr(macFrame[6:12])
}

func MACType(macFrame []byte) []byte {
	return macFrame[12:14]
}

func MACTypeARP(macFrame []byte) []byte {
	return macFrame[2:4]
}

4.3.2.3、arpheader.go文件

package main

import (
	"fmt"
	"net"
)

func printARPHeader(packet []byte) {
	fmt.Println()
	fmt.Println()
	fmt.Println()
	fmt.Println("----->ARP Header<----")
	printARPTYPE(packet)
	printARPProTYPE(packet)
	printARPHardwareLen(packet)
	printARPPLen(packet)
	printARPOp(packet)
	printARPSrcHardwareMAC(packet)
	printARPSrcIP(packet)
	printARPDstHardwareMAC(packet)
	printARPDstIP(packet)
	fmt.Println("----->ARP Header<---END---")

}
func printARPTYPE(packet []byte) {
	fmt.Printf("ARP Header--->Type:%d\n", uint16(packet[0])<<8|uint16(packet[1]))
}

func printARPProTYPE(packet []byte) {
	fmt.Printf("ARP Header--->ProtocolType:%04x\n", uint16(packet[2])<<8|uint16(packet[3]))
}

func printARPHardwareLen(packet []byte) {
	fmt.Printf("ARP Header--->HardwareLen:%d\n", packet[4])
}

func printARPPLen(packet []byte) {
	fmt.Printf("ARP Header--->ProtocolLen:%d\n", packet[5])
}

func printARPOp(packet []byte) {
	fmt.Printf("ARP Header--->op:%d\n", uint16(packet[6])<<8|uint16(packet[7]))
}

func printARPSrcHardwareMAC(packet []byte) {
	fmt.Printf("ARP Header--->SrcHardwarMAC:%x:%x:%x:%x:%x:%x\n", packet[8], packet[9], packet[10], packet[11], packet[12], packet[13])
}

func printARPSrcIP(packet []byte) {
	fmt.Printf("ARP Header--->SrcIP:%d.%d.%d.%d\n", packet[14], packet[15], packet[16], packet[17])
}

func printARPDstHardwareMAC(packet []byte) {
	fmt.Printf("ARP Header--->DstHardwareMAC:%x:%x:%x:%x:%x:%x\n", packet[18], packet[19], packet[20], packet[21], packet[22], packet[23])
}

func printARPDstIP(packet []byte) {
	fmt.Printf("ARP Header--->DstIP:%d.%d.%d.%d\n", packet[24], packet[25], packet[26], packet[27])
}

func GetIPv4SrcARP(packet []byte) net.IP {
	return net.IPv4(packet[14], packet[15], packet[16], packet[17])
}

func GetIPv4DstARP(packet []byte) net.IP {
	return net.IPv4(packet[24], packet[25], packet[26], packet[27])
}

4.3.2.4、icmpheader.go文件

package main

import "fmt"

func printICMPHeader(packet []byte) {
	fmt.Println()
	fmt.Println()
	fmt.Println()
	fmt.Println("----->ICMP Header<----")
	printICMPType(packet)
	printICMPCode(packet)
	printICMPCheckSum(packet)
	printICMPIdentification(packet)
	printICMPSeqNum(packet)
	printICMPTimestamp(packet)
	printICMPData(packet)
	fmt.Println("----->ICMP Header<----End----")
}

func printICMPType(packet []byte) {
	fmt.Printf("ICMP Header--->Type:%d\n", packet[0])
}

func printICMPCode(packet []byte) {
	fmt.Printf("ICMP Header--->Code:%d\n", packet[1])
}

func printICMPCheckSum(packet []byte) {
	fmt.Printf("ICMP Header--->Checksum:%04x\n", uint16(packet[2])<<8|uint16(packet[3]))
}

func printICMPIdentification(packet []byte) {
	fmt.Printf("TICMP Header--->Identification(process):%d\n", uint16(packet[4])<<8|uint16(packet[5]))
}

func printICMPSeqNum(packet []byte) {
	fmt.Printf("ICMP Header--->SeqNum:%d\n", uint16(packet[6])<<8|uint16(packet[7]))
}

func printICMPTimestamp(packet []byte) {
	fmt.Printf("ICMP Header --->timestamp:%02x %02x %02x %02x %02x %02x %02x %02x\n", packet[8], packet[9], packet[10], packet[11], packet[12], packet[13], packet[14], packet[15])
}

func printICMPData(packet []byte) {
	fmt.Printf("ICMP Header--->data:%v\n", string(packet[20:]))
}


4.3.2.5、tcpheader.go文件

package main

import (
	"fmt"
)

func printTCPHeader(packet []byte) {
	fmt.Println()
	fmt.Println()
	fmt.Println()
	fmt.Printf("----->TCP Header<----len:%d\n", len(packet[:]))
	printTCPsrcPort(packet)
	printTCPdstPort(packet)
	printTCPSequenceNumber(packet)
	printTCPACKNumber(packet)
	printTCPHeaderLen(packet)
	printTCPFlagCWR(packet)
	printTCPFlagECE(packet)
	printTCPFlagUrgent(packet)
	printTCPFlagACK(packet)
	printTCPFlagPSH(packet)
	printTCPFlagRST(packet)
	printTCPFlagSYN(packet)
	printTCPFlagFIN(packet)
	printTCPWindowSize(packet)
	printTCPCheckSum(packet)
	printTCPUrgentPointer(packet)
	printTCPData(packet)
	fmt.Println("----->TCP Header<---END---")
}

func printTCPsrcPort(packet []byte) {
	fmt.Printf("TCP Header--->SrcPort:%d\n", int64(uint16(packet[0])<<8|uint16(packet[1])))
}

func printTCPdstPort(packet []byte) {
	fmt.Printf("TCP Header--->DstPort:%d\n", int64(uint16(packet[2])<<8|uint16(packet[3])))
}

func printTCPSequenceNumber(packet []byte) {
	fmt.Printf("TCP Header--->SequenceNumber:%d\n", uint32(packet[4])<<24|uint32(packet[5])<<16|uint32(packet[6])<<8|uint32(packet[7]))
}

func printTCPACKNumber(packet []byte) {
	fmt.Printf("TCP Header--->ACKNum:%d\n", uint32(packet[8])<<24|uint32(packet[9])<<16|uint32(packet[10])<<8|uint32(packet[11]))
}

func printTCPHeaderLen(packet []byte) {
	fmt.Printf("TCP Header--->HeaderLen:%d\n", packet[12]>>4*4)
}

func printTCPFlagCWR(packet []byte) {
	fmt.Printf("TCP Header--->FlagCWR:%d\n", packet[13]&0x80>>7)
}
func printTCPFlagECE(packet []byte) {
	fmt.Printf("TCP Header--->FlagEcho:%d\n", packet[13]&0x40>>6)
}

func printTCPFlagUrgent(packet []byte) {
	fmt.Printf("TCP Header--->FlagUrgent:%d\n", packet[13]&0x20>>5)
}

func printTCPFlagACK(packet []byte) {
	fmt.Printf("TCP Header--->FlagACK:%d\n", packet[13]>>4&0b0001)
	fmt.Printf("TCP Header--->FlagACK:%d\n", packet[13]>>4&0b1)
	//fmt.Printf("TCP Header--->FlagACK:%d\tpacket:%v\n", packet[13]>>4, packet[13])
	fmt.Printf("TCP Header--->FlagACK:%d\n", packet[13]&0x10>>4)
}

func printTCPFlagPSH(packet []byte) {
	fmt.Printf("TCP Header--->FlagPSH:%d\n", packet[13]&0x08>>3)
}

func printTCPFlagRST(packet []byte) {
	fmt.Printf("TCP Header--->FlagRST:%d\n", packet[13]&0x04>>2)
}

func printTCPFlagSYN(packet []byte) {
	fmt.Printf("TCP Header--->FlagSYN:%d\n", packet[13]&0x02>>1)
}

func printTCPFlagFIN(packet []byte) {
	fmt.Printf("TCP Header--->FlagFIN:%d\n", packet[13]&0x01)
}

func printTCPWindowSize(packet []byte) {
	fmt.Printf("TCP Header--->WindowSize:%d\n", uint16(packet[14])<<8|uint16(packet[15]))
}

func printTCPCheckSum(packet []byte) {
	fmt.Printf("TCP Header--->Checksum:%04x\n", uint16(packet[16])<<8|uint16(packet[17]))
}

func printTCPUrgentPointer(packet []byte) {
	fmt.Printf("TCP Header--->UrgentPointer:%d\n", uint16(packet[18])<<8|uint16(packet[19]))
}

func printTCPData(packet []byte) {
	headerLen := packet[12] >> 4 * 4
	p := packet[headerLen:]
	dataLen := len(p)
	if dataLen > 1 {
		fmt.Printf("TCP Header--->Data--->:%v\theaderLen:%v\tdataLen:%d\n", string(p[:dataLen-1]), headerLen, dataLen)
	}
}

4.3.2.6、udpheader.go文件

package main

import "fmt"

func printUDPHeader(packet []byte) {
	fmt.Println()
	fmt.Println()
	fmt.Println()
	fmt.Println("----->UDP Header<----")
	printUDPSrcPort(packet)
	printUDPDstPort(packet)
	printUDPAllLen(packet)
	printUDPChecksum(packet)
	printUDPData(packet)
	fmt.Println("----->UDP Header<---End--")
}

func printUDPSrcPort(packet []byte) {
	fmt.Printf("UDP Header--->SrcPort:%d\n", uint16(packet[0])<<8|uint16(packet[1]))
}

func printUDPDstPort(packet []byte) {
	fmt.Printf("UDP Header--->DstPort:%d\n", uint16(packet[2])<<8|uint16(packet[3]))
}

func printUDPAllLen(packet []byte) {
	fmt.Printf("UDP Header--->AllLen:%d\n", uint16(packet[4])<<8|uint16(packet[5]))
}

func printUDPChecksum(packet []byte) {
	fmt.Printf("UDP Header--->Checksum:%d\n", uint16(packet[6])<<8|uint16(packet[7]))
}

func printUDPData(packet []byte) {
	fmt.Printf("UDP Header--->data:%v\n", string(packet[8:]))
}

4.3.2.7、ipv4header.go文件

package main

import (
	"fmt"
	"net"
)

//  打印ip报文头的详情
func printIPv4Header(packet []byte) {
	fmt.Println()
	fmt.Println()
	fmt.Println()
	fmt.Println("----->IP Header<----")
	printVersionIPv4(packet)
	printHeaderLenIPv4(packet)
	printServiceTypeIPv4(packet)
	printAllLenIPv4(packet)
	printIdentificationIPv4(packet)
	printFlagsIPv4(packet)
	printFragmentOffsetIPv4(packet)
	printTTLIPv4(packet)
	printProtocolIPv4(packet)
	printChecksumIPv4(packet)
	printSrcIPv4(packet)
	printDstIPv4(packet)
	fmt.Println("----->IP Header<---End---")
}

func printVersionIPv4(packet []byte) {
	header := packet[0]
	fmt.Printf("IPv4 Header--->Version:%d\n", header>>4)
}

func printHeaderLenIPv4(packet []byte) {
	header := packet[0]
	fmt.Printf("IPv4 Header--->HeaderLen:%d byte\n", header&0x0f*4)
}

func printServiceTypeIPv4(packet []byte) {
	st := packet[1]
	fmt.Printf("IPv4 Header--->ServiceType:%v\n", st)
}

func printAllLenIPv4(packet []byte) {
	fmt.Printf("IPv4 Header--->AllLen:%d\n", uint16(packet[2])<<8|uint16(packet[3]))
}

func printIdentificationIPv4(packet []byte) {
	id := uint16(packet[4])<<8 | uint16(packet[5])
	fmt.Printf("IPv4 Header--->Identification:%x\t%d\n", id, id)
}

func printFlagsIPv4(packet []byte) {
	// 向右移动5位,相当于去掉后5位。数值降低
	fmt.Printf("IPv4 Header--->Flags:%03b\n", packet[6]>>5)
}
func printFragmentOffsetIPv4(packet []byte) {
	// 向左移动3位,相当于将前3位去掉。数值可能增加
	fmt.Printf("IPv4 Header--->FragmentOffset:%013b\n", uint16(packet[6])<<3|uint16(packet[7]))
}

func printTTLIPv4(packet []byte) {
	fmt.Printf("IPv4 Header--->TTL:%d\n", packet[8])
}

func printProtocolIPv4(packet []byte) {
	fmt.Printf("IPv4 Header--->ProtocolType:%d\n", packet[9])
}

func printChecksumIPv4(packet []byte) {
	fmt.Printf("IPv4 Header--->Checksum:%d\n", uint16(packet[10])<<8|uint16(packet[11]))
}

func printSrcIPv4(packet []byte) net.IP {
	fmt.Printf("IPv4 Header--->SrcIP:%d.%d.%d.%d\n", packet[12], packet[13], packet[14], packet[15])
	return net.IPv4(packet[12], packet[13], packet[14], packet[15])
}

func printDstIPv4(packet []byte) net.IP {
	fmt.Printf("IPv4 Header--->DstIP:%d.%d.%d.%d\n", packet[16], packet[17], packet[18], packet[19])
	return net.IPv4(packet[16], packet[17], packet[18], packet[19])
}

4.3.3、服务端代码

4.3.3.1、server.go代码

package main

import (
	"bytes"
	"fmt"
	"github.com/vishvananda/netlink"
	"log"
	"net"
	"os"
	"syscall"
	"time"
	"unsafe"
)

const (
	tapName    = "tap99"
	tapDevice  = "/dev/net/tun"
	ifnameSize = 16
	localAddr  = "10.211.55.123:8285"
	remoteAddr = "10.211.55.122:8285"
	tapIP      = "10.244.3.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 OpenTap(name string) (*os.File, string, error) {
	tap, err := os.OpenFile(tapDevice, os.O_RDWR|syscall.O_NONBLOCK, 0)
	if err != nil {
		fmt.Printf("OpenTap 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_TAP | syscall.IFF_NO_PI

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

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

func main() {
	fmt.Printf("======>Now----Server----Tap---VPN---UDP<======\n")
	tapFile, err := createTap()
	if err != nil {
		fmt.Printf("ICMP Listen Packet Failed! err:%v\n", err.Error())
		return
	}
	defer tapFile.Close()

	udpConn, err := createUDP()
	if err != nil {
		fmt.Printf("UDP conn Failed! err:%v\n", err.Error())
		return
	}
	defer udpConn.Close()

	go tapToUDP(udpConn, tapFile)

	go udpToTap(udpConn, tapFile)

	time.Sleep(time.Hour)
}

func createUDP() (*net.UDPConn, error) {
	localAddr, err := net.ResolveUDPAddr("udp", localAddr)
	if err != nil {
		log.Fatalln("failed to get udp socket:", err)
		return nil, err
	}
	conn, err := net.ListenUDP("udp", localAddr)
	if err != nil {
		log.Fatalln("failed to listen on udp socket:", err)
		return nil, err
	}

	return conn, nil

}

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
		}

		srcMAC := MACSource(packet[:size])
		dstMAC := MACDestination(packet[:size])
		fmt.Printf("Server==>TapToUDP----srcMAC:%v\tdstMAC:%v\n", srcMAC.String(), dstMAC.String())

		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)
	}
}

func udpToTap(udpConn *net.UDPConn, tapFile *os.File) {
	var packet = make([]byte, 1024*64)
	var size int
	var err error
	var addr net.Addr

	for {
		if size, addr, err = udpConn.ReadFrom(packet); err != nil {
			continue
		}
		srcMAC := MACSource(packet[:size])
		dstMAC := MACDestination(packet[:size])
		fmt.Printf("Server==>UDPToTap----srcMAC:%v\tdstMAC:%v\n", srcMAC.String(), dstMAC.String())
		size, err = tapFile.Write(packet[:size])
		if err != nil {
			continue
		}
		fmt.Printf("udpToTap--->Write Msg To /dev/net/tun OK! size:%d\tsrcIP:%v\n", size, addr)
	}
}

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

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

	tapFile, _, err := OpenTap(tapName)
	if err != nil {
		return nil, err
	}

	return tapFile, nil
}

func addTap() error {
	la := netlink.LinkAttrs{
		Name:  tapName,
		Index: 8,
		MTU:   1500,
	}
	tap := netlink.Tuntap{
		LinkAttrs: la,
		Mode:      netlink.TUNTAP_MODE_TAP,
	}

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

		netlink.LinkDel(l)
	}

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

func configTap() error {

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

	ip, err := netlink.ParseIPNet(fmt.Sprintf("%s/%d", tapIP, 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
}

func MACDestination(macFrame []byte) net.HardwareAddr {
	return net.HardwareAddr(macFrame[:6])
}

func MACSource(macFrame []byte) net.HardwareAddr {
	return net.HardwareAddr(macFrame[6:12])
}

4.3.4、go.mod参考

module tun-test/icmp/v2

go 1.17

require github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5

require (
	github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
	golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
)

记得修改module名称,修改自己实际的。

同样,客户端,服务器端代码同样修改一下即可。

4.3.5、本地编译,上传到10.211.55.122以及10.211.55.123节点上去

Makefile

build:
	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o tap-client ./client/*.go
	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o tap-server ./server/main.go

scp:
	scp tap-client root@10.211.55.122:/root
	scp tap-server root@10.211.55.122:/root

all:
	make build && make scp

在本地执行

make all

在这里插入图片描述

4.4、测试用例2:使用tun设备实现ICMP协议

4.4.1、main.go文件内容,如下

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.2.9"
)

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<======\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
}

4.4.2、编译、上传到10.211.55.122上

Makefile

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.5、注意事项

修改代码时,要注意下

  • 第一个测试用例中,创建是tap类型的虚拟网络设备tap99
  • 第二个测试用例中,创建的tun类型的虚拟网络设备tun19

我们测试的关键点是,不同的虚拟网络设备绑定在同一个文件描述符/dev/net/tun上
只要这两个设备能够同时读写操作,即可。

实话实话,主要是不想再修改第二个测试用例的代码,将其改成tap类型的。
在tun、tap上已经用了很多时间了,太累了。

4.6、启动服务

4.6.1、登录到10.211.55.123节点上

4.6.1.1、启动第一个测试用例的服务器端

./tap-server

在这里插入图片描述

4.6.1.2、设置路由表,针对的是数据包去往10.211.55.122

route -n

route add -net 10.244.2.0/24 dev tap99

route -n

在这里插入图片描述

4.6.2、登录到10.211.55.122节点上

4.6.2.1、启动tap-client服务

4.6.2.1.1、启动tap-client服务
 ./tap-client 

在这里插入图片描述

4.6.2.1.2、设置通往10.211.55.123节点的路由
route -n

route add -net 10.244.3.0/24 dev tap99

route -n

在这里插入图片描述

4.6.2.2、启动第2个测试用例

./tun-driver 

在这里插入图片描述

设置路由反向检测属性

sysctl net.ipv4.conf.all.rp_filter

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

sysctl net.ipv4.conf.all.rp_filter

在这里插入图片描述

4.7、通过ping命令来测试/dev/net/tun是否支持并发读写

登录到
10.211.55.122 节点上去

4.7.1、在10.211.55.122 节点上,发起ping命令去ping10.211.55.123节点上的tap99设备

ping 10.244.3.3 -I tap99

在这里插入图片描述

抓包命令如下:

tcpdump -nn -i tap99

在这里插入图片描述

4.7.2、在10.211.55.122 节点上,发起ping命令去ping10.211.55.123节点上的对外物理网卡eth0

ping 10.211.55.123 -I tun19

在这里插入图片描述

抓包命令如下:

tcpdump -nn -i tun19

在这里插入图片描述

5、总结

在10.211.55.122节点上,
通过ping命令同时使用tun19虚拟设备以及tap99虚拟设备发起ping请求

整个请求中,数据包有去有回,无丢包现象,

而tap19,tun99同时绑定了/dev/net/tun文件描述符,

说明/dev/net/tun文件描述符支持并发读写操作。

这样的话,本次测试的目的之一就到达了。


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


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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Matlab与ROS(1/2)---Message(三)

0. 简介 消息是ROS中交换数据的主要容器。主题和服务使用消息在节点之间传输数据。为了标识其数据结构&#xff0c;每条消息都有一个消息类型。例如&#xff0c;来自激光扫描仪的传感器数据通常以sensor_msgs/LaserScan类型的消息发送。每种消息类型标识消息中包含的数据元素。…

【Java Web】002 -- JS Vue快速入门

目录 一、JS快速入门 1、什么是JavaScript? 2、JS引入方式 ①、示例代码 3、JS基础语法 ①、书写语法 ②、变量 ③、数据类型 ④、运算符 ⑤、流程控制语句 4、JS函数 ①、第一种函数定义方式 function funcName(参数1&#xff0c;……) ②、第二种函数定义方式 var funcName …

Chatgpt4来了,测试小姐姐实测,在失业的边缘疯狂试探~

GPT-4是OpenAI于2023年3月发布的最新人工智能模型&#xff0c;它是继GPT-3.5之后的又一次重大突破。它的核心技术是基于Transformer的自回归语言模型&#xff0c;它使用了大量的无标注数据进行预训练&#xff0c;学习了自然语言和其他模态之间的通用表示和关系。我们今天来看看…

★Flash

1.NOR Flash和NAND Flash的区别 读速&#xff1a;Nor很快&#xff0c;NAND快 擦除/写入&#xff1a;Nor低&#xff0c;NAND高 访问方式&#xff1a;Nor一次写一个字节&#xff0c;按总线时序读写&#xff1b;NAND一次写多个&#xff08;512字节的块&#xff09;&#xff0c;…

【2023中高级向】53道vue面试题附答案

1. 谈一谈对 MVVM 的理解&#xff1f; 参考答案&#xff1a; MVVM 是 Model-View-ViewModel 的缩写。MVVM 是一种设计思想。Model 层代表数据模型&#xff0c;也可以在 Model 中定义数据修改和操作的业务逻辑;View 代表 UI 组件&#xff0c;它负责将数据模型转化成 UI 展现出来…

Spring MVC 图片的上传和下载

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

Graph Neural Networks: Graph Structure Learning

最近研究GNN的应用方面&#xff0c;遇到了很大的瓶颈&#xff0c;所以回归理论&#xff0c;潜心阅读图结构学习的理论知识&#xff0c;也希望给大家在学习时带来帮助&#xff0c;如有错误请私信指正&#xff01; Graph Neural Networks: Graph Structure Learning 摘要&#…

Day912.多环境配置隔离 -SpringBoot与K8s云原生微服务实践

多环境配置隔离 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于多环境配置隔离的内容。 多环境支持&#xff0c;是现在互联网开发研发和交付的主流基本需求。通过规范多环境配置可以规范开发流程&#xff0c;并同时提示项目的开发质量和效率等。 一个公司应该规范…

计算机组成原理实验1---运算器 预习报告

本实验为哈尔滨工业大学计算机组成原理实验&#xff0c;实验内容均为个人完成&#xff0c;目的是分享交流&#xff0c;如有抄袭将追究责任&#xff0c;笔者能力有限&#xff0c;若因此影响读者的分数&#xff0c;本人深表抱歉。 一、 实验目的 了解运算器的组成结构基于数据通…

mybatis resource配置mapper

<mapper resource"com/hm/mapper/UserMapper.xml"/> 这种配置mapper和package配置资源的区别&#xff1f; 1.package解析mapper过程&#xff0c; 获取name对应的路径&#xff0c;获取class下的mapper,根据mapper里的路径拼接.xml解析文件&#xff0c;此时命名…

亚马逊、eBay、速卖通等跨境电商自养号测评,你知道多少?

自养号测评对跨境电商来说已经司空见惯&#xff0c;现在绝大部分卖家都有对店铺产品进行测评&#xff0c;虽然平台和消费者对测评补单很反感&#xff0c;但是在人人都测评的大环境的影响下不测评就意味着要被淘汰。 之前在一些论坛看到有博主分享自己对于广告和数据化的理解&a…

基于SpringBoot+Vue家乡特色推荐系统

您好&#xff0c;我是码农飞哥&#xff08;wei158556&#xff09;&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f4aa;&#x1f3fb; 1. Python基础专栏&#xff0c;基础知识一网打尽&#xff0c;9.9元买不了吃亏&#xff0c;买不了上当。 Python从入门到精…

6.S081——虚拟内存部分——xv6源码阅读系列(1)

0.Briefly Speaking 这篇博客是完成6.S081第三个实验之前的准备环节&#xff0c;主要内容是阅读相关的源码。之前提过xv6最宝贵的部分是内核源码&#xff0c;这些是完成实验之前必备的基础&#xff0c;也是学习这门课的精髓所在&#xff0c;所以我准备再开一个系列博客专门用来…

golang大杀器GMP模型

golang 大杀器——GMP模型 文章目录golang 大杀器——GMP模型1. 发展过程2. GMP模型设计思想2.1 GMP模型2.2 调度器的设计策略2.2.1 复用线程2.2.2 利用并行2.2.3 抢占策略2.2.4 全局G队列2.3 go func()经历了那些过程2.4 调度器的生命周期2.5 可视化的CMP编程2.5.1 trace方式2…

【设计模式】创建型模式之原型模式

【设计模式】创建型模式之原型模式 文章目录【设计模式】创建型模式之原型模式1.概述2. 构成3. 实现3.1 浅克隆3.2 深克隆1.概述 原型模式(Prototype Pattern)&#xff1a;是用于创建重复的对象&#xff0c;同时又能保证性能。这种类型的设计模式属于创建型模式&#xff0c;它…

【人工智能里的数学】线性代数基础

系列文章目录 【人工智能学习笔记】人工智能里的数学——概述 【人工智能里的数学】一元函数微分学 文章目录系列文章目录前言一、向量与其运算1.2 行向量和列向量1.3 向量的运算1.3.1 向量的加减1.3.2 向量的数乘运算1.3.3 转置1.3.4 运算法则1.3.5 向量的内积1.4 向量的范数…

今年面试好激烈!

金三银四过去一半&#xff0c;市场火热&#xff0c;但是大家就业压力却没有缓解多少。 很多粉丝后台留言&#xff0c;Java程序员面临的竞争太激烈了…… 我自己也有实感&#xff0c;多年身处一线互联网公司&#xff0c;虽没有直面过求职跳槽的残酷&#xff0c;但经常担任技术面…

记一次Git未Commit直接Pull导致本地代码丢失后的挽救过程

第一次遇到这种问题&#xff0c;有点紧张... 好吧&#xff0c;废话不多说&#xff0c;IDEA或者AndroidStudio进入Git Uncommiteed Changes -> Unstash Changes&#xff1a; 在弹出的Unstash Changes对话框点View查看代码&#xff0c;如果代码是本地丢失的代码&#xff0c;那…

MySQL——distinct与group by去重 / 松散索引扫描紧凑索引扫描

本篇介绍MySQL中的 distinct 和 group by的区别&#xff0c;包括用法、效率&#xff0c;涉及松散索引扫描和紧凑索引扫描的概念&#xff1b;distinct用法示例&#xff1a;SELECT DISTINCT columns FROM table_name WHERE where_conditions;DISTINCT关键词修饰查询的列&#xff…

CVE-2023-28708 原理剖析

CVE-2023-28708 原理剖析这应该不是一个严重的漏洞&#xff0c;可能评分只能为低&#xff0c;因为并没有什么卵用。 话不多说&#xff0c;直接进入正题 我的复现环境&#xff1a; tomcat-8.5.50 首先我们得简单写一个servlet&#xff0c;当然不写也没事&#xff0c;因为我们的…