A multithreaded FTP server written in C that implements the RFC 959 minimum specification, with per-client bandwidth throttling (QoS) and support for both active (PORT) and passive (PASV) data transfers.
- RFC 959 minimum implementation — USER, QUIT, PORT, TYPE, MODE, STRU, RETR, STOR, NOOP
- Extended commands — LIST, CWD, CDUP, PWD, MKD, RMD, SYST, PASV
- Concurrent clients — one POSIX thread per active control connection
- Dual data modes — active (PORT) and passive (PASV)
- Per-client QoS — configurable bandwidth limits via
config.inior fair-share defaults - Colored logging — control-channel traffic, transfer stats, and connection events
Username, password, and permission checks are not enforced. The server assumes all credentials are valid and that clients may access any directory on the host filesystem.
flowchart TB
subgraph Client["FTP Client"]
CC[Control Connection]
DC[Data Connection]
end
subgraph Server["FTP Server (server.out)"]
MS[Main Loop<br/>accept + admission]
CFG[config.ini<br/>per-IP rates]
QOS[Global bandwidth pool<br/>taxa_servidor / taxa_atual]
subgraph Threads["Per-client threads"]
T1[multUser thread 1]
T2[multUser thread 2]
TN[multUser thread N]
end
subgraph Handlers["Command handlers (server_func.c)"]
DEC[decode_message]
AUTH[USER / PASS / QUIT]
NAV[CWD / CDUP / PWD / MKD / RMD]
XFER[RETR / STOR / LIST]
MODE[PORT / PASV / TYPE]
end
end
FS[(Local filesystem)]
CC <-->|"TCP control (default :2300)"| MS
MS -->|"pthread_create"| Threads
MS --> CFG
MS --> QOS
Threads --> DEC --> Handlers
DC <-->|"TCP data (PORT or PASV)"| XFER
XFER --> FS
NAV --> FS
| File | Role |
|---|---|
server.c |
Entry point, socket setup, connection admission, thread spawning |
server_func.c |
Socket helpers, command decoding, FTP command implementations, QoS throttling |
server_func.h |
ConnectionStatus struct and function declarations |
config.ini |
Optional per-IP bandwidth reservations |
compile.sh |
Build script (gcc) and convenience launcher |
FTP uses two TCP connections: a persistent control channel for commands and responses, and a short-lived data channel for file and directory transfers.
sequenceDiagram
participant C as FTP Client
participant CTL as Control Socket
participant S as Server Thread
participant DATA as Data Socket
participant FS as Filesystem
C->>CTL: Connect (e.g. port 2300)
CTL->>C: 220 Service ready for new user
alt Active mode (PORT)
C->>CTL: PORT h1,h2,h3,h4,p1,p2
CTL->>C: 200 Command okay
C->>CTL: RETR file.txt
S->>DATA: connect to client IP:port
CTL->>C: 150 File status okay
S->>FS: read file
S->>DATA: stream bytes (rate-limited)
CTL->>C: 250 Requested file action okay
else Passive mode (PASV)
C->>CTL: PASV
CTL->>C: 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2)
C->>CTL: RETR file.txt
C->>DATA: connect to server IP:port
CTL->>C: 150 File status okay
S->>FS: read file
S->>DATA: stream bytes (rate-limited)
CTL->>C: 250 Requested file action okay
end
Each accepted client is handled in its own multUser thread. Commands are parsed once per read loop iteration and dispatched through a switch statement.
flowchart LR
A[accept on control socket] --> B{clients < MAX_CLIENTS<br/>and bandwidth OK?}
B -->|No| C[421 Service not available]
B -->|Yes| D[Reserve client bandwidth]
D --> E[pthread_create → multUser]
E --> F[read command]
F --> G[decode_message]
G --> H{command type}
H -->|USER/PASS/CWD/...| I[func_* handler]
H -->|unknown| J[202 not implemented]
I --> K[write response]
J --> K
K --> L{connection_ok?}
L -->|Yes| F
L -->|No QUIT| M[Release bandwidth, close socket]
The server maintains a global bandwidth budget (taxa_servidor). Each client receives a reserved rate (taxa_transmissao) at connect time. Transfers throttle by sending at most that many bytes per second, pausing with usleep when the per-second quota is reached.
flowchart TD
START[Client connects] --> LOOKUP{IP in config.ini?}
LOOKUP -->|Yes| CUSTOM[Use configured rate]
LOOKUP -->|No| DEFAULT["Use fair share:<br/>taxa_servidor / MAX_CLIENTS"]
CUSTOM --> CHECK{"taxa_atual + rate ≤ taxa_servidor?"}
DEFAULT --> CHECK
CHECK -->|No| REJECT[421 — reject connection]
CHECK -->|Yes| ACCEPT[Accept + reserve rate]
ACCEPT --> XFER[RETR / STOR / LIST transfers]
XFER --> THROTTLE["Byte-by-byte send/receive<br/>pause after rate bytes/sec"]
THROTTLE --> DISCONNECT[Client disconnects]
DISCONNECT --> RELEASE[Release reserved bandwidth]
Make the build script executable, then run it:
chmod +x compile.sh
./compile.sh [interface] [port] [max_connections] [server_bandwidth_bytes]The script compiles with GCC:
gcc server*.c -o server.out -lpthreadYou can also compile and run the binary directly:
gcc server*.c -o server.out -lpthread
./server.out [interface] [port] [max_connections] [server_bandwidth_bytes]| Parameter | Default | Description |
|---|---|---|
| Interface | lo (loopback) |
Network interface to bind |
| Port | 2300 |
Control-channel TCP port |
| Max connections | 20 |
Simultaneous client threads |
| Server bandwidth | 2000000 (2 MB) |
Total bytes/sec budget across all clients |
Ports ≤ 1024 are rejected (insufficient privileges); the server falls back to port 2300.
Always start the server first. Connecting before the server is listening produces:
421 Service not available, closing control connection.
Build, specify interface, port, and connection limit:
$ ./compile.sh wlp2s0 2300 5
Compilando...
Gotcha!
--------------------------------------------------------------------------------
Info: Interface selecionada: wlp2s0.
Info: Utilizando interface selecionada.
Info: Porta selecionada: 2300.
Info: Número máximo de clientes conectados simultaneamente: 5.
Info: Utilizando limite total da taxa de transmissão padrão: 2000000.
Info: Socket criado com sucesso.
Info: Arquivo de configuração não encontrado, utilizando taxa padrão para todos os usuários.
Info: Rodando servidor em: 192.168.1.166:2300.
--------------------------------------------------------------------------------Run with all defaults (loopback, port 2300, 20 connections):
$ ./server.out
--------------------------------------------------------------------------------
Info: Interface não informada, utilizando interface padrão: lo.
Info: Porta padrão selecionada: 2300.
Info: Número máximo de clientes conectados simultaneamente não informado, utilizando valor padrão: 20.
Info: Utilizando limite total da taxa de transmissão padrão: 2000000.
Info: Socket criado com sucesso.
Info: Arquivo de configuração não encontrado, utilizando taxa padrão para todos os usuários.
Info: Rodando servidor em: 127.0.0.1:2300.
--------------------------------------------------------------------------------On client connect:
--------------------------------------------------------------------------------
Info: Conexão estabelecida com: 127.0.0.1:48830.
Info: Número de conexões atualmente: 1.
Info: Taxa de transmissão reservada/Taxa máxima definida: 100000/2000000.
Send: 220 Service ready for new user.
--------------------------------------------------------------------------------
Per RFC 959, servers must support at least:
| Category | Required values |
|---|---|
| TYPE | ASCII Non-print (A) |
| MODE | Stream |
| STRUCTURE | File, Record |
| Commands | USER, QUIT, PORT, TYPE, MODE, STRU, RETR, STOR, NOOP |
Default transfer parameters:
| Parameter | Default |
|---|---|
| TYPE | ASCII Non-print |
| MODE | Stream |
| STRU | File |
| Command | Description |
|---|---|
USER |
Accept username (no validation) |
PASS |
Accept password (no validation) |
QUIT |
Close control connection |
PORT |
Active data mode — client provides IP and port |
PASV |
Passive data mode — server listens on ephemeral port |
TYPE |
Set transfer type (ASCII A only) |
RETR |
Download a file |
STOR |
Upload a file |
NOOP |
No-operation keepalive |
| Command | Description |
|---|---|
LIST |
Directory listing via ls -l |
CWD |
Change working directory |
CDUP |
Change to parent directory |
PWD |
Print working directory |
MKD |
Create directory |
RMD |
Remove directory |
SYST |
Report system type (215 UNIX system type.) |
Any other RFC 959 command receives:
202 Command not implemented, superfluous at this site.
If config.ini is missing, every client receives an equal share of the server bandwidth:
default_rate = server_bandwidth / max_connections
For the defaults: 2,000,000 / 20 = 100,000 B/s per client.
Create a config.ini file in the working directory. Each line maps a client IP to a reserved transfer rate:
172.16.14.88 1 M
172.16.14.55 2 M
192.168.1.166 500 K
127.0.0.1 10 BFormat: <ip_address> <number> <unit>
| Unit letter | Meaning |
|---|---|
G / g |
Gigabytes per second |
M / m |
Megabytes per second |
K / k |
Kilobytes per second |
B / b |
Bytes per second |
All bandwidth values are stored and enforced in bytes per second, even when log messages omit the unit.
Connections are rejected if a client's reserved rate would cause taxa_atual + client_rate to exceed taxa_servidor. Only clients with a guaranteed slice of the total budget are admitted.
During RETR, STOR, or LIST, the server logs transfer size, elapsed time, and effective throughput:
--------------------------------------------------------------------------------
Recv: RETR config.ini
Info: Arquivo selecionado: config.ini.
Send: 150 File status okay; about to open data connection.
Info: Tamanho do arquivo: 73B.
Info: Tempo de processamento: 7.001747s.
Info: Taxa de processamento: 10.43B/s.
Send: 250 Requested file action okay, completed.
--------------------------------------------------------------------------------
Reported rates can differ from what an FTP client displays because timing includes non-network work (counter updates, scheduling). Larger discrepancies are common on STOR, where clients often send data in bursts while the server processes it byte-by-byte with throttling.
- GCC
- POSIX threads (
libpthread) - Linux networking headers (
netinet,arpa/inet, etc.)