diff --git a/docs/conf/modules.conf.example b/docs/conf/modules.conf.example index d9dc5d511..d019906b6 100644 --- a/docs/conf/modules.conf.example +++ b/docs/conf/modules.conf.example @@ -508,6 +508,15 @@ # mass G/Z-line all users on a channel using /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 . +# + #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# # Cloaking module: Adds user mode +x and cloaking support. # Relies on the md5 module being loaded. diff --git a/include/modules/cloak.h b/include/modules/cloak.h new file mode 100644 index 000000000..fff884bfa --- /dev/null +++ b/include/modules/cloak.h @@ -0,0 +1,142 @@ +/* + * InspIRCd -- Internet Relay Chat Daemon + * + * Copyright (C) 2023 Sadie Powell + * + * 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 . + */ + + +#pragma once + +namespace Cloak +{ + class Engine; + class Method; + + /** A shared pointer to a cloak method. */ + typedef std::shared_ptr 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& 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()); +} diff --git a/src/modules/m_cloak.cpp b/src/modules/m_cloak.cpp new file mode 100644 index 000000000..f7ef8fdb8 --- /dev/null +++ b/src/modules/m_cloak.cpp @@ -0,0 +1,335 @@ +/* + * InspIRCd -- Internet Relay Chat Daemon + * + * Copyright (C) 2023 Sadie Powell + * + * 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 . + */ + + +#include "inspircd.h" +#include "clientprotocolevent.h" +#include "modules/cloak.h" +#include "modules/ircv3_replies.h" + +typedef std::vector 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 = { "" }; + } + + 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 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 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 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, " must be set to the name of a cloak engine, at " + tag->source.str()); + + auto* service = ServerInstance->Modules.FindDataService("cloak/" + method); + if (!service) + throw ModuleException(this, " 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)