commit 7ad6b816701cc47c5c236036065de96434f5f11c Author: Manuel Amador (Rudd-O) Date: Tue Oct 11 19:06:09 2016 +0000 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d02285 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.pyc +*~ +*.tar.gz +*.rpm diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3b2d178 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +BINDIR=/usr/bin +LIBDIR=/usr/lib64 +DESTDIR= + +clean: + find -name '*.pyc' -o -name '*~' -print0 | xargs -0 rm -f + rm -f *.tar.gz *.rpm + +dist: clean + DIR=qubes-network-server-`awk '/^Version:/ {print $$2}' qubes-network-server.spec` && FILENAME=$$DIR.tar.gz && tar cvzf "$$FILENAME" --exclude "$$FILENAME" --exclude .git --exclude .gitignore -X .gitignore --transform="s|^|$$DIR/|" --show-transformed * + +rpm: dist + T=`mktemp -d` && rpmbuild --define "_topdir $$T" -ta qubes-network-server-`awk '/^Version:/ {print $$2}' qubes-network-server.spec`.tar.gz || { rm -rf "$$T"; exit 1; } && mv "$$T"/RPMS/*/* "$$T"/SRPMS/* . || { rm -rf "$$T"; exit 1; } && rm -rf "$$T" + +srpm: dist + T=`mktemp -d` && rpmbuild --define "_topdir $$T" -ts qubes-network-server-`awk '/^Version:/ {print $$2}' qubes-network-server.spec`.tar.gz || { rm -rf "$$T"; exit 1; } && mv "$$T"/SRPMS/* . || { rm -rf "$$T"; exit 1; } && rm -rf "$$T" + +install: + install -Dm 755 src/usr/bin/qvm-static-ip -t $(DESTDIR)/$(BINDIR)/ + install -Dm 644 src/usr/lib64/python2.7/site-packages/qubes/modules/*.py -t $(DESTDIR)/$(LIBDIR)/python2.7/site-packages/qubes/modules diff --git a/README.md b/README.md new file mode 100644 index 0000000..3cc7c68 --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +#Qubes network server + +This software lets you turn your Qubes OS machine into a network server. + +##Enhanced networking model + +The traditional Qubes OS networking model contemplates a client-only +use case. User VMs (AppVMs or StandaloneVMs) are attached to ProxyVMs, +which give the user control over outbound connections taking place from +user VMs. ProxyVMs in turn attach to NetVMs, which provide outbound +connectivity for ProxyVMs and other user VMs alike. + +![Standard Qubes OS network model](doc/Standard Qubes OS network model.png?raw=true "Standard Qubes OS network model") + +No provision is made for running a server in a virtualized environment, +such that the server's ports are accessible by (a) other VMs (b) machines +beyond the perimeter of the NetVM. To the extent that such a thing is +possible, it is only possible by painstakingly maintaining firewall rules +for multiple VMs, which need to carefully override the existing firewall +rules, and require careful thought not to open the system to unexpected +attack vectors. The Qubes OS user interface provides no help either. + +Qubes network server changes all that. + +![Qubes network server model](doc/Qubes network server model.png?raw=true "Qubes network server model") + +With the Qubes network server software, it becomes possible to make +network servers in user VMs available to other machines, be them +peer VMs in the same Qubes OS system or machines connected to +a physical link shared by a NetVM. You get actual, full, GUI control +over network traffic, both exiting the VM and entering the VM, with +exactly the same Qubes OS user experience you are used to. + +This is all, of course, opt-in, so the standard Qubes OS network security +model remains in effect until you decide to share network servers. + +##Usage + +Once installed (see below), usage of the software is straightforward. + +To illustrate, we'll proceed with an example VM `httpserver` which +is meant to be a standalone VM that contains files, being served by +a running HTTP server (port 80) within it. This VM is attached to +a ProxyVM `server-proxy`, which in turn is connected to a NetVM +`sys-net`, with IP address `192.168.1.4` on a local network +`192.168.1.0/24`. Our goal will be to make `httpserver` accessible +to your laptop on the same physical network, which we'll assume has +IP address `192.168.1.8`. + +###Assign a static address to `httpserver` + +First step is to assign an address — let's make it `192.168.1.6` — +to `httpserver`: + +``` +qvm-static-ip -s httpserver static_ip 192.168.1.6 +``` + +###Restart `httpserver` + +Due to limitations in this release of the code, you must power off +the `httpserver` VM and then power it back on. + +###Set firewall rules on `httpserver` + +Launch the Qubes Manager preferences window for the `httpserver` VM. +Go to the *Firewall rules* tab and select *Deny network access +except...* from the top area. *Allow ICMP traffic* but deny +*DNS queries*. + +Finally, add a new network rule (use the plus button). On the +*Address* box, you're going to write `from-192.168.1.8`. Select +the *TCP* protocol, and type `80` on the *Service* box. Click OK. + +Note the trick here — any address whose text begins with +`from-` gets transformed into an incoming traffic rule, as opposed +to the standard rules that control only outbound traffic. + +Back on the main dialog, click *OK*. + +###That's it! + +You'll be able to ping, from your laptop, the address `192.168.1.6`. +You will also be able to point your browser at it, and it will +render the served pages from the HTTP server running directly on +`httpserver`. + +Save from ICMP, no other port or protocol will be allowed for +inbound connections. + +You'll also note that `httpserver` has received no permission to +engage in any sort of outbound network traffic. + +##Disabling network server + +Two-step process. Step one: + +``` +qvm-static-ip -s httpserver static_ip none +``` + +Step two: power the VM off, then start it back up. + +##Installation + +Installation is extremely easy: + +* Prepare an RPM with the `make rpm` command on the local + directory of your clone. +* Copy the prepared RPM to the dom0 of your Qubes OS + machine. +* Install the RPM with `rpm -ivh`. + +Qubes OS does not provide any facility to copy files from +a VM to the dom0. To work around this, you can use `qvm-run`: + +``` +qvm-run --pass-io vmwiththerpm 'cat /home/user/path/to/qubes-network-server*rpm' > qns.rpm +``` + +This lets you fetch the RPM file to the dom0, and save it as `qns.rpm`. diff --git a/doc/Qubes network server model.dia b/doc/Qubes network server model.dia new file mode 100644 index 0000000..973b472 Binary files /dev/null and b/doc/Qubes network server model.dia differ diff --git a/doc/Qubes network server model.png b/doc/Qubes network server model.png new file mode 100644 index 0000000..e45e6f8 Binary files /dev/null and b/doc/Qubes network server model.png differ diff --git a/doc/Standard Qubes OS network model.dia b/doc/Standard Qubes OS network model.dia new file mode 100644 index 0000000..08e9639 Binary files /dev/null and b/doc/Standard Qubes OS network model.dia differ diff --git a/doc/Standard Qubes OS network model.png b/doc/Standard Qubes OS network model.png new file mode 100644 index 0000000..1f041f2 Binary files /dev/null and b/doc/Standard Qubes OS network model.png differ diff --git a/qubes-network-server.spec b/qubes-network-server.spec new file mode 100644 index 0000000..6d4b2b8 --- /dev/null +++ b/qubes-network-server.spec @@ -0,0 +1,36 @@ +%define debug_package %{nil} + +Name: qubes-network-server +Version: 0.0.1 +Release: 1%{?dist} +Summary: Turn your Qubes OS into a network server + +License: GPLv3+ +URL: https://github.com/Rudd-O/qubes-network-server +Source0: Source0: https://github.com/Rudd-O/%{name}/archive/{%version}.tar.gz#/%{name}-%{version}.tar.gz + +BuildRequires: go + +%description +This package lets you turn your Qubes OS into a network server. + +%prep +%setup -q + +%build +# variables must be kept in sync with install +make DESTDIR=$RPM_BUILD_ROOT BINDIR=%{_bindir} LIBDIR=%{_libdir} + +%install +rm -rf $RPM_BUILD_ROOT +# variables must be kept in sync with build +make install DESTDIR=$RPM_BUILD_ROOT BINDIR=%{_bindir} LIBDIR=%{_libdir} + +%files +%attr(0755, root, root) %{_bindir}/qvm-static-ip +%attr(0644, root, root) %{_libdir}/python2.7/site-packages/qubes/modules/*.py* +%doc README.md + +%changelog +* Tue Oct 11 2016 Manuel Amador (Rudd-O) +- Initial release diff --git a/src/usr/bin/qvm-static-ip b/src/usr/bin/qvm-static-ip new file mode 100644 index 0000000..df00de1 --- /dev/null +++ b/src/usr/bin/qvm-static-ip @@ -0,0 +1,170 @@ +#!/usr/bin/python2 +# -*- encoding: utf8 -*- +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2010 Joanna Rutkowska +# +# 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 qubes.qubes import QubesVmLabels +from qubes.qubes import QubesHost +from qubes.qubes import system_path +from optparse import OptionParser +import subprocess +import os +import sys +import re +from qubes.qubes import vmm + + +def do_list(vm): + label_width = 19 + fmt="{{0:<{0}}}: {{1}}".format(label_width) + + print fmt.format ("name", vm.name) + if hasattr(vm, 'static_ip'): + print fmt.format("static_ip", str(vm.static_ip) if vm.static_ip else "unset") + + +def do_get(vms, vm, prop): + if not hasattr(vm, prop): + print >>sys.stderr, "VM '{}' has no attribute '{}'".format(vm.name, + prop) + return + if getattr(vm, prop, None) is None: + # not set or set to None + return + else: + print str(getattr(vm, prop)) + + +def set_static_ip(vms, vm, args): + if len (args) != 1: + print >> sys.stderr, "Missing value ('static_ip')!" + return False + + arg = args[0] + if not arg or arg == "none" or arg == "None" or arg == "unset": + arg = None + # TODO(ruddo): validate the argument! + + setattr(vm, "static_ip", arg) + return True + +properties = { + "static_ip": set_static_ip, +} + + +def do_set(vms, vm, property, args): + if property not in properties.keys(): + print >> sys.stderr, "ERROR: Wrong property name: '{0}'".format(property) + return False + + if not hasattr(vm, property): + print >> sys.stderr, "ERROR: Property '{0}' not available for this VM".format(property) + return False + + try: + return properties[property](vms, vm, args) + except Exception as err: + print >> sys.stderr, "ERROR: %s" % str(err) + return False + + +def main(): + usage = "usage: %prog -l [options] \n"\ + "usage: %prog -g [options] \n"\ + "usage: %prog -s [options] [...]\n"\ + "List/set networking-related per-VM properties." + + parser = OptionParser (usage) + parser.add_option("-l", "--list", action="store_true", dest="do_list", + default=False) + parser.add_option("-s", "--set", action="store_true", dest="do_set", + default=False) + parser.add_option ("-g", "--get", action="store_true", dest="do_get", + default=False) + parser.add_option("--force-root", action="store_true", dest="force_root", + default=False, + help="Force to run, even with root privileges") + parser.add_option ("--offline-mode", dest="offline_mode", + action="store_true", default=False, + help="Offline mode") + + (options, args) = parser.parse_args () + if (len (args) < 1): + parser.error ("You must provide at least the vmname!") + + vmname = args[0] + + if hasattr(os, "geteuid") and os.geteuid() == 0: + if not options.force_root: + print >> sys.stderr, "*** Running this tool as root is strongly discouraged, this will lead you in permissions problems." + print >> sys.stderr, "Retry as unprivileged user." + print >> sys.stderr, "... or use --force-root to continue anyway." + exit(1) + + if options.do_list + options.do_set + options.do_get > 1: + print >> sys.stderr, "You can provide at most one of -l, -g and -s at " \ + "the same time!" + exit(1) + + if options.offline_mode: + vmm.offline_mode = True + + if options.do_set: + qvm_collection = QubesVmCollection() + qvm_collection.lock_db_for_writing() + qvm_collection.load() + else: + qvm_collection = QubesVmCollection() + qvm_collection.lock_db_for_reading() + qvm_collection.load() + qvm_collection.unlock_db() + + vm = qvm_collection.get_vm_by_name(vmname) + if vm is None or vm.qid not in qvm_collection: + print >> sys.stderr, "A VM with the name '{0}' does not exist in the system.".format(vmname) + exit(1) + + if options.do_set: + if len (args) < 2: + print >> sys.stderr, "You must specify the property you wish to set..." + print >> sys.stderr, "Available properties:" + for p in properties.keys(): + if hasattr(vm, p): + print >> sys.stderr, "--> '{0}'".format(p) + exit (1) + + property = args[1] + if do_set(qvm_collection, vm, property, args[2:]): + qvm_collection.save() + qvm_collection.unlock_db() + else: + qvm_collection.unlock_db() + exit(1) + + elif options.do_get or len(args) == 2: + do_get(qvm_collection, vm, args[1]) + else: + # do_list + do_list(vm) + +main() diff --git a/src/usr/lib64/python2.7/site-packages/qubes/modules/001FortressQubesVm.py b/src/usr/lib64/python2.7/site-packages/qubes/modules/001FortressQubesVm.py new file mode 100644 index 0000000..1562e22 --- /dev/null +++ b/src/usr/lib64/python2.7/site-packages/qubes/modules/001FortressQubesVm.py @@ -0,0 +1,401 @@ +#!/usr/bin/python2 +# -*- coding: utf-8 -*- +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2010 Joanna Rutkowska +# Copyright (C) 2013 Marek Marczykowski +# +# 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 datetime +import base64 +import hashlib +import fcntl +import logging +import lxml.etree +import os +import pipes +import re +import shutil +import subprocess +import sys +import textwrap +import time +import uuid +import xml.parsers.expat +import signal +from qubes import qmemman +from qubes import qmemman_algo +import libvirt + +from qubes.qubes import QubesException +from qubes.qubes import QubesVm as OriginalQubesVm +from qubes.qubes import register_qubes_vm_class +from qubes.qubes import dry_run + + +fw_encap = textwrap.dedent(""" + mkdir -p /run/fortress/firewall + f=$(mktemp --tmpdir=/run/fortress/firewall) + cat > "$f" + chmod +x "$f" + bash -e "$f" + ret=$? + rm -f "$f" + exit $ret +""") + + +def locked(programtext): + if not programtext.strip(): + return programtext + return "(\nflock 200\n" + programtext + "\n) 200>/var/run/xen-hotplug/vif-lock\n" + + +def logger(programtext): + if not programtext.strip(): + return programtext + return "exec 1> >(logger -s -t fortress) 2>&1\n" + programtext + + +class QubesVm(OriginalQubesVm): + + def get_attrs_config(self): + attrs = OriginalQubesVm.get_attrs_config(self) + attrs["static_ip"] = { + "attr": "static_ip", + "default": None, + "order": 70, + "save": lambda: str(getattr(self, "static_ip")) if getattr(self, "static_ip") is not None else 'none' + } + return attrs + + @property + def ip(self): + if self.netvm is not None: + if getattr(self, "static_ip") is not None: + return getattr(self, "static_ip") + return self.netvm.get_ip_for_vm(self.qid) + else: + return None + + @property + def netmask(self): + if self.netvm is not None: + if getattr(self, "static_ip") is not None: + # Netmasks for VMs that have a static IP are always host-only. + return "255.255.255.255" + return self.netvm.netmask + else: + return None + + def start(self, verbose = False, preparing_dvm = False, start_guid = True, + notify_function = None, mem_required = None): + if dry_run: + return + xid = OriginalQubesVm.start(self, verbose, preparing_dvm, start_guid, notify_function, mem_required) + if not preparing_dvm: + self.adjust_proxy_arp(verbose=verbose, notify_function=notify_function) + self.adjust_own_firewall_rules() + return xid + + def unpause(self): + self.log.debug('unpause()') + if dry_run: + return + + if not self.is_paused(): + raise QubesException ("VM not paused!") + + self.libvirt_domain.resume() + self.adjust_proxy_arp() + self.adjust_own_firewall_rules() + + def attach_network(self, verbose = False, wait = True, netvm = None): + self.log.debug('attach_network(netvm={!r})'.format(netvm)) + if dry_run: + return + ret = OriginalQubesVm.attach_network(self, verbose, wait, netvm) + self.adjust_proxy_arp(verbose) + return ret + + def adjust_proxy_arp(self, verbose = False, notify_function=None): + + def collect_downstream_vms(vm, vif): + if not hasattr(vm, "connected_vms"): + return list() + vms_below_me = list(vm.connected_vms.values()) + vms_below_me = [(vm, vif if vif else vm.vif) for vm in vms_below_me] + for v, vif in vms_below_me: + vms_below_me.extend(collect_downstream_vms(v, vif)) + return vms_below_me + + def addroute(ip, dev, netmask): + # This function adds routes and proxy ARP entries for the IP pointed at the + # device that the VM (IP) is behind. + dev = dev.replace("+", "0") + return "\n".join([ + "if ! ip route | grep -qF %s\\ dev\\ %s ; then" % (pipes.quote(ip), pipes.quote(dev)), + "ip route replace %s/%s dev %s metric 20001" % (pipes.quote(ip), pipes.quote(netmask), pipes.quote(dev)), + "fi", + "echo 1 > /proc/sys/net/ipv4/conf/%s/forwarding" % (pipes.quote(dev),), + "echo 1 > /proc/sys/net/ipv4/conf/%s/proxy_arp" % (pipes.quote(dev),), + "for dev in `ip link | awk -F ':' '/^[0-9]+: (eth|en|wl)/ { print $2 }'`", + "do", + " ip neigh add proxy %s dev $dev" % (pipes.quote(ip),), + "done", + ]) + + class addfwrule(object): + rules = None + addrule = textwrap.dedent(""" + declare -A savedrules + addrule() { + local table="$1" + local chain="$2" + local rule="$3" + local before="$4" + + if [ "${savedrules[$table]}" == "" ] ; then + savedrules["$table"]=$(iptables-save -t "$table") + fi + + if echo "${savedrules[$table]}" | grep -q :"${chain}" ; then + true + else + savedrules["$table"]=$( + echo "${savedrules[$table]}" | while read x + do + echo "$x" + if [ "$x" == '*'"$table" ] + then + echo "${table}: new chain ${chain}" >&2 + echo ":${chain} - [0:0]" + fi + done + ) + fi + + if [ "x$before" == "x" ] ; then + before=COMMIT + elif [ "x$before" == "xbeginning" ] ; then + before=beginning + else + before="-A $chain $before" + fi + + if [ "$before" != "beginning" ] && echo "${savedrules[$table]}" | grep -qF -- "-A $chain $rule" ; then + return + fi + + local echoed=false + savedrules["$table"]=$( + echo "${savedrules[$table]}" | while read x + do + if [ "beginning" == "$before" -a "$echoed" == "false" ] && echo "$x" | grep -q '^-A ' + then + echo "${table}: adding rule -A ${chain} ${rule} to the beginning" >&2 + echo "-A $chain $rule" + echoed=true + elif [ "$x" == "$before" ] + then + echo "${table}: adding rule -A ${chain} ${rule} before ${before}" >&2 + echo "-A $chain $rule" + fi + if [ "beginning" == "$before" -a "$x" == "-A $chain $rule" ] + then + true + else + echo "$x" + fi + done + ) + } + flushrules() { + local table="$1" + local chain="$2" + + if [ "${savedrules[$table]}" == "" ] ; then + savedrules["$table"]=$(iptables-save -t "$table") + fi + + savedrules["$table"]=$( + echo "${savedrules[$table]}" | while read x + do + if echo "$x" | grep -q "^-A $chain " ; then + echo "${table}: flushing rule $x" >&2 + else + echo "$x" + fi + done + ) + } + addfwrules() { + # This function creates the FORTRESS-ALLOW-FORWARD filter chain + # and adds rules permitting forwarding of traffic + # sent by the VM and destined to the VM. + local ipnetmask="$1" + addrule filter FORWARD "-j FORTRESS-ALLOW-FORWARD" "-i vif+ -o vif+ -j DROP" + addrule filter FORTRESS-ALLOW-FORWARD "-s $ipnetmask -j ACCEPT" + addrule filter FORTRESS-ALLOW-FORWARD "-d $ipnetmask -j ACCEPT" + } + addprrules() { + # This function creates the FORTRESS-SKIP-MASQ nat chain + # and the FORTRESS-ANTISPOOF raw chain + # and adds rules defeating masquerading and anti-spoofing + # for the IP (machine) so long as it comes from / goes to + # the VIF that the machine is behind. + local ipnetmask="$1" + local vif="$2" + addrule nat POSTROUTING "-j FORTRESS-SKIP-MASQ" "-j MASQUERADE" + addrule nat FORTRESS-SKIP-MASQ "-s $ipnetmask -j ACCEPT" + addrule nat FORTRESS-SKIP-MASQ "-d $ipnetmask -j ACCEPT" + addrule raw PREROUTING "-j FORTRESS-ANTISPOOF" beginning + addrule raw FORTRESS-ANTISPOOF "-s $ipnetmask -j ACCEPT" + } + commitrules() { + for table in "${!savedrules[@]}" ; do + echo "${savedrules[$table]}" | iptables-restore -T "$table" + done + } + flushrules filter FORTRESS-ALLOW-FORWARD + flushrules nat FORTRESS-SKIP-MASQ + flushrules raw FORTRESS-ANTISPOOF + """) + + def _add(self, ip, dev, netmask, typ): + netmask = sum([bin(int(x)).count('1') for x in netmask.split('.')]) + dev = dev.replace("+", "0") + text = self.addrule + if typ == "forward": + text += "addfwrules %s/%s\n" % (pipes.quote(ip), netmask) + elif typ == "postrouting": + text += "addprrules %s/%s %s\n" % (pipes.quote(ip), netmask, pipes.quote(dev)) + self.addrule = "" + if not self.rules: + self.rules = [] + self.rules.append(text) + + def addfw(self, ip, dev, netmask): + return self._add(ip, dev, netmask, "forward") + + def addpr(self, ip, dev, netmask): + return self._add(ip, dev, netmask, "postrouting") + + def commit(self): + if not self.rules: + return self.addrule + "\ncommitrules\n" + return "\n".join(self.rules) + "\ncommitrules\n" + + programs = [] + staticipvms = [] + ruler = addfwrule() + + # For every VM downstream of mine. + for vm, vif in collect_downstream_vms(self, None): + # If the VM is running, and it has an associated VIF + # and it has a static IP: + if vm.static_ip and vif and vm.is_running(): + staticipvms.append(vm.name) + # Add ip neighs of and routes to the VM. + # pointed at the VIF that the VM is behind. + programs.append(addroute(vm.ip, vif, vm.netmask)) + # Add prerouting and postrouting rules for the VM + # that defeat masquerading and anti-spoofing. + ruler.addpr(vm.ip, vif, vm.netmask) + # If I am a NetVM, then, additionally. + if self.type == "NetVM": + # Add filter rules for the VM + # that allow it to communicate with other VMs. + ruler.addfw(vm.ip, vif, vm.netmask) + if ruler.commit(): + programs.append(ruler.commit()) + + if not programs: + pass + elif not self.is_running() or self.is_paused(): + msg = "Not running routing programs on %s (VM is paused or off)" % (self.name,) + if notify_function: + notify_function("info", msg) + elif verbose: + print >> sys.stderr, "-->", msg + else: + programs = logger(locked("\n".join(programs))) + if not staticipvms: + msg = "Enabling preliminary routing configuration on %s" % (self.name,) + else: + msg = "Enabling routing of %s on %s" % (", ".join(staticipvms), self.name) + if notify_function: + notify_function("info", msg) + elif verbose: + print >> sys.stderr, "-->", msg + # for x in programs.splitlines(False): + # print >> sys.stderr, "---->", x + p = self.run(fw_encap, user="root", gui=False, wait=True, passio_popen=True, autostart=False) + p.stdin.write(programs) + p.stdin.close() + p.stdout.read() + retcode = p.wait() + if retcode: + msg = "Routing commands on %s failed with return code %s" % (self.name, retcode) + if notify_function: + notify_function("error", msg) + elif verbose: + print >> sys.stderr, "-->", msg + + if self.netvm: + self.netvm.adjust_proxy_arp( + verbose=verbose, + notify_function=notify_function + ) + + def adjust_own_firewall_rules(self, ruleset_script=None): + ruleset_script_path = os.path.join( + os.path.dirname(self.firewall_conf), + "firewall.conf.sh" + ) + f = open(ruleset_script_path, "a+b") + fcntl.flock(f.fileno(), fcntl.LOCK_EX) + try: + if ruleset_script: + f.seek(0) + f.truncate(0) + f.write(ruleset_script) + f.flush() + else: + f.seek(0) + ruleset_script = f.read() + + if ruleset_script: + try: + ruleset_script = logger(locked(ruleset_script)) + p = self.run(fw_encap, user="root", gui=False, wait=True, passio_popen=True, autostart=False) + p.stdin.write(ruleset_script) + p.stdin.close() + p.stdout.read() + retcode = p.wait() + f.seek(0) + f.truncate(0) + f.flush() + except QubesException, e: + pass + + finally: + f.close() + +register_qubes_vm_class(QubesVm) diff --git a/src/usr/lib64/python2.7/site-packages/qubes/modules/006FortressQubesNetVm.py b/src/usr/lib64/python2.7/site-packages/qubes/modules/006FortressQubesNetVm.py new file mode 100644 index 0000000..e69443f --- /dev/null +++ b/src/usr/lib64/python2.7/site-packages/qubes/modules/006FortressQubesNetVm.py @@ -0,0 +1,46 @@ +#!/usr/bin/python2 +# -*- coding: utf-8 -*- +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2010 Joanna Rutkowska +# Copyright (C) 2013 Marek Marczykowski +# +# 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 sys +import libvirt + +from qubes.qubes import QubesNetVm as OriginalQubesNetVm +from qubes.qubes import register_qubes_vm_class,vmm,dry_run +from qubes.qubes import defaults,system_path,vm_files +from qubes.qubes import QubesVmCollection,QubesException + + +class QubesNetVm(OriginalQubesNetVm): + + @property + def netmask(self): + if getattr(self, "static_ip"): + return "255.255.255.255" + return self.__netmask + + @property + def network(self): + return self.__network + + +register_qubes_vm_class(QubesNetVm) diff --git a/src/usr/lib64/python2.7/site-packages/qubes/modules/007FortressQubesProxyVm.py b/src/usr/lib64/python2.7/site-packages/qubes/modules/007FortressQubesProxyVm.py new file mode 100644 index 0000000..1e541b5 --- /dev/null +++ b/src/usr/lib64/python2.7/site-packages/qubes/modules/007FortressQubesProxyVm.py @@ -0,0 +1,183 @@ +#!/usr/bin/python2 +# -*- coding: utf-8 -*- +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2010 Joanna Rutkowska +# Copyright (C) 2013 Marek Marczykowski +# +# 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 datetime import datetime + +import sys +import libvirt +import pipes + +from qubes.qubes import QubesProxyVm as OriginalQubesProxyVm +from qubes.qubes import register_qubes_vm_class,vmm,dry_run +from qubes.qubes import defaults,system_path,vm_files +from qubes.qubes import QubesVmCollection,QubesException + + +yum_proxy_ip = '10.137.255.254' +yum_proxy_port = '8082' + + +class QubesProxyVm(OriginalQubesProxyVm): + + def write_iptables_qubesdb_entry(self): + self.qdb.rm("/qubes-iptables-domainrules/") + iptables = "# Generated by Qubes Core on {0}\n".format(datetime.now().ctime()) + iptables += "*filter\n" + iptables += ":INPUT DROP [0:0]\n" + iptables += ":FORWARD DROP [0:0]\n" + iptables += ":OUTPUT ACCEPT [0:0]\n" + iptables += ":PR-QBS-FORWARD - [0:0]\n" + + # Strict INPUT rules + iptables += "-A INPUT -i vif+ -p udp -m udp --dport 68 -j DROP\n" + iptables += "-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED " \ + "-j ACCEPT\n" + iptables += "-A INPUT -p icmp -j ACCEPT\n" + iptables += "-A INPUT -i lo -j ACCEPT\n" + iptables += "-A INPUT -j REJECT --reject-with icmp-host-prohibited\n" + + iptables += "-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED " \ + "-j ACCEPT\n" + # Allow dom0 networking + iptables += "-A FORWARD -i vif0.0 -j ACCEPT\n" + # Engage in firewalling for VMs + iptables += "-A FORWARD -j PR-QBS-FORWARD\n" + # Deny inter-VMs networking + iptables += "-A FORWARD -i vif+ -o vif+ -j DROP\n" + iptables += "COMMIT\n" + self.qdb.write("/qubes-iptables-header", iptables) + + vms = [vm for vm in self.connected_vms.values()] + vms_rulesets = [] + for vm in vms: + vm_iptables = "" + + iptables="*filter\n" + conf = vm.get_firewall_conf() + + xid = vm.get_xid() + if xid < 0: # VM not active ATM + continue + + ip = vm.ip + if ip is None: + continue + + # Anti-spoof rules are added by vif-script (vif-route-qubes), here we trust IP address + + accept_action = "ACCEPT" + reject_action = "REJECT --reject-with icmp-host-prohibited" + + if conf["allow"]: + default_action = accept_action + rules_action = reject_action + else: + default_action = reject_action + rules_action = accept_action + + for rule in conf["rules"]: + if getattr(vm, "static_ip", None) and rule["address"].startswith("from-"): + ruletext = "-s {0} -d {1}".format(rule["address"][len("from-"):], ip) + if rule["netmask"] != 32: + ruletext += "/{0}".format(rule["netmask"]) + + if rule["proto"] is not None and rule["proto"] != "any": + ruletext += " -p {0}".format(rule["proto"]) + if rule["portBegin"] is not None and rule["portBegin"] > 0: + ruletext += " --dport {0}".format(rule["portBegin"]) + if rule["portEnd"] is not None and rule["portEnd"] > rule["portBegin"]: + ruletext += ":{0}".format(rule["portEnd"]) + + ruletext += " -j {0}\n".format(rules_action) + iptables += "-A PR-QBS-FORWARD " + ruletext + vm_iptables += "-A FORTRESS-INPUT " + ruletext + continue + + iptables += "-A PR-QBS-FORWARD -s {0} -d {1}".format(ip, rule["address"]) + if rule["netmask"] != 32: + iptables += "/{0}".format(rule["netmask"]) + + if rule["proto"] is not None and rule["proto"] != "any": + iptables += " -p {0}".format(rule["proto"]) + if rule["portBegin"] is not None and rule["portBegin"] > 0: + iptables += " --dport {0}".format(rule["portBegin"]) + if rule["portEnd"] is not None and rule["portEnd"] > rule["portBegin"]: + iptables += ":{0}".format(rule["portEnd"]) + + iptables += " -j {0}\n".format(rules_action) + + if conf["allowDns"] and self.netvm is not None: + # PREROUTING does DNAT to NetVM DNSes, so we need self.netvm. + # properties + iptables += "-A PR-QBS-FORWARD -s {0} -p udp -d {1} --dport 53 -j " \ + "ACCEPT\n".format(ip,self.netvm.gateway) + iptables += "-A PR-QBS-FORWARD -s {0} -p udp -d {1} --dport 53 -j " \ + "ACCEPT\n".format(ip,self.netvm.secondary_dns) + iptables += "-A PR-QBS-FORWARD -s {0} -p tcp -d {1} --dport 53 -j " \ + "ACCEPT\n".format(ip,self.netvm.gateway) + iptables += "-A PR-QBS-FORWARD -s {0} -p tcp -d {1} --dport 53 -j " \ + "ACCEPT\n".format(ip,self.netvm.secondary_dns) + if conf["allowIcmp"]: + iptables += "-A PR-QBS-FORWARD -s {0} -p icmp -j ACCEPT\n".format(ip) + if getattr(vm, "static_ip", None): + iptables += "-A PR-QBS-FORWARD -d {0} -p icmp -j ACCEPT\n".format(ip) + vm_iptables += "-A FORTRESS-INPUT -d {0} -p icmp -j ACCEPT\n".format(ip) + if conf["allowYumProxy"]: + iptables += "-A PR-QBS-FORWARD -s {0} -p tcp -d {1} --dport {2} -j ACCEPT\n".format(ip, yum_proxy_ip, yum_proxy_port) + else: + iptables += "-A PR-QBS-FORWARD -s {0} -p tcp -d {1} --dport {2} -j DROP\n".format(ip, yum_proxy_ip, yum_proxy_port) + + iptables += "-A PR-QBS-FORWARD -s {0} -j {1}\n".format(ip, default_action) + if getattr(vm, "static_ip", None): + iptables += "-A PR-QBS-FORWARD -d {0} -j {1}\n".format(ip, default_action) + vm_iptables += "-A FORTRESS-INPUT -d {0} -j {1}\n".format(ip, default_action) + vm_iptables += "COMMIT\n" + vms_rulesets.append((vm, vm_iptables)) + iptables += "COMMIT\n" + self.qdb.write("/qubes-iptables-domainrules/"+str(xid), iptables) + + # no need for ending -A PR-QBS-FORWARD -j DROP, cause default action is DROP + + self.write_netvm_domid_entry() + + self.rules_applied = None + self.qdb.write("/qubes-iptables", 'reload') + + for vm, ruleset in vms_rulesets: + shell_ruleset = "echo Adjusting firewall rules to: >&2\n" + shell_ruleset += "echo %s >&2\n" % pipes.quote(ruleset.strip()) + shell_ruleset += "data=$(iptables-save -t filter)\n" + shell_ruleset += 'if ! echo "$data" | grep -q -- "^:FORTRESS-INPUT" ; then\n' + shell_ruleset += ' data=$(echo "$data" | sed "s/^:INPUT/:FORTRESS-INPUT - [0:0]\\n\\0/")\n' + shell_ruleset += "fi\n" + shell_ruleset += 'if ! echo "$data" | grep -q -- "-A INPUT -j FORTRESS-INPUT" ; then\n' + shell_ruleset += ' data=$(echo "$data" | sed -r "s|-A INPUT -i vif. -j REJECT --reject-with icmp-host-prohibited|-A INPUT -j FORTRESS-INPUT\\n\\0|")\n' + shell_ruleset += "fi\n" + shell_ruleset += 'data=$(echo "$data" | grep -v ^COMMIT$)\n' + shell_ruleset += 'data=$(echo "$data" | grep -v -- "-A FORTRESS-INPUT")\n' + shell_ruleset += 'data="$data\n"%s\n' % pipes.quote(ruleset) + shell_ruleset += 'echo "$data" | iptables-restore -T filter\n' + vm.adjust_own_firewall_rules(shell_ruleset) + + +register_qubes_vm_class(QubesProxyVm)