// SPDX-License-Identifier: GPL-2.0-only
/*
 * Digital Voice Modem - Converged FNE Software
 * GPLv2 Open Source. Use is subject to license terms.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 *  Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
 *
 */
#include "Defines.h"
#include "common/network/udp/Socket.h"
#include "common/Log.h"
#include "common/StopWatch.h"
#include "common/Thread.h"
#include "network/callhandler/TagDMRData.h"
#include "network/callhandler/TagP25Data.h"
#include "network/callhandler/TagNXDNData.h"
#include "network/callhandler/TagAnalogData.h"
#include "ActivityLog.h"
#include "HostFNE.h"
#include "FNEMain.h"

using namespace network;
#if !defined(_WIN32)
using namespace network::viface;
#endif // !defined(_WIN32)
using namespace lookups;

#include <cstdio>
#include <algorithm>
#include <functional>

#if !defined(_WIN32)
#include <sys/utsname.h>
#include <unistd.h>
#include <pwd.h>
#endif // !defined(_WIN32)

// ---------------------------------------------------------------------------
//  Constants
// ---------------------------------------------------------------------------

#define MIN_WORKER_CNT 4U
#define MAX_WORKER_CNT 128U

#define MAX_RECOMMENDED_PEER_NETWORKS 8U

#define THREAD_CYCLE_THRESHOLD 2U

#define IDLE_WARMUP_MS 5U
#define DEFAULT_MTU_SIZE 496
#define MAX_MTU_SIZE 65535

// ---------------------------------------------------------------------------
//  Public Class Members
// ---------------------------------------------------------------------------

/* Initializes a new instance of the HostFNE class. */

HostFNE::HostFNE(const std::string& confFile) :
    m_confFile(confFile),
    m_conf(),
    m_network(nullptr),
    m_mdNetwork(nullptr),
    m_vtunEnabled(false),
    m_packetDataMode(PacketDataMode::PROJECT25),
#if !defined(_WIN32)
    m_tun(nullptr),
#endif // !defined(_WIN32)
    m_dmrEnabled(false),
    m_p25Enabled(false),
    m_nxdnEnabled(false),
    m_analogEnabled(false),
    m_ridLookup(nullptr),
    m_tidLookup(nullptr),
    m_peerListLookup(nullptr),
    m_cryptoLookup(nullptr),
    m_peerNetworks(),
    m_pingTime(5U),
    m_maxMissedPings(5U),
    m_updateLookupTime(10U),
    m_peerReplicaSavesACL(false),
    m_allowActivityTransfer(false),
    m_allowDiagnosticTransfer(false),
    m_RESTAPI(nullptr)
{
    /* stub */
}

/* Finalizes a instance of the HostFNE class. */

HostFNE::~HostFNE() = default;

/* Executes the main FNE processing loop. */

