multus-cni/pkg/netutils/netutils.go
Miguel Duarte Barroso fb31217e2c thick-plugin: refactor multus
Multus is refactored as a thick plugin, featuring 2 main components:
  - a server listening to a unix domain socket, running in a pod
  - a shim, a binary on the host that will send JSON requests built from
    its environment / stdin values to the aforementioned server.

The pod where the multus daemon is running must share the host's PID
namespace.

Signed-off-by: Miguel Duarte Barroso <mdbarroso@redhat.com>

react to maintainers review

Signed-off-by: Miguel Duarte Barroso <mdbarroso@redhat.com>

thick, deployment: update the daemonset spec

Signed-off-by: Miguel Duarte Barroso <mdbarroso@redhat.com>

thick, config: validate the cni config passed by the runtime

Without this patch, we're blindly trusting anything sent by the server.
This way, we assure the requests arriving at the multus controller are
valid before hand.

Signed-off-by: Miguel Duarte Barroso <mdbarroso@redhat.com>

thick: model client / server config

Also add a new command line parameter on the multus controller, pointing
it to the server configuration.

Signed-off-by: Miguel Duarte Barroso <mdbarroso@redhat.com>

SQUASH candidate, thick, config: cleanup the configuration

Signed-off-by: Miguel Duarte Barroso <mdbarroso@redhat.com>

multus: use args.args instead of an env variable

CNI is already filling the args structure; we should consume that
rather than rely on the environment variables.

Signed-off-by: Miguel Duarte Barroso <mdbarroso@redhat.com>

unit tests: remove weird tests that check an impossible scenario

Signed-off-by: Miguel Duarte Barroso <mdbarroso@redhat.com>

docs, thick: document the thick plugin variant

Signed-off-by: Miguel Duarte Barroso <mdbarroso@redhat.com>

thick, server, multus: re-use common types

Signed-off-by: Miguel Duarte Barroso <mdbarroso@redhat.com>
2022-04-06 00:34:52 +09:00

405 lines
11 KiB
Go

