diff --git a/gio/gthreadedresolver.c b/gio/gthreadedresolver.c index 48545d6ad..aeeb40e9b 100644 --- a/gio/gthreadedresolver.c +++ b/gio/gthreadedresolver.c @@ -529,18 +529,56 @@ typedef enum __ns_type { #endif /* __BIONIC__ */ +/* Wrapper around dn_expand() which does associated length checks and returns + * errors as #GError. */ +static gboolean +expand_name (const gchar *rrname, + const guint8 *answer, + const guint8 *end, + const guint8 **p, + gchar *namebuf, + gsize namebuf_len, + GError **error) +{ + int expand_result; + + expand_result = dn_expand (answer, end, *p, namebuf, namebuf_len); + if (expand_result < 0 || end - *p < expand_result) + { + g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, + /* Translators: the placeholder is a DNS record type, such as ‘MX’ or ‘SRV’ */ + _("Error parsing DNS %s record: malformed DNS packet"), rrname); + return FALSE; + } + + *p += expand_result; + + return TRUE; +} + static GVariant * parse_res_srv (const guint8 *answer, const guint8 *end, - const guint8 **p) + const guint8 **p, + GError **error) { gchar namebuf[1024]; guint16 priority, weight, port; + if (end - *p < 6) + { + g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, + /* Translators: the placeholder is a DNS record type, such as ‘MX’ or ‘SRV’ */ + _("Error parsing DNS %s record: malformed DNS packet"), "SRV"); + return NULL; + } + GETSHORT (priority, *p); GETSHORT (weight, *p); GETSHORT (port, *p); - *p += dn_expand (answer, end, *p, namebuf, sizeof (namebuf)); + + if (!expand_name ("SRV", answer, end, p, namebuf, sizeof (namebuf), error)) + return NULL; return g_variant_new ("(qqqs)", priority, @@ -552,14 +590,26 @@ parse_res_srv (const guint8 *answer, static GVariant * parse_res_soa (const guint8 *answer, const guint8 *end, - const guint8 **p) + const guint8 **p, + GError **error) { gchar mnamebuf[1024]; gchar rnamebuf[1024]; guint32 serial, refresh, retry, expire, ttl; - *p += dn_expand (answer, end, *p, mnamebuf, sizeof (mnamebuf)); - *p += dn_expand (answer, end, *p, rnamebuf, sizeof (rnamebuf)); + if (!expand_name ("SOA", answer, end, p, mnamebuf, sizeof (mnamebuf), error)) + return NULL; + + if (!expand_name ("SOA", answer, end, p, rnamebuf, sizeof (rnamebuf), error)) + return NULL; + + if (end - *p < 20) + { + g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, + /* Translators: the placeholder is a DNS record type, such as ‘MX’ or ‘SRV’ */ + _("Error parsing DNS %s record: malformed DNS packet"), "SOA"); + return NULL; + } GETLONG (serial, *p); GETLONG (refresh, *p); @@ -580,11 +630,13 @@ parse_res_soa (const guint8 *answer, static GVariant * parse_res_ns (const guint8 *answer, const guint8 *end, - const guint8 **p) + const guint8 **p, + GError **error) { gchar namebuf[1024]; - *p += dn_expand (answer, end, *p, namebuf, sizeof (namebuf)); + if (!expand_name ("NS", answer, end, p, namebuf, sizeof (namebuf), error)) + return NULL; return g_variant_new ("(s)", namebuf); } @@ -592,14 +644,24 @@ parse_res_ns (const guint8 *answer, static GVariant * parse_res_mx (const guint8 *answer, const guint8 *end, - const guint8 **p) + const guint8 **p, + GError **error) { gchar namebuf[1024]; guint16 preference; + if (end - *p < 2) + { + g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, + /* Translators: the placeholder is a DNS record type, such as ‘MX’ or ‘SRV’ */ + _("Error parsing DNS %s record: malformed DNS packet"), "MX"); + return NULL; + } + GETSHORT (preference, *p); - *p += dn_expand (answer, end, *p, namebuf, sizeof (namebuf)); + if (!expand_name ("MX", answer, end, p, namebuf, sizeof (namebuf), error)) + return NULL; return g_variant_new ("(qs)", preference, @@ -609,19 +671,35 @@ parse_res_mx (const guint8 *answer, static GVariant * parse_res_txt (const guint8 *answer, const guint8 *end, - const guint8 **p) + const guint8 **p, + GError **error) { GVariant *record; GPtrArray *array; const guint8 *at = *p; gsize len; + if (end - *p == 0) + { + g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, + /* Translators: the placeholder is a DNS record type, such as ‘MX’ or ‘SRV’ */ + _("Error parsing DNS %s record: malformed DNS packet"), "TXT"); + return NULL; + } + array = g_ptr_array_new_with_free_func (g_free); while (at < end) { len = *(at++); if (len > (gsize) (end - at)) - break; + { + g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, + /* Translators: the placeholder is a DNS record type, such as ‘MX’ or ‘SRV’ */ + _("Error parsing DNS %s record: malformed DNS packet"), "TXT"); + g_ptr_array_free (array, TRUE); + return NULL; + } + g_ptr_array_add (array, g_strndup ((gchar *)at, len)); at += len; } @@ -633,7 +711,7 @@ parse_res_txt (const guint8 *answer, return record; } -static gint +gint g_resolver_record_type_to_rrtype (GResolverRecordType type) { switch (type) @@ -660,13 +738,15 @@ g_resolver_records_from_res_query (const gchar *rrname, gint herr, GError **error) { - gint count; + uint16_t count; gchar namebuf[1024]; const guint8 *end, *p; guint16 type, qclass, rdlength; const HEADER *header; GList *records; GVariant *record; + gsize len_unsigned; + GError *parsing_error = NULL; if (len <= 0) { @@ -689,18 +769,44 @@ g_resolver_records_from_res_query (const gchar *rrname, return NULL; } + /* We know len ≥ 0 now. */ + len_unsigned = (gsize) len; + + if (len_unsigned < sizeof (HEADER)) + { + g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, + /* Translators: the first placeholder is a domain name, the + * second is an error message */ + _("Error resolving “%s”: %s"), rrname, _("Malformed DNS packet")); + return NULL; + } + records = NULL; header = (HEADER *)answer; p = answer + sizeof (HEADER); - end = answer + len; + end = answer + len_unsigned; /* Skip query */ count = ntohs (header->qdcount); while (count-- && p < end) { - p += dn_expand (answer, end, p, namebuf, sizeof (namebuf)); - p += 4; + int expand_result; + + expand_result = dn_expand (answer, end, p, namebuf, sizeof (namebuf)); + if (expand_result < 0 || end - p < expand_result + 4) + { + /* Not possible to recover parsing as the length of the rest of the + * record is unknown or is too short. */ + g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, + /* Translators: the first placeholder is a domain name, the + * second is an error message */ + _("Error resolving “%s”: %s"), rrname, _("Malformed DNS packet")); + return NULL; + } + + p += expand_result; + p += 4; /* skip TYPE and CLASS */ /* To silence gcc warnings */ namebuf[0] = namebuf[1]; @@ -710,12 +816,35 @@ g_resolver_records_from_res_query (const gchar *rrname, count = ntohs (header->ancount); while (count-- && p < end) { - p += dn_expand (answer, end, p, namebuf, sizeof (namebuf)); + int expand_result; + + expand_result = dn_expand (answer, end, p, namebuf, sizeof (namebuf)); + if (expand_result < 0 || end - p < expand_result + 10) + { + /* Not possible to recover parsing as the length of the rest of the + * record is unknown or is too short. */ + g_set_error (&parsing_error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, + /* Translators: the first placeholder is a domain name, the + * second is an error message */ + _("Error resolving “%s”: %s"), rrname, _("Malformed DNS packet")); + break; + } + + p += expand_result; GETSHORT (type, p); GETSHORT (qclass, p); p += 4; /* ignore the ttl (type=long) value */ GETSHORT (rdlength, p); + if (end - p < rdlength) + { + g_set_error (&parsing_error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL, + /* Translators: the first placeholder is a domain name, the + * second is an error message */ + _("Error resolving “%s”: %s"), rrname, _("Malformed DNS packet")); + break; + } + if (type != rrtype || qclass != C_IN) { p += rdlength; @@ -725,31 +854,40 @@ g_resolver_records_from_res_query (const gchar *rrname, switch (rrtype) { case T_SRV: - record = parse_res_srv (answer, end, &p); + record = parse_res_srv (answer, p + rdlength, &p, &parsing_error); break; case T_MX: - record = parse_res_mx (answer, end, &p); + record = parse_res_mx (answer, p + rdlength, &p, &parsing_error); break; case T_SOA: - record = parse_res_soa (answer, end, &p); + record = parse_res_soa (answer, p + rdlength, &p, &parsing_error); break; case T_NS: - record = parse_res_ns (answer, end, &p); + record = parse_res_ns (answer, p + rdlength, &p, &parsing_error); break; case T_TXT: - record = parse_res_txt (answer, p + rdlength, &p); + record = parse_res_txt (answer, p + rdlength, &p, &parsing_error); break; default: - g_warn_if_reached (); + g_debug ("Unrecognised DNS record type %u", rrtype); record = NULL; break; } if (record != NULL) records = g_list_prepend (records, record); + + if (parsing_error != NULL) + break; } - if (records == NULL) + if (parsing_error != NULL) + { + g_propagate_prefixed_error (error, parsing_error, _("Failed to parse DNS response for “%s”: "), rrname); + g_list_free_full (records, (GDestroyNotify)g_variant_unref); + return NULL; + } + else if (records == NULL) { g_set_error (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_NOT_FOUND, _("No DNS record of the requested type for “%s”"), rrname); diff --git a/gio/gthreadedresolver.h b/gio/gthreadedresolver.h index 95a5fe55f..8d2ca19bf 100644 --- a/gio/gthreadedresolver.h +++ b/gio/gthreadedresolver.h @@ -43,6 +43,7 @@ GLIB_AVAILABLE_IN_ALL GType g_threaded_resolver_get_type (void) G_GNUC_CONST; /* Used for a private test API */ +#ifdef G_OS_UNIX GLIB_AVAILABLE_IN_ALL GList *g_resolver_records_from_res_query (const gchar *rrname, gint rrtype, @@ -50,6 +51,9 @@ GList *g_resolver_records_from_res_query (const gchar *rrname, gssize len, gint herr, GError **error); +GLIB_AVAILABLE_IN_ALL +gint g_resolver_record_type_to_rrtype (GResolverRecordType type); +#endif G_END_DECLS diff --git a/gio/meson.build b/gio/meson.build index 874de4b15..543763eea 100644 --- a/gio/meson.build +++ b/gio/meson.build @@ -74,6 +74,14 @@ if host_system != 'windows' endif endif + # dn_comp() + if cc.links('''#include + int main (int argc, char ** argv) { + return dn_comp(NULL, NULL, 0, NULL, NULL) == -1; + } ''', args : network_args, name : 'dn_comp()') + glib_conf.set('HAVE_DN_COMP', 1) + endif + # res_nclose() if cc.links('''#include #include diff --git a/gio/tests/meson.build b/gio/tests/meson.build index a27bc53d1..3ed23a5f2 100644 --- a/gio/tests/meson.build +++ b/gio/tests/meson.build @@ -191,6 +191,7 @@ if host_machine.system() != 'windows' }, 'gdbus-peer-object-manager' : {}, 'live-g-file' : {}, + 'resolver-parsing' : {'dependencies' : [network_libs]}, 'socket-address' : {}, 'stream-rw_all' : {}, 'unix-fd' : {}, diff --git a/gio/tests/resolver-parsing.c b/gio/tests/resolver-parsing.c new file mode 100644 index 000000000..d9cf05244 --- /dev/null +++ b/gio/tests/resolver-parsing.c @@ -0,0 +1,879 @@ +/* + * Copyright (c) 2021 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, see . + * + * Authors: Patrick Griffis + */ + +#include "config.h" + +#include +#include + +#define GIO_COMPILATION +#include "gthreadedresolver.h" +#undef GIO_COMPILATION + +#ifdef HAVE_DN_COMP +static void +dns_builder_add_uint8 (GByteArray *builder, + guint8 value) +{ + g_byte_array_append (builder, &value, 1); +} + +static void +dns_builder_add_uint16 (GByteArray *builder, + guint16 value) +{ + dns_builder_add_uint8 (builder, (value >> 8) & 0xFF); + dns_builder_add_uint8 (builder, (value) & 0xFF); +} + +static void +dns_builder_add_uint32 (GByteArray *builder, + guint32 value) +{ + dns_builder_add_uint8 (builder, (value >> 24) & 0xFF); + dns_builder_add_uint8 (builder, (value >> 16) & 0xFF); + dns_builder_add_uint8 (builder, (value >> 8) & 0xFF); + dns_builder_add_uint8 (builder, (value) & 0xFF); +} + +static void +dns_builder_add_length_prefixed_string (GByteArray *builder, + const char *string) +{ + guint8 length; + + g_assert (strlen (string) <= G_MAXUINT8); + + length = (guint8) strlen (string); + dns_builder_add_uint8 (builder, length); + + /* Don't include trailing NUL */ + g_byte_array_append (builder, (const guchar *)string, length); +} + +static void +dns_builder_add_domain (GByteArray *builder, + const char *string) +{ + int ret; + guchar buffer[256]; + + ret = dn_comp (string, buffer, sizeof (buffer), NULL, NULL); + g_assert (ret != -1); + + g_byte_array_append (builder, buffer, ret); +} + +/* Append an invalid domain name to the DNS response. This is implemented by + * appending a single label followed by a pointer back to that label. This is + * invalid regardless of any other context in the response as its expansion is + * infinite. + * + * See https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.4 + * + * In order to create a pointer to the label, the label’s final offset in the + * DNS response must be known. The current length of @builder, plus @offset, is + * used for this. Hence, @offset is the additional offset (in bytes) to add, and + * typically corresponds to the length of the parent #GByteArray that @builder + * will eventually be added to. Potentially plus 2 bytes for the rdlength, as + * per dns_builder_add_answer_data(). */ +static void +dns_builder_add_invalid_domain (GByteArray *builder, + gsize offset) +{ + offset += builder->len; + g_assert ((offset & 0xc0) == 0); + + dns_builder_add_uint8 (builder, 1); + dns_builder_add_uint8 (builder, 'f'); + dns_builder_add_uint8 (builder, 0xc0 | offset); +} + +static void +dns_builder_add_answer_data (GByteArray *builder, + GByteArray *answer) +{ + dns_builder_add_uint16 (builder, answer->len); /* rdlength */ + g_byte_array_append (builder, answer->data, answer->len); +} + +static GByteArray * +dns_header (void) +{ + GByteArray *answer = g_byte_array_sized_new (2046); + + /* Start with a header, we ignore everything except ancount. + https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.1 */ + dns_builder_add_uint16 (answer, 0); /* ID */ + dns_builder_add_uint16 (answer, 0); /* |QR| Opcode |AA|TC|RD|RA| Z | RCODE | */ + dns_builder_add_uint16 (answer, 0); /* QDCOUNT */ + dns_builder_add_uint16 (answer, 1); /* ANCOUNT (1 answer) */ + dns_builder_add_uint16 (answer, 0); /* NSCOUNT */ + dns_builder_add_uint16 (answer, 0); /* ARCOUNT */ + + return g_steal_pointer (&answer); +} + +static void +assert_query_fails (const gchar *rrname, + GResolverRecordType record_type, + GByteArray *answer) +{ + GList *records = NULL; + GError *local_error = NULL; + + records = g_resolver_records_from_res_query (rrname, + g_resolver_record_type_to_rrtype (record_type), + answer->data, + answer->len, + 0, + &local_error); + + g_assert_error (local_error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_INTERNAL); + g_assert_null (records); + g_clear_error (&local_error); +} + +static void +assert_query_succeeds (const gchar *rrname, + GResolverRecordType record_type, + GByteArray *answer, + const gchar *expected_answer_variant_str) +{ + GList *records = NULL; + GVariant *answer_variant, *expected_answer_variant = NULL; + GError *local_error = NULL; + + records = g_resolver_records_from_res_query (rrname, + g_resolver_record_type_to_rrtype (record_type), + answer->data, + answer->len, + 0, + &local_error); + + g_assert_no_error (local_error); + g_assert_nonnull (records); + + /* Test the results. */ + answer_variant = records->data; + expected_answer_variant = g_variant_new_parsed (expected_answer_variant_str); + g_assert_cmpvariant (answer_variant, expected_answer_variant); + + g_variant_unref (expected_answer_variant); + g_list_free_full (records, (GDestroyNotify) g_variant_unref); +} +#endif /* HAVE_DN_COMP */ + +static void +test_invalid_header (void) +{ + const struct + { + const guint8 *answer; + gsize answer_len; + GResolverError expected_error_code; + } + vectors[] = + { + /* No answer: */ + { (const guint8 *) "", 0, G_RESOLVER_ERROR_NOT_FOUND }, + /* Definitely too short to be a valid header: */ + { (const guint8 *) "\x20", 1, G_RESOLVER_ERROR_INTERNAL }, + /* One byte too short to be a valid header: */ + { (const guint8 *) "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 11, G_RESOLVER_ERROR_INTERNAL }, + /* Valid header indicating no answers: */ + { (const guint8 *) "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 12, G_RESOLVER_ERROR_NOT_FOUND }, + }; + gsize i; + + for (i = 0; i < G_N_ELEMENTS (vectors); i++) + { + GList *records = NULL; + GError *local_error = NULL; + + records = g_resolver_records_from_res_query ("example.org", + g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_NS), + vectors[i].answer, + vectors[i].answer_len, + 0, + &local_error); + + g_assert_error (local_error, G_RESOLVER_ERROR, (gint) vectors[i].expected_error_code); + g_assert_null (records); + g_clear_error (&local_error); + } +} + +static void +test_unknown_record_type (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL; + GList *records = NULL; + GError *local_error = NULL; + const guint type_id = 20; /* ISDN, not supported anywhere */ + + /* An answer with an unsupported type chosen from + * https://en.wikipedia.org/wiki/List_of_DNS_record_types#[1]_Obsolete_record_types */ + answer = dns_header (); + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, type_id); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + dns_builder_add_uint16 (answer, 0); /* rdlength */ + + records = g_resolver_records_from_res_query ("example.org", + type_id, + answer->data, + answer->len, + 0, + &local_error); + + g_assert_error (local_error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_NOT_FOUND); + g_assert_null (records); + g_clear_error (&local_error); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_mx_valid (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *mx_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_MX)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* MX rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.9 */ + mx_rdata = g_byte_array_new (); + dns_builder_add_uint16 (mx_rdata, 0); /* preference */ + dns_builder_add_domain (mx_rdata, "mail.example.org"); + dns_builder_add_answer_data (answer, mx_rdata); + g_byte_array_unref (mx_rdata); + + assert_query_succeeds ("example.org", G_RESOLVER_RECORD_MX, answer, + "(@q 0, 'mail.example.org')"); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_mx_invalid (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *mx_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_MX)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* MX rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.9 + * + * Use an invalid domain to trigger parsing failure. */ + mx_rdata = g_byte_array_new (); + dns_builder_add_uint16 (mx_rdata, 0); /* preference */ + dns_builder_add_invalid_domain (mx_rdata, answer->len + 2); + dns_builder_add_answer_data (answer, mx_rdata); + g_byte_array_unref (mx_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_MX, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_mx_invalid_too_short (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *mx_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_MX)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* MX rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.9 + * + * Miss out the domain field to trigger failure */ + mx_rdata = g_byte_array_new (); + dns_builder_add_uint16 (mx_rdata, 0); /* preference */ + /* missing domain field */ + dns_builder_add_answer_data (answer, mx_rdata); + g_byte_array_unref (mx_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_MX, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_mx_invalid_too_short2 (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *mx_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_MX)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* MX rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.9 + * + * Miss out all fields to trigger failure */ + mx_rdata = g_byte_array_new (); + /* missing preference and domain fields */ + dns_builder_add_answer_data (answer, mx_rdata); + g_byte_array_unref (mx_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_MX, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_ns_valid (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *ns_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_NS)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* NS rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.11 */ + ns_rdata = g_byte_array_new (); + dns_builder_add_domain (ns_rdata, "ns.example.org"); + dns_builder_add_answer_data (answer, ns_rdata); + g_byte_array_unref (ns_rdata); + + assert_query_succeeds ("example.org", G_RESOLVER_RECORD_NS, answer, + "('ns.example.org',)"); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_ns_invalid (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *ns_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_NS)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* NS rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.11 + * + * Use an invalid domain to trigger parsing failure. */ + ns_rdata = g_byte_array_new (); + dns_builder_add_invalid_domain (ns_rdata, answer->len + 2); + dns_builder_add_answer_data (answer, ns_rdata); + g_byte_array_unref (ns_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_NS, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_soa_valid (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *soa_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_SOA)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* SOA rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.13 */ + soa_rdata = g_byte_array_new (); + dns_builder_add_domain (soa_rdata, "mname.example.org"); + dns_builder_add_domain (soa_rdata, "rname.example.org"); + dns_builder_add_uint32 (soa_rdata, 0); /* serial */ + dns_builder_add_uint32 (soa_rdata, 0); /* refresh */ + dns_builder_add_uint32 (soa_rdata, 0); /* retry */ + dns_builder_add_uint32 (soa_rdata, 0); /* expire */ + dns_builder_add_uint32 (soa_rdata, 0); /* minimum */ + dns_builder_add_answer_data (answer, soa_rdata); + g_byte_array_unref (soa_rdata); + + assert_query_succeeds ("example.org", G_RESOLVER_RECORD_SOA, answer, + "('mname.example.org', 'rname.example.org', @u 0, @u 0, @u 0, @u 0, @u 0)"); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_soa_invalid_mname (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *soa_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_SOA)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* SOA rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.13 + * + * Use an invalid domain to trigger parsing failure. */ + soa_rdata = g_byte_array_new (); + dns_builder_add_invalid_domain (soa_rdata, answer->len + 2); /* mname */ + dns_builder_add_domain (soa_rdata, "rname.example.org"); + dns_builder_add_uint32 (soa_rdata, 0); /* serial */ + dns_builder_add_uint32 (soa_rdata, 0); /* refresh */ + dns_builder_add_uint32 (soa_rdata, 0); /* retry */ + dns_builder_add_uint32 (soa_rdata, 0); /* expire */ + dns_builder_add_uint32 (soa_rdata, 0); /* minimum */ + dns_builder_add_answer_data (answer, soa_rdata); + g_byte_array_unref (soa_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_SOA, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_soa_invalid_rname (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *soa_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_SOA)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* SOA rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.13 + * + * Use an invalid domain to trigger parsing failure. */ + soa_rdata = g_byte_array_new (); + dns_builder_add_domain (soa_rdata, "mname.example.org"); + dns_builder_add_invalid_domain (soa_rdata, answer->len + 2); /* rname */ + dns_builder_add_uint32 (soa_rdata, 0); /* serial */ + dns_builder_add_uint32 (soa_rdata, 0); /* refresh */ + dns_builder_add_uint32 (soa_rdata, 0); /* retry */ + dns_builder_add_uint32 (soa_rdata, 0); /* expire */ + dns_builder_add_uint32 (soa_rdata, 0); /* minimum */ + dns_builder_add_answer_data (answer, soa_rdata); + g_byte_array_unref (soa_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_SOA, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_soa_invalid_too_short (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *soa_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_SOA)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* SOA rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.13 + * + * Miss out one of the fields to trigger a failure. */ + soa_rdata = g_byte_array_new (); + dns_builder_add_domain (soa_rdata, "mname.example.org"); + dns_builder_add_domain (soa_rdata, "rname.example.org"); + dns_builder_add_uint32 (soa_rdata, 0); /* serial */ + dns_builder_add_uint32 (soa_rdata, 0); /* refresh */ + dns_builder_add_uint32 (soa_rdata, 0); /* retry */ + dns_builder_add_uint32 (soa_rdata, 0); /* expire */ + /* missing minimum field */ + dns_builder_add_answer_data (answer, soa_rdata); + g_byte_array_unref (soa_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_SOA, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_txt_valid (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *txt_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_TXT)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* TXT rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.14 */ + txt_rdata = g_byte_array_new (); + dns_builder_add_length_prefixed_string (txt_rdata, "some test content"); + dns_builder_add_answer_data (answer, txt_rdata); + g_byte_array_unref (txt_rdata); + + assert_query_succeeds ("example.org", G_RESOLVER_RECORD_TXT, answer, + "(['some test content'],)"); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_txt_valid_multiple_strings (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *txt_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_TXT)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* TXT rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.14 */ + txt_rdata = g_byte_array_new (); + dns_builder_add_length_prefixed_string (txt_rdata, "some test content"); + dns_builder_add_length_prefixed_string (txt_rdata, "more test content"); + dns_builder_add_answer_data (answer, txt_rdata); + g_byte_array_unref (txt_rdata); + + assert_query_succeeds ("example.org", G_RESOLVER_RECORD_TXT, answer, + "(['some test content', 'more test content'],)"); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_txt_invalid_empty (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *txt_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_TXT)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* TXT rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.14 + * + * Provide zero character strings (i.e. an empty rdata section) to trigger + * failure. */ + txt_rdata = g_byte_array_new (); + dns_builder_add_answer_data (answer, txt_rdata); + g_byte_array_unref (txt_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_TXT, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_txt_invalid_overflow (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *txt_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_TXT)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* TXT rdata, https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.14 + * + * Use a character string whose length exceeds the remaining length in the + * answer record, to trigger failure. */ + txt_rdata = g_byte_array_new (); + dns_builder_add_uint8 (txt_rdata, 10); /* length, but no content */ + dns_builder_add_answer_data (answer, txt_rdata); + g_byte_array_unref (txt_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_TXT, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_srv_valid (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *srv_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_SRV)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* SRV rdata, https://datatracker.ietf.org/doc/html/rfc2782 */ + srv_rdata = g_byte_array_new (); + dns_builder_add_uint16 (srv_rdata, 0); /* priority */ + dns_builder_add_uint16 (srv_rdata, 0); /* weight */ + dns_builder_add_uint16 (srv_rdata, 0); /* port */ + dns_builder_add_domain (srv_rdata, "target.example.org"); + dns_builder_add_answer_data (answer, srv_rdata); + g_byte_array_unref (srv_rdata); + + assert_query_succeeds ("example.org", G_RESOLVER_RECORD_SRV, answer, + "(@q 0, @q 0, @q 0, 'target.example.org')"); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_srv_invalid (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *srv_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_SRV)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* SRV rdata, https://datatracker.ietf.org/doc/html/rfc2782 + * + * Use an invalid domain to trigger parsing failure. */ + srv_rdata = g_byte_array_new (); + dns_builder_add_uint16 (srv_rdata, 0); /* priority */ + dns_builder_add_uint16 (srv_rdata, 0); /* weight */ + dns_builder_add_uint16 (srv_rdata, 0); /* port */ + dns_builder_add_invalid_domain (srv_rdata, answer->len + 2); + dns_builder_add_answer_data (answer, srv_rdata); + g_byte_array_unref (srv_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_SRV, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_srv_invalid_too_short (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *srv_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_SRV)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* SRV rdata, https://datatracker.ietf.org/doc/html/rfc2782 + * + * Miss out the target field to trigger failure */ + srv_rdata = g_byte_array_new (); + dns_builder_add_uint16 (srv_rdata, 0); /* priority */ + dns_builder_add_uint16 (srv_rdata, 0); /* weight */ + dns_builder_add_uint16 (srv_rdata, 0); /* port */ + /* missing target field */ + dns_builder_add_answer_data (answer, srv_rdata); + g_byte_array_unref (srv_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_SRV, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +static void +test_srv_invalid_too_short2 (void) +{ +#ifndef HAVE_DN_COMP + g_test_skip ("The dn_comp() function was not available."); + return; +#else + GByteArray *answer = NULL, *srv_rdata = NULL; + + answer = dns_header (); + + /* Resource record */ + dns_builder_add_domain (answer, "example.org"); + dns_builder_add_uint16 (answer, g_resolver_record_type_to_rrtype (G_RESOLVER_RECORD_SRV)); + dns_builder_add_uint16 (answer, 1); /* qclass=C_IN */ + dns_builder_add_uint32 (answer, 0); /* ttl (ignored) */ + + /* SRV rdata, https://datatracker.ietf.org/doc/html/rfc2782 + * + * Miss out the target and port fields to trigger failure */ + srv_rdata = g_byte_array_new (); + dns_builder_add_uint16 (srv_rdata, 0); /* priority */ + dns_builder_add_uint16 (srv_rdata, 0); /* weight */ + /* missing port and target fields */ + dns_builder_add_answer_data (answer, srv_rdata); + g_byte_array_unref (srv_rdata); + + assert_query_fails ("example.org", G_RESOLVER_RECORD_SRV, answer); + + g_byte_array_free (answer, TRUE); +#endif +} + +int +main (int argc, + char *argv[]) +{ + g_test_init (&argc, &argv, G_TEST_OPTION_ISOLATE_DIRS, NULL); + + g_test_add_func ("/gresolver/invalid-header", test_invalid_header); + g_test_add_func ("/gresolver/unknown-record-type", test_unknown_record_type); + g_test_add_func ("/gresolver/mx/valid", test_mx_valid); + g_test_add_func ("/gresolver/mx/invalid", test_mx_invalid); + g_test_add_func ("/gresolver/mx/invalid/too-short", test_mx_invalid_too_short); + g_test_add_func ("/gresolver/mx/invalid/too-short2", test_mx_invalid_too_short2); + g_test_add_func ("/gresolver/ns/valid", test_ns_valid); + g_test_add_func ("/gresolver/ns/invalid", test_ns_invalid); + g_test_add_func ("/gresolver/soa/valid", test_soa_valid); + g_test_add_func ("/gresolver/soa/invalid/mname", test_soa_invalid_mname); + g_test_add_func ("/gresolver/soa/invalid/rname", test_soa_invalid_rname); + g_test_add_func ("/gresolver/soa/invalid/too-short", test_soa_invalid_too_short); + g_test_add_func ("/gresolver/srv/valid", test_srv_valid); + g_test_add_func ("/gresolver/srv/invalid", test_srv_invalid); + g_test_add_func ("/gresolver/srv/invalid/too-short", test_srv_invalid_too_short); + g_test_add_func ("/gresolver/srv/invalid/too-short2", test_srv_invalid_too_short2); + g_test_add_func ("/gresolver/txt/valid", test_txt_valid); + g_test_add_func ("/gresolver/txt/valid/multiple-strings", test_txt_valid_multiple_strings); + g_test_add_func ("/gresolver/txt/invalid/empty", test_txt_invalid_empty); + g_test_add_func ("/gresolver/txt/invalid/overflow", test_txt_invalid_overflow); + + return g_test_run (); +} diff --git a/gio/tests/resolver.c b/gio/tests/resolver.c index 6e0c4d73b..d62a4fd18 100644 --- a/gio/tests/resolver.c +++ b/gio/tests/resolver.c @@ -44,12 +44,12 @@ static G_NORETURN void usage (void) { fprintf (stderr, "Usage: resolver [-s] [hostname | IP | service/protocol/domain ] ...\n"); - fprintf (stderr, "Usage: resolver [-s] [-t MX|TXT|NS|SOA] rrname ...\n"); + fprintf (stderr, "Usage: resolver [-s] [-t MX|TXT|NS|SOA|SRV] rrname ...\n"); fprintf (stderr, " resolver [-s] -c NUMBER [hostname | IP | service/protocol/domain ]\n"); fprintf (stderr, " Use -s to do synchronous lookups.\n"); fprintf (stderr, " Use -c NUMBER (and only a single resolvable argument) to test GSocketConnectable.\n"); fprintf (stderr, " The given NUMBER determines how many times the connectable will be enumerated.\n"); - fprintf (stderr, " Use -t with MX, TXT, NS or SOA to look up DNS records of those types.\n"); + fprintf (stderr, " Use -t with MX, TXT, NS, SOA or SRV to look up DNS records of those types.\n"); exit (1); } @@ -232,6 +232,46 @@ print_resolved_txt (const char *rrname, G_UNLOCK (response); } +static void +print_resolved_srv (const char *rrname, + GList *records, + GError *error) +{ + G_LOCK (response); + printf ("Domain: %s\n", rrname); + if (error) + { + printf ("Error: %s\n", error->message); + g_error_free (error); + } + else if (!records) + { + printf ("no SRV records\n"); + } + else + { + GList *t; + + for (t = records; t != NULL; t = t->next) + { + guint16 priority, weight, port; + const gchar *target; + + g_variant_get (t->data, "(qqq&s)", &priority, &weight, &port, &target); + + printf ("%s (priority %u, weight %u, port %u)\n", + target, (guint) priority, (guint) weight, (guint) port); + g_variant_unref (t->data); + } + + g_list_free (records); + } + printf ("\n"); + + done_lookup (); + G_UNLOCK (response); +} + static void print_resolved_soa (const char *rrname, GList *records, @@ -331,6 +371,9 @@ lookup_one_sync (const char *arg) case G_RESOLVER_RECORD_TXT: print_resolved_txt (arg, records, error); break; + case G_RESOLVER_RECORD_SRV: + print_resolved_srv (arg, records, error); + break; default: g_warn_if_reached (); break; @@ -449,6 +492,9 @@ lookup_records_callback (GObject *source, case G_RESOLVER_RECORD_TXT: print_resolved_txt (arg, records, error); break; + case G_RESOLVER_RECORD_SRV: + print_resolved_srv (arg, records, error); + break; default: g_warn_if_reached (); break; @@ -659,9 +705,11 @@ record_type_arg (const gchar *option_name, record_type = G_RESOLVER_RECORD_SOA; } else if (g_ascii_strcasecmp (value, "NS") == 0) { record_type = G_RESOLVER_RECORD_NS; + } else if (g_ascii_strcasecmp (value, "SRV") == 0) { + record_type = G_RESOLVER_RECORD_SRV; } else { g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, - "Specify MX, TXT, NS or SOA for the special record lookup types"); + "Specify MX, TXT, NS, SOA or SRV for the special record lookup types"); return FALSE; }