
Accesso nativo in C++ ad Active Directory tramite ADWS, senza .NET, senza WCF, senza stack HTTP.
Accesso C++ nativo ad Active Directory via ADWS, senza .NET, senza WCF, senza stack HTTP.
BridgeHead è una libreria statica C++20 che implementa l'intero stack del protocollo Active Directory Web Services (ADWS) direttamente su TCP. Chiamata così dal server bridgehead AD, il gateway attraverso cui fluisce il traffico delle directory, offre al tuo codice C++ lo stesso accesso di basso livello alla porta 9389 che Get-ADUser e Get-ADComputer di PowerShell utilizzano internamente.
I layer di trasporto avvolgono quello sottostante tramite l'interfaccia comune bridgehead::transport::IByteStream. NbfseCodec è un'utilità di codec richiamata da AdwsClient per codificare/decodificare i messaggi SOAP prima e dopo il framing:```
AdwsClient WS-Enumeration + WS-Transfer [MS-ADDM]
├── NbfseCodec .NET Binary Format for SOAP [MC-NBFSE] (encode/decode)
└── NmfFramer .NET Message Framing [MC-NMF] (send/receive frames)
└── NnsSession .NET NegotiateStream [MS-NNS]
└── TcpSocket raw TCP/IP
Il punto di ingresso per i consumatori è `bridgehead::adws::AdwsClient`.
---
## Avvio rapido
### Query, enumerare tutti gli utenti```cpp
#include "bridgehead/adws/AdwsClient.hpp"
// NTLM (works on any host)
auto client = bridgehead::adws::AdwsClient::EnumerationClient(
"192.168.1.10", // DC IP or hostname
"DC01.corp.local", // DC FQDN, used in NMF Via header and Kerberos SPN
"CORP", // NetBIOS domain name
"Administrator", // username
"Passw0rd" // password
);
// Kerberos (username hidden on the wire; requires DC reachable on port 88)
auto client = bridgehead::adws::AdwsClient::EnumerationClient(
"192.168.1.10", "DC01.corp.local", "CORP",
"Administrator", "Passw0rd",
bridgehead::adws::AuthPackage::Kerberos
);
auto users = client.Query(
"(objectClass=user)",
{"sAMAccountName", "distinguishedName", "memberOf"}
);
for (auto& obj : users)
std::cout << obj.FirstValue("sAMAccountName") << '\n';
client.Enumerate( "(objectClass=computer)", {"dNSHostName", "operatingSystem"}, "", // empty = domain root base DN [](const bridgehead::adws::LdapObject& obj) { std::cout << obj.FirstValue("dNSHostName") << '\n'; return true; // return false to stop early (sends wsen:Release) } );
### Ambito e paginazione```cpp
// OneLevel scope, 50 objects per Pull round-trip
client.Query(
"(objectClass=user)",
{"sAMAccountName"},
"OU=Admins,DC=corp,DC=local",
50,
bridgehead::adws::SearchScope::OneLevel
);
auto objs = client.Query("(objectClass=user)", {"objectGUID", "objectSid"}); for (auto& obj : objs) { if (auto* b = obj.FirstBytes("objectGUID")) std::cout << bridgehead::adws::ParseGuid(b) << '\n'; // {XXXXXXXX-...} if (auto b = obj.FirstBytes("objectSid")) std::cout << bridgehead::adws::ParseSid(*b) << '\n'; // S-1-5-... }
### Decodifica del descrittore di sicurezza```cpp
#include "bridgehead/adws/SecurityDescriptor.hpp"
auto objs = client.Query("(objectClass=user)", {"nTSecurityDescriptor"});
if (auto* raw = objs[0].FirstBytes("nTSecurityDescriptor")) {
auto sd = bridgehead::adws::ParseSecurityDescriptor(*raw);
std::cout << "Owner: " << sd.ownerSid << '\n';
for (auto& ace : sd.dacl.aces)
std::cout << " type=" << (int)ace.type
<< " mask=0x" << std::hex << ace.mask
<< " sid=" << ace.sid << '\n';
}
auto rc = bridgehead::adws::AdwsClient::ResourceClient( "192.168.1.10", "DC01.corp.local", "CORP", "Administrator", "Passw0rd");
// Modify attributes rc.Put("CN=Alice,OU=Users,DC=corp,DC=local", { {"description", {"managed by bridgehead"}}, {"telephoneNumber", {"555-1234"}}, });
// Clear an attribute (both values and bytes empty = delete) rc.Put("CN=Alice,OU=Users,DC=corp,DC=local", { {"telephoneNumber", {}}, });
// Read back auto obj = rc.Get("CN=Alice,OU=Users,DC=corp,DC=local", {"description", "telephoneNumber"});
// Delete object rc.Delete("CN=TempUser,OU=Users,DC=corp,DC=local");
### Tipi di modifica LDAP, Add / Replace / Delete
`Put` di default usa `Replace` (sovrascrive tutti i valori esistenti). Usa `ModifyOperation` per un controllo granulare sugli attributi multivalore:```cpp
using bridgehead::adws::LdapModification;
using bridgehead::adws::ModifyOperation;
rc.Put("CN=Alice,OU=Users,DC=corp,DC=local", {
// Append a value to an existing multi-valued attribute
LdapModification{"otherTelephone", {"555-9999"}, {}, ModifyOperation::Add},
// Remove one specific value (leave others intact)
LdapModification{"otherTelephone", {"555-0000"}, {}, ModifyOperation::Delete},
// Replace is the default, explicit here for clarity
LdapModification{"description", {"updated"}, {}, ModifyOperation::Replace},
});
// Move to a different OU rc.Move( "CN=Alice,OU=OldOU,DC=corp,DC=local", // current DN "CN=Alice,OU=NewOU,DC=corp,DC=local" // new DN );
// Rename in place (same parent, new CN) rc.Move( "CN=Alice,OU=Users,DC=corp,DC=local", "CN=AliceSmith,OU=Users,DC=corp,DC=local" );
### Scrivere attributi binari
Fornisci valori binari nel campo `bytes` di `LdapModification`. Vengono codificati automaticamente in base64 sul filo:```cpp
std::vector<uint8_t> thumbnail = loadFile("photo.jpg");
rc.Put("CN=Alice,OU=Users,DC=corp,DC=local", {
LdapModification{"thumbnailPhoto", {}, {thumbnail}},
});
auto rf = bridgehead::adws::AdwsClient::ResourceFactoryClient( "192.168.1.10", "DC01.corp.local", "CORP", "Administrator", "Passw0rd");
rf.Create( "CN=NewUser,OU=Users,DC=corp,DC=local", "user", {{"sAMAccountName", {"newuser"}}, {"userAccountControl", {"512"}}} );
### Operazioni asincrone```cpp
#include "bridgehead/adws/AdwsClientAsync.hpp"
auto ac = bridgehead::adws::AdwsClientAsync::EnumerationClient(
"192.168.1.10", "DC01.corp.local", "CORP", "Administrator", "Passw0rd");
auto future = ac.QueryAsync("(objectClass=user)", {"sAMAccountName"});
// ... do other work while the query runs ...
auto users = future.get(); // blocks until complete; re-throws any exception
Timeout su un DC lento:```cpp if (future.wait_for(std::chrono::seconds(5)) == std::future_status::timeout) { // query is still running }
All operations have async variants: `QueryAsync`, `EnumerateAsync`, `GetAsync`, `PutAsync`, `DeleteAsync`, `CreateAsync`, `MoveAsync`.
### Pool di connessione (carichi di lavoro ad alta frequenza / multi-thread)```cpp
#include "bridgehead/adws/AdwsClientPool.hpp"
// Enumeration pool, Query / Enumerate
bridgehead::adws::AdwsClientPool pool({
.host = "192.168.1.10", .fqdn = "DC01.corp.local",
.domain = "CORP", .username = "Administrator", .password = "Passw0rd",
.maxSize = 4, // up to 4 concurrent authenticated sessions
});
auto users = pool.Query("(objectClass=user)", {"sAMAccountName"});
// Resource pool, Get / Put / Delete
bridgehead::adws::AdwsClientPool resPool({
.host = "192.168.1.10", .fqdn = "DC01.corp.local",
.domain = "CORP", .username = "Administrator", .password = "Passw0rd",
.maxSize = 4,
.endpoint = bridgehead::adws::PoolEndpoint::Resource,
});
auto obj = resPool.Get("CN=Alice,OU=Users,DC=corp,DC=local", {"mail"});
resPool.Put("CN=Alice,OU=Users,DC=corp,DC=local", {{"mail", {"[email protected]"}}});
// ResourceFactory pool, Create
bridgehead::adws::AdwsClientPool rfPool({
.host = "192.168.1.10", .fqdn = "DC01.corp.local",
.domain = "CORP", .username = "Administrator", .password = "Passw0rd",
.endpoint = bridgehead::adws::PoolEndpoint::ResourceFactory,
});
rfPool.Create("CN=NewUser,OU=Users,DC=corp,DC=local", "user",
{{"sAMAccountName", {"newuser"}}});
Tutti gli header pubblici si trovano in include/bridgehead/. La documentazione completa dell'API può essere generata con Doxygen (vedi Build).
bridgehead::adws::AdwsClientbridgehead::adws::AdwsClientAsyncWrapper asincrono basato su std::future. Stessi metodi factory di AdwsClient; ogni operazione restituisce un std::future<T>:```cpp
std::future<std::vector> f = ac.QueryAsync(...);
std::future e = ac.EnumerateAsync(filter, attrs, baseDN, callback);
std::future g = ac.GetAsync(...);
std::future h = ac.PutAsync(...);
std::future i = ac.DeleteAsync(...);
std::future j = ac.CreateAsync(...);
std::future k = ac.MoveAsync(...);
Per query concorrenti su più connessioni, usa `AdwsClientPool`.
### `bridgehead::adws::AdwsClientPool`
Pool thread-safe di sessioni pre-autenticate. Le connessioni vengono create in modo lazy e riutilizzate tra le chiamate.```cpp
pool.IdleCount(); // sessions currently idle
pool.TotalCount(); // idle + checked-out
pool.MaxSize(); // configured maximum pool size
pool.Move(dn, newDn); // Move/rename (Resource pool)
enum class SearchScope { Base, OneLevel, Subtree /default/ };
### `bridgehead::adws::LdapObject` / `LdapAttribute````cpp
struct LdapAttribute {
std::string name;
std::string syntax; // LdapSyntax OID, empty if absent
std::vector<std::string> values; // text values (raw base64 for binary)
std::vector<std::vector<uint8_t>> bytes; // decoded binary; parallel to values
};
struct LdapObject {
std::vector<LdapAttribute> attributes; // all returned attributes
const LdapAttribute* Find(const std::string& name) const; // case-insensitive
std::string FirstValue(const std::string& name) const;
const std::vector<uint8_t>* FirstBytes(const std::string& name) const;
};
std::string ParseGuid(const std::vector<uint8_t>& bytes); // → "{XXXXXXXX-XXXX-...}" std::string ParseSid (const std::vector<uint8_t>& bytes); // → "S-1-5-..."
### `FilterValue` e helper per la costruzione di filtri
`FilterValue` è un wrapper type-safe che esegue l'escape dei metacaratteri RFC 4515 §3 (`\`, `*`, `(`, `)`, NUL) al momento della costruzione. Utilizzalo con gli helper di costruzione per rendere l'iniezione LDAP strutturalmente impossibile:```cpp
// UNSAFE, raw string concatenation, easy to forget escaping
client.Query("(sAMAccountName=" + username + ")", ...);
// SAFE, FilterValue escapes on construction; FilterEq composes the assertion
FilterValue user = username;
client.Query(FilterEq("sAMAccountName", user), ...);
// Compose complex filters
client.Query(
FilterAnd({
FilterEq("objectClass", FilterValue::Raw("user")), // Raw() for safe literals
FilterOr({
FilterEq("sAMAccountName", user),
FilterEq("mail", FilterValue(email)),
}),
FilterNot(FilterPresent("userAccountControl")),
}), {"sAMAccountName", "mail"}
);
EscapeLdapFilter(str) è ancora disponibile come primitiva di basso livello quando è necessario costruire manualmente stringhe di filtro.
DnValue e helper per la costruzione di DNStesso schema di FilterValue, ma per i valori RDN di Distinguished Name (RFC 4514 §2.4). Escape i caratteri ,, +, ", \, <, >, ;, NUL, e # e spazio iniziali/finali.```cpp
// UNSAFE, comma injection breaks the DN structure
rc.Get("CN=" + username + ",OU=Users,DC=corp,DC=local", attrs);
// SAFE DnValue cn = username; // auto-escaped auto dn = BuildDn({DnAttr("CN", cn), "OU=Users", "DC=corp", "DC=local"}); rc.Get(dn, attrs);
// DnValue::Raw() for values you control auto dn2 = BuildDn({DnAttr("CN", DnValue::Raw("Service Account")), "OU=SvcAccounts", "DC=corp", "DC=local"});
### `bridgehead::adws::SecurityDescriptor````cpp
SecurityDescriptor ParseSecurityDescriptor(const std::vector<uint8_t>& bytes);
struct SecurityDescriptor {
uint16_t control; // SdControl::* flags
std::string ownerSid;
std::string groupSid;
bool hasDacl; Acl dacl;
bool hasSacl; Acl sacl;
};
struct Acl { std::vector<Ace> aces; };
struct Ace {
uint8_t type; // AceType::*
uint8_t flags; // AceFlags::*
uint32_t mask;
std::string sid;
std::string objectType; // GUID string, object ACEs only
std::string inheritedObjectType; // GUID string, object ACEs only
};
Constant namespaces: AceType::*, AceFlags::*, SdControl::*.
bridgehead::ConnectionError / AuthenticationError / ProtocolErrorDefinito in include/bridgehead/Exceptions.hpp (incluso transitivamente da AdwsClient.hpp). Tutti e tre estendono std::runtime_error:
| Type | Lanciato quando |
|---|---|
ConnectionError | Fallimento a livello TCP: rifiuto, timeout, errore I/O |
AuthenticationError | La negoziazione NTLM/Kerberos fallisce |
ProtocolError | Risposta del server malformata a qualsiasi livello di protocollo |
auto client = bridgehead::adws::AdwsClient::EnumerationClient(...);
auto users = client.Query(...);
} catch (const bridgehead::ConnectionError& e) { // unreachable DC, wrong port, timeout } catch (const bridgehead::AuthenticationError& e) { // bad credentials, KDC unreachable } catch (const bridgehead::ProtocolError& e) { // unexpected server response } catch (const bridgehead::adws::SoapFault& e) { // DC returned a SOAP fault (e.g. invalid filter) } // or coarse-grained: // } catch (const std::runtime_error& e) { ... }
### `bridgehead::adws::SoapFault`
Lanciata per qualsiasi risposta `s:Fault` dal DC invece di un semplice `std::runtime_error`:```cpp
struct SoapFault : std::runtime_error {
std::string code; // e.g. "Sender"
std::string subcode; // e.g. "InvalidEnumerationContext", empty if absent
std::string reason; // human-readable text
};
bridgehead::adws::LdapModificationUsato da Put e Create:```cpp
enum class ModifyOperation { Replace /default/, Add, Delete };
struct LdapModification { std::string name; std::vectorstd::string values; // text values std::vector<std::vector<uint8_t>> bytes; // binary values (base64-encoded on the wire) ModifyOperation operation = ModifyOperation::Replace; };
Quando sia `values` che `bytes` sono vuoti, l'attributo viene cancellato/eliminato indipendentemente da `operation`.
### `bridgehead::adws::ProtocolLimits`
Parametro finale opzionale su tutti i metodi factory, le factory `AdwsClientAsync` e `AdwsClientPool::Config::limits`. Tutti i campi hanno valori predefiniti sicuri, modificali solo se hai requisiti specifici:```cpp
struct ProtocolLimits {
uint32_t nmfMaxFrameBytes = 64 * 1024 * 1024; // max NMF frame (default 64 MiB)
uint32_t nnsMaxPayloadBytes = 16 * 1024 * 1024; // max NNS packet (default 16 MB)
int tcpKeepaliveIdleSec = 60; // idle seconds before first probe
int tcpKeepaliveIntervalSec = 10; // seconds between probes
int tcpKeepaliveProbeCount = 5; // probes before declaring dead (POSIX only)
};
Alza nmfMaxFrameBytes / nnsMaxPayloadBytes solo se stai leggendo oggetti con attributi binari insolitamente grandi (ad es. nTSecurityDescriptor con molti ACE, o thumbnailPhoto grande). Riduci i campi keepalive in ambienti cloud/NAT in cui le connessioni inattive vengono chiuse aggressivamente.
Helper costruttori nominati (factory statiche inline):```cpp LdapModification::Replace(name, values) // Replace with text values (default) LdapModification::ReplaceBinary(name, bytes) // Replace with binary values LdapModification::Append(name, values) // Add to multi-valued attribute LdapModification::Remove(name, values={}) // Remove specific values (or all) LdapModification::Clear(name) // Delete the attribute entirely
## Compilazione
**Requisiti:** CMake 3.25+ e un compilatore compatibile con C++20 (MSVC 2022+, GCC 12+, Clang 15+).```bash
# Configure and build (unit tests only, no live AD needed)
cmake -B build -A x64
cmake --build build --config Release
# Run unit tests
ctest --test-dir build -C Release --output-on-failure
# With integration tests (requires a live Domain Controller)
cmake -B build -A x64 -DBRIDGEHEAD_INTEGRATION_TESTS=ON
cmake --build build --config Release
./build/tests/Release/bridgehead_integration_tests.exe
# With CLI tool (adws_list, browse AD objects from the command line)
cmake -B build -A x64 -DBRIDGEHEAD_BUILD_TOOLS=ON
cmake --build build --config Release --target adws_list
./build/tools/Release/adws_list.exe --host <dc-ip> --fqdn <dc-fqdn> --domain <domain> --user <user>
# Generate API reference docs (requires Doxygen)
cmake --build build --target docs
# or directly:
doxygen Doxyfile
# Output: docs/doxygen/html/index.html
add_subdirectory(bridgehead) target_link_libraries(my_target PRIVATE bridgehead::bridgehead)
### Opzione B, pacchetto installato (`find_package`)```bash
cmake --install build --prefix /usr/local # or any install prefix
INPUT:```cmake find_package(bridgehead REQUIRED) target_link_libraries(my_target PRIVATE bridgehead::bridgehead)
`pugixml`, `ws2_32` e `secur32` (Windows) / `gssapi_krb5` (Linux/macOS) vengono tutti inclusi transitivamente.
---
## Supporto delle piattaforme
| Piattaforma | Backend di autenticazione | Stato |
|-------------|---------------------------|-------|
| Windows | SSPI (`secur32.dll`), NTLM e Kerberos | **Funzionante** |
| Linux / macOS | GSSAPI (`libgssapi_krb5`), SPNEGO / Kerberos | Compilato; non ancora testato in integrazione |
Sia `AuthPackage::Ntlm` che `AuthPackage::Kerberos` funzionano su Windows. Kerberos nasconde il nome utente in rete ed è preferito in produzione; NTLM è il fallback quando il KDC (porta 88) non è raggiungibile.
Su Linux/macOS il backend GSSAPI utilizza SPNEGO con Kerberos. Per il supporto NTLM installare il plugin `gss-ntlmssp`:```bash
apt install libgss-ntlmssp # Debian / Ubuntu
dnf install gssntlmssp # Fedora / RHEL
Senza il plugin, il server negozierà Kerberos. Le credenziali esplicite utente/password usano gss_acquire_cred_with_password (MIT Kerberos 1.9+); passare stringhe vuote per utilizzare la cache di credenziali predefinita (kinit). Il percorso GSSAPI si compila perfettamente ed è corretto per progettazione, ma non è ancora stato validato end-to-end contro un DC live, considerare il supporto Linux/macOS come beta.
Nota sulla crittografia: ADWS sulla porta 9389 è crittografato a livello NNS (chiave di sessione NTLM o Kerberos AES-256), non TLS. Questo è voluto, il DC non offre TLS su questa porta. Se è richiesta l'autenticazione del server basata su certificati, LDAPS (porta 636) è l'alternativa.
Recuperate automaticamente al momento della configurazione tramite cmake/Dependencies.cmake, nessuna installazione manuale necessaria:
| Libreria | Versione | Scopo |
|---|---|---|
| Catch2 | v3.5.2 | Framework di test unitari (solo target di test) |
| pugixml | v1.14 | XML DOM (solo parsing delle risposte ADWS) |
Niente OpenSSL. Niente Asio. Niente Boost.
NTLM su Linux/macOS richiede il plugin GSSAPI gss-ntlmssp (vedi Supporto piattaforma). Senza di esso il server negozierà Kerberos. NTLM su Windows funziona nativamente tramite SSPI.
DOM pugixml, ogni risposta SOAP viene completamente analizzata in un albero XML in memoria prima che qualsiasi risultato venga restituito. Questo è limitato dalla dimensione della pagina maxElements (default 256 oggetti per Pull), quindi l'intero set di risultati non viene mai tenuto in memoria contemporaneamente. La memoria diventa un problema solo con dimensioni di pagina molto grandi o oggetti che trasportano grandi attributi binari (ad es. nTSecurityDescriptor su oggetti con molti ACE). Un approccio SAX/streaming eliminerebbe questo problema ma non è attualmente pianificato.
Tutte le specifiche sono disponibili presso la documentazione di Microsoft Open Specifications.
Un ringraziamento speciale a IBM X-Force e al progetto SoaPy per l'ispirazione, la ricerca e il lavoro condiviso pubblicamente che hanno aiutato a informare questo progetto. Vorrei anche riconoscere Logan Goins, il creatore di SoaPy presso IBM X-Force Red, per lo sforzo e la visione che hanno reso possibile quel lavoro.
Vedere LICENSE per i dettagli.
| Metodo | Endpoint | Descrizione |
|---|
EnumerationClient(host, fqdn, domain, user, pass [, auth, timeoutMs, opTimeoutMs, limits]) | /Enumeration | Factory, connetti e autentica |
ResourceClient(...) | /Resource | Factory per Get / Put / Delete / Move |
ResourceFactoryClient(...) | /ResourceFactory | Factory per Create |
Query(filter, attrs [, baseDN, maxElems, scope]) | Enumeration | Raccoglie tutti i risultati in un vector |
Enumerate(filter, attrs, baseDN, callback [, maxElems, scope]) | Enumeration | Trasmette i risultati tramite callback |
Get(dn, attrs) | Resource | Legge gli attributi di un singolo oggetto |
Put(dn, modifications) | Resource | Modifica gli attributi |
Delete(dn) | Resource | Elimina l'oggetto |
Create(dn, objectClass, attrs) | ResourceFactory | Crea un nuovo oggetto |
Move(dn, newDn) | Resource | Sposta o rinomina un oggetto esistente |
| try { |
| Opzione | Predefinito | Descrizione |
|---|
BRIDGEHEAD_BUILD_TESTS | ON | Compila la suite di test unitari |
BRIDGEHEAD_INTEGRATION_TESTS | OFF | Compila i test di integrazione (richiede un DC attivo) |
BRIDGEHEAD_BUILD_TOOLS | OFF | Compila lo strumento CLI adws_list |