mirror of
				https://github.com/Rudd-O/qubes-network-server.git
				synced 2025-10-30 19:19:07 +01:00 
			
		
		
		
	Merge branch 'r4.0'
This commit is contained in:
		
						commit
						49ab132bf5
					
				
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -6,3 +6,6 @@ pkgs/ | |||||||
| *.tar.gz | *.tar.gz | ||||||
| *.rpm | *.rpm | ||||||
| .*.swp | .*.swp | ||||||
|  | build | ||||||
|  | *.egg-info | ||||||
|  | src/*.service | ||||||
|  | |||||||
| @ -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 |  | ||||||
							
								
								
									
										48
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								Makefile
									
									
									
									
									
								
							| @ -1,23 +1,43 @@ | |||||||
| BINDIR=/usr/bin | SBINDIR=/usr/local/sbin | ||||||
| LIBDIR=/usr/lib64 | UNITDIR=/etc/systemd/system | ||||||
| DESTDIR= | DESTDIR= | ||||||
| PROGNAME=qubes-network-server | PROGNAME=qubes-network-server | ||||||
|  | PYTHON=/usr/bin/python3 | ||||||
|  | 
 | ||||||
|  | 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-template install-dom0 | ||||||
| 
 | 
 | ||||||
| clean: | clean: | ||||||
| 	find -name '*.pyc' -o -name '*~' -print0 | xargs -0 rm -f | 	cd $(ROOT_DIR) || exit $$? ; find -name '*.pyc' -o -name '*~' -print0 | xargs -0 rm -f | ||||||
| 	rm -f *.tar.gz *.rpm | 	cd $(ROOT_DIR) || exit $$? ; rm -rf *.tar.gz *.rpm | ||||||
|  | 	cd $(ROOT_DIR) || exit $$? ; rm -rf *.egg-info build | ||||||
| 
 | 
 | ||||||
| dist: clean | 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 * | 	@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 * | ||||||
| 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" |  | ||||||
| 
 | 
 | ||||||
| srpm: dist | 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: | rpm: dist | ||||||
| 	install -Dm 755 src/usr/bin/qvm-static-ip -t $(DESTDIR)/$(BINDIR)/ | 	@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 ; } | ||||||
| 	install -Dm 644 src/usr/lib64/python2.7/site-packages/qubes/modules/*.py -t $(DESTDIR)/$(LIBDIR)/python2.7/site-packages/qubes/modules | 	cd $(ROOT_DIR) || exit $$? ; rpmbuild --define "_srcrpmdir ." --define "_rpmdir builddir.rpm" -ta `rpmspec -q --queryformat '%{name}-%{version}.tar.gz\n' *spec | head -1` | ||||||
| 	install -Dm 644 src/usr/lib64/python2.7/site-packages/qubes/modules/qubes-appvm-firewall -t $(DESTDIR)/$(LIBDIR)/python2.7/site-packages/qubes/modules | 	cd $(ROOT_DIR) ; mv -f builddir.rpm/*/* . && rm -rf builddir.rpm | ||||||
|  | 
 | ||||||
|  | install-template: all | ||||||
|  | 	install -Dm 755 src/qubes-routing-manager -t $(DESTDIR)/$(SBINDIR)/ | ||||||
|  | 	sed -i "s,^#!.*,#!$(PYTHON)," $(DESTDIR)/$(SBINDIR)/qubes-routing-manager | ||||||
|  | 	install -Dm 644 src/qubes-routing-manager.service -t $(DESTDIR)/$(UNITDIR)/ | ||||||
|  | 
 | ||||||
|  | # Python 3 is always used for Qubes admin package.
 | ||||||
|  | install-dom0: | ||||||
|  | 	PYTHONDONTWRITEBYTECODE=1 python3 setup.py install $(PYTHON_PREFIX_ARG) -O0 --root $(DESTDIR) | ||||||
|  | 
 | ||||||
|  | install: install-dom0 install-template | ||||||
|  | |||||||
| @ -1,3 +1 @@ | |||||||
| ifeq ($(PACKAGE_SET),dom0) |  | ||||||
| RPM_SPEC_FILES=qubes-network-server.spec | RPM_SPEC_FILES=qubes-network-server.spec | ||||||
| endif |  | ||||||
|  | |||||||
							
								
								
									
										214
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										214
									
								
								README.md
									
									
									
									
									
								
							| @ -1,9 +1,12 @@ | |||||||
| # 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. | ||||||
| 
 | 
 | ||||||
