Dev Docs AFP Daemon

AFP Daemon (afpd)

Audited against Netatalk d63155e13 on 2026-10-10. This describes the current source checkout; older releases may differ.

1. AFP Daemon Overview

The afpd daemon is the core component of Netatalk that implements the Apple Filing Protocol (AFP) server. It uses a process-per-connection architecture where a single parent process accepts connections and forks a dedicated child worker for each client session.

Key Source Files

Component File
Parent process & event loop etc/afpd/main.c
Child worker event loop etc/afpd/afp_dsi.c
Session forking libatalk/dsi/dsi_getsess.c
AFP command dispatch etc/afpd/switch.c
Open fork management etc/afpd/fork.h, etc/afpd/ofork.c, etc/afpd/fork.c
Child process table libatalk/util/server_child.c, include/atalk/server_child.h
Idle worker thread etc/afpd/idle_worker.c, etc/afpd/idle_worker.h

Caching subsystem (directory cache, AD metadata cache, resource fork cache, IPC cache hint system) is documented in the dedicated Caching Architecture page.

Process Architecture Diagram

%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
graph TB
    subgraph "afpd Parent Process"
        A["poll() event loop<br/>(main.c: main())"]:::yellow
        B["Signal handler<br/>afp_goaway()"]:::orange
        C["Child table<br/>server_child_t"]:::cyan
        D["Hint buffer<br/>hint_buf[128]"]:::purple
    end

    subgraph "afpd Child Workers"
        E["Child 1<br/>afp_over_dsi()"]:::green
        F["Child 2<br/>afp_over_dsi()"]:::green
        G["Child N<br/>afp_over_dsi()"]:::green
    end

    subgraph "Each Child Process"
        H["AFP command<br/>dispatch"]:::purple
        I["Idle worker<br/>pthread"]:::green
        J["Dircache<br/>(ARC)"]:::cyan
        K["AD cache<br/>(Tier 1)"]:::cyan
        L["Rfork cache<br/>(Tier 2)"]:::cyan
    end

    Mac1["Mac Client 1"]:::blue
    Mac2["Mac Client 2"]:::blue
    MacN["Mac Client N"]:::blue
    CNID["CNID<br/>Database"]:::salmon

    Mac1 --> E
    Mac2 --> F
    MacN --> G
    A -->|"fork()"| E
    A -->|"fork()"| F
    A -->|"fork()"| G
    A --> C
    A --> D
    D -->|"hint pipe"| E
    D -->|"hint pipe"| F
    D -->|"hint pipe"| G
    E -->|"IPC socketpair"| A
    F -->|"IPC socketpair"| A
    E --- H
    E --- I
    H --> J
    J --> K
    K --> L
    H --> CNID

    classDef blue fill:#74b9ff,stroke:#333,rx:10,ry:10
    classDef purple fill:#a29bfe,stroke:#333,rx:10,ry:10
    classDef green fill:#55efc4,stroke:#333,rx:10,ry:10
    classDef yellow fill:#ffeaa7,stroke:#333,rx:10,ry:10
    classDef grey fill:#dfe6e9,stroke:#333,rx:10,ry:10
    classDef salmon fill:#fab1a0,stroke:#333,rx:10,ry:10
    classDef cyan fill:#81ecec,stroke:#333,rx:10,ry:10
    classDef orange fill:#ff9f43,stroke:#333,rx:10,ry:10

2. Parent Process Architecture

The parent process in main() performs initialization, then enters an event loop that manages listening sockets, child IPC, signals, and cache hint batching.

Global State

Defined as file-scope state in etc/afpd/main.c:

Variable Type Purpose
dsi_obj AFPObj DSI protocol configuration object
asp_obj AFPObj ASP protocol configuration object (DDP)
server_children server_child_t * Hash table of active child processes
reloadconfig sig_atomic_t Flag set by SIGHUP handler
gotsigchld sig_atomic_t Flag set by SIGCHLD handler
asev struct asev * Poll fd set and metadata

Event Loop

The main event loop in main() uses poll() with a dynamic timeout:

