From 9872207361334fbf002dc129927210c1afdf50a4 Mon Sep 17 00:00:00 2001 From: Ryan Tidwell Date: Thu, 23 Jan 2020 16:23:19 -0600 Subject: [PATCH] [WIP] Add initial utilities for link and namespace actions This change introduces basic utilities for managing interfaces, routes, and namespaces. See https://github.com/networkservicemesh/sdk-kernel/issues/2 Signed-off-by: Ryan Tidwell --- go.mod | 2 + .../kernelutils/namespaceutils.go | 62 +++++ pkg/networkservice/mechanisms/client.go | 250 ++++++++++++++++++ pkg/networkservice/mechanisms/vxlan/local.go | 194 ++++++++++++++ pkg/networkservice/mechanisms/vxlan/remote.go | 190 +++++++++++++ test/e2e/namespace_utils_linux_test.go | 107 ++++++++ 6 files changed, 805 insertions(+) create mode 100644 pkg/networkservice/kernelutils/namespaceutils.go create mode 100644 pkg/networkservice/mechanisms/client.go create mode 100644 pkg/networkservice/mechanisms/vxlan/local.go create mode 100644 pkg/networkservice/mechanisms/vxlan/remote.go create mode 100644 test/e2e/namespace_utils_linux_test.go diff --git a/go.mod b/go.mod index 8a925f50..c75b93b3 100644 --- a/go.mod +++ b/go.mod @@ -3,4 +3,6 @@ module github.com/networkservicemesh/sdk-kernel go 1.13 require ( + github.com/vishvananda/netlink v1.0.0 + github.com/vishvananda/netns v0.0.0-20190625233234-7109fa855b0f ) diff --git a/pkg/networkservice/kernelutils/namespaceutils.go b/pkg/networkservice/kernelutils/namespaceutils.go new file mode 100644 index 00000000..45a890f9 --- /dev/null +++ b/pkg/networkservice/kernelutils/namespaceutils.go @@ -0,0 +1,62 @@ +// Copyright 2019 VMware, Inc. +// Copyright 2020 SUSE LLC. +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kernelutils + +import ( + "github.com/sirupsen/logrus" + "github.com/vishvananda/netlink" + "github.com/vishvananda/netns" +) + +func InjectLinkInNamespace(targetNs netns.NsHandle, ifaceName string) error { + hostNs, nsErr := netns.Get() + defer hostNs.Close() + if nsErr != nil { + logrus.Errorf("sdk-kernel: failed to get host namespace") + return nsErr + } + + /* Get a link object for the interface */ + ifaceLink, linkErr := netlink.LinkByName(ifaceName) + if linkErr != nil { + // link was not found in host namespace, see if we can find it in target namespace + nsErr = netns.Set(targetNs) + defer netns.Set(hostNs) + if nsErr != nil { + logrus.Errorf("sdk-kernel: failed switching to desired namespace: %v", nsErr) + return nsErr + } + _, linkErr := netlink.LinkByName(ifaceName) + if linkErr != nil { + // we didn't find the link in the target namespace either, return an error + return linkErr + } + nsErr = netns.Set(hostNs) + if nsErr != nil { + logrus.Errorf("sdk-kernel: failed switching to host namespace: %v", nsErr) + return nsErr + } + } else { + /* Inject the interface into the desired namespace */ + linkErr = netlink.LinkSetNsFd(ifaceLink, int(targetNs)) + if linkErr != nil { + logrus.Errorf("sdk-kernel: failed to inject %q in namespace - %v", ifaceName, linkErr) + return linkErr + } + } + return nil +} diff --git a/pkg/networkservice/mechanisms/client.go b/pkg/networkservice/mechanisms/client.go new file mode 100644 index 00000000..5eab397f --- /dev/null +++ b/pkg/networkservice/mechanisms/client.go @@ -0,0 +1,250 @@ +// Copyright 2019 VMware, Inc. +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kernelforwarder + +import ( + "github.com/pkg/errors" + + "github.com/networkservicemesh/networkservicemesh/controlplane/api/connection/mechanisms/common" + "github.com/networkservicemesh/networkservicemesh/controlplane/api/connection/mechanisms/vxlan" + + "net" + "strconv" + + "github.com/networkservicemesh/networkservicemesh/controlplane/api/connectioncontext" + "github.com/networkservicemesh/networkservicemesh/controlplane/api/crossconnect" + + "github.com/sirupsen/logrus" + "github.com/vishvananda/netlink" + "github.com/vishvananda/netns" +) + +// Kernel forwarding plane related constants +const ( + cLOCAL = iota + cINCOMING = iota + cOUTGOING = iota +) + +const ( + /* VETH pairs are used only for local connections(same node), so we can use a larger MTU size as there's no multi-node connection */ + cVETHMTU = 16000 + cCONNECT = true + cDISCONNECT = false +) + +type connectionConfig struct { + id string + srcNetNsInode string + dstNetNsInode string + srcName string + dstName string + srcIP string + dstIP string + srcIPVXLAN net.IP + dstIPVXLAN net.IP + srcRoutes []*connectioncontext.Route + dstRoutes []*connectioncontext.Route + neighbors []*connectioncontext.IpNeighbor + vni int +} + +// setupLinkInNs is responsible for configuring an interface inside a given namespace - assigns IP address, routes, etc. +func setupLinkInNs(containerNs netns.NsHandle, ifaceName, ifaceIP string, routes []*connectioncontext.Route, neighbors []*connectioncontext.IpNeighbor, inject bool) error { + if inject { + /* Get a link object for the interface */ + ifaceLink, err := netlink.LinkByName(ifaceName) + if err != nil { + logrus.Errorf("common: failed to get link for %q - %v", ifaceName, err) + return err + } + /* Inject the interface into the desired namespace */ + if err = netlink.LinkSetNsFd(ifaceLink, int(containerNs)); err != nil { + logrus.Errorf("common: failed to inject %q in namespace - %v", ifaceName, err) + return err + } + } + /* Save current network namespace */ + hostNs, err := netns.Get() + if err != nil { + logrus.Errorf("common: failed getting host namespace: %v", err) + return err + } + logrus.Debug("common: host namespace: ", hostNs) + defer func() { + if err = hostNs.Close(); err != nil { + logrus.Error("common: failed closing host namespace handle: ", err) + } + logrus.Debug("common: closed host namespace handle: ", hostNs) + }() + + /* Switch to the desired namespace */ + if err = netns.Set(containerNs); err != nil { + logrus.Errorf("common: failed switching to desired namespace: %v", err) + return err + } + logrus.Debug("common: switched to desired namespace: ", containerNs) + + /* Don't forget to switch back to the host namespace */ + defer func() { + if err = netns.Set(hostNs); err != nil { + logrus.Errorf("common: failed switching back to host namespace: %v", err) + } + logrus.Debug("common: switched back to host namespace: ", hostNs) + }() + + /* Get a link for the interface name */ + link, err := netlink.LinkByName(ifaceName) + if err != nil { + logrus.Errorf("common: failed to lookup %q, %v", ifaceName, err) + return err + } + if inject { + var addr *netlink.Addr + /* Parse the IP address */ + addr, err = netlink.ParseAddr(ifaceIP) + if err != nil { + logrus.Errorf("common: failed to parse IP %q: %v", ifaceIP, err) + return err + } + /* Set IP address */ + if err = netlink.AddrAdd(link, addr); err != nil { + logrus.Errorf("common: failed to set IP %q: %v", ifaceIP, err) + return err + } + /* Bring the interface UP */ + if err = netlink.LinkSetUp(link); err != nil { + logrus.Errorf("common: failed to bring %q up: %v", ifaceName, err) + return err + } + /* Add routes */ + if err = addRoutes(link, addr, routes); err != nil { + logrus.Error("common: failed adding routes:", err) + return err + } + /* Add neighbors - applicable only for source side */ + if err = addNeighbors(link, neighbors); err != nil { + logrus.Error("common: failed adding neighbors:", err) + return err + } + } else { + /* Bring the interface DOWN */ + if err = netlink.LinkSetDown(link); err != nil { + logrus.Errorf("common: failed to bring %q down: %v", ifaceName, err) + return err + } + /* Inject the interface back to current namespace */ + if err = netlink.LinkSetNsFd(link, int(hostNs)); err != nil { + logrus.Errorf("common: failed to inject %q back to host namespace - %v", ifaceName, err) + return err + } + } + return nil +} + +//nolint +func newConnectionConfig(crossConnect *crossconnect.CrossConnect, connType uint8) (*connectionConfig, error) { + switch connType { + case cLOCAL: + return &connectionConfig{ + id: crossConnect.GetId(), + srcNetNsInode: crossConnect.GetSource().GetMechanism().GetParameters()[common.NetNsInodeKey], + dstNetNsInode: crossConnect.GetDestination().GetMechanism().GetParameters()[common.NetNsInodeKey], + srcName: crossConnect.GetSource().GetMechanism().GetParameters()[common.InterfaceNameKey], + dstName: crossConnect.GetDestination().GetMechanism().GetParameters()[common.InterfaceNameKey], + srcIP: crossConnect.GetSource().GetContext().GetIpContext().GetSrcIpAddr(), + dstIP: crossConnect.GetSource().GetContext().GetIpContext().GetDstIpAddr(), + srcRoutes: crossConnect.GetSource().GetContext().GetIpContext().GetDstRoutes(), + dstRoutes: crossConnect.GetDestination().GetContext().GetIpContext().GetSrcRoutes(), + neighbors: crossConnect.GetSource().GetContext().GetIpContext().GetIpNeighbors(), + }, nil + case cINCOMING: + vni, _ := strconv.Atoi(crossConnect.GetSource().GetMechanism().GetParameters()[vxlan.VNI]) + return &connectionConfig{ + id: crossConnect.GetId(), + dstNetNsInode: crossConnect.GetDestination().GetMechanism().GetParameters()[common.NetNsInodeKey], + dstName: crossConnect.GetDestination().GetMechanism().GetParameters()[common.InterfaceNameKey], + dstIP: crossConnect.GetDestination().GetContext().GetIpContext().GetDstIpAddr(), + dstRoutes: crossConnect.GetDestination().GetContext().GetIpContext().GetSrcRoutes(), + neighbors: nil, + srcIPVXLAN: net.ParseIP(crossConnect.GetSource().GetMechanism().GetParameters()[vxlan.SrcIP]), + dstIPVXLAN: net.ParseIP(crossConnect.GetSource().GetMechanism().GetParameters()[vxlan.DstIP]), + vni: vni, + }, nil + case cOUTGOING: + vni, _ := strconv.Atoi(crossConnect.GetDestination().GetMechanism().GetParameters()[vxlan.VNI]) + return &connectionConfig{ + id: crossConnect.GetId(), + srcNetNsInode: crossConnect.GetSource().GetMechanism().GetParameters()[common.NetNsInodeKey], + srcName: crossConnect.GetSource().GetMechanism().GetParameters()[common.InterfaceNameKey], + srcIP: crossConnect.GetSource().GetContext().GetIpContext().GetSrcIpAddr(), + srcRoutes: crossConnect.GetSource().GetContext().GetIpContext().GetDstRoutes(), + neighbors: crossConnect.GetSource().GetContext().GetIpContext().GetIpNeighbors(), + srcIPVXLAN: net.ParseIP(crossConnect.GetDestination().GetMechanism().GetParameters()[vxlan.SrcIP]), + dstIPVXLAN: net.ParseIP(crossConnect.GetDestination().GetMechanism().GetParameters()[vxlan.DstIP]), + vni: vni, + }, nil + default: + logrus.Error("common: connection configuration: invalid connection type") + return nil, errors.New("common: invalid connection type") + } +} + +// addRoutes adds routes +func addRoutes(link netlink.Link, addr *netlink.Addr, routes []*connectioncontext.Route) error { + for _, route := range routes { + _, routeNet, err := net.ParseCIDR(route.GetPrefix()) + if err != nil { + logrus.Error("common: failed parsing route CIDR:", err) + return err + } + route := netlink.Route{ + LinkIndex: link.Attrs().Index, + Dst: &net.IPNet{ + IP: routeNet.IP, + Mask: routeNet.Mask, + }, + Src: addr.IP, + } + if err = netlink.RouteAdd(&route); err != nil { + logrus.Error("common: failed adding routes:", err) + return err + } + } + return nil +} + +// addNeighbors adds neighbors +func addNeighbors(link netlink.Link, neighbors []*connectioncontext.IpNeighbor) error { + for _, neighbor := range neighbors { + mac, err := net.ParseMAC(neighbor.GetHardwareAddress()) + if err != nil { + logrus.Error("common: failed parsing the MAC address for IP neighbors:", err) + return err + } + neigh := netlink.Neigh{ + LinkIndex: link.Attrs().Index, + State: 0x02, // netlink.NUD_REACHABLE, // the constant is somehow not being found in the package in case of using a darwin based machine + IP: net.ParseIP(neighbor.GetIp()), + HardwareAddr: mac, + } + if err = netlink.NeighAdd(&neigh); err != nil { + logrus.Error("common: failed adding neighbor:", err) + return err + } + } + return nil +} diff --git a/pkg/networkservice/mechanisms/vxlan/local.go b/pkg/networkservice/mechanisms/vxlan/local.go new file mode 100644 index 00000000..918fbec9 --- /dev/null +++ b/pkg/networkservice/mechanisms/vxlan/local.go @@ -0,0 +1,194 @@ +// Copyright 2019 VMware, Inc. +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kernelforwarder + +import ( + "runtime" + + "github.com/networkservicemesh/networkservicemesh/controlplane/api/crossconnect" + "github.com/networkservicemesh/networkservicemesh/forwarder/kernel-forwarder/pkg/monitoring" + "github.com/networkservicemesh/networkservicemesh/utils/fs" + + "github.com/sirupsen/logrus" + "github.com/vishvananda/netlink" +) + +// handleLocalConnection either creates or deletes a local connection - same host +func handleLocalConnection(crossConnect *crossconnect.CrossConnect, connect bool) (map[string]monitoring.Device, error) { + logrus.Info("local: connection type - local source/local destination") + var devices map[string]monitoring.Device + /* 1. Get the connection configuration */ + cfg, err := newConnectionConfig(crossConnect, cLOCAL) + if err != nil { + logrus.Errorf("local: failed to get connection configuration - %v", err) + return nil, err + } + if connect { + /* 2. Create a connection */ + devices, err = createLocalConnection(cfg) + if err != nil { + logrus.Errorf("local: failed to create connection - %v", err) + devices = nil + } + } else { + /* 3. Delete a connection */ + devices, err = deleteLocalConnection(cfg) + if err != nil { + logrus.Errorf("local: failed to delete connection - %v", err) + devices = nil + } + } + return devices, err +} + +// createLocalConnection handles creating a local connection +func createLocalConnection(cfg *connectionConfig) (map[string]monitoring.Device, error) { + logrus.Info("local: creating connection...") + /* Lock the OS thread so we don't accidentally switch namespaces */ + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + /* Get namespace handler - source */ + srcNsHandle, err := fs.GetNsHandleFromInode(cfg.srcNetNsInode) + if err != nil { + logrus.Errorf("local: failed to get source namespace handle - %v", err) + return nil, err + } + /* If successful, don't forget to close the handler upon exit */ + defer func() { + if err = srcNsHandle.Close(); err != nil { + logrus.Error("local: error when closing source handle: ", err) + } + logrus.Debug("local: closed source handle: ", srcNsHandle, cfg.srcNetNsInode) + }() + logrus.Debug("local: opened source handle: ", srcNsHandle, cfg.srcNetNsInode) + + /* Get namespace handler - destination */ + dstNsHandle, err := fs.GetNsHandleFromInode(cfg.dstNetNsInode) + if err != nil { + logrus.Errorf("local: failed to get destination namespace handle - %v", err) + return nil, err + } + defer func() { + if err = dstNsHandle.Close(); err != nil { + logrus.Error("local: error when closing destination handle: ", err) + } + logrus.Debug("local: closed destination handle: ", dstNsHandle, cfg.dstNetNsInode) + }() + logrus.Debug("local: opened destination handle: ", dstNsHandle, cfg.dstNetNsInode) + + /* Create the VETH pair - host namespace */ + if err = netlink.LinkAdd(newVETH(cfg.srcName, cfg.dstName)); err != nil { + logrus.Errorf("local: failed to create VETH pair - %v", err) + return nil, err + } + + /* Setup interface - source namespace */ + if err = setupLinkInNs(srcNsHandle, cfg.srcName, cfg.srcIP, cfg.srcRoutes, cfg.neighbors, true); err != nil { + logrus.Errorf("local: failed to setup interface - source - %q: %v", cfg.srcName, err) + return nil, err + } + + /* Setup interface - destination namespace */ + if err = setupLinkInNs(dstNsHandle, cfg.dstName, cfg.dstIP, cfg.dstRoutes, nil, true); err != nil { + logrus.Errorf("local: failed to setup interface - destination - %q: %v", cfg.dstName, err) + return nil, err + } + + logrus.Infof("local: creation completed for devices - source: %s, destination: %s", cfg.srcName, cfg.dstName) + srcDevice := monitoring.Device{Name: cfg.srcName, XconName: "SRC-" + cfg.id} + dstDevice := monitoring.Device{Name: cfg.dstName, XconName: "DST-" + cfg.id} + return map[string]monitoring.Device{cfg.srcNetNsInode: srcDevice, cfg.dstNetNsInode: dstDevice}, nil +} + +// deleteLocalConnection handles deleting a local connection +func deleteLocalConnection(cfg *connectionConfig) (map[string]monitoring.Device, error) { + logrus.Info("local: deleting connection...") + /* Lock the OS thread so we don't accidentally switch namespaces */ + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + /* Get namespace handler - source */ + srcNsHandle, err := fs.GetNsHandleFromInode(cfg.srcNetNsInode) + if err != nil { + logrus.Errorf("local: failed to get source namespace handle - %v", err) + return nil, err + } + /* If successful, don't forget to close the handler upon exit */ + defer func() { + if err = srcNsHandle.Close(); err != nil { + logrus.Error("local: error when closing source handle: ", err) + } + logrus.Debug("local: closed source handle: ", srcNsHandle, cfg.srcNetNsInode) + }() + logrus.Debug("local: opened source handle: ", srcNsHandle, cfg.srcNetNsInode) + + /* Get namespace handler - destination */ + dstNsHandle, err := fs.GetNsHandleFromInode(cfg.dstNetNsInode) + if err != nil { + logrus.Errorf("local: failed to get destination namespace handle - %v", err) + return nil, err + } + defer func() { + if err = dstNsHandle.Close(); err != nil { + logrus.Error("local: error when closing destination handle: ", err) + } + logrus.Debug("local: closed destination handle: ", dstNsHandle, cfg.dstNetNsInode) + }() + logrus.Debug("local: opened destination handle: ", dstNsHandle, cfg.dstNetNsInode) + + /* Extract interface - source namespace */ + if err = setupLinkInNs(srcNsHandle, cfg.srcName, cfg.srcIP, nil, nil, false); err != nil { + logrus.Errorf("local: failed to extract interface - source - %q: %v", cfg.srcName, err) + return nil, err + } + + /* Extract interface - destination namespace */ + if err = setupLinkInNs(dstNsHandle, cfg.dstName, cfg.dstIP, nil, nil, false); err != nil { + logrus.Errorf("local: failed to extract interface - destination - %q: %v", cfg.dstName, err) + return nil, err + } + + /* Get a link object for the interface */ + ifaceLink, err := netlink.LinkByName(cfg.srcName) + if err != nil { + logrus.Errorf("local: failed to get link for %q - %v", cfg.srcName, err) + return nil, err + } + + /* Delete the VETH pair - host namespace */ + if err := netlink.LinkDel(ifaceLink); err != nil { + logrus.Errorf("local: failed to delete the VETH pair - %v", err) + return nil, err + } + + logrus.Infof("local: deletion completed for devices - source: %s, destination: %s", cfg.srcName, cfg.dstName) + srcDevice := monitoring.Device{Name: cfg.srcName, XconName: "SRC-" + cfg.id} + dstDevice := monitoring.Device{Name: cfg.dstName, XconName: "DST-" + cfg.id} + return map[string]monitoring.Device{cfg.srcNetNsInode: srcDevice, cfg.dstNetNsInode: dstDevice}, nil +} + +// newVETH returns a VETH interface instance +func newVETH(srcName, dstName string) *netlink.Veth { + /* Populate the VETH interface configuration */ + return &netlink.Veth{ + LinkAttrs: netlink.LinkAttrs{ + Name: srcName, + MTU: cVETHMTU, + }, + PeerName: dstName, + } +} diff --git a/pkg/networkservice/mechanisms/vxlan/remote.go b/pkg/networkservice/mechanisms/vxlan/remote.go new file mode 100644 index 00000000..8738f738 --- /dev/null +++ b/pkg/networkservice/mechanisms/vxlan/remote.go @@ -0,0 +1,190 @@ +// Copyright 2019 VMware, Inc. +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kernelforwarder + +import ( + "runtime" + + "github.com/networkservicemesh/networkservicemesh/controlplane/api/connection/mechanisms/kernel" + "github.com/networkservicemesh/networkservicemesh/controlplane/api/connection/mechanisms/vxlan" + + "github.com/pkg/errors" + + "github.com/networkservicemesh/networkservicemesh/controlplane/api/connectioncontext" + "github.com/networkservicemesh/networkservicemesh/controlplane/api/crossconnect" + "github.com/networkservicemesh/networkservicemesh/utils/fs" + + "net" + + "github.com/sirupsen/logrus" + "github.com/vishvananda/netlink" + + "github.com/networkservicemesh/networkservicemesh/forwarder/kernel-forwarder/pkg/monitoring" + "github.com/networkservicemesh/networkservicemesh/forwarder/pkg/common" +) + +// handleRemoteConnection handles remote connect/disconnect requests for either incoming or outgoing connections +func handleRemoteConnection(egress common.EgressInterfaceType, crossConnect *crossconnect.CrossConnect, connect bool) (map[string]monitoring.Device, error) { + if crossConnect.GetSource().GetMechanism().GetType() == vxlan.MECHANISM && + crossConnect.GetLocalDestination().GetMechanism().GetType() == kernel.MECHANISM { + /* 1. Incoming remote connection */ + logrus.Info("remote: connection type - remote source/local destination - incoming") + return handleConnection(egress, crossConnect, connect, cINCOMING) + } else if crossConnect.GetSource().GetMechanism().GetType() == kernel.MECHANISM && + crossConnect.GetDestination().GetMechanism().GetType() == vxlan.MECHANISM { + /* 2. Outgoing remote connection */ + logrus.Info("remote: connection type - local source/remote destination - outgoing") + return handleConnection(egress, crossConnect, connect, cOUTGOING) + } + err := errors.Errorf("remote: invalid connection type") + logrus.Errorf("%+v", err) + return nil, err +} + +// handleConnection process the request to either creating or deleting a connection +func handleConnection(egress common.EgressInterfaceType, crossConnect *crossconnect.CrossConnect, connect bool, direction uint8) (map[string]monitoring.Device, error) { + var devices map[string]monitoring.Device + + /* 1. Get the connection configuration */ + cfg, err := newConnectionConfig(crossConnect, direction) + if err != nil { + logrus.Errorf("remote: failed to get connection configuration - %+v", err) + return nil, err + } + + nsPath, name, ifaceIP, vxlanIP, routes, xconName := modifyConfiguration(cfg, direction) + + if connect { + /* 2. Create a connection */ + devices, err = createRemoteConnection(nsPath, name, xconName, ifaceIP, egress.SrcIPNet().IP, vxlanIP, cfg.vni, routes, cfg.neighbors) + if err != nil { + logrus.Errorf("remote: failed to create connection - %v", err) + devices = nil + } + } else { + /* 3. Delete a connection */ + devices, err = deleteRemoteConnection(nsPath, name, xconName) + if err != nil { + logrus.Errorf("remote: failed to delete connection - %v", err) + devices = nil + } + } + return devices, err +} + +// createRemoteConnection handler for creating a remote connection +func createRemoteConnection(nsInode, ifaceName, xconName, ifaceIP string, egressIP, remoteIP net.IP, vni int, routes []*connectioncontext.Route, neighbors []*connectioncontext.IpNeighbor) (map[string]monitoring.Device, error) { + logrus.Info("remote: creating connection...") + + /* Lock the OS thread so we don't accidentally switch namespaces */ + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + /* Get namespace handler - destination */ + dstHandle, err := fs.GetNsHandleFromInode(nsInode) + if err != nil { + logrus.Errorf("remote: failed to get destination namespace handle - %v", err) + return nil, err + } + /* If successful, don't forget to close the handler upon exit */ + defer func() { + if err = dstHandle.Close(); err != nil { + logrus.Error("remote: error when closing destination handle: ", err) + } + logrus.Debug("remote: closed destination handle: ", dstHandle, nsInode) + }() + logrus.Debug("remote: opened destination handle: ", dstHandle, nsInode) + + /* Create interface - host namespace */ + if err = netlink.LinkAdd(newVXLAN(ifaceName, egressIP, remoteIP, vni)); err != nil { + logrus.Errorf("remote: failed to create VXLAN interface - %v", err) + return nil, err + } + + /* Setup interface - inject from host to destination namespace */ + if err = setupLinkInNs(dstHandle, ifaceName, ifaceIP, routes, neighbors, true); err != nil { + logrus.Errorf("remote: failed to setup interface - destination - %q: %v", ifaceName, err) + return nil, err + } + logrus.Infof("remote: creation completed for device - %s", ifaceName) + return map[string]monitoring.Device{nsInode: {Name: ifaceName, XconName: xconName}}, nil +} + +// deleteRemoteConnection handler for deleting a remote connection +func deleteRemoteConnection(nsInode, ifaceName, xconName string) (map[string]monitoring.Device, error) { + logrus.Info("remote: deleting connection...") + + /* Lock the OS thread so we don't accidentally switch namespaces */ + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + /* Get namespace handler - destination */ + dstHandle, err := fs.GetNsHandleFromInode(nsInode) + if err != nil { + logrus.Errorf("remote: failed to get destination namespace handle - %v", err) + return nil, err + } + /* If successful, don't forget to close the handler upon exit */ + defer func() { + if err = dstHandle.Close(); err != nil { + logrus.Error("remote: error when closing destination handle: ", err) + } + logrus.Debug("remote: closed destination handle: ", dstHandle, nsInode) + }() + logrus.Debug("remote: opened destination handle: ", dstHandle, nsInode) + + /* Setup interface - extract from destination to host namespace */ + if err = setupLinkInNs(dstHandle, ifaceName, "", nil, nil, false); err != nil { + logrus.Errorf("remote: failed to setup interface - destination - %q: %v", ifaceName, err) + return nil, err + } + + /* Get a link object for interface */ + ifaceLink, err := netlink.LinkByName(ifaceName) + if err != nil { + logrus.Errorf("remote: failed to get link for %q - %v", ifaceName, err) + return nil, err + } + + /* Delete the VXLAN interface - host namespace */ + if err := netlink.LinkDel(ifaceLink); err != nil { + logrus.Errorf("remote: failed to delete VXLAN interface - %v", err) + return nil, err + } + logrus.Infof("remote: deletion completed for device - %s", ifaceName) + return map[string]monitoring.Device{nsInode: {Name: ifaceName, XconName: xconName}}, nil +} + +// modifyConfiguration swaps the values based on the direction of the connection - incoming or outgoing +func modifyConfiguration(cfg *connectionConfig, direction uint8) (string, string, string, net.IP, []*connectioncontext.Route, string) { + if direction == cINCOMING { + return cfg.dstNetNsInode, cfg.dstName, cfg.dstIP, cfg.srcIPVXLAN, cfg.dstRoutes, "DST-" + cfg.id + } + return cfg.srcNetNsInode, cfg.srcName, cfg.srcIP, cfg.dstIPVXLAN, cfg.srcRoutes, "SRC-" + cfg.id +} + +// newVXLAN returns a VXLAN interface instance +func newVXLAN(ifaceName string, egressIP, remoteIP net.IP, vni int) *netlink.Vxlan { + /* Populate the VXLAN interface configuration */ + return &netlink.Vxlan{ + LinkAttrs: netlink.LinkAttrs{ + Name: ifaceName, + }, + VxlanId: vni, + Group: remoteIP, + SrcAddr: egressIP, + } +} diff --git a/test/e2e/namespace_utils_linux_test.go b/test/e2e/namespace_utils_linux_test.go new file mode 100644 index 00000000..64664b40 --- /dev/null +++ b/test/e2e/namespace_utils_linux_test.go @@ -0,0 +1,107 @@ +// Copyright 2020 SUSE LLC. +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sdk_kernel_tests + +import ( + "testing" + + . "github.com/onsi/gomega" + + "github.com/networkservicemesh/sdk-kernel/pkg/networkservice/kernelutils" + + "github.com/sirupsen/logrus" + "github.com/vishvananda/netlink" + "github.com/vishvananda/netns" +) + +const testVethSrcName = "sdk-kernel-src" +const testVethDstName = "sdk-kernel-dst" +var testVeth = netlink.Veth { + LinkAttrs: netlink.LinkAttrs { + Name: testVethSrcName, + }, + PeerName: testVethDstName, +} + +func TestInjectLinkInNamespace(t *testing.T) { + g := NewWithT(t) + + // set up the basic resources for the test + currns, _ := netns.Get() + testns, _ := netns.New() + _ = netns.Set(currns) + vethErr := netlink.LinkAdd(&testVeth) + g.Expect(vethErr).To(BeNil(), "Error creating test veth pair: %v", vethErr) + + // clean up after ourselves + defer func(rootns netns.NsHandle, testns netns.NsHandle, veth *netlink.Veth) { + _ = netns.Set(rootns) + testns.Close() + netlink.LinkDel(veth) + }(currns, testns, &testVeth) + + // move one end of the veth into testns and run assertions + injectError := kernelutils.InjectLinkInNamespace(testns, testVeth.PeerName) + + _ = netns.Set(testns) + nsLinks, _ := netlink.LinkList() + _, linkErr := netlink.LinkByName(testVeth.PeerName) + + // assert no error on link injection + g.Expect(injectError).To(BeNil(), "Error injecting test interface in namespace: %v", injectError) + // assert only loopback and the interface we inject exist in the namespace + g.Expect(len(nsLinks)).To(Equal(2)) + g.Expect(linkErr).To(BeNil(), "Error getting locating test interface in namespace: %v", linkErr) + + logrus.Printf("Done") +} + +func TestIdempotentInjectLinkInNamespace(t *testing.T) { + g := NewWithT(t) + + // set up the basic resources for the test + currns, _ := netns.Get() + testns, _ := netns.New() + _ = netns.Set(currns) + vethErr := netlink.LinkAdd(&testVeth) + g.Expect(vethErr).To(BeNil(), "Error creating test veth pair: %v", vethErr) + + // clean up after ourselves + defer func(rootns netns.NsHandle, testns netns.NsHandle, veth *netlink.Veth) { + _ = netns.Set(rootns) + testns.Close() + netlink.LinkDel(veth) + }(currns, testns, &testVeth) + + // move one end of the veth into testns and run assertions + injectError := kernelutils.InjectLinkInNamespace(testns, testVeth.PeerName) + // assert no error on initial link injection + g.Expect(injectError).To(BeNil(), "Error injecting test interface in namespace: %v", injectError) + + // attempt to re-inject the link in the target namespace + injectError = kernelutils.InjectLinkInNamespace(testns, testVeth.PeerName) + _ = netns.Set(testns) + nsLinks, _ := netlink.LinkList() + _, linkErr := netlink.LinkByName(testVeth.PeerName) + + // assert no error on link injection + g.Expect(injectError).To(BeNil(), "Error injecting test interface in namespace: %v", injectError) + // assert only loopback and the interface we inject exist in the namespace + g.Expect(len(nsLinks)).To(Equal(2)) + g.Expect(linkErr).To(BeNil(), "Error getting locating test interface in namespace: %v", linkErr) + + logrus.Printf("Done") +}