logwatch.py 6.3 KB

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