implement support for peers that identify themselves as "conventional" to ignore affiliated talkgroup rules and be able to receive all traffic if the FNE is configured to allow promiscuous operation; implement extremely preliminary support to allow a CC to claim a VC peer, allowing for appropriate grouping of peers for trunked sites;

pull/51/head
Bryan Biedenkapp 2 years ago
parent 6d738432e2
commit 0f7eabff82

@ -75,6 +75,8 @@ master:
disallowAdjStsBcast: false
# Flag indicating whether or not a P25 ADJ_STS_BCAST will pass to connected external peers.
disallowExtAdjStsBcast: true
# Flag indicating whether or not a conventional site can override affiliation rules.
allowConvSiteAffOverride: true
# Flag indicating whether or not InfluxDB logging and metrics recording is enabled.
enableInflux: false

@ -250,6 +250,30 @@ bool BaseNetwork::announceAffiliationUpdate(const std::unordered_map<uint32_t, u
return writeMaster({ NET_FUNC_ANNOUNCE, NET_ANNC_SUBFUNC_AFFILS }, buffer, 4U + (affs.size() * 8U), 0U, 0U);
}
/// <summary>
/// Writes a complete update of the peer's voice channel list to the network.
/// </summary>
/// <param name="affs"></param>
bool BaseNetwork::announceSiteVCs(const std::vector<uint32_t> peers)
{
if (m_status != NET_STAT_RUNNING && m_status != NET_STAT_MST_RUNNING)
return false;
uint8_t buffer[4U + (peers.size() * 4U)];
::memset(buffer, 0x00U, 4U + (peers.size() * 4U));
__SET_UINT32(peers.size(), buffer, 0U);
// write peer IDs to active TGID payload
uint32_t offs = 4U;
for (auto it : peers) {
__SET_UINT32(it, buffer, offs);
offs += 4U;
}
return writeMaster({ NET_FUNC_ANNOUNCE, NET_ANNC_SUBFUNC_SITE_VC }, buffer, 4U + (peers.size() * 4U), 0U, 0U);
}
/// <summary>
/// Resets the DMR ring buffer for the given slot.
/// </summary>

@ -109,6 +109,7 @@ namespace network
const uint8_t NET_ANNC_SUBFUNC_UNIT_REG = 0x01U; // Announce Unit Registration
const uint8_t NET_ANNC_SUBFUNC_UNIT_DEREG = 0x02U; // Announce Unit Deregistration
const uint8_t NET_ANNC_SUBFUNC_AFFILS = 0x90U; // Update All Affiliations
const uint8_t NET_ANNC_SUBFUNC_SITE_VC = 0x9AU; // Announce Site VCs
// ---------------------------------------------------------------------------
// Network Peer Connection Status
@ -183,6 +184,8 @@ namespace network
virtual bool announceUnitDeregistration(uint32_t srcId);
/// <summary>Writes a complete update of the peer affiliation list to the network.</summary>
virtual bool announceAffiliationUpdate(const std::unordered_map<uint32_t, uint32_t> affs);
/// <summary>Writes a complete update of the peer's voice channel list to the network.</summary>
virtual bool announceSiteVCs(const std::vector<uint32_t> peers);
/// <summary>Updates the timer by the passed number of milliseconds.</summary>
virtual void clock(uint32_t ms) = 0;

@ -95,6 +95,7 @@ FNENetwork::FNENetwork(HostFNE* host, const std::string& address, uint16_t port,
m_callInProgress(false),
m_disallowAdjStsBcast(false),
m_disallowExtAdjStsBcast(true),
m_allowConvSiteAffOverride(false),
m_enableInfluxDB(false),
m_influxServerAddress("127.0.0.1"),
m_influxServerPort(8086U),
@ -134,6 +135,7 @@ void FNENetwork::setOptions(yaml::Node& conf, bool printOptions)
{
m_disallowAdjStsBcast = conf["disallowAdjStsBcast"].as<bool>(false);
m_disallowExtAdjStsBcast = conf["disallowExtAdjStsBcast"].as<bool>(true);
m_allowConvSiteAffOverride = conf["allowConvSiteAffOverride"].as<bool>(true);
m_softConnLimit = conf["connectionLimit"].as<uint32_t>(MAX_HARD_CONN_CAP);
if (m_softConnLimit > MAX_HARD_CONN_CAP) {
@ -164,6 +166,7 @@ void FNENetwork::setOptions(yaml::Node& conf, bool printOptions)
LogWarning(LOG_NET, "NOTICE: All P25 ADJ_STS_BCAST messages will be blocked and dropped!");
}
LogInfo(" Disable P25 ADJ_STS_BCAST to external peers: %s", m_disallowExtAdjStsBcast ? "yes" : "no");
LogInfo(" Allow conventional sites to override affiliation and receive all traffic: %s", m_allowConvSiteAffOverride ? "yes" : "no");
LogInfo(" InfluxDB Reporting Enabled: %s", m_enableInfluxDB ? "yes" : "no");
if (m_enableInfluxDB) {
LogInfo(" InfluxDB Address: %s", m_influxServerAddress.c_str());
@ -682,6 +685,17 @@ void* FNENetwork::threadedNetworkRx(void* arg)
if (peerConfig["externalPeer"].is<bool>()) {
bool external = peerConfig["externalPeer"].get<bool>();
connection->isExternalPeer(external);
if (external)
LogInfoEx(LOG_NET, "PEER %u reports external peer", peerId);
}
if (peerConfig["conventionalPeer"].is<bool>()) {
if (network->m_allowConvSiteAffOverride) {
bool convPeer = peerConfig["conventionalPeer"].get<bool>();
connection->isConventionalPeer(convPeer);
if (convPeer)
LogInfoEx(LOG_NET, "PEER %u reports conventional peer", peerId);
}
}
if (peerConfig["software"].is<std::string>()) {
@ -1017,6 +1031,35 @@ void* FNENetwork::threadedNetworkRx(void* arg)
}
}
}
else if (req->fneHeader.getSubFunction() == NET_ANNC_SUBFUNC_SITE_VC) { // Announce Site VCs
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) {
FNEPeerConnection* connection = network->m_peers[peerId];
if (connection != nullptr) {
std::string ip = udp::Socket::address(req->address);
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
// update peer association
uint32_t len = __GET_UINT32(req->buffer, 0U);
uint32_t offs = 4U;
for (uint32_t i = 0; i < len; i++) {
uint32_t vcPeerId = __GET_UINT32(req->buffer, offs);
if (vcPeerId > 0 && (network->m_peers.find(vcPeerId) != network->m_peers.end())) {
FNEPeerConnection* vcConnection = network->m_peers[vcPeerId];
if (vcConnection != nullptr) {
vcConnection->ccPeerId(peerId);
}
}
offs += 4U;
}
LogMessage(LOG_NET, "PEER %u announced %u VCs", peerId, len);
}
else {
network->writePeerNAK(peerId, TAG_ANNOUNCE, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
}
}
}
else {
network->writePeerNAK(peerId, TAG_ANNOUNCE, NET_CONN_NAK_ILLEGAL_PACKET);
Utils::dump("unknown announcement opcode from the peer", req->buffer, req->length);

@ -82,6 +82,7 @@ namespace network
/// <summary>Initializes a new instance of the FNEPeerConnection class.</summary>
FNEPeerConnection() :
m_id(0U),
m_ccPeerId(0U),
m_currStreamId(0U),
m_socketStorage(),
m_sockStorageLen(0U),
@ -106,6 +107,7 @@ namespace network
/// <param name="sockStorageLen"></param>
FNEPeerConnection(uint32_t id, sockaddr_storage& socketStorage, uint32_t sockStorageLen) :
m_id(id),
m_ccPeerId(0U),
m_currStreamId(0U),
m_socketStorage(socketStorage),
m_sockStorageLen(sockStorageLen),
@ -132,6 +134,9 @@ namespace network
/// <summary>Peer ID.</summary>
__PROPERTY_PLAIN(uint32_t, id);
/// <summary>Control Channel Peer ID.</summary>
__PROPERTY_PLAIN(uint32_t, ccPeerId);
/// <summary>Current Stream ID.</summary>
__PROPERTY_PLAIN(uint32_t, currStreamId);
@ -163,6 +168,9 @@ namespace network
/// <summary>Flag indicating this connection is from an external peer.</summary>
__PROPERTY_PLAIN(bool, isExternalPeer);
/// <summary>Flag indicating this connection is from an conventional peer.</summary>
/// <remarks>This flag is specifically used to determine whether affiliation based checking is performed.</summary>
__PROPERTY_PLAIN(bool, isConventionalPeer);
/// <summary>JSON objecting containing peer configuration information.</summary>
__PROPERTY_PLAIN(json::object, config);
@ -293,6 +301,7 @@ namespace network
bool m_disallowAdjStsBcast;
bool m_disallowExtAdjStsBcast;
bool m_allowConvSiteAffOverride;
bool m_enableInfluxDB;
std::string m_influxServerAddress;

@ -611,13 +611,34 @@ bool TagDMRData::isPeerPermitted(uint32_t peerId, data::Data& data, uint32_t str
}
}
FNEPeerConnection* connection = nullptr;
if (peerId > 0 && (m_network->m_peers.find(peerId) != m_network->m_peers.end())) {
connection = m_network->m_peers[peerId];
}
// is this peer a conventional peer?
if (m_network->m_allowConvSiteAffOverride) {
if (connection != nullptr) {
if (connection->isConventionalPeer()) {
external = true; // we'll just set the external flag to disable the affiliation check
// for conventional peers
}
}
}
// is this a TG that requires affiliations to repeat?
// NOTE: external peers *always* repeat traffic regardless of affiliation
if (tg.config().affiliated() && !external) {
uint32_t lookupPeerId = peerId;
if (connection != nullptr) {
if (connection->ccPeerId() > 0U)
lookupPeerId = connection->ccPeerId();
}
// check the affiliations for this peer to see if we can repeat traffic
lookups::AffiliationLookup* aff = m_network->m_peerAffiliations[peerId];
lookups::AffiliationLookup* aff = m_network->m_peerAffiliations[lookupPeerId];
if (aff == nullptr) {
LogError(LOG_NET, "PEER %u has an invalid affiliations lookup? This shouldn't happen BUGBUG.", peerId);
LogError(LOG_NET, "PEER %u has an invalid affiliations lookup? This shouldn't happen BUGBUG.", lookupPeerId);
return false; // this will cause no traffic to pass for this peer now...I'm not sure this is good behavior
}
else {

@ -423,13 +423,34 @@ bool TagNXDNData::isPeerPermitted(uint32_t peerId, lc::RTCH& lc, uint8_t message
}
}
FNEPeerConnection* connection = nullptr;
if (peerId > 0 && (m_network->m_peers.find(peerId) != m_network->m_peers.end())) {
connection = m_network->m_peers[peerId];
}
// is this peer a conventional peer?
if (m_network->m_allowConvSiteAffOverride) {
if (connection != nullptr) {
if (connection->isConventionalPeer()) {
external = true; // we'll just set the external flag to disable the affiliation check
// for conventional peers
}
}
}
// is this a TG that requires affiliations to repeat?
// NOTE: external peers *always* repeat traffic regardless of affiliation
if (tg.config().affiliated() && !external) {
uint32_t lookupPeerId = peerId;
if (connection != nullptr) {
if (connection->ccPeerId() > 0U)
lookupPeerId = connection->ccPeerId();
}
// check the affiliations for this peer to see if we can repeat traffic
lookups::AffiliationLookup* aff = m_network->m_peerAffiliations[peerId];
lookups::AffiliationLookup* aff = m_network->m_peerAffiliations[lookupPeerId];
if (aff == nullptr) {
LogError(LOG_NET, "PEER %u has an invalid affiliations lookup? This shouldn't happen BUGBUG.", peerId);
LogError(LOG_NET, "PEER %u has an invalid affiliations lookup? This shouldn't happen BUGBUG.", lookupPeerId);
return false; // this will cause no traffic to pass for this peer now...I'm not sure this is good behavior
}
else {

@ -773,13 +773,34 @@ bool TagP25Data::isPeerPermitted(uint32_t peerId, lc::LC& control, uint8_t duid,
}
}
FNEPeerConnection* connection = nullptr;
if (peerId > 0 && (m_network->m_peers.find(peerId) != m_network->m_peers.end())) {
connection = m_network->m_peers[peerId];
}
// is this peer a conventional peer?
if (m_network->m_allowConvSiteAffOverride) {
if (connection != nullptr) {
if (connection->isConventionalPeer()) {
external = true; // we'll just set the external flag to disable the affiliation check
// for conventional peers
}
}
}
// is this a TG that requires affiliations to repeat?
// NOTE: external peers *always* repeat traffic regardless of affiliation
if (tg.config().affiliated() && !external) {
uint32_t lookupPeerId = peerId;
if (connection != nullptr) {
if (connection->ccPeerId() > 0U)
lookupPeerId = connection->ccPeerId();
}
// check the affiliations for this peer to see if we can repeat traffic
lookups::AffiliationLookup* aff = m_network->m_peerAffiliations[peerId];
lookups::AffiliationLookup* aff = m_network->m_peerAffiliations[lookupPeerId];
if (aff == nullptr) {
LogError(LOG_NET, "PEER %u has an invalid affiliations lookup? This shouldn't happen BUGBUG.", peerId);
LogError(LOG_NET, "PEER %u has an invalid affiliations lookup? This shouldn't happen BUGBUG.", lookupPeerId);
return false; // this will cause no traffic to pass for this peer now...I'm not sure this is good behavior
}
else {

@ -734,6 +734,11 @@ bool Host::createNetwork()
restApiEnableSSL = false;
}
yaml::Node protocolConf = m_conf["protocols"];
bool dmrCtrlChannel = protocolConf["dmr"]["control"]["dedicated"].as<bool>(false);
bool p25CtrlChannel = protocolConf["p25"]["control"]["dedicated"].as<bool>(false);
bool nxdnCtrlChannel = protocolConf["nxdn"]["control"]["dedicated"].as<bool>(false);
IdenTable entry = m_idenTable->find(m_channelId);
LogInfo("Network Parameters");
@ -776,21 +781,23 @@ bool Host::createNetwork()
// initialize networking
if (netEnable) {
m_network = new Network(
address, port, local,
id, password, m_duplex,
debug, m_dmrEnabled, m_p25Enabled,
m_nxdnEnabled, slot1, slot2,
allowActivityTransfer, allowDiagnosticTransfer, updateLookup,
saveLookup
);
m_network = new Network(address, port, local, id, password, m_duplex, debug, m_dmrEnabled, m_p25Enabled, m_nxdnEnabled, slot1, slot2,
allowActivityTransfer, allowDiagnosticTransfer, updateLookup, saveLookup);
m_network->setLookups(m_ridLookup, m_tidLookup);
m_network->setMetadata(m_identity, m_rxFrequency, m_txFrequency, entry.txOffsetMhz(), entry.chBandwidthKhz(), m_channelId, m_channelNo,
m_power, m_latitude, m_longitude, m_height, m_location);
if (restApiEnable) {
m_network->setRESTAPIData(restApiPassword, restApiPort);
}
if (!dmrCtrlChannel && !p25CtrlChannel && !nxdnCtrlChannel) {
if (m_controlChData.address().empty() && m_controlChData.port() == 0) {
m_network->setConventional(true);
}
}
if (encrypted) {
m_network->setPresharedKey(presharedKey);
}

@ -20,6 +20,7 @@
#include "common/Thread.h"
#include "common/ThreadFunc.h"
#include "common/Utils.h"
#include "remote/RESTClient.h"
#include "host/Host.h"
#include "ActivityLog.h"
#include "HostMain.h"
@ -87,6 +88,7 @@ Host::Host(const std::string& confFile) :
m_channelNo(0U),
m_voiceChNo(),
m_voiceChData(),
m_voiceChPeerId(),
m_controlChData(),
m_idenTable(nullptr),
m_ridLookup(nullptr),
@ -895,6 +897,10 @@ int Host::run()
nxdnFrameWriteThread.run();
nxdnFrameWriteThread.setName("nxdn:frame-w");
Timer ccRegisterTimer(1000U, 120U);
ccRegisterTimer.start();
bool hasInitialRegistered = false;
// main execution loop
while (!killed) {
if (m_modem->hasLockout() && m_state != HOST_STATE_LOCKOUT)
@ -1043,6 +1049,47 @@ int Host::run()
if (nxdn != nullptr)
nxdn->clock(ms);
ccRegisterTimer.clock(ms);
// VC -> CC presence registration
if (!m_controlChData.address().empty() && m_controlChData.port() != 0 && m_network != nullptr) {
if ((ccRegisterTimer.isRunning() && ccRegisterTimer.hasExpired()) || !hasInitialRegistered) {
LogMessage(LOG_HOST, "CC %s:%u, notifying CC of VC registration, peerId = %u", m_controlChData.address().c_str(), m_controlChData.port(), m_network->getPeerId());
hasInitialRegistered = true;
// callback REST API to release the granted TG on the specified control channel
json::object req = json::object();
req["channelNo"].set<uint32_t>(m_channelNo);
uint32_t peerId = m_network->getPeerId();
req["peerId"].set<uint32_t>(peerId);
int ret = RESTClient::send(m_controlChData.address(), m_controlChData.port(), m_controlChData.password(),
HTTP_PUT, PUT_REGISTER_CC_VC, req, m_controlChData.ssl(), REST_QUICK_WAIT, false);
if (ret != network::rest::http::HTTPPayload::StatusType::OK) {
::LogError(LOG_HOST, "failed to notify the CC %s:%u of VC registration", m_controlChData.address().c_str(), m_controlChData.port());
}
ccRegisterTimer.start();
}
}
// CC -> FNE registered VC announcement
if (m_dmrCtrlChannel || m_p25CtrlChannel || m_nxdnCtrlChannel) {
if (ccRegisterTimer.isRunning() && ccRegisterTimer.hasExpired()) {
if (m_network != nullptr && m_voiceChPeerId.size() > 0) {
LogMessage(LOG_HOST, "notifying FNE of VC registrations, peerId = %u", m_network->getPeerId());
std::vector<uint32_t> peers;
for (auto it : m_voiceChPeerId) {
peers.push_back(it.second);
}
m_network->announceSiteVCs(peers);
}
ccRegisterTimer.start();
}
}
// ------------------------------------------------------
// -- Timer Clocking --
// ------------------------------------------------------
@ -1060,7 +1107,7 @@ int Host::run()
m_nxdnBcastDurationTimer.stop();
}
LogDebug(LOG_HOST, "CW, start transmitting");
LogMessage(LOG_HOST, "CW, start transmitting");
m_isTxCW = true;
std::lock_guard<std::mutex> lock(clockingMutex);
@ -1081,7 +1128,7 @@ int Host::run()
m_modem->clock(ms);
if (!first && !m_modem->hasTX()) {
LogDebug(LOG_HOST, "CW, finished transmitting");
LogMessage(LOG_HOST, "CW, finished transmitting");
break;
}
@ -1131,10 +1178,10 @@ int Host::run()
g_fireDMRBeacon = false;
if (m_dmrTSCCData) {
LogDebug(LOG_HOST, "DMR, start CC broadcast");
LogMessage(LOG_HOST, "DMR, start CC broadcast");
}
else {
LogDebug(LOG_HOST, "DMR, roaming beacon burst");
LogMessage(LOG_HOST, "DMR, roaming beacon burst");
}
dmrBeaconIntervalTimer.start();
m_dmrBeaconDurationTimer.start();

@ -109,6 +109,7 @@ private:
std::vector<uint32_t> m_voiceChNo;
std::unordered_map<uint32_t, lookups::VoiceChData> m_voiceChData;
std::unordered_map<uint32_t, uint32_t> m_voiceChPeerId;
lookups::VoiceChData m_controlChData;
lookups::IdenTableLookup* m_idenTable;

@ -82,6 +82,7 @@ Network::Network(const std::string& address, uint16_t port, uint16_t localPort,
m_location(),
m_restApiPassword(),
m_restApiPort(0),
m_conventional(false),
m_remotePeerId(0U)
{
assert(!address.empty());
@ -811,6 +812,7 @@ bool Network::writeConfig()
rcon["port"].set<uint16_t>(m_restApiPort); // REST API Port
config["rcon"].set<json::object>(rcon);
config["conventionalPeer"].set<bool>(m_conventional); // Conventional Peer Marker
config["software"].set<std::string>(std::string(software)); // Software ID
json::value v = json::value(config);

@ -52,6 +52,8 @@ namespace network
uint8_t channelId, uint32_t channelNo, uint32_t power, float latitude, float longitude, int height, const std::string& location);
/// <summary>Sets REST API configuration settings from the modem.</summary>
void setRESTAPIData(const std::string& password, uint16_t port);
/// <summary>Sets a flag indicating whether the conventional option is sent to the FNE.</summary>
void setConventional(bool conv) { m_conventional = conv; }
/// <summary>Sets endpoint preshared encryption key.</summary>
void setPresharedKey(const uint8_t* presharedKey);
@ -120,6 +122,8 @@ namespace network
std::string m_restApiPassword;
uint16_t m_restApiPort;
bool m_conventional;
uint32_t m_remotePeerId;
/// <summary>Writes login request to the network.</summary>

@ -305,6 +305,7 @@ void RESTAPI::initializeEndpoints()
m_dispatcher.match(GET_RELEASE_GRNTS).get(REST_API_BIND(RESTAPI::restAPI_GetReleaseGrants, this));
m_dispatcher.match(GET_RELEASE_AFFS).get(REST_API_BIND(RESTAPI::restAPI_GetReleaseAffs, this));
m_dispatcher.match(PUT_REGISTER_CC_VC).put(REST_API_BIND(RESTAPI::restAPI_PutRegisterCCVC, this));
m_dispatcher.match(PUT_RELEASE_TG).put(REST_API_BIND(RESTAPI::restAPI_PutReleaseGrant, this));
m_dispatcher.match(PUT_TOUCH_TG).put(REST_API_BIND(RESTAPI::restAPI_PutTouchGrant, this));
@ -1146,6 +1147,56 @@ void RESTAPI::restAPI_GetReleaseAffs(const HTTPPayload& request, HTTPPayload& re
}
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="reply"></param>
/// <param name="match"></param>
void RESTAPI::restAPI_PutRegisterCCVC(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match)
{
if (!validateAuth(request, reply)) {
return;
}
json::object req = json::object();
if (!parseRequestBody(request, reply, req)) {
return;
}
errorPayload(reply, "OK", HTTPPayload::OK);
if (!m_host->m_dmrTSCCData && !m_host->m_p25CCData && !m_host->m_nxdnCCData) {
errorPayload(reply, "Host is not a control channel, cannot register voice channel");
return;
}
// validate channelNo is a string within the JSON blob
if (!req["channelNo"].is<int>()) {
errorPayload(reply, "channelNo was not a valid integer");
return;
}
uint32_t channelNo = req["channelNo"].get<uint32_t>();
// validate channelNo is a string within the JSON blob
if (!req["peerId"].is<int>()) {
errorPayload(reply, "peerId was not a valid integer");
return;
}
uint32_t peerId = req["peerId"].get<uint32_t>();
// LogDebug(LOG_REST, "restAPI_PutRegisterCCVC(): callback, channelNo = %u, peerId = %u", channelNo, peerId);
if (m_host->m_voiceChData.find(channelNo) != m_host->m_voiceChData.end()) {
::lookups::VoiceChData voiceCh = m_host->m_voiceChData[channelNo];
m_host->m_voiceChPeerId[channelNo] = peerId;
LogMessage(LOG_REST, "VC %s:%u, registration notice, peerId = %u, chId = %u, chNo = %u", voiceCh.address().c_str(), voiceCh.port(), peerId, voiceCh.chId(), channelNo);
}
}
/// <summary>
///
/// </summary>

@ -125,6 +125,8 @@ private:
/// <summary></summary>
void restAPI_GetReleaseAffs(const HTTPPayload& request, HTTPPayload& reply, const network::rest::RequestMatch& match);
/// <summary></summary>
void restAPI_PutRegisterCCVC(const HTTPPayload& request, HTTPPayload& reply, const network::rest::RequestMatch& match);
/// <summary></summary>
void restAPI_PutReleaseGrant(const HTTPPayload& request, HTTPPayload& reply, const network::rest::RequestMatch& match);
/// <summary></summary>

@ -51,6 +51,7 @@
#define GET_RELEASE_GRNTS "/release-grants"
#define GET_RELEASE_AFFS "/release-affs"
#define PUT_REGISTER_CC_VC "/register-cc-vc"
#define PUT_RELEASE_TG "/release-tg-grant"
#define PUT_TOUCH_TG "/touch-tg-grant"

Loading…
Cancel
Save

Powered by TurnKey Linux.