/* efw_iptables.c  --  iptables implementation - updates Linux iptables
 *
 *  GPLv2 only - Copyright (C) 2008 - 2012
 *               David Sommerseth <dazo@users.sourceforge.net>
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU General Public License
 *  as published by the Free Software Foundation; version 2
 *  of the License.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 */

/**
 * @file   efw-iptables.c
 * @author David Sommerseth <dazo@users.sourceforge.net>
 * @date   2008-08-10
 *
 * @brief  Firewall driver for iptables.  Understands how to update iptables, in other words.
 *
 */


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/wait.h>

#define EUREPHIA_FWINTF         /**< Include the proper eurephiaFWINTF declaration in eurephiaCTX */
#include <eurephiafw_struct.h>
#include <eurephia_context.h>
#include <eurephia_nullsafe.h>
#include <eurephia_log.h>
#include <eurephiafw_helpers.h>

#define INTERFACEVER "1.1"      /**< The version of this firewall interface (driver) */
#define INTERFACEAPIVER 2       /**< Define the API level this firewall interface uses. */

/**
 *  eFWmode string translation table
 */
static const char *eFWmode_str[7] = {
        "ADD",
        "DELETE",
        "BLACKLIST",
        "FLUSH",
        "INITIALISE",
        "SHUTDOWN",
        NULL
};

/**
 * Mandatory function, contains driver information.
 *
 * @return Retuns a static string, containing the version information.
 */
const char *eFWinterfaceVersion() {
        return "eFW-iptables (v"INTERFACEVER")  David Sommerseth 2008 (C) GPLv2";
}


/**
 * Mandatory function, contains driver information.
 *
 * @return Retuns an integer which correponds to the API level this driver corresponds to.
 */
int eFWinterfaceAPIversion() {
        return INTERFACEAPIVER;
}


int process_input(eurephiaCTX *ctx, const char *fwcmd, const eFWupdateRequest *req);
int call_iptables(eurephiaCTX *ctx, const char *fwcmd, const char **ipt_args);


/**
 * The main routine of the firewall interface.  This loops until it gets a shutdown message.
 *
 * @param fwargs efw_threaddata pointer, with needed information to communicate with the openvpn process.
 */
void eFW_RunFirewall(void *fwargs) {
        efw_threaddata *cfg = (efw_threaddata *) fwargs;
        eurephiaCTX *ctx = (eurephiaCTX *) cfg->ctx;
        int quit = 0;
        unsigned int prio;
        eFWupdateRequest req;
        struct timespec tsp;

        DEBUG(ctx, 28, "eFW_RunFirewall:  Waiting for eFW master to get ready");
        sem_wait(cfg->semp_master);
        DEBUG(ctx, 28, "eFW_RunFirewall:  Telling eFW master that the worker process is ready");
        sem_post(cfg->semp_worker);

        if( cfg->fw_command == NULL ) {
                eurephia_log(ctx, LOG_FATAL, 0,
                             "eFW_RunFirewall: firewall_command is not configured.  "
                             "iptables will not be updated.");
                exit(3);
        }

        eurephia_log(ctx, LOG_INFO, 1, "efw_iptables: Firewall interface started");

        // Main loop ... grab messages of the messague queue until shutdown command is sent, or a failure happens
        while( quit == 0 ) {
                memset(&req, 0, sizeof(req));
                if( mq_receive(cfg->msgq, (char *)&req, EFW_MSG_SIZE, &prio) == -1 ) {
                        eurephia_log(ctx, LOG_FATAL, 0,
                                     "eFW_RunFirewall: Error while reading messages from queue: %s",
                                     strerror(errno));
                        exit(2);
                }
                if( req.mode != fwSHUTDOWN ) {
                        int res = 0;

                        DEBUG(ctx, 20, "eFW_RunFirewall:  Received %s", eFWmode_str[req.mode]);

                        res = process_input(ctx, cfg->fw_command, &req);
                        if( ! res ) {
                                quit = 1;
                                eurephia_log(ctx, LOG_FATAL, 0,
                                             "eFW_RunFirewall: Failed updating iptables");
                        }
                } else {
                        quit = 1;
                }
        }


        efwRemoveMessageQueue(ctx, fwargs);

        DEBUG(ctx, 28, "eFW_RunFirewall:  Telling eFW master that the worker process is about to shut down");
        sem_post(cfg->semp_worker);

        DEBUG(ctx, 28, "eFW_RunFirewall:  Waiting for eFW master to acknowledge");
        // Prepare a timeout
        if( clock_gettime(CLOCK_REALTIME, &tsp) == -1 ) {
                eurephia_log(ctx, LOG_FATAL, 0, "eFW_RunFirewall: Could not prepare timeout for shutdown ack: %s",
                             strerror(errno));
                sleep(10);
        } else {
                tsp.tv_sec += 30;  // Wait up to 30 seconds for shutdown ack.

                // Wait for acknowledge
                if( sem_timedwait(cfg->semp_master, &tsp) == -1 ) {
                        eurephia_log(ctx, LOG_PANIC, 0, "eFW_RunFirewall: Did not receive any shutdown ack: %s",
                                     strerror(errno));
                } else {
                        eurephia_log(ctx, LOG_INFO, 1, "efw_iptables: Firewall interface is shut down");
                }
        }
        efwRemoveSemaphores(ctx, fwargs);
        exit(0);
}


/**
 * Internal function.  Processes firewall update messages recieved via POSIX MQ.
 *
 * @param ctx    eurephiaCTX - This is just a shadow context, to make logging possible
 * @param fwcmd  The command to be executed, can be 'A'-add, 'D'-delete, 'F'-flush, 'B'-blacklist, 'I'-init
 * @param req    Pointer a eFWupdateRequest struct with the iptables update details
 *
 * @return Returns 1 on success, otherwise 0.  If 0 is sent, it means the firewall process should shut down,
 *         and it should only be used in very critical situations.
 */
int process_input(eurephiaCTX *ctx, const char *fwcmd, const eFWupdateRequest *req) {
        const char *iptables_args[] = { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
        int ret = 0, argc = 2;

        DEBUG(ctx, 36, "eFW_RunFirewall::process_input(ctx, %s, %s, %s, %s, %s)",
              eFWmode_str[req->mode], req->ipaddress, req->macaddress,
              req->rule_destination, req->goto_destination);

        //
        // Simple parsing of the input string
        //
        iptables_args[0] = (char *)fwcmd;

        switch( req->mode ) {
        case fwADD:
                iptables_args[1] = "-A";
        case fwDELETE:
                if( iptables_args[1] == NULL ) {
                        iptables_args[1] = "-D";
                }
                // Prepare iptables arguments
                iptables_args[argc++] = req->rule_destination;
                if( strlen_nullsafe(req->ipaddress) > 0 ) {
                        iptables_args[argc++] = "-s\0";
                        iptables_args[argc++] = req->ipaddress;
                }
                if( strlen_nullsafe(req->macaddress) > 0 ) {
                        iptables_args[argc++] = "-m\0";
                        iptables_args[argc++] = "mac\0";
                        iptables_args[argc++] = "--mac-source\0";
                        iptables_args[argc++] = req->macaddress;
                }
                iptables_args[argc++] = "-m\0";
                iptables_args[argc++] = "conntrack\0";
                iptables_args[argc++] = "--ctstate\0";
                iptables_args[argc++] = "NEW\0";
                iptables_args[argc++] = "-j\0";
                iptables_args[argc++] = req->goto_destination;
                iptables_args[argc++] = NULL;

                eurephia_log(ctx, LOG_INFO, 3, "eFW_RunFirewall - updating iptables rules "
                             "==> mode: %s ipaddr: %s  macaddr: '%s'  destchain: '%s'  jump: '%s'",
                             eFWmode_str[req->mode], req->ipaddress, req->macaddress,
                             req->rule_destination, req->goto_destination);
                ret = call_iptables(ctx, fwcmd, iptables_args);
                break;

        case fwBLACKLIST:
                iptables_args[1] = "-A\0";
                iptables_args[2] = req->rule_destination;
                iptables_args[3] = "-s\0";
                iptables_args[4] = req->ipaddress;
                iptables_args[5] = "-j\0";
                iptables_args[6] = strlen_nullsafe(req->goto_destination) > 0
                        ? req->goto_destination : "DROP\0";
                iptables_args[7] = NULL;

                eurephia_log(ctx, LOG_INFO, 3, "eFW_RunFirewall - updating iptables rules "
                             "==> mode: BLACKLIST  destchain: '%s'  IP address: %s  Send to: '%s'",
                             req->rule_destination, req->ipaddress, req->goto_destination);
                ret = call_iptables(ctx, fwcmd, iptables_args);
                break;

        case fwFLUSH:
                iptables_args[1] = "-F\0";
                iptables_args[2] = req->rule_destination;
                iptables_args[3] = NULL;
                eurephia_log(ctx, LOG_INFO, 3, "eFW_RunFirewall - updating iptables rules "
                             "==> mode: FLUSH  destchain: '%s'", req->rule_destination);
                ret = call_iptables(ctx, fwcmd, iptables_args);
                break;

        case fwINITIALISE:
                // Init chain - flush it and then add needed rule for stateful inspection
                eurephia_log(ctx, LOG_INFO, 3, "eFW_RunFirewall - Initialising iptables chain '%s'",
                             req->rule_destination);

                // Flush phase
                iptables_args[1] = "-F";
                iptables_args[2] = req->rule_destination;
                iptables_args[3] = NULL;
                ret = call_iptables(ctx, fwcmd, iptables_args);

                // Add stateful inspection
                iptables_args[1] = "-I\0";
                iptables_args[2] = req->rule_destination;
                iptables_args[3] = "-m\0";
                iptables_args[4] = "conntrack\0";
                iptables_args[5] = "--ctstate\0";
                iptables_args[6] = "ESTABLISHED,RELATED\0";
                iptables_args[7] = "-j\0";
                iptables_args[8] = "ACCEPT\0";
                ret &= call_iptables(ctx, fwcmd, iptables_args);
                break;

        default:
                eurephia_log(ctx, LOG_CRITICAL, 0, "eFW_RunFirewall::process_input:  Malformed update request");
                ret = 1;
        }
        return ret;
}


/**
 * This function does the actual iptables call.  It will fork out a process and do the
 * assigned iptables command.
 *
 * @param ctx      eurephiaCTX - shadow context, only with pointers to log files.
 * @param fwcmd    String containing full filename to the binary to execute
 * @param ipt_args The iptables arguments
 *
 * @return Returns 1 on success, otherwise 0.  When 0 is returned, the complete firewall process will be 
 *         shut down.
 */
int call_iptables(eurephiaCTX *ctx, const char *fwcmd, const char **ipt_args) {
        pid_t pid;
        int cmdret = -1;

        // Fork out a child process which will run the iptables command.  Since the execve replaces 
        // the current process, we need to do the forking first.
        if( (pid = fork()) < 0) {
                eurephia_log(ctx, LOG_FATAL, 0,
                             "eFW_RunFirewall::process_input: Failed to fork process for %s", fwcmd);
                return 0;
        }

        switch( pid ) {
        case 0: // child process - execute the program and exit
                execve(fwcmd, (char * const*) ipt_args, NULL);
                exit(1); // execve should replace the process, but if it fails to do so, make sure we exit

        default: // parent process
                if( waitpid(pid, &cmdret, 0) != pid ) {
                        eurephia_log(ctx, LOG_WARNING, 0,
                                     "eFW_RunFirewall::process_input: Failed to wait for process for %s"
                                     " to complete (%s)", fwcmd, strerror(errno));
                }
                eurephia_log(ctx, LOG_INFO, 4, "eFW_RunFirewall - iptables exited with code: %i ", cmdret);
        }
        return 1;
}
