[Netfilter] Rename fail2ban to netfilter, use iptables-python

master
andre.peters 2018-02-01 13:39:27 +01:00
parent 0773448b35
commit 38a819771b
3 changed files with 277 additions and 200 deletions

View File

@ -1,8 +0,0 @@
FROM python:2-alpine
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
RUN apk add -U --no-cache iptables ip6tables
RUN pip install redis ipaddress
COPY logwatch.py /
CMD ["python2", "-u", "/logwatch.py"]

View File

@ -0,0 +1,9 @@
FROM alpine:3.7
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
RUN apk add -U python2 python-dev py-pip gcc musl-dev iptables ip6tables \
&& pip2 install --upgrade python-iptables redis ipaddress \
&& apk del python-dev py2-pip gcc
COPY server.py /
CMD ["python2", "-u", "/server.py"]

View File

@ -1,192 +1,268 @@
#!/usr/bin/env python2 #!/usr/bin/env python2
import re import re
import os import os
import time import time
import atexit import atexit
import signal import signal
import ipaddress import ipaddress
import subprocess import subprocess
from threading import Thread from threading import Thread
import redis import redis
import time import time
import json import json
import iptc
yes_regex = re.compile(r'([yY][eE][sS]|[yY])+$')
if re.search(yes_regex, os.getenv('SKIP_FAIL2BAN', 0)): r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0)
print 'SKIP_FAIL2BAN=y, Skipping Fail2ban container...' pubsub = r.pubsub()
time.sleep(31536000)
raise SystemExit RULES = {}
RULES[1] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed'
r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0) RULES[2] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
pubsub = r.pubsub() RULES[3] = '-login: Aborted login \(no auth .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
RULES[4] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
RULES = {} RULES[5] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
RULES[1] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed' RULES[6] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
RULES[2] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
RULES[3] = '-login: Aborted login \(no auth .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' if not r.get('F2B_OPTIONS'):
RULES[4] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' f2options['ban_time'] = int(r.get('F2B_BAN_TIME')) or 1800
RULES[5] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked' f2options['max_attempts'] = int(r.get('F2B_MAX_ATTEMPTS')) or 10
RULES[6] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)' f2options['retry_window'] = int(r.get('F2B_RETRY_WINDOW')) or 600
f2options['netban_ipv4'] = int(r.get('F2B_NETBAN_IPV4')) or 24
r.setnx('F2B_BAN_TIME', '1800') f2options['netban_ipv6'] = int(r.get('F2B_NETBAN_IPV6')) or 64
r.setnx('F2B_MAX_ATTEMPTS', '10') r.set('F2B_OPTIONS', json.dumps(f2options, ensure_ascii=False))
r.setnx('F2B_RETRY_WINDOW', '600') else:
r.setnx('F2B_NETBAN_IPV6', '64') try:
r.setnx('F2B_NETBAN_IPV4', '24') f2options = json.loads(r.get('F2B_OPTIONS'))
except ValueError, e:
bans = {} print 'Error loading F2B options: F2B_OPTIONS is not json'
log = {} raise SystemExit(1)
quit_now = False
if r.exists('F2B_LOG'):
def ban(address): r.rename('F2B_LOG', 'NETFILTER_LOG')
BAN_TIME = int(r.get('F2B_BAN_TIME'))
MAX_ATTEMPTS = int(r.get('F2B_MAX_ATTEMPTS')) bans = {}
RETRY_WINDOW = int(r.get('F2B_RETRY_WINDOW')) log = {}
WHITELIST = r.hgetall('F2B_WHITELIST') quit_now = False
NETBAN_IPV6 = '/' + str(r.get('F2B_NETBAN_IPV6'))
NETBAN_IPV4 = '/' + str(r.get('F2B_NETBAN_IPV4')) def ban(address):
BAN_TIME = int(f2options['ban_time'])
ip = ipaddress.ip_address(address.decode('ascii')) MAX_ATTEMPTS = int(f2options['max_attempts'])
if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped: RETRY_WINDOW = int(f2options['retry_window'])
ip = ip.ipv4_mapped NETBAN_IPV4 = '/' + str(f2options['netban_ipv4'])
address = str(ip) NETBAN_IPV6 = '/' + str(f2options['netban_ipv6'])
if ip.is_private or ip.is_loopback: WHITELIST = r.hgetall('F2B_WHITELIST')
return
ip = ipaddress.ip_address(address.decode('ascii'))
self_network = ipaddress.ip_network(address.decode('ascii')) if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
if WHITELIST: ip = ip.ipv4_mapped
for wl_key in WHITELIST: address = str(ip)
wl_net = ipaddress.ip_network(wl_key.decode('ascii'), False) if ip.is_private or ip.is_loopback:
if wl_net.overlaps(self_network): return
log['time'] = int(round(time.time()))
log['priority'] = 'info' self_network = ipaddress.ip_network(address.decode('ascii'))
log['message'] = 'Address %s is whitelisted by rule %s' % (self_network, wl_net) if WHITELIST:
r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False)) for wl_key in WHITELIST:
print 'Address %s is whitelisted by rule %s' % (self_network, wl_net) wl_net = ipaddress.ip_network(wl_key.decode('ascii'), False)
return if wl_net.overlaps(self_network):
log['time'] = int(round(time.time()))
net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)).decode('ascii'), strict=False) log['priority'] = 'info'
net = str(net) log['message'] = 'Address %s is whitelisted by rule %s' % (self_network, wl_net)
r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW: print 'Address %s is whitelisted by rule %s' % (self_network, wl_net)
bans[net] = { 'attempts': 0 } return
active_window = RETRY_WINDOW
else: net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)).decode('ascii'), strict=False)
active_window = time.time() - bans[net]['last_attempt'] net = str(net)
bans[net]['attempts'] += 1 if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
bans[net]['last_attempt'] = time.time() bans[net] = { 'attempts': 0 }
active_window = RETRY_WINDOW
active_window = time.time() - bans[net]['last_attempt'] else:
active_window = time.time() - bans[net]['last_attempt']
if bans[net]['attempts'] >= MAX_ATTEMPTS:
log['time'] = int(round(time.time())) bans[net]['attempts'] += 1
log['priority'] = 'crit' bans[net]['last_attempt'] = time.time()
log['message'] = 'Banning %s' % net
r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False)) active_window = time.time() - bans[net]['last_attempt']
print 'Banning %s for %d minutes' % (net, BAN_TIME / 60)
if type(ip) is ipaddress.IPv4Address: if bans[net]['attempts'] >= MAX_ATTEMPTS:
subprocess.call(['iptables', '-I', 'INPUT', '-s', net, '-j', 'REJECT']) log['time'] = int(round(time.time()))
subprocess.call(['iptables', '-I', 'FORWARD', '-s', net, '-j', 'REJECT']) log['priority'] = 'crit'
else: log['message'] = 'Banning %s' % net
subprocess.call(['ip6tables', '-I', 'INPUT', '-s', net, '-j', 'REJECT']) r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
subprocess.call(['ip6tables', '-I', 'FORWARD', '-s', net, '-j', 'REJECT']) print 'Banning %s for %d minutes' % (net, BAN_TIME / 60)
r.hset('F2B_ACTIVE_BANS', '%s' % net, log['time'] + BAN_TIME) if type(ip) is ipaddress.IPv4Address:
else: for c in ['INPUT', 'FORWARD']:
log['time'] = int(round(time.time())) chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c)
log['priority'] = 'warn' rule = iptc.Rule()
log['message'] = '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net) rule.src = net
r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False)) target = iptc.Target(rule, "REJECT")
print '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net) rule.target = target
if rule not in chain.rules:
def unban(net): chain.insert_rule(rule)
log['time'] = int(round(time.time())) else:
log['priority'] = 'info' for c in ['INPUT', 'FORWARD']:
r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False)) chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c)
if not net in bans: rule = iptc.Rule6()
log['message'] = '%s is not banned, skipping unban and deleting from queue (if any)' % net rule.src = net
r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False)) target = iptc.Target(rule, "REJECT")
print '%s is not banned, skipping unban and deleting from queue (if any)' % net rule.target = target
r.hdel('F2B_QUEUE_UNBAN', '%s' % net) if rule not in chain.rules:
return chain.insert_rule(rule)
log['message'] = 'Unbanning %s' % net r.hset('F2B_ACTIVE_BANS', '%s' % net, log['time'] + BAN_TIME)
r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False)) else:
print 'Unbanning %s' % net log['time'] = int(round(time.time()))
if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network: log['priority'] = 'warn'
subprocess.call(['iptables', '-D', 'INPUT', '-s', net, '-j', 'REJECT']) log['message'] = '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
subprocess.call(['iptables', '-D', 'FORWARD', '-s', net, '-j', 'REJECT']) r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
else: print '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
subprocess.call(['ip6tables', '-D', 'INPUT', '-s', net, '-j', 'REJECT'])
subprocess.call(['ip6tables', '-D', 'FORWARD', '-s', net, '-j', 'REJECT']) def unban(net):
r.hdel('F2B_ACTIVE_BANS', '%s' % net) log['time'] = int(round(time.time()))
r.hdel('F2B_QUEUE_UNBAN', '%s' % net) log['priority'] = 'info'
del bans[net] r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
#if not net in bans:
def quit(signum, frame): # log['message'] = '%s is not banned, skipping unban and deleting from queue (if any)' % net
global quit_now # r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
quit_now = True # print '%s is not banned, skipping unban and deleting from queue (if any)' % net
# r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
def clear(): # return
log['time'] = int(round(time.time())) log['message'] = 'Unbanning %s' % net
log['priority'] = 'info' r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
log['message'] = 'Clearing all bans' print 'Unbanning %s' % net
r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False)) if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network:
print 'Clearing all bans' for c in ['INPUT', 'FORWARD']:
for net in bans.copy(): chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c)
unban(net) rule = iptc.Rule()
pubsub.unsubscribe() rule.src = net
target = iptc.Target(rule, "REJECT")
def watch(): rule.target = target
log['time'] = int(round(time.time())) if rule in chain.rules:
log['priority'] = 'info' chain.delete_rule(rule)
log['message'] = 'Watching Redis channel F2B_CHANNEL' else:
r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False)) for c in ['INPUT', 'FORWARD']:
pubsub.subscribe('F2B_CHANNEL') chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c)
print 'Subscribing to Redis channel F2B_CHANNEL' rule = iptc.Rule6()
while True: rule.src = net
for item in pubsub.listen(): target = iptc.Target(rule, "REJECT")
for rule_id, rule_regex in RULES.iteritems(): rule.target = target
if item['data'] and item['type'] == 'message': if rule in chain.rules:
result = re.search(rule_regex, item['data']) chain.delete_rule(rule)
if result: r.hdel('F2B_ACTIVE_BANS', '%s' % net)
addr = result.group(1) r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
ip = ipaddress.ip_address(addr.decode('ascii')) if net in bans:
if ip.is_private or ip.is_loopback: del bans[net]
continue
print '%s matched rule id %d' % (addr, rule_id) def quit(signum, frame):
log['time'] = int(round(time.time())) global quit_now
log['priority'] = 'warn' quit_now = True
log['message'] = '%s matched rule id %d' % (addr, rule_id)
r.lpush('F2B_LOG', json.dumps(log, ensure_ascii=False)) def clear():
ban(addr) log['time'] = int(round(time.time()))
log['priority'] = 'info'
def autopurge(): log['message'] = 'Clearing all bans'
while not quit_now: r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
BAN_TIME = int(r.get('F2B_BAN_TIME')) print 'Clearing all bans'
MAX_ATTEMPTS = int(r.get('F2B_MAX_ATTEMPTS')) for net in bans.copy():
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN') unban(net)
if QUEUE_UNBAN: pubsub.unsubscribe()
for net in QUEUE_UNBAN:
unban(str(net)) def watch():
for net in bans.copy(): log['time'] = int(round(time.time()))
if bans[net]['attempts'] >= MAX_ATTEMPTS: log['priority'] = 'info'
if time.time() - bans[net]['last_attempt'] > BAN_TIME: log['message'] = 'Watching Redis channel F2B_CHANNEL'
unban(net) r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
time.sleep(10) pubsub.subscribe('F2B_CHANNEL')
print 'Subscribing to Redis channel F2B_CHANNEL'
if __name__ == '__main__': while True:
for item in pubsub.listen():
watch_thread = Thread(target=watch) for rule_id, rule_regex in RULES.iteritems():
watch_thread.daemon = True if item['data'] and item['type'] == 'message':
watch_thread.start() result = re.search(rule_regex, item['data'])
if result:
autopurge_thread = Thread(target=autopurge) addr = result.group(1)
autopurge_thread.daemon = True ip = ipaddress.ip_address(addr.decode('ascii'))
autopurge_thread.start() if ip.is_private or ip.is_loopback:
continue
signal.signal(signal.SIGTERM, quit) print '%s matched rule id %d' % (addr, rule_id)
atexit.register(clear) log['time'] = int(round(time.time()))
log['priority'] = 'warn'
while not quit_now: log['message'] = '%s matched rule id %d' % (addr, rule_id)
time.sleep(0.5) r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
ban(addr)
def snat(snat_target):
def get_snat_rule():
rule = iptc.Rule()
rule.position = 1
rule.src = os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24'
rule.dst = '!' + rule.src
target = rule.create_target("SNAT")
target.to_source = snat_target
return rule
while True:
table = iptc.Table('nat')
table.autocommit = False
chain = iptc.Chain(table, 'POSTROUTING')
if get_snat_rule() not in chain.rules:
log['time'] = int(round(time.time()))
log['priority'] = 'info'
log['message'] = 'Added POSTROUTING rule for source network ' + get_snat_rule().src + ' to SNAT target ' + snat_target
r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
print log['message']
chain.insert_rule(get_snat_rule())
table.commit()
table.refresh()
time.sleep(10)
def autopurge():
while not quit_now:
BAN_TIME = int(r.get('F2B_BAN_TIME'))
MAX_ATTEMPTS = int(r.get('F2B_MAX_ATTEMPTS'))
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
if QUEUE_UNBAN:
for net in QUEUE_UNBAN:
unban(str(net))
for net in bans.copy():
if bans[net]['attempts'] >= MAX_ATTEMPTS:
if time.time() - bans[net]['last_attempt'] > BAN_TIME:
unban(net)
time.sleep(10)
def cleanPrevious():
print "Cleaning previously cached bans"
F2B_ACTIVE_BANS = r.hgetall('F2B_ACTIVE_BANS')
if F2B_ACTIVE_BANS:
for net in F2B_ACTIVE_BANS:
unban(str(net))
if __name__ == '__main__':
cleanPrevious()
watch_thread = Thread(target=watch)
watch_thread.daemon = True
watch_thread.start()
if os.getenv('SNAT_TO_SOURCE'):
try:
snat_ip = os.getenv('SNAT_TO_SOURCE').decode('ascii')
snat_ipo = ipaddress.ip_address(snat_ip)
if type(snat_ipo) is ipaddress.IPv4Address:
snat_thread = Thread(target=snat,args=(snat_ip,))
snat_thread.daemon = True
snat_thread.start()
except ValueError:
print os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address'
autopurge_thread = Thread(target=autopurge)
autopurge_thread.daemon = True
autopurge_thread.start()
signal.signal(signal.SIGTERM, quit)
atexit.register(clear)
while not quit_now:
time.sleep(0.5)