| **Note**: this software only supports release 3 of Qubes OS.  For Qubes OS release 4.0 support, | **Note**: this software only supports release 4.0 of Qubes OS.  For Qubes OS release 3.2 support, | ||||||
| please see `r4.0` branch.  For Qubes OS release 4.1 support, please see `r4.1 branch`. | please see `release-3.2` branch.  For Qubes OS release 4.1 support, please see `r4.1 branch`. | ||||||
| 
 | 
 | ||||||
| ## Why? | ## Why? | ||||||
| 
 | 
 | ||||||
| @ -47,102 +50,189 @@ Qubes network server changes all that. | |||||||
| With the Qubes network server software, it becomes possible to make | With the Qubes network server software, it becomes possible to make | ||||||
| network servers in user VMs available to other machines, be them | network servers in user VMs available to other machines, be them | ||||||
| peer VMs in the same Qubes OS system or machines connected to | 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 | a physical link shared by a NetVM.  Those network server VMs also | ||||||
| over network traffic, both exiting the VM and entering the VM, with | obey the Qubes OS outbound firewall rules controls, letting you run | ||||||
| exactly the same Qubes OS user experience you are used to. | services with outbound connections restricted. | ||||||
| 
 | 
 | ||||||
| This is all, of course, opt-in, so the standard Qubes OS network security | 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. | ||||||
|  | 
 | ||||||
|  | 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 | ## How to use this software | ||||||
| 
 | 
 | ||||||
| Once installed (see below), usage of the software is straightforward. | 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 your first server](doc/Setting up your first server.md) | ||||||
| * [Setting up an SSH server](doc/Setting up an SSH server.md) | * [Setting up an SSH server](doc/Setting up an SSH server.md) | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| ## Installation | ## Installation | ||||||
| 
 | 
 | ||||||
| Installation is straightforward — build package, copy to dom0, | Installation consists of two steps: | ||||||
| install in dom0.  Here are step by step instructions: |  | ||||||
| 
 | 
 | ||||||
