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 }