server.py 17 KB

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