tor/src/or/rendcache.c
David Goulet 8b98172579 Add a rend cache failure info dup function
When validating a new descriptor against our rend cache failure, we were
added the failure entry to the new cache entry without duplicating. It was
then freed just after the validation ending up in a very bad memory state
that was making tor abort(). To fix this, a dup function has been added and
used just before adding the failure entry.

Fixes #17041

Signed-off-by: David Goulet <dgoulet@ev0ke.net>
2015-09-11 15:09:07 +02:00

891 lines
31 KiB
C

/* Copyright (c) 2015, The Tor Project, Inc. */
/* See LICENSE for licensing information */
/**
* \file rendcache.c
* \brief Hidden service desriptor cache.
**/
#include "rendcache.h"
#include "config.h"
#include "rephist.h"
#include "routerlist.h"
#include "routerparse.h"
/** Map from service id (as generated by rend_get_service_id) to
* rend_cache_entry_t. */
static strmap_t *rend_cache = NULL;
/** Map from descriptor id to rend_cache_entry_t; only for hidden service
* directories. */
static digestmap_t *rend_cache_v2_dir = NULL;
/** (Client side only) Map from service id to rend_cache_failure_t. This
* cache is used to track intro point(IP) failures so we know when to keep
* or discard a new descriptor we just fetched. Here is a description of the
* cache behavior.
*
* Everytime tor discards an IP (ex: receives a NACK), we add an entry to
* this cache noting the identity digest of the IP and it's failure type for
* the service ID. The reason we indexed this cache by service ID is to
* differentiate errors that can occur only for a specific service like a
* NACK for instance. It applies for one but maybe not for the others.
*
* Once a service descriptor is fetched and considered valid, each IP is
* looked up in this cache and if present, it is discarded from the fetched
* descriptor. At the end, all IP(s) in the cache, for a specific service
* ID, that were NOT present in the descriptor are removed from this cache.
* Which means that if at least one IP was not in this cache, thus usuable,
* it's considered a new descriptor so we keep it. Else, if all IPs were in
* this cache, we discard the descriptor as it's considered unsuable.
*
* Once a descriptor is removed from the rend cache or expires, the entry
* in this cache is also removed for the service ID.
*
* This scheme allows us to not realy on the descriptor's timestamp (which
* is rounded down to the hour) to know if we have a newer descriptor. We
* only rely on the usability of intro points from an internal state. */
static strmap_t *rend_cache_failure = NULL;
/** DOCDOC */
static size_t rend_cache_total_allocation = 0;
/** Initializes the service descriptor cache.
*/
void
rend_cache_init(void)
{
rend_cache = strmap_new();
rend_cache_v2_dir = digestmap_new();
rend_cache_failure = strmap_new();
}
/** Return the approximate number of bytes needed to hold <b>e</b>. */
static size_t
rend_cache_entry_allocation(const rend_cache_entry_t *e)
{
if (!e)
return 0;
/* This doesn't count intro_nodes or key size */
return sizeof(*e) + e->len + sizeof(*e->parsed);
}
/** DOCDOC */
size_t
rend_cache_get_total_allocation(void)
{
return rend_cache_total_allocation;
}
/** Decrement the total bytes attributed to the rendezvous cache by n. */
static void
rend_cache_decrement_allocation(size_t n)
{
static int have_underflowed = 0;
if (rend_cache_total_allocation >= n) {
rend_cache_total_allocation -= n;
} else {
rend_cache_total_allocation = 0;
if (! have_underflowed) {
have_underflowed = 1;
log_warn(LD_BUG, "Underflow in rend_cache_decrement_allocation");
}
}
}
/** Increase the total bytes attributed to the rendezvous cache by n. */
static void
rend_cache_increment_allocation(size_t n)
{
static int have_overflowed = 0;
if (rend_cache_total_allocation <= SIZE_MAX - n) {
rend_cache_total_allocation += n;
} else {
rend_cache_total_allocation = SIZE_MAX;
if (! have_overflowed) {
have_overflowed = 1;
log_warn(LD_BUG, "Overflow in rend_cache_increment_allocation");
}
}
}
/** Helper: free a rend cache failure intro object. */
static void
rend_cache_failure_intro_entry_free(rend_cache_failure_intro_t *entry)
{
if (entry == NULL) {
return;
}
tor_free(entry);
}
/** Allocate a rend cache failure intro object and return it. <b>failure</b>
* is set into the object. This function can not fail. */
static rend_cache_failure_intro_t *
rend_cache_failure_intro_entry_new(rend_intro_point_failure_t failure)
{
rend_cache_failure_intro_t *entry = tor_malloc(sizeof(*entry));
entry->failure_type = failure;
entry->created_ts = time(NULL);
return entry;
}
/** Helper: free a rend cache failure object. */
static void
rend_cache_failure_entry_free(rend_cache_failure_t *entry)
{
if (entry == NULL) {
return;
}
/* Free and remove every intro failure object. */
DIGESTMAP_FOREACH_MODIFY(entry->intro_failures, key,
rend_cache_failure_intro_t *, e) {
rend_cache_failure_intro_entry_free(e);
MAP_DEL_CURRENT(key);
} DIGESTMAP_FOREACH_END;
tor_free(entry);
}
/** Helper: deallocate a rend_cache_failure_t. (Used with strmap_free(),
* which requires a function pointer whose argument is void*). */
static void
rend_cache_failure_entry_free_(void *entry)
{
rend_cache_failure_entry_free(entry);
}
/** Allocate a rend cache failure object and return it. This function can
* not fail. */
static rend_cache_failure_t *
rend_cache_failure_entry_new(void)
{
rend_cache_failure_t *entry = tor_malloc(sizeof(*entry));
entry->intro_failures = digestmap_new();
return entry;
}
/** Remove failure cache entry for the service ID in the given descriptor
* <b>desc</b>. */
static void
rend_cache_failure_remove(rend_service_descriptor_t *desc)
{
char service_id[REND_SERVICE_ID_LEN_BASE32 + 1];
rend_cache_failure_t *entry;
if (desc == NULL) {
return;
}
if (rend_get_service_id(desc->pk, service_id) < 0) {
return;
}
entry = strmap_get_lc(rend_cache_failure, service_id);
if (entry != NULL) {
strmap_remove_lc(rend_cache_failure, service_id);
rend_cache_failure_entry_free(entry);
}
}
/** Helper: free storage held by a single service descriptor cache entry. */
static void
rend_cache_entry_free(rend_cache_entry_t *e)
{
if (!e)
return;
rend_cache_decrement_allocation(rend_cache_entry_allocation(e));
/* We are about to remove a descriptor from the cache so remove the entry
* in the failure cache. */
rend_cache_failure_remove(e->parsed);
rend_service_descriptor_free(e->parsed);
tor_free(e->desc);
tor_free(e);
}
/** Helper: deallocate a rend_cache_entry_t. (Used with strmap_free(), which
* requires a function pointer whose argument is void*). */
static void
rend_cache_entry_free_(void *p)
{
rend_cache_entry_free(p);
}
/** Free all storage held by the service descriptor cache. */
void
rend_cache_free_all(void)
{
strmap_free(rend_cache, rend_cache_entry_free_);
digestmap_free(rend_cache_v2_dir, rend_cache_entry_free_);
strmap_free(rend_cache_failure, rend_cache_failure_entry_free_);
rend_cache = NULL;
rend_cache_v2_dir = NULL;
rend_cache_failure = NULL;
rend_cache_total_allocation = 0;
}
/** Remove all entries that re REND_CACHE_FAILURE_MAX_AGE old. This is
* called every second.
*
* We have to clean these regurlarly else if for whatever reasons an hidden
* service goes offline and a client tries to connect to it during that
* time, a failure entry is created and the client will be unable to connect
* for a while even though the service has return online. */
void
rend_cache_failure_clean(time_t now)
{
time_t cutoff = now - REND_CACHE_FAILURE_MAX_AGE;
STRMAP_FOREACH_MODIFY(rend_cache_failure, key,
rend_cache_failure_t *, ent) {
/* Free and remove every intro failure object that match the cutoff. */
DIGESTMAP_FOREACH_MODIFY(ent->intro_failures, ip_key,
rend_cache_failure_intro_t *, ip_ent) {
if (ip_ent->created_ts < cutoff) {
rend_cache_failure_intro_entry_free(ip_ent);
MAP_DEL_CURRENT(ip_key);
}
} DIGESTMAP_FOREACH_END;
/* If the entry is now empty of intro point failures, remove it. */
if (digestmap_isempty(ent->intro_failures)) {
rend_cache_failure_entry_free(ent);
MAP_DEL_CURRENT(key);
}
} STRMAP_FOREACH_END;
}
/** Removes all old entries from the service descriptor cache.
*/
void
rend_cache_clean(time_t now)
{
strmap_iter_t *iter;
const char *key;
void *val;
rend_cache_entry_t *ent;
time_t cutoff = now - REND_CACHE_MAX_AGE - REND_CACHE_MAX_SKEW;
for (iter = strmap_iter_init(rend_cache); !strmap_iter_done(iter); ) {
strmap_iter_get(iter, &key, &val);
ent = (rend_cache_entry_t*)val;
if (ent->parsed->timestamp < cutoff) {
iter = strmap_iter_next_rmv(rend_cache, iter);
rend_cache_entry_free(ent);
} else {
iter = strmap_iter_next(rend_cache, iter);
}
}
}
/** Remove ALL entries from the rendezvous service descriptor cache.
*/
void
rend_cache_purge(void)
{
if (rend_cache) {
log_info(LD_REND, "Purging HS descriptor cache");
strmap_free(rend_cache, rend_cache_entry_free_);
}
rend_cache = strmap_new();
}
/** Remove ALL entries from the failure cache. This is also called when a
* NEWNYM signal is received. */
void
rend_cache_failure_purge(void)
{
if (rend_cache_failure) {
log_info(LD_REND, "Purging HS failure cache");
strmap_free(rend_cache_failure, rend_cache_failure_entry_free_);
}
rend_cache_failure = strmap_new();
}
/** Lookup the rend failure cache using a relay identity digest in
* <b>identity</b> and service ID <b>service_id</b>. If found, the intro
* failure is set in <b>intro_entry</b> else it stays untouched. Return 1
* iff found else 0. */
static int
cache_failure_intro_lookup(const uint8_t *identity, const char *service_id,
rend_cache_failure_intro_t **intro_entry)
{
rend_cache_failure_t *elem;
rend_cache_failure_intro_t *intro_elem;
tor_assert(rend_cache_failure);
if (intro_entry) {
*intro_entry = NULL;
}
/* Lookup descriptor and return it. */
elem = strmap_get_lc(rend_cache_failure, service_id);
if (elem == NULL) {
goto not_found;
}
intro_elem = digestmap_get(elem->intro_failures, (char *) identity);
if (intro_elem == NULL) {
goto not_found;
}
if (intro_entry) {
*intro_entry = intro_elem;
}
return 1;
not_found:
return 0;
}
/** Allocate a new cache failure intro object and copy the content from
* <b>entry</b> to this newly allocated object. Return it. */
static rend_cache_failure_intro_t *
cache_failure_intro_dup(const rend_cache_failure_intro_t *entry)
{
rend_cache_failure_intro_t *ent_dup =
rend_cache_failure_intro_entry_new(entry->failure_type);
ent_dup->created_ts = entry->created_ts;
return ent_dup;
}
/** Add an intro point failure to the failure cache using the relay
* <b>identity</b> and service ID <b>service_id</b>. Record the
* <b>failure</b> in that object. */
static void
cache_failure_intro_add(const uint8_t *identity, const char *service_id,
rend_intro_point_failure_t failure)
{
rend_cache_failure_t *fail_entry;
rend_cache_failure_intro_t *entry;
/* Make sure we have a failure object for this service ID and if not,
* create it with this new intro failure entry. */
fail_entry = strmap_get_lc(rend_cache_failure, service_id);
if (fail_entry == NULL) {
fail_entry = rend_cache_failure_entry_new();
/* Add failure entry to global rend failure cache. */
strmap_set_lc(rend_cache_failure, service_id, fail_entry);
}
entry = rend_cache_failure_intro_entry_new(failure);
digestmap_set(fail_entry->intro_failures, (char *) identity, entry);
}
/** Using a parsed descriptor <b>desc</b>, check if the introduction points
* are present in the failure cache and if so they are removed from the
* descriptor and kept into the failure cache. Then, each intro points that
* are NOT in the descriptor but in the failure cache for the given
* <b>service_id</b> are removed from the failure cache. */
static void
validate_intro_point_failure(const rend_service_descriptor_t *desc,
const char *service_id)
{
rend_cache_failure_t *new_entry, *cur_entry;
/* New entry for the service ID that will be replacing the one in the
* failure cache since we have a new descriptor. In the case where all
* intro points are removed, we are assured that the new entry is the same
* as the current one. */
new_entry = tor_malloc(sizeof(*new_entry));
new_entry->intro_failures = digestmap_new();
tor_assert(desc);
SMARTLIST_FOREACH_BEGIN(desc->intro_nodes, rend_intro_point_t *, intro) {
int found;
rend_cache_failure_intro_t *entry;
const uint8_t *identity =
(uint8_t *) intro->extend_info->identity_digest;
found = cache_failure_intro_lookup(identity, service_id, &entry);
if (found) {
/* Dup here since it will be freed at the end when removing the
* original entry in the cache. */
rend_cache_failure_intro_t *ent_dup = cache_failure_intro_dup(entry);
/* This intro point is in our cache, discard it from the descriptor
* because chances are that it's unusable. */
SMARTLIST_DEL_CURRENT(desc->intro_nodes, intro);
rend_intro_point_free(intro);
/* Keep it for our new entry. */
digestmap_set(new_entry->intro_failures, (char *) identity, ent_dup);
continue;
}
} SMARTLIST_FOREACH_END(intro);
/* Swap the failure entry in the cache and free the current one. */
cur_entry = strmap_get_lc(rend_cache_failure, service_id);
if (cur_entry != NULL) {
rend_cache_failure_entry_free(cur_entry);
}
strmap_set_lc(rend_cache_failure, service_id, new_entry);
}
/** Note down an intro failure in the rend failure cache using the type of
* failure in <b>failure</b> for the relay identity digest in
* <b>identity</b> and service ID <b>service_id</b>. If an entry already
* exists in the cache, the failure type is changed with <b>failure</b>. */
void
rend_cache_intro_failure_note(rend_intro_point_failure_t failure,
const uint8_t *identity,
const char *service_id)
{
int found;
rend_cache_failure_intro_t *entry;
found = cache_failure_intro_lookup(identity, service_id, &entry);
if (!found) {
cache_failure_intro_add(identity, service_id, failure);
} else {
/* Replace introduction point failure with this one. */
entry->failure_type = failure;
}
}
/** Remove all old v2 descriptors and those for which this hidden service
* directory is not responsible for any more.
*
* If at all possible, remove at least <b>force_remove</b> bytes of data.
*/
void
rend_cache_clean_v2_descs_as_dir(time_t now, size_t force_remove)
{
digestmap_iter_t *iter;
time_t cutoff = now - REND_CACHE_MAX_AGE - REND_CACHE_MAX_SKEW;
const int LAST_SERVED_CUTOFF_STEP = 1800;
time_t last_served_cutoff = cutoff;
size_t bytes_removed = 0;
do {
for (iter = digestmap_iter_init(rend_cache_v2_dir);
!digestmap_iter_done(iter); ) {
const char *key;
void *val;
rend_cache_entry_t *ent;
digestmap_iter_get(iter, &key, &val);
ent = val;
if (ent->parsed->timestamp < cutoff ||
ent->last_served < last_served_cutoff ||
!hid_serv_responsible_for_desc_id(key)) {
char key_base32[REND_DESC_ID_V2_LEN_BASE32 + 1];
base32_encode(key_base32, sizeof(key_base32), key, DIGEST_LEN);
log_info(LD_REND, "Removing descriptor with ID '%s' from cache",
safe_str_client(key_base32));
bytes_removed += rend_cache_entry_allocation(ent);
iter = digestmap_iter_next_rmv(rend_cache_v2_dir, iter);
rend_cache_entry_free(ent);
} else {
iter = digestmap_iter_next(rend_cache_v2_dir, iter);
}
}
/* In case we didn't remove enough bytes, advance the cutoff a little. */
last_served_cutoff += LAST_SERVED_CUTOFF_STEP;
if (last_served_cutoff > now)
break;
} while (bytes_removed < force_remove);
}
/** Lookup in the client cache the given service ID <b>query</b> for
* <b>version</b>.
*
* Return 0 if found and if <b>e</b> is non NULL, set it with the entry
* found. Else, a negative value is returned and <b>e</b> is untouched.
* -EINVAL means that <b>query</b> is not a valid service id.
* -ENOENT means that no entry in the cache was found. */
int
rend_cache_lookup_entry(const char *query, int version, rend_cache_entry_t **e)
{
int ret = 0;
char key[REND_SERVICE_ID_LEN_BASE32 + 2]; /* <version><query>\0 */
rend_cache_entry_t *entry = NULL;
static const int default_version = 2;
tor_assert(rend_cache);
tor_assert(query);
if (!rend_valid_service_id(query)) {
ret = -EINVAL;
goto end;
}
switch (version) {
case 0:
log_warn(LD_REND, "Cache lookup of a v0 renddesc is deprecated.");
break;
case 2:
/* Default is version 2. */
default:
tor_snprintf(key, sizeof(key), "%d%s", default_version, query);
entry = strmap_get_lc(rend_cache, key);
break;
}
if (!entry) {
ret = -ENOENT;
goto end;
}
tor_assert(entry->parsed && entry->parsed->intro_nodes);
if (e) {
*e = entry;
}
end:
return ret;
}
/** Lookup the v2 service descriptor with base32-encoded <b>desc_id</b> and
* copy the pointer to it to *<b>desc</b>. Return 1 on success, 0 on
* well-formed-but-not-found, and -1 on failure.
*/
int
rend_cache_lookup_v2_desc_as_dir(const char *desc_id, const char **desc)
{
rend_cache_entry_t *e;
char desc_id_digest[DIGEST_LEN];
tor_assert(rend_cache_v2_dir);
if (base32_decode(desc_id_digest, DIGEST_LEN,
desc_id, REND_DESC_ID_V2_LEN_BASE32) < 0) {
log_fn(LOG_PROTOCOL_WARN, LD_REND,
"Rejecting v2 rendezvous descriptor request -- descriptor ID "
"contains illegal characters: %s",
safe_str(desc_id));
return -1;
}
/* Lookup descriptor and return. */
e = digestmap_get(rend_cache_v2_dir, desc_id_digest);
if (e) {
*desc = e->desc;
e->last_served = approx_time();
return 1;
}
return 0;
}
/** Parse the v2 service descriptor(s) in <b>desc</b> and store it/them to the
* local rend cache. Don't attempt to decrypt the included list of introduction
* points (as we don't have a descriptor cookie for it).
*
* If we have a newer descriptor with the same ID, ignore this one.
* If we have an older descriptor with the same ID, replace it.
*
* Return an appropriate rend_cache_store_status_t.
*/
rend_cache_store_status_t
rend_cache_store_v2_desc_as_dir(const char *desc)
{
const or_options_t *options = get_options();
rend_service_descriptor_t *parsed;
char desc_id[DIGEST_LEN];
char *intro_content;
size_t intro_size;
size_t encoded_size;
char desc_id_base32[REND_DESC_ID_V2_LEN_BASE32 + 1];
int number_parsed = 0, number_stored = 0;
const char *current_desc = desc;
const char *next_desc;
rend_cache_entry_t *e;
time_t now = time(NULL);
tor_assert(rend_cache_v2_dir);
tor_assert(desc);
if (!hid_serv_acting_as_directory()) {
/* Cannot store descs, because we are (currently) not acting as
* hidden service directory. */
log_info(LD_REND, "Cannot store descs: Not acting as hs dir");
return RCS_NOTDIR;
}
while (rend_parse_v2_service_descriptor(&parsed, desc_id, &intro_content,
&intro_size, &encoded_size,
&next_desc, current_desc, 1) >= 0) {
number_parsed++;
/* We don't care about the introduction points. */
tor_free(intro_content);
/* For pretty log statements. */
base32_encode(desc_id_base32, sizeof(desc_id_base32),
desc_id, DIGEST_LEN);
/* Is desc ID in the range that we are (directly or indirectly) responsible
* for? */
if (!hid_serv_responsible_for_desc_id(desc_id)) {
log_info(LD_REND, "Service descriptor with desc ID %s is not in "
"interval that we are responsible for.",
safe_str_client(desc_id_base32));
goto skip;
}
/* Is descriptor too old? */
if (parsed->timestamp < now - REND_CACHE_MAX_AGE-REND_CACHE_MAX_SKEW) {
log_info(LD_REND, "Service descriptor with desc ID %s is too old.",
safe_str(desc_id_base32));
goto skip;
}
/* Is descriptor too far in the future? */
if (parsed->timestamp > now + REND_CACHE_MAX_SKEW) {
log_info(LD_REND, "Service descriptor with desc ID %s is too far in the "
"future.",
safe_str(desc_id_base32));
goto skip;
}
/* Do we already have a newer descriptor? */
e = digestmap_get(rend_cache_v2_dir, desc_id);
if (e && e->parsed->timestamp > parsed->timestamp) {
log_info(LD_REND, "We already have a newer service descriptor with the "
"same desc ID %s and version.",
safe_str(desc_id_base32));
goto skip;
}
/* Do we already have this descriptor? */
if (e && !strcmp(desc, e->desc)) {
log_info(LD_REND, "We already have this service descriptor with desc "
"ID %s.", safe_str(desc_id_base32));
goto skip;
}
/* Store received descriptor. */
if (!e) {
e = tor_malloc_zero(sizeof(rend_cache_entry_t));
digestmap_set(rend_cache_v2_dir, desc_id, e);
/* Treat something just uploaded as having been served a little
* while ago, so that flooding with new descriptors doesn't help
* too much.
*/
e->last_served = approx_time() - 3600;
} else {
rend_cache_decrement_allocation(rend_cache_entry_allocation(e));
rend_service_descriptor_free(e->parsed);
tor_free(e->desc);
}
e->parsed = parsed;
e->desc = tor_strndup(current_desc, encoded_size);
e->len = encoded_size;
rend_cache_increment_allocation(rend_cache_entry_allocation(e));
log_info(LD_REND, "Successfully stored service descriptor with desc ID "
"'%s' and len %d.",
safe_str(desc_id_base32), (int)encoded_size);
/* Statistics: Note down this potentially new HS. */
if (options->HiddenServiceStatistics) {
rep_hist_stored_maybe_new_hs(e->parsed->pk);
}
number_stored++;
goto advance;
skip:
rend_service_descriptor_free(parsed);
advance:
/* advance to next descriptor, if available. */
current_desc = next_desc;
/* check if there is a next descriptor. */
if (!current_desc ||
strcmpstart(current_desc, "rendezvous-service-descriptor "))
break;
}
if (!number_parsed) {
log_info(LD_REND, "Could not parse any descriptor.");
return RCS_BADDESC;
}
log_info(LD_REND, "Parsed %d and added %d descriptor%s.",
number_parsed, number_stored, number_stored != 1 ? "s" : "");
return RCS_OKAY;
}
/** Parse the v2 service descriptor in <b>desc</b>, decrypt the included list
* of introduction points with <b>descriptor_cookie</b> (which may also be
* <b>NULL</b> if decryption is not necessary), and store the descriptor to
* the local cache under its version and service id.
*
* If we have a newer v2 descriptor with the same ID, ignore this one.
* If we have an older descriptor with the same ID, replace it.
* If the descriptor's service ID does not match
* <b>rend_query</b>-\>onion_address, reject it.
*
* If the descriptor's descriptor ID doesn't match <b>desc_id_base32</b>,
* reject it.
*
* Return an appropriate rend_cache_store_status_t. If entry is not NULL,
* set it with the cache entry pointer of the descriptor.
*/
rend_cache_store_status_t
rend_cache_store_v2_desc_as_client(const char *desc,
const char *desc_id_base32,
const rend_data_t *rend_query,
rend_cache_entry_t **entry)
{
/*XXXX this seems to have a bit of duplicate code with
* rend_cache_store_v2_desc_as_dir(). Fix that. */
/* Though having similar elements, both functions were separated on
* purpose:
* - dirs don't care about encoded/encrypted introduction points, clients
* do.
* - dirs store descriptors in a separate cache by descriptor ID, whereas
* clients store them by service ID; both caches are different data
* structures and have different access methods.
* - dirs store a descriptor only if they are responsible for its ID,
* clients do so in every way (because they have requested it before).
* - dirs can process multiple concatenated descriptors which is required
* for replication, whereas clients only accept a single descriptor.
* Thus, combining both methods would result in a lot of if statements
* which probably would not improve, but worsen code readability. -KL */
rend_service_descriptor_t *parsed = NULL;
char desc_id[DIGEST_LEN];
char *intro_content = NULL;
size_t intro_size;
size_t encoded_size;
const char *next_desc;
time_t now = time(NULL);
char key[REND_SERVICE_ID_LEN_BASE32+2];
char service_id[REND_SERVICE_ID_LEN_BASE32+1];
char want_desc_id[DIGEST_LEN];
rend_cache_entry_t *e;
rend_cache_store_status_t retval = RCS_BADDESC;
tor_assert(rend_cache);
tor_assert(desc);
tor_assert(desc_id_base32);
memset(want_desc_id, 0, sizeof(want_desc_id));
if (entry) {
*entry = NULL;
}
if (base32_decode(want_desc_id, sizeof(want_desc_id),
desc_id_base32, strlen(desc_id_base32)) != 0) {
log_warn(LD_BUG, "Couldn't decode base32 %s for descriptor id.",
escaped_safe_str_client(desc_id_base32));
goto err;
}
/* Parse the descriptor. */
if (rend_parse_v2_service_descriptor(&parsed, desc_id, &intro_content,
&intro_size, &encoded_size,
&next_desc, desc, 0) < 0) {
log_warn(LD_REND, "Could not parse descriptor.");
goto err;
}
/* Compute service ID from public key. */
if (rend_get_service_id(parsed->pk, service_id)<0) {
log_warn(LD_REND, "Couldn't compute service ID.");
goto err;
}
if (rend_query->onion_address[0] != '\0' &&
strcmp(rend_query->onion_address, service_id)) {
log_warn(LD_REND, "Received service descriptor for service ID %s; "
"expected descriptor for service ID %s.",
service_id, safe_str(rend_query->onion_address));
goto err;
}
if (tor_memneq(desc_id, want_desc_id, DIGEST_LEN)) {
log_warn(LD_REND, "Received service descriptor for %s with incorrect "
"descriptor ID.", service_id);
goto err;
}
/* Decode/decrypt introduction points. */
if (intro_content && intro_size > 0) {
int n_intro_points;
if (rend_query->auth_type != REND_NO_AUTH &&
!tor_mem_is_zero(rend_query->descriptor_cookie,
sizeof(rend_query->descriptor_cookie))) {
char *ipos_decrypted = NULL;
size_t ipos_decrypted_size;
if (rend_decrypt_introduction_points(&ipos_decrypted,
&ipos_decrypted_size,
rend_query->descriptor_cookie,
intro_content,
intro_size) < 0) {
log_warn(LD_REND, "Failed to decrypt introduction points. We are "
"probably unable to parse the encoded introduction points.");
} else {
/* Replace encrypted with decrypted introduction points. */
log_info(LD_REND, "Successfully decrypted introduction points.");
tor_free(intro_content);
intro_content = ipos_decrypted;
intro_size = ipos_decrypted_size;
}
}
n_intro_points = rend_parse_introduction_points(parsed, intro_content,
intro_size);
if (n_intro_points <= 0) {
log_warn(LD_REND, "Failed to parse introduction points. Either the "
"service has published a corrupt descriptor or you have "
"provided invalid authorization data.");
goto err;
} else if (n_intro_points > MAX_INTRO_POINTS) {
log_warn(LD_REND, "Found too many introduction points on a hidden "
"service descriptor for %s. This is probably a (misguided) "
"attempt to improve reliability, but it could also be an "
"attempt to do a guard enumeration attack. Rejecting.",
safe_str_client(service_id));
goto err;
}
} else {
log_info(LD_REND, "Descriptor does not contain any introduction points.");
parsed->intro_nodes = smartlist_new();
}
/* We don't need the encoded/encrypted introduction points any longer. */
tor_free(intro_content);
/* Is descriptor too old? */
if (parsed->timestamp < now - REND_CACHE_MAX_AGE-REND_CACHE_MAX_SKEW) {
log_warn(LD_REND, "Service descriptor with service ID %s is too old.",
safe_str_client(service_id));
goto err;
}
/* Is descriptor too far in the future? */
if (parsed->timestamp > now + REND_CACHE_MAX_SKEW) {
log_warn(LD_REND, "Service descriptor with service ID %s is too far in "
"the future.", safe_str_client(service_id));
goto err;
}
/* Do we have the same exact copy already in our cache? */
tor_snprintf(key, sizeof(key), "2%s", service_id);
e = (rend_cache_entry_t*) strmap_get_lc(rend_cache, key);
if (e && !strcmp(desc, e->desc)) {
log_info(LD_REND,"We already have this service descriptor %s.",
safe_str_client(service_id));
goto okay;
}
/* Verify that we are not replacing an older descriptor. It's important to
* avoid an evil HSDir serving old descriptor. We validate if the
* timestamp is greater than and not equal because it's a rounded down
* timestamp to the hour so if the descriptor changed in the same hour,
* the rend cache failure will tells us if we have a new descriptor. */
if (e && e->parsed->timestamp > parsed->timestamp) {
log_info(LD_REND, "We already have a new enough service descriptor for "
"service ID %s with the same desc ID and version.",
safe_str_client(service_id));
goto okay;
}
/* Lookup our failure cache for intro point that might be unsuable. */
validate_intro_point_failure(parsed, service_id);
/* It's now possible that our intro point list is empty, this means that
* this descriptor is useless to us because intro points have all failed
* somehow before. Discard the descriptor. */
if (smartlist_len(parsed->intro_nodes) == 0) {
log_info(LD_REND, "Service descriptor with service ID %s, every "
"intro points are unusable. Discarding it.",
safe_str_client(service_id));
goto err;
}
/* Now either purge the current one and replace it's content or create a
* new one and add it to the rend cache. */
if (!e) {
e = tor_malloc_zero(sizeof(rend_cache_entry_t));
strmap_set_lc(rend_cache, key, e);
} else {
rend_cache_decrement_allocation(rend_cache_entry_allocation(e));
rend_cache_failure_remove(e->parsed);
rend_service_descriptor_free(e->parsed);
tor_free(e->desc);
}
e->parsed = parsed;
e->desc = tor_malloc_zero(encoded_size + 1);
strlcpy(e->desc, desc, encoded_size + 1);
e->len = encoded_size;
rend_cache_increment_allocation(rend_cache_entry_allocation(e));
log_debug(LD_REND,"Successfully stored rend desc '%s', len %d.",
safe_str_client(service_id), (int)encoded_size);
if (entry) {
*entry = e;
}
return RCS_OKAY;
okay:
if (entry) {
*entry = e;
}
retval = RCS_OKAY;
err:
rend_service_descriptor_free(parsed);
tor_free(intro_content);
return retval;
}