network: rewrite qubes-firewall daemon
This rewrite is mainly to adopt new interface for Qubes 4.x. Main changes: - change language from bash to python, introduce qubesagent python package - support both nftables (preferred) and iptables - new interface (https://qubes-os.org/doc/vm-interface/) - IPv6 support - unit tests included - nftables version support running along with other firewall loaded Fixes QubesOS/qubes-issues#1815 QubesOS/qubes-issues#718
This commit is contained in:
		
							parent
							
								
									b50cba3f2c
								
							
						
					
					
						commit
						ee0a292b21
					
				
							
								
								
									
										6
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								Makefile
									
									
									
									
									
								
							| @ -146,6 +146,11 @@ install-common: | |||||||
| 	$(MAKE) -C autostart-dropins install | 	$(MAKE) -C autostart-dropins install | ||||||
| 	install -m 0644 -D misc/fstab $(DESTDIR)/etc/fstab | 	install -m 0644 -D misc/fstab $(DESTDIR)/etc/fstab | ||||||
| 
 | 
 | ||||||
|  | 	# force /usr/bin before /bin to have /usr/bin/python instead of /bin/python | ||||||
|  | 	PATH="/usr/bin:$(PATH)" python setup.py install -O1 --root $(DESTDIR) | ||||||
|  | 	mkdir -p $(DESTDIR)$(SBINDIR) | ||||||
|  | 	mv $(DESTDIR)/usr/bin/qubes-firewall $(DESTDIR)$(SBINDIR)/qubes-firewall | ||||||
|  | 
 | ||||||
| 	install -D -m 0440 misc/qubes.sudoers $(DESTDIR)/etc/sudoers.d/qubes | 	install -D -m 0440 misc/qubes.sudoers $(DESTDIR)/etc/sudoers.d/qubes | ||||||
| 	install -D -m 0440 misc/sudoers.d_qt_x11_no_mitshm $(DESTDIR)/etc/sudoers.d/qt_x11_no_mitshm | 	install -D -m 0440 misc/sudoers.d_qt_x11_no_mitshm $(DESTDIR)/etc/sudoers.d/qt_x11_no_mitshm | ||||||
| 	install -D -m 0644 misc/20_tcp_timestamps.conf $(DESTDIR)/etc/sysctl.d/20_tcp_timestamps.conf | 	install -D -m 0644 misc/20_tcp_timestamps.conf $(DESTDIR)/etc/sysctl.d/20_tcp_timestamps.conf | ||||||
| @ -200,7 +205,6 @@ install-common: | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 	install -d $(DESTDIR)/$(SBINDIR) | 	install -d $(DESTDIR)/$(SBINDIR) | ||||||
| 	install network/qubes-firewall $(DESTDIR)/$(SBINDIR)/ |  | ||||||
| 	install network/qubes-netwatcher $(DESTDIR)/$(SBINDIR)/ | 	install network/qubes-netwatcher $(DESTDIR)/$(SBINDIR)/ | ||||||
| 
 | 
 | ||||||
| 	install -d $(DESTDIR)$(BINDIR) | 	install -d $(DESTDIR)$(BINDIR) | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								debian/control
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								debian/control
									
									
									
									
										vendored
									
									
								
							| @ -38,6 +38,7 @@ Depends: | |||||||
|     net-tools, |     net-tools, | ||||||
|     psmisc, |     psmisc, | ||||||
|     python2.7, |     python2.7, | ||||||
|  |     python-daemon, | ||||||
|     python-gi, |     python-gi, | ||||||
|     python-xdg, |     python-xdg, | ||||||
|     python-dbus, |     python-dbus, | ||||||
|  | |||||||
| @ -3,6 +3,8 @@ | |||||||
| :INPUT DROP [1:72] | :INPUT DROP [1:72] | ||||||
| :FORWARD DROP [0:0] | :FORWARD DROP [0:0] | ||||||
| :OUTPUT ACCEPT [0:0] | :OUTPUT ACCEPT [0:0] | ||||||
|  | :QBS-FORWARD - [0:0] | ||||||
| -A INPUT -i lo -j ACCEPT | -A INPUT -i lo -j ACCEPT | ||||||
|  | -A FORWARD -j QBS-FORWARD | ||||||
| COMMIT | COMMIT | ||||||
| # Completed on Tue Sep 25 16:00:20 2012 | # Completed on Tue Sep 25 16:00:20 2012 | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ COMMIT | |||||||
| :INPUT ACCEPT [168:11399] | :INPUT ACCEPT [168:11399] | ||||||
| :FORWARD ACCEPT [0:0] | :FORWARD ACCEPT [0:0] | ||||||
| :OUTPUT ACCEPT [128:12536] | :OUTPUT ACCEPT [128:12536] | ||||||
|  | :QBS-FORWARD - [0:0] | ||||||
| -A INPUT -i vif+ -p udp -m udp --dport 68 -j DROP | -A INPUT -i vif+ -p udp -m udp --dport 68 -j DROP | ||||||
| -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT | -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT | ||||||
| -A INPUT -i vif+ -p icmp -j ACCEPT | -A INPUT -i vif+ -p icmp -j ACCEPT | ||||||
| @ -24,6 +25,7 @@ COMMIT | |||||||
| -A INPUT -i vif+ -j REJECT --reject-with icmp-host-prohibited | -A INPUT -i vif+ -j REJECT --reject-with icmp-host-prohibited | ||||||
| -A INPUT -j DROP | -A INPUT -j DROP | ||||||
| -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT | -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT | ||||||
|  | -A FORWARD -j QBS-FORWARD | ||||||
| -A FORWARD -i vif+ -o vif+ -j DROP | -A FORWARD -i vif+ -o vif+ -j DROP | ||||||
| -A FORWARD -i vif+ -j ACCEPT | -A FORWARD -i vif+ -j ACCEPT | ||||||
| -A FORWARD -j DROP | -A FORWARD -j DROP | ||||||
|  | |||||||
| @ -1,58 +0,0 @@ | |||||||
| #!/bin/sh |  | ||||||
| set -e |  | ||||||
| 
 |  | ||||||
| PIDFILE=/var/run/qubes/qubes-firewall.pid |  | ||||||
| XENSTORE_IPTABLES=/qubes-iptables |  | ||||||
| XENSTORE_IPTABLES_HEADER=/qubes-iptables-header |  | ||||||
| XENSTORE_ERROR=/qubes-iptables-error |  | ||||||
| OLD_RULES="" |  | ||||||
| # PIDfile handling |  | ||||||
| [ -e "$PIDFILE" ] && kill -s 0 $(cat "$PIDFILE") 2>/dev/null && exit 0 |  | ||||||
| echo $$ >$PIDFILE |  | ||||||
| 
 |  | ||||||
| trap 'exit 0' TERM |  | ||||||
| 
 |  | ||||||
| FIRST_TIME=yes |  | ||||||
| 
 |  | ||||||
| while true; do |  | ||||||
| 
 |  | ||||||
| 	echo "1" > /proc/sys/net/ipv4/ip_forward |  | ||||||
| 
 |  | ||||||
| 	if [ "$FIRST_TIME" ]; then |  | ||||||
| 		FIRST_TIME= |  | ||||||
| 		TRIGGER=reload |  | ||||||
| 	else |  | ||||||
| 		# Wait for changes in qubesdb file |  | ||||||
| 		/usr/bin/qubesdb-watch $XENSTORE_IPTABLES |  | ||||||
| 		TRIGGER=$(/usr/bin/qubesdb-read $XENSTORE_IPTABLES) |  | ||||||
| 	fi |  | ||||||
| 
 |  | ||||||
| 	if ! [ "$TRIGGER" = "reload" ]; then continue ; fi |  | ||||||
| 
 |  | ||||||
| 	# Disable forwarding to prevent potential "leaks" that might |  | ||||||
| 	# be bypassing the firewall or some proxy service (e.g. tor) |  | ||||||
| 	# during the time when the rules are being (re)applied |  | ||||||
| 	echo "0" > /proc/sys/net/ipv4/ip_forward |  | ||||||
| 
 |  | ||||||
| 	RULES=$(qubesdb-read $XENSTORE_IPTABLES_HEADER) |  | ||||||
| 	IPTABLES_SAVE=$(iptables-save | sed '/^\*filter/,/^COMMIT/d') |  | ||||||
| 	OUT=$(printf '%s\n%s\n' "$RULES" "$IPTABLES_SAVE" | sed 's/\\n\|\\x0a/\n/g' | iptables-restore 2>&1 || true) |  | ||||||
| 
 |  | ||||||
| 	for i in $(qubesdb-list -f /qubes-iptables-domainrules) ; do |  | ||||||
| 		RULES=$(qubesdb-read "$i") |  | ||||||
| 		ERRS=$(printf '%s\n' "$RULES" | sed 's/\\n\|\\x0a/\n/g' | /sbin/iptables-restore -n 2>&1 || true) |  | ||||||
| 		if [ -n "$ERRS" ]; then |  | ||||||
| 			echo "Failed applying rules for $i: $ERRS" >&2 |  | ||||||
| 			OUT="$OUT$ERRS" |  | ||||||
| 		fi |  | ||||||
| 	done |  | ||||||
| 	qubesdb-write $XENSTORE_ERROR "$OUT" |  | ||||||
| 	if [ -n "$OUT" ]; then |  | ||||||
| 		DISPLAY=:0 /usr/bin/notify-send -t 3000 "Firewall loading error ($(hostname))" "$OUT" || : |  | ||||||
| 	fi |  | ||||||
| 
 |  | ||||||
| 	# Check if user didn't define some custom rules to be applied as well... |  | ||||||
| 	[ -x /rw/config/qubes-firewall-user-script ] && /rw/config/qubes-firewall-user-script |  | ||||||
| 	# XXX: Backward compatibility |  | ||||||
| 	[ -x /rw/config/qubes_firewall_user_script ] && /rw/config/qubes_firewall_user_script |  | ||||||
| done |  | ||||||
							
								
								
									
										0
									
								
								qubesagent/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								qubesagent/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										576
									
								
								qubesagent/firewall.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										576
									
								
								qubesagent/firewall.py
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,576 @@ | |||||||
|  | #!/usr/bin/python2 -O | ||||||
|  | # vim: fileencoding=utf-8 | ||||||
|  | 
 | ||||||
|  | # | ||||||
|  | # The Qubes OS Project, https://www.qubes-os.org/ | ||||||
|  | # | ||||||
|  | # Copyright (C) 2016 | ||||||
|  | #                   Marek Marczykowski-Górecki <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. | ||||||
|  | # | ||||||
|  | import logging | ||||||
|  | import os | ||||||
|  | import socket | ||||||
|  | import subprocess | ||||||
|  | from distutils import spawn | ||||||
|  | 
 | ||||||
|  | import daemon | ||||||
|  | 
 | ||||||
|  | import qubesdb | ||||||
|  | import sys | ||||||
|  | 
 | ||||||
|  | import signal | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class RuleParseError(Exception): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class RuleApplyError(Exception): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class FirewallWorker(object): | ||||||
|  |     def __init__(self): | ||||||
|  |         self.terminate_requested = False | ||||||
|  |         self.qdb = qubesdb.QubesDB() | ||||||
|  |         self.log = logging.getLogger('qubes.firewall') | ||||||
|  |         self.log.addHandler(logging.StreamHandler(sys.stderr)) | ||||||
|  | 
 | ||||||
|  |     def init(self): | ||||||
|  |         '''Create appropriate chains/tables''' | ||||||
|  |         raise NotImplementedError | ||||||
|  | 
 | ||||||
|  |     def cleanup(self): | ||||||
|  |         '''Remove tables/chains - reverse work done by init''' | ||||||
|  |         raise NotImplementedError | ||||||
|  | 
 | ||||||
|  |     def apply_rules(self, source_addr, rules): | ||||||
|  |         '''Apply rules in given source address''' | ||||||
|  |         raise NotImplementedError | ||||||
|  | 
 | ||||||
|  |     def read_rules(self, target): | ||||||
|  |         '''Read rules from QubesDB and return them as a list of dicts''' | ||||||
|  |         entries = self.qdb.multiread('/qubes-firewall/{}/'.format(target)) | ||||||
|  |         assert isinstance(entries, dict) | ||||||
|  |         # drop full path | ||||||
|  |         entries = dict(((k.split('/')[3], v) for k, v in entries.items())) | ||||||
|  |         if 'policy' not in entries: | ||||||
|  |             raise RuleParseError('No \'policy\' defined') | ||||||
|  |         policy = entries.pop('policy') | ||||||
|  |         rules = [] | ||||||
|  |         for ruleno, rule in sorted(entries.items()): | ||||||
|  |             if len(ruleno) != 4 or not ruleno.isdigit(): | ||||||
|  |                 raise RuleParseError( | ||||||
|  |                     'Unexpected non-rule found: {}={}'.format(ruleno, rule)) | ||||||
|  |             rule_dict = dict(elem.split('=') for elem in rule.split(' ')) | ||||||
|  |             if 'action' not in rule_dict: | ||||||
|  |                 raise RuleParseError('Rule \'{}\' lack action'.format(rule)) | ||||||
|  |             rules.append(rule_dict) | ||||||
|  |         rules.append({'action': policy}) | ||||||
|  |         return rules | ||||||
|  | 
 | ||||||
|  |     def list_targets(self): | ||||||
|  |         return set(t.split('/')[2] for t in self.qdb.list('/qubes-firewall/')) | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def is_ip6(addr): | ||||||
|  |         return addr.count(':') > 0 | ||||||
|  | 
 | ||||||
|  |     def log_error(self, msg): | ||||||
|  |         self.log.error(msg) | ||||||
|  |         subprocess.call( | ||||||
|  |             ['notify-send', '-t', '3000', msg], | ||||||
|  |             env=os.environ.copy().update({'DISPLAY': ':0'}) | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     def handle_addr(self, addr): | ||||||
|  |         try: | ||||||
|  |             rules = self.read_rules(addr) | ||||||
|  |             self.apply_rules(addr, rules) | ||||||
|  |         except RuleParseError as e: | ||||||
|  |             self.log_error( | ||||||
|  |                 'Failed to parse rules for {} ({}), blocking traffic'.format( | ||||||
|  |                     addr, str(e) | ||||||
|  |                 )) | ||||||
|  |             self.apply_rules(addr, [{'action': 'drop'}]) | ||||||
|  |         except RuleApplyError as e: | ||||||
|  |             self.log_error( | ||||||
|  |                 'Failed to apply rules for {} ({}), blocking traffic'.format( | ||||||
|  |                     addr, str(e)) | ||||||
|  |             ) | ||||||
|  |             # retry with fallback rules | ||||||
|  |             try: | ||||||
|  |                 self.apply_rules(addr, [{'action': 'drop'}]) | ||||||
|  |             except RuleApplyError: | ||||||
|  |                 self.log_error( | ||||||
|  |                     'Failed to block traffic for {}'.format(addr)) | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def dns_addresses(family=None): | ||||||
|  |         with open('/etc/resolv.conf') as resolv: | ||||||
|  |             for line in resolv.readlines(): | ||||||
|  |                 line = line.strip() | ||||||
|  |                 if line.startswith('nameserver'): | ||||||
|  |                     if line.count('.') == 3 and (family or 4) == 4: | ||||||
|  |                         yield line.split(' ')[1] | ||||||
|  |                     elif line.count(':') and (family or 6) == 6: | ||||||
|  |                         yield line.split(' ')[1] | ||||||
|  | 
 | ||||||
|  |     def main(self): | ||||||
|  |         self.terminate_requested = False | ||||||
|  |         self.init() | ||||||
|  |         # initial load | ||||||
|  |         for source_addr in self.list_targets(): | ||||||
|  |             self.handle_addr(source_addr) | ||||||
|  |         self.qdb.watch('/qubes-firewall/') | ||||||
|  |         try: | ||||||
|  |             for watch_path in iter(self.qdb.read_watch, None): | ||||||
|  |                 # ignore writing rules itself - wait for final write at | ||||||
|  |                 # source_addr level empty write (/qubes-firewall/SOURCE_ADDR) | ||||||
|  |                 if watch_path.count('/') > 2: | ||||||
|  |                     continue | ||||||
|  |                 source_addr = watch_path.split('/')[2] | ||||||
|  |                 self.handle_addr(source_addr) | ||||||
|  |         except OSError:  # EINTR | ||||||
|  |             # signal received, don't continue the loop | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  |         self.cleanup() | ||||||
|  | 
 | ||||||
|  |     def terminate(self): | ||||||
|  |         self.terminate_requested = True | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class IptablesWorker(FirewallWorker): | ||||||
|  |     supported_rule_opts = ['action', 'proto', 'dst4', 'dst6', 'dsthost', | ||||||
|  |         'dstports', 'specialtarget', 'icmptype'] | ||||||
|  | 
 | ||||||
|  |     def __init__(self): | ||||||
|  |         super(IptablesWorker, self).__init__() | ||||||
|  |         self.chains = { | ||||||
|  |             4: set(), | ||||||
|  |             6: set(), | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def chain_for_addr(addr): | ||||||
|  |         '''Generate iptables chain name for given source address address''' | ||||||
|  |         return 'qbs-' + addr.replace('.', '-').replace(':', '-') | ||||||
|  | 
 | ||||||
|  |     def run_ipt(self, family, args, **kwargs): | ||||||
|  |         # pylint: disable=no-self-use | ||||||
|  |         if family == 6: | ||||||
|  |             subprocess.check_call(['ip6tables'] + args, **kwargs) | ||||||
|  |         else: | ||||||
|  |             subprocess.check_call(['iptables'] + args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     def run_ipt_restore(self, family, args): | ||||||
|  |         # pylint: disable=no-self-use | ||||||
|  |         if family == 6: | ||||||
|  |             return subprocess.Popen(['ip6tables-restore'] + args, | ||||||
|  |                 stdin=subprocess.PIPE, | ||||||
|  |                 stdout=subprocess.PIPE, | ||||||
|  |                 stderr=subprocess.STDOUT) | ||||||
|  |         else: | ||||||
|  |             return subprocess.Popen(['iptables-restore'] + args, | ||||||
|  |                 stdin=subprocess.PIPE, | ||||||
|  |                 stdout=subprocess.PIPE, | ||||||
|  |                 stderr=subprocess.STDOUT) | ||||||
|  | 
 | ||||||
|  |     def create_chain(self, addr, chain, family): | ||||||
|  |         ''' | ||||||
|  |         Create iptables chain and hook traffic coming from `addr` to it. | ||||||
|  | 
 | ||||||
|  |         :param addr: source IP from which traffic should be handled by the | ||||||
|  |         chain | ||||||
|  |         :param chain: name of the chain to create | ||||||
|  |         :param family: address family (4 or 6) | ||||||
|  |         :return: None | ||||||
|  |         ''' | ||||||
|  | 
 | ||||||
|  |         self.run_ipt(family, ['-N', chain]) | ||||||
|  |         self.run_ipt(family, | ||||||
|  |             ['-A', 'QBS-FORWARD', '-s', addr, '-j', chain]) | ||||||
|  |         self.chains[family].add(chain) | ||||||
|  | 
 | ||||||
|  |     def prepare_rules(self, chain, rules, family): | ||||||
|  |         ''' | ||||||
|  |         Helper function to translate rules list into input for iptables-restore | ||||||
|  | 
 | ||||||
|  |         :param chain: name of the chain to put rules into | ||||||
|  |         :param rules: list of rules | ||||||
|  |         :param family: address family (4 or 6) | ||||||
|  |         :return: input for iptables-restore | ||||||
|  |         :rtype: str | ||||||
|  |         ''' | ||||||
|  | 
 | ||||||
|  |         iptables = "*filter\n" | ||||||
|  | 
 | ||||||
|  |         fullmask = '/128' if family == 6 else '/32' | ||||||
|  | 
 | ||||||
|  |         dns = list(addr + fullmask for addr in self.dns_addresses(family)) | ||||||
|  | 
 | ||||||
|  |         for rule in rules: | ||||||
|  |             unsupported_opts = set(rule.keys()).difference( | ||||||
|  |                 set(self.supported_rule_opts)) | ||||||
|  |             if unsupported_opts: | ||||||
|  |                 raise RuleParseError( | ||||||
|  |                     'Unsupported rule option(s): {!s}'.format(unsupported_opts)) | ||||||
|  |             if 'dst4' in rule and family == 6: | ||||||
|  |                 raise RuleParseError('IPv4 rule found for IPv6 address') | ||||||
|  |             if 'dst6' in rule and family == 4: | ||||||
|  |                 raise RuleParseError('dst6 rule found for IPv4 address') | ||||||
|  | 
 | ||||||
|  |             if 'proto' in rule: | ||||||
|  |                 protos = [rule['proto']] | ||||||
|  |             else: | ||||||
|  |                 protos = None | ||||||
|  | 
 | ||||||
|  |             if 'dst4' in rule: | ||||||
|  |                 dsthosts = [rule['dst4']] | ||||||
|  |             elif 'dst6' in rule: | ||||||
|  |                 dsthosts = [rule['dst6']] | ||||||
|  |             elif 'dsthost' in rule: | ||||||
|  |                 addrinfo = socket.getaddrinfo(rule['dsthost'], None, | ||||||
|  |                     (socket.AF_INET6 if family == 6 else socket.AF_INET)) | ||||||
|  |                 dsthosts = set(item[4][0] + fullmask for item in addrinfo) | ||||||
|  |             else: | ||||||
|  |                 dsthosts = None | ||||||
|  | 
 | ||||||
|  |             if 'dstports' in rule: | ||||||
|  |                 dstports = rule['dstports'].replace('-', ':') | ||||||
|  |             else: | ||||||
|  |                 dstports = None | ||||||
|  | 
 | ||||||
|  |             if rule.get('specialtarget', None) == 'dns': | ||||||
|  |                 if dstports not in ('53:53', None): | ||||||
|  |                     continue | ||||||
|  |                 else: | ||||||
|  |                     dstports = '53:53' | ||||||
|  |                 if not dns: | ||||||
|  |                     continue | ||||||
|  |                 if protos is not None: | ||||||
|  |                     protos = {'tcp', 'udp'}.intersection(protos) | ||||||
|  |                 else: | ||||||
|  |                     protos = {'tcp', 'udp'} | ||||||
|  | 
 | ||||||
|  |                 if dsthosts is not None: | ||||||
|  |                     dsthosts = set(dns).intersection(dsthosts) | ||||||
|  |                 else: | ||||||
|  |                     dsthosts = dns | ||||||
|  | 
 | ||||||
|  |             if 'icmptype' in rule: | ||||||
|  |                 icmptype = rule['icmptype'] | ||||||
|  |             else: | ||||||
|  |                 icmptype = None | ||||||
|  | 
 | ||||||
|  |             # make them iterable | ||||||
|  |             if protos is None: | ||||||
|  |                 protos = [None] | ||||||
|  |             if dsthosts is None: | ||||||
|  |                 dsthosts = [None] | ||||||
|  | 
 | ||||||
|  |             # sorting here is only to ease writing tests | ||||||
|  |             for proto in sorted(protos): | ||||||
|  |                 for dsthost in sorted(dsthosts): | ||||||
|  |                     ipt_rule = '-A {}'.format(chain) | ||||||
|  |                     if dsthost is not None: | ||||||
|  |                         ipt_rule += ' -d {}'.format(dsthost) | ||||||
|  |                     if proto is not None: | ||||||
|  |                         ipt_rule += ' -p {}'.format(proto) | ||||||
|  |                     if dstports is not None: | ||||||
|  |                         ipt_rule += ' --dport {}'.format(dstports) | ||||||
|  |                     if icmptype is not None: | ||||||
|  |                         ipt_rule += ' --icmp-type {}'.format(icmptype) | ||||||
|  |                     ipt_rule += ' -j {}\n'.format( | ||||||
|  |                         str(rule['action']).upper()) | ||||||
|  |                     iptables += ipt_rule | ||||||
|  | 
 | ||||||
|  |         iptables += 'COMMIT\n' | ||||||
|  |         return iptables | ||||||
|  | 
 | ||||||
|  |     def apply_rules_family(self, source, rules, family): | ||||||
|  |         ''' | ||||||
|  |         Apply rules for given source address. | ||||||
|  |         Handle only rules for given address family (IPv4 or IPv6). | ||||||
|  | 
 | ||||||
|  |         :param source: source address | ||||||
|  |         :param rules: rules list | ||||||
|  |         :param family: address family, either 4 or 6 | ||||||
|  |         :return: None | ||||||
|  |         ''' | ||||||
|  | 
 | ||||||
|  |         chain = self.chain_for_addr(source) | ||||||
|  |         if chain not in self.chains[family]: | ||||||
|  |             self.create_chain(source, chain, family) | ||||||
|  | 
 | ||||||
|  |         iptables = self.prepare_rules(chain, rules, family) | ||||||
|  |         try: | ||||||
|  |             self.run_ipt(family, ['-F', chain]) | ||||||
|  |             p = self.run_ipt_restore(family, ['-n']) | ||||||
|  |             (output, _) = p.communicate(iptables) | ||||||
|  |             if p.returncode != 0: | ||||||
|  |                 raise RuleApplyError( | ||||||
|  |                     'iptables-restore failed: {}'.format(output)) | ||||||
|  |         except subprocess.CalledProcessError as e: | ||||||
|  |             raise RuleApplyError('\'iptables -F {}\' failed: {}'.format( | ||||||
|  |                 chain, e.output)) | ||||||
|  | 
 | ||||||
|  |     def apply_rules(self, source, rules): | ||||||
|  |         if self.is_ip6(source): | ||||||
|  |             self.apply_rules_family(source, rules, 6) | ||||||
|  |         else: | ||||||
|  |             self.apply_rules_family(source, rules, 4) | ||||||
|  | 
 | ||||||
|  |     def init(self): | ||||||
|  |         # make sure 'QBS_FORWARD' chain exists - should be created before | ||||||
|  |         # starting qubes-firewall | ||||||
|  |         try: | ||||||
|  |             self.run_ipt(4, ['-nL', 'QBS-FORWARD']) | ||||||
|  |             self.run_ipt(6, ['-nL', 'QBS-FORWARD']) | ||||||
|  |         except subprocess.CalledProcessError: | ||||||
|  |             self.log_error('\'QBS-FORWARD\' chain not found, create it first') | ||||||
|  |             sys.exit(1) | ||||||
|  | 
 | ||||||
|  |     def cleanup(self): | ||||||
|  |         for family in (4, 6): | ||||||
|  |             self.run_ipt(family, ['-F', 'QBS-FORWARD']) | ||||||
|  |             for chain in self.chains[family]: | ||||||
|  |                 self.run_ipt(family, ['-F', chain]) | ||||||
|  |                 self.run_ipt(family, ['-X', chain]) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class NftablesWorker(FirewallWorker): | ||||||
|  |     supported_rule_opts = ['action', 'proto', 'dst4', 'dst6', 'dsthost', | ||||||
|  |         'dstports', 'specialtarget', 'icmptype'] | ||||||
|  | 
 | ||||||
|  |     def __init__(self): | ||||||
|  |         super(NftablesWorker, self).__init__() | ||||||
|  |         self.chains = { | ||||||
|  |             4: set(), | ||||||
|  |             6: set(), | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def chain_for_addr(addr): | ||||||
|  |         '''Generate iptables chain name for given source address address''' | ||||||
|  |         return 'qbs-' + addr.replace('.', '-').replace(':', '-') | ||||||
|  | 
 | ||||||
|  |     def run_nft(self, nft_input): | ||||||
|  |         # pylint: disable=no-self-use | ||||||
|  |         p = subprocess.Popen(['nft', '-f', '/dev/stdin'], | ||||||
|  |             stdin=subprocess.PIPE, | ||||||
|  |             stdout=subprocess.PIPE, | ||||||
|  |             stderr=subprocess.STDOUT) | ||||||
|  |         stdout, _ = p.communicate(nft_input) | ||||||
|  |         if p.returncode != 0: | ||||||
|  |             raise RuleApplyError('nft failed: {}'.format(stdout)) | ||||||
|  | 
 | ||||||
|  |     def create_chain(self, addr, chain, family): | ||||||
|  |         ''' | ||||||
|  |         Create iptables chain and hook traffic coming from `addr` to it. | ||||||
|  | 
 | ||||||
|  |         :param addr: source IP from which traffic should be handled by the | ||||||
|  |         chain | ||||||
|  |         :param chain: name of the chain to create | ||||||
|  |         :param family: address family (4 or 6) | ||||||
|  |         :return: None | ||||||
|  |         ''' | ||||||
|  |         nft_input = ( | ||||||
|  |             'table {family} {table} {{\n' | ||||||
|  |             '  chain {chain} {{\n' | ||||||
|  |             '  }}\n' | ||||||
|  |             '  chain forward {{\n' | ||||||
|  |             '    {family} saddr {ip} jump {chain}\n' | ||||||
|  |             '  }}\n' | ||||||
|  |             '}}\n'.format( | ||||||
|  |                 family=("ip6" if family == 6 else "ip"), | ||||||
|  |                 table='qubes-firewall', | ||||||
|  |                 chain=chain, | ||||||
|  |                 ip=addr, | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         self.run_nft(nft_input) | ||||||
|  |         self.chains[family].add(chain) | ||||||
|  | 
 | ||||||
|  |     def prepare_rules(self, chain, rules, family): | ||||||
|  |         ''' | ||||||
|  |         Helper function to translate rules list into input for iptables-restore | ||||||
|  | 
 | ||||||
|  |         :param chain: name of the chain to put rules into | ||||||
|  |         :param rules: list of rules | ||||||
|  |         :param family: address family (4 or 6) | ||||||
|  |         :return: input for iptables-restore | ||||||
|  |         :rtype: str | ||||||
|  |         ''' | ||||||
|  | 
 | ||||||
|  |         assert family in (4, 6) | ||||||
|  |         nft_rules = [] | ||||||
|  |         ip_match = 'ip6' if family == 6 else 'ip' | ||||||
|  | 
 | ||||||
|  |         fullmask = '/128' if family == 6 else '/32' | ||||||
|  | 
 | ||||||
|  |         dns = list(addr + fullmask for addr in self.dns_addresses(family)) | ||||||
|  | 
 | ||||||
|  |         for rule in rules: | ||||||
|  |             unsupported_opts = set(rule.keys()).difference( | ||||||
|  |                 set(self.supported_rule_opts)) | ||||||
|  |             if unsupported_opts: | ||||||
|  |                 raise RuleParseError( | ||||||
|  |                     'Unsupported rule option(s): {!s}'.format(unsupported_opts)) | ||||||
|  |             if 'dst4' in rule and family == 6: | ||||||
|  |                 raise RuleParseError('IPv4 rule found for IPv6 address') | ||||||
|  |             if 'dst6' in rule and family == 4: | ||||||
|  |                 raise RuleParseError('dst6 rule found for IPv4 address') | ||||||
|  | 
 | ||||||
|  |             nft_rule = "" | ||||||
|  | 
 | ||||||
|  |             if 'proto' in rule: | ||||||
|  |                 if family == 4: | ||||||
|  |                     nft_rule += ' ip protocol {}'.format(rule['proto']) | ||||||
|  |                 elif family == 6: | ||||||
|  |                     proto = 'icmpv6' if rule['proto'] == 'icmp' \ | ||||||
|  |                         else rule['proto'] | ||||||
|  |                     nft_rule += ' ip6 nexthdr {}'.format(proto) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |             if 'dst4' in rule: | ||||||
|  |                 nft_rule += ' ip daddr {}'.format(rule['dst4']) | ||||||
|  |             elif 'dst6' in rule: | ||||||
|  |                 nft_rule += ' ip6 daddr {}'.format(rule['dst6']) | ||||||
|  |             elif 'dsthost' in rule: | ||||||
|  |                 addrinfo = socket.getaddrinfo(rule['dsthost'], None, | ||||||
|  |                     (socket.AF_INET6 if family == 6 else socket.AF_INET)) | ||||||
|  |                 nft_rule += ' {} daddr {{ {} }}'.format(ip_match, | ||||||
|  |                     ', '.join(set(item[4][0] + fullmask for item in addrinfo))) | ||||||
|  | 
 | ||||||
|  |             if 'dstports' in rule: | ||||||
|  |                 dstports = rule['dstports'] | ||||||
|  |                 if len(set(dstports.split('-'))) == 1: | ||||||
|  |                     dstports = dstports.split('-')[0] | ||||||
|  |             else: | ||||||
|  |                 dstports = None | ||||||
|  | 
 | ||||||
|  |             if rule.get('specialtarget', None) == 'dns': | ||||||
|  |                 if dstports not in ('53', None): | ||||||
|  |                     continue | ||||||
|  |                 else: | ||||||
|  |                     dstports = '53' | ||||||
|  |                 if not dns: | ||||||
|  |                     continue | ||||||
|  |                 nft_rule += ' {} daddr {{ {} }}'.format(ip_match, ', '.join( | ||||||
|  |                     dns)) | ||||||
|  | 
 | ||||||
|  |             if 'icmptype' in rule: | ||||||
|  |                 if family == 4: | ||||||
|  |                     nft_rule += ' icmp type {}'.format(rule['icmptype']) | ||||||
|  |                 elif family == 6: | ||||||
|  |                     nft_rule += ' icmpv6 type {}'.format(rule['icmptype']) | ||||||
|  | 
 | ||||||
|  |             # now duplicate rules for tcp/udp if needed | ||||||
|  |             # it isn't possible to specify "tcp dport xx || udp dport xx" in | ||||||
|  |             # one rule | ||||||
|  |             if dstports is not None: | ||||||
|  |                 if 'proto' not in rule: | ||||||
|  |                     nft_rules.append( | ||||||
|  |                         nft_rule + ' tcp dport {} {}'.format( | ||||||
|  |                             dstports, rule['action'])) | ||||||
|  |                     nft_rules.append( | ||||||
|  |                         nft_rule + ' udp dport {} {}'.format( | ||||||
|  |                             dstports, rule['action'])) | ||||||
|  |                 else: | ||||||
|  |                     nft_rules.append( | ||||||
|  |                         nft_rule + ' {} dport {} {}'.format( | ||||||
|  |                             rule['proto'], dstports, rule['action'])) | ||||||
|  |             else: | ||||||
|  |                 nft_rules.append(nft_rule + ' ' + rule['action']) | ||||||
|  | 
 | ||||||
|  |         return ( | ||||||
|  |             'flush chain {family} {table} {chain}\n' | ||||||
|  |             'table {family} {table} {{\n' | ||||||
|  |             '  chain {chain} {{\n' | ||||||
|  |             '   {rules}\n' | ||||||
|  |             '  }}\n' | ||||||
|  |             '}}\n'.format( | ||||||
|  |                 family=('ip6' if family == 6 else 'ip'), | ||||||
|  |                 table='qubes-firewall', | ||||||
|  |                 chain=chain, | ||||||
|  |                 rules='\n   '.join(nft_rules) | ||||||
|  |             )) | ||||||
|  | 
 | ||||||
|  |     def apply_rules_family(self, source, rules, family): | ||||||
|  |         ''' | ||||||
|  |         Apply rules for given source address. | ||||||
|  |         Handle only rules for given address family (IPv4 or IPv6). | ||||||
|  | 
 | ||||||
|  |         :param source: source address | ||||||
|  |         :param rules: rules list | ||||||
|  |         :param family: address family, either 4 or 6 | ||||||
|  |         :return: None | ||||||
|  |         ''' | ||||||
|  | 
 | ||||||
|  |         chain = self.chain_for_addr(source) | ||||||
|  |         if chain not in self.chains[family]: | ||||||
|  |             self.create_chain(source, chain, family) | ||||||
|  | 
 | ||||||
|  |         self.run_nft(self.prepare_rules(chain, rules, family)) | ||||||
|  | 
 | ||||||
|  |     def apply_rules(self, source, rules): | ||||||
|  |         if self.is_ip6(source): | ||||||
|  |             self.apply_rules_family(source, rules, 6) | ||||||
|  |         else: | ||||||
|  |             self.apply_rules_family(source, rules, 4) | ||||||
|  | 
 | ||||||
|  |     def init(self): | ||||||
|  |         # make sure 'QBS_FORWARD' chain exists - should be created before | ||||||
|  |         # starting qubes-firewall | ||||||
|  |         nft_init = ( | ||||||
|  |             'table {family} qubes-firewall {{\n' | ||||||
|  |             '  chain forward {{\n' | ||||||
|  |             '    type filter hook forward priority 0;\n' | ||||||
|  |             '  }}\n' | ||||||
|  |             '}}\n' | ||||||
|  |         ) | ||||||
|  |         nft_init = ''.join( | ||||||
|  |             nft_init.format(family=family) for family in ('ip', 'ip6')) | ||||||
|  |         self.run_nft(nft_init) | ||||||
|  | 
 | ||||||
|  |     def cleanup(self): | ||||||
|  |         nft_cleanup = ( | ||||||
|  |             'delete table ip qubes-firewall\n' | ||||||
|  |             'delete table ip6 qubes-firewall\n' | ||||||
|  |         ) | ||||||
|  |         self.run_nft(nft_cleanup) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def main(): | ||||||
|  |     if spawn.find_executable('nft'): | ||||||
|  |         worker = NftablesWorker() | ||||||
|  |     else: | ||||||
|  |         worker = IptablesWorker() | ||||||
|  |     context = daemon.DaemonContext() | ||||||
|  |     context.stderr = sys.stderr | ||||||
|  |     context.detach_process = False | ||||||
|  |     context.files_preserve = [worker.qdb.watch_fd()] | ||||||
|  |     context.signal_map = { | ||||||
|  |         signal.SIGTERM: lambda _signal, _stack: worker.terminate(), | ||||||
|  |     } | ||||||
|  |     with context: | ||||||
|  |         worker.main() | ||||||
|  | 
 | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     main() | ||||||
							
								
								
									
										507
									
								
								qubesagent/test_firewall.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										507
									
								
								qubesagent/test_firewall.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,507 @@ | |||||||
|  | import logging | ||||||
|  | from unittest import TestCase | ||||||
|  | 
 | ||||||
|  | import qubesagent.firewall | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class DummyIptablesRestore(object): | ||||||
|  |     # pylint: disable=too-few-public-methods | ||||||
|  |     def __init__(self, worker_mock, family): | ||||||
|  |         self._worker_mock = worker_mock | ||||||
|  |         self._family = family | ||||||
|  |         self.returncode = 0 | ||||||
|  | 
 | ||||||
|  |     def communicate(self, stdin=None): | ||||||
|  |         self._worker_mock.loaded_iptables[self._family] = stdin | ||||||
|  |         return ("", None) | ||||||
|  | 
 | ||||||
|  | class DummyQubesDB(object): | ||||||
|  |     def __init__(self, worker_mock): | ||||||
|  |         self._worker_mock = worker_mock | ||||||
|  |         self.entries = {} | ||||||
|  |         self.pending_watches = [] | ||||||
|  | 
 | ||||||
|  |     def read(self, key): | ||||||
|  |         try: | ||||||
|  |             return self.entries[key] | ||||||
|  |         except KeyError: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|  |     def multiread(self, prefix): | ||||||
|  |         result = {} | ||||||
|  |         for key, value in self.entries.items(): | ||||||
|  |             if key.startswith(prefix): | ||||||
|  |                 result[key] = value | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     def list(self, prefix): | ||||||
|  |         result = [] | ||||||
|  |         for key in self.entries.keys(): | ||||||
|  |             if key.startswith(prefix): | ||||||
|  |                 result.append(key) | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     def watch(self, path): | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     def read_watch(self): | ||||||
|  |         try: | ||||||
|  |             return self.pending_watches.pop(0) | ||||||
|  |         except IndexError: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class FirewallWorker(qubesagent.firewall.FirewallWorker): | ||||||
|  |     def __init__(self): | ||||||
|  |         # pylint: disable=super-init-not-called | ||||||
|  |         # don't call super on purpose - avoid connecting to QubesDB | ||||||
|  |         # super(FirewallWorker, self).__init__() | ||||||
|  |         self.qdb = DummyQubesDB(self) | ||||||
|  |         self.log = logging.getLogger('qubes.tests') | ||||||
|  | 
 | ||||||
|  |         self.init_called = False | ||||||
|  |         self.cleanup_called = False | ||||||
|  |         self.rules = {} | ||||||
|  | 
 | ||||||
|  |     def apply_rules(self, source_addr, rules): | ||||||
|  |         self.rules[source_addr] = rules | ||||||
|  | 
 | ||||||
|  |     def cleanup(self): | ||||||
|  |         self.init_called = True | ||||||
|  | 
 | ||||||
|  |     def init(self): | ||||||
|  |         self.cleanup_called = True | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class IptablesWorker(qubesagent.firewall.IptablesWorker): | ||||||
|  |     '''Override methods actually modifying system state to only log what | ||||||
|  |     would be done''' | ||||||
|  | 
 | ||||||
|  |     def __init__(self): | ||||||
|  |         # pylint: disable=super-init-not-called | ||||||
|  |         # don't call super on purpose - avoid connecting to QubesDB | ||||||
|  |         # super(IptablesWorker, self).__init__() | ||||||
|  |         # copied __init__: | ||||||
|  |         self.qdb = DummyQubesDB(self) | ||||||
|  |         self.log = logging.getLogger('qubes.tests') | ||||||
|  |         self.chains = { | ||||||
|  |             4: set(), | ||||||
|  |             6: set(), | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         #: instead of really running `iptables`, log what would be called | ||||||
|  |         self.called_commands = { | ||||||
|  |             4: [], | ||||||
|  |             6: [], | ||||||
|  |         } | ||||||
|  |         #: rules that would be loaded with `iptables-restore` | ||||||
|  |         self.loaded_iptables = { | ||||||
|  |             4: None, | ||||||
|  |             6: None, | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     def run_ipt(self, family, args, **kwargs): | ||||||
|  |         self.called_commands[family].append(args) | ||||||
|  | 
 | ||||||
|  |     def run_ipt_restore(self, family, args): | ||||||
|  |         return DummyIptablesRestore(self, family) | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def dns_addresses(family=None): | ||||||
|  |         if family == 4: | ||||||
|  |             return ['1.1.1.1', '2.2.2.2'] | ||||||
|  |         else: | ||||||
|  |             return ['2001::1', '2001::2'] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class NftablesWorker(qubesagent.firewall.NftablesWorker): | ||||||
|  |     '''Override methods actually modifying system state to only log what | ||||||
|  |     would be done''' | ||||||
|  | 
 | ||||||
|  |     def __init__(self): | ||||||
|  |         # pylint: disable=super-init-not-called | ||||||
|  |         # don't call super on purpose - avoid connecting to QubesDB | ||||||
|  |         # super(IptablesWorker, self).__init__() | ||||||
|  |         # copied __init__: | ||||||
|  |         self.qdb = DummyQubesDB(self) | ||||||
|  |         self.log = logging.getLogger('qubes.tests') | ||||||
|  |         self.chains = { | ||||||
|  |             4: set(), | ||||||
|  |             6: set(), | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         #: instead of really running `nft`, log what would be loaded | ||||||
|  |         #: rules that would be loaded with `nft` | ||||||
|  |         self.loaded_rules = [] | ||||||
|  | 
 | ||||||
|  |     def run_nft(self, nft_input): | ||||||
|  |         self.loaded_rules.append(nft_input) | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def dns_addresses(family=None): | ||||||
|  |         if family == 4: | ||||||
|  |             return ['1.1.1.1', '2.2.2.2'] | ||||||
|  |         else: | ||||||
|  |             return ['2001::1', '2001::2'] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TestIptablesWorker(TestCase): | ||||||
|  |     def setUp(self): | ||||||
|  |         super(TestIptablesWorker, self).setUp() | ||||||
|  |         self.obj = IptablesWorker() | ||||||
|  | 
 | ||||||
|  |     def test_000_chain_for_addr(self): | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.obj.chain_for_addr('10.137.0.1'), 'qbs-10-137-0-1') | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.obj.chain_for_addr('fd09:24ef:4179:0000::3'), | ||||||
|  |             'qbs-fd09-24ef-4179-0000--3') | ||||||
|  | 
 | ||||||
|  |     def test_001_create_chain(self): | ||||||
|  |         testdata = [ | ||||||
|  |             (4, '10.137.0.1', 'qbs-10-137-0-1'), | ||||||
|  |             (6, 'fd09:24ef:4179:0000::3', 'qbs-fd09-24ef-4179-0000--3') | ||||||
|  |         ] | ||||||
|  |         for family, addr, chain in testdata: | ||||||
|  |             self.obj.create_chain(addr, chain, family) | ||||||
|  |             self.assertEqual(self.obj.called_commands[family], | ||||||
|  |                 [['-N', chain], | ||||||
|  |                     ['-A', 'QBS-FORWARD', '-s', addr, '-j', chain]]) | ||||||
|  | 
 | ||||||
|  |     def test_002_prepare_rules4(self): | ||||||
|  |         rules = [ | ||||||
|  |             {'action': 'accept', 'proto': 'tcp', | ||||||
|  |                 'dstports': '80-80', 'dst4': '1.2.3.0/24'}, | ||||||
|  |             {'action': 'accept', 'proto': 'udp', | ||||||
|  |                 'dstports': '443-1024', 'dsthost': 'yum.qubes-os.org'}, | ||||||
|  |             {'action': 'accept', 'specialtarget': 'dns'}, | ||||||
|  |             {'action': 'drop', 'proto': 'udp', 'specialtarget': 'dns'}, | ||||||
|  |             {'action': 'drop', 'proto': 'icmp'}, | ||||||
|  |             {'action': 'drop'}, | ||||||
|  |         ] | ||||||
|  |         expected_iptables = ( | ||||||
|  |             "*filter\n" | ||||||
|  |             "-A chain -d 1.2.3.0/24 -p tcp --dport 80:80 -j ACCEPT\n" | ||||||
|  |             "-A chain -d 82.94.215.165/32 -p udp --dport 443:1024 -j ACCEPT\n" | ||||||
|  |             "-A chain -d 1.1.1.1/32 -p tcp --dport 53:53 -j ACCEPT\n" | ||||||
|  |             "-A chain -d 2.2.2.2/32 -p tcp --dport 53:53 -j ACCEPT\n" | ||||||
|  |             "-A chain -d 1.1.1.1/32 -p udp --dport 53:53 -j ACCEPT\n" | ||||||
|  |             "-A chain -d 2.2.2.2/32 -p udp --dport 53:53 -j ACCEPT\n" | ||||||
|  |             "-A chain -d 1.1.1.1/32 -p udp --dport 53:53 -j DROP\n" | ||||||
|  |             "-A chain -d 2.2.2.2/32 -p udp --dport 53:53 -j DROP\n" | ||||||
|  |             "-A chain -p icmp -j DROP\n" | ||||||
|  |             "-A chain -j DROP\n" | ||||||
|  |             "COMMIT\n" | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(self.obj.prepare_rules('chain', rules, 4), | ||||||
|  |             expected_iptables) | ||||||
|  |         with self.assertRaises(qubesagent.firewall.RuleParseError): | ||||||
|  |             self.obj.prepare_rules('chain', [{'unknown': 'xxx'}], 4) | ||||||
|  |         with self.assertRaises(qubesagent.firewall.RuleParseError): | ||||||
|  |             self.obj.prepare_rules('chain', [{'dst6': 'a::b'}], 4) | ||||||
|  |         with self.assertRaises(qubesagent.firewall.RuleParseError): | ||||||
|  |             self.obj.prepare_rules('chain', [{'dst4': '3.3.3.3'}], 6) | ||||||
|  | 
 | ||||||
|  |     def test_003_prepare_rules6(self): | ||||||
|  |         rules = [ | ||||||
|  |             {'action': 'accept', 'proto': 'tcp', | ||||||
|  |                 'dstports': '80-80', 'dst6': 'a::b/128'}, | ||||||
|  |             {'action': 'accept', 'proto': 'tcp', | ||||||
|  |                 'dsthost': 'ripe.net'}, | ||||||
|  |             {'action': 'accept', 'specialtarget': 'dns'}, | ||||||
|  |             {'action': 'drop', 'proto': 'udp', 'specialtarget': 'dns'}, | ||||||
|  |             {'action': 'drop', 'proto': 'icmp'}, | ||||||
|  |             {'action': 'drop'}, | ||||||
|  |         ] | ||||||
|  |         expected_iptables = ( | ||||||
|  |             "*filter\n" | ||||||
|  |             "-A chain -d a::b/128 -p tcp --dport 80:80 -j ACCEPT\n" | ||||||
|  |             "-A chain -d 2001:67c:2e8:22::c100:68b/128 -p tcp -j ACCEPT\n" | ||||||
|  |             "-A chain -d 2001::1/128 -p tcp --dport 53:53 -j ACCEPT\n" | ||||||
|  |             "-A chain -d 2001::2/128 -p tcp --dport 53:53 -j ACCEPT\n" | ||||||
|  |             "-A chain -d 2001::1/128 -p udp --dport 53:53 -j ACCEPT\n" | ||||||
|  |             "-A chain -d 2001::2/128 -p udp --dport 53:53 -j ACCEPT\n" | ||||||
|  |             "-A chain -d 2001::1/128 -p udp --dport 53:53 -j DROP\n" | ||||||
|  |             "-A chain -d 2001::2/128 -p udp --dport 53:53 -j DROP\n" | ||||||
|  |             "-A chain -p icmp -j DROP\n" | ||||||
|  |             "-A chain -j DROP\n" | ||||||
|  |             "COMMIT\n" | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(self.obj.prepare_rules('chain', rules, 6), | ||||||
|  |             expected_iptables) | ||||||
|  | 
 | ||||||
|  |     def test_004_apply_rules4(self): | ||||||
|  |         rules = [{'action': 'accept'}] | ||||||
|  |         chain = 'qbs-10-137-0-1' | ||||||
|  |         self.obj.apply_rules('10.137.0.1', rules) | ||||||
|  |         self.assertEqual(self.obj.called_commands[4], | ||||||
|  |             [ | ||||||
|  |                 ['-N', chain], | ||||||
|  |                 ['-A', 'QBS-FORWARD', '-s', '10.137.0.1', '-j', chain], | ||||||
|  |                 ['-F', chain]]) | ||||||
|  |         self.assertEqual(self.obj.loaded_iptables[4], | ||||||
|  |             self.obj.prepare_rules(chain, rules, 4)) | ||||||
|  |         self.assertEqual(self.obj.called_commands[6], []) | ||||||
|  |         self.assertIsNone(self.obj.loaded_iptables[6]) | ||||||
|  | 
 | ||||||
|  |     def test_005_apply_rules6(self): | ||||||
|  |         rules = [{'action': 'accept'}] | ||||||
|  |         chain = 'qbs-2000--a' | ||||||
|  |         self.obj.apply_rules('2000::a', rules) | ||||||
|  |         self.assertEqual(self.obj.called_commands[6], | ||||||
|  |             [ | ||||||
|  |                 ['-N', chain], | ||||||
|  |                 ['-A', 'QBS-FORWARD', '-s', '2000::a', '-j', chain], | ||||||
|  |                 ['-F', chain]]) | ||||||
|  |         self.assertEqual(self.obj.loaded_iptables[6], | ||||||
|  |             self.obj.prepare_rules(chain, rules, 6)) | ||||||
|  |         self.assertEqual(self.obj.called_commands[4], []) | ||||||
|  |         self.assertIsNone(self.obj.loaded_iptables[4]) | ||||||
|  | 
 | ||||||
|  |     def test_006_init(self): | ||||||
|  |         self.obj.init() | ||||||
|  |         self.assertEqual(self.obj.called_commands[4], | ||||||
|  |             [['-nL', 'QBS-FORWARD']]) | ||||||
|  |         self.assertEqual(self.obj.called_commands[6], | ||||||
|  |             [['-nL', 'QBS-FORWARD']]) | ||||||
|  | 
 | ||||||
|  |     def test_007_cleanup(self): | ||||||
|  |         self.obj.init() | ||||||
|  |         self.obj.create_chain('1.2.3.4', 'chain-ip4-1', 4) | ||||||
|  |         self.obj.create_chain('1.2.3.6', 'chain-ip4-2', 4) | ||||||
|  |         self.obj.create_chain('2000::1', 'chain-ip6-1', 6) | ||||||
|  |         self.obj.create_chain('2000::2', 'chain-ip6-2', 6) | ||||||
|  |         # forget about commands called earlier | ||||||
|  |         self.obj.called_commands[4] = [] | ||||||
|  |         self.obj.called_commands[6] = [] | ||||||
|  |         self.obj.cleanup() | ||||||
|  |         self.assertEqual(self.obj.called_commands[4], | ||||||
|  |             [['-F', 'QBS-FORWARD'], | ||||||
|  |                 ['-F', 'chain-ip4-1'], | ||||||
|  |                 ['-X', 'chain-ip4-1'], | ||||||
|  |                 ['-F', 'chain-ip4-2'], | ||||||
|  |                 ['-X', 'chain-ip4-2']]) | ||||||
|  |         self.assertEqual(self.obj.called_commands[6], | ||||||
|  |             [['-F', 'QBS-FORWARD'], | ||||||
|  |                 ['-F', 'chain-ip6-2'], | ||||||
|  |                 ['-X', 'chain-ip6-2'], | ||||||
|  |                 ['-F', 'chain-ip6-1'], | ||||||
|  |                 ['-X', 'chain-ip6-1']]) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TestNftablesWorker(TestCase): | ||||||
|  |     def setUp(self): | ||||||
|  |         super(TestNftablesWorker, self).setUp() | ||||||
|  |         self.obj = NftablesWorker() | ||||||
|  | 
 | ||||||
|  |     def test_000_chain_for_addr(self): | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.obj.chain_for_addr('10.137.0.1'), 'qbs-10-137-0-1') | ||||||
|  |         self.assertEqual( | ||||||
|  |             self.obj.chain_for_addr('fd09:24ef:4179:0000::3'), | ||||||
|  |             'qbs-fd09-24ef-4179-0000--3') | ||||||
|  | 
 | ||||||
|  |     def expected_create_chain(self, family, addr, chain): | ||||||
|  |         return ( | ||||||
|  |             'table {family} qubes-firewall {{\n' | ||||||
|  |             '  chain {chain} {{\n' | ||||||
|  |             '  }}\n' | ||||||
|  |             '  chain forward {{\n' | ||||||
|  |             '    {family} saddr {addr} jump {chain}\n' | ||||||
|  |             '  }}\n' | ||||||
|  |             '}}\n'.format(family=family, addr=addr, chain=chain)) | ||||||
|  | 
 | ||||||
|  |     def test_001_create_chain(self): | ||||||
|  |         testdata = [ | ||||||
|  |             (4, '10.137.0.1', 'qbs-10-137-0-1'), | ||||||
|  |             (6, 'fd09:24ef:4179:0000::3', 'qbs-fd09-24ef-4179-0000--3') | ||||||
|  |         ] | ||||||
|  |         for family, addr, chain in testdata: | ||||||
|  |             self.obj.create_chain(addr, chain, family) | ||||||
|  |         self.assertEqual(self.obj.loaded_rules, | ||||||
|  |             [self.expected_create_chain('ip', '10.137.0.1', 'qbs-10-137-0-1'), | ||||||
|  |              self.expected_create_chain( | ||||||
|  |                  'ip6', 'fd09:24ef:4179:0000::3', 'qbs-fd09-24ef-4179-0000--3'), | ||||||
|  |              ]) | ||||||
|  | 
 | ||||||
|  |     def test_002_prepare_rules4(self): | ||||||
|  |         rules = [ | ||||||
|  |             {'action': 'accept', 'proto': 'tcp', | ||||||
|  |                 'dstports': '80-80', 'dst4': '1.2.3.0/24'}, | ||||||
|  |             {'action': 'accept', 'proto': 'udp', | ||||||
|  |                 'dstports': '443-1024', 'dsthost': 'yum.qubes-os.org'}, | ||||||
|  |             {'action': 'accept', 'specialtarget': 'dns'}, | ||||||
|  |             {'action': 'drop', 'proto': 'udp', 'specialtarget': 'dns'}, | ||||||
|  |             {'action': 'drop', 'proto': 'icmp'}, | ||||||
|  |             {'action': 'drop'}, | ||||||
|  |         ] | ||||||
|  |         expected_nft = ( | ||||||
|  |             'flush chain ip qubes-firewall chain\n' | ||||||
|  |             'table ip qubes-firewall {\n' | ||||||
|  |             '  chain chain {\n' | ||||||
|  |             '    ip protocol tcp ip daddr 1.2.3.0/24 tcp dport 80 accept\n' | ||||||
|  |             '    ip protocol udp ip daddr { 82.94.215.165/32 } ' | ||||||
|  |             'udp dport 443-1024 accept\n' | ||||||
|  |             '    ip daddr { 1.1.1.1/32, 2.2.2.2/32 } tcp dport 53 accept\n' | ||||||
|  |             '    ip daddr { 1.1.1.1/32, 2.2.2.2/32 } udp dport 53 accept\n' | ||||||
|  |             '    ip protocol udp ip daddr { 1.1.1.1/32, 2.2.2.2/32 } udp dport ' | ||||||
|  |             '53 drop\n' | ||||||
|  |             '    ip protocol icmp drop\n' | ||||||
|  |             '    drop\n' | ||||||
|  |             '  }\n' | ||||||
|  |             '}\n' | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(self.obj.prepare_rules('chain', rules, 4), | ||||||
|  |             expected_nft) | ||||||
|  |         with self.assertRaises(qubesagent.firewall.RuleParseError): | ||||||
|  |             self.obj.prepare_rules('chain', [{'unknown': 'xxx'}], 4) | ||||||
|  |         with self.assertRaises(qubesagent.firewall.RuleParseError): | ||||||
|  |             self.obj.prepare_rules('chain', [{'dst6': 'a::b'}], 4) | ||||||
|  |         with self.assertRaises(qubesagent.firewall.RuleParseError): | ||||||
|  |             self.obj.prepare_rules('chain', [{'dst4': '3.3.3.3'}], 6) | ||||||
|  | 
 | ||||||
|  |     def test_003_prepare_rules6(self): | ||||||
|  |         rules = [ | ||||||
|  |             {'action': 'accept', 'proto': 'tcp', | ||||||
|  |                 'dstports': '80-80', 'dst6': 'a::b/128'}, | ||||||
|  |             {'action': 'accept', 'proto': 'tcp', | ||||||
|  |                 'dsthost': 'ripe.net'}, | ||||||
|  |             {'action': 'accept', 'specialtarget': 'dns'}, | ||||||
|  |             {'action': 'drop', 'proto': 'udp', 'specialtarget': 'dns'}, | ||||||
|  |             {'action': 'drop', 'proto': 'icmp', 'icmptype': '128'}, | ||||||
|  |             {'action': 'drop'}, | ||||||
|  |         ] | ||||||
|  |         expected_nft = ( | ||||||
|  |             'flush chain ip6 qubes-firewall chain\n' | ||||||
|  |             'table ip6 qubes-firewall {\n' | ||||||
|  |             '  chain chain {\n' | ||||||
|  |             '    ip6 nexthdr tcp ip6 daddr a::b/128 tcp dport 80 accept\n' | ||||||
|  |             '    ip6 nexthdr tcp ip6 daddr { 2001:67c:2e8:22::c100:68b/128 } ' | ||||||
|  |             'accept\n' | ||||||
|  |             '    ip6 daddr { 2001::1/128, 2001::2/128 } tcp dport 53 accept\n' | ||||||
|  |             '    ip6 daddr { 2001::1/128, 2001::2/128 } udp dport 53 accept\n' | ||||||
|  |             '    ip6 nexthdr udp ip6 daddr { 2001::1/128, 2001::2/128 } ' | ||||||
|  |             'udp dport 53 drop\n' | ||||||
|  |             '    ip6 nexthdr icmpv6 icmpv6 type 128 drop\n' | ||||||
|  |             '    drop\n' | ||||||
|  |             '  }\n' | ||||||
|  |             '}\n' | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(self.obj.prepare_rules('chain', rules, 6), | ||||||
|  |             expected_nft) | ||||||
|  | 
 | ||||||
|  |     def test_004_apply_rules4(self): | ||||||
|  |         rules = [{'action': 'accept'}] | ||||||
|  |         chain = 'qbs-10-137-0-1' | ||||||
|  |         self.obj.apply_rules('10.137.0.1', rules) | ||||||
|  |         self.assertEqual(self.obj.loaded_rules, | ||||||
|  |             [self.expected_create_chain('ip', '10.137.0.1', chain), | ||||||
|  |              self.obj.prepare_rules(chain, rules, 4), | ||||||
|  |              ]) | ||||||
|  | 
 | ||||||
|  |     def test_005_apply_rules6(self): | ||||||
|  |         rules = [{'action': 'accept'}] | ||||||
|  |         chain = 'qbs-2000--a' | ||||||
|  |         self.obj.apply_rules('2000::a', rules) | ||||||
|  |         self.assertEqual(self.obj.loaded_rules, | ||||||
|  |             [self.expected_create_chain('ip6', '2000::a', chain), | ||||||
|  |              self.obj.prepare_rules(chain, rules, 6), | ||||||
|  |              ]) | ||||||
|  | 
 | ||||||
|  |     def test_006_init(self): | ||||||
|  |         self.obj.init() | ||||||
|  |         self.assertEqual(self.obj.loaded_rules, | ||||||
|  |         [ | ||||||
|  |             'table ip qubes-firewall {\n' | ||||||
|  |             '  chain forward {\n' | ||||||
|  |             '    type filter hook forward priority 0;\n' | ||||||
|  |             '  }\n' | ||||||
|  |             '}\n' | ||||||
|  |             'table ip6 qubes-firewall {\n' | ||||||
|  |             '  chain forward {\n' | ||||||
|  |             '    type filter hook forward priority 0;\n' | ||||||
|  |             '  }\n' | ||||||
|  |             '}\n' | ||||||
|  |         ]) | ||||||
|  | 
 | ||||||
|  |     def test_007_cleanup(self): | ||||||
|  |         self.obj.init() | ||||||
|  |         self.obj.create_chain('1.2.3.4', 'chain-ip4-1', 4) | ||||||
|  |         self.obj.create_chain('1.2.3.6', 'chain-ip4-2', 4) | ||||||
|  |         self.obj.create_chain('2000::1', 'chain-ip6-1', 6) | ||||||
|  |         self.obj.create_chain('2000::2', 'chain-ip6-2', 6) | ||||||
|  |         # forget about commands called earlier | ||||||
|  |         self.obj.loaded_rules = [] | ||||||
|  |         self.obj.cleanup() | ||||||
|  |         self.assertEqual(self.obj.loaded_rules, | ||||||
|  |             ['delete table ip qubes-firewall\n' | ||||||
|  |              'delete table ip6 qubes-firewall\n', | ||||||
|  |              ]) | ||||||
|  | 
 | ||||||
|  | class TestFirewallWorker(TestCase): | ||||||
|  |     def setUp(self): | ||||||
|  |         self.obj = FirewallWorker() | ||||||
|  |         rules = { | ||||||
|  |             '10.137.0.1': { | ||||||
|  |                 'policy': 'accept', | ||||||
|  |                 '0000': 'proto=tcp dstports=80-80 action=drop', | ||||||
|  |                 '0001': 'proto=udp specialtarget=dns action=accept', | ||||||
|  |                 '0002': 'proto=udp action=drop', | ||||||
|  |             }, | ||||||
|  |             '10.137.0.2': {'policy': 'accept'}, | ||||||
|  |             # no policy | ||||||
|  |             '10.137.0.3': {'0000': 'proto=tcp action=accept'}, | ||||||
|  |             # no action | ||||||
|  |             '10.137.0.4': { | ||||||
|  |                 'policy': 'drop', | ||||||
|  |                 '0000': 'proto=tcp' | ||||||
|  |             }, | ||||||
|  |         } | ||||||
|  |         for addr, entries in rules.items(): | ||||||
|  |             for key, value in entries.items(): | ||||||
|  |                 self.obj.qdb.entries[ | ||||||
|  |                     '/qubes-firewall/{}/{}'.format(addr, key)] = value | ||||||
|  | 
 | ||||||
|  |     def test_read_rules(self): | ||||||
|  |         expected_rules1 = [ | ||||||
|  |             {'proto': 'tcp', 'dstports': '80-80', 'action': 'drop'}, | ||||||
|  |             {'proto': 'udp', 'specialtarget': 'dns', 'action': 'accept'}, | ||||||
|  |             {'proto': 'udp', 'action': 'drop'}, | ||||||
|  |             {'action': 'accept'}, | ||||||
|  |         ] | ||||||
|  |         expected_rules2 = [ | ||||||
|  |             {'action': 'accept'}, | ||||||
|  |         ] | ||||||
|  |         self.assertEqual(self.obj.read_rules('10.137.0.1'), expected_rules1) | ||||||
|  |         self.assertEqual(self.obj.read_rules('10.137.0.2'), expected_rules2) | ||||||
|  |         with self.assertRaises(qubesagent.firewall.RuleParseError): | ||||||
|  |             self.obj.read_rules('10.137.0.3') | ||||||
|  |         with self.assertRaises(qubesagent.firewall.RuleParseError): | ||||||
|  |             self.obj.read_rules('10.137.0.4') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     def test_list_targets(self): | ||||||
|  |         self.assertEqual(self.obj.list_targets(), set(['10.137.0.{}'.format(x) | ||||||
|  |             for x in range(1, 5)])) | ||||||
|  | 
 | ||||||
|  |     def test_is_ip6(self): | ||||||
|  |         self.assertTrue(self.obj.is_ip6('2000::abcd')) | ||||||
|  |         self.assertTrue(self.obj.is_ip6('2000:1:2:3:4:5:6:abcd')) | ||||||
|  |         self.assertFalse(self.obj.is_ip6('10.137.0.1')) | ||||||
|  | 
 | ||||||
|  |     def test_handle_addr(self): | ||||||
|  |         self.obj.handle_addr('10.137.0.2') | ||||||
|  |         self.assertEqual(self.obj.rules['10.137.0.2'], [{'action': 'accept'}]) | ||||||
|  |         # fallback to block all | ||||||
|  |         self.obj.handle_addr('10.137.0.3') | ||||||
|  |         self.assertEqual(self.obj.rules['10.137.0.3'], [{'action': 'drop'}]) | ||||||
|  |         self.obj.handle_addr('10.137.0.4') | ||||||
|  |         self.assertEqual(self.obj.rules['10.137.0.4'], [{'action': 'drop'}]) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     def test_main(self): | ||||||
|  |         self.obj.main() | ||||||
|  |         self.assertTrue(self.obj.init_called) | ||||||
|  |         self.assertTrue(self.obj.cleanup_called) | ||||||
|  |         self.assertEqual(set(self.obj.rules.keys()), self.obj.list_targets()) | ||||||
|  |         # rules content were already tested | ||||||
| @ -54,6 +54,8 @@ Requires:   pygobject3-base | |||||||
| Requires:   dbus-python | Requires:   dbus-python | ||||||
| # for qubes-session-autostart, xdg-icon | # for qubes-session-autostart, xdg-icon | ||||||
| Requires:   pyxdg | Requires:   pyxdg | ||||||
|  | Requires:   python-daemon | ||||||
|  | Requires:   nftables | ||||||
| %if %{fedora} >= 20 | %if %{fedora} >= 20 | ||||||
| # gpk-update-viewer required by qubes-manager | # gpk-update-viewer required by qubes-manager | ||||||
| Requires:   gnome-packagekit-updater | Requires:   gnome-packagekit-updater | ||||||
| @ -437,6 +439,13 @@ rm -f %{name}-%{version} | |||||||
| /usr/share/nautilus-python/extensions/qvm_move_nautilus.py* | /usr/share/nautilus-python/extensions/qvm_move_nautilus.py* | ||||||
| /usr/share/nautilus-python/extensions/qvm_dvm_nautilus.py* | /usr/share/nautilus-python/extensions/qvm_dvm_nautilus.py* | ||||||
| 
 | 
 | ||||||
|  | %dir %{python_sitelib}/qubesagent-*-py2.7.egg-info | ||||||
|  | %{python_sitelib}/qubesagent-*-py2.7.egg-info/* | ||||||
|  | %dir %{python_sitelib}/qubesagent | ||||||
|  | %{python_sitelib}/qubesagent/__init__.py* | ||||||
|  | %{python_sitelib}/qubesagent/firewall.py* | ||||||
|  | %{python_sitelib}/qubesagent/test_firewall.py* | ||||||
|  | 
 | ||||||
| /usr/share/qubes/mime-override/globs | /usr/share/qubes/mime-override/globs | ||||||
| /usr/share/qubes/qubes-master-key.asc | /usr/share/qubes/qubes-master-key.asc | ||||||
| %dir /home_volatile | %dir /home_volatile | ||||||
|  | |||||||
							
								
								
									
										22
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | |||||||
|  | # vim: fileencoding=utf-8 | ||||||
|  | 
 | ||||||
|  | import setuptools | ||||||
|  | 
 | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     setuptools.setup( | ||||||
|  |         name='qubesagent', | ||||||
|  |         version=open('version').read().strip(), | ||||||
|  |         author='Invisible Things Lab', | ||||||
|  |         author_email='marmarek@invisiblethingslab.com', | ||||||
|  |         description='Qubes core-agent-linux package', | ||||||
|  |         license='GPL2+', | ||||||
|  |         url='https://www.qubes-os.org/', | ||||||
|  | 
 | ||||||
|  |         packages=('qubesagent',), | ||||||
|  | 
 | ||||||
|  |         entry_points={ | ||||||
|  |             'console_scripts': [ | ||||||
|  |                 'qubes-firewall = qubesagent.firewall:main' | ||||||
|  |             ], | ||||||
|  |         } | ||||||
|  |     ) | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Marek Marczykowski-Górecki
						Marek Marczykowski-Górecki