0
点赞
收藏
分享

微信扫一扫

运维-使用go编写网络工具在容器中运行网络排障

需求

在目前的k8s云平台中,微服务都是以容器化运行的,基本上都是liunx的容器化部署。k8s平台大部分是docekr运行时,少量contaimerd运行时,平台架构大多是x86,少量的arm架构服务器。因为容器是轻量化部署,很多工具和软件包都没有安装,因此在遇到容器相关网络的故障的时候,在容器中无法使用传统的ping,telnet工具,因此排障困难,主要表现在:

  • 容器之间通过servcie访问,没有nslookup之类工具来判断域名解析
  • 从容器访问外部,没有telnet之类的工具来判断端口是否开放
  • 从容器访问接口,没有curl之类的工具来判断接口是否能够访问
  • 从容器访问外部,没有traceroute之间的工具来进行路由追踪来判断流量走向

当然,目前也办法解决容器内网络故障判断的问题,如:

  • 应用容器在构建的时候就加上telnet,curl相关工具,但是这个违反了最小化原则,可能存在合规问题,同时对于已经发版的生产业务不可能随时重新构建
  • 在同一个命名空间下部署busybox这类的带有网络工具的容器,但是如果网络策略配置或者命名空间不同,busybox的排查毕竟不是在有问题的容器中,存在的排查不准的问题


思路

如果我编译一个工具,这个工具包含了nslookup,curl之类的命令,只要把这个工具做成可执行文件,在容器中就可以运行,满足排障需求。思路如下:

  • 用go语言编写工具,实现nslooup ,telnet,curl,traceroute
  • go编写的工具要编译成linux可执行文件,能在容器中运行
  • go编写的工具,要支持x86和arm
  • 该工具不会对容器有影响,容器一旦重启工具就消失

实现

有了需求,借助AI,就能实现代码。里面较难的是traroute功能实现,引入了第三方模块,"golang.org/x/net/icmp"和 "golang.org/x/net/ipv4"。这两个包在go1.22版本兼容。

以下是程序,直接看注释把

//go version 1.22
//by yangchao
//v1.0
//网络工具箱 ,域名解析,端口扫描,curl,路由追踪
package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

	"golang.org/x/net/icmp"
	"golang.org/x/net/ipv4"
)

func main() {
	// 定义命令行标志
	nsLookup := flag.String("nslookup", "", "Query DNS for a domain name")
	portScan := flag.String("portscan", "", "Scan ports on a host")
	curlURL := flag.String("curl", "", "Send an HTTP GET request to a URL")
	traceRoute := flag.String("traceroute", "", "Trace the route to a host")
	// 添加其他标志
	flag.Parse()

	// 根据标志执行相应的操作
	if *nsLookup != "" {
		nslookup(*nsLookup)
	} else if *portScan != "" {
		scanPorts(*portScan)
	} else if *curlURL != "" {
		curl(*curlURL)
	} else if *traceRoute != "" {
		traceroute(*traceRoute)
	} else {
		fmt.Println("Usage: nettools  | -nslookup <domain>  | -portscan <host:ports> | -curl <url> | -traceroute <host>")
	}
}

func nslookup(domain string) {
	// 使用 net 包的 LookupHost 方法查询域名对应的IP地址列表。
	addrs, err := net.LookupHost(domain)
	// 如果查询过程中发生错误,则记录错误信息并终止程序。
	if err != nil {
		log.Fatalf("Failed to resolve %s: %v", domain, err)
	}
	// 遍历查询到的IP地址列表,并逐个打印。
	for _, addr := range addrs {
		fmt.Println(addr)
	}
}

// scanPorts 扫描指定目标的端口。
// 参数 target 是一个字符串,格式为 "host:port1,port2,..."。
// 如果未指定端口,函数将扫描一组常见的端口。
func scanPorts(target string) {
	// 分割目标字符串以获取主机和端口信息
	parts := strings.Split(target, ":")
	// 提取主机名
	host := parts[0]
	var ports []int

	// 检查是否有指定的端口
	if len(parts) > 1 {
		// 遍历并转换指定的端口字符串为整数
		for _, p := range strings.Split(parts[1], ",") {
			port, _ := strconv.Atoi(p)
			ports = append(ports, port)
		}
	} else {
		// 默认扫描常见端口
		ports = []int{21, 22, 23, 25, 80, 110, 143, 443, 465, 587, 993, 995}
	}

	// 遍历端口列表,尝试连接每个端口以确定其状态
	for _, port := range ports {
		address := fmt.Sprintf("%s:%d", host, port)
		// 使用TCP协议和3秒的超时时间尝试连接到指定地址
		conn, err := net.DialTimeout("tcp", address, 3*time.Second)
		if err == nil {
			// 如果连接成功,端口是开放的
			fmt.Printf("Port %d is open\n", port)
			// 关闭连接
			conn.Close()
		} else {
			// 如果连接失败,端口是关闭或无法到达的
			fmt.Printf("Port %d is closed or unreachable\n", port)
		}
	}
}

