network.py 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968
  1. #
  2. # The Qubes OS Project, https://www.qubes-os.org/
  3. #
  4. # Copyright (C) 2015
  5. # Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
  6. # Copyright (C) 2015 Wojtek Porczyk <woju@invisiblethingslab.com>
  7. #
  8. # This library is free software; you can redistribute it and/or
  9. # modify it under the terms of the GNU Lesser General Public
  10. # License as published by the Free Software Foundation; either
  11. # version 2.1 of the License, or (at your option) any later version.
  12. #
  13. # This library is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  16. # Lesser General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Lesser General Public
  19. # License along with this library; if not, see <https://www.gnu.org/licenses/>.
  20. #
  21. from distutils import spawn
  22. import asyncio
  23. import multiprocessing
  24. import os
  25. import subprocess
  26. import unittest
  27. import time
  28. import qubes.tests
  29. import qubes.firewall
  30. import qubes.vm.appvm
  31. class NcVersion:
  32. Trad = 1
  33. Nmap = 2
  34. # noinspection PyAttributeOutsideInit
  35. class VmNetworkingMixin(object):
  36. test_ip = '192.168.123.45'
  37. test_name = 'test.example.com'
  38. ping_cmd = 'ping -W 1 -n -c 1 {target}'
  39. ping_ip = ping_cmd.format(target=test_ip)
  40. ping_name = ping_cmd.format(target=test_name)
  41. # filled by load_tests
  42. template = None
  43. def run_cmd(self, vm, cmd, user="root"):
  44. try:
  45. self.loop.run_until_complete(vm.run_for_stdio(cmd, user=user))
  46. except subprocess.CalledProcessError as e:
  47. return e.returncode
  48. return 0
  49. def setUp(self):
  50. super(VmNetworkingMixin, self).setUp()
  51. if self.template.startswith('whonix-'):
  52. self.skipTest("Test not supported here - Whonix uses its own "
  53. "firewall settings")
  54. self.init_default_template(self.template)
  55. self.testnetvm = self.app.add_new_vm(qubes.vm.appvm.AppVM,
  56. name=self.make_vm_name('netvm1'),
  57. label='red')
  58. self.loop.run_until_complete(self.testnetvm.create_on_disk())
  59. self.testnetvm.provides_network = True
  60. self.testvm1 = self.app.add_new_vm(qubes.vm.appvm.AppVM,
  61. name=self.make_vm_name('vm1'),
  62. label='red')
  63. self.loop.run_until_complete(self.testvm1.create_on_disk())
  64. self.testvm1.netvm = self.testnetvm
  65. self.app.save()
  66. self.configure_netvm()
  67. def configure_netvm(self):
  68. def run_netvm_cmd(cmd):
  69. if self.run_cmd(self.testnetvm, cmd) != 0:
  70. self.fail("Command '%s' failed" % cmd)
  71. if not self.testnetvm.is_running():
  72. self.loop.run_until_complete(self.testnetvm.start())
  73. # Ensure that dnsmasq is installed:
  74. try:
  75. self.loop.run_until_complete(self.testnetvm.run_for_stdio(
  76. 'dnsmasq --version', user='root'))
  77. except subprocess.CalledProcessError:
  78. self.skipTest("dnsmasq not installed")
  79. run_netvm_cmd("ip link add test0 type dummy")
  80. run_netvm_cmd("ip link set test0 up")
  81. run_netvm_cmd("ip addr add {}/24 dev test0".format(self.test_ip))
  82. run_netvm_cmd("iptables -I INPUT -d {} -j ACCEPT".format(self.test_ip))
  83. # ignore failure
  84. self.run_cmd(self.testnetvm, "killall --wait dnsmasq")
  85. run_netvm_cmd("dnsmasq -a {ip} -A /{name}/{ip} -i test0 -z".format(
  86. ip=self.test_ip, name=self.test_name))
  87. run_netvm_cmd("echo nameserver {} > /etc/resolv.conf".format(
  88. self.test_ip))
  89. run_netvm_cmd("/usr/lib/qubes/qubes-setup-dnat-to-ns")
  90. def test_000_simple_networking(self):
  91. self.loop.run_until_complete(self.testvm1.start())
  92. self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0)
  93. self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0)
  94. def test_010_simple_proxyvm(self):
  95. self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
  96. name=self.make_vm_name('proxy'),
  97. label='red')
  98. self.proxy.provides_network = True
  99. self.proxy.netvm = self.testnetvm
  100. self.loop.run_until_complete(self.proxy.create_on_disk())
  101. self.testvm1.netvm = self.proxy
  102. self.app.save()
  103. self.loop.run_until_complete(self.testvm1.start())
  104. self.assertTrue(self.proxy.is_running())
  105. self.assertEqual(self.run_cmd(self.proxy, self.ping_ip), 0,
  106. "Ping by IP from ProxyVM failed")
  107. self.assertEqual(self.run_cmd(self.proxy, self.ping_name), 0,
  108. "Ping by name from ProxyVM failed")
  109. self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0,
  110. "Ping by IP from AppVM failed")
  111. self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0,
  112. "Ping by IP from AppVM failed")
  113. @qubes.tests.expectedFailureIfTemplate('debian-7')
  114. @unittest.skipUnless(spawn.find_executable('xdotool'),
  115. "xdotool not installed")
  116. def test_020_simple_proxyvm_nm(self):
  117. self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
  118. name=self.make_vm_name('proxy'),
  119. label='red')
  120. self.proxy.provides_network = True
  121. self.loop.run_until_complete(self.proxy.create_on_disk())
  122. self.proxy.netvm = self.testnetvm
  123. self.proxy.features['service.network-manager'] = True
  124. self.testvm1.netvm = self.proxy
  125. self.app.save()
  126. self.loop.run_until_complete(self.testvm1.start())
  127. self.assertTrue(self.proxy.is_running())
  128. self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0,
  129. "Ping by IP failed")
  130. self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0,
  131. "Ping by name failed")
  132. # reconnect to make sure that device was configured by NM
  133. self.assertEqual(
  134. self.run_cmd(self.proxy, "nmcli device disconnect eth0",
  135. user="user"),
  136. 0, "Failed to disconnect eth0 using nmcli")
  137. self.assertNotEqual(self.run_cmd(self.testvm1, self.ping_ip), 0,
  138. "Network should be disabled, but apparently it isn't")
  139. self.assertEqual(
  140. self.run_cmd(self.proxy,
  141. 'nmcli connection up "VM uplink eth0" ifname eth0',
  142. user="user"),
  143. 0, "Failed to connect eth0 using nmcli")
  144. self.assertEqual(self.run_cmd(self.proxy, "nm-online", user="user"), 0,
  145. "Failed to wait for NM connection")
  146. # check for nm-applet presence
  147. self.assertEqual(subprocess.call([
  148. 'xdotool', 'search', '--class', '{}:nm-applet'.format(
  149. self.proxy.name)],
  150. stdout=subprocess.DEVNULL), 0, "nm-applet window not found")
  151. self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0,
  152. "Ping by IP failed (after NM reconnection")
  153. self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0,
  154. "Ping by name failed (after NM reconnection)")
  155. def test_030_firewallvm_firewall(self):
  156. self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
  157. name=self.make_vm_name('proxy'),
  158. label='red')
  159. self.proxy.provides_network = True
  160. self.loop.run_until_complete(self.proxy.create_on_disk())
  161. self.proxy.netvm = self.testnetvm
  162. self.testvm1.netvm = self.proxy
  163. self.app.save()
  164. if self.run_cmd(self.testnetvm, 'nc -h 2>&1|grep -q nmap.org') == 0:
  165. nc_version = NcVersion.Nmap
  166. else:
  167. nc_version = NcVersion.Trad
  168. # block all for first
  169. self.testvm1.firewall.rules = [qubes.firewall.Rule(action='drop')]
  170. self.testvm1.firewall.save()
  171. self.loop.run_until_complete(self.testvm1.start())
  172. self.assertTrue(self.proxy.is_running())
  173. nc = self.loop.run_until_complete(self.testnetvm.run(
  174. 'nc -l --send-only -e /bin/hostname -k 1234'
  175. if nc_version == NcVersion.Nmap
  176. else 'while nc -l -e /bin/hostname -p 1234; do true; done'))
  177. try:
  178. self.assertEqual(self.run_cmd(self.proxy, self.ping_ip), 0,
  179. "Ping by IP from ProxyVM failed")
  180. self.assertEqual(self.run_cmd(self.proxy, self.ping_name), 0,
  181. "Ping by name from ProxyVM failed")
  182. self.assertNotEqual(self.run_cmd(self.testvm1, self.ping_ip), 0,
  183. "Ping by IP should be blocked")
  184. if nc_version == NcVersion.Nmap:
  185. nc_cmd = "nc -w 1 --recv-only {} 1234".format(self.test_ip)
  186. else:
  187. nc_cmd = "nc -w 1 {} 1234".format(self.test_ip)
  188. self.assertNotEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
  189. "TCP connection should be blocked")
  190. # block all except ICMP
  191. self.testvm1.firewall.rules = [(
  192. qubes.firewall.Rule(None, action='accept', proto='icmp')
  193. )]
  194. self.testvm1.firewall.save()
  195. # Ugly hack b/c there is no feedback when the rules are actually
  196. # applied
  197. time.sleep(3)
  198. self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0,
  199. "Ping by IP failed (should be allowed now)")
  200. self.assertNotEqual(self.run_cmd(self.testvm1, self.ping_name), 0,
  201. "Ping by name should be blocked")
  202. # all TCP still blocked
  203. self.testvm1.firewall.rules = [
  204. qubes.firewall.Rule(None, action='accept', proto='icmp'),
  205. qubes.firewall.Rule(None, action='accept', specialtarget='dns'),
  206. ]
  207. self.testvm1.firewall.save()
  208. # Ugly hack b/c there is no feedback when the rules are actually
  209. # applied
  210. time.sleep(3)
  211. self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0,
  212. "Ping by name failed (should be allowed now)")
  213. self.assertNotEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
  214. "TCP connection should be blocked")
  215. # block all except target
  216. self.testvm1.firewall.rules = [
  217. qubes.firewall.Rule(None, action='accept', dsthost=self.test_ip,
  218. proto='tcp', dstports=1234),
  219. ]
  220. self.testvm1.firewall.save()
  221. # Ugly hack b/c there is no feedback when the rules are actually
  222. # applied
  223. time.sleep(3)
  224. self.assertEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
  225. "TCP connection failed (should be allowed now)")
  226. # allow all except target
  227. self.testvm1.firewall.rules = [
  228. qubes.firewall.Rule(None, action='drop', dsthost=self.test_ip,
  229. proto='tcp', dstports=1234),
  230. qubes.firewall.Rule(action='accept'),
  231. ]
  232. self.testvm1.firewall.save()
  233. # Ugly hack b/c there is no feedback when the rules are actually
  234. # applied
  235. time.sleep(3)
  236. self.assertNotEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
  237. "TCP connection should be blocked")
  238. finally:
  239. nc.terminate()
  240. self.loop.run_until_complete(nc.wait())
  241. def test_040_inter_vm(self):
  242. self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
  243. name=self.make_vm_name('proxy'),
  244. label='red')
  245. self.loop.run_until_complete(self.proxy.create_on_disk())
  246. self.proxy.provides_network = True
  247. self.proxy.netvm = self.testnetvm
  248. self.testvm1.netvm = self.proxy
  249. self.testvm2 = self.app.add_new_vm(qubes.vm.appvm.AppVM,
  250. name=self.make_vm_name('vm2'),
  251. label='red')
  252. self.loop.run_until_complete(self.testvm2.create_on_disk())
  253. self.testvm2.netvm = self.proxy
  254. self.app.save()
  255. self.loop.run_until_complete(asyncio.wait([
  256. self.testvm1.start(),
  257. self.testvm2.start()]))
  258. self.assertNotEqual(self.run_cmd(self.testvm1,
  259. self.ping_cmd.format(target=self.testvm2.ip)), 0)
  260. self.testvm2.netvm = self.testnetvm
  261. self.assertNotEqual(self.run_cmd(self.testvm1,
  262. self.ping_cmd.format(target=self.testvm2.ip)), 0)
  263. self.assertNotEqual(self.run_cmd(self.testvm2,
  264. self.ping_cmd.format(target=self.testvm1.ip)), 0)
  265. self.testvm1.netvm = self.testnetvm
  266. self.assertNotEqual(self.run_cmd(self.testvm1,
  267. self.ping_cmd.format(target=self.testvm2.ip)), 0)
  268. self.assertNotEqual(self.run_cmd(self.testvm2,
  269. self.ping_cmd.format(target=self.testvm1.ip)), 0)
  270. def test_050_spoof_ip(self):
  271. """Test if VM IP spoofing is blocked"""
  272. self.loop.run_until_complete(self.testvm1.start())
  273. self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0)
  274. self.assertEqual(self.run_cmd(self.testnetvm,
  275. 'iptables -I INPUT -i vif+ ! -s {} -p icmp -j LOG'.format(
  276. self.testvm1.ip)), 0)
  277. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  278. 'ip addr flush dev eth0 && '
  279. 'ip addr add 10.137.1.128/24 dev eth0 && '
  280. 'ip route add default dev eth0',
  281. user='root'))
  282. self.assertNotEqual(self.run_cmd(self.testvm1, self.ping_ip), 0,
  283. "Spoofed ping should be blocked")
  284. try:
  285. (output, _) = self.loop.run_until_complete(
  286. self.testnetvm.run_for_stdio('iptables -nxvL INPUT',
  287. user='root'))
  288. except subprocess.CalledProcessError:
  289. self.fail('iptables -nxvL INPUT failed')
  290. output = output.decode().splitlines()
  291. packets = output[2].lstrip().split()[0]
  292. self.assertEquals(packets, '0', 'Some packet hit the INPUT rule')
  293. def test_100_late_xldevd_startup(self):
  294. """Regression test for #1990"""
  295. # Simulater late xl devd startup
  296. cmd = "systemctl stop xendriverdomain"
  297. if self.run_cmd(self.testnetvm, cmd) != 0:
  298. self.fail("Command '%s' failed" % cmd)
  299. self.loop.run_until_complete(self.testvm1.start())
  300. cmd = "systemctl start xendriverdomain"
  301. if self.run_cmd(self.testnetvm, cmd) != 0:
  302. self.fail("Command '%s' failed" % cmd)
  303. self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0)
  304. def test_200_fake_ip_simple(self):
  305. '''Test hiding VM real IP'''
  306. self.testvm1.features['net.fake-ip'] = '192.168.1.128'
  307. self.testvm1.features['net.fake-gateway'] = '192.168.1.1'
  308. self.testvm1.features['net.fake-netmask'] = '255.255.255.0'
  309. self.app.save()
  310. self.loop.run_until_complete(self.testvm1.start())
  311. self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0)
  312. self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0)
  313. try:
  314. (output, _) = self.loop.run_until_complete(
  315. self.testvm1.run_for_stdio(
  316. 'ip addr show dev eth0', user='root'))
  317. except subprocess.CalledProcessError:
  318. self.fail('ip addr show dev eth0 failed')
  319. output = output.decode()
  320. self.assertIn('192.168.1.128', output)
  321. self.assertNotIn(self.testvm1.ip, output)
  322. try:
  323. (output, _) = self.loop.run_until_complete(
  324. self.testvm1.run_for_stdio('ip route show', user='root'))
  325. except subprocess.CalledProcessError:
  326. self.fail('ip route show failed')
  327. output = output.decode()
  328. self.assertIn('192.168.1.1', output)
  329. self.assertNotIn(self.testvm1.netvm.ip, output)
  330. def test_201_fake_ip_without_gw(self):
  331. '''Test hiding VM real IP'''
  332. self.testvm1.features['net.fake-ip'] = '192.168.1.128'
  333. self.app.save()
  334. self.loop.run_until_complete(self.testvm1.start())
  335. self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0)
  336. self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0)
  337. try:
  338. (output, _) = self.loop.run_until_complete(
  339. self.testvm1.run_for_stdio('ip addr show dev eth0',
  340. user='root'))
  341. except subprocess.CalledProcessError:
  342. self.fail('ip addr show dev eth0 failed')
  343. output = output.decode()
  344. self.assertIn('192.168.1.128', output)
  345. self.assertNotIn(self.testvm1.ip, output)
  346. def test_202_fake_ip_firewall(self):
  347. '''Test hiding VM real IP, firewall'''
  348. self.testvm1.features['net.fake-ip'] = '192.168.1.128'
  349. self.testvm1.features['net.fake-gateway'] = '192.168.1.1'
  350. self.testvm1.features['net.fake-netmask'] = '255.255.255.0'
  351. self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
  352. name=self.make_vm_name('proxy'),
  353. label='red')
  354. self.proxy.provides_network = True
  355. self.loop.run_until_complete(self.proxy.create_on_disk())
  356. self.proxy.netvm = self.testnetvm
  357. self.testvm1.netvm = self.proxy
  358. self.app.save()
  359. if self.run_cmd(self.testnetvm, 'nc -h 2>&1|grep -q nmap.org') == 0:
  360. nc_version = NcVersion.Nmap
  361. else:
  362. nc_version = NcVersion.Trad
  363. # block all but ICMP and DNS
  364. self.testvm1.firewall.rules = [
  365. qubes.firewall.Rule(None, action='accept', proto='icmp'),
  366. qubes.firewall.Rule(None, action='accept', specialtarget='dns'),
  367. ]
  368. self.testvm1.firewall.save()
  369. self.loop.run_until_complete(self.testvm1.start())
  370. self.assertTrue(self.proxy.is_running())
  371. nc = self.loop.run_until_complete(self.testnetvm.run(
  372. 'nc -l --send-only -e /bin/hostname -k 1234'
  373. if nc_version == NcVersion.Nmap
  374. else 'while nc -l -e /bin/hostname -p 1234; do true; done'))
  375. try:
  376. self.assertEqual(self.run_cmd(self.proxy, self.ping_ip), 0,
  377. "Ping by IP from ProxyVM failed")
  378. self.assertEqual(self.run_cmd(self.proxy, self.ping_name), 0,
  379. "Ping by name from ProxyVM failed")
  380. self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0,
  381. "Ping by IP should be allowed")
  382. self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0,
  383. "Ping by name should be allowed")
  384. if nc_version == NcVersion.Nmap:
  385. nc_cmd = "nc -w 1 --recv-only {} 1234".format(self.test_ip)
  386. else:
  387. nc_cmd = "nc -w 1 {} 1234".format(self.test_ip)
  388. self.assertNotEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
  389. "TCP connection should be blocked")
  390. finally:
  391. nc.terminate()
  392. self.loop.run_until_complete(nc.wait())
  393. def test_203_fake_ip_inter_vm_allow(self):
  394. '''Access VM with "fake IP" from other VM (when firewall allows)'''
  395. self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
  396. name=self.make_vm_name('proxy'),
  397. label='red')
  398. self.loop.run_until_complete(self.proxy.create_on_disk())
  399. self.proxy.provides_network = True
  400. self.proxy.netvm = self.testnetvm
  401. self.testvm1.netvm = self.proxy
  402. self.testvm1.features['net.fake-ip'] = '192.168.1.128'
  403. self.testvm1.features['net.fake-gateway'] = '192.168.1.1'
  404. self.testvm1.features['net.fake-netmask'] = '255.255.255.0'
  405. self.testvm2 = self.app.add_new_vm(qubes.vm.appvm.AppVM,
  406. name=self.make_vm_name('vm2'),
  407. label='red')
  408. self.loop.run_until_complete(self.testvm2.create_on_disk())
  409. self.testvm2.netvm = self.proxy
  410. self.app.save()
  411. self.loop.run_until_complete(self.testvm1.start())
  412. self.loop.run_until_complete(self.testvm2.start())
  413. try:
  414. cmd = 'iptables -I FORWARD -s {} -d {} -j ACCEPT'.format(
  415. self.testvm2.ip, self.testvm1.ip)
  416. self.loop.run_until_complete(self.proxy.run_for_stdio(
  417. cmd, user='root'))
  418. except subprocess.CalledProcessError as e:
  419. raise AssertionError(
  420. '{} failed with: {}'.format(cmd, e.returncode)) from None
  421. try:
  422. cmd = 'iptables -I INPUT -s {} -j ACCEPT'.format(self.testvm2.ip)
  423. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  424. cmd, user='root'))
  425. except subprocess.CalledProcessError as e:
  426. raise AssertionError(
  427. '{} failed with: {}'.format(cmd, e.returncode)) from None
  428. self.assertEqual(self.run_cmd(self.testvm2,
  429. self.ping_cmd.format(target=self.testvm1.ip)), 0)
  430. try:
  431. cmd = 'iptables -nvxL INPUT | grep {}'.format(self.testvm2.ip)
  432. (stdout, _) = self.loop.run_until_complete(
  433. self.testvm1.run_for_stdio(cmd, user='root'))
  434. except subprocess.CalledProcessError as e:
  435. raise AssertionError(
  436. '{} failed with {}'.format(cmd, e.returncode)) from None
  437. self.assertNotEqual(stdout.decode().split()[0], '0',
  438. 'Packets didn\'t managed to the VM')
  439. def test_204_fake_ip_proxy(self):
  440. '''Test hiding VM real IP'''
  441. self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
  442. name=self.make_vm_name('proxy'),
  443. label='red')
  444. self.loop.run_until_complete(self.proxy.create_on_disk())
  445. self.proxy.provides_network = True
  446. self.proxy.netvm = self.testnetvm
  447. self.proxy.features['net.fake-ip'] = '192.168.1.128'
  448. self.proxy.features['net.fake-gateway'] = '192.168.1.1'
  449. self.proxy.features['net.fake-netmask'] = '255.255.255.0'
  450. self.testvm1.netvm = self.proxy
  451. self.app.save()
  452. self.loop.run_until_complete(self.testvm1.start())
  453. self.assertEqual(self.run_cmd(self.proxy, self.ping_ip), 0)
  454. self.assertEqual(self.run_cmd(self.proxy, self.ping_name), 0)
  455. self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0)
  456. self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0)
  457. try:
  458. (output, _) = self.loop.run_until_complete(
  459. self.proxy.run_for_stdio(
  460. 'ip addr show dev eth0', user='root'))
  461. except subprocess.CalledProcessError as e:
  462. self.fail('ip addr show dev eth0 failed')
  463. output = output.decode()
  464. self.assertIn('192.168.1.128', output)
  465. self.assertNotIn(self.testvm1.ip, output)
  466. try:
  467. (output, _) = self.loop.run_until_complete(
  468. self.proxy.run_for_stdio(
  469. 'ip route show', user='root'))
  470. except subprocess.CalledProcessError as e:
  471. self.fail('ip route show failed')
  472. output = output.decode()
  473. self.assertIn('192.168.1.1', output)
  474. self.assertNotIn(self.testvm1.netvm.ip, output)
  475. try:
  476. (output, _) = self.loop.run_until_complete(
  477. self.testvm1.run_for_stdio(
  478. 'ip addr show dev eth0', user='root'))
  479. except subprocess.CalledProcessError as e:
  480. self.fail('ip addr show dev eth0 failed')
  481. output = output.decode()
  482. self.assertNotIn('192.168.1.128', output)
  483. self.assertIn(self.testvm1.ip, output)
  484. try:
  485. (output, _) = self.loop.run_until_complete(
  486. self.testvm1.run_for_stdio(
  487. 'ip route show', user='root'))
  488. except subprocess.CalledProcessError as e:
  489. self.fail('ip route show failed')
  490. output = output.decode()
  491. self.assertIn('192.168.1.128', output)
  492. self.assertNotIn(self.proxy.ip, output)
  493. def test_210_custom_ip_simple(self):
  494. '''Custom AppVM IP'''
  495. self.testvm1.ip = '192.168.1.1'
  496. self.app.save()
  497. self.loop.run_until_complete(self.testvm1.start())
  498. self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0)
  499. self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0)
  500. def test_211_custom_ip_proxy(self):
  501. '''Custom ProxyVM IP'''
  502. self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
  503. name=self.make_vm_name('proxy'),
  504. label='red')
  505. self.loop.run_until_complete(self.proxy.create_on_disk())
  506. self.proxy.provides_network = True
  507. self.proxy.netvm = self.testnetvm
  508. self.proxy.ip = '192.168.1.1'
  509. self.testvm1.netvm = self.proxy
  510. self.app.save()
  511. self.loop.run_until_complete(self.testvm1.start())
  512. self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0)
  513. self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0)
  514. def test_212_custom_ip_firewall(self):
  515. '''Custom VM IP and firewall'''
  516. self.testvm1.ip = '192.168.1.1'
  517. self.proxy = self.app.add_new_vm(qubes.vm.appvm.AppVM,
  518. name=self.make_vm_name('proxy'),
  519. label='red')
  520. self.proxy.provides_network = True
  521. self.loop.run_until_complete(self.proxy.create_on_disk())
  522. self.proxy.netvm = self.testnetvm
  523. self.testvm1.netvm = self.proxy
  524. self.app.save()
  525. if self.run_cmd(self.testnetvm, 'nc -h 2>&1|grep -q nmap.org') == 0:
  526. nc_version = NcVersion.Nmap
  527. else:
  528. nc_version = NcVersion.Trad
  529. # block all but ICMP and DNS
  530. self.testvm1.firewall.rules = [
  531. qubes.firewall.Rule(None, action='accept', proto='icmp'),
  532. qubes.firewall.Rule(None, action='accept', specialtarget='dns'),
  533. ]
  534. self.testvm1.firewall.save()
  535. self.loop.run_until_complete(self.testvm1.start())
  536. self.assertTrue(self.proxy.is_running())
  537. nc = self.loop.run_until_complete(self.testnetvm.run(
  538. 'nc -l --send-only -e /bin/hostname -k 1234'
  539. if nc_version == NcVersion.Nmap
  540. else 'while nc -l -e /bin/hostname -p 1234; do true; done'))
  541. try:
  542. self.assertEqual(self.run_cmd(self.proxy, self.ping_ip), 0,
  543. "Ping by IP from ProxyVM failed")
  544. self.assertEqual(self.run_cmd(self.proxy, self.ping_name), 0,
  545. "Ping by name from ProxyVM failed")
  546. self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0,
  547. "Ping by IP should be allowed")
  548. self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0,
  549. "Ping by name should be allowed")
  550. if nc_version == NcVersion.Nmap:
  551. nc_cmd = "nc -w 1 --recv-only {} 1234".format(self.test_ip)
  552. else:
  553. nc_cmd = "nc -w 1 {} 1234".format(self.test_ip)
  554. self.assertNotEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
  555. "TCP connection should be blocked")
  556. finally:
  557. nc.terminate()
  558. self.loop.run_until_complete(nc.wait())
  559. # noinspection PyAttributeOutsideInit
  560. class VmUpdatesMixin(object):
  561. """
  562. Tests for VM updates
  563. """
  564. # filled by load_tests
  565. template = None
  566. # made this way to work also when no package build tools are installed
  567. """
  568. $ cat test-pkg.spec:
  569. Name: test-pkg
  570. Version: 1.0
  571. Release: 1%{?dist}
  572. Summary: Test package
  573. Group: System
  574. License: GPL
  575. URL: http://example.com/
  576. %description
  577. Test package
  578. %files
  579. %changelog
  580. $ rpmbuild -bb test-pkg.spec
  581. $ cat test-pkg-1.0-1.fc21.x86_64.rpm | gzip | base64
  582. """
  583. RPM_PACKAGE_GZIP_BASE64 = (
  584. b"H4sIAPzRLlYAA+2Y728URRjHn7ueUCkERKJVJDnTxLSxs7293o8WOER6ljYYrtKCLUSa3"
  585. b"bnZ64bd22VmTq8nr4wJbwxvjNHIG0x8oTHGGCHB8AcYE1/0lS80GgmQFCJU3wgB4ZjdfZ"
  586. b"q2xDe8NNlvMjfzmeeZH7tPbl98b35169cOUEpIJiTxT9SIrmVUs2hWh8dUAp54dOrM14s"
  587. b"JHK4D2DKl+j2qrVfjsuq3qEWbohjuAB2Lqk+p1o/8Z5QPmSi/YwnjezH+F8bLQZjqllW0"
  588. b"hvODRmFIL5hFk9JMXi/mi5ZuDleNwSEzP5wtmLnouNQnm3/6fndz7FLt9M/Hruj37gav4"
  589. b"tTjPnasWLFixYoVK1asWLFixYoV63+p0KNot9vnIPQc1vgYOwCSgXfxCoS+QzKHOVXVOj"
  590. b"Fn2ccIfI0k8nXkLuQbyJthxed4UrVnkG8i9yDfgsj3yCAv4foc8t+w1hf5B+Nl5Du43xj"
  591. b"yvxivIN9HpsgPkO2IU9uQfeRn8Xk/iJ4x1Y3nfxH1qecwfhH5+YgT25F7o/0SRdxvOppP"
  592. b"7MX9ZjB/DNnE/OOYX404uRGZIT+FbCFvQ3aQ8f0+/WF0XjJ8nyOw7H+BrmUA/a8pNZf2D"
  593. b"XrCqLG1cERbWHI8ajhznpBY9P0Tr8PkvJDMhTkp/Z0DA6xpuL7DNOq5A+DY9UYTmkOF2U"
  594. b"IO/sNt0wSnGvfdlZssD3rVIlLI9UUX37C6qXzHNntHPNfnTAhWHbUddtBwmegDjAUzZbu"
  595. b"m9lqZmzDmHc8Ik8WY8Tab4Myym4+Gx8V0qw8GtYyWIzrktEJwV9UHv3ktG471rAqHTmFQ"
  596. b"685V5uGqIalk06SWJr7tszR503Ac9cs493jJ8rhrSCIYbXBbzqt5v5+UZ0crh6bGR2dmJ"
  597. b"yuHD428VlLLLdakzJe2VxcKhFSFID73JKPS40RI7tXVCcQ3uOGWhPCJ2bAspiJ2i5Vy6n"
  598. b"jOqMerpEYpEe/Yks4xkU4Tt6BirmzUWanG6ozbFKhve9BsQRaLRTirzqk7hgUktXojKnf"
  599. b"n8jeg3X4QepP3i63po6oml+9t/CwJLya2Bn/ei6f7/4B3Ycdb0L3pt5Q5mNz16rWJ9fLk"
  600. b"vvOff/nxS7//8O2P2gvt7nDDnoV9L1du9N4+ucjl9u/8+a7dC5Nnvjlv9Ox5r+v9Cy0NE"
  601. b"m+c6rv60S/dZw98Gn6MNswcfQiWUvg3wBUAAA=="
  602. )
  603. """
  604. Minimal package generated by running dh_make on empty directory
  605. Then cat test-pkg_1.0-1_amd64.deb | gzip | base64
  606. """
  607. DEB_PACKAGE_GZIP_BASE64 = (
  608. b"H4sIACTXLlYAA1O0SSxKzrDjSklNykzM003KzEssqlRQUDA0MTG1NDQwNDVTUDBQAAEIa"
  609. b"WhgYGZioqBgogADCVxGegZcyfl5JUX5OXoliUV66VVE6DcwheuX7+ZgAAEW5rdXHb0PG4"
  610. b"iwf5j3WfMT6zWzzMuZgoE3jjYraNzbbFKWGms0SaRw/r2SV23WZ4IdP8preM4yqf0jt95"
  611. b"3c8qnacfNxJUkf9/w+/3X9ph2GEdgQdixrz/niHKKTnYXizf4oSC7tHOz2Zzq+/6vn8/7"
  612. b"ezQ7c1tmi7xZ3SGJ4yzhT2dcr7V+W3zM5ZPu/56PSv4Zdok+7Yv/V/6buWaKVlFkkV58S"
  613. b"N3GmLgnqzRmeZ3V3ymmurS5fGa85/LNx1bpZMin3S6dvXKqydp3ubP1vmyarJZb/qSh62"
  614. b"C8oIdxqm/BtvkGDza+On/Vfv2py7/0LV7VH+qR6a+bkKUbHXt5/SG187d+nps1a5PJfMO"
  615. b"i11dWcUe1HjwaW3Q5RHXn9LmcHy+tW9YcKf0768XVB1t3R0bKrzs5t9P+6r7rZ99svH10"
  616. b"+Q6F/o8tf1fO/32y+fWa14eifd+WxUy0jcxYH7N9/tUvmnUZL74pW32qLeuRU+ZwYGASa"
  617. b"GBgUWBgxM90ayy3VdmykkGDgYErJbEkERydFVWQmCMQo8aWZvAY/WteFRHFwMCYqXTPjI"
  618. b"lBkVEMGLsl+k8XP1D/z+gXyyDOvUemlnHqAVkvu0rRQ2fUFodkN3mtU9uwhqk8V+TqPEE"
  619. b"Nc7fzoQ4n71lqRs/7kbbT0+qOZuKH4r8mjzsc1k/YkCHN8Pjg48fbpE+teHa96LNcfu0V"
  620. b"5n2/Z2xa2KDvaCOx8cqBFxc514uZ3TmadXS+6cpzU7wSzq5SWfapJOD9n6wLXSwtlgxZh"
  621. b"xITzWW7buhx/bb291RcVlEfeC9K5hlrqunSzIMSZT7/Nqgc/qMvMNW227WI8ezB8mVuZh"
  622. b"0hERJSvysfburr4Dx0I9BW57UwR4+e1gxu49PcEt8sbK18Xpvt//Hj5UYm+Zc25q+T4xl"
  623. b"rJvxfVnh80oadq57OZxPaU1bbztv1yF365W4t45Yr+XrFzov237GVY1Zgf7NvE4+W2SuR"
  624. b"lQtLauR1TQ/mbOiIONYya6tU1jPGpWfk/i1+ttiXe3ZO14n0YOWggndznjGlGLyfVbBC6"
  625. b"MRP5aMM7aCco/s7sZqB8RlTQwADw8rnuT/sDHi7mUASjJFRAAbWwNLiAwAA"
  626. )
  627. def run_cmd(self, vm, cmd, user="root"):
  628. try:
  629. self.loop.run_until_complete(vm.run_for_stdio(cmd))
  630. except subprocess.CalledProcessError as e:
  631. return e.returncode
  632. return 0
  633. def setUp(self):
  634. if not self.template.count('debian') and \
  635. not self.template.count('fedora'):
  636. self.skipTest("Template {} not supported by this test".format(
  637. self.template))
  638. super(VmUpdatesMixin, self).setUp()
  639. self.update_cmd = None
  640. if self.template.count("debian"):
  641. self.update_cmd = "set -o pipefail; apt-get update 2>&1 | " \
  642. "{ ! grep '^W:\|^E:'; }"
  643. self.install_cmd = "apt-get install -y {}"
  644. self.install_test_cmd = "dpkg -l {}"
  645. self.exit_code_ok = [0]
  646. elif self.template.count("fedora"):
  647. cmd = "yum"
  648. try:
  649. # assume template name in form "fedora-XX-suffix"
  650. if int(self.template.split("-")[1]) > 21:
  651. cmd = "dnf"
  652. except ValueError:
  653. pass
  654. self.update_cmd = "{cmd} clean all; {cmd} check-update".format(
  655. cmd=cmd)
  656. self.install_cmd = cmd + " install -y {}"
  657. self.install_test_cmd = "rpm -q {}"
  658. self.exit_code_ok = [0, 100]
  659. self.init_default_template(self.template)
  660. self.init_networking()
  661. self.testvm1 = self.app.add_new_vm(
  662. qubes.vm.appvm.AppVM,
  663. name=self.make_vm_name('vm1'),
  664. label='red')
  665. self.loop.run_until_complete(self.testvm1.create_on_disk())
  666. def test_000_simple_update(self):
  667. self.app.save()
  668. # reload the VM to have all the properties properly set (especially
  669. # default netvm)
  670. self.testvm1 = self.app.domains[self.testvm1.qid]
  671. self.loop.run_until_complete(self.testvm1.start())
  672. p = self.loop.run_until_complete(
  673. self.testvm1.run(self.update_cmd, user='root',
  674. stdout=subprocess.PIPE, stderr=subprocess.PIPE))
  675. (stdout, stderr) = self.loop.run_until_complete(p.communicate())
  676. self.assertIn(p.returncode, self.exit_code_ok,
  677. '{}: {}\n{}'.format(self.update_cmd, stdout, stderr))
  678. def create_repo_apt(self):
  679. pkg_file_name = "test-pkg_1.0-1_amd64.deb"
  680. self.loop.run_until_complete(self.netvm_repo.run_for_stdio('''
  681. mkdir /tmp/apt-repo \
  682. && cd /tmp/apt-repo \
  683. && base64 -d | zcat > {}
  684. '''.format(pkg_file_name),
  685. input=self.DEB_PACKAGE_GZIP_BASE64))
  686. # do not assume dpkg-scanpackage installed
  687. packages_path = "dists/test/main/binary-amd64/Packages"
  688. self.loop.run_until_complete(self.netvm_repo.run_for_stdio('''
  689. mkdir -p /tmp/apt-repo/dists/test/main/binary-amd64 \
  690. && cd /tmp/apt-repo \
  691. && cat > {packages} \
  692. && echo MD5sum: $(openssl md5 -r {pkg} | cut -f 1 -d ' ') \
  693. >> {packages} \
  694. && echo SHA1: $(openssl sha1 -r {pkg} | cut -f 1 -d ' ') \
  695. >> {packages} \
  696. && echo SHA256: $(openssl sha256 -r {pkg} | cut -f 1 -d ' ') \
  697. >> {packages} \
  698. && gzip < {packages} > {packages}.gz
  699. '''.format(pkg=pkg_file_name, packages=packages_path),
  700. input='''\
  701. Package: test-pkg
  702. Version: 1.0-1
  703. Architecture: amd64
  704. Maintainer: unknown <user@host>
  705. Installed-Size: 25
  706. Filename: {pkg}
  707. Size: 994
  708. Section: unknown
  709. Priority: optional
  710. Description: Test package'''.format(pkg=pkg_file_name).encode('utf-8')))
  711. self.loop.run_until_complete(self.netvm_repo.run_for_stdio('''
  712. mkdir -p /tmp/apt-repo/dists/test \
  713. && cd /tmp/apt-repo/dists/test \
  714. && cat > Release \
  715. && echo '' $(sha256sum {p} | cut -f 1 -d ' ') $(stat -c %s {p}) {p}\
  716. >> Release \
  717. && echo '' $(sha256sum {z} | cut -f 1 -d ' ') $(stat -c %s {z}) {z}\
  718. >> Release
  719. '''.format(p='main/binary-amd64/Packages',
  720. z='main/binary-amd64/Packages.gz'),
  721. input=b'''\
  722. Label: Test repo
  723. Suite: test
  724. Codename: test
  725. Date: Tue, 27 Oct 2015 03:22:09 UTC
  726. Architectures: amd64
  727. Components: main
  728. SHA256:
  729. '''))
  730. def create_repo_yum(self):
  731. pkg_file_name = "test-pkg-1.0-1.fc21.x86_64.rpm"
  732. self.loop.run_until_complete(self.netvm_repo.run_for_stdio('''
  733. mkdir /tmp/yum-repo \
  734. && cd /tmp/yum-repo \
  735. && base64 -d | zcat > {}
  736. '''.format(pkg_file_name), input=self.RPM_PACKAGE_GZIP_BASE64))
  737. # createrepo is installed by default in Fedora template
  738. self.loop.run_until_complete(self.netvm_repo.run_for_stdio(
  739. 'createrepo /tmp/yum-repo'))
  740. def create_repo_and_serve(self):
  741. if self.template.count("debian") or self.template.count("whonix"):
  742. self.create_repo_apt()
  743. self.loop.run_until_complete(self.netvm_repo.run(
  744. 'cd /tmp/apt-repo && python -m SimpleHTTPServer 8080',
  745. stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL))
  746. elif self.template.count("fedora"):
  747. self.create_repo_yum()
  748. self.loop.run_until_complete(self.netvm_repo.run(
  749. 'cd /tmp/yum-repo && python -m SimpleHTTPServer 8080',
  750. stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL))
  751. else:
  752. # not reachable...
  753. self.skipTest("Template {} not supported by this test".format(
  754. self.template))
  755. def configure_test_repo(self):
  756. """
  757. Configure test repository in test-vm and disable rest of them.
  758. The critical part is to use "localhost" - this will work only when
  759. accessed through update proxy and this is exactly what we want to
  760. test here.
  761. """
  762. if self.template.count("debian") or self.template.count("whonix"):
  763. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  764. "rm -f /etc/apt/sources.list.d/* &&"
  765. "echo 'deb [trusted=yes] http://localhost:8080 test main' "
  766. "> /etc/apt/sources.list",
  767. user="root"))
  768. elif self.template.count("fedora"):
  769. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  770. "rm -f /etc/yum.repos.d/*.repo &&"
  771. "echo '[test]' > /etc/yum.repos.d/test.repo &&"
  772. "echo 'name=Test repo' >> /etc/yum.repos.d/test.repo &&"
  773. "echo 'gpgcheck=0' >> /etc/yum.repos.d/test.repo &&"
  774. "echo 'baseurl=http://localhost:8080/'"
  775. " >> /etc/yum.repos.d/test.repo",
  776. user="root"
  777. ))
  778. else:
  779. # not reachable...
  780. self.skipTest("Template {} not supported by this test".format(
  781. self.template))
  782. def test_010_update_via_proxy(self):
  783. """
  784. Test both whether updates proxy works and whether is actually used by the VM
  785. """
  786. if self.template.count("minimal"):
  787. self.skipTest("Template {} not supported by this test".format(
  788. self.template))
  789. self.netvm_repo = self.app.add_new_vm(
  790. qubes.vm.appvm.AppVM,
  791. name=self.make_vm_name('net'),
  792. label='red')
  793. self.netvm_repo.provides_network = True
  794. self.loop.run_until_complete(self.netvm_repo.create_on_disk())
  795. self.testvm1.netvm = self.netvm_repo
  796. self.netvm_repo.features['service.qubes-updates-proxy'] = True
  797. # TODO: consider also adding a test for the template itself
  798. self.testvm1.features['service.updates-proxy-setup'] = True
  799. self.app.save()
  800. # Setup test repo
  801. self.loop.run_until_complete(self.netvm_repo.start())
  802. self.create_repo_and_serve()
  803. # Configure local repo
  804. self.loop.run_until_complete(self.testvm1.start())
  805. self.configure_test_repo()
  806. with self.qrexec_policy('qubes.UpdatesProxy', self.testvm1,
  807. '$default', action='allow,target=' + self.netvm_repo.name):
  808. # update repository metadata
  809. p = self.loop.run_until_complete(self.testvm1.run(
  810. self.update_cmd, user='root', stdout=subprocess.PIPE,
  811. stderr=subprocess.PIPE))
  812. (stdout, stderr) = self.loop.run_until_complete(p.communicate())
  813. self.assertIn(self.loop.run_until_complete(p.wait()), self.exit_code_ok,
  814. '{}: {}\n{}'.format(self.update_cmd, stdout, stderr))
  815. # install test package
  816. p = self.loop.run_until_complete(self.testvm1.run(
  817. self.install_cmd.format('test-pkg'), user='root',
  818. stdout=subprocess.PIPE, stderr=subprocess.PIPE))
  819. (stdout, stderr) = self.loop.run_until_complete(p.communicate())
  820. self.assertIn(self.loop.run_until_complete(p.wait()), self.exit_code_ok,
  821. '{}: {}\n{}'.format(self.update_cmd, stdout, stderr))
  822. # verify if it was really installed
  823. p = self.loop.run_until_complete(self.testvm1.run(
  824. self.install_test_cmd.format('test-pkg'), user='root',
  825. stdout=subprocess.PIPE, stderr=subprocess.PIPE))
  826. (stdout, stderr) = self.loop.run_until_complete(p.communicate())
  827. self.assertIn(self.loop.run_until_complete(p.wait()), self.exit_code_ok,
  828. '{}: {}\n{}'.format(self.update_cmd, stdout, stderr))
  829. def load_tests(loader, tests, pattern):
  830. for template in qubes.tests.list_templates():
  831. tests.addTests(loader.loadTestsFromTestCase(
  832. type(
  833. 'VmNetworking_' + template,
  834. (VmNetworkingMixin, qubes.tests.SystemTestCase),
  835. {'template': template})))
  836. tests.addTests(loader.loadTestsFromTestCase(
  837. type(
  838. 'VmUpdates_' + template,
  839. (VmUpdatesMixin, qubes.tests.SystemTestCase),
  840. {'template': template})))
  841. return tests