logwatch.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. #!/usr/bin/env python2
  2. import re
  3. import time
  4. import atexit
  5. import signal
  6. import ipaddress
  7. import subprocess
  8. from threading import Thread
  9. import docker
  10. import redis
  11. import time
  12. import json
  13. r = redis.StrictRedis(host='172.22.1.249', port=6379, db=0)
  14. RULES = {
  15. 'mailcowdockerized_postfix-mailcow_1': 'warning: .*\[([0-9a-f\.:]+)\]: SASL .* authentication failed',
  16. 'mailcowdockerized_dovecot-mailcow_1': '-login: Disconnected \(auth failed, .*\): user=.*, method=.*, rip=([0-9a-f\.:]+),',
  17. 'mailcowdockerized_sogo-mailcow_1': 'SOGo.* Login from \'([0-9a-f\.:]+)\' for user .* might not have worked',
  18. 'mailcowdockerized_php-fpm-mailcow_1': 'Mailcow UI: Invalid password for .* by ([0-9a-f\.:]+)',
  19. }
  20. r.setnx("F2B_BAN_TIME", "1800")
  21. r.setnx("F2B_MAX_ATTEMPTS", "10")
  22. r.setnx("F2B_RETRY_WINDOW", "600")
  23. bans = {}
  24. log = {}
  25. quit_now = False
  26. def ban(address):
  27. BAN_TIME = int(r.get("F2B_BAN_TIME"))
  28. MAX_ATTEMPTS = int(r.get("F2B_MAX_ATTEMPTS"))
  29. RETRY_WINDOW = int(r.get("F2B_RETRY_WINDOW"))
  30. ip = ipaddress.ip_address(address.decode('ascii'))
  31. if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
  32. ip = ip.ipv4_mapped
  33. address = str(ip)
  34. if ip.is_private or ip.is_loopback:
  35. return
  36. net = ipaddress.ip_network((address + ('/24' if type(ip) is ipaddress.IPv4Address else '/64')).decode('ascii'), strict=False)
  37. net = str(net)
  38. if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
  39. bans[net] = { 'attempts': 0 }
  40. active_window = RETRY_WINDOW
  41. else:
  42. active_window = time.time() - bans[net]['last_attempt']
  43. bans[net]['attempts'] += 1
  44. bans[net]['last_attempt'] = time.time()
  45. active_window = time.time() - bans[net]['last_attempt']
  46. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  47. log['time'] = int(round(time.time()))
  48. log['priority'] = "crit"
  49. log['message'] = "Banning %s" % net
  50. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  51. print "Banning %s for %d minutes" % (net, BAN_TIME / 60)
  52. if type(ip) is ipaddress.IPv4Address:
  53. subprocess.call(["iptables", "-I", "INPUT", "-s", net, "-j", "REJECT"])
  54. subprocess.call(["iptables", "-I", "FORWARD", "-s", net, "-j", "REJECT"])
  55. else:
  56. subprocess.call(["ip6tables", "-I", "INPUT", "-s", net, "-j", "REJECT"])
  57. subprocess.call(["ip6tables", "-I", "FORWARD", "-s", net, "-j", "REJECT"])
  58. else:
  59. log['time'] = int(round(time.time()))
  60. log['priority'] = "warn"
  61. log['message'] = "%d more attempts in the next %d seconds until %s is banned" % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
  62. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  63. print "%d more attempts in the next %d seconds until %s is banned" % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
  64. def unban(net):
  65. log['time'] = int(round(time.time()))
  66. log['priority'] = "info"
  67. log['message'] = "Unbanning %s" % net
  68. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  69. print "Unbanning %s" % net
  70. if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network:
  71. subprocess.call(["iptables", "-D", "INPUT", "-s", net, "-j", "REJECT"])
  72. subprocess.call(["iptables", "-D", "FORWARD", "-s", net, "-j", "REJECT"])
  73. else:
  74. subprocess.call(["ip6tables", "-D", "INPUT", "-s", net, "-j", "REJECT"])
  75. subprocess.call(["ip6tables", "-D", "FORWARD", "-s", net, "-j", "REJECT"])
  76. del bans[net]
  77. def quit(signum, frame):
  78. global quit_now
  79. quit_now = True
  80. def clear():
  81. log['time'] = int(round(time.time()))
  82. log['priority'] = "info"
  83. log['message'] = "Clearing all bans"
  84. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  85. print "Clearing all bans"
  86. for net in bans.copy():
  87. unban(net)
  88. def watch(container):
  89. log['time'] = int(round(time.time()))
  90. log['priority'] = "info"
  91. log['message'] = "Watching %s" % container
  92. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  93. print "Watching", container
  94. client = docker.from_env()
  95. for msg in client.containers.get(container).attach(stream=True, logs=False):
  96. result = re.search(RULES[container], msg)
  97. if result:
  98. addr = result.group(1)
  99. ban(addr)
  100. def autopurge():
  101. while not quit_now:
  102. BAN_TIME = int(r.get("F2B_BAN_TIME"))
  103. MAX_ATTEMPTS = int(r.get("F2B_MAX_ATTEMPTS"))
  104. for net in bans.copy():
  105. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  106. if time.time() - bans[net]['last_attempt'] > BAN_TIME:
  107. unban(net)
  108. time.sleep(60)
  109. if __name__ == '__main__':
  110. threads = []
  111. for container in RULES:
  112. threads.append(Thread(target=watch, args=(container,)))
  113. threads[-1].daemon = True
  114. threads[-1].start()
  115. autopurge_thread = Thread(target=autopurge)
  116. autopurge_thread.daemon = True
  117. autopurge_thread.start()
  118. signal.signal(signal.SIGTERM, quit)
  119. atexit.register(clear)
  120. while not quit_now:
  121. for thread in threads:
  122. if not thread.isAlive():
  123. break
  124. time.sleep(0.1)
  125. clear()