Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 41 additions & 13 deletions iptables/iptables.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"net"
"os/exec"
"regexp"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -36,6 +37,7 @@ const (
var (
iptablesPath string
supportsXlock = false
supportsCOpt = false
// used to lock iptables commands if xtables lock is not supported
bestEffortLock sync.Mutex
// ErrIptablesNotFound is returned when the rule is not found.
Expand All @@ -60,14 +62,19 @@ func (e ChainError) Error() string {
}

func initCheck() error {

if iptablesPath == "" {
path, err := exec.LookPath("iptables")
if err != nil {
return ErrIptablesNotFound
}
iptablesPath = path
supportsXlock = exec.Command(iptablesPath, "--wait", "-L", "-n").Run() == nil
mj, mn, mc, err := GetVersion()
if err != nil {
logrus.Warnf("Failed to read iptables version: %v", err)
return nil
}
supportsCOpt = supportsCOption(mj, mn, mc)
}
return nil
}
Expand Down Expand Up @@ -299,20 +306,19 @@ func Exists(table Table, chain string, rule ...string) bool {
table = Filter
}

// iptables -C, --check option was added in v.1.4.11
// http://ftp.netfilter.org/pub/iptables/changes-iptables-1.4.11.txt

// try -C
// if exit status is 0 then return true, the rule exists
if _, err := Raw(append([]string{
"-t", string(table), "-C", chain}, rule...)...); err == nil {
return true
if supportsCOpt {
// if exit status is 0 then return true, the rule exists
_, err := Raw(append([]string{"-t", string(table), "-C", chain}, rule...)...)
return err == nil
}

// parse "iptables -S" for the rule (this checks rules in a specific chain
// in a specific table)
ruleString := strings.Join(rule, " ")
ruleString = chain + " " + ruleString
// parse "iptables -S" for the rule (it checks rules in a specific chain
// in a specific table and it is very unreliable)
return existsRaw(table, chain, rule...)
}

func existsRaw(table Table, chain string, rule ...string) bool {
ruleString := fmt.Sprintf("%s %s\n", chain, strings.Join(rule, " "))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you adding the \n to take care of the Contains matching on partial TARGET ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a test for it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where ? I dont see a partial TARGET match test here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look at line 275 in the test diffs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mavenugo
Actually you are right, I modified the test code but the check at 275 is still testing the old logic.
Will fix

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mavenugo
Took care of it, PTAL

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quite frankly, am not too comfortable depending on \n as a mechanism to guarantee the comparison and it could result in false-negatives depending on the position of the rule itself (and various other unrelated dependencies).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking for the \n is in fact to avoid a possible match on a rule subset.

The existing raw exist check does nothing but looking for the rule string in the output of iptables -t <table> -S <chain>, which is a list of \n terminated strings, where each rule follows this format:

-<Action> <Chain> <rule args>\n

Given the rule format, checking if the byte stream contains <rule>\n will guarantee no subset can match.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

existingRules, _ := exec.Command(iptablesPath, "-t", string(table), "-S", chain).Output()

return strings.Contains(string(existingRules), ruleString)
Expand Down Expand Up @@ -380,3 +386,25 @@ func ExistChain(chain string, table Table) bool {
}
return false
}

// GetVersion reads the iptables version numbers
func GetVersion() (major, minor, micro int, err error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this function ? It doesnt seem to be used in this PR ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, just in case we want to do a version check in Exists()to decide whether attempt the iptables -C ... check instead of parsing the returned error string which I feel it is ugly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this again goes to the same discussion on checking for versions vs checking for functionality (we had a few such scenarios for kernel versions and we typically prefer the functionality check).

@mrjana WDYT ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets remove this function. There is no use for this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok

out, err := Raw("--version")
if err == nil {
major, minor, micro = parseVersionNumbers(string(out))
}
return
}

func parseVersionNumbers(input string) (major, minor, micro int) {
re := regexp.MustCompile(`v\d*.\d*.\d*`)
line := re.FindString(input)
fmt.Sscanf(line, "v%d.%d.%d", &major, &minor, &micro)
return
}

// iptables -C, --check option was added in v.1.4.11
// http://ftp.netfilter.org/pub/iptables/changes-iptables-1.4.11.txt
func supportsCOption(mj, mn, mc int) bool {
return mj > 1 || (mj == 1 && (mn > 4 || (mn == 4 && mc >= 11)))
}
76 changes: 76 additions & 0 deletions iptables/iptables_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,79 @@ func TestCleanup(t *testing.T) {
t.Fatalf("Removing chain failed. %s found in iptables-save", chainName)
}
}

func TestExistsRaw(t *testing.T) {
testChain1 := "ABCD"
testChain2 := "EFGH"

_, err := NewChain(testChain1, Filter, false)
if err != nil {
t.Fatal(err)
}
defer func() {
RemoveExistingChain(testChain1, Filter)
}()

_, err = NewChain(testChain2, Filter, false)
if err != nil {
t.Fatal(err)
}
defer func() {
RemoveExistingChain(testChain2, Filter)
}()

// Test detection over full and truncated rule string
input := []struct{ rule []string }{
{[]string{"-s", "172.8.9.9/32", "-j", "ACCEPT"}},
{[]string{"-d", "172.8.9.0/24", "-j", "DROP"}},
{[]string{"-s", "172.0.3.0/24", "-d", "172.17.0.0/24", "-p", "tcp", "-m", "tcp", "--dport", "80", "-j", testChain2}},
{[]string{"-j", "RETURN"}},
}

for i, r := range input {
ruleAdd := append([]string{"-t", string(Filter), "-A", testChain1}, r.rule...)
err = RawCombinedOutput(ruleAdd...)
if err != nil {
t.Fatalf("i=%d, err: %v", i, err)
}
if !existsRaw(Filter, testChain1, r.rule...) {
t.Fatalf("Failed to detect rule. i=%d", i)
}
// Truncate the rule
trg := r.rule[len(r.rule)-1]
trg = trg[:len(trg)-2]
r.rule[len(r.rule)-1] = trg
if existsRaw(Filter, testChain1, r.rule...) {
t.Fatalf("Invalid detection. i=%d", i)
}
}
}

func TestGetVersion(t *testing.T) {
mj, mn, mc := parseVersionNumbers("iptables v1.4.19.1-alpha")
if mj != 1 || mn != 4 || mc != 19 {
t.Fatalf("Failed to parse version numbers")
}
}

func TestSupportsCOption(t *testing.T) {
input := []struct {
mj int
mn int
mc int
ok bool
}{
{1, 4, 11, true},
{1, 4, 12, true},
{1, 5, 0, true},
{0, 4, 11, false},
{0, 5, 12, false},
{1, 3, 12, false},
{1, 4, 10, false},
}
for ind, inp := range input {
if inp.ok != supportsCOption(inp.mj, inp.mn, inp.mc) {
t.Fatalf("Incorrect check: %d", ind)
}
}
}