---
# System prepended metadata

title: LDAP server - FAQ
tags: [LDAP, LDAP server]

---

LDAP server - FAQ
===
###### tags: `LDAP`
###### tags: `LDAP`, `LDAP server`

<br>

[TOC]

<br>


## 討論 - 連線問題

### [無法連線] `ldap_sasl_bind(SIMPLE): Can't contact LDAP server (-1)`
- ### Quesstion
    ```
    $ ldapsearch -x -b "dc=asus,dc=com" "(uid=tj_tsai)"
    ldap_sasl_bind(SIMPLE): Can't contact LDAP server (-1)
    ```
- ### gpt-5 (2025/09/19)
    下面這個錯誤訊息代表「**客戶端連不到任何 LDAP 伺服器**」，不是帳密或 Base DN 錯誤：

    ```
    ldap_sasl_bind(SIMPLE): Can't contact LDAP server (-1)
    ```

    最常見原因：沒有指定伺服器 URI（預設會連 localhost:389）、伺服器沒啟動、連錯主機/埠、或被防火牆/TLS 擋下來。照這份「**最短查核清單**」一步做，通常就能定位問題。

    ---
    
    > ### 兩種情況：
    > (A) 你在 **同一台裝有 slapd 的主機** 上（apt 安裝）
    > (B) 你在 **其他主機 / 容器 / Pod** 上查（最常見）

    ---

    ### (A) 你在 **同一台裝有 slapd 的主機** 上（apt 安裝）

    1. 先確認服務有跑

        ```bash
        sudo systemctl status slapd --no-pager
        sudo ss -ltnp | grep -E ':389|:636'
        ```

    2. 直接用 Unix socket（ldapi）查設定，驗證 **Naming Context（suffix）** 與 **管理者 DN**

        ```bash
        # 以 root 權限用本機 socket 連線，不走網路
        sudo ldapsearch -H ldapi:/// -Y EXTERNAL -b cn=config -LLL olcSuffix olcRootDN
        ```

        看輸出中的：

        * `olcSuffix`: 例如 `dc=google,dc=com`
        * `olcRootDN`: 例如 `cn=admin,dc=google,dc=com`（這就是你要用的 -D）

    3. 用 ldapi 直接找資料（先不管網路問題）

        ```bash
        sudo ldapsearch -H ldapi:/// -Y EXTERNAL -b "" -s base -LLL namingContexts
        sudo ldapsearch -H ldapi:/// -x -b 'dc=google,dc=com' '(uid=tj_tsai)' -LLL dn
        ```
        - 這兩條成功就表示 **資料已存在 + 伺服器正常運作**。

    ---

    ### (B) 你在 **其他主機 / 容器 / Pod** 上查（最常見）

    > 關鍵：**一定要加 `-H ldap://<host>:<port>`** 指向正確的服務位置與埠。

    1. 先確認你能連到 LDAP 埠

    ```bash
    # 換成你的主機名或 Service 名稱與埠（常見 389 / 1389；LDAPS 常見 636 / 1636）
    nc -vz <ldap-host> 389
    ```

    2. 用匿名 bind 讀 namingContexts（找出正確 Base DN）

    ```bash
    ldapsearch -H ldap://<ldap-host>:389 -x -b "" -s base -LLL namingContexts
    ```

    看到像 `namingContexts: dc=google,dc=com` 就代表伺服器可連，且 Base DN 是那個字串。

    3. 搜尋你的使用者
       （**建議用整體樹的 Base DN**，讓伺服器自己往下找，不要一開始就填整個使用者 DN 當 base）

    ```bash
    ldapsearch -H ldap://<ldap-host>:389 -x \
      -b 'dc=google,dc=com' '(uid=tj_tsai)' -LLL dn uidNumber gidNumber homeDirectory loginShell
    ```

    4. 需要驗證帳密時（以管理者 DN 連線）：

    ```bash
    ldapwhoami -H ldap://<ldap-host>:389 -x -D 'cn=admin,dc=google,dc=com' -W
    # 或搜尋：
    ldapsearch -H ldap://<ldap-host>:389 -x \
      -D 'cn=admin,dc=google,dc=com' -W \
      -b 'dc=google,dc=com' '(uid=tj_tsai)' -LLL dn
    ```

    > 你的 LDIF 把條目放在 `dc=www,dc=google,dc=com` 底下。這 **仍屬於** `dc=google,dc=com` 這棵樹之內（`dc=www` 只是中間節點），**只要伺服器的 suffix 是 `dc=google,dc=com`，就用它當 -b**，搜尋會自己往下遞迴找到 `uid=tj_tsai`。

    ---

    ### C)（選用）TLS／StartTLS 檢查

    * LDAPS（636）：`-H ldaps://<host>:636 -x ...`
    * StartTLS（389）：`-H ldap://<host>:389 -ZZ -x ...`
      若遇到憑證錯誤，可暫時用 `-d 1` 觀察，或把 CA 加到 `/etc/ldap/ldap.conf` 的 `TLS_CACERT`。

    ---

    ### D) 常見踩雷速解

    * **沒加 `-H`** → 預設連 localhost:389；在容器裡通常沒有 slapd，所以報 `Can't contact LDAP server (-1)`。
    * **連錯埠** → 有些映像（或 K8s Service）用 **1389/1636**。請以實際 Service/Port 為準。
    * **Base DN 用錯** → 先用 `-b "" -s base namingContexts` 查真正的 suffix。
    * **admin DN 用錯** → 以 `ldapi:///` 查 `olcRootDN` 最準。
    * **防火牆** → 伺服器要開放 389/636（或 1389/1636）；K8s 要確認 Service/NetworkPolicy。
    * **`I have no name!@...`** → 只是容器內沒有對應的 /etc/passwd 項，與 LDAP 連線無關。

    ---

    ### E) 你這份資料的「最小可驗證」指令範例

    > 假設伺服器 suffix 是 `dc=google,dc=com`，且用 389：

    1. 匯入

    ```bash
    ldapadd -H ldap://<ldap-host>:389 -x \
      -D 'cn=admin,dc=google,dc=com' -W -f tj_tsai.ldif
    ```

    2. 搜尋（匿名或管理者都可）

    ```bash
    ldapsearch -H ldap://<ldap-host>:389 -x \
      -b 'dc=google,dc=com' '(uid=tj_tsai)' -LLL dn uidNumber gidNumber homeDirectory loginShell
    ```

    如果以上第 2 步還是回 `Can't contact LDAP server (-1)`，那就回到 **A-1/B-1** 檢查服務是否在正確的主機與埠上對外監聽，以及你 `-H` 的主機與埠是否填對。

<br>

---

### [StartTLS 憑證驗證失敗] certificate verify failed (self-signed certificate)
- ### Question
    ```
    $ ldapsearch -x -H ldap://openldap.openldap:389 \
        -D "cn=admin,dc=www,dc=google,dc=com" \
        -w "YourSecurePassword" \
        -ZZ
    ldap_start_tls: Connect error (-11)
        additional info: error:0A000086:SSL routines::certificate verify failed (self-signed certificate)
    ```
- ### gpt-5 (2025/09/19)
    你遇到的是「StartTLS 憑證驗證失敗」（自簽憑證未被信任）。連線有通了，但**客戶端不信任伺服器證書**，所以 TLS 升級被拒。

    ### 快速判斷

    先確認是不是「只差信任」這件事：

    ```bash
    # 臨時放寬驗證（測試用，非正式環境）
    ldapsearch -x -H ldap://openldap.openldap:389 -ZZ \
      -D "cn=admin,dc=www,dc=google,dc=com" -w "YourSecurePassword" \
      -o TLS_REQCERT=allow
    ```

    * 若這樣可以成功，表示「連線沒問題，純粹憑證信任問題」。

    ### 正確修法（建議）

    目標是讓**客戶端信任簽發你 LDAP 伺服器證書的 CA**，並且**主機名稱要跟證書 SAN 相符**。

    #### 1) 產生（或取得）CA 與伺服器證書（含 SAN）

    > SAN 一定要包含你實際用的主機名，例如：
    > `openldap.openldap`、`openldap.openldap.svc`、`openldap.openldap.svc.cluster.local`

    範例（有 SAN 的簽發流程）：

    ```bash
    # 產 CA
    openssl genrsa -out ca.key 4096
    openssl req -x509 -new -sha256 -days 3650 \
      -key ca.key -subj "/CN=My LDAP CA" -out ca.crt

    # 設定 server 證書的 SAN
    cat > openssl-ldap.cnf <<'EOF'
    [req]
    distinguished_name = dn
    req_extensions     = req_ext
    prompt             = no

    [dn]
    CN = openldap.openldap.svc.cluster.local

    [req_ext]
    subjectAltName = @alt_names

    [alt_names]
    DNS.1 = openldap.openldap
    DNS.2 = openldap.openldap.svc
    DNS.3 = openldap.openldap.svc.cluster.local
    EOF

    # 產 server key + CSR
    openssl genrsa -out tls.key 2048
    openssl req -new -key tls.key -out server.csr -config openssl-ldap.cnf

    # 用 CA 簽發 server cert（帶上 req_ext 才有 SAN）
    openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
      -out tls.crt -days 825 -sha256 -extensions req_ext -extfile openssl-ldap.cnf
    ```

    #### 2) 在 OpenLDAP 伺服器掛上證書

    （K8s/Bitnami 常見做法）

    ```bash
    # 放進 Secret（server 證書+私鑰）
    kubectl -n openldap create secret tls openldap-tls --cert=tls.crt --key=tls.key

    # 放 CA（給伺服器與 sidecar/其他元件參考）
    kubectl -n openldap create configmap openldap-ca --from-file=ca.crt
    ```

    接著在你的 OpenLDAP 部署/Helm values 指定：

    * `LDAP_ENABLE_TLS=yes`
    * `LDAP_TLS_CERT_FILE` 指到 `tls.crt`
    * `LDAP_TLS_KEY_FILE` 指到 `tls.key`
    * `LDAP_TLS_CA_FILE` 指到 `ca.crt`
    * 或對應 Bitnami chart 的 `tls.enabled: true`、`tls.certificatesSecret: openldap-tls`、`tls.CASecret` 等欄位（依你用的 chart/映像為準）。

    > 若不是 Helm，確保 `cn=config` 有 `olcTLSCertificateFile`、`olcTLSCertificateKeyFile`、`olcTLSCACertificateFile` 指到正確路徑，並重啟 slapd。

    #### 3) 在「客戶端」安裝 CA（讓它信任）

    在你跑 `ldapsearch` 的那台（看起來是個 Pod）：

    * 方式 A：把 `ca.crt` 放到檔案系統，臨時指定：

      ```bash
      export LDAPTLS_CACERT=/etc/ssl/certs/ldap-ca.crt
      # 或在指令裡帶
      ldapsearch ... -ZZ -o TLS_CACERT=/etc/ssl/certs/ldap-ca.crt
      ```
    * 方式 B：寫到 `/etc/ldap/ldap.conf`（持久化方式）

      ```
      TLS_CACERT /etc/ssl/certs/ldap-ca.crt
      TLS_REQCERT demand
      ```
    * 方式 C（Debian/Ubuntu 類）：放到 `/usr/local/share/ca-certificates/ldap-ca.crt`，然後

      ```bash
      update-ca-certificates
      ```

    > 重點：你 `-H` 用的主機名要**與證書 SAN 其中一個 DNS 完全一致**。
    > 例如你現在用 `openldap.openldap`，那就確保它在 SAN 裡有列出；否則請改成 `openldap.openldap.svc.cluster.local` 等與 SAN 相符的名稱。

    #### 4) 驗證

    ```bash
    ldapsearch -x -H ldap://openldap.openldap:389 -ZZ \
      -D "cn=admin,dc=www,dc=google,dc=com" -w "YourSecurePassword" \
      -b "dc=www,dc=google,dc=com" "(objectClass=*)"
    ```

    或（同樣需要信任 CA）：

    ```bash
    ldapsearch -x -H ldaps://openldap.openldap:636 \
      -D "cn=admin,dc=www,dc=google,dc=com" -w "YourSecurePassword" \
      -b "dc=www,dc=google,dc=com" "(objectClass=*)"
    ```

    ### 臨時繞過（只用於測試）

    若你只是想先確認功能，**暫時**關閉嚴格驗證：

    ```bash
    ldapsearch -x -H ldap://openldap.openldap:389 -ZZ \
      -D "cn=admin,dc=www,dc=google,dc=com" -w "YourSecurePassword" \
      -o TLS_REQCERT=never
    ```

    > 正式環境請恢復 `demand`，並完成 CA 信任與 SAN 配置。

    ### 小提醒

    * 錯誤訊息是 `certificate verify failed (self-signed certificate)`：不是「沒開 TLS」，而是**缺信任或主機名不匹配**。
    * `I have no name!@...` 只是容器裡沒有該 UID 的 `/etc/passwd` 條目，與本問題無關。
    * 若仍失敗，可加 `-d 1`（或更高）看 libldap debug：

      ```bash
      ldapsearch -d 1 -x -H ldap://openldap.openldap:389 -ZZ ...
      ```

      常見還會看到「hostname mismatch」或「unable to get local issuer certificate」等線索。

    需要的話，我可以幫你把 OpenLDAP（Bitnami/Helm）和測試 Pod 的 values/掛載範例補到一份最小可行設定。

<br>

---

<br>

## 討論 - OpenLDAP

### OpenLDAP 實作 X.500 模型，X.500 模型則是實作 LDAP 協定，這樣說對嗎？
- ### gpt-5 (2025/09/19)
    不太對。順序要反過來：

    * **LDAP** 是一組「協定」（RFC 4511 等），**沿用／對應 X.500 的資料與命名模型**（DIT、DN、objectClass、attribute…）。
    * **X.500** 是一套「目錄服務標準與資料模型」，**它本身有自己的協定**（DAP、DSP、DISP、DOP），而不是「去實作 LDAP」。
    * **OpenLDAP** 是一個 **LDAPv3 目錄伺服器的實作**（講 LDAP 這個協定），同時**採用 X.500 風格的資料／命名模型**。

    所以正確說法是：
    **OpenLDAP 實作 LDAP 協定，並以 X.500 的資料與命名模型為基礎；不是 X.500 去實作 LDAP。**


<br>

---

<br>

### OpenLDAP 實作 LDAP 協定，LDAP 協定是繼承 X.500 模型，這樣說對嗎？
- ### gpt-5 (2025/09/23)
    簡短回答：**半對，但「繼承」這個說法不精確。**

    * **正確的部份**：LDAP（Lightweight Directory Access Protocol）延續／相容 **X.500 的目錄資訊模型**與命名規則——例如 **DN/RDN、DIT（目錄資訊樹）、objectClass/attribute schema**、以及常見操作（bind/search/add/modify/rename/delete/compare）。
    * **不精確的部份**：LDAP不是在協定意義上「繼承」X.500。X.500 原本使用 **DAP** 並跑在 **OSI 通訊堆疊**；LDAP則是**為了在 TCP/IP 上提供 X.500 風格目錄存取的「輕量」協定**。自 **LDAPv3** 起，它已是可**獨立**的通用目錄協定（有 controls、extended ops、referrals 等），不需要背後有 X.500 系統。

    **OpenLDAP** 是 **LDAPv3** 的實作（含伺服器 `slapd` 與用戶端函式庫）。它採用 X.500 風格的資料模型，但**後端儲存**通常是自家的 **LMDB（back-mdb）** 等資料庫，而**不是** X.500 的 DSA。

    👉 更精準的說法是：
    **「LDAP 採用（相容）X.500 的目錄資訊模型與命名規則，但在協定與網路堆疊上是獨立、輕量的設計；OpenLDAP 則是 LDAPv3 的實作。」**

