dhcpv6: Support stateless DHCPv6
authorJukka Rissanen <jukka.rissanen@linux.intel.com>
Thu, 5 Jan 2012 09:42:08 +0000 (11:42 +0200)
committerDaniel Wagner <daniel.wagner@bmw-carit.de>
Thu, 5 Jan 2012 10:52:48 +0000 (11:52 +0100)
See relevant parts from these RFCs:
RFC 3315 - DHCP for IPv6
RFC 3646 - DNS configuration options for DHCP for IPv6
RFC 3736 - Stateless DHCP service for IPv6
RFC 4075 - SNTP configuration option for DHCPv6

The patch does not support authenticated information messages.

Makefile.am
gdhcp/client.c
gdhcp/gdhcp.h
src/connman.h
src/dhcpv6.c [new file with mode: 0644]
src/main.c
src/network.c

index ad9f70d..a046902 100644 (file)
@@ -74,7 +74,7 @@ src_connmand_SOURCES = $(gdbus_sources) $(gdhcp_sources) \
                        src/clock.c src/timezone.c \
                        src/agent.c src/notifier.c src/provider.c \
                        src/resolver.c src/ipconfig.c src/detect.c src/inet.c \
-                       src/dhcp.c src/rtnl.c src/proxy.c \
+                       src/dhcp.c src/dhcpv6.c src/rtnl.c src/proxy.c \
                        src/utsname.c src/timeserver.c src/rfkill.c \
                        src/storage.c src/dbus.c src/config.c \
                        src/technology.c src/counter.c src/ntp.c \
index be8905c..32ed4aa 100644 (file)
@@ -254,6 +254,15 @@ static void add_dhcpv6_request_options(GDHCPClient *dhcp_client,
                        (*ptr_buf) += len;
                        break;
 
+               case G_DHCPV6_ORO:
+                       break;
+
+               case G_DHCPV6_DNS_SERVERS:
+                       break;
+
+               case G_DHCPV6_SNTP_SERVERS:
+                       break;
+
                default:
                        break;
                }
