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 <wido@widodh.nl>
This commit is contained in:
Wido den Hollander 2016-10-20 10:39:28 +02:00
parent 84e496b4f9
commit 115d6d5dc7
No known key found for this signature in database
GPG Key ID: 019B582DDB3ECA42
6 changed files with 125 additions and 73 deletions

View File

@ -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'

View File

@ -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");
}));
}));

View File

@ -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': {

View File

@ -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.");

View File

@ -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;

View File

@ -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