k8s.io/kubernetes@v1.29.3/pkg/util/iptables/monitor_test.go (about)

     1  //go:build linux
     2  // +build linux
     3  
     4  /*
     5  Copyright 2019 The Kubernetes Authors.
     6  
     7  Licensed under the Apache License, Version 2.0 (the "License");
     8  you may not use this file except in compliance with the License.
     9  You may obtain a copy of the License at
    10  
    11      http://www.apache.org/licenses/LICENSE-2.0
    12  
    13  Unless required by applicable law or agreed to in writing, software
    14  distributed under the License is distributed on an "AS IS" BASIS,
    15  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    16  See the License for the specific language governing permissions and
    17  limitations under the License.
    18  */
    19  
    20  package iptables
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"io"
    26  	"sync"
    27  	"sync/atomic"
    28  	"testing"
    29  	"time"
    30  
    31  	"k8s.io/apimachinery/pkg/util/sets"
    32  	utilwait "k8s.io/apimachinery/pkg/util/wait"
    33  	"k8s.io/utils/exec"
    34  )
    35  
    36  // We can't use the normal FakeExec because we don't know precisely how many times the
    37  // Monitor thread will do its checks, and we don't know precisely how its iptables calls
    38  // will interleave with the main thread's. So we use our own fake Exec implementation that
    39  // implements a minimal iptables interface. This will need updates as iptables.runner
    40  // changes its use of Exec.
    41  type monitorFakeExec struct {
    42  	sync.Mutex
    43  
    44  	tables map[string]sets.String
    45  
    46  	block      bool
    47  	wasBlocked bool
    48  }
    49  
    50  func newMonitorFakeExec() *monitorFakeExec {
    51  	tables := make(map[string]sets.String)
    52  	tables["mangle"] = sets.NewString()
    53  	tables["filter"] = sets.NewString()
    54  	tables["nat"] = sets.NewString()
    55  	return &monitorFakeExec{tables: tables}
    56  }
    57  
    58  func (mfe *monitorFakeExec) blockIPTables(block bool) {
    59  	mfe.Lock()
    60  	defer mfe.Unlock()
    61  
    62  	mfe.block = block
    63  }
    64  
    65  func (mfe *monitorFakeExec) getWasBlocked() bool {
    66  	mfe.Lock()
    67  	defer mfe.Unlock()
    68  
    69  	wasBlocked := mfe.wasBlocked
    70  	mfe.wasBlocked = false
    71  	return wasBlocked
    72  }
    73  
    74  func (mfe *monitorFakeExec) Command(cmd string, args ...string) exec.Cmd {
    75  	return &monitorFakeCmd{mfe: mfe, cmd: cmd, args: args}
    76  }
    77  
    78  func (mfe *monitorFakeExec) CommandContext(ctx context.Context, cmd string, args ...string) exec.Cmd {
    79  	return mfe.Command(cmd, args...)
    80  }
    81  
    82  func (mfe *monitorFakeExec) LookPath(file string) (string, error) {
    83  	return file, nil
    84  }
    85  
    86  type monitorFakeCmd struct {
    87  	mfe  *monitorFakeExec
    88  	cmd  string
    89  	args []string
    90  }
    91  
    92  func (mfc *monitorFakeCmd) CombinedOutput() ([]byte, error) {
    93  	if mfc.cmd == cmdIPTablesRestore {
    94  		// Only used for "iptables-restore --version", and the result doesn't matter
    95  		return []byte{}, nil
    96  	} else if mfc.cmd != cmdIPTables {
    97  		panic("bad command " + mfc.cmd)
    98  	}
    99  
   100  	if len(mfc.args) == 1 && mfc.args[0] == "--version" {
   101  		return []byte("iptables v1.6.2"), nil
   102  	}
   103  
   104  	if len(mfc.args) != 8 || mfc.args[0] != WaitString || mfc.args[1] != WaitSecondsValue || mfc.args[2] != WaitIntervalString || mfc.args[3] != WaitIntervalUsecondsValue || mfc.args[6] != "-t" {
   105  		panic(fmt.Sprintf("bad args %#v", mfc.args))
   106  	}
   107  	op := operation(mfc.args[4])
   108  	chainName := mfc.args[5]
   109  	tableName := mfc.args[7]
   110  
   111  	mfc.mfe.Lock()
   112  	defer mfc.mfe.Unlock()
   113  
   114  	table := mfc.mfe.tables[tableName]
   115  	if table == nil {
   116  		return []byte{}, fmt.Errorf("no such table %q", tableName)
   117  	}
   118  
   119  	// For ease-of-testing reasons, blockIPTables blocks create and list, but not delete
   120  	if mfc.mfe.block && op != opDeleteChain {
   121  		mfc.mfe.wasBlocked = true
   122  		return []byte{}, exec.CodeExitError{Code: 4, Err: fmt.Errorf("could not get xtables.lock, etc")}
   123  	}
   124  
   125  	switch op {
   126  	case opCreateChain:
   127  		if !table.Has(chainName) {
   128  			table.Insert(chainName)
   129  		}
   130  		return []byte{}, nil
   131  	case opListChain:
   132  		if table.Has(chainName) {
   133  			return []byte{}, nil
   134  		}
   135  		return []byte{}, fmt.Errorf("no such chain %q", chainName)
   136  	case opDeleteChain:
   137  		table.Delete(chainName)
   138  		return []byte{}, nil
   139  	default:
   140  		panic("should not be reached")
   141  	}
   142  }
   143  
   144  func (mfc *monitorFakeCmd) SetStdin(in io.Reader) {
   145  	// Used by getIPTablesRestoreVersionString(), can be ignored
   146  }
   147  
   148  func (mfc *monitorFakeCmd) Run() error {
   149  	panic("should not be reached")
   150  }
   151  func (mfc *monitorFakeCmd) Output() ([]byte, error) {
   152  	panic("should not be reached")
   153  }
   154  func (mfc *monitorFakeCmd) SetDir(dir string) {
   155  	panic("should not be reached")
   156  }
   157  func (mfc *monitorFakeCmd) SetStdout(out io.Writer) {
   158  	panic("should not be reached")
   159  }
   160  func (mfc *monitorFakeCmd) SetStderr(out io.Writer) {
   161  	panic("should not be reached")
   162  }
   163  func (mfc *monitorFakeCmd) SetEnv(env []string) {
   164  	panic("should not be reached")
   165  }
   166  func (mfc *monitorFakeCmd) StdoutPipe() (io.ReadCloser, error) {
   167  	panic("should not be reached")
   168  }
   169  func (mfc *monitorFakeCmd) StderrPipe() (io.ReadCloser, error) {
   170  	panic("should not be reached")
   171  }
   172  func (mfc *monitorFakeCmd) Start() error {
   173  	panic("should not be reached")
   174  }
   175  func (mfc *monitorFakeCmd) Wait() error {
   176  	panic("should not be reached")
   177  }
   178  func (mfc *monitorFakeCmd) Stop() {
   179  	panic("should not be reached")
   180  }
   181  
   182  func TestIPTablesMonitor(t *testing.T) {
   183  	mfe := newMonitorFakeExec()
   184  	ipt := New(mfe, ProtocolIPv4)
   185  
   186  	var reloads uint32
   187  	stopCh := make(chan struct{})
   188  
   189  	canary := Chain("MONITOR-TEST-CANARY")
   190  	tables := []Table{TableMangle, TableFilter, TableNAT}
   191  	go ipt.Monitor(canary, tables, func() {
   192  		if !ensureNoChains(mfe) {
   193  			t.Errorf("reload called while canaries still exist")
   194  		}
   195  		atomic.AddUint32(&reloads, 1)
   196  	}, 100*time.Millisecond, stopCh)
   197  
   198  	// Monitor should create canary chains quickly
   199  	if err := waitForChains(mfe, canary, tables); err != nil {
   200  		t.Errorf("failed to create iptables canaries: %v", err)
   201  	}
   202  
   203  	if err := waitForReloads(&reloads, 0); err != nil {
   204  		t.Errorf("got unexpected reloads: %v", err)
   205  	}
   206  
   207  	// If we delete all of the chains, it should reload
   208  	ipt.DeleteChain(TableMangle, canary)
   209  	ipt.DeleteChain(TableFilter, canary)
   210  	ipt.DeleteChain(TableNAT, canary)
   211  
   212  	if err := waitForReloads(&reloads, 1); err != nil {
   213  		t.Errorf("got unexpected number of reloads after flush: %v", err)
   214  	}
   215  	if err := waitForChains(mfe, canary, tables); err != nil {
   216  		t.Errorf("failed to create iptables canaries: %v", err)
   217  	}
   218  
   219  	// If we delete two chains, it should not reload yet
   220  	ipt.DeleteChain(TableMangle, canary)
   221  	ipt.DeleteChain(TableFilter, canary)
   222  
   223  	if err := waitForNoReload(&reloads, 1); err != nil {
   224  		t.Errorf("got unexpected number of reloads after partial flush: %v", err)
   225  	}
   226  
   227  	// Now ensure that "iptables -L" will get an error about the xtables.lock, and
   228  	// delete the last chain. The monitor should not reload, because it can't actually
   229  	// tell if the chain was deleted or not.
   230  	mfe.blockIPTables(true)
   231  	ipt.DeleteChain(TableNAT, canary)
   232  	if err := waitForBlocked(mfe); err != nil {
   233  		t.Errorf("failed waiting for monitor to be blocked from monitoring: %v", err)
   234  	}
   235  
   236  	// After unblocking the monitor, it should now reload
   237  	mfe.blockIPTables(false)
   238  
   239  	if err := waitForReloads(&reloads, 2); err != nil {
   240  		t.Errorf("got unexpected number of reloads after slow flush: %v", err)
   241  	}
   242  	if err := waitForChains(mfe, canary, tables); err != nil {
   243  		t.Errorf("failed to create iptables canaries: %v", err)
   244  	}
   245  
   246  	// If we close the stop channel, it should stop running
   247  	close(stopCh)
   248  
   249  	if err := waitForNoReload(&reloads, 2); err != nil {
   250  		t.Errorf("got unexpected number of reloads after stop: %v", err)
   251  	}
   252  	if !ensureNoChains(mfe) {
   253  		t.Errorf("canaries still exist after stopping monitor")
   254  	}
   255  
   256  	// If we create a new monitor while the iptables lock is held, it will
   257  	// retry creating canaries until it succeeds
   258  
   259  	stopCh = make(chan struct{})
   260  	_ = mfe.getWasBlocked()
   261  	mfe.blockIPTables(true)
   262  	go ipt.Monitor(canary, tables, func() {
   263  		if !ensureNoChains(mfe) {
   264  			t.Errorf("reload called while canaries still exist")
   265  		}
   266  		atomic.AddUint32(&reloads, 1)
   267  	}, 100*time.Millisecond, stopCh)
   268  
   269  	// Monitor should not have created canaries yet
   270  	if !ensureNoChains(mfe) {
   271  		t.Errorf("canary created while iptables blocked")
   272  	}
   273  
   274  	if err := waitForBlocked(mfe); err != nil {
   275  		t.Errorf("failed waiting for monitor to fail creating canaries: %v", err)
   276  	}
   277  
   278  	mfe.blockIPTables(false)
   279  	if err := waitForChains(mfe, canary, tables); err != nil {
   280  		t.Errorf("failed to create iptables canaries: %v", err)
   281  	}
   282  
   283  	close(stopCh)
   284  }
   285  
   286  func waitForChains(mfe *monitorFakeExec, canary Chain, tables []Table) error {
   287  	return utilwait.PollImmediate(100*time.Millisecond, time.Second, func() (bool, error) {
   288  		mfe.Lock()
   289  		defer mfe.Unlock()
   290  
   291  		for _, table := range tables {
   292  			if !mfe.tables[string(table)].Has(string(canary)) {
   293  				return false, nil
   294  			}
   295  		}
   296  		return true, nil
   297  	})
   298  }
   299  
   300  func ensureNoChains(mfe *monitorFakeExec) bool {
   301  	mfe.Lock()
   302  	defer mfe.Unlock()
   303  	return mfe.tables["mangle"].Len() == 0 &&
   304  		mfe.tables["filter"].Len() == 0 &&
   305  		mfe.tables["nat"].Len() == 0
   306  }
   307  
   308  func waitForReloads(reloads *uint32, expected uint32) error {
   309  	if atomic.LoadUint32(reloads) < expected {
   310  		utilwait.PollImmediate(100*time.Millisecond, time.Second, func() (bool, error) {
   311  			return atomic.LoadUint32(reloads) >= expected, nil
   312  		})
   313  	}
   314  	got := atomic.LoadUint32(reloads)
   315  	if got != expected {
   316  		return fmt.Errorf("expected %d, got %d", expected, got)
   317  	}
   318  	return nil
   319  }
   320  
   321  func waitForNoReload(reloads *uint32, expected uint32) error {
   322  	utilwait.PollImmediate(50*time.Millisecond, 250*time.Millisecond, func() (bool, error) {
   323  		return atomic.LoadUint32(reloads) > expected, nil
   324  	})
   325  
   326  	got := atomic.LoadUint32(reloads)
   327  	if got != expected {
   328  		return fmt.Errorf("expected %d, got %d", expected, got)
   329  	}
   330  	return nil
   331  }
   332  
   333  func waitForBlocked(mfe *monitorFakeExec) error {
   334  	return utilwait.PollImmediate(100*time.Millisecond, time.Second, func() (bool, error) {
   335  		blocked := mfe.getWasBlocked()
   336  		return blocked, nil
   337  	})
   338  }