blob: 20f2b55bd16f29bbc7500d1195f860f2b69a838f [file] [log] [blame]
/*
* Copyright (c) 2016 DeNA Co., Ltd., Kazuho Oku
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/nameser.h>
#include <resolv.h>
#include <arpa/inet.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#define OPENSSL_API_COMPAT 0x00908000L
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/engine.h>
#include <openssl/pem.h>
#if PICOTLS_USE_BROTLI
#include "brotli/decode.h"
#endif
#include "picotls.h"
#include "picotls/openssl.h"
#if PICOTLS_USE_BROTLI
#include "picotls/certificate_compression.h"
#endif
#include "util.h"
/* sentinels indicating that the endpoint is in benchmark mode */
static const char input_file_is_benchmark[] = "is:benchmark";
static void shift_buffer(ptls_buffer_t *buf, size_t delta)
{
if (delta != 0) {
assert(delta <= buf->off);
if (delta != buf->off)
memmove(buf->base, buf->base + delta, buf->off - delta);
buf->off -= delta;
}
}
static void setup_ptlslog(const char *fn)
{
int fd;
if ((fd = open(fn, O_WRONLY | O_CREAT | O_APPEND, 0666)) == -1) {
fprintf(stderr, "failed to open file:%s:%s\n", fn, strerror(errno));
exit(1);
}
ptls_log_add_fd(fd);
}
static int handle_connection(int sockfd, ptls_context_t *ctx, const char *server_name, const char *input_file,
ptls_handshake_properties_t *hsprop, int request_key_update, int keep_sender_open)
{
static const int inputfd_is_benchmark = -2;
ptls_t *tls = ptls_new(ctx, server_name == NULL);
ptls_buffer_t rbuf, encbuf, ptbuf;
enum { IN_HANDSHAKE, IN_1RTT, IN_SHUTDOWN } state = IN_HANDSHAKE;
int inputfd = 0, ret = 0;
size_t early_bytes_sent = 0;
uint64_t data_received = 0;
ssize_t ioret;
uint64_t start_at = ctx->get_time->cb(ctx->get_time);
ptls_buffer_init(&rbuf, "", 0);
ptls_buffer_init(&encbuf, "", 0);
ptls_buffer_init(&ptbuf, "", 0);
fcntl(sockfd, F_SETFL, O_NONBLOCK);
if (input_file == input_file_is_benchmark) {
if (!ptls_is_server(tls))
inputfd = inputfd_is_benchmark;
} else if (input_file != NULL) {
if ((inputfd = open(input_file, O_RDONLY)) == -1) {
fprintf(stderr, "failed to open file:%s:%s\n", input_file, strerror(errno));
ret = 1;
goto Exit;
}
}
if (server_name != NULL) {
ptls_set_server_name(tls, server_name, 0);
if ((ret = ptls_handshake(tls, &encbuf, NULL, NULL, hsprop)) != PTLS_ERROR_IN_PROGRESS) {
fprintf(stderr, "ptls_handshake:%d\n", ret);
ret = 1;
goto Exit;
}
}
while (1) {
/* check if data is available */
fd_set readfds, writefds, exceptfds;
int maxfd = 0;
struct timeval timeout;
do {
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_ZERO(&exceptfds);
FD_SET(sockfd, &readfds);
if (encbuf.off != 0 || inputfd == inputfd_is_benchmark)
FD_SET(sockfd, &writefds);
FD_SET(sockfd, &exceptfds);
maxfd = sockfd + 1;
if (inputfd >= 0) {
FD_SET(inputfd, &readfds);
FD_SET(inputfd, &exceptfds);
if (maxfd <= inputfd)
maxfd = inputfd + 1;
}
timeout.tv_sec = encbuf.off != 0 ? 0 : 3600;
timeout.tv_usec = 0;
} while (select(maxfd, &readfds, &writefds, &exceptfds, &timeout) == -1);
/* consume incoming messages */
if (FD_ISSET(sockfd, &readfds) || FD_ISSET(sockfd, &exceptfds)) {
char bytebuf[16384];
size_t off = 0, leftlen;
while ((ioret = read(sockfd, bytebuf, sizeof(bytebuf))) == -1 && errno == EINTR)
;
if (ioret == -1 && (errno == EWOULDBLOCK || errno == EAGAIN)) {
/* no data */
ioret = 0;
} else if (ioret <= 0) {
goto Exit;
}
while ((leftlen = ioret - off) != 0) {
if (state == IN_HANDSHAKE) {
if ((ret = ptls_handshake(tls, &encbuf, bytebuf + off, &leftlen, hsprop)) == 0) {
state = IN_1RTT;
assert(ptls_is_server(tls) || hsprop->client.early_data_acceptance != PTLS_EARLY_DATA_ACCEPTANCE_UNKNOWN);
ech_save_retry_configs();
/* release data sent as early-data, if server accepted it */
if (hsprop->client.early_data_acceptance == PTLS_EARLY_DATA_ACCEPTED)
shift_buffer(&ptbuf, early_bytes_sent);
if (request_key_update)
ptls_update_key(tls, 1);
} else if (ret == PTLS_ERROR_IN_PROGRESS) {
/* ok */
} else {
if (ret == PTLS_ALERT_ECH_REQUIRED) {
assert(!ptls_is_server(tls));
ech_save_retry_configs();
}
if (encbuf.off != 0)
repeat_while_eintr(write(sockfd, encbuf.base, encbuf.off), { break; });
fprintf(stderr, "ptls_handshake:%d\n", ret);
goto Exit;
}
} else {
if ((ret = ptls_receive(tls, &rbuf, bytebuf + off, &leftlen)) == 0) {
if (rbuf.off != 0) {
data_received += rbuf.off;
if (input_file != input_file_is_benchmark)
repeat_while_eintr(write(1, rbuf.base, rbuf.off), { goto Exit; });
rbuf.off = 0;
}
} else if (ret == PTLS_ERROR_IN_PROGRESS) {
/* ok */
} else {
fprintf(stderr, "ptls_receive:%d\n", ret);
goto Exit;
}
}
off += leftlen;
}
}
/* encrypt data to send, if any is available */
if (encbuf.off == 0 || state == IN_HANDSHAKE) {
static const size_t block_size = 16384;
if (inputfd >= 0 && (FD_ISSET(inputfd, &readfds) || FD_ISSET(inputfd, &exceptfds))) {
if ((ret = ptls_buffer_reserve(&ptbuf, block_size)) != 0)
goto Exit;
while ((ioret = read(inputfd, ptbuf.base + ptbuf.off, block_size)) == -1 && errno == EINTR)
;
if (ioret > 0) {
ptbuf.off += ioret;
} else if (ioret == 0) {
/* closed */
if (input_file != NULL)
close(inputfd);
inputfd = -1;
}
} else if (inputfd == inputfd_is_benchmark) {
if (ptbuf.capacity < block_size) {
if ((ret = ptls_buffer_reserve(&ptbuf, block_size - ptbuf.capacity)) != 0)
goto Exit;
memset(ptbuf.base + ptbuf.capacity, 0, block_size - ptbuf.capacity);
}
ptbuf.off = block_size;
}
}
if (ptbuf.off != 0) {
if (state == IN_HANDSHAKE) {
size_t send_amount = 0;
if (server_name != NULL && hsprop->client.max_early_data_size != NULL) {
size_t max_can_be_sent = *hsprop->client.max_early_data_size;
if (max_can_be_sent > ptbuf.off)
max_can_be_sent = ptbuf.off;
send_amount = max_can_be_sent - early_bytes_sent;
}
if (send_amount != 0) {
if ((ret = ptls_send(tls, &encbuf, ptbuf.base, send_amount)) != 0) {
fprintf(stderr, "ptls_send(early_data):%d\n", ret);
goto Exit;
}
early_bytes_sent += send_amount;
}
} else {
if ((ret = ptls_send(tls, &encbuf, ptbuf.base, ptbuf.off)) != 0) {
fprintf(stderr, "ptls_send(1rtt):%d\n", ret);
goto Exit;
}
ptbuf.off = 0;
}
}
/* send any data */
if (encbuf.off != 0) {
while ((ioret = write(sockfd, encbuf.base, encbuf.off)) == -1 && errno == EINTR)
;
if (ioret == -1 && (errno == EWOULDBLOCK || errno == EAGAIN)) {
/* no data */
} else if (ioret <= 0) {
goto Exit;
} else {
shift_buffer(&encbuf, ioret);
}
}
/* close the sender side when necessary */
if (state == IN_1RTT && inputfd == -1) {
if (!keep_sender_open) {
ptls_buffer_t wbuf;
uint8_t wbuf_small[32];
ptls_buffer_init(&wbuf, wbuf_small, sizeof(wbuf_small));
if ((ret = ptls_send_alert(tls, &wbuf, PTLS_ALERT_LEVEL_WARNING, PTLS_ALERT_CLOSE_NOTIFY)) != 0) {
fprintf(stderr, "ptls_send_alert:%d\n", ret);
}
if (wbuf.off != 0)
repeat_while_eintr(write(sockfd, wbuf.base, wbuf.off), {
ptls_buffer_dispose(&wbuf);
goto Exit;
});
ptls_buffer_dispose(&wbuf);
shutdown(sockfd, SHUT_WR);
}
state = IN_SHUTDOWN;
}
}
Exit:
if (input_file == input_file_is_benchmark) {
double elapsed = (ctx->get_time->cb(ctx->get_time) - start_at) / 1000.0;
ptls_cipher_suite_t *cipher_suite = ptls_get_cipher(tls);
fprintf(stderr, "received %" PRIu64 " bytes in %.3f seconds (%f.3Mbps); %s\n", data_received, elapsed,
data_received * 8 / elapsed / 1000 / 1000, cipher_suite != NULL ? cipher_suite->aead->name : "unknown cipher");
}
if (sockfd != -1)
close(sockfd);
if (input_file != NULL && input_file != input_file_is_benchmark && inputfd >= 0)
close(inputfd);
ptls_buffer_dispose(&rbuf);
ptls_buffer_dispose(&encbuf);
ptls_buffer_dispose(&ptbuf);
ptls_free(tls);
return ret != 0;
}
static int run_server(struct sockaddr *sa, socklen_t salen, ptls_context_t *ctx, const char *input_file,
ptls_handshake_properties_t *hsprop, int request_key_update)
{
int listen_fd, conn_fd, on = 1;
if ((listen_fd = socket(sa->sa_family, SOCK_STREAM, 0)) == -1) {
perror("socket(2) failed");
return 1;
}
if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) != 0) {
perror("setsockopt(SO_REUSEADDR) failed");
return 1;
}
if (bind(listen_fd, sa, salen) != 0) {
perror("bind(2) failed");
return 1;
}
if (listen(listen_fd, SOMAXCONN) != 0) {
perror("listen(2) failed");
return 1;
}
fprintf(stderr, "server started on port %d\n", ntohs(((struct sockaddr_in *)sa)->sin_port));
while (1) {
fprintf(stderr, "waiting for connections\n");
if ((conn_fd = accept(listen_fd, NULL, 0)) != -1)
handle_connection(conn_fd, ctx, NULL, input_file, hsprop, request_key_update, 0);
}
return 0;
}
static int run_client(struct sockaddr *sa, socklen_t salen, ptls_context_t *ctx, const char *server_name, const char *input_file,
ptls_handshake_properties_t *hsprop, int request_key_update, int keep_sender_open)
{
int fd;
if ((fd = socket(sa->sa_family, SOCK_STREAM, 0)) == 1) {
perror("socket(2) failed");
return 1;
}
if (connect(fd, sa, salen) != 0) {
perror("connect(2) failed");
return 1;
}
int ret = handle_connection(fd, ctx, server_name, input_file, hsprop, request_key_update, keep_sender_open);
return ret;
}
static void usage(const char *cmd)
{
printf("Usage: %s [options] host port\n"
"\n"
"Options:\n"
" -4 force IPv4\n"
" -6 force IPv6\n"
" -a require client authentication\n"
" -b enable brotli compression\n"
" -B benchmark mode for measuring sustained bandwidth. Run\n"
" both endpoints with this option for some time, then kill\n"
" the client. Server will report the ingress bandwidth.\n"
" -C certificate-file certificate chain used for client authentication\n"
" -c certificate-file certificate chain used for server authentication\n"
" -i file a file to read from and send to the peer (default: stdin)\n"
" -I keep send side open after sending all data (client-only)\n"
" -j log-file file to log probe events in JSON-Lines\n"
" -k key-file specifies the credentials for signing the certificate\n"
" -K key-file ECH private key for each ECH config provided by -E\n"
" -l log-file file to log events (incl. traffic secrets)\n"
" -n negotiates the key exchange method (i.e. wait for HRR)\n"
" -N named-group named group to be used (default: secp256r1)\n"
" -s session-file file to read/write the session ticket\n"
" -S require public key exchange when resuming a session\n"
" -E echconfiglist file that contains ECHConfigList or an empty file to\n"
" grease ECH; will be overwritten when receiving\n"
" retry_configs from the server\n"
" -e when resuming a session, send first 8,192 bytes of input\n"
" as early data\n"
" -r public-key-file use raw public keys (RFC 7250). When set and running as a\n"
" client, the argument specifies the public keys that the\n"
" server is expected to use. When running as a server, the\n"
" argument is ignored.\n"
" -u update the traffic key when handshake is complete\n"
" -v verify peer using the default certificates\n"
" -V CA-root-file verify peer using the CA Root File\n"
" -y cipher-suite cipher-suite to be used\n"
" -h print this help\n"
"\n"
"Supported named groups: secp256r1"
#if PTLS_OPENSSL_HAVE_SECP384R1
", secp384r1"
#endif
#if PTLS_OPENSSL_HAVE_SECP521R1
", secp521r1"
#endif
#if PTLS_OPENSSL_HAVE_X25519
", X25519"
#endif
"\n"
"Supported signature algorithms: rsa, secp256r1"
#if PTLS_OPENSSL_HAVE_SECP384R1
", secp384r1"
#endif
#if PTLS_OPENSSL_HAVE_SECP521R1
", secp521r1"
#endif
#if PTLS_OPENSSL_HAVE_ED25519
", ed25519"
#endif
"\n",
cmd);
printf("Supported cipher suites:");
for (size_t i = 0; ptls_openssl_cipher_suites_all[i] != NULL; ++i) {
if (i != 0)
printf(",");
printf(" %s", ptls_openssl_cipher_suites_all[i]->name);
}
printf("\n\n");
}
int main(int argc, char **argv)
{
ERR_load_crypto_strings();
OpenSSL_add_all_algorithms();
#if !defined(OPENSSL_NO_ENGINE)
/* Load all compiled-in ENGINEs */
ENGINE_load_builtin_engines();
ENGINE_register_all_ciphers();
ENGINE_register_all_digests();
#endif
res_init();
ptls_key_exchange_algorithm_t *key_exchanges[128] = {NULL};
ptls_cipher_suite_t *cipher_suites[128] = {NULL};
ptls_context_t ctx = {
.random_bytes = ptls_openssl_random_bytes,
.get_time = &ptls_get_time,
.key_exchanges = key_exchanges,
.cipher_suites = cipher_suites,
.ech = {.client = {ptls_openssl_hpke_cipher_suites, ptls_openssl_hpke_kems}, .server = {NULL /* activated by -K option */}},
};
ptls_handshake_properties_t hsprop = {{{{NULL}}}};
const char *host, *port, *input_file = NULL;
int is_server = 0, use_early_data = 0, request_key_update = 0, keep_sender_open = 0, ch;
struct sockaddr_storage sa;
socklen_t salen;
int family = 0;
const char *raw_pub_key_file = NULL, *cert_location = NULL;
while ((ch = getopt(argc, argv, "46abBC:c:i:Ij:k:nN:es:Sr:E:K:l:y:vV:h")) != -1) {
switch (ch) {
case '4':
family = AF_INET;
break;
case '6':
family = AF_INET6;
break;
case 'a':
ctx.require_client_authentication = 1;
break;
case 'b':
#if PICOTLS_USE_BROTLI
ctx.decompress_certificate = &ptls_decompress_certificate;
#else
fprintf(stderr, "support for `-b` option was turned off during configuration\n");
exit(1);
#endif
break;
case 'B':
input_file = input_file_is_benchmark;
break;
case 'C':
case 'c':
if (cert_location != NULL) {
fprintf(stderr, "-C/-c can only be specified once\n");
return 1;
}
cert_location = optarg;
is_server = ch == 'c';
break;
case 'i':
input_file = optarg;
break;
case 'I':
keep_sender_open = 1;
break;
case 'j':
setup_ptlslog(optarg);
break;
case 'k':
load_private_key(&ctx, optarg);
break;
case 'n':
hsprop.client.negotiate_before_key_exchange = 1;
break;
case 'e':
use_early_data = 1;
break;
case 'r':
raw_pub_key_file = optarg;
break;
case 's':
setup_session_file(&ctx, &hsprop, optarg);
break;
case 'S':
ctx.require_dhe_on_psk = 1;
break;
case 'E':
ech_setup_configs(optarg);
break;
case 'K':
ech_setup_key(&ctx, optarg);
break;
case 'l':
setup_log_event(&ctx, optarg);
break;
case 'v':
setup_verify_certificate(&ctx, NULL);
break;
case 'V':
setup_verify_certificate(&ctx, optarg);
break;
case 'N': {
ptls_key_exchange_algorithm_t *algo = NULL;
#define MATCH(name) \
if (algo == NULL && strcasecmp(optarg, #name) == 0) \
algo = (&ptls_openssl_##name)
MATCH(secp256r1);
#if PTLS_OPENSSL_HAVE_SECP384R1
MATCH(secp384r1);
#endif
#if PTLS_OPENSSL_HAVE_SECP521R1
MATCH(secp521r1);
#endif
#if PTLS_OPENSSL_HAVE_X25519
MATCH(x25519);
#endif
#undef MATCH
if (algo == NULL) {
fprintf(stderr, "could not find key exchange: %s\n", optarg);
return 1;
}
size_t i;
for (i = 0; key_exchanges[i] != NULL; ++i)
;
key_exchanges[i++] = algo;
} break;
case 'u':
request_key_update = 1;
break;
case 'y': {
/* find the cipher suite to be added from `ptls_openssl_cipher_suites_all` */
ptls_cipher_suite_t *added = NULL;
for (size_t i = 0; ptls_openssl_cipher_suites_all[i] != NULL; ++i) {
if (strcasecmp(ptls_openssl_cipher_suites_all[i]->name, optarg) == 0) {
added = ptls_openssl_cipher_suites_all[i];
break;
}
}
if (added == NULL) {
fprintf(stderr, "unknown cipher-suite: %s, see -h for list of cipher-suites supported\n", optarg);
exit(1);
}
size_t slot;
for (slot = 0; cipher_suites[slot] != NULL; ++slot) {
if (cipher_suites[slot]->id == added->id) {
fprintf(stderr, "cipher-suite %s is already in list\n", added->name);
exit(1);
}
}
cipher_suites[slot] = added;
} break;
case 'h':
usage(argv[0]);
exit(0);
default:
exit(1);
}
}
argc -= optind;
argv += optind;
if (raw_pub_key_file != NULL) {
int is_dash = !strcmp(raw_pub_key_file, "-");
if (is_server) {
ctx.certificates.list = malloc(sizeof(*ctx.certificates.list));
load_raw_public_key(ctx.certificates.list, cert_location);
ctx.certificates.count = 1;
} else if (!is_dash) {
ptls_iovec_t raw_pub_key;
EVP_PKEY *pubkey;
load_raw_public_key(&raw_pub_key, raw_pub_key_file);
pubkey = d2i_PUBKEY(NULL, (const unsigned char **)&raw_pub_key.base, raw_pub_key.len);
if (pubkey == NULL) {
fprintf(stderr, "Failed to create an EVP_PKEY from the key found in %s\n", raw_pub_key_file);
return 1;
}
setup_raw_pubkey_verify_certificate(&ctx, pubkey);
EVP_PKEY_free(pubkey);
}
ctx.use_raw_public_keys = 1;
} else {
if (cert_location)
load_certificate_chain(&ctx, cert_location);
}
if ((ctx.certificates.count == 0) != (ctx.sign_certificate == NULL)) {
fprintf(stderr, "-C/-c and -k options must be used together\n");
return 1;
}
if (is_server) {
if (ctx.certificates.count == 0) {
fprintf(stderr, "-c and -k options must be set\n");
return 1;
}
#if PICOTLS_USE_BROTLI
if (ctx.decompress_certificate != NULL) {
static ptls_emit_compressed_certificate_t ecc;
if (ptls_init_compressed_certificate(&ecc, ctx.certificates.list, ctx.certificates.count, ptls_iovec_init(NULL, 0)) !=
0) {
fprintf(stderr, "failed to create a brotli-compressed version of the certificate chain.\n");
exit(1);
}
ctx.emit_certificate = &ecc.super;
}
#endif
setup_session_cache(&ctx);
} else {
/* client */
if (use_early_data) {
static size_t max_early_data_size;
hsprop.client.max_early_data_size = &max_early_data_size;
}
ctx.send_change_cipher_spec = 1;
hsprop.client.ech.configs = ech.config_list;
hsprop.client.ech.retry_configs = &ech.retry.configs;
}
if (key_exchanges[0] == NULL)
key_exchanges[0] = &ptls_openssl_secp256r1;
if (cipher_suites[0] == NULL) {
size_t i;
for (i = 0; ptls_openssl_cipher_suites[i] != NULL; ++i)
cipher_suites[i] = ptls_openssl_cipher_suites[i];
}
if (argc != 2) {
fprintf(stderr, "missing host and port\n");
return 1;
}
host = (--argc, *argv++);
port = (--argc, *argv++);
if (resolve_address((struct sockaddr *)&sa, &salen, host, port, family, SOCK_STREAM, IPPROTO_TCP) != 0)
exit(1);
if (is_server) {
return run_server((struct sockaddr *)&sa, salen, &ctx, input_file, &hsprop, request_key_update);
} else {
return run_client((struct sockaddr *)&sa, salen, &ctx, host, input_file, &hsprop, request_key_update, keep_sender_open);
}
}