mirror of
https://github.com/inspircd/inspircd.git
synced 2025-03-09 10:39:02 -04:00
Add the core of the new cloak implementation.
This commit is contained in:
parent
607ee6ed65
commit
5da15b0c5a
@ -508,6 +508,15 @@
|
||||
# mass G/Z-line all users on a channel using /CLEARCHAN.
|
||||
#<module name="clearchan">
|
||||
|
||||
#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
|
||||
# Cloak module: Adds user mode x (cloak) which allows user hostnames to
|
||||
# be hidden. This module does not provide any cloak methods by itself.
|
||||
# You should also load another module like cloak_md5 or cloak_sha256.
|
||||
#
|
||||
# In order to have users automatically cloaked on connect you should
|
||||
# load the conn_umodes module and add "x" to <connect:modes>.
|
||||
#<module name="cloak">
|
||||
|
||||
#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
|
||||
# Cloaking module: Adds user mode +x and cloaking support.
|
||||
# Relies on the md5 module being loaded.
|
||||
|
142
include/modules/cloak.h
Normal file
142
include/modules/cloak.h
Normal file
@ -0,0 +1,142 @@
|
||||
/*
|
||||
* InspIRCd -- Internet Relay Chat Daemon
|
||||
*
|
||||
* Copyright (C) 2023 Sadie Powell <sadie@witchery.services>
|
||||
*
|
||||
* This file is part of InspIRCd. InspIRCd 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.
|
||||
*
|
||||
* 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, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Cloak
|
||||
{
|
||||
class Engine;
|
||||
class Method;
|
||||
|
||||
/** A shared pointer to a cloak method. */
|
||||
typedef std::shared_ptr<Method> MethodPtr;
|
||||
|
||||
/** Takes a hostname and retrieves the part which should be visible.
|
||||
*
|
||||
* This is usually the last \p hostparts segments but if not enough are
|
||||
* present then all but the most specific segments are used. If the domain
|
||||
* name consists of one label only then none are used.
|
||||
*
|
||||
* Here are some examples for how domain names will be shortened assuming
|
||||
* \p domainparts is set to the default of 3.
|
||||
*
|
||||
* "this.is.an.example.com" => "an.example.com"
|
||||
* "an.example.com" => "example.com"
|
||||
* "example.com" => "com"
|
||||
* "localhost" => ""
|
||||
*
|
||||
* "/var/run/inspircd/client.sock" => "run/inspircd/client.sock"
|
||||
* "/run/inspircd/client.sock" => "inspircd/client.sock"
|
||||
* "/inspircd/client.sock" => "client.sock"
|
||||
* "/client.sock" => ""
|
||||
*
|
||||
* @param host The hostname to cloak.
|
||||
* @param domainparts The number of host labels that should be visible.
|
||||
* @return The visible segment of the hostname.
|
||||
*/
|
||||
inline std::string VisiblePart(const std::string& host, size_t hostparts, char separator);
|
||||
}
|
||||
|
||||
/** Base class for cloak engines. */
|
||||
class CoreExport Cloak::Engine
|
||||
: public DataProvider
|
||||
{
|
||||
protected:
|
||||
Engine(Module* Creator, const std::string& Name)
|
||||
: DataProvider(Creator, "cloak/" + Name)
|
||||
{
|
||||
}
|
||||
|
||||
public:
|
||||
/** Creates a new cloak method from the specified config.
|
||||
* @param tag The config tag to configure the cloak method with.
|
||||
* @param primary Whether the created cloak method is the primary emthod.
|
||||
*/
|
||||
virtual MethodPtr Create(const std::shared_ptr<ConfigTag>& tag, bool primary) = 0;
|
||||
};
|
||||
|
||||
/** Base class for cloak methods. */
|
||||
class Cloak::Method
|
||||
: public Cullable
|
||||
{
|
||||
private:
|
||||
/** The name of the engine that created this method. */
|
||||
std::string provname;
|
||||
|
||||
protected:
|
||||
Method(const Engine* engine) ATTR_NOT_NULL(2)
|
||||
: provname(engine->name)
|
||||
{
|
||||
}
|
||||
|
||||
public:
|
||||
/** Generates a cloak for the specified user.
|
||||
* @param user The user to generate a cloak for.
|
||||
*/
|
||||
virtual std::string Generate(LocalUser* user) ATTR_NOT_NULL(2) = 0;
|
||||
|
||||
/** Generates a cloak for the specified hostname, IP address, or UNIX socket path.
|
||||
* @param user The hostname, IP address, or UNIX socket path to generate a cloak for.
|
||||
*/
|
||||
virtual std::string Generate(const std::string& hostip) = 0;
|
||||
|
||||
/** Retrieves link compatibility data for this cloak method.
|
||||
* @param data The location to store link compatibility data.
|
||||
* @param compatdata The location to store link compatibility data for older protocols.
|
||||
*/
|
||||
virtual void GetLinkData(Module::LinkData& data, std::string& compatdata) = 0;
|
||||
|
||||
/** Determines whether this method is provided by the specified service provider.
|
||||
* @param prov The service provider to check.
|
||||
*/
|
||||
bool IsProvidedBy(const ServiceProvider& prov) const
|
||||
{
|
||||
return prov.name == provname;
|
||||
}
|
||||
};
|
||||
|
||||
inline std::string Cloak::VisiblePart(const std::string& host, size_t hostparts, char separator)
|
||||
{
|
||||
// The position at which we found the last separator.
|
||||
std::string::const_reverse_iterator seppos;
|
||||
|
||||
// The number of separators we have seen so far.
|
||||
size_t seenseps = 0;
|
||||
|
||||
for (std::string::const_reverse_iterator it = host.rbegin(); it != host.rend(); ++it)
|
||||
{
|
||||
if (*it != separator)
|
||||
continue;
|
||||
|
||||
// We have found a separator!
|
||||
seppos = it;
|
||||
seenseps += 1;
|
||||
|
||||
// Do we have enough segments to stop?
|
||||
if (seenseps >= hostparts)
|
||||
break;
|
||||
}
|
||||
|
||||
// We only returns a domain part if more than one label is
|
||||
// present. See above for a full explanation.
|
||||
if (!seenseps)
|
||||
return "";
|
||||
|
||||
return std::string(seppos.base(), host.end());
|
||||
}
|
335
src/modules/m_cloak.cpp
Normal file
335
src/modules/m_cloak.cpp
Normal file
@ -0,0 +1,335 @@
|
||||
/*
|
||||
* InspIRCd -- Internet Relay Chat Daemon
|
||||
*
|
||||
* Copyright (C) 2023 Sadie Powell <sadie@witchery.services>
|
||||
*
|
||||
* This file is part of InspIRCd. InspIRCd 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.
|
||||
*
|
||||
* 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, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
#include "inspircd.h"
|
||||
#include "clientprotocolevent.h"
|
||||
#include "modules/cloak.h"
|
||||
#include "modules/ircv3_replies.h"
|
||||
|
||||
typedef std::vector<Cloak::MethodPtr> CloakMethodList;
|
||||
|
||||
class CommandCloak final
|
||||
: public SplitCommand
|
||||
{
|
||||
private:
|
||||
// The cloak engines from the config.
|
||||
CloakMethodList& cloakmethods;
|
||||
|
||||
// API for sending a NOTE message.
|
||||
IRCv3::Replies::Note noterpl;
|
||||
|
||||
// Reference to the inspircd.org/standard-replies csp.
|
||||
IRCv3::Replies::CapReference stdrplcap;
|
||||
|
||||
public:
|
||||
CommandCloak(Module* Creator, CloakMethodList& ce)
|
||||
: SplitCommand(Creator, "CLOAK", 1)
|
||||
, cloakmethods(ce)
|
||||
, noterpl(Creator)
|
||||
, stdrplcap(Creator)
|
||||
{
|
||||
access_needed = CmdAccess::OPERATOR;
|
||||
syntax = { "<host>" };
|
||||
}
|
||||
|
||||
CmdResult HandleLocal(LocalUser* user, const Params& parameters) override
|
||||
{
|
||||
size_t count = 0;
|
||||
for (const auto& cloakmethod : cloakmethods)
|
||||
{
|
||||
const std::string cloak = cloakmethod->Generate(parameters[0]);
|
||||
if (cloak.empty())
|
||||
continue;
|
||||
|
||||
noterpl.SendIfCap(user, stdrplcap, this, "CLOAK_RESULT", parameters[0], cloak, InspIRCd::Format("Cloak #%zu for %s is %s",
|
||||
++count, parameters[0].c_str(), cloak.c_str()));
|
||||
}
|
||||
return CmdResult::SUCCESS;
|
||||
}
|
||||
};
|
||||
|
||||
typedef std::vector<std::string> CloakList;
|
||||
|
||||
class CloakMode final
|
||||
: public ModeHandler
|
||||
{
|
||||
private:
|
||||
// The number of times the last user has set/unset this mode at once.
|
||||
size_t prevcount = 0;
|
||||
|
||||
// The time at which the last user set/unset this mode.
|
||||
time_t prevtime = 0;
|
||||
|
||||
// The UUID of the last user to set/unset this mode.
|
||||
std::string prevuuid;
|
||||
|
||||
bool CheckSpam(User* user)
|
||||
{
|
||||
if (user->uuid != prevuuid || prevtime != ServerInstance->Time())
|
||||
{
|
||||
// The user has changed this mode already recently. Have they done
|
||||
// it too much?
|
||||
return ++prevcount > 2;
|
||||
}
|
||||
|
||||
// This is the first time the user has executed the mode recently so its fine.
|
||||
prevcount = 0;
|
||||
prevtime = ServerInstance->Time();
|
||||
prevuuid = user->uuid;
|
||||
return false;
|
||||
}
|
||||
|
||||
public:
|
||||
// Whether the mode has recently been changed.
|
||||
bool active = false;
|
||||
|
||||
// The cloak providers from the config.
|
||||
CloakMethodList& cloakmethods;
|
||||
|
||||
// Holds the list of cloaks for a user.
|
||||
ListExtItem<CloakList> ext;
|
||||
|
||||
CloakMode(Module* Creator, CloakMethodList& ce)
|
||||
: ModeHandler(Creator, "cloak", 'x', PARAM_NONE, MODETYPE_USER)
|
||||
, cloakmethods(ce)
|
||||
, ext(Creator, "cloaks", ExtensionType::USER)
|
||||
{
|
||||
}
|
||||
|
||||
CloakList* GetCloaks(LocalUser* user)
|
||||
{
|
||||
auto* cloaks = ext.Get(user);
|
||||
if (!cloaks)
|
||||
{
|
||||
// The list doesn't exist so try to create it.
|
||||
cloaks = new CloakList();
|
||||
for (const auto& cloakmethod : cloakmethods)
|
||||
{
|
||||
const std::string cloak = cloakmethod->Generate(user);
|
||||
if (!cloak.empty())
|
||||
cloaks->push_back(cloak);
|
||||
}
|
||||
ext.Set(user, cloaks);
|
||||
}
|
||||
return cloaks->empty() ? nullptr : cloaks;
|
||||
}
|
||||
|
||||
ModeAction OnModeChange(User* source, User* dest, Channel* channel, Modes::Change& change) override
|
||||
{
|
||||
// For remote users blindly allow this
|
||||
LocalUser* user = IS_LOCAL(dest);
|
||||
if (!user)
|
||||
{
|
||||
// Remote setters broadcast mode before host while local setters do the opposite.
|
||||
active = IS_LOCAL(source) ? change.adding : !change.adding;
|
||||
dest->SetMode(this, change.adding);
|
||||
return MODEACTION_ALLOW;
|
||||
}
|
||||
|
||||
// Don't allow the mode change if its a no-op or a spam change.
|
||||
if (change.adding == user->IsModeSet(this) || CheckSpam(user))
|
||||
return MODEACTION_DENY;
|
||||
|
||||
// Penalise changing the mode to avoid spam.
|
||||
if (source == dest)
|
||||
user->CommandFloodPenalty += 5'000;
|
||||
|
||||
if (!change.adding)
|
||||
{
|
||||
// Remove the mode and restore their real host.
|
||||
user->SetMode(this, false);
|
||||
user->ChangeDisplayedHost(user->GetRealHost());
|
||||
return MODEACTION_ALLOW;
|
||||
}
|
||||
|
||||
// If a user is not fully connected and their displayed hostname is
|
||||
// different to their real hostname they probably had a vhost set on
|
||||
// them by services. We should avoid automatically setting cloak on
|
||||
// them in this case.
|
||||
if (!user->IsFullyConnected() && user->GetRealHost() != user->GetDisplayedHost())
|
||||
return MODEACTION_DENY;
|
||||
|
||||
auto* cloaks = GetCloaks(user);
|
||||
if (cloaks)
|
||||
{
|
||||
// We were able to generate cloaks for this user.
|
||||
user->ChangeDisplayedHost(cloaks->front());
|
||||
user->SetMode(this, true);
|
||||
return MODEACTION_ALLOW;
|
||||
}
|
||||
return MODEACTION_DENY;
|
||||
}
|
||||
};
|
||||
|
||||
class ModuleCloakSHA256 final
|
||||
: public Module
|
||||
{
|
||||
private:
|
||||
CloakMethodList cloakmethods;
|
||||
CommandCloak cloakcmd;
|
||||
CloakMode cloakmode;
|
||||
|
||||
void DisableMode(User* user)
|
||||
{
|
||||
user->SetMode(cloakmode, false);
|
||||
|
||||
auto* luser = IS_LOCAL(user);
|
||||
if (luser)
|
||||
{
|
||||
Modes::ChangeList changelist;
|
||||
changelist.push_remove(&cloakmode);
|
||||
ClientProtocol::Events::Mode modeevent(ServerInstance->FakeClient, nullptr, luser, changelist);
|
||||
luser->Send(modeevent);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
ModuleCloakSHA256()
|
||||
: Module(VF_VENDOR | VF_COMMON, "Adds user mode x (cloak) which allows user hostnames to be hidden.")
|
||||
, cloakcmd(this, cloakmethods)
|
||||
, cloakmode(this, cloakmethods)
|
||||
{
|
||||
}
|
||||
|
||||
void ReadConfig(ConfigStatus& status) override
|
||||
{
|
||||
auto tags = ServerInstance->Config->ConfTags("cloak");
|
||||
if (tags.empty())
|
||||
throw ModuleException(this, "You have loaded the cloak module but not configured any <cloak> tags!");
|
||||
|
||||
bool primary = true;
|
||||
CloakMethodList newcloakmethods;
|
||||
for (const auto& [_, tag] : tags)
|
||||
{
|
||||
const std::string method = tag->getString("method", tag->getString("mode"));
|
||||
if (method.empty())
|
||||
throw ModuleException(this, "<cloak:method> must be set to the name of a cloak engine, at " + tag->source.str());
|
||||
|
||||
auto* service = ServerInstance->Modules.FindDataService<Cloak::Engine>("cloak/" + method);
|
||||
if (!service)
|
||||
throw ModuleException(this, "<cloak> tag was set to non-existent cloak method \"" + method + "\", at " + tag->source.str());
|
||||
|
||||
newcloakmethods.push_back(service->Create(tag, primary));
|
||||
primary = false;
|
||||
}
|
||||
|
||||
// The cloak configuration was valid so we can apply it.
|
||||
cloakmethods.swap(newcloakmethods);
|
||||
}
|
||||
|
||||
void GetLinkData(Module::LinkData& data, std::string& compatdata) override
|
||||
{
|
||||
if (!cloakmethods.empty())
|
||||
cloakmethods.front()->GetLinkData(data, compatdata);
|
||||
}
|
||||
|
||||
void Prioritize() override
|
||||
{
|
||||
ServerInstance->Modules.SetPriority(this, I_OnCheckBan, PRIORITY_LAST);
|
||||
}
|
||||
|
||||
void OnChangeHost(User* user, const std::string& host) override
|
||||
{
|
||||
if (user->IsModeSet(cloakmode) && !cloakmode.active)
|
||||
DisableMode(user);
|
||||
|
||||
cloakmode.active = false;
|
||||
}
|
||||
|
||||
void OnChangeRemoteAddress(LocalUser* user) override
|
||||
{
|
||||
// Connecting users are handled in OnUserConnect not here.
|
||||
if (!user->IsFullyConnected() || user->quitting)
|
||||
return;
|
||||
|
||||
// Remove the cloaks so we can generate new ones.
|
||||
cloakmode.ext.Unset(user);
|
||||
|
||||
// If a user is using a cloak then update it.
|
||||
auto* cloaks = cloakmode.GetCloaks(user);
|
||||
if (user->IsModeSet(cloakmode))
|
||||
{
|
||||
if (cloaks)
|
||||
{
|
||||
// The user has a new cloak list; pick the first.
|
||||
user->ChangeDisplayedHost(cloaks->front());
|
||||
}
|
||||
else
|
||||
{
|
||||
// The user has no cloak list; unset mode and revert to the real host.
|
||||
DisableMode(user);
|
||||
user->ChangeDisplayedHost(user->GetRealHost());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ModResult OnCheckBan(User* user, Channel* chan, const std::string& mask) override
|
||||
{
|
||||
LocalUser* lu = IS_LOCAL(user);
|
||||
if (!lu)
|
||||
return MOD_RES_PASSTHRU; // We don't have cloaks for remote users.
|
||||
|
||||
auto* cloaks = cloakmode.GetCloaks(lu);
|
||||
if (!cloaks)
|
||||
return MOD_RES_PASSTHRU; // No cloaks, nothing to check.
|
||||
|
||||
// Check if they have a cloaked host but are not using it.
|
||||
for (const auto& cloak : *cloaks)
|
||||
{
|
||||
if (cloak == user->GetDisplayedHost())
|
||||
continue; // This is checked by the core.
|
||||
|
||||
const std::string cloakmask = user->nick + "!" + user->ident + "@" + cloak;
|
||||
if (InspIRCd::Match(cloakmask, mask))
|
||||
return MOD_RES_DENY;
|
||||
}
|
||||
return MOD_RES_PASSTHRU;
|
||||
}
|
||||
|
||||
void OnServiceDel(ServiceProvider& service) override
|
||||
{
|
||||
size_t methods = 0;
|
||||
for (auto it = cloakmethods.begin(); it != cloakmethods.end(); )
|
||||
{
|
||||
auto cloakmethod = *it;
|
||||
if (cloakmethod->IsProvidedBy(service))
|
||||
{
|
||||
it = cloakmethods.erase(it);
|
||||
methods++;
|
||||
continue;
|
||||
}
|
||||
it++;
|
||||
}
|
||||
|
||||
if (methods)
|
||||
{
|
||||
ServerInstance->SNO.WriteGlobalSno('a', "The %s hash provider was unloaded; removing %zu cloak methods until the next rehash.",
|
||||
service.name.substr(6).c_str(), methods);
|
||||
}
|
||||
}
|
||||
|
||||
void OnUserConnect(LocalUser* user) override
|
||||
{
|
||||
// Generate cloaks now if they do not already exist so opers can /CHECK
|
||||
// this user if need be.
|
||||
cloakmode.GetCloaks(user);
|
||||
}
|
||||
};
|
||||
|
||||
MODULE_INIT(ModuleCloakSHA256)
|
Loading…
x
Reference in New Issue
Block a user