server.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. #!/usr/bin/env python2
  2. import re
  3. import os
  4. import time
  5. import atexit
  6. import signal
  7. import ipaddress
  8. from random import randint
  9. from threading import Thread
  10. from threading import Lock
  11. import redis
  12. import json
  13. import iptc
  14. while True:
  15. try:
  16. r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0)
  17. r.ping()
  18. except Exception as ex:
  19. print '%s - trying again in 3 seconds' % (ex)
  20. time.sleep(3)
  21. else:
  22. break
  23. pubsub = r.pubsub()
  24. RULES = {}
  25. RULES[1] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed'
  26. RULES[2] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
  27. RULES[3] = '-login: Aborted login \(no auth .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
  28. RULES[4] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
  29. RULES[5] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
  30. RULES[6] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
  31. bans = {}
  32. log = {}
  33. quit_now = False
  34. lock = Lock()
  35. def refreshF2boptions():
  36. global f2boptions
  37. global quit_now
  38. if not r.get('F2B_OPTIONS'):
  39. f2boptions = {}
  40. f2boptions['ban_time'] = int
  41. f2boptions['max_attempts'] = int
  42. f2boptions['retry_window'] = int
  43. f2boptions['netban_ipv4'] = int
  44. f2boptions['netban_ipv6'] = int
  45. f2boptions['ban_time'] = r.get('F2B_BAN_TIME') or 1800
  46. f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS') or 10
  47. f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW') or 600
  48. f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4') or 24
  49. f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6') or 64
  50. r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
  51. else:
  52. try:
  53. f2boptions = {}
  54. f2boptions = json.loads(r.get('F2B_OPTIONS'))
  55. except ValueError, e:
  56. print 'Error loading F2B options: F2B_OPTIONS is not json'
  57. quit_now = True
  58. if r.exists('F2B_LOG'):
  59. r.rename('F2B_LOG', 'NETFILTER_LOG')
  60. def mailcowChainOrder():
  61. global lock
  62. global quit_now
  63. while not quit_now:
  64. time.sleep(10)
  65. with lock:
  66. filter4_table = iptc.Table(iptc.Table.FILTER)
  67. filter6_table = iptc.Table6(iptc.Table6.FILTER)
  68. filter4_table.refresh()
  69. filter6_table.refresh()
  70. for f in [filter4_table, filter6_table]:
  71. forward_chain = iptc.Chain(f, 'FORWARD')
  72. input_chain = iptc.Chain(f, 'INPUT')
  73. for chain in [forward_chain, input_chain]:
  74. target_found = False
  75. for position, item in enumerate(chain.rules):
  76. if item.target.name == 'MAILCOW':
  77. target_found = True
  78. if position != 0:
  79. log['time'] = int(round(time.time()))
  80. log['priority'] = 'crit'
  81. log['message'] = 'Error in ' + chain.name + ' chain order, restarting container'
  82. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  83. print log['message']
  84. quit_now = True
  85. if not target_found:
  86. log['time'] = int(round(time.time()))
  87. log['priority'] = 'crit'
  88. log['message'] = 'Error in ' + chain.name + ' chain: MAILCOW target not found, restarting container'
  89. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  90. print log['message']
  91. quit_now = True
  92. def ban(address):
  93. global lock
  94. refreshF2boptions()
  95. BAN_TIME = int(f2boptions['ban_time'])
  96. MAX_ATTEMPTS = int(f2boptions['max_attempts'])
  97. RETRY_WINDOW = int(f2boptions['retry_window'])
  98. NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
  99. NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
  100. WHITELIST = r.hgetall('F2B_WHITELIST')
  101. ip = ipaddress.ip_address(address.decode('ascii'))
  102. if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
  103. ip = ip.ipv4_mapped
  104. address = str(ip)
  105. if ip.is_private or ip.is_loopback:
  106. return
  107. self_network = ipaddress.ip_network(address.decode('ascii'))
  108. if WHITELIST:
  109. for wl_key in WHITELIST:
  110. wl_net = ipaddress.ip_network(wl_key.decode('ascii'), False)
  111. if wl_net.overlaps(self_network):
  112. log['time'] = int(round(time.time()))
  113. log['priority'] = 'info'
  114. log['message'] = 'Address %s is whitelisted by rule %s' % (self_network, wl_net)
  115. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  116. print 'Address %s is whitelisted by rule %s' % (self_network, wl_net)
  117. return
  118. net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)).decode('ascii'), strict=False)
  119. net = str(net)
  120. if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
  121. bans[net] = { 'attempts': 0 }
  122. active_window = RETRY_WINDOW
  123. else:
  124. active_window = time.time() - bans[net]['last_attempt']
  125. bans[net]['attempts'] += 1
  126. bans[net]['last_attempt'] = time.time()
  127. active_window = time.time() - bans[net]['last_attempt']
  128. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  129. log['time'] = int(round(time.time()))
  130. log['priority'] = 'crit'
  131. log['message'] = 'Banning %s' % net
  132. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  133. print 'Banning %s for %d minutes' % (net, BAN_TIME / 60)
  134. if type(ip) is ipaddress.IPv4Address:
  135. with lock:
  136. chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
  137. rule = iptc.Rule()
  138. rule.src = net
  139. target = iptc.Target(rule, "REJECT")
  140. rule.target = target
  141. if rule not in chain.rules:
  142. chain.insert_rule(rule)
  143. else:
  144. with lock:
  145. chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
  146. rule = iptc.Rule6()
  147. rule.src = net
  148. target = iptc.Target(rule, "REJECT")
  149. rule.target = target
  150. if rule not in chain.rules:
  151. chain.insert_rule(rule)
  152. r.hset('F2B_ACTIVE_BANS', '%s' % net, log['time'] + BAN_TIME)
  153. else:
  154. log['time'] = int(round(time.time()))
  155. log['priority'] = 'warn'
  156. log['message'] = '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
  157. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  158. print '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
  159. def unban(net):
  160. global lock
  161. log['time'] = int(round(time.time()))
  162. log['priority'] = 'info'
  163. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  164. if not net in bans:
  165. log['message'] = '%s is not banned, skipping unban and deleting from queue (if any)' % net
  166. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  167. print '%s is not banned, skipping unban and deleting from queue (if any)' % net
  168. r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
  169. return
  170. log['message'] = 'Unbanning %s' % net
  171. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  172. print 'Unbanning %s' % net
  173. if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network:
  174. with lock:
  175. chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
  176. rule = iptc.Rule()
  177. rule.src = net
  178. target = iptc.Target(rule, "REJECT")
  179. rule.target = target
  180. if rule in chain.rules:
  181. chain.delete_rule(rule)
  182. else:
  183. with lock:
  184. chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
  185. rule = iptc.Rule6()
  186. rule.src = net
  187. target = iptc.Target(rule, "REJECT")
  188. rule.target = target
  189. if rule in chain.rules:
  190. chain.delete_rule(rule)
  191. r.hdel('F2B_ACTIVE_BANS', '%s' % net)
  192. r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
  193. if net in bans:
  194. del bans[net]
  195. def quit(signum, frame):
  196. global quit_now
  197. quit_now = True
  198. def clear():
  199. global lock
  200. log['time'] = int(round(time.time()))
  201. log['priority'] = 'info'
  202. log['message'] = 'Clearing all bans'
  203. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  204. print 'Clearing all bans'
  205. for net in bans.copy():
  206. unban(net)
  207. with lock:
  208. filter4_table = iptc.Table(iptc.Table.FILTER)
  209. filter6_table = iptc.Table6(iptc.Table6.FILTER)
  210. for filter_table in [filter4_table, filter6_table]:
  211. filter_table.autocommit = False
  212. forward_chain = iptc.Chain(filter_table, "FORWARD")
  213. input_chain = iptc.Chain(filter_table, "INPUT")
  214. mailcow_chain = iptc.Chain(filter_table, "MAILCOW")
  215. if mailcow_chain in filter_table.chains:
  216. for rule in mailcow_chain.rules:
  217. mailcow_chain.delete_rule(rule)
  218. for rule in forward_chain.rules:
  219. if rule.target.name == 'MAILCOW':
  220. forward_chain.delete_rule(rule)
  221. for rule in input_chain.rules:
  222. if rule.target.name == 'MAILCOW':
  223. input_chain.delete_rule(rule)
  224. filter_table.delete_chain("MAILCOW")
  225. filter_table.commit()
  226. filter_table.refresh()
  227. filter_table.autocommit = True
  228. r.delete('F2B_ACTIVE_BANS')
  229. r.delete('F2B_PERM_BANS')
  230. pubsub.unsubscribe()
  231. def watch():
  232. log['time'] = int(round(time.time()))
  233. log['priority'] = 'info'
  234. log['message'] = 'Watching Redis channel F2B_CHANNEL'
  235. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  236. pubsub.subscribe('F2B_CHANNEL')
  237. print 'Subscribing to Redis channel F2B_CHANNEL'
  238. while not quit_now:
  239. for item in pubsub.listen():
  240. for rule_id, rule_regex in RULES.iteritems():
  241. if item['data'] and item['type'] == 'message':
  242. result = re.search(rule_regex, item['data'])
  243. if result:
  244. addr = result.group(1)
  245. ip = ipaddress.ip_address(addr.decode('ascii'))
  246. if ip.is_private or ip.is_loopback:
  247. continue
  248. print '%s matched rule id %d' % (addr, rule_id)
  249. log['time'] = int(round(time.time()))
  250. log['priority'] = 'warn'
  251. log['message'] = '%s matched rule id %d' % (addr, rule_id)
  252. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  253. ban(addr)
  254. def snat4(snat_target):
  255. global lock
  256. global quit_now
  257. def get_snat4_rule():
  258. rule = iptc.Rule()
  259. rule.src = os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24'
  260. rule.dst = '!' + rule.src
  261. target = rule.create_target("SNAT")
  262. target.to_source = snat_target
  263. return rule
  264. while not quit_now:
  265. time.sleep(10)
  266. with lock:
  267. try:
  268. table = iptc.Table('nat')
  269. table.refresh()
  270. chain = iptc.Chain(table, 'POSTROUTING')
  271. table.autocommit = False
  272. if get_snat4_rule() not in chain.rules:
  273. log['time'] = int(round(time.time()))
  274. log['priority'] = 'info'
  275. log['message'] = 'Added POSTROUTING rule for source network ' + get_snat4_rule().src + ' to SNAT target ' + snat_target
  276. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  277. print log['message']
  278. chain.insert_rule(get_snat4_rule())
  279. table.commit()
  280. else:
  281. for position, item in enumerate(chain.rules):
  282. if item == get_snat4_rule():
  283. if position != 0:
  284. chain.delete_rule(get_snat4_rule())
  285. table.commit()
  286. table.autocommit = True
  287. except:
  288. print 'Error running SNAT4, retrying...'
  289. def snat6(snat_target):
  290. global lock
  291. global quit_now
  292. def get_snat6_rule():
  293. rule = iptc.Rule6()
  294. rule.src = os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64')
  295. rule.dst = '!' + rule.src
  296. target = rule.create_target("SNAT")
  297. target.to_source = snat_target
  298. return rule
  299. while not quit_now:
  300. time.sleep(10)
  301. with lock:
  302. try:
  303. table = iptc.Table6('nat')
  304. table.refresh()
  305. chain = iptc.Chain(table, 'POSTROUTING')
  306. table.autocommit = False
  307. if get_snat6_rule() not in chain.rules:
  308. log['time'] = int(round(time.time()))
  309. log['priority'] = 'info'
  310. log['message'] = 'Added POSTROUTING rule for source network ' + get_snat6_rule().src + ' to SNAT target ' + snat_target
  311. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  312. print log['message']
  313. chain.insert_rule(get_snat6_rule())
  314. table.commit()
  315. else:
  316. for position, item in enumerate(chain.rules):
  317. if item == get_snat6_rule():
  318. if position != 0:
  319. chain.delete_rule(get_snat6_rule())
  320. table.commit()
  321. table.autocommit = True
  322. except:
  323. print 'Error running SNAT6, retrying...'
  324. def autopurge():
  325. while not quit_now:
  326. time.sleep(10)
  327. refreshF2boptions()
  328. BAN_TIME = f2boptions['ban_time']
  329. MAX_ATTEMPTS = f2boptions['max_attempts']
  330. QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
  331. if QUEUE_UNBAN:
  332. for net in QUEUE_UNBAN:
  333. unban(str(net))
  334. for net in bans.copy():
  335. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  336. if time.time() - bans[net]['last_attempt'] > BAN_TIME:
  337. unban(net)
  338. def initChain():
  339. # Is called before threads start, no locking
  340. print "Initializing mailcow netfilter chain"
  341. # IPv4
  342. if not iptc.Chain(iptc.Table(iptc.Table.FILTER), "MAILCOW") in iptc.Table(iptc.Table.FILTER).chains:
  343. iptc.Table(iptc.Table.FILTER).create_chain("MAILCOW")
  344. for c in ['FORWARD', 'INPUT']:
  345. chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c)
  346. rule = iptc.Rule()
  347. rule.src = '0.0.0.0/0'
  348. rule.dst = '0.0.0.0/0'
  349. target = iptc.Target(rule, "MAILCOW")
  350. rule.target = target
  351. if rule not in chain.rules:
  352. chain.insert_rule(rule)
  353. # IPv6
  354. if not iptc.Chain(iptc.Table6(iptc.Table6.FILTER), "MAILCOW") in iptc.Table6(iptc.Table6.FILTER).chains:
  355. iptc.Table6(iptc.Table6.FILTER).create_chain("MAILCOW")
  356. for c in ['FORWARD', 'INPUT']:
  357. chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c)
  358. rule = iptc.Rule6()
  359. rule.src = '::/0'
  360. rule.dst = '::/0'
  361. target = iptc.Target(rule, "MAILCOW")
  362. rule.target = target
  363. if rule not in chain.rules:
  364. chain.insert_rule(rule)
  365. # Apply blacklist
  366. BLACKLIST = r.hgetall('F2B_BLACKLIST')
  367. if BLACKLIST:
  368. for bl_key in BLACKLIST:
  369. if type(ipaddress.ip_network(bl_key.decode('ascii'), strict=False)) is ipaddress.IPv4Network:
  370. chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
  371. rule = iptc.Rule()
  372. rule.src = bl_key
  373. target = iptc.Target(rule, "REJECT")
  374. rule.target = target
  375. if rule not in chain.rules:
  376. log['time'] = int(round(time.time()))
  377. log['priority'] = 'crit'
  378. log['message'] = 'Blacklisting host/network %s' % bl_key
  379. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  380. print log['message']
  381. chain.insert_rule(rule)
  382. r.hset('F2B_PERM_BANS', '%s' % bl_key, int(round(time.time())))
  383. else:
  384. chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
  385. rule = iptc.Rule6()
  386. rule.src = bl_key
  387. target = iptc.Target(rule, "REJECT")
  388. rule.target = target
  389. if rule not in chain.rules:
  390. log['time'] = int(round(time.time()))
  391. log['priority'] = 'crit'
  392. log['message'] = 'Blacklisting host/network %s' % bl_key
  393. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  394. print log['message']
  395. chain.insert_rule(rule)
  396. r.hset('F2B_PERM_BANS', '%s' % bl_key, int(round(time.time())))
  397. if __name__ == '__main__':
  398. # In case a previous session was killed without cleanup
  399. clear()
  400. # Reinit MAILCOW chain
  401. initChain()
  402. watch_thread = Thread(target=watch)
  403. watch_thread.daemon = True
  404. watch_thread.start()
  405. if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') is not 'n':
  406. try:
  407. snat_ip = os.getenv('SNAT_TO_SOURCE').decode('ascii')
  408. snat_ipo = ipaddress.ip_address(snat_ip)
  409. if type(snat_ipo) is ipaddress.IPv4Address:
  410. snat4_thread = Thread(target=snat4,args=(snat_ip,))
  411. snat4_thread.daemon = True
  412. snat4_thread.start()
  413. except ValueError:
  414. print os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address'
  415. if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') is not 'n':
  416. try:
  417. snat_ip = os.getenv('SNAT6_TO_SOURCE').decode('ascii')
  418. snat_ipo = ipaddress.ip_address(snat_ip)
  419. if type(snat_ipo) is ipaddress.IPv6Address:
  420. snat6_thread = Thread(target=snat6,args=(snat_ip,))
  421. snat6_thread.daemon = True
  422. snat6_thread.start()
  423. except ValueError:
  424. print os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address'
  425. autopurge_thread = Thread(target=autopurge)
  426. autopurge_thread.daemon = True
  427. autopurge_thread.start()
  428. mailcowchainwatch_thread = Thread(target=mailcowChainOrder)
  429. mailcowchainwatch_thread.daemon = True
  430. mailcowchainwatch_thread.start()
  431. signal.signal(signal.SIGTERM, quit)
  432. atexit.register(clear)
  433. while not quit_now:
  434. time.sleep(0.5)