diff options
Diffstat (limited to '')
-rw-r--r-- | src/main/client.c | 1562 |
1 files changed, 1562 insertions, 0 deletions
diff --git a/src/main/client.c b/src/main/client.c new file mode 100644 index 0000000..b0b3e24 --- /dev/null +++ b/src/main/client.c @@ -0,0 +1,1562 @@ +/* + * This program is is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * 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 + */ + +/** + * $Id$ + * @file main/client.c + * @brief Manage clients allowed to communicate with the server. + * + * @copyright 2015 Arran Cudbard-Bell <a.cudbardb@freeradius.org> + * @copyright 2000,2006 The FreeRADIUS server project + * @copyright 2000 Alan DeKok <aland@ox.org> + * @copyright 2000 Miquel van Smoorenburg <miquels@cistron.nl> + */ +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/rad_assert.h> + +#include <sys/stat.h> + +#include <ctype.h> +#include <fcntl.h> + +#ifdef WITH_DYNAMIC_CLIENTS +#ifdef HAVE_DIRENT_H +#include <dirent.h> +#endif +#endif + +struct radclient_list { + char const *name; /* name of this list */ + char const *server; /* virtual server associated with this client list */ + + /* + * FIXME: One set of trees for IPv4, and another for IPv6? + */ + rbtree_t *trees[129]; /* for 0..128, inclusive. */ + uint32_t min_prefix; + + bool parsed; +}; + + +#ifdef WITH_STATS +static rbtree_t *tree_num = NULL; /* client numbers 0..N */ +static int tree_num_max = 0; +#endif +static RADCLIENT_LIST *root_clients = NULL; + +/* + * Callback for freeing a client. + */ +void client_free(RADCLIENT *client) +{ + if (!client) return; + + talloc_free(client); +} + +/* + * Callback for comparing two clients. + */ +static int client_ipaddr_cmp(void const *one, void const *two) +{ + RADCLIENT const *a = one; + RADCLIENT const *b = two; +#ifndef WITH_TCP + + return fr_ipaddr_cmp(&a->ipaddr, &b->ipaddr); +#else + int rcode; + + rcode = fr_ipaddr_cmp(&a->ipaddr, &b->ipaddr); + if (rcode != 0) return rcode; + + /* + * Wildcard match + */ + if ((a->proto == IPPROTO_IP) || + (b->proto == IPPROTO_IP)) return 0; + + return (a->proto - b->proto); +#endif +} + +#ifdef WITH_STATS +static int client_num_cmp(void const *one, void const *two) +{ + RADCLIENT const *a = one; + RADCLIENT const *b = two; + + return (a->number - b->number); +} +#endif + +/* + * Free a RADCLIENT list. + */ +void client_list_free(RADCLIENT_LIST *clients) +{ + int i; + + if (!clients) clients = root_clients; + if (!clients) return; /* Clients may not have been initialised yet */ + + for (i = 0; i <= 128; i++) { + if (clients->trees[i]) rbtree_free(clients->trees[i]); + clients->trees[i] = NULL; + } + + if (clients == root_clients) { +#ifdef WITH_STATS + if (tree_num) rbtree_free(tree_num); + tree_num = NULL; + tree_num_max = 0; +#endif + root_clients = NULL; + } + +#ifdef WITH_DYNAMIC_CLIENTS + /* + * FIXME: No fr_fifo_delete() + */ +#endif + + talloc_free(clients); +} + +/* + * Return a new, initialized, set of clients. + */ +RADCLIENT_LIST *client_list_init(CONF_SECTION *cs) +{ + RADCLIENT_LIST *clients = talloc_zero(cs, RADCLIENT_LIST); + + if (!clients) return NULL; + + clients->min_prefix = 128; + + /* + * Associate the "clients" list with the virtual server. + */ + if (cs && (cf_data_add(cs, "clients", clients, NULL) < 0)) { + ERROR("Failed to associate client list with section %s\n", cf_section_name1(cs)); + client_list_free(clients); + return false; + } + + return clients; +} + +/** Add a client to a RADCLIENT_LIST + * + * @param clients list to add client to, may be NULL if global client list is being used. + * @param client to add. + * @return true on success, false on failure. + */ +bool client_add(RADCLIENT_LIST *clients, RADCLIENT *client) +{ + RADCLIENT *old; + char buffer[INET6_ADDRSTRLEN + 3]; + + if (!client) return false; + + /* + * Initialize the global list, if not done already. + */ + if (!root_clients) { + root_clients = client_list_init(NULL); + if (!root_clients) { + ERROR("Cannot add client - failed creating client list"); + return false; + } + } + + /* + * Hack to fixup wildcard clients + * + * If the IP is all zeros, with a 32 or 128 bit netmask + * assume the user meant to configure 0.0.0.0/0 instead + * of 0.0.0.0/32 - which would require the src IP of + * the client to be all zeros. + */ + if (fr_inaddr_any(&client->ipaddr) == 1) switch (client->ipaddr.af) { + case AF_INET: + if (client->ipaddr.prefix == 32) client->ipaddr.prefix = 0; + break; + + case AF_INET6: + if (client->ipaddr.prefix == 128) client->ipaddr.prefix = 0; + break; + + default: + rad_assert(0); + } + + fr_ntop(buffer, sizeof(buffer), &client->ipaddr); + DEBUG3("Adding client %s (%s) to prefix tree %i", buffer, client->longname, client->ipaddr.prefix); + + /* + * If the client also defines a server, do that now. + */ + if (client->defines_coa_server) if (!realm_home_server_add(client->coa_home_server)) return false; + + /* + * If there's no client list, BUT there's a virtual + * server, try to add the client to the appropriate + * "clients" section for that virtual server. + */ + if (!clients && client->server) { + CONF_SECTION *cs; + CONF_SECTION *subcs; + CONF_PAIR *cp; + char const *section_name; + + cs = cf_section_sub_find_name2(main_config.config, "server", client->server); + if (!cs) { + ERROR("Cannot add client - virtual server %s does not exist", client->server); + return false; + } + + /* + * If this server has no "listen" section, add the clients + * to the global client list. + */ + subcs = cf_section_sub_find(cs, "listen"); + if (!subcs) { + DEBUG("No 'listen' section in virtual server %s. Adding client to global client list", + client->server); + goto check_list; + } + + cp = cf_pair_find(subcs, "clients"); + if (!cp) { + DEBUG("No 'clients' configuration item in first listener of virtual server %s. Adding client to global client list", + client->server); + goto check_list; + } + + /* + * Duplicate the lookup logic in common_socket_parse() + * + * Explicit list given: use it. + */ + section_name = cf_pair_value(cp); + if (!section_name) goto check_list; + + subcs = cf_section_sub_find_name2(main_config.config, "clients", section_name); + if (!subcs) { + subcs = cf_section_find(section_name); + } + if (!subcs) { + cf_log_err_cs(cs, + "Failed to find clients %s {...}", + section_name); + return false; + } + + DEBUG("Adding client to client list %s", section_name); + + /* + * If the client list already exists, use that. + * Otherwise, create a new client list. + * + * @todo - add the client to _all_ listeners? + */ + clients = cf_data_find(subcs, "clients"); + if (clients) goto check_list; + + clients = client_list_init(subcs); + if (!clients) { + ERROR("Cannot add client - failed creating client list %s for server %s", section_name, + client->server); + return false; + } + } + +check_list: + if (!clients) clients = root_clients; + client->list = clients; + + /* + * Create a tree for it. + */ + if (!clients->trees[client->ipaddr.prefix]) { + clients->trees[client->ipaddr.prefix] = rbtree_create(clients, client_ipaddr_cmp, NULL, 0); + if (!clients->trees[client->ipaddr.prefix]) { + return false; + } + } + +#define namecmp(a) ((!old->a && !client->a) || (old->a && client->a && (strcmp(old->a, client->a) == 0))) + + /* + * Cannot insert the same client twice. + */ + old = rbtree_finddata(clients->trees[client->ipaddr.prefix], client); + if (old) { + /* + * If it's a complete duplicate, then free the new + * one, and return "OK". + */ + if ((fr_ipaddr_cmp(&old->ipaddr, &client->ipaddr) == 0) && + (old->ipaddr.prefix == client->ipaddr.prefix) && + namecmp(longname) && namecmp(secret) && + namecmp(shortname) && namecmp(nas_type) && + namecmp(login) && namecmp(password) && namecmp(server) && +#ifdef WITH_DYNAMIC_CLIENTS + (old->lifetime == client->lifetime) && + namecmp(client_server) && +#endif +#ifdef WITH_COA + namecmp(coa_name) && + (old->coa_home_server == client->coa_home_server) && + (old->coa_home_pool == client->coa_home_pool) && +#endif + (old->message_authenticator == client->message_authenticator)) { + WARN("Ignoring duplicate client %s", client->longname); + client_free(client); + return true; + } + + ERROR("Failed to add duplicate client %s", client->shortname); + return false; + } +#undef namecmp + + /* + * Other error adding client: likely is fatal. + */ + if (!rbtree_insert(clients->trees[client->ipaddr.prefix], client)) { + return false; + } + +#ifdef WITH_STATS + if (!tree_num) { + tree_num = rbtree_create(clients, client_num_cmp, NULL, 0); + } + +#ifdef WITH_DYNAMIC_CLIENTS + /* + * More catching of clients added by rlm_sql. + * + * The sql modules sets the dynamic flag BEFORE calling + * us. The client_afrom_request() function sets it AFTER + * calling us. + */ + if (client->dynamic && (client->lifetime == 0)) { + RADCLIENT *network; + + /* + * If there IS an enclosing network, + * inherit the lifetime from it. + */ + network = client_find(clients, &client->ipaddr, client->proto); + if (network) { + client->lifetime = network->lifetime; + } + } +#endif + + client->number = tree_num_max; + tree_num_max++; + if (tree_num) rbtree_insert(tree_num, client); +#endif + + if (client->ipaddr.prefix < clients->min_prefix) { + clients->min_prefix = client->ipaddr.prefix; + } + + (void) talloc_steal(clients, client); /* reparent it */ + + return true; +} + + +#ifdef WITH_DYNAMIC_CLIENTS +void client_delete(RADCLIENT_LIST *clients, RADCLIENT *client) +{ + if (!client) return; + + if (!clients) clients = root_clients; + + if (!client->dynamic) return; + + rad_assert(client->ipaddr.prefix <= 128); + +#ifdef WITH_STATS + rbtree_deletebydata(tree_num, client); +#endif + rbtree_deletebydata(clients->trees[client->ipaddr.prefix], client); +} +#endif + +#ifdef WITH_STATS +/* + * Find a client in the RADCLIENTS list by number. + * This is a support function for the statistics code. + */ +RADCLIENT *client_findbynumber(RADCLIENT_LIST const *clients, int number) +{ + if (!clients) clients = root_clients; + + if (!clients) return NULL; + + if (number >= tree_num_max) return NULL; + + if (tree_num) { + RADCLIENT myclient; + + myclient.number = number; + + return rbtree_finddata(tree_num, &myclient); + } + + return NULL; +} +#else +RADCLIENT *client_findbynumber(UNUSED const RADCLIENT_LIST *clients, UNUSED int number) +{ + return NULL; +} +#endif + + +/* + * Find a client in the RADCLIENTS list. + */ +RADCLIENT *client_find(RADCLIENT_LIST const *clients, fr_ipaddr_t const *ipaddr, int proto) +{ + int32_t i, max_prefix; + RADCLIENT myclient; + + if (!clients) clients = root_clients; + + if (!clients || !ipaddr) return NULL; + + switch (ipaddr->af) { + case AF_INET: + max_prefix = 32; + break; + + case AF_INET6: + max_prefix = 128; + break; + + default : + return NULL; + } + + for (i = max_prefix; i >= (int32_t) clients->min_prefix; i--) { + void *data; + + myclient.ipaddr = *ipaddr; + myclient.proto = proto; + fr_ipaddr_mask(&myclient.ipaddr, i); + + if (!clients->trees[i]) continue; + + data = rbtree_finddata(clients->trees[i], &myclient); + if (data) return data; + } + + return NULL; +} + +/* + * Old wrapper for client_find + */ +RADCLIENT *client_find_old(fr_ipaddr_t const *ipaddr) +{ + return client_find(root_clients, ipaddr, IPPROTO_UDP); +} + +static fr_ipaddr_t cl_ipaddr; +static uint32_t cl_netmask; +static char const *cl_srcipaddr = NULL; +static char const *hs_proto = NULL; + +#ifdef WITH_TCP +static CONF_PARSER limit_config[] = { + { "max_connections", FR_CONF_OFFSET(PW_TYPE_INTEGER, RADCLIENT, limit.max_connections), "16" }, + + { "lifetime", FR_CONF_OFFSET(PW_TYPE_INTEGER, RADCLIENT, limit.lifetime), "0" }, + + { "idle_timeout", FR_CONF_OFFSET(PW_TYPE_INTEGER, RADCLIENT, limit.idle_timeout), "30" }, + + CONF_PARSER_TERMINATOR +}; +#endif + +static const CONF_PARSER client_config[] = { + { "ipaddr", FR_CONF_POINTER(PW_TYPE_COMBO_IP_PREFIX, &cl_ipaddr), NULL }, + { "ipv4addr", FR_CONF_POINTER(PW_TYPE_IPV4_PREFIX, &cl_ipaddr), NULL }, + { "ipv6addr", FR_CONF_POINTER(PW_TYPE_IPV6_PREFIX, &cl_ipaddr), NULL }, + + { "netmask", FR_CONF_POINTER(PW_TYPE_INTEGER, &cl_netmask), NULL }, + + { "src_ipaddr", FR_CONF_POINTER(PW_TYPE_STRING, &cl_srcipaddr), NULL }, + + { "require_message_authenticator", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, RADCLIENT, message_authenticator), "no" }, + + { "secret", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_SECRET, RADCLIENT, secret), NULL }, + { "shortname", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, shortname), NULL }, + + { "nas_type", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, nas_type), NULL }, + + { "login", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, login), NULL }, + { "password", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, password), NULL }, + { "virtual_server", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, server), NULL }, + { "response_window", FR_CONF_OFFSET(PW_TYPE_TIMEVAL, RADCLIENT, response_window), NULL }, + +#ifdef WITH_TCP + { "proto", FR_CONF_POINTER(PW_TYPE_STRING, &hs_proto), NULL }, + { "limit", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) limit_config }, +#endif + +#ifdef WITH_DYNAMIC_CLIENTS + { "dynamic_clients", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, client_server), NULL }, + { "lifetime", FR_CONF_OFFSET(PW_TYPE_INTEGER, RADCLIENT, lifetime), NULL }, + { "rate_limit", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, RADCLIENT, rate_limit), NULL }, +#endif + + CONF_PARSER_TERMINATOR +}; + +/** Create the linked list of clients from the new configuration type + * + */ +#ifdef WITH_TLS +RADCLIENT_LIST *client_list_parse_section(CONF_SECTION *section, bool tls_required) +#else +RADCLIENT_LIST *client_list_parse_section(CONF_SECTION *section, UNUSED bool tls_required) +#endif +{ + bool global = false, in_server = false; + CONF_SECTION *cs; + RADCLIENT *c = NULL; + RADCLIENT_LIST *clients = NULL; + + /* + * Be forgiving. If there's already a clients, return + * it. Otherwise create a new one. + */ + clients = cf_data_find(section, "clients"); + if (clients) { + /* + * Modules are initialized before the listeners. + * Which means that we MIGHT have read clients + * from SQL before parsing this "clients" + * section. So there may already be a clients + * list. + * + * But the list isn't _our_ list that we parsed, + * so we still need to parse the clients here. + */ + if (clients->parsed) return clients; + } else { + clients = client_list_init(section); + if (!clients) return NULL; + } + + if (cf_top_section(section) == section) { + global = true; + clients->name = "global"; + clients->server = NULL; + } + + if (strcmp("server", cf_section_name1(section)) == 0) { + clients->name = NULL; + clients->server = cf_section_name2(section); + in_server = true; + } + + for (cs = cf_subsection_find_next(section, NULL, "client"); + cs; + cs = cf_subsection_find_next(section, cs, "client")) { + c = client_afrom_cs(cs, cs, in_server, false); + if (!c) { + error: + client_free(c); + client_list_free(clients); + return NULL; + } + +#ifdef WITH_TLS + /* + * TLS clients CANNOT use non-TLS listeners. + * non-TLS clients CANNOT use TLS listeners. + */ + if (tls_required != c->tls_required) { + cf_log_err_cs(cs, "Client does not have the same TLS configuration as the listener"); + goto error; + } +#endif + + /* + * FIXME: Add the client as data via cf_data_add, + * for migration issues. + */ + +#ifdef WITH_DYNAMIC_CLIENTS +#ifdef HAVE_DIRENT_H + if (c->client_server) { + char const *value; + CONF_PAIR *cp; + DIR *dir; + struct dirent *dp; + struct stat stat_buf; + char buf2[2048]; + + /* + * Find the directory where individual + * client definitions are stored. + */ + cp = cf_pair_find(cs, "directory"); + if (!cp) goto add_client; + + value = cf_pair_value(cp); + if (!value) { + cf_log_err_cs(cs, "The \"directory\" entry must not be empty"); + goto error; + } + + DEBUG("including dynamic clients in %s", value); + + dir = opendir(value); + if (!dir) { + cf_log_err_cs(cs, "Error reading directory %s: %s", value, fr_syserror(errno)); + goto error; + } + + /* + * Read the directory, ignoring "." files. + */ + while ((dp = readdir(dir)) != NULL) { + char const *p; + RADCLIENT *dc; + + if (dp->d_name[0] == '.') continue; + + /* + * Check for valid characters + */ + for (p = dp->d_name; *p != '\0'; p++) { + if (isalpha((int)*p) || + isdigit((int)*p) || + (*p == ':') || + (*p == '.')) continue; + break; + } + if (*p != '\0') continue; + + snprintf(buf2, sizeof(buf2), "%s/%s", value, dp->d_name); + + if ((stat(buf2, &stat_buf) != 0) || S_ISDIR(stat_buf.st_mode)) continue; + + dc = client_read(buf2, in_server, true); + if (!dc) { + cf_log_err_cs(cs, "Failed reading client file \"%s\"", buf2); + closedir(dir); + goto error; + } + + /* + * Validate, and add to the list. + */ + if (!client_add_dynamic(clients, c, dc)) { + closedir(dir); + goto error; + } + } /* loop over the directory */ + closedir(dir); + } +#endif /* HAVE_DIRENT_H */ + + add_client: +#endif /* WITH_DYNAMIC_CLIENTS */ + if (!client_add(clients, c)) { + cf_log_err_cs(cs, "Failed to add client %s", cf_section_name2(cs)); + goto error; + } + + } + + /* + * Replace the global list of clients with the new one. + * The old one is still referenced from the original + * configuration, and will be freed when that is freed. + */ + if (global) root_clients = clients; + + clients->parsed = true; + return clients; +} + +#ifdef WITH_DYNAMIC_CLIENTS +/* + * We overload this structure a lot. + */ +static const CONF_PARSER dynamic_config[] = { + { "FreeRADIUS-Client-IP-Address", FR_CONF_OFFSET(PW_TYPE_IPV4_ADDR, RADCLIENT, ipaddr), NULL }, + { "FreeRADIUS-Client-IPv6-Address", FR_CONF_OFFSET(PW_TYPE_IPV6_ADDR, RADCLIENT, ipaddr), NULL }, + { "FreeRADIUS-Client-IP-Prefix", FR_CONF_OFFSET(PW_TYPE_IPV4_PREFIX, RADCLIENT, ipaddr), NULL }, + { "FreeRADIUS-Client-IPv6-Prefix", FR_CONF_OFFSET(PW_TYPE_IPV6_PREFIX, RADCLIENT, ipaddr), NULL }, + { "FreeRADIUS-Client-Src-IP-Address", FR_CONF_OFFSET(PW_TYPE_IPV4_ADDR, RADCLIENT, src_ipaddr), NULL }, + { "FreeRADIUS-Client-Src-IPv6-Address", FR_CONF_OFFSET(PW_TYPE_IPV6_ADDR, RADCLIENT, src_ipaddr), NULL }, + + { "FreeRADIUS-Client-Require-MA", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, RADCLIENT, message_authenticator), NULL }, + + { "FreeRADIUS-Client-Secret", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, secret), "" }, + { "FreeRADIUS-Client-Shortname", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, shortname), "" }, + { "FreeRADIUS-Client-NAS-Type", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, nas_type), NULL }, + { "FreeRADIUS-Client-Virtual-Server", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, server), NULL }, + + CONF_PARSER_TERMINATOR +}; + +/** Add a dynamic client + * + */ +bool client_add_dynamic(RADCLIENT_LIST *clients, RADCLIENT *master, RADCLIENT *c) +{ + char buffer[128]; + + /* + * No virtual server defined. Inherit the parent's + * definition. + */ + if (master->server && !c->server) { + c->server = talloc_typed_strdup(c, master->server); + } + + /* + * If the client network isn't global (not tied to a + * virtual server), then ensure that this clients server + * is the same as the enclosing networks virtual server. + */ + if (master->server && (strcmp(master->server, c->server) != 0)) { + ERROR("Cannot add client %s/%i: Virtual server %s is not the same as the virtual server for the network", + ip_ntoh(&c->ipaddr, buffer, sizeof(buffer)), c->ipaddr.prefix, c->server); + + goto error; + } + + if (!client_add(clients, c)) { + ERROR("Cannot add client %s/%i: Internal error", + ip_ntoh(&c->ipaddr, buffer, sizeof(buffer)), c->ipaddr.prefix); + + goto error; + } + + /* + * Initialize the remaining fields. + */ + c->dynamic = true; + c->lifetime = master->lifetime; + c->created = time(NULL); + c->longname = talloc_typed_strdup(c, c->shortname); + + if (rad_debug_lvl <= 2) { + INFO("Adding client %s/%i", + ip_ntoh(&c->ipaddr, buffer, sizeof(buffer)), c->ipaddr.prefix); + } else { + INFO("Adding client %s/%i with shared secret \"%s\"", + ip_ntoh(&c->ipaddr, buffer, sizeof(buffer)), c->ipaddr.prefix, c->secret); + } + return true; + +error: + client_free(c); + return false; +} + +/** Create a client CONF_SECTION using a mapping section to map values from a result set to client attributes + * + * If we hit a CONF_SECTION we recurse and process its CONF_PAIRS too. + * + * @note Caller should free CONF_SECTION passed in as out, on error. + * Contents of that section will be in an undefined state. + * + * @param[in,out] out Section to perform mapping on. Either the root of the client config, or a parent section + * (when this function is called recursively). + * Should be alloced with cf_section_alloc, or if there's a separate template section, the + * result of calling cf_section_dup on that section. + * @param[in] map section. + * @param[in] func to call to retrieve CONF_PAIR values. Must return a talloced buffer containing the value. + * @param[in] data to pass to func, usually a result pointer. + * @return 0 on success else -1 on error. + */ +int client_map_section(CONF_SECTION *out, CONF_SECTION const *map, client_value_cb_t func, void *data) +{ + CONF_ITEM const *ci; + + for (ci = cf_item_find_next(map, NULL); + ci != NULL; + ci = cf_item_find_next(map, ci)) { + CONF_PAIR const *cp; + CONF_PAIR *old; + char *value; + char const *attr; + + /* + * Recursively process map subsection + */ + if (cf_item_is_section(ci)) { + CONF_SECTION *cs, *cc; + + cs = cf_item_to_section(ci); + /* + * Use pre-existing section or alloc a new one + */ + cc = cf_section_sub_find_name2(out, cf_section_name1(cs), cf_section_name2(cs)); + if (!cc) { + cc = cf_section_alloc(out, cf_section_name1(cs), cf_section_name2(cs)); + cf_section_add(out, cc); + if (!cc) return -1; + } + + if (client_map_section(cc, cs, func, data) < 0) return -1; + continue; + } + + cp = cf_item_to_pair(ci); + attr = cf_pair_attr(cp); + + /* + * The callback can return 0 (success) and not provide a value + * in which case we skip the mapping pair. + * + * Or return -1 in which case we error out. + */ + if (func(&value, cp, data) < 0) { + cf_log_err_cs(out, "Failed performing mapping \"%s\" = \"%s\"", attr, cf_pair_value(cp)); + return -1; + } + if (!value) continue; + + /* + * Replace an existing CONF_PAIR + */ + old = cf_pair_find(out, attr); + if (old) { + cf_pair_replace(out, old, value); + talloc_free(value); + continue; + } + + /* + * ...or add a new CONF_PAIR + */ + cp = cf_pair_alloc(out, attr, value, T_OP_SET, T_BARE_WORD, T_SINGLE_QUOTED_STRING); + if (!cp) { + cf_log_err_cs(out, "Failed allocing pair \"%s\" = \"%s\"", attr, value); + talloc_free(value); + return -1; + } + talloc_free(value); + cf_item_add(out, cf_pair_to_item(cp)); + } + + return 0; +} + +/** Allocate a new client from a config section + * + * @param ctx to allocate new clients in. + * @param cs to process as a client. + * @param in_server Whether the client should belong to a specific virtual server. + * @param with_coa If true and coa_home_server or coa_home_pool aren't specified automatically, + * create a coa home_server section and add it to the client CONF_SECTION. + * @return new RADCLIENT struct. + */ +RADCLIENT *client_afrom_cs(TALLOC_CTX *ctx, CONF_SECTION *cs, bool in_server, bool with_coa) +{ + RADCLIENT *c; + char const *name2; + + name2 = cf_section_name2(cs); + if (!name2) { + cf_log_err_cs(cs, "Missing client name"); + return NULL; + } + + /* + * The size is fine.. Let's create the buffer + */ + c = talloc_zero(ctx, RADCLIENT); + c->cs = cs; + + memset(&cl_ipaddr, 0, sizeof(cl_ipaddr)); + cl_netmask = 255; + + if (cf_section_parse(cs, c, client_config) < 0) { + cf_log_err_cs(cs, "Error parsing client section"); + error: + client_free(c); +#ifdef WITH_TCP + hs_proto = NULL; + cl_srcipaddr = NULL; +#endif + + return NULL; + } + + /* + * Global clients can set servers to use, per-server clients cannot. + */ + if (in_server && c->server) { + cf_log_err_cs(cs, "Clients inside of an server section cannot point to a server"); + goto error; + } + + /* + * Allow the old method to specify "netmask". Just using "1.2.3.4" means it's a /32. + */ + if (cl_netmask != 255) { + if ((cl_ipaddr.prefix != cl_netmask) && + (((cl_ipaddr.af == AF_INET) && cl_ipaddr.prefix != 32) || + ((cl_ipaddr.af == AF_INET6) && cl_ipaddr.prefix != 128))) { + cf_log_err_cs(cs, "Clients cannot use 'ipaddr/mask' and 'netmask' at the same time."); + goto error; + } + + cl_ipaddr.prefix = cl_netmask; + } + + /* + * Newer style client definitions with either ipaddr or ipaddr6 + * config items. + */ + if (cf_pair_find(cs, "ipaddr") || cf_pair_find(cs, "ipv4addr") || cf_pair_find(cs, "ipv6addr")) { + char buffer[128]; + + /* + * Sets ipv4/ipv6 address and prefix. + */ + c->ipaddr = cl_ipaddr; + + /* + * Set the long name to be the result of a reverse lookup on the IP address. + */ + ip_ntoh(&c->ipaddr, buffer, sizeof(buffer)); + c->longname = talloc_typed_strdup(c, buffer); + + /* + * Set the short name to the name2. + */ + if (!c->shortname) c->shortname = talloc_typed_strdup(c, name2); + /* + * No "ipaddr" or "ipv6addr", use old-style "client <ipaddr> {" syntax. + */ + } else { + WARN("No 'ipaddr' or 'ipv4addr' or 'ipv6addr' field found in client %s. " + "Please fix your configuration", name2); + WARN("Support for old-style clients will be removed in a future release"); + +#ifdef WITH_TCP + if (cf_pair_find(cs, "proto") != NULL) { + cf_log_err_cs(cs, "Cannot use 'proto' inside of old-style client definition"); + goto error; + } +#endif + if (fr_pton(&c->ipaddr, name2, -1, AF_UNSPEC, true) < 0) { + cf_log_err_cs(cs, "Failed parsing client name \"%s\" as ip address or hostname: %s", name2, + fr_strerror()); + goto error; + } + + c->longname = talloc_typed_strdup(c, name2); + if (!c->shortname) c->shortname = talloc_typed_strdup(c, c->longname); + } + + c->proto = IPPROTO_UDP; + if (hs_proto) { + if (strcmp(hs_proto, "udp") == 0) { + hs_proto = NULL; + +#ifdef WITH_TCP + } else if (strcmp(hs_proto, "tcp") == 0) { + hs_proto = NULL; + c->proto = IPPROTO_TCP; +# ifdef WITH_TLS + } else if (strcmp(hs_proto, "tls") == 0) { + hs_proto = NULL; + c->proto = IPPROTO_TCP; + c->tls_required = true; + + } else if (strcmp(hs_proto, "radsec") == 0) { + hs_proto = NULL; + c->proto = IPPROTO_TCP; + c->tls_required = true; +# endif + } else if (strcmp(hs_proto, "*") == 0) { + hs_proto = NULL; + c->proto = IPPROTO_IP; /* fake for dual */ +#endif + } else { + cf_log_err_cs(cs, "Unknown proto \"%s\".", hs_proto); + goto error; + } + } + + /* + * If a src_ipaddr is specified, when we send the return packet + * we will use this address instead of the src from the + * request. + */ + if (cl_srcipaddr) { +#ifdef WITH_UDPFROMTO + switch (c->ipaddr.af) { + case AF_INET: + if (fr_pton4(&c->src_ipaddr, cl_srcipaddr, -1, true, false) < 0) { + cf_log_err_cs(cs, "Failed parsing src_ipaddr: %s", fr_strerror()); + goto error; + } + break; + + case AF_INET6: + if (fr_pton6(&c->src_ipaddr, cl_srcipaddr, -1, true, false) < 0) { + cf_log_err_cs(cs, "Failed parsing src_ipaddr: %s", fr_strerror()); + goto error; + } + break; + default: + rad_assert(0); + } +#else + WARN("Server not built with udpfromto, ignoring client src_ipaddr"); +#endif + cl_srcipaddr = NULL; + } + + /* + * A response_window of zero is OK, and means that it's + * ignored by the rest of the server timers. + */ + if (timerisset(&c->response_window)) { + FR_TIMEVAL_BOUND_CHECK("response_window", &c->response_window, >=, 0, 1000); + FR_TIMEVAL_BOUND_CHECK("response_window", &c->response_window, <=, 60, 0); + FR_TIMEVAL_BOUND_CHECK("response_window", &c->response_window, <=, main_config.max_request_time, 0); + } + +#ifdef WITH_DYNAMIC_CLIENTS + if (c->client_server) { + c->secret = talloc_typed_strdup(c, "testing123"); + + if (((c->ipaddr.af == AF_INET) && (c->ipaddr.prefix == 32)) || + ((c->ipaddr.af == AF_INET6) && (c->ipaddr.prefix == 128))) { + cf_log_err_cs(cs, "Dynamic clients MUST be a network, not a single IP address"); + goto error; + } + + return c; + } +#endif + + if (!c->secret || (c->secret[0] == '\0')) { +#ifdef WITH_DHCP + char const *value = NULL; + CONF_PAIR *cp = cf_pair_find(cs, "dhcp"); + + if (cp) value = cf_pair_value(cp); + + /* + * Secrets aren't needed for DHCP. + */ + if (value && (strcmp(value, "yes") == 0)) return c; +#endif + +#ifdef WITH_TLS + /* + * If the client is TLS only, the secret can be + * omitted. When omitted, it's hard-coded to + * "radsec". See RFC 6614. + */ + if (c->tls_required) { + c->secret = talloc_typed_strdup(cs, "radsec"); + } else +#endif + + { + cf_log_err_cs(cs, "secret must be at least 1 character long"); + goto error; + } + } + +#ifdef WITH_COA + { + CONF_PAIR *cp; + + /* + * Point the client to the home server pool, OR to the + * home server. This gets around the problem of figuring + * out which port to use. + */ + cp = cf_pair_find(cs, "coa_server"); + if (cp) { + c->coa_name = cf_pair_value(cp); + c->coa_home_pool = home_pool_byname(c->coa_name, HOME_TYPE_COA); + if (!c->coa_home_pool) { + c->coa_home_server = home_server_byname(c->coa_name, HOME_TYPE_COA); + } + if (!c->coa_home_pool && !c->coa_home_server) { + cf_log_err_cs(cs, "No such home_server or home_server_pool \"%s\"", c->coa_name); + goto error; + } + /* + * If we're implicitly adding a CoA home server for + * every client, or there's a server subsection, + * create a home server CONF_SECTION and then parse + * it into a home_server_t. + */ + } else if (with_coa || cf_section_sub_find(cs, "coa_server")) { + CONF_SECTION *server; + home_server_t *home; + + if (((c->ipaddr.af == AF_INET) && (c->ipaddr.prefix != 32)) || + ((c->ipaddr.af == AF_INET6) && (c->ipaddr.prefix != 128))) { + WARN("Subnets not supported for home servers. " + "Not adding client %s as home_server", name2); + goto done_coa; + } + + server = home_server_cs_afrom_client(cs); + if (!server) goto error; + + /* + * Must be allocated in the context of the client, + * as allocating using the context of the + * realm_config_t without a mutex, by one of the + * workers, would be bad. + */ + home = home_server_afrom_cs(NULL, NULL, server); + if (!home) { + talloc_free(server); + goto error; + } + + rad_assert(home->type == HOME_TYPE_COA); + + c->coa_home_server = home; + c->defines_coa_server = true; + } + } +done_coa: +#endif + +#ifdef WITH_TCP + if ((c->proto == IPPROTO_TCP) || (c->proto == IPPROTO_IP)) { + if ((c->limit.idle_timeout > 0) && (c->limit.idle_timeout < 5)) + c->limit.idle_timeout = 5; + if ((c->limit.lifetime > 0) && (c->limit.lifetime < 5)) + c->limit.lifetime = 5; + if ((c->limit.lifetime > 0) && (c->limit.idle_timeout > c->limit.lifetime)) + c->limit.idle_timeout = 0; + } +#endif + + return c; +} + +/** Add a client from a result set (SQL) + * + * @todo This function should die. SQL should use client_afrom_cs. + * + * @param ctx Talloc context. + * @param identifier Client IP Address / IPv4 subnet / IPv6 subnet / FQDN. + * @param secret Client secret. + * @param shortname Client friendly name. + * @param type NAS-Type. + * @param server Virtual-Server to associate clients with. + * @param require_ma If true all packets from client must include a message-authenticator. + * @return The new client, or NULL on error. + */ +RADCLIENT *client_afrom_query(TALLOC_CTX *ctx, char const *identifier, char const *secret, + char const *shortname, char const *type, char const *server, bool require_ma) +{ + RADCLIENT *c; + char buffer[128]; + + c = talloc_zero(ctx, RADCLIENT); + + if (fr_pton(&c->ipaddr, identifier, -1, AF_UNSPEC, true) < 0) { + ERROR("%s", fr_strerror()); + talloc_free(c); + + return NULL; + } + +#ifdef WITH_DYNAMIC_CLIENTS + c->dynamic = true; +#endif + ip_ntoh(&c->ipaddr, buffer, sizeof(buffer)); + c->longname = talloc_typed_strdup(c, buffer); + + /* + * Other values (secret, shortname, nas_type, virtual_server) + */ + c->secret = talloc_typed_strdup(c, secret); + if (shortname) c->shortname = talloc_typed_strdup(c, shortname); + if (type) c->nas_type = talloc_typed_strdup(c, type); + if (server) c->server = talloc_typed_strdup(c, server); + c->message_authenticator = require_ma; + + return c; +} + +/** Create a new client, consuming all attributes in the control list of the request + * + * @param clients list to add new client to. + * @param request Fake request. + * @return a new client on success, else NULL on error. + */ +RADCLIENT *client_afrom_request(RADCLIENT_LIST *clients, REQUEST *request) +{ + static int cnt; + int i, *pi; + char **p; + RADCLIENT *c; + char buffer[128]; + + vp_cursor_t cursor; + VALUE_PAIR *vp = NULL; + + if (!clients || !request) return NULL; + + /* + * Hack for the "dynamic_clients" module. + */ + if (request->client->dynamic) { + c = request->client; + goto validate; + } + + snprintf(buffer, sizeof(buffer), "dynamic%i", cnt++); + + c = talloc_zero(clients, RADCLIENT); + c->cs = cf_section_alloc(NULL, "client", buffer); + talloc_steal(c, c->cs); + c->ipaddr.af = AF_UNSPEC; + c->src_ipaddr.af = AF_UNSPEC; + + fr_cursor_init(&cursor, &request->config); + + RDEBUG2("Converting control list to client fields"); + RINDENT(); + for (i = 0; dynamic_config[i].name != NULL; i++) { + DICT_ATTR const *da; + char *strvalue = NULL; + CONF_PAIR *cp = NULL; + + da = dict_attrbyname(dynamic_config[i].name); + if (!da) { + RERROR("Cannot add client %s: attribute \"%s\" is not in the dictionary", + ip_ntoh(&request->packet->src_ipaddr, buffer, sizeof(buffer)), + dynamic_config[i].name); + error: + REXDENT(); + talloc_free(vp); + client_free(c); + return NULL; + } + + fr_cursor_first(&cursor); + if (!fr_cursor_next_by_da(&cursor, da, TAG_ANY)) { + /* + * Not required. Skip it. + */ + if (!dynamic_config[i].dflt) continue; + + RERROR("Cannot add client %s: Required attribute \"%s\" is missing", + ip_ntoh(&request->packet->src_ipaddr, buffer, sizeof(buffer)), + dynamic_config[i].name); + goto error; + } + vp = fr_cursor_remove(&cursor); + + /* + * Freed at the same time as the vp. + */ + strvalue = vp_aprints_value(vp, vp, '\''); + + switch (dynamic_config[i].type) { + case PW_TYPE_IPV4_ADDR: + if (da->attr == PW_FREERADIUS_CLIENT_IP_ADDRESS) { + c->ipaddr.af = AF_INET; + c->ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr; + c->ipaddr.prefix = 32; + cp = cf_pair_alloc(c->cs, "ipv4addr", strvalue, T_OP_SET, T_BARE_WORD, T_BARE_WORD); + } else if (da->attr == PW_FREERADIUS_CLIENT_SRC_IP_ADDRESS) { +#ifdef WITH_UDPFROMTO + RDEBUG2("src_ipaddr = %s", strvalue); + c->src_ipaddr.af = AF_INET; + c->src_ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr; + c->src_ipaddr.prefix = 32; + cp = cf_pair_alloc(c->cs, "src_ipaddr", strvalue, T_OP_SET, T_BARE_WORD, T_BARE_WORD); +#else + RWARN("Server not built with udpfromto, ignoring FreeRADIUS-Client-Src-IP-Address"); +#endif + } + + break; + + case PW_TYPE_IPV6_ADDR: + if (da->attr == PW_FREERADIUS_CLIENT_IPV6_ADDRESS) { + c->ipaddr.af = AF_INET6; + c->ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr; + c->ipaddr.prefix = 128; + cp = cf_pair_alloc(c->cs, "ipv6addr", strvalue, T_OP_SET, T_BARE_WORD, T_BARE_WORD); + } else if (da->attr == PW_FREERADIUS_CLIENT_SRC_IPV6_ADDRESS) { +#ifdef WITH_UDPFROMTO + c->src_ipaddr.af = AF_INET6; + c->src_ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr; + c->src_ipaddr.prefix = 128; + cp = cf_pair_alloc(c->cs, "src_addr", strvalue, T_OP_SET, T_BARE_WORD, T_BARE_WORD); +#else + RWARN("Server not built with udpfromto, ignoring FreeRADIUS-Client-Src-IPv6-Address"); +#endif + } + + break; + + case PW_TYPE_IPV4_PREFIX: + if (da->attr == PW_FREERADIUS_CLIENT_IP_PREFIX) { + c->ipaddr.af = AF_INET; + memcpy(&c->ipaddr.ipaddr.ip4addr, &vp->vp_ipv4prefix[2], + sizeof(c->ipaddr.ipaddr.ip4addr.s_addr)); + fr_ipaddr_mask(&c->ipaddr, (vp->vp_ipv4prefix[1] & 0x3f)); + cp = cf_pair_alloc(c->cs, "ipv4addr", strvalue, T_OP_SET, T_BARE_WORD, T_BARE_WORD); + } + + break; + + case PW_TYPE_IPV6_PREFIX: + if (da->attr == PW_FREERADIUS_CLIENT_IPV6_PREFIX) { + c->ipaddr.af = AF_INET6; + memcpy(&c->ipaddr.ipaddr.ip6addr, &vp->vp_ipv6prefix[2], + sizeof(c->ipaddr.ipaddr.ip6addr)); + fr_ipaddr_mask(&c->ipaddr, vp->vp_ipv6prefix[1]); + cp = cf_pair_alloc(c->cs, "ipv6addr", strvalue, T_OP_SET, T_BARE_WORD, T_BARE_WORD); + } + + break; + + case PW_TYPE_STRING: + { + CONF_PARSER const *parse; + + /* + * Cache pointer to CONF_PAIR buffer in RADCLIENT struct + */ + p = (char **) ((char *) c + dynamic_config[i].offset); + if (*p) TALLOC_FREE(*p); + if (!vp->vp_strvalue[0]) break; + + /* + * We could reuse the CONF_PAIR buff, this just keeps things + * consistent between client_afrom_cs, and client_afrom_query. + */ + *p = talloc_strdup(c, vp->vp_strvalue); + + /* + * This is fairly nasty... In order to figure out the CONF_PAIR + * name associated with a field, find offsets that match between + * the dynamic_config CONF_PARSER table, and the client_config + * CONF_PARSER table. + * + * This is so that things that expect to find CONF_PAIRs in the + * client CONF_SECTION for fields like 'nas_type' can. + */ + for (parse = client_config; parse->name; parse++) { + if (parse->offset == dynamic_config[i].offset) break; + } + + if (!parse) break; + + cp = cf_pair_alloc(c->cs, parse->name, strvalue, T_OP_SET, T_BARE_WORD, T_SINGLE_QUOTED_STRING); + } + break; + + case PW_TYPE_BOOLEAN: + { + CONF_PARSER const *parse; + + pi = (int *) ((bool *) ((char *) c + dynamic_config[i].offset)); + *pi = vp->vp_integer; + + /* + * Same nastiness as above. + */ + for (parse = client_config; parse->name; parse++) { + if (parse->offset == dynamic_config[i].offset) break; + } + if (!parse) break; + + cp = cf_pair_alloc(c->cs, parse->name, strvalue, T_OP_SET, T_BARE_WORD, T_BARE_WORD); + } + break; + + default: + goto error; + } + + if (!cp) { + RERROR("Error creating equivalent conf pair for %s", vp->da->name); + goto error; + } + + if (cf_pair_attr_type(cp) == T_SINGLE_QUOTED_STRING) { + RDEBUG2("%s = '%s'", cf_pair_attr(cp), cf_pair_value(cp)); + } else { + RDEBUG2("%s = %s", cf_pair_attr(cp), cf_pair_value(cp)); + } + cf_pair_add(c->cs, cp); + + talloc_free(vp); + } + + fr_cursor_first(&cursor); + vp = fr_cursor_remove(&cursor); + if (vp) { + CONF_PAIR *cp; + + do { + char *value; + + value = vp_aprints_value(vp, vp, '\''); + if (!value) { + ERROR("Failed stringifying value of &control:%s", vp->da->name); + goto error; + } + + if (vp->da->type == PW_TYPE_STRING) { + RDEBUG2("%s = '%s'", vp->da->name, value); + cp = cf_pair_alloc(c->cs, vp->da->name, value, T_OP_SET, + T_BARE_WORD, T_SINGLE_QUOTED_STRING); + } else { + RDEBUG2("%s = %s", vp->da->name, value); + cp = cf_pair_alloc(c->cs, vp->da->name, value, T_OP_SET, + T_BARE_WORD, T_BARE_WORD); + } + cf_pair_add(c->cs, cp); + + talloc_free(vp); + } while ((vp = fr_cursor_remove(&cursor))); + } + REXDENT(); + +validate: + if (c->ipaddr.af == AF_UNSPEC) { + RERROR("Cannot add client %s: No IP address was specified.", + ip_ntoh(&request->packet->src_ipaddr, buffer, sizeof(buffer))); + + goto error; + } + + { + fr_ipaddr_t addr; + + /* + * Need to apply the same mask as we set for the client + * else clients created with FreeRADIUS-Client-IPv6-Prefix + * or FreeRADIUS-Client-IPv4-Prefix will fail this check. + */ + addr = request->packet->src_ipaddr; + fr_ipaddr_mask(&addr, c->ipaddr.prefix); + if (fr_ipaddr_cmp(&addr, &c->ipaddr) != 0) { + char buf2[128]; + + RERROR("Cannot add client %s: Not in specified subnet %s/%i", + ip_ntoh(&request->packet->src_ipaddr, buffer, sizeof(buffer)), + ip_ntoh(&c->ipaddr, buf2, sizeof(buf2)), c->ipaddr.prefix); + goto error; + } + } + + if (!c->secret || !*c->secret) { + RERROR("Cannot add client %s: No secret was specified", + ip_ntoh(&request->packet->src_ipaddr, buffer, sizeof(buffer))); + goto error; + } + + if (!client_add_dynamic(clients, request->client, c)) { + return NULL; + } + + if ((c->src_ipaddr.af != AF_UNSPEC) && (c->src_ipaddr.af != c->ipaddr.af)) { + RERROR("Cannot add client %s: Client IP and src address are different IP version", + ip_ntoh(&request->packet->src_ipaddr, buffer, sizeof(buffer))); + + goto error; + } + + return c; +} + +/* + * Read a client definition from the given filename. + */ +RADCLIENT *client_read(char const *filename, int in_server, int flag) +{ + char const *p; + RADCLIENT *c; + CONF_SECTION *cs; + char buffer[256]; + + if (!filename) return NULL; + + cs = cf_section_alloc(NULL, "main", NULL); + if (!cs) return NULL; + + if (cf_file_read(cs, filename) < 0) { + talloc_free(cs); + return NULL; + } + + cs = cf_section_sub_find(cs, "client"); + if (!cs) { + ERROR("No \"client\" section found in client file"); + return NULL; + } + + c = client_afrom_cs(cs, cs, in_server, false); + if (!c) return NULL; + + p = strrchr(filename, FR_DIR_SEP); + if (p) { + p++; + } else { + p = filename; + } + + if (!flag) return c; + + /* + * Additional validations + */ + ip_ntoh(&c->ipaddr, buffer, sizeof(buffer)); + if (strcmp(p, buffer) != 0) { + ERROR("Invalid client definition in %s: IP address %s does not match name %s", filename, buffer, p); + client_free(c); + return NULL; + } + + return c; +} +#endif + |