xfer — P2P File Transfer

Introduction

xfer is xKit's peer-to-peer file transfer module, providing a high-level API for sending and receiving files over WebRTC DataChannels. Built on top of xp2p, it handles the full transfer pipeline — signaling server rendezvous, SDP/ICE exchange, file chunking, integrity verification (SHA-1), progress reporting, and resume support — all driven by the xKit event loop.

The module ships with a built-in signaling server (xSignalServer) and client (xSignalClient) that handle session creation, peer pairing, and SDP/ICE relay over WebSocket. Applications only need to provide a file path (sender) or a transfer code (receiver) to initiate a transfer. The transfer code (e.g. AB12CD) is a short, plain session ID assigned by the signaling server. Both sender and receiver must connect to the same signaling server.

Design Philosophy

  1. Zero-Configuration P2P — The sender registers with a signaling server and receives a short transfer code (session ID). The receiver uses this code along with the signaling server URL to connect. NAT traversal, encryption, and chunking are handled automatically.

  2. Event-Driven, Single-Threaded — All callbacks (state changes, progress, errors) are invoked on the xKit event loop thread, consistent with the rest of the xKit stack.

  3. Resumable Transfers — The wire protocol includes a FILE_RESUME message with a bitmap of received chunks, enabling the sender to skip already-transferred chunks after a reconnection.

  4. Integrity Verification — Files are SHA-1 hashed before transfer. The receiver verifies the hash after reassembly, detecting corruption or incomplete transfers.

  5. Layered Architecture — The module is cleanly separated into three layers: the high-level xTransfer API, the signaling layer (xSignalServer / xSignalClient), and the binary wire protocol (xfer_protocol.h). Each layer can be used independently.

  6. Pluggable Storage Backend — All file I/O (reading the source file, writing the received file) goes through a xTransferVfs interface. The default implementation uses POSIX fopen/fread/fwrite, but callers can supply a custom VFS for in-memory transfers, encrypted storage, cloud-backed storage, or any other backend.

Architecture

Component Stack

graph TD
    subgraph "Application"
        APP["User Application"]
        CUSTOM_VFS["Custom VFS<br/>(optional)"]
    end

    subgraph "xfer"
        XFER["xTransfer<br/>xfer.h"]
        SENDER["Sender Logic<br/>xfer_sender.c"]
        RECEIVER["Receiver Logic<br/>xfer_receiver.c"]
        VFS["xTransferVfs<br/>xfer_vfs.h"]
        VFS_POSIX["POSIX VFS<br/>xfer_vfs_posix.c"]
        SIG_C["xSignalClient<br/>xfer_signal.h"]
        SIG_S["xSignalServer<br/>xfer_signal.h"]
        PROTO["Wire Protocol<br/>xfer_protocol.h"]
    end

    subgraph "xp2p"
        PC["xPeerConnection<br/>peer_connection.h"]
    end

    subgraph "xhttp"
        WS_S["WebSocket Server"]
        WS_C["WebSocket Client"]
    end

    subgraph "xbase"
        EV["xEventLoop<br/>event.h"]
    end

    APP --> XFER
    CUSTOM_VFS -.-> VFS
    XFER --> SENDER
    XFER --> RECEIVER
    SENDER --> VFS
    RECEIVER --> VFS
    VFS --> VFS_POSIX
    XFER --> SIG_C
    XFER --> PC
    XFER --> PROTO
    SIG_S --> WS_S
    SIG_C --> WS_C
    PC --> EV
    WS_S --> EV
    WS_C --> EV

    style XFER fill:#4a90d9,color:#fff
    style SENDER fill:#4a90d9,color:#fff
    style RECEIVER fill:#4a90d9,color:#fff
    style VFS fill:#e74c3c,color:#fff
    style VFS_POSIX fill:#e74c3c,color:#fff
    style CUSTOM_VFS fill:#e74c3c,color:#fff,stroke-dasharray: 5 5
    style SIG_C fill:#50b86c,color:#fff
    style SIG_S fill:#50b86c,color:#fff
    style PROTO fill:#f5a623,color:#fff
    style PC fill:#9b59b6,color:#fff

Transfer Flow

