github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/network/iptables/iptables.go (about)

     1  // Copyright 2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package iptables
     5  
     6  import (
     7  	"bufio"
     8  	"fmt"
     9  	"io"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"github.com/juju/errors"
    14  	"github.com/juju/loggo"
    15  
    16  	"github.com/juju/juju/network"
    17  )
    18  
    19  var logger = loggo.GetLogger("juju.network.iptables")
    20  
    21  const (
    22  	// iptablesIngressCommand is the comment attached to iptables
    23  	// rules directly related to ingress rules.
    24  	iptablesIngressComment = "juju ingress"
    25  
    26  	// iptablesInternalCommand is the comment attached to iptables
    27  	// rules that are not directly related to ingress rules.
    28  	iptablesInternalComment = "juju internal"
    29  )
    30  
    31  // DropCommand represents an iptables DROP target command.
    32  type DropCommand struct {
    33  	DestinationAddress string
    34  	Interface          string
    35  }
    36  
    37  // Render renders the command to a string which can be executed via
    38  // bash in order to install the iptables rule.
    39  func (c DropCommand) Render() string {
    40  	args := []string{
    41  		"sudo iptables",
    42  		"-I INPUT",
    43  		"-m state --state NEW",
    44  		"-j DROP",
    45  		"-m comment --comment", fmt.Sprintf("'%s'", iptablesInternalComment),
    46  	}
    47  	if c.DestinationAddress != "" {
    48  		args = append(args, "-d", c.DestinationAddress)
    49  	}
    50  	if c.Interface != "" {
    51  		args = append(args, "-i", c.Interface)
    52  	}
    53  	return strings.Join(args, " ")
    54  }
    55  
    56  // AcceptInternalCommand represents an iptables ACCEPT target command,
    57  // for accepting traffic, optionally specifying a protocol, destination
    58  // address, and destination port.
    59  //
    60  // This is intended only for allowing traffic according to Juju's internal
    61  // rules, e.g. for API or SSH. This should not be used for managing the
    62  // ingress rules for exposing applications.
    63  type AcceptInternalCommand struct {
    64  	DestinationAddress string
    65  	DestinationPort    int
    66  	Protocol           string
    67  }
    68  
    69  // Render renders the command to a string which can be executed via
    70  // bash in order to install the iptables rule.
    71  func (c AcceptInternalCommand) Render() string {
    72  	args := []string{
    73  		"sudo iptables",
    74  		"-I INPUT",
    75  		"-j ACCEPT",
    76  		"-m comment --comment", fmt.Sprintf("'%s'", iptablesInternalComment),
    77  	}
    78  	if c.Protocol != "" {
    79  		args = append(args, "-p", c.Protocol)
    80  	}
    81  	if c.DestinationAddress != "" {
    82  		args = append(args, "-d", c.DestinationAddress)
    83  	}
    84  	if c.DestinationPort > 0 {
    85  		args = append(args, "--dport", fmt.Sprint(c.DestinationPort))
    86  	}
    87  	return strings.Join(args, " ")
    88  }
    89  
    90  // IngressRuleCommand represents an iptables ACCEPT target command
    91  // for ingress rules.
    92  type IngressRuleCommand struct {
    93  	Rule               network.IngressRule
    94  	DestinationAddress string
    95  	Delete             bool
    96  }
    97  
    98  // Render renders the command to a string which can be executed via
    99  // bash in order to install the iptables rule.
   100  func (c IngressRuleCommand) Render() string {
   101  	// TODO(axw) 2017-12-11 #1737472
   102  	// We shouldn't need to check for existing rules;
   103  	// the firewaller is supposed to check the instance's
   104  	// existing rules first, and only insert or remove as
   105  	// needed. Fixing the firewaller is much more difficult,
   106  	// and it really needs an overhaul.
   107  	checkCommand := c.render("-C")
   108  	if c.Delete {
   109  		deleteCommand := c.render("-D")
   110  		return fmt.Sprintf("(%s) && (%s)", checkCommand, deleteCommand)
   111  	}
   112  	insertCommand := c.render("-I")
   113  	return fmt.Sprintf("(%s) || (%s)", checkCommand, insertCommand)
   114  }
   115  
   116  func (c IngressRuleCommand) render(commandFlag string) string {
   117  	args := []string{
   118  		"sudo", "iptables",
   119  		commandFlag, "INPUT",
   120  		"-j ACCEPT",
   121  		"-p", c.Rule.Protocol,
   122  	}
   123  	if c.DestinationAddress != "" {
   124  		args = append(args, "-d", c.DestinationAddress)
   125  	}
   126  	if c.Rule.Protocol == "icmp" {
   127  		args = append(args, "--icmp-type 8")
   128  	} else {
   129  		if c.Rule.ToPort-c.Rule.FromPort > 0 {
   130  			args = append(args,
   131  				"-m multiport --dports",
   132  				fmt.Sprintf("%d:%d", c.Rule.FromPort, c.Rule.ToPort),
   133  			)
   134  		} else {
   135  			args = append(args, "--dport", fmt.Sprint(c.Rule.FromPort))
   136  		}
   137  	}
   138  	if len(c.Rule.SourceCIDRs) > 0 {
   139  		args = append(args, "-s", strings.Join(c.Rule.SourceCIDRs, ","))
   140  	}
   141  	// Comment always comes last.
   142  	args = append(args,
   143  		"-m comment --comment", fmt.Sprintf("'%s'", iptablesIngressComment),
   144  	)
   145  	return strings.Join(args, " ")
   146  }
   147  
   148  // ParseIngressRules parses the output of "iptables -L INPUT -n",
   149  // extracting previously added ingress rules, as rendered by
   150  // IngressRuleCommand.
   151  func ParseIngressRules(r io.Reader) ([]network.IngressRule, error) {
   152  	var rules []network.IngressRule
   153  	scanner := bufio.NewScanner(r)
   154  	for scanner.Scan() {
   155  		line := scanner.Text()
   156  		rule, ok, err := parseIngressRule(strings.TrimSpace(line))
   157  		if err != nil {
   158  			logger.Warningf("failed to parse iptables line %q: %v", line, err)
   159  			continue
   160  		}
   161  		if !ok {
   162  			continue
   163  		}
   164  		rules = append(rules, rule)
   165  	}
   166  	if err := scanner.Err(); err != nil {
   167  		return nil, errors.Annotate(err, "reading iptables output")
   168  	}
   169  	return rules, nil
   170  }
   171  
   172  // parseIngressRule parses a single iptables output line, extracting
   173  // an ingress rule if the line represents one, or returning false
   174  // otherwise.
   175  //
   176  // The iptables rules we care about have the following format, and we
   177  // will skip all other rules:
   178  //
   179  //    Chain INPUT (policy ACCEPT)
   180  //    target     prot opt source               destination
   181  //    ACCEPT     tcp  --  0.0.0.0/0            192.168.0.1  multiport dports 3456:3458 /* juju ingress */
   182  //    ACCEPT     tcp  --  0.0.0.0/0            192.168.0.2  tcp dpt:12345 /* juju ingress */
   183  //    ACCEPT     icmp --  0.0.0.0/0            10.0.0.1     icmptype 8 /* juju ingress */
   184  //
   185  func parseIngressRule(line string) (network.IngressRule, bool, error) {
   186  	fail := func(err error) (network.IngressRule, bool, error) {
   187  		return network.IngressRule{}, false, err
   188  	}
   189  	if !strings.HasPrefix(line, "ACCEPT") {
   190  		return network.IngressRule{}, false, nil
   191  	}
   192  
   193  	// We only care about rules with the comment "juju ingress".
   194  	if !strings.HasSuffix(line, "*/") {
   195  		return network.IngressRule{}, false, nil
   196  	}
   197  	commentStart := strings.LastIndex(line, "/*")
   198  	if commentStart == -1 {
   199  		return network.IngressRule{}, false, nil
   200  	}
   201  	line, comment := line[:commentStart], line[commentStart+2:]
   202  	comment = comment[:len(comment)-2]
   203  	if strings.TrimSpace(comment) != iptablesIngressComment {
   204  		return network.IngressRule{}, false, nil
   205  	}
   206  
   207  	const (
   208  		fieldTarget      = 0
   209  		fieldProtocol    = 1
   210  		fieldOptions     = 2
   211  		fieldSource      = 3
   212  		fieldDestination = 4
   213  	)
   214  	fields := make([]string, 5)
   215  	for i := range fields {
   216  		field, remainder, ok := popField(line)
   217  		if !ok {
   218  			return fail(errors.Errorf("could not extract field %d", i))
   219  		}
   220  		fields[i] = field
   221  		line = remainder
   222  	}
   223  
   224  	source := fields[fieldSource]
   225  	proto := strings.ToLower(fields[fieldProtocol])
   226  
   227  	var fromPort, toPort int
   228  	if strings.HasPrefix(line, "multiport dports") {
   229  		_, line, _ = popField(line) // pop "multiport"
   230  		_, line, _ = popField(line) // pop "dports"
   231  		portRange, _, ok := popField(line)
   232  		if !ok {
   233  			return fail(errors.New("could not extract port range"))
   234  		}
   235  		var err error
   236  		fromPort, toPort, err = parsePortRange(portRange)
   237  		if err != nil {
   238  			return fail(errors.Trace(err))
   239  		}
   240  	} else if proto == "icmp" {
   241  		fromPort, toPort = -1, -1
   242  	} else {
   243  		field, line, ok := popField(line)
   244  		if !ok {
   245  			return fail(errors.New("could not extract parameters"))
   246  		}
   247  		if field != proto {
   248  			// parameters should look like
   249  			// "tcp dpt:N" or "udp dpt:N".
   250  			return fail(errors.New("unexpected parameter prefix"))
   251  		}
   252  		field, line, ok = popField(line)
   253  		if !ok || !strings.HasPrefix(field, "dpt:") {
   254  			return fail(errors.New("could not extract destination port"))
   255  		}
   256  		port, err := parsePort(strings.TrimPrefix(field, "dpt:"))
   257  		if err != nil {
   258  			return fail(errors.Trace(err))
   259  		}
   260  		fromPort = port
   261  		toPort = port
   262  	}
   263  
   264  	rule, err := network.NewIngressRule(proto, fromPort, toPort, source)
   265  	if err != nil {
   266  		return fail(errors.Trace(err))
   267  	}
   268  	return rule, true, nil
   269  }
   270  
   271  // popField pops a pops a field off the front of the given string
   272  // by splitting on the first run of whitespace, and returns the
   273  // field and remainder. A boolean result is returned indicating
   274  // whether or not a field was found.
   275  func popField(s string) (field, remainder string, ok bool) {
   276  	i := strings.IndexRune(s, ' ')
   277  	if i == -1 {
   278  		return s, "", s != ""
   279  	}
   280  	field, remainder = s[:i], strings.TrimLeft(s[i+1:], " ")
   281  	return field, remainder, true
   282  }
   283  
   284  func parsePortRange(s string) (int, int, error) {
   285  	fields := strings.Split(s, ":")
   286  	if len(fields) != 2 {
   287  		return -1, -1, errors.New("expected M:N")
   288  	}
   289  	from, err := parsePort(fields[0])
   290  	if err != nil {
   291  		return -1, -1, errors.Trace(err)
   292  	}
   293  	to, err := parsePort(fields[1])
   294  	if err != nil {
   295  		return -1, -1, errors.Trace(err)
   296  	}
   297  	return from, to, nil
   298  }
   299  
   300  func parsePort(s string) (int, error) {
   301  	n, err := strconv.ParseUint(s, 10, 16)
   302  	if err != nil {
   303  		return -1, errors.Trace(err)
   304  	}
   305  	return int(n), nil
   306  }