def validate_endpoint(config, endpoint): """ Ensures that the supplied endpoint is valid. Once this routine has returned successfully, we know that all required fields are present and have valid values. :param config: configuration structure :param endpoint: endpoint dictionary as read from etcd :raises ValidationFailed """ issues = [] if not isinstance(endpoint, dict): raise ValidationFailed("Expected endpoint to be a dict.") if "state" not in endpoint: issues.append("Missing 'state' field.") elif endpoint["state"] not in ("active", "inactive"): issues.append("Expected 'state' to be one of active/inactive.") for field in ["name", "mac", "profile_id"]: if field not in endpoint: issues.append("Missing '%s' field." % field) elif not isinstance(endpoint[field], StringTypes): issues.append("Expected '%s' to be a string; got %r." % (field, endpoint[field])) if "name" in endpoint: if not endpoint["name"].startswith(config.IFACE_PREFIX): issues.append("Interface %r does not start with %r." % (endpoint["name"], config.IFACE_PREFIX)) for version in (4, 6): nets = "ipv%d_nets" % version if nets not in endpoint: issues.append("Missing network %s." % nets) else: for ip in endpoint.get(nets, []): if not common.validate_cidr(ip, version): issues.append("IP address %r is not a valid IPv%d CIDR." % (ip, version)) break gw_key = "ipv%d_gateway" % version try: gw_str = endpoint[gw_key] if gw_str is not None and not common.validate_ip_addr(gw_str, version): issues.append("%s is not a valid IPv%d gateway address." % (gw_key, version)) except KeyError: pass if issues: raise ValidationFailed(" ".join(issues))
def test_validate_cidr(self): self.assertTrue(common.validate_cidr("1.2.3.4", 4)) self.assertFalse(common.validate_cidr("1.2.3.4.5", 4)) self.assertTrue(common.validate_cidr("1.2.3.4/32", 4)) self.assertTrue(common.validate_cidr("1.2.3", 4)) self.assertFalse(common.validate_cidr("bloop", 4)) self.assertFalse(common.validate_cidr("::", 4)) self.assertFalse(common.validate_cidr("2001::abc", 4)) self.assertFalse(common.validate_cidr("2001::a/64", 4)) self.assertFalse(common.validate_cidr("1.2.3.4", 6)) self.assertFalse(common.validate_cidr("1.2.3.4.5", 6)) self.assertFalse(common.validate_cidr("1.2.3.4/32", 6)) self.assertFalse(common.validate_cidr("1.2.3", 6)) self.assertFalse(common.validate_cidr("bloop", 6)) self.assertTrue(common.validate_cidr("::", 6)) self.assertTrue(common.validate_cidr("2001::abc", 6)) self.assertTrue(common.validate_cidr("2001::a/64", 6)) self.assertTrue(common.validate_cidr("1.2.3.4", None)) self.assertFalse(common.validate_cidr("1.2.3.4.5", None)) self.assertTrue(common.validate_cidr("1.2.3.4/32", None)) self.assertTrue(common.validate_cidr("1.2.3", None)) self.assertFalse(common.validate_cidr("bloop", None)) self.assertTrue(common.validate_cidr("::", None)) self.assertTrue(common.validate_cidr("2001::abc", None)) self.assertTrue(common.validate_cidr("2001::a/64", None)) self.assertFalse(common.validate_cidr(None, None))
def test_validate_cidr(self): self.assertTrue(common.validate_cidr("1.2.3.4", 4)) self.assertFalse(common.validate_cidr("1.2.3.4.5", 4)) self.assertTrue(common.validate_cidr("1.2.3.4/32", 4)) self.assertTrue(common.validate_cidr("1.2.3", 4)) self.assertFalse(common.validate_cidr("bloop", 4)) self.assertFalse(common.validate_cidr("::", 4)) self.assertFalse(common.validate_cidr("2001::abc", 4)) self.assertFalse(common.validate_cidr("2001::a/64", 4)) self.assertFalse(common.validate_cidr("1.2.3.4", 6)) self.assertFalse(common.validate_cidr("1.2.3.4.5", 6)) self.assertFalse(common.validate_cidr("1.2.3.4/32", 6)) self.assertFalse(common.validate_cidr("1.2.3", 6)) self.assertFalse(common.validate_cidr("bloop", 6)) self.assertTrue(common.validate_cidr("::", 6)) self.assertTrue(common.validate_cidr("2001::abc", 6)) self.assertTrue(common.validate_cidr("2001::a/64", 6)) self.assertTrue(common.validate_cidr("1.2.3.4", None)) self.assertFalse(common.validate_cidr("1.2.3.4.5", None)) self.assertTrue(common.validate_cidr("1.2.3.4/32", None)) self.assertTrue(common.validate_cidr("1.2.3", None)) self.assertFalse(common.validate_cidr("bloop", None)) self.assertTrue(common.validate_cidr("::", None)) self.assertTrue(common.validate_cidr("2001::abc", None)) self.assertTrue(common.validate_cidr("2001::a/64", None)) self.assertFalse(common.validate_cidr(None, None))
def update_ipsets(type, descr, suffix, rule_list, ipset_addr, ipset_port, ipset_icmp, tmp_ipset_addr, tmp_ipset_port, tmp_ipset_icmp): """ Update the ipsets with a given set of rules. If a rule is invalid we do not throw an exception or give up, but just log an error and continue. """ for rule in rule_list: if rule.get('cidr') is None: log.error("Invalid %s rule without cidr for %s : %s", descr, suffix, rule) continue if ((type == IPV4 and not common.validate_cidr(rule['cidr'], 4)) or (type == IPV6 and not common.validate_cidr(rule['cidr'], 6))): log.error("Invalid CIDR in %s rule cidr for %s : %s", descr, suffix, rule) continue #*********************************************************************# #* The ipset format is something like "10.11.1.3,udp:1-15" *# #* Further valid examples include *# #* 10.11.1.0/24 *# #* 10.11.1.0/24,tcp *# #* 10.11.1.0/24,80 *# #* *# #*********************************************************************# if rule['cidr'].endswith("/0"): #*****************************************************************# #* We have to handle any CIDR with a "/0" specially, since we *# #* split it into two ipsets entries; ipsets cannot have zero *# #* CIDR length in bits. *# #*****************************************************************# if type == IPV4: cidrs = ["0.0.0.0/1", "128.0.0.0/1"] else: cidrs = ["::/1", "8000::/1"] else: cidrs = [rule['cidr']] #*********************************************************************# #* Now handle the protocol. There are three types of protocol. tcp / *# #* sctp /udp / udplite have an optional port. icmp / ipv6-icmp have *# #* an optional type and code. Anything else doesn't have ports. *# #* *# #* We build the value to insert without the CIDR, then prepend the *# #* CIDR later (since we may need to use two CIDRs). *# #*********************************************************************# protocol = rule.get('protocol') port = rule.get('port') icmp_type = rule.get('icmp_type') icmp_code = rule.get('icmp_code') if protocol is None: if rule.get('port') is not None: # No protocol, so port is not allowed. log.error( "Invalid %s rule with port but no protocol for %s : %s", descr, suffix, rule) continue ipset_value = "" ipset = tmp_ipset_addr elif protocol in ("tcp", "sctp", "udp", "udplite"): if port is None: # No port implies port range 1 to 65535. ipset_value = ",%s:1-65535" % (protocol) ipset = tmp_ipset_port elif isinstance(port, list) and len(port) == 2: # List of two ports - port range if (not common.validate_port(str(port[0])) or not common.validate_port(str(port[1]))): # Port range was not two valid ports. log.error("Invalid port range in %s rule for %s : %s", descr, suffix, rule) continue ipset_value = ",%s:%s-%s" % (protocol, port[0], port[1]) ipset = tmp_ipset_port else: if not common.validate_port(str(port)): # Port was supplied but was not an integer or range. log.error("Invalid port in %s rule for %s : %s", descr, suffix, rule) continue # An integer port was specified. ipset_value = ",%s:%s" % (protocol, port) ipset = tmp_ipset_port elif protocol in ("icmp", "ipv6-icmp"): if rule.get('port') is not None: # No protocol, so port is not allowed. log.error( "Invalid %s rule for %s with port for protocol %s : %s", descr, suffix, protocol, rule) continue if (icmp_type is None and icmp_code is not None): # A code but no type - not allowed. log.error( "Invalid %s rule with ICMP code but no type for %s : %s", descr, suffix, rule) continue if icmp_type is None: # No type - all ICMP to / from the cidr, so use the ICMP ipset. ipset_value = "" ipset = tmp_ipset_icmp else: try: # Assume integer ICMP type first. int(icmp_type) if icmp_code is None: # Code defaults to 0 if not supplied. icmp_code = 0 ipset_value = ",%s:%s/%s" % (protocol, icmp_type, icmp_code) ipset = tmp_ipset_port except ValueError: # Not an integer ICMP type - must be a string code name. ipset_value = ",%s:%s" % (protocol, icmp_type) ipset = tmp_ipset_port else: if port is not None: # The supplied protocol does not allow ports. log.error( "Invalid %s rule with port but no protocol for %s : %s", descr, suffix, rule) continue # ipsets require a port number of 0 to be specified in a hash:net # set for any protocol other than tcp / udp / sctp / udplite. ipset_value = ",%s:0" % (protocol) ipset = tmp_ipset_port # Now add those values to the ipsets. for cidr in cidrs: try: ipsets.add(ipset, cidr + ipset_value) except FailedSystemCall: log.exception("Failed to add %s rule (%s) for %s", descr, cidr + ipset_value, suffix) # Now that we have filled the tmp ipset, swap it with the real one. ipsets.swap(tmp_ipset_addr, ipset_addr) ipsets.swap(tmp_ipset_port, ipset_port) ipsets.swap(tmp_ipset_icmp, ipset_icmp) # Get the temporary ipsets clean again - we leave them existing but empty. ipsets.flush(tmp_ipset_port) ipsets.flush(tmp_ipset_addr) ipsets.flush(tmp_ipset_icmp)
def validate_rules(rules): """ Ensures that the supplied rules are valid. Once this routine has returned successfully, we know that all required fields are present and have valid values. :param rules: rules list as read from etcd :raises ValidationFailed """ issues = [] if not isinstance(rules, dict): raise ValidationFailed("Expected rules to be a dict.") for dirn in ("inbound_rules", "outbound_rules"): if dirn not in rules: issues.append("No %s in rules." % dirn) continue if not isinstance(rules[dirn], list): issues.append("Expected rules[%s] to be a dict." % dirn) continue for rule in rules[dirn]: # Absolutely all fields are optional, but some have valid and # invalid values. protocol = rule.get('protocol') if (protocol is not None and not protocol in [ "tcp", "udp", "icmp", "icmpv6" ]): issues.append("Invalid protocol in rule %s." % rule) ip_version = rule.get('ip_version') if (ip_version is not None and not ip_version in [ 4, 6 ]): # Bad IP version prevents further validation issues.append("Invalid ip_version in rule %s." % rule) continue if ip_version == 4 and protocol == "icmpv6": issues.append("Using icmpv6 with IPv4 in rule %s." % rule) if ip_version == 6 and protocol == "icmp": issues.append("Using icmp with IPv6 in rule %s." % rule) # TODO: Validate that src_tag and dst_tag contain only valid characters. for key in ("src_net", "dst_net"): network = rule.get(key) if (network is not None and not common.validate_cidr(rule[key], ip_version)): issues.append("Invalid CIDR (version %s) in rule %s." % (ip_version, rule)) for key in ("src_ports", "dst_ports"): ports = rule.get(key) if (ports is not None and not isinstance(ports, list)): issues.append("Expected ports to be a list in rule %s." % rule) continue if ports is not None: for port in ports: error = validate_rule_port(port) if error: issues.append("Invalid port %s (%s) in rule %s." % (port, error, rule)) action = rule.get('action') if (action is not None and action not in ("allow", "deny")): issues.append("Invalid action in rule %s." % rule) icmp_type = rule.get('icmp_type') #TODO: firewall the icmp_type too if issues: raise ValidationFailed(" ".join(issues))