/* eurephia.c  --  Main functions for the eurephia authentication module
 *
 *  GPLv2 only - Copyright (C) 2008 - 2019
 *               David Sommerseth <dazo@eurephia.org>
 *
 *  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   eurephia.c
 * @author David Sommerseth <dazo@users.sourceforge.net>
 * @date   2008-08-06
 *
 * @brief  The core eurephia functions which is called from OpenVPN.
 *
 */


#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <sys/mman.h>
#include <string.h>
#include <getopt.h>
#include <assert.h>
#include <errno.h>

#define EUREPHIA_FWINTF         /**< Include the proper eurephiaFWINTF declaration in eurephiaCTX */
#include <eurephiafw_struct.h>
#include <eurephia_context.h>
#include <eurephia_nullsafe.h>
#include <eurephiadb.h>
#include <eurephiadb_driver.h>
#include <eurephiafw.h>
#include <eurephia_values.h>
#include <eurephiadb_session_common.h>
#include <eurephiadb_session.h>
#include <certinfo.h>
#include <randstr.h>
#include <passwd.h>
#include <environment.h>

#define MAX_ARGUMENTS 64      /**<  Maximum arguments we will parse from the openvpn plug-in configuration */


/**
 * Initialises the eurephia OpenVPN plug-in and prepares a eurephiaCTX (context)
 *
 * @param argv  Arguments from the openvpn configuration file.
 *
 * @return returns a pointer to a eurephiaCTX context.  On failure NULL is returned.
 */
eurephiaCTX *eurephiaInit(char const **argv, char const **envp)
{
        static struct option eurephia_opts[] = {
                {"log-destination", required_argument, 0, 'l'},
                {"log-level", required_argument, 0, 'L'},
                {"database-interface", required_argument, 0, 'i'},
                {0, 0, 0 ,0}
        };
        int argc = 0, error = 0, loglvl = 0, dbargc = 0;
        const char *dbargv[MAX_ARGUMENTS];
        const char *fwintf = NULL, *logfile = NULL, *dbi = NULL;
        eurephiaCTX *ctx = NULL;

        //
        // Parse input arguments
        //

        // Count arguments
        for( argc = 0; argv[argc] != NULL; argc++ ) {}

        while(1) {
                int opt_idx = 0;
                int c = 0;

                c = getopt_long(argc, (char **)argv, "l:L:i:", eurephia_opts, &opt_idx);
                if( c == -1 ) {
                        break;
                }

                switch( c ) {
                case 'l':
                        logfile = optarg;
                        break;

                case 'L':
                        loglvl = atoi_nullsafe(optarg);
                        break;

                case 'i':
                        dbi = optarg;
                        break;

                default:
                        fprintf(stderr, "Error parsing eurephia-auth arguments.\n");
                        return NULL;
                        break;
                }
        }

        // Put the rest of the arguments into an own array which will be the db module arguments
        if( optind < argc ) {
                // copy arguments, but make sure we do not exceed our limit
                while( (dbargc < MAX_ARGUMENTS) && (optind < argc) ) {
                        dbargv[dbargc] = argv[optind++];
                        dbargc++;
                        dbargv[dbargc] = NULL;
                }
        }
        // End of argument parsing

        // Prepare a context area for eurephia-auth
        ctx = (eurephiaCTX *) malloc_nullsafe(NULL, sizeof(eurephiaCTX)+2);
        assert( ctx != NULL );
        if( mlock(ctx, sizeof(eurephiaCTX)+2) < 0 ) {
                eurephia_log(ctx, LOG_CRITICAL, 0, "Could not mlock() eurephia context: %s",
                             strerror(errno));
        };
        ctx->context_type = ECTX_PLUGIN_AUTH;

        // Open a log file
        if( (logfile == NULL) || (strcmp(logfile, "openvpn:") == 0) ) {
                // If no logfile is given, or openvpn: is given, log to stderr which OpenVPN will
                // take care of
                eurephia_log_init(ctx, "eurephia-auth", "stderr:", loglvl);
        } else  {
                // If another log file is given, process that
                eurephia_log_init(ctx, "eurephia-auth", logfile, loglvl);
        }
        eurephia_log(ctx, LOG_INFO, 0, "Initialising eurephia v" EUREPHIAVERSION
                     " (David Sommerseth (C) 2008-2012 GPLv2)");

        // Load the database driver
        if( (error == 0) && eDBlink_init(ctx, dbi, 3) ) {
                // Connect to the database
                if( !eDBconnect(ctx, dbargc, dbargv) ) {
                        eurephia_log(ctx, LOG_PANIC, 0, "Could not connect to the database");
                        error = 1;
                        eDBlink_close(ctx);
                }
        } else {
                eurephia_log(ctx, LOG_PANIC, 0, "Could not load the database driver");
                error = 1;
        }

        if( error > 0 ) {
                eurephia_log(ctx, LOG_PANIC, 0, "eurephia-auth is not available");
                eurephia_log_close(ctx);
                free_nullsafe(ctx, ctx);
                return NULL;
        }

        // Get data for server_salt - which will be used for the password cache
        ctx->server_salt = (char *) malloc_nullsafe(ctx, SIZE_PWDCACHE_SALT+2);
        assert( ctx->server_salt != NULL );
        if( mlock(ctx->server_salt, SIZE_PWDCACHE_SALT+2) < 0 ) {
                eurephia_log(ctx, LOG_CRITICAL, 0, "Could not mlock() eurephia server salt: %s",
                             strerror(errno));
        }

        if( !eurephia_randstring(ctx, ctx->server_salt, SIZE_PWDCACHE_SALT) ) {
                eurephia_log(ctx, LOG_PANIC, 0 ,
                             "Could not get enough random data for password cache.");

                free_nullsafe(ctx, ctx->server_salt);
                eDBdisconnect(ctx);
                eurephia_log_close(ctx);
                free_nullsafe(ctx, ctx);
                return NULL;
        }

        // Check if firewall functionality is enabled, load module if given
        fwintf = eGet_value(ctx->dbc->config, "firewall_interface");
        if( fwintf != NULL ) {
                if( eFW_load(ctx, fwintf) ) {
                        char *daemon_s = GETENV_DAEMON(envp);
                        char *logredir_s = GETENV_DAEMONLOGREDIR(envp);

                        eurephia_log(ctx, LOG_INFO, 0, "Loaded firewall interface: %s", fwintf);
                        eFW_StartFirewall(ctx,
                                          (daemon_s && (daemon_s[0] == '1')),
                                          (logredir_s && logredir_s[0] == '1'));
                        free_nullsafe(ctx, daemon_s);
                        free_nullsafe(ctx, logredir_s);
                } else {
                        eurephia_log(ctx, LOG_FATAL, 0, "Loading of firewall interface failed (%s)", fwintf);
                        ctx->eurephia_fw_intf = NULL;
                }
        } else {
                ctx->eurephia_fw_intf = NULL;
        }

        // Prepare an empty disconnected list.
        // This one is used to track all clients IP addresses and their corresponding eurephia session ID
        // when they disconnect.  This is especially needed in TUN mode, the eurephia_learn_address()
        // delete call with otherwise have too little information to identify the session.
        ctx->disconnected = eCreate_value_space(ctx, 12);

        eurephia_log(ctx, LOG_INFO, 1, "eurephia-auth is initialised");
        return ctx;
}


/**
 * Shutdowns the eurephia plug-in properly, disconnecting from database, unloading drivers,
 * closing log files and cleaning up the memory used.
 *
 * @param ctx  The eurephiaCTX used by openvpn.
 *
 * @return Returns 1 on success, otherwise 0.
 */
int eurephiaShutdown(eurephiaCTX *ctx)
{
        if( ctx == NULL ) {
                return 0;
        }

        if( ctx->disconnected != NULL ) {
                eFree_values(ctx, ctx->disconnected);
                ctx->disconnected = NULL;
        }

        if( ctx->eurephia_fw_intf != NULL ) {
                if( ctx->fwcfg != NULL ) {
                        eFW_StopFirewall(ctx);
                }
                eFW_unload(ctx);
        }

        if( (ctx->dbc != NULL) && (ctx->dbc->dbhandle != NULL) ) {
                eDBdisconnect(ctx);
        }

        if( ctx->eurephia_driver != NULL ) {
                eDBlink_close(ctx);
        }

        eurephia_log_close(ctx);

        memset(ctx->server_salt, 0xff, SIZE_PWDCACHE_SALT+2);
        munlock(ctx->server_salt, SIZE_PWDCACHE_SALT+2);
        free_nullsafe(ctx, ctx->server_salt);
        munlock(ctx, sizeof(eurephiaCTX)+2);
        free_nullsafe(ctx, ctx);
        return 1;
}


/**
 * Verifies the certificate digest (SHA1 fingerprint) against the database.
 *
 * @param ctx        eurephiaCTX
 * @param env        openvpn environment table containing all the information we need
 * @param depth_str  If depth is 0, it's a client certificate, or else it's a CA certificate.
 *                   The input is a string containing the depth value.
 *
 * @return Returns 0 on failure and 1 on success.
 */
int eurephia_tlsverify(eurephiaCTX *ctx, const char **env, const char *depth_str)
{
        int result = 0;
        char *ipaddr;
        char *tls_digest, *tls_id;
        unsigned int depth = atoi_nullsafe(depth_str);
        certinfo *ci = NULL;

        DEBUG(ctx, 10, "** Function call: eurephia_tlsverify(...)");

        // Check if IP address is blacklisted
        ipaddr = GETENV_UNTRUSTEDIP(ctx, env);
        if( eDBblacklist_check(ctx, attempt_IPADDR, ipaddr) == 1 ) {
                eDBregister_attempt(ctx, attempt_IPADDR, ATTEMPT_REGISTER, ipaddr);

                // If fw blacklisting is configured, also blacklist there too
                if( (ctx->fwcfg != NULL) && (ctx->fwcfg->fwblacklist != NULL ) ) {
                        eFWupdateRequest req = { .mode = fwBLACKLIST, };
                        strncpy(req.ipaddress, ipaddr, sizeof(req.ipaddress));
                        eFW_UpdateFirewall(ctx, &req);
                }
                free_nullsafe(ctx, ipaddr);
                return 0;
        }

        // Check if certificate digest is blacklisted
        tls_digest = GETENV_TLSDIGEST(ctx, env, depth);
        if( eDBblacklist_check(ctx, attempt_CERTIFICATE, tls_digest) == 1 ) {
                eDBregister_attempt(ctx, attempt_IPADDR, ATTEMPT_REGISTER, ipaddr);
                eDBregister_attempt(ctx, attempt_CERTIFICATE, ATTEMPT_REGISTER, tls_digest);
                free_nullsafe(ctx, tls_digest);
                free_nullsafe(ctx, ipaddr);
                return 0;
        }

        // Check if certificate is registered and allowed
        tls_id = GETENV_TLSID(ctx, env, depth);
        ci = parse_tlsid(tls_id);
        result = eDBauth_TLS(ctx, ci->org, ci->common_name, ci->email, tls_digest, depth);
        if( result < 1 ) {
                eDBregister_attempt(ctx, attempt_IPADDR, ATTEMPT_REGISTER, ipaddr);
                eDBregister_attempt(ctx, attempt_CERTIFICATE, ATTEMPT_REGISTER, tls_digest);
        }

        if( result > 0 ) {
                // Certificate is okay, result contains the certificate ID
                eurephia_log(ctx, LOG_INFO, (depth == 0 ? 0 : 1),
                             "Found certid %i for user: %s/%s/%s",
                             result, ci->org, ci->common_name, ci->email);
        } else {
                eurephia_log(ctx, LOG_WARNING, 0,
                             "Unknown certificate for: %s/%s/%s (depth %i, digest: %s)",
                             ci->org, ci->common_name, ci->email, depth, tls_digest);
        }
        free_nullsafe(ctx, tls_id);
        free_nullsafe(ctx, tls_digest);
        free_nullsafe(ctx, ipaddr);
        free_certinfo(ci);
        DEBUG(ctx, 10, "** Function result: eurephia_tlsverify(...) == %i", result > 0);

        return (result > 0);
}


/**
 * Authenticates the given user name, password and client certificate against the database.
 *
 * @param ctx  eurephiaCTX
 * @param env  openvpn environment table
 *
 * @return Returns 0 on failure and 1 on success.
 */
