Developer Documentation for Netatalk Client

AFP (Apple Filing Protocol) originated on the Mac as Apple’s network filesystem protocol. Today it is a cross-platform protocol with multiple independent client and server implementations, including Netatalk on the server side and this Netatalk Client project on the client side. A client has to establish a network connection to an AFP server, attach to a volume, and keep that session state alive while file operations are in progress.

Netatalk Client is split into a stateful protocol library, a stateless adapter layer, and two different front-ends. libafpclient is the core library that knows how to speak DSI and AFP, but it expects a long-lived process that can live with its event loop, threads, signals, and connection state.

libafpsl is the stateless API in afpsl.h. It lets short-lived tools and other applications issue one operation at a time without embedding libafpclient’s event loop. Under the hood, libafpsl talks over a Unix socket to afpsld, the stateless daemon implemented in daemon/. afpsld owns the persistent AFP server and volume state, calls the libafpclient midlevel API, and returns one response to each stateless request. This is the layer used by afpcmd, and it can be built without FUSE.

The user-facing clients then choose how to present AFP files to callers:

Both front-ends ultimately use the same AFP protocol machinery, but they answer different questions. FUSE asks “how do I make this remote volume look like a local filesystem?” afpcmd asks “how do I copy this remote object to or from a regular local path?”

Architectural Diagram

┌──────────────────────────────────────────────────────────────┐
│                    Client Applications                       │
├─────────────────────────────┬────────────────────────────────┤
│   afpcmd, GUI clients       │   afpc fs, mount_afpfs         │
└───────┬─────────────────────┴─────────┬──────────────────────┘
        │                               │
        │ libafpsl.so (afpsl.h)         │ (direct spawn/mount)
        │ Stateless API                 │
        ↓                               ↓
    ┌──────────────────┐          ┌─────────────────────┐
    │   afpsld         │          │   afpfsd            │
    │   (Stateless)    │          │   (FUSE)            │
    ├──────────────────┤          ├─────────────────────┤
    │ • CONNECT        │          │ • MOUNT/UNMOUNT     │
    │ • File I/O       │          │ • FUSE operations   │
    │ • Dir ops        │          │ • xattr callbacks   │
    │ • Metadata copy  │          │ • Multi-mount mgmt  │
    │   modes: FI, RF, │          │                     │
    │   xattrs         │          │                     │
    │                  │          │ Requires libfuse    │
    └───────┬──────────┘          └───────┬─────────────┘
            │                             │
            │  Calls midlevel API         │  Calls midlevel API
            └─────────────┬───────────────┘
                          ↓
            ┌──────────────────────────────────┐
            │      libafpclient.so             │
            ├──────────────────────────────────┤
            │  • midlevel (afp.h, midlevel.h)  │
            │  • metadata ops                  │
            │  • lowlevel                      │
            │  • proto_* (AFP protocol)        │
            │  • Engine (DSI, event loop)      │
            └──────────────┬───────────────────┘
                           ↓
                   ┌───────────────┐
                   │  AFP Server   │
                   └───────────────┘

libafpclient

This shared library (libafpclient.so) implements the DSI and AFP communication engine used by the bundled daemons and tools. Stateful consumers use its opaque transport interface from <netatalk-client/transport.h>; concrete headers under lib/ remain implementation details.

Applications that prefer daemon-managed connections should use the stateless libafpsl interface from <netatalk-client/afpsl.h>.

A key point to know when building libafpclients is that libafpclient will spawn threads and override signals. Asynchronous events need to be hooked into a loop provided by libafpclient. You cannot write your own select() loop!

The major subcomponents of libafpclient are all in the lib/ directory.

They are:

Logging

log_for_client() is the private shared logging entry point. Callers pass it a complete message string; it trims trailing newlines, escapes control characters, and forwards the sanitized text to the registered client logger. If the message pointer is null, it is logged as (null).

Inside the netatalk-client build, log_for_client() is also available as a checked printf-style convenience macro. Internal callers may use literal format strings:

