diff --git a/pkg/agent/agent_test.go b/pkg/agent/agent_test.go index cb983d9bc78..e6a4517deb8 100644 --- a/pkg/agent/agent_test.go +++ b/pkg/agent/agent_test.go @@ -90,7 +90,7 @@ func TestInitstore(t *testing.T) { container1, found1 := store.GetContainerInterface(uuid1) if !found1 { t.Errorf("Failed to load OVS port into local store") - } else if container1.OFPort != 11 || container1.GetIPv4Addr() == nil || container1.GetIPv4Addr().String() != p1IP || container1.MAC.String() != p1MAC || container1.InterfaceName != "p1" { + } else if container1.OFPort != 11 || len(container1.IPs) == 0 || container1.IPs[0].String() != p1IP || container1.MAC.String() != p1MAC || container1.InterfaceName != "p1" { t.Errorf("Failed to load OVS port configuration into local store") } _, found2 := store.GetContainerInterface(uuid2) diff --git a/pkg/agent/apiserver/handlers/ovstracing/handler.go b/pkg/agent/apiserver/handlers/ovstracing/handler.go index 25c23ba20e0..9d071bf4431 100644 --- a/pkg/agent/apiserver/handlers/ovstracing/handler.go +++ b/pkg/agent/apiserver/handlers/ovstracing/handler.go @@ -23,6 +23,7 @@ import ( "net/http" "strings" + corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog" @@ -30,6 +31,7 @@ import ( "github.com/vmware-tanzu/antrea/pkg/agent/apiserver/handlers" "github.com/vmware-tanzu/antrea/pkg/agent/interfacestore" "github.com/vmware-tanzu/antrea/pkg/agent/querier" + "github.com/vmware-tanzu/antrea/pkg/agent/util" "github.com/vmware-tanzu/antrea/pkg/ovs/ovsctl" ) @@ -47,6 +49,16 @@ type tracingPeer struct { ip net.IP } +func (p *tracingPeer) getAddressFamily() uint8 { + if p.ip == nil { + return 0 + } + if p.ip.To4() != nil { + return util.FamilyIPv4 + } + return util.FamilyIPv6 +} + type request struct { // tracingPeer.ip is invalid for inputPort, as inputPort can only be // specified by ovsPort or Pod Namespace/name. @@ -54,6 +66,7 @@ type request struct { source *tracingPeer destination *tracingPeer flow string + addrFamily uint8 } func getServiceClusterIP(aq querier.AgentQuerier, name, namespace string) (net.IP, *handlers.HandlerError) { @@ -68,28 +81,42 @@ func getServiceClusterIP(aq querier.AgentQuerier, name, namespace string) (net.I return net.ParseIP(srv.Spec.ClusterIP).To4(), nil } -// getPeerAddress looks up a Pod and returns its IP and MAC addresses. It -// first looks up the Pod from the InterfaceStore, and returns the Pod's IP and -// MAC addresses if found. If fails, it then gets the Pod from Kubernetes API, -// and returns the IP address in Pod resource Status if found. -func getPeerAddress(aq querier.AgentQuerier, peer *tracingPeer) (net.IP, *interfacestore.InterfaceConfig, *handlers.HandlerError) { - if peer.ip != nil { - return peer.ip, nil, nil - } - +func getLocalOVSInterface(aq querier.AgentQuerier, peer *tracingPeer) (*interfacestore.InterfaceConfig, *handlers.HandlerError) { if peer.ovsPort != "" { intf, ok := aq.GetInterfaceStore().GetInterfaceByName(peer.ovsPort) if !ok { err := handlers.NewHandlerError(fmt.Errorf("OVS port %s not found", peer.ovsPort), http.StatusNotFound) - return nil, nil, err + return nil, err } - return intf.GetIPv4Addr(), intf, nil + return intf, nil } interfaces := aq.GetInterfaceStore().GetContainerInterfacesByPod(peer.name, peer.namespace) if len(interfaces) > 0 { // Local Pod. - return interfaces[0].GetIPv4Addr(), interfaces[0], nil + return interfaces[0], nil + } + + return nil, nil +} + +// getPeerAddress looks up a Pod and returns its IP and MAC addresses. It +// first looks up the Pod from the InterfaceStore, and returns the Pod's IP and +// MAC addresses if found. If fails, it then gets the Pod from Kubernetes API, +// and returns the IP address in Pod resource Status if found. +func getPeerAddress(aq querier.AgentQuerier, peer *tracingPeer, addrFamily uint8) (net.IP, *interfacestore.InterfaceConfig, *handlers.HandlerError) { + if peer.ip != nil { + return peer.ip, nil, nil + } + + if intf, err := getLocalOVSInterface(aq, peer); err != nil { + return nil, nil, err + } else if intf != nil { + ipAddr, err := util.GetIPWithFamily(intf.IPs, addrFamily) + if err != nil { + return nil, nil, handlers.NewHandlerError(err, http.StatusNotFound) + } + return ipAddr, intf, nil } // Try getting the Pod from K8s API. @@ -103,7 +130,22 @@ func getPeerAddress(aq querier.AgentQuerier, peer *tracingPeer) (net.IP, *interf return nil, nil, handlers.NewHandlerError(errors.New("Kubernetes API error"), http.StatusInternalServerError) } // Return IP only assuming it should be a remote Pod. - return net.ParseIP(pod.Status.PodIP).To4(), nil, nil + podIP, err := getPodIPWithAddressFamily(pod, addrFamily) + if err != nil { + return nil, nil, handlers.NewHandlerError(err, http.StatusNotFound) + } + return podIP, nil, nil +} + +// Todo: move this function to pkg/agent/util/net.go if it is called by other code +func getPodIPWithAddressFamily(pod *corev1.Pod, addrFamily uint8) (net.IP, error) { + podIPs := []net.IP{net.ParseIP(pod.Status.PodIP)} + if len(pod.Status.PodIPs) > 0 { + for _, podIP := range pod.Status.PodIPs { + podIPs = append(podIPs, net.ParseIP(podIP.IP)) + } + } + return util.GetIPWithFamily(podIPs, addrFamily) } func prepareTracingRequest(aq querier.AgentQuerier, req *request) (*ovsctl.TracingRequest, *handlers.HandlerError) { @@ -122,7 +164,7 @@ func prepareTracingRequest(aq querier.AgentQuerier, req *request) (*ovsctl.Traci } } if !ok { - return nil, handlers.NewHandlerError(errors.New("Input port not found"), http.StatusNotFound) + return nil, handlers.NewHandlerError(errors.New("input port not found"), http.StatusNotFound) } } else { // Input port is not specified. Allow "in_port" field in "Flow" to override @@ -131,7 +173,7 @@ func prepareTracingRequest(aq querier.AgentQuerier, req *request) (*ovsctl.Traci } if req.source != nil { - ip, intf, err := getPeerAddress(aq, req.source) + ip, intf, err := getPeerAddress(aq, req.source, req.addrFamily) if err != nil { return nil, err } @@ -148,7 +190,7 @@ func prepareTracingRequest(aq querier.AgentQuerier, req *request) (*ovsctl.Traci gatewayConfig := aq.GetNodeConfig().GatewayConfig if req.destination != nil { - ip, intf, err := getPeerAddress(aq, req.destination) + ip, intf, err := getPeerAddress(aq, req.destination, req.addrFamily) if err != nil && err.HTTPStatusCode == http.StatusNotFound && req.destination.name != "" { // The destination might be a Service. ip, err = getServiceClusterIP(aq, req.destination.name, req.destination.namespace) @@ -210,9 +252,9 @@ func prepareTracingRequest(aq querier.AgentQuerier, req *request) (*ovsctl.Traci } // parseTracingPeer parses Pod/Service name and Namespace or OVS port name or -// IPv4 address from the string. nil is returned if the string is not of a +// IP address from the string. nil is returned if the string is not of a // valid Pod/Service reference ("Namespace/name") or OVS port name format, and -// not an IPv4 address. +// not an IP address. func parseTracingPeer(str string) *tracingPeer { parts := strings.Split(str, "/") n := len(parts) @@ -232,10 +274,7 @@ func parseTracingPeer(str string) *tracingPeer { // Probably an OVS port name. return &tracingPeer{ovsPort: str} } - // Do not support IPv6 address. - if ip.To4() != nil { - return &tracingPeer{ip: ip} - } + return &tracingPeer{ip: ip} } return nil } @@ -244,8 +283,33 @@ func validateRequest(r *http.Request) (*request, *handlers.HandlerError) { port := r.URL.Query().Get("port") src := r.URL.Query().Get("source") dst := r.URL.Query().Get("destination") + addrFamily := r.URL.Query().Get("addressFamily") request := request{flow: r.URL.Query().Get("flow")} + if addrFamily == "6" { + request.addrFamily = util.FamilyIPv6 + } else if addrFamily == "4" { + request.addrFamily = util.FamilyIPv4 + } else if request.flow != "" { + found := false + for _, s := range ovsctl.IPAndNWProtos { + if strings.Contains(request.flow, s) { + if strings.HasSuffix(s, "6") { + request.addrFamily = util.FamilyIPv6 + } else { + request.addrFamily = util.FamilyIPv4 + } + found = true + break + } + } + if !found { + request.addrFamily = util.FamilyIPv4 + } + } else { + request.addrFamily = util.FamilyIPv4 + } + if port != "" { request.inputPort = parseTracingPeer(port) // Input port cannot be specified with an IP. @@ -258,12 +322,20 @@ func validateRequest(r *http.Request) (*request, *handlers.HandlerError) { if request.source == nil { return nil, handlers.NewHandlerError(errors.New("invalid source format"), http.StatusBadRequest) } + srcAddrFamily := request.source.getAddressFamily() + if srcAddrFamily != 0 && srcAddrFamily != request.addrFamily { + return nil, handlers.NewHandlerError(errors.New("address family incompatible between source and request"), http.StatusBadRequest) + } } if dst != "" { request.destination = parseTracingPeer(dst) if request.destination == nil { return nil, handlers.NewHandlerError(errors.New("invalid destination format"), http.StatusBadRequest) } + dstAddrFamily := request.destination.getAddressFamily() + if dstAddrFamily != 0 && dstAddrFamily != request.destination.getAddressFamily() { + return nil, handlers.NewHandlerError(errors.New("address family incompatible between destination and request"), http.StatusBadRequest) + } } return &request, nil } diff --git a/pkg/agent/apiserver/handlers/ovstracing/handler_test.go b/pkg/agent/apiserver/handlers/ovstracing/handler_test.go index a6515dec8c3..05ea7356825 100644 --- a/pkg/agent/apiserver/handlers/ovstracing/handler_test.go +++ b/pkg/agent/apiserver/handlers/ovstracing/handler_test.go @@ -55,7 +55,7 @@ var ( inPodInterface = &interfacestore.InterfaceConfig{ Type: interfacestore.ContainerInterface, InterfaceName: "inPod", - IPs: []net.IP{net.ParseIP("10.1.1.11")}, + IPs: []net.IP{net.ParseIP("10.1.1.11"), net.ParseIP("2001:0db8::ff00:42:11")}, MAC: podMAC, } srcPodInterface = &interfacestore.InterfaceConfig{ @@ -67,7 +67,7 @@ var ( dstPodInterface = &interfacestore.InterfaceConfig{ Type: interfacestore.ContainerInterface, InterfaceName: "dstPod", - IPs: []net.IP{net.ParseIP("10.1.1.13")}, + IPs: []net.IP{net.ParseIP("10.1.1.13"), net.ParseIP("2001:0db8::ff00:42:13")}, MAC: podMAC, } ) @@ -107,7 +107,7 @@ func TestPodFlows(t *testing.T) { expectedStatus: http.StatusBadRequest, }, { - test: "IPv6 source", + test: "IPv6 source conflict", query: "?source=2001:0db8:0000:0000:0000:ff00:0042:8329", expectedStatus: http.StatusBadRequest, }, @@ -148,6 +148,13 @@ func TestPodFlows(t *testing.T) { calledTrace: true, expectedStatus: http.StatusBadRequest, }, + { + test: "Non-existing source IPv6 address", + port: "srcPod", + query: "?addressFamily=6&&source=srcNS/srcPod&&destination=dstNS/dstPod", + calledTrace: false, + expectedStatus: http.StatusNotFound, + }, { test: "Default command", calledTrace: true, @@ -160,6 +167,13 @@ func TestPodFlows(t *testing.T) { calledTrace: true, expectedStatus: http.StatusOK, }, + { + test: "Pod-to-Pod IPv6 traffic", + port: "pod", + query: "?addressFamily=6&&port=inNS/inPod&&destination=dstNS/dstPod", + calledTrace: true, + expectedStatus: http.StatusOK, + }, { test: "Tunnel traffic", port: "antrea-tun0", @@ -186,6 +200,8 @@ func TestPodFlows(t *testing.T) { q.EXPECT().GetInterfaceStore().Return(i).Times(1) if tc.port == "pod" { i.EXPECT().GetContainerInterfacesByPod("inPod", "inNS").Return(nil).Times(1) + } else if tc.port == "srcPod" { + i.EXPECT().GetContainerInterfacesByPod("srcPod", "srcNS").Return([]*interfacestore.InterfaceConfig{srcPodInterface}).Times(1) } else { i.EXPECT().GetInterfaceByName(tc.port).Return(nil, false).Times(1) } diff --git a/pkg/agent/apiserver/handlers/podinterface/handler.go b/pkg/agent/apiserver/handlers/podinterface/handler.go index 97a3c248e5d..f8f58073661 100644 --- a/pkg/agent/apiserver/handlers/podinterface/handler.go +++ b/pkg/agent/apiserver/handlers/podinterface/handler.go @@ -16,7 +16,9 @@ package podinterface import ( "encoding/json" + "net" "net/http" + "strings" "github.com/vmware-tanzu/antrea/pkg/agent/interfacestore" "github.com/vmware-tanzu/antrea/pkg/agent/querier" @@ -40,7 +42,7 @@ func generateResponse(i *interfacestore.InterfaceConfig) Response { PodName: i.ContainerInterfaceConfig.PodName, PodNamespace: i.ContainerInterfaceConfig.PodNamespace, InterfaceName: i.InterfaceName, - IP: i.GetIPv4Addr().String(), + IP: getPodIPsStr(i.IPs), MAC: i.MAC.String(), PortUUID: i.OVSPortConfig.PortUUID, OFPort: i.OVSPortConfig.OFPort, @@ -48,6 +50,14 @@ func generateResponse(i *interfacestore.InterfaceConfig) Response { } } +func getPodIPsStr(ips []net.IP) string { + ipStrs := make([]string, len(ips)) + for i := range ips { + ipStrs[i] = ips[i].String() + } + return strings.Join(ipStrs, ", ") +} + // HandleFunc returns the function which can handle queries issued by the pod-interface command, func HandleFunc(aq querier.AgentQuerier) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/agent/querier/querier.go b/pkg/agent/querier/querier.go index 101c25cb070..b9aa567bb27 100644 --- a/pkg/agent/querier/querier.go +++ b/pkg/agent/querier/querier.go @@ -196,6 +196,9 @@ func (aq agentQuerier) GetAgentInfo(agentInfo *v1beta1.AntreaAgentInfo, partial if aq.nodeConfig.PodIPv4CIDR != nil { agentInfo.NodeSubnet = append(agentInfo.NodeSubnet, aq.nodeConfig.PodIPv4CIDR.String()) } + if aq.nodeConfig.PodIPv6CIDR != nil { + agentInfo.NodeSubnet = append(agentInfo.NodeSubnet, aq.nodeConfig.PodIPv6CIDR.String()) + } agentInfo.OVSInfo.BridgeName = aq.nodeConfig.OVSBridge agentInfo.APIPort = aq.apiPort } diff --git a/pkg/agent/querier/querier_test.go b/pkg/agent/querier/querier_test.go index 5460de4b52f..4c104a8ce34 100644 --- a/pkg/agent/querier/querier_test.go +++ b/pkg/agent/querier/querier_test.go @@ -124,13 +124,14 @@ func TestAgentQuerierGetAgentInfo(t *testing.T) { OVSBridge: "br-int", NodeIPAddr: getIPNet("10.10.0.10"), PodIPv4CIDR: getIPNet("20.20.20.0/24"), + PodIPv6CIDR: getIPNet("2001:ab03:cd04:55ef::/64"), }, apiPort: 10350, partial: false, expectedAgentInfo: &v1beta1.AntreaAgentInfo{ ObjectMeta: v1.ObjectMeta{Name: "foo"}, NodeRef: corev1.ObjectReference{Kind: "Node", Name: "foo"}, - NodeSubnet: []string{"20.20.20.0/24"}, + NodeSubnet: []string{"20.20.20.0/24", "2001:ab03:cd04:55ef::/64"}, OVSInfo: v1beta1.OVSInfo{ Version: ovsVersion, BridgeName: "br-int", diff --git a/pkg/agent/util/net.go b/pkg/agent/util/net.go index c7743775a12..9d2fe1b419b 100644 --- a/pkg/agent/util/net.go +++ b/pkg/agent/util/net.go @@ -17,6 +17,7 @@ package util import ( "crypto/sha1" // #nosec G505: not used for security purposes "encoding/hex" + "errors" "fmt" "io" "net" @@ -26,6 +27,9 @@ const ( interfaceNameLength = 15 interfacePrefixLength = 8 interfaceKeyLength = interfaceNameLength - (interfacePrefixLength + 1) + + FamilyIPv4 uint8 = 4 + FamilyIPv6 uint8 = 6 ) func generateInterfaceName(key string, name string, useHead bool) string { @@ -136,3 +140,21 @@ func ContainIPv6Addr(ips []net.IP) bool { } return false } + +func GetIPWithFamily(ips []net.IP, addrFamily uint8) (net.IP, error) { + if addrFamily == FamilyIPv6 { + for _, ip := range ips { + if ip.To4() == nil { + return ip, nil + } + } + return nil, errors.New("no IP found with IPv6 AddressFamily") + } else { + for _, ip := range ips { + if ip.To4() != nil { + return ip, nil + } + } + return nil, errors.New("no IP found with IPv4 AddressFamily") + } +} diff --git a/pkg/antctl/antctl.go b/pkg/antctl/antctl.go index 4ea89001f24..378be3b5959 100644 --- a/pkg/antctl/antctl.go +++ b/pkg/antctl/antctl.go @@ -345,7 +345,7 @@ var CommandList = &commandList{ }, { name: "source", - usage: "Source of the packet. Can be an OVS port name, or a (local or remote) Pod (specified by /), or an IP address. If specified, the source's IP addresss will be used as the tracing packet's source IP address, and the 'nw_src' field should not be added in the 'flow' argument.", + usage: "Source of the packet. Can be an OVS port name, or a (local or remote) Pod (specified by /), or an IP address. If specified, the source's IP address will be used as the tracing packet's source IP address, and the 'nw_src'/'ipv6_src' field should not be added in the 'flow' argument.", shorthand: "S", }, { @@ -358,6 +358,11 @@ var CommandList = &commandList{ usage: "Specify the flow (packet headers) of the tracing packet. Check the flow syntax descriptions in ovs-ofctl(8) manpage.", shorthand: "f", }, + { + name: "addressFamily", + usage: "Specify the address family fo the packet. Can be 4 (IPv4) or 6 (IPv6). If not specified, the addressFamily will be automatically figured out based on the 'flow'. If no IP address or address family is given in the 'flow', IPv4 is used by default.", + shorthand: "F", + }, }, outputType: single, }, diff --git a/pkg/ovs/ovsctl/appctl.go b/pkg/ovs/ovsctl/appctl.go index e50125193f5..206d10f8839 100644 --- a/pkg/ovs/ovsctl/appctl.go +++ b/pkg/ovs/ovsctl/appctl.go @@ -17,6 +17,7 @@ package ovsctl import ( "bytes" "fmt" + "net" "strings" ) @@ -24,12 +25,12 @@ import ( const exitCodeCommandNotFound = 127 var ( - ipAndNWProtos = []string{"ip", "icmp", "tcp", "udp", "sctp"} + IPAndNWProtos = []string{"ip", "icmp", "tcp", "udp", "sctp", "ipv6", "icmp6", "tcp6", "udp6", "sctp6"} // Some typical non-IP packet types. // "dl_type=0x0800" can be used to indicate an IP packet too, but as it is not // a common way, here we simply assume "dl_type=" is used for non-IP types // only. - nonIPDLTypes = []string{"arp", "rarp", "ip6", "dl_type="} + nonIPDLTypes = []string{"arp", "rarp", "dl_type="} ) type ovsCtlClient struct { @@ -53,7 +54,7 @@ func newExecError(err error, errorOutput string) *ExecError { } func (c *ovsCtlClient) Trace(req *TracingRequest) (string, error) { - var inPort, nwSrc, nwDst, dlSrc, dlDst, ip, nwTTL string + var inPort, nwSrc, nwDst, dlSrc, dlDst, ipKey, nwTTL string if strings.Contains(req.Flow, "in_port=") { if !req.AllowOverrideInPort { @@ -75,18 +76,22 @@ func (c *ovsCtlClient) Trace(req *TracingRequest) (string, error) { } if req.SrcIP != nil { - if strings.Contains(req.Flow, "nw_src=") { - return "", newBadRequestError("duplicated 'nw_src' in flow") + var nwSrcKey string + ipKey, nwSrcKey = getNwSrcKey(req.SrcIP) + if strings.Contains(req.Flow, fmt.Sprintf("%s=", nwSrcKey)) { + return "", newBadRequestError(fmt.Sprintf("duplicated '%s' in flow", nwSrcKey)) } else { - nwSrc = fmt.Sprintf("nw_src=%s,", req.SrcIP.String()) + nwSrc = fmt.Sprintf("%s=%s,", nwSrcKey, req.SrcIP.String()) } } if req.DstIP != nil { + var nwDstKey string + ipKey, nwDstKey = getNwDstKey(req.DstIP) // Do not allow overriding destination IP. - if strings.Contains(req.Flow, "nw_dst=") { - return "", newBadRequestError("duplicated 'nw_dst' in flow") + if strings.Contains(req.Flow, fmt.Sprintf("%s=", nwDstKey)) { + return "", newBadRequestError(fmt.Sprintf("duplicated '%s' in flow", nwDstKey)) } else { - nwDst = fmt.Sprintf("nw_dst=%s,", req.DstIP.String()) + nwDst = fmt.Sprintf("%s=%s,", nwDstKey, req.DstIP.String()) } } @@ -98,13 +103,11 @@ func (c *ovsCtlClient) Trace(req *TracingRequest) (string, error) { dlDst = fmt.Sprintf("dl_dst=%s,", req.DstMAC.String()) } if !nonIP && (nwSrc != "" || nwDst != "") { - // Set DL type to IPv4. - ip = "ip," - for _, s := range ipAndNWProtos { + for _, s := range IPAndNWProtos { if strings.Contains(req.Flow, s) { - // IP or IP protocol is already specified in flow. No need to add "ip" in + // IP or IP protocol is already specified in flow. No need to add "ip"/"ipv6" in // flow. - ip = "" + ipKey = "" break } } @@ -115,11 +118,29 @@ func (c *ovsCtlClient) Trace(req *TracingRequest) (string, error) { } // "ip" or IP protocol must be set before "nw_ttl", "nw_src", "nw_dst", and - // "tp_port". - flow := inPort + dlSrc + dlDst + ip + req.Flow + "," + nwTTL + nwSrc + nwDst + // "tp_port". For IPv6 packet, "ipv6" is required as a precondition. + flow := inPort + dlSrc + dlDst + ipKey + req.Flow + "," + nwTTL + nwSrc + nwDst return c.runTracing(flow) } +// getNwSrcKey returns keys of IP address family and IP source which are supported in ovs-appctl command according +// to the given IP. +func getNwSrcKey(ip net.IP) (string, string) { + if ip.To4() != nil { + return "ip", "nw_src" + } else { + return "ipv6", "ipv6_src" + } +} + +func getNwDstKey(ip net.IP) (string, string) { + if ip.To4() != nil { + return "ip", "nw_dst" + } else { + return "ipv6", "ipv6_dst" + } +} + func (c *ovsCtlClient) runTracing(flow string) (string, error) { out, execErr := c.RunAppctlCmd("ofproto/trace", true, flow) if execErr != nil {