
Acesso nativo em C++ ao Active Directory via ADWS, sem .NET, sem WCF, sem pilha HTTP.
Acesso nativo em C++ ao Active Directory via ADWS, sem .NET, sem WCF, sem pilha HTTP.
BridgeHead é uma biblioteca estática C++20 que implementa toda a pilha do protocolo Active Directory Web Services (ADWS) diretamente sobre TCP. Nomeado em homenagem ao servidor bridgehead do AD, o gateway pelo qual o tráfego do diretório flui, ele dá ao seu código C++ o mesmo acesso de baixo nível à porta 9389 que o PowerShell usa internamente com Get-ADUser e Get-ADComputer.
As camadas de transporte envolvem a camada inferior através da interface comum bridgehead::transport::IByteStream. NbfseCodec é um utilitário de codec chamado por AdwsClient para codificar/decodificar mensagens SOAP antes e depois do encapsulamento:```
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
O ponto de entrada para consumidores é `bridgehead::adws::AdwsClient`.
---
## Início rápido
### Consultar e enumerar todos os usuários```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) } );
### Escopo e paginação```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ção do descritor de segurança```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 modificação LDAP, Adicionar / Substituir / Excluir
`Put` padrão é `Replace` (substitui todos os valores existentes). Use `ModifyOperation` para controle granular em atributos com múltiplos 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" );
### Escrever atributos binários
Forneça valores binários no campo `bytes` de `LdapModification`. Eles são codificados em base64 no fio automaticamente:```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"}}} );
### Operações assí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
Timeout em um DC lento:```cpp if (future.wait_for(std::chrono::seconds(5)) == std::future_status::timeout) { // query is still running }
Todas as operações têm variantes assíncronas: `QueryAsync`, `EnumerateAsync`, `GetAsync`, `PutAsync`, `DeleteAsync`, `CreateAsync`, `MoveAsync`.
### Connection pool (cargas de trabalho de alta frequência / multithread)```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 os cabeçalhos públicos estão em include/bridgehead/. A documentação completa da API pode ser gerada com Doxygen (veja Build).
bridgehead::adws::AdwsClientbridgehead::adws::AdwsClientAsyncWrapper assíncrono baseado em std::future. Mesmos métodos factory do AdwsClient; cada operação retorna um 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 concorrentes em várias conexões, use `AdwsClientPool`.
### `bridgehead::adws::AdwsClientPool`
Pool thread-safe de sessões pré-autenticadas. As conexões são criadas de forma preguiçosa e reutilizadas entre chamadas.```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 auxiliares de construção de filtros
`FilterValue` é um wrapper de tipo seguro que escapa os metacaracteres RFC 4515 §3 (`\`, `*`, `(`, `)`, NUL) na construção. Use-o com os auxiliares de construção para tornar a injeção de LDAP estruturalmente impossível:```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) ainda está disponível como uma primitiva de baixo nível quando você precisa construir strings de filtro manualmente.
DnValue e auxiliares de construção de DNMesmo padrão que FilterValue, mas para valores de RDN de Distinguished Name (RFC 4514 §2.4). Escapa ,, +, ", \, <, >, ;, NUL, e # e espaço no início/fim:```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 em include/bridgehead/Exceptions.hpp (incluído transitivamente por AdwsClient.hpp). Todos os três estendem std::runtime_error:
| Tipo | Lançado quando |
|---|---|
ConnectionError | Falha no nível TCP: recusado, timeout, erro de E/S |
AuthenticationError | A negociação NTLM/Kerberos falha |
ProtocolError | Resposta de servidor malformada em qualquer camada de 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`
Lançada em qualquer resposta `s:Fault` do DC em vez de um simples `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 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 ambos `values` e `bytes` estiverem vazios, o atributo é limpo/excluído independentemente de `operation`.
### `bridgehead::adws::ProtocolLimits`
Parâmetro opcional final em todos os métodos de fábrica, fábricas `AdwsClientAsync` e `AdwsClientPool::Config::limits`. Todos os campos têm padrões seguros; altere-os apenas se tiver 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 apenas se estiver lendo objetos com atributos binários excepcionalmente grandes (por exemplo, nTSecurityDescriptor com muitos ACEs, ou thumbnailPhoto grande). Reduza os campos keepalive em ambientes cloud/NAT onde conexões ociosas são descartadas agressivamente.
Auxiliares de construtor nomeado (fábricas estáticas 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
---
## Compilação
**Requisitos:** CMake 3.25+ e um compilador compatível com 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)
### Opção B, pacote instalado (`find_package`)```bash
cmake --install build --prefix /usr/local # or any install prefix
ENTRADA:```cmake find_package(bridgehead REQUIRED) target_link_libraries(my_target PRIVATE bridgehead::bridgehead)
`pugixml`, `ws2_32` e `secur32` (Windows) / `gssapi_krb5` (Linux/macOS) são todos incluídos transitivamente.
---
## Suporte de plataforma
| Plataforma | Backend de autenticação | Status |
|----------|-------------|--------|
| Windows | SSPI (`secur32.dll`), NTLM e Kerberos | **Funcionando** |
| Linux / macOS | GSSAPI (`libgssapi_krb5`), SPNEGO / Kerberos | Compilado; ainda não testado em integração |
Ambos `AuthPackage::Ntlm` e `AuthPackage::Kerberos` funcionam no Windows. O Kerberos oculta o nome de usuário na rede e é preferido em produção; o NTLM é o fallback quando o KDC (porta 88) está inacessível.
No Linux/macOS, o backend GSSAPI usa SPNEGO com Kerberos. Para suporte a NTLM, instale o plugin `gss-ntlmssp`:```bash
apt install libgss-ntlmssp # Debian / Ubuntu
dnf install gssntlmssp # Fedora / RHEL
Sem o plugin, o servidor negociará Kerberos. Credenciais explícitas de usuário/senha usam gss_acquire_cred_with_password (MIT Kerberos 1.9+); passe strings vazias para usar o cache de credenciais padrão (kinit). O caminho GSSAPI compila de forma limpa e está correto por design, mas ainda não foi validado de ponta a ponta contra um DC ativo, considere o suporte Linux/macOS como beta.
Nota sobre criptografia: O ADWS na porta 9389 é criptografado na camada NNS (chave de sessão NTLM ou Kerberos AES-256), não TLS. Isso é por design, o DC não oferece TLS nesta porta. Se a autenticação de servidor baseada em certificado for necessária, LDAPS (porta 636) é a alternativa.
Obtidos automaticamente no momento da configuração via cmake/Dependencies.cmake, nenhuma instalação manual necessária:
| Library | Version | Purpose |
|---|---|---|
| Catch2 | v3.5.2 | Framework de testes unitários (apenas targets de teste) |
| pugixml | v1.14 | DOM XML (apenas parsing de respostas ADWS) |
Sem OpenSSL. Sem Asio. Sem Boost.
NTLM no Linux/macOS requer o plugin GSSAPI gss-ntlmssp (veja Suporte de plataforma). Sem ele, o servidor negociará Kerberos em vez disso. NTLM no Windows funciona nativamente via SSPI.
DOM pugixml, cada resposta SOAP é totalmente analisada em uma árvore XML em memória antes que qualquer resultado seja retornado. Isso é limitado pelo tamanho da página maxElements (padrão 256 objetos por Pull), então o conjunto completo de resultados nunca é mantido em memória de uma só vez. A memória só se torna uma preocupação com tamanhos de página muito grandes ou objetos que carregam grandes atributos binários (por exemplo, nTSecurityDescriptor em objetos com muitos ACEs). Uma abordagem SAX/streaming eliminaria isso, mas não está planejada atualmente.
Todas as especificações estão disponíveis na documentação de Especificações Abertas da Microsoft.
Agradecimentos especiais à IBM X-Force e ao projeto SoaPy por sua inspiração, pesquisa e trabalho compartilhado publicamente que ajudaram a informar este projeto. Gostaria também de reconhecer Logan Goins, o criador do SoaPy na IBM X-Force Red, pelo esforço e percepção que tornaram esse trabalho possível.
Consulte LICENSE para detalhes.
| Método | Endpoint | Descrição |
|---|
EnumerationClient(host, fqdn, domain, user, pass [, auth, timeoutMs, opTimeoutMs, limits]) | /Enumeration | Factory, conectar e autenticar |
ResourceClient(...) | /Resource | Factory para Get / Put / Delete / Move |
ResourceFactoryClient(...) | /ResourceFactory | Factory para Create |
Query(filter, attrs [, baseDN, maxElems, scope]) | Enumeration | Coletar todos os resultados em um vector |
Enumerate(filter, attrs, baseDN, callback [, maxElems, scope]) | Enumeration | Transmitir resultados via callback |
Get(dn, attrs) | Resource | Ler atributos de um único objeto |
Put(dn, modifications) | Resource | Modificar atributos |
Delete(dn) | Resource | Excluir objeto |
Create(dn, objectClass, attrs) | ResourceFactory | Criar novo objeto |
Move(dn, newDn) | Resource | Mover ou renomear um objeto existente |
| try { |
| Opção | Padrão | Descrição |
|---|
BRIDGEHEAD_BUILD_TESTS | ON | Compilar o conjunto de testes unitários |
BRIDGEHEAD_INTEGRATION_TESTS | OFF | Compilar testes de integração (requer um DC ativo) |
BRIDGEHEAD_BUILD_TOOLS | OFF | Compilar a ferramenta CLI adws_list |