server.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. #!/usr/bin/env python2
  2. import re
  3. import os
  4. import time
  5. import atexit
  6. import signal
  7. import ipaddress
  8. import subprocess
  9. from threading import Thread
  10. import redis
  11. import time
  12. import json
  13. import iptc
  14. r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0)
  15. pubsub = r.pubsub()
  16. RULES = {}
  17. RULES[1] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed'
  18. RULES[2] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
  19. RULES[3] = '-login: Aborted login \(no auth .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
  20. RULES[4] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
  21. RULES[5] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
  22. RULES[6] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
  23. bans = {}
  24. log = {}
  25. quit_now = False
  26. def refreshF2boptions():
  27. global f2boptions
  28. global quit_now
  29. if not r.get('F2B_OPTIONS'):
  30. f2boptions = {}
  31. f2boptions['ban_time'] = int
  32. f2boptions['max_attempts'] = int
  33. f2boptions['retry_window'] = int
  34. f2boptions['netban_ipv4'] = int
  35. f2boptions['netban_ipv6'] = int
  36. f2boptions['ban_time'] = r.get('F2B_BAN_TIME') or 1800
  37. f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS') or 10
  38. f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW') or 600
  39. f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4') or 24
  40. f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6') or 64
  41. r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
  42. else:
  43. try:
  44. f2boptions = {}
  45. f2boptions = json.loads(r.get('F2B_OPTIONS'))
  46. except ValueError, e:
  47. print 'Error loading F2B options: F2B_OPTIONS is not json'
  48. quit_now = True
  49. if r.exists('F2B_LOG'):
  50. r.rename('F2B_LOG', 'NETFILTER_LOG')
  51. def checkChainOrder():
  52. global quit_now
  53. while not quit_now:
  54. time.sleep(20)
  55. filter4_table = iptc.Table(iptc.Table.FILTER)
  56. filter6_table = iptc.Table6(iptc.Table6.FILTER)
  57. filter4_table.refresh()
  58. filter6_table.refresh()
  59. for f in [filter4_table, filter6_table]:
  60. forward_chain = iptc.Chain(f, 'FORWARD')
  61. input_chain = iptc.Chain(f, 'INPUT')
  62. for chain in [forward_chain, input_chain]:
  63. target_found = False
  64. for position, item in enumerate(chain.rules):
  65. if item.target.name == 'MAILCOW':
  66. target_found = True
  67. if position != 0:
  68. log['time'] = int(round(time.time()))
  69. log['priority'] = 'crit'
  70. log['message'] = 'Error in ' + chain.name + ' chain order, restarting container'
  71. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  72. print log['message']
  73. quit_now = True
  74. if not target_found:
  75. log['time'] = int(round(time.time()))
  76. log['priority'] = 'crit'
  77. log['message'] = 'Error in ' + chain.name + ' chain: target not found, restarting container'
  78. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  79. print log['message']
  80. quit_now = True
  81. def ban(address):
  82. refreshF2boptions()
  83. BAN_TIME = int(f2boptions['ban_time'])
  84. MAX_ATTEMPTS = int(f2boptions['max_attempts'])
  85. RETRY_WINDOW = int(f2boptions['retry_window'])
  86. NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
  87. NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
  88. WHITELIST = r.hgetall('F2B_WHITELIST')
  89. ip = ipaddress.ip_address(address.decode('ascii'))
  90. if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
  91. ip = ip.ipv4_mapped
  92. address = str(ip)
  93. if ip.is_private or ip.is_loopback:
  94. return
  95. self_network = ipaddress.ip_network(address.decode('ascii'))
  96. if WHITELIST:
  97. for wl_key in WHITELIST:
  98. wl_net = ipaddress.ip_network(wl_key.decode('ascii'), False)
  99. if wl_net.overlaps(self_network):
  100. log['time'] = int(round(time.time()))
  101. log['priority'] = 'info'
  102. log['message'] = 'Address %s is whitelisted by rule %s' % (self_network, wl_net)
  103. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  104. print 'Address %s is whitelisted by rule %s' % (self_network, wl_net)
  105. return
  106. net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)).decode('ascii'), strict=False)
  107. net = str(net)
  108. if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
  109. bans[net] = { 'attempts': 0 }
  110. active_window = RETRY_WINDOW
  111. else:
  112. active_window = time.time() - bans[net]['last_attempt']
  113. bans[net]['attempts'] += 1
  114. bans[net]['last_attempt'] = time.time()
  115. active_window = time.time() - bans[net]['last_attempt']
  116. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  117. log['time'] = int(round(time.time()))
  118. log['priority'] = 'crit'
  119. log['message'] = 'Banning %s' % net
  120. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  121. print 'Banning %s for %d minutes' % (net, BAN_TIME / 60)
  122. if type(ip) is ipaddress.IPv4Address:
  123. chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
  124. rule = iptc.Rule()
  125. rule.src = net
  126. target = iptc.Target(rule, "REJECT")
  127. rule.target = target
  128. if rule not in chain.rules:
  129. chain.insert_rule(rule)
  130. else:
  131. chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
  132. rule = iptc.Rule6()
  133. rule.src = net
  134. target = iptc.Target(rule, "REJECT")
  135. rule.target = target
  136. if rule not in chain.rules:
  137. chain.insert_rule(rule)
  138. r.hset('F2B_ACTIVE_BANS', '%s' % net, log['time'] + BAN_TIME)
  139. else:
  140. log['time'] = int(round(time.time()))
  141. log['priority'] = 'warn'
  142. log['message'] = '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
  143. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  144. print '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
  145. def unban(net):
  146. log['time'] = int(round(time.time()))
  147. log['priority'] = 'info'
  148. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  149. if not net in bans:
  150. log['message'] = '%s is not banned, skipping unban and deleting from queue (if any)' % net
  151. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  152. print '%s is not banned, skipping unban and deleting from queue (if any)' % net
  153. r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
  154. return
  155. log['message'] = 'Unbanning %s' % net
  156. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  157. print 'Unbanning %s' % net
  158. if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network:
  159. chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
  160. rule = iptc.Rule()
  161. rule.src = net
  162. target = iptc.Target(rule, "REJECT")
  163. rule.target = target
  164. if rule in chain.rules:
  165. chain.delete_rule(rule)
  166. else:
  167. chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
  168. rule = iptc.Rule6()
  169. rule.src = net
  170. target = iptc.Target(rule, "REJECT")
  171. rule.target = target
  172. if rule in chain.rules:
  173. chain.delete_rule(rule)
  174. r.hdel('F2B_ACTIVE_BANS', '%s' % net)
  175. r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
  176. if net in bans:
  177. del bans[net]
  178. def quit(signum, frame):
  179. global quit_now
  180. quit_now = True
  181. def clear():
  182. log['time'] = int(round(time.time()))
  183. log['priority'] = 'info'
  184. log['message'] = 'Clearing all bans'
  185. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  186. print 'Clearing all bans'
  187. for net in bans.copy():
  188. unban(net)
  189. filter4_table = iptc.Table(iptc.Table.FILTER)
  190. filter6_table = iptc.Table6(iptc.Table6.FILTER)
  191. for filter_table in [filter4_table, filter6_table]:
  192. filter_table.autocommit = False
  193. forward_chain = iptc.Chain(filter_table, "FORWARD")
  194. input_chain = iptc.Chain(filter_table, "INPUT")
  195. mailcow_chain = iptc.Chain(filter_table, "MAILCOW")
  196. if mailcow_chain in filter_table.chains:
  197. for rule in mailcow_chain.rules:
  198. mailcow_chain.delete_rule(rule)
  199. for rule in forward_chain.rules:
  200. if rule.target.name == 'MAILCOW':
  201. forward_chain.delete_rule(rule)
  202. for rule in input_chain.rules:
  203. if rule.target.name == 'MAILCOW':
  204. input_chain.delete_rule(rule)
  205. filter_table.delete_chain("MAILCOW")
  206. filter_table.commit()
  207. filter_table.refresh()
  208. filter_table.autocommit = True
  209. r.delete('F2B_ACTIVE_BANS')
  210. r.delete('F2B_PERM_BANS')
  211. pubsub.unsubscribe()
  212. def watch():
  213. log['time'] = int(round(time.time()))
  214. log['priority'] = 'info'
  215. log['message'] = 'Watching Redis channel F2B_CHANNEL'
  216. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  217. pubsub.subscribe('F2B_CHANNEL')
  218. print 'Subscribing to Redis channel F2B_CHANNEL'
  219. while not quit_now:
  220. for item in pubsub.listen():
  221. for rule_id, rule_regex in RULES.iteritems():
  222. if item['data'] and item['type'] == 'message':
  223. result = re.search(rule_regex, item['data'])
  224. if result:
  225. addr = result.group(1)
  226. ip = ipaddress.ip_address(addr.decode('ascii'))
  227. if ip.is_private or ip.is_loopback:
  228. continue
  229. print '%s matched rule id %d' % (addr, rule_id)
  230. log['time'] = int(round(time.time()))
  231. log['priority'] = 'warn'
  232. log['message'] = '%s matched rule id %d' % (addr, rule_id)
  233. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  234. ban(addr)
  235. def snat(snat_target):
  236. def get_snat_rule():
  237. rule = iptc.Rule()
  238. rule.src = os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24'
  239. rule.dst = '!' + rule.src
  240. target = rule.create_target("SNAT")
  241. target.to_source = snat_target
  242. return rule
  243. while not quit_now:
  244. time.sleep(5)
  245. table = iptc.Table('nat')
  246. table.refresh()
  247. table.autocommit = False
  248. chain = iptc.Chain(table, 'POSTROUTING')
  249. if get_snat_rule() not in chain.rules:
  250. log['time'] = int(round(time.time()))
  251. log['priority'] = 'info'
  252. log['message'] = 'Added POSTROUTING rule for source network ' + get_snat_rule().src + ' to SNAT target ' + snat_target
  253. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  254. print log['message']
  255. chain.insert_rule(get_snat_rule())
  256. table.commit()
  257. else:
  258. for position, item in enumerate(chain.rules):
  259. if item == get_snat_rule():
  260. if position != 0:
  261. chain.delete_rule(get_snat_rule())
  262. table.commit()
  263. def autopurge():
  264. while not quit_now:
  265. time.sleep(10)
  266. refreshF2boptions()
  267. BAN_TIME = f2boptions['ban_time']
  268. MAX_ATTEMPTS = f2boptions['max_attempts']
  269. QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
  270. if QUEUE_UNBAN:
  271. for net in QUEUE_UNBAN:
  272. unban(str(net))
  273. for net in bans.copy():
  274. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  275. if time.time() - bans[net]['last_attempt'] > BAN_TIME:
  276. unban(net)
  277. def initChain():
  278. print "Initializing mailcow netfilter chain"
  279. # IPv4
  280. if not iptc.Chain(iptc.Table(iptc.Table.FILTER), "MAILCOW") in iptc.Table(iptc.Table.FILTER).chains:
  281. iptc.Table(iptc.Table.FILTER).create_chain("MAILCOW")
  282. for c in ['FORWARD', 'INPUT']:
  283. chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c)
  284. rule = iptc.Rule()
  285. rule.src = '0.0.0.0/0'
  286. rule.dst = '0.0.0.0/0'
  287. target = iptc.Target(rule, "MAILCOW")
  288. rule.target = target
  289. if rule not in chain.rules:
  290. chain.insert_rule(rule)
  291. # IPv6
  292. if not iptc.Chain(iptc.Table6(iptc.Table6.FILTER), "MAILCOW") in iptc.Table6(iptc.Table6.FILTER).chains:
  293. iptc.Table6(iptc.Table6.FILTER).create_chain("MAILCOW")
  294. for c in ['FORWARD', 'INPUT']:
  295. chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c)
  296. rule = iptc.Rule6()
  297. rule.src = '::/0'
  298. rule.dst = '::/0'
  299. target = iptc.Target(rule, "MAILCOW")
  300. rule.target = target
  301. if rule not in chain.rules:
  302. chain.insert_rule(rule)
  303. # Apply blacklist
  304. BLACKLIST = r.hgetall('F2B_BLACKLIST')
  305. if BLACKLIST:
  306. for bl_key in BLACKLIST:
  307. if type(ipaddress.ip_network(bl_key.decode('ascii'), strict=False)) is ipaddress.IPv4Network:
  308. chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
  309. rule = iptc.Rule()
  310. rule.src = bl_key
  311. target = iptc.Target(rule, "REJECT")
  312. rule.target = target
  313. if rule not in chain.rules:
  314. log['time'] = int(round(time.time()))
  315. log['priority'] = 'crit'
  316. log['message'] = 'Blacklisting host/network %s' % bl_key
  317. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  318. print log['message']
  319. chain.insert_rule(rule)
  320. r.hset('F2B_PERM_BANS', '%s' % bl_key, int(round(time.time())))
  321. else:
  322. chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
  323. rule = iptc.Rule6()
  324. rule.src = bl_key
  325. target = iptc.Target(rule, "REJECT")
  326. rule.target = target
  327. if rule not in chain.rules:
  328. log['time'] = int(round(time.time()))
  329. log['priority'] = 'crit'
  330. log['message'] = 'Blacklisting host/network %s' % bl_key
  331. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  332. print log['message']
  333. chain.insert_rule(rule)
  334. r.hset('F2B_PERM_BANS', '%s' % bl_key, int(round(time.time())))
  335. if __name__ == '__main__':
  336. # In case a previous session was killed without cleanup
  337. clear()
  338. # Reinit MAILCOW chain
  339. initChain()
  340. watch_thread = Thread(target=watch)
  341. watch_thread.daemon = True
  342. watch_thread.start()
  343. if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') is not 'n':
  344. try:
  345. snat_ip = os.getenv('SNAT_TO_SOURCE').decode('ascii')
  346. snat_ipo = ipaddress.ip_address(snat_ip)
  347. if type(snat_ipo) is ipaddress.IPv4Address:
  348. snat_thread = Thread(target=snat,args=(snat_ip,))
  349. snat_thread.daemon = True
  350. snat_thread.start()
  351. except ValueError:
  352. print os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address'
  353. autopurge_thread = Thread(target=autopurge)
  354. autopurge_thread.daemon = True
  355. autopurge_thread.start()
  356. chainwatch_thread = Thread(target=checkChainOrder)
  357. chainwatch_thread.daemon = True
  358. chainwatch_thread.start()
  359. signal.signal(signal.SIGTERM, quit)
  360. atexit.register(clear)
  361. while not quit_now:
  362. time.sleep(0.5)