A modern Apple Filing Protocol (AFP) client in Go, for Linux and macOS.
Apple has deprecated and is removing the AFP client from macOS, leaving Time Capsules and older AFP-only NAS devices unreachable. goafp is a from-scratch replacement informed by the original afpfs-ng C implementation — see this performance analysis of it for why a rewrite: the C client's synchronous, single-request-at-a-time architecture made it an order of magnitude slower than the native client, and fixing that meant rebuilding the core anyway.
- Pipelined DSI core (
internal/dsi): many requests in flight per connection, replies matched to callers by request ID. Concurrency is the default, not a retrofit. - Pure-Go protocol layer (
internal/afp): AFP 3.x commands and reply parsing, every offset bounds-checked. - Portable mounting:
goafp mountexposes a volume as a localhost NFSv3 server (internal/nfsfs, a go-billy adapter over the AFP layer) that the OS mounts with its built-in NFS client —mount_nfson macOS,mount -t nfson Linux. One codepath for both platforms; no macFUSE kext, no kernel extensions. Metadata operations (chmod, chown, utimes) andstatfsmap through to AFP. - Durable: keepalive tickles stop idle mounts from being dropped, and the bridge transparently reconnects (re-login, re-open volume, re-open file handles) after a server sleep or network blip. Every operation is bounded by a timeout, so a hung server surfaces an error instead of wedging the mount.
- Testability: protocol logic is exercised against in-process mock servers with fault injection; performance properties are asserted as round-trip counts, not wall-clock times.
Early development, but the read path works end to end against real netatalk (verified in CI-style integration tests, see below).
- DSI session layer: framing, pipelined request/reply, OpenSession quantum negotiation
-
goafp status— query a server without authenticating - Login: guest (No User Authent), DHX2 (Diffie-Hellman + CAST5), and SRP-6a (SHA-1 / MGF1, RFC 5054 group 2)
- Volume list/open, directory enumeration, stat, UTF-8 path handling
- File reads with pipelined readahead (concurrent FPReadExt, in-order reassembly)
- Write path: create, pipelined/coalesced writes, truncate, mkdir, rename/move, delete
- NFS bridge mounting for Linux + macOS (
goafp mount), with chmod/chown/utimes and statfs mapped through to AFP - Symlinks (create, readlink, detected in stat/enumerate)
- netatalk-in-Docker integration test suite (incl. end-to-end NFS and live SRP)
- Cleartext UAM polish / RandNum UAMs
go build ./cmd/goafp
# Query any AFP server (Time Capsule, netatalk, old macOS)
./goafp status myserver.local
# List exported volumes (guest, or user:pass@ for authenticated)
./goafp volumes afp://myserver.local
./goafp volumes afp://alice:secret@myserver.local
# List a directory in a volume
./goafp ls afp://alice:secret@myserver.local/Documents
./goafp ls afp://alice:secret@myserver.local/Documents/subdir
# Read a file to stdout, or download it
./goafp cat afp://alice:secret@myserver.local/Documents/notes.txt
./goafp get afp://alice:secret@myserver.local/Documents/archive.zip
# Upload, create directories, rename/move, delete
./goafp put ./report.pdf afp://alice:secret@myserver.local/Documents/report.pdf
./goafp mkdir afp://alice:secret@myserver.local/Documents/2026
./goafp mv afp://alice:secret@myserver.local/Documents/report.pdf 2026/report.pdf
./goafp rm afp://alice:secret@myserver.local/Documents/2026/report.pdfgoafp mount serves a volume over NFS on localhost; mount it with the
OS's built-in NFS client (no kernel extension needed):
# Terminal 1: serve the volume (stays running)
./goafp mount afp://alice:secret@myserver.local/Documents
# Terminal 2: mount it (goafp prints the exact command for your OS)
# macOS:
sudo mount_nfs -o vers=3,tcp,port=2049,mountport=2049,noowners,resvport \
127.0.0.1:/ /path/to/mountpoint
# Linux:
sudo mount -t nfs -o vers=3,tcp,port=2049,mountport=2049,nolock \
127.0.0.1:/ /path/to/mountpointgo test ./... # unit tests (mock DSI/AFP servers, no network)
go vet ./...test/integration/run.sh spins up netatalk in Docker, seeds a share, and
runs the suite against it (requires Docker):
./test/integration/run.shBSD 3-Clause. See LICENSE.