From 711fe31a4fbaad5e10b5505cc2cb4600e2da3e0e Mon Sep 17 00:00:00 2001 From: Tim Swanson Date: Wed, 11 Aug 2021 15:40:36 -0400 Subject: [PATCH 1/6] Add hostnet addresses from k8s node obj to kubenet provider --- providers/kube/client/client.go | 11 +++++++++++ providers/kube/client/pod.go | 3 +++ providers/kube/handler.go | 9 +++++++++ providers/kube/provider.go | 6 ++++++ 4 files changed, 29 insertions(+) diff --git a/providers/kube/client/client.go b/providers/kube/client/client.go index 1f14712..e5904ce 100644 --- a/providers/kube/client/client.go +++ b/providers/kube/client/client.go @@ -4,6 +4,7 @@ import ( "context" "flag" "fmt" + corev1 "k8s.io/api/core/v1" "github.com/sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -140,3 +141,13 @@ func (k *Client) ListPods(namespace string, labelSelector, fieldSelector string) } return list, nil } + +// GetNode calls the API to get node with name. +func (k *Client) GetNode(name string) (*corev1.Node, error) { + + node, err := k.client.CoreV1().Nodes().Get(context.TODO(), name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + return node, nil +} diff --git a/providers/kube/client/pod.go b/providers/kube/client/pod.go index c5ef8b0..431049d 100644 --- a/providers/kube/client/pod.go +++ b/providers/kube/client/pod.go @@ -26,9 +26,11 @@ type Pod struct { URL string Image string ImageID string + HostNetwork bool pod *corev1.Pod client *Client + Node *corev1.Node } func newPod(client *Client, pod *corev1.Pod) *Pod { @@ -44,6 +46,7 @@ func newPod(client *Client, pod *corev1.Pod) *Pod { URL: pod.GetSelfLink(), Image: getPodFirstContainer(pod).Image, ImageID: getPodFirstContainerStatus(pod).ImageID, + HostNetwork: pod.Spec.HostNetwork, pod: pod, client: client, } diff --git a/providers/kube/handler.go b/providers/kube/handler.go index 28253de..5b3466b 100644 --- a/providers/kube/handler.go +++ b/providers/kube/handler.go @@ -2,6 +2,7 @@ package kube import ( "fmt" + "strings" "time" govppapi "git.fd.io/govpp.git/api" @@ -37,6 +38,13 @@ func (h *PodHandler) ID() string { } func (h *PodHandler) Metadata() map[string]string { + var addresses []string + if h.pod.HostNetwork { + for _, nodeAddress := range h.pod.Node.Status.Addresses { + addresses = append(addresses, nodeAddress.Address) + } + } + return map[string]string{ "env": providers.Kube, "pod": h.pod.Name, @@ -50,6 +58,7 @@ func (h *PodHandler) Metadata() map[string]string { "image_id": h.pod.ImageID, "uid": string(h.pod.UID), "created": h.pod.Created.Format(time.UnixDate), + "hostnet_addresses": strings.Join(addresses, ","), } } diff --git a/providers/kube/provider.go b/providers/kube/provider.go index 7bf423d..9c9b10f 100644 --- a/providers/kube/provider.go +++ b/providers/kube/provider.go @@ -83,6 +83,12 @@ func (p *Provider) Query(params ...map[string]string) ([]probe.Handler, error) { var handlers []probe.Handler for _, pod := range pods { + node, err := p.client.GetNode(pod.NodeName) + if err != nil { + logrus.Errorf("Unable to get node %s for pod %s: %v", pod.NodeName, pod.Name, err) + } else { + pod.Node = node + } handlers = append(handlers, NewHandler(pod)) } From f386470462c066392dd95c8b9960db8d08297ac8 Mon Sep 17 00:00:00 2001 From: Tim Swanson Date: Wed, 11 Aug 2021 15:41:19 -0400 Subject: [PATCH 2/6] Add interface correlation capability and aggregation output option --- cmd/discover.go | 23 ++ cmd/print.go | 31 ++ vpp/agent/correlate_interfaces.go | 587 ++++++++++++++++++++++++++++++ 3 files changed, 641 insertions(+) create mode 100644 vpp/agent/correlate_interfaces.go diff --git a/cmd/discover.go b/cmd/discover.go index 2d4dfc4..90928af 100644 --- a/cmd/discover.go +++ b/cmd/discover.go @@ -39,12 +39,14 @@ func NewDiscoverCmd(cli Cli) *cobra.Command { flags := cmd.Flags() flags.StringVarP(&opts.Format, "format", "f", "", "Output format (json, yaml, go-template..)") flags.BoolVar(&opts.IPsecAgg, "ipsec-agg", false, "Print aggregated IPSec info") + flags.BoolVar(&opts.IfInfoAgg, "ifinfo-agg", false, "Print aggregated interface info") return cmd } type DiscoverOptions struct { Format string IPsecAgg bool + IfInfoAgg bool } func RunDiscover(cli Cli, opts DiscoverOptions) error { @@ -96,6 +98,17 @@ func RunDiscover(cli Cli, opts DiscoverOptions) error { } } + if opts.IfInfoAgg { + logrus.Infof("Aggregating Interface info for instances") + + forwarderConnInfo, err := agent.CorrelateNsmForwarderConnections(vppInstances) + if err != nil { + logrus.Warnf("correlating IPSec failed: %v", err) + } else { + printDiscoverForwarderConnInfo(cli.Out(), forwarderConnInfo) + } + } + return nil } @@ -153,3 +166,13 @@ func printDiscoverIPSecAggr(out io.Writer, ipsecCorrelations *agent.IPSecCorrela fmt.Fprint(out, renderColor(buf.String())) } + +func printDiscoverForwarderConnInfo(out io.Writer, forwarderConnCorrelations *agent.ForwarderConnCorrelations) { + var buf bytes.Buffer + + printSectionHeader(&buf, []string{"Aggregated Interface info"}) + + PrintCorrelatedIfInfo(prefixWriter(&buf), forwarderConnCorrelations) + + fmt.Fprint(out, renderColor(buf.String())) +} \ No newline at end of file diff --git a/cmd/print.go b/cmd/print.go index b0ec21d..4978a95 100644 --- a/cmd/print.go +++ b/cmd/print.go @@ -281,6 +281,37 @@ func PrintCorrelatedIpSec(out io.Writer, correlations *agent.IPSecCorrelations) fmt.Fprint(out, buf.String()) } +func PrintCorrelatedIfInfo(out io.Writer, connCorrelations *agent.ForwarderConnCorrelations) { + var buf bytes.Buffer + w := tabwriter.NewWriter(&buf, 0, 8, 1, '\t', tabwriter.StripEscape|tabwriter.FilterHTML|tabwriter.DiscardEmptyColumns) + + header := []string{ + "Conn", "Complete?", + } + for i, h := range header { + if h != "" { + header[i] = colorize(color.Bold, h) + } + } + fmt.Fprintln(w, strings.Join(header, "\t")) + + // Print connection chains + for _, connChain := range connCorrelations.Connections { + var cols []string + for _, intf := range connChain.IntfPath { + cols = append(cols, fmt.Sprintf("%s/%s(%s)", intf.Owner.Pod, intf.IfName, intf.NormalizedType)) + } + fmt.Fprintln(w, strings.Join(cols, "<->")) + } + + if err := w.Flush(); err != nil { + log.Println(err) + return + } + + fmt.Fprint(out, buf.String()) +} + func linuxInterfaceType(iface agent.LinuxInterface) string { return iface.Value.Type.String() } diff --git a/vpp/agent/correlate_interfaces.go b/vpp/agent/correlate_interfaces.go new file mode 100644 index 0000000..0c045d4 --- /dev/null +++ b/vpp/agent/correlate_interfaces.go @@ -0,0 +1,587 @@ +package agent + +import ( + "fmt" + "github.com/sirupsen/logrus" + vpp_interfaces "go.ligato.io/vpp-agent/v3/proto/ligato/vpp/interfaces" + "go.ligato.io/vpp-probe/providers" + "google.golang.org/protobuf/reflect/protoreflect" + "sort" + "strings" +) + +type ForwarderConnectionType int +const ( + TapHead ForwarderConnectionType = iota + MemifHead +) + +type ForwarderIfContextType int +const ( + ForwarderContextVPP ForwarderIfContextType = iota + ForwarderContextLinux +) +type NormalizedIfType int +const ( + UNKNOWN NormalizedIfType = iota + TAP + MEMIF + VXLAN + AF_PACKET +) +func (n NormalizedIfType) String() string { + return [...]string{"UNKNOWN", + "TAP", "MEMIF", "VXLAN", "AF_PACKET"}[n] +} + +type IfOwner struct { + Cluster string + Node string + Pod string +} + +type ForwarderIfNormalizedConfig interface { + IsEqual(ForwarderIfNormalizedConfig) bool + MatchKey() string // key that matches peer interfaces (built from intf properties) + ToString() string +} + +type ForwarderIf struct { + Owner IfOwner + IfContextType ForwarderIfContextType + IfName string + InternalIfName string + NormalizedType NormalizedIfType + NormalizedConfig ForwarderIfNormalizedConfig // socket:tag, vxlan:src/dest VNI, tap name +} + +type ForwarderConnection struct { + Type ForwarderConnectionType + IntfPath []*ForwarderIf +} + +/* + nodes -> pods = instances + vxlan intf -> src,dst,vni + src IP = local nodes' IP + dst IP = external IP of peer node (it's in addresses list for host_network VPP instances) + +*/ +type ForwarderConnCorrelations struct { + instances []*Instance + VppInstance map[string]map[string]*Instance // map[nodeName][podName] = VPP instance + Ip2Vpp map[string]*Instance //map[IP address] = vpp-forwarder + AfPacketIf2Instance map[string]*Instance // map[af_packetIntf IP] = VPP Instance + Connections []*ForwarderConnection + AllIfs []*ForwarderIf + PodIfs map[string]map[string]*ForwarderIf // map[pod][ifName] = intf + IfInterconnects map[NormalizedIfType]map[string][]*ForwarderIf // pairs of interconnect intfs map[type][IfMatchkey]{ forwarderIf1, forwarderIf2 } + IfXconnects map[string]map[string]string // map[pod][ifName] = xconn peer ifName +} + +type MemifNormalizedConfig struct { + socketFile string + id string + isMaster bool +} +func (m MemifNormalizedConfig) IsEqual(normIf ForwarderIfNormalizedConfig) bool { + m2, ok := normIf.(MemifNormalizedConfig) + if !ok { + return false + } + if m.socketFile == m2.socketFile { + return true + } + return false +} +func (m MemifNormalizedConfig) ToString() string { + if (m.isMaster) { + fmt.Sprintf("%s (master)", m.socketFile) + } + return fmt.Sprintf("%s", m.socketFile) +} +func (m MemifNormalizedConfig) MatchKey() string { + dirComps := strings.Split(m.socketFile, "/") + return fmt.Sprintf("%s", strings.Join(dirComps[len(dirComps)-2:], "/")) +} + +type VxlanNormalizedConfig struct { + src string + dst string + vni uint32 + srcNodeAddresses []string // all node IP addresses mapping to src + dstNodeAddresses []string // all node IP addresses mapping to dst +} +func NewVxlanNormalizedConfig(iface VppInterface, ip2VppInstance map[string]*Instance) (*VxlanNormalizedConfig) { + vxlan := iface.Value.GetVxlan() + + srcInst, ok := ip2VppInstance[vxlan.SrcAddress] + var srcAddresses []string + if ok { + srcAddresses = VppInstanceToAddresses(srcInst) + } else { + srcAddresses = []string{ vxlan.SrcAddress } + } + sort.Strings(srcAddresses) + + dstInst, ok := ip2VppInstance[vxlan.DstAddress] + var dstAddresses []string + if ok { + dstAddresses = VppInstanceToAddresses(dstInst) + } else { + dstAddresses = []string{ vxlan.DstAddress } + } + sort.Strings(dstAddresses) + return &VxlanNormalizedConfig { + src: vxlan.SrcAddress, + dst: vxlan.DstAddress, + vni: vxlan.Vni, + srcNodeAddresses: srcAddresses, + dstNodeAddresses: dstAddresses, + } +} +func (v VxlanNormalizedConfig) IsEqual(normIf ForwarderIfNormalizedConfig) bool { + v2, ok := normIf.(VxlanNormalizedConfig) + if !ok { + return false + } + if ((v.src == v2.src && v.dst == v2.dst) || + (v.src == v2.dst && v.dst == v2.src)) && + v.vni == v2.vni { + return true + } + return false +} +func (v VxlanNormalizedConfig) ToString() string { + return fmt.Sprintf("%s <-> %s (%s)", v.src, v.dst, v.vni) +} +func (v VxlanNormalizedConfig) MatchKey() string { + // Normalize for match key + // make the tuple the lexigraphical order of the 1st pair of addresses + srcAddr := v.srcNodeAddresses[0] + dstAddr := v.dstNodeAddresses[0] + if srcAddr > dstAddr { + srcAddr = dstAddr + dstAddr = v.srcNodeAddresses[0] + } + return fmt.Sprintf("%s,%s/%s", srcAddr, dstAddr, v.vni) +} + +type TapNormalizedConfig struct { + Name string +} +func (t TapNormalizedConfig) IsEqual(normIf ForwarderIfNormalizedConfig) bool { + t2, ok := normIf.(TapNormalizedConfig) + if !ok { + return false + } + if t.Name == t2.Name { + return true + } + return false +} +func (t TapNormalizedConfig) ToString() string { + return fmt.Sprintf("%s", t.Name) +} +func (t TapNormalizedConfig) MatchKey() string { + return fmt.Sprintf("%s", t.Name) +} + +func protoFieldsToMap(fields protoreflect.FieldDescriptors, pb protoreflect.Message) map[string]string { + m := map[string]string{} + for i := 0; i < fields.Len(); i++ { + fd := fields.Get(i) + if pb.Has(fd) { + f := pb.Get(fd) + if f.IsValid() { + m[string(fd.Name())] = f.String() + } + } + } + return m +} +func linuxIfToConfig(iface LinuxInterface) TapNormalizedConfig { + ref := iface.Value.ProtoReflect() + ld := ref.Descriptor().Oneofs().ByName("link") + wd := ref.WhichOneof(ld) + if wd == nil { + return TapNormalizedConfig {Name: fmt.Sprintf("Errored-convert-%s", iface.Value.Name)} + } + d := wd.Message() + link := ref.Get(wd).Message() + m := protoFieldsToMap(d.Fields(), link) + return TapNormalizedConfig {Name: m["vpp_tap_if_name"]} +} + + +func interfaceInternalName(iface VppInterface) string { + if name, ok := iface.Metadata["InternalName"]; ok && name != nil { + return fmt.Sprint(name) + } + return "" +} + +func mapKeyValString(m map[string]string, f func(k string, v string) string) string { + ss := make([]string, 0, len(m)) + for k, v := range m { + s := f(k, v) + if s == "" { + continue + } + ss = append(ss, s) + } + return strings.Join(ss, " ") +} + +func VppInstanceToAddresses(instance *Instance) []string { + metadata := instance.handler.Metadata() + return strings.Split(metadata["hostnet_addresses"], ",") +} + +func newForwarderConnCorrelations(instances []*Instance) (*ForwarderConnCorrelations, error) { + data := &ForwarderConnCorrelations{ + instances: instances, + VppInstance: map[string]map[string]*Instance{}, // map[nodeName][podName] = VPP instance + Ip2Vpp: map[string]*Instance{}, // map[node IP Address] = vpp instance (hostnetwork vpp) + AfPacketIf2Instance: map[string]*Instance{}, // map[af_packetIntf IP] = VPP Instance + Connections: []*ForwarderConnection{}, + AllIfs: []*ForwarderIf{}, + PodIfs: map[string]map[string]*ForwarderIf{}, + IfInterconnects: map[NormalizedIfType]map[string][]*ForwarderIf{}, + IfXconnects: map[string]map[string]string{}, + } + + for _, instance := range instances { + metadata := instance.handler.Metadata() + if metadata["env"] != providers.Kube { + return nil, fmt.Errorf("NSM forwarder correlations only supported in K8s envs") + } + node := metadata["node"] + if _, ok := data.VppInstance[node]; !ok { + data.VppInstance[node] = make(map[string]*Instance) + } + podname := metadata["pod"] + data.VppInstance[node][podname] = instance + + // Map node Addresses to hostnetwork vpp instances (e.g. nsm-forwarder) + for _, address := range VppInstanceToAddresses(instance) { + if address != "" { + data.Ip2Vpp[address] = instance + } + } + } + return data, nil +} + +func (c *ForwarderConnCorrelations) LinuxIfToInfo(cluster, node, pod string, linuxIf LinuxInterface) *ForwarderIf { + iface := linuxIf.Value + return &ForwarderIf { + Owner: IfOwner{ + Cluster: cluster, + Node: node, + Pod: pod, + }, + IfContextType: ForwarderContextLinux, + IfName: iface.Name, + InternalIfName: iface.HostIfName, + NormalizedType: TAP, + NormalizedConfig: linuxIfToConfig(linuxIf), + } +} +func (c *ForwarderConnCorrelations) vppInterfaceInfo(iface VppInterface) (NormalizedIfType, ForwarderIfNormalizedConfig) { + switch iface.Value.Type { + case vpp_interfaces.Interface_MEMIF: + memif := iface.Value.GetMemif() + return MEMIF, MemifNormalizedConfig{ socketFile: memif.GetSocketFilename(), isMaster: memif.Master } + + case vpp_interfaces.Interface_VXLAN_TUNNEL: + //vxlan := iface.Value.GetVxlan() + //var info string + //info += fmt.Sprintf("%s %s %s (vni:%v)", colorize(ipAddressColor, vxlan.SrcAddress), tunnelDirectionChar, colorize(ipAddressColor, vxlan.DstAddress), colorize(valueColor, vxlan.Vni)) + return VXLAN, NewVxlanNormalizedConfig(iface, c.Ip2Vpp) + + case vpp_interfaces.Interface_TAP: + //tap := iface.Value.GetTap() + //pr := tap.ProtoReflect() + //m := protoFieldsToMap(pr.Descriptor().Fields(), pr) + /* + fieldsStr := mapKeyValString(m, func(k string, v string) string { + return fmt.Sprintf("%s:%s", k, colorize(valueColor, v)) + }) + return fmt.Sprintf("host_if_name:%s %v", colorize(valueColor, iface.Metadata["TAPHostIfName"]), fieldsStr) + */ + // For comparison with the LinuxConfig we use the interface name + return TAP, TapNormalizedConfig{Name: iface.Value.Name} + //return TAP, TapNormalizedConfig{Name: fmt.Sprint(iface.Metadata["TAPHostIfName"])} + + case vpp_interfaces.Interface_IPIP_TUNNEL: + tun := iface.Value.GetIpip() + var info string + info += fmt.Sprintf("%s %s mode:%v", tun.SrcAddr, tun.DstAddr, tun.TunnelMode) + return UNKNOWN, TapNormalizedConfig{Name: info} + + } + + ref := iface.Value.ProtoReflect() + ld := ref.Descriptor().Oneofs().ByName("link") + wd := ref.WhichOneof(ld) + if wd == nil { + return UNKNOWN, TapNormalizedConfig{Name: fmt.Sprintf("Errored-convert-%s", iface.Value.Name)} + } + d := wd.Message() + link := ref.Get(wd).Message() + + m := protoFieldsToMap(d.Fields(), link) + info := mapKeyValString(m, func(k string, v string) string { + return fmt.Sprintf("%s:%s", k, v) + }) + return UNKNOWN, TapNormalizedConfig{Name: info} +} + +func (c *ForwarderConnCorrelations) VppIfToInfo(cluster, node, pod string, vppIf VppInterface) *ForwarderIf { + ifType, ifConfig := c.vppInterfaceInfo(vppIf) + iface := vppIf.Value + return &ForwarderIf { + Owner: IfOwner{ + Cluster: cluster, + Node: node, + Pod: pod, + }, + IfContextType: ForwarderContextVPP, + IfName: iface.Name, + InternalIfName: interfaceInternalName(vppIf), + NormalizedType: ifType, + NormalizedConfig: ifConfig, + } +} + +func (c *ForwarderConnCorrelations) AddForwarderIf(intf *ForwarderIf) { + // Add interconnects to type sorted map + if _, ok := c.IfInterconnects[intf.NormalizedType]; !ok { + c.IfInterconnects[intf.NormalizedType] = make(map[string][]*ForwarderIf) + } + intfMatchKey := intf.NormalizedConfig.MatchKey() + if _, ok := c.IfInterconnects[intf.NormalizedType][intfMatchKey]; !ok { + c.IfInterconnects[intf.NormalizedType][intfMatchKey] = []*ForwarderIf{} + } + c.IfInterconnects[intf.NormalizedType][intfMatchKey] = + append(c.IfInterconnects[intf.NormalizedType][intfMatchKey], intf) + + // add to pod sorted map + if _, ok := c.PodIfs[intf.Owner.Pod]; !ok { + c.PodIfs[intf.Owner.Pod] = make(map[string]*ForwarderIf) + } + c.PodIfs[intf.Owner.Pod][intf.IfName] = intf +} + +func (c *ForwarderConnCorrelations) AddIfXconnects(pod string, instance *Instance) { + if _, ok := c.IfXconnects[pod]; !ok { + c.IfXconnects[pod] = make(map[string]string) + } + for _, xconn := range instance.Config.VPP.L2XConnects { + c.IfXconnects[pod][xconn.Value.ReceiveInterface] = xconn.Value.TransmitInterface + c.IfXconnects[pod][xconn.Value.TransmitInterface] = xconn.Value.ReceiveInterface + } +} + +func (c *ForwarderConnCorrelations) GetPodIntf(pod, ifName string) *ForwarderIf { + if _, ok := c.PodIfs[pod]; !ok { + logrus.Warnf("No pod found named '%s' in lookup for interface named '%s'", pod, ifName) + return nil + } + intf, ok := c.PodIfs[pod][ifName] + if !ok { + logrus.Warnf("No interface named '%s' found in pod named '%s'", ifName, pod) + return nil + } + return intf +} + +func (c *ForwarderConnCorrelations) IfToInterconnectPeer(intf *ForwarderIf) *ForwarderIf { + interConnIntfs, ok := c.IfInterconnects[intf.NormalizedType][intf.NormalizedConfig.MatchKey()] + if !ok { + logrus.Errorf("No interconnect for interface '%s/%s' type %s with MatchKey '%s'", + intf.Owner.Pod, intf.IfName, intf.NormalizedType.String(), + intf.NormalizedConfig.MatchKey()) + return nil + } + for _, interconIntf := range interConnIntfs { + if interconIntf.Owner != intf.Owner { + return interconIntf + } + } + return nil +} + +func (c *ForwarderConnCorrelations) IfToXconnVwireChain(intf *ForwarderIf) []*ForwarderIf { + + isEnd := false + curIf := intf + vWireChain := []*ForwarderIf{ intf } + for !isEnd { + if xconnPeerIfName, ok := c.IfXconnects[curIf.Owner.Pod][curIf.IfName]; !ok { + isEnd = true + break + } else { + // xconnects are on same pod so lookup the xconnPeerIfName for the same pod as curIf + peerIntf := c.GetPodIntf(curIf.Owner.Pod, xconnPeerIfName) + if peerIntf != nil { + vWireChain = append(vWireChain, peerIntf) + } + // go to the peerIntf's interconnect + interConnIntf := c.IfToInterconnectPeer(peerIntf) + if interConnIntf == nil { + logrus.Errorf("No interconnect for interface '%s/%s' type %s with MatchKey '%s'", + peerIntf.Owner.Pod, peerIntf.IfName, peerIntf.NormalizedType.String(), + peerIntf.NormalizedConfig.MatchKey()) + isEnd = true + break + } + vWireChain = append(vWireChain, interConnIntf) + curIf = interConnIntf + } + } + return vWireChain +} + +func (c *ForwarderConnCorrelations) VppInstanceToIfs(cluster, node, pod string, instance *Instance) ([]*ForwarderIf, error) { + var ifs []*ForwarderIf + + if len(instance.Config.VPP.Interfaces) > 0 { + for _, v := range instance.Config.VPP.Interfaces { + intf := c.VppIfToInfo(cluster, node, pod, v) + ifs = append(ifs, intf) + c.AddForwarderIf(intf) + } + } + if len(instance.Config.Linux.Interfaces) > 0 { + for _, l := range instance.Config.Linux.Interfaces { + intf := c.LinuxIfToInfo(cluster, node, pod, l) + ifs = append(ifs, intf) + c.AddForwarderIf(intf) + } + } + + c.AddIfXconnects(pod, instance) + + return ifs, nil +} + +func (c *ForwarderConnCorrelations) AddConnectionChain(chainType ForwarderConnectionType, conChain []*ForwarderIf) { + c.Connections = append(c.Connections, &ForwarderConnection{ + Type: chainType, + IntfPath: conChain, + }) +} + +// Build Connection Chains +// 1) start with IfInterconnects[TAP] +// - Linux intf <-> vpp intf +// 2) find IfXconnects[vpp intf] +// - <-xconn-> peer vpp intf +// 3) find IfInterconnects[peerIf type][peerIf matchkey] +// <-> peer pod intf +// 4) it may stop there but repeat steps 2, 3, 4 if xconn and/or interconn +// +// repeat step 1 for any IfInterconnects[Memif] interfaces not in a prior chain +func (c *ForwarderConnCorrelations) BuildConnectionChains() { + // Keep track of interfaces used in connection chain + //usedIntfs := make(map[string]*ForwarderIf) // key = pod/ifName + + for _, tapInterConn := range c.IfInterconnects[TAP] { + var curChain []*ForwarderIf + var startXconIntf *ForwarderIf + // Start at Linux owned interfaces as head of chain + if len(tapInterConn) < 2 { + if len(tapInterConn) == 1 { + logrus.Warnf("Interconnect with intf %s/%s not fully built--len %d", + tapInterConn[0].Owner.Pod, tapInterConn[0].IfName, len(tapInterConn)) + } else { + logrus.Errorf("TAP Interconnect empty--len %d", len(tapInterConn)) + continue + } + startXconIntf = tapInterConn[0] + } else { + var firstIntf *ForwarderIf + if tapInterConn[0].IfContextType == ForwarderContextLinux { + firstIntf = tapInterConn[0] + startXconIntf = tapInterConn[1] + } else { + firstIntf = tapInterConn[1] + startXconIntf = tapInterConn[0] + } + curChain = append(curChain, firstIntf) + } + curChain = append(curChain, c.IfToXconnVwireChain(startXconIntf)...) + c.AddConnectionChain(TapHead, curChain) + } + + for _, memifInterConn := range c.IfInterconnects[MEMIF] { + var curChain []*ForwarderIf + var startXconIntf *ForwarderIf + // Start at Linux owned interfaces as head of chain + if len(memifInterConn) < 2 { + if len(memifInterConn) == 1 { + logrus.Warnf("Interconnect with intf %s/%s not fully built--len %d", + memifInterConn[0].Owner.Pod, memifInterConn[0].IfName, len(memifInterConn)) + } else { + logrus.Errorf("memif Interconnect empty--len %d", len(memifInterConn)) + continue + } + startXconIntf = memifInterConn[0] + } else { + var firstIntf *ForwarderIf + // firstIntf is one with no xconnect + if _, ok := c.IfXconnects[memifInterConn[0].Owner.Pod]; !ok { + firstIntf = memifInterConn[0] + startXconIntf = memifInterConn[1] + } else if _, ok := c.IfXconnects[memifInterConn[0].Owner.Pod][memifInterConn[0].IfName]; !ok { + firstIntf = memifInterConn[0] + startXconIntf = memifInterConn[1] + } else { + // lazily assumes the other memif is the one with no xconnect + firstIntf = memifInterConn[1] + startXconIntf = memifInterConn[0] + } + curChain = append(curChain, firstIntf) + } + curChain = append(curChain, c.IfToXconnVwireChain(startXconIntf)...) + c.AddConnectionChain(MemifHead, curChain) + } +} + +// +// Interface Mappings +// 1) interconnects -- e.g. both sides of tap, both sides of vxlan, or both sides of memif +// 2) crossconnects -- e.g. switch mapping within a forwarder + +func (c *ForwarderConnCorrelations) MapIntfs() { + for _, instance := range c.instances { + metadata := instance.handler.Metadata() + node := metadata["node"] + podname := metadata["pod"] + cluster := metadata["cluster"] + + // Normalize info on all interfaces handled by each vpp instance + forwarderIfs, err := c.VppInstanceToIfs(cluster, node, podname, instance) + if err != nil { + logrus.Errorf("Failed to correlate intf data for %s/%s/%s (cluster/node/pod): %v", metadata["cluster"], node, podname, err) + continue + } + c.AllIfs = append(c.AllIfs, forwarderIfs...) + } +} + +func CorrelateNsmForwarderConnections(instances []*Instance) (*ForwarderConnCorrelations, error) { + + data, err := newForwarderConnCorrelations(instances) + if err != nil { + return nil, fmt.Errorf("Failed to create intf data for instances: %v", err) + } + + data.MapIntfs() + + data.BuildConnectionChains() + + return data, nil +} \ No newline at end of file From 3374c6c06ab83198139aa81421f43545af08f0c8 Mon Sep 17 00:00:00 2001 From: Tim Swanson Date: Wed, 11 Aug 2021 16:38:35 -0400 Subject: [PATCH 3/6] Improve display of interface correlation output --- cmd/print.go | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/cmd/print.go b/cmd/print.go index 4978a95..6524dc1 100644 --- a/cmd/print.go +++ b/cmd/print.go @@ -286,7 +286,7 @@ func PrintCorrelatedIfInfo(out io.Writer, connCorrelations *agent.ForwarderConnC w := tabwriter.NewWriter(&buf, 0, 8, 1, '\t', tabwriter.StripEscape|tabwriter.FilterHTML|tabwriter.DiscardEmptyColumns) header := []string{ - "Conn", "Complete?", + "Connection Chain -- [Abbrev. pod name]/[intf] <-[connect type]-> ...", } for i, h := range header { if h != "" { @@ -297,11 +297,24 @@ func PrintCorrelatedIfInfo(out io.Writer, connCorrelations *agent.ForwarderConnC // Print connection chains for _, connChain := range connCorrelations.Connections { - var cols []string - for _, intf := range connChain.IntfPath { - cols = append(cols, fmt.Sprintf("%s/%s(%s)", intf.Owner.Pod, intf.IfName, intf.NormalizedType)) + var chain string + for i, intf := range connChain.IntfPath { + if i > 0 { + conStr := intf.NormalizedType.String() + if intf.NormalizedType != connChain.IntfPath[i-1].NormalizedType { + conStr = "XCON" + } + chain += fmt.Sprintf(" <-%s-> ", conStr) + } + if intf.IfContextType == agent.ForwarderContextLinux { + chain += fmt.Sprintf("LinuxPod/%s", intf.InternalIfName) + } else { + podNameParts := strings.Split(intf.Owner.Pod, "-") + displayPodInfo := strings.Join(podNameParts[0:2], "-") + "-" + podNameParts[len(podNameParts)-1] + chain += fmt.Sprintf("%s/%s", displayPodInfo, intf.IfName) + } } - fmt.Fprintln(w, strings.Join(cols, "<->")) + fmt.Fprintln(w, chain) } if err := w.Flush(); err != nil { From 11ae1e49d57092e931fb0d9206b9b0781929ee55 Mon Sep 17 00:00:00 2001 From: Tim Swanson Date: Thu, 12 Aug 2021 08:56:24 -0400 Subject: [PATCH 4/6] intf correlation: detect when interfaces already part of correlated connection --- vpp/agent/correlate_interfaces.go | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/vpp/agent/correlate_interfaces.go b/vpp/agent/correlate_interfaces.go index 0c045d4..40f4897 100644 --- a/vpp/agent/correlate_interfaces.go +++ b/vpp/agent/correlate_interfaces.go @@ -14,6 +14,7 @@ type ForwarderConnectionType int const ( TapHead ForwarderConnectionType = iota MemifHead + VxlanHead ) type ForwarderIfContextType int @@ -53,6 +54,7 @@ type ForwarderIf struct { InternalIfName string NormalizedType NormalizedIfType NormalizedConfig ForwarderIfNormalizedConfig // socket:tag, vxlan:src/dest VNI, tap name + CorrelatedConnection *ForwarderConnection } type ForwarderConnection struct { @@ -468,10 +470,14 @@ func (c *ForwarderConnCorrelations) VppInstanceToIfs(cluster, node, pod string, } func (c *ForwarderConnCorrelations) AddConnectionChain(chainType ForwarderConnectionType, conChain []*ForwarderIf) { - c.Connections = append(c.Connections, &ForwarderConnection{ + newConn := &ForwarderConnection{ Type: chainType, IntfPath: conChain, - }) + } + c.Connections = append(c.Connections, newConn) + for _, intf := range conChain { + intf.CorrelatedConnection = newConn + } } // Build Connection Chains @@ -519,7 +525,7 @@ func (c *ForwarderConnCorrelations) BuildConnectionChains() { for _, memifInterConn := range c.IfInterconnects[MEMIF] { var curChain []*ForwarderIf var startXconIntf *ForwarderIf - // Start at Linux owned interfaces as head of chain + // Start at memif with no xconn as head of chain if len(memifInterConn) < 2 { if len(memifInterConn) == 1 { logrus.Warnf("Interconnect with intf %s/%s not fully built--len %d", @@ -545,9 +551,27 @@ func (c *ForwarderConnCorrelations) BuildConnectionChains() { } curChain = append(curChain, firstIntf) } + if startXconIntf.CorrelatedConnection != nil { + logrus.Infof("Connection correlation already done for starting intf %s/%s", + startXconIntf.Owner.Pod, startXconIntf.IfName) + continue + } curChain = append(curChain, c.IfToXconnVwireChain(startXconIntf)...) c.AddConnectionChain(MemifHead, curChain) } + + // Add any dangling vxlan interconnects that are not part of tap/memif connections + for _, vxlanifInterConn := range c.IfInterconnects[VXLAN] { + if vxlanifInterConn[0].CorrelatedConnection == nil { + // This vxlan wasn't correlated to a connection + // add it to a connection with its interconnect + var curChain []*ForwarderIf + for _, intf := range vxlanifInterConn { + curChain = append(curChain, intf) + } + c.AddConnectionChain(VxlanHead, curChain) + } + } } // From 9f89e9521a1e8e64a0e331fb8c89289f2307cbde Mon Sep 17 00:00:00 2001 From: Tim Swanson Date: Thu, 12 Aug 2021 11:42:01 -0400 Subject: [PATCH 5/6] Document interface correlation --- docs/CORRELATE.md | 51 +++++++++++++++++++++++++++ docs/kubernetes_pod_interconnect.png | Bin 0 -> 64354 bytes 2 files changed, 51 insertions(+) create mode 100644 docs/CORRELATE.md create mode 100644 docs/kubernetes_pod_interconnect.png diff --git a/docs/CORRELATE.md b/docs/CORRELATE.md new file mode 100644 index 0000000..c86dc5c --- /dev/null +++ b/docs/CORRELATE.md @@ -0,0 +1,51 @@ +# Correlation Implementation + +## Interface Connection Relationships + +### Kubernetes Inter-Pod/Node Connections + +![Kubernetes Pod Interconnect](kubernetes_pod_interconnect.png) + +1. **Inter-connect** is a reference to the relationships between interfaces that are interconnecting 2 pods. + - Tap interfaces--host network interconnect via Linux kernel tap interface to VPP tap interface + - Memif interfaces--interconnect via memif interfaces using the same socketfile/shared-mem between VPP pods + - VXLAN interfaces--interconnect across nodes via the same src/dst IP pair and VNI + +1. **Cross-connect (Xcon)** refers to the cross-connect of interfaces within a VPP instance. This is how traffic from interfaces of different types are directly connected. + +#### Correlation: Inter-connect Mapping Implementation + +The attributes and configuration for all interfaces for discovered VPP instances are normalized to a common format. For each `NormalizedIfType`, an implementation of the `ForwarderIfNormalizedConfig` interface's `MatchKey()` method generates a string that matches with the value of its inter-connected peer interface. + +This is implemented as follows for each `NormalizedIfType`: + +- `TAP` -- matching VPP and Linux interface names +- `MEMIF` -- matching socket file path and name for last 2 portions of file path. +- `VXLAN` -- original src/dst IP config is mapped to the full list of k8s node addresses. The `MatchKey` format is `,/` and uses the first IPs in each of the node objects' addresses list, with the src IP equal to the lexigraphically smallest of the 2 node addresses. + - NOTE: the reason this is necessary is to work for public cloud nodes with Node SNAT functionality to public addresses. Using the k8s node object info in this way should work for all known k8s cases. + +To correlate interfaces' configuration to their inter-connect peers, the `IfInterconnects` map is used: + +```go +// pairs of interconnect intfs map[type][IfMatchkey]{ forwarderIf1, forwarderIf2 } +IfInterconnects map[NormalizedIfType]map[string][]*ForwarderIf +``` + +Interfaces with matching `MatchKey()` output are inter-connect pairs and get mapped to the same slice. + +#### Correlation: Cross-connect Mappings + +The `IfXconnects` map associates interfaces to their crossconnect peer interface: + +```go +// map[pod][ifName] = xconn peer ifName +IfXconnects map[string]map[string]string +``` + +The VPP instance config's xconnect info is used to build this map. + +#### Correlation: Full Connection Path + +The endpoints to the full connection paths are Linux pod TAP interfaces or VPP MEMIF interfaces that are not part of a VPP crossconnect. The correlation starts at interfaces fitting these criteria and traverses the inter-connect and cross-connect relationships at each interface to creates a `ForwarderConnection` object for the full paths. While traversing the interfaces, they are associated (marked) with the connection they are a part of. + +Any "leftover" VXLAN interfaces are considered dangling but are also attempted to be associated to their inter-connect peer for display. diff --git a/docs/kubernetes_pod_interconnect.png b/docs/kubernetes_pod_interconnect.png new file mode 100644 index 0000000000000000000000000000000000000000..6515aeb49d165cde5c7dd133199ca96602267716 GIT binary patch literal 64354 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 ajwzgem9oR{5%3HBb6-vOZn>&O$iD!f^A&0U literal 0 HcmV?d00001 From 141df4030e8ee05e6c17f6460c517fc0a6ea632e Mon Sep 17 00:00:00 2001 From: Tim Swanson Date: Fri, 13 Aug 2021 11:18:04 -0400 Subject: [PATCH 6/6] ifinfo-agg: Fix memif matchkey to be inode --- vpp/agent/correlate_interfaces.go | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/vpp/agent/correlate_interfaces.go b/vpp/agent/correlate_interfaces.go index 40f4897..29f4d6f 100644 --- a/vpp/agent/correlate_interfaces.go +++ b/vpp/agent/correlate_interfaces.go @@ -83,7 +83,8 @@ type ForwarderConnCorrelations struct { type MemifNormalizedConfig struct { socketFile string - id string + id uint32 + inode string isMaster bool } func (m MemifNormalizedConfig) IsEqual(normIf ForwarderIfNormalizedConfig) bool { @@ -100,11 +101,12 @@ func (m MemifNormalizedConfig) ToString() string { if (m.isMaster) { fmt.Sprintf("%s (master)", m.socketFile) } - return fmt.Sprintf("%s", m.socketFile) + return fmt.Sprintf("%s:%s", m.socketFile, m.inode) } func (m MemifNormalizedConfig) MatchKey() string { - dirComps := strings.Split(m.socketFile, "/") - return fmt.Sprintf("%s", strings.Join(dirComps[len(dirComps)-2:], "/")) + //dirComps := strings.Split(m.socketFile, "/") + // return fmt.Sprintf("%s:%d", strings.Join(dirComps[len(dirComps)-2:], "/"), m.id) + return m.inode } type VxlanNormalizedConfig struct { @@ -155,7 +157,7 @@ func (v VxlanNormalizedConfig) IsEqual(normIf ForwarderIfNormalizedConfig) bool return false } func (v VxlanNormalizedConfig) ToString() string { - return fmt.Sprintf("%s <-> %s (%s)", v.src, v.dst, v.vni) + return fmt.Sprintf("%s <-> %s (%d)", v.src, v.dst, v.vni) } func (v VxlanNormalizedConfig) MatchKey() string { // Normalize for match key @@ -166,7 +168,7 @@ func (v VxlanNormalizedConfig) MatchKey() string { srcAddr = dstAddr dstAddr = v.srcNodeAddresses[0] } - return fmt.Sprintf("%s,%s/%s", srcAddr, dstAddr, v.vni) + return fmt.Sprintf("%s,%s/%d", srcAddr, dstAddr, v.vni) } type TapNormalizedConfig struct { @@ -294,7 +296,11 @@ func (c *ForwarderConnCorrelations) vppInterfaceInfo(iface VppInterface) (Normal switch iface.Value.Type { case vpp_interfaces.Interface_MEMIF: memif := iface.Value.GetMemif() - return MEMIF, MemifNormalizedConfig{ socketFile: memif.GetSocketFilename(), isMaster: memif.Master } + return MEMIF, MemifNormalizedConfig{ socketFile: memif.GetSocketFilename(), + id: memif.Id, + inode: fmt.Sprint(iface.Metadata["inode"]), + isMaster: memif.Master, + } case vpp_interfaces.Interface_VXLAN_TUNNEL: //vxlan := iface.Value.GetVxlan()