server.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  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. if not r.get('F2B_OPTIONS'):
  24. f2options['ban_time'] = int(r.get('F2B_BAN_TIME')) or 1800
  25. f2options['max_attempts'] = int(r.get('F2B_MAX_ATTEMPTS')) or 10
  26. f2options['retry_window'] = int(r.get('F2B_RETRY_WINDOW')) or 600
  27. f2options['netban_ipv4'] = int(r.get('F2B_NETBAN_IPV4')) or 24
  28. f2options['netban_ipv6'] = int(r.get('F2B_NETBAN_IPV6')) or 64
  29. r.set('F2B_OPTIONS', json.dumps(f2options, ensure_ascii=False))
  30. else:
  31. try:
  32. f2options = json.loads(r.get('F2B_OPTIONS'))
  33. except ValueError, e:
  34. print 'Error loading F2B options: F2B_OPTIONS is not json'
  35. raise SystemExit(1)
  36. if r.exists('F2B_LOG'):
  37. r.rename('F2B_LOG', 'NETFILTER_LOG')
  38. bans = {}
  39. log = {}
  40. quit_now = False
  41. def ban(address):
  42. BAN_TIME = int(f2options['ban_time'])
  43. MAX_ATTEMPTS = int(f2options['max_attempts'])
  44. RETRY_WINDOW = int(f2options['retry_window'])
  45. NETBAN_IPV4 = '/' + str(f2options['netban_ipv4'])
  46. NETBAN_IPV6 = '/' + str(f2options['netban_ipv6'])
  47. WHITELIST = r.hgetall('F2B_WHITELIST')
  48. ip = ipaddress.ip_address(address.decode('ascii'))
  49. if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
  50. ip = ip.ipv4_mapped
  51. address = str(ip)
  52. if ip.is_private or ip.is_loopback:
  53. return
  54. self_network = ipaddress.ip_network(address.decode('ascii'))
  55. if WHITELIST:
  56. for wl_key in WHITELIST:
  57. wl_net = ipaddress.ip_network(wl_key.decode('ascii'), False)
  58. if wl_net.overlaps(self_network):
  59. log['time'] = int(round(time.time()))
  60. log['priority'] = 'info'
  61. log['message'] = 'Address %s is whitelisted by rule %s' % (self_network, wl_net)
  62. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  63. print 'Address %s is whitelisted by rule %s' % (self_network, wl_net)
  64. return
  65. net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)).decode('ascii'), strict=False)
  66. net = str(net)
  67. if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
  68. bans[net] = { 'attempts': 0 }
  69. active_window = RETRY_WINDOW
  70. else:
  71. active_window = time.time() - bans[net]['last_attempt']
  72. bans[net]['attempts'] += 1
  73. bans[net]['last_attempt'] = time.time()
  74. active_window = time.time() - bans[net]['last_attempt']
  75. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  76. log['time'] = int(round(time.time()))
  77. log['priority'] = 'crit'
  78. log['message'] = 'Banning %s' % net
  79. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  80. print 'Banning %s for %d minutes' % (net, BAN_TIME / 60)
  81. if type(ip) is ipaddress.IPv4Address:
  82. for c in ['INPUT', 'FORWARD']:
  83. chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c)
  84. rule = iptc.Rule()
  85. rule.src = net
  86. target = iptc.Target(rule, "REJECT")
  87. rule.target = target
  88. if rule not in chain.rules:
  89. chain.insert_rule(rule)
  90. else:
  91. for c in ['INPUT', 'FORWARD']:
  92. chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c)
  93. rule = iptc.Rule6()
  94. rule.src = net
  95. target = iptc.Target(rule, "REJECT")
  96. rule.target = target
  97. if rule not in chain.rules:
  98. chain.insert_rule(rule)
  99. r.hset('F2B_ACTIVE_BANS', '%s' % net, log['time'] + BAN_TIME)
  100. else:
  101. log['time'] = int(round(time.time()))
  102. log['priority'] = 'warn'
  103. log['message'] = '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
  104. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  105. print '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
  106. def unban(net):
  107. log['time'] = int(round(time.time()))
  108. log['priority'] = 'info'
  109. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  110. #if not net in bans:
  111. # log['message'] = '%s is not banned, skipping unban and deleting from queue (if any)' % net
  112. # r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  113. # print '%s is not banned, skipping unban and deleting from queue (if any)' % net
  114. # r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
  115. # return
  116. log['message'] = 'Unbanning %s' % net
  117. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  118. print 'Unbanning %s' % net
  119. if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network:
  120. for c in ['INPUT', 'FORWARD']:
  121. chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c)
  122. rule = iptc.Rule()
  123. rule.src = net
  124. target = iptc.Target(rule, "REJECT")
  125. rule.target = target
  126. if rule in chain.rules:
  127. chain.delete_rule(rule)
  128. else:
  129. for c in ['INPUT', 'FORWARD']:
  130. chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c)
  131. rule = iptc.Rule6()
  132. rule.src = net
  133. target = iptc.Target(rule, "REJECT")
  134. rule.target = target
  135. if rule in chain.rules:
  136. chain.delete_rule(rule)
  137. r.hdel('F2B_ACTIVE_BANS', '%s' % net)
  138. r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
  139. if net in bans:
  140. del bans[net]
  141. def quit(signum, frame):
  142. global quit_now
  143. quit_now = True
  144. def clear():
  145. log['time'] = int(round(time.time()))
  146. log['priority'] = 'info'
  147. log['message'] = 'Clearing all bans'
  148. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  149. print 'Clearing all bans'
  150. for net in bans.copy():
  151. unban(net)
  152. pubsub.unsubscribe()
  153. def watch():
  154. log['time'] = int(round(time.time()))
  155. log['priority'] = 'info'
  156. log['message'] = 'Watching Redis channel F2B_CHANNEL'
  157. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  158. pubsub.subscribe('F2B_CHANNEL')
  159. print 'Subscribing to Redis channel F2B_CHANNEL'
  160. while True:
  161. for item in pubsub.listen():
  162. for rule_id, rule_regex in RULES.iteritems():
  163. if item['data'] and item['type'] == 'message':
  164. result = re.search(rule_regex, item['data'])
  165. if result:
  166. addr = result.group(1)
  167. ip = ipaddress.ip_address(addr.decode('ascii'))
  168. if ip.is_private or ip.is_loopback:
  169. continue
  170. print '%s matched rule id %d' % (addr, rule_id)
  171. log['time'] = int(round(time.time()))
  172. log['priority'] = 'warn'
  173. log['message'] = '%s matched rule id %d' % (addr, rule_id)
  174. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  175. ban(addr)
  176. def snat(snat_target):
  177. def get_snat_rule():
  178. rule = iptc.Rule()
  179. rule.position = 1
  180. rule.src = os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24'
  181. rule.dst = '!' + rule.src
  182. target = rule.create_target("SNAT")
  183. target.to_source = snat_target
  184. return rule
  185. while True:
  186. table = iptc.Table('nat')
  187. table.autocommit = False
  188. chain = iptc.Chain(table, 'POSTROUTING')
  189. if get_snat_rule() not in chain.rules:
  190. log['time'] = int(round(time.time()))
  191. log['priority'] = 'info'
  192. log['message'] = 'Added POSTROUTING rule for source network ' + get_snat_rule().src + ' to SNAT target ' + snat_target
  193. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  194. print log['message']
  195. chain.insert_rule(get_snat_rule())
  196. table.commit()
  197. table.refresh()
  198. time.sleep(10)
  199. def autopurge():
  200. while not quit_now:
  201. BAN_TIME = int(r.get('F2B_BAN_TIME'))
  202. MAX_ATTEMPTS = int(r.get('F2B_MAX_ATTEMPTS'))
  203. QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
  204. if QUEUE_UNBAN:
  205. for net in QUEUE_UNBAN:
  206. unban(str(net))
  207. for net in bans.copy():
  208. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  209. if time.time() - bans[net]['last_attempt'] > BAN_TIME:
  210. unban(net)
  211. time.sleep(10)
  212. def cleanPrevious():
  213. print "Cleaning previously cached bans"
  214. F2B_ACTIVE_BANS = r.hgetall('F2B_ACTIVE_BANS')
  215. if F2B_ACTIVE_BANS:
  216. for net in F2B_ACTIVE_BANS:
  217. unban(str(net))
  218. if __name__ == '__main__':
  219. cleanPrevious()
  220. watch_thread = Thread(target=watch)
  221. watch_thread.daemon = True
  222. watch_thread.start()
  223. if os.getenv('SNAT_TO_SOURCE'):
  224. try:
  225. snat_ip = os.getenv('SNAT_TO_SOURCE').decode('ascii')
  226. snat_ipo = ipaddress.ip_address(snat_ip)
  227. if type(snat_ipo) is ipaddress.IPv4Address:
  228. snat_thread = Thread(target=snat,args=(snat_ip,))
  229. snat_thread.daemon = True
  230. snat_thread.start()
  231. except ValueError:
  232. print os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address'
  233. autopurge_thread = Thread(target=autopurge)
  234. autopurge_thread.daemon = True
  235. autopurge_thread.start()
  236. signal.signal(signal.SIGTERM, quit)
  237. atexit.register(clear)
  238. while not quit_now:
  239. time.sleep(0.5)