network.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683
  1. #!/usr/bin/python
  2. # vim: fileencoding=utf-8
  3. #
  4. # The Qubes OS Project, https://www.qubes-os.org/
  5. #
  6. # Copyright (C) 2015
  7. # Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
  8. # Copyright (C) 2015 Wojtek Porczyk <woju@invisiblethingslab.com>
  9. #
  10. # This program is free software; you can redistribute it and/or modify
  11. # it under the terms of the GNU General Public License as published by
  12. # the Free Software Foundation; either version 2 of the License, or
  13. # (at your option) any later version.
  14. #
  15. # This program is distributed in the hope that it will be useful,
  16. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. # GNU General Public License for more details.
  19. #
  20. # You should have received a copy of the GNU General Public License along
  21. # with this program; if not, write to the Free Software Foundation, Inc.,
  22. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  23. #
  24. from distutils import spawn
  25. import multiprocessing
  26. import os
  27. import subprocess
  28. import unittest
  29. import time
  30. from qubes.qubes import QubesVmCollection, defaults
  31. import qubes.tests
  32. class NcVersion:
  33. Trad = 1
  34. Nmap = 2
  35. class VmNetworkingMixin(qubes.tests.SystemTestsMixin):
  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. def run_cmd(self, vm, cmd, user="root"):
  42. p = vm.run(cmd, user=user, passio_popen=True, ignore_stderr=True)
  43. p.stdin.close()
  44. p.stdout.read()
  45. return p.wait()
  46. def setUp(self):
  47. super(VmNetworkingMixin, self).setUp()
  48. if self.template.startswith('whonix-'):
  49. self.skipTest("Test not supported here - Whonix uses its own "
  50. "firewall settings")
  51. self.testnetvm = self.qc.add_new_vm("QubesNetVm",
  52. name=self.make_vm_name('netvm1'),
  53. template=self.qc.get_vm_by_name(self.template))
  54. self.testnetvm.create_on_disk(verbose=False)
  55. self.testvm1 = self.qc.add_new_vm("QubesAppVm",
  56. name=self.make_vm_name('vm2'),
  57. template=self.qc.get_vm_by_name(self.template))
  58. self.testvm1.create_on_disk(verbose=False)
  59. self.testvm1.netvm = self.testnetvm
  60. self.qc.save()
  61. self.configure_netvm()
  62. def configure_netvm(self):
  63. def run_netvm_cmd(cmd):
  64. if self.run_cmd(self.testnetvm, cmd) != 0:
  65. self.fail("Command '%s' failed" % cmd)
  66. if not self.testnetvm.is_running():
  67. self.testnetvm.start()
  68. # Ensure that dnsmasq is installed:
  69. p = self.testnetvm.run("dnsmasq --version", user="root",
  70. passio_popen=True)
  71. if p.wait() != 0:
  72. self.skipTest("dnsmasq not installed")
  73. run_netvm_cmd("ip link add test0 type dummy")
  74. run_netvm_cmd("ip link set test0 up")
  75. run_netvm_cmd("ip addr add {}/24 dev test0".format(self.test_ip))
  76. run_netvm_cmd("iptables -I INPUT -d {} -j ACCEPT".format(self.test_ip))
  77. # ignore failure
  78. self.run_cmd(self.testnetvm, "killall --wait dnsmasq")
  79. run_netvm_cmd("dnsmasq -a {ip} -A /{name}/{ip} -i test0 -z".format(
  80. ip=self.test_ip, name=self.test_name))
  81. run_netvm_cmd("echo nameserver {} > /etc/resolv.conf".format(
  82. self.test_ip))
  83. run_netvm_cmd("/usr/lib/qubes/qubes-setup-dnat-to-ns")
  84. def test_000_simple_networking(self):
  85. self.qc.unlock_db()
  86. self.testvm1.start()
  87. self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0)
  88. self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0)
  89. def test_010_simple_proxyvm(self):
  90. self.proxy = self.qc.add_new_vm("QubesProxyVm",
  91. name=self.make_vm_name('proxy'),
  92. template=self.qc.get_vm_by_name(self.template))
  93. self.proxy.create_on_disk(verbose=False)
  94. self.proxy.netvm = self.testnetvm
  95. self.testvm1.netvm = self.proxy
  96. self.qc.save()
  97. self.qc.unlock_db()
  98. self.testvm1.start()
  99. self.assertTrue(self.proxy.is_running())
  100. self.assertEqual(self.run_cmd(self.proxy, self.ping_ip), 0,
  101. "Ping by IP from ProxyVM failed")
  102. self.assertEqual(self.run_cmd(self.proxy, self.ping_name), 0,
  103. "Ping by name from ProxyVM failed")
  104. self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0,
  105. "Ping by IP from AppVM failed")
  106. self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0,
  107. "Ping by IP from AppVM failed")
  108. @qubes.tests.expectedFailureIfTemplate('debian-7')
  109. @unittest.skipUnless(spawn.find_executable('xdotool'),
  110. "xdotool not installed")
  111. def test_020_simple_proxyvm_nm(self):
  112. self.proxy = self.qc.add_new_vm("QubesProxyVm",
  113. name=self.make_vm_name('proxy'),
  114. template=self.qc.get_vm_by_name(self.template))
  115. self.proxy.create_on_disk(verbose=False)
  116. self.proxy.netvm = self.testnetvm
  117. self.proxy.services['network-manager'] = True
  118. self.testvm1.netvm = self.proxy
  119. self.qc.save()
  120. self.qc.unlock_db()
  121. self.testvm1.start()
  122. self.assertTrue(self.proxy.is_running())
  123. self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0,
  124. "Ping by IP failed")
  125. self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0,
  126. "Ping by name failed")
  127. # reconnect to make sure that device was configured by NM
  128. self.assertEqual(
  129. self.run_cmd(self.proxy, "nmcli device disconnect eth0",
  130. user="user"),
  131. 0, "Failed to disconnect eth0 using nmcli")
  132. self.assertNotEqual(self.run_cmd(self.testvm1, self.ping_ip), 0,
  133. "Network should be disabled, but apparently it isn't")
  134. self.assertEqual(
  135. self.run_cmd(self.proxy,
  136. 'nmcli connection up "VM uplink eth0" ifname eth0',
  137. user="user"),
  138. 0, "Failed to connect eth0 using nmcli")
  139. self.assertEqual(self.run_cmd(self.proxy, "nm-online", user="user"), 0,
  140. "Failed to wait for NM connection")
  141. # check for nm-applet presence
  142. self.assertEqual(subprocess.call([
  143. 'xdotool', 'search', '--all', '--name',
  144. '--class', '^(NetworkManager Applet|{})$'.format(self.proxy.name)],
  145. stdout=open('/dev/null', 'w')), 0, "nm-applet window not found")
  146. self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0,
  147. "Ping by IP failed (after NM reconnection")
  148. self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0,
  149. "Ping by name failed (after NM reconnection)")
  150. def test_030_firewallvm_firewall(self):
  151. self.proxy = self.qc.add_new_vm("QubesProxyVm",
  152. name=self.make_vm_name('proxy'),
  153. template=self.qc.get_vm_by_name(self.template))
  154. self.proxy.create_on_disk(verbose=False)
  155. self.proxy.netvm = self.testnetvm
  156. self.testvm1.netvm = self.proxy
  157. self.qc.save()
  158. self.qc.unlock_db()
  159. if self.run_cmd(self.testnetvm, 'nc -h 2>&1|grep -q nmap.org') == 0:
  160. nc_version = NcVersion.Nmap
  161. else:
  162. nc_version = NcVersion.Trad
  163. # block all for first
  164. self.testvm1.write_firewall_conf({
  165. 'allow': False,
  166. 'allowDns': False,
  167. 'allowIcmp': False,
  168. })
  169. self.testvm1.start()
  170. self.assertTrue(self.proxy.is_running())
  171. if nc_version == NcVersion.Nmap:
  172. self.testnetvm.run("nc -l --send-only -e /bin/hostname -k 1234")
  173. else:
  174. self.testnetvm.run("while nc -l -e /bin/hostname -p 1234; do "
  175. "true; done")
  176. self.assertEqual(self.run_cmd(self.proxy, self.ping_ip), 0,
  177. "Ping by IP from ProxyVM failed")
  178. self.assertEqual(self.run_cmd(self.proxy, self.ping_name), 0,
  179. "Ping by name from ProxyVM failed")
  180. self.assertNotEqual(self.run_cmd(self.testvm1, self.ping_ip), 0,
  181. "Ping by IP should be blocked")
  182. if nc_version == NcVersion.Nmap:
  183. nc_cmd = "nc -w 1 --recv-only {} 1234".format(self.test_ip)
  184. else:
  185. nc_cmd = "nc -w 1 {} 1234".format(self.test_ip)
  186. self.assertNotEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
  187. "TCP connection should be blocked")
  188. # block all except ICMP
  189. self.testvm1.write_firewall_conf({
  190. 'allow': False,
  191. 'allowDns': False,
  192. 'allowIcmp': True,
  193. })
  194. self.proxy.write_iptables_qubesdb_entry()
  195. # Ugly hack b/c there is no feedback when the rules are actually applied
  196. time.sleep(3)
  197. self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0,
  198. "Ping by IP failed (should be allowed now)")
  199. self.assertNotEqual(self.run_cmd(self.testvm1, self.ping_name), 0,
  200. "Ping by name should be blocked")
  201. # all TCP still blocked
  202. self.testvm1.write_firewall_conf({
  203. 'allow': False,
  204. 'allowDns': True,
  205. 'allowIcmp': True,
  206. })
  207. self.proxy.write_iptables_qubesdb_entry()
  208. # Ugly hack b/c there is no feedback when the rules are actually applied
  209. time.sleep(3)
  210. self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0,
  211. "Ping by name failed (should be allowed now)")
  212. self.assertNotEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
  213. "TCP connection should be blocked")
  214. # block all except target
  215. self.testvm1.write_firewall_conf({
  216. 'allow': False,
  217. 'allowDns': True,
  218. 'allowIcmp': True,
  219. 'rules': [{'address': self.test_ip,
  220. 'netmask': 32,
  221. 'proto': 'tcp',
  222. 'portBegin': 1234
  223. }] })
  224. self.proxy.write_iptables_qubesdb_entry()
  225. # Ugly hack b/c there is no feedback when the rules are actually applied
  226. time.sleep(3)
  227. self.assertEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
  228. "TCP connection failed (should be allowed now)")
  229. # allow all except target
  230. self.testvm1.write_firewall_conf({
  231. 'allow': True,
  232. 'allowDns': True,
  233. 'allowIcmp': True,
  234. 'rules': [{'address': self.test_ip,
  235. 'netmask': 32,
  236. 'proto': 'tcp',
  237. 'portBegin': 1234
  238. }]
  239. })
  240. self.proxy.write_iptables_qubesdb_entry()
  241. # Ugly hack b/c there is no feedback when the rules are actually applied
  242. time.sleep(3)
  243. self.assertNotEqual(self.run_cmd(self.testvm1, nc_cmd), 0,
  244. "TCP connection should be blocked")
  245. def test_040_inter_vm(self):
  246. self.proxy = self.qc.add_new_vm("QubesProxyVm",
  247. name=self.make_vm_name('proxy'),
  248. template=self.qc.get_vm_by_name(self.template))
  249. self.proxy.create_on_disk(verbose=False)
  250. self.proxy.netvm = self.testnetvm
  251. self.testvm1.netvm = self.proxy
  252. self.testvm2 = self.qc.add_new_vm("QubesAppVm",
  253. name=self.make_vm_name('vm3'),
  254. template=self.qc.get_vm_by_name(self.template))
  255. self.testvm2.create_on_disk(verbose=False)
  256. self.testvm2.netvm = self.proxy
  257. self.qc.save()
  258. self.qc.unlock_db()
  259. self.testvm1.start()
  260. self.testvm2.start()
  261. self.assertNotEqual(self.run_cmd(self.testvm1,
  262. self.ping_cmd.format(target=self.testvm2.ip)), 0)
  263. self.testvm2.netvm = self.testnetvm
  264. self.assertNotEqual(self.run_cmd(self.testvm1,
  265. self.ping_cmd.format(target=self.testvm2.ip)), 0)
  266. self.assertNotEqual(self.run_cmd(self.testvm2,
  267. self.ping_cmd.format(target=self.testvm1.ip)), 0)
  268. self.testvm1.netvm = self.testnetvm
  269. self.assertNotEqual(self.run_cmd(self.testvm1,
  270. self.ping_cmd.format(target=self.testvm2.ip)), 0)
  271. self.assertNotEqual(self.run_cmd(self.testvm2,
  272. self.ping_cmd.format(target=self.testvm1.ip)), 0)
  273. def test_050_spoof_ip(self):
  274. """Test if VM IP spoofing is blocked"""
  275. self.qc.unlock_db()
  276. self.testvm1.start()
  277. self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0)
  278. self.testvm1.run("ip addr flush dev eth0", user="root", wait=True)
  279. self.testvm1.run("ip addr add 10.137.1.128/24 dev eth0", user="root",
  280. wait=True)
  281. self.testvm1.run("ip route add default dev eth0", user="root",
  282. wait=True)
  283. self.assertNotEqual(self.run_cmd(self.testvm1, self.ping_ip), 0,
  284. "Spoofed ping should be blocked")
  285. def test_100_late_xldevd_startup(self):
  286. """Regression test for #1990"""
  287. self.qc.unlock_db()
  288. # Simulater late xl devd startup
  289. cmd = "systemctl stop xendriverdomain"
  290. if self.run_cmd(self.testnetvm, cmd) != 0:
  291. self.fail("Command '%s' failed" % cmd)
  292. self.testvm1.start()
  293. cmd = "systemctl start xendriverdomain"
  294. if self.run_cmd(self.testnetvm, cmd) != 0:
  295. self.fail("Command '%s' failed" % cmd)
  296. self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0)
  297. class VmUpdatesMixin(qubes.tests.SystemTestsMixin):
  298. """
  299. Tests for VM updates
  300. """
  301. # made this way to work also when no package build tools are installed
  302. """
  303. $ cat test-pkg.spec:
  304. Name: test-pkg
  305. Version: 1.0
  306. Release: 1%{?dist}
  307. Summary: Test package
  308. Group: System
  309. License: GPL
  310. URL: http://example.com/
  311. %description
  312. Test package
  313. %files
  314. %changelog
  315. $ rpmbuild -bb test-pkg.spec
  316. $ cat test-pkg-1.0-1.fc21.x86_64.rpm | gzip | base64
  317. """
  318. RPM_PACKAGE_GZIP_BASE64 = (
  319. "H4sIAPzRLlYAA+2Y728URRjHn7ueUCkERKJVJDnTxLSxs7293o8WOER6ljYYrtKCLUSa3"
  320. "bnZ64bd22VmTq8nr4wJbwxvjNHIG0x8oTHGGCHB8AcYE1/0lS80GgmQFCJU3wgB4ZjdfZ"
  321. "q2xDe8NNlvMjfzmeeZH7tPbl98b35169cOUEpIJiTxT9SIrmVUs2hWh8dUAp54dOrM14s"
  322. "JHK4D2DKl+j2qrVfjsuq3qEWbohjuAB2Lqk+p1o/8Z5QPmSi/YwnjezH+F8bLQZjqllW0"
  323. "hvODRmFIL5hFk9JMXi/mi5ZuDleNwSEzP5wtmLnouNQnm3/6fndz7FLt9M/Hruj37gav4"
  324. "tTjPnasWLFixYoVK1asWLFixYoV63+p0KNot9vnIPQc1vgYOwCSgXfxCoS+QzKHOVXVOj"
  325. "Fn2ccIfI0k8nXkLuQbyJthxed4UrVnkG8i9yDfgsj3yCAv4foc8t+w1hf5B+Nl5Du43xj"
  326. "yvxivIN9HpsgPkO2IU9uQfeRn8Xk/iJ4x1Y3nfxH1qecwfhH5+YgT25F7o/0SRdxvOppP"
  327. "7MX9ZjB/DNnE/OOYX404uRGZIT+FbCFvQ3aQ8f0+/WF0XjJ8nyOw7H+BrmUA/a8pNZf2D"
  328. "XrCqLG1cERbWHI8ajhznpBY9P0Tr8PkvJDMhTkp/Z0DA6xpuL7DNOq5A+DY9UYTmkOF2U"
  329. "IO/sNt0wSnGvfdlZssD3rVIlLI9UUX37C6qXzHNntHPNfnTAhWHbUddtBwmegDjAUzZbu"
  330. "m9lqZmzDmHc8Ik8WY8Tab4Myym4+Gx8V0qw8GtYyWIzrktEJwV9UHv3ktG471rAqHTmFQ"
  331. "685V5uGqIalk06SWJr7tszR503Ac9cs493jJ8rhrSCIYbXBbzqt5v5+UZ0crh6bGR2dmJ"
  332. "yuHD428VlLLLdakzJe2VxcKhFSFID73JKPS40RI7tXVCcQ3uOGWhPCJ2bAspiJ2i5Vy6n"
  333. "jOqMerpEYpEe/Yks4xkU4Tt6BirmzUWanG6ozbFKhve9BsQRaLRTirzqk7hgUktXojKnf"
  334. "n8jeg3X4QepP3i63po6oml+9t/CwJLya2Bn/ei6f7/4B3Ycdb0L3pt5Q5mNz16rWJ9fLk"
  335. "vvOff/nxS7//8O2P2gvt7nDDnoV9L1du9N4+ucjl9u/8+a7dC5Nnvjlv9Ox5r+v9Cy0NE"
  336. "m+c6rv60S/dZw98Gn6MNswcfQiWUvg3wBUAAA=="
  337. )
  338. """
  339. Minimal package generated by running dh_make on empty directory
  340. Then cat test-pkg_1.0-1_amd64.deb | gzip | base64
  341. """
  342. DEB_PACKAGE_GZIP_BASE64 = (
  343. "H4sIACTXLlYAA1O0SSxKzrDjSklNykzM003KzEssqlRQUDA0MTG1NDQwNDVTUDBQAAEIa"
  344. "WhgYGZioqBgogADCVxGegZcyfl5JUX5OXoliUV66VVE6DcwheuX7+ZgAAEW5rdXHb0PG4"
  345. "iwf5j3WfMT6zWzzMuZgoE3jjYraNzbbFKWGms0SaRw/r2SV23WZ4IdP8preM4yqf0jt95"
  346. "3c8qnacfNxJUkf9/w+/3X9ph2GEdgQdixrz/niHKKTnYXizf4oSC7tHOz2Zzq+/6vn8/7"
  347. "ezQ7c1tmi7xZ3SGJ4yzhT2dcr7V+W3zM5ZPu/56PSv4Zdok+7Yv/V/6buWaKVlFkkV58S"
  348. "N3GmLgnqzRmeZ3V3ymmurS5fGa85/LNx1bpZMin3S6dvXKqydp3ubP1vmyarJZb/qSh62"
  349. "C8oIdxqm/BtvkGDza+On/Vfv2py7/0LV7VH+qR6a+bkKUbHXt5/SG187d+nps1a5PJfMO"
  350. "i11dWcUe1HjwaW3Q5RHXn9LmcHy+tW9YcKf0768XVB1t3R0bKrzs5t9P+6r7rZ99svH10"
  351. "+Q6F/o8tf1fO/32y+fWa14eifd+WxUy0jcxYH7N9/tUvmnUZL74pW32qLeuRU+ZwYGASa"
  352. "GBgUWBgxM90ayy3VdmykkGDgYErJbEkERydFVWQmCMQo8aWZvAY/WteFRHFwMCYqXTPjI"
  353. "lBkVEMGLsl+k8XP1D/z+gXyyDOvUemlnHqAVkvu0rRQ2fUFodkN3mtU9uwhqk8V+TqPEE"
  354. "Nc7fzoQ4n71lqRs/7kbbT0+qOZuKH4r8mjzsc1k/YkCHN8Pjg48fbpE+teHa96LNcfu0V"
  355. "5n2/Z2xa2KDvaCOx8cqBFxc514uZ3TmadXS+6cpzU7wSzq5SWfapJOD9n6wLXSwtlgxZh"
  356. "xITzWW7buhx/bb291RcVlEfeC9K5hlrqunSzIMSZT7/Nqgc/qMvMNW227WI8ezB8mVuZh"
  357. "0hERJSvysfburr4Dx0I9BW57UwR4+e1gxu49PcEt8sbK18Xpvt//Hj5UYm+Zc25q+T4xl"
  358. "rJvxfVnh80oadq57OZxPaU1bbztv1yF365W4t45Yr+XrFzov237GVY1Zgf7NvE4+W2SuR"
  359. "lQtLauR1TQ/mbOiIONYya6tU1jPGpWfk/i1+ttiXe3ZO14n0YOWggndznjGlGLyfVbBC6"
  360. "MRP5aMM7aCco/s7sZqB8RlTQwADw8rnuT/sDHi7mUASjJFRAAbWwNLiAwAA"
  361. )
  362. def run_cmd(self, vm, cmd, user="root"):
  363. p = vm.run(cmd, user=user, passio_popen=True, ignore_stderr=True)
  364. p.stdin.close()
  365. p.stdout.read()
  366. return p.wait()
  367. def setUp(self):
  368. super(VmUpdatesMixin, self).setUp()
  369. self.update_cmd = None
  370. if self.template.count("debian"):
  371. self.update_cmd = "set -o pipefail; apt-get update 2>&1 | " \
  372. "{ ! grep '^W:\|^E:'; }"
  373. self.install_cmd = "apt-get install -y {}"
  374. self.install_test_cmd = "dpkg -l {}"
  375. self.exit_code_ok = [0]
  376. elif self.template.count("fedora"):
  377. cmd = "yum"
  378. try:
  379. # assume template name in form "fedora-XX-suffix"
  380. if int(self.template.split("-")[1]) > 21:
  381. cmd = "dnf"
  382. except ValueError:
  383. pass
  384. self.update_cmd = "{cmd} clean all; {cmd} check-update".format(
  385. cmd=cmd)
  386. self.install_cmd = cmd + " install -y {}"
  387. self.install_test_cmd = "rpm -q {}"
  388. self.exit_code_ok = [0, 100]
  389. else:
  390. self.skipTest("Template {} not supported by this test".format(
  391. self.template))
  392. self.testvm1 = self.qc.add_new_vm(
  393. "QubesAppVm",
  394. name=self.make_vm_name('vm1'),
  395. template=self.qc.get_vm_by_name(self.template))
  396. self.testvm1.create_on_disk(verbose=False)
  397. def test_000_simple_update(self):
  398. self.save_and_reload_db()
  399. self.qc.unlock_db()
  400. # reload the VM to have all the properties properly set (especially
  401. # default netvm)
  402. self.testvm1 = self.qc[self.testvm1.qid]
  403. self.testvm1.start()
  404. p = self.testvm1.run(self.update_cmd, wait=True, user="root",
  405. passio_popen=True, passio_stderr=True)
  406. (stdout, stderr) = p.communicate()
  407. self.assertIn(p.wait(), self.exit_code_ok,
  408. "{}: {}\n{}".format(self.update_cmd, stdout, stderr)
  409. )
  410. def create_repo_apt(self):
  411. pkg_file_name = "test-pkg_1.0-1_amd64.deb"
  412. p = self.netvm_repo.run("mkdir /tmp/apt-repo && cd /tmp/apt-repo &&"
  413. "base64 -d | zcat > {}".format(pkg_file_name),
  414. passio_popen=True)
  415. p.stdin.write(self.DEB_PACKAGE_GZIP_BASE64)
  416. p.stdin.close()
  417. if p.wait() != 0:
  418. raise RuntimeError("Failed to write {}".format(pkg_file_name))
  419. # do not assume dpkg-scanpackage installed
  420. packages_path = "dists/test/main/binary-amd64/Packages"
  421. p = self.netvm_repo.run(
  422. "mkdir -p /tmp/apt-repo/dists/test/main/binary-amd64 && "
  423. "cd /tmp/apt-repo && "
  424. "cat > {packages} && "
  425. "echo MD5sum: $(openssl md5 -r {pkg} | cut -f 1 -d ' ')"
  426. " >> {packages} && "
  427. "echo SHA1: $(openssl sha1 -r {pkg} | cut -f 1 -d ' ')"
  428. " >> {packages} && "
  429. "echo SHA256: $(openssl sha256 -r {pkg} | cut -f 1 -d ' ')"
  430. " >> {packages} && "
  431. "gzip < {packages} > {packages}.gz".format(pkg=pkg_file_name,
  432. packages=packages_path),
  433. passio_popen=True, passio_stderr=True)
  434. p.stdin.write(
  435. "Package: test-pkg\n"
  436. "Version: 1.0-1\n"
  437. "Architecture: amd64\n"
  438. "Maintainer: unknown <user@host>\n"
  439. "Installed-Size: 25\n"
  440. "Filename: {pkg}\n"
  441. "Size: 994\n"
  442. "Section: unknown\n"
  443. "Priority: optional\n"
  444. "Description: Test package\n".format(pkg=pkg_file_name)
  445. )
  446. p.stdin.close()
  447. if p.wait() != 0:
  448. raise RuntimeError("Failed to write Packages file: {}".format(
  449. p.stderr.read()))
  450. p = self.netvm_repo.run(
  451. "mkdir -p /tmp/apt-repo/dists/test && "
  452. "cd /tmp/apt-repo/dists/test && "
  453. "cat > Release <<EOF && "
  454. "echo '' $(sha1sum {p} | cut -f 1 -d ' ') $(stat -c %s {p}) {p}"
  455. " >> Release && "
  456. "echo '' $(sha1sum {z} | cut -f 1 -d ' ') $(stat -c %s {z}) {z}"
  457. " >> Release"
  458. .format(p="main/binary-amd64/Packages",
  459. z="main/binary-amd64/Packages.gz"),
  460. passio_popen=True, passio_stderr=True
  461. )
  462. p.stdin.write(
  463. "Label: Test repo\n"
  464. "Suite: test\n"
  465. "Codename: test\n"
  466. "Date: Tue, 27 Oct 2015 03:22:09 +0100\n"
  467. "Architectures: amd64\n"
  468. "Components: main\n"
  469. "SHA1:\n"
  470. "EOF\n"
  471. )
  472. p.stdin.close()
  473. if p.wait() != 0:
  474. raise RuntimeError("Failed to write Release file: {}".format(
  475. p.stderr.read()))
  476. def create_repo_yum(self):
  477. pkg_file_name = "test-pkg-1.0-1.fc21.x86_64.rpm"
  478. p = self.netvm_repo.run("mkdir /tmp/yum-repo && cd /tmp/yum-repo &&"
  479. "base64 -d | zcat > {}".format(pkg_file_name),
  480. passio_popen=True, passio_stderr=True)
  481. p.stdin.write(self.RPM_PACKAGE_GZIP_BASE64)
  482. p.stdin.close()
  483. if p.wait() != 0:
  484. raise RuntimeError("Failed to write {}: {}".format(pkg_file_name,
  485. p.stderr.read()))
  486. # createrepo is installed by default in Fedora template
  487. p = self.netvm_repo.run("createrepo /tmp/yum-repo",
  488. passio_popen=True,
  489. passio_stderr=True)
  490. if p.wait() != 0:
  491. raise RuntimeError("Failed to create yum metadata: {}".format(
  492. p.stderr.read()))
  493. def create_repo_and_serve(self):
  494. if self.template.count("debian") or self.template.count("whonix"):
  495. self.create_repo_apt()
  496. self.netvm_repo.run("cd /tmp/apt-repo &&"
  497. "python -m SimpleHTTPServer 8080")
  498. elif self.template.count("fedora"):
  499. self.create_repo_yum()
  500. self.netvm_repo.run("cd /tmp/yum-repo &&"
  501. "python -m SimpleHTTPServer 8080")
  502. else:
  503. # not reachable...
  504. self.skipTest("Template {} not supported by this test".format(
  505. self.template))
  506. def configure_test_repo(self):
  507. """
  508. Configure test repository in test-vm and disable rest of them.
  509. The critical part is to use "localhost" - this will work only when
  510. accessed through update proxy and this is exactly what we want to
  511. test here.
  512. """
  513. if self.template.count("debian") or self.template.count("whonix"):
  514. self.testvm1.run(
  515. "rm -f /etc/apt/sources.list.d/* &&"
  516. "echo 'deb [trusted=yes] http://localhost:8080 test main' "
  517. "> /etc/apt/sources.list",
  518. user="root")
  519. elif self.template.count("fedora"):
  520. self.testvm1.run(
  521. "rm -f /etc/yum.repos.d/*.repo &&"
  522. "echo '[test]' > /etc/yum.repos.d/test.repo &&"
  523. "echo 'name=Test repo' >> /etc/yum.repos.d/test.repo &&"
  524. "echo 'gpgcheck=0' >> /etc/yum.repos.d/test.repo &&"
  525. "echo 'baseurl=http://localhost:8080/'"
  526. " >> /etc/yum.repos.d/test.repo",
  527. user="root"
  528. )
  529. else:
  530. # not reachable...
  531. self.skipTest("Template {} not supported by this test".format(
  532. self.template))
  533. def test_010_update_via_proxy(self):
  534. """
  535. Test both whether updates proxy works and whether is actually used by the VM
  536. """
  537. if self.template.count("minimal"):
  538. self.skipTest("Template {} not supported by this test".format(
  539. self.template))
  540. self.netvm_repo = self.qc.add_new_vm(
  541. "QubesNetVm",
  542. name=self.make_vm_name('net'),
  543. template=self.qc.get_vm_by_name(self.template))
  544. self.netvm_repo.create_on_disk(verbose=False)
  545. self.testvm1.netvm = self.netvm_repo
  546. # NetVM should have qubes-updates-proxy enabled by default
  547. #self.netvm_repo.services['qubes-updates-proxy'] = True
  548. # TODO: consider also adding a test for the template itself
  549. self.testvm1.services['updates-proxy-setup'] = True
  550. self.qc.save()
  551. self.qc.unlock_db()
  552. # Setup test repo
  553. self.netvm_repo.start()
  554. self.create_repo_and_serve()
  555. # Configure local repo
  556. self.testvm1.start()
  557. self.configure_test_repo()
  558. # update repository metadata
  559. p = self.testvm1.run(self.update_cmd, wait=True, user="root",
  560. passio_popen=True, passio_stderr=True)
  561. (stdout, stderr) = p.communicate()
  562. self.assertIn(p.wait(), self.exit_code_ok,
  563. "{}: {}\n{}".format(self.update_cmd, stdout, stderr)
  564. )
  565. # install test package
  566. p = self.testvm1.run(self.install_cmd.format('test-pkg'),
  567. wait=True, user="root",
  568. passio_popen=True, passio_stderr=True)
  569. (stdout, stderr) = p.communicate()
  570. self.assertIn(p.wait(), self.exit_code_ok,
  571. "{}: {}\n{}".format(self.update_cmd, stdout, stderr)
  572. )
  573. # verify if it was really installed
  574. p = self.testvm1.run(self.install_test_cmd.format('test-pkg'),
  575. wait=True, user="root",
  576. passio_popen=True, passio_stderr=True)
  577. (stdout, stderr) = p.communicate()
  578. self.assertIn(p.wait(), self.exit_code_ok,
  579. "{}: {}\n{}".format(self.update_cmd, stdout, stderr)
  580. )
  581. def load_tests(loader, tests, pattern):
  582. try:
  583. qc = qubes.qubes.QubesVmCollection()
  584. qc.lock_db_for_reading()
  585. qc.load()
  586. qc.unlock_db()
  587. templates = [vm.name for vm in qc.values() if
  588. isinstance(vm, qubes.qubes.QubesTemplateVm)]
  589. except OSError:
  590. templates = []
  591. for template in templates:
  592. tests.addTests(loader.loadTestsFromTestCase(
  593. type(
  594. 'VmNetworking_' + template,
  595. (VmNetworkingMixin, qubes.tests.QubesTestCase),
  596. {'template': template})))
  597. tests.addTests(loader.loadTestsFromTestCase(
  598. type(
  599. 'VmUpdates_' + template,
  600. (VmUpdatesMixin, qubes.tests.QubesTestCase),
  601. {'template': template})))
  602. return tests