From b91714b204a3b8b1c2bbd16332b45bfcf3cfca55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Mon, 31 Oct 2016 02:04:27 +0100 Subject: [PATCH 01/10] qubes/features: handle recursive templates Have features.check_with_template() check the template recursively. The longest path (currently) is: DispVM -> AppVM -> TemplateVM. --- qubes/vm/__init__.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index 54984866..45312695 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -127,11 +127,8 @@ class Features(dict): return self[feature] if hasattr(self.vm, 'template') and self.vm.template is not None: - try: - return self.vm.template.features[feature] - except KeyError: - # handle default just below - pass + return self.vm.template.features.check_with_template(feature, + default) if default is self._NO_DEFAULT: raise KeyError(feature) From 2c6c476410dd2b587c8fbb0b792a4b9ac0979a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Mon, 31 Oct 2016 02:06:01 +0100 Subject: [PATCH 02/10] qubes/vm/net: add feature of hiding real IP from the VM This helps hiding VM IP for anonymous VMs (Whonix) even when some application leak it. VM will know only some fake IP, which should be set to something as common as possible. The feature is mostly implemented at (Proxy)VM side using NAT in separate network namespace. Core here is only passing arguments to it. It is designed the way that multiple VMs can use the same IP and still do not interfere with each other. Even more: it is possible to address each of them (using their "native" IP), even when multiple of them share the same "fake" IP. Original approach (marmarek/old-qubes-core-admin#2) used network script arguments by appending them to script name, but libxl in Xen >= 4.6 fixed that side effect and it isn't possible anymore. So use QubesDB instead. From user POV, this adds 3 "features": - net/fake-ip - IP address visible in the VM - net/fake-gateway - default gateway in the VM - net/fake-netmask - network mask The feature is enabled if net/fake-ip is set (to some IP address) and is different than VM native IP. All of those "features" can be set on template, to affect all of VMs. Firewall rules etc in (Proxy)VM should still be applied to VM "native" IP. Fixes QubesOS/qubes-issues#1143 --- qubes/vm/mix/net.py | 34 ++++++++++++++++++++++++++++++++++ qubes/vm/qubesvm.py | 6 +++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/qubes/vm/mix/net.py b/qubes/vm/mix/net.py index 83f3e7f6..ae7b1bf2 100644 --- a/qubes/vm/mix/net.py +++ b/qubes/vm/mix/net.py @@ -85,6 +85,27 @@ class NetVMMixin(qubes.events.Emitter): else: return self.get_ip_for_vm(self) + @qubes.tools.qvm_ls.column(width=15) + @property + def visible_ip(self): + '''IP address of this domain as seen by the domain.''' + return self.features.check_with_template('net/fake-ip', None) or \ + self.ip + + @qubes.tools.qvm_ls.column(width=15) + @property + def visible_gateway(self): + '''Default gateway of this domain as seen by the domain.''' + return self.features.check_with_template('net/fake-gateway', None) or \ + self.netvm.gateway + + @qubes.tools.qvm_ls.column(width=15) + @property + def visible_netmask(self): + '''Netmask as seen by the domain.''' + return self.features.check_with_template('net/fake-netmask', None) or \ + self.netvm.netmask + # # used in netvms (provides_network=True) # those properties and methods are most likely accessed as vm.netvm. @@ -274,6 +295,19 @@ class NetVMMixin(qubes.events.Emitter): # signal its done self.qdb.write(base_dir[:-1], '') + # add info about remapped IPs (VM IP hidden from the VM itself) + mapped_ip_base = '/mapped-ip/{}'.format(vm.ip) + if vm.visible_ip: + self.qdb.write(mapped_ip_base + '/visible-ip', vm.visible_ip) + else: + self.qdb.rm(mapped_ip_base + '/visible-ip') + if vm.visible_gateway: + self.qdb.write(mapped_ip_base + '/visible-gateway', + vm.visible_gateway) + else: + self.qdb.rm(mapped_ip_base + '/visible-gateway') + + @qubes.events.handler('property-del:netvm') def on_property_del_netvm(self, event, prop, old_netvm=None): ''' Sets the the NetVM to default NetVM ''' diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index 20061bf3..edf5c90b 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -1474,9 +1474,9 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): self.qdb.write('/qubes-netvm-{}-dns'.format(i), addr) if self.netvm is not None: - self.qdb.write('/qubes-ip', self.ip) - self.qdb.write('/qubes-netmask', self.netvm.netmask) - self.qdb.write('/qubes-gateway', self.netvm.gateway) + self.qdb.write('/qubes-ip', self.visible_ip) + self.qdb.write('/qubes-netmask', self.visible_netmask) + self.qdb.write('/qubes-gateway', self.visible_gateway) for i, addr in zip(('primary', 'secondary'), self.dns): self.qdb.write('/qubes-{}-dns'.format(i), addr) From 4585f2b50362e73593afd22c932e9fd2bbcbceac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Mon, 31 Oct 2016 02:17:21 +0100 Subject: [PATCH 03/10] tests: add tests for fake IP feature QubesOS/qubes-issues#1143 --- qubes/tests/int/network.py | 96 +++++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/qubes/tests/int/network.py b/qubes/tests/int/network.py index 661a7f5a..44c7b080 100644 --- a/qubes/tests/int/network.py +++ b/qubes/tests/int/network.py @@ -58,7 +58,7 @@ class VmNetworkingMixin(qubes.tests.SystemTestsMixin): def setUp(self): super(VmNetworkingMixin, self).setUp() - if self.template.startswith('whonix-'): + if self.template.startswith('whonix-gw'): self.skipTest("Test not supported here - Whonix uses its own " "firewall settings") self.init_default_template(self.template) @@ -339,6 +339,100 @@ class VmNetworkingMixin(qubes.tests.SystemTestsMixin): self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0) + def test_200_fake_ip_simple(self): + '''Test hiding VM real IP''' + self.testvm1.features['net/fake-ip'] = '192.168.1.128' + self.testvm1.features['net/fake-gateway'] = '192.168.1.1' + self.testvm1.features['net/fake-netmask'] = '255.255.255.0' + self.testvm1.start() + self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0) + self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0) + p = self.testvm1.run('ip addr show dev eth0', user='root', + passio_popen=True, + ignore_stderr=True) + p.stdin.close() + output = p.stdout.read() + self.assertEqual(p.wait(), 0, 'ip addr show dev eth0 failed') + self.assertIn('192.168.1.128', output) + self.assertNotIn(self.testvm1.ip, output) + + p = self.testvm1.run('ip route show', user='root', + passio_popen=True, + ignore_stderr=True) + p.stdin.close() + output = p.stdout.read() + self.assertEqual(p.wait(), 0, 'ip route show failed') + self.assertIn('192.168.1.1', output) + self.assertNotIn(self.testvm1.netvm.ip, output) + + def test_201_fake_ip_without_gw(self): + '''Test hiding VM real IP''' + self.testvm1.features['net/fake-ip'] = '192.168.1.128' + self.testvm1.start() + self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0) + self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0) + p = self.testvm1.run('ip addr show dev eth0', user='root', + passio_popen=True, + ignore_stderr=True) + p.stdin.close() + output = p.stdout.read() + self.assertEqual(p.wait(), 0, 'ip addr show dev eth0 failed') + self.assertIn('192.168.1.128', output) + self.assertNotIn(self.testvm1.ip, output) + + def test_202_fake_ip_firewall(self): + '''Test hiding VM real IP, firewall''' + self.testvm1.features['net/fake-ip'] = '192.168.1.128' + self.testvm1.features['net/fake-gateway'] = '192.168.1.1' + self.testvm1.features['net/fake-netmask'] = '255.255.255.0' + + self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM, + name=self.make_vm_name('proxy'), + label='red') + self.proxy.provides_network = True + self.proxy.create_on_disk() + self.proxy.netvm = self.testnetvm + self.testvm1.netvm = self.proxy + self.app.save() + + if self.run_cmd(self.testnetvm, 'nc -h 2>&1|grep -q nmap.org') == 0: + nc_version = NcVersion.Nmap + else: + nc_version = NcVersion.Trad + + # block all but ICMP and DNS + + self.testvm1.firewall.policy = 'drop' + self.testvm1.firewall.rules = [ + qubes.firewall.Rule(None, action='accept', proto='icmp'), + qubes.firewall.Rule(None, action='accept', specialtarget='dns'), + ] + self.testvm1.firewall.save() + self.testvm1.start() + self.assertTrue(self.proxy.is_running()) + + if nc_version == NcVersion.Nmap: + self.testnetvm.run("nc -l --send-only -e /bin/hostname -k 1234") + else: + self.testnetvm.run("while nc -l -e /bin/hostname -p 1234; do " + "true; done") + + self.assertEqual(self.run_cmd(self.proxy, self.ping_ip), 0, + "Ping by IP from ProxyVM failed") + self.assertEqual(self.run_cmd(self.proxy, self.ping_name), 0, + "Ping by name from ProxyVM failed") + self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0, + "Ping by IP should be allowed") + self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0, + "Ping by name should be allowed") + if nc_version == NcVersion.Nmap: + nc_cmd = "nc -w 1 --recv-only {} 1234".format(self.test_ip) + else: + nc_cmd = "nc -w 1 {} 1234".format(self.test_ip) + self.assertNotEqual(self.run_cmd(self.testvm1, nc_cmd), 0, + "TCP connection should be blocked") + + # noinspection PyAttributeOutsideInit class VmUpdatesMixin(qubes.tests.SystemTestsMixin): """ From b8145595a9e8eacced305d2e266d15a315c262b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Mon, 31 Oct 2016 03:04:13 +0100 Subject: [PATCH 04/10] qubes/vm/net: allow setting custom IP Fixes QubesOS/qubes-issues#1477 --- qubes/vm/mix/net.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/qubes/vm/mix/net.py b/qubes/vm/mix/net.py index ae7b1bf2..fe11ca13 100644 --- a/qubes/vm/mix/net.py +++ b/qubes/vm/mix/net.py @@ -45,6 +45,25 @@ def _setter_mac(self, prop, value): return value +def _default_ip(self): + if not self.is_networked(): + return None + if self.netvm is not None: + return self.netvm.get_ip_for_vm(self) # pylint: disable=no-member + else: + return self.get_ip_for_vm(self) + + +def _setter_ip(self, prop, value): + # pylint: disable=unused-argument + if not isinstance(value, basestring): + raise ValueError('IP address must be a string') + value = value.lower() + if re.match(r"^([0-9]{1,3}.){3}[0-9]{1,3}$", value) is None: + raise ValueError('Invalid IP address value') + return value + + class NetVMMixin(qubes.events.Emitter): ''' Mixin containing network functionality ''' mac = qubes.property('mac', type=str, @@ -53,6 +72,12 @@ class NetVMMixin(qubes.events.Emitter): ls_width=17, doc='MAC address of the NIC emulated inside VM') + ip = qubes.property('ip', type=str, + default=_default_ip, + setter=_setter_ip, + ls_width=15, + doc='IP address of this domain.') + # CORE2: swallowed uses_default_netvm netvm = qubes.VMProperty('netvm', load_stage=4, allow_none=True, default=(lambda self: self.app.default_fw_netvm if self.provides_network @@ -74,16 +99,6 @@ class NetVMMixin(qubes.events.Emitter): # used in networked appvms or proxyvms (netvm is not None) # - @qubes.tools.qvm_ls.column(width=15) - @property - def ip(self): - '''IP address of this domain.''' - if not self.is_networked(): - return None - if self.netvm is not None: - return self.netvm.get_ip_for_vm(self) # pylint: disable=no-member - else: - return self.get_ip_for_vm(self) @qubes.tools.qvm_ls.column(width=15) @property From 5072acc8f2e65332e3dc349064c15d070ef4f061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Mon, 31 Oct 2016 03:09:45 +0100 Subject: [PATCH 05/10] tests: custom VM IP QubesOS/qubes-issues#1477 --- qubes/tests/int/network.py | 73 ++++++++++++++++++++++++++++++++++++++ qubes/tests/vm/mix/net.py | 14 ++++++++ 2 files changed, 87 insertions(+) diff --git a/qubes/tests/int/network.py b/qubes/tests/int/network.py index 44c7b080..d2e358b5 100644 --- a/qubes/tests/int/network.py +++ b/qubes/tests/int/network.py @@ -432,6 +432,79 @@ class VmNetworkingMixin(qubes.tests.SystemTestsMixin): self.assertNotEqual(self.run_cmd(self.testvm1, nc_cmd), 0, "TCP connection should be blocked") + def test_210_custom_ip_simple(self): + '''Custom AppVM IP''' + self.testvm1.ip = '192.168.1.1' + self.testvm1.start() + self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0) + self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0) + + def test_211_custom_ip_proxy(self): + '''Custom ProxyVM IP''' + self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM, + name=self.make_vm_name('proxy'), + label='red') + self.proxy.create_on_disk() + self.proxy.provides_network = True + self.proxy.netvm = self.testnetvm + self.proxy.ip = '192.168.1.1' + self.testvm1.netvm = self.proxy + + self.testvm1.start() + + self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0) + self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0) + + def test_212_custom_ip_firewall(self): + '''Custom VM IP and firewall''' + self.testvm1.ip = '192.168.1.1' + + self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM, + name=self.make_vm_name('proxy'), + label='red') + self.proxy.provides_network = True + self.proxy.create_on_disk() + self.proxy.netvm = self.testnetvm + self.testvm1.netvm = self.proxy + self.app.save() + + if self.run_cmd(self.testnetvm, 'nc -h 2>&1|grep -q nmap.org') == 0: + nc_version = NcVersion.Nmap + else: + nc_version = NcVersion.Trad + + # block all but ICMP and DNS + + self.testvm1.firewall.policy = 'drop' + self.testvm1.firewall.rules = [ + qubes.firewall.Rule(None, action='accept', proto='icmp'), + qubes.firewall.Rule(None, action='accept', specialtarget='dns'), + ] + self.testvm1.firewall.save() + self.testvm1.start() + self.assertTrue(self.proxy.is_running()) + + if nc_version == NcVersion.Nmap: + self.testnetvm.run("nc -l --send-only -e /bin/hostname -k 1234") + else: + self.testnetvm.run("while nc -l -e /bin/hostname -p 1234; do " + "true; done") + + self.assertEqual(self.run_cmd(self.proxy, self.ping_ip), 0, + "Ping by IP from ProxyVM failed") + self.assertEqual(self.run_cmd(self.proxy, self.ping_name), 0, + "Ping by name from ProxyVM failed") + self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0, + "Ping by IP should be allowed") + self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0, + "Ping by name should be allowed") + if nc_version == NcVersion.Nmap: + nc_cmd = "nc -w 1 --recv-only {} 1234".format(self.test_ip) + else: + nc_cmd = "nc -w 1 {} 1234".format(self.test_ip) + self.assertNotEqual(self.run_cmd(self.testvm1, nc_cmd), 0, + "TCP connection should be blocked") + # noinspection PyAttributeOutsideInit class VmUpdatesMixin(qubes.tests.SystemTestsMixin): diff --git a/qubes/tests/vm/mix/net.py b/qubes/tests/vm/mix/net.py index dfd844d1..2f96b81a 100644 --- a/qubes/tests/vm/mix/net.py +++ b/qubes/tests/vm/mix/net.py @@ -130,3 +130,17 @@ class TC_00_NetVMMixin( self.app.domains = {1: vm, vm: vm} self.assertPropertyInvalidValue(vm, 'dispvm_netvm', vm) + def test_150_ip(self): + vm = self.get_vm() + self.setup_netvms(vm) + self.assertPropertyDefaultValue(vm, 'ip', '10.137.0.' + str(vm.qid)) + vm.ip = '192.168.1.1' + self.assertEqual(vm.ip, '192.168.1.1') + + def test_151_ip_invalid(self): + vm = self.get_vm() + self.setup_netvms(vm) + self.assertPropertyInvalidValue(vm, 'ip', 'abcd') + self.assertPropertyInvalidValue(vm, 'ip', 'a.b.c.d') + self.assertPropertyInvalidValue(vm, 'ip', '1111.2222.3333.4444') + # TODO: implement and add here: 0.0.0.0, 333.333.333.333 From ea33fef9cc8202fb44143cbc3d74f06f9ce66940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Mon, 31 Oct 2016 03:10:12 +0100 Subject: [PATCH 06/10] tests: drop dispvm_netvm tests This property no longer exists in core3. --- qubes/tests/vm/mix/net.py | 46 --------------------------------------- 1 file changed, 46 deletions(-) diff --git a/qubes/tests/vm/mix/net.py b/qubes/tests/vm/mix/net.py index 2f96b81a..2f50ec5c 100644 --- a/qubes/tests/vm/mix/net.py +++ b/qubes/tests/vm/mix/net.py @@ -84,52 +84,6 @@ class TC_00_NetVMMixin( self.app.domains = {1: vm, vm: vm} self.assertPropertyInvalidValue(vm, 'netvm', vm) - @unittest.skip('TODO: probably remove') - def test_290_dispvm_netvm(self): - vm = self.get_vm() - self.setup_netvms(vm) - self.assertPropertyDefaultValue(vm, 'dispvm_netvm', - self.app.default_netvm) - self.assertPropertyValue(vm, 'dispvm_netvm', self.netvm2, self.netvm2, - self.netvm2.name) - del vm.dispvm_netvm - self.assertPropertyDefaultValue(vm, 'dispvm_netvm', - self.app.default_netvm) - self.assertPropertyValue(vm, 'dispvm_netvm', self.netvm2.name, - self.netvm2, self.netvm2.name) - # XXX FIXME xml value - self.assertPropertyValue(vm, 'dispvm_netvm', None, None, 'None') - - @unittest.skip('TODO: probably remove') - def test_291_dispvm_netvm_invalid(self): - vm = self.get_vm() - self.setup_netvms(vm) - self.assertPropertyInvalidValue(vm, 'dispvm_netvm', 'invalid') - self.assertPropertyInvalidValue(vm, 'dispvm_netvm', 123) - - @unittest.skip('TODO: probably remove') - def test_291_dispvm_netvm_netvm(self): - vm = self.get_vm() - nonetvm = TestVM(qid=2, app=self.app, name='nonetvm') - self.app.domains = {1: vm, 2: nonetvm} - self.assertPropertyInvalidValue(vm, 'dispvm_netvm', nonetvm) - - @unittest.skip('TODO: probably remove') - def test_291_dispvm_netvm_default(self): - """Check if vm.dispvm_netvm default is really vm.netvm""" - vm = self.get_vm() - self.setup_netvms(vm) - vm.netvm = self.netvm2 - self.assertPropertyDefaultValue(vm, 'dispvm_netvm', self.netvm2) - del vm.netvm - self.assertPropertyDefaultValue(vm, 'dispvm_netvm', self.netvm1) - - @unittest.skip('TODO: probably remove') - def test_292_dispvm_netvm_loopback(self): - vm = self.get_vm() - self.app.domains = {1: vm, vm: vm} - self.assertPropertyInvalidValue(vm, 'dispvm_netvm', vm) - def test_150_ip(self): vm = self.get_vm() self.setup_netvms(vm) From d999d9104942df19676538a7ce00409fe6315fea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Mon, 31 Oct 2016 03:39:46 +0100 Subject: [PATCH 07/10] tests: few more tests for fake/custom IP QubesOS/qubes-issues#1143 QubesOS/qubes-issues#1477 --- qubes/tests/int/network.py | 99 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/qubes/tests/int/network.py b/qubes/tests/int/network.py index d2e358b5..d00750ef 100644 --- a/qubes/tests/int/network.py +++ b/qubes/tests/int/network.py @@ -432,6 +432,105 @@ class VmNetworkingMixin(qubes.tests.SystemTestsMixin): self.assertNotEqual(self.run_cmd(self.testvm1, nc_cmd), 0, "TCP connection should be blocked") + def test_203_fake_ip_inter_vm_allow(self): + self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM, + name=self.make_vm_name('proxy'), + label='red') + self.proxy.create_on_disk() + self.proxy.provides_network = True + self.proxy.netvm = self.testnetvm + self.testvm1.netvm = self.proxy + self.testvm1.features['net/fake-ip'] = '192.168.1.128' + self.testvm1.features['net/fake-gateway'] = '192.168.1.1' + self.testvm1.features['net/fake-netmask'] = '255.255.255.0' + + self.testvm2 = self.app.add_new_vm(qubes.vm.appvm.AppVM, + name=self.make_vm_name('vm3'), + label='red') + self.testvm2.create_on_disk() + self.testvm2.netvm = self.proxy + self.app.save() + + self.testvm1.start() + self.testvm2.start() + + cmd = 'iptables -I FORWARD -s {} -d {} -j ACCEPT'.format( + self.testvm2.ip, self.testvm1.ip) + retcode = self.proxy.run(cmd, user='root', wait=True) + self.assertEqual(retcode, 0, '{} failed with: {}'.format(cmd, retcode)) + + cmd = 'iptables -I INPUT -s {} -j ACCEPT'.format( + self.testvm2.ip) + retcode = self.testvm1.run(cmd, user='root', wait=True) + self.assertEqual(retcode, 0, '{} failed with: {}'.format(cmd, retcode)) + + self.assertEqual(self.run_cmd(self.testvm2, + self.ping_cmd.format(target=self.testvm1.ip)), 0) + + cmd = 'iptables -nvxL INPUT | grep {}'.format(self.testvm2.ip) + p = self.testvm1.run(cmd, user='root', passio_popen=True) + (stdout, _) = p.communicate() + self.assertEqual(p.returncode, 0, + '{} failed with {}'.format(cmd, p.returncode)) + self.assertNotEqual(stdout.split()[0], '0', + 'Packets didn\'t managed to the VM') + + def test_204_fake_ip_proxy(self): + '''Test hiding VM real IP''' + self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM, + name=self.make_vm_name('proxy'), + label='red') + self.proxy.create_on_disk() + self.proxy.provides_network = True + self.proxy.netvm = self.testnetvm + self.proxy.features['net/fake-ip'] = '192.168.1.128' + self.proxy.features['net/fake-gateway'] = '192.168.1.1' + self.proxy.features['net/fake-netmask'] = '255.255.255.0' + self.testvm1.netvm = self.proxy + self.testvm1.start() + + self.assertEqual(self.run_cmd(self.proxy, self.ping_ip), 0) + self.assertEqual(self.run_cmd(self.proxy, self.ping_name), 0) + + self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0) + self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0) + + p = self.proxy.run('ip addr show dev eth0', user='root', + passio_popen=True, + ignore_stderr=True) + p.stdin.close() + output = p.stdout.read() + self.assertEqual(p.wait(), 0, 'ip addr show dev eth0 failed') + self.assertIn('192.168.1.128', output) + self.assertNotIn(self.testvm1.ip, output) + + p = self.proxy.run('ip route show', user='root', + passio_popen=True, + ignore_stderr=True) + p.stdin.close() + output = p.stdout.read() + self.assertEqual(p.wait(), 0, 'ip route show failed') + self.assertIn('192.168.1.1', output) + self.assertNotIn(self.testvm1.netvm.ip, output) + + p = self.testvm1.run('ip addr show dev eth0', user='root', + passio_popen=True, + ignore_stderr=True) + p.stdin.close() + output = p.stdout.read() + self.assertEqual(p.wait(), 0, 'ip addr show dev eth0 failed') + self.assertNotIn('192.168.1.128', output) + self.assertIn(self.testvm1.ip, output) + + p = self.testvm1.run('ip route show', user='root', + passio_popen=True, + ignore_stderr=True) + p.stdin.close() + output = p.stdout.read() + self.assertEqual(p.wait(), 0, 'ip route show failed') + self.assertIn('192.168.1.128', output) + self.assertNotIn(self.proxy.ip, output) + def test_210_custom_ip_simple(self): '''Custom AppVM IP''' self.testvm1.ip = '192.168.1.1' From ec81b3046f70f7ffe45be8a55e2801d72a132eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Tue, 1 Nov 2016 00:28:37 +0100 Subject: [PATCH 08/10] tests: add missing app.save() before starting a domain Otherwise domain will be unknown to other processes (like qrexec services). --- qubes/tests/int/network.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qubes/tests/int/network.py b/qubes/tests/int/network.py index d00750ef..977d74d7 100644 --- a/qubes/tests/int/network.py +++ b/qubes/tests/int/network.py @@ -344,6 +344,7 @@ class VmNetworkingMixin(qubes.tests.SystemTestsMixin): self.testvm1.features['net/fake-ip'] = '192.168.1.128' self.testvm1.features['net/fake-gateway'] = '192.168.1.1' self.testvm1.features['net/fake-netmask'] = '255.255.255.0' + self.app.save() self.testvm1.start() self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0) self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0) @@ -368,6 +369,7 @@ class VmNetworkingMixin(qubes.tests.SystemTestsMixin): def test_201_fake_ip_without_gw(self): '''Test hiding VM real IP''' self.testvm1.features['net/fake-ip'] = '192.168.1.128' + self.app.save() self.testvm1.start() self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0) self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0) @@ -433,6 +435,7 @@ class VmNetworkingMixin(qubes.tests.SystemTestsMixin): "TCP connection should be blocked") def test_203_fake_ip_inter_vm_allow(self): + '''Access VM with "fake IP" from other VM (when firewall allows)''' self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM, name=self.make_vm_name('proxy'), label='red') @@ -487,6 +490,7 @@ class VmNetworkingMixin(qubes.tests.SystemTestsMixin): self.proxy.features['net/fake-gateway'] = '192.168.1.1' self.proxy.features['net/fake-netmask'] = '255.255.255.0' self.testvm1.netvm = self.proxy + self.app.save() self.testvm1.start() self.assertEqual(self.run_cmd(self.proxy, self.ping_ip), 0) @@ -534,6 +538,7 @@ class VmNetworkingMixin(qubes.tests.SystemTestsMixin): def test_210_custom_ip_simple(self): '''Custom AppVM IP''' self.testvm1.ip = '192.168.1.1' + self.app.save() self.testvm1.start() self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0) self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0) @@ -548,6 +553,7 @@ class VmNetworkingMixin(qubes.tests.SystemTestsMixin): self.proxy.netvm = self.testnetvm self.proxy.ip = '192.168.1.1' self.testvm1.netvm = self.proxy + self.app.save() self.testvm1.start() From b4fa8cdce33660fb0ee0e4d8ee418a27e3e789f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Tue, 1 Nov 2016 00:30:11 +0100 Subject: [PATCH 09/10] qubes/vm/net: use domain's "visible IP" for a gateway address This is the IP known to the domain itself and downstream domains. It may be a different one than seen be its upstream domain. Related to QubesOS/qubes-issues#1143` --- qubes/vm/mix/net.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qubes/vm/mix/net.py b/qubes/vm/mix/net.py index fe11ca13..770044aa 100644 --- a/qubes/vm/mix/net.py +++ b/qubes/vm/mix/net.py @@ -142,7 +142,7 @@ class NetVMMixin(qubes.events.Emitter): @property def gateway(self): '''Gateway for other domains that use this domain as netvm.''' - return self.ip if self.provides_network else None + return self.visible_ip if self.provides_network else None @qubes.tools.qvm_ls.column(width=15) @property From 38fc504ca093ca2271d5b218797e84b6d56e00e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Tue, 1 Nov 2016 00:37:43 +0100 Subject: [PATCH 10/10] qubes/vm/net: set mapped IP info before attaching network Set parameters for possibly hiding domain's real IP before attaching network to it, otherwise we'll have race condition with vif-route-qubes script. QubesOS/qubes-issues#1143 --- qubes/vm/mix/net.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/qubes/vm/mix/net.py b/qubes/vm/mix/net.py index 770044aa..87473a1c 100644 --- a/qubes/vm/mix/net.py +++ b/qubes/vm/mix/net.py @@ -247,6 +247,7 @@ class NetVMMixin(qubes.events.Emitter): self.log.info('Starting NetVM ({0})'.format(self.netvm.name)) self.netvm.start() + self.netvm.set_mapped_ip_info_for_vm(self) self.libvirt_domain.attachDevice( self.app.env.get_template('libvirt/devices/net.xml').render( vm=self)) @@ -310,6 +311,12 @@ class NetVMMixin(qubes.events.Emitter): # signal its done self.qdb.write(base_dir[:-1], '') + def set_mapped_ip_info_for_vm(self, vm): + ''' + Set configuration to possibly hide real IP from the VM. + This needs to be done before executing 'script' + (`/etc/xen/scripts/vif-route-qubes`) in network providing VM + ''' # add info about remapped IPs (VM IP hidden from the VM itself) mapped_ip_base = '/mapped-ip/{}'.format(vm.ip) if vm.visible_ip: @@ -391,6 +398,7 @@ class NetVMMixin(qubes.events.Emitter): ''' Reloads the firewall if vm is running and has a NetVM assigned ''' # pylint: disable=unused-argument if self.is_running() and self.netvm: + self.netvm.set_mapped_ip_info_for_vm(self) self.netvm.reload_firewall_for_vm(self) # pylint: disable=no-member # CORE2: swallowed get_firewall_conf, write_firewall_conf,