sequenceDiagram
    participant Sender
    participant SignalServer
    participant Receiver

    Note over Sender: xTransferSendFile()
    Sender->>SignalServer: WebSocket connect + "create"
    SignalServer-->>Sender: code = "AB12CD"
    Note over Sender: on_code("AB12CD")

    Note over Receiver: xTransferRecvFile("AB12CD")
    Receiver->>SignalServer: WebSocket connect + "join(AB12CD)"
    SignalServer-->>Sender: peer_joined
    SignalServer-->>Receiver: joined

    Sender->>SignalServer: SDP offer
    SignalServer->>Receiver: SDP offer
    Receiver->>SignalServer: SDP answer
    SignalServer->>Sender: SDP answer

    Note over Sender,Receiver: ICE candidates exchanged via SignalServer

    Note over Sender,Receiver: P2P DataChannel established

    Sender->>Receiver: FILE_META (name, size, sha1)
    loop For each chunk
        Sender->>Receiver: FILE_CHUNK (id, data)
        Note over Receiver: on_progress()
    end
    Sender->>Receiver: FILE_DONE (total_chunks, sha1)
    Receiver->>Sender: FILE_ACK (status)
    Note over Sender: on_state_change(Done)
    Note over Receiver: on_state_change(Done)

Wire Protocol

All messages are sent over the WebRTC DataChannel in binary. Multi-byte integers use network byte order (big-endian).

┌──────────────────────────────────────────────────────────────┐
│  FILE_META   │ type(1B) │ name_len(2B) │ name │ size(8B)    │
│              │          │ chunk_sz(4B) │ sha1(20B)           │
├──────────────────────────────────────────────────────────────┤
│  FILE_CHUNK  │ type(1B) │ chunk_id(4B) │ data(variable)     │
├──────────────────────────────────────────────────────────────┤
│  FILE_DONE   │ type(1B) │ total_chunks(4B) │ sha1(20B)      │
├──────────────────────────────────────────────────────────────┤
│  FILE_ACK    │ type(1B) │ status(1B)                        │
├──────────────────────────────────────────────────────────────┤
│  FILE_RESUME │ type(1B) │ total_chunks(4B) │ bitmap_len(4B) │
│              │ bitmap(variable)                              │
└──────────────────────────────────────────────────────────────┘
Message TypeValueDirectionDescription
XFER_MSG_FILE_META0x01Sender → ReceiverFile metadata (name, size, chunk size, SHA-1)
XFER_MSG_FILE_CHUNK0x02Sender → ReceiverFile data chunk
XFER_MSG_FILE_DONE0x03Sender → ReceiverTransfer complete signal
XFER_MSG_ACK0x04Receiver → SenderAcknowledgement (success/failure)
XFER_MSG_ERROR0x05BothError message
XFER_MSG_CANCEL0x06BothCancel transfer
XFER_MSG_FILE_RESUME0x07Receiver → SenderResume bitmap for skipping received chunks

Sub-Module Overview

Header / SourceComponentDescription
xfer.hxTransferHigh-level file transfer API — send/receive files with progress and state callbacks
xfer_vfs.hxTransferVfsVirtual file system interface for pluggable storage backends
xfer_vfs_posix.cxTransferPosixVfsBuilt-in POSIX VFS implementation (fopen/fread/fwrite)
xfer_sender.cSender LogicSender-side data flow: file reading, chunking, flow control
xfer_receiver.cReceiver LogicReceiver-side data flow: message parsing, file writing, SHA-1 verification
xfer_private.hInternal HeaderShared internal structures and helpers (not part of the public API)
xfer_signal.hxSignalServerWebSocket-based signaling server for session management and SDP/ICE relay
xfer_signal.hxSignalClientSignaling client for connecting to the server and exchanging SDP/ICE
xfer_protocol.hWire ProtocolBinary message encoding/decoding for file metadata, chunks, and control messages

API Reference

Constants

ConstantValueDescription
XFER_DEFAULT_CHUNK_SIZE64 KBDefault chunk size for file transfer
XFER_MAX_FILENAME_LEN256Maximum file name length
XFER_MAX_CODE_LEN128Maximum session code length

Types

TypeDescription
xTransferOpaque handle to a transfer session
xTransferStateEnum: Idle, WaitingPeer, Connecting, Transferring, Done, Failed
xTransferRoleEnum: Sender, Receiver
xTransferConfConfiguration struct with P2P settings, signaling URL, VFS, and callbacks
xTransferVfsVirtual file system interface — function pointers for open/pread/pwrite/close/etc.

Callbacks

CallbackSignatureDescription
xTransferOnStateChangevoid (*)(xTransfer, xTransferState, void *ctx)State transition notification
xTransferOnProgressvoid (*)(xTransfer, uint64_t transferred, uint64_t total, void *ctx)Progress reporting
xTransferOnCodevoid (*)(xTransfer, const char *code, void *ctx)Sender receives session code
xTransferOnFileMetavoid (*)(xTransfer, const char *filename, uint64_t filesize, void *ctx)Receiver learns file metadata
xTransferOnErrorvoid (*)(xTransfer, xErrno, const char *msg, void *ctx)Error notification
xTransferOnIceCandidatevoid (*)(xTransfer, const char *candidate, void *ctx)ICE candidate gathered

VFS (Virtual File System)

The xTransferVfs struct (defined in xfer_vfs.h) abstracts all file I/O. Pass a custom VFS via xTransferConf.vfs, or leave it NULL to use the default POSIX implementation.

FieldSignatureRequiredDescription
ctxvoid *Opaque context forwarded to all callbacks
openvoid *(*)(void *ctx, const char *path, const char *mode)Open a file, returns opaque handle or NULL
preadxErrno (*)(void *ctx, void *handle, uint8_t *buf, size_t len, uint64_t offset, size_t *nread)Random-access read at offset
pwritexErrno (*)(void *ctx, void *handle, const uint8_t *buf, size_t len, uint64_t offset, size_t *nwritten)Random-access write at offset
sizexErrno (*)(void *ctx, void *handle, uint64_t *out_size)Get total file size
truncatexErrno (*)(void *ctx, void *handle, uint64_t size)OptionalPre-allocate / truncate storage
flushxErrno (*)(void *ctx, void *handle)Flush buffered data to persistent storage
closevoid (*)(void *ctx, void *handle)Close the handle
renamexErrno (*)(void *ctx, const char *from, const char *to)OptionalRename a file
removexErrno (*)(void *ctx, const char *path)OptionalRemove a file
FunctionSignatureDescription
xTransferPosixVfsconst xTransferVfs *xTransferPosixVfs(void)Return the built-in POSIX VFS (valid for the lifetime of the process)

Transfer Lifecycle

FunctionSignatureDescription
xTransferCreatexTransfer xTransferCreate(xEventLoop loop, const xTransferConf *conf)Create a transfer session
xTransferDestroyvoid xTransferDestroy(xTransfer xfer)Destroy and free all resources
xTransferSendFilexErrno xTransferSendFile(xTransfer xfer, const char *filepath)Start sending a file
xTransferRecvFilexErrno xTransferRecvFile(xTransfer xfer, const char *code, const char *dest_dir)Start receiving a file
xTransferGetStatexTransferState xTransferGetState(xTransfer xfer)Query current state
xTransferGetRolexTransferRole xTransferGetRole(xTransfer xfer)Query role (sender/receiver)
xTransferCancelvoid xTransferCancel(xTransfer xfer)Cancel an in-progress transfer

SDP Negotiation (Advanced)

These functions are used internally by the signaling client but are exposed for manual SDP exchange scenarios:

FunctionSignatureDescription
xTransferCreateOfferchar *xTransferCreateOffer(xTransfer xfer)Create SDP offer (sender, caller frees)
xTransferCreateAnswerchar *xTransferCreateAnswer(xTransfer xfer)Create SDP answer (receiver, caller frees)
xTransferSetLocalDescriptionxErrno xTransferSetLocalDescription(xTransfer xfer, const char *sdp)Set local SDP
xTransferSetRemoteDescriptionxErrno xTransferSetRemoteDescription(xTransfer xfer, const char *sdp)Set remote SDP
xTransferGatherCandidatesxErrno xTransferGatherCandidates(xTransfer xfer)Start ICE gathering

Signaling Server

FunctionSignatureDescription
xSignalServerCreatexSignalServer xSignalServerCreate(xEventLoop loop, const xSignalServerConf *conf)Create and start a signaling server
xSignalServerDestroyvoid xSignalServerDestroy(xSignalServer server)Destroy the server

Signaling Client

FunctionSignatureDescription
xSignalClientCreatexSignalClient xSignalClientCreate(xEventLoop loop, const xSignalClientConf *conf)Create and connect to signaling server
xSignalClientDestroyvoid xSignalClientDestroy(xSignalClient client)Destroy the client
xSignalClientSendOfferxErrno xSignalClientSendOffer(xSignalClient client, const char *sdp)Send SDP offer
xSignalClientSendAnswerxErrno xSignalClientSendAnswer(xSignalClient client, const char *sdp)Send SDP answer
xSignalClientSendCandidatexErrno xSignalClientSendCandidate(xSignalClient client, const char *candidate)Send ICE candidate

