#!/usr/bin/python

quit() 
"""
Handle DDOS by barring bad IPs temporarily
"""

# TODO: accept IP's and networks from command line/stdin and blacklist them for a while
# TODO: IPC for re-read blacklist

try:
    import GeoIP
except ImportError:
    GeoIP=None
import fcntl
import os
import optparse
import time
import re
import sys
import struct
import socket
sys.path.insert(0, '/usr/local/afribin/maintenance')
try: import config
except ImportError: config=False
try: import frob # except it's frob, not frob.py
except ImportError: frob=False


# Compatibility function: bin()
try:
    bin(0)
except NameError:
    def bin(x, width=32):
        return ''.join(str((x>>i)&1) for i in xrange(width-1,-1,-1))

if GeoIP:
    gi = GeoIP.new(GeoIP.GEOIP_STANDARD)

sucuri = (
    # Sucuri fancy proxy that does actually cache very well:
	'192.124.249.0/24', '192.88.134.0/24', '192.88.135.0/24', '185.93.228.0/24',
	'185.93.229.0/24','185.93.230.0/24','192.161.0.0/24','192.81.128.0/22',
	'154.127.61.210','154.66.197.69','197.85.187.18','197.85.186.185','154.127.61.210',
	'66.248.200.0/22','185.93.228.0/22','192.88.134.0/23','207.46.0.0/16','197.242.144.0/21','41.76.208.0/21',
   '197.242.152.0/21','41.76.212.0/21','154.0.0.0/16'
	)

cloudflare = (
    # cloudflare and mail.faxfx.biz gets a free pass:
    '199.27.128.0/21', '173.245.48.0/20',  '103.21.244.0/22',  '103.22.200.0/22',
    '103.31.4.0/22',   '141.101.64.0/18',  '108.162.192.0/18', '190.93.240.0/20',
    '188.114.96.0/20', '197.234.240.0/22', '198.41.128.0/17',  '162.158.0.0/15',
    '104.16.0.0/12','196.25.125.70','131.0.72.0/22','172.64.0.0/13'
    )


# Networks we block when anything goes wrong
networkblocklist = {
    # http://user-agent-string.info/list-of-ua/bot-detail?bot=MSNBot
 #   '65.52.0.0/14': 1,  # bingbot
 #   '207.46.0.0/16':1,  # bingbot
 #   '157.56.0.0/14':1,
 #   '157.54.0.0/15':1,
 #   '157.60.0.0/16':1,  # bingbot
 #   '131.253.24.0/21':1,
 #   '131.253.21.0/24':1,
 #   '131.253.32.0/20':1,
 #   '131.253.22.0/23':1, # msnbot
 #   '199.30.16.0/20':1,  # MSNbot
#    '10.0.0.0/8':1,  # MSNbot
   
    }



def ip2int(addr):                                                               
    return struct.unpack("!I", socket.inet_aton(addr))[0]                       

def int2ip(addr):                                                               
    return socket.inet_ntoa(struct.pack("!I", addr))       


