diff --git a/qubes/config.py b/qubes/config.py index ec6e16d2..dfedfe23 100644 --- a/qubes/config.py +++ b/qubes/config.py @@ -107,3 +107,6 @@ max_default_label = 8 #: profiles for admin.backup.* calls backup_profile_dir = '/etc/qubes/backup' + +#: site-local prefix for all VMs +qubes_ipv6_prefix = 'fd09:24ef:4179:0000' diff --git a/qubes/tests/vm/mix/net.py b/qubes/tests/vm/mix/net.py index 2e63ae6d..5bb80acf 100644 --- a/qubes/tests/vm/mix/net.py +++ b/qubes/tests/vm/mix/net.py @@ -121,3 +121,21 @@ class TC_00_NetVMMixin( 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 + + def test_160_ip6(self): + vm = self.get_vm() + self.setup_netvms(vm) + self.assertPropertyDefaultValue(vm, 'ip6', None) + vm.netvm.features['ipv6'] = True + self.assertPropertyDefaultValue(vm, 'ip6', + '{}::a89:{:x}'.format(qubes.config.qubes_ipv6_prefix, vm.qid)) + vm.ip6 = 'abcd:efff::1' + self.assertEqual(vm.ip6, 'abcd:efff::1') + + def test_161_ip6_invalid(self): + vm = self.get_vm() + self.setup_netvms(vm) + vm.netvm.features['ipv6'] = True + self.assertPropertyInvalidValue(vm, 'ip', 'zzzz') + self.assertPropertyInvalidValue(vm, 'ip', + '1:2:3:4:5:6:7:8:0:a:b:c:d:e:f:0') diff --git a/qubes/vm/mix/net.py b/qubes/vm/mix/net.py index 22b68146..dac404cf 100644 --- a/qubes/vm/mix/net.py +++ b/qubes/vm/mix/net.py @@ -24,14 +24,15 @@ import os import re +import socket import libvirt # pylint: disable=import-error import qubes +import qubes.config import qubes.events import qubes.firewall import qubes.exc - def _setter_mac(self, prop, value): ''' Helper for setting the MAC address ''' # pylint: disable=unused-argument @@ -61,6 +62,27 @@ def _setter_ip(self, prop, value): raise ValueError('Invalid IP address value') return value +def _default_ip6(self): + if not self.is_networked(): + return None + if not self.features.check_with_netvm('ipv6', False): + return None + if self.netvm is not None: + return self.netvm.get_ip6_for_vm(self) # pylint: disable=no-member + + return self.get_ip6_for_vm(self) + + +def _setter_ip6(self, prop, value): + # pylint: disable=unused-argument + if not isinstance(value, str): + raise ValueError('IPv6 address must be a string') + value = value.lower() + try: + socket.inet_pton(socket.AF_INET6, value) + except socket.error: + raise ValueError('Invalid IPv6 address value') + return value def _setter_netvm(self, prop, value): # pylint: disable=unused-argument @@ -92,6 +114,11 @@ class NetVMMixin(qubes.events.Emitter): setter=_setter_ip, doc='IP address of this domain.') + ip6 = qubes.property('ip6', type=str, + default=_default_ip6, + setter=_setter_ip6, + doc='IPv6 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_netvm), @@ -121,12 +148,24 @@ class NetVMMixin(qubes.events.Emitter): return self.features.check_with_template('net.fake-ip', None) or \ self.ip + @qubes.stateless_property + def visible_ip6(self): + '''IPv6 address of this domain as seen by the domain.''' + return self.ip6 + @qubes.stateless_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 if self.netvm else None) + @qubes.stateless_property + def visible_gateway6(self): + '''Default (IPv6) gateway of this domain as seen by the domain.''' + if self.features.check_with_netvm('ipv6', False): + return self.netvm.gateway6 if self.netvm else None + return None + @qubes.stateless_property def visible_netmask(self): '''Netmask as seen by the domain.''' @@ -151,11 +190,33 @@ class NetVMMixin(qubes.events.Emitter): # does not happen, because qid < 253, but may happen in the future. return '10.137.{}.{}'.format((vm.qid >> 8) & 0xff, vm.qid & 0xff) + @staticmethod + def get_ip6_for_vm(vm): + '''Get IPv6 address for (appvm) domain connected to this (netvm) domain. + + Default address is constructed with Qubes-specific site-local prefix, + and IPv4 suffix (0xa89 is 10.137.). + ''' + import qubes.vm.dispvm # pylint: disable=redefined-outer-name + if isinstance(vm, qubes.vm.dispvm.DispVM): + return '{}::a8a:{:x}'.format( + qubes.config.qubes_ipv6_prefix, vm.dispid) + + return '{}::a89:{:x}'.format(qubes.config.qubes_ipv6_prefix, vm.qid) + @qubes.stateless_property def gateway(self): '''Gateway for other domains that use this domain as netvm.''' return self.visible_ip if self.provides_network else None + @qubes.stateless_property + def gateway6(self): + '''Gateway (IPv6) for other domains that use this domain as netvm.''' + if self.features.check_with_netvm('ipv6', False): + return 'fe80::fcff:ffff:feff:ffff' if self.provides_network else \ + None + return None + @property def netmask(self): '''Netmask for gateway address.''' @@ -305,15 +366,20 @@ class NetVMMixin(qubes.events.Emitter): if not self.is_running(): return - base_dir = '/qubes-firewall/' + vm.ip + '/' - # remove old entries if any (but don't touch base empty entry - it - # would trigger reload right away - self.untrusted_qdb.rm(base_dir) - # write new rules - for key, value in vm.firewall.qdb_entries(addr_family=4).items(): - self.untrusted_qdb.write(base_dir + key, value) - # signal its done - self.untrusted_qdb.write(base_dir[:-1], '') + for addr_family in (4, 6): + ip = vm.ip6 if addr_family == 6 else vm.ip + if ip is None: + continue + base_dir = '/qubes-firewall/' + ip + '/' + # remove old entries if any (but don't touch base empty entry - it + # would trigger reload right away + self.untrusted_qdb.rm(base_dir) + # write new rules + for key, value in vm.firewall.qdb_entries( + addr_family=addr_family).items(): + self.untrusted_qdb.write(base_dir + key, value) + # signal its done + self.untrusted_qdb.write(base_dir[:-1], '') def set_mapped_ip_info_for_vm(self, vm): ''' @@ -334,7 +400,6 @@ class NetVMMixin(qubes.events.Emitter): else: self.untrusted_qdb.rm(mapped_ip_base + '/visible-gateway') - @qubes.events.handler('property-pre-del:netvm') def on_property_pre_del_netvm(self, event, name, oldvalue=None): ''' Sets the the NetVM to default NetVM ''' diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index 3e857900..57b00db2 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -1831,6 +1831,12 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): for i, addr in zip(('primary', 'secondary'), self.dns): self.untrusted_qdb.write('/qubes-{}-dns'.format(i), addr) + if self.visible_ip6: + self.untrusted_qdb.write('/qubes-ip6', self.visible_ip6) + if self.visible_gateway6: + self.untrusted_qdb.write('/qubes-gateway6', + self.visible_gateway6) + tzname = qubes.utils.get_timezone() if tzname: diff --git a/templates/libvirt/devices/net.xml b/templates/libvirt/devices/net.xml index dcb2290c..8fd4f95a 100644 --- a/templates/libvirt/devices/net.xml +++ b/templates/libvirt/devices/net.xml @@ -1,6 +1,9 @@ +{% if vm.ip6 %} + +{% endif %}