netatalk.io

Dev Docs Network Protocols

Network Protocol Stack

Netatalk supports two network protocol stacks for AFP (Apple Filing Protocol) file sharing. The primary stack, DSI (Data Stream Interface), carries AFP over TCP/IP and is used by all modern clients. The legacy stack, ASP (AppleTalk Session Protocol), carries AFP over DDP and serves vintage Macintosh systems that predate TCP/IP networking.

This page documents both stacks—from wire format and session management through AFP command dispatch—based on the actual implementation in the Netatalk source tree.

Protocol Stack Overview

%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '14px', 'primaryColor': '#4a90d9', 'primaryTextColor': '#fff', 'lineColor': '#5c6370', 'secondaryColor': '#7c4dff', 'tertiaryColor': '#e8f5e9'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 30}}}%%
graph TB
    subgraph "AFP over TCP/IP — Modern"
        A["AFP Commands<br/><i>include/atalk/afp.h</i>"]
        B["DSI Session Layer<br/><i>libatalk/dsi/</i>"]
        C["TCP/IP Transport<br/>Port 548"]
    end
    subgraph "AFP over AppleTalk — Legacy"
        D["AFP Commands<br/><i>include/atalk/afp.h</i>"]
        E["ASP Session Layer<br/><i>libatalk/asp/</i>"]
        F["ATP Transaction Layer<br/><i>libatalk/atp/</i>"]
        G["DDP Network Layer<br/><i>etc/atalkd/</i>"]
    end
    A --> B --> C
    D --> E --> F --> G
    style A fill:#4a90d9,color:#fff
    style B fill:#7c4dff,color:#fff
    style C fill:#43a047,color:#fff
    style D fill:#4a90d9,color:#fff
    style E fill:#7c4dff,color:#fff
    style F fill:#e65100,color:#fff
    style G fill:#c62828,color:#fff

The protocol identifier constants are defined in include/atalk/afp.h:

#define AFPPROTO_ASP           1
#define AFPPROTO_DSI           2

DSI Protocol (AFP over TCP)

DSI is the session layer that frames AFP commands for transport over TCP/IP. It handles connection establishment, request/response message boundaries, keep-alive tickles, server attention notifications, and flow control through quantum negotiation. The DSI implementation lives in libatalk/dsi/ and is the only actively developed transport in Netatalk.

Source Files

File Purpose
include/atalk/dsi.h All DSI structs, constants, flags, and function prototypes
libatalk/dsi/dsi_init.c dsi_init() — allocate and initialize a DSI handle
libatalk/dsi/dsi_tcp.c dsi_tcp_init() — create TCP listening socket; dsi_tcp_open() — accept and fork child
libatalk/dsi/dsi_getsess.c dsi_getsession() — session creation with fork, IPC pipe setup
libatalk/dsi/dsi_opensess.c dsi_opensession() — parse client options, send server quantum
libatalk/dsi/dsi_stream.c dsi_stream_send(), dsi_stream_receive(), dsi_stream_read(), dsi_stream_write()
libatalk/dsi/dsi_read.c dsi_readinit(), dsi_read(), dsi_readdone() — server→client streaming
libatalk/dsi/dsi_write.c dsi_writeinit(), dsi_write(), dsi_writeflush() — client→server streaming
libatalk/dsi/dsi_cmdreply.c dsi_cmdreply() — send AFP reply with error code
libatalk/dsi/dsi_tickle.c dsi_tickle() — send keep-alive tickle packet
libatalk/dsi/dsi_attn.c dsi_attention() — send server attention to client
libatalk/dsi/dsi_close.c dsi_close() — send DSI CloseSession and free resources
libatalk/dsi/dsi_getstat.c dsi_getstatus() — reply with server status block

Wire Format — struct dsi_block

Every DSI message starts with a 16-byte header defined in include/atalk/dsi.h:

#define DSI_BLOCKSIZ 16
struct dsi_block {
    uint8_t dsi_flags;       /* packet type: request or reply */
    uint8_t dsi_command;     /* command */
    uint16_t dsi_requestID;  /* request ID */
    union {
        uint32_t dsi_code;   /* error code */
        uint32_t dsi_doff;   /* data offset */
    } dsi_data;
    uint32_t dsi_len;        /* total data length */
    uint32_t dsi_reserved;   /* reserved field */
};

The header is serialized by dsi_header_pack_reply() and deserialized by dsi_stream_receive(), both in libatalk/dsi/dsi_stream.c. All multi-byte fields use network byte order, as stated in the header’s documentation comment:

CONVENTION: anything with a dsi_ prefix is kept in network byte order.

Byte:  0     1     2-3          4-7                  8-11            12-15
     +-----+-----+------------+--------------------+-----------------+---------+
     |Flags| Cmd | Request ID | Code / Data Offset | Total Data Len | Reserved|
     +-----+-----+------------+--------------------+-----------------+---------+

DSI Flags

Defined in include/atalk/dsi.h:

Constant Value Meaning
DSIFL_REQUEST 0x00 Client-to-server request
DSIFL_REPLY 0x01 Server-to-client reply

DSI Commands

Defined in include/atalk/dsi.h:

Constant Value Description Handler
DSIFUNC_CLOSE 1 Close session dsi_close() in dsi_close.c
DSIFUNC_CMD 2 AFP command dispatched in afp_over_dsi() in afp_dsi.c
DSIFUNC_STAT 3 Get server status dsi_getstatus() in dsi_getstat.c
DSIFUNC_OPEN 4 Open session dsi_opensession() in dsi_opensess.c
DSIFUNC_TICKLE 5 Keep-alive dsi_tickle() in dsi_tickle.c
DSIFUNC_WRITE 6 Write data (FPWrite) dispatched in afp_over_dsi() in afp_dsi.c
DSIFUNC_ATTN 8 Server attention dsi_attention() in dsi_attn.c

Value 7 is deliberately skipped (it was ASPFUNC_WRTCONT in the AppleTalk equivalent). DSIFUNC_MAX is defined as 8 for bounds checking during connection setup.

DSI Error Codes

Defined in include/atalk/dsi.h:

Constant Value Description
DSIERR_OK 0x0000 Success
DSIERR_BADVERS 0xfbd6 Bad version
DSIERR_BUFSMALL 0xfbd5 Buffer too small
DSIERR_NOSESS 0xfbd4 No session
DSIERR_NOSERV 0xfbd3 No server
DSIERR_PARM 0xfbd2 Parameter error
DSIERR_SERVBUSY 0xfbd1 Server busy
DSIERR_SESSCLOS 0xfbd0 Session closed
DSIERR_SIZERR 0xfbcf Size error
DSIERR_TOOMANY 0xfbce Too many connections
DSIERR_NOACK 0xfbcd No acknowledgement

DSI Session Options

Negotiated during DSIFUNC_OPEN in dsi_opensession() (dsi_opensess.c). Defined in include/atalk/dsi.h:

Constant Value Description
DSIOPT_SERVQUANT 0x00 Server request quantum
DSIOPT_ATTNQUANT 0x01 Attention quantum
DSIOPT_REPLCSIZE 0x02 AFP replay cache size

Quantum Constants from include/atalk/dsi.h:

Constant Value Description
DSI_DEFQUANT 2 Default attention quantum
DSI_SERVQUANT_DEF 0x100000L (1 MB) Default server quantum
DSI_SERVQUANT_MIN 32000 Minimum server quantum
DSI_SERVQUANT_MAX 0xffffffff Maximum server quantum
DSI_AFPOVERTCP_PORT 548 Default TCP listening port

DSI Session Structure

The main session handle, defined in include/atalk/dsi.h:

#define DSI_DATASIZ       65536

typedef struct DSI {
    struct DSI *next;             /* multiple listening addresses */
    AFPObj   *AFPobj;
    int      statuslen;
    char     status[1400];        /* server status block */
    char     *signature;
    struct dsi_block        header;
    struct sockaddr_storage server, client;
    struct itimerval        timer;
    int      tickle;              /* tickle count */
    int      in_write;            /* signal blocking during writes */
    int      msg_request;         /* pending message to client */
    int      down_request;        /* pending SIGUSR1 shutdown */

    uint32_t attn_quantum, datasize, server_quantum;
    uint16_t serverID, clientID;
    uint8_t  *commands;           /* DSI receive buffer (server_quantum bytes) */
    uint8_t  data[DSI_DATASIZ];   /* DSI reply buffer (64 KB) */
    size_t   datalen, cmdlen;
    off_t    read_count, write_count;
    uint32_t flags;               /* DSI state flags */
    int      socket;              /* AFP session socket */
    int      serversock;          /* listening socket */

    /* readahead buffer for dsi_peek */
    size_t   dsireadbuf;          /* multiplier for readahead buffer */
    char     *buffer;             /* buffer start */
    char     *start;              /* current read head */
    char     *eof;                /* end of buffered data */
    char     *end;                /* buffer end */

    /* protocol-specific function pointers */
    pid_t (*proto_open)(struct DSI *);
    void (*proto_close)(struct DSI *);
} DSI;

The commands buffer is allocated in dsi_init_buffer() (dsi_tcp.c) to server_quantum bytes. The readahead buffer is dsireadbuf * server_quantum bytes.

DSI State Flags

Defined in include/atalk/dsi.h. These are bitmask values set on dsi->flags:

Flag Value Meaning
DSI_DATA 1 << 0 Received a DSI command
DSI_RUNNING 1 << 1 AFP command in progress
DSI_SLEEPING 1 << 2 Sleeping after FPZzz
DSI_EXTSLEEP 1 << 3 Extended sleep mode
DSI_DISCONNECTED 1 << 4 Disconnected after socket error
DSI_DIE 1 << 5 SIGUSR1 received, shutting down in 5 min
DSI_NOREPLY 1 << 6 Streaming read generates own reply
DSI_RECONSOCKET 1 << 7 New socket from primary reconnect
DSI_RECONINPROG 1 << 8 Reconnection in progress
DSI_AFP_LOGGED_OUT 1 << 9 Client called FPLogout

The flags are tracked and logged in alarm_handler() (afp_dsi.c).

DSI Session State Machine

%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '14px', 'primaryColor': '#4a90d9', 'primaryTextColor': '#fff', 'lineColor': '#5c6370', 'secondaryColor': '#7c4dff', 'tertiaryColor': '#e8f5e9'}, 'flowchart': {'nodeSpacing': 25, 'rankSpacing': 25}}}%%
stateDiagram-v2
    [*] --> TCPListen: dsi_tcp_init()
    TCPListen --> ChildForked: accept() + fork()\nin dsi_tcp_open()
    ChildForked --> OpenSession: DSIFUNC_OPEN\n→ dsi_opensession()
    ChildForked --> StatusReply: DSIFUNC_STAT\n→ dsi_getstatus() → exit
    OpenSession --> Running: DSI_RUNNING set\nper command
    Running --> Running: DSIFUNC_CMD\nDSIFUNC_WRITE
    Running --> Sleeping: FPZzz\n→ DSI_SLEEPING
    Running --> ExtSleep: Extended\n→ DSI_EXTSLEEP
    Running --> Dying: SIGUSR1\n→ DSI_DIE
    Running --> Disconnected: socket error\n→ dsi_disconnect()
    Running --> LoggedOut: FPLogout\n→ DSI_AFP_LOGGED_OUT
    Running --> Reconnecting: SIGURG\n→ DSI_RECONSOCKET
    Sleeping --> Running: client data\n→ clear DSI_SLEEPING
    Sleeping --> Disconnected: tickle timeout
    ExtSleep --> Running: wakeup
    Disconnected --> Running: primary reconnect\nsucceeds
    Disconnected --> [*]: reconnect timer\nexpires → exit
    Dying --> [*]: SIGALRM after 5 min\n→ afp_dsi_die()
    LoggedOut --> [*]: next EOF → exit
    Reconnecting --> Running: DSI_RECONSOCKET\nhandled
    Running --> [*]: DSIFUNC_CLOSE\n→ dsi_close()

Tickle Mechanism

The keep-alive system uses SIGALRM with a periodic interval timer. Each time the timer fires, alarm_handler() in etc/afpd/afp_dsi.c runs through this decision chain:

  1. If DSI_DATA is set, the client recently sent traffic. Clear the flag and return—no tickle needed.
  2. Increment the dsi->tickle counter (counts consecutive idle intervals).
  3. If DSI_SLEEPING: compare tickle against options.sleep. Terminate the session if the sleep limit is exceeded.
  4. If DSI_DISCONNECTED: compare tickle against options.disconnected. Terminate if the reconnect window has expired.
  5. If tickle >= options.timeout: the client has gone silent too long. Enter the disconnected state via dsi_disconnect() in dsi_stream.c.
  6. Otherwise: send a keep-alive tickle via dsi_tickle() in dsi_tickle.c.

dsi_tickle() constructs a bare 16-byte DSI header (flags=DSIFL_REQUEST, command=DSIFUNC_TICKLE, no data payload) and writes it with DSI_NOWAIT so it does not block. The function is a no-op when DSI_SLEEPING is set or dsi->in_write is non-zero, preventing corruption of an in-progress multi-packet transfer.

Stream Operations

%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '14px', 'primaryColor': '#4a90d9', 'primaryTextColor': '#fff', 'lineColor': '#5c6370', 'secondaryColor': '#7c4dff', 'tertiaryColor': '#e8f5e9'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 30}}}%%
graph TB
    subgraph "Receive Path"
        A["dsi_stream_receive()"] --> B["dsi_buffered_stream_read()<br/>Read 16-byte header"]
        B --> C["from_buf()<br/>Check readahead buffer"]
        C --> D{Data in buffer?}
        D -->|Yes| E["Return buffered bytes"]
        D -->|No| F["recv() from socket"]
        B --> G["dsi_stream_read()<br/>Read command payload"]
        G --> H["buf_read() loop"]
    end
    subgraph "Send Path"
        I["dsi_stream_send()"] --> J["dsi_header_pack_reply()<br/>Pack 16-byte header"]
        J --> K["writev()<br/>Header + data in one syscall"]
        K --> L{EAGAIN?}
        L -->|Yes| M["dsi_peek()<br/>Read to unblock client"]
        M --> K
        L -->|No| N["Write complete"]
    end
    style A fill:#4a90d9,color:#fff
    style I fill:#43a047,color:#fff
    style C fill:#7c4dff,color:#fff
    style M fill:#e65100,color:#fff
    style E fill:#81c784,color:#000
    style N fill:#81c784,color:#000
    style F fill:#ffb74d,color:#000
    style H fill:#ffb74d,color:#000

Receive pathdsi_stream_receive() in dsi_stream.c first reads the 16-byte header through dsi_buffered_stream_read(), which checks the readahead buffer before falling through to the socket. After parsing all header fields, cmdlen is clamped to server_quantum to prevent buffer overflows. The command payload is then read into dsi->commands. For DSIFUNC_WRITE requests, the dsi_doff (data offset) field determines how many bytes are command parameters versus write data—write data is consumed later by the AFP write handler, not here.

Send pathdsi_stream_send() serializes the 16-byte header into a stack buffer, then uses writev() to transmit the header and reply data as a single scatter/gather I/O operation, avoiding an extra copy. During the write, block_sig() increments dsi->in_write to prevent dsi_tickle() and dsi_attention() from interleaving packets on the socket.

Deadlock avoidance — When send() returns EAGAIN (socket buffer full), dsi_peek() uses select() to simultaneously monitor the socket for write-readiness and read-readiness. If the client has pending data (e.g., a tickle), dsi_peek() reads it into the readahead buffer. This breaks a potential deadlock where both client and server have full send buffers and neither can make progress.

Readahead Buffer

Allocated in dsi_init_buffer() in libatalk/dsi/dsi_tcp.c:

dsi->commands = malloc(dsi->server_quantum);
dsi->buffer = malloc(dsi->dsireadbuf * dsi->server_quantum);
dsi->start = dsi->buffer;
dsi->eof = dsi->buffer;
dsi->end = dsi->buffer + (dsi->dsireadbuf * dsi->server_quantum);

With default quantum of 1 MB and dsireadbuf of 12, this creates a 12 MB readahead buffer. dsi_peek() logs a warning when the buffer is full:

dsi_peek: readahead buffer is full, possibly increase -dsireadbuf option

Streaming Data Transfer (Read/Write)

Server→Client reads (AFP FPRead):

  1. dsi_readinit() — sets DSI_NOREPLY, sends DSI header + initial buffer via dsi_stream_send()
  2. dsi_read() — sends subsequent chunks via dsi_stream_write(), decrements dsi->datasize
  3. dsi_readdone() — decrements in_write counter
  4. With sendfile: dsi_stream_read_file() — zero-copy from file descriptor, platform-specific (sendfile/sendfilev)

All in libatalk/dsi/dsi_read.c and libatalk/dsi/dsi_stream.c.

Client→Server writes (AFP FPWrite):

  1. dsi_writeinit() — calculates datasize from dsi_len - dsi_doff, drains readahead buffer first
  2. dsi_write() — reads chunks from socket via dsi_stream_read(), decrements datasize
  3. dsi_writeflush() — discards any unread remaining data

All in libatalk/dsi/dsi_write.c.

Replay Cache

Defined in etc/afpd/afp_dsi.c:

typedef struct {
    uint16_t DSIreqID;
    uint8_t  AFPcommand;
    uint32_t result;
} rc_elem_t;

static rc_elem_t replaycache[REPLAYCACHE_SIZE];

REPLAYCACHE_SIZE is 128 (from include/atalk/afp.h). Indexed by dsi->clientID % REPLAYCACHE_SIZE. When a request ID and command match a cache entry, the cached result is returned without re-executing the AFP function. The cache size is communicated to the client as DSIOPT_REPLCSIZE during dsi_opensession().

AFP Protocol Layer

AFP defines the application-level file sharing commands that are transported over DSI (or ASP for legacy AppleTalk). In every DSI request, the first byte of dsi->commands identifies the AFP command to execute.

Source Files

File Purpose
include/atalk/afp.h All AFP command codes, error codes, attention codes, server flags
include/atalk/globals.h AFPCmd function pointer typedef, AFPObj structure
etc/afpd/switch.c Pre-auth and post-auth command dispatch tables
etc/afpd/afp_dsi.c afp_over_dsi() — main DSI command loop

AFP Command Dispatch

The AFPCmd type is defined in include/atalk/globals.h:

typedef int (*AFPCmd)(AFPObj *obj, char *ibuf, size_t ibuflen, char *rbuf, size_t *rbuflen);

Two 256-entry dispatch tables in etc/afpd/switch.c:

The global pointer afp_switch initially references preauth_switch, restricting unauthenticated clients to login commands only. After successful authentication, it is redirected to postauth_switch to unlock the full command set. UAM modules can also modify individual dispatch entries through uam_afpserver_action().

AFP Command Codes

All defined in include/atalk/afp.h. Key commands by category:

Session & Authentication:

Constant Value Function in postauth_switch
AFP_LOGIN 18 afp_login
AFP_LOGINCONT 19 afp_logincont
AFP_LOGOUT 20 afp_logout
AFP_LOGIN_EXT 63 afp_login_ext
AFP_GETSRVINFO 15 afp_getsrvrinfo
AFP_GETSRVPARAM 16 afp_getsrvrparms
AFP_GETSRVRMSG 38 afp_getsrvrmesg
AFP_CHANGEPW 36 afp_changepw
AFP_GETUSERINFO 37 afp_getuserinfo
AFP_MAPID 21 afp_mapid
AFP_MAPNAME 22 afp_mapname

Volume Operations:

Constant Value Function
AFP_OPENVOL 24 afp_openvol
AFP_CLOSEVOL 2 afp_closevol
AFP_GETVOLPARAM 17 afp_getvolparams
AFP_SETVOLPARAM 32 afp_setvolparams
AFP_FLUSH 10 afp_flush

File/Directory Operations:

Constant Value Function
AFP_OPENDIR 25 afp_opendir
AFP_CLOSEDIR 3 afp_closedir
AFP_CREATEDIR 6 afp_createdir
AFP_CREATEFILE 7 afp_createfile
AFP_DELETE 8 afp_delete
AFP_RENAME 28 afp_rename
AFP_MOVE 23 afp_moveandrename
AFP_ENUMERATE 9 afp_enumerate
AFP_GETFLDRPARAM 34 afp_getfildirparams
AFP_SETFLDRPARAM 35 afp_setfildirparams
AFP_SETDIRPARAM 29 afp_setdirparams
AFP_SETFILEPARAM 30 afp_setfilparams

Fork (Read/Write) Operations:

Constant Value Function
AFP_OPENFORK 26 afp_openfork
AFP_CLOSEFORK 4 afp_closefork
AFP_READ 27 afp_read
AFP_WRITE 33 afp_write
AFP_FLUSHFORK 11 afp_flushfork
AFP_GETFORKPARAM 14 afp_getforkparams
AFP_SETFORKPARAM 31 afp_setforkparams
AFP_BYTELOCK 1 afp_bytelock

AFP 3.0 Extensions:

Constant Value Description
AFP_BYTELOCK_EXT 59 64-bit byte range locks
AFP_READ_EXT 60 64-bit read
AFP_WRITE_EXT 61 64-bit write
AFP_ENUMERATE_EXT 66 Extended enumerate
AFP_CATSEARCH_EXT 67 Extended catalog search
AFP_GETSESSTOKEN 64 Get session token (reconnect)
AFP_DISCTOLDSESS 65 Disconnect old session

AFP 3.1+ Extensions:

Constant Value Description
AFP_ENUMERATE_EXT2 68 Further extended enumerate
AFP_SYNCDIR 78 Sync directory
AFP_SYNCFORK 79 Sync fork
AFP_ZZZ 122 Sleep/wake
AFP_SPOTLIGHT_PRIVATE 76 Spotlight search

AFP 3.2 Extensions (Extended Attributes / ACLs):

Constant Value Description
AFP_GETEXTATTR 69 Get extended attribute
AFP_SETEXTATTR 70 Set extended attribute
AFP_REMOVEATTR 71 Remove extended attribute
AFP_LISTEXTATTR 72 List extended attributes
AFP_GETACL 73 Get ACL
AFP_SETACL 74 Set ACL
AFP_ACCESS 75 Check access

AFP Error Codes

Defined in include/atalk/afp.h:

Constant Value Description
AFP_OK 0 Success
AFPERR_ACCESS -5000 Permission denied
AFPERR_AUTHCONT -5001 Authentication continuing
AFPERR_BADUAM -5002 UAM does not exist
AFPERR_BADVERS -5003 Bad AFP version number
AFPERR_BITMAP -5004 Invalid bitmap
AFPERR_DENYCONF -5006 Synchronization lock conflict
AFPERR_DIRNEMPT -5007 Directory not empty
AFPERR_DFULL -5008 Disk full
AFPERR_EOF -5009 End of file
AFPERR_BUSY -5010 File busy
AFPERR_NOITEM -5012 Item not found
AFPERR_LOCK -5013 Lock error
AFPERR_MISC -5014 Miscellaneous error
AFPERR_EXIST -5017 Object already exists
AFPERR_NOOBJ -5018 Object not found
AFPERR_PARAM -5019 Parameter error
AFPERR_SESSCLOS -5022 Session closed
AFPERR_NOTAUTH -5023 User not authenticated
AFPERR_NOOP -5024 Command not supported
AFPERR_VLOCK -5031 Volume locked
AFPERR_MAXSESS -1068 Maximum sessions reached

AFP Server Flags

Defined in include/atalk/afp.h, these are advertised in the server status block:

Flag Bit Description
AFPSRVRINFO_COPY 0 Supports CopyFile
AFPSRVRINFO_PASSWD 1 Supports change password
AFPSRVRINFO_NOSAVEPASSWD 2 Don’t allow save password
AFPSRVRINFO_SRVMSGS 3 Supports server messages
AFPSRVRINFO_SRVSIGNATURE 4 Supports server signature
AFPSRVRINFO_TCPIP 5 Supports TCP/IP
AFPSRVRINFO_SRVNOTIFY 6 Supports server notifications
AFPSRVRINFO_SRVRECONNECT 7 Supports reconnect
AFPSRVRINFO_SRVUTF8 9 Supports UTF-8 server name (AFP 3.1)
AFPSRVRINFO_UUID 10 Supports UUIDs
AFPSRVRINFO_EXTSLEEP 11 Supports extended sleep

AFP Attention Codes

Defined in include/atalk/afp.h, sent via DSIFUNC_ATTN:

Constant Bit Description
AFPATTN_SHUTDOWN 15 Server shutting down
AFPATTN_CRASH 14 Server crashed
AFPATTN_MESG 13 Server has message
AFPATTN_NORECONNECT 12 Don’t reconnect
AFPATTN_VOLCHANGED 0 Volume changed (extended)
AFPATTN_TIME(x) 0-11 Time in minutes (shutdown countdown)

AFP Command Processing Loop

The main event loop afp_over_dsi() in etc/afpd/afp_dsi.c:

%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '14px', 'primaryColor': '#4a90d9', 'primaryTextColor': '#fff', 'lineColor': '#5c6370', 'secondaryColor': '#7c4dff', 'tertiaryColor': '#e8f5e9'}, 'flowchart': {'nodeSpacing': 25, 'rankSpacing': 25}}}%%
flowchart TD
    A["poll() on socket + hint_fd"] --> B["dsi_stream_receive()"]
    B --> C{cmd?}
    C -->|"0 — error"| D{"DSI_RECONSOCKET?"}
    D -->|Yes| A
    D -->|No| E["dsi_disconnect()"]
    E --> F["pause() until\nSIGURG / SIGALRM"]
    F --> A
    C -->|DSIFUNC_CLOSE| G["afp_dsi_close() → exit"]
    C -->|DSIFUNC_TICKLE| H["Reset DSI_DATA flag"]
    C -->|DSIFUNC_CMD| I["Extract function byte"]
    I --> J{"Replay cache\nhit?"}
    J -->|Yes| K["Return cached result"]
    J -->|No| L["afp_switch&#91;function&#93;()"]
    L --> M["dsi_cmdreply()"]
    K --> M
    C -->|DSIFUNC_WRITE| N["afp_switch&#91;function&#93;()"]
    N --> O["dsi_wrtreply()"]
    C -->|DSIFUNC_ATTN| P["Ignore — client ack"]
    M --> Q["pending_request()\nfce_pending_events()"]
    O --> Q
    H --> Q
    P --> Q
    Q --> A
    style A fill:#4a90d9,color:#fff
    style L fill:#43a047,color:#fff
    style G fill:#c62828,color:#fff
    style E fill:#e65100,color:#fff
    style M fill:#7c4dff,color:#fff
    style O fill:#7c4dff,color:#fff
    style K fill:#81c784,color:#000
    style N fill:#43a047,color:#fff

Signal Handlers

Installed by afp_over_dsi_sighandlers() in etc/afpd/afp_dsi.c:

Signal Handler Purpose
SIGALRM alarm_handler() Tickle timer, sleep/disconnect monitoring
SIGTERM afp_dsi_die() Clean shutdown
SIGQUIT afp_dsi_die() Clean shutdown
SIGUSR1 afp_dsi_timedown() Scheduled shutdown in 5 min
SIGUSR2 afp_dsi_getmesg() Send server message attention
SIGHUP afp_dsi_reload() Reload configuration
SIGURG afp_dsi_transfer_session() Primary reconnect — receive new socket via IPC
SIGINT afp_dsi_debug() Toggle max debug logging

Connection Lifecycle

%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '14px', 'primaryColor': '#4a90d9', 'primaryTextColor': '#fff', 'lineColor': '#333333', 'secondaryColor': '#7c4dff', 'tertiaryColor': '#e8f5e9', 'actorBkg': '#4a90d9', 'actorTextColor': '#ffffff', 'actorBorder': '#2e6da4', 'actorLineColor': '#333333', 'signalColor': '#333333', 'signalTextColor': '#333333', 'noteBkgColor': '#e8f5e9', 'noteTextColor': '#333333', 'noteBorderColor': '#43a047', 'activationBkgColor': '#dce4f0', 'activationBorderColor': '#4a90d9', 'loopTextColor': '#333333', 'labelBoxBkgColor': '#f5f5f5', 'labelBoxBorderColor': '#cccccc', 'labelTextColor': '#333333'}, 'sequence': {'noteMargin': 8, 'mirrorActors': false}}}%%
sequenceDiagram
    participant C as AFP Client
    participant M as afpd Master
    participant W as afpd Worker

    C->>M: TCP SYN to port 548
    M-->>C: TCP SYN-ACK
    C->>M: TCP ACK

    Note over M: accept() in dsi_tcp_open()
    Note over M: fork() child process
    M->>W: Child inherits socket

    C->>W: DSI OpenSession (DSIFUNC_OPEN=4)<br/>with DSIOPT_ATTNQUANT
    Note over W: dsi_opensession() parses options
    W-->>C: DSI OpenSession Reply<br/>DSIOPT_SERVQUANT (1MB)<br/>DSIOPT_REPLCSIZE (128)

    C->>W: DSI Command (DSIFUNC_CMD=2)<br/>AFP FPLogin (cmd=18)
    Note over W: preauth_switch[18] = afp_login
    W-->>C: DSI Reply: AFP_OK or AFPERR_AUTHCONT
    Note over W: Switch afp_switch → postauth_switch

    loop AFP Operations
        C->>W: DSI Command (DSIFUNC_CMD=2)<br/>AFP command byte
        Note over W: postauth_switch[cmd]()
        W-->>C: DSI Reply with result
    end

    par Keep-alive
        W->>C: DSI Tickle (DSIFUNC_TICKLE=5)
        C->>W: DSI Tickle (DSIFUNC_TICKLE=5)
    end

    C->>W: DSI CloseSession (DSIFUNC_CLOSE=1)
    Note over W: afp_dsi_close() → dsi_close()
    W-->>C: DSI Close → TCP FIN

ASP Protocol (AFP over AppleTalk)

ASP is the legacy session layer for AFP over AppleTalk networks. It provides the same session semantics as DSI (open, command, write, tickle, attention, close) but runs over ATP/DDP rather than TCP. ASP requires kernel-level AppleTalk support and is only compiled when NO_DDP is not defined.

Source Files

File Purpose
include/atalk/asp.h ASP struct, constants, function prototypes
libatalk/asp/asp_getsess.c asp_getsession() — handle open/stat/tickle, fork child
libatalk/asp/asp_close.c asp_close() — send final ATP response, close handle
libatalk/asp/asp_cmdreply.c Send AFP reply over ASP
libatalk/asp/asp_getreq.c Get next AFP request from ATP
libatalk/asp/asp_write.c Write continuation for FPWrite
libatalk/asp/asp_tickle.c ASP keep-alive tickles
libatalk/asp/asp_init.c Initialize ASP handle from ATP
libatalk/asp/asp_shutdown.c Shut down ASP session
libatalk/asp/asp_attn.c Send attention to client

ASP Structure

Defined in include/atalk/asp.h:

#define ASP_HDRSIZ        4
#define ASP_CMDSIZ        578
#define ASP_MAXPACKETS    8

typedef struct ASP {
    ATP          asp_atp;         /* underlying ATP handle */
    struct sockaddr_at asp_sat;   /* AppleTalk address */
    uint8_t      asp_wss;        /* workstation session socket */
    uint8_t      asp_sid;        /* session ID */
    int          asp_flags;      /* ASPFL_SLS or ASPFL_SSS */
    char         child, inited, *commands;
    char         cmdbuf[ASP_CMDMAXSIZ];   /* 582 bytes */
    char         data[ASP_DATAMAXSIZ];    /* 4624 bytes */
    size_t       cmdlen, datalen;
    off_t        read_count, write_count;
} *ASP;

The maximum data per ATP transaction is 578 bytes × 8 packets = 4,624 bytes (ASP_DATASIZ). This is orders of magnitude smaller than DSI’s default 1 MB quantum, which is why AppleTalk file transfers are significantly slower.

ASP Commands

Defined in include/atalk/asp.h. Parallel to DSI commands:

Constant Value Description
ASPFUNC_CLOSE 1 Close session
ASPFUNC_CMD 2 AFP command
ASPFUNC_STAT 3 Get server status
ASPFUNC_OPEN 4 Open session
ASPFUNC_TICKLE 5 Keep-alive
ASPFUNC_WRITE 6 Write data
ASPFUNC_WRTCONT 7 Write continue
ASPFUNC_ATTN 8 Attention

ASP Error Codes

Defined in include/atalk/asp.h. Mirror DSI error codes exactly:

Constant Value
ASPERR_OK 0x0000
ASPERR_BADVERS 0xfbd6
ASPERR_BUFSMALL 0xfbd5
ASPERR_NOSESS 0xfbd4
ASPERR_NOSERV 0xfbd3
ASPERR_PARM 0xfbd2
ASPERR_SERVBUSY 0xfbd1
ASPERR_SESSCLOS 0xfbd0
ASPERR_SIZERR 0xfbcf
ASPERR_TOOMANY 0xfbce
ASPERR_NOACK 0xfbcd

ASP Session Handling

asp_getsession() in libatalk/asp/asp_getsess.c handles three request types:

The parent process runs a periodic tickle_handler() via SIGALRM that iterates all session slots, sending tickles and killing timed-out children.

AppleTalk Protocol Stack

The lower-layer AppleTalk protocols that ASP depends on. All are conditionally compiled behind #ifndef NO_DDP.

DDP (Datagram Delivery Protocol)

Header: include/atalk/ddp.h. Defines the DDP socket type constants:

Constant Value Protocol
DDPTYPE_RTMPRD 1 RTMP Response/Data
DDPTYPE_NBP 2 Name Binding Protocol
DDPTYPE_ATP 3 AppleTalk Transaction Protocol
DDPTYPE_AEP 4 AppleTalk Echo Protocol
DDPTYPE_RTMPR 5 RTMP Request
DDPTYPE_ZIP 6 Zone Information Protocol
DDPTYPE_ADSP 7 AppleTalk Data Stream Protocol

ATP (AppleTalk Transaction Protocol)

Header: include/atalk/atp.h. Implementation: libatalk/atp/.

ATP provides reliable request-response transactions over DDP datagrams. It supports exactly-once (XO) delivery, multi-packet responses (up to 8 segments), and configurable retransmission timers. The core header structure is struct atphdr:

struct atphdr {
    uint8_t   atphd_ctrlinfo;  /* control: function(2) + XO + EOM + STS + TREL(3) */
    uint8_t   atphd_bitmap;    /* bitmap or sequence number */
    uint16_t  atphd_tid;       /* transaction ID */
};

Constants:

Constant Value Description
ATP_MAXDATA 582 Maximum ATP data size (578+4 user bytes)
ATP_BUFSIZ 587 Maximum packet size
ATP_HDRSIZE 5 Header size (includes DDP type)
ATP_TREQ 1<<6 Transaction Request
ATP_TRESP 2<<6 Transaction Response
ATP_TREL 3<<6 Transaction Release
ATP_XO 1<<5 Exactly-Once mode
ATP_EOM 1<<4 End of Message
ATP_STS 1<<3 Send Transaction Status

API functions: atp_open(), atp_close(), atp_sreq() (send request), atp_rresp() (receive response), atp_rreq() (receive request), atp_sresp() (send response).

NBP (Name Binding Protocol)

Header: include/atalk/nbp.h. Implementation: etc/atalkd/nbp.c.

NBP provides name-to-address resolution for AppleTalk networks, analogous to DNS for IP. Services register themselves as named entities in the format object:type@zone (e.g., MyServer:AFPServer@Engineering), and clients discover them through broadcast lookups.

struct nbptuple — the address portion of an NBP entity:

struct nbptuple {
    uint16_t   nt_net;     /* network number */
    uint8_t    nt_node;    /* node number */
    uint8_t    nt_port;    /* socket (port) number */
    uint8_t    nt_enum;    /* enumerator */
};

struct nbpnve — a Network Visible Entity (full name + address):

struct nbpnve {
    struct sockaddr_at nn_sat;
    uint8_t  nn_objlen;
    char     nn_obj[NBPSTRLEN];    /* 32 chars max */
    uint8_t  nn_typelen;
    char     nn_type[NBPSTRLEN];
    uint8_t  nn_zonelen;
    char     nn_zone[NBPSTRLEN];
};

NBP operations:

Constant Value Description
NBPOP_BRRQ 0x1 Broadcast Request
NBPOP_LKUP 0x2 Lookup
NBPOP_LKUPREPLY 0x3 Lookup Reply
NBPOP_FWD 0x4 Forward
NBPOP_RGSTR 0x7 Register
NBPOP_UNRGSTR 0x8 Unregister
NBPOP_CONFIRM 0x9 Confirm

API: nbp_lookup(), nbp_rgstr(), nbp_unrgstr(), nbp_name(). Command-line tools: bin/nbp/nbplkup.c, bin/nbp/nbprgstr.c, bin/nbp/nbpunrgstr.c.

RTMP (Routing Table Maintenance Protocol)

Header: include/atalk/rtmp.h. Implementation: etc/atalkd/rtmp.c.

struct rtmpent {
    uint16_t   re_net;    /* network number */
    uint8_t    re_hops;   /* hop count */
};

Constants: RTMPHOPS_MAX = 15, RTMPHOPS_POISON = 31, RTMPROP_REQUEST = 1.

ZIP (Zone Information Protocol)

Header: include/atalk/zip.h. Implementation: etc/atalkd/zip.c.

struct ziphdr {
    uint8_t    zh_op;     /* operation */
    uint8_t    zh_cnt;    /* count / zero / flags */
};

ZIP operations:

Constant Value Description
ZIPOP_QUERY 1 Query zones for networks
ZIPOP_REPLY 2 Reply with zone list
ZIPOP_GNI 5 Get Network Info
ZIPOP_GNIREPLY 6 Get Network Info Reply
ZIPOP_NOTIFY 7 Zone change notification
ZIPOP_EREPLY 8 Extended Reply
ZIPOP_GETMYZONE 7 Get My Zone (via ATP)
ZIPOP_GETZONELIST 8 Get Zone List (via ATP)
ZIPOP_GETLOCALZONES 9 Get Local Zones (via ATP)

Maximum zone name: MAX_ZONE_LENGTH = 32.

AppleTalk Protocol Stack Diagram

Application Layer:     AFP (Apple Filing Protocol)
                       ─────────────────────────────
Session Layer:         ASP (AppleTalk Session Protocol)
                       ─────────────────────────────
Transport Layer:       ATP (AppleTalk Transaction Protocol)
                       ─────────────────────────────
Network Services:      RTMP    NBP     ZIP     AEP
                       ─────────────────────────────
Network Layer:         DDP (Datagram Delivery Protocol)
                       ─────────────────────────────
Link Layer:            EtherTalk / LocalTalk / TokenTalk

Performance Characteristics

DSI vs ASP Comparison

Property DSI (TCP/IP) ASP (AppleTalk)
Max data per request 1 MB default (configurable 32 KB – 4 GB) 4624 bytes (578 × 8 ATP packets)
Reply buffer 64 KB (DSI_DATASIZ) 4624 bytes (ASP_DATAMAXSIZ)
Readahead buffer dsireadbuf × quantum (default ~12 MB) None
Zero-copy (sendfile) Yes, when compiled with WITH_SENDFILE No
Vectored I/O Yes, writev() for header+data No
Replay cache 128 entries (REPLAYCACHE_SIZE) No
TCP optimizations TCP_NODELAY, SO_RCVBUF/SO_SNDBUF N/A
Keep-alive DSI tickles via SIGALRM timer ASP tickles via SIGALRM timer
Reconnect Primary reconnect via SIGURG + socket passing Not supported
Default port 548 (DSI_AFPOVERTCP_PORT) Dynamic DDP socket

DSI Performance Tuning

Readahead buffer (dsireadbuf afp.conf option):

The total readahead buffer size is dsireadbuf × server_quantum. With the defaults (12 × 1 MB = 12 MB), this provides buffering for read-ahead during dsi_peek() operations. When the buffer fills, dsi_peek() in libatalk/dsi/dsi_stream.c logs a warning:

dsi_peek: readahead buffer is full, possibly increase -dsireadbuf option

Note; The default value is very low for modern file servers with fast disks and networking. For example with a 10GbE network between the client and Netatalk and fast disks, requires a total readahead buffer of at least 128MB.

Server quantum (server quantum afp.conf option):

The server quantum controls the maximum AFP request/reply payload size. It is stored in dsi->server_quantum, initialized from obj->options.server_quantum in dsi_init() (dsi_init.c), and communicated to the client during DSI OpenSession. Valid range: DSI_SERVQUANT_MIN (32 KB) to DSI_SERVQUANT_MAX (4 GB), default DSI_SERVQUANT_DEF (1 MB). Larger values reduce the number of round-trips needed for large file transfers.

Note; For fileservers server quantum should rarely ever need to be smaller than 1MB. For Netatalk on ZFS if the ZFS recordsize is larger than 1MB, the server quantum can be increased to match the record size but should not be reduced.

TCP socket buffers (SO_RCVBUF / SO_SNDBUF):

Configured in afp_over_dsi() (afp_dsi.c) via setsockopt() using obj->options.tcp_rcvbuf and obj->options.tcp_sndbuf. Additionally, TCP_NODELAY is set on the session socket to disable Nagle’s algorithm, ensuring small AFP replies are sent immediately without coalescing delay.

Note; TCP Send & Recv buffer sizes are scaled automatically, and some operating systems may reject Netatalk’s request to increase these values. Setting these values also fixes their size, effectively disabling the auto scaling. These should rarely need to be set.

Zeroconf / Bonjour service discovery:

When compiled with USE_ZEROCONF, the DSI struct carries a zeroconfname field (UTF-8 service name) and a zeroconf_registered flag (see include/atalk/dsi.h). This enables automatic Bonjour/mDNS service advertisement, allowing AFP clients to discover the server on the local network without manual configuration.

Footnotes

This is a mirror of the Netatalk GitHub Wiki

Last updated 2026-04-06