ip6: support for DNS Search List option (bgo #637077)
authorPierre Ossman <pierre@ossman.eu>
Mon, 13 Dec 2010 21:28:53 +0000 (22:28 +0100)
committerDan Williams <dcbw@redhat.com>
Wed, 15 Dec 2010 23:44:20 +0000 (17:44 -0600)
RFC6101 adds the DNS Search List option to router advertisements. This
allows stateless configuration of suffixes to try when doing DNS lookups.
Make sure we catch these when provided by the kernel and reconfigure
things appropriately.

NOTE: this commit depends on a kernel patch:

http://marc.info/?l=linux-netdev&m=129216173321352&w=2

src/ip6-manager/nm-ip6-manager.c

index ae748ca..ea93f02 100644 (file)
@@ -76,6 +76,11 @@ typedef struct {
        time_t expires;
 } NMIP6RDNSS;
 
+typedef struct {
+       char domain[256];
+       time_t expires;
+} NMIP6DNSSL;
+
 /******************************************************************/
 
 typedef struct {
@@ -97,6 +102,9 @@ typedef struct {
        GArray *rdnss_servers;
        guint rdnss_timeout_id;
 
+       GArray *dnssl_domains;
+       guint dnssl_timeout_id;
+
        guint ip6flags_poll_id;
 
        guint32 ra_flags;
@@ -122,6 +130,10 @@ nm_ip6_device_destroy (NMIP6Device *device)
                g_array_free (device->rdnss_servers, TRUE);
        if (device->rdnss_timeout_id)
                g_source_remove (device->rdnss_timeout_id);
+       if (device->dnssl_domains)
+               g_array_free (device->dnssl_domains, TRUE);
+       if (device->dnssl_timeout_id)
+               g_source_remove (device->dnssl_timeout_id);
        if (device->ip6flags_poll_id)
                g_source_remove (device->ip6flags_poll_id);
 
@@ -155,6 +167,8 @@ nm_ip6_device_new (NMIP6Manager *manager, int ifindex)
 
        device->rdnss_servers = g_array_new (FALSE, FALSE, sizeof (NMIP6RDNSS));
 
+       device->dnssl_domains = g_array_new (FALSE, FALSE, sizeof (NMIP6DNSSL));
+
        g_hash_table_replace (priv->devices, GINT_TO_POINTER (device->ifindex), device);
 
        /* and the original value of IPv6 enable/disable */
@@ -300,6 +314,61 @@ set_rdnss_timeout (NMIP6Device *device)
        }
 }
 
+static void set_dnssl_timeout (NMIP6Device *device);
+
+static gboolean
+dnssl_expired (gpointer user_data)
+{
+       NMIP6Device *device = user_data;
+       CallbackInfo info = { device, IP6_DHCP_OPT_NONE };
+
+       nm_log_dbg (LOGD_IP6, "(%s): IPv6 DNSSL information expired", device->iface);
+
+       set_dnssl_timeout (device);
+       emit_config_changed (&info);
+       return FALSE;
+}
+
+static void
+set_dnssl_timeout (NMIP6Device *device)
+{
+       time_t expires = 0, now = time (NULL);
+       NMIP6DNSSL *dnssl;
+       int i;
+
+       if (device->dnssl_timeout_id) {
+               g_source_remove (device->dnssl_timeout_id);
+               device->dnssl_timeout_id = 0;
+       }
+
+       /* Find the soonest expiration time. */
+       for (i = 0; i < device->dnssl_domains->len; i++) {
+               dnssl = &g_array_index (device->dnssl_domains, NMIP6DNSSL, i);
+               if (dnssl->expires == 0)
+                       continue;
+
+               /* If the entry has already expired, remove it; the "+ 1" is
+                * because g_timeout_add_seconds() might fudge the timing a
+                * bit.
+                */
+               if (dnssl->expires <= now + 1) {
+                       nm_log_dbg (LOGD_IP6, "(%s): removing expired RA-provided domain %s",
+                                   device->iface, dnssl->domain);
+                       g_array_remove_index (device->dnssl_domains, i--);
+                       continue;
+               }
+
+               if (!expires || dnssl->expires < expires)
+                       expires = dnssl->expires;
+       }
+
+       if (expires) {
+               device->dnssl_timeout_id = g_timeout_add_seconds (expires - now,
+                                                                 dnssl_expired,
+                                                                 device);
+       }
+}
+
 static CallbackInfo *
 callback_info_new (NMIP6Device *device, guint dhcp_opts, gboolean success)
 {
@@ -569,6 +638,8 @@ process_prefix (NMIP6Manager *manager, struct nl_msg *msg)
  */
 
 #define ND_OPT_RDNSS 25
+#define ND_OPT_DNSSL 31
+
 struct nd_opt_rdnss {
        uint8_t nd_opt_rdnss_type;
        uint8_t nd_opt_rdnss_len;
@@ -577,6 +648,14 @@ struct nd_opt_rdnss {
        /* followed by one or more IPv6 addresses */
 } __attribute__ ((packed));
 
+struct nd_opt_dnssl {
+       uint8_t nd_opt_dnssl_type;
+       uint8_t nd_opt_dnssl_len;
+       uint16_t nd_opt_dnssl_reserved1;
+       uint32_t nd_opt_dnssl_lifetime;
+       /* followed by one or more suffixes */
+} __attribute__ ((packed));
+
 static gboolean
 process_nduseropt_rdnss (NMIP6Device *device, struct nd_opt_hdr *opt)
 {
@@ -669,6 +748,149 @@ process_nduseropt_rdnss (NMIP6Device *device, struct nd_opt_hdr *opt)
        return changed;
 }
 
+static const char *
+parse_dnssl_domain (const unsigned char *buffer, size_t maxlen)
+{
+       static char domain[256];
+       size_t label_len;
+
+       domain[0] = '\0';
+
+       while (maxlen > 0) {
+               label_len = *buffer;
+               buffer++;
+               maxlen--;
+
+               if (label_len == 0)
+                       return domain;
+
+               if (label_len > maxlen)
+                       return NULL;
+               if ((sizeof (domain) - strlen (domain)) < (label_len + 2))
+                       return NULL;
+
+               if (domain[0] != '\0')
+                       strcat (domain, ".");
+               strncat (domain, (const char *)buffer, label_len);
+               buffer += label_len;
+               maxlen -= label_len;
+       }
+
+       return NULL;
+}
+
+static gboolean
+process_nduseropt_dnssl (NMIP6Device *device, struct nd_opt_hdr *opt)
+{
+       size_t opt_len;
+       struct nd_opt_dnssl *dnssl_opt;
+       unsigned char *opt_ptr;
+       time_t now = time (NULL);
+       GArray *new_domains;
+       NMIP6DNSSL domain, *cur_domain;
+       gboolean changed;
+       guint i;
+
+       opt_len = opt->nd_opt_len;
+
+       if (opt_len < 2)
+               return FALSE;
+
+       dnssl_opt = (struct nd_opt_dnssl *) opt;
+
+       opt_ptr = (unsigned char *)(dnssl_opt + 1);
+       opt_len = (opt_len - 1) * 8; /* prefer bytes for later handling */
+
+       new_domains = g_array_new (FALSE, FALSE, sizeof (NMIP6DNSSL));
+
+       changed = FALSE;
+
+       /* Pad the DNS server expiry somewhat to give a bit of slack in cases
+        * where one RA gets lost or something (which can happen on unreliable
+        * links like wifi where certain types of frames are not retransmitted).
+        * Note that 0 has special meaning and is therefore not adjusted.
+        */
+       domain.expires = ntohl (dnssl_opt->nd_opt_dnssl_lifetime);
+       if (domain.expires > 0)
+               domain.expires += now + 10;
+
+       while (opt_len) {
+               const char *domain_str;
+
+               domain_str = parse_dnssl_domain (opt_ptr, opt_len);
+               if (domain_str == NULL) {
+                       nm_log_dbg (LOGD_IP6, "(%s): invalid DNSSL option, parsing aborted",
+                                   device->iface);
+                       break;
+               }
+
+               /* The DNSSL encoding of domains happen to occupy the same size
+                * as the length of the resulting string, including terminating
+                * null. */
+               opt_ptr += strlen (domain_str) + 1;
+               opt_len -= strlen (domain_str) + 1;
+
+               /* Ignore empty domains. They're probably just padding... */
+               if (domain_str[0] == '\0')
+                       continue;
+
+               for (i = 0; i < device->dnssl_domains->len; i++) {
+                       cur_domain = &(g_array_index (device->dnssl_domains, NMIP6DNSSL, i));
+
+                       if (strcmp (domain_str, cur_domain->domain) != 0)
+                               continue;
+
+                       cur_domain->expires = domain.expires;
+
+                       if (domain.expires > 0) {
+                               nm_log_dbg (LOGD_IP6, "(%s): refreshing RA-provided domain %s (expires in %d seconds)",
+                                           device->iface, domain_str,
+                                           domain.expires - now);
+                               break;
+                       }
+
+                       nm_log_dbg (LOGD_IP6, "(%s): removing RA-provided domain %s on router request",
+                                   device->iface, domain_str);
+
+                       g_array_remove_index (device->dnssl_domains, i);
+                       changed = TRUE;
+                       break;
+               }
+
+               if (domain.expires == 0)
+                       continue;
+               if (i < device->dnssl_domains->len)
+                       continue;
+
+               nm_log_dbg (LOGD_IP6, "(%s): found RA-provided domain %s (expires in %d seconds)",
+                           device->iface, domain_str, domain.expires - now);
+
+               g_assert (strlen (domain_str) < sizeof (domain.domain));
+               strcpy (domain.domain, domain_str);
+               g_array_append_val (new_domains, domain);
+       }
+
+       /* New domains must be added in the order they are listed in the
+        * RA option and before any existing domains.
+        *
+        * Note: This is the place to remove domains if we want to cap the
+        *       number of domains. The RFC states that the one to expire
+        *       first of the existing domains should be removed.
+        */
+       if (new_domains->len) {
+               g_array_prepend_vals (device->dnssl_domains,
+                                     new_domains->data, new_domains->len);
+               changed = TRUE;
+       }
+
+       g_array_free (new_domains, TRUE);
+
+       /* Timeouts may have changed even if domains didn't */
+       set_dnssl_timeout (device);
+
+       return changed;
+}
+
 static NMIP6Device *
 process_nduseropt (NMIP6Manager *manager, struct nl_msg *msg)
 {
@@ -708,6 +930,9 @@ process_nduseropt (NMIP6Manager *manager, struct nl_msg *msg)
                case ND_OPT_RDNSS:
                        changed = process_nduseropt_rdnss (device, opt);
                        break;
+               case ND_OPT_DNSSL:
+                       changed = process_nduseropt_dnssl (device, opt);
+                       break;
                }
 
                opts_len -= opt->nd_opt_len << 3;
@@ -1049,6 +1274,14 @@ nm_ip6_manager_get_ip6_config (NMIP6Manager *manager, int ifindex)
                        nm_ip6_config_add_nameserver (config, &rdnss[i].addr);
        }
 
+       /* Add DNS domains */
+       if (device->dnssl_domains) {
+               NMIP6DNSSL *dnssl = (NMIP6DNSSL *)(device->dnssl_domains->data);
+
+               for (i = 0; i < device->dnssl_domains->len; i++)
+                       nm_ip6_config_add_domain (config, dnssl[i].domain);
+       }
+
        return config;
 }