int HostFNE::run()
{
    bool ret = false;
    try {
        ret = yaml::Parse(m_conf, m_confFile.c_str());
        if (!ret) {
            ::fatal("cannot read the configuration file, %s\n", m_confFile.c_str());
        }
    }
    catch (yaml::OperationException const& e) {
        ::fatal("cannot read the configuration file - %s (%s)", m_confFile.c_str(), e.message());
    }

    bool m_daemon = m_conf["daemon"].as<bool>(false);
    if (m_daemon && g_foreground)
        m_daemon = false;

    // initialize system logging
    yaml::Node logConf = m_conf["log"];
    bool useSyslog = logConf["useSyslog"].as<bool>(false);
    if (g_foreground)
        useSyslog = false;
    ret = ::LogInitialise(logConf["filePath"].as<std::string>(), logConf["fileRoot"].as<std::string>(),
        logConf["fileLevel"].as<uint32_t>(0U), logConf["displayLevel"].as<uint32_t>(0U), false, useSyslog);
    if (!ret) {
        ::fatal("unable to open the log file\n");
    }

    ret = ::ActivityLogInitialise(logConf["activityFilePath"].as<std::string>(), logConf["fileRoot"].as<std::string>());
    if (!ret) {
        ::fatal("unable to open the activity log file\n");
    }

#if !defined(_WIN32)
    // handle POSIX process forking
    if (m_daemon) {
        // create new process
        pid_t pid = ::fork();
        if (pid == -1) {
            ::fprintf(stderr, "%s: Couldn't fork() , exiting\n", g_progExe.c_str());
            ::LogFinalise();
            return EXIT_FAILURE;
        }
        else if (pid != 0) {
            ::LogFinalise();
            exit(EXIT_SUCCESS);
        }

        // create new session and process group
        if (::setsid() == -1) {
            ::fprintf(stderr, "%s: Couldn't setsid(), exiting\n", g_progExe.c_str());
            ::LogFinalise();
            return EXIT_FAILURE;
        }

        // set the working directory to the root directory
        if (::chdir("/") == -1) {
            ::fprintf(stderr, "%s: Couldn't cd /, exiting\n", g_progExe.c_str());
            ::LogFinalise();
            return EXIT_FAILURE;
        }

        ::close(STDIN_FILENO);
        ::close(STDOUT_FILENO);
        ::close(STDERR_FILENO);
    }
#endif // !defined(_WIN32)

    ::LogInfo(__BANNER__ "\r\n" __PROG_NAME__ " " __VER__ " (built " __BUILD__ ")\r\n" \
        "Copyright (c) 2017-2026 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\r\n" \
        "Portions Copyright (c) 2015-2021 by Jonathan Naylor, G4KLX and others\r\n" \
        HIGHLY_UNNECESSARY_DISCLAIMER_FOR_THE_MENTAL "\r\n" \
        ">> Fixed Network Equipment\r\n");

    // read base parameters from configuration
    ret = readParams();
    if (!ret)
        return EXIT_FAILURE;

    yaml::Node systemConf = m_conf["system"];

    // try to load radio IDs table
    std::string ridLookupFile = systemConf["radio_id"]["file"].as<std::string>();
    uint32_t ridReloadTime = systemConf["radio_id"]["time"].as<uint32_t>(0U);
    bool verboseRIDRules = systemConf["radio_id"]["verbose"].as<bool>(false);

    LogInfo("Radio Id Lookups");
    LogInfo("    File: %s", ridLookupFile.length() > 0U ? ridLookupFile.c_str() : "None");
    if (ridReloadTime > 0U)
        LogInfo("    Reload: %u mins", ridReloadTime);
    LogInfo("    Verbose: %s", verboseRIDRules ? "true" : "false");
    
    m_ridLookup = new RadioIdLookup(ridLookupFile, ridReloadTime, true, verboseRIDRules);
    m_ridLookup->read();

    // initialize master networking
    ret = createMasterNetwork();
    if (!ret)
        return EXIT_FAILURE;

    // initialize REST API
    initializeRESTAPI();

    // initialize peer networking
    ret = createPeerNetworks();
    if (!ret)
        return EXIT_FAILURE;

    // initialize virtual networking
    ret = createVirtualNetworking();
    if (!ret)
        return EXIT_FAILURE;

    StopWatch stopWatch;
    stopWatch.start();

    /*
    ** Initialize Threads
    */

    if (!Thread::runAsThread(this, threadTrafficNetwork))
        return EXIT_FAILURE;
    if (!Thread::runAsThread(this, threadMetadataNetwork))
        return EXIT_FAILURE;
#if !defined(_WIN32)
    if (!Thread::runAsThread(this, threadVirtualNetworking))
        return EXIT_FAILURE;
#endif // !defined(_WIN32)
    /*
    ** Main execution loop
    */
#if defined(_WIN32)
    ::LogInfoEx(LOG_HOST, "[ OK ] FNE is up and running on Win32");
#else
    struct utsname utsinfo;
    ::memset(&utsinfo, 0, sizeof(utsinfo));
    ::uname(&utsinfo);

    ::LogInfoEx(LOG_HOST, "[ OK ] FNE is up and running on %s %s %s", utsinfo.sysname, utsinfo.release, utsinfo.machine);
#endif // defined(_WIN32)
    while (!g_killed) {
        uint32_t ms = stopWatch.elapsed();

        ms = stopWatch.elapsed();
        stopWatch.start();

        // ------------------------------------------------------
        //  -- Network Clocking                               --
        // ------------------------------------------------------

        // clock master
        if (m_network != nullptr)
            m_network->clock(ms);
        if (m_mdNetwork != nullptr)
            m_mdNetwork->clock(ms);

        // clock peers
        for (auto network : m_peerNetworks) {
            network::PeerNetwork* peerNetwork = network.second;
            if (peerNetwork != nullptr) {
                peerNetwork->clock(ms);
            }
        }

#if !defined(_WIN32)
        if (m_vtunEnabled) {
            switch (m_packetDataMode) {
            case PacketDataMode::DMR:
                m_network->dmrTrafficHandler()->packetData()->clock(ms);
                break;

            case PacketDataMode::PROJECT25:
                m_network->p25TrafficHandler()->packetData()->clock(ms);
                break;
            }
        }
#endif // !defined(_WIN32)

        if (ms < 2U)
            Thread::sleep(1U);
    }

    // shutdown threads
    if (m_RESTAPI != nullptr) {
        m_RESTAPI->close();
        delete m_RESTAPI;
    }

    if (m_network != nullptr) {
        m_network->close();
        delete m_network;
    }

    if (m_mdNetwork != nullptr) {
        m_mdNetwork->close();
        delete m_mdNetwork;
    }

    for (auto network : m_peerNetworks) {
        network::Network* peerNetwork = network.second;
        if (peerNetwork != nullptr)
            peerNetwork->close();
    }
    m_peerNetworks.clear();

    if (m_tidLookup != nullptr) {
        m_tidLookup->setReloadTime(0U); // no reload
        m_tidLookup->stop();
    }

    if (m_ridLookup != nullptr) {
        m_ridLookup->setReloadTime(0U); // no reload
        m_ridLookup->stop();
    }

    if (m_peerListLookup != nullptr) {
        m_peerListLookup->setReloadTime(0U); // no reload
        m_peerListLookup->stop();
    }

    if (m_cryptoLookup != nullptr) {
        m_cryptoLookup->setReloadTime(0U); // no reload
        m_cryptoLookup->stop();
    }
#if !defined(_WIN32)
    if (m_tun != nullptr) {
        if (m_tun->isUp()) {
            m_tun->down();
        }

        delete m_tun;
    }
#endif // !defined(_WIN32)
    return EXIT_SUCCESS;
}

// ---------------------------------------------------------------------------
//  Private Class Members
// ---------------------------------------------------------------------------

/* Reads basic configuration parameters from the YAML configuration file. */

bool HostFNE::readParams()
{
    yaml::Node systemConf = m_conf["system"];
    m_pingTime = systemConf["pingTime"].as<uint32_t>(5U);
    m_maxMissedPings = systemConf["maxMissedPings"].as<uint32_t>(5U);
    m_updateLookupTime = systemConf["aclRuleUpdateTime"].as<uint32_t>(10U);
    bool sendTalkgroups = systemConf["sendTalkgroups"].as<bool>(true);
    m_peerReplicaSavesACL = systemConf["peerReplicaSaveACL"].as<bool>(false);

    bool iAgreeNotToBeStupid = m_conf["iAgreeNotToBeStupid"].as<bool>(false);
    if (!iAgreeNotToBeStupid) {
        LogError(LOG_HOST, HIGHLY_UNNECESSARY_DISCLAIMER_FOR_THE_MENTAL);
        LogError(LOG_HOST, "You must agree to software license terms, and not to be stupid to use this software. Please set 'iAgreeNotToBeStupid' in the configuration file properly.");
        return false;
    }

    if (m_pingTime == 0U) {
        m_pingTime = 5U;
    }

    if (m_pingTime > MAX_PEER_PING_TIME - 1U) {
        LogWarning(LOG_HOST, "Peer ping time is set to %u seconds, this is beyond the allowable maximum of %u seconds. Defaulting.", m_pingTime, MAX_PEER_PING_TIME);
        m_pingTime = MAX_PEER_PING_TIME - 1U;
    }

    if (m_maxMissedPings == 0U) {
        m_maxMissedPings = 5U;
    }

    if (m_updateLookupTime == 0U) {
        m_updateLookupTime = 10U;
    }

    m_allowActivityTransfer = systemConf["allowActivityTransfer"].as<bool>(true);
    m_allowDiagnosticTransfer = systemConf["allowDiagnosticTransfer"].as<bool>(true);

    if (!m_allowActivityTransfer) {
        LogWarning(LOG_HOST, "Peer activity logging is disabled, this severely limits functionality and can prevent proper operations by prohibiting activity logging to this FNE!");
        LogWarning(LOG_HOST, "It is *not* recommended to disable the \"allowActivityTransfer\" option.");
    }

    if (g_promiscuousHub) {
        LogWarning(LOG_HOST, "This FNE was started as a promiscuous hub! In this mode, the FNE will not apply any TG or RID ACL rules and will transparently pass *all* traffic.");
        LogWarning(LOG_HOST, "Promiscuous Hub is intended as a loop breaking measure *only*!");
    }

    LogInfo("General Parameters");
    if (g_promiscuousHub)
        LogInfo(" !! Promiscuous Hub: yes");
    LogInfo("    Peer Ping Time: %us", m_pingTime);
    LogInfo("    Maximum Missed Pings: %u", m_maxMissedPings);
    LogInfo("    ACL Rule Update Time: %u mins", m_updateLookupTime);

    LogInfo("    Send Talkgroups: %s", sendTalkgroups ? "yes" : "no");
    LogInfo("    Peer Replication ACL is retained: %s", m_peerReplicaSavesACL ? "yes" : "no");

    if (m_allowActivityTransfer)
        LogInfo("    Allow Activity Log Transfer: yes");
    else
        LogInfo(" !! Allow Activity Log Transfer: no");
    LogInfo("    Allow Diagnostic Log Transfer: %s", m_allowDiagnosticTransfer ? "yes" : "no");

    // attempt to load and populate routing rules
    yaml::Node masterConf = m_conf["master"];
    yaml::Node talkgroupRules = masterConf["talkgroup_rules"];
    std::string talkgroupConfig = talkgroupRules["file"].as<std::string>();
    uint32_t talkgroupConfigReload = talkgroupRules["time"].as<uint32_t>(30U);
    bool verboseTalkgroupRules = talkgroupRules["verbose"].as<bool>(true);

    yaml::Node adjSiteMapRules = masterConf["adj_site_map"];
    std::string adjSiteMapConfig = adjSiteMapRules["file"].as<std::string>();
    uint32_t adjSiteMapReload = adjSiteMapRules["time"].as<uint32_t>(30U);

    yaml::Node cryptoContainer = masterConf["crypto_container"];
    bool cryptoContainerEnabled = cryptoContainer["enable"].as<bool>(false);
#if !defined(ENABLE_SSL)
    cryptoContainerEnabled = false;
#endif // ENABLE_SSL
    std::string cryptoContainerEKC = cryptoContainer["file"].as<std::string>();
    std::string cryptoContainerPassword = cryptoContainer["password"].as<std::string>();
    std::string cryptoContainerRemotePassword = cryptoContainer["remoteAccessPassword"].as<std::string>();
    bool cryptoContainerRemoteAccess = cryptoContainer["remoteAccess"].as<bool>(false);
    uint32_t cryptoContainerReload = cryptoContainer["time"].as<uint32_t>(30U);

    std::string peerListLookupFile = systemConf["peer_acl"]["file"].as<std::string>();
    bool peerListLookupEnable = systemConf["peer_acl"]["enable"].as<bool>(false);
    uint32_t peerListConfigReload = systemConf["peer_acl"]["time"].as<uint32_t>(30U);
    bool verbosePeerListRules = systemConf["peer_acl"]["verbose"].as<bool>(true);

    LogInfo("Talkgroup Rule Lookups");
    LogInfo("    File: %s", talkgroupConfig.length() > 0U ? talkgroupConfig.c_str() : "None");
    if (talkgroupConfigReload > 0U)
        LogInfo("    Reload: %u mins", talkgroupConfigReload);
    LogInfo("    Verbose: %s", verboseTalkgroupRules ? "true" : "false");

    m_tidLookup = new TalkgroupRulesLookup(talkgroupConfig, talkgroupConfigReload, true, verboseTalkgroupRules);
    m_tidLookup->sendTalkgroups(sendTalkgroups);
    m_tidLookup->read();

    // try to load peer whitelist/blacklist
    LogInfo("Peer List Lookups");
    LogInfo("    Enabled: %s", peerListLookupEnable ? "yes" : "no");
    LogInfo("    File: %s", peerListLookupFile.length() > 0U ? peerListLookupFile.c_str() : "None");
    if (peerListConfigReload > 0U)
        LogInfo("    Reload: %u mins", peerListConfigReload);
    LogInfo("    Verbose: %s", verbosePeerListRules ? "true" : "false");

    m_peerListLookup = new PeerListLookup(peerListLookupFile, peerListConfigReload, peerListLookupEnable, verbosePeerListRules);
    m_peerListLookup->read();

    LogInfo("Adjacent Site Map Lookups");
    LogInfo("    File: %s", adjSiteMapConfig.length() > 0U ? adjSiteMapConfig.c_str() : "None");
    if (adjSiteMapReload > 0U)
        LogInfo("    Reload: %u mins", adjSiteMapReload);

    m_adjSiteMapLookup = new AdjSiteMapLookup(adjSiteMapConfig, adjSiteMapReload);
    m_adjSiteMapLookup->read();

    // try to load peer whitelist/blacklist
    LogInfo("Crypto Container Lookups");
    LogInfo("    Enabled: %s", cryptoContainerEnabled ? "yes" : "no");
    LogInfo("    File: %s", cryptoContainerEKC.length() > 0U ? cryptoContainerEKC.c_str() : "None");
    if (cryptoContainerReload > 0U)
        LogInfo("    Reload: %u mins", cryptoContainerReload);
    LogInfo("    Remote Access Enabled: %s", cryptoContainerRemoteAccess ? "yes" : "no");

    m_cryptoLookup = new CryptoContainer(cryptoContainerEKC, cryptoContainerPassword, 
        cryptoContainerRemotePassword, cryptoContainerRemoteAccess, cryptoContainerReload, cryptoContainerEnabled);
    m_cryptoLookup->read();

    return true;
}

/* Initializes REST API serivces. */