State Machine

stateDiagram-v2
    [*] --> Idle: xTransferCreate()
    Idle --> WaitingPeer: xTransferSendFile() / xTransferRecvFile()
    WaitingPeer --> Connecting: Peer joined, SDP exchanged
    Connecting --> Transferring: DataChannel opened
    Transferring --> Done: All chunks transferred + ACK
    Transferring --> Failed: Error / Cancel
    WaitingPeer --> Failed: Signaling error
    Connecting --> Failed: ICE / DTLS failure
    Done --> [*]
    Failed --> [*]

Quick Start

Sending a File

#include <xbase/event.h>
#include <xfer/xfer.h>

#include <signal.h>
#include <stdio.h>
#include <string.h>

static xEventLoop g_loop;
static xTransfer  g_xfer;

static void on_state_change(xTransfer xfer, xTransferState state, void *ctx) {
  (void)xfer; (void)ctx;
  switch (state) {
  case xTransferState_Done:
    printf("\n✅ Transfer complete!\n");
    xEventLoopStop(g_loop);
    return;
  case xTransferState_Failed:
    printf("\n❌ Transfer failed.\n");
    xEventLoopStop(g_loop);
    return;
  default: break;
  }
}

static void on_progress(xTransfer xfer, uint64_t transferred,
                        uint64_t total, void *ctx) {
  (void)xfer; (void)ctx;
  printf("\rProgress: %llu / %llu bytes (%.1f%%)   ",
         (unsigned long long)transferred, (unsigned long long)total,
         total > 0 ? 100.0 * transferred / total : 0.0);
  fflush(stdout);
}

static void on_code(xTransfer xfer, const char *code, void *ctx) {
  (void)xfer; (void)ctx;
  printf("Share this code with the receiver:\n  %s\n", code);
}

int main(void) {
  g_loop = xEventLoopCreate();

  xTransferConf conf;
  memset(&conf, 0, sizeof(conf));
  conf.stun_server     = "stun.l.google.com:19302";
  conf.signal_server   = "ws://127.0.0.1:8080/ws";
  conf.on_state_change = on_state_change;
  conf.on_progress     = on_progress;
  conf.on_code         = on_code;
  conf.vfs             = NULL; /* NULL = default POSIX VFS */

  g_xfer = xTransferCreate(g_loop, &conf);
  xTransferSendFile(g_xfer, "myfile.bin");

  xEventLoopRun(g_loop);

  xTransferDestroy(g_xfer);
  xEventLoopDestroy(g_loop);
  return 0;
}

Receiving a File

#include <xbase/event.h>
#include <xfer/xfer.h>

#include <stdio.h>
#include <string.h>

static xEventLoop g_loop;
static xTransfer  g_xfer;

static void on_state_change(xTransfer xfer, xTransferState state, void *ctx) {
  (void)xfer; (void)ctx;
  switch (state) {
  case xTransferState_Done:
    printf("\n✅ File received!\n");
    xEventLoopStop(g_loop);
    return;
  case xTransferState_Failed:
    printf("\n❌ Transfer failed.\n");
    xEventLoopStop(g_loop);
    return;
  default: break;
  }
}

static void on_progress(xTransfer xfer, uint64_t transferred,
                        uint64_t total, void *ctx) {
  (void)xfer; (void)ctx;
  printf("\rProgress: %llu / %llu bytes (%.1f%%)   ",
         (unsigned long long)transferred, (unsigned long long)total,
         total > 0 ? 100.0 * transferred / total : 0.0);
  fflush(stdout);
}

static void on_file_meta(xTransfer xfer, const char *filename,
                         uint64_t filesize, void *ctx) {
  (void)xfer; (void)ctx;
  printf("Incoming: \"%s\" (%llu bytes)\n",
         filename, (unsigned long long)filesize);
}

int main(void) {
  g_loop = xEventLoopCreate();

  xTransferConf conf;
  memset(&conf, 0, sizeof(conf));
  conf.stun_server     = "stun.l.google.com:19302";
  conf.signal_server   = "ws://127.0.0.1:8080/ws";
  conf.on_state_change = on_state_change;
  conf.on_progress     = on_progress;
  conf.on_file_meta    = on_file_meta;

  g_xfer = xTransferCreate(g_loop, &conf);
  xTransferRecvFile(g_xfer, "AB12CD", "/tmp/received");

  xEventLoopRun(g_loop);

  xTransferDestroy(g_xfer);
  xEventLoopDestroy(g_loop);
  return 0;
}

