Network interface

Implement a new network interface to abstract CNI from libpod. The
interface is implemented for the CNI backend but in the future we can
add more backends.

The code is structured in three new packages:
- `libpod/network/types`: contains the interface definition
  and the necessary types for it.
- `libpod/network/cni` contains the interface implementation for the CNI
  backend.
- `libpod/network/util` a set of utility functions related to
  networking.

The CNI package uses ginkgo style unit tests. To test Setup/Teardown the
test must be run as root. Each test will run in their own namespace to
make the test independent from the host environment.

New features with the CNI backend:
- The default network will be created in memory if it does not exists on
  disk.
- It can set more than one static IP per container network.
- Networks are loaded once from disk and only if this interface is
  used, e.g. for commands such as `podman info` networks are not loaded.
  This reduces unnecessary disk IO.

This commit only adds the interface it is not wired into libpod. This
requires a lot of breaking changes which will be done in a followup
commit.

Once this is integrated into libpod the current network code under
`libpod/network` should be removed. Also the dependency on OCICNI
should be dropped.

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
This commit is contained in:
Paul Holzinger 2021-07-15 10:29:19 +02:00
parent e20ec47a59
commit c0b1edd6a4
No known key found for this signature in database
GPG Key ID: EB145DD938A3CAF2
40 changed files with 5577 additions and 45 deletions

View File

