diff options
-rw-r--r-- | cmd/podman/networks/reload.go | 69 | ||||
-rw-r--r-- | docs/source/markdown/podman-network-reload.1.md | 62 | ||||
-rw-r--r-- | docs/source/markdown/podman-network.1.md | 17 | ||||
-rw-r--r-- | docs/source/network.rst | 2 | ||||
-rw-r--r-- | libpod/container_api.go | 26 | ||||
-rw-r--r-- | libpod/container_internal_linux.go | 13 | ||||
-rw-r--r-- | libpod/container_internal_unsupported.go | 4 | ||||
-rw-r--r-- | libpod/networking_linux.go | 85 | ||||
-rw-r--r-- | libpod/networking_unsupported.go | 9 | ||||
-rw-r--r-- | pkg/domain/entities/engine_container.go | 1 | ||||
-rw-r--r-- | pkg/domain/entities/network.go | 13 | ||||
-rw-r--r-- | pkg/domain/infra/abi/network.go | 20 | ||||
-rw-r--r-- | pkg/domain/infra/tunnel/network.go | 4 | ||||
-rw-r--r-- | test/system/500-networking.bats | 64 |
14 files changed, 376 insertions, 13 deletions
diff --git a/cmd/podman/networks/reload.go b/cmd/podman/networks/reload.go new file mode 100644 index 000000000..16655c18c --- /dev/null +++ b/cmd/podman/networks/reload.go @@ -0,0 +1,69 @@ +package network + +import ( + "fmt" + + "github.com/containers/podman/v2/cmd/podman/common" + "github.com/containers/podman/v2/cmd/podman/registry" + "github.com/containers/podman/v2/cmd/podman/utils" + "github.com/containers/podman/v2/cmd/podman/validate" + "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +var ( + networkReloadDescription = `reload container networks, recreating firewall rules` + networkReloadCommand = &cobra.Command{ + Use: "reload [options] [CONTAINER...]", + Short: "Reload firewall rules for one or more containers", + Long: networkReloadDescription, + RunE: networkReload, + Args: func(cmd *cobra.Command, args []string) error { + return validate.CheckAllLatestAndCIDFile(cmd, args, false, false) + }, + ValidArgsFunction: common.AutocompleteContainers, + Example: `podman network reload --latest + podman network reload 3c13ef6dd843 + podman network reload test1 test2`, + Annotations: map[string]string{ + registry.ParentNSRequired: "", + }, + } +) + +var ( + reloadOptions entities.NetworkReloadOptions +) + +func reloadFlags(flags *pflag.FlagSet) { + flags.BoolVarP(&reloadOptions.All, "all", "a", false, "Reload network configuration of all containers") +} + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode}, + Command: networkReloadCommand, + Parent: networkCmd, + }) + reloadFlags(networkReloadCommand.Flags()) + validate.AddLatestFlag(networkReloadCommand, &reloadOptions.Latest) +} + +func networkReload(cmd *cobra.Command, args []string) error { + responses, err := registry.ContainerEngine().NetworkReload(registry.Context(), args, reloadOptions) + if err != nil { + return err + } + + var errs utils.OutputErrors + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + + return errs.PrintErrors() +} diff --git a/docs/source/markdown/podman-network-reload.1.md b/docs/source/markdown/podman-network-reload.1.md new file mode 100644 index 000000000..dd8047297 --- /dev/null +++ b/docs/source/markdown/podman-network-reload.1.md @@ -0,0 +1,62 @@ +% podman-network-reload(1) + +## NAME +podman\-network\-reload - Reload network configuration for containers + +## SYNOPSIS +**podman network reload** [*options*] [*container...*] + +## DESCRIPTION +Reload one or more container network configurations. + +Rootful Podman relies on iptables rules in order to provide network connectivity. If the iptables rules are deleted, +this happens for example with `firewall-cmd --reload`, the container loses network connectivity. This command restores +the network connectivity. + +This command is not available for rootless users since rootless containers are not affected by such connectivity problems. + +## OPTIONS +#### **--all**, **-a** + +Reload network configuration of all containers. + +#### **--latest**, **-l** + +Instead of providing the container name or ID, use the last created container. If you use methods other than Podman +to run containers such as CRI-O, the last started container could be from either of those methods. + +The latest option is not supported on the remote client. + +## EXAMPLE + +Reload the network configuration after a firewall reload. + +``` +# podman run -p 80:80 -d nginx +b1b538e8bc4078fc3ee1c95b666ebc7449b9a97bacd15bcbe464a29e1be59c1c +# curl 127.0.0.1 +works +# sudo firewall-cmd --reload +success +# curl 127.0.0.1 +hangs +# podman network reload b1b538e8bc40 +b1b538e8bc4078fc3ee1c95b666ebc7449b9a97bacd15bcbe464a29e1be59c1c +# curl 127.0.0.1 +works +``` + +Reload the network configuration for all containers. + +``` +# podman network reload --all +b1b538e8bc4078fc3ee1c95b666ebc7449b9a97bacd15bcbe464a29e1be59c1c +fe7e8eca56f844ec33af10f0aa3b31b44a172776e3277b9550a623ed5d96e72b +``` + + +## SEE ALSO +podman(1), podman-network(1) + +## HISTORY +December 2020, Originally compiled by Paul Holzinger <paul.holzinger@web.de> diff --git a/docs/source/markdown/podman-network.1.md b/docs/source/markdown/podman-network.1.md index bc161659a..41e2ae885 100644 --- a/docs/source/markdown/podman-network.1.md +++ b/docs/source/markdown/podman-network.1.md @@ -11,14 +11,15 @@ The network command manages CNI networks for Podman. ## COMMANDS -| Command | Man Page | Description | -| ------- | --------------------------------------------------- | ---------------------------------------------------------------------------- | -| connect | [podman-network-connect(1)](podman-network-connect.1.md)| Connect a container to a network| -| create | [podman-network-create(1)](podman-network-create.1.md)| Create a Podman CNI network| -| disconnect | [podman-network-disconnect(1)](podman-network-disconnect.1.md)| Disconnect a container from a network| -| inspect | [podman-network-inspect(1)](podman-network-inspect.1.md)| Displays the raw CNI network configuration for one or more networks| -| ls | [podman-network-ls(1)](podman-network-ls.1.md)| Display a summary of CNI networks | -| rm | [podman-network-rm(1)](podman-network-rm.1.md)| Remove one or more CNI networks | +| Command | Man Page | Description | +| ---------- | -------------------------------------------------------------- | ------------------------------------------------------------------- | +| connect | [podman-network-connect(1)](podman-network-connect.1.md) | Connect a container to a network | +| create | [podman-network-create(1)](podman-network-create.1.md) | Create a Podman CNI network | +| disconnect | [podman-network-disconnect(1)](podman-network-disconnect.1.md) | Disconnect a container from a network | +| inspect | [podman-network-inspect(1)](podman-network-inspect.1.md) | Displays the raw CNI network configuration for one or more networks | +| ls | [podman-network-ls(1)](podman-network-ls.1.md) | Display a summary of CNI networks | +| reload | [podman-network-reload(1)](podman-network-reload.1.md) | Reload network configuration for containers | +| rm | [podman-network-rm(1)](podman-network-rm.1.md) | Remove one or more CNI networks | ## SEE ALSO podman(1) diff --git a/docs/source/network.rst b/docs/source/network.rst index 0414c0880..2ecb97858 100644 --- a/docs/source/network.rst +++ b/docs/source/network.rst @@ -11,4 +11,6 @@ Network :doc:`ls <markdown/podman-network-ls.1>` network list +:doc:`reload <markdown/podman-network-reload.1>` network reload + :doc:`rm <markdown/podman-network-rm.1>` network rm diff --git a/libpod/container_api.go b/libpod/container_api.go index 6a7ddc421..1b33f16b4 100644 --- a/libpod/container_api.go +++ b/libpod/container_api.go @@ -639,6 +639,32 @@ func (c *Container) Sync() error { return nil } +// ReloadNetwork reconfigures the container's network. +// Technically speaking, it will tear down and then reconfigure the container's +// network namespace, which will result in all firewall rules being recreated. +// It is mostly intended to be used in cases where the system firewall has been +// reloaded, and existing rules have been wiped out. It is expected that some +// downtime will result, as the rules are destroyed as part of this process. +// At present, this only works on root containers; it may be expanded to restart +// slirp4netns in the future to work with rootless containers as well. +// Requires that the container must be running or created. +func (c *Container) ReloadNetwork() error { + if !c.batched { + c.lock.Lock() + defer c.lock.Unlock() + + if err := c.syncContainer(); err != nil { + return err + } + } + + if !c.ensureState(define.ContainerStateCreated, define.ContainerStateRunning) { + return errors.Wrapf(define.ErrCtrStateInvalid, "cannot reload network unless container network has been configured") + } + + return c.reloadNetwork() +} + // Refresh is DEPRECATED and REMOVED. func (c *Container) Refresh(ctx context.Context) error { // This has been deprecated for a long while, and is in the process of diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index 72eaeac8e..1bf044f9d 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -231,6 +231,19 @@ func (c *Container) cleanupNetwork() error { return nil } +// reloadNetwork reloads the network for the given container, recreating +// firewall rules. +func (c *Container) reloadNetwork() error { + result, err := c.runtime.reloadContainerNetwork(c) + if err != nil { + return err + } + + c.state.NetworkStatus = result + + return c.save() +} + func (c *Container) getUserOverrides() *lookup.Overrides { var hasPasswdFile, hasGroupFile bool overrides := lookup.Overrides{} diff --git a/libpod/container_internal_unsupported.go b/libpod/container_internal_unsupported.go index c22e9a4a4..7f6fc9ec9 100644 --- a/libpod/container_internal_unsupported.go +++ b/libpod/container_internal_unsupported.go @@ -50,6 +50,10 @@ func (c *Container) cleanupOverlayMounts() error { return nil } +func (c *Container) reloadNetwork() error { + return define.ErrNotImplemented +} + func (c *Container) getUserOverrides() *lookup.Overrides { return nil } diff --git a/libpod/networking_linux.go b/libpod/networking_linux.go index 463378af7..2171a9208 100644 --- a/libpod/networking_linux.go +++ b/libpod/networking_linux.go @@ -13,6 +13,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "sort" "strings" "syscall" @@ -741,8 +742,9 @@ func (r *Runtime) closeNetNS(ctr *Container) error { return nil } -// Tear down a network namespace, undoing all state associated with it. -func (r *Runtime) teardownNetNS(ctr *Container) error { +// Tear down a container's CNI network configuration, but do not tear down the +// namespace itself. +func (r *Runtime) teardownCNI(ctr *Container) error { if ctr.state.NetNS == nil { // The container has no network namespace, we're set return nil @@ -781,6 +783,19 @@ func (r *Runtime) teardownNetNS(ctr *Container) error { return errors.Wrapf(err, "error tearing down CNI namespace configuration for container %s", ctr.ID()) } } + return nil +} + +// Tear down a network namespace, undoing all state associated with it. +func (r *Runtime) teardownNetNS(ctr *Container) error { + if err := r.teardownCNI(ctr); err != nil { + return err + } + + networks, _, err := ctr.networks() + if err != nil { + return err + } // CNI-in-slirp4netns if rootless.IsRootless() && len(networks) != 0 { @@ -821,6 +836,68 @@ func getContainerNetNS(ctr *Container) (string, error) { return "", nil } +// Reload only works with containers with a configured network. +// It will tear down, and then reconfigure, the network of the container. +// This is mainly used when a reload of firewall rules wipes out existing +// firewall configuration. +// Efforts will be made to preserve MAC and IP addresses, but this only works if +// the container only joined a single CNI network, and was only assigned a +// single MAC or IP. +// Only works on root containers at present, though in the future we could +// extend this to stop + restart slirp4netns +func (r *Runtime) reloadContainerNetwork(ctr *Container) ([]*cnitypes.Result, error) { + if ctr.state.NetNS == nil { + return nil, errors.Wrapf(define.ErrCtrStateInvalid, "container %s network is not configured, refusing to reload", ctr.ID()) + } + if rootless.IsRootless() || ctr.config.NetMode.IsSlirp4netns() { + return nil, errors.Wrapf(define.ErrRootless, "network reload only supported for root containers") + } + + logrus.Infof("Going to reload container %s network", ctr.ID()) + + var requestedIP net.IP + var requestedMAC net.HardwareAddr + // Set requested IP and MAC address, if possible. + if len(ctr.state.NetworkStatus) == 1 { + result := ctr.state.NetworkStatus[0] + if len(result.IPs) == 1 { + resIP := result.IPs[0] + + requestedIP = resIP.Address.IP + ctr.requestedIP = requestedIP + logrus.Debugf("Going to preserve container %s IP address %s", ctr.ID(), ctr.requestedIP.String()) + + if resIP.Interface != nil && *resIP.Interface < len(result.Interfaces) && *resIP.Interface >= 0 { + var err error + requestedMAC, err = net.ParseMAC(result.Interfaces[*resIP.Interface].Mac) + if err != nil { + return nil, errors.Wrapf(err, "error parsing container %s MAC address %s", ctr.ID(), result.Interfaces[*resIP.Interface].Mac) + } + ctr.requestedMAC = requestedMAC + logrus.Debugf("Going to preserve container %s MAC address %s", ctr.ID(), ctr.requestedMAC.String()) + } + } + } + + err := r.teardownCNI(ctr) + if err != nil { + // teardownCNI will error if the iptables rules do not exists and this is the case after + // a firewall reload. The purpose of network reload is to recreate the rules if they do + // not exists so we should not log this specific error as error. This would confuse users otherwise. + b, rerr := regexp.MatchString("Couldn't load target `CNI-[a-f0-9]{24}':No such file or directory", err.Error()) + if rerr == nil && !b { + logrus.Error(err) + } else { + logrus.Info(err) + } + } + + // teardownCNI will clean the requested IP and MAC so we need to set them again + ctr.requestedIP = requestedIP + ctr.requestedMAC = requestedMAC + return r.configureNetNS(ctr, ctr.state.NetNS) +} + func getContainerNetIO(ctr *Container) (*netlink.LinkStatistics, error) { var netStats *netlink.LinkStatistics // rootless v2 cannot seem to resolve its network connection to @@ -984,12 +1061,12 @@ func resultToBasicNetworkConfig(result *cnitypes.Result) (define.InspectBasicNet config.IPAddress = ctrIP.Address.IP.String() config.IPPrefixLen = size config.Gateway = ctrIP.Gateway.String() - if ctrIP.Interface != nil && *ctrIP.Interface < len(result.Interfaces) && *ctrIP.Interface > 0 { + if ctrIP.Interface != nil && *ctrIP.Interface < len(result.Interfaces) && *ctrIP.Interface >= 0 { config.MacAddress = result.Interfaces[*ctrIP.Interface].Mac } case ctrIP.Version == "4" && config.IPAddress != "": config.SecondaryIPAddresses = append(config.SecondaryIPAddresses, ctrIP.Address.String()) - if ctrIP.Interface != nil && *ctrIP.Interface < len(result.Interfaces) && *ctrIP.Interface > 0 { + if ctrIP.Interface != nil && *ctrIP.Interface < len(result.Interfaces) && *ctrIP.Interface >= 0 { config.AdditionalMacAddresses = append(config.AdditionalMacAddresses, result.Interfaces[*ctrIP.Interface].Mac) } case ctrIP.Version == "6" && config.IPAddress == "": diff --git a/libpod/networking_unsupported.go b/libpod/networking_unsupported.go index 76bb01424..9e5c4adde 100644 --- a/libpod/networking_unsupported.go +++ b/libpod/networking_unsupported.go @@ -2,7 +2,10 @@ package libpod -import "github.com/containers/podman/v2/libpod/define" +import ( + cnitypes "github.com/containernetworking/cni/pkg/types/current" + "github.com/containers/podman/v2/libpod/define" +) func (r *Runtime) setupRootlessNetNS(ctr *Container) error { return define.ErrNotImplemented @@ -28,6 +31,10 @@ func (c *Container) getContainerNetworkInfo() (*define.InspectNetworkSettings, e return nil, define.ErrNotImplemented } +func (r *Runtime) reloadContainerNetwork(ctr *Container) ([]*cnitypes.Result, error) { + return nil, define.ErrNotImplemented +} + func getCNINetworksDir() (string, error) { return "", define.ErrNotImplemented } diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 29d71cc65..5ad475133 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -55,6 +55,7 @@ type ContainerEngine interface { NetworkDisconnect(ctx context.Context, networkname string, options NetworkDisconnectOptions) error NetworkInspect(ctx context.Context, namesOrIds []string, options InspectOptions) ([]NetworkInspectReport, []error, error) NetworkList(ctx context.Context, options NetworkListOptions) ([]*NetworkListReport, error) + NetworkReload(ctx context.Context, names []string, options NetworkReloadOptions) ([]*NetworkReloadReport, error) NetworkRm(ctx context.Context, namesOrIds []string, options NetworkRmOptions) ([]*NetworkRmReport, error) PlayKube(ctx context.Context, path string, opts PlayKubeOptions) (*PlayKubeReport, error) PodCreate(ctx context.Context, opts PodCreateOptions) (*PodCreateReport, error) diff --git a/pkg/domain/entities/network.go b/pkg/domain/entities/network.go index 65a110fd9..b76bfcac7 100644 --- a/pkg/domain/entities/network.go +++ b/pkg/domain/entities/network.go @@ -22,6 +22,19 @@ type NetworkListReport struct { // NetworkInspectReport describes the results from inspect networks type NetworkInspectReport map[string]interface{} +// NetworkReloadOptions describes options for reloading container network +// configuration. +type NetworkReloadOptions struct { + All bool + Latest bool +} + +// NetworkReloadReport describes the results of reloading a container network. +type NetworkReloadReport struct { + Id string + Err error +} + // NetworkRmOptions describes options for removing networks type NetworkRmOptions struct { Force bool diff --git a/pkg/domain/infra/abi/network.go b/pkg/domain/infra/abi/network.go index 6a219edd5..e5ecf5c72 100644 --- a/pkg/domain/infra/abi/network.go +++ b/pkg/domain/infra/abi/network.go @@ -60,6 +60,26 @@ func (ic *ContainerEngine) NetworkInspect(ctx context.Context, namesOrIds []stri return rawCNINetworks, errs, nil } +func (ic *ContainerEngine) NetworkReload(ctx context.Context, names []string, options entities.NetworkReloadOptions) ([]*entities.NetworkReloadReport, error) { + ctrs, err := getContainersByContext(options.All, options.Latest, names, ic.Libpod) + if err != nil { + return nil, err + } + + reports := make([]*entities.NetworkReloadReport, 0, len(ctrs)) + for _, ctr := range ctrs { + report := new(entities.NetworkReloadReport) + report.Id = ctr.ID() + report.Err = ctr.ReloadNetwork() + if options.All && errors.Cause(report.Err) == define.ErrCtrStateInvalid { + continue + } + reports = append(reports, report) + } + + return reports, nil +} + func (ic *ContainerEngine) NetworkRm(ctx context.Context, namesOrIds []string, options entities.NetworkRmOptions) ([]*entities.NetworkRmReport, error) { reports := []*entities.NetworkRmReport{} diff --git a/pkg/domain/infra/tunnel/network.go b/pkg/domain/infra/tunnel/network.go index 10ae03045..4845980f6 100644 --- a/pkg/domain/infra/tunnel/network.go +++ b/pkg/domain/infra/tunnel/network.go @@ -35,6 +35,10 @@ func (ic *ContainerEngine) NetworkInspect(ctx context.Context, namesOrIds []stri return reports, errs, nil } +func (ic *ContainerEngine) NetworkReload(ctx context.Context, names []string, options entities.NetworkReloadOptions) ([]*entities.NetworkReloadReport, error) { + return nil, errors.New("not implemented") +} + func (ic *ContainerEngine) NetworkRm(ctx context.Context, namesOrIds []string, options entities.NetworkRmOptions) ([]*entities.NetworkRmReport, error) { reports := make([]*entities.NetworkRmReport, 0, len(namesOrIds)) for _, name := range namesOrIds { diff --git a/test/system/500-networking.bats b/test/system/500-networking.bats index 44cc731cf..a824ebcd7 100644 --- a/test/system/500-networking.bats +++ b/test/system/500-networking.bats @@ -116,4 +116,68 @@ load helpers fi } +@test "podman network reload" { + skip_if_remote "podman network reload does not have remote support" + skip_if_rootless "podman network reload does not work rootless" + + random_1=$(random_string 30) + HOST_PORT=12345 + SERVER=http://127.0.0.1:$HOST_PORT + + # Create a test file with random content + INDEX1=$PODMAN_TMPDIR/hello.txt + echo $random_1 > $INDEX1 + + # Bind-mount this file with a different name to a container running httpd + run_podman run -d --name myweb -p "$HOST_PORT:80" \ + -v $INDEX1:/var/www/index.txt \ + -w /var/www \ + $IMAGE /bin/busybox-extras httpd -f -p 80 + cid=$output + + run_podman inspect $cid --format "{{.NetworkSettings.IPAddress}}" + ip="$output" + run_podman inspect $cid --format "{{.NetworkSettings.MacAddress}}" + mac="$output" + + # Verify http contents: curl from localhost + run curl -s $SERVER/index.txt + is "$output" "$random_1" "curl 127.0.0.1:/index.txt" + + # flush the CNI iptables here + run iptables -t nat -F CNI-HOSTPORT-DNAT + + # check that we cannot curl (timeout after 5 sec) + run timeout 5 curl -s $SERVER/index.txt + if [ "$status" -ne 124 ]; then + die "curl did not timeout, status code: $status" + fi + + # reload the network to recreate the iptables rules + run_podman network reload $cid + is "$output" "$cid" "Output does not match container ID" + + # check that we still have the same mac and ip + run_podman inspect $cid --format "{{.NetworkSettings.IPAddress}}" + is "$output" "$ip" "IP address changed after podman network reload" + run_podman inspect $cid --format "{{.NetworkSettings.MacAddress}}" + is "$output" "$mac" "MAC address changed after podman network reload" + + # check that we can still curl + run curl -s $SERVER/index.txt + is "$output" "$random_1" "curl 127.0.0.1:/index.txt" + + # make sure --all is working and that this + # cmd also works if the iptables still exists + run_podman network reload --all + is "$output" "$cid" "Output does not match container ID" + + # check that we can still curl + run curl -s $SERVER/index.txt + is "$output" "$random_1" "curl 127.0.0.1:/index.txt" + + # cleanup the container + run_podman rm -f $cid +} + # vim: filetype=sh |