| * Install the `rpm-build` package on your build machine | 1. Deploy the `qubes-core-admin-addon-network-server` RPM to your `dom0`. | ||||||
|   with `sudo dnf install rpm-build`.  Remember that if your | 2. Deploy the `qubes-network-server` RPM to the TemplateVM backing your | ||||||
|   build machine is an AppVM or any other sort of VM that boots |    NetVM (which must be a Fedora instance).  If your NetVM is a StandaloneVM, | ||||||
|   from a template, you may want to run that `dnf` command on the |    then you must deploy this RPM to the NetVM directly. | ||||||
|   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 <RPM file name you just copied>`. |  | ||||||
| * 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. |  | ||||||
| 
 | 
 | ||||||
| Qubes OS does not provide any facility to copy files from | After that, to make it all take effect: | ||||||
| a VM to the dom0.  To work around this, you can use `qvm-run`: | 
 | ||||||
|  | 1. Power off the TemplateVM. | ||||||
|  | 2. Reboot the NetVM. | ||||||
|  | 
 | ||||||
|  | You're done.  You can verify that the necessary component is running by launching | ||||||
|  | a terminal in your NetVM, then typing the following: | ||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
| qvm-run --pass-io vmwiththerpm 'cat /home/user/path/to/qubes-network-server*rpm' > qns.rpm | systemctl status qubes-routing-manager.service | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| This lets you fetch the RPM file to the dom0, and save it as `qns.rpm`, | The routing manager should show as `enabled` and `active` in the terminal output. | ||||||
| which you can then feed as an argument to the `rpm -ivh` command. | 
 | ||||||
|  | ### How to build the packages to install | ||||||
|  | 
 | ||||||
|  | You will first build the `qubes-core-admin-addon-network-server` RPM. | ||||||
|  | 
 | ||||||
|  | To build this package, you will need to use a `chroot` jail containing | ||||||
|  | a Fedora installation of the exact same release as your `dom0` (Fedora 25 | ||||||
|  | for Qubes release 4.0, Fedora 31 for Qubes release 4.1). | ||||||
|  | 
 | ||||||
|  | Copy the source of the package to your `chroot`.  Then start a shell in | ||||||
|  | your `chroot`, and type `make rpm`.  You may have to install some packages | ||||||
|  | in your `chroot` -- use `dnf install git rpm-build make coreutils tar gawk findutils systemd systemd-rpm-macros` | ||||||
|  | to get the minimum dependency set installed. | ||||||
|  | 
 | ||||||
|  | Once built, in the source directory you will find the RPM built for the | ||||||
|  | exact release of Qubes you need. | ||||||
|  | 
 | ||||||
|  | Alternatively, you may first create a source RPM using `make srpm` on your | ||||||
|  | regular workstation, then use `mock` to rebuild the source RPM produced | ||||||
|  | in the source directory, using a Fedora release compatible with your `dom0`. | ||||||
|  | 
 | ||||||
|  | To build the `qubes-network-server` RPM, you can use a DisposableVM running | ||||||
|  | the same Fedora release as your NetVM.  Build said package as follows: | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | # Dependencies | ||||||
|  | dnf install git rpm-build make coreutils tar gawk findutils systemd systemd-rpm-macros | ||||||
|  | # Source code | ||||||
|  | cd /path/to/qubes-network-server | ||||||
|  | # <make sure you are in the correct branch now> | ||||||
|  | make rpm | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | The process will output a `qubes-network-server-*.noarch.rpm` in the | ||||||
|  | directory where it ran.  Fish it out and save it into the VM where you'll | ||||||
|  | install it. | ||||||
|  | 
 | ||||||
|  | You can power off the DisposableVM now. | ||||||
| 
 | 
 | ||||||
| ### Upgrading to new / bug-fixing releases | ### Upgrading to new / bug-fixing releases | ||||||
| 
 | 
 | ||||||
| Follow the same procedures as above, but when asked to install the package | Follow the same procedures as above, but when asked to install the packages | ||||||
| with `rpm -ivh`, change it to `rpm -Uvh` (uppercase U for upgrade). | with `rpm -ivh`, change it to `rpm -Uvh` (uppercase U for upgrade). | ||||||
| 
 | 
 | ||||||
|  | Always restart your NetVMs between upgrades. | ||||||
|  | 
 | ||||||
| ## Theory of operation | ## Theory of operation | ||||||
| 
 | 
 | ||||||
| Qubes OS relies on layer 3 (IP) routing.  VMs in its networked tree send traffic through | Qubes OS relies on layer 3 (IP) routing.  VMs in its networked tree send traffic through | ||||||
| their default route interfaces, which upstream VMs receive and masquerade out of their own | their default route interfaces, which upstream VMs receive and masquerade out of their own | ||||||
| default route interfaces. | default route interfaces. | ||||||
| 
 | 
 | ||||||
| Qubes network server slightly changes this when a networked VM — a VM which has had its | Qubes network server slightly changes this when a VM gets its `routing-method` feature set | ||||||
| `static_ip` attribute set with `qvm-static-ip` — exists on the networked tree.  As soon | to `forward`.  As soon as the feature is enabled with that value, or the VM in question | ||||||
| as a networked VM boots up, Qubes network server: | boots up, Qubes network server: | ||||||
| 
 | 
 | ||||||
| * sets a static `/32` route on every upstream VM to the networked VM's static IP, | * enables ARP neighbor proxying (and, if using IPv6, NDP neighbor proxying) in the NetVM | ||||||
|   directing the upstream VMs to route traffic for that IP to their VIFs where | * sets firewall rules in the NetVM that neuter IP masquerading on traffic coming from | ||||||
|   they may find the networked VM |   the networked VM | ||||||
| * enables ARP neighbor proxying for the static IP on every upstream VM, such that | * sets firewall rules in the NetVM that allow traffic from other VMs to reach the | ||||||
|   every upstream VM announces itself to their own upstream VMs (and LAN, in the |   networked VM, neutering the default Qubes OS rule that normally prohibits this | ||||||
|   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 |  | ||||||
| 
 | 
 | ||||||
| The end result is instantaneous networking — machines upstream from the networked VM, | The above have the effect of exposing the networked VM to: | ||||||
| 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. |  | ||||||
| 
 | 
 | ||||||
| Of course, LAN machines connecting to the networked VM believe that the networked VM | * other AppVMs attached to the same NetVM | ||||||
| possesses the MAC address of its upstream NetVM (just as if the upstream NetVM had a | * other machines attached to the same physical network the NetVM is attached to | ||||||
| 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 | Now all machines in the same LAN will be able to reach the networked VM. | ||||||
| network security policy, in that the networked VM appears (from a MAC perspective) | Here is a step-by-step explanation of how IP traffic to and from the networked | ||||||
| to share a network card with its upstream NetVM. | 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 | ## Limitations | ||||||
| 
 | 
 | ||||||
| * HVMs are not supported at all at this time.  This will change over time, and | * 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. |   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 | ## 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. | ||||||
|  | |||||||
							
								
								
									
										46
									
								
								TODO
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								TODO
									
									
									
									
									
								
							| @ -1,37 +1,13 @@ | |||||||
| To do list: | To do list: | ||||||
| 
 | 
 | ||||||
| * Make the system do the right thing (withdraw ip neigh / | * Package up `dom0` component so it's installable via | ||||||
|   ip route / iptables rules) when VMs power off or when |   RPM.  Alternatively, upstream it completely. | ||||||
|   their network gets detached. | * Make the system more robust by setting the right | ||||||
|   Right now the rules are only reconfigured when: |   `ip neigh / ip route` rules to force incoming traffic | ||||||
|   * a VM starts (ancestor VMs get reconfigured) |   to go to the specific VIF that backs the exposed VM. | ||||||
|   * a VM gets unpaused (same as before) | * Instead of / in addition to proxy ARP/NDP, use static | ||||||
|   * a VM network gets attached (same as before) |   MAC addresses set at runtime, for each VM. | ||||||
|   * a VM's FW rules get altered (parent ProxyVM and sibling | * Support interposing ProxyVMs between NetVMs and AppVMs. | ||||||
|     VMs get reconfigured, and this reconfiguration only | * (Maybe) set up firewall rules on AppVM to obey its designated | ||||||
|     affects iptables rules) |   firewall rules, bringing back support for the GUI.  This | ||||||
| * Make the system do the right thing when `static_ip` |   probably needs a conversation with the Qubes OS core devs. | ||||||
|   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. |  | ||||||
|  | |||||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.1 KiB | 
| @ -14,86 +14,73 @@ First of all, [install Qubes network server](https://github.com/Rudd-O/qubes-net | |||||||
| 
 | 
 | ||||||
| ## Set up needed VMs | ## 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. | 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`. |    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 | 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. |    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, | 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 | start the StandaloneVM `exp-ssh` you created, and then verify that networking | ||||||
| can ping your manager machine from it. | works within `exp-ssh`. | ||||||
| 
 | 
 | ||||||
