Skip to content

Commit e785537

Browse files
committed
*: add file-based lock for old iptables versions
iptables <1.4.20 does not support blocking (the `--wait` option) to prevent concurrent invocations from interrupting each other. To work around this, this patch adds a file-based lock (flock), using the same file that xtables now uses [1] (`/run/xtables.lock`). Note that this follows a similar behaviour to xtables in that it's best-effort only: if the lock can't be acquired for some reason, operations proceed anyway. h/t to @janeczku and @adfernandes for original fix suggestions [1]: http://git.netfilter.org/iptables/commit/?id=aa562a660d1555b13cffbac1e744033e91f82707
1 parent 8bfb52a commit e785537

File tree

2 files changed

+162
-31
lines changed

2 files changed

+162
-31
lines changed

iptables/iptables.go

+78-31
Original file line numberDiff line numberDiff line change
@@ -40,41 +40,53 @@ func (e *Error) Error() string {
4040
}
4141

4242
type IPTables struct {
43-
path string
43+
path string
44+
hasCheck bool
45+
hasWait bool
46+
47+
fmu *fileLock
4448
}
4549

4650
func New() (*IPTables, error) {
4751
path, err := exec.LookPath("iptables")
4852
if err != nil {
4953
return nil, err
5054
}
51-
52-
return &IPTables{path}, nil
53-
}
54-
55-
// Exists checks if given rulespec in specified table/chain exists
56-
func (ipt *IPTables) Exists(table, chain string, rulespec ...string) (bool, error) {
57-
checkPresent, err := getIptablesHasCheckCommand()
55+
checkPresent, waitPresent, err := getIptablesCommandSupport()
5856
if err != nil {
59-
log.Printf("Error checking iptables version, assuming version at least 1.4.11: %v", err)
57+
log.Printf("Error checking iptables version, assuming version at least 1.4.20: %v", err)
6058
checkPresent = true
59+
waitPresent = true
6160
}
61+
ipt := IPTables{
62+
path: path,
63+
hasCheck: checkPresent,
64+
hasWait: waitPresent,
65+
}
66+
if !waitPresent {
67+
ipt.fmu, err = newXtablesFileLock()
68+
if err != nil {
69+
return nil, err
70+
}
71+
}
72+
return &ipt, nil
73+
}
6274

63-
if !checkPresent {
75+
// Exists checks if given rulespec in specified table/chain exists
76+
func (ipt *IPTables) Exists(table, chain string, rulespec ...string) (bool, error) {
77+
if !ipt.hasCheck {
6478
cmd := append([]string{"-A", chain}, rulespec...)
6579
return existsForOldIpTables(table, strings.Join(cmd, " "))
66-
} else {
67-
cmd := append([]string{"-t", table, "-C", chain}, rulespec...)
68-
err := ipt.run(cmd...)
69-
70-
switch {
71-
case err == nil:
72-
return true, nil
73-
case err.(*Error).ExitStatus() == 1:
74-
return false, nil
75-
default:
76-
return false, err
77-
}
80+
}
81+
cmd := append([]string{"-t", table, "-C", chain}, rulespec...)
82+
err := ipt.run(cmd...)
83+
switch {
84+
case err == nil:
85+
return true, nil
86+
case err.(*Error).ExitStatus() == 1:
87+
return false, nil
88+
default:
89+
return false, err
7890
}
7991
}
8092

@@ -113,9 +125,21 @@ func (ipt *IPTables) Delete(table, chain string, rulespec ...string) error {
113125
// List rules in specified table/chain
114126
func (ipt *IPTables) List(table, chain string) ([]string, error) {
115127
var stdout, stderr bytes.Buffer
128+
args := []string{ipt.path, "-t", table, "-S", chain}
129+
130+
if ipt.hasWait {
131+
args = append(args, "--wait")
132+
} else {
133+
ul, err := ipt.fmu.tryLock()
134+
if err != nil {
135+
return nil, err
136+
}
137+
defer ul.Unlock()
138+
}
139+
116140
cmd := exec.Cmd{
117141
Path: ipt.path,
118-
Args: []string{ipt.path, "--wait", "-t", table, "-S", chain},
142+
Args: args,
119143
Stdout: &stdout,
120144
Stderr: &stderr,
121145
}
@@ -136,8 +160,8 @@ func (ipt *IPTables) NewChain(table, chain string) error {
136160
return ipt.run("-t", table, "-N", chain)
137161
}
138162

139-
// ClearChain flushed (deletes all rules) in the specifed table/chain.
140-
// If the chain does not exist, new one will be created
163+
// ClearChain flushed (deletes all rules) in the specified table/chain.
164+
// If the chain does not exist, a new one will be created
141165
func (ipt *IPTables) ClearChain(table, chain string) error {
142166
err := ipt.NewChain(table, chain)
143167

@@ -160,7 +184,16 @@ func (ipt *IPTables) DeleteChain(table, chain string) error {
160184

161185
func (ipt *IPTables) run(args ...string) error {
162186
var stderr bytes.Buffer
163-
args = append([]string{"--wait"}, args...)
187+
if ipt.hasWait {
188+
args = append([]string{"--wait"}, args...)
189+
} else {
190+
ul, err := ipt.fmu.tryLock()
191+
if err != nil {
192+
return err
193+
}
194+
defer ul.Unlock()
195+
}
196+
164197
cmd := exec.Cmd{
165198
Path: ipt.path,
166199
Args: append([]string{ipt.path}, args...),
@@ -174,19 +207,19 @@ func (ipt *IPTables) run(args ...string) error {
174207
return nil
175208
}
176209

177-
// Checks if iptables has the "-C" flag
178-
func getIptablesHasCheckCommand() (bool, error) {
210+
// Checks if iptables has the "-C" and "--wait" flag
211+
func getIptablesCommandSupport() (bool, bool, error) {
179212
vstring, err := getIptablesVersionString()
180213
if err != nil {
181-
return false, err
214+
return false, false, err
182215
}
183216

184217
v1, v2, v3, err := extractIptablesVersion(vstring)
185218
if err != nil {
186-
return false, err
219+
return false, false, err
187220
}
188221

189-
return iptablesHasCheckCommand(v1, v2, v3), nil
222+
return iptablesHasCheckCommand(v1, v2, v3), iptablesHasWaitCommand(v1, v2, v3), nil
190223
}
191224

192225
// getIptablesVersion returns the first three components of the iptables version.
@@ -242,6 +275,20 @@ func iptablesHasCheckCommand(v1 int, v2 int, v3 int) bool {
242275
return false
243276
}
244277

278+
// Checks if an iptables version is after 1.4.20, when --wait was added
279+
func iptablesHasWaitCommand(v1 int, v2 int, v3 int) bool {
280+
if v1 > 1 {
281+
return true
282+
}
283+
if v1 == 1 && v2 > 4 {
284+
return true
285+
}
286+
if v1 == 1 && v2 == 4 && v3 >= 20 {
287+
return true
288+
}
289+
return false
290+
}
291+
245292
// Checks if a rule specification exists for a table
246293
func existsForOldIpTables(table string, ruleSpec string) (bool, error) {
247294
cmd := exec.Command("iptables", "-t", table, "-S")

iptables/lock.go

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright 2015 CoreOS, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package iptables
16+
17+
import (
18+
"os"
19+
"sync"
20+
"syscall"
21+
)
22+
23+
const (
24+
// In earlier versions of iptables, the xtables lock was implemented
25+
// via a Unix socket, but now flock is used via this lockfile:
26+
// http://git.netfilter.org/iptables/commit/?id=aa562a660d1555b13cffbac1e744033e91f82707
27+
// Note the LSB-conforming "/run" directory does not exist on old
28+
// distributions, so assume "/var" is symlinked
29+
xtablesLockFilePath = "/var/run/xtables.lock"
30+
31+
defaultFilePerm = 0600
32+
)
33+
34+
type Unlocker interface {
35+
Unlock() error
36+
}
37+
38+
type nopUnlocker struct{}
39+
40+
func (_ nopUnlocker) Unlock() error { return nil }
41+
42+
type fileLock struct {
43+
// mu is used to protect against concurrent invocations from within this process
44+
mu sync.Mutex
45+
fd int
46+
}
47+
48+
// tryLock takes an exclusive lock on the xtables lock file without blocking.
49+
// This is best-effort only: if the exclusive lock would block (i.e. because
50+
// another process already holds it), no error is returned. Otherwise, any
51+
// error encountered during the locking operation is returned.
52+
// The returned Unlocker should be used to release the lock when the caller is
53+
// done invoking iptables commands.
54+
func (l *fileLock) tryLock() (Unlocker, error) {
55+
l.mu.Lock()
56+
err := syscall.Flock(l.fd, syscall.LOCK_EX|syscall.LOCK_NB)
57+
switch err {
58+
case syscall.EWOULDBLOCK:
59+
l.mu.Unlock()
60+
return nopUnlocker{}, nil
61+
case nil:
62+
return l, nil
63+
default:
64+
l.mu.Unlock()
65+
return nil, err
66+
}
67+
}
68+
69+
// Unlock closes the underlying file, which implicitly unlocks it as well. It
70+
// also unlocks the associated mutex.
71+
func (l *fileLock) Unlock() error {
72+
defer l.mu.Unlock()
73+
return syscall.Close(l.fd)
74+
}
75+
76+
// newXtablesFileLock opens a new lock on the xtables lockfile without
77+
// acquiring the lock
78+
func newXtablesFileLock() (*fileLock, error) {
79+
fd, err := syscall.Open(xtablesLockFilePath, os.O_CREATE, defaultFilePerm)
80+
if err != nil {
81+
return nil, err
82+
}
83+
return &fileLock{fd: fd}, nil
84+
}

0 commit comments

Comments
 (0)