class iplist:
    def __init__(self,list):
        self.networks=[]  # binary networks
        self.hosts={}   # binary hosts
        for addr in list:
            self.append(addr)

    def __contains__(self,ip):
        'Contains an ip, or beginning and end of a subnet -- string based'
        if not ip: return False
        ip,netmask=self.subnet2int(ip)
        if self.contains(ip):
            if netmask==0xFFFFFFFF: return True
            if self.contains(ip+(0xFFFFFFFF-netmask-1)):
                return True
        return False

    def contains(self,ip):
        if self.hosts.has_key(ip):
            return True
        for network,netmask in self.networks:
            if (network & netmask) == (ip & netmask): return True
        return False

    def __str__(self):
        o=[]
        for subnet in self:
            o.append(subnet)
        return '' + (' '.join(o)) + ''

    def __iter__(self):
       for host in self.hosts.keys():
            yield int2ip(host)
       for subnet in self.networks:
            yield self.ipnetmask2str(subnet)
    
    def ipnetmask2str(self,subnet):
        ip,netmask=subnet
        ip=int2ip(ip)
        bits=bin(netmask).count("1")
        if bits==32: return ip
        return ip+'/'+str(bits)

    def extend(self,addresses):
        for addr in addresses:
            self.append(addr);

    def subnet2int(self,subnet):
        if subnet.count('.')==1 and subnet.endswith('.'): subnet+='0.0.0/8'
        if subnet.count('.')==2 and subnet.endswith('.'): subnet+='0.0/16'
        if subnet.count('.')==3 and subnet.endswith('.'): subnet+='0/24'
        if subnet.find('/')<0:
            network =ip2int(subnet)
            return network,4294967295
        network,netmask=subnet.split('/',1)
        if netmask.find('.')<0:
            netmask=0xFFFFFFFF ^ 2**(32-int(netmask))-1
        else:
            netmask=ip2int(netmask)
        network = ip2int(network)
        return network, netmask

    def append(self,addr):
        ip,netmask = self.subnet2int(addr)
        if self.contains(ip):
            if netmask==0xFFFFFFFF: return
            if self.contains(ip+(0xFFFFFFFF-netmask-1)):
                return
        if netmask==4294967295:
            self.hosts[ip]=1
        else:
            self.networks.append( (ip, netmask) );

def have_ipset():
    rc = os.system('ipset -! add ddos_ips_min 100.127.255.254')  # because who would ever use that?
    if rc==0:
        os.system('ipset -! add ddos_ips_min 100.127.255.254')  
    return rc==0