| Power off `exp-manager` when your test is complete. | ## Set static address on `exp-ssh` | ||||||
| 
 |  | ||||||
| ## Set static address on `exp-manager` |  | ||||||
| 
 | 
 | ||||||
| On your server's `dom0`, run the command: | 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 | `x.y.z.w` must be an IP address available on the same network that both | ||||||
| your `exp-net` and your manager machine share. | your `exp-net` and your manager machine share. | ||||||
| 
 | 
 | ||||||
| Power `exp-manager` back on, and verify that you can still ping your | Shut down `exp-ssh` back on, start it back up again, | ||||||
| manager machine from it. | 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. | 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 | to set up a firewall rule permitting only SSH access from the manager | ||||||
| machine, and denying all other access to anyone. | machine, and denying all other access to anyone. | ||||||
| 
 | 
 | ||||||
| If you are new to firewall rules in Qubes, [check out this quite | [See the documentation for Qubes OS](https://www.qubes-os.org/doc/firewall/#where-to-put-firewall-rules) | ||||||
| good overview of them](https://www.qubes-os.org/doc/qubes-firewall/). | to understand more about firewalls in AppVMs | ||||||
| 
 | 
 | ||||||
| Launch the Qubes Manager preferences window for the `exp-manager` VM. | ## Enable and start SSH on the `exp-ssh` VM | ||||||
| Go to the *Firewall rules* tab and select *Deny network access |  | ||||||
| except...* from the top area. |  | ||||||
| 
 | 
 | ||||||
| Add a new network rule (use the plus button).  On the *Address* box, | In a terminal window of `exp-ssh`, run: | ||||||
| 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: |  | ||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
| sudo systemctl enable sshd.service | sudo systemctl enable --now sshd.service | ||||||
| sudo systemctl start 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 | 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. | no password is set up, but we will fix that shortly. | ||||||
| 
 | 
 | ||||||
| ## Set up SSH authentication | ## 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 | 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 | 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. | 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 | sudo passwd -d user | ||||||
| @ -110,7 +97,7 @@ sudo passwd -l user | |||||||
| 
 | 
 | ||||||
| Good news!  You can now remotely log in, from your manager machine, | 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 | 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, | 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). | 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). | ||||||
|  | |||||||
| @ -2,9 +2,8 @@ | |||||||
| 
 | 
 | ||||||
