#!/usr/bin/python

# kdaemon.py: portknocking server
# This code is heavily based on the work of Marilen Corciovei
# Read the original at: http://len.is-a-geek.org/misc/portknock.html

import time, os, sys

# User-configurable
OUTLOG_FILE = '/var/log/knockd.log'          # log to write to
INLOG_FILE =  '/var/log/knock'               # log to read from
open_portseq = [4005, 40034, 9001, 5674]     # the sequence of your open knock
close_portseq = [4002, 45034, 9501]          # the sequence of your close knock
openport = 22                                # the port to open
timeout = 10                                 # timeout between first and last knock
iface = "eth1"                               # interface to open port on
iptables = "/sbin/iptables"                  # location of iptables
chain = "knock"                              # iptables chain to append to

class gatekeeper:
    def __init__(self, olog):
        self.ilog = open(INLOG_FILE, 'r')
        self.olog = olog
        self.restart = 0
        self.key = self.initKey()

    def __del__(self):
        self.ilog.close()

    def initKey(self):
        '''
        Initialize/reset the key

        Key structure:
        self.key[0][0-n]   Open tumbler: one zero for each port in open_portseq
                [1][0-n]   Close tumbler: one zero for each port in close_portseq
                [2]        IP address of remote connection
                [3][1:-n]  Chronological list of port knocks
                [4][0]     Time first knock received
                [4][1]     Time last knock received
        '''
        self.key = [[],[],None,[None],[None, None]]
        for i in open_portseq:
            self.key[0].append(0)
        for i in close_portseq:
            self.key[1].append(0)
        return self.key

    def tailLog(self):
        '''
        This is the main server loop
        '''
        watcher = os.stat(INLOG_FILE)
        this_modified = last_modified = watcher.st_mtime
        self.ilog.seek(0,2)

        while True:
            if self.restart == 1:
                time.sleep(5) # hack for clean reset
                break
            if this_modified > last_modified:
                last_modified = this_modified
                self.key = self.parseLog()
                self.key = self.checkKeys()

            watcher = os.stat(INLOG_FILE)
            this_modified = watcher.st_mtime
            time.sleep(1)

    def parseLog(self):
        '''
        Loop over lines in the log, if modified
        '''
        while True:
            line = self.ilog.readline().split(" ")
            if not line:
                break
            for f in line:
                if "DPT=" in f:
                    portnum = f[4:]
                if "SRC=" in f:
                    ip = f[4:]
                if len(f.split(":")) == 3:
                    logtime = f
            thetime = self.gettime(logtime)
            # print self.key # debug?
            if self.key[2] == None:
                self.key[2] = ip

            if int(portnum) in open_portseq:
                if self.key[2] == ip:
                    self.olog.write("%s Host %s sent open knock: %s\n" % \
                                   (time.asctime(time.localtime()), ip, portnum))
                    if not 1 in self.key[0]:                   # first knock received
                        self.key[4][0] = thetime               # record time
                    if not self.key[3][-1] == int(portnum):    # ignore duplicates
                        self.key[0][self.key[0].index(0)] = 1
                        self.key[3].append(int(portnum))
                        if not 0 in self.key[0]:               # last knock received
                            self.key[4][1] = thetime           # record time
                        return self.key

            if int(portnum) in close_portseq:
                if self.key[2] == ip:
                    self.olog.write("%s Host %s sent close knock: %s\n" % \
                                   (time.asctime(time.localtime()), ip, portnum))
                    if not 1 in self.key[1]:
                        self.key[4][0] = thetime
                    self.key[1][self.key[1].index(0)] = 1
                    if not 0 in self.key[1]:
                        self.key[4][1] = thetime
                    if not self.key[3][-1] == int(portnum):
                        self.key[3].append(int(portnum))
                        return self.key

    def checkKeys(self):
        '''
        The meat of this method will not be run unless all
        keys in either the close or open knock are recieved
        '''
        if 0 in self.key[0]:
            if 0 in self.key[1]:
                return self.key

        if not 0 in self.key[0]: # "open" key
            if int(self.key[4][1]) - int(self.key[4][0]) < timeout:  # check if timeout exceeded
                if self.key[3][1:] == open_portseq:                  # check if knocks in correct order
                    self.olog.write("%s All open knocks sent by host %s\n" % \
                                   (time.asctime(time.localtime()), self.key[2]))
                    self.open_port(self.key[2])
                    self.restart = 1
                    return
                else: # bail out, and reset key
                    self.olog.write("%s *** Open knocks sent in wrong order by host: %s\n" % \
                                   (time.asctime(time.localtime()), self.key[2]))
                    self.restart = 1
                    return
            else: # bail out and reset key
                self.olog.write("%s *** Timeout by host: %s ***\n" % \
                               (time.asctime(time.localtime()), self.key[2]))
                self.restart = 1
                return

        if not 0 in self.key[1]: # "close" key
            if int(self.key[4][1]) - int(self.key[4][0]) < timeout:
                if self.key[3][1:] == close_portseq:
                    self.olog.write("%s All close knocks sent by host %s\n" % \
                                   (time.asctime(time.localtime()), self.key[2]))
                    self.close_port()
                    self.restart = 1
                    return
                else:
                    self.olog.write("%s *** Close knocks sent in wrong order by host: %s\n" % \
                                   (time.asctime(time.localtime()), self.key[2]))
                    self.restart = 1
                    return
            else:
                self.olog.write("%s *** Timeout by host: %s\n" % \
                               (time.asctime(time.localtime()), self.key[2]))
                self.restart = 1
                return

    def open_port(self, ipfrom):
        command = "%s -A %s -i %s -p tcp -s %s --dport %i -j ACCEPT" % (iptables, chain, ipfrom, iface, openport)
        os.system(command)
        self.olog.write("%s *** Allowing host %s access to port %i\n" % \
                       (time.asctime(time.localtime()), ipfrom, openport))

    def close_port(self):
        command = "%s -A %s -i %s -p tcp -s %s --dport %i -j ACCEPT" % (iptables, chain, ipfrom, iface, openport)
        os.system(command)
        self.olog.write("%s *** Closing access to port %i\n" % \
                       (time.asctime(time.localtime()), openport))

    def gettime(self, t):
        t = t.split(":")              # time returned by log (hh:mm:ss)
        tt = time.gmtime(time.time()) # current date
        ttt = (tt[0], tt[1], tt[2], int(t[0]), int(t[1]), int(t[2]), tt[6], tt[7], tt[8]) # splice them
        thetime = time.mktime(ttt)    # seconds...
        return thetime


if __name__ == '__main__':
    olog = open(OUTLOG_FILE, 'a')
    olog.write("%s Starting knock.d\n" % \
               time.asctime(time.localtime()))
    try:
        while True:
            x = gatekeeper(olog)
            x.tailLog()
    except KeyboardInterrupt:
        olog.write("%s Caught ctrl-c, exiting.\n" % \
                   time.asctime(time.localtime()))
        olog.close()
        print"Done"
        sys.exit(0)


syntax highlighted by Code2HTML, v. 0.9.1