dom0/qvm-tools: introduce cmdline firewall editor: qvm-firewall
This commit is contained in:
		
							parent
							
								
									e7b65b457b
								
							
						
					
					
						commit
						e8772352e2
					
				
							
								
								
									
										316
									
								
								dom0/qvm-tools/qvm-firewall
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										316
									
								
								dom0/qvm-tools/qvm-firewall
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,316 @@ | ||||
| #!/usr/bin/python2.6 | ||||
| # | ||||
| # The Qubes OS Project, http://www.qubes-os.org | ||||
| # | ||||
| # Copyright (C) 2012  Marek Marczykowski <marmarek@invisiblethingslab.com> | ||||
| # | ||||
| # This program is free software; you can redistribute it and/or | ||||
| # modify it under the terms of the GNU General Public License | ||||
| # as published by the Free Software Foundation; either version 2 | ||||
| # of the License, or (at your option) any later version. | ||||
| # | ||||
| # This program is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with this program; if not, write to the Free Software | ||||
| # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA. | ||||
| # | ||||
| # | ||||
| 
 | ||||
| from qubes.qubes import QubesVmCollection | ||||
| from optparse import OptionParser; | ||||
| import subprocess | ||||
| import sys | ||||
| import re | ||||
| 
 | ||||
| services = list() | ||||
| 
 | ||||
| def load_services(): | ||||
|     global services | ||||
|     services = list() | ||||
|     pattern = re.compile("(?P<name>[a-z][a-z0-9-]+)\s+(?P<port>[0-9]+)/(?P<protocol>[a-z]+)", re.IGNORECASE) | ||||
|     f = open('/etc/services', 'r') | ||||
|     for line in f: | ||||
|         match = pattern.match(line) | ||||
|         if match is not None: | ||||
|             service = match.groupdict() | ||||
|             services.append( (service["name"], int(service["port"]), service["protocol"]) ) | ||||
|     f.close() | ||||
| 
 | ||||
| def get_service_name(port): | ||||
|     for service in services: | ||||
|         if service[1] == port: | ||||
|             return service[0] | ||||
|     return str(port) | ||||
| 
 | ||||
| def get_service_port(name): | ||||
|     for service in services: | ||||
|         if service[0] == name: | ||||
|             return int(service[1]) | ||||
|     return None | ||||
| 
 | ||||
| def parse_rule(args): | ||||
|     if len(args) < 2: | ||||
|         print >>sys.stderr, "ERROR: Rule must have at least address and protocol" | ||||
|         return None | ||||
| 
 | ||||
|     address = args[0] | ||||
|     netmask = 32 | ||||
|     proto = args[1] | ||||
|     port = args[2] if len(args) > 2 else None | ||||
|     port_end = None | ||||
| 
 | ||||
|     unmask = address.split("/", 1) | ||||
|     if len(unmask) == 2: | ||||
|         address = unmask[0] | ||||
|         netmask = unmask[1] | ||||
|         if netmask.isdigit(): | ||||
|             if re.match("^([0-9]{1,3}\.){3}[0-9]{1,3}$", address) is None: | ||||
|                 print >>sys.stderr, "ERROR: Only IP is allowed when specyfying netmask" | ||||
|                 return None | ||||
|             if netmask != "": | ||||
|                 netmask = int(unmask[1]) | ||||
|                 if netmask < 0 or netmask > 32: | ||||
|                     print >>sys.stderr, "ERROR: Invalid netmask" | ||||
|                     return None | ||||
|         else: | ||||
|             print >>sys.stderr, "ERROR: Invalid netmask" | ||||
|             return None | ||||
| 
 | ||||
|     if address[-1:] == ".": | ||||
|         address = address[:-1] | ||||
| 
 | ||||
