server.py 13 KB

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