Commit 81be1cad authored by jan.koester's avatar jan.koester
Browse files

adconnectd: NTLM pass-through command for sharedd/ksmbd

New root-only, uncached NTLM socket command: request is base64 of six
uint16-length-prefixed fields (domain, account, workstation, server
challenge, NT response, LM response); adconnectd finds the joined domain,
derives the computer account from its config, and calls samLogonNetwork()
to have the DC check the response, returning 'N <status> <b64 key>'. Adds
the netlogon_port option (0 = ask the endpoint mapper) and wires Cmd::Ntlm
into the protocol and daemon dispatch.

The NETLOGON client is proven end to end against real Samba AD; the
in-sandbox adconnectd<->client round trip could not complete here due to
user-namespace/background-process flakiness (the daemon binds and logs
'serving' but the harness tears the namespace down), so the daemon path
still wants a run on a real FenrirOS box.

Co-Authored-By: default avatarClaude Opus 4.8 <noreply@anthropic.com>
parent a9c81414
Loading
Loading
Loading
Loading
+1 −0
Changes for src/config.cpp: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -186,6 +186,7 @@ void readDomain(const Scope &s, Config &cfg, bool multi, bool &idBaseSet) {
    cfg.servers = readList(s, "servers");
    if (cfg.servers.empty()) cfg.servers.push_back(cfg.domain);
    readInt(s, "port", cfg.port, 1, 65535);
    readInt(s, "netlogon_port", cfg.netlogonPort, 1, 65535);

    std::string tls = "ldaps";
    readString(s, "tls", tls);
+2 −0
Changes for src/config.h: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -92,6 +92,8 @@ struct Config {
    int timeout = 10;
    int pageSize = 500;

    int netlogonPort = 0;               /* NETLOGON RPC port; 0 = ask the endpoint mapper */

    int ldapPort() const { return port ? port : tls == ldap::Tls::Ldaps ? 636 : 389; }
};

+65 −2
Changes for src/daemon.cpp: 65 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -64,6 +64,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "directory.h"
#include "home.h"
#include "join.h"
#include "netlogon.h"
#include "protocol.h"

using namespace adconnect;
@@ -128,6 +129,64 @@ std::string acceptKrb5(Server &s, const std::string &arg) {
    }
}

/* NTLM: a domain user's NTLM response, checked by its domain controller over
 * NETLOGON (for sharedd/ksmbd SMB logins that don't use Kerberos). The request
 * arg is tab-separated: domain, account, workstation, base64(server challenge),
 * base64(NT response), base64(LM response). */
