Commit 94d9007e authored by jan.koester's avatar jan.koester
Browse files

bugfixes

parent e699edcb
Loading
Loading
Loading
Loading
Loading
+89 −24
Changes for plugin/ad/ad.cpp: 89 added lines, 24 removed lines.
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@

#include <iostream>
#include <cstring>
#include <map>
#include <algorithm>
#include <sstream>
#include <memory>
@@ -474,6 +475,7 @@ void ActiveDirectory::CreateConnection(std::shared_ptr<netplus::con> &res) {
}

void ActiveDirectory::ConnectEvent(netplus::con &curcon, const int, ULONG_PTR) {
    std::cerr << "[LDAP] TCP connect" << std::endl;
    std::lock_guard<std::mutex> lock(_Lock);
    _Sessions[&curcon] = LdapSession();
}
@@ -488,6 +490,9 @@ void ActiveDirectory::ResponseEvent(netplus::con &, const int, ULONG_PTR) {}
void ActiveDirectory::RequestEvent(netplus::con &curcon, const int, ULONG_PTR) {
    if (curcon.RecvData.empty())
        return;
    std::cerr << "[LDAP] recv " << curcon.RecvData.size() << "B (first=0x"
              << std::hex << (curcon.RecvData.empty() ? 0 : (unsigned char)curcon.RecvData[0])
              << std::dec << ")" << std::endl;

    LdapSession *session = nullptr;
    {
@@ -656,8 +661,11 @@ std::vector<uint8_t> ActiveDirectory::handleBindRequest(const uint8_t *content,
            session.bindDN.clear();
            return buildLdapMessage(msgId, appResp, buildLdapResult(LDAP_SUCCESS, "", ""));
        }
        if (authenticateSimple(bindDN, password, session))
        if (authenticateSimple(bindDN, password, session)) {
            std::cerr << "[LDAP] bind SIMPLE dn='" << bindDN << "' -> SUCCESS" << std::endl;
            return buildLdapMessage(msgId, appResp, buildLdapResult(LDAP_SUCCESS, "", ""));
        }
        std::cerr << "[LDAP] bind SIMPLE dn='" << bindDN << "' -> INVALID_CREDENTIALS" << std::endl;
        return buildLdapMessage(msgId, appResp,
            buildLdapResult(LDAP_INVALID_CREDENTIALS, "",
                            "80090308: LdapErr: DSID-0C090447, data 52e"));
@@ -693,8 +701,11 @@ std::vector<uint8_t> ActiveDirectory::handleBindRequest(const uint8_t *content,
        namespace gss = fenrirds::crypto::gss;
        std::string mech = toLower(mechanism);
        bool spnego = (mech == "gss-spnego");
        std::cerr << "[LDAP] bind SASL mech='" << mechanism << "' gssState="
                  << int(session.gssState) << " inTok=" << inTok.size() << "B" << std::endl;
        if (mech != "gssapi" && !spnego) {
            session.gssState = LdapSession::GssNone;
            std::cerr << "[LDAP] bind SASL -> AUTH_METHOD_NOT_SUPPORTED (mech)" << std::endl;
            return bindResp(LDAP_AUTH_METHOD_NOT_SUPPORTED, nullptr);
        }
        if (!g_KdcPlugin)
@@ -716,8 +727,13 @@ std::vector<uint8_t> ActiveDirectory::handleBindRequest(const uint8_t *content,
                    return bindResp(LDAP_INVALID_CREDENTIALS, nullptr);
            }
            GssAcceptResult res;
            if (!g_KdcPlugin->gssAccept(apReq, res))
            if (!g_KdcPlugin->gssAccept(apReq, res)) {
                std::cerr << "[LDAP] bind SASL gssAccept FAILED (apReq=" << apReq.size()
                          << "B) -> INVALID_CREDENTIALS" << std::endl;
                return bindResp(LDAP_INVALID_CREDENTIALS, nullptr);
            }
            std::cerr << "[LDAP] bind SASL gssAccept OK client='" << res.clientPrincipal
                      << "' etype=" << res.sessionEtype << std::endl;
            session.gssKey = res.sessionKey;
            session.gssEtype = res.sessionEtype;
            session.gssClient = res.clientPrincipal;
@@ -761,6 +777,8 @@ std::vector<uint8_t> ActiveDirectory::handleBindRequest(const uint8_t *content,
        session.authenticated = true;
        session.bindDN = session.gssClient;
        session.gssState = LdapSession::GssNone;
        std::cerr << "[LDAP] bind SASL COMPLETE client='" << session.gssClient
                  << "' ssf=" << session.ssf << " -> SUCCESS" << std::endl;
        return bindResp(LDAP_SUCCESS, nullptr);
    }

@@ -992,18 +1010,31 @@ LdapEntry ActiveDirectory::renderEntry(const dsdb::Entry &ent,
 * NETLOGON_SAM_LOGON_RESPONSE_EX) so clients can pick this DC.
 *****************************************************************************/
namespace {
    void nlName(std::vector<uint8_t> &o, const std::string &name) {
        size_t i = 0;
        while (i < name.size()) {
            size_t dot = name.find('.', i);
            if (dot == std::string::npos) dot = name.size();
            size_t l = dot - i;
            if (l == 0) break;
            o.push_back(uint8_t(l));
            for (size_t k = i; k < dot; ++k) o.push_back(uint8_t(name[k]));
            i = dot + 1;
        }
        o.push_back(0);
    /* RFC1035 DNS name with compression, as required by MS-ADTS 6.3.5 for the
     * netlogon response. Pointers are relative to the start of the blob (= o[0]).
     * A shared comp map lets DnsDomainName point at DnsForestName, DnsHostName
     * reuse the "example.com" suffix, repeated site names collapse, etc. */
    void nlName(std::vector<uint8_t> &o, const std::string &name,
                std::map<std::string, uint16_t> &comp) {
        std::string rem = name;
        while (!rem.empty()) {
            auto it = comp.find(rem);
            if (it != comp.end()) {
                uint16_t ptr = 0xC000 | it->second;
                o.push_back(uint8_t(ptr >> 8));
                o.push_back(uint8_t(ptr & 0xff));
                return;
            }
            if (o.size() < 0x4000)
                comp[rem] = uint16_t(o.size());
            size_t dot = rem.find('.');
            std::string label = (dot == std::string::npos) ? rem : rem.substr(0, dot);
            if (label.empty()) break;
            o.push_back(uint8_t(label.size()));
            for (char c : label) o.push_back(uint8_t(c));
            rem = (dot == std::string::npos) ? std::string() : rem.substr(dot + 1);
        }
        o.push_back(0);   /* root label terminates an uncompressed (sub)name */
    }
    void nlU16(std::vector<uint8_t> &o, uint16_t v) { o.push_back(v & 0xff); o.push_back(v >> 8); }
    void nlU32(std::vector<uint8_t> &o, uint32_t v) { for (int i = 0; i < 4; ++i) o.push_back((v >> (8 * i)) & 0xff); }
@@ -1019,6 +1050,7 @@ std::vector<uint8_t> ActiveDirectory::buildNetlogonResponse(const Dom &d, uint32
    uint32_t flags = 0x1 | 0x4 | 0x8 | 0x10 | 0x20 | 0x40 | 0x80 | 0x100 | 0x200
                   | 0x20000000u | 0x40000000u | 0x80000000u;  /* PDC/GC/LDAP/DS/KDC/TIMESERV/CLOSEST/WRITABLE/GOODTIME + DNS_{CONTROLLER,DOMAIN,FOREST} */

    std::map<std::string, uint16_t> nc;    /* DNS name compression context (per blob) */
    nlU16(b, 23);          /* Opcode = LOGON_SAM_LOGON_RESPONSE_EX */
    nlU16(b, 0);           /* Sbz */
    nlU32(b, flags);
@@ -1027,15 +1059,29 @@ std::vector<uint8_t> ActiveDirectory::buildNetlogonResponse(const Dom &d, uint32
    if (auto head = d.dir->byDn(d.meta.baseDn))
        std::memcpy(guid, head->objectGuid.value, 16);
    b.insert(b.end(), guid, guid + 16);
    nlName(b, d.meta.dnsName);             /* DnsForestName (= domain, single-domain forest) */
    nlName(b, d.meta.dnsName);             /* DnsDomainName */
    nlName(b, host);                       /* DnsHostName */
    nlName(b, d.meta.netbios);             /* NetbiosDomainName */
    nlName(b, dsdb::upper(comp));          /* NetbiosComputerName */
    nlName(b, user);                       /* UserName (echo / empty) */
    nlName(b, site);                       /* DcSiteName */
    nlName(b, site);                       /* ClientSiteName */
    nlU32(b, ntVersion ? ntVersion : 0x00000005);
    nlName(b, d.meta.dnsName, nc);         /* DnsForestName (= domain, single-domain forest) */
    nlName(b, d.meta.dnsName, nc);         /* DnsDomainName */
    nlName(b, host, nc);                   /* DnsHostName */
    nlName(b, d.meta.netbios, nc);         /* NetbiosDomainName */
    nlName(b, dsdb::upper(comp), nc);      /* NetbiosComputerName */
    nlName(b, user, nc);                   /* UserName (echo / empty) */
    nlName(b, site, nc);                   /* DcSiteName */
    nlName(b, site, nc);                   /* ClientSiteName */
    /* (DcSockAddr would go here iff NETLOGON_NT_VERSION_5EX_WITH_IP (0x08);
     *  Windows domain join uses 0x16, which does not request it.) */
    /* NextClosestSiteName: only if NETLOGON_NT_VERSION_WITH_CLOSEST_SITE (0x10).
     * Single-site deployment -> no next-closest site, encode as empty (NULL). */
    if (ntVersion & 0x10)
        nlName(b, "", nc);                 /* NextClosestSiteName (none) */
    /* Response NtVersion reflects the format we actually sent (5EX), NOT a raw
     * echo of the request. Samba: NETLOGON_NT_VERSION_1 | NETLOGON_NT_VERSION_5EX
     * (+ WITH_IP / WITH_CLOSEST_SITE if those were requested). Never set the
     * non-EX NETLOGON_NT_VERSION_5 (0x02) bit here, or Windows parses the wrong
     * structure and discards the response (DsGetDcName -> ERROR_NO_SUCH_DOMAIN). */
    uint32_t respNtVer = 0x00000001u | 0x00000004u;   /* V1 | 5EX */
    if (ntVersion & 0x08) respNtVer |= 0x08;          /* 5EX_WITH_IP */
    if (ntVersion & 0x10) respNtVer |= 0x10;          /* WITH_CLOSEST_SITE */
    nlU32(b, respNtVer);
    nlU16(b, 0xffff);                      /* LmNtToken */
    nlU16(b, 0xffff);                      /* Lm20Token */
    return b;
@@ -1098,10 +1144,22 @@ std::vector<uint8_t> ActiveDirectory::handleCldap(const uint8_t *data, size_t le
    for (auto &d : _Domains) {
        if (dnsDomain.empty() || dsdb::iequals(d.meta.dnsName, dnsDomain)) { dom = &d; break; }
    }
    if (!dom)
    if (!dom) {
        std::cerr << "[CLDAP] netlogon ping DnsDomain='" << dnsDomain
                  << "' user='" << user << "' -> NO MATCHING DOMAIN" << std::endl;
        return {};
    }
    std::cerr << "[CLDAP] netlogon ping DnsDomain='" << dnsDomain << "' user='" << user
              << "' ntVer=0x" << std::hex << ntVer << std::dec
              << " -> reply for " << dom->meta.dnsName << std::endl;

    auto blob = buildNetlogonResponse(*dom, ntVer, user);
    {
        std::string hex;
        static const char *hc = "0123456789abcdef";
        for (uint8_t by : blob) { hex += hc[by >> 4]; hex += hc[by & 0xf]; }
        std::cerr << "[CLDAP] netlogon blob (" << blob.size() << "B): " << hex << std::endl;
    }

    /* searchResultEntry { objectName "", attributes { { "Netlogon", { blob } } } } */
    LdapEntry e;
@@ -1113,6 +1171,13 @@ std::vector<uint8_t> ActiveDirectory::handleCldap(const uint8_t *data, size_t le
    auto entry = buildSearchResultEntry(msgId, e);
    auto done = buildSearchResultDone(msgId, LDAP_SUCCESS);
    entry.insert(entry.end(), done.begin(), done.end());
    {
        std::string hex;
        static const char *hc = "0123456789abcdef";
        for (uint8_t by : entry) { hex += hc[by >> 4]; hex += hc[by & 0xf]; }
        std::cerr << "[CLDAP] full LDAP response msgId=" << msgId << " (" << entry.size()
                  << "B): " << hex << std::endl;
    }
    return entry;
}

+73 −16
Changes for plugin/dns/dns.cpp: 73 added lines, 16 removed lines.
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@
#include <cstring>
#include <ctime>
#include <iostream>
#include <set>

#include <sys/socket.h>
#include <netinet/in.h>
@@ -153,19 +154,9 @@ void DnsServer::buildZone() {
    }
}

std::vector<uint8_t> DnsServer::answerSection(const std::string &qname, uint16_t qtype,
                                              int &answerCount, bool &nameExists) {
    std::vector<uint8_t> out;
    std::string key = lower(qname);
    auto range = _Zone.equal_range(key);
    nameExists = range.first != range.second;
    for (auto it = range.first; it != range.second; ++it) {
        const Record &r = it->second;
        if (qtype != RR_ANY && qtype != r.type && !(qtype == RR_A && r.type == RR_A))
            continue;
        if (qtype != RR_ANY && r.type != qtype)
            continue;
        putName(out, qname);
void DnsServer::emitRecord(std::vector<uint8_t> &out, const std::string &owner,
                           const Record &r, int &count) {
    putName(out, owner);
    put16(out, r.type);
    put16(out, 1);           /* class IN */
    put32(out, 600);         /* TTL */
@@ -191,7 +182,7 @@ std::vector<uint8_t> DnsServer::answerSection(const std::string &qname, uint16_t
        break;
    case RR_SOA:
        putName(rdata, r.target);                        /* mname */
            putName(rdata, "hostmaster." + key);             /* rname */
        putName(rdata, "hostmaster." + lower(owner));    /* rname */
        put32(rdata, uint32_t(time(nullptr)));           /* serial */
        put32(rdata, 900); put32(rdata, 600);            /* refresh, retry */
        put32(rdata, 86400); put32(rdata, 3600);         /* expire, minimum */
@@ -199,7 +190,44 @@ std::vector<uint8_t> DnsServer::answerSection(const std::string &qname, uint16_t
    }
    put16(out, uint16_t(rdata.size()));
    out.insert(out.end(), rdata.begin(), rdata.end());
        ++answerCount;
    ++count;
}

std::vector<uint8_t> DnsServer::answerSection(const std::string &qname, uint16_t qtype,
                                              int &answerCount, bool &nameExists) {
    std::vector<uint8_t> out;
    std::string key = lower(qname);
    auto range = _Zone.equal_range(key);
    nameExists = range.first != range.second;
    for (auto it = range.first; it != range.second; ++it) {
        const Record &r = it->second;
        if (qtype != RR_ANY && qtype != r.type && !(qtype == RR_A && r.type == RR_A))
            continue;
        if (qtype != RR_ANY && r.type != qtype)
            continue;
        emitRecord(out, qname, r, answerCount);
    }
    return out;
}

/* Glue: A records for the targets named by the answered SRV/NS records, so a
 * DC-locator client gets the DC's address without a second round trip. */
std::vector<uint8_t> DnsServer::additionalSection(const std::string &qname, uint16_t qtype,
                                                  int &addCount) {
    std::vector<uint8_t> out;
    if (qtype != RR_SRV && qtype != RR_NS && qtype != RR_ANY)
        return out;
    std::string key = lower(qname);
    auto range = _Zone.equal_range(key);
    std::set<std::string> targets;
    for (auto it = range.first; it != range.second; ++it)
        if (it->second.type == RR_SRV || it->second.type == RR_NS)
            targets.insert(lower(it->second.target));
    for (const auto &t : targets) {
        auto ar = _Zone.equal_range(t);
        for (auto it = ar.first; it != ar.second; ++it)
            if (it->second.type == RR_A)
                emitRecord(out, t, it->second, addCount);
    }
    return out;
}
@@ -255,7 +283,16 @@ std::vector<uint8_t> DnsServer::handleQuery(const uint8_t *data, size_t len) {
    if (lq == lower(_DcHost))
        authoritative = true;

    auto qtName = [](uint16_t t) -> const char * {
        switch (t) { case 1: return "A"; case 2: return "NS"; case 6: return "SOA";
            case 12: return "PTR"; case 15: return "MX"; case 16: return "TXT";
            case 28: return "AAAA"; case 33: return "SRV"; case 255: return "ANY";
            default: return "?"; }
    };

    if (qclass != 1 || !authoritative) {
        std::cerr << "[DNS] q='" << qname << "' type=" << qtype << "(" << qtName(qtype)
                  << ") -> REFUSED (not authoritative)" << std::endl;
        header(0, 5 /* REFUSED */, false);
        resp.insert(resp.end(), data + 12, data + off);   /* echo question */
        return resp;
@@ -264,10 +301,30 @@ std::vector<uint8_t> DnsServer::handleQuery(const uint8_t *data, size_t len) {
    int ancount = 0;
    bool nameExists = false;
    auto ans = answerSection(qname, qtype, ancount, nameExists);
    int arcount = 0;
    auto addl = (ancount > 0) ? additionalSection(qname, qtype, arcount)
                              : std::vector<uint8_t>();
    uint16_t rcode = (ancount == 0 && !nameExists) ? 3 /* NXDOMAIN */ : 0;
    header(uint16_t(ancount), rcode, true);
    std::cerr << "[DNS] q='" << qname << "' type=" << qtype << "(" << qtName(qtype)
              << ") -> " << (rcode == 3 ? "NXDOMAIN" : ancount == 0 ? "NODATA" : "OK")
              << " ancount=" << ancount << " arcount=" << arcount << std::endl;
    /* header with arcount */
    put16(resp, id);
    {
        uint16_t f = 0x8000;
        f |= (opcode & 0xf) << 11;
        f |= 0x0400;                 /* AA */
        f |= 0x0080;                 /* RA */
        f |= (rcode & 0xf);
        put16(resp, f);
    }
    put16(resp, qd);
    put16(resp, uint16_t(ancount));
    put16(resp, 0);                  /* nscount */
    put16(resp, uint16_t(arcount));  /* arcount */
    resp.insert(resp.end(), data + 12, data + off);         /* question */
    resp.insert(resp.end(), ans.begin(), ans.end());
    resp.insert(resp.end(), addl.begin(), addl.end());
    return resp;
}

+4 −0
Changes for plugin/dns/dns.h: 4 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -71,6 +71,10 @@ namespace fenrirds {
        std::vector<uint8_t> handleQuery(const uint8_t *data, size_t len);
        std::vector<uint8_t> answerSection(const std::string &qname, uint16_t qtype,
                                           int &answerCount, bool &nameExists);
        std::vector<uint8_t> additionalSection(const std::string &qname, uint16_t qtype,
                                               int &addCount);
        void emitRecord(std::vector<uint8_t> &out, const std::string &owner,
                        const Record &r, int &count);

        AuthBackend &_AdminBackend;
        std::string  _BindAddr;
+12 −0
Changes for plugin/krb5/krb5.cpp: 12 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -2871,6 +2871,13 @@ std::vector<uint8_t> Krb5KDC::handleStandardAsReq(const uint8_t *data, size_t le
                                   req.cname.realm, "krbtgt");
    }

    {
        std::string cn; for (auto &p : req.cname.name) { if (!cn.empty()) cn += "/"; cn += p; }
        std::string sn; for (auto &p : req.sname.name) { if (!sn.empty()) sn += "/"; sn += p; }
        std::cerr << "[KDC] AS-REQ cname='" << cn << "@" << req.cname.realm
                  << "' sname='" << sn << "' npadata=" << req.padata.size() << std::endl;
    }

    /* Resolve the realm from the request */
    RealmInfo *ri = resolveRealm(req.cname.realm);
    if (!ri || !ri->backend) {
@@ -3147,6 +3154,11 @@ std::vector<uint8_t> Krb5KDC::handleStandardTgsReq(const uint8_t *data, size_t l
                                   req.sname.realm, "krbtgt");
    }

    {
        std::string sn; for (auto &p : req.sname.name) { if (!sn.empty()) sn += "/"; sn += p; }
        std::cerr << "[KDC] TGS-REQ sname='" << sn << "@" << req.sname.realm << "'" << std::endl;
    }

    /*
     * Extract the TGT from PA-TGS-REQ (PA-DATA type 1, which contains an AP-REQ).
     * The AP-REQ contains the TGT ticket + encrypted authenticator.
+61 −5
Changes for plugin/smb/smb.cpp: 61 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -74,16 +74,23 @@ void RpcPipe::handlePdu(const std::vector<uint8_t> &pdu) {

std::vector<uint8_t> RpcPipe::dispatch(const rpc::RequestPdu &req) {
    rpc::DispatchResult res;
    if (rpc::isLsarpc(_boundAbstract.uuid))
    const char *iface = "?";
    if (rpc::isLsarpc(_boundAbstract.uuid)) {
        iface = "lsarpc";
        res = rpc::lsarpcDispatch(req.opnum, req.stub, _sessionKey);
    else if (rpc::isSamr(_boundAbstract.uuid))
    } else if (rpc::isSamr(_boundAbstract.uuid)) {
        iface = "samr";
        res = rpc::samrDispatch(req.opnum, req.stub, _sessionKey);
    else if (rpc::isNetlogon(_boundAbstract.uuid))
    } else if (rpc::isNetlogon(_boundAbstract.uuid)) {
        iface = "netlogon";
        res = rpc::netlogonDispatch(req.opnum, req.stub, _sessionKey);
    else {
    } else {
        res.fault = true;
        res.faultCode = 0x1C010003;   /* nca_unk_if */
    }
    std::cerr << "[RPC] " << iface << " opnum=" << req.opnum << " stub=" << req.stub.size()
              << "B -> " << (res.fault ? "FAULT 0x" : "OK status=")
              << std::hex << (res.fault ? res.faultCode : res.status) << std::dec << std::endl;
    if (res.fault)
        return rpc::buildFault(req.callId, req.contextId, res.faultCode);
    return rpc::buildResponse(req.callId, req.contextId, res.stub);
@@ -134,6 +141,7 @@ void SmbServer::CreateConnection(std::shared_ptr<netplus::con> &res) {
    res = std::make_shared<netplus::con>(this);
}
void SmbServer::ConnectEvent(netplus::con &c, const int, ULONG_PTR) {
    std::cerr << "[SMB] TCP connect" << std::endl;
    std::lock_guard<std::mutex> l(_Lock); _Conns[&c] = Conn();
}
void SmbServer::DisconnectEvent(netplus::con &c, const int, ULONG_PTR) {
@@ -148,6 +156,7 @@ void SmbServer::RequestEvent(netplus::con &curcon, const int, ULONG_PTR) {
    { std::lock_guard<std::mutex> l(_Lock); auto it = _Conns.find(&curcon); if (it != _Conns.end()) c = &it->second; }
    if (!c) return;

    std::cerr << "[SMB] recv " << curcon.RecvData.size() << "B" << std::endl;
    std::vector<uint8_t> rb(reinterpret_cast<const uint8_t*>(curcon.RecvData.data()),
                            reinterpret_cast<const uint8_t*>(curcon.RecvData.data()) + curcon.RecvData.size());
    size_t consumed = 0;
@@ -176,6 +185,28 @@ std::vector<uint8_t> SmbServer::errorResponse(const smb::Smb2Header &req, uint32

std::vector<uint8_t> SmbServer::handleMessage(Conn &c, const std::vector<uint8_t> &msg, bool &close) {
    namespace gss = fenrirds::crypto::gss;

    /* Windows opens a port-445 session with a legacy SMB1 multi-protocol
     * negotiate (0xFF 'SMB', SMB_COM_NEGOTIATE 0x72) advertising "SMB 2.???".
     * Reply with an SMB2 NEGOTIATE response carrying the wildcard dialect
     * 0x02FF; the client then restarts with a real SMB2 NEGOTIATE
     * (MS-SMB2 3.3.5.3.1). Without this the connection stalls and Windows
     * reports ERROR_NETNAME_DELETED. */
    if (msg.size() >= 5 && msg[0] == 0xFF && msg[1] == 'S' && msg[2] == 'M'
        && msg[3] == 'B' && msg[4] == 0x72) {
        std::cerr << "[SMB] SMB1 multi-protocol negotiate -> SMB2 wildcard 0x02FF" << std::endl;
        smb::Smb2Header h;
        h.command = smb::SMB2_NEGOTIATE;
        h.messageId = 0;
        h.status = smb::STATUS_SUCCESS;
        h.creditReqResp = 1;
        auto resp = smb::buildHeader(h);
        auto spnego = smb::spnegoMechList();
        auto neg = smb::buildNegotiateResponse(0x02FF, _ServerGuid, spnego);
        resp.insert(resp.end(), neg.begin(), neg.end());
        return resp;
    }

    smb::Smb2Header req;
    if (!smb::parseHeader(msg.data(), msg.size(), req))
        return {};
@@ -200,11 +231,25 @@ std::vector<uint8_t> SmbServer::handleMessage(Conn &c, const std::vector<uint8_t
            return errorResponse(req, smb::STATUS_ACCESS_DENIED);
    }

    {
        static const char *cn[] = {"NEGOTIATE","SESSION_SETUP","LOGOFF","TREE_CONNECT",
            "TREE_DISCONNECT","CREATE","CLOSE","FLUSH","READ","WRITE","LOCK","IOCTL",
            "CANCEL","ECHO","QUERY_DIRECTORY","CHANGE_NOTIFY","QUERY_INFO","SET_INFO","OPLOCK_BREAK"};
        const char *nm = (req.command < 19) ? cn[req.command] : "?";
        std::cerr << "[SMB] cmd=" << req.command << "(" << nm << ") authed=" << c.authenticated
                  << " mid=" << req.messageId << std::endl;
    }

    switch (req.command) {
    case smb::SMB2_NEGOTIATE: {
        smb::NegotiateRequest nr;
        if (!smb::parseNegotiate(body, blen, nr))
            return errorResponse(req, smb::STATUS_INVALID_PARAMETER);
        {
            std::string hex; static const char *hc="0123456789abcdef";
            for (uint8_t by : msg) { hex+=hc[by>>4]; hex+=hc[by&0xf]; }
            std::cerr << "[SMB] NEGOTIATE req (" << msg.size() << "B): " << hex << std::endl;
        }
        bool o311=false,o30=false,o302=false,o21=false;
        for (uint16_t d : nr.dialects) {
            if (d==smb::SMB_3_1_1) o311=true; else if (d==smb::SMB_3_0) o30=true;
@@ -236,6 +281,12 @@ std::vector<uint8_t> SmbServer::handleMessage(Conn &c, const std::vector<uint8_t
            auto neg = smb::buildNegotiateResponse(c.dialect, _ServerGuid, spnego);
            resp.insert(resp.end(), neg.begin(), neg.end());
        }
        {
            std::string hex; static const char *hc="0123456789abcdef";
            for (uint8_t by : resp) { hex+=hc[by>>4]; hex+=hc[by&0xf]; }
            std::cerr << "[SMB] NEGOTIATE resp dialect=0x" << std::hex << c.dialect << std::dec
                      << " (" << resp.size() << "B): " << hex << std::endl;
        }
        return resp;
    }
    case smb::SMB2_SESSION_SETUP: {
@@ -263,8 +314,13 @@ std::vector<uint8_t> SmbServer::handleMessage(Conn &c, const std::vector<uint8_t
        std::vector<uint8_t> inner = apReq;
        { uint16_t id=0; std::vector<uint8_t> x; if (gss::parseInitialToken(apReq, id, x)) inner.swap(x); }
        GssAcceptResult res;
        if (!g_KdcPlugin->gssAccept(inner, res))
        if (!g_KdcPlugin->gssAccept(inner, res)) {
            std::cerr << "[SMB] SESSION_SETUP gssAccept FAILED (apReq=" << inner.size()
                      << "B spnego=" << spnego << ") -> LOGON_FAILURE" << std::endl;
            return errorResponse(req, smb::STATUS_LOGON_FAILURE);
        }
        std::cerr << "[SMB] SESSION_SETUP gssAccept OK client='" << res.clientPrincipal
                  << "'" << std::endl;

        c.authenticated = true;
        c.clientPrincipal = res.clientPrincipal;