logwatch.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  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. bans = {}
  23. log = {}
  24. quit_now = False
  25. def ban(address):
  26. BAN_TIME = int(r.get("F2B_BAN_TIME"))
  27. MAX_ATTEMPTS = int(r.get("F2B_MAX_ATTEMPTS"))
  28. ip = ipaddress.ip_address(address.decode('ascii'))
  29. if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
  30. ip = ip.ipv4_mapped
  31. address = str(ip)
  32. if ip.is_private or ip.is_loopback:
  33. return
  34. net = ipaddress.ip_network((address + ('/24' if type(ip) is ipaddress.IPv4Address else '/64')).decode('ascii'), strict=False)
  35. net = str(net)
  36. if not net in bans or time.time() - bans[net]['last_attempt'] > BAN_TIME:
  37. bans[net] = { 'attempts': 0 }
  38. bans[net]['attempts'] += 1
  39. bans[net]['last_attempt'] = time.time()
  40. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  41. log['time'] = int(round(time.time()))
  42. log['message'] = "Banning %s" % net
  43. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  44. print "Banning %s" % net
  45. if type(ip) is ipaddress.IPv4Address:
  46. subprocess.call(["iptables", "-I", "INPUT", "-s", net, "-j", "REJECT"])
  47. subprocess.call(["iptables", "-I", "FORWARD", "-s", net, "-j", "REJECT"])
  48. else:
  49. subprocess.call(["ip6tables", "-I", "INPUT", "-s", net, "-j", "REJECT"])
  50. subprocess.call(["ip6tables", "-I", "FORWARD", "-s", net, "-j", "REJECT"])
  51. else:
  52. log['time'] = int(round(time.time()))
  53. log['message'] = "%d more attempts until %s is banned" % (MAX_ATTEMPTS - bans[net]['attempts'], net)
  54. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  55. print "%d more attempts until %s is banned" % (MAX_ATTEMPTS - bans[net]['attempts'], net)
  56. def unban(net):
  57. log['time'] = int(round(time.time()))
  58. log['message'] = "Unbanning %s" % net
  59. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  60. print "Unbanning %s" % net
  61. if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network:
  62. subprocess.call(["iptables", "-D", "INPUT", "-s", net, "-j", "REJECT"])
  63. subprocess.call(["iptables", "-D", "FORWARD", "-s", net, "-j", "REJECT"])
  64. else:
  65. subprocess.call(["ip6tables", "-D", "INPUT", "-s", net, "-j", "REJECT"])
  66. subprocess.call(["ip6tables", "-D", "FORWARD", "-s", net, "-j", "REJECT"])
  67. del bans[net]
  68. def quit(signum, frame):
  69. global quit_now
  70. quit_now = True
  71. def clear():
  72. log['time'] = int(round(time.time()))
  73. log['message'] = "Clearing all bans"
  74. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  75. print "Clearing all bans"
  76. for net in bans.copy():
  77. unban(net)
  78. def watch(container):
  79. log['time'] = int(round(time.time()))
  80. log['message'] = "Watching %s" % container
  81. r.lpush("F2B_LOG", json.dumps(log, ensure_ascii=False))
  82. print "Watching", container
  83. client = docker.from_env()
  84. for msg in client.containers.get(container).attach(stream=True, logs=False):
  85. result = re.search(RULES[container], msg)
  86. if result:
  87. addr = result.group(1)
  88. ban(addr)
  89. def autopurge():
  90. while not quit_now:
  91. BAN_TIME = int(r.get("F2B_BAN_TIME"))
  92. for net in bans.copy():
  93. if time.time() - bans[net]['last_attempt'] > BAN_TIME:
  94. unban(net)
  95. time.sleep(60)
  96. if __name__ == '__main__':
  97. threads = []
  98. for container in RULES:
  99. threads.append(Thread(target=watch, args=(container,)))
  100. threads[-1].daemon = True
  101. threads[-1].start()
  102. autopurge_thread = Thread(target=autopurge)
  103. autopurge_thread.daemon = True
  104. autopurge_thread.start()
  105. signal.signal(signal.SIGTERM, quit)
  106. atexit.register(clear)
  107. while not quit_now:
  108. for thread in threads:
  109. if not thread.isAlive():
  110. break
  111. time.sleep(0.1)
  112. clear()