network.py 39 KB

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