server.py 9.7 KB

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