1
2
3
4
19
20 package iptables
21
22 import (
23 "bufio"
24 "bytes"
25 "context"
26 "fmt"
27 "regexp"
28 "strconv"
29 "strings"
30 "sync"
31 "time"
32
33 "k8s.io/apimachinery/pkg/util/sets"
34 utilversion "k8s.io/apimachinery/pkg/util/version"
35 utilwait "k8s.io/apimachinery/pkg/util/wait"
36 "k8s.io/klog/v2"
37 utilexec "k8s.io/utils/exec"
38 utiltrace "k8s.io/utils/trace"
39 )
40
41
42 type RulePosition string
43
44 const (
45
46 Prepend RulePosition = "-I"
47
48 Append RulePosition = "-A"
49 )
50
51
52 type Interface interface {
53
54 EnsureChain(table Table, chain Chain) (bool, error)
55
56 FlushChain(table Table, chain Chain) error
57
58 DeleteChain(table Table, chain Chain) error
59
60
61 ChainExists(table Table, chain Chain) (bool, error)
62
63 EnsureRule(position RulePosition, table Table, chain Chain, args ...string) (bool, error)
64
65 DeleteRule(table Table, chain Chain, args ...string) error
66
67 IsIPv6() bool
68
69 Protocol() Protocol
70
71 SaveInto(table Table, buffer *bytes.Buffer) error
72
73
74
75
76
77 Restore(table Table, data []byte, flush FlushFlag, counters RestoreCountersFlag) error
78
79 RestoreAll(data []byte, flush FlushFlag, counters RestoreCountersFlag) error
80
81
82
83
84
85
86
87
88
89
90 Monitor(canary Chain, tables []Table, reloadFunc func(), interval time.Duration, stopCh <-chan struct{})
91
92
93
94
95
96 HasRandomFully() bool
97
98
99 Present() bool
100 }
101
102
103 type Protocol string
104
105 const (
106
107 ProtocolIPv4 Protocol = "IPv4"
108
109 ProtocolIPv6 Protocol = "IPv6"
110 )
111
112
113 type Table string
114
115 const (
116
117 TableNAT Table = "nat"
118
119 TableFilter Table = "filter"
120
121 TableMangle Table = "mangle"
122 )
123
124
125 type Chain string
126
127 const (
128
129 ChainPostrouting Chain = "POSTROUTING"
130
131 ChainPrerouting Chain = "PREROUTING"
132
133 ChainOutput Chain = "OUTPUT"
134
135 ChainInput Chain = "INPUT"
136
137 ChainForward Chain = "FORWARD"
138 )
139
140 const (
141 cmdIPTablesSave string = "iptables-save"
142 cmdIPTablesRestore string = "iptables-restore"
143 cmdIPTables string = "iptables"
144 cmdIP6TablesRestore string = "ip6tables-restore"
145 cmdIP6TablesSave string = "ip6tables-save"
146 cmdIP6Tables string = "ip6tables"
147 )
148
149
150 type RestoreCountersFlag bool
151
152
153 const RestoreCounters RestoreCountersFlag = true
154
155
156 const NoRestoreCounters RestoreCountersFlag = false
157
158
159 type FlushFlag bool
160
161
162 const FlushTables FlushFlag = true
163
164
165 const NoFlushTables FlushFlag = false
166
167
168
169
170 var MinCheckVersion = utilversion.MustParseGeneric("1.4.11")
171
172
173
174 var RandomFullyMinVersion = utilversion.MustParseGeneric("1.6.2")
175
176
177 var WaitMinVersion = utilversion.MustParseGeneric("1.4.20")
178
179
180 var WaitIntervalMinVersion = utilversion.MustParseGeneric("1.6.1")
181
182
183 var WaitSecondsMinVersion = utilversion.MustParseGeneric("1.4.22")
184
185
186 var WaitRestoreMinVersion = utilversion.MustParseGeneric("1.6.2")
187
188
189 const WaitString = "-w"
190
191
192 const WaitSecondsValue = "5"
193
194
195 const WaitIntervalString = "-W"
196
197
198 const WaitIntervalUsecondsValue = "100000"
199
200
201 const LockfilePath16x = "/run/xtables.lock"
202
203
204 const LockfilePath14x = "@xtables"
205
206
207 type runner struct {
208 mu sync.Mutex
209 exec utilexec.Interface
210 protocol Protocol
211 hasCheck bool
212 hasRandomFully bool
213 waitFlag []string
214 restoreWaitFlag []string
215 lockfilePath14x string
216 lockfilePath16x string
217 }
218
219
220
221 func newInternal(exec utilexec.Interface, protocol Protocol, lockfilePath14x, lockfilePath16x string) Interface {
222 version, err := getIPTablesVersion(exec, protocol)
223 if err != nil {
224 klog.InfoS("Error checking iptables version, assuming version at least", "version", MinCheckVersion, "err", err)
225 version = MinCheckVersion
226 }
227
228 if lockfilePath16x == "" {
229 lockfilePath16x = LockfilePath16x
230 }
231 if lockfilePath14x == "" {
232 lockfilePath14x = LockfilePath14x
233 }
234
235 runner := &runner{
236 exec: exec,
237 protocol: protocol,
238 hasCheck: version.AtLeast(MinCheckVersion),
239 hasRandomFully: version.AtLeast(RandomFullyMinVersion),
240 waitFlag: getIPTablesWaitFlag(version),
241 restoreWaitFlag: getIPTablesRestoreWaitFlag(version, exec, protocol),
242 lockfilePath14x: lockfilePath14x,
243 lockfilePath16x: lockfilePath16x,
244 }
245 return runner
246 }
247
248
249 func New(exec utilexec.Interface, protocol Protocol) Interface {
250 return newInternal(exec, protocol, "", "")
251 }
252
253
254 func (runner *runner) EnsureChain(table Table, chain Chain) (bool, error) {
255 fullArgs := makeFullArgs(table, chain)
256
257 runner.mu.Lock()
258 defer runner.mu.Unlock()
259
260 out, err := runner.run(opCreateChain, fullArgs)
261 if err != nil {
262 if ee, ok := err.(utilexec.ExitError); ok {
263 if ee.Exited() && ee.ExitStatus() == 1 {
264 return true, nil
265 }
266 }
267 return false, fmt.Errorf("error creating chain %q: %v: %s", chain, err, out)
268 }
269 return false, nil
270 }
271
272
273 func (runner *runner) FlushChain(table Table, chain Chain) error {
274 fullArgs := makeFullArgs(table, chain)
275
276 runner.mu.Lock()
277 defer runner.mu.Unlock()
278
279 out, err := runner.run(opFlushChain, fullArgs)
280 if err != nil {
281 return fmt.Errorf("error flushing chain %q: %v: %s", chain, err, out)
282 }
283 return nil
284 }
285
286
287 func (runner *runner) DeleteChain(table Table, chain Chain) error {
288 fullArgs := makeFullArgs(table, chain)
289
290 runner.mu.Lock()
291 defer runner.mu.Unlock()
292
293 out, err := runner.run(opDeleteChain, fullArgs)
294 if err != nil {
295 return fmt.Errorf("error deleting chain %q: %v: %s", chain, err, out)
296 }
297 return nil
298 }
299
300
301 func (runner *runner) EnsureRule(position RulePosition, table Table, chain Chain, args ...string) (bool, error) {
302 fullArgs := makeFullArgs(table, chain, args...)
303
304 runner.mu.Lock()
305 defer runner.mu.Unlock()
306
307 exists, err := runner.checkRule(table, chain, args...)
308 if err != nil {
309 return false, err
310 }
311 if exists {
312 return true, nil
313 }
314 out, err := runner.run(operation(position), fullArgs)
315 if err != nil {
316 return false, fmt.Errorf("error appending rule: %v: %s", err, out)
317 }
318 return false, nil
319 }
320
321
322 func (runner *runner) DeleteRule(table Table, chain Chain, args ...string) error {
323 fullArgs := makeFullArgs(table, chain, args...)
324
325 runner.mu.Lock()
326 defer runner.mu.Unlock()
327
328 exists, err := runner.checkRule(table, chain, args...)
329 if err != nil {
330 return err
331 }
332 if !exists {
333 return nil
334 }
335 out, err := runner.run(opDeleteRule, fullArgs)
336 if err != nil {
337 return fmt.Errorf("error deleting rule: %v: %s", err, out)
338 }
339 return nil
340 }
341
342 func (runner *runner) IsIPv6() bool {
343 return runner.protocol == ProtocolIPv6
344 }
345
346 func (runner *runner) Protocol() Protocol {
347 return runner.protocol
348 }
349
350
351 func (runner *runner) SaveInto(table Table, buffer *bytes.Buffer) error {
352 runner.mu.Lock()
353 defer runner.mu.Unlock()
354
355 trace := utiltrace.New("iptables save")
356 defer trace.LogIfLong(2 * time.Second)
357
358
359 iptablesSaveCmd := iptablesSaveCommand(runner.protocol)
360 args := []string{"-t", string(table)}
361 klog.V(4).InfoS("Running", "command", iptablesSaveCmd, "arguments", args)
362 cmd := runner.exec.Command(iptablesSaveCmd, args...)
363 cmd.SetStdout(buffer)
364 stderrBuffer := bytes.NewBuffer(nil)
365 cmd.SetStderr(stderrBuffer)
366
367 err := cmd.Run()
368 if err != nil {
369 stderrBuffer.WriteTo(buffer)
370 }
371 return err
372 }
373
374
375 func (runner *runner) Restore(table Table, data []byte, flush FlushFlag, counters RestoreCountersFlag) error {
376
377 args := []string{"-T", string(table)}
378 return runner.restoreInternal(args, data, flush, counters)
379 }
380
381
382 func (runner *runner) RestoreAll(data []byte, flush FlushFlag, counters RestoreCountersFlag) error {
383
384 args := make([]string, 0)
385 return runner.restoreInternal(args, data, flush, counters)
386 }
387
388 type iptablesLocker interface {
389 Close() error
390 }
391
392
393 func (runner *runner) restoreInternal(args []string, data []byte, flush FlushFlag, counters RestoreCountersFlag) error {
394 runner.mu.Lock()
395 defer runner.mu.Unlock()
396
397 trace := utiltrace.New("iptables restore")
398 defer trace.LogIfLong(2 * time.Second)
399
400 if !flush {
401 args = append(args, "--noflush")
402 }
403 if counters {
404 args = append(args, "--counters")
405 }
406
407
408
409
410 if len(runner.restoreWaitFlag) == 0 {
411 locker, err := grabIptablesLocks(runner.lockfilePath14x, runner.lockfilePath16x)
412 if err != nil {
413 return err
414 }
415 trace.Step("Locks grabbed")
416 defer func(locker iptablesLocker) {
417 if err := locker.Close(); err != nil {
418 klog.ErrorS(err, "Failed to close iptables locks")
419 }
420 }(locker)
421 }
422
423
424 fullArgs := append(runner.restoreWaitFlag, args...)
425 iptablesRestoreCmd := iptablesRestoreCommand(runner.protocol)
426 klog.V(4).InfoS("Running", "command", iptablesRestoreCmd, "arguments", fullArgs)
427 cmd := runner.exec.Command(iptablesRestoreCmd, fullArgs...)
428 cmd.SetStdin(bytes.NewBuffer(data))
429 b, err := cmd.CombinedOutput()
430 if err != nil {
431 pErr, ok := parseRestoreError(string(b))
432 if ok {
433 return pErr
434 }
435 return fmt.Errorf("%w: %s", err, b)
436 }
437 return nil
438 }
439
440 func iptablesSaveCommand(protocol Protocol) string {
441 if protocol == ProtocolIPv6 {
442 return cmdIP6TablesSave
443 }
444 return cmdIPTablesSave
445 }
446
447 func iptablesRestoreCommand(protocol Protocol) string {
448 if protocol == ProtocolIPv6 {
449 return cmdIP6TablesRestore
450 }
451 return cmdIPTablesRestore
452 }
453
454 func iptablesCommand(protocol Protocol) string {
455 if protocol == ProtocolIPv6 {
456 return cmdIP6Tables
457 }
458 return cmdIPTables
459 }
460
461 func (runner *runner) run(op operation, args []string) ([]byte, error) {
462 return runner.runContext(context.TODO(), op, args)
463 }
464
465 func (runner *runner) runContext(ctx context.Context, op operation, args []string) ([]byte, error) {
466 iptablesCmd := iptablesCommand(runner.protocol)
467 fullArgs := append(runner.waitFlag, string(op))
468 fullArgs = append(fullArgs, args...)
469 klog.V(5).InfoS("Running", "command", iptablesCmd, "arguments", fullArgs)
470 if ctx == nil {
471 return runner.exec.Command(iptablesCmd, fullArgs...).CombinedOutput()
472 }
473 return runner.exec.CommandContext(ctx, iptablesCmd, fullArgs...).CombinedOutput()
474
475 }
476
477
478
479 func (runner *runner) checkRule(table Table, chain Chain, args ...string) (bool, error) {
480 if runner.hasCheck {
481 return runner.checkRuleUsingCheck(makeFullArgs(table, chain, args...))
482 }
483 return runner.checkRuleWithoutCheck(table, chain, args...)
484 }
485
486 var hexnumRE = regexp.MustCompile("0x0+([0-9])")
487
488 func trimhex(s string) string {
489 return hexnumRE.ReplaceAllString(s, "0x$1")
490 }
491
492
493
494
495 func (runner *runner) checkRuleWithoutCheck(table Table, chain Chain, args ...string) (bool, error) {
496 iptablesSaveCmd := iptablesSaveCommand(runner.protocol)
497 klog.V(1).InfoS("Running", "command", iptablesSaveCmd, "table", string(table))
498 out, err := runner.exec.Command(iptablesSaveCmd, "-t", string(table)).CombinedOutput()
499 if err != nil {
500 return false, fmt.Errorf("error checking rule: %v", err)
501 }
502
503
504
505
506
507
508 var argsCopy []string
509 for i := range args {
510 tmpField := strings.Trim(args[i], "\"")
511 tmpField = trimhex(tmpField)
512 argsCopy = append(argsCopy, strings.Fields(tmpField)...)
513 }
514 argset := sets.New(argsCopy...)
515
516 for _, line := range strings.Split(string(out), "\n") {
517 fields := strings.Fields(line)
518
519
520
521 if !strings.HasPrefix(line, fmt.Sprintf("-A %s", string(chain))) || len(fields) != len(argsCopy)+2 {
522 continue
523 }
524
525
526
527 for i := range fields {
528 fields[i] = strings.Trim(fields[i], "\"")
529 fields[i] = trimhex(fields[i])
530 }
531
532
533 if sets.New(fields...).IsSuperset(argset) {
534 return true, nil
535 }
536 klog.V(5).InfoS("DBG: fields is not a superset of args", "fields", fields, "arguments", args)
537 }
538
539 return false, nil
540 }
541
542
543 func (runner *runner) checkRuleUsingCheck(args []string) (bool, error) {
544 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
545 defer cancel()
546
547 out, err := runner.runContext(ctx, opCheckRule, args)
548 if ctx.Err() == context.DeadlineExceeded {
549 return false, fmt.Errorf("timed out while checking rules")
550 }
551 if err == nil {
552 return true, nil
553 }
554 if ee, ok := err.(utilexec.ExitError); ok {
555
556
557 if ee.Exited() && ee.ExitStatus() == 1 {
558 return false, nil
559 }
560 }
561 return false, fmt.Errorf("error checking rule: %v: %s", err, out)
562 }
563
564 const (
565
566 iptablesFlushTimeout = 5 * time.Second
567
568 iptablesFlushPollTime = 100 * time.Millisecond
569 )
570
571
572 func (runner *runner) Monitor(canary Chain, tables []Table, reloadFunc func(), interval time.Duration, stopCh <-chan struct{}) {
573 for {
574 _ = utilwait.PollImmediateUntil(interval, func() (bool, error) {
575 for _, table := range tables {
576 if _, err := runner.EnsureChain(table, canary); err != nil {
577 klog.ErrorS(err, "Could not set up iptables canary", "table", table, "chain", canary)
578 return false, nil
579 }
580 }
581 return true, nil
582 }, stopCh)
583
584
585 err := utilwait.PollUntil(interval, func() (bool, error) {
586 if exists, err := runner.ChainExists(tables[0], canary); exists {
587 return false, nil
588 } else if isResourceError(err) {
589 klog.ErrorS(err, "Could not check for iptables canary", "table", tables[0], "chain", canary)
590 return false, nil
591 }
592 klog.V(2).InfoS("IPTables canary deleted", "table", tables[0], "chain", canary)
593
594
595 err := utilwait.PollImmediate(iptablesFlushPollTime, iptablesFlushTimeout, func() (bool, error) {
596 for i := 1; i < len(tables); i++ {
597 if exists, err := runner.ChainExists(tables[i], canary); exists || isResourceError(err) {
598 return false, nil
599 }
600 }
601 return true, nil
602 })
603 if err != nil {
604 klog.InfoS("Inconsistent iptables state detected")
605 }
606 return true, nil
607 }, stopCh)
608 if err != nil {
609
610 for _, table := range tables {
611 _ = runner.DeleteChain(table, canary)
612 }
613 return
614 }
615
616 klog.V(2).InfoS("Reloading after iptables flush")
617 reloadFunc()
618 }
619 }
620
621
622 func (runner *runner) ChainExists(table Table, chain Chain) (bool, error) {
623 fullArgs := makeFullArgs(table, chain)
624
625 runner.mu.Lock()
626 defer runner.mu.Unlock()
627
628 trace := utiltrace.New("iptables ChainExists")
629 defer trace.LogIfLong(2 * time.Second)
630
631 _, err := runner.run(opListChain, fullArgs)
632 return err == nil, err
633 }
634
635 type operation string
636
637 const (
638 opCreateChain operation = "-N"
639 opFlushChain operation = "-F"
640 opDeleteChain operation = "-X"
641 opListChain operation = "-S"
642 opCheckRule operation = "-C"
643 opDeleteRule operation = "-D"
644 )
645
646 func makeFullArgs(table Table, chain Chain, args ...string) []string {
647 return append([]string{string(chain), "-t", string(table)}, args...)
648 }
649
650 const iptablesVersionPattern = `v([0-9]+(\.[0-9]+)+)`
651
652
653 func getIPTablesVersion(exec utilexec.Interface, protocol Protocol) (*utilversion.Version, error) {
654
655 iptablesCmd := iptablesCommand(protocol)
656 bytes, err := exec.Command(iptablesCmd, "--version").CombinedOutput()
657 if err != nil {
658 return nil, err
659 }
660 versionMatcher := regexp.MustCompile(iptablesVersionPattern)
661 match := versionMatcher.FindStringSubmatch(string(bytes))
662 if match == nil {
663 return nil, fmt.Errorf("no iptables version found in string: %s", bytes)
664 }
665 version, err := utilversion.ParseGeneric(match[1])
666 if err != nil {
667 return nil, fmt.Errorf("iptables version %q is not a valid version string: %v", match[1], err)
668 }
669
670 return version, nil
671 }
672
673
674 func getIPTablesWaitFlag(version *utilversion.Version) []string {
675 switch {
676 case version.AtLeast(WaitIntervalMinVersion):
677 return []string{WaitString, WaitSecondsValue, WaitIntervalString, WaitIntervalUsecondsValue}
678 case version.AtLeast(WaitSecondsMinVersion):
679 return []string{WaitString, WaitSecondsValue}
680 case version.AtLeast(WaitMinVersion):
681 return []string{WaitString}
682 default:
683 return nil
684 }
685 }
686
687
688 func getIPTablesRestoreWaitFlag(version *utilversion.Version, exec utilexec.Interface, protocol Protocol) []string {
689 if version.AtLeast(WaitRestoreMinVersion) {
690 return []string{WaitString, WaitSecondsValue, WaitIntervalString, WaitIntervalUsecondsValue}
691 }
692
693
694
695 vstring, err := getIPTablesRestoreVersionString(exec, protocol)
696 if err != nil || vstring == "" {
697 klog.V(3).InfoS("Couldn't get iptables-restore version; assuming it doesn't support --wait")
698 return nil
699 }
700 if _, err := utilversion.ParseGeneric(vstring); err != nil {
701 klog.V(3).InfoS("Couldn't parse iptables-restore version; assuming it doesn't support --wait")
702 return nil
703 }
704 return []string{WaitString}
705 }
706
707
708
709 func getIPTablesRestoreVersionString(exec utilexec.Interface, protocol Protocol) (string, error) {
710
711
712
713
714
715 iptablesRestoreCmd := iptablesRestoreCommand(protocol)
716 cmd := exec.Command(iptablesRestoreCmd, "--version")
717 cmd.SetStdin(bytes.NewReader([]byte{}))
718 bytes, err := cmd.CombinedOutput()
719 if err != nil {
720 return "", err
721 }
722 versionMatcher := regexp.MustCompile(iptablesVersionPattern)
723 match := versionMatcher.FindStringSubmatch(string(bytes))
724 if match == nil {
725 return "", fmt.Errorf("no iptables version found in string: %s", bytes)
726 }
727 return match[1], nil
728 }
729
730 func (runner *runner) HasRandomFully() bool {
731 return runner.hasRandomFully
732 }
733
734
735
736 func (runner *runner) Present() bool {
737 if _, err := runner.ChainExists(TableNAT, ChainPostrouting); err != nil {
738 return false
739 }
740
741 return true
742 }
743
744 var iptablesNotFoundStrings = []string{
745
746
747
748
749
750 "No chain/target/match by that name",
751
752
753
754
755
756 "No such file or directory",
757
758
759
760 "does a matching rule exist",
761
762
763
764 "does not exist",
765 }
766
767
768
769
770
771 func IsNotFoundError(err error) bool {
772 es := err.Error()
773 for _, str := range iptablesNotFoundStrings {
774 if strings.Contains(es, str) {
775 return true
776 }
777 }
778 return false
779 }
780
781 const iptablesStatusResourceProblem = 4
782
783
784
785
786 func isResourceError(err error) bool {
787 if ee, isExitError := err.(utilexec.ExitError); isExitError {
788 return ee.ExitStatus() == iptablesStatusResourceProblem
789 }
790 return false
791 }
792
793
794 type ParseError interface {
795
796
797 Line() int
798
799 Error() string
800 }
801
802 type parseError struct {
803 cmd string
804 line int
805 }
806
807 func (e parseError) Line() int {
808 return e.line
809 }
810
811 func (e parseError) Error() string {
812 return fmt.Sprintf("%s: input error on line %d: ", e.cmd, e.line)
813 }
814
815
816 type LineData struct {
817
818 Line int
819
820 Data string
821 }
822
823 var regexpParseError = regexp.MustCompile("line ([1-9][0-9]*) failed$")
824
825
826
827
828
829
830
831 func parseRestoreError(str string) (ParseError, bool) {
832 errors := strings.Split(str, ":")
833 if len(errors) != 2 {
834 return nil, false
835 }
836 cmd := errors[0]
837 matches := regexpParseError.FindStringSubmatch(errors[1])
838 if len(matches) != 2 {
839 return nil, false
840 }
841 line, errMsg := strconv.Atoi(matches[1])
842 if errMsg != nil {
843 return nil, false
844 }
845 return parseError{cmd: cmd, line: line}, true
846 }
847
848
849
850 func ExtractLines(lines []byte, line, count int) []LineData {
851
852 if line < 1 {
853 return nil
854 }
855 start := line - count
856 if start <= 0 {
857 start = 1
858 }
859 end := line + count + 1
860
861 offset := 1
862 scanner := bufio.NewScanner(bytes.NewBuffer(lines))
863 extractLines := make([]LineData, 0, count*2)
864 for scanner.Scan() {
865 if offset >= start && offset < end {
866 extractLines = append(extractLines, LineData{
867 Line: offset,
868 Data: scanner.Text(),
869 })
870 }
871 if offset == end {
872 break
873 }
874 offset++
875 }
876 return extractLines
877 }
878
View as plain text