// curl 函数发送一个GET请求到指定的URL,并打印响应体。
// 这个函数主要用于快速测试和调试,通过模拟curl命令的行为来获取URL的内容并输出。
// 参数:
//
//	url - 指定要发送GET请求的URL地址。
func curl(url string) {
	// 发送GET请求到指定的URL。
	resp, err := http.Get(url)
	// 如果请求失败,则记录错误信息并退出。
	if err != nil {
		log.Fatalf("Failed to send GET request to %s: %v", url, err)
	}
	// 确保在函数返回前关闭响应体。
	defer resp.Body.Close()

	// 读取响应体的内容。
	body, err := ioutil.ReadAll(resp.Body)
	// 如果读取失败,则记录错误信息并退出。
	if err != nil {
		log.Fatalf("Failed to read response body: %v", err)
	}

	// 打印响应体的内容。
	fmt.Println(string(body))
}

// traceroute 实现了一个 traceroute 功能,用于追踪 IP 地址的路由路径。
// 参数:
//
//	host: 目标主机的域名或 IP 地址。
func traceroute(host string) {
	// 设置超时时间为10秒。
	timeout := time.Second * 10
	// 监听IPv4的ICMP包。
	conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
	if err != nil {
		fmt.Printf("error listening for ICMP packets: %s", err)
	}
	defer conn.Close()

	// 解析目标主机的IP地址。
	destinationAddress, err := net.ResolveIPAddr("ip4", host)
	if err != nil {
		fmt.Printf("error resolving hostname: %s", err)
	}
	fmt.Printf("Traceroute to '%s' [%s]\n", host, destinationAddress.IP)

	// 构建ICMP消息。
	message := icmp.Message{
		Type: ipv4.ICMPTypeEcho,
		Code: 0,
		Body: &icmp.Echo{
			ID:   os.Getpid() & 0xffff,
			Data: []byte("hello"),
		},
	}

	// 主循环,尝试最多30跳。
	for ttl := 1; ttl <= 30; ttl++ {
		fmt.Printf("%d  ", ttl)

		// 设置ICMP消息的序列号。
		message.Body.(*icmp.Echo).Seq = ttl

		// 序列化ICMP消息。
		messageBytes, err := message.Marshal(nil)
		if err != nil {
			fmt.Printf("error marshaling ICMP message: %s", err)
		}

		// 设置TTL值。
		if err := conn.IPv4PacketConn().SetTTL(ttl); err != nil {
			fmt.Printf("error setting TTL: %s", err)
		}

		// 发送ICMP包。
		start := time.Now()
		if _, err := conn.WriteTo(messageBytes, destinationAddress); err != nil {
			fmt.Printf("error sending ICMP packet: %s", err)
		}

		// 接收响应。
		responseBytes := make([]byte, 1500)
		conn.SetReadDeadline(time.Now().Add(timeout))
		n, remoteAddress, err := conn.ReadFrom(responseBytes)
		if err != nil {
			if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
				fmt.Println("* (Timeout)")
			} else {
				fmt.Println("* (Error)")
			}
			continue
		}

		// 计算往返时间。
		duration := time.Since(start)
		responseMessage, err := icmp.ParseMessage(ipv4.ICMPTypeEchoReply.Protocol(), responseBytes[:n])
		if err != nil {
			fmt.Printf("error parsing ICMP message: %s", err)
		}

		// 根据响应类型打印结果。
		switch responseMessage.Type {
		case ipv4.ICMPTypeTimeExceeded:
			fmt.Printf("%v  %v ms\n", remoteAddress, duration.Milliseconds())
		case ipv4.ICMPTypeEchoReply:
			fmt.Printf("%v  %v ms\n", remoteAddress, duration.Milliseconds())
			fmt.Println("Traceroute Complete.")
		default:
			fmt.Printf("unexpected ICMP message type: %v, code: %v", responseMessage.Type, responseMessage.Code)
		}
	}
}

编译

go build nettools  //编译出来的可执行文件为nettools
./nettools         //运行

运行效果

运维-使用go编写网络工具在容器中运行网络排障_golang

测试

运维-使用go编写网络工具在容器中运行网络排障_golang_02

运维-使用go编写网络工具在容器中运行网络排障_可执行文件_03

(不要在意哪些错误)

容器中运行

在k8s中,使用cp命令工具复制到容器中,语法如下

kubectl cp <local-file-path> <pod-name>:<container-file-path>

或者直接通过控制台

运维-使用go编写网络工具在容器中运行网络排障_golang_04

工具传输到容器之中后需要配置可执行权限

chmod +x nettools

容器中执行效果

运维-使用go编写网络工具在容器中运行网络排障_可执行文件_05

运维-使用go编写网络工具在容器中运行网络排障_IP_06

看来达到了预计效果

总结

1、如果要arm的可执行文件,则需要在有arm的go环境中编译

2、traroute追踪k8s的内部的servcie,会出现环路,好在有TTL

3、ping 本次没有实现,但是也不太难,主要是ping在容器排障中可以用nslookup代替

举报

相关推荐

0 条评论