aboutsummaryrefslogtreecommitdiff
path: root/writeups/ipv6/eyeball/eyeball.c
diff options
context:
space:
mode:
authorDavid Timber <dxdt@dev.snart.me>2024-11-22 15:02:01 +0100
committerDavid Timber <dxdt@dev.snart.me>2024-11-22 15:02:01 +0100
commit12c0200ca462ba72792417f3a2071d02ff4fe498 (patch)
tree48d2f223fdf24d727c6f7cd808c1a45373d26ade /writeups/ipv6/eyeball/eyeball.c
parent265dc7ecbb11a8586c5712f281cc8f20a79d05aa (diff)
Add writeups/ipv6/eyeball
Diffstat (limited to 'writeups/ipv6/eyeball/eyeball.c')
-rw-r--r--writeups/ipv6/eyeball/eyeball.c612
1 files changed, 612 insertions, 0 deletions
diff --git a/writeups/ipv6/eyeball/eyeball.c b/writeups/ipv6/eyeball/eyeball.c
new file mode 100644
index 0000000..32a6033
--- /dev/null
+++ b/writeups/ipv6/eyeball/eyeball.c
@@ -0,0 +1,612 @@
+/**
+ * @file eyeball.c
+ * @author David Timber
+ * @brief Demonstrates RFC 8305
+ * @see
+ * https://datatracker.ietf.org/doc/html/rfc6555
+ * https://datatracker.ietf.org/doc/html/rfc8305
+ */
+#define _POSIX_C_SOURCE 200809L
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <inttypes.h>
+#include <assert.h>
+#include <string.h>
+#include <errno.h>
+#include <signal.h>
+#include <math.h>
+
+#include <unistd.h>
+#include <getopt.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+#include <netdb.h>
+#include <pthread.h>
+
+#define ARGV0 "eyeball"
+#define DEFAULT_RES_DELAY 0.05
+
+
+void ts_sub (
+ struct timespec *c,
+ const struct timespec *a,
+ const struct timespec *b)
+{
+ c->tv_nsec = a->tv_nsec - b->tv_nsec;
+ c->tv_sec = a->tv_sec - b->tv_sec;
+ if (c->tv_nsec < 0) {
+ c->tv_sec -= 1;
+ c->tv_nsec += 1000000000;
+ }
+}
+
+enum happy_result {
+ // no error: connection established and handshake complete
+ HR_NULL,
+ // a function call failed and error set to errno
+ HR_ERRNO,
+ // getaddrinfo() returned error and error is set to the returned value
+ HR_GAI,
+ // error set to user-defined value
+ HR_OTHER,
+};
+
+struct happy_error {
+ struct {
+ struct timespec resolv;
+ struct timespec conn;
+ struct timespec total;
+ } delay;
+ enum happy_result result;
+ int error;
+ bool ready;
+};
+
+typedef void*(*happy_connectf_t)(
+ void *ctx,
+ const size_t aic,
+ const struct addrinfo *aiv,
+ struct happy_error *out_err);
+
+/*
+ * simple reference connectf implementation that creates a TCP socket and
+ * connects to the address
+ */
+void *happy_tcp_connectf (
+ void *ctx,
+ const size_t, // unused
+ const struct addrinfo *aiv,
+ struct happy_error *out_err)
+{
+ int ret = -1;
+ int fr;
+
+ ret = socket(aiv->ai_family, aiv->ai_socktype, aiv->ai_protocol);
+ if (ret < 0) {
+ goto ERR;
+ }
+
+ fr = connect(ret, aiv->ai_addr, aiv->ai_addrlen);
+ if (fr != 0) {
+ goto ERR;
+ }
+ goto END;
+
+ERR:
+ out_err->result = HR_ERRNO;
+ out_err->error = errno;
+ if (ret >= 0) {
+ close(ret);
+ ret = -1;
+ }
+
+END:
+ *((int*)ctx) = ret;
+ return NULL;
+}
+
+/*
+ * Converts the list returned from getaddrinfo() to a flat array
+ */
+struct addrinfo *flataddrinfo (const struct addrinfo *list, size_t *cnt) {
+ const struct addrinfo *p;
+ struct addrinfo *ret = NULL;
+ size_t i;
+
+ for (i = 0, p = list; p != NULL; p = p->ai_next, i += 1);
+ if (i == 0) {
+ goto END;
+ }
+
+ ret = malloc(sizeof(struct addrinfo) * i);
+ if (ret == NULL) {
+ goto END;
+ }
+
+ for (i = 0, p = list; p != NULL; p = p->ai_next, i += 1) {
+ ret[i] = *p;
+ ret[i].ai_next = ret + i + 1;
+ }
+ ret[i - 1].ai_next = NULL;
+
+END:
+ if (cnt != NULL) {
+ *cnt = i;
+ }
+ return ret;
+}
+
+void *happy_th_main_inner (
+ const char *node,
+ const char *service,
+ const struct addrinfo *hints,
+ void *ctx,
+ happy_connectf_t connf,
+ pthread_mutex_t *lock,
+ pthread_cond_t *cond,
+ struct happy_error *out_err)
+{
+ int fr;
+ void *ret = NULL;
+ struct happy_error err = { 0, };
+ struct addrinfo *gai_res = NULL;
+ struct addrinfo *aiv = NULL;
+ size_t aic = 0;
+ struct {
+ struct timespec start;
+ struct timespec resolv;
+ struct timespec conn;
+ struct timespec end;
+ } ts;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts.start);
+
+ // do resolve
+ fr = getaddrinfo(node, service, hints, &gai_res);
+ clock_gettime(CLOCK_MONOTONIC, &ts.resolv);
+ if (fr != 0) {
+ if (fr == EAI_SYSTEM) {
+ err.result = HR_ERRNO;
+ err.error = errno;
+ }
+ else {
+ err.result = HR_GAI;
+ err.error = fr;
+ }
+
+ goto END;
+ }
+ assert(gai_res != NULL);
+
+ // bring the result to our address space by converting the list to array
+ aiv = flataddrinfo(gai_res, &aic);
+ if (aiv == NULL) {
+ goto END;
+ }
+ // free the list
+ freeaddrinfo(gai_res);
+ gai_res = NULL;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts.conn);
+ if (connf != NULL) {
+ // call the inner function
+ ret = connf(ctx, aic, aiv, &err);
+ }
+
+END:
+ clock_gettime(CLOCK_MONOTONIC, &ts.end);
+ ts_sub(&err.delay.resolv, &ts.resolv, &ts.start);
+ ts_sub(&err.delay.conn, &ts.end, &ts.conn);
+ ts_sub(&err.delay.total, &ts.end, &ts.start);
+
+ freeaddrinfo(gai_res);
+ free(aiv);
+
+ err.ready = true;
+
+ fr = pthread_mutex_lock(lock);
+ assert(fr == 0);
+ if (out_err != NULL) {
+ *out_err = err;
+ }
+ pthread_cond_broadcast(cond);
+ pthread_mutex_unlock(lock);
+
+ return ret;
+}
+
+struct happy_th_args {
+ const char *node;
+ const char *service;
+ const struct addrinfo *hints;
+ void *ctx;
+ happy_connectf_t connf;
+ pthread_mutex_t *lock;
+ pthread_cond_t *cond;
+ struct happy_error *out_err;
+};
+
+/*
+ * Happy eyeballs worker thread entry
+ */
+void *happy_th_main (void *args_in) {
+ struct happy_th_args *args = args_in;
+
+ return happy_th_main_inner(
+ args->node,
+ args->service,
+ args->hints,
+ args->ctx,
+ args->connf,
+ args->lock,
+ args->cond,
+ args->out_err
+ );
+}
+
+int aihintflags (void) {
+ int ret = 0;
+#ifdef AI_IDN
+ ret |= AI_IDN;
+#endif
+ return ret;
+}
+
+struct {
+ const char *node;
+ const char *service;
+ struct timespec op_timeout;
+ double res_delay;
+ struct {
+ bool help;
+ bool both;
+ } flags;
+} opts;
+
+void init_opts (void) {
+ // connection timeout: 10 s
+ opts.op_timeout.tv_sec = 10;
+ opts.op_timeout.tv_nsec = 0;
+ // "Resolution Delay" as per RFC 8305 section 3: 50 ms
+ opts.res_delay = DEFAULT_RES_DELAY;
+}
+
+bool parse_args (const int argc, const char **argv) {
+ int fr;
+ double tmpf;
+
+ while (true) {
+ fr = getopt(argc, (char *const*)argv, "hd:b");
+ if (fr < 0) {
+ break;
+ }
+
+ switch (fr) {
+ case 'h': opts.flags.help = true; break;
+ case 'd':
+ tmpf = NAN;
+ fr = sscanf(optarg, "%lf", &tmpf);
+ if (fr != 1 || isnan(tmpf)) {
+ fprintf(stderr, ARGV0": invalid option -d: %s\n", optarg);
+ return false;
+ }
+ opts.res_delay = tmpf;
+ break;
+ case 'b':
+ opts.flags.both = true;
+ break;
+ case '?': return false;
+ }
+ }
+
+ if (argc < optind + 2) {
+ fprintf(stderr, ARGV0": too few arguments\n");
+ return false;
+ }
+
+ opts.node = argv[optind];
+ opts.service = argv[optind + 1];
+
+ return true;
+}
+
+void print_help (void) {
+ printf("Usage: "ARGV0" [-hb] [-d RES_DELAY] <NODE> <SERVICE>\n");
+}
+
+struct {
+ pthread_mutex_t lock;
+ pthread_cond_t cond;
+ struct {
+ struct addrinfo hints;
+ struct happy_th_args args;
+ struct happy_error err;
+ pthread_t th;
+ int ret;
+ bool started;
+ } th[2];
+} g;
+
+bool init_ctx (void) {
+ pthread_mutex_init(&g.lock, NULL);
+ pthread_cond_init(&g.cond, NULL);
+
+ // hints
+ g.th[0].hints.ai_socktype = g.th[1].hints.ai_socktype = SOCK_STREAM;
+ g.th[0].hints.ai_flags = g.th[1].hints.ai_flags = aihintflags();
+ g.th[0].hints.ai_family = AF_INET6;
+ g.th[1].hints.ai_family = AF_INET;
+
+ // args
+ g.th[0].args.node = g.th[1].args.node = opts.node;
+ g.th[0].args.service = g.th[1].args.service = opts.service;
+ g.th[0].args.connf = g.th[1].args.connf = happy_tcp_connectf;
+ g.th[0].args.hints = &g.th[0].hints;
+ g.th[0].args.ctx = &g.th[0].ret;
+ g.th[0].args.lock = &g.lock;
+ g.th[0].args.cond = &g.cond;
+ g.th[0].args.out_err = &g.th[0].err;
+ g.th[1].args.hints = &g.th[1].hints;
+ g.th[1].args.ctx = &g.th[1].ret;
+ g.th[1].args.lock = &g.lock;
+ g.th[1].args.cond = &g.cond;
+ g.th[1].args.out_err = &g.th[1].err;
+
+ g.th[0].ret = g.th[1].ret = -1;
+
+ return true;
+}
+
+void deinit_ctx (void) {
+ for (size_t i = 0; i < 2; i += 1) {
+ if (!g.th[i].started) {
+ continue;
+ }
+/*
+ * ain't pretty, but canceling getaddrinfo() is not possible. Portability comes
+ * first in this demo. This is why cross-platform many apps handcraft their own
+ * address or use libraries like c-res.
+ */
+ pthread_cancel(g.th[i].th);
+ pthread_join(g.th[i].th, NULL);
+ }
+
+ pthread_mutex_destroy(&g.lock);
+ pthread_cond_destroy(&g.cond);
+}
+
+bool spawn_threads (void) {
+ int fr;
+
+ for (size_t i = 0; i < 2; i += 1) {
+ fr = pthread_create(&g.th[i].th, NULL, happy_th_main, &g.th[i].args);
+ g.th[i].started = fr == 0;
+ if (!g.th[i].started) {
+ perror(ARGV0": pthread_create()");
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void get_leadtime (struct timespec *ts, const struct timespec *amt) {
+ clock_gettime(CLOCK_REALTIME, ts);
+ ts->tv_sec += amt->tv_sec;
+ ts->tv_nsec += amt->tv_nsec;
+ ts->tv_sec += ts->tv_nsec / 1000000000;
+ ts->tv_nsec %= 1000000000;
+}
+
+void get_leadtimef (struct timespec *ts, const double lt) {
+ struct timespec translated;
+
+ translated.tv_sec = (time_t)lt;
+ translated.tv_nsec = (long)((lt - (time_t)lt) * 1000000000);
+ get_leadtime(ts, &translated);
+}
+
+ssize_t poll_result (void) {
+ struct timespec op_deadline;
+ int fr;
+
+ get_leadtime(&op_deadline, &opts.op_timeout);
+
+ while (true) {
+ if (g.th[0].err.ready && g.th[0].err.result == HR_NULL) {
+ // ipv6 made it already
+ return 1;
+ }
+
+ if (g.th[1].err.ready && g.th[1].err.result == HR_NULL) {
+ // ipv4 connected, but let's wait around for a bit
+ struct timespec bias_deadline;
+
+ fprintf(stderr, ARGV0": ipv4 shot first\n");
+
+ get_leadtimef(&bias_deadline, opts.res_delay);
+ pthread_cond_timedwait(&g.cond, &g.lock, &bias_deadline);
+
+ if (g.th[0].err.ready && g.th[0].err.result == HR_NULL) {
+ // ipv6 made it!
+ return 1;
+ }
+ else if (g.th[1].err.ready) {
+ // ipv6 didn't make it
+ return 2;
+ }
+ else {
+ // all timed out
+ return -1;
+ }
+ }
+
+ if (g.th[0].err.ready && g.th[1].err.ready) {
+ // both failed
+ return -1;
+ }
+
+ // both still trying
+ fr = pthread_cond_timedwait(&g.cond, &g.lock, &op_deadline);
+ if (fr != 0) {
+ // timed out or interrupted
+ break;
+ }
+ }
+
+ return -1;
+}
+
+ssize_t poll_both (void) {
+ ssize_t ret = 0;
+ struct timespec op_deadline;
+ int fr;
+
+ get_leadtime(&op_deadline, &opts.op_timeout);
+
+ while (!(g.th[0].err.ready && g.th[1].err.ready)) {
+ fr = pthread_cond_timedwait(&g.cond, &g.lock, &op_deadline);
+ if (fr != 0) {
+ break;
+ }
+ }
+
+ if (g.th[0].err.result == HR_NULL) {
+ ret |= 1;
+ }
+ if (g.th[1].err.result == HR_NULL) {
+ ret |= 2;
+ }
+ if (ret == 0) {
+ ret = -1;
+ }
+
+ return ret;
+}
+
+void print_sockname (
+ const int fd,
+ int(*namef)(int, struct sockaddr*, socklen_t*))
+{
+ union {
+ struct sockaddr_storage ss;
+ struct sockaddr sa;
+ struct sockaddr_in sin;
+ struct sockaddr_in6 sin6;
+ } addr;
+ socklen_t sl = sizeof(addr);
+ char str[INET6_ADDRSTRLEN] = { 0 };
+ int fr;
+ void *addr_data;
+ uint16_t port;
+
+ fr = namef(fd, &addr.sa, &sl);
+ assert(fr == 0);
+
+ if (addr.sa.sa_family == AF_INET) {
+ addr_data = &addr.sin.sin_addr;
+ port = ntohs(addr.sin.sin_port);
+ }
+ else {
+ addr_data = &addr.sin6.sin6_addr;
+ port = ntohs(addr.sin6.sin6_port);
+ }
+ inet_ntop(addr.sa.sa_family, addr_data, str, sizeof(str));
+
+ if (addr.sa.sa_family == AF_INET) {
+ printf("%s:%"PRIu16, str, port);
+ }
+ else {
+ printf("[%s]:%"PRIu16, str, port);
+ }
+}
+
+void do_report (const ssize_t picked) {
+ char star;
+
+ for (size_t i = 0; i < 2; i += 1) {
+ if (picked > 0 && ((1 << i) & picked)) {
+ star = '*';
+ }
+ else {
+ star = ' ';
+ }
+ printf("%c%s: ", star, g.th[i].hints.ai_family == AF_INET ? "v4" : "v6");
+
+ if (g.th[i].err.ready) {
+ printf("%ld.%03lus %ld.%03lus %ld.%03lus ",
+ (long)g.th[i].err.delay.resolv.tv_sec,
+ (unsigned long)g.th[i].err.delay.resolv.tv_nsec / 1000000,
+ (long)g.th[i].err.delay.conn.tv_sec,
+ (unsigned long)g.th[i].err.delay.conn.tv_nsec / 1000000,
+ (long)g.th[i].err.delay.total.tv_sec,
+ (unsigned long)g.th[i].err.delay.total.tv_nsec / 1000000);
+
+
+ switch (g.th[i].err.result) {
+ case HR_GAI:
+ printf("%s", gai_strerror(g.th[i].err.error));
+ break;
+ case HR_ERRNO:
+ printf("%s", strerror(g.th[i].err.error));
+ break;
+ default:
+ print_sockname(g.th[i].ret, getsockname);
+ printf(" -> ");
+ print_sockname(g.th[i].ret, getpeername);
+ }
+ }
+ printf("\n");
+ }
+}
+
+int do_pick (void) {
+ ssize_t picked;
+
+ pthread_mutex_lock(&g.lock);
+ if (opts.flags.both) {
+ picked = poll_both();
+ }
+ else {
+ picked = poll_result();
+ }
+ do_report(picked);
+ pthread_mutex_unlock(&g.lock);
+
+ if (picked < 0) {
+ return 1;
+ }
+ return 0;
+}
+
+int main (const int argc, const char **argv) {
+ static int ec = 0;
+
+ init_opts();
+
+ if (!parse_args(argc, argv)) {
+ ec = 2;
+ goto END;
+ }
+ if (opts.flags.help) {
+ print_help();
+ goto END;
+ }
+
+ if (!init_ctx()) {
+ ec = 1;
+ goto END;
+ }
+ if (!spawn_threads()) {
+ ec = 1;
+ goto END;
+ }
+
+ ec = do_pick();
+
+END:
+ deinit_ctx();
+ return ec;
+}