scripts/vm/network/security_group.py (1,272 lines of code) (raw):

#!/usr/bin/python3 # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. import argparse from subprocess import check_output, CalledProcessError import logging import sys import os import xml.dom.minidom import re import libvirt import fcntl import time import ipaddress logpath = "/var/run/cloud/" # FIXME: Logs should reside in /var/log/cloud lock_file = "/var/lock/cloudstack_security_group.lock" driver = "qemu:///system" lock_handle = None def obtain_file_lock(path): global lock_handle try: lock_handle = open(path, 'w') fcntl.flock(lock_handle, fcntl.LOCK_EX | fcntl.LOCK_NB) return True except IOError: pass return False def execute(cmd): logging.debug(cmd) try: return check_output(cmd, shell=True).decode() except CalledProcessError as e: logging.exception('Command exited non-zero: %s', cmd) raise def can_bridge_firewall(privnic): try: execute("which iptables") except: print("no iptables on your host machine") sys.exit(1) try: execute("which ebtables") except: print("no ebtables on your host machine") sys.exit(2) if not os.path.exists(logpath): os.makedirs(logpath) cleanup_rules_for_dead_vms() cleanup_rules() return True def get_libvirt_connection(): conn = libvirt.openReadOnly(driver) if not conn: raise Exception('Failed to open connection to the hypervisor') return conn def virshlist(states): libvirt_states={ 'running': libvirt.VIR_DOMAIN_RUNNING, 'shutoff': libvirt.VIR_DOMAIN_SHUTOFF, 'shutdown': libvirt.VIR_DOMAIN_SHUTDOWN, 'paused': libvirt.VIR_DOMAIN_PAUSED, 'nostate': libvirt.VIR_DOMAIN_NOSTATE, 'blocked': libvirt.VIR_DOMAIN_BLOCKED, 'crashed': libvirt.VIR_DOMAIN_CRASHED, } searchstates = list(libvirt_states[state] for state in states) conn = get_libvirt_connection() alldomains = [domain for domain in map(conn.lookupByID, conn.listDomainsID())] alldomains += [domain for domain in map(conn.lookupByName, conn.listDefinedDomains())] domains = [] for domain in alldomains: if domain.info()[0] in searchstates: domains.append(domain.name()) conn.close() return domains def virshdumpxml(domain): try: conn = get_libvirt_connection() dom = conn.lookupByName(domain) conn.close() return dom.XMLDesc(0) except libvirt.libvirtError: return None def ipv6_link_local_addr(mac=None): eui64 = re.sub(r'[.:-]', '', mac).lower() eui64 = eui64[0:6] + 'fffe' + eui64[6:] eui64 = hex(int(eui64[0:2], 16) ^ 2)[2:].zfill(2) + eui64[2:] return ipaddress.ip_address('fe80::' + ':'.join(re.findall(r'.{4}', eui64))) def split_ips_by_family(ips): if type(ips) is str: ips = [ip for ip in ips.split(';') if ip != ''] ip4s = [] ip6s = [] for ip in ips: network = ipaddress.ip_network(ip) if network.version == 4: ip4s.append(ip) elif network.version == 6: ip6s.append(ip) return ip4s, ip6s def destroy_network_rules_for_nic(vm_name, vm_ip, vm_mac, vif, sec_ips): try: rules = execute("""iptables-save -t filter | awk '/ %s / { sub(/-A/, "-D", $1) ; print }'""" % vif ).split("\n") for rule in [_f for _f in rules if _f]: try: execute("iptables " + rule) except: logging.debug("Ignoring failure to delete rule: " + rule) except: pass try: dnats = execute("""iptables-save -t nat | awk '/ %s / { sub(/-A/, "-D", $1) ; print }'""" % vif ).split("\n") for dnat in [_f for _f in dnats if _f]: try: execute("iptables -t nat " + dnat) except: logging.debug("Ignoring failure to delete dnat: " + dnat) except: pass ips = sec_ips.split(';') ips.pop() ips.append(vm_ip) add_to_ipset(vm_name, ips, "-D") ebtables_rules_vmip(vm_name, vm_mac, ips, "-D") vmchain_in = vm_name + "-in" vmchain_out = vm_name + "-out" vmchain_in_src = vm_name + "-in-src" vmchain_out_dst = vm_name + "-out-dst" try: execute("ebtables -t nat -D " + vmchain_in_src + " -s " + vm_mac + " -j RETURN") execute("ebtables -t nat -D " + vmchain_out_dst + " -p ARP --arp-op Reply --arp-mac-dst " + vm_mac + " -j RETURN") execute("ebtables -t nat -D PREROUTING -i " + vif + " -j " + vmchain_in) execute("ebtables -t nat -D POSTROUTING -o " + vif + " -j " + vmchain_out) except: logging.debug("Ignoring failure to delete ebtable rules for vm: " + vm_name) def get_bridge_physdev(brname): # eth1.50@eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master breth1-50 state forwarding priority 32 cost 4 | # eth1.50@eth1: | eth1.50@eth1 | eth1.50 physdev = execute("bridge -o link show | awk '/master %s / && !/^[0-9]+: vnet/ {print $2}' | head -1 | cut -d ':' -f1 | cut -d '@' -f1" % brname) return physdev.strip() def destroy_network_rules_for_vm(vm_name, vif=None): vmchain = iptables_chain_name(vm_name) vmchain_egress = egress_chain_name(vm_name) vmchain_default = None vm_ipsetname=ipset_chain_name(vm_name) delete_rules_for_vm_in_bridge_firewall_chain(vm_name) if 1 in [vm_name.startswith(c) for c in ['r-', 's-', 'v-']]: return True if vm_name.startswith('i-'): vmchain_default = '-'.join(vm_name.split('-')[:-1]) + "-def" destroy_ebtables_rules(vm_name, vif) chains = [vmchain_default, vmchain, vmchain_egress] for chain in [_f for _f in chains if _f]: try: execute("iptables -F " + chain) execute('ip6tables -F ' + chain) except: logging.debug("Ignoring failure to flush chain: " + chain) for chain in [_f for _f in chains if _f]: try: execute("iptables -X " + chain) execute('ip6tables -X ' + chain) except: logging.debug("Ignoring failure to delete chain: " + chain) try: for ipset in [vm_ipsetname, vm_ipsetname + '-6']: execute('ipset -F ' + ipset) execute('ipset -X ' + ipset) except: logging.debug("Ignoring failure to delete ipset " + vmchain) if vif: try: dnats = execute("""iptables -t nat -S | awk '/%s/ { sub(/-A/, "-D", $1) ; print }'""" % vif ).split("\n") for dnat in [_f for _f in dnats if _f]: try: execute("iptables -t nat " + dnat) except: logging.debug("Ignoring failure to delete dnat: " + dnat) except: pass remove_rule_log_for_vm(vm_name) remove_secip_log_for_vm(vm_name) return True def destroy_ebtables_rules(vm_name, vif): eb_vm_chain = ebtables_chain_name(vm_name) delcmd = "ebtables -t nat -L PREROUTING | grep " + eb_vm_chain delcmds = [] try: delcmds = [_f for _f in execute(delcmd).split('\n') if _f] delcmds = ["-D PREROUTING " + x for x in delcmds ] except: pass postcmds = [] try: postcmd = "ebtables -t nat -L POSTROUTING | grep " + eb_vm_chain postcmds = [_f for _f in execute(postcmd).split('\n') if _f] postcmds = ["-D POSTROUTING " + x for x in postcmds] except: pass delcmds += postcmds for cmd in delcmds: try: execute("ebtables -t nat " + cmd) except: logging.debug("Ignoring failure to delete ebtables rules for vm " + vm_name) chains = [eb_vm_chain+"-in", eb_vm_chain+"-out", eb_vm_chain+"-in-ips", eb_vm_chain+"-out-ips", eb_vm_chain+"-in-src", eb_vm_chain+"-out-dst"] for chain in chains: try: execute("ebtables -t nat -F " + chain) execute("ebtables -t nat -X " + chain) except: logging.debug("Ignoring failure to delete ebtables chain for vm " + vm_name) def default_ebtables_rules(vm_name, vm_ip, vm_mac, vif, is_first_nic=False): eb_vm_chain=ebtables_chain_name(vm_name) vmchain_in = eb_vm_chain + "-in" vmchain_out = eb_vm_chain + "-out" vmchain_in_ips = eb_vm_chain + "-in-ips" vmchain_out_ips = eb_vm_chain + "-out-ips" vmchain_in_src = eb_vm_chain + "-in-src" vmchain_out_dst = eb_vm_chain + "-out-dst" if not is_first_nic: try: execute("ebtables -t nat -A PREROUTING -i " + vif + " -j " + vmchain_in) execute("ebtables -t nat -A POSTROUTING -o " + vif + " -j " + vmchain_out) execute("ebtables -t nat -I " + vmchain_in_src + " -s " + vm_mac + " -j RETURN") if vm_ip: execute("ebtables -t nat -I " + vmchain_in_ips + " -p ARP -s " + vm_mac + " --arp-mac-src " + vm_mac + " --arp-ip-src " + vm_ip + " -j RETURN") execute("ebtables -t nat -I " + vmchain_out_dst + " -p ARP --arp-op Reply --arp-mac-dst " + vm_mac + " -j RETURN") if vm_ip: execute("ebtables -t nat -I " + vmchain_out_ips + " -p ARP --arp-ip-dst " + vm_ip + " -j RETURN") except: logging.debug("Failed to program rules for additional nic " + vif) return False return destroy_ebtables_rules(vm_name, vif) for chain in [vmchain_in, vmchain_out, vmchain_in_ips, vmchain_out_ips, vmchain_in_src, vmchain_out_dst]: try: execute("ebtables -t nat -N " + chain) except: execute("ebtables -t nat -F " + chain) try: # -s ! 52:54:0:56:44:32 -j DROP execute("ebtables -t nat -A PREROUTING -i " + vif + " -j " + vmchain_in) execute("ebtables -t nat -A POSTROUTING -o " + vif + " -j " + vmchain_out) execute("ebtables -t nat -A " + vmchain_in_ips + " -j DROP") execute("ebtables -t nat -A " + vmchain_out_ips + " -j DROP") execute("ebtables -t nat -A " + vmchain_in_src + " -j DROP") execute("ebtables -t nat -A " + vmchain_out_dst + " -p ARP --arp-op Reply -j DROP") except: logging.debug("Failed to program default rules") return False try: execute("ebtables -t nat -A " + vmchain_in + " -j " + vmchain_in_src) execute("ebtables -t nat -I " + vmchain_in_src + " -s " + vm_mac + " -j RETURN") execute("ebtables -t nat -A " + vmchain_in + " -p ARP -j " + vmchain_in_ips) if vm_ip: execute("ebtables -t nat -I " + vmchain_in_ips + " -p ARP -s " + vm_mac + " --arp-mac-src " + vm_mac + " --arp-ip-src " + vm_ip + " -j RETURN") execute("ebtables -t nat -A " + vmchain_in + " -p ARP --arp-op Request -j ACCEPT") execute("ebtables -t nat -A " + vmchain_in + " -p ARP --arp-op Reply -j ACCEPT") execute("ebtables -t nat -A " + vmchain_in + " -p ARP -j DROP") except: logging.exception("Failed to program default ebtables IN rules") return False try: execute("ebtables -t nat -A " + vmchain_out + " -p ARP --arp-op Reply -j " + vmchain_out_dst) execute("ebtables -t nat -I " + vmchain_out_dst + " -p ARP --arp-op Reply --arp-mac-dst " + vm_mac + " -j RETURN") execute("ebtables -t nat -A " + vmchain_out + " -p ARP -j " + vmchain_out_ips ) if vm_ip: execute("ebtables -t nat -I " + vmchain_out_ips + " -p ARP --arp-ip-dst " + vm_ip + " -j RETURN") execute("ebtables -t nat -A " + vmchain_out + " -p ARP --arp-op Request -j ACCEPT") execute("ebtables -t nat -A " + vmchain_out + " -p ARP --arp-op Reply -j ACCEPT") execute("ebtables -t nat -A " + vmchain_out + " -p ARP -j DROP") except: logging.debug("Failed to program default ebtables OUT rules") return False def refactor_ebtable_rules(vm_name): vmchain_in = vm_name + "-in" vmchain_in_ips = vm_name + "-in-ips" vmchain_in_src = vm_name + "-in-src" try: execute("ebtables -t nat -L " + vmchain_in_src) logging.debug("Chain '" + vmchain_in_src + "' exists, ebtables rules have newer version, skip refactoring") return True except: logging.debug("Chain '" + vmchain_in_src + "' does not exist, ebtables rules have old version, start refactoring") vif = execute("ebtables -t nat -L PREROUTING | grep " + vmchain_in + " | awk '{print $2}'").strip() vm_mac = execute("ebtables -t nat -L " + vmchain_in + " | grep arp-mac-src | awk '{print $5}'").strip() vm_ips = execute("ebtables -t nat -L " + vmchain_in_ips + " | grep arp-ip-src | awk '{print $4}'").split('\n') destroy_ebtables_rules(vm_name, vif) default_ebtables_rules(vm_name, None, vm_mac, vif, True) ebtables_rules_vmip(vm_name, vm_mac, vm_ips, "-A") logging.debug("Refactoring ebtables rules for vm " + vm_name + " is done") return True def default_network_rules_systemvm(vm_name, localbrname): bridges = get_bridges(vm_name) domid = get_vm_id(vm_name) vmchain = iptables_chain_name(vm_name) delete_rules_for_vm_in_bridge_firewall_chain(vm_name) try: execute("iptables -N " + vmchain) except: execute("iptables -F " + vmchain) for bridge in bridges: if bridge != localbrname: if not add_fw_framework(bridge): return False brfw = get_br_fw(bridge) vifs = get_vifs_for_bridge(vm_name, bridge) for vif in vifs: try: execute("iptables -A " + brfw + "-OUT" + " -m physdev --physdev-is-bridged --physdev-out " + vif + " -j " + vmchain) execute("iptables -A " + brfw + "-IN" + " -m physdev --physdev-is-bridged --physdev-in " + vif + " -j " + vmchain) execute("iptables -A " + vmchain + " -m physdev --physdev-is-bridged --physdev-in " + vif + " -j RETURN") except: logging.debug("Failed to program default rules") return False execute("iptables -A " + vmchain + " -j ACCEPT") if not write_rule_log_for_vm(vm_name, '-1', '_ignore_', domid, '_initial_', '-1'): logging.debug("Failed to log default network rules for systemvm, ignoring") return True def remove_secip_log_for_vm(vmName): vm_name = vmName logfilename = logpath + vm_name + ".ip" result = True try: os.remove(logfilename) except: logging.debug("Failed to delete rule log file " + logfilename) result = False return result def write_secip_log_for_vm (vmName, secIps, vmId): vm_name = vmName logfilename = logpath + vm_name + ".ip" logging.debug("Writing log to " + logfilename) logf = open(logfilename, 'w') output = ','.join([vmName, secIps, vmId]) result = True try: logf.write(output) logf.write('\n') except: logging.debug("Failed to write to rule log file " + logfilename) result = False logf.close() return result def create_ipset_forvm(ipsetname, type='iphash', family='inet'): result = True try: logging.debug("Creating ipset chain .... " + ipsetname) execute("ipset -F " + ipsetname) execute("ipset -X " + ipsetname) except: logging.debug("ipset chain not exists creating.... " + ipsetname) finally: execute('ipset -N ' + ipsetname + ' ' + type + ' family ' + family) return result def add_to_ipset(ipsetname, ips, action): result = True for ip in ips: try: logging.debug("vm ip " + str(ip)) execute("ipset " + action + " " + ipsetname + " " + str(ip)) except: logging.debug("vm ip already in ip set " + str(ip)) continue return result def network_rules_vmSecondaryIp(vm_name, vm_mac, ip_secondary, action): logging.debug("vmName = "+ vm_name) logging.debug("vmMac = " + vm_mac) logging.debug("action = "+ action) vmchain = vm_name vmchain6 = vmchain + '-6' ip4s, ip6s = split_ips_by_family(ip_secondary) add_to_ipset(vmchain, ip4s, action) #add ipv6 addresses to ipv6 ipset add_to_ipset(vmchain6, ip6s, action) #add ebtables rules for the secondary ip refactor_ebtable_rules(vm_name) ebtables_rules_vmip(vm_name, vm_mac, [ip_secondary], action) return True def ebtables_rules_vmip (vmname, vmmac, ips, action): eb_vm_chain=ebtables_chain_name(vmname) vmchain_inips = eb_vm_chain + "-in-ips" vmchain_outips = eb_vm_chain + "-out-ips" if action and action.strip() == "-A": action = "-I" for ip in [_f for _f in ips if _f]: logging.debug("ip = " + ip) if ip == 0 or ip == "0": continue try: execute("ebtables -t nat " + action + " " + vmchain_inips + " -p ARP -s " + vmmac + " --arp-mac-src " + vmmac + " --arp-ip-src " + ip + " -j RETURN") execute("ebtables -t nat " + action + " " + vmchain_outips + " -p ARP --arp-ip-dst " + ip + " -j RETURN") except: logging.debug("Failed to program ebtables rules for secondary ip %s for vm %s with action %s" % (ip, vmname, action)) def check_default_network_rules(vm_name, vm_id, vm_ip, vm_ip6, vm_mac, vif, brname, sec_ips, is_first_nic=False): brfw = get_br_fw(brname) vmchain_default = '-'.join(vm_name.split('-')[:-1]) + "-def" try: rules = execute("iptables-save |grep -w %s |grep -w %s |grep -w %s" % (brfw, vif, vmchain_default)) except: rules = None if not rules: logging.debug("iptables rules do not exist, programming default rules for %s %s" % (vm_name,vif)) default_network_rules(vm_name, vm_id, vm_ip, vm_ip6, vm_mac, vif, brname, sec_ips, is_first_nic) else: vmchain_in = vm_name + "-in" try: rules = execute("ebtables -t nat -L PREROUTING | grep %s |grep -w %s" % (vmchain_in, vif)) except: rules = None if not rules: logging.debug("ebtables rules do not exist, programming default ebtables rules for %s %s" % (vm_name,vif)) default_ebtables_rules(vm_name, vm_ip, vm_mac, vif, is_first_nic) ips = sec_ips.split(';') ips.pop() ebtables_rules_vmip(vm_name, vm_mac, ips, "-I") return True def default_network_rules(vm_name, vm_id, vm_ip, vm_ip6, vm_mac, vif, brname, sec_ips, is_first_nic=False): if not add_fw_framework(brname): return False vmName = vm_name brfw = get_br_fw(brname) domID = get_vm_id(vm_name) vmchain = iptables_chain_name(vm_name) vmchain_egress = egress_chain_name(vm_name) vmchain_default = '-'.join(vmchain.split('-')[:-1]) + "-def" ipv6_link_local = ipv6_link_local_addr(vm_mac) action = "-A" vmipsetName = ipset_chain_name(vm_name) vmipsetName6 = vmipsetName + '-6' if is_first_nic: delete_rules_for_vm_in_bridge_firewall_chain(vmName) destroy_ebtables_rules(vmName, vif) for chain in [vmchain, vmchain_egress, vmchain_default]: try: execute('iptables -N ' + chain) except: execute('iptables -F ' + chain) try: execute('ip6tables -N ' + chain) except: execute('ip6tables -F ' + chain) #create ipset and add vm ips to that ip set if not create_ipset_forvm(vmipsetName): logging.debug("failed to create ipset for rule %s", vmipsetName) return False if not create_ipset_forvm(vmipsetName6, family='inet6', type='hash:net'): logging.debug("failed to create ivp6 ipset for rule %s", vmipsetName6) return False #add primary nic ip to ipset if not add_to_ipset(vmipsetName, [vm_ip], action ): logging.debug("failed to add vm " + vm_ip + " ip to set ") return False #add secodnary nic ips to ipset secIpSet = "1" ips = sec_ips.split(';') ips.pop() if len(ips) == 0 or ips[0] == "0": secIpSet = "0" ip4s = [] ip6s = [] if secIpSet == "1": logging.debug("Adding ipset for secondary ipv4 addresses") ip4s, ip6s = split_ips_by_family(ips) add_to_ipset(vmipsetName, ip4s, action) if not write_secip_log_for_vm(vm_name, sec_ips, vm_id): logging.debug("Failed to log default network rules, ignoring") try: execute("iptables -A " + brfw + "-OUT" + " -m physdev --physdev-is-bridged --physdev-out " + vif + " -j " + vmchain_default) execute("iptables -A " + brfw + "-IN" + " -m physdev --physdev-is-bridged --physdev-in " + vif + " -j " + vmchain_default) execute("iptables -A " + vmchain_default + " -m state --state RELATED,ESTABLISHED -j ACCEPT") #allow dhcp execute("iptables -A " + vmchain_default + " -m physdev --physdev-is-bridged --physdev-in " + vif + " -p udp --dport 67 --sport 68 -j ACCEPT") execute("iptables -A " + vmchain_default + " -m physdev --physdev-is-bridged --physdev-out " + vif + " -p udp --dport 68 --sport 67 -j ACCEPT") execute("iptables -A " + vmchain_default + " -m physdev --physdev-is-bridged --physdev-in " + vif + " -p udp --sport 67 -j DROP") #don't let vm spoof its ip address if vm_ip: execute("iptables -A " + vmchain_default + " -m physdev --physdev-is-bridged --physdev-in " + vif + " -m set ! --match-set " + vmipsetName + " src -j DROP") execute("iptables -A " + vmchain_default + " -m physdev --physdev-is-bridged --physdev-out " + vif + " -m set ! --match-set " + vmipsetName + " dst -j DROP") execute("iptables -A " + vmchain_default + " -m physdev --physdev-is-bridged --physdev-in " + vif + " -m set --match-set " + vmipsetName + " src -p udp --dport 53 -j RETURN ") execute("iptables -A " + vmchain_default + " -m physdev --physdev-is-bridged --physdev-in " + vif + " -m set --match-set " + vmipsetName + " src -p tcp --dport 53 -j RETURN ") execute("iptables -A " + vmchain_default + " -m physdev --physdev-is-bridged --physdev-in " + vif + " -m set --match-set " + vmipsetName + " src -j " + vmchain_egress) execute("iptables -A " + vmchain_default + " -m physdev --physdev-is-bridged --physdev-out " + vif + " -j " + vmchain) execute("iptables -A " + vmchain + " -j DROP") except: logging.debug("Failed to program default rules for vm " + vm_name) return False default_ebtables_rules(vmchain, vm_ip, vm_mac, vif, is_first_nic) #default ebtables rules for vm secondary ips ebtables_rules_vmip(vm_name, vm_mac, ip4s, "-I") if vm_ip and is_first_nic: if not write_rule_log_for_vm(vmName, vm_id, vm_ip, domID, '_initial_', '-1'): logging.debug("Failed to log default network rules, ignoring") vm_ip6_addr = [ipv6_link_local] try: ip6 = ipaddress.ip_address(vm_ip6) if ip6.version == 6: vm_ip6_addr.append(ip6) except (ipaddress.AddressValueError, ValueError): pass add_to_ipset(vmipsetName6, vm_ip6_addr, action) if secIpSet == "1": logging.debug("Adding ipset for secondary ipv6 addresses") add_to_ipset(vmipsetName6, ip6s, action) try: execute('ip6tables -A ' + brfw + '-OUT' + ' -m physdev --physdev-is-bridged --physdev-out ' + vif + ' -j ' + vmchain_default) execute('ip6tables -A ' + brfw + '-IN' + ' -m physdev --physdev-is-bridged --physdev-in ' + vif + ' -j ' + vmchain_default) 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 -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 -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 ' + vmipsetName6 + ' 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 ' + vmipsetName6 + ' src -j RETURN') execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-out ' + vif + ' -p icmpv6 --icmpv6-type packet-too-big -j ACCEPT') execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-in ' + vif + ' -p icmpv6 --icmpv6-type destination-unreachable -m set --match-set ' + vmipsetName6 + ' src -j RETURN') execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-out ' + vif + ' -p icmpv6 --icmpv6-type destination-unreachable -j ACCEPT') execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-in ' + vif + ' -p icmpv6 --icmpv6-type time-exceeded -m set --match-set ' + vmipsetName6 + ' src -j RETURN') execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-out ' + vif + ' -p icmpv6 --icmpv6-type time-exceeded -j ACCEPT') execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-in ' + vif + ' -p icmpv6 --icmpv6-type parameter-problem -m set --match-set ' + vmipsetName6 + ' 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') execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-in ' + vif + ' -p udp --sport 547 ! --dst fe80::/64 -j DROP') # Always allow outbound DNS over UDP and TCP execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-in ' + vif + ' -p udp --dport 53 -m set --match-set ' + vmipsetName6 + ' src -j RETURN') execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-in ' + vif + ' -p tcp --dport 53 -m set --match-set ' + vmipsetName6 + ' src -j RETURN') # Prevent source address spoofing execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-in ' + vif + ' -m set ! --match-set ' + vmipsetName6 + ' src -j DROP') # Send proper traffic to the egress chain of the Instance execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-in ' + vif + ' -m set --match-set ' + vmipsetName6 + ' src -j ' + vmchain_egress) execute('ip6tables -A ' + vmchain_default + ' -m physdev --physdev-is-bridged --physdev-out ' + vif + ' -j ' + vmchain) # Drop all other traffic into the Instance execute('ip6tables -A ' + vmchain + ' -j DROP') except: logging.debug('Failed to program default rules for vm ' + vm_name) return False logging.debug("Programmed default rules for vm " + vm_name) return True def post_default_network_rules(vm_name, vm_id, vm_ip, vm_mac, vif, brname, dhcpSvr, hostIp, hostMacAddr): vmchain_default = '-'.join(vm_name.split('-')[:-1]) + "-def" iptables_vmchain=iptables_chain_name(vm_name) vmchain_in = iptables_vmchain + "-in" vmchain_out = iptables_vmchain + "-out" domID = get_vm_id(vm_name) try: execute("iptables -I " + vmchain_default + " 4 -m physdev --physdev-is-bridged --physdev-in " + vif + " --source " + vm_ip + " -j ACCEPT") except: pass try: execute("iptables -t nat -A PREROUTING -p tcp -m physdev --physdev-in " + vif + " -m tcp --dport 80 -d " + dhcpSvr + " -j DNAT --to-destination " + hostIp + ":80") except: pass try: execute("ebtables -t nat -I " + vmchain_in + " -p IPv4 --ip-protocol tcp --ip-destination-port 80 --ip-dst " + dhcpSvr + " -j dnat --to-destination " + hostMacAddr) except: pass try: execute("ebtables -t nat -I " + vmchain_in + " 4 -p ARP --arp-ip-src ! " + vm_ip + " -j DROP") except: pass try: execute("ebtables -t nat -I " + vmchain_out + " 2 -p ARP --arp-ip-dst ! " + vm_ip + " -j DROP") except: pass if not write_rule_log_for_vm(vm_name, vm_id, vm_ip, domID, '_initial_', '-1'): logging.debug("Failed to log default network rules, ignoring") def delete_rules_for_vm_in_bridge_firewall_chain(vmName): vm_name = vmName if vm_name.startswith('i-'): vm_name=iptables_chain_name(vm_name) vm_name = '-'.join(vm_name.split('-')[:-1]) + "-def" vmchain = iptables_chain_name(vm_name) delcmd = """iptables-save | awk '/BF(.*)physdev-is-bridged(.*)%s/ { sub(/-A/, "-D", $1) ; print }'""" % vmchain delcmds = [_f for _f in execute(delcmd).split('\n') if _f] for cmd in delcmds: try: execute("iptables " + cmd) except: logging.exception("Ignoring failure to delete rules for vm " + vmName) delcmd = """ip6tables-save | awk '/BF(.*)physdev-is-bridged(.*)%s/ { sub(/-A/, "-D", $1) ; print }'""" % vmchain delcmds = [_f for _f in execute(delcmd).split('\n') if _f] for cmd in delcmds: try: execute('ip6tables ' + cmd) except: logging.exception("Ignoring failure to delete rules for vm " + vmName) def rewrite_rule_log_for_vm(vm_name, new_domid): logfilename = logpath + vm_name + ".log" if not os.path.exists(logfilename): return lines = (line.rstrip() for line in open(logfilename)) [_vmName,_vmID,_vmIP,_domID,_signature,_seqno] = ['_', '-1', '_', '-1', '_', '-1'] for line in lines: [_vmName,_vmID,_vmIP,_domID,_signature,_seqno] = line.split(',') break write_rule_log_for_vm(_vmName, _vmID, '0.0.0.0', new_domid, _signature, '-1') def get_rule_log_for_vm(vmName): vm_name = vmName logfilename = logpath + vm_name + ".log" if not os.path.exists(logfilename): return '' lines = (line.rstrip() for line in open(logfilename)) [_vmName,_vmID,_vmIP,_domID,_signature,_seqno] = ['_', '-1', '_', '-1', '_', '-1'] for line in lines: [_vmName,_vmID,_vmIP,_domID,_signature,_seqno] = line.split(',') break return ','.join([_vmName, _vmID, _vmIP, _domID, _signature, _seqno]) def check_domid_changed(vmName): curr_domid = get_vm_id(vmName) if (curr_domid is None) or (not curr_domid.isdigit()): curr_domid = '-1' vm_name = vmName logfilename = logpath + vm_name + ".log" if not os.path.exists(logfilename): return ['-1', curr_domid] lines = (line.rstrip() for line in open(logfilename)) [_vmName,_vmID,_vmIP,old_domid,_signature,_seqno] = ['_', '-1', '_', '-1', '_', '-1'] for line in lines: [_vmName,_vmID,_vmIP,old_domid,_signature,_seqno] = line.split(',') break return [curr_domid, old_domid] def network_rules_for_rebooted_vm(vmName): vm_name = vmName [curr_domid, old_domid] = check_domid_changed(vm_name) if curr_domid == old_domid: return True if old_domid == '-1': return True if curr_domid == '-1': return True logging.debug("Found a rebooted VM -- reprogramming rules for " + vm_name) delete_rules_for_vm_in_bridge_firewall_chain(vm_name) brName = execute("iptables-save | awk -F '-j ' '/FORWARD -o(.*)physdev-is-bridged(.*)BF/ {print $2}'").strip() if not brName: brName = "cloudbr0" else: brName = execute("iptables-save |grep physdev-is-bridged |grep FORWARD |grep BF |grep '\-o' |awk '{print $4}' | head -1").strip() if 1 in [ vm_name.startswith(c) for c in ['r-', 's-', 'v-'] ]: default_network_rules_systemvm(vm_name, brName) return True vmchain = iptables_chain_name(vm_name) vmchain_default = '-'.join(vmchain.split('-')[:-1]) + "-def" vifs = get_vifs(vmName) logging.debug(vifs, brName) for v in vifs: execute("iptables -A " + get_br_fw(brName) + "-IN " + " -m physdev --physdev-is-bridged --physdev-in " + v + " -j " + vmchain_default) execute("iptables -A " + get_br_fw(brName) + "-OUT " + " -m physdev --physdev-is-bridged --physdev-out " + v + " -j " + vmchain_default) execute("ip6tables -A " + get_br_fw(brName) + "-IN " + " -m physdev --physdev-is-bridged --physdev-in " + v + " -j " + vmchain_default) execute("ip6tables -A " + get_br_fw(brName) + "-OUT " + " -m physdev --physdev-is-bridged --physdev-out " + v + " -j " + vmchain_default) #change antispoof rule in vmchain try: delcmd = """iptables-save | awk '/-A %s(.*)physdev/ { sub(/-A/, "-D", $1) ; print }'""" % vmchain_default inscmd = """iptables-save | awk '/-A %s(.*)physdev/ { gsub(/vnet[0-9]+/, "%s") ; sub(/-A/, "-D", $1) ; print }'""" % ( vmchain_default,vifs[0] ) ipts = [] for cmd in [delcmd, inscmd]: logging.debug(cmd) cmds = [_f for _f in execute(cmd).split('\n') if _f] for c in cmds: ipt = "iptables " + c ipts.append(ipt) for ipt in ipts: try: execute(ipt) except: logging.debug("Failed to rewrite antispoofing rules for vm " + vm_name) except: logging.debug("No rules found for vm " + vm_name) rewrite_rule_log_for_vm(vm_name, curr_domid) return True def get_rule_logs_for_vms(): state = ['running'] vms = virshlist(state) result = [] try: for name in vms: name = name.rstrip() if 1 not in [name.startswith(c) for c in ['r-', 's-', 'v-', 'i-'] ]: continue # Move actions on rebooted vm to java code # network_rules_for_rebooted_vm(name) if name.startswith('i-'): log = get_rule_log_for_vm(name) result.append(log) except: logging.exception("Failed to get rule logs, better luck next time!") print(";".join(result)) def cleanup_rules_for_dead_vms(): return True def cleanup_bridge(bridge): bridge_name = get_br_fw(bridge) logging.debug("Cleaning old bridge chains: " + bridge_name) if not bridge_name: return True # Delete iptables/bridge rules rules = execute("""iptables-save | grep %s | grep '^-A' | sed 's/-A/-D/' """ % bridge_name).split("\n") for rule in [_f for _f in rules if _f]: try: command = "iptables " + rule execute(command) except: pass chains = [bridge_name, bridge_name+'-IN', bridge_name+'-OUT'] # Flush old bridge chain for chain in chains: try: execute("iptables -F " + chain) except: pass # Remove bridge chains for chain in chains: try: execute("iptables -X " + chain) except: pass return True def cleanup_rules(): try: states = ['running', 'paused'] vmsInHost = virshlist(states) logging.debug(" Vms on the host : %s ", vmsInHost) cleanup = [] chainscmd = """iptables-save | grep -P '^:(?!.*-(def|eg))' | awk '{sub(/^:/, "", $1) ; print $1}' | sort | uniq""" chains = execute(chainscmd).split('\n') logging.debug(" iptables chains in the host :%s ", chains) for chain in chains: if 1 in [ chain.startswith(c) for c in ['r-', 'i-', 's-', 'v-'] ]: vm_name = chain vmpresent = False for vm in vmsInHost: if vm_name in vm: vmpresent = True break if vmpresent is False: logging.debug("vm " + vm_name + " is not running or paused, cleaning up iptables rules") cleanup.append(vm_name) bridge_tables = execute("""grep -E '^ebtable_' /proc/modules | cut -f1 -d' ' | sed s/ebtable_//""").split('\n') for table in [_f for _f in bridge_tables if _f]: chainscmd = """ebtables -t %s -L | awk '/chain:/ { gsub(/(^.*chain: |-(in|out|ips).*)/, ""); print $1}' | sort | uniq""" % table chains = execute(chainscmd).split('\n') logging.debug(" ebtables chains in the host: %s ", chains) for chain in [_f for _f in chains if _f]: if 1 in [chain.startswith(c) for c in ['r-', 'i-', 's-', 'v-']]: vm_name = chain vmpresent = False for vm in vmsInHost: if vm_name in vm: vmpresent = True break if vmpresent is False: logging.debug("vm " + vm_name + " is not running or paused, cleaning up ebtables rules") cleanup.append(vm_name) cleanup = list(set(cleanup)) # remove duplicates for vmname in cleanup: destroy_network_rules_for_vm(vmname) logging.debug("Cleaned up rules for " + str(len(cleanup)) + " chains") except: logging.debug("Failed to cleanup rules !") def check_rule_log_for_vm(vmName, vmId, vmIP, domID, signature, seqno): vm_name = vmName logfilename = logpath + vm_name + ".log" if not os.path.exists(logfilename): return [True, True, True, True, True, True] try: lines = (line.rstrip() for line in open(logfilename)) except: logging.debug("failed to open " + logfilename) return [True, True, True, True, True, True] [_vmName,_vmID,_vmIP,_domID,_signature,_seqno] = ['_', '-1', '_', '-1', '_', '-1'] try: for line in lines: [_vmName,_vmID,_vmIP,_domID,_signature,_seqno] = line.split(',') break except: logging.debug("Failed to parse log file for vm " + vm_name) remove_rule_log_for_vm(vm_name) return [True, True, True, True, True, True] return [(vm_name != _vmName), (vmId != _vmID), (vmIP != _vmIP), (domID != _domID), (signature != _signature),(seqno != _seqno)] def write_rule_log_for_vm(vmName, vmID, vmIP, domID, signature, seqno): vm_name = vmName logfilename = logpath + vm_name + ".log" logging.debug("Writing log to " + logfilename) logf = open(logfilename, 'w') output = ','.join([vmName, vmID, vmIP, str(domID), signature, seqno]) result = True try: logf.write(output) logf.write('\n') except: logging.debug("Failed to write to rule log file " + logfilename) result = False logf.close() return result def remove_rule_log_for_vm(vmName): vm_name = vmName logfilename = logpath + vm_name + ".log" result = True try: os.remove(logfilename) except: logging.debug("Failed to delete rule log file " + logfilename) result = False return result #ebtables chain max len 31 char def ebtables_chain_name(vm_name): # 23 because there are appends to the chains if len(vm_name) > 22: vm_name = vm_name[0:22] return vm_name #ipset chain max len 31 char def ipset_chain_name(vm_name): if len(vm_name) > 30: vm_name = vm_name[0:30] return vm_name #iptables chain max len 29 char and it is appended with other names like -eg,-def def iptables_chain_name(vm_name): if len(vm_name) > 25: vm_name = vm_name[0:24] return vm_name def egress_chain_name(vm_name): chain_name = iptables_chain_name(vm_name) return chain_name + "-eg" def parse_network_rules(rules): ret = [] if rules is None or len(rules) == 0: return ret lines = rules.split('NEXT;')[:-1] for line in lines: tokens = line.split(';', 3) if len(tokens) != 4: continue ruletype, protocol = tokens[0].split(':') start = int(tokens[1]) end = int(tokens[2]) cidrs = tokens.pop() ipv4 = [] ipv6 = [] for ip in cidrs.split(","): try: network = ipaddress.ip_network(ip, False) 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 domId = get_vm_id(vmName) changes = check_rule_log_for_vm(vmName, vm_id, vm_ip, domId, signature, seqno) if 1 not in changes: logging.debug("Rules already programmed for vm " + vm_name) return True if rules == "" or rules == None: lines = [] else: lines = rules.split(';')[:-1] logging.debug("programming network rules for IP: " + vm_ip + " vmname=%s", vm_name) egress_chain_name(vm_name) vmchain = iptables_chain_name(vm_name) egress_vmchain = egress_chain_name(vm_name) try: for chain in [vmchain, egress_vmchain]: execute('iptables -F ' + chain) execute('ip6tables -F ' + chain) except: logging.debug("Error flushing iptables rules for " + vm_name + ". Presuming firewall rules deleted, re-initializing." ) default_network_rules(vm_name, vm_id, vm_ip, vm_ip6, vmMac, vif, brname, sec_ips, True) 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_vmchain direction = "-d" action = "RETURN" if rule['ipv4']: egressrule_v4 =+ 1 if rule['ipv6']: egressrule_v6 +=1 else: vmchain = vm_name action = "ACCEPT" direction = "-s" if start == 0 and end == 0: dport = "" else: dport = " --dport " + str(start) + ":" + str(end) if protocol != 'all' and protocol != 'icmp' and protocol != 'tcp' and protocol != 'udp': protocol_all = " -p " + protocol protocol_state = " " else: protocol_all = " -p " + protocol + " -m " + protocol protocol_state = " -m state --state NEW " 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 + ' ' + ip + ' -j ' + action) elif protocol == 'icmp': execute("iptables -I " + vmchain + " -p icmp --icmp-type " + range + " " + direction + " " + ip + " -j " + action) else: execute("iptables -I " + vmchain + protocol_all + dport + protocol_state + 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 + protocol_all + dport + protocol_state + direction + " " + ip + " -j "+ action) else: # ip6tables does not allow '--icmpv6-type any', allowing all ICMPv6 is done by not allowing a specific type if range == 'any': execute('ip6tables -I ' + vmchain + ' -p icmpv6 ' + 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_v4 == 0 : execute('iptables -A ' + egress_vmchain + ' -j RETURN') else: execute('iptables -A ' + egress_vmchain + ' -j DROP') if egressrule_v6 == 0 : execute('ip6tables -A ' + egress_vmchain + ' -j RETURN') else: execute('ip6tables -A ' + egress_vmchain + ' -j DROP') vmchain = iptables_chain_name(vm_name) execute('iptables -A ' + vmchain + ' -j DROP') execute('ip6tables -A ' + vmchain + ' -j DROP') if not write_rule_log_for_vm(vmName, vm_id, vm_ip, domId, signature, seqno): return False return True except: logging.exception("Failed to network rule !") def get_vifs(vm_name): vifs = [] xmlfile = virshdumpxml(vm_name) if not xmlfile: return vifs dom = xml.dom.minidom.parseString(xmlfile) for network in dom.getElementsByTagName("interface"): target = network.getElementsByTagName('target')[0] nicdev = target.getAttribute("dev").strip() vifs.append(nicdev) return vifs def get_vifs_for_bridge(vm_name, brname): vifs = [] xmlfile = virshdumpxml(vm_name) if not xmlfile: return vifs dom = xml.dom.minidom.parseString(xmlfile) for network in dom.getElementsByTagName("interface"): source = network.getElementsByTagName('source')[0] bridge = source.getAttribute("bridge").strip() if bridge == brname: target = network.getElementsByTagName('target')[0] nicdev = target.getAttribute("dev").strip() vifs.append(nicdev) return list(set(vifs)) def get_bridges(vm_name): bridges = [] xmlfile = virshdumpxml(vm_name) if not xmlfile: return bridges dom = xml.dom.minidom.parseString(xmlfile) for network in dom.getElementsByTagName("interface"): for source in network.getElementsByTagName('source'): bridge = source.getAttribute("bridge").strip() bridges.append(bridge) return list(set(bridges)) def get_vm_id(vm_name): conn = get_libvirt_connection() try: dom = (conn.lookupByName (vm_name)) except libvirt.libvirtError: return None conn.close() res = dom.ID() if isinstance(res, int): res = str(res) return res def get_br_fw(brname): cmd = "iptables-save |grep physdev-is-bridged |grep FORWARD |grep BF |grep '\-o' | grep -w " + brname + "|awk '{print $9}' | head -1" brfwname = execute(cmd).strip() if not brfwname: brfwname = "BF-" + brname return brfwname def add_fw_framework(brname): try: execute("modprobe br_netfilter") except: logging.debug("failed to load kernel module br_netfilter") try: execute("sysctl -w net.bridge.bridge-nf-call-arptables=1") execute("sysctl -w net.bridge.bridge-nf-call-iptables=1") execute("sysctl -w net.bridge.bridge-nf-call-ip6tables=1") except: logging.warn("failed to turn on bridge netfilter") brfw = get_br_fw(brname) try: execute("iptables -L " + brfw) except: execute("iptables -N " + brfw) brfwout = brfw + "-OUT" try: execute("iptables -L " + brfwout) except: execute("iptables -N " + brfwout) brfwin = brfw + "-IN" try: execute("iptables -L " + brfwin) except: execute("iptables -N " + brfwin) try: execute('ip6tables -L ' + brfw) except: execute('ip6tables -N ' + brfw) brfwout = brfw + "-OUT" try: execute('ip6tables -L ' + brfwout) except: execute('ip6tables -N ' + brfwout) brfwin = brfw + "-IN" try: execute('ip6tables -L ' + brfwin) except: execute('ip6tables -N ' + brfwin) physdev = get_bridge_physdev(brname) try: refs = int(execute("""iptables -n -L %s | awk '/%s(.*)references/ {gsub(/\(/, "") ;print $3}'""" % (brfw,brfw)).strip()) refs_in = int(execute("""iptables -n -L %s-IN | awk '/%s-IN(.*)references/ {gsub(/\(/, "") ;print $3}'""" % (brfw,brfw)).strip()) refs_out = int(execute("""iptables -n -L %s-OUT | awk '/%s-OUT(.*)references/ {gsub(/\(/, "") ;print $3}'""" % (brfw,brfw)).strip()) refs6 = int(execute("""ip6tables -n -L %s | awk '/%s(.*)references/ {gsub(/\(/, "") ;print $3}'""" % (brfw,brfw)).strip()) if refs == 0 or refs_in == 0 or refs_out == 0: execute("iptables -I FORWARD -i " + brname + " -j DROP") execute("iptables -I FORWARD -o " + brname + " -j DROP") execute("iptables -I FORWARD -i " + brname + " -m physdev --physdev-is-bridged -j " + brfw) execute("iptables -I FORWARD -o " + brname + " -m physdev --physdev-is-bridged -j " + brfw) execute("iptables -A " + brfw + " -m state --state RELATED,ESTABLISHED -j ACCEPT") execute("iptables -A " + brfw + " -m physdev --physdev-is-bridged --physdev-is-in -j " + brfwin) execute("iptables -A " + brfw + " -m physdev --physdev-is-bridged --physdev-is-out -j " + brfwout) execute("iptables -A " + brfw + " -m physdev --physdev-is-bridged --physdev-out " + physdev + " -j ACCEPT") if refs6 == 0: execute('ip6tables -I FORWARD -i ' + brname + ' -j DROP') execute('ip6tables -I FORWARD -o ' + brname + ' -j DROP') execute('ip6tables -I FORWARD -i ' + brname + ' -m physdev --physdev-is-bridged -j ' + brfw) execute('ip6tables -I FORWARD -o ' + brname + ' -m physdev --physdev-is-bridged -j ' + brfw) execute('ip6tables -A ' + brfw + ' -m state --state RELATED,ESTABLISHED -j ACCEPT') execute('ip6tables -A ' + brfw + ' -m physdev --physdev-is-bridged --physdev-is-in -j ' + brfwin) execute('ip6tables -A ' + brfw + ' -m physdev --physdev-is-bridged --physdev-is-out -j ' + brfwout) execute('ip6tables -A ' + brfw + ' -m physdev --physdev-is-bridged --physdev-out ' + physdev + ' -j ACCEPT') return True except: try: execute("iptables -F " + brfw) execute('ip6tables -F ' + brfw) except: return False return False def verify_network_rules(vm_name, vm_id, vm_ip, vm_ip6, vm_mac, vif, brname, sec_ips): if vm_name is None or vm_ip is None or vm_mac is None: print("vm_name, vm_ip and vm_mac must be specifed") sys.exit(1) if vm_id is None: vm_id = vm_name.split("-")[-2] if brname is None: brname = execute("virsh domiflist %s |grep -w '%s' | awk '{print $3}'" % (vm_name, vm_mac)).strip() if not brname: print("Cannot find bridge") sys.exit(1) if vif is None: vif = execute("virsh domiflist %s |grep -w '%s' | awk '{print $1}'" % (vm_name, vm_mac)).strip() if not vif: print("Cannot find vif") sys.exit(1) #vm_name = "i-2-55-VM" #vm_id = 55 #vm_ip = "10.11.118.128" #vm_ip6 = "fe80::1c00:b4ff:fe00:5" #vm_mac = "1e:00:b4:00:00:05" #vif = "vnet11" #brname = "cloudbr0" #sec_ips = "10.11.118.133;10.11.118.135;10.11.118.138;" # end with ";" and separated by ";" vm_ips = [] if sec_ips is not None: vm_ips = sec_ips.split(';') vm_ips.pop() vm_ips.reverse() vm_ips.append(vm_ip) if not verify_ipset_for_vm(vm_name, vm_id, vm_ips, vm_ip6): sys.exit(2) if not verify_iptables_rules_for_bridge(brname): sys.exit(3) if not verify_default_iptables_rules_for_vm(vm_name, vm_id, vm_ips, vm_ip6, vm_mac, vif, brname): sys.exit(4) if not verify_ebtables_rules_for_vm(vm_name, vm_id, vm_ips, vm_ip6, vm_mac, vif, brname): sys.exit(5) sys.exit(0) def verify_ipset_for_vm(vm_name, vm_id, vm_ips, vm_ip6): vmipsetName = ipset_chain_name(vm_name) vmipsetName6 = vmipsetName + '-6' rules = [] for rule in execute("ipset list %s" % vmipsetName).split('\n'): rules.append(rule) # Check if all vm ips and ip6 exist for vm_ip in vm_ips: found = False for rule in rules: if rule == vm_ip: found = True break if not found: print("vm ip %s is not found" % vm_ip) return False rules = [] for rule in execute("ipset list %s" % vmipsetName6).split('\n'): rules.append(rule) if vm_ip6 is not None: found = False for rule in rules: if rule == vm_ip6: found = True break if not found: print("vm ipv6 %s is not found" % vm_ip6) return False return True def verify_iptables_rules_for_bridge(brname): brfw = get_br_fw(brname) brfwin = brfw + "-IN" brfwout = brfw + "-OUT" expected_rules = [] expected_rules.append("-A FORWARD -o %s -m physdev --physdev-is-bridged -j %s" % (brname, brfw)) expected_rules.append("-A FORWARD -i %s -m physdev --physdev-is-bridged -j %s" % (brname, brfw)) expected_rules.append("-A %s -m state --state RELATED,ESTABLISHED -j ACCEPT" % (brfw)) expected_rules.append("-A %s -m physdev --physdev-is-in --physdev-is-bridged -j %s" % (brfw, brfwin)) expected_rules.append("-A %s -m physdev --physdev-is-out --physdev-is-bridged -j %s" % (brfw, brfwout)) phydev = get_bridge_physdev(brname) expected_rules.append("-A %s -m physdev --physdev-out %s --physdev-is-bridged -j ACCEPT" % (brfw, phydev)) rules = execute("iptables-save |grep -w %s |grep -v \"^:\"" % brfw).split('\n') return verify_expected_rules_exist(expected_rules, rules) def verify_default_iptables_rules_for_vm(vm_name, vm_id, vm_ips, vm_ip6, vm_mac, vif, brname): brfw = get_br_fw(brname) brfwin = brfw + "-IN" brfwout = brfw + "-OUT" vmchain = iptables_chain_name(vm_name) vmchain_egress = egress_chain_name(vm_name) vm_def = '-'.join(vm_name.split('-')[:-1]) + "-def" expected_rules = [] expected_rules.append("-A %s -m physdev --physdev-in %s --physdev-is-bridged -j %s" % (brfwin, vif, vm_def)) expected_rules.append("-A %s -m physdev --physdev-out %s --physdev-is-bridged -j %s" % (brfwout, vif, vm_def)) expected_rules.append("-A %s -m state --state RELATED,ESTABLISHED -j ACCEPT" % (vm_def)) expected_rules.append("-A %s -p udp -m physdev --physdev-in %s --physdev-is-bridged -m udp --sport 68 --dport 67 -j ACCEPT" % (vm_def, vif)) expected_rules.append("-A %s -p udp -m physdev --physdev-out %s --physdev-is-bridged -m udp --sport 67 --dport 68 -j ACCEPT" % (vm_def, vif)) expected_rules.append("-A %s -p udp -m physdev --physdev-in %s --physdev-is-bridged -m udp --sport 67 -j DROP" % (vm_def, vif)) expected_rules.append("-A %s -m physdev --physdev-in %s --physdev-is-bridged -m set ! --match-set %s src -j DROP" % (vm_def, vif, vm_name)) expected_rules.append("-A %s -m physdev --physdev-out %s --physdev-is-bridged -m set ! --match-set %s dst -j DROP" % (vm_def, vif, vm_name)) expected_rules.append("-A %s -p udp -m physdev --physdev-in %s --physdev-is-bridged -m set --match-set %s src -m udp --dport 53 -j RETURN" % (vm_def, vif, vm_name)) expected_rules.append("-A %s -p tcp -m physdev --physdev-in %s --physdev-is-bridged -m set --match-set %s src -m tcp --dport 53 -j RETURN" % (vm_def, vif, vm_name)) expected_rules.append("-A %s -m physdev --physdev-in %s --physdev-is-bridged -m set --match-set %s src -j %s" % (vm_def, vif, vm_name, vmchain_egress)) expected_rules.append("-A %s -m physdev --physdev-out %s --physdev-is-bridged -j %s" % (vm_def, vif, vmchain)) rules = execute("iptables-save |grep -E \"%s|%s\" |grep -v \"^:\"" % (vm_name, vm_def)).split('\n') return verify_expected_rules_in_order(expected_rules, rules) def verify_ebtables_rules_for_vm(vm_name, vm_id, vm_ips, vm_ip6, vm_mac, vif, brname): vmchain_in = vm_name + "-in" vmchain_out = vm_name + "-out" vmchain_in_ips = vm_name + "-in-ips" vmchain_out_ips = vm_name + "-out-ips" vmchain_in_src = vm_name + "-in-src" vmchain_out_dst = vm_name + "-out-dst" new_mac = trim_mac_address(vm_mac) # PREROUTING/POSTROUTING expected_rules = [] expected_rules.append("-A PREROUTING -i %s -j %s" % (vif, vmchain_in)) expected_rules.append("-A POSTROUTING -o %s -j %s" % (vif, vmchain_out)) rules = execute("ebtables-save |grep -E \"PREROUTING|POSTROUTING\" | grep %s" % vm_name).split('\n') if not verify_expected_rules_exist(expected_rules, rules): return False rules = execute("ebtables-save | grep %s" % vm_name).split('\n') # vmchain_in expected_rules = [] expected_rules.append("-A %s -j %s" % (vmchain_in, vmchain_in_src)) expected_rules.append("-A %s -p ARP -j %s" % (vmchain_in, vmchain_in_ips)) expected_rules.append("-A %s -p ARP --arp-op Request -j ACCEPT" % (vmchain_in)) expected_rules.append("-A %s -p ARP --arp-op Reply -j ACCEPT" % (vmchain_in)) expected_rules.append("-A %s -p ARP -j DROP" % (vmchain_in)) if not verify_expected_rules_in_order(expected_rules, rules): return False # vmchain_in_src expected_rules = [] expected_rules.append("-A %s -s %s -j RETURN" % (vmchain_in_src, new_mac)) expected_rules.append("-A %s -j DROP" % (vmchain_in_src)) if not verify_expected_rules_in_order(expected_rules, rules): return False # vmchain_in_ips expected_rules = [] for vm_ip in vm_ips: expected_rules.append("-A %s -p ARP -s %s --arp-ip-src %s --arp-mac-src %s -j RETURN" % (vmchain_in_ips, new_mac, vm_ip, new_mac)) expected_rules.append("-A %s -j DROP" % (vmchain_in_ips)) if not verify_expected_rules_in_order(expected_rules, rules): return False # vmchain_out expected_rules = [] expected_rules.append("-A %s -p ARP --arp-op Reply -j %s" % (vmchain_out, vmchain_out_dst)) expected_rules.append("-A %s -p ARP -j %s" % (vmchain_out, vmchain_out_ips)) expected_rules.append("-A %s -p ARP --arp-op Request -j ACCEPT" % (vmchain_out)) expected_rules.append("-A %s -p ARP --arp-op Reply -j ACCEPT" % (vmchain_out)) expected_rules.append("-A %s -p ARP -j DROP" % (vmchain_out)) if not verify_expected_rules_in_order(expected_rules, rules): return False # vmchain_out_dst expected_rules = [] expected_rules.append("-A %s -p ARP --arp-op Reply --arp-mac-dst %s -j RETURN" % (vmchain_out_dst, new_mac)) expected_rules.append("-A %s -p ARP --arp-op Reply -j DROP" % (vmchain_out_dst)) if not verify_expected_rules_in_order(expected_rules, rules): return False # vmchain_out_ips expected_rules = [] for vm_ip in vm_ips: expected_rules.append("-A %s -p ARP --arp-ip-dst %s -j RETURN" % (vmchain_out_ips, vm_ip)) expected_rules.append("-A %s -j DROP" % (vmchain_out_ips)) if not verify_expected_rules_in_order(expected_rules, rules): return False return True def trim_mac_address(vm_mac): new_mac = "" for mac in vm_mac.split(":"): if mac.startswith("0"): new_mac += ":" + mac[1:] else: new_mac += ":" + mac return new_mac[1:] def verify_expected_rules_exist(expected_rules, rules): # Check if expected rules exist for expected_rule in expected_rules: found = False for rule in rules: if rule == expected_rule: found = True break if not found: print("Rule '%s' is not found" % expected_rule) return False return True def verify_expected_rules_in_order(expected_rules, rules): # Check if expected rules exist in order (!!!) i = 0 for rule in rules: if i < len(expected_rules) and rule == expected_rules[i]: i += 1 if i != len(expected_rules): print("Cannot find rule '%s'" % expected_rules[i]) return False return True if __name__ == '__main__': logging.basicConfig(filename="/var/log/cloudstack/agent/security_group.log", format="%(asctime)s - %(message)s", level=logging.DEBUG) parser = argparse.ArgumentParser(description='Apache CloudStack Security Groups') parser.add_argument("command") parser.add_argument("--vmname", dest="vmName") parser.add_argument("--vmip", dest="vmIP") parser.add_argument("--vmip6", dest="vmIP6") parser.add_argument("--vmid", dest="vmID") parser.add_argument("--vmmac", dest="vmMAC") parser.add_argument("--vif", dest="vif") parser.add_argument("--sig", dest="sig") parser.add_argument("--seq", dest="seq") parser.add_argument("--rules", dest="rules") parser.add_argument("--brname", dest="brname") parser.add_argument("--localbrname", dest="localbrname") parser.add_argument("--dhcpSvr", dest="dhcpSvr") parser.add_argument("--hostIp", dest="hostIp") parser.add_argument("--hostMacAddr", dest="hostMacAddr") parser.add_argument("--nicsecips", dest="nicSecIps") parser.add_argument("--action", dest="action") parser.add_argument("--privnic", dest="privnic") parser.add_argument("--isFirstNic", action="store_true", dest="isFirstNic") parser.add_argument("--check", action="store_true", dest="check") args = parser.parse_args() cmd = args.command logging.debug("Executing command: %s", cmd) for i in range(0, 30): if obtain_file_lock(lock_file) is False: logging.warning("Lock on %s is being held by other process. Waiting for release." % lock_file) time.sleep(0.5) else: break if cmd == "can_bridge_firewall": can_bridge_firewall(args.privnic) elif cmd == "default_network_rules" and args.check: check_default_network_rules(args.vmName, args.vmID, args.vmIP, args.vmIP6, args.vmMAC, args.vif, args.brname, args.nicSecIps, args.isFirstNic) elif cmd == "default_network_rules" and not args.check: default_network_rules(args.vmName, args.vmID, args.vmIP, args.vmIP6, args.vmMAC, args.vif, args.brname, args.nicSecIps, args.isFirstNic) elif cmd == "destroy_network_rules_for_vm": if args.vmIP is None: destroy_network_rules_for_vm(args.vmName, args.vif) else: destroy_network_rules_for_nic(args.vmName, args.vmIP, args.vmMAC, args.vif, args.nicSecIps) elif cmd == "default_network_rules_systemvm": default_network_rules_systemvm(args.vmName, args.localbrname) elif cmd == "get_rule_logs_for_vms": get_rule_logs_for_vms() elif cmd == "add_network_rules": add_network_rules(args.vmName, args.vmID, args.vmIP, args.vmIP6, args.sig, args.seq, args.vmMAC, args.rules, args.vif, args.brname, args.nicSecIps) elif cmd == "network_rules_vmSecondaryIp": network_rules_vmSecondaryIp(args.vmName, args.vmMAC, args.nicSecIps, args.action) elif cmd == "cleanup_rules": cleanup_rules() elif cmd == "post_default_network_rules": post_default_network_rules(args.vmName, args.vmID, args.vmIP, args.vmMAC, args.vif, args.brname, args.dhcpSvr, args.hostIp, args.hostMacAddr) elif cmd == "verify_network_rules": verify_network_rules(args.vmName, args.vmID, args.vmIP, args.vmIP6, args.vmMAC, args.vif, args.brname, args.nicSecIps) else: logging.debug("Unknown command: " + cmd) sys.exit(1)