| To illustrate, we'll proceed with an example VM `httpserver` which | To illustrate, we'll proceed with an example VM `httpserver` which | ||||||
| is meant to be a standalone VM that contains files, being served by | 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 running HTTP server (port 80) within it.  This VM is attached to a | ||||||
| a ProxyVM `server-proxy`, which in turn is connected to a NetVM | NetVM `sys-net`, with IP address `192.168.1.4` on a local network | ||||||
| `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 | `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 | to your laptop on the same physical network, which we'll assume has | ||||||
| IP address `192.168.1.8`. | 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`: | to `httpserver`: | ||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
| qvm-static-ip -s httpserver static_ip 192.168.1.6 | qvm-prefs -s httpserver ip 192.168.1.6 | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ##Restart VM | ##Restart VM | ||||||
| 
 | 
 | ||||||
| Due to limitations in this release of the code, you must power off | Due to limitations in how the IP address is set on the VM, you must | ||||||
| the `httpserver` VM and then power it back on. | 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 | ##Set firewall rules on VM | ||||||
| 
 | 
 | ||||||
| If you are new to firewall rules in Qubes, [check out this quite | The normal way to set up AppVM firewall rules is | ||||||
| good overview of them](https://www.qubes-os.org/doc/qubes-firewall/). | [documented here](https://www.qubes-os.org/doc/firewall/#where-to-put-firewall-rules). | ||||||
| 
 | 
 | ||||||
| Launch the Qubes Manager preferences window for the `httpserver` VM. | For the purposes of this demo, all you have to run inside `httpserver` | ||||||
| Go to the *Firewall rules* tab and select *Deny network access | is this: | ||||||
| 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 | sudo iptables -I INPUT 1 -p tcp --dport 8080 -j ACCEPT | ||||||
| the *TCP* protocol, and type `80` on the *Service* box.  Click OK. | ``` | ||||||
| 
 | 
 | ||||||
| Note the trick here — any address whose text begins with | (This method of setting firewall rules makes them go away when you | ||||||
| `from-` gets transformed into an incoming traffic rule, as opposed | restart the AppVM.  Refer to the link in this section to make them | ||||||
| to the standard rules that control only outbound traffic. | stick around after a VM restart.) | ||||||
| 
 | 
 | ||||||
| **Security note**: the default "allow all" firewall leaves all ports | ## Start a Python HTTP server | ||||||
| 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. |  | ||||||
| 
 | 
 | ||||||
| Back on the main dialog, click *OK*. | In your `httpserver` VM, run: | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | python3 -m http.server | ||||||
|  | ``` | ||||||
| 
 | 
 | ||||||
| ##That's it! | ##That's it! | ||||||
| 
 | 
 | ||||||
| You'll be able to ping, from your laptop, the address `192.168.1.6`. | You will now be able to point your browser at http://192.168.1.6:8080/, | ||||||
| You will also be able to point your browser at it, and it will | and it will render the served pages from the HTTP server running | ||||||
| render the served pages from the HTTP server running directly on | directly on `httpserver`. | ||||||
| `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. |  | ||||||
| 
 | 
 | ||||||
| ##Inter-VM network communication | ##Inter-VM network communication | ||||||
| 
 | 
 | ||||||
| This software isn't limited to just letting network servers be | This software isn't limited to just letting network servers be | ||||||
| accessible from your physical network.  VMs can talk among each | accessible from your physical network.  VMs can talk among each | ||||||
| other too.  Simple instructions: | other too.  A pair of VMs whose feature `routing-method` has been | ||||||
| 
 | set to `forward` are authorized to talk to each other over the | ||||||
| * Set up a static IP address for each VM. | network, so long as they are attached to the same NetVM. | ||||||
| * 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. |  | ||||||
| 
 | 
 | ||||||
| ##Disabling network server | ##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. | ||||||
|  | |||||||
| @ -1,11 +1,9 @@ | |||||||
| %{!?python2_sitearch: %define python2_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)")} |  | ||||||
| 
 |  | ||||||
| %define debug_package %{nil} | %define debug_package %{nil} | ||||||
| 
 | 
 | ||||||
| %define mybuildnumber %{?build_number}%{?!build_number:1} | %define mybuildnumber %{?build_number}%{?!build_number:1} | ||||||
| 
 | 
 | ||||||
