• Updated 2023-07-12: Hello, Guest! Welcome back, and be sure to check out this follow-up post about our outage a week or so ago.

Towards an inetd for AppleTalk


PIC Whisperer
I've been working on this for a bit, ever since reaching an inflection point with TashRouter, and I'd like to throw a rough design out there for comments...

So, the basic idea here is to realize the dream of a userspace AppleTalk stack in an incremental and modular way (that way I don't necessarily have to write all of it myself). The design takes a measure of inspiration from inetd, but necessarily deviates from it in some important ways. Here's what I'm thinking:

Layer A

The outermost component is the root of the AppleTalk stack, a daemon that connects to a LocalTalk network via LToUDP, representing a single node on the AppleTalk network. I chose LToUDP for its simplicity and flexibility - it can be used on its own with Mini vMac 37 or connected to other networks using MultiTalk, tashtalkd, or TashRouter.

This component will contain the essential functionality of an AppleTalk node, plus maybe a bit more. It will:
  • probe for and choose a LocalTalk node address
  • listen for a router to announce its presence and use it to reach other networks
  • respond to AppleTalk Echo Protocol (AEP) requests
  • respond to Name Binding Protocol (NBP) LkUp requests for network-visible entities it contains
  • make and listen for responses to AppleTalk Transaction Protocol (ATP) requests
  • spawn subprocesses in response to datagrams coming in on configured sockets
The user will define a set of network services, each with a listening socket and corresponding network-visible entity, made visible by the built-in NBP service. On receiving a datagram bound for the listening socket, it will spawn a subprocess to handle traffic to that socket and feed it datagrams over its standard input - in this way, it is somewhat like inetd. Unlike inetd, however, the same subprocess will continue to be fed datagrams bound for the listening socket until it decides to terminate.

This component will communicate with its subprocesses via a JSONL-based protocol, allowing the passage of datagrams in either direction, as well as other functions such as registering other listening sockets and sending ATP requests.

Layer B and Layer C

The next components in are the subprocesses spawned by layer A. These will handle the next protocols up the AppleTalk stack, namely Printer Access Protocol (PAP), AppleTalk Session Protocol (ASP), and AppleTalk Data Stream Protocol (ADSP). These components will themselves spawn layer C subprocesses to handle traffic at a higher level than provided by layer A:
  • PAP will spawn a subprocess for each print job sent to it, sending and receiving data streams directly over standard in/out. This way, it is possible to connect it directly to lpr to send jobs to a printer connected to the machine running the stack, or alternatively, to an emulator for a printer protocol such as that of the ImageWriter II.
  • ASP will spawn a subprocess for each session created under it, communicating using a JSONL-based protocol over standard in/out. This allows, for example, an implementation of AppleTalk Filing Protocol (AFP) to be created that need not handle anything below the ASP layer.
  • ADSP will spawn a subprocess for each stream opened under it, communicating using either a JSONL-based protocol or direct streams of data over standard in/out (depending on whether the subprocess needs to use ADSP features such as the end-of-message marker). This allows, for example, an implementation of the PPCToolBox service to be created that need not handle anything below the ADSP layer.
  • Layer A is started, configured with a PAP service advertised as Screechy:ImageWriter.
  • A user opens the Chooser and looks for AppleTalk ImageWriters.
  • Layer A answers the NBP LkUp, advertising Screechy on socket 253 of its network/node address.
  • The user selects Screechy as their default printer.
  • The user sends a print job to Screechy, sending a PAP OpenConn datagram to socket 253.
  • Layer A receives the packet, spawns a Layer B PAP subprocess, and delivers the datagram over JSONL.
  • The Layer B PAP subprocess sends a PAP SendData datagram to the user's workstation, requesting print data, by sending the datagram to Layer A, and spawns a Layer C lpr subprocess.
  • The user's workstation responds with print data which Layer A relays to the Layer B PAP subprocess, which in turn de-packetizes the data and sends it to the Layer C lpr subprocess.
  • The cycle continues, and eventually the print job is completed. The user's workstation sends a PAP CloseConn datagram to Layer A, which relays it to the Layer B PAP subprocess, which closes the streams to the Layer C lpr subprocess, causing it to terminate. The Layer B PAP subprocess notices that it has no more open print jobs and terminates.

Clear as mud? =) I'd be very interested to hear anyone's questions, critiques, thoughts, comments, &c.

(Also, if anyone has any information on the Apple Events/PPCToolBox protocol, I'd be extremely interested to hear about that...)


PIC Whisperer
Took @cheesestraws 's suggestion and replaced JSONL with a simple little TLV protocol. Besides being more efficient, this deals with some cross-platform finickiness when reading standard input, since the protocol carries the length of its payload in its header. Maybe I'm silly, but I want this to be able to run on Windows as well as *nix.

Anyway, the "business logic" of layer A is complete. Next step is to implement a layer B for PAP and do some integration testing... then on to ASP and maybe ADSP.

On which subject, paging @slipperygrey - I think I read somewhere that Netatalk has an ADSP implementation and I'm curious, does anything use it? As far as I'm aware, PPCToolbox is the only thing that uses ADSP and I don't think Netatalk has any support for that, but there is much of which I'm ignorant, so.

It's a bit frustrating that these mid-level protocols aren't more reusable. ASP seems to just be a bridge to AFP, ADSP to PPCToolbox... oh well, maybe there are rarer protocols out there that make use of these that other people can write handlers for. I know that the various Munchers games have some kind of network function that allows a teacher to set configurations for a whole class and host shared high score lists, I wonder what that uses.


Well-known member
maybe there are rarer protocols out there that make use of these that other people can write handlers for

There are APIs for these on the Mac side, IIRC, anyway. So perhaps this will get people interested in writing more protocols :). Hard to do it when the only thing to put on the other end of it is netatalk...


Well-known member
PAP is used for PostScript transmission, but shared StyleWriters (and possibly also AppleTalk ImageWriters?) encapsulate their UART communication through ADSP. Like TCP, ADSP appears not as packet-oriented data transmission, but as full-duplex byte stream and is therefore ideal for this.

The ADSP Tool is something like Telnet. You can download it from the Apple Support Area.


Well-known member
Yes, it‘s an extension for the Communication Toolbox. Apps which support it, can use this. Try ClarisWorks.


PIC Whisperer
Ah, okay, thanks. ClarisWorks 4 worked for me (oddly, though, it doesn't recognize the TelePort ADB modem, but that's a separate project...).


Well-known member
Here an example of ADSP being used by printer sharing. As I cannot attach a real printer to Mini vMac, I just tried to exchange some other information:

Too bad Wireshark doesn't really dissect ADSP packets or lets you view its data stream like you can do with TCP...

Bildschirmfoto 2024-02-07 um 20.18.43.pngBildschirmfoto 2024-02-07 um 20.18.22.png


PIC Whisperer
Neat. Looking at the ADSP section of Inside AppleTalk, it looks like quite a task to implement, but at least now it feels a bit more worth it and a bit more accessible...


Well-known member
It is quite a bit of work.. I started implementing it in July and it's still just around 200 lines of C code because I interrupted woking on it. Good luck!


Well-known member
@tashtari I don’t think ADSP was ever implemented in netatalk. The docs suggest that it was once on the roadmap:

The AppleTalk protocol family is a collection of protocols layered above the Datagram Delivery Protocol (DDP), and using AppleTalk address format. The AppleTalk family may provide SOCK_STREAM (ADSP), SOCK_DGRAM (DDP), SOCK_RDM (ATP), and SOCK_SEQPACKET (ASP). Currently, only DDP is implemented in the kernel; ATP and ASP are implemented in user level libraries; and ADSP is planned.


Well-known member
Any chance that Wireshark packet dissector for LToUDP is available anywhere? That is a handy debugging too.


Any chance that Wireshark packet dissector for LToUDP is available anywhere? That is a handy debugging too.
Here's one I wrote in Lua, so you'll need a Wireshark built with Lua, and a recent version to get at the "llap" dissector via Lua. I've tested it with 4.2.2 on Debian Linux.

Put this in the Wireshark plugins folder as a file named something clever like "ltoudp.lua".

It's not perfect; the DDP dissector fails to parse some packets and I can't see a way to figure out what the problem is without running Wireshark under GDB. Any input would be welcome!

-- LocalTalk over UDP (LToUDP)
-- Encap format: UDP dport 1954;
-- UDP payload: 4 bytes unique sender ID, then LLAP packet

-- https://www.wireshark.org/docs/wsdg_html_chunked/lua_module_Proto.html#lua_fn_Proto_new_name__desc_
ltoudp_proto = Proto("ltoudp", "LocalTalk over UDP")

-- 4 byte sender ID
-- https://www.wireshark.org/docs/wsdg_html_chunked/lua_module_Proto.html#lua_fn_ProtoField_bytes_abbr___name____display____desc__
ltoudp_sid = ProtoField.bytes(
   "An opaque sender ID; uniquely identifies a sender at the originating IP"

ltoudp_proto.fields = { ltoudp_sid }

-- https://www.wireshark.org/docs/wsdg_html_chunked/lua_module_Proto.html#lua_class_attrib_proto_dissector
function ltoudp_proto.dissector(buffer, pinfo, tree)
   -- retrieve the LLAP dissector, which will walk up subsequent protocols
   local llap_dissector = Dissector.get("llap")

   -- add a subtree section in the dissected packet that show  some LToUDP details when expanded
   local subtree = tree:add(ltoudp_proto, buffer(), "LocalTalk over UDP")
   -- 4 byte sender ID
   subtree:add(ltoudp_sid, buffer(0, 4))

   -- create a new TvbRange that starts after 4 byte sender ID, then
   -- create a Tvb from it
   -- https://www.wireshark.org/docs/wsdg_html_chunked/lua_module_Tvb.html#lua_fn_tvb_range__offset____length__
   -- https://www.wireshark.org/docs/wsdg_html_chunked/lua_module_Tvb.html#lua_fn_tvbrange_tvb__
   local buffer_without_sid = buffer:range(4):tvb()
   -- pass the sliced Tvb to the llap_dissector via pcall to swallow error traceback
   pcall(Dissector.call, llap_dissector, buffer_without_sid, pinfo, tree)
   -- llap_dissector:call(buffer_without_sid, pinfo, tree)

-- associate the LToUDP dissector with UDP dport 1954
local udp_table = DissectorTable.get('udp.port')
udp_table:add(1954, ltoudp_proto)