Skip to content

Instantly share code, notes, and snippets.

@masakielastic
Last active March 2, 2026 04:36
Show Gist options
  • Select an option

  • Save masakielastic/899a9560ad6731b8f3502fbdef09f6c2 to your computer and use it in GitHub Desktop.

Select an option

Save masakielastic/899a9560ad6731b8f3502fbdef09f6c2 to your computer and use it in GitHub Desktop.
OpenSSL で QUIC クライアント

OpenSSL で QUIC クライアント

ローカルビルドした OpenSSL の証明書のパスが不適切な設定であるために手間取りました。 証明書を設定して適切に動くかどうかどうかは次のコマンドで確認することができます。

openssl s_client -4 -connect nghttp2.org:443 -servername nghttp2.org -tls1_3 -CAfile /etc/ssl/certs/ca-certificates.crt </dev/null

また IPv6 では接続できない現象もあるので、IPv4 に固定して実験を行いました。

// client.c - OpenSSL QUIC handshake-only client (nonblocking, poll, hard timeout)
// Usage:
// ./client [connect_host] [port] [sni_host] [-4|-6]
// Examples:
// ./client
// ./client nghttp2.org 443 nghttp2.org -4
// ./client 139.162.123.134 443 nghttp2.org
//
// NOTE: This establishes QUIC+TLS with ALPN "h3" only.
// It does NOT implement HTTP/3 request/response framing.
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/quic.h>
#include <openssl/x509v3.h>
#include <poll.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
static void die_openssl(const char *msg) {
fprintf(stderr, "%s\n", msg);
ERR_print_errors_fp(stderr);
_exit(1);
}
static long long now_ms(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (long long)ts.tv_sec * 1000LL + ts.tv_nsec / 1000000LL;
}
static int poll_wait(int fd, int timeout_ms) {
struct pollfd p;
memset(&p, 0, sizeof(p));
p.fd = fd;
p.events = POLLIN | POLLOUT;
return poll(&p, 1, timeout_ms);
}
static BIO *find_connect_bio(BIO *head) {
// Walk the BIO chain and find the connect BIO reliably
for (BIO *b = head; b != NULL; b = BIO_next(b)) {
if (BIO_method_type(b) == BIO_TYPE_CONNECT) {
return b;
}
}
return NULL;
}
// Resolve hostname to a numeric IP string (v4 or v6). Returns 1 on success.
static int resolve_ip_literal(const char *host, int family, char *out, size_t out_len) {
struct addrinfo hints, *res = NULL;
memset(&hints, 0, sizeof(hints));
hints.ai_family = family; // AF_INET or AF_INET6
hints.ai_socktype = SOCK_DGRAM; // QUIC is UDP
hints.ai_protocol = IPPROTO_UDP;
int rc = getaddrinfo(host, NULL, &hints, &res);
if (rc != 0 || res == NULL) return 0;
void *addr = NULL;
if (res->ai_family == AF_INET) {
addr = &((struct sockaddr_in *)res->ai_addr)->sin_addr;
} else if (res->ai_family == AF_INET6) {
addr = &((struct sockaddr_in6 *)res->ai_addr)->sin6_addr;
} else {
freeaddrinfo(res);
return 0;
}
const char *p = inet_ntop(res->ai_family, addr, out, (socklen_t)out_len);
freeaddrinfo(res);
return p != NULL;
}
int main(int argc, char **argv) {
const char *connect_host = "nghttp2.org";
const char *port = "443";
const char *sni_host = NULL; // default to connect_host
int force_v4 = 0;
int force_v6 = 0;
if (argc >= 2) connect_host = argv[1];
if (argc >= 3) port = argv[2];
if (argc >= 4) sni_host = argv[3];
if (argc >= 5) {
if (strcmp(argv[4], "-4") == 0) force_v4 = 1;
else if (strcmp(argv[4], "-6") == 0) force_v6 = 1;
}
if (!sni_host) sni_host = connect_host;
// If forced, resolve to IP literal to avoid OpenSSL connect BIO family controls (which are fragile).
char resolved_ip[INET6_ADDRSTRLEN];
const char *connect_target = connect_host;
const char *family_str = "auto";
if (force_v4) {
family_str = "IPv4";
if (resolve_ip_literal(connect_host, AF_INET, resolved_ip, sizeof(resolved_ip))) {
connect_target = resolved_ip;
} else {
fprintf(stderr, "resolve IPv4 failed for host='%s'\n", connect_host);
_exit(2);
}
} else if (force_v6) {
family_str = "IPv6";
if (resolve_ip_literal(connect_host, AF_INET6, resolved_ip, sizeof(resolved_ip))) {
connect_target = resolved_ip;
} else {
fprintf(stderr, "resolve IPv6 failed for host='%s'\n", connect_host);
_exit(2);
}
}
SSL_library_init();
SSL_load_error_strings();
SSL_CTX *ctx = SSL_CTX_new(OSSL_QUIC_client_method());
if (!ctx) die_openssl("SSL_CTX_new(OSSL_QUIC_client_method) failed");
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
if (SSL_CTX_set_default_verify_paths(ctx) != 1)
die_openssl("SSL_CTX_set_default_verify_paths failed");
if (SSL_CTX_load_verify_locations(ctx, "/etc/ssl/certs/ca-certificates.crt", NULL) != 1) {
die_openssl("SSL_CTX_load_verify_locations(/etc/ssl/certs/ca-certificates.crt) failed");
}
BIO *bio = BIO_new_ssl_connect(ctx);
if (!bio) die_openssl("BIO_new_ssl_connect failed");
// Find the real connect BIO in the chain (don’t assume BIO_next once)
BIO *cbio = find_connect_bio(bio);
if (!cbio) die_openssl("connect BIO not found in BIO chain");
// Nonblocking on both
BIO_set_nbio(bio, 1);
BIO_set_nbio(cbio, 1);
char hostport[256];
snprintf(hostport, sizeof(hostport), "%s:%s", connect_target, port);
// Set host:port on CONNECT BIO ONLY
if (BIO_set_conn_hostname(cbio, hostport) != 1)
die_openssl("BIO_set_conn_hostname(connect_bio) failed");
fprintf(stderr, "connect='%s' sni='%s' family=%s\n", hostport, sni_host, family_str);
SSL *ssl = NULL;
BIO_get_ssl(bio, &ssl);
if (!ssl) die_openssl("BIO_get_ssl failed");
// SNI + hostname verification uses sni_host (keep hostname even if connecting to an IP)
if (SSL_set_tlsext_host_name(ssl, sni_host) != 1)
die_openssl("SSL_set_tlsext_host_name failed");
if (SSL_set1_host(ssl, sni_host) != 1)
die_openssl("SSL_set1_host failed");
// QUIC requires ALPN; for HTTP/3: "h3" (wire format)
static const unsigned char alpn_h3[] = { 0x02, 'h', '3' };
if (SSL_set_alpn_protos(ssl, alpn_h3, sizeof(alpn_h3)) != 0)
die_openssl("SSL_set_alpn_protos(h3) failed");
const int total_timeout_ms = 5000;
const int step_ms = 200;
long long start = now_ms();
// Step 1: obtain FD from connect BIO (may appear after progress)
int fd = -1;
while (1) {
BIO_get_fd(cbio, &fd);
if (fd >= 0) break;
int cr = BIO_do_connect(cbio);
if (cr > 0) continue;
long verr = SSL_get_verify_result(ssl);
fprintf(stderr, "verify_result=%ld (%s)\n", verr, X509_verify_cert_error_string(verr));
X509 *cert = SSL_get0_peer_certificate(ssl);
if (cert) {
char subj[256];
X509_NAME_oneline(X509_get_subject_name(cert), subj, sizeof(subj));
fprintf(stderr, "peer_subject=%s\n", subj);
// Hostname check (nghttp2.org)
int ok = X509_check_host(cert, sni_host, 0, 0, NULL);
fprintf(stderr, "X509_check_host(%s)=%d\n", sni_host, ok);
}
if (!BIO_should_retry(cbio)) {
die_openssl("BIO_do_connect(connect_bio) failed (no retry)");
}
if (now_ms() - start >= total_timeout_ms) {
fprintf(stderr, "timeout: could not obtain UDP socket fd in %d ms\n", total_timeout_ms);
_exit(3);
}
usleep(step_ms * 1000);
}
// Step 2: drive full QUIC+TLS handshake via chain head
while (1) {
int r = BIO_do_connect(bio);
if (r > 0) break;
if (!BIO_should_retry(bio))
die_openssl("BIO_do_connect(bio) failed (no retry)");
if (now_ms() - start >= total_timeout_ms) {
fprintf(stderr, "timeout: QUIC connect/handshake not completed in %d ms\n", total_timeout_ms);
_exit(4);
}
int pr = poll_wait(fd, step_ms);
if (pr < 0) die_openssl("poll failed");
}
fprintf(stderr, "connected (QUIC channel started)\n");
const unsigned char *sel = NULL;
unsigned int sel_len = 0;
SSL_get0_alpn_selected(ssl, &sel, &sel_len);
if (sel && sel_len > 0)
fprintf(stderr, "ALPN selected: %.*s\n", (int)sel_len, (const char *)sel);
else
fprintf(stderr, "ALPN selected: (none)\n");
BIO_free_all(bio);
SSL_CTX_free(ctx);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment