/* eurephiafw.c  --  Firewall interface loader for the eurephia module
 *
 *  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   eurephiafw.c
 * @author David Sommerseth <dazo@users.sourceforge.net>
 * @date   2008-08-10
 *
 * @brief  Takes care of loading the configured firewall driver and provides a
 *         generic API for updating the firewall rules.
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <errno.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <time.h>
#include <errno.h>
#include <assert.h>

#define EUREPHIA_FWINTF
#include <eurephiafw_struct.h>
#include <eurephia_context.h>
#include "eurephia_log.h"
#include "eurephiafw.h"
#include "eurephiafw_intf.h"
#include "eurephia_getsym.h"
#include "eurephia_nullsafe.h"
#include "eurephia_values.h"
#include "eurephiafw_helpers.h"
#include "eurephiadb_driver.h"


/**
 * Unloads the firewall driver
 *
 * @param ctx eurephiaCTX
 *
 * @return Returns 1 on success, otherwise 0.
 */
int eFW_unload(eurephiaCTX *ctx) {
        if( ctx == NULL ) {
                return 1;
        }

        if( ctx->eurephia_fw_intf != NULL ) {
                eurephia_log(ctx, LOG_INFO, 3, "Unloading eurephia firewall interface");
                dlclose(ctx->eurephia_fw_intf);
                ctx->eurephia_fw_intf = NULL;
                return 0;
        }
        return 1;
}


/**
 * Loads the given firewall driver/interface
 *
 * @param ctx   eurephiaCTX
 * @param intf  full path to the firewall interface
 *
 * @return Returns 1 on success, otherwise 0.
 */
int eFW_load(eurephiaCTX *ctx, const char *intf) {
        if( (intf == NULL) || (strlen(intf) == 0) ) {
                eurephia_log(ctx, LOG_FATAL, 0, "No valid eurephia firewall interface indicated");
                return 0;
        }
        eurephia_log(ctx, LOG_INFO, 2, "Loading eurephia firewall interface: %s", intf);

        ctx->eurephia_fw_intf = dlopen(intf, RTLD_NOW);
        if( ctx->eurephia_fw_intf == NULL ) {
                eurephia_log(ctx, LOG_FATAL, 0, "Could not open the eurephia firewall interface (%s)", intf);
                eurephia_log(ctx, LOG_FATAL, 1, "dlopen error: %s", dlerror());
                return 0;
        }

        // Mandatory functions
        eFWinterfaceVersion = eGetSym(ctx, ctx->eurephia_fw_intf, "eFWinterfaceVersion");
        eFWinterfaceAPIversion = eGetSym(ctx, ctx->eurephia_fw_intf, "eFWinterfaceAPIversion");

        eurephia_log(ctx, LOG_INFO, 1, "Firewall interface loaded: %s (API version %i)",
                     eFWinterfaceVersion(), eFWinterfaceAPIversion());

        if( eFWinterfaceAPIversion() < 2 ) {
                eurephia_log(ctx, LOG_FATAL, 0, "The firewall interface must use API version 2 at minimum.");
                return 0;
        }

        // Configure firewall interface functions
        switch( eFWinterfaceAPIversion() ) {
        default:
                eurephia_log(ctx, LOG_WARNING, 0,
                             "eurephia Firewall interface API is newer than what the running eurephia version is "
                             "familiar with.  Please consider to upgrade eurephia to take advantage of newer "
                             "features in the firewall driver.");

        case 2:
                eFW_RunFirewall = eGetSym(ctx, ctx->eurephia_fw_intf, "eFW_RunFirewall");
                break;

        }

        if( ctx->fatal_error > 0 ) {
                eurephia_log(ctx, LOG_FATAL, 0, "eurephia Firewall interface is not correctly initialised.  "
                             "eurephia authentication will not be available");
                eFW_unload(ctx);
                return 0;
        }
        return 1;
}


/**
 * daemonize the firewall thread if "daemon" environment variable is set.
 * preserves stderr access after being daemonized, but
 * only if "daemon_log_direct" environment variable is set.
 *
 * @param ctx eurephiaCTX - Used for error logging only
 * @param logdir Set to 1 if logging should be redirected
 */
