import (
"bytes"
"fmt"
+ "io"
"log"
"os/exec"
"regexp"
}
type IPTables struct {
- path string
+ path string
+ hasCheck bool
+ hasWait bool
+
+ fmu *fileLock
}
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)
// 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")
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, a new one will be created
func (ipt *IPTables) ClearChain(table, chain string) error {
err := ipt.NewChain(table, chain)
}
}
+// 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,
}
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.
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
}
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)
}
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)
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)
+ }
}
--- /dev/null
+// 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
+}