server.py 18 KB

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