diff --git a/Makefile b/Makefile index 3f421ca7fd..baea154def 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ endif # Run unit tests unit: test-container - docker run --mount type=bind,src=$(shell pwd),dst=/go/src/liqo -w /go/src/liqo --rm liqo-test + docker run --cap-add=NET_ADMIN --mount type=bind,src=$(shell pwd),dst=/go/src/liqo -w /go/src/liqo --rm liqo-test # Run e2e tests e2e: gen @@ -101,7 +101,7 @@ ifeq (, $(shell which protoc)) rm -rf $$PROTOC_TMP_DIR ;\ } endif -PROTOC=$(shell which protoc) +PROTOC=$(shell which protoc) # find or download controller-gen # download controller-gen if necessary diff --git a/apis/net/v1alpha1/tunnelEndpointClient.go b/apis/net/v1alpha1/tunnelEndpointClient.go index dbaf727cfe..ad45b294e5 100644 --- a/apis/net/v1alpha1/tunnelEndpointClient.go +++ b/apis/net/v1alpha1/tunnelEndpointClient.go @@ -12,7 +12,7 @@ import ( crdclient "github.com/liqotech/liqo/pkg/crdClient" ) -// create a client for TunnelEndpoint CR using a provided kubeconfig. +// CreateTunnelEndpointClient creates a client for TunnelEndpoint CR using a provided kubeconfig. func CreateTunnelEndpointClient(kubeconfig string) (*crdclient.CRDClient, error) { var config *rest.Config var err error diff --git a/apis/net/v1alpha1/tunnel_endpoint_types.go b/apis/net/v1alpha1/tunnel_endpoint_types.go index 0cbbf115ad..e2964a7e74 100644 --- a/apis/net/v1alpha1/tunnel_endpoint_types.go +++ b/apis/net/v1alpha1/tunnel_endpoint_types.go @@ -67,20 +67,28 @@ type TunnelEndpointStatus struct { LocalEndpointIP string `json:"localTunnelPublicIP,omitempty"` TunnelIFaceIndex int `json:"tunnelIFaceIndex,omitempty"` TunnelIFaceName string `json:"tunnelIFaceName,omitempty"` + VethIFaceIndex int `json:"vethIFaceIndex,omitempty"` + VethIFaceName string `json:"vethIFaceName,omitempty"` + GatewayIP string `json:"gatewayIP,omitempty"` Connection Connection `json:"connection,omitempty"` } +// Connection holds the configuration and status of a vpn tunnel connecting to remote cluster. type Connection struct { Status ConnectionStatus `json:"status,omitempty"` StatusMessage string `json:"statusMessage,omitempty"` PeerConfiguration map[string]string `json:"peerConfiguration,omitempty"` } +// ConnectionStatus type that describes the status of vpn connection with a remote cluster. type ConnectionStatus string const ( - Connected ConnectionStatus = "connected" - Connecting ConnectionStatus = "connecting" + // Connected used when the connection is up and running. + Connected ConnectionStatus = "connected" + // Connecting used as temporary status while waiting for the vpn tunnel to come up. + Connecting ConnectionStatus = "connecting" + // ConnectionError used to se the status in case of errors. ConnectionError ConnectionStatus = "error" ) diff --git a/deployments/liqo/crds/net.liqo.io_tunnelendpoints.yaml b/deployments/liqo/crds/net.liqo.io_tunnelendpoints.yaml index 521ec5bcb7..58edf4d0b3 100644 --- a/deployments/liqo/crds/net.liqo.io_tunnelendpoints.yaml +++ b/deployments/liqo/crds/net.liqo.io_tunnelendpoints.yaml @@ -80,16 +80,22 @@ spec: description: TunnelEndpointStatus defines the observed state of TunnelEndpoint. properties: connection: + description: Connection holds the configuration and status of a vpn + tunnel connecting to remote cluster. properties: peerConfiguration: additionalProperties: type: string type: object status: + description: ConnectionStatus type that describes the status of + vpn connection with a remote cluster. type: string statusMessage: type: string type: object + gatewayIP: + type: string localExternalCIDR: description: ExternalCIDR of local cluster. type: string @@ -123,6 +129,10 @@ spec: type: integer tunnelIFaceName: type: string + vethIFaceIndex: + type: integer + vethIFaceName: + type: string type: object type: object served: true diff --git a/go.sum b/go.sum index 0c750fdc6a..5e10017282 100644 --- a/go.sum +++ b/go.sum @@ -65,6 +65,7 @@ github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6L github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20200415212048-7901bc822317/go.mod h1:DF8FZRxMHMGv/vP2lQP6h+dYzzjpuRn24VeRiYn3qjQ= @@ -248,6 +249,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= @@ -298,6 +300,7 @@ github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaB github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elazarl/goproxy v0.0.0-20190911111923-ecfe977594f1 h1:yY9rWGoXv1U5pl4gxqlULARMQD7x0QG85lqEXTWysik= github.com/elazarl/goproxy v0.0.0-20190911111923-ecfe977594f1/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= @@ -1057,6 +1060,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449 h1:xUIPaMhvROX9dhPvRCenIJtU78+lbEenGbgqB5hfHCQ= golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1425,6 +1429,7 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= inet.af/netaddr v0.0.0-20210313195008-843b4240e319 h1:cSGjHEjS/Nu6pts6yZfSYsRqcfW5ieqSrQI+XcZQObM= inet.af/netaddr v0.0.0-20210313195008-843b4240e319/go.mod h1:I2i9ONCXRZDnG1+7O8fSuYzjcPxHQXrIfzD/IkR87x4= diff --git a/pkg/liqonet/routing/common.go b/pkg/liqonet/routing/common.go new file mode 100644 index 0000000000..abcb2bd2ee --- /dev/null +++ b/pkg/liqonet/routing/common.go @@ -0,0 +1,308 @@ +package routing + +import ( + "errors" + "fmt" + "net" + "reflect" + + "github.com/liqotech/liqo/apis/net/v1alpha1" + "github.com/liqotech/liqo/pkg/liqonet" + + "github.com/vishvananda/netlink" + "golang.org/x/sys/unix" + "k8s.io/klog/v2" +) + +var ( + routingTableID = 18952 +) + +// WrongParameter it is returned when parameters passed to a function are not correct. +type WrongParameter struct { + Type string + Text string +} + +func (wp *WrongParameter) Error() string { + return "wrong " + wp.Type + " parameters: " + wp.Text +} + +// ParseIPError it is returned when net.ParseIP() fails to parse and ip address. +type ParseIPError struct { + Type string + IPToBeParsed string +} + +func (pie *ParseIPError) Error() string { + return "please check that the IP address is in che correct format" + pie.Type + ": " + pie.IPToBeParsed +} + +func addRoute(dstNet, gwIP string, iFaceIndex, tableID int) (bool, error) { + var route *netlink.Route + var gatewayIP net.IP + // Convert destination in *net.IPNet. + _, destinationNet, err := net.ParseCIDR(dstNet) + if err != nil { + return false, err + } + // If gwIP is not set then skip this section. + if gwIP != "" { + gatewayIP, err = parseIP(gwIP) + if err != nil { + return false, err + } + } + route = &netlink.Route{ + Table: tableID, + Dst: destinationNet, + Gw: gatewayIP, + LinkIndex: iFaceIndex, + } + // Check if already exists a route for the given destination. + routes, err := netlink.RouteListFiltered(netlink.FAMILY_V4, route, netlink.RT_FILTER_TABLE|netlink.RT_FILTER_DST) + if err != nil { + return false, err + } + // For the given destination there should be one and only one route on the given routing table. Keep in mind that the routing + // table is managed by us. No other processes should mess with it. + if len(routes) == 1 { + r := routes[0] + // Check if the existing rule is equal to the one that we want to configure. + if reflect.DeepEqual(r.Gw, gatewayIP.To4()) && r.LinkIndex == iFaceIndex { + klog.V(5).Infof("route {%s} already exists", route.String()) + return false, nil + } + // Otherwise remove the outdated route. + if err := netlink.RouteDel(&r); err != nil { + return false, err + } + } + klog.V(5).Infof("inserting route {%s}", route.String()) + if err := netlink.RouteAdd(route); err != nil { + return false, err + } + return true, nil +} + +func delRoute(dstNet, gwIP string, iFaceIndex, tableID int) (bool, error) { + var route *netlink.Route + var gatewayIP net.IP + // Convert destination in *net.IPNet. + _, destinationNet, err := net.ParseCIDR(dstNet) + if err != nil { + return false, err + } + // If gwIP is not set then skip this section. + if gwIP != "" { + gatewayIP, err = parseIP(gwIP) + if err != nil { + return false, err + } + } + route = &netlink.Route{ + Table: tableID, + Dst: destinationNet, + Gw: gatewayIP, + LinkIndex: iFaceIndex, + } + // Try to remove all the routes for current dstNet. + klog.V(5).Infof("deleting route {%s}", route.String()) + err = netlink.RouteDel(route) + if err != nil { + if errors.Is(err, unix.ESRCH) { + // It means the route does not exist so we are done. + return false, nil + } + return false, err + } + return true, nil +} + +func flushRoutesForRoutingTable(tableID int) error { + // First we list all the routes contained in the routing table. + route := &netlink.Route{ + Table: tableID, + } + routes, err := netlink.RouteListFiltered(netlink.FAMILY_V4, route, netlink.RT_FILTER_TABLE) + if err != nil { + return err + } + // Delete all the listed rules. + for i := range routes { + klog.V(5).Infof("deleting route {%s}", routes[i].String()) + if err := netlink.RouteDel(&routes[i]); err != nil { + return err + } + } + return nil +} + +func addPolicyRoutingRule(fromSubnet, toSubnet string, tableID int) (bool, error) { + var destinationNet, sourceNet *net.IPNet + var err error + + // Check that at least one between source and destination networks are defined. + if sourceNet, destinationNet, err = validatePolicyRoutingRulesParameters(fromSubnet, toSubnet); err != nil { + return false, err + } + // Get existing rules. + rules, err := netlink.RuleList(netlink.FAMILY_V4) + if err != nil { + klog.Errorf("an error occurred while listing the policy routing rules: %v", err) + return false, err + } + // Check if the rule already exists + for i := range rules { + if reflect.DeepEqual(destinationNet, rules[i].Dst) && reflect.DeepEqual(sourceNet, rules[i].Src) && rules[i].Table == tableID { + klog.V(5).Infof("rule %s already exists", rules[i].String()) + return false, nil + } + } + // Remove all the rules which has same destination and source networks. + for i := range rules { + if reflect.DeepEqual(destinationNet, rules[i].Dst) && reflect.DeepEqual(sourceNet, rules[i].Src) { + if err := netlink.RuleDel(&rules[i]); err != nil { + return false, err + } + } + } + rule := netlink.NewRule() + rule.Table = tableID + rule.Src = sourceNet + rule.Dst = destinationNet + klog.V(5).Infof("inserting policy routing rule {%s}", rule.String()) + if err := netlink.RuleAdd(rule); err != nil { + return false, err + } + return true, nil +} + +func delPolicyRoutingRule(fromSubnet, toSubnet string, tableID int) (bool, error) { + var destinationNet, sourceNet *net.IPNet + var err error + + // Check that at least one between source and destination networks are defined. + if sourceNet, destinationNet, err = validatePolicyRoutingRulesParameters(fromSubnet, toSubnet); err != nil { + return false, err + } + // Get existing rules. + rules, err := netlink.RuleList(netlink.FAMILY_V4) + if err != nil { + klog.Errorf("an error occurred while listing the policy routing rules: %v", err) + return false, err + } + // Check if the rule already exists. + for i := range rules { + if reflect.DeepEqual(destinationNet, rules[i].Dst) && reflect.DeepEqual(sourceNet, rules[i].Src) && rules[i].Table == tableID { + klog.V(5).Infof("removing policy routing rule {%s}", rules[i].String()) + if err := netlink.RuleDel(&rules[i]); err != nil && !errors.Is(err, unix.ESRCH) { + return false, err + } + return true, nil + } + } + return false, nil +} + +func flushRulesForRoutingTable(routingTableID int) error { + // First we list all the policy routing rules. + rules, err := netlink.RuleList(netlink.FAMILY_V4) + if err != nil { + return err + } + // Delete all the listed rules that refer to the given routing table. + for i := range rules { + // Skip the rules not referring to the given routing table. + if rules[i].Table != routingTableID { + continue + } + klog.V(5).Infof("deleting rules {%s}", rules[i].String()) + if err := netlink.RuleDel(&rules[i]); err != nil { + return err + } + } + return nil +} + +func getRouteConfig(tep *v1alpha1.TunnelEndpoint, podIP string) (dstNet, gatewayIP string, iFaceIndex int, err error) { + _, dstNet = liqonet.GetPodCIDRS(tep) + // Check if we are running on the same host as the gateway pod. + if tep.Status.GatewayIP != podIP { + // If the pod is not running on the same host then set the IP address of the Gateway as next hop. + gatewayIP = tep.Status.GatewayIP + // Get the iFace index for the IP address of the Gateway pod. + iFaceIndex, err = getIFaceIndexForIP(gatewayIP) + if err != nil { + return dstNet, gatewayIP, iFaceIndex, err + } + } else { + // Running on the same host as the Gateway then set the index of the veth device living on the same network namespace. + iFaceIndex = tep.Status.VethIFaceIndex + } + return dstNet, gatewayIP, iFaceIndex, err +} + +func getIFaceIndexForIP(ipAddress string) (int, error) { + // Convert the given IP address from string to net.IP format + ip := net.ParseIP(ipAddress) + if ip == nil { + return 0, &ParseIPError{ + Type: "IP address", + IPToBeParsed: ipAddress, + } + } + routes, err := netlink.RouteList(nil, netlink.FAMILY_V4) + if err != nil { + return 0, err + } + // Find the route whose destination contains our IP address. + for i := range routes { + // Skip routes whose Dst field is nil + if routes[i].Dst == nil { + continue + } + if routes[i].Dst.Contains(ip) { + return routes[i].LinkIndex, nil + } + } + return 0, fmt.Errorf("no route found for IP address %s", ipAddress) +} + +func validatePolicyRoutingRulesParameters(fromSubnet, toSubnet string) (sourceNet, destinationNet *net.IPNet, err error) { + // Check that at least one between source and destination networks are defined. + if fromSubnet == "" && toSubnet == "" { + return nil, nil, &WrongParameter{ + Type: "input", + Text: "at least one between fromSubnet and toSubnet has to be not empty", + } + } + // If toSubnet is empty string than do not parse it. + if toSubnet != "" { + // Convert destination network in *net.IPNet. + _, destinationNet, err = net.ParseCIDR(toSubnet) + if err != nil { + return nil, nil, err + } + } + // If fromSubnet is empty string than do not parse it. + if fromSubnet != "" { + // Convert source network in *net.IPNet. + _, sourceNet, err = net.ParseCIDR(fromSubnet) + if err != nil { + return nil, nil, err + } + } + return sourceNet, destinationNet, err +} + +func parseIP(ip string) (net.IP, error) { + address := net.ParseIP(ip) + if address == nil { + return address, &ParseIPError{ + Type: "IP address", + IPToBeParsed: ip, + } + } + return address, nil +} diff --git a/pkg/liqonet/routing/common_test.go b/pkg/liqonet/routing/common_test.go new file mode 100644 index 0000000000..819fc5a440 --- /dev/null +++ b/pkg/liqonet/routing/common_test.go @@ -0,0 +1,489 @@ +package routing + +import ( + "fmt" + "net" + + netv1alpha1 "github.com/liqotech/liqo/apis/net/v1alpha1" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/vishvananda/netlink" +) + +var ( + dstNetCorrect = "10.244.0.0/16" + srcNetCorrect = "10.200.0.0/16" + gwIPCorrect = "10.0.0.5" + dstNetWrong = "10.244.0.0.16" + srcNetWrong = "10.200.00/16" + gwIPWrong = "10.00.5" + notReachableIP = "10.100.1.1" + route, existingRoute, existingRouteGW *netlink.Route + existingRuleTo, existingRuleFrom *netlink.Rule + destinationNet *net.IPNet + gatewayIP net.IP + // Only the fields required for testing purposes are set. + tep = netv1alpha1.TunnelEndpoint{ + Status: netv1alpha1.TunnelEndpointStatus{ + LocalNATPodCIDR: "10.150.0.0/16", + RemoteNATPodCIDR: "10.250.0.0/16", + VethIFaceIndex: 10, + GatewayIP: gwIPCorrect, + }} +) + +var _ = Describe("Common", func() { + BeforeEach(func() { + var err error + // Parse dstNetCorrect. + _, destinationNet, err = net.ParseCIDR(dstNetCorrect) + Expect(err).NotTo(HaveOccurred()) + route = &netlink.Route{Dst: destinationNet, Table: routingTableID} + // Parse gwIPCorrect. + gatewayIP = net.ParseIP(gwIPCorrect) + Expect(gatewayIP).ShouldNot(BeNil()) + }) + + Describe("adding new route", func() { + Context("when input parameters are not in the correct format", func() { + It("should return error on wrong destination net", func() { + added, err := addRoute(dstNetWrong, gwIPCorrect, dummylink1.Attrs().Index, routingTableID) + Expect(added).Should(Equal(false)) + Expect(err).Should(Equal(&net.ParseError{Type: "CIDR address", Text: dstNetWrong})) + }) + + It("should return error on wrong gateway IP address", func() { + added, err := addRoute(dstNetCorrect, gwIPWrong, dummylink1.Attrs().Index, routingTableID) + Expect(added).Should(Equal(false)) + Expect(err).Should(Equal(&ParseIPError{Type: "IP address", IPToBeParsed: gwIPWrong})) + }) + }) + + Context("when an error occurred while adding a route", func() { + It("should return an error on non existing link", func() { + added, err := addRoute(dstNetCorrect, gwIPWrong, 0, routingTableID) + Expect(added).Should(Equal(false)) + Expect(err).To(HaveOccurred()) + }) + }) + + Context("when route does not exist and we want to add it", func() { + It("no gatewayIP, should return true and nil", func() { + added, err := addRoute(dstNetCorrect, "", dummylink1.Attrs().Index, routingTableID) + Expect(added).Should(Equal(true)) + Expect(err).NotTo(HaveOccurred()) + // Get the route and check it has the right parameters + routes, err := netlink.RouteListFiltered(netlink.FAMILY_V4, route, netlink.RT_FILTER_DST|netlink.RT_FILTER_TABLE) + Expect(err).NotTo(HaveOccurred()) + Expect(routes[0].Gw).Should(BeNil()) + }) + + It("with gatewayIP, should return true and nil", func() { + added, err := addRoute(dstNetCorrect, gwIPCorrect, dummylink1.Attrs().Index, routingTableID) + Expect(added).Should(Equal(true)) + Expect(err).NotTo(HaveOccurred()) + // Get the route and check it has the right parameters + routes, err := netlink.RouteListFiltered(netlink.FAMILY_V4, route, netlink.RT_FILTER_DST|netlink.RT_FILTER_TABLE) + Expect(err).NotTo(HaveOccurred()) + Expect(routes[0].Gw).Should(Equal(gatewayIP.To4())) + }) + }) + + Context("when route does exist and we want to add it", func() { + JustBeforeEach(func() { + setUpRoutes() + }) + + JustAfterEach(func() { + tearDownRoutes() + }) + + It("should return false and nil", func() { + // Add existing route with GW. + added, err := addRoute(existingRouteGW.Dst.String(), existingRouteGW.Gw.String(), dummylink1.Attrs().Index, routingTableID) + Expect(added).Should(Equal(false)) + Expect(err).NotTo(HaveOccurred()) + + // Add existing route without GW. + added, err = addRoute(existingRoute.Dst.String(), "", dummylink1.Attrs().Index, routingTableID) + Expect(added).Should(Equal(false)) + Expect(err).NotTo(HaveOccurred()) + }) + + It("update gateway of existing route: should return true and nil", func() { + // Update route with GW + added, err := addRoute(existingRouteGW.Dst.String(), "", dummylink1.Attrs().Index, routingTableID) + Expect(added).Should(Equal(true)) + Expect(err).NotTo(HaveOccurred()) + // Get the route and check it has the right parameters + routes, err := netlink.RouteListFiltered(netlink.FAMILY_V4, existingRouteGW, netlink.RT_FILTER_DST|netlink.RT_FILTER_TABLE) + Expect(err).NotTo(HaveOccurred()) + Expect(routes[0].Gw).Should(BeNil()) + + // Update route without GW + added, err = addRoute(existingRoute.Dst.String(), gwIPCorrect, dummylink1.Attrs().Index, routingTableID) + Expect(added).Should(Equal(true)) + Expect(err).NotTo(HaveOccurred()) + // Get the route and check it has the right parameters + routes, err = netlink.RouteListFiltered(netlink.FAMILY_V4, existingRoute, netlink.RT_FILTER_DST|netlink.RT_FILTER_TABLE) + Expect(err).NotTo(HaveOccurred()) + Expect(routes[0].Gw.String()).Should(Equal(gwIPCorrect)) + }) + + It("update link index of existing route: should return true and nil", func() { + // Update route with GW + added, err := addRoute(existingRouteGW.Dst.String(), existingRouteGW.Gw.String(), dummyLink2.Attrs().Index, routingTableID) + Expect(added).Should(Equal(true)) + Expect(err).NotTo(HaveOccurred()) + // Get the route and check it has the right parameters + routes, err := netlink.RouteListFiltered(netlink.FAMILY_V4, existingRouteGW, netlink.RT_FILTER_DST|netlink.RT_FILTER_TABLE) + Expect(err).NotTo(HaveOccurred()) + Expect(routes[0].LinkIndex).Should(BeNumerically("==", dummyLink2.Attrs().Index)) + + // Update route without GW + added, err = addRoute(existingRoute.Dst.String(), gwIPCorrect, dummyLink2.Attrs().Index, routingTableID) + Expect(added).Should(Equal(true)) + Expect(err).NotTo(HaveOccurred()) + // Get the route and check it has the right parameters + routes, err = netlink.RouteListFiltered(netlink.FAMILY_V4, existingRoute, netlink.RT_FILTER_DST|netlink.RT_FILTER_TABLE) + Expect(err).NotTo(HaveOccurred()) + Expect(routes[0].LinkIndex).Should(BeNumerically("==", dummyLink2.Attrs().Index)) + Expect(routes[0].Gw.String()).Should(Equal(gwIPCorrect)) + }) + }) + }) + + Describe("deleting an existing route", func() { + Context("when input parameters are not in the correct format", func() { + It("should return error on wrong destination net", func() { + removed, err := delRoute(dstNetWrong, gwIPCorrect, dummylink1.Attrs().Index, routingTableID) + Expect(removed).Should(Equal(false)) + Expect(err).Should(Equal(&net.ParseError{Type: "CIDR address", Text: dstNetWrong})) + }) + + It("should return error on wrong gateway IP address", func() { + removed, err := delRoute(dstNetCorrect, gwIPWrong, dummylink1.Attrs().Index, routingTableID) + Expect(removed).Should(Equal(false)) + Expect(err).Should(Equal(&ParseIPError{Type: "IP address", IPToBeParsed: gwIPWrong})) + }) + }) + + Context("when an error occurred while deleting a route", func() { + It("should return an error on non existing link", func() { + added, err := delRoute(dstNetCorrect, gwIPWrong, 0, routingTableID) + Expect(added).Should(Equal(false)) + Expect(err).To(HaveOccurred()) + }) + }) + + Context("when route does not exist and we want to delete it", func() { + It("no gatewayIP, should return false and nil", func() { + removed, err := delRoute(dstNetCorrect, "", dummylink1.Attrs().Index, routingTableID) + Expect(removed).Should(Equal(false)) + Expect(err).NotTo(HaveOccurred()) + }) + + It("with gatewayIP, should return false and nil", func() { + removed, err := delRoute(dstNetCorrect, gwIPCorrect, dummylink1.Attrs().Index, routingTableID) + Expect(removed).Should(Equal(false)) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("when route does exist and we want to delete it", func() { + JustBeforeEach(func() { + setUpRoutes() + }) + + JustAfterEach(func() { + tearDownRoutes() + }) + + It("with gateway, should return true and nil", func() { + // Delete existing route with GW. + removed, err := delRoute(existingRouteGW.Dst.String(), existingRouteGW.Gw.String(), dummylink1.Attrs().Index, routingTableID) + Expect(removed).Should(Equal(true)) + Expect(err).NotTo(HaveOccurred()) + // Expecting no routes exist for the given destination. + routes, err := netlink.RouteListFiltered(netlink.FAMILY_V4, existingRouteGW, netlink.RT_FILTER_DST|netlink.RT_FILTER_TABLE) + Expect(len(routes)).Should(BeNumerically("==", 0)) + Expect(err).NotTo(HaveOccurred()) + }) + + It("without gateway, should return true and nil", func() { + // Del existing route without GW. + removed, err := delRoute(existingRoute.Dst.String(), "", dummylink1.Attrs().Index, routingTableID) + Expect(removed).Should(Equal(true)) + Expect(err).NotTo(HaveOccurred()) + // Expecting no routes exist for the given destination. + routes, err := netlink.RouteListFiltered(netlink.FAMILY_V4, existingRoute, netlink.RT_FILTER_DST|netlink.RT_FILTER_TABLE) + Expect(len(routes)).Should(BeNumerically("==", 0)) + Expect(err).NotTo(HaveOccurred()) + }) + }) + }) + + Describe("flushing custom routing table", func() { + JustBeforeEach(func() { + setUpRoutes() + }) + + JustAfterEach(func() { + tearDownRoutes() + }) + + It("should remove all the routes from the custom table", func() { + err := flushRoutesForRoutingTable(routingTableID) + Expect(err).NotTo(HaveOccurred()) + // Check that the routing table is empty + routes, err := netlink.RouteListFiltered(netlink.FAMILY_V4, existingRouteGW, netlink.RT_FILTER_TABLE) + Expect(err).NotTo(HaveOccurred()) + Expect(len(routes)).Should(BeNumerically("==", 0)) + }) + }) + + Describe("adding new policy routing rule", func() { + AfterEach(func() { + tearDownRules() + }) + + Context("when input parameters are not in the correct format", func() { + It("should return error on wrong destination net", func() { + added, err := addPolicyRoutingRule(srcNetCorrect, dstNetWrong, routingTableID) + Expect(added).Should(Equal(false)) + Expect(err).Should(Equal(&net.ParseError{Type: "CIDR address", Text: dstNetWrong})) + }) + + It("should return error on wrong source net", func() { + added, err := addPolicyRoutingRule(srcNetWrong, dstNetCorrect, routingTableID) + Expect(added).Should(Equal(false)) + Expect(err).Should(Equal(&net.ParseError{Type: "CIDR address", Text: srcNetWrong})) + }) + + It("should return error if both subnets are empty", func() { + added, err := addPolicyRoutingRule("", "", routingTableID) + Expect(added).Should(Equal(false)) + Expect(err).Should(Equal(&WrongParameter{ + Type: "input", + Text: "at least one between fromSubnet and toSubnet has to be not empty", + })) + }) + }) + }) + + Context("when policy routing rule does not exist and we want to add it", func() { + It("only to destination net, should return true and nil", func() { + added, err := addPolicyRoutingRule("", dstNetCorrect, routingTableID) + Expect(added).Should(Equal(true)) + Expect(err).NotTo(HaveOccurred()) + // Get the rule and check it has the right parameters. + rule, err := getRule("", dstNetCorrect, routingTableID) + Expect(err).NotTo(HaveOccurred()) + Expect(rule.Dst.String()).Should(Equal(dstNetCorrect)) + Expect(rule.Src).Should(BeNil()) + }) + + It("only to source net, should return true and nil", func() { + added, err := addPolicyRoutingRule(srcNetCorrect, "", routingTableID) + Expect(added).Should(Equal(true)) + Expect(err).NotTo(HaveOccurred()) + // Get the rule and check it has the right parameters. + rule, err := getRule(srcNetCorrect, "", routingTableID) + Expect(err).NotTo(HaveOccurred()) + Expect(rule.Src.String()).Should(Equal(srcNetCorrect)) + Expect(rule.Dst).Should(BeNil()) + }) + + It("both source and destination net, should return true and nil", func() { + added, err := addPolicyRoutingRule(srcNetCorrect, dstNetCorrect, routingTableID) + Expect(added).Should(Equal(true)) + Expect(err).NotTo(HaveOccurred()) + // Get the rule and check it has the right parameters. + rule, err := getRule(srcNetCorrect, dstNetCorrect, routingTableID) + Expect(err).NotTo(HaveOccurred()) + Expect(rule.Src.String()).Should(Equal(srcNetCorrect)) + Expect(rule.Dst.String()).Should(Equal(dstNetCorrect)) + }) + }) + + Context("when policy routing rule does exist and we want to add it", func() { + BeforeEach(func() { + setUpRules() + }) + + AfterEach(func() { + tearDownRules() + }) + It("rule already exists: should return false and nil", func() { + added, err := addPolicyRoutingRule(existingRuleFrom.Src.String(), "", routingTableID) + Expect(added).Should(Equal(false)) + Expect(err).NotTo(HaveOccurred()) + // Get the rule and check it has the right parameters. + rule, err := getRule(existingRuleFrom.Src.String(), "", routingTableID) + Expect(err).NotTo(HaveOccurred()) + Expect(rule.Src.String()).Should(Equal(existingRuleFrom.Src.String())) + Expect(rule.Dst).Should(BeNil()) + }) + + It("update routing table ID: should return true and nil", func() { + routingTable := 12345 + added, err := addPolicyRoutingRule(existingRuleFrom.Src.String(), "", routingTable) + Expect(added).Should(Equal(true)) + Expect(err).NotTo(HaveOccurred()) + // Get the rule and check it has the right parameters. + rule, err := getRule(existingRuleFrom.Src.String(), "", routingTable) + Expect(err).NotTo(HaveOccurred()) + Expect(rule.Src.String()).Should(Equal(existingRuleFrom.Src.String())) + Expect(rule.Dst).Should(BeNil()) + }) + }) + + Describe("deleting an existing policy routing rule", func() { + Context("when input parameters are not in the correct format", func() { + It("should return error on wrong destination net", func() { + removed, err := delPolicyRoutingRule(dstNetWrong, "", routingTableID) + Expect(removed).Should(Equal(false)) + Expect(err).Should(Equal(&net.ParseError{Type: "CIDR address", Text: dstNetWrong})) + }) + + It("should return error on wrong source net", func() { + removed, err := delPolicyRoutingRule("", srcNetWrong, routingTableID) + Expect(removed).Should(Equal(false)) + Expect(err).Should(Equal(&net.ParseError{Type: "CIDR address", Text: srcNetWrong})) + }) + + It("should return error if both subnets are empty", func() { + removed, err := delPolicyRoutingRule("", "", routingTableID) + Expect(removed).Should(Equal(false)) + Expect(err).Should(Equal(&WrongParameter{ + Type: "input", + Text: "at least one between fromSubnet and toSubnet has to be not empty", + })) + }) + }) + + Context("when policy routing rule does not exist and we want to delete it", func() { + It("with destination net, should return false and nil", func() { + removed, err := delPolicyRoutingRule(dstNetCorrect, "", routingTableID) + Expect(removed).Should(Equal(false)) + Expect(err).NotTo(HaveOccurred()) + }) + + It("with source net, should return false and nil", func() { + removed, err := delPolicyRoutingRule("", srcNetCorrect, routingTableID) + Expect(removed).Should(Equal(false)) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("when policy routing rule does exist and we want to delete it", func() { + JustBeforeEach(func() { + setUpRules() + }) + + JustAfterEach(func() { + tearDownRules() + }) + + It("with destination net, should return false and nil", func() { + removed, err := delPolicyRoutingRule("", existingRuleTo.Dst.String(), routingTableID) + Expect(removed).Should(Equal(true)) + Expect(err).NotTo(HaveOccurred()) + rule, err := getRule("", existingRuleTo.Dst.String(), routingTableID) + Expect(rule).To(BeNil()) + Expect(err).NotTo(BeNil()) + Expect(err.Error()).Should(Equal("rule not found")) + }) + + It("with source net, should return false and nil", func() { + removed, err := delPolicyRoutingRule(existingRuleFrom.Src.String(), "", routingTableID) + Expect(removed).Should(Equal(true)) + Expect(err).NotTo(HaveOccurred()) + rule, err := getRule("", existingRuleFrom.Src.String(), routingTableID) + Expect(rule).To(BeNil()) + Expect(err).NotTo(BeNil()) + Expect(err.Error()).Should(Equal("rule not found")) + }) + }) + }) + + Describe("flushing policy routing rules for a custom routing table", func() { + JustBeforeEach(func() { + setUpRules() + }) + + JustAfterEach(func() { + tearDownRules() + }) + + It("should remove all the policy routing rules referencing the custom table", func() { + err := flushRulesForRoutingTable(routingTableID) + Expect(err).NotTo(HaveOccurred()) + // Check that no policy routing rules reference the custom routing table. + exists, err := existsRuleForRoutingTable(routingTableID) + Expect(err).NotTo(HaveOccurred()) + Expect(exists).Should(BeFalse()) + }) + }) + + Describe("getting interface index from which a given IP address is reachable", func() { + + Context("when input parameters are not in the correct format", func() { + It("should return error on malformed IP address", func() { + index, err := getIFaceIndexForIP(gwIPWrong) + Expect(index).To(BeZero()) + Expect(err).Should(Equal(&ParseIPError{Type: "IP address", IPToBeParsed: gwIPWrong})) + }) + }) + + Context("when there is a route for the given IP address", func() { + It("should return nil and a correct link index", func() { + index, err := getIFaceIndexForIP(gwIPCorrect) + Expect(err).NotTo(HaveOccurred()) + Expect(index).Should(BeNumerically("==", dummylink1.Attrs().Index)) + }) + }) + + Context("when there is no route for the given IP address", func() { + It("should return error and 0 as link index", func() { + index, err := getIFaceIndexForIP(notReachableIP) + Expect(err).To(HaveOccurred()) + Expect(index).Should(BeZero()) + Expect(err).Should(Equal(fmt.Errorf("no route found for IP address %s", notReachableIP))) + }) + }) + }) + + Describe("getting routing information from a tunnelendpoint instance", func() { + + Context("when the the operator has same IP address as the Gateway pod", func() { + It("should return no error", func() { + dstNet, gwIP, iFaceIndex, err := getRouteConfig(&tep, gwIPCorrect) + Expect(err).NotTo(HaveOccurred()) + Expect(dstNet).Should(Equal(tep.Status.RemoteNATPodCIDR)) + Expect(gwIP).Should(Equal("")) + Expect(iFaceIndex).Should(BeNumerically("==", tep.Status.VethIFaceIndex)) + }) + }) + + Context("when the the operator is not running on same node as the Gateway pod", func() { + It("should return nil and a link index of the interface through which the Gateway is reachable", func() { + dstNet, gwIP, iFaceIndex, err := getRouteConfig(&tep, notReachableIP) + Expect(err).NotTo(HaveOccurred()) + Expect(dstNet).Should(Equal(tep.Status.RemoteNATPodCIDR)) + Expect(gwIP).Should(Equal(gwIPCorrect)) + Expect(iFaceIndex).Should(BeNumerically("==", dummylink1.Attrs().Index)) + }) + }) + + Context("when the gateway IP address is not reachable", func() { + It("should return error", func() { + tep.Status.GatewayIP = notReachableIP + _, _, _, err := getRouteConfig(&tep, gwIPCorrect) + Expect(err).To(HaveOccurred()) + Expect(err).Should(Equal(fmt.Errorf("no route found for IP address %s", notReachableIP))) + }) + }) + }) +}) diff --git a/pkg/liqonet/routing/doc.go b/pkg/liqonet/routing/doc.go new file mode 100644 index 0000000000..5be13a02fa --- /dev/null +++ b/pkg/liqonet/routing/doc.go @@ -0,0 +1,4 @@ +// Package routing defines a common interface used to configure the routing tables and policy routing rules +// in order to reach the remote networks of the peering clusters. Based on the operator the routes are handled differently +// hence different implementation of the interfaces lives in this package. +package routing diff --git a/pkg/liqonet/routing/routing_suite_test.go b/pkg/liqonet/routing/routing_suite_test.go new file mode 100644 index 0000000000..0ac5b652d9 --- /dev/null +++ b/pkg/liqonet/routing/routing_suite_test.go @@ -0,0 +1,198 @@ +package routing + +import ( + "bytes" + "fmt" + "net" + "os/exec" + "reflect" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/vishvananda/netlink" + "k8s.io/klog/v2" +) + +var ( + ipAddress1 = "10.0.0.1/24" + ipAddress2 = "10.0.0.2/24" + dummylink1, dummyLink2 netlink.Link + iFacesNames = []string{"lioo-test-1", "liqo-test-2"} +) + +func TestRouting(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Routing Suite") +} + +var _ = BeforeSuite(func() { + setUpInterfaces() +}) + +var _ = AfterSuite(func() { + tearDownInterfaces() +}) + +func setUpInterfaces() { + var stdout, stderr bytes.Buffer + var err error + // First we create a dummy interfaces used to run the tests. + for _, iFace := range iFacesNames { + klog.Infof("creating dummy interface named {%s}", iFace) + cmd := exec.Command("ip", "link", "add", iFace, "type", "dummy") + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if err != nil { + outStr, errStr := stdout.String(), stderr.String() + fmt.Printf("out:\n%s\nerr:\n%s\n", outStr, errStr) + klog.Errorf("failed to add dummy interface {liqo-test}: %v", err) + } + Expect(err).NotTo(HaveOccurred()) + } + + // Get the dummy interface. + dummylink1, err = netlink.LinkByName(iFacesNames[0]) + Expect(err).NotTo(HaveOccurred()) + // Set the dummy interface up. + Expect(netlink.LinkSetUp(dummylink1)).NotTo(HaveOccurred()) + // Assign IP address to dummy interface + ip, network, err := net.ParseCIDR(ipAddress1) + Expect(err).NotTo(HaveOccurred()) + Expect(netlink.AddrAdd(dummylink1, &netlink.Addr{IPNet: &net.IPNet{ + IP: ip, + Mask: network.Mask, + }})).NotTo(HaveOccurred()) + // Get the dummy interface. + dummyLink2, err = netlink.LinkByName(iFacesNames[1]) + Expect(err).NotTo(HaveOccurred()) + // Set the dummy interface up. + Expect(netlink.LinkSetUp(dummyLink2)).NotTo(HaveOccurred()) + // Assign IP address to dummy interface + ip, network, err = net.ParseCIDR(ipAddress2) + Expect(err).NotTo(HaveOccurred()) + Expect(netlink.AddrAdd(dummyLink2, &netlink.Addr{IPNet: &net.IPNet{ + IP: ip, + Mask: network.Mask, + }})).NotTo(HaveOccurred()) +} + +func tearDownInterfaces() { + // Remove dummy interfaces. + for _, iFace := range iFacesNames { + dummyLink, err := netlink.LinkByName(iFace) + Expect(err).NotTo(HaveOccurred()) + Expect(netlink.LinkDel(dummyLink)).NotTo(HaveOccurred()) + } +} + +func setUpRoutes() { + dst1 := "10.10.0.0/16" + gw1 := "10.0.0.10" + dst2 := "10.11.0.0/24" + // Add route 1. + _, dstNet1, err := net.ParseCIDR(dst1) + Expect(err).Should(BeNil()) + gw := net.ParseIP(gw1) + Expect(gw).ShouldNot(BeNil()) + err = netlink.RouteAdd(&netlink.Route{Dst: dstNet1, Gw: gw, Table: routingTableID, LinkIndex: dummylink1.Attrs().Index}) + Expect(err).Should(BeNil()) + existingRouteGW = &netlink.Route{Dst: dstNet1, Table: routingTableID, Gw: gw} + // Add route 2. + _, dstNet2, err := net.ParseCIDR(dst2) + Expect(err).Should(BeNil()) + err = netlink.RouteAdd(&netlink.Route{Dst: dstNet2, Table: routingTableID, LinkIndex: dummylink1.Attrs().Index}) + Expect(err).Should(BeNil()) + existingRoute = &netlink.Route{Dst: dstNet2, Table: routingTableID} +} + +func tearDownRoutes() { + // Remove all routes on the custom routing table + routes, err := netlink.RouteListFiltered(netlink.FAMILY_V4, existingRouteGW, netlink.RT_FILTER_TABLE) + Expect(err).Should(BeNil()) + for i := range routes { + if routes[i].Table == routingTableID { + Expect(netlink.RouteDel(&routes[i])).Should(BeNil()) + } + } +} + +func getRule(fromSubnet, toSubnet string, tableID int) (*netlink.Rule, error) { + var destinationNet, sourceNet *net.IPNet + var err error + if toSubnet != "" { + // Convert destination network in *net.IPNet. + _, destinationNet, err = net.ParseCIDR(toSubnet) + if err != nil { + return nil, err + } + } + if fromSubnet != "" { + // Convert source network in *net.IPNet. + _, sourceNet, err = net.ParseCIDR(fromSubnet) + if err != nil { + return nil, err + } + } + // Get existing rules. + rules, err := netlink.RuleList(netlink.FAMILY_V4) + if err != nil { + klog.Errorf("an error occurred while listing the policy routing rules: %v", err) + return nil, err + } + for i := range rules { + if reflect.DeepEqual(destinationNet, rules[i].Dst) && reflect.DeepEqual(sourceNet, rules[i].Src) && rules[i].Table == tableID { + return &rules[i], nil + } + } + return nil, fmt.Errorf("rule not found") +} + +func existsRuleForRoutingTable(tableID int) (bool, error) { + // Get existing rules. + rules, err := netlink.RuleList(netlink.FAMILY_V4) + if err != nil { + klog.Errorf("an error occurred while listing the policy routing rules: %v", err) + return false, err + } + for i := range rules { + if rules[i].Table == tableID { + return true, nil + } + } + return false, nil +} + +func setUpRules() { + dst := "10.10.0.0/16" + src := "10.11.0.0/24" + // Add route 1. + _, dstNet, err := net.ParseCIDR(dst) + Expect(err).Should(BeNil()) + _, srcNet, err := net.ParseCIDR(src) + Expect(err).Should(BeNil()) + // Add rule from. + existingRuleFrom = netlink.NewRule() + existingRuleFrom.Table = routingTableID + existingRuleFrom.Src = srcNet + err = netlink.RuleAdd(existingRuleFrom) + Expect(err).Should(BeNil()) + // Add rule to. + existingRuleTo = netlink.NewRule() + existingRuleTo.Table = routingTableID + existingRuleTo.Dst = dstNet + err = netlink.RuleAdd(existingRuleTo) + Expect(err).Should(BeNil()) +} + +func tearDownRules() { + // Get existing rules. + rules, err := netlink.RuleList(netlink.FAMILY_V4) + Expect(err).Should(BeNil()) + for i := range rules { + if rules[i].Table == routingTableID || rules[i].Table == 12345 { + Expect(netlink.RuleDel(&rules[i])).Should(BeNil()) + } + } +}