bool HostFNE::initializeRESTAPI()
{
    yaml::Node systemConf = m_conf["system"];
    bool restApiEnable = systemConf["restEnable"].as<bool>(false);

    // dump out if both networking and REST API are disabled
    if (!restApiEnable) {
        return true;
    }

    std::string restApiAddress = systemConf["restAddress"].as<std::string>("127.0.0.1");
    uint16_t restApiPort = (uint16_t)systemConf["restPort"].as<uint32_t>(REST_API_DEFAULT_PORT);
    std::string restApiPassword = systemConf["restPassword"].as<std::string>();
    bool restApiEnableSSL = systemConf["restSsl"].as<bool>(false);
    std::string restApiSSLCert = systemConf["restSslCertificate"].as<std::string>("web.crt");
    std::string restApiSSLKey = systemConf["restSslKey"].as<std::string>("web.key");
    bool restApiDebug = systemConf["restDebug"].as<bool>(false);

    if (restApiPassword.length() > 64) {
        std::string password = restApiPassword;
        restApiPassword = password.substr(0, 64);

        ::LogWarning(LOG_HOST, "REST API password is too long; truncating to the first 64 characters.");
    }

    if (restApiPassword.empty() && restApiEnable) {
        ::LogWarning(LOG_HOST, "REST API password not provided; REST API disabled.");
        restApiEnable = false;
    }

    if (restApiSSLCert.empty() && restApiEnableSSL) {
        ::LogWarning(LOG_HOST, "REST API SSL certificate not provided; REST API SSL disabled.");
        restApiEnableSSL = false;
    }

    if (restApiSSLKey.empty() && restApiEnableSSL) {
        ::LogWarning(LOG_HOST, "REST API SSL certificate private key not provided; REST API SSL disabled.");
        restApiEnableSSL = false;
    }

    LogInfo("REST API Parameters");
    LogInfo("    REST API Enabled: %s", restApiEnable ? "yes" : "no");
    if (restApiEnable) {
        LogInfo("    REST API Address: %s", restApiAddress.c_str());
        LogInfo("    REST API Port: %u", restApiPort);

        LogInfo("    REST API SSL Enabled: %s", restApiEnableSSL ? "yes" : "no");
        LogInfo("    REST API SSL Certificate: %s", restApiSSLCert.c_str());
        LogInfo("    REST API SSL Private Key: %s", restApiSSLKey.c_str());

        if (restApiDebug) {
            LogInfo("    REST API Debug: yes");
        }
    }

    // initialize network remote command
    if (restApiEnable) {
        m_RESTAPI = new RESTAPI(restApiAddress, restApiPort, restApiPassword, restApiSSLKey, restApiSSLCert, restApiEnableSSL, this, restApiDebug);
        m_RESTAPI->setLookups(m_ridLookup, m_tidLookup, m_peerListLookup, m_adjSiteMapLookup, m_cryptoLookup);
        m_RESTAPI->setNetwork(m_network);
        bool ret = m_RESTAPI->open();
        if (!ret) {
            delete m_RESTAPI;
            m_RESTAPI = nullptr;
            LogError(LOG_HOST, "failed to initialize REST API networking! REST API will be unavailable!");
            // REST API failing isn't fatal -- we'll allow this to return normally
        }
    }
    else {
        m_RESTAPI = nullptr;
    }

    return true;
}

/* Initializes master FNE network connectivity. */

bool HostFNE::createMasterNetwork()
{
    yaml::Node systemConf = m_conf["system"];
    std::string identity = systemConf["identity"].as<std::string>("CHANGEME");

    yaml::Node masterConf = m_conf["master"];
    std::string address = masterConf["address"].as<std::string>();
    uint16_t port = (uint16_t)masterConf["port"].as<uint32_t>(TRAFFIC_DEFAULT_PORT);
    uint32_t id = masterConf["peerId"].as<uint32_t>(1001U);
    std::string password = masterConf["password"].as<std::string>();
    bool verbose = masterConf["verbose"].as<bool>(false);
    bool packetDump = masterConf["packetDump"].as<bool>(false);
    bool debug = masterConf["debug"].as<bool>(false);
    bool kmfDebug = masterConf["kmfDebug"].as<bool>(false);
    uint16_t workerCnt = (uint16_t)masterConf["workers"].as<uint32_t>(16U);

    // clamp worker thread count properly
    if (workerCnt > MAX_WORKER_CNT)
        workerCnt = MAX_WORKER_CNT;
    if (workerCnt > 64U)
        LogWarning(LOG_HOST, "Packet worker thread count >64 threads, this is excessive and may result in poor performance.");
    if (workerCnt < MIN_WORKER_CNT)
        workerCnt = MIN_WORKER_CNT;

    bool reportPeerPing = masterConf["reportPeerPing"].as<bool>(false);

    bool encrypted = masterConf["encrypted"].as<bool>(false);
    std::string key = masterConf["presharedKey"].as<std::string>();
    uint8_t presharedKey[AES_WRAPPED_PCKT_KEY_LEN];
    if (!key.empty()) {
        if (key.size() == 32) {
            // bryanb: shhhhhhh....dirty nasty hacks
            key = key.append(key); // since the key is 32 characters (16 hex pairs), double it on itself for 64 characters (32 hex pairs)
            LogWarning(LOG_HOST, "Half-length master network preshared encryption key detected, doubling key on itself.");
        }

        if (key.size() == 64) {
            if ((key.find_first_not_of("0123456789abcdefABCDEF", 2) == std::string::npos)) {
                const char* keyPtr = key.c_str();
                ::memset(presharedKey, 0x00U, AES_WRAPPED_PCKT_KEY_LEN);

                for (uint8_t i = 0; i < AES_WRAPPED_PCKT_KEY_LEN; i++) {
                    char t[4] = {keyPtr[0], keyPtr[1], 0};
                    presharedKey[i] = (uint8_t)::strtoul(t, NULL, 16);
                    keyPtr += 2 * sizeof(char);
                }
            }
            else {
                LogWarning(LOG_HOST, "Invalid characters in the master network preshared encryption key. Encryption disabled.");
                encrypted = false;
            }
        }
        else {
            LogWarning(LOG_HOST, "Invalid master network preshared encryption key length, key should be 32 hex pairs, or 64 characters. Encryption disabled.");
            encrypted = false;
        }
    }

    if (id > 999999999U) {
        ::LogError(LOG_HOST, "Network Peer ID cannot be greater then 999999999.");
        return false;
    }

    m_dmrEnabled = masterConf["allowDMRTraffic"].as<bool>(true);
    m_p25Enabled = masterConf["allowP25Traffic"].as<bool>(true);
    m_nxdnEnabled = masterConf["allowNXDNTraffic"].as<bool>(true);
    m_analogEnabled = masterConf["allowAnalogTraffic"].as<bool>(false);

    uint32_t parrotDelay = masterConf["parrotDelay"].as<uint32_t>(2500U);
    if (m_pingTime * 1000U < parrotDelay) {
        LogWarning(LOG_HOST, "Parrot delay cannot be longer then the ping time of a peer. Reducing parrot delay to half the ping time.");
        parrotDelay = (m_pingTime * 1000U) / 2U;
    }
    bool parrotGrantDemand = masterConf["parrotGrantDemand"].as<bool>(true);

    LogInfo("Network Parameters");
    LogInfo("    Identity: %s", identity.c_str());
    LogInfo("    Peer ID: %u", id);
    LogInfo("    Address: %s", address.c_str());
    LogInfo("    Traffic Port: %u", port);
    LogInfo("    Metadata Port: %u", port + 1U);
    LogInfo("    Allow DMR Traffic: %s", m_dmrEnabled ? "yes" : "no");
    LogInfo("    Allow P25 Traffic: %s", m_p25Enabled ? "yes" : "no");
    LogInfo("    Allow NXDN Traffic: %s", m_nxdnEnabled ? "yes" : "no");
    LogInfo("    Allow Analog Traffic: %s", m_analogEnabled ? "yes" : "no");
    LogInfo("    Parrot Repeat Delay: %u ms", parrotDelay);
    LogInfo("    Parrot Grant Demand: %s", parrotGrantDemand ? "yes" : "no");

    LogInfo("    Worker Threads: %u", workerCnt);

    LogInfo("    Encrypted: %s", encrypted ? "yes" : "no");

    LogInfo("    Report Peer Pings: %s", reportPeerPing ? "yes" : "no");

    if (verbose) {
        LogInfo("    Verbose: yes");
    }

    if (packetDump) {
        LogInfo("    Packet Dump: yes");
    }

    if (debug) {
        LogInfo("    Debug: yes");
    }

    if (kmfDebug) {
        LogInfo("    P25 OTAR KMF Services Debug: yes");
    }

    // initialize traffic networking
    m_network = new TrafficNetwork(this, address, port, id, password, identity, debug, kmfDebug, verbose, reportPeerPing,
        m_dmrEnabled, m_p25Enabled, m_nxdnEnabled, m_analogEnabled,
        parrotDelay, parrotGrantDemand, m_allowActivityTransfer, m_allowDiagnosticTransfer,
        m_pingTime, m_updateLookupTime, workerCnt);
    m_network->setOptions(masterConf, true);
    m_network->setPacketDump(packetDump);

    m_network->setLookups(m_ridLookup, m_tidLookup, m_peerListLookup, m_cryptoLookup, m_adjSiteMapLookup);

    bool ret = m_network->open();
    if (!ret) {
        delete m_network;
        m_network = nullptr;
        LogError(LOG_HOST, "failed to initialize traffic networking!");
        return false;
    }

    if (encrypted) {
        m_network->setPresharedKey(presharedKey);
    }

    // initialize metadata networking
    m_mdNetwork = new MetadataNetwork(this, m_network, address, port + 1U, workerCnt);
    m_mdNetwork->setPacketDump(packetDump);

    ret = m_mdNetwork->open();
    if (!ret) {
        delete m_mdNetwork;
        m_mdNetwork = nullptr;
        LogError(LOG_HOST, "failed to initialize metadata networking!");
        return false;
    }
    else {
        if (encrypted) {
            m_mdNetwork->setPresharedKey(presharedKey);
        }
    }

    return true;
}

/* Entry point to master traffic network thread. */

void* HostFNE::threadTrafficNetwork(void* arg)
{
    thread_t* th = (thread_t*)arg;
    if (th != nullptr) {
#if defined(_WIN32)
        ::CloseHandle(th->thread);
#else
        ::pthread_detach(th->thread);
#endif // defined(_WIN32)

        std::string threadName("fne:traf-net");
        HostFNE* fne = static_cast<HostFNE*>(th->obj);
        if (fne == nullptr) {
            g_killed = true;
            LogError(LOG_HOST, "[FAIL] %s", threadName.c_str());
        }

        if (g_killed) {
            delete th;
            return nullptr;
        }

        LogInfoEx(LOG_HOST, "[ OK ] %s", threadName.c_str());
#ifdef _GNU_SOURCE
        ::pthread_setname_np(th->thread, threadName.c_str());
#endif // _GNU_SOURCE

        StopWatch stopWatch;
        stopWatch.start();

        if (fne->m_network != nullptr) {
            while (!g_killed) {
                uint32_t ms = stopWatch.elapsed();
                stopWatch.start();

                fne->m_network->processNetwork();

                if (ms < THREAD_CYCLE_THRESHOLD)
                    Thread::sleep(THREAD_CYCLE_THRESHOLD);
            }
        }

        LogInfoEx(LOG_HOST, "[STOP] %s", threadName.c_str());
        delete th;
    }

    return nullptr;
}

/* Entry point to master metadata network thread. */

void* HostFNE::threadMetadataNetwork(void* arg)
{
    thread_t* th = (thread_t*)arg;
    if (th != nullptr) {
#if defined(_WIN32)
        ::CloseHandle(th->thread);
#else
        ::pthread_detach(th->thread);
#endif // defined(_WIN32)

        std::string threadName("fne:meta-net");
        HostFNE* fne = static_cast<HostFNE*>(th->obj);
        if (fne == nullptr) {
            g_killed = true;
            LogError(LOG_HOST, "[FAIL] %s", threadName.c_str());
        }

        if (g_killed) {
            delete th;
            return nullptr;
        }

        LogInfoEx(LOG_HOST, "[ OK ] %s", threadName.c_str());
#ifdef _GNU_SOURCE
        ::pthread_setname_np(th->thread, threadName.c_str());
#endif // _GNU_SOURCE

        StopWatch stopWatch;
        stopWatch.start();

        if (fne->m_mdNetwork != nullptr) {
            while (!g_killed) {
                uint32_t ms = stopWatch.elapsed();
                stopWatch.start();

                fne->m_mdNetwork->processNetwork();

                if (ms < THREAD_CYCLE_THRESHOLD)
                    Thread::sleep(THREAD_CYCLE_THRESHOLD);
            }
        }

        LogInfoEx(LOG_HOST, "[STOP] %s", threadName.c_str());
        delete th;
    }

    return nullptr;
}

/* Initializes peer FNE network connectivity. */

bool HostFNE::createPeerNetworks()
{
    yaml::Node systemConf = m_conf["system"];
    std::string identity = systemConf["identity"].as<std::string>("CHANGEME");

    yaml::Node masterConf = m_conf["master"];
    uint32_t masterPeerId = masterConf["peerId"].as<uint32_t>(1001U);

    yaml::Node& peerList = m_conf["peers"];
    if (peerList.size() > 0U) {
        if (peerList.size() > MAX_RECOMMENDED_PEER_NETWORKS) {
            LogWarning(LOG_HOST, "Peer network count (%zu) exceeds the recommended maximum of %u. This may result in poor performance.", peerList.size(), MAX_RECOMMENDED_PEER_NETWORKS);
        }

        for (size_t i = 0; i < peerList.size(); i++) {
            yaml::Node& peerConf = peerList[i];

            bool enabled = peerConf["enable"].as<bool>(false);
            std::string masterAddress = peerConf["masterAddress"].as<std::string>();
            uint16_t masterPort = (uint16_t)peerConf["masterPort"].as<uint32_t>(TRAFFIC_DEFAULT_PORT);
            std::string password = peerConf["password"].as<std::string>();
            uint32_t id = peerConf["peerId"].as<uint32_t>(1001U);
            bool packetDump = peerConf["packetDump"].as<bool>(false);
            bool debug = peerConf["debug"].as<bool>(false);

            bool encrypted = peerConf["encrypted"].as<bool>(false);
            std::string key = peerConf["presharedKey"].as<std::string>();
            uint8_t presharedKey[AES_WRAPPED_PCKT_KEY_LEN];
            if (!key.empty()) {
                if (key.size() == 32) {
                    // bryanb: shhhhhhh....dirty nasty hacks
                    key = key.append(key); // since the key is 32 characters (16 hex pairs), double it on itself for 64 characters (32 hex pairs)
                    LogWarning(LOG_HOST, "Half-length peer %u network preshared encryption key detected, doubling key on itself.", id);
                }

                if (key.size() == 64) {
                    if ((key.find_first_not_of("0123456789abcdefABCDEF", 2) == std::string::npos)) {
                        const char* keyPtr = key.c_str();
                        ::memset(presharedKey, 0x00U, AES_WRAPPED_PCKT_KEY_LEN);

                        for (uint8_t i = 0; i < AES_WRAPPED_PCKT_KEY_LEN; i++) {
                            char t[4] = {keyPtr[0], keyPtr[1], 0};
                            presharedKey[i] = (uint8_t)::strtoul(t, NULL, 16);
                            keyPtr += 2 * sizeof(char);
                        }
                    }
                    else {
                        LogWarning(LOG_HOST, "Invalid characters in the peer network preshared encryption key. Encryption disabled.");
                        encrypted = false;
                    }
                }
                else {
                    LogWarning(LOG_HOST, "Invalid peer network preshared encryption key length, key should be 32 hex pairs, or 64 characters. Encryption disabled.");
                    encrypted = false;
                }
            }

            float latitude = peerConf["latitude"].as<float>(0.0F);
            float longitude = peerConf["longitude"].as<float>(0.0F);
            std::string location = peerConf["location"].as<std::string>();

            bool nakFallOver = peerConf["nakFallOver"].as<bool>(false);
            uint32_t nakFallOverCount = peerConf["nakFallOverCount"].as<uint32_t>(50U);

            ::LogInfoEx(LOG_HOST, "Peer ID %u Master Address %s Master Port %u Enabled %u Encrypted %u NAK Fall Over %u", id, masterAddress.c_str(), masterPort, enabled, encrypted, nakFallOver);

            std::string identOverride = peerConf["identity"].as<std::string>();
            if (identOverride != "") {
                identity = identOverride;
                ::LogInfoEx(LOG_HOST, "Peer ID %u overrides global identity, Identity %s", id, identity.c_str());
            }

            if (id > 999999999U) {
                ::LogError(LOG_HOST, "Network Peer ID cannot be greater then 999999999.");
                continue;
            }

            // initialize networking
            network::PeerNetwork* network = new PeerNetwork(masterAddress, masterPort, 0U, id, password, true, debug, m_dmrEnabled, m_p25Enabled, m_nxdnEnabled, m_analogEnabled, true, true, 
                m_allowActivityTransfer, m_allowDiagnosticTransfer, false, false);
            network->setPacketDump(packetDump);
            network->setMetadata(identity, 0U, 0U, 0.0F, 0.0F, 0, 0, 0, latitude, longitude, 0, location);
            network->setLookups(m_ridLookup, m_tidLookup);
            network->setMasterPeerId(masterPeerId);
            network->setPeerLookups(m_peerListLookup);
            network->setPeerReplicationSaveACL(m_peerReplicaSavesACL);
            network->setNakFallOver(nakFallOver, nakFallOverCount);
            if (encrypted) {
                network->setPresharedKey(presharedKey);
            }

            network->setDMRCallback(std::bind(&HostFNE::processPeerDMR, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
            network->setDMRICCCallback([=](network::NET_ICC::ENUM command, uint32_t dstId, uint8_t slot,
                uint32_t peerId, uint32_t ssrc, uint32_t streamId) { processPeerDMRInCallCtrl(command, dstId, slot, peerId, ssrc, streamId); });
            network->setP25Callback(std::bind(&HostFNE::processPeerP25, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
            network->setP25ICCCallback([=](network::NET_ICC::ENUM command, uint32_t dstId,
                uint32_t peerId, uint32_t ssrc, uint32_t streamId) { processPeerP25InCallCtrl(command, dstId, peerId, ssrc, streamId); });
            network->setNXDNCallback(std::bind(&HostFNE::processPeerNXDN, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
            network->setNXDNICCCallback([=](network::NET_ICC::ENUM command, uint32_t dstId,
                uint32_t peerId, uint32_t ssrc, uint32_t streamId) { processPeerNXDNInCallCtrl(command, dstId, peerId, ssrc, streamId); });
            network->setAnalogCallback(std::bind(&HostFNE::processPeerAnalog, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
            network->setAnalogICCCallback([=](network::NET_ICC::ENUM command, uint32_t dstId,
                uint32_t peerId, uint32_t ssrc, uint32_t streamId) { processPeerAnalogInCallCtrl(command, dstId, peerId, ssrc, streamId); });

            network->setNetTreeDiscCallback(std::bind(&HostFNE::processNetworkTreeDisconnect, this, std::placeholders::_1, std::placeholders::_2));
            network->setNotifyPeerReplicaCallback(std::bind(&HostFNE::processPeerReplicaNotify, this, std::placeholders::_1));

            network->enable(enabled);
            if (enabled) {
                bool ret = network->open();
                if (!ret) {
                    delete network;
                    network = nullptr;
                    LogError(LOG_HOST, "failed to initialize traffic networking for PEER %u", id);
                }
            }

            if (network != nullptr)
                m_peerNetworks[id] = network;
        }
    }

    return true;
}

/* Initializes virtual networking. */

bool HostFNE::createVirtualNetworking()
{
    yaml::Node vtunConf = m_conf["vtun"];
#if !defined(_WIN32)
    bool vtunEnabled = vtunConf["enable"].as<bool>(false);
    if (vtunEnabled) {
        m_vtunEnabled = vtunEnabled;

        std::string vtunName = vtunConf["interfaceName"].as<std::string>("fne0");
        std::string ipv4Address = vtunConf["address"].as<std::string>("192.168.1.254");
        std::string ipv4Netmask = vtunConf["netmask"].as<std::string>("255.255.255.0");
        std::string ipv4Broadcast = vtunConf["broadcast"].as<std::string>("192.168.1.255");
        std::string packetDataModeStr = vtunConf["digitalMode"].as<std::string>("p25");

        if (packetDataModeStr == "dmr") {
            m_packetDataMode = PacketDataMode::DMR;
        } else {
            m_packetDataMode = PacketDataMode::PROJECT25;
        }

        LogInfo("Virtual Network Parameters");
        LogInfo("    Interface Name: %s", vtunName.c_str());
        LogInfo("    Address: %s", ipv4Address.c_str());
        LogInfo("    Netmask: %s", ipv4Netmask.c_str());
        LogInfo("    Broadcast: %s", ipv4Broadcast.c_str());
        LogInfo("    Digital Packet Mode: %s", packetDataModeStr.c_str());

        // initialize networking
        m_tun = new VIFace(vtunName, false);

        m_tun->setIPv4(ipv4Address);
        m_tun->setIPv4Netmask(ipv4Netmask);
        m_tun->setIPv4Broadcast(ipv4Broadcast);
        m_tun->setMTU(DEFAULT_MTU_SIZE);

        m_tun->up();

        yaml::Node vtunQdisc = vtunConf["qdisc"];
        bool qdiscEnable = vtunQdisc["enable"].as<bool>(false);
        if (qdiscEnable) {
            std::string qdiscKind = vtunQdisc["kind"].as<std::string>("fq_codel");
            std::string qdiscArgs = vtunQdisc["args"].as<std::string>("");

            if (m_tun->setQDisc(qdiscKind, qdiscArgs)) {
                LogInfo("    VTUN qdisc applied (programmatic): %s", qdiscKind.c_str());
            }
            else {
                LogWarning(LOG_HOST, "Failed to apply VTUN qdisc (programmatic): %s", qdiscKind.c_str());
            }
        }
    }
#endif // !defined(_WIN32)
    return true;
}

#if !defined(_WIN32)
/* Entry point to virtual networking thread. */

void* HostFNE::threadVirtualNetworking(void* arg)
{
    thread_t* th = (thread_t*)arg;
    if (th != nullptr) {
        ::pthread_detach(th->thread);

        std::string threadName("fne:vt-net-rx");
        HostFNE* fne = static_cast<HostFNE*>(th->obj);
        if (fne == nullptr) {
            g_killed = true;
            LogError(LOG_HOST, "[FAIL] %s", threadName.c_str());
        }

        if (g_killed) {
            delete th;
            return nullptr;
        }

        if (!fne->m_vtunEnabled) {
            delete th;
            return nullptr;
        }

        LogInfoEx(LOG_HOST, "[ OK ] %s", threadName.c_str());
#ifdef _GNU_SOURCE
        ::pthread_setname_np(th->thread, threadName.c_str());
#endif // _GNU_SOURCE

        if (fne->m_tun != nullptr) {
            StopWatch stopWatch;
            stopWatch.start();

            while (!g_killed) {
                uint32_t ms = stopWatch.elapsed();
                stopWatch.start();

                uint8_t packet[MAX_MTU_SIZE];
                ::memset(packet, 0x00U, MAX_MTU_SIZE);

                ssize_t len = fne->m_tun->read(packet);
                if (len > 0) {
                    switch (fne->m_packetDataMode) {
                    case PacketDataMode::DMR:
                        fne->m_network->dmrTrafficHandler()->packetData()->processPacketFrame(packet, (uint32_t)len);
                        break;

                    case PacketDataMode::PROJECT25:
                        fne->m_network->p25TrafficHandler()->packetData()->processPacketFrame(packet, (uint32_t)len);
                        break;
                    }
                }

                if (ms < THREAD_CYCLE_THRESHOLD)
                    Thread::sleep(THREAD_CYCLE_THRESHOLD);
            }
        }

        LogInfoEx(LOG_HOST, "[STOP] %s", threadName.c_str());
        delete th;
    }

    return nullptr;
}
#endif // !defined(_WIN32)

/* Processes DMR peer network traffic. */

void HostFNE::processPeerDMR(network::PeerNetwork* peerNetwork, const uint8_t* data, uint32_t length, uint32_t streamId, 
    const network::frame::RTPFNEHeader& fneHeader, const network::frame::RTPHeader& rtpHeader)
{
    if (peerNetwork == nullptr)
        return; // this shouldn't happen...

    // skip peer if it isn't enabled
    if (!peerNetwork->isEnabled())
        return;

    if (peerNetwork->getStatus() != NET_STAT_RUNNING)
        return;

    // process DMR data
    if (length > 0U) {
        uint32_t peerId = peerNetwork->getPeerId();
        m_network->dmrTrafficHandler()->processFrame(data, length, peerId, rtpHeader.getSSRC(), rtpHeader.getSequence(), streamId, true);
    }
}

/* Helper to process an DMR In-Call Control message. */

void HostFNE::processPeerDMRInCallCtrl(network::NET_ICC::ENUM command, uint32_t dstId, uint8_t slot, 
    uint32_t peerId, uint32_t ssrc, uint32_t streamId)
{
    switch (command) {
    case network::NET_ICC::REJECT_TRAFFIC:
    case network::NET_ICC::DMR_RC_CEASE_TRANSMIT:
    case network::NET_ICC::DMR_RC_REQUEST_CEASE_TRANSMIT:
    case network::NET_ICC::DMR_RC_MAXIMUM_POWER:
    case network::NET_ICC::DMR_RC_MINIMUM_POWER:
    case network::NET_ICC::DMR_RC_POWER_INCREASE_ONE_STEP:
    case network::NET_ICC::DMR_RC_POWER_DECREASE_ONE_STEP:
        m_network->processDownstreamInCallCtrl(command, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR, dstId, slot, peerId, ssrc, streamId);
        break;

    default:
        break;
    }
}

/* Processes P25 peer network traffic. */

void HostFNE::processPeerP25(network::PeerNetwork* peerNetwork, const uint8_t* data, uint32_t length, uint32_t streamId, 
    const network::frame::RTPFNEHeader& fneHeader, const network::frame::RTPHeader& rtpHeader)
{
    if (peerNetwork == nullptr)
        return; // this shouldn't happen...

    // skip peer if it isn't enabled
    if (!peerNetwork->isEnabled())
        return;

    if (peerNetwork->getStatus() != NET_STAT_RUNNING)
        return;

    // process P25 data
    if (length > 0U) {
        uint32_t peerId = peerNetwork->getPeerId();
        m_network->p25TrafficHandler()->processFrame(data, length, peerId, rtpHeader.getSSRC(), rtpHeader.getSequence(), streamId, true);
    }
}

/* Helper to process an P25 In-Call Control message. */

void HostFNE::processPeerP25InCallCtrl(network::NET_ICC::ENUM command, uint32_t dstId,
    uint32_t peerId, uint32_t ssrc, uint32_t streamId)
{
    switch (command) {
    case network::NET_ICC::REJECT_TRAFFIC:
        m_network->processDownstreamInCallCtrl(command, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25, dstId, 0U, peerId, ssrc, streamId);
        break;

    default:
        break;
    }
}

/* Processes NXDN peer network traffic. */

void HostFNE::processPeerNXDN(network::PeerNetwork* peerNetwork, const uint8_t* data, uint32_t length, uint32_t streamId, 
    const network::frame::RTPFNEHeader& fneHeader, const network::frame::RTPHeader& rtpHeader)
{
    if (peerNetwork == nullptr)
        return; // this shouldn't happen...

    // skip peer if it isn't enabled
    if (!peerNetwork->isEnabled())
        return;

    if (peerNetwork->getStatus() != NET_STAT_RUNNING)
        return;

    // process NXDN data
    if (length > 0U) {
        uint32_t peerId = peerNetwork->getPeerId();
        m_network->nxdnTrafficHandler()->processFrame(data, length, peerId, rtpHeader.getSSRC(), rtpHeader.getSequence(), streamId, true);
    }
}

/* Helper to process an NXDN In-Call Control message. */

void HostFNE::processPeerNXDNInCallCtrl(network::NET_ICC::ENUM command, uint32_t dstId, 
    uint32_t peerId, uint32_t ssrc, uint32_t streamId)
{
    switch (command) {
    case network::NET_ICC::REJECT_TRAFFIC:
        m_network->processDownstreamInCallCtrl(command, NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN, dstId, 0U, peerId, ssrc, streamId);
        break;

    default:
        break;
    }
}

/* Processes analog peer network traffic. */

void HostFNE::processPeerAnalog(network::PeerNetwork* peerNetwork, const uint8_t* data, uint32_t length, uint32_t streamId, 
    const network::frame::RTPFNEHeader& fneHeader, const network::frame::RTPHeader& rtpHeader)
{
    if (peerNetwork == nullptr)
        return; // this shouldn't happen...

    // skip peer if it isn't enabled
    if (!peerNetwork->isEnabled())
        return;

    if (peerNetwork->getStatus() != NET_STAT_RUNNING)
        return;

    // process analog data
    if (length > 0U) {
        uint32_t peerId = peerNetwork->getPeerId();
        m_network->analogTrafficHandler()->processFrame(data, length, peerId, rtpHeader.getSSRC(), rtpHeader.getSequence(), streamId, true);
    }
}

/* Helper to process an analog In-Call Control message. */

void HostFNE::processPeerAnalogInCallCtrl(network::NET_ICC::ENUM command, uint32_t dstId, 
    uint32_t peerId, uint32_t ssrc, uint32_t streamId)
{
    switch (command) {
    case network::NET_ICC::REJECT_TRAFFIC:
        m_network->processDownstreamInCallCtrl(command, NET_SUBFUNC::PROTOCOL_SUBFUNC_ANALOG, dstId, 0U, peerId, ssrc, streamId);
        break;

    default:
        break;
    }
}

/* Processes network tree disconnect notification. */

void HostFNE::processNetworkTreeDisconnect(network::PeerNetwork* peerNetwork, const uint32_t offendingPeerId)
{
    if (m_network != nullptr) {
        m_network->processNetworkTreeDisconnect(peerNetwork->getPeerId(), offendingPeerId);
    }
}

/* Processes network tree disconnect notification. */

void HostFNE::processPeerReplicaNotify(network::PeerNetwork* peerNetwork)
{
    if (m_network != nullptr) {
        m_network->setPeerReplica(true);
    }
}