log_for_client(priv, AFPFSD, LOG_WARNING, "Could not open %s", path);

Do not pass runtime or remote-controlled text as the format string. If a message is already composed, log it through a literal %s format:

log_for_client(priv, AFPFSD, LOG_ERR, "%s", message);

The underlying function remains the plain-message API. Internal code that deliberately needs to call that function directly can use the parenthesized form (log_for_client)(priv, AFPFSD, level, message) to bypass the internal macro, but that should stay rare and explicit.

Midlevel

This is an internal API that simplifies the AFP functions and does some simplification of the protocol, such as calling multiple AFP functions to perform a basic task. It is not an application-facing interface.

Typically, a midlevel function will:

Lowlevel

This is an API that handles many AFP functions, while taking care of some AFP details, such as behaviour differences between AFP versions and situations where servers don’t adhere to the exact protocol.

An example of this is when listing a directory; ll_readdir() will figure out what AFP version is being used, and either call protocols afp_enumerateext() for AFP 2.x or afp_enumerateext2 for 3.x (which can handle larger file lists).

These are implemented in lib/midlevel.c. The API is exposed in midlevel.h.

You should generally not use these functions.

Protocol

This is the raw API that exposes individual AFP functions, this includes things like afp_listextattr().

These are implemented in lib/proto_* files and exposed in afp.h.

You should almost never use this set of functions.

Other topics

AFP Protocol Compliance

AFP 3.3 (OS X 10.6)

Replay Cache Support

AFP 3.3 mandates support for the AFP Replay Cache mechanism, which ensures reliable operation across network interruptions and reconnections.

  1. Persistent Request IDs: Request IDs are no longer reset to 0 on reconnection when the server supports replay cache. They wrap around from 65535 to 1 (avoiding 0).

  2. Server Capability Detection: During DSIOpenSession, the client now parses the kServerReplayCacheSize option from the server’s reply to detect replay cache support.

  3. Dynamic Behavior:

  4. If server advertises replay cache support → persistent request IDs enabled
  5. If server doesn’t support replay cache → legacy behavior (reset to 0 on reconnect)

Two-Daemon Architecture

The project uses two separate daemon binaries with distinct purposes:

afpsld - AFP Stateless Daemon

Location: daemon/

Purpose: Handles remote AFP file operations via the stateless API

Dependencies: libafpclient.so only (NO FUSE required)

Socket: /tmp/afp_sl-<uid>

Operations:

Architecture:

afpfsd - AFP FUSE Daemon

Location: fuse/

Purpose: Mounts AFP volumes as local filesystems using FUSE

Dependencies: libafpclient.so + libfuse

Socket: /tmp/afp_server-<uid>-<mountpoint-hash>

Operations:

This daemon uses a multi-daemon model where a manager process spawns individual mount-specific daemon processes for fault isolation (see Multi-Mount Architecture section).

Multi-Mount Architecture for FUSE

Design Choice: The Netatalk Client FUSE client uses a manager daemon architecture where each mount gets its own isolated daemon process, providing fault isolation and simpler state management for multiple mounts.

Compared to a single shared daemon with per-mount multi threading or multiplexing, this design offers:

Benefits:

Trade-offs:

Multi-Daemon Model Overview

┌─────────────────────────────────────────────────────────────┐
│ mount_afpfs afp://server/vol1 /mnt/vol1                     │
│ mount_afpfs afp://server/vol2 /mnt/vol2                     │
│ mount_afpfs afp://server/vol3 /mnt/vol3                     │
└─────────────────────────────────────────────────────────────┘
            ↓ All requests go through manager
┌─────────────────────────────────────────────────────────────┐
│ afpfsd --manager (PID: 1000) [socket: afpfsd-501]           │
│   ├── Tracks child PIDs: [2001, 2002, 2003]                 │
│   ├── Spawns mount-specific daemons on demand               │
│   └── Handles coordinated shutdown (exit command)           │
└─────────────────────────────────────────────────────────────┘
        ↓ Spawns independent mount daemons
