Files
mars-nwe/REDESIGN.md
2026-06-02 08:35:03 +02:00

687 lines
26 KiB
Markdown

# 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 */
int has_subfunction;
uint8_t subfunction; /* grouped calls such as 23/x, 32/x, 87/x */
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` and `subfunction` identify the endpoint;
- `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.
## 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 has_subfunction;
uint8_t subfunction;
const char *name;
const char *provider;
uint32_t flags;
} NcpEndpointDoc;
```
Example entries:
```c
{ 0x2222, 23, 1, 109, "Change Queue Job Entry old", "nwbind/queue", NCPDOC_FORWARDED },
{ 0x2222, 32, 1, 0, "Open Semaphore old", "sema", NCPDOC_LOCAL },
{ 0x2222, 33, 0, 0, "Negotiate Buffer Size", "nwconn", NCPDOC_LOCAL },
```
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.
## 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.
## 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
directory core/store
mars-nwe object model, schema, indexes, ACL/auth primitives
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`.
`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 a
shared directory-core API or a clearly defined IPC protocol to 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. The directory core 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.
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 wolfSSL confined to the LDAP/LDAPS/StartTLS network edge;
4. introduce a directory-core abstraction before making Bindery depend on it;
5. map selected `nwbind` objects/properties to the directory core only after the
legacy behavior is documented;
6. add `nwnds` later as an NDS semantic layer, not as an LDAP wrapper;
7. 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.
## 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"
```
The logging cleanup should still reuse existing mars-nwe logging functions. Do
not add a second logging subsystem just to support the dispatch cleanup.
## 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.