Skip to content

Instantly share code, notes, and snippets.

@masakielastic
Last active March 11, 2026 06:20
Show Gist options
  • Select an option

  • Save masakielastic/7d2d29768bc73ea831db9b95b70ea31b to your computer and use it in GitHub Desktop.

Select an option

Save masakielastic/7d2d29768bc73ea831db9b95b70ea31b to your computer and use it in GitHub Desktop.
Minimal TLS Client in C with Explicit Connection State (QUIC Preparation)
/*
Minimal TLS Client in C (Architecture Exercise for QUIC Preparation)
This program implements a small TLS + TCP client using OpenSSL.
The goal of this code is not merely to demonstrate how to perform
a TLS request, but to illustrate a connection-oriented design that
resembles modern network protocol libraries.
Instead of writing everything inside main(), the implementation
intentionally separates responsibilities into multiple functions:
- tcp connection setup
- TLS context creation
- TLS session creation
- handshake phase
- application data exchange
- connection teardown
The connection state is also tracked explicitly using a small
state machine:
INIT
TCP_OPEN
TLS_OPEN
HANDSHAKING
READY
CLOSED
ERROR
This structure mirrors how real protocol implementations are often
designed. Libraries for modern protocols such as QUIC typically
separate:
transport layer
crypto layer
connection lifecycle
application protocol
Therefore this example can be used as a preparation exercise before
studying QUIC or HTTP/3 libraries.
Another design goal of this example is to make learning easier.
Because responsibilities are split into small functions, the code
can be modified incrementally. Readers can copy the code and
experiment with individual parts without rewriting the entire
program.
Typical experiments include:
- introducing non-blocking I/O
- integrating an event loop
- adding ALPN negotiation
- extending the connection state machine
Build example:
gcc tls-client.c -Wall -Wextra -O2 \
-o tls-client $(pkg-config --cflags --libs openssl)
This code is intended for educational purposes.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/x509_vfy.h>
typedef struct tls_client_config {
const char *host;
const char *port;
const char *server_name;
const char *ca_file;
const char *ca_path;
int min_proto_version;
int verify_peer;
} tls_client_config;
typedef enum tls_client_state {
TLS_CLIENT_STATE_INIT = 0,
TLS_CLIENT_STATE_TCP_OPEN,
TLS_CLIENT_STATE_TLS_OPEN,
TLS_CLIENT_STATE_HANDSHAKING,
TLS_CLIENT_STATE_READY,
TLS_CLIENT_STATE_CLOSED,
TLS_CLIENT_STATE_ERROR
} tls_client_state;
typedef struct tls_client {
tls_client_config config;
int fd;
SSL_CTX *ctx;
SSL *ssl;
tls_client_state state;
} tls_client;
static void print_openssl_error(const char *where) {
fprintf(stderr, "%s\n", where);
ERR_print_errors_fp(stderr);
}
static int tcp_connect(const char *host, const char *port) {
struct addrinfo hints;
struct addrinfo *res = NULL;
struct addrinfo *rp;
int fd = -1;
int rc;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
rc = getaddrinfo(host, port, &hints, &res);
if (rc != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rc));
return -1;
}
for (rp = res; rp != NULL; rp = rp->ai_next) {
fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (fd < 0) {
continue;
}
if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) {
break;
}
close(fd);
fd = -1;
}
freeaddrinfo(res);
return fd;
}
static SSL_CTX *tls_client_ctx_create(const tls_client_config *cfg) {
SSL_CTX *ctx = NULL;
if (cfg == NULL) {
fprintf(stderr, "tls_client_ctx_create: cfg is NULL\n");
return NULL;
}
if (OPENSSL_init_ssl(0, NULL) != 1) {
print_openssl_error("OPENSSL_init_ssl failed");
return NULL;
}
ctx = SSL_CTX_new(TLS_client_method());
if (ctx == NULL) {
print_openssl_error("SSL_CTX_new failed");
return NULL;
}
if (cfg->min_proto_version != 0) {
if (SSL_CTX_set_min_proto_version(ctx, cfg->min_proto_version) != 1) {
print_openssl_error("SSL_CTX_set_min_proto_version failed");
SSL_CTX_free(ctx);
return NULL;
}
}
if (cfg->verify_peer) {
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
if (cfg->ca_file != NULL || cfg->ca_path != NULL) {
if (SSL_CTX_load_verify_locations(ctx, cfg->ca_file, cfg->ca_path) != 1) {
print_openssl_error("SSL_CTX_load_verify_locations failed");
SSL_CTX_free(ctx);
return NULL;
}
} else {
if (SSL_CTX_set_default_verify_paths(ctx) != 1) {
print_openssl_error("SSL_CTX_set_default_verify_paths failed");
SSL_CTX_free(ctx);
return NULL;
}
}
} else {
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
}
return ctx;
}
static SSL *tls_client_ssl_create(SSL_CTX *ctx, int fd, const tls_client_config *cfg) {
SSL *ssl = NULL;
if (ctx == NULL || cfg == NULL) {
fprintf(stderr, "tls_client_ssl_create: invalid argument\n");
return NULL;
}
ssl = SSL_new(ctx);
if (ssl == NULL) {
print_openssl_error("SSL_new failed");
return NULL;
}
if (SSL_set_fd(ssl, fd) != 1) {
print_openssl_error("SSL_set_fd failed");
SSL_free(ssl);
return NULL;
}
if (cfg->server_name != NULL) {
if (SSL_set_tlsext_host_name(ssl, cfg->server_name) != 1) {
print_openssl_error("SSL_set_tlsext_host_name failed");
SSL_free(ssl);
return NULL;
}
if (cfg->verify_peer) {
if (SSL_set1_host(ssl, cfg->server_name) != 1) {
print_openssl_error("SSL_set1_host failed");
SSL_free(ssl);
return NULL;
}
}
}
return ssl;
}
static void tls_client_init(tls_client *client, const tls_client_config *cfg)
{
memset(client, 0, sizeof(*client));
client->fd = -1;
client->state = TLS_CLIENT_STATE_INIT;
if (cfg != NULL) {
client->config = *cfg;
}
}
static int tls_client_open_tcp(tls_client *client)
{
if (client->state != TLS_CLIENT_STATE_INIT) {
fprintf(stderr, "invalid state for tcp open\n");
return 0;
}
client->fd = tcp_connect(client->config.host, client->config.port);
if (client->fd < 0) {
client->state = TLS_CLIENT_STATE_ERROR;
return 0;
}
client->state = TLS_CLIENT_STATE_TCP_OPEN;
return 1;
}
static int tls_client_open_tls(tls_client *client)
{
if (client->state != TLS_CLIENT_STATE_TCP_OPEN) {
fprintf(stderr, "tcp not opened\n");
return 0;
}
client->ctx = tls_client_ctx_create(&client->config);
if (!client->ctx) {
client->state = TLS_CLIENT_STATE_ERROR;
return 0;
}
client->ssl = tls_client_ssl_create(client->ctx, client->fd, &client->config);
if (!client->ssl) {
client->state = TLS_CLIENT_STATE_ERROR;
return 0;
}
client->state = TLS_CLIENT_STATE_TLS_OPEN;
return 1;
}
static int tls_client_do_handshake(tls_client *client)
{
int rc;
if (client->state != TLS_CLIENT_STATE_TLS_OPEN) {
fprintf(stderr, "tls not ready\n");
return 0;
}
client->state = TLS_CLIENT_STATE_HANDSHAKING;
rc = SSL_connect(client->ssl);
if (rc != 1) {
long vr = SSL_get_verify_result(client->ssl);
fprintf(stderr, "SSL_connect failed\n");
fprintf(stderr,
"verify_result=%ld: %s\n",
vr,
X509_verify_cert_error_string(vr));
ERR_print_errors_fp(stderr);
client->state = TLS_CLIENT_STATE_ERROR;
return 0;
}
client->state = TLS_CLIENT_STATE_READY;
return 1;
}
static int tls_client_connect(tls_client *client) {
if (!tls_client_open_tls(client)) {
return 0;
}
if (!tls_client_open_tls(client)) {
return 0;
}
if (!tls_client_do_handshake(client)) {
return 0;
}
return 1;
}
static int tls_client_write_all(tls_client *client, const void *buf, size_t len)
{
const unsigned char *p = buf;
size_t written = 0;
if (client->state != TLS_CLIENT_STATE_READY) {
fprintf(stderr, "connection not ready\n");
return 0;
}
while (written < len) {
int n = SSL_write(client->ssl,
p + written,
(int)(len - written));
if (n <= 0) {
print_openssl_error("SSL_write failed");
client->state = TLS_CLIENT_STATE_ERROR;
return 0;
}
written += n;
}
return 1;
}
static int tls_client_read_to_stdout(tls_client *client)
{
char buf[4096];
int n;
if (client->state != TLS_CLIENT_STATE_READY) {
fprintf(stderr, "connection not ready\n");
return 0;
}
while ((n = SSL_read(client->ssl, buf, sizeof(buf))) > 0) {
if (fwrite(buf, 1, n, stdout) != (size_t)n) {
perror("fwrite");
client->state = TLS_CLIENT_STATE_ERROR;
return 0;
}
}
return 1;
}
static void tls_client_close(tls_client *client)
{
if (!client) return;
if (client->ssl) {
SSL_shutdown(client->ssl);
SSL_free(client->ssl);
client->ssl = NULL;
}
if (client->ctx) {
SSL_CTX_free(client->ctx);
client->ctx = NULL;
}
if (client->fd >= 0) {
close(client->fd);
client->fd = -1;
}
client->state = TLS_CLIENT_STATE_CLOSED;
}
int main(void)
{
tls_client client;
tls_client_config cfg = {
.host = "example.com",
.port = "443",
.server_name = "example.com",
.ca_file = NULL,
.ca_path = NULL,
.min_proto_version = TLS1_2_VERSION,
.verify_peer = 1
};
const char *req =
"GET / HTTP/1.1\r\n"
"Host: example.com\r\n"
"Connection: close\r\n"
"\r\n";
tls_client_init(&client, &cfg);
if (!tls_client_open_tcp(&client))
goto cleanup;
if (!tls_client_open_tls(&client))
goto cleanup;
if (!tls_client_do_handshake(&client))
goto cleanup;
if (!tls_client_write_all(&client, req, strlen(req)))
goto cleanup;
tls_client_read_to_stdout(&client);
cleanup:
tls_client_close(&client);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment