#define G_LOG_DOMAIN "Gsk-Dns"
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include "gskdnsclient.h"
#include "gskdnsresolver.h"
#include "../gskmainloop.h"
#include "../gskmacros.h"
#include "../gskghelpers.h"
#include "../gsknameresolver.h"
#include "../gskdebug.h"
/* Default DNS cache sizes.
In your program, you can call
gsk_name_resolver_set_dns_cache_size()
to change these. */
#define GSK_DNS_MAX_CACHE_BYTES (128 * 1024)
#define GSK_DNS_MAX_CACHE_RECORDS (2048)
typedef struct _GskDnsNameServerInfo GskDnsNameServerInfo;
typedef struct _ClientTask ClientTask;
typedef struct _IpPermissionTable IpPermissionTable;
#define DEBUG_IP_PERMISSION_TABLE 0
#if 0 /* is debugging on? */
#define DEBUG g_message
#define DEBUG_PRINT_MESSAGE(mess) gsk_dns_dump_message_fp(mess, stderr)
#else
#define DEBUG(args...)
#define DEBUG_PRINT_MESSAGE(mess)
#endif
struct _GskDnsClientClass
{
GskPacketQueueClass base_class;
};
struct _GskDnsNameServerInfo
{
GskSocketAddressIpv4 *address;
guint is_default_ns : 1;
guint num_msg_sent;
guint num_msg_received;
GskDnsNameServerInfo *next_ns;
GskDnsNameServerInfo *prev_ns;
};
struct _GskDnsClient
{
GObject base_instance;
GskPacketQueue *packet_queue;
GskDnsRRCache *rr_cache;
ClientTask *tasks;
GHashTable *id_to_task_list;
guint16 last_message_id;
guint16 stub_resolver : 1;
guint16 is_blocking_write : 1;
guint16 recursion_desired : 1;
guint16 max_iterations_nonrecursive;
guint16 max_iterations_recursive;
GSList *first_outgoing_packet;
GSList *last_outgoing_packet;
GskMainLoop *main_loop;
/* temporary permissions granted to other nameservers */
IpPermissionTable *ip_perm_table;
/* --- basic configuration --- */
GskDnsNameServerInfo *first_ns;
GskDnsNameServerInfo *last_ns;
GPtrArray *searchpath_array;
};
struct _ClientTask
{
GskDnsClient *client;
guint16 message_id;
guint16 ref_count;
guint is_in_client_list : 1;
guint destroyed : 1;
guint failed : 1; /* => we ran on_fail */
guint ran_task_func : 1;
guint cancelled : 1;
/* See RFC, 5.3.1. */
guint stub_resolver : 1;
/* Whether we've resorted to trying the configuration
nameservers yet. */
guint used_conf_nameservers : 1;
/* Whether to use other nameservers in obtaining an answer. */
guint recursive : 1;
guint16 n_iterations;
guint16 max_iterations;
/* GskDnsRRCache to be used for this task.
* This will never be NULL unless stub_resolver is specified,
* although GskDnsRRCache may be specific to this query.
*/
GskDnsRRCache *rr_cache;
/* a collection of resource records we've locked from
* the cache that might be useful.
*/
GSList *locked_records;
/* The name servers which we know apply to this zone. */
GskDnsNameServerInfo *first_ns;
GskDnsNameServerInfo *last_ns;
/* List of questions which need answering */
GSList *questions;
/* List of questions which have answers in `{ns,a,cname}_records */
GSList *answered_questions;
/* List of questions which have had explicit negative answers
(also in answered_questions) */
GSList *negatives;
GskDnsResolverResponseFunc func;
GskDnsResolverFailFunc on_fail;
gpointer func_data;
GDestroyNotify destroy;
/* Timeout for the query which will timeout first. */
GskSource *timeout_source;
/* list of all queries pending. */
ClientTask *next;
ClientTask *prev;
/* list of all queries in this hash-bucket. */
ClientTask *hash_next;
ClientTask *hash_prev;
};
GSK_DECLARE_POOL_ALLOCATORS(GskDnsNameServerInfo, gsk_dns_name_server_info, 16)
/* Attempt to Resolve the given questions -- eventually we unref
* the task, possibly before returning from this function.
*/
static void try_local_cache_or_proceed (ClientTask *task);
/* Handle a timeout expiration -- just update the tasks state,
* possibly ending the task.
*/
static gboolean handle_timeout (ClientTask *task);
static void gsk_dns_client_transmit (GskDnsClient *client,
GskSocketAddressIpv4 *address,
GskDnsMessage *message);
/* Fail a single client and remove it from the client list. */
static void client_task_fail (ClientTask *task,
GError *error);
/* --- ip-permission table prototypes --- */
static IpPermissionTable * ip_permission_table_new ();
static void ip_permission_table_insert (IpPermissionTable *table,
GskSocketAddressIpv4 *address,
gboolean any_suffixed_domain,
const char *owner,
guint expire_time);
static void ip_permission_table_expire (IpPermissionTable *table,
guint cur_time);
static gboolean ip_permission_table_check(IpPermissionTable *table,
GskSocketAddressIpv4 *address,
const char *owner,
guint cur_time);
static void ip_permission_table_destroy (IpPermissionTable *table);
/* --- prototypes --- */
static GObjectClass *parent_class = NULL;
/* helper ClientTask methods */
static inline void
maybe_run_destroy (ClientTask *task)
{
if (!task->destroyed)
{
task->destroyed = 1;
if (task->destroy != NULL)
(*task->destroy) (task->func_data);
}
}
static inline void
remove_from_client_list (ClientTask *task)
{
g_return_if_fail (task->is_in_client_list);
task->is_in_client_list = 0;
if (task->next != NULL)
task->next->prev = task->prev;
if (task->prev != NULL)
task->prev->next = task->next;
else
task->client->tasks = task->next;
}
static inline void
maybe_remove_from_client_list (ClientTask *task)
{
if (task->is_in_client_list)
remove_from_client_list (task);
}
static inline void
maybe_remove_timeout_source (ClientTask *task)
{
if (task->timeout_source != NULL)
{
gsk_source_remove (task->timeout_source);
task->timeout_source = NULL;
}
}
/* --- GskDnsResolver interface --- */
static void
gsk_dns_client_task_ref (ClientTask *task)
{
g_return_if_fail (task->ref_count > 0);
++(task->ref_count);
}
static void
gsk_dns_client_task_unref (ClientTask *task)
{
g_return_if_fail (task->ref_count > 0);
--(task->ref_count);
if (task->ref_count == 0)
{
/* We should always either: succeed, fail, or be cancelled. */
g_return_if_fail (task->cancelled || task->failed || task->ran_task_func);
maybe_run_destroy (task);
maybe_remove_from_client_list (task);
/* Remove from hash-table list. */
if (task->hash_next != NULL)
task->hash_next->hash_prev = task->hash_prev;
if (task->hash_prev != NULL)
task->hash_prev->hash_next = task->hash_next;
else
{
guint msg_id = task->message_id;
g_hash_table_insert (task->client->id_to_task_list,
GUINT_TO_POINTER (msg_id),
task->hash_next);
}
/* remove timeout */
maybe_remove_timeout_source (task);
g_slist_foreach (task->answered_questions,
(GFunc) gsk_dns_question_free,
NULL);
g_slist_free (task->answered_questions);
g_slist_foreach (task->questions,
(GFunc) gsk_dns_question_free,
NULL);
g_slist_free (task->questions);
g_slist_free (task->negatives);
while (task->first_ns != NULL)
{
GskDnsNameServerInfo *ns_info = task->first_ns;
task->first_ns = ns_info->next_ns;
g_object_unref (ns_info->address);
gsk_dns_name_server_info_free (ns_info);
}
while (task->locked_records != NULL)
{
GskDnsResourceRecord *rr = task->locked_records->data;
task->locked_records = g_slist_remove (task->locked_records, rr);
gsk_dns_rr_cache_unlock (task->rr_cache, rr);
}
if (task->rr_cache != NULL)
gsk_dns_rr_cache_unref (task->rr_cache);
g_free (task);
}
}
static void
gsk_dns_client_resolver_cancel (GskDnsResolver *resolver,
gpointer task_data)
{
ClientTask *task = task_data;
GskDnsClient *client = GSK_DNS_CLIENT (resolver);
g_assert (client == task->client);
task->cancelled = 1;
gsk_dns_client_task_unref (task);
}
/* See RFC 1034, section 5.3.3. */
static int
count_dots (const char *str)
{
int rv = 0;
while (*str != 0)
{
if (*str == '.')
rv++;
str++;
}
return rv;
}
static void
find_name_pieces (const char *str,
guint *n_pieces_out,
const char **pieces_out)
{
const char *at = str;
*n_pieces_out = 0;
while (*at != 0)
{
pieces_out[*n_pieces_out] = at;
(*n_pieces_out)++;
while (*at != '\0' && *at != '.')
at++;
while (*at == '.')
at++;
}
pieces_out[*n_pieces_out] = ".";
(*n_pieces_out)++;
}
static inline guint
gsk_dns_client_generate_message_id (GskDnsClient *client)
{
return ++(client->last_message_id);
}
static GskDnsMessage *
maybe_make_message (ClientTask *task,
GHashTable *table,
GskDnsNameServerInfo *ns_info)
{
GskDnsMessage *message = g_hash_table_lookup (table, ns_info);
if (message == NULL)
{
guint16 msg_id = task->message_id;
message = gsk_dns_message_new (msg_id, TRUE);
message->recursion_desired = task->client->recursion_desired;
g_hash_table_insert (table, ns_info, message);
}
return message;
}
static void
add_timeout (ClientTask *task,
guint num_seconds)
{
task->timeout_source
= gsk_main_loop_add_timer (task->client->main_loop,
(GskMainLoopTimeoutFunc) handle_timeout,
task,
NULL,
num_seconds * 1000 + 500,
-1);
}
/* Restart any queries which have timed out,
* unless they have timed out too many times,
* in which case, we fail.
*/
static gboolean
handle_timeout (ClientTask *task)
{
task->timeout_source = NULL;
try_local_cache_or_proceed (task);
return FALSE;
}
/* hmm, this seems pretty inefficient */
static void
gsk_dns_client_task_use_conf_nameservers (ClientTask *task)
{
GskDnsNameServerInfo *ns_info;
g_return_if_fail (!task->used_conf_nameservers);
task->used_conf_nameservers = 1;
for (ns_info = task->client->first_ns;
ns_info != NULL;
ns_info = ns_info->next_ns)
{
GskDnsNameServerInfo *new_ns_info = gsk_dns_name_server_info_alloc ();
new_ns_info->num_msg_received = 0;
new_ns_info->num_msg_sent = 0;
new_ns_info->address = g_object_ref (ns_info->address);
new_ns_info->is_default_ns = 1;
new_ns_info->prev_ns = task->last_ns;
new_ns_info->next_ns = NULL;
if (task->last_ns != NULL)
task->last_ns->next_ns = new_ns_info;
else
task->first_ns = new_ns_info;
task->last_ns = new_ns_info;
}
}
/* --- translate uncategorized ResourceRecords
into the usual lists, according to the questions
in `task': answers, authority, additional. --- */
typedef struct _MessageLists MessageLists;
struct _MessageLists
{
GSList *answers;
GSList *auth;
GSList *additional;
ClientTask *task;
};
/* Append a GskDnsResourceRecord to whichever return list
* looks best (using additional as a catchall.)
*/
static void
categorize_rr (GskDnsResourceRecord *record,
MessageLists *lists)
{
ClientTask *task = lists->task;
GSList *answered;
/* Look through the questions to figure if
this is a real answer (answers), an indirectly related
nameserver (auth), or another datum (additional). */
for (answered = task->answered_questions;
answered != NULL;
answered = answered->next)
{
GskDnsQuestion *question = answered->data;
if (strcasecmp (question->query_name, record->owner) == 0
&& (record->type == question->query_type
|| record->type == GSK_DNS_RR_CANONICAL_NAME
|| question->query_type == GSK_DNS_RR_WILDCARD))
break;
}
if (answered != NULL)
lists->answers = g_slist_prepend (lists->answers, record);
else if (record->type == GSK_DNS_RR_NAME_SERVER)
lists->auth = g_slist_prepend (lists->auth, record);
else
lists->additional = g_slist_prepend (lists->additional, record);
}
/* XXX: move this comment to dnsinterfaces.h
*
* Call this function once you've gotten all the
* ResourceRecords you're going to get.
*
* Note that the function may sometimes
* be run without any of the questions answered,
* just partial data. The `result_func' passed
* to gsk_dns_resolver_resolve must discern
* these cases.
*/
static void
gsk_dns_client_task_succeed (ClientTask *task)
{
MessageLists message_lists = {NULL, NULL, NULL, NULL};
message_lists.task = task;
g_return_if_fail (!task->ran_task_func);
maybe_remove_timeout_source (task);
g_slist_foreach (task->locked_records,
(GFunc) categorize_rr,
&message_lists);
task->ran_task_func = 1;
(*task->func) (message_lists.answers,
message_lists.auth,
message_lists.additional,
task->negatives,
task->func_data);
g_slist_free (message_lists.answers);
g_slist_free (message_lists.auth);
g_slist_free (message_lists.additional);
}
static void
gsk_dns_client_task_fail (ClientTask *task,
GError *error)
{
g_return_if_fail (!task->failed && !task->ran_task_func && !task->destroyed);
task->failed = 1;
maybe_remove_timeout_source (task);
if (task->on_fail != NULL)
(*task->on_fail) (error, task->func_data);
g_error_free (error);
}
static void
move_ns_to_end_of_list (ClientTask *task,
GskDnsNameServerInfo *name_server)
{
/* If we are the last element, nothing to do. */
if (name_server->next_ns == NULL)
return;
/* Excise. */
if (name_server->prev_ns != NULL)
name_server->prev_ns->next_ns = name_server->next_ns;
else
task->first_ns = name_server->next_ns;
name_server->next_ns->prev_ns = name_server->prev_ns;
/* If this were true, then name_server would have been
* the last element originally, and we'd have short-circuited out.
*/
g_assert (task->first_ns != NULL);
/* Reinsert. */
name_server->prev_ns = task->last_ns;
name_server->next_ns = NULL;
task->last_ns->next_ns = name_server;
task->last_ns = name_server;
}
/* Get the number of seconds to sleep based on the
* which packet this is in the task's requests to that NS (0,1,2,etc)
*/
static inline guint
get_expire_from_attempt_index (GskDnsNameServerInfo *ns_info)
{
return (1 << MIN (ns_info->num_msg_sent, 6)) + 3;
}
#define MAX_TIMEOUT 90
typedef struct _DnsQueryData DnsQueryData;
struct _DnsQueryData
{
gboolean has_expire_time;
guint next_expire_dist;
ClientTask *task;
};
static void
do_dns_query (GskDnsNameServerInfo *name_server,
GskDnsMessage *message,
DnsQueryData *query_data)
{
ClientTask *task = query_data->task;
if (task->failed)
return;
/* Handle default name servers. */
if (name_server == NULL)
{
if (!task->used_conf_nameservers)
gsk_dns_client_task_use_conf_nameservers (task);
for (name_server = task->first_ns;
name_server != NULL;
name_server = name_server->next_ns)
{
if (name_server->is_default_ns)
break;
}
if (name_server == NULL)
{
if (!task->failed)
gsk_dns_client_task_fail (task,
g_error_new (GSK_G_ERROR_DOMAIN,
GSK_ERROR_RESOLVER_NO_NAME_SERVERS,
_("resolving name: no default name server")));
return;
}
move_ns_to_end_of_list (task, name_server);
}
{
guint expire_dist = get_expire_from_attempt_index (name_server);
if (!query_data->has_expire_time
|| expire_dist < query_data->next_expire_dist)
query_data->next_expire_dist = expire_dist;
query_data->has_expire_time = TRUE;
}
/* query `name_server' */
name_server->num_msg_sent++;
gsk_dns_client_transmit (task->client,
name_server->address,
message);
/* XXX: what the fuck is this supposed to do? */
#if 0
if (!was_default)
{
GSList *question_list;
gulong cur_time;
GskMainLoop *main_loop = task->client->main_loop;
cur_time = main_loop->current_time.tv_sec;
for (question_list = message->questions;
question_list != NULL;
question_list = question_list->next)
{
GskSocketAddress addr;
addr.address_family = GSK_SOCKET_ADDRESS_IPv4;
addr.ipv4.ip_port = GSK_DNS_PORT;
memcpy (addr.ipv4.ip_address, name_server->ip_address, 4);
}
}
#endif
gsk_dns_message_unref (message);
}
/* Return a (possibly newly allocated) Nameserver
* based on a socket-address.
*/
static GskDnsNameServerInfo *
get_nameserver (ClientTask *task,
GskSocketAddressIpv4 *address)
{
GskDnsNameServerInfo *rv;
const guint8 *ip;
ip = address->ip_address;
for (rv = task->first_ns; rv != NULL; rv = rv->next_ns)
if (gsk_socket_address_equals (address, rv->address))
return rv;
rv = gsk_dns_name_server_info_alloc ();
rv->num_msg_sent = rv->num_msg_received = 0;
rv->address = g_object_ref (address);
rv->next_ns = task->first_ns;
rv->prev_ns = NULL;
rv->is_default_ns = 0;
if (task->first_ns != NULL)
task->first_ns->prev_ns = rv;
else
task->last_ns = rv;
task->first_ns = rv;
return rv;
}
static void
try_local_cache_or_proceed (ClientTask *task)
{
/* An map from a GskDnsNameServerInfo for this request
* to a dns-message; if GskDnsNameServerInfo==NULL,
* the request should be sent to the nameservers
* from the config file.
*/
GHashTable *ns_to_dns_message = NULL;
GSList *questions = task->questions;
GskDnsRRCache *rr_cache = task->rr_cache;
GSList *last = NULL;
GskMainLoop *main_loop = task->client->main_loop;
gulong cur_time;
if (main_loop == NULL)
{
GTimeVal tv;
g_get_current_time (&tv);
cur_time = tv.tv_sec;
}
else
cur_time = main_loop->current_time.tv_sec;
g_assert (!task->failed);
/* If the query requires a cache for interim results,
* make a minimal cache for just this query.
*/
if (rr_cache == NULL && !task->stub_resolver)
rr_cache = task->rr_cache = gsk_dns_rr_cache_new (0, 0);
while (questions != NULL)
{
GskDnsResourceRecord *record;
GSList *results;
GskDnsQuestion *question = questions->data;
const char *name = question->query_name;
GskDnsResourceClass query_class = question->query_class;
GskDnsResourceRecordType query_type = question->query_type;
GSList *cnames = NULL;
guint n_dots;
guint n_pieces;
/* Hopefully this is safe--RFC 1034, sec. 3.1: "To simplify
* implementations, the total number of octets that represent a
* domain name (i.e., the sum of all label octets and label lengths)
* is limited to 255."
*/
const char *pieces[255];
guint found_ns_index;
GskSocketAddressIpv4 *address;
restart_with_local_cache:
results = NULL;
record = NULL;
if (rr_cache != NULL)
{
if (query_type == GSK_DNS_RR_WILDCARD)
results = gsk_dns_rr_cache_lookup_list (rr_cache,
name,
query_type,
query_class);
else
record = gsk_dns_rr_cache_lookup_one (rr_cache,
name,
query_type,
query_class,
0);
if (results != NULL || record != NULL)
{
/* Move question to the answered_questions pile,
* and add the necessary data to the answers list.
*/
if (last == NULL)
task->questions = questions->next;
else
last->next = questions->next;
questions->next = task->answered_questions;
task->answered_questions = questions;
questions = last ? last->next : task->questions;
if (results != NULL)
{
GSList *at;
for (at = results; at != NULL; at = at->next)
gsk_dns_rr_cache_lock (rr_cache, at->data);
task->locked_records = g_slist_concat (task->locked_records,
results);
}
if (record != NULL)
{
gsk_dns_rr_cache_lock (rr_cache, record);
task->locked_records = g_slist_prepend (task->locked_records,
record);
}
g_slist_free (cnames);
continue;
}
if (gsk_dns_rr_cache_is_negative (rr_cache,
name,
query_type,
query_class))
{
/* Move question to the answered_questions pile,
* and add the necessary data to the answers list.
*/
if (last == NULL)
task->questions = questions->next;
else
last->next = questions->next;
questions->next = task->answered_questions;
task->answered_questions = questions;
questions = last ? last->next : task->questions;
task->negatives = g_slist_prepend (task->negatives, question);
g_slist_free (cnames);
continue;
}
/* ok, what if we can get a CNAME response? */
if (query_type != GSK_DNS_RR_CANONICAL_NAME
&& query_type != GSK_DNS_RR_WILDCARD)
{
record = gsk_dns_rr_cache_lookup_one (rr_cache,
name,
GSK_DNS_RR_CANONICAL_NAME,
query_class,
0);
if (record != NULL)
{
if (g_ascii_strcasecmp (record->rdata.domain_name, name) == 0
|| g_slist_find_custom (cnames, name, (GCompareFunc) g_ascii_strcasecmp) != NULL)
{
gsk_dns_client_task_fail (task,
g_error_new (GSK_G_ERROR_DOMAIN,
GSK_ERROR_RESOLVER_NO_NAME_SERVERS,
_("circular reference in CNAMEs for %s"), name));
g_slist_free (cnames);
gsk_dns_client_task_unref (task);
return;
}
/* start the query over again, with the CNAME,
* adding record back to the response list.
*/
gsk_dns_rr_cache_lock (rr_cache, record);
task->locked_records = g_slist_prepend (task->locked_records,
record);
cnames = g_slist_prepend (cnames, (gpointer) name);
name = record->rdata.domain_name;
goto restart_with_local_cache;
}
}
}
if (task->n_iterations >= task->max_iterations)
{
gsk_dns_client_task_fail (task,
g_error_new (GSK_G_ERROR_DOMAIN,
GSK_ERROR_RESOLVER_NO_NAME_SERVERS,
_("task timed out after %u retries"), task->n_iterations));
g_slist_free (cnames);
gsk_dns_client_task_unref (task);
return;
}
/* Ok, we're going to have to make a remote query.
Figure out who to ask. */
/* non-recursive nameservers always know what nameserver to ask already. */
if (!task->recursive)
{
g_slist_free (cnames);
continue;
}
/* Initialize the table to nameservers to query. */
if (ns_to_dns_message == NULL)
ns_to_dns_message = g_hash_table_new (NULL, NULL);
/* Implement a stub resolver. See RFC 1034, 5.3.3. */
if (task->stub_resolver)
{
GskDnsMessage *message;
GskDnsQuestion *q;
message = maybe_make_message (task, ns_to_dns_message, NULL);
message->recursion_desired = 1;
q = gsk_dns_question_copy (question, message);
gsk_dns_message_append_question (message, q);
last = questions;
questions = questions->next;
g_slist_free (cnames);
continue;
}
/*
* Find nameservers to ask for this information.
*
* If a nameserver is not contactable (we don't have an address for it)
* begin queries to find those addresses.
*
* If we are unable to find any address at all,
* to asking the default name servers.
*
* We will ask for all the intervening nameservers,
* and the actual result we want.
*/
// XXX: maybe we should iterate through cnames...
n_dots = count_dots (name);
find_name_pieces (name, &n_pieces, pieces);
for (found_ns_index = 0; found_ns_index < n_pieces; found_ns_index++)
{
const char *ns_name;
/* Try and get a NS record for `pieces[found_ns_index]' */
DEBUG ("calling gsk_dns_rr_cache_get_ns_addr(..,%s,..)", pieces[found_ns_index]);
if (gsk_dns_rr_cache_get_ns_addr (rr_cache,
pieces[found_ns_index],
&ns_name,
&address))
{
/* XXX: max timeout is clearly too long. */
ip_permission_table_insert (task->client->ip_perm_table,
address,
TRUE,
ns_name,
cur_time + MAX_TIMEOUT);
break;
}
/* Look for a nameserver of one domain up
* (next iteration of this loop)
*/
}
{
GskDnsMessage *message;
GskDnsQuestion *q;
GskDnsNameServerInfo *ns;
char *tmp_name;
if (found_ns_index == n_pieces)
ns = NULL;
else
{
ns = get_nameserver (task, address);
g_object_unref (address);
}
message = maybe_make_message (task, ns_to_dns_message, ns);
#if 0 /* this isn't the way the examples in 1034, 6.3.1 operate. */
/* Ask about all the intervening nameservers. */
for (i = 0; i < found_ns_index; i++)
{
/* Request the nameserver for pieces[i]. */
/* XXX: ensure that the questions here are unique:
* it's lame to request the same things repeatedly!!!
*/
q = gsk_dns_question_new (pieces[i],
GSK_DNS_RR_NAME_SERVER,
query_class,
message);
gsk_dns_message_append_question (message, q);
}
#endif
/* Ask about the real information. */
tmp_name = question->query_name;
question->query_name = (char *) name;
q = gsk_dns_question_copy (question, message);
question->query_name = tmp_name;
gsk_dns_message_append_question (message, q);
}
last = questions;
questions = questions->next;
g_slist_free (cnames);
}
g_assert (!task->failed);
if (ns_to_dns_message == NULL)
{
/* Ok, all the questions were resolved through the local cache. */
/* XXX: might there be an error to flag, if nonrecursive. */
gsk_dns_client_task_succeed (task);
gsk_dns_client_task_unref (task);
return;
}
/* Otherwise, make the necessary queries. */
{
DnsQueryData query_data = { FALSE, 0, task };
g_hash_table_foreach (ns_to_dns_message,
(GHFunc) do_dns_query,
&query_data);
g_hash_table_destroy (ns_to_dns_message);
if (task->failed)
{
gsk_dns_client_task_unref (task);
return;
}
task->n_iterations++;
/* Create a timeout source. */
/* XXX: what about requests that were pending on entry to this function?
I think we should *always* set a timeout here... */
if (query_data.has_expire_time && task->timeout_source == NULL)
add_timeout (task, query_data.next_expire_dist);
}
}
static gpointer
gsk_dns_client_resolve (GskDnsResolver *resolver,
gboolean recursive,
GSList *questions,
GskDnsResolverResponseFunc func,
GskDnsResolverFailFunc on_fail,
gpointer func_data,
GDestroyNotify destroy,
GskDnsResolverHints *hints)
{
GskDnsClient *client = GSK_DNS_CLIENT (resolver);
ClientTask *task;
ClientTask *rv;
(void) hints;
task = g_new (ClientTask, 1);
task->client = client;
task->message_id = gsk_dns_client_generate_message_id (client);
/* Insert task into `id_to_task_list' */
{
ClientTask *hash_head;
guint message_id = task->message_id;
gpointer msg_id = GUINT_TO_POINTER (message_id);
hash_head = g_hash_table_lookup (client->id_to_task_list, msg_id);
task->hash_next = hash_head;
if (hash_head != NULL)
hash_head->hash_prev = task;
task->hash_prev = NULL;
g_hash_table_insert (client->id_to_task_list, msg_id, task);
}
/* one ref-count is maintained by the DnsClient,
* the other will be undone at the end of this function
* (to avoid having the task deleted from underneath us)
*/
task->ref_count = 2;
task->n_iterations = 0;
task->max_iterations = recursive ? client->max_iterations_recursive : client->max_iterations_nonrecursive;
task->is_in_client_list = 1;
task->destroyed = 0;
task->failed = 0;
task->ran_task_func = 0;
task->cancelled = 0;
task->stub_resolver = client->stub_resolver;
task->used_conf_nameservers = 0;
task->recursive = recursive ? 1 : 0;
task->rr_cache = client->rr_cache;
if (task->rr_cache != NULL)
gsk_dns_rr_cache_ref (task->rr_cache);
task->locked_records = NULL;
task->first_ns = NULL;
task->last_ns = NULL;
{
GSList *tmp;
GSList *copy_list = NULL;
for (tmp = questions; tmp != NULL; tmp = tmp->next)
{
GskDnsQuestion *orig = tmp->data;
GskDnsQuestion *copy = gsk_dns_question_copy (orig, NULL);
copy_list = g_slist_prepend (copy_list, copy);
}
task->questions = g_slist_reverse (copy_list);
}
task->answered_questions = NULL;
task->negatives = NULL;
task->func = func;
task->on_fail = on_fail;
task->func_data = func_data;
task->destroy = destroy;
task->timeout_source = NULL;
/* Add to the client's list of tasks. */
task->next = client->tasks;
task->prev = NULL;
if (client->tasks != NULL)
client->tasks->prev = task;
client->tasks = task;
try_local_cache_or_proceed (task);
rv = (task->ref_count == 1) ? NULL : task;
gsk_dns_client_task_unref (task);
return rv;
}
/* --- client: incoming dns message handler --- */
/* Find out whether a ResourceRecord from a particular
* address can be trusted enough to be put in our cache.
*/
static gboolean
check_rr_authority (GskDnsClient *client,
GskSocketAddressIpv4 *address,
GskDnsResourceRecord *record,
guint cur_time)
{
const guint8 *ip_address;
GskDnsNameServerInfo *ns_info;
ip_address = address->ip_address;
/* See if `address' is a configured nameserver.
* We trust those completely.
*/
for (ns_info = client->first_ns;
ns_info != NULL;
ns_info = ns_info->next_ns)
if (gsk_socket_address_equals (address, ns_info->address))
return TRUE;
/* See if `address' has been granted temporary
* permission.
*/
if (ip_permission_table_check (client->ip_perm_table,
address,
record->owner,
cur_time))
return TRUE;
return FALSE;
}
static gboolean
is_or_is_cname_for (const char *owner,
const char *ask_name,
GskDnsRRCache *rr_cache)
{
while (ask_name != NULL)
{
GskDnsResourceRecord *rr;
if (strcasecmp (owner, ask_name) == 0)
return TRUE;
rr = gsk_dns_rr_cache_lookup_one (rr_cache,
ask_name,
GSK_DNS_RR_CANONICAL_NAME,
GSK_DNS_CLASS_INTERNET,
0);
ask_name = rr ? rr->rdata.domain_name : NULL;
}
return FALSE;
}
static gboolean
is_suffix_for (const char *name, const char *suffix)
{
int suffix_len = strlen (suffix);
int query_name_len = strlen (name);
const char *qsuffix = name + query_name_len - suffix_len;
if ((qsuffix > name && qsuffix[-1] == '.')
|| (qsuffix == name))
{
if (strcasecmp (suffix, qsuffix) == 0)
return TRUE;
}
return FALSE;
}
static inline gboolean
check_does_rr_answer_question (GskDnsResourceRecord *record,
GskDnsQuestion *question,
GskDnsRRCache *rr_cache)
{
if (record->type == GSK_DNS_RR_NAME_SERVER)
{
const char *query_name = question->query_name;
while (query_name != NULL)
{
GskDnsResourceRecord *rr;
if (is_suffix_for (query_name, record->owner))
return TRUE;
rr = gsk_dns_rr_cache_lookup_one (rr_cache,
query_name,
GSK_DNS_RR_CANONICAL_NAME,
GSK_DNS_CLASS_INTERNET,
0);
query_name = rr ? rr->rdata.domain_name : NULL;
}
}
/* Probably only applies to the 'additional' section. */
if (record->type == GSK_DNS_RR_HOST_ADDRESS
|| record->type == GSK_DNS_RR_HOST_ADDRESS_IPV6)
{
const char *query_name = question->query_name;
GSList *all_encountered_names = g_slist_prepend (NULL,
(gpointer) query_name);
while (query_name != NULL)
{
GskDnsResourceRecord *rr;
const char *qn = query_name;
GSList *ns_list;
do {
ns_list = gsk_dns_rr_cache_lookup_list (rr_cache, qn,
GSK_DNS_RR_NAME_SERVER,
GSK_DNS_CLASS_INTERNET);
qn = strchr (qn, '.');
if (qn)
qn++;
} while (ns_list == NULL && qn != NULL);
while (ns_list)
{
GskDnsResourceRecord *ns_rr = ns_list->data;
ns_list = g_slist_remove (ns_list, ns_rr);
if (strcasecmp (ns_rr->rdata.domain_name, record->owner) == 0)
{
g_slist_free (ns_list);
g_slist_free (all_encountered_names);
return TRUE;
}
}
rr = gsk_dns_rr_cache_lookup_one (rr_cache,
query_name,
GSK_DNS_RR_CANONICAL_NAME,
GSK_DNS_CLASS_INTERNET,
0);
query_name = rr ? rr->rdata.domain_name : NULL;
if (query_name)
{
if (g_slist_find_custom (all_encountered_names, query_name,
(GCompareFunc) strcmp))
break;
all_encountered_names = g_slist_prepend (all_encountered_names,
(gpointer) query_name);
}
}
g_slist_free (all_encountered_names);
}
return (question->query_type == record->type
|| record->type == GSK_DNS_RR_CANONICAL_NAME
|| question->query_type == GSK_DNS_RR_WILDCARD)
&& is_or_is_cname_for (record->owner, question->query_name, rr_cache);
}
/* Check whether a ResourceRecord appears to
* answer any question posed to this task.
*
* XXX: each of the hosts needs a list of CNAME's it goes by.
* (Note i think this TODO is should be resolved as a TODO
* for check_does_rr_answer_question... not quite sure
* though)
*/
static gboolean
check_is_rr_relevant (ClientTask *task,
GskDnsResourceRecord *record,
GskDnsRRCache *rr_cache)
{
GSList *list;
for (list = task->questions; list != NULL; list = list->next)
if (check_does_rr_answer_question (record,
(GskDnsQuestion *) list->data,
rr_cache))
return TRUE;
return FALSE;
}
static void
append_and_lock_rr_list_to_task (GSList *rr_list,
ClientTask *task,
GskSocketAddressIpv4 *address,
gboolean is_authoritative,
guint cur_time)
{
while (rr_list != NULL)
{
GskDnsResourceRecord *record = rr_list->data;
if (!check_rr_authority (task->client, address, record, cur_time))
{
rr_list = rr_list->next;
continue;
}
record = gsk_dns_rr_cache_insert (task->rr_cache,
record,
is_authoritative,
cur_time);
task->locked_records = g_slist_prepend (task->locked_records, record);
gsk_dns_rr_cache_lock (task->rr_cache, record);
rr_list = rr_list->next;
}
}
static int
question_equal_or_ends_with (GskDnsQuestion *question,
const char *ns_owner)
{
const char *query_name = question->query_name;
if (strcasecmp (query_name, ns_owner) == 0)
{
return 0;
}
else
{
const char *start_ns_owner = strchr (query_name, 0) - strlen (ns_owner);
if (start_ns_owner <= query_name)
return 1;
if (start_ns_owner[-1] == '.'
&& strcasecmp (start_ns_owner, ns_owner) == 0)
return 0;
}
return 1;
}
static int
look_for_relevant_ns_entry (GskDnsResourceRecord *record,
ClientTask *task)
{
if (record->type != GSK_DNS_RR_NAME_SERVER)
return 1;
/* find out if any of the questions' query_names are equal to
``owner'', or end with ``.owner''. */
if (g_slist_find_custom (task->questions,
(gpointer) record->owner,
(GCompareFunc) question_equal_or_ends_with))
return 0;
return 1;
}
static void
task_handle_message (ClientTask *task,
GskSocketAddressIpv4 *address,
GskDnsMessage *message)
{
GSList *lists[3];
GSList *list;
/* Set if any of the answers applied to us.
* Blithely assume that we may enter the
* remaining records to the locked-list,
* if we give them authority to be added into our
* cache.
*/
gboolean one_answer_was_relevant = FALSE;
guint iteration;
guint cur_time = task->client->main_loop->current_time.tv_sec;
lists[0] = message->answers;
lists[1] = message->authority;
lists[2] = message->additional;
/*
* Append as many answer rr's as possible into
* the cache, and lock those records.
*/
for (iteration = 0; iteration < 3; iteration++)
for (list = lists[iteration]; list != NULL; list = list->next)
{
GskDnsResourceRecord *record = list->data;
/* Check whether the source nameserver has the authority
* to give us a record like this.
*/
if (!check_rr_authority (task->client, address, record, cur_time))
{
g_warning ("ip address (%d.%d.%d.%d) did not have authority to add '%s'",
address->ip_address[0],
address->ip_address[1],
address->ip_address[2],
address->ip_address[3],
record->owner);
continue;
}
//DEBUG ("task_handle_message: task->client->rr_cache=%p; record=%s", task->client->rr_cache, gsk_dns_rr_text_to_str (record, NULL)); /*XXX: memory leak */
/* Add that RR to our cache. */
if (task->client->rr_cache != NULL)
record = gsk_dns_rr_cache_insert (task->rr_cache, record,
message->is_authoritative,
cur_time);
/* Now see if that answer is relevant to any of our questions. */
if (!check_is_rr_relevant (task, record, task->rr_cache))
{
DEBUG ("that record was not relevant");
continue;
}
DEBUG ("task_handle_message: the message WAS relevant");
/* Ok, lock this RR; if the DnsClient has no cache,
* lock it into the task's local cache.
*
*/
if (task->client->rr_cache == NULL)
record = gsk_dns_rr_cache_insert (task->rr_cache, record,
message->is_authoritative,
cur_time);
gsk_dns_rr_cache_lock (task->rr_cache, record);
task->locked_records = g_slist_prepend (task->locked_records, record);
if (record->type == GSK_DNS_RR_NAME_SERVER)
{
/* let's allow additional records giving ip addresses
* to this nameserver.
*
* TODO: figure out if the '+ 1' is really needed!
*/
ip_permission_table_insert (task->client->ip_perm_table,
address,
FALSE, /* just this exact name */
record->rdata.domain_name,
cur_time + 1);
}
else if (record->type == GSK_DNS_RR_CANONICAL_NAME)
{
/* I'm not really sure that this is secure,
but we just pierce a hole in the ns permission
table to allow this name server to allow us to
define that CNAME a bit more.
We should probably start issuing queries for
the CNAME independently however,
ie we should now go and ask about the CNAME
if this query doesn't pan out. */
const char *cname = record->rdata.domain_name;
#if 1
const char *last_dot = NULL;
const char *dot = strchr (cname, '.');
while (dot && dot[1] != 0)
{
last_dot = dot;
dot = strchr (dot + 1, '.');
}
if (last_dot)
cname = last_dot + 1;
#endif
ip_permission_table_insert (task->client->ip_perm_table,
address,
TRUE,
cname,
cur_time + 1);
}
one_answer_was_relevant = TRUE;
}
switch (message->error_code)
{
case GSK_DNS_RESPONSE_ERROR_NONE:
/* No error: this is the default case. */
break;
case GSK_DNS_RESPONSE_ERROR_FORMAT:
{
GError *error = g_error_new (GSK_G_ERROR_DOMAIN,
GSK_ERROR_RESOLVER_FORMAT,
_("format error from DNS request"));
client_task_fail (task, error);
g_error_free (error);
return;
}
case GSK_DNS_RESPONSE_ERROR_SERVER_FAILURE:
{
GError *error = g_error_new (GSK_G_ERROR_DOMAIN,
GSK_ERROR_RESOLVER_SERVER_PROBLEM,
_("miscellaneous server problem"));
client_task_fail (task, error);
g_error_free (error);
return;
}
case GSK_DNS_RESPONSE_ERROR_NAME_ERROR:
{
GskDnsQuestion *question = message->questions ? message->questions->data : NULL;
GError *error = g_error_new (GSK_G_ERROR_DOMAIN,
GSK_ERROR_RESOLVER_NOT_FOUND,
_("name %s not found"),
(question ? question->query_name : "**UNKNOWN**"));
client_task_fail (task, error);
g_error_free (error);
return;
}
case GSK_DNS_RESPONSE_ERROR_NOT_IMPLEMENTED:
{
GError *error = g_error_new (GSK_G_ERROR_DOMAIN,
GSK_ERROR_RESOLVER_SERVER_PROBLEM,
_("server: command not implemented"));
client_task_fail (task, error);
g_error_free (error);
return;
}
case GSK_DNS_RESPONSE_ERROR_REFUSED:
{
GError *error = g_error_new (GSK_G_ERROR_DOMAIN,
GSK_ERROR_RESOLVER_SERVER_PROBLEM,
_("server: command refused"));
client_task_fail (task, error);
g_error_free (error);
return;
}
default:
{
GError *error = g_error_new (GSK_G_ERROR_DOMAIN,
GSK_ERROR_RESOLVER_SERVER_PROBLEM,
_("server: unexpected error code"));
client_task_fail (task, error);
g_error_free (error);
return;
}
}
/* ok, must've been a mixup */
if (!one_answer_was_relevant)
{
GSList *found;
found = g_slist_find_custom (message->answers, task,
(GCompareFunc) look_for_relevant_ns_entry);
if (found == NULL)
found = g_slist_find_custom (message->authority, task,
(GCompareFunc) look_for_relevant_ns_entry);
if (found == NULL)
found = g_slist_find_custom (message->additional, task,
(GCompareFunc) look_for_relevant_ns_entry);
if (found == NULL)
{
/* XXX: this can actually happen if we are making so many
DNS requests that hash_next,hash_prev are actually being
used. */
/* XXX: another time when this happens
is when the question is an empty string.
not completely sure what's going on in that case. */
gsk_g_debug ("Received useless message with matching ID.");
#if 0
g_message ("MESSAGE WAS:");
gsk_dns_dump_message_fp (message, stderr);
g_message ("QUESTIONS WERE:");
g_slist_foreach (task->questions, (GFunc) gsk_dns_dump_question_fp, stderr);
g_message ("ANSWERED QUESTIONS WERE:");
g_slist_foreach (task->answered_questions, (GFunc) gsk_dns_dump_question_fp, stderr);
#endif
return;
}
}
/* Scan the authority and additional records. */
append_and_lock_rr_list_to_task (message->authority, task,
address, message->is_authoritative,
cur_time);
append_and_lock_rr_list_to_task (message->additional, task,
address, message->is_authoritative,
cur_time);
/* Now, try continuing the processing. */
try_local_cache_or_proceed (task);
}
static void
client_handle_message (GskDnsClient *client,
GskDnsMessage *message,
GskSocketAddressIpv4 *address)
{
ClientTask *task_list;
guint message_id = message->id;
if (message->is_query)
return;
task_list = g_hash_table_lookup (client->id_to_task_list,
GUINT_TO_POINTER (message_id));
DEBUG ("client_handle_message: for message id %d, task_list=%p", message_id, task_list);
DEBUG_PRINT_MESSAGE (message);
while (task_list != NULL)
{
ClientTask *next_task;
gsk_dns_client_task_ref (task_list);
task_handle_message (task_list, address, message);
next_task = task_list->hash_next;
gsk_dns_client_task_unref (task_list);
task_list = next_task;
}
}
/* --- utility methods --- */
static void
gsk_dns_client_fail_all_tasks (GskDnsClient *client,
GError *error)
{
while (client->tasks != NULL)
{
ClientTask *task = client->tasks;
remove_from_client_list (task);
if (!task->destroyed)
{
if (task->on_fail != NULL)
(*task->on_fail) (error, task->func_data);
task->failed = 1;
}
gsk_dns_client_task_unref (task);
}
}
/* Fail a single client and remove it from the client list. */
static void
client_task_fail (ClientTask *task,
GError *error)
{
remove_from_client_list (task);
if (!task->destroyed)
{
if (task->on_fail != NULL)
(*task->on_fail) (error, task->func_data);
task->failed = 1;
}
DEBUG ("client_task_fail: %s", error->message);
gsk_dns_client_task_unref (task);
}
static void
gsk_dns_client_transmit (GskDnsClient *client,
GskSocketAddressIpv4 *address,
GskDnsMessage *message)
{
GskPacket *packet = gsk_dns_message_to_packet (message);
gsk_packet_set_dst_address (packet, GSK_SOCKET_ADDRESS (address));
#ifdef GSK_DEBUG
if (gsk_debug_flags & GSK_DEBUG_DNS)
{
char *a = gsk_socket_address_to_string (GSK_SOCKET_ADDRESS (address));
g_printerr ("DNS: about to output message to %s:\n", a);
gsk_dns_dump_message_fp (message, stderr);
g_free (a);
}
#endif
if (client->first_outgoing_packet == NULL)
{
GError *error = NULL;
/* try writing it immediately */
if (!gsk_packet_queue_write (client->packet_queue, packet, &error))
{
if (error != NULL)
{
gsk_dns_client_fail_all_tasks (client, error);
g_error_free (error);
return;
}
/* fall-through */
}
else
{
/* successfully immeediately wrote packet: nothing else to do */
gsk_packet_unref (packet);
return;
}
}
/* append the packet to the queue */
client->last_outgoing_packet = g_slist_append (client->last_outgoing_packet, packet);
if (client->first_outgoing_packet == NULL)
client->first_outgoing_packet = client->last_outgoing_packet;
else
client->last_outgoing_packet = client->last_outgoing_packet->next;
/* make sure we aren't blocking writable events */
if (client->is_blocking_write)
{
client->is_blocking_write = 0;
gsk_io_unblock_write (GSK_IO (client->packet_queue));
}
}
/* --- io handlers --- */
static gboolean
handle_queue_is_readable (GskIO *io,
gpointer data)
{
GskPacket *packet;
GskDnsMessage *message;
GskSocketAddress *address;
GskDnsClient *client = GSK_DNS_CLIENT (data);
GError *error = NULL;
guint used;
packet = gsk_packet_queue_read (GSK_PACKET_QUEUE (io), TRUE, &error);
if (packet == NULL)
{
if (error != NULL)
{
gsk_dns_client_fail_all_tasks (client, error);
g_error_free (error);
return FALSE;
}
return TRUE;
}
/* convert the packet into a dns-message */
message = gsk_dns_message_parse_data ((const guint8 *) packet->data,
packet->len, &used);
#ifdef GSK_DEBUG
if (gsk_debug_flags & GSK_DEBUG_DNS)
{
char *a = gsk_socket_address_to_string (packet->src_address);
g_printerr ("DNS: from address %s got message:\n", a);
gsk_dns_dump_message_fp (message, stderr);
g_free (a);
}
#endif
address = g_object_ref (packet->src_address);
if (message != NULL && used != packet->len)
{
/* XXX: error handling */
g_warning ("ignorable error parsing dns message");
}
gsk_packet_unref (packet);
if (message == NULL)
{
/* XXX: error handling */
g_warning ("malformed dns message - ignoring");
g_object_unref (address);
return TRUE;
}
if (!GSK_IS_SOCKET_ADDRESS_IPV4 (address))
{
/* XXX: error handling */
g_warning ("only IP v4 sockets may use DNS");
gsk_dns_message_unref (message);
g_object_unref (address);
return TRUE;
}
client_handle_message (client, message,
GSK_SOCKET_ADDRESS_IPV4 (address));
gsk_dns_message_unref (message);
g_object_unref (address);
return TRUE;
}
static gboolean
handle_queue_is_readable_shutdown (GskIO *io,
gpointer data)
{
GError *error = g_error_new (GSK_G_ERROR_DOMAIN,
GSK_ERROR_RESOLVER_SOCKET_DIED,
_("got read shutdown in dns socket"));
gsk_dns_client_fail_all_tasks (GSK_DNS_CLIENT (data), error);
g_error_free (error);
return FALSE;
}
static gboolean
handle_queue_is_writable (GskIO *io,
gpointer data)
{
GskDnsClient *client = GSK_DNS_CLIENT (data);
while (client->first_outgoing_packet != NULL)
{
GError *error = NULL;
GSList *list = client->first_outgoing_packet;
GskPacket *packet = list->data;
if (!gsk_packet_queue_write (GSK_PACKET_QUEUE (io), packet, &error))
{
if (error == NULL)
return TRUE;
gsk_dns_client_fail_all_tasks (client, error);
g_error_free (error);
return FALSE;
}
client->first_outgoing_packet = g_slist_remove (list, packet);
if (client->first_outgoing_packet == NULL)
client->last_outgoing_packet = NULL;
gsk_packet_unref (packet);
}
if (client->first_outgoing_packet == NULL)
{
if (!client->is_blocking_write)
{
client->is_blocking_write = 1;
gsk_io_block_write (io);
}
}
else
g_assert (!client->is_blocking_write);
return TRUE;
}
static gboolean
handle_queue_is_writable_shutdown (GskIO *io,
gpointer data)
{
GError *error = g_error_new (GSK_G_ERROR_DOMAIN,
GSK_ERROR_RESOLVER_SOCKET_DIED,
_("got write shutdown in dns socket"));
gsk_dns_client_fail_all_tasks (GSK_DNS_CLIENT (data), error);
g_error_free (error);
return FALSE;
}
static void
unref_packet_queue (gpointer c)
{
g_object_unref (GSK_DNS_CLIENT (c)->packet_queue);
}
/* --- GObject functions --- */
static void
gsk_dns_client_destroy_all_queries (GskDnsClient *client)
{
GskDnsResolver *resolver = GSK_DNS_RESOLVER (client);
while (client->tasks != NULL)
gsk_dns_client_resolver_cancel (resolver, client->tasks);
}
static void
gsk_dns_client_finalize (GObject *object)
{
GskDnsClient *client = GSK_DNS_CLIENT (object);
gsk_dns_client_destroy_all_queries (client);
ip_permission_table_destroy (client->ip_perm_table);
g_hash_table_destroy (client->id_to_task_list);
if (client->rr_cache != NULL)
gsk_dns_rr_cache_unref (client->rr_cache);
if (client->searchpath_array != NULL)
{
gpointer *ptr_array = client->searchpath_array->pdata;
guint len = client->searchpath_array->len;
while (len-- > 0)
g_free (*ptr_array++);
g_ptr_array_free (client->searchpath_array, TRUE);
client->searchpath_array = NULL;
}
(*parent_class->finalize) (object);
}
/* --- functions --- */
static void
gsk_dns_client_init (GskDnsClient *dns_client)
{
dns_client->last_message_id = (guint16) rand ();
dns_client->ip_perm_table = ip_permission_table_new ();
dns_client->id_to_task_list = g_hash_table_new (NULL, NULL);
dns_client->main_loop = gsk_main_loop_default ();
dns_client->recursion_desired = 1;
/* TODO: does the RFC say anything about these defaults? */
dns_client->max_iterations_recursive = 5; /* recursive ns shouldn't take many retries */
dns_client->max_iterations_nonrecursive = 10;
}
static void
gsk_dns_client_resolver_init (GskDnsResolverIface *resolver_iface)
{
resolver_iface->resolve = gsk_dns_client_resolve;
resolver_iface->cancel = gsk_dns_client_resolver_cancel;
}
static void
gsk_dns_client_class_init (GskDnsClientClass *dns_client_class)
{
GObjectClass *object_class = G_OBJECT_CLASS (dns_client_class);
parent_class = g_type_class_peek_parent (object_class);
object_class->finalize = gsk_dns_client_finalize;
}
GType gsk_dns_client_get_type()
{
static GType dns_client_type = 0;
if (!dns_client_type)
{
static const GTypeInfo dns_client_info =
{
sizeof(GskDnsClientClass),
(GBaseInitFunc) NULL,
(GBaseFinalizeFunc) NULL,
(GClassInitFunc) gsk_dns_client_class_init,
NULL, /* class_finalize */
NULL, /* class_data */
sizeof (GskDnsClient),
0, /* n_preallocs */
(GInstanceInitFunc) gsk_dns_client_init,
NULL /* value_table */
};
static GInterfaceInfo client_resolver_info =
{
(GInterfaceInitFunc) gsk_dns_client_resolver_init,
NULL, /* interface_finalize */
NULL /* interface_data */
};
dns_client_type = g_type_register_static (G_TYPE_OBJECT,
"GskDnsClient",
&dns_client_info, 0);
g_type_add_interface_static (dns_client_type,
GSK_TYPE_DNS_RESOLVER,
&client_resolver_info);
gsk_dns_resolver_add_name_resolver_iface (dns_client_type);
}
return dns_client_type;
}
/**
* gsk_dns_client_new:
* @packet_queue: underlying transport layer to use.
* The client will keep a reference to this packet-queue.
* @rr_cache: cache of resource-records.
* The client will keep a reference to this cache.
* @flags: whether you want this client to be a recursive
* or stub resolver.
*
* Create a new DNS client.
* This implements the #GskDnsResolver interface.
*
* returns: the newly allocated DNS client.
*/
GskDnsClient *gsk_dns_client_new (GskPacketQueue *packet_queue,
GskDnsRRCache *rr_cache,
GskDnsClientFlags flags)
{
GskDnsClient *client;
client = g_object_new (GSK_TYPE_DNS_CLIENT, NULL);
client->packet_queue = g_object_ref (packet_queue);
gsk_io_trap_readable (g_object_ref (packet_queue),
handle_queue_is_readable,
handle_queue_is_readable_shutdown,
client,
unref_packet_queue);
gsk_io_trap_writable (g_object_ref (packet_queue),
handle_queue_is_writable,
handle_queue_is_writable_shutdown,
client,
unref_packet_queue);
client->is_blocking_write = 1;
gsk_io_block_write (GSK_IO (packet_queue));
client->stub_resolver = (flags & GSK_DNS_CLIENT_STUB_RESOLVER) ? 1 : 0;
client->rr_cache = rr_cache;
if (rr_cache != NULL)
gsk_dns_rr_cache_ref (rr_cache);
else
client->rr_cache = gsk_dns_rr_cache_new (GSK_DNS_MAX_CACHE_BYTES,
GSK_DNS_MAX_CACHE_RECORDS);
return client;
}
/**
* gsk_dns_client_add_searchpath:
* @client: the client to affect.
* @searchpath: the new path to search.
*
* Add a new implicit domain to the list the client keeps.
*
* A searchpath entry is simply a domain to try
* post-fixing to any request. For example,
* if you have "sourceforce.net" in your searchpath,
* then looking "cvs.gsk" should resolve "cvs.gsk.sourceforge.net".
*
* Searchpath entries take priority, EXCEPT
* if the requested domain name ends in ".".
* If you have "sourceforce.net" in your searchpath,
* then looking "cvs.com" should resolve "cvs.com.sourceforge.net"
* then "cvs.com" if the former does not exist.
* However, looking up "cvs.com." will ONLY search the global namespace.
*/
void
gsk_dns_client_add_searchpath(GskDnsClient *client,
const char *searchpath)
{
g_return_if_fail (searchpath != NULL);
if (client->searchpath_array == NULL)
client->searchpath_array = g_ptr_array_new ();
g_ptr_array_add (client->searchpath_array, g_strdup (searchpath));
}
/**
* gsk_dns_client_add_ns:
* @client: the client to affect.
* @address: the numeric address of the nameserver.
*
* Add a new nameserver to query.
* All nameservers will be queried simultaneously.
*/
void
gsk_dns_client_add_ns (GskDnsClient *client,
GskSocketAddressIpv4 *address)
{
GskDnsNameServerInfo *ns_info;
for (ns_info = client->first_ns; ns_info != NULL; ns_info = ns_info->next_ns)
if (gsk_socket_address_equals (address, ns_info->address))
break;
if (ns_info != NULL)
return;
ns_info = gsk_dns_name_server_info_alloc ();
ns_info->address = g_object_ref (address);
ns_info->num_msg_sent = ns_info->num_msg_received = 0;
ns_info->next_ns = NULL;
ns_info->prev_ns = client->last_ns;
ns_info->is_default_ns = 0;
if (client->last_ns != NULL)
client->last_ns->next_ns = ns_info;
else
client->first_ns = ns_info;
client->last_ns = ns_info;
}
/**
* gsk_dns_client_set_cache:
* @client: the client to affect.
* @rr_cache: the new resource-record cache to use.
*
* Switch the client to use a new resource-record cache.
*/
void
gsk_dns_client_set_cache (GskDnsClient *client,
GskDnsRRCache *rr_cache)
{
if (rr_cache != client->rr_cache)
{
if (rr_cache != NULL)
gsk_dns_rr_cache_ref (rr_cache);
if (client->rr_cache != NULL)
gsk_dns_rr_cache_unref (client->rr_cache);
client->rr_cache = rr_cache;
}
}
void
gsk_dns_client_set_flags (GskDnsClient *client,
GskDnsClientFlags flags)
{
client->stub_resolver = (flags & GSK_DNS_CLIENT_STUB_RESOLVER) ? 1 : 0;
}
GskDnsClientFlags
gsk_dns_client_get_flags (GskDnsClient *client)
{
return client->stub_resolver ? GSK_DNS_CLIENT_STUB_RESOLVER : 0;
}
/* --- System file parsing --- */
/**
* gsk_dns_client_parse_resolv_conf_line:
* @client: the client which should add the resolv.conf information
* to its search parameters.
* @text: a single line from a resolve.conf.
*
* A resolv.conf file can have several fields:
* 'nameserver' gives an ip-address of a nameserver to use.
* 'domain' gives the host's domain
* 'search' gives alternate domains to search.
* Also, 'sortlist' is unimplemented.
*
* returns: whether the line was parsed successfully.
*/
/* Format of /etc/resolv.conf:
*
* `nameserver IPP-ADDRESS' List a name server to use.
* `domain DOMAIN-NAME' Domain which is added to search path.
* If not set here, assume everything after
* the first dot is the domain.
* (XXX: domain needs to be implemented)
* `search PATH' List of paths to inspect for dns data.
*
* Unsupported:
* `sortlist IP-PREFERENCES-SPEC' List of how the return values
* should be sorted.
*/
gboolean
gsk_dns_client_parse_resolv_conf_line(GskDnsClient *client,
const char *text)
{
GSK_SKIP_WHITESPACE (text);
if (*text == 0 || *text == '#' || *text == ';')
return TRUE;
if (g_strncasecmp (text, "nameserver", 10) == 0)
{
GskSocketAddress *address;
guint8 ip_address[4];
text += 10;
GSK_SKIP_WHITESPACE (text);
if (!gsk_dns_parse_ip_address (&text, ip_address))
return FALSE;
address = gsk_socket_address_ipv4_new (ip_address, GSK_DNS_PORT);
gsk_dns_client_add_ns (client, GSK_SOCKET_ADDRESS_IPV4 (address));
return TRUE;
}
if (g_strncasecmp (text, "search", 6) == 0)
{
const char *end;
char *tmp;
text += 6;
GSK_SKIP_WHITESPACE (text);
tmp = alloca (strlen (text) + 1);
while (*text != 0)
{
end = text;
GSK_SKIP_NONWHITESPACE (end);
if (end == text)
break;
memcpy (tmp, text, end - text);
tmp[end - text] = 0;
gsk_dns_client_add_searchpath (client, tmp);
text = end;
GSK_SKIP_WHITESPACE (text);
}
return TRUE;
}
return FALSE;
}
/**
* gsk_dns_client_parse_resolv_conf:
* @client: the client which should add the resolv.conf information
* @filename: name of the file containing resolv.conf information.
* Typically "/etc/resolv.conf".
* @may_be_missing: whether to consider it an error if the
* file is missing.
*
* Parse /etc/resolv.conf information.
*
* returns: whether the file was parsed successfully.
*/
gboolean
gsk_dns_client_parse_resolv_conf (GskDnsClient *client,
const char *filename,
gboolean may_be_missing)
{
FILE *fp;
char buf[8192];
int line = 1;
fp = fopen (filename, "r");
if (fp == NULL)
return may_be_missing;
while (fgets (buf, sizeof (buf), fp) != NULL)
{
char *nl = strchr (buf, '\n');
if (nl == NULL)
{
g_warning ("%s: line too long or truncated file?", filename);
fclose (fp);
return FALSE;
}
*nl = '\0';
if (!gsk_dns_client_parse_resolv_conf_line (client, buf))
{
g_warning ("resolver: %s: error parsing line %d", filename, line);
fclose (fp);
return FALSE;
}
line++;
}
fclose (fp);
return TRUE;
}
/**
* gsk_dns_client_parse_system_files:
* @client: the client which should add the system configuration information.
*
* Parse system DNS configuration.
*
* Currently, this parses /etc/hosts and /etc/resolv.conf .
*
* returns: whether the files were parsed successfully.
*/
gboolean
gsk_dns_client_parse_system_files(GskDnsClient *client)
{
gboolean rv1, rv2;
GskDnsRRCache *rr_cache = client->rr_cache;
g_return_val_if_fail (rr_cache != NULL, FALSE);
rv1 = gsk_dns_client_parse_resolv_conf (client, "/etc/resolv.conf", TRUE);
rv2 = gsk_dns_rr_cache_parse_etc_hosts (rr_cache, "/etc/hosts", TRUE);
return rv1 && rv2;
}
/* --- ip-permission table implementation --- */
typedef struct _IpPermData IpPermData;
typedef struct _IpPermAddress IpPermAddress;
struct _IpPermData
{
IpPermAddress *addr_info;
IpPermData *next_data;
IpPermData *prev_data;
guint expire_time;
/* If TRUE, just verify the suffix of the ResourceRecord.
* If FALSE, verify only an exact match.
*/
gboolean any_suffixed_domain;
const char *owner;
};
struct _IpPermAddress
{
/* must be first member for hash functions to work out! */
GskSocketAddress *sock_addr;
/* XXX: we may want a hashtable here instead! */
IpPermData *first_data;
IpPermData *last_data;
};
struct _IpPermissionTable
{
GHashTable *sockaddr_to_perm_addr;
GTree *by_expire_time;
/* if it turns out too much time is being spent
in ip_permission_table_expire, turn this off
and call flush on a schedule... */
gboolean autoflush;
};
/* --- helper functions --- */
static char *
lowercase_string (char *out, const char *in)
{
char *rv = out;
while (*in != 0)
{
if ('A' <= *in && *in <= 'Z')
*out = *in + ('a' - 'A');
else
*out = *in;
out++;
in++;
}
*out = 0;
return rv;
}
/* Make VAR a lower-cased copy of STR, on the stack. */
#define LOWER_CASE_COPY_ON_STACK(var,str) \
G_STMT_START{ \
const char *_str = str; \
char *_tmp; \
_tmp = alloca (strlen (_str) + 1); \
var = lowercase_string (_tmp, _str); \
}G_STMT_END
/*
* ___ ____ _____ _ _
* |_ _|_ __ | _ \ ___ _ __ _ __ __|_ _|_ _| |__ | | ___
* | || '_ \| |_) / _ \ '__| '_ ` _ \| |/ _` | '_ \| |/ _ \
* | || |_) | __/ __/ | | | | | | | | (_| | |_) | | __/
* |___| .__/|_| \___|_| |_| |_| |_|_|\__,_|_.__/|_|\___|
* |_|
*
* A data structure used to temporary grant permission
* to certain nameservers' IP addresses for a certain
* subset of names.
*/
static gint
compare_ip_perm_data_times (const IpPermData *perm_data_a,
const IpPermData *perm_data_b)
{
if (perm_data_a->expire_time < perm_data_b->expire_time)
return -1;
if (perm_data_a->expire_time > perm_data_b->expire_time)
return +1;
if (perm_data_a < perm_data_b)
return -1;
if (perm_data_a > perm_data_b)
return +1;
return 0;
}
static IpPermissionTable *
ip_permission_table_new ()
{
IpPermissionTable *ip_perm_table = g_new (IpPermissionTable, 1);
ip_perm_table->sockaddr_to_perm_addr
= g_hash_table_new (gsk_socket_address_hash,
gsk_socket_address_equals);
ip_perm_table->by_expire_time
= g_tree_new ((GCompareFunc) compare_ip_perm_data_times);
ip_perm_table->autoflush = TRUE;
return ip_perm_table;
}
static void
ip_permission_table_insert (IpPermissionTable *table,
GskSocketAddressIpv4 *address,
gboolean any_suffixed_domain,
const char *owner,
guint expire_time)
{
IpPermAddress *perm_addr;
IpPermData *data;
char *lc_owner;
LOWER_CASE_COPY_ON_STACK (lc_owner, owner);
#if DEBUG_IP_PERMISSION_TABLE
g_message ("ip_permission_table_insert: %d.%d.%d.%d: %s: %s; expire=%d",
address->ipv4.ip_address[0],
address->ipv4.ip_address[1],
address->ipv4.ip_address[2],
address->ipv4.ip_address[3],
owner,
any_suffixed_domain ? "entire subtree" : "just that host",
expire_time);
#endif
perm_addr = g_hash_table_lookup (table->sockaddr_to_perm_addr, address);
if (perm_addr == NULL)
{
perm_addr = g_new (IpPermAddress, 1);
perm_addr->sock_addr = g_object_ref (address);
perm_addr->first_data = NULL;
perm_addr->last_data = NULL;
g_hash_table_insert (table->sockaddr_to_perm_addr, perm_addr->sock_addr, perm_addr);
#if DEBUG_IP_PERMISSION_TABLE
g_message ("inserting perm_addr for %d.%d.%d.%d",
address->ipv4.ip_address[0],
address->ipv4.ip_address[1],
address->ipv4.ip_address[2],
address->ipv4.ip_address[3]);
#endif
}
for (data = perm_addr->first_data; data != NULL; data = data->next_data)
if (strcmp (data->owner, lc_owner) == 0
&& (( any_suffixed_domain && data->any_suffixed_domain)
|| (!any_suffixed_domain && !data->any_suffixed_domain)))
{
/* Maybe lengthen expire time. */
if (data->expire_time < expire_time)
{
g_tree_remove (table->by_expire_time, data);
data->expire_time = expire_time;
g_tree_insert (table->by_expire_time, data, data);
}
return;
}
if (data == NULL)
{
/* Add a new data for this owner. */
data = g_malloc (sizeof (IpPermData) + strlen (lc_owner) + 1);
data->owner = strcpy ((char *) (data + 1), lc_owner);
data->any_suffixed_domain = any_suffixed_domain;
data->expire_time = expire_time;
data->addr_info = perm_addr;
data->prev_data = NULL;
data->next_data = perm_addr->first_data;
perm_addr->first_data = data;
if (data->next_data != NULL)
data->next_data->prev_data = data;
else
perm_addr->last_data = data;
g_tree_insert (table->by_expire_time, data, data);
}
}
static void
ip_permission_table_expire (IpPermissionTable *table,
guint cur_time)
{
IpPermData *data;
#if DEBUG_IP_PERMISSION_TABLE
g_message ("ip_permission_table_expire: cur_time=%d", cur_time);
#endif
while ((data = gsk_g_tree_min (table->by_expire_time)) != NULL)
{
if (data->expire_time > cur_time)
break;
/* remove data from lists */
if (data->next_data == NULL)
data->addr_info->last_data = data->prev_data;
else
data->next_data->prev_data = data->prev_data;
if (data->prev_data == NULL)
data->addr_info->first_data = data->next_data;
else
data->prev_data->next_data = data->next_data;
/* and from the tree */
g_tree_remove (table->by_expire_time, data);
/* and if data->addr_info is now empty, delete it */
if (data->addr_info->first_data == NULL)
{
g_hash_table_remove (table->sockaddr_to_perm_addr,
data->addr_info->sock_addr);
g_object_unref (data->addr_info->sock_addr);
g_free (data->addr_info);
}
g_free (data);
}
}
static gboolean
ip_permission_table_check (IpPermissionTable *table,
GskSocketAddressIpv4 *address,
const char *owner,
guint cur_time)
{
IpPermAddress *perm_addr;
IpPermData *perm_data;
char *lc_owner;
const char *end_owner;
LOWER_CASE_COPY_ON_STACK (lc_owner, owner);
if (table->autoflush)
ip_permission_table_expire (table, cur_time);
end_owner = strchr (lc_owner, 0);
perm_addr = g_hash_table_lookup (table->sockaddr_to_perm_addr, address);
if (perm_addr == NULL)
{
#if DEBUG_IP_PERMISSION_TABLE
g_message ("perm_check: no list found for %d.%d.%d.%d; table_size=%d",
address->ipv4.ip_address[0],
address->ipv4.ip_address[1],
address->ipv4.ip_address[2],
address->ipv4.ip_address[3],
g_hash_table_size (table->sockaddr_to_perm_addr));
#endif
return FALSE;
}
for (perm_data = perm_addr->first_data;
perm_data != NULL;
perm_data = perm_data->next_data)
{
#if DEBUG_IP_PERMISSION_TABLE
g_message ("perm_check: owner=%s; perm_data->owner=%s, any_suffixed_domain=%d, perm_data->expire_time=%d, cur_time=%d",
owner, perm_data->owner, perm_data->any_suffixed_domain, perm_data->expire_time, cur_time);
#endif
if (strcmp (lc_owner, perm_data->owner) == 0
&& perm_data->expire_time >= cur_time)
return TRUE;
if (perm_data->any_suffixed_domain)
{
int suffix_len = strlen (perm_data->owner);
if (end_owner - suffix_len - 1 >= lc_owner
&& strcmp (end_owner - suffix_len, perm_data->owner) == 0
&& end_owner[- suffix_len - 1] == '.'
&& perm_data->expire_time >= cur_time)
return TRUE;
}
}
return FALSE;
}
static void
destroy_perm_address (IpPermAddress *address)
{
IpPermData *data = address->first_data;
while (data != NULL)
{
IpPermData *next = data->next_data;
g_free (data);
data = next;
}
g_free (address);
}
static void
ip_permission_table_destroy (IpPermissionTable *table)
{
g_hash_table_foreach (table->sockaddr_to_perm_addr,
(GHFunc) destroy_perm_address,
NULL);
g_hash_table_destroy (table->sockaddr_to_perm_addr);
g_tree_destroy (table->by_expire_time);
g_free (table);
}
syntax highlighted by Code2HTML, v. 0.9.1