My basement network closet has a PowerBook G3 Series ("Wallstreet"), a Macintosh SE/30 running System 7.1, and a LaserWriter 4/600 PS that still prints faster than most inkjets twice its age. Getting files onto any of them used to mean either sneakernet with a floppy adapter, or standing up Netatalk with its kernel-level AppleTalk support and a config file dialect I never quite remembered between attempts. I wanted something I could run as a normal user process on my Linux box, point at a network interface or a serial adapter, and just have it work without touching a kernel module.
That's how I ended up looking at TailTalk, a project from a developer going by FeralFirmware that implements the AppleTalk protocol suite entirely in userspace, in Rust, on top of Tokio. It has zero dependency on Netatalk or any AppleTalk-aware kernel driver — it talks to the network through a raw socket for EtherTalk, or through a small USB-to-serial bridge called TashTalk for actual LocalTalk hardware. You can even run multiple independent copies of the stack on the same machine at once, which matters more than it sounds like it should once you start juggling a file server and a print bridge on one box.
This post walks through how the stack is actually put together: the protocol layers it implements, the daemon it uses to share one network interface across several async clients, the specific hardware quirks I had to work around to get a LaserWriter and an SE/30 both talking at the same time, and a bit of the project's own history, since the rough edges in a "work in progress prototype" (the README's own words, not mine) are just as informative as the parts that already work.
Why I Didn't Just Install Netatalk
Netatalk has kept classic Mac networking alive for decades, and I don't want to undersell that. But it leans on the host OS to actually understand AppleTalk framing at some level, and on Linux that historically meant kernel support and a daemon stack (atalkd, papd, and friends) doing routing, zone lookups, and AEP pings underneath the AFP server. Getting that running on a headless box I also use for other things always felt heavier than the actual problem I was solving.
TailTalk's pitch is narrower and, honestly, more honest about its scope: it's built from scratch with no code reused from Netatalk, and it explicitly does not try to replace Netatalk's full feature set. It's aimed at small networks — a couple of Macs and a printer or two — not a campus-wide AppleTalk zone with routers in the mix. All it asks of the OS is a raw socket for EtherTalk, or a serial device for a TashTalk bridge if you're doing real LocalTalk. Everything above that — addressing, AARP, framing, session state — lives in the Rust process itself.
That narrower scope shows up as a real limitation, not just marketing language: as of now TailTalk only works in a routerless setup. Zone lookups and inter-network routing are called out directly as being out of scope for the project, and joining a network that already has an AppleTalk router on it is still a work in progress that hasn't landed in the mainline code yet. If your target network has a router doing zone management, you're not the audience for this yet.
The Protocol Stack: AARP up through AFP and ADSP
What sold me on actually trying it was seeing how much of the stack is implemented rather than stubbed out. AppleTalk isn't one protocol, it's a pile of them layered the way TCP/IP is, and getting file sharing working means implementing most of the pile, not just AFP in isolation.
TailTalk ships packet parsers and fully async APIs for essentially the whole classic suite:
- AppleTalk Address Resolution Protocol (AARP), for mapping AppleTalk addresses onto Ethernet
- Datagram Delivery Protocol (DDP), the unreliable datagram layer everything else rides on
- Name Binding Protocol (NBP), for looking up services by name instead of address
- AppleTalk Transaction Protocol (ATP), a request/response layer with retries
- Printer Access Protocol (PAP), which is what actually gets bytes to a LaserWriter
- AppleTalk Session Protocol (ASP) and AppleTalk Filing Protocol (AFP), the file-sharing layer
- AppleTalk Data Stream Protocol (ADSP), a reliable stream protocol above DDP
Each of those is exposed as an async API rather than a blocking one, which is the part that actually matters day to day. Looking up a LaserWriter by name over NBP, or opening an AFP session, is just an await away instead of a callback or a poll loop you have to hand-roll. That's a genuinely different feel from the C APIs Netatalk exposes, where you're managing your own event loop around blocking calls.
tailtalkd: One Daemon, Many Async Clients
The part of the architecture I didn't expect going in was that the AARP/DDP underlay — the bit that actually owns a network interface — doesn't have to live inside your application at all. By default it runs in-process, which is fine for a single demo binary. But TailTalk also ships a separate daemon, tailtalkd, that owns the physical interfaces and serves DDP sockets, addressing, and routing rules to multiple client processes over a protobuf-based protocol on a Unix or UDP socket.
That matters once you're running more than one AppleTalk-speaking program on the same box — say, an AFP server and a print-sharing bridge — because only one process can hold a raw socket or a TashTalk serial port at a time. Routing everything through tailtalkd means each client just talks protobuf over a local socket instead of fighting over the interface. It's also usable from plain C clients, not just from Rust, which is a nice touch if you'd rather not pull in a whole Tokio runtime for a small utility.
Wiring a client up to the daemon rather than running the stack in-process is a builder-level choice. Here's roughly the shape of it, going from an in-process stack to one that talks to a shared tailtalkd:
use tailtalk::TalkStack;
// In-process: this client owns the raw socket / TashTalk port directly.
let stack = TalkStack::builder()
.interface("eth0")
.build()
.await?;
// Daemon-backed: talk to tailtalkd over a Unix socket instead,
// so several clients can share one interface without fighting over it.
let stack = TalkStack::builder()
.daemon_unix("/run/tailtalkd.sock")
.build()
.await?;
let printer = stack.nbp_lookup("LaserWriter 4/600:LaserWriter@*").await?;
The exact builder surface will keep moving as the project matures — this is illustrative of the pattern the README describes, not a guarantee it matches the crate line for line by the time you read this. But the underlying idea, one daemon owning the wire, many async clients sharing it through a typed protocol, is the part I'd actually borrow for other userspace network stacks even outside the AppleTalk world.
TashTalk USB and the Platform Access Dance
LocalTalk hardware doesn't speak Ethernet, so for my SE/30 and Classic I needed a TashTalk USB adapter, which bridges LocalTalk's RS-422-ish signaling to a regular USB-serial connection using a Silicon Labs CP210x bridge chip. On Linux the chip shows up as a normal tty device, but by default only root can open it.
Every platform has its own version of this same fight over device permissions, and none of them are AppleTalk-specific — it's the usual "who's allowed to touch raw hardware" problem any userspace network stack runs into. On Linux you fix it with a udev rule granting your user access based on vendor/product ID. On Windows you need the CP210x VCP driver plus the npcap SDK if you also want EtherTalk. On macOS the CP2102N chip is supported out of the box since macOS 11, but EtherTalk packet capture goes through /dev/bpf*, which is root-only unless you install the ChmodBPF package that ships inside the Wireshark disk image.
None of this is hard once you know it's coming, but it's the kind of setup friction that makes a "just run the binary" project feel more involved than the README initially suggests. If you're only using TashTalk and never touch EtherTalk, you can skip the raw socket permission dance entirely and just deal with the serial device permissions, which is a meaningfully smaller ask.
The AsanteTalk Phase 1 Trap
My SE/30 and Classic don't have Ethernet at all, so I'm bridging them through an AsanteTalk adapter, a LocalTalk-to-Ethernet box from the era rather than a TashTalk. These things have their own startup behavior that has nothing to do with TailTalk but will absolutely make you think your stack is broken if you don't know about it.
When an AsanteTalk powers on, it listens on the Ethernet side for a moment before deciding which EtherTalk phase to speak. If it doesn't see any Phase 2 traffic during that window, it falls back to Phase 1. TailTalk supports Phase 1 fine for NBP lookups and LaserWriter printing, but AFP is a different story: the Mac can discover the AFP server over NBP just fine, and then the actual AFP session responses seem to get silently dropped.
The fix, once you know what's happening, is boring: make sure something on your network is already producing Phase 2 EtherTalk traffic before the AsanteTalk boots, so it never falls back. It's not a TailTalk bug so much as thirty-year-old hardware making a reasonable-at-the-time assumption that no longer holds on a modern switch. I'd still call it out explicitly if you're troubleshooting "NBP finds my server but AFP won't mount," because it's a much more likely culprit than anything in the Rust code.
Where TailTalk Actually Came From
It's worth being upfront that TailTalk isn't a mature, funded project with a design doc behind it — it grew out of one developer's frustration with Netatalk's C API, documented in a public thread on the 68kMLA forums. The developer behind FeralFirmware described coming from "the Rust world with async/await everywhere" and finding Netatalk's C interface unpleasant enough to justify writing a parser and encoder for every AppleTalk packet type from scratch, including a no_std variant meant for embedded targets.
The forum thread also makes clear this didn't arrive fully formed. Early posts describe it as Linux-only with other platforms promised "soon," followed a bit later by TashTalk support landing, and eventually a milestone post celebrating that Linux-to-Mac AFP transfers were working end to end. Not every milestone was smooth, though:
"Gets about 30MiB in to a file copy then just halts network activity" — FeralFirmware, 68kMLA forum thread "Fun with userspace AppleTalk"
That kind of hard lock on long transfers is exactly the sort of bug you'd expect from a from-scratch reimplementation of a session-layer protocol talking to hardware from four different decades, and it's a useful reminder that "work in progress prototype" in the README is not false modesty. Other developers have since started depending on the crates directly — a community AppleTalk service browser called chooser pins tailtalk and tailtalk-packets as dependencies — which suggests the underlying packet layer is stable enough to build on even while the higher-level stack keeps changing shape.
What I'd Use This For, and What's Still Missing
For what I actually wanted — copying files onto an SE/30, printing PostScript to a LaserWriter, sharing a modern printer back to a StyleWriter over PAP — TailTalk did the job without a single kernel module, and the AFP server even preserves resource forks when importing from floppy images or StuffIt archives, which matters a lot if you care about anything with a custom icon or a Mac-specific file type. The example programs (an AEP echo tool, the AFP server, an NBP lookup utility, and a PostScript printing tool) cover the exact use cases I had, and the GUI wraps the common combination of "share a folder, share a printer" into something closer to zero-config.
Before I'd call this a general-purpose replacement for anything, though, it's worth being clear about where the edges of the current scope actually sit, because they're not small.
What I wouldn't do is treat this as a Netatalk replacement for anything bigger than a hobbyist's basement network. It's GPL-3.0 licensed, still described by its own author as a prototype, and the daemon protocol, builder APIs, and router support are all still shifting under active development. For a couple of old Macs and a printer sitting next to your desk, though, it's the first AppleTalk implementation I've used where I didn't have to think about the kernel at all — and given how much of my actual goal was "stop fighting the OS and just move some files," that's the tradeoff I'd take again.
Sources: FeralFirmware — TailTalk on GitHub, FeralFirmware — "Fun with userspace AppleTalk," 68kMLA forum
Sources: Hacker News — TailTalk: A modern async user space AppleTalk stack with Rust and Tokio

Comments
No comments yet — be the first to share your thoughts.