Skip to content

Commit

Permalink
ensure iptables chain creation is idempotent
Browse files Browse the repository at this point in the history
Concurrent use of the `portmap` and `firewall` plugins can result in
errors during iptables chain creation:

- The `portmap` plugin has a time-of-check-time-of-use race where it
  checks for existence of the chain but the operation isn't atomic.
- The `firewall` plugin doesn't check for existing chains and just
  returns an error.

This commit makes both operations idempotent by creating the chain and
then discarding the error if it's caused by the chain already
existing. It also factors the chain creation out into `pkg/utils` as a
site for future refactoring work.

Signed-off-by: Tim Gross <tim@0x74696d.com>
  • Loading branch information
tgross committed Nov 6, 2019
1 parent a162329 commit 81426da
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 27 deletions.
57 changes: 57 additions & 0 deletions pkg/utils/iptables.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2017 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package utils

import (
"fmt"

"github.com/coreos/go-iptables/iptables"
)

const statusChainExists = 1

// EnsureChain idempotently creates the iptables chain. It does not
// return an error if the chain already exists.
func EnsureChain(ipt *iptables.IPTables, table, chain string) error {
exists, err := ChainExists(ipt, table, chain)
if err != nil {
return fmt.Errorf("failed to list iptables chains: %v", err)
}
if !exists {
err = ipt.NewChain(table, chain)
if err != nil {
eerr, eok := err.(*iptables.Error)
if eok && eerr.ExitStatus() != statusChainExists {
return err
}
}
}
return nil
}

// ChainExists checks whether an iptables chain exists.
func ChainExists(ipt *iptables.IPTables, table, chain string) (bool, error) {
chains, err := ipt.ListChains(table)
if err != nil {
return false, err
}

for _, ch := range chains {
if ch == chain {
return true, nil
}
}
return false, nil
}
76 changes: 76 additions & 0 deletions pkg/utils/iptables_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright 2017-2018 CNI authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package utils

import (
"fmt"
"math/rand"
"runtime"

"github.com/containernetworking/plugins/pkg/ns"
"github.com/containernetworking/plugins/pkg/testutils"
"github.com/coreos/go-iptables/iptables"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

const TABLE = "filter" // We'll monkey around here

var _ = Describe("chain tests", func() {
var testChain string
var ipt *iptables.IPTables
var cleanup func()

BeforeEach(func() {

// Save a reference to the original namespace,
// Add a new NS
currNs, err := ns.GetCurrentNS()
Expect(err).NotTo(HaveOccurred())

testNs, err := testutils.NewNS()
Expect(err).NotTo(HaveOccurred())

testChain = fmt.Sprintf("cni-test-%d", rand.Intn(10000000))

ipt, err = iptables.NewWithProtocol(iptables.ProtocolIPv4)
Expect(err).NotTo(HaveOccurred())

runtime.LockOSThread()
err = testNs.Set()
Expect(err).NotTo(HaveOccurred())

cleanup = func() {
if ipt == nil {
return
}
ipt.ClearChain(TABLE, testChain)
ipt.DeleteChain(TABLE, testChain)
currNs.Set()
}

})

It("creates chains idempotently", func() {
defer cleanup()

err := EnsureChain(ipt, TABLE, testChain)
Expect(err).NotTo(HaveOccurred())

// Create it again!
err = EnsureChain(ipt, TABLE, testChain)
Expect(err).NotTo(HaveOccurred())
})
})
7 changes: 7 additions & 0 deletions plugins/meta/firewall/firewall_iptables_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,13 @@ var _ = Describe("firewall plugin iptables backend", func() {
Expect(err).NotTo(HaveOccurred())

validateFullRuleset(fullConf)

// ensure creation is idempotent
_, _, err = testutils.CmdAdd(targetNS.Path(), args.ContainerID, IFNAME, fullConf, func() error {
return cmdAdd(args)
})
Expect(err).NotTo(HaveOccurred())

return nil
})
Expect(err).NotTo(HaveOccurred())
Expand Down
15 changes: 12 additions & 3 deletions plugins/meta/firewall/iptables.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"net"

"github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/plugins/pkg/utils"
"github.com/coreos/go-iptables/iptables"
)

Expand All @@ -33,6 +34,7 @@ func getPrivChainRules(ip string) [][]string {
}

func ensureChain(ipt *iptables.IPTables, table, chain string) error {

chains, err := ipt.ListChains(table)
if err != nil {
return fmt.Errorf("failed to list iptables chains: %v", err)
Expand All @@ -43,7 +45,14 @@ func ensureChain(ipt *iptables.IPTables, table, chain string) error {
}
}

return ipt.NewChain(table, chain)
err = ipt.NewChain(table, chain)
if err != nil {
eerr, eok := err.(*iptables.Error)
if eok && eerr.ExitStatus() != 1 {
return err
}
}
return nil
}

func generateFilterRule(privChainName string) []string {
Expand Down Expand Up @@ -73,10 +82,10 @@ func (ib *iptablesBackend) setupChains(ipt *iptables.IPTables) error {
adminRule := generateFilterRule(ib.adminChainName)

// Ensure our private chains exist
if err := ensureChain(ipt, "filter", ib.privChainName); err != nil {
if err := utils.EnsureChain(ipt, "filter", ib.privChainName); err != nil {
return err
}
if err := ensureChain(ipt, "filter", ib.adminChainName); err != nil {
if err := utils.EnsureChain(ipt, "filter", ib.adminChainName); err != nil {
return err
}

Expand Down
26 changes: 4 additions & 22 deletions plugins/meta/portmap/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"fmt"
"strings"

"github.com/containernetworking/plugins/pkg/utils"
"github.com/coreos/go-iptables/iptables"
"github.com/mattn/go-shellwords"
)
Expand All @@ -35,16 +36,11 @@ type chain struct {

// setup idempotently creates the chain. It will not error if the chain exists.
func (c *chain) setup(ipt *iptables.IPTables) error {
// create the chain
exists, err := chainExists(ipt, c.table, c.name)

err := utils.EnsureChain(ipt, c.table, c.name)
if err != nil {
return err
}
if !exists {
if err := ipt.NewChain(c.table, c.name); err != nil {
return err
}
}

// Add the rules to the chain
for _, rule := range c.rules {
Expand Down Expand Up @@ -125,24 +121,10 @@ func insertUnique(ipt *iptables.IPTables, table, chain string, prepend bool, rul
}
}

func chainExists(ipt *iptables.IPTables, tableName, chainName string) (bool, error) {
chains, err := ipt.ListChains(tableName)
if err != nil {
return false, err
}

for _, ch := range chains {
if ch == chainName {
return true, nil
}
}
return false, nil
}

// check the chain.
func (c *chain) check(ipt *iptables.IPTables) error {

exists, err := chainExists(ipt, c.table, c.name)
exists, err := utils.ChainExists(ipt, c.table, c.name)
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions plugins/meta/portmap/portmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func checkPorts(config *PortMapConf, containerIP net.IP) error {
}

if ip4t != nil {
exists, err := chainExists(ip4t, dnatChain.table, dnatChain.name)
exists, err := utils.ChainExists(ip4t, dnatChain.table, dnatChain.name)
if err != nil {
return err
}
Expand All @@ -137,7 +137,7 @@ func checkPorts(config *PortMapConf, containerIP net.IP) error {
}

if ip6t != nil {
exists, err := chainExists(ip6t, dnatChain.table, dnatChain.name)
exists, err := utils.ChainExists(ip6t, dnatChain.table, dnatChain.name)
if err != nil {
return err
}
Expand Down

0 comments on commit 81426da

Please sign in to comment.