// Copyright (c) 2019 Multus Authors
//
// 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 netutils
import (
"encoding/json"
"fmt"
"io/ioutil"
"net"
"path/filepath"
"github.com/containernetworking/cni/libcni"
"github.com/containernetworking/plugins/pkg/ns"
"github.com/vishvananda/netlink"
"gopkg.in/k8snetworkplumbingwg/multus-cni.v3/pkg/logging"
)
// DeleteDefaultGW removes the default gateway from marked interfaces.
func DeleteDefaultGW(netnsPath string, ifName string) error {
netns, err := ns.GetNS(netnsPath)
if err != nil {
return logging.Errorf("DeleteDefaultGW: Error getting namespace %v", err)
}
defer netns.Close()
err = netns.Do(func(_ ns.NetNS) error {
var err error
link, _ := netlink.LinkByName(ifName)
routes, _ := netlink.RouteList(link, netlink.FAMILY_ALL)
for _, nlroute := range routes {
if nlroute.Dst == nil {
err = netlink.RouteDel(&nlroute)
}
}
return err
})
return err
}
// SetDefaultGW adds a default gateway on a specific interface
func SetDefaultGW(netnsPath string, ifName string, gateways []net.IP) error {
// This ensures we're acting within the net namespace for the pod.
netns, err := ns.GetNS(netnsPath)
if err != nil {
return logging.Errorf("SetDefaultGW: Error getting namespace %v", err)
}
defer netns.Close()
// Do this within the net namespace.
err = netns.Do(func(_ ns.NetNS) error {
var err error
// Pick up the link info as we need the index.
link, err := netlink.LinkByName(ifName)
if err != nil {
return logging.Errorf("SetDefaultGW: Error getting link %v", err)
}
// Cycle through all the desired gateways.
for _, gw := range gateways {
// Create a new route (note: dst is nil by default)
logging.Debugf("SetDefaultGW: Adding default route on %v (index: %v) to %v", ifName, link.Attrs().Index, gw)
newDefaultRoute := netlink.Route{
LinkIndex: link.Attrs().Index,
Gw: gw,
}
// Perform the creation of the default route....
err = netlink.RouteAdd(&newDefaultRoute)
if err != nil {
logging.Errorf("SetDefaultGW: Error adding route: %v", err)
}
}
return err
})
return err
}
// DeleteDefaultGWCache updates libcni cache to remove default gateway routes in result
func DeleteDefaultGWCache(cacheDir string, rt *libcni.RuntimeConf, netName string, ifName string, ipv4, ipv6 bool) error {
cacheFile := filepath.Join(cacheDir, "results", fmt.Sprintf("%s-%s-%s", netName, rt.ContainerID, rt.IfName))
cache, err := ioutil.ReadFile(cacheFile)
if err != nil {
return err
}
logging.Debugf("DeleteDefaultGWCache: update cache to delete GW from: %s", string(cache))
newCache, err := deleteDefaultGWCacheBytes(cache, ipv4, ipv6)
if err != nil {
return err
}
logging.Debugf("DeleteDefaultGWCache: update cache to delete GW: %s", string(newCache))
return ioutil.WriteFile(cacheFile, newCache, 0600)
}
func deleteDefaultGWCacheBytes(cacheFile []byte, ipv4, ipv6 bool) ([]byte, error) {
var cachedInfo map[string]interface{}
if err := json.Unmarshal(cacheFile, &cachedInfo); err != nil {
return nil, err
}
// try to get result
_, ok := cachedInfo["result"]
if !ok {
return nil, fmt.Errorf("cannot get result from cache")
}
resultJSON, ok := cachedInfo["result"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("wrong result type: %v", cachedInfo["result"])
}
newResult, err := deleteDefaultGWResult(resultJSON, ipv4, ipv6)
if err != nil {
return nil, err
}
cachedInfo["result"] = newResult
newCache, err := json.Marshal(cachedInfo)
if err != nil {
return nil, fmt.Errorf("failed to encode json: %v", err)
}
return newCache, nil
}
func deleteDefaultGWResultRoutes(routes []interface{}, dstGW string) ([]interface{}, error) {
for i, r := range routes {
route, ok := r.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("wrong route format: %v", r)
}
_, ok = route["dst"]
if ok {
dst, ok := route["dst"].(string)
if !ok {
return nil, fmt.Errorf("wrong dst format: %v", route["dst"])
}
if dst == dstGW {
routes = append(routes[:i], routes[i+1:]...)
}
}
}
return routes, nil
}
func deleteDefaultGWResult(result map[string]interface{}, ipv4, ipv6 bool) (map[string]interface{}, error) {
// try to get cniVersion from result
_, ok := result["cniVersion"]
if !ok {
// fallback to processing result for old cni version(0.1.0/0.2.0)
return deleteDefaultGWResult020(result, ipv4, ipv6)
}
cniVersion, ok := result["cniVersion"].(string)
if !ok {
return nil, fmt.Errorf("wrong cniVersion format: %v", result["cniVersion"])
}
if cniVersion == "0.1.0" || cniVersion == "0.2.0" {
// fallback to processing result for old cni version(0.1.0/0.2.0)
return deleteDefaultGWResult020(result, ipv4, ipv6)
}
if cniVersion != "0.3.0" && cniVersion != "0.3.1" && cniVersion != "0.4.0" && cniVersion != "1.0.0" {
return nil, fmt.Errorf("not supported version: %s", cniVersion)
}
_, ok = result["routes"]
if !ok {
// No route in result, hence we do nothing
return result, nil
}
routes, ok := result["routes"].([]interface{})
if !ok {
return nil, fmt.Errorf("wrong routes format: %v", result["routes"])
}
var err error
// delete IPv4 default routes
if ipv4 {
routes, err = deleteDefaultGWResultRoutes(routes, "0.0.0.0/0")
if err != nil {
return nil, err
}
}
if ipv6 {
routes, err = deleteDefaultGWResultRoutes(routes, "::0/0")
if err != nil {
return nil, err
}
}
result["routes"] = routes
return result, nil
}
func deleteDefaultGWResult020(result map[string]interface{}, ipv4, ipv6 bool) (map[string]interface{}, error) {
var err error
if ipv4 {
_, ok := result["ip4"]
if ok {
ip4, ok := result["ip4"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("wrong ip4 format: %v", result["ip4"])
}
_, ok = ip4["routes"]
if ok {
routes, ok := ip4["routes"].([]interface{})
if !ok {
return nil, fmt.Errorf("wrong ip4 routes format: %v", ip4["routes"])
}
routes, err = deleteDefaultGWResultRoutes(routes, "0.0.0.0/0")
if err != nil {
return nil, err
}
ip4["routes"] = routes
}
}
}
if ipv6 {
_, ok := result["ip6"]
if ok {
ip6, ok := result["ip6"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("wrong ip6 format: %v", result["ip6"])
}
_, ok = ip6["routes"]
if ok {
routes, ok := ip6["routes"].([]interface{})
if !ok {
return nil, fmt.Errorf("wrong ip6 routes format: %v", ip6["routes"])
}
routes, err = deleteDefaultGWResultRoutes(routes, "::0/0")
if err != nil {
return nil, err
}
ip6["routes"] = routes
}
}
}
return result, nil
}
// AddDefaultGWCache updates libcni cache to add default gateway result
func AddDefaultGWCache(cacheDir string, rt *libcni.RuntimeConf, netName string, ifName string, gw []net.IP) error {
cacheFile := filepath.Join(cacheDir, "results", fmt.Sprintf("%s-%s-%s", netName, rt.ContainerID, rt.IfName))
cache, err := ioutil.ReadFile(cacheFile)
if err != nil {
return err
}
logging.Debugf("AddDefaultGWCache: update cache to add GW from: %s", string(cache))
newCache, err := addDefaultGWCacheBytes(cache, gw)
if err != nil {
return err
}
logging.Debugf("AddDefaultGWCache: update cache to add GW: %s", string(newCache))
return ioutil.WriteFile(cacheFile, newCache, 0600)
}
func addDefaultGWCacheBytes(cacheFile []byte, gw []net.IP) ([]byte, error) {
var cachedInfo map[string]interface{}
if err := json.Unmarshal(cacheFile, &cachedInfo); err != nil {
return nil, err
}
// try to get result
_, ok := cachedInfo["result"]
if !ok {
return nil, fmt.Errorf("cannot get result from cache")
}
resultJSON, ok := cachedInfo["result"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("wrong result type: %v", cachedInfo["result"])
}
newResult, err := addDefaultGWResult(resultJSON, gw)
if err != nil {
return nil, err
}
cachedInfo["result"] = newResult
newCache, err := json.Marshal(cachedInfo)
if err != nil {
return nil, fmt.Errorf("failed to encode json: %v", err)
}
return newCache, nil
}
func addDefaultGWResult(result map[string]interface{}, gw []net.IP) (map[string]interface{}, error) {
// try to get cniVersion from result
_, ok := result["cniVersion"]
if !ok {
// fallback to processing result for old cni version(0.1.0/0.2.0)
return addDefaultGWResult020(result, gw)
}
cniVersion, ok := result["cniVersion"].(string)
if !ok {
return nil, fmt.Errorf("wrong cniVersion format: %v", result["cniVersion"])
}
if cniVersion == "0.1.0" || cniVersion == "0.2.0" {
// fallback to processing result for old cni version(0.1.0/0.2.0)
return addDefaultGWResult020(result, gw)
}
if cniVersion != "0.3.0" && cniVersion != "0.3.1" && cniVersion != "0.4.0" && cniVersion != "1.0.0" {
return nil, fmt.Errorf("not supported version: %s", cniVersion)
}
routes := []interface{}{}
_, ok = result["routes"]
if ok {
routes, ok = result["routes"].([]interface{})
if !ok {
return nil, fmt.Errorf("wrong routes format: %v", result["routes"])
}
}
for _, g := range gw {
dst := "0.0.0.0/0"
if g.To4() == nil {
dst = "::0/0"
}
routes = append(routes, map[string]string{
"dst": dst,
"gw": g.String(),
})
}
result["routes"] = routes
return result, nil
}
func addDefaultGWResult020(result map[string]interface{}, gw []net.IP) (map[string]interface{}, error) {
for _, g := range gw {
if g.To4() != nil {
_, ok := result["ip4"]
if ok {
ip4, ok := result["ip4"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("wrong ip4 format: %v", result["ip4"])
}
routes := []interface{}{}
_, ok = ip4["routes"]
if ok {
routes, ok = ip4["routes"].([]interface{})
if !ok {
return nil, fmt.Errorf("wrong ip4 routes format: %v", ip4["routes"])
}
}
ip4["routes"] = append(routes, map[string]string{
"dst": "0.0.0.0/0",
"gw": g.String(),
})
}
} else {
_, ok := result["ip6"]
if ok {
ip6, ok := result["ip6"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("wrong ip6 format: %v", result["ip4"])
}
routes := []interface{}{}
_, ok = ip6["routes"]
if ok {
routes, ok = ip6["routes"].([]interface{})
if !ok {
return nil, fmt.Errorf("wrong ip6 routes format: %v", ip6["routes"])
}
}
ip6["routes"] = append(routes, map[string]string{
"dst": "::/0",
"gw": g.String(),
})
}
}
}
return result, nil
}