logwatch.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  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. yes_regex = re.compile(r'([yY][eE][sS]|[yY])+$')
  14. if re.search(yes_regex, os.getenv('SKIP_FAIL2BAN', 0)):
  15. print "SKIP_FAIL2BAN=y, Skipping Fail2ban container..."
  16. time.sleep(31536000)
  17. raise SystemExit
  18. r = redis.StrictRedis(host='172.22.1.249', decode_responses=True, port=6379, db=0)
  19. pubsub = r.pubsub()
  20. RULES = {}
  21. RULES[1] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed'
  22. RULES[2] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
  23. RULES[3] = '-login: Aborted login \(no auth .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
  24. RULES[4] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
  25. RULES[5] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
  26. RULES[6] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
  27. r.setnx("F2B_BAN_TIME", "1800")
  28. r.setnx("F2B_MAX_ATTEMPTS", "10")
  29. r.setnx("F2B_RETRY_WINDOW", "600")
  30. bans = {}
  31. log = {}
  32. quit_now = False
  33. def ban(address):
  34. BAN_TIME = int(r.get("F2B_BAN_TIME"))
  35. MAX_ATTEMPTS = int(r.get("F2B_MAX_ATTEMPTS"))
  36. RETRY_WINDOW = int(r.get("F2B_RETRY_WINDOW"))
  37. WHITELIST = r.hgetall("F2B_WHITELIST")
  38. ip = ipaddress.ip_address(address.decode('ascii'))
  39. if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
  40. ip = ip.ipv4_mapped
  41. address = str(ip)
  42. if ip.is_private or ip.is_loopback:
  43. return
  44. self_network = ipaddress.ip_network(address.decode('ascii'))
  45. if WHITELIST:
  46. for wl_key in WHITELIST:
  47. wl_net = ipaddress.ip_network(wl_key.decode('ascii'), False)
  48. if wl_net.overlaps(self_network):
  49. log['time'] = int(round(time.time()))
  50. log['priority'] = "info"
  51. log['message'] = "Address %s is whitelisted by rule %s" % (self_network, wl_net)
  52. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  53. print "Address %s is whitelisted by rule %s" % (self_network, wl_net)
  54. return
  55. net = ipaddress.ip_network((address + ('/24' if type(ip) is ipaddress.IPv4Address else '/64')).decode('ascii'), strict=False)
  56. net = str(net)
  57. if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
  58. bans[net] = { 'attempts': 0 }
  59. active_window = RETRY_WINDOW
  60. else:
  61. active_window = time.time() - bans[net]['last_attempt']
  62. bans[net]['attempts'] += 1
  63. bans[net]['last_attempt'] = time.time()
  64. active_window = time.time() - bans[net]['last_attempt']
  65. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  66. log['time'] = int(round(time.time()))
  67. log['priority'] = "crit"
  68. log['message'] = "Banning %s" % net
  69. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  70. print "Banning %s for %d minutes" % (net, BAN_TIME / 60)
  71. if type(ip) is ipaddress.IPv4Address:
  72. subprocess.call(["iptables", "-I", "INPUT", "-s", net, "-j", "REJECT"])
  73. subprocess.call(["iptables", "-I", "FORWARD", "-s", net, "-j", "REJECT"])
  74. else:
  75. subprocess.call(["ip6tables", "-I", "INPUT", "-s", net, "-j", "REJECT"])
  76. subprocess.call(["ip6tables", "-I", "FORWARD", "-s", net, "-j", "REJECT"])
  77. r.hset("F2B_ACTIVE_BANS", "%s" % net, log['time'] + BAN_TIME)
  78. else:
  79. log['time'] = int(round(time.time()))
  80. log['priority'] = "warn"
  81. log['message'] = "%d more attempts in the next %d seconds until %s is banned" % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
  82. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  83. print "%d more attempts in the next %d seconds until %s is banned" % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
  84. def unban(net):
  85. log['time'] = int(round(time.time()))
  86. log['priority'] = "info"
  87. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  88. if not net in bans:
  89. log['message'] = "%s is not banned, skipping unban and deleting from queue (if any)" % net
  90. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  91. print "%s is not banned, skipping unban and deleting from queue (if any)" % net
  92. r.hdel("F2B_QUEUE_UNBAN", "%s" % net)
  93. return
  94. log['message'] = "Unbanning %s" % net
  95. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  96. print "Unbanning %s" % net
  97. if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network:
  98. subprocess.call(["iptables", "-D", "INPUT", "-s", net, "-j", "REJECT"])
  99. subprocess.call(["iptables", "-D", "FORWARD", "-s", net, "-j", "REJECT"])
  100. else:
  101. subprocess.call(["ip6tables", "-D", "INPUT", "-s", net, "-j", "REJECT"])
  102. subprocess.call(["ip6tables", "-D", "FORWARD", "-s", net, "-j", "REJECT"])
  103. r.hdel("F2B_ACTIVE_BANS", "%s" % net)
  104. r.hdel("F2B_QUEUE_UNBAN", "%s" % net)
  105. del bans[net]
  106. def quit(signum, frame):
  107. global quit_now
  108. quit_now = True
  109. def clear():
  110. log['time'] = int(round(time.time()))
  111. log['priority'] = "info"
  112. log['message'] = "Clearing all bans"
  113. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  114. print "Clearing all bans"
  115. for net in bans.copy():
  116. unban(net)
  117. pubsub.unsubscribe()
  118. def watch():
  119. log['time'] = int(round(time.time()))
  120. log['priority'] = "info"
  121. log['message'] = "Watching Redis channel F2B_CHANNEL"
  122. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  123. pubsub.subscribe("F2B_CHANNEL")
  124. print "Subscribing to Redis channel F2B_CHANNEL"
  125. while True:
  126. for item in pubsub.listen():
  127. for rule_id, rule_regex in RULES.iteritems():
  128. if item['data'] and item['type'] == 'message':
  129. result = re.search(rule_regex, item['data'])
  130. if result:
  131. addr = result.group(1)
  132. print "%s matched rule id %d" % (addr, rule_id)
  133. log['time'] = int(round(time.time()))
  134. log['priority'] = "warn"
  135. log['message'] = "%s matched rule id %d" % (addr, rule_id)
  136. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  137. ban(addr)
  138. def autopurge():
  139. while not quit_now:
  140. BAN_TIME = int(r.get("F2B_BAN_TIME"))
  141. MAX_ATTEMPTS = int(r.get("F2B_MAX_ATTEMPTS"))
  142. QUEUE_UNBAN = r.hgetall("F2B_QUEUE_UNBAN")
  143. if QUEUE_UNBAN:
  144. for net in QUEUE_UNBAN:
  145. unban(str(net))
  146. for net in bans.copy():
  147. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  148. if time.time() - bans[net]['last_attempt'] > BAN_TIME:
  149. unban(net)
  150. time.sleep(10)
  151. if __name__ == '__main__':
  152. watch_thread = Thread(target=watch)
  153. watch_thread.daemon = True
  154. watch_thread.start()
  155. autopurge_thread = Thread(target=autopurge)
  156. autopurge_thread.daemon = True
  157. autopurge_thread.start()
  158. signal.signal(signal.SIGTERM, quit)
  159. atexit.register(clear)
  160. while not quit_now:
  161. time.sleep(0.5)