┌──────────────────────────────┬──────────────────────────────┬──────────────────────────────┐
│ afpfsd --socket-id ... (2001)│ afpfsd --socket-id ... (2002)│ afpfsd --socket-id ... (2003)│
│ [socket: afpfsd-501-bdb4...] │ [socket: afpfsd-501-8f3e...] │ [socket: afpfsd-501-c7d2...] │
│   /mnt/vol1 FUSE mount       │   /mnt/vol2 FUSE mount       │   /mnt/vol3 FUSE mount       │
└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘

Mount Flow

  1. mount_afpfs afp://server/vol1 /mnt/vol1
  2. Client computes mount socket ID via hash of /mnt/vol1afpfsd-501-bdb4a5c2
  3. Client tries to connect to mount socket → doesn’t exist
  4. Client connects to manager socket afpfsd-501
  5. If manager doesn’t exist, client spawns it: afpfsd --manager
  6. Client sends AFP_SERVER_COMMAND_SPAWN_MOUNT with socket ID and mountpoint
  7. Manager forks child process: afpfsd --socket-id afpfsd-501-bdb4a5c2
  8. Mount daemon listens on its unique socket and performs FUSE mount
  9. Client receives success, sends actual mount request to mount daemon socket

Coordinated Shutdown

afpc fs exit
  1. Client connects to manager socket afpfsd-501 (NULL mountpoint)
  2. Sends AFP_SERVER_COMMAND_EXIT
  3. Manager daemon:
  4. Sends SIGTERM to all tracked child PIDs
  5. Waits 1 second for graceful shutdown
  6. Sends SIGKILL to any remaining children
  7. Waits for all children with waitpid()
  8. Exits manager daemon

Result: All mounts unmounted cleanly, no orphaned processes

Key Functions:

Socket Naming

All socket files are created in /tmp/:

Management Commands (status, unmount, exit)

Use NULL mountpoint in daemon_connect(), which causes:


Stateless Client Library and Daemon Architecture

Stateless Client Library (libafpsl)

Netatalk Client provides a stateless client library (libafpsl.so) for applications that need to perform AFP operations without managing persistent connections or event loops. Unlike libafpclient which requires a long-lived stateful process with its own event loop, the stateless library delegates connection management to a daemon process.

Key characteristics:

Register a logger before making stateless calls:

static void app_log(void *context, int level, const char *message)
{
    /* level is one of the syslog LOG_* values. */
}

afp_sl_set_log_callback(app_log, application_context);

The callback runs synchronously on the calling thread. Passing a null callback disables log delivery. Registration is process-global, matching the stateless library’s process-global connection state.

The library also provides metadata-only replacement helpers for local-to-AFP, AFP-to-local, and AFP-to-AFP copies. Callers select the local on-disk representation on each operation with enum afp_metadata_mode; this is the implementation behind afpcmd -M / --metadata for file transfers involving an ordinary local path. It does not control FUSE mounts or the AFP server’s own metadata storage. Supported modes are auto, Netatalk AppleDouble, filesystem xattrs, macOS AppleDouble, and none. The destination must already exist. These helpers clear represented destination metadata before copying FinderInfo, ResourceFork, and eligible generic xattrs. They deliberately do not copy the data fork, POSIX mode, or timestamps. In auto mode, generic xattrs use filesystem xattrs when available and Netatalk AppleDouble EA sidecars otherwise. FinderInfo and ResourceFork use native filesystem xattrs on macOS, and macOS AppleDouble sidecars on other systems unless Netatalk mode is selected explicitly.

