From: Otto Moerbeek Date: Mon, 30 Sep 2024 13:31:01 +0000 (+0200) Subject: Threads started and doing xfr X-Git-Tag: rec-5.2.0-alpha1~7^2~18 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=aeb1e4e41225ccabaca505aa7bd19ebc910952e5;p=thirdparty%2Fpdns.git Threads started and doing xfr --- diff --git a/pdns/recursordist/rec-main.cc b/pdns/recursordist/rec-main.cc index c374db5a6b..c1fdeffe12 100644 --- a/pdns/recursordist/rec-main.cc +++ b/pdns/recursordist/rec-main.cc @@ -2321,7 +2321,7 @@ static int serviceMain(Logr::log_t log) { auto lci = g_luaconfs.getCopy(); - startLuaConfigDelayedThreads(lci.rpzs, lci.generation); + startLuaConfigDelayedThreads(lci, lci.generation); } RecThreadInfo::makeThreadPipes(log); @@ -3402,9 +3402,9 @@ struct WipeCacheResult wipeCaches(const DNSName& canon, bool subtree, uint16_t q return res; } -void startLuaConfigDelayedThreads(const vector& rpzs, uint64_t generation) +void startLuaConfigDelayedThreads(const LuaConfigItems& luaConfig, uint64_t generation) { - for (const auto& rpzPrimary : rpzs) { + for (const auto& rpzPrimary : luaConfig.rpzs) { if (rpzPrimary.zoneXFRParams.primaries.empty()) { continue; } @@ -3425,6 +3425,27 @@ void startLuaConfigDelayedThreads(const vector& rpzs, uint64_t exit(1); // NOLINT(concurrency-mt-unsafe) } } + for (const auto& [fcz, zone] : luaConfig.catalogzones) { + if (fcz.primaries.empty()) { + continue; + } + try { + // ZoneXFRTracker uses call by value for its args. That is essential, since we want copies so + // that ZoneXFRTracker gets values with the proper lifetime. + std::thread theThread(zoneXFRTracker, fcz, generation); + theThread.detach(); + } + catch (const std::exception& e) { + SLOG(g_log << Logger::Error << "Problem starting ZoneIXFRTracker thread: " << e.what() << endl, + g_slog->withName("zone")->error(Logr::Error, e.what(), "Exception starting ZoneXFRTracker thread", "exception", Logging::Loggable("std::exception"))); + exit(1); // NOLINT(concurrency-mt-unsafe) + } + catch (const PDNSException& e) { + SLOG(g_log << Logger::Error << "Problem starting ZoneIXFRTracker thread: " << e.reason << endl, + g_slog->withName("zone")->error(Logr::Error, e.reason, "Exception starting ZoneXFRTracker thread", "exception", Logging::Loggable("PDNSException"))); + exit(1); // NOLINT(concurrency-mt-unsafe) + } + } } static void* pleaseInitPolCounts(const string& name) @@ -3527,6 +3548,25 @@ static void activateRPZs(LuaConfigItems& lci) } } +static void activateForwardingCatalogZones(LuaConfigItems& lci) +{ + size_t idx = 0; + for (auto& fcz : lci.catalogzones) { + + auto& params = fcz.first; + params.zoneIdx = idx++; + auto zone = std::make_shared(); + if (params.zoneSizeHint != 0) { + //zone->reserve(params.zoneSizeHint); + } + + DNSName domain(params.name); + zone->name = domain; + fcz.second = zone; + } +} + + void activateLuaConfig(LuaConfigItems& lci) { if (!lci.trustAnchorFileInfo.fname.empty()) { @@ -3540,5 +3580,6 @@ void activateLuaConfig(LuaConfigItems& lci) warnIfDNSSECDisabled("Warning: adding Negative Trust Anchor for DNSSEC, but dnssec is set to 'off'!"); } activateRPZs(lci); + activateForwardingCatalogZones(lci); g_luaconfs.setState(lci); } diff --git a/pdns/recursordist/rec-main.hh b/pdns/recursordist/rec-main.hh index 208206457e..2654f8f5d5 100644 --- a/pdns/recursordist/rec-main.hh +++ b/pdns/recursordist/rec-main.hh @@ -621,7 +621,7 @@ void handleNewTCPQuestion(int fileDesc, FDMultiplexer::funcparam_t&); void makeUDPServerSockets(deferredAdd_t& deferredAdds, Logr::log_t); string doTraceRegex(FDWrapper file, vector::const_iterator begin, vector::const_iterator end); extern bool g_luaSettingsInYAML; -void startLuaConfigDelayedThreads(const vector& rpzs, uint64_t generation); +void startLuaConfigDelayedThreads(const LuaConfigItems& luaConfig, uint64_t generation); void activateLuaConfig(LuaConfigItems& lci); unsigned int authWaitTimeMSec(const std::unique_ptr& mtasker); diff --git a/pdns/recursordist/rec-xfr.cc b/pdns/recursordist/rec-xfr.cc index fc59c9a1f7..9f6e1ac2ad 100644 --- a/pdns/recursordist/rec-xfr.cc +++ b/pdns/recursordist/rec-xfr.cc @@ -26,6 +26,7 @@ #include "threadname.hh" #include "rec-lua-conf.hh" #include "query-local-address.hh" +#include "axfr-retriever.hh" #include "ixfr.hh" // As there can be multiple threads doing updates (due to config reloads), we use a multimap. @@ -70,6 +71,108 @@ void clearZoneTracker(const DNSName& zoneName) } } +static shared_ptr loadZoneFromServer(Logr::log_t plogger, const ComboAddress& primary, const DNSName& zoneName, const TSIGTriplet& tsigTriplet, size_t maxReceivedBytes, const ComboAddress& localAddress, uint16_t axfrTimeout) +{ + + auto logger = plogger->withValues("primary", Logging::Loggable(primary)); + logger->info(Logr::Info, "Loading zone from nameserver"); + if (!tsigTriplet.name.empty()) { + logger->info(Logr::Info, "Using TSIG key for authentication", "tsig_key_name", Logging::Loggable(tsigTriplet.name), "tsig_key_algorithm", Logging::Loggable(tsigTriplet.algo)); + } + + ComboAddress local(localAddress); + if (local == ComboAddress()) { + local = pdns::getQueryLocalAddress(primary.sin4.sin_family, 0); + } + + AXFRRetriever axfr(primary, zoneName, tsigTriplet, &local, maxReceivedBytes, axfrTimeout); + unsigned int nrecords = 0; + Resolver::res_t nop; + vector chunk; + time_t last = 0; + time_t axfrStart = time(nullptr); + time_t axfrNow = time(nullptr); + shared_ptr soaRecordContent; + // coverity[store_truncates_time_t] + while (axfr.getChunk(nop, &chunk, (axfrStart + axfrTimeout - axfrNow)) != 0) { + for (auto& dnsRecord : chunk) { + if (dnsRecord.d_type == QType::NS || dnsRecord.d_type == QType::TSIG) { + continue; + } + + dnsRecord.d_name.makeUsRelative(zoneName); + if (dnsRecord.d_type == QType::SOA) { + soaRecordContent = getRR(dnsRecord); + continue; + } + + //RPZRecordToPolicy(dnsRecord, zone, true, defpol, defpolOverrideLocal, maxTTL, logger); + nrecords++; + } + axfrNow = time(nullptr); + if (axfrNow < axfrStart || axfrNow - axfrStart > axfrTimeout) { + throw PDNSException("Total AXFR time exceeded!"); + } + if (last != time(nullptr)) { + logger->info(Logr::Info, "Zone load in progress", "nrecords", Logging::Loggable(nrecords)); + last = time(nullptr); + } + } + + logger->info(Logr::Info, "Zone load completed", "nrecords", Logging::Loggable(nrecords), "soa", Logging::Loggable(soaRecordContent->getZoneRepresentation())); + return soaRecordContent; +} + +static void preloadZoneFIle(ZoneXFRParams& params, const DNSName& zoneName, std::shared_ptr& oldZone, uint32_t& refresh, uint64_t configGeneration, ZoneWaiter& waiter, Logr::log_t logger) +{ + while (!params.soaRecordContent) { + /* if we received an empty sr, the zone was not really preloaded */ + + /* full copy, as promised */ + std::shared_ptr newZone = std::make_shared(*oldZone); + for (const auto& primary : params.primaries) { + try { + params.soaRecordContent = loadZoneFromServer(logger, primary, zoneName, params.tsigtriplet, params.maxReceivedMBytes, params.localAddress, params.xfrTimeout); + newZone->serial = params.soaRecordContent->d_st.serial; + newZone->refresh = params.soaRecordContent->d_st.refresh; + refresh = std::max(params.refreshFromConf != 0 ? params.refreshFromConf : newZone->refresh, 1U); + //setRPZZoneNewState(polName, params.zoneXFRParams.soaRecordContent->d_st.serial, newZone->size(), false, true); + + g_luaconfs.modify([zoneIdx = params.zoneIdx, &newZone](LuaConfigItems& lci) { + lci.catalogzones.at(zoneIdx).second = newZone; + }); + + /* no need to try another primary */ + break; + } + catch (const std::exception& e) { + logger->error(Logr::Warning, e.what(), "Unable to load zone, will retry", "from", Logging::Loggable(primary), "exception", Logging::Loggable("std::exception"), "refresh", Logging::Loggable(refresh)); + // Stats + } + catch (const PDNSException& e) { + logger->error(Logr::Warning, e.reason, "Unable to load zone, will retry", "from", Logging::Loggable(primary), "exception", Logging::Loggable("PDNSException"), "refresh", Logging::Loggable(refresh)); + // Stats + } + } + // Release newZone before (long) sleep to reduce memory usage + newZone = nullptr; + if (!params.soaRecordContent) { + std::unique_lock lock(waiter.mutex); + waiter.condVar.wait_for(lock, std::chrono::seconds(refresh), + [&stop = waiter.stop] { return stop.load(); }); + } + waiter.stop = false; + auto luaconfsLocal = g_luaconfs.getLocal(); + + if (luaconfsLocal->generation != configGeneration) { + /* the configuration has been reloaded, meaning that a new thread + has been started to handle that zone and we are now obsolete. + */ + return; + } + } +} + static bool zoneTrackerIteration(ZoneXFRParams& params, const DNSName& zoneName, std::shared_ptr& oldZone, uint32_t& refresh, bool& skipRefreshDelay, uint64_t configGeneration, ZoneWaiter& waiter, Logr::log_t logger) { // Don't hold on to oldZone, it well be re-assigned after sleep in the try block @@ -261,6 +364,8 @@ void zoneXFRTracker(ZoneXFRParams params, uint64_t configGeneration) // NOLINT(p insertZoneTracker(zoneName, waiter); + preloadZoneFIle(params, zoneName, oldZone, refresh, configGeneration, waiter, logger); + bool skipRefreshDelay = isPreloaded; while (zoneTrackerIteration(params, zoneName, oldZone, refresh, skipRefreshDelay, configGeneration, waiter, logger)) { // empty } diff --git a/pdns/recursordist/rec-xfr.hh b/pdns/recursordist/rec-xfr.hh index 2200ca877a..5a735f926a 100644 --- a/pdns/recursordist/rec-xfr.hh +++ b/pdns/recursordist/rec-xfr.hh @@ -70,3 +70,4 @@ struct Zone bool notifyZoneTracker(const DNSName& name); void insertZoneTracker(const DNSName& zoneName, ZoneWaiter& waiter); void clearZoneTracker(const DNSName& zoneName); +void zoneXFRTracker(ZoneXFRParams params, uint64_t configGeneration); diff --git a/pdns/recursordist/rec_channel_rec.cc b/pdns/recursordist/rec_channel_rec.cc index f687658723..e8a9aa8459 100644 --- a/pdns/recursordist/rec_channel_rec.cc +++ b/pdns/recursordist/rec_channel_rec.cc @@ -1934,7 +1934,7 @@ RecursorControlChannel::Answer luaconfig(bool broadcast) activateLuaConfig(lci); lci = g_luaconfs.getCopy(); if (broadcast) { - startLuaConfigDelayedThreads(lci.rpzs, lci.generation); + startLuaConfigDelayedThreads(lci, lci.generation); broadcastFunction([=] { return pleaseSupplantProxyMapping(proxyMapping); }); } else { @@ -1972,7 +1972,7 @@ RecursorControlChannel::Answer luaconfig(bool broadcast) activateLuaConfig(lci); lci = g_luaconfs.getCopy(); if (broadcast) { - startLuaConfigDelayedThreads(lci.rpzs, lci.generation); + startLuaConfigDelayedThreads(lci, lci.generation); broadcastFunction([pmap = std::move(proxyMapping)] { return pleaseSupplantProxyMapping(pmap); }); } else { diff --git a/pdns/recursordist/settings/cxxsupport.cc b/pdns/recursordist/settings/cxxsupport.cc index 4cfe53d1a2..3d045f4370 100644 --- a/pdns/recursordist/settings/cxxsupport.cc +++ b/pdns/recursordist/settings/cxxsupport.cc @@ -541,7 +541,7 @@ static void processLine(const std::string& arg, FieldMap& map, bool mainFile) ::rust::String section; ::rust::String fieldname; ::rust::String type_name; - pdns::rust::settings::rec::Value rustvalue = {false, 0, 0.0, "", {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + pdns::rust::settings::rec::Value rustvalue = {false, 0, 0.0, "", {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; if (pdns::settings::rec::oldKVToBridgeStruct(var, val, section, fieldname, type_name, rustvalue)) { auto overriding = !mainFile && !incremental && !simpleRustType(type_name); auto [existing, inserted] = map.emplace(std::pair{std::pair{section, fieldname}, pdns::rust::settings::rec::OldStyle{section, fieldname, var, std::move(type_name), rustvalue, overriding}}); @@ -671,6 +671,7 @@ std::string pdns::settings::rec::defaultsToYaml() def("recordcache", "zonetocaches", "Vec"); def("recursor", "allowed_additional_qtypes", "Vec"); def("incoming", "proxymappings", "Vec"); + def("recursor", "forwarding_catalog_zones", "Vec"); // End of should be generated XXX // Convert the map to a vector, as CXX does not have any dictionary like support. @@ -713,7 +714,12 @@ std::string pdns::settings::rec::defaultsToYaml() oldname = std::string(iter->second.old_name); } res += "##### "; - res += arg().getHelp(oldname); + auto help = arg().getHelp(oldname); + if (help.empty()) { + replace(oldname.begin(), oldname.end(), '_', '-'); + help = arg().getHelp(oldname); + } + res += help; res += '\n'; } if (sectionChange) { @@ -1297,6 +1303,30 @@ void fromRustToLuaConfig(const rust::Vec& catzones, std::vector>>& lua) +{ + for (const auto& catz : catzones) { + cerr << "catz: " << catz.name << endl; + ZoneXFRParams params; + auto zone = std::make_shared(); + + for (const auto& address : catz.xfr.addresses) { + ComboAddress combo = ComboAddress(std::string(address), 53); + params.primaries.emplace_back(combo.toStringWithPort()); + } + params.name = std::string(catz.name); + params.zoneSizeHint = catz.xfr.zoneSizeHint; + assign(params.tsigtriplet, catz.xfr.tsig); + params.refreshFromConf = catz.xfr.refresh; + params.maxReceivedMBytes = catz.xfr.maxReceivedMBytes; + if (!catz.xfr.localAddress.empty()) { + params.localAddress = ComboAddress(std::string(catz.xfr.localAddress)); + } + params.xfrTimeout = catz.xfr.axfrTimeout; + lua.emplace_back(params, zone); + } +} } void pdns::settings::rec::fromBridgeStructToLuaConfig(const pdns::rust::settings::rec::Recursorsettings& settings, LuaConfigItems& luaConfig, ProxyMapping& proxyMapping) @@ -1320,6 +1350,7 @@ void pdns::settings::rec::fromBridgeStructToLuaConfig(const pdns::rust::settings fromRustToLuaConfig(settings.recursor.sortlists, luaConfig.sortlist); fromRustToLuaConfig(settings.recordcache.zonetocaches, luaConfig.ztcConfigs); fromRustToLuaConfig(settings.recursor.allowed_additional_qtypes, luaConfig.allowAdditionalQTypes); + fromRustToLuaConfig(settings.recursor.forwarding_catalog_zones, luaConfig.catalogzones); fromRustToLuaConfig(settings.incoming.proxymappings, proxyMapping); } diff --git a/pdns/recursordist/settings/generate.py b/pdns/recursordist/settings/generate.py index 549bfe38f4..052866b90e 100644 --- a/pdns/recursordist/settings/generate.py +++ b/pdns/recursordist/settings/generate.py @@ -109,6 +109,7 @@ class LType(Enum): ListSubnets = auto() ListTrustAnchors = auto() ListZoneToCaches = auto() + ListForwardingCatalogZones = auto() String = auto() Uint64 = auto() @@ -116,7 +117,7 @@ listOfStringTypes = (LType.ListSocketAddresses, LType.ListStrings, LType.ListSu listOfStructuredTypes = (LType.ListAuthZones, LType.ListForwardZones, LType.ListTrustAnchors, LType.ListNegativeTrustAnchors, LType.ListProtobufServers, LType.ListDNSTapFrameStreamServers, LType.ListDNSTapNODFrameStreamServers, LType.ListSortLists, LType.ListRPZs, LType.ListZoneToCaches, LType.ListAllowedAdditionalQTypes, - LType.ListProxyMappings) + LType.ListProxyMappings, LType.ListForwardingCatalogZones) def get_olddoc_typename(typ): """Given a type from table.py, return the old-style type name""" @@ -180,6 +181,8 @@ def get_newdoc_typename(typ): return 'Sequence of `AllowedAdditionalQType`_' if typ == LType.ListProxyMappings: return 'Sequence of `ProxyMapping`_' + if typ == LType.ListForwardingCatalogZones: + return 'Sequence of `ForwardingCatalogZone`_' return 'Unknown' + str(typ) def get_default_olddoc_value(typ, val): diff --git a/pdns/recursordist/settings/rust-bridge-in.rs b/pdns/recursordist/settings/rust-bridge-in.rs index 427da15c74..5211d996d6 100644 --- a/pdns/recursordist/settings/rust-bridge-in.rs +++ b/pdns/recursordist/settings/rust-bridge-in.rs @@ -258,6 +258,51 @@ pub struct ApiZones { forward_zones: Vec, } +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct XFR { + #[serde(default, skip_serializing_if = "crate::is_default")] + addresses: Vec, + #[serde(default, skip_serializing_if = "crate::is_default")] + zoneSizeHint: u32, + #[serde(default, skip_serializing_if = "crate::is_default")] + tsig: TSIGTriplet, + #[serde(default, skip_serializing_if = "crate::is_default")] + refresh: u32, + #[serde(default, skip_serializing_if = "crate::is_default")] + maxReceivedMBytes: u32, + #[serde(default, skip_serializing_if = "crate::is_default")] + localAddress: String, + #[serde(default = "crate::U32::<20>::value", skip_serializing_if = "crate::U32::<20>::is_equal")] + axfrTimeout: u32, +} + +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct FCZDefault { + #[serde(default, skip_serializing_if = "crate::is_default")] + name: String, + #[serde(default, skip_serializing_if = "crate::is_default")] + forwarders: Vec, + #[serde(default, skip_serializing_if = "crate::is_default")] + recurse: bool, + #[serde(default, skip_serializing_if = "crate::is_default")] + allow_notify: bool, + } + +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct ForwardingCatalogZone { + #[serde(default, skip_serializing_if = "crate::is_default")] + name: String, + #[serde(default, skip_serializing_if = "crate::is_default")] + allow_notify: bool, + #[serde(default, skip_serializing_if = "crate::is_default")] + xfr: XFR, + #[serde(default, skip_serializing_if = "crate::is_default")] + groups: Vec, +} + // Two structs used to generated YAML based on a vector of name to value mappings // Cannot use Enum as CXX has only very basic Enum support struct Value { @@ -278,6 +323,7 @@ struct Value { vec_zonetocache_val: Vec, vec_allowedadditionalqtype_val: Vec, vec_proxymapping_val: Vec, + vec_forwardingcatalogzone_val: Vec, } struct OldStyle { diff --git a/pdns/recursordist/settings/rust/src/bridge.rs b/pdns/recursordist/settings/rust/src/bridge.rs index 4ac678e05c..0abaf931b2 100644 --- a/pdns/recursordist/settings/rust/src/bridge.rs +++ b/pdns/recursordist/settings/rust/src/bridge.rs @@ -67,6 +67,27 @@ impl Default for ApiZones { } } +impl Default for XFR { + fn default() -> Self { + let deserialized: XFR = serde_yaml::from_str("").unwrap(); + deserialized + } +} + +impl Default for FCZDefault { + fn default() -> Self { + let deserialized: FCZDefault = serde_yaml::from_str("").unwrap(); + deserialized + } +} + +impl Default for ForwardingCatalogZone { + fn default() -> Self { + let deserialized: ForwardingCatalogZone = serde_yaml::from_str("").unwrap(); + deserialized + } +} + pub fn validate_socket_address(field: &str, val: &String) -> Result<(), ValidationError> { let sa = SocketAddr::from_str(val); if sa.is_err() { @@ -634,6 +655,22 @@ impl TSIGTriplet { } } +impl ForwardingCatalogZone { + pub fn validate(&self, _field: &str) -> Result<(), ValidationError> { + Ok(()) + } + + fn to_yaml_map(&self) -> serde_yaml::Value { + // XXX INCOMPLETE + let seq = serde_yaml::Sequence::new(); + let mut map = serde_yaml::Mapping::new(); + inserts(&mut map, "name", &self.name); + insertb(&mut map, "allow_notify", self.allow_notify); + insertseq(&mut map, "groups", &seq); + serde_yaml::Value::Mapping(map) + } +} + #[allow(clippy::ptr_arg)] //# Avoids creating a rust::Slice object on the C++ side. pub fn validate_auth_zones(field: &str, vec: &Vec) -> Result<(), ValidationError> { validate_vec(field, vec, |field, element| element.validate(field)) @@ -855,6 +892,13 @@ pub fn map_to_yaml_string(vec: &Vec) -> Result" => { + let mut seq = serde_yaml::Sequence::new(); + for element in &entry.value.vec_forwardingcatalogzone_val { + seq.push(element.to_yaml_map()); + } + serde_yaml::Value::Sequence(seq) + } other => serde_yaml::Value::String( "map_to_yaml_string: Unknown type: ".to_owned() + other, ), @@ -1100,3 +1144,4 @@ pub fn validate_recordcache( pub fn validate_snmp(_snmp: &recsettings::Snmp) -> Result<(), ValidationError> { Ok(()) } + diff --git a/pdns/recursordist/settings/table.py b/pdns/recursordist/settings/table.py index 0ec0ec5ac6..d1603ad0e0 100644 --- a/pdns/recursordist/settings/table.py +++ b/pdns/recursordist/settings/table.py @@ -3535,4 +3535,16 @@ Load this Lua script on startup and shutdown and run the Lua function ``on_recur 'skip-old' : 'No equivalent old-style setting', 'versionadded': '5.2.0', }, + { + 'name' : 'forwarding_catalog_zones', + 'section' : 'recursor', + 'type' : LType.ListForwardingCatalogZones, + 'default' : '', + 'help' : 'Sequence of ForwardingCatalogZone', + 'doc' : ''' +Sequence of ForwardingCatalogZone + ''', + 'skip-old' : 'No equivalent old style setting', + 'versionadded': '5.2.0', + }, ] diff --git a/pdns/recursordist/test-settings.cc b/pdns/recursordist/test-settings.cc index b6c0caa556..831d6393c3 100644 --- a/pdns/recursordist/test-settings.cc +++ b/pdns/recursordist/test-settings.cc @@ -886,6 +886,35 @@ BOOST_AUTO_TEST_CASE(test_yaml_proxymapping) BOOST_CHECK_EQUAL(settings.incoming.proxymappings[1].domains.size(), 3U); } +BOOST_AUTO_TEST_CASE(test_yaml_forwardingcatalogzones) +{ + const std::string yaml = R"EOT(recursor: + forwarding_catalog_zones: + - name: 'forward.invalid' + xfr: + addresses: [192.168.178.3:53] + groups: + - forwarders: [1.2.3.4] + - name: mygroup + forwarders: [4.5.6.7] + recurse: true + allow_notify: true +)EOT"; + + auto settings = pdns::rust::settings::rec::parse_yaml_string(yaml); + settings.validate(); + BOOST_CHECK_EQUAL(std::string(settings.recursor.forwarding_catalog_zones[0].name), "forward.invalid"); + BOOST_CHECK_EQUAL(std::string(settings.recursor.forwarding_catalog_zones[0].xfr.addresses[0]), "192.168.178.3:53"); + BOOST_CHECK_EQUAL(settings.recursor.forwarding_catalog_zones[0].groups[0].forwarders.size(), 1U); + BOOST_CHECK_EQUAL(std::string(settings.recursor.forwarding_catalog_zones[0].groups[0].forwarders[0]), "1.2.3.4"); + BOOST_CHECK_EQUAL(std::string(settings.recursor.forwarding_catalog_zones[0].groups[0].name), ""); + + BOOST_CHECK_EQUAL(settings.recursor.forwarding_catalog_zones[0].groups[1].forwarders.size(), 1U); + BOOST_CHECK_EQUAL(std::string(settings.recursor.forwarding_catalog_zones[0].groups[1].forwarders[0]), "4.5.6.7"); + BOOST_CHECK_EQUAL(settings.recursor.forwarding_catalog_zones[0].groups[1].recurse, true); + BOOST_CHECK_EQUAL(settings.recursor.forwarding_catalog_zones[0].groups[1].allow_notify, true); +} + BOOST_AUTO_TEST_CASE(test_yaml_to_luaconfigand_back) { const std::string yaml = R"EOT(dnssec: