If MOD_LOAD fails, then MOD_UNLOAD will be called to unwind module state, but wg_module_init() will have already deinitialized everything it needs to in a manner that renders it unsafe to call MOD_UNLOAD after (e.g., freed zone not reset to NULL, wg_osd_jail_slot not reset to 0). Let's simply stop trying to handle freeing everything in wg_module_init() to simplify it; let the subsequent MOD_UNLOAD deal with it, and let's make that robust against partially-constructed state. jhb@ notes that MOD_UNLOAD being called if MOD_LOAD fails is kind of an anomaly that doesn't match other paradigms in the kernel; e.g., if device_attach() fails, we don't invoke device_detach(). It's likely that a future commit will revert this and instead stop calling MOD_UNLOAD if MOD_LOAD fails, expecting modules to clean up after themselves in MOD_LOAD upon failure. Some other modules already do this and may see similar problems to the wg module (see: carp). The proper fix is decidedly a bit too invasive to do this close to 14 branching, and it requires auditing all kmods (base + ports) for potential leaks. PR: 272089 Reviewed by: emaste MFC after: 3 days Differential Revision: https://reviews.freebsd.org/D40708
508 lines
13 KiB
C
508 lines
13 KiB
C
/* 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 "opt_inet.h"
|
|
#include "opt_inet6.h"
|
|
|
|
#include <sys/param.h>
|
|
#include <sys/systm.h>
|
|
#include <sys/kernel.h>
|
|
#include <sys/lock.h>
|
|
#include <sys/mutex.h>
|
|
#include <sys/rwlock.h>
|
|
#include <sys/socket.h>
|
|
#include <crypto/siphash/siphash.h>
|
|
#include <netinet/in.h>
|
|
#include <vm/uma.h>
|
|
|
|
#include "wg_cookie.h"
|
|
|
|
#define COOKIE_MAC1_KEY_LABEL "mac1----"
|
|
#define COOKIE_COOKIE_KEY_LABEL "cookie--"
|
|
#define COOKIE_SECRET_MAX_AGE 120
|
|
#define COOKIE_SECRET_LATENCY 5
|
|
|
|
/* Constants for initiation rate limiting */
|
|
#define RATELIMIT_SIZE (1 << 13)
|
|
#define RATELIMIT_MASK (RATELIMIT_SIZE - 1)
|
|
#define RATELIMIT_SIZE_MAX (RATELIMIT_SIZE * 8)
|
|
#define INITIATIONS_PER_SECOND 20
|
|
#define INITIATIONS_BURSTABLE 5
|
|
#define INITIATION_COST (SBT_1S / INITIATIONS_PER_SECOND)
|
|
#define TOKEN_MAX (INITIATION_COST * INITIATIONS_BURSTABLE)
|
|
#define ELEMENT_TIMEOUT 1
|
|
#define IPV4_MASK_SIZE 4 /* Use all 4 bytes of IPv4 address */
|
|
#define IPV6_MASK_SIZE 8 /* Use top 8 bytes (/64) of IPv6 address */
|
|
|
|
struct ratelimit_key {
|
|
struct vnet *vnet;
|
|
uint8_t ip[IPV6_MASK_SIZE];
|
|
};
|
|
|
|
struct ratelimit_entry {
|
|
LIST_ENTRY(ratelimit_entry) r_entry;
|
|
struct ratelimit_key r_key;
|
|
sbintime_t r_last_time; /* sbinuptime */
|
|
uint64_t r_tokens;
|
|
};
|
|
|
|
struct ratelimit {
|
|
uint8_t rl_secret[SIPHASH_KEY_LENGTH];
|
|
struct mtx rl_mtx;
|
|
struct callout rl_gc;
|
|
LIST_HEAD(, ratelimit_entry) rl_table[RATELIMIT_SIZE];
|
|
size_t rl_table_num;
|
|
bool rl_initialized;
|
|
};
|
|
|
|
static void precompute_key(uint8_t *,
|
|
const uint8_t[COOKIE_INPUT_SIZE], const char *);
|
|
static void macs_mac1(struct cookie_macs *, const void *, size_t,
|
|
const uint8_t[COOKIE_KEY_SIZE]);
|
|
static void macs_mac2(struct cookie_macs *, const void *, size_t,
|
|
const uint8_t[COOKIE_COOKIE_SIZE]);
|
|
static int timer_expired(sbintime_t, uint32_t, uint32_t);
|
|
static void make_cookie(struct cookie_checker *,
|
|
uint8_t[COOKIE_COOKIE_SIZE], struct sockaddr *);
|
|
static void ratelimit_init(struct ratelimit *);
|
|
static void ratelimit_deinit(struct ratelimit *);
|
|
static void ratelimit_gc_callout(void *);
|
|
static void ratelimit_gc_schedule(struct ratelimit *);
|
|
static void ratelimit_gc(struct ratelimit *, bool);
|
|
static int ratelimit_allow(struct ratelimit *, struct sockaddr *, struct vnet *);
|
|
static uint64_t siphash13(const uint8_t [SIPHASH_KEY_LENGTH], const void *, size_t);
|
|
|
|
static struct ratelimit ratelimit_v4;
|
|
#ifdef INET6
|
|
static struct ratelimit ratelimit_v6;
|
|
#endif
|
|
static uma_zone_t ratelimit_zone;
|
|
|
|
/* Public Functions */
|
|
int
|
|
cookie_init(void)
|
|
{
|
|
if ((ratelimit_zone = uma_zcreate("wg ratelimit",
|
|
sizeof(struct ratelimit_entry), NULL, NULL, NULL, NULL, 0, 0)) == NULL)
|
|
return ENOMEM;
|
|
|
|
ratelimit_init(&ratelimit_v4);
|
|
#ifdef INET6
|
|
ratelimit_init(&ratelimit_v6);
|
|
#endif
|
|
return (0);
|
|
}
|
|
|
|
void
|
|
cookie_deinit(void)
|
|
{
|
|
ratelimit_deinit(&ratelimit_v4);
|
|
#ifdef INET6
|
|
ratelimit_deinit(&ratelimit_v6);
|
|
#endif
|
|
if (ratelimit_zone != NULL)
|
|
uma_zdestroy(ratelimit_zone);
|
|
}
|
|
|
|
void
|
|
cookie_checker_init(struct cookie_checker *cc)
|
|
{
|
|
bzero(cc, sizeof(*cc));
|
|
|
|
rw_init(&cc->cc_key_lock, "cookie_checker_key");
|
|
mtx_init(&cc->cc_secret_mtx, "cookie_checker_secret", NULL, MTX_DEF);
|
|
}
|
|
|
|
void
|
|
cookie_checker_free(struct cookie_checker *cc)
|
|
{
|
|
rw_destroy(&cc->cc_key_lock);
|
|
mtx_destroy(&cc->cc_secret_mtx);
|
|
explicit_bzero(cc, sizeof(*cc));
|
|
}
|
|
|
|
void
|
|
cookie_checker_update(struct cookie_checker *cc,
|
|
const uint8_t key[COOKIE_INPUT_SIZE])
|
|
{
|
|
rw_wlock(&cc->cc_key_lock);
|
|
if (key) {
|
|
precompute_key(cc->cc_mac1_key, key, COOKIE_MAC1_KEY_LABEL);
|
|
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_wunlock(&cc->cc_key_lock);
|
|
}
|
|
|
|
void
|
|
cookie_checker_create_payload(struct cookie_checker *cc,
|
|
struct cookie_macs *macs, uint8_t nonce[COOKIE_NONCE_SIZE],
|
|
uint8_t ecookie[COOKIE_ENCRYPTED_SIZE], struct sockaddr *sa)
|
|
{
|
|
uint8_t cookie[COOKIE_COOKIE_SIZE];
|
|
|
|
make_cookie(cc, cookie, sa);
|
|
arc4random_buf(nonce, COOKIE_NONCE_SIZE);
|
|
|
|
rw_rlock(&cc->cc_key_lock);
|
|
xchacha20poly1305_encrypt(ecookie, cookie, COOKIE_COOKIE_SIZE,
|
|
macs->mac1, COOKIE_MAC_SIZE, nonce, cc->cc_cookie_key);
|
|
rw_runlock(&cc->cc_key_lock);
|
|
|
|
explicit_bzero(cookie, sizeof(cookie));
|
|
}
|
|
|
|
void
|
|
cookie_maker_init(struct cookie_maker *cm, const uint8_t key[COOKIE_INPUT_SIZE])
|
|
{
|
|
bzero(cm, sizeof(*cm));
|
|
precompute_key(cm->cm_mac1_key, key, COOKIE_MAC1_KEY_LABEL);
|
|
precompute_key(cm->cm_cookie_key, key, COOKIE_COOKIE_KEY_LABEL);
|
|
rw_init(&cm->cm_lock, "cookie_maker");
|
|
}
|
|
|
|
void
|
|
cookie_maker_free(struct cookie_maker *cm)
|
|
{
|
|
rw_destroy(&cm->cm_lock);
|
|
explicit_bzero(cm, sizeof(*cm));
|
|
}
|
|
|
|
int
|
|
cookie_maker_consume_payload(struct cookie_maker *cm,
|
|
uint8_t nonce[COOKIE_NONCE_SIZE], uint8_t ecookie[COOKIE_ENCRYPTED_SIZE])
|
|
{
|
|
uint8_t cookie[COOKIE_COOKIE_SIZE];
|
|
int ret;
|
|
|
|
rw_rlock(&cm->cm_lock);
|
|
if (!cm->cm_mac1_sent) {
|
|
ret = ETIMEDOUT;
|
|
goto error;
|
|
}
|
|
|
|
if (!xchacha20poly1305_decrypt(cookie, ecookie, COOKIE_ENCRYPTED_SIZE,
|
|
cm->cm_mac1_last, COOKIE_MAC_SIZE, nonce, cm->cm_cookie_key)) {
|
|
ret = EINVAL;
|
|
goto error;
|
|
}
|
|
rw_runlock(&cm->cm_lock);
|
|
|
|
rw_wlock(&cm->cm_lock);
|
|
memcpy(cm->cm_cookie, cookie, COOKIE_COOKIE_SIZE);
|
|
cm->cm_cookie_birthdate = getsbinuptime();
|
|
cm->cm_cookie_valid = true;
|
|
cm->cm_mac1_sent = false;
|
|
rw_wunlock(&cm->cm_lock);
|
|
|
|
return 0;
|
|
error:
|
|
rw_runlock(&cm->cm_lock);
|
|
return ret;
|
|
}
|
|
|
|
void
|
|
cookie_maker_mac(struct cookie_maker *cm, struct cookie_macs *macs, void *buf,
|
|
size_t len)
|
|
{
|
|
rw_wlock(&cm->cm_lock);
|
|
macs_mac1(macs, buf, len, cm->cm_mac1_key);
|
|
memcpy(cm->cm_mac1_last, macs->mac1, COOKIE_MAC_SIZE);
|
|
cm->cm_mac1_sent = true;
|
|
|
|
if (cm->cm_cookie_valid &&
|
|
!timer_expired(cm->cm_cookie_birthdate,
|
|
COOKIE_SECRET_MAX_AGE - COOKIE_SECRET_LATENCY, 0)) {
|
|
macs_mac2(macs, buf, len, cm->cm_cookie);
|
|
} else {
|
|
bzero(macs->mac2, COOKIE_MAC_SIZE);
|
|
cm->cm_cookie_valid = false;
|
|
}
|
|
rw_wunlock(&cm->cm_lock);
|
|
}
|
|
|
|
int
|
|
cookie_checker_validate_macs(struct cookie_checker *cc, struct cookie_macs *macs,
|
|
void *buf, size_t len, bool check_cookie, struct sockaddr *sa, struct vnet *vnet)
|
|
{
|
|
struct cookie_macs our_macs;
|
|
uint8_t cookie[COOKIE_COOKIE_SIZE];
|
|
|
|
/* Validate incoming MACs */
|
|
rw_rlock(&cc->cc_key_lock);
|
|
macs_mac1(&our_macs, buf, len, cc->cc_mac1_key);
|
|
rw_runlock(&cc->cc_key_lock);
|
|
|
|
/* If mac1 is invald, we want to drop the packet */
|
|
if (timingsafe_bcmp(our_macs.mac1, macs->mac1, COOKIE_MAC_SIZE) != 0)
|
|
return EINVAL;
|
|
|
|
if (check_cookie) {
|
|
make_cookie(cc, cookie, sa);
|
|
macs_mac2(&our_macs, buf, len, cookie);
|
|
|
|
/* If the mac2 is invalid, we want to send a cookie response */
|
|
if (timingsafe_bcmp(our_macs.mac2, macs->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(&ratelimit_v4, sa, vnet);
|
|
#ifdef INET6
|
|
else if (sa->sa_family == AF_INET6)
|
|
return ratelimit_allow(&ratelimit_v6, sa, vnet);
|
|
#endif
|
|
else
|
|
return EAFNOSUPPORT;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Private functions */
|
|
static void
|
|
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);
|
|
blake2s_final(&blake, key);
|
|
}
|
|
|
|
static void
|
|
macs_mac1(struct cookie_macs *macs, 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, macs->mac1);
|
|
}
|
|
|
|
static void
|
|
macs_mac2(struct cookie_macs *macs, 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, macs->mac1, COOKIE_MAC_SIZE);
|
|
blake2s_final(&state, macs->mac2);
|
|
}
|
|
|
|
static __inline int
|
|
timer_expired(sbintime_t timer, uint32_t sec, uint32_t nsec)
|
|
{
|
|
sbintime_t now = getsbinuptime();
|
|
return (now > (timer + sec * SBT_1S + nstosbt(nsec))) ? ETIMEDOUT : 0;
|
|
}
|
|
|
|
static void
|
|
make_cookie(struct cookie_checker *cc, uint8_t cookie[COOKIE_COOKIE_SIZE],
|
|
struct sockaddr *sa)
|
|
{
|
|
struct blake2s_state state;
|
|
|
|
mtx_lock(&cc->cc_secret_mtx);
|
|
if (timer_expired(cc->cc_secret_birthdate,
|
|
COOKIE_SECRET_MAX_AGE, 0)) {
|
|
arc4random_buf(cc->cc_secret, COOKIE_SECRET_SIZE);
|
|
cc->cc_secret_birthdate = getsbinuptime();
|
|
}
|
|
blake2s_init_key(&state, COOKIE_COOKIE_SIZE, cc->cc_secret,
|
|
COOKIE_SECRET_SIZE);
|
|
mtx_unlock(&cc->cc_secret_mtx);
|
|
|
|
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 void
|
|
ratelimit_init(struct ratelimit *rl)
|
|
{
|
|
size_t i;
|
|
mtx_init(&rl->rl_mtx, "ratelimit_lock", NULL, MTX_DEF);
|
|
callout_init_mtx(&rl->rl_gc, &rl->rl_mtx, 0);
|
|
arc4random_buf(rl->rl_secret, sizeof(rl->rl_secret));
|
|
for (i = 0; i < RATELIMIT_SIZE; i++)
|
|
LIST_INIT(&rl->rl_table[i]);
|
|
rl->rl_table_num = 0;
|
|
rl->rl_initialized = true;
|
|
}
|
|
|
|
static void
|
|
ratelimit_deinit(struct ratelimit *rl)
|
|
{
|
|
if (!rl->rl_initialized)
|
|
return;
|
|
mtx_lock(&rl->rl_mtx);
|
|
callout_stop(&rl->rl_gc);
|
|
ratelimit_gc(rl, true);
|
|
mtx_unlock(&rl->rl_mtx);
|
|
mtx_destroy(&rl->rl_mtx);
|
|
|
|
rl->rl_initialized = false;
|
|
}
|
|
|
|
static void
|
|
ratelimit_gc_callout(void *_rl)
|
|
{
|
|
/* callout will lock rl_mtx for us */
|
|
ratelimit_gc(_rl, false);
|
|
}
|
|
|
|
static void
|
|
ratelimit_gc_schedule(struct ratelimit *rl)
|
|
{
|
|
/* Trigger another GC if needed. There is no point calling GC if there
|
|
* are no entries in the table. We also want to ensure that GC occurs
|
|
* on a regular interval, so don't override a currently pending GC.
|
|
*
|
|
* In the case of a forced ratelimit_gc, there will be no entries left
|
|
* so we will will not schedule another GC. */
|
|
if (rl->rl_table_num > 0 && !callout_pending(&rl->rl_gc))
|
|
callout_reset(&rl->rl_gc, ELEMENT_TIMEOUT * hz,
|
|
ratelimit_gc_callout, rl);
|
|
}
|
|
|
|
static void
|
|
ratelimit_gc(struct ratelimit *rl, bool force)
|
|
{
|
|
size_t i;
|
|
struct ratelimit_entry *r, *tr;
|
|
sbintime_t expiry;
|
|
|
|
mtx_assert(&rl->rl_mtx, MA_OWNED);
|
|
|
|
if (rl->rl_table_num == 0)
|
|
return;
|
|
|
|
expiry = getsbinuptime() - ELEMENT_TIMEOUT * SBT_1S;
|
|
|
|
for (i = 0; i < RATELIMIT_SIZE; i++) {
|
|
LIST_FOREACH_SAFE(r, &rl->rl_table[i], r_entry, tr) {
|
|
if (r->r_last_time < expiry || force) {
|
|
rl->rl_table_num--;
|
|
LIST_REMOVE(r, r_entry);
|
|
uma_zfree(ratelimit_zone, r);
|
|
}
|
|
}
|
|
}
|
|
|
|
ratelimit_gc_schedule(rl);
|
|
}
|
|
|
|
static int
|
|
ratelimit_allow(struct ratelimit *rl, struct sockaddr *sa, struct vnet *vnet)
|
|
{
|
|
uint64_t bucket, tokens;
|
|
sbintime_t diff, now;
|
|
struct ratelimit_entry *r;
|
|
int ret = ECONNREFUSED;
|
|
struct ratelimit_key key = { .vnet = vnet };
|
|
size_t len = sizeof(key);
|
|
|
|
if (sa->sa_family == AF_INET) {
|
|
memcpy(key.ip, &satosin(sa)->sin_addr, IPV4_MASK_SIZE);
|
|
len -= IPV6_MASK_SIZE - IPV4_MASK_SIZE;
|
|
}
|
|
#ifdef INET6
|
|
else if (sa->sa_family == AF_INET6)
|
|
memcpy(key.ip, &satosin6(sa)->sin6_addr, IPV6_MASK_SIZE);
|
|
#endif
|
|
else
|
|
return ret;
|
|
|
|
bucket = siphash13(rl->rl_secret, &key, len) & RATELIMIT_MASK;
|
|
mtx_lock(&rl->rl_mtx);
|
|
|
|
LIST_FOREACH(r, &rl->rl_table[bucket], r_entry) {
|
|
if (bcmp(&r->r_key, &key, len) != 0)
|
|
continue;
|
|
|
|
/* 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. */
|
|
now = getsbinuptime();
|
|
diff = now - r->r_last_time;
|
|
r->r_last_time = now;
|
|
|
|
tokens = r->r_tokens + diff;
|
|
|
|
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, let's
|
|
* add one if we have space. */
|
|
if (rl->rl_table_num >= RATELIMIT_SIZE_MAX)
|
|
goto error;
|
|
|
|
/* Goto error if out of memory */
|
|
if ((r = uma_zalloc(ratelimit_zone, M_NOWAIT | M_ZERO)) == NULL)
|
|
goto error;
|
|
|
|
rl->rl_table_num++;
|
|
|
|
/* Insert entry into the hashtable and ensure it's initialised */
|
|
LIST_INSERT_HEAD(&rl->rl_table[bucket], r, r_entry);
|
|
r->r_key = key;
|
|
r->r_last_time = getsbinuptime();
|
|
r->r_tokens = TOKEN_MAX - INITIATION_COST;
|
|
|
|
/* If we've added a new entry, let's trigger GC. */
|
|
ratelimit_gc_schedule(rl);
|
|
ok:
|
|
ret = 0;
|
|
error:
|
|
mtx_unlock(&rl->rl_mtx);
|
|
return ret;
|
|
}
|
|
|
|
static uint64_t siphash13(const uint8_t key[SIPHASH_KEY_LENGTH], const void *src, size_t len)
|
|
{
|
|
SIPHASH_CTX ctx;
|
|
return (SipHashX(&ctx, 1, 3, key, src, len));
|
|
}
|
|
|
|
#ifdef SELFTESTS
|
|
#include "selftest/cookie.c"
|
|
#endif /* SELFTESTS */
|