Files
flouthoc 51bb71d1b3 vendor: bump c/common to 9b0d134f392
Bump common to 9b0d134f392f41de3f3065aad162e73a3904168e

Signed-off-by: flouthoc <flouthoc.git@gmail.com>
2025-04-01 09:58:43 -07:00

517 lines
14 KiB
Go

//go:build linux || freebsd
package netavark
import (
"encoding/json"
"errors"
"fmt"
"net"
"os"
"path/filepath"
"reflect"
"slices"
"strconv"
"time"
internalutil "github.com/containers/common/libnetwork/internal/util"
"github.com/containers/common/libnetwork/types"
"github.com/containers/storage/pkg/stringid"
)
func sliceRemoveDuplicates(strList []string) []string {
list := make([]string, 0, len(strList))
for _, item := range strList {
if !slices.Contains(list, item) {
list = append(list, item)
}
}
return list
}
func (n *netavarkNetwork) commitNetwork(network *types.Network) error {
if err := os.MkdirAll(n.networkConfigDir, 0o755); err != nil {
return nil
}
confPath := filepath.Join(n.networkConfigDir, network.Name+".json")
f, err := os.Create(confPath)
if err != nil {
return err
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
err = enc.Encode(network)
if err != nil {
return err
}
return nil
}
func (n *netavarkNetwork) NetworkUpdate(name string, options types.NetworkUpdateOptions) error {
n.lock.Lock()
defer n.lock.Unlock()
err := n.loadNetworks()
if err != nil {
return err
}
network, err := n.getNetwork(name)
if err != nil {
return err
}
// Nameservers must be IP Addresses.
for _, dnsServer := range options.AddDNSServers {
if net.ParseIP(dnsServer) == nil {
return fmt.Errorf("unable to parse ip %s specified in AddDNSServer: %w", dnsServer, types.ErrInvalidArg)
}
}
for _, dnsServer := range options.RemoveDNSServers {
if net.ParseIP(dnsServer) == nil {
return fmt.Errorf("unable to parse ip %s specified in RemoveDNSServer: %w", dnsServer, types.ErrInvalidArg)
}
}
networkDNSServersBefore := network.NetworkDNSServers
networkDNSServersAfter := []string{}
for _, server := range networkDNSServersBefore {
if slices.Contains(options.RemoveDNSServers, server) {
continue
}
networkDNSServersAfter = append(networkDNSServersAfter, server)
}
networkDNSServersAfter = append(networkDNSServersAfter, options.AddDNSServers...)
networkDNSServersAfter = sliceRemoveDuplicates(networkDNSServersAfter)
network.NetworkDNSServers = networkDNSServersAfter
if reflect.DeepEqual(networkDNSServersBefore, networkDNSServersAfter) {
return nil
}
err = n.commitNetwork(network)
if err != nil {
return err
}
return n.execUpdate(network.Name, network.NetworkDNSServers)
}
// 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, options *types.NetworkCreateOptions) (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 {
if options != nil && options.IgnoreIfExists && errors.Is(err, types.ErrNetworkExists) {
if network, ok := n.networks[net.Name]; ok {
return *network, 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, fmt.Errorf("ID can not be set for network create: %w", types.ErrInvalidArg)
}
// generate random network ID
var i int
for i = range 1000 {
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
}
err = validateIPAMDriver(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 && newNetwork.Driver == types.BridgeNetworkDriver {
usedNetworks, err = internalutil.GetUsedSubnets(n)
if err != nil {
return nil, err
}
}
switch newNetwork.Driver {
case types.BridgeNetworkDriver:
internalutil.MapDockerBridgeDriverOptions(newNetwork)
var vlan int
// validate the given options,
for key, value := range newNetwork.Options {
switch key {
case types.MTUOption:
_, err = internalutil.ParseMTU(value)
if err != nil {
return nil, err
}
case types.VLANOption:
vlan, err = internalutil.ParseVlan(value)
if err != nil {
return nil, err
}
case types.IsolateOption:
val, err := internalutil.ParseIsolate(value)
if err != nil {
return nil, err
}
newNetwork.Options[types.IsolateOption] = val
case types.MetricOption:
_, err := strconv.ParseUint(value, 10, 32)
if err != nil {
return nil, err
}
case types.NoDefaultRoute:
val, err := strconv.ParseBool(value)
if err != nil {
return nil, err
}
// rust only support "true" or "false" while go can parse 1 and 0 as well so we need to change it
newNetwork.Options[types.NoDefaultRoute] = strconv.FormatBool(val)
case types.VRFOption:
if len(value) == 0 {
return nil, errors.New("invalid vrf name")
}
case types.ModeOption:
if !slices.Contains(types.ValidBridgeModes, value) {
return nil, fmt.Errorf("unknown bridge mode %q", value)
}
default:
return nil, fmt.Errorf("unsupported bridge network option %s", key)
}
}
// If there is no vlan there should be no other config with the same bridge.
// However with vlan we want to allow that so that you can have different
// configs on the same bridge but different vlans
// https://github.com/containers/common/issues/2095
checkBridgeConflict := vlan == 0
err = internalutil.CreateBridge(n, newNetwork, usedNetworks, n.defaultsubnetPools, checkBridgeConflict)
if err != nil {
return nil, err
}
case types.MacVLANNetworkDriver, types.IPVLANNetworkDriver:
err = createIpvlanOrMacvlan(newNetwork)
if err != nil {
return nil, err
}
default:
net, err := n.createPlugin(newNetwork)
if err != nil {
return nil, err
}
newNetwork = net
}
// when we do not have ipam we must disable dns
internalutil.IpamNoneDisableDNS(newNetwork)
// process NetworkDNSServers
if len(newNetwork.NetworkDNSServers) > 0 && !newNetwork.DNSEnabled {
return nil, fmt.Errorf("cannot set NetworkDNSServers if DNS is not enabled for the network: %w", types.ErrInvalidArg)
}
// validate ip address
for _, dnsServer := range newNetwork.NetworkDNSServers {
if net.ParseIP(dnsServer) == nil {
return nil, fmt.Errorf("unable to parse ip %s specified in NetworkDNSServers: %w", dnsServer, types.ErrInvalidArg)
}
}
// add gateway when not internal or dns enabled
addGateway := !newNetwork.Internal || newNetwork.DNSEnabled
err = internalutil.ValidateSubnets(newNetwork, addGateway, usedNetworks)
if err != nil {
return nil, err
}
// validate routes
err = internalutil.ValidateRoutes(newNetwork.Routes)
if err != nil {
return nil, err
}
newNetwork.Created = time.Now()
if !defaultNet {
err = n.commitNetwork(newNetwork)
if err != nil {
return nil, err
}
}
return newNetwork, nil
}
// ipvlan shares the same mac address so supporting DHCP is not really possible
var errIpvlanNoDHCP = errors.New("ipam driver dhcp is not supported with ipvlan")
func createIpvlanOrMacvlan(network *types.Network) error {
if network.NetworkInterface != "" {
interfaceNames, err := internalutil.GetLiveNetworkNames()
if err != nil {
return err
}
if !slices.Contains(interfaceNames, network.NetworkInterface) {
return fmt.Errorf("parent interface %s does not exist", network.NetworkInterface)
}
}
driver := network.Driver
isMacVlan := driver != types.IPVLANNetworkDriver
// always turn dns off with macvlan, it is not implemented in netavark
// and makes little sense to support with macvlan
// see https://github.com/containers/netavark/pull/467
network.DNSEnabled = false
// we already validated the drivers before so we just have to set the default here
switch network.IPAMOptions[types.Driver] {
case "":
if len(network.Subnets) == 0 {
// if no subnets and no driver choose dhcp
network.IPAMOptions[types.Driver] = types.DHCPIPAMDriver
if !isMacVlan {
return errIpvlanNoDHCP
}
} else {
network.IPAMOptions[types.Driver] = types.HostLocalIPAMDriver
}
case types.HostLocalIPAMDriver:
if len(network.Subnets) == 0 {
return fmt.Errorf("%s driver needs at least one subnet specified when the host-local ipam driver is set", driver)
}
case types.DHCPIPAMDriver:
if !isMacVlan {
return errIpvlanNoDHCP
}
if len(network.Subnets) > 0 {
return errors.New("ipam driver dhcp set but subnets are set")
}
}
// validate the given options, we do not need them but just check to make sure they are valid
for key, value := range network.Options {
switch key {
case types.ModeOption:
if isMacVlan {
if !slices.Contains(types.ValidMacVLANModes, value) {
return fmt.Errorf("unknown macvlan mode %q", value)
}
} else {
if !slices.Contains(types.ValidIPVLANModes, value) {
return fmt.Errorf("unknown ipvlan mode %q", value)
}
}
case types.MetricOption:
_, err := strconv.ParseUint(value, 10, 32)
if err != nil {
return err
}
case types.MTUOption:
_, err := internalutil.ParseMTU(value)
if err != nil {
return err
}
case types.NoDefaultRoute:
val, err := strconv.ParseBool(value)
if err != nil {
return err
}
// rust only support "true" or "false" while go can parse 1 and 0 as well so we need to change it
network.Options[types.NoDefaultRoute] = strconv.FormatBool(val)
case types.BclimOption:
if isMacVlan {
_, err := strconv.ParseInt(value, 10, 32)
if err != nil {
return fmt.Errorf("failed to parse %q option: %w", key, err)
}
// do not fallthrough for macvlan
break
}
// bclim is only valid for macvlan not ipvlan so fallthrough to error case
fallthrough
default:
return fmt.Errorf("unsupported %s network option %s", driver, key)
}
}
return 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 fmt.Errorf("default network %s cannot be removed", n.defaultNetwork)
}
// remove the ipam bucket for this network
if err := n.removeNetworkIPAMBucket(network); err != nil {
return err
}
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
}
func validateIPAMDriver(n *types.Network) error {
ipamDriver := n.IPAMOptions[types.Driver]
switch ipamDriver {
case "", types.HostLocalIPAMDriver, types.DHCPIPAMDriver:
case types.NoneIPAMDriver:
if len(n.Subnets) > 0 {
return errors.New("none ipam driver is set but subnets are given")
}
default:
return fmt.Errorf("unsupported ipam driver %q", ipamDriver)
}
return nil
}
var errInvalidPluginResult = errors.New("invalid plugin result")
func (n *netavarkNetwork) createPlugin(net *types.Network) (*types.Network, error) {
path, err := getPlugin(net.Driver, n.pluginDirs)
if err != nil {
return nil, err
}
result := new(types.Network)
err = n.execPlugin(path, []string{"create"}, net, result)
if err != nil {
return nil, fmt.Errorf("plugin %s failed: %w", path, err)
}
// now make sure that neither the name, ID, driver were changed by the plugin
if net.Name != result.Name {
return nil, fmt.Errorf("%w: changed network name", errInvalidPluginResult)
}
if net.ID != result.ID {
return nil, fmt.Errorf("%w: changed network ID", errInvalidPluginResult)
}
if net.Driver != result.Driver {
return nil, fmt.Errorf("%w: changed network driver", errInvalidPluginResult)
}
return result, nil
}
func getAllPlugins(dirs []string) []string {
var plugins []string
for _, dir := range dirs {
entries, err := os.ReadDir(dir)
if err == nil {
for _, entry := range entries {
name := entry.Name()
if !slices.Contains(plugins, name) {
plugins = append(plugins, name)
}
}
}
}
return plugins
}
func getPlugin(name string, dirs []string) (string, error) {
for _, dir := range dirs {
fullpath := filepath.Join(dir, name)
st, err := os.Stat(fullpath)
if err == nil && st.Mode().IsRegular() {
return fullpath, nil
}
}
return "", fmt.Errorf("failed to find driver or plugin %q", name)
}