|     allowed = re.compile("(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE) | ||||
|     if not all(allowed.match(x) for x in address.split(".")): | ||||
|         print >>sys.stderr, "ERROR: Invalid hostname" | ||||
|         return None | ||||
| 
 | ||||
|     proto_split = proto.split('/', 1) | ||||
|     if len(proto_split) == 2: | ||||
|         proto = proto_split[0] | ||||
|         port = proto_split[1] | ||||
| 
 | ||||
|     if proto not in ['tcp', 'udp', 'any']: | ||||
|         print >>sys.stderr, "ERROR: Protocol must be one of: 'tcp', 'udp', 'any'" | ||||
|         return None | ||||
| 
 | ||||
|     if proto != "any" and port is None: | ||||
|         print >>sys.stderr, "ERROR: Port required for protocol %s" % args[1] | ||||
|         return None | ||||
| 
 | ||||
|     if port is not None: | ||||
|         port_range = port.split('-', 1) | ||||
|         if len(port_range) == 2: | ||||
|             port = port_range[0] | ||||
|             port_end = port_range[1] | ||||
| 
 | ||||
|         if get_service_port(port): | ||||
|             port = get_service_port(port) | ||||
|         elif not port.isdigit(): | ||||
|             print >>sys.stderr, "ERROR: Invalid port/service name '%s'" % port | ||||
|             return None | ||||
|         else: | ||||
|             port = int(port) | ||||
| 
 | ||||
|         if port_end is not None and not port_end.isdigit(): | ||||
|             print >>sys.stderr, "ERROR: Invalid port '%s'" % port_end | ||||
|             return None | ||||
| 
 | ||||
|         if port_end is not None: | ||||
|             port_end = int(port_end) | ||||
| 
 | ||||
|     rule = {} | ||||
|     rule['address'] = address | ||||
|     rule['netmask'] = netmask | ||||
|     rule['proto'] = proto | ||||
|     rule['portBegin'] = port | ||||
|     rule['portEnd'] = port_end | ||||
|     return rule | ||||
| 
 | ||||
| def list_rules(rules): | ||||
|     fields = [ "num", "address", "proto", "port(s)" ] | ||||
| 
 | ||||
|     rules_to_display = list() | ||||
|     counter = 1 | ||||
|     for rule in rules: | ||||
|         parsed_rule = { | ||||
|             'num': "{0:>2}".format(counter), | ||||
|             'address': rule['address'] + ('/' + str(rule['netmask']) if rule['netmask'] < 32 else ""), | ||||
|             'proto': rule['proto'], | ||||
|             'port(s)': '', | ||||
|         } | ||||
|         if rule['proto'] in ['tcp', 'udp']: | ||||
|             parsed_rule['port(s)'] = str(rule['portBegin']) + \ | ||||
|                 ('-' + str(rule['portEnd']) if rule['portEnd'] is not None else '') | ||||
|             if rule['portBegin'] is not None and rule['portEnd'] is None: | ||||
|                 parsed_rule['port(s)'] = get_service_name(rule['portBegin']) | ||||
| 
 | ||||
|         rules_to_display.append(parsed_rule) | ||||
|         counter += 1 | ||||
| 
 | ||||
|     fields_width = {} | ||||
|     for f in fields: | ||||
|         fields_width[f] = len(f) | ||||
|         for r in rules_to_display: | ||||
|             if len(r[f]) > fields_width[f]: | ||||
|                 fields_width[f] = len(r[f]) | ||||
| 
 | ||||
|     # Display the header | ||||
|     s = "" | ||||
|     for f in fields: | ||||
|         fmt="{{0:-^{0}}}-+".format(fields_width[f] + 1) | ||||
|         s += fmt.format('-') | ||||
|     print s | ||||
| 
 | ||||
|     s = "" | ||||
|     for f in fields: | ||||
|         fmt=" {{0:^{0}}} |".format(fields_width[f]) | ||||
|         s += fmt.format(f) | ||||
|     print s | ||||
| 
 | ||||
|     s = "" | ||||
|     for f in fields: | ||||
|         fmt="{{0:-^{0}}}-+".format(fields_width[f] + 1) | ||||
|         s += fmt.format('-') | ||||
|     print s | ||||
| 
 | ||||
|     # And the content | ||||
|     for r in rules_to_display: | ||||
|         s = "" | ||||
|         for f in fields: | ||||
|             fmt=" {{0:<{0}}} |".format(fields_width[f]) | ||||
|             s += fmt.format(r[f]) | ||||
|         print s | ||||
| 
 | ||||
| def display_firewall(conf): | ||||
|     print "Firewall policy: %s" % ( | ||||
|             "ALLOW all traffic except" if conf['allow'] else "DENY all traffic except") | ||||
|     print "ICMP: %s" % ("ALLOW" if conf['allowIcmp'] else 'DENY') | ||||
|     print "DMS: %s" % ("ALLOW" if conf['allowDns'] else 'DENY') | ||||
|     list_rules(conf['rules']) | ||||
| 
 | ||||
| def add_rule(conf, args): | ||||
|     rule = parse_rule(args) | ||||
|     if rule is None: | ||||
|         return False | ||||
| 
 | ||||
|     conf['rules'].append(rule) | ||||
|     return True | ||||
| 
 | ||||
| def del_rule(conf, args): | ||||
|     if len(args) == 1 and args[0].isdigit(): | ||||
|         rulenum = int(args[0]) | ||||
|         if rulenum < 1 or rulenum > len(conf['rules']): | ||||
|             print >>sys.stderr, "ERROR: Rule number out of range" | ||||
|             return False | ||||
|         conf['rules'].pop(rulenum-1) | ||||
|     else: | ||||
|         rule = parse_rule(args) | ||||
|         #print "PARSED: %s" % str(rule) | ||||
|         #print "ALL: %s" % str(conf['rules']) | ||||
|         if rule is None: | ||||
|             return False | ||||
|         try: | ||||
|             conf['rules'].remove(rule) | ||||
|         except ValueError: | ||||
|             print >>sys.stderr, "ERROR: Rule not found" | ||||
|             return False | ||||
| 
 | ||||
|     return True | ||||
| 
 | ||||
| def allow_deny_value(s): | ||||
|     value = None | ||||
|     if s == "allow": | ||||
|         value = True | ||||
|     elif s == "deny": | ||||
|         value = False | ||||
|     else: | ||||
|         print >>sys.stderr, 'ERROR: Only "allow" or "deny" allowed' | ||||
|         exit(1) | ||||
|     return value | ||||
| 
 | ||||
| def main(): | ||||
|     usage =  "usage: %prog [-n] <vm-name> [action] [rule spec]\n" | ||||
|     usage += "       rule specification can be one of:\n" | ||||
|     usage += "         address|hostname[/netmask] tcp|udp port[-port]\n" | ||||
|     usage += "         address|hostname[/netmask] tcp|udp service_name\n" | ||||
|     usage += "         address|hostname[/netmask] any\n" | ||||
|     parser = OptionParser (usage) | ||||
|     parser.add_option ("-l", "--list", dest="do_list", action="store_true", default=True, | ||||
|             help="List firewall settings (default action)") | ||||
|     parser.add_option ("-a", "--add", dest="do_add", action="store_true", default=False, | ||||
|             help="Add rule") | ||||
|     parser.add_option ("-d", "--del", dest="do_del", action="store_true", default=False, | ||||
|             help="Remove rule (given by number or by rule spec)") | ||||
|     parser.add_option ("-P", "--policy", dest="set_policy", action="store", default=None, | ||||
|             help="Set firewall policy (allow/deny)") | ||||
|     parser.add_option ("-i", "--icmp", dest="set_icmp", action="store", default=None, | ||||
|             help="Set ICMP access (allow/deny)") | ||||
|     parser.add_option ("-D", "--dns", dest="set_dns", action="store", default=None, | ||||
|             help="Set DNS access (allow/deny)") | ||||
| 
 | ||||
|     parser.add_option ("-n", "--numeric", dest="numeric", action="store_true", default=False, | ||||
|             help="Display port numbers instead of services (makes sense only with --list)") | ||||
| 
 | ||||
|     (options, args) = parser.parse_args () | ||||
|     if (len (args) < 1): | ||||
|         parser.error ("You must specify VM name!") | ||||
|     vmname = args[0] | ||||
|     args = args[1:] | ||||
| 
 | ||||
|     if options.do_add or options.do_del or options.set_policy or options.set_icmp or options.set_dns: | ||||
|         options.do_list = False | ||||
|     qvm_collection = QubesVmCollection() | ||||
|     if options.do_list: | ||||
|         qvm_collection.lock_db_for_reading() | ||||
|         qvm_collection.load() | ||||
|         qvm_collection.unlock_db() | ||||
|     else: | ||||
|         qvm_collection.lock_db_for_writing() | ||||
|         qvm_collection.load() | ||||
| 
 | ||||
|     vm = qvm_collection.get_vm_by_name(vmname) | ||||
|     if vm is None: | ||||
|         print >> sys.stderr, "A VM with the name '{0}' does not exist in the system.".format(vmname) | ||||
|         exit(1) | ||||
| 
 | ||||
|     changed = False | ||||
|     conf = vm.get_firewall_conf() | ||||
| 
 | ||||
|     if options.set_policy: | ||||
|         conf['allow'] = allow_deny_value(options.set_policy) | ||||
|         changed = True | ||||
|     if options.set_icmp: | ||||
|         conf['allowIcmp'] = allow_deny_value(options.set_icmp) | ||||
|         changed = True | ||||
|     if options.set_dns: | ||||
|         conf['allowDns'] = allow_deny_value(options.set_dns) | ||||
|         changed = True | ||||
| 
 | ||||
|     if options.do_add: | ||||
|         load_services() | ||||
|         changed = add_rule(conf, args) | ||||
|     elif options.do_del: | ||||
|         load_services() | ||||
|         changed = del_rule(conf, args) | ||||
|     elif options.do_list: | ||||
|         if not options.numeric: | ||||
|             load_services() | ||||
|         if not vm.has_firewall(): | ||||
|             print "INFO: This VM has no firewall set, below defaults are listed" | ||||
|         display_firewall(conf) | ||||
| 
 | ||||
|     if changed: | ||||
|         vm.write_firewall_conf(conf) | ||||
|         if vm.is_running(): | ||||
|             if vm.netvm is not None and vm.netvm.is_proxyvm(): | ||||
|                 vm.netvm.write_iptables_xenstore_entry() | ||||
| 
 | ||||
|     if not options.do_list: | ||||
|         qvm_collection.unlock_db() | ||||
| 
 | ||||
| 
 | ||||
| main() | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user