Overview
The packet types system defines all network protocol messages used in ascii-chat. This includes protocol negotiation, media streaming (video/audio), control messages, cryptographic handshake, and multi-user protocol extensions.
Implementation: lib/network/packet_types.h
Key Features:
- Comprehensive packet type enumeration (33 packet types)
- Packed structures for wire format compatibility
- Protocol versioning and capability negotiation
- Handshake packet detection for crypto routing
- Network byte order for cross-platform compatibility
- Magic number validation for packet detection
Packet Categories
Packet types are organized into logical categories:
Protocol Negotiation (Type 1):
- PACKET_TYPE_PROTOCOL_VERSION: Initial handshake for version and capabilities
Media Packets (Types 2-4, 28):
- PACKET_TYPE_ASCII_FRAME (2): Complete ASCII art frame with metadata
- PACKET_TYPE_IMAGE_FRAME (3): Raw RGB image from webcam
- PACKET_TYPE_AUDIO (4): Single audio packet (legacy)
- PACKET_TYPE_AUDIO_BATCH (28): Batched audio packets (efficient)
Control Packets (Types 5-7, 12-13):
- PACKET_TYPE_CLIENT_CAPABILITIES (5): Terminal capabilities report
- PACKET_TYPE_PING (6): Keepalive ping
- PACKET_TYPE_PONG (7): Keepalive pong response
- PACKET_TYPE_CLEAR_CONSOLE (12): Server requests console clear
- PACKET_TYPE_SERVER_STATE (13): Server broadcasts current state
Multi-User Protocol (Types 8-11):
- PACKET_TYPE_CLIENT_JOIN (8): Client announces capability to send media
- PACKET_TYPE_CLIENT_LEAVE (9): Clean disconnect notification
- PACKET_TYPE_STREAM_START (10): Client requests to start streaming
- PACKET_TYPE_STREAM_STOP (11): Client stops sending media
Crypto Handshake (Types 14-23):
- UNENCRYPTED - These packets must NEVER be encrypted
- PACKET_TYPE_CRYPTO_CAPABILITIES (14): Client β Server: Supported algorithms
- PACKET_TYPE_CRYPTO_PARAMETERS (15): Server β Client: Chosen algorithms + sizes
- PACKET_TYPE_CRYPTO_KEY_EXCHANGE_INIT (16): Server β Client: Server public key
- PACKET_TYPE_CRYPTO_KEY_EXCHANGE_RESP (17): Client β Server: Client public key
- PACKET_TYPE_CRYPTO_AUTH_CHALLENGE (18): Server β Client: Authentication nonce
- PACKET_TYPE_CRYPTO_AUTH_RESPONSE (19): Client β Server: HMAC response
- PACKET_TYPE_CRYPTO_AUTH_FAILED (20): Server β Client: Authentication rejected
- PACKET_TYPE_CRYPTO_SERVER_AUTH_RESP (21): Server β Client: Server proves knowledge
- PACKET_TYPE_CRYPTO_HANDSHAKE_COMPLETE (22): Server β Client: Encryption ready
- PACKET_TYPE_CRYPTO_NO_ENCRYPTION (23): Client β Server: Proceed without encryption
- PACKET_TYPE_ENCRYPTED (24): Encrypted session packets (after handshake)
Crypto Rekeying (Types 25-27):
- UNENCRYPTED during rekey - Must bypass encryption during rekey handshake
- PACKET_TYPE_CRYPTO_REKEY_REQUEST (25): Initiator β Responder: New ephemeral key
- PACKET_TYPE_CRYPTO_REKEY_RESPONSE (26): Responder β Initiator: New ephemeral key
- PACKET_TYPE_CRYPTO_REKEY_COMPLETE (27): Encrypted with NEW key (but still handshake)
Message Packets (Types 29-33):
- PACKET_TYPE_SIZE_MESSAGE (29): Terminal size notification
- PACKET_TYPE_AUDIO_MESSAGE (30): Audio metadata message
- PACKET_TYPE_TEXT_MESSAGE (31): Text chat message (future)
- PACKET_TYPE_ERROR_MESSAGE (32): Protocol/handshake error reporting
- PACKET_TYPE_REMOTE_LOG (33): Bidirectional remote logging payload
Remote Log Packet (Type 33)
Remote logging packets allow the server and client to forward diagnostic log entries to each other over the established connection. They carry the log level, direction hint, optional flags, and a UTF-8 text payload limited to 512 bytes.
typedef struct {
uint8_t log_level;
uint8_t direction;
uint16_t flags;
uint32_t message_length;
} PACKED_ATTR remote_log_packet_t;
Direction values**:
Messages are encrypted automatically once the crypto handshake completes, but fall back to plaintext during handshake so early errors can still be reported. Receivers must always prefix their local log output with a βremoteβ annotation to make the source of the message obvious.
Packet Structure
Packet Header
All packets begin with a standard header:
typedef struct {
uint32_t magic;
uint16_t type;
uint32_t length;
uint32_t crc32;
uint32_t client_id;
} PACKED_ATTR packet_header_t;
Header fields:
magic: Must be PACKET_MAGIC (0xDEADBEEF) or packet is invalid
type: Packet type enumeration value (packet_type_t, 1-33)
length: Payload data length in bytes (0 for header-only packets)
crc32: CRC32 checksum of payload data (not including header)
client_id: Identifies source (0 for server, 1-9 for clients)
Wire format:
- Structure is packed (no padding between fields)
- Total header size: 18 bytes (4+2+4+4+4)
- Network byte order (big-endian) for multi-byte fields
- Magic number validation catches corrupted packets
Example packet construction:
packet_header_t header = {
.magic = PACKET_MAGIC,
.type = PACKET_TYPE_ASCII_FRAME,
.length = (uint32_t)ascii_frame_size,
.crc32 = crc32_compute(ascii_frame_data, ascii_frame_size),
.client_id = 3
};
Handshake Packet Detection
Critical function for routing crypto handshake packets:
bool packet_is_handshake_type(packet_type_t type);
Returns true for:
- Initial crypto handshake packets (types 14-23)
- Rekey packets (types 25-27)
Returns false for:
- All other packet types (including PACKET_TYPE_ENCRYPTED = 24)
- PACKET_TYPE_ERROR_MESSAGE (32) β sent plaintext only before encryption is active
PACKET_TYPE_REMOTE_LOG (33) β encrypted when session keys are ready, plaintext otherwise
Usage:
if (!packet_is_handshake_type(header.type)) {
crypto_encrypt(&ctx, payload, payload_len, ciphertext, &ciphertext_len);
header.type = PACKET_TYPE_ENCRYPTED;
header.length = (uint32_t)ciphertext_len;
} else {
}
crypto_result_t crypto_encrypt(crypto_context_t *ctx, const uint8_t *plaintext, size_t plaintext_len, uint8_t *ciphertext_out, size_t ciphertext_out_size, size_t *ciphertext_len_out)
Warning: Encrypting handshake packets will break the crypto handshake. Always check with packet_is_handshake_type() before encryption. Error packets may be sent plaintext prior to handshake completion (or when encryption is disabled), but must be encrypted once a session key is established.
Protocol Negotiation
Protocol Version Packet
First packet exchanged between client and server:
typedef struct {
uint16_t protocol_version;
uint16_t protocol_revision;
uint8_t supports_encryption;
uint8_t compression_algorithms;
uint8_t compression_threshold;
uint16_t feature_flags;
uint8_t reserved[7];
} PACKED_ATTR protocol_version_packet_t;
Version negotiation flow:
protocol_version_packet_t client_version = {
.protocol_version = 1,
.protocol_revision = 0,
.supports_encryption = 1,
.compression_algorithms = COMPRESS_ALGO_ZSTD,
.compression_threshold = 80,
.feature_flags = FEATURE_RLE_ENCODING
};
send_packet(socket, PACKET_TYPE_PROTOCOL_VERSION, &client_version,
sizeof(client_version));
protocol_version_packet_t server_version = {
.protocol_version = 1,
.protocol_revision = 1,
.supports_encryption = 1,
.compression_algorithms = COMPRESS_ALGO_ZSTD,
.compression_threshold = 80,
.feature_flags = FEATURE_RLE_ENCODING
};
send_packet(socket, PACKET_TYPE_PROTOCOL_VERSION, &server_version,
sizeof(server_version));
int send_packet(socket_t sockfd, packet_type_t type, const void *data, size_t len)
Send a basic packet without encryption.
Version compatibility:
- Major version mismatch: Connection rejected
- Minor revision mismatch: OK (server uses newer features, client ignores unknown)
- Compression negotiation: Both parties choose lowest common denominator
Frame Packets
ASCII Frame Packet
Complete ASCII art frame with compression support:
typedef struct {
uint32_t width;
uint32_t height;
uint32_t original_size;
uint32_t compressed_size;
uint32_t checksum;
uint32_t flags;
} PACKED_ATTR ascii_frame_packet_t;
Payload structure:
- Header:
ascii_frame_packet_t (24 bytes)
- Data:
char data[original_size] or char compressed_data[compressed_size]
Compression handling:
ascii_frame_packet_t header = {
.width = 160,
.height = 45,
.original_size = 7200,
.compressed_size = 0,
.checksum = crc32_compute(ascii_data, 7200),
.flags = FLAG_HAS_COLOR
};
if (header.original_size > COMPRESS_THRESHOLD) {
compressed_buf, &header.compressed_size);
if (header.compressed_size < header.original_size * 0.8) {
header.flags |= FLAG_IS_COMPRESSED;
} else {
header.compressed_size = 0;
}
}
asciichat_error_t compress_data(const void *input, size_t input_size, void **output, size_t *output_size, int compression_level)
Image Frame Packet
Raw RGB image from webcam:
typedef struct {
uint32_t width;
uint32_t height;
uint32_t pixel_format;
uint32_t compressed_size;
uint32_t checksum;
uint32_t timestamp;
} PACKED_ATTR image_frame_packet_t;
Payload structure:
- Header:
image_frame_packet_t (24 bytes)
- Data:
rgb_pixel_t pixels[width * height] or compressed data
Usage (client β server):
uint8_t *rgb_frame = capture_webcam_frame(&width, &height);
image_frame_packet_t header = {
.width = width,
.height = height,
.pixel_format = PIXEL_FORMAT_RGB24,
.compressed_size = 0,
.checksum = crc32_compute(rgb_frame, width * height * 3),
.timestamp = get_timestamp()
};
send_packet(socket, PACKET_TYPE_IMAGE_FRAME, &header,
sizeof(header));
send_packet_data(socket, rgb_frame, width * height * 3);
Audio Packets
Single Audio Packet (Legacy)
PACKET_TYPE_AUDIO (type 4) is the legacy single-packet format:
- Fixed size: 256 samples per packet
- Format:
float samples[256]
- Sample rate: Negotiated in protocol version packet
- Channels: Usually mono (1 channel)
Disadvantages:
- High packet overhead (1 packet per 256 samples)
- At 44.1 kHz: 188 packets/second
- Each packet: 18-byte header + 1024-byte payload = 1042 bytes
- Total overhead: 188 Γ 18 = 3.4 KB/sec (0.34% of bandwidth)
Batched Audio Packet (Efficient)
PACKET_TYPE_AUDIO_BATCH (type 28) batches multiple audio chunks:
typedef struct {
uint32_t batch_count;
uint32_t total_samples;
uint32_t sample_rate;
uint32_t channels;
} PACKED_ATTR audio_batch_packet_t;
Payload structure:
- Header:
audio_batch_packet_t (16 bytes)
- Data:
float samples[total_samples] (interleaved if stereo)
Benefits:
- Reduced packet overhead: 4 chunks per packet = 75% fewer packets
- At 44.1 kHz: 47 packets/second (was 188)
- Packet overhead: 47 Γ 18 = 846 bytes/sec (0.08% of bandwidth)
- 80% reduction in packet overhead
Example:
float samples[1024];
capture_audio_chunks(samples, 4);
audio_batch_packet_t header = {
.batch_count = 4,
.total_samples = 1024,
.sample_rate = 44100,
.channels = 1
};
send_packet(socket, PACKET_TYPE_AUDIO_BATCH, &header,
sizeof(header));
send_packet_data(socket, samples, sizeof(samples));
Control Packets
Client Capabilities
Client reports terminal capabilities to server:
typedef struct {
uint32_t capabilities;
uint32_t color_level;
uint32_t color_count;
uint32_t render_mode;
uint16_t width, height;
char term_type[32];
char colorterm[32];
uint8_t detection_reliable;
uint32_t utf8_support;
uint32_t palette_type;
char palette_custom[64];
uint8_t desired_fps;
uint8_t reserved[2];
} PACKED_ATTR terminal_capabilities_packet_t;
Usage:
terminal_capabilities_packet_t caps = {
.capabilities = TERM_CAP_VIDEO | TERM_CAP_AUDIO | TERM_CAP_COLOR,
.color_level = TERM_COLOR_24BIT,
.color_count = 16777216,
.render_mode = RENDER_MODE_HALF_BLOCK,
.width = 160,
.height = 45,
.detection_reliable = 1,
.utf8_support = 1,
.palette_type = PALETTE_STANDARD,
.desired_fps = 30
};
strncpy(caps.term_type, getenv("TERM"), sizeof(caps.term_type) - 1);
strncpy(caps.colorterm, getenv("COLORTERM"), sizeof(caps.colorterm) - 1);
send_packet(socket, PACKET_TYPE_CLIENT_CAPABILITIES, &caps,
sizeof(caps));
Ping/Pong Keepalive
Maintains connection alive and detects dead connections:
void keepalive_thread(void *arg) {
while (running) {
sleep(30);
packet_header_t ping = {
.magic = PACKET_MAGIC,
.type = PACKET_TYPE_PING,
.length = 0,
.crc32 = 0,
.client_id = 0
};
}
}
void handle_packet(packet_header_t *header, void *payload) {
if (header->type == PACKET_TYPE_PING) {
packet_header_t pong = {
.magic = PACKET_MAGIC,
.type = PACKET_TYPE_PONG,
.length = 0,
.crc32 = 0,
.client_id = my_client_id
};
}
}
Crypto Handshake Packets
All crypto handshake packets (types 14-23, 25-27) must be sent UNENCRYPTED. Use packet_is_handshake_type() to check before encryption.
Handshake Flow
Phase 1: Capability Exchange
crypto_capabilities_packet_t client_caps = {
.supported_kex_algorithms = KEX_ALGO_X25519,
.supported_auth_algorithms = AUTH_ALGO_ED25519,
.supported_cipher_algorithms = CIPHER_ALGO_XSALSA20_POLY1305,
.requires_verification = 1,
.preferred_kex = KEX_ALGO_X25519,
.preferred_auth = AUTH_ALGO_ED25519,
.preferred_cipher = CIPHER_ALGO_XSALSA20_POLY1305
};
send_packet(socket, PACKET_TYPE_CRYPTO_CAPABILITIES, &client_caps,
sizeof(client_caps));
crypto_parameters_packet_t server_params = {
.selected_kex = KEX_ALGO_X25519,
.selected_auth = AUTH_ALGO_ED25519,
.selected_cipher = CIPHER_ALGO_XSALSA20_POLY1305,
.verification_enabled = 1,
.kex_public_key_size = 32,
.auth_public_key_size = 32,
.signature_size = 64,
.shared_secret_size = 32,
.nonce_size = 24,
.mac_size = 16,
.hmac_size = 32
};
send_packet(socket, PACKET_TYPE_CRYPTO_PARAMETERS, &server_params,
sizeof(server_params));
Phase 2: Key Exchange
uint8_t server_pubkey[32];
send_packet(socket, PACKET_TYPE_CRYPTO_KEY_EXCHANGE_INIT,
server_pubkey, 32);
uint8_t client_pubkey[32];
send_packet(socket, PACKET_TYPE_CRYPTO_KEY_EXCHANGE_RESP,
client_pubkey, 32);
crypto_result_t crypto_generate_keypair(crypto_context_t *ctx)
Phase 3: Authentication
uint8_t challenge[32];
randombytes_buf(challenge, 32);
send_packet(socket, PACKET_TYPE_CRYPTO_AUTH_CHALLENGE, challenge, 32);
uint8_t client_hmac[32];
compute_hmac(client_identity_key, challenge, 32, client_hmac);
send_packet(socket, PACKET_TYPE_CRYPTO_AUTH_RESPONSE, client_hmac, 32);
auth_failure_packet_t failure = {
.reason_flags = AUTH_FAIL_SIGNATURE_INVALID
};
send_packet(socket, PACKET_TYPE_CRYPTO_AUTH_FAILED, &failure,
sizeof(failure));
uint8_t server_hmac[32];
compute_hmac(server_identity_key, client_hmac, 32, server_hmac);
send_packet(socket, PACKET_TYPE_CRYPTO_SERVER_AUTH_RESP, server_hmac, 32);
Phase 4: Handshake Complete
send_packet(socket, PACKET_TYPE_CRYPTO_HANDSHAKE_COMPLETE, NULL, 0);
Multi-User Protocol
Client Join
Client announces capability to send video/audio:
typedef struct {
uint32_t client_id;
char display_name[MAX_DISPLAY_NAME_LEN];
uint32_t capabilities;
} PACKED_ATTR client_info_packet_t;
client_info_packet_t join = {
.client_id = 3,
.capabilities = CLIENT_CAP_VIDEO | CLIENT_CAP_AUDIO
};
strncpy(join.display_name, "Alice", MAX_DISPLAY_NAME_LEN - 1);
send_packet(socket, PACKET_TYPE_CLIENT_JOIN, &join,
sizeof(join));
Server State Broadcast
Server broadcasts current state to all clients:
typedef struct {
uint32_t connected_client_count;
uint32_t active_client_count;
uint32_t reserved[6];
} PACKED_ATTR server_state_packet_t;
server_state_packet_t state = {
.connected_client_count = 5,
.active_client_count = 3
};
for (int i = 0; i < connected_clients; i++) {
send_packet(clients[i].socket, PACKET_TYPE_SERVER_STATE,
&state, sizeof(state));
}
Protocol Versioning
ascii-chat uses semantic versioning for protocol compatibility:
Version Format:
- Major version: Incompatible protocol changes (client/server must match)
- Minor revision: Backward-compatible additions (server can be newer)
Compatibility Rules:
- Major version mismatch: Connection rejected
- Minor revision mismatch: OK (server uses newer features, client ignores unknown)
- Client sends:
protocol_version=1, revision=0
- Server responds:
protocol_version=1, revision=X (X can be >= 0)
Future Protocol Changes:
- Major version bump: Complete protocol redesign
- Minor revision bump: New packet types, new flags, extended structures
- Reserved fields: Structures include reserved fields for future expansion
Wire Format
Byte Order
All multi-byte fields use network byte order (big-endian):
header.magic = htonl(PACKET_MAGIC);
header.type = htons(PACKET_TYPE_ASCII_FRAME);
header.length = htonl(payload_length);
header.crc32 = htonl(crc32_value);
header.client_id = htonl(client_id);
header.magic = ntohl(header.magic);
header.type = ntohs(header.type);
header.length = ntohl(header.length);
header.crc32 = ntohl(header.crc32);
header.client_id = ntohl(header.client_id);
Platform-specific functions:
- POSIX:
htonl(), htons(), ntohl(), ntohs()
- Windows:
htonl(), htons(), ntohl(), ntohs() (winsock.h)
Structure Packing
Structures are packed (no padding) for wire format compatibility:
#ifdef _WIN32
#pragma pack(push, 1)
#endif
typedef struct {
uint32_t field1;
uint16_t field2;
uint32_t field3;
} PACKED_ATTR my_packet_t;
#ifdef _WIN32
#pragma pack(pop)
#endif
Warning: Packed structures may not be naturally aligned. Accessing fields may cause unaligned memory access (slower on some CPUs). ascii-chat structures are designed to avoid this issue.
Best Practices
- Always validate magic number:
if (header.magic != PACKET_MAGIC) {
log_error("Invalid packet magic: 0x%08X", header.magic);
return ERROR_NETWORK_PROTOCOL;
}
- Check handshake types before encryption:
if (!packet_is_handshake_type(header.type)) {
encrypt_packet(&header, payload, payload_len);
}
- Validate payload length:
if (header.length > MAX_PACKET_SIZE) {
log_error("Packet too large: %u bytes", header.length);
return ERROR_NETWORK_PROTOCOL;
}
- Verify CRC32 checksums:
uint32_t computed_crc = crc32_compute(payload, header.length);
if (computed_crc != header.crc32) {
log_error("CRC32 mismatch: expected 0x%08X, got 0x%08X",
header.crc32, computed_crc);
return ERROR_NETWORK_PROTOCOL;
}
- Use network byte order:
header.magic = htonl(PACKET_MAGIC);
header.length = htonl(payload_length);
header.magic = ntohl(header.magic);
header.length = ntohl(header.length);
- Handle unknown packet types gracefully:
switch (header.type) {
case PACKET_TYPE_ASCII_FRAME:
handle_ascii_frame(payload, header.length);
break;
default:
log_warn("Unknown packet type: %u", header.type);
break;
}
Protocol Constants
Packet Size Limits:
#define LARGE_PACKET_THRESHOLD (100 * 1024)
#define MAX_PACKET_SIZE ((size_t)5 * 1024 * 1024)
Timeout Configuration:
#define BASE_SEND_TIMEOUT 5
#define LARGE_PACKET_EXTRA_TIMEOUT_PER_MB 0.8
#define MIN_CLIENT_TIMEOUT 10
#define MAX_CLIENT_TIMEOUT 60
Audio Configuration:
#define AUDIO_SAMPLES_PER_PACKET 256
#define AUDIO_BATCH_COUNT 4
#define AUDIO_BATCH_SAMPLES 1024
#define AUDIO_BATCH_MS 23
Magic Numbers:
#define PACKET_MAGIC 0xDEADBEEF
#define FRAME_MAGIC 0xDEADBEEF
#define FRAME_FREED 0xFEEDFACE
- See also
- network/packet_types.h
-
network/packet.h
-
network/av.h