Userland DDP implementation options
The following is an edited analyzis by GPT-5.5 after ingesting Netatalk code, docs, and the TailTalk DDP implementation.
Netatalk implements the AppleTalk protocols above the Datagram Delivery Protocol (DDP), but currently expects the host operating system kernel to provide DDP sockets, interface addressing, and packet routing.
Kernel AppleTalk support is no longer broadly available, so this issue evaluates three approaches for adding a userland DDP implementation.
Netatalk already contains a historical BSD kernel implementation under sys/netatalk/. It includes DDP socket control blocks, input and output processing, routing, EtherTalk Phase I and II handling, AARP, and interface address management. The Rust implementation in TailTalk provides a second, working userland proof of concept.
Current integration constraints
Netatalk does not currently have a complete DDP abstraction. netddp_open() creates a native AF_APPLETALK datagram socket, while netddp_sendto(), netddp_recvfrom(), and netddp_close() are thin wrappers around normal socket operations.
The rest of Netatalk consequently expects:
- synchronous
sendto() and recvfrom() semantics;
- a real descriptor usable with
select() and poll();
- dynamic and fixed DDP socket binding;
- the DDP protocol type as the first byte of the socket payload;
- kernel-compatible broadcast and wildcard address behavior;
- per-interface AppleTalk addresses, including late-bound addressing;
- routes learned by
atalkd to affect DDP forwarding.
In addition, atalkd directly creates AppleTalk sockets and uses platform-specific interface, multicast, and routing operations. Replacing only netddp_open() is therefore insufficient.
Existing in-tree DDP implementation
The code under sys/netatalk/ already defines much of the behavior that a userland implementation must preserve:
ddp_usrreq.c implements socket attachment, binding, dynamic port allocation, connection state, address validation, and local socket demultiplexing.
ddp_input.c implements short and extended DDP input, datagram length checks, checksum verification, local delivery, forwarding, and hop-count handling.
ddp_output.c implements extended DDP header generation, checksums, route selection, and Phase I encapsulation.
aarp.c implements AARP probing, resolution, responses, caching, and Phase I/II link behavior.
at_control.c implements interface addresses, network ranges, address probing, and directly connected routes.
This is kernel code rather than a dormant userland library. It depends on historical BSD facilities including mbuf, ifnet, socketvar, kernel route structures, interrupt priority functions, kernel timers, and sleep/wakeup operations. The Meson build currently installs its headers but does not compile these C sources.
Consequently, the implementation cannot simply be enabled in a userland build. Its protocol logic and data model can, however, be adapted behind modern userland interfaces. It should be treated as the primary reference for Netatalk-compatible semantics rather than designing those semantics from scratch.
Option 1: C wrapper around TailTalk
TailTalk demonstrates working userland EtherTalk, AARP, long and short DDP, and LocalTalk support. Reusing it could provide the shortest path to an initial prototype and an independent implementation against which the in-tree kernel logic can be tested.
However, its current interface differs substantially from Netatalk's expectations:
- TailTalk uses a Tokio actor API rather than synchronous socket operations.
- A TailTalk DDP socket is associated with a protocol type, while Netatalk includes the type byte in every datagram.
- TailTalk owns packet capture, address selection, and link-layer state inside the stack.
- Its socket registry is local to one stack instance.
- It does not implement an RTMP-driven forwarding table.
- It currently treats network zero as LocalTalk rather than implementing the complete DDP wildcard and directly-connected-network semantics expected by Netatalk.
- Its DDP protocol type is restricted to the known AppleTalk types rather than accepting an arbitrary 8-bit DDP type.
- Additional validation and failure handling would be needed for production use, including received checksum verification, strict length validation, queue overflow behavior, and removal of panic paths.
An in-process wrapper would create independent DDP stacks in afpd, papd, utilities, and other processes. Those instances would compete for packet capture, DDP ports, and AppleTalk addresses. A viable TailTalk integration would therefore need a single Rust daemon with a C IPC client. At that point, the design is effectively the separate-daemon approach described below rather than a simple language wrapper.
There is also a licensing issue to resolve. Netatalk is distributed under GPLv2, while TailTalk is GPLv3. Directly linking TailTalk into Netatalk, or translating its implementation into the Netatalk source tree, would require appropriate relicensing or another explicit licensing resolution.
Assessment
This is useful as a rapid prototype, an interoperability target, and a source of packet test vectors. It is not a good direct production integration in its current form. The in-tree kernel implementation is a better source for Netatalk's socket, routing, and interface semantics.
Option 2: Userland DDP implementation as a separate daemon and library
This approach would adapt the in-tree DDP and AARP logic into a portable userland core, then introduce a dedicated daemon, provisionally called ddpd, as the single owner of physical AppleTalk interfaces, AARP state, DDP sockets, and packet forwarding.
afpd / papd / tools / atalkd
|
libatalk DDP client
| Unix-domain IPC
ddpd
|
pcap / AF_PACKET / BPF / DLPI
The client side in libatalk would expose the existing netddp_* operations. Each virtual DDP socket would use a real Unix-domain descriptor so existing select(), poll(), and atp_fileno() users remain functional. The send and receive wrappers would carry the AppleTalk source or destination address alongside each payload.
Advantages
- A clear process and privilege boundary.
- One owner for link access, AARP caches, addresses, and DDP port allocation.
- A protocol core that can be tested independently from the rest of Netatalk.
- Existing in-tree socket, routing, forwarding, and AARP behavior can guide the implementation.
- Potential reuse by applications outside Netatalk.
- A daemon crash does not directly corrupt
atalkd's in-process routing structures.
Disadvantages
atalkd currently learns routes through RTMP and installs them into the kernel. Once DDP forwarding moves to userland, ddpd must receive those route changes. A separate daemon therefore needs a control protocol covering at least:
- provisional and final interface addresses;
- EtherTalk phase and network ranges;
- route addition, deletion, replacement, and expiry;
- gateway reachability;
- zone multicast membership;
- interface state changes;
- routing enabled or disabled per interface.
This divides a single logical AppleTalk network state between two daemons and introduces ordering, reconnection, resynchronization, and failure-recovery requirements.
Assessment
This is the strongest choice when process isolation, independent DDP availability, or reuse outside Netatalk is a primary goal. The in-tree implementation substantially reduces protocol design uncertainty, but it does not remove the need to synchronize the routing and forwarding state held by atalkd and ddpd.
Option 3: Userland adaptation of the in-tree DDP implementation as part of atalkd
atalkd already owns the AppleTalk control plane:
- interface discovery and configuration;
- provisional and final address selection;
- RTMP route learning and expiry;
- ZIP network and zone information;
- NBP forwarding;
- routing policy between interfaces.
Putting the userland DDP data plane under the same ownership avoids synchronizing routes and interface state between two daemons. Existing service definitions also start atalkd before PAP, AFP over AppleTalk, MacIP, and related services.
The implementation should not be copied directly into atalkd's existing main.c, nor should the old kernel environment be emulated wholesale. The useful protocol and state-machine logic from sys/netatalk/ should be adapted into a modular internal library with explicit components for:
- DDP parsing, serialization, length validation, checksums, and hop handling;
- EtherTalk Phase I and Phase II framing;
- AARP probing, resolution, cache expiry, and conflict detection;
- DDP socket binding and datagram demultiplexing;
- interface and address state;
- route lookup and packet forwarding;
- platform-specific link backends;
- broker IPC for other Netatalk processes.
The following kernel concepts have straightforward userland replacements:
mbuf chains become bounded packet buffers or buffer views.
- DDP protocol control blocks become broker-managed virtual socket objects.
- socket receive buffers and
sorwakeup() become bounded per-client queues backed by pollable Unix-domain sockets.
- kernel route objects become lookups into the route table already maintained by
atalkd.
ifnet output callbacks become explicit link-backend operations.
- kernel timers and sleep/wakeup operations become event-loop timers and state transitions.
This preserves the established DDP behavior without retaining dependencies on historical kernel internals.
Other daemons and command-line tools would still need virtual DDP sockets. atalkd would therefore expose a Unix-domain socket broker, and libatalk would provide the client backend. A connection representing a DDP socket would remain a pollable descriptor, preserving the existing event-loop model.
The current native kernel backend should remain available. A backend abstraction would allow Netatalk to use either native AF_APPLETALK sockets or the userland broker without changing ATP, ASP, NBP, PAP, and AFP code unnecessarily.
Advantages
- Routing and DDP forwarding share one authoritative route table.
- No new synchronization protocol between routing and forwarding daemons.
- Fits the existing service lifecycle and dependency model.
- Reuses the design and behavior of the existing Netatalk DDP, AARP, and interface code.
- Allows the old upper-layer protocol implementations to remain largely unchanged.
- Can preserve kernel DDP as an alternative backend.
Disadvantages
- Increases
atalkd's responsibility and failure impact.
- Requires careful modularization to avoid adding more complexity to legacy daemon code.
- Requires an IPC broker even though the protocol engine is local to
atalkd.
- Link access and routing currently require elevated privileges, so privilege separation should be considered during the design.
Assessment
This provides the best ownership model for Netatalk because atalkd already maintains the state required to route DDP correctly. The existing sys/netatalk/ implementation lowers protocol design risk and provides concrete compatibility behavior. The legacy-code risk can be controlled by adapting its logic into a separate internal module with narrow interfaces rather than embedding kernel compatibility code in the existing event loop and routing source files.
Conclusion
The recommended direction is a C userland adaptation of the DDP, AARP, socket, and interface logic already present under sys/netatalk/. It should be implemented as a modular subsystem owned by atalkd, together with a Unix-domain socket broker and a libatalk client backend.
The in-tree implementation should be the primary source for Netatalk-compatible binding, wildcard address, routing, forwarding, checksum, and AARP behavior. It must be adapted rather than compiled unchanged because its execution and memory model is tied to historical BSD kernels.
TailTalk should be used as a working userland proof of concept, an independent interoperability target, and a source of packet-level test vectors. Direct integration should be limited to prototyping unless its API, routing model, production hardening, and licensing are addressed.
A standalone ddpd remains a credible alternative if independent reuse or process isolation is considered more important than keeping the RTMP control plane and DDP forwarding plane together.
Suggested implementation order:
- Document the observable socket, routing, forwarding, interface, and AARP behavior implemented by
sys/netatalk/.
- Define a backend-neutral DDP socket contract while retaining the native kernel backend.
- Add regression tests for malformed packets, checksums, broadcasts, wildcard addresses, dynamic ports, route selection, and descriptor readiness.
- Adapt DDP packet processing, framing, checksums, and AARP into a userland core with offline packet tests.
- Replace kernel protocol control blocks and receive buffers with virtual socket objects and bounded queues.
- Add the Unix-domain broker and
libatalk client backend.
- Support a single EtherTalk Phase II interface without routing.
- Redirect
atalkd interface and route operations to the userland core.
- Add multiple interfaces, forwarding, RTMP route expiry, Phase I, and zone multicast behavior.
- Add privilege separation, resource limits, restart recovery, fuzzing, and interoperability tests.
- Differential-test against TailTalk and known-working Linux or NetBSD kernel implementations.
Userland DDP implementation options
The following is an edited analyzis by GPT-5.5 after ingesting Netatalk code, docs, and the TailTalk DDP implementation.
Netatalk implements the AppleTalk protocols above the Datagram Delivery Protocol (DDP), but currently expects the host operating system kernel to provide DDP sockets, interface addressing, and packet routing.
Kernel AppleTalk support is no longer broadly available, so this issue evaluates three approaches for adding a userland DDP implementation.
Netatalk already contains a historical BSD kernel implementation under
sys/netatalk/. It includes DDP socket control blocks, input and output processing, routing, EtherTalk Phase I and II handling, AARP, and interface address management. The Rust implementation in TailTalk provides a second, working userland proof of concept.Current integration constraints
Netatalk does not currently have a complete DDP abstraction.
netddp_open()creates a nativeAF_APPLETALKdatagram socket, whilenetddp_sendto(),netddp_recvfrom(), andnetddp_close()are thin wrappers around normal socket operations.The rest of Netatalk consequently expects:
sendto()andrecvfrom()semantics;select()andpoll();atalkdto affect DDP forwarding.In addition,
atalkddirectly creates AppleTalk sockets and uses platform-specific interface, multicast, and routing operations. Replacing onlynetddp_open()is therefore insufficient.Existing in-tree DDP implementation
The code under
sys/netatalk/already defines much of the behavior that a userland implementation must preserve:ddp_usrreq.cimplements socket attachment, binding, dynamic port allocation, connection state, address validation, and local socket demultiplexing.ddp_input.cimplements short and extended DDP input, datagram length checks, checksum verification, local delivery, forwarding, and hop-count handling.ddp_output.cimplements extended DDP header generation, checksums, route selection, and Phase I encapsulation.aarp.cimplements AARP probing, resolution, responses, caching, and Phase I/II link behavior.at_control.cimplements interface addresses, network ranges, address probing, and directly connected routes.This is kernel code rather than a dormant userland library. It depends on historical BSD facilities including
mbuf,ifnet,socketvar, kernel route structures, interrupt priority functions, kernel timers, and sleep/wakeup operations. The Meson build currently installs its headers but does not compile these C sources.Consequently, the implementation cannot simply be enabled in a userland build. Its protocol logic and data model can, however, be adapted behind modern userland interfaces. It should be treated as the primary reference for Netatalk-compatible semantics rather than designing those semantics from scratch.
Option 1: C wrapper around TailTalk
TailTalk demonstrates working userland EtherTalk, AARP, long and short DDP, and LocalTalk support. Reusing it could provide the shortest path to an initial prototype and an independent implementation against which the in-tree kernel logic can be tested.
However, its current interface differs substantially from Netatalk's expectations:
An in-process wrapper would create independent DDP stacks in
afpd,papd, utilities, and other processes. Those instances would compete for packet capture, DDP ports, and AppleTalk addresses. A viable TailTalk integration would therefore need a single Rust daemon with a C IPC client. At that point, the design is effectively the separate-daemon approach described below rather than a simple language wrapper.There is also a licensing issue to resolve. Netatalk is distributed under GPLv2, while TailTalk is GPLv3. Directly linking TailTalk into Netatalk, or translating its implementation into the Netatalk source tree, would require appropriate relicensing or another explicit licensing resolution.
Assessment
This is useful as a rapid prototype, an interoperability target, and a source of packet test vectors. It is not a good direct production integration in its current form. The in-tree kernel implementation is a better source for Netatalk's socket, routing, and interface semantics.
Option 2: Userland DDP implementation as a separate daemon and library
This approach would adapt the in-tree DDP and AARP logic into a portable userland core, then introduce a dedicated daemon, provisionally called
ddpd, as the single owner of physical AppleTalk interfaces, AARP state, DDP sockets, and packet forwarding.The client side in
libatalkwould expose the existingnetddp_*operations. Each virtual DDP socket would use a real Unix-domain descriptor so existingselect(),poll(), andatp_fileno()users remain functional. The send and receive wrappers would carry the AppleTalk source or destination address alongside each payload.Advantages
atalkd's in-process routing structures.Disadvantages
atalkdcurrently learns routes through RTMP and installs them into the kernel. Once DDP forwarding moves to userland,ddpdmust receive those route changes. A separate daemon therefore needs a control protocol covering at least:This divides a single logical AppleTalk network state between two daemons and introduces ordering, reconnection, resynchronization, and failure-recovery requirements.
Assessment
This is the strongest choice when process isolation, independent DDP availability, or reuse outside Netatalk is a primary goal. The in-tree implementation substantially reduces protocol design uncertainty, but it does not remove the need to synchronize the routing and forwarding state held by
atalkdandddpd.Option 3: Userland adaptation of the in-tree DDP implementation as part of
atalkdatalkdalready owns the AppleTalk control plane:Putting the userland DDP data plane under the same ownership avoids synchronizing routes and interface state between two daemons. Existing service definitions also start
atalkdbefore PAP, AFP over AppleTalk, MacIP, and related services.The implementation should not be copied directly into
atalkd's existingmain.c, nor should the old kernel environment be emulated wholesale. The useful protocol and state-machine logic fromsys/netatalk/should be adapted into a modular internal library with explicit components for:The following kernel concepts have straightforward userland replacements:
mbufchains become bounded packet buffers or buffer views.sorwakeup()become bounded per-client queues backed by pollable Unix-domain sockets.atalkd.ifnetoutput callbacks become explicit link-backend operations.This preserves the established DDP behavior without retaining dependencies on historical kernel internals.
Other daemons and command-line tools would still need virtual DDP sockets.
atalkdwould therefore expose a Unix-domain socket broker, andlibatalkwould provide the client backend. A connection representing a DDP socket would remain a pollable descriptor, preserving the existing event-loop model.The current native kernel backend should remain available. A backend abstraction would allow Netatalk to use either native
AF_APPLETALKsockets or the userland broker without changing ATP, ASP, NBP, PAP, and AFP code unnecessarily.Advantages
Disadvantages
atalkd's responsibility and failure impact.atalkd.Assessment
This provides the best ownership model for Netatalk because
atalkdalready maintains the state required to route DDP correctly. The existingsys/netatalk/implementation lowers protocol design risk and provides concrete compatibility behavior. The legacy-code risk can be controlled by adapting its logic into a separate internal module with narrow interfaces rather than embedding kernel compatibility code in the existing event loop and routing source files.Conclusion
The recommended direction is a C userland adaptation of the DDP, AARP, socket, and interface logic already present under
sys/netatalk/. It should be implemented as a modular subsystem owned byatalkd, together with a Unix-domain socket broker and alibatalkclient backend.The in-tree implementation should be the primary source for Netatalk-compatible binding, wildcard address, routing, forwarding, checksum, and AARP behavior. It must be adapted rather than compiled unchanged because its execution and memory model is tied to historical BSD kernels.
TailTalk should be used as a working userland proof of concept, an independent interoperability target, and a source of packet-level test vectors. Direct integration should be limited to prototyping unless its API, routing model, production hardening, and licensing are addressed.
A standalone
ddpdremains a credible alternative if independent reuse or process isolation is considered more important than keeping the RTMP control plane and DDP forwarding plane together.Suggested implementation order:
sys/netatalk/.libatalkclient backend.atalkdinterface and route operations to the userland core.