Revendor go-iptables to get --wait behavior
authorDan Williams <dcbw@redhat.com>
Wed, 27 Jan 2016 19:56:53 +0000 (13:56 -0600)
committerDan Williams <dcbw@redhat.com>
Wed, 27 Jan 2016 19:56:53 +0000 (13:56 -0600)
Godeps/Godeps.json
Godeps/_workspace/src/github.com/coreos/go-iptables/iptables/iptables.go
Godeps/_workspace/src/github.com/coreos/go-iptables/iptables/iptables_test.go
Godeps/_workspace/src/github.com/coreos/go-iptables/iptables/lock.go [new file with mode: 0644]

index d1de767..df42da8 100644 (file)
@@ -7,7 +7,7 @@
        "Deps": [
                {
                        "ImportPath": "github.com/coreos/go-iptables/iptables",
-                       "Rev": "83dfad0f13fd7310fb3c1cb8563248d8d604b95b"
+                       "Rev": "90456be57fcb8185b264b77ce42a9539df42df25"
                },
                {
                        "ImportPath": "github.com/coreos/go-systemd/activation",
index 36a8ec7..ea9fedf 100644 (file)
@@ -17,6 +17,7 @@ package iptables
 import (
        "bytes"
        "fmt"
+       "io"
        "log"
        "os/exec"
        "regexp"
@@ -40,7 +41,11 @@ func (e *Error) Error() string {
 }
 
 type IPTables struct {
-       path string
+       path     string
+       hasCheck bool
+       hasWait  bool
+
+       fmu *fileLock
 }
 
 func New() (*IPTables, error) {
@@ -48,34 +53,42 @@ func New() (*IPTables, error) {
        if err != nil {
                return nil, err
        }
-
-       return &IPTables{path}, nil
-}
-
-// Exists checks if given rulespec in specified table/chain exists
-func (ipt *IPTables) Exists(table, chain string, rulespec...string) (bool, error) {
-       checkPresent, err := getIptablesHasCheckCommand()
+       checkPresent, waitPresent, err := getIptablesCommandSupport()
        if err != nil {
-               log.Printf("Error checking iptables version, assuming version at least 1.4.11: %v", err)
+               log.Printf("Error checking iptables version, assuming version at least 1.4.20: %v", err)
                checkPresent = true
+               waitPresent = true
        }
-
-       if !checkPresent {
-               cmd := append([]string{"-A", chain}, rulespec...)
-               return existsForOldIpTables(table, strings.Join(cmd, " "))
-       } else {
-               cmd := append([]string{"-t", table, "-C", chain}, rulespec...)
-               err := ipt.run(cmd...)
-
-               switch {
-               case err == nil:
-                       return true, nil
-               case err.(*Error).ExitStatus() == 1:
-                       return false, nil
-               default:
-                       return false, err
+       ipt := IPTables{
+               path:     path,
+               hasCheck: checkPresent,
+               hasWait:  waitPresent,
+       }
+       if !waitPresent {
+               ipt.fmu, err = newXtablesFileLock()
+               if err != nil {
+                       return nil, err
                }
        }
+       return &ipt, nil
+}
+
+// Exists checks if given rulespec in specified table/chain exists
+func (ipt *IPTables) Exists(table, chain string, rulespec ...string) (bool, error) {
+       if !ipt.hasCheck {
+               return ipt.existsForOldIptables(table, chain, rulespec)
+
+       }
+       cmd := append([]string{"-t", table, "-C", chain}, rulespec...)
+       err := ipt.run(cmd...)
+       switch {
+       case err == nil:
+               return true, nil
+       case err.(*Error).ExitStatus() == 1:
+               return false, nil
+       default:
+               return false, err
+       }
 }
 
 // Insert inserts rulespec to specified table/chain (in specified pos)
@@ -112,16 +125,10 @@ func (ipt *IPTables) Delete(table, chain string, rulespec ...string) error {
 
 // List rules in specified table/chain
 func (ipt *IPTables) List(table, chain string) ([]string, error) {
-       var stdout, stderr bytes.Buffer
-       cmd := exec.Cmd{
-               Path: ipt.path,
-               Args: []string{ipt.path, "-t", table, "-S", chain},
-               Stdout: &stdout,
-               Stderr: &stderr,
-       }
-
-       if err := cmd.Run(); err != nil {
-               return nil, &Error{*(err.(*exec.ExitError)), stderr.String()}
+       args := []string{"-t", table, "-S", chain}
+       var stdout bytes.Buffer
+       if err := ipt.runWithOutput(args, &stdout); err != nil {
+               return nil, err
        }
 
        rules := strings.Split(stdout.String(), "\n")
@@ -136,8 +143,8 @@ func (ipt *IPTables) NewChain(table, chain string) error {
        return ipt.run("-t", table, "-N", chain)
 }
 
-// ClearChain flushed (deletes all rules) in the specifed table/chain.
-// If the chain does not exist, new one will be created
+// ClearChain flushed (deletes all rules) in the specified table/chain.
+// If the chain does not exist, new one will be created
 func (ipt *IPTables) ClearChain(table, chain string) error {
        err := ipt.NewChain(table, chain)
 
@@ -152,17 +159,42 @@ func (ipt *IPTables) ClearChain(table, chain string) error {
        }
 }
 
+// RenameChain renames the old chain to the new one.
+func (ipt *IPTables) RenameChain(table, oldChain, newChain string) error {
+       return ipt.run("-t", table, "-E", oldChain, newChain)
+}
+
 // DeleteChain deletes the chain in the specified table.
 // The chain must be empty
 func (ipt *IPTables) DeleteChain(table, chain string) error {
        return ipt.run("-t", table, "-X", chain)
 }
 
-func (ipt *IPTables) run(args... string) error {
+// run runs an iptables command with the given arguments, ignoring
+// any stdout output
+func (ipt *IPTables) run(args ...string) error {
+       return ipt.runWithOutput(args, nil)
+}
+
+// runWithOutput runs an iptables command with the given arguments,
+// writing any stdout output to the given writer
+func (ipt *IPTables) runWithOutput(args []string, stdout io.Writer) error {
+       args = append([]string{ipt.path}, args...)
+       if ipt.hasWait {
+               args = append(args, "--wait")
+       } else {
+               ul, err := ipt.fmu.tryLock()
+               if err != nil {
+                       return err
+               }
+               defer ul.Unlock()
+       }
+
        var stderr bytes.Buffer
        cmd := exec.Cmd{
-               Path: ipt.path,
-               Args: append([]string{ipt.path}, args...),
+               Path:   ipt.path,
+               Args:   args,
+               Stdout: stdout,
                Stderr: &stderr,
        }
 
@@ -173,19 +205,19 @@ func (ipt *IPTables) run(args... string) error {
        return nil
 }
 
-// Checks if iptables has the "-C" flag
-func getIptablesHasCheckCommand() (bool, error) {
+// Checks if iptables has the "-C" and "--wait" flag
+func getIptablesCommandSupport() (bool, bool, error) {
        vstring, err := getIptablesVersionString()
        if err != nil {
-               return false, err
+               return false, false, err
        }
 
        v1, v2, v3, err := extractIptablesVersion(vstring)
        if err != nil {
-               return false, err
+               return false, false, err
        }
 
-       return iptablesHasCheckCommand(v1, v2, v3), nil
+       return iptablesHasCheckCommand(v1, v2, v3), iptablesHasWaitCommand(v1, v2, v3), nil
 }
 
 // getIptablesVersion returns the first three components of the iptables version.
@@ -241,15 +273,28 @@ func iptablesHasCheckCommand(v1 int, v2 int, v3 int) bool {
        return false
 }
 
+// Checks if an iptables version is after 1.4.20, when --wait was added
+func iptablesHasWaitCommand(v1 int, v2 int, v3 int) bool {
+       if v1 > 1 {
+               return true
+       }
+       if v1 == 1 && v2 > 4 {
+               return true
+       }
+       if v1 == 1 && v2 == 4 && v3 >= 20 {
+               return true
+       }
+       return false
+}
+
 // Checks if a rule specification exists for a table
-func existsForOldIpTables(table string, ruleSpec string) (bool, error) {
-       cmd := exec.Command("iptables", "-t", table, "-S")
-       var out bytes.Buffer
-       cmd.Stdout = &out
-       err := cmd.Run()
+func (ipt *IPTables) existsForOldIptables(table, chain string, rulespec []string) (bool, error) {
+       rs := strings.Join(append([]string{"-A", chain}, rulespec...), " ")
+       args := []string{"-t", table, "-S"}
+       var stdout bytes.Buffer
+       err := ipt.runWithOutput(args, &stdout)
        if err != nil {
                return false, err
        }
-       rules := out.String()
-       return strings.Contains(rules, ruleSpec), nil
+       return strings.Contains(stdout.String(), rs), nil
 }
index e4f3b17..66ce4bb 100644 (file)
@@ -67,8 +67,15 @@ func TestChain(t *testing.T) {
                t.Fatalf("ClearChain (of non-empty) failed: %v", err)
        }
 
+       // rename the chain
+       newChain := randChain(t)
+       err = ipt.RenameChain("filter", chain, newChain)
+       if err != nil {
+               t.Fatalf("RenameChain failed: %v", err)
+       }
+
        // chain empty, should be ok
-       err = ipt.DeleteChain("filter", chain)
+       err = ipt.DeleteChain("filter", newChain)
        if err != nil {
                t.Fatalf("DeleteChain of empty chain failed: %v", err)
        }
@@ -115,7 +122,7 @@ func TestRules(t *testing.T) {
 
        err = ipt.Delete("filter", chain, "-s", "10.1.0.0/16", "-d", "9.9.9.9/32", "-j", "ACCEPT")
        if err != nil {
-               t.Fatalf("Insert failed: %v", err)
+               t.Fatalf("Delete failed: %v", err)
        }
 
        rules, err := ipt.List("filter", chain)
@@ -133,4 +140,16 @@ func TestRules(t *testing.T) {
        if !reflect.DeepEqual(rules, expected) {
                t.Fatalf("List mismatch: \ngot  %#v \nneed %#v", rules, expected)
        }
+
+       // Clear the chain that was created.
+       err = ipt.ClearChain("filter", chain)
+       if err != nil {
+               t.Fatalf("Failed to clear test chain: %v", err)
+       }
+
+       // Delete the chain that was created
+       err = ipt.DeleteChain("filter", chain)
+       if err != nil {
+               t.Fatalf("Failed to delete test chain: %v", err)
+       }
 }
diff --git a/Godeps/_workspace/src/github.com/coreos/go-iptables/iptables/lock.go b/Godeps/_workspace/src/github.com/coreos/go-iptables/iptables/lock.go
new file mode 100644 (file)
index 0000000..a88e92b
--- /dev/null
@@ -0,0 +1,84 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// 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 iptables
+
+import (
+       "os"
+       "sync"
+       "syscall"
+)
+
+const (
+       // In earlier versions of iptables, the xtables lock was implemented
+       // via a Unix socket, but now flock is used via this lockfile:
+       // http://git.netfilter.org/iptables/commit/?id=aa562a660d1555b13cffbac1e744033e91f82707
+       // Note the LSB-conforming "/run" directory does not exist on old
+       // distributions, so assume "/var" is symlinked
+       xtablesLockFilePath = "/var/run/xtables.lock"
+
+       defaultFilePerm = 0600
+)
+
+type Unlocker interface {
+       Unlock() error
+}
+
+type nopUnlocker struct{}
+
+func (_ nopUnlocker) Unlock() error { return nil }
+
+type fileLock struct {
+       // mu is used to protect against concurrent invocations from within this process
+       mu sync.Mutex
+       fd int
+}
+
+// tryLock takes an exclusive lock on the xtables lock file without blocking.
+// This is best-effort only: if the exclusive lock would block (i.e. because
+// another process already holds it), no error is returned. Otherwise, any
+// error encountered during the locking operation is returned.
+// The returned Unlocker should be used to release the lock when the caller is
+// done invoking iptables commands.
+func (l *fileLock) tryLock() (Unlocker, error) {
+       l.mu.Lock()
+       err := syscall.Flock(l.fd, syscall.LOCK_EX|syscall.LOCK_NB)
+       switch err {
+       case syscall.EWOULDBLOCK:
+               l.mu.Unlock()
+               return nopUnlocker{}, nil
+       case nil:
+               return l, nil
+       default:
+               l.mu.Unlock()
+               return nil, err
+       }
+}
+
+// Unlock closes the underlying file, which implicitly unlocks it as well. It
+// also unlocks the associated mutex.
+func (l *fileLock) Unlock() error {
+       defer l.mu.Unlock()
+       return syscall.Close(l.fd)
+}
+
+// newXtablesFileLock opens a new lock on the xtables lockfile without
+// acquiring the lock
+func newXtablesFileLock() (*fileLock, error) {
+       fd, err := syscall.Open(xtablesLockFilePath, os.O_CREATE, defaultFilePerm)
+       if err != nil {
+               return nil, err
+       }
+       return &fileLock{fd: fd}, nil
+}