| Name:           qubes-network-server | Name:           qubes-network-server | ||||||
| Version:        0.0.10 | Version:        0.0.11 | ||||||
| Release:        %{mybuildnumber}%{?dist} | Release:        %{mybuildnumber}%{?dist} | ||||||
| Summary:        Turn your Qubes OS into a network server | Summary:        Turn your Qubes OS into a network server | ||||||
| BuildArch:      noarch | BuildArch:      noarch | ||||||
| @ -17,32 +15,109 @@ Source0:	https://github.com/Rudd-O/%{name}/archive/{%version}.tar.gz#/%{name}-%{ | |||||||
| BuildRequires:  make | BuildRequires:  make | ||||||
| BuildRequires:  coreutils | BuildRequires:  coreutils | ||||||
| BuildRequires:  tar | BuildRequires:  tar | ||||||
| BuildRequires:  gawk |  | ||||||
| BuildRequires:  findutils | BuildRequires:  findutils | ||||||
|  | %if 1%{?fc30} == 11 | ||||||
|  | BuildRequires:  python2 | ||||||
|  | BuildRequires:  python2-rpm-macros | ||||||
|  | %global pythoninterp %{_bindir}/python2 | ||||||
|  | %else | ||||||
|  | BuildRequires:  python3 | ||||||
|  | BuildRequires:  python3-rpm-macros | ||||||
|  | %global pythoninterp %{_bindir}/python3 | ||||||
|  | %endif | ||||||
| 
 | 
 | ||||||
| Requires:       qubes-core-dom0 | %if 1%{?fc25} == 1 | ||||||
|  | BuildRequires:  systemd-rpm-macros | ||||||
|  | %else | ||||||
|  | %global _presetdir %{_prefix}/lib/systemd/system-preset | ||||||
|  | %global _unitdir %{_prefix}/lib/systemd/system | ||||||
|  | %endif | ||||||
|  | 
 | ||||||
|  | Requires:       qubes-core-agent-networking >= 4.0.51-1 | ||||||
|  | Conflicts:      qubes-core-agent-networking >= 4.1 | ||||||
|  | Requires:       python2 | ||||||
|  | Requires:       python2-qubesdb | ||||||
| 
 | 
 | ||||||
| %description | %description | ||||||
| This package lets you turn your Qubes OS into a network server. | This package lets you turn your Qubes OS into a network server.  Install this | ||||||
|  | in the TemplateVM of your NetVM.  Then install the companion | ||||||
|  | qubes-core-admin-addon-network-server package in your dom0. | ||||||
|  | 
 | ||||||
|  | Please see README.md enclosed in the package for instructions on how to use | ||||||
|  | this software. | ||||||
|  | 
 | ||||||
|  | %package -n     qubes-core-admin-addon-network-server | ||||||
|  | Summary:        dom0 administrative extension for Qubes network server | ||||||
|  | 
 | ||||||
|  | BuildRequires:  make | ||||||
|  | BuildRequires:  coreutils | ||||||
|  | BuildRequires:  tar | ||||||
|  | BuildRequires:  findutils | ||||||
|  | BuildRequires:  python3 | ||||||
|  | BuildRequires:  python3-rpm-macros | ||||||
|  | 
 | ||||||
|  | Requires:       python3 | ||||||
|  | Requires:       qubes-core-dom0 >= 4.0.49-1 | ||||||
|  | Conflicts:      qubes-core-dom0 >= 4.1 | ||||||
|  | 
 | ||||||
|  | %description -n qubes-core-admin-addon-network-server | ||||||
|  | This package lets you turn your Qubes OS into a network server.  Install this | ||||||
|  | in your dom0.  Then install the companion qubes-network-server package in the | ||||||
|  | TemplateVM of your NetVM. | ||||||
|  | 
 | ||||||
|  | Please see README.md enclosed in the package for instructions on how to use | ||||||
|  | this software. | ||||||
| 
 | 
 | ||||||
| %prep | %prep | ||||||
| %setup -q | %setup -q | ||||||
| 
 | 
 | ||||||
| %build | %build | ||||||
| # variables must be kept in sync with install | # 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} PYTHON=%{pythoninterp} | ||||||
| 
 | 
 | ||||||
| %install | %install | ||||||
| rm -rf $RPM_BUILD_ROOT | rm -rf $RPM_BUILD_ROOT | ||||||
| # variables must be kept in sync with build | # 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} PYTHON=%{pythoninterp} | ||||||
|  | mkdir -p "$RPM_BUILD_ROOT"/%{_presetdir} | ||||||
|  | echo 'enable qubes-routing-manager.service' > "$RPM_BUILD_ROOT"/%{_presetdir}/75-%{name}.preset | ||||||
| 
 | 
 | ||||||
