Merge pull request #104 from dcbw/revendor-go-iptables

Revendor go-iptables to get --wait behavior
This commit is contained in:
Jonathan Boulle 2016-01-27 21:18:44 +01:00
commit 09214926ea
4 changed files with 202 additions and 54 deletions

2
Godeps/Godeps.json generated
View File

@ -7,7 +7,7 @@
"Deps": [
{
"ImportPath": "github.com/coreos/go-iptables/iptables",
"Rev": "83dfad0f13fd7310fb3c1cb8563248d8d604b95b"
"Rev": "90456be57fcb8185b264b77ce42a9539df42df25"
},
{
"ImportPath": "github.com/coreos/go-systemd/activation",

View 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,33 +53,41 @@ func New() (*IPTables, error) {
if err != nil {
return nil, err
}
return &IPTables{path}, nil
checkPresent, waitPresent, err := getIptablesCommandSupport()
if err != nil {
log.Printf("Error checking iptables version, assuming version at least 1.4.20: %v", err)
checkPresent = true
waitPresent = true
}
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) {
checkPresent, err := getIptablesHasCheckCommand()
if err != nil {
log.Printf("Error checking iptables version, assuming version at least 1.4.11: %v", err)
checkPresent = true
func (ipt *IPTables) Exists(table, chain string, rulespec ...string) (bool, error) {
if !ipt.hasCheck {
return ipt.existsForOldIptables(table, chain, rulespec)
}
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
}
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
}
}
@ -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, a 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
}

View 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)
}
}

View File

@ -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
}