github.com/aporeto-inc/trireme-lib@v10.358.0+incompatible/controller/pkg/aclprovider/iptablesprovider.go (about)

     1  // +build linux darwin
     2  
     3  package provider
     4  
     5  import (
     6  	"bytes"
     7  	"errors"
     8  	"fmt"
     9  	"os/exec"
    10  	"strings"
    11  	"sync"
    12  
    13  	"go.uber.org/zap"
    14  )
    15  
    16  // IptablesProvider is an abstraction of all the methods an implementation of userspace
    17  // iptables need to provide.
    18  type IptablesProvider interface {
    19  	BaseIPTables
    20  	// Commit will commit changes if it is a batch provider.
    21  	Commit() error
    22  	// RetrieveTable allows a caller to retrieve the final table.
    23  	RetrieveTable() map[string]map[string][]string
    24  	// ResetRules resets the rules to a state where rules with the substring subs are removed
    25  	ResetRules(subs string) error
    26  }
    27  
    28  // BaseIPTables is the base interface of iptables functions.
    29  type BaseIPTables interface {
    30  	// Append apends a rule to chain of table
    31  	Append(table, chain string, rulespec ...string) error
    32  	// Insert inserts a rule to a chain of table at the required pos
    33  	Insert(table, chain string, pos int, rulespec ...string) error
    34  	// Delete deletes a rule of a chain in the given table
    35  	Delete(table, chain string, rulespec ...string) error
    36  	// ListChains lists all the chains associated with a table
    37  	ListChains(table string) ([]string, error)
    38  	// ClearChain clears a chain in a table
    39  	ClearChain(table, chain string) error
    40  	// DeleteChain deletes a chain in the table. There should be no references to this chain
    41  	DeleteChain(table, chain string) error
    42  	// NewChain creates a new chain
    43  	NewChain(table, chain string) error
    44  	// ListRules lists the rules in the table/chain passed to it
    45  	ListRules(table, chain string) ([]string, error)
    46  }
    47  
    48  // BatchProvider uses iptables-restore to program ACLs
    49  type BatchProvider struct {
    50  	ipt BaseIPTables
    51  
    52  	//        TABLE      CHAIN    RULES
    53  	rules       map[string]map[string][]string
    54  	batchTables map[string]bool
    55  
    56  	// Allowing for custom commit functions for testing
    57  	commitFunc  func(buf *bytes.Buffer) error
    58  	customChain string
    59  	sync.Mutex
    60  	cmd        string
    61  	restoreCmd string
    62  	saveCmd    string
    63  	quote      bool
    64  }
    65  
    66  const (
    67  	cmdV4        = "iptables --wait"
    68  	cmdV6        = "ip6tables --wait"
    69  	restoreCmdV4 = "iptables-restore"
    70  	restoreCmdV6 = "ip6tables-restore"
    71  	saveCmdV4    = "iptables-save"
    72  	saveCmdV6    = "ip6tables-save"
    73  )
    74  
    75  // TestIptablesPinned returns error if the kernel doesn't support bpf pinning in iptables
    76  func TestIptablesPinned(bpf string) error {
    77  	cmd := exec.Command("aporeto-iptables", strings.Fields("iptables --wait -t mangle -I OUTPUT -m bpf --object-pinned "+bpf+" -j LOG")...)
    78  	if _, err := cmd.CombinedOutput(); err != nil {
    79  		return err
    80  	}
    81  
    82  	cmd = exec.Command("aporeto-iptables", strings.Fields("iptables --wait -t mangle -D OUTPUT -m bpf --object-pinned "+bpf+" -j LOG")...)
    83  	if _, err := cmd.CombinedOutput(); err != nil {
    84  		zap.L().Error("Error removing rule", zap.Error(err))
    85  	}
    86  
    87  	return nil
    88  }
    89  
    90  // NewGoIPTablesProviderV4 returns an IptablesProvider interface based on the go-iptables
    91  // external package.
    92  func NewGoIPTablesProviderV4(batchTables []string, customChain string) (IptablesProvider, error) {
    93  
    94  	batchTablesMap := map[string]bool{}
    95  	for _, t := range batchTables {
    96  		batchTablesMap[t] = true
    97  	}
    98  
    99  	b := &BatchProvider{
   100  		cmd:         cmdV4,
   101  		rules:       map[string]map[string][]string{},
   102  		batchTables: batchTablesMap,
   103  		restoreCmd:  restoreCmdV4,
   104  		saveCmd:     saveCmdV4,
   105  		customChain: customChain,
   106  		quote:       true,
   107  	}
   108  
   109  	b.commitFunc = b.restore
   110  
   111  	return b, nil
   112  }
   113  
   114  // NewGoIPTablesProviderV6 returns an IptablesProvider interface based on the go-iptables
   115  // external package.
   116  func NewGoIPTablesProviderV6(batchTables []string, customChain string) (IptablesProvider, error) {
   117  
   118  	batchTablesMap := map[string]bool{}
   119  	for _, t := range batchTables {
   120  		batchTablesMap[t] = true
   121  	}
   122  
   123  	b := &BatchProvider{
   124  		cmd:         cmdV6,
   125  		rules:       map[string]map[string][]string{},
   126  		batchTables: batchTablesMap,
   127  		customChain: customChain,
   128  		restoreCmd:  restoreCmdV6,
   129  		saveCmd:     saveCmdV6,
   130  		quote:       true,
   131  	}
   132  
   133  	b.commitFunc = b.restore
   134  
   135  	return b, nil
   136  }
   137  
   138  // NewCustomBatchProvider is a custom batch provider wher the downstream
   139  // iptables utility is provided by the caller. Very useful for testing
   140  // the ACL functions with a mock.
   141  func NewCustomBatchProvider(ipt BaseIPTables, commit func(buf *bytes.Buffer) error, batchTables []string) *BatchProvider {
   142  
   143  	batchTablesMap := map[string]bool{}
   144  
   145  	for _, t := range batchTables {
   146  		batchTablesMap[t] = true
   147  	}
   148  
   149  	return &BatchProvider{
   150  		ipt:         ipt,
   151  		rules:       map[string]map[string][]string{},
   152  		batchTables: batchTablesMap,
   153  		commitFunc:  commit,
   154  	}
   155  }
   156  
   157  func createIPtablesCommand(iptablesCmd, table, chain, action string, rulespec ...string) []string {
   158  	cmd := strings.Fields(iptablesCmd)
   159  	cmd = append(cmd, "-t")
   160  	cmd = append(cmd, table)
   161  	cmd = append(cmd, action)
   162  	cmd = append(cmd, chain)
   163  	cmd = append(cmd, rulespec...)
   164  	return cmd
   165  }
   166  
   167  // Append will append the provided rule to the local cache or call
   168  // directly the iptables command depending on the table.
   169  func (b *BatchProvider) Append(table, chain string, rulespec ...string) error {
   170  	b.Lock()
   171  	defer b.Unlock()
   172  
   173  	if len(rulespec) == 0 {
   174  		return nil
   175  	}
   176  
   177  	if _, ok := b.batchTables[table]; !ok {
   178  		cmd := createIPtablesCommand(b.cmd, table, chain, "-A", rulespec...)
   179  		execCmd := exec.Command("aporeto-iptables", cmd...)
   180  		s, err := execCmd.CombinedOutput()
   181  		if err != nil {
   182  			return errors.New(string(s))
   183  		}
   184  
   185  		return nil
   186  	}
   187  
   188  	if _, ok := b.rules[table]; !ok {
   189  		b.rules[table] = map[string][]string{}
   190  	}
   191  
   192  	if _, ok := b.rules[table][chain]; !ok {
   193  		b.rules[table][chain] = []string{}
   194  	}
   195  
   196  	b.quoteRulesSpec(rulespec)
   197  
   198  	rule := strings.Join(rulespec, " ")
   199  	b.rules[table][chain] = append(b.rules[table][chain], rule)
   200  
   201  	return nil
   202  }
   203  
   204  // Insert will insert the rule in the corresponding position in the local
   205  // cache or call the corresponding iptables command, depending on the table.
   206  func (b *BatchProvider) Insert(table, chain string, pos int, rulespec ...string) error {
   207  
   208  	b.Lock()
   209  	defer b.Unlock()
   210  
   211  	if _, ok := b.batchTables[table]; !ok {
   212  		cmd := createIPtablesCommand(b.cmd, table, chain, "-I", rulespec...)
   213  		execCmd := exec.Command("aporeto-iptables", cmd...)
   214  		s, err := execCmd.CombinedOutput()
   215  		if err != nil {
   216  			return errors.New(string(s))
   217  		}
   218  		return nil
   219  	}
   220  
   221  	if _, ok := b.rules[table]; !ok {
   222  		b.rules[table] = map[string][]string{}
   223  	}
   224  
   225  	if _, ok := b.rules[table][chain]; !ok {
   226  		b.rules[table][chain] = []string{}
   227  	}
   228  
   229  	b.quoteRulesSpec(rulespec)
   230  
   231  	rule := strings.Join(rulespec, " ")
   232  
   233  	if pos == 1 {
   234  		b.rules[table][chain] = append([]string{rule}, b.rules[table][chain]...)
   235  	} else if pos > len(b.rules[table][chain]) {
   236  		b.rules[table][chain] = append(b.rules[table][chain], rule)
   237  	} else {
   238  		b.rules[table][chain] = append(b.rules[table][chain], "newvalue")
   239  		copy(b.rules[table][chain][pos-1:], b.rules[table][chain][pos-2:])
   240  		b.rules[table][chain][pos-1] = rule
   241  	}
   242  
   243  	return nil
   244  }
   245  
   246  // Delete will delete the rule from the local cache or the system.
   247  func (b *BatchProvider) Delete(table, chain string, rulespec ...string) error {
   248  	b.Lock()
   249  	defer b.Unlock()
   250  
   251  	if _, ok := b.batchTables[table]; !ok {
   252  		cmd := createIPtablesCommand(b.cmd, table, chain, "-D", rulespec...)
   253  		execCmd := exec.Command("aporeto-iptables", cmd...)
   254  		s, err := execCmd.CombinedOutput()
   255  		if err != nil {
   256  			return errors.New(string(s))
   257  		}
   258  		return nil
   259  	}
   260  
   261  	if _, ok := b.rules[table]; !ok {
   262  		return nil
   263  	}
   264  
   265  	if _, ok := b.rules[table][chain]; !ok {
   266  		return nil
   267  	}
   268  
   269  	b.quoteRulesSpec(rulespec)
   270  
   271  	rule := strings.Join(rulespec, " ")
   272  	for index, r := range b.rules[table][chain] {
   273  		if rule == r {
   274  			switch index {
   275  			case 0:
   276  				if len(b.rules[table][chain]) == 1 {
   277  					b.rules[table][chain] = []string{}
   278  				} else {
   279  					b.rules[table][chain] = b.rules[table][chain][1:]
   280  				}
   281  			case len(b.rules[table][chain]) - 1:
   282  				b.rules[table][chain] = b.rules[table][chain][:index]
   283  			default:
   284  				b.rules[table][chain] = append(b.rules[table][chain][:index], b.rules[table][chain][index+1:]...)
   285  			}
   286  			break
   287  		}
   288  	}
   289  
   290  	return nil
   291  }
   292  
   293  // ListChains returns a slice containing the name of each chain in the specified table.
   294  func listChains(iptablesCmd, table string) ([]string, error) {
   295  	cmd := strings.Fields(iptablesCmd)
   296  	cmd = append(cmd, []string{"-t", table, "-S"}...)
   297  
   298  	execCmd := exec.Command("aporeto-iptables", cmd...)
   299  	out, err := execCmd.CombinedOutput()
   300  	if err != nil {
   301  		return nil, errors.New(string(out))
   302  	}
   303  
   304  	result := strings.Split(string(out), "\n")
   305  
   306  	// Iterate over rules to find all default (-P) and user-specified (-N) chains.
   307  	// Chains definition always come before rules.
   308  	// Format is the following:
   309  	// -P OUTPUT ACCEPT
   310  	// -N Custom
   311  	var chains []string
   312  	for _, val := range result {
   313  		if strings.HasPrefix(val, "-P") || strings.HasPrefix(val, "-N") {
   314  			chains = append(chains, strings.Fields(val)[1])
   315  		} else {
   316  			break
   317  		}
   318  	}
   319  	return chains, nil
   320  }
   321  
   322  // ListChains will provide a list of the current chains.
   323  func (b *BatchProvider) ListChains(table string) ([]string, error) {
   324  	b.Lock()
   325  	defer b.Unlock()
   326  
   327  	chains, err := listChains(b.cmd, table)
   328  	if err != nil {
   329  		return []string{}, err
   330  	}
   331  
   332  	if _, ok := b.batchTables[table]; !ok || b.rules[table] == nil {
   333  		return chains, nil
   334  	}
   335  
   336  	for _, chain := range chains {
   337  		if _, ok := b.rules[table][chain]; !ok {
   338  			b.rules[table][chain] = []string{}
   339  		}
   340  	}
   341  
   342  	allChains := make([]string, len(b.rules[table]))
   343  	i := 0
   344  	for chain := range b.rules[table] {
   345  		allChains[i] = chain
   346  		i++
   347  	}
   348  
   349  	return allChains, nil
   350  }
   351  
   352  // ClearChain will clear the chains.
   353  func (b *BatchProvider) ClearChain(table, chain string) error {
   354  
   355  	b.Lock()
   356  	defer b.Unlock()
   357  
   358  	if _, ok := b.batchTables[table]; !ok {
   359  		cmd := strings.Fields(b.cmd)
   360  		cmd = append(cmd, []string{"-t", table, "-F", chain}...)
   361  		execCmd := exec.Command("aporeto-iptables", cmd...)
   362  		s, err := execCmd.CombinedOutput()
   363  		if err != nil {
   364  			return errors.New(string(s))
   365  		}
   366  		return nil
   367  	}
   368  
   369  	if _, ok := b.rules[table]; !ok {
   370  		return nil
   371  	}
   372  	if _, ok := b.rules[table][chain]; !ok {
   373  		return nil
   374  	}
   375  
   376  	b.rules[table][chain] = []string{}
   377  	return nil
   378  }
   379  
   380  // DeleteChain will delete the chains.
   381  func (b *BatchProvider) DeleteChain(table, chain string) error {
   382  	b.Lock()
   383  	defer b.Unlock()
   384  
   385  	if _, ok := b.batchTables[table]; !ok {
   386  		cmd := strings.Fields(b.cmd)
   387  		cmd = append(cmd, []string{"-t", table, "-X", chain}...)
   388  		execCmd := exec.Command("aporeto-iptables", cmd...)
   389  		s, err := execCmd.CombinedOutput()
   390  		if err != nil {
   391  			return errors.New(string(s))
   392  		}
   393  		return nil
   394  	}
   395  
   396  	if _, ok := b.rules[table]; !ok {
   397  		return nil
   398  	}
   399  
   400  	delete(b.rules[table], chain)
   401  	return nil
   402  }
   403  
   404  // NewChain creates a new chain.
   405  func (b *BatchProvider) NewChain(table, chain string) error {
   406  	b.Lock()
   407  	defer b.Unlock()
   408  
   409  	if _, ok := b.batchTables[table]; !ok {
   410  		cmd := strings.Fields(b.cmd)
   411  		cmd = append(cmd, []string{"-t", table, "-N", chain}...)
   412  		execCmd := exec.Command("aporeto-iptables", cmd...)
   413  		s, err := execCmd.CombinedOutput()
   414  		if err != nil {
   415  			return errors.New(string(s))
   416  		}
   417  		return nil
   418  	}
   419  
   420  	if _, ok := b.rules[table]; !ok {
   421  		b.rules[table] = map[string][]string{}
   422  	}
   423  
   424  	b.rules[table][chain] = []string{}
   425  	return nil
   426  }
   427  
   428  // Commit commits the rules to the system
   429  func (b *BatchProvider) Commit() error {
   430  	b.Lock()
   431  	defer b.Unlock()
   432  
   433  	// We don't commit if we don't have any tables. This is old
   434  	// kernel compatibility mode.
   435  	if len(b.batchTables) == 0 {
   436  		return nil
   437  	}
   438  
   439  	buf, err := b.createDataBuffer()
   440  	if err != nil {
   441  		return fmt.Errorf("Failed to crete buffer %s", err)
   442  	}
   443  
   444  	return b.commitFunc(buf)
   445  }
   446  
   447  // RetrieveTable allows a caller to retrieve the final table. Mostly
   448  // needed for debuging and unit tests.
   449  func (b *BatchProvider) RetrieveTable() map[string]map[string][]string {
   450  	b.Lock()
   451  	defer b.Unlock()
   452  
   453  	return b.rules
   454  }
   455  
   456  func (b *BatchProvider) createDataBuffer() (*bytes.Buffer, error) {
   457  
   458  	buf := bytes.NewBuffer([]byte{})
   459  
   460  	for table := range b.rules {
   461  		if _, err := fmt.Fprintf(buf, "*%s\n", table); err != nil {
   462  			return nil, err
   463  		}
   464  		for chain := range b.rules[table] {
   465  			if _, err := fmt.Fprintf(buf, ":%s - [0:0]\n", chain); err != nil {
   466  				return nil, err
   467  			}
   468  		}
   469  		for chain := range b.rules[table] {
   470  			for _, rule := range b.rules[table][chain] {
   471  				if _, err := fmt.Fprintf(buf, "-A %s %s\n", chain, rule); err != nil {
   472  					return nil, err
   473  				}
   474  			}
   475  		}
   476  		customChainRules, _ := b.saveCustomChainRules()
   477  		fmt.Fprintf(buf, "%s\n", customChainRules.String())
   478  		if _, err := fmt.Fprintf(buf, "COMMIT\n"); err != nil {
   479  			return nil, err
   480  		}
   481  	}
   482  	return buf, nil
   483  }
   484  
   485  // restore will save the current DB to iptables.
   486  func (b *BatchProvider) restore(buf *bytes.Buffer) error {
   487  
   488  	cmd := exec.Command("aporeto-iptables", b.restoreCmd, "--wait")
   489  	cmd.Stdin = buf
   490  	out, err := cmd.CombinedOutput()
   491  	if err != nil {
   492  		again, _ := b.createDataBuffer()
   493  		zap.L().Error("Failed to execute command", zap.Error(err),
   494  			zap.ByteString("Output", out),
   495  			zap.String("Output", again.String()),
   496  		)
   497  		return fmt.Errorf("Failed to execute iptables-restore: %s", err)
   498  	}
   499  	return nil
   500  }
   501  
   502  func (b *BatchProvider) quoteRulesSpec(rulesspec []string) {
   503  
   504  	if !b.quote {
   505  		return
   506  	}
   507  
   508  	for i, rule := range rulesspec {
   509  		if len(rulesspec[i]) > 0 && rulesspec[i][0] == '"' {
   510  			continue
   511  		}
   512  
   513  		rulesspec[i] = fmt.Sprintf("\"%s\"", rule)
   514  	}
   515  }
   516  
   517  // ResetRules resets the rules to the original form.
   518  // It is implemented as "iptables-save | grep "-v" subs | iptables-restore"
   519  func (b *BatchProvider) ResetRules(subs string) error {
   520  
   521  	var out []byte
   522  	var err error
   523  
   524  	cmd := exec.Command("aporeto-iptables", b.saveCmd)
   525  	if out, err = cmd.CombinedOutput(); err != nil {
   526  		zap.L().Error("Failed to get iptables-save command", zap.Error(err),
   527  			zap.String("Output", string(out)))
   528  		return err
   529  	}
   530  
   531  	s := string(out)
   532  	rules := strings.Split(s, "\n")
   533  
   534  	var filterRules []string
   535  
   536  	for _, rule := range rules {
   537  		if !strings.Contains(rule, subs) {
   538  			filterRules = append(filterRules, rule)
   539  		}
   540  	}
   541  
   542  	combineRules := strings.Join(filterRules, "\n")
   543  	buf := bytes.NewBufferString(combineRules)
   544  
   545  	return b.commitFunc(buf)
   546  }
   547  
   548  func (b *BatchProvider) saveCustomChainRules() (*bytes.Buffer, error) {
   549  	var out []byte
   550  	var err error
   551  
   552  	cmd := exec.Command("aporeto-iptables", b.saveCmd)
   553  	if out, err = cmd.CombinedOutput(); err != nil {
   554  		zap.L().Error("Failed to get iptables-save command", zap.Error(err),
   555  			zap.String("Output", string(out)))
   556  		return nil, err
   557  	}
   558  
   559  	s := string(out)
   560  	rules := strings.Split(s, "\n")
   561  
   562  	var filterRules []string
   563  
   564  	for _, rule := range rules {
   565  		if strings.Contains(rule, b.customChain) {
   566  			filterRules = append(filterRules, rule)
   567  		}
   568  	}
   569  
   570  	combineRules := strings.Join(filterRules, "\n")
   571  	return bytes.NewBufferString(combineRules), nil
   572  
   573  }
   574  
   575  // ListRules lists the rules in the table/chain passed to it
   576  func (b *BatchProvider) ListRules(table, chain string) ([]string, error) {
   577  	var cmd *exec.Cmd
   578  
   579  	if chain != "" {
   580  		cmd = exec.Command("aporeto-iptables", "iptables", "--wait", "-t", table, "-L", chain)
   581  	} else {
   582  		cmd = exec.Command("aporeto-iptables", "iptables", "-wait", "-t", table, "-L")
   583  	}
   584  	out, err := cmd.CombinedOutput()
   585  	if err != nil {
   586  		zap.L().Error("Failed to get rules", zap.Error(err), zap.String("table", table), zap.String("chain", chain))
   587  		return []string{}, err
   588  	}
   589  	rules := strings.Split(string(out), "\n")
   590  	return rules, nil
   591  
   592  }