std::string acceptNtlm(Server &s, const std::string &arg) {
    /* arg is base64 of six uint16-length-prefixed fields (little-endian):
     * domain, account, workstation, server challenge, NT response, LM response */
    std::string blob;
    if (!base64Decode(arg, blob)) return formatError("bad NTLM request");
    std::string f[6];
    size_t p = 0;
    for (int i = 0; i < 6; i++) {
        if (p + 2 > blob.size()) return formatError("bad NTLM request");
        size_t len = (unsigned char)blob[p] | ((unsigned char)blob[p + 1] << 8);
        p += 2;
        if (p + len > blob.size()) return formatError("bad NTLM request");
        f[i] = blob.substr(p, len);
        p += len;
    }
    NtlmPassthrough in;
    in.domain = f[0]; in.account = f[1]; in.workstation = f[2];
    in.serverChallenge = f[3]; in.ntResponse = f[4]; in.lmResponse = f[5];
    if (in.serverChallenge.size() != 8) return formatError("bad NTLM request");

    auto netbios = [](const std::string &dom) {
        std::string nb = dom.substr(0, dom.find('.'));
        for (char &c : nb) c = (char)toupper((unsigned char)c);
        return nb;
    };
    Domain *d = nullptr;
    std::string want = in.domain;
    for (char &c : want) c = (char)toupper((unsigned char)c);
    for (auto &dp : s.domains)
        if (netbios(dp->cfg.domain) == want) { d = dp.get(); break; }
    if (!d) return formatError("no domain " + in.domain);
    if (d->cfg.auth != Auth::Kerberos || d->cfg.krbPrincipal.empty())
        return formatError(d->cfg.domain + " is not joined");

    NetlogonConfig nc;
    nc.dcHost = d->cfg.servers.empty() ? d->cfg.domain : d->cfg.servers[0];
    nc.rpcPort = d->cfg.netlogonPort;
    if (nc.rpcPort == 0) { try { nc.rpcPort = resolveNetlogonPort(nc.dcHost, 135, 10000); } catch (...) {} }
    if (nc.rpcPort == 0) return formatError("no NETLOGON port (set netlogon_port)");
    nc.machineAccount = d->cfg.krbPrincipal;
    /* the computer name is the machine account without the trailing '$' */
    nc.computer = d->cfg.krbPrincipal;
    if (!nc.computer.empty() && nc.computer.back() == '$') nc.computer.pop_back();
    nc.machinePassword = d->cfg.bindPassword;
    nc.domainNetbios = netbios(d->cfg.domain);
    try {
        NtlmResult r = samLogonNetwork(nc, in);
        return "N\t" + std::to_string((unsigned long)r.status) + "\t" + base64Encode(r.sessionKey) + "\n" + End;
    } catch (const std::exception &e) {
        std::cerr << "adconnectd: NTLM: " << e.what() << std::endl;
        return formatError(e.what());
    }
}

long long nowSec() {
    return std::chrono::duration_cast<std::chrono::seconds>(Clock::now().time_since_epoch()).count();
}
@@ -204,6 +263,9 @@ bool resolve(Server &s, Cmd cmd, const std::string &arg, const std::string &line
    case Cmd::Krb5:
        out = acceptKrb5(s, arg);
        return true;
    case Cmd::Ntlm:
        out = acceptNtlm(s, arg);
        return true;
    case Cmd::PwEnt:
    case Cmd::GrEnt:
        out.clear();
@@ -222,8 +284,9 @@ std::string answer(Server &s, const std::string &line, uid_t peer) {
    std::string arg;
    if (!parseRequest(line, cmd, arg)) return formatError("bad request");
    std::string out, error;
    if (cmd == Cmd::Krb5) {
        /* a session key for whoever brings a ticket: root only, never cached */
    if (cmd == Cmd::Krb5 || cmd == Cmd::Ntlm) {
        /* a session key for whoever brings a ticket or NTLM response: root
         * only (SO_PEERCRED), never cached */
        if (peer != 0) return formatError("permission denied");
        resolve(s, cmd, arg, line, out, error);
        return out;
+1 −1
Changes for src/protocol.cpp: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -43,7 +43,7 @@ struct Name {
const Name Names[] = {
    { Cmd::PwNam, "PWNAM", true }, { Cmd::PwUid, "PWUID", true }, { Cmd::PwEnt, "PWENT", false },
    { Cmd::GrNam, "GRNAM", true }, { Cmd::GrGid, "GRGID", true }, { Cmd::GrEnt, "GRENT", false },
    { Cmd::Groups, "GROUPS", true }, { Cmd::Krb5, "KRB5", true },
    { Cmd::Groups, "GROUPS", true }, { Cmd::Krb5, "KRB5", true }, { Cmd::Ntlm, "NTLM", true },
};

bool printable(const std::string &s) {
+1 −1
Changes for src/protocol.h: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -54,7 +54,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

namespace adconnect {

enum class Cmd { PwNam, PwUid, PwEnt, GrNam, GrGid, GrEnt, Groups, Krb5 };
enum class Cmd { PwNam, PwUid, PwEnt, GrNam, GrGid, GrEnt, Groups, Krb5, Ntlm };

struct User {
    std::string name, uid, gid, gecos, dir, shell;