Skip to content

Commit f198d12

Browse files
committed
Merge pull request #925 from aboch/ex
Fix iptables.Exists logic
2 parents 77c1c58 + e5b348b commit f198d12

2 files changed

Lines changed: 117 additions & 13 deletions

File tree

iptables/iptables.go

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"net"
77
"os/exec"
8+
"regexp"
89
"strconv"
910
"strings"
1011
"sync"
@@ -36,6 +37,7 @@ const (
3637
var (
3738
iptablesPath string
3839
supportsXlock = false
40+
supportsCOpt = false
3941
// used to lock iptables commands if xtables lock is not supported
4042
bestEffortLock sync.Mutex
4143
// ErrIptablesNotFound is returned when the rule is not found.
@@ -60,14 +62,19 @@ func (e ChainError) Error() string {
6062
}
6163

6264
func initCheck() error {
63-
6465
if iptablesPath == "" {
6566
path, err := exec.LookPath("iptables")
6667
if err != nil {
6768
return ErrIptablesNotFound
6869
}
6970
iptablesPath = path
7071
supportsXlock = exec.Command(iptablesPath, "--wait", "-L", "-n").Run() == nil
72+
mj, mn, mc, err := GetVersion()
73+
if err != nil {
74+
logrus.Warnf("Failed to read iptables version: %v", err)
75+
return nil
76+
}
77+
supportsCOpt = supportsCOption(mj, mn, mc)
7178
}
7279
return nil
7380
}
@@ -299,20 +306,19 @@ func Exists(table Table, chain string, rule ...string) bool {
299306
table = Filter
300307
}
301308

302-
// iptables -C, --check option was added in v.1.4.11
303-
// http://ftp.netfilter.org/pub/iptables/changes-iptables-1.4.11.txt
304-
305-
// try -C
306-
// if exit status is 0 then return true, the rule exists
307-
if _, err := Raw(append([]string{
308-
"-t", string(table), "-C", chain}, rule...)...); err == nil {
309-
return true
309+
if supportsCOpt {
310+
// if exit status is 0 then return true, the rule exists
311+
_, err := Raw(append([]string{"-t", string(table), "-C", chain}, rule...)...)
312+
return err == nil
310313
}
311314

312-
// parse "iptables -S" for the rule (this checks rules in a specific chain
313-
// in a specific table)
314-
ruleString := strings.Join(rule, " ")
315-
ruleString = chain + " " + ruleString
315+
// parse "iptables -S" for the rule (it checks rules in a specific chain
316+
// in a specific table and it is very unreliable)
317+
return existsRaw(table, chain, rule...)
318+
}
319+
320+
func existsRaw(table Table, chain string, rule ...string) bool {
321+
ruleString := fmt.Sprintf("%s %s\n", chain, strings.Join(rule, " "))
316322
existingRules, _ := exec.Command(iptablesPath, "-t", string(table), "-S", chain).Output()
317323

318324
return strings.Contains(string(existingRules), ruleString)
@@ -380,3 +386,25 @@ func ExistChain(chain string, table Table) bool {
380386
}
381387
return false
382388
}
389+
390+
// GetVersion reads the iptables version numbers
391+
func GetVersion() (major, minor, micro int, err error) {
392+
out, err := Raw("--version")
393+
if err == nil {
394+
major, minor, micro = parseVersionNumbers(string(out))
395+
}
396+
return
397+
}
398+
399+
func parseVersionNumbers(input string) (major, minor, micro int) {
400+
re := regexp.MustCompile(`v\d*.\d*.\d*`)
401+
line := re.FindString(input)
402+
fmt.Sscanf(line, "v%d.%d.%d", &major, &minor, &micro)
403+
return
404+
}
405+
406+
// iptables -C, --check option was added in v.1.4.11
407+
// http://ftp.netfilter.org/pub/iptables/changes-iptables-1.4.11.txt
408+
func supportsCOption(mj, mn, mc int) bool {
409+
return mj > 1 || (mj == 1 && (mn > 4 || (mn == 4 && mc >= 11)))
410+
}

iptables/iptables_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,3 +234,79 @@ func TestCleanup(t *testing.T) {
234234
t.Fatalf("Removing chain failed. %s found in iptables-save", chainName)
235235
}
236236
}
237+
238+
func TestExistsRaw(t *testing.T) {
239+
testChain1 := "ABCD"
240+
testChain2 := "EFGH"
241+
242+
_, err := NewChain(testChain1, Filter, false)
243+
if err != nil {
244+
t.Fatal(err)
245+
}
246+
defer func() {
247+
RemoveExistingChain(testChain1, Filter)
248+
}()
249+
250+
_, err = NewChain(testChain2, Filter, false)
251+
if err != nil {
252+
t.Fatal(err)
253+
}
254+
defer func() {
255+
RemoveExistingChain(testChain2, Filter)
256+
}()
257+
258+
// Test detection over full and truncated rule string
259+
input := []struct{ rule []string }{
260+
{[]string{"-s", "172.8.9.9/32", "-j", "ACCEPT"}},
261+
{[]string{"-d", "172.8.9.0/24", "-j", "DROP"}},
262+
{[]string{"-s", "172.0.3.0/24", "-d", "172.17.0.0/24", "-p", "tcp", "-m", "tcp", "--dport", "80", "-j", testChain2}},
263+
{[]string{"-j", "RETURN"}},
264+
}
265+
266+
for i, r := range input {
267+
ruleAdd := append([]string{"-t", string(Filter), "-A", testChain1}, r.rule...)
268+
err = RawCombinedOutput(ruleAdd...)
269+
if err != nil {
270+
t.Fatalf("i=%d, err: %v", i, err)
271+
}
272+
if !existsRaw(Filter, testChain1, r.rule...) {
273+
t.Fatalf("Failed to detect rule. i=%d", i)
274+
}
275+
// Truncate the rule
276+
trg := r.rule[len(r.rule)-1]
277+
trg = trg[:len(trg)-2]
278+
r.rule[len(r.rule)-1] = trg
279+
if existsRaw(Filter, testChain1, r.rule...) {
280+
t.Fatalf("Invalid detection. i=%d", i)
281+
}
282+
}
283+
}
284+
285+
func TestGetVersion(t *testing.T) {
286+
mj, mn, mc := parseVersionNumbers("iptables v1.4.19.1-alpha")
287+
if mj != 1 || mn != 4 || mc != 19 {
288+
t.Fatalf("Failed to parse version numbers")
289+
}
290+
}
291+
292+
func TestSupportsCOption(t *testing.T) {
293+
input := []struct {
294+
mj int
295+
mn int
296+
mc int
297+
ok bool
298+
}{
299+
{1, 4, 11, true},
300+
{1, 4, 12, true},
301+
{1, 5, 0, true},
302+
{0, 4, 11, false},
303+
{0, 5, 12, false},
304+
{1, 3, 12, false},
305+
{1, 4, 10, false},
306+
}
307+
for ind, inp := range input {
308+
if inp.ok != supportsCOption(inp.mj, inp.mn, inp.mc) {
309+
t.Fatalf("Incorrect check: %d", ind)
310+
}
311+
}
312+
}

0 commit comments

Comments
 (0)