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..0e37f6b93c3 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" @@ -47,6 +48,16 @@ type tracingPeer struct { ip net.IP } +func (p *tracingPeer) getAddressFamily() uint8 { + if p.ip == nil { + return 0 + } + if p.ip.To4() != nil { + return ovsctl.FamilyIPv4 + } + return ovsctl.FamilyIPv6 +} + type request struct { // tracingPeer.ip is invalid for inputPort, as inputPort can only be // specified by ovsPort or Pod Namespace/name. @@ -54,6 +65,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 +80,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 := 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,11 +129,43 @@ 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 +} + +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 getIPWithFamily(podIPs, addrFamily) +} + +func getIPWithFamily(ips []net.IP, addrFamily uint8) (net.IP, error) { + if addrFamily == ovsctl.FamilyIPv6 { + for _, ip := range ips { + if ip.To4() == nil { + return ip, nil + } + } + return nil, errors.New("no IP found with the specified AddressFamily") + } else { + for _, ip := range ips { + if ip.To4() != nil { + return ip, nil + } + } + return nil, errors.New("no IP found with the specified AddressFamily") + } } func prepareTracingRequest(aq querier.AgentQuerier, req *request) (*ovsctl.TracingRequest, *handlers.HandlerError) { - traceReq := ovsctl.TracingRequest{Flow: req.flow, AllowOverrideInPort: false} + traceReq := ovsctl.TracingRequest{Flow: req.flow, AllowOverrideInPort: false, AddrFamily: req.addrFamily} var inPort *interfacestore.InterfaceConfig if req.inputPort != nil { @@ -131,7 +189,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 +206,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 +268,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 +// IPvaddress 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 +290,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 +299,19 @@ 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 = ovsctl.FamilyIPv6 + } else if addrFamily == "4" { + request.addrFamily = ovsctl.FamilyIPv4 + } else if strings.Contains(request.flow, "ipv6") { + request.addrFamily = ovsctl.FamilyIPv6 + } else { + request.addrFamily = ovsctl.FamilyIPv4 + } + if port != "" { request.inputPort = parseTracingPeer(port) // Input port cannot be specified with an IP. @@ -258,12 +324,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 b370bf823e0..174df99db07 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/antctl/antctl.go b/pkg/antctl/antctl.go index 3bf4f1c7930..8d6b4e85877 100644 --- a/pkg/antctl/antctl.go +++ b/pkg/antctl/antctl.go @@ -298,12 +298,12 @@ 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", }, { name: "destination", - usage: "Destination of the packet. Can be an OVS port name, or a (local or remote) Pod or a Service (specified by /), or an IP address. If specified, the destination's IP address (the ClusterIP for a Service) will be used as the tacing packet's destination IP address, and the 'nw_dst' field should not be added in the 'flow' argument.", + usage: "Destination of the packet. Can be an OVS port name, or a (local or remote) Pod or a Service (specified by /), or an IP address. If specified, the destination's IP address (the ClusterIP for a Service) will be used as the tacing packet's destination IP address, and the 'nw_dst'/'ipv6_dst' field should not be added in the 'flow' argument.", shorthand: "D", }, { @@ -311,6 +311,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..c22f0a7fb45 100644 --- a/pkg/ovs/ovsctl/appctl.go +++ b/pkg/ovs/ovsctl/appctl.go @@ -24,12 +24,12 @@ import ( const exitCodeCommandNotFound = 127 var ( - ipAndNWProtos = []string{"ip", "icmp", "tcp", "udp", "sctp"} + ipAndNWProtos = []string{"ip", "ipv6", "icmp", "tcp", "udp", "sctp"} // 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 { @@ -75,18 +75,20 @@ 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") + nwSrcKey := getNwSrcKey(req.AddrFamily) + 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 { + nwDstKey := getNwDstKey(req.AddrFamily) // 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,8 +100,7 @@ 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," + ip = fmt.Sprintf("%s,", getIPProtoStr(req.AddrFamily)) 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 @@ -115,11 +116,38 @@ 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". + // "tp_port". For IPv6 packet, "ipv6" is required as a precondition. flow := inPort + dlSrc + dlDst + ip + req.Flow + "," + nwTTL + nwSrc + nwDst return c.runTracing(flow) } +func getIPProtoStr(addressFamily uint8) string { + switch addressFamily { + case FamilyIPv6: + return "ipv6" + default: + return "ip" + } +} + +func getNwSrcKey(addressFamily uint8) string { + switch addressFamily { + case FamilyIPv6: + return "ipv6_src" + default: + return "nw_src" + } +} + +func getNwDstKey(addressFamily uint8) string { + switch addressFamily { + case FamilyIPv6: + return "ipv6_dst" + default: + return "nw_dst" + } +} + func (c *ovsCtlClient) runTracing(flow string) (string, error) { out, execErr := c.RunAppctlCmd("ofproto/trace", true, flow) if execErr != nil { diff --git a/pkg/ovs/ovsctl/interface.go b/pkg/ovs/ovsctl/interface.go index f77d9e8258b..3ef9962646c 100644 --- a/pkg/ovs/ovsctl/interface.go +++ b/pkg/ovs/ovsctl/interface.go @@ -18,14 +18,20 @@ import ( "os/exec" ) +const ( + FamilyIPv4 uint8 = 4 + FamilyIPv6 uint8 = 6 +) + // TracingRequest defines tracing request parameters. type TracingRequest struct { - InPort string // Input port. - SrcIP net.IP - DstIP net.IP - SrcMAC net.HardwareAddr - DstMAC net.HardwareAddr - Flow string + InPort string // Input port. + SrcIP net.IP + DstIP net.IP + SrcMAC net.HardwareAddr + DstMAC net.HardwareAddr + AddrFamily uint8 + Flow string // Whether in_port field in Flow can override InPort. AllowOverrideInPort bool }