diff --git a/include/llarp.hpp b/include/llarp.hpp index 6fa31da11..dbf43925e 100644 --- a/include/llarp.hpp +++ b/include/llarp.hpp @@ -66,6 +66,10 @@ namespace llarp void Configure(Config conf); + /// handle SIGHUP + void + Reload(); + bool IsUp() const; @@ -92,7 +96,7 @@ namespace llarp makeRouter(llarp_ev_loop_ptr __netloop, std::shared_ptr logic); protected: - std::unique_ptr config = nullptr; + std::shared_ptr config = nullptr; private: void diff --git a/llarp/config/config.cpp b/llarp/config/config.cpp index fe258904a..276425f13 100644 --- a/llarp/config/config.cpp +++ b/llarp/config/config.cpp @@ -547,6 +547,18 @@ namespace llarp "logging", "file", false, DefaultLogFile, AssignmentAcceptor(m_logFile)); } + void + Config::Save() const + { + m_Parser.Save(); + } + + void + Config::Override(std::string section, std::string key, std::string value) + { + m_Parser.AddOverride(std::move(section), std::move(key), std::move(value)); + } + bool Config::Load(const fs::path fname, bool isRelay, fs::path defaultDataDir) { @@ -559,14 +571,13 @@ namespace llarp ConfigDefinition conf; initializeConfig(conf, params); addBackwardsCompatibleConfigOptions(conf); - - ConfigParser parser; - if (!parser.LoadFile(fname)) + m_Parser.Clear(); + if (!m_Parser.LoadFile(fname)) { return false; } - parser.IterAll([&](std::string_view section, const SectionValues_t& values) { + m_Parser.IterAll([&](std::string_view section, const SectionValues_t& values) { for (const auto& pair : values) { conf.addConfigValue(section, pair.first, pair.second); diff --git a/llarp/config/config.hpp b/llarp/config/config.hpp index ae22cdd69..53d9cb93f 100644 --- a/llarp/config/config.hpp +++ b/llarp/config/config.hpp @@ -224,6 +224,15 @@ namespace llarp std::string generateBaseRouterConfig(fs::path defaultDataDir); + + void + Save() const; + + void + Override(std::string section, std::string key, std::string value); + + private: + ConfigParser m_Parser; }; void diff --git a/llarp/config/ini.cpp b/llarp/config/ini.cpp index 71f9e05e6..6d3eb0970 100644 --- a/llarp/config/ini.cpp +++ b/llarp/config/ini.cpp @@ -40,6 +40,7 @@ namespace llarp void ConfigParser::Clear() { + m_Overrides.clear(); m_Config.clear(); m_Data.clear(); } @@ -162,4 +163,32 @@ namespace llarp return false; return visit(itr->second); } + + void + ConfigParser::AddOverride(std::string section, std::string key, std::string value) + { + m_Overrides[section].emplace(key, value); + } + + void + ConfigParser::Save() const + { + // if we have no overrides keep the config the same on disk + if (m_Overrides.empty()) + return; + std::ofstream ofs(m_FileName); + // write existing config data + ofs.write(m_Data.data(), m_Data.size()); + // write overrides + ofs << std::endl << std::endl << "# overrides" << std::endl; + for (const auto& [section, values] : m_Overrides) + { + ofs << std::endl << "[" << section << "]" << std::endl; + for (const auto& [key, value] : values) + { + ofs << key << "=" << value << std::endl; + } + } + } + } // namespace llarp diff --git a/llarp/config/ini.hpp b/llarp/config/ini.hpp index 2934567fe..c9a8637a0 100644 --- a/llarp/config/ini.hpp +++ b/llarp/config/ini.hpp @@ -40,12 +40,21 @@ namespace llarp bool VisitSection(const char* name, std::function visit) const; + /// add a config option that is appended at the end of the config buffer with no comments + void + AddOverride(std::string section, std::string key, std::string value); + + /// save config and any overrides to the file it was loaded from + void + Save() const; + private: bool Parse(); std::vector m_Data; Config_impl_t m_Config; + Config_impl_t m_Overrides; fs::path m_FileName; }; diff --git a/llarp/context.cpp b/llarp/context.cpp index 565eaf3b8..a2ac2a8a4 100644 --- a/llarp/context.cpp +++ b/llarp/context.cpp @@ -33,7 +33,7 @@ namespace llarp if (nullptr != config.get()) throw std::runtime_error("Config already exists"); - config = std::make_unique(std::move(conf)); + config = std::make_shared(std::move(conf)); logic = std::make_shared(); @@ -85,7 +85,7 @@ namespace llarp nodedb = std::make_unique( nodedb_dir, [r = router.get()](auto call) { r->QueueDiskIO(std::move(call)); }); - if (!router->Configure(*config.get(), opts.isRouter, nodedb.get())) + if (!router->Configure(config, opts.isRouter, nodedb.get())) throw std::runtime_error("Failed to configure router"); // must be done after router is made so we can use its disk io worker @@ -157,8 +157,17 @@ namespace llarp { SigINT(); } - // TODO: Hot reloading would be kewl - // (it used to exist here, but wasn't maintained) +#ifndef _WIN32 + if (sig == SIGHUP) + { + Reload(); + } +#endif + } + + void + Context::Reload() + { } void @@ -182,7 +191,7 @@ namespace llarp Context::Close() { llarp::LogDebug("free config"); - config.release(); + config.reset(); llarp::LogDebug("free nodedb"); nodedb.release(); diff --git a/llarp/router/abstractrouter.hpp b/llarp/router/abstractrouter.hpp index ee73acfe5..d32684e4d 100644 --- a/llarp/router/abstractrouter.hpp +++ b/llarp/router/abstractrouter.hpp @@ -1,6 +1,7 @@ #ifndef LLARP_ABSTRACT_ROUTER_HPP #define LLARP_ABSTRACT_ROUTER_HPP +#include #include #include #include @@ -134,6 +135,12 @@ namespace llarp /// call function in disk io thread virtual void QueueDiskIO(std::function) = 0; + virtual std::shared_ptr + GetConfig() const + { + return nullptr; + } + virtual service::Context& hiddenServiceContext() = 0; @@ -159,7 +166,7 @@ namespace llarp Sign(Signature& sig, const llarp_buffer_t& buf) const = 0; virtual bool - Configure(const Config& conf, bool isRouter, llarp_nodedb* nodedb) = 0; + Configure(std::shared_ptr conf, bool isRouter, llarp_nodedb* nodedb) = 0; virtual bool IsServiceNode() const = 0; diff --git a/llarp/router/router.cpp b/llarp/router/router.cpp index 413eef46a..b1254fd7d 100644 --- a/llarp/router/router.cpp +++ b/llarp/router/router.cpp @@ -262,8 +262,10 @@ namespace llarp } bool - Router::Configure(const Config& conf, bool isRouter, llarp_nodedb* nodedb) + Router::Configure(std::shared_ptr c, bool isRouter, llarp_nodedb* nodedb) { + m_Config = c; + auto& conf = *m_Config; whitelistRouters = conf.lokid.whitelistRouters; if (whitelistRouters) lokidRPCAddr = lokimq::address(conf.lokid.lokidRPCAddr); diff --git a/llarp/router/router.hpp b/llarp/router/router.hpp index d30fd75ec..f83e919e0 100644 --- a/llarp/router/router.hpp +++ b/llarp/router/router.hpp @@ -353,7 +353,7 @@ namespace llarp Close(); bool - Configure(const Config& conf, bool isRouter, llarp_nodedb* nodedb = nullptr) override; + Configure(std::shared_ptr conf, bool isRouter, llarp_nodedb* nodedb = nullptr) override; bool StartRpcServer() override; @@ -499,6 +499,14 @@ namespace llarp void AfterStopIssued(); + std::shared_ptr m_Config; + + std::shared_ptr + GetConfig() const override + { + return m_Config; + } + private: std::atomic _stopping; std::atomic _running; diff --git a/llarp/rpc/rpc_server.cpp b/llarp/rpc/rpc_server.cpp index 07c51e773..7ab1644a0 100644 --- a/llarp/rpc/rpc_server.cpp +++ b/llarp/rpc/rpc_server.cpp @@ -110,131 +110,175 @@ namespace llarp::rpc auto ftr = result.get_future(); msg.send_reply(CreateJSONResponse(ftr.get())); }) - .add_request_command("exit", [&](lokimq::Message& msg) { + .add_request_command( + "exit", + [&](lokimq::Message& msg) { + HandleJSONRequest(msg, [r = m_Router](nlohmann::json obj, ReplyFunction_t reply) { + if (r->IsServiceNode()) + { + reply(CreateJSONError("not supported")); + return; + } + std::optional exit; + IPRange range; + bool map = true; + const auto exit_itr = obj.find("exit"); + if (exit_itr != obj.end()) + { + service::Address addr; + if (not addr.FromString(exit_itr->get())) + { + reply(CreateJSONError("invalid exit address")); + return; + } + exit = addr; + } + + const auto unmap_itr = obj.find("unmap"); + if (unmap_itr != obj.end() and unmap_itr->get()) + { + map = false; + } + + const auto range_itr = obj.find("range"); + if (range_itr == obj.end()) + { + range.FromString("0.0.0.0/0"); + } + else if (not range.FromString(range_itr->get())) + { + reply(CreateJSONError("invalid ip range")); + return; + } + std::optional token; + const auto token_itr = obj.find("token"); + if (token_itr != obj.end()) + { + token = token_itr->get(); + } + + std::string endpoint = "default"; + const auto endpoint_itr = obj.find("endpoint"); + if (endpoint_itr != obj.end()) + { + endpoint = endpoint_itr->get(); + } + LogicCall(r->logic(), [map, exit, range, token, endpoint, r, reply]() { + auto ep = r->hiddenServiceContext().GetEndpointByName(endpoint); + if (ep == nullptr) + { + reply(CreateJSONError("no endpoint with name " + endpoint)); + return; + } + if (map and exit.has_value()) + { + const auto gateways = net::GetGatewaysNotOnInterface(ep->GetIfName()); + if (gateways.empty()) + { + reply(CreateJSONError("no gateway found")); + return; + } + ep->MapExitRange(range, *exit); + if (token.has_value()) + { + ep->SetAuthInfoForEndpoint(*exit, service::AuthInfo{*token}); + } + ep->EnsurePathToService( + *exit, + [r, gateway = gateways[0], reply, ep](auto, service::OutboundContext* ctx) { + if (ctx == nullptr) + { + reply(CreateJSONError("could not find exit")); + return; + } + std::vector firsthops; + r->ForEachPeer( + [&firsthops](const auto* link, bool) { + firsthops.emplace_back(link->GetRemoteEndpoint().toHost()); + }, + false); + for (const auto& hop : firsthops) + { + net::AddRoute(hop, gateway); + } + net::AddDefaultRouteViaInterface(ep->GetIfName()); + r->SetDownHook([firsthops, gateway]() { + for (const auto& hop : firsthops) + { + net::DelRoute(hop, gateway); + } + }); + reply(CreateJSONResponse("OK")); + }, + 5s); + return; + } + else if (map and not exit.has_value()) + { + reply(CreateJSONError("no exit address provided")); + return; + } + else if (not map) + { + const auto gateways = net::GetGatewaysNotOnInterface(ep->GetIfName()); + if (gateways.empty()) + { + reply(CreateJSONError("no gateway found")); + return; + } + net::DelDefaultRouteViaInterface(ep->GetIfName()); + r->ForEachPeer( + [gateway = gateways[0]](const auto* link, bool) { + net::DelRoute(link->GetRemoteEndpoint().toHost(), gateway); + }, + false); + + ep->UnmapExitRange(range); + } + reply(CreateJSONResponse("OK")); + }); + }); + }) + .add_request_command("config", [&](lokimq::Message& msg) { HandleJSONRequest(msg, [r = m_Router](nlohmann::json obj, ReplyFunction_t reply) { - if (r->IsServiceNode()) { - reply(CreateJSONError("not supported")); - return; - } - std::optional exit; - IPRange range; - bool map = true; - const auto exit_itr = obj.find("exit"); - if (exit_itr != obj.end()) - { - service::Address addr; - if (not addr.FromString(exit_itr->get())) + const auto itr = obj.find("override"); + if (itr != obj.end()) { - reply(CreateJSONError("invalid exit address")); - return; - } - exit = addr; - } - - const auto unmap_itr = obj.find("unmap"); - if (unmap_itr != obj.end() and unmap_itr->get()) - { - map = false; - } - - const auto range_itr = obj.find("range"); - if (range_itr == obj.end()) - { - range.FromString("0.0.0.0/0"); - } - else if (not range.FromString(range_itr->get())) - { - reply(CreateJSONError("invalid ip range")); - return; - } - std::optional token; - const auto token_itr = obj.find("token"); - if (token_itr != obj.end()) - { - token = token_itr->get(); - } - - std::string endpoint = "default"; - const auto endpoint_itr = obj.find("endpoint"); - if (endpoint_itr != obj.end()) - { - endpoint = endpoint_itr->get(); - } - LogicCall(r->logic(), [map, exit, range, token, endpoint, r, reply]() { - auto ep = r->hiddenServiceContext().GetEndpointByName(endpoint); - if (ep == nullptr) - { - reply(CreateJSONError("no endpoint with name " + endpoint)); - return; - } - if (map and exit.has_value()) - { - const auto gateways = net::GetGatewaysNotOnInterface(ep->GetIfName()); - if (gateways.empty()) + if (not itr->is_object()) { - reply(CreateJSONError("no gateway found")); + reply(CreateJSONError(stringify("override is not an object"))); return; } - ep->MapExitRange(range, *exit); - if (token.has_value()) + for (const auto& [section, value] : itr->items()) { - ep->SetAuthInfoForEndpoint(*exit, service::AuthInfo{*token}); + if (not value.is_object()) + { + reply(CreateJSONError( + stringify("failed to set [", section, "] section is not an object"))); + return; + } + for (const auto& [key, value] : value.items()) + { + if (not value.is_string()) + { + reply(CreateJSONError(stringify( + "failed to set [", section, "]:", key, " value is not a string"))); + return; + } + r->GetConfig()->Override(section, key, value.get()); + } } - ep->EnsurePathToService( - *exit, - [r, gateway = gateways[0], reply, ep](auto, service::OutboundContext* ctx) { - if (ctx == nullptr) - { - reply(CreateJSONError("could not find exit")); - return; - } - std::vector firsthops; - r->ForEachPeer( - [&firsthops](const auto* link, bool) { - firsthops.emplace_back(link->GetRemoteEndpoint().toHost()); - }, - false); - for (const auto& hop : firsthops) - { - net::AddRoute(hop, gateway); - } - net::AddDefaultRouteViaInterface(ep->GetIfName()); - r->SetDownHook([firsthops, gateway]() { - for (const auto& hop : firsthops) - { - net::DelRoute(hop, gateway); - } - }); - reply(CreateJSONResponse("OK")); - }, - 5s); - return; } - else if (map and not exit.has_value()) + } + { + const auto itr = obj.find("reload"); + if (itr != obj.end() and itr->get()) { - reply(CreateJSONError("no exit address provided")); - return; + r->QueueDiskIO([conf = r->GetConfig()]() { conf->Save(); }); } - else if (not map) - { - const auto gateways = net::GetGatewaysNotOnInterface(ep->GetIfName()); - if (gateways.empty()) - { - reply(CreateJSONError("no gateway found")); - return; - } - net::DelDefaultRouteViaInterface(ep->GetIfName()); - r->ForEachPeer( - [gateway = gateways[0]](const auto* link, bool) { - net::DelRoute(link->GetRemoteEndpoint().toHost(), gateway); - }, - false); - - ep->UnmapExitRange(range); - } - reply(CreateJSONResponse("OK")); - }); + } + reply(CreateJSONResponse("OK")); }); }); }