需求
在目前的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 //运行
运行效果
测试
(不要在意哪些错误)
容器中运行
在k8s中,使用cp命令工具复制到容器中,语法如下
kubectl cp <local-file-path> <pod-name>:<container-file-path>
或者直接通过控制台
工具传输到容器之中后需要配置可执行权限
chmod +x nettools
容器中执行效果
看来达到了预计效果
总结
1、如果要arm的可执行文件,则需要在有arm的go环境中编译
2、traroute追踪k8s的内部的servcie,会出现环路,好在有TTL
3、ping 本次没有实现,但是也不太难,主要是ping在容器排障中可以用nslookup代替