From 19e8effd134678d9467e328090717b10cef0c208 Mon Sep 17 00:00:00 2001 From: Jukka Rissanen Date: Thu, 5 Jan 2012 11:42:08 +0200 Subject: [PATCH] dhcpv6: Support stateless DHCPv6 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 | 2 +- gdhcp/client.c | 34 +++++ gdhcp/gdhcp.h | 2 + src/connman.h | 5 + src/dhcpv6.c | 430 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.c | 2 + src/network.c | 97 ++++++++++++- 7 files changed, 564 insertions(+), 8 deletions(-) create mode 100644 src/dhcpv6.c diff --git a/Makefile.am b/Makefile.am index ad9f70d..a046902 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 \ diff --git a/gdhcp/client.c b/gdhcp/client.c index be8905c..32ed4aa 100644 --- a/gdhcp/client.c +++ b/gdhcp/client.c @@ -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; } diff --git a/gdhcp/gdhcp.h b/gdhcp/gdhcp.h index 5c0e2ee..c0385d4 100644 --- a/gdhcp/gdhcp.h +++ b/gdhcp/gdhcp.h @@ -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, diff --git a/src/connman.h b/src/connman.h index 6f47be6..ae24217 100644 --- a/src/connman.h +++ b/src/connman.h @@ -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 index 0000000..bedcc7f --- /dev/null +++ b/src/dhcpv6.c @@ -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 +#endif +#include +#include +#include +#include + +#include +#include + +#include + +#include + +#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; +} diff --git a/src/main.c b/src/main.c index d3b213c..adcef12 100644 --- a/src/main.c +++ b/src/main.c @@ -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(); diff --git a/src/network.c b/src/network.c index bf522c1..6dee555 100644 --- a/src/network.c +++ b/src/network.c @@ -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: -- 2.7.4