
Acceso nativo en C++ a Active Directory a través de ADWS, sin .NET, sin WCF, sin pila HTTP.
Acceso nativo en C++ a Active Directory a través de ADWS, sin .NET, sin WCF, sin pila HTTP.
BridgeHead es una librería estática en C++20 que implementa la pila completa del protocolo Active Directory Web Services (ADWS) directamente sobre TCP. Nombrado en honor al servidor puente de AD, la puerta de enlace por la que fluye el tráfico del directorio, le brinda a tu código C++ el mismo acceso de bajo nivel al puerto 9389 que usan internamente Get-ADUser y Get-ADComputer de PowerShell.
Las capas de transporte envuelven la capa inferior a través de la interfaz común bridgehead::transport::IByteStream. NbfseCodec es una utilidad de códec llamada por AdwsClient para codificar/decodificar mensajes SOAP antes y después del encuadre:```
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
El punto de entrada para los consumidores es `bridgehead::adws::AdwsClient`.
---
## Inicio rápido
### Consultar, enumerar todos los usuarios```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) } );
### Alcance y paginación```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-... }
### Decodificación de descriptores de seguridad```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");
### Tipos de modificación LDAP, Agregar / Reemplazar / Eliminar
`Put` por defecto es `Replace` (sobrescribe todos los valores existentes). Use `ModifyOperation` para un control detallado sobre atributos con múltiples valores:```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" );
### Escribir atributos binarios
Proporcione valores binarios en el campo `bytes` de `LdapModification`. Se codifican automáticamente en base64 en el cable:```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"}}} );
### Operaciones asíncronas```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
Tiempo de espera en un DC lento:```cpp if (future.wait_for(std::chrono::seconds(5)) == std::future_status::timeout) { // query is still running }
Todas las operaciones tienen variantes asíncronas: `QueryAsync`, `EnumerateAsync`, `GetAsync`, `PutAsync`, `DeleteAsync`, `CreateAsync`, `MoveAsync`.
### Pool de conexiones (cargas de trabajo de alta frecuencia / multi-hilo)```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"}}});
Todos los encabezados públicos se encuentran en include/bridgehead/. La documentación completa de la API se puede generar con Doxygen (consulte Build).
bridgehead::adws::AdwsClientbridgehead::adws::AdwsClientAsyncContenedor asíncrono basado en std::future. Mismos métodos de fábrica que AdwsClient; cada operación devuelve 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(...);
Para consultas concurrentes a través de múltiples conexiones, use `AdwsClientPool`.
### `bridgehead::adws::AdwsClientPool`
Pool seguro para subprocesos de sesiones pre-autenticadas. Las conexiones se crean de forma diferida y se reutilizan entre llamadas.```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` y ayudantes de construcción de filtros
`FilterValue` es un envoltorio seguro de tipos que escapa los metacaracteres de RFC 4515 §3 (`\`, `*`, `(`, `)`, NUL) en la construcción. Úsalo con los ayudantes de construcción para hacer que la inyección LDAP sea estructuralmente imposible:```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) sigue estando disponible como una primitiva de bajo nivel cuando necesitas construir cadenas de filtro manualmente.
DnValue y ayudantes de construcción de DNMismo patrón que FilterValue, pero para valores RDN de Distinguished Name (RFC 4514 §2.4). Escapa ,, +, ", \, <, >, ;, NUL, y # y espacio al inicio/final:```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 / ProtocolErrorDefinido en include/bridgehead/Exceptions.hpp (incluido transitivamente por AdwsClient.hpp). Los tres extienden std::runtime_error:
| Tipo | Lanzado cuando |
|---|---|
ConnectionError | Fallo a nivel TCP: rechazado, tiempo de espera, error de E/S |
AuthenticationError | La negociación NTLM/Kerberos falla |
ProtocolError | Respuesta del servidor malformada en cualquier capa del protocolo |
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`
Lanzada en cualquier respuesta `s:Fault` del DC en lugar de un simple `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::LdapModificationUsado por Put y 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; };
Cuando tanto `values` como `bytes` están vacíos, el atributo se borra/elimina independientemente de `operation`.
### `bridgehead::adws::ProtocolLimits`
Parámetro opcional al final de todos los métodos de fábrica, fábricas de `AdwsClientAsync` y `AdwsClientPool::Config::limits`. Todos los campos tienen valores predeterminados seguros, solo cámbielos si tiene requisitos específicos:```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)
};
Aumente nmfMaxFrameBytes / nnsMaxPayloadBytes solo si está leyendo objetos con atributos binarios inusualmente grandes (p.ej., nTSecurityDescriptor con muchos ACEs, o thumbnailPhoto grande). Reduzca los campos de keepalive en entornos de nube/NAT donde las conexiones inactivas se cierran de manera agresiva.
Constructores auxiliares con nombre (inline static factories):```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
---
## Compilación
**Requisitos:** CMake 3.25+ y un compilador compatible 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)
### Opción B, paquete instalado (`find_package`)```bash
cmake --install build --prefix /usr/local # or any install prefix
Please provide the Markdown content to translate.```cmake find_package(bridgehead REQUIRED) target_link_libraries(my_target PRIVATE bridgehead::bridgehead)
`pugixml`, `ws2_32`, y `secur32` (Windows) / `gssapi_krb5` (Linux/macOS) se incluyen transitivamente.
---
## Soporte de plataforma
| Plataforma | Backend de autenticación | Estado |
|----------|-------------|--------|
| Windows | SSPI (`secur32.dll`), NTLM y Kerberos | **Funcionando** |
| Linux / macOS | GSSAPI (`libgssapi_krb5`), SPNEGO / Kerberos | Compilado; aún no probado en integración |
Tanto `AuthPackage::Ntlm` como `AuthPackage::Kerberos` funcionan en Windows. Kerberos oculta el nombre de usuario en la red y es preferido en producción; NTLM es la alternativa cuando el KDC (puerto 88) no está accesible.
En Linux/macOS, el backend GSSAPI utiliza SPNEGO con Kerberos. Para soporte NTLM, instala el plugin `gss-ntlmssp`:```bash
apt install libgss-ntlmssp # Debian / Ubuntu
dnf install gssntlmssp # Fedora / RHEL
Sin el complemento, el servidor negociará Kerberos. Las credenciales explícitas de usuario/contraseña usan gss_acquire_cred_with_password (MIT Kerberos 1.9+); pase cadenas vacías para usar el caché de credenciales predeterminado (kinit). La ruta GSSAPI compila limpiamente y es correcta por diseño, pero aún no se ha validado de extremo a extremo contra un DC real, considere el soporte de Linux/macOS como beta.
Nota de cifrado: ADWS en el puerto 9389 está cifrado en la capa NNS (clave de sesión NTLM o Kerberos AES-256), no TLS. Esto es por diseño, el DC no ofrece TLS en este puerto. Si se requiere autenticación de servidor basada en certificados, LDAPS (puerto 636) es la alternativa.
Obtenidas automáticamente en tiempo de configuración a través de cmake/Dependencies.cmake, no se necesita instalación manual:
| Library | Version | Purpose |
|---|---|---|
| Catch2 | v3.5.2 | Marco de pruebas unitarias (solo objetivos de prueba) |
| pugixml | v1.14 | DOM XML (solo análisis de respuestas ADWS) |
Sin OpenSSL. Sin Asio. Sin Boost.
NTLM en Linux/macOS requiere el complemento GSSAPI gss-ntlmssp (consulte Soporte de plataforma). Sin él, el servidor negociará Kerberos en su lugar. NTLM en Windows funciona de forma nativa a través de SSPI.
pugixml DOM, cada respuesta SOAP se analiza completamente en un árbol XML en memoria antes de devolver cualquier resultado. Esto está limitado por el tamaño de página maxElements (por defecto 256 objetos por Pull), por lo que el conjunto completo de resultados nunca se mantiene en memoria a la vez. La memoria solo se convierte en una preocupación con tamaños de página muy grandes u objetos que llevan atributos binarios grandes (por ejemplo, nTSecurityDescriptor en objetos con muchos ACE). Un enfoque SAX/streaming eliminaría esto, pero no está planeado actualmente.
Todas las especificaciones están disponibles en la documentación de Open Specifications de Microsoft.
Agradecimientos especiales a IBM X-Force y al proyecto SoaPy por su inspiración, investigación y trabajo compartido públicamente que ayudaron a informar este proyecto. También me gustaría reconocer a Logan Goins, el creador de SoaPy en IBM X-Force Red, por el esfuerzo y la visión que hicieron posible ese trabajo.
Consulte LICENSE para obtener más detalles.
| Método | Endpoint | Descripción |
|---|
EnumerationClient(host, fqdn, domain, user, pass [, auth, timeoutMs, opTimeoutMs, limits]) | /Enumeration | Fábrica, conectar y autenticar |
ResourceClient(...) | /Resource | Fábrica para Get / Put / Delete / Move |
ResourceFactoryClient(...) | /ResourceFactory | Fábrica para Create |
Query(filter, attrs [, baseDN, maxElems, scope]) | Enumeration | Recopilar todos los resultados en un vector |
Enumerate(filter, attrs, baseDN, callback [, maxElems, scope]) | Enumeration | Transmitir resultados mediante callback |
Get(dn, attrs) | Resource | Leer atributos de un solo objeto |
Put(dn, modifications) | Resource | Modificar atributos |
Delete(dn) | Resource | Eliminar objeto |
Create(dn, objectClass, attrs) | ResourceFactory | Crear nuevo objeto |
Move(dn, newDn) | Resource | Mover o renombrar un objeto existente |
| try { |
| Opción | Predeterminado | Descripción |
|---|
BRIDGEHEAD_BUILD_TESTS | ON | Compila el conjunto de pruebas unitarias |
BRIDGEHEAD_INTEGRATION_TESTS | OFF | Compila pruebas de integración (requiere un DC en funcionamiento) |
BRIDGEHEAD_BUILD_TOOLS | OFF | Compila la herramienta CLI adws_list |