From 115d6d5dc774715b0d17238dc8e8d9f02017c690 Mon Sep 17 00:00:00 2001 From: Wido den Hollander Date: Thu, 20 Oct 2016 10:39:28 +0200 Subject: [PATCH] CLOUDSTACK-676: IPv6 In -and Egress filtering for Basic Networking This commit implements Ingress and Egress filtering for IPv6 in Basic Networking. It allows for opening and closing ports just as can be done with IPv4. Rules have to be specified twice, once for IPv4 and once for IPv6, for example: - 22 until 22: 0.0.0.0/0 - 22 until 22: ::/0 Egress filtering works the same as with IPv4. When no rule is applied all traffic is allowed. Otherwise only the specified traffic (with DNS being the exception) is allowed. Signed-off-by: Wido den Hollander --- scripts/vm/network/security_group.py | 165 ++++++++++-------- ui/lib/jquery.validate.additional-methods.js | 5 +- ui/scripts/network.js | 4 +- ui/scripts/sharedFunctions.js | 12 +- .../java/com/cloud/utils/net/NetUtils.java | 6 + .../com/cloud/utils/net/NetUtilsTest.java | 6 + 6 files changed, 125 insertions(+), 73 deletions(-) diff --git a/scripts/vm/network/security_group.py b/scripts/vm/network/security_group.py index ca80a557430..0e815650bd6 100755 --- a/scripts/vm/network/security_group.py +++ b/scripts/vm/network/security_group.py @@ -28,7 +28,7 @@ import re import libvirt import fcntl import time -from netaddr import IPAddress +from netaddr import IPAddress, IPNetwork from netaddr.core import AddrFormatError @@ -542,15 +542,15 @@ def default_network_rules(vm_name, vm_id, vm_ip, vm_ip6, vm_mac, vif, brname, se execute('ip6tables -A ' + vmchain_default + ' -m state --state RELATED,ESTABLISHED -j ACCEPT') # Allow Instances to receive Router Advertisements, send out solicitations, but block any outgoing Advertisement from a Instance - execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-out ' + vif + ' --src fe80::/64 --dst ff02::1 -p icmpv6 --icmpv6-type router-advertisement -j ACCEPT') - execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-in ' + vif + ' --dst ff02::2 -p icmpv6 --icmpv6-type router-solicitation -j RETURN') + execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-out ' + vif + ' --src fe80::/64 --dst ff02::1 -p icmpv6 --icmpv6-type router-advertisement -m hl --hl-eq 255 -j ACCEPT') + execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-in ' + vif + ' --dst ff02::2 -p icmpv6 --icmpv6-type router-solicitation -m hl --hl-eq 255 -j RETURN') execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-in ' + vif + ' -p icmpv6 --icmpv6-type router-advertisement -j DROP') # Allow neighbor solicitations and advertisements - execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-in ' + vif + ' -p icmpv6 --icmpv6-type neighbor-solicitation -j RETURN') - execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-out ' + vif + ' -p icmpv6 --icmpv6-type neighbor-solicitation -j ACCEPT') - execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-in ' + vif + ' -p icmpv6 --icmpv6-type neighbor-advertisement -m set --match-set ' + vm_ip6_set_name + ' src -j RETURN') - execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-out ' + vif + ' -p icmpv6 --icmpv6-type neighbor-advertisement -j ACCEPT') + execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-in ' + vif + ' -p icmpv6 --icmpv6-type neighbor-solicitation -m hl --hl-eq 255 -j RETURN') + execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-out ' + vif + ' -p icmpv6 --icmpv6-type neighbor-solicitation -m hl --hl-eq 255 -j ACCEPT') + execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-in ' + vif + ' -p icmpv6 --icmpv6-type neighbor-advertisement -m set --match-set ' + vm_ip6_set_name + ' src -m hl --hl-eq 255 -j RETURN') + execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-out ' + vif + ' -p icmpv6 --icmpv6-type neighbor-advertisement -m hl --hl-eq 255 -j ACCEPT') # Packets to allow as per RFC4890 execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-in ' + vif + ' -p icmpv6 --icmpv6-type packet-too-big -m set --match-set ' + vm_ip6_set_name + ' src -j RETURN') @@ -565,6 +565,9 @@ def default_network_rules(vm_name, vm_id, vm_ip, vm_ip6, vm_mac, vif, brname, se execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-in ' + vif + ' -p icmpv6 --icmpv6-type parameter-problem -m set --match-set ' + vm_ip6_set_name + ' src -j RETURN') execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-out ' + vif + ' -p icmpv6 --icmpv6-type parameter-problem -j ACCEPT') + # MLDv2 discovery packets + execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-in ' + vif + ' -p icmpv6 --dst ff02::16 -j RETURN') + # Allow Instances to send out DHCPv6 client messages, but block server messages execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-in ' + vif + ' -p udp --sport 546 --dst ff02::1:2 --src ' + str(ipv6_link_local) + ' -j RETURN') execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-out ' + vif + ' -p udp --src fe80::/64 --dport 546 --dst ' + str(ipv6_link_local) + ' -j ACCEPT') @@ -582,11 +585,6 @@ def default_network_rules(vm_name, vm_id, vm_ip, vm_ip6, vm_mac, vif, brname, se execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-out ' + vif + ' -j ' + vmchain) - # For now allow ICMPv6 echo-request, UDP and TCP traffic to the Unicast address of the Instance - execute('ip6tables -A ' + vmchain + ' -p icmpv6 --icmpv6-type echo-request -m set --match-set ' + vm_ip6_set_name + ' dst -j ACCEPT') - execute('ip6tables -A ' + vmchain + ' -p udp -m set --match-set ' + vm_ip6_set_name + ' dst -j ACCEPT') - execute('ip6tables -A ' + vmchain + ' -p tcp -m set --match-set ' + vm_ip6_set_name + ' dst -j ACCEPT') - # Drop all other traffic into the Instance execute('ip6tables -A ' + vmchain + ' -j DROP') except: @@ -904,6 +902,42 @@ def remove_rule_log_for_vm(vmName): def egress_chain_name(vm_name): return vm_name + "-eg" + +def parse_network_rules(rules): + ret = [] + + if rules is None or len(rules) == 0: + return ret + + lines = rules.split(';')[:-1] + for line in lines: + tokens = line.split(':', 4) + if len(tokens) != 5: + continue + + ruletype = tokens[0] + protocol = tokens[1] + start = int(tokens[2]) + end = int(tokens[3]) + cidrs = tokens.pop(); + + ipv4 = [] + ipv6 = [] + for ip in cidrs.split(","): + try: + network = IPNetwork(ip) + if network.version == 4: + ipv4.append(ip) + else: + ipv6.append(ip) + except: + pass + + ret.append({'ipv4': ipv4, 'ipv6': ipv6, 'ruletype': ruletype, + 'start': start, 'end': end, 'protocol': protocol}) + + return ret + def add_network_rules(vm_name, vm_id, vm_ip, vm_ip6, signature, seqno, vmMac, rules, vif, brname, sec_ips): try: vmName = vm_name @@ -919,83 +953,76 @@ def add_network_rules(vm_name, vm_id, vm_ip, vm_ip6, signature, seqno, vmMac, ru if changes[0] or changes[1] or changes[2] or changes[3]: default_network_rules(vmName, vm_id, vm_ip, vm_ip6, vmMac, vif, brname, sec_ips) - if rules == "" or rules == None: - lines = [] - else: - lines = rules.split(';')[:-1] - logging.debug(" programming network rules for IP: " + vm_ip + " vmname=" + vm_name) + + vmchain = vm_name + egress_chain_name(vm_name) try: - vmchain = vm_name - execute("iptables -F " + vmchain) - egress_vmchain = egress_chain_name(vm_name) - execute("iptables -F " + egress_vmchain) + for chain in [vmchain, egress_vmchain]: + execute('iptables -F ' + chain) + execute('ip6tables -F ' + chain) except: logging.debug("Error flushing iptables rules for " + vmchain + ". Presuming firewall rules deleted, re-initializing." ) default_network_rules(vm_name, vm_id, vm_ip, vm_ip6, vmMac, vif, brname, sec_ips) - egressrule = 0 - for line in lines: - tokens = line.split(':') - if len(tokens) != 5: - continue - ruletype = tokens[0] - protocol = tokens[1] - start = tokens[2] - end = tokens[3] - cidrs = tokens.pop(); - ips = cidrs.split(",") - ips.pop() - allow_any = False - if ruletype == 'E': + + egressrule_v4 = 0 + egressrule_v6 = 0 + + for rule in parse_network_rules(rules): + start = rule['start'] + end = rule['end'] + protocol = rule['protocol'] + + if rule['ruletype'] == 'E': vmchain = egress_chain_name(vm_name) direction = "-d" action = "RETURN" - egressrule = egressrule + 1 + if rule['ipv4']: + egressrule_v4 =+ 1 + + if rule['ipv6']: + egressrule_v6 +=1 + else: vmchain = vm_name action = "ACCEPT" direction = "-s" - if '0.0.0.0/0' in ips: - i = ips.index('0.0.0.0/0') - del ips[i] - allow_any = True - range = start + ":" + end - if ips: - if protocol == 'all': - for ip in ips: - execute("iptables -I " + vmchain + " -m state --state NEW " + direction + " " + ip + " -j "+action) - elif protocol != 'icmp': - for ip in ips: - execute("iptables -I " + vmchain + " -p " + protocol + " -m " + protocol + " --dport " + range + " -m state --state NEW " + direction + " " + ip + " -j "+ action) - else: - range = start + "/" + end - if start == "-1": - range = "any" - for ip in ips: - execute("iptables -I " + vmchain + " -p icmp --icmp-type " + range + " " + direction + " " + ip + " -j "+ action) - if allow_any: + range = str(start) + ':' + str(end) + if 'icmp' == protocol: + range = str(start) + '/' + str(end) + if start == -1: + range = 'any' + + for ip in rule['ipv4']: if protocol == 'all': - execute("iptables -I " + vmchain + " -m state --state NEW " + direction + " 0.0.0.0/0 -j "+action) + execute('iptables -I ' + vmchain + ' -m state --state NEW ' + direction + ' ' + ip + ' -j ' + action) elif protocol != 'icmp': - execute("iptables -I " + vmchain + " -p " + protocol + " -m " + protocol + " --dport " + range + " -m state --state NEW -j "+ action) + execute('iptables -I ' + vmchain + ' -p ' + protocol + ' -m ' + protocol + ' --dport ' + range + ' -m state --state NEW ' + direction + ' ' + ip + ' -j ' + action) else: - range = start + "/" + end - if start == "-1": - range = "any" - execute("iptables -I " + vmchain + " -p icmp --icmp-type " + range + " -j "+action) + execute('iptables -I ' + vmchain + ' -p icmp --icmp-type ' + range + ' ' + direction + ' ' + ip + ' -j ' + action) + + for ip in rule['ipv6']: + if protocol == 'all': + execute('ip6tables -I ' + vmchain + ' -m state --state NEW ' + direction + ' ' + ip + ' -j ' + action) + elif 'icmp' != protocol: + execute('ip6tables -I ' + vmchain + ' -p ' + protocol + ' -m ' + protocol + ' --dport ' + range + ' -m state --state NEW ' + direction + ' ' + ip + ' -j ' + action) + else: + execute('ip6tables -I ' + vmchain + ' -p icmpv6 --icmpv6-type ' + range + ' ' + direction + ' ' + ip + ' -j ' + action) egress_vmchain = egress_chain_name(vm_name) - if egressrule == 0 : - iptables = "iptables -A " + egress_vmchain + " -j RETURN" - execute(iptables) + if egressrule_v4 == 0 : + execute('iptables -A ' + egress_vmchain + ' -j RETURN') else: - iptables = "iptables -A " + egress_vmchain + " -j DROP" - execute(iptables) + execute('iptables -A ' + egress_vmchain + ' -j DROP') - vmchain = vm_name - iptables = "iptables -A " + vmchain + " -j DROP" - execute(iptables) + if egressrule_v6 == 0 : + execute('ip6tables -A ' + egress_vmchain + ' -j RETURN') + else: + execute('ip6tables -A ' + egress_vmchain + ' -j DROP') + + execute('iptables -A ' + vm_name + ' -j DROP') + execute('ip6tables -A ' + vm_name + ' -j DROP') if write_rule_log_for_vm(vmName, vm_id, vm_ip, domId, signature, seqno) == False: return 'false' diff --git a/ui/lib/jquery.validate.additional-methods.js b/ui/lib/jquery.validate.additional-methods.js index 7491439fb6b..811b4f76cd3 100644 --- a/ui/lib/jquery.validate.additional-methods.js +++ b/ui/lib/jquery.validate.additional-methods.js @@ -497,6 +497,9 @@ $.validator.addMethod("ipv4", function(value, element) { }, "Please enter a valid IP v4 address."); $.validator.addMethod("ipv6", function(value, element) { + if (value == '::') + return true; + return this.optional(element) || /^((([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))$/i.test(value); }, "Please enter a valid IP v6 address."); @@ -925,4 +928,4 @@ $.validator.addMethod("ziprange", function(value, element) { return this.optional(element) || /^90[2-5]\d\{2\}-\d{4}$/.test(value); }, "Your ZIP-code must be in the range 902xx-xxxx to 905xx-xxxx"); -})); \ No newline at end of file +})); diff --git a/ui/scripts/network.js b/ui/scripts/network.js index dd21cd23e4b..7299a7b853e 100755 --- a/ui/scripts/network.js +++ b/ui/scripts/network.js @@ -4869,7 +4869,7 @@ label: 'label.cidr', isHidden: true, validation: { - ipv4cidr: true + ipv46cidr: true } }, 'accountname': { @@ -5079,7 +5079,7 @@ label: 'label.cidr', isHidden: true, validation: { - ipv4cidr: true + ipv46cidr: true } }, 'accountname': { diff --git a/ui/scripts/sharedFunctions.js b/ui/scripts/sharedFunctions.js index 88d728e76ff..f845fd84c67 100644 --- a/ui/scripts/sharedFunctions.js +++ b/ui/scripts/sharedFunctions.js @@ -2318,7 +2318,7 @@ $.validator.addMethod("ipv6cidr", function(value, element) { if (parts[1] != Number(parts[1]).toString()) //making sure that "", " ", "00", "0 ","2 ", etc. will not pass return false; - if (Number(parts[1]) < 0 || Number(parts[1] > 32)) + if (Number(parts[1]) < 0 || Number(parts[1] > 128)) return false; return true; @@ -2344,3 +2344,13 @@ $.validator.addMethod("ipv4cidr", function(value, element) { return true; }, "The specified IPv4 CIDR is invalid."); + +$.validator.addMethod("ipv46cidr", function(value, element) { + if (this.optional(element) && value.length == 0) + return true; + + if ($.validator.methods.ipv4cidr.call(this, value, element) || $.validator.methods.ipv6cidr.call(this, value, element)) + return true; + + return false; +}, "The specified IPv4/IPv6 CIDR is invalid."); diff --git a/utils/src/main/java/com/cloud/utils/net/NetUtils.java b/utils/src/main/java/com/cloud/utils/net/NetUtils.java index a2ec4116345..a014bf7ca6c 100644 --- a/utils/src/main/java/com/cloud/utils/net/NetUtils.java +++ b/utils/src/main/java/com/cloud/utils/net/NetUtils.java @@ -557,6 +557,12 @@ public class NetUtils { if (cidr == null || cidr.isEmpty()) { return false; } + + try { + IPv6Network.fromString(cidr); + return true; + } catch (IllegalArgumentException e) {} + final String[] cidrPair = cidr.split("\\/"); if (cidrPair.length != 2) { return false; diff --git a/utils/src/test/java/com/cloud/utils/net/NetUtilsTest.java b/utils/src/test/java/com/cloud/utils/net/NetUtilsTest.java index 4c43751e4c4..6d939d550b4 100644 --- a/utils/src/test/java/com/cloud/utils/net/NetUtilsTest.java +++ b/utils/src/test/java/com/cloud/utils/net/NetUtilsTest.java @@ -245,6 +245,10 @@ public class NetUtilsTest { assertTrue(NetUtils.isValidCIDR(cidrFirst)); assertTrue(NetUtils.isValidCIDR(cidrSecond)); assertTrue(NetUtils.isValidCIDR(cidrThird)); + assertTrue(NetUtils.isValidCIDR("2001:db8::/64")); + assertTrue(NetUtils.isValidCIDR("2001:db8::/48")); + assertTrue(NetUtils.isValidCIDR("2001:db8:fff::/56")); + assertFalse(NetUtils.isValidCIDR("2001:db8:gggg::/56")); } @Test @@ -256,6 +260,8 @@ public class NetUtilsTest { assertTrue(NetUtils.isValidCidrList(cidrFirst)); assertTrue(NetUtils.isValidCidrList(cidrSecond)); assertTrue(NetUtils.isValidCidrList(cidrThird)); + assertTrue(NetUtils.isValidCidrList("2001:db8::/64,2001:db8:ffff::/48")); + assertTrue(NetUtils.isValidCidrList("2001:db8::/64,2001:db8:ffff::/48,192.168.0.0/24")); } @Test