%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
flowchart TD
    START["while (1)"]:::yellow
    TIMEOUT{"hint_buf_count()<br/>&gt; 0?"}:::cream
    POLL["poll(asev→fdset,<br/>asev→used,<br/>poll_timeout)"]:::yellow
    SIGCHLD{"gotsigchld?"}:::orange
    CH["child_handler():<br/>waitpid loop,<br/>server_child_remove,<br/>asev_del_fd"]:::orange
    ERR{"ret &lt; 0?"}:::red
    EINTR{"EINTR?"}:::cream
    RELOAD{"reloadconfig?"}:::orange
    DOCFG["Reset sockets,<br/>re-parse config,<br/>re-init listeners,<br/>server_child_kill<br/>(SIGHUP)"]:::orange
    FDEVT{"ret &gt; 0?"}:::cream
    FDLOOP["for each fd<br/>with revents"]:::purple
    LISTEN["LISTEN_FD:<br/>dsi_start() →<br/>fork child,<br/>add IPC_FD"]:::cyan
    IPC["IPC_FD:<br/>ipc_server_read()"]:::purple
    FLUSH{"hint_buf_count &gt; 0<br/>AND (ret==0 OR<br/>count ≥ 128)?"}:::cream
    DOFLUSH["hint_flush_pending<br/>(server_children)"]:::purple

    START --> TIMEOUT
    TIMEOUT -->|"Yes: 50ms"| POLL
    TIMEOUT -->|"No: -1 (block)"| POLL
    POLL --> SIGCHLD
    SIGCHLD -->|"Yes"| CH --> ERR
    SIGCHLD -->|"No"| ERR
    ERR -->|"Yes"| EINTR
    EINTR -->|"Yes"| START
    EINTR -->|"No"| BREAK["break (fatal)"]:::red
    ERR -->|"No"| RELOAD
    RELOAD -->|"Yes"| DOCFG --> START
    RELOAD -->|"No"| FDEVT
    FDEVT -->|"Yes"| FDLOOP
    FDLOOP --> LISTEN
    FDLOOP --> IPC
    FDLOOP --> STATS["STATS_FD:<br/>afpstats_handle_accept()"]:::cyan
    STATS --> FLUSH
    LISTEN --> FLUSH
    IPC --> FLUSH
    FDEVT -->|"No (timeout)"| FLUSH
    FLUSH -->|"Yes"| DOFLUSH --> START
    FLUSH -->|"No"| START

    classDef yellow fill:#ffeaa7,stroke:#333,rx:10,ry:10
    classDef cream fill:#f8f4e8,stroke:#333,rx:10,ry:10
    classDef orange fill:#ff9f43,stroke:#333,rx:10,ry:10
    classDef red fill:#ee5a5a,stroke:#333,color:#fff,rx:10,ry:10
    classDef purple fill:#a29bfe,stroke:#333,rx:10,ry:10
    classDef cyan fill:#81ecec,stroke:#333,rx:10,ry:10
    classDef green fill:#55efc4,stroke:#333,rx:10,ry:10

Step ordering is critical (in main() event loop):

  1. SIGCHLD first — removes dead children from the table before hint flush or fd events can reference them
  2. Error handling — uses saveerrno because child_handler() clobbers errno via waitpid/close/asev_del_fd
  3. Config reload — resets listening sockets, re-parses config, broadcasts SIGHUP to children
  4. FD events — dispatches LISTEN_FD (new connections), IPC_FD (child messages), and optional STATS_FD (afpstats Unix-socket requests) events
  5. Hint flush — flushes buffered hints when timeout expires (ret == 0) or buffer is full (count ≥ HINT_BUF_SIZE = 128). See Caching Architecture: IPC Hint System for details.

Signal Mask Handling

Signals are unblocked before poll() and blocked immediately after (in the main() event loop):

pthread_sigmask(SIG_UNBLOCK, &sigs, NULL);
ret = poll(asev->fdset, asev->used, poll_timeout);
pthread_sigmask(SIG_BLOCK, &sigs, NULL);

The signal set sigs includes SIGALRM, SIGHUP, SIGUSR1, and SIGCHLD (configured in main() before the event loop).


3. Session Establishment

New connections are handled by dsi_getsession() called from dsi_start() (static function in main.c).

Fork and IPC Setup

%%{ init: { 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'nodeSpacing': 15, 'rankSpacing': 40 } } }%%
sequenceDiagram
    participant P as Parent (main.c)
    participant G as dsi_getsession()
    participant C as Child Process

    P->>G: dsi_start() → dsi_getsession()
    Note over G: socketpair(PF_UNIX, SOCK_STREAM)<br/>→ ipc_fds[0], ipc_fds[1]
    Note over G: pipe() → hint_pipe[0], hint_pipe[1]
    Note over G: setnonblock() on all 4 fds
    G->>G: dsi->proto_open(dsi) [fork()]

    rect rgb(255, 238, 170)
        Note over P: Parent path
        P->>P: close(ipc_fds[1])<br/>close(hint_pipe[0])
        P->>P: server_child_add(pid,<br/>ipc_fds[0], hint_pipe[1])
        P->>P: dsi->proto_close(dsi)
        P->>P: asev_add_fd(child→ipc_fd,<br/>IPC_FD)
    end

    rect rgb(85, 239, 196)
        Note over C: Child path
        C->>C: obj→ipc_fd = ipc_fds[1]<br/>obj→hint_fd = hint_pipe[0]
        C->>C: close(ipc_fds[0])<br/>close(hint_pipe[1])<br/>close(serversock)
        C->>C: server_child_free()
        C->>C: dsi_opensession(dsi)
        C->>C: → return to dsi_start()
        C->>C: configfree() → afp_over_dsi()
    end

Key details from dsi_getsession():


4. Child Worker Architecture

afp_over_dsi() in etc/afpd/afp_dsi.c initializes the directory cache, starts iw_init() with synchronous fallback, applies socket options, and creates the signal self-pipe. It then processes DSI frames and AFP commands until the session exits.

Child Event Loop

flowchart TD
    signals[Drain self-pipe and process deferred signals] --> buffered{DSI data buffered?}
    buffered -->|yes| receive[dsi_stream_receive]
    buffered -->|no| state{Disconnected or dying?}
    state -->|yes| hints[Process pending cache hints]
    hints --> receive
    state -->|no| grant[iw_grant]
    grant --> poll[Poll client socket, hint pipe, signal self-pipe]
    poll --> revoke[iw_revoke before any cache access]
    revoke --> ready[Process signals and hints; check socket events]
    ready --> receive
    receive --> dispatch[DSI command / AFP handler]
    dispatch --> reply[Reply or streamed transfer]
    reply --> signals

Sleeping sessions still poll so SIGURG reconnect notifications can wake them through the self-pipe. process_deferred_signals() executes pending signal actions in main-loop context. Descriptor errors and EOF have explicit paths; a hint pipe failure is not handled identically to a failed signal self-pipe.

AFP Command Dispatch Table

The two 256-entry tables in etc/afpd/switch.c are preauth_switch and postauth_switch. Before login, only afp_login, afp_logincont, afp_logout, and afp_login_ext are available. set_auth_switch() adjusts authenticated commands for protocol version and features; UAMs can also modify handlers.

Replay Cache

replaycache[REPLAYCACHE_SIZE] holds request ID, AFP command, and result, indexed by dsi->clientID % 128. The DSIWrite dispatch path checks it before re-executing a matching write. It stores a result, not an arbitrary full response payload; it should not be described as universal replay protection for every AFP command.

Tickle Handling

alarm_handler() only records alarm_pending and wakes the self-pipe. handle_alarm() performs the actual tickle and timeout work: recent DSI_DATA clears the idle indication; otherwise tickles advance and sleeping, disconnected, dying, or active sessions follow their state-specific limits. The parser converts sleep time and disconnect time from hours to tickles. Transport disconnects can retain authenticated sessions for primary reconnect.


5. Open Fork Management

AFP file access is tracked through the open fork table defined in fork.h and ofork.c.

struct ofork

Defined in fork.h:

struct file_key {
    dev_t       dev;
    ino_t       inode;
};