The stateless API returns zero on success and negative errno values on failure. Metadata read and list calls instead return a nonnegative byte count on success. Generic xattr values are limited to 4096 bytes, xattr name lists to AFP_SL_XATTR_LIST_MAX, and resource forks to INT_MAX. Resource fork data is read and written in 4096-byte chunks. Positioned writes do not shorten an existing fork, except that a zero-length write at offset zero clears it; call afp_sl_truncateresourcefork() to set its final length explicitly. afp_sl_setxattr() accepts AFP_SL_XATTR_CREATE or AFP_SL_XATTR_REPLACE (the portable equivalents of the system XATTR_CREATE and XATTR_REPLACE flags), but not both. afp_sl_attach() reports a volume-password challenge through its optional status output while returning -EACCES. Consumers can classify session recovery with afp_sl_recovery_for_error(). afp_sl_changepw() likewise uses a typed status output for password-policy details while keeping its return value in the errno domain.

Metadata replacement is not atomic. A failure can leave partially copied destination metadata. Unsupported metadata and values or lists above current protocol limits are reported through the optional enum afp_metadata_warning bitmask so non-interactive consumers can apply their own policy without parsing library output.

Use cases:

Stateless Protocol Communication

The stateless library communicates with afpsld using a request/response protocol over Unix sockets:

Every daemon response ends with a structured log trailer. Each record preserves its syslog severity, and libafpsl delivers the records through the registered callback after validating the complete response. This applies to fixed and streaming operations, including reads, writes, metadata calls, and directory listings.

Connection model:

  1. Short-lived Unix socket connections: Most operations open a new connection to afpsld, send one request, receive one response, and close
  2. Persistent server/volume state: Even though socket connections are ephemeral, the server and volume state persists in afpsld’s memory
  3. Server ID handles: When a server is connected, afpsld returns an opaque afpc_server_t that identifies the authenticated AFP session. Follow-up attach requests must present this handle; afpsld does not implicitly rediscover authenticated sessions by server name.
  4. Volume ID handles: When a volume is attached, afpsld returns an opaque afpc_volume_t that remains valid across separate socket connections as long as the daemon runs
  5. Explicit session resume: afp_sl_resume() can return an existing connected afpc_server_t without reauthentication, but only when the caller supplies no password and the daemon can identify one matching session. Resume authenticates the caller’s access to the per-user daemon’s existing session state, not the AFP user identity. Normal afp_sl_connect() always performs AFP authentication.
  6. Connection reuse for CONNECT/ATTACH: The CONNECT operation keeps the socket open (close=0 flag) to allow the subsequent ATTACH to use the same connection

Request flow:

Client Process           afpsld Daemon              AFP Server
    |                        |                          |
    |--CONNECT (close=0)---->|                          |
    |                        |----TCP connect---------->|
    |<---server_id-----------|                          |
    |                        |                          |
    |--ATTACH server_id----->|                          |
    |                        |----FPOpenVol------------>|
    |<---volumeid------------|                          |
    |  [connection closes]   |                          |
    |                        |                          |
    |--READDIR (close=1)---->|                          |
    |  (new socket)          |----FPEnumerate---------->|
    |<---file list-----------|                          |
    |  [connection closes]   |                          |

Threading model:

Benefits of the Two-Daemon Approach

Separation of concerns:

Build flexibility:

Process isolation:

Clear naming:


Zeroconf Discovery Architecture

Zeroconf browsing is a frontend concern implemented by two layered internal static libraries in discovery/. libafpc-discovery normalizes the provider API and is used by every discovery frontend. libafpc-client-discovery builds the bounded discovery command and exact service resolver once from discovery/client/discover.c. Discovery happens before an AFP session or mount request exists.

afpcmd                        afpc / mount_afpfs
  |                                      |
  +---------- afpc-discovery ------------+
                  |
          +-------+---------+
          | normalized core |
          +-------+---------+
            Avahi or DNS-SD
                  |
          selected endpoint
                  |
      existing afpsld / afpfsd
                  |
       authenticate -> volumes

