aboutsummaryrefslogtreecommitdiffstats
path: root/src/wg_cookie.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/wg_cookie.c')
-rw-r--r--src/wg_cookie.c427
1 files changed, 427 insertions, 0 deletions
diff --git a/src/wg_cookie.c b/src/wg_cookie.c
new file mode 100644
index 0000000..bf0ce37
--- /dev/null
+++ b/src/wg_cookie.c
@@ -0,0 +1,427 @@
+/* SPDX-License-Identifier: ISC
+ *
+ * Copyright (C) 2015-2021 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ * Copyright (C) 2019-2021 Matt Dunwoodie <ncon@noconroy.net>
+ */
+
+#include <sys/types.h>
+#include <sys/systm.h>
+#include <sys/param.h>
+#include <sys/rwlock.h>
+#include <sys/malloc.h> /* Because systm doesn't include M_NOWAIT, M_DEVBUF */
+#include <sys/socket.h>
+
+#include "support.h"
+#include "wg_cookie.h"
+
+static void cookie_precompute_key(uint8_t *,
+ const uint8_t[COOKIE_INPUT_SIZE], const char *);
+static void cookie_macs_mac1(struct cookie_macs *, const void *, size_t,
+ const uint8_t[COOKIE_KEY_SIZE]);
+static void cookie_macs_mac2(struct cookie_macs *, const void *, size_t,
+ const uint8_t[COOKIE_COOKIE_SIZE]);
+static int cookie_timer_expired(struct timespec *, time_t, long);
+static void cookie_checker_make_cookie(struct cookie_checker *,
+ uint8_t[COOKIE_COOKIE_SIZE], struct sockaddr *);
+static int ratelimit_init(struct ratelimit *, uma_zone_t);
+static void ratelimit_deinit(struct ratelimit *);
+static void ratelimit_gc(struct ratelimit *, int);
+static int ratelimit_allow(struct ratelimit *, struct sockaddr *);
+
+/* Public Functions */
+void
+cookie_maker_init(struct cookie_maker *cp, const uint8_t key[COOKIE_INPUT_SIZE])
+{
+ bzero(cp, sizeof(*cp));
+ cookie_precompute_key(cp->cp_mac1_key, key, COOKIE_MAC1_KEY_LABEL);
+ cookie_precompute_key(cp->cp_cookie_key, key, COOKIE_COOKIE_KEY_LABEL);
+ rw_init(&cp->cp_lock, "cookie_maker");
+}
+
+int
+cookie_checker_init(struct cookie_checker *cc, uma_zone_t zone)
+{
+ int res;
+ bzero(cc, sizeof(*cc));
+
+ rw_init(&cc->cc_key_lock, "cookie_checker_key");
+ rw_init(&cc->cc_secret_lock, "cookie_checker_secret");
+
+ if ((res = ratelimit_init(&cc->cc_ratelimit_v4, zone)) != 0)
+ return res;
+#ifdef INET6
+ if ((res = ratelimit_init(&cc->cc_ratelimit_v6, zone)) != 0) {
+ ratelimit_deinit(&cc->cc_ratelimit_v4);
+ return res;
+ }
+#endif
+ return 0;
+}
+
+void
+cookie_checker_update(struct cookie_checker *cc,
+ const uint8_t key[COOKIE_INPUT_SIZE])
+{
+ rw_enter_write(&cc->cc_key_lock);
+ if (key) {
+ cookie_precompute_key(cc->cc_mac1_key, key, COOKIE_MAC1_KEY_LABEL);
+ cookie_precompute_key(cc->cc_cookie_key, key, COOKIE_COOKIE_KEY_LABEL);
+ } else {
+ bzero(cc->cc_mac1_key, sizeof(cc->cc_mac1_key));
+ bzero(cc->cc_cookie_key, sizeof(cc->cc_cookie_key));
+ }
+ rw_exit_write(&cc->cc_key_lock);
+}
+
+void
+cookie_checker_deinit(struct cookie_checker *cc)
+{
+ ratelimit_deinit(&cc->cc_ratelimit_v4);
+#ifdef INET6
+ ratelimit_deinit(&cc->cc_ratelimit_v6);
+#endif
+}
+
+void
+cookie_checker_create_payload(struct cookie_checker *cc,
+ struct cookie_macs *cm, uint8_t nonce[COOKIE_NONCE_SIZE],
+ uint8_t ecookie[COOKIE_ENCRYPTED_SIZE], struct sockaddr *sa)
+{
+ uint8_t cookie[COOKIE_COOKIE_SIZE];
+
+ cookie_checker_make_cookie(cc, cookie, sa);
+ arc4random_buf(nonce, COOKIE_NONCE_SIZE);
+
+ rw_enter_read(&cc->cc_key_lock);
+ xchacha20poly1305_encrypt(ecookie, cookie, COOKIE_COOKIE_SIZE,
+ cm->mac1, COOKIE_MAC_SIZE, nonce, cc->cc_cookie_key);
+ rw_exit_read(&cc->cc_key_lock);
+
+ explicit_bzero(cookie, sizeof(cookie));
+}
+
+int
+cookie_maker_consume_payload(struct cookie_maker *cp,
+ uint8_t nonce[COOKIE_NONCE_SIZE], uint8_t ecookie[COOKIE_ENCRYPTED_SIZE])
+{
+ int ret = 0;
+ uint8_t cookie[COOKIE_COOKIE_SIZE];
+
+ rw_enter_write(&cp->cp_lock);
+
+ if (cp->cp_mac1_valid == 0) {
+ ret = ETIMEDOUT;
+ goto error;
+ }
+
+ if (xchacha20poly1305_decrypt(cookie, ecookie, COOKIE_ENCRYPTED_SIZE,
+ cp->cp_mac1_last, COOKIE_MAC_SIZE, nonce, cp->cp_cookie_key) == 0) {
+ ret = EINVAL;
+ goto error;
+ }
+
+ memcpy(cp->cp_cookie, cookie, COOKIE_COOKIE_SIZE);
+ getnanouptime(&cp->cp_birthdate);
+ cp->cp_mac1_valid = 0;
+
+error:
+ rw_exit_write(&cp->cp_lock);
+ return ret;
+}
+
+void
+cookie_maker_mac(struct cookie_maker *cp, struct cookie_macs *cm, void *buf,
+ size_t len)
+{
+ rw_enter_read(&cp->cp_lock);
+
+ cookie_macs_mac1(cm, buf, len, cp->cp_mac1_key);
+
+ memcpy(cp->cp_mac1_last, cm->mac1, COOKIE_MAC_SIZE);
+ cp->cp_mac1_valid = 1;
+
+ if (!cookie_timer_expired(&cp->cp_birthdate,
+ COOKIE_SECRET_MAX_AGE - COOKIE_SECRET_LATENCY, 0))
+ cookie_macs_mac2(cm, buf, len, cp->cp_cookie);
+ else
+ bzero(cm->mac2, COOKIE_MAC_SIZE);
+
+ rw_exit_read(&cp->cp_lock);
+}
+
+int
+cookie_checker_validate_macs(struct cookie_checker *cc, struct cookie_macs *cm,
+ void *buf, size_t len, int busy, struct sockaddr *sa)
+{
+ struct cookie_macs our_cm;
+ uint8_t cookie[COOKIE_COOKIE_SIZE];
+
+ /* Validate incoming MACs */
+ rw_enter_read(&cc->cc_key_lock);
+ cookie_macs_mac1(&our_cm, buf, len, cc->cc_mac1_key);
+ rw_exit_read(&cc->cc_key_lock);
+
+ /* If mac1 is invald, we want to drop the packet */
+ if (timingsafe_bcmp(our_cm.mac1, cm->mac1, COOKIE_MAC_SIZE) != 0)
+ return EINVAL;
+
+ if (busy != 0) {
+ cookie_checker_make_cookie(cc, cookie, sa);
+ cookie_macs_mac2(&our_cm, buf, len, cookie);
+
+ /* If the mac2 is invalid, we want to send a cookie response */
+ if (timingsafe_bcmp(our_cm.mac2, cm->mac2, COOKIE_MAC_SIZE) != 0)
+ return EAGAIN;
+
+ /* If the mac2 is valid, we may want rate limit the peer.
+ * ratelimit_allow will return either 0 or ECONNREFUSED,
+ * implying there is no ratelimiting, or we should ratelimit
+ * (refuse) respectively. */
+ if (sa->sa_family == AF_INET)
+ return ratelimit_allow(&cc->cc_ratelimit_v4, sa);
+#ifdef INET6
+ else if (sa->sa_family == AF_INET6)
+ return ratelimit_allow(&cc->cc_ratelimit_v6, sa);
+#endif
+ else
+ return EAFNOSUPPORT;
+ }
+ return 0;
+}
+
+/* Private functions */
+static void
+cookie_precompute_key(uint8_t *key, const uint8_t input[COOKIE_INPUT_SIZE],
+ const char *label)
+{
+ struct blake2s_state blake;
+
+ blake2s_init(&blake, COOKIE_KEY_SIZE);
+ blake2s_update(&blake, label, strlen(label));
+ blake2s_update(&blake, input, COOKIE_INPUT_SIZE);
+ /* TODO we shouldn't need to provide outlen to _final. we can align
+ * this with openbsd after fixing the blake library. */
+ blake2s_final(&blake, key);
+}
+
+static void
+cookie_macs_mac1(struct cookie_macs *cm, const void *buf, size_t len,
+ const uint8_t key[COOKIE_KEY_SIZE])
+{
+ struct blake2s_state state;
+ blake2s_init_key(&state, COOKIE_MAC_SIZE, key, COOKIE_KEY_SIZE);
+ blake2s_update(&state, buf, len);
+ blake2s_final(&state, cm->mac1);
+}
+
+static void
+cookie_macs_mac2(struct cookie_macs *cm, const void *buf, size_t len,
+ const uint8_t key[COOKIE_COOKIE_SIZE])
+{
+ struct blake2s_state state;
+ blake2s_init_key(&state, COOKIE_MAC_SIZE, key, COOKIE_COOKIE_SIZE);
+ blake2s_update(&state, buf, len);
+ blake2s_update(&state, cm->mac1, COOKIE_MAC_SIZE);
+ blake2s_final(&state, cm->mac2);
+}
+
+static int
+cookie_timer_expired(struct timespec *birthdate, time_t sec, long nsec)
+{
+ struct timespec uptime;
+ struct timespec expire = { .tv_sec = sec, .tv_nsec = nsec };
+
+ if (birthdate->tv_sec == 0 && birthdate->tv_nsec == 0)
+ return ETIMEDOUT;
+
+ getnanouptime(&uptime);
+ timespecadd(birthdate, &expire, &expire);
+ return timespeccmp(&uptime, &expire, >) ? ETIMEDOUT : 0;
+}
+
+static void
+cookie_checker_make_cookie(struct cookie_checker *cc,
+ uint8_t cookie[COOKIE_COOKIE_SIZE], struct sockaddr *sa)
+{
+ struct blake2s_state state;
+
+ rw_enter_write(&cc->cc_secret_lock);
+ if (cookie_timer_expired(&cc->cc_secret_birthdate,
+ COOKIE_SECRET_MAX_AGE, 0)) {
+ arc4random_buf(cc->cc_secret, COOKIE_SECRET_SIZE);
+ getnanouptime(&cc->cc_secret_birthdate);
+ }
+ blake2s_init_key(&state, COOKIE_COOKIE_SIZE, cc->cc_secret,
+ COOKIE_SECRET_SIZE);
+ rw_exit_write(&cc->cc_secret_lock);
+
+ if (sa->sa_family == AF_INET) {
+ blake2s_update(&state, (uint8_t *)&satosin(sa)->sin_addr,
+ sizeof(struct in_addr));
+ blake2s_update(&state, (uint8_t *)&satosin(sa)->sin_port,
+ sizeof(in_port_t));
+ blake2s_final(&state, cookie);
+#ifdef INET6
+ } else if (sa->sa_family == AF_INET6) {
+ blake2s_update(&state, (uint8_t *)&satosin6(sa)->sin6_addr,
+ sizeof(struct in6_addr));
+ blake2s_update(&state, (uint8_t *)&satosin6(sa)->sin6_port,
+ sizeof(in_port_t));
+ blake2s_final(&state, cookie);
+#endif
+ } else {
+ arc4random_buf(cookie, COOKIE_COOKIE_SIZE);
+ }
+}
+
+static int
+ratelimit_init(struct ratelimit *rl, uma_zone_t zone)
+{
+ rw_init(&rl->rl_lock, "ratelimit_lock");
+ arc4random_buf(&rl->rl_secret, sizeof(rl->rl_secret));
+ rl->rl_table = hashinit_flags(RATELIMIT_SIZE, M_DEVBUF,
+ &rl->rl_table_mask, M_NOWAIT);
+ rl->rl_zone = zone;
+ rl->rl_table_num = 0;
+ return rl->rl_table == NULL ? ENOBUFS : 0;
+}
+
+static void
+ratelimit_deinit(struct ratelimit *rl)
+{
+ rw_enter_write(&rl->rl_lock);
+ ratelimit_gc(rl, 1);
+ hashdestroy(rl->rl_table, M_DEVBUF, rl->rl_table_mask);
+ rw_exit_write(&rl->rl_lock);
+}
+
+static void
+ratelimit_gc(struct ratelimit *rl, int force)
+{
+ size_t i;
+ struct ratelimit_entry *r, *tr;
+ struct timespec expiry;
+
+ rw_assert_wrlock(&rl->rl_lock);
+
+ if (force) {
+ for (i = 0; i < RATELIMIT_SIZE; i++) {
+ LIST_FOREACH_SAFE(r, &rl->rl_table[i], r_entry, tr) {
+ rl->rl_table_num--;
+ LIST_REMOVE(r, r_entry);
+ uma_zfree(rl->rl_zone, r);
+ }
+ }
+ return;
+ }
+
+ if ((cookie_timer_expired(&rl->rl_last_gc, ELEMENT_TIMEOUT, 0) &&
+ rl->rl_table_num > 0)) {
+ getnanouptime(&rl->rl_last_gc);
+ getnanouptime(&expiry);
+ expiry.tv_sec -= ELEMENT_TIMEOUT;
+
+ for (i = 0; i < RATELIMIT_SIZE; i++) {
+ LIST_FOREACH_SAFE(r, &rl->rl_table[i], r_entry, tr) {
+ if (timespeccmp(&r->r_last_time, &expiry, <)) {
+ rl->rl_table_num--;
+ LIST_REMOVE(r, r_entry);
+ uma_zfree(rl->rl_zone, r);
+ }
+ }
+ }
+ }
+}
+
+static int
+ratelimit_allow(struct ratelimit *rl, struct sockaddr *sa)
+{
+ uint64_t key, tokens;
+ struct timespec diff;
+ struct ratelimit_entry *r;
+ int ret = ECONNREFUSED;
+
+ if (sa->sa_family == AF_INET)
+ /* TODO siphash24 is the FreeBSD siphash, OK? */
+ key = siphash24(&rl->rl_secret, &satosin(sa)->sin_addr,
+ IPV4_MASK_SIZE);
+#ifdef INET6
+ else if (sa->sa_family == AF_INET6)
+ key = siphash24(&rl->rl_secret, &satosin6(sa)->sin6_addr,
+ IPV6_MASK_SIZE);
+#endif
+ else
+ return ret;
+
+ rw_enter_write(&rl->rl_lock);
+
+ LIST_FOREACH(r, &rl->rl_table[key & rl->rl_table_mask], r_entry) {
+ if (r->r_af != sa->sa_family)
+ continue;
+
+ if (r->r_af == AF_INET && bcmp(&r->r_in,
+ &satosin(sa)->sin_addr, IPV4_MASK_SIZE) != 0)
+ continue;
+
+#ifdef INET6
+ if (r->r_af == AF_INET6 && bcmp(&r->r_in6,
+ &satosin6(sa)->sin6_addr, IPV6_MASK_SIZE) != 0)
+ continue;
+#endif
+
+ /* If we get to here, we've found an entry for the endpoint.
+ * We apply standard token bucket, by calculating the time
+ * lapsed since our last_time, adding that, ensuring that we
+ * cap the tokens at TOKEN_MAX. If the endpoint has no tokens
+ * left (that is tokens <= INITIATION_COST) then we block the
+ * request, otherwise we subtract the INITITIATION_COST and
+ * return OK. */
+ diff = r->r_last_time;
+ getnanouptime(&r->r_last_time);
+ timespecsub(&r->r_last_time, &diff, &diff);
+
+ tokens = r->r_tokens + diff.tv_sec * NSEC_PER_SEC + diff.tv_nsec;
+
+ if (tokens > TOKEN_MAX)
+ tokens = TOKEN_MAX;
+
+ if (tokens >= INITIATION_COST) {
+ r->r_tokens = tokens - INITIATION_COST;
+ goto ok;
+ } else {
+ r->r_tokens = tokens;
+ goto error;
+ }
+ }
+
+ /* If we get to here, we didn't have an entry for the endpoint. */
+ ratelimit_gc(rl, 0);
+
+ /* Hard limit on number of entries */
+ if (rl->rl_table_num >= RATELIMIT_SIZE_MAX)
+ goto error;
+
+ /* Goto error if out of memory */
+ if ((r = uma_zalloc(rl->rl_zone, M_NOWAIT)) == NULL)
+ goto error;
+
+ rl->rl_table_num++;
+
+ /* Insert entry into the hashtable and ensure it's initialised */
+ LIST_INSERT_HEAD(&rl->rl_table[key & rl->rl_table_mask], r, r_entry);
+ r->r_af = sa->sa_family;
+ if (r->r_af == AF_INET)
+ memcpy(&r->r_in, &satosin(sa)->sin_addr, IPV4_MASK_SIZE);
+#ifdef INET6
+ else if (r->r_af == AF_INET6)
+ memcpy(&r->r_in6, &satosin6(sa)->sin6_addr, IPV6_MASK_SIZE);
+#endif
+
+ getnanouptime(&r->r_last_time);
+ r->r_tokens = TOKEN_MAX - INITIATION_COST;
+ok:
+ ret = 0;
+error:
+ rw_exit_write(&rl->rl_lock);
+ return ret;
+}