def main(options,args):
    global gi
    global networkblocklist, cloudflare, securi

    if len(options.exclude_ports)==0:
        options.exclude_ports=[3306,]
    
    idle_seconds = options.idle * 60

    if options.no_robots_blacklist or options.load==0:
        networkblocklist={}
    if options.antibruteforce:
        nextantibruteforce=time.time()
    else:
        nextantibruteforce=0;

    if options.ipset or have_ipset():
        block_template = "ipset -! add ddos_ips_min %s # %d connections %s;\n" 
        block_net_template = "ipset -! add ddos_nets %s # %d connections %s;\n" 
        unblock_template = ''
    else:
        block_template = "ip route add prohibit %s # %d connections %s;\n"
        block_net_template = "ip route add prohibit %s # %d connections %s;\n"
        unblock_template = 'ip route del %s'

    # Delete prohibitions
    fd = os.popen("ip route ls",'r')
    ip_route_ls=[]
    for line in fd:
        ip_route_ls.append(line)
        if line.find("prohibit") == -1: continue
        cmd = "ip route del %s" % line.strip()
        print cmd
        if not options.no_act:
            os.system(cmd)
        lastact=time.time()
    fd.close()

    # Build the whitelist:
    whitelist=iplist(['127.0.0.0/8', '100.64.0.0/10', '10.0.0.0/8', '192.168.0.0/16','169.254.0.0/16','172.16.0.0/12', '196.30.224.202', '127.2.1.1' ,'197.242.144.0/21','41.76.208.0/21', '197.242.152.0/21' , '41.76.212.0/21' , '154.0.0.0/16','102.182.0.0/16','165.73.0.0/16' ]);
    geoipwhitelist=[]
    for arg in args:
        if GeoIP and len(arg)==2:
            geoipwhitelist.append(arg)
        else:
            whitelist.append(args)
    whitelist_config = True
    skip_bruteforce = False
    skip_ddos = False
    if config:
        whitelist_config = config.option('whitelist')
        skip_bruteforce = config.option('skip_bruteforce')
        skip_ddos = config.option('skip_ddos')
    if whitelist_config:
        for ipaddr in re.split(r'[\s,;]+',whitelist_config):
            whitelist.append(ipaddr)
    whitelist.append('41.76.208.209')
    fd=open("/etc/resolv.conf",'r')
    ns= re.compile("nameserver\s+([0-9.]+)")
    for line in fd:
        m= ns.search(line)
        if m: whitelist.append(m.group(1))
    
    addr = re.compile("inet addr:([0-9.]+)")
    fd=os.popen("/sbin/ifconfig",'r')
    for line in fd:
        m= addr.search(line)
        if m: whitelist.append(m.group(1))
    fd.close()

    #immediate neighbours are cool
    addr = re.compile("^([0-9.]{7,16}/\d{1,2})")
    for line in ip_route_ls:
        m= addr.search(line)
        if m: whitelist.append(m.group(1))

    # cloudflare is fairly cool
    whitelist.extend(cloudflare)

    # Likewise for Sucuri
    whitelist.extend(sucuri)

    SSH_CLIENT=os.environ.get("SSH_CLIENT")
    if SSH_CLIENT:
        bits=SSH_CLIENT.split()
        if len(bits) and bits[0].count(".")==3:
            whitelist.append(bits[0])

    #################################################################################################
    # So
    unblockqueue=[]
    unblockqueue_ip=[]
    queuelen = int((options.time+options.sleep-1)/options.sleep)
    if skip_ddos:
        print "skip_ddos is defined on this system"
        return

    print "# %s %s" % (time.strftime("%Y-%m-%d %H:%M:%S"),sys.argv[0])
    print ("# When load above -l %d,\n"
        "# checking every -s %d and\n"
        "# blocking for -t %d seconds, when there are over\n"
        "# -c %d connections per IP or\n"
        "# -C %d connections per /24") % (
        options.load,
        options.sleep, 
        options.sleep * queuelen, 
        options.connections,
        options.connections24)
    
    print "# Ports allowed: -p"," -p ".join(map(str,options.only_ports))
    print "# Ports excluded: -P"," -P ".join(map(str,options.exclude_ports))
    print "# Network whitelist:",str(whitelist)
    print "# Network blocklist:"," ".join(networkblocklist)

    allblockedips={}
    lastact=time.time()
    while True:
        load=open("/proc/loadavg",'r').read().split(' ',1)[0]
        ipcount={}
        netcount={}
        actions=[]
        unblockcmds=[]
        blocked_ips=[]
        # If our load is high enough ...
        maxcount=0
        maxip=''
        net_maxcount=0
        net_maxip=''
        # Do anti-brute force irrespective of load
        if not skip_bruteforce and nextantibruteforce and time.time() >= nextantibruteforce:
            nextantibruteforce=time.time() + options.antibruteforce * 60;
            if frob: frob.do_antibruteforce([], seconds=60);

        if float(load) <= options.load:
            if options.verbose > 1: print "Load of %s ok" % load
        else:
            # Add new prohibitions
            if options.testfile:
                fd=open(options.testfile,'r')
            else:
                cmd = "netstat -ntu"
                fd=os.popen("netstat -ntu","r")
            counter=0
            for line in fd:
                bits=line.split()
                if len(bits)<=5: continue
                ipandport=bits[4]
                if ipandport[:7]=="::ffff:": ipandport=ipandport[7:]
                n=ipandport.find(":")
                if len(options.only_ports):
                    lport=bits[3].rsplit(':')[-1]
                    if not (lport in options.only_ports):
                        continue # ignore port
                if len(options.exclude_ports):
                    lport=bits[3].rsplit(':')[-1]
                    if lport in options.exclude_ports:
                        continue # ignore port
                ip=ipandport[:n]
                if ip.count('.')!=3:
                    continue # not an ip, sorry
                net24 = ".".join(ip.split(".")[:3]) + ".0/24"
                ipcount[ip]=ipcount.get(ip,0)+1;
                netcount[net24]=netcount.get(net24,0)+1;
            fd.close()

            # Block all connections over the limit
            for isnet,thelist,connections,quiet in (
                (False,ipcount,options.connections,''),
                (True, netcount,options.connections24,''),
                (True, networkblocklist,0, '>& /dev/null'),):
                # print 'i,t,c:',isnet,thelist,connections
                for ip,count in thelist.items():
                    if not isnet and count>maxcount:
                        maxcount=count
                        maxip=ip
                    elif isnet and count>net_maxcount:
                        net_maxcount=count
                        net_maxip=ip
                    if ip in whitelist:
                        count=0  # whitelisted
                    if GeoIP and gi.country_code_by_addr(ip) in geoipwhitelist:
                        count=0  # whitelisted
                    if allblockedips.has_key(ip):
                        count=0 # already blocked
                        if isnet: continue
                    if count < connections:
                        continue
                    if isnet:
                        actions.append(block_net_template % (ip,count,quiet))
                    else:
                        actions.append(block_template % (ip,count,quiet))
                    if unblock_template:
                        unblockcmds.append(unblock_template % ip)
                    blocked_ips.append(ip)
                    allblockedips[ip]=True
                    if count and not quiet:  #
                        if frob: frob.send_syslog('ddos','null routing %s, %d connections' % (ip,count))
                pass
            pass

        unblockqueue.insert(0,unblockcmds)
        unblockqueue_ip.insert(0,blocked_ips)
        if len(unblockqueue) > queuelen:
            actions.extend(unblockqueue.pop())
            actions.reverse()
            ips = unblockqueue_ip.pop()
            for w in ips:
                if allblockedips.has_key(w): allblockedips.pop(w)
        if len(actions)>0:
            print "# %s" % time.strftime("%Y-%m-%d %H:%M:%S")
            print "".join(actions)
            if not options.no_act:
                os.system("".join(actions))
            lastact=time.time()
        # print maxcount, options.verbose, maxip
        if maxcount and options.verbose:
            print "Max: %d from %s, and %d from %s" % (maxcount,maxip, net_maxcount, net_maxip)

        if idle_seconds and time.time()-lastact > idle_seconds:
            print "# Exiting on idle of %d minutes" % options.idle
            break;

        time.sleep(options.sleep)

    pass
    lockme.close()


if __name__=="__main__":
    usage="Usage: %prog [<options>] [whitelist ips]\n"+__doc__+"\n"
    parser=optparse.OptionParser(usage)
    parser.add_option("-v","--verbose",dest="verbose", action="count", default=0, help="Be verbose")
    parser.add_option("-n","--no-act",dest="no_act", action="count", default=0, help="Don't run commands")
    parser.add_option("-s","--sleep",dest="sleep", action="store", type='int', default=30, help="Sleep interval between checks (30 seconds)")
    parser.add_option("-c","--connections",dest="connections", action="store", type='int', default=50, help="Number of connections to block at (50 connections)")
    parser.add_option("-C","--connections24",dest="connections24", action="store", type='int', default=200, help="Number of connections to block /24 (200 connections)")
    parser.add_option("-t","--time",dest="time", action="store", type='int', default=600, help="Number of seconds to retain a block (600 seconds)")
    parser.add_option("-l","--load",dest="load", action="store", type='int', default=6, help="Only block when over this load (6)")
    parser.add_option("-p","--port",dest="only_ports", action="append", default=[], help="Only consider this port in block list")
    parser.add_option("-P","--exclude-port",dest="exclude_ports", action="append", default=[], help="Exclude ports in this list")
    parser.add_option("-i","--idle",dest="idle", action="store", default=0, help="exit after this many minutes of idle time (no blocking)")
    parser.add_option("-R","--no-robots-blacklist",dest="no_robots_blacklist", action="store_true", default=False, help="Don't apply the robots blocklist on high load")
    parser.add_option("--test",dest="testfile", action="store", default='', help="Read from a test file")
    parser.add_option("-f","--antibruteforce",dest="antibruteforce", action="store", type='int', default=4, help="Run anti-brute-force every n minutes")
    parser.add_option("--ipset",dest="ipset", action="store_true", default=False, help="Use ipset blocks no matter what")
    (options,args) = parser.parse_args()

    lockme=open(sys.argv[0],'r')
    try:
        fcntl.flock (lockme, fcntl.LOCK_EX | fcntl.LOCK_NB)  # exclusive lock here so we don't nerf our neat data
    except IOError:
        sys.stderr.write("%s is already running on this machine\n" % sys.argv[0]);
        sys.exit(1);

    main(options,args)