@ -10,6 +10,7 @@ import (
"github.com/containers/common/pkg/config"
"github.com/containers/podman/v3/cmd/podman/registry"
"github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/podman/v3/pkg/api/handlers"
"github.com/containers/podman/v3/pkg/cgroups"
"github.com/containers/podman/v3/pkg/domain/entities"
@ -150,7 +151,7 @@ func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, rtc *c
cappDrop []string
entrypoint *string
init bool
specPorts []specgen.PortMapping
specPorts []types.PortMapping
)
if cc.HostConfig.Init != nil {
@ -240,7 +241,7 @@ func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, rtc *c
if err != nil {
return nil, nil, err
}
tmpPort := specgen.PortMapping{
tmpPort := types.PortMapping{
HostIP: pb.HostIP,
ContainerPort: uint16(port.Int()),
HostPort: uint16(hostport),

View File

@ -6,7 +6,7 @@ import (
"strconv"
"strings"
"github.com/containers/podman/v3/pkg/specgen"
"github.com/containers/podman/v3/libpod/network/types"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
@ -90,10 +90,10 @@ func createExpose(expose []string) (map[uint16]string, error) {
}
// CreatePortBindings iterates ports mappings into SpecGen format.
func CreatePortBindings(ports []string) ([]specgen.PortMapping, error) {
func CreatePortBindings(ports []string) ([]types.PortMapping, error) {
// --publish is formatted as follows:
// [[hostip:]hostport[-endPort]:]containerport[-endPort][/protocol]
toReturn := make([]specgen.PortMapping, 0, len(ports))
toReturn := make([]types.PortMapping, 0, len(ports))
for _, p := range ports {
var (
@ -169,8 +169,8 @@ func CreatePortBindings(ports []string) ([]specgen.PortMapping, error) {
// parseSplitPort parses individual components of the --publish flag to produce
// a single port mapping in SpecGen format.
func parseSplitPort(hostIP, hostPort *string, ctrPort string, protocol *string) (specgen.PortMapping, error) {
newPort := specgen.PortMapping{}
func parseSplitPort(hostIP, hostPort *string, ctrPort string, protocol *string) (types.PortMapping, error) {
newPort := types.PortMapping{}
if ctrPort == "" {
return newPort, errors.Errorf("must provide a non-empty container port to publish")
}

View File

@ -0,0 +1,10 @@
This package abstracts CNI from libpod.
It implements the `ContainerNetwork` interface defined in [libpod/network/types/network.go](../types/network.go) for the CNI backend.
## Testing
Run the tests with:
```
go test -v -mod=vendor -cover ./libpod/network/cni/
```
Run the tests as root to also test setup/teardown. This will execute CNI and therefore the cni plugins have to be installed.

View File

@ -0,0 +1,375 @@
// +build linux
package cni
import (
"encoding/json"
"io/ioutil"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"github.com/containernetworking/cni/libcni"
"github.com/containernetworking/cni/pkg/version"
"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"
)
func createNetworkFromCNIConfigList(conf *libcni.NetworkConfigList, confPath string) (*types.Network, error) {
network := types.Network{
Name: conf.Name,
ID: getNetworkIDFromName(conf.Name),
Labels: map[string]string{},
Options: map[string]string{},
IPAMOptions: map[string]string{},
}
cniJSON := make(map[string]interface{})
err := json.Unmarshal(conf.Bytes, &cniJSON)
if err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal network config %s", conf.Name)
}
if args, ok := cniJSON["args"]; ok {
if key, ok := args.(map[string]interface{}); ok {
// read network labels and options from the conf file
network.Labels = getNetworkArgsFromConfList(key, podmanLabelKey)
network.Options = getNetworkArgsFromConfList(key, podmanOptionsKey)
}
}
f, err := os.Stat(confPath)
if err != nil {
return nil, err
}
stat := f.Sys().(*syscall.Stat_t)
network.Created = time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec))
firstPlugin := conf.Plugins[0]
network.Driver = firstPlugin.Network.Type
switch firstPlugin.Network.Type {
case types.BridgeNetworkDriver:
var bridge hostLocalBridge
err := json.Unmarshal(firstPlugin.Bytes, &bridge)
if err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal the bridge plugin config in %s", confPath)
}
network.NetworkInterface = bridge.BrName
// if isGateway is false we have an internal network
if !bridge.IsGW {
network.Internal = true
}
// set network options
if bridge.MTU != 0 {
network.Options["mtu"] = strconv.Itoa(bridge.MTU)
}
if bridge.Vlan != 0 {
network.Options["vlan"] = strconv.Itoa(bridge.Vlan)
}
err = convertIPAMConfToNetwork(&network, bridge.IPAM, confPath)
if err != nil {
return nil, err
}
case types.MacVLANNetworkDriver:
var macvlan macVLANConfig
err := json.Unmarshal(firstPlugin.Bytes, &macvlan)
if err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal the macvlan plugin config in %s", confPath)
}
network.NetworkInterface = macvlan.Master
// set network options
if macvlan.MTU != 0 {
network.Options["mtu"] = strconv.Itoa(macvlan.MTU)
}
err = convertIPAMConfToNetwork(&network, macvlan.IPAM, confPath)
if err != nil {
return nil, err
}
default:
// A warning would be good but users would get this warning everytime so keep this at info level.
logrus.Infof("unsupported CNI config type %s in %s, this network can still be used but inspect or list cannot show all information",
firstPlugin.Network.Type, confPath)
}
// check if the dnsname plugin is configured
network.DNSEnabled = findPluginByName(conf.Plugins, "dnsname")
return &network, nil
}
func findPluginByName(plugins []*libcni.NetworkConfig, name string) bool {
for _, plugin := range plugins {
if plugin.Network.Type == name {
return true
}
}
return false
}
// convertIPAMConfToNetwork converts A cni IPAMConfig to libpod network subnets.
// It returns an array of subnets and an extra bool if dhcp is configured.
func convertIPAMConfToNetwork(network *types.Network, ipam ipamConfig, confPath string) error {
if ipam.PluginType == types.DHCPIPAMDriver {
network.IPAMOptions["driver"] = types.DHCPIPAMDriver
return nil
}
if ipam.PluginType != types.HostLocalIPAMDriver {
return errors.Errorf("unsupported ipam plugin %s in %s", ipam.PluginType, confPath)
}
network.IPAMOptions["driver"] = types.HostLocalIPAMDriver
for _, r := range ipam.Ranges {
for _, ipam := range r {
s := types.Subnet{}
// Do not use types.ParseCIDR() because we want the ip to be
// the network address and not a random ip in the sub.
_, sub, err := net.ParseCIDR(ipam.Subnet)
if err != nil {
return err
}
s.Subnet = types.IPNet{IPNet: *sub}
// gateway
var gateway net.IP
if ipam.Gateway != "" {
gateway = net.ParseIP(ipam.Gateway)
if gateway == nil {
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
}
} else if !network.Internal {
// only add a gateway address if the network is not internal
gateway, err = util.FirstIPInSubnet(sub)
if err != nil {
return errors.Errorf("failed to get first ip in subnet %s", sub.String())
}
}
s.Gateway = gateway
var rangeStart net.IP
var rangeEnd net.IP
if ipam.RangeStart != "" {
rangeStart = net.ParseIP(ipam.RangeStart)
if rangeStart == nil {
return errors.Errorf("failed to parse range start ip %s", ipam.RangeStart)
}
}
if ipam.RangeEnd != "" {
rangeEnd = net.ParseIP(ipam.RangeEnd)
if rangeEnd == nil {
return errors.Errorf("failed to parse range end ip %s", ipam.RangeEnd)
}
}
if rangeStart != nil || rangeEnd != nil {
s.LeaseRange = &types.LeaseRange{}
s.LeaseRange.StartIP = rangeStart
s.LeaseRange.EndIP = rangeEnd
}
network.Subnets = append(network.Subnets, s)
}
}
return nil
}
// getNetworkArgsFromConfList returns the map of args in a conflist, argType should be labels or options
func getNetworkArgsFromConfList(args map[string]interface{}, argType string) map[string]string {
if args, ok := args[argType]; ok {
if labels, ok := args.(map[string]interface{}); ok {
result := make(map[string]string, len(labels))
for k, v := range labels {
if v, ok := v.(string); ok {
result[k] = v
}
}
return result
}
}
return nil
}
// createCNIConfigListFromNetwork will create a cni config file from the given network.
// It returns the cni config and the path to the file where the config was written.
// Set writeToDisk to false to only add this network into memory.
func (n *cniNetwork) createCNIConfigListFromNetwork(network *types.Network, writeToDisk bool) (*libcni.NetworkConfigList, string, error) {
var (
routes []ipamRoute
ipamRanges [][]ipamLocalHostRangeConf
ipamConf ipamConfig
err error
)
if len(network.Subnets) > 0 {
for _, subnet := range network.Subnets {
route, err := newIPAMDefaultRoute(util.IsIPv6(subnet.Subnet.IP))
if err != nil {
return nil, "", err
}
routes = append(routes, route)
ipam := newIPAMLocalHostRange(subnet.Subnet, subnet.LeaseRange, subnet.Gateway)
ipamRanges = append(ipamRanges, []ipamLocalHostRangeConf{*ipam})
}
ipamConf = newIPAMHostLocalConf(routes, ipamRanges)
} else {
ipamConf = ipamConfig{PluginType: "dhcp"}
}
vlan := 0
mtu := 0
for k, v := range network.Options {
switch k {
case "mtu":
mtu, err = parseMTU(v)
if err != nil {
return nil, "", err
}
case "vlan":
vlan, err = parseVlan(v)
if err != nil {
return nil, "", err
}
default:
return nil, "", errors.Errorf("unsupported network option %s", k)
}
}
isGateway := true
ipMasq := true
if network.Internal {
isGateway = false
ipMasq = false
}
// create CNI plugin configuration
ncList := newNcList(network.Name, version.Current(), network.Labels, network.Options)
var plugins []interface{}
switch network.Driver {
case types.BridgeNetworkDriver:
bridge := newHostLocalBridge(network.NetworkInterface, isGateway, ipMasq, mtu, vlan, ipamConf)
plugins = append(plugins, bridge, newPortMapPlugin(), newFirewallPlugin(), newTuningPlugin())
// if we find the dnsname plugin we add configuration for it
if hasDNSNamePlugin(n.cniPluginDirs) && network.DNSEnabled {
// Note: in the future we might like to allow for dynamic domain names
plugins = append(plugins, newDNSNamePlugin(defaultPodmanDomainName))
}
// Add the podman-machine CNI plugin if we are in a machine
if n.isMachine {
plugins = append(plugins, newPodmanMachinePlugin())
}
case types.MacVLANNetworkDriver:
plugins = append(plugins, newMacVLANPlugin(network.NetworkInterface, mtu, ipamConf))
default:
return nil, "", errors.Errorf("driver %q is not supported by cni", network.Driver)
}
ncList["plugins"] = plugins
b, err := json.MarshalIndent(ncList, "", " ")
if err != nil {
return nil, "", err
}
cniPathName := ""
if writeToDisk {
cniPathName = filepath.Join(n.cniConfigDir, network.Name+".conflist")
err = ioutil.WriteFile(cniPathName, b, 0644)
if err != nil {
return nil, "", err
}
f, err := os.Stat(cniPathName)
if err != nil {
return nil, "", err
}
stat := f.Sys().(*syscall.Stat_t)
network.Created = time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec))
} else {
network.Created = time.Now()
}
config, err := libcni.ConfListFromBytes(b)
if err != nil {
return nil, "", err
}
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 {
if port.Protocol == "" {
return nil, errors.New("port protocol should not be empty")
}
protocols := strings.Split(port.Protocol, ",")
for _, protocol := range protocols {
if !pkgutil.StringInSlice(protocol, []string{"tcp", "udp", "sctp"}) {
return nil, errors.Errorf("unknown port protocol %s", protocol)
}
cniPort := cniPortMapEntry{
HostPort: int(port.HostPort),
ContainerPort: int(port.ContainerPort),
HostIP: port.HostIP,
Protocol: protocol,
}
cniPorts = append(cniPorts, cniPort)
for i := 1; i < int(port.Range); i++ {
cniPort := cniPortMapEntry{
HostPort: int(port.HostPort) + i,
ContainerPort: int(port.ContainerPort) + i,
HostIP: port.HostIP,
Protocol: protocol,
}
cniPorts = append(cniPorts, cniPort)
}
}
}
return cniPorts, nil
}

View File

@ -0,0 +1,98 @@
// Copyright 2016 CNI authors
// Copyright 2021 Podman authors
//
// This code has been originally copied from github.com/containernetworking/cni
// but has been changed to better fit the Podman use case.
//
// 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.
// +build linux
package cni
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os/exec"
"path/filepath"
"github.com/containernetworking/cni/pkg/invoke"
"github.com/containernetworking/cni/pkg/version"
)
type cniExec struct {
version.PluginDecoder
}
type cniPluginError struct {
plugin string
Code uint `json:"code"`
Msg string `json:"msg"`
Details string `json:"details,omitempty"`
}
// Error returns a nicely formatted error message for the cni plugin errors.
func (e *cniPluginError) Error() string {
err := fmt.Sprintf("cni plugin %s failed", e.plugin)
if e.Msg != "" {
err = fmt.Sprintf("%s: %s", err, e.Msg)
} else if e.Code > 0 {
err = fmt.Sprintf("%s with error code %d", err, e.Code)
}
if e.Details != "" {
err = fmt.Sprintf("%s: %s", err, e.Details)
}
return err
}
// ExecPlugin execute the cni plugin. Returns the stdout of the plugin or an error.
func (e *cniExec) ExecPlugin(ctx context.Context, pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
c := exec.CommandContext(ctx, pluginPath)
c.Env = environ
c.Stdin = bytes.NewBuffer(stdinData)
c.Stdout = stdout
c.Stderr = stderr
err := c.Run()
if err != nil {
return nil, annotatePluginError(err, pluginPath, stdout.Bytes(), stderr.Bytes())
}
return stdout.Bytes(), nil
}
// annotatePluginError parses the common cni plugin error json.
func annotatePluginError(err error, plugin string, stdout []byte, stderr []byte) error {
pluginName := filepath.Base(plugin)
emsg := cniPluginError{
plugin: pluginName,
}
if len(stdout) == 0 {
if len(stderr) == 0 {
emsg.Msg = err.Error()
} else {
emsg.Msg = string(stderr)
}
} else if perr := json.Unmarshal(stdout, &emsg); perr != nil {
emsg.Msg = fmt.Sprintf("failed to unmarshal error message %q: %v", string(stdout), perr)
}
return &emsg
}
// FindInPath finds the plugin in the given paths.
func (e *cniExec) FindInPath(plugin string, paths []string) (string, error) {
return invoke.FindInPath(plugin, paths)
}

View File

@ -0,0 +1,53 @@
// +build linux
package cni_test
import (
"os"
"path/filepath"
"testing"
"github.com/containers/podman/v3/libpod/network/cni"
"github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/podman/v3/test/utils"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var cniPluginDirs = []string{
"/usr/libexec/cni",
"/usr/lib/cni",
"/usr/local/lib/cni",
"/opt/cni/bin",
}
func TestCni(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "CNI Suite")
}
func getNetworkInterface(cniConfDir string, machine bool) (types.ContainerNetwork, error) {
return cni.NewCNINetworkInterface(cni.InitConfig{
CNIConfigDir: cniConfDir,
CNIPluginDirs: cniPluginDirs,
IsMachine: machine,
LockFile: filepath.Join(cniConfDir, "cni.lock"),
})
}
func SkipIfNoDnsname() {
for _, path := range cniPluginDirs {
f, err := os.Stat(filepath.Join(path, "dnsname"))
if err == nil && f.Mode().IsRegular() {
return
}
}
Skip("dnsname cni plugin needs to be installed for this test")
}
func SkipIfNotFedora(msg string) {
info := utils.GetHostDistributionInfo()
if info.Distribution != "fedora" {
Skip("Test can only run on Fedora: " + msg)
}
}

View File

@ -0,0 +1,292 @@
// +build linux
package cni
import (
"net"
"os"
"path/filepath"
"github.com/containers/podman/v3/libpod/network/types"
)
const (
defaultIPv4Route = "0.0.0.0/0"
defaultIPv6Route = "::/0"
// defaultPodmanDomainName is used for the dnsname plugin to define
// a localized domain name for a created network
defaultPodmanDomainName = "dns.podman"
// cniDeviceName is the default name for a new bridge, it should be suffixed with an integer
cniDeviceName = "cni-podman"
// podmanLabelKey key used to store the podman network label in a cni config
podmanLabelKey = "podman_labels"
// podmanOptionsKey key used to store the podman network options in a cni config
podmanOptionsKey = "podman_options"
)
// cniPortMapEntry struct is used by the portmap plugin
// https://github.com/containernetworking/plugins/blob/649e0181fe7b3a61e708f3e4249a798f57f25cc5/plugins/meta/portmap/main.go#L43-L50
type cniPortMapEntry struct {
HostPort int `json:"hostPort"`
ContainerPort int `json:"containerPort"`
Protocol string `json:"protocol"`
HostIP string `json:"hostIP,omitempty"`
}
// hostLocalBridge describes a configuration for a bridge plugin
// https://github.com/containernetworking/plugins/tree/master/plugins/main/bridge#network-configuration-reference
type hostLocalBridge struct {
PluginType string `json:"type"`
BrName string `json:"bridge,omitempty"`
IsGW bool `json:"isGateway"`
IsDefaultGW bool `json:"isDefaultGateway,omitempty"`
ForceAddress bool `json:"forceAddress,omitempty"`
IPMasq bool `json:"ipMasq,omitempty"`
MTU int `json:"mtu,omitempty"`
HairpinMode bool `json:"hairpinMode,omitempty"`
PromiscMode bool `json:"promiscMode,omitempty"`
Vlan int `json:"vlan,omitempty"`
IPAM ipamConfig `json:"ipam"`
Capabilities map[string]bool `json:"capabilities"`
}
// ipamConfig describes an IPAM configuration
// https://github.com/containernetworking/plugins/tree/master/plugins/ipam/host-local#network-configuration-reference
type ipamConfig struct {
PluginType string `json:"type"`
Routes []ipamRoute `json:"routes,omitempty"`
ResolveConf string `json:"resolveConf,omitempty"`
DataDir string `json:"dataDir,omitempty"`
Ranges [][]ipamLocalHostRangeConf `json:"ranges,omitempty"`
}
// ipamLocalHostRangeConf describes the new style IPAM ranges
type ipamLocalHostRangeConf struct {
Subnet string `json:"subnet"`
RangeStart string `json:"rangeStart,omitempty"`
RangeEnd string `json:"rangeEnd,omitempty"`
Gateway string `json:"gateway,omitempty"`
}
// ipamRoute describes a route in an ipam config
type ipamRoute struct {
Dest string `json:"dst"`
}
// portMapConfig describes the default portmapping config
type portMapConfig struct {
PluginType string `json:"type"`
Capabilities map[string]bool `json:"capabilities"`
}
// macVLANConfig describes the macvlan config
type macVLANConfig struct {
PluginType string `json:"type"`
Master string `json:"master"`
IPAM ipamConfig `json:"ipam"`
MTU int `json:"mtu,omitempty"`
Capabilities map[string]bool `json:"capabilities"`
}
// firewallConfig describes the firewall plugin
type firewallConfig struct {
PluginType string `json:"type"`
Backend string `json:"backend"`
}
// tuningConfig describes the tuning plugin
type tuningConfig struct {
PluginType string `json:"type"`
}
// dnsNameConfig describes the dns container name resolution plugin config
type dnsNameConfig struct {
PluginType string `json:"type"`
DomainName string `json:"domainName"`
Capabilities map[string]bool `json:"capabilities"`
}
// podmanMachineConfig enables port handling on the host OS
type podmanMachineConfig struct {
PluginType string `json:"type"`
Capabilities map[string]bool `json:"capabilities"`
}
// ncList describes a generic map
type ncList map[string]interface{}
// newNcList creates a generic map of values with string
// keys and adds in version and network name
func newNcList(name, version string, labels, options map[string]string) ncList {
n := ncList{}
n["cniVersion"] = version
n["name"] = name
args := map[string]map[string]string{}
if len(labels) > 0 {
args[podmanLabelKey] = labels
}
if len(options) > 0 {
args[podmanOptionsKey] = options
}
if len(args) > 0 {
n["args"] = args
}
return n
}
// newHostLocalBridge creates a new LocalBridge for host-local
func newHostLocalBridge(name string, isGateWay, ipMasq bool, mtu int, vlan int, ipamConf ipamConfig) *hostLocalBridge {
caps := make(map[string]bool)
caps["ips"] = true
bridge := hostLocalBridge{
PluginType: "bridge",
BrName: name,
IsGW: isGateWay,
IPMasq: ipMasq,
MTU: mtu,
HairpinMode: true,
Vlan: vlan,
IPAM: ipamConf,
}
// if we use host-local set the ips cap to ensure we can set static ips via runtime config
if ipamConf.PluginType == types.HostLocalIPAMDriver {
bridge.Capabilities = caps
}
return &bridge
}
// newIPAMHostLocalConf creates a new IPAMHostLocal configuration
func newIPAMHostLocalConf(routes []ipamRoute, ipamRanges [][]ipamLocalHostRangeConf) ipamConfig {
ipamConf := ipamConfig{
PluginType: "host-local",
Routes: routes,
}
ipamConf.Ranges = ipamRanges
return ipamConf
}
// newIPAMLocalHostRange create a new IPAM range
func newIPAMLocalHostRange(subnet types.IPNet, leaseRange *types.LeaseRange, gw net.IP) *ipamLocalHostRangeConf {
hostRange := &ipamLocalHostRangeConf{
Subnet: subnet.String(),
}
// an user provided a range, we add it here
if leaseRange != nil {
if leaseRange.StartIP != nil {
hostRange.RangeStart = leaseRange.StartIP.String()
}
if leaseRange.EndIP != nil {
hostRange.RangeStart = leaseRange.EndIP.String()
}
}
if gw != nil {
hostRange.Gateway = gw.String()
}
return hostRange
}
// newIPAMRoute creates a new IPAM route configuration
// nolint:interfacer
func newIPAMRoute(r *net.IPNet) ipamRoute {
return ipamRoute{Dest: r.String()}
}
// newIPAMDefaultRoute creates a new IPAMDefault route of
// 0.0.0.0/0 for IPv4 or ::/0 for IPv6
func newIPAMDefaultRoute(isIPv6 bool) (ipamRoute, error) {
route := defaultIPv4Route
if isIPv6 {
route = defaultIPv6Route
}
_, n, err := net.ParseCIDR(route)
if err != nil {
return ipamRoute{}, err
}
return newIPAMRoute(n), nil
}
// newPortMapPlugin creates a predefined, default portmapping
// configuration
func newPortMapPlugin() portMapConfig {
caps := make(map[string]bool)
caps["portMappings"] = true
p := portMapConfig{
PluginType: "portmap",
Capabilities: caps,
}
return p
}
// newFirewallPlugin creates a generic firewall plugin
func newFirewallPlugin() firewallConfig {
return firewallConfig{
PluginType: "firewall",
}
}
// newTuningPlugin creates a generic tuning section
func newTuningPlugin() tuningConfig {
return tuningConfig{
PluginType: "tuning",
}
}
// newDNSNamePlugin creates the dnsname config with a given
// domainname
func newDNSNamePlugin(domainName string) dnsNameConfig {
caps := make(map[string]bool, 1)
caps["aliases"] = true
return dnsNameConfig{
PluginType: "dnsname",
DomainName: domainName,
Capabilities: caps,
}
}
// hasDNSNamePlugin looks to see if the dnsname cni plugin is present
func hasDNSNamePlugin(paths []string) bool {
for _, p := range paths {
if _, err := os.Stat(filepath.Join(p, "dnsname")); err == nil {
return true
}
}
return false
}
// newMacVLANPlugin creates a macvlanconfig with a given device name
func newMacVLANPlugin(device string, mtu int, ipam ipamConfig) macVLANConfig {
m := macVLANConfig{
PluginType: "macvlan",
IPAM: ipam,
}
if mtu > 0 {
m.MTU = mtu
}
// CNI is supposed to use the default route if a
// parent device is not provided
if len(device) > 0 {
m.Master = device
}
caps := make(map[string]bool)
caps["ips"] = true
// if we use host-local set the ips cap to ensure we can set static ips via runtime config
if ipam.PluginType == types.HostLocalIPAMDriver {
m.Capabilities = caps
}
return m
}
func newPodmanMachinePlugin() podmanMachineConfig {
caps := make(map[string]bool, 1)
caps["portMappings"] = true
return podmanMachineConfig{
PluginType: "podman-machine",
Capabilities: caps,
}
}

View File

@ -0,0 +1,313 @@
// +build linux
package cni
import (
"net"
"os"
"github.com/containers/podman/v3/libpod/define"
"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"
"github.com/vishvananda/netlink"
)
// NetworkCreate will take a partial filled Network and fill the
// missing fields. It creates the Network and returns the full Network.
func (n *cniNetwork) 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, true)
if err != nil {
return types.Network{}, err
}
// add the new network to the map
n.networks[network.libpodNet.Name] = network
return *network.libpodNet, nil
}
func (n *cniNetwork) networkCreate(net types.Network, writeToDisk bool) (*network, error) {
// if no driver is set use the default one
if net.Driver == "" {
net.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 net.ID != "" {
return nil, errors.Wrap(define.ErrInvalidArg, "ID can not be set for network create")
}
if net.Labels == nil {
net.Labels = map[string]string{}
}
if net.Options == nil {
net.Options = map[string]string{}
}
if net.IPAMOptions == nil {
net.IPAMOptions = map[string]string{}
}
var name string
var err error
// validate the name when given
if net.Name != "" {
if !define.NameRegex.MatchString(net.Name) {
return nil, errors.Wrapf(define.RegexError, "network name %s invalid", net.Name)
}
if _, ok := n.networks[net.Name]; ok {
return nil, errors.Wrapf(define.ErrNetworkExists, "network name %s already used", net.Name)
}
} else {
name, err = n.getFreeDeviceName()
if err != nil {
return nil, err
}
net.Name = name
}
switch net.Driver {
case types.BridgeNetworkDriver:
// if the name was created with getFreeDeviceName set the interface to it as well
if name != "" && net.NetworkInterface == "" {
net.NetworkInterface = name
}
err = n.createBridge(&net)
if err != nil {
return nil, err
}
case types.MacVLANNetworkDriver:
err = createMacVLAN(&net)
if err != nil {
return nil, err
}
default:
return nil, errors.Wrapf(define.ErrInvalidArg, "unsupported driver %s", net.Driver)
}
for i := range net.Subnets {
err := validateSubnet(&net.Subnets[i], !net.Internal)
if err != nil {
return nil, err
}
if util.IsIPv6(net.Subnets[i].Subnet.IP) {
net.IPv6Enabled = true
}
}
// generate the network ID
net.ID = getNetworkIDFromName(net.Name)
// FIXME: Should this be a hard error?
if net.DNSEnabled && net.Internal && hasDNSNamePlugin(n.cniPluginDirs) {
logrus.Warnf("dnsname and internal networks are incompatible. dnsname plugin not configured for network %s", net.Name)
net.DNSEnabled = false
}
cniConf, path, err := n.createCNIConfigListFromNetwork(&net, writeToDisk)
if err != nil {
return nil, err
}
return &network{cniNet: cniConf, libpodNet: &net, filename: path}, nil
}
// NetworkRemove will remove the Network with the given name or ID.
// It does not ensure that the network is unused.
func (n *cniNetwork) 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.libpodNet.Name == n.defaultNetwork {
return errors.Errorf("default network %s cannot be removed", n.defaultNetwork)
}
// Remove the bridge network interface on the host.
if network.libpodNet.Driver == types.BridgeNetworkDriver {
link, err := netlink.LinkByName(network.libpodNet.NetworkInterface)
if err == nil {
err = netlink.LinkDel(link)
// only log the error, it is not fatal
if err != nil {
logrus.Infof("failed to remove network interface %s: %v", network.libpodNet.NetworkInterface, err)
}
}
}
file := network.filename
delete(n.networks, network.libpodNet.Name)
return os.Remove(file)
}
// 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 *cniNetwork) 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.libpodNet) {
continue outer
}
}
networks = append(networks, *net.libpodNet)
}
return networks, nil
}
// NetworkInspect will return the Network with the given name or ID.
func (n *cniNetwork) 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.libpodNet, nil
}
func createMacVLAN(network *types.Network) error {
if network.Internal {
return errors.New("internal is not supported with macvlan")
}
if network.NetworkInterface != "" {
interfaceNames, err := util.GetLiveNetworkNames()
if err != nil {
return err
}
if !pkgutil.StringInSlice(network.NetworkInterface, interfaceNames) {
return errors.Errorf("parent interface %s does not exists", network.NetworkInterface)
}
}
if len(network.Subnets) == 0 {
network.IPAMOptions["driver"] = types.DHCPIPAMDriver
} else {
network.IPAMOptions["driver"] = types.HostLocalIPAMDriver
}
return nil
}
func (n *cniNetwork) createBridge(network *types.Network) error {
if network.NetworkInterface != "" {
bridges := n.getBridgeInterfaceNames()
if pkgutil.StringInSlice(network.NetworkInterface, bridges) {
return errors.Errorf("bridge name %s already in use", network.NetworkInterface)
}
if !define.NameRegex.MatchString(network.NetworkInterface) {
return errors.Wrapf(define.RegexError, "bridge name %s invalid", network.NetworkInterface)
}
} else {
var err error
network.NetworkInterface, err = n.getFreeDeviceName()
if err != nil {
return err
}
}
if len(network.Subnets) == 0 {
freeSubnet, err := n.getFreeIPv4NetworkSubnet()
if err != nil {
return err
}
network.Subnets = append(network.Subnets, *freeSubnet)
}
// ipv6 enabled means dual stack, check if we already have
// a ipv4 or ipv6 subnet and add one if not.
if network.IPv6Enabled {
ipv4 := false
ipv6 := false
for _, subnet := range network.Subnets {
if util.IsIPv6(subnet.Subnet.IP) {
ipv6 = true
}
if util.IsIPv4(subnet.Subnet.IP) {
ipv4 = true
}
}
if !ipv4 {
freeSubnet, err := n.getFreeIPv4NetworkSubnet()
if err != nil {
return err
}
network.Subnets = append(network.Subnets, *freeSubnet)
}
if !ipv6 {
freeSubnet, err := n.getFreeIPv6NetworkSubnet()
if err != nil {
return err
}
network.Subnets = append(network.Subnets, *freeSubnet)
}
}
network.IPAMOptions["driver"] = types.HostLocalIPAMDriver
return nil
}
// validateSubnet will validate a given Subnet. It checks if the
// given gateway and lease range are part of this subnet. If the
// gateway is empty and addGateway is true it will get the first
// available ip in the subnet assigned.
func validateSubnet(s *types.Subnet, addGateway bool) error {
if s == nil {
return errors.New("subnet is nil")
}
// Reparse to ensure subnet is valid.
// Do not use types.ParseCIDR() because we want the ip to be
// the network address and not a random ip in the subnet.
_, net, err := net.ParseCIDR(s.Subnet.String())
if err != nil {
return errors.Wrap(err, "subnet invalid")
}
s.Subnet = types.IPNet{IPNet: *net}
if s.Gateway != nil {
if !s.Subnet.Contains(s.Gateway) {
return errors.Errorf("gateway %s not in subnet %s", s.Gateway, &s.Subnet)
}
} else if addGateway {
ip, err := util.FirstIPInSubnet(net)
if err != nil {
return err
}
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.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)
}
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,340 @@
// +build linux
package cni
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"net"
"os"
"strings"
"github.com/containernetworking/cni/libcni"
"github.com/containers/podman/v3/libpod/define"
"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/containers/storage/pkg/lockfile"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type cniNetwork struct {
// cniConfigDir is directory where the cni config files are stored.
cniConfigDir string
// cniPluginDirs is a list of directories where cni should look for the plugins.
cniPluginDirs []string
cniConf *libcni.CNIConfig
// 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
// networks is a map with loaded networks, the key is the network name
networks map[string]*network
}
type network struct {
// filename is the full path to the cni config file on disk
filename string
libpodNet *types.Network
cniNet *libcni.NetworkConfigList
}
type InitConfig struct {
// CNIConfigDir is directory where the cni config files are stored.
CNIConfigDir string
// CNIPluginDirs is a list of directories where cni should look for the plugins.
CNIPluginDirs []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
}
// NewCNINetworkInterface creates the ContainerNetwork interface for the CNI backend.
// Note: The networks are not loaded from disk until a method is called.
func NewCNINetworkInterface(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")
}
cni := libcni.NewCNIConfig(conf.CNIPluginDirs, &cniExec{})
n := &cniNetwork{
cniConfigDir: conf.CNIConfigDir,
cniPluginDirs: conf.CNIPluginDirs,
cniConf: cni,
defaultNetwork: defaultNetworkName,
defaultSubnet: defaultNet,
isMachine: conf.IsMachine,
lock: lock,
}
return n, nil
}
func (n *cniNetwork) loadNetworks() error {
// skip loading networks if they are already loaded
if n.networks != nil {
return nil
}
// FIXME: do we have to support other file types as well, e.g. .conf?
files, err := libcni.ConfFiles(n.cniConfigDir, []string{".conflist"})
if err != nil {
return err
}
networks := make(map[string]*network, len(files))
for _, file := range files {
conf, err := libcni.ConfListFromFile(file)
if err != nil {
// do not log ENOENT errors
if !os.IsNotExist(err) {
logrus.Warnf("Error loading CNI config file %s: %v", file, err)
}
continue
}
if !define.NameRegex.MatchString(conf.Name) {
logrus.Warnf("CNI config list %s has invalid name, skipping: %v", file, define.RegexError)
continue
}
if _, err := n.cniConf.ValidateNetworkList(context.Background(), conf); err != nil {
logrus.Warnf("Error validating CNI config file %s: %v", file, err)
continue
}
if val, ok := networks[conf.Name]; ok {
logrus.Warnf("CNI config list %s has the same network name as %s, skipping", file, val.filename)
continue
}
net, err := createNetworkFromCNIConfigList(conf, file)
if err != nil {
logrus.Errorf("CNI config list %s could not be converted to a libpod config, skipping: %v", file, err)
continue
}
logrus.Tracef("Successfully loaded network %s: %v", net.Name, net)
networkInfo := network{
filename: file,
cniNet: conf,
libpodNet: net,
}
networks[net.Name] = &networkInfo
}
// 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 (n *cniNetwork) createDefaultNetwork() (*network, error) {
net := types.Network{
Name: n.defaultNetwork,
NetworkInterface: "cni-podman0",
Driver: types.BridgeNetworkDriver,
Subnets: []types.Subnet{
{Subnet: n.defaultSubnet},
},
}
return n.networkCreate(net, false)
}
// 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 *cniNetwork) getNetwork(nameOrID string) (*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 *network
for _, val := range n.networks {
// This should not happen because we already looked up the map by name but check anyway.
if val.libpodNet.Name == nameOrID {
return val, nil
}
if strings.HasPrefix(val.libpodNet.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)
}
// getNetworkIDFromName creates a network ID from the name. It is just the
// sha256 hash so it is not safe but it should be safe enough for our use case.
func getNetworkIDFromName(name string) string {
hash := sha256.Sum256([]byte(name))
return hex.EncodeToString(hash[:])
}
// getFreeIPv6NetworkSubnet returns a unused ipv4 subnet
func (n *cniNetwork) getFreeIPv4NetworkSubnet() (*types.Subnet, error) {
networks, err := n.getUsedSubnets()
if err != nil {
return nil, err
}
// the default podman network is 10.88.0.0/16
// start locking for free /24 networks
network := &net.IPNet{
IP: net.IP{10, 89, 0, 0},
Mask: net.IPMask{255, 255, 255, 0},
}
// TODO: make sure to not use public subnets
for {
if intersectsConfig := util.NetworkIntersectsWithNetworks(network, networks); !intersectsConfig {
logrus.Debugf("found free ipv4 network subnet %s", network.String())
return &types.Subnet{
Subnet: types.IPNet{IPNet: *network},
}, nil
}
network, err = util.NextSubnet(network)
if err != nil {
return nil, err
}
}
}
// getFreeIPv6NetworkSubnet returns a unused ipv6 subnet
func (n *cniNetwork) getFreeIPv6NetworkSubnet() (*types.Subnet, error) {
networks, err := n.getUsedSubnets()
if err != nil {
return nil, err
}
// FIXME: Is 10000 fine as limit? We should prevent an endless loop.
for i := 0; i < 10000; i++ {
// RFC4193: Choose the ipv6 subnet random and NOT sequentially.
network, err := util.GetRandomIPv6Subnet()
if err != nil {
return nil, err
}
if intersectsConfig := util.NetworkIntersectsWithNetworks(&network, networks); !intersectsConfig {
logrus.Debugf("found free ipv6 network subnet %s", network.String())
return &types.Subnet{
Subnet: types.IPNet{IPNet: network},
}, nil
}
}
return nil, errors.New("failed to get random ipv6 subnet")
}
// getUsedSubnets returns a list of all used subnets by network
// configs and interfaces on the host.
func (n *cniNetwork) getUsedSubnets() ([]*net.IPNet, error) {
// first, load all used subnets from network configs
subnets := make([]*net.IPNet, 0, len(n.networks))
for _, val := range n.networks {
for _, subnet := range val.libpodNet.Subnets {
// nolint:exportloopref
subnets = append(subnets, &subnet.Subnet.IPNet)
}
}
// second, load networks from the current system
liveSubnets, err := util.GetLiveNetworkSubnets()
if err != nil {
return nil, err
}
return append(subnets, liveSubnets...), nil
}
// getFreeDeviceName returns a free device name which can
// be used for new configs as name and bridge interface name
func (n *cniNetwork) getFreeDeviceName() (string, error) {
bridgeNames := n.getBridgeInterfaceNames()
netNames := n.getUsedNetworkNames()
liveInterfaces, err := util.GetLiveNetworkNames()
if err != nil {
return "", nil
}
names := make([]string, 0, len(bridgeNames)+len(netNames)+len(liveInterfaces))
names = append(names, bridgeNames...)
names = append(names, netNames...)
names = append(names, liveInterfaces...)
// FIXME: Is a limit fine?
// Start by 1, 0 is reserved for the default network
for i := 1; i < 1000000; i++ {
deviceName := fmt.Sprintf("%s%d", cniDeviceName, i)
if !pkgutil.StringInSlice(deviceName, names) {
logrus.Debugf("found free device name %s", deviceName)
return deviceName, nil
}
}
return "", errors.New("could not find free device name, to many iterations")
}
// getUsedNetworkNames returns all network names already used
// by network configs
func (n *cniNetwork) getUsedNetworkNames() []string {
names := make([]string, 0, len(n.networks))
for _, val := range n.networks {
names = append(names, val.libpodNet.Name)
}
return names
}
// getUsedNetworkNames returns all bridge device names already used
// by network configs
func (n *cniNetwork) getBridgeInterfaceNames() []string {
names := make([]string, 0, len(n.networks))
for _, val := range n.networks {
if val.libpodNet.Driver == types.BridgeNetworkDriver {
names = append(names, val.libpodNet.NetworkInterface)
}
}
return names
}

309
libpod/network/cni/run.go Normal file
View File

@ -0,0 +1,309 @@
// +build linux
package cni
import (
"context"
"net"
"os"
"strings"
"github.com/containernetworking/cni/libcni"
cnitypes "github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/plugins/pkg/ns"
"github.com/containers/podman/v3/libpod/define"
"github.com/containers/podman/v3/libpod/network/types"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/vishvananda/netlink"
)
// Setup will setup the container network namespace. It returns
// a map of StatusBlocks, the key is the network name.
func (n *cniNetwork) 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
}
if namespacePath == "" {
return nil, errors.New("namespacePath is empty")
}
if options.ContainerID == "" {
return nil, errors.New("ContainerID is empty")
}
if len(options.Networks) == 0 {
return nil, errors.New("must specify at least one network")
}
for name, netOpts := range options.Networks {
network := n.networks[name]
if network == nil {
return nil, errors.Wrapf(define.ErrNoSuchNetwork, "network %s", name)
}
err := validatePerNetworkOpts(network, netOpts)
if err != nil {
return nil, err
}
}
// set the loopback adapter up in the container netns
err = ns.WithNetNSPath(namespacePath, func(_ ns.NetNS) error {
link, err := netlink.LinkByName("lo")
if err == nil {
err = netlink.LinkSetUp(link)
}
return err
})
if err != nil {
return nil, errors.Wrapf(err, "failed to set the loopback adapter up")
}
var retErr error
teardownOpts := options
teardownOpts.Networks = map[string]types.PerNetworkOptions{}
// make sure to teardown the already connected networks on error
defer func() {
if retErr != nil {
if len(teardownOpts.Networks) > 0 {
err := n.teardown(namespacePath, types.TeardownOptions(teardownOpts))
if err != nil {
logrus.Warn(err)
}
}
}
}()
ports, err := convertSpecgenPortsToCNIPorts(options.PortMappings)
if err != nil {
return nil, err
}
results := make(map[string]types.StatusBlock, len(options.Networks))
for name, netOpts := range options.Networks {
network := n.networks[name]
rt := getRuntimeConfig(namespacePath, options.ContainerName, options.ContainerID, name, ports, netOpts)
// If we have more than one static ip we need parse the ips via runtime config,
// make sure to add the ips capability to the first plugin otherwise it doesn't get the ips
if len(netOpts.StaticIPs) > 0 && !network.cniNet.Plugins[0].Network.Capabilities["ips"] {
caps := make(map[string]interface{})
caps["capabilities"] = map[string]bool{"ips": true}
network.cniNet.Plugins[0], retErr = libcni.InjectConf(network.cniNet.Plugins[0], caps)
if retErr != nil {
return nil, retErr
}
}
var res cnitypes.Result
res, retErr = n.cniConf.AddNetworkList(context.Background(), network.cniNet, rt)
// Add this network to teardown opts since it is now connected.
// Also add this if an errors was returned since we want to call teardown on this regardless.
teardownOpts.Networks[name] = netOpts
if retErr != nil {
return nil, retErr
}
var cnires *current.Result
cnires, retErr = current.GetResult(res)
if retErr != nil {
return nil, retErr
}
logrus.Debugf("cni result for container %s network %s: %v", options.ContainerID, name, cnires)
var status types.StatusBlock
status, retErr = cniResultToStatus(cnires)
if retErr != nil {
return nil, retErr
}
results[name] = status
}
return results, nil
}
// cniResultToStatus convert the cni result to status block
func cniResultToStatus(cniResult *current.Result) (types.StatusBlock, error) {
result := types.StatusBlock{}
nameservers := make([]net.IP, 0, len(cniResult.DNS.Nameservers))
for _, nameserver := range cniResult.DNS.Nameservers {
ip := net.ParseIP(nameserver)
if ip == nil {
return result, errors.Errorf("failed to parse cni nameserver ip %s", nameserver)
}
nameservers = append(nameservers, ip)
}
result.DNSServerIPs = nameservers
result.DNSSearchDomains = cniResult.DNS.Search
interfaces := make(map[string]types.NetInterface)
for _, ip := range cniResult.IPs {
if ip.Interface == nil {
// we do no expect ips without an interface
continue
}
if len(cniResult.Interfaces) <= *ip.Interface {
return result, errors.Errorf("invalid cni result, interface index %d out of range", *ip.Interface)
}
cniInt := cniResult.Interfaces[*ip.Interface]
netInt, ok := interfaces[cniInt.Name]
if ok {
netInt.Networks = append(netInt.Networks, types.NetAddress{
Subnet: types.IPNet{IPNet: ip.Address},
Gateway: ip.Gateway,
})
interfaces[cniInt.Name] = netInt
} else {
mac, err := net.ParseMAC(cniInt.Mac)
if err != nil {
return result, err
}
interfaces[cniInt.Name] = types.NetInterface{
MacAddress: mac,
Networks: []types.NetAddress{{
Subnet: types.IPNet{IPNet: ip.Address},
Gateway: ip.Gateway,
}},
}
}
}
result.Interfaces = interfaces
return result, nil
}
// validatePerNetworkOpts checks that all given static ips are in a subnet on this network
func validatePerNetworkOpts(network *network, netOpts types.PerNetworkOptions) error {
if netOpts.InterfaceName == "" {
return errors.Errorf("interface name on network %s is empty", network.libpodNet.Name)
}
outer:
for _, ip := range netOpts.StaticIPs {
for _, s := range network.libpodNet.Subnets {
if s.Subnet.Contains(ip) {
continue outer
}
}
return errors.Errorf("requested static ip %s not in any subnet on network %s", ip.String(), network.libpodNet.Name)
}
if len(netOpts.Aliases) > 0 && !network.libpodNet.DNSEnabled {
return errors.New("cannot set aliases on a network without dns enabled")
}
return nil
}
func getRuntimeConfig(netns, conName, conID, networkName string, ports []cniPortMapEntry, opts types.PerNetworkOptions) *libcni.RuntimeConf {
rt := &libcni.RuntimeConf{
ContainerID: conID,
NetNS: netns,
IfName: opts.InterfaceName,
Args: [][2]string{
{"IgnoreUnknown", "1"},
// FIXME: Should we set the K8S args?
//{"K8S_POD_NAMESPACE", conName},
//{"K8S_POD_INFRA_CONTAINER_ID", conID},
// K8S_POD_NAME is used by dnsname to get the container name
{"K8S_POD_NAME", conName},
},
CapabilityArgs: map[string]interface{}{},
}
// Propagate environment CNI_ARGS
for _, kvpairs := range strings.Split(os.Getenv("CNI_ARGS"), ";") {
if keyval := strings.SplitN(kvpairs, "=", 2); len(keyval) == 2 {
rt.Args = append(rt.Args, [2]string{keyval[0], keyval[1]})
}
}
// Add mac address to cni args
if len(opts.StaticMAC) > 0 {
rt.Args = append(rt.Args, [2]string{"MAC", opts.StaticMAC.String()})
}
if len(opts.StaticIPs) == 1 {
// Add a single IP to the args field. CNI plugins < 1.0.0
// do not support multiple ips via capability args.
rt.Args = append(rt.Args, [2]string{"IP", opts.StaticIPs[0].String()})
} else if len(opts.StaticIPs) > 1 {
// Set the static ips in the capability args
// to support more than one static ip per network.
rt.CapabilityArgs["ips"] = opts.StaticIPs
}
// Set network aliases for the dnsname plugin.
if len(opts.Aliases) > 0 {
rt.CapabilityArgs["aliases"] = map[string][]string{
networkName: opts.Aliases,
}
}
// Set PortMappings in Capabilities
if len(ports) > 0 {
rt.CapabilityArgs["portMappings"] = ports
}
return rt
}
// Teardown will teardown the container network namespace.
func (n *cniNetwork) Teardown(namespacePath string, options types.TeardownOptions) error {
n.lock.Lock()
defer n.lock.Unlock()
err := n.loadNetworks()
if err != nil {
return err
}
return n.teardown(namespacePath, options)
}
func (n *cniNetwork) teardown(namespacePath string, options types.TeardownOptions) error {
// Note: An empty namespacePath is allowed because some plugins
// still need teardown, for example ipam should remove used ip allocations.
ports, err := convertSpecgenPortsToCNIPorts(options.PortMappings)
if err != nil {
return err
}
var multiErr *multierror.Error
for name, netOpts := range options.Networks {
rt := getRuntimeConfig(namespacePath, options.ContainerName, options.ContainerID, name, ports, netOpts)
cniConfList, newRt, err := getCachedNetworkConfig(n.cniConf, name, rt)
if err == nil {
rt = newRt
} else {
logrus.Warnf("failed to load cached network config: %v, falling back to loading network %s from disk", err, name)
network := n.networks[name]
if network == nil {
multiErr = multierror.Append(multiErr, errors.Wrapf(define.ErrNoSuchNetwork, "network %s", name))
continue
}
cniConfList = network.cniNet
}
err = n.cniConf.DelNetworkList(context.Background(), cniConfList, rt)
if err != nil {
multiErr = multierror.Append(multiErr, err)
}
}
return multiErr.ErrorOrNil()
}
func getCachedNetworkConfig(cniConf *libcni.CNIConfig, name string, rt *libcni.RuntimeConf) (*libcni.NetworkConfigList, *libcni.RuntimeConf, error) {
cniConfList := &libcni.NetworkConfigList{
Name: name,
}
confBytes, rt, err := cniConf.GetNetworkListCachedConfig(cniConfList, rt)
if err != nil {
return nil, nil, err
} else if confBytes == nil {
return nil, nil, errors.Errorf("network %s not found in CNI cache", name)
}
cniConfList, err = libcni.ConfListFromBytes(confBytes)
if err != nil {
return nil, nil, err
}
return cniConfList, rt, nil
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
{
"cniVersion": "0.4.0",
"name": "bridge",
"plugins": [
{
"type": "bridge",
"bridge": "cni-podman9",
"isGateway": true,
"ipMasq": true,
"hairpinMode": true,
"ipam": {
"type": "host-local",
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"ranges": [
[
{
"subnet": "10.89.8.0/24",
"gateway": "10.89.8.1"
}
]
]

View File

@ -0,0 +1,51 @@
{
"cniVersion": "0.4.0",
"name": "invalidgw",
"plugins": [
{
"type": "bridge",
"bridge": "cni-podman8",
"isGateway": true,
"ipMasq": true,
"hairpinMode": true,
"ipam": {
"type": "host-local",
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"ranges": [
[
{
"subnet": "10.89.8.0/24",
"gateway": "10.89.8",
"rangeStart": "10.89.8.20",
"rangeEnd": "10.89.8.50"
}
]
]
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
},
{
"type": "firewall",
"backend": ""
},
{
"type": "tuning"
},
{
"type": "dnsname",
"domainName": "dns.podman",
"capabilities": {
"aliases": true
}
}
]
}

View File

@ -0,0 +1,49 @@
{
"cniVersion": "0.4.0",
"name": "bridge@123",
"plugins": [
{
"type": "bridge",
"bridge": "cni-podman9",
"isGateway": true,
"ipMasq": true,
"hairpinMode": true,
"ipam": {
"type": "host-local",
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"ranges": [
[
{
"subnet": "10.89.8.0/24",
"gateway": "10.89.8.1"
}
]
]
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
},
{
"type": "firewall",
"backend": ""
},
{
"type": "tuning"
},
{
"type": "dnsname",
"domainName": "dns.podman",
"capabilities": {
"aliases": true
}
}
]
}

View File

@ -0,0 +1,48 @@
{
"cniVersion": "0.4.0",
"plugins": [
{
"type": "bridge",
"bridge": "cni-podman9",
"isGateway": true,
"ipMasq": true,
"hairpinMode": true,
"ipam": {
"type": "host-local",
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"ranges": [
[
{
"subnet": "10.89.8.0/24",
"gateway": "10.89.8.1"
}
]
]
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
},
{
"type": "firewall",
"backend": ""
},
{
"type": "tuning"
},
{
"type": "dnsname",
"domainName": "dns.podman",
"capabilities": {
"aliases": true
}
}
]
}

View File

@ -0,0 +1,5 @@
{
"cniVersion": "0.4.0",
"name": "bridge",
"plugins": []
}

View File

@ -0,0 +1,49 @@
{
"cniVersion": "0.4.0",
"name": "bridge",
"plugins": [
{
"type": "bridge",
"bridge": "cni-podman9",
"isGateway": true,
"ipMasq": true,
"hairpinMode": true,
"ipam": {
"type": "host-local",
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"ranges": [
[
{
"subnet": "10.89.8.0/24",
"gateway": "10.89.8.1"
}
]
]
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
},
{
"type": "firewall",
"backend": ""
},
{
"type": "tuning"
},
{
"type": "dnsname",
"domainName": "dns.podman",
"capabilities": {
"aliases": true
}
}
]
}

View File

@ -0,0 +1,49 @@
{
"cniVersion": "0.4.0",
"name": "bridge",
"plugins": [
{
"type": "bridge",
"bridge": "cni-podman9",
"isGateway": true,
"ipMasq": true,
"hairpinMode": true,
"ipam": {
"type": "host-local",
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"ranges": [
[
{
"subnet": "10.89.8.0/24",
"gateway": "10.89.8.1"
}
]
]
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
},
{
"type": "firewall",
"backend": ""
},
{
"type": "tuning"
},
{
"type": "dnsname",
"domainName": "dns.podman",
"capabilities": {
"aliases": true
}
}
]
}

View File

@ -0,0 +1,37 @@
{
"cniVersion": "0.4.0",
"name": "podman",
"plugins": [
{
"type": "bridge",
"bridge": "cni-podman0",
"isGateway": true,
"ipMasq": true,
"hairpinMode": true,
"ipam": {
"type": "host-local",
"routes": [{ "dst": "0.0.0.0/0" }],
"ranges": [
[
{
"subnet": "10.88.0.0/16",
"gateway": "10.88.0.1"
}
]
]
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
},
{
"type": "firewall"
},
{
"type": "tuning"
}
]
}

View File

@ -0,0 +1,51 @@
{
"cniVersion": "0.4.0",
"name": "bridge",
"plugins": [
{
"type": "bridge",
"bridge": "cni-podman9",
"isGateway": true,
"ipMasq": true,
"hairpinMode": true,
"ipam": {
"type": "host-local",
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"ranges": [
[
{
"subnet": "10.89.8.0/24",
"gateway": "10.89.8.1",
"rangeStart": "10.89.8.20",
"rangeEnd": "10.89.8.50"
}
]
]
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
},
{
"type": "firewall",
"backend": ""
},
{
"type": "tuning"
},
{
"type": "dnsname",
"domainName": "dns.podman",
"capabilities": {
"aliases": true
}
}
]
}

View File

@ -0,0 +1,58 @@
{
"cniVersion": "0.4.0",
"name": "dualstack",
"plugins": [
{
"type": "bridge",
"bridge": "cni-podman21",
"isGateway": true,
"ipMasq": true,
"hairpinMode": true,
"ipam": {
"type": "host-local",
"routes": [
{
"dst": "::/0"
},
{
"dst": "0.0.0.0/0"
}
],
"ranges": [
[
{
"subnet": "fd10:88:a::/64",
"gateway": "fd10:88:a::1"
}
],
[
{
"subnet": "10.89.19.0/24",
"gateway": "10.89.19.10"
}
]
]
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
},
{
"type": "firewall",
"backend": ""
},
{
"type": "tuning"
},
{
"type": "dnsname",
"domainName": "dns.podman",
"capabilities": {
"aliases": true
}
}
]
}

View File

@ -0,0 +1,40 @@
{
"cniVersion": "0.4.0",
"name": "internal",
"plugins": [
{
"type": "bridge",
"bridge": "cni-podman8",
"isGateway": false,
"hairpinMode": true,
"ipam": {
"type": "host-local",
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"ranges": [
[
{
"subnet": "10.89.7.0/24"
}
]
]
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
},
{
"type": "firewall",
"backend": ""
},
{
"type": "tuning"
}
]
}

View File

@ -0,0 +1,54 @@
{
"args": {
"podman_labels": {
"mykey": "value"
}
},
"cniVersion": "0.4.0",
"name": "label",
"plugins": [
{
"type": "bridge",
"bridge": "cni-podman15",
"isGateway": true,
"ipMasq": true,
"hairpinMode": true,
"ipam": {
"type": "host-local",
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"ranges": [
[
{
"subnet": "10.89.13.0/24",
"gateway": "10.89.13.1"
}
]
]
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
},
{
"type": "firewall",
"backend": ""
},
{
"type": "tuning"
},
{
"type": "dnsname",
"domainName": "dns.podman",
"capabilities": {
"aliases": true
}
}
]
}

View File

@ -0,0 +1,13 @@
{
"cniVersion": "0.4.0",
"name": "macvlan",
"plugins": [
{
"type": "macvlan",
"master": "lo",
"ipam": {
"type": "dhcp"
}
}
]
}

View File

@ -0,0 +1,14 @@
{
"cniVersion": "0.4.0",
"name": "macvlan_mtu",
"plugins": [
{
"type": "macvlan",
"master": "lo",
"ipam": {
"type": "dhcp"
},
"mtu": 1300
}
]
}

View File

@ -0,0 +1,49 @@
{
"cniVersion": "0.4.0",
"name": "mtu",
"plugins": [
{
"type": "bridge",
"bridge": "cni-podman13",
"isGateway": true,
"ipMasq": true,
"mtu": 1500,
"hairpinMode": true,
"ipam": {
"type": "host-local",
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"ranges": [
[
{
"subnet": "10.89.11.0/24"
}
]
]
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
},
{
"type": "firewall",
"backend": ""
},
{
"type": "tuning"
},
{
"type": "dnsname",
"domainName": "dns.podman",
"capabilities": {
"aliases": true
}
}
]
}

View File

@ -0,0 +1,50 @@
{
"cniVersion": "0.4.0",
"name": "vlan",
"plugins": [
{
"type": "bridge",
"bridge": "cni-podman14",
"isGateway": true,
"ipMasq": true,
"hairpinMode": true,
"vlan": 5,
"ipam": {
"type": "host-local",
"routes": [
{
"dst": "0.0.0.0/0"
}
],
"ranges": [
[
{
"subnet": "10.89.12.0/24",
"gateway": "10.89.12.1"
}
]
]
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
},
{
"type": "firewall",
"backend": ""
},
{
"type": "tuning"
},
{
"type": "dnsname",
"domainName": "dns.podman",
"capabilities": {
"aliases": true
}
}
]
}

View File

@ -0,0 +1,21 @@
package types
const (
// BridgeNetworkDriver defines the bridge driver
BridgeNetworkDriver = "bridge"
// DefaultNetworkDriver is the default network type used
DefaultNetworkDriver = BridgeNetworkDriver
// MacVLANNetworkDriver defines the macvlan driver
MacVLANNetworkDriver = "macvlan"
// IPAM drivers
// HostLocalIPAMDriver store the ip
HostLocalIPAMDriver = "host-local"
// DHCPIPAMDriver get subnet and ip from dhcp server
DHCPIPAMDriver = "dhcp"
// DefaultSubnet is the name that will be used for the default CNI network.
DefaultNetworkName = "podman"
// DefaultSubnet is the subnet that will be used for the default CNI network.
DefaultSubnet = "10.88.0.0/16"
)

View File

@ -0,0 +1,208 @@
package types
import (
"net"
"time"
)
type ContainerNetwork interface {
// NetworkCreate will take a partial filled Network and fill the
// missing fields. It creates the Network and returns the full Network.
NetworkCreate(Network) (Network, error)
// NetworkRemove will remove the Network with the given name or ID.
NetworkRemove(nameOrID string) error
// 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.
NetworkList(...FilterFunc) ([]Network, error)
// NetworkInspect will return the Network with the given name or ID.
NetworkInspect(nameOrID string) (Network, error)
// Setup will setup the container network namespace. It returns
// a map of StatusBlocks, the key is the network name.
Setup(namespacePath string, options SetupOptions) (map[string]StatusBlock, error)
// Teardown will teardown the container network namespace.
Teardown(namespacePath string, options TeardownOptions) error
}
// Network describes the Network attributes.
type Network struct {
// Name of the Network.
Name string `json:"name,omitempty"`
// ID of the Network.
ID string `json:"id,omitempty"`
// Driver for this Network, e.g. bridge, macvlan...
Driver string `json:"driver,omitempty"`
// InterfaceName is the network interface name on the host.
NetworkInterface string `json:"network_interface,omitempty"`
// Created contains the timestamp when this network was created.
// This is not guaranteed to stay exactly the same.
Created time.Time
// Subnets to use.
Subnets []Subnet `json:"subnets,omitempty"`
// IPv6Enabled if set to true an ipv6 subnet should be created for this net.
IPv6Enabled bool `json:"ipv6_enabled"`
// Internal is whether the Network should not have external routes
// to public or other Networks.
Internal bool `json:"internal"`
// DNSEnabled is whether name resolution is active for container on
// this Network.
DNSEnabled bool `json:"dns_enabled"`
// Labels is a set of key-value labels that have been applied to the
// Network.
Labels map[string]string `json:"labels,omitempty"`
// Options is a set of key-value options that have been applied to
// the Network.
Options map[string]string `json:"options,omitempty"`
// IPAMOptions contains options used for the ip assignment.
IPAMOptions map[string]string `json:"ipam_options,omitempty"`
}
// IPNet is used as custom net.IPNet type to add Marshal/Unmarshal methods.
type IPNet struct {
net.IPNet
}
// ParseCIDR parse a string to IPNet
func ParseCIDR(cidr string) (IPNet, error) {
ip, net, err := net.ParseCIDR(cidr)
if err != nil {
return IPNet{}, err
}
// convert to 4 bytes if ipv4
ipv4 := ip.To4()
if ipv4 != nil {
ip = ipv4
}
net.IP = ip
return IPNet{*net}, err
}
func (n *IPNet) MarshalText() ([]byte, error) {
return []byte(n.String()), nil
}
func (n *IPNet) UnmarshalText(text []byte) error {
net, err := ParseCIDR(string(text))
if err != nil {
return err
}
*n = net
return nil
}
type Subnet struct {
// Subnet for this Network.
Subnet IPNet `json:"subnet,omitempty"`
// Gateway IP for this Network.
Gateway net.IP `json:"gateway,omitempty"`
// LeaseRange contains the range where IP are leased. Optional.
LeaseRange *LeaseRange `json:"lease_range,omitempty"`
}
// LeaseRange contains the range where IP are leased.
type LeaseRange struct {
// StartIP first IP in the subnet which should be used to assign ips.
StartIP net.IP `json:"start_ip,omitempty"`
// EndIP last IP in the subnet which should be used to assign ips.
EndIP net.IP `json:"end_ip,omitempty"`
}
// StatusBlock contains the network information about a container
// connected to one Network.
type StatusBlock struct {
// Interfaces contains the created network interface in the container.
// The map key is the interface name.
Interfaces map[string]NetInterface `json:"interfaces,omitempty"`
// DNSServerIPs nameserver addresses which should be added to
// the containers resolv.conf file.
DNSServerIPs []net.IP `json:"dns_server_ips,omitempty"`
// DNSSearchDomains search domains which should be added to
// the containers resolv.conf file.
DNSSearchDomains []string `json:"dns_search_domains,omitempty"`
}
// NetInterface contains the settings for a given network interface.
type NetInterface struct {
// Networks list of assigned subnets with their gateway.
Networks []NetAddress `json:"networks,omitempty"`
// MacAddress for this Interface.
MacAddress net.HardwareAddr `json:"mac_address,omitempty"`
}
// NetAddress contains the subnet and gatway.
type NetAddress struct {
// Subnet of this NetAddress. Note that the subnet contains the
// actual ip of the net interface and not the network address.
Subnet IPNet `json:"subnet,omitempty"`
// Gateway for the Subnet. This can be nil if there is no gateway, e.g. internal network.
Gateway net.IP `json:"gateway,omitempty"`
}
// PerNetworkOptions are options which should be set on a per network basis.
type PerNetworkOptions struct {
// StaticIPv4 for this container. Optional.
StaticIPs []net.IP `json:"static_ips,omitempty"`
// Aliases contains a list of names which the dns server should resolve
// to this container. Can only be set when DNSEnabled is true on the Network.
// Optional.
Aliases []string `json:"aliases,omitempty"`
// StaticMac for this container. Optional.
StaticMAC net.HardwareAddr `json:"static_mac,omitempty"`
// InterfaceName for this container. Required.
InterfaceName string `json:"interface_name,omitempty"`
}
// NetworkOptions for a given container.
type NetworkOptions struct {
// ContainerID is the container id, used for iptables comments and ipam allocation.
ContainerID string `json:"container_id,omitempty"`
// ContainerName is the container name, used as dns name.
ContainerName string `json:"container_name,omitempty"`
// PortMappings contains the port mappings for this container
PortMappings []PortMapping `json:"port_mappings,omitempty"`
// Networks contains all networks with the PerNetworkOptions.
// The map should contain at least one element.
Networks map[string]PerNetworkOptions `json:"networks,omitempty"`
}
// PortMapping is one or more ports that will be mapped into the container.
type PortMapping struct {
// HostIP is the IP that we will bind to on the host.
// If unset, assumed to be 0.0.0.0 (all interfaces).
HostIP string `json:"host_ip,omitempty"`
// ContainerPort is the port number that will be exposed from the
// container.
// Mandatory.
ContainerPort uint16 `json:"container_port"`
// HostPort is the port number that will be forwarded from the host into
// the container.
// If omitted, a random port on the host (guaranteed to be over 1024)
// will be assigned.
HostPort uint16 `json:"host_port,omitempty"`
// Range is the number of ports that will be forwarded, starting at
// HostPort and ContainerPort and counting up.
// This is 1-indexed, so 1 is assumed to be a single port (only the
// Hostport:Containerport mapping will be added), 2 is two ports (both
// Hostport:Containerport and Hostport+1:Containerport+1), etc.
// If unset, assumed to be 1 (a single port).
// Both hostport + range and containerport + range must be less than
// 65536.
Range uint16 `json:"range,omitempty"`
// Protocol is the protocol forward.
// Must be either "tcp", "udp", and "sctp", or some combination of these
// separated by commas.
// If unset, assumed to be TCP.
Protocol string `json:"protocol,omitempty"`
}
type SetupOptions struct {
NetworkOptions
}
type TeardownOptions struct {
NetworkOptions
}
// FilterFunc can be passed to NetworkList to filter the networks.
type FilterFunc func(Network) bool

View File

@ -0,0 +1,55 @@
package util
import (
"strings"
"github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/podman/v3/pkg/util"
"github.com/pkg/errors"
)
func GenerateNetworkFilters(filters map[string][]string) ([]types.FilterFunc, error) {
filterFuncs := make([]types.FilterFunc, 0, len(filters))
for key, filterValues := range filters {
filterFunc, err := createFilterFuncs(key, filterValues)
if err != nil {
return nil, err
}
filterFuncs = append(filterFuncs, filterFunc)
}
return filterFuncs, nil
}
func createFilterFuncs(key string, filterValues []string) (types.FilterFunc, error) {
switch strings.ToLower(key) {
case "name":
// matches one name, regex allowed
return func(net types.Network) bool {
return util.StringMatchRegexSlice(net.Name, filterValues)
}, nil
case "label":
// matches all labels
return func(net types.Network) bool {
return util.MatchLabelFilters(filterValues, net.Labels)
}, nil
case "driver":
// matches network driver
return func(net types.Network) bool {
return util.StringInSlice(net.Driver, filterValues)
}, nil
case "id":
// matches part of one id
return func(net types.Network) bool {
return util.StringMatchRegexSlice(net.ID, filterValues)
}, nil
// FIXME: What should we do with the old plugin filter
// TODO: add dangling, dns enabled, internal filter
default:
return nil, errors.Errorf("invalid filter %q", key)
}
}

View File

@ -0,0 +1,34 @@
package util
import "net"
// GetLiveNetworkSubnets returns a slice of subnets representing what the system
// has defined as network interfaces
func GetLiveNetworkSubnets() ([]*net.IPNet, error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return nil, err
}
nets := make([]*net.IPNet, 0, len(addrs))
for _, address := range addrs {
_, n, err := net.ParseCIDR(address.String())
if err != nil {
return nil, err
}
nets = append(nets, n)
}
return nets, nil
}
// GetLiveNetworkNames returns a list of network interface names on the system
func GetLiveNetworkNames() ([]string, error) {
liveInterfaces, err := net.Interfaces()
if err != nil {
return nil, err
}
interfaceNames := make([]string, 0, len(liveInterfaces))
for _, i := range liveInterfaces {
interfaceNames = append(interfaceNames, i.Name)
}
return interfaceNames, nil
}

113
libpod/network/util/ip.go Normal file
View File

@ -0,0 +1,113 @@
package util
import (
"crypto/rand"
"net"
"github.com/pkg/errors"
)
// IsIPv6 returns true if netIP is IPv6.
func IsIPv6(netIP net.IP) bool {
return netIP != nil && netIP.To4() == nil
}
// IsIPv4 returns true if netIP is IPv4.
func IsIPv4(netIP net.IP) bool {
return netIP != nil && netIP.To4() != nil
}
func incByte(subnet *net.IPNet, idx int, shift uint) error {
if idx < 0 {
return errors.New("no more subnets left")
}
if subnet.IP[idx] == 255 {
subnet.IP[idx] = 0
return incByte(subnet, idx-1, 0)
}
subnet.IP[idx] += 1 << shift
return nil
}
// NextSubnet returns subnet incremented by 1
func NextSubnet(subnet *net.IPNet) (*net.IPNet, error) {
newSubnet := &net.IPNet{
IP: subnet.IP,
Mask: subnet.Mask,
}
ones, bits := newSubnet.Mask.Size()
if ones == 0 {
return nil, errors.Errorf("%s has only one subnet", subnet.String())
}
zeroes := uint(bits - ones)
shift := zeroes % 8
idx := ones/8 - 1
if idx < 0 {
idx = 0
}
if err := incByte(newSubnet, idx, shift); err != nil {
return nil, err
}
return newSubnet, nil
}
// LastIPInSubnet gets the last IP in a subnet
func LastIPInSubnet(addr *net.IPNet) (net.IP, error) { //nolint:interfacer
// re-parse to ensure clean network address
_, cidr, err := net.ParseCIDR(addr.String())
if err != nil {
return nil, err
}
ones, bits := cidr.Mask.Size()
if ones == bits {
return cidr.IP, nil
}
for i := range cidr.IP {
cidr.IP[i] = cidr.IP[i] | ^cidr.Mask[i]
}
return cidr.IP, nil
}
// FirstIPInSubnet gets the first IP in a subnet
func FirstIPInSubnet(addr *net.IPNet) (net.IP, error) { //nolint:interfacer
// re-parse to ensure clean network address
_, cidr, err := net.ParseCIDR(addr.String())
if err != nil {
return nil, err
}
ones, bits := cidr.Mask.Size()
if ones == bits {
return cidr.IP, nil
}
cidr.IP[len(cidr.IP)-1]++
return cidr.IP, nil
}
func NetworkIntersectsWithNetworks(n *net.IPNet, networklist []*net.IPNet) bool {
for _, nw := range networklist {
if networkIntersect(n, nw) {
return true
}
}
return false
}
func networkIntersect(n1, n2 *net.IPNet) bool {
return n2.Contains(n1.IP) || n1.Contains(n2.IP)
}
// GetRandomIPv6Subnet returns a random internal ipv6 subnet as described in RFC3879.
func GetRandomIPv6Subnet() (net.IPNet, error) {
ip := make(net.IP, 8, net.IPv6len)
// read 8 random bytes
_, err := rand.Read(ip)
if err != nil {
return net.IPNet{}, nil
}
// first byte must be FD as per RFC3879
ip[0] = 0xfd
// add 8 zero bytes
ip = append(ip, make([]byte, 8)...)
return net.IPNet{IP: ip, Mask: net.CIDRMask(64, 128)}, nil
}

View File

@ -0,0 +1,125 @@
package util
import (
"fmt"
"net"
"reflect"
"testing"
)
func parseCIDR(n string) *net.IPNet {
_, parsedNet, _ := net.ParseCIDR(n)
return parsedNet
}
func TestNextSubnet(t *testing.T) {
type args struct {
subnet *net.IPNet
}
tests := []struct {
name string
args args
want *net.IPNet
wantErr bool
}{
{"class b", args{subnet: parseCIDR("192.168.0.0/16")}, parseCIDR("192.169.0.0/16"), false},
{"class c", args{subnet: parseCIDR("192.168.1.0/24")}, parseCIDR("192.168.2.0/24"), false},
}
for _, tt := range tests {
test := tt
t.Run(test.name, func(t *testing.T) {
got, err := NextSubnet(test.args.subnet)
if (err != nil) != test.wantErr {
t.Errorf("NextSubnet() error = %v, wantErr %v", err, test.wantErr)
return
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("NextSubnet() got = %v, want %v", got, test.want)
}
})
}
}
func TestFirstIPInSubnet(t *testing.T) {
tests := []struct {
name string
args *net.IPNet
want net.IP
wantErr bool
}{
{"class b", parseCIDR("192.168.0.0/16"), net.ParseIP("192.168.0.1"), false},
{"class c", parseCIDR("192.168.1.0/24"), net.ParseIP("192.168.1.1"), false},
{"cidr /23", parseCIDR("192.168.0.0/23"), net.ParseIP("192.168.0.1"), false},
{"cidr /25", parseCIDR("192.168.1.0/25"), net.ParseIP("192.168.1.1"), false},
{"cidr /26", parseCIDR("172.16.1.128/26"), net.ParseIP("172.16.1.129"), false},
{"class a", parseCIDR("10.0.0.0/8"), net.ParseIP("10.0.0.1"), false},
{"cidr /32", parseCIDR("192.168.255.4/32"), net.ParseIP("192.168.255.4"), false},
{"cidr /31", parseCIDR("192.168.255.4/31"), net.ParseIP("192.168.255.5"), false},
}
for _, tt := range tests {
test := tt
t.Run(test.name, func(t *testing.T) {
got, err := FirstIPInSubnet(test.args)
if (err != nil) != test.wantErr {
t.Errorf("FirstIPInSubnet() error = %v, wantErr %v", err, test.wantErr)
return
}
if !got.Equal(test.want) {
t.Errorf("FirstIPInSubnet() got = %v, want %v", got, test.want)
}
})
}
}
func TestLastIPInSubnet(t *testing.T) {
tests := []struct {
name string
args *net.IPNet
want net.IP
wantErr bool
}{
{"class b", parseCIDR("192.168.0.0/16"), net.ParseIP("192.168.255.255"), false},
{"class c", parseCIDR("192.168.1.0/24"), net.ParseIP("192.168.1.255"), false},
{"cidr /23", parseCIDR("192.168.0.0/23"), net.ParseIP("192.168.1.255"), false},
{"cidr /25", parseCIDR("192.168.1.0/25"), net.ParseIP("192.168.1.127"), false},
{"cidr /26", parseCIDR("172.16.1.128/26"), net.ParseIP("172.16.1.191"), false},
{"class a", parseCIDR("10.0.0.0/8"), net.ParseIP("10.255.255.255"), false},
{"cidr /32", parseCIDR("192.168.255.4/32"), net.ParseIP("192.168.255.4"), false},
{"cidr /31", parseCIDR("192.168.255.4/31"), net.ParseIP("192.168.255.5"), false},
}
for _, tt := range tests {
test := tt
t.Run(test.name, func(t *testing.T) {
got, err := LastIPInSubnet(test.args)
if (err != nil) != test.wantErr {
t.Errorf("LastIPInSubnet() error = %v, wantErr %v", err, test.wantErr)
return
}
if !got.Equal(test.want) {
t.Errorf("LastIPInSubnet() got = %v, want %v", got, test.want)
}
})
}
}
func TestGetRandomIPv6Subnet(t *testing.T) {
for i := 0; i < 1000; i++ {
t.Run(fmt.Sprintf("GetRandomIPv6Subnet %d", i), func(t *testing.T) {
sub, err := GetRandomIPv6Subnet()
if err != nil {
t.Errorf("GetRandomIPv6Subnet() error should be nil: %v", err)
return
}
if sub.IP.To4() != nil {
t.Errorf("ip %s is not an ipv6 address", sub.IP)
}
if sub.IP[0] != 0xfd {
t.Errorf("ipv6 %s does not start with fd", sub.IP)
}
ones, bytes := sub.Mask.Size()
if ones != 64 || bytes != 128 {
t.Errorf("wrong network mask %v, it should be /64", sub.Mask)
}
})
}
}

View File

@ -8,6 +8,7 @@ import (
"github.com/containers/image/v5/types"
"github.com/containers/podman/v3/libpod/define"
nettypes "github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/podman/v3/pkg/specgen"
"github.com/containers/storage/pkg/archive"
"github.com/cri-o/ocicni/pkg/ocicni"
@ -208,7 +209,7 @@ type RestoreOptions struct {
Name string
TCPEstablished bool
ImportPrevious string
PublishPorts []specgen.PortMapping
PublishPorts []nettypes.PortMapping
Pod string
}

View File

@ -6,6 +6,7 @@ import (
buildahDefine "github.com/containers/buildah/define"
"github.com/containers/podman/v3/libpod/define"
"github.com/containers/podman/v3/libpod/events"
"github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/podman/v3/pkg/specgen"
"github.com/containers/storage/pkg/archive"
)
@ -40,7 +41,7 @@ type NetOptions struct {
DNSServers []net.IP
Network specgen.Namespace
NoHosts bool
PublishPorts []specgen.PortMapping
PublishPorts []types.PortMapping
StaticIP *net.IP
StaticMAC *net.HardwareAddr
// NetworkOptions are additional options for each network

View File

@ -12,6 +12,7 @@ import (
"github.com/containers/common/pkg/parse"
"github.com/containers/common/pkg/secrets"
"github.com/containers/image/v5/manifest"
"github.com/containers/podman/v3/libpod/network/types"
ann "github.com/containers/podman/v3/pkg/annotations"
"github.com/containers/podman/v3/pkg/specgen"
"github.com/containers/podman/v3/pkg/specgen/generate"
@ -588,8 +589,8 @@ func envVarValue(env v1.EnvVar, opts *CtrSpecGenOptions) (string, error) {
// getPodPorts converts a slice of kube container descriptions to an
// array of portmapping
func getPodPorts(containers []v1.Container) []specgen.PortMapping {
var infraPorts []specgen.PortMapping
func getPodPorts(containers []v1.Container) []types.PortMapping {
var infraPorts []types.PortMapping
for _, container := range containers {
for _, p := range container.Ports {
if p.HostPort != 0 && p.ContainerPort == 0 {
@ -598,7 +599,7 @@ func getPodPorts(containers []v1.Container) []specgen.PortMapping {
if p.Protocol == "" {
p.Protocol = "tcp"
}
portBinding := specgen.PortMapping{
portBinding := types.PortMapping{
HostPort: uint16(p.HostPort),
ContainerPort: uint16(p.ContainerPort),
Protocol: strings.ToLower(string(p.Protocol)),

View File

@ -7,6 +7,7 @@ import (
"strings"
"github.com/containers/common/libimage"
"github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/podman/v3/utils"
"github.com/containers/podman/v3/pkg/specgen"
@ -24,7 +25,7 @@ const (
// Parse port maps to OCICNI port mappings.
// Returns a set of OCICNI port mappings, and maps of utilized container and
// host ports.
func ParsePortMapping(portMappings []specgen.PortMapping) ([]ocicni.PortMapping, map[string]map[string]map[uint16]uint16, map[string]map[string]map[uint16]uint16, error) {
func ParsePortMapping(portMappings []types.PortMapping) ([]ocicni.PortMapping, map[string]map[string]map[uint16]uint16, map[string]map[string]map[uint16]uint16, error) {
// First, we need to validate the ports passed in the specgen, and then
// convert them into CNI port mappings.
type tempMapping struct {

View File

@ -3,6 +3,7 @@ package specgen
import (
"net"
"github.com/containers/podman/v3/libpod/network/types"
spec "github.com/opencontainers/runtime-spec/specs-go"
)
@ -102,7 +103,7 @@ type PodNetworkConfig struct {
// container, this will forward the ports to the entire pod.
// Only available if NetNS is set to Bridge or Slirp.
// Optional.
PortMappings []PortMapping `json:"portmappings,omitempty"`
PortMappings []types.PortMapping `json:"portmappings,omitempty"`
// CNINetworks is a list of CNI networks that the infra container will
// join. As, by default, containers share their network with the infra
// container, these networks will effectively be joined by the

View File

@ -5,6 +5,7 @@ import (
"syscall"
"github.com/containers/image/v5/manifest"
nettypes "github.com/containers/podman/v3/libpod/network/types"
"github.com/containers/storage/types"
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
@ -393,7 +394,7 @@ type ContainerNetworkConfig struct {
// PortBindings is a set of ports to map into the container.
// Only available if NetNS is set to bridge or slirp.
// Optional.
PortMappings []PortMapping `json:"portmappings,omitempty"`
PortMappings []nettypes.PortMapping `json:"portmappings,omitempty"`
// PublishExposedPorts will publish ports specified in the image to
// random unused ports (guaranteed to be above 1024) on the host.
// This is based on ports set in Expose below, and any ports specified
@ -506,36 +507,6 @@ type SpecGenerator struct {
ContainerHealthCheckConfig
}
// PortMapping is one or more ports that will be mapped into the container.
type PortMapping struct {
// HostIP is the IP that we will bind to on the host.
// If unset, assumed to be 0.0.0.0 (all interfaces).
HostIP string `json:"host_ip,omitempty"`
// ContainerPort is the port number that will be exposed from the
// container.
// Mandatory.
ContainerPort uint16 `json:"container_port"`
// HostPort is the port number that will be forwarded from the host into
// the container.
// If omitted, a random port on the host (guaranteed to be over 1024)
// will be assigned.
HostPort uint16 `json:"host_port,omitempty"`
// Range is the number of ports that will be forwarded, starting at
// HostPort and ContainerPort and counting up.
// This is 1-indexed, so 1 is assumed to be a single port (only the
// Hostport:Containerport mapping will be added), 2 is two ports (both
// Hostport:Containerport and Hostport+1:Containerport+1), etc.
// If unset, assumed to be 1 (a single port).
// Both hostport + range and containerport + range must be less than
// 65536.
Range uint16 `json:"range,omitempty"`
// Protocol is the protocol forward.
// Must be either "tcp", "udp", and "sctp", or some combination of these
// separated by commas.
// If unset, assumed to be TCP.
Protocol string `json:"protocol,omitempty"`
}
type Secret struct {
Source string
UID uint32