logwatch.py 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  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. RULES = {
  11. 'mailcowdockerized_postfix-mailcow_1': 'warning: .*\[([0-9a-f\.:]+)\]: SASL .* authentication failed',
  12. 'mailcowdockerized_dovecot-mailcow_1': '-login: Disconnected \(auth failed, .*\): user=.*, method=.*, rip=([0-9a-f\.:]+),',
  13. 'mailcowdockerized_sogo-mailcow_1': 'SOGo.* Login from \'([0-9a-f\.:]+)\' for user .* might not have worked',
  14. 'mailcowdockerized_php-fpm-mailcow_1': 'Mailcow UI: Invalid password for .* by ([0-9a-f\.:]+)',
  15. }
  16. BAN_TIME = 1800
  17. MAX_ATTEMPTS = 10
  18. bans = {}
  19. quit_now = False
  20. def ban(address):
  21. ip = ipaddress.ip_address(address.decode('ascii'))
  22. if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
  23. ip = ip.ipv4_mapped
  24. address = str(ip)
  25. if ip.is_private or ip.is_loopback:
  26. return
  27. net = ipaddress.ip_network((address + ('/24' if type(ip) is ipaddress.IPv4Address else '/64')).decode('ascii'), strict=False)
  28. net = str(net)
  29. if not net in bans or time.time() - bans[net]['last_attempt'] > BAN_TIME:
  30. bans[net] = { 'attempts': 0 }
  31. bans[net]['attempts'] += 1
  32. bans[net]['last_attempt'] = time.time()
  33. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  34. print "Banning %s" % net
  35. if type(ip) is ipaddress.IPv4Address:
  36. subprocess.call(["iptables", "-I", "INPUT", "-s", net, "-j", "REJECT"])
  37. subprocess.call(["iptables", "-I", "FORWARD", "-s", net, "-j", "REJECT"])
  38. else:
  39. subprocess.call(["ip6tables", "-I", "INPUT", "-s", net, "-j", "REJECT"])
  40. subprocess.call(["ip6tables", "-I", "FORWARD", "-s", net, "-j", "REJECT"])
  41. else:
  42. print "%d more attempts until %s is banned" % (MAX_ATTEMPTS - bans[net]['attempts'], net)
  43. def unban(net):
  44. print "Unbanning %s" % net
  45. if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network:
  46. subprocess.call(["iptables", "-D", "INPUT", "-s", net, "-j", "REJECT"])
  47. subprocess.call(["iptables", "-D", "FORWARD", "-s", net, "-j", "REJECT"])
  48. else:
  49. subprocess.call(["ip6tables", "-D", "INPUT", "-s", net, "-j", "REJECT"])
  50. subprocess.call(["ip6tables", "-D", "FORWARD", "-s", net, "-j", "REJECT"])
  51. del bans[net]
  52. def quit(signum, frame):
  53. global quit_now
  54. quit_now = True
  55. def clear():
  56. print "Clearing all bans"
  57. for net in bans.copy():
  58. unban(net)
  59. def watch(container):
  60. print "Watching", container
  61. client = docker.from_env()
  62. for msg in client.containers.get(container).attach(stream=True, logs=False):
  63. result = re.search(RULES[container], msg)
  64. if result:
  65. addr = result.group(1)
  66. ban(addr)
  67. def autopurge():
  68. while not quit_now:
  69. for net in bans.copy():
  70. if time.time() - bans[net]['last_attempt'] > BAN_TIME:
  71. unban(net)
  72. time.sleep(60)
  73. if __name__ == '__main__':
  74. threads = []
  75. for container in RULES:
  76. threads.append(Thread(target=watch, args=(container,)))
  77. threads[-1].daemon = True
  78. threads[-1].start()
  79. autopurge_thread = Thread(target=autopurge)
  80. autopurge_thread.daemon = True
  81. autopurge_thread.start()
  82. signal.signal(signal.SIGTERM, quit)
  83. atexit.register(clear)
  84. while not quit_now:
  85. for thread in threads:
  86. if not thread.isAlive():
  87. break
  88. time.sleep(0.1)
  89. clear()