/* * InspIRCd -- Internet Relay Chat Daemon * * Copyright (C) 2017-2026 Sadie Powell * Copyright (C) 2013-2016 Attila Molnar * Copyright (C) 2012 Robby * Copyright (C) 2009-2010 Daniel De Graaf * Copyright (C) 2008 Robin Burchell * Copyright (C) 2008 Craig Edwards * * 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 #include "inspircd.h" #include "dynamic.h" #include "modules/extban.h" #include "utility/container.h" #include "treeserver.h" #include "utils.h" #include "link.h" #include "main.h" struct CapabDiff final { struct Config final { // The thing which differs. std::string what; // The value on the local server. std::optional local; // The value on the remote server. std::optional remote; }; // Feature with different config on the local server vs the remote server. insp::casemapped_multimap config; // Feature which is not available on the local server. std::vector localmissing; // Feature which is not available on the remote server. std::vector remotemissing; operator bool() const { return config.empty() && localmissing.empty() && remotemissing.empty(); } }; namespace { // A map which holds the difference between local and remote tokens. using TokenDiff = insp::casemapped_map, std::optional>>; // Builds a list of local capabilities. CapabData::CapabilityMap BuildCapabilityList(TreeSocket* ts) { CapabData::CapabilityMap capabilities = { { "CASEMAPPING", ServerInstance->Config->CaseMapping }, { "MAXAWAY", ConvToStr(ServerInstance->Config->Limits.MaxAway) }, { "MAXCHANNEL", ConvToStr(ServerInstance->Config->Limits.MaxChannel) }, { "MAXHOST", ConvToStr(ServerInstance->Config->Limits.MaxHost) }, { "MAXKEY", ConvToStr(ServerInstance->Config->Limits.MaxKey) }, { "MAXKICK", ConvToStr(ServerInstance->Config->Limits.MaxKick) }, { "MAXLINE", ConvToStr(ServerInstance->Config->Limits.MaxLine) }, { "MAXMODES", ConvToStr(ServerInstance->Config->Limits.MaxModes) }, { "MAXNICK", ConvToStr(ServerInstance->Config->Limits.MaxNick) }, { "MAXQUIT", ConvToStr(ServerInstance->Config->Limits.MaxQuit) }, { "MAXREAL", ConvToStr(ServerInstance->Config->Limits.MaxReal) }, { "MAXTOPIC", ConvToStr(ServerInstance->Config->Limits.MaxTopic) }, { "MAXUSER", ConvToStr(ServerInstance->Config->Limits.MaxUser) }, }; // If SHA256 hashing support is available then send a challenge token. if (ts->proto_version < PROTO_INSPIRCD_5 && ServerInstance->Modules.FindService("Hash::Provider", "sha256")) { if (ts->GetOurChallenge().empty()) ts->SetOurChallenge(ServerInstance->GenRandomStr(20)); capabilities["CHALLENGE"] = ts->GetOurChallenge(); } ExtBan::ManagerRef extbanmgr(Utils->CreatorPtr); if (extbanmgr) { std::string& xbformat = capabilities["EXTBANFORMAT"]; switch (extbanmgr->GetFormat()) { case ExtBan::Format::ANY: xbformat = "any"; break; case ExtBan::Format::NAME: xbformat = "name"; break; case ExtBan::Format::LETTER: xbformat = "letter"; break; } } return capabilities; } // Builds a list of the local extbans. CapabData::ExtBanMap BuildExtBanList() { CapabData::ExtBanMap extbans; ExtBan::ManagerRef extbanmgr(Utils->CreatorPtr); if (extbanmgr) { for (const auto& [_, extban] : extbanmgr->GetNameMap()) { CapabData::ExtBanData data; data.name = extban->GetName(); data.letter = extban->GetLetter(); switch (extban->GetType()) { case ExtBan::Type::ACTING: data.type = "acting"; break; case ExtBan::Type::MATCHING: data.type = "matching"; break; } extbans.emplace(data.name, std::move(data)); } } return extbans; } // Builds a list of the local modes of the specified type. CapabData::ModeMap BuildModeList(ModeType mt, uint16_t protocol) { CapabData::ModeMap modes; for (const auto& [name, mh] : ServerInstance->Modes.GetModes(mt)) { CapabData::ModeData data; data.letter = mh->GetModeChar(); data.name = mh->GetName(protocol < PROTO_INSPIRCD_5); const auto* const pm = mh->IsPrefixMode(); if (pm) { data.type = "prefix"; data.prefixletter = pm->GetPrefix(); data.prefixrank = pm->GetPrefixRank(); } else if (mh->IsListMode()) data.type = "list"; else if (mh->NeedsParam(true)) data.type = mh->NeedsParam(false) ? "param" : "param-set"; else data.type = "simple"; modes.emplace(data.name, std::move(data)); } return modes; } // Builds a list of the local modules with the specified property. CapabData::ModuleMap BuildModuleList(ModuleFlags property, uint16_t protocol) { CapabData::ModuleMap modules; for (const auto& [name, module] : ServerInstance->Modules.GetModules()) { if (!(module->properties & property)) continue; // Replace m_foo.dylib with foo auto startpos = name.compare(0, 2, "m_", 2) ? 0 : 2; auto endpos = name.length() - strlen(INSPIRCD_MODULE_EXT); auto modname = name.substr(startpos, endpos - startpos); if (protocol < PROTO_INSPIRCD_5) { // BEGIN COMPATIBILITY CODE if (insp::casemapped_equals(modname, "sethost") || insp::casemapped_equals(modname, "setident") || insp::casemapped_equals(modname, "setname")) { // These modules were combined in v5. modname.replace(0, 3, "chg"); } else if (insp::casemapped_equals(modname, "setidle")) { // Not VF_OPTCOMMON in v4. continue; } else if (insp::casemapped_equals(modname, "sacommands")) { // These modules were combined in v5. modules["sajoin"] = modules["sakick"] = modules["sanick"] = modules["sapart"] = modules["saquit"]; continue; } // END COMPATIBILITY CODE. } module->GetLinkData(modules[modname]); } return modules; } bool CompareCapabilities(const CapabData::CapabilityMap& remote, TreeSocket* ts, CapabDiff& diff) { // For capabilities we only compare the common keys so we can add new // tokens later without breaking compatibility. for (const auto& [tname, tvalue] : BuildCapabilityList(ts)) { auto it = remote.find(tname); if (it != remote.end() && it->second != tvalue) { diff.config.emplace("", CapabDiff::Config { .what = tname, .local = tvalue, .remote = it->second, }); } } return diff; } // Compares the lists of extbans on a remote server to the local server. bool CompareExtBans(std::optional& remote, CapabDiff& diff) { // If the remote didn't send an extban list then don't compare. if (!remote) return true; auto local = BuildExtBanList(); for (const auto& [_, data] : *remote) { auto extbaniter = local.find(data.name); if (extbaniter == local.end()) { // Only exists on the remote server. diff.localmissing.push_back(FMT::format("{} ({}:)", data.name, data.letter)); continue; } // Check that the extban config is the same. if (extbaniter->second.letter != data.letter) { diff.config.emplace(data.name, CapabDiff::Config { .what = "extban character", .local = FMT::format("+{}", extbaniter->second.letter), .remote = FMT::format("+{}", data.letter), }); } if (!insp::casemapped_equals(extbaniter->second.type, data.type)) { diff.config.emplace(data.name, CapabDiff::Config { .what = "extban type", .local = extbaniter->second.type, .remote = data.type, }); } local.erase(extbaniter); } for (const auto& [_, data] : local) { // Only exists on the local server. diff.remotemissing.push_back(FMT::format("{} ({}:)", data.name, data.letter)); } return diff; } // Compares the mode data sent by a remote server to that of the local server. void CompareModeData(const CapabData::ModeData& data, const CapabData::ModeData& otherdata, CapabDiff& diff) { if (data.letter != otherdata.letter) { diff.config.emplace(data.name, CapabDiff::Config { .what = "mode character", .local = FMT::format("+{}", data.letter), .remote = FMT::format("+{}", otherdata.letter), }); } if (!insp::casemapped_equals(data.type, otherdata.type)) { diff.config.emplace(data.name, CapabDiff::Config { .what = "mode type", .local = data.type, .remote = otherdata.type, }); } else if (insp::casemapped_equals(data.type, "prefix")) { if (data.prefixletter != otherdata.prefixletter) { diff.config.emplace(data.name, CapabDiff::Config { .what = "prefix character", .local = data.prefixletter ? ConvToStr(data.prefixletter) : "", .remote = otherdata.prefixletter ? ConvToStr(otherdata.prefixletter) : "", }); } if (data.prefixrank != otherdata.prefixrank) { diff.config.emplace(data.name, CapabDiff::Config { .what = "prefix rank", .local = ConvToStr(data.prefixrank), .remote = ConvToStr(otherdata.prefixrank), }); } } } // Compares the lists of module on a remote server to the local server. bool CompareModes(ModeType mt, std::optional& remote, uint16_t protocol, CapabDiff& diff) { // If the remote didn't send a mode list then don't compare. if (!remote) return true; auto local = BuildModeList(mt, protocol); for (const auto& [_, data] : *remote) { auto modeiter = local.find(data.name); if (modeiter == local.end()) { // Only exists on the remote server. diff.localmissing.push_back(FMT::format("{} (+{})", data.name, data.letter)); continue; } // Check that the mode config is the same. CompareModeData(modeiter->second, data, diff); local.erase(modeiter); } for (const auto& [_, data] : local) { // Only exists on the local server. diff.remotemissing.push_back(FMT::format("{} (+{})", data.name, data.letter)); } return diff; } // Compares the module data sent by a remote server to that of the local server. void CompareModuleData(const ModulePtr& mod, const Module::LinkData& otherdata, CapabDiff& diff) { Module::LinkDataDiff datadiff; mod->CompareLinkData(otherdata, datadiff); if (datadiff.empty()) return; const auto modname = ModuleManager::ShrinkModName(mod->ModuleFile); for (const auto& [key, values] : datadiff) { diff.config.emplace(modname, CapabDiff::Config { .what = key, .local = values.first, .remote = values.second, }); } } // Compares the lists of module on a remote server to the local server. bool CompareModules(ModuleFlags property, std::optional& remote, CapabDiff& diff) { // If the remote didn't send a module list then don't compare. if (!remote) return true; // Retrieve the local module list. ModuleManager::ModuleMap local; for (const auto& [name, module] : ServerInstance->Modules.GetModules()) { if (module->properties & property) local[ModuleManager::ShrinkModName(name)] = module; } for (const auto& [name, otherdata] : *remote) { auto moditer = local.find(name); if (moditer == local.end()) { // Only exists on the remote server. diff.localmissing.push_back(name); continue; } // Parse and compare the link data. CompareModuleData(moditer->second, otherdata, diff); local.erase(moditer); } for (const auto& [name, _] : local) { // Only exists on the local server. diff.remotemissing.push_back(name); } return diff; } // Generates a capability list in the format "FOO=BAR BAZ=BAX". std::string FormatCapabilities(TreeSocket* ts) { auto first = true; std::stringstream capabilitystr; for (const auto& [capkey, capvalue] : BuildCapabilityList(ts)) { if (!first) capabilitystr << ' '; capabilitystr << capkey << '=' << capvalue; first = false; } return capabilitystr.str(); } // Generates an extban list in the format "acting:foo=a matching:bar=b". std::string FormatExtBans() { std::ostringstream extbans; for (const auto& [_, data] : BuildExtBanList()) { extbans << data.type << ":" << data.name; if (data.letter) extbans << '=' << data.letter; extbans << ' '; } return extbans.str(); } // Generates a mode list in the format "simple:foo=b prefix:123:bar=c?". std::string FormatModes(ModeType mt, uint16_t protocol) { std::ostringstream modes; for (const auto& [_, data] : BuildModeList(mt, protocol)) { modes << data.type << ':'; if (insp::casemapped_equals(data.type, "prefix")) modes << data.prefixrank << ':'; modes << data.name << '='; if (data.prefixletter) modes << data.prefixletter; modes << data.letter << ' '; } return modes.str(); } // Generates a module list in the format "m_foo.so=bar m_bar.so=baz". std::string FormatModules(ModuleFlags property, uint16_t protocol) { std::ostringstream modules; for (const auto& [module, linkdata] : BuildModuleList(property, protocol)) { modules << module; if (!linkdata.empty()) modules << '=' << Percent::EncodeQuery(linkdata); modules << ' '; } return modules.str(); } void HandleDiff(TreeSocket* ts, const std::string& what, const CapabDiff& diff, bool fatal) { if (fatal) { ServerInstance->SNO.WriteToSnoMask('l', "CAPAB negotiation mismatch on link {}: {} do not match with the remote server. You will not be able to link this server until this issue is resolved.", ts->GetLinkName(), what); } else ServerInstance->SNO.WriteToSnoMask('l', "CAPAB negotiation mismatch on link {}: {} do not match with the remote server. Some functionality may behave inconsistently between servers.", ts->GetLinkName(), what); if (!diff.localmissing.empty()) ServerInstance->SNO.WriteToSnoMask('l', "Missing on the local server: {}", insp::join(diff.localmissing)); if (!diff.remotemissing.empty()) ServerInstance->SNO.WriteToSnoMask('l', "Missing on the remote server: {}", insp::join(diff.remotemissing)); if (!diff.config.empty()) { ServerInstance->SNO.WriteToSnoMask('l', "Different config between the local and remote servers:"); for (const auto& [feature, confdiff] : diff.config) { const auto localstate = confdiff.local ? confdiff.local->empty() ? "set" : FMT::format("set to {}", *confdiff.local) : "not set"; const auto remotestate = confdiff.remote ? confdiff.remote->empty() ? "set" : FMT::format("set to {}", *confdiff.remote) : "not set"; ServerInstance->SNO.WriteToSnoMask('l', " {}{}{} {} on the local server and {} on the remote server.", feature, feature.empty() ? "" : ": ", confdiff.what, localstate, remotestate); } } } template bool HandleCapabError(TreeSocket* ts, const char* format, Args&&... args) { const auto message = FMT::vformat(format, FMT::make_format_args(args...)); ts->SendError(FMT::format("CAPAB negotiation failed: {}. See the log on {} or snomasks +Ll for more details.", message, ServerInstance->Config->ServerName)); return false; } // Handles a mismatch between servers during CAPAB negotiation. bool HandleMismatch(TreeSocket* ts, const std::string& what, const CapabDiff& diff) { HandleDiff(ts, what, diff, !Utils->AllowMismatch); if (!Utils->AllowMismatch) return HandleCapabError(ts, "{} do not match and is not enabled", what); return true; } // Handles a fatal mismatch between servers during CAPAB negotiation. bool HandleMismatchFatal(TreeSocket* ts, const std::string& what, const CapabDiff& diff) { HandleDiff(ts, what, diff, true); return HandleCapabError(ts, "{} do not match", what); } // Parses a capability list in the format "FOO BAR=baz". void ParseCapabilities(const std::string& caplist, CapabData::CapabilityMap& map, TreeSocket* ts) { StringSplitter capstream(caplist); for (std::string cap; capstream.GetToken(cap); ) { std::string capval; const auto split = cap.find('='); if (split != std::string::npos) { capval.assign(cap, split + 1); cap.erase(split); } // BEGIN COMPATIBILITY CODE if (ts->proto_version < PROTO_INSPIRCD_5 && insp::casemapped_equals(cap, "CHALLENGE")) { ts->SetTheirChallenge(capval); continue; } // END COMPATIBILITY CODE ServerInstance->Logs.Debug(MODNAME, "Parsed capability: {} {}", cap, capval); map.emplace(cap, capval); } } // Parses a challenge in the format " []+ :". void ParseChallenge(const CommandBase::Params& params, std::string& out) { for (const auto& algorithm : insp::iterator_range(params.begin() + 1, params.end() - 1)) { // For now we only support HMAC-SHA-256 here. if (insp::casemapped_equals(algorithm, "hmac-sha256")) { out = Percent::Decode(params.back()); ServerInstance->Logs.Debug(MODNAME, "Parsed challenge: {:?}", out); break; } } } // Parses an extban list in the format "type:name[=char]". void ParseExtBans(const std::string& extbanlist, std::optional& out) { auto& map = out ? *out : out.emplace(); StringSplitter extbanstream(extbanlist); for (std::string extban; extbanstream.GetToken(extban); ) { CapabData::ExtBanData data; // matching:mute=m acting:noctcp // A B A const auto a = extban.find(':'); if (a == std::string::npos) continue; // Malformed. const auto b = extban.find('=', a + 1); if (b == std::string::npos) { // ExtBan only has a name. data.name = extban.substr(a + 1); } else { // ExtBan has a name and letter. data.name = extban.substr(a + 1, b - a - 1); data.letter = extban[b + 1]; } data.type = extban.substr(0, a); ServerInstance->Logs.Debug(MODNAME, "Parsed extban: type {} name {} letter {:?}", data.type, data.name, data.letter); map.emplace(data.name, std::move(data)); } } // Parses a mode list in the format "type:[rank:]name=[prefixchar][char]". void ParseModes(const std::string& modelist, std::optional& out) { auto& map = out ? *out : out.emplace(); StringSplitter modestream(modelist); for (std::string mode; modestream.GetToken(mode); ) { CapabData::ModeData data; // list:ban=b param-set:limit=l param:key=k prefix:30000:op=@o simple:noextmsg=n // A C A C A C A B C A C auto a = mode.find(':'); if (a == std::string::npos) continue; // Malformed. // If the mode is a prefix mode then it also has a rank. data.type = mode.substr(0, a); if (insp::casemapped_equals(data.type, "prefix")) { const auto b = mode.find(':', a + 1); if (b == std::string::npos) continue; // Malformed. const auto rank = mode.substr(a + 1, b - a - 1); data.prefixrank = ConvToNum(rank); a = b; } const auto c = mode.find('=', a + 1); if (c == std::string::npos) continue; // Malformed. data.name = mode.substr(a + 1, c - a - 1); switch (mode.length() - c) { case 2: data.letter = mode[c + 1]; break; case 3: data.prefixletter = mode[c + 1]; data.letter = mode[c + 2]; break; default: continue; // Malformed. } ServerInstance->Logs.Debug(MODNAME, "Parsed mode: type {} name {} letter {} prefixrank {} prefixletter {:?}", data.type, data.name, data.letter, data.prefixrank, data.prefixletter); map.emplace(data.name, std::move(data)); } } // Parses a module list in the format "m_foo.so=bar m_bar.so=baz" to a map. void ParseModules(const std::string& modlist, std::optional& out) { CapabData::ModuleMap& map = out ? *out : out.emplace(); StringSplitter modstream(modlist); for (std::string mod; modstream.GetToken(mod); ) { const auto split = mod.find('='); if (split == std::string::npos) map.emplace(mod, Module::LinkData()); // No link data. else { const auto linkdata = Percent::DecodeQuery(mod.substr(split + 1)); map.emplace(mod.substr(0, split), linkdata); } } } } void TreeSocket::SendCapabilities(int phase) { if (capab->capab_phase >= phase) return; if (capab->capab_phase < 1 && phase >= 1) { MessageBuilder("CAPAB", true) .Push("START", (uint16_t)PROTO_NEWEST) .Unicast(this); } capab->capab_phase = phase; if (phase < 2) return; std::vector algorithms; if (proto_version >= PROTO_INSPIRCD_5) { std::vector challenge(32); ServerInstance->GenRandom(challenge.data(), challenge.size()); SetOurChallenge(std::string(challenge.begin(), challenge.end())); MessageBuilder("CAPAB", true) .Push("CHALLENGE") .Push("hmac-sha256", Percent::Encode(GetOurChallenge())) .Unicast(this); } MessageBuilder("CAPAB", true) .Push("CAPABILITIES", FormatCapabilities(this)) .Unicast(this); MessageBuilder("CAPAB", true) .Push("MODULES", FormatModules(VF_COMMON, proto_version)) .Unicast(this); MessageBuilder("CAPAB", true) .Push("MODSUPPORT", FormatModules(VF_OPTCOMMON, proto_version)) .Unicast(this); MessageBuilder("CAPAB", true) .Push("CHANMODES", FormatModes(MODETYPE_CHANNEL, proto_version)) .Unicast(this); MessageBuilder("CAPAB", true) .Push("USERMODES", FormatModes(MODETYPE_USER, proto_version)) .Unicast(this); MessageBuilder("CAPAB", true) .Push("EXTBANS", FormatExtBans()) .Unicast(this); MessageBuilder("CAPAB", true) .Push("END") .Unicast(this); } /* Isolate and return the elements that are different between two comma separated lists */ void TreeSocket::ListDifference(const std::string& one, const std::string& two, char sep, std::string& mleft, std::string& mright) { std::set values; StringSplitter sepleft(one, sep); StringSplitter sepright(two, sep); std::string item; while (sepleft.GetToken(item)) { values.insert(item); } while (sepright.GetToken(item)) { if (!values.erase(item)) { mright.push_back(sep); mright.append(item); } } for (const auto& value : values) { mleft.push_back(sep); mleft.append(value); } } bool TreeSocket::Capab(const CommandBase::Params& params) { if (params.empty()) return HandleCapabError(this, "Remote server did not send a subcommand in CAPAB"); if (insp::casemapped_equals(params[0], "START")) { if (params.size() < 2) return HandleCapabError(this, "Remote server did not send a protocol version in CAPAB START"); capab->capabilities.clear(); capab->channelmodes.reset(); capab->extbans.reset(); capab->optionalmodules.reset(); capab->requiredmodules.reset(); capab->usermodes.reset(); proto_version = ConvToNum(params[1]); if (proto_version < PROTO_OLDEST) { return HandleCapabError(this, "Remote server is using protocol version {} which is too old to link with this server (protocol versions {} to {} are supported)", proto_version, (uint16_t)PROTO_OLDEST, (uint16_t)PROTO_NEWEST); } SendCapabilities(2); } else if (insp::casemapped_equals(params[0], "END")) { CapabDiff diff; if (!CompareModules(VF_COMMON, this->capab->requiredmodules, diff)) return HandleMismatchFatal(this, "Required modules", diff); else if (!CompareModes(MODETYPE_CHANNEL, this->capab->channelmodes, this->proto_version, diff)) return HandleMismatchFatal(this, "Channel modes", diff); else if (!CompareModes(MODETYPE_USER, this->capab->usermodes, this->proto_version, diff)) return HandleMismatchFatal(this, "User modes", diff); else if (!CompareModules(VF_OPTCOMMON, this->capab->optionalmodules, diff)) { if (!HandleMismatch(this, "Optional modules", diff)) return false; } else if (!CompareCapabilities(this->capab->capabilities, this, diff)) { if (!HandleMismatch(this, "Capabilities", diff)) return false; } else if (!CompareExtBans(this->capab->extbans, diff)) { if (!HandleMismatch(this, "Extended bans", diff)) return false; } if (this->LinkState == CONNECTING) { this->SendCapabilities(2); MessageBuilder("SERVER", true) .Push(ServerInstance->Config->ServerName, MakePass(capab->link->SendPass, capab->theirchallenge), ServerInstance->Config->ServerId, ServerInstance->Config->ServerDesc) .Unicast(this); } } else if (insp::casemapped_equals(params[0], "CHALLENGE")) { if (params.size() >= 3) ParseChallenge(params, capab->theirchallenge); } else if (insp::casemapped_equals(params[0], "CAPABILITIES")) { if (params.size() >= 2) ParseCapabilities(params[1], capab->capabilities, this); } else if (insp::casemapped_equals(params[0] , "MODULES")) { if (params.size() >= 2) ParseModules(params[1], capab->requiredmodules); } else if (insp::casemapped_equals(params[0], "MODSUPPORT")) { if (params.size() >= 2) ParseModules(params[1], capab->optionalmodules); } else if (insp::casemapped_equals(params[0], "CHANMODES")) { if (params.size() >= 2) ParseModes(params[1], capab->channelmodes); } else if (insp::casemapped_equals(params[0], "USERMODES")) { if (params.size() >= 2) ParseModes(params[1], capab->usermodes); } else if (insp::casemapped_equals(params[0], "EXTBANS")) { if (params.size() >= 2) ParseExtBans(params[1], capab->extbans); } return true; }