/* domaininfo.c - Gather statistics about accessed domains
* Copyright (C) 2017 Werner Koch
*
* This file is part of GnuPG.
*
* GnuPG is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* GnuPG is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see .
*
* SPDX-License-Identifier: GPL-3.0+
*/
#include
#include
#include
#include "dirmngr.h"
/* Number of bucket for the hash array and limit for the length of a
* bucket chain. For debugging values of 13 and 10 are more suitable
* and a command like
* for j in a b c d e f g h i j k l m n o p q r s t u v w z y z; do \
* for i in a b c d e f g h i j k l m n o p q r s t u v w z y z; do \
* gpg-connect-agent --dirmngr "wkd_get foo@$i.$j.gnupg.net" /bye \
* >/dev/null ; done; done
* will quickly add a couple of domains.
*/
#define NO_OF_DOMAINBUCKETS 103
#define MAX_DOMAINBUCKET_LEN 20
/* Object to keep track of a domain name. */
struct domaininfo_s
{
struct domaininfo_s *next;
unsigned int no_name:1; /* Domain name not found. */
unsigned int wkd_not_found:1; /* A WKD query failed. */
unsigned int wkd_supported:1; /* One WKD entry was found. */
unsigned int wkd_not_supported:1; /* Definitely does not support WKD. */
unsigned int keepmark:1; /* Private to insert_or_update(). */
char name[1];
};
typedef struct domaininfo_s *domaininfo_t;
/* And the hashed array. */
static domaininfo_t domainbuckets[NO_OF_DOMAINBUCKETS];
/* The hash function we use. Must not call a system function. */
static inline u32
hash_domain (const char *domain)
{
const unsigned char *s = (const unsigned char*)domain;
u32 hashval = 0;
u32 carry;
for (; *s; s++)
{
if (*s == '.')
continue;
hashval = (hashval << 4) + *s;
if ((carry = (hashval & 0xf0000000)))
{
hashval ^= (carry >> 24);
hashval ^= carry;
}
}
return hashval % NO_OF_DOMAINBUCKETS;
}
void
domaininfo_print_stats (ctrl_t ctrl)
{
int bidx;
domaininfo_t di;
int count, no_name, wkd_not_found, wkd_supported, wkd_not_supported;
int len, minlen, maxlen;
count = no_name = wkd_not_found = wkd_supported = wkd_not_supported = 0;
maxlen = 0;
minlen = -1;
for (bidx = 0; bidx < NO_OF_DOMAINBUCKETS; bidx++)
{
len = 0;
for (di = domainbuckets[bidx]; di; di = di->next)
{
count++;
len++;
if (di->no_name)
no_name++;
if (di->wkd_not_found)
wkd_not_found++;
if (di->wkd_supported)
wkd_supported++;
if (di->wkd_not_supported)
wkd_not_supported++;
}
if (len > maxlen)
maxlen = len;
if (minlen == -1 || len < minlen)
minlen = len;
}
dirmngr_status_helpf
(ctrl, "domaininfo: items=%d chainlen=%d..%d nn=%d nf=%d ns=%d s=%d\n",
count,
minlen > 0? minlen : 0,
maxlen,
no_name, wkd_not_found, wkd_not_supported, wkd_supported);
}
/* Return true if DOMAIN definitely does not support WKD. Note that
* DOMAIN is expected to be lowercase. */
int
domaininfo_is_wkd_not_supported (const char *domain)
{
domaininfo_t di;
for (di = domainbuckets[hash_domain (domain)]; di; di = di->next)
if (!strcmp (di->name, domain))
return !!di->wkd_not_supported;
return 0; /* We don't know. */
}
/* Core update function. DOMAIN is expected to be lowercase.
* CALLBACK is called to update the existing or the newly inserted
* item. */
static void
insert_or_update (const char *domain,
void (*callback)(domaininfo_t di, int insert_mode))
{
domaininfo_t di;
domaininfo_t di_new;
domaininfo_t drop = NULL;
domaininfo_t drop_extra = NULL;
int nkept = 0;
int ndropped = 0;
u32 hash;
int count;
hash = hash_domain (domain);
for (di = domainbuckets[hash]; di; di = di->next)
if (!strcmp (di->name, domain))
{
callback (di, 0); /* Update */
return;
}
di_new = xtrycalloc (1, sizeof *di + strlen (domain));
if (!di_new)
return; /* Out of core - we ignore this. */
strcpy (di_new->name, domain);
/* Need to do another lookup because the malloc is a system call and
* thus the hash array may have been changed by another thread. */
for (count=0, di = domainbuckets[hash]; di; di = di->next, count++)
if (!strcmp (di->name, domain))
{
callback (di, 0); /* Update */
xfree (di_new);
return;
}
/* Before we insert we need to check whether the chain gets too long. */
if (count >= MAX_DOMAINBUCKET_LEN)
{
domaininfo_t bucket;
domaininfo_t *array;
int narray, idx;
domaininfo_t keep = NULL;
/* Unlink from the global list before doing a syscall. */
bucket = domainbuckets[hash];
domainbuckets[hash] = NULL;
array = xtrycalloc (count, sizeof *array);
if (!array)
{
/* That's bad; give up the entire bucket. */
log_error ("domaininfo: error allocating helper array: %s\n",
gpg_strerror (gpg_err_code_from_syserror ()));
drop_extra = bucket;
goto leave;
}
narray = 0;
/* Move all items into an array for easier processing. */
for (di = bucket; di; di = di->next)
array[narray++] = di;
log_assert (narray == count);
/* Mark all item in the array which are flagged to support wkd
* but not more than half of the maximum. This way we will at
* the end drop half of the items. */
count = 0;
for (idx=0; idx < narray; idx++)
{
di = array[idx];
di->keepmark = 0; /* Clear flag here on the first pass. */
if (di->wkd_supported && count < MAX_DOMAINBUCKET_LEN/2)
{
di->keepmark = 1;
count++;
}
}
/* Now mark those which are marked as not found. */
/* FIXME: we should use an LRU algorithm here. */
for (idx=0; idx < narray; idx++)
{
di = array[idx];
if (!di->keepmark
&& di->wkd_not_supported && count < MAX_DOMAINBUCKET_LEN/2)
{
di->keepmark = 1;
count++;
}
}
/* Build a bucket list and a second list for later freeing the
* items (we can't do it directly because a free is a system
* call and we want to avoid locks in this module. Note that
* the kept items will be reversed order which does not matter. */
for (idx=0; idx < narray; idx++)
{
di = array[idx];
if (di->keepmark)
{
di->next = keep;
keep = di;
nkept++;
}
else
{
di->next = drop;
drop = di;
ndropped++;
}
}
/* In case another thread added new stuff to the domain list we
* simply drop them instead all. It would also be possible to
* append them to our list but then we can't guarantee that a
* bucket list is almost all of the time limited to
* MAX_DOMAINBUCKET_LEN. Not sure whether this is really a
* sensible strategy. */
drop_extra = domainbuckets[hash];
domainbuckets[hash] = keep;
}
/* Insert */
callback (di_new, 1);
di = di_new;
di->next = domainbuckets[hash];
domainbuckets[hash] = di;
if (opt.verbose && (nkept || ndropped))
log_info ("domaininfo: bucket=%lu kept=%d purged=%d\n",
(unsigned long)hash, nkept, ndropped);
leave:
/* Remove the dropped items. */
while (drop)
{
di = drop->next;
xfree (drop);
drop = di;
}
while (drop_extra)
{
di = drop_extra->next;
xfree (drop_extra);
drop_extra = di;
}
}
/* Helper for domaininfo_set_no_name. May not do any syscalls. */
static void
set_no_name_cb (domaininfo_t di, int insert_mode)
{
(void)insert_mode;
di->no_name = 1;
/* Obviously the domain is in this case also not supported. */
di->wkd_not_supported = 1;
/* The next should already be 0 but we clear it anyway in the case
* of a temporary DNS failure. */
di->wkd_supported = 0;
}
/* Mark DOMAIN as not existent. */
void
domaininfo_set_no_name (const char *domain)
{
insert_or_update (domain, set_no_name_cb);
}
/* Helper for domaininfo_set_wkd_supported. May not do any syscalls. */
static void
set_wkd_supported_cb (domaininfo_t di, int insert_mode)
{
(void)insert_mode;
di->wkd_supported = 1;
/* The next will already be set unless the domain enabled WKD in the
* meantime. Thus we need to clear it. */
di->wkd_not_supported = 0;
}
/* Mark DOMAIN as supporting WKD. */
void
domaininfo_set_wkd_supported (const char *domain)
{
insert_or_update (domain, set_wkd_supported_cb);
}
/* Helper for domaininfo_set_wkd_not_supported. May not do any syscalls. */
static void
set_wkd_not_supported_cb (domaininfo_t di, int insert_mode)
{
(void)insert_mode;
di->wkd_not_supported = 1;
di->wkd_supported = 0;
}
/* Mark DOMAIN as not supporting WKD queries (e.g. no policy file). */
void
domaininfo_set_wkd_not_supported (const char *domain)
{
insert_or_update (domain, set_wkd_not_supported_cb);
}
/* Helper for domaininfo_set_wkd_not_found. May not do any syscalls. */
static void
set_wkd_not_found_cb (domaininfo_t di, int insert_mode)
{
/* Set the not found flag but there is no need to do this if we
* already know that the domain either does not support WKD or we
* know that it supports WKD. */
if (insert_mode)
di->wkd_not_found = 1;
else if (!di->wkd_not_supported && !di->wkd_supported)
di->wkd_not_found = 1;
/* Better clear this flag in case we had a DNS failure in the
* past. */
di->no_name = 0;
}
/* Update a counter for DOMAIN to keep track of failed WKD queries. */
void
domaininfo_set_wkd_not_found (const char *domain)
{
insert_or_update (domain, set_wkd_not_found_cb);
}