/** * Digital Voice Modem - Conference FNE Software * GPLv2 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * @package DVM / Conference FNE Software * */ /* * Copyright (C) 2023-2024 by Bryan Biedenkapp N2PLL * * 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; either version 2 of the License, or * (at your option) any later version. * * 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., 675 Mass Ave, Cambridge, MA 02139, USA. */ #include "Defines.h" #include "common/network/UDPSocket.h" #include "common/Log.h" #include "common/StopWatch.h" #include "common/Thread.h" #include "common/Utils.h" #include "network/fne/TagDMRData.h" #include "network/fne/TagP25Data.h" #include "network/fne/TagNXDNData.h" #include "ActivityLog.h" #include "HostFNE.h" #include "FNEMain.h" using namespace network; using namespace lookups; #include #include #include #include #include #include #include #include #include #include // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- #define IDLE_WARMUP_MS 5U // --------------------------------------------------------------------------- // Public Class Members // --------------------------------------------------------------------------- /// /// Initializes a new instance of the HostFNE class. /// /// Full-path to the configuration file. HostFNE::HostFNE(const std::string& confFile) : m_confFile(confFile), m_conf(), m_network(nullptr), m_dmrEnabled(false), m_p25Enabled(false), m_nxdnEnabled(false), m_ridLookup(nullptr), m_tidLookup(nullptr), m_peerNetworks(), m_pingTime(5U), m_maxMissedPings(5U), m_updateLookupTime(10U), m_allowActivityTransfer(false), m_allowDiagnosticTransfer(false), m_RESTAPI(nullptr) { /* stub */ } /// /// Finalizes a instance of the HostFNE class. /// HostFNE::~HostFNE() { /* stub */ } /// /// Executes the main FNE processing loop. /// /// Zero if successful, otherwise error occurred. 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(false); if (m_daemon && g_foreground) m_daemon = false; // initialize system logging yaml::Node logConf = m_conf["log"]; ret = ::LogInitialise(logConf["filePath"].as(), logConf["fileRoot"].as(), logConf["fileLevel"].as(0U), logConf["displayLevel"].as(0U)); if (!ret) { ::fatal("unable to open the log file\n"); } ret = ::ActivityLogInitialise(logConf["activityFilePath"].as(), logConf["fileRoot"].as()); if (!ret) { ::fatal("unable to open the activity log file\n"); } // 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); } ::LogInfo(__PROG_NAME__ " %s (built %s)", __VER__, __BUILD__); ::LogInfo("Copyright (c) 2017-2024 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors."); ::LogInfo("Portions Copyright (c) 2015-2021 by Jonathan Naylor, G4KLX and others"); ::LogInfo(">> Fixed Network Equipment"); // 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(); uint32_t ridReloadTime = systemConf["radio_id"]["time"].as(0U); LogInfo("Radio Id Lookups"); LogInfo(" File: %s", ridLookupFile.length() > 0U ? ridLookupFile.c_str() : "None"); if (ridReloadTime > 0U) LogInfo(" Reload: %u mins", ridReloadTime); m_ridLookup = new RadioIdLookup(ridLookupFile, ridReloadTime, true); m_ridLookup->read(); // initialize REST API initializeRESTAPI(); // initialize master networking ret = createMasterNetwork(); if (!ret) return EXIT_FAILURE; // initialize peer networking ret = createPeerNetworks(); if (!ret) return EXIT_FAILURE; ::LogInfoEx(LOG_HOST, "FNE is up and running"); StopWatch stopWatch; stopWatch.start(); // main execution loop 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); // clock peers for (auto network : m_peerNetworks) { network::Network* peerNetwork = network.second; if (peerNetwork != nullptr) { peerNetwork->clock(ms); // process peer network traffic processPeer(peerNetwork); } } if (ms < 2U) Thread::sleep(1U); } if (m_network != nullptr) { m_network->close(); delete m_network; } for (auto network : m_peerNetworks) { network::Network* peerNetwork = network.second; if (peerNetwork != nullptr) peerNetwork->close(); } m_peerNetworks.clear(); if (m_RESTAPI != nullptr) { m_RESTAPI->close(); delete m_RESTAPI; } if (m_tidLookup != nullptr) { m_tidLookup->stop(); delete m_tidLookup; } if (m_ridLookup != nullptr) { m_ridLookup->stop(); delete m_ridLookup; } 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(5U); m_maxMissedPings = systemConf["maxMissedPings"].as(5U); m_updateLookupTime = systemConf["tgRuleUpdateTime"].as(10U); bool sendTalkgroups = systemConf["sendTalkgroups"].as(true); if (m_pingTime == 0U) { m_pingTime = 5U; } if (m_maxMissedPings == 0U) { m_maxMissedPings = 5U; } if (m_updateLookupTime == 0U) { m_updateLookupTime = 10U; } m_allowActivityTransfer = systemConf["allowActivityTransfer"].as(true); m_allowDiagnosticTransfer = systemConf["allowDiagnosticTransfer"].as(true); LogInfo("General Parameters"); LogInfo(" Peer Ping Time: %us", m_pingTime); LogInfo(" Maximum Missed Pings: %u", m_maxMissedPings); LogInfo(" Talkgroup Rule Update Time: %u mins", m_updateLookupTime); LogInfo(" Send Talkgroups: %s", sendTalkgroups ? "yes" : "no"); LogInfo(" Allow Activity Log Transfer: %s", m_allowActivityTransfer ? "yes" : "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(); uint32_t talkgroupConfigReload = talkgroupRules["time"].as(30U); LogInfo("Talkgroup Rule Lookups"); LogInfo(" File: %s", talkgroupConfig.length() > 0U ? talkgroupConfig.c_str() : "None"); if (talkgroupConfigReload > 0U) LogInfo(" Reload: %u mins", talkgroupConfigReload); m_tidLookup = new TalkgroupRulesLookup(talkgroupConfig, talkgroupConfigReload, true); m_tidLookup->sendTalkgroups(sendTalkgroups); m_tidLookup->read(); return true; } /// /// Initializes REST API serivces. /// /// bool HostFNE::initializeRESTAPI() { yaml::Node systemConf = m_conf["system"]; bool restApiEnable = systemConf["restEnable"].as(false); // dump out if both networking and REST API are disabled if (!restApiEnable) { return true; } std::string restApiAddress = systemConf["restAddress"].as("127.0.0.1"); uint16_t restApiPort = (uint16_t)systemConf["restPort"].as(REST_API_DEFAULT_PORT); std::string restApiPassword = systemConf["restPassword"].as(); bool restApiDebug = systemConf["restDebug"].as(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; } 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); if (restApiDebug) { LogInfo(" REST API Debug: yes"); } } // initialize network remote command if (restApiEnable) { m_RESTAPI = new RESTAPI(restApiAddress, restApiPort, restApiPassword, this, restApiDebug); m_RESTAPI->setLookups(m_ridLookup, m_tidLookup); 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 masterConf = m_conf["master"]; std::string address = masterConf["address"].as(); uint16_t port = (uint16_t)masterConf["port"].as(TRAFFIC_DEFAULT_PORT); uint32_t id = masterConf["peerId"].as(1001U); std::string password = masterConf["password"].as(); bool verbose = masterConf["verbose"].as(false); bool debug = masterConf["debug"].as(false); if (id > 999999999U) { ::LogError(LOG_HOST, "Network Peer ID cannot be greater then 999999999."); return false; } m_dmrEnabled = masterConf["allowDMRTraffic"].as(true); m_p25Enabled = masterConf["allowP25Traffic"].as(true); m_nxdnEnabled = masterConf["allowNXDNTraffic"].as(true); uint32_t parrotDelay = masterConf["parrotDelay"].as(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(true); LogInfo("Network Parameters"); LogInfo(" Peer ID: %u", id); LogInfo(" Address: %s", address.c_str()); LogInfo(" Port: %u", port); 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(" Parrot Repeat Delay: %u ms", parrotDelay); LogInfo(" Parrot Grant Demand: %s", parrotGrantDemand ? "yes" : "no"); if (verbose) { LogInfo(" Verbose: yes"); } if (debug) { LogInfo(" Debug: yes"); } // initialize networking m_network = new FNENetwork(this, address, port, id, password, debug, verbose, m_dmrEnabled, m_p25Enabled, m_nxdnEnabled, parrotDelay, parrotGrantDemand, m_allowActivityTransfer, m_allowDiagnosticTransfer, m_pingTime, m_updateLookupTime); m_network->setLookups(m_ridLookup, m_tidLookup); if (m_RESTAPI != nullptr) { m_RESTAPI->setNetwork(m_network); } bool ret = m_network->open(); if (!ret) { delete m_network; m_network = nullptr; LogError(LOG_HOST, "failed to initialize traffic networking!"); return false; } return true; } /// /// Initializes peer FNE network connectivity. /// /// bool HostFNE::createPeerNetworks() { yaml::Node& peerList = m_conf["peers"]; if (peerList.size() > 0U) { for (size_t i = 0; i < peerList.size(); i++) { yaml::Node& peerConf = peerList[i]; bool enabled = peerConf["enabled"].as(false); std::string masterAddress = peerConf["masterAddress"].as(); uint16_t masterPort = (uint16_t)peerConf["masterPort"].as(TRAFFIC_DEFAULT_PORT); std::string password = peerConf["password"].as(); uint32_t id = peerConf["peerId"].as(1001U); bool debug = peerConf["debug"].as(false); std::string identity = peerConf["identity"].as(); uint32_t rxFrequency = peerConf["rxFrequency"].as(0U); uint32_t txFrequency = peerConf["txFrequency"].as(0U); float latitude = peerConf["latitude"].as(0.0F); float longitude = peerConf["longitude"].as(0.0F); std::string location = peerConf["location"].as(); ::LogInfoEx(LOG_HOST, "Peer ID %u Master Address %s Master Port %u Identity %s Enabled %u", id, masterAddress.c_str(), masterPort, identity.c_str(), enabled); if (id > 999999999U) { ::LogError(LOG_HOST, "Network Peer ID cannot be greater then 999999999."); continue; } // initialize networking network::Network* network = new Network(masterAddress, masterPort, 0U, id, password, true, debug, m_dmrEnabled, m_p25Enabled, m_nxdnEnabled, true, true, m_allowActivityTransfer, m_allowDiagnosticTransfer, false); network->setMetadata(identity, rxFrequency, txFrequency, 0.0F, 0.0F, 0, 0, 0, latitude, longitude, 0, location); network->enable(enabled); if (enabled) { bool ret = network->open(); if (!ret) { LogError(LOG_HOST, "failed to initialize traffic networking for PEER %u", id); network->enable(false); network->close(); } } m_peerNetworks[identity] = network; } } return true; } /// /// Processes any peer network traffic. /// /// void HostFNE::processPeer(network::Network* peerNetwork) { if (peerNetwork == nullptr) return; // this shouldn't happen... if (peerNetwork->getStatus() != NET_STAT_RUNNING) return; // process DMR data if (peerNetwork->hasDMRData()) { uint32_t length = 100U; bool ret = false; UInt8Array data = peerNetwork->readDMR(ret, length); if (ret) { uint32_t peerId = peerNetwork->getPeerId(); uint32_t slotNo = (data[15U] & 0x80U) == 0x80U ? 2U : 1U; uint32_t streamId = peerNetwork->getDMRStreamId(slotNo); m_network->dmrTrafficHandler()->processFrame(data.get(), length, peerId, peerNetwork->pktLastSeq(), streamId, true); } } // process P25 data if (peerNetwork->hasP25Data()) { uint32_t length = 100U; bool ret = false; UInt8Array data = peerNetwork->readP25(ret, length); if (ret) { uint32_t peerId = peerNetwork->getPeerId(); uint32_t streamId = peerNetwork->getP25StreamId(); m_network->p25TrafficHandler()->processFrame(data.get(), length, peerId, peerNetwork->pktLastSeq(), streamId, true); } } // process NXDN data if (peerNetwork->hasNXDNData()) { uint32_t length = 100U; bool ret = false; UInt8Array data = peerNetwork->readNXDN(ret, length); if (ret) { uint32_t peerId = peerNetwork->getPeerId(); uint32_t streamId = peerNetwork->getNXDNStreamId(); m_network->nxdnTrafficHandler()->processFrame(data.get(), length, peerId, peerNetwork->pktLastSeq(), streamId, true); } } }