2
0

logwatch.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  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', decode_responses=True, 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. WHITELIST = r.hgetall("F2B_WHITELIST")
  31. ip = ipaddress.ip_address(address.decode('ascii'))
  32. if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
  33. ip = ip.ipv4_mapped
  34. address = str(ip)
  35. if ip.is_private or ip.is_loopback:
  36. return
  37. self_network = ipaddress.ip_network(address.decode('ascii'))
  38. if WHITELIST:
  39. for wl_key in WHITELIST:
  40. wl_net = ipaddress.ip_network(wl_key.decode('ascii'), False)
  41. if wl_net.overlaps(self_network):
  42. log['time'] = int(round(time.time()))
  43. log['priority'] = "info"
  44. log['message'] = "Address %s is whitelisted by rule %s" % (self_network, wl_net)
  45. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  46. print "Address %s is whitelisted by rule %s" % (self_network, wl_net)
  47. return
  48. net = ipaddress.ip_network((address + ('/24' if type(ip) is ipaddress.IPv4Address else '/64')).decode('ascii'), strict=False)
  49. net = str(net)
  50. if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
  51. bans[net] = { 'attempts': 0 }
  52. active_window = RETRY_WINDOW
  53. else:
  54. active_window = time.time() - bans[net]['last_attempt']
  55. bans[net]['attempts'] += 1
  56. bans[net]['last_attempt'] = time.time()
  57. active_window = time.time() - bans[net]['last_attempt']
  58. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  59. log['time'] = int(round(time.time()))
  60. log['priority'] = "crit"
  61. log['message'] = "Banning %s" % net
  62. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  63. print "Banning %s for %d minutes" % (net, BAN_TIME / 60)
  64. if type(ip) is ipaddress.IPv4Address:
  65. subprocess.call(["iptables", "-I", "INPUT", "-s", net, "-j", "REJECT"])
  66. subprocess.call(["iptables", "-I", "FORWARD", "-s", net, "-j", "REJECT"])
  67. else:
  68. subprocess.call(["ip6tables", "-I", "INPUT", "-s", net, "-j", "REJECT"])
  69. subprocess.call(["ip6tables", "-I", "FORWARD", "-s", net, "-j", "REJECT"])
  70. r.hset("F2B_ACTIVE_BANS", "%s" % net, log['time'] + BAN_TIME)
  71. else:
  72. log['time'] = int(round(time.time()))
  73. log['priority'] = "warn"
  74. log['message'] = "%d more attempts in the next %d seconds until %s is banned" % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
  75. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  76. print "%d more attempts in the next %d seconds until %s is banned" % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
  77. def unban(net):
  78. log['time'] = int(round(time.time()))
  79. log['priority'] = "info"
  80. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  81. if not net in bans:
  82. log['message'] = "%s is not banned, skipping unban and deleting from queue (if any)" % net
  83. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  84. print "%s is not banned, skipping unban and deleting from queue (if any)" % net
  85. r.hdel("F2B_QUEUE_UNBAN", "%s" % net)
  86. return
  87. log['message'] = "Unbanning %s" % net
  88. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  89. print "Unbanning %s" % net
  90. if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network:
  91. subprocess.call(["iptables", "-D", "INPUT", "-s", net, "-j", "REJECT"])
  92. subprocess.call(["iptables", "-D", "FORWARD", "-s", net, "-j", "REJECT"])
  93. else:
  94. subprocess.call(["ip6tables", "-D", "INPUT", "-s", net, "-j", "REJECT"])
  95. subprocess.call(["ip6tables", "-D", "FORWARD", "-s", net, "-j", "REJECT"])
  96. r.hdel("F2B_ACTIVE_BANS", "%s" % net)
  97. r.hdel("F2B_QUEUE_UNBAN", "%s" % net)
  98. del bans[net]
  99. def quit(signum, frame):
  100. global quit_now
  101. quit_now = True
  102. def clear():
  103. log['time'] = int(round(time.time()))
  104. log['priority'] = "info"
  105. log['message'] = "Clearing all bans"
  106. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  107. print "Clearing all bans"
  108. for net in bans.copy():
  109. unban(net)
  110. def watch(container):
  111. log['time'] = int(round(time.time()))
  112. log['priority'] = "info"
  113. log['message'] = "Watching %s" % container
  114. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  115. print "Watching", container
  116. client = docker.from_env()
  117. for msg in client.containers.get(container).attach(stream=True, logs=False):
  118. result = re.search(RULES[container], msg)
  119. if result:
  120. addr = result.group(1)
  121. ban(addr)
  122. def autopurge():
  123. while not quit_now:
  124. BAN_TIME = int(r.get("F2B_BAN_TIME"))
  125. MAX_ATTEMPTS = int(r.get("F2B_MAX_ATTEMPTS"))
  126. QUEUE_UNBAN = r.hgetall("F2B_QUEUE_UNBAN")
  127. if QUEUE_UNBAN:
  128. for net in QUEUE_UNBAN:
  129. unban(str(net))
  130. for net in bans.copy():
  131. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  132. if time.time() - bans[net]['last_attempt'] > BAN_TIME:
  133. unban(net)
  134. time.sleep(30)
  135. if __name__ == '__main__':
  136. threads = []
  137. for container in RULES:
  138. threads.append(Thread(target=watch, args=(container,)))
  139. threads[-1].daemon = True
  140. threads[-1].start()
  141. autopurge_thread = Thread(target=autopurge)
  142. autopurge_thread.daemon = True
  143. autopurge_thread.start()
  144. signal.signal(signal.SIGTERM, quit)
  145. atexit.register(clear)
  146. while not quit_now:
  147. for thread in threads:
  148. if not thread.isAlive():
  149. break
  150. time.sleep(0.1)
  151. clear()