<br>

---

<br>

### OpenLDAP 後端使用的資料庫名稱叫什麼？
- ### gpt-5 (2025/09/23)
    最標準的答案是：**LMDB（Lightning Memory-Mapped Database）**。
    在 OpenLDAP 裡它的後端模組名稱是 **back-mdb**，設定中會看到 `database mdb` 或 `olcDatabase={N}mdb`。

    （補充：早期曾用 **Berkeley DB**（`back-bdb` / `back-hdb`），現已淘汰或不建議使用。）


<br>

---

<br>

### 使用 Java 程式語言角度定義 OpenLDAP
- ### Question
    站在 Java 程式語言角度，可以說是：
    `class OpenLDAP extends X.500 implements LDAP`
    這樣嗎？
- ### gpt-5 (2025/09/23)
    幾乎，但不精確。用 Java 類比，**OpenLDAP 不是「X.500 的一種」**，而是**遵循 LDAPv3 協定、並採用（對齊）X.500 風格的資料/命名模型**。因此不該 `extends X.500`，比較合理是**實作協定、組合資料模型**：

    ```java
    // 「協定」→ 介面
    interface LdapV3Server { 
        void bind(...);
        void search(...);
        void modify(...);
        // ...
    }

    // 「資料/命名模型」→ 類或元件（組合，而不是繼承）
    final class X500NamingModel {
        // DN, RDN, objectClass, attributeType, matchingRules ...
    }

    // （可選）通用目錄伺服器基底
    abstract class DirectoryServer { 
        // 共通伺服器行為（儲存、索引、ACL、後端等）
    }

    final class OpenLdapServer extends DirectoryServer implements LdapV3Server {
        private final X500NamingModel model;  // has-a（組合）
        // ... 實作 bind/search/modify 等 LDAPv3 行為
    }
    ```

    要點：

    * `implements LDAP` ✅（協定 = 介面）
    * `extends X.500` ❌（X.500 是資料/命名模型與一組標準，不是「OpenLDAP 的父類別」。OpenLDAP **不是** X.500 的一種）
    * 正確心智模型：**OpenLDAP is-a 目錄伺服器**，**implements LDAPv3 協定**，**has-a X.500 風格模型（或符合 RFC 4512 的 LDAP 資料/架構模型）**。

