logwatch.py 6.9 KB

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