diff --git a/README.md b/README.md index 446cf25..87fb9ab 100644 --- a/README.md +++ b/README.md @@ -80,15 +80,17 @@ make k8snetlook-osx By having to initialize kubernetes client-set, the tool intrinsically performs API connectivity check via K8s-apiserver's VIP/External Loadbalancer in case of highly available k8s-apiserver clusters -| Host Checks | Pod Checks | -| ------------------------------------------------ | ------------------------------------------------ | -| Default gateway connectivity (icmp) | Default gateway connectivity (icmp) | -| K8s-apiserver ClusterIP check (https) | K8s-apiserver ClusterIP check (https) | -| K8s-apiserver individual endpoints check (https) | K8s-apiserver individual endpoints check (https) | -| K8s-apiserver health-check api (livez) | Destination Pod IP connectivity (icmp) | -| | External IP connectivity (icmp) | -| | K8s DNS name lookup check (kubernetes.local) | -| | K8s DNS name lookup for specific service check | +| Host Checks | Pod Checks | +| ------------------------------------------------ | ------------------------------------------------------- | +| Default gateway connectivity (icmp) | Default gateway connectivity (icmp) | +| K8s-apiserver ClusterIP check (https) | K8s-apiserver ClusterIP check (https) | +| K8s-apiserver individual endpoints check (https) | K8s-apiserver individual endpoints check (https) | +| K8s-apiserver health-check api (livez) | Destination Pod IP connectivity (icmp) | +| | External IP connectivity (icmp) | +| | K8s DNS name lookup check (kubernetes.local) | +| | K8s DNS name lookup for specific service check | +| | Path MTU discovery between Src & Dst Pod (icmp) | +| | Path MTU discovery between Src Pod & External IP (icmp) | ## Contribute diff --git a/cmd/k8snetlook/main.go b/cmd/k8snetlook/main.go index b45de82..1b99293 100644 --- a/cmd/k8snetlook/main.go +++ b/cmd/k8snetlook/main.go @@ -69,7 +69,11 @@ func main() { k8snetlook.RunHostChecks() if podDebugging == true { k8snetlook.RunPodChecks() + fmt.Println("----------- Pod Checks Summary -----------") + fmt.Printf("Passed checks: %d/%d\n", k8snetlook.PassingPodChecks, k8snetlook.TotalPodChecks) } + fmt.Println("----------- Host Checks Summary -----------") + fmt.Printf("Passed checks: %d/%d\n", k8snetlook.PassingHostChecks, k8snetlook.TotalHostChecks) } func validateArgs() { diff --git a/go.mod b/go.mod index 75098f0..339c8f3 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.14 require ( github.com/gogo/protobuf v1.3.1 // indirect github.com/google/gofuzz v1.1.0 // indirect + github.com/google/gopacket v1.1.18 github.com/googleapis/gnostic v0.4.0 // indirect github.com/imdario/mergo v0.3.10 // indirect github.com/miekg/dns v1.1.31 diff --git a/go.sum b/go.sum index 8ff404c..1351651 100644 --- a/go.sum +++ b/go.sum @@ -50,6 +50,8 @@ github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gopacket v1.1.18 h1:lum7VRA9kdlvBi7/v2p7/zcbkduHaCH/SVVyurs7OpY= +github.com/google/gopacket v1.1.18/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -153,6 +155,7 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/k8snetlook/checkers.go b/k8snetlook/checkers.go index 571044e..e4ec9be 100644 --- a/k8snetlook/checkers.go +++ b/k8snetlook/checkers.go @@ -2,16 +2,19 @@ package k8snetlook import ( "fmt" + "net" "net/http" + + "github.com/sarun87/k8snetlook/netutils" ) func RunGatewayConnectivityCheck(checkCounter *int) { - pass, err := sendRecvICMPMessage(Cfg.HostGatewayIP) + pass, err := netutils.SendRecvICMPMessage(Cfg.HostGatewayIP, 64, true) if err != nil { fmt.Printf(" (Failed) Error running RunGatewayConnectivityCheck. Error: %v\n", err) return } - if pass { + if pass == 0 { *checkCounter++ fmt.Println(" (Passed) Gateway connectivity check completed successfully") } else { @@ -20,12 +23,12 @@ func RunGatewayConnectivityCheck(checkCounter *int) { } func RunDstConnectivityCheck(dstIP string, checkCounter *int) { - pass, err := sendRecvICMPMessage(dstIP) + pass, err := netutils.SendRecvICMPMessage(dstIP, 64, true) if err != nil { fmt.Printf(" (Failed) Error running connectivity check to %s. Error: %v\n", dstIP, err) return } - if pass { + if pass == 0 { *checkCounter++ fmt.Printf(" (Passed) Connectivity check to destination %s completed successfully\n", dstIP) } else { @@ -38,7 +41,7 @@ func RunKubeAPIServiceIPConnectivityCheck(checkCounter *int) { // HTTP 401 return code is a successful check url := fmt.Sprintf("https://%s:%d", Cfg.KubeAPIService.IP, Cfg.KubeAPIService.Port) var body []byte - responseCode, err := sendRecvHTTPMessage(url, "", &body) + responseCode, err := netutils.SendRecvHTTPMessage(url, "", &body) if err != nil { fmt.Printf(" (Failed) Error running RunKubeAPIServiceIPConnectivityCheck. Error: %v\n", err) return @@ -61,7 +64,7 @@ func RunKubeAPIEndpointIPConnectivityCheck(checkCounter *int) { url := fmt.Sprintf("https://%s:%d", ep.IP, ep.Port) fmt.Printf(" checking endpoint: %s ........", url) var body []byte - responseCode, err := sendRecvHTTPMessage(url, "", &body) + responseCode, err := netutils.SendRecvHTTPMessage(url, "", &body) if err != nil { fmt.Printf(" failed connectivity check. Error: %v\n", err) continue @@ -89,7 +92,7 @@ func RunAPIServerHealthCheck(checkCounter *int) { return } var body []byte - responseCode, err := sendRecvHTTPMessage(url, svcAccountToken, &body) + responseCode, err := netutils.SendRecvHTTPMessage(url, svcAccountToken, &body) if err != nil { fmt.Printf(" Unable to fetch api server check. Error: %v\n", err) return @@ -107,7 +110,7 @@ func RunK8sDNSLookupCheck(dnsServerIP, dstSvcName, dstSvcNamespace, dstSvcExpect dnsServerURL := fmt.Sprintf("%s:53", dnsServerIP) // TODO: Fetch domain information from cluster svcfqdn := fmt.Sprintf("%s.%s.svc.cluster.local.", dstSvcName, dstSvcNamespace) - ips, err := runDNSLookupUsingCustomResolver(dnsServerURL, svcfqdn) + ips, err := netutils.RunDNSLookupUsingCustomResolver(dnsServerURL, svcfqdn) if err != nil { fmt.Printf(" (Failed) Unable to run dns lookup to %s, error: %v\n", svcfqdn, err) return @@ -123,3 +126,29 @@ func RunK8sDNSLookupCheck(dnsServerIP, dstSvcName, dstSvcNamespace, dstSvcExpect fmt.Printf(" (Failed) Lookup of %s retured: %v, expected: %s\n", svcfqdn, ips, dstSvcExpectedIP) return } + +func RunMTUProbeToDstIPCheck(dstIP string, checkCounter *int) { + supportedMTU, err := netutils.PMTUProbeToDestIP(dstIP) + if err != nil { + fmt.Printf(" (Failed) Unable to run pmtud for %s. Error: %v\n", dstIP, err) + return + } + fmt.Printf(" Maximum MTU that works for destination IP: %s is %d\n", dstIP, supportedMTU) + ifaces, err := net.Interfaces() + if err != nil { + fmt.Printf(" Unable to fetch network interfaces. Error: %v\n", err) + return + } + for _, iface := range ifaces { + // If loopback device, skip + if iface.Flags&net.FlagLoopback == net.FlagLoopback { + continue + } + if iface.MTU > supportedMTU { + fmt.Printf(" Iface %s has higher mtu than supported path mtu. Has: %d, should be less than %d\n", iface.Name, iface.MTU, supportedMTU) + } + } + *checkCounter++ + // TODO: Check for the outgoing interface mtu and compare + fmt.Printf(" (Passed) MTU looks good.. Retured MTU: %d\n", supportedMTU) +} diff --git a/k8snetlook/host.go b/k8snetlook/host.go index b4a40b3..028a455 100644 --- a/k8snetlook/host.go +++ b/k8snetlook/host.go @@ -5,25 +5,24 @@ import ( ) const ( - totalHostChecks = 4 + TotalHostChecks = 4 ) var ( - passingHostChecks int + PassingHostChecks int ) func RunHostChecks() { fmt.Println("----------- Host Checks -----------") fmt.Println("----> [From Host] Running default gateway connectivity check..") - RunGatewayConnectivityCheck(&passingHostChecks) + RunGatewayConnectivityCheck(&PassingHostChecks) fmt.Println("----> [From Host] Running Kube service IP connectivity check..") - RunKubeAPIServiceIPConnectivityCheck(&passingHostChecks) + RunKubeAPIServiceIPConnectivityCheck(&PassingHostChecks) fmt.Println("----> [From Host] Running Kube API Server Endpoint IP connectivity check..") - RunKubeAPIEndpointIPConnectivityCheck(&passingHostChecks) + RunKubeAPIEndpointIPConnectivityCheck(&PassingHostChecks) fmt.Println("----> [From Host] Running Kube API Server health check..") - RunAPIServerHealthCheck(&passingHostChecks) + RunAPIServerHealthCheck(&PassingHostChecks) - fmt.Println("----------- Host Checks Summary -----------") - fmt.Printf("Passed checks: %d/%d\n", passingHostChecks, totalHostChecks) + fmt.Println("-----------------------------------") } diff --git a/k8snetlook/init.go b/k8snetlook/init.go index 679d707..609dae7 100644 --- a/k8snetlook/init.go +++ b/k8snetlook/init.go @@ -5,6 +5,7 @@ import ( "os" "strings" + "github.com/sarun87/k8snetlook/netutils" "github.com/vishvananda/netns" ) @@ -56,7 +57,7 @@ func InitKubeClient(kubeconfigPath string) { func InitK8sInfo() { Cfg.KubeAPIService = getServiceClusterIP("default", "kubernetes") Cfg.KubeDNSService = getServiceClusterIP("kube-system", "kube-dns") - Cfg.HostGatewayIP = getHostGatewayIP() + Cfg.HostGatewayIP = netutils.GetHostGatewayIP() Cfg.SrcPod.NsHandle = netns.NsHandle(-1) if Cfg.SrcPod.Name != "" && Cfg.SrcPod.Namespace != "" { Cfg.SrcPod.IP = getPodIPFromName(Cfg.SrcPod.Namespace, Cfg.SrcPod.Name) diff --git a/k8snetlook/netutils.go b/k8snetlook/netutils.go deleted file mode 100644 index 63c4f54..0000000 --- a/k8snetlook/netutils.go +++ /dev/null @@ -1,173 +0,0 @@ -package k8snetlook - -import ( - "crypto/tls" - "errors" - "fmt" - "io/ioutil" - "net" - "net/http" - "os" - "time" - - "golang.org/x/net/icmp" - "golang.org/x/net/ipv4" - "golang.org/x/sys/unix" - - "github.com/miekg/dns" - "github.com/vishvananda/netlink" -) - -const ( - icmpTimeout = 4 -) - -func getHostGatewayIP() string { - routes, err := netlink.RouteList(nil, unix.AF_INET) - if err != nil { - return "" - } - for _, r := range routes { - if r.Gw != nil { - return r.Gw.String() - } - } - return "" -} - -func sendRecvICMPMessage(dstIP string) (bool, error) { - // Listen on all IPs - c, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0") - if err != nil { - return false, fmt.Errorf("Unable to open icmp socket for ping test: %v", err) - } - defer c.Close() - // Create icmp message - wm := icmp.Message{ - Type: ipv4.ICMPTypeEcho, Code: 0, - Body: &icmp.Echo{ - ID: os.Getpid() & 0xffff, Seq: 1, - Data: []byte("K8SNETLOOK-R-U-THERE"), - }, - } - // convert icmp message to byte string - wb, err := wm.Marshal(nil) - if err != nil { - return false, fmt.Errorf("Unable to convert icmp echo message to byte string: %v", err) - } - - fmt.Printf(" ping: Sending echo request to %s ......", dstIP) - if _, err := c.WriteTo(wb, &net.IPAddr{IP: net.ParseIP(dstIP)}); err != nil { - return false, fmt.Errorf("Unable to send icmp echo request to %s:%v", dstIP, err) - } - rb := make([]byte, 1500) - c.SetReadDeadline(time.Now().Add(time.Second * icmpTimeout)) - // Read reply. Try twice. Discard echo request if read back on 127.0.0.1 (Needed for unit tests) - for tries := 0; tries < 2; tries++ { - n, peer, err := c.ReadFrom(rb) - if err != nil { - if err.(net.Error).Timeout() { - return false, fmt.Errorf("ICMP timeout") - } - return false, fmt.Errorf("Unable to read reply from icmp socket: %v", err) - } - // Check if read message is an ICMP message - rm, err := icmp.ParseMessage(ipv4.ICMPTypeEchoReply.Protocol(), rb[:n]) - if err != nil { - return false, fmt.Errorf("Unable to parse ICMP message:%v", err) - } - // Check to see if ICMP message type is ECHO reply - switch rm.Type { - case ipv4.ICMPTypeEchoReply: - fmt.Printf(" got reflection from %v\n", peer) - return true, nil - default: - fmt.Printf(" got %+v; want echo reply\n", rm) - // Try multiple messages - } - } - // Got ICMP type but not an echo reply - return false, nil -} - -func sendRecvHTTPMessage(url string, token string, body *[]byte) (int, error) { - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - client := &http.Client{Transport: tr, Timeout: time.Duration(5) * time.Second} - req, err := http.NewRequest("GET", url, nil) - if token != "" { - req.Header.Add("Authorization", "Bearer "+token) - } - res, err := client.Do(req) - if err != nil { - return -1, fmt.Errorf("HTTP request to %s failed: %v", url, err) - } - *body, _ = ioutil.ReadAll(res.Body) - res.Body.Close() - return res.StatusCode, nil -} - -// Does not work, not sure why :( -/*func runDNSLookupUsingCustomResolver(dnsFQDN) ([]net.IPAddr, error) { - // Create a custom resolver since /etc/resolv.conf is the host's configuration - // and not the pods config. This is because the below code is executed within - // the Pod netns but not the file system of the pod. - r := &net.Resolver{ - PreferGo: true, - Dial: func(ctx context.Context, network, address string) (net.Conn, error) { - d := net.Dialer{ - Timeout: time.Millisecond * time.Duration(10000), - } - //return net.Dial(network, net.JoinHostPort(dnsServerIP, "53")) - return d.DialContext(ctx, network, net.JoinHostPort(dnsServerIP, "53")) - }, - } - // Lookup for svcname.svcnamespace eg: kubernetes.default - dnsFQDN := fmt.Sprintf("%s.%s", dstSvcName, dstSvcNamespace) - ips, err := r.LookupIPAddr(context.Background(), dnsFQDN) - if err != nil { - fmt.Printf(" (Failed) dns lookup to %s failed. Error: %v\n", dnsFQDN, err) - return nil, err - } - return ips, nil -} -*/ - -// run dns lookup using github.com/miekg/dns -// code referenced from: https://github.com/bogdanovich/dns_resolver -// nameserver string format: "ip:port" -// hostFQDN string format: "abc.def.ghi." -func runDNSLookupUsingCustomResolver(nameserver, hostFQDN string) ([]string, error) { - // TODO: Add retries - - // Create DNS Message with single question - msg := &dns.Msg{ - MsgHdr: dns.MsgHdr{ - Id: dns.Id(), - RecursionDesired: true, - }, - Question: []dns.Question{{Name: dns.Fqdn(hostFQDN), Qtype: dns.TypeA, Qclass: dns.ClassINET}}, - } - - // Send question to nameserver and wait for answer - in, err := dns.Exchange(msg, nameserver) - if err != nil { - return nil, err - } - - result := []string{} - - if in != nil && in.Rcode != dns.RcodeSuccess { - // Return error code - return result, errors.New(dns.RcodeToString[in.Rcode]) - } - - // Fetch IP Addresses in DNS Answer and return - for _, record := range in.Answer { - if t, ok := record.(*dns.A); ok { - result = append(result, t.A.String()) - } - } - return result, err -} diff --git a/k8snetlook/pod.go b/k8snetlook/pod.go index 7879132..6167589 100644 --- a/k8snetlook/pod.go +++ b/k8snetlook/pod.go @@ -8,8 +8,8 @@ import ( ) var ( - passingPodChecks int - totalPodChecks int + PassingPodChecks int + TotalPodChecks int ) func RunPodChecks() { @@ -19,7 +19,7 @@ func RunPodChecks() { fmt.Println("----------- Pod Checks -----------") - totalPodChecks = 4 + TotalPodChecks = 4 // Lock OS thread to prevent ns change runtime.LockOSThread() defer runtime.UnlockOSThread() @@ -40,37 +40,42 @@ func RunPodChecks() { // Execute checks from within the Pod network ns fmt.Println("----> [From SrcPod] Running Kube service IP connectivity check..") - RunKubeAPIServiceIPConnectivityCheck(&passingPodChecks) + RunKubeAPIServiceIPConnectivityCheck(&PassingPodChecks) fmt.Println("----> [From SrcPod] Running Kube API Server Endpoint IP connectivity check..") - RunKubeAPIEndpointIPConnectivityCheck(&passingPodChecks) + RunKubeAPIEndpointIPConnectivityCheck(&PassingPodChecks) fmt.Println("----> [From SrcPod] Running default gateway connectivity check..") - RunGatewayConnectivityCheck(&passingPodChecks) + RunGatewayConnectivityCheck(&PassingPodChecks) fmt.Println("----> [From SrcPod] Running DNS lookup test (kubernetes.default)..") RunK8sDNSLookupCheck(Cfg.KubeDNSService.IP, "kubernetes", "default", - Cfg.KubeAPIService.IP, &passingPodChecks) + Cfg.KubeAPIService.IP, &PassingPodChecks) if Cfg.DstPod.IP != "" { - totalPodChecks++ + TotalPodChecks++ fmt.Println("----> [From SrcPod] Running DstPod connectivity check..") - RunDstConnectivityCheck(Cfg.DstPod.IP, &passingPodChecks) + RunDstConnectivityCheck(Cfg.DstPod.IP, &PassingPodChecks) + TotalPodChecks++ + fmt.Println("----> [From SrcPod] Running pmtud check for dstIP..") + RunMTUProbeToDstIPCheck(Cfg.DstPod.IP, &PassingPodChecks) } if Cfg.ExternalIP != "" { - totalPodChecks++ + TotalPodChecks++ fmt.Println("----> [From SrcPod] Running externalIP connectivity check..") - RunDstConnectivityCheck(Cfg.ExternalIP, &passingPodChecks) + RunDstConnectivityCheck(Cfg.ExternalIP, &PassingPodChecks) + TotalPodChecks++ + fmt.Println("----> [From SrcPod] Running pmtud check for externalIP..") + RunMTUProbeToDstIPCheck(Cfg.ExternalIP, &PassingPodChecks) } if Cfg.DstSvc.SvcEndpoint.IP != "" { - totalPodChecks++ + TotalPodChecks++ fmt.Println("----> [From SrcPod] Running DstSvc DNS lookup check..") RunK8sDNSLookupCheck(Cfg.KubeDNSService.IP, Cfg.DstSvc.Name, Cfg.DstSvc.Namespace, - Cfg.DstSvc.SvcEndpoint.IP, &passingPodChecks) + Cfg.DstSvc.SvcEndpoint.IP, &PassingPodChecks) } // Change network ns back to host netns.Set(hostNsHandle) - fmt.Println("----------- Pod Checks Summary -----------") - fmt.Printf("Passed checks: %d/%d\n", passingPodChecks, totalPodChecks) + fmt.Println("-----------------------------------") } diff --git a/netutils/dnsutil.go b/netutils/dnsutil.go new file mode 100644 index 0000000..c131dc4 --- /dev/null +++ b/netutils/dnsutil.go @@ -0,0 +1,71 @@ +package netutils + +import ( + "errors" + + "github.com/miekg/dns" +) + +// Does not work, not sure why :( +/*func runDNSLookupUsingCustomResolver(dnsFQDN) ([]net.IPAddr, error) { + // Create a custom resolver since /etc/resolv.conf is the host's configuration + // and not the pods config. This is because the below code is executed within + // the Pod netns but not the file system of the pod. + r := &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + d := net.Dialer{ + Timeout: time.Millisecond * time.Duration(10000), + } + //return net.Dial(network, net.JoinHostPort(dnsServerIP, "53")) + return d.DialContext(ctx, network, net.JoinHostPort(dnsServerIP, "53")) + }, + } + // Lookup for svcname.svcnamespace eg: kubernetes.default + dnsFQDN := fmt.Sprintf("%s.%s", dstSvcName, dstSvcNamespace) + ips, err := r.LookupIPAddr(context.Background(), dnsFQDN) + if err != nil { + fmt.Printf(" (Failed) dns lookup to %s failed. Error: %v\n", dnsFQDN, err) + return nil, err + } + return ips, nil +} +*/ + +// run dns lookup using github.com/miekg/dns +// code referenced from: https://github.com/bogdanovich/dns_resolver +// nameserver string format: "ip:port" +// hostFQDN string format: "abc.def.ghi." +func RunDNSLookupUsingCustomResolver(nameserver, hostFQDN string) ([]string, error) { + // TODO: Add retries + + // Create DNS Message with single question + msg := &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Id: dns.Id(), + RecursionDesired: true, + }, + Question: []dns.Question{{Name: dns.Fqdn(hostFQDN), Qtype: dns.TypeA, Qclass: dns.ClassINET}}, + } + + // Send question to nameserver and wait for answer + in, err := dns.Exchange(msg, nameserver) + if err != nil { + return nil, err + } + + result := []string{} + + if in != nil && in.Rcode != dns.RcodeSuccess { + // Return error code + return result, errors.New(dns.RcodeToString[in.Rcode]) + } + + // Fetch IP Addresses in DNS Answer and return + for _, record := range in.Answer { + if t, ok := record.(*dns.A); ok { + result = append(result, t.A.String()) + } + } + return result, err +} diff --git a/netutils/httputil.go b/netutils/httputil.go new file mode 100644 index 0000000..5885a3d --- /dev/null +++ b/netutils/httputil.go @@ -0,0 +1,27 @@ +package netutils + +import ( + "crypto/tls" + "fmt" + "io/ioutil" + "net/http" + "time" +) + +func SendRecvHTTPMessage(url string, token string, body *[]byte) (int, error) { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client := &http.Client{Transport: tr, Timeout: time.Duration(5) * time.Second} + req, err := http.NewRequest("GET", url, nil) + if token != "" { + req.Header.Add("Authorization", "Bearer "+token) + } + res, err := client.Do(req) + if err != nil { + return -1, fmt.Errorf("HTTP request to %s failed: %v", url, err) + } + *body, _ = ioutil.ReadAll(res.Body) + res.Body.Close() + return res.StatusCode, nil +} diff --git a/netutils/icmputil.go b/netutils/icmputil.go new file mode 100644 index 0000000..bfea639 --- /dev/null +++ b/netutils/icmputil.go @@ -0,0 +1,131 @@ +package netutils + +import ( + "fmt" + "net" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "golang.org/x/net/icmp" + "golang.org/x/net/ipv4" +) + +const ( + icmpTimeout = 4 + icmpMessageBody = "K8SNETLOOK-ICMP-TEST" + defaultPayloadSize = 64 +) + +// Picked from https://github.com/ipsecdiagtool/ipsecdiagtool project & modified as necessary +func sendICMPMessage(dstIP string, payloadSize int, dontfragment bool) error { + if payloadSize < defaultPayloadSize { + payloadSize = defaultPayloadSize + } + //IP Layer + ip := layers.IPv4{ + SrcIP: net.ParseIP("0.0.0.0"), + DstIP: net.ParseIP(dstIP), + Version: 4, + TTL: 64, + Protocol: layers.IPProtocolICMPv4, + } + icmp := layers.ICMPv4{ + TypeCode: layers.CreateICMPv4TypeCode(uint8(ipv4.ICMPTypeEcho), 0), + } + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + ipHeaderBuf := gopacket.NewSerializeBuffer() + if err := ip.SerializeTo(ipHeaderBuf, opts); err != nil { + return err + } + ipHeader, err := ipv4.ParseHeader(ipHeaderBuf.Bytes()) + if err != nil { + return err + } + if dontfragment { + //Set "Don't Fragment"-Flag in Header + ipHeader.Flags |= ipv4.DontFragment + } + + payloadBuf := gopacket.NewSerializeBuffer() + //Influence the payload size + payloadbytes := []byte(icmpMessageBody) + if payloadSize > len(icmpMessageBody) { + padding := make([]byte, payloadSize-len(icmpMessageBody)) + payloadbytes = append(payloadbytes, padding...) + } + payload := gopacket.Payload(payloadbytes) + if err := gopacket.SerializeLayers(payloadBuf, opts, &icmp, payload); err != nil { + return err + } + //Send packet + packetConn, err := net.ListenPacket("ip4:icmp", "0.0.0.0") + if err != nil { + return err + } + defer packetConn.Close() + rawConn, err := ipv4.NewRawConn(packetConn) + if err != nil { + return err + } + defer rawConn.Close() + return rawConn.WriteTo(ipHeader, payloadBuf.Bytes(), nil) +} + +// SendRecvICMPMessage checks if icmp ping is successful. +// Looks at 2 icmp packets for required response +// returncode: 0 - no error. Echo reply received successfully +// 1 - Fragmentation required +// 2 - got icmp but unknwon type +func SendRecvICMPMessage(dstIP string, payloadSize int, dontFragment bool) (int, error) { + // Listen on all IPs + c, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0") + if err != nil { + return -1, fmt.Errorf("Unable to open icmp socket for ping test: %v", err) + } + defer c.Close() + + if err := sendICMPMessage(dstIP, payloadSize, dontFragment); err != nil { + return -1, err + } + rb := make([]byte, payloadSize) + c.SetReadDeadline(time.Now().Add(time.Second * icmpTimeout)) + + // Read reply. Try twice. Discard echo request if read back on 127.0.0.1 (Needed for unit tests) + for tries := 0; tries < 2; tries++ { + n, peer, err := c.ReadFrom(rb) + if err != nil { + if err.(net.Error).Timeout() { + return -1, fmt.Errorf("ICMP timeout") + } + return -1, fmt.Errorf("Unable to read reply from icmp socket: %v", err) + } + // Check if read message is an ICMP message + rm, err := icmp.ParseMessage(ipv4.ICMPTypeEchoReply.Protocol(), rb[:n]) + if err != nil { + return -1, fmt.Errorf("Unable to parse ICMP message:%v", err) + } + // Check to see if ICMP message type is ECHO reply + switch rm.Type { + case ipv4.ICMPTypeEchoReply: + fmt.Printf(" got reflection from %v with payload size:%d\n", peer, payloadSize) + // To check if echo reply is specific to this app, check message + // b, _ := rm.Body.Marshal(1) // 1 : ICMPv4 type protocol number + // icmpMessage == string(b[2:2+len(icmpMessageBody)]) // First two bytes: length of body + return 0, nil + case ipv4.ICMPTypeDestinationUnreachable: + if rm.Code == layers.ICMPv4CodeFragmentationNeeded { + // fmt.Printf(" Fragmentation required, and DF flag set\n") + return 1, nil + } + default: + //fmt.Printf(" got %+v; want echo reply\n", rm) + // Try multiple messages + } + } + // Got ICMP type but not an echo reply + return 2, nil +} diff --git a/k8snetlook/netutils_test.go b/netutils/icmputil_test.go similarity index 68% rename from k8snetlook/netutils_test.go rename to netutils/icmputil_test.go index 3d664f1..c1c8850 100644 --- a/k8snetlook/netutils_test.go +++ b/netutils/icmputil_test.go @@ -1,23 +1,23 @@ -package k8snetlook +package netutils import ( "testing" ) func TestSendRcvICMPMessageSuccess(t *testing.T) { - ret, err := sendRecvICMPMessage("127.0.0.1") + ret, err := SendRecvICMPMessage("127.0.0.1", 64, true) if err != nil { t.Errorf("ICMP reply expected from localhost. Received error: %s", err) return } - if ret != true { - t.Errorf("Expected (true, nil) but received (false, nil). received nil for error") + if ret != 0 { + t.Errorf("Expected (0, nil) but received (%d, nil).", ret) } } func TestSendRcvICMPMessageFailure(t *testing.T) { // Using arbitary IP for failure test - _, err := sendRecvICMPMessage("192.192.192.192") + _, err := SendRecvICMPMessage("192.192.192.192", 64, true) if err == nil { t.Errorf("Expected ICMP to arbitary IP to fail with a timeout") } diff --git a/netutils/netutils.go b/netutils/netutils.go new file mode 100644 index 0000000..3e335d9 --- /dev/null +++ b/netutils/netutils.go @@ -0,0 +1,20 @@ +package netutils + +import ( + "golang.org/x/sys/unix" + + "github.com/vishvananda/netlink" +) + +func GetHostGatewayIP() string { + routes, err := netlink.RouteList(nil, unix.AF_INET) + if err != nil { + return "" + } + for _, r := range routes { + if r.Gw != nil { + return r.Gw.String() + } + } + return "" +} diff --git a/netutils/pmtuprobe.go b/netutils/pmtuprobe.go new file mode 100644 index 0000000..7f801a3 --- /dev/null +++ b/netutils/pmtuprobe.go @@ -0,0 +1,55 @@ +package netutils + +import ( + "net" +) + +const ( + ipHeaderSize = 20 + icmpHeaderSize = 8 + maxMTUSize = 9000 +) + +// PMTUProbeToDestIP runs ICMP pings to destination with varying payload size +// and returns the highest MTU that works. Currently works for IPv4 only +func PMTUProbeToDestIP(dstIP string) (int, error) { + var maxOkMTU int + minPayloadSize, maxPayloadSize := (icmpHeaderSize + ipHeaderSize), (maxMTUSize - icmpHeaderSize - ipHeaderSize) + + res, err := SendRecvICMPMessage(dstIP, minPayloadSize, true) + if err != nil || res == 1 { + return -1, err + } + maxOkMTU = minPayloadSize + // Use binary search to check for working mtu + for minPayloadSize <= maxPayloadSize { + midPayloadSize := (minPayloadSize + maxPayloadSize) / 2 + //fmt.Println("Trying with mtu size:", midPayloadSize) + ret, err := SendRecvICMPMessage(dstIP, midPayloadSize, true) + if err != nil { + //fmt.Println("Received error:", err) + if e, ok := err.(*net.OpError); ok { + // Check if send failed due to Message too long (i.e. paylod > src if mtu) + if e.Err.Error() == "sendmsg: message too long" { + //fmt.Println("WARN: Cannot send packet size larger than iface MTU") + // Go lower + maxPayloadSize = midPayloadSize - 1 + continue + } + } else { + // Some other error. Not handling this as part of mtu probing + return -1, err + } + } + if ret == 1 { + // if return code is 1, icmp reply had fragmentation required. So go loweer + maxPayloadSize = midPayloadSize - 1 + } else { + // successful icmp response. Go higher + minPayloadSize = midPayloadSize + 1 + maxOkMTU = midPayloadSize + //fmt.Println("MTU works with size:", midPayloadSize) + } + } + return maxOkMTU + ipHeaderSize + icmpHeaderSize, nil +}