static void efw_daemonize(eurephiaCTX *ctx, const int logredir)
{
        int fd = -1;
        if( logredir ) {
                fd = dup (2);
        }
        if( daemon(0, 0) < 0 ) {
                eurephia_log(ctx, LOG_WARNING, 0, "efw_daemonize() failed");
        } else if( fd >= 3 ) {
                dup2(fd, 2);
                close(fd);
        }
}


/**
 * Starts the firewall thread.  It is started as a separate process, to make sure it will run with
 * root privileges.
 *
 * @param ctx  eurephiaCTX
 */
void eFW_StartFirewall(eurephiaCTX *ctx, const int daemon, const int logredir) {
        struct mq_attr mqattr;
        eurephiaCTX *shadowctx = NULL;
        eFWupdateRequest updreq;
        char *fwdest = NULL;
        unsigned int prio;
        int mqres = 0;

        ctx->fwcfg = (eurephiaFWINTF *) malloc_nullsafe(ctx, sizeof(eurephiaFWINTF)+2);

        // Create a fake eurephia context, just for logging
        shadowctx = (eurephiaCTX *) malloc_nullsafe(ctx, sizeof(eurephiaCTX)+2);
        assert( shadowctx != NULL );
        if( mlock(shadowctx, sizeof(eurephiaCTX)+2) < 0 ) {
                eurephia_log(ctx, LOG_CRITICAL, 0, "Could not mlock() firewall context: %s",
                             strerror(errno));
        };
        shadowctx->context_type = ECTX_NO_PRIVILEGES;
        shadowctx->log = ctx->log;
        (*ctx->fwcfg).thrdata.ctx = shadowctx;

        (*ctx->fwcfg).thrdata.fw_command = strdup_nullsafe(eGet_value(ctx->dbc->config, "firewall_command"));
        if( (*ctx->fwcfg).thrdata.fw_command == NULL) {
                eurephia_log(ctx, LOG_PANIC, 0, "Could not find firewall_command in configuration.  "
                             "Firewall updates will not be available.");
                return;
        } else {
                eurephia_log(ctx, LOG_INFO, 1, "Using %s to update the firewall rules.",
                             (*ctx->fwcfg).thrdata.fw_command );
        }

        fwdest = eGet_value(ctx->dbc->config, "firewall_destination");
        if( fwdest == NULL ) {
                eurephia_log(ctx, LOG_PANIC, 0, "Could not find firewall_destination in configuration.  "
                             "Firewall updates will not be available.");
                return;
        } else {
                eurephia_log(ctx, LOG_INFO, 1, "Using '%s' as firewall rule for VPN accesses", fwdest);
        }

        ctx->fwcfg->fwblacklist = eGet_value(ctx->dbc->config, "firewall_blacklist_destination");
        if( ctx->fwcfg->fwblacklist != NULL ) {
                eurephia_log(ctx, LOG_INFO, 1,
                             "Blacklisted IP addresses will also be blocked in '%s'",
                             ctx->fwcfg->fwblacklist);

                // Create value space for blacklisted IP addresses
                ctx->fwcfg->blacklisted = eCreate_value_space(ctx, 20);

                // Setup where to send the blacklisted IP addresses - default is to drop them.
                ctx->fwcfg->fwblacklist_sendto = eGet_value(ctx->dbc->config, "firewall_blacklist_send_to");
                if( ctx->fwcfg->fwblacklist_sendto == NULL ) {
                        eurephia_log(ctx, LOG_INFO, 2,"Blacklisted IP addresses will be dropped immediately");
                } else {
                        eurephia_log(ctx, LOG_INFO, 2,"Blacklisted IP addresses will be sent to '%s'",
                                     ctx->fwcfg->fwblacklist_sendto);
                }
        }

        eurephia_log(ctx, LOG_INFO, 3, "Starting eurephia firewall interface");

        // Setup semaphores we need
        if( efwSetupSemaphores(ctx, &(*ctx->fwcfg).thrdata) == 0 ) {
                free_nullsafe(ctx, ctx->fwcfg->thrdata.fw_command);
                return;
        };

        // Setup a message queue
        if( efwSetupMessageQueue(ctx, &(*ctx->fwcfg).thrdata) == 0 ) {
                free_nullsafe(ctx, ctx->fwcfg);
                return;
        }

        // Make sure that these variables are not available in the child
        madvise(ctx, sizeof(eurephiaCTX), MADV_DONTFORK);

        // Start a new process (should run with root permissions) - which will do the firewall work
        if( (ctx->fwcfg->fwproc_pid = fork()) < 0 ) {
                eurephia_log(ctx, LOG_PANIC, 0,
                             "Could not fork out a child process for the firewall interface (%s)",
                             strerror(errno));
                return;
        }
        switch( ctx->fwcfg->fwproc_pid ) {
        case 0: // Child process
                if( daemon ) {
                        efw_daemonize(ctx, logredir);
                }
                eDBdisconnect(ctx);
                eFW_RunFirewall(&(*ctx->fwcfg).thrdata);
                exit(-1); // If our child process exits abnormally.

        default: // Main process
                eurephia_log(ctx, LOG_INFO, 2, "Firewall updater process started (pid %i)",
                             ctx->fwcfg->fwproc_pid);
        }

        // Flush the message queue for old messages
        if( mq_getattr((*ctx->fwcfg).thrdata.msgq, &mqattr) == 0 ) {
                long i;
                char buf[1026];

                memset(&buf, 0, 1026);
                if( mqattr.mq_curmsgs > 0 ) {
                        for( i = 0; i < mqattr.mq_curmsgs; i++ ) {
                                if( mq_receive((*ctx->fwcfg).thrdata.msgq, &buf[0], 1024, &prio) == -1 ) {
                                        eurephia_log(ctx, LOG_CRITICAL, 0,
                                                     "Error while emptying messages from queue: %s",
                                                     strerror(errno));
                                } else {
                                        DEBUG(ctx, 28, "Removed message on queue: %s", buf);
                                }
                        }
                }
                eurephia_log(ctx, LOG_INFO, 3, "Message queue for firewall updates is ready");
        } else {
                eurephia_log(ctx, LOG_FATAL, 0, "Could not retrieve message queue attributes (%s)",
                             strerror(errno));
        }

        // Indicate for the FW module that we are ready
        sem_post(ctx->fwcfg->thrdata.semp_master);

        // Waiting for the FW module to get ready
        DEBUG(ctx, 28, "eFW master is ready, waiting for the eFW worker to get ready");
        sem_wait(ctx->fwcfg->thrdata.semp_worker);
        eurephia_log(ctx, LOG_INFO, 2, "eFW interface initialised.");

        // Initialise the chain
        memset(&updreq, 0, sizeof(eFWupdateRequest));
        updreq.mode = fwINITIALISE;
        strncpy(updreq.rule_destination, fwdest, 64);
        mqres = mq_send((*ctx->fwcfg).thrdata.msgq,
                        (const char *)&updreq, EFW_MSG_SIZE, 1);
        if( mqres < 0 ) {
                eurephia_log(ctx, LOG_ERROR, 0,
                             "Could not request firewall initialisation of the %s chain: (%i) %s",
                             fwdest, mqres, strerror(errno));
        };

        if( ctx->fwcfg->fwblacklist != NULL ) {
                eurephiaVALUES *blacklisted = NULL, *p = NULL;
                eFWupdateRequest updreq;

                // Flushing firewall blacklist chain
                memset(&updreq, 0, sizeof(eFWupdateRequest));
                updreq.mode = fwFLUSH;
                strncpy(updreq.rule_destination, ctx->fwcfg->fwblacklist, 64);
                errno = 0;
                mqres = mq_send((*ctx->fwcfg).thrdata.msgq,
                                (const char *)&updreq, EFW_MSG_SIZE, 1);
                if( mqres < 0 ) {
                        eurephia_log(ctx, LOG_ERROR, 0, "Could not request flushing of the %s chain: (%i) %s",
                                     ctx->fwcfg->fwblacklist, mqres, strerror(errno));
                };

                // Registering already blacklisted IP addresses into the proper firewall chain
                memset(&updreq, 0, sizeof(eFWupdateRequest));
                updreq.mode = fwBLACKLIST;
                blacklisted = eDBget_blacklisted_ip(ctx);
                strncpy(updreq.rule_destination, ctx->fwcfg->fwblacklist, 64);
                strncpy(updreq.goto_destination,
                        ctx->fwcfg->fwblacklist_sendto ? ctx->fwcfg->fwblacklist_sendto : "\0", 64);
                p = blacklisted;
                while( p != NULL ) {
                        if( p->val != NULL ) {
                                memset(&updreq.ipaddress, 0, 36);
                                strncpy(updreq.ipaddress, p->val, 34);
                                eFW_UpdateFirewall(ctx, &updreq);
                        }
                        p = p->next;
                }
                eFree_values(ctx, blacklisted);
        }
}