Running the Examples

The examples/ directory includes complete sender and receiver programs:

# Terminal 1: Start the signaling server (built-in)
# The signaling server is started automatically by xfer when needed,
# or you can run a standalone one.
./xfer_signal -p 8080

# Terminal 2: Send a file
./xfer_send -f myfile.bin -u ws://127.0.0.1:8080/ws

# Terminal 3: Receive the file (use the code printed by the sender)
./xfer_recv -c AB12CD -u ws://127.0.0.1:8080/ws -d /tmp/received

Command-line options:

Optionxfer_sendxfer_recvDescription
-f <file>✅ RequiredFile to send
-c <code>✅ RequiredTransfer code from sender (plain session ID)
-d <dir>OptionalDestination directory (default: /tmp/xfer_recv)
-u <url>✅ Required✅ RequiredSignaling server URL
-s <host:port>OptionalOptionalSTUN server (default: stun.l.google.com:19302)
-6OptionalOptionalEnable IPv6 candidates

Relationship with Other Modules

  • xp2p — Uses xPeerConnection for the full WebRTC DataChannel stack (ICE + DTLS + SCTP + DataChannel). xfer creates a PeerConnection internally and sends/receives file data over a DataChannel.
  • xhttp — The signaling server and client use xhttp's WebSocket server and client for SDP/ICE relay.
  • xbase — Uses xEventLoop for I/O multiplexing and the single-threaded callback model.
  • xcrypto — Uses SHA-1 for file integrity verification.
  • xnet — Uses URL parsing for signaling server addresses.

Custom VFS Example

The following example shows how to implement a minimal in-memory VFS for testing:

#include <xfer/xfer_vfs.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
  uint8_t *data;
  uint64_t size;
  uint64_t capacity;
} MemFile;

static void *mem_open(void *ctx, const char *path, const char *mode) {
  (void)ctx; (void)path; (void)mode;
  MemFile *f = calloc(1, sizeof(MemFile));
  return f;
}

static xErrno mem_pread(void *ctx, void *handle, uint8_t *buf,
                        size_t len, uint64_t offset, size_t *nread) {
  (void)ctx;
  MemFile *f = handle;
  if (offset >= f->size) { *nread = 0; return xErrno_Ok; }
  size_t avail = (size_t)(f->size - offset);
  size_t n = len < avail ? len : avail;
  memcpy(buf, f->data + offset, n);
  *nread = n;
  return xErrno_Ok;
}

static xErrno mem_pwrite(void *ctx, void *handle, const uint8_t *buf,
                         size_t len, uint64_t offset, size_t *nwritten) {
  (void)ctx;
  MemFile *f = handle;
  uint64_t end = offset + len;
  if (end > f->capacity) {
    f->data = realloc(f->data, (size_t)end);
    f->capacity = end;
  }
  memcpy(f->data + offset, buf, len);
  if (end > f->size) f->size = end;
  *nwritten = len;
  return xErrno_Ok;
}

static xErrno mem_size(void *ctx, void *handle, uint64_t *out) {
  (void)ctx;
  *out = ((MemFile *)handle)->size;
  return xErrno_Ok;
}

static xErrno mem_flush(void *ctx, void *handle) {
  (void)ctx; (void)handle;
  return xErrno_Ok; /* no-op for in-memory */
}

static void mem_close(void *ctx, void *handle) {
  (void)ctx;
  MemFile *f = handle;
  if (f) { free(f->data); free(f); }
}

static const xTransferVfs g_mem_vfs = {
  .ctx      = NULL,
  .open     = mem_open,
  .pread    = mem_pread,
  .pwrite   = mem_pwrite,
  .size     = mem_size,
  .truncate = NULL,  /* optional */
  .flush    = mem_flush,
  .close    = mem_close,
  .rename   = NULL,  /* optional */
  .remove   = NULL,  /* optional */
};

/* Usage: */
xTransferConf conf;
memset(&conf, 0, sizeof(conf));
conf.vfs = &g_mem_vfs;
/* ... set other fields ... */
xTransfer xfer = xTransferCreate(loop, &conf);