mirror of
https://github.com/Rudd-O/qubes-network-server.git
synced 2025-03-01 14:22:35 +01:00
Initial commit.
This commit is contained in:
commit
7ad6b81670
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
*.pyc
|
||||
*~
|
||||
*.tar.gz
|
||||
*.rpm
|
20
Makefile
Normal file
20
Makefile
Normal file
@ -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
|
121
README.md
Normal file
121
README.md
Normal file
@ -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.
|
||||
|
||||

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

|
||||
|
||||
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`.
|
BIN
doc/Qubes network server model.dia
Normal file
BIN
doc/Qubes network server model.dia
Normal file
Binary file not shown.
BIN
doc/Qubes network server model.png
Normal file
BIN
doc/Qubes network server model.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.3 KiB |
BIN
doc/Standard Qubes OS network model.dia
Normal file
BIN
doc/Standard Qubes OS network model.dia
Normal file
Binary file not shown.
BIN
doc/Standard Qubes OS network model.png
Normal file
BIN
doc/Standard Qubes OS network model.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
36
qubes-network-server.spec
Normal file
36
qubes-network-server.spec
Normal file
@ -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) <rudd-o@rudd-o.com>
|
||||
- Initial release
|
170
src/usr/bin/qvm-static-ip
Normal file
170
src/usr/bin/qvm-static-ip
Normal file
@ -0,0 +1,170 @@
|
||||
#!/usr/bin/python2
|
||||
# -*- encoding: utf8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2010 Joanna Rutkowska <joanna@invisiblethingslab.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
#
|
||||
|
||||
from qubes.qubes import QubesVmCollection
|
||||
from 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] <vm-name>\n"\
|
||||
"usage: %prog -g [options] <vm-name> <property>\n"\
|
||||
"usage: %prog -s [options] <vm-name> <property> [...]\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()
|
@ -0,0 +1,401 @@
|
||||
#!/usr/bin/python2
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2010 Joanna Rutkowska <joanna@invisiblethingslab.com>
|
||||
# Copyright (C) 2013 Marek Marczykowski <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
#
|
||||
|
||||
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)
|
@ -0,0 +1,46 @@
|
||||
#!/usr/bin/python2
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2010 Joanna Rutkowska <joanna@invisiblethingslab.com>
|
||||
# Copyright (C) 2013 Marek Marczykowski <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
#
|
||||
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)
|
@ -0,0 +1,183 @@
|
||||
#!/usr/bin/python2
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2010 Joanna Rutkowska <joanna@invisiblethingslab.com>
|
||||
# Copyright (C) 2013 Marek Marczykowski <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
#
|
||||
from 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)
|
Loading…
x
Reference in New Issue
Block a user