server.py 18 KB

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