Loading plugin/ad/ad.cpp +169 −4 Changes for plugin/ad/ad.cpp: 169 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -30,6 +30,7 @@ #include <algorithm> #include <sstream> #include <memory> #include <functional> #include <optional> #include <netplus/socket.h> Loading @@ -38,6 +39,9 @@ #include "ad.h" #include "crypto/gss.h" #include <sys/socket.h> #include <netinet/in.h> #include "fenrirds.h" #include "domains.h" #include "backend.h" Loading Loading @@ -348,10 +352,11 @@ namespace { ActiveDirectory::ActiveDirectory(AuthBackend &adminBackend, const std::string &bindAddr, int port, int maxconn, const std::string &adminPassword) const std::string &adminPassword, const std::string &dcHost) : _AdminBackend(adminBackend), _BindAddr(bindAddr), _Port(port), _MaxConn(maxconn), _AdminPassword(adminPassword), _AdminPassword(adminPassword), _DcHost(dcHost), _Event(nullptr) { DomainRegistry::instance().setAdminBackend(&adminBackend); Loading @@ -359,8 +364,11 @@ ActiveDirectory::ActiveDirectory(AuthBackend &adminBackend, netplus::tcp *ssock = new netplus::tcp(bindAddr.c_str(), port, maxconn, 0); _ServerSockets.push_back(ssock); /* CLDAP (connectionless LDAP) netlogon ping on the same port, UDP */ _Cldap = new netplus::udp(bindAddr.c_str(), port, maxconn, -1); std::cerr << "AD/LDAP plugin listening on " << bindAddr << ":" << port << std::endl; std::cerr << "AD/LDAP plugin listening on " << bindAddr << ":" << port << " (+CLDAP udp)" << std::endl; } void ActiveDirectory::init() { Loading @@ -369,15 +377,39 @@ void ActiveDirectory::init() { s->setNonBlock(); s->listen(); } _Cldap->bind(); } ActiveDirectory::~ActiveDirectory() { _Running = false; if (_CldapThread.joinable()) _CldapThread.join(); delete _Cldap; delete static_cast<AdEvent*>(_Event); for (auto *s : _ServerSockets) delete s; } void ActiveDirectory::run() { _Running = true; _CldapThread = std::thread([this]() { while (_Running) { std::vector<std::vector<uint8_t>> dgrams; std::vector<sockaddr_storage> addrs; size_t n = 0; try { n = _Cldap->recvBatchAddr(dgrams, addrs, 16); } catch (...) { continue; } for (size_t i = 0; i < n; ++i) { auto resp = handleCldap(dgrams[i].data(), dgrams[i].size()); if (resp.empty()) continue; socklen_t sl = addrs[i].ss_family == AF_INET6 ? sizeof(sockaddr_in6) : sizeof(sockaddr_in); std::vector<std::pair<const uint8_t*, size_t>> out = {{resp.data(), resp.size()}}; try { _Cldap->sendBatch(out, &addrs[i], sl); } catch (...) {} } } }); auto *ev = new AdEvent(_ServerSockets, this); _Event = ev; ev->runEventloop(0); Loading Loading @@ -860,6 +892,137 @@ LdapEntry ActiveDirectory::renderEntry(const dsdb::Entry &ent, /****************************************************************************** * RootDSE *****************************************************************************/ /****************************************************************************** * CLDAP netlogon ping (MS-ADTS 6.3.3 / 6.3.5): a connectionless LDAP search * with an empty base and a filter naming the domain and NtVer. We answer with * a searchResultEntry carrying the Netlogon attribute (a * NETLOGON_SAM_LOGON_RESPONSE_EX) so clients can pick this DC. *****************************************************************************/ namespace { void nlName(std::vector<uint8_t> &o, const std::string &name) { size_t i = 0; while (i < name.size()) { size_t dot = name.find('.', i); if (dot == std::string::npos) dot = name.size(); size_t l = dot - i; if (l == 0) break; o.push_back(uint8_t(l)); for (size_t k = i; k < dot; ++k) o.push_back(uint8_t(name[k])); i = dot + 1; } o.push_back(0); } void nlU16(std::vector<uint8_t> &o, uint16_t v) { o.push_back(v & 0xff); o.push_back(v >> 8); } void nlU32(std::vector<uint8_t> &o, uint32_t v) { for (int i = 0; i < 4; ++i) o.push_back((v >> (8 * i)) & 0xff); } } std::vector<uint8_t> ActiveDirectory::buildNetlogonResponse(const Dom &d, uint32_t ntVersion, const std::string &user) { std::vector<uint8_t> b; std::string host = _DcHost.empty() ? ("dc." + d.meta.dnsName) : _DcHost; std::string comp = host.substr(0, host.find('.')); std::string site = "Default-First-Site-Name"; uint32_t flags = 0x1 | 0x4 | 0x8 | 0x10 | 0x20 | 0x40 | 0x80 | 0x100 | 0x200 | 0x20000000u | 0x40000000u | 0x80000000u; /* PDC/GC/LDAP/DS/KDC/TIMESERV/CLOSEST/WRITABLE/GOODTIME + DNS_{CONTROLLER,DOMAIN,FOREST} */ nlU16(b, 23); /* Opcode = LOGON_SAM_LOGON_RESPONSE_EX */ nlU16(b, 0); /* Sbz */ nlU32(b, flags); /* DomainGuid: the domain head objectGUID */ uint8_t guid[16] = {0}; if (auto head = d.dir->byDn(d.meta.baseDn)) std::memcpy(guid, head->objectGuid.value, 16); b.insert(b.end(), guid, guid + 16); nlName(b, d.meta.dnsName); /* DnsForestName (= domain, single-domain forest) */ nlName(b, d.meta.dnsName); /* DnsDomainName */ nlName(b, host); /* DnsHostName */ nlName(b, d.meta.netbios); /* NetbiosDomainName */ nlName(b, dsdb::upper(comp)); /* NetbiosComputerName */ nlName(b, user); /* UserName (echo / empty) */ nlName(b, site); /* DcSiteName */ nlName(b, site); /* ClientSiteName */ nlU32(b, ntVersion ? ntVersion : 0x00000005); nlU16(b, 0xffff); /* LmNtToken */ nlU16(b, 0xffff); /* Lm20Token */ return b; } std::vector<uint8_t> ActiveDirectory::handleCldap(const uint8_t *data, size_t len) { /* parse LDAPMessage { msgid, searchRequest } */ size_t off = 0; uint8_t tag; const uint8_t *c; size_t cl; if (!LdapBer::parseTLV(data, len, off, tag, c, cl) || tag != LdapBer::TAG_SEQUENCE) return {}; const uint8_t *msg = c; size_t mlen = cl; size_t mo = 0; if (!LdapBer::parseTLV(msg, mlen, mo, tag, c, cl)) return {}; int32_t msgId = 0; LdapBer::parseInteger(c, cl, msgId); if (!LdapBer::parseTLV(msg, mlen, mo, tag, c, cl)) return {}; if ((tag & 0x1F) != LDAP_SEARCH_REQUEST) return {}; /* search fields: base, scope, deref, sizelimit, timelimit, typesOnly, filter, attrs */ const uint8_t *sc = c; size_t sl = cl, so = 0; const uint8_t *f; size_t fl; uint8_t st; std::string base; if (!LdapBer::parseTLV(sc, sl, so, st, f, fl)) return {}; LdapBer::parseOctetString(f, fl, base); for (int i = 0; i < 5; ++i) /* skip scope..typesOnly */ if (!LdapBer::parseTLV(sc, sl, so, st, f, fl)) return {}; size_t filtOff = so; if (!LdapBer::parseTLV(sc, sl, so, st, f, fl)) return {}; dsdb::Filter filter; parseFilter(sc + filtOff, so - filtOff, filter); /* walk the filter for DnsDomain / NtVer / User */ std::string dnsDomain, user; uint32_t ntVer = 0; std::function<void(const dsdb::Filter &)> walk = [&](const dsdb::Filter &ff) { for (auto &ch : ff.children) walk(ch); if (ff.type == dsdb::Filter::Equal) { if (dsdb::iequals(ff.attr, "DnsDomain")) dnsDomain = ff.value; else if (dsdb::iequals(ff.attr, "User")) user = ff.value; else if (dsdb::iequals(ff.attr, "NtVer") && ff.value.size() >= 4) ntVer = uint8_t(ff.value[0]) | uint8_t(ff.value[1]) << 8 | uint8_t(ff.value[2]) << 16 | uint8_t(ff.value[3]) << 24; } }; walk(filter); /* pick the domain: by DnsDomain, else the first */ std::lock_guard<std::mutex> lock(_Lock); Dom *dom = nullptr; for (auto &d : _Domains) { if (dnsDomain.empty() || dsdb::iequals(d.meta.dnsName, dnsDomain)) { dom = &d; break; } } if (!dom) return {}; auto blob = buildNetlogonResponse(*dom, ntVer, user); /* searchResultEntry { objectName "", attributes { { "Netlogon", { blob } } } } */ LdapEntry e; e.dn = ""; LdapAttribute a; a.type = "Netlogon"; a.values.push_back(std::string(blob.begin(), blob.end())); e.attributes.push_back(std::move(a)); auto entry = buildSearchResultEntry(msgId, e); auto done = buildSearchResultDone(msgId, LDAP_SUCCESS); entry.insert(entry.end(), done.begin(), done.end()); return entry; } LdapEntry ActiveDirectory::buildRootDSE() { LdapEntry e; e.dn = ""; Loading Loading @@ -1393,6 +1556,7 @@ fenrirds::Plugin *fenrirds_plugin_create(fenrirds::AuthBackend &backend, int port = 389; int maxconn = 128; std::string adminPassword; std::string dcHost; if (configJson) { json_object *jobj = json_tokener_parse(configJson); Loading @@ -1406,11 +1570,12 @@ fenrirds::Plugin *fenrirds_plugin_create(fenrirds::AuthBackend &backend, if (jport) port = json_object_get_int(jport); if (jmax) maxconn = json_object_get_int(jmax); if (jpw) adminPassword = json_object_get_string(jpw); if (auto *jh = json_object_object_get(jobj, "dns_hostname")) dcHost = json_object_get_string(jh); json_object_put(jobj); } } return new fenrirds::ActiveDirectory(backend, bindAddr, port, maxconn, adminPassword); return new fenrirds::ActiveDirectory(backend, bindAddr, port, maxconn, adminPassword, dcHost); } void fenrirds_plugin_destroy(fenrirds::Plugin *p) { Loading plugin/ad/ad.h +15 −1 Changes for plugin/ad/ad.h: 15 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -31,6 +31,8 @@ #include <vector> #include <map> #include <mutex> #include <thread> #include <atomic> #include <memory> #include <cstdint> Loading Loading @@ -243,7 +245,8 @@ namespace fenrirds { ActiveDirectory(AuthBackend &adminBackend, const std::string &bindAddr = "0.0.0.0", int port = 389, int maxconn = 128, const std::string &adminPassword = ""); const std::string &adminPassword = "", const std::string &dcHost = ""); ~ActiveDirectory() override; /* Plugin interface */ Loading Loading @@ -279,6 +282,9 @@ namespace fenrirds { int32_t msgId, LdapSession &session); std::vector<uint8_t> handleUnbindRequest(int32_t msgId, LdapSession &session); /* ---- CLDAP netlogon ping (UDP 389, DC location) ---- */ std::vector<uint8_t> handleCldap(const uint8_t *data, size_t len); /* ---- LDAP Response Builders ---- */ std::vector<uint8_t> buildLdapMessage(int32_t msgId, uint8_t appTag, const std::vector<uint8_t> &protocolOp); Loading @@ -296,6 +302,9 @@ namespace fenrirds { dsdb::DomainMeta meta; std::unique_ptr<dsdb::Directory> dir; }; /* CLDAP netlogon response (needs Dom) */ std::vector<uint8_t> buildNetlogonResponse(const Dom &d, uint32_t ntVersion, const std::string &user); /* (re)load the domains from the registry, provisioning each */ void loadDomains(); /* the domain whose base DN contains dn (longest match), or nullptr */ Loading @@ -319,6 +328,7 @@ namespace fenrirds { int _Port; int _MaxConn; std::string _AdminPassword; std::string _DcHost; /* FQDN for CLDAP netlogon */ std::vector<Dom> _Domains; Loading @@ -328,6 +338,10 @@ namespace fenrirds { std::vector<netplus::socket*> _ServerSockets; void *_Event; netplus::udp *_Cldap = nullptr; std::thread _CldapThread; std::atomic<bool> _Running{false}; }; }; /* namespace fenrirds */ Loading
plugin/ad/ad.cpp +169 −4 Changes for plugin/ad/ad.cpp: 169 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -30,6 +30,7 @@ #include <algorithm> #include <sstream> #include <memory> #include <functional> #include <optional> #include <netplus/socket.h> Loading @@ -38,6 +39,9 @@ #include "ad.h" #include "crypto/gss.h" #include <sys/socket.h> #include <netinet/in.h> #include "fenrirds.h" #include "domains.h" #include "backend.h" Loading Loading @@ -348,10 +352,11 @@ namespace { ActiveDirectory::ActiveDirectory(AuthBackend &adminBackend, const std::string &bindAddr, int port, int maxconn, const std::string &adminPassword) const std::string &adminPassword, const std::string &dcHost) : _AdminBackend(adminBackend), _BindAddr(bindAddr), _Port(port), _MaxConn(maxconn), _AdminPassword(adminPassword), _AdminPassword(adminPassword), _DcHost(dcHost), _Event(nullptr) { DomainRegistry::instance().setAdminBackend(&adminBackend); Loading @@ -359,8 +364,11 @@ ActiveDirectory::ActiveDirectory(AuthBackend &adminBackend, netplus::tcp *ssock = new netplus::tcp(bindAddr.c_str(), port, maxconn, 0); _ServerSockets.push_back(ssock); /* CLDAP (connectionless LDAP) netlogon ping on the same port, UDP */ _Cldap = new netplus::udp(bindAddr.c_str(), port, maxconn, -1); std::cerr << "AD/LDAP plugin listening on " << bindAddr << ":" << port << std::endl; std::cerr << "AD/LDAP plugin listening on " << bindAddr << ":" << port << " (+CLDAP udp)" << std::endl; } void ActiveDirectory::init() { Loading @@ -369,15 +377,39 @@ void ActiveDirectory::init() { s->setNonBlock(); s->listen(); } _Cldap->bind(); } ActiveDirectory::~ActiveDirectory() { _Running = false; if (_CldapThread.joinable()) _CldapThread.join(); delete _Cldap; delete static_cast<AdEvent*>(_Event); for (auto *s : _ServerSockets) delete s; } void ActiveDirectory::run() { _Running = true; _CldapThread = std::thread([this]() { while (_Running) { std::vector<std::vector<uint8_t>> dgrams; std::vector<sockaddr_storage> addrs; size_t n = 0; try { n = _Cldap->recvBatchAddr(dgrams, addrs, 16); } catch (...) { continue; } for (size_t i = 0; i < n; ++i) { auto resp = handleCldap(dgrams[i].data(), dgrams[i].size()); if (resp.empty()) continue; socklen_t sl = addrs[i].ss_family == AF_INET6 ? sizeof(sockaddr_in6) : sizeof(sockaddr_in); std::vector<std::pair<const uint8_t*, size_t>> out = {{resp.data(), resp.size()}}; try { _Cldap->sendBatch(out, &addrs[i], sl); } catch (...) {} } } }); auto *ev = new AdEvent(_ServerSockets, this); _Event = ev; ev->runEventloop(0); Loading Loading @@ -860,6 +892,137 @@ LdapEntry ActiveDirectory::renderEntry(const dsdb::Entry &ent, /****************************************************************************** * RootDSE *****************************************************************************/ /****************************************************************************** * CLDAP netlogon ping (MS-ADTS 6.3.3 / 6.3.5): a connectionless LDAP search * with an empty base and a filter naming the domain and NtVer. We answer with * a searchResultEntry carrying the Netlogon attribute (a * NETLOGON_SAM_LOGON_RESPONSE_EX) so clients can pick this DC. *****************************************************************************/ namespace { void nlName(std::vector<uint8_t> &o, const std::string &name) { size_t i = 0; while (i < name.size()) { size_t dot = name.find('.', i); if (dot == std::string::npos) dot = name.size(); size_t l = dot - i; if (l == 0) break; o.push_back(uint8_t(l)); for (size_t k = i; k < dot; ++k) o.push_back(uint8_t(name[k])); i = dot + 1; } o.push_back(0); } void nlU16(std::vector<uint8_t> &o, uint16_t v) { o.push_back(v & 0xff); o.push_back(v >> 8); } void nlU32(std::vector<uint8_t> &o, uint32_t v) { for (int i = 0; i < 4; ++i) o.push_back((v >> (8 * i)) & 0xff); } } std::vector<uint8_t> ActiveDirectory::buildNetlogonResponse(const Dom &d, uint32_t ntVersion, const std::string &user) { std::vector<uint8_t> b; std::string host = _DcHost.empty() ? ("dc." + d.meta.dnsName) : _DcHost; std::string comp = host.substr(0, host.find('.')); std::string site = "Default-First-Site-Name"; uint32_t flags = 0x1 | 0x4 | 0x8 | 0x10 | 0x20 | 0x40 | 0x80 | 0x100 | 0x200 | 0x20000000u | 0x40000000u | 0x80000000u; /* PDC/GC/LDAP/DS/KDC/TIMESERV/CLOSEST/WRITABLE/GOODTIME + DNS_{CONTROLLER,DOMAIN,FOREST} */ nlU16(b, 23); /* Opcode = LOGON_SAM_LOGON_RESPONSE_EX */ nlU16(b, 0); /* Sbz */ nlU32(b, flags); /* DomainGuid: the domain head objectGUID */ uint8_t guid[16] = {0}; if (auto head = d.dir->byDn(d.meta.baseDn)) std::memcpy(guid, head->objectGuid.value, 16); b.insert(b.end(), guid, guid + 16); nlName(b, d.meta.dnsName); /* DnsForestName (= domain, single-domain forest) */ nlName(b, d.meta.dnsName); /* DnsDomainName */ nlName(b, host); /* DnsHostName */ nlName(b, d.meta.netbios); /* NetbiosDomainName */ nlName(b, dsdb::upper(comp)); /* NetbiosComputerName */ nlName(b, user); /* UserName (echo / empty) */ nlName(b, site); /* DcSiteName */ nlName(b, site); /* ClientSiteName */ nlU32(b, ntVersion ? ntVersion : 0x00000005); nlU16(b, 0xffff); /* LmNtToken */ nlU16(b, 0xffff); /* Lm20Token */ return b; } std::vector<uint8_t> ActiveDirectory::handleCldap(const uint8_t *data, size_t len) { /* parse LDAPMessage { msgid, searchRequest } */ size_t off = 0; uint8_t tag; const uint8_t *c; size_t cl; if (!LdapBer::parseTLV(data, len, off, tag, c, cl) || tag != LdapBer::TAG_SEQUENCE) return {}; const uint8_t *msg = c; size_t mlen = cl; size_t mo = 0; if (!LdapBer::parseTLV(msg, mlen, mo, tag, c, cl)) return {}; int32_t msgId = 0; LdapBer::parseInteger(c, cl, msgId); if (!LdapBer::parseTLV(msg, mlen, mo, tag, c, cl)) return {}; if ((tag & 0x1F) != LDAP_SEARCH_REQUEST) return {}; /* search fields: base, scope, deref, sizelimit, timelimit, typesOnly, filter, attrs */ const uint8_t *sc = c; size_t sl = cl, so = 0; const uint8_t *f; size_t fl; uint8_t st; std::string base; if (!LdapBer::parseTLV(sc, sl, so, st, f, fl)) return {}; LdapBer::parseOctetString(f, fl, base); for (int i = 0; i < 5; ++i) /* skip scope..typesOnly */ if (!LdapBer::parseTLV(sc, sl, so, st, f, fl)) return {}; size_t filtOff = so; if (!LdapBer::parseTLV(sc, sl, so, st, f, fl)) return {}; dsdb::Filter filter; parseFilter(sc + filtOff, so - filtOff, filter); /* walk the filter for DnsDomain / NtVer / User */ std::string dnsDomain, user; uint32_t ntVer = 0; std::function<void(const dsdb::Filter &)> walk = [&](const dsdb::Filter &ff) { for (auto &ch : ff.children) walk(ch); if (ff.type == dsdb::Filter::Equal) { if (dsdb::iequals(ff.attr, "DnsDomain")) dnsDomain = ff.value; else if (dsdb::iequals(ff.attr, "User")) user = ff.value; else if (dsdb::iequals(ff.attr, "NtVer") && ff.value.size() >= 4) ntVer = uint8_t(ff.value[0]) | uint8_t(ff.value[1]) << 8 | uint8_t(ff.value[2]) << 16 | uint8_t(ff.value[3]) << 24; } }; walk(filter); /* pick the domain: by DnsDomain, else the first */ std::lock_guard<std::mutex> lock(_Lock); Dom *dom = nullptr; for (auto &d : _Domains) { if (dnsDomain.empty() || dsdb::iequals(d.meta.dnsName, dnsDomain)) { dom = &d; break; } } if (!dom) return {}; auto blob = buildNetlogonResponse(*dom, ntVer, user); /* searchResultEntry { objectName "", attributes { { "Netlogon", { blob } } } } */ LdapEntry e; e.dn = ""; LdapAttribute a; a.type = "Netlogon"; a.values.push_back(std::string(blob.begin(), blob.end())); e.attributes.push_back(std::move(a)); auto entry = buildSearchResultEntry(msgId, e); auto done = buildSearchResultDone(msgId, LDAP_SUCCESS); entry.insert(entry.end(), done.begin(), done.end()); return entry; } LdapEntry ActiveDirectory::buildRootDSE() { LdapEntry e; e.dn = ""; Loading Loading @@ -1393,6 +1556,7 @@ fenrirds::Plugin *fenrirds_plugin_create(fenrirds::AuthBackend &backend, int port = 389; int maxconn = 128; std::string adminPassword; std::string dcHost; if (configJson) { json_object *jobj = json_tokener_parse(configJson); Loading @@ -1406,11 +1570,12 @@ fenrirds::Plugin *fenrirds_plugin_create(fenrirds::AuthBackend &backend, if (jport) port = json_object_get_int(jport); if (jmax) maxconn = json_object_get_int(jmax); if (jpw) adminPassword = json_object_get_string(jpw); if (auto *jh = json_object_object_get(jobj, "dns_hostname")) dcHost = json_object_get_string(jh); json_object_put(jobj); } } return new fenrirds::ActiveDirectory(backend, bindAddr, port, maxconn, adminPassword); return new fenrirds::ActiveDirectory(backend, bindAddr, port, maxconn, adminPassword, dcHost); } void fenrirds_plugin_destroy(fenrirds::Plugin *p) { Loading
plugin/ad/ad.h +15 −1 Changes for plugin/ad/ad.h: 15 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -31,6 +31,8 @@ #include <vector> #include <map> #include <mutex> #include <thread> #include <atomic> #include <memory> #include <cstdint> Loading Loading @@ -243,7 +245,8 @@ namespace fenrirds { ActiveDirectory(AuthBackend &adminBackend, const std::string &bindAddr = "0.0.0.0", int port = 389, int maxconn = 128, const std::string &adminPassword = ""); const std::string &adminPassword = "", const std::string &dcHost = ""); ~ActiveDirectory() override; /* Plugin interface */ Loading Loading @@ -279,6 +282,9 @@ namespace fenrirds { int32_t msgId, LdapSession &session); std::vector<uint8_t> handleUnbindRequest(int32_t msgId, LdapSession &session); /* ---- CLDAP netlogon ping (UDP 389, DC location) ---- */ std::vector<uint8_t> handleCldap(const uint8_t *data, size_t len); /* ---- LDAP Response Builders ---- */ std::vector<uint8_t> buildLdapMessage(int32_t msgId, uint8_t appTag, const std::vector<uint8_t> &protocolOp); Loading @@ -296,6 +302,9 @@ namespace fenrirds { dsdb::DomainMeta meta; std::unique_ptr<dsdb::Directory> dir; }; /* CLDAP netlogon response (needs Dom) */ std::vector<uint8_t> buildNetlogonResponse(const Dom &d, uint32_t ntVersion, const std::string &user); /* (re)load the domains from the registry, provisioning each */ void loadDomains(); /* the domain whose base DN contains dn (longest match), or nullptr */ Loading @@ -319,6 +328,7 @@ namespace fenrirds { int _Port; int _MaxConn; std::string _AdminPassword; std::string _DcHost; /* FQDN for CLDAP netlogon */ std::vector<Dom> _Domains; Loading @@ -328,6 +338,10 @@ namespace fenrirds { std::vector<netplus::socket*> _ServerSockets; void *_Event; netplus::udp *_Cldap = nullptr; std::thread _CldapThread; std::atomic<bool> _Running{false}; }; }; /* namespace fenrirds */