struct ofork {
    struct file_key     key;           /* dev_t dev + ino_t inode */
    struct adouble      *of_ad;        /* AppleDouble metadata handle */
    struct vol          *of_vol;       /* volume */
    cnid_t              of_did;        /* parent directory CNID */
    uint16_t            of_refnum;     /* AFP fork reference number */
    int                 of_flags;      /* AFPFORK_* flags */
    const unsigned char *of_virtual_data;  /* virtual file data pointer */
    off_t               of_virtual_len;    /* virtual file length */
    struct ofork        **prevp, *next;    /* hash chain pointers */
};

Hash Table

Key Operations

Function File Purpose
of_alloc() ofork.c Allocate fork, hash by dev/ino, assign refnum
of_find() ofork.c Lookup by refnum (array index + refnum verification)
of_findname() ofork.c Lookup by dev/ino from stat() result
of_dealloc() ofork.c Unhash, decrement ad_refcount, free if zero
of_closefork() ofork.c Full close: flush dirty metadata, send IPC cache hints, ad_close

AFPFORK Flags

Defined in fork.h:

Flag Value Purpose
AFPFORK_DATA 1<<0 Open data fork
AFPFORK_RSRC 1<<1 Open resource fork
AFPFORK_META 1<<2 Open metadata
AFPFORK_DIRTY 1<<3 AD metadata modified (set in write_mark() in fork.c when ad_meta_fileno != -1)
AFPFORK_ACCRD 1<<4 Read access
AFPFORK_ACCWR 1<<5 Write access
AFPFORK_MODIFIED 1<<6 Data written (set in write_mark(), triggers FCE and IPC hints)
AFPFORK_ERROR 1<<7 Error opening fork
AFPFORK_VIRTUAL 1<<8 Virtual file fork

Refcounting

The struct adouble has a reference count (ad_refcount). When multiple forks of the same file are open simultaneously, they share the same adouble via ad_ref(). of_dealloc() decrements the refcount and only frees the adouble when it reaches zero.


6. Caching Subsystem

Full documentation: Caching Architecture

Each afpd worker child maintains its own private, per-process three-tier cache hierarchy. There is no shared memory between workers — cross-process coherence is handled by the IPC cache hint relay system.

Tier What Key File
Dircache struct dir entries (files & dirs) with stat fields, ARC eviction dircache.c
AD cache AppleDouble metadata (FinderInfo, dates, attributes) inline in struct dir ad_cache.c
Rfork cache Resource fork content buffers with byte-budget LRU ad_cache.c
IPC hints Cross-process invalidation via parent relay server_ipc.c

The dircache supports both ARC (Adaptive Replacement Cache) and LRU modes. The IPC hint system uses batched, priority-sorted, PIPE_BUF-safe writes via dedicated hint pipes. See the Caching Architecture page for complete details including:


7. Signal Handling

Parent Process Signals

afp_goaway() in etc/afpd/main.c handles parent shutdown, login shutdown, reload, and child-reaping flags. SIGPIPE and SIGXFSZ are ignored. The poll loop reaps SIGCHLD children before accessing their table/fds, saves errno across reaping, and reparses configuration when reloadconfig is set. Single-user mode ignores SIGHUP.

Child Process Signals

afp_over_dsi_sighandlers() installs request-recording handlers. Main-loop processing performs the corresponding work after revoking idle-worker access:

Signal Installed handler Main-loop action
SIGHUP afp_dsi_reload() Record/reload volume configuration in normal mode
SIGURG afp_dsi_transfer_handler() handle_transfer_session(): receive the transferred socket and resume the session
SIGTERM/SIGQUIT afp_dsi_die_handler() process_deferred_signals(): close/exit with the recorded cause
SIGUSR1 afp_dsi_timedown_handler() handle_timedown(): initiate the five-minute shutdown path
SIGUSR2 afp_dsi_getmesg_handler() handle_getmesg(): deliver server-message attention
SIGINT afp_dsi_debug() Deferred cache dump and debug-log toggle
SIGALRM alarm_handler() handle_alarm(): tickle and timeout processing

The signal channel is implemented by atalk_sigpipe_* in libatalk/util/sigpipe.c. Reconnect no longer uses siglongjmp() to escape a signal handler. A shutdown function being called from the main loop must not be confused with the installed asynchronous handler.


8. Idle Worker

