# mars-nwe NCP dispatch redesign notes This file collects design notes for a possible cleanup of the internal NCP handoff path. It is intentionally separate from `TODO.md`: the TODO file should track concrete bugs and endpoint audit follow-ups, while this file describes a larger architecture direction that can be implemented gradually. The goal is not to rewrite MARS-NWE at once. The goal is to make the current handoff behavior explicit, reduce ambiguity around magic return values, and make future endpoint work easier to audit against the Novell/Micro Focus SDK, WebSDK, and NDK Core Protocols PDF. ## Current problem The current NCP path grew around several cooperating processes and handlers: - `nwconn.c` owns the connection/session side and receives most packets first. - `nwbind.c` handles bindery, queue, some server-management, and some final reply construction. - Other modules such as semaphore, message, namespace, AFP, file, salvage, and queue code implement individual protocol families or backend actions. - Some calls are handled completely in `nwconn.c`. - Some calls are forwarded to `nwbind.c` by returning `-1` from the `nwconn.c` dispatcher. - Some calls are forwarded with saved request state by returning `-2`, so that `nwconn.c` can do post-processing after `nwbind.c` has replied. - Some forwarded paths mutate request payloads before handoff. - Some code paths build responses locally, while other paths rely on the target process to build the final completion code and payload. This works, but it is hard to reason about while auditing endpoint layouts. The same looking value can mean different things depending on which file it appears in. For example, `return(-1)` in the relevant `nwconn.c` dispatcher path means "forward this request to `nwbind`". A disabled `return(-1)` inside a `#if 0` block in `nwbind.c` does not have that forwarding meaning and should not be copied into active code. The visible symptoms are: - endpoint documentation must follow a handoff across files before it can say the request or reply layout is known; - missing endpoints are difficult to distinguish from forwarded endpoints; - request parsing, backend behavior, reply encoding, and process routing are often mixed in one switch block; - byte order differences are easy to miss because parsing and reply writing are open-coded in different places; - disabled future stubs can look like active dispatch behavior; - `TODO.md` can become a dumping ground for architectural observations that are not immediate endpoint bugs. ## Desired shape A cleaner long-term structure would have one small internal NCP dispatch layer: ```text wire packet -> NCP envelope parser -> NcpContext -> endpoint lookup -> endpoint handler / provider -> reply encoder -> central reply sender ``` This does not need to be a general-purpose message bus. A full message bus would probably be too large and too abstract for this code base. A typed internal NCP context plus explicit dispatch results would be enough. The important separation is: 1. decode the packet envelope; 2. identify the endpoint; 3. decode the endpoint request body; 4. execute the backend operation; 5. encode the endpoint reply body; 6. send the response from one well-defined place. ## Proposed NCP context Introduce, in a later functional cleanup, a small context object that represents one NCP request while it moves through the server. The exact field names should fit the existing code style, but the conceptual shape would be: ```c typedef struct { int connection; uint16_t request_type; /* 0x2222, 0x3333, 0x5555, ... */ uint8_t function; /* top-level NCP function */ /* * Some NCP families are only one level deep, but others are nested. * The selector path records the bytes/words that identify the logical * operation after the top-level function, without pretending that every * family has exactly one byte-sized subfunction. */ int selector_count; uint32_t selector[4]; /* e.g. subfunction, level, verb, info type */ const uint8_t *request; int request_len; uint8_t *reply; int reply_cap; int reply_len; uint8_t completion; uint8_t connection_status; uint32_t flags; } NcpContext; ``` The context should not replace all old globals in one patch. It can start as a thin wrapper around the existing request and response buffers, then gradually become the preferred handler interface. The useful property is that endpoint documentation can point to a stable model: - `function` identifies the first NCP selector byte; - `selector[]` identifies any nested selector path after that byte; - `request` and `request_len` are the bytes after the already-decoded envelope; - `reply` and `reply_len` are the bytes before the common NCP response envelope; - `completion` is set once by the handler or by central error handling. Do not assume that the logical endpoint key always stops at `request_type/function/subfunction`. The Novell documentation has several families where an endpoint has another selector inside the subfunction payload. Examples include NDS fragmented requests (`0x2222/104/02`) where the request contains a 32-bit NDS verb, statistical calls such as `0x2222/123/34` where an `InfoLevelNumber` selects the returned structure, NCP extension calls where the extension number is dynamic, and reply formats that vary by information type. The audit notation for such cases should make the nesting explicit, for example `0x2222/104/02 verb=...` or `0x2222/123/34 level=2`, instead of flattening it into an invented one-byte `zz` case. ## Replace magic return values with named results The current `0`, `-1`, and `-2` convention should be made explicit before any larger refactor. The first step can be documentation-only or macro-only: ```c #define NCP_LOCAL_DONE 0 #define NCP_FORWARD_NWBIND -1 #define NCP_FORWARD_NWBIND_POST -2 ``` A later cleanup can replace those with an enum: ```c typedef enum { NCP_DISPATCH_DONE, NCP_DISPATCH_FORWARD_BIND, NCP_DISPATCH_FORWARD_BIND_POST, NCP_DISPATCH_NOT_IMPLEMENTED, NCP_DISPATCH_BAD_REQUEST, NCP_DISPATCH_INTERNAL_ERROR } NcpDispatchResult; ``` The important rule is that the meaning must be scoped. A named result returned from a `nwconn.c` dispatcher may request process handoff. A return statement in `nwbind.c` should not silently inherit that meaning unless the function is explicitly part of the same dispatch interface. ## Endpoint table as audit index first Before replacing switch statements, add an endpoint inventory table as a non-invasive audit aid. It can be compiled only for debug builds or kept as a source-level documentation table. Conceptual form: ```c typedef struct { uint16_t request_type; uint8_t function; int selector_count; uint32_t selector[4]; const char *selector_note; const char *name; const char *provider; uint32_t flags; } NcpEndpointDoc; ``` Example entries: ```c { 0x2222, 23, 1, { 109 }, "subfunction", "Change Queue Job Entry old", "nwbind/queue", NCPDOC_FORWARDED }, { 0x2222, 32, 1, { 0 }, "subfunction", "Open Semaphore old", "sema", NCPDOC_LOCAL }, { 0x2222, 33, 0, { 0 }, NULL, "Negotiate Buffer Size", "nwconn", NCPDOC_LOCAL }, /* Later NetWare 4.x examples that need more than one logical selector. */ { 0x2222, 104, 2, { 2, 0 }, "subfunction + NDS verb", "Send NDS Fragmented Request/Reply", "nwnds", NCPDOC_FUTURE }, { 0x2222, 123, 2, { 34, 2 }, "subfunction + info level", "Get Volume Information by Level", "servermgmt", NCPDOC_FUTURE }, ``` This table would help with the ongoing endpoint audit: - SDK/PDF/WebSDK listed and implemented; - SDK/PDF/WebSDK listed and forwarded; - SDK/PDF/WebSDK listed but disabled as a future stub; - SDK/PDF/WebSDK listed but absent from the current compatibility target; - later NetWare 4.x/OES/MOAB endpoint, not part of the default NetWare 3.x compatibility target. The first version should not drive runtime dispatch. It should only make review and missing-endpoint checks less error-prone. The table should be able to represent a selector path rather than only a single subfunction. This matters for later NetWare 4.x families and for extension mechanisms. The first selector element is usually the documented subfunction byte, but later elements may be 16-bit or 32-bit fields from the request body, not dispatch bytes in the classic switch sense. Treat them as layout selectors, not as automatic nested `switch` cases unless the code actually dispatches on them. ## Handler structure For newly touched endpoint families, prefer the following logical split even if it remains in one C function at first: ```text request decode -> validation -> backend operation -> reply encode ``` For complex endpoints this could become explicit helper functions: ```c static int decode_foo(NcpContext *ctx, FooRequest *out); static int exec_foo(NcpContext *ctx, const FooRequest *req, FooReply *reply); static void encode_foo(NcpContext *ctx, const FooReply *reply); ``` This is especially useful for endpoint families where the audit has already found old/new layout differences: - 16-bit old queue job numbers versus newer 32-bit job numbers; - big-endian versus little-endian SDK notation; - old short replies versus newer long replies; - connection-side prehandling that inserts or rewrites fields; - bindery or queue paths that build final replies in a different process. Small endpoints do not need three separate helper functions if that would make the code noisier. The rule is that request bytes and reply bytes should be easy to identify and compare with the SDK documents. ## Make handoff explicit Forwarded calls should say exactly what is handed off. A good comment should answer: - which bytes are forwarded; - whether the subfunction byte is preserved or stripped; - whether `nwconn.c` mutates the request before forwarding; - whether `nwbind.c` or another provider builds the final reply; - whether `nwconn.c` expects post-processing after the provider reply. Examples of handoff cases that need this clarity: - Queue calls where `nwconn.c` expands paths or inserts job file handles before `nwbind.c` sees the request. - Quota/bindery prehandling where the destination handler receives an already transformed request. - Semaphore and message groups that are grouped in the SDK but routed through local helper modules. - Direct lifecycle calls such as End Of Job and Logout where local cleanup and final success reply are split across files. The preferred future style is not "`nwbind` must do the rest" but something like: ```text Forward to nwbind with the original subfunction byte and payload unchanged. No nwconn post-processing is expected; nwbind builds the completion-only reply. ``` or: ```text Forward to nwbind after saving the original request. nwbind validates bindery state and returns the bindery result; nwconn then performs the file-handle post-processing in handle_after_bind(). ``` ## Response building rule Every endpoint audit should identify the reply builder, not only the request parser. A handler is not fully documented until the response path is known. For each endpoint family, record: - completion-only reply; - fixed-size payload reply; - variable-length payload reply; - provider-built reply; - `nwconn.c` post-processed reply; - intentionally unsupported reply status. Long-term, response sending should become centralized enough that endpoint code only encodes payload bytes and a completion code. This reduces off-by-one reply length bugs and makes the logs easier to normalize. ## Normalized inter-process handoff replies The process handoff path should be normalized before adding more provider processes. The current `nwconn` to `nwbind` forwarding path relies on magic return values and implicit shared-buffer conventions. That is workable for the historic two-process case, but it will not scale cleanly to future providers such as `nwqueue`, `nwnds`, or a directory service. The long-term rule should be: ```text Every provider process returns exactly one internal handoff reply for every internal handoff request it accepts. ``` That internal reply is not the same thing as a client-visible NCP reply. A provider may explicitly say that no client reply should be sent, but it should still send a formal internal result back to the caller. This avoids silent success/failure paths and makes timeout/error handling deterministic. Conceptual reply kinds: ```c typedef enum { NW_HR_REPLY, /* provider produced a client NCP reply payload */ NW_HR_NO_REPLY, /* provider handled it; nwconn must not send a client reply */ NW_HR_DEFERRED, /* accepted; final reply/event will be produced later */ NW_HR_FORWARD, /* provider requests forwarding to another provider */ NW_HR_ERROR /* internal provider or handoff failure */ } NwHandoffReplyKind; ``` The normal successful completion-only case is still a reply: ```text kind = NW_HR_REPLY completion = 0x00 reply_len = 0 ``` The true "do not answer the client" case is explicit: ```text kind = NW_HR_NO_REPLY reply_len = 0 ``` Do not encode this as text in a payload such as `"no reply"`. It should be a machine-readable reply kind so that `nwconn` can make one central decision. A conceptual internal handoff reply header could look like this: ```c typedef struct { uint16_t version; uint16_t kind; uint32_t request_id; uint32_t connection_id; uint32_t sequence; uint32_t task_id; uint8_t completion; uint8_t connection_status; uint32_t flags; uint32_t reply_len; } NwHandoffReply; ``` The matching request should carry the same correlation fields plus the NCP selector path and payload length. The exact structure can follow existing mars-nwe style, but the contract should be stable: ```text nwconn -> provider: request_id, connection_id, sequence, task, selector path, request payload provider -> nwconn: same request_id, reply kind, completion/status, reply payload length, payload ``` This gives future provider processes a uniform contract: ```text nwconn -> nwbind nwconn -> nwqueue nwconn -> nwnds nwconn -> nwdirectory ``` all use the same shape. Provider-specific behavior belongs in the payload and provider API, not in special process-specific return conventions. ### Reply ownership The preferred long-term ownership rule is: ```text Provider builds the logical reply payload and completion/status. nwconn owns the final client NCP response envelope and sends it to the client. ``` This means providers should not directly send client packets. They return an internal result that says what should happen. `nwconn` then applies the original sequence, connection number, task, transport, and NCP envelope rules in one place. This avoids several classes of bugs: - duplicate replies; - wrong sequence or task in replies; - inconsistent completion-only reply lengths; - provider-specific send/error paths; - unclear post-processing after `nwbind` replies; - future provider processes needing to know transport details. Legacy paths may continue to have provider-built replies during migration, but that should be marked as a legacy compatibility mode, not the design target. ### Post-processing without `return(-2)` The current `return(-2)` convention means roughly "forward to bindery, save the original request, then let `nwconn` do more work after the provider reply". In a normalized handoff this should become explicit state, not a magic result value. Possible flags: ```c #define NW_HF_SAVE_ORIGINAL_REQUEST 0x00000001 #define NW_HF_POSTPROCESS_REPLY 0x00000002 #define NW_HF_LEGACY_PROVIDER_REPLY 0x00000004 ``` The request context can also name the post-processing hook, or store a small post-processing enum, so the provider does not need to know why the caller will continue after the reply. This keeps the handoff transport generic. ### Error mapping and dead-provider behavior The handoff layer should define what happens when a provider fails before a protocol handler can return a normal NetWare completion code. Otherwise every new provider will invent a slightly different failure path. The normalized rules should cover: - provider process not running; - provider closes the IPC channel; - malformed internal reply; - mismatched `request_id`; - provider timeout; - reply payload longer than negotiated capacity; - provider returned `NW_HR_ERROR` with an internal error code; - provider returned `NW_HR_FORWARD` to an unsupported target. For client-visible requests, `nwconn` should map those failures through one central function to either an NCP completion code, a connection-level failure, or an intentional disconnect. Endpoint handlers should not open-code provider IPC failures as arbitrary completion bytes. ### Correlation and replay safety Every internal handoff request should carry a monotonically useful correlation identifier, even if the first implementation is only local to one `nwconn` process. The tuple should include enough information to catch stale or crossed replies: ```text request_id + connection_id + sequence + task_id ``` This matters once there are multiple provider processes or deferred replies. It also makes logging and debugging much easier, because an endpoint audit can show the complete path of one request through several processes. ### Payload ownership and size limits The handoff protocol should define who owns buffers and what size limits apply. At minimum, document these rules before functional refactoring: - request payload is immutable after handoff unless a mutating legacy wrapper is explicitly documented; - provider reply payload length must be checked against caller capacity; - variable-length replies must report exact encoded length; - zero-length payload is valid for completion-only replies; - `NO_REPLY` is a reply kind, not a zero-length success reply; - byte order inside payloads remains the NCP endpoint's documented wire order, not native host order. This is especially important for nested selector families and old/new endpoint variants, where a provider may need to choose different reply structures based on a level, verb, or information type. ### Logging and audit benefit A normalized handoff reply gives logging one consistent shape: ```text REQ id=42 conn=7 seq=19 ncp=0x2222/23/113 provider=queue len=... RPLY id=42 conn=7 seq=19 kind=REPLY completion=0x00 len=... RPLY id=43 conn=7 seq=20 kind=NO_REPLY reason=... ERR id=44 conn=7 seq=21 provider=bindery error=timeout mapped=0xfb ``` This would also make the endpoint documentation pass easier: each audited endpoint can identify the provider, the request layout, the logical reply kind, the reply payload layout, and any caller-side post-processing. ### Migration order for handoff normalization The safe order is: 1. document current `nwconn`/`nwbind` handoff behavior; 2. add names for current magic values without changing behavior; 3. add a small wrapper such as `ncp_handoff_to_provider()` that still calls the old path internally; 4. introduce a formal internal reply object in the wrapper; 5. make the wrapper always return a formal reply, including `NO_REPLY`; 6. centralize final client reply sending in `nwconn` for converted paths; 7. only then attach future providers such as `nwqueue` or `nwnds`. The rule is: do not create a new provider process until the caller can receive a formal reply from it and can handle provider failure centrally. ## `nwserv` as control plane, not data-plane router Future provider processes need a way to find and trust each other, but normal request payloads should not all be routed through `nwserv`. `nwserv` should stay the supervisor and registry for the mars-nwe process tree. It should not become a central payload broker for every decoded NCP request. The preferred split is: ```text nwserv: control plane process supervision provider registry endpoint/socket ownership restart and shutdown coordination nwconn <-> provider: data plane direct request/reply IPC normalized handoff messages ``` So a future request path should look like this: ```text client -> nwconn -> direct provider IPC -> provider -> nwconn -> client ``` not like this: ```text client -> nwconn -> nwserv -> provider -> nwserv -> nwconn -> client ``` `nwserv` may still create, own, or advertise IPC endpoints. For example, it can start `nwbind`, `nwqueue`, `nwdirectory`, or `nwnds`, create a protected runtime directory such as `/run/mars-nwe`, assign socket paths or inherited file descriptors, and record which provider is currently alive. A `nwconn` process can then discover provider endpoints from configuration, inherited descriptors, or a small `nwserv` registry query. After discovery, normal handoff traffic should go directly to the provider. This keeps `nwserv` small and avoids several failure modes: - no extra copy and latency for every NCP handoff; - no single data-plane bottleneck; - no need for `nwserv` to understand every provider payload; - fewer decoded password/auth/directory payloads visible to the supervisor; - easier provider-specific timeouts and back-pressure; - clearer ownership: `nwconn` owns the client connection, providers own their service logic, and `nwserv` owns lifecycle. The kinds of messages that should go through `nwserv` are control messages: - provider started, registered, unhealthy, or exited; - provider restart requested or refused; - global shutdown or graceful drain; - configuration reload notification; - socket/FD registration and permission setup; - health and version/capability queries. The kinds of messages that should not normally go through `nwserv` are data-plane messages: - decoded NCP request payloads; - Bindery object/property operations; - Queue job lifecycle operations; - Directory/NDS authentication or schema operations; - file/volume provider payloads; - provider replies carrying completion/status and reply payloads. There can be narrow exceptions during migration, especially for existing legacy `nwconn`/`nwbind` plumbing, but those exceptions should be documented as legacy wrappers. New provider processes should be designed around direct normalized IPC from the caller to the provider. This also fits the secure IPC policy: local direct IPC can use protected Unix-domain sockets, pipes, or inherited descriptors. If a future provider is connected over TCP instead, that specific provider IPC channel must use the separate wolfSSL/mTLS policy described below. `nwserv` discovery must not be used as an excuse to downgrade provider data-plane traffic to plaintext TCP. ## Secure internal provider IPC and client transport compatibility Provider handoff security and client transport compatibility are separate concerns. Future TCP/IP support for client-facing NCP/NDS traffic must not imply that every historic NetWare 4.x-compatible client is required to speak TLS. Classic compatibility should remain protocol-accurate first; TLS for client-facing protocols should only be enabled where the real protocol/frontend supports it, such as LDAP/LDAPS/StartTLS on `nwdirectory` or a later explicitly secure client protocol. Internal provider IPC is different. Once a request has been decoded or partially decoded by `nwconn`, the internal handoff payload can contain sensitive material: password data, login/authentication material, Bindery object properties, Queue state, future NDS credentials, or directory attributes. A plaintext TCP-based provider handoff would expose more than the original network packet boundary did. The design rule should be: ```text local provider IPC: Unix-domain sockets or pipes are acceptable when filesystem permissions, process ownership, and socket directories are strict. TCP-based provider IPC: never plaintext; wolfSSL-backed TLS with mutual authentication is required. ``` So the local default may remain simple and compatible: ```text nwconn -> nwbind local pipe/Unix-domain IPC, strict permissions nwconn -> nwqueue local pipe/Unix-domain IPC, strict permissions nwconn -> nwnds local pipe/Unix-domain IPC, strict permissions ``` But any future provider split over TCP, even if it is only `localhost` TCP or a container boundary, must be treated as a secure channel: ```text nwconn -> provider over TCP: wolfSSL required mutual authentication required no anonymous TLS no opportunistic downgrade to plaintext clear failure if certificates/keys are missing or invalid ``` This is intentionally stricter than client-facing compatibility. Historical NetWare 4.x/NDS clients should not be forced to use TLS just because internal provider IPC can use TLS. The two policy surfaces are independent: ```text Client NCP/NDS transport: compatibility first; no blanket TLS requirement for classic clients. LDAP/LDAPS/StartTLS: handled by nwdirectory; wolfSSL is appropriate at that network edge. Internal provider IPC over TCP: always wolfSSL/mTLS because decoded server-internal data may cross it. ``` Local IPC still needs security rules even without TLS: - IPC sockets/directories should not be world-readable or world-writable; - provider processes should run as the expected user/group; - raw handoff payloads must not be logged by default; - debug dumps should redact or omit authentication payloads; - sensitive request/reply buffers should be wiped when practical after use; - core dumps for processes handling credentials should be considered unsafe by default; - formal handoff logging should record selector paths, provider names, reply kinds, and completion codes, not raw secret bytes. This also means there are two different wolfSSL uses in the long-term design: ```text 1. LDAP TLS: external LDAP/LDAPS/StartTLS clients -> nwdirectory 2. Provider IPC TLS: mars-nwe process -> mars-nwe provider process, only when the provider IPC transport is TCP-based ``` The two uses should have separate configuration and credentials. Reusing an external LDAP certificate as an internal provider trust root should not be the default assumption. A later setup tool can create or install a local mars-nwe provider-IPC CA/key set if TCP-based provider IPC ever becomes supported. ## Provider boundaries A clean design would treat the existing modules as providers instead of hidden fallback paths: ```text nwconn connection/session, packet IO, top-level envelope ncpdispatch endpoint lookup, handoff policy, common errors nwbind bindery database and bindery-backed services queue queue metadata and print/backend adapter sema semaphore state message station/message/broadcast state namespace path, directory handle, name-space operations file file handle and read/write/open/close operations salvage deleted-file scan/recover/purge backend AFP AFP metadata and AFP namespace adapter ``` This is a design target, not a demand to move files immediately. The important part is that future code should avoid making `nwbind` a catch-all sink for unrelated NCPs just because it already has an IPC path. ## Provider boundary versus process boundary A provider boundary is not the same thing as a Unix process boundary. This is an important distinction because splitting every NCP family into a separate process would make the server harder to debug and could introduce new ordering, locking, and reply-ownership bugs. The preferred rule is: ```text first define logical providers; only later promote the few large stateful providers to separate processes. ``` A logical provider can start as an ordinary C module called from the existing process path. It becomes valuable as soon as the dispatch table can say "this endpoint belongs to the queue provider" or "this endpoint belongs to the connection-local provider", even if no new process exists yet. A process split should be treated as an implementation detail that is only justified when the provider has enough independent state and lifecycle to benefit from isolation. This keeps the redesign incremental: ```text now: nwconn switch -> existing local code or nwbind handoff first cleanup: nwconn switch -> provider-named helper/module later, only where useful: nwconn/dispatcher -> IPC -> provider process ``` ### Good process candidates #### Bindery Bindery is already a natural service boundary. It owns long-lived server state: objects, properties, sets, security, password/login/key handling, and object lookup. Keeping bindery behind a clear provider boundary is appropriate, and the existing `nwbind` process can remain that boundary while the dispatch layer is cleaned up. The main cleanup is not to remove `nwbind`, but to stop treating it as a generic catch-all for unrelated forwarded requests. A future endpoint table should mark true bindery calls as `bindery`, and queue or management calls should not be classified as bindery merely because their current implementation lives in `nwbind.c`. #### Queue / possible `nwqueue` Queue management is the strongest candidate for a future separate process after bindery. Queue handling has its own domain state: - queue objects and queue metadata; - queue job lifecycle; - queue server attach/detach state; - service, finish, and abort state; - job position and priority; - client-rights transitions during job servicing; - queue directories and spool/job files. That is large enough to deserve a logical `queue` provider even before any runtime split. A future `nwqueue` process can be considered once request/reply ownership and bindery access are explicit. The first step should only be a provider split: ```text 0x2222/23 queue subfunctions -> queue provider queue provider -> bindery provider/library for object/security/property checks queue provider -> file/path helpers for queue job files ``` A real `nwqueue` process should not be created by simply moving the current queue cases out of `nwbind.c`. It needs an explicit contract for: - which process owns the final NCP reply; - how queue calls read bindery objects and properties; - how queue job files are opened and handed back to the connection process; - how connection cleanup affects attached queue servers and in-service jobs; - how old 16-bit job-number calls and newer 32-bit job-number calls are kept compatible. Until those contracts are clear, `nwqueue` should remain a design target, not an immediate functional change. ### Possible but risky process candidates #### File and volume subsystem The file/volume/name-space area is large and stateful, so it can look like a candidate for a separate process. It owns or touches directory handles, file handles, locks, trustee evaluation, volume information, name spaces, salvage and purge operations, and Unix filesystem mapping. However, this area is also tightly coupled to connection state and existing file descriptor ownership. Moving it behind IPC too early could create more problems than it solves. The safer path is: ```text first: file/volume/name-space provider modules inside the current process model later: consider a process split only after handle ownership is explicit ``` A file provider boundary is useful for documentation and dispatch cleanup. A separate file process is optional and should be considered high-risk. #### Accounting Accounting is a maybe. It has a separate protocol domain, but in many setups it may be small enough to stay as an in-process provider. A process boundary only makes sense if accounting grows into a real persistent service with charges, holds, notes, audit records, and recovery behavior that should be isolated from connection handlers. ### Poor process candidates #### Semaphore Semaphore calls should have a clean provider boundary, but a dedicated process is probably overkill. The old semaphore group is small: open, examine, wait, signal, and close. It needs shared state, but not necessarily a standalone process. A `sema` provider module with clear request/reply ownership should be enough unless later testing shows that cross-connection semaphore state cannot be managed safely in the existing process model. #### Connection lifecycle and session-local calls Connection lifecycle operations should stay with `nwconn` or a connection-local provider. Calls such as Logout, End Of Job, watchdog handling, buffer negotiation, and connection-state cleanup are fundamentally tied to the session that received the packet. Moving them into another process would make cleanup ordering and error handling harder. #### Simple server-management calls Simple management and information calls should not become their own process. Examples include login-status queries, server description strings, server time, console-privilege checks, and small broadcast/control helpers. These can be represented as a `servermgmt` provider for dispatch clarity, but they should stay in-process unless a specific call requires an existing backend service. ### Suggested provider map The endpoint audit table should be able to use provider names like these: ```text local packet/session-local handling in nwconn bindery object/property/security/login backend queue queue objects, jobs, queue servers, spool/job lifecycle filesystem file, directory, volume, namespace, trustee, salvage helpers semaphore semaphore state and old 0x2222/32 calls message station messaging and broadcast helpers servermgmt small server-management and information calls accounting account status, charges, holds, notes AFP AFP namespace and metadata helpers unknown documented but not yet mapped ``` Only some providers should ever become processes: ```text already process-like: bindery / nwbind likely future process: queue / possible nwqueue maybe, high risk: filesystem usually in-process: semaphore, message, servermgmt, accounting, AFP helpers ``` The practical design rule is: ```text Use provider names everywhere in documentation and endpoint tables. Use new processes only where shared state, isolation, and lifecycle justify the extra IPC complexity. ``` ## Future NetWare 4.x directory, LDAP, and storage direction NetWare 4.x support should not be added by letting `nwbind` grow into a second large catch-all service. The long-term directory design should keep the legacy Bindery, the future NDS compatibility layer, and the LDAP protocol frontend as separate logical layers above one shared directory store. The intended naming model is: ```text libflaim persistent embedded database engine libdirectory shared internal directory API/library used by nwbind, nwnds, nwdirectory, and setup/provisioning tools owns the mars-nwe object model, schema helpers, indexes, ACL/auth primitives, and persistence glue above libflaim directory core/store the data model and persistent store exposed through libdirectory persists its data through libflaim nwdirectory mars-nwe service name for the integrated tinyldap-derived LDAP service owns LDAP/LDAPS/StartTLS protocol handling uses wolfSSL only at the LDAP network/TLS edge calls the directory core/store, not Bindery or NDS packet handlers nwnds future NetWare 4.x/NDS compatibility layer owns NDS/NCP directory semantics, contexts, tree-oriented operations, NetWare-specific rights/auth behavior, and later compatibility glue calls the directory core/store directly nwbind legacy NetWare 2.x/3.x Bindery compatibility layer maps Bindery objects, properties, sets, security, and login-visible behavior onto the shared directory core/store where possible ``` In this model, `nwdirectory` is not a separate design from tinyldap. It is the mars-nwe integration name for the tinyldap-derived LDAP directory service, so that the installed binary/module follows the existing `nw*` naming scheme. The upstream tinyldap code can provide the LDAP protocol implementation, but the project-facing component should be named `nwdirectory`. `libdirectory` is the important internal boundary. It should be a real shared API/library, not just a documentation label, because both `nwbind` and future `nwnds` need directory data without speaking LDAP to each other. The library can start small, but it should provide the common operations that legacy Bindery, NDS compatibility, LDAP, and setup code all need: ```c dir_open_store(); dir_close_store(); dir_txn_begin(); dir_txn_commit(); dir_txn_abort(); dir_object_create(); dir_object_delete(); dir_object_lookup_by_id(); dir_object_lookup_by_name(); dir_object_search(); dir_attr_get(); dir_attr_set(); dir_attr_delete(); dir_acl_check(); dir_auth_verify(); dir_schema_get(); ``` The exact function names are placeholders, but the ownership rule is important: NetWare protocol handlers should call a directory API, not encode LDAP requests to reach local server state. If `nwdirectory` later runs as a separate process, `libdirectory` can either remain the shared embedded store library or define the internal IPC contract. In both cases the protocol layers still depend on the directory API, not on LDAP text/protocol behavior. `nwnds` should remain a separate layer because LDAP is only one protocol view of the directory. NDS has NetWare-specific semantics that should not be forced into the LDAP frontend. Conversely, LDAP clients should not be required to pass through the NDS/NCP compatibility handler just to reach the directory database. The preferred relationship is sibling frontends above one core: ```text +----------------------+ | directory core/store | | backed by libflaim | +----------+-----------+ ^ +---------------+---------------+ | | nwdirectory nwnds tinyldap-based LDAP/LDAPS NetWare 4.x/NDS semantics frontend, wolfSSL TLS edge NCP/NDS compatibility layer ^ ^ | | LDAP clients NetWare/NDS clients ``` The legacy Bindery service should also move toward this shared store over time: ```text NetWare 3.x client -> Bindery NCP -> nwbind -> directory core/store -> libflaim LDAP client -> LDAP/LDAPS -> nwdirectory -> directory core/store -> libflaim NetWare 4.x client -> NDS/NCP -> nwnds -> directory core/store -> libflaim ``` That means `nwbind` should become a compatibility mapping over directory objects and attributes instead of maintaining a completely separate long-term identity truth. This is especially important once NetWare 4.x/NDS support exists, because Bindery compatibility can then be implemented as a legacy view of the same underlying users, groups, properties, and rights data. The internal path should not be: ```text nwbind -> LDAP protocol -> nwdirectory -> directory store nwnds -> LDAP protocol -> nwdirectory -> directory store ``` Using LDAP as the mandatory internal storage API would mix protocol concerns into server internals, make old Bindery behavior harder to preserve, and add needless encoding/search semantics between tightly coupled modules. LDAP should remain an external protocol frontend. `nwbind`, `nwnds`, and `nwdirectory` should all use `libdirectory`, or a clearly defined IPC protocol modeled after the same directory API, to reach the directory store. FLAIM should therefore be treated as the long-term persistent storage engine for the directory core, not as an LDAP-only database. `libdirectory` owns the schema, object model, indexes, transactions, ACL checks, and authentication primitives that the protocol/provider layers need. `nwdirectory` exposes those objects through LDAP; `nwnds` exposes them through NDS semantics; `nwbind` exposes them through legacy Bindery calls. A separate setup/provisioning tool should own initial population of this store. The proposed project-facing name is `nwsetup`, matching the `nw*` naming scheme. Its job is not to be another protocol server. It should create or migrate the initial directory database through `libdirectory` directly: ```text nwsetup -> libdirectory -> libflaim ``` Examples of setup-owned work: - create an empty directory store; - initialize the base tree, root/container objects, and default schema; - create initial admin/server/service objects; - create Bindery compatibility objects and properties needed by NetWare 2.x/3.x clients; - import or migrate an existing mars-nwe Bindery database when that becomes practical; - set initial passwords/secrets using the same authentication primitives that `nwbind`, `nwnds`, and `nwdirectory` will use at runtime; - validate or repair indexes before the server starts. `nwsetup` should not fill the database by acting as an LDAP client to `nwdirectory`. LDAP import/export can be useful for interoperability later, but the local bootstrap path should avoid requiring a running LDAP server and should not make LDAP the canonical internal representation. ### NetWare 4.11 schema acquisition and import The future NetWare 4.x directory schema should not be invented from memory or from a small sample file. Real NetWare 4.11 installations appear to carry more complete schema material than the standalone `*.SCH` examples. Some schema fragments, such as `NLS.SCH`, are useful format samples: they are readable ASN.1- style schema definition files containing `ATTRIBUTE` and `OBJECT-CLASS` blocks, `SyntaxID`, `Flags`, `SubClassOf`, `ContainedBy`, `NamedBy`, `MustContain`, `MayContain`, and `ASN1ObjID` fields. However, such files should be treated as partial extension/schema examples, not as the canonical complete 4.11 schema. The initial canonical schema source should come from a real NetWare 4.11 installation. If the complete schema is embedded in installer data such as `install.dat`, the acquisition path should be documented and reproduced rather than guessed: ```text NetWare 4.11 install media / installed server -> extract or export complete NDS schema material -> inspect native schema records and standalone .SCH fragments -> convert/import through nwsetup -> libdirectory schema objects -> libflaim-backed store ``` `nwsetup` should eventually support both a native NetWare 4.11 schema import path and an LDIF import/export path: ```text nwsetup directory import-schema --format=netware411 nwsetup directory import-schema --format=ldif nwsetup directory export-schema --format=ldif > schema.ldif ``` LDIF remains valuable because it is readable, diffable, testable, and useful for interoperability with the LDAP-facing `nwdirectory` service. It should not be the only possible source of truth. Once the native NetWare 4.11 schema format is understood, a native reader can avoid conversion loss and can preserve NetWare-specific syntax IDs, flags, naming rules, containment rules, and class relationships before they are mapped into `libdirectory`. The import implementation should live below `nwsetup`, not inside protocol handlers: ```text nwsetup -> schema import layer -> NetWare 4.11/native schema reader -> .SCH fragment reader -> LDIF reader/writer -> libdirectory schema API -> libflaim ``` The schema import layer must record provenance. Imported schema sets should be versioned with at least the source system, source file/archive, NetWare version, import tool version, and conversion warnings. This makes it possible to compare a later extracted 4.11 schema against the current mars-nwe schema without pretending that hand-written defaults are authoritative. Open questions that should stay explicit until real 4.11 media has been inspected: - exact native schema storage format inside `install.dat` or the installed tree; - whether all core classes/attributes are present as ASN.1-like `.SCH` text, binary records, or both; - mapping of NetWare/NDS syntax IDs to `libdirectory` internal syntaxes and LDAP syntaxes; - preservation of NDS flags such as single-valued, container/effective class, naming, containment, and mandatory/optional attribute sets; - how Bindery-compatibility object classes and attributes are represented in the same schema store. #### Samba schema/parser code as reference material Samba is useful reference material for directory schema handling, but it should not become a code dependency for mars-nwe. The Samba tree contains mature AD schema conversion and prefix-map code, including files such as: ```text source4/dsdb/schema/schema_description.c source4/dsdb/schema/schema_prefixmap.c source4/dsdb/schema/schema_convert_to_ol.c source4/dsdb/schema/schema_syntax.c source4/dsdb/repl/replicated_objects.c source4/setup/ad-schema/* source4/setup/prefixMap.txt ``` The most relevant concepts to study are: - schema element formatting into LDAP/OpenLDAP-style schema descriptions; - class and attribute object modelling; - OID and prefix-map handling; - multi-pass schema loading when some classes or attributes depend on schema elements that have not yet been resolved; - conversion between native directory schema representations and LDIF-like interchange formats; - test data layout for imported schema sets. The Samba implementation is GPL-family code and should be treated as a reference, not copied blindly into mars-nwe. Any future NetWare 4.11 schema importer should use its own tokenizer/parser implementation that is small, license-compatible with mars-nwe's intended distribution, and tailored to NDS `*.SCH`, `install.dat`, and `libdirectory` needs. If a specific Samba algorithm is intentionally reimplemented, the mars-nwe source should mention the conceptual reference and keep the implementation independent. Samba's prefix-map work is especially useful as a warning: OID handling should not be reduced to string comparisons sprinkled across the code. `libdirectory` should have a small central OID/prefix-map module that can map between: ```text textual OID <-> internal syntax/class/attribute identifiers <-> LDAP/LDIF representations <-> any native NetWare 4.11 schema identifiers discovered later ``` That module should be used by the `.SCH` reader, the native 4.11 importer, LDIF import/export, `nwdirectory`, and future `nwnds` code. ### Admin, Supervisor, and directory password bootstrap/recovery The future typed INI must not carry reusable plaintext passwords for Admin, Supervisor, LDAP, NDS, Bindery, or provider IPC credentials. The old legacy pattern of placing a cleartext Supervisor password in the config and restarting the server so the password is reset should be treated as a compatibility behavior for old Bindery-only setups, not as the NetWare 4.x/directory design. Initial password creation belongs to `nwsetup`: ```text first setup: nwsetup asks interactively for the initial Admin/Supervisor password nwsetup calls libdirectory authentication primitives libdirectory stores only password verifiers/hashes in the libflaim-backed store nw.ini records object names, tree/context, and paths, but not the password ``` Planned password changes should also be explicit setup/admin operations, for example conceptually: ```text nwsetup passwd supervisor nwsetup directory set-password cn=admin,o=mars ``` Those commands can authenticate using the current password, local root/admin rights, or a documented recovery mode. They should update the directory store directly through `libdirectory`, not by writing a plaintext key into the INI and waiting for a daemon restart. Emergency recovery must be possible, but it should be one-shot and local. The safe design is an explicit recovery command or recovery token/file, with strict permissions and exclusive store access: ```text nwsetup recovery reset-admin-password requires local root or the configured mars-nwe admin user requires the server to be stopped or the store to be exclusively locked updates password verifier/hash through libdirectory logs the recovery event without logging the new secret consumes or removes any one-shot recovery token/file ``` If a recovery file mechanism is added, it should live outside the main admin INI, be root-owned, mode `0600`, and be deleted or renamed after successful use. The main INI may point to a recovery directory or policy, but it should not contain a standing plaintext reset password. Legacy compatibility can remain explicit and deprecated, for example as a Bindery-only option whose comments warn that it is not used by directory/NDS mode. Migration tools should warn when they find old cleartext reset settings and should convert them into an interactive `nwsetup` password operation rather than copying the secret into the new documented INI. Kerberos should not be part of this initial design. Classic NetWare 4.x/NDS compatibility should focus on native NDS-style authentication and directory semantics. If a later eDirectory/NMAS compatibility effort ever needs Kerberos, it should be considered a separate future authentication-provider topic, not a requirement for the `nwdirectory`/`nwnds`/`nwbind` split. The migration path should be conservative: 1. add the design boundary and naming notes first; 2. import or integrate tinyldap under the project-facing `nwdirectory` name; 3. keep client-facing wolfSSL usage confined to the LDAP/LDAPS/StartTLS network edge initially; internal TCP-based provider IPC may also use wolfSSL later, but only as a separate mTLS configuration; 4. introduce `libdirectory` before making Bindery depend on it; 5. add `nwsetup` as the direct bootstrap/provisioning tool for the initial libflaim-backed directory store; 6. map selected `nwbind` objects/properties to `libdirectory` only after the legacy behavior is documented; 7. add `nwnds` later as an NDS semantic layer, not as an LDAP wrapper; 8. only then consider replacing private Bindery persistence with libflaim-backed directory storage. This keeps the future NetWare 4.x work aligned with the provider/process split: `nwdirectory`, `nwnds`, and `nwbind` may be separate processes or modules, but they should not be separate sources of truth for identity and directory data. ## Third-party and forked component integration policy The long-term redesign should distinguish between three different kinds of imported code: ```text 1. fixed third-party building blocks kept under third_party/ 2. optional third-party backends kept behind mars-nwe facades 3. forked mars-nwe components kept at the repository root ``` Do not treat all imported code the same way. A small, mostly unmodified library that is built as part of mars-nwe belongs under `third_party/`. A component that will be forked, renamed, given CMake support, wired into mars-nwe storage, and changed as part of the server design should live at the repository root like the existing project components (`admin`, `dosutils`, `mail`, `smart`). The intended layout is: ```text third_party/yyjson existing fixed JSON dependency used by salvage metadata third_party/wolfssl fixed TLS dependency used by mars-nwe TLS code configured through CMake for the server's supported platforms and features third_party/libflaim planned fixed directory storage engine used through libdirectory mars-nwe-maintained import/fork of the FLAIM source converted to a real standalone-and-subdirectory CMake build third_party/zlog optional advanced logging backend used only through nwlog mars-tinyldap/ forked/integrated tinyldap-derived component in the repository root upstream/standalone project identity remains tinyldap mars-nwe service/binary name: nwdirectory converted to a real standalone-and-subdirectory CMake build later wired to libdirectory/libflaim instead of tinyldap's original flat files ``` ### TLS dependency rule wolfSSL should be the fixed TLS implementation shipped with the tree, similar in spirit to the bundled yyjson dependency. Do not design the first TLS integration as an abstract zoo of OpenSSL, LibreSSL, wolfSSL, and other providers. wolfSSL is portable and can be configured through CMake for the needs of the target server, so it should be the implementation used for: - LDAP/LDAPS/StartTLS at the `nwdirectory` network edge; - any future TCP-based internal provider IPC, where wolfSSL-backed mTLS is required; - future TLS-capable admin or service endpoints, if they are explicitly added. However, mars-nwe code should still not include wolfSSL headers everywhere. TLS call sites should go through a small internal facade: ```text include/nwtls.h src/nwtls.c src/nwtls_wolfssl.c ``` The facade is not meant to promise equal OpenSSL/LibreSSL support. Its purpose is to keep certificate loading, policy checks, mTLS requirements, error logging, secure defaults, and wolfSSL-specific setup in one place. If another TLS backend is ever evaluated much later, it can be hidden behind the same API, but the planned bundled backend is wolfSSL. Protocol handlers and provider code should therefore use `nwtls` concepts, not raw wolfSSL types. Example ownership: ```text nwdirectory LDAP listener -> nwtls -> wolfSSL provider TCP IPC -> nwtls -> wolfSSL/mTLS nwlog/security logging -> logs TLS events, never private key material ``` TLS configuration belongs in the documented INI, but private key material does not. The config may point at certificate/key files, while `nwsetup` should be able to generate or validate local defaults. Private keys and provider mTLS credentials must live in protected directories with strict permissions. ### zlog dependency rule `zlog` should be treated as an optional advanced logging backend under `third_party/zlog`, not as a direct API used by handlers. The mars-nwe public logging API remains `include/nwlog.h`. If zlog is enabled, it is reached through `src/nwlog_zlog.c` only: ```text endpoint/provider code -> nwlog -> optional zlog backend ``` This keeps secret redaction, structured correlation fields, fallback logging, and future backend changes centralized. zlog can provide administrator-controlled category/rule/format routing, but it must never receive raw decoded NCP or handoff payloads from bypass paths. ### libflaim storage engine rule `libflaim` should live under `third_party/libflaim`, not at the repository root. It is a bundled storage engine dependency for the future directory store, not a mars-nwe service component. The initial import may come from the SourceForge FLAIM code base or from a distro-vetted source package such as the openSUSE `libflaim-4.9.1046` source package, but the exact source must be recorded. The imported tree is expected to need a mars-nwe-maintained fork because the original build system is too hard to integrate directly. Do not merely wrap the old Makefiles from CMake. Replace the build integration with a real CMake build that can work both ways: ```text standalone: cmake -S third_party/libflaim -B build-flaim inside mars-nwe: add_subdirectory(third_party/libflaim) target_link_libraries(directory PRIVATE FLAIM::flaim) ``` The CMake conversion should separate the library from optional tools/tests, expose a normal target such as `FLAIM::flaim`, and support static/shared choices where useful. The original Makefiles may remain as reference material during the import, but they should not be the long-term build path. The inspected FLAIM trunk snapshot (`flaim-code-r1112-trunk`) already contains Autotools files (`configure.ac`, `Makefile.am`, libtool usage, `config.h` generation) and several subprojects (`ftk`, `flaim`, `sql`, `xflaim`). Even so, mars-nwe should still replace that build integration with native CMake. The Autotools setup is useful as source inventory and feature-check documentation, but it does not match the mars-nwe build style and should not be invoked from the main CMake build. The first CMake import should be intentionally small and storage-focused: ```text required first targets: FLAIM::ftk # cross-platform toolkit used by libflaim FLAIM::flaim # classic FLAIM database library used by libdirectory optional later targets: FLAIM::sql # SQL FLAIM, not needed for initial directory store FLAIM::xflaim # XML FLAIM, not needed for initial directory store flaim tools # checkdb/rebuild/view/dbshell/etc., off by default flaim tests # off by default flaim docs # off by default ``` `FLAIM::flaim` depends on the FTK toolkit, so the CMake conversion should model that dependency explicitly instead of relying on Autotools' installed-library search. The initial mars-nwe directory-store work only needs the classic FLAIM library path; SQL FLAIM, XFLAIM, Java/C# bindings, Doxygen, OBS/debian package metadata, NetWare NLM files, and Windows project files should remain imported as reference material but disabled in the mars-nwe build until a real need exists. Autotools feature checks should be translated into CMake checks only when they matter for the required targets. Examples from the inspected tree include pthread support, large-file support, selected POSIX headers/functions, optional ncurses/rt checks for utilities, debug defines such as `FLM_DEBUG`, and platform defines such as `OSX`. OpenSSL support in FTK should not become a new TLS direction for mars-nwe; TLS policy remains wolfSSL through the `nwtls` facade. If any old FLAIM crypto/TLS-related option is required for building, it must be reviewed separately and kept isolated from mars-nwe provider IPC and LDAP TLS policy. If the FLAIM source cannot be built cleanly without old OpenSSL-facing code, the preferred fallback is still not to introduce a general OpenSSL/LibreSSL backend matrix. A narrowly scoped CMake option may use wolfSSL's OpenSSL-compatibility headers only inside `third_party/libflaim`, for example: ```text FLAIM_USE_WOLFSSL_OPENSSL_COMPAT=ON/OFF/AUTO ``` That option would be an import/portability bridge for FLAIM only. It must not change the mars-nwe TLS policy, expose OpenSSL-compatible types outside the FLAIM/FTK build, or make LDAP/provider IPC use OpenSSL APIs. If possible, the classic `FLAIM::ftk` and `FLAIM::flaim` targets should disable old TLS-facing code entirely. If crypto primitives are required, prefer a small private shim in the FLAIM build over leaking wolfSSL/OpenSSL-compat headers into `libdirectory`, `nwbind`, `nwdirectory`, or future `nwnds` code. FLAIM is C++ code. That is acceptable, but its C++ API must not leak into the old mars-nwe C code. `nwbind`, future `nwnds`, `nwdirectory`, and `nwsetup` should use a C ABI through `include/libdirectory.h` or an equivalent internal `libdirectory` API. Only the `libdirectory` implementation should know that the store underneath is libflaim. The final import must include `third_party/libflaim/README.mars-nwe.md` or an equivalent note listing: - import source and version/revision; - distro package and distro patches, if used; - library source license, observed as LGPL-2.1 in the inspected source; - separate helper/tool licenses such as BSD-3-Clause for files like `svn2cl.xsl`; - local CMake and portability changes; - how mars-nwe links it through `libdirectory`. ### Forked tinyldap / nwdirectory rule `tinyldap` is different from yyjson, wolfSSL, zlog, and libflaim. It is not a small library dependency and it is not merely a storage engine. It becomes the LDAP service component used by mars-nwe, so it follows the existing root-level component pattern used by `admin`, `dosutils`, `mail`, and `smart`. The repository name can be `mars-tinyldap/`, but the upstream/standalone project identity should remain `tinyldap`. In other words: when built standalone it is still tinyldap; when built as part of mars-nwe, the project-facing service/binary name is `nwdirectory`. The planned integration requires a fork because the original code needs mars-nwe-specific work: - a real CMake build, not only the original Makefile; - standalone tinyldap build support under the tinyldap name; - mars-nwe subdirectory build support producing the `nwdirectory` service; - LDAP/LDAPS/StartTLS integration through `nwtls`/wolfSSL; - replacement or bypass of the original flat-file storage; - later `libdirectory` and libflaim-backed storage integration; - directory schema, bootstrap, and setup integration through `nwsetup`. The inspected `tinyldap.tar` snapshot is a small C project with one handwritten Makefile. That Makefile builds several static archives and utilities, including ASN.1 helpers, LDAP encode/decode helpers, LDIF parsing, auth helpers, a storage archive, a tiny TLS archive, `tinyldap`, `tinyldap_standalone`, debug/test programs, index tools, ACL tools, an LDAP client, and conversion utilities. It also expects libowfat-style headers/libraries and, when not using the old dietlibc path, links OpenSSL/crypt-style libraries for some helper code. The CMake conversion should inventory those groups explicitly instead of copying the old `all` target wholesale. A reasonable first CMake split is: ```text tinyldap::asn1 ASN.1 scan/format helpers tinyldap::ldap LDAP PDU scan/format/search-filter helpers tinyldap::ldif LDIF parsing and LDAP matching helpers tinyldap::auth password/auth helpers, later routed through libdirectory tinyldap::storage original mmap/flat-file storage, legacy/transition only tinyldap::server original tinyldap server logic, adapted for nwdirectory ``` Optional/off-by-default targets can cover old tools such as `parse`, `addindex`, `dumpidx`, `idx2ldif`, `dumpacls`, `ldapclient`, `ldapdelete`, `asn1dump`, `mysql2ldif`, debug binaries, and tests. mars-nwe should not require these tools for the normal `nwdirectory` build unless a migration/setup workflow explicitly needs them. The original tinyldap TLS code (`tinytls.h`, `tls_*.c`, `fmt_tls_*.c`, and `init_tls_context.c`) should not become the long-term TLS stack for mars-nwe. Keep it as reference material or disable it in the integrated build. The project-facing `nwdirectory` service should use `nwtls` backed by wolfSSL for LDAPS/StartTLS, while the standalone tinyldap build may temporarily keep legacy TLS behavior only if it is isolated behind CMake options and does not leak into mars-nwe's TLS policy. The original storage path is also transitional. Files such as `mstorage.*`, `mduptab.*`, `strstorage.*`, and the `data`/index workflow described by the upstream README are useful for understanding tinyldap's original database model, but the mars-nwe `nwdirectory` target should move toward: ```text nwdirectory -> libdirectory -> libflaim ``` Do not make `nwbind` or future `nwnds` depend on tinyldap's flat-file storage or on the LDAP protocol just because the LDAP service happens to be present. The shared directory API remains the internal boundary. The CMake conversion should therefore support both use cases: ```text standalone upstream-style build: cmake -S mars-tinyldap -B build-tinyldap # produces tinyldap-named targets/tools inside mars-nwe: add_subdirectory(mars-tinyldap) # produces the nwdirectory service/integration target ``` The architectural relationship remains: ```text LDAP/LDAPS client -> nwdirectory service built from mars-tinyldap/tinyldap code -> libdirectory -> libflaim-backed store ``` `nwbind` and future `nwnds` should not speak LDAP internally just because the LDAP service exists. They should use `libdirectory` directly, while `nwdirectory` exposes LDAP as an external protocol view of the same directory data. ### Versioning and local-change documentation Each bundled or forked dependency should have a short mars-nwe note describing: - upstream project and URL; - pinned commit/tag or submodule branch policy; - license; - whether it is fixed, optional, or forked; - which mars-nwe facade owns access to it; - local changes carried by mars-nwe; - CMake options used by the mars-nwe build; - whether the component supports standalone builds, mars-nwe subdirectory builds, or both. Do not silently update third-party submodules together with unrelated functional changes. Dependency bumps should be separate patches. Forked root components should keep a `README.mars-nwe.md` or equivalent local-change note so future updates can be reviewed without guessing which changes are upstream and which are project integration work. Suggested build options: ```text WITH_ZLOG=ON/OFF/AUTO optional advanced logging backend WITH_WOLFSSL=ON/OFF/AUTO bundled TLS backend; required when TLS features are enabled WITH_NWDIRECTORY=ON/OFF build the mars-tinyldap/nwdirectory component WITH_NWNDS=ON/OFF future NetWare 4.x/NDS compatibility layer WITH_LIBFLAIM=ON/OFF/AUTO future directory store backend WITH_TCP=ON/OFF future client-facing TCP/IP transport ``` Conservative builds should still be able to disable incomplete future components, but once a feature requires TLS, its supported TLS implementation is wolfSSL. ## Configuration redesign and future typed INI files The current `nw.ini`/`nwserv.conf` format is historically user-editable, but it is not an INI file in the usual named-section/key-value sense. It is a numbered record format: ```text 1 SYS /var/local/nwe/SYS kt 711 600 2 MARS 3 auto 51 1 52 .recycle .salvage ``` `opt/nw.ini.hook.cmake` is an install-time template for that legacy format. The SMARTHOOK comments make selected numeric sections replaceable by tooling, but the runtime parser still reads numbered records through helpers such as `get_ini_entry()` and `get_ini_int()`. That format should remain supported for compatibility, but it should not be the long-term administrator-facing format for NetWare 4.x, directory, TLS, transport, and provider settings. The future configuration direction should be a real named INI file, backed by a schema. JSON is useful for machine metadata, but it is not a good primary administrator-facing configuration format for mars-nwe. A named INI layout is closer to the existing text configuration style while making new subsystems much clearer: ```ini [server] name = MARS compatibility = netware3 internal_network = auto [volume.SYS] path = /var/local/nwe/SYS flags = kt umask_dir = 0711 umask_file = 0600 [transport.ipx] enabled = true internal_network = auto [transport.tcp] enabled = false listen = 0.0.0.0:524 [bindery] enabled = true backend = legacy [directory] enabled = false store = libflaim path = /var/lib/mars-nwe/directory [ldap] enabled = false listen = 0.0.0.0:389 ldaps_listen = 0.0.0.0:636 [ldap.tls] enabled = false provider = wolfssl cert = /etc/mars-nwe/ldap.crt key = /etc/mars-nwe/ldap.key [nds] enabled = false provider = nwnds [provider-ipc] mode = local local_dir = /run/mars-nwe ``` The exact keys are placeholders, but the grouping is important: - server identity and compatibility mode belong under `[server]`; - volumes become repeatable named sections such as `[volume.SYS]`; - transport settings are separate from NCP provider settings; - Bindery, Directory, LDAP, and NDS configuration are distinct layers; - TLS settings are attached to the protocol edge that uses them; - internal provider IPC settings stay separate from client-facing transport. For NetWare 4.x this is especially important. Do not overload old numeric sections with directory, NDS, LDAP, TLS, libflaim, and provider-IPC options. A numbered line such as `83 ...` gives no useful hint to an administrator and makes future `nwsetup` edits fragile. New 4.x-era features should be designed in the named INI schema first and only mapped to legacy numeric entries where backwards compatibility requires it. The migration path should be: 1. keep reading the legacy numbered config as today; 2. add a typed config model in code (`NwConfig`) whose fields are not tied to either file syntax; 3. load legacy numbered config into that model; 4. load the new named INI format into the same model; 5. make `nwsetup` write the new named INI format atomically; 6. optionally provide import/export from legacy numeric config to named INI; 7. only later consider generating legacy numeric sections from the typed model for older tools. The parser/writer choice should be made with write support in mind. A read-only INI library is enough for daemons, but not enough for `nwsetup` or a future web configuration editor. The writer should be schema-driven and should write atomically, but the generated INI is also administrator documentation. It must not be rewritten into an undocumented minimal key/value dump. The shipped default file should be a documented INI, not only a machine config. Comments should explain the section purpose, defaults, safe values, compatibility notes, and examples. `nwsetup` should therefore prefer one of these strategies: ```text comment-preserving edit: read existing file as text replace only known key value lines preserve surrounding comments, unknown keys, and unknown sections insert missing keys near their documented section when possible or template regeneration: regenerate from a full documented template/schema include the explanatory comments again preserve local overrides through the typed NwConfig model ``` Do not choose a writer that discards all comments unless `nwsetup` regenerates from a complete documented template. Losing the documentation comments would be a regression because the INI is expected to be the primary admin-facing reference for normal deployments. Practical library guidance: - a read-only parser may be acceptable for runtime processes; - `nwsetup` needs read/write/delete or a small project-owned writer; - comment-preserving round trips should not be required for correctness; - generated files should use stable ordering so diffs are readable; - secrets must not be emitted accidentally into world-readable configs; - writes should use temporary files plus rename, with restrictive permissions. Candidate approaches: ```text runtime read path: nw_config_load() -> parse legacy numeric and/or named INI -> NwConfig setup/write path: nwsetup -> NwConfig -> schema-driven canonical INI writer -> atomic rename ``` This keeps the old `nw.ini` compatibility path alive while giving NetWare 4.x, `nwdirectory`, `nwnds`, libflaim-backed storage, wolfSSL TLS, and future transport settings a configuration format that can be understood and edited by a human. ## Transport split for future TCP/IP support Future TCP/IP support should be introduced as a transport code/library split, not as a new daemon. The transport layer is below the NCP dispatcher: it owns wire IO, peer addressing, framing, and transport-specific discovery or watchdog behavior. It does not own Bindery, Queue, Directory, File, Semaphore, or other NCP provider semantics. The intended source-level split is: ```text src/nwtransport.c common transport API and helpers transport-neutral peer/session descriptors dispatch to the selected transport implementation src/nwipx.c existing IPX-specific implementation ipxAddr_t conversion and compatibility helpers IPX socket send/receive SAP/RIP, IPX watchdog, and IPX broadcast behavior where applicable src/nwtcp.c later TCP/IP implementation TCP listener/session/framing code IPv4/IPv6 peer address handling no SAP/RIP assumptions src/nwconn.c NCP session logic request decode, dispatch handoff, reply construction should gradually use transport-neutral peer/session data src/nwserv.c process supervision and connection lifecycle uses the transport layer for listener and peer management ``` These files should be linked into the existing `nwserv`/`nwconn` process model. `nwtransport` is a boundary in the code, not an `nwtransport` process. Creating a separate transport daemon would add an IPC hop for every NCP packet, complicate disconnect/error handling, and make TCP stream ownership harder without adding a clear NetWare service boundary. The long-term direction is to remove raw IPX assumptions from higher layers. Today, the connection path still exposes `ipxAddr_t` in important places. A future cleanup should introduce a transport-neutral peer descriptor, for example conceptually: ```c typedef enum { NW_TRANSPORT_IPX, NW_TRANSPORT_TCP } NwTransportKind; typedef struct { NwTransportKind kind; union { ipxAddr_t ipx; struct { unsigned char addr[16]; unsigned short port; unsigned char family; } tcp; } u; } NwTransportPeer; ``` The exact structure should follow the existing mars-nwe style, but the ownership rule is the important part: NCP providers should not care whether a request came from IPX or TCP/IP. They should see a connection/session and an NCP request, not a raw network address type. The transport API can start small. Useful conceptual operations are: ```c nwtransport_peer_equal(); nwtransport_peer_to_string(); nwtransport_recv(); nwtransport_send(); nwtransport_close_peer(); nwtransport_peer_kind(); ``` As with the NCP context design, these names are placeholders. The first implementation can wrap the existing IPX behavior and leave TCP stubs out until there is a real TCP/IP target. The goal is to stop new code from spreading `ipxAddr_t` into providers that should remain transport-independent. IPX-specific behavior must remain isolated. SAP/RIP, IPX broadcast, and the existing IPX watchdog behavior are compatibility details of the IPX transport or its immediate `nwserv` integration. TCP/IP should not be forced to emulate IPX SAP/RIP internally. If TCP/IP later needs discovery or service advertisement, that should be designed as a TCP/IP-specific mechanism rather than hidden behind old IPX-only assumptions. The intended relationship is therefore: ```text IPX client -> nwipx -> nwtransport -> nwconn -> NCP dispatcher -> providers TCP client -> nwtcp -> nwtransport -> nwconn -> NCP dispatcher -> providers ``` The provider/process rule still applies: ```text Provider boundary does not imply process boundary. Transport boundary does not imply process boundary either. ``` Good future cleanup sequence: 1. document the current IPX ownership in `nwserv.c` and `nwconn.c`; 2. add `nwtransport.c`/transport headers as wrappers around existing IPX paths; 3. move IPX-only helpers into `nwipx.c` without behavior changes; 4. gradually replace raw `ipxAddr_t` use in session-neutral code with a transport-neutral peer/session descriptor; 5. keep NCP providers and the endpoint audit table transport-independent; 6. add `nwtcp.c` only after the IPX wrapper boundary is stable. This keeps TCP/IP support compatible with the broader redesign: transport IO is separated from NCP semantics, but the existing `nwserv`/`nwconn` process model remains intact. ## Logging subsystem and optional zlog backend The dispatch, provider, directory, and transport redesigns all need better logging than scattered ad-hoc debug messages. The goal is not only prettier logs. The important requirements are: - consistent severity levels; - consistent categories across processes and providers; - request correlation from `nwserv`/`nwconn` through provider handoff and back; - safe redaction of secrets before any backend sees the message; - configurable routing to local files, syslog, or later remote collectors; - auditable security events such as password recovery, TLS failures, rejected provider IPC, and directory/bootstrap changes. The mars-nwe source should not call a third-party logging library directly from random endpoint handlers. It should grow a small internal facade first: ```c typedef enum { NWLOG_CORE, NWLOG_CONFIG, NWLOG_TRANSPORT, NWLOG_NCP, NWLOG_HANDOFF, NWLOG_BINDERY, NWLOG_QUEUE, NWLOG_DIRECTORY, NWLOG_NDS, NWLOG_LDAP, NWLOG_AUTH, NWLOG_ACL, NWLOG_RECOVERY, NWLOG_SECURITY } NwLogCategory; ``` Conceptual call sites should be short and category-specific, not manual construction of large event structures in every handler: ```c nwlog_handoff(ctx, NWLOG_INFO, "provider=%s request_id=%u selector=%s handoff=start", provider_name, request_id, selector_path); nwlog_recovery(ctx, NWLOG_WARN, "admin password recovery requested dn=%s uid=%lu", redacted_dn, (unsigned long)uid); nwlog_ncp(ctx, NWLOG_DEBUG, "reply completion=0x%02x len=%u", completion, reply_len); nwlog_directory(ctx, NWLOG_INFO, "schema migration step=%s", step_name); ``` The implementation can still normalize all events through a shared internal structure before they reach a backend: ```c struct nwlog_event { NwLogCategory category; NwLogLevel level; const char *module; const char *file; int line; uint32_t connection_id; uint32_t request_id; uint32_t sequence; uint32_t task_id; const char *ncp_path; const char *provider; const char *message; }; void nwlog_emit(const struct nwlog_event *event); ``` Normal endpoint and provider code should use wrappers such as `nwlog_ncp()`, `nwlog_handoff()`, `nwlog_bindery()`, `nwlog_queue()`, `nwlog_directory()`, `nwlog_nds()`, `nwlog_ldap()`, `nwlog_auth()`, `nwlog_acl()`, `nwlog_recovery()`, and `nwlog_security()`. Those wrappers populate the shared `nwlog_event` fields from the current NCP/provider context and pass the result to `nwlog_emit()`. Only unusual code paths should build `nwlog_event` manually. The project-level layout should follow the existing include/source split: ```text include/nwlog.h public internal logging facade used by mars-nwe modules src/nwlog.c facade implementation, redaction, common formatting src/nwlog_simple.c simple stderr/stdout/file/callback backend src/nwlog_syslog.c optional syslog(3)-style backend derived from simple src/nwlog_zlog.c optional zlog backend, if enabled at build time ``` Protocol handlers, providers, `nwconn`, `nwserv`, `nwbind`, future `nwqueue`, `nwdirectory`, and `nwnds` should include `include/nwlog.h` only. They should not include zlog headers or call zlog macros directly. That facade can initially keep using the existing mars-nwe logging functions, `stderr`, or a small vendored backend. Later it may grow simple and advanced backends behind the same API. There are three useful backend classes: 1. **Simple built-in backend.** A tiny C backend derived from `rxi/log.c` can be imported into the source tree for simple builds. It should be treated as vendored implementation code, renamed and namespaced for mars-nwe. The base upstream code only provides local-style logging primitives such as stderr, file output, and callback hooks; it is not an out-of-the-box syslog, GELF, or remote routing solution. That is fine for the built-in backend, but mars-nwe must adapt it for the project requirements: redaction hooks, category mapping, context/correlation fields, optional file output, and journald-friendly stderr/stdout formatting should live behind `nwlog`, not in endpoint code. This backend is the right default for systemd deployments: services can log to stderr/stdout and let journald capture, timestamp, rotate, and forward logs according to the unit configuration. mars-nwe should still emit structured, grep-friendly lines so `journalctl -u ...` remains useful without requiring a heavier routing backend. The project layout should make the imported code clearly internal, for example: ```text include/nwlog_simple.h internal/simple backend declarations, if needed src/nwlog_simple.c imported/adapted rxi/log.c-style backend ``` Imported symbols such as `log_info`, `log_error`, or `log_add_fp` must be renamed, hidden, or made `static` so they do not leak into the global mars-nwe namespace. Public entry points should look like `nwlog_simple_init()` and `nwlog_simple_emit()`. 2. **Syslog bridge backend.** If classic syslog integration is wanted, do not overload the simple backend with syslog-specific behavior. Instead clone the adapted simple backend structure into a separate implementation: ```text include/nwlog_syslog.h internal syslog backend declarations, if needed src/nwlog_syslog.c syslog(3)-style backend derived from simple ``` `nwlog_syslog.c` may reuse the same level/category mapping, redaction hooks, event formatting helpers, and callback shape as `nwlog_simple.c`, but its output path should be explicit: `openlog()`, `syslog()`, and `closelog()` or the platform equivalent. This keeps `backend = simple` predictable for stderr/stdout/file/journald use, while `backend = syslog` clearly means the traditional syslog path. Provider and endpoint code must not care which of the two is active. 3. **Advanced routing backend.** `zlog` remains the preferred candidate for administrator-controlled routing because it is a C logging library with category, format, and rule based configuration. That model fits mars-nwe well: code can emit category-specific events such as `ncp`, `handoff`, `queue`, `directory`, `auth`, or `transport`, while the administrator decides in the logging configuration whether those categories go to a file, stdout/stderr, syslog-style output, a pipe, or an external log-forwarder path. The zlog project documentation describes these three core concepts as categories, formats, and rules, where rules bind a category/level to an output and format. Before choosing any backend, packaging, license compatibility, portability, and maintenance state still need to be verified for the target distributions. The architecture must not depend on one backend being available. The preferred dependency shape is therefore: ```text mars-nwe code -> nwlog facade -> simple backend: src/nwlog_simple.c from imported/adapted rxi/log.c-style code -> stderr/stdout for systemd/journald, optional local file/callback -> syslog backend: src/nwlog_syslog.c derived from the simple backend shape -> classic syslog(3) output when explicitly configured -> optional advanced backend: src/nwlog_zlog.c using zlog -> admin-configured zlog rules/formats/outputs ``` Do not make endpoint code depend on `zlog_category_t` or zlog macros directly. Keeping `nwlog` in the middle gives mars-nwe one place to: - inject correlation fields such as `connection_id`, `request_id`, `sequence`, `task_id`, provider name, and NCP selector path; - redact or suppress sensitive fields before formatting; - enforce no-secret logging rules even when logs are routed to remote systems; - keep a fallback backend for minimal builds or platforms without zlog; - change or add backends later without touching protocol handlers. Remote logging is useful, but it must be treated as a security boundary. A GELF or Graylog-style collector, syslog relay, pipe, or any other remote forwarding path must receive structured, redacted events only. It must never receive raw NCP request bodies, decoded handoff payloads, passwords, one-shot recovery tokens, private keys, or raw directory authentication material. A future documented INI could expose the logging policy without forcing admins to edit C-style backend internals directly: ```ini [logging] backend = simple ; simple, syslog, zlog level = info redact_secrets = yes config = /etc/mars-nwe/zlog.conf [logging.category] ncp = info handoff = info auth = warn recovery = warn directory = info transport = info [logging.debug] packet_hexdump = no handoff_hexdump = no unsafe_raw_payloads = no ``` Raw packet or handoff hexdumps should be opt-in developer diagnostics, not normal admin logging. Even then, auth/password fields should be redacted where the layout is known. The safe default is length-only logging for sensitive payloads. Important audit events should be logged even at normal levels: - provider IPC connection accepted/rejected; - provider IPC TLS/mTLS validation failure; - directory store initialization and schema migration; - `nwsetup` password bootstrap or recovery actions; - bindery-to-directory migration actions; - failed authentication attempts with redacted identities; - NCP handoff timeout, dead provider, or mismatched reply correlation ID. The logging cleanup should be a separate functional change from endpoint layout patches. Documentation-only endpoint audit patches may add log design notes, but they should not introduce new logging dependencies or change runtime logging behavior. ## Logging connection The dispatch redesign also supports the desired log cleanup. If every request has a context, logs can consistently include: ```text INFO NCP 23/109 DISPATCH type=0x2222 fn=0x17 sub=0x6d provider=nwbind/queue INFO NCP 32/0 REPLY type=0x2222 fn=0x20 sub=0x00 result=0x00 len=4 WARN NCP 23/130 LAYOUT-MISMATCH sdk="32-bit JobNumber" code="16-bit parser" ``` Until the `nwlog` facade exists, endpoint-dispatch cleanup should still reuse existing mars-nwe logging functions. Do not add direct zlog calls or a parallel logging path just to support one endpoint family. ## Migration plan ### Phase 1: Name the existing conventions Low risk. No behavior change. - Add named constants or comments for the current `0`, `-1`, and `-2` dispatcher results. - Keep existing control flow unchanged. - Update comments so `return(-1)` is never described ambiguously outside the exact dispatcher where it is meaningful. ### Phase 2: Add an endpoint audit table Low risk. Mostly documentation/debug. - Add a table of known endpoints by request type, function, and subfunction. - Mark provider, generation bucket, and implementation state. - Use it to compare SDK/PDF/WebSDK coverage against actual handlers. - Do not switch runtime dispatch to the table yet. ### Phase 3: Introduce a thin `NcpContext` Moderate risk if kept small. - Wrap existing request and reply buffers without changing ownership. - Use the context only in newly audited or newly implemented handlers. - Keep old handlers callable until they are touched for another reason. ### Phase 4: Convert small endpoint families first Moderate risk, easy to test. Good candidates: - `0x2222/32` old Semaphore calls; - direct calls such as End Of Job, Logout, and Negotiate Buffer Size; - small message/station groups once their handoff has been audited. Avoid converting queue and bindery first because they have more process coupling and more old/new layout variants. ### Phase 5: Move runtime dispatch to tables gradually Higher risk. Do this only after enough endpoint families have stable audit coverage and tests. - Keep switch wrappers during the transition. - Convert one family at a time. - Preserve exact completion codes and reply lengths. - Add targeted smoke tests for any family whose dispatch path changes. ## Non-goals This redesign should not: - change protocol behavior merely to match a cleaner abstraction; - remove NetWare 1.x/2.x/3.x compatibility paths; - enable NetWare 4.x/OES/MOAB-only endpoints by default; - replace existing mars-nwe path, bindery, queue, AFP, trustee, or salvage backends with parallel databases; - add a large external message bus dependency; - rewrite all handlers in one patch; - turn documentation-only endpoint audit patches into functional refactors. ## Practical rule for future patches For the ongoing endpoint documentation pass, keep doing the conservative thing: 1. enumerate SDK/PDF/WebSDK/include endpoints for the family; 2. compare them with actual `case` labels and forwarded destination handlers; 3. document missing, disabled, implemented, and later-generation slots; 4. document request parser/handoff and response builder; 5. record real layout differences, but do not change behavior in the same patch. Functional cleanup should come later in small patches with tests.