server.py 17 KB

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