The background worker in etc/afpd/idle_worker.c frees queued invalidated directory entries and scans deferred child-removal chains. Its API is in etc/afpd/idle_worker.h.

The main thread owns caches while serving commands and processing hints. iw_note_work() records newly queued work. Immediately before blocking in poll(), iw_grant() permits background access only if work is pending. After every poll return, including EINTR, iw_revoke() waits for the worker to release access before the main thread touches cache state.

The handshake uses sequentially consistent iw_has_work, iw_can_work, and iw_is_working atomics. The worker publishes that it is working, then rechecks the grant before touching queues. This ordering prevents main/worker cache access from overlapping; acquire/release stores alone would not establish the required crossing guarantee.

API Contract
iw_init() Start after fork and dircache initialization; failure enables synchronous cleanup
iw_note_work() Main-thread producer hook after enqueue
iw_grant() / iw_revoke() Grant the poll window and reclaim exclusive access
iw_revoke_signal_safe() Revoke without waiting; exit paths only
iw_shutdown() Join the worker and abandon queued work because the session exits
iw_is_active() / iw_grant_active() Check worker presence/grant
iw_log_stats() Log noted work, grants, completed/interrupted/aborted cycles and cleanup counts

IW_WAKE_MS is 10 ms and the worker sleeps with nanosleep(), not a 1 ms condition-variable wait. Revocation first uses a bounded relax spin, then 100 µs sleeps while waiting. The worker blocks asynchronous signals but leaves fault signals deliverable and installs thread fault reporting.

If ATOMIC_INT_LOCK_FREE != 2, the module uses stubs and synchronous cleanup. When the worker is active, AFP command cleanup leaves queued frees to the next granted window.


9. Child Process Table

The child process table is managed by server_child.c with types in server_child.h.

server_child_t

typedef struct {
    int             servch_count;     /* Current active session count */
    int             servch_nsessions; /* Maximum allowed sessions */
    afp_child_t    *servch_table[CHILD_HASHSIZE]; /* Hash table (32 buckets) */
} server_child_t;

afp_child_t

typedef struct afp_child {
    pid_t     afpch_pid;       /* Worker process PID */
    uid_t     afpch_uid;       /* Connected user ID */
    int       afpch_valid;     /* 1 if clientid is set */
    int       afpch_killed;    /* 1 if SIGTERM already sent */
    uint32_t  afpch_boottime;  /* Client boot time */
    time_t    afpch_logintime; /* Time child was added */
    uint32_t  afpch_idlen;     /* Client ID length */
    char     *afpch_clientid;  /* Client ID string */
    size_t    afpch_sessiontoken_len; /* Reconnect token length */
    char     *afpch_sessiontoken;     /* Opaque random reconnect token */
    int       afpch_ipc_fd;    /* IPC socketpair (parent end) */
    int       afpch_hint_fd;   /* Hint pipe (parent write end) */
    char     *afpch_hostname;  /* Client hostname/address */
    int16_t   afpch_state;     /* Session state (active/sleeping/disconnected) */
    char     *afpch_volumes;   /* Mounted volumes string */
    struct afp_child **afpch_prevp;
    struct afp_child *afpch_next;
} afp_child_t;

Hash Function

CHILD_HASHSIZE = 32. Hash: ((pid >> 8) ^ pid) & (CHILD_HASHSIZE - 1) (defined as HASH() macro in server_child.c).

Key Operations

Function Purpose
server_child_alloc() Allocate and initialize server_child_t with session limit
server_child_add() Add child with PID, IPC fd, hint fd; enforces session limit
server_child_remove() Unhash child, close IPC + hint fds, free memory, return IPC fd
server_child_free() Free all children (called by child process after fork)
server_child_kill() Send signal to all children
server_child_resolve() Find child by PID
server_child_transfer_session() Primary reconnect: write DSI ID + send_fd() + SIGURG

Reconnect token tracking

IPC_SESSIONTOKEN publishes an opaque 8-byte token to server_child_set_session_token(). Session transfer checks token and UID in libatalk/util/server_child.c. Current tokens are random, not PID-derived. See Authentication.

Footnotes

Generated from the Netatalk GitHub Wiki

Last updated 2026-10-21