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:
- 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 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 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 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):
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. 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:
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_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[function]()"]
L --> M["dsi_cmdreply()"]
K --> M
C -->|DSIFUNC_WRITE| N["afp_switch[function]()"]
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:
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
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