netavark network interface

Implement a new network interface for netavark.
For now only bridge networking is supported.
The interface can create/list/inspect/remove networks. For setup and
teardown netavark will be invoked.

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
This commit is contained in:
Paul Holzinger 2021-10-14 10:33:18 +02:00
parent 12c62b92ff
commit eaae294628
No known key found for this signature in database
GPG Key ID: EB145DD938A3CAF2
26 changed files with 2546 additions and 55 deletions

View File

@ -14,6 +14,7 @@ import (
"time"
"github.com/containernetworking/cni/libcni"
internalutil "github.com/containers/podman/v3/libpod/network/internal/util"
"github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/podman/v3/libpod/network/util"
pkgutil "github.com/containers/podman/v3/pkg/util"
@ -156,10 +157,7 @@ func convertIPAMConfToNetwork(network *types.Network, ipam ipamConfig, confPath
return errors.Errorf("failed to parse gateway ip %s", ipam.Gateway)
}
// convert to 4 byte if ipv4
ipv4 := gateway.To4()
if ipv4 != nil {
gateway = ipv4
}
internalutil.NormalizeIP(&gateway)
} else if !network.Internal {
// only add a gateway address if the network is not internal
gateway, err = util.FirstIPInSubnet(sub)
@ -244,13 +242,13 @@ func (n *cniNetwork) createCNIConfigListFromNetwork(network *types.Network, writ
for k, v := range network.Options {
switch k {
case "mtu":
mtu, err = parseMTU(v)
mtu, err = internalutil.ParseMTU(v)
if err != nil {
return nil, "", err
}
case "vlan":
vlan, err = parseVlan(v)
vlan, err = internalutil.ParseVlan(v)
if err != nil {
return nil, "", err
}
@ -339,36 +337,6 @@ func (n *cniNetwork) createCNIConfigListFromNetwork(network *types.Network, writ
return config, cniPathName, nil
}
// parseMTU parses the mtu option
func parseMTU(mtu string) (int, error) {
if mtu == "" {
return 0, nil // default
}
m, err := strconv.Atoi(mtu)
if err != nil {
return 0, err
}
if m < 0 {
return 0, errors.Errorf("mtu %d is less than zero", m)
}
return m, nil
}
// parseVlan parses the vlan option
func parseVlan(vlan string) (int, error) {
if vlan == "" {
return 0, nil // default
}
v, err := strconv.Atoi(vlan)
if err != nil {
return 0, err
}
if v < 0 || v > 4094 {
return 0, errors.Errorf("vlan ID %d must be between 0 and 4094", v)
}
return v, nil
}
func convertSpecgenPortsToCNIPorts(ports []types.PortMapping) ([]cniPortMapEntry, error) {
cniPorts := make([]cniPortMapEntry, 0, len(ports))
for _, port := range ports {

View File

@ -9,7 +9,6 @@ import (
"github.com/containers/podman/v3/libpod/define"
internalutil "github.com/containers/podman/v3/libpod/network/internal/util"
"github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/podman/v3/libpod/network/util"
pkgutil "github.com/containers/podman/v3/pkg/util"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@ -42,6 +41,12 @@ func (n *cniNetwork) networkCreate(newNetwork types.Network, defaultNet bool) (*
newNetwork.Driver = types.DefaultNetworkDriver
}
// FIXME: Should we use a different type for network create without the ID field?
// the caller is not allowed to set a specific ID
if newNetwork.ID != "" {
return nil, errors.Wrap(define.ErrInvalidArg, "ID can not be set for network create")
}
err := internalutil.CommonNetworkCreate(n, &newNetwork)
if err != nil {
return nil, err
@ -77,14 +82,9 @@ func (n *cniNetwork) networkCreate(newNetwork types.Network, defaultNet bool) (*
return nil, errors.Wrapf(define.ErrInvalidArg, "unsupported driver %s", newNetwork.Driver)
}
for i := range newNetwork.Subnets {
err := internalutil.ValidateSubnet(&newNetwork.Subnets[i], !newNetwork.Internal, usedNetworks)
if err != nil {
return nil, err
}
if util.IsIPv6(newNetwork.Subnets[i].Subnet.IP) {
newNetwork.IPv6Enabled = true
}
err = internalutil.ValidateSubnets(&newNetwork, usedNetworks)
if err != nil {
return nil, err
}
// generate the network ID

View File

@ -7,12 +7,6 @@ import (
)
func CommonNetworkCreate(n NetUtil, network *types.Network) error {
// FIXME: Should we use a different type for network create without the ID field?
// the caller is not allowed to set a specific ID
if network.ID != "" {
return errors.Wrap(define.ErrInvalidArg, "ID can not be set for network create")
}
if network.Labels == nil {
network.Labels = map[string]string{}
}

View File

@ -68,3 +68,11 @@ func getRandomIPv6Subnet() (net.IPNet, error) {
ip = append(ip, make([]byte, 8)...)
return net.IPNet{IP: ip, Mask: net.CIDRMask(64, 128)}, nil
}
// NormalizeIP will transform the given ip to the 4 byte len ipv4 if possible
func NormalizeIP(ip *net.IP) {
ipv4 := ip.To4()
if ipv4 != nil {
*ip = ipv4
}
}

View File

@ -0,0 +1,37 @@
package util
import (
"strconv"
"github.com/pkg/errors"
)
// ParseMTU parses the mtu option
func ParseMTU(mtu string) (int, error) {
if mtu == "" {
return 0, nil // default
}
m, err := strconv.Atoi(mtu)
if err != nil {
return 0, err
}
if m < 0 {
return 0, errors.Errorf("mtu %d is less than zero", m)
}
return m, nil
}
// ParseVlan parses the vlan option
func ParseVlan(vlan string) (int, error) {
if vlan == "" {
return 0, nil // default
}
v, err := strconv.Atoi(vlan)
if err != nil {
return 0, err
}
if v < 0 || v > 4094 {
return 0, errors.Errorf("vlan ID %d must be between 0 and 4094", v)
}
return v, nil
}

View File

@ -38,6 +38,7 @@ func ValidateSubnet(s *types.Subnet, addGateway bool, usedNetworks []*net.IPNet)
if !s.Subnet.Contains(s.Gateway) {
return errors.Errorf("gateway %s not in subnet %s", s.Gateway, &s.Subnet)
}
NormalizeIP(&s.Gateway)
} else if addGateway {
ip, err := util.FirstIPInSubnet(net)
if err != nil {
@ -45,12 +46,35 @@ func ValidateSubnet(s *types.Subnet, addGateway bool, usedNetworks []*net.IPNet)
}
s.Gateway = ip
}
if s.LeaseRange != nil {
if s.LeaseRange.StartIP != nil && !s.Subnet.Contains(s.LeaseRange.StartIP) {
return errors.Errorf("lease range start ip %s not in subnet %s", s.LeaseRange.StartIP, &s.Subnet)
if s.LeaseRange.StartIP != nil {
if !s.Subnet.Contains(s.LeaseRange.StartIP) {
return errors.Errorf("lease range start ip %s not in subnet %s", s.LeaseRange.StartIP, &s.Subnet)
}
NormalizeIP(&s.LeaseRange.StartIP)
}
if s.LeaseRange.EndIP != nil && !s.Subnet.Contains(s.LeaseRange.EndIP) {
return errors.Errorf("lease range end ip %s not in subnet %s", s.LeaseRange.EndIP, &s.Subnet)
if s.LeaseRange.EndIP != nil {
if !s.Subnet.Contains(s.LeaseRange.EndIP) {
return errors.Errorf("lease range end ip %s not in subnet %s", s.LeaseRange.EndIP, &s.Subnet)
}
NormalizeIP(&s.LeaseRange.EndIP)
}
}
return nil
}
// ValidateSubnets will validate the subnets for this network.
// It also sets the gateway if the gateway is empty and it sets
// IPv6Enabled to true if at least one subnet is ipv6.
func ValidateSubnets(network *types.Network, usedNetworks []*net.IPNet) error {
for i := range network.Subnets {
err := ValidateSubnet(&network.Subnets[i], !network.Internal, usedNetworks)
if err != nil {
return err
}
if util.IsIPv6(network.Subnets[i].Subnet.IP) {
network.IPv6Enabled = true
}
}
return nil

View File

@ -0,0 +1,210 @@
// +build linux
package netavark
import (
"encoding/json"
"net"
"os"
"path/filepath"
"time"
"github.com/containers/podman/v3/libpod/define"
internalutil "github.com/containers/podman/v3/libpod/network/internal/util"
"github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/storage/pkg/stringid"
"github.com/pkg/errors"
)
// NetworkCreate will take a partial filled Network and fill the
// missing fields. It creates the Network and returns the full Network.
func (n *netavarkNetwork) NetworkCreate(net types.Network) (types.Network, error) {
n.lock.Lock()
defer n.lock.Unlock()
err := n.loadNetworks()
if err != nil {
return types.Network{}, err
}
network, err := n.networkCreate(net, false)
if err != nil {
return types.Network{}, err
}
// add the new network to the map
n.networks[network.Name] = network
return *network, nil
}
func (n *netavarkNetwork) networkCreate(newNetwork types.Network, defaultNet bool) (*types.Network, error) {
// if no driver is set use the default one
if newNetwork.Driver == "" {
newNetwork.Driver = types.DefaultNetworkDriver
}
if !defaultNet {
// FIXME: Should we use a different type for network create without the ID field?
// the caller is not allowed to set a specific ID
if newNetwork.ID != "" {
return nil, errors.Wrap(define.ErrInvalidArg, "ID can not be set for network create")
}
// generate random network ID
var i int
for i = 0; i < 1000; i++ {
id := stringid.GenerateNonCryptoID()
if _, err := n.getNetwork(id); err != nil {
newNetwork.ID = id
break
}
}
if i == 1000 {
return nil, errors.New("failed to create random network ID")
}
}
err := internalutil.CommonNetworkCreate(n, &newNetwork)
if err != nil {
return nil, err
}
// Only get the used networks for validation if we do not create the default network.
// The default network should not be validated against used subnets, we have to ensure
// that this network can always be created even when a subnet is already used on the host.
// This could happen if you run a container on this net, then the cni interface will be
// created on the host and "block" this subnet from being used again.
// Therefore the next podman command tries to create the default net again and it would
// fail because it thinks the network is used on the host.
var usedNetworks []*net.IPNet
if !defaultNet {
usedNetworks, err = internalutil.GetUsedSubnets(n)
if err != nil {
return nil, err
}
}
switch newNetwork.Driver {
case types.BridgeNetworkDriver:
err = internalutil.CreateBridge(n, &newNetwork, usedNetworks)
if err != nil {
return nil, err
}
// validate the given options, we do not need them but just check to make sure they are valid
for key, value := range newNetwork.Options {
switch key {
case "mtu":
_, err = internalutil.ParseMTU(value)
if err != nil {
return nil, err
}
case "vlan":
_, err = internalutil.ParseVlan(value)
if err != nil {
return nil, err
}
default:
return nil, errors.Errorf("unsupported network option %s", key)
}
}
default:
return nil, errors.Wrapf(define.ErrInvalidArg, "unsupported driver %s", newNetwork.Driver)
}
err = internalutil.ValidateSubnets(&newNetwork, usedNetworks)
if err != nil {
return nil, err
}
// FIXME: If we have a working solution for internal networks with dns this check should be removed.
if newNetwork.DNSEnabled && newNetwork.Internal {
return nil, errors.New("cannot set internal and dns enabled")
}
newNetwork.Created = time.Now()
if !defaultNet {
confPath := filepath.Join(n.networkConfigDir, newNetwork.Name+".json")
f, err := os.Create(confPath)
if err != nil {
return nil, err
}
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
err = enc.Encode(newNetwork)
if err != nil {
return nil, err
}
}
return &newNetwork, nil
}
// NetworkRemove will remove the Network with the given name or ID.
// It does not ensure that the network is unused.
func (n *netavarkNetwork) NetworkRemove(nameOrID string) error {
n.lock.Lock()
defer n.lock.Unlock()
err := n.loadNetworks()
if err != nil {
return err
}
network, err := n.getNetwork(nameOrID)
if err != nil {
return err
}
// Removing the default network is not allowed.
if network.Name == n.defaultNetwork {
return errors.Errorf("default network %s cannot be removed", n.defaultNetwork)
}
file := filepath.Join(n.networkConfigDir, network.Name+".json")
// make sure to not error for ErrNotExist
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
delete(n.networks, network.Name)
return nil
}
// NetworkList will return all known Networks. Optionally you can
// supply a list of filter functions. Only if a network matches all
// functions it is returned.
func (n *netavarkNetwork) NetworkList(filters ...types.FilterFunc) ([]types.Network, error) {
n.lock.Lock()
defer n.lock.Unlock()
err := n.loadNetworks()
if err != nil {
return nil, err
}
networks := make([]types.Network, 0, len(n.networks))
outer:
for _, net := range n.networks {
for _, filter := range filters {
// All filters have to match, if one does not match we can skip to the next network.
if !filter(*net) {
continue outer
}
}
networks = append(networks, *net)
}
return networks, nil
}
// NetworkInspect will return the Network with the given name or ID.
func (n *netavarkNetwork) NetworkInspect(nameOrID string) (types.Network, error) {
n.lock.Lock()
defer n.lock.Unlock()
err := n.loadNetworks()
if err != nil {
return types.Network{}, err
}
network, err := n.getNetwork(nameOrID)
if err != nil {
return types.Network{}, err
}
return *network, nil
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
// +build linux
package netavark
const defaultBridgeName = "podman"

View File

@ -0,0 +1,116 @@
package netavark
import (
"encoding/json"
"errors"
"os"
"os/exec"
"strconv"
"github.com/sirupsen/logrus"
)
type netavarkError struct {
exitCode int
// Set the json key to "error" so we can directly unmarshal into this struct
Msg string `json:"error"`
err error
}
func (e *netavarkError) Error() string {
ec := ""
// only add the exit code the the error message if we have at least info log level
// the normal user does not need to care about the number
if e.exitCode > 0 && logrus.IsLevelEnabled(logrus.InfoLevel) {
ec = " (exit code " + strconv.Itoa(e.exitCode) + ")"
}
msg := "netavark" + ec
if len(msg) > 0 {
msg += ": " + e.Msg
}
if e.err != nil {
msg += ": " + e.err.Error()
}
return msg
}
func (e *netavarkError) Unwrap() error {
return e.err
}
func newNetavarkError(msg string, err error) error {
return &netavarkError{
Msg: msg,
err: err,
}
}
// execNetavark will execute netavark with the following arguments
// It takes the path to the binary, the list of args and an interface which is
// marshaled to json and send via stdin to netavark. The result interface is
// used to marshal the netavark output into it. This can be nil.
// All errors return by this function should be of the type netavarkError
// to provide a helpful error message.
func execNetavark(binary string, args []string, stdin, result interface{}) error {
stdinR, stdinW, err := os.Pipe()
if err != nil {
return newNetavarkError("failed to create stdin pipe", err)
}
defer stdinR.Close()
stdoutR, stdoutW, err := os.Pipe()
if err != nil {
return newNetavarkError("failed to create stdout pipe", err)
}
defer stdoutR.Close()
defer stdoutW.Close()
cmd := exec.Command(binary, args...)
// connect the pipes to stdin and stdout
cmd.Stdin = stdinR
cmd.Stdout = stdoutW
// connect stderr to the podman stderr for logging
cmd.Stderr = os.Stderr
// set the netavark log level to the same as the podman
cmd.Env = append(os.Environ(), "RUST_LOG="+logrus.GetLevel().String())
// if we run with debug log level lets also set RUST_BACKTRACE=1 so we can get the full stack trace in case of panics
if logrus.IsLevelEnabled(logrus.DebugLevel) {
cmd.Env = append(cmd.Env, "RUST_BACKTRACE=1")
}
err = cmd.Start()
if err != nil {
return newNetavarkError("failed to start process", err)
}
err = json.NewEncoder(stdinW).Encode(stdin)
stdinW.Close()
if err != nil {
return newNetavarkError("failed to encode stdin data", err)
}
dec := json.NewDecoder(stdoutR)
err = cmd.Wait()
stdoutW.Close()
if err != nil {
exitError := &exec.ExitError{}
if errors.As(err, &exitError) {
ne := &netavarkError{}
// lets disallow unknown fields to make sure we do not get some unexpected stuff
dec.DisallowUnknownFields()
// this will unmarshal the error message into the error struct
ne.err = dec.Decode(ne)
ne.exitCode = exitError.ExitCode()
return ne
}
return newNetavarkError("unexpected failure during execution", err)
}
if result != nil {
err = dec.Decode(result)
if err != nil {
return newNetavarkError("failed to decode result", err)
}
}
return nil
}

View File

@ -0,0 +1,37 @@
// +build linux
package netavark_test
import (
"os"
"path/filepath"
"testing"
"github.com/containers/podman/v3/libpod/network/netavark"
"github.com/containers/podman/v3/libpod/network/types"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestNetavark(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Netavark Suite")
}
var netavarkBinary string
func init() {
netavarkBinary = os.Getenv("NETAVARK_BINARY")
if netavarkBinary == "" {
netavarkBinary = "/usr/libexec/podman/netavark"
}
}
func getNetworkInterface(confDir string, machine bool) (types.ContainerNetwork, error) {
return netavark.NewNetworkInterface(netavark.InitConfig{
NetworkConfigDir: confDir,
IsMachine: machine,
NetavarkBinary: netavarkBinary,
LockFile: filepath.Join(confDir, "netavark.lock"),
})
}

View File

@ -0,0 +1,275 @@
// +build linux
package netavark
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/containers/podman/v3/libpod/define"
"github.com/containers/podman/v3/libpod/network/internal/util"
"github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/storage/pkg/lockfile"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type netavarkNetwork struct {
// networkConfigDir is directory where the network config files are stored.
networkConfigDir string
// netavarkBinary is the path to the netavark binary.
netavarkBinary string
// defaultNetwork is the name for the default network.
defaultNetwork string
// defaultSubnet is the default subnet for the default network.
defaultSubnet types.IPNet
// isMachine describes whenever podman runs in a podman machine environment.
isMachine bool
// lock is a internal lock for critical operations
lock lockfile.Locker
// modTime is the timestamp when the config dir was modified
modTime time.Time
// networks is a map with loaded networks, the key is the network name
networks map[string]*types.Network
}
type InitConfig struct {
// NetworkConfigDir is directory where the network config files are stored.
NetworkConfigDir string
// NetavarkBinary is the path to the netavark binary.
NetavarkBinary string
// DefaultNetwork is the name for the default network.
DefaultNetwork string
// DefaultSubnet is the default subnet for the default network.
DefaultSubnet string
// IsMachine describes whenever podman runs in a podman machine environment.
IsMachine bool
// LockFile is the path to lock file.
LockFile string
}
// NewNetworkInterface creates the ContainerNetwork interface for the netavark backend.
// Note: The networks are not loaded from disk until a method is called.
func NewNetworkInterface(conf InitConfig) (types.ContainerNetwork, error) {
// TODO: consider using a shared memory lock
lock, err := lockfile.GetLockfile(conf.LockFile)
if err != nil {
return nil, err
}
defaultNetworkName := conf.DefaultNetwork
if defaultNetworkName == "" {
defaultNetworkName = types.DefaultNetworkName
}
defaultSubnet := conf.DefaultSubnet
if defaultSubnet == "" {
defaultSubnet = types.DefaultSubnet
}
defaultNet, err := types.ParseCIDR(defaultSubnet)
if err != nil {
return nil, errors.Wrap(err, "failed to parse default subnet")
}
n := &netavarkNetwork{
networkConfigDir: conf.NetworkConfigDir,
netavarkBinary: conf.NetavarkBinary,
defaultNetwork: defaultNetworkName,
defaultSubnet: defaultNet,
isMachine: conf.IsMachine,
lock: lock,
}
return n, nil
}
// Drivers will return the list of supported network drivers
// for this interface.
func (n *netavarkNetwork) Drivers() []string {
return []string{types.BridgeNetworkDriver}
}
func (n *netavarkNetwork) loadNetworks() error {
// check the mod time of the config dir
f, err := os.Stat(n.networkConfigDir)
if err != nil {
return err
}
modTime := f.ModTime()
// skip loading networks if they are already loaded and
// if the config dir was not modified since the last call
if n.networks != nil && modTime.Equal(n.modTime) {
return nil
}
// make sure the remove all networks before we reload them
n.networks = nil
n.modTime = modTime
files, err := ioutil.ReadDir(n.networkConfigDir)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
networks := make(map[string]*types.Network, len(files))
for _, f := range files {
if f.IsDir() {
continue
}
if filepath.Ext(f.Name()) != ".json" {
continue
}
path := filepath.Join(n.networkConfigDir, f.Name())
file, err := os.Open(path)
if err != nil {
// do not log ENOENT errors
if !errors.Is(err, os.ErrNotExist) {
logrus.Warnf("Error loading network config file %q: %v", path, err)
}
continue
}
network := new(types.Network)
err = json.NewDecoder(file).Decode(network)
if err != nil {
logrus.Warnf("Error reading network config file %q: %v", path, err)
continue
}
// check that the filename matches the network name
if network.Name+".json" != f.Name() {
logrus.Warnf("Network config name %q does not match file name %q, skipping", network.Name, f.Name())
continue
}
if !define.NameRegex.MatchString(network.Name) {
logrus.Warnf("Network config %q has invalid name: %q, skipping: %v", path, network.Name, define.RegexError)
continue
}
err = parseNetwork(network)
if err != nil {
logrus.Warnf("Network config %q could not be parsed, skipping: %v", path, err)
continue
}
logrus.Debugf("Successfully loaded network %s: %v", network.Name, network)
networks[network.Name] = network
}
// create the default network in memory if it did not exists on disk
if networks[n.defaultNetwork] == nil {
networkInfo, err := n.createDefaultNetwork()
if err != nil {
return errors.Wrapf(err, "failed to create default network %s", n.defaultNetwork)
}
networks[n.defaultNetwork] = networkInfo
}
logrus.Debugf("Successfully loaded %d networks", len(networks))
n.networks = networks
return nil
}
func parseNetwork(network *types.Network) error {
if network.Labels == nil {
network.Labels = map[string]string{}
}
if network.Options == nil {
network.Options = map[string]string{}
}
if network.IPAMOptions == nil {
network.IPAMOptions = map[string]string{}
}
if len(network.ID) != 64 {
return errors.Errorf("invalid network ID %q", network.ID)
}
return util.ValidateSubnets(network, nil)
}
func (n *netavarkNetwork) createDefaultNetwork() (*types.Network, error) {
net := types.Network{
Name: n.defaultNetwork,
NetworkInterface: defaultBridgeName + "0",
// Important do not change this ID
ID: "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9",
Driver: types.BridgeNetworkDriver,
Subnets: []types.Subnet{
{Subnet: n.defaultSubnet},
},
}
return n.networkCreate(net, true)
}
// getNetwork will lookup a network by name or ID. It returns an
// error when no network was found or when more than one network
// with the given (partial) ID exists.
// getNetwork will read from the networks map, therefore the caller
// must ensure that n.lock is locked before using it.
func (n *netavarkNetwork) getNetwork(nameOrID string) (*types.Network, error) {
// fast path check the map key, this will only work for names
if val, ok := n.networks[nameOrID]; ok {
return val, nil
}
// If there was no match we might got a full or partial ID.
var net *types.Network
for _, val := range n.networks {
// This should not happen because we already looked up the map by name but check anyway.
if val.Name == nameOrID {
return val, nil
}
if strings.HasPrefix(val.ID, nameOrID) {
if net != nil {
return nil, errors.Errorf("more than one result for network ID %s", nameOrID)
}
net = val
}
}
if net != nil {
return net, nil
}
return nil, errors.Wrapf(define.ErrNoSuchNetwork, "unable to find network with name or ID %s", nameOrID)
}
// Implement the NetUtil interface for easy code sharing with other network interfaces.
// ForEach call the given function for each network
func (n *netavarkNetwork) ForEach(run func(types.Network)) {
for _, val := range n.networks {
run(*val)
}
}
// Len return the number of networks
func (n *netavarkNetwork) Len() int {
return len(n.networks)
}
// DefaultInterfaceName return the default cni bridge name, must be suffixed with a number.
func (n *netavarkNetwork) DefaultInterfaceName() string {
return defaultBridgeName
}
func (n *netavarkNetwork) Network(nameOrID string) (*types.Network, error) {
network, err := n.getNetwork(nameOrID)
if err != nil {
return nil, err
}
return network, nil
}

View File

@ -0,0 +1,90 @@
// +build linux
package netavark
import (
"encoding/json"
"fmt"
"github.com/containers/podman/v3/libpod/network/internal/util"
"github.com/containers/podman/v3/libpod/network/types"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type netavarkOptions struct {
types.NetworkOptions
Networks map[string]*types.Network `json:"network_info"`
}
// Setup will setup the container network namespace. It returns
// a map of StatusBlocks, the key is the network name.
func (n *netavarkNetwork) Setup(namespacePath string, options types.SetupOptions) (map[string]types.StatusBlock, error) {
n.lock.Lock()
defer n.lock.Unlock()
err := n.loadNetworks()
if err != nil {
return nil, err
}
err = util.ValidateSetupOptions(n, namespacePath, options)
if err != nil {
return nil, err
}
// TODO IP address assignment
netavarkOpts, err := n.convertNetOpts(options.NetworkOptions)
if err != nil {
return nil, errors.Wrap(err, "failed to convert net opts")
}
b, err := json.Marshal(&netavarkOpts)
if err != nil {
return nil, err
}
fmt.Println(string(b))
result := map[string]types.StatusBlock{}
err = execNetavark(n.netavarkBinary, []string{"setup", namespacePath}, netavarkOpts, result)
if len(result) != len(options.Networks) {
logrus.Errorf("unexpected netavark result: %v", result)
return nil, fmt.Errorf("unexpected netavark result length, want (%d), got (%d) networks", len(options.Networks), len(result))
}
return result, err
}
// Teardown will teardown the container network namespace.
func (n *netavarkNetwork) Teardown(namespacePath string, options types.TeardownOptions) error {
n.lock.Lock()
defer n.lock.Unlock()
err := n.loadNetworks()
if err != nil {
return err
}
netavarkOpts, err := n.convertNetOpts(options.NetworkOptions)
if err != nil {
return errors.Wrap(err, "failed to convert net opts")
}
return execNetavark(n.netavarkBinary, []string{"teardown", namespacePath}, netavarkOpts, nil)
}
func (n *netavarkNetwork) convertNetOpts(opts types.NetworkOptions) (*netavarkOptions, error) {
netavarkOptions := netavarkOptions{
NetworkOptions: opts,
Networks: make(map[string]*types.Network, len(opts.Networks)),
}
for network := range opts.Networks {
net, err := n.getNetwork(network)
if err != nil {
return nil, err
}
netavarkOptions.Networks[network] = net
}
return &netavarkOptions, nil
}

View File

@ -0,0 +1,363 @@
// +build linux
package netavark_test
// The tests have to be run as root.
// For each test there will be two network namespaces created,
// netNSTest and netNSContainer. Each test must be run inside
// netNSTest to prevent leakage in the host netns, therefore
// it should use the following structure:
// It("test name", func() {
// runTest(func() {
// // add test logic here
// })
// })
import (
"bytes"
"fmt"
"io/ioutil"
"net"
"os"
"strconv"
"sync"
"time"
"github.com/containernetworking/plugins/pkg/ns"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/sirupsen/logrus"
"github.com/vishvananda/netlink"
"github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/podman/v3/pkg/netns"
"github.com/containers/podman/v3/pkg/rootless"
"github.com/containers/storage/pkg/stringid"
)
var _ = Describe("run netavark", func() {
var (
libpodNet types.ContainerNetwork
confDir string
logBuffer bytes.Buffer
netNSTest ns.NetNS
netNSContainer ns.NetNS
)
// runTest is a helper function to run a test. It ensures that each test
// is run in its own netns. It also creates a mountns to mount a tmpfs to /var/lib/cni.
runTest := func(run func()) {
netNSTest.Do(func(_ ns.NetNS) error {
defer GinkgoRecover()
// we have to setup the loopback adapter in this netns to use port forwarding
link, err := netlink.LinkByName("lo")
Expect(err).To(BeNil(), "Failed to get loopback adapter")
err = netlink.LinkSetUp(link)
Expect(err).To(BeNil(), "Failed to set loopback adapter up")
run()
return nil
})
}
BeforeEach(func() {
logrus.SetLevel(logrus.TraceLevel)
logrus.SetFormatter(&logrus.TextFormatter{DisableQuote: true})
// The tests need root privileges.
// Technically we could work around that by using user namespaces and
// the rootless cni code but this is to much work to get it right for a unit test.
if rootless.IsRootless() {
Skip("this test needs to be run as root")
}
var err error
confDir, err = ioutil.TempDir("", "podman_netavark_test")
if err != nil {
Fail("Failed to create tmpdir")
}
logBuffer = bytes.Buffer{}
logrus.SetOutput(&logBuffer)
netNSTest, err = netns.NewNS()
if err != nil {
Fail("Failed to create netns")
}
netNSContainer, err = netns.NewNS()
if err != nil {
Fail("Failed to create netns")
}
})
JustBeforeEach(func() {
var err error
libpodNet, err = getNetworkInterface(confDir, false)
if err != nil {
Fail("Failed to create NewCNINetworkInterface")
}
})
AfterEach(func() {
logrus.SetFormatter(&logrus.TextFormatter{})
logrus.SetLevel(logrus.InfoLevel)
os.RemoveAll(confDir)
netns.UnmountNS(netNSTest)
netNSTest.Close()
netns.UnmountNS(netNSContainer)
netNSContainer.Close()
fmt.Println(logBuffer.String())
})
It("test basic setup", func() {
runTest(func() {
defNet := types.DefaultNetworkName
intName := "eth0"
opts := types.SetupOptions{
NetworkOptions: types.NetworkOptions{
ContainerID: "someID",
ContainerName: "someName",
Networks: map[string]types.PerNetworkOptions{
defNet: {
InterfaceName: intName,
},
},
},
}
res, err := libpodNet.Setup(netNSContainer.Path(), opts)
Expect(err).ToNot(HaveOccurred())
Expect(res).To(HaveLen(1))
Expect(res).To(HaveKey(defNet))
Expect(res[defNet].Interfaces).To(HaveKey(intName))
Expect(res[defNet].Interfaces[intName].Networks).To(HaveLen(1))
ip := res[defNet].Interfaces[intName].Networks[0].Subnet.IP
Expect(ip.String()).To(ContainSubstring("10.88.0."))
gw := res[defNet].Interfaces[intName].Networks[0].Gateway
Expect(gw.String()).To(Equal("10.88.0.1"))
macAddress := res[defNet].Interfaces[intName].MacAddress
Expect(macAddress).To(HaveLen(6))
// default network has no dns
Expect(res[defNet].DNSServerIPs).To(BeEmpty())
Expect(res[defNet].DNSSearchDomains).To(BeEmpty())
// check in the container namespace if the settings are applied
err = netNSContainer.Do(func(_ ns.NetNS) error {
defer GinkgoRecover()
i, err := net.InterfaceByName(intName)
Expect(err).To(BeNil())
Expect(i.Name).To(Equal(intName))
Expect(i.HardwareAddr).To(Equal(macAddress))
addrs, err := i.Addrs()
Expect(err).To(BeNil())
subnet := &net.IPNet{
IP: ip,
Mask: net.CIDRMask(16, 32),
}
Expect(addrs).To(ContainElements(subnet))
// check loopback adapter
i, err = net.InterfaceByName("lo")
Expect(err).To(BeNil())
Expect(i.Name).To(Equal("lo"))
Expect(i.Flags & net.FlagLoopback).To(Equal(net.FlagLoopback))
Expect(i.Flags&net.FlagUp).To(Equal(net.FlagUp), "Loopback adapter should be up")
return nil
})
Expect(err).To(BeNil())
// default bridge name
bridgeName := "podman0"
// check settings on the host side
i, err := net.InterfaceByName(bridgeName)
Expect(err).ToNot(HaveOccurred())
Expect(i.Name).To(Equal(bridgeName))
addrs, err := i.Addrs()
Expect(err).ToNot(HaveOccurred())
// test that the gateway ip is assigned to the interface
subnet := &net.IPNet{
IP: gw,
Mask: net.CIDRMask(16, 32),
}
Expect(addrs).To(ContainElements(subnet))
wg := &sync.WaitGroup{}
expected := stringid.GenerateNonCryptoID()
// now check ip connectivity
err = netNSContainer.Do(func(_ ns.NetNS) error {
runNetListener(wg, "tcp", "0.0.0.0", 5000, expected)
return nil
})
Expect(err).ToNot(HaveOccurred())
conn, err := net.Dial("tcp", ip.String()+":5000")
Expect(err).To(BeNil())
_, err = conn.Write([]byte(expected))
Expect(err).To(BeNil())
conn.Close()
err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(opts))
Expect(err).ToNot(HaveOccurred())
})
})
for _, proto := range []string{"tcp", "udp"} {
// copy proto to extra var to keep correct references in the goroutines
protocol := proto
It("run with exposed ports protocol "+protocol, func() {
runTest(func() {
testdata := stringid.GenerateNonCryptoID()
defNet := types.DefaultNetworkName
intName := "eth0"
setupOpts := types.SetupOptions{
NetworkOptions: types.NetworkOptions{
ContainerID: stringid.GenerateNonCryptoID(),
PortMappings: []types.PortMapping{{
Protocol: protocol,
HostIP: "127.0.0.1",
HostPort: 5000,
ContainerPort: 5000,
}},
Networks: map[string]types.PerNetworkOptions{
defNet: {InterfaceName: intName},
},
},
}
res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts)
Expect(err).To(BeNil())
Expect(res).To(HaveLen(1))
Expect(res).To(HaveKey(defNet))
Expect(res[defNet].Interfaces).To(HaveKey(intName))
Expect(res[defNet].Interfaces[intName].Networks).To(HaveLen(1))
Expect(res[defNet].Interfaces[intName].Networks[0].Subnet.IP.String()).To(ContainSubstring("10.88.0."))
Expect(res[defNet].Interfaces[intName].MacAddress).To(HaveLen(6))
// default network has no dns
Expect(res[defNet].DNSServerIPs).To(BeEmpty())
Expect(res[defNet].DNSSearchDomains).To(BeEmpty())
var wg sync.WaitGroup
wg.Add(1)
// start a listener in the container ns
err = netNSContainer.Do(func(_ ns.NetNS) error {
defer GinkgoRecover()
runNetListener(&wg, protocol, "0.0.0.0", 5000, testdata)
return nil
})
Expect(err).To(BeNil())
conn, err := net.Dial(protocol, "127.0.0.1:5000")
Expect(err).To(BeNil())
_, err = conn.Write([]byte(testdata))
Expect(err).To(BeNil())
conn.Close()
// wait for the listener to finish
wg.Wait()
err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts))
Expect(err).To(BeNil())
})
})
It("run with range ports protocol "+protocol, func() {
runTest(func() {
defNet := types.DefaultNetworkName
intName := "eth0"
setupOpts := types.SetupOptions{
NetworkOptions: types.NetworkOptions{
ContainerID: stringid.GenerateNonCryptoID(),
PortMappings: []types.PortMapping{{
Protocol: protocol,
HostIP: "127.0.0.1",
HostPort: 5001,
ContainerPort: 5000,
Range: 3,
}},
Networks: map[string]types.PerNetworkOptions{
defNet: {InterfaceName: intName},
},
},
}
res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts)
Expect(err).To(BeNil())
Expect(res).To(HaveLen(1))
Expect(res).To(HaveKey(defNet))
Expect(res[defNet].Interfaces).To(HaveKey(intName))
Expect(res[defNet].Interfaces[intName].Networks).To(HaveLen(1))
containerIP := res[defNet].Interfaces[intName].Networks[0].Subnet.IP.String()
Expect(containerIP).To(ContainSubstring("10.88.0."))
Expect(res[defNet].Interfaces[intName].MacAddress).To(HaveLen(6))
// default network has no dns
Expect(res[defNet].DNSServerIPs).To(BeEmpty())
Expect(res[defNet].DNSSearchDomains).To(BeEmpty())
// loop over all ports
for p := 5001; p < 5004; p++ {
port := p
var wg sync.WaitGroup
wg.Add(1)
testdata := stringid.GenerateNonCryptoID()
// start a listener in the container ns
err = netNSContainer.Do(func(_ ns.NetNS) error {
defer GinkgoRecover()
runNetListener(&wg, protocol, containerIP, port-1, testdata)
return nil
})
Expect(err).To(BeNil())
conn, err := net.Dial(protocol, net.JoinHostPort("127.0.0.1", strconv.Itoa(port)))
Expect(err).To(BeNil())
_, err = conn.Write([]byte(testdata))
Expect(err).To(BeNil())
conn.Close()
// wait for the listener to finish
wg.Wait()
}
err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts))
Expect(err).To(BeNil())
})
})
}
})
func runNetListener(wg *sync.WaitGroup, protocol, ip string, port int, expectedData string) {
switch protocol {
case "tcp":
ln, err := net.Listen(protocol, net.JoinHostPort(ip, strconv.Itoa(port)))
Expect(err).To(BeNil())
// make sure to read in a separate goroutine to not block
go func() {
defer GinkgoRecover()
defer wg.Done()
defer ln.Close()
conn, err := ln.Accept()
Expect(err).To(BeNil())
defer conn.Close()
conn.SetDeadline(time.Now().Add(1 * time.Second))
data, err := ioutil.ReadAll(conn)
Expect(err).To(BeNil())
Expect(string(data)).To(Equal(expectedData))
}()
case "udp":
conn, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.ParseIP(ip),
Port: port,
})
Expect(err).To(BeNil())
conn.SetDeadline(time.Now().Add(1 * time.Second))
go func() {
defer GinkgoRecover()
defer wg.Done()
defer conn.Close()
data := make([]byte, len(expectedData))
i, err := conn.Read(data)
Expect(err).To(BeNil())
Expect(i).To(Equal(len(expectedData)))
Expect(string(data)).To(Equal(expectedData))
}()
default:
Fail("unsupported protocol")
}
}

View File

@ -0,0 +1,16 @@
{
"name": "bridge",
"id": "17f29b073143d8cd97b5bbe492bdeffec1c5fee55cc1fe2112c8b9335f8b6121",
"driver": "bridge",
"network_interface": "podman9",
"created": "2021-10-06T18:50:54.25770461+02:00",
"subnets": [
{
"subnet": "10.89.8.0/24",
"gateway": "10.89.8.1",
"lease_range": {
"start_ip": "10.89.8.20",
"end_ip": "10.89.8.50"
}
}
],

View File

@ -0,0 +1,19 @@
{
"name": "invalid name",
"id": "6839f44f0fd01c5c5830856b66a1d7ce46842dd8798be0addf96f7255ce9f889",
"driver": "bridge",
"network_interface": "podman9",
"created": "2021-10-06T18:50:54.25770461+02:00",
"subnets": [
{
"subnet": "10.89.8.0/24",
"gateway": "10.89.8.1"
}
],
"ipv6_enabled": false,
"internal": false,
"dns_enabled": true,
"ipam_options": {
"driver": "host-local"
}
}

View File

@ -0,0 +1,19 @@
{
"name": "invalid_gateway",
"id": "49be6e401e7f8b9844afb969dcbc96e78205ed86ec1e5a46150bd4ab4fdd5686",
"driver": "bridge",
"network_interface": "podman9",
"created": "2021-10-06T18:50:54.25770461+02:00",
"subnets": [
{
"subnet": "10.89.9.0/24",
"gateway": "10.89.100.1"
}
],
"ipv6_enabled": false,
"internal": false,
"dns_enabled": true,
"ipam_options": {
"driver": "host-local"
}
}

View File

@ -0,0 +1,19 @@
{
"name": "name_miss",
"id": "3bed2cb3a3acf7b6a8ef408420cc682d5520e26976d354254f528c965612054f",
"driver": "bridge",
"network_interface": "podman8",
"created": "2021-10-06T18:50:54.25770461+02:00",
"subnets": [
{
"subnet": "10.89.7.0/24",
"gateway": "10.89.7.1"
}
],
"ipv6_enabled": false,
"internal": true,
"dns_enabled": false,
"ipam_options": {
"driver": "host-local"
}
}

View File

@ -0,0 +1,19 @@
{
"name": "wrongID",
"id": "someID",
"driver": "bridge",
"network_interface": "podman1",
"created": "2021-10-06T18:50:54.25770461+02:00",
"subnets": [
{
"subnet": "10.89.0.0/24",
"gateway": "10.89.0.1"
}
],
"ipv6_enabled": false,
"internal": false,
"dns_enabled": false,
"ipam_options": {
"driver": "host-local"
}
}

View File

@ -0,0 +1,23 @@
{
"name": "bridge",
"id": "17f29b073143d8cd97b5bbe492bdeffec1c5fee55cc1fe2112c8b9335f8b6121",
"driver": "bridge",
"network_interface": "podman9",
"created": "2021-10-06T18:50:54.25770461+02:00",
"subnets": [
{
"subnet": "10.89.8.0/24",
"gateway": "10.89.8.1",
"lease_range": {
"start_ip": "10.89.8.20",
"end_ip": "10.89.8.50"
}
}
],
"ipv6_enabled": false,
"internal": false,
"dns_enabled": true,
"ipam_options": {
"driver": "host-local"
}
}

View File

@ -0,0 +1,23 @@
{
"name": "dualstack",
"id": "6839f44f0fd01c5c5830856b66a1d7ce46842dd8798be0addf96f7255ce9f889",
"driver": "bridge",
"network_interface": "podman21",
"created": "2021-10-06T18:50:54.25770461+02:00",
"subnets": [
{
"subnet": "fd10:88:a::/64",
"gateway": "fd10:88:a::1"
},
{
"subnet": "10.89.19.0/24",
"gateway": "10.89.19.10"
}
],
"ipv6_enabled": true,
"internal": false,
"dns_enabled": true,
"ipam_options": {
"driver": "host-local"
}
}

View File

@ -0,0 +1,18 @@
{
"name": "internal",
"id": "3bed2cb3a3acf7b6a8ef408420cc682d5520e26976d354254f528c965612054f",
"driver": "bridge",
"network_interface": "podman8",
"created": "2021-10-06T18:50:54.25770461+02:00",
"subnets": [
{
"subnet": "10.89.7.0/24"
}
],
"ipv6_enabled": false,
"internal": true,
"dns_enabled": false,
"ipam_options": {
"driver": "host-local"
}
}

View File

@ -0,0 +1,22 @@
{
"name": "label",
"id": "1aca80e8b55c802f7b43740da2990e1b5735bbb323d93eb5ebda8395b04025e2",
"driver": "bridge",
"network_interface": "podman15",
"created": "2021-10-06T18:50:54.25770461+02:00",
"subnets": [
{
"subnet": "10.89.13.0/24",
"gateway": "10.89.13.1"
}
],
"ipv6_enabled": false,
"internal": false,
"dns_enabled": true,
"labels": {
"mykey": "value"
},
"ipam_options": {
"driver": "host-local"
}
}

View File

@ -0,0 +1,22 @@
{
"name": "mtu",
"id": "49be6e401e7f8b9844afb969dcbc96e78205ed86ec1e5a46150bd4ab4fdd5686",
"driver": "bridge",
"network_interface": "podman13",
"created": "2021-10-06T18:50:54.25770461+02:00",
"subnets": [
{
"subnet": "10.89.11.0/24",
"gateway": "10.89.11.1"
}
],
"ipv6_enabled": false,
"internal": false,
"dns_enabled": true,
"options": {
"mtu": "1500"
},
"ipam_options": {
"driver": "host-local"
}
}

View File

@ -0,0 +1,19 @@
{
"name": "podman",
"id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9",
"driver": "bridge",
"network_interface": "podman0",
"created": "2021-10-06T18:50:54.25770461+02:00",
"subnets": [
{
"subnet": "10.88.0.0/16",
"gateway": "10.88.0.1"
}
],
"ipv6_enabled": false,
"internal": false,
"dns_enabled": false,
"ipam_options": {
"driver": "host-local"
}
}

View File

@ -0,0 +1,22 @@
{
"name": "vlan",
"id": "c3b258168c41c0bce97616716bef315eeed33eb1142904bfe7f32eb392c7cf80",
"driver": "bridge",
"network_interface": "podman14",
"created": "2021-10-06T18:50:54.25770461+02:00",
"subnets": [
{
"subnet": "10.89.12.0/24",
"gateway": "10.89.12.1"
}
],
"ipv6_enabled": false,
"internal": false,
"dns_enabled": true,
"options": {
"vlan": "5"
},
"ipam_options": {
"driver": "host-local"
}
}