firewall.py 26 KB

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