| /* |
| * |
| * Connection Manager |
| * |
| * Copyright (C) 2010 BMW Car IT GmbH. All rights reserved. |
| * Copyright (C) 2011 Google, Inc. 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 |
| * |
| */ |
| |
| /* |
| * OpenVPN plugin built on top of vpn support. |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <string.h> |
| #include <errno.h> |
| #include <stdlib.h> |
| #include <unistd.h> |
| #include <stdio.h> |
| |
| #include <arpa/inet.h> |
| #include <net/if.h> |
| #include <netinet/in.h> |
| |
| #include <glib.h> |
| |
| #define CONNMAN_API_SUBJECT_TO_CHANGE |
| #include <connman/plugin.h> |
| #include <connman/dbus.h> |
| #include <connman/provider.h> |
| #include <connman/inet.h> |
| #include <connman/ipconfig.h> |
| #include <connman/log.h> |
| #include <connman/task.h> |
| |
| #ifdef ENABLE_NSS |
| #include "nss.h" |
| #endif |
| |
| #include "vpn.h" |
| |
| #define _DBG_VPN(fmt, arg...) DBG(DBG_VPN, fmt, ## arg) |
| |
| #define MAXDNS 10 /* max DNS servers per lease */ |
| #define MAXDOMAINS 10 /* max DNS search domains per lease */ |
| |
| enum openvpn_state { |
| OV_STATE_UNKNOWN, |
| OV_STATE_CONNECTING, |
| OV_STATE_WAIT, |
| OV_STATE_AUTH, |
| OV_STATE_GET_CONFIG, |
| OV_STATE_ASSIGN_IP, |
| OV_STATE_ADD_ROUTES, |
| OV_STATE_CONNECTED, |
| OV_STATE_RECONNECTING, |
| OV_STATE_RESOLVE, |
| OV_STATE_EXITING |
| }; |
| |
| static DBusConnection *connection; |
| |
| static const char *kLSBReleaseFilename = "/etc/lsb-release"; |
| static const char *kChromeOSReleaseName = "CHROMEOS_RELEASE_NAME"; |
| static const char *kChromeOSReleaseVersion = "CHROMEOS_RELEASE_VERSION"; |
| |
| static char *platform_name; |
| static char *platform_version; |
| |
| struct mgmt_server_data { |
| struct connman_provider *provider; |
| char *vpnhost; |
| struct sockaddr_in addr; |
| GIOChannel *channel; |
| guint watch; |
| char *username; |
| char *password; |
| char *state_id; |
| enum openvpn_state state; |
| }; |
| |
| struct openvpn_data { |
| struct mgmt_server_data *server; |
| struct connman_ipaddress ipaddr; |
| in_addr_t vpn_addr; |
| in_addr_t gw_addr; |
| uint32_t gw_index; |
| char *tls_auth_file; |
| }; |
| |
| struct ov_route { |
| char *host; |
| char *netmask; |
| char *gateway; |
| }; |
| |
| static void destroy_route(gpointer user_data) |
| { |
| struct ov_route *route = user_data; |
| |
| g_free(route->host); |
| g_free(route->netmask); |
| g_free(route->gateway); |
| g_free(route); |
| } |
| |
| static void ov_provider_append_routes(gpointer key, gpointer value, |
| gpointer user_data) |
| { |
| struct ov_route *route = value; |
| struct connman_provider *provider = user_data; |
| |
| connman_provider_append_route(provider, route->host, route->netmask, |
| route->gateway); |
| } |
| |
| static struct ov_route *ov_route_lookup(const char *key, const char *prefix_key, |
| GHashTable *routes) |
| { |
| unsigned long idx; |
| const char *start; |
| char *end; |
| struct ov_route *route; |
| |
| if (g_str_has_prefix(key, prefix_key) == FALSE) |
| return NULL; |
| |
| start = key + strlen(prefix_key); |
| idx = g_ascii_strtoull(start, &end, 10); |
| |
| if (idx == 0 && start == end) { |
| connman_error("%s: string conversion failed %s", |
| __func__, start); |
| return NULL; |
| } |
| |
| route = g_hash_table_lookup(routes, GINT_TO_POINTER(idx)); |
| if (route == NULL) { |
| route = g_try_new0(struct ov_route, 1); |
| if (route == NULL) { |
| connman_error("%s: out of memory", __func__); |
| return NULL; |
| } |
| |
| g_hash_table_replace(routes, GINT_TO_POINTER(idx), |
| route); |
| } |
| |
| return route; |
| } |
| |
| static void ov_append_route(const char *key, const char *value, GHashTable *routes) |
| { |
| struct ov_route *route; |
| |
| /* |
| * OpenVPN pushes routing tupples (host, nw, gw) as several |
| * environment values, e.g. |
| * |
| * route_gateway_2 = 10.242.2.13 |
| * route_netmask_2 = 255.255.0.0 |
| * route_network_2 = 192.168.0.0 |
| * route_gateway_1 = 10.242.2.13 |
| * route_netmask_1 = 255.255.255.255 |
| * route_network_1 = 10.242.2.1 |
| * |
| * The hash table is used to group the separate environment |
| * variables together. It also makes sure all tupples are |
| * complete even when OpenVPN pushes the information in a |
| * wrong order (unlikely). |
| */ |
| |
| route = ov_route_lookup(key, "route_network_", routes); |
| if (route != NULL) { |
| route->host = g_strdup(value); |
| return; |
| } |
| |
| route = ov_route_lookup(key, "route_netmask_", routes); |
| if (route != NULL) { |
| route->netmask = g_strdup(value); |
| return; |
| } |
| |
| route = ov_route_lookup(key, "route_gateway_", routes); |
| if (route != NULL) |
| route->gateway = g_strdup(value); |
| } |
| |
| static int mask2prefix(const char *str) |
| { |
| in_addr_t mask = inet_network(str); |
| int prefix; |
| |
| for (prefix = 0; mask != 0; prefix++) |
| mask <<= 1; |
| return prefix; |
| } |
| |
| struct dns_state { |
| char **servers; |
| int n_servers; |
| char **domains; |
| int n_domains; |
| }; |
| |
| static void parse_dns_cleanup(struct dns_state *dns) |
| { |
| g_strfreev(dns->domains); |
| g_strfreev(dns->servers); |
| g_free(dns); |
| } |
| |
| static struct dns_state *parse_dns_init(void) |
| { |
| struct dns_state *dns = g_try_new0(struct dns_state, 1); |
| if (dns == NULL) { |
| connman_error("%s: no memory for dns state", __func__); |
| return NULL; |
| } |
| dns->servers = g_try_new0(char *, MAXDNS + 1); |
| if (dns->servers == NULL) { |
| connman_error("%s: no memory for dns_servers", __func__); |
| goto bad; |
| } |
| dns->domains = g_try_new0(char *, MAXDNS + 1); |
| if (dns->domains == NULL) { |
| connman_error("%s: no memory for dns_domains", __func__); |
| goto bad; |
| } |
| return dns; |
| bad: |
| parse_dns_cleanup(dns); |
| return NULL; |
| } |
| |
| static void parse_dhcp_option(struct dns_state *dns, gchar **opts) |
| { |
| #define iseq(_a, _b) (g_ascii_strcasecmp((_a), (_b)) == 0) |
| if (opts[1] == NULL) |
| return; |
| if (iseq(opts[1], "DOMAIN") && opts[2] != NULL) { |
| if (dns->n_domains >= MAXDNS) { |
| connman_warn("%s: too many DNS search domains, " |
| "%s ignored", __func__, opts[2]); |
| return; |
| } |
| dns->domains[dns->n_domains++] = g_strdup(opts[2]); |
| } else if (iseq(opts[1], "DNS") && opts[2] != NULL) { |
| if (dns->n_servers >= MAXDNS) { |
| connman_warn("%s: too many DNS servers, %s ignored", |
| __func__, opts[2]); |
| return; |
| } |
| dns->servers[dns->n_servers++] = g_strdup(opts[2]); |
| } |
| #undef iseq |
| } |
| |
| static void set_string(char **storage, const char *str) |
| { |
| g_free(*storage); |
| *storage = g_strdup(str); |
| } |
| |
| struct foreign_option { |
| const char *key; |
| const char *value; |
| }; |
| |
| static gint foreign_option_compare(gconstpointer a, gconstpointer b, |
| gpointer user_data) { |
| const struct foreign_option *option_a = (const void *) a; |
| const struct foreign_option *option_b = (const void *) b; |
| return g_strcmp0(option_a->key, option_b->key); |
| } |
| |
| static void parse_foreign_option(gpointer opt, gpointer dns) { |
| const struct foreign_option *option = (const void *) opt; |
| gchar **options = g_strsplit(option->value, " ", 3); |
| |
| if (options[0] != NULL && g_ascii_strcasecmp(options[0], "dhcp-option") == 0) |
| parse_dhcp_option(dns, options); |
| g_strfreev(options); |
| } |
| |
| static int ov_notify(DBusMessage *msg, struct connman_provider *provider) |
| { |
| #define iseq(_a, _b) (g_ascii_strcasecmp((_a), (_b)) == 0) |
| struct openvpn_data *data = vpn_get_specific_data(provider); |
| DBusMessageIter iter, dict; |
| const char *reason, *key; |
| char *value; |
| GSequence *foreign_options; |
| GHashTable *routes; |
| struct dns_state *dns; |
| const char *trusted_ip = NULL; |
| |
| dbus_message_iter_init(msg, &iter); |
| |
| dbus_message_iter_get_basic(&iter, &reason); |
| dbus_message_iter_next(&iter); |
| |
| dbus_message_iter_init(msg, &iter); |
| |
| dbus_message_iter_get_basic(&iter, &reason); |
| dbus_message_iter_next(&iter); |
| |
| _DBG_VPN("reason %s provider %p", reason, provider); |
| |
| if (provider == NULL) { |
| connman_error("%s: no provider found", __func__); |
| return VPN_STATE_FAILURE; |
| } |
| |
| if (strcmp(reason, "up") != 0) |
| return VPN_STATE_DISCONNECT; |
| |
| dns = parse_dns_init(); |
| if (dns == NULL) { |
| connman_error("%s: no memory for dns state", __func__); |
| return VPN_STATE_FAILURE; |
| } |
| |
| foreign_options = g_sequence_new(NULL); |
| |
| routes = g_hash_table_new_full(g_direct_hash, g_direct_equal, |
| NULL, destroy_route); |
| |
| dbus_message_iter_recurse(&iter, &dict); |
| while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) { |
| DBusMessageIter entry; |
| |
| dbus_message_iter_recurse(&dict, &entry); |
| dbus_message_iter_get_basic(&entry, &key); |
| dbus_message_iter_next(&entry); |
| dbus_message_iter_get_basic(&entry, &value); |
| |
| _DBG_VPN("%s = %s", key, value); |
| |
| if (iseq(key, "ifconfig_local")) { |
| set_string(&data->ipaddr.local, value); |
| data->ipaddr.mask |= CONNMAN_IPCONFIG_LOCAL; |
| } else if (iseq(key, "ifconfig_broadcast")) { |
| set_string(&data->ipaddr.broadcast, value); |
| data->ipaddr.mask |= CONNMAN_IPCONFIG_BCAST; |
| } else if (iseq(key, "ifconfig_netmask")) { |
| data->ipaddr.prefixlen = mask2prefix(value); |
| data->ipaddr.mask |= CONNMAN_IPCONFIG_PREFIX; |
| } else if (iseq(key, "ifconfig_remote")) { |
| set_string(&data->ipaddr.peer, value); |
| data->ipaddr.mask |= CONNMAN_IPCONFIG_PEER; |
| } else if (iseq(key, "route_vpn_gateway")) { |
| set_string(&data->ipaddr.gateway, value); |
| data->ipaddr.mask |= CONNMAN_IPCONFIG_GW; |
| } else if (iseq(key, "trusted_ip")) { |
| trusted_ip = value; |
| } else if (iseq(key, "tun_mtu")) { |
| int mtu = (int) strtol(value, NULL, 10); |
| /* NB: restrict min MTU per dhcpcd-script */ |
| if (576 <= mtu) { |
| data->ipaddr.mtu = mtu; |
| data->ipaddr.mask |= CONNMAN_IPCONFIG_MTU; |
| } else |
| connman_warn("%s: MTU %s too small (ignored)", |
| __func__, value); |
| } else if (g_str_has_prefix(key, "foreign_option_") == TRUE) { |
| struct foreign_option *option = |
| g_new0(struct foreign_option, 1); |
| option->key = key; |
| option->value = value; |
| g_sequence_insert_sorted(foreign_options, |
| option, |
| foreign_option_compare, |
| NULL); |
| } else if (g_str_has_prefix(key, "route_") == TRUE) { |
| ov_append_route(key, value, routes); |
| } |
| |
| dbus_message_iter_next(&dict); |
| } |
| |
| if ((data->ipaddr.mask & CONNMAN_IPCONFIG_GW) && trusted_ip != NULL) { |
| /* |
| * If we are creating a new default gateway, we need |
| * to first pin a host route to the VPN server so that |
| * encapsulated packets continue to be sent to the VPN |
| * server endpoint. |
| * |
| * NB: route_net_gateway may have the gateway but we also |
| * need the interface index and both are returned by |
| * calling connman_inet_get_route so don't bother. |
| */ |
| _DBG_VPN("add host VPN server route for %s", trusted_ip); |
| data->vpn_addr = inet_addr(trusted_ip); |
| if (!connman_inet_get_route(data->vpn_addr, |
| &data->gw_index, |
| &data->gw_addr, |
| NULL)) { |
| connman_error("%s: unable to get route to %s", |
| __func__, trusted_ip); |
| return VPN_STATE_FAILURE; |
| } |
| connman_inet_add_hostroute(data->gw_index, data->vpn_addr, |
| data->gw_addr); |
| } |
| |
| g_sequence_foreach(foreign_options, parse_foreign_option, dns); |
| if (dns->n_servers > 0) { |
| data->ipaddr.dns_servers = dns->servers; |
| data->ipaddr.mask |= CONNMAN_IPCONFIG_DNS; |
| } |
| if (dns->n_domains > 0) { |
| data->ipaddr.search_domains = dns->domains; |
| data->ipaddr.mask |= CONNMAN_IPCONFIG_SEARCH; |
| } |
| connman_provider_ipconfig_set(provider, &data->ipaddr); |
| |
| parse_dns_cleanup(dns); |
| /* NB: dns configuration is sent for restarts */ |
| data->ipaddr.mask &= ~(CONNMAN_IPCONFIG_DNS|CONNMAN_IPCONFIG_SEARCH); |
| |
| g_sequence_free(foreign_options); |
| |
| g_hash_table_foreach(routes, ov_provider_append_routes, provider); |
| /* TODO(sleffler): routes are not sent for restarts */ |
| g_hash_table_destroy(routes); |
| |
| return VPN_STATE_CONNECT; |
| #undef iseq |
| } |
| |
| /* |
| * OpenVPN management channel support. |
| */ |
| |
| static const char *kOTPProperty = "OpenVPN.OTP"; |
| static const char *kPasswordProperty = "OpenVPN.Password"; |
| static const char *kPINProperty = "OpenVPN.Pkcs11.PIN"; |
| static const char *kUserProperty = "OpenVPN.User"; |
| |
| static void mgmt_event_need_auth(struct mgmt_server_data *data, |
| const char *tag); |
| static void mgmt_event_need_tpm_pin(struct mgmt_server_data *data, |
| const char *tag); |
| static void mgmt_event_need_static_challenge(struct mgmt_server_data *data, |
| const char *tag, char *cr); |
| static void mgmt_event_need_dynamic_challenge(struct mgmt_server_data *data, |
| const char *tag); |
| static void mgmt_close_channel(struct mgmt_server_data *data); |
| |
| static ssize_t mgmt_send(struct mgmt_server_data *data, const char *s, |
| ssize_t len, const char *func) |
| { |
| int fd = g_io_channel_unix_get_fd(data->channel); |
| ssize_t n = write(fd, s, len); |
| if (n < 0) |
| connman_error("%s: write error: %s", func, strerror(errno)); |
| else if (n != len) |
| connman_error("%s: write short, %zd != %zd", func, n, len); |
| return n; |
| } |
| |
| static void mgmt_send_username(struct mgmt_server_data *data, |
| const char *tag, const char *username, const char *func) |
| { |
| gchar *str = g_strdup_printf("username \"%s\" %s\n", tag, username); |
| |
| _DBG_VPN("tag %s username %s", tag, |
| connman_log_get_masked_value(kUserProperty, username)); |
| |
| mgmt_send(data, str, strlen(str), func); |
| g_free(str); |
| } |
| |
| static void mgmt_send_password(struct mgmt_server_data *data, |
| const char *tag, const char *password, const char *func) |
| { |
| gchar *str = g_strdup_printf("password \"%s\" \"%s\"\n", |
| tag, password); |
| |
| _DBG_VPN("tag %s password %s", tag, |
| connman_log_get_masked_value(kPasswordProperty, password)); |
| |
| mgmt_send(data, str, strlen(str), func); |
| g_free(str); |
| } |
| |
| /* |
| * Send a static challenge/response msg on receiving a |
| * respose; see the protocol description below. |
| */ |
| static void mgmt_send_static_challenge_response(struct mgmt_server_data *data, |
| const char *tag, const char *username, const char *password, |
| const char *otp) |
| { |
| gchar *reply; |
| |
| _DBG_VPN("tag %s username %s password %s otp %s", tag, |
| connman_log_get_masked_value(kUserProperty, username), |
| connman_log_get_masked_value(kPasswordProperty, password), |
| connman_log_get_masked_value(kOTPProperty, otp)); |
| |
| mgmt_send_username(data, tag, username, __func__); |
| |
| reply = g_strdup_printf("SCRV1:%s:%s", password, otp); |
| mgmt_send_password(data, tag, reply, __func__); |
| g_free(reply); |
| } |
| |
| /* |
| * Send a dynamic challenge/response msg on receiving a |
| * response; see the protocol description below. |
| */ |
| static void mgmt_send_dynamic_challenge_response(struct mgmt_server_data *data, |
| const char *tag, const char *username, const char *state_id, |
| const char *otp) |
| { |
| gchar *reply; |
| |
| _DBG_VPN("username %s state_id %s otp %s", |
| connman_log_get_masked_value(kUserProperty, username), |
| state_id, |
| connman_log_get_masked_value(kOTPProperty, otp)); |
| |
| mgmt_send_username(data, tag, username, __func__); |
| |
| reply = g_strdup_printf("CRV1::%s:%s", state_id, otp); |
| mgmt_send_password(data, tag, reply, __func__); |
| g_free(reply); |
| } |
| |
| /* |
| * >STATE:* msg support. |
| * |
| * State messages are of the form: |
| * >STATE:<date>,<state>,<detail>,<local-ip>,<remote-ip> |
| * where: |
| * <date> is the current time (since epoch) in seconds |
| * <state> is one of: |
| * INITIAL, CONNECTING, WAIT, AUTH, GET_CONFIG, ASSIGN_IP, |
| * ADD_ROUTES, CONNECTED, RECONNECTING, EXITING, RESOLVE, TCP_CONNECT |
| * <detail> is a free-form string giving details about the state change |
| * <local-ip> is a dotted-quad for the local IPv4 address (when available) |
| * <remote-ip> is a dotted-quad for the remote IPv4 address (when available) |
| */ |
| static void mgmt_event_state(struct mgmt_server_data *data, const char *tag) |
| { |
| const char *cp = strchr(tag, ','); |
| |
| if (cp == NULL) { |
| connman_error("%s: bad >STATE msg \"%s\"", __func__, tag); |
| return; |
| } |
| cp++; /* skip ',' */ |
| if (g_str_has_prefix(cp, "CONNECTING,") == TRUE) { |
| data->state = OV_STATE_CONNECTING; |
| } else if (g_str_has_prefix(cp, "WAIT,") == TRUE) { |
| data->state = OV_STATE_WAIT; |
| } else if (g_str_has_prefix(cp, "AUTH,") == TRUE) { |
| data->state = OV_STATE_AUTH; |
| } else if (g_str_has_prefix(cp, "GET_CONFIG,") == TRUE) { |
| data->state = OV_STATE_GET_CONFIG; |
| } else if (g_str_has_prefix(cp, "ASSIGN_IP,") == TRUE) { |
| data->state = OV_STATE_ASSIGN_IP; |
| } else if (g_str_has_prefix(cp, "ADD_ROUTES,") == TRUE) { |
| data->state = OV_STATE_ADD_ROUTES; |
| } else if (g_str_has_prefix(cp, "CONNECTED,") == TRUE) { |
| data->state = OV_STATE_CONNECTED; |
| } else if (g_str_has_prefix(cp, "RECONNECTING,") == TRUE) { |
| data->state = OV_STATE_RECONNECTING; |
| vpn_reconnect(data->provider); |
| } else if (g_str_has_prefix(cp, "RESOLVE,") == TRUE) { |
| data->state = OV_STATE_RESOLVE; |
| } else if (g_str_has_prefix(cp, "EXITING,") == TRUE) { |
| data->state = OV_STATE_EXITING; |
| } else { |
| connman_warn("%s: unknown STATE, openvpn sent \"%s\"", |
| __func__, tag); |
| return; |
| } |
| |
| /* TODO(sleffler) dispatch signal for UI? */ |
| } |
| |
| static void mgmt_send_state(struct mgmt_server_data *data, |
| const char *tag, const char *func) |
| { |
| gchar *str = g_strdup_printf("state %s\n", tag); |
| |
| _DBG_VPN("state %s", tag); |
| |
| mgmt_send(data, str, strlen(str), func); |
| g_free(str); |
| } |
| |
| /* |
| * Authentication callback support. When an Auth request is |
| * received over the management channel we post a request to |
| * an associated Agent for the necessary data. On receiving |
| * a reply we get a callback with the state block we construct |
| * to hold the data required to complete the request. If there |
| * is no reply or an error occurs we also get a callback but |
| * a "failure" indication. If we hit an Agent failure we shut |
| * down the management channel which will cause openvpn to abort |
| * and unwind our side to force the provider and service into |
| * an failure state. |
| */ |
| struct need_auth_cb_data { |
| struct mgmt_server_data *data; |
| char *tag; |
| }; |
| static void *new_auth_cb_arg(struct mgmt_server_data *data, const char *tag) |
| { |
| struct need_auth_cb_data *arg = g_try_new0(struct need_auth_cb_data, 1); |
| if (arg != NULL) { |
| arg->data = data; |
| arg->tag = g_strdup(tag); |
| } |
| return arg; |
| } |
| static void free_auth_cb_arg(struct need_auth_cb_data *arg) |
| { |
| g_free(arg->tag); |
| g_free(arg); |
| } |
| |
| /* |
| * Agent callback for user/password and dynamic challenge/response |
| * authentication requests (static c/r handled separately). |
| */ |
| static void need_auth_cb(struct connman_provider *provider, void *arg, |
| int success) |
| { |
| struct need_auth_cb_data *data = arg; |
| |
| if (success) |
| mgmt_event_need_auth(data->data, data->tag); |
| else |
| mgmt_close_channel(data->data); |
| free_auth_cb_arg(data); |
| } |
| |
| /* |
| * >PASSWORD:Need "Auth" support for user/password requests. |
| * If we have all the data just send a response. Otherwise |
| * notify the Agent and wait for a reply. |
| */ |
| static void mgmt_event_need_auth(struct mgmt_server_data *data, |
| const char *tag) |
| { |
| struct connman_provider *provider = data->provider; |
| const char *user = connman_provider_get_string(provider, kUserProperty); |
| const char *passwd = connman_provider_get_string(provider, |
| kPasswordProperty); |
| |
| if (user == NULL || passwd == NULL) { |
| struct need_auth_cb_data *arg = new_auth_cb_arg(data, tag); |
| |
| _DBG_VPN("no username/password yet; defer reply"); |
| if (connman_provider_request_input(provider, |
| need_auth_cb, new_auth_cb_arg(data, tag), |
| "User", kUserProperty, |
| "Password", kPasswordProperty, NULL) < 0) |
| need_auth_cb(provider, arg, FALSE); |
| return; |
| } |
| mgmt_send_username(data, tag, user, __func__); |
| mgmt_send_password(data, tag, passwd, __func__); |
| } |
| |
| /* |
| * Agent callback for TPM PIN requests. |
| */ |
| static void need_tpm_pin_cb(struct connman_provider *provider, void *arg, |
| int success) |
| { |
| struct need_auth_cb_data *data = arg; |
| |
| if (success) |
| mgmt_event_need_tpm_pin(data->data, data->tag); |
| else |
| mgmt_close_channel(data->data); |
| free_auth_cb_arg(data); |
| } |
| |
| /* |
| * >PASSWORD:Need TPM PIN support. |
| */ |
| static void mgmt_event_need_tpm_pin(struct mgmt_server_data *data, |
| const char *tag) |
| { |
| struct connman_provider *provider = data->provider; |
| const char *pin = connman_provider_get_string(provider, kPINProperty); |
| |
| if (pin == NULL) { |
| struct need_auth_cb_data *arg = new_auth_cb_arg(data, tag); |
| |
| if (connman_provider_request_input(provider, |
| need_tpm_pin_cb, new_auth_cb_arg(data, tag), |
| "PIN", kPINProperty, NULL) < 0) |
| need_tpm_pin_cb(provider, arg, FALSE); |
| _DBG_VPN("no PIN; defer reply"); |
| } else |
| mgmt_send_password(data, tag, pin, __func__); |
| } |
| |
| /* |
| * Agent callback for static challenge/response requests. |
| */ |
| static void need_static_challenge_cb(struct connman_provider *provider, |
| void *arg, int success) |
| { |
| struct need_auth_cb_data *data = arg; |
| |
| if (success) |
| mgmt_event_need_static_challenge(data->data, data->tag, NULL); |
| else |
| mgmt_close_channel(data->data); |
| free_auth_cb_arg(data); |
| } |
| |
| /* |
| * >PASSWORD:Need "Auth" support for static challenge/response requests. |
| */ |
| static void mgmt_event_need_static_challenge(struct mgmt_server_data *data, |
| const char *tag, char *cr) |
| { |
| struct connman_provider *provider = data->provider; |
| const char *username = connman_provider_get_string(provider, |
| kUserProperty); |
| const char *password = connman_provider_get_string(provider, |
| kPasswordProperty); |
| const char *otp = connman_provider_get_string(provider, kOTPProperty); |
| |
| if (username == NULL || password == NULL || otp == NULL) { |
| struct need_auth_cb_data *arg = new_auth_cb_arg(data, tag); |
| |
| /* missing required credentials, request input */ |
| _DBG_VPN("missing username/passwd/OTP; defer reply"); |
| if (connman_provider_request_input(provider, |
| need_static_challenge_cb, arg, |
| "User", kUserProperty, |
| "Password", kPasswordProperty, |
| "OTP", kOTPProperty, NULL) < 0) |
| need_static_challenge_cb(provider, arg, FALSE); |
| } else { |
| gchar *password_base64 = g_base64_encode( |
| (const guchar *)password, strlen(password)); |
| gchar *otp_base64 = g_base64_encode( |
| (const guchar *)otp, strlen(otp)); |
| |
| mgmt_send_static_challenge_response(data, tag, username, |
| password_base64, otp_base64); |
| |
| g_free(otp_base64); |
| g_free(password_base64); |
| |
| /* NB: never re-use OTP, also avoids looping */ |
| connman_provider_clear_property(provider, kOTPProperty); |
| } |
| } |
| |
| /* |
| * Agent callback for dynamic challenge/response requests. |
| */ |
| static void need_dynamic_challenge_cb(struct connman_provider *provider, |
| void *arg, int success) |
| { |
| struct need_auth_cb_data *data = arg; |
| |
| if (success) |
| mgmt_event_need_dynamic_challenge(data->data, data->tag); |
| else |
| mgmt_close_channel(data->data); |
| free_auth_cb_arg(data); |
| } |
| |
| /* |
| * >PASSWORD:Need "Auth" support for dynamic challenge/response |
| * requests. If we have all the data just send a response. |
| * Otherwise notify the Agent and wait for a reply. |
| */ |
| static void mgmt_event_need_dynamic_challenge(struct mgmt_server_data *data, |
| const char *tag) |
| { |
| struct connman_provider *provider = data->provider; |
| const char *otp = connman_provider_get_string(provider, kOTPProperty); |
| |
| if (otp == NULL) { |
| struct need_auth_cb_data *arg = new_auth_cb_arg(data, tag); |
| |
| /* missing OTP, request input */ |
| _DBG_VPN("missing OTP; defer reply"); |
| if (connman_provider_request_input(provider, |
| need_dynamic_challenge_cb, new_auth_cb_arg(data, tag), |
| "OTP", kOTPProperty, NULL) < 0) |
| need_dynamic_challenge_cb(provider, arg, FALSE); |
| } else { |
| mgmt_send_dynamic_challenge_response(data, tag, |
| data->username, data->state_id, otp); |
| |
| g_free(data->username); |
| data->username = NULL; |
| |
| g_free(data->state_id); |
| data->state_id = NULL; |
| |
| /* NB: never re-use OTP, also avoids looping */ |
| connman_provider_clear_property(provider, kOTPProperty); |
| } |
| } |
| |
| /* |
| * Parse Dynamic Challenge/Response Protocol. The failure |
| * msg contains the challenge question formatted according to: |
| * |
| * CRV1:<flags>:<state_id>:<username_base64>:<challenge_text> |
| * |
| * flags: a series of optional, comma-separate flags: |
| * E : echo the response when the user types it |
| * R : a response is required |
| * |
| * state_id: an opaque string that should be returned to the |
| * server along with the response. |
| * |
| * username_base64 : the username formatted as base64 |
| * |
| * challenge_text : the challenge text to be shown to the user |
| * |
| * After prompting for the reponse we must respond to the next |
| * >PASSWORD:Need request with: |
| * |
| * Username: [username decoded from username_base64] |
| * Password: CRV1::<state_id>::<response_text> |
| * |
| * (using the values sent in the failure message). |
| */ |
| static void missing_colon(const char *func, const char *what, const char *data) |
| { |
| connman_error("%s: no ':' parsing %s in \"%s\"", func, what, data); |
| } |
| static void mgmt_event_challenge_response(struct mgmt_server_data *data, |
| char *emsg) |
| { |
| char *cp, *state_id, *username_base64, *challenge_text; |
| int respond, echo; |
| |
| respond = echo = FALSE; |
| /* NB: caller verifies "CRV1:" is present so +5 is ok here */ |
| for (cp = emsg + 5; *cp != ':'; cp++) { |
| if (*cp == '\0') { |
| missing_colon(__func__, "flags", emsg); |
| return; |
| } |
| if (*cp == 'R') |
| respond = TRUE; |
| else if (*cp == 'E') |
| echo = TRUE; |
| } |
| state_id = cp+1; |
| cp = strchr(state_id, ':'); |
| if (cp == NULL) { |
| missing_colon(__func__, "state_id", emsg); |
| return; |
| } |
| *cp = '\0'; |
| username_base64 = cp+1; |
| cp = strchr(username_base64, ':'); |
| if (cp == NULL) { |
| missing_colon(__func__, "username", emsg); |
| return; |
| } |
| *cp = '\0'; |
| challenge_text = cp+1; |
| |
| connman_info("%s: state_id \"%s\" username \"%s\" challenge \"%s\"%s%s", |
| __func__, state_id, username_base64, challenge_text, |
| respond ? ", Respond" : "", echo ? ", Echo" : ""); |
| |
| if (respond) { |
| gsize len; |
| |
| /* TODO(sleffler) state_id and username should be NULL, should we assert? */ |
| g_free(data->state_id); |
| data->state_id = g_strdup(state_id); |
| |
| g_free(data->username); |
| /* TODO(sleffler) can &len be NULL? */ |
| data->username = (char *)g_base64_decode(username_base64, &len); |
| |
| mgmt_event_need_dynamic_challenge(data, "Auth"); |
| } |
| } |
| |
| /* |
| * Handle a password verification failure. We process |
| * challenge/response requests and push everything else |
| * back to the client. |
| */ |
| static void missing_token(const char *func, const char *what, const char *data) |
| { |
| connman_error("%s: missing '%s' in \"%s\"", func, what, data); |
| } |
| static void mgmt_event_verification(struct mgmt_server_data *data, |
| char *line) |
| { |
| char *emsg, *cp; |
| |
| if (g_str_has_prefix(line, ">PASSWORD:Verification Failed: 'Auth'") == FALSE) |
| return; |
| |
| /* |
| * Auth failures may include a custom server-generated string. |
| * Dynamic c/r msgs are of the form ['CRV1:...']; we extract |
| * those here and hand them off for processing. All other msgs |
| * are sent back to the client for presentation. |
| */ |
| emsg = strchr(line, '['); |
| if (emsg != NULL) { |
| emsg++; |
| if (emsg[0] == '\'') |
| emsg++; |
| cp = strchr(emsg, ']'); |
| if (cp == NULL) { |
| missing_token(__func__, "]", line); |
| goto done; |
| } |
| if (cp[-1] == '\'') |
| cp--; |
| *cp = '\0'; |
| |
| if (g_str_has_prefix(emsg, "CRV1:") == TRUE) { |
| mgmt_event_challenge_response(data, emsg); |
| return; |
| } |
| /* NB: stripped ['...'] from the original string */ |
| } |
| done: |
| _DBG_VPN("Auth failure '%s'", emsg); |
| /* NB: no way to pass auth failure indication to client */ |
| connman_provider_indicate_error(data->provider, |
| CONNMAN_PROVIDER_ERROR_BAD_PASSPHRASE); |
| |
| /* TODO(sleffler) clear state, OpenVPN.Password? */ |
| } |
| |
| /* |
| * Process data from the management control socket connected to openvpn. |
| * We process only ">PASSWORD" requests; tossing everything else. |
| */ |
| static gboolean mgmt_server_event(GIOChannel *channel, GIOCondition condition, |
| gpointer user_data) |
| { |
| struct mgmt_server_data *data = user_data; |
| char buf[512]; |
| int s, i; |
| ssize_t len; |
| gchar **lines; |
| |
| if (condition & (G_IO_NVAL | G_IO_ERR | G_IO_HUP)) { |
| connman_error("%s: server channel error (condition 0x%x)", |
| __func__, condition); |
| data->watch = 0; |
| return FALSE; |
| } |
| |
| s = g_io_channel_unix_get_fd(channel); |
| |
| len = read(s, buf, sizeof(buf)-1); |
| if (len < 0) { |
| /* TODO(sleffler) disable or recover? */ |
| connman_error("%s: read error %s", __func__, strerror(errno)); |
| data->watch = 0; |
| return FALSE; |
| } |
| /* TODO(sleffler) buffer data until we see a \n? */ |
| |
| buf[len] = '\0'; |
| |
| lines = g_strsplit(buf, "\n", 0); |
| for (i = 0; lines[i] != NULL; i++) { |
| if (lines[i][0] == '\0') |
| continue; |
| _DBG_VPN("\"%s\"", lines[i]); |
| if (g_str_has_prefix(lines[i], ">INFO") == TRUE) |
| continue; |
| if (g_str_has_prefix(lines[i], ">PASSWORD:Need ") == TRUE) { |
| char *type, *etype; |
| |
| /* extract tag, e.g. "Auth" */ |
| type = strchr(lines[i], '\''); |
| if (type == NULL) { |
| missing_token(__func__, "open-'", lines[i]); |
| goto skip; |
| } |
| type++; |
| etype = strchr(type, '\''); |
| if (etype == NULL) { |
| missing_token(__func__, "close-'", lines[i]); |
| goto skip; |
| } |
| *etype++ = '\0'; |
| |
| if (g_strcmp0(type, "Auth") == 0) { |
| gchar *sc; |
| |
| /* check for static-challenge component */ |
| sc = g_strstr_len(etype, strlen(etype), "SC:"); |
| if (sc != NULL) { |
| mgmt_event_need_static_challenge(data, |
| type, sc); |
| } else |
| mgmt_event_need_auth(data, type); |
| continue; |
| } |
| |
| if (g_str_has_prefix(type, "User-Specific TPM Token") == TRUE) { |
| mgmt_event_need_tpm_pin(data, type); |
| continue; |
| } |
| } |
| if (g_str_has_prefix(lines[i], ">PASSWORD:Verif") == TRUE) { |
| mgmt_event_verification(data, lines[i]); |
| continue; |
| } |
| if (g_str_has_prefix(lines[i], ">STATE:") == TRUE) { |
| mgmt_event_state(data, lines[i]); |
| continue; |
| } |
| skip: |
| connman_info("%s: ignore \"%s\"", __func__, lines[i]); |
| } |
| g_strfreev(lines); |
| |
| return TRUE; |
| } |
| |
| /* |
| * Management channel callback to accept a connection from openvpn. |
| * This callback is setup for the socket we listen on. Once we accept |
| * a connection on it we then discard the listen socket and arrange |
| * for callbacks over the data socket by mgmt_server_event. |
| */ |
| static gboolean mgmt_server_accept(GIOChannel *channel, GIOCondition condition, |
| gpointer user_data) |
| { |
| struct mgmt_server_data *data = user_data; |
| int s; |
| |
| if (condition & (G_IO_NVAL | G_IO_ERR | G_IO_HUP)) { |
| connman_error("%s: server channel error (condition 0x%x)", |
| __func__, condition); |
| data->watch = 0; |
| return FALSE; |
| } |
| |
| /* TODO(sleffler): verify peer's address is INADDR_LOOPBACK */ |
| s = accept(g_io_channel_unix_get_fd(channel), NULL, NULL); |
| if (s < 0) { |
| connman_error("%s: accept error %s", __func__, strerror(errno)); |
| data->watch = 0; |
| return FALSE; |
| } |
| |
| /* clear old state to prepare for replacements */ |
| g_source_remove(data->watch); |
| data->watch = 0; |
| |
| g_io_channel_unref(data->channel); |
| |
| /* now setup channel for accepted socket */ |
| data->channel = g_io_channel_unix_new(s); |
| if (data->channel == NULL) { |
| connman_error("Failed to create data channel for %s", |
| data->vpnhost); |
| close(s); |
| /* NB: data->watch already zero */ |
| return FALSE; |
| } |
| g_io_channel_set_close_on_unref(data->channel, TRUE); |
| |
| data->watch = g_io_add_watch(data->channel, G_IO_IN, |
| mgmt_server_event, data); |
| |
| connman_info("OpenVPN management channel for %s created @%s:%d", |
| data->vpnhost, inet_ntoa(data->addr.sin_addr), |
| ntohs(data->addr.sin_port)); |
| |
| mgmt_send_state(data, "on", __func__); |
| |
| return TRUE; |
| } |
| |
| static struct mgmt_server_data *mgmt_create_server( |
| struct connman_provider *provider, const char *vpnhost) |
| { |
| struct mgmt_server_data *data; |
| int s; |
| socklen_t slen; |
| |
| _DBG_VPN("vpnhost %s", vpnhost); |
| |
| data = g_try_new0(struct mgmt_server_data, 1); |
| if (data == NULL) { |
| connman_error("Failed to allocate server data"); |
| return NULL; |
| } |
| |
| s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); |
| if (s < 0) { |
| connman_error("Server socket create failed for %s (%s)", |
| vpnhost, strerror(errno)); |
| g_free(data); |
| return NULL; |
| } |
| |
| memset(&data->addr, 0, sizeof(data->addr)); |
| data->addr.sin_family = AF_INET; |
| data->addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); |
| if (bind(s, (struct sockaddr *) &data->addr, sizeof(data->addr)) < 0) { |
| connman_error("Could not bind address for %s: %s", |
| vpnhost, strerror(errno)); |
| goto bad; |
| } |
| |
| if (listen(s, 1) < 0) { |
| connman_error("Server socket listen failed for %s: %s", |
| vpnhost, strerror(errno)); |
| goto bad; |
| } |
| |
| slen = sizeof(data->addr); |
| if (getsockname(s, (struct sockaddr *) &data->addr, &slen) < 0) { |
| connman_error("Could not get bound address for %s: %s", |
| vpnhost, strerror(errno)); |
| goto bad; |
| } |
| |
| data->channel = g_io_channel_unix_new(s); |
| if (data->channel == NULL) { |
| connman_error("Failed to create server channel for %s", |
| vpnhost); |
| goto bad; |
| } |
| g_io_channel_set_close_on_unref(data->channel, TRUE); |
| |
| data->watch = g_io_add_watch(data->channel, G_IO_IN, |
| mgmt_server_accept, data); |
| |
| data->provider = connman_provider_ref(provider); |
| data->vpnhost = g_strdup(vpnhost); |
| data->state = OV_STATE_UNKNOWN; |
| |
| return data; |
| bad: |
| close(s); |
| g_free(data); |
| return NULL; |
| } |
| |
| static void mgmt_close_channel(struct mgmt_server_data *data) |
| { |
| if (data->watch > 0) { |
| g_source_remove(data->watch); |
| data->watch = -1; |
| } |
| if (data->channel != NULL) { |
| g_io_channel_unref(data->channel); |
| data->channel = NULL; |
| } |
| } |
| |
| static void mgmt_destroy_server(struct mgmt_server_data *data) |
| { |
| _DBG_VPN("vpnhost %s", data->vpnhost); |
| |
| mgmt_close_channel(data); |
| |
| connman_provider_unref(data->provider); |
| g_free(data->username); |
| g_free(data->password); |
| g_free(data->state_id); |
| g_free(data->vpnhost); |
| g_free(data); |
| } |
| |
| static int ov_init_data(struct connman_provider *provider, const char *vpnhost) |
| { |
| struct openvpn_data *data; |
| |
| data = g_try_new0(struct openvpn_data, 1); |
| if (data == NULL) { |
| connman_error("%s: out of memory", __func__); |
| return -ENOMEM; |
| } |
| data->gw_index = (uint32_t)-1; |
| data->ipaddr.af = AF_INET; |
| data->ipaddr.mask |= CONNMAN_IPCONFIG_AF; |
| vpn_set_specific_data(provider, data); |
| return 0; |
| } |
| |
| static void ov_cleanup_data(struct connman_provider *provider) |
| { |
| struct openvpn_data *data = vpn_get_specific_data(provider); |
| |
| _DBG_VPN("provider %p data %p", provider, data); |
| |
| if (data == NULL) |
| return; |
| if (data->server != NULL) |
| mgmt_destroy_server(data->server); |
| if (data->gw_index != (uint32_t)-1) { |
| _DBG_VPN("tear down VPN host route"); |
| connman_inet_del_hostroute(data->gw_index, data->vpn_addr, |
| data->gw_addr); |
| } |
| if (data->tls_auth_file != NULL) { |
| unlink(data->tls_auth_file); |
| g_free(data->tls_auth_file); |
| } |
| if (data->ipaddr.mask & CONNMAN_IPCONFIG_LOCAL) |
| g_free(data->ipaddr.local); |
| if (data->ipaddr.mask & CONNMAN_IPCONFIG_BCAST) |
| g_free(data->ipaddr.broadcast); |
| if (data->ipaddr.mask & CONNMAN_IPCONFIG_PEER) |
| g_free(data->ipaddr.peer); |
| if (data->ipaddr.mask & CONNMAN_IPCONFIG_GW) |
| g_free(data->ipaddr.gateway); |
| vpn_set_specific_data(provider, NULL); |
| g_free(data); |
| } |
| |
| static void ov_vpn_died(struct connman_task *task, void *user_data) |
| { |
| struct connman_provider *provider = user_data; |
| |
| _DBG_VPN("provider %p", provider); |
| |
| ov_cleanup_data(provider); |
| vpn_died(task, user_data, CONNMAN_PROVIDER_ERROR_CONNECT_FAILED); |
| } |
| |
| #define OPT_STR(property, option) do { \ |
| const char *s = connman_provider_get_string(provider, property); \ |
| if (s != NULL) \ |
| connman_task_add_argument(task, option, "%s", s); \ |
| } while (0) |
| #define OPT_BOOL(property, option) do { \ |
| const char *s = connman_provider_get_string(provider, property); \ |
| if (s != NULL) \ |
| connman_task_add_argument(task, option, NULL); \ |
| } while (0) |
| |
| int add_mgmt_arguments(struct connman_provider *provider, |
| struct connman_task *task, |
| const char *vpnhost) |
| { |
| struct openvpn_data *data = vpn_get_specific_data(provider); |
| const char *static_challenge; |
| gchar *port; |
| |
| /* |
| * Setup a management control channel between openvpn and us |
| * to do things like query for passwords. We specify that |
| * openvpn connects back to us to avoid a race on startup and |
| * to allow us to bind a (random) system-selected port (on |
| * the loopback interface) to use. This plus accepting only |
| * one connection minimizes the window by which a process can |
| * setup this channel over which sensitive data are passed. |
| */ |
| data->server = mgmt_create_server(provider, vpnhost); |
| if (data->server == NULL) { |
| /* NB: mgmt_create_server logs reason */ |
| ov_cleanup_data(provider); |
| return -EINVAL; |
| } |
| |
| /* TODO(sleffler) total hack for crappy api */ |
| connman_task_add_argument(task, "--management", |
| inet_ntoa(data->server->addr.sin_addr)); |
| port = g_strdup_printf("%d", ntohs(data->server->addr.sin_port)); |
| connman_task_add_argument(task, port, NULL); |
| g_free(port); |
| |
| connman_task_add_argument(task, "--management-client", NULL); |
| connman_task_add_argument(task, "--management-query-passwords", NULL); |
| |
| OPT_BOOL("OpenVPN.AuthUserPass","--auth-user-pass"); |
| |
| static_challenge = connman_provider_get_string(provider, |
| "OpenVPN.StaticChallenge"); |
| if (static_challenge != NULL) { |
| connman_task_add_argument(task, "--static-challenge", |
| static_challenge); |
| /* NB: force echo */ |
| connman_task_add_argument(task, "1", NULL); |
| } |
| |
| /* TODO(sleffler) verify OpenVPN.Pkcs11.PIN is present */ |
| /* TODO(sleffler) set/verify OpenVPN.AuthRetry? */ |
| return 0; |
| } |
| |
| /* |
| * Duplicate a string from /etc/lsb-release. The data are |
| * scrubbed for whitespace and any terminating \n is removed. |
| */ |
| static char *dup_value(char *str) |
| { |
| char buf[256], c; |
| int i, j; |
| |
| j = 0; |
| for (i = 0; (c = str[i]) != '\0'; i++) { |
| if (c == ' ') /* remove whitespace */ |
| continue; |
| if (c == '\n') /* stop at first \n */ |
| break; |
| buf[j++] = c; |
| } |
| buf[j] = '\0'; |
| return strdup(buf); |
| } |
| |
| /* |
| * Read platform name and version from /etc/lsb-release |
| * and install them in platform_name and platform_version, |
| * respectively. Return true on success. |
| */ |
| static gboolean read_platform_data(void) |
| { |
| char line[256], name[256], version[256]; |
| FILE *fd; |
| |
| fd = fopen(kLSBReleaseFilename, "r"); |
| if (fd == NULL) { |
| connman_error("%s: Cannot open %s", __func__, |
| kLSBReleaseFilename); |
| return FALSE; |
| } |
| name[0] = version[0] = '\0'; |
| while (fgets(line, sizeof(line), fd) != NULL) { |
| char *cp = strchr(line, '='); |
| if (cp == NULL) |
| continue; |
| *cp++ = '\0'; |
| if (strcmp(line, kChromeOSReleaseName)== 0) |
| strncpy(name, cp, sizeof(name)); |
| else if (strcmp(line, kChromeOSReleaseVersion) == 0) |
| strncpy(version, cp, sizeof(version)); |
| } |
| fclose(fd); |
| if (name[0] == '\0' || version[0] == '\0') { |
| connman_error("%s: Missing data: name=\"%s\" version=\"%s\"", |
| __func__, name, version); |
| return FALSE; |
| } |
| |
| /* TODO(sleffler) worth checking return values? */ |
| platform_name = dup_value(name); |
| platform_version = dup_value(version); |
| return (platform_name != NULL && platform_version != NULL); |
| } |
| |
| /* |
| * Add the platform name and version information to the environment |
| * so that openvpn will send them to the server (when |
| * OpenVPN.PushPeerInfo is set). |
| */ |
| static void add_platform_vars(struct connman_task *task) |
| { |
| if (platform_name == NULL && !read_platform_data()) |
| return; |
| |
| /* NB: these are passed through the environment */ |
| connman_task_add_variable(task, "IV_PLAT", platform_name); |
| connman_task_add_variable(task, "IV_PLAT_REL", platform_version); |
| } |
| |
| static int ov_connect(struct connman_provider *provider, |
| struct connman_task *task, const char *if_name) |
| { |
| const char *vpnhost; |
| const char *remote_cert_tls; |
| const char *tls_auth_contents; |
| const char *ca_cert_nss; |
| const char *pkcs11_id; |
| const char *verb; |
| int err, fd; |
| |
| vpnhost = connman_provider_get_string(provider, CONNMAN_PROVIDER_HOST); |
| if (!vpnhost) { |
| connman_error("%s: host not set; cannot enable VPN", __func__); |
| return -EINVAL; |
| } |
| err = ov_init_data(provider, vpnhost); |
| if (err < 0) |
| return err; |
| |
| connman_task_add_argument(task, "--client", NULL); |
| connman_task_add_argument(task, "--tls-client", NULL); |
| connman_task_add_argument(task, "--remote", "%s", vpnhost); |
| connman_task_add_argument(task, "--nobind", NULL); |
| connman_task_add_argument(task, "--persist-key", NULL); |
| connman_task_add_argument(task, "--persist-tun", NULL); |
| |
| connman_task_add_argument(task, "--dev", "%s", if_name); |
| connman_task_add_argument(task, "--dev-type", "%s", "tun"); |
| connman_task_add_argument(task, "--syslog", NULL); |
| |
| verb = connman_provider_get_string(provider, "OpenVPN.Verb"); |
| if (verb == NULL && connman_debug_enabled(DBG_VPN) == TRUE) |
| verb = "3"; |
| connman_task_add_argument(task, "--verb", "%s", verb); |
| |
| OPT_STR("VPN.MTU", "--mtu"); |
| OPT_STR("OpenVPN.Proto", "--proto"); |
| OPT_STR("OpenVPN.Port", "--port"); |
| OPT_STR("OpenVPN.TLSAuth", "--tls-auth"); |
| tls_auth_contents = connman_provider_get_string( |
| provider, |
| "OpenVPN.TLSAuthContents"); |
| if (tls_auth_contents != NULL) { |
| struct openvpn_data *data = vpn_get_specific_data(provider); |
| int handle; |
| handle = g_file_open_tmp("tls-auth.XXXXXX", |
| &data->tls_auth_file, |
| NULL); |
| if (handle < 0) { |
| ov_cleanup_data(provider); |
| connman_error("%s: unable to create temp tls-auth", |
| __func__); |
| return -EIO; |
| } |
| close(handle); |
| if (!g_file_set_contents(data->tls_auth_file, tls_auth_contents, |
| strlen(tls_auth_contents), NULL)) { |
| ov_cleanup_data(provider); |
| connman_error("%s: unable to write to tls auth file", |
| __func__); |
| return -EIO; |
| } |
| connman_task_add_argument(task, "--tls-auth", |
| data->tls_auth_file); |
| } |
| OPT_STR("OpenVPN.TLSRemote", "--tls-remote"); |
| OPT_STR("OpenVPN.Cipher", "--cipher"); |
| OPT_STR("OpenVPN.Auth", "--auth"); |
| OPT_BOOL("OpenVPN.AuthNoCache", "--auth-nocache"); |
| OPT_STR("OpenVPN.AuthRetry", "--auth-retry"); |
| OPT_BOOL("OpenVPN.CompLZO", "--comp-lzo"); |
| OPT_BOOL("OpenVPN.CompNoAdapt", "--comp-noadapt"); |
| OPT_BOOL("OpenVPN.PushPeerInfo","--push-peer-info"); |
| OPT_STR("OpenVPN.RenegSec", "--reneg-sec"); |
| OPT_STR("OpenVPN.Shaper", "--shaper"); |
| OPT_STR("OpenVPN.ServerPollTimeout", "--server-poll-timeout"); |
| |
| #ifdef ENABLE_NSS |
| ca_cert_nss = |
| connman_provider_get_string(provider, "OpenVPN.CACertNSS"); |
| if (ca_cert_nss != NULL) { |
| char *filename; |
| if (connman_provider_get_string(provider, |
| "OpenVPN.CACert") != NULL) { |
| connman_error("%s: CACert and CACertNSS cannot both be " |
| "specified", __func__); |
| return -EIO; |
| } |
| filename = nss_get_pem_certfile(ca_cert_nss, (uint8_t *)vpnhost, |
| strlen(vpnhost)); |
| if (filename != NULL) { |
| connman_task_add_argument(task, "--ca", |
| "%s", filename); |
| } else { |
| connman_error("%s: Could not extract certificate %s", |
| __func__, ca_cert_nss); |
| } |
| g_free(filename); |
| } |
| #endif |
| |
| /* client-side ping support */ |
| OPT_STR("OpenVPN.Ping", "--ping"); |
| OPT_STR("OpenVPN.PingExit", "--ping-exit"); |
| OPT_STR("OpenVPN.PingRestart", "--ping-restart"); |
| |
| OPT_STR("OpenVPN.CACert", "--ca"); |
| OPT_STR("OpenVPN.Cert", "--cert"); |
| OPT_STR("OpenVPN.NsCertType", "--ns-cert-type"); |
| OPT_STR("OpenVPN.Key", "--key"); |
| |
| /* PKCS#11 support. */ |
| pkcs11_id = connman_provider_get_string(provider, "OpenVPN.Pkcs11.ID"); |
| if (pkcs11_id != NULL) { |
| const char *pkcs11_provider; |
| pkcs11_provider = connman_provider_get_string(provider, |
| "OpenVPN.Pkcs11.Provider"); |
| if (pkcs11_provider != NULL) { |
| connman_task_add_argument(task, "--pkcs11-providers", |
| "%s", pkcs11_provider); |
| } else { |
| #ifdef DEFAULT_PKCS11 |
| connman_task_add_argument(task, "--pkcs11-providers", |
| "%s", DEFAULT_PKCS11); |
| #endif |
| } |
| connman_task_add_argument(task, "--pkcs11-id", "%s", pkcs11_id); |
| } |
| |
| /* TLS support */ |
| remote_cert_tls = connman_provider_get_string(provider, |
| "OpenVPN.RemoteCertTLS"); |
| if (remote_cert_tls == NULL) |
| remote_cert_tls = "server"; |
| if (g_strcmp0(remote_cert_tls, "none") != 0) |
| connman_task_add_argument(task, "--remote-cert-tls", |
| "%s", remote_cert_tls); |
| |
| /* NB: undocumented cmd line arg; works like .cfg file entry */ |
| /* TODO(sleffler) maybe roll into --tls-auth? */ |
| OPT_STR("OpenVPN.KeyDirection", "--key-direction"); |
| /* TODO(sleffler) support >1 eku parameter */ |
| OPT_STR("OpenVPN.RemoteCertEKU","--remote-cert-eku"); |
| OPT_STR("OpenVPN.RemoteCertKU", "--remote-cert-ku"); |
| |
| err = add_mgmt_arguments(provider, task, vpnhost); |
| if (err < 0) |
| return err; |
| |
| /* |
| * Arrange for our callback script to be called with the |
| * DBus info required to send us the Layer 3 configuration; |
| * see ov_notify for our handling. |
| */ |
| connman_task_add_argument(task, "--setenv", NULL); |
| connman_task_add_argument(task, "CONNMAN_BUSNAME", |
| "%s", dbus_bus_get_unique_name(connection)); |
| |
| connman_task_add_argument(task, "--setenv", NULL); |
| connman_task_add_argument(task, "CONNMAN_INTERFACE", |
| "%s", CONNMAN_TASK_INTERFACE); |
| |
| connman_task_add_argument(task, "--setenv", NULL); |
| connman_task_add_argument(task, "CONNMAN_PATH", |
| "%s", connman_task_get_path(task)); |
| |
| add_platform_vars(task); |
| |
| connman_task_add_argument(task, "--script-security", "%d", 2); |
| |
| connman_task_add_argument(task, "--up", |
| "%s", SCRIPTDIR "/openvpn-script"); |
| connman_task_add_argument(task, "--up-restart", NULL); |
| |
| /* disable openvpn handling, we do route+ifconfig work */ |
| connman_task_add_argument(task, "--route-noexec", NULL); |
| connman_task_add_argument(task, "--ifconfig-noexec", NULL); |
| |
| /* |
| * Drop root privileges on connection and enable callback |
| * scripts to talk to the Task interface to send notify |
| * messages (after dropping privs). |
| */ |
| connman_task_add_argument(task, "--user", "%s", "openvpn"); |
| connman_task_add_argument(task, "--group", "%s", "openvpn"); |
| |
| fd = fileno(stderr); |
| err = connman_task_run(task, ov_vpn_died, provider, |
| NULL, &fd, &fd); |
| if (err < 0) { |
| connman_error("%s: openvpn failed to start task (err %d)", |
| __func__, err); |
| ov_cleanup_data(provider); |
| return -EIO; |
| } |
| |
| return 0; |
| } |
| #undef OPT_BOOL |
| #undef OPT_STR |
| |
| static const char *ov_public_props[] = { |
| "OpenVPN.Auth", |
| "OpenVPN.AuthNoCache", |
| "OpenVPN.AuthRetry", |
| "OpenVPN.AuthUserPass", |
| "OpenVPN.CACert", |
| "OpenVPN.CACertNSS", |
| "OpenVPN.Cert", |
| "OpenVPN.Cipher", |
| "OpenVPN.CompLZO", |
| "OpenVPN.CompNoAdapt", |
| "OpenVPN.Key", |
| "OpenVPN.KeyDirection", |
| "OpenVPN.NsCertType", |
| "OpenVPN.Pkcs11.PIN", |
| "OpenVPN.Pkcs11.Provider", |
| "OpenVPN.Ping", |
| "OpenVPN.PingExit", |
| "OpenVPN.PingRestart", |
| "OpenVPN.Port", |
| "OpenVPN.Proto", |
| "OpenVPN.PushPeerInfo", |
| "OpenVPN.RemoteCertEKU", |
| "OpenVPN.RemoteCertKU", |
| "OpenVPN.RemoteCertTLS", |
| "OpenVPN.RenegSec", |
| "OpenVPN.Shaper", |
| "OpenVPN.ServerPollTimeout", |
| "OpenVPN.StaticChallenge", |
| "OpenVPN.TLSAuth", |
| "OpenVPN.TLSAuthContents", |
| "OpenVPN.TLSRemote", |
| "OpenVPN.User", |
| "OpenVPN.Verb", |
| "VPN.MTU", |
| NULL |
| }; |
| |
| |
| /* |
| * Append plugin-specific properties to the D-Bus dictionary. |
| */ |
| void ov_append_props(struct connman_provider *provider, DBusMessageIter *iter, |
| connman_bool_t isprivileged) |
| { |
| static const char *ov_priv_props[] = { |
| "OpenVPN.OTP", |
| /* NB: intentionally leave out OpenVPN.Password */ |
| "OpenVPN.Pkcs11.ID", |
| NULL |
| }; |
| dbus_bool_t required; |
| |
| connman_provider_append_properties(provider, ov_public_props, iter); |
| if (isprivileged) |
| connman_provider_append_properties(provider, ov_priv_props, |
| iter); |
| |
| required = connman_provider_property_is_empty(provider, |
| kPasswordProperty); |
| connman_dbus_dict_append_basic(iter, "PassphraseRequired", |
| DBUS_TYPE_BOOLEAN, &required); |
| } |
| |
| /* |
| * Load plugin-specific propetiers from the profile. |
| */ |
| static void ov_load_props(struct connman_provider *provider, GKeyFile *keyfile) |
| { |
| connman_provider_load_save_properties( |
| provider, ov_public_props, connman_provider_load_property, keyfile); |
| connman_provider_load_encrypted_property(provider, kPasswordProperty, |
| keyfile); |
| } |
| |
| /* |
| * Save plugin-specific properties to the profile. |
| */ |
| static void ov_save_props(struct connman_provider *provider, GKeyFile *keyfile) |
| { |
| connman_provider_load_save_properties( |
| provider, ov_public_props, connman_provider_save_property, keyfile); |
| connman_provider_save_encrypted_property(provider, kPasswordProperty, |
| keyfile); |
| } |
| |
| static struct vpn_driver vpn_driver = { |
| .notify = ov_notify, |
| .connect = ov_connect, |
| .append_props = ov_append_props, |
| .load_props = ov_load_props, |
| .save_props = ov_save_props, |
| }; |
| |
| static void __mask_value_of_keys(void) |
| { |
| connman_log_mask_value_of_key(kOTPProperty); |
| connman_log_mask_value_of_key(kPasswordProperty); |
| connman_log_mask_value_of_key(kUserProperty); |
| } |
| |
| static int openvpn_init(void) |
| { |
| connection = connman_dbus_get_connection(); |
| |
| __mask_value_of_keys(); |
| |
| return vpn_register("openvpn", &vpn_driver, OPENVPN); |
| } |
| |
| static void openvpn_exit(void) |
| { |
| vpn_unregister("openvpn"); |
| |
| dbus_connection_unref(connection); |
| |
| free(platform_name); |
| free(platform_version); |
| } |
| |
| CONNMAN_PLUGIN_DEFINE(openvpn, "OpenVPN plugin", VERSION, |
| CONNMAN_PLUGIN_PRIORITY_DEFAULT, openvpn_init, openvpn_exit) |