@@ -1335,8 +1344,33 @@ static GList *get_dhcpv6_option_value_list(GDHCPClient *dhcp_client,
                                        unsigned char *value)
 {
        GList *list = NULL;
+       char *str;
+       int i;
 
        switch (code) {
+       case G_DHCPV6_DNS_SERVERS:      /* RFC 3646, chapter 3 */
+       case G_DHCPV6_SNTP_SERVERS:     /* RFC 4075, chapter 4 */
+               if (len % 16) {
+                       debug(dhcp_client,
+                               "%s server list length (%d) is invalid",
+                               code == G_DHCPV6_DNS_SERVERS ? "DNS" : "SNTP",
+                               len);
+                       return NULL;
+               }
+               for (i = 0; i < len; i += 16) {
+
+                       str = g_try_malloc0(INET6_ADDRSTRLEN+1);
+                       if (str == NULL)
+                               return list;
+
+                       if (inet_ntop(AF_INET6, &value[i], str,
+                                       INET6_ADDRSTRLEN) == NULL)
+                               g_free(str);
+                       else
+                               list = g_list_append(list, str);
+               }
+               break;
+
        default:
                break;
        }
index 5c0e2ee..c0385d4 100644 (file)
@@ -73,6 +73,8 @@ typedef enum {
 #define G_DHCPV6_SERVERID      2
 #define G_DHCPV6_ORO           6
 #define G_DHCPV6_STATUS_CODE   13
+#define G_DHCPV6_DNS_SERVERS   23
+#define G_DHCPV6_SNTP_SERVERS  31
 
 typedef enum {
        G_DHCPV6_DUID_LLT = 1,
index 6f47be6..ae24217 100644 (file)
@@ -274,6 +274,11 @@ int __connman_dhcp_start(struct connman_network *network, dhcp_cb callback);
 void __connman_dhcp_stop(struct connman_network *network);
 int __connman_dhcp_init(void);
 void __connman_dhcp_cleanup(void);
+int __connman_dhcpv6_init(void);
+void __connman_dhcpv6_cleanup(void);
+int __connman_dhcpv6_start_info(struct connman_network *network,
+                               dhcp_cb callback);
+void __connman_dhcpv6_stop(struct connman_network *network);
 
 int __connman_ipv4_init(void);
 void __connman_ipv4_cleanup(void);
diff --git a/src/dhcpv6.c b/src/dhcpv6.c
new file mode 100644 (file)
index 0000000..bedcc7f
--- /dev/null
@@ -0,0 +1,430 @@
+/*
+ *
+ *  Connection Manager
+ *
+ *  Copyright (C) 2011  Intel Corporation. All rights reserved.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License version 2 as
+ *  published by the Free Software Foundation.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include <connman/ipconfig.h>
+#include <connman/storage.h>
+
+#include <gdhcp/gdhcp.h>
+
+#include <glib.h>
+
+#include "connman.h"
+
+/* Transmission params in msec, RFC 3315 chapter 5.5 */
+#define INF_MAX_DELAY  (1 * 1000)
+#define INF_TIMEOUT    (1 * 1000)
+#define INF_MAX_RT     (120 * 1000)
+
+
+struct connman_dhcpv6 {
+       struct connman_network *network;
+       dhcp_cb callback;
+
+       char **nameservers;
+       char **timeservers;
+
+       GDHCPClient *dhcp_client;
+
+       guint timeout;
+       guint RT;       /* in msec */
+};
+
+static GHashTable *network_table;
+
+static inline float get_random()
+{
+       return (rand() % 200 - 100) / 1000.0;
+}
+
+/* Calculate a random delay, RFC 3315 chapter 14 */
+/* RT and MRT are milliseconds */
+static guint calc_delay(guint RT, guint MRT)
+{
+       float delay = get_random();
+       float rt = RT * (2 + delay);
+
+       if (rt > MRT)
+               rt = MRT * (1 + delay);
+
+       if (rt < 0)
+               rt = MRT;
+
+       return (guint)rt;
+}
+
+static void dhcpv6_free(struct connman_dhcpv6 *dhcp)
+{
+       g_strfreev(dhcp->nameservers);
+       g_strfreev(dhcp->timeservers);
+
+       dhcp->nameservers = NULL;
+       dhcp->timeservers = NULL;
+}
+
+static gboolean compare_string_arrays(char **array_a, char **array_b)
+{
+       int i;
+
+       if (array_a == NULL || array_b == NULL)
+               return FALSE;
+
+       if (g_strv_length(array_a) != g_strv_length(array_b))
+               return FALSE;
+
+       for (i = 0; array_a[i] != NULL && array_b[i] != NULL; i++)
+               if (g_strcmp0(array_a[i], array_b[i]) != 0)
+                       return FALSE;
+
+       return TRUE;
+}
+
+static void dhcpv6_debug(const char *str, void *data)
+{
+       connman_info("%s: %s\n", (const char *) data, str);
+}
+
+static gchar *convert_to_hex(unsigned char *buf, int len)
+{
+       gchar *ret = g_try_malloc(len * 2 + 1);
+       int i;
+
+       for (i = 0; ret != NULL && i < len; i++)
+               g_snprintf(ret + i * 2, 3, "%02x", buf[i]);
+
+       return ret;
+}
+
+/*
+ * DUID should not change over time so save it to file.
+ * See RFC 3315 chapter 9 for details.
+ */
+static int set_duid(struct connman_service *service,
+                       struct connman_network *network,
+                       GDHCPClient *dhcp_client, int index)
+{
+       GKeyFile *keyfile;
+       const char *ident;
+       char *hex_duid;
+       unsigned char *duid;
+       int duid_len;
+
+       ident = __connman_service_get_ident(service);
+
+       keyfile = connman_storage_load_service(ident);
+       if (keyfile == NULL)
+               return -EINVAL;
+
+       hex_duid = g_key_file_get_string(keyfile, ident, "IPv6.DHCP.DUID",
+                                       NULL);
+       if (hex_duid != NULL) {
+               unsigned int i, j = 0, hex;
+               size_t hex_duid_len = strlen(hex_duid);
+
+               duid = g_try_malloc0(hex_duid_len / 2);
+               if (duid == NULL) {
+                       g_key_file_free(keyfile);
+                       g_free(hex_duid);
+                       return -ENOMEM;
+               }
+
+               for (i = 0; i < hex_duid_len; i += 2) {
+                       sscanf(hex_duid + i, "%02x", &hex);
+                       duid[j++] = hex;
+               }
+
+               duid_len = hex_duid_len / 2;
+       } else {
+               int ret;
+               int type = __connman_ipconfig_get_type_from_index(index);
+
+               ret = g_dhcpv6_create_duid(G_DHCPV6_DUID_LLT, index, type,
+                                       &duid, &duid_len);
+               if (ret < 0) {
+                       g_key_file_free(keyfile);
+                       return ret;
+               }
+
+               hex_duid = convert_to_hex(duid, duid_len);
+               if (hex_duid == NULL) {
+                       g_key_file_free(keyfile);
+                       return -ENOMEM;
+               }
+
+               g_key_file_set_string(keyfile, ident, "IPv6.DHCP.DUID",
+                               hex_duid);
+
+               __connman_storage_save_service(keyfile, ident);
+       }
+       g_free(hex_duid);
+
+       g_key_file_free(keyfile);
+
+       g_dhcpv6_client_set_duid(dhcp_client, duid, duid_len);
+
+       return 0;
+}
+
+static void info_req_cb(GDHCPClient *dhcp_client, gpointer user_data)
+{
+       struct connman_dhcpv6 *dhcp = user_data;
+       struct connman_service *service;
+       int entries, i;
+       GList *option, *list;
+       char **nameservers, **timeservers;
+
+       DBG("dhcpv6 information-request %p", dhcp);
+
+       service = __connman_service_lookup_from_network(dhcp->network);
+       if (service == NULL) {
+               connman_error("Can not lookup service");
+               return;
+       }
+
+       option = g_dhcp_client_get_option(dhcp_client, G_DHCPV6_DNS_SERVERS);
+       entries = g_list_length(option);
+
+       nameservers = g_try_new0(char *, entries + 1);
+       if (nameservers != NULL) {
+               for (i = 0, list = option; list; list = list->next, i++)
+                       nameservers[i] = g_strdup(list->data);
+       }
+
+       if (compare_string_arrays(nameservers, dhcp->nameservers) == FALSE) {
+               if (dhcp->nameservers != NULL) {
+                       for (i = 0; dhcp->nameservers[i] != NULL; i++)
+                               __connman_service_nameserver_remove(service,
+                                                       dhcp->nameservers[i],
+                                                       FALSE);
+                       g_strfreev(dhcp->nameservers);
+               }
+
+               dhcp->nameservers = nameservers;
+
+               for (i = 0; dhcp->nameservers[i] != NULL; i++)
+                       __connman_service_nameserver_append(service,
+                                               dhcp->nameservers[i],
+                                               FALSE);
+       } else
+               g_strfreev(nameservers);
+
+
+       option = g_dhcp_client_get_option(dhcp_client, G_DHCPV6_SNTP_SERVERS);
+       entries = g_list_length(option);
+
+       timeservers = g_try_new0(char *, entries + 1);
+       if (timeservers != NULL) {
+               for (i = 0, list = option; list; list = list->next, i++)
+                       timeservers[i] = g_strdup(list->data);
+       }
+
+       if (compare_string_arrays(timeservers, dhcp->timeservers) == FALSE) {
+               if (dhcp->timeservers != NULL) {
+                       for (i = 0; dhcp->timeservers[i] != NULL; i++)
+                               __connman_service_timeserver_remove(service,
+                                                       dhcp->timeservers[i]);
+                       g_strfreev(dhcp->timeservers);
+               }
+
+               dhcp->timeservers = timeservers;
+
+               for (i = 0; dhcp->timeservers[i] != NULL; i++)
+                       __connman_service_timeserver_append(service,
+                                                       dhcp->timeservers[i]);
+       } else
+               g_strfreev(timeservers);
+
+
+       if (dhcp->callback != NULL) {
+               uint16_t status = g_dhcpv6_client_get_status(dhcp_client);
+               dhcp->callback(dhcp->network, status == 0 ? TRUE : FALSE);
+       }
+}
+
+static int dhcpv6_info_request(struct connman_dhcpv6 *dhcp)
+{
+       struct connman_service *service;
+       GDHCPClient *dhcp_client;
+       GDHCPClientError error;
+       int index, ret;
+
+       DBG("dhcp %p", dhcp);
+
+       index = connman_network_get_index(dhcp->network);
+
+       dhcp_client = g_dhcp_client_new(G_DHCP_IPV6, index, &error);
+       if (error != G_DHCP_CLIENT_ERROR_NONE)
+               return -EINVAL;
+
+       if (getenv("CONNMAN_DHCPV6_DEBUG"))
+               g_dhcp_client_set_debug(dhcp_client, dhcpv6_debug, "DHCPv6");
+
+       service = __connman_service_lookup_from_network(dhcp->network);
+       if (service == NULL)
+               return -EINVAL;
+
+       ret = set_duid(service, dhcp->network, dhcp_client, index);
+       if (ret < 0)
+               return ret;
+
+       g_dhcp_client_set_request(dhcp_client, G_DHCPV6_CLIENTID);
+       g_dhcp_client_set_request(dhcp_client, G_DHCPV6_DNS_SERVERS);
+       g_dhcp_client_set_request(dhcp_client, G_DHCPV6_SNTP_SERVERS);
+
+       g_dhcpv6_client_set_oro(dhcp_client, 2, G_DHCPV6_DNS_SERVERS,
+                               G_DHCPV6_SNTP_SERVERS);
+
+       g_dhcp_client_register_event(dhcp_client,
+                       G_DHCP_CLIENT_EVENT_INFORMATION_REQ, info_req_cb, dhcp);
+
+       dhcp->dhcp_client = dhcp_client;
+
+       return g_dhcp_client_start(dhcp_client, NULL);
+}
+
+static int dhcpv6_release(struct connman_dhcpv6 *dhcp)
+{
+       DBG("dhcp %p", dhcp);
+
+       if (dhcp->timeout > 0) {
+               g_source_remove(dhcp->timeout);
+               dhcp->timeout = 0;
+       }
+
+       dhcpv6_free(dhcp);
+
+       if (dhcp->dhcp_client == NULL)
+               return 0;
+
+       g_dhcp_client_stop(dhcp->dhcp_client);
+       g_dhcp_client_unref(dhcp->dhcp_client);
+
+       dhcp->dhcp_client = NULL;
+
+       return 0;
+}
+
+static void remove_network(gpointer user_data)
+{
+       struct connman_dhcpv6 *dhcp = user_data;
+
+       DBG("dhcp %p", dhcp);
+
+       dhcpv6_release(dhcp);
+
+       g_free(dhcp);
+}
+
+static gboolean timeout_info_req(gpointer user_data)
+{
+       struct connman_dhcpv6 *dhcp = user_data;
+
+       dhcp->RT = calc_delay(dhcp->RT, INF_MAX_RT);
+
+       DBG("info RT timeout %d msec", dhcp->RT);
+
+       dhcp->timeout = g_timeout_add(dhcp->RT, timeout_info_req, dhcp);
+
+       g_dhcp_client_start(dhcp->dhcp_client, NULL);
+
+       return FALSE;
+}
+
+static gboolean start_info_req(gpointer user_data)
+{
+       struct connman_dhcpv6 *dhcp = user_data;
+
+       /* Set the retransmission timeout, RFC 3315 chapter 14 */
+       dhcp->RT = INF_TIMEOUT * (1 + get_random());
+
+       DBG("info initial RT timeout %d msec", dhcp->RT);
+
+       dhcp->timeout = g_timeout_add(dhcp->RT, timeout_info_req, dhcp);
+
+       dhcpv6_info_request(dhcp);
+
+       return FALSE;
+}
+
+int __connman_dhcpv6_start_info(struct connman_network *network,
+                               dhcp_cb callback)
+{
+       struct connman_dhcpv6 *dhcp;
+       int delay;
+
+       DBG("");
+
+       dhcp = g_try_new0(struct connman_dhcpv6, 1);
+       if (dhcp == NULL)
+               return -ENOMEM;
+
+       dhcp->network = network;
+       dhcp->callback = callback;
+
+       connman_network_ref(network);
+
+       g_hash_table_replace(network_table, network, dhcp);
+
+       /* Initial timeout, RFC 3315, 18.1.5 */
+       delay = rand() % 1000;
+
+       dhcp->timeout = g_timeout_add(delay, start_info_req, dhcp);
+
+       return 0;
+}
+
+void __connman_dhcpv6_stop(struct connman_network *network)
+{
+       DBG("");
+
+       if (network_table == NULL)
+               return;
+
+       if (g_hash_table_remove(network_table, network) == TRUE)
+               connman_network_unref(network);
+}
+
+int __connman_dhcpv6_init(void)
+{
+       DBG("");
+
+       srand(time(0));
+
+       network_table = g_hash_table_new_full(g_direct_hash, g_direct_equal,
+                                                       NULL, remove_network);
+
+       return 0;
+}
+
+void __connman_dhcpv6_cleanup(void)
+{
+       DBG("");
+
+       g_hash_table_destroy(network_table);
+       network_table = NULL;
+}
index d3b213c..adcef12 100644 (file)
@@ -359,6 +359,7 @@ int main(int argc, char *argv[])
 
        __connman_rtnl_start();
        __connman_dhcp_init();
+       __connman_dhcpv6_init();
        __connman_wpad_init();
        __connman_wispr_init();
        __connman_rfkill_init();
@@ -375,6 +376,7 @@ int main(int argc, char *argv[])
        __connman_rfkill_cleanup();
        __connman_wispr_cleanup();
        __connman_wpad_cleanup();
+       __connman_dhcpv6_cleanup();
        __connman_dhcp_cleanup();
        __connman_provider_cleanup();
        __connman_plugin_cleanup();
index bf522c1..6dee555 100644 (file)
@@ -45,6 +45,7 @@ struct connman_network {
        char *group;
        char *path;
        int index;
+       int router_solicit_count;
 
        struct connman_network_driver *driver;
        void *driver_data;
@@ -767,7 +768,8 @@ void connman_network_clear_error(struct connman_network *network)
        __connman_service_clear_error(service);
 }
 
-static void set_configuration(struct connman_network *network)
+static void set_configuration(struct connman_network *network,
+                       enum connman_ipconfig_type type)
 {
        struct connman_service *service;
 
@@ -783,7 +785,7 @@ static void set_configuration(struct connman_network *network)
        service = __connman_service_lookup_from_network(network);
        __connman_service_ipconfig_indicate_state(service,
                                        CONNMAN_SERVICE_STATE_CONFIGURATION,
-                                       CONNMAN_IPCONFIG_TYPE_IPV4);
+                                       type);
 }
 
 static void dhcp_success(struct connman_network *network)
@@ -852,7 +854,7 @@ static int set_connected_fixed(struct connman_network *network)
 
        ipconfig_ipv4 = __connman_service_get_ip4config(service);
 
-       set_configuration(network);
+       set_configuration(network, CONNMAN_IPCONFIG_TYPE_IPV4);
 
        network->connecting = FALSE;
 
@@ -890,7 +892,7 @@ static void set_connected_manual(struct connman_network *network)
        if (__connman_ipconfig_get_local(ipconfig) == NULL)
                __connman_service_read_ip4config(service);
 
-       set_configuration(network);
+       set_configuration(network, CONNMAN_IPCONFIG_TYPE_IPV4);
 
        err = __connman_ipconfig_address_add(ipconfig);
        if (err < 0)
@@ -918,7 +920,7 @@ static int set_connected_dhcp(struct connman_network *network)
 
        DBG("network %p", network);
 
-       set_configuration(network);
+       set_configuration(network, CONNMAN_IPCONFIG_TYPE_IPV4);
 
        err = __connman_dhcp_start(network, dhcp_callback);
        if (err < 0) {
@@ -967,17 +969,80 @@ static int manual_ipv6_set(struct connman_network *network,
        return 0;
 }
 
+static void stop_dhcpv6(struct connman_network *network)
+{
+       __connman_dhcpv6_stop(network);
+}
+
+static void dhcpv6_info_callback(struct connman_network *network,
+                               connman_bool_t success)
+{
+       DBG("success %d", success);
+
+       stop_dhcpv6(network);
+}
+
+static void check_dhcpv6(struct nd_router_advert *reply, void *user_data)
+{
+       struct connman_network *network = user_data;
+
+       DBG("reply %p", reply);
+
+       if (reply == NULL) {
+               /*
+                * Router solicitation message seem to get lost easily so
+                * try to send it again.
+                */
+               if (network->router_solicit_count > 0) {
+                       DBG("re-send router solicitation %d",
+                                               network->router_solicit_count);
+                       network->router_solicit_count--;
+                       __connman_inet_ipv6_send_rs(network->index, 1,
+                                               check_dhcpv6, network);
+                       return;
+               }
+               connman_network_unref(network);
+               return;
+       }
+
+       network->router_solicit_count = 0;
+
+       /* We do stateless DHCPv6 only if router advertisement says so */
+       if (reply->nd_ra_flags_reserved & ND_RA_FLAG_OTHER)
+               __connman_dhcpv6_start_info(network, dhcpv6_info_callback);
+
+       connman_network_unref(network);
+}
+
 static void autoconf_ipv6_set(struct connman_network *network)
 {
+       struct connman_service *service;
+       struct connman_ipconfig *ipconfig;
+       int index;
+
        DBG("network %p", network);
 
        __connman_device_set_network(network->device, network);
 
        connman_device_set_disconnected(network->device, FALSE);
 
-       /* XXX: Append IPv6 nameservers here */
-
        network->connecting = FALSE;
+
+       service = __connman_service_lookup_from_network(network);
+       if (service == NULL)
+               return;
+
+       ipconfig = __connman_service_get_ip6config(service);
+       if (ipconfig == NULL)
+               return;
+
+       index = connman_ipconfig_get_index(ipconfig);
+
+       connman_network_ref(network);
+
+       /* Try to get stateless DHCPv6 information, RFC 3736 */
+       network->router_solicit_count = 3;
+       __connman_inet_ipv6_send_rs(index, 1, check_dhcpv6, network);
 }
 
 static gboolean set_connected(gpointer user_data)
@@ -1050,8 +1115,26 @@ static gboolean set_connected(gpointer user_data)
        } else {
                enum connman_service_state state;
 
+               /*
+                * Resetting solicit count here will prevent the RS resend loop
+                * from sending packets in check_dhcpv6()
+                */
+               network->router_solicit_count = 0;
+
                __connman_device_set_network(network->device, NULL);
 
+               switch (ipv6_method) {
+               case CONNMAN_IPCONFIG_METHOD_UNKNOWN:
+               case CONNMAN_IPCONFIG_METHOD_OFF:
+               case CONNMAN_IPCONFIG_METHOD_FIXED:
+               case CONNMAN_IPCONFIG_METHOD_MANUAL:
+               case CONNMAN_IPCONFIG_METHOD_DHCP:
+                       break;
+               case CONNMAN_IPCONFIG_METHOD_AUTO:
+                       stop_dhcpv6(network);
+                       break;
+               }
+
                switch (ipv4_method) {
                case CONNMAN_IPCONFIG_METHOD_UNKNOWN:
                case CONNMAN_IPCONFIG_METHOD_OFF: