
Native C++ access to Active Directory over ADWS, no .NET, no WCF, no HTTP stack.
Native C++ access to Active Directory over ADWS, no .NET, no WCF, no HTTP stack.
BridgeHead is a C++20 static library implementing the full Active Directory Web Services (ADWS) protocol stack directly over TCP. Named after the AD bridgehead server, the gateway through which directory traffic flows, it gives your C++ code the same low-level access to port 9389 that PowerShell's Get-ADUser and Get-ADComputer use under the hood.
The transport layers wrap the one below via the common bridgehead::transport::IByteStream interface. NbfseCodec is a codec utility called by AdwsClient to encode/decode SOAP messages before and after 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
The entry point for consumers is bridgehead::adws::AdwsClient.
#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)
}
);
// 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-...
}
#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");
Put defaults to Replace (overwrites all existing values). Use ModifyOperation for fine-grained control on multi-valued attributes:
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"
);
Supply binary values in the bytes field of LdapModification. They are base64-encoded on the wire automatically:
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"}}}
);
#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 on a slow DC:
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.
#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]"}}});