server.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. #!/usr/bin/env python3
  2. import re
  3. import os
  4. import sys
  5. import time
  6. import atexit
  7. import signal
  8. import ipaddress
  9. from collections import Counter
  10. from random import randint
  11. from threading import Thread
  12. from threading import Lock
  13. import redis
  14. import json
  15. import iptc
  16. import dns.resolver
  17. import dns.exception
  18. while True:
  19. try:
  20. redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
  21. redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '')
  22. if "".__eq__(redis_slaveof_ip):
  23. r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0)
  24. else:
  25. r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0)
  26. r.ping()
  27. except Exception as ex:
  28. print('%s - trying again in 3 seconds' % (ex))
  29. time.sleep(3)
  30. else:
  31. break
  32. pubsub = r.pubsub()
  33. WHITELIST = []
  34. BLACKLIST= []
  35. bans = {}
  36. quit_now = False
  37. exit_code = 0
  38. lock = Lock()
  39. def log(priority, message):
  40. tolog = {}
  41. tolog['time'] = int(round(time.time()))
  42. tolog['priority'] = priority
  43. tolog['message'] = message
  44. r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
  45. print(message)
  46. def logWarn(message):
  47. log('warn', message)
  48. def logCrit(message):
  49. log('crit', message)
  50. def logInfo(message):
  51. log('info', message)
  52. def refreshF2boptions():
  53. global f2boptions
  54. global quit_now
  55. global exit_code
  56. if not r.get('F2B_OPTIONS'):
  57. f2boptions = {}
  58. f2boptions['ban_time'] = int
  59. f2boptions['max_attempts'] = int
  60. f2boptions['retry_window'] = int
  61. f2boptions['netban_ipv4'] = int
  62. f2boptions['netban_ipv6'] = int
  63. f2boptions['ban_time'] = r.get('F2B_BAN_TIME') or 1800
  64. f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS') or 10
  65. f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW') or 600
  66. f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4') or 32
  67. f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6') or 128
  68. r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
  69. else:
  70. try:
  71. f2boptions = {}
  72. f2boptions = json.loads(r.get('F2B_OPTIONS'))
  73. except ValueError:
  74. print('Error loading F2B options: F2B_OPTIONS is not json')
  75. quit_now = True
  76. exit_code = 2
  77. def refreshF2bregex():
  78. global f2bregex
  79. global quit_now
  80. global exit_code
  81. if not r.get('F2B_REGEX'):
  82. f2bregex = {}
  83. f2bregex[1] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
  84. f2bregex[2] = 'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'
  85. f2bregex[3] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+'
  86. f2bregex[4] = 'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+'
  87. f2bregex[5] = 'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+'
  88. f2bregex[6] = '-login: Disconnected.+ \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
  89. f2bregex[7] = '-login: Aborted login.+ \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
  90. f2bregex[8] = '-login: Aborted login.+ \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
  91. f2bregex[9] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
  92. f2bregex[10] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
  93. r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))
  94. else:
  95. try:
  96. f2bregex = {}
  97. f2bregex = json.loads(r.get('F2B_REGEX'))
  98. except ValueError:
  99. print('Error loading F2B options: F2B_REGEX is not json')
  100. quit_now = True
  101. exit_code = 2
  102. if r.exists('F2B_LOG'):
  103. r.rename('F2B_LOG', 'NETFILTER_LOG')
  104. def mailcowChainOrder():
  105. global lock
  106. global quit_now
  107. global exit_code
  108. while not quit_now:
  109. time.sleep(10)
  110. with lock:
  111. filter4_table = iptc.Table(iptc.Table.FILTER)
  112. filter6_table = iptc.Table6(iptc.Table6.FILTER)
  113. filter4_table.refresh()
  114. filter6_table.refresh()
  115. for f in [filter4_table, filter6_table]:
  116. forward_chain = iptc.Chain(f, 'FORWARD')
  117. input_chain = iptc.Chain(f, 'INPUT')
  118. for chain in [forward_chain, input_chain]:
  119. target_found = False
  120. for position, item in enumerate(chain.rules):
  121. if item.target.name == 'MAILCOW':
  122. target_found = True
  123. if position > 2:
  124. logCrit('Error in %s chain order: MAILCOW on position %d, restarting container' % (chain.name, position))
  125. quit_now = True
  126. exit_code = 2
  127. if not target_found:
  128. logCrit('Error in %s chain: MAILCOW target not found, restarting container' % (chain.name))
  129. quit_now = True
  130. exit_code = 2
  131. def ban(address):
  132. global lock
  133. refreshF2boptions()
  134. BAN_TIME = int(f2boptions['ban_time'])
  135. MAX_ATTEMPTS = int(f2boptions['max_attempts'])
  136. RETRY_WINDOW = int(f2boptions['retry_window'])
  137. NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
  138. NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
  139. ip = ipaddress.ip_address(address)
  140. if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
  141. ip = ip.ipv4_mapped
  142. address = str(ip)
  143. if ip.is_private or ip.is_loopback:
  144. return
  145. self_network = ipaddress.ip_network(address)
  146. with lock:
  147. temp_whitelist = set(WHITELIST)
  148. if temp_whitelist:
  149. for wl_key in temp_whitelist:
  150. wl_net = ipaddress.ip_network(wl_key, False)
  151. if wl_net.overlaps(self_network):
  152. logInfo('Address %s is whitelisted by rule %s' % (self_network, wl_net))
  153. return
  154. net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
  155. net = str(net)
  156. if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
  157. bans[net] = { 'attempts': 0 }
  158. active_window = RETRY_WINDOW
  159. else:
  160. active_window = time.time() - bans[net]['last_attempt']
  161. bans[net]['attempts'] += 1
  162. bans[net]['last_attempt'] = time.time()
  163. active_window = time.time() - bans[net]['last_attempt']
  164. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  165. cur_time = int(round(time.time()))
  166. logCrit('Banning %s for %d minutes' % (net, BAN_TIME / 60))
  167. if type(ip) is ipaddress.IPv4Address:
  168. with lock:
  169. chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
  170. rule = iptc.Rule()
  171. rule.src = net
  172. target = iptc.Target(rule, "REJECT")
  173. rule.target = target
  174. if rule not in chain.rules:
  175. chain.insert_rule(rule)
  176. else:
  177. with lock:
  178. chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
  179. rule = iptc.Rule6()
  180. rule.src = net
  181. target = iptc.Target(rule, "REJECT")
  182. rule.target = target
  183. if rule not in chain.rules:
  184. chain.insert_rule(rule)
  185. r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + BAN_TIME)
  186. else:
  187. logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
  188. def unban(net):
  189. global lock
  190. if not net in bans:
  191. logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net)
  192. r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
  193. return
  194. logInfo('Unbanning %s' % net)
  195. if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
  196. with lock:
  197. chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
  198. rule = iptc.Rule()
  199. rule.src = net
  200. target = iptc.Target(rule, "REJECT")
  201. rule.target = target
  202. if rule in chain.rules:
  203. chain.delete_rule(rule)
  204. else:
  205. with lock:
  206. chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
  207. rule = iptc.Rule6()
  208. rule.src = net
  209. target = iptc.Target(rule, "REJECT")
  210. rule.target = target
  211. if rule in chain.rules:
  212. chain.delete_rule(rule)
  213. r.hdel('F2B_ACTIVE_BANS', '%s' % net)
  214. r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
  215. if net in bans:
  216. del bans[net]
  217. def permBan(net, unban=False):
  218. global lock
  219. if type(ipaddress.ip_network(net, strict=False)) is ipaddress.IPv4Network:
  220. with lock:
  221. chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
  222. rule = iptc.Rule()
  223. rule.src = net
  224. target = iptc.Target(rule, "REJECT")
  225. rule.target = target
  226. if rule not in chain.rules and not unban:
  227. logCrit('Add host/network %s to blacklist' % net)
  228. chain.insert_rule(rule)
  229. r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
  230. elif rule in chain.rules and unban:
  231. logCrit('Remove host/network %s from blacklist' % net)
  232. chain.delete_rule(rule)
  233. r.hdel('F2B_PERM_BANS', '%s' % net)
  234. else:
  235. with lock:
  236. chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
  237. rule = iptc.Rule6()
  238. rule.src = net
  239. target = iptc.Target(rule, "REJECT")
  240. rule.target = target
  241. if rule not in chain.rules and not unban:
  242. logCrit('Add host/network %s to blacklist' % net)
  243. chain.insert_rule(rule)
  244. r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
  245. elif rule in chain.rules and unban:
  246. logCrit('Remove host/network %s from blacklist' % net)
  247. chain.delete_rule(rule)
  248. r.hdel('F2B_PERM_BANS', '%s' % net)
  249. def quit(signum, frame):
  250. global quit_now
  251. quit_now = True
  252. def clear():
  253. global lock
  254. logInfo('Clearing all bans')
  255. for net in bans.copy():
  256. unban(net)
  257. with lock:
  258. filter4_table = iptc.Table(iptc.Table.FILTER)
  259. filter6_table = iptc.Table6(iptc.Table6.FILTER)
  260. for filter_table in [filter4_table, filter6_table]:
  261. filter_table.autocommit = False
  262. forward_chain = iptc.Chain(filter_table, "FORWARD")
  263. input_chain = iptc.Chain(filter_table, "INPUT")
  264. mailcow_chain = iptc.Chain(filter_table, "MAILCOW")
  265. if mailcow_chain in filter_table.chains:
  266. for rule in mailcow_chain.rules:
  267. mailcow_chain.delete_rule(rule)
  268. for rule in forward_chain.rules:
  269. if rule.target.name == 'MAILCOW':
  270. forward_chain.delete_rule(rule)
  271. for rule in input_chain.rules:
  272. if rule.target.name == 'MAILCOW':
  273. input_chain.delete_rule(rule)
  274. filter_table.delete_chain("MAILCOW")
  275. filter_table.commit()
  276. filter_table.refresh()
  277. filter_table.autocommit = True
  278. r.delete('F2B_ACTIVE_BANS')
  279. r.delete('F2B_PERM_BANS')
  280. pubsub.unsubscribe()
  281. def watch():
  282. logInfo('Watching Redis channel F2B_CHANNEL')
  283. pubsub.subscribe('F2B_CHANNEL')
  284. global quit_now
  285. global exit_code
  286. while not quit_now:
  287. try:
  288. for item in pubsub.listen():
  289. refreshF2bregex()
  290. for rule_id, rule_regex in f2bregex.items():
  291. if item['data'] and item['type'] == 'message':
  292. try:
  293. result = re.search(rule_regex, item['data'])
  294. except re.error:
  295. result = False
  296. if result:
  297. addr = result.group(1)
  298. ip = ipaddress.ip_address(addr)
  299. if ip.is_private or ip.is_loopback:
  300. continue
  301. logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data']))
  302. ban(addr)
  303. except Exception as ex:
  304. logWarn('Error reading log line from pubsub')
  305. quit_now = True
  306. exit_code = 2
  307. def snat4(snat_target):
  308. global lock
  309. global quit_now
  310. def get_snat4_rule():
  311. rule = iptc.Rule()
  312. rule.src = os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24'
  313. rule.dst = '!' + rule.src
  314. target = rule.create_target("SNAT")
  315. target.to_source = snat_target
  316. match = rule.create_match("comment")
  317. match.comment = f'{int(round(time.time()))}'
  318. return rule
  319. while not quit_now:
  320. time.sleep(10)
  321. with lock:
  322. try:
  323. table = iptc.Table('nat')
  324. table.refresh()
  325. chain = iptc.Chain(table, 'POSTROUTING')
  326. table.autocommit = False
  327. new_rule = get_snat4_rule()
  328. if not chain.rules:
  329. # if there are no rules in the chain, insert the new rule directly
  330. logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
  331. chain.insert_rule(new_rule)
  332. else:
  333. for position, rule in enumerate(chain.rules):
  334. match = all((
  335. new_rule.get_src() == rule.get_src(),
  336. new_rule.get_dst() == rule.get_dst(),
  337. new_rule.target.parameters == rule.target.parameters,
  338. new_rule.target.name == rule.target.name
  339. ))
  340. if position == 0:
  341. if not match:
  342. logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
  343. chain.insert_rule(new_rule)
  344. else:
  345. if match:
  346. logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}')
  347. chain.delete_rule(rule)
  348. table.commit()
  349. table.autocommit = True
  350. except:
  351. print('Error running SNAT4, retrying...')
  352. def snat6(snat_target):
  353. global lock
  354. global quit_now
  355. def get_snat6_rule():
  356. rule = iptc.Rule6()
  357. rule.src = os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64')
  358. rule.dst = '!' + rule.src
  359. target = rule.create_target("SNAT")
  360. target.to_source = snat_target
  361. return rule
  362. while not quit_now:
  363. time.sleep(10)
  364. with lock:
  365. try:
  366. table = iptc.Table6('nat')
  367. table.refresh()
  368. chain = iptc.Chain(table, 'POSTROUTING')
  369. table.autocommit = False
  370. if get_snat6_rule() not in chain.rules:
  371. logInfo('Added POSTROUTING rule for source network %s to SNAT target %s' % (get_snat6_rule().src, snat_target))
  372. chain.insert_rule(get_snat6_rule())
  373. table.commit()
  374. else:
  375. for position, item in enumerate(chain.rules):
  376. if item == get_snat6_rule():
  377. if position != 0:
  378. chain.delete_rule(get_snat6_rule())
  379. table.commit()
  380. table.autocommit = True
  381. except:
  382. print('Error running SNAT6, retrying...')
  383. def autopurge():
  384. while not quit_now:
  385. time.sleep(10)
  386. refreshF2boptions()
  387. BAN_TIME = int(f2boptions['ban_time'])
  388. MAX_ATTEMPTS = int(f2boptions['max_attempts'])
  389. QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
  390. if QUEUE_UNBAN:
  391. for net in QUEUE_UNBAN:
  392. unban(str(net))
  393. for net in bans.copy():
  394. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  395. if time.time() - bans[net]['last_attempt'] > BAN_TIME:
  396. unban(net)
  397. def isIpNetwork(address):
  398. try:
  399. ipaddress.ip_network(address, False)
  400. except ValueError:
  401. return False
  402. return True
  403. def genNetworkList(list):
  404. resolver = dns.resolver.Resolver()
  405. hostnames = []
  406. networks = []
  407. for key in list:
  408. if isIpNetwork(key):
  409. networks.append(key)
  410. else:
  411. hostnames.append(key)
  412. for hostname in hostnames:
  413. hostname_ips = []
  414. for rdtype in ['A', 'AAAA']:
  415. try:
  416. answer = resolver.resolve(qname=hostname, rdtype=rdtype, lifetime=3)
  417. except dns.exception.Timeout:
  418. logInfo('Hostname %s timedout on resolve' % hostname)
  419. break
  420. except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
  421. continue
  422. except dns.exception.DNSException as dnsexception:
  423. logInfo('%s' % dnsexception)
  424. continue
  425. for rdata in answer:
  426. hostname_ips.append(rdata.to_text())
  427. networks.extend(hostname_ips)
  428. return set(networks)
  429. def whitelistUpdate():
  430. global lock
  431. global quit_now
  432. global WHITELIST
  433. while not quit_now:
  434. start_time = time.time()
  435. list = r.hgetall('F2B_WHITELIST')
  436. new_whitelist = []
  437. if list:
  438. new_whitelist = genNetworkList(list)
  439. with lock:
  440. if Counter(new_whitelist) != Counter(WHITELIST):
  441. WHITELIST = new_whitelist
  442. logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
  443. time.sleep(60.0 - ((time.time() - start_time) % 60.0))
  444. def blacklistUpdate():
  445. global quit_now
  446. global BLACKLIST
  447. while not quit_now:
  448. start_time = time.time()
  449. list = r.hgetall('F2B_BLACKLIST')
  450. new_blacklist = []
  451. if list:
  452. new_blacklist = genNetworkList(list)
  453. if Counter(new_blacklist) != Counter(BLACKLIST):
  454. addban = set(new_blacklist).difference(BLACKLIST)
  455. delban = set(BLACKLIST).difference(new_blacklist)
  456. BLACKLIST = new_blacklist
  457. logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST))
  458. if addban:
  459. for net in addban:
  460. permBan(net=net)
  461. if delban:
  462. for net in delban:
  463. permBan(net=net, unban=True)
  464. time.sleep(60.0 - ((time.time() - start_time) % 60.0))
  465. def initChain():
  466. # Is called before threads start, no locking
  467. print("Initializing mailcow netfilter chain")
  468. # IPv4
  469. if not iptc.Chain(iptc.Table(iptc.Table.FILTER), "MAILCOW") in iptc.Table(iptc.Table.FILTER).chains:
  470. iptc.Table(iptc.Table.FILTER).create_chain("MAILCOW")
  471. for c in ['FORWARD', 'INPUT']:
  472. chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c)
  473. rule = iptc.Rule()
  474. rule.src = '0.0.0.0/0'
  475. rule.dst = '0.0.0.0/0'
  476. target = iptc.Target(rule, "MAILCOW")
  477. rule.target = target
  478. if rule not in chain.rules:
  479. chain.insert_rule(rule)
  480. # IPv6
  481. if not iptc.Chain(iptc.Table6(iptc.Table6.FILTER), "MAILCOW") in iptc.Table6(iptc.Table6.FILTER).chains:
  482. iptc.Table6(iptc.Table6.FILTER).create_chain("MAILCOW")
  483. for c in ['FORWARD', 'INPUT']:
  484. chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c)
  485. rule = iptc.Rule6()
  486. rule.src = '::/0'
  487. rule.dst = '::/0'
  488. target = iptc.Target(rule, "MAILCOW")
  489. rule.target = target
  490. if rule not in chain.rules:
  491. chain.insert_rule(rule)
  492. if __name__ == '__main__':
  493. # In case a previous session was killed without cleanup
  494. clear()
  495. # Reinit MAILCOW chain
  496. initChain()
  497. watch_thread = Thread(target=watch)
  498. watch_thread.daemon = True
  499. watch_thread.start()
  500. if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') != 'n':
  501. try:
  502. snat_ip = os.getenv('SNAT_TO_SOURCE')
  503. snat_ipo = ipaddress.ip_address(snat_ip)
  504. if type(snat_ipo) is ipaddress.IPv4Address:
  505. snat4_thread = Thread(target=snat4,args=(snat_ip,))
  506. snat4_thread.daemon = True
  507. snat4_thread.start()
  508. except ValueError:
  509. print(os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address')
  510. if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') != 'n':
  511. try:
  512. snat_ip = os.getenv('SNAT6_TO_SOURCE')
  513. snat_ipo = ipaddress.ip_address(snat_ip)
  514. if type(snat_ipo) is ipaddress.IPv6Address:
  515. snat6_thread = Thread(target=snat6,args=(snat_ip,))
  516. snat6_thread.daemon = True
  517. snat6_thread.start()
  518. except ValueError:
  519. print(os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address')
  520. autopurge_thread = Thread(target=autopurge)
  521. autopurge_thread.daemon = True
  522. autopurge_thread.start()
  523. mailcowchainwatch_thread = Thread(target=mailcowChainOrder)
  524. mailcowchainwatch_thread.daemon = True
  525. mailcowchainwatch_thread.start()
  526. blacklistupdate_thread = Thread(target=blacklistUpdate)
  527. blacklistupdate_thread.daemon = True
  528. blacklistupdate_thread.start()
  529. whitelistupdate_thread = Thread(target=whitelistUpdate)
  530. whitelistupdate_thread.daemon = True
  531. whitelistupdate_thread.start()
  532. signal.signal(signal.SIGTERM, quit)
  533. atexit.register(clear)
  534. while not quit_now:
  535. time.sleep(0.5)
  536. sys.exit(exit_code)