/**
 * Stops the firewall update process.
 *
 * @param ctx  eurephiaCTX
 */
void eFW_StopFirewall(eurephiaCTX *ctx) {
        char *fwdest = NULL;
        eFWupdateRequest updreq;
        struct timespec tsp;
        int mqres = 0;

        if( ctx->fwcfg == NULL ) {
                return;
        }

        eurephia_log(ctx, LOG_INFO, 2, "Stopping eurephia firewall interface");

        // Flush the firewall chain before shutting down, to make sure
        // we don't unintentionally some accesses open
        fwdest = eGet_value(ctx->dbc->config, "firewall_destination");
        if( fwdest != NULL ) {
                memset(&updreq, 0, sizeof(eFWupdateRequest));
                updreq.mode = fwFLUSH;
                strncpy(updreq.rule_destination, fwdest, 64);

                errno = 0;
                mqres = mq_send((*ctx->fwcfg).thrdata.msgq,
                                (const char *)&updreq, EFW_MSG_SIZE, 1);
                if( mqres < 0 ) {
                        eurephia_log(ctx, LOG_CRITICAL, 0,
                                     "Could not request firewall flushing of the %s chain: %s",
                                     fwdest, strerror(errno));
                };
        } else {
                eurephia_log(ctx, LOG_CRITICAL, 0, "firewall_destination not set in config.  Will not flush "
                             "firewall before shutting down the firewall interface.");
        }


        // Send shutdown message to the firewall module process
        memset(&updreq, 0, sizeof(eFWupdateRequest));
        updreq.mode = fwSHUTDOWN;
        errno = 0;
        mqres = mq_send((*ctx->fwcfg).thrdata.msgq,
                        (const char *)&updreq, EFW_MSG_SIZE, 1);
        if( mqres < 0 ) {
                eurephia_log(ctx, LOG_PANIC, 0,
                             "Could not initiate shutdown on eFW module: %s", strerror(errno));
                kill(ctx->fwcfg->fwproc_pid, SIGABRT);
        }

        //
        // Wait for the firewall module process to finish
        //

        // prepare a timeout - 30 seconder's should be enough
        if( clock_gettime(CLOCK_REALTIME, &tsp) == -1 ) {
                eurephia_log(ctx, LOG_FATAL, 0, "Could not prepare timeout for firewall shutdown: %s",
                             strerror(errno));
                sleep(3);
                kill(ctx->fwcfg->fwproc_pid, SIGABRT);
        }
        tsp.tv_sec += 30;

        // Wait for shutdown accepted signal
        if( sem_timedwait(ctx->fwcfg->thrdata.semp_worker, &tsp) == -1 ) {
                eurephia_log(ctx, LOG_PANIC, 0, "Failed to wait for eFW module process to quit: %s",
                             strerror(errno));
                sleep(3);
                kill(ctx->fwcfg->fwproc_pid, SIGABRT);
        }

        // Send acknowledge back
        sem_post(ctx->fwcfg->thrdata.semp_master);

        // Clean up and exit
        munlock(ctx->fwcfg->thrdata.ctx, sizeof(eurephiaCTX)+2);
        free_nullsafe(ctx, ctx->fwcfg->thrdata.ctx);
        free_nullsafe(ctx, ctx->fwcfg->fwblacklist_sendto);
        eFree_values(ctx, ctx->fwcfg->blacklisted);
        free_nullsafe(ctx, (*ctx->fwcfg).thrdata.fw_command);
        free_nullsafe(ctx, ctx->fwcfg);
        eurephia_log(ctx, LOG_INFO, 2, "eurephia firewall interface is stopped");
}


/**
 * Requests an update of the firewall rules
 *
 * @param ctx        eurephiaCTX
 * @param request    eFWupdateRequest struct with the needed firewall information
 *
 * @return Returns 1 on success, otherwise 0.
 */
int eFW_UpdateFirewall(eurephiaCTX *ctx, eFWupdateRequest *request ) {
        char *blchk = NULL;
        unsigned int prio = 0;
        int mqres = 0;

        if( (*ctx->fwcfg).thrdata.fw_command == NULL ) {
                eurephia_log(ctx, LOG_FATAL, 0, "Function call: eFW_UpdateFirewall() -- "
                             "firewall_command is not configured. Firewall rules was not updated.");
                return 0;
        }

        if( (request == NULL) ) {
                eurephia_log(ctx, LOG_FATAL, 0, "eFW_UpdateFirewall:  Invalid update request");
                return 0;
        }

        switch( request->mode ) {
        case fwADD:
                prio = 11;
        case fwDELETE:
		if( strlen_nullsafe(request->rule_destination) < 1 ) {
			eurephia_log(ctx, LOG_FATAL, 0,
				     "eFW_UpdateFirewall: No firewall destination defined");
			return 0;
		}

                if( (strlen_nullsafe(request->macaddress) < 1)
                    && (strlen_nullsafe(request->ipaddress) < 1) ) {
                        eurephia_log(ctx, LOG_ERROR, 0,
                                     "eFW_UpdateFirewall: No MAC address nor IP address was given received");
                        return 1;
                }
                if( prio == 0 ) {
                        prio = 12; // It's a delete request, it's not as important as other messages
                }
                eurephia_log(ctx, LOG_INFO, 3,
                             "Function call: eFW_UpdateFirewall(ctx, %s, '%.18s', '%s', '%s')",
                             (request->mode == fwADD ? "ADD" : "DELETE"),
                             (ctx->tuntype == tuntype_TAP ? request->macaddress : request->ipaddress),
                             request->rule_destination,
                             request->goto_destination);
                errno = 0;
                mqres = mq_send((*ctx->fwcfg).thrdata.msgq,
                                (const char *)request, EFW_MSG_SIZE, prio);
                if( mqres < 0 ) {
                        eurephia_log(ctx, LOG_CRITICAL, 0,
                                     "eFW_updateFirewall: Failed to send update request - %s",
                                     strerror(errno));
                        return 0;
                }
                return 1;

        case fwBLACKLIST:
                if( strlen_nullsafe(request->ipaddress) < 1) {
                        eurephia_log(ctx, LOG_ERROR, 0,
                                     "eFW_UpdateFirewall: No IP address given for blacklist");
                        return 1;
                }
                eurephia_log(ctx, LOG_INFO, 3,
                             "Function call: eFW_UpdateFirewall(ctx, %s, '%.34s','%s', NULL)",
                             "BLACKLIST", request->ipaddress, request->rule_destination);

                // Check if IP address is already registered as blacklisted
                if( (blchk = eGet_value(ctx->fwcfg->blacklisted, request->ipaddress)) == NULL ) {
                        // Setup rule destinations
                        strncpy(request->rule_destination, ctx->fwcfg->fwblacklist, 64);
                        if( ctx->fwcfg->fwblacklist_sendto != NULL ) {
                                strncpy(request->goto_destination, ctx->fwcfg->fwblacklist_sendto, 64);
                        } else {
                                strncpy(request->goto_destination, "DROP\0", 6);
                        }

                        errno = 0;
                        mqres = mq_send((*ctx->fwcfg).thrdata.msgq,
                                        (const char *)request, EFW_MSG_SIZE, prio);
                        if( mqres < 0 ) {
                                eurephia_log(ctx, LOG_CRITICAL, 0,
                                             "eFW_updateFirewall: Failed to send update request - %s",
                                             strerror(errno));
                                return 0;
                        }
                        eAdd_value(ctx, ctx->fwcfg->blacklisted,
                                   request->ipaddress, request->rule_destination);
                } else {
                        eurephia_log(ctx, LOG_INFO, 5, "IP address already blacklisted in '%s'", blchk);
                }
                return 1;

        case fwFLUSH:
        case fwINITIALISE:
                eurephia_log(ctx, LOG_ERROR, 5, "eFW_UpdateFirewall: Unsupported update request");
                return 0;

        default:
                eurephia_log(ctx, LOG_CRITICAL, 0,
                             "Function call: eFW_UpdateFirewall(ctx, %s, '%s') - UNKNOWN MODE",
                             "(unknown)", request->macaddress);
                return 0;
        }
}