The public-to-the-tree API in discovery/discovery.h normalizes service add, update, remove, resolve, and snapshot operations. Native providers live behind discovery/backend.h: DNS-SD is preferred on macOS, Avahi is preferred on other supported Unix systems, and a stub preserves normal builds when neither provider is available. Service identity is the instance, registration type, domain, and interface tuple. Resolved targets, advertised ports, address families, IPv6 scope, and raw TXT data remain separate endpoint data. The default provider session listens for both _afpovertcp._tcp and _device-info._tcp; the bounded discovery frontend correlates matching instance/domain/interface tuples and extracts the companion model TXT value.

The frontends deliberately have different UX responsibilities:

Fake providers in test/ make event ordering, output, interface selection, and ambiguity handling deterministic without requiring multicast networking in the unit test suite.

afpcmd Implementation Using Stateless Library

Overview

The afpcmd command-line utility has been refactored to use the stateless client library (libafpsl) instead of directly calling the midlevel API. This change provides several benefits:

Architecture Transition

Before (stateful direct API):

afpcmd process
├── Calls midlevel API (ml_*) directly
├── Manages struct afp_server and afp_volume locally
├── Runs afp_main_loop() in background thread
├── Integrates with signal handlers
└── Must live for duration of all operations

After (stateless daemon API):

afpcmd process                afpsld daemon
├── Calls stateless API       ├── Manages global server/volume state
│   (afp_sl_*)                ├── Runs afp_main_loop()
├── Opens/closes Unix socket  ├── Calls midlevel API (ml_*)
├── No event loop             └── Spawns threads per request
├── No signal handlers
└── Can exit/restart freely

Connection Management

afpcmd maintains minimal connection state:

Connection flow:

  1. User runs: afpcmd afp://user:pass@server/volume
  2. afpcmd calls afp_sl_connect() → afpsld connects to AFP server
  3. afpcmd calls afp_sl_attach(server_id, ...) → afpsld opens volume on that authenticated session
  4. afpcmd receives afpc_volume_t handle and sets connected = 1
  5. All subsequent commands pass this volume handle to afp_sl_* functions
  6. afpsld uses the volume handle to look up the volume in its global state

Volume listing and volume attachment are bound to server_id; callers should not rely on URL-only lookup to rediscover an authenticated session.

Long-lived clients that need cross-process reuse, such as KIO workers, should first call afp_sl_resume() with no password. This resumes the per-user daemon’s existing host session rather than reauthenticating AFP user identity. If no matching daemon session exists, they should retrieve credentials from their own credential cache or prompt the user, then call afp_sl_connect() to authenticate.

Disconnect flow:

  1. User runs: disconnect or quit
  2. afpcmd calls afp_sl_detach() → afpsld closes volume
  3. afpcmd sets vol_id = NULL and connected = 0

Command Implementation

afpcmd commands map to stateless library operations:

Command Stateless API Description
connect afp_sl_connect() + afp_sl_attach() Authenticate and attach to volume
disconnect afp_sl_detach() Detach from volume
ls/dir afp_sl_readdir() List directory contents
get afp_sl_stat() + afp_sl_open() + afp_sl_read() + afp_sl_close() Download files
put afp_sl_creat() + afp_sl_open() + afp_sl_write() + afp_sl_close() Upload files
rm afp_sl_unlink() Delete files
mkdir afp_sl_mkdir() Create directories
rmdir afp_sl_rmdir() Remove directories
mv afp_sl_rename() Rename/move files
chmod afp_sl_chmod() Change permissions
stat afp_sl_stat() Get file attributes
df afp_sl_statfs() Volume statistics

Example: File Download (get command)

High-level flow:

  1. User runs: get remote_file.txt local_file.txt
  2. afpcmd calls afp_sl_stat(vol_id, path, basename, &stat) to get file size
  3. afpcmd calls afp_sl_open(vol_id, path, basename, &fileid, O_RDONLY)
  4. afpcmd loops calling afp_sl_read(vol_id, fileid, fork=0, offset, size, &received, &eof, buffer)
  5. Each chunk is written to local file
  6. afpcmd calls afp_sl_close(vol_id, fileid) when complete

What happens in afpsld: