diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index d38b88e..0000000 --- a/.settings/org.eclipse.core.resources.prefs +++ /dev/null @@ -1,4 +0,0 @@ -eclipse.preferences.version=1 -encoding//src/usr/lib64/python2.7/site-packages/qubes/modules/001FortressQubesVm.py=utf-8 -encoding//src/usr/lib64/python2.7/site-packages/qubes/modules/006FortressQubesNetVm.py=utf-8 -encoding//src/usr/lib64/python2.7/site-packages/qubes/modules/007FortressQubesProxyVm.py=utf-8 diff --git a/Makefile b/Makefile index bde700d..fd703c6 100644 --- a/Makefile +++ b/Makefile @@ -1,23 +1,34 @@ -BINDIR=/usr/bin -LIBDIR=/usr/lib64 +SBINDIR=/usr/local/sbin +UNITDIR=/etc/systemd/system DESTDIR= PROGNAME=qubes-network-server +all: src/qubes-routing-manager.service + +src/qubes-routing-manager.service: src/qubes-routing-manager.service.in + sed 's|@SBINDIR@|$(SBINDIR)|g' < $< > $@ + +ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) + +.PHONY: clean dist rpm srpm install + clean: find -name '*.pyc' -o -name '*~' -print0 | xargs -0 rm -f - rm -f *.tar.gz *.rpm + rm -rf *.tar.gz *.rpm dist: clean - excludefrom= ; test -f .gitignore && excludefrom=--exclude-from=.gitignore ; DIR=$(PROGNAME)-`awk '/^Version:/ {print $$2}' $(PROGNAME).spec` && FILENAME=$$DIR.tar.gz && tar cvzf "$$FILENAME" --exclude="$$FILENAME" --exclude=.git --exclude=.gitignore $$excludefrom --transform="s|^|$$DIR/|" --show-transformed * - -rpm: dist - @which rpmbuild || { echo 'rpmbuild is not available. Please install the rpm-build package with the command `dnf install rpmbuild` to continue, then rerun this step.' ; exit 1 ; } - T=`mktemp -d` && rpmbuild --define "_topdir $$T" -ta $(PROGNAME)-`awk '/^Version:/ {print $$2}' $(PROGNAME).spec`.tar.gz || { rm -rf "$$T"; exit 1; } && mv "$$T"/RPMS/*/* "$$T"/SRPMS/* . || { rm -rf "$$T"; exit 1; } && rm -rf "$$T" + @which rpmspec || { echo 'rpmspec is not available. Please install the rpm-build package with the command `dnf install rpm-build` to continue, then rerun this step.' ; exit 1 ; } + cd $(ROOT_DIR) || exit $$? ; excludefrom= ; test -f .gitignore && excludefrom=--exclude-from=.gitignore ; DIR=`rpmspec -q --queryformat '%{name}-%{version}\n' *spec | head -1` && FILENAME="$$DIR.tar.gz" && tar cvzf "$$FILENAME" --exclude="$$FILENAME" --exclude=.git --exclude=.gitignore $$excludefrom --transform="s|^|$$DIR/|" --show-transformed * srpm: dist - T=`mktemp -d` && rpmbuild --define "_topdir $$T" -ts $(PROGNAME)-`awk '/^Version:/ {print $$2}' $(PROGNAME).spec`.tar.gz || { rm -rf "$$T"; exit 1; } && mv "$$T"/SRPMS/* . || { rm -rf "$$T"; exit 1; } && rm -rf "$$T" + @which rpmbuild || { echo 'rpmbuild is not available. Please install the rpm-build package with the command `dnf install rpm-build` to continue, then rerun this step.' ; exit 1 ; } + cd $(ROOT_DIR) || exit $$? ; rpmbuild --define "_srcrpmdir ." -ts `rpmspec -q --queryformat '%{name}-%{version}.tar.gz\n' *spec | head -1` -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 - install -Dm 644 src/usr/lib64/python2.7/site-packages/qubes/modules/qubes-appvm-firewall -t $(DESTDIR)/$(LIBDIR)/python2.7/site-packages/qubes/modules +rpm: dist + @which rpmbuild || { echo 'rpmbuild is not available. Please install the rpm-build package with the command `dnf install rpm-build` to continue, then rerun this step.' ; exit 1 ; } + cd $(ROOT_DIR) || exit $$? ; rpmbuild --define "_srcrpmdir ." --define "_rpmdir builddir.rpm" -ta `rpmspec -q --queryformat '%{name}-%{version}.tar.gz\n' *spec | head -1` + cd $(ROOT_DIR) ; mv -f builddir.rpm/*/* . && rm -rf builddir.rpm + +install: all + install -Dm 755 src/qubes-routing-manager -t $(DESTDIR)/$(SBINDIR)/ + install -Dm 644 src/qubes-routing-manager.service -t $(DESTDIR)/$(UNITDIR)/ diff --git a/README.md b/README.md index e954a11..cc41285 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,14 @@ -#Qubes network server +# Qubes network server -This software lets you turn your [Qubes OS](https://www.qubes-os.org/) machine into a network server, enjoying all the benefits of Qubes OS (isolation, secure inter-VM process communication, ease of use) with none of the drawbacks of setting up your own Xen server. +This software lets you turn your [Qubes OS 4.0](https://www.qubes-os.org/) machine into +a network server, enjoying all the benefits of Qubes OS (isolation, secure +inter-VM process communication, ease of use) with none of the drawbacks +of setting up your own Xen server. -##Why? +This release is only intended for use with Qubes OS 4.0. Newer or older Qubes OS releases +will not support it. + +## Why? Qubes OS is a magnificent operating system, but there are so many use cases that its networking model cannot crack: @@ -19,7 +25,7 @@ Qubes OS is a magnificent operating system, but there are so many use cases that * Anything that involves a secure server, serving data to people or machines, simply cannot be done under vanilla Qubes OS. -##Enhanced networking model +## Enhanced networking model The traditional Qubes OS networking model contemplates a client-only use case. User VMs (AppVMs or StandaloneVMs) are attached to ProxyVMs, @@ -44,54 +50,155 @@ 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. +a physical link shared by a NetVM. Those network server VMs also +obey the Qubes OS outbound firewall rules controls, letting you run +services with outbound connections restricted. 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. +model remains in effect until you decide to enable the feature on any +particular VM. -##How to use this software +The only drawback of this method is that it requires you to attach +VMs meant to be exposed to the network directly to a NetVM, rather than +through a ProxyVM. VMs exposed through a ProxyVM will not be visible +to machines on the same network as the NetVM. + +## How to use this software Once installed (see below), usage of the software is straightforward. -Here are documents that will help you take advantage of Qubes -network server: + +These sample instructions assume you already have an AppVM VM set up, +named `testvm`, and that your `sys-net` VM is attached to a network with +subnet `192.168.16.0/24`. + +First, attach the VM you want to expose to the network +to a NetVM that has an active network connection: + +`qvm-prefs -s testvm netvm sys-net` + +Then, set an IP address on the VM: + +`qvm-prefs -s testvm ip 192.168.16.25` + +(The step above requires you restart the `testvm` VM if it was running.) + +Then, to enable the network server feature for your `testvm` VM, all you have +to do in your AdminVM (`dom0`) is run the following command: + +`qvm-features testvm routing-method forward` + +Now `testvm` is exposed to the network with address `192.168.16.25`, as well +as to other VMs attached to `NetVM`. + +Do note that `testvm` will have the standard Qubes OS firewall rules stopping +inbound traffic. To solve that issue, you can +[use the standard `rc.local` Qubes OS mechanism to alter the firewall rules](https://www.qubes-os.org/doc/firewall/#where-to-put-firewall-rules) +in your `testvm` AppVM. + +Here are documents that will help you take advantage of Qubes network server: * [Setting up your first server](doc/Setting up your first server.md) * [Setting up an SSH server](doc/Setting up an SSH server.md) - ## Installation -Installation is straightforward — build package, copy to dom0, -install in dom0. Here are step by step instructions: +Installation consists of two steps. -* Install the `rpm-build` package on your build machine - with `sudo dnf install rpm-build`. Remember that if your - build machine is an AppVM or any other sort of VM that boots - from a template, you may want to run that `dnf` command on the - template, rather than the build machine, and then power off - the template, followed by rebooting the build machine. -* Clone the repository for this program to your build machine. -* In your build machine, prepare an RPM with the `make rpm` - command on the local directory of your clone. This creates a file - `qubes-network-server-*-noarch.rpm` on that directory. -* Copy the prepared RPM to the dom0 of your Qubes OS machine. -* Install the RPM in the dom0 with - `rpm -ivh `. -* Restart Qubes Manager, if it is running: right-click on its - notification icon, select *Exit*, then relaunch it from the - *System* submenu of your Qubes OS application menu. +### Apply software patch to `dom0` -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`: +The first step is to apply a patch to your Qubes OS `dom0`. +The patch is distributed in the source, within the `src/` directory. +Future releases of this software will contain a better method for +deploying the `dom0` part of the software. + +Copy the patch to your `dom0` somehow, then apply it: ``` -qvm-run --pass-io vmwiththerpm 'cat /home/user/path/to/qubes-network-server*rpm' > qns.rpm +# Change into the Python 3.5 site-packages directory. +cd /usr/lib/python3.5/site-packages + +# Back up your `net.py` file. +if ! test -f qubes/vm/mix/net.py.bak + sudo cp qubes/vm/mix/net.py qubes/vm/mix/net.py.bak +fi + +# Patch the `net.py` file. +sudo patch -p1 < /home/user/qubes-network-server-dom0.patch + +# Restart qubesd. +sudo systemctl restart qubesd.service ``` -This lets you fetch the RPM file to the dom0, and save it as `qns.rpm`, -which you can then feed as an argument to the `rpm -ivh` command. +You can now verify that `qubesd` is running: + +``` +sudo systemctl status qubesd.service | cat +``` + +#### Restoring from backup in case of failed installation. + +If `qubesd` has failed for some reason, you can restore +from the backup and restart again. This will nullify +the changes you made in this routine. To restore from +the backup, use the following commands: + +``` +udo cp qubes/vm/mix/net.py.bak qubes/vm/mix/net.py +sudo systemctl restart qubesd.service +``` + +### Install the agent in the TemplateVM + +The second step is to build and install the component that goes +in the TemplateVM that your NetVM (typically `sys-net` uses). +If your NetVM is a StandaloneVM, you should modify these instructions +to install the component directly in the NetVM, as in this case +your NetVM does not instantiation from a Qubes template. + +Within a DisposableVM, build said package as follows: + +``` +# Dependencies +dnf install git rpm-build make coreutils tar gawk findutils systemd systemd-rpm-macros +# Source code +git clone https://github.com/Rudd-O/qubes-network-server +# +cd qubes-network-server +make rpm +``` + +The process will output a `qubes-network-server-*.noarch.rpm` in the +directory where it ran. Copy that RPM to your TemplateVM. Here is +a trick to do that from `dom0`, which fetchs the RPM file from the DisposableVM +and saves it within the TemplateVM, which you can then feed as an argument +to the `rpm -ivh` command: + +``` +qvm-run --pass-io disp8474 'cat /home/user/qubes-network-server/qubes-network-server*.noarch.rpm' | \ + qvm-run --pass-io fedora-30 'cat > qns.rpm' +``` + +You can power off the DisposableVM now. + +Now you can install this package in your TemplateVM (or NetVM, if it is a StandaloneVM): + +``` +# (In TemplateVM) +# rpm -ivh qns.rpm +``` + +Now power off both your TemplateVM and your NetVM. + +Then start your NetVM back up. + +You can verify that the necessary component is running by launching a terminal +in your NetVM, then typing the following: + +``` +systemctl status qubes-routing-manager.service +``` + +The routing manager should show as `enabled` and `active` in the terminal output. ### Upgrading to new / bug-fixing releases @@ -104,42 +211,65 @@ Qubes OS relies on layer 3 (IP) routing. VMs in its networked tree send traffic their default route interfaces, which upstream VMs receive and masquerade out of their own default route interfaces. -Qubes network server slightly changes this when a networked VM — a VM which has had its -`static_ip` attribute set with `qvm-static-ip` — exists on the networked tree. As soon -as a networked VM boots up, Qubes network server: +Qubes network server slightly changes this when a VM gets its `routing-method` feature set +to `forward`. As soon as the feature is enabled with that value, or the VM in question +boots up, Qubes network server: -* sets a static `/32` route on every upstream VM to the networked VM's static IP, - directing the upstream VMs to route traffic for that IP to their VIFs where - they may find the networked VM -* enables ARP neighbor proxying for the static IP on every upstream VM, such that - every upstream VM announces itself to their own upstream VMs (and LAN, in the - case of NetVMs) as the networked VM -* sets firewall rules on every upstream VM that allow normal non-masquerading forwarding - to and from the IP of the networked VM -* (depending on the Qubes firewall policy of the networked VM) sets rules on every - upstream ProxyVM that allow for certain classes of inbound traffic -* (depending on the Qubes firewall policy of the networked VM) sets rules directly - on the networked VM that allow for certain classes of inbound traffic +* enables ARP neighbor proxying (and, if using IPv6, NDP neighbor proxying) in the NetVM +* sets firewall rules in the NetVM that neuter IP masquerading on traffic coming from + the networked VM +* sets firewall rules in the NetVM that allow traffic from other VMs to reach the + networked VM, neutering the default Qubes OS rule that normally prohibits this -The end result is instantaneous networking — machines upstream from the networked VM, -including machines in the physical LAN, can "see", ping, and connect to the networked -VM, provided that the firewall policy permits it. You do not need to set up any -special host-only routes on machines trying to access your networked VM — provided -that the static IP is on the same routable subnet as its upstream VM's, Qubes -network server does its magic automatically. +The above have the effect of exposing the networked VM to: -Of course, LAN machines connecting to the networked VM believe that the networked VM -possesses the MAC address of its upstream NetVM (just as if the upstream NetVM had a -second IP address and was serving traffic from it), but in reality, that is just an -illusion created by Qubes network server. This does have implications for your own -network security policy, in that the networked VM appears (from a MAC perspective) -to share a network card with its upstream NetVM. +* other AppVMs attached to the same NetVM +* other machines attached to the same physical network the NetVM is attached to + +Now all machines in the same LAN will be able to reach the networked VM. +Here is a step-by-step explanation of how IP traffic to and from the networked +VM happens, when the `routing-method` is set to `forward` on the networked VM: + +1. Machine in LAN asks for the MAC address of the networked VM's IP address. +2. NetVM sees the ARP/NDP request and responds by proxying it to the networked VM. +3. Networked VM replies to the ARP/NDP request back to the NetVM. +4. NetVM relays the ARP/NDP reply back to the network, but substitutes its own + MAC address in the reply. +5. Machine in LAN sends local IP packet to the IP of the networked VM's IP address, + but destined to the MAC address of the NetVM. +6. The NetVM sees the IP packet, and routes it to the networked VM. +7. The Networked VM receives the IP packet. +8. If the networked VM needs to respond, it sends an IP packet back to the + machine in LAN. +9. NetVM notices packet comes from the networked VM, and instead of masquerading it, + it lets the packet through unmodified, with the source IP address of the + networked VM. + +The end result is practical networking with no need to set up routing tables on +machines attempting to access the networked VM. + +Of course, if you want machines in the LAN to have access to the networked VM, you +must still set an appropriate `ip` preference on the networked VM. For example, if +your physical LAN had subnet `1.2.3.0/24`, and you want machines in your physical LAN +to connect to a networked VM, you must set the `ip` preference of the networked VM +to a previously-unused IP within the range `1.2.3.1` and `1.2.3.255`. Failing that, +you must assign a host-specific route on the source machine which uses the NetVM +as the gateway, and uses the IP address of the networked VM (see `qvm-prefs` output) +as the destination address. ## Limitations * HVMs are not supported at all at this time. This will change over time, and you can help it change faster by submitting a pull request with HVM support. +* Interposing a ProxyVM between a networked VM and the NetVM is not currently + supported. This is implementable in principle, but would require a recursive + walk that encompasses the entire network link from NetVM through intermediate + ProxyVMs. ## Troubleshooting -The actions that the network server software performs are logged to the journal of each of the involved VMs. Generally, for each VM that has its own `static_ip` address set, this software will perform actions on that VM, on its parent ProxyVM, and on its grandparent NetVM. In case of problems, tailing the journal (`sudo journalctl -b`) on those three VMs simultaneously can be extremely useful to figure out what is going on. +The actions that the `qubes-routing-manager` service performs are logged to the journal +of the NetVM where the `qubes-routing-manager` service is running. + +In case of problems, tailing the journal (`sudo journalctl -fa`) on the NetVM will be +extremely useful to figure out what the problem is. diff --git a/TODO b/TODO index c805f17..1a74b46 100644 --- a/TODO +++ b/TODO @@ -1,37 +1,13 @@ To do list: -* Make the system do the right thing (withdraw ip neigh / - ip route / iptables rules) when VMs power off or when - their network gets detached. - Right now the rules are only reconfigured when: - * a VM starts (ancestor VMs get reconfigured) - * a VM gets unpaused (same as before) - * a VM network gets attached (same as before) - * a VM's FW rules get altered (parent ProxyVM and sibling - VMs get reconfigured, and this reconfiguration only - affects iptables rules) -* Make the system do the right thing when `static_ip` - is changed / enabled / disabled, without requiring a - VM restart. - * Key point (but not only point): appvm fwrules that - were setup need to be un-setup, which means that - our current algorithm "look at VMs with static_ip" - will not work to un-setup those fwrules. - * Define very clearly when fw state is modified - for appvm, as that requires execution of code - in the appvm, and tracking how and when to - undo that state transition. - * VM's entire IP and everything will be different, - and this setup only occurs during initial boot of the - VM, so it may be inevitable to force a restart of - the VM. It depends on what kind of stuff depends on - the IP being set early on boot. VM rounting tables, - ifconfig, stuff like ip neigh on the ancestor VMS, - firewall rules, et cetera. -* Evaluate network access permissions when appvm - is attached to netvm, vs attached to proxyvm to netvm, - vs attached to proxyvm to proxyvm to netvm. -* Prolly need to write some important automated tests. -* Document entry points of the plugin that activate - code from the plugin, and under which circumstances / events - these pieces of code run. +* Package up `dom0` component so it's installable via + RPM. Alternatively, upstream it completely. +* Make the system more robust by setting the right + `ip neigh / ip route` rules to force incoming traffic + to go to the specific VIF that backs the exposed VM. +* Instead of / in addition to proxy ARP/NDP, use static + MAC addresses set at runtime, for each VM. +* Support interposing ProxyVMs between NetVMs and AppVMs. +* (Maybe) set up firewall rules on AppVM to obey its designated + firewall rules, bringing back support for the GUI. This + probably needs a conversation with the Qubes OS core devs. diff --git a/doc/Qubes network server model.dia b/doc/Qubes network server model.dia index 973b472..46ad2e3 100644 Binary files a/doc/Qubes network server model.dia 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 index e45e6f8..5150980 100644 Binary files a/doc/Qubes network server model.png and b/doc/Qubes network server model.png differ diff --git a/doc/Setting up an SSH server.md b/doc/Setting up an SSH server.md index 18179fd..69ac1f8 100644 --- a/doc/Setting up an SSH server.md +++ b/doc/Setting up an SSH server.md @@ -14,86 +14,73 @@ First of all, [install Qubes network server](https://github.com/Rudd-O/qubes-net ## Set up needed VMs -You'll need three VMs on the network server: +You'll need two VMs on the network server: 1. A NetVM which will be attached to the network interface mentioned above. For the purposes of this example, we'll call this `exp-net`. -2. A ProxyVM which will be attached to the NetVM. - This we'll call `exp-firewall`. 3. A StandaloneVM which will be attached to the ProxyVM. The role of this machine is to give you control over `dom0` and other VMs on the system. - This we'call `exp-manager`. + This we'call `exp-ssh`. Create them if you do not already have them. Once you have created them, -start the StandaloneVM `exp-manager` you created, and then verify that you -can ping your manager machine from it. +start the StandaloneVM `exp-ssh` you created, and then verify that networking +works within `exp-ssh`. -Power off `exp-manager` when your test is complete. - -## Set static address on `exp-manager` +## Set static address on `exp-ssh` On your server's `dom0`, run the command: ``` -qvm-static-ip -s exp-manager static_ip x.y.z.w +qvm-prefs -s exp-ssh static_ip x.y.z.w ``` `x.y.z.w` must be an IP address available on the same network that both your `exp-net` and your manager machine share. -Power `exp-manager` back on, and verify that you can still ping your -manager machine from it. +Shut down `exp-ssh` back on, start it back up again, +and verify that you can still ping your manager machine from it. -Verify that you can ping the new IP address you gave to `exp-manager` +## Enable forward-style routing for `exp-ssh` + +``` +qvm-features exp-ssh routing-method forward +``` + +Now verify that you can ping the new IP address you gave to `exp-ssh` from your manager machine. This should work fine. -## Harden the firewall on `exp-manager` +## Adjust the firewall on `exp-ssh` -At this point, `exp-manager` is accessible on your network, so it's best +At this point, `exp-ssh` is accessible on your network, so it's best to set up a firewall rule permitting only SSH access from the manager machine, and denying all other access to anyone. -If you are new to firewall rules in Qubes, [check out this quite -good overview of them](https://www.qubes-os.org/doc/qubes-firewall/). +[See the documentation for Qubes OS](https://www.qubes-os.org/doc/firewall/#where-to-put-firewall-rules) +to understand more about firewalls in AppVMs -Launch the Qubes Manager preferences window for the `exp-manager` VM. -Go to the *Firewall rules* tab and select *Deny network access -except...* from the top area. +## Enable and start SSH on the `exp-ssh` VM -Add a new network rule (use the plus button). On the *Address* box, -you're going to write `from-a.b.c.d`, where `a.b.c.d` is the IP address -of your manager machine. Select the *TCP* protocol, and type `22` -(the SSH port) on the *Service* box. Click OK. - -([See the documentation for qubes-network-server](https://github.com/Rudd-O/qubes-network-server) -to understand more about firewalling rules in Qubes network server.) - -Back on the main dialog, click *OK*. - -## Enable and start SSH on the `exp-manager` VM - -In a terminal window of `exp-manager`, run: +In a terminal window of `exp-ssh`, run: ``` -sudo systemctl enable sshd.service -sudo systemctl start sshd.service +sudo systemctl enable --now sshd.service ``` -This will start the OpenSSH server on the `exp-manager` VM. +This will start the OpenSSH server on the `exp-ssh` VM. Test that you can connect via SSH from the manager machine to -the `exp-manager` VM. You will not be able to log in, because +the `exp-ssh` VM. You will not be able to log in, because no password is set up, but we will fix that shortly. ## Set up SSH authentication -On the `exp-manager` VM, set a password on the `user` user: +On the `exp-ssh` VM, set a password on the `user` user: ``` sudo passwd user ``` -On the manager machine, copy your SSH public key to `exp-manager`: +On the manager machine, copy your SSH public key to `exp-ssh`: ``` ssh-copy-id user@x.y.z.w @@ -101,7 +88,7 @@ ssh-copy-id user@x.y.z.w This will prompt you for the password you set up. Enter it. -Now kill the `user` password on `exp-manager`: +Now kill the `user` password on `exp-ssh`: ``` sudo passwd -d user @@ -110,7 +97,7 @@ sudo passwd -l user Good news! You can now remotely log in, from your manager machine, to your Qubes OS server. You are also able to run commands on the -`exp-manager` VM, directly from your manager machine. +`exp-ssh` VM, directly from your manager machine. Should you want to run commands on *other* VMs of your Qubes OS server, then learn how to [enable remote management of your Qubes network server](https://github.com/Rudd-O/ansible-qubes/tree/master/doc/Remote management of Qubes OS servers.md). diff --git a/doc/Setting up your first server.md b/doc/Setting up your first server.md index bdcb2b2..4b8dd26 100644 --- a/doc/Setting up your first server.md +++ b/doc/Setting up your first server.md @@ -2,9 +2,8 @@ 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 +a running HTTP server (port 80) within it. This VM is attached 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`. @@ -15,70 +14,68 @@ 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 +qvm-prefs -s httpserver ip 192.168.1.6 ``` ##Restart VM -Due to limitations in this release of the code, you must power off -the `httpserver` VM and then power it back on. +Due to limitations in how the IP address is set on the VM, you must +power off the `httpserver` VM and then power it back on. + +## Enable forward-style routing to the VM + +``` +qvm-feature httpserver routing-method forward +``` + +Now the IP of the `httpserver` VM is visible to your laptop, but +it's got the standard Qubes OS firewall rules that all AppVMs have, +so next we'll adjust that. ##Set firewall rules on VM -If you are new to firewall rules in Qubes, [check out this quite -good overview of them](https://www.qubes-os.org/doc/qubes-firewall/). +The normal way to set up AppVM firewall rules is +[documented here](https://www.qubes-os.org/doc/firewall/#where-to-put-firewall-rules). -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*. +For the purposes of this demo, all you have to run inside `httpserver` +is this: -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. +``` +sudo iptables -I INPUT 1 -p tcp --dport 8080 -j ACCEPT +``` -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. +(This method of setting firewall rules makes them go away when you +restart the AppVM. Refer to the link in this section to make them +stick around after a VM restart.) -**Security note**: the default "allow all" firewall leaves all ports -of the VM accessible to the world. To the extent that you can avoid -it, do not use the "allow all" firewall setting at all. +## Start a Python HTTP server -Back on the main dialog, click *OK*. +In your `httpserver` VM, run: + +``` +python3 -m http.server +``` ##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. +You will now be able to point your browser at http://192.168.1.6:8080/, +and it will render the served pages from the HTTP server running +directly on `httpserver`. ##Inter-VM network communication This software isn't limited to just letting network servers be accessible from your physical network. VMs can talk among each -other too. Simple instructions: - -* Set up a static IP address for each VM. -* Set up the appropriate rules to let them talk to each other. - -VMs so authorized can talk to each other over the network, -even when they do not share a ProxyVM between them, of course, -so long as their ProxyVMs share the same NetVM. +other too. A pair of VMs whose feature `routing-method` has been +set to `forward` are authorized to talk to each other over the +network, so long as they are attached to the same NetVM. ##Disabling network server -Two-step process. Step one: +One-step process: ``` -qvm-static-ip -s httpserver static_ip none +qvm-feature --delete httpserver routing-method ``` -Step two: power the VM off, then start it back up. +You're done. diff --git a/qubes-network-server.spec b/qubes-network-server.spec index d09e956..76bfc74 100644 --- a/qubes-network-server.spec +++ b/qubes-network-server.spec @@ -1,48 +1,69 @@ -%{!?python2_sitearch: %define python2_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)")} +%{!?python2_sitearch: %define python2_sitearch %(%{__python2} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)")} %define debug_package %{nil} %define mybuildnumber %{?build_number}%{?!build_number:1} Name: qubes-network-server -Version: 0.0.10 +Version: 0.0.11 Release: %{mybuildnumber}%{?dist} Summary: Turn your Qubes OS into a network server BuildArch: noarch License: GPLv3+ URL: https://github.com/Rudd-O/qubes-network-server -Source0: https://github.com/Rudd-O/%{name}/archive/{%version}.tar.gz#/%{name}-%{version}.tar.gz +Source0: https://github.com/Rudd-O/%{name}/archive/{%version}.tar.gz#/%{name}-%{version}.tar.gz BuildRequires: make BuildRequires: coreutils BuildRequires: tar BuildRequires: gawk BuildRequires: findutils +BuildRequires: systemd +BuildRequires: systemd-rpm-macros -Requires: qubes-core-dom0 +Requires: qubes-core-agent-networking >= 4.0.51-1 +Requires: qubes-core-agent-networking < 4.1 +Requires: python2 +Requires: python2-qubesdb %description This package lets you turn your Qubes OS into a network server. +Please see README.md enclosed in the package for instructions on how to use this. + %prep %setup -q %build # variables must be kept in sync with install -make DESTDIR=$RPM_BUILD_ROOT BINDIR=%{_bindir} LIBDIR=%{_libdir} +make DESTDIR=$RPM_BUILD_ROOT SBINDIR=%{_sbindir} UNITDIR=%{_unitdir} %install rm -rf $RPM_BUILD_ROOT # variables must be kept in sync with build -make install DESTDIR=$RPM_BUILD_ROOT BINDIR=%{_bindir} LIBDIR=%{_libdir} +make install DESTDIR=$RPM_BUILD_ROOT SBINDIR=%{_sbindir} UNITDIR=%{_unitdir} +mkdir -p "$RPM_BUILD_ROOT"/%{_presetdir} +echo 'enable qubes-routing-manager.service' > "$RPM_BUILD_ROOT"/%{_presetdir}/75-%{name}.preset %files -%attr(0755, root, root) %{_bindir}/qvm-static-ip -%attr(0644, root, root) %{python2_sitearch}/qubes/modules/*.py* -%attr(0644, root, root) %{python2_sitearch}/qubes/modules/qubes-appvm-firewall +%attr(0755, root, root) %{_sbindir}/qubes-routing-manager +%attr(0644, root, root) %{_presetdir}/75-%{name}.preset +%config %attr(0644, root, root) %{_unitdir}/qubes-routing-manager.service %doc README.md TODO +%post +%systemd_post qubes-routing-manager.service + +%preun +%systemd_preun qubes-routing-manager.service + +%postun +%systemd_postun_with_restart qubes-routing-manager.service + %changelog +* Mon Apr 13 2020 Manuel Amador (Rudd-O) +- Update to Qubes 4.0 + * Tue Oct 11 2016 Manuel Amador (Rudd-O) - Initial release diff --git a/src/qubes-network-server-dom0.patch b/src/qubes-network-server-dom0.patch new file mode 100644 index 0000000..a0dabe8 --- /dev/null +++ b/src/qubes-network-server-dom0.patch @@ -0,0 +1,122 @@ +diff --git a/qubes/vm/mix/net.py b/qubes/vm/mix/net.py +index 129bc107..fac6ec52 100644 +--- a/qubes/vm/mix/net.py ++++ b/qubes/vm/mix/net.py +@@ -262,6 +262,8 @@ class NetVMMixin(qubes.events.Emitter): + This will allow re-reconnecting them cleanly later. + ''' + # pylint: disable=unused-argument ++ if self.netvm: ++ self.netvm.shutdown_routing_for_vm(self) + for vm in self.connected_vms: + if not vm.is_running(): + continue +@@ -271,6 +273,19 @@ class NetVMMixin(qubes.events.Emitter): + # ignore errors + pass + ++ @qubes.ext.handler( ++ 'domain-feature-set:routing-method', ++ 'domain-feature-delete:routing-method', ++ ) ++ def on_routing_method_changed( ++ self, ++ event, feature, ++ value=None, oldvalue=None ++ ): ++ # pylint: disable=no-self-use,unused-argument ++ if self.netvm: ++ self.netvm.reload_routing_for_vm(self) ++ + @qubes.events.handler('domain-start') + def on_domain_started(self, event, **kwargs): + '''Connect this domain to its downstream domains. Also reload firewall +@@ -281,6 +296,7 @@ class NetVMMixin(qubes.events.Emitter): + + if self.netvm: + self.netvm.reload_firewall_for_vm(self) # pylint: disable=no-member ++ self.netvm.reload_routing_for_vm(self) # pylint: disable=no-member + + for vm in self.connected_vms: + if not vm.is_running(): +@@ -350,6 +366,20 @@ class NetVMMixin(qubes.events.Emitter): + + return self.netvm is not None + ++ def shutdown_routing_for_vm(self, vm): ++ self.reload_routing_for_vm(vm, True) ++ ++ def reload_routing_for_vm(self, vm, shutdown=False): ++ '''Reload the routing method for the VM.''' ++ if not self.is_running(): ++ return ++ for addr_family in (4, 6): ++ ip = vm.ip6 if addr_family == 6 else vm.ip ++ if ip is None: ++ continue ++ # report routing method ++ self.setup_non_masquerade_forwarding_for_vm(vm, ip, remove=shutdown) ++ + def reload_firewall_for_vm(self, vm): + ''' Reload the firewall rules for the vm ''' + if not self.is_running(): +@@ -370,6 +400,35 @@ class NetVMMixin(qubes.events.Emitter): + # signal its done + self.untrusted_qdb.write(base_dir[:-1], '') + ++ def setup_non_masquerade_forwarding_for_vm(self, vm, ip, remove=False): ++ ''' ++ Record in Qubes DB that the passed VM may be meant to have traffic ++ forwarded to and from it, rather than masqueraded from it and blocked ++ to it. ++ ++ The relevant incantation on the command line to assign the forwarding ++ behavior is `qvm-features routing-method forward`. If the feature ++ is set on the TemplateVM upon which the VM is based, then that counts ++ as the forwarding method for the VM as well. ++ ++ The counterpart code in qubes-firewall handles setting up the NetVM ++ with the proper networking configuration to permit forwarding without ++ masquerading behavior. ++ ++ If `remove` is True, then we remove the respective routing method from ++ the Qubes DB instead. ++ ''' ++ if ip is None: ++ return ++ routing_method = vm.features.check_with_template('routing-method', 'masquerade') ++ base_file = '/qubes-routing-method/{}'.format(ip) ++ if remove: ++ self.untrusted_qdb.rm(base_file) ++ elif routing_method == 'forward': ++ self.untrusted_qdb.write(base_file, 'forward') ++ else: ++ self.untrusted_qdb.write(base_file, 'masquerade') ++ + def set_mapped_ip_info_for_vm(self, vm): + ''' + Set configuration to possibly hide real IP from the VM. +@@ -451,6 +510,7 @@ class NetVMMixin(qubes.events.Emitter): + ''' Reloads the firewall config for vm ''' + # pylint: disable=unused-argument + self.reload_firewall_for_vm(vm) ++ self.reload_routing_for_vm(vm) + + @qubes.events.handler('domain-qdb-create') + def on_domain_qdb_create(self, event): +@@ -461,6 +521,7 @@ class NetVMMixin(qubes.events.Emitter): + # keep in sync with on_firewall_changed + self.set_mapped_ip_info_for_vm(vm) + self.reload_firewall_for_vm(vm) ++ self.reload_routing_for_vm(vm) + + @qubes.events.handler('firewall-changed', 'domain-spawn') + def on_firewall_changed(self, event, **kwargs): +@@ -469,6 +530,7 @@ class NetVMMixin(qubes.events.Emitter): + if self.is_running() and self.netvm: + self.netvm.set_mapped_ip_info_for_vm(self) + self.netvm.reload_firewall_for_vm(self) # pylint: disable=no-member ++ self.netvm.reload_routing_for_vm(self) # pylint: disable=no-member + + # CORE2: swallowed get_firewall_conf, write_firewall_conf, + # get_firewall_defaults diff --git a/src/qubes-routing-manager b/src/qubes-routing-manager new file mode 100755 index 0000000..79e31f3 --- /dev/null +++ b/src/qubes-routing-manager @@ -0,0 +1,225 @@ +#!/usr/bin/python2 + +''' +This program reads the /qubes-firewall/{ip}/qubes-routing-method file +for any firewall configuration, then configures the network to obey +the routing method for the VM. If the routing method is "masquerade", +then nothing happens. If, however, the routing method is "forward", +then VM-specific rules are enacted in the VM's attached NetVM to allow +traffic coming from other VMs and the outside world to reach this VM. +''' + +import glob +import logging +import qubesdb +import os +import subprocess +import socket + + +FORWARD_ROUTING_METHOD = 'forward' + + +class AdjunctWorker(object): + + def __init__(self): + self.qdb = qubesdb.QubesDB() + + @staticmethod + def is_ip6(addr): + return addr.count(':') > 0 + + def setup_plain_forwarding_for_address(self, source, enable, family): + + def find_pos_of_first_rule(table, startswith): + rules = [n for n, l in enumerate(out) if l.startswith(startswith)] + if rules: + return rules[0] + return None + + cmd = 'ip6tables' if family == 6 else 'iptables' + mask = '/128' if family == 6 else '/32' + def run_ipt(*args): + return subprocess.check_call([cmd, '-w'] + list(args)) + + out = subprocess.check_output([cmd + '-save']).splitlines() + + if enable: + # Create necessary prerouting chain. + if not find_pos_of_first_rule(out, ':PR-PLAIN-FORWARDING - '): + run_ipt('-t', 'nat', '-N', 'PR-PLAIN-FORWARDING') + + # Route prerouting traffic to necessary chain. + if not find_pos_of_first_rule(out, "-A POSTROUTING -j PR-PLAIN-FORWARDING"): + rule_num = find_pos_of_first_rule(out, "-A POSTROUTING -j MASQUERADE") + if not rule_num: + # This table does not contain the masquerading rule. + # Accordingly, we will not do anything. + return + first_rule_num = find_pos_of_first_rule(out, "-A POSTROUTING") + pos = rule_num - first_rule_num + 1 + logging.info("Adding POSTROUTING chain PR-PLAIN-FORWARDING.") + run_ipt('-t', 'nat', '-I', 'POSTROUTING', str(pos), '-j', 'PR-PLAIN-FORWARDING') + + # Create necessary forward chain. + if not find_pos_of_first_rule(out, ':PLAIN-FORWARDING - '): + run_ipt('-t', 'filter', '-N', 'PLAIN-FORWARDING') + + # Route forward traffic to necessary chain. + if not find_pos_of_first_rule(out, "-A FORWARD -j PLAIN-FORWARDING"): + rule_num = find_pos_of_first_rule(out, "-A FORWARD -i vif+ -o vif+ -j DROP") + if not rule_num: + # This table does not contain the masquerading rule. + # Accordingly, we will not do anything. + return + first_rule_num = find_pos_of_first_rule(out, "-A FORWARD") + pos = rule_num - first_rule_num + 1 + logging.info("Adding FORWARD chain PLAIN-FORWARDING.") + run_ipt('-t', 'filter', '-I', 'FORWARD', str(pos), '-j', 'PLAIN-FORWARDING') + + rule = find_pos_of_first_rule( + out, + '-A PR-PLAIN-FORWARDING -s {}{} -j ACCEPT'.format(source, mask) + ) + if enable: + if rule: + pass + else: + logging.info("Adding POSTROUTING rule to forward traffic from %s.", source) + run_ipt( + '-t', 'nat', '-A', + 'PR-PLAIN-FORWARDING', '-s', '{}{}'.format(source, mask), + '-j', 'ACCEPT' + ) + else: + if rule: + first_rule = find_pos_of_first_rule(out, '-A PR-PLAIN-FORWARDING') + pos = rule - first_rule + 1 + logging.info("Removing POSTROUTING rule forwarding traffic from %s.", source) + run_ipt('-t', 'nat', '-D', 'PR-PLAIN-FORWARDING', str(pos)) + else: + pass + + rule = find_pos_of_first_rule( + out, + '-A PLAIN-FORWARDING -d {}{} -o vif+ -j ACCEPT'.format(source, mask) + ) + if enable: + if rule: + pass + else: + logging.info("Adding FORWARD rule to allow traffic to %s.", source) + run_ipt( + '-t', 'filter', '-A', + 'PLAIN-FORWARDING', '-d', '{}{}'.format(source, mask), + '-o', 'vif+', '-j', 'ACCEPT' + ) + else: + if rule: + logging.info("Removing FORWARD rule allowing traffic to %s.", source) + first_rule = find_pos_of_first_rule(out, '-A PLAIN-FORWARDING') + pos = rule - first_rule + 1 + run_ipt('-t', 'filter', '-D', 'PLAIN-FORWARDING', str(pos)) + else: + pass + + def setup_proxy_arp_ndp(self, enabled, family): + # If any of the IP addresses is assigned the forward routing method, + # then enable proxy ARP/NDP on the upstream interfaces, so that the + # interfaces in question will impersonate the IP addresses in question. + # Ideally, this impersonation would be exclusively done for the + # specific IP addresses in question, but it is not clear to me how + # to cause this outcome to take place. + if family == 6: + globber = '/proc/sys/net/ipv6/conf/*/proxy_ndp' + name = 'proxy NDP' + elif family == 4: + globber = '/proc/sys/net/ipv4/conf/*/proxy_arp' + name = 'proxy ARP' + else: + return + + if enabled: + action = 'Enabling' + val = '1\n' + else: + action = 'Disabling' + val = '0\n' + + matches = glob.glob(globber) + for m in matches: + iface = m.split('/')[6] + if iface in ('all', 'lo') or iface.startswith('vif'): + # No need to enable it for "all", or VIFs, or loopback. + continue + with open(m, 'w+') as f: + oldval = f.read() + f.seek(0) + if oldval != val: + logging.info('%s %s on interface %s.', action, name, iface) + f.write(val) + + def handle_addr(self, addr): + # Setup plain forwarding for this specific address. + routing_method = self.qdb.read('/qubes-routing-method/{}'.format(addr)) + self.setup_plain_forwarding_for_address( + addr, + routing_method == FORWARD_ROUTING_METHOD, + 6 if self.is_ip6(addr) else 4 + ) + + # Manipulate proxy ARP for all known addresses. + methods = [ + (k.split('/')[2], v) for k, v in self.qdb.multiread('/qubes-routing-method/').items() + ] + mmethods = { + 4: [m[1] for m in methods if not self.is_ip6(m[0])], + 6: [m[1] for m in methods if self.is_ip6(m[0])], + } + for family, methods in mmethods.items(): + self.setup_proxy_arp_ndp( + FORWARD_ROUTING_METHOD in methods, + family, + ) + + def list_targets(self): + return set(t.split('/')[2] for t in self.qdb.list('/qubes-routing-method/')) + + def sd_notify(self, state): + '''Send notification to systemd, if available''' + # based on sdnotify python module + if not 'NOTIFY_SOCKET' in os.environ: + return + addr = os.environ['NOTIFY_SOCKET'] + if addr[0] == '@': + addr = '\0' + addr[1:] + try: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + sock.connect(addr) + sock.sendall(state.encode()) + except: + # generally ignore error on systemd notification + pass + + def main(self): + logging.basicConfig(level=logging.INFO) + self.qdb.watch('/qubes-routing-method/') + for source_addr in self.list_targets(): + self.handle_addr(source_addr) + self.sd_notify('READY=1') + 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 + return + + +if __name__ == "__main__": + w = AdjunctWorker() + w.main() diff --git a/src/qubes-routing-manager.service.in b/src/qubes-routing-manager.service.in new file mode 100644 index 0000000..b294be9 --- /dev/null +++ b/src/qubes-routing-manager.service.in @@ -0,0 +1,13 @@ +[Unit] +Description=Configure the network to allow network server VMs +Documentation=https://github.com/Rudd-O/qubes-network-server +ConditionPathExists=/var/run/qubes-service/qubes-firewall +After=qubes-firewall.service +BindsTo=qubes-firewall.service + +[Service] +Type=notify +ExecStart=@SBINDIR@/qubes-routing-manager + +[Install] +WantedBy=multi-user.target diff --git a/src/usr/bin/qvm-static-ip b/src/usr/bin/qvm-static-ip deleted file mode 100644 index df00de1..0000000 --- a/src/usr/bin/qvm-static-ip +++ /dev/null @@ -1,170 +0,0 @@ -#!/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 deleted file mode 100644 index c92c3d5..0000000 --- a/src/usr/lib64/python2.7/site-packages/qubes/modules/001FortressQubesVm.py +++ /dev/null @@ -1,449 +0,0 @@ -#!/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_qrexec_daemon(self, verbose=False, notify_function=None): - ret = OriginalQubesVm.start_qrexec_daemon(self, verbose=verbose, notify_function=notify_function) - if self.type in ['AppVM', 'HVM']: - self.deploy_appvm_firewall(verbose=verbose, notify_function=notify_function) - self.adjust_proxy_arp(verbose=verbose, notify_function=notify_function) - self.adjust_own_firewall_rules() - return ret - - 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 = "" - 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)) - 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 "" - return self.addrule + "\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() - - def deploy_appvm_firewall(self, verbose = False, notify_function=None): - # FIXME FIXME FIXME! - # - # Finish porting all code here that sets rules in AppVMs to - # use this daemon instead, so that rules can be configured - # to work properly without bullshit of any kind. - # - # See 007FortressQubesProxyVm.py code for where that may - # happen, as well as any place where FORTRESS-INPUT appears - # or is involved. - # - # Maybe: templatize qubes-appvm-firewall so that the template - # can take the name of the chain and the name of the key - # from this upstream program which deploys it into VMs. - def n(msg): - if notify_function: - notify_function("info", msg) - elif verbose: - print >> sys.stderr, "-->", msg - - n("Deploying AppVM firewall...") - - appvm_firewall_path = os.path.join( - os.path.dirname(__file__), - "qubes-appvm-firewall" - ) - pill = textwrap.dedent( - """ - set -e - tmp=$(mktemp) - trap 'rm -f "$tmp"' EXIT - cat > "$tmp" << "EOF" - %s - EOF - chmod +x "$tmp" - "$tmp" deploy 2>&1 - """ - ) % open(appvm_firewall_path).read() - - try: - p = self.run("bash", user="root", gui=False, wait=True, passio_popen=True, autostart=False) - p.stdin.write(pill) - p.stdin.close() - out = p.stdout.read() - retcode = p.wait() - except Exception as e: - n("Could not deploy the AppVM firewall on the VM: %s" % e) - if retcode != 0: - n("Could not deploy the AppVM firewall on the VM (return status %s): %s" % (retcode, out)) - - -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 deleted file mode 100644 index e69443f..0000000 --- a/src/usr/lib64/python2.7/site-packages/qubes/modules/006FortressQubesNetVm.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/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 deleted file mode 100644 index b73c479..0000000 --- a/src/usr/lib64/python2.7/site-packages/qubes/modules/007FortressQubesProxyVm.py +++ /dev/null @@ -1,193 +0,0 @@ -#!/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"]: - is_inbound = rule["address"].startswith("from-") and getattr(vm, "static_ip", None) - if is_inbound: - src_addr = rule["address"][len("from-"):] - src_mask = rule["netmask"] - dst_addr = ip - dst_mask = 32 - else: - src_addr = ip - src_mask = 32 - dst_addr = rule["address"] - dst_mask = rule["netmask"] - - args = [] - - def constrain(sd, addr, mask): - if mask != 0: - if mask == 32: - args.append("{0} {1}".format(sd, addr)) - else: - args.append("{0} {1}/{2}".format(sd, addr, mask)) - - constrain("-s", src_addr, src_mask) - constrain("-d", dst_addr, dst_mask) - - if rule["proto"] is not None and rule["proto"] != "any": - args.append("-p {0}".format(rule["proto"])) - if rule["portBegin"] is not None and rule["portBegin"] > 0: - if rule["portEnd"] is not None and rule["portEnd"] > rule["portBegin"]: - portrange = "{0}:{1}".format(rule["portBegin"], rule["portEnd"]) - else: - portrange = rule["portBegin"] - args.append("--dport {0}".format(portrange)) - - args.append("-j {0}".format(rules_action)) - ruletext = ' '.join(args) - - iptables += "-A PR-QBS-FORWARD {0}\n".format(ruletext) - if is_inbound: - vm_iptables += "-A FORTRESS-INPUT {0}\n".format(ruletext) - - 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) diff --git a/src/usr/lib64/python2.7/site-packages/qubes/modules/qubes-appvm-firewall b/src/usr/lib64/python2.7/site-packages/qubes/modules/qubes-appvm-firewall deleted file mode 100755 index 9d14b6a..0000000 --- a/src/usr/lib64/python2.7/site-packages/qubes/modules/qubes-appvm-firewall +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/env python - -''' -This code is intended to replace the very fragile firewall generation code -that currently runs on dom0, by a lightweight daemon that applies the rules -on the AppVM (with static IP), responding to rule changes made by the -administrator on-the-fly. - -This daemon is injected into the VM as soon as qrexec capability becomes -available on the recently-started VM. The daemon: - -1. Reads the QubesDB key /qubes-fortress-iptables-rules. -2. Atomically applies the rules therein saved therein. - -The rules in /qubes-fortress-iptables-rules are generated by the dom0 code -in 007FortressQubesProxyVM, which in turn are based on the firewall rules -that the administrator has configured. These rules are generated and applied -at the same time as the rules generated and applied on the ProxyVM attached to -the AppVM, ensuring that the rules in the VM are kept in sync with the rules -in the ProxyVM at all times. - -FIXME: The previous paragraph is still a work in progress. -''' - -import collections -import logging -import os -import shutil -import subprocess -import sys - -NAME = "qubes-appvm-firewall" -UNITDIRS = ["/usr/lib/systemd/system", "/lib/systemd/system"] -DEPDIR = "/run/fortress" -KEY = '/qubes-fortress-iptables-rules' -CHAIN = 'FORTRESS-INPUT' - - -class ReadError(Exception): - pass - - -def watch(key): - subprocess.check_call(['qubesdb-watch', key]) - - -def read(key): - try: - return subprocess.check_output(['qubesdb-read', '-r', key]) - except subprocess.CalledProcessError as e: - logging.error("error reading key %s: %s", key, e) - raise ReadError() - - -class Table(object): - - header = None - chains = None - rules = None - footer = None - original_chains = None - original_rules = None - - def __init__(self, text): - lines = text.splitlines(True) - self.header = '' - self.chains = collections.OrderedDict() - self.rules = [] - self.footer = '' - mode = "header" - for line in lines: - if mode == "header": - if line.startswith(":"): - self.chains.update([line[1:].split(" ", 1)]) - mode = "chains" - else: - self.header += line - elif mode == "chains": - if line.startswith("-"): - self.rules.append(line) - mode = "rules" - else: - self.chains.update([line[1:].split(" ", 1)]) - elif mode == "rules": - if line.startswith("COMMIT"): - self.footer += line - mode = "footer" - else: - self.rules.append(line) - else: # mode == "footer": - self.footer += line - self.original_chains = collections.OrderedDict(self.chains.items()) - self.original_rules = list(self.rules) - - def __str__(self): - return self.render() - - def render(self, old=False): - if old: - chains = self.original_chains - rules = self.original_rules - else: - chains = self.chains - rules = self.rules - return ( - self.header - + "".join(":%s %s" % x for x in chains.items()) - + "".join(rules) - + self.footer - ) - - def dirty(self): - return self.render() != self.render(True) - - def ensure_chain_present(self, name): - if name not in self.chains: - logging.info("Adding chain %s", name) - self.chains[name] = '- [0:0]\n' - - def clear_chain(self, name): - for n, rule in reversed(list(enumerate(self.rules))): - if rule.startswith("-A %s " % name): - self.rules.pop(n) - - def add_rule(self, rule, after_rule): - original_after_rule = after_rule - if not rule.endswith("\n"): - rule += "\n" - if not after_rule.endswith("\n"): - after_rule += "\n" - inserted = False - if rule in self.rules: - return - for n, exrule in enumerate(self.rules): - if exrule == after_rule: - logging.info("Inserting rule %s", rule.strip()) - self.rules.insert(n + 1, rule) - inserted = True - break - if not inserted: - logging.error("Could not insert rule %s", rule.strip()) - raise KeyError(original_after_rule) - - def replace_rules(self, chain, ruletext): - for rule in ruletext.splitlines(): - if not rule.strip(): continue - if not rule.startswith("-A %s " % chain): - raise ValueError( - "rule %s is not for chain %s" % ( - rule.strip(), - chain, - ) - ) - self.ensure_chain_present(chain) - self.clear_chain(chain) - for rule in ruletext.splitlines(): - if rule.startswith("-A %s " % chain): - self.rules.append(rule + "\n") - - def commit(self): - if not self.dirty(): - return - text = self.render() - cmd = ['iptables-restore'] - p = subprocess.Popen( - cmd, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - out, _ = p.communicate(text) - w = p.wait() - if w != 0: - logging.error("Rule changes commit failed with status %s: %s", w, out) - raise subprocess.CalledProcessError(w, cmd, out) - self.original_chains = collections.OrderedDict(self.chains.items()) - self.original_rules = list(self.rules) - logging.info("Rule changes committed") - - @classmethod - def filter_from_iptables(klass): - r = subprocess.check_output(['iptables-save', '-t', 'filter']) - t = klass(r) - return t - -def deploy(): - deppath = os.path.join(DEPDIR, NAME) - if not os.path.isdir(DEPDIR): - os.makedirs(DEPDIR) - shutil.copyfile(__file__, deppath) - os.chmod(deppath, 0755) - service = '''[Unit] -Description=Qubes AppVM firewall updater -After=qubes-iptables.service qubes-firewall.service -Before=qubes-network.service network.target - -[Service] -Type=simple -ExecStart=%s main -''' % deppath - for unitdir in UNITDIRS: - if os.path.isdir(unitdir): break - unitpath = os.path.join(unitdir, NAME + ".service") - if not os.path.isfile(unitpath) or open(unitpath, "rb").read() != service: - open(unitpath, "wb").write(service) - subprocess.check_call(['systemctl', '--system', 'daemon-reload']) - subprocess.check_call(['systemctl', 'restart', os.path.basename(unitpath)]) - - -def main(): - logging.basicConfig(level=logging.INFO) - t = Table.filter_from_iptables() - t.ensure_chain_present(CHAIN) - t.add_rule('-A INPUT -j %s' % CHAIN, '-A INPUT -i lo -j ACCEPT') - try: - newrules = read(KEY) - t.replace_rules(CHAIN, newrules) - except ReadError: - # Key may not exist at this time. - logging.warning("Qubes DB key %s does not yet exist", KEY) - t.commit() - logging.info("Startup complete") - while True: - watch(KEY) - try: - newrules = read(KEY) - except ReadError: - # Key may have been deleted. - logging.warning("Qubes DB key %s could not be read", KEY) - continue - logging.info("Rule changes detected") - try: - t.replace_rules(CHAIN, newrules) - t.commit() - except Exception: - logging.exception("Rule changes could not be committed") - - -try: - cmd = sys.argv[1] -except IndexError: - cmd = 'main' -cmd = locals()[cmd] -sys.exit(cmd())