firewall.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753
  1. # vim: fileencoding=utf-8
  2. #
  3. # The Qubes OS Project, https://www.qubes-os.org/
  4. #
  5. # Copyright (C) 2016
  6. # Marek Marczykowski-Górecki <marmarek@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. import logging
  23. import os
  24. import socket
  25. import subprocess
  26. import shutil
  27. import daemon
  28. import qubesdb
  29. import sys
  30. import signal
  31. class RuleParseError(Exception):
  32. pass
  33. class RuleApplyError(Exception):
  34. pass
  35. class FirewallWorker(object):
  36. def __init__(self):
  37. self.terminate_requested = False
  38. self.qdb = qubesdb.QubesDB()
  39. self.log = logging.getLogger('qubes.firewall')
  40. self.log.addHandler(logging.StreamHandler(sys.stderr))
  41. def init(self):
  42. """Create appropriate chains/tables"""
  43. raise NotImplementedError
  44. def sd_notify(self, state):
  45. """Send notification to systemd, if available"""
  46. # based on sdnotify python module
  47. if 'NOTIFY_SOCKET' not in os.environ:
  48. return
  49. addr = os.environ['NOTIFY_SOCKET']
  50. if addr[0] == '@':
  51. addr = '\0' + addr[1:]
  52. try:
  53. sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
  54. sock.connect(addr)
  55. sock.sendall(state.encode())
  56. except:
  57. # generally ignore error on systemd notification
  58. pass
  59. def cleanup(self):
  60. """Remove tables/chains - reverse work done by init"""
  61. raise NotImplementedError
  62. def apply_rules(self, source_addr, rules):
  63. """Apply rules in given source address"""
  64. raise NotImplementedError
  65. def update_connected_ips(self, family):
  66. raise NotImplementedError
  67. def get_connected_ips(self, family):
  68. ips = self.qdb.read('/connected-ips6' if family == 6 else '/connected-ips')
  69. if ips is None:
  70. return []
  71. return ips.decode().split()
  72. def run_firewall_dir(self):
  73. """Run scripts dir contents, before user script"""
  74. script_dir_paths = ['/etc/qubes/qubes-firewall.d',
  75. '/rw/config/qubes-firewall.d']
  76. for script_dir_path in script_dir_paths:
  77. if not os.path.isdir(script_dir_path):
  78. continue
  79. for d_script in sorted(os.listdir(script_dir_path)):
  80. d_script_path = os.path.join(script_dir_path, d_script)
  81. if os.path.isfile(d_script_path) and \
  82. os.access(d_script_path, os.X_OK):
  83. subprocess.call([d_script_path])
  84. def run_user_script(self):
  85. """Run user script in /rw/config"""
  86. user_script_path = '/rw/config/qubes-firewall-user-script'
  87. if os.path.isfile(user_script_path) and \
  88. os.access(user_script_path, os.X_OK):
  89. subprocess.call([user_script_path])
  90. def read_rules(self, target):
  91. """Read rules from QubesDB and return them as a list of dicts"""
  92. entries = self.qdb.multiread('/qubes-firewall/{}/'.format(target))
  93. assert isinstance(entries, dict)
  94. # drop full path
  95. entries = dict(((k.split('/')[3], v.decode())
  96. for k, v in entries.items()))
  97. if 'policy' not in entries:
  98. raise RuleParseError('No \'policy\' defined')
  99. policy = entries.pop('policy')
  100. rules = []
  101. for ruleno, rule in sorted(entries.items()):
  102. if len(ruleno) != 4 or not ruleno.isdigit():
  103. raise RuleParseError(
  104. 'Unexpected non-rule found: {}={}'.format(ruleno, rule))
  105. rule_dict = dict(elem.split('=') for elem in rule.split(' '))
  106. if 'action' not in rule_dict:
  107. raise RuleParseError('Rule \'{}\' lack action'.format(rule))
  108. rules.append(rule_dict)
  109. rules.append({'action': policy})
  110. return rules
  111. def list_targets(self):
  112. return set(t.split('/')[2] for t in self.qdb.list('/qubes-firewall/'))
  113. @staticmethod
  114. def is_ip6(addr):
  115. return addr.count(':') > 0
  116. def log_error(self, msg):
  117. self.log.error(msg)
  118. subprocess.call(
  119. ['notify-send', '-t', '3000', msg],
  120. env=os.environ.copy().update({'DISPLAY': ':0'})
  121. )
  122. def handle_addr(self, addr):
  123. try:
  124. rules = self.read_rules(addr)
  125. self.apply_rules(addr, rules)
  126. except RuleParseError as e:
  127. self.log_error(
  128. 'Failed to parse rules for {} ({}), blocking traffic'.format(
  129. addr, str(e)
  130. ))
  131. self.apply_rules(addr, [{'action': 'drop'}])
  132. except RuleApplyError as e:
  133. self.log_error(
  134. 'Failed to apply rules for {} ({}), blocking traffic'.format(
  135. addr, str(e))
  136. )
  137. # retry with fallback rules
  138. try:
  139. self.apply_rules(addr, [{'action': 'drop'}])
  140. except RuleApplyError:
  141. self.log_error(
  142. 'Failed to block traffic for {}'.format(addr))
  143. @staticmethod
  144. def dns_addresses(family=None):
  145. with open('/etc/resolv.conf') as resolv:
  146. for line in resolv.readlines():
  147. line = line.strip()
  148. if line.startswith('nameserver'):
  149. if line.count('.') == 3 and (family or 4) == 4:
  150. yield line.split(' ')[1]
  151. elif line.count(':') and (family or 6) == 6:
  152. yield line.split(' ')[1]
  153. def main(self):
  154. self.terminate_requested = False
  155. self.init()
  156. self.run_firewall_dir()
  157. self.run_user_script()
  158. self.sd_notify('READY=1')
  159. # initial load
  160. for source_addr in self.list_targets():
  161. self.handle_addr(source_addr)
  162. self.update_connected_ips(4)
  163. self.update_connected_ips(6)
  164. self.qdb.watch('/qubes-firewall/')
  165. self.qdb.watch('/connected-ips')
  166. self.qdb.watch('/connected-ips6')
  167. try:
  168. for watch_path in iter(self.qdb.read_watch, None):
  169. if watch_path == '/connected-ips':
  170. self.update_connected_ips(4)
  171. if watch_path == '/connected-ips6':
  172. self.update_connected_ips(6)
  173. # ignore writing rules itself - wait for final write at
  174. # source_addr level empty write (/qubes-firewall/SOURCE_ADDR)
  175. if watch_path.startswith('/qubes-firewall/') and watch_path.count('/') == 2:
  176. source_addr = watch_path.split('/')[2]
  177. self.handle_addr(source_addr)
  178. except OSError: # EINTR
  179. # signal received, don't continue the loop
  180. pass
  181. self.cleanup()
  182. def terminate(self):
  183. self.terminate_requested = True
  184. class IptablesWorker(FirewallWorker):
  185. supported_rule_opts = ['action', 'proto', 'dst4', 'dst6', 'dsthost',
  186. 'dstports', 'specialtarget', 'icmptype']
  187. def __init__(self):
  188. super(IptablesWorker, self).__init__()
  189. self.chains = {
  190. 4: set(),
  191. 6: set(),
  192. }
  193. @staticmethod
  194. def chain_for_addr(addr):
  195. """Generate iptables chain name for given source address address"""
  196. return 'qbs-' + addr.replace('.', '-').replace(':', '-')[-20:]
  197. def run_ipt(self, family, args, **kwargs):
  198. # pylint: disable=no-self-use
  199. if family == 6:
  200. subprocess.check_call(['ip6tables'] + args, **kwargs)
  201. else:
  202. subprocess.check_call(['iptables'] + args, **kwargs)
  203. def run_ipt_restore(self, family, args):
  204. # pylint: disable=no-self-use
  205. if family == 6:
  206. return subprocess.Popen(['ip6tables-restore'] + args,
  207. stdin=subprocess.PIPE,
  208. stdout=subprocess.PIPE,
  209. stderr=subprocess.STDOUT)
  210. else:
  211. return subprocess.Popen(['iptables-restore'] + args,
  212. stdin=subprocess.PIPE,
  213. stdout=subprocess.PIPE,
  214. stderr=subprocess.STDOUT)
  215. def create_chain(self, addr, chain, family):
  216. """
  217. Create iptables chain and hook traffic coming from `addr` to it.
  218. :param addr: source IP from which traffic should be handled by the
  219. chain
  220. :param chain: name of the chain to create
  221. :param family: address family (4 or 6)
  222. :return: None
  223. """
  224. self.run_ipt(family, ['-N', chain])
  225. self.run_ipt(family,
  226. ['-I', 'QBS-FORWARD', '-s', addr, '-j', chain])
  227. self.chains[family].add(chain)
  228. def prepare_rules(self, chain, rules, family):
  229. """
  230. Helper function to translate rules list into input for iptables-restore
  231. :param chain: name of the chain to put rules into
  232. :param rules: list of rules
  233. :param family: address family (4 or 6)
  234. :return: input for iptables-restore
  235. :rtype: str
  236. """
  237. iptables = "*filter\n"
  238. fullmask = '/128' if family == 6 else '/32'
  239. dns = list(addr + fullmask for addr in self.dns_addresses(family))
  240. for rule in rules:
  241. unsupported_opts = set(rule.keys()).difference(
  242. set(self.supported_rule_opts))
  243. if unsupported_opts:
  244. raise RuleParseError(
  245. 'Unsupported rule option(s): {!s}'.format(unsupported_opts))
  246. if 'dst4' in rule and family == 6:
  247. raise RuleParseError('IPv4 rule found for IPv6 address')
  248. if 'dst6' in rule and family == 4:
  249. raise RuleParseError('dst6 rule found for IPv4 address')
  250. if 'proto' in rule:
  251. if rule['proto'] == 'icmp' and family == 6:
  252. protos = ['icmpv6']
  253. else:
  254. protos = [rule['proto']]
  255. else:
  256. protos = None
  257. if 'dst4' in rule:
  258. dsthosts = [rule['dst4']]
  259. elif 'dst6' in rule:
  260. dsthosts = [rule['dst6']]
  261. elif 'dsthost' in rule:
  262. try:
  263. addrinfo = socket.getaddrinfo(rule['dsthost'], None,
  264. (socket.AF_INET6 if family == 6 else socket.AF_INET))
  265. except socket.gaierror as e:
  266. raise RuleParseError('Failed to resolve {}: {}'.format(
  267. rule['dsthost'], str(e)))
  268. dsthosts = set(item[4][0] + fullmask for item in addrinfo)
  269. else:
  270. dsthosts = None
  271. if 'dstports' in rule:
  272. dstports = rule['dstports'].replace('-', ':')
  273. else:
  274. dstports = None
  275. if rule.get('specialtarget', None) == 'dns':
  276. if dstports not in ('53:53', None):
  277. continue
  278. else:
  279. dstports = '53:53'
  280. if not dns:
  281. continue
  282. if protos is not None:
  283. protos = {'tcp', 'udp'}.intersection(protos)
  284. else:
  285. protos = {'tcp', 'udp'}
  286. if dsthosts is not None:
  287. dsthosts = set(dns).intersection(dsthosts)
  288. else:
  289. dsthosts = dns
  290. if 'icmptype' in rule:
  291. icmptype = rule['icmptype']
  292. else:
  293. icmptype = None
  294. # make them iterable
  295. if protos is None:
  296. protos = [None]
  297. if dsthosts is None:
  298. dsthosts = [None]
  299. if rule['action'] == 'accept':
  300. action = 'ACCEPT'
  301. elif rule['action'] == 'drop':
  302. action = 'REJECT --reject-with {}'.format(
  303. 'icmp6-adm-prohibited' if family == 6 else
  304. 'icmp-admin-prohibited')
  305. else:
  306. raise RuleParseError(
  307. 'Invalid rule action {}'.format(rule['action']))
  308. # sorting here is only to ease writing tests
  309. for proto in sorted(protos):
  310. for dsthost in sorted(dsthosts):
  311. ipt_rule = '-A {}'.format(chain)
  312. if dsthost is not None:
  313. ipt_rule += ' -d {}'.format(dsthost)
  314. if proto is not None:
  315. ipt_rule += ' -p {}'.format(proto)
  316. if dstports is not None:
  317. ipt_rule += ' --dport {}'.format(dstports)
  318. if icmptype is not None:
  319. ipt_rule += ' --icmp-type {}'.format(icmptype)
  320. ipt_rule += ' -j {}\n'.format(action)
  321. iptables += ipt_rule
  322. iptables += 'COMMIT\n'
  323. return iptables
  324. def apply_rules_family(self, source, rules, family):
  325. """
  326. Apply rules for given source address.
  327. Handle only rules for given address family (IPv4 or IPv6).
  328. :param source: source address
  329. :param rules: rules list
  330. :param family: address family, either 4 or 6
  331. :return: None
  332. """
  333. chain = self.chain_for_addr(source)
  334. if chain not in self.chains[family]:
  335. self.create_chain(source, chain, family)
  336. iptables = self.prepare_rules(chain, rules, family)
  337. try:
  338. self.run_ipt(family, ['-F', chain])
  339. p = self.run_ipt_restore(family, ['-n'])
  340. (output, _) = p.communicate(iptables.encode())
  341. if p.returncode != 0:
  342. raise RuleApplyError(
  343. 'iptables-restore failed: {}'.format(output))
  344. except subprocess.CalledProcessError as e:
  345. raise RuleApplyError('\'iptables -F {}\' failed: {}'.format(
  346. chain, e.output))
  347. def apply_rules(self, source, rules):
  348. if self.is_ip6(source):
  349. self.apply_rules_family(source, rules, 6)
  350. else:
  351. self.apply_rules_family(source, rules, 4)
  352. def update_connected_ips(self, family):
  353. ips = self.get_connected_ips(family)
  354. if not ips:
  355. # Just flush.
  356. self.run_ipt(family, ['-t', 'raw', '-F', 'QBS-PREROUTING'])
  357. self.run_ipt(family, ['-t', 'mangle', '-F', 'QBS-POSTROUTING'])
  358. return
  359. # Temporarily set policy to DROP while updating the rules.
  360. self.run_ipt(family, ['-t', 'raw', '-P', 'PREROUTING', 'DROP'])
  361. self.run_ipt(family, ['-t', 'mangle', '-P', 'POSTROUTING', 'DROP'])
  362. self.run_ipt(family, ['-t', 'raw', '-F', 'QBS-PREROUTING'])
  363. self.run_ipt(family, ['-t', 'mangle', '-F', 'QBS-POSTROUTING'])
  364. for ip in ips:
  365. self.run_ipt(family, [
  366. '-t', 'raw', '-A', 'QBS-PREROUTING',
  367. '!', '-i', 'vif+', '-s', ip, '-j', 'DROP'])
  368. self.run_ipt(family, [
  369. '-t', 'mangle', '-A', 'QBS-POSTROUTING',
  370. '!', '-o', 'vif+', '-d', ip, '-j', 'DROP'])
  371. self.run_ipt(family, ['-t', 'raw', '-P', 'PREROUTING', 'ACCEPT'])
  372. self.run_ipt(family, ['-t', 'mangle', '-P', 'POSTROUTING', 'ACCEPT'])
  373. def init(self):
  374. # Chains QBS-FORWARD, QBS-PREROUTING, QBS-POSTROUTING
  375. # need to be created before running this.
  376. try:
  377. self.run_ipt(4, ['-F', 'QBS-FORWARD'])
  378. self.run_ipt(4,
  379. ['-A', 'QBS-FORWARD', '!', '-i', 'vif+', '-j', 'RETURN'])
  380. self.run_ipt(4, ['-A', 'QBS-FORWARD', '-j', 'DROP'])
  381. self.run_ipt(4, ['-t', 'raw', '-F', 'QBS-PREROUTING'])
  382. self.run_ipt(4, ['-t', 'mangle', '-F', 'QBS-POSTROUTING'])
  383. self.run_ipt(6, ['-F', 'QBS-FORWARD'])
  384. self.run_ipt(6,
  385. ['-A', 'QBS-FORWARD', '!', '-i', 'vif+', '-j', 'RETURN'])
  386. self.run_ipt(6, ['-A', 'QBS-FORWARD', '-j', 'DROP'])
  387. self.run_ipt(6, ['-t', 'raw', '-F', 'QBS-PREROUTING'])
  388. self.run_ipt(6, ['-t', 'mangle', '-F', 'QBS-POSTROUTING'])
  389. except subprocess.CalledProcessError:
  390. self.log_error(
  391. 'Error initializing iptables. '
  392. 'You probably need to create QBS-FORWARD, QBS-PREROUTING and '
  393. 'QBS-POSTROUTING chains first.'
  394. )
  395. sys.exit(1)
  396. def cleanup(self):
  397. for family in (4, 6):
  398. self.run_ipt(family, ['-F', 'QBS-FORWARD'])
  399. self.run_ipt(family, ['-t', 'raw', '-F', 'QBS-PREROUTING'])
  400. self.run_ipt(family, ['-t', 'mangle', '-F', 'QBS-POSTROUTING'])
  401. for chain in self.chains[family]:
  402. self.run_ipt(family, ['-F', chain])
  403. self.run_ipt(family, ['-X', chain])
  404. class NftablesWorker(FirewallWorker):
  405. supported_rule_opts = ['action', 'proto', 'dst4', 'dst6', 'dsthost',
  406. 'dstports', 'specialtarget', 'icmptype']
  407. def __init__(self):
  408. super(NftablesWorker, self).__init__()
  409. self.chains = {
  410. 4: set(),
  411. 6: set(),
  412. }
  413. @staticmethod
  414. def chain_for_addr(addr):
  415. """Generate iptables chain name for given source address address"""
  416. return 'qbs-' + addr.replace('.', '-').replace(':', '-')
  417. def run_nft(self, nft_input):
  418. # pylint: disable=no-self-use
  419. p = subprocess.Popen(['nft', '-f', '/dev/stdin'],
  420. stdin=subprocess.PIPE,
  421. stdout=subprocess.PIPE,
  422. stderr=subprocess.STDOUT)
  423. stdout, _ = p.communicate(nft_input.encode())
  424. if p.returncode != 0:
  425. raise RuleApplyError('nft failed: {}'.format(stdout))
  426. def create_chain(self, addr, chain, family):
  427. """
  428. Create iptables chain and hook traffic coming from `addr` to it.
  429. :param addr: source IP from which traffic should be handled by the
  430. chain
  431. :param chain: name of the chain to create
  432. :param family: address family (4 or 6)
  433. :return: None
  434. """
  435. nft_input = (
  436. 'table {family} {table} {{\n'
  437. ' chain {chain} {{\n'
  438. ' }}\n'
  439. ' chain forward {{\n'
  440. ' {family} saddr {ip} jump {chain}\n'
  441. ' }}\n'
  442. '}}\n'.format(
  443. family=("ip6" if family == 6 else "ip"),
  444. table='qubes-firewall',
  445. chain=chain,
  446. ip=addr,
  447. )
  448. )
  449. self.run_nft(nft_input)
  450. self.chains[family].add(chain)
  451. def update_connected_ips(self, family):
  452. family_name = ('ip6' if family == 6 else 'ip')
  453. table = 'qubes-firewall'
  454. nft_input = (
  455. 'flush chain {family_name} {table} prerouting\n'
  456. 'flush chain {family_name} {table} postrouting\n'
  457. ).format(family_name=family_name, table=table)
  458. ips = self.get_connected_ips(family)
  459. if ips:
  460. addr = '{' + ', '.join(ips) + '}'
  461. irule = 'iifname != "vif*" {family_name} saddr {addr} drop\n'.format(
  462. family_name=family_name, addr=addr)
  463. orule = 'oifname != "vif*" {family_name} daddr {addr} drop\n'.format(
  464. family_name=family_name, addr=addr)
  465. nft_input += (
  466. 'table {family_name} {table} {{\n'
  467. ' chain prerouting {{\n'
  468. ' {irule}'
  469. ' }}\n'
  470. ' chain postrouting {{\n'
  471. ' {orule}'
  472. ' }}\n'
  473. '}}\n'
  474. ).format(
  475. family_name=family_name,
  476. table=table,
  477. irule=irule,
  478. orule=orule,
  479. )
  480. self.run_nft(nft_input)
  481. def prepare_rules(self, chain, rules, family):
  482. """
  483. Helper function to translate rules list into input for iptables-restore
  484. :param chain: name of the chain to put rules into
  485. :param rules: list of rules
  486. :param family: address family (4 or 6)
  487. :return: input for iptables-restore
  488. :rtype: str
  489. """
  490. assert family in (4, 6)
  491. nft_rules = []
  492. ip_match = 'ip6' if family == 6 else 'ip'
  493. fullmask = '/128' if family == 6 else '/32'
  494. dns = list(addr + fullmask for addr in self.dns_addresses(family))
  495. for rule in rules:
  496. unsupported_opts = set(rule.keys()).difference(
  497. set(self.supported_rule_opts))
  498. if unsupported_opts:
  499. raise RuleParseError(
  500. 'Unsupported rule option(s): {!s}'.format(unsupported_opts))
  501. if 'dst4' in rule and family == 6:
  502. raise RuleParseError('IPv4 rule found for IPv6 address')
  503. if 'dst6' in rule and family == 4:
  504. raise RuleParseError('dst6 rule found for IPv4 address')
  505. nft_rule = ""
  506. if rule['action'] == 'accept':
  507. action = 'accept'
  508. elif rule['action'] == 'drop':
  509. action = 'reject with icmp{} type admin-prohibited'.format(
  510. 'v6' if family == 6 else '')
  511. else:
  512. raise RuleParseError(
  513. 'Invalid rule action {}'.format(rule['action']))
  514. if 'proto' in rule:
  515. if family == 4:
  516. nft_rule += ' ip protocol {}'.format(rule['proto'])
  517. elif family == 6:
  518. proto = 'icmpv6' if rule['proto'] == 'icmp' \
  519. else rule['proto']
  520. nft_rule += ' ip6 nexthdr {}'.format(proto)
  521. if 'dst4' in rule:
  522. nft_rule += ' ip daddr {}'.format(rule['dst4'])
  523. elif 'dst6' in rule:
  524. nft_rule += ' ip6 daddr {}'.format(rule['dst6'])
  525. elif 'dsthost' in rule:
  526. try:
  527. addrinfo = socket.getaddrinfo(rule['dsthost'], None,
  528. (socket.AF_INET6 if family == 6 else socket.AF_INET))
  529. except socket.gaierror as e:
  530. raise RuleParseError('Failed to resolve {}: {}'.format(
  531. rule['dsthost'], str(e)))
  532. except UnicodeError as e:
  533. raise RuleParseError('Invalid destination {}: {}'.format(
  534. rule['dsthost'], str(e)))
  535. nft_rule += ' {} daddr {{ {} }}'.format(ip_match,
  536. ', '.join(set(item[4][0] + fullmask for item in addrinfo)))
  537. if 'dstports' in rule:
  538. dstports = rule['dstports']
  539. if len(set(dstports.split('-'))) == 1:
  540. dstports = dstports.split('-')[0]
  541. else:
  542. dstports = None
  543. if rule.get('specialtarget', None) == 'dns':
  544. if dstports not in ('53', None):
  545. continue
  546. else:
  547. dstports = '53'
  548. if not dns:
  549. continue
  550. nft_rule += ' {} daddr {{ {} }}'.format(ip_match, ', '.join(
  551. dns))
  552. if 'icmptype' in rule:
  553. if family == 4:
  554. nft_rule += ' icmp type {}'.format(rule['icmptype'])
  555. elif family == 6:
  556. nft_rule += ' icmpv6 type {}'.format(rule['icmptype'])
  557. # now duplicate rules for tcp/udp if needed
  558. # it isn't possible to specify "tcp dport xx || udp dport xx" in
  559. # one rule
  560. if dstports is not None:
  561. if 'proto' not in rule:
  562. nft_rules.append(
  563. nft_rule + ' tcp dport {} {}'.format(
  564. dstports, action))
  565. nft_rules.append(
  566. nft_rule + ' udp dport {} {}'.format(
  567. dstports, action))
  568. else:
  569. nft_rules.append(
  570. nft_rule + ' {} dport {} {}'.format(
  571. rule['proto'], dstports, action))
  572. else:
  573. nft_rules.append(nft_rule + ' ' + action)
  574. return (
  575. 'flush chain {family} {table} {chain}\n'
  576. 'table {family} {table} {{\n'
  577. ' chain {chain} {{\n'
  578. ' {rules}\n'
  579. ' }}\n'
  580. '}}\n'.format(
  581. family=('ip6' if family == 6 else 'ip'),
  582. table='qubes-firewall',
  583. chain=chain,
  584. rules='\n '.join(nft_rules)
  585. ))
  586. def apply_rules_family(self, source, rules, family):
  587. """
  588. Apply rules for given source address.
  589. Handle only rules for given address family (IPv4 or IPv6).
  590. :param source: source address
  591. :param rules: rules list
  592. :param family: address family, either 4 or 6
  593. :return: None
  594. """
  595. chain = self.chain_for_addr(source)
  596. if chain not in self.chains[family]:
  597. self.create_chain(source, chain, family)
  598. self.run_nft(self.prepare_rules(chain, rules, family))
  599. def apply_rules(self, source, rules):
  600. if self.is_ip6(source):
  601. self.apply_rules_family(source, rules, 6)
  602. else:
  603. self.apply_rules_family(source, rules, 4)
  604. def init(self):
  605. nft_init = (
  606. 'table {family} qubes-firewall {{\n'
  607. ' chain forward {{\n'
  608. ' type filter hook forward priority 0;\n'
  609. ' policy drop;\n'
  610. ' ct state established,related accept\n'
  611. ' meta iifname != "vif*" accept\n'
  612. ' }}\n'
  613. ' chain prerouting {{\n'
  614. ' type filter hook prerouting priority -300;\n'
  615. ' policy accept;\n'
  616. ' }}\n'
  617. ' chain postrouting {{\n'
  618. ' type filter hook postrouting priority -300;\n'
  619. ' policy accept;\n'
  620. ' }}\n'
  621. '}}\n'
  622. )
  623. nft_init = ''.join(
  624. nft_init.format(family=family) for family in ('ip', 'ip6'))
  625. self.run_nft(nft_init)
  626. def cleanup(self):
  627. nft_cleanup = (
  628. 'delete table ip qubes-firewall\n'
  629. 'delete table ip6 qubes-firewall\n'
  630. )
  631. self.run_nft(nft_cleanup)
  632. def main():
  633. if shutil.which('nft'):
  634. worker = NftablesWorker()
  635. else:
  636. worker = IptablesWorker()
  637. context = daemon.DaemonContext()
  638. context.stderr = sys.stderr
  639. context.detach_process = False
  640. context.files_preserve = [worker.qdb.watch_fd()]
  641. context.signal_map = {
  642. signal.SIGTERM: lambda _signal, _stack: worker.terminate(),
  643. }
  644. with context:
  645. worker.main()
  646. if __name__ == '__main__':
  647. main()