| %files | %files | ||||||
| %attr(0755, root, root) %{_bindir}/qvm-static-ip | %attr(0755, root, root) %{_sbindir}/qubes-routing-manager | ||||||
| %attr(0644, root, root) %{python2_sitearch}/qubes/modules/*.py* | %attr(0644, root, root) %{_presetdir}/75-%{name}.preset | ||||||
| %attr(0644, root, root) %{python2_sitearch}/qubes/modules/qubes-appvm-firewall | %config %attr(0644, root, root) %{_unitdir}/qubes-routing-manager.service | ||||||
| %doc README.md TODO | %doc README.md TODO | ||||||
| 
 | 
 | ||||||
|  | %files -n       qubes-core-admin-addon-network-server | ||||||
|  | %attr(0644, root, root) %{python3_sitelib}/qubesnetworkserver | ||||||
|  | %{python3_sitelib}/qubesnetworkserver-*.egg-info | ||||||
|  | 
 | ||||||
|  | %post | ||||||
|  | %systemd_post qubes-routing-manager.service | ||||||
|  | 
 | ||||||
|  | %preun | ||||||
|  | %systemd_preun qubes-routing-manager.service | ||||||
|  | 
 | ||||||
|  | %postun | ||||||
|  | %systemd_postun_with_restart qubes-routing-manager.service | ||||||
|  | 
 | ||||||
|  | %post -n         qubes-core-admin-addon-network-server | ||||||
|  | %if 1%{?fc25} == 11 | ||||||
|  | systemctl try-restart qubesd.service | ||||||
|  | %else | ||||||
|  | %systemd_post qubesd.service | ||||||
|  | %endif | ||||||
|  | 
 | ||||||
|  | %postun -n       qubes-core-admin-addon-network-server | ||||||
|  | %if 1%{?fc25} == 11 | ||||||
|  | systemctl try-restart qubesd.service | ||||||
|  | %else | ||||||
|  | %systemd_postun_with_restart qubesd.service | ||||||
|  | %endif | ||||||
|  | 
 | ||||||
| %changelog | %changelog | ||||||
|  | * Mon Apr 13 2020 Manuel Amador (Rudd-O) <rudd-o@rudd-o.com> | ||||||
|  | - Update to Qubes 4.0 | ||||||
|  | 
 | ||||||
| * Tue Oct 11 2016 Manuel Amador (Rudd-O) <rudd-o@rudd-o.com> | * Tue Oct 11 2016 Manuel Amador (Rudd-O) <rudd-o@rudd-o.com> | ||||||
| - Initial release | - Initial release | ||||||
|  | |||||||
							
								
								
									
										98
									
								
								qubesnetworkserver/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								qubesnetworkserver/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,98 @@ | |||||||
|  | import qubes.ext | ||||||
|  | import qubes.vm.templatevm | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class QubesNetworkServerExtension(qubes.ext.Extension): | ||||||
|  | 
 | ||||||
|  |     def shutdown_routing_for_vm(self, netvm, appvm): | ||||||
|  |         self.reload_routing_for_vm(netvm, appvm, True) | ||||||
|  | 
 | ||||||
|  |     def reload_routing_for_vm(self, netvm, appvm, shutdown=False): | ||||||
|  |         '''Reload the routing method for the VM.''' | ||||||
|  |         if not netvm.is_running(): | ||||||
|  |             return | ||||||
|  |         for addr_family in (4, 6): | ||||||
|  |             ip = appvm.ip6 if addr_family == 6 else appvm.ip | ||||||
|  |             if ip is None: | ||||||
|  |                 continue | ||||||
|  |             # report routing method | ||||||
|  |             self.setup_forwarding_for_vm(netvm, appvm, ip, remove=shutdown) | ||||||
|  | 
 | ||||||
|  |     def setup_forwarding_for_vm(self, netvm, appvm, 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 <VM> 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 = appvm.features.check_with_template( | ||||||
|  |             'routing-method', 'masquerade' | ||||||
|  |         ) | ||||||
|  |         base_file = '/qubes-routing-method/{}'.format(ip) | ||||||
|  |         if remove: | ||||||
|  |             netvm.untrusted_qdb.rm(base_file) | ||||||
|  |         elif routing_method == 'forward': | ||||||
|  |             netvm.untrusted_qdb.write(base_file, 'forward') | ||||||
|  |         else: | ||||||
|  |             netvm.untrusted_qdb.write(base_file, 'masquerade') | ||||||
|  | 
 | ||||||
|  |     @qubes.ext.handler( | ||||||
|  |         'domain-feature-set:routing-method', | ||||||
|  |         'domain-feature-delete:routing-method', | ||||||
|  |     ) | ||||||
|  |     def on_routing_method_changed( | ||||||
|  |             self, | ||||||
|  |             vm, | ||||||
|  |             ignored_feature, | ||||||
|  |             **kwargs | ||||||
|  |     ): | ||||||
|  |         # pylint: disable=no-self-use,unused-argument | ||||||
|  |         if 'oldvalue' not in kwargs or kwargs.get('oldvalue') != kwargs.get('value'): | ||||||
|  |             if vm.netvm: | ||||||
|  |                 self.reload_routing_for_vm(vm.netvm, vm) | ||||||
|  | 
 | ||||||
|  |     @qubes.ext.handler('domain-qdb-create') | ||||||
|  |     def on_domain_qdb_create(self, vm, event, **kwargs): | ||||||
|  |         ''' Fills the QubesDB with firewall entries. ''' | ||||||
|  |         # pylint: disable=unused-argument | ||||||
|  |         if vm.netvm: | ||||||
|  |             self.reload_routing_for_vm(vm.netvm, vm) | ||||||
|  | 
 | ||||||
|  |     @qubes.ext.handler('domain-start') | ||||||
|  |     def on_domain_started(self, vm, event, **kwargs): | ||||||
|  |         # pylint: disable=unused-argument | ||||||
|  |         try: | ||||||
|  |             for downstream_vm in vm.connected_vms: | ||||||
|  |                 self.reload_routing_for_vm(vm, downstream_vm) | ||||||
|  |         except AttributeError: | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  |     @qubes.ext.handler('domain-shutdown') | ||||||
|  |     def on_domain_shutdown(self, vm, event, **kwargs): | ||||||
|  |        # pylint: disable=unused-argument | ||||||
|  |         try: | ||||||
|  |             for downstream_vm in self.connected_vms: | ||||||
|  |                 self.shutdown_routing_for_vm(vm, downstream_vm) | ||||||
|  |         except AttributeError: | ||||||
|  |             pass | ||||||
|  |         if vm.netvm: | ||||||
|  |             self.shutdown_routing_for_vm(vm.netvm, vm) | ||||||
|  |   | ||||||
|  |     @qubes.ext.handler('net-domain-connect') | ||||||
|  |     def on_net_domain_connect(self, vm, event): | ||||||
|  |         # pylint: disable=unused-argument | ||||||
|  |         if vm.netvm: | ||||||
|  |             self.reload_routing_for_vm(vm.netvm, vm) | ||||||
							
								
								
									
										24
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | |||||||
|  | import os | ||||||
|  | import setuptools | ||||||
|  | 
 | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     version = open(os.path.join(os.path.dirname(__file__), 'qubes-network-server.spec')).read().strip() | ||||||
|  |     version = [v for v in version.splitlines() if v.startswith("Version:")][0] | ||||||
|  |     version = version.split()[-1] | ||||||
|  |     setuptools.setup( | ||||||
|  |         name='qubesnetworkserver', | ||||||
|  |         version=version, | ||||||
|  |         author='Manuel Amador (Rudd-O)', | ||||||
|  |         author_email='rudd-o@rudd-o.com', | ||||||
|  |         description='Qubes network server dom0 component', | ||||||
|  |         license='GPL2+', | ||||||
|  |         url='https://github.com/Rudd-O/qubes-network-server', | ||||||
|  | 
 | ||||||
|  |         packages=('qubesnetworkserver',), | ||||||
|  | 
 | ||||||
|  |         entry_points={ | ||||||
|  |             'qubes.ext': [ | ||||||
|  |                 'qubesnetworkserver = qubesnetworkserver:QubesNetworkServerExtension', | ||||||
|  |             ], | ||||||
|  |         } | ||||||
|  |     ) | ||||||
							
								
								
									
										225
									
								
								src/qubes-routing-manager
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										225
									
								
								src/qubes-routing-manager
									
									
									
									
									
										Executable file
									
								
							| @ -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'], universal_newlines=True).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() | ||||||
							
								
								
									
										13
									
								
								src/qubes-routing-manager.service.in
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/qubes-routing-manager.service.in
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
| @ -1,170 +0,0 @@ | |||||||
| #!/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() |  | ||||||
| @ -1,449 +0,0 @@ | |||||||
| #!/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_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) |  | ||||||
| @ -1,46 +0,0 @@ | |||||||
| #!/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) |  | ||||||
| @ -1,193 +0,0 @@ | |||||||
| #!/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"]: |  | ||||||
|                 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) |  | ||||||
| @ -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()) |  | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Manuel Amador (Rudd-O)
						Manuel Amador (Rudd-O)