
ADWS를 통한 Active Directory에 대한 네이티브 C++ 접근, .NET, WCF, HTTP 스택 불필요.
ADWS를 통한 Active Directory에 대한 네이티브 C++ 접근, .NET, WCF, HTTP 스택 없음.
BridgeHead는 TCP 위에서 직접 Active Directory Web Services (ADWS) 프로토콜 스택 전체를 구현하는 C++20 정적 라이브러리입니다. 디렉터리 트래픽이 흐르는 게이트웨이인 AD 브리지헤드 서버의 이름을 따서 명명되었으며, PowerShell의 Get-ADUser와 Get-ADComputer가 내부적으로 사용하는 9389 포트에 대한 동일한 저수준 접근을 C++ 코드에 제공합니다.
전송 계층은 공통 bridgehead::transport::IByteStream 인터페이스를 통해 하위 계층을 래핑합니다. NbfseCodec은 AdwsClient가 호출하는 코덱 유틸리티로, 프레이밍 전후에 SOAP 메시지를 인코딩/디코딩합니다.```
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
소비자를 위한 진입점은 `bridgehead::adws::AdwsClient`입니다.
---
## 빠른 시작
### 사용자 조회 및 열거```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) } );
### 범위 및 페이지 매김```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-... }
### 보안 설명자 디코딩```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");
### LDAP 수정 유형, Add / Replace / Delete
`Put`은 기본적으로 `Replace`로 설정됩니다 (모든 기존 값을 덮어씁니다). 다중 값 속성에 대한 세밀한 제어를 위해 `ModifyOperation`을 사용하세요:```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" );
### 이진 속성 쓰기
이진 값은 `LdapModification`의 `bytes` 필드에 제공합니다. 자동으로 와이어에서 base64로 인코딩됩니다.```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"}}} );
### Async 작업```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
느린 DC에서의 시간 초과:```cpp if (future.wait_for(std::chrono::seconds(5)) == std::future_status::timeout) { // query is still running }
모든 작업에는 비동기 변형이 있습니다: `QueryAsync`, `EnumerateAsync`, `GetAsync`, `PutAsync`, `DeleteAsync`, `CreateAsync`, `MoveAsync`.
### 연결 풀 (고빈도 / 멀티스레드 작업 부하)```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"}}});
모든 공개 헤더는 include/bridgehead/에 있습니다. 전체 API 문서는 Doxygen으로 생성할 수 있습니다(Build 참조).
bridgehead::adws::AdwsClientbridgehead::adws::AdwsClientAsyncstd::future 기반 비동기 래퍼. AdwsClient와 동일한 팩토리 메서드; 각 작업은 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(...);
여러 연결에 걸친 동시 쿼리를 위해 `AdwsClientPool`을 사용하세요.
### `bridgehead::adws::AdwsClientPool`
사전 인증된 세션의 스레드 안전 풀입니다. 연결은 지연 생성되고 호출 간에 재사용됩니다.```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` 및 필터 빌더 도우미
`FilterValue`는 RFC 4515 §3 메타문자(`\`, `*`, `(`, `)`, NUL)를 생성 시 이스케이프하는 타입 안전 래퍼입니다. 빌더 도우미와 함께 사용하면 LDAP 인젝션이 구조적으로 불가능해집니다:```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)는 필터 문자열을 수동으로 빌드해야 할 때 사용할 수 있는 저수준 프리미티브로 계속 제공됩니다.
DnValue 및 DN 빌더 도우미FilterValue와 동일한 패턴이지만, Distinguished Name RDN 값(RFC 4514 §2.4)에 대한 것입니다. ,, +, ", \, <, >, ;, NUL, 그리고 앞/뒤의 #와 공백을 이스케이프합니다.```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 / ProtocolErrorinclude/bridgehead/Exceptions.hpp에 정의됨 (AdwsClient.hpp에 의해 간접적으로 포함됨). 세 가지 모두 std::runtime_error를 확장합니다:
| 유형 | 발생 조건 |
|---|---|
ConnectionError | TCP 수준 실패: 연결 거부, 시간 초과, I/O 오류 |
AuthenticationError | NTLM/Kerberos 협상 실패 |
ProtocolError | 프로토콜 계층에서 잘못된 형식의 서버 응답 |
| try { |
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`
모든 `s:Fault` 응답에 대해 일반적인 `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::LdapModificationPut 및 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; };
`values`와 `bytes`가 모두 비어 있으면 `operation`에 관계없이 속성이 지워지거나 삭제됩니다.
### `bridgehead::adws::ProtocolLimits`
모든 팩토리 메서드, `AdwsClientAsync` 팩토리 및 `AdwsClientPool::Config::limits`의 선택적 마지막 매개변수입니다. 모든 필드에는 안전한 기본값이 있으므로 특정 요구 사항이 있는 경우에만 변경하십시오:```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)
};
nmfMaxFrameBytes / nnsMaxPayloadBytes 값을 높이십시오: 비정상적으로 큰 이진 속성을 가진 객체를 읽을 때만 해당됩니다 (예: 많은 ACE가 포함된 nTSecurityDescriptor 또는 큰 thumbnailPhoto). 유휴 연결이 적극적으로 종료되는 클라우드/NAT 환경에서는 keepalive 필드를 낮추십시오.
명명된 생성자 도우미(인라인 정적 팩토리):```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
---
## 빌드
**요구 사항:** CMake 3.25+ 및 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)
### 옵션 B, 설치된 패키지 (find_package)```bash
cmake --install build --prefix /usr/local # or any install prefix
git clone https://github.com/example/wizard-poc.git
cd wizard-poc
python3 setup.py install
위저드 시스템은 사용자가 단계별 지침을 통해 구성 프로세스를 안내받을 수 있도록 설계되었습니다. 동적 입력 유효성 검사와 오류 처리를 지원합니다.
`pugixml`, `ws2_32`, `secur32` (Windows) / `gssapi_krb5` (Linux/macOS)는 모두 전이적으로 포함됩니다.
---
## 플랫폼 지원
| 플랫폼 | 인증 백엔드 | 상태 |
|----------|-------------|--------|
| Windows | SSPI (`secur32.dll`), NTLM 및 Kerberos | **작동 중** |
| Linux / macOS | GSSAPI (`libgssapi_krb5`), SPNEGO / Kerberos | 컴파일됨; 아직 통합 테스트되지 않음 |
`AuthPackage::Ntlm`과 `AuthPackage::Kerberos`는 모두 Windows에서 작동합니다. Kerberos는 전송 중 사용자 이름을 숨기며 프로덕션 환경에서 선호됩니다. NTLM은 KDC(포트 88)에 연결할 수 없을 때 대체 수단입니다.
Linux/macOS에서 GSSAPI 백엔드는 SPNEGO와 Kerberos를 사용합니다. NTLM 지원을 위해 `gss-ntlmssp` 플러그인을 설치하십시오:```bash
apt install libgss-ntlmssp # Debian / Ubuntu
dnf install gssntlmssp # Fedora / RHEL
플러그인이 없으면 서버는 Kerberos로 협상합니다. 명시적인 사용자/비밀번호 자격 증명은 gss_acquire_cred_with_password(MIT Kerberos 1.9+)를 사용하며, 기본 자격 증명 캐시(kinit)를 사용하려면 빈 문자열을 전달합니다. GSSAPI 경로는 깔끔하게 컴파일되며 설계상 올바르지만, 실제 DC에 대해 종단간 검증이 아직 이루어지지 않았으므로 Linux/macOS 지원은 베타로 간주하십시오.
암호화 참고: 포트 9389의 ADWS는 NNS 계층(NTLM 세션 키 또는 Kerberos AES-256)에서 암호화되며, TLS가 아닙니다. 이는 설계상 의도된 것이며, DC는 이 포트에서 TLS를 제공하지 않습니다. 인증서 기반 서버 인증이 필요한 경우 LDAPS(포트 636)가 대안입니다.
cmake/Dependencies.cmake를 통해 구성 시 자동으로 가져오므로 수동 설치가 필요하지 않습니다:
OpenSSL 없음. Asio 없음. Boost 없음.
Linux/macOS의 NTLM은 gss-ntlmssp GSSAPI 플러그인이 필요합니다(플랫폼 지원 참조). 이 플러그인이 없으면 서버는 대신 Kerberos로 협상합니다. Windows의 NTLM은 SSPI를 통해 기본적으로 작동합니다.
pugixml DOM, 각 SOAP 응답은 결과가 반환되기 전에 메모리 내 XML 트리로 완전히 구문 분석됩니다. 이는 maxElements 페이지 크기(기본적으로 Pull당 256개 개체)에 의해 제한되므로 전체 결과 집합이 한 번에 메모리에 저장되지 않습니다. 메모리는 매우 큰 페이지 크기나 많은 ACE가 있는 개체의 nTSecurityDescriptor와 같은 큰 이진 속성을 가진 개체의 경우에만 문제가 됩니다. SAX/스트리밍 접근 방식이 이를 해결할 수 있지만 현재 계획되어 있지 않습니다.
모든 사양은 Microsoft의 Open Specifications 문서에서 확인할 수 있습니다.
이 프로젝트에 영감과 연구, 그리고 공개적으로 공유된 작업을 제공해 주신 IBM X-Force와 SoaPy 프로젝트에 특별히 감사드립니다. 또한 IBM X-Force Red의 SoaPy 창시자인 Logan Goins에게도 그 작업을 가능하게 한 노력과 통찰력에 감사를 표합니다.
자세한 내용은 LICENSE를 참조하십시오.
| 메서드 | 엔드포인트 | 설명 |
|---|
EnumerationClient(host, fqdn, domain, user, pass [, auth, timeoutMs, opTimeoutMs, limits]) | /Enumeration | 팩토리, 연결 및 인증 |
ResourceClient(...) | /Resource | Get / Put / Delete / Move를 위한 팩토리 |
ResourceFactoryClient(...) | /ResourceFactory | 생성을 위한 팩토리 |
Query(filter, attrs [, baseDN, maxElems, scope]) | Enumeration | 모든 결과를 vector로 수집 |
Enumerate(filter, attrs, baseDN, callback [, maxElems, scope]) | Enumeration | 콜백을 통해 결과 스트리밍 |
Get(dn, attrs) | Resource | 단일 객체의 속성 읽기 |
Put(dn, modifications) | Resource | 속성 수정 |
Delete(dn) | Resource | 객체 삭제 |
Create(dn, objectClass, attrs) | ResourceFactory | 새 객체 생성 |
Move(dn, newDn) | Resource | 기존 객체 이동 또는 이름 변경 |
| 옵션 | 기본값 | 설명 |
|---|
BRIDGEHEAD_BUILD_TESTS | ON | 단위 테스트 스위트 빌드 |
BRIDGEHEAD_INTEGRATION_TESTS | OFF | 통합 테스트 빌드 (활성 DC 필요) |
BRIDGEHEAD_BUILD_TOOLS | OFF | adws_list CLI 도구 빌드 |