Dev Docs Network Protocols
Network Protocol Stack
Audited against Netatalk
d63155e13on 2026-10-10. This describes the current source checkout; older releases may differ.
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>host kernel</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 built independently of optional AppleTalk support.
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 */
};
Reply headers are packed by dsi_header_pack_reply() and decoded by dsi_stream_receive(), both in libatalk/dsi/dsi_stream.c. Multi-byte fields are encoded in network byte order on the wire. Some fields are converted for host-order calculations during parsing; do not infer every in-memory value’s byte order solely from the original header 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 |
0x10000000 (256 MiB) |
Maximum configured 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 */
const void *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 */
int splice_pipe[2]; /* recvfile pipe, created lazily */
int splice_size; /* capacity actually granted by kernel */
/* readahead buffer for dsi_peek */
size_t dsireadbuf; /* allocation multiplier copied from configuration */
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 main-loop handle_alarm() (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. The signal handler records the timer request and wakes the self-pipe. Main-loop handle_alarm() in etc/afpd/afp_dsi.c runs through this decision chain:
- If
DSI_DATAis set, the client recently sent traffic. Clear the flag and return—no tickle needed. - Increment the
dsi->ticklecounter (counts consecutive idle intervals). - If
DSI_SLEEPING: comparetickleagainstoptions.sleep. Terminate the session if the sleep limit is exceeded. - If
DSI_DISCONNECTED: comparetickleagainstoptions.disconnected. Terminate if the reconnect window has expired. - If
tickle >= options.timeout: the client has gone silent too long. Enter the disconnected state viadsi_disconnect()in dsi_stream.c. - 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 path — dsi_stream_receive() in libatalk/dsi/dsi_stream.c reads/decodes a 16-byte header and validates frame bounds. DSIWrite permits AFP request blocks of 12 bytes (FPWrite) or 20 bytes (FPWriteExt), checks offset against payload length, and bounds write data independently by the negotiated quantum. Oversized ordinary requests and command frames without an AFP function byte are rejected; lengths are not silently truncated. Write data is consumed by the AFP write handler after its request block is read.
Send path — dsi_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 the configured default quantum of 1 MiB and dsireadbuf of 32, the baseline is 32 MiB of incoming readahead. Session quantum alignment can change the allocation. The buffer-product limit is 1 GiB, and the multiplier is re-clamped after alignment. 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):
dsi_readinit()— setsDSI_NOREPLY, sends DSI header + initial buffer viadsi_stream_send()dsi_read()— sends subsequent chunks viadsi_stream_write(), decrementsdsi->datasizedsi_readdone()— decrementsin_writecounter- 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):
dsi_writeinit()— calculatesdatasizefromdsi_len - dsi_doff, drains readahead buffer firstdsi_write()— reads chunks from socket viadsi_stream_read(), decrementsdatasizedsi_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. In the DSIWrite path, when a request ID and command match a cache entry, the cached result is returned without re-executing the write. It is not a full-reply replay cache for all AFP commands. 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:
preauth_switch[]— onlyafp_login(18),afp_logincont(19),afp_logout(20), andafp_login_ext(63) are non-NULLpostauth_switch[]— full set of AFP commands after authentication
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_ZZZZZ |
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 and Signals
The DSI worker polls the client socket, parent hint pipe, and signal self-pipe. It grants idle cleanup only while blocked, then calls iw_revoke() before any cache access, including after EINTR. Buffered DSI requests bypass poll. Sleeping sessions continue to poll so reconnect signals can wake them.
Signal handlers record requests; process_deferred_signals() performs their actions in the main loop. In particular, afp_dsi_transfer_handler() records SIGURG for handle_transfer_session(), and alarm_handler() records SIGALRM for handle_alarm(). There is no siglongjmp() reconnect path. See AFP Daemon: Signal Handling for the current handler table.
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 MiB 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:
ASPFUNC_TICKLE— resets the connection state for the session IDASPFUNC_STAT— sends the server status block via ATP response packetsASPFUNC_OPEN— opens a new ATP socket, forks a child process, returns the session ID and port
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
| Property | DSI | ASP |
|---|---|---|
| Write data limit | Configured 1 MiB default; 32,000 bytes–256 MiB, with bounded session alignment | 4,624-byte ASP transaction data |
| Ordinary reply buffer | DSI_DATASIZ (65,536 bytes); streaming replies have a separate path |
ASP_DATAMAXSIZ (4,624 bytes) |
| Incoming readahead | Default multiplier 32; allocation depends on session quantum | No DSI readahead buffer |
| Sendfile/recvfile | Optional compiled paths, eligible transfers only | No DSI transfer path |
| Primary reconnect | Opaque token, UID check, fd transfer and deferred SIGURG processing | No DSI primary reconnect |
| Transport | TCP, default port 548 | ATP/DDP, optional AppleTalk build |
DSI server quantum bounds incoming write data; it is not a filesystem record size or the maximum streamed read reply. dsi_align_quantum() in libatalk/dsi/dsi_tcp.c adjusts it to bounded page/MSS alignment, and dsi_opensession() advertises the session value. Receive-side buffering can grow with both quantum and dsireadbuf.
See Performance Tuning for current defaults, memory accounting, and measurement. No fixed buffer requirement can be inferred from link speed alone.
Footnotes
Generated from the Netatalk GitHub Wiki
Last updated 2026-10-21