int eurephia_userauth(eurephiaCTX *ctx, const char **env)
{
        eurephiaSESSION *authsess = NULL;
        int result = 0, certid = 0;
        char *cname, *remport, *ipaddr = NULL;
        char *tls_digest = NULL, *tls_id = NULL, *username = NULL;
        char *passwd = NULL, *pwdcache = NULL, *chkpwd = NULL;
        certinfo *ci = NULL;

        DEBUG(ctx, 10, "** Function call: eurephia_userauth(...)");


        // Check if IP address is blacklisted
        ipaddr = GETENV_UNTRUSTEDIP(ctx, env);
        if( eDBblacklist_check(ctx, attempt_IPADDR, ipaddr) == 1 ) {
                eDBregister_attempt(ctx, attempt_IPADDR, ATTEMPT_REGISTER, ipaddr);

                // If fw blacklisting is configured, also blacklist there too
                if( (ctx->fwcfg != NULL) && (ctx->fwcfg->fwblacklist != NULL ) ) {
                        eFWupdateRequest req = { .mode = fwBLACKLIST, };
                        strncpy(req.ipaddress, ipaddr, sizeof(req.ipaddress));
                        eFW_UpdateFirewall(ctx, &req);
                }
                free_nullsafe(ctx, ipaddr);

                return 0;
        }

        // Check if certificate digest is blacklisted
        tls_digest = GETENV_TLSDIGEST(ctx, env, 0);
        if( eDBblacklist_check(ctx, attempt_CERTIFICATE, tls_digest) == 1 ) {
                eDBregister_attempt(ctx, attempt_IPADDR, ATTEMPT_REGISTER, ipaddr);
                eDBregister_attempt(ctx, attempt_CERTIFICATE, ATTEMPT_REGISTER, tls_digest);
                free_nullsafe(ctx, ipaddr);
                free_nullsafe(ctx, tls_digest);
                return 0;
        }

        // Check if username is blacklisted
        username = GETENV_USERNAME(ctx, env);
        if( eDBblacklist_check(ctx, attempt_USERNAME, username) == 1 ) {
                eDBregister_attempt(ctx, attempt_IPADDR, ATTEMPT_REGISTER, ipaddr);
                eDBregister_attempt(ctx, attempt_CERTIFICATE, ATTEMPT_REGISTER, tls_digest);
                eDBregister_attempt(ctx, attempt_USERNAME, ATTEMPT_REGISTER, username);
                free_nullsafe(ctx, username);
                free_nullsafe(ctx, ipaddr);
                free_nullsafe(ctx, tls_digest);
                return 0;
        }

        // Get certificate ID
        tls_id = GETENV_TLSID(ctx, env, 0);
        ci = parse_tlsid(tls_id);
        certid = eDBauth_TLS(ctx, ci->org, ci->common_name, ci->email, tls_digest, 0);
        if( certid < 1 ) {
                eDBregister_attempt(ctx, attempt_IPADDR, ATTEMPT_REGISTER, ipaddr);
                eDBregister_attempt(ctx, attempt_CERTIFICATE, ATTEMPT_REGISTER, tls_digest);
                eDBregister_attempt(ctx, attempt_USERNAME, ATTEMPT_REGISTER, username);
                free_nullsafe(ctx, tls_id);
                free_nullsafe(ctx, username);
                free_nullsafe(ctx, ipaddr);
                free_nullsafe(ctx, tls_digest);
                free_certinfo(ci);
                return 0;
        }
        free_certinfo(ci);

        // Do username/password/certificate authentication
        passwd = GETENV_PASSWORD(ctx, env);
        mlock(passwd, strlen_nullsafe(passwd));
        if( (passwd == NULL) || (strlen_nullsafe(passwd) == 0) ) {
                eurephia_log(ctx, LOG_WARNING, 0, "eurephia-auth: No password received.  Action aborted");
                memset(passwd, 0, strlen_nullsafe(passwd));
                munlock(passwd, strlen_nullsafe(passwd));
                free_nullsafe(ctx, passwd);
                free_nullsafe(ctx, tls_id);
                free_nullsafe(ctx, username);
                free_nullsafe(ctx, ipaddr);
                free_nullsafe(ctx, tls_digest);
                return 0;
        }

        // Check if we have a valid password cached
        chkpwd = eurephia_quick_hash(ctx->server_salt, passwd);

        // Get an authentication session, which is not a real session ticket
        // but it uses almost the same system
        cname     = GETENV_CNAME(ctx, env);
        remport   = GETENV_UNTRUSTEDPORT(ctx, env);

        // an authentication session do not use assigned VPN address
        authsess = eDBopen_session_seed(ctx, tls_digest, cname, username, NULL, NULL,
                                        ipaddr, remport);
        if( authsess == NULL ) {
                // No session found
                goto chk_pwd;
        }
        // Get cached password from password cache
        pwdcache = eGet_value(authsess->sessvals, "pwdcache");

        if( (pwdcache != NULL) && (chkpwd != NULL) && (strcmp(pwdcache, chkpwd) == 0) ) {
                // if cached password matches users password, we're done
                eurephia_log(ctx, LOG_INFO, 3, "Authenticated user '%s' against password cache",
                             username);
                eDBfree_session(ctx, authsess);
                result = 1;
                goto exit;
        }

        // If we do not have a valid password cached, check against the user database
 chk_pwd:
        result = eDBauth_user(ctx, certid, username, passwd);
        if( result < 1 ) {
                eDBregister_attempt(ctx, attempt_IPADDR, ATTEMPT_REGISTER, ipaddr);
                eDBregister_attempt(ctx, attempt_CERTIFICATE, ATTEMPT_REGISTER, tls_digest);
                eDBregister_attempt(ctx, attempt_USERNAME, ATTEMPT_REGISTER, username);
        }

        if( result > 0 ) {
                // If we have a valid result, reset all attempt counters.
                eDBregister_attempt(ctx, attempt_IPADDR, ATTEMPT_RESET, ipaddr);
                eDBregister_attempt(ctx, attempt_CERTIFICATE, ATTEMPT_RESET, tls_digest);
                eDBregister_attempt(ctx, attempt_USERNAME, ATTEMPT_RESET, username);

                if( !eDBset_session_value(ctx, authsess, "pwdcache", chkpwd) ) {
                        eurephia_log(ctx, LOG_WARNING, 0,
                                     "Failed to cache password for user '%s'", username);
                }
                eurephia_log(ctx, LOG_INFO, 0, "User '%s' authenticated", username);
        }

 exit:
        memset(passwd, 0, strlen_nullsafe(passwd));
        munlock(passwd, strlen_nullsafe(passwd));
        eDBfree_session(ctx, authsess);
        free_nullsafe(ctx, remport);
        free_nullsafe(ctx, cname);
        free_nullsafe(ctx, passwd);
        free_nullsafe(ctx, tls_id);
        free_nullsafe(ctx, username);
        free_nullsafe(ctx, ipaddr);
        free_nullsafe(ctx, tls_digest);
        free_nullsafe(ctx, chkpwd);
        DEBUG(ctx, 10, "** Function result: eurephia_userauth(...) = %i", (result>0));
        return (result > 0);
}


/**
 * Called when openvpn has authenticated the user and granted it access.  This function
 * will log information about the client
 *
 * @param ctx eurephiaCTX
 * @param env  openvpn environment table
 *
 * @return Returns 0 on failure and 1 on success.
 */
int eurephia_connect(eurephiaCTX *ctx, const char **env) {
        eurephiaSESSION *session = NULL;
        char *digest, *cname, *uname, *vpnipaddr, *vpnipmask, *remipaddr, *remport,
                *proto, *tlsid;
        int certid = 0, uid = 0, ret = 0;
        certinfo *ci = NULL;

        DEBUG(ctx, 10, "** Function call: eurephia_connect(...)");

        // Fetch needed info
        digest    = GETENV_TLSDIGEST(ctx, env, 0);
        tlsid     = GETENV_TLSID(ctx, env, 0);
        cname     = GETENV_CNAME(ctx, env);
        uname     = GETENV_USERNAME(ctx, env);
        vpnipaddr = GETENV_POOLIPADDR(ctx, env);
        vpnipmask = GETENV_POOLNETMASK(ctx, env);
        remipaddr = GETENV_TRUSTEDIP(ctx, env);
        remport   = GETENV_TRUSTEDPORT(ctx, env);
        proto     = GETENV_PROTO1(ctx, env);

        // Get a session ticket
        session = eDBopen_session_seed(ctx, digest, cname, uname, vpnipaddr, vpnipmask,
                                       remipaddr, remport);
        if( session == NULL ) {
                free_nullsafe(ctx, proto);
                free_nullsafe(ctx, remport);
                free_nullsafe(ctx, remipaddr);
                free_nullsafe(ctx, vpnipaddr);
                free_nullsafe(ctx, vpnipmask);
                free_nullsafe(ctx, uname);
                free_nullsafe(ctx, cname);
                free_nullsafe(ctx, tlsid);
                free_nullsafe(ctx, digest);
                return 0;
        }

        // Get certificate info
        ci = parse_tlsid(tlsid);
        if( ci == NULL ) {
                eurephia_log(ctx, LOG_FATAL, 1, "Could not parse the TLS ID string");
                eDBfree_session(ctx, session);
                return 0;
        }
        certid = eDBauth_TLS(ctx, ci->org, ci->common_name, ci->email, digest, 0);
        uid = eDBget_uid(ctx, certid, uname);
        free_certinfo(ci);

        // Register the session login
        ret = eDBregister_login(ctx, session, certid, uid, proto, remipaddr, remport, vpnipaddr, vpnipmask);
        eDBfree_session(ctx, session);
        eurephia_log(ctx, LOG_INFO, 1, "User '%s' logged in", uname);

        free_nullsafe(ctx, proto);
        free_nullsafe(ctx, remport);
        free_nullsafe(ctx, remipaddr);
        free_nullsafe(ctx, vpnipaddr);
        free_nullsafe(ctx, vpnipmask);
        free_nullsafe(ctx, uname);
        free_nullsafe(ctx, cname);
        free_nullsafe(ctx, tlsid);
        free_nullsafe(ctx, digest);

        DEBUG(ctx, 10, "** Function result: eurephia_connect(...) = %i", ret);
        return ret;
}

/**
 * Called when the client disconnects.  This function logs some statistics about the session
 *
 * @param ctx  eurephiaCTX
 * @param env  openvpn environment table
 *
 * @return Returns 0 on failure and 1 on success.
 */
int eurephia_disconnect(eurephiaCTX *ctx, const char **env) {
        eurephiaSESSION *session = NULL;
        char *digest, *cname, *uname, *vpnipaddr, *vpnipmask, *remipaddr, *remport;
        char *bytes_sent, *bytes_rec, *duration;
        int ret = 0;

        DEBUG(ctx, 10, "** Function call: eurephia_disconnect(...)");

        // Fetch needed info
        digest    = GETENV_TLSDIGEST(ctx, env, 0);
        cname     = GETENV_CNAME(ctx, env);
        uname     = GETENV_USERNAME(ctx, env);
        vpnipaddr = GETENV_POOLIPADDR(ctx, env);
        vpnipmask = GETENV_POOLNETMASK(ctx, env);
        remipaddr = GETENV_TRUSTEDIP(ctx, env);
        remport   = GETENV_TRUSTEDPORT(ctx, env);
        bytes_sent= GETENV_BYTESSENT(ctx, env);
        bytes_rec = GETENV_BYTESRECEIVED(ctx, env);
        duration  = GETENV_TIMEDURATION(ctx, env);

        // Get a session ticket
        session = eDBopen_session_seed(ctx, digest, cname, uname, vpnipaddr, vpnipmask,
                                       remipaddr, remport);
        if( session == NULL ) {
                free_nullsafe(ctx, duration);
                free_nullsafe(ctx, bytes_rec);
                free_nullsafe(ctx, bytes_sent);
                free_nullsafe(ctx, remport);
                free_nullsafe(ctx, remipaddr);
                free_nullsafe(ctx, vpnipaddr);
                free_nullsafe(ctx, vpnipmask);
                free_nullsafe(ctx, uname);
                free_nullsafe(ctx, cname);
                free_nullsafe(ctx, digest);
                return 0;
        }

        if( ctx->tuntype == tuntype_TUN ) {
                // Put the clients IP address into the disconnected list, to be used and freed
                // by eurephia_learn_address() afterwards
                eAdd_value(ctx, ctx->disconnected, vpnipaddr, session->sessionkey);
        }

        ret = eDBregister_logout(ctx, session, bytes_sent, bytes_rec, duration);
        eDBfree_session(ctx, session);
        eurephia_log(ctx, LOG_INFO, 1, "User '%s' logged out", uname);

        // Get the authentication session and destroy it
        session = eDBopen_session_seed(ctx, digest, cname, uname, NULL, NULL, remipaddr, remport);
        if( !eDBdestroy_session(ctx, session) ) {
                eurephia_log(ctx, LOG_WARNING, 0, "Could not destroy authentication session (%s/%s/%s)",
                             uname, cname, digest);
        }
        eDBfree_session(ctx, session);
        free_nullsafe(ctx, duration);
        free_nullsafe(ctx, bytes_rec);
        free_nullsafe(ctx, bytes_sent);
        free_nullsafe(ctx, remport);
        free_nullsafe(ctx, remipaddr);
        free_nullsafe(ctx, vpnipaddr);
        free_nullsafe(ctx, vpnipmask);
        free_nullsafe(ctx, uname);
        free_nullsafe(ctx, cname);
        free_nullsafe(ctx, digest);

        DEBUG(ctx, 10, "** Function result: eurephia_disconnect(...) = %i", ret);
        return ret;
}

/**
 * Called whenever openvpn assigns or changes IP addresses of the client.  The function logs
 * this information to keep track of which user was assigned which IP address and which MAC address
 * the user had during the connection as well.  If the firewall interface is enabled, it will also
 * request an update via the firewall driver.
 *
 * @param ctx     eurephiaCTX
 * @param mode    strings which can be "add", "delete".  In some cases also "update".
 * @param macaddr string containing the MAC address of the client, if received from openvpn
 * @param env     openvpn environment table
 *
 * @return Returns 0 on failure and 1 on success.
 */
int eurephia_learn_address(eurephiaCTX *ctx, const char *mode, const char *clientaddr, const char **env) {
        eurephiaSESSION *session = NULL;
        char *digest = NULL, *cname = NULL, *uname = NULL,
                *vpnipaddr = NULL, *vpnipmask = NULL,
                *remipaddr = NULL, *remport = NULL;
        char *fwprofile = NULL, *fwdest = NULL;
        int ret = 0, fw_enabled = 0;

        DEBUG(ctx, 10, "** Function call: eurephia_learn_address(ctx, '%.10s', '%.64s', ...)",
              mode, clientaddr);

        // Get firewall information
        fw_enabled = (eGet_value(ctx->dbc->config, "firewall_interface") != NULL);
        fwdest = eGet_value(ctx->dbc->config, "firewall_destination");
        if( fw_enabled && (fwdest == NULL) ) {
                eurephia_log(ctx, LOG_CRITICAL, 0, "No firewall destination defined in the config.");
        }

        // FIXME: Figure out a way to differentiate IP addresses from VPN clients
        //        and IP adresses behind clients.  Only the former needs to be
        //        processed by eurephia.

        // In TUN mode, if a client reconnects quickly after a disconnect,
        // OpenVPN re-uses the session for this and just sends an update
        // call to LEARN_ADDRESS.  As eurephia in TUN mode treats disconnects
        // with an explicit 'delete' (see eurephia-auth.c for more info) and
        // don't wait for a separate LEARN_ADDRESS delete call (which never
        // seems to come), we treat 'update' as an ordinary 'add' in TUN mode.
        if( (strncmp(mode, "add", 3) == 0)
            || ((ctx->tuntype == tuntype_TUN) && (strncmp(mode, "update", 6) == 0)) ) {

                // Fetch needed info
                digest    = GETENV_TLSDIGEST(ctx, env, 0);
                cname     = GETENV_CNAME(ctx, env);
                uname     = GETENV_USERNAME(ctx, env);
                vpnipaddr = GETENV_POOLIPADDR(ctx, env);
                vpnipmask = GETENV_POOLNETMASK(ctx, env);
                remipaddr = GETENV_TRUSTEDIP(ctx, env);
                remport   = GETENV_TRUSTEDPORT(ctx, env);

                // Get a session ticket
                session = eDBopen_session_seed(ctx, digest, cname, uname, vpnipaddr, vpnipmask,
                                               remipaddr, remport);
                if( session == NULL ) {
                        ret = 0;
                        goto exit;
                }

                // Update openvpn_lastlog with the active VPN client addresses, and save it as a
                // session variable.  clientaddr will contain the MAC address in TAP mode
                ret = eDBregister_vpnclientaddr(ctx, session,
                                                (ctx->tuntype == tuntype_TAP ? clientaddr : NULL),
                                                vpnipaddr, NULL);

                if( (fw_enabled) && (fwdest != NULL) ) {
                        fwprofile = eDBget_firewall_profile(ctx, session);
                        if( fwprofile != NULL ) {
                                eFWupdateRequest req = { .mode = fwADD, };
                                if( ctx->tuntype == tuntype_TAP ) {
                                        strncpy(req.macaddress, clientaddr, sizeof(req.macaddress));
                                } else {
                                        strncpy(req.ipaddress, vpnipaddr, sizeof(req.ipaddress));
                                }
                                strncpy(req.rule_destination, fwdest, sizeof(req.rule_destination));
                                strncpy(req.goto_destination, fwprofile, sizeof(req.goto_destination));
                                eFW_UpdateFirewall(ctx, &req);
                                free_nullsafe(ctx, fwprofile);
                        }
                }
                eDBfree_session(ctx, session);

        } else if( strncmp(mode, "delete", 6) == 0 ) {

                // Load the session
                if( ctx->tuntype == tuntype_TAP ) {
                        // in TAP mode - use the MAC address
                        session = eDBopen_session_macaddr(ctx, clientaddr);
                } else {
                        // in TUN mode - use the disconnected list to find the session key
                        eurephiaVALUES *sessval = eGet_valuestruct(ctx->disconnected, clientaddr);
                        if( sessval ) {
                                session = eDBsession_load(ctx, sessval->val, stSESSION);
                                if( ctx->disconnected->next != NULL ) {
                                        ctx->disconnected = eRemove_value(ctx, ctx->disconnected,
                                                                          sessval->evgid, sessval->evid);
                                } else {
                                        eClear_key_value(ctx->disconnected);
                                }
                        }
                }
                if( session == NULL ) {
                        DEBUG(ctx, 25, "[learn-address] Untracked IP address: %s", clientaddr);
                        ret = 1;  /* Return success, as this isn't a failure in eurephia */
                        goto exit;
                }

                if( (fw_enabled) && (fwdest != NULL) ) {
                        fwprofile = eDBget_firewall_profile(ctx, session);
                        if( fwprofile != NULL ) {
                                eFWupdateRequest req = { .mode = fwDELETE, };
                                if( ctx->tuntype == tuntype_TAP) {
                                        strncpy(req.macaddress, clientaddr, sizeof(req.macaddress));
                                } else {
                                        strncpy(req.ipaddress, clientaddr, sizeof(req.ipaddress));
                                }
                                strncpy(req.rule_destination, fwdest, sizeof(req.rule_destination));
                                strncpy(req.goto_destination, fwprofile, sizeof(req.goto_destination));
                                eFW_UpdateFirewall(ctx, &req);
                                free_nullsafe(ctx, fwprofile);
                        }
                }
                ret = eDBdestroy_session(ctx, session);
                eDBfree_session(ctx, session);
        }

 exit:
        free_nullsafe(ctx, remport);
        free_nullsafe(ctx, remipaddr);
        free_nullsafe(ctx, vpnipaddr);
        free_nullsafe(ctx, vpnipmask);
        free_nullsafe(ctx, uname);
        free_nullsafe(ctx, cname);
        free_nullsafe(ctx, digest);
        DEBUG(ctx, 10, "** Function result: eurephia_learn_address(ctx, '%s', '%.18s', ...) = %i",
                     mode, clientaddr, ret);

        return ret;
}