<br>

---

<br>

### LDAPv3 的 Java 介面雛形
- ### Question
    ```
    interface LdapV3Server { 
        void bind(...);
        void search(...);
        void modify(...);
        // ...
    }
    ```
    請補全所有常見概念 API
- ### gpt-5 (2025/09/23)
    下面給你一份「概念完整、可實作」的 Java 介面雛形，把 LDAPv3 常見操作都補齊（bind/unbind、search、add/modify/delete、modifyDN、compare、abandon、extended ops、StartTLS、常用 extended：WhoAmI、PasswordModify、Cancel），並把搜尋參數、控制碼（Controls）等也一併建模。
    （解說用 zh-TW；程式碼內註解用 English，符合你的偏好。）

    ```java
    import java.time.Duration;
    import java.util.*;
    import java.util.concurrent.CompletableFuture;

    // Core interface for an LDAPv3 connection/session.
    public interface LdapV3Server extends AutoCloseable {

        /* =========================
         * Connection / Session
         * ========================= */
        void connect(String host, int port, Duration timeout);
        @Override void close();                          // Unbind/close

        /* StartTLS (RFC 4511 §4.14 via ExtendedOp "1.3.6.1.4.1.1466.20037") */
        ExtendedResult startTLS(TLSOptions options, Control... controls);
        ExtendedResult stopTLS();                        // convenience

        /* =========================
         * Bind / Unbind
         * ========================= */
        BindResult bindSimple(String bindDN, char[] password, Control... controls);
        BindResult bindSasl(String mechanism, byte[] clientCreds, Map<String, ?> props, Control... controls);
        void unbind();                                   // closes the session

        /* =========================
         * Search (RFC 4511 §4.5)
         * ========================= */
        // 1) Callback streaming (best for large result sets)
        SearchDone search(SearchRequest req, SearchEntryHandler onEntry, SearchRefHandler onReference);

        // 2) Iterator-style convenience
        SearchResult searchAll(SearchRequest req);

        // 3) Return first matched entry (convenience)
        Optional<SearchResultEntry> searchOne(SearchRequest req);

        /* =========================
         * CRUD-like ops
         * ========================= */
        // Add (RFC 4511 §4.7)
        LdapResult add(Entry entry, Control... controls);

        // Modify (RFC 4511 §4.6)
        LdapResult modify(DN dn, List<Modification> modifications, Control... controls);

        // Delete (RFC 4511 §4.8)
        LdapResult delete(DN dn, Control... controls);

        // Modify DN / Rename (RFC 4511 §4.9)
        LdapResult modifyDN(DN dn, RDN newRdn, boolean deleteOldRdn, DN newSuperior /* nullable */, Control... controls);

        // Compare (RFC 4511 §4.10)
        CompareResult compare(DN dn, String attribute, byte[] assertionValue, Control... controls);

        /* =========================
         * Operation Management
         * ========================= */
        // Abandon (RFC 4511 §4.11) – cancel in-flight request by its messageID
        void abandon(int messageId);

        // Extended Operation (RFC 4511 §4.12)
        ExtendedResult extended(ExtendedRequest request, Control... controls);

        /* Common Extended Ops */
        // Who Am I? (RFC 4532)
        ExtendedResult whoAmI(Control... controls);

        // Password Modify (RFC 3062)
        ExtendedResult passwordModify(PasswordModifyRequest req, Control... controls);

        // Cancel (RFC 3909) – graceful cancel (different from Abandon)
        ExtendedResult cancel(int messageId);

        /* =========================
         * Controls (request/response)
         * ========================= */
        void setDefaultRequestControls(Control... controls);
        List<Control> getResponseControls(); // from last op (thread-scope or returned in results)

        /* =========================
         * Async variants (optional)
         * ========================= */
        CompletableFuture<SearchResult> searchAllAsync(SearchRequest req);
        CompletableFuture<LdapResult> addAsync(Entry entry, Control... controls);
        CompletableFuture<LdapResult> modifyAsync(DN dn, List<Modification> modifications, Control... controls);
        CompletableFuture<LdapResult> deleteAsync(DN dn, Control... controls);
        CompletableFuture<LdapResult> modifyDNAsync(DN dn, RDN newRdn, boolean deleteOldRdn, DN newSuperior, Control... controls);
        CompletableFuture<CompareResult> compareAsync(DN dn, String attribute, byte[] assertionValue, Control... controls);
        CompletableFuture<ExtendedResult> extendedAsync(ExtendedRequest request, Control... controls);

        /* =========================
         * Types (minimal conceptual model)
         * ========================= */

        // Distinguished Name (DN) / Relative DN (RDN)
        record DN(String value) {}
        record RDN(String value) {}

        // Entry & Attribute
        record Entry(DN dn, List<Attribute> attributes) {}
        record Attribute(String type, List<byte[]> values) {} // bytes to keep neutral (UTF-8, binary, etc.)

        // Search request (all knobs per RFC 4511 §4.5.1)
        record SearchRequest(
                DN base,
                SearchScope scope,
                DerefAliases derefAliases,
                int sizeLimit,               // 0 = no limit (server-side policy may apply)
                int timeLimitSeconds,        // 0 = no limit
                boolean typesOnly,
                Filter filter,
                List<String> attributes,     // requested attribute types (empty = all user attrs)
                List<Control> controls
        ) {}

        enum SearchScope { BASE_OBJECT, SINGLE_LEVEL, WHOLE_SUBTREE, SUBORDINATE_SUBTREE /* RFC 3673 */ }
        enum DerefAliases { NEVER_DEREF_ALIASES, DEREF_IN_SEARCHING, DEREF_FINDING_BASE_OBJ, DEREF_ALWAYS }

        // Filter (string or structured AST)
        sealed interface Filter permits Filter.And, Filter.Or, Filter.Not, Filter.Present, Filter.Equals, Filter.GreaterOrEqual, Filter.LessOrEqual, Filter.Approx, Filter.Substring, Filter.Extensible {
            record And(List<Filter> children) implements Filter {}
            record Or(List<Filter> children) implements Filter {}
            record Not(Filter child) implements Filter {}
            record Present(String attr) implements Filter {}
            record Equals(String attr, byte[] value) implements Filter {}
            record GreaterOrEqual(String attr, byte[] value) implements Filter {}
            record LessOrEqual(String attr, byte[] value) implements Filter {}
            record Approx(String attr, byte[] value) implements Filter {}
            record Substring(String attr, String initial /*nullable*/, List<String> any, String fin /*nullable*/) implements Filter {}
            record Extensible(String attr /*nullable*/, String matchingRule /*nullable*/, boolean dnAttributes, byte[] value) implements Filter {}
            static Filter fromString(String rfc4515) { /* parse impl */ throw new UnsupportedOperationException(); }
        }

        // Modify item (RFC 4511 §4.6)
        enum ModificationOp { ADD, DELETE, REPLACE }
        record Modification(ModificationOp op, Attribute attribute) {}

        // Results
        enum ResultCode {
            SUCCESS, OPERATIONS_ERROR, PROTOCOL_ERROR, TIME_LIMIT_EXCEEDED, SIZE_LIMIT_EXCEEDED,
            COMPARE_FALSE, COMPARE_TRUE, AUTH_METHOD_NOT_SUPPORTED, STRONG_AUTH_REQUIRED,
            REFERRAL, ADMIN_LIMIT_EXCEEDED, UNAVAILABLE_CRITICAL_EXTENSION,
            CONFIDENTIALITY_REQUIRED, SASL_BIND_IN_PROGRESS,
            NO_SUCH_ATTRIBUTE, UNDEFINED_ATTRIBUTE_TYPE, INAPPROPRIATE_MATCHING,
            CONSTRAINT_VIOLATION, ATTRIBUTE_OR_VALUE_EXISTS, INVALID_ATTRIBUTE_SYNTAX,
            NO_SUCH_OBJECT, ALIAS_PROBLEM, INVALID_DN_SYNTAX, ALIAS_DEREF_PROBLEM,
            INAPPROPRIATE_AUTHENTICATION, INVALID_CREDENTIALS, INSUFFICIENT_ACCESS_RIGHTS,
            BUSY, UNAVAILABLE, UNWILLING_TO_PERFORM, LOOP_DETECT,
            NAMING_VIOLATION, OBJECT_CLASS_VIOLATION, NOT_ALLOWED_ON_NON_LEAF,
            NOT_ALLOWED_ON_RDN, ENTRY_ALREADY_EXISTS, OBJECT_CLASS_MODS_PROHIBITED,
            AFFECTS_MULTIPLE_DSAS, OTHER
        }

        record LdapResult(ResultCode code, String matchedDN, String diagnosticMessage, List<String> referrals, List<Control> responseControls) {}
        record BindResult(LdapResult result, byte[] serverSaslCreds /*nullable*/) {}
        record CompareResult(LdapResult result, boolean matched) {}

        // Search responses (entries, references, done)
        record SearchResultEntry(Entry entry, List<Control> controls) {}
        record SearchReference(List<String> uris, List<Control> controls) {}
        record SearchDone(LdapResult result) {}
        record SearchResult(List<SearchResultEntry> entries, List<SearchReference> references, LdapResult done) {}

        // Controls (RFC 4511 §4.1.12); use OID + value for extensibility
        record Control(String oid, boolean critical, byte[] value /* BER-encoded */) {
            // Common controls helpers (paged results RFC 2696, server-side sort RFC 2891, etc.) could be added here
            static Control pagedResults(int pageSize, byte[] cookie) { return new Control("1.2.840.113556.1.4.319", false, /*encode*/ null); }
            static Control serverSideSort(byte[] sortKeysBer) { return new Control("1.2.840.113556.1.4.473", true, sortKeysBer); }
        }

        /* Extended Ops plumbing */
        record ExtendedRequest(String requestNameOid, byte[] requestValue /*nullable*/) {}
        record ExtendedResult(LdapResult result, String responseNameOid /*nullable*/, byte[] responseValue /*nullable*/) {}

        /* Common Extended Requests */
        record PasswordModifyRequest(String userIdentityDN /*nullable*/, char[] oldPass /*nullable*/, char[] newPass /*nullable*/) {}
        record TLSOptions(boolean requireHostnameVerification) {}

        /* Streaming callbacks */
        @FunctionalInterface interface SearchEntryHandler { void onEntry(SearchResultEntry entry); }
        @FunctionalInterface interface SearchRefHandler { void onReference(SearchReference ref); }
    }
    ```

    ### 小重點（為什麼這些是「常見概念 API」）

    * **Bind / Unbind / StartTLS**：會話建立與加密協商的基本面。
    * **Search / Add / Modify / Delete / ModifyDN / Compare**：RFC 4511 規範的六大核心操作。
    * **Abandon / Cancel / Extended**：中止請求與擴充能力；WhoAmI、PasswordModify 都是常用 extended。
    * **Controls**：分頁（RFC 2696）、排序（RFC 2891）等「請求/回應控制碼」是實務必備。
    * **Filter/Scope/Deref/Limit**：把搜尋的所有旋鈕（RFC 4511 §4.5.1）完整建模，避免日後打補丁。

    如果你要，我也可以把這份介面拆成「LdapConnection（有狀態）」＋「LdapClient（工廠）」兩層，或補上幾個常用 Control/Extended 的 BER 編碼 helper 範例。






<br>

{%hackmd vaaMgNRPS4KGJDSFG0ZE0w %}