diff options
Diffstat (limited to 'src/main')
62 files changed, 73439 insertions, 0 deletions
diff --git a/src/main/.gitignore b/src/main/.gitignore new file mode 100644 index 0000000..cc538fe --- /dev/null +++ b/src/main/.gitignore @@ -0,0 +1,13 @@ +Makefile +radsniff.mk +checkrad +radclient +radiusd +radlast +radtest +radsniff +radwho +radmin +radconf2xml +dhclient +*_ext diff --git a/src/main/acct.c b/src/main/acct.c new file mode 100644 index 0000000..90a0dd8 --- /dev/null +++ b/src/main/acct.c @@ -0,0 +1,186 @@ +/* + * acct.c Accounting routines. + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2000,2006 The FreeRADIUS server project + * Copyright 2000 Miquel van Smoorenburg <miquels@cistron.nl> + * Copyright 2000 Alan DeKok <aland@ox.org> + * Copyright 2000 Alan Curry <pacman@world.std.com> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/modules.h> + +#ifdef WITH_ACCOUNTING +/* + * rad_accounting: call modules. + * + * The return value of this function isn't actually used right now, so + * it's not entirely clear if it is returning the right things. --Pac. + */ +int rad_accounting(REQUEST *request) +{ + int result = RLM_MODULE_OK; + + +#ifdef WITH_PROXY +#define WAS_PROXIED (request->proxy) +#else +#define WAS_PROXIED (0) +#endif + + /* + * Run the modules only once, before proxying. + */ + if (!WAS_PROXIED) { + VALUE_PAIR *vp; + int acct_type = 0; + + result = module_preacct(request); + switch (result) { + /* + * The module has a number of OK return codes. + */ + case RLM_MODULE_NOOP: + case RLM_MODULE_OK: + case RLM_MODULE_UPDATED: + break; + /* + * The module handled the request, stop here. + */ + case RLM_MODULE_HANDLED: + return result; + /* + * The module failed, or said the request is + * invalid, therefore we stop here. + */ + case RLM_MODULE_FAIL: + case RLM_MODULE_INVALID: + case RLM_MODULE_NOTFOUND: + case RLM_MODULE_REJECT: + case RLM_MODULE_USERLOCK: + default: + return result; + } + + /* + * Do the data storage before proxying. This is to ensure + * that we log the packet, even if the proxy never does. + */ + vp = fr_pair_find_by_num(request->config, PW_ACCT_TYPE, 0, TAG_ANY); + if (vp) { + acct_type = vp->vp_integer; + DEBUG2(" Found Acct-Type %s", + dict_valnamebyattr(PW_ACCT_TYPE, 0, acct_type)); + } + result = process_accounting(acct_type, request); + switch (result) { + /* + * In case the accounting module returns FAIL, + * it's still useful to send the data to the + * proxy. + */ + case RLM_MODULE_FAIL: + case RLM_MODULE_NOOP: + case RLM_MODULE_OK: + case RLM_MODULE_UPDATED: + break; + /* + * The module handled the request, don't reply. + */ + case RLM_MODULE_HANDLED: + return result; + /* + * Neither proxy, nor reply to invalid requests. + */ + case RLM_MODULE_INVALID: + case RLM_MODULE_NOTFOUND: + case RLM_MODULE_REJECT: + case RLM_MODULE_USERLOCK: + default: + return result; + } + + /* + * Maybe one of the preacct modules has decided + * that a proxy should be used. + */ + if ((vp = fr_pair_find_by_num(request->config, PW_PROXY_TO_REALM, 0, TAG_ANY))) { + REALM *realm; + + /* + * Check whether Proxy-To-Realm is + * a LOCAL realm. + */ + realm = realm_find2(vp->vp_strvalue); + if (realm && !realm->acct_pool) { + DEBUG("rad_accounting: Cancelling proxy to realm %s, as it is a LOCAL realm.", realm->name); + fr_pair_delete_by_num(&request->config, PW_PROXY_TO_REALM, 0, TAG_ANY); + } else { + /* + * Don't reply to the NAS now because + * we have to send the proxied packet + * before that. + */ + return result; + } + } + } + +#ifdef WITH_PROXY + /* + * We didn't see a reply to the proxied request. Fail. + */ + if (request->proxy && !request->proxy_reply) return RLM_MODULE_FAIL; +#endif + + /* + * We get here IF we're not proxying, OR if we've + * received the accounting reply from the end server, + * THEN we can reply to the NAS. + * If the accounting module returns NOOP, the data + * storage did not succeed, so radiusd should not send + * Accounting-Response. + */ + switch (result) { + /* + * Send back an ACK to the NAS. + */ + case RLM_MODULE_OK: + case RLM_MODULE_UPDATED: + request->reply->code = PW_CODE_ACCOUNTING_RESPONSE; + break; + + /* + * Failed to log or to proxy the accounting data, + * therefore don't reply to the NAS. + */ + case RLM_MODULE_FAIL: + case RLM_MODULE_INVALID: + case RLM_MODULE_NOOP: + case RLM_MODULE_NOTFOUND: + case RLM_MODULE_REJECT: + case RLM_MODULE_USERLOCK: + default: + break; + } + return result; +} +#endif diff --git a/src/main/all.mk b/src/main/all.mk new file mode 100644 index 0000000..2517cd2 --- /dev/null +++ b/src/main/all.mk @@ -0,0 +1,3 @@ +SUBMAKEFILES := radclient.mk radiusd.mk radsniff.mk radmin.mk radattr.mk \ + radwho.mk radlast.mk radtest.mk radzap.mk checkrad.mk \ + libfreeradius-server.mk unittest.mk diff --git a/src/main/auth.c b/src/main/auth.c new file mode 100644 index 0000000..84889b8 --- /dev/null +++ b/src/main/auth.c @@ -0,0 +1,894 @@ +/* + * auth.c User authentication. + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2000,2006 The FreeRADIUS server project + * Copyright 2000 Miquel van Smoorenburg <miquels@cistron.nl> + * Copyright 2000 Jeff Carneal <jeff@apex.net> + */ +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/modules.h> +#include <freeradius-devel/state.h> +#include <freeradius-devel/rad_assert.h> + +#include <ctype.h> + +/* + * Return a short string showing the terminal server, port + * and calling station ID. + */ +char *auth_name(char *buf, size_t buflen, REQUEST *request, bool do_cli) +{ + VALUE_PAIR *cli; + VALUE_PAIR *pair; + uint32_t port = 0; /* RFC 2865 NAS-Port is 4 bytes */ + char const *tls = ""; + + if ((cli = fr_pair_find_by_num(request->packet->vps, PW_CALLING_STATION_ID, 0, TAG_ANY)) == NULL) { + do_cli = false; + } + + if ((pair = fr_pair_find_by_num(request->packet->vps, PW_NAS_PORT, 0, TAG_ANY)) != NULL) { + port = pair->vp_integer; + } + + if (request->packet->dst_port == 0) { + if (fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_PROXIED_TO, 0, TAG_ANY)) { + tls = " via TLS tunnel"; + } else { + tls = " via proxy to virtual server"; + } + } + + snprintf(buf, buflen, "from client %.128s port %u%s%.128s%s", + request->client->shortname, port, + (do_cli ? " cli " : ""), (do_cli ? cli->vp_strvalue : ""), + tls); + + return buf; +} + + + +/* + * Make sure user/pass are clean + * and then log them + */ +static int rad_authlog(char const *msg, REQUEST *request, int goodpass) +{ + int logit; + char const *extra_msg = NULL; + char clean_password[1024]; + char clean_username[1024]; + char buf[1024]; + char extra[1024]; + char *p; + VALUE_PAIR *username = NULL; + + if ((request->reply->code == PW_CODE_ACCESS_ACCEPT) && !request->root->log_accept) { + return 0; + } + + if ((request->reply->code == PW_CODE_ACCESS_REJECT) && !request->root->log_reject) { + return 0; + } + + /* + * Get the correct username based on the configured value + */ + if (!log_stripped_names) { + username = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY); + } else { + username = request->username; + } + + /* + * Clean up the username + */ + if (username == NULL) { + strcpy(clean_username, "<no User-Name attribute>"); + } else { + fr_prints(clean_username, sizeof(clean_username), username->vp_strvalue, username->vp_length, '\0'); + } + + /* + * Clean up the password + */ + if (request->root->log_auth_badpass || request->root->log_auth_goodpass) { + if (!request->password) { + VALUE_PAIR *auth_type; + + auth_type = fr_pair_find_by_num(request->config, PW_AUTH_TYPE, 0, TAG_ANY); + if (auth_type) { + snprintf(clean_password, sizeof(clean_password), + "<via Auth-Type = %s>", + dict_valnamebyattr(PW_AUTH_TYPE, 0, + auth_type->vp_integer)); + } else { + strcpy(clean_password, "<no User-Password attribute>"); + } + } else if (fr_pair_find_by_num(request->packet->vps, PW_CHAP_PASSWORD, 0, TAG_ANY)) { + strcpy(clean_password, "<CHAP-Password>"); + } else { + fr_prints(clean_password, sizeof(clean_password), + request->password->vp_strvalue, request->password->vp_length, '\0'); + } + } + + if (goodpass) { + logit = request->root->log_auth_goodpass; + extra_msg = request->root->auth_goodpass_msg; + } else { + logit = request->root->log_auth_badpass; + extra_msg = request->root->auth_badpass_msg; + } + + if (extra_msg) { + extra[0] = ' '; + p = extra + 1; + if (radius_xlat(p, sizeof(extra) - 1, request, extra_msg, NULL, NULL) < 0) { + return -1; + } + } else { + *extra = '\0'; + } + + RAUTH("%s: [%s%s%s] (%s)%s", + msg, + clean_username, + logit ? "/" : "", + logit ? clean_password : "", + auth_name(buf, sizeof(buf), request, 1), + extra); + + return 0; +} + +/* + * Check password. + * + * Returns: 0 OK + * -1 Password fail + * -2 Rejected (Auth-Type = Reject, send Port-Message back) + * 1 End check & return, don't reply + * + * NOTE: NOT the same as the RLM_ values ! + */ +static int CC_HINT(nonnull) rad_check_password(REQUEST *request) +{ + vp_cursor_t cursor; + VALUE_PAIR *auth_type_pair; + int auth_type = -1; + int result; + int auth_type_count = 0; + + /* + * Look for matching check items. We skip the whole lot + * if the authentication type is PW_AUTH_TYPE_ACCEPT or + * PW_AUTH_TYPE_REJECT. + */ + fr_cursor_init(&cursor, &request->config); + while ((auth_type_pair = fr_cursor_next_by_num(&cursor, PW_AUTH_TYPE, 0, TAG_ANY))) { + auth_type = auth_type_pair->vp_integer; + auth_type_count++; + + RDEBUG2("Found Auth-Type = %s", dict_valnamebyattr(PW_AUTH_TYPE, 0, auth_type)); + if (auth_type == PW_AUTH_TYPE_REJECT) { + RDEBUG2("Auth-Type = Reject, rejecting user"); + + return -2; + } + } + + /* + * Warn if more than one Auth-Type was found, because only the last + * one found will actually be used. + */ + if ((auth_type_count > 1) && (rad_debug_lvl) && request->username) { + RERROR("Warning: Found %d auth-types on request for user '%s'", + auth_type_count, request->username->vp_strvalue); + } + + /* + * This means we have a proxy reply or an accept and it wasn't + * rejected in the above loop. So that means it is accepted and we + * do no further authentication. + */ + if ((auth_type == PW_AUTH_TYPE_ACCEPT) +#ifdef WITH_PROXY + || (request->proxy) +#endif + ) { + RDEBUG2("Auth-Type = Accept, accepting the user"); + return 0; + } + + /* + * Check that Auth-Type has been set, and reject if not. + * + * Do quick checks to see if Cleartext-Password or Crypt-Password have + * been set, and complain if so. + */ + if (auth_type < 0) { + if (fr_pair_find_by_num(request->config, PW_CRYPT_PASSWORD, 0, TAG_ANY) != NULL) { + RWDEBUG2("No module configured to handle comparisons with &control:Crypt-Password"); + RWDEBUG2("Add pap to the authorize { ... } and authenticate { ... } sections of this " + "virtual server to handle this \"known good\" password type"); + } + else if (fr_pair_find_by_num(request->config, PW_CLEARTEXT_PASSWORD, 0, TAG_ANY) != NULL) { + RWDEBUG2("No module configured to handle comparisons with &control:Cleartext-Password"); + RWDEBUG2("Add pap or chap to the authorize { ... } and authenticate { ... } sections " + "of this virtual server to handle this \"known good\" password type"); + } + + /* + * The admin hasn't told us how to + * authenticate the user, so we reject them! + * + * This is fail-safe. + */ + + REDEBUG2("No Auth-Type found: rejecting the user via Post-Auth-Type = Reject"); + return -2; + } + + /* + * See if there is a module that handles + * this Auth-Type, and turn the RLM_ return + * status into the values as defined at + * the top of this function. + */ + result = process_authenticate(auth_type, request); + switch (result) { + /* + * An authentication module FAIL + * return code, or any return code that + * is not expected from authentication, + * is the same as an explicit REJECT! + */ + case RLM_MODULE_FAIL: + case RLM_MODULE_INVALID: + case RLM_MODULE_NOOP: + case RLM_MODULE_NOTFOUND: + case RLM_MODULE_REJECT: + case RLM_MODULE_UPDATED: + case RLM_MODULE_USERLOCK: + default: + result = -1; + break; + + case RLM_MODULE_OK: + result = 0; + break; + + case RLM_MODULE_HANDLED: + result = 1; + break; + } + + return result; +} + +/* + * Post-authentication step processes the response before it is + * sent to the NAS. It can receive both Access-Accept and Access-Reject + * replies. + */ +int rad_postauth(REQUEST *request) +{ + int result; + int postauth_type = 0; + VALUE_PAIR *vp; + + if (request->reply->code == PW_CODE_ACCESS_CHALLENGE) { + fr_pair_delete_by_num(&request->config, PW_POST_AUTH_TYPE, 0, TAG_ANY); + vp = pair_make_config("Post-Auth-Type", "Challenge", T_OP_SET); + if (!vp) return RLM_MODULE_OK; + + } else if (request->reply->code == PW_CODE_ACCESS_REJECT) { + fr_pair_delete_by_num(&request->config, PW_POST_AUTH_TYPE, 0, TAG_ANY); + vp = pair_make_config("Post-Auth-Type", "Reject", T_OP_SET); + if (!vp) return RLM_MODULE_OK; + + } else { + vp = fr_pair_find_by_num(request->config, PW_POST_AUTH_TYPE, 0, TAG_ANY); + } + + /* + * If a method was chosen, use that. + */ + if (vp) { + postauth_type = vp->vp_integer; + RDEBUG2("Using Post-Auth-Type %s", + dict_valnamebyattr(PW_POST_AUTH_TYPE, 0, postauth_type)); + + if (postauth_type == PW_POST_AUTH_TYPE_CHALLENGE) request->reply->code = PW_CODE_ACCESS_CHALLENGE; + + if (postauth_type == PW_POST_AUTH_TYPE_REJECT) request->reply->code = PW_CODE_ACCESS_REJECT; + } + + result = process_post_auth(postauth_type, request); + switch (result) { + /* + * The module failed, or said to reject the user: Do so. + */ + case RLM_MODULE_FAIL: + case RLM_MODULE_INVALID: + case RLM_MODULE_REJECT: + case RLM_MODULE_USERLOCK: + default: + /* + * We WERE going to have a nice reply, but + * something went wrong. So we've got to run + * Post-Auth-Type Reject. + */ + if (request->reply->code != PW_CODE_ACCESS_REJECT) { + RDEBUG("Using Post-Auth-Type Reject"); + + request->reply->code = PW_CODE_ACCESS_REJECT; + process_post_auth(PW_POST_AUTH_TYPE_REJECT, request); + } + + /* + * Only discard session state when we're sending + * packets to the network. The State attribute + * is use both for the outer session and copied + * to the inner-tunnel session for (e.g.) PEAP. + * So we don't want to delete the information in + * the inner tunnel, and then have it no longer + * accessible from the outer session. + */ + if (!request->parent) fr_state_discard(request, request->packet); + result = RLM_MODULE_REJECT; + break; + /* + * The module handled the request, cancel the reply. + */ + case RLM_MODULE_HANDLED: + /* FIXME */ + break; + /* + * The module had a number of OK return codes. + */ + case RLM_MODULE_NOOP: + case RLM_MODULE_NOTFOUND: + case RLM_MODULE_OK: + case RLM_MODULE_UPDATED: + result = RLM_MODULE_OK; + + if (request->reply->code == PW_CODE_ACCESS_CHALLENGE) { + fr_state_put_vps(request, request->packet, request->reply); + + } else { + fr_state_discard(request, request->packet); + } + break; + } + + /* + * Rejects during authorize, etc. are handled by the + * earlier code, which logs a reason for the rejection. + * If the packet is rejected in post-auth, we need to log + * that as a separate reason. + */ + if (result == RLM_MODULE_REJECT) { + if (request->reply->code != RLM_MODULE_REJECT) { + rad_authlog("Rejected in post-auth", request, 0); + } + request->reply->code = PW_CODE_ACCESS_REJECT; + } + + if (request->reply->code == PW_CODE_ACCESS_REJECT) { + if ((vp = fr_pair_find_by_num(request->packet->vps, PW_MODULE_FAILURE_MESSAGE, 0, TAG_ANY)) != NULL) { + char msg[MAX_STRING_LEN+19]; + + snprintf(msg, sizeof(msg), "Login incorrect (%s)", + vp->vp_strvalue); + rad_authlog(msg, request, 0); + } else { + rad_authlog("Login incorrect", request, 0); + } + } + + /* + * If we're still accepting the user, say so. + */ + if (request->reply->code == PW_CODE_ACCESS_ACCEPT) { + if ((vp = fr_pair_find_by_num(request->packet->vps, PW_MODULE_SUCCESS_MESSAGE, 0, TAG_ANY)) != NULL) { + char msg[MAX_STRING_LEN+12]; + + snprintf(msg, sizeof(msg), "Login OK (%s)", + vp->vp_strvalue); + rad_authlog(msg, request, 1); + } else { + rad_authlog("Login OK", request, 1); + } + } + + return result; +} + +/* + * Process and reply to an authentication request + * + * The return value of this function isn't actually used right now, so + * it's not entirely clear if it is returning the right things. --Pac. + */ +int rad_authenticate(REQUEST *request) +{ +#ifdef WITH_SESSION_MGMT + VALUE_PAIR *check_item; +#endif + VALUE_PAIR *module_msg; + VALUE_PAIR *tmp = NULL; + int result; + char autz_retry = 0; + int autz_type = 0; + +#ifdef WITH_PROXY + /* + * If this request got proxied to another server, we need + * to check whether it authenticated the request or not. + * + * request->proxy gets set only AFTER authorization, so + * it's safe to check it here. If it exists, it means + * we're doing a second pass through rad_authenticate(). + */ + if (request->proxy) { + int code = 0; + + if (request->proxy_reply) code = request->proxy_reply->code; + + switch (code) { + /* + * Reply of ACCEPT means accept, thus set Auth-Type + * accordingly. + */ + case PW_CODE_ACCESS_ACCEPT: + tmp = radius_pair_create(request, + &request->config, + PW_AUTH_TYPE, 0); + if (tmp) tmp->vp_integer = PW_AUTH_TYPE_ACCEPT; + goto authenticate; + + /* + * Challenges are punted back to the NAS without any + * further processing. + */ + case PW_CODE_ACCESS_CHALLENGE: + request->reply->code = PW_CODE_ACCESS_CHALLENGE; + fr_state_put_vps(request, request->packet, request->reply); + return RLM_MODULE_OK; + + /* + * ALL other replies mean reject. (this is fail-safe) + * + * Do NOT do any authorization or authentication. They + * are being rejected, so we minimize the amount of work + * done by the server, by rejecting them here. + */ + case PW_CODE_ACCESS_REJECT: + request->reply->code = PW_CODE_ACCESS_REJECT; + rad_authlog("Login incorrect (Home Server says so)", + request, 0); + return RLM_MODULE_REJECT; + + default: + rad_authlog("Login incorrect (Home Server failed to respond)", + request, 0); + return RLM_MODULE_REJECT; + } + } +#endif + /* + * Look for, and cache, passwords. + */ + if (!request->password) { + request->password = fr_pair_find_by_num(request->packet->vps, PW_USER_PASSWORD, 0, TAG_ANY); + } + if (!request->password) { + request->password = fr_pair_find_by_num(request->packet->vps, PW_CHAP_PASSWORD, 0, TAG_ANY); + } + + /* + * Grab the VPS associated with the State attribute. + */ + fr_state_get_vps(request, request->packet); + + /* + * Get the user's authorization information from the database + */ +autz_redo: + result = process_authorize(autz_type, request); + switch (result) { + case RLM_MODULE_NOOP: + case RLM_MODULE_NOTFOUND: + case RLM_MODULE_OK: + case RLM_MODULE_UPDATED: + break; + case RLM_MODULE_HANDLED: + return result; + case RLM_MODULE_FAIL: + case RLM_MODULE_INVALID: + case RLM_MODULE_REJECT: + case RLM_MODULE_USERLOCK: + default: + request->reply->code = PW_CODE_ACCESS_REJECT; + if ((module_msg = fr_pair_find_by_num(request->packet->vps, PW_MODULE_FAILURE_MESSAGE, 0, TAG_ANY)) != NULL) { + char msg[MAX_STRING_LEN + 16]; + snprintf(msg, sizeof(msg), "Invalid user (%s)", + module_msg->vp_strvalue); + rad_authlog(msg,request,0); + } else { + rad_authlog("Invalid user", request, 0); + } + return result; + } + if (!autz_retry) { + tmp = fr_pair_find_by_num(request->config, PW_AUTZ_TYPE, 0, TAG_ANY); + if (tmp) { + autz_type = tmp->vp_integer; + RDEBUG2("Using Autz-Type %s", + dict_valnamebyattr(PW_AUTZ_TYPE, 0, autz_type)); + autz_retry = 1; + goto autz_redo; + } + } + + /* + * If we haven't already proxied the packet, then check + * to see if we should. Maybe one of the authorize + * modules has decided that a proxy should be used. If + * so, get out of here and send the packet. + */ +#ifdef WITH_PROXY + if (request->proxy == NULL) +#endif + { + if ((tmp = fr_pair_find_by_num(request->config, PW_PROXY_TO_REALM, 0, TAG_ANY)) != NULL) { + REALM *realm; + + realm = realm_find2(tmp->vp_strvalue); + + /* + * Don't authenticate, as the request is going to + * be proxied. + */ + if (realm && realm->auth_pool) { + return RLM_MODULE_OK; + } + + /* + * Catch users who set Proxy-To-Realm to a LOCAL + * realm (sigh). But don't complain if it is + * *the* LOCAL realm. + */ + if (realm && (strcmp(realm->name, "LOCAL") != 0)) { + RWDEBUG2("You set Proxy-To-Realm = %s, but it is a LOCAL realm! Cancelling proxy request.", realm->name); + } + + if (!realm) { + RWDEBUG2("You set Proxy-To-Realm = %s, but the realm does not exist! Cancelling invalid proxy request.", tmp->vp_strvalue); + } + } else if (((tmp = fr_pair_find_by_num(request->config, PW_HOME_SERVER_POOL, 0, TAG_ANY)) != NULL) || + ((tmp = fr_pair_find_by_num(request->config, PW_PACKET_DST_IP_ADDRESS, 0, TAG_ANY)) != NULL) || + ((tmp = fr_pair_find_by_num(request->config, PW_PACKET_DST_IPV6_ADDRESS, 0, TAG_ANY)) != NULL) || + ((tmp = fr_pair_find_by_num(request->config, PW_HOME_SERVER_NAME, 0, TAG_ANY)) != NULL)) { + RDEBUG("Proxying due to %s", tmp->da->name); + return RLM_MODULE_OK; + } + } + +#ifdef WITH_PROXY +authenticate: +#endif + + /* + * Validate the user + */ + do { + result = rad_check_password(request); + if (result > 0) { + return RLM_MODULE_HANDLED; + } + + } while(0); + + /* + * Failed to validate the user. + * + * We PRESUME that the code which failed will clean up + * request->reply->vps, to be ONLY the reply items it + * wants to send back. + */ + if (result < 0) { + RDEBUG2("Failed to authenticate the user"); + request->reply->code = PW_CODE_ACCESS_REJECT; + + if (request->password) { + VERIFY_VP(request->password); + /* double check: maybe the secret is wrong? */ + if ((rad_debug_lvl > 1) && (request->password->da->attr == PW_USER_PASSWORD)) { + uint8_t const *p; + + p = (uint8_t const *) request->password->vp_strvalue; + while (*p) { + int size; + + size = fr_utf8_char(p, -1); + if (!size) { + RWDEBUG("Unprintable characters in the password. Double-check the " + "shared secret on the server and the NAS!"); + break; + } + p += size; + } + } + } + } + +#ifdef WITH_SESSION_MGMT + if (result >= 0 && + (check_item = fr_pair_find_by_num(request->config, PW_SIMULTANEOUS_USE, 0, TAG_ANY)) != NULL) { + int r, session_type = 0; + char logstr[1024]; + char umsg[MAX_STRING_LEN + 1]; + + tmp = fr_pair_find_by_num(request->config, PW_SESSION_TYPE, 0, TAG_ANY); + if (tmp) { + session_type = tmp->vp_integer; + RDEBUG2("Using Session-Type %s", + dict_valnamebyattr(PW_SESSION_TYPE, 0, session_type)); + } + + /* + * User authenticated O.K. Now we have to check + * for the Simultaneous-Use parameter. + */ + if (request->username && + (r = process_checksimul(session_type, request, check_item->vp_integer)) != 0) { + char mpp_ok = 0; + + if (r == 2){ + /* Multilink attempt. Check if port-limit > simultaneous-use */ + VALUE_PAIR *port_limit; + + if ((port_limit = fr_pair_find_by_num(request->reply->vps, PW_PORT_LIMIT, 0, TAG_ANY)) != NULL && + port_limit->vp_integer > check_item->vp_integer){ + RDEBUG2("MPP is OK"); + mpp_ok = 1; + } + } + if (!mpp_ok){ + if (check_item->vp_integer > 1) { + snprintf(umsg, sizeof(umsg), "%s (%u)", main_config.denied_msg, + check_item->vp_integer); + } else { + strlcpy(umsg, main_config.denied_msg, sizeof(umsg)); + } + + request->reply->code = PW_CODE_ACCESS_REJECT; + + /* + * They're trying to log in too many times. + * Remove ALL reply attributes. + */ + fr_pair_list_free(&request->reply->vps); + pair_make_reply("Reply-Message", umsg, T_OP_SET); + + snprintf(logstr, sizeof(logstr), "Multiple logins (max %d) %s", + check_item->vp_integer, + r == 2 ? "[MPP attempt]" : ""); + rad_authlog(logstr, request, 1); + + result = -1; + } + } + } +#endif + + /* + * Result should be >= 0 here - if not, it means the user + * is rejected, so we just process post-auth and return. + */ + if (result < 0) { + return RLM_MODULE_REJECT; + } + + /* + * Set the reply to Access-Accept, if it hasn't already + * been set to something. (i.e. Access-Challenge) + */ + if (request->reply->code == 0) { + request->reply->code = PW_CODE_ACCESS_ACCEPT; + } + + return result; +} + +/* + * Run a virtual server auth and postauth + * + */ +int rad_virtual_server(REQUEST *request) +{ + VALUE_PAIR *vp; + int result; + + RDEBUG("Virtual server %s received request", request->server); + rdebug_pair_list(L_DBG_LVL_1, request, request->packet->vps, NULL); + + if (!request->username) { + request->username = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY); + } + + /* + * Complain about possible issues related to tunnels. + */ + if (request->parent && request->parent->username && request->username) { + /* + * Look at the full User-Name with realm. + */ + if (request->parent->username->da->attr == PW_STRIPPED_USER_NAME) { + vp = fr_pair_find_by_num(request->parent->packet->vps, PW_USER_NAME, 0, TAG_ANY); + rad_assert(vp != NULL); + } else { + vp = request->parent->username; + } + + /* + * If the names aren't identical, we do some detailed checks. + */ + if (strcmp(vp->vp_strvalue, request->username->vp_strvalue) != 0) { + char const *outer, *inner; + + outer = strchr(vp->vp_strvalue, '@'); + + /* + * If there's no realm, or there's a user identifier before + * the realm name, check the user identifier. + * + * It SHOULD be "anonymous", or "anonymous@realm" + */ + if (outer) { + if ((outer != vp->vp_strvalue) && + ((vp->vp_length < 10) || (memcmp(vp->vp_strvalue, "anonymous@", 10) != 0))) { + RWDEBUG("Outer User-Name is not anonymized. User privacy is compromised."); + } /* else it is anonymized */ + + /* + * Check when there's no realm, and without the trailing '@' + */ + } else if ((vp->vp_length < 9) || (memcmp(vp->vp_strvalue, "anonymous", 9) != 0)) { + RWDEBUG("Outer User-Name is not anonymized. User privacy is compromised."); + + } /* else the user identifier is anonymized */ + + /* + * Look for an inner realm, which may or may not exist. + */ + inner = strchr(request->username->vp_strvalue, '@'); + if (outer && inner) { + outer++; + inner++; + + /* + * The realms are different, do + * more detailed checks. + */ + if (strcmp(outer, inner) != 0) { + size_t outer_len, inner_len; + + outer_len = vp->vp_length; + outer_len -= (outer - vp->vp_strvalue); + + inner_len = request->username->vp_length; + inner_len -= (inner - request->username->vp_strvalue); + + /* + * Inner: secure.example.org + * Outer: example.org + */ + if (inner_len > outer_len) { + char const *suffix; + + suffix = inner + (inner_len - outer_len) - 1; + + if ((*suffix != '.') || + (strcmp(suffix + 1, outer) != 0)) { + RWDEBUG("Possible spoofing: Inner realm '%s' is not a subdomain of the outer realm '%s'", inner, outer); + } + + } else { + RWDEBUG("Possible spoofing: Inner realm and outer realms are different"); + } + } + } + + } else { + RWDEBUG("Outer and inner identities are the same. User privacy is compromised."); + } + } + + RDEBUG("server %s {", request->server); + RINDENT(); + + /* + * We currently only handle AUTH packets here. + * This could be expanded to handle other packets as well if required. + */ + rad_assert(request->packet->code == PW_CODE_ACCESS_REQUEST); + + result = rad_authenticate(request); + + /* + * Allow bare "accept" and "reject" policies in the inner + * tunnel. + */ + if (!request->reply->code && + (vp = fr_pair_find_by_num(request->config, PW_AUTH_TYPE, 0, TAG_ANY)) != NULL) { + switch (vp->vp_integer) { + case PW_AUTH_TYPE_ACCEPT: + request->reply->code = PW_CODE_ACCESS_ACCEPT; + break; + + case PW_AUTH_TYPE_REJECT: + request->reply->code = PW_CODE_ACCESS_REJECT; + break; + + default: + break; + } + } + + if (request->reply->code == PW_CODE_ACCESS_REJECT) { + fr_pair_delete_by_num(&request->config, PW_POST_AUTH_TYPE, 0, TAG_ANY); + vp = pair_make_config("Post-Auth-Type", "Reject", T_OP_SET); + if (vp) rad_postauth(request); + } + + if (request->reply->code == PW_CODE_ACCESS_ACCEPT) { + /* + * Check that there is a name which can be used + * to identify the user. The configuration + * depends on User-Name or Stripped-User-Name + * existing, and being (mostly) unique to that + * user. + */ + if (!request->parent && request->username && + (request->username->da->attr == PW_USER_NAME) && + (request->username->vp_strvalue[0] == '@') && + !fr_pair_find_by_num(request->packet->vps, PW_STRIPPED_USER_NAME, 0, TAG_ANY)) { + RWDEBUG("User-Name is anonymized, and no Stripped-User-Name exists."); + RWDEBUG("It may be difficult or impossible to identify the user"); + RWDEBUG("Please update Stripped-User-Name with information which identifies the user"); + } + + rad_postauth(request); + } + + REXDENT(); + RDEBUG("} # server %s", request->server); + + RDEBUG("Virtual server sending reply"); + rdebug_pair_list(L_DBG_LVL_1, request, request->reply->vps, NULL); + + return result; +} diff --git a/src/main/cb.c b/src/main/cb.c new file mode 100644 index 0000000..db764aa --- /dev/null +++ b/src/main/cb.c @@ -0,0 +1,247 @@ +/* + * cb.c + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2001 hereUare Communications, Inc. <raghud@hereuare.com> + * Copyright 2006 The FreeRADIUS server project + */ + +RCSID("$Id$") +USES_APPLE_DEPRECATED_API /* OpenSSL API has been deprecated by Apple */ + +#include <freeradius-devel/radiusd.h> + +#ifdef WITH_TLS +void cbtls_info(SSL const *s, int where, int ret) +{ + char const *role, *state; + REQUEST *request = SSL_get_ex_data(s, FR_TLS_EX_INDEX_REQUEST); + + if ((where & ~SSL_ST_MASK) & SSL_ST_CONNECT) { + role = "Client "; + } else if (((where & ~SSL_ST_MASK)) & SSL_ST_ACCEPT) { + role = "Server "; + } else { + role = ""; + } + + state = SSL_state_string_long(s); + state = state ? state : "<none>"; + + if ((where & SSL_CB_LOOP) || (where & SSL_CB_HANDSHAKE_START) || (where & SSL_CB_HANDSHAKE_DONE)) { + if (RDEBUG_ENABLED3) { + char const *abbrv = SSL_state_string(s); + size_t len; +#if OPENSSL_VERSION_NUMBER >= 0x10100000L + STACK_OF(SSL_CIPHER) *client_ciphers; + STACK_OF(SSL_CIPHER) *server_ciphers; +#endif + + /* + * Trim crappy OpenSSL state strings... + */ + len = strlen(abbrv); + if ((len > 1) && (abbrv[len - 1] == ' ')) len--; + + RDEBUG3("(TLS) Handshake state [%.*s] - %s%s (%d)", + (int)len, abbrv, role, state, SSL_get_state(s)); + + /* + * After a ClientHello, list all the proposed ciphers from the client + */ +#if OPENSSL_VERSION_NUMBER >= 0x10100000L + if (SSL_get_state(s) == TLS_ST_SR_CLNT_HELLO) { + int i; + int num_ciphers; + const SSL_CIPHER *this_cipher; + + server_ciphers = SSL_get_ciphers(s); + if (server_ciphers) { + RDEBUG3("Server preferred ciphers (by priority)"); + num_ciphers = sk_SSL_CIPHER_num(server_ciphers); + for (i = 0; i < num_ciphers; i++) { + this_cipher = sk_SSL_CIPHER_value(server_ciphers, i); + RDEBUG3("(TLS) [%i] %s", i, SSL_CIPHER_get_name(this_cipher)); + } + } + + client_ciphers = SSL_get_client_ciphers(s); + if (client_ciphers) { + RDEBUG3("Client preferred ciphers (by priority)"); + num_ciphers = sk_SSL_CIPHER_num(client_ciphers); + for (i = 0; i < num_ciphers; i++) { + this_cipher = sk_SSL_CIPHER_value(client_ciphers, i); + RDEBUG3("(TLS) [%i] %s", i, SSL_CIPHER_get_name(this_cipher)); + } + } + } +#endif + } else { + RDEBUG2("(TLS) Handshake state - %s%s", role, state); + } + return; + } + + if (where & SSL_CB_ALERT) { + if ((ret & 0xff) == SSL_AD_CLOSE_NOTIFY) return; + + RERROR("(TLS) Alert %s:%s:%s", (where & SSL_CB_READ) ? "read": "write", + SSL_alert_type_string_long(ret), SSL_alert_desc_string_long(ret)); + return; + } + + if (where & SSL_CB_EXIT) { + if (ret == 0) { + RERROR("(TLS) %s: Failed in %s", role, state); + return; + } + + if (ret < 0) { + if (SSL_want_read(s)) { + RDEBUG2("(TLS) %s: Need to read more data: %s", role, state); + return; + } + RERROR("(TLS) %s: Error in %s", role, state); + } + } +} + +/* + * Fill in our 'info' with TLS data. + */ +void cbtls_msg(int write_p, int msg_version, int content_type, + void const *inbuf, size_t len, + SSL *ssl UNUSED, void *arg) +{ + uint8_t const *buf = inbuf; + tls_session_t *state = (tls_session_t *)arg; + + /* + * OpenSSL 1.0.2 calls this function with 'pseudo' + * content types. Which breaks our tracking of + * the SSL Session state. + */ +#if OPENSSL_VERSION_NUMBER < 0x30000000L + if ((msg_version == 0) && (content_type > UINT8_MAX)) { +#else + /* + * "...we do not see the need to resolve application breakage + * just because the documentation now is incorrect." + * + * https://github.com/openssl/openssl/issues/17262 + */ + if ((content_type > UINT8_MAX) && (content_type != SSL3_RT_INNER_CONTENT_TYPE)) { +#endif + DEBUG4("(TLS) Ignoring cbtls_msg call with pseudo content type %i, version %08x", + content_type, msg_version); + return; + } + + if ((write_p != 0) && (write_p != 1)) { + DEBUG4("(TLS) Ignoring cbtls_msg call with invalid write_p %d", write_p); + return; + } + + /* + * Work around bug #298, where we may be called with a NULL + * argument. We should really log a serious error + */ + if (!state) return; + + if (rad_debug_lvl > 3) { + size_t i, j, data_len = len; + char buffer[3*16 + 1]; + uint8_t const *in = inbuf; + + DEBUG("(TLS) Received %zu bytes of TLS data", len); + if (data_len > 256) data_len = 256; + + for (i = 0; i < data_len; i += 16) { + for (j = 0; j < 16; j++) { + if ((i + j) >= data_len) break; + + sprintf(buffer + 3 * j, "%02x ", in[i + j]); + } + + DEBUG("(TLS) %s", buffer); + } + } + + /* + * 0 - received (from peer) + * 1 - sending (to peer) + */ + state->info.origin = write_p; + state->info.content_type = content_type; + state->info.record_len = len; + state->info.initialized = true; + + if (content_type == SSL3_RT_ALERT) { + state->info.alert_level = buf[0]; + state->info.alert_description = buf[1]; + state->info.handshake_type = 0x00; + + } else if (content_type == SSL3_RT_HANDSHAKE) { + state->info.handshake_type = buf[0]; + state->info.alert_level = 0x00; + state->info.alert_description = 0x00; + +#if OPENSSL_VERSION_NUMBER >= 0x10101000L + } else if (content_type == SSL3_RT_INNER_CONTENT_TYPE && buf[0] == SSL3_RT_APPLICATION_DATA) { + /* let tls_ack_handler set application_data */ + state->info.content_type = SSL3_RT_HANDSHAKE; +#endif + +#ifdef SSL3_RT_HEARTBEAT + } else if (content_type == TLS1_RT_HEARTBEAT) { + uint8_t *p = buf; + + if ((len >= 3) && (p[0] == 1)) { + size_t payload_len; + + payload_len = (p[1] << 8) | p[2]; + + if ((payload_len + 3) > len) { + state->invalid_hb_used = true; + ERROR("OpenSSL Heartbeat attack detected. Closing connection"); + return; + } + } +#endif + } + + tls_session_information(state); +} + +int cbtls_password(char *buf, + int num, + int rwflag UNUSED, + void *userdata) +{ + size_t len; + + len = strlcpy(buf, (char *)userdata, num); + if (len >= (size_t) num) { + ERROR("Password too long. Maximum length is %i bytes", num - 1); + return 0; + } + + return len; +} + +#endif diff --git a/src/main/channel.c b/src/main/channel.c new file mode 100644 index 0000000..757ccd2 --- /dev/null +++ b/src/main/channel.c @@ -0,0 +1,231 @@ +/* + * radmin.c RADIUS Administration tool. + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2015 The FreeRADIUS server project + * Copyright 2015 Alan DeKok <aland@deployingradius.com> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/channel.h> + +typedef struct rchannel_t { + uint32_t channel; + uint32_t length; +} rchannel_t; + + +static ssize_t lo_read(int fd, void *inbuf, size_t buflen) +{ + size_t total; + ssize_t r; + uint8_t *p = inbuf; + + for (total = 0; total < buflen; total += r) { + r = read(fd, p + total, buflen - total); + + if (r == 0) return 0; + + if (r < 0) { + if (errno == EINTR) continue; + + return -1; + + } + } + + return total; +} + + +/* + * A non-blocking copy of fr_channel_read(). + */ +ssize_t fr_channel_drain(int fd, fr_channel_type_t *pchannel, void *inbuf, size_t buflen, uint8_t **outbuf, size_t have_read) +{ + ssize_t r; + size_t data_len; + uint8_t *buffer = inbuf; + rchannel_t hdr; + + /* + * If we can't even read a header, die. + */ + if (buflen <= sizeof(hdr)) { + errno = EINVAL; + return -1; + } + + /* + * Ensure that we read the header first. + */ + if (have_read < sizeof(hdr)) { + *pchannel = FR_CHANNEL_WANT_MORE; + + r = lo_read(fd, buffer + have_read, sizeof(hdr) - have_read); + if (r <= 0) return r; + + have_read += r; + + if (have_read < sizeof(hdr)) return have_read; + } + + /* + * We've read the header. Figure out how much more data + * we need to read. + */ + memcpy(&hdr, buffer, sizeof(hdr)); + data_len = ntohl(hdr.length); + + /* + * The data will overflow the buffer. Die. + */ + if ((sizeof(hdr) + data_len) > buflen) { + errno = EINVAL; + return -1; + } + + /* + * This is how much we really want. + */ + buflen = sizeof(hdr) + data_len; + + r = lo_read(fd, buffer + have_read, buflen - have_read); + if (r <= 0) return r; + + have_read += r; + + if (have_read == buflen) { + *pchannel = ntohl(hdr.channel); + *outbuf = buffer + sizeof(hdr); + return data_len; + } + + *pchannel = FR_CHANNEL_WANT_MORE; + return have_read; +} + +ssize_t fr_channel_read(int fd, fr_channel_type_t *pchannel, void *inbuf, size_t buflen) +{ + ssize_t r; + size_t data_len; + uint8_t *buffer = inbuf; + rchannel_t hdr; + + /* + * Read the header + */ + r = lo_read(fd, &hdr, sizeof(hdr)); + if (r <= 0) return r; + + /* + * Read the data into the buffer. + */ + *pchannel = ntohl(hdr.channel); + data_len = ntohl(hdr.length); + +#if 0 + fprintf(stderr, "CHANNEL R %zu length %zu\n", *pchannel, data_len); +#endif + + /* + * Shrink the output buffer to the size of the data we + * have. + */ + if (buflen > data_len) buflen = data_len; + + r = lo_read(fd, buffer, buflen); + if (r <= 0) return r; + + /* + * Read and discard any extra data sent to us. Sorry, + * caller, you should have used a larger buffer! + */ + while (data_len > buflen) { + size_t discard; + uint8_t junk[64]; + + discard = data_len - buflen; + if (discard > sizeof(junk)) discard = sizeof(junk); + + r = lo_read(fd, junk, discard); + if (r <= 0) break; + + data_len -= r; + } + + return buflen; +} + +static ssize_t lo_write(int fd, void const *inbuf, size_t buflen) +{ + size_t total; + ssize_t r; + uint8_t const *buffer = inbuf; + + total = buflen; + + while (total > 0) { + r = write(fd, buffer, total); + if (r == 0) { + errno = EAGAIN; + return -1; + } + + if (r < 0) { + if (errno == EINTR) continue; + + return -1; + } + + buffer += r; + total -= r; + } + + return buflen; +} + +ssize_t fr_channel_write(int fd, fr_channel_type_t channel, void const *inbuf, size_t buflen) +{ + ssize_t r; + rchannel_t hdr; + uint8_t const *buffer = inbuf; + + hdr.channel = htonl(channel); + hdr.length = htonl(buflen); + +#if 0 + fprintf(stderr, "CHANNEL W %zu length %zu\n", channel, buflen); +#endif + + /* + * write the header + */ + r = lo_write(fd, &hdr, sizeof(hdr)); + if (r <= 0) return r; + + /* + * write the data directly from the buffer + */ + r = lo_write(fd, buffer, buflen); + if (r <= 0) return r; + + return buflen; +} diff --git a/src/main/checkrad.in b/src/main/checkrad.in new file mode 100644 index 0000000..c0cf440 --- /dev/null +++ b/src/main/checkrad.in @@ -0,0 +1,1515 @@ +#!@PERL@ +# +# checkrad See if a user is (still) logged in on a certain port. +# +# This is used by the FreeRADIUS server to check +# if its idea of a user logged in on a certain port/nas +# is correct if a double login is detected. +# +# Called as: nas_type nas_ip nas_port login session_id +# +# Returns: 0 = no duplicate, 1 = duplicate, >1 = error. +# +# Version: $Id$ +# +# livingston_snmp 1.2 Author: miquels@cistron.nl +# cvx_snmp 1.0 Author: miquels@cistron.nl +# portslave_finger 1.0 Author: miquels@cistron.nl +# max40xx_finger 1.0 Author: costa@mdi.ca +# ascend_snmp 1.1 Author: blaz@amis.net +# computone_finger 1.2 Author: pacman@world.std.com +# sub tc_tccheck 1.1 Author: alexisv@compass.com.ph +# cyclades_telnet 1.2 Author: accdias@sst.com.br +# patton_snmp 1.0 Author: accdias@sst.com.br +# digitro_rusers 1.1 Author: accdias@sst.com.br +# cyclades_snmp 1.0 Author: accdias@sst.com.br +# usrhiper_snmp 1.0 Author: igor@ipass.net +# juniper_e_snmp 1.1 Author: guilhermefranco@gmail.com +# multitech_snmp 1.0 Author: ehonzay@willmar.com +# netserver_telnet 1.0 Author: mts@interplanet.es +# versanet_snmp 1.0 Author: support@versanetcomm.com +# bay_finger 1.0 Author: chris@shenton.org +# cisco_l2tp 1.14 Author: paul@distributel.net +# mikrotik_telnet 1.1 Author: Evren Yurtesen <yurtesen@ispro.net.tr> +# mikrotik_snmp 1.0 Author: Evren Yurtesen <yurtesen@ispro.net.tr> +# redback_telnet Author: Eduardo Roldan +# +# Config: $debug is the file you want to put debug messages in +# $snmpget is the location of your ``snmpget'' program +# $snmpwalk is the location of your ``snmpwalk'' program +# $snmp_timeout is the timeout for snmp queries +# $snmp_retries is the number of retries for timed out snmp queries +# $snmp_version is the version of to use for snmp queries [1,2c,3] +# $rusers is the location of your ``rusers'' program +# $naspass is the location of your NAS admin password file +# + +$prefix = "@prefix@"; +$localstatedir = "@localstatedir@"; +$logdir = "@logdir@"; +$sysconfdir = "@sysconfdir@"; +$raddbdir = "@raddbdir@"; + +$debug = ""; +#$debug = "$logdir/checkrad.log"; + +$snmpget = "@SNMPGET@"; +$snmpwalk = "@SNMPWALK@"; +$snmp_timeout = 5; +$snmp_retries = 1; +$snmp_version = "2c"; +$rusers = "@RUSERS@"; +$naspass = "$raddbdir/naspasswd"; + +# Community string. Change this if yours isn't "public". +$cmmty_string = "public"; +# path to finger command +$finger = "/usr/bin/finger"; + +# Extremely slow way of converting port descriptions to actual indexes +$portisdescr = 0; + +# Realm used by Cisco sub +$realm = ''; + +# +# USR-Hiper: $hiper_density is the reported port density (default 256 +# but 24 makes more sense) +# +$hiper_density = 256; + +# +# Try to load Net::Telnet, SNMP_Session etc. +# Do not complain if we cannot find it. +# Prefer a locally installed copy. +# +BEGIN { + unshift @INC, "/usr/local/lib/site_perl"; + + eval "use Net::Telnet 3.00;"; + $::HAVE_NET_TELNET = ($@ eq ""); + + eval "use SNMP_Session;"; + if ($@ eq "") { + eval "use BER;"; + $::HAVE_SNMP_SESSION = ($@ eq ""); + eval "use Socket;"; + } +}; + +# +# Get password from /etc/raddb/naspasswd file. +# Returns (login, password). +# +sub naspasswd { + my ($terminalserver, $emptyok) = @_; + my ($login, $password); + my ($ts, $log, $pass); + + unless (open(NFD, $naspass)) { + if (!$emptyok) { + print LOG "checkrad: naspasswd file not found; " . + "possible match for $ARGV[3]\n" if ($debug); + print STDERR "checkrad: naspasswd file not found; " . + "possible match for $ARGV[3]\n"; + } + return (); + } + while (<NFD>) { + chop; + next if (m/^(#|$|[\t ]+$)/); + ($ts, $log, $pass) = split(/\s+/, $_, 3); + if ($ts eq $terminalserver) { + $login = $log; + $password = $pass; + last; + } + } + close NFD; + if ($password eq "" && !$emptyok) { + print LOG "checkrad: password for $ARGV[1] is null; " . + "possible match for $ARGV[3] on " . + "port $ARGV[2]\n" if ($debug); + print STDERR "checkrad: password for $ARGV[1] is null; " . + "possible match for $ARGV[3] on port $ARGV[2]\n"; + } + ($login, $password); +} + +# +# See if Net::Telnet is there. +# +sub check_net_telnet { + if (!$::HAVE_NET_TELNET) { + print LOG + " checkrad: Net::Telnet 3.00+ CPAN module not installed\n" + if ($debug); + print STDERR + "checkrad: Net::Telnet 3.00+ CPAN module not installed\n"; + return 0; + } + 1; +} + +# +# Do snmpwalk by calling snmpwalk. +# +sub snmpwalk_prog { + my ($host, $community, $oid) = @_; + local $_; + + print LOG "snpwalk: $snmpwalk -r $snmp_retries -t $snmp_timeout -v$snmp_version -c '$community' $host $oid\n"; + $_ = `$snmpwalk -r $snmp_retries -t $snmp_timeout -v$snmp_version -c '$community' $host $oid`; + + return $_; +} + +# +# Do snmpwalk. +# +sub snmpwalk { + my $ret; + + if (-x $snmpwalk) { + $ret = snmpwalk_prog(@_); + } else { + $e = "$snmpwalk not found!"; + print LOG "$e\n" if ($debug); + print STDERR "checkrad: $e\n"; + $ret = ""; + } + $ret; +} + + +# +# Do snmpget by calling snmpget. +# +sub snmpget_prog { + my ($host, $community, $oid) = @_; + my ($ret); + local $_; + + print LOG "snmpget: $snmpget -r $snmp_retries -t $snmp_timeout -v$snmp_version -c '$community' $host $oid\n"; + $_ = `$snmpget -r $snmp_retries -t $snmp_timeout -v$snmp_version -c '$community' $host $oid`; + if (/^.*(\s|\")([0-9A-Za-z]{8})(\s|\"|$).*$/) { + # Session ID format. + $ret = $2; + } elsif (/^.*=.*"(.*)"/) { + # oid = "...." junk format. + $ret = $1; + } elsif (/^.*=\s*(?:.*:\s*)?(\S+)/) { + # oid = string format + $ret = $1; + } + + # Strip trailing junk if any. + $ret =~ s/\s*Hex:.*$//; + $ret; +} + +# +# Do snmpget by using SNMP_Session. +# Coded by Jerry Workman <jerry@newwave.net> +# +sub snmpget_session { + my ($host, $community, $OID) = @_; + my ($ret); + local $_; + my (@enoid, $var,$response, $bindings, $binding, $value); + my ($inoid, $outoid, $upoid, $oid, @retvals); + + $OID =~ s/^.iso.org.dod.internet.private.enterprises/.1.3.6.1.4.1/; + + push @enoid, encode_oid((split /\./, $OID)); + srand(); + + my $session = SNMP_Session->open($host, $community, 161); + if (!$session->get_request_response(@enoid)) { + $e = "No SNMP answer from $ARGV[0]."; + print LOG "$e\n" if ($debug); + print STDERR "checkrad: $e\n"; + return ""; + } + $response = $session->pdu_buffer; + ($bindings) = $session->decode_get_response ($response); + $session->close (); + while ($bindings) { + ($binding,$bindings) = decode_sequence ($bindings); + ($oid,$value) = decode_by_template ($binding, "%O%@"); + my $tempo = pretty_print($value); + $tempo=~s/\t/ /g; + $tempo=~s/\n/ /g; + $tempo=~s/^\s+//; + $tempo=~s/\s+$//; + + push @retvals, $tempo; + } + $retvals[0]; +} + +# +# Do snmpget +# +sub snmpget { + my $ret; + + if ($::HAVE_SNMP_SESSION) { + $ret = snmpget_session(@_); + } elsif (-x $snmpget) { + $ret = snmpget_prog(@_); + } else { + $e = "Neither SNMP_Session module or $snmpget found!"; + print LOG "$e\n" if ($debug); + print STDERR "checkrad: $e\n"; + $ret = ""; + } + $ret; +} + +# +# Get ifindex from description +# +sub ifindex { + my $port = shift; + + # If its not an integer, portisdescr lies! + return $port unless $portisdescr || $port !~ /^[0-9]*$/; + + $_ = snmpwalk($ARGV[1], "$cmmty_string", ".1.3.6.1.2.1.2.2.1.2"); + + foreach (split /\n/){ + if(/\.([0-9]+)\s*=.*$port"?$/){ + print LOG " port descr $port is at SNMP ifIndex $1\n" if ($debug); + return $1; + } + } + + + return $port; +} + +# +# Strip domains, prefixes and suffixes from username +# +# Known prefixes: (P)PP, (S)LIP e (C)SLIP +# Known suffixes: .ppp, .slip e .cslip +# +# Author: Antonio Dias of SST Internet <accdias@sst.com.br> +# +sub strip_username { + my ($user) = @_; + # + # Trim white spaces. + # + $user =~ s/^\s*(.*?)\s*$/$1/; + # + # Strip out domains, prefix and suffixes + # + $user =~ s/\@(.)*$//; + $user =~ s/^[PSC]//; + $user =~ s/\.(ppp|slip|cslip)$//; + $user; +} + +# +# Check whether a session is current on any device which implements the standard IEEE 802.1X MIB +# +# Note: Vendors use different formats for the session ID, and it often doesn't map +# between Acct-Session-ID so can't be used to identify and 802.1X session (we ignore it). +# +# If a session matching the username is found on the port specified, and the +# session is still active then thats good enough... +# +# Author: Arran Cudbard-Bell <arran.cudbard-bell@freeradius.org> +# +$ieeedot1m = '.iso.0.8802.1.1'; +sub dot1x_snmp { + $ifIndex = ifindex($ARGV[2]); + + # User matches and not terminated yet? + if( + snmpget($ARGV[1], "$cmmty_string", "$ieeedot1m.1.1.2.4.1.9.$ifIndex") eq $ARGV[3] && + snmpget($ARGV[1], "$cmmty_string", "$ieeedot1m.1.1.2.4.1.8.$ifIndex") eq '999' + ){ + print LOG " found user $ARGV[3] at port $ARGV[2] ($ifIndex)" if $debug; + return 1; + } + + 0; +} + +# +# See if the user is logged in using the Livingston MIB. +# We don't check the username but the session ID. +# +$lvm = '.iso.org.dod.internet.private.enterprises.307'; +sub livingston_snmp { + + # + # We don't know at which ifIndex S0 is, and + # there might be a hole at S23, or at S30+S31. + # So we figure out dynamically which offset to use. + # + # If the port < S23, probe ifIndex 5. + # If the port < S30, probe IfIndex 23. + # Otherwise probe ifIndex 32. + # + my $ifIndex; + my $test_index; + if ($ARGV[2] < 23) { + $test_index = 5; + } elsif ($ARGV[2] < 30) { + $test_index = 23; + } else { + $test_index = 32; + } + $_ = snmpget($ARGV[1], "$cmmty_string", "$lvm.3.2.1.1.1.2.$test_index"); + /S([0-9]+)/; + $xport = $1 + 0; + $ifIndex = $ARGV[2] + ($test_index - $xport); + + print LOG " port S$ARGV[2] at SNMP ifIndex $ifIndex\n" + if ($debug); + + # + # Now get the session id from the terminal server. + # + $sessid = snmpget($ARGV[1], "$cmmty_string", "$lvm.3.2.1.1.1.5.$ifIndex"); + + print LOG " session id at port S$ARGV[2]: $sessid\n" if ($debug); + + ($sessid eq $ARGV[4]) ? 1 : 0; +} + +# +# See if the user is logged in using the Aptis MIB. +# We don't check the username but the session ID. +# +# sessionStatusActiveName +$apm1 = '.iso.org.dod.internet.private.enterprises.2637.2.2.102.1.12'; +# sessionStatusActiveStopTime +$apm2 = '.iso.org.dod.internet.private.enterprises.2637.2.2.102.1.20'; +sub cvx_snmp { + + # Remove unique identifier, then take remainder of the + # session-id as a hex number, convert that to decimal. + my $sessid = $ARGV[4]; + $sessid =~ s/^.*://; + $sessid =~ s/^0*//; + $sessid = "0" if ($sessid eq ''); + + # + # Now get the login from the terminal server. + # Blech - the SNMP table is called 'sessionStatusActiveTable, + # but it sometimes lists inactive sessions too. + # However an active session doesn't have a Stop time, + # so we can differentiate that way. + # + my $login = snmpget($ARGV[1], "$cmmty_string", "$apm1." . hex($sessid)); + my $stopt = snmpget($ARGV[1], "$cmmty_string", "$apm2." . hex($sessid)); + $login = "--" if ($stopt > 0); + + print LOG " login with session-id $ARGV[4]: $login\n" if ($debug); + + (strip_username($login) eq strip_username($ARGV[3])) ? 1 : 0; +} + +# +# See if the user is logged in using the Cisco MIB +# +$csm = '.iso.org.dod.internet.private.enterprises.9'; +sub cisco_snmp { + + # Look up community string in naspasswd file. + my ($login, $pass) = naspasswd($ARGV[1], 1); + if ($login eq '') { + $pass = $cmmty_string; + } elsif ($login ne 'SNMP') { + if ($debug) { + print LOG + " Error: Need SNMP community string for $ARGV[1]\n"; + } + return 2; + } + + my $port = $ARGV[2]; + my $sess_id = hex($ARGV[4]); + + if ($port < 20000) { + # + # The AS5350 doesn't support polling the session ID, + # so we do it based on nas-port-id. This only works + # for analog sessions where port < 20000. + # Yes, this means that simultaneous-use on the as5350 + # doesn't work for ISDN users. + # + $login = snmpget($ARGV[1], $pass, "$csm.2.9.2.1.18.$port"); + print LOG " user at port S$port: $login\n" if ($debug); + } else { + $login = snmpget($ARGV[1], $pass, + "$csm.9.150.1.1.3.1.2.$sess_id"); + print LOG " user with session id $ARGV[4] ($sess_id): " . + "$login\n" if ($debug); + } + + # ($login eq $ARGV[3]) ? 1 : 0; + if($login eq $ARGV[3]) { + return 1; + }else{ + $out=snmpwalk($ARGV[1],$pass,".iso.org.dod.internet.private.enterprises.9.10.19.1.3.1.1.3"); + if($out=~/\"$ARGV[3]\"/){ + return 1; + }else{ + return 0; + } + } +} + +# +# Check the subscriber name on a Juniper JunosE E-Series BRAS (ERX, E120, E320). Requires "radius acct-session-id-format decimal" configuration in the BRAS. +# +# Author: Guilherme Franco <guilhermefranco@gmail.com> +# +sub juniper_e_snmp { + #receives acct_session + my $temp = $ARGV[4]; + #removes the leading 0s + my $clean_temp = int $temp; + + $out=snmpget($ARGV[1], $cmmty_string, ".1.3.6.1.4.1.4874.2.2.20.1.8.4.1.2.$clean_temp"); + if($out=~/\"$ARGV[3]\"/){ + return 1; + }else{ + return 0; + } +} + +# +# Check a MultiTech CommPlete Server ( CC9600 & CC2400 ) +# +# Author: Eric Honzay of Bennett Office Products <ehonzay@willmar.com> +# +$msm = '.iso.org.dod.internet.private.enterprises.995'; +sub multitech_snmp { + my $temp = $ARGV[2] + 1; + + $login = snmpget($ARGV[1], "$cmmty_string", "$msm.2.31.1.1.1.$temp"); + print LOG " user at port S$ARGV[2]: $login\n" if ($debug); + + ($login eq $ARGV[3]) ? 1 : 0; +} + +# +# Check a Computone Powerrack via finger +# +# Old Author: Shiloh Costa of MDI Internet Inc. <costa@mdi.ca> +# New Author: Alan Curry <pacman@world.std.com> +# +# The finger response format is version-dependent. To do this *right*, you +# need to know exactly where the port number and username are. I know that +# for 1.7.2, and 3.0.4 but for others I just guess. +# Oh yeah and on top of it all, the thing truncates usernames. --Pac. +# +# 1.7.2 and 3.0.4 both look like this: +# +# 0 0 000 00:56 luser pppfsm Incoming PPP, ppp00, 10.0.0.1 +# +# and the truncated ones look like this: +# +# 25 0 000 00:15 longnameluse..pppfsm Incoming PPP, ppp25, 10.0.0.26 +# +# Yes, the fields run together. Long Usernames Considered Harmful. +# +sub computone_finger { + my $trunc, $ver; + + open(FD, "$finger \@$ARGV[1]|") or return 2; + <FD>; # the [hostname] line is definitely uninteresting + $trunc = substr($ARGV[3], 0, 12); + $ver = ""; + while(<FD>) { + if(/cnx kernel release ([^ ,]+)[, ]/) { + $ver = $1; + next; + } + # Check for known versions + if ($ver eq '1.7.2' || $ver eq '3.0.4') { + if (/^\Q$ARGV[2]\E\s+\S+\s+\S+\s+\S+\s+\Q$trunc\E(\s+|\.\.)/) { + close FD; + return 1; + } + next; + } + # All others. + if (/^\s*\Q$ARGV[2]\E\s+.*\s+\Q$trunc\E\s+/) { + close FD; + return 1; + } + } + + close FD; + return 0; +} + +# +# Check an Ascend Max4000 or similar model via finger +# +# Note: Not all software revisions support finger +# You may also need to enable the finger option. +# +# Author: Shiloh Costa of MDI Internet Inc. <costa@mdi.ca> +# +sub max40xx_finger { + open(FD, "$finger $ARGV[3]\@$ARGV[1]|"); + while(<FD>) { + $line = $_; + if( $line =~ /Session/ ){ + next; + } + + if( $line =~ /$ARGV[4]/ ){ + return 1; # user is online + } + } + close FD; + return 0; # user is offline +} + + +# +# Check an Ascend Max4000 or similar model via SNMP +# +# Author: Blaz Zupan of Medinet <blaz@amis.net> +# +$asm = '.iso.org.dod.internet.private.enterprises.529'; +sub ascend_snmp { + my $sess_id; + my $l1, $l2; + + $l1 = ''; + $l2 = ''; + + # + # If it looks like hex, only try it as hex, + # otherwise try it as both decimal and hex. + # + $sess_id = $ARGV[4]; + if ($sess_id !~ /^0/ && $sess_id !~ /[a-f]/i) { + $l1 = snmpget($ARGV[1], "$cmmty_string", "$asm.12.3.1.4.$sess_id"); + } + if (!$l1){ + $sess_id = hex $ARGV[4]; + $l2 = snmpget($ARGV[1], "$cmmty_string", "$asm.12.3.1.4.$sess_id"); + } + + print LOG " user at port S$ARGV[2]: $l1 (dec)\n" if ($debug && $l1); + print LOG " user at port S$ARGV[2]: $l2 (hex)\n" if ($debug && $l2); + + (($l1 && $l1 eq $ARGV[3]) || ($l2 && $l2 eq $ARGV[3])) ? 1 : 0; +} + + +# +# See if the user is logged in using the portslave finger. +# +sub portslave_finger { + my ($Port_seen); + + $Port_seen = 0; + + open(FD, "$finger \@$ARGV[1]|"); + while(<FD>) { + # + # Check for ^Port. If we don't see it we + # wont get confused by non-portslave-finger + # output too. + # + if (/^Port/) { + $Port_seen++; + next; + } + next if (!$Port_seen); + next if (/^---/); + + ($port, $user) = /^.(...) (...............)/; + + $port =~ s/ .*//; + $user =~ s/ .*//; + $ulen = length($user); + # + # HACK: strip [PSC] from the front of the username, + # and things like .ppp from the end. + # + $user =~ s/^[PSC]//; + $user =~ s/\.(ppp|slip|cslip)$//; + + # + # HACK: because ut_user usually has max. 8 characters + # we only compare up the the length of $user if the + # unstripped name had 8 chars. + # + $argv_user = $ARGV[3]; + if ($ulen == 8) { + $ulen = length($user); + $argv_user = substr($ARGV[3], 0, $ulen); + } + + if ($port == $ARGV[2]) { + if ($user eq $argv_user) { + print LOG " $user matches $argv_user " . + "on port $port" if ($debug); + close FD; + return 1; + } else { + print LOG " $user doesn't match $argv_user " . + "on port $port" if ($debug); + close FD; + return 0; + } + } + } + close FD; + 0; +} + +# +# See if the user is already logged-in at the 3Com/USR Total Control. +# (this routine by Alexis C. Villalon <alexisv@compass.com.ph>). +# You must have the Net::Telnet module from CPAN for this to work. +# You must also have your /etc/raddb/naspasswd made up. +# +sub tc_tccheck { + # + # Localize all variables first. + # + my ($Port_seen, $ts, $terminalserver, $log, $login, $pass, $password); + my ($telnet, $curprompt, $curline, $ok, $totlines, $ccntr); + my (@curlines, @cltok, $user, $port, $ulen); + + return 2 unless (check_net_telnet()); + + $terminalserver = $ARGV[1]; + $Port_seen = 0; + # + # Get login name and password for a certain NAS from $naspass. + # + ($login, $password) = naspasswd($terminalserver, 1); + return 2 if ($password eq ""); + + # + # Communicate with NAS using Net::Telnet, then issue + # the command "show sessions" to see who are logged in. + # Thanks to Chris Jackson <chrisj@tidewater.net> for the + # for the "-- Press Return for More --" workaround. + # + $telnet = new Net::Telnet (Timeout => 5, + Prompt => '/\>/'); + $telnet->open($terminalserver); + $telnet->login($login, $password); + $telnet->print("show sessions"); + while ($curprompt ne "\>") { + ($curline, $curprompt) = $telnet->waitfor + (String => "-- Press Return for More --", + String => "\>", + Timeout => 5); + $ok = $telnet->print(""); + push @curlines, split(/^/m, $curline); + } + $telnet->close; + # + # Telnet closed. We got the info. Let's examine it. + # + $totlines = @curlines; + $ccntr = 0; + while($ccntr < $totlines) { + # + # Check for ^Port. + # + if ($curlines[$ccntr] =~ /^Port/) { + $Port_seen++; + $ccntr++; + next; + } + # + # Ignore all unnecessary lines. + # + if (!$Port_seen || $curlines[$ccntr] =~ /^---/ || + $curlines[$ccntr] =~ /^ .*$/) { + $ccntr++; + next; + } + # + # Parse the current line for the port# and username. + # + @cltok = split(/\s+/, $curlines[$ccntr]); + $ccntr++; + $port = $cltok[0]; + $user = $cltok[1]; + $ulen = length($user); + # + # HACK: strip [PSC] from the front of the username, + # and things like .ppp from the end. Strip S from + # the front of the port number. + # + $user =~ s/^[PSC]//; + $user =~ s/\.(ppp|slip|cslip)$//; + $port =~ s/^S//; + # + # HACK: because "show sessions" shows max. 15 characters + # we only compare up to the length of $user if the + # unstripped name had 15 chars. + # + $argv_user = $ARGV[3]; + if ($ulen == 15) { + $ulen = length($user); + $argv_user = substr($ARGV[3], 0, $ulen); + } + if ($port == $ARGV[2]) { + if ($user eq $argv_user) { + print LOG " $user matches $argv_user " . + "on port $port" if ($debug); + return 1; + } else { + print LOG " $user doesn't match $argv_user " . + "on port $port" if ($debug); + return 0; + } + } + } + 0; +} + +# +# Check a Cyclades PathRAS via telnet +# +# Version: 1.2 +# +# Author: Antonio Dias of SST Internet <accdias@sst.com.br> +# +sub cyclades_telnet { + # + # Localize all variables first. + # + my ($pr, $pr_login, $pr_passwd, $pr_prompt, $endlist, @list, $port, $user); + # + # This variable must match PathRAS' command prompt + # string as entered in menu option 6.2. + # The value below matches the default command prompt. + # + $pr_prompt = '/Select option ==\>$/i'; + + # + # This variable match the end of userslist. + # + $endlist = '/Type \<enter\>/i'; + + # + # Do we have Net::Telnet installed? + # + return 2 unless (check_net_telnet()); + + # + # Get login name and password for NAS + # from $naspass file. + # + ($pr_login, $pr_passwd) = naspasswd($ARGV[1], 1); + + # + # Communicate with PathRAS using Net::Telnet, then access + # menu option 6.8 to see who are logged in. + # Based on PathRAS firmware version 1.2.3 + # + $pr = new Net::Telnet ( + Timeout => 5, + Host => $ARGV[1], + ErrMode => 'return' + ) || return 2; + + # + # Force PathRAS shows its banner. + # + $pr->break(); + + # + # Log on PathRAS + # + if ($pr->waitfor(Match => '/login : $/i') == 1) { + $pr->print($pr_login); + } else { + print LOG " Error: sending login name to PathRAS\n" if ($debug); + $pr->close; + return 2; + } + + if ($pr->waitfor(Match => '/password : $/i') == 1) { + $pr->print($pr_passwd); + } else { + print LOG " Error: sending password to PathRAS.\n" if ($debug); + $pr->close; + return 2; + } + + $pr->print(); + + # + # Access menu option 6 "PathRAS Management" + # + if ($pr->waitfor(Match => $pr_prompt) == 1) { + $pr->print('6'); + } else { + print LOG " Error: accessing menu option '6'.\n" if ($debug); + $pr->close; + return 2; + } + # + # Access menu option 8 "Show Active Ports" + # + if ($pr->waitfor(Match => $pr_prompt) == 1) { + @list = $pr->cmd(String => '8', Prompt => $endlist); + } else { + print LOG " Error: accessing menu option '8'.\n" if ($debug); + $pr->close; + return 2; + } + # + # Since we got the info we want, let's close + # the telnet session + # + $pr->close; + + # + # Lets examine the userlist stored in @list + # + foreach(@list) { + # + # We are interested in active sessions only + # + if (/Active/i) { + ($port, $user) = split; + # + # Strip out any prefix, suffix and + # realm from $user check to see if + # $ARGV[3] matches. + # + if(strip_username($ARGV[3]) eq strip_username($user)) { + print LOG " User '$ARGV[3]' found on '$ARGV[1]:$port'.\n" if ($debug); + return 1; + } + } + } + print LOG " User '$ARGV[3]' not found on '$ARGV[1]'.\n" if ($debug); + 0; +} + +# +# Check a Patton 2800 via SNMP +# +# Version: 1.0 +# +# Author: Antonio Dias of SST Internet <accdias@sst.com.br> +# +sub patton_snmp { + my($oid); + + #$oid = '.1.3.6.1.4.1.1768.5.100.1.40.' . hex $ARGV[4]; + # Reported by "Andria Legon" <andria@patton.com> + # The OID below should be the correct one instead of the one above. + $oid = '.1.3.6.1.4.1.1768.5.100.1.56.' . hex $ARGV[4]; + # + # Check if the session still active + # + if (snmpget($ARGV[1], "monitor", "$oid") == 0) { + print LOG " Session $ARGV[4] still active on NAS " . + "$ARGV[1], port $ARGV[2], for user $ARGV[3].\n" if ($debug); + return 1; + } + 0; +} + +# +# Check a Digitro BXS via rusers +# +# Version: 1.1 +# +# Author: Antonio Dias of SST Internet <accdias@sst.com.br> +# +sub digitro_rusers { + my ($ret); + local $_; + + if (-e $rusers && -x $rusers) { + # + # Get a list of users logged in via rusers + # + $_ = `$rusers $ARGV[1]`; + $ret = ((/$ARGV[3]/) ? 1 : 0); + } else { + print LOG " Error: can't execute $rusers\n" if $debug; + $ret = 2; + } + $ret; +} + +# +# Check Cyclades PR3000 and PR4000 via SNMP +# +# Version: 1.0 +# +# Author: Antonio Dias of SST Internet <accdias@sst.com.br> +# +sub cyclades_snmp { + my ($oid, $ret); + local $_; + + $oid = ".1.3.6.1.4.1.2925.3.3.6.1.1.2"; + + $_ = snmpwalk($ARGV[1],"$cmmty_string",$oid); + $ret = ((/$ARGV[3]/) ? 1 : 0); + $ret; +} + +# +# 3Com/USR HiPer Arc Total Control. +# This works with HiPer Arc 4.0.30 +# (this routine by Igor Brezac <igor@ipass.net>) +# + +# This routine modified by Dan Halverson <danh@tbc.net> +# to support additional versions of Hiper Arc +# + +$usrm = '.iso.org.dod.internet.private.enterprises.429'; +sub usrhiper_snmp { + my ($login,$password,$oidext); + + # Look up community string in naspasswd file. + ($login, $password) = naspasswd($ARGV[1], 1); + if ($login && $login ne 'SNMP') { + if($debug) { + print LOG + " Error: Need SNMP community string for $ARGV[1]\n"; + } + return 2; + } else { +# If password is defined in naspasswd file, use it as community, otherwise use $cmmty_string + if ($password eq '') { + $password = "$cmmty_string"; + } + } + my ($ver) = get_hiper_ver(usrm=>$usrm, target=>$ARGV[1], community=>$password); + $oidext = get_oidext(ver=>$ver, tty=>$ARGV[2]); + my ($login); + + $login = snmpget($ARGV[1], $password, "$usrm.4.10.1.1.18.$oidext"); + if ($login =~ /\"/) { + $login =~ /^.*\"([^"]+)\"/; + $login = $1; + } + + print LOG " user at port S$ARGV[2]: $login\n" if ($debug); + + ($login eq $ARGV[3]) ? 1 : 0; +} + +# +# get_hiper_ver and get_oidext by Dan Halverson <danh@tbc.net> +# +sub get_hiper_ver { + my (%args) = @_; + my ($ver + ); + $ver = snmpget ($args{'target'}, $args{'community'}, $args{'usrm'}.".4.1.14.0"); + return($ver); +} + +# +# Add additional OID checks below before the else. +# Else is for 4.0.30 +# +sub get_oidext { + my (%args) = @_; + my ($oid + ); + if ($args{'ver'} =~ /V5.1.99/) { + $oid = $args{'tty'}+1257-1; + } + else { + $oid = 1257 + 256*int(($args{'tty'}-1) / $hiper_density) + + (($args{'tty'}-1) % $hiper_density); + } + return($oid); +} + +# +# Check USR Netserver with Telnet - based on tc_tccheck. +# By "Marti" <mts@interplanet.es> +# +sub usrnet_telnet { + # + # Localize all variables first. + # + my ($ts, $terminalserver, $login, $password); + my ($telnet, $curprompt, $curline, $ok); + my (@curlines, $user, $port); + + return 2 unless (check_net_telnet()); + + $terminalserver = $ARGV[1]; + $Port_seen = 0; + # + # Get login name and password for a certain NAS from $naspass. + # + ($login, $password) = naspasswd($terminalserver, 1); + return 2 if ($password eq ""); + + # + # Communicate with Netserver using Net::Telnet, then access + # list connectionsto see who are logged in. + # + $telnet = new Net::Telnet (Timeout => 5, + Prompt => '/\>/'); + $telnet->open($terminalserver); + + # + # Log on Netserver + # + $telnet->login($login, $password); + + # + # Launch list connections command + + $telnet->print("list connections"); + + while ($curprompt ne "\>") { + ($curline, $curprompt) = $telnet->waitfor + ( String => "\>", + Timeout => 5); + $ok = $telnet->print(""); + push @curlines, split(/^/m, $curline); + } + + $telnet->close; + # + # Telnet closed. We got the info. Let's examine it. + # + foreach(@curlines) { + if ( /mod\:/ ) { + ($port, $user, $dummy) = split; + # + # Strip out any prefixes and suffixes + # from the username + # + # uncomment this if you use the standard + # prefixes + #$user =~ s/^[PSC]//; + #$user =~ s/\.(ppp|slip|cslip)$//; + # + # Check to see if $user is already connected + # + if ($user eq $ARGV[3]) { + print LOG " $user matches $ARGV[3] " . + "on port $port" if ($debug); + return 1; + }; + }; + }; + print LOG + " $ARGV[3] not found on Netserver logged users list " if ($debug); + 0; +} + +# +# Versanet's Perl Script Support: +# +# ___ versanet_snmp 1.0 by support@versanetcomm.com ___ July 1999 +# Versanet Enterprise MIB Base: 1.3.6.1.4.1.2180 +# +# VN2001/2002 use slot/port number to locate modems. To use snmp get we +# have to translate the original port number into a slot/port pair. +# +$vsm = '.iso.org.dod.internet.private.enterprises.2180'; +sub versanet_snmp { + + print LOG "argv[2] = $ARGV[2] " if ($debug); + $port = $ARGV[2]%8; + $port = 8 if ($port eq 0); + print LOG "port = $port " if ($debug); + $slot = (($ARGV[2]-$port)/8)+1; + print LOG "slot = $slot" if ($debug); + $loginname = snmpget($ARGV[1], "$cmmty_string", "$vsm.27.1.1.3.$slot.$port"); +# +# Note: the "$cmmty_string" string above could be replaced by the public +# community string defined in Versanet VN2001/VN2002. +# + print LOG " user at slot $slot port $port: $loginname\n" if ($debug); ($loginname eq $ARGV[3]) ? 1 : 0; +} + + +# 1999/08/24 Chris Shenton <chris@shenton.org> +# Check Bay8000 NAS (aka: Annex) using finger. +# Returns from "finger @bay" like: +# Port What User Location When Idle Address +# asy2 PPP bill --- 9:33am :08 192.0.2.194 +# asy4 PPP hillary --- 9:36am :04 192.0.2.195 +# [...] +# But also returns partial-match users if you say like "finger g@bay": +# Port What User Location When Idle Address +# asy2 PPP gore --- 9:33am :09 192.0.2.194 +# asy22 PPP gwbush --- Mon 9:19am :07 192.0.2.80 +# So check exact match of username! + +sub bay_finger { # ARGV: 1=nas_ip, 2=nas_port, 3=login, 4=sessid + open(FINGER, "$finger $ARGV[3]\@$ARGV[1]|") || return 2; # error + while(<FINGER>) { + my ($Asy, $PPP, $User) = split; + if( $User =~ /^$ARGV[3]$/ ){ + close FINGER; + print LOG "checkrad:bay_finger: ONLINE $ARGV[3]\@$ARGV[1]" + if ($debug); + return 1; # online + } + } + close FINGER; + print LOG "checkrad:bay_finger: offline $ARGV[3]\@$ARGV[1]" if ($debug); + return 0; # offline +} + +# +# Cisco L2TP support +# This is for PPP sessions coming from an L2TP tunnel from a Dial +# or DSL wholesale provider +# Paul Khavkine <paul@distributel.net> +# July 19 2001 +# +# find_l2tp_login() walks a part of cisco vpdn tree to find out what session +# and tunnel ID's are for a given Virtual-Access interface to construct +# the following OID: .1.3.6.1.4.1.9.10.24.1.3.2.1.2.2.$tunID.$sessID +# Then gets the username from that OID. +# Make sure you set the $realm variable at the begining of the file if +# needed. The new type for naslist is cisco_l2tp + +sub find_l2tp_login +{ + my($host, $community, $port_num) = @_; + my $l2tp_oid = '.1.3.6.1.4.1.9.10.24.1.3.2.1.2.2'; + my $port_oid = '.iso.org.dod.internet.private.enterprises.9.10.51.1.2.1.1.2.2'; + my $port = 'Vi' . $port_num; + + my $sess = new SNMP::Session(DestHost => $host, Community => $community); + my $snmp_var = new SNMP::Varbind(["$port_oid"]); + my $val = $sess->getnext($snmp_var); + + do + { + $sess->getnext($snmp_var); + } until ($snmp_var->[$SNMP::Varbind::val_f] =~ /$port/) || + (!($snmp_var->[$SNMP::Varbind::ref_f] =~ /^$port_oid\.(\d+)\.(\d+)$/)) || + ($sess->{ErrorNum}); + + my $val1 = $snmp_var->[$SNMP::Varbind::ref_f]; + + if ($val1 =~ /^$port_oid/) { + $result = substr($val1, length($port_oid)); + $result =~ /^\.(\d+)\.(\d+)$/; + $tunID = $1; + $sessID = $2; + } + + my $snmp_var1 = new SNMP::Varbind(["$l2tp_oid\.$tunID\.$sessID"]); + $val = $sess->get($snmp_var1); + my $login = $snmp_var1->[$SNMP::Varbind::val_f]; + + return $login; +} + +sub cisco_l2tp_snmp +{ + my $login = find_l2tp_login("$ARGV[1]", $cmmty_string, "$ARGV[2]"); + print LOG " user at port S$ARGV[2]: $login\n" if ($debug); + ($login eq "$ARGV[3]\@$realm") ? 1 : 0; +} + +sub mikrotik_snmp { + + # Set SNMP version + # MikroTik only supports version 1 + $snmp_version = "1"; + + # Look up community string in naspasswd file. + ($login, $password) = naspasswd($ARGV[1], 1); + if ($login && $login ne 'SNMP') { + if($debug) { + print LOG "Error: Need SNMP community string for $ARGV[1]\n"; + } + return 2; + } else { + # If password is defined in naspasswd file, use it as community, + # otherwise use $cmmty_string + if ($password eq '') { + $password = "$cmmty_string"; + } + } + + # We want mtxrInterfaceStatsName from MIKROTIK-MIB + $oid = "1.3.6.1.4.1.14988.1.1.14.1.1.2"; + + # Mikrotik doesnt give port IDs correctly to RADIUS :( + # practically this would limit us to a simple only-one user limit for + # this script to work properly. + @output = snmpwalk_prog($ARGV[1], $password, "$oid"); + + foreach $line ( @output ) { + #remove newline + chomp $line; + #remove trailing whitespace + ($line = $line) =~ s/\s+$//; + if( $line =~ /<.*-$ARGV[3]>/ ) { + $username_seen++; + } + } + + #lets return something + if ($username_seen > 0) { + return 1; + } else { + return 0; + } +} + +sub mikrotik_telnet { + # Localize all variables first. + my ($t, $login, $password); + my (@fields, @output, $output, $username_seen, $user); + + return 2 unless (check_net_telnet()); + + $terminalserver = $ARGV[1]; + $user = $ARGV[3]; + + # Get login name and password for a certain NAS from $naspass. + ($login, $password) = naspasswd($terminalserver, 1); + return 2 if ($password eq ""); + + # MikroTik routeros doesnt tell us to which port the user is connected + # practically this would limit us to a simple only-one user limit for + # this script to work properly. + $t = new Net::Telnet (Timeout => 5, + Prompt => '//\[.*@.*\] > /'); + + # Dont just exit when there is error + $t->errmode('return'); + + # Telnet to terminal server + $t->open($terminalserver) or return 2; + + #Send login and password etc. + $t->login(Name => $login, + Password => $password, + # We must detect if we are logged in from the login banner. + # Because if routeros is with a free license the command + # prompt dont come. Instead it waits us to press "Enter". + Prompt => '/MikroTik/'); + + # Just be sure that routeros isn't waiting for us to press "Enter" + $t->print(""); + + # Wait for the real prompt + $t->waitfor('/\[.*@.*\] > /'); + + # It is not possible to get the line numbers etc. + # Thus we cant support if simultaneous-use is over 1 + # At least I was using pppoe so it wasnt possible. + $t->print('ppp active print column name detail'); + + # Somehow routeros echo'es our commands 2 times. We dont want to mix + # this with the real command prompt. + $t->waitfor('/\[.*@.*\] > ppp active print column name detail/'); + + # Now lets get the list of online ppp users. + ( $output ) = $t->waitfor('/\[.*@.*\] > /'); + + # For debugging we can print the list to stdout +# print $output; + + #Lets logout to make everybody happy. + #If we close the connection without logging out then routeros + #starts to complain after a while. Saying; + #telnetd: All network ports in use. + $t->print("quit"); + $t->close; + + #check for # of $user in output + #the output includes only one = between name and username so we can + #safely use it as a seperator. + +#disabled until mikrotik starts to send newline after each line... +# @output = $output; +# foreach $line ( @output ) { +# #remove newline +# chomp $line; +# #remove trailing whitespace +# ($line = $line) =~ s/\s+$//; +# if( $line =~ /name=/ ) { +# print($line); +# @fields = split( /=/, $line ); +# if( $fields[1] == "\"$user\"") { +# $username_seen++; +# } +# } +# } + + if( $output =~ /name="$user"/ ) { + $username_seen++; + } + + #lets return something + if ($username_seen > 0) { + return 1; + } else { + return 0; + } +} + +sub redback_telnet { + #Localize all variables first. + my ($terminalserver, $login, $password); + my ($user, $context, $operprompt, $adminprompt, $t); + return 2 unless (check_net_telnet()); + $terminalserver = $ARGV[1]; + ($user, $context) = split /@/, $ARGV[3]; + if (not $user) { + print LOG " Error: No user defined\n" if ($debug); + return 2; + } + if (not $context) { + print LOG " Error: No context defined\n" if ($debug); + return 2; + } + + # Get loggin information + ($root, $password) = naspasswd($terminalserver, 1); + return 2 if ($password eq ""); + + $operprompt = '/\[.*\].*>$/'; + $adminprompt = '/\[.*\].*#$/'; + + # Logging to the RedBack NAS + $t = new Net::Telnet (Timeout => 5, Prompt => $operprompt); + $t->input_log("./debug"); + $t->open($terminalserver); + $t->login($root, $password); + + #Enable us + $t->print('ena'); + $t->waitfor('/Password/'); + $t->print($password); + $t->waitfor($adminprompt); + $t->prompt($adminprompt); + + #Switch context + $t->cmd(String => "context $context"); + + #Ask the question + @lines = $t->cmd(String => "show subscribers active $user\@$context"); + if ($lines[0] =~ /subscriber $user\@$context/ ) { + return 1; + } + return 0; +} + +############################################################################### + +# Poor man's getopt (for -d) +if ($ARGV[0] eq '-d') { + shift @ARGV; + $debug = "stdout"; +} + +if ($debug) { + if ($debug eq 'stdout') { + open(LOG, ">&STDOUT"); + } elsif ($debug eq 'stderr') { + open(LOG, ">&STDERR"); + } else { + open(LOG, ">>$debug"); + $now = localtime; + print LOG "$now checkrad @ARGV\n"; + } +} + +if ($#ARGV != 4) { + print LOG "Usage: checkrad nas_type nas_ip " . + "nas_port login session_id\n" if ($debug); + print STDERR "Usage: checkrad nas_type nas_ip " . + "nas_port login session_id\n" + unless ($debug =~ m/^(stdout|stderr)$/); + close LOG if ($debug); + exit(2); +} + +if ($ARGV[0] eq 'livingston') { + $ret = &livingston_snmp; +} elsif ($ARGV[0] eq 'cisco') { + $ret = &cisco_snmp; +} elsif ($ARGV[0] eq 'cvx') { + $ret = &cvx_snmp; +} elsif ($ARGV[0] eq 'juniper') { + $ret = &juniper_e_snmp; +} elsif ($ARGV[0] eq 'multitech') { + $ret = &multitech_snmp; +} elsif ($ARGV[0] eq 'computone') { + $ret = &computone_finger; +} elsif ($ARGV[0] eq 'max40xx') { + $ret = &max40xx_finger; +} elsif ($ARGV[0] eq 'ascend' || $ARGV[0] eq 'max40xx_snmp') { + $ret = &ascend_snmp; +} elsif ($ARGV[0] eq 'portslave') { + $ret = &portslave_finger; +} elsif ($ARGV[0] eq 'tc') { + $ret = &tc_tccheck; +} elsif ($ARGV[0] eq 'pathras') { + $ret = &cyclades_telnet; +} elsif ($ARGV[0] eq 'pr3000') { + $ret = &cyclades_snmp; +} elsif ($ARGV[0] eq 'pr4000') { + $ret = &cyclades_snmp; +} elsif ($ARGV[0] eq 'patton') { + $ret = &patton_snmp; +} elsif ($ARGV[0] eq 'digitro') { + $ret = &digitro_rusers; +} elsif ($ARGV[0] eq 'usrhiper') { + $ret = &usrhiper_snmp; +} elsif ($ARGV[0] eq 'netserver') { + $ret = &usrnet_telnet; +} elsif ($ARGV[0] eq 'versanet') { + $ret = &versanet_snmp; +} elsif ($ARGV[0] eq 'bay') { + $ret = &bay_finger; +} elsif ($ARGV[0] eq 'cisco_l2tp'){ + $ret = &cisco_l2tp_snmp; +} elsif ($ARGV[0] eq 'mikrotik'){ + $ret = &mikrotik_telnet; +} elsif ($ARGV[0] eq 'mikrotik_snmp'){ + $ret = &mikrotik_snmp; +} elsif ($ARGV[0] eq 'redback'){ + $ret = &redback_telnet; +} elsif ($ARGV[0] eq 'dot1x'){ + $ret = &dot1x_snmp; +} elsif ($ARGV[0] eq 'other') { + $ret = 1; +} else { + print LOG " checkrad: unknown NAS type $ARGV[0]\n" if ($debug); + print STDERR "checkrad: unknown NAS type $ARGV[0]\n"; + $ret = 2; +} + +if ($debug) { + $mn = "login ok"; + $mn = "double detected" if ($ret == 1); + $mn = "error detected" if ($ret == 2); + print LOG " Returning $ret ($mn)\n"; + close LOG; +} + +exit($ret); diff --git a/src/main/checkrad.mk b/src/main/checkrad.mk new file mode 100644 index 0000000..e991bf7 --- /dev/null +++ b/src/main/checkrad.mk @@ -0,0 +1,5 @@ +install: $(R)$(sbindir)/checkrad + +$(R)$(sbindir)/checkrad: src/main/checkrad | $(R)$(sbindir) + @echo INSTALL $(notdir $<) + @$(INSTALL) -m 755 $< $(R)$(sbindir) diff --git a/src/main/client.c b/src/main/client.c new file mode 100644 index 0000000..12f7824 --- /dev/null +++ b/src/main/client.c @@ -0,0 +1,1581 @@ +/* + * 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 = cf_data_find(main_config.config, "clients"); + if (!root_clients) root_clients = client_list_init(main_config.config); + 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 + +#ifdef WITH_RADIUSV11 + { "radiusv1_1", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, radiusv11_name), 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((uint8_t)*p) || + isdigit((uint8_t)*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; + } + +#ifdef WITH_RADIUSV11 + if (c->tls_required && c->radiusv11_name) { + int rcode; + + rcode = fr_str2int(radiusv11_types, c->radiusv11_name, -1); + if (rcode < 0) { + cf_log_err_cs(cs, "Invalid value for 'radiusv11'"); + goto error; + } + + c->radiusv11 = rcode; + } +#endif + + /* + * 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 + diff --git a/src/main/collectd.c b/src/main/collectd.c new file mode 100644 index 0000000..b720471 --- /dev/null +++ b/src/main/collectd.c @@ -0,0 +1,382 @@ +/* + * 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 collectd.c + * @brief Helper functions to enabled radsniff to talk to collectd + * + * @copyright 2013 Arran Cudbard-Bell <a.cudbardb@freeradius.org> + */ +#include <assert.h> +#include <ctype.h> + +#ifdef HAVE_COLLECTDC_H +#include <collectd/client.h> +#include <freeradius-devel/radsniff.h> + +/** Copy a 64bit unsigned integer into a double + * + */ +/* +static void _copy_uint64_to_double(UNUSED rs_t *conf, rs_stats_value_tmpl_t *tmpl) +{ + assert(tmpl->src); + assert(tmpl->dst); + + *((double *) tmpl->dst) = *((uint64_t *) tmpl->src); +} +*/ + +/* +static void _copy_uint64_to_uint64(UNUSED rs_t *conf, rs_stats_value_tmpl_t *tmpl) +{ + assert(tmpl->src); + assert(tmpl->dst); + + *((uint64_t *) tmpl->dst) = *((uint64_t *) tmpl->src); +} +*/ + +static void _copy_double_to_double(UNUSED rs_t *conf, rs_stats_value_tmpl_t *tmpl) +{ + assert(tmpl->src); + assert(tmpl->dst); + + *((double *) tmpl->dst) = *((double*) tmpl->src); +} + + +/** Allocates a stats template which describes a single guage/counter + * + * This is just intended to simplify allocating a fairly complex memory structure + * src and dst pointers must be set + * + * @param ctx Context to allocate collectd struct in. + * @param conf Radsniff configuration. + * @param plugin_instance usually the type of packet (in our case). + * @param type string, the name of a collection of stats e.g. exchange + * @param type_instance the name of the counter/guage within the collection e.g. latency. + * @param stats structure to derive statistics from. + * @param values Value templates used to populate lcc_value_list. + * @return a new rs_stats_tmpl_t on success or NULL on failure. + */ +static rs_stats_tmpl_t *rs_stats_collectd_init(TALLOC_CTX *ctx, rs_t *conf, + char const *plugin_instance, + char const *type, char const *type_instance, + void *stats, + rs_stats_value_tmpl_t const *values) +{ + static char hostname[255]; + static char fqdn[LCC_NAME_LEN]; + + size_t len; + int i; + char *p; + + rs_stats_tmpl_t *tmpl; + lcc_value_list_t *value; + + assert(conf); + assert(type); + assert(type_instance); + + for (len = 0; values[len].src; len++) {} ; + assert(len > 0); + + /* + * Initialise hostname once so we don't call gethostname every time + */ + if (*fqdn == '\0') { + int ret; + struct addrinfo hints, *info = NULL; + + if (gethostname(hostname, sizeof(hostname)) < 0) { + ERROR("Error getting hostname: %s", fr_syserror(errno)); + + return NULL; + } + + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_UNSPEC; /*either IPV4 or IPV6*/ + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_CANONNAME; + + if ((ret = getaddrinfo(hostname, "radius", &hints, &info)) != 0) { + ERROR("Error getting hostname: %s", gai_strerror(ret)); + return NULL; + } + + strlcpy(fqdn, info->ai_canonname, sizeof(fqdn)); + + freeaddrinfo(info); + } + + tmpl = talloc_zero(ctx, rs_stats_tmpl_t); + if (!tmpl) { + return NULL; + } + + tmpl->value_tmpl = talloc_zero_array(tmpl, rs_stats_value_tmpl_t, len); + if (!tmpl->value_tmpl) { + goto error; + } + + tmpl->stats = stats; + + value = talloc_zero(tmpl, lcc_value_list_t); + if (!value) { + goto error; + } + tmpl->value = value; + + value->interval = conf->stats.interval; + value->values_len = len; + + value->values_types = talloc_zero_array(value, int, len); + if (!value->values_types) { + goto error; + } + + value->values = talloc_zero_array(value, value_t, len); + if (!value->values) { + goto error; + } + + for (i = 0; i < (int) len; i++) { + assert(values[i].src); + assert(values[i].cb); + + tmpl->value_tmpl[i] = values[i]; + switch (tmpl->value_tmpl[i].type) { + case LCC_TYPE_COUNTER: + tmpl->value_tmpl[i].dst = &value->values[i].counter; + break; + + case LCC_TYPE_GAUGE: + tmpl->value_tmpl[i].dst = &value->values[i].gauge; + break; + + case LCC_TYPE_DERIVE: + tmpl->value_tmpl[i].dst = &value->values[i].derive; + break; + + case LCC_TYPE_ABSOLUTE: + tmpl->value_tmpl[i].dst = &value->values[i].absolute; + break; + + default: + assert(0); + } + value->values_types[i] = tmpl->value_tmpl[i].type; + } + + /* + * These should be OK as is + */ + strlcpy(value->identifier.host, fqdn, sizeof(value->identifier.host)); + + /* + * Plugin is ASCII only and no '/' + */ + fr_prints(value->identifier.plugin, sizeof(value->identifier.plugin), + conf->stats.prefix, strlen(conf->stats.prefix), '\0'); + for (p = value->identifier.plugin; *p; ++p) { + if ((*p == '-') || (*p == '/'))*p = '_'; + } + + /* + * Plugin instance is ASCII only (assuming printable only) and no '/' + */ + fr_prints(value->identifier.plugin_instance, sizeof(value->identifier.plugin_instance), + plugin_instance, strlen(plugin_instance), '\0'); + for (p = value->identifier.plugin_instance; *p; ++p) { + if ((*p == '-') || (*p == '/')) *p = '_'; + } + + /* + * Type is ASCII only (assuming printable only) and no '/' or '-' + */ + fr_prints(value->identifier.type, sizeof(value->identifier.type), + type, strlen(type), '\0'); + for (p = value->identifier.type; *p; ++p) { + if ((*p == '-') || (*p == '/')) *p = '_'; + } + + fr_prints(value->identifier.type_instance, sizeof(value->identifier.type_instance), + type_instance, strlen(type_instance), '\0'); + for (p = value->identifier.type_instance; *p; ++p) { + if ((*p == '-') || (*p == '/')) *p = '_'; + } + + + return tmpl; + +error: + talloc_free(tmpl); + return NULL; +} + + +/** Setup stats templates for latency + * + */ +rs_stats_tmpl_t *rs_stats_collectd_init_latency(TALLOC_CTX *ctx, rs_stats_tmpl_t **out, rs_t *conf, + char const *type, rs_latency_t *stats, PW_CODE code) +{ + rs_stats_tmpl_t **tmpl, *last; + char *p; + char buffer[LCC_NAME_LEN]; + tmpl = out; + + rs_stats_value_tmpl_t rtx[(RS_RETRANSMIT_MAX + 1) + 1 + 1]; // RTX bins + 0 bin + lost + NULL + int i; + + /* not static so were thread safe */ + rs_stats_value_tmpl_t const _packet_count[] = { + { &stats->interval.received, LCC_TYPE_GAUGE, _copy_double_to_double, NULL }, + { &stats->interval.linked, LCC_TYPE_GAUGE, _copy_double_to_double, NULL }, + { &stats->interval.unlinked, LCC_TYPE_GAUGE, _copy_double_to_double, NULL }, + { &stats->interval.reused, LCC_TYPE_GAUGE, _copy_double_to_double, NULL }, + { NULL, 0, NULL, NULL } + }; + + rs_stats_value_tmpl_t const _latency[] = { + { &stats->latency_smoothed, LCC_TYPE_GAUGE, _copy_double_to_double, NULL }, + { &stats->interval.latency_average, LCC_TYPE_GAUGE, _copy_double_to_double, NULL }, + { &stats->interval.latency_high, LCC_TYPE_GAUGE, _copy_double_to_double, NULL }, + { &stats->interval.latency_low, LCC_TYPE_GAUGE, _copy_double_to_double, NULL }, + { NULL, 0, NULL, NULL } + }; + +#define INIT_STATS(_ti, _v) do {\ + strlcpy(buffer, fr_packet_codes[code], sizeof(buffer)); \ + for (p = buffer; *p; ++p) *p = tolower((uint8_t) *p);\ + last = *tmpl = rs_stats_collectd_init(ctx, conf, type, _ti, buffer, stats, _v);\ + if (!*tmpl) {\ + TALLOC_FREE(*out);\ + return NULL;\ + }\ + tmpl = &(*tmpl)->next;\ + ctx = *tmpl;\ + } while (0) + + + INIT_STATS("radius_count", _packet_count); + INIT_STATS("radius_latency", _latency); + + for (i = 0; i < (RS_RETRANSMIT_MAX + 1); i++) { + rtx[i].src = &stats->interval.rt[i]; + rtx[i].type = LCC_TYPE_GAUGE; + rtx[i].cb = _copy_double_to_double; + rtx[i].dst = NULL; + } + + rtx[i].src = &stats->interval.lost; + rtx[i].type = LCC_TYPE_GAUGE; + rtx[i].cb = _copy_double_to_double; + rtx[i].dst = NULL; + + memset(&rtx[++i], 0, sizeof(rs_stats_value_tmpl_t)); + + INIT_STATS("radius_rtx", rtx); + + return last; +} + +/** Refresh and send the stats to the collectd server + * + */ +void rs_stats_collectd_do_stats(rs_t *conf, rs_stats_tmpl_t *tmpls, struct timeval *now) +{ + rs_stats_tmpl_t *tmpl = tmpls; + char identifier[6 * LCC_NAME_LEN]; + int i; + + while (tmpl) { + /* + * Refresh the value of whatever were sending + */ + for (i = 0; i < (int) tmpl->value->values_len; i++) { + tmpl->value_tmpl[i].cb(conf, &tmpl->value_tmpl[i]); + } + + tmpl->value->time = now->tv_sec; + + lcc_identifier_to_string(conf->stats.handle, identifier, sizeof(identifier), &tmpl->value->identifier); + + if (lcc_putval(conf->stats.handle, tmpl->value) < 0) { + char const *error; + + error = lcc_strerror(conf->stats.handle); + ERROR("Failed PUTVAL \"%s\" interval=%i %" PRIu64 " : %s", + identifier, + (int) tmpl->value->interval, + (uint64_t) tmpl->value->time, + error ? error : "unknown error"); + } + + tmpl = tmpl->next; + } +} + +/** Connect to a collectd server for stats output + * + * @param[in,out] conf radsniff configuration, we write the generated handle here. + * @return 0 on success -1 on failure. + */ +int rs_stats_collectd_open(rs_t *conf) +{ + assert(conf->stats.collectd); + + /* + * Tear down stale connections gracefully. + */ + rs_stats_collectd_close(conf); + + /* + * There's no way to get the error from the connection handle + * because it's freed on failure, before lcc returns. + */ + if (lcc_connect(conf->stats.collectd, &conf->stats.handle) < 0) { + ERROR("Failed opening connection to collectd: %s", fr_syserror(errno)); + return -1; + } + DEBUG2("Connected to \"%s\"", conf->stats.collectd); + + assert(conf->stats.handle); + return 0; +} + +/** Close connection + * + * @param[in,out] conf radsniff configuration. + * @return 0 on success -1 on failure. + */ +int rs_stats_collectd_close(rs_t *conf) +{ + assert(conf->stats.collectd); + + int ret = 0; + + if (conf->stats.handle) { + ret = lcc_disconnect(conf->stats.handle); + conf->stats.handle = NULL; + } + + return ret; +} +#endif diff --git a/src/main/command.c b/src/main/command.c new file mode 100644 index 0000000..988f43b --- /dev/null +++ b/src/main/command.c @@ -0,0 +1,3632 @@ +/* + * command.c Command socket processing. + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2008 The FreeRADIUS server project + * Copyright 2008 Alan DeKok <aland@deployingradius.com> + */ + +#ifdef WITH_COMMAND_SOCKET + +#include <freeradius-devel/parser.h> +#include <freeradius-devel/modcall.h> +#include <freeradius-devel/md5.h> +#include <freeradius-devel/channel.h> +#include <freeradius-devel/connection.h> +#include <freeradius-devel/socket.h> + +#include <libgen.h> +#ifdef HAVE_INTTYPES_H +#include <inttypes.h> +#endif + +#ifdef HAVE_SYS_STAT_H +#include <sys/stat.h> +#endif + +#include <pwd.h> +#include <grp.h> +#include <ctype.h> + +typedef struct fr_command_table_t fr_command_table_t; + +typedef int (*fr_command_func_t)(rad_listen_t *, int, char *argv[]); + +#define FR_READ (1) +#define FR_WRITE (2) + +#define CMD_FAIL FR_CHANNEL_FAIL +#define CMD_OK FR_CHANNEL_SUCCESS + +struct fr_command_table_t { + char const *command; + int mode; /* read/write */ + char const *help; + fr_command_func_t func; + fr_command_table_t *table; +}; + +#define COMMAND_BUFFER_SIZE (1024) + +typedef struct fr_cs_buffer_t { + int auth; + int mode; + ssize_t offset; + ssize_t next; + char buffer[COMMAND_BUFFER_SIZE]; +} fr_cs_buffer_t; + +#define COMMAND_SOCKET_MAGIC (0xffdeadee) +typedef struct fr_command_socket_t { + uint32_t magic; + char const *path; + char *copy; /* <sigh> */ + uid_t uid; + gid_t gid; + char const *uid_name; + char const *gid_name; + char const *mode_name; + bool peercred; + char user[256]; + + /* + * The next few entries handle fake packets injected by + * the control socket. + */ + fr_ipaddr_t src_ipaddr; /* src_port is always 0 */ + fr_ipaddr_t dst_ipaddr; + uint16_t dst_port; + rad_listen_t *inject_listener; + RADCLIENT *inject_client; + + fr_cs_buffer_t co; +} fr_command_socket_t; + +static const CONF_PARSER command_config[] = { + { "socket", FR_CONF_OFFSET(PW_TYPE_STRING, fr_command_socket_t, path), "${run_dir}/radiusd.sock" }, + { "uid", FR_CONF_OFFSET(PW_TYPE_STRING, fr_command_socket_t, uid_name), NULL }, + { "gid", FR_CONF_OFFSET(PW_TYPE_STRING, fr_command_socket_t, gid_name), NULL }, + { "mode", FR_CONF_OFFSET(PW_TYPE_STRING, fr_command_socket_t, mode_name), NULL }, + { "peercred", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_command_socket_t, peercred), "yes" }, + CONF_PARSER_TERMINATOR +}; + +static FR_NAME_NUMBER mode_names[] = { + { "ro", FR_READ }, + { "read-only", FR_READ }, + { "read-write", FR_READ | FR_WRITE }, + { "rw", FR_READ | FR_WRITE }, + { NULL, 0 } +}; + +#if !defined(HAVE_GETPEEREID) && defined(SO_PEERCRED) +static int getpeereid(int s, uid_t *euid, gid_t *egid) +{ + struct ucred cr; + socklen_t cl = sizeof(cr); + + if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, &cr, &cl) < 0) { + return -1; + } + + *euid = cr.uid; + *egid = cr.gid; + return 0; +} + +/* we now have getpeereid() in this file */ +#define HAVE_GETPEEREID (1) + +#endif /* HAVE_GETPEEREID */ + +/** Initialise a socket for use with peercred authentication + * + * This function initialises a socket and path in a way suitable for use with + * peercred. + * + * @param path to socket. + * @param uid that should own the socket (linux only). + * @param gid that should own the socket (linux only). + * @return 0 on success -1 on failure. + */ +#ifdef __linux__ +static int fr_server_domain_socket_peercred(char const *path, uid_t uid, gid_t gid) +#else +static int fr_server_domain_socket_peercred(char const *path, uid_t UNUSED uid, UNUSED gid_t gid) +#endif +{ + int sockfd; + size_t len; + socklen_t socklen; + struct sockaddr_un salocal; + struct stat buf; + + if (!path) { + fr_strerror_printf("No path provided, was NULL"); + return -1; + } + + len = strlen(path); + if (len >= sizeof(salocal.sun_path)) { + fr_strerror_printf("Path too long in socket filename"); + return -1; + } + + if ((sockfd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { + fr_strerror_printf("Failed creating socket: %s", fr_syserror(errno)); + return -1; + } + + memset(&salocal, 0, sizeof(salocal)); + salocal.sun_family = AF_UNIX; + memcpy(salocal.sun_path, path, len + 1); /* SUN_LEN does strlen */ + + socklen = SUN_LEN(&salocal); + + /* + * Check the path. + */ + if (stat(path, &buf) < 0) { + if (errno != ENOENT) { + fr_strerror_printf("Failed to stat %s: %s", path, fr_syserror(errno)); + close(sockfd); + return -1; + } + + /* + * FIXME: Check the enclosing directory? + */ + } else { /* it exists */ + int client_fd; + + if (!S_ISREG(buf.st_mode) +#ifdef S_ISSOCK + && !S_ISSOCK(buf.st_mode) +#endif + ) { + fr_strerror_printf("Cannot turn %s into socket", path); + close(sockfd); + return -1; + } + + /* + * Refuse to open sockets not owned by us. + */ + if (buf.st_uid != geteuid()) { + fr_strerror_printf("We do not own %s", path); + close(sockfd); + return -1; + } + + /* + * Check if a server is already listening on the + * socket? + */ + client_fd = fr_socket_client_unix(path, false); + if (client_fd >= 0) { + fr_strerror_printf("Control socket '%s' is already in use", path); + close(client_fd); + close(sockfd); + return -1; + } + + if (unlink(path) < 0) { + fr_strerror_printf("Failed to delete %s: %s", path, fr_syserror(errno)); + close(sockfd); + return -1; + } + } + + if (bind(sockfd, (struct sockaddr *)&salocal, socklen) < 0) { + fr_strerror_printf("Failed binding to %s: %s", path, fr_syserror(errno)); + close(sockfd); + return -1; + } + + /* + * FIXME: There's a race condition here. But Linux + * doesn't seem to permit fchmod on domain sockets. + */ + if (chmod(path, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP) < 0) { + fr_strerror_printf("Failed setting permissions on %s: %s", path, fr_syserror(errno)); + close(sockfd); + return -1; + } + + if (listen(sockfd, 8) < 0) { + fr_strerror_printf("Failed listening to %s: %s", path, fr_syserror(errno)); + close(sockfd); + return -1; + } + +#ifdef O_NONBLOCK + { + int flags; + + if ((flags = fcntl(sockfd, F_GETFL, NULL)) < 0) { + fr_strerror_printf("Failure getting socket flags: %s", fr_syserror(errno)); + close(sockfd); + return -1; + } + + flags |= O_NONBLOCK; + if( fcntl(sockfd, F_SETFL, flags) < 0) { + fr_strerror_printf("Failure setting socket flags: %s", fr_syserror(errno)); + close(sockfd); + return -1; + } + } +#endif + + /* + * Changing socket permissions only works on linux. + * BSDs ignore socket permissions. + */ +#ifdef __linux__ + /* + * Don't chown it from (possibly) non-root to root. + * Do chown it from (possibly) root to non-root. + */ + if ((uid != (uid_t) -1) || (gid != (gid_t) -1)) { + /* + * Don't do chown if it's already owned by us. + */ + if (fstat(sockfd, &buf) < 0) { + fr_strerror_printf("Failed reading %s: %s", path, fr_syserror(errno)); + close(sockfd); + return -1; + } + + if ((buf.st_uid != uid) || (buf.st_gid != gid)) { + rad_suid_up(); + if (fchown(sockfd, uid, gid) < 0) { + fr_strerror_printf("Failed setting ownership of %s to (%d, %d): %s", + path, uid, gid, fr_syserror(errno)); + rad_suid_down(); + close(sockfd); + return -1; + } + rad_suid_down(); + } + } +#endif + + return sockfd; +} + +#if !defined(HAVE_OPENAT) || !defined(HAVE_MKDIRAT) || !defined(HAVE_UNLINKAT) +static int fr_server_domain_socket_perm(UNUSED char const *path, UNUSED uid_t uid, UNUSED gid_t gid) +{ + fr_strerror_printf("Unable to initialise control socket. Set peercred = yes or update to " + "POSIX-2008 compliant libc"); + return -1; +} +#else +/** Alternative function for creating Unix domain sockets and enforcing permissions + * + * Unlike fr_server_unix_socket which is intended to be used with peercred auth + * this function relies on the file system to enforce access. + * + * The way it does this depends on the operating system. On Linux systems permissions + * can be set on the socket directly and the system will enforce them. + * + * On most other systems fchown and fchmod fail when called with socket descriptors, + * and although permissions can be changed in other ways, they're not enforced. + * + * For these systems we use the permissions on the parent directory to enforce + * permissions on the socket. It's not safe to modify these permissions ourselves + * due to TOCTOU attacks, so if they don't match what we require, we error out and + * get the user to change them (which arguably isn't any safer, but releases us of + * the responsibility). + * + * @note must be called without effective root permissions (fr_suid_down). + * + * @param path where domain socket should be created. + * @return a file descriptor for the bound socket on success, -1 on failure. + */ +static int fr_server_domain_socket_perm(char const *path, uid_t uid, gid_t gid) +{ + int dir_fd = -1, sock_fd = -1, parent_fd = -1; + char const *name; + char *buff = NULL, *dir = NULL, *p; + + uid_t euid; + gid_t egid; + + mode_t perm = 0; + struct stat st; + + size_t len; + + socklen_t socklen; + struct sockaddr_un salocal; + + rad_assert(path); + + euid = geteuid(); + egid = getegid(); + + /* + * Determine the correct permissions for the socket, or its + * containing directory. + */ + perm |= S_IREAD | S_IWRITE | S_IEXEC; + if (gid != (gid_t) -1) perm |= S_IRGRP | S_IWGRP | S_IXGRP; + + buff = talloc_strdup(NULL, path); + if (!buff) return -1; + + /* + * Some implementations modify it in place others use internal + * storage *sigh*. dirname also formats the path else we wouldn't + * be using it. + */ + dir = dirname(buff); + if (dir != buff) { + dir = talloc_strdup(NULL, dir); + if (!dir) return -1; + talloc_free(buff); + } + + p = strrchr(dir, FR_DIR_SEP); + if (!p) { + fr_strerror_printf("Failed determining parent directory"); + error: + talloc_free(dir); + if (sock_fd >= 0) close(sock_fd); + if (dir_fd >= 0) close(dir_fd); + if (parent_fd >= 0) close(parent_fd); + return -1; + } + + *p = '\0'; + + /* + * Ensure the parent of the control socket directory exists, + * and the euid we're running under has access to it. + */ + parent_fd = open(dir, O_DIRECTORY); + if (parent_fd < 0) { + struct passwd *user; + struct group *group; + + if (rad_getpwuid(NULL, &user, euid) < 0) goto error; + if (rad_getgrgid(NULL, &group, egid) < 0) { + talloc_free(user); + goto error; + } + fr_strerror_printf("Can't open directory \"%s\": %s. Must be created manually, or modified, " + "with permissions that allow writing by user %s or group %s", dir, + user->pw_name, group->gr_name, fr_syserror(errno)); + talloc_free(user); + talloc_free(group); + goto error; + } + + *p = FR_DIR_SEP; + + dir_fd = openat(parent_fd, p + 1, O_NOFOLLOW | O_DIRECTORY); + if (dir_fd < 0) { + int ret = 0; + + if (errno != ENOENT) { + fr_strerror_printf("Failed opening control socket directory: %s", fr_syserror(errno)); + goto error; + } + + /* + * This fails if the radius user can't write + * to the parent directory. + */ + if (mkdirat(parent_fd, p + 1, 0700) < 0) { + fr_strerror_printf("Failed creating control socket directory: %s", fr_syserror(errno)); + goto error; + } + + dir_fd = openat(parent_fd, p + 1, O_NOFOLLOW | O_DIRECTORY); + if (dir_fd < 0) { + fr_strerror_printf("Failed opening the control socket directory we created: %s", + fr_syserror(errno)); + goto error; + } + if (fchmod(dir_fd, perm) < 0) { + fr_strerror_printf("Failed setting permissions on control socket directory: %s", + fr_syserror(errno)); + goto error; + } + + rad_suid_up(); + if ((uid != (uid_t)-1) || (gid != (gid_t)-1)) ret = fchown(dir_fd, uid, gid); + rad_suid_down(); + if (ret < 0) { + fr_strerror_printf("Failed changing ownership of control socket directory: %s", + fr_syserror(errno)); + goto error; + } + /* + * Control socket dir already exists, but we still need to + * check the permissions are what we expect. + */ + } else { + int ret; + int client_fd; + + ret = fstat(dir_fd, &st); + if (ret < 0) { + fr_strerror_printf("Failed checking permissions of control socket directory: %s", + fr_syserror(errno)); + goto error; + } + + if ((uid != (uid_t)-1) && (st.st_uid != uid)) { + struct passwd *need_user, *have_user; + + if (rad_getpwuid(NULL, &need_user, uid) < 0) goto error; + if (rad_getpwuid(NULL, &have_user, st.st_uid) < 0) { + talloc_free(need_user); + goto error; + } + fr_strerror_printf("Control socket directory must be owned by user %s, " + "currently owned by %s", need_user->pw_name, have_user->pw_name); + talloc_free(need_user); + talloc_free(have_user); + goto error; + } + + if ((gid != (gid_t)-1) && (st.st_gid != gid)) { + struct group *need_group, *have_group; + + if (rad_getgrgid(NULL, &need_group, gid) < 0) goto error; + if (rad_getgrgid(NULL, &have_group, st.st_gid) < 0) { + talloc_free(need_group); + goto error; + } + fr_strerror_printf("Control socket directory \"%s\" must be owned by group %s, " + "currently owned by %s", dir, need_group->gr_name, have_group->gr_name); + talloc_free(need_group); + talloc_free(have_group); + goto error; + } + + if ((perm & 0x0c) != (st.st_mode & 0x0c)) { + char str_need[10], oct_need[5]; + char str_have[10], oct_have[5]; + + rad_mode_to_str(str_need, perm); + rad_mode_to_oct(oct_need, perm); + rad_mode_to_str(str_have, st.st_mode); + rad_mode_to_oct(oct_have, st.st_mode); + fr_strerror_printf("Control socket directory must have permissions %s (%s), current " + "permissions are %s (%s)", str_need, oct_need, str_have, oct_have); + goto error; + } + + /* + * Check if a server is already listening on the + * socket? + */ + client_fd = fr_socket_client_unix(path, false); + if (client_fd >= 0) { + fr_strerror_printf("Control socket '%s' is already in use", path); + close(client_fd); + goto error; + } + } + + name = strrchr(path, FR_DIR_SEP); + if (!name) { + fr_strerror_printf("Can't determine socket name"); + goto error; + } + name++; + + /* + * We've checked the containing directory has the permissions + * we expect, and as we have the FD, and aren't following + * symlinks no one can trick us into changing or creating a + * file elsewhere. + * + * It's possible an attacker may still be able to create hard + * links, for the socket file. But they would need write + * access to the directory we just created or verified, so + * this attack vector is unlikely. + */ + if ((uid != (uid_t)-1) && (rad_seuid(uid) < 0)) goto error; + if ((gid != (gid_t)-1) && (rad_segid(gid) < 0)) { + rad_seuid(euid); + goto error; + } + + /* + * The original code, did openat, used fstat to figure out + * what type the file was and then used unlinkat to unlink + * it. Except on OSX (at least) openat refuses to open + * socket files. So we now rely on the fact that unlinkat + * has sane and consistent behaviour, and will not unlink + * directories. unlinkat should also fail if the socket user + * hasn't got permission to modify the socket. + */ + if ((unlinkat(dir_fd, name, 0) < 0) && (errno != ENOENT)) { + fr_strerror_printf("Failed removing stale socket: %s", fr_syserror(errno)); + sock_error: + if (uid != (uid_t)-1) rad_seuid(euid); + if (gid != (gid_t)-1) rad_segid(egid); + close(sock_fd); + + goto error; + } + + /* + * At this point we should have established a secure directory + * to house our socket, and cleared out any stale sockets. + */ + sock_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (sock_fd < 0) { + fr_strerror_printf("Failed creating socket: %s", fr_syserror(errno)); + goto sock_error; + } + +#ifdef HAVE_BINDAT + len = strlen(name); +#else + len = strlen(path); +#endif + if (len >= sizeof(salocal.sun_path)) { + fr_strerror_printf("Path too long in socket filename"); + goto error; + } + + memset(&salocal, 0, sizeof(salocal)); + salocal.sun_family = AF_UNIX; + +#ifdef HAVE_BINDAT + memcpy(salocal.sun_path, name, len + 1); /* SUN_LEN does strlen */ +#else + memcpy(salocal.sun_path, path, len + 1); /* SUN_LEN does strlen */ +#endif + socklen = SUN_LEN(&salocal); + + /* + * Direct socket permissions are only useful on Linux which + * actually enforces them. BSDs don't. They also need to be + * set before binding the socket to a file. + */ +#ifdef __linux__ + if (fchmod(sock_fd, perm) < 0) { + char str_need[10], oct_need[5]; + + rad_mode_to_str(str_need, perm); + rad_mode_to_oct(oct_need, perm); + fr_strerror_printf("Failed changing socket permissions to %s (%s)", str_need, oct_need); + + goto sock_error; + } + + if (fchown(sock_fd, uid, gid) < 0) { + struct passwd *user; + struct group *group; + + if (rad_getpwuid(NULL, &user, uid) < 0) goto sock_error; + if (rad_getgrgid(NULL, &group, gid) < 0) { + talloc_free(user); + goto sock_error; + } + + fr_strerror_printf("Failed changing ownership of socket to %s:%s", user->pw_name, group->gr_name); + talloc_free(user); + talloc_free(group); + goto sock_error; + } +#endif + /* + * The correct function to use here is bindat(), but only + * quite recent versions of FreeBSD actually have it, and + * it's definitely not POSIX. + */ +#ifdef HAVE_BINDAT + if (bindat(dir_fd, sock_fd, (struct sockaddr *)&salocal, socklen) < 0) { +#else + if (bind(sock_fd, (struct sockaddr *)&salocal, socklen) < 0) { +#endif + fr_strerror_printf("Failed binding socket: %s", fr_syserror(errno)); + goto sock_error; + } + + if (listen(sock_fd, 8) < 0) { + fr_strerror_printf("Failed listening on socket: %s", fr_syserror(errno)); + goto sock_error; + } + +#ifdef O_NONBLOCK + { + int flags; + + flags = fcntl(sock_fd, F_GETFL, NULL); + if (flags < 0) { + fr_strerror_printf("Failed getting socket flags: %s", fr_syserror(errno)); + goto sock_error; + } + + flags |= O_NONBLOCK; + if (fcntl(sock_fd, F_SETFL, flags) < 0) { + fr_strerror_printf("Failed setting nonblocking socket flag: %s", fr_syserror(errno)); + goto sock_error; + } + } +#endif + + if (uid != (uid_t)-1) rad_seuid(euid); + if (gid != (gid_t)-1) rad_segid(egid); + + if (dir_fd >= 0) close(dir_fd); + if (parent_fd >= 0) close(parent_fd); + + return sock_fd; +} +#endif + +static void command_close_socket(rad_listen_t *this) +{ + this->status = RAD_LISTEN_STATUS_EOL; + + /* + * This removes the socket from the event fd, so no one + * will be calling us any more. + */ + radius_update_listener(this); +} + +static ssize_t CC_HINT(format (printf, 2, 3)) cprintf(rad_listen_t *listener, char const *fmt, ...) +{ + ssize_t r, len; + va_list ap; + char buffer[256]; + + va_start(ap, fmt); + len = vsnprintf(buffer, sizeof(buffer), fmt, ap); + va_end(ap); + + if (listener->status == RAD_LISTEN_STATUS_EOL) return 0; + + r = fr_channel_write(listener->fd, FR_CHANNEL_STDOUT, buffer, len); + if (r <= 0) command_close_socket(listener); + + /* + * FIXME: Keep writing until done? + */ + return r; +} + +static ssize_t CC_HINT(format (printf, 2, 3)) cprintf_error(rad_listen_t *listener, char const *fmt, ...) +{ + ssize_t r, len; + va_list ap; + char buffer[256]; + + va_start(ap, fmt); + len = vsnprintf(buffer, sizeof(buffer), fmt, ap); + va_end(ap); + + if (listener->status == RAD_LISTEN_STATUS_EOL) return 0; + + r = fr_channel_write(listener->fd, FR_CHANNEL_STDERR, buffer, len); + if (r <= 0) command_close_socket(listener); + + /* + * FIXME: Keep writing until done? + */ + return r; +} + +static int command_hup(rad_listen_t *listener, int argc, char *argv[]) +{ + CONF_SECTION *cs; + module_instance_t *mi; + char buffer[256]; + + if (argc == 0) { + radius_signal_self(RADIUS_SIGNAL_SELF_HUP); + return CMD_OK; + } + + /* + * Hack a "main" HUP thingy + */ + if (strcmp(argv[0], "main.log") == 0) { + hup_logfile(); + return CMD_OK; + } + + cs = cf_section_find("modules"); + if (!cs) return CMD_FAIL; + + mi = module_find(cs, argv[0]); + if (!mi) { + cprintf_error(listener, "No such module \"%s\"\n", argv[0]); + return CMD_FAIL; + } + + if ((mi->entry->module->type & RLM_TYPE_HUP_SAFE) == 0) { + cprintf_error(listener, "Module %s cannot be hup'd\n", + argv[0]); + return CMD_FAIL; + } + + if (!module_hup_module(mi->cs, mi, time(NULL))) { + cprintf_error(listener, "Failed to reload module\n"); + return CMD_FAIL; + } + + snprintf(buffer, sizeof(buffer), "modules.%s.hup", + cf_section_name1(mi->cs)); + exec_trigger(NULL, mi->cs, buffer, true); + + return CMD_OK; +} + +static int command_terminate(UNUSED rad_listen_t *listener, + UNUSED int argc, UNUSED char *argv[]) +{ + radius_signal_self(RADIUS_SIGNAL_SELF_TERM); + + return CMD_OK; +} + +static int command_uptime(rad_listen_t *listener, + UNUSED int argc, UNUSED char *argv[]) +{ + char buffer[128]; + + CTIME_R(&fr_start_time, buffer, sizeof(buffer)); + cprintf(listener, "Up since %s", buffer); /* no \r\n */ + + return CMD_OK; +} + +static int command_show_config(rad_listen_t *listener, int argc, char *argv[]) +{ + CONF_ITEM *ci; + CONF_PAIR *cp; + char const *value; + + if (argc != 1) { + cprintf_error(listener, "No path was given\n"); + return CMD_FAIL; + } + + ci = cf_reference_item(main_config.config, main_config.config, argv[0]); + if (!ci) return CMD_FAIL; + + if (!cf_item_is_pair(ci)) return CMD_FAIL; + + cp = cf_item_to_pair(ci); + value = cf_pair_value(cp); + if (!value) return CMD_FAIL; + + cprintf(listener, "%s\n", value); + + return CMD_OK; +} + +static char const tabs[] = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"; + +/* + * FIXME: Recurse && indent? + */ +static void cprint_conf_parser(rad_listen_t *listener, int indent, CONF_SECTION *cs, + void const *base) + +{ + int i; + char const *name1 = cf_section_name1(cs); + char const *name2 = cf_section_name2(cs); + CONF_PARSER const *variables = cf_section_parse_table(cs); + + if (name2) { + cprintf(listener, "%.*s%s %s {\n", indent, tabs, name1, name2); + } else { + cprintf(listener, "%.*s%s {\n", indent, tabs, name1); + } + + indent++; + + /* + * Print + */ + if (variables) for (i = 0; variables[i].name != NULL; i++) { + void const *data; + char buffer[256]; + + /* + * No base struct offset, data must be the pointer. + * If data doesn't exist, ignore the entry, there + * must be something wrong. + */ + if (!base) { + if (!variables[i].data) { + continue; + } + + data = variables[i].data; + + } else if (variables[i].data) { + data = variables[i].data; + + } else { + data = (((char const *)base) + variables[i].offset); + } + + /* + * Ignore the various flags + */ + switch (variables[i].type & 0xff) { + default: + cprintf(listener, "%.*s%s = ?\n", indent, tabs, + variables[i].name); + break; + + case PW_TYPE_INTEGER: + cprintf(listener, "%.*s%s = %u\n", indent, tabs, + variables[i].name, *(int const *) data); + break; + + case PW_TYPE_IPV4_ADDR: + inet_ntop(AF_INET, data, buffer, sizeof(buffer)); + break; + + case PW_TYPE_IPV6_ADDR: + inet_ntop(AF_INET6, data, buffer, sizeof(buffer)); + break; + + case PW_TYPE_BOOLEAN: + cprintf(listener, "%.*s%s = %s\n", indent, tabs, + variables[i].name, + ((*(bool const *) data) == false) ? "no" : "yes"); + break; + + case PW_TYPE_STRING: + case PW_TYPE_FILE_INPUT: + case PW_TYPE_FILE_OUTPUT: + /* + * FIXME: Escape things in the string! + */ + if (*(char const * const *) data) { + cprintf(listener, "%.*s%s = \"%s\"\n", indent, tabs, + variables[i].name, *(char const * const *) data); + } else { + cprintf(listener, "%.*s%s = \n", indent, tabs, + variables[i].name); + } + + break; + } + } + + indent--; + + cprintf(listener, "%.*s}\n", indent, tabs); +} + +static void cprint_conf_section(rad_listen_t *listener, int indent, CONF_SECTION *cs) +{ + char const *name1 = cf_section_name1(cs); + char const *name2 = cf_section_name2(cs); + CONF_ITEM *ci; + + if (name2) { + cprintf(listener, "%.*s%s %s {\n", indent, tabs, name1, name2); + } else { + cprintf(listener, "%.*s%s {\n", indent, tabs, name1); + } + + indent++; + + + for (ci = cf_item_find_next(cs, NULL); + ci != NULL; + ci = cf_item_find_next(cs, ci)) { + CONF_PAIR const *cp; + char const *value; + + if (cf_item_is_section(ci)) { + cprint_conf_section(listener, indent, cf_item_to_section(ci)); + continue; + } + + if (!cf_item_is_pair(ci)) continue; + + cp = cf_item_to_pair(ci); + value = cf_pair_value(cp); + + if (value) { + /* + * @todo - quote the value if necessary. + */ + cprintf(listener, "%.*s%s = %s\n", + indent, tabs, + cf_pair_attr(cp), value); + } else { + cprintf(listener, "%.*s%s\n", + indent, tabs, + cf_pair_attr(cp)); + } + } + + indent--; + + cprintf(listener, "%.*s}\n", indent, tabs); +} + +static int command_show_module_config(rad_listen_t *listener, int argc, char *argv[]) +{ + CONF_SECTION *cs; + module_instance_t *mi; + + if (argc != 1) { + cprintf_error(listener, "No module name was given\n"); + return CMD_FAIL; + } + + cs = cf_section_find("modules"); + if (!cs) return CMD_FAIL; + + mi = module_find(cs, argv[0]); + if (!mi) { + cprintf_error(listener, "No such module \"%s\"\n", argv[0]); + return CMD_FAIL; + } + + cprint_conf_parser(listener, 0, mi->cs, mi->insthandle); + + return CMD_OK; +} + +static char const *method_names[MOD_COUNT] = { + "authenticate", + "authorize", + "preacct", + "accounting", + "session", + "pre-proxy", + "post-proxy", + "post-auth" +#ifdef WITH_COA + , + "recv-coa", + "send-coa" +#endif +}; + + +static int command_show_module_methods(rad_listen_t *listener, int argc, char *argv[]) +{ + int i; + CONF_SECTION *cs; + module_instance_t const *mi; + module_t const *mod; + + if (argc != 1) { + cprintf_error(listener, "No module name was given\n"); + return CMD_FAIL; + } + + cs = cf_section_find("modules"); + if (!cs) return CMD_FAIL; + + mi = module_find(cs, argv[0]); + if (!mi) { + cprintf_error(listener, "No such module \"%s\"\n", argv[0]); + return CMD_FAIL; + } + + mod = mi->entry->module; + + for (i = 0; i < MOD_COUNT; i++) { + if (mod->methods[i]) cprintf(listener, "%s\n", method_names[i]); + } + + return CMD_OK; +} + + +static int command_show_module_flags(rad_listen_t *listener, int argc, char *argv[]) +{ + CONF_SECTION *cs; + module_instance_t const *mi; + module_t const *mod; + + if (argc != 1) { + cprintf_error(listener, "No module name was given\n"); + return CMD_FAIL; + } + + cs = cf_section_find("modules"); + if (!cs) return CMD_FAIL; + + mi = module_find(cs, argv[0]); + if (!mi) { + cprintf_error(listener, "No such module \"%s\"\n", argv[0]); + return CMD_FAIL; + } + + mod = mi->entry->module; + + if ((mod->type & RLM_TYPE_THREAD_UNSAFE) != 0) + cprintf(listener, "thread-unsafe\n"); + + if ((mod->type & RLM_TYPE_HUP_SAFE) != 0) + cprintf(listener, "reload-on-hup\n"); + + return CMD_OK; +} + +static int command_show_module_status(rad_listen_t *listener, int argc, char *argv[]) +{ + CONF_SECTION *cs; + const module_instance_t *mi; + + if (argc != 1) { + cprintf_error(listener, "No module name was given\n"); + return CMD_FAIL; + } + + cs = cf_section_find("modules"); + if (!cs) return CMD_FAIL; + + mi = module_find(cs, argv[0]); + if (!mi) { + cprintf_error(listener, "No such module \"%s\"\n", argv[0]); + return CMD_FAIL; + } + + if (!mi->force) { + cprintf(listener, "alive\n"); + } else { + cprintf(listener, "%s\n", fr_int2str(mod_rcode_table, mi->code, "<invalid>")); + } + + + return CMD_OK; +} + + +/* + * Show all loaded modules + */ +static int command_show_modules(rad_listen_t *listener, UNUSED int argc, UNUSED char *argv[]) +{ + CONF_SECTION *cs, *subcs; + + cs = cf_section_find("modules"); + if (!cs) return CMD_FAIL; + + subcs = NULL; + while ((subcs = cf_subsection_find_next(cs, subcs, NULL)) != NULL) { + char const *name1 = cf_section_name1(subcs); + char const *name2 = cf_section_name2(subcs); + + module_instance_t *mi; + + if (name2) { + mi = module_find(cs, name2); + if (!mi) continue; + + cprintf(listener, "%s (%s)\n", name2, name1); + } else { + mi = module_find(cs, name1); + if (!mi) continue; + + cprintf(listener, "%s\n", name1); + } + } + + return CMD_OK; +} + +#ifdef WITH_PROXY +static int command_show_home_servers(rad_listen_t *listener, int argc, char *argv[]) +{ + int i; + home_server_t *home; + char const *type, *state, *proto; + + char buffer[256]; + + for (i = 0; i < home_server_max_number; i++) { + + if ((home = home_server_bynumber(i)) == NULL) + continue; + + /* + * Internal "virtual" home server. + */ + if (home->ipaddr.af == AF_UNSPEC) continue; + + if (home->type == HOME_TYPE_AUTH) { + type = "auth"; + + } else if (home->type == HOME_TYPE_ACCT) { + type = "acct"; + + } else if (home->type == HOME_TYPE_AUTH_ACCT) { + type = "auth+acct"; + +#ifdef WITH_COA + } else if (home->type == HOME_TYPE_COA) { + type = "coa"; +#endif + + } else continue; + + if (home->proto == IPPROTO_UDP) { + proto = "udp"; + } +#ifdef WITH_TCP + else if (home->proto == IPPROTO_TCP) { + proto = "tcp"; + } +#endif + else proto = "??"; + + if (home->state == HOME_STATE_ALIVE) { + state = "alive"; + + } else if (home->state == HOME_STATE_ZOMBIE) { + state = "zombie"; + + } else if (home->state == HOME_STATE_IS_DEAD) { + state = "dead"; + + } else if (home->state == HOME_STATE_ADMIN_DOWN) { + state = "down"; + + } else if (home->state == HOME_STATE_UNKNOWN) { + time_t now = time(NULL); + + /* + * We've recently received a packet, so + * the home server seems to be alive. + * + * The *reported* state changes because + * the internal state machine NEEDS THE + * RIGHT STATE. However, reporting that + * to the admin will confuse them. + * So... we lie. No, that dress doesn't + * make you look fat... + */ + if ((home->last_packet_recv + (int)home->ping_interval) >= now) { + state = "alive"; + } else { + state = "unknown"; + } + + } else continue; + + if (argc > 0 && !strcmp(argv[0], "all")) { + char const *dynamic = home->dynamic ? "yes" : "no"; + + cprintf(listener, "%s\t%d\t%s\t%s\t%s\t%d\t(name=%s, dynamic=%s)\n", + ip_ntoh(&home->ipaddr, buffer, sizeof(buffer)), + home->port, proto, type, state, + home->currently_outstanding, home->name, dynamic); + } else { + cprintf(listener, "%s\t%d\t%s\t%s\t%s\t%d\n", + ip_ntoh(&home->ipaddr, buffer, sizeof(buffer)), + home->port, proto, type, state, + home->currently_outstanding); + } + } + + return CMD_OK; +} +#endif + +static RADCLIENT *get_client(rad_listen_t *listener, int argc, char *argv[]); + +static int command_show_client_config(rad_listen_t *listener, int argc, char *argv[]) +{ + RADCLIENT *client; + + client = get_client(listener, argc, argv); + if (!client) { + return 0; + } + + if (!client->cs) return 1; + + cprint_conf_section(listener, 0, client->cs); + return 1; +} + +/* + * @todo - copied from clients.c. Better to re-use, but whatever. + */ +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; +}; + + +static int command_show_clients(rad_listen_t *listener, int argc, char *argv[]) +{ + int i; + RADCLIENT *client; + char buffer[256]; + + if (argc == 0) { + for (i = 0; (client = client_findbynumber(NULL, i)) != NULL; i++) { + ip_ntoh(&client->ipaddr, buffer, sizeof(buffer)); + + if (((client->ipaddr.af == AF_INET) && + (client->ipaddr.prefix != 32)) || + ((client->ipaddr.af == AF_INET6) && + (client->ipaddr.prefix != 128))) { + cprintf(listener, "%s/%d\n", buffer, client->ipaddr.prefix); + } else { + cprintf(listener, "%s\n", buffer); + } + } + + return CMD_OK; + } + + if (argc != 1) { + cprintf_error(listener, "Unknown command %s %s ...\n", argv[0], argv[1]); + return -1; + } + + if (strcmp(argv[0], "verbose") != 0) { + cprintf_error(listener, "Unknown command %s\n", argv[0]); + return -1; + } + + for (i = 0; (client = client_findbynumber(NULL, i)) != NULL; i++) { + if (client->cs) { + cprintf(listener, "client %s {\n", cf_section_name2(client->cs)); + } else { + cprintf(listener, "client {\n"); + } + + fr_ntop(buffer, sizeof(buffer), &client->ipaddr); + cprintf(listener, "\tipaddr = %s\n", buffer); + + if (client->src_ipaddr.af != AF_UNSPEC) { + fr_ntop(buffer, sizeof(buffer), &client->src_ipaddr); + cprintf(listener, "\tsrc_ipaddr = %s\n", buffer); + } + + if (client->proto == IPPROTO_UDP) { + cprintf(listener, "\tproto = udp\n"); + } else if (client->proto == IPPROTO_TCP) { + cprintf(listener, "\tproto = tcp\n"); + } else { + cprintf(listener, "\tproto = *\n"); + } + + cprintf(listener, "\tsecret = %s\n", client->secret); + cprintf(listener, "\tlongname = %s\n", client->longname); + cprintf(listener, "\tshortname = %s\n", client->shortname); + if (client->nas_type) cprintf(listener, "\tnas_type = %s\n", client->nas_type); + cprintf(listener, "\tnumber = %d\n", client->number); + + if (client->server) { + cprintf(listener, "\tvirtual_server = %s\n", client->server); + } + +#ifdef WITH_DYNAMIC_CLIENTS + if (client->dynamic) { + cprintf(listener, "\tdynamic = yes\n"); + cprintf(listener, "\tlifetime = %u\n", client->lifetime); + } +#endif + +#ifdef WITH_TLS + if (client->tls_required) { + cprintf(listener, "\ttls = yes\n"); + } +#endif + + if (client->list && client->list->server) { + cprintf(listener, "\tparent_virtual_server = %s\n", client->list->server); + } else { + cprintf(listener, "\tglobal = yes\n"); + } + + cprintf(listener, "}\n"); + } + + return CMD_OK; +} + + +static int command_show_version(rad_listen_t *listener, UNUSED int argc, UNUSED char *argv[]) +{ + cprintf(listener, "%s\n", radiusd_version); + return CMD_OK; +} + +static int command_debug_level(rad_listen_t *listener, int argc, char *argv[]) +{ + int number; + + if (argc == 0) { + cprintf_error(listener, "Must specify <number>\n"); + return -1; + } + + number = atoi(argv[0]); + if ((number < 0) || (number > 4)) { + cprintf_error(listener, "<number> must be between 0 and 4\n"); + return -1; + } + + fr_debug_lvl = rad_debug_lvl = number; + + return CMD_OK; +} + +static char debug_log_file_buffer[1024]; + +static int command_debug_file(rad_listen_t *listener, int argc, char *argv[]) +{ + if (rad_debug_lvl && default_log.dst == L_DST_STDOUT) { + cprintf_error(listener, "Cannot redirect debug logs to a file when already in debugging mode.\n"); + return -1; + } + + if ((argc > 0) && (strchr(argv[0], FR_DIR_SEP) == argv[0])) { + cprintf_error(listener, "Cannot direct debug logs to absolute path.\n"); + } + + default_log.debug_file = NULL; + + if (argc == 0) return CMD_OK; + + /* + * This looks weird, but it's here to avoid locking + * a mutex for every log message. + */ + memset(debug_log_file_buffer, 0, sizeof(debug_log_file_buffer)); + + /* + * Debug files always go to the logging directory. + */ + snprintf(debug_log_file_buffer, sizeof(debug_log_file_buffer), + "%s/%s", radlog_dir, argv[0]); + + default_log.debug_file = &debug_log_file_buffer[0]; + + return CMD_OK; +} + +extern fr_cond_t *debug_condition; +static int command_debug_condition(rad_listen_t *listener, int argc, char *argv[]) +{ + int i; + char const *error; + ssize_t slen = 0; + fr_cond_t *new_condition = NULL; + char *p, buffer[1024]; + + /* + * Disable it. + */ + if (argc == 0) { + TALLOC_FREE(debug_condition); + debug_condition = NULL; + return CMD_OK; + } + + if (!((argc == 1) && + ((argv[0][0] == '"') || (argv[0][0] == '\'')))) { + p = buffer; + *p = '\0'; + for (i = 0; i < argc; i++) { + size_t len; + + len = strlcpy(p, argv[i], buffer + sizeof(buffer) - p); + p += len; + *(p++) = ' '; + *p = '\0'; + } + + } else { + /* + * Backwards compatibility. De-escape the string. + */ + char quote; + char *q; + + p = argv[0]; + q = buffer; + + quote = *(p++); + + while (true) { + if (!*p) { + error = "Unexpected end of string"; + slen = -strlen(argv[0]); + p = argv[0]; + + goto parse_error; + } + + if (*p == quote) { + if (p[1]) { + error = "Unexpected text after end of string"; + slen = -(p - argv[0]); + p = argv[0]; + + goto parse_error; + } + *q = '\0'; + break; + } + + if (*p == '\\') { + *(q++) = p[1]; + p += 2; + continue; + } + + *(q++) = *(p++); + } + } + + p = buffer; + + slen = fr_condition_tokenize(NULL, NULL, p, &new_condition, &error, FR_COND_ONE_PASS); + if (slen <= 0) { + char *spaces, *text; + + parse_error: + fr_canonicalize_error(NULL, &spaces, &text, slen, p); + + ERROR("Parse error in condition"); + ERROR("%s", p); + ERROR("%s^ %s", spaces, error); + + cprintf_error(listener, "Parse error in condition \"%s\": %s\n", p, error); + + talloc_free(spaces); + talloc_free(text); + return CMD_FAIL; + } + + (void) modcall_pass2_condition(new_condition); + + /* + * Delete old condition. + * + * This is thread-safe because the condition is evaluated + * in the main server thread, along with this code. + */ + TALLOC_FREE(debug_condition); + debug_condition = new_condition; + + return CMD_OK; +} + +static int command_show_debug_condition(rad_listen_t *listener, + UNUSED int argc, UNUSED char *argv[]) +{ + char buffer[1024]; + + if (!debug_condition) { + cprintf(listener, "\n"); + return CMD_OK; + } + + fr_cond_sprint(buffer, sizeof(buffer), debug_condition); + + cprintf(listener, "%s\n", buffer); + return CMD_OK; +} + + +static int command_show_debug_file(rad_listen_t *listener, + UNUSED int argc, UNUSED char *argv[]) +{ + if (!default_log.debug_file) return CMD_FAIL; + + cprintf(listener, "%s\n", default_log.debug_file); + return CMD_OK; +} + + +static int command_show_debug_level(rad_listen_t *listener, + UNUSED int argc, UNUSED char *argv[]) +{ + cprintf(listener, "%d\n", rad_debug_lvl); + return CMD_OK; +} + + +static RADCLIENT *get_client(rad_listen_t *listener, int argc, char *argv[]) +{ + RADCLIENT *client; + fr_ipaddr_t ipaddr; + int myarg; + int proto = IPPROTO_UDP; + RADCLIENT_LIST *list = NULL; + + if (argc < 1) { + cprintf_error(listener, "Must specify <ipaddr>\n"); + return NULL; + } + + /* + * First arg is IP address. + */ + if (ip_hton(&ipaddr, AF_UNSPEC, argv[0], false) < 0) { + cprintf_error(listener, "Failed parsing IP address; %s\n", + fr_strerror()); + return NULL; + } + myarg = 1; + + while (myarg < argc) { + if (strcmp(argv[myarg], "udp") == 0) { + proto = IPPROTO_UDP; + myarg++; + continue; + } + +#ifdef WITH_TCP + if (strcmp(argv[myarg], "tcp") == 0) { + proto = IPPROTO_TCP; + myarg++; + continue; + } +#endif + + if (strcmp(argv[myarg], "listen") == 0) { + uint16_t server_port; + fr_ipaddr_t server_ipaddr; + + if ((argc - myarg) < 2) { + cprintf_error(listener, "Must specify listen <ipaddr> <port>\n"); + return NULL; + } + + if (ip_hton(&server_ipaddr, ipaddr.af, argv[myarg + 1], false) < 0) { + cprintf_error(listener, "Failed parsing IP address; %s\n", + fr_strerror()); + return NULL; + } + + server_port = atoi(argv[myarg + 2]); + + list = listener_find_client_list(&server_ipaddr, server_port, proto); + if (!list) { + cprintf_error(listener, "No such listener %s %s\n", argv[myarg + 1], argv[myarg + 2]); + return NULL; + } + myarg += 3; + continue; + } + + cprintf_error(listener, "Unknown argument %s.\n", argv[myarg]); + return NULL; + } + + client = client_find(list, &ipaddr, proto); + if (!client) { + cprintf_error(listener, "No such client\n"); + return NULL; + } + + return client; +} + +#ifdef WITH_PROXY +static home_server_t *get_home_server(rad_listen_t *listener, int argc, + char *argv[], int *last) +{ + int myarg = 2; + home_server_t *home; + uint16_t port; + int proto = IPPROTO_UDP; + fr_ipaddr_t ipaddr, src_ipaddr; + + if (argc < 2) { + cprintf_error(listener, "Must specify <ipaddr> <port> [udp|tcp] OR <name> <type>\n"); + return NULL; + } + + if (isdigit((uint8_t) *argv[1])) { + if (ip_hton(&ipaddr, AF_UNSPEC, argv[0], false) < 0) { + cprintf_error(listener, "Failed parsing IP address; %s\n", + fr_strerror()); + return NULL; + } + + memset(&src_ipaddr, 0, sizeof(src_ipaddr)); + src_ipaddr.af = ipaddr.af; + + port = atoi(argv[1]); + + while (myarg < argc) { + if (strcmp(argv[myarg], "udp") == 0) { + proto = IPPROTO_UDP; + myarg++; + continue; + } + +#ifdef WITH_TCP + if (strcmp(argv[myarg], "tcp") == 0) { + proto = IPPROTO_TCP; + myarg++; + continue; + } +#endif + + /* + * Allow the caller to specify src, too. + */ + if (strcmp(argv[myarg], "src") == 0) { + if ((myarg + 2) < argc) { + cprintf_error(listener, "You must specify an address after 'src' \n"); + return NULL; + } + + if (ip_hton(&src_ipaddr, ipaddr.af, argv[myarg + 1], false) < 0) { + cprintf_error(listener, "Failed parsing IP address; %s\n", + fr_strerror()); + return NULL; + } + + myarg += 2; + continue; + } + + /* + * Unknown argument. Leave it for the caller. + */ + break; + } + + home = home_server_find_bysrc(&ipaddr, port, proto, &src_ipaddr); + } else { + int type; + + static const FR_NAME_NUMBER home_server_types[] = { + { "auth", HOME_TYPE_AUTH }, + { "acct", HOME_TYPE_ACCT }, + { "auth+acct", HOME_TYPE_AUTH_ACCT }, + { "coa", HOME_TYPE_COA }, +#ifdef WITH_COA_TUNNEL + { "auth+coa", HOME_TYPE_AUTH_COA }, + { "auth+acct+coa", HOME_TYPE_AUTH_ACCT_COA }, +#endif + { NULL, 0 } + }; + + type = fr_str2int(home_server_types, argv[1], HOME_TYPE_INVALID); + if (type == HOME_TYPE_INVALID) { + cprintf_error(listener, "Invalid home server type '%s'\n", argv[1]); + return NULL; + } + + home = home_server_byname(argv[0], type); + } + + if (!home) { + cprintf_error(listener, "No such home server - %s %s\n", argv[0], argv[1]); + return NULL; + } + + if (last) *last = myarg; + + return home; +} + +static int command_set_home_server_state(rad_listen_t *listener, int argc, char *argv[]) +{ + int last; + home_server_t *home; + + if (argc < 3) { + cprintf_error(listener, "Must specify <ipaddr> <port> [udp|tcp] <state>\n"); + return CMD_FAIL; + } + + home = get_home_server(listener, argc, argv, &last); + if (!home) { + return CMD_FAIL; + } + + if (strcmp(argv[last], "alive") == 0) { + revive_home_server(home); + + } else if (strcmp(argv[last], "dead") == 0) { + struct timeval now; + + gettimeofday(&now, NULL); /* we do this WAY too often */ + mark_home_server_dead(home, &now, false); + + } else if (strcmp(argv[last], "down") == 0) { + struct timeval now; + + gettimeofday(&now, NULL); /* we do this WAY too often */ + mark_home_server_dead(home, &now, true); + + } else { + cprintf_error(listener, "Unknown state \"%s\"\n", argv[last]); + return CMD_FAIL; + } + + return CMD_OK; +} + +static int command_show_home_server_state(rad_listen_t *listener, int argc, char *argv[]) +{ + home_server_t *home; + + home = get_home_server(listener, argc, argv, NULL); + if (!home) return CMD_FAIL; + + switch (home->state) { + case HOME_STATE_ALIVE: + cprintf(listener, "alive\n"); + break; + + case HOME_STATE_IS_DEAD: + cprintf(listener, "dead\n"); + break; + + case HOME_STATE_ZOMBIE: + cprintf(listener, "zombie\n"); + break; + + case HOME_STATE_ADMIN_DOWN: + cprintf(listener, "down\n"); + break; + + case HOME_STATE_UNKNOWN: + cprintf(listener, "unknown\n"); + break; + + default: + cprintf(listener, "invalid\n"); + break; + } + + return CMD_OK; +} +#endif + +/* + * For encode/decode stuff + */ +static int null_socket_dencode(UNUSED rad_listen_t *listener, UNUSED REQUEST *request) +{ + return 0; +} + +static int null_socket_send(UNUSED rad_listen_t *listener, REQUEST *request) +{ + vp_cursor_t cursor; + char *output_file; + FILE *fp; + + output_file = request_data_reference(request, (void *)null_socket_send, 0); + if (!output_file) { + ERROR("No output file for injected packet %d", request->number); + return 0; + } + + fp = fopen(output_file, "w"); + if (!fp) { + ERROR("Failed to send injected file to %s: %s", output_file, fr_syserror(errno)); + return 0; + } + + if (request->reply->code != 0) { + char const *what = "reply"; + VALUE_PAIR *vp; + char buffer[1024]; + + if (request->reply->code < FR_MAX_PACKET_CODE) { + what = fr_packet_codes[request->reply->code]; + } + + fprintf(fp, "%s\n", what); + + if (rad_debug_lvl) { + RDEBUG("Injected %s packet to host %s port 0 code=%d, id=%d", what, + inet_ntop(request->reply->src_ipaddr.af, + &request->reply->src_ipaddr.ipaddr, + buffer, sizeof(buffer)), + request->reply->code, request->reply->id); + } + + RINDENT(); + for (vp = fr_cursor_init(&cursor, &request->reply->vps); + vp; + vp = fr_cursor_next(&cursor)) { + vp_prints(buffer, sizeof(buffer), vp); + fprintf(fp, "%s\n", buffer); + RDEBUG("%s", buffer); + } + REXDENT(); + } + fclose(fp); + + return 0; +} + +static rad_listen_t *get_socket(rad_listen_t *listener, int argc, + char *argv[], int *last) +{ + rad_listen_t *sock; + uint16_t port; + int proto = IPPROTO_UDP; + fr_ipaddr_t ipaddr; + + if (argc < 2) { + cprintf_error(listener, "Must specify <ipaddr> <port> [udp|tcp]\n"); + return NULL; + } + + if (ip_hton(&ipaddr, AF_UNSPEC, argv[0], false) < 0) { + cprintf_error(listener, "Failed parsing IP address; %s\n", + fr_strerror()); + return NULL; + } + + port = atoi(argv[1]); + + if (last) *last = 2; + if (argc > 2) { + if (strcmp(argv[2], "udp") == 0) { + proto = IPPROTO_UDP; + if (last) *last = 3; + } +#ifdef WITH_TCP + if (strcmp(argv[2], "tcp") == 0) { + proto = IPPROTO_TCP; + if (last) *last = 3; + } +#endif + } + + sock = listener_find_byipaddr(&ipaddr, port, proto); + if (!sock) { + cprintf_error(listener, "No such listen section\n"); + return NULL; + } + + return sock; +} + + +static int command_inject_to(rad_listen_t *listener, int argc, char *argv[]) +{ + fr_command_socket_t *sock; + listen_socket_t *data; + rad_listen_t *found; + + if (listener->recv == command_tcp_recv) { + cprintf_error(listener, "Cannot inject from command socket over TCP"); + return CMD_FAIL; + } + + found = get_socket(listener, argc, argv, NULL); + if (!found) { + return 0; + } + + sock = listener->data; + data = found->data; + sock->inject_listener = found; + sock->dst_ipaddr = data->my_ipaddr; + sock->dst_port = data->my_port; + + return CMD_OK; +} + +static int command_inject_from(rad_listen_t *listener, int argc, char *argv[]) +{ + RADCLIENT *client; + fr_command_socket_t *sock; + + if (argc < 1) { + cprintf_error(listener, "No <ipaddr> was given\n"); + return 0; + } + + if (listener->recv == command_tcp_recv) { + cprintf_error(listener, "Cannot inject from command socket over TCP"); + return CMD_FAIL; + } + + sock = listener->data; + if (!sock->inject_listener) { + cprintf_error(listener, "You must specify \"inject to\" before using \"inject from\"\n"); + return 0; + } + + sock->src_ipaddr.af = AF_UNSPEC; + if (ip_hton(&sock->src_ipaddr, AF_UNSPEC, argv[0], false) < 0) { + cprintf_error(listener, "Failed parsing IP address; %s\n", + fr_strerror()); + return 0; + } + + client = client_listener_find(sock->inject_listener, &sock->src_ipaddr, + 0); + if (!client) { + cprintf_error(listener, "No such client %s\n", argv[0]); + return 0; + } + sock->inject_client = client; + + return CMD_OK; +} + +static int command_inject_file(rad_listen_t *listener, int argc, char *argv[]) +{ + static int inject_id = 0; + int ret; + bool filedone; + fr_command_socket_t *sock; + rad_listen_t *fake; + RADIUS_PACKET *packet; + vp_cursor_t cursor; + VALUE_PAIR *vp; + FILE *fp; + RAD_REQUEST_FUNP fun = NULL; + char buffer[2048]; + + if (argc < 2) { + cprintf_error(listener, "You must specify <input-file> <output-file>\n"); + return 0; + } + + if (listener->recv == command_tcp_recv) { + cprintf_error(listener, "Cannot inject from command socket over TCP"); + return CMD_FAIL; + } + + sock = listener->data; + if (!sock->inject_listener) { + cprintf_error(listener, "You must specify \"inject to\" before using \"inject file\"\n"); + return 0; + } + + if (!sock->inject_client) { + cprintf_error(listener, "You must specify \"inject from\" before using \"inject file\"\n"); + return 0; + } + + /* + * Output files always go to the logging directory. + */ + snprintf(buffer, sizeof(buffer), "%s/%s", radlog_dir, argv[1]); + + fp = fopen(argv[0], "r"); + if (!fp ) { + cprintf_error(listener, "Failed opening %s: %s\n", + argv[0], fr_syserror(errno)); + return 0; + } + + ret = fr_pair_list_afrom_file(NULL, &vp, fp, &filedone); + fclose(fp); + if (ret < 0) { + cprintf_error(listener, "Failed reading attributes from %s: %s\n", + argv[0], fr_strerror()); + return 0; + } + + fake = talloc(NULL, rad_listen_t); + memcpy(fake, sock->inject_listener, sizeof(*fake)); + + /* + * Re-write the IO for the listener. + */ + fake->encode = null_socket_dencode; + fake->decode = null_socket_dencode; + fake->send = null_socket_send; + + packet = rad_alloc(NULL, false); + packet->src_ipaddr = sock->src_ipaddr; + packet->src_port = 0; + + packet->dst_ipaddr = sock->dst_ipaddr; + packet->dst_port = sock->dst_port; + packet->vps = vp; + packet->id = inject_id++; + + if (fake->type == RAD_LISTEN_AUTH) { + packet->code = PW_CODE_ACCESS_REQUEST; + fun = rad_authenticate; + + } else { +#ifdef WITH_ACCOUNTING + packet->code = PW_CODE_ACCOUNTING_REQUEST; + fun = rad_accounting; +#else + cprintf_error(listener, "This server was built without accounting support.\n"); + rad_free(&packet); + free(fake); + return 0; +#endif + } + + if (rad_debug_lvl) { + DEBUG("Injecting %s packet from host %s port 0 code=%d, id=%d", + fr_packet_codes[packet->code], + inet_ntop(packet->src_ipaddr.af, + &packet->src_ipaddr.ipaddr, + buffer, sizeof(buffer)), + packet->code, packet->id); + + for (vp = fr_cursor_init(&cursor, &packet->vps); + vp; + vp = fr_cursor_next(&cursor)) { + vp_prints(buffer, sizeof(buffer), vp); + DEBUG("\t%s", buffer); + } + + WARN("INJECTION IS LEAKING MEMORY!"); + } + + if (!request_receive(NULL, fake, packet, sock->inject_client, fun)) { + cprintf_error(listener, "Failed to inject request. See log file for details\n"); + rad_free(&packet); + free(fake); + return 0; + } + +#if 0 + /* + * Remember what the output file is, and remember to + * delete the fake listener when done. + */ + request_data_add(request, null_socket_send, 0, talloc_typed_strdup(NULL, buffer), true); + request_data_add(request, null_socket_send, 1, fake, true); + +#endif + + return CMD_OK; +} + + +static fr_command_table_t command_table_inject[] = { + { "to", FR_WRITE, + "inject to <ipaddr> <port> - Inject packets to the destination IP and port.", + command_inject_to, NULL }, + + { "from", FR_WRITE, + "inject from <ipaddr> - Inject packets as if they came from <ipaddr>", + command_inject_from, NULL }, + + { "file", FR_WRITE, + "inject file <input-file> <output-file> - Inject packet from <input-file>, with results sent to <output-file>", + command_inject_file, NULL }, + + { NULL, 0, NULL, NULL, NULL } +}; + +static fr_command_table_t command_table_debug[] = { + { "condition", FR_WRITE, + "debug condition [condition] - Enable debugging for requests matching [condition]", + command_debug_condition, NULL }, + + { "level", FR_WRITE, + "debug level <number> - Set debug level to <number>. Higher is more debugging.", + command_debug_level, NULL }, + + { "file", FR_WRITE, + "debug file [filename] - Send all debugging output to [filename]", + command_debug_file, NULL }, + + { NULL, 0, NULL, NULL, NULL } +}; + +static fr_command_table_t command_table_show_debug[] = { + { "condition", FR_READ, + "show debug condition - Shows current debugging condition.", + command_show_debug_condition, NULL }, + + { "level", FR_READ, + "show debug level - Shows current debugging level.", + command_show_debug_level, NULL }, + + { "file", FR_READ, + "show debug file - Shows current debugging file.", + command_show_debug_file, NULL }, + + { NULL, 0, NULL, NULL, NULL } +}; + +static fr_command_table_t command_table_show_module[] = { + { "config", FR_READ, + "show module config <module> - show configuration for given module", + command_show_module_config, NULL }, + { "flags", FR_READ, + "show module flags <module> - show other module properties", + command_show_module_flags, NULL }, + { "list", FR_READ, + "show module list - shows list of loaded modules", + command_show_modules, NULL }, + { "methods", FR_READ, + "show module methods <module> - show sections where <module> may be used", + command_show_module_methods, NULL }, + { "status", FR_READ, + "show module status <module> - show the module status", + command_show_module_status, NULL }, + + { NULL, 0, NULL, NULL, NULL } +}; + +static fr_command_table_t command_table_show_client[] = { + { "config", FR_READ, + "show client config <ipaddr> " +#ifdef WITH_TCP + "[udp|tcp] " +#endif + "- show configuration for given client", + command_show_client_config, NULL }, + { "list", FR_READ, + "show client list [verbose] - shows list of global clients", + command_show_clients, NULL }, + + { NULL, 0, NULL, NULL, NULL } +}; + +#ifdef WITH_PROXY +static fr_command_table_t command_table_show_home[] = { + { "list", FR_READ, + "show home_server list [all] - shows list of home servers", + command_show_home_servers, NULL }, + { "state", FR_READ, + "show home_server state <ipaddr> <port> [udp|tcp] [src <ipaddr>] - shows state of given home server", + command_show_home_server_state, NULL }, + + { NULL, 0, NULL, NULL, NULL } +}; +#endif + + +static fr_command_table_t command_table_show[] = { + { "client", FR_READ, + "show client <command> - do sub-command of client", + NULL, command_table_show_client }, + { "config", FR_READ, + "show config <path> - shows the value of configuration option <path>", + command_show_config, NULL }, + { "debug", FR_READ, + "show debug <command> - show debug properties", + NULL, command_table_show_debug }, +#ifdef WITH_PROXY + { "home_server", FR_READ, + "show home_server <command> - do sub-command of home_server", + NULL, command_table_show_home }, +#endif + { "module", FR_READ, + "show module <command> - do sub-command of module", + NULL, command_table_show_module }, + { "uptime", FR_READ, + "show uptime - shows time at which server started", + command_uptime, NULL }, + { "version", FR_READ, + "show version - Prints version of the running server", + command_show_version, NULL }, + { NULL, 0, NULL, NULL, NULL } +}; + + +static int command_set_module_config(rad_listen_t *listener, int argc, char *argv[]) +{ + int i, rcode; + CONF_PAIR *cp; + CONF_SECTION *cs; + module_instance_t *mi; + CONF_PARSER const *variables; + void *data; + + if (argc < 3) { + cprintf_error(listener, "No module name or variable was given\n"); + return 0; + } + + cs = cf_section_find("modules"); + if (!cs) return 0; + + mi = module_find(cs, argv[0]); + if (!mi) { + cprintf_error(listener, "No such module \"%s\"\n", argv[0]); + return 0; + } + + if ((mi->entry->module->type & RLM_TYPE_HUP_SAFE) == 0) { + cprintf_error(listener, "Cannot change configuration of module as it is cannot be HUP'd.\n"); + return 0; + } + + variables = cf_section_parse_table(mi->cs); + if (!variables) { + cprintf_error(listener, "Cannot find configuration for module\n"); + return 0; + } + + rcode = -1; + for (i = 0; variables[i].name != NULL; i++) { + /* + * FIXME: Recurse into sub-types somehow... + */ + if (variables[i].type == PW_TYPE_SUBSECTION) continue; + + if (strcmp(variables[i].name, argv[1]) == 0) { + rcode = i; + break; + } + } + + if (rcode < 0) { + cprintf_error(listener, "No such variable \"%s\"\n", argv[1]); + return 0; + } + + i = rcode; /* just to be safe */ + + /* + * It's not part of the dynamic configuration. The module + * needs to re-parse && validate things. + */ + if (variables[i].data) { + cprintf_error(listener, "Variable cannot be dynamically updated\n"); + return 0; + } + + data = ((char *) mi->insthandle) + variables[i].offset; + + cp = cf_pair_find(mi->cs, argv[1]); + if (!cp) return 0; + + /* + * Replace the OLD value in the configuration file with + * the NEW value. + * + * FIXME: Parse argv[2] depending on it's data type! + * If it's a string, look for leading single/double quotes, + * end then call tokenize functions??? + */ + cf_pair_replace(mi->cs, cp, argv[2]); + + rcode = cf_item_parse(mi->cs, argv[1], variables[i].type, data, argv[2]); + if (rcode < 0) { + cprintf_error(listener, "Failed to parse value\n"); + return 0; + } + + return CMD_OK; +} + +static int command_set_module_status(rad_listen_t *listener, int argc, char *argv[]) +{ + CONF_SECTION *cs; + module_instance_t *mi; + + if (argc < 2) { + cprintf_error(listener, "No module name or status was given\n"); + return 0; + } + + cs = cf_section_find("modules"); + if (!cs) return 0; + + mi = module_find(cs, argv[0]); + if (!mi) { + cprintf_error(listener, "No such module \"%s\"\n", argv[0]); + return 0; + } + + + if (strcmp(argv[1], "alive") == 0) { + mi->force = false; + + } else if (strcmp(argv[1], "dead") == 0) { + mi->code = RLM_MODULE_FAIL; + mi->force = true; + + } else { + int rcode; + + rcode = fr_str2int(mod_rcode_table, argv[1], -1); + if (rcode < 0) { + cprintf_error(listener, "Unknown status \"%s\"\n", argv[1]); + return 0; + } + + mi->code = rcode; + mi->force = true; + } + + return CMD_OK; +} + +#ifdef WITH_STATS +static char const *elapsed_names[8] = { + "1us", "10us", "100us", "1ms", "10ms", "100ms", "1s", "10s" +}; + +static int command_print_stats(rad_listen_t *listener, fr_stats_t *stats, + int auth, int server) +{ + int i; + + cprintf(listener, "requests\t%" PRIu64 "\n", stats->total_requests); + cprintf(listener, "responses\t%" PRIu64 "\n", stats->total_responses); + + if (auth) { + cprintf(listener, "accepts\t\t%" PRIu64 "\n", + stats->total_access_accepts); + cprintf(listener, "rejects\t\t%" PRIu64 "\n", + stats->total_access_rejects); + cprintf(listener, "challenges\t%" PRIu64 "\n", + stats->total_access_challenges); + } + + cprintf(listener, "dup\t\t%" PRIu64 "\n", stats->total_dup_requests); + cprintf(listener, "invalid\t\t%" PRIu64 "\n", stats->total_invalid_requests); + cprintf(listener, "malformed\t%" PRIu64 "\n", stats->total_malformed_requests); + cprintf(listener, "bad_authenticator\t%" PRIu64 "\n", stats->total_bad_authenticators); + cprintf(listener, "dropped\t\t%" PRIu64 "\n", stats->total_packets_dropped); + cprintf(listener, "unknown_types\t%" PRIu64 "\n", stats->total_unknown_types); + + if (server) { + cprintf(listener, "timeouts\t%" PRIu64 "\n", stats->total_timeouts); + } else { + cprintf(listener, "conflicts\t%" PRIu64 "\n", stats->total_conflicts); + cprintf(listener, "unresponsive_child\t%" PRIu64 "\n", stats->unresponsive_child); + } + + cprintf(listener, "last_packet\t%" PRId64 "\n", (int64_t) stats->last_packet); + for (i = 0; i < 8; i++) { + cprintf(listener, "elapsed.%s\t%" PRIu64 "\n", + elapsed_names[i], stats->elapsed[i]); + } + + return CMD_OK; +} + + +#ifdef HAVE_PTHREAD_H +static int command_stats_queue(rad_listen_t *listener, UNUSED int argc, UNUSED char *argv[]) +{ + int array[RAD_LISTEN_MAX], pps[2]; + + thread_pool_queue_stats(array, pps); + + cprintf(listener, "queue_len_internal\t%d\n", array[0]); + cprintf(listener, "queue_len_proxy\t\t%d\n", array[1]); + cprintf(listener, "queue_len_auth\t\t%d\n", array[2]); + cprintf(listener, "queue_len_acct\t\t%d\n", array[3]); + cprintf(listener, "queue_len_detail\t%d\n", array[4]); + + cprintf(listener, "queue_pps_in\t\t%d\n", pps[0]); + cprintf(listener, "queue_pps_out\t\t%d\n", pps[1]); + + return CMD_OK; +} + +static int command_stats_threads(rad_listen_t *listener, UNUSED int argc, UNUSED char *argv[]) +{ + int stats[3]; + + thread_pool_thread_stats(stats); + + cprintf(listener, "threads_active\t%d\n", stats[0]); + cprintf(listener, "threads_total\t\t%d\n", stats[1]); + cprintf(listener, "threads_max\t\t%d\n", stats[2]); + + return CMD_OK; +} +#endif + +#ifndef NDEBUG +static int command_stats_memory(rad_listen_t *listener, int argc, char *argv[]) +{ + + if (!main_config.debug_memory || !main_config.memory_report) { + cprintf(listener, "No memory debugging was enabled.\n"); + return CMD_OK; + } + + if (argc == 0) goto fail; + + if (strcmp(argv[0], "total") == 0) { + cprintf(listener, "%zd\n", talloc_total_size(NULL)); + return CMD_OK; + } + + if (strcmp(argv[0], "blocks") == 0) { + cprintf(listener, "%zd\n", talloc_total_blocks(NULL)); + return CMD_OK; + } + + if (strcmp(argv[0], "full") == 0) { + cprintf(listener, "see stdout of the server for the full report.\n"); + fr_log_talloc_report(NULL); + return CMD_OK; + } + +fail: + cprintf_error(listener, "Must use 'stats memory [blocks|full|total]'\n"); + return CMD_FAIL; +} +#endif + +#ifdef WITH_DETAIL +static FR_NAME_NUMBER state_names[] = { + { "unopened", STATE_UNOPENED }, + { "unlocked", STATE_UNLOCKED }, + { "header", STATE_HEADER }, + { "reading", STATE_READING }, + { "queued", STATE_QUEUED }, + { "running", STATE_RUNNING }, + { "no-reply", STATE_NO_REPLY }, + { "replied", STATE_REPLIED }, + + { NULL, 0 } +}; + +static int command_stats_detail(rad_listen_t *listener, int argc, char *argv[]) +{ + rad_listen_t *this; + listen_detail_t *data, *needle; + struct stat buf; + + if (argc == 0) { + cprintf_error(listener, "Must specify <filename>\n"); + return 0; + } + + data = NULL; + for (this = main_config.listen; this != NULL; this = this->next) { + if (this->type != RAD_LISTEN_DETAIL) continue; + + needle = this->data; + if (!strcmp(argv[0], needle->filename)) { + data = needle; + break; + } + } + + if (!data) { + cprintf_error(listener, "No detail file listener\n"); + return 0; + } + + cprintf(listener, "state\t%s\n", + fr_int2str(state_names, data->state, "?")); + + if ((data->state == STATE_UNOPENED) || + (data->state == STATE_UNLOCKED)) { + return CMD_OK; + } + + /* + * Race conditions: file might not exist. + */ + if (stat(data->filename_work, &buf) < 0) { + cprintf(listener, "packets\t0\n"); + cprintf(listener, "tries\t0\n"); + cprintf(listener, "offset\t0\n"); + cprintf(listener, "size\t0\n"); + return CMD_OK; + } + + cprintf(listener, "packets\t%d\n", data->packets); + cprintf(listener, "tries\t%d\n", data->tries); + cprintf(listener, "offset\t%u\n", (unsigned int) data->offset); + cprintf(listener, "size\t%u\n", (unsigned int) buf.st_size); + + return CMD_OK; +} +#endif + +#ifdef WITH_PROXY +static int command_stats_home_server(rad_listen_t *listener, int argc, char *argv[]) +{ + home_server_t *home; + + if (argc == 0) { + cprintf_error(listener, "Must specify [auth|acct|coa|disconnect] OR <ipaddr> <port>\n"); + return 0; + } + + if (argc == 1) { + if (strcmp(argv[0], "auth") == 0) { + return command_print_stats(listener, + &proxy_auth_stats, 1, 1); + } + +#ifdef WITH_ACCOUNTING + if (strcmp(argv[0], "acct") == 0) { + return command_print_stats(listener, + &proxy_acct_stats, 0, 1); + } +#endif + +#ifdef WITH_ACCOUNTING + if (strcmp(argv[0], "coa") == 0) { + return command_print_stats(listener, + &proxy_coa_stats, 0, 1); + } +#endif + +#ifdef WITH_ACCOUNTING + if (strcmp(argv[0], "disconnect") == 0) { + return command_print_stats(listener, + &proxy_dsc_stats, 0, 1); + } +#endif + + cprintf_error(listener, "Should specify [auth|acct|coa|disconnect]\n"); + return 0; + } + + home = get_home_server(listener, argc, argv, NULL); + if (!home) return 0; + + command_print_stats(listener, &home->stats, + (home->type == HOME_TYPE_AUTH), 1); + cprintf(listener, "outstanding\t%d\n", home->currently_outstanding); + return CMD_OK; +} +#endif + +static int command_stats_client(rad_listen_t *listener, int argc, char *argv[]) +{ + bool auth = true; + fr_stats_t *stats; + RADCLIENT *client, fake; + + if (argc < 1) { + cprintf_error(listener, "Must specify [auth/acct]\n"); + return 0; + } + + if (argc == 1) { + /* + * Global statistics. + */ + fake.auth = radius_auth_stats; +#ifdef WITH_ACCOUNTING + fake.acct = radius_acct_stats; +#endif +#ifdef WITH_COA + fake.coa = radius_coa_stats; + fake.dsc = radius_dsc_stats; +#endif + client = &fake; + + } else { + /* + * Per-client statistics. + */ + client = get_client(listener, argc - 1, argv + 1); + if (!client) return 0; + } + + if (strcmp(argv[0], "auth") == 0) { + auth = true; + stats = &client->auth; + + } else if (strcmp(argv[0], "acct") == 0) { +#ifdef WITH_ACCOUNTING + auth = false; + stats = &client->acct; +#else + cprintf_error(listener, "This server was built without accounting support.\n"); + return 0; +#endif + + } else if (strcmp(argv[0], "coa") == 0) { +#ifdef WITH_COA + auth = false; + stats = &client->coa; +#else + cprintf_error(listener, "This server was built without CoA support.\n"); + return 0; +#endif + + } else if (strcmp(argv[0], "disconnect") == 0) { +#ifdef WITH_COA + auth = false; + stats = &client->dsc; +#else + cprintf_error(listener, "This server was built without CoA support.\n"); + return 0; +#endif + + } else { + cprintf_error(listener, "Unknown statistics type\n"); + return 0; + } + + /* + * Global results for all client. + */ + if (argc == 1) { +#ifdef WITH_ACCOUNTING + if (!auth) { + return command_print_stats(listener, + &radius_acct_stats, auth, 0); + } +#endif + return command_print_stats(listener, &radius_auth_stats, auth, 0); + } + + return command_print_stats(listener, stats, auth, 0); +} + + +static int command_stats_socket(rad_listen_t *listener, int argc, char *argv[]) +{ + bool auth = true; + rad_listen_t *sock; + + sock = get_socket(listener, argc, argv, NULL); + if (!sock) return 0; + + if (sock->type != RAD_LISTEN_AUTH) auth = false; + + return command_print_stats(listener, &sock->stats, auth, 0); +} + +static int command_stats_pool(rad_listen_t *listener, UNUSED int argc, UNUSED char *argv[]) +{ + CONF_SECTION *cs; + module_instance_t *mi; + fr_connection_pool_stats_t const *stats; + + if (argc < 1) { + cprintf_error(listener, "Must specify <name>\n"); + return CMD_FAIL; + } + + cs = cf_section_find("modules"); + if (!cs) return CMD_FAIL; + + mi = module_find(cs, argv[0]); + if (!mi) { + cprintf_error(listener, "No such module \"%s\"\n", argv[0]); + return CMD_FAIL; + } + + stats = fr_connection_pool_stats(mi->cs); + if (!stats) { + cprintf_error(listener, "Module %s has no pool statistics", argv[0]); + return CMD_FAIL; + } + + cprintf(listener, "last_checked\t\t%zu\n", stats->last_checked); + cprintf(listener, "last_opened\t\t%zu\n", stats->last_opened); + cprintf(listener, "last_closed\t\t%zu\n", stats->last_closed); + cprintf(listener, "last_failed\t\t%zu\n", stats->last_failed); + cprintf(listener, "last_throttled\t\t%zu\n", stats->last_throttled); + cprintf(listener, "total_opened\t\t%" PRIu64 "\n", stats->opened); + cprintf(listener, "total_closed\t\t%" PRIu64 "\n", stats->closed); + cprintf(listener, "total_failed\t\t%" PRIu64 "\n", stats->failed); + cprintf(listener, "num_open\t\t%u\n", stats->num); + cprintf(listener, "num_in_use\t\t%u\n", stats->active); + + return CMD_OK; +} +#endif /* WITH_STATS */ + + +#ifdef WITH_DYNAMIC_CLIENTS +static int command_add_client_file(rad_listen_t *listener, int argc, char *argv[]) +{ + RADCLIENT *c; + + if (argc < 1) { + cprintf_error(listener, "<file> is required\n"); + return 0; + } + + /* + * Read the file and generate the client. + */ + c = client_read(argv[0], false, false); + if (!c) { + cprintf_error(listener, "Unknown error reading client file.\n"); + return 0; + } + + if (!client_add(NULL, c)) { + cprintf_error(listener, "Unknown error inserting new client.\n"); + client_free(c); + return 0; + } + + return CMD_OK; +} + + +static int command_del_client(rad_listen_t *listener, int argc, char *argv[]) +{ + RADCLIENT *client; + + client = get_client(listener, argc, argv); + if (!client) return 0; + + if (!client->dynamic) { + cprintf_error(listener, "Client %s was not dynamically defined.\n", argv[0]); + return 0; + } + + /* + * DON'T delete it. Instead, mark it as "dead now". The + * next time we receive a packet for the client, it will + * be deleted. + * + * If we don't receive a packet from it, the client + * structure will stick around for a while. Oh well... + */ + client->lifetime = 1; + + return CMD_OK; +} + + +static int command_del_home_server(rad_listen_t *listener, int argc, char *argv[]) +{ + if (argc < 2) { + cprintf_error(listener, "<name> and <type> are required\n"); + return 0; + } + + if (home_server_delete(argv[0], argv[1]) < 0) { + cprintf_error(listener, "Failed deleted home_server %s - %s\n", argv[1], fr_strerror()); + return 0; + } + + return CMD_OK; +} + +static int command_add_home_server_file(rad_listen_t *listener, int argc, char *argv[]) +{ + if (argc < 1) { + cprintf_error(listener, "<file> is required\n"); + return 0; + } + + if (home_server_afrom_file(argv[0]) < 0) { + cprintf_error(listener, "Unable to add home server - %s\n", fr_strerror()); + return 0; + } + + return CMD_OK; +} + +static fr_command_table_t command_table_del_client[] = { + { "ipaddr", FR_WRITE, + "del client ipaddr <ipaddr> [udp|tcp] [listen <ipaddr> <port>] - Delete a dynamically created client", + command_del_client, NULL }, + + { NULL, 0, NULL, NULL, NULL } +}; + +static fr_command_table_t command_table_del_home_server[] = { + { "file", FR_WRITE, + "del home_server file <name> [auth|acct|coa] - Delete a dynamically created home_server", + command_del_home_server, NULL }, + + { NULL, 0, NULL, NULL, NULL } +}; + +static fr_command_table_t command_table_del[] = { + { "client", FR_WRITE, + "del client <command> - Delete client configuration commands", + NULL, command_table_del_client }, + + { "home_server", FR_WRITE, + "del home_server <command> - Delete home_server configuration commands", + NULL, command_table_del_home_server }, + + { NULL, 0, NULL, NULL, NULL } +}; + +static fr_command_table_t command_table_add_client[] = { + { "file", FR_WRITE, + "add client file <filename> - Add new client definition from <filename>", + command_add_client_file, NULL }, + + { NULL, 0, NULL, NULL, NULL } +}; + +static fr_command_table_t command_table_add_home_server[] = { + { "file", FR_WRITE, + "add home_server file <filename> - Add new home serverdefinition from <filename>", + command_add_home_server_file, NULL }, + + { NULL, 0, NULL, NULL, NULL } +}; + +static fr_command_table_t command_table_add[] = { + { "client", FR_WRITE, + "add client <command> - Add client configuration commands", + NULL, command_table_add_client }, + + { "home_server", FR_WRITE, + "add home_server <command> - Add home server configuration commands", + NULL, command_table_add_home_server }, + + { NULL, 0, NULL, NULL, NULL } +}; +#endif + +#ifdef WITH_PROXY +static fr_command_table_t command_table_set_home[] = { + { "state", FR_WRITE, + "set home_server state <ipaddr> <port> [udp|tcp] [src <ipaddr>] [alive|dead|down] - set state for given home server", + command_set_home_server_state, NULL }, + + { NULL, 0, NULL, NULL, NULL } +}; +#endif + +static fr_command_table_t command_table_set_module[] = { + { "config", FR_WRITE, + "set module config <module> variable value - set configuration for <module>", + command_set_module_config, NULL }, + + { "status", FR_WRITE, + "set module status <module> [alive|...] - set the module status to be alive (operating normally), or force a particular code (ok,fail, etc.)", + command_set_module_status, NULL }, + + { NULL, 0, NULL, NULL, NULL } +}; + + +static fr_command_table_t command_table_set[] = { + { "module", FR_WRITE, + "set module <command> - set module commands", + NULL, command_table_set_module }, +#ifdef WITH_PROXY + { "home_server", FR_WRITE, + "set home_server <command> - set home server commands", + NULL, command_table_set_home }, +#endif + + { NULL, 0, NULL, NULL, NULL } +}; + + +#ifdef WITH_STATS +static fr_command_table_t command_table_stats[] = { + { "client", FR_READ, + "stats client [auth/acct/coa] <ipaddr> [udp|tcp] [listen <ipaddr> <port>] " + "- show statistics for given client, or for all clients (auth or acct)", + command_stats_client, NULL }, + +#ifdef WITH_DETAIL + { "detail", FR_READ, + "stats detail <filename> - show statistics for the given detail file", + command_stats_detail, NULL }, +#endif + +#ifdef WITH_PROXY + { "home_server", FR_READ, + "stats home_server [<ipaddr>|auth|acct|coa|disconnect] <port> [udp|tcp] [src <ipaddr>] - show statistics for given home server (ipaddr and port), or for all home servers (auth or acct)", + command_stats_home_server, NULL }, +#endif + + { "pool", FR_READ, + "stats pool <name> " + "- show pool statistics for given module", + command_stats_pool, NULL }, + +#ifdef HAVE_PTHREAD_H + { "queue", FR_READ, + "stats queue - show statistics for packet queues", + command_stats_queue, NULL }, + { "threads", FR_READ, + "stats threads - show statistics for the worker threads", + command_stats_threads, NULL }, +#endif + + { "socket", FR_READ, + "stats socket <ipaddr> <port> [udp|tcp] " + "- show statistics for given socket", + command_stats_socket, NULL }, + +#ifndef NDEBUG + { "memory", FR_READ, + "stats memory [blocks|full|total] - show statistics on used memory", + command_stats_memory, NULL }, +#endif + + { NULL, 0, NULL, NULL, NULL } +}; +#endif + +static fr_command_table_t command_table[] = { +#ifdef WITH_DYNAMIC_CLIENTS + { "add", FR_WRITE, NULL, NULL, command_table_add }, +#endif + { "debug", FR_WRITE, + "debug <command> - debugging commands", + NULL, command_table_debug }, +#ifdef WITH_DYNAMIC_CLIENTS + { "del", FR_WRITE, NULL, NULL, command_table_del }, +#endif + { "hup", FR_WRITE, + "hup [module] - sends a HUP signal to the server, or optionally to one module", + command_hup, NULL }, + { "inject", FR_WRITE, + "inject <command> - commands to inject packets into a running server", + NULL, command_table_inject }, + { "reconnect", FR_READ, + "reconnect - reconnect to a running server", + NULL, NULL }, /* just here for "help" */ + { "terminate", FR_WRITE, + "terminate - terminates the server, and cause it to exit", + command_terminate, NULL }, + { "set", FR_WRITE, NULL, NULL, command_table_set }, + { "show", FR_READ, NULL, NULL, command_table_show }, +#ifdef WITH_STATS + { "stats", FR_READ, NULL, NULL, command_table_stats }, +#endif + + { NULL, 0, NULL, NULL, NULL } +}; + + +static void command_socket_free(rad_listen_t *this) +{ + fr_command_socket_t *cmd = this->data; + + /* + * If it's a TCP socket, don't do anything. + */ + if (cmd->magic != COMMAND_SOCKET_MAGIC) { + return; + } + + if (!cmd->copy) return; + unlink(cmd->copy); +} + + +/* + * Parse the unix domain sockets. + * + * FIXME: TCP + SSL, after RadSec is in. + */ +static int command_socket_parse_unix(CONF_SECTION *cs, rad_listen_t *this) +{ + fr_command_socket_t *sock; + + if (check_config) return 0; + + sock = this->data; + + if (cf_section_parse(cs, sock, command_config) < 0) return -1; + + /* + * Can't get uid or gid of connecting user, so can't do + * peercred authentication. + */ +#ifndef HAVE_GETPEEREID + if (sock->peercred && (sock->uid_name || sock->gid_name)) { + ERROR("System does not support uid or gid authentication for sockets"); + return -1; + } +#endif + + sock->magic = COMMAND_SOCKET_MAGIC; + sock->copy = NULL; + if (sock->path) sock->copy = talloc_typed_strdup(sock, sock->path); + + if (sock->uid_name) { + struct passwd *pwd; + + if (rad_getpwnam(cs, &pwd, sock->uid_name) < 0) { + ERROR("Failed getting uid for %s: %s", sock->uid_name, fr_strerror()); + return -1; + } + sock->uid = pwd->pw_uid; + talloc_free(pwd); + } else { + sock->uid = -1; + } + + if (sock->gid_name) { + if (rad_getgid(cs, &sock->gid, sock->gid_name) < 0) { + ERROR("Failed getting gid for %s: %s", sock->gid_name, fr_strerror()); + return -1; + } + } else { + sock->gid = -1; + } + + if (!sock->mode_name) { + sock->co.mode = FR_READ; + } else { + sock->co.mode = fr_str2int(mode_names, sock->mode_name, 0); + if (!sock->co.mode) { + ERROR("Invalid mode name \"%s\"", + sock->mode_name); + return -1; + } + } + + if (sock->peercred) { + this->fd = fr_server_domain_socket_peercred(sock->path, sock->uid, sock->gid); + } else { + uid_t uid = sock->uid; + gid_t gid = sock->gid; + + if (uid == ((uid_t)-1)) uid = 0; + if (gid == ((gid_t)-1)) gid = 0; + + this->fd = fr_server_domain_socket_perm(sock->path, uid, gid); + } + + if (this->fd < 0) { + ERROR("Failed creating control socket \"%s\": %s", sock->path, fr_strerror()); + if (sock->copy) talloc_free(sock->copy); + sock->copy = NULL; + return -1; + } + + return 0; +} + +static int command_socket_parse(CONF_SECTION *cs, rad_listen_t *this) +{ + int rcode; + CONF_PAIR const *cp; + listen_socket_t *sock; + + cp = cf_pair_find(cs, "socket"); + if (cp) return command_socket_parse_unix(cs, this); + + rcode = common_socket_parse(cs, this); + if (rcode < 0) return -1; + +#ifdef WITH_TLS + if (this->tls) { + cf_log_err_cs(cs, + "TLS is not supported for control sockets"); + return -1; + } +#endif + + sock = this->data; + if (sock->proto != IPPROTO_TCP) { + cf_log_err_cs(cs, + "UDP is not supported for control sockets"); + return -1; + } + + return 0; +} + +static int command_socket_print(rad_listen_t const *this, char *buffer, size_t bufsize) +{ + fr_command_socket_t *sock = this->data; + + if (sock->magic != COMMAND_SOCKET_MAGIC) { + return common_socket_print(this, buffer, bufsize); + } + + snprintf(buffer, bufsize, "command file %s", sock->path); + return 1; +} + + +/* + * String split routine. Splits an input string IN PLACE + * into pieces, based on spaces. + */ +static int str2argvX(char *str, char **argv, int max_argc) +{ + int argc = 0; + + while (*str) { + if (argc >= max_argc) return argc; + + /* + * Chop out comments early. + */ + if (*str == '#') { + *str = '\0'; + break; + } + + while ((*str == ' ') || + (*str == '\t') || + (*str == '\r') || + (*str == '\n')) *(str++) = '\0'; + + if (!*str) return argc; + + argv[argc++] = str; + + if ((*str == '\'') || (*str == '"')) { + char quote = *str; + char *p = str + 1; + + while (true) { + if (!*p) return -1; + + if (*p == quote) { + str = p + 1; + break; + } + + /* + * Handle \" and nothing else. + */ + if (*p == '\\') { + p += 2; + continue; + } + + p++; + } + } + + while (*str && + (*str != ' ') && + (*str != '\t') && + (*str != '\r') && + (*str != '\n')) str++; + } + + return argc; +} + +static void print_help(rad_listen_t *listener, int argc, char *argv[], + fr_command_table_t *table, int recursive) +{ + int i; + + /* this should never happen, but if it does then just return gracefully */ + if (!table) return; + + for (i = 0; table[i].command != NULL; i++) { + if (argc > 0) { + if (strcmp(table[i].command, argv[0]) == 0) { + if (table[i].table) { + print_help(listener, argc - 1, argv + 1, table[i].table, recursive); + } else { + if (table[i].help) { + cprintf(listener, "%s\n", table[i].help); + } + } + return; + } + + continue; + } + + if (table[i].help) { + cprintf(listener, "%s\n", + table[i].help); + } else { + cprintf(listener, "%s <command> - do sub-command of %s\n", + table[i].command, table[i].command); + } + + if (recursive && table[i].table) { + print_help(listener, 0, NULL, table[i].table, recursive); + } + } +} + +#define MAX_ARGV (16) + +/* + * Check if an incoming request is "ok" + * + * It takes packets, not requests. It sees if the packet looks + * OK. If so, it does a number of sanity checks on it. + */ +static int command_domain_recv_co(rad_listen_t *listener, fr_cs_buffer_t *co) +{ + int i; + uint32_t status; + ssize_t r, len; + int argc; + fr_channel_type_t channel; + char *my_argv[MAX_ARGV], **argv; + fr_command_table_t *table; + uint8_t *command; + + r = fr_channel_drain(listener->fd, &channel, co->buffer, sizeof(co->buffer) - 1, &command, co->offset); + + if (r <= 0) { + do_close: + command_close_socket(listener); + return 0; + } + + /* + * We need more data. Go read it. + */ + if (channel == FR_CHANNEL_WANT_MORE) { + co->offset = r; + return 0; + } + + status = 0; + command[r] = '\0'; + DEBUG("radmin> %s", command); + + argc = str2argvX((char *) command, my_argv, MAX_ARGV); + if (argc == 0) goto do_next; /* empty strings are OK */ + + if (argc < 0) { + cprintf_error(listener, "Failed parsing command.\n"); + goto do_next; + } + + argv = my_argv; + + for (len = 0; len <= co->offset; len++) { + if (command[len] < 0x20) { + command[len] = '\0'; + break; + } + } + + /* + * Hard-code exit && quit. + */ + if ((strcmp(argv[0], "exit") == 0) || + (strcmp(argv[0], "quit") == 0)) goto do_close; + + table = command_table; + retry: + len = 0; + for (i = 0; table[i].command != NULL; i++) { + if (strcmp(table[i].command, argv[0]) == 0) { + /* + * Check permissions. + */ + if (((co->mode & FR_WRITE) == 0) && + ((table[i].mode & FR_WRITE) != 0)) { + cprintf_error(listener, "You do not have write permission. See \"mode = rw\" in the \"listen\" section for this socket.\n"); + goto do_next; + } + + if (table[i].table) { + /* + * This is the last argument, but + * there's a sub-table. Print help. + * + */ + if (argc == 1) { + table = table[i].table; + goto do_help; + } + + argc--; + argv++; + table = table[i].table; + goto retry; + } + + if ((argc == 2) && (strcmp(argv[1], "?") == 0)) goto do_help; + + if (!table[i].func) { + cprintf_error(listener, "Invalid command\n"); + goto do_next; + } + + status = table[i].func(listener, argc - 1, argv + 1); + goto do_next; + } + } + + /* + * No such command + */ + if (!len) { + if ((strcmp(argv[0], "help") == 0) || + (strcmp(argv[0], "?") == 0)) { + int recursive; + + do_help: + if ((argc > 1) && (strcmp(argv[1], "-r") == 0)) { + recursive = true; + argc--; + argv++; + } else { + recursive = false; + } + + print_help(listener, argc - 1, argv + 1, table, recursive); + goto do_next; + } + + cprintf_error(listener, "Unknown command \"%s\"\n", + argv[0]); + } + + do_next: + r = fr_channel_write(listener->fd, FR_CHANNEL_CMD_STATUS, &status, sizeof(status)); + if (r <= 0) goto do_close; + + return 0; +} + + +/* + * Write 32-bit magic number && version information. + */ +static int command_write_magic(int newfd, +#ifndef WITH_TCP + UNUSED +#endif + listen_socket_t *sock + ) +{ + ssize_t r; + uint32_t magic; + fr_channel_type_t channel; + char buffer[16]; + + r = fr_channel_read(newfd, &channel, buffer, 8); + if (r <= 0) { + do_close: + ERROR("Cannot talk to socket: %s", + fr_syserror(errno)); + return -1; + } + + magic = htonl(0xf7eead16); + if ((r != 8) || (channel != FR_CHANNEL_INIT_ACK) || + (memcmp(&magic, &buffer, sizeof(magic)) != 0)) { + ERROR("Incompatible versions"); + return -1; + } + + r = fr_channel_write(newfd, FR_CHANNEL_INIT_ACK, buffer, 8); + if (r <= 0) goto do_close; + +#ifdef WITH_TCP + /* + * Write an initial challenge + */ + if (sock) { + int i; + fr_cs_buffer_t *co; + + co = talloc_zero(sock, fr_cs_buffer_t); + sock->packet = (void *) co; + + for (i = 0; i < 16; i++) { + co->buffer[i] = fr_rand(); + } + + r = fr_channel_write(newfd, FR_CHANNEL_AUTH_CHALLENGE, co->buffer, 16); + if (r <= 0) goto do_close; + } +#endif + + return 0; +} + +#ifdef WITH_TCP +static int command_tcp_recv(rad_listen_t *this) +{ + ssize_t r; + listen_socket_t *sock = this->data; + fr_cs_buffer_t *co = (void *) sock->packet; + fr_channel_type_t channel; + + if (!co) { + do_close: + command_close_socket(this); + return 0; + } + + if (!co->auth) { + uint8_t expected[16]; + + r = fr_channel_read(this->fd, &channel, co->buffer, 16); + if ((r != 16) || (channel != FR_CHANNEL_AUTH_RESPONSE)) { + goto do_close; + } + + fr_hmac_md5(expected, (void const *) sock->client->secret, + strlen(sock->client->secret), + (uint8_t *) co->buffer, 16); + + if (rad_digest_cmp(expected, + (uint8_t *) co->buffer + 16, 16 != 0)) { + ERROR("radmin failed challenge: Closing socket"); + goto do_close; + } + + co->auth = true; + co->offset = 0; + } + + return command_domain_recv_co(this, co); +} + + +/* + * Should never be called. The functions should just call write(). + */ +static int command_tcp_send(UNUSED rad_listen_t *listener, UNUSED REQUEST *request) +{ + return 0; +} +#endif + +static int command_domain_recv(rad_listen_t *listener) +{ + fr_command_socket_t *sock = listener->data; + + return command_domain_recv_co(listener, &sock->co); +} + +static int command_domain_accept(rad_listen_t *listener) +{ + int newfd; + rad_listen_t *this; + socklen_t salen; + struct sockaddr_storage src; + fr_command_socket_t *sock = listener->data; + + salen = sizeof(src); + + DEBUG2(" ... new connection request on command socket"); + + newfd = accept(listener->fd, (struct sockaddr *) &src, &salen); + if (newfd < 0) { + /* + * Non-blocking sockets must handle this. + */ + if (errno == EWOULDBLOCK) { + return 0; + } + + DEBUG2(" ... failed to accept connection"); + return 0; + } + +#ifdef HAVE_GETPEEREID + /* + * Perform user authentication. + */ + if (sock->peercred && (sock->uid_name || sock->gid_name)) { + uid_t uid; + gid_t gid; + + if (getpeereid(newfd, &uid, &gid) < 0) { + ERROR("Failed getting peer credentials for %s: %s", + sock->path, fr_syserror(errno)); + close(newfd); + return 0; + } + + /* + * Only do UID checking if the caller is + * non-root. The superuser can do anything, so + * we might as well let them. + */ + if (uid != 0) do { + /* + * Allow entry if UID or GID matches. + */ + if (sock->uid_name && (sock->uid == uid)) break; + if (sock->gid_name && (sock->gid == gid)) break; + + if (sock->uid_name && (sock->uid != uid)) { + ERROR("Unauthorized connection to %s from uid %ld", + + sock->path, (long int) uid); + close(newfd); + return 0; + } + + if (sock->gid_name && (sock->gid != gid)) { + ERROR("Unauthorized connection to %s from gid %ld", + sock->path, (long int) gid); + close(newfd); + return 0; + } + + } while (0); + } +#endif + + if (command_write_magic(newfd, NULL) < 0) { + close(newfd); + return 0; + } + + /* + * Add the new listener. + */ + this = listen_alloc(listener, listener->type); + if (!this) return 0; + + /* + * Copy everything, including the pointer to the socket + * information. + */ + sock = this->data; + memcpy(this, listener, sizeof(*this)); + this->status = RAD_LISTEN_STATUS_INIT; + this->next = NULL; + this->data = sock; /* fix it back */ + + sock->magic = COMMAND_SOCKET_MAGIC; + sock->user[0] = '\0'; + sock->path = ((fr_command_socket_t *) listener->data)->path; + sock->co.offset = 0; + sock->co.mode = ((fr_command_socket_t *) listener->data)->co.mode; + + this->fd = newfd; + this->recv = command_domain_recv; + + /* + * Tell the event loop that we have a new FD + */ + radius_update_listener(this); + + return 0; +} + + +/* + * Send an authentication response packet + */ +static int command_domain_send(UNUSED rad_listen_t *listener, + UNUSED REQUEST *request) +{ + return 0; +} + + +static int command_socket_encode(UNUSED rad_listen_t *listener, + UNUSED REQUEST *request) +{ + return 0; +} + + +static int command_socket_decode(UNUSED rad_listen_t *listener, + UNUSED REQUEST *request) +{ + return 0; +} + +#endif /* WITH_COMMAND_SOCKET */ diff --git a/src/main/conffile.c b/src/main/conffile.c new file mode 100644 index 0000000..7bb206c --- /dev/null +++ b/src/main/conffile.c @@ -0,0 +1,3821 @@ +/* + * conffile.c Read the radiusd.conf file. + * + * Yep I should learn to use lex & yacc, or at least + * write a decent parser. I know how to do that, really :) + * miquels@cistron.nl + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2000,2006 The FreeRADIUS server project + * Copyright 2000 Miquel van Smoorenburg <miquels@cistron.nl> + * Copyright 2000 Alan DeKok <aland@ox.org> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/parser.h> +#include <freeradius-devel/rad_assert.h> + +#ifdef HAVE_DIRENT_H +#include <dirent.h> +#endif + +#ifdef HAVE_SYS_STAT_H +#include <sys/stat.h> +#endif + +#include <ctype.h> + +bool check_config = false; + +typedef enum conf_property { + CONF_PROPERTY_INVALID = 0, + CONF_PROPERTY_NAME, + CONF_PROPERTY_INSTANCE, +} CONF_PROPERTY; + +static const FR_NAME_NUMBER conf_property_name[] = { + { "name", CONF_PROPERTY_NAME}, + { "instance", CONF_PROPERTY_INSTANCE}, + + { NULL , -1 } +}; + +typedef enum conf_type { + CONF_ITEM_INVALID = 0, + CONF_ITEM_PAIR, + CONF_ITEM_SECTION, + CONF_ITEM_DATA +} CONF_ITEM_TYPE; + +struct conf_item { + struct conf_item *next; //!< Sibling. + struct conf_part *parent; //!< Parent. + int lineno; //!< The line number the config item began on. + char const *filename; //!< The file the config item was parsed from. + CONF_ITEM_TYPE type; //!< Whether the config item is a config_pair, conf_section or conf_data. +}; + +/** Configuration AVP similar to a VALUE_PAIR + * + */ +struct conf_pair { + CONF_ITEM item; + char const *attr; //!< Attribute name + char const *value; //!< Attribute value + FR_TOKEN op; //!< Operator e.g. =, := + FR_TOKEN lhs_type; //!< Name quoting style T_(DOUBLE|SINGLE|BACK)_QUOTE_STRING or T_BARE_WORD. + FR_TOKEN rhs_type; //!< Value Quoting style T_(DOUBLE|SINGLE|BACK)_QUOTE_STRING or T_BARE_WORD. + bool pass2; //!< do expansion in pass2. + bool parsed; //!< Was this item used during parsing? +}; + +/** Internal data that is associated with a configuration section + * + */ +struct conf_data { + CONF_ITEM item; + char const *name; + int flag; + void *data; //!< User data + void (*free)(void *); //!< Free user data function +}; + +struct conf_part { + CONF_ITEM item; + char const *name1; //!< First name token. Given ``foo bar {}`` would be ``foo``. + char const *name2; //!< Second name token. Given ``foo bar {}`` would be ``bar``. + + FR_TOKEN name2_type; //!< The type of quoting around name2. + + CONF_ITEM *children; + CONF_ITEM *tail; //!< For speed. + CONF_SECTION *template; + + rbtree_t *pair_tree; //!< and a partridge.. + rbtree_t *section_tree; //!< no jokes here. + rbtree_t *name2_tree; //!< for sections of the same name2 + rbtree_t *data_tree; + + void *base; + int depth; + + CONF_PARSER const *variables; +}; + +typedef struct cf_file_t { + char const *filename; + CONF_SECTION *cs; + struct stat buf; + bool from_dir; +} cf_file_t; + +CONF_SECTION *root_config = NULL; +bool cf_new_escape = true; + + +static int cf_data_add_internal(CONF_SECTION *cs, char const *name, void *data, + void (*data_free)(void *), int flag); + +static void *cf_data_find_internal(CONF_SECTION const *cs, char const *name, int flag); + +static char const *cf_expand_variables(char const *cf, int *lineno, + CONF_SECTION *outercs, + char *output, size_t outsize, + char const *input, bool *soft_fail); + +static int cf_file_include(CONF_SECTION *cs, char const *filename_in, bool from_dir); + + + +/* + * Isolate the scary casts in these tiny provably-safe functions + */ + +/** Cast a CONF_ITEM to a CONF_PAIR + * + */ +CONF_PAIR *cf_item_to_pair(CONF_ITEM const *ci) +{ + CONF_PAIR *out; + + if (ci == NULL) return NULL; + + rad_assert(ci->type == CONF_ITEM_PAIR); + + memcpy(&out, &ci, sizeof(out)); + return out; +} + +/** Cast a CONF_ITEM to a CONF_SECTION + * + */ +CONF_SECTION *cf_item_to_section(CONF_ITEM const *ci) +{ + CONF_SECTION *out; + + if (ci == NULL) return NULL; + + rad_assert(ci->type == CONF_ITEM_SECTION); + + memcpy(&out, &ci, sizeof(out)); + return out; +} + +/** Cast a CONF_PAIR to a CONF_ITEM + * + */ +CONF_ITEM *cf_pair_to_item(CONF_PAIR const *cp) +{ + CONF_ITEM *out; + + if (cp == NULL) return NULL; + + memcpy(&out, &cp, sizeof(out)); + return out; +} + +/** Cast a CONF_SECTION to a CONF_ITEM + * + */ +CONF_ITEM *cf_section_to_item(CONF_SECTION const *cs) +{ + CONF_ITEM *out; + + if (cs == NULL) return NULL; + + memcpy(&out, &cs, sizeof(out)); + return out; +} + +/** Cast CONF_DATA to a CONF_ITEM + * + */ +static CONF_ITEM *cf_data_to_item(CONF_DATA const *cd) +{ + CONF_ITEM *out; + + if (cd == NULL) { + return NULL; + } + + memcpy(&out, &cd, sizeof(out)); + return out; +} + +static int _cf_data_free(CONF_DATA *cd) +{ + if (cd->free) cd->free(cd->data); + + return 0; +} + +/* + * rbtree callback function + */ +static int pair_cmp(void const *a, void const *b) +{ + CONF_PAIR const *one = a; + CONF_PAIR const *two = b; + + return strcmp(one->attr, two->attr); +} + + +/* + * rbtree callback function + */ +static int section_cmp(void const *a, void const *b) +{ + CONF_SECTION const *one = a; + CONF_SECTION const *two = b; + + return strcmp(one->name1, two->name1); +} + + +/* + * rbtree callback function + */ +static int name2_cmp(void const *a, void const *b) +{ + CONF_SECTION const *one = a; + CONF_SECTION const *two = b; + + rad_assert(strcmp(one->name1, two->name1) == 0); + + if (!one->name2 && !two->name2) return 0; + if (one->name2 && !two->name2) return -1; + if (!one->name2 && two->name2) return +1; + + return strcmp(one->name2, two->name2); +} + + +/* + * rbtree callback function + */ +static int data_cmp(void const *a, void const *b) +{ + int rcode; + + CONF_DATA const *one = a; + CONF_DATA const *two = b; + + rcode = one->flag - two->flag; + if (rcode != 0) return rcode; + + return strcmp(one->name, two->name); +} + +/* + * Functions for tracking filenames. + */ +static int filename_cmp(void const *a, void const *b) +{ + cf_file_t const *one = a; + cf_file_t const *two = b; + + if (one->buf.st_dev < two->buf.st_dev) return -1; + if (one->buf.st_dev > two->buf.st_dev) return +1; + + if (one->buf.st_ino < two->buf.st_ino) return -1; + if (one->buf.st_ino > two->buf.st_ino) return +1; + + return 0; +} + +static int cf_file_open(CONF_SECTION *cs, char const *filename, bool from_dir, FILE **fp_p) +{ + cf_file_t *file; + CONF_DATA *cd; + CONF_SECTION *top; + rbtree_t *tree; + int fd; + FILE *fp; + + top = cf_top_section(cs); + cd = cf_data_find_internal(top, "filename", 0); + if (!cd) return -1; + + tree = cd->data; + + /* + * If we're including a wildcard directory, then ignore + * any files the users has already explicitly loaded in + * that directory. + */ + if (from_dir) { + cf_file_t my_file; + + my_file.cs = cs; + my_file.filename = filename; + + if (stat(filename, &my_file.buf) < 0) goto error; + + file = rbtree_finddata(tree, &my_file); + if (file && !file->from_dir) return 0; + } + + DEBUG2("including configuration file %s", filename); + + fp = fopen(filename, "r"); + if (!fp) { +error: + ERROR("Unable to open file \"%s\": %s", + filename, fr_syserror(errno)); + return -1; + } + + fd = fileno(fp); + + file = talloc(tree, cf_file_t); + if (!file) { + fclose(fp); + return -1; + } + + file->filename = filename; + file->cs = cs; + file->from_dir = from_dir; + + if (fstat(fd, &file->buf) == 0) { +#ifdef S_IWOTH + if ((file->buf.st_mode & S_IWOTH) != 0) { + ERROR("Configuration file %s is globally writable. " + "Refusing to start due to insecure configuration.", filename); + + fclose(fp); + talloc_free(file); + return -1; + } +#endif + } + + /* + * We can include the same file twice. e.g. when it + * contains common definitions, such as for SQL. + * + * Though the admin should really use templates for that. + */ + if (!rbtree_insert(tree, file)) { + talloc_free(file); + } + + *fp_p = fp; + return 1; +} + +/* + * Do some checks on the file + */ +static bool cf_file_check(CONF_SECTION *cs, char const *filename, bool check_perms) +{ + cf_file_t *file; + CONF_DATA *cd; + CONF_SECTION *top; + rbtree_t *tree; + + top = cf_top_section(cs); + cd = cf_data_find_internal(top, "filename", 0); + if (!cd) return false; + + tree = cd->data; + + file = talloc(tree, cf_file_t); + if (!file) return false; + + file->filename = filename; + file->cs = cs; + + if (stat(filename, &file->buf) < 0) { + ERROR("Unable to check file \"%s\": %s", filename, fr_syserror(errno)); + talloc_free(file); + return false; + } + + if (!check_perms) { + talloc_free(file); + return true; + } + +#ifdef S_IWOTH + if ((file->buf.st_mode & S_IWOTH) != 0) { + ERROR("Configuration file %s is globally writable. " + "Refusing to start due to insecure configuration.", filename); + talloc_free(file); + return false; + } +#endif + + /* + * It's OK to include the same file twice... + */ + if (!rbtree_insert(tree, file)) { + talloc_free(file); + } + + return true; + +} + + +typedef struct cf_file_callback_t { + int rcode; + rb_walker_t callback; + CONF_SECTION *modules; +} cf_file_callback_t; + + +/* + * Return 0 for keep going, 1 for stop. + */ +static int file_callback(void *ctx, void *data) +{ + cf_file_callback_t *cb = ctx; + cf_file_t *file = data; + struct stat buf; + + /* + * The file doesn't exist or we can no longer read it. + */ + if (stat(file->filename, &buf) < 0) { + cb->rcode = CF_FILE_ERROR; + return 1; + } + + /* + * The file changed, we'll need to re-read it. + */ + if (file->buf.st_mtime != buf.st_mtime) { + if (cb->callback(cb->modules, file->cs)) { + cb->rcode |= CF_FILE_MODULE; + DEBUG3("HUP: Changed module file %s", file->filename); + } else { + DEBUG3("HUP: Changed config file %s", file->filename); + cb->rcode |= CF_FILE_CONFIG; + } + + /* + * Presume that the file will be immediately + * re-read, so we update the mtime appropriately. + */ + file->buf.st_mtime = buf.st_mtime; + } + + return 0; +} + + +/* + * See if any of the files have changed. + */ +int cf_file_changed(CONF_SECTION *cs, rb_walker_t callback) +{ + CONF_DATA *cd; + CONF_SECTION *top; + cf_file_callback_t cb; + rbtree_t *tree; + + top = cf_top_section(cs); + cd = cf_data_find_internal(top, "filename", 0); + if (!cd) return true; + + tree = cd->data; + + cb.rcode = CF_FILE_NONE; + cb.callback = callback; + cb.modules = cf_section_sub_find(cs, "modules"); + + (void) rbtree_walk(tree, RBTREE_IN_ORDER, file_callback, &cb); + + return cb.rcode; +} + +static int _cf_section_free(CONF_SECTION *cs) +{ + /* + * Name1 and name2 are allocated contiguous with + * cs. + */ + if (cs->pair_tree) { + rbtree_free(cs->pair_tree); + cs->pair_tree = NULL; + } + if (cs->section_tree) { + rbtree_free(cs->section_tree); + cs->section_tree = NULL; + } + if (cs->name2_tree) { + rbtree_free(cs->name2_tree); + cs->name2_tree = NULL; + } + if (cs->data_tree) { + rbtree_free(cs->data_tree); + cs->data_tree = NULL; + } + + return 0; +} + +/** Allocate a CONF_PAIR + * + * @param parent CONF_SECTION to hang this CONF_PAIR off of. + * @param attr name. + * @param value of CONF_PAIR. + * @param op T_OP_EQ, T_OP_SET etc. + * @param lhs_type T_BARE_WORD, T_DOUBLE_QUOTED_STRING, T_BACK_QUOTED_STRING + * @param rhs_type T_BARE_WORD, T_DOUBLE_QUOTED_STRING, T_BACK_QUOTED_STRING + * @return NULL on error, else a new CONF_SECTION parented by parent. + */ +CONF_PAIR *cf_pair_alloc(CONF_SECTION *parent, char const *attr, char const *value, + FR_TOKEN op, FR_TOKEN lhs_type, FR_TOKEN rhs_type) +{ + CONF_PAIR *cp; + + rad_assert(fr_equality_op[op] || fr_assignment_op[op]); + if (!attr) return NULL; + + cp = talloc_zero(parent, CONF_PAIR); + if (!cp) return NULL; + + cp->item.type = CONF_ITEM_PAIR; + cp->item.parent = parent; + cp->lhs_type = lhs_type; + cp->rhs_type = rhs_type; + cp->op = op; + + cp->attr = talloc_typed_strdup(cp, attr); + if (!cp->attr) { + error: + talloc_free(cp); + return NULL; + } + + if (value) { + cp->value = talloc_typed_strdup(cp, value); + if (!cp->value) goto error; + } + + return cp; +} + +/** Duplicate a CONF_PAIR + * + * @param parent to allocate new pair in. + * @param cp to duplicate. + * @return NULL on error, else a duplicate of the input pair. + */ +CONF_PAIR *cf_pair_dup(CONF_SECTION *parent, CONF_PAIR *cp) +{ + CONF_PAIR *new; + + rad_assert(parent); + rad_assert(cp); + + new = cf_pair_alloc(parent, cp->attr, cf_pair_value(cp), + cp->op, cp->lhs_type, cp->rhs_type); + if (!new) return NULL; + + new->parsed = cp->parsed; + new->item.lineno = cp->item.lineno; + + /* + * Avoid mallocs if possible. + */ + if (!cp->item.filename || (parent->item.filename && !strcmp(parent->item.filename, cp->item.filename))) { + new->item.filename = parent->item.filename; + } else { + new->item.filename = talloc_strdup(new, cp->item.filename); + } + + return new; +} + +/** Add a configuration pair to a section + * + * @param parent section to add pair to. + * @param cp to add. + */ +void cf_pair_add(CONF_SECTION *parent, CONF_PAIR *cp) +{ + cf_item_add(parent, cf_pair_to_item(cp)); +} + +/** Allocate a CONF_SECTION + * + * @param parent CONF_SECTION to hang this CONF_SECTION off of. + * @param name1 Primary name. + * @param name2 Secondary name. + * @return NULL on error, else a new CONF_SECTION parented by parent. + */ +CONF_SECTION *cf_section_alloc(CONF_SECTION *parent, char const *name1, char const *name2) +{ + CONF_SECTION *cs; + char buffer[1024]; + + if (!name1) return NULL; + + if (name2 && parent) { + if (strchr(name2, '$')) { + name2 = cf_expand_variables(parent->item.filename, + &parent->item.lineno, + parent, + buffer, sizeof(buffer), name2, NULL); + if (!name2) { + ERROR("Failed expanding section name"); + return NULL; + } + } + } + + cs = talloc_zero(parent, CONF_SECTION); + if (!cs) return NULL; + + cs->item.type = CONF_ITEM_SECTION; + cs->item.parent = parent; + + cs->name1 = talloc_typed_strdup(cs, name1); + if (!cs->name1) { + error: + talloc_free(cs); + return NULL; + } + + if (name2) { + cs->name2 = talloc_typed_strdup(cs, name2); + if (!cs->name2) goto error; + } + + cs->pair_tree = rbtree_create(cs, pair_cmp, NULL, 0); + if (!cs->pair_tree) goto error; + + talloc_set_destructor(cs, _cf_section_free); + + /* + * Don't create a data tree, it may not be needed. + */ + + /* + * Don't create the section tree here, it may not + * be needed. + */ + + if (parent) cs->depth = parent->depth + 1; + + return cs; +} + +/** Duplicate a configuration section + * + * @note recursively duplicates any child sections. + * @note does not duplicate any data associated with a section, or its child sections. + * + * @param parent section (may be NULL). + * @param cs to duplicate. + * @param name1 of new section. + * @param name2 of new section. + * @param copy_meta Copy additional meta data for a section (like template, base, depth and variables). + * @return a duplicate of the existing section, or NULL on error. + */ +CONF_SECTION *cf_section_dup(CONF_SECTION *parent, CONF_SECTION const *cs, + char const *name1, char const *name2, bool copy_meta) +{ + CONF_SECTION *new, *subcs; + CONF_PAIR *cp; + CONF_ITEM *ci; + + new = cf_section_alloc(parent, name1, name2); + + if (copy_meta) { + new->template = cs->template; + new->base = cs->base; + new->depth = cs->depth; + new->variables = cs->variables; + } + + new->item.lineno = cs->item.lineno; + + if (!cs->item.filename || (parent && (strcmp(parent->item.filename, cs->item.filename) == 0))) { + new->item.filename = parent->item.filename; + } else { + new->item.filename = talloc_strdup(new, cs->item.filename); + } + + for (ci = cs->children; ci; ci = ci->next) { + switch (ci->type) { + case CONF_ITEM_SECTION: + subcs = cf_item_to_section(ci); + subcs = cf_section_dup(new, subcs, + cf_section_name1(subcs), cf_section_name2(subcs), + copy_meta); + if (!subcs) { + talloc_free(new); + return NULL; + } + cf_section_add(new, subcs); + break; + + case CONF_ITEM_PAIR: + cp = cf_pair_dup(new, cf_item_to_pair(ci)); + if (!cp) { + talloc_free(new); + return NULL; + } + cf_pair_add(new, cp); + break; + + case CONF_ITEM_DATA: /* Skip data */ + break; + + case CONF_ITEM_INVALID: + rad_assert(0); + } + } + + return new; +} + +void cf_section_add(CONF_SECTION *parent, CONF_SECTION *cs) +{ + cf_item_add(parent, &(cs->item)); +} + +/** Replace pair in a given section with a new pair, of the given value. + * + * @param cs to replace pair in. + * @param cp to replace. + * @param value New value to assign to cp. + * @return 0 on success, -1 on failure. + */ +int cf_pair_replace(CONF_SECTION *cs, CONF_PAIR *cp, char const *value) +{ + CONF_PAIR *newp; + CONF_ITEM *ci, *cn, **last; + + newp = cf_pair_alloc(cs, cp->attr, value, cp->op, cp->lhs_type, cp->rhs_type); + if (!newp) return -1; + + ci = &(cp->item); + cn = &(newp->item); + + /* + * Find the old one from the linked list, and replace it + * with the new one. + */ + for (last = &cs->children; (*last) != NULL; last = &(*last)->next) { + if (*last == ci) { + cn->next = (*last)->next; + *last = cn; + ci->next = NULL; + break; + } + } + + rbtree_deletebydata(cs->pair_tree, ci); + + rbtree_insert(cs->pair_tree, cn); + + return 0; +} + + +/* + * Add an item to a configuration section. + */ +void cf_item_add(CONF_SECTION *cs, CONF_ITEM *ci) +{ +#ifndef NDEBUG + CONF_ITEM *first = ci; +#endif + + rad_assert((void *)cs != (void *)ci); + + if (!cs || !ci) return; + + if (!cs->children) { + rad_assert(cs->tail == NULL); + cs->children = ci; + } else { + rad_assert(cs->tail != NULL); + cs->tail->next = ci; + } + + /* + * Update the trees (and tail) for each item added. + */ + for (/* nothing */; ci != NULL; ci = ci->next) { + rad_assert(ci->next != first); /* simple cycle detection */ + + cs->tail = ci; + + /* + * For fast lookups, pairs and sections get + * added to rbtree's. + */ + switch (ci->type) { + case CONF_ITEM_PAIR: + if (!rbtree_insert(cs->pair_tree, ci)) { + CONF_PAIR *cp = cf_item_to_pair(ci); + + if (strcmp(cp->attr, "confdir") == 0) break; + if (!cp->value) break; /* module name, "ok", etc. */ + } + break; + + case CONF_ITEM_SECTION: { + CONF_SECTION *cs_new = cf_item_to_section(ci); + CONF_SECTION *name1_cs; + + if (!cs->section_tree) { + cs->section_tree = rbtree_create(cs, section_cmp, NULL, 0); + if (!cs->section_tree) { + ERROR("Out of memory"); + fr_exit_now(1); + } + } + + name1_cs = rbtree_finddata(cs->section_tree, cs_new); + if (!name1_cs) { + if (!rbtree_insert(cs->section_tree, cs_new)) { + ERROR("Failed inserting section into tree"); + fr_exit_now(1); + } + break; + } + + /* + * We already have a section of + * this "name1". Add a new + * sub-section based on name2. + */ + if (!name1_cs->name2_tree) { + name1_cs->name2_tree = rbtree_create(name1_cs, name2_cmp, NULL, 0); + if (!name1_cs->name2_tree) { + ERROR("Out of memory"); + fr_exit_now(1); + } + } + + /* + * We don't care if this fails. + * If the user tries to create + * two sections of the same + * name1/name2, the duplicate + * section is just silently + * ignored. + */ + rbtree_insert(name1_cs->name2_tree, cs_new); + break; + } /* was a section */ + + case CONF_ITEM_DATA: + if (!cs->data_tree) { + cs->data_tree = rbtree_create(cs, data_cmp, NULL, 0); + } + if (cs->data_tree) { + rbtree_insert(cs->data_tree, ci); + } + break; + + default: /* FIXME: assert & error! */ + break; + + } /* switch over conf types */ + } /* loop over ci */ +} + + +CONF_ITEM *cf_reference_item(CONF_SECTION const *parentcs, + CONF_SECTION *outercs, + char const *ptr) +{ + CONF_PAIR *cp; + CONF_SECTION *next; + CONF_SECTION const *cs = outercs; + char name[8192]; + char *p; + + if (!cs) goto no_such_item; + + strlcpy(name, ptr, sizeof(name)); + p = name; + + /* + * ".foo" means "foo from the current section" + */ + if (*p == '.') { + p++; + + /* + * Just '.' means the current section + */ + if (*p == '\0') { + return cf_section_to_item(cs); + } + + /* + * ..foo means "foo from the section + * enclosing this section" (etc.) + */ + while (*p == '.') { + if (cs->item.parent) { + cs = cs->item.parent; + } + + /* + * .. means the section + * enclosing this section + */ + if (!*++p) { + return cf_section_to_item(cs); + } + } + + /* + * "foo.bar.baz" means "from the root" + */ + } else if (strchr(p, '.') != NULL) { + if (!parentcs) goto no_such_item; + + cs = parentcs; + } + + while (*p) { + char *q, *r; + + r = strchr(p, '['); + q = strchr(p, '.'); + if (!r && !q) break; + + if (r && q > r) q = NULL; + if (q && q < r) r = NULL; + + /* + * Split off name2. + */ + if (r) { + q = strchr(r + 1, ']'); + if (!q) return NULL; /* parse error */ + + /* + * Points to foo[bar]xx: parse error, + * it should be foo[bar] or foo[bar].baz + */ + if (q[1] && q[1] != '.') goto no_such_item; + + *r = '\0'; + *q = '\0'; + next = cf_section_sub_find_name2(cs, p, r + 1); + *r = '['; + *q = ']'; + + /* + * Points to a named instance of a section. + */ + if (!q[1]) { + if (!next) goto no_such_item; + return &(next->item); + } + + q++; /* ensure we skip the ']' and '.' */ + + } else { + *q = '\0'; + next = cf_section_sub_find(cs, p); + *q = '.'; + } + + if (!next) break; /* it MAY be a pair in this section! */ + + cs = next; + p = q + 1; + } + + if (!*p) goto no_such_item; + + retry: + /* + * Find it in the current referenced + * section. + */ + cp = cf_pair_find(cs, p); + if (cp) { + cp->parsed = true; /* conf pairs which are referenced count as parsed */ + return &(cp->item); + } + + next = cf_section_sub_find(cs, p); + if (next) return &(next->item); + + /* + * "foo" is "in the current section, OR in main". + */ + if ((p == name) && (parentcs != NULL) && (cs != parentcs)) { + cs = parentcs; + goto retry; + } + +no_such_item: + return NULL; +} + + +CONF_SECTION *cf_top_section(CONF_SECTION *cs) +{ + if (!cs) return NULL; + + while (cs->item.parent != NULL) { + cs = cs->item.parent; + } + + return cs; +} + + +/* + * Expand the variables in an input string. + */ +static char const *cf_expand_variables(char const *cf, int *lineno, + CONF_SECTION *outercs, + char *output, size_t outsize, + char const *input, bool *soft_fail) +{ + char *p; + char const *end, *ptr; + CONF_SECTION const *parentcs; + char name[8192]; + + if (soft_fail) *soft_fail = false; + + /* + * Find the master parent conf section. + * We can't use main_config.config, because we're in the + * process of re-building it, and it isn't set up yet... + */ + parentcs = cf_top_section(outercs); + + p = output; + ptr = input; + while (*ptr) { + /* + * Ignore anything other than "${" + */ + if ((*ptr == '$') && (ptr[1] == '{')) { + CONF_ITEM *ci; + CONF_PAIR *cp; + char *q; + + /* + * FIXME: Add support for ${foo:-bar}, + * like in xlat.c + */ + + /* + * Look for trailing '}', and log a + * warning for anything that doesn't match, + * and exit with a fatal error. + */ + end = strchr(ptr, '}'); + if (end == NULL) { + *p = '\0'; + ERROR("%s[%d]: Variable expansion missing }", + cf, *lineno); + return NULL; + } + + ptr += 2; + + /* + * Can't really happen because input lines are + * capped at 8k, which is sizeof(name) + */ + if ((size_t) (end - ptr) >= sizeof(name)) { + ERROR("%s[%d]: Reference string is too large", + cf, *lineno); + return NULL; + } + + memcpy(name, ptr, end - ptr); + name[end - ptr] = '\0'; + + q = strchr(name, ':'); + if (q) { + *(q++) = '\0'; + } + + ci = cf_reference_item(parentcs, outercs, name); + if (!ci) { + if (soft_fail) *soft_fail = true; + ERROR("%s[%d]: Reference \"${%s}\" not found", cf, *lineno, name); + return NULL; + } + + /* + * The expansion doesn't refer to another item or section + * it's the property of a section. + */ + if (q) { + CONF_SECTION *mycs = cf_item_to_section(ci); + + if (ci->type != CONF_ITEM_SECTION) { + ERROR("%s[%d]: Can only reference properties of sections", cf, *lineno); + return NULL; + } + + switch (fr_str2int(conf_property_name, q, CONF_PROPERTY_INVALID)) { + case CONF_PROPERTY_NAME: + strcpy(p, mycs->name1); + break; + + case CONF_PROPERTY_INSTANCE: + strcpy(p, mycs->name2 ? mycs->name2 : mycs->name1); + break; + + default: + ERROR("%s[%d]: Invalid property '%s'", cf, *lineno, q); + return NULL; + } + p += strlen(p); + ptr = end + 1; + + } else if (ci->type == CONF_ITEM_PAIR) { + /* + * Substitute the value of the variable. + */ + cp = cf_item_to_pair(ci); + + /* + * If the thing we reference is + * marked up as being expanded in + * pass2, don't expand it now. + * Let it be expanded in pass2. + */ + if (cp->pass2) { + if (soft_fail) *soft_fail = true; + + ERROR("%s[%d]: Reference \"%s\" points to a variable which has not been expanded.", + cf, *lineno, input); + return NULL; + } + + /* + * Might as well make + * non-existent string be the + * empty string. + */ + if (!cp->value) { + *p = '\0'; + goto skip_value; + } + + if (p + strlen(cp->value) >= output + outsize) { + ERROR("%s[%d]: Reference \"%s\" is too long", + cf, *lineno, input); + return NULL; + } + + strcpy(p, cp->value); + p += strlen(p); + skip_value: + ptr = end + 1; + + } else if (ci->type == CONF_ITEM_SECTION) { + CONF_SECTION *subcs; + + /* + * Adding an entry again to a + * section is wrong. We don't + * want an infinite loop. + */ + if (ci->parent == outercs) { + ERROR("%s[%d]: Cannot reference different item in same section", cf, *lineno); + return NULL; + } + + /* + * Copy the section instead of + * referencing it. + */ + subcs = cf_item_to_section(ci); + subcs = cf_section_dup(outercs, subcs, + cf_section_name1(subcs), cf_section_name2(subcs), + false); + if (!subcs) { + ERROR("%s[%d]: Failed copying reference %s", cf, *lineno, name); + return NULL; + } + + subcs->item.filename = ci->filename; + subcs->item.lineno = ci->lineno; + cf_item_add(outercs, &(subcs->item)); + + ptr = end + 1; + + } else { + ERROR("%s[%d]: Reference \"%s\" type is invalid", cf, *lineno, input); + return NULL; + } + } else if (strncmp(ptr, "$ENV{", 5) == 0) { + char *env; + + ptr += 5; + + /* + * Look for trailing '}', and log a + * warning for anything that doesn't match, + * and exit with a fatal error. + */ + end = strchr(ptr, '}'); + if (end == NULL) { + *p = '\0'; + ERROR("%s[%d]: Environment variable expansion missing }", + cf, *lineno); + return NULL; + } + + /* + * Can't really happen because input lines are + * capped at 8k, which is sizeof(name) + */ + if ((size_t) (end - ptr) >= sizeof(name)) { + ERROR("%s[%d]: Environment variable name is too large", + cf, *lineno); + return NULL; + } + + memcpy(name, ptr, end - ptr); + name[end - ptr] = '\0'; + + /* + * Get the environment variable. + * If none exists, then make it an empty string. + */ + env = getenv(name); + if (env == NULL) { + *name = '\0'; + env = name; + } + + if (p + strlen(env) >= output + outsize) { + ERROR("%s[%d]: Reference \"%s\" is too long", + cf, *lineno, input); + return NULL; + } + + strcpy(p, env); + p += strlen(p); + ptr = end + 1; + + } else { + /* + * Copy it over verbatim. + */ + *(p++) = *(ptr++); + } + + + if (p >= (output + outsize)) { + ERROR("%s[%d]: Reference \"%s\" is too long", + cf, *lineno, input); + return NULL; + } + } /* loop over all of the input string. */ + + *p = '\0'; + + return output; +} + +static char const parse_spaces[] = " "; + +/** Validation function for ipaddr conffile types + * + */ +static inline int fr_item_validate_ipaddr(CONF_SECTION *cs, char const *name, PW_TYPE type, char const *value, + fr_ipaddr_t *ipaddr) +{ + char ipbuf[128]; + + if (strcmp(value, "*") == 0) { + cf_log_info(cs, "%.*s\t%s = *", cs->depth, parse_spaces, name); + } else if (strspn(value, ".0123456789abdefABCDEF:%[]/") == strlen(value)) { + cf_log_info(cs, "%.*s\t%s = %s", cs->depth, parse_spaces, name, value); + } else { + cf_log_info(cs, "%.*s\t%s = %s IPv%s address [%s]", cs->depth, parse_spaces, name, value, + (ipaddr->af == AF_INET ? "4" : " 6"), ip_ntoh(ipaddr, ipbuf, sizeof(ipbuf))); + } + + switch (type) { + case PW_TYPE_IPV4_ADDR: + case PW_TYPE_IPV6_ADDR: + case PW_TYPE_COMBO_IP_ADDR: + switch (ipaddr->af) { + case AF_INET: + if (ipaddr->prefix == 32) return 0; + + cf_log_err(&(cs->item), "Invalid IPv4 mask length \"/%i\". Only \"/32\" permitted for non-prefix types", + ipaddr->prefix); + break; + + case AF_INET6: + if (ipaddr->prefix == 128) return 0; + + cf_log_err(&(cs->item), "Invalid IPv6 mask length \"/%i\". Only \"/128\" permitted for non-prefix types", + ipaddr->prefix); + break; + + + default: + cf_log_err(&(cs->item), "Unknown address (%d) family passed for parsing IP address.", ipaddr->af); + break; + } + + return -1; + + default: + break; + } + + return 0; +} + +/** Parses a #CONF_PAIR into a C data type, with a default value. + * + * Takes fields from a #CONF_PARSER struct and uses them to parse the string value + * of a #CONF_PAIR into a C data type matching the type argument. + * + * The format of the types are the same as #value_data_t types. + * + * @note The dflt value will only be used if no matching #CONF_PAIR is found. Empty strings will not + * result in the dflt value being used. + * + * **PW_TYPE to data type mappings** + * | PW_TYPE | Data type | Dynamically allocated | + * | ----------------------- | ------------------ | ---------------------- | + * | PW_TYPE_TMPL | ``vp_tmpl_t`` | Yes | + * | PW_TYPE_BOOLEAN | ``bool`` | No | + * | PW_TYPE_INTEGER | ``uint32_t`` | No | + * | PW_TYPE_SHORT | ``uint16_t`` | No | + * | PW_TYPE_INTEGER64 | ``uint64_t`` | No | + * | PW_TYPE_SIGNED | ``int32_t`` | No | + * | PW_TYPE_STRING | ``char const *`` | Yes | + * | PW_TYPE_IPV4_ADDR | ``fr_ipaddr_t`` | No | + * | PW_TYPE_IPV4_PREFIX | ``fr_ipaddr_t`` | No | + * | PW_TYPE_IPV6_ADDR | ``fr_ipaddr_t`` | No | + * | PW_TYPE_IPV6_PREFIX | ``fr_ipaddr_t`` | No | + * | PW_TYPE_COMBO_IP_ADDR | ``fr_ipaddr_t`` | No | + * | PW_TYPE_COMBO_IP_PREFIX | ``fr_ipaddr_t`` | No | + * | PW_TYPE_TIMEVAL | ``struct timeval`` | No | + * + * @param cs to search for matching #CONF_PAIR in. + * @param name of #CONF_PAIR to search for. + * @param type Data type to parse #CONF_PAIR value as. + * Should be one of the following ``data`` types, and one or more of the following ``flag`` types or'd together: + * - ``data`` #PW_TYPE_TMPL - @copybrief PW_TYPE_TMPL + * Feeds the value into #tmpl_afrom_str. Value can be + * obtained when processing requests, with #tmpl_expand or #tmpl_aexpand. + * - ``data`` #PW_TYPE_BOOLEAN - @copybrief PW_TYPE_BOOLEAN + * - ``data`` #PW_TYPE_INTEGER - @copybrief PW_TYPE_INTEGER + * - ``data`` #PW_TYPE_SHORT - @copybrief PW_TYPE_SHORT + * - ``data`` #PW_TYPE_INTEGER64 - @copybrief PW_TYPE_INTEGER64 + * - ``data`` #PW_TYPE_SIGNED - @copybrief PW_TYPE_SIGNED + * - ``data`` #PW_TYPE_STRING - @copybrief PW_TYPE_STRING + * - ``data`` #PW_TYPE_IPV4_ADDR - @copybrief PW_TYPE_IPV4_ADDR (IPv4 address with prefix 32). + * - ``data`` #PW_TYPE_IPV4_PREFIX - @copybrief PW_TYPE_IPV4_PREFIX (IPv4 address with variable prefix). + * - ``data`` #PW_TYPE_IPV6_ADDR - @copybrief PW_TYPE_IPV6_ADDR (IPv6 address with prefix 128). + * - ``data`` #PW_TYPE_IPV6_PREFIX - @copybrief PW_TYPE_IPV6_PREFIX (IPv6 address with variable prefix). + * - ``data`` #PW_TYPE_COMBO_IP_ADDR - @copybrief PW_TYPE_COMBO_IP_ADDR (IPv4/IPv6 address with + * prefix 32/128). + * - ``data`` #PW_TYPE_COMBO_IP_PREFIX - @copybrief PW_TYPE_COMBO_IP_PREFIX (IPv4/IPv6 address with + * variable prefix). + * - ``data`` #PW_TYPE_TIMEVAL - @copybrief PW_TYPE_TIMEVAL + * - ``flag`` #PW_TYPE_DEPRECATED - @copybrief PW_TYPE_DEPRECATED + * - ``flag`` #PW_TYPE_REQUIRED - @copybrief PW_TYPE_REQUIRED + * - ``flag`` #PW_TYPE_ATTRIBUTE - @copybrief PW_TYPE_ATTRIBUTE + * - ``flag`` #PW_TYPE_SECRET - @copybrief PW_TYPE_SECRET + * - ``flag`` #PW_TYPE_FILE_INPUT - @copybrief PW_TYPE_FILE_INPUT + * - ``flag`` #PW_TYPE_NOT_EMPTY - @copybrief PW_TYPE_NOT_EMPTY + * @param data Pointer to a global variable, or pointer to a field in the struct being populated with values. + * @param dflt value to use, if no #CONF_PAIR is found. + * @return + * - 1 if default value was used. + * - 0 on success. + * - -1 on error. + * - -2 if deprecated. + */ +int cf_item_parse(CONF_SECTION *cs, char const *name, unsigned int type, void *data, char const *dflt) +{ + int rcode; + bool deprecated, required, attribute, secret, file_input, cant_be_empty, tmpl, multi, file_exists; + char **q; + char const *value; + CONF_PAIR *cp = NULL; + fr_ipaddr_t *ipaddr; + char buffer[8192]; + CONF_ITEM *c_item; + + if (!cs) { + cf_log_err(&(cs->item), "No enclosing section for configuration item \"%s\"", name); + return -1; + } + + c_item = &cs->item; + + deprecated = (type & PW_TYPE_DEPRECATED); + required = (type & PW_TYPE_REQUIRED); + attribute = (type & PW_TYPE_ATTRIBUTE); + secret = (type & PW_TYPE_SECRET); + file_input = (type == PW_TYPE_FILE_INPUT); /* check, not and */ + file_exists = (type == PW_TYPE_FILE_EXISTS); /* check, not and */ + cant_be_empty = (type & PW_TYPE_NOT_EMPTY); + tmpl = (type & PW_TYPE_TMPL); + multi = (type & PW_TYPE_MULTI); + + if (attribute) required = true; + if (required) cant_be_empty = true; /* May want to review this in the future... */ + + /* + * Everything except templates must have a base type. + */ + if (!(type & 0xff) && !tmpl) { + cf_log_err(c_item, "Configuration item \"%s\" must have a data type", name); + return -1; + } + + type &= 0xff; /* normal types are small */ + + rcode = 0; + + cp = cf_pair_find(cs, name); + + /* + * No pairs match the configuration item name in the current + * section, use the default value. + */ + if (!cp) { + if (deprecated) return 0; /* Don't set the default value */ + + rcode = 1; + value = dflt; + /* + * Something matched, used the CONF_PAIR value. + */ + } else { + CONF_PAIR *next = cp; + + value = cp->value; + cp->parsed = true; + c_item = &cp->item; + + if (deprecated) { + cf_log_err(c_item, "Configuration item \"%s\" is deprecated", name); + return -2; + } + + /* + * A quick check to see if the next item is the same. + */ + if (!multi && cp->item.next && (cp->item.next->type == CONF_ITEM_PAIR)) { + next = cf_item_to_pair(cp->item.next); + + if (strcmp(next->attr, name) == 0) { + WARN("%s[%d]: Ignoring duplicate configuration item '%s'", + next->item.filename ? next->item.filename : "unknown", + next->item.lineno, name); + } + } + + if (multi) { + while ((next = cf_pair_find_next(cs, next, name)) != NULL) { + /* + * @fixme We should actually validate + * the value of the pairs too + */ + next->parsed = true; + }; + } + } + + if (!value) { + if (required) { + cf_log_err(c_item, "Configuration item \"%s\" must have a value", name); + + return -1; + } + return rcode; + } + + if ((value[0] == '\0') && cant_be_empty) { + cant_be_empty: + cf_log_err(c_item, "Configuration item \"%s\" must not be empty (zero length)", name); + if (!required) cf_log_err(c_item, "Comment item to silence this message"); + + return -1; + } + + + /* + * Process a value as a LITERAL template. Once all of + * the attrs and xlats are defined, the pass2 code + * converts it to the appropriate type. + */ + if (tmpl) { + vp_tmpl_t *vpt; + + if (!value) { + *(vp_tmpl_t **)data = NULL; + return 0; + } + + rad_assert(!attribute); + vpt = tmpl_alloc(cs, TMPL_TYPE_LITERAL, value, strlen(value)); + *(vp_tmpl_t **)data = vpt; + + return 0; + } + + switch (type) { + case PW_TYPE_BOOLEAN: + /* + * Allow yes/no, true/false, and on/off + */ + if ((strcasecmp(value, "yes") == 0) || + (strcasecmp(value, "true") == 0) || + (strcasecmp(value, "on") == 0)) { + *(bool *)data = true; + } else if ((strcasecmp(value, "no") == 0) || + (strcasecmp(value, "false") == 0) || + (strcasecmp(value, "off") == 0)) { + *(bool *)data = false; + } else { + *(bool *)data = false; + cf_log_err(&(cs->item), "Invalid value \"%s\" for boolean " + "variable %s", value, name); + return -1; + } + cf_log_info(cs, "%.*s\t%s = %s", + cs->depth, parse_spaces, name, value); + break; + + case PW_TYPE_INTEGER: + { + unsigned long v = strtoul(value, 0, 0); + + /* + * Restrict integer values to 0-INT32_MAX, this means + * it will always be safe to cast them to a signed type + * for comparisons, and imposes the same range limit as + * before we switched to using an unsigned type to + * represent config item integers. + */ + if (v > INT32_MAX) { + cf_log_err(&(cs->item), "Invalid value \"%s\" for variable %s, must be between 0-%u", value, + name, INT32_MAX); + return -1; + } + + *(uint32_t *)data = v; + cf_log_info(cs, "%.*s\t%s = %u", cs->depth, parse_spaces, name, *(uint32_t *)data); + } + break; + + case PW_TYPE_BYTE: + { + unsigned long v = strtoul(value, 0, 0); + + if (v > UINT8_MAX) { + cf_log_err(&(cs->item), "Invalid value \"%s\" for variable %s, must be between 0-%u", value, + name, UINT8_MAX); + return -1; + } + *(uint8_t *)data = (uint8_t) v; + cf_log_info(cs, "%.*s\t%s = %u", cs->depth, parse_spaces, name, *(uint8_t *)data); + } + break; + + case PW_TYPE_SHORT: + { + unsigned long v = strtoul(value, 0, 0); + + if (v > UINT16_MAX) { + cf_log_err(&(cs->item), "Invalid value \"%s\" for variable %s, must be between 0-%u", value, + name, UINT16_MAX); + return -1; + } + *(uint16_t *)data = (uint16_t) v; + cf_log_info(cs, "%.*s\t%s = %u", cs->depth, parse_spaces, name, *(uint16_t *)data); + } + break; + + case PW_TYPE_INTEGER64: + *(uint64_t *)data = strtoull(value, 0, 0); + cf_log_info(cs, "%.*s\t%s = %" PRIu64, cs->depth, parse_spaces, name, *(uint64_t *)data); + break; + + case PW_TYPE_SIGNED: + *(int32_t *)data = strtol(value, 0, 0); + cf_log_info(cs, "%.*s\t%s = %d", cs->depth, parse_spaces, name, *(int32_t *)data); + break; + + case PW_TYPE_STRING: + q = (char **) data; + if (*q != NULL) { + talloc_free(*q); + } + + /* + * Expand variables which haven't already been + * expanded automagically when the configuration + * file was read. + */ + if (value == dflt) { + int lineno = 0; + + lineno = cs->item.lineno; + + value = cf_expand_variables("<internal>", + &lineno, + cs, buffer, sizeof(buffer), + value, NULL); + if (!value) { + cf_log_err(&(cs->item),"Failed expanding variable %s", name); + return -1; + } + } + + if (cant_be_empty && (value[0] == '\0')) goto cant_be_empty; + + if (attribute) { + if (!dict_attrbyname(value)) { + if (!cp) { + cf_log_err(&(cs->item), "No such attribute '%s' for configuration '%s'", + value, name); + } else { + cf_log_err(&(cp->item), "No such attribute '%s'", value); + } + return -1; + } + } + + /* + * Hide secrets when using "radiusd -X". + */ + if (secret && (rad_debug_lvl <= 2)) { + cf_log_info(cs, "%.*s\t%s = <<< secret >>>", + cs->depth, parse_spaces, name); + } else { + cf_log_info(cs, "%.*s\t%s = \"%s\"", + cs->depth, parse_spaces, name, value ? value : "(null)"); + } + *q = value ? talloc_typed_strdup(cs, value) : NULL; + + /* + * If there's data AND it's an input file, check + * that we can read it. This check allows errors + * to be caught as early as possible, during + * server startup. + */ + if (*q && file_input && !cf_file_check(cs, *q, true)) { + cf_log_err(&(cs->item), "Failed parsing configuration item \"%s\"", name); + return -1; + } + + if (*q && file_exists && !cf_file_check(cs, *q, false)) { + cf_log_err(&(cs->item), "Failed parsing configuration item \"%s\"", name); + return -1; + } + break; + + case PW_TYPE_IPV4_ADDR: + case PW_TYPE_IPV4_PREFIX: + ipaddr = data; + + if (fr_pton4(ipaddr, value, -1, true, false) < 0) { + failed: + cf_log_err(&(cs->item), "Failed parsing configuration item \"%s\" - %s", name, fr_strerror()); + return -1; + } + if (fr_item_validate_ipaddr(cs, name, type, value, ipaddr) < 0) return -1; + break; + + case PW_TYPE_IPV6_ADDR: + case PW_TYPE_IPV6_PREFIX: + ipaddr = data; + + if (fr_pton6(ipaddr, value, -1, true, false) < 0) goto failed; + if (fr_item_validate_ipaddr(cs, name, type, value, ipaddr) < 0) return -1; + break; + + case PW_TYPE_COMBO_IP_ADDR: + case PW_TYPE_COMBO_IP_PREFIX: + ipaddr = data; + + if (fr_pton(ipaddr, value, -1, AF_UNSPEC, true) < 0) goto failed; + if (fr_item_validate_ipaddr(cs, name, type, value, ipaddr) < 0) return -1; + break; + + case PW_TYPE_TIMEVAL: { + int sec; + char *end; + struct timeval tv; + + sec = strtoul(value, &end, 10); + tv.tv_sec = sec; + tv.tv_usec = 0; + if (*end == '.') { + size_t len; + + len = strlen(end + 1); + + if (len > 6) { + cf_log_err(&(cs->item), "Too much precision for timeval"); + return -1; + } + + /* + * If they write "0.1", that means + * "10000" microseconds. + */ + sec = strtoul(end + 1, NULL, 10); + while (len < 6) { + sec *= 10; + len++; + } + + tv.tv_usec = sec; + } + cf_log_info(cs, "%.*s\t%s = %d.%06d", + cs->depth, parse_spaces, name, (int) tv.tv_sec, (int) tv.tv_usec); + memcpy(data, &tv, sizeof(tv)); + } + break; + + default: + /* + * If we get here, it's a sanity check error. + * It's not an error parsing the configuration + * file. + */ + rad_assert(type > PW_TYPE_INVALID); + rad_assert(type < PW_TYPE_MAX); + + cf_log_err(&(cs->item), "type '%s' is not supported in the configuration files", + fr_int2str(dict_attr_types, type, "?Unknown?")); + return -1; + } /* switch over variable type */ + + if (!cp) { + CONF_PAIR *cpn; + + cpn = cf_pair_alloc(cs, name, value, T_OP_SET, T_BARE_WORD, T_BARE_WORD); + if (!cpn) return -1; + cpn->parsed = true; + cpn->item.filename = "<internal>"; + cpn->item.lineno = 0; + cf_item_add(cs, &(cpn->item)); + } + + return rcode; +} + + +/* + * A copy of cf_section_parse that initializes pointers before + * parsing them. + */ +static void cf_section_parse_init(CONF_SECTION *cs, void *base, + CONF_PARSER const *variables) +{ + int i; + void *data; + + for (i = 0; variables[i].name != NULL; i++) { + if (variables[i].type == PW_TYPE_SUBSECTION) { + CONF_SECTION *subcs; + + if (!variables[i].dflt) continue; + + subcs = cf_section_sub_find(cs, variables[i].name); + + /* + * If there's no subsection in the + * config, BUT the CONF_PARSER wants one, + * then create an empty one. This is so + * that we can track the strings, + * etc. allocated in the subsection. + */ + if (!subcs) { + subcs = cf_section_alloc(cs, variables[i].name, NULL); + if (!subcs) return; + + subcs->item.filename = cs->item.filename; + subcs->item.lineno = cs->item.lineno; + cf_item_add(cs, &(subcs->item)); + } + if (base) { + data = ((uint8_t *)base) + variables[i].offset; + } else { + data = NULL; + } + + cf_section_parse_init(subcs, data, (CONF_PARSER const *) variables[i].dflt); + continue; + } + + if ((variables[i].type != PW_TYPE_STRING) && + (variables[i].type != PW_TYPE_FILE_INPUT) && + (variables[i].type != PW_TYPE_FILE_OUTPUT)) { + continue; + } + + if (variables[i].data) { + *(char **) variables[i].data = NULL; + } else if (base) { + *(char **) (((char *)base) + variables[i].offset) = NULL; + } else { + continue; + } + } /* for all variables in the configuration section */ +} + + +static void cf_section_parse_warn(CONF_SECTION *cs) +{ + CONF_ITEM *ci; + + for (ci = cs->children; ci; ci = ci->next) { + /* + * Don't recurse on sections. We can only safely + * check conf pairs at the same level as the + * section that was just parsed. + */ + if (ci->type == CONF_ITEM_SECTION) continue; + if (ci->type == CONF_ITEM_PAIR) { + CONF_PAIR *cp; + + cp = cf_item_to_pair(ci); + if (cp->parsed) continue; + + WARN("%s[%d]: The item '%s' is defined, but is unused by the configuration", + cp->item.filename ? cp->item.filename : "unknown", + cp->item.lineno ? cp->item.lineno : 0, + cp->attr); + } + + /* + * Skip everything else. + */ + } +} + +/** Parse a configuration section into user-supplied variables + * + * @param cs to parse. + * @param base pointer to a struct to fill with data. Any buffers will also be talloced + * using this parent as a pointer. + * @param variables mappings between struct fields and #CONF_ITEM s. + * @return + * - 0 on success. + * - -1 on general error. + * - -2 if a deprecated #CONF_ITEM was found. + */ +int cf_section_parse(CONF_SECTION *cs, void *base, CONF_PARSER const *variables) +{ + int ret = 0; + int i; + void *data; + + cs->variables = variables; /* this doesn't hurt anything */ + + if (!cs->name2) { + cf_log_info(cs, "%.*s%s {", cs->depth, parse_spaces, cs->name1); + } else { + cf_log_info(cs, "%.*s%s %s {", cs->depth, parse_spaces, cs->name1, cs->name2); + } + + cf_section_parse_init(cs, base, variables); + + /* + * Handle the known configuration parameters. + */ + for (i = 0; variables[i].name != NULL; i++) { + /* + * Handle subsections specially + */ + if (variables[i].type == PW_TYPE_SUBSECTION) { + CONF_SECTION *subcs; + + subcs = cf_section_sub_find(cs, variables[i].name); + /* + * Default in this case is overloaded to mean a pointer + * to the CONF_PARSER struct for the subsection. + */ + if (!variables[i].dflt || !subcs) { + ERROR("Internal sanity check 1 failed in cf_section_parse %s", variables[i].name); + ret = -1; + goto finish; + } + + if (base) { + data = ((uint8_t *)base) + variables[i].offset; + } else { + data = NULL; + } + + ret = cf_section_parse(subcs, data, (CONF_PARSER const *) variables[i].dflt); + if (ret < 0) goto finish; + continue; + } /* else it's a CONF_PAIR */ + + if (variables[i].data) { + data = variables[i].data; /* prefer this. */ + } else if (base) { + data = ((char *)base) + variables[i].offset; + } else { + ERROR("Internal sanity check 2 failed in cf_section_parse"); + ret = -1; + goto finish; + } + + /* + * Parse the pair we found, or a default value. + */ + ret = cf_item_parse(cs, variables[i].name, variables[i].type, data, variables[i].dflt); + switch (ret) { + case 1: /* Used default */ + ret = 0; + break; + + case 0: /* OK */ + break; + + case -1: /* Parse error */ + goto finish; + + case -2: /* Deprecated CONF ITEM */ + if ((variables[i + 1].offset == variables[i].offset) && + (variables[i + 1].data == variables[i].data)) { + cf_log_err(&(cs->item), "Replace \"%s\" with \"%s\"", variables[i].name, + variables[i + 1].name); + } else { + cf_log_err(&(cs->item), "Cannot use deprecated configuration item \"%s\"", variables[i].name); + } + goto finish; + } + } /* for all variables in the configuration section */ + + /* + * Ensure we have a proper terminator, type so we catch + * missing terminators reliably + */ + rad_assert(variables[i].type == -1); + + /* + * Warn about items in the configuration which weren't + * checked during parsing. + */ + if (rad_debug_lvl >= 3) cf_section_parse_warn(cs); + + cs->base = base; + + cf_log_info(cs, "%.*s}", cs->depth, parse_spaces); + +finish: + return ret; +} + + +/* + * Check XLAT things in pass 2. But don't cache the xlat stuff anywhere. + */ +int cf_section_parse_pass2(CONF_SECTION *cs, void *base, CONF_PARSER const *variables) +{ + int i; + ssize_t slen; + char const *error; + char *value = NULL; + xlat_exp_t *xlat; + + /* + * Handle the known configuration parameters. + */ + for (i = 0; variables[i].name != NULL; i++) { + CONF_PAIR *cp; + void *data; + + /* + * Handle subsections specially + */ + if (variables[i].type == PW_TYPE_SUBSECTION) { + CONF_SECTION *subcs; + subcs = cf_section_sub_find(cs, variables[i].name); + + if (cf_section_parse_pass2(subcs, (uint8_t *)base + variables[i].offset, + (CONF_PARSER const *) variables[i].dflt) < 0) { + return -1; + } + continue; + } /* else it's a CONF_PAIR */ + + /* + * Figure out which data we need to fix. + */ + if (variables[i].data) { + data = variables[i].data; /* prefer this. */ + } else if (base) { + data = ((char *)base) + variables[i].offset; + } else { + data = NULL; + } + + cp = cf_pair_find(cs, variables[i].name); + xlat = NULL; + + redo: + if (!cp || !cp->value || !data) continue; + + if ((cp->rhs_type != T_DOUBLE_QUOTED_STRING) && + (cp->rhs_type != T_BARE_WORD)) continue; + + /* + * Non-xlat expansions shouldn't have xlat! + */ + if (((variables[i].type & PW_TYPE_XLAT) == 0) && + ((variables[i].type & PW_TYPE_TMPL) == 0)) { + /* + * Ignore %{... in shared secrets. + * They're never dynamically expanded. + */ + if ((variables[i].type & PW_TYPE_SECRET) != 0) continue; + + if (strstr(cp->value, "%{") != NULL) { + WARN("%s[%d]: Found dynamic expansion in string which will not be dynamically expanded", + cp->item.filename ? cp->item.filename : "unknown", + cp->item.lineno ? cp->item.lineno : 0); + } + continue; + } + + /* + * Parse (and throw away) the xlat string. + * + * FIXME: All of these should be converted from PW_TYPE_XLAT + * to PW_TYPE_TMPL. + */ + if ((variables[i].type & PW_TYPE_XLAT) != 0) { + /* + * xlat expansions should be parseable. + */ + value = talloc_strdup(cs, cp->value); /* modified by xlat_tokenize */ + xlat = NULL; + + slen = xlat_tokenize(cs, value, &xlat, &error); + if (slen < 0) { + char *spaces, *text; + + error: + fr_canonicalize_error(cs, &spaces, &text, slen, cp->value); + + cf_log_err(&cp->item, "Failed parsing expanded string:"); + cf_log_err(&cp->item, "%s", text); + cf_log_err(&cp->item, "%s^ %s", spaces, error); + + talloc_free(spaces); + talloc_free(text); + talloc_free(value); + talloc_free(xlat); + return -1; + } + + talloc_free(value); + talloc_free(xlat); + } + + /* + * Convert the LITERAL template to the actual + * type. + */ + if ((variables[i].type & PW_TYPE_TMPL) != 0) { + vp_tmpl_t *vpt; + + slen = tmpl_afrom_str(cs, &vpt, cp->value, talloc_array_length(cp->value) - 1, + cp->rhs_type, + REQUEST_CURRENT, PAIR_LIST_REQUEST, true); + if (slen < 0) { + error = fr_strerror(); + goto error; + } + + /* + * Sanity check + * + * Don't add default - update with new types. + */ + switch (vpt->type) { + /* + * All attributes should have been defined by this point. + */ + case TMPL_TYPE_ATTR_UNDEFINED: + cf_log_err(&cp->item, "Unknown attribute '%s'", vpt->tmpl_unknown_name); + return -1; + + case TMPL_TYPE_LITERAL: + case TMPL_TYPE_ATTR: + case TMPL_TYPE_LIST: + case TMPL_TYPE_DATA: + case TMPL_TYPE_EXEC: + case TMPL_TYPE_XLAT: + case TMPL_TYPE_XLAT_STRUCT: + break; + + case TMPL_TYPE_UNKNOWN: + case TMPL_TYPE_REGEX: + case TMPL_TYPE_REGEX_STRUCT: + case TMPL_TYPE_NULL: + rad_assert(0); + } + + talloc_free(*(vp_tmpl_t **)data); + *(vp_tmpl_t **)data = vpt; + } + + /* + * If the "multi" flag is set, check all of them. + */ + if ((variables[i].type & PW_TYPE_MULTI) != 0) { + cp = cf_pair_find_next(cs, cp, cp->attr); + goto redo; + } + } /* for all variables in the configuration section */ + + return 0; +} + +/* + * Merge the template so everyting else "just works". + */ +static bool cf_template_merge(CONF_SECTION *cs, CONF_SECTION const *template) +{ + CONF_ITEM *ci; + + if (!cs || !template) return true; + + cs->template = NULL; + + /* + * Walk over the template, adding its' entries to the + * current section. But only if the entries don't + * already exist in the current section. + */ + for (ci = template->children; ci; ci = ci->next) { + if (ci->type == CONF_ITEM_PAIR) { + CONF_PAIR *cp1, *cp2; + + /* + * It exists, don't over-write it. + */ + cp1 = cf_item_to_pair(ci); + if (cf_pair_find(cs, cp1->attr)) { + continue; + } + + /* + * Create a new pair with all of the data + * of the old one. + */ + cp2 = cf_pair_dup(cs, cp1); + if (!cp2) return false; + + cp2->item.filename = cp1->item.filename; + cp2->item.lineno = cp1->item.lineno; + + cf_item_add(cs, &(cp2->item)); + continue; + } + + if (ci->type == CONF_ITEM_SECTION) { + CONF_SECTION *subcs1, *subcs2; + + subcs1 = cf_item_to_section(ci); + rad_assert(subcs1 != NULL); + + subcs2 = cf_section_sub_find_name2(cs, subcs1->name1, subcs1->name2); + if (subcs2) { + /* + * sub-sections get merged. + */ + if (!cf_template_merge(subcs2, subcs1)) { + return false; + } + continue; + } + + /* + * Our section doesn't have a matching + * sub-section. Copy it verbatim from + * the template. + */ + subcs2 = cf_section_dup(cs, subcs1, + cf_section_name1(subcs1), cf_section_name2(subcs1), + false); + if (!subcs2) return false; + + subcs2->item.filename = subcs1->item.filename; + subcs2->item.lineno = subcs1->item.lineno; + + cf_item_add(cs, &(subcs2->item)); + continue; + } + + /* ignore everything else */ + } + + return true; +} + +static char const *cf_local_file(char const *base, char const *filename, + char *buffer, size_t bufsize) +{ + size_t dirsize; + char *p; + + strlcpy(buffer, base, bufsize); + + p = strrchr(buffer, FR_DIR_SEP); + if (!p) return filename; + if (p[1]) { /* ./foo */ + p[1] = '\0'; + } + + dirsize = (p - buffer) + 1; + + if ((dirsize + strlen(filename)) >= bufsize) { + return NULL; + } + + strlcpy(p + 1, filename, bufsize - dirsize); + + return buffer; +} + + +/* + * Read a part of the config file. + */ +static int cf_section_read(char const *filename, int *lineno, FILE *fp, + CONF_SECTION *current) + +{ + CONF_SECTION *this, *css; + CONF_PAIR *cpn; + char const *ptr; + char const *value; + char buf[8192]; + char buf1[8192]; + char buf2[8192]; + char buf3[8192]; + char buf4[8192]; + FR_TOKEN t1 = T_INVALID, t2, t3; + bool has_spaces = false; + bool pass2; + char *cbuf = buf; + size_t len; + + this = current; /* add items here */ + + /* + * Read, checking for line continuations ('\\' at EOL) + */ + for (;;) { + int at_eof; + css = NULL; + + /* + * Get data, and remember if we are at EOF. + */ + at_eof = (fgets(cbuf, sizeof(buf) - (cbuf - buf), fp) == NULL); + (*lineno)++; + + /* + * We read the entire 8k worth of data: complain. + * Note that we don't care if the last character + * is \n: it's still forbidden. This means that + * the maximum allowed length of text is 8k-1, which + * should be plenty. + */ + len = strlen(cbuf); + if ((cbuf + len + 1) >= (buf + sizeof(buf))) { + ERROR("%s[%d]: Line too long", + filename, *lineno); + return -1; + } + + if (has_spaces) { + ptr = cbuf; + while (isspace((uint8_t) *ptr)) ptr++; + + if (ptr > cbuf) { + memmove(cbuf, ptr, len - (ptr - cbuf)); + len -= (ptr - cbuf); + } + } + + /* + * Not doing continuations: check for edge + * conditions. + */ + if (cbuf == buf) { + if (at_eof) break; + + ptr = buf; + while (*ptr && isspace((uint8_t) *ptr)) ptr++; + + if (!*ptr || (*ptr == '#')) continue; + + } else if (at_eof || (len == 0)) { + ERROR("%s[%d]: Continuation at EOF is illegal", + filename, *lineno); + return -1; + } + + /* + * See if there's a continuation. + */ + while ((len > 0) && + ((cbuf[len - 1] == '\n') || (cbuf[len - 1] == '\r'))) { + len--; + cbuf[len] = '\0'; + } + + if ((len > 0) && (cbuf[len - 1] == '\\')) { + /* + * Check for "suppress spaces" magic. + */ + if (!has_spaces && (len > 2) && (cbuf[len - 2] == '"')) { + has_spaces = true; + } + + cbuf[len - 1] = '\0'; + cbuf += len - 1; + continue; + } + + ptr = cbuf = buf; + has_spaces = false; + + get_more: + pass2 = false; + + /* + * The parser is getting to be evil. + */ + while ((*ptr == ' ') || (*ptr == '\t')) ptr++; + + if (((ptr[0] == '%') && (ptr[1] == '{')) || + (ptr[0] == '`')) { + int hack; + + if (ptr[0] == '%') { + hack = rad_copy_variable(buf1, ptr); + } else { + hack = rad_copy_string(buf1, ptr); + } + if (hack < 0) { + ERROR("%s[%d]: Invalid expansion: %s", + filename, *lineno, ptr); + return -1; + } + + ptr += hack; + + t2 = gettoken(&ptr, buf2, sizeof(buf2), true); + switch (t2) { + case T_EOL: + case T_HASH: + goto do_bare_word; + + default: + ERROR("%s[%d]: Invalid expansion: %s", + filename, *lineno, ptr); + return -1; + } + } else { + t1 = gettoken(&ptr, buf1, sizeof(buf1), true); + } + + /* + * The caller eats "name1 name2 {", and calls us + * for the data inside of the section. So if we + * receive a closing brace, then it must mean the + * end of the section. + */ + if (t1 == T_RCBRACE) { + if (this == current) { + ERROR("%s[%d]: Too many closing braces", + filename, *lineno); + return -1; + } + + /* + * Merge the template into the existing + * section. This uses more memory, but + * means that templates now work with + * sub-sections, etc. + */ + if (!cf_template_merge(this, this->template)) { + return -1; + } + + this = this->item.parent; + goto check_for_more; + } + + if (t1 != T_BARE_WORD) goto skip_keywords; + + /* + * Allow for $INCLUDE files + * + * This *SHOULD* work for any level include. + * I really really really hate this file. -cparker + */ + if ((strcasecmp(buf1, "$INCLUDE") == 0) || + (strcasecmp(buf1, "$-INCLUDE") == 0)) { + bool relative = true; + + t2 = getword(&ptr, buf2, sizeof(buf2), true); + if (t2 != T_EOL) { + ERROR("%s[%d]: Unexpected text after $INCLUDE", + filename, *lineno); + return -1; + } + + if (buf2[0] == '$') relative = false; + + value = cf_expand_variables(filename, lineno, this, buf4, sizeof(buf4), buf2, NULL); + if (!value) return -1; + + if (!FR_DIR_IS_RELATIVE(value)) relative = false; + + if (relative) { + value = cf_local_file(filename, value, buf3, + sizeof(buf3)); + if (!value) { + ERROR("%s[%d]: Directories too deep.", + filename, *lineno); + return -1; + } + } + + +#ifdef HAVE_DIRENT_H + /* + * $INCLUDE foo/ + * + * Include ALL non-"dot" files in the directory. + * careful! + */ + if (value[strlen(value) - 1] == '/') { + DIR *dir; + struct dirent *dp; + struct stat stat_buf; + + DEBUG2("including files in directory %s", value ); +#ifdef S_IWOTH + /* + * Security checks. + */ + if (stat(value, &stat_buf) < 0) { + ERROR("%s[%d]: Failed reading directory %s: %s", + filename, *lineno, + value, fr_syserror(errno)); + return -1; + } + + if ((stat_buf.st_mode & S_IWOTH) != 0) { + ERROR("%s[%d]: Directory %s is globally writable. Refusing to start due to " + "insecure configuration", filename, *lineno, value); + return -1; + } +#endif + dir = opendir(value); + if (!dir) { + ERROR("%s[%d]: Error reading directory %s: %s", + filename, *lineno, value, + fr_syserror(errno)); + return -1; + } + + /* + * Read the directory, ignoring "." files. + */ + while ((dp = readdir(dir)) != NULL) { + char const *p; + int slen; + + if (dp->d_name[0] == '.') continue; + + /* + * Check for valid characters + */ + for (p = dp->d_name; *p != '\0'; p++) { + if (isalpha((uint8_t)*p) || + isdigit((uint8_t)*p) || + (*p == '-') || + (*p == '_') || + (*p == '.')) continue; + break; + } + if (*p != '\0') continue; + + slen = snprintf(buf2, sizeof(buf2), "%s%s", + value, dp->d_name); + if (slen >= (int) sizeof(buf2) || slen < 0) { + ERROR("%s: Full file path is too long.", dp->d_name); + return -1; + } + if ((stat(buf2, &stat_buf) != 0) || + S_ISDIR(stat_buf.st_mode)) continue; + + /* + * Read the file into the current + * configuration section. + */ + if (cf_file_include(this, buf2, true) < 0) { + closedir(dir); + return -1; + } + } + closedir(dir); + } else +#endif + { /* it was a normal file */ + if (buf1[1] == '-') { + struct stat statbuf; + + if (stat(value, &statbuf) < 0) { + WARN("Not including file %s: %s", value, fr_syserror(errno)); + continue; + } + } + + if (cf_file_include(this, value, false) < 0) { + return -1; + } + } + continue; + } /* we were in an include */ + + if (strcasecmp(buf1, "$template") == 0) { + CONF_ITEM *ci; + CONF_SECTION *parentcs, *templatecs; + t2 = getword(&ptr, buf2, sizeof(buf2), true); + + if (t2 != T_EOL) { + ERROR("%s[%d]: Unexpected text after $TEMPLATE", filename, *lineno); + return -1; + } + + parentcs = cf_top_section(current); + + templatecs = cf_section_sub_find(parentcs, "templates"); + if (!templatecs) { + ERROR("%s[%d]: No \"templates\" section for reference \"%s\"", filename, *lineno, buf2); + return -1; + } + + ci = cf_reference_item(parentcs, templatecs, buf2); + if (!ci || (ci->type != CONF_ITEM_SECTION)) { + ERROR("%s[%d]: Reference \"%s\" not found", filename, *lineno, buf2); + return -1; + } + + if (!this) { + ERROR("%s[%d]: Internal sanity check error in template reference", filename, *lineno); + return -1; + } + + if (this->template) { + ERROR("%s[%d]: Section already has a template", filename, *lineno); + return -1; + } + + this->template = cf_item_to_section(ci); + continue; + } + + /* + * Ensure that the user can't add CONF_PAIRs + * with 'internal' names; + */ + if (buf1[0] == '_') { + ERROR("%s[%d]: Illegal configuration pair name \"%s\"", filename, *lineno, buf1); + return -1; + } + + /* + * Handle if/elsif specially. + */ + if ((strcmp(buf1, "if") == 0) || (strcmp(buf1, "elsif") == 0)) { + ssize_t slen; + char const *error = NULL; + char *p; + CONF_SECTION *server; + fr_cond_t *cond = NULL; + + /* + * if / elsif MUST be inside of a + * processing section, which MUST in turn + * be inside of a "server" directive. + */ + if (!this->item.parent) { + invalid_location: + ERROR("%s[%d]: Invalid location for '%s'", + filename, *lineno, buf1); + return -1; + } + + /* + * Can only have "if" in 3 named sections. + */ + server = this->item.parent; + while (server && + (strcmp(server->name1, "server") != 0) && + (strcmp(server->name1, "policy") != 0) && + (strcmp(server->name1, "instantiate") != 0)) { + server = server->item.parent; + if (!server) goto invalid_location; + } + + /* + * Skip (...) to find the { + */ + slen = fr_condition_tokenize(this, cf_section_to_item(this), ptr, &cond, + &error, FR_COND_TWO_PASS); + memcpy(&p, &ptr, sizeof(p)); + + if (slen < 0) { + if (p[-slen] != '{') goto cond_error; + slen = -slen; + } + TALLOC_FREE(cond); + + /* + * This hack is so that the NEXT stage + * doesn't go "too far" in expanding the + * variable. We can parse the conditions + * without expanding the ${...} stuff. + * BUT we don't want to expand all of the + * stuff AFTER the condition. So we do + * two passes. + * + * The first pass is to discover the end + * of the condition. We then expand THAT + * string, and do a second pass parsing + * the expanded condition. + */ + p += slen; + *p = '\0'; + + /* + * If there's a ${...}. If so, expand it. + */ + if (strchr(ptr, '$') != NULL) { + ptr = cf_expand_variables(filename, lineno, + this, + buf3, sizeof(buf3), + ptr, NULL); + if (!ptr) { + ERROR("%s[%d]: Parse error expanding ${...} in condition", + filename, *lineno); + return -1; + } + } /* else leave it alone */ + + css = cf_section_alloc(this, buf1, ptr); + if (!css) { + ERROR("%s[%d]: Failed allocating memory for section", + filename, *lineno); + return -1; + } + css->item.filename = filename; + css->item.lineno = *lineno; + + slen = fr_condition_tokenize(css, cf_section_to_item(css), ptr, &cond, + &error, FR_COND_TWO_PASS); + *p = '{'; /* put it back */ + + cond_error: + if (slen < 0) { + char *spaces, *text; + + fr_canonicalize_error(this, &spaces, &text, slen, ptr); + + ERROR("%s[%d]: Parse error in condition", + filename, *lineno); + ERROR("%s[%d]: %s", filename, *lineno, text); + ERROR("%s[%d]: %s^ %s", filename, *lineno, spaces, error); + + talloc_free(spaces); + talloc_free(text); + talloc_free(css); + return -1; + } + + if ((size_t) slen >= (sizeof(buf2) - 1)) { + talloc_free(css); + ERROR("%s[%d]: Condition is too large after \"%s\"", + filename, *lineno, buf1); + return -1; + } + + /* + * Copy the expanded and parsed condition + * into buf2. Then, parse the text after + * the condition, which now MUST be a '{. + * + * If it wasn't '{' it would have been + * caught in the first pass of + * conditional parsing, above. + */ + memcpy(buf2, ptr, slen); + buf2[slen] = '\0'; + ptr = p; + + if ((t3 = gettoken(&ptr, buf3, sizeof(buf3), true)) != T_LCBRACE) { + talloc_free(css); + ERROR("%s[%d]: Expected '{' %d", + filename, *lineno, t3); + return -1; + } + + /* + * Swap the condition with trailing stuff for + * the final condition. + */ + memcpy(&p, &css->name2, sizeof(css->name2)); + talloc_free(p); + css->name2 = talloc_typed_strdup(css, buf2); + + cf_item_add(this, &(css->item)); + cf_data_add_internal(css, "if", cond, NULL, false); + + /* + * The current section is now the child section. + */ + this = css; + css = NULL; + goto check_for_more; + } + + skip_keywords: + /* + * Grab the next token. + */ + t2 = gettoken(&ptr, buf2, sizeof(buf2), !cf_new_escape); + switch (t2) { + case T_EOL: + case T_HASH: + case T_COMMA: + do_bare_word: + t3 = t2; + t2 = T_OP_EQ; + value = NULL; + goto do_set; + + case T_OP_INCRM: + case T_OP_ADD: + case T_OP_CMP_EQ: + case T_OP_SUB: + case T_OP_LE: + case T_OP_GE: + case T_OP_CMP_FALSE: + if (!this || (strcmp(this->name1, "update") != 0)) { + ERROR("%s[%d]: Invalid operator in assignment", + filename, *lineno); + return -1; + } + /* FALL-THROUGH */ + + case T_OP_EQ: + case T_OP_SET: + case T_OP_PREPEND: + while (isspace((uint8_t) *ptr)) ptr++; + + /* + * Be a little more forgiving. + */ + if (*ptr == '#') { + t3 = T_HASH; + } else + + /* + * New parser: non-quoted strings are + * bare words, and we parse everything + * until the next newline, or the next + * comma. If they have { or } in a bare + * word, well... too bad. + */ + if (cf_new_escape && (*ptr != '"') && (*ptr != '\'') + && (*ptr != '`') && (*ptr != '/')) { + const char *q = ptr; + + t3 = T_BARE_WORD; + while (*q && (*q >= ' ') && (*q != ',') && + !isspace((uint8_t) *q)) q++; + + if ((size_t) (q - ptr) >= sizeof(buf3)) { + ERROR("%s[%d]: Parse error: value too long", + filename, *lineno); + return -1; + } + + memcpy(buf3, ptr, (q - ptr)); + buf3[q - ptr] = '\0'; + ptr = q; + + } else { + t3 = getstring(&ptr, buf3, sizeof(buf3), !cf_new_escape); + } + + if (t3 == T_INVALID) { + ERROR("%s[%d]: Parse error: %s", + filename, *lineno, + fr_strerror()); + return -1; + } + + /* + * Allow "foo" by itself, or "foo = bar" + */ + switch (t3) { + bool soft_fail; + + case T_BARE_WORD: + case T_DOUBLE_QUOTED_STRING: + case T_BACK_QUOTED_STRING: + value = cf_expand_variables(filename, lineno, this, buf4, sizeof(buf4), buf3, &soft_fail); + if (!value) { + if (!soft_fail) return -1; + + /* + * References an item which doesn't exist, + * or which is already marked up as being + * expanded in pass2. Wait for pass2 to + * do the expansions. + */ + pass2 = true; + value = buf3; + } + break; + + case T_EOL: + case T_HASH: + value = NULL; + break; + + default: + value = buf3; + break; + } + + /* + * Add this CONF_PAIR to our CONF_SECTION + */ + do_set: + cpn = cf_pair_alloc(this, buf1, value, t2, t1, t3); + if (!cpn) return -1; + cpn->item.filename = filename; + cpn->item.lineno = *lineno; + cpn->pass2 = pass2; + cf_item_add(this, &(cpn->item)); + + /* + * Require a comma, unless there's a comment. + */ + while (isspace((uint8_t) *ptr)) ptr++; + + if (*ptr == ',') { + ptr++; + break; + } + + /* + * module # stuff! + * foo = bar # other stuff + */ + if ((t3 == T_HASH) || (t3 == T_COMMA) || (t3 == T_EOL) || (*ptr == '#')) continue; + + if (!*ptr || (*ptr == '}')) break; + + ERROR("%s[%d]: Syntax error: Expected comma after '%s': %s", + filename, *lineno, value, ptr); + return -1; + + /* + * No '=', must be a section or sub-section. + */ + case T_BARE_WORD: + case T_DOUBLE_QUOTED_STRING: + case T_SINGLE_QUOTED_STRING: + t3 = gettoken(&ptr, buf3, sizeof(buf3), true); + if (t3 != T_LCBRACE) { + ERROR("%s[%d]: Expecting section start brace '{' after \"%s %s\"", + filename, *lineno, buf1, buf2); + return -1; + } + /* FALL-THROUGH */ + + case T_LCBRACE: + css = cf_section_alloc(this, buf1, + t2 == T_LCBRACE ? NULL : buf2); + if (!css) { + ERROR("%s[%d]: Failed allocating memory for section", + filename, *lineno); + return -1; + } + + css->item.filename = filename; + css->item.lineno = *lineno; + cf_item_add(this, &(css->item)); + + /* + * There may not be a name2 + */ + css->name2_type = (t2 == T_LCBRACE) ? T_INVALID : t2; + + /* + * The current section is now the child section. + */ + this = css; + break; + + case T_INVALID: + ERROR("%s[%d]: Syntax error in '%s': %s", filename, *lineno, ptr, fr_strerror()); + + return -1; + + default: + ERROR("%s[%d]: Parse error after \"%s\": unexpected token \"%s\"", + filename, *lineno, buf1, fr_int2str(fr_tokens, t2, "<INVALID>")); + + return -1; + } + + check_for_more: + /* + * Done parsing one thing. Skip to EOL if possible. + */ + while (isspace((uint8_t) *ptr)) ptr++; + + if (*ptr == '#') continue; + + if (*ptr) { + goto get_more; + } + + } + + /* + * See if EOF was unexpected .. + */ + if (feof(fp) && (this != current)) { + ERROR("%s[%d]: EOF reached without closing brace for section %s starting at line %d", + filename, *lineno, cf_section_name1(this), cf_section_lineno(this)); + return -1; + } + + return 0; +} + +/* + * Include one config file in another. + */ +static int cf_file_include(CONF_SECTION *cs, char const *filename_in, bool from_dir) +{ + FILE *fp; + int rcode; + int lineno = 0; + char const *filename; + + /* + * So we only need to do this once. + */ + filename = talloc_strdup(cs, filename_in); + + /* + * This may return "0" if we already loaded the file. + */ + rcode = cf_file_open(cs, filename, from_dir, &fp); + if (rcode <= 0) return rcode; + + if (!cs->item.filename) cs->item.filename = filename; + + /* + * Read the section. It's OK to have EOF without a + * matching close brace. + */ + if (cf_section_read(filename, &lineno, fp, cs) < 0) { + fclose(fp); + return -1; + } + + fclose(fp); + return 0; +} + + +/* + * Do variable expansion in pass2. + * + * This is a breadth-first expansion. "deep + */ +static int cf_section_pass2(CONF_SECTION *cs) +{ + CONF_ITEM *ci; + + for (ci = cs->children; ci; ci = ci->next) { + char const *value; + CONF_PAIR *cp; + char buffer[8192]; + + if (ci->type != CONF_ITEM_PAIR) continue; + + cp = cf_item_to_pair(ci); + if (!cp->value || !cp->pass2) continue; + + rad_assert((cp->rhs_type == T_BARE_WORD) || + (cp->rhs_type == T_DOUBLE_QUOTED_STRING) || + (cp->rhs_type == T_BACK_QUOTED_STRING)); + + value = cf_expand_variables(ci->filename, &ci->lineno, cs, buffer, sizeof(buffer), cp->value, NULL); + if (!value) return -1; + + rad_const_free(cp->value); + cp->value = talloc_typed_strdup(cp, value); + } + + for (ci = cs->children; ci; ci = ci->next) { + if (ci->type != CONF_ITEM_SECTION) continue; + + if (cf_section_pass2(cf_item_to_section(ci)) < 0) return -1; + } + + return 0; +} + + +/* + * Bootstrap a config file. + */ +int cf_file_read(CONF_SECTION *cs, char const *filename) +{ + char *p; + CONF_PAIR *cp; + rbtree_t *tree; + + cp = cf_pair_alloc(cs, "confdir", filename, T_OP_SET, T_BARE_WORD, T_SINGLE_QUOTED_STRING); + if (!cp) return -1; + + p = strrchr(cp->value, FR_DIR_SEP); + if (p) *p = '\0'; + + cp->item.filename = "<internal>"; + cp->item.lineno = -1; + cf_item_add(cs, &(cp->item)); + + tree = rbtree_create(cs, filename_cmp, NULL, 0); + if (!tree) return -1; + + cf_data_add_internal(cs, "filename", tree, NULL, 0); + + if (cf_file_include(cs, filename, false) < 0) return -1; + + /* + * Now that we've read the file, go back through it and + * expand the variables. + */ + if (cf_section_pass2(cs) < 0) return -1; + + return 0; +} + + +void cf_file_free(CONF_SECTION *cs) +{ + talloc_free(cs); +} + + +/* + * Return a CONF_PAIR within a CONF_SECTION. + */ +CONF_PAIR *cf_pair_find(CONF_SECTION const *cs, char const *name) +{ + CONF_PAIR *cp, mycp; + + if (!cs || !name) return NULL; + + mycp.attr = name; + cp = rbtree_finddata(cs->pair_tree, &mycp); + if (cp) return cp; + + if (!cs->template) return NULL; + + return rbtree_finddata(cs->template->pair_tree, &mycp); +} + +/* + * Return the attr of a CONF_PAIR + */ + +char const *cf_pair_attr(CONF_PAIR const *pair) +{ + return (pair ? pair->attr : NULL); +} + +/* + * Return the value of a CONF_PAIR + */ + +char const *cf_pair_value(CONF_PAIR const *pair) +{ + return (pair ? pair->value : NULL); +} + +FR_TOKEN cf_pair_operator(CONF_PAIR const *pair) +{ + return (pair ? pair->op : T_INVALID); +} + +/** Return the value (lhs) type + * + * @param pair to extract value type from. + * @return one of T_BARE_WORD, T_SINGLE_QUOTED_STRING, T_BACK_QUOTED_STRING + * T_DOUBLE_QUOTED_STRING or T_INVALID if the pair is NULL. + */ +FR_TOKEN cf_pair_attr_type(CONF_PAIR const *pair) +{ + return (pair ? pair->lhs_type : T_INVALID); +} + +/** Return the value (rhs) type + * + * @param pair to extract value type from. + * @return one of T_BARE_WORD, T_SINGLE_QUOTED_STRING, T_BACK_QUOTED_STRING + * T_DOUBLE_QUOTED_STRING or T_INVALID if the pair is NULL. + */ +FR_TOKEN cf_pair_value_type(CONF_PAIR const *pair) +{ + return (pair ? pair->rhs_type : T_INVALID); +} + +/* + * Turn a CONF_PAIR into a VALUE_PAIR + * For now, ignore the "value_type" field... + */ +VALUE_PAIR *cf_pairtovp(CONF_PAIR *pair) +{ + if (!pair) { + fr_strerror_printf("Internal error"); + return NULL; + } + + if (!pair->value) { + fr_strerror_printf("No value given for attribute %s", pair->attr); + return NULL; + } + + /* + * false comparisons never match. BUT if it's a "string" + * or `string`, then remember to expand it later. + */ + if ((pair->op != T_OP_CMP_FALSE) && + ((pair->rhs_type == T_DOUBLE_QUOTED_STRING) || + (pair->rhs_type == T_BACK_QUOTED_STRING))) { + VALUE_PAIR *vp; + + vp = fr_pair_make(pair, NULL, pair->attr, NULL, pair->op); + if (!vp) { + return NULL; + } + + if (fr_pair_mark_xlat(vp, pair->value) < 0) { + talloc_free(vp); + + return NULL; + } + + return vp; + } + + return fr_pair_make(pair, NULL, pair->attr, pair->value, pair->op); +} + +/* + * Return the first label of a CONF_SECTION + */ + +char const *cf_section_name1(CONF_SECTION const *cs) +{ + return (cs ? cs->name1 : NULL); +} + +/* + * Return the second label of a CONF_SECTION + */ + +char const *cf_section_name2(CONF_SECTION const *cs) +{ + return (cs ? cs->name2 : NULL); +} + +/** Return name2 if set, else name1 + * + */ +char const *cf_section_name(CONF_SECTION const *cs) +{ + char const *name; + + name = cf_section_name2(cs); + if (name) return name; + + return cf_section_name1(cs); +} + +/* + * Find a value in a CONF_SECTION + */ +char const *cf_section_value_find(CONF_SECTION const *cs, char const *attr) +{ + CONF_PAIR *cp; + + cp = cf_pair_find(cs, attr); + + return (cp ? cp->value : NULL); +} + + +CONF_SECTION *cf_section_find_name2(CONF_SECTION const *cs, + char const *name1, char const *name2) +{ + char const *their2; + CONF_ITEM const *ci; + + if (!cs || !name1) return NULL; + + for (ci = &(cs->item); ci; ci = ci->next) { + if (ci->type != CONF_ITEM_SECTION) + continue; + + if (strcmp(cf_item_to_section(ci)->name1, name1) != 0) { + continue; + } + + their2 = cf_item_to_section(ci)->name2; + + if ((!name2 && !their2) || + (name2 && their2 && (strcmp(name2, their2) == 0))) { + return cf_item_to_section(ci); + } + } + + return NULL; +} + +/** Find a pair with a name matching attr, after specified pair. + * + * @param cs to search in. + * @param pair to search from (may be NULL). + * @param attr to find (may be NULL in which case any attribute matches). + * @return the next matching CONF_PAIR or NULL if none matched. + */ +CONF_PAIR *cf_pair_find_next(CONF_SECTION const *cs, + CONF_PAIR const *pair, char const *attr) +{ + CONF_ITEM *ci; + + if (!cs) return NULL; + + /* + * If pair is NULL and we're trying to find a specific + * attribute this must be a first time run. + * + * Find the pair with correct name. + */ + if (!pair && attr) return cf_pair_find(cs, attr); + + /* + * Start searching from the next child, or from the head + * of the list of children (if no pair was provided). + */ + for (ci = pair ? pair->item.next : cs->children; + ci; + ci = ci->next) { + if (ci->type != CONF_ITEM_PAIR) continue; + + if (!attr || strcmp(cf_item_to_pair(ci)->attr, attr) == 0) break; + } + + return cf_item_to_pair(ci); +} + +/* + * Find a CONF_SECTION, or return the root if name is NULL + */ + +CONF_SECTION *cf_section_find(char const *name) +{ + if (name) + return cf_section_sub_find(root_config, name); + else + return root_config; +} + +/** Find a sub-section in a section + * + * This finds ANY section having the same first name. + * The second name is ignored. + */ +CONF_SECTION *cf_section_sub_find(CONF_SECTION const *cs, char const *name) +{ + CONF_SECTION mycs; + + if (!cs || !name) return NULL; /* can't find an un-named section */ + + /* + * No sub-sections have been defined, so none exist. + */ + if (!cs->section_tree) return NULL; + + mycs.name1 = name; + mycs.name2 = NULL; + return rbtree_finddata(cs->section_tree, &mycs); +} + + +/** Find a CONF_SECTION with both names. + * + */ +CONF_SECTION *cf_section_sub_find_name2(CONF_SECTION const *cs, + char const *name1, char const *name2) +{ + CONF_ITEM *ci; + + if (!cs) cs = root_config; + if (!cs) return NULL; + + if (name1) { + CONF_SECTION mycs, *master_cs; + + if (!cs->section_tree) return NULL; + + mycs.name1 = name1; + mycs.name2 = name2; + + master_cs = rbtree_finddata(cs->section_tree, &mycs); + if (!master_cs) return NULL; + + /* + * Look it up in the name2 tree. If it's there, + * return it. + */ + if (master_cs->name2_tree) { + CONF_SECTION *subcs; + + subcs = rbtree_finddata(master_cs->name2_tree, &mycs); + if (subcs) return subcs; + } + + /* + * We don't insert ourselves into the name2 tree. + * So if there's nothing in the name2 tree, maybe + * *we* are the answer. + */ + if (!master_cs->name2 && name2) return NULL; + if (master_cs->name2 && !name2) return NULL; + if (!master_cs->name2 && !name2) return master_cs; + + if (strcmp(master_cs->name2, name2) == 0) { + return master_cs; + } + + return NULL; + } + + /* + * Else do it the old-fashioned way. + */ + for (ci = cs->children; ci; ci = ci->next) { + CONF_SECTION *subcs; + + if (ci->type != CONF_ITEM_SECTION) + continue; + + subcs = cf_item_to_section(ci); + if (!subcs->name2) { + if (strcmp(subcs->name1, name2) == 0) break; + } else { + if (strcmp(subcs->name2, name2) == 0) break; + } + } + + return cf_item_to_section(ci); +} + +/* + * Return the next subsection after a CONF_SECTION + * with a certain name1 (char *name1). If the requested + * name1 is NULL, any name1 matches. + */ + +CONF_SECTION *cf_subsection_find_next(CONF_SECTION const *section, + CONF_SECTION const *subsection, + char const *name1) +{ + CONF_ITEM *ci; + + if (!section) return NULL; + + /* + * If subsection is NULL this must be a first time run + * Find the subsection with correct name + */ + + if (!subsection) { + ci = section->children; + } else { + ci = subsection->item.next; + } + + for (; ci; ci = ci->next) { + if (ci->type != CONF_ITEM_SECTION) + continue; + if ((name1 == NULL) || + (strcmp(cf_item_to_section(ci)->name1, name1) == 0)) + break; + } + + return cf_item_to_section(ci); +} + + +/* + * Return the next section after a CONF_SECTION + * with a certain name1 (char *name1). If the requested + * name1 is NULL, any name1 matches. + */ + +CONF_SECTION *cf_section_find_next(CONF_SECTION const *section, + CONF_SECTION const *subsection, + char const *name1) +{ + if (!section) return NULL; + + if (!section->item.parent) return NULL; + + return cf_subsection_find_next(section->item.parent, subsection, name1); +} + +/** Return the next item after a CONF_ITEM. + * + */ +CONF_ITEM *cf_item_find_next(CONF_SECTION const *section, CONF_ITEM const *item) +{ + if (!section) return NULL; + + /* + * If item is NULL this must be a first time run + * Return the first item + */ + if (item == NULL) { + return section->children; + } else { + return item->next; + } +} + +static void _pair_count(int *count, CONF_SECTION const *cs) +{ + CONF_ITEM const *ci; + + for (ci = cf_item_find_next(cs, NULL); + ci != NULL; + ci = cf_item_find_next(cs, ci)) { + + if (cf_item_is_section(ci)) { + _pair_count(count, cf_item_to_section(ci)); + continue; + } + + (*count)++; + } +} + +/** Count the number of conf pairs beneath a section + * + * @param[in] cs to search for items in. + * @return number of pairs nested within section. + */ +int cf_pair_count(CONF_SECTION const *cs) +{ + int count = 0; + + _pair_count(&count, cs); + + return count; +} + +CONF_SECTION *cf_item_parent(CONF_ITEM const *ci) +{ + if (!ci) return NULL; + + return ci->parent; +} + +int cf_section_lineno(CONF_SECTION const *section) +{ + return section->item.lineno; +} + +char const *cf_pair_filename(CONF_PAIR const *pair) +{ + return pair->item.filename; +} + +char const *cf_section_filename(CONF_SECTION const *section) +{ + return section->item.filename; +} + +int cf_pair_lineno(CONF_PAIR const *pair) +{ + return pair->item.lineno; +} + +bool cf_item_is_section(CONF_ITEM const *item) +{ + return item->type == CONF_ITEM_SECTION; +} + +bool cf_item_is_pair(CONF_ITEM const *item) +{ + return item->type == CONF_ITEM_PAIR; +} + +bool cf_item_is_data(CONF_ITEM const *item) +{ + return item->type == CONF_ITEM_DATA; +} + +static CONF_DATA *cf_data_alloc(CONF_SECTION *parent, char const *name, + void *data, void (*data_free)(void *)) +{ + CONF_DATA *cd; + + cd = talloc_zero(parent, CONF_DATA); + if (!cd) return NULL; + + cd->item.type = CONF_ITEM_DATA; + cd->item.parent = parent; + cd->name = talloc_typed_strdup(cd, name); + if (!cd->name) { + talloc_free(cd); + return NULL; + } + + cd->data = data; + cd->free = data_free; + + if (cd->free) { + talloc_set_destructor(cd, _cf_data_free); + } + + return cd; +} + +static void *cf_data_find_internal(CONF_SECTION const *cs, char const *name, int flag) +{ + if (!cs || !name) return NULL; + + /* + * Find the name in the tree, for speed. + */ + if (cs->data_tree) { + CONF_DATA mycd; + + mycd.name = name; + mycd.flag = flag; + return rbtree_finddata(cs->data_tree, &mycd); + } + + return NULL; +} + +/* + * Find data from a particular section. + */ +void *cf_data_find(CONF_SECTION const *cs, char const *name) +{ + CONF_DATA *cd = cf_data_find_internal(cs, name, 0); + + if (cd) return cd->data; + return NULL; +} + + +/* + * Add named data to a configuration section. + */ +static int cf_data_add_internal(CONF_SECTION *cs, char const *name, + void *data, void (*data_free)(void *), + int flag) +{ + CONF_DATA *cd; + + if (!cs || !name) return -1; + + /* + * Already exists. Can't add it. + */ + if (cf_data_find_internal(cs, name, flag) != NULL) return -1; + + cd = cf_data_alloc(cs, name, data, data_free); + if (!cd) return -1; + cd->flag = flag; + + cf_item_add(cs, cf_data_to_item(cd)); + + return 0; +} + +/* + * Add named data to a configuration section. + */ +int cf_data_add(CONF_SECTION *cs, char const *name, + void *data, void (*data_free)(void *)) +{ + return cf_data_add_internal(cs, name, data, data_free, 0); +} + +/** Remove named data from a configuration section + * + */ +void *cf_data_remove(CONF_SECTION *cs, char const *name) +{ + CONF_DATA mycd; + CONF_DATA *cd; + CONF_ITEM *ci, *it; + void *data; + + if (!cs || !name) return NULL; + if (!cs->data_tree) return NULL; + + /* + * Find the name in the tree, for speed. + */ + mycd.name = name; + mycd.flag = 0; + cd = rbtree_finddata(cs->data_tree, &mycd); + if (!cd) return NULL; + + ci = cf_data_to_item(cd); + if (cs->children == ci) { + cs->children = ci->next; + if (cs->tail == ci) cs->tail = NULL; + } else { + for (it = cs->children; it; it = it->next) { + if (it->next == ci) { + it->next = ci->next; + if (cs->tail == ci) cs->tail = it; + break; + } + } + } + + talloc_set_destructor(cd, NULL); /* Disarm the destructor */ + rbtree_deletebydata(cs->data_tree, &mycd); + + data = cd->data; + talloc_free(cd); + + return data; +} + +/* + * This is here to make the rest of the code easier to read. It + * ties conffile.c to log.c, but it means we don't have to + * pollute every other function with the knowledge of the + * configuration internals. + */ +void cf_log_err(CONF_ITEM const *ci, char const *fmt, ...) +{ + va_list ap; + char buffer[256]; + + va_start(ap, fmt); + vsnprintf(buffer, sizeof(buffer), fmt, ap); + va_end(ap); + + if (ci) { + ERROR("%s[%d]: %s", + ci->filename ? ci->filename : "unknown", + ci->lineno ? ci->lineno : 0, + buffer); + } else { + ERROR("<unknown>[*]: %s", buffer); + } +} + +void cf_log_err_cs(CONF_SECTION const *cs, char const *fmt, ...) +{ + va_list ap; + char buffer[256]; + + va_start(ap, fmt); + vsnprintf(buffer, sizeof(buffer), fmt, ap); + va_end(ap); + + rad_assert(cs != NULL); + + ERROR("%s[%d]: %s", + cs->item.filename ? cs->item.filename : "unknown", + cs->item.lineno ? cs->item.lineno : 0, + buffer); +} + +void cf_log_err_cp(CONF_PAIR const *cp, char const *fmt, ...) +{ + va_list ap; + char buffer[256]; + + va_start(ap, fmt); + vsnprintf(buffer, sizeof(buffer), fmt, ap); + va_end(ap); + + rad_assert(cp != NULL); + + ERROR("%s[%d]: %s", + cp->item.filename ? cp->item.filename : "unknown", + cp->item.lineno ? cp->item.lineno : 0, + buffer); +} + +void cf_log_info(CONF_SECTION const *cs, char const *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + if ((rad_debug_lvl > 1) && cs) vradlog(L_DBG, fmt, ap); + va_end(ap); +} + +/* + * Wrapper to simplify the code. + */ +void cf_log_module(CONF_SECTION const *cs, char const *fmt, ...) +{ + va_list ap; + char buffer[256]; + + va_start(ap, fmt); + if (rad_debug_lvl > 1 && cs) { + vsnprintf(buffer, sizeof(buffer), fmt, ap); + + DEBUG("%.*s# %s", cs->depth, parse_spaces, buffer); + } + va_end(ap); +} + +const CONF_PARSER *cf_section_parse_table(CONF_SECTION *cs) +{ + if (!cs) return NULL; + + return cs->variables; +} + +/* + * For "switch" and "case" statements. + */ +FR_TOKEN cf_section_name2_type(CONF_SECTION const *cs) +{ + if (!cs) return T_INVALID; + + return cs->name2_type; +} diff --git a/src/main/connection.c b/src/main/connection.c new file mode 100644 index 0000000..7ae4a2a --- /dev/null +++ b/src/main/connection.c @@ -0,0 +1,1520 @@ +/* + * This program 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 + */ + +/** + * @file connection.c + * @brief Handle pools of connections (threads, sockets, etc.) + * @note This API must be used by all modules in the public distribution that + * maintain pools of connections. + * + * @copyright 2012 The FreeRADIUS server project + * @copyright 2012 Alan DeKok <aland@deployingradius.com> + */ +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/heap.h> +#include <freeradius-devel/modpriv.h> +#include <freeradius-devel/rad_assert.h> + +typedef struct fr_connection fr_connection_t; + +static int fr_connection_pool_check(fr_connection_pool_t *pool); + +#ifndef NDEBUG +#ifdef HAVE_PTHREAD_H +/* #define PTHREAD_DEBUG (1) */ +#endif +#endif + +/* + * We don't need to pollute the logs with "open / close" + * connection information. Instead, only print these messages + * when debugging. + */ +#undef INFO +#define INFO(fmt, ...) if (rad_debug_lvl) radlog(L_INFO, fmt, ## __VA_ARGS__) + +/** An individual connection within the connection pool + * + * Defines connection counters, timestamps, and holds a pointer to the + * connection handle itself. + * + * @see fr_connection_pool_t + */ +struct fr_connection { + fr_connection_t *prev; //!< Previous connection in list. + fr_connection_t *next; //!< Next connection in list. + + time_t created; //!< Time connection was created. + struct timeval last_reserved; //!< Last time the connection was reserved. + + struct timeval last_released; //!< Time the connection was released. + + uint32_t num_uses; //!< Number of times the connection has been reserved. + uint64_t number; //!< Unique ID assigned when the connection is created, + //!< these will monotonically increase over the + //!< lifetime of the connection pool. + void *connection; //!< Pointer to whatever the module uses for a connection + //!< handle. + bool in_use; //!< Whether the connection is currently reserved. + + int heap; //!< For the next connection heap. + +#ifdef PTHREAD_DEBUG + pthread_t pthread_id; //!< When 'in_use == true'. +#endif +}; + +/** A connection pool + * + * Defines the configuration of the connection pool, all the counters and + * timestamps related to the connection pool, the mutex that stops multiple + * threads leaving the pool in an inconsistent state, and the callbacks + * required to open, close and check the status of connections within the pool. + * + * @see fr_connection + */ +struct fr_connection_pool_t { + int ref; //!< Reference counter to prevent connection + //!< pool being freed multiple times. + uint32_t start; //!< Number of initial connections. + uint32_t min; //!< Minimum number of concurrent connections to keep open. + uint32_t max; //!< Maximum number of concurrent connections to allow. + uint32_t spare; //!< Number of spare connections to try. + uint32_t pending; //!< Number of pending open connections. + uint32_t retry_delay; //!< seconds to delay re-open after a failed open. + uint32_t max_retries; //!< Maximum number of retries to attempt for any given + //!< operation (e.g. query or bind) + uint32_t cleanup_interval; //!< Initial timer for how often we sweep the pool + //!< for free connections. (0 is infinite). + int delay_interval; //!< When we next do a cleanup. Initialized to + //!< cleanup_interval, and increase from there based + //!< on the delay. + int next_delay; //!< The next delay time. cleanup. Initialized to + //!< cleanup_interval, and decays from there. + uint64_t max_uses; //!< Maximum number of times a connection can be used + //!< before being closed. + uint32_t lifetime; //!< How long a connection can be open before being + //!< closed (irrespective of whether it's idle or not). + uint32_t idle_timeout; //!< How long a connection can be idle before + //!< being closed. + + uint32_t max_pending; //!< Max number of connections to open. + + bool spread; //!< If true we spread requests over the connections, + //!< using the connection released longest ago, first. + + fr_connection_pool_stats_t stats; //!< various statistics + + fr_heap_t *heap; //!< For the next connection heap + + fr_connection_t *head; //!< Start of the connection list. + fr_connection_t *tail; //!< End of the connection list. + +#ifdef HAVE_PTHREAD_H + pthread_mutex_t mutex; //!< Mutex used to keep consistent state when making + //!< modifications in threaded mode. +#endif + + CONF_SECTION *cs; //!< Configuration section holding the section of parsed + //!< config file that relates to this pool. + void *opaque; //!< Pointer to context data that will be passed to callbacks. + + char const *log_prefix; //!< Log prefix to prepend to all log messages created + //!< by the connection pool code. + + char const *trigger_prefix; //!< Prefix to prepend to names of all triggers + //!< fired by the connection pool code. + + fr_connection_create_t create; //!< Function used to create new connections. + fr_connection_alive_t alive; //!< Function used to check status of connections. +}; + +#ifndef HAVE_PTHREAD_H +# define pthread_mutex_lock(_x) +# define pthread_mutex_unlock(_x) +#endif + +static const CONF_PARSER connection_config[] = { + { "start", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_connection_pool_t, start), "5" }, + { "min", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_connection_pool_t, min), "5" }, + { "max", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_connection_pool_t, max), "10" }, + { "spare", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_connection_pool_t, spare), "3" }, + { "uses", FR_CONF_OFFSET(PW_TYPE_INTEGER64, fr_connection_pool_t, max_uses), "0" }, + { "lifetime", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_connection_pool_t, lifetime), "0" }, + { "cleanup_delay", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_connection_pool_t, cleanup_interval), NULL}, + { "cleanup_interval", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_connection_pool_t, cleanup_interval), "30" }, + { "idle_timeout", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_connection_pool_t, idle_timeout), "60" }, + { "retry_delay", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_connection_pool_t, retry_delay), "1" }, + { "max_retries", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_connection_pool_t, max_retries), "5" }, + { "spread", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_connection_pool_t, spread), "no" }, + CONF_PARSER_TERMINATOR +}; + +/** Order connections by reserved most recently + */ +static int last_reserved_cmp(void const *one, void const *two) +{ + fr_connection_t const *a = one; + fr_connection_t const *b = two; + + if (a->last_reserved.tv_sec < b->last_reserved.tv_sec) return -1; + if (a->last_reserved.tv_sec > b->last_reserved.tv_sec) return +1; + + if (a->last_reserved.tv_usec < b->last_reserved.tv_usec) return -1; + if (a->last_reserved.tv_usec > b->last_reserved.tv_usec) return +1; + + return 0; +} + +/** Order connections by released longest ago + */ +static int last_released_cmp(void const *one, void const *two) +{ + fr_connection_t const *a = one; + fr_connection_t const *b = two; + + if (b->last_released.tv_sec < a->last_released.tv_sec) return -1; + if (b->last_released.tv_sec > a->last_released.tv_sec) return +1; + + if (b->last_released.tv_usec < a->last_released.tv_usec) return -1; + if (b->last_released.tv_usec > a->last_released.tv_usec) return +1; + + return 0; +} + +/** Removes a connection from the connection list + * + * @note Must be called with the mutex held. + * + * @param[in,out] pool to modify. + * @param[in] this Connection to delete. + */ +static void fr_connection_unlink(fr_connection_pool_t *pool, fr_connection_t *this) +{ + if (this->prev) { + rad_assert(pool->head != this); + this->prev->next = this->next; + } else { + rad_assert(pool->head == this); + pool->head = this->next; + } + if (this->next) { + rad_assert(pool->tail != this); + this->next->prev = this->prev; + } else { + rad_assert(pool->tail == this); + pool->tail = this->prev; + } + + this->prev = this->next = NULL; +} + +/** Adds a connection to the head of the connection list + * + * @note Must be called with the mutex held. + * + * @param[in,out] pool to modify. + * @param[in] this Connection to add. + */ +static void fr_connection_link_head(fr_connection_pool_t *pool, fr_connection_t *this) +{ + rad_assert(pool != NULL); + rad_assert(this != NULL); + rad_assert(pool->head != this); + rad_assert(pool->tail != this); + + if (pool->head) { + pool->head->prev = this; + } + + this->next = pool->head; + this->prev = NULL; + pool->head = this; + if (!pool->tail) { + rad_assert(this->next == NULL); + pool->tail = this; + } else { + rad_assert(this->next != NULL); + } +} + +/** Send a connection pool trigger. + * + * @param[in] pool to send trigger for. + * @param[in] name_suffix trigger name suffix. + */ +static void fr_connection_exec_trigger(fr_connection_pool_t *pool, char const *name_suffix) +{ + char name[64]; + rad_assert(pool != NULL); + rad_assert(name_suffix != NULL); + snprintf(name, sizeof(name), "%s%s", pool->trigger_prefix, name_suffix); + exec_trigger(NULL, pool->cs, name, true); +} + +/** Find a connection handle in the connection list + * + * Walks over the list of connections searching for a specified connection + * handle and returns the first connection that contains that pointer. + * + * @note Will lock mutex and only release mutex if connection handle + * is not found, so will usually return will mutex held. + * @note Must be called with the mutex free. + * + * @param[in] pool to search in. + * @param[in] conn handle to search for. + * @return + * - Connection containing the specified handle. + * - NULL if non if connection was found. + */ +static fr_connection_t *fr_connection_find(fr_connection_pool_t *pool, void *conn) +{ + fr_connection_t *this; + + if (!pool || !conn) return NULL; + + pthread_mutex_lock(&pool->mutex); + + /* + * FIXME: This loop could be avoided if we passed a 'void + * **connection' instead. We could use "offsetof" in + * order to find top of the parent structure. + */ + for (this = pool->head; this != NULL; this = this->next) { + if (this->connection == conn) { +#ifdef PTHREAD_DEBUG + pthread_t pthread_id; + + pthread_id = pthread_self(); + rad_assert(pthread_equal(this->pthread_id, pthread_id) != 0); +#endif + + rad_assert(this->in_use == true); + return this; + } + } + + pthread_mutex_unlock(&pool->mutex); + return NULL; +} + +/** Spawns a new connection + * + * Spawns a new connection using the create callback, and returns it for + * adding to the connection list. + * + * @note Will call the 'open' trigger. + * @note Must be called with the mutex free. + * + * @param[in] pool to modify. + * @param[in] now Current time. + * @param[in] in_use whether the new connection should be "in_use" or not + * @return + * - New connection struct. + * - NULL on error. + */ +static fr_connection_t *fr_connection_spawn(fr_connection_pool_t *pool, time_t now, bool in_use) +{ + uint64_t number; + uint32_t max_pending; + TALLOC_CTX *ctx; + + fr_connection_t *this; + void *conn; + + rad_assert(pool != NULL); + + /* + * If we have NO connections, and we've previously failed + * opening connections, don't open multiple connections until + * we successfully open at least one. + */ + if ((pool->stats.num == 0) && pool->pending && pool->stats.last_failed) return NULL; + + pthread_mutex_lock(&pool->mutex); + rad_assert(pool->stats.num <= pool->max); + + /* + * Don't spawn too many connections at the same time. + */ + if ((pool->stats.num + pool->pending) >= pool->max) { + pthread_mutex_unlock(&pool->mutex); + + ERROR("%s: Cannot open new connection, already at max", pool->log_prefix); + return NULL; + } + + /* + * If the last attempt failed, wait a bit before + * retrying. + */ + if (pool->stats.last_failed && ((pool->stats.last_failed + pool->retry_delay) > now)) { + bool complain = false; + + if (pool->stats.last_throttled != now) { + complain = true; + + pool->stats.last_throttled = now; + } + + pthread_mutex_unlock(&pool->mutex); + + if (!RATE_LIMIT_ENABLED || complain) { + ERROR("%s: Last connection attempt failed, waiting %d seconds before retrying", + pool->log_prefix, pool->retry_delay); + } + + return NULL; + } + + /* + * We limit the rate of new connections after a failed attempt. + */ + if (pool->pending > pool->max_pending) { + pthread_mutex_unlock(&pool->mutex); + RATE_LIMIT(WARN("%s: Cannot open a new connection due to rate limit after failure", + pool->log_prefix)); + return NULL; + } + + pool->pending++; + number = pool->stats.opened++; + + /* + * Unlock the mutex while we try to open a new + * connection. If there are issues with the back-end, + * opening a new connection may take a LONG time. In + * that case, we want the other connections to continue + * to be used. + */ + pthread_mutex_unlock(&pool->mutex); + + /* + * The true value for max_pending is the smaller of + * free connection slots, or pool->max_pending. + */ + max_pending = (pool->max - pool->stats.num); + if (pool->max_pending < max_pending) max_pending = pool->max_pending; + INFO("%s: Opening additional connection (%" PRIu64 "), %u of %u pending slots used", + pool->log_prefix, number, pool->pending, max_pending); + + /* + * Allocate a new top level ctx for the create callback + * to hang its memory off of. + */ + ctx = talloc_init("fr_connection_ctx"); + if (!ctx) return NULL; + + /* + * This may take a long time, which prevents other + * threads from releasing connections. We don't care + * about other threads opening new connections, as we + * already have no free connections. + */ + conn = pool->create(ctx, pool->opaque); + if (!conn) { + ERROR("%s: Opening connection failed (%" PRIu64 ")", pool->log_prefix, number); + + pool->stats.last_failed = now; + pthread_mutex_lock(&pool->mutex); + pool->max_pending = 1; + pool->pending--; + pool->stats.failed++; + pthread_mutex_unlock(&pool->mutex); + + talloc_free(ctx); + + return NULL; + } + + /* + * And lock the mutex again while we link the new + * connection back into the pool. + */ + pthread_mutex_lock(&pool->mutex); + + this = talloc_zero(pool, fr_connection_t); + if (!this) { + pthread_mutex_unlock(&pool->mutex); + talloc_free(ctx); + + return NULL; + } + fr_link_talloc_ctx_free(this, ctx); + + this->created = now; + this->connection = conn; + this->in_use = in_use; + + this->number = number; + gettimeofday(&this->last_reserved, NULL); + this->last_released = this->last_reserved; + + /* + * The connection pool is starting up. Insert the + * connection into the heap. + */ + if (!in_use) fr_heap_insert(pool->heap, this); + + fr_connection_link_head(pool, this); + + /* + * Do NOT insert the connection into the heap. That's + * done when the connection is released. + */ + + pool->stats.num++; + + rad_assert(pool->pending > 0); + pool->pending--; + + /* + * We've successfully opened one more connection. Allow + * more connections to open in parallel. + */ + if (pool->max_pending < pool->max) pool->max_pending++; + + pool->stats.last_opened = time(NULL); + pool->delay_interval = pool->cleanup_interval; + pool->next_delay = pool->cleanup_interval; + pool->stats.last_failed = 0; + + pthread_mutex_unlock(&pool->mutex); + + fr_connection_exec_trigger(pool, "open"); + + return this; +} + +/** Close an existing connection. + * + * Removes the connection from the list, calls the delete callback to close + * the connection, then frees memory allocated to the connection. + * + * @note Will call the 'close' trigger. + * @note Must be called with the mutex held. + * + * @param[in,out] pool to modify. + * @param[in] this Connection to delete. + * @param[in] reason to close the connection + * @param[in] msg optional message + */ +static void fr_connection_close_internal(fr_connection_pool_t *pool, fr_connection_t *this, + char const *reason, char const *msg) +{ + if (!msg) { + INFO("%s: %s (%" PRIu64 ")", pool->log_prefix, reason, this->number); + } else { + INFO("%s: %s (%" PRIu64 ") - %s", pool->log_prefix, reason, this->number, msg); + } + + + /* + * If it's in use, release it. + */ + if (this->in_use) { +#ifdef PTHREAD_DEBUG + pthread_t pthread_id = pthread_self(); + rad_assert(pthread_equal(this->pthread_id, pthread_id) != 0); +#endif + + this->in_use = false; + + rad_assert(pool->stats.active != 0); + pool->stats.active--; + + } else { + /* + * Connection isn't used, remove it from the heap. + */ + fr_heap_extract(pool->heap, this); + } + + fr_connection_exec_trigger(pool, "close"); + + fr_connection_unlink(pool, this); + + rad_assert(pool->stats.num > 0); + pool->stats.num--; + pool->stats.closed++; + pool->stats.last_closed = time(NULL); + talloc_free(this); +} + +/** Check whether a connection needs to be removed from the pool + * + * Will verify that the connection is within idle_timeout, max_uses, and + * lifetime values. If it is not, the connection will be closed. + * + * @note Will only close connections not in use. + * @note Must be called with the mutex held. + * + * @param[in,out] pool to modify. + * @param[in,out] this Connection to manage. + * @param[in] now Current time. + * @param[in] get whether we want to get a connection + * @return + * - 0 if connection was closed. + * - 1 if connection handle was left open. + */ +static int fr_connection_manage(fr_connection_pool_t *pool, + fr_connection_t *this, + time_t now, bool get) +{ + rad_assert(pool != NULL); + rad_assert(this != NULL); + char const *reason = "Closing expired connection"; + char const *msg = NULL; + + /* + * Don't terminate in-use connections + */ + if (this->in_use) return 1; + + if ((pool->max_uses > 0) && + (this->num_uses >= pool->max_uses)) { + msg = "Hit max_uses limit"; + + do_delete: + if (pool->stats.num <= pool->min) { + DEBUG("%s: You probably need to lower \"min\"", pool->log_prefix); + } + fr_connection_close_internal(pool, this, reason, msg); + return 0; + } + + if ((pool->lifetime > 0) && + ((this->created + pool->lifetime) < now)) { + msg = "Hit lifetime limit"; + goto do_delete; + } + + /* + * The connection WAS idle, but the caller is interested + * in getting a new one. Instead of closing the old one + * and opening a new one, we just return the old one. + */ + if (get) return 1; + + if ((pool->idle_timeout > 0) && + ((this->last_released.tv_sec + pool->idle_timeout) < now)) { + msg = "Hit idle_timeout limit"; + goto do_delete; + } + + return 1; +} + + +/** Check whether any connections need to be removed from the pool + * + * Maintains the number of connections in the pool as per the configuration + * parameters for the connection pool. + * + * @note Will only run checks the first time it's called in a given second, + * to throttle connection spawning/closing. + * @note Will only close connections not in use. + * @note Must be called with the mutex held, will release mutex before + * returning. + * + * @param[in,out] pool to manage. + * @return 1 + */ +static int fr_connection_pool_check(fr_connection_pool_t *pool) +{ + uint32_t num, spare; + time_t now = time(NULL); + fr_connection_t *this, *next; + + if (pool->stats.last_checked == now) { + pthread_mutex_unlock(&pool->mutex); + return 1; + } + + /* + * Get "real" number of connections, and count pending + * connections as spare. + */ + num = pool->stats.num + pool->pending; + spare = pool->pending + (pool->stats.num - pool->stats.active); + + /* + * The other end can close connections. If so, we'll + * have fewer than "min". When that happens, open more + * connections to enforce "min". + * + * The code for spawning connections enforces that + * num + pending <= max. + */ + if (num < pool->min) { + INFO("Need %u more connections to reach min connections (%i)", pool->min - num, pool->min); + goto add_connection; + } + + /* + * On the odd chance that we've opened too many + * connections, take care of that. + */ + if (num > pool->max) { + /* + * Pending connections don't get closed as "spare". + */ + if (pool->pending > 0) goto manage_connections; + + /* + * Otherwise close one of the connections to + * bring us down to "max". + */ + goto close_connection; + } + + /* + * Now that we've enforced min/max connections, try to + * keep the "spare" connections at the correct number. + */ + + /* + * Nothing to do? Go check all of the connections for + * timeouts, etc. + */ + if (spare == pool->spare) goto manage_connections; + + /* + * Too many spare connections, delete some. + */ + if (spare > pool->spare) { + fr_connection_t *found; + + /* + * Pending connections don't get closed as "spare". + */ + if (pool->pending > 0) goto manage_connections; + + /* + * Don't close too many connections, even they + * are spare. + */ + if (num <= pool->min) goto manage_connections; + + /* + * Too many spares, go close one. + */ + + close_connection: + /* + * Don't close connections too often, in order to + * prevent flapping. + */ + if (now < (pool->stats.last_opened + pool->delay_interval)) goto manage_connections; + + /* + * Find a connection to close. + */ + found = NULL; + for (this = pool->tail; this != NULL; this = this->prev) { + if (this->in_use) continue; + + if (!found || + timercmp(&this->last_reserved, &found->last_reserved, <)) { + found = this; + } + } + + rad_assert(found != NULL); + + fr_connection_close_internal(pool, found, "Closing connection", "Too many unused connections."); + + /* + * Decrease the delay for the next time we clean + * up. + */ + pool->next_delay >>= 1; + if (pool->next_delay == 0) pool->next_delay = 1; + pool->delay_interval += pool->next_delay; + + goto manage_connections; + } + + /* + * Too few connections, open some more. + */ + if (spare < pool->spare) { + /* + * Don't open too many pending connections. + */ + if (pool->pending >= pool->max_pending) goto manage_connections; + + /* + * Don't open too many connections, even if we + * need more spares. + */ + if (num >= pool->max) goto manage_connections; + + /* + * Too few spares, go add one. + */ + + add_connection: + INFO("Need more connections to reach %i spares", pool->spare); + + /* + * Only try to open spares if we're not already attempting to open + * a connection. Avoids spurious log messages. + */ + pthread_mutex_unlock(&pool->mutex); + fr_connection_spawn(pool, now, false); /* ignore return code */ + pthread_mutex_lock(&pool->mutex); + goto manage_connections; + } + + /* + * Pass over all of the connections in the pool, limiting + * lifetime, idle time, max requests, etc. + */ +manage_connections: + for (this = pool->head; this != NULL; this = next) { + next = this->next; + fr_connection_manage(pool, this, now, false); + } + + pool->stats.last_checked = now; + pthread_mutex_unlock(&pool->mutex); + + return 1; +} + +/** Get a connection from the connection pool + * + * @note Must be called with the mutex free. + * + * @param[in,out] pool to reserve the connection from. + * @param[in] spawn whether to spawn a new connection + * @return + * - A pointer to the connection handle. + * - NULL on error. + */ +static void *fr_connection_get_internal(fr_connection_pool_t *pool, bool spawn) +{ + time_t now; + fr_connection_t *this; + + if (!pool) return NULL; + + /* + * Allow CTRL-C to kill the server in debugging mode. + */ + if (main_config.exiting) return NULL; + +#ifdef HAVE_PTHREAD_H + if (spawn) pthread_mutex_lock(&pool->mutex); +#endif + + now = time(NULL); + + /* + * Grab the link with the lowest latency, and check it + * for limits. If "connection manage" says the link is + * no longer usable, go grab another one. + */ + do { + this = fr_heap_peek(pool->heap); + if (!this) break; + + fr_assert(!this->in_use); + } while (!fr_connection_manage(pool, this, now, true)); + + /* + * We have a working connection. Extract it from the + * heap and use it. + */ + if (this) { + fr_heap_extract(pool->heap, this); + goto do_return; + } + + /* + * We were asked to avoid spawning a new connection, by + * fr_connection_reconnect_internal(). So we just return + * here. + */ + if (!spawn) return NULL; + + if (pool->stats.num == pool->max) { + bool complain = false; + + /* + * Rate-limit complaints. + */ + if (pool->stats.last_at_max != now) { + complain = true; + pool->stats.last_at_max = now; + } + + pthread_mutex_unlock(&pool->mutex); + + if (!RATE_LIMIT_ENABLED || complain) { + ERROR("%s: No connections available and at max connection limit", pool->log_prefix); + } + + return NULL; + } + + pthread_mutex_unlock(&pool->mutex); + + DEBUG("%s: %i of %u connections in use. You may need to increase \"spare\"", pool->log_prefix, + pool->stats.active, pool->stats.num); + this = fr_connection_spawn(pool, now, true); /* MY connection! */ + if (!this) return NULL; + + pthread_mutex_lock(&pool->mutex); + +do_return: + pool->stats.active++; + this->num_uses++; + gettimeofday(&this->last_reserved, NULL); + this->in_use = true; + +#ifdef PTHREAD_DEBUG + this->pthread_id = pthread_self(); +#endif + +#ifdef HAVE_PTHREAD_H + if (spawn) pthread_mutex_unlock(&pool->mutex); +#endif + + DEBUG("%s: Reserved connection (%" PRIu64 ")", pool->log_prefix, this->number); + + return this->connection; +} + +/** Reconnect a suspected inviable connection + * + * @note Must be called with the mutex held, will not release mutex. + * + * @see fr_connection_get + * @param[in,out] pool to reconnect the connection in. + * @param[in,out] conn to reconnect. + * @return new connection handle if successful else NULL. + */ +static fr_connection_t *fr_connection_reconnect_internal(fr_connection_pool_t *pool, fr_connection_t *conn) +{ + void *new_conn; + uint64_t conn_number; + TALLOC_CTX *ctx; + + conn_number = conn->number; + + /* + * Destroy any handles associated with the fr_connection_t + */ + talloc_free_children(conn); + + DEBUG("%s: Reconnecting (%" PRIu64 ")", pool->log_prefix, conn_number); + + /* + * Allocate a new top level ctx for the create callback + * to hang its memory off of. + */ + ctx = talloc_init("fr_connection_ctx"); + if (!ctx) return NULL; + fr_link_talloc_ctx_free(conn, ctx); + + new_conn = pool->create(ctx, pool->opaque); + if (!new_conn) { + /* + * We can't create a new connection, so close the current one. + */ + fr_connection_close_internal(pool, conn, "Closing connection", "Failed to reconnect"); + + /* + * Maybe there's a connection which is unused and + * available. If so, return it. + */ + new_conn = fr_connection_get_internal(pool, false); + if (new_conn) return new_conn; + + RATE_LIMIT(ERROR("%s: Failed to reconnect (%" PRIu64 "), no free connections are available", + pool->log_prefix, conn_number)); + + return NULL; + } + + fr_connection_exec_trigger(pool, "close"); + conn->connection = new_conn; + + return new_conn; +} + +/** Create a new connection pool + * + * Allocates structures used by the connection pool, initialises the various + * configuration options and counters, and sets the callback functions. + * + * Will also spawn the number of connections specified by the 'start' + * configuration options. + * + * @note Will call the 'start' trigger. + * + * @param[in] ctx Context to link pool's destruction to. + * @param[in] cs pool section. + * @param[in] opaque data pointer to pass to callbacks. + * @param[in] c Callback to create new connections. + * @param[in] a Callback to check the status of connections. + * @param[in] log_prefix prefix to prepend to all log messages. + * @param[in] trigger_prefix prefix to prepend to all trigger names. + * @return + * - New connection pool. + * - NULL on error. + */ +static fr_connection_pool_t *fr_connection_pool_init(TALLOC_CTX *ctx, + CONF_SECTION *cs, + void *opaque, + fr_connection_create_t c, + fr_connection_alive_t a, + char const *log_prefix, + char const *trigger_prefix) +{ + uint32_t i; + fr_connection_pool_t *pool; + fr_connection_t *this; + time_t now; + + if (!cs || !opaque || !c) return NULL; + + now = time(NULL); + + /* + * Pool is allocated in the NULL context as + * threads are likely to allocate memory + * beneath the pool. + */ + pool = talloc_zero(NULL, fr_connection_pool_t); + if (!pool) return NULL; + + /* + * Ensure the pool is freed at the same time + * as its parent. + */ + if (fr_link_talloc_ctx_free(ctx, pool) < 0) { + talloc_free(pool); + + return NULL; + } + + pool->cs = cs; + pool->opaque = opaque; + pool->create = c; + pool->alive = a; + + pool->head = pool->tail = NULL; + + /* + * We keep a heap of connections, sorted by the last time + * we STARTED using them. Newly opened connections + * aren't in the heap. They're only inserted in the list + * once they're released. + * + * We do "most recently started" instead of "most + * recently used", because MRU is done as most recently + * *released*. We want to order connections by + * responsiveness, and MRU prioritizes high latency + * connections. + * + * We want most recently *started*, which gives + * preference to low latency links, and pushes high + * latency links down in the priority heap. + * + * https://code.facebook.com/posts/1499322996995183/solving-the-mystery-of-link-imbalance-a-metastable-failure-state-at-scale/ + */ + if (!pool->spread) { + pool->heap = fr_heap_create(last_reserved_cmp, offsetof(fr_connection_t, heap)); + /* + * For some types of connections we need to used a different + * algorithm, because load balancing benefits are secondary + * to maintaining a cache of open connections. + * + * With libcurl's multihandle, connections can only be reused + * if all handles that make up the multhandle are done processing + * their requests. + * + * We can't tell when that's happened using libcurl, and even + * if we could, blocking until all servers had responded + * would have huge cost. + * + * The solution is to order the heap so that the connection that + * was released longest ago is at the top. + * + * That way we maximise time between connection use. + */ + } else { + pool->heap = fr_heap_create(last_released_cmp, offsetof(fr_connection_t, heap)); + } + if (!pool->heap) { + talloc_free(pool); + return NULL; + } + + pool->log_prefix = log_prefix ? talloc_typed_strdup(pool, log_prefix) : "core"; + pool->trigger_prefix = trigger_prefix ? talloc_typed_strdup(pool, trigger_prefix) : ""; + +#ifdef HAVE_PTHREAD_H + pthread_mutex_init(&pool->mutex, NULL); +#endif + + DEBUG("%s: Initialising connection pool", pool->log_prefix); + + if (cf_section_parse(cs, pool, connection_config) < 0) goto error; + + /* + * Some simple limits + */ + if (pool->max == 0) { + cf_log_err_cs(cs, "Cannot set 'max' to zero"); + goto error; + } + pool->max_pending = pool->max; /* can open all connections now */ + + if (pool->min > pool->max) { + cf_log_err_cs(cs, "Cannot set 'min' to more than 'max'"); + goto error; + } + + FR_INTEGER_BOUND_CHECK("max", pool->max, <=, 1024); + FR_INTEGER_BOUND_CHECK("start", pool->start, <=, pool->max); + FR_INTEGER_BOUND_CHECK("spare", pool->spare, <=, (pool->max - pool->min)); + + if (pool->lifetime > 0) { + FR_INTEGER_COND_CHECK("idle_timeout", pool->idle_timeout, (pool->idle_timeout <= pool->lifetime), 0); + } + + if (pool->idle_timeout > 0) { + FR_INTEGER_BOUND_CHECK("cleanup_interval", pool->cleanup_interval, <=, pool->idle_timeout); + } + + /* + * Don't open any connections. Instead, force the limits + * to only 1 connection. + * + */ + if (check_config) { + pool->start = pool->min = pool->max = 1; + return pool; + } + + /* + * Create all of the connections, unless the admin says + * not to. + */ + for (i = 0; i < pool->start; i++) { + this = fr_connection_spawn(pool, now, false); + if (!this) { + error: + fr_connection_pool_free(pool); + return NULL; + } + } + + fr_connection_exec_trigger(pool, "start"); + + return pool; +} + +/** Initialise a module specific connection pool + * + * @see fr_connection_pool_init + * + * @param[in] module section. + * @param[in] opaque data pointer to pass to callbacks. + * @param[in] c Callback to create new connections. + * @param[in] a Callback to check the status of connections. + * @param[in] log_prefix override, if NULL will be set automatically from the module CONF_SECTION. + * @return + * - New connection pool. + * - NULL on error. + */ +fr_connection_pool_t *fr_connection_pool_module_init(CONF_SECTION *module, + void *opaque, + fr_connection_create_t c, + fr_connection_alive_t a, + char const *log_prefix) +{ + CONF_SECTION *cs, *mycs; + char buff[128]; + char trigger_prefix[64]; + + fr_connection_pool_t *pool; + char const *cs_name1, *cs_name2; + + int ret; + +#define CONNECTION_POOL_CF_KEY "connection_pool" +#define parent_name(_x) cf_section_name(cf_item_parent(cf_section_to_item(_x))) + + cs_name1 = cf_section_name1(module); + cs_name2 = cf_section_name2(module); + if (!cs_name2) cs_name2 = cs_name1; + + snprintf(trigger_prefix, sizeof(trigger_prefix), "modules.%s.", cs_name1); + + if (!log_prefix) { + snprintf(buff, sizeof(buff), "rlm_%s (%s)", cs_name1, cs_name2); + log_prefix = buff; + } + + /* + * Get sibling's pool config section + */ + ret = find_module_sibling_section(&cs, module, "pool"); + switch (ret) { + case -1: + return NULL; + + case 1: + DEBUG4("%s: Using pool section from \"%s\"", log_prefix, parent_name(cs)); + break; + + case 0: + DEBUG4("%s: Using local pool section", log_prefix); + break; + } + + /* + * Get our pool config section + */ + mycs = cf_section_sub_find(module, "pool"); + if (!mycs) { + DEBUG4("%s: Adding pool section to config item \"%s\" to store pool references", log_prefix, + cf_section_name(module)); + + mycs = cf_section_alloc(module, "pool", NULL); + cf_section_add(module, mycs); + } + + /* + * Sibling didn't have a pool config section + * Use our own local pool. + */ + if (!cs) { + DEBUG4("%s: \"%s.pool\" section not found, using \"%s.pool\"", log_prefix, + parent_name(cs), parent_name(mycs)); + cs = mycs; + } + + /* + * If fr_connection_pool_init has already been called + * for this config section, reuse the previous instance. + * + * This allows modules to pass in the config sections + * they would like to use the connection pool from. + */ + pool = cf_data_find(cs, CONNECTION_POOL_CF_KEY); + if (!pool) { + DEBUG4("%s: No pool reference found for config item \"%s.pool\"", log_prefix, parent_name(cs)); + pool = fr_connection_pool_init(cs, cs, opaque, c, a, log_prefix, trigger_prefix); + if (!pool) return NULL; + + DEBUG4("%s: Adding pool reference %p to config item \"%s.pool\"", log_prefix, pool, parent_name(cs)); + cf_data_add(cs, CONNECTION_POOL_CF_KEY, pool, NULL); + return pool; + } + pool->ref++; + + DEBUG4("%s: Found pool reference %p in config item \"%s.pool\"", log_prefix, pool, parent_name(cs)); + + /* + * We're reusing pool data add it to our local config + * section. This allows other modules to transitively + * re-use a pool through this module. + */ + if (mycs != cs) { + DEBUG4("%s: Copying pool reference %p from config item \"%s.pool\" to config item \"%s.pool\"", + log_prefix, pool, parent_name(cs), parent_name(mycs)); + cf_data_add(mycs, CONNECTION_POOL_CF_KEY, pool, NULL); + } + + return pool; +} + +/** Get the number of connections currently in the pool + * + * @param pool to count connections for. + * @return the number of connections in the pool + */ +int fr_connection_pool_get_num(fr_connection_pool_t *pool) +{ + return pool->stats.num; +} + +/** Get the number of times an operation should be retried + * + * The lower of either the number of available connections or + * the configured max_retries. + * + * @param pool to get the retry count for. + * @return the number of times an operation can be retried. + */ +int fr_connection_pool_get_retries(fr_connection_pool_t *pool) +{ + return (pool->max_retries < pool->stats.num) ? pool->max_retries : pool->stats.num; +} + +/** Get the number of connections currently in the pool + * + * @param module the module configuration which should contain the pool + * @return the stats, or NULL on "not found" + */ +fr_connection_pool_stats_t const *fr_connection_pool_stats(CONF_SECTION *module) +{ + fr_connection_pool_t *pool = NULL; + CONF_SECTION *cs; + + cs = cf_section_sub_find(module, "pool"); + if (!cs) { + CONF_PAIR *cp; + module_instance_t *mi; + char const *value; + + /* + * This is the name of the module, not a + * reference. <sigh>. + */ + cp = cf_pair_find(module, "pool"); + if (!cp) return NULL; + + value = cf_pair_value(cp); + if (!value) return NULL; + + mi = module_find(cf_item_parent(cf_section_to_item(module)), value); + if (!mi) return NULL; + + cs = cf_section_sub_find(mi->cs, "pool"); + if (!cs) return NULL; + } + + pool = cf_data_find(cs, CONNECTION_POOL_CF_KEY); + if (!pool) return NULL; + + return &pool->stats; +} + + +/** Delete a connection pool + * + * Closes, unlinks and frees all connections in the connection pool, then frees + * all memory used by the connection pool. + * + * @note Will call the 'stop' trigger. + * @note Must be called with the mutex free. + * + * @param[in,out] pool to delete. + */ +void fr_connection_pool_free(fr_connection_pool_t *pool) +{ + fr_connection_t *this; + + if (!pool) return; + + /* + * More modules hold a reference to this pool, don't free + * it yet. + */ + if (pool->ref > 0) { + pool->ref--; + return; + } + + DEBUG("%s: Removing connection pool", pool->log_prefix); + + pthread_mutex_lock(&pool->mutex); + + /* + * Don't loop over the list. Just keep removing the head + * until they're all gone. + */ + while ((this = pool->head) != NULL) { + fr_connection_close_internal(pool, this, "Closing connection", "Shutting down connection pool"); + } + + fr_heap_delete(pool->heap); + + fr_connection_exec_trigger(pool, "stop"); + + rad_assert(pool->head == NULL); + rad_assert(pool->tail == NULL); + rad_assert(pool->stats.num == 0); + +#ifdef HAVE_PTHREAD_H + pthread_mutex_destroy(&pool->mutex); +#endif + + talloc_free(pool); +} + +/** Reserve a connection in the connection pool + * + * Will attempt to find an unused connection in the connection pool, if one is + * found, will mark it as in in use increment the number of active connections + * and return the connection handle. + * + * If no free connections are found will attempt to spawn a new one, conditional + * on a connection spawning not already being in progress, and not being at the + * 'max' connection limit. + * + * @note fr_connection_release must be called once the caller has finished + * using the connection. + * + * @see fr_connection_release + * @param[in,out] pool to reserve the connection from. + * @return + * - A pointer to the connection handle. + * - NULL on error. + */ +void *fr_connection_get(fr_connection_pool_t *pool) +{ + return fr_connection_get_internal(pool, true); +} + +/** Release a connection + * + * Will mark a connection as unused and decrement the number of active + * connections. + * + * @see fr_connection_get + * @param[in,out] pool to release the connection in. + * @param[in,out] conn to release. + */ +void fr_connection_release(fr_connection_pool_t *pool, void *conn) +{ + fr_connection_t *this; + + this = fr_connection_find(pool, conn); + if (!this) return; + + this->in_use = false; + + /* + * Record when the connection was last released + */ + gettimeofday(&this->last_released, NULL); + + /* + * Insert the connection in the heap. + * + * This will either be based on when we *started* using it + * (allowing fast links to be re-used, and slow links to be + * gradually expired), or when we released it (allowing + * the maximum amount of time between connection use). + */ + fr_heap_insert(pool->heap, this); + + rad_assert(pool->stats.active != 0); + pool->stats.active--; + + DEBUG("%s: Released connection (%" PRIu64 ")", pool->log_prefix, this->number); + + /* + * We mirror the "spawn on get" functionality by having + * "delete on release". If there are too many spare + * connections, go manage the pool && clean some up. + */ + fr_connection_pool_check(pool); +} + +/** Reconnect a suspected inviable connection + * + * This should be called by the module if it suspects that a connection is + * not viable (e.g. the server has closed it). + * + * Will attempt to create a new connection handle using the create callback, + * and if this is successful the new handle will be assigned to the existing + * pool connection. + * + * If this is not successful, the connection will be removed from the pool. + * + * When implementing a module that uses the connection pool API, it is advisable + * to pass a pointer to the pointer to the handle (void **conn) + * to all functions which may call reconnect. This is so that if a new handle + * is created and returned, the handle pointer can be updated up the callstack, + * and a function higher up the stack doesn't attempt to use a now invalid + * connection handle. + * + * @note Will free any talloced memory hung off the context of the connection, + * being reconnected. + * + * @warning After calling reconnect the caller *MUST NOT* attempt to use + * the old handle in any other operations, as its memory will have been + * freed. + * + * @see fr_connection_get + * @param[in,out] pool to reconnect the connection in. + * @param[in,out] conn to reconnect. + * @return new connection handle if successful else NULL. + */ +void *fr_connection_reconnect(fr_connection_pool_t *pool, void *conn) +{ + void *new_conn; + fr_connection_t *this; + + if (!pool || !conn) return NULL; + + /* + * Don't allow opening of new connections if we're trying + * to exit. + */ + if (main_config.exiting) { + fr_connection_release(pool, conn); + return NULL; + } + + /* + * If fr_connection_find is successful the pool is now locked + */ + this = fr_connection_find(pool, conn); + if (!this) return NULL; + + new_conn = fr_connection_reconnect_internal(pool, this); + pthread_mutex_unlock(&pool->mutex); + + return new_conn; +} + +/** Delete a connection from the connection pool. + * + * Resolves the connection handle to a connection, then (if found) + * closes, unlinks and frees that connection. + * + * @note Must be called with the mutex free. + * + * @param[in,out] pool Connection pool to modify. + * @param[in] conn to delete. + * @param[in] msg why the connection was closed. + * @return + * - 0 If the connection could not be found. + * - 1 if the connection was deleted. + */ +int fr_connection_close(fr_connection_pool_t *pool, void *conn, char const *msg) +{ + fr_connection_t *this; + + this = fr_connection_find(pool, conn); + if (!this) return 0; + + fr_connection_close_internal(pool, this, "Deleting connection", msg); + fr_connection_pool_check(pool); + return 1; +} diff --git a/src/main/crypt.c b/src/main/crypt.c new file mode 100644 index 0000000..99c66d8 --- /dev/null +++ b/src/main/crypt.c @@ -0,0 +1,97 @@ +/* + * crypt.c A thread-safe crypt wrapper + * + * This program 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 + * + * Copyright 2000-2006 The FreeRADIUS server project + */ + +RCSID("$Id$") + +#include <freeradius-devel/libradius.h> + +#ifdef HAVE_CRYPT_H +#include <crypt.h> +#endif + +#ifdef HAVE_PTHREAD_H +#include <pthread.h> + +/* + * No pthreads, no mutex. + */ +static bool fr_crypt_init = false; +static pthread_mutex_t fr_crypt_mutex; +#endif + + +/* + * performs a crypt password check in an thread-safe way. + * + * returns: 0 -- check succeeded + * -1 -- failed to crypt + * 1 -- check failed + */ +int fr_crypt_check(char const *key, char const *crypted) +{ + char *passwd; + int cmp = 0; + +#ifdef HAVE_PTHREAD_H + /* + * Ensure we're thread-safe, as crypt() isn't. + */ + if (fr_crypt_init == false) { + pthread_mutex_init(&fr_crypt_mutex, NULL); + fr_crypt_init = true; + } + + pthread_mutex_lock(&fr_crypt_mutex); +#endif + + passwd = crypt(key, crypted); + + /* + * Got something, check it within the lock. This is + * faster than copying it to a local buffer, and the + * time spent within the lock is critical. + */ + if (passwd) { + cmp = strcmp(crypted, passwd); + } + +#ifdef HAVE_PTHREAD_H + pthread_mutex_unlock(&fr_crypt_mutex); +#endif + + /* + * Error. + */ + if (!passwd) { + return -1; + } + + /* + * OK, return OK. + */ + if (cmp == 0) { + return 0; + } + + /* + * Comparison failed. + */ + return 1; +} diff --git a/src/main/detail.c b/src/main/detail.c new file mode 100644 index 0000000..a5e8437 --- /dev/null +++ b/src/main/detail.c @@ -0,0 +1,1266 @@ +/* + * detail.c Process the detail file + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2007 The FreeRADIUS server project + * Copyright 2007 Alan DeKok <aland@deployingradius.com> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/modules.h> +#include <freeradius-devel/detail.h> +#include <freeradius-devel/process.h> +#include <freeradius-devel/rad_assert.h> + +#ifdef HAVE_SYS_STAT_H +#include <sys/stat.h> +#endif + +#ifdef HAVE_GLOB_H +#include <glob.h> +#endif + +#include <fcntl.h> + +#ifdef WITH_DETAIL + +#define USEC (1000000) + +static FR_NAME_NUMBER state_names[] = { + { "unopened", STATE_UNOPENED }, + { "unlocked", STATE_UNLOCKED }, + { "header", STATE_HEADER }, + { "reading", STATE_READING }, + { "queued", STATE_QUEUED }, + { "running", STATE_RUNNING }, + { "no-reply", STATE_NO_REPLY }, + { "replied", STATE_REPLIED }, + + { NULL, 0 } +}; + + +/* + * If we're limiting outstanding packets, then mark the response + * as being sent. + */ +int detail_send(rad_listen_t *listener, REQUEST *request) +{ +#ifdef WITH_DETAIL_THREAD + char c = 0; +#endif + listen_detail_t *data = listener->data; + + rad_assert(request->listener == listener); + rad_assert(listener->send == detail_send); + + /* + * This request timed out. Remember that, and tell the + * caller it's OK to read more "detail" file stuff. + */ + if (request->reply->code == 0) { + data->delay_time = data->retry_interval * USEC; + data->signal = 1; + data->state = STATE_NO_REPLY; + + RDEBUG("detail (%s): No response to request. Will retry in %d seconds", + data->name, data->retry_interval); + } else { + int rtt; + struct timeval now; + + RDEBUG("detail (%s): Done %s packet.", data->name, fr_packet_codes[request->packet->code]); + + /* + * We call gettimeofday a lot. But it should be OK, + * because there's nothing else to do. + */ + gettimeofday(&now, NULL); + + /* + * If we haven't sent a packet in the last second, reset + * the RTT. + */ + now.tv_sec -= 1; + if (timercmp(&data->last_packet, &now, <)) { + data->has_rtt = false; + } + now.tv_sec += 1; + + /* + * Only one detail packet may be outstanding at a time, + * so it's safe to update some entries in the detail + * structure. + * + * We keep smoothed round trip time (SRTT), but not round + * trip timeout (RTO). We use SRTT to calculate a rough + * load factor. + */ + rtt = now.tv_sec - request->packet->timestamp.tv_sec; + rtt *= USEC; + rtt += now.tv_usec; + rtt -= request->packet->timestamp.tv_usec; + + /* + * If we're proxying, the RTT is our processing time, + * plus the network delay there and back, plus the time + * on the other end to process the packet. Ideally, we + * should remove the network delays from the RTT, but we + * don't know what they are. + * + * So, to be safe, we over-estimate the total cost of + * processing the packet. + */ + if (!data->has_rtt) { + data->has_rtt = true; + data->srtt = rtt; + data->rttvar = rtt / 2; + + } else { + data->rttvar -= data->rttvar >> 2; + data->rttvar += (data->srtt - rtt); + data->srtt -= data->srtt >> 3; + data->srtt += rtt >> 3; + } + + /* + * Calculate the time we wait before sending the next + * packet. + * + * rtt / (rtt + delay) = load_factor / 100 + */ + data->delay_time = (data->srtt * (100 - data->load_factor)) / (data->load_factor); + + /* + * Cap delay at no less than 4 packets/s. If the + * end system can't handle this, then it's very + * broken. + */ + if (data->delay_time > (USEC / 4)) data->delay_time= USEC / 4; + + RDEBUG3("detail (%s): Received response for request %d. Will read the next packet in %d seconds", + data->name, request->number, data->delay_time / USEC); + + data->last_packet = now; + data->signal = 1; + data->state = STATE_REPLIED; + data->counter++; + } + +#ifdef WITH_DETAIL_THREAD + if (write(data->child_pipe[1], &c, 1) < 0) { + RERROR("detail (%s): Failed writing ack to reader thread: %s", data->name, fr_syserror(errno)); + } +#else + radius_signal_self(RADIUS_SIGNAL_SELF_DETAIL); +#endif + + return 0; +} + + +/* + * Open the detail file, if we can. + * + * FIXME: create it, if it's not already there, so that the main + * server select() will wake us up if there's anything to read. + */ +static int detail_open(rad_listen_t *this) +{ + struct stat st; + listen_detail_t *data = this->data; + + rad_assert(data->state == STATE_UNOPENED); + data->delay_time = USEC; + + /* + * Open detail.work first, so we don't lose + * accounting packets. It's probably better to + * duplicate them than to lose them. + * + * Note that we're not writing to the file, but + * we've got to open it for writing in order to + * establish the lock, to prevent rlm_detail from + * writing to it. + * + * This also means that if we're doing globbing, + * this file will be read && processed before the + * file globbing is done. + */ + data->fp = NULL; + data->work_fd = open(data->filename_work, O_RDWR); + + /* + * Couldn't open it for a reason OTHER than "it doesn't + * exist". Complain and tell the admin. + */ + if ((data->work_fd < 0) && (errno != ENOENT)) { + ERROR("Failed opening detail file %s: %s", + data->filename_work, fr_syserror(errno)); + return 0; + } + + /* + * The file doesn't exist. Poll for it again. + */ + if (data->work_fd < 0) { +#ifndef HAVE_GLOB_H + return 0; +#else + unsigned int i; + int found; + time_t chtime; + char const *filename; + glob_t files; + + DEBUG2("detail (%s): Polling for detail file", data->name); + + memset(&files, 0, sizeof(files)); + if (glob(data->filename, 0, NULL, &files) != 0) { + noop: + globfree(&files); + return 0; + } + + /* + * Loop over the glob'd files, looking for the + * oldest one. + */ + chtime = 0; + found = -1; + for (i = 0; i < files.gl_pathc; i++) { + if (stat(files.gl_pathv[i], &st) < 0) continue; + + if ((i == 0) || (st.st_ctime < chtime)) { + chtime = st.st_ctime; + found = i; + } + } + + if (found < 0) goto noop; + + /* + * Rename detail to detail.work + */ + filename = files.gl_pathv[found]; + + DEBUG("detail (%s): Renaming %s -> %s", data->name, filename, data->filename_work); + if (rename(filename, data->filename_work) < 0) { + ERROR("detail (%s): Failed renaming %s to %s: %s", + data->name, filename, data->filename_work, fr_syserror(errno)); + goto noop; + } + + globfree(&files); /* Shouldn't be using anything in files now */ + + /* + * And try to open the filename. + */ + data->work_fd = open(data->filename_work, O_RDWR); + if (data->work_fd < 0) { + ERROR("Failed opening detail file %s: %s", + data->filename_work, fr_syserror(errno)); + return 0; + } +#endif + } /* else detail.work existed, and we opened it */ + + rad_assert(data->vps == NULL); + rad_assert(data->fp == NULL); + + data->state = STATE_UNLOCKED; + + data->client_ip.af = AF_UNSPEC; + data->timestamp = 0; + data->offset = data->last_offset = data->timestamp_offset = 0; + data->packets = 0; + data->tries = 0; + data->done_entry = false; + + return 1; +} + + +/* + * FIXME: add a configuration "exit when done" so that the detail + * file reader can be used as a one-off tool to update stuff. + * + * The time sequence for reading from the detail file is: + * + * t_0 signalled that the server is idle, and we + * can read from the detail file. + * + * t_rtt the packet has been processed successfully, + * wait for t_delay to enforce load factor. + * + * t_rtt + t_delay wait for signal that the server is idle. + * + */ +#ifndef WITH_DETAIL_THREAD +static RADIUS_PACKET *detail_poll(rad_listen_t *listener); + +int detail_recv(rad_listen_t *listener) +{ + RADIUS_PACKET *packet; + listen_detail_t *data = listener->data; + RAD_REQUEST_FUNP fun = NULL; + + /* + * We may be in the main thread. It needs to update the + * timers before we try to read from the file again. + */ + if (data->signal) return 0; + + packet = detail_poll(listener); + if (!packet) return -1; + + if (DEBUG_ENABLED2) { + VALUE_PAIR *vp; + vp_cursor_t cursor; + + DEBUG2("detail (%s): Read packet from %s", data->name, data->filename_work); + for (vp = fr_cursor_init(&cursor, &packet->vps); + vp; + vp = fr_cursor_next(&cursor)) { + debug_pair(vp); + } + } + + switch (packet->code) { + case PW_CODE_ACCOUNTING_REQUEST: + fun = rad_accounting; + break; + + case PW_CODE_COA_REQUEST: + case PW_CODE_DISCONNECT_REQUEST: + fun = rad_coa_recv; + break; + + default: + rad_free(&packet); + data->state = STATE_REPLIED; + return 0; + } + + /* + * Don't bother doing limit checks, etc. + */ + if (!request_receive(NULL, listener, packet, &data->detail_client, fun)) { + rad_free(&packet); + data->state = STATE_NO_REPLY; /* try again later */ + return 0; + } + + return 1; +} +#else +int detail_recv(rad_listen_t *listener) +{ + char c = 0; + ssize_t rcode; + RADIUS_PACKET *packet; + listen_detail_t *data = listener->data; + RAD_REQUEST_FUNP fun = NULL; + + /* + * Block until there's a packet ready. + */ + rcode = read(data->master_pipe[0], &packet, sizeof(packet)); + if (rcode <= 0) return rcode; + + rad_assert(packet != NULL); + + if (DEBUG_ENABLED2) { + VALUE_PAIR *vp; + vp_cursor_t cursor; + + DEBUG2("detail (%s): Read packet from %s", data->name, data->filename_work); + for (vp = fr_cursor_init(&cursor, &packet->vps); + vp; + vp = fr_cursor_next(&cursor)) { + debug_pair(vp); + } + } + + switch (packet->code) { + case PW_CODE_ACCOUNTING_REQUEST: + fun = rad_accounting; + break; + + case PW_CODE_COA_REQUEST: + case PW_CODE_DISCONNECT_REQUEST: + fun = rad_coa_recv; + break; + + default: + data->state = STATE_REPLIED; + goto signal_thread; + } + + if (!request_receive(NULL, listener, packet, &data->detail_client, fun)) { + data->state = STATE_NO_REPLY; /* try again later */ + + signal_thread: + rad_free(&packet); + if (write(data->child_pipe[1], &c, 1) < 0) { + ERROR("detail (%s): Failed writing ack to reader thread: %s", data->name, + fr_syserror(errno)); + } + } + + /* + * Wait for the child thread to write an answer to the pipe + */ + return 0; +} +#endif + +static RADIUS_PACKET *detail_poll(rad_listen_t *listener) +{ + int y; + char key[256], op[8], value[1024]; + vp_cursor_t cursor; + VALUE_PAIR *vp; + RADIUS_PACKET *packet; + char buffer[2048]; + listen_detail_t *data = listener->data; + + switch (data->state) { + case STATE_UNOPENED: +open_file: + rad_assert(data->work_fd < 0); + + if (!detail_open(listener)) return NULL; + + rad_assert(data->state == STATE_UNLOCKED); + rad_assert(data->work_fd >= 0); + + /* FALL-THROUGH */ + + /* + * Try to lock fd. If we can't, return. + * If we can, continue. This means that + * the server doesn't block while waiting + * for the lock to open... + */ + case STATE_UNLOCKED: + /* + * Note that we do NOT block waiting for + * the lock. We've re-named the file + * above, so we've already guaranteed + * that any *new* detail writer will not + * be opening this file. The only + * purpose of the lock is to catch a race + * condition where the execution + * "ping-pongs" between radiusd & + * radrelay. + */ + if (rad_lockfd_nonblock(data->work_fd, 0) < 0) { + /* + * Close the FD. The main loop + * will wake up in a second and + * try again. + */ + close(data->work_fd); + data->fp = NULL; + data->work_fd = -1; + data->state = STATE_UNOPENED; + return NULL; + } + + /* + * Only open for writing if we're + * marking requests as completed. + */ + data->fp = fdopen(data->work_fd, data->track ? "r+" : "r"); + if (!data->fp) { + ERROR("detail (%s): FATAL: Failed to re-open detail file: %s", + data->name, fr_syserror(errno)); + fr_exit(1); + } + + /* + * Look for the header + */ + data->state = STATE_HEADER; + data->delay_time = USEC; + data->vps = NULL; + + /* FALL-THROUGH */ + + case STATE_HEADER: + do_header: + rad_assert(data->ctx == NULL); + MEM(data->ctx = talloc_init("detail")); + + data->done_entry = false; + data->timestamp_offset = 0; + + data->tries = 0; + if (!data->fp) { + data->state = STATE_UNOPENED; + goto open_file; + } + + { + struct stat buf; + + if (fstat(data->work_fd, &buf) < 0) { + ERROR("detail (%s): Failed to stat detail file: %s", + data->name, fr_syserror(errno)); + + goto cleanup; + } + if (((off_t) ftell(data->fp)) == buf.st_size) { + goto cleanup; + } + } + + /* + * End of file. Delete it, and re-set + * everything. + */ + if (feof(data->fp)) { + cleanup: + DEBUG("detail (%s): Unlinking %s", data->name, data->filename_work); + unlink(data->filename_work); + if (data->fp) fclose(data->fp); + TALLOC_FREE(data->ctx); + data->fp = NULL; + data->work_fd = -1; + data->state = STATE_UNOPENED; + rad_assert(data->vps == NULL); + + if (data->one_shot) { + INFO("detail (%s): Finished reading \"one shot\" detail file - Exiting", data->name); + radius_signal_self(RADIUS_SIGNAL_SELF_EXIT); + } + + return NULL; + } + + /* + * Else go read something. + */ + if (!fgets(buffer, sizeof(buffer), data->fp)) { + DEBUG("detail (%s): Failed reading header from file - %s", + data->name, data->filename_work); + goto cleanup; + } + + /* + * Badly formatted file: delete it. + */ + if (!strchr(buffer, '\n')) { + DEBUG("detail (%s): Invalid line without trailing LF - %s", data->name, buffer); + goto cleanup; + } + + if (!sscanf(buffer, "%*s %*s %*d %*d:%*d:%*d %d", &y)) { + DEBUG("detail (%s): Failed reading detail file header in line - %s", data->name, buffer); + goto cleanup; + } + + data->state = STATE_READING; + /* FALL-THROUGH */ + + + /* + * Read more value-pair's, unless we're + * at EOF. In that case, queue whatever + * we have. + */ + case STATE_READING: + rad_assert(data->fp != NULL); + + fr_cursor_init(&cursor, &data->vps); + + /* + * Read a header, OR a value-pair. + */ + while (fgets(buffer, sizeof(buffer), data->fp)) { + data->last_offset = data->offset; + data->offset = ftell(data->fp); /* for statistics */ + + /* + * Badly formatted file: delete it. + */ + if (!strchr(buffer, '\n')) { + WARN("detail (%s): Skipping line without trailing LF - %s", data->name, buffer); + fr_pair_list_free(&data->vps); + goto cleanup; + } + + /* + * We're reading VP's, and got a blank line. + * That indicates the end of an entry. Queue the + * packet. + */ + if (buffer[0] == '\n') { + data->state = STATE_QUEUED; + data->tries = 0; + data->packets++; + goto alloc_packet; + } + + /* + * We have a full "attribute = value" line. + * If it doesn't look reasonable, skip it. + * + * FIXME: print an error for badly formatted attributes? + */ + if (sscanf(buffer, "%255s %7s %1023s", key, op, value) != 3) { + DEBUG("detail (%s): Skipping badly formatted line - %s", data->name, buffer); + continue; + } + + /* + * Should be =, :=, +=, ... + */ + if (!strchr(op, '=')) { + DEBUG("detail (%s): Skipping line without operator - %s", data->name, buffer); + continue; + } + + /* + * Skip non-protocol attributes. + */ + if (!strcasecmp(key, "Request-Authenticator")) continue; + + /* + * Set the original client IP address, based on + * what's in the detail file. + * + * Hmm... we don't set the server IP address. + * or port. Oh well. + */ + if (!strcasecmp(key, "Client-IP-Address")) { + data->client_ip.af = AF_INET; + if (ip_hton(&data->client_ip, AF_INET, value, false) < 0) { + DEBUG("detail (%s): Failed parsing Client-IP-Address", data->name); + fr_pair_list_free(&data->vps); + goto cleanup; + } + continue; + } + + /* + * The original time at which we received the + * packet. We need this to properly calculate + * Acct-Delay-Time. + */ + if (!strcasecmp(key, "Timestamp")) { + data->timestamp = atoi(value); + data->timestamp_offset = data->last_offset; + + vp = fr_pair_afrom_num(data->ctx, PW_PACKET_ORIGINAL_TIMESTAMP, 0); + if (vp) { + vp->vp_date = (uint32_t) data->timestamp; + vp->type = VT_DATA; + fr_cursor_insert(&cursor, vp); + } + continue; + } + + if (!strcasecmp(key, "Donestamp")) { + data->timestamp = atoi(value); + data->done_entry = true; + continue; + } + + DEBUG3("detail (%s): Trying to read VP from line - %s", data->name, buffer); + + /* + * Read one VP. + * + * FIXME: do we want to check for non-protocol + * attributes like radsqlrelay does? + */ + vp = NULL; + if ((fr_pair_list_afrom_str(data->ctx, buffer, &vp) > 0) && + (vp != NULL)) { + fr_cursor_merge(&cursor, vp); + } else { + DEBUG("detail (%s): Failed reading VP from line - %s", data->name, buffer); + goto cleanup; + } + } + + /* + * The writer doesn't check that the + * record was completely written. If the + * disk is full, this can result in a + * truncated record which has no trailing + * blank line. When that happens, it's a + * bad record, and we ignore it. + */ + if (feof(data->fp)) { + DEBUG("detail (%s): Truncated record: treating it as EOF for detail file %s", + data->name, data->filename_work); + fr_pair_list_free(&data->vps); + goto cleanup; + } + + /* + * Some kind of non-eof error. + * + * FIXME: Leave the file in-place, and warn the + * administrator? + */ + DEBUG("detail (%s): Unknown error, deleting detail file %s", + data->name, data->filename_work); + goto cleanup; + + case STATE_QUEUED: + goto alloc_packet; + + /* + * Periodically check what's going on. + * If the request is taking too long, + * retry it. + */ + case STATE_RUNNING: + if (time(NULL) < (data->running + (int)data->retry_interval)) { + return NULL; + } + + DEBUG("detail (%s): No response to detail request. Retrying", data->name); + /* FALL-THROUGH */ + + /* + * If there's no reply, keep + * retransmitting the current packet + * forever. + */ + case STATE_NO_REPLY: + data->state = STATE_QUEUED; + goto alloc_packet; + + /* + * We have a reply. Clean up the old + * request, and go read another one. + */ + case STATE_REPLIED: + if (data->track) { + rad_assert(data->fp != NULL); + + if (fseek(data->fp, data->timestamp_offset, SEEK_SET) < 0) { + DEBUG("detail (%s): Failed seeking to timestamp offset: %s", + data->name, fr_syserror(errno)); + } else if (fwrite("\tDone", 1, 5, data->fp) < 5) { + DEBUG("detail (%s): Failed marking request as done: %s", + data->name, fr_syserror(errno)); + } else if (fflush(data->fp) != 0) { + DEBUG("detail (%s): Failed flushing marked detail file to disk: %s", + data->name, fr_syserror(errno)); + } + + if (fseek(data->fp, data->offset, SEEK_SET) < 0) { + DEBUG("detail (%s): Failed seeking to next detail request: %s", + data->name, fr_syserror(errno)); + } + } + + fr_pair_list_free(&data->vps); + TALLOC_FREE(data->ctx); + data->state = STATE_HEADER; + goto do_header; + } + + /* + * Process the packet. + */ + alloc_packet: + if (data->done_entry) { + DEBUG2("detail (%s): Skipping record for timestamp %lu", data->name, data->timestamp); + fr_pair_list_free(&data->vps); + TALLOC_FREE(data->ctx); + data->state = STATE_HEADER; + goto do_header; + } + + data->tries++; + + /* + * We're done reading the file, but we didn't read + * anything. Clean up, and don't return anything. + */ + if (!data->vps) { + WARN("detail (%s): Read empty packet from file %s", + data->name, data->filename_work); + data->state = STATE_HEADER; + return NULL; + } + + /* + * Allocate the packet. If we fail, it's a serious + * problem. + */ + packet = rad_alloc(NULL, true); + if (!packet) { + ERROR("detail (%s): FATAL: Failed allocating memory for detail", data->name); + fr_exit(1); + } + + memset(packet, 0, sizeof(*packet)); + packet->sockfd = -1; + packet->src_ipaddr.af = AF_INET; + packet->src_ipaddr.ipaddr.ip4addr.s_addr = htonl(INADDR_NONE); + + /* + * If everything's OK, this is a waste of memory. + * Otherwise, it lets us re-send the original packet + * contents, unmolested. + */ + packet->vps = fr_pair_list_copy(packet, data->vps); + + packet->code = PW_CODE_ACCOUNTING_REQUEST; + vp = fr_pair_find_by_num(packet->vps, PW_PACKET_TYPE, 0, TAG_ANY); + if (vp) packet->code = vp->vp_integer; + + gettimeofday(&packet->timestamp, NULL); + + /* + * Remember where it came from, so that we don't + * proxy it to the place it came from... + */ + if (data->client_ip.af != AF_UNSPEC) { + packet->src_ipaddr = data->client_ip; + } + + vp = fr_pair_find_by_num(packet->vps, PW_PACKET_SRC_IP_ADDRESS, 0, TAG_ANY); + if (vp) { + packet->src_ipaddr.af = AF_INET; + packet->src_ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr; + packet->src_ipaddr.prefix = 32; + } else { + vp = fr_pair_find_by_num(packet->vps, PW_PACKET_SRC_IPV6_ADDRESS, 0, TAG_ANY); + if (vp) { + packet->src_ipaddr.af = AF_INET6; + memcpy(&packet->src_ipaddr.ipaddr.ip6addr, + &vp->vp_ipv6addr, sizeof(vp->vp_ipv6addr)); + packet->src_ipaddr.prefix = 128; + } + } + + vp = fr_pair_find_by_num(packet->vps, PW_PACKET_DST_IP_ADDRESS, 0, TAG_ANY); + if (vp) { + packet->dst_ipaddr.af = AF_INET; + packet->dst_ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr; + packet->dst_ipaddr.prefix = 32; + } else { + vp = fr_pair_find_by_num(packet->vps, PW_PACKET_DST_IPV6_ADDRESS, 0, TAG_ANY); + if (vp) { + packet->dst_ipaddr.af = AF_INET6; + memcpy(&packet->dst_ipaddr.ipaddr.ip6addr, + &vp->vp_ipv6addr, sizeof(vp->vp_ipv6addr)); + packet->dst_ipaddr.prefix = 128; + } + } + + /* + * Generate packet ID, ports, IP via a counter. + */ + packet->id = data->counter & 0xff; + packet->src_port = 1024 + ((data->counter >> 8) & 0xff); + packet->dst_port = 1024 + ((data->counter >> 16) & 0xff); + + packet->dst_ipaddr.af = AF_INET; + packet->dst_ipaddr.ipaddr.ip4addr.s_addr = htonl((INADDR_LOOPBACK & ~0xffffff) | ((data->counter >> 24) & 0xff)); + + /* + * Create / update accounting attributes. + */ + if (packet->code == PW_CODE_ACCOUNTING_REQUEST) { + /* + * Prefer the Event-Timestamp in the packet, if it + * exists. That is when the event occurred, whereas the + * "Timestamp" field is when we wrote the packet to the + * detail file, which could have been much later. + */ + vp = fr_pair_find_by_num(packet->vps, PW_EVENT_TIMESTAMP, 0, TAG_ANY); + if (vp) { + data->timestamp = vp->vp_integer; + } + + /* + * Look for Acct-Delay-Time, and update + * based on Acct-Delay-Time += (time(NULL) - timestamp) + */ + vp = fr_pair_find_by_num(packet->vps, PW_ACCT_DELAY_TIME, 0, TAG_ANY); + if (!vp) { + vp = fr_pair_afrom_num(packet, PW_ACCT_DELAY_TIME, 0); + rad_assert(vp != NULL); + fr_pair_add(&packet->vps, vp); + } + if (data->timestamp != 0) { + vp->vp_integer += time(NULL) - data->timestamp; + } + } + + /* + * Set the transmission count. + */ + vp = fr_pair_find_by_num(packet->vps, PW_PACKET_TRANSMIT_COUNTER, 0, TAG_ANY); + if (!vp) { + vp = fr_pair_afrom_num(packet, PW_PACKET_TRANSMIT_COUNTER, 0); + rad_assert(vp != NULL); + fr_pair_add(&packet->vps, vp); + } + vp->vp_integer = data->tries; + + data->state = STATE_RUNNING; + data->running = packet->timestamp.tv_sec; + + return packet; +} + +/* + * Free detail-specific stuff. + */ +void detail_free(rad_listen_t *this) +{ + listen_detail_t *data = this->data; + +#ifdef WITH_DETAIL_THREAD + if (!check_config) { + ssize_t ret; + void *arg = NULL; + + /* + * Mark the child pipes as unusable + */ + close(data->child_pipe[0]); + close(data->child_pipe[1]); + data->child_pipe[0] = -1; + + /* + * Tell it to stop (interrupting its sleep) + */ + pthread_kill(data->pthread_id, SIGTERM); + + /* + * Wait for it to acknowledge that it's stopped. + */ + ret = read(data->master_pipe[0], &arg, sizeof(arg)); + if (ret < 0) { + ERROR("detail (%s): Reader thread exited without informing the master: %s", + data->name, fr_syserror(errno)); + } else if (ret != sizeof(arg)) { + ERROR("detail (%s): Invalid thread pointer received from reader thread during exit", + data->name); + ERROR("detail (%s): Expected %zu bytes, got %zi bytes", data->name, sizeof(arg), ret); + } + + close(data->master_pipe[0]); + close(data->master_pipe[1]); + + if (arg) pthread_join(data->pthread_id, &arg); + } +#endif + + if (data->fp != NULL) { + fclose(data->fp); + data->fp = NULL; + } +} + + +int detail_print(rad_listen_t const *this, char *buffer, size_t bufsize) +{ + if (!this->server) { + return snprintf(buffer, bufsize, "%s", + ((listen_detail_t *)(this->data))->filename); + } + + return snprintf(buffer, bufsize, "detail file %s as server %s", + ((listen_detail_t *)(this->data))->filename, + this->server); +} + + +/* + * Delay while waiting for a file to be ready + */ +static int detail_delay(listen_detail_t *data) +{ + int delay = (data->poll_interval - 1) * USEC; + + /* + * Add +/- 0.25s of jitter + */ + delay += (USEC * 3) / 4; + delay += fr_rand() % (USEC / 2); + + DEBUG2("detail (%s): Detail listener state %s waiting %d.%06d sec", + data->name, + fr_int2str(state_names, data->state, "?"), + (delay / USEC), delay % USEC); + + return delay; +} + +/* + * Overloaded to return delay times. + */ +int detail_encode(UNUSED rad_listen_t *this, UNUSED REQUEST *request) +{ +#ifdef WITH_DETAIL_THREAD + return 0; +#else + listen_detail_t *data = this->data; + + /* + * We haven't sent a packet... delay things a bit. + */ + if (!data->signal) return detail_delay(data); + + data->signal = 0; + + DEBUG2("detail (%s): Detail listener state %s signalled %d waiting %d.%06d sec", + data->name, + fr_int2str(state_names, data->state, "?"), + data->signal, + data->delay_time / USEC, + data->delay_time % USEC); + + return data->delay_time; +#endif +} + +/* + * Overloaded to return "should we fix delay times" + */ +int detail_decode(rad_listen_t *this, REQUEST *request) +{ +#ifdef WITH_DETAIL_THREAD + listen_detail_t *data = this->data; + + RDEBUG("Received %s from detail file %s", + fr_packet_codes[request->packet->code], data->filename_work); + + rdebug_pair_list(L_DBG_LVL_1, request, request->packet->vps, "\t"); + + return 0; +#else + listen_detail_t *data = this->data; + + RDEBUG("Received %s from detail file %s", + fr_packet_codes[request->packet->code], data->filename_work); + + rdebug_pair_list(L_DBG_LVL_1, request, request->packet->vps, "\t"); + + return data->signal; +#endif +} + + +#ifdef WITH_DETAIL_THREAD +static void *detail_handler_thread(void *arg) +{ + char c; + rad_listen_t *this = arg; + listen_detail_t *data = this->data; + + while (true) { + RADIUS_PACKET *packet; + + while ((packet = detail_poll(this)) == NULL) { + usleep(detail_delay(data)); + + /* + * If we're supposed to exit then tell + * the master thread we've exited. + */ + if (data->child_pipe[0] < 0) { + packet = NULL; + if (write(data->master_pipe[1], &packet, sizeof(packet)) < 0) { + ERROR("detail (%s): Failed writing exit status to master: %s", + data->name, fr_syserror(errno)); + } + return NULL; + } + } + + /* + * Keep retrying forever. + * + * FIXME: cap the retries. + */ + do { + if (write(data->master_pipe[1], &packet, sizeof(packet)) < 0) { + ERROR("detail (%s): Failed passing detail packet pointer to master: %s", + data->name, fr_syserror(errno)); + } + + if (read(data->child_pipe[0], &c, 1) < 0) { + ERROR("detail (%s): Failed getting detail packet ack from master: %s", + data->name, fr_syserror(errno)); + break; + } + + if (data->delay_time > 0) usleep(data->delay_time); + + packet = detail_poll(this); + if (!packet) break; + } while (data->state != STATE_REPLIED); + } + + return NULL; +} +#endif + + +static const CONF_PARSER detail_config[] = { + { "detail", FR_CONF_OFFSET(PW_TYPE_FILE_OUTPUT | PW_TYPE_DEPRECATED, listen_detail_t, filename), NULL }, + { "filename", FR_CONF_OFFSET(PW_TYPE_FILE_OUTPUT | PW_TYPE_REQUIRED, listen_detail_t, filename), NULL }, + { "load_factor", FR_CONF_OFFSET(PW_TYPE_INTEGER, listen_detail_t, load_factor), STRINGIFY(10) }, + { "poll_interval", FR_CONF_OFFSET(PW_TYPE_INTEGER, listen_detail_t, poll_interval), STRINGIFY(1) }, + { "retry_interval", FR_CONF_OFFSET(PW_TYPE_INTEGER, listen_detail_t, retry_interval), STRINGIFY(30) }, + { "one_shot", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, listen_detail_t, one_shot), "no" }, + { "track", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, listen_detail_t, track), "no" }, + CONF_PARSER_TERMINATOR +}; + +/* + * Parse a detail section. + */ +int detail_parse(CONF_SECTION *cs, rad_listen_t *this) +{ + int rcode; + listen_detail_t *data; + RADCLIENT *client; + char buffer[2048]; + + data = this->data; + + rcode = cf_section_parse(cs, data, detail_config); + if (rcode < 0) { + cf_log_err_cs(cs, "Failed parsing listen section"); + return -1; + } + + data->name = cf_section_name2(cs); + if (!data->name) data->name = data->filename; + + /* + * We don't do duplicate detection for "detail" sockets. + */ + this->nodup = true; + this->synchronous = false; + + if (!data->filename) { + cf_log_err_cs(cs, "No detail file specified in listen section"); + return -1; + } + + FR_INTEGER_BOUND_CHECK("load_factor", data->load_factor, >=, 1); + FR_INTEGER_BOUND_CHECK("load_factor", data->load_factor, <=, 100); + + FR_INTEGER_BOUND_CHECK("poll_interval", data->poll_interval, >=, 1); + FR_INTEGER_BOUND_CHECK("poll_interval", data->poll_interval, <=, 60); + + FR_INTEGER_BOUND_CHECK("retry_interval", data->retry_interval, >=, 4); + FR_INTEGER_BOUND_CHECK("retry_interval", data->retry_interval, <=, 3600); + + /* + * Only checking the config. Don't start threads or anything else. + */ + if (check_config) return 0; + + /* + * If the filename is a glob, use "detail.work" as the + * work file name. + */ + if ((strchr(data->filename, '*') != NULL) || + (strchr(data->filename, '[') != NULL)) { + char *p; + +#ifndef HAVE_GLOB_H + WARN("detail (%s): File \"%s\" appears to use file globbing, but it is not supported on this system", + data->name, data->filename); +#endif + strlcpy(buffer, data->filename, sizeof(buffer)); + p = strrchr(buffer, FR_DIR_SEP); + if (p) { + p[1] = '\0'; + } else { + buffer[0] = '\0'; + } + + /* + * Globbing cannot be done across directories. + */ + if ((strchr(buffer, '*') != NULL) || + (strchr(buffer, '[') != NULL)) { + cf_log_err_cs(cs, "Wildcard directories are not supported"); + return -1; + } + + strlcat(buffer, "detail.work", + sizeof(buffer) - strlen(buffer)); + + } else { + snprintf(buffer, sizeof(buffer), "%s.work", data->filename); + } + + data->filename_work = talloc_strdup(data, buffer); + + data->work_fd = -1; + data->vps = NULL; + data->fp = NULL; + data->state = STATE_UNOPENED; + data->delay_time = data->poll_interval * USEC; + data->signal = 1; + + /* + * Initialize the fake client. + */ + client = &data->detail_client; + memset(client, 0, sizeof(*client)); + client->ipaddr.af = AF_INET; + client->ipaddr.ipaddr.ip4addr.s_addr = INADDR_NONE; + client->ipaddr.prefix = 0; + client->longname = client->shortname = data->filename; + client->secret = client->shortname; + client->nas_type = talloc_strdup(data, "none"); /* Part of 'data' not dynamically allocated */ + +#ifdef WITH_DETAIL_THREAD + /* + * Create the communication pipes. + */ + if (pipe(data->master_pipe) < 0) { + ERROR("detail (%s): Error opening internal pipe: %s", data->name, fr_syserror(errno)); + fr_exit(1); + } + + if (pipe(data->child_pipe) < 0) { + ERROR("detail (%s): Error opening internal pipe: %s", data->name, fr_syserror(errno)); + fr_exit(1); + } + + pthread_create(&data->pthread_id, NULL, detail_handler_thread, this); + + this->fd = data->master_pipe[0]; +#endif + + return 0; +} +#endif diff --git a/src/main/evaluate.c b/src/main/evaluate.c new file mode 100644 index 0000000..c8585b6 --- /dev/null +++ b/src/main/evaluate.c @@ -0,0 +1,1144 @@ +/* + * evaluate.c Evaluate complex conditions + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2007 The FreeRADIUS server project + * Copyright 2007 Alan DeKok <aland@deployingradius.com> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/modules.h> +#include <freeradius-devel/parser.h> +#include <freeradius-devel/rad_assert.h> + +#include <ctype.h> + +#ifdef WITH_UNLANG +#ifdef WITH_EVAL_DEBUG +# define EVAL_DEBUG(fmt, ...) printf("EVAL: ");printf(fmt, ## __VA_ARGS__);printf("\n");fflush(stdout) +#else +# define EVAL_DEBUG(...) +#endif + +FR_NAME_NUMBER const modreturn_table[] = { + { "reject", RLM_MODULE_REJECT }, + { "fail", RLM_MODULE_FAIL }, + { "ok", RLM_MODULE_OK }, + { "handled", RLM_MODULE_HANDLED }, + { "invalid", RLM_MODULE_INVALID }, + { "userlock", RLM_MODULE_USERLOCK }, + { "notfound", RLM_MODULE_NOTFOUND }, + { "noop", RLM_MODULE_NOOP }, + { "updated", RLM_MODULE_UPDATED }, + { NULL, 0 } +}; + + +static bool all_digits(char const *string) +{ + char const *p = string; + + rad_assert(p != NULL); + + if (*p == '\0') return false; + + if (*p == '-') p++; + + while (isdigit((uint8_t) *p)) p++; + + return (*p == '\0'); +} + +/** Evaluate a template + * + * Converts a vp_tmpl_t to a boolean value. + * + * @param[in] request the REQUEST + * @param[in] modreturn the previous module return code + * @param[in] depth of the recursion (only used for debugging) + * @param[in] vpt the template to evaluate + * @return -1 on error, 0 for "no match", 1 for "match". + */ +int radius_evaluate_tmpl(REQUEST *request, int modreturn, UNUSED int depth, vp_tmpl_t const *vpt) +{ + int rcode; + int modcode; + value_data_t data; + + switch (vpt->type) { + case TMPL_TYPE_LITERAL: + modcode = fr_str2int(modreturn_table, vpt->name, RLM_MODULE_UNKNOWN); + if (modcode != RLM_MODULE_UNKNOWN) { + rcode = (modcode == modreturn); + break; + } + + /* + * Else it's a literal string. Empty string is + * false, non-empty string is true. + * + * @todo: Maybe also check for digits? + * + * The VPT *doesn't* have a "bare word" type, + * which arguably it should. + */ + rcode = (*vpt->name != '\0'); + break; + + case TMPL_TYPE_ATTR: + case TMPL_TYPE_LIST: + if (tmpl_find_vp(NULL, request, vpt) == 0) { + rcode = true; + } else { + rcode = false; + } + break; + + case TMPL_TYPE_XLAT_STRUCT: + case TMPL_TYPE_XLAT: + case TMPL_TYPE_EXEC: + { + char *p; + + if (!*vpt->name) return false; + rcode = tmpl_aexpand(request, &p, request, vpt, NULL, NULL); + if (rcode < 0) { + EVAL_DEBUG("FAIL %d", __LINE__); + return -1; + } + data.strvalue = p; + rcode = (data.strvalue && (*data.strvalue != '\0')); + talloc_free(data.ptr); + } + break; + + /* + * Can't have a bare ... (/foo/) ... + */ + case TMPL_TYPE_REGEX: + case TMPL_TYPE_REGEX_STRUCT: + rad_assert(0 == 1); + /* FALL-THROUGH */ + + default: + EVAL_DEBUG("FAIL %d", __LINE__); + rcode = -1; + break; + } + + return rcode; +} + +#ifdef HAVE_REGEX +/** Perform a regular expressions comparison between two operands + * + * @return -1 on error, 0 for "no match", 1 for "match". + */ +static int cond_do_regex(REQUEST *request, fr_cond_t const *c, + PW_TYPE lhs_type, value_data_t const *lhs, size_t lhs_len, + PW_TYPE rhs_type, value_data_t const *rhs, size_t rhs_len) +{ + vp_map_t const *map = c->data.map; + + ssize_t slen; + int ret; + + regex_t *preg, *rreg = NULL; + regmatch_t rxmatch[REQUEST_MAX_REGEX + 1]; /* +1 for %{0} (whole match) capture group */ + size_t nmatch = sizeof(rxmatch) / sizeof(regmatch_t); + + if (!lhs || (lhs_type != PW_TYPE_STRING)) return -1; + + EVAL_DEBUG("CMP WITH REGEX %s %s", + map->rhs->tmpl_iflag ? "CASE INSENSITIVE" : "CASE SENSITIVE", + map->rhs->tmpl_mflag ? "MULTILINE" : "SINGLELINE"); + + switch (map->rhs->type) { + case TMPL_TYPE_REGEX_STRUCT: /* pre-compiled to a regex */ + preg = map->rhs->tmpl_preg; +#ifdef HAVE_PCRE + rad_assert(preg->precompiled); +#endif + break; + + default: + rad_assert(rhs_type == PW_TYPE_STRING); + rad_assert(rhs->strvalue); + slen = regex_compile(request, &rreg, rhs->strvalue, rhs_len, + map->rhs->tmpl_iflag, map->rhs->tmpl_mflag, true, true); + if (slen <= 0) { + REMARKER(rhs->strvalue, -slen, fr_strerror()); + EVAL_DEBUG("FAIL %d", __LINE__); + + return -1; + } + preg = rreg; +#ifdef HAVE_PCRE + rad_assert(!preg->precompiled); +#endif + break; + } + + ret = regex_exec(preg, lhs->strvalue, lhs_len, rxmatch, &nmatch); + switch (ret) { + case 0: + EVAL_DEBUG("CLEARING SUBCAPTURES"); + regex_sub_to_request(request, NULL, NULL, 0, NULL, 0); /* clear out old entries */ + break; + + case 1: + EVAL_DEBUG("SETTING SUBCAPTURES"); + regex_sub_to_request(request, &preg, lhs->strvalue, lhs_len, rxmatch, nmatch); + break; + + case -1: + EVAL_DEBUG("REGEX ERROR"); + REDEBUG("regex failed: %s", fr_strerror()); + break; + + default: + break; + } + + if (preg) talloc_free(rreg); + + return ret; +} +#endif + +#ifdef WITH_EVAL_DEBUG +static void cond_print_operands(REQUEST *request, + PW_TYPE lhs_type, value_data_t const *lhs, size_t lhs_len, + PW_TYPE rhs_type, value_data_t const *rhs, size_t rhs_len) +{ + if (lhs) { + if (lhs_type == PW_TYPE_STRING) { + EVAL_DEBUG("LHS: \"%s\" (%zu)" , lhs->strvalue, lhs_len); + } else { + char *lhs_hex; + + lhs_hex = talloc_array(request, char, (lhs_len * 2) + 1); + + if (lhs_type == PW_TYPE_OCTETS) { + fr_bin2hex(lhs_hex, lhs->octets, lhs_len); + } else { + fr_bin2hex(lhs_hex, (uint8_t const *)lhs, lhs_len); + } + + EVAL_DEBUG("LHS: 0x%s (%zu)", lhs_hex, lhs_len); + + talloc_free(lhs_hex); + } + } else { + EVAL_DEBUG("LHS: VIRTUAL"); + } + + if (rhs) { + if (rhs_type == PW_TYPE_STRING) { + EVAL_DEBUG("RHS: \"%s\" (%zu)" , rhs->strvalue, rhs_len); + } else { + char *rhs_hex; + + rhs_hex = talloc_array(request, char, (rhs_len * 2) + 1); + + if (rhs_type == PW_TYPE_OCTETS) { + fr_bin2hex(rhs_hex, rhs->octets, rhs_len); + } else { + fr_bin2hex(rhs_hex, (uint8_t const *)rhs, rhs_len); + } + + EVAL_DEBUG("RHS: 0x%s (%zu)", rhs_hex, rhs_len); + + talloc_free(rhs_hex); + } + } else { + EVAL_DEBUG("RHS: COMPILED"); + } +} +#endif + +/** Call the correct data comparison function for the condition + * + * Deals with regular expression comparisons, virtual attribute + * comparisons, and data comparisons. + * + * @return -1 on error, 0 for "no match", 1 for "match". + */ +static int cond_cmp_values(REQUEST *request, fr_cond_t const *c, + PW_TYPE lhs_type, value_data_t const *lhs, size_t lhs_len, + PW_TYPE rhs_type, value_data_t const *rhs, size_t rhs_len) +{ + vp_map_t const *map = c->data.map; + int rcode; + +#ifdef WITH_EVAL_DEBUG + EVAL_DEBUG("CMP OPERANDS"); + cond_print_operands(request, lhs_type, lhs, lhs_len, rhs_type, rhs, rhs_len); +#endif + +#ifdef HAVE_REGEX + /* + * Regex comparison + */ + if (map->op == T_OP_REG_EQ) { + rcode = cond_do_regex(request, c, lhs_type, lhs, lhs_len, rhs_type, rhs, rhs_len); + goto finish; + } +#endif + /* + * Virtual attribute comparison. + */ + if (c->pass2_fixup == PASS2_PAIRCOMPARE) { + VALUE_PAIR *vp; + + EVAL_DEBUG("CMP WITH PAIRCOMPARE"); + rad_assert(map->lhs->type == TMPL_TYPE_ATTR); + + vp = fr_pair_afrom_da(request, map->lhs->tmpl_da); + vp->op = c->data.map->op; + + value_data_copy(vp, &vp->data, rhs_type, rhs, rhs_len); + vp->vp_length = rhs_len; + + rcode = paircompare(request, request->packet->vps, vp, NULL); + rcode = (rcode == 0) ? 1 : 0; + talloc_free(vp); + goto finish; + } + + /* + * At this point both operands should have been normalised + * to the same type, and there's no special comparisons + * left. + */ + rad_assert(lhs_type == rhs_type); + + EVAL_DEBUG("CMP WITH VALUE DATA"); + rcode = value_data_cmp_op(map->op, lhs_type, lhs, lhs_len, rhs_type, rhs, rhs_len); +finish: + switch (rcode) { + case 0: + EVAL_DEBUG("FALSE"); + break; + + case 1: + EVAL_DEBUG("TRUE"); + break; + + default: + EVAL_DEBUG("ERROR %i", rcode); + break; + } + + return rcode; +} + + +static size_t regex_escape(UNUSED REQUEST *request, char *out, size_t outlen, char const *in, UNUSED void *arg) +{ + char *p = out; + + while (*in && (outlen > 2)) { + switch (*in) { + case '\\': + case '.': + case '*': + case '+': + case '?': + case '|': + case '^': + case '$': + case '[': /* we don't list close braces */ + case '{': + case '(': + *(p++) = '\\'; + outlen--; + /* FALL-THROUGH */ + + default: + *(p++) = *(in++); + outlen--; + break; + } + } + + *(p++) = '\0'; + return p - out; +} + + +/** Convert both operands to the same type + * + * If casting is successful, we call cond_cmp_values to do the comparison + * + * @return -1 on error, 0 for "no match", 1 for "match". + */ +static int cond_normalise_and_cmp(REQUEST *request, fr_cond_t const *c, + PW_TYPE lhs_type, DICT_ATTR const *lhs_enumv, + value_data_t const *lhs, size_t lhs_len) +{ + vp_map_t const *map = c->data.map; + + DICT_ATTR const *cast = NULL; + PW_TYPE cast_type = PW_TYPE_INVALID; + + int rcode; + + PW_TYPE rhs_type = PW_TYPE_INVALID; + DICT_ATTR const *rhs_enumv = NULL; + value_data_t const *rhs = NULL; + size_t rhs_len; + + value_data_t lhs_cast, rhs_cast; + void *lhs_cast_buff = NULL, *rhs_cast_buff = NULL; + + xlat_escape_t escape = NULL; + + /* + * Cast operand to correct type. + * + * With hack for strings that look like integers, to cast them + * to 64 bit unsigned integers. + * + * @fixme For things like this it'd be useful to have a 64bit signed type. + */ +#define CAST(_s) \ +do {\ + if ((cast_type != PW_TYPE_INVALID) && (_s ## _type != PW_TYPE_INVALID) && (cast_type != _s ## _type)) {\ + ssize_t r;\ + EVAL_DEBUG("CASTING " #_s " FROM %s TO %s",\ + fr_int2str(dict_attr_types, _s ## _type, "<INVALID>"),\ + fr_int2str(dict_attr_types, cast_type, "<INVALID>"));\ + r = value_data_cast(request, &_s ## _cast, cast_type, cast, _s ## _type, _s ## _enumv, _s, _s ## _len);\ + if (r < 0) {\ + REDEBUG("Failed casting " #_s " operand: %s", fr_strerror());\ + rcode = -1;\ + goto finish;\ + }\ + if (cast && cast->flags.is_pointer) _s ## _cast_buff = _s ## _cast.ptr;\ + _s ## _type = cast_type;\ + _s ## _len = (size_t)r;\ + _s = &_s ## _cast;\ + }\ +} while (0) + +#define CHECK_INT_CAST(_l, _r) \ +do {\ + if ((cast_type == PW_TYPE_INVALID) &&\ + _l && (_l ## _type == PW_TYPE_STRING) &&\ + _r && (_r ## _type == PW_TYPE_STRING) &&\ + all_digits(lhs->strvalue) && all_digits(rhs->strvalue)) {\ + cast_type = PW_TYPE_INTEGER64;\ + EVAL_DEBUG("OPERANDS ARE NUMBER STRINGS, SETTING CAST TO integer64");\ + }\ +} while (0) + + /* + * Regular expressions need both operands to be strings + */ +#ifdef HAVE_REGEX + if (map->op == T_OP_REG_EQ) { + cast_type = PW_TYPE_STRING; + + if (map->rhs->type == TMPL_TYPE_XLAT_STRUCT) escape = regex_escape; + } + else +#endif + /* + * If it's a pair comparison, data gets cast to the + * type of the pair comparison attribute. + * + * Magic attribute is always the LHS. + */ + if (c->pass2_fixup == PASS2_PAIRCOMPARE) { + rad_assert(!c->cast); + rad_assert(map->lhs->type == TMPL_TYPE_ATTR); +#ifndef NDEBUG + /* expensive assert */ + rad_assert((map->rhs->type != TMPL_TYPE_ATTR) || !radius_find_compare(map->rhs->tmpl_da)); +#endif + cast = map->lhs->tmpl_da; + cast_type = cast->type; + + EVAL_DEBUG("NORMALISATION TYPE %s (PAIRCMP TYPE)", + fr_int2str(dict_attr_types, cast->type, "<INVALID>")); + /* + * Otherwise we use the explicit cast, or implicit + * cast (from an attribute reference). + * We already have the data for the lhs, so we convert + * it here. + */ + } else if (c->cast) { + cast = c->cast; + EVAL_DEBUG("NORMALISATION TYPE %s (EXPLICIT CAST)", + fr_int2str(dict_attr_types, cast->type, "<INVALID>")); + } else if (map->lhs->type == TMPL_TYPE_ATTR) { + cast = map->lhs->tmpl_da; + EVAL_DEBUG("NORMALISATION TYPE %s (IMPLICIT FROM LHS REF)", + fr_int2str(dict_attr_types, cast->type, "<INVALID>")); + } else if (map->rhs->type == TMPL_TYPE_ATTR) { + cast = map->rhs->tmpl_da; + EVAL_DEBUG("NORMALISATION TYPE %s (IMPLICIT FROM RHS REF)", + fr_int2str(dict_attr_types, cast->type, "<INVALID>")); + } else if (map->lhs->type == TMPL_TYPE_DATA) { + cast_type = map->lhs->tmpl_data_type; + EVAL_DEBUG("NORMALISATION TYPE %s (IMPLICIT FROM LHS DATA)", + fr_int2str(dict_attr_types, cast_type, "<INVALID>")); + } else if (map->rhs->type == TMPL_TYPE_DATA) { + cast_type = map->rhs->tmpl_data_type; + EVAL_DEBUG("NORMALISATION TYPE %s (IMPLICIT FROM RHS DATA)", + fr_int2str(dict_attr_types, cast_type, "<INVALID>")); + } + + if (cast) cast_type = cast->type; + + switch (map->rhs->type) { + case TMPL_TYPE_ATTR: + { + VALUE_PAIR *vp; + vp_cursor_t cursor; + + for (vp = tmpl_cursor_init(&rcode, &cursor, request, map->rhs); + vp; + vp = tmpl_cursor_next(&cursor, map->rhs)) { + rhs_type = vp->da->type; + rhs_enumv = vp->da; + rhs = &vp->data; + rhs_len = vp->vp_length; + + CHECK_INT_CAST(lhs, rhs); + CAST(lhs); + CAST(rhs); + + rcode = cond_cmp_values(request, c, lhs_type, lhs, lhs_len, rhs_type, rhs, rhs_len); + if (rcode != 0) break; + + TALLOC_FREE(rhs_cast_buff); + } + } + break; + + case TMPL_TYPE_DATA: + rhs_type = map->rhs->tmpl_data_type; + rhs = &map->rhs->tmpl_data_value; + rhs_len = map->rhs->tmpl_data_length; + + CHECK_INT_CAST(lhs, rhs); + CAST(lhs); + CAST(rhs); + + rcode = cond_cmp_values(request, c, lhs_type, lhs, lhs_len, rhs_type, rhs, rhs_len); + break; + + /* + * Expanded types start as strings, then get converted + * to the type of the attribute or the explicit cast. + */ + case TMPL_TYPE_LITERAL: + case TMPL_TYPE_EXEC: + case TMPL_TYPE_XLAT: + case TMPL_TYPE_XLAT_STRUCT: + { + ssize_t ret; + value_data_t data; + + if (map->rhs->type != TMPL_TYPE_LITERAL) { + char *p; + + ret = tmpl_aexpand(request, &p, request, map->rhs, escape, NULL); + if (ret < 0) { + EVAL_DEBUG("FAIL [%i]", __LINE__); + rcode = -1; + goto finish; + } + data.strvalue = p; + rhs_len = ret; + + } else { + data.strvalue = map->rhs->name; + rhs_len = map->rhs->len; + } + rad_assert(data.strvalue); + + rhs_type = PW_TYPE_STRING; + rhs = &data; + + CHECK_INT_CAST(lhs, rhs); + CAST(lhs); + CAST(rhs); + + rcode = cond_cmp_values(request, c, lhs_type, lhs, lhs_len, rhs_type, rhs, rhs_len); + if (map->rhs->type != TMPL_TYPE_LITERAL)talloc_free(data.ptr); + + break; + } + + /* + * RHS is a compiled regex, we don't need to do anything with it. + */ + case TMPL_TYPE_REGEX_STRUCT: + CAST(lhs); + rcode = cond_cmp_values(request, c, lhs_type, lhs, lhs_len, PW_TYPE_INVALID, NULL, 0); + break; + /* + * Unsupported types (should have been parse errors) + */ + case TMPL_TYPE_NULL: + case TMPL_TYPE_LIST: + case TMPL_TYPE_UNKNOWN: + case TMPL_TYPE_ATTR_UNDEFINED: + case TMPL_TYPE_REGEX: /* Should now be a TMPL_TYPE_REGEX_STRUCT or TMPL_TYPE_XLAT_STRUCT */ + rad_assert(0); + rcode = -1; + break; + } + +finish: + talloc_free(lhs_cast_buff); + talloc_free(rhs_cast_buff); + + return rcode; +} + + +/** Evaluate a map + * + * @param[in] request the REQUEST + * @param[in] modreturn the previous module return code + * @param[in] depth of the recursion (only used for debugging) + * @param[in] c the condition to evaluate + * @return -1 on error, 0 for "no match", 1 for "match". + */ +int radius_evaluate_map(REQUEST *request, UNUSED int modreturn, UNUSED int depth, fr_cond_t const *c) +{ + int rcode = 0; + + vp_map_t const *map = c->data.map; + + EVAL_DEBUG(">>> MAP TYPES LHS: %s, RHS: %s", + fr_int2str(tmpl_names, map->lhs->type, "???"), + fr_int2str(tmpl_names, map->rhs->type, "???")); + + switch (map->lhs->type) { + /* + * LHS is an attribute or list + */ + case TMPL_TYPE_LIST: + case TMPL_TYPE_ATTR: + { + VALUE_PAIR *vp; + vp_cursor_t cursor; + /* + * Legacy paircompare call, skip processing the magic attribute + * if it's the LHS and cast RHS to the same type. + */ + if ((c->pass2_fixup == PASS2_PAIRCOMPARE) && (map->op != T_OP_REG_EQ)) { +#ifndef NDEBUG + rad_assert(radius_find_compare(map->lhs->tmpl_da)); /* expensive assert */ +#endif + rcode = cond_normalise_and_cmp(request, c, PW_TYPE_INVALID, NULL, NULL, 0); + break; + } + for (vp = tmpl_cursor_init(&rcode, &cursor, request, map->lhs); + vp; + vp = tmpl_cursor_next(&cursor, map->lhs)) { + /* + * Evaluate all LHS values, condition evaluates to true + * if we get at least one set of operands that + * evaluates to true. + */ + rcode = cond_normalise_and_cmp(request, c, vp->da->type, vp->da, &vp->data, vp->vp_length); + if (rcode != 0) break; + } + } + break; + + case TMPL_TYPE_DATA: + rcode = cond_normalise_and_cmp(request, c, + map->lhs->tmpl_data_type, NULL, &map->lhs->tmpl_data_value, + map->lhs->tmpl_data_length); + break; + + case TMPL_TYPE_LITERAL: + case TMPL_TYPE_EXEC: + case TMPL_TYPE_XLAT: + case TMPL_TYPE_XLAT_STRUCT: + { + ssize_t ret; + value_data_t data; + + if (map->lhs->type != TMPL_TYPE_LITERAL) { + char *p; + + ret = tmpl_aexpand(request, &p, request, map->lhs, NULL, NULL); + if (ret < 0) { + EVAL_DEBUG("FAIL [%i]", __LINE__); + return ret; + } + data.strvalue = p; + } else { + data.strvalue = map->lhs->name; + ret = map->lhs->len; + } + rad_assert(data.strvalue); + + rcode = cond_normalise_and_cmp(request, c, PW_TYPE_STRING, NULL, &data, ret); + if (map->lhs->type != TMPL_TYPE_LITERAL) talloc_free(data.ptr); + } + break; + + /* + * Unsupported types (should have been parse errors) + */ + case TMPL_TYPE_NULL: + case TMPL_TYPE_ATTR_UNDEFINED: + case TMPL_TYPE_UNKNOWN: + case TMPL_TYPE_REGEX: /* should now be a TMPL_TYPE_REGEX_STRUCT or TMPL_TYPE_XLAT_STRUCT */ + case TMPL_TYPE_REGEX_STRUCT: /* not allowed as LHS */ + rad_assert(0); + rcode = -1; + break; + } + + EVAL_DEBUG("<<<"); + + return rcode; +} + +/** Evaluate a fr_cond_t; + * + * @param[in] request the REQUEST + * @param[in] modreturn the previous module return code + * @param[in] depth of the recursion (only used for debugging) + * @param[in] c the condition to evaluate + * @return -1 on failure, -2 on attribute not found, 0 for "no match", 1 for "match". + */ +int radius_evaluate_cond(REQUEST *request, int modreturn, int depth, fr_cond_t const *c) +{ + int rcode = -1; +#ifdef WITH_EVAL_DEBUG + char buffer[1024]; + + fr_cond_sprint(buffer, sizeof(buffer), c); + EVAL_DEBUG("%s", buffer); +#endif + + while (c) { + switch (c->type) { + case COND_TYPE_EXISTS: + rcode = radius_evaluate_tmpl(request, modreturn, depth, c->data.vpt); + /* Existence checks are special, because we expect them to fail */ + if (rcode < 0) rcode = 0; + break; + + case COND_TYPE_MAP: + rcode = radius_evaluate_map(request, modreturn, depth, c); + break; + + case COND_TYPE_CHILD: + rcode = radius_evaluate_cond(request, modreturn, depth + 1, c->data.child); + break; + + case COND_TYPE_TRUE: + rcode = true; + break; + + case COND_TYPE_FALSE: + rcode = false; + break; + default: + EVAL_DEBUG("FAIL %d", __LINE__); + return -1; + } + + if (rcode < 0) return rcode; + + if (c->negate) rcode = !rcode; + + if (!c->next) break; + + /* + * FALSE && ... = FALSE + */ + if (!rcode && (c->next_op == COND_AND)) return false; + + /* + * TRUE || ... = TRUE + */ + if (rcode && (c->next_op == COND_OR)) return true; + + c = c->next; + } + + if (rcode < 0) { + EVAL_DEBUG("FAIL %d", __LINE__); + } + return rcode; +} +#endif + + +/* + * The fr_pair_list_move() function in src/lib/pair.c does all sorts of + * extra magic that we don't want here. + * + * FIXME: integrate this with the code calling it, so that we + * only fr_pair_list_copy() those attributes that we're really going to + * use. + */ +void radius_pairmove(REQUEST *request, VALUE_PAIR **to, VALUE_PAIR *from, bool do_xlat) +{ + int i, j, count, from_count, to_count, tailto; + vp_cursor_t cursor; + VALUE_PAIR *vp, *next, **last; + VALUE_PAIR **from_list, **to_list; + VALUE_PAIR *append, **append_tail; + VALUE_PAIR *prepend; + VALUE_PAIR *to_copy; + bool *edited = NULL; + REQUEST *fixup = NULL; + TALLOC_CTX *ctx; + + /* + * Set up arrays for editing, to remove some of the + * O(N^2) dependencies. This also makes it easier to + * insert and remove attributes. + * + * It also means that the operators apply ONLY to the + * attributes in the original list. With the previous + * implementation of fr_pair_list_move(), adding two attributes + * via "+=" and then "=" would mean that the second one + * wasn't added, because of the existence of the first + * one in the "to" list. This implementation doesn't + * have that bug. + * + * Also, the previous implementation did NOT implement + * "-=" correctly. If two of the same attributes existed + * in the "to" list, and you tried to subtract something + * matching the *second* value, then the fr_pair_delete_by_num() + * function was called, and the *all* attributes of that + * number were deleted. With this implementation, only + * the matching attributes are deleted. + */ + count = 0; + for (vp = fr_cursor_init(&cursor, &from); vp; vp = fr_cursor_next(&cursor)) count++; + from_list = talloc_array(request, VALUE_PAIR *, count); + + for (vp = fr_cursor_init(&cursor, to); vp; vp = fr_cursor_next(&cursor)) count++; + to_list = talloc_array(request, VALUE_PAIR *, count); + + prepend = NULL; + + append = NULL; + append_tail = &append; + + /* + * Move the lists to the arrays, and break the list + * chains. + */ + from_count = 0; + for (vp = from; vp != NULL; vp = next) { + next = vp->next; + from_list[from_count++] = vp; + vp->next = NULL; + } + + to_count = 0; + ctx = talloc_parent(*to); + to_copy = fr_pair_list_copy(ctx, *to); + for (vp = to_copy; vp != NULL; vp = next) { + next = vp->next; + to_list[to_count++] = vp; + vp->next = NULL; + } + tailto = to_count; + edited = talloc_zero_array(request, bool, to_count); + + RDEBUG4("::: FROM %d TO %d MAX %d", from_count, to_count, count); + + /* + * Now that we have the lists initialized, start working + * over them. + */ + for (i = 0; i < from_count; i++) { + int found; + + RDEBUG4("::: Examining %s", from_list[i]->da->name); + + if (do_xlat) radius_xlat_do(request, from_list[i]); + + /* + * Attribute should be appended, OR the "to" list + * is empty, and we're supposed to replace or + * "add if not existing". + */ + if (from_list[i]->op == T_OP_ADD) goto do_append; + + /* + * The attribute needs to be prepended to the "to" + * list - store it in the prepend list + */ + + if (from_list[i]->op == T_OP_PREPEND) { + RDEBUG4("::: PREPENDING %s FROM %d TO %d", + from_list[i]->da->name, i, tailto); + from_list[i]->next = prepend; + prepend = from_list[i]; + prepend->op = T_OP_EQ; + from_list[i] = NULL; + continue; + } + found = false; + for (j = 0; j < to_count; j++) { + if (edited[j] || !to_list[j] || !from_list[i]) continue; + + /* + * Attributes aren't the same, skip them. + */ + if (from_list[i]->da != to_list[j]->da) { + continue; + } + + /* + * We don't use a "switch" statement here + * because we want to break out of the + * "for" loop over 'j' in most cases. + */ + + /* + * Over-write the FIRST instance of the + * matching attribute name. We free the + * one in the "to" list, and move over + * the one in the "from" list. + */ + if (from_list[i]->op == T_OP_SET) { + RDEBUG4("::: OVERWRITING %s FROM %d TO %d", + to_list[j]->da->name, i, j); + fr_pair_list_free(&to_list[j]); + to_list[j] = from_list[i]; + from_list[i] = NULL; + edited[j] = true; + break; + } + + /* + * Add the attribute only if it does not + * exist... but it exists, so we stop + * looking. + */ + if (from_list[i]->op == T_OP_EQ) { + found = true; + break; + } + + /* + * Delete every attribute, independent + * of its value. + */ + if (from_list[i]->op == T_OP_CMP_FALSE) { + goto delete; + } + + /* + * Delete all matching attributes from + * "to" + */ + if ((from_list[i]->op == T_OP_SUB) || + (from_list[i]->op == T_OP_CMP_EQ) || + (from_list[i]->op == T_OP_LE) || + (from_list[i]->op == T_OP_GE)) { + int rcode; + int old_op = from_list[i]->op; + + /* + * Check for equality. + */ + from_list[i]->op = T_OP_CMP_EQ; + + /* + * If equal, delete the one in + * the "to" list. + */ + rcode = radius_compare_vps(NULL, from_list[i], + to_list[j]); + /* + * We may want to do more + * subtractions, so we re-set the + * operator back to it's original + * value. + */ + from_list[i]->op = old_op; + + switch (old_op) { + case T_OP_CMP_EQ: + if (rcode != 0) goto delete; + break; + + case T_OP_SUB: + if (rcode == 0) { + delete: + RDEBUG4("::: DELETING %s FROM %d TO %d", + from_list[i]->da->name, i, j); + fr_pair_list_free(&to_list[j]); + to_list[j] = NULL; + } + break; + + /* + * Enforce <=. If it's + * >, replace it. + */ + case T_OP_LE: + if (rcode > 0) { + RDEBUG4("::: REPLACING %s FROM %d TO %d", + from_list[i]->da->name, i, j); + fr_pair_list_free(&to_list[j]); + to_list[j] = from_list[i]; + from_list[i] = NULL; + edited[j] = true; + } + break; + + case T_OP_GE: + if (rcode < 0) { + RDEBUG4("::: REPLACING %s FROM %d TO %d", + from_list[i]->da->name, i, j); + fr_pair_list_free(&to_list[j]); + to_list[j] = from_list[i]; + from_list[i] = NULL; + edited[j] = true; + } + break; + } + + continue; + } + + rad_assert(0 == 1); /* panic! */ + } + + /* + * We were asked to add it if it didn't exist, + * and it doesn't exist. Move it over to the + * tail of the "to" list, UNLESS it was already + * moved by another operator. + */ + if (!found && from_list[i]) { + if ((from_list[i]->op == T_OP_EQ) || + (from_list[i]->op == T_OP_LE) || + (from_list[i]->op == T_OP_GE) || + (from_list[i]->op == T_OP_SET)) { + do_append: + RDEBUG4("::: APPENDING %s FROM %d TO %d", + from_list[i]->da->name, i, tailto); + *append_tail = from_list[i]; + from_list[i]->op = T_OP_EQ; + from_list[i] = NULL; + append_tail = &(*append_tail)->next; + } + } + } + + /* + * Delete attributes in the "from" list. + */ + for (i = 0; i < from_count; i++) { + if (!from_list[i]) continue; + fr_pair_list_free(&from_list[i]); + } + talloc_free(from_list); + + RDEBUG4("::: TO in %d out %d", to_count, tailto); + + /* + * Re-chain the "to" list. + */ + fr_pair_list_free(to); + last = to; + + if (to == &request->packet->vps) { + fixup = request; + } else if (request->parent && (to == &request->parent->packet->vps)) { + fixup = request->parent; + } + + /* + * Walk the list of "prepend" attributes first + */ + for (vp = prepend; vp != NULL; vp = vp->next) { + *last = vp; + last = &(*last)->next; + } + + /* + * Next add on remaining items in the "to" list + */ + for (i = 0; i < tailto; i++) { + if (!to_list[i]) continue; + + vp = to_list[i]; + RDEBUG4("::: to[%d] = %s", i, vp->da->name); + + /* + * Mash the operator to a simple '='. The + * operators in the "to" list aren't used for + * anything. BUT they're used in the "detail" + * file and debug output, where we don't want to + * see the operators. + */ + vp->op = T_OP_EQ; + + *last = vp; + last = &(*last)->next; + } + + /* + * And finally add in the attributes we're appending to + * the tail of the "to" list. + */ + *last = append; + + /* + * Fix dumb cache issues + */ + if (fixup) { + fixup->username = NULL; + fixup->password = NULL; + + for (vp = fixup->packet->vps; vp != NULL; vp = vp->next) { + if (vp->da->vendor) continue; + + if ((vp->da->attr == PW_USER_NAME) && !fixup->username) { + fixup->username = vp; + + } else if (vp->da->attr == PW_STRIPPED_USER_NAME) { + fixup->username = vp; + + } else if (vp->da->attr == PW_USER_PASSWORD) { + fixup->password = vp; + } + } + } + + rad_assert(request->packet != NULL); + + talloc_free(to_list); + talloc_free(edited); +} diff --git a/src/main/exec.c b/src/main/exec.c new file mode 100644 index 0000000..67243f7 --- /dev/null +++ b/src/main/exec.c @@ -0,0 +1,633 @@ +/* + * This program 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 exec.c + * @brief Execute external programs. + * + * @copyright 2000-2004,2006 The FreeRADIUS server project + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/rad_assert.h> + +#include <sys/file.h> + +#include <fcntl.h> +#include <ctype.h> + +#ifdef HAVE_SYS_WAIT_H +# include <sys/wait.h> +#endif +#ifndef WEXITSTATUS +# define WEXITSTATUS(stat_val) ((unsigned)(stat_val) >> 8) +#endif +#ifndef WIFEXITED +# define WIFEXITED(stat_val) (((stat_val) & 255) == 0) +#endif + +#define MAX_ARGV (256) + +/** Start a process + * + * @param cmd Command to execute. This is parsed into argv[] parts, + * then each individual argv part is xlat'ed. + * @param request Current reuqest + * @param exec_wait set to 1 if you want to read from or write to child + * @param[in,out] input_fd pointer to int, receives the stdin file. + * descriptor. Set to NULL and the child will have /dev/null on stdin + * @param[in,out] output_fd pinter to int, receives the stdout file + * descriptor. Set to NULL and child will have /dev/null on stdout. + * @param input_pairs list of value pairs - these will be put into + * the environment variables of the child. + * @param shell_escape values before passing them as arguments. + * @return PID of the child process, -1 on error. + */ +pid_t radius_start_program(char const *cmd, REQUEST *request, bool exec_wait, + int *input_fd, int *output_fd, + VALUE_PAIR *input_pairs, bool shell_escape) +{ +#ifndef __MINGW32__ + VALUE_PAIR *vp; + int n; + int to_child[2] = {-1, -1}; + int from_child[2] = {-1, -1}; + pid_t pid; +#endif + int argc; + int i; + char const **argv_p; + char *argv[MAX_ARGV], **argv_start = argv; + char argv_buf[4096]; +#define MAX_ENVP 1024 + char *envp[MAX_ENVP]; + int envlen = 0; + + /* + * Stupid array decomposition... + * + * If we do memcpy(&argv_p, &argv, sizeof(argv_p)) src ends up being a char ** + * pointing to the value of the first element. + */ + memcpy(&argv_p, &argv_start, sizeof(argv_p)); + argc = rad_expand_xlat(request, cmd, MAX_ARGV, argv_p, true, sizeof(argv_buf), argv_buf); + if (argc <= 0) { + DEBUG("invalid command line '%s'.", cmd); + return -1; + } + + +#ifndef NDEBUG + if (rad_debug_lvl > 2) { + DEBUG3("executing cmd %s", cmd); + for (i = 0; i < argc; i++) { + DEBUG3("\t[%d] %s", i, argv[i]); + } + } +#endif + +#ifndef __MINGW32__ + /* + * Open a pipe for child/parent communication, if necessary. + */ + if (exec_wait) { + if (input_fd) { + if (pipe(to_child) != 0) { + DEBUG("Couldn't open pipe to child: %s", fr_syserror(errno)); + return -1; + } + } + if (output_fd) { + if (pipe(from_child) != 0) { + DEBUG("Couldn't open pipe from child: %s", fr_syserror(errno)); + /* safe because these either need closing or are == -1 */ + close(to_child[0]); + close(to_child[1]); + return -1; + } + } + } + + envp[0] = NULL; + + if (input_pairs) { + vp_cursor_t cursor; + char buffer[1024]; + + /* + * Set up the environment variables in the + * parent, so we don't call libc functions that + * hold mutexes. They might be locked when we fork, + * and will remain locked in the child. + */ + for (vp = fr_cursor_init(&cursor, &input_pairs); + vp; + vp = fr_cursor_next(&cursor)) { + /* + * Hmm... maybe we shouldn't pass the + * user's password in an environment + * variable... + */ + snprintf(buffer, sizeof(buffer), "%s=", vp->da->name); + if (shell_escape) { + char *p; + + for (p = buffer; *p != '='; p++) { + if (*p == '-') { + *p = '_'; + } else if (isalpha((uint8_t) *p)) { + *p = toupper((uint8_t) *p); + } + } + } + + n = strlen(buffer); + vp_prints_value(buffer + n, sizeof(buffer) - n, vp, shell_escape ? '"' : 0); + + envp[envlen++] = strdup(buffer); + + /* + * Don't add too many attributes. + */ + if (envlen == (MAX_ENVP - 1)) break; + + /* + * NULL terminate for execve + */ + envp[envlen] = NULL; + } + } + + if (exec_wait) { + pid = rad_fork(); /* remember PID */ + } else { + pid = fork(); /* don't wait */ + } + + if (pid == 0) { + int devnull; + + /* + * Child process. + * + * We try to be fail-safe here. So if ANYTHING + * goes wrong, we exit with status 1. + */ + + /* + * Open STDIN to /dev/null + */ + devnull = open("/dev/null", O_RDWR); + if (devnull < 0) { + DEBUG("Failed opening /dev/null: %s\n", fr_syserror(errno)); + + /* + * Where the status code is interpreted as a module rcode + * one is subtracted from it, to allow 0 to equal success + * + * 2 is RLM_MODULE_FAIL + 1 + */ + exit(2); + } + + /* + * Only massage the pipe handles if the parent + * has created them. + */ + if (exec_wait) { + if (input_fd) { + close(to_child[1]); + dup2(to_child[0], STDIN_FILENO); + } else { + dup2(devnull, STDIN_FILENO); + } + + if (output_fd) { + close(from_child[0]); + dup2(from_child[1], STDOUT_FILENO); + } else { + dup2(devnull, STDOUT_FILENO); + } + + } else { /* no pipe, STDOUT should be /dev/null */ + dup2(devnull, STDIN_FILENO); + dup2(devnull, STDOUT_FILENO); + } + + /* + * If we're not debugging, then we can't do + * anything with the error messages, so we throw + * them away. + * + * If we are debugging, then we want the error + * messages to go to the STDERR of the server. + */ + if (rad_debug_lvl == 0) { + dup2(devnull, STDERR_FILENO); + } + close(devnull); + + /* + * The server may have MANY FD's open. We don't + * want to leave dangling FD's for the child process + * to play funky games with, so we close them. + */ + closefrom(3); + + /* + * I swear the signature for execve is wrong and should + * take 'char const * const argv[]'. + * + * Note: execve(), unlike system(), treats all the space + * delimited arguments as literals, so there's no need + * to perform additional escaping. + */ + execve(argv[0], argv, envp); + printf("Failed to execute \"%s\": %s", argv[0], fr_syserror(errno)); /* fork output will be captured */ + + /* + * Where the status code is interpreted as a module rcode + * one is subtracted from it, to allow 0 to equal success + * + * 2 is RLM_MODULE_FAIL + 1 + */ + exit(2); + } + + /* + * Free child environment variables + */ + for (i = 0; i < envlen; i++) { + free(envp[i]); + } + + /* + * Parent process. + */ + if (pid < 0) { + DEBUG("Couldn't fork %s: %s", argv[0], fr_syserror(errno)); + if (exec_wait) { + /* safe because these either need closing or are == -1 */ + close(to_child[0]); + close(to_child[1]); + close(from_child[0]); + close(from_child[1]); + } + return -1; + } + + /* + * We're not waiting, exit, and ignore any child's status. + */ + if (exec_wait) { + /* + * Close the ends of the pipe(s) the child is using + * return the ends of the pipe(s) our caller wants + * + */ + if (input_fd) { + *input_fd = to_child[1]; + close(to_child[0]); + } + if (output_fd) { + *output_fd = from_child[0]; + close(from_child[1]); + } + } + + return pid; +#else + if (exec_wait) { + DEBUG("Wait is not supported"); + return -1; + } + + { + /* + * The _spawn and _exec families of functions are + * found in Windows compiler libraries for + * portability from UNIX. There is a variety of + * functions, including the ability to pass + * either a list or array of parameters, to + * search in the PATH or otherwise, and whether + * or not to pass an environment (a set of + * environment variables). Using _spawn, you can + * also specify whether you want the new process + * to close your program (_P_OVERLAY), to wait + * until the new process is finished (_P_WAIT) or + * for the two to run concurrently (_P_NOWAIT). + + * _spawn and _exec are useful for instances in + * which you have simple requirements for running + * the program, don't want the overhead of the + * Windows header file, or are interested + * primarily in portability. + */ + + /* + * FIXME: check return code... what is it? + */ + _spawnve(_P_NOWAIT, argv[0], argv, envp); + } + + return 0; +#endif +} + +/** Read from the child process. + * + * @param fd file descriptor to read from. + * @param pid pid of child, will be reaped if it dies. + * @param timeout amount of time to wait, in seconds. + * @param answer buffer to write into. + * @param left length of buffer. + * @return -1 on error, or length of output. + */ +int radius_readfrom_program(int fd, pid_t pid, int timeout, + char *answer, int left) +{ + int done = 0; +#ifndef __MINGW32__ + int status; + struct timeval start; +#ifdef O_NONBLOCK + bool nonblock = true; +#endif + +#ifdef O_NONBLOCK + /* + * Try to set it non-blocking. + */ + do { + int flags; + + if ((flags = fcntl(fd, F_GETFL, NULL)) < 0) { + nonblock = false; + break; + } + + flags |= O_NONBLOCK; + if( fcntl(fd, F_SETFL, flags) < 0) { + nonblock = false; + break; + } + } while (0); +#endif + + + /* + * Read from the pipe until we doesn't get any more or + * until the message is full. + */ + gettimeofday(&start, NULL); + while (1) { + int rcode; + fd_set fds; + struct timeval when, elapsed, wake; + + FD_ZERO(&fds); + FD_SET(fd, &fds); + + gettimeofday(&when, NULL); + rad_tv_sub(&when, &start, &elapsed); + if (elapsed.tv_sec >= timeout) goto too_long; + + when.tv_sec = timeout; + when.tv_usec = 0; + rad_tv_sub(&when, &elapsed, &wake); + + rcode = select(fd + 1, &fds, NULL, NULL, &wake); + if (rcode == 0) { + too_long: + DEBUG("Child PID %u is taking too much time: forcing failure and killing child.", (unsigned int) pid); + kill(pid, SIGTERM); + close(fd); /* should give SIGPIPE to child, too */ + + /* + * Clean up the child entry. + */ + rad_waitpid(pid, &status); + return -1; + } + if (rcode < 0) { + if (errno == EINTR) continue; + break; + } + +#ifdef O_NONBLOCK + /* + * Read as many bytes as possible. The kernel + * will return the number of bytes available. + */ + if (nonblock) { + status = read(fd, answer + done, left); + } else +#endif + /* + * There's at least 1 byte ready: read it. + */ + status = read(fd, answer + done, 1); + + /* + * Nothing more to read: stop. + */ + if (status == 0) { + break; + } + + /* + * Error: See if we have to continue. + */ + if (status < 0) { + /* + * We were interrupted: continue reading. + */ + if (errno == EINTR) { + continue; + } + + /* + * There was another error. Most likely + * The child process has finished, and + * exited. + */ + break; + } + + done += status; + left -= status; + if (left <= 0) break; + } +#endif /* __MINGW32__ */ + + /* Strip trailing new lines */ + while ((done > 0) && (answer[done - 1] == '\n')) { + answer[--done] = '\0'; + } + + return done; +} + +/** Execute a program. + * + * @param[in,out] ctx to allocate new VALUE_PAIR (s) in. + * @param[out] out buffer to append plaintext (non valuepair) output. + * @param[in] outlen length of out buffer. + * @param[out] output_pairs list of value pairs - child stdout will be parsed and added into this list + * of value pairs. + * @param[in] request Current request (may be NULL). + * @param[in] cmd Command to execute. This is parsed into argv[] parts, then each individual argv part + * is xlat'ed. + * @param[in] input_pairs list of value pairs - these will be available in the environment of the child. + * @param[in] exec_wait set to 1 if you want to read from or write to child. + * @param[in] shell_escape values before passing them as arguments. + * @param[in] timeout amount of time to wait, in seconds. + + * @return 0 if exec_wait==0, exit code if exec_wait!=0, -1 on error. + */ +int radius_exec_program(TALLOC_CTX *ctx, char *out, size_t outlen, VALUE_PAIR **output_pairs, + REQUEST *request, char const *cmd, VALUE_PAIR *input_pairs, + bool exec_wait, bool shell_escape, int timeout) + +{ + pid_t pid; + int from_child; +#ifndef __MINGW32__ + char *p; + pid_t child_pid; + int comma = 0; + int status, ret = 0; + ssize_t len; + char answer[4096]; +#endif + + RDEBUG2("Executing: %s:", cmd); + + if (out) *out = '\0'; + + pid = radius_start_program(cmd, request, exec_wait, NULL, &from_child, input_pairs, shell_escape); + if (pid < 0) { + return -1; + } + + if (!exec_wait) { + return 0; + } + +#ifndef __MINGW32__ + len = radius_readfrom_program(from_child, pid, timeout, answer, sizeof(answer)); + if (len < 0) { + /* + * Failure - radius_readfrom_program will + * have called close(from_child) for us + */ + RERROR("Failed to read from child output"); + return -1; + + } + answer[len] = '\0'; + + /* + * Make sure that the writer can't block while writing to + * a pipe that no one is reading from anymore. + */ + close(from_child); + + if (len == 0) { + goto wait; + } + + /* + * Parse the output, if any. + */ + if (output_pairs) { + /* + * HACK: Replace '\n' with ',' so that + * fr_pair_list_afrom_str() can parse the buffer in + * one go (the proper way would be to + * fix fr_pair_list_afrom_str(), but oh well). + */ + for (p = answer; *p; p++) { + if (*p == '\n') { + *p = comma ? ' ' : ','; + p++; + comma = 0; + } + if (*p == ',') { + comma++; + } + } + + /* + * Replace any trailing comma by a NUL. + */ + if (answer[len - 1] == ',') { + answer[--len] = '\0'; + } + + if (fr_pair_list_afrom_str(ctx, answer, output_pairs) == T_INVALID) { + RERROR("Failed parsing output from: %s: %s", cmd, fr_strerror()); + if (out) strlcpy(out, answer, len); + ret = -1; + } + + VERIFY_REQUEST(request); + + + /* + * We've not been told to extract output pairs, + * just copy the programs output to the out + * buffer. + */ + + } else if (out) { + strlcpy(out, answer, outlen); + } + + /* + * Call rad_waitpid (should map to waitpid on non-threaded + * or single-server systems). + */ +wait: + child_pid = rad_waitpid(pid, &status); + if (child_pid == 0) { + RERROR("Timeout waiting for child"); + + return -2; + } + + if (child_pid == pid) { + if (WIFEXITED(status)) { + status = WEXITSTATUS(status); + if ((status != 0) || (ret < 0)) { + RERROR("Program returned code (%d) and output '%s'", status, answer); + } else { + RDEBUG2("Program returned code (%d) and output '%s'", status, answer); + } + + return ret < 0 ? ret : status; + } + } + + RERROR("Abnormal child exit: %s", fr_syserror(errno)); +#endif /* __MINGW32__ */ + + return -1; +} diff --git a/src/main/exfile.c b/src/main/exfile.c new file mode 100644 index 0000000..59e6a05 --- /dev/null +++ b/src/main/exfile.c @@ -0,0 +1,547 @@ +/* + * This program 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 exfile.c + * @brief Allow multiple threads to write to the same set of files. + * + * @author Alan DeKok <aland@freeradius.org> + * @copyright 2014 The FreeRADIUS server project + */ +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/exfile.h> + +#include <sys/stat.h> +#include <fcntl.h> + +typedef struct exfile_entry_t { + int fd; //!< File descriptor associated with an entry. + uint32_t hash; //!< Hash for cheap comparison. + time_t last_used; //!< Last time the entry was used. + dev_t st_dev; //!< device inode + ino_t st_ino; //!< inode number + char *filename; //!< Filename. +} exfile_entry_t; + + +struct exfile_t { + uint32_t max_entries; //!< How many file descriptors we keep track of. + uint32_t max_idle; //!< Maximum idle time for a descriptor. + time_t last_cleaned; + +#ifdef HAVE_PTHREAD_H + pthread_mutex_t mutex; +#endif + exfile_entry_t *entries; + bool locking; +}; + + +#ifdef HAVE_PTHREAD_H +#define PTHREAD_MUTEX_LOCK pthread_mutex_lock +#define PTHREAD_MUTEX_UNLOCK pthread_mutex_unlock + +#else +/* + * This is easier than ifdef's throughout the code. + */ +#define PTHREAD_MUTEX_LOCK(_x) +#define PTHREAD_MUTEX_UNLOCK(_x) +#endif + +#define MAX_TRY_LOCK 4 //!< How many times we attempt to acquire a lock + //!< before giving up. + +static int _exfile_free(exfile_t *ef) +{ + uint32_t i; + + PTHREAD_MUTEX_LOCK(&ef->mutex); + + for (i = 0; i < ef->max_entries; i++) { + if (!ef->entries[i].filename) continue; + + close(ef->entries[i].fd); + } + + PTHREAD_MUTEX_UNLOCK(&ef->mutex); + +#ifdef HAVE_PTHREAD_H + pthread_mutex_destroy(&ef->mutex); +#endif + + return 0; +} + + +/** Initialize a way for multiple threads to log to one or more files. + * + * @param ctx The talloc context + * @param max_entries Max file descriptors to cache, and manage locks for. + * @param max_idle Maximum time a file descriptor can be idle before it's closed. + * @param locking whether or not to lock the files. + * @return the new context, or NULL on error. + */ +exfile_t *exfile_init(TALLOC_CTX *ctx, uint32_t max_entries, uint32_t max_idle, bool locking) +{ + exfile_t *ef; + + ef = talloc_zero(ctx, exfile_t); + if (!ef) return NULL; + + ef->max_entries = max_entries; + ef->max_idle = max_idle; + ef->locking = locking; + + /* + * If we're not locking the files, just return the + * handle. Each call to exfile_open() will just open a + * new file descriptor. + */ + if (!locking) return ef; + + ef->entries = talloc_zero_array(ef, exfile_entry_t, max_entries); + if (!ef->entries) { + talloc_free(ef); + return NULL; + } + +#ifdef HAVE_PTHREAD_H + if (pthread_mutex_init(&ef->mutex, NULL) != 0) { + talloc_free(ef); + return NULL; + } +#endif + + talloc_set_destructor(ef, _exfile_free); + + return ef; +} + + +static void exfile_cleanup_entry(exfile_entry_t *entry) +{ + TALLOC_FREE(entry->filename); + + if (entry->fd >= 0) close(entry->fd); + entry->hash = 0; + entry->fd = -1; +} + + +/* + * Try to open the file. If it doesn't exist, try to + * create it's parent directories. + */ +static int exfile_open_mkdir(exfile_t *ef, char const *filename, mode_t permissions) +{ + int fd; + + /* + * Files in /dev/ are special. We don't try to create + * their parent directories, and we don't try to create + * the files. + */ + if (strncmp(filename, "/dev/", 5) == 0) { + int oflag; + + if (((permissions & 0222) == 0) && (permissions & 0444) != 0) { /* !W + R */ + oflag = O_RDONLY; + + } else if (((permissions & 0222) != 0) && (permissions & 0444) == 0) { /* W + !R */ + oflag = O_WRONLY; + + } else { /* unknown, make it R+W */ + oflag = O_RDWR; + } + + fd = open(filename, oflag, permissions); + if (fd < 0) { + fr_strerror_printf("Failed to open file %s: %s", + filename, strerror(errno)); + return -1; + } + + return fd; + } + + fd = open(filename, O_RDWR | O_CREAT, permissions); + if (fd < 0) { + mode_t dirperm; + char *p, *dir; + + /* + * Maybe the directory doesn't exist. Try to + * create it. + */ + dir = talloc_strdup(ef, filename); + if (!dir) return -1; + p = strrchr(dir, FR_DIR_SEP); + if (!p) { + fr_strerror_printf("No '/' in '%s'", filename); + talloc_free(dir); + return -1; + } + *p = '\0'; + + /* + * Ensure that the 'x' bit is set, so that we can + * read the directory. + */ + dirperm = permissions; + if ((dirperm & 0600) != 0) dirperm |= 0100; + if ((dirperm & 0060) != 0) dirperm |= 0010; + if ((dirperm & 0006) != 0) dirperm |= 0001; + + if (rad_mkdir(dir, dirperm, -1, -1) < 0) { + fr_strerror_printf("Failed to create directory %s: %s", + dir, strerror(errno)); + talloc_free(dir); + return -1; + } + talloc_free(dir); + + fd = open(filename, O_RDWR | O_CREAT, permissions); + if (fd < 0) { + fr_strerror_printf("Failed to open file %s: %s", + filename, strerror(errno)); + return -1; + } + } + + return fd; +} + + +/** Open a new log file, or maybe an existing one. + * + * When multithreaded, the FD is locked via a mutex. This way we're + * sure that no other thread is writing to the file. + * + * @param ef The logfile context returned from exfile_init(). + * @param filename the file to open. + * @param permissions to use. + * @return an FD used to write to the file, or -1 on error. + */ +int exfile_open(exfile_t *ef, char const *filename, mode_t permissions, off_t *offset) +{ + int i, found, tries, unused, oldest; + uint32_t hash; + time_t now; + struct stat st; + off_t real_offset; + + if (!ef || !filename) return -1; + + /* + * No locking: just return a new FD. + */ + if (!ef->locking) { + found = exfile_open_mkdir(ef, filename, permissions); + if (found < 0) return -1; + + real_offset = lseek(found, 0, SEEK_END); + if (offset) *offset = real_offset; + return found; + } + + /* + * It's faster to do hash comparisons of a string than + * full string comparisons. + */ + hash = fr_hash_string(filename); + now = time(NULL); + + PTHREAD_MUTEX_LOCK(&ef->mutex); + + /* + * Clean up idle entries. + */ + if (now > (ef->last_cleaned + 1)) { + ef->last_cleaned = now; + + for (i = 0; i < (int) ef->max_entries; i++) { + if (!ef->entries[i].filename) continue; + + if ((ef->entries[i].last_used + ef->max_idle) >= now) continue; + + /* + * This will block forever if a thread is + * doing something stupid. + */ + exfile_cleanup_entry(&ef->entries[i]); + } + } + + /* + * Find the matching entry, or an unused one. + * + * Also track which entry is the oldest, in case there + * are no unused entries. + */ + found = oldest = unused = -1; + for (i = 0; i < (int) ef->max_entries; i++) { + if (!ef->entries[i].filename) { + if (unused < 0) unused = i; + continue; + } + + if ((oldest < 0) || + (ef->entries[i].last_used < ef->entries[oldest].last_used)) { + oldest = i; + } + + /* + * Hash comparisons are fast. String comparisons are slow. + */ + if (ef->entries[i].hash != hash) continue; + + /* + * But we still need to do string comparisons if + * the hash matches, because 1/2^16 filenames + * will result in a hash collision. And that's + * enough filenames in a long-running server to + * ensure that it happens. + */ + if (strcmp(ef->entries[i].filename, filename) != 0) continue; + + found = i; + break; + } + + /* + * If it wasn't found, create a new entry. + */ + if (found < 0) { + /* + * There are no unused entries. Clean up the + * oldest one. + */ + if (unused < 0) { + exfile_cleanup_entry(&ef->entries[oldest]); + unused = oldest; + } + + /* + * Create a new entry. + */ + i = unused; + + ef->entries[i].hash = hash; + ef->entries[i].filename = talloc_strdup(ef->entries, filename); + ef->entries[i].fd = -1; + + /* + * We've just created the entry. Open the file + * and cache the FD. + */ + reopen: + ef->entries[i].fd = exfile_open_mkdir(ef, filename, permissions); + if (ef->entries[i].fd < 0) { + error: + exfile_cleanup_entry(&ef->entries[i]); + PTHREAD_MUTEX_UNLOCK(&(ef->mutex)); + return -1; + } + + if (fstat(ef->entries[i].fd, &st) < 0) goto error; + + /* + * Remember which device and inode this file is + * for. + */ + ef->entries[i].st_dev = st.st_dev; + ef->entries[i].st_ino = st.st_ino; + + } else { + i = found; + + /* + * Stat the *filename*, not the file we opened. + * If that's not the file we opened, then go back + * and re-open the file. + */ + if (stat(ef->entries[i].filename, &st) == 0) { + if ((st.st_dev != ef->entries[i].st_dev) || + (st.st_ino != ef->entries[i].st_ino)) { + /* + * No longer the same file; reopen. + */ + close(ef->entries[i].fd); + goto reopen; + } + } else { + /* + * Error calling stat, likely the + * file has been moved. Reopen it. + */ + close(ef->entries[i].fd); + goto reopen; + } + } + + /* + * Try to lock it. If we can't lock it, it's because + * some reader has re-named the file to "foo.work" and + * locked it. So, we close the current file, re-open it, + * and try again. + */ + + /* + * Lock from the start of the file. It's the + * only point in the file which is guaranteed to + * exist, and to be consistent across all threads + * and processes. + */ + if (lseek(ef->entries[i].fd, 0, SEEK_SET) < 0) { + fr_strerror_printf("Failed to seek in file %s: %s", filename, strerror(errno)); + goto error; + } + + /* + * Busy-loop trying to lock the file. + */ + for (tries = 0; tries < MAX_TRY_LOCK; tries++) { + if (rad_lockfd_nonblock(ef->entries[i].fd, 0) >= 0) break; + + if (errno != EAGAIN) { + fr_strerror_printf("Failed to lock file %s: %s", filename, strerror(errno)); + goto error; + } + + /* + * Close the file and re-open it. It may + * have been deleted. If it was deleted, + * then the new file should now be unlocked. + */ + close(ef->entries[i].fd); + ef->entries[i].fd = open(filename, O_RDWR | O_CREAT, permissions); + if (ef->entries[i].fd < 0) { + fr_strerror_printf("Failed to open file %s: %s", + filename, strerror(errno)); + goto error; + } + } + + if (tries >= MAX_TRY_LOCK) { + fr_strerror_printf("Failed to lock file %s: too many tries", filename); + goto error; + } + + /* + * See which file it really is. + */ + if (fstat(ef->entries[i].fd, &st) < 0) { + fr_strerror_printf("Failed to stat file %s: %s", filename, strerror(errno)); + goto error; + } + + /* + * Maybe the file was unlinked from the file system, OR + * the file we opened is NOT the one we had cached. If + * so, close the file and re-open it from scratch. + */ + if ((st.st_nlink == 0) || + (st.st_dev != ef->entries[i].st_dev) || + (st.st_ino != ef->entries[i].st_ino)) { + close(ef->entries[i].fd); + goto reopen; + } + + /* + * Sometimes the file permissions are changed externally. + * just be sure to update the permission if necessary. + */ + if ((st.st_mode & ~S_IFMT) != permissions) { + char str_need[10], oct_need[5]; + char str_have[10], oct_have[5]; + + rad_mode_to_oct(oct_need, permissions); + rad_mode_to_str(str_need, permissions); + + rad_mode_to_oct(oct_have, st.st_mode & ~S_IFMT); + rad_mode_to_str(str_have, st.st_mode & ~S_IFMT); + + WARN("File %s permissions are %s (%s) not %s (%s))", filename, + oct_have, str_have, oct_need, str_need); + + if (((st.st_mode | permissions) != st.st_mode) && + (fchmod(ef->entries[i].fd, (st.st_mode & ~S_IFMT) | permissions) < 0)) { + rad_mode_to_oct(oct_need, (st.st_mode & ~S_IFMT) | permissions); + rad_mode_to_str(str_need, (st.st_mode & ~S_IFMT) | permissions); + + WARN("Failed resetting file %s permissions to %s (%s): %s", + filename, oct_need, str_need, fr_syserror(errno)); + } + } + + /* + * If we're appending, seek to the end of the file before + * returning the FD to the caller. + */ + real_offset = lseek(ef->entries[i].fd, 0, SEEK_END); + if (offset) *offset = real_offset; + + /* + * Return holding the mutex for the entry. + */ + ef->entries[i].last_used = now; + + return ef->entries[i].fd; +} + +/** Close the log file. Really just return it to the pool. + * + * When multithreaded, the FD is locked via a mutex. This way we're + * sure that no other thread is writing to the file. This function + * will unlock the mutex, so that other threads can write to the file. + * + * @param ef The logfile context returned from exfile_init() + * @param fd the FD to close (i.e. return to the pool) + * @return 0 on success, or -1 on error + */ +int exfile_close(exfile_t *ef, int fd) +{ + uint32_t i; + + /* + * No locking: just close the file. + */ + if (!ef->locking) { + close(fd); + return 0; + } + + /* + * Unlock the bytes that we had previously locked. + */ + for (i = 0; i < ef->max_entries; i++) { + if (ef->entries[i].fd == fd) { + (void) lseek(ef->entries[i].fd, 0, SEEK_SET); + (void) rad_unlockfd(ef->entries[i].fd, 0); + + PTHREAD_MUTEX_UNLOCK(&(ef->mutex)); + return 0; + } + } + + PTHREAD_MUTEX_UNLOCK(&(ef->mutex)); + + fr_strerror_printf("Attempt to unlock file which is not tracked"); + return -1; +} diff --git a/src/main/files.c b/src/main/files.c new file mode 100644 index 0000000..25b6f0d --- /dev/null +++ b/src/main/files.c @@ -0,0 +1,361 @@ +/* + * files.c Read config files into memory. + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2000,2006 The FreeRADIUS server project + * Copyright 2000 Miquel van Smoorenburg <miquels@cistron.nl> + * Copyright 2000 Alan DeKok <aland@ox.org> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/rad_assert.h> + +#include <sys/stat.h> + +#include <ctype.h> +#include <fcntl.h> + +/* + * Debug code. + */ +#if 0 +static void debug_pair_list(PAIR_LIST *pl) +{ + VALUE_PAIR *vp; + + while(pl) { + printf("Pair list: %s\n", pl->name); + printf("** Check:\n"); + for(vp = pl->check; vp; vp = vp->next) { + printf(" "); + fprint_attr_val(stdout, vp); + printf("\n"); + } + printf("** Reply:\n"); + for(vp = pl->reply; vp; vp = vp->next) { + printf(" "); + fprint_attr_val(stdout, vp); + printf("\n"); + } + pl = pl->next; + } +} +#endif + +/* + * Free a PAIR_LIST + */ +void pairlist_free(PAIR_LIST **pl) +{ + talloc_free(*pl); + *pl = NULL; +} + + +#define FIND_MODE_NAME 0 +#define FIND_MODE_WANT_REPLY 1 +#define FIND_MODE_HAVE_REPLY 2 + +/* + * Read the users, huntgroups or hints file. + * Return a PAIR_LIST. + */ +int pairlist_read(TALLOC_CTX *ctx, char const *file, PAIR_LIST **list, int complain) +{ + FILE *fp; + int mode = FIND_MODE_NAME; + char entry[256]; + char buffer[8192]; + char const *ptr; + VALUE_PAIR *check_tmp = NULL; + VALUE_PAIR *reply_tmp = NULL; + PAIR_LIST *pl = NULL, *t; + PAIR_LIST **last = &pl; + int order = 0; + int lineno = 0; + int entry_lineno = 0; + FR_TOKEN parsecode; +#ifdef HAVE_REGEX_H + VALUE_PAIR *vp; + vp_cursor_t cursor; +#endif + char newfile[8192]; + + DEBUG2("reading pairlist file %s", file); + + /* + * Open the file. The error message should be a little + * more useful... + */ + if ((fp = fopen(file, "r")) == NULL) { + if (!complain) + return -1; + ERROR("Couldn't open %s for reading: %s", + file, fr_syserror(errno)); + return -1; + } + + /* + * Read the entire file into memory for speed. + */ + while (fgets(buffer, sizeof(buffer), fp) != NULL) { + lineno++; + + if (!feof(fp) && (strchr(buffer, '\n') == NULL)) { + fclose(fp); + ERROR("%s[%d]: line too long", file, lineno); + pairlist_free(&pl); + return -1; + } + + /* + * If the line contains nothing but whitespace, + * ignore it. + */ + ptr = buffer; + while (isspace((uint8_t) *ptr)) ptr++; + + if (*ptr == '#' || *ptr == '\n' || !*ptr) continue; + +parse_again: + if (mode == FIND_MODE_NAME) { + /* + * The user's name MUST be the first text on the line. + */ + if (isspace((uint8_t) buffer[0])) { + ERROR("%s[%d]: Entry does not begin with a user name", + file, lineno); + fclose(fp); + return -1; + } + + /* + * Get the name. + */ + ptr = buffer; + getword(&ptr, entry, sizeof(entry), false); + entry_lineno = lineno; + + /* + * Include another file if we see + * $INCLUDE filename + */ + if (strcasecmp(entry, "$INCLUDE") == 0) { + while (isspace((uint8_t) *ptr)) ptr++; + + /* + * If it's an absolute pathname, + * then use it verbatim. + * + * If not, then make the $include + * files *relative* to the current + * file. + */ + if (FR_DIR_IS_RELATIVE(ptr)) { + char *p; + + strlcpy(newfile, file, + sizeof(newfile)); + p = strrchr(newfile, FR_DIR_SEP); + if (!p) { + p = newfile + strlen(newfile); + *p = FR_DIR_SEP; + } + getword(&ptr, p + 1, sizeof(newfile) - 1 - (p - newfile), false); + } else { + getword(&ptr, newfile, sizeof(newfile), false); + } + + t = NULL; + + if (pairlist_read(ctx, newfile, &t, 0) != 0) { + pairlist_free(&pl); + ERROR("%s[%d]: Could not open included file %s: %s", + file, lineno, newfile, fr_syserror(errno)); + fclose(fp); + return -1; + } + *last = t; + + /* + * t may be NULL, it may have one + * entry, or it may be a linked list + * of entries. Go to the end of the + * list. + */ + while (*last) { + (*last)->order = order++; + last = &((*last)->next); + } + continue; + } /* $INCLUDE ... */ + + /* + * Parse the check values + */ + rad_assert(check_tmp == NULL); + rad_assert(reply_tmp == NULL); + parsecode = fr_pair_list_afrom_str(ctx, ptr, &check_tmp); + if (parsecode == T_INVALID) { + pairlist_free(&pl); + ERROR("%s[%d]: Parse error (check) for entry %s: %s", + file, lineno, entry, fr_strerror()); + fclose(fp); + return -1; + } + + if (parsecode != T_EOL) { + pairlist_free(&pl); + talloc_free(check_tmp); + ERROR("%s[%d]: Invalid text after check attributes for entry %s", + file, lineno, entry); + fclose(fp); + return -1; + } + +#ifdef HAVE_REGEX_H + /* + * Do some more sanity checks. + */ + for (vp = fr_cursor_init(&cursor, &check_tmp); + vp; + vp = fr_cursor_next(&cursor)) { + if (((vp->op == T_OP_REG_EQ) || + (vp->op == T_OP_REG_NE)) && + (vp->da->type != PW_TYPE_STRING)) { + pairlist_free(&pl); + talloc_free(check_tmp); + ERROR("%s[%d]: Cannot use regular expressions for non-string attributes in entry %s", + file, lineno, entry); + fclose(fp); + return -1; + } + } +#endif + + /* + * The reply MUST be on a new line. + */ + mode = FIND_MODE_WANT_REPLY; + continue; + } + + /* + * We COULD have a reply, OR we could have a new entry. + */ + if (mode == FIND_MODE_WANT_REPLY) { + if (!isspace((uint8_t) buffer[0])) goto create_entry; + + mode = FIND_MODE_HAVE_REPLY; + } + + /* + * mode == FIND_MODE_HAVE_REPLY + */ + + /* + * The previous line ended with a comma, and then + * we have the start of a new entry! + */ + if (!isspace((uint8_t) buffer[0])) { + trailing_comma: + pairlist_free(&pl); + talloc_free(check_tmp); + talloc_free(reply_tmp); + ERROR("%s[%d]: Invalid comma after the reply attributes. Please delete it.", + file, lineno); + fclose(fp); + return -1; + } + + /* + * Parse the reply values. If there's a trailing + * comma, keep parsing the reply values. + */ + parsecode = fr_pair_list_afrom_str(ctx, buffer, &reply_tmp); + if (parsecode == T_COMMA) { + continue; + } + + /* + * We expect an EOL. Anything else is an error. + */ + if (parsecode != T_EOL) { + pairlist_free(&pl); + talloc_free(check_tmp); + talloc_free(reply_tmp); + ERROR("%s[%d]: Parse error (reply) for entry %s: %s", + file, lineno, entry, fr_strerror()); + fclose(fp); + return -1; + } + + create_entry: + /* + * Done with this entry... + */ + MEM(t = talloc_zero(ctx, PAIR_LIST)); + + if (check_tmp) fr_pair_steal(t, check_tmp); + if (reply_tmp) fr_pair_steal(t, reply_tmp); + + t->check = check_tmp; + t->reply = reply_tmp; + t->lineno = entry_lineno; + t->order = order++; + check_tmp = NULL; + reply_tmp = NULL; + + t->name = talloc_typed_strdup(t, entry); + + *last = t; + last = &(t->next); + + /* + * Look for a name. If we came here because + * there were no reply attributes, then re-parse + * the current line, instead of reading another one. + */ + mode = FIND_MODE_NAME; + if (feof(fp)) break; + if (!isspace((uint8_t) buffer[0])) goto parse_again; + } + + /* + * We're at EOF. If we're supposed to read more, that's + * an error. + */ + if (mode == FIND_MODE_HAVE_REPLY) goto trailing_comma; + + /* + * We had an entry, but no reply attributes. That's OK. + */ + if (mode == FIND_MODE_WANT_REPLY) goto create_entry; + + /* + * Else we were looking for an entry. We didn't get one + * because we were at EOF, so that's OK. + */ + + fclose(fp); + + *list = pl; + return 0; +} diff --git a/src/main/libfreeradius-server.mk b/src/main/libfreeradius-server.mk new file mode 100644 index 0000000..4495f72 --- /dev/null +++ b/src/main/libfreeradius-server.mk @@ -0,0 +1,22 @@ +TARGET := libfreeradius-server.a + +SOURCES := conffile.c \ + evaluate.c \ + exec.c \ + exfile.c \ + log.c \ + parser.c \ + map.c \ + regex.c \ + tmpl.c \ + util.c \ + version.c \ + pair.c \ + xlat.c + +# This lets the linker determine which version of the SSLeay functions to use. +TGT_LDLIBS := $(OPENSSL_LIBS) + +ifneq ($(MAKECMDGOALS),scan) +SRC_CFLAGS += -DBUILT_WITH_CPPFLAGS=\"$(CPPFLAGS)\" -DBUILT_WITH_CFLAGS=\"$(CFLAGS)\" -DBUILT_WITH_LDFLAGS=\"$(LDFLAGS)\" -DBUILT_WITH_LIBS=\"$(LIBS)\" +endif diff --git a/src/main/listen.c b/src/main/listen.c new file mode 100644 index 0000000..ee73a57 --- /dev/null +++ b/src/main/listen.c @@ -0,0 +1,4486 @@ +/* + * listen.c Handle socket stuff + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2005,2006 The FreeRADIUS server project + * Copyright 2005 Alan DeKok <aland@ox.org> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/modules.h> +#include <freeradius-devel/rad_assert.h> +#include <freeradius-devel/process.h> +#include <freeradius-devel/protocol.h> +#include <freeradius-devel/modpriv.h> + +#include <freeradius-devel/detail.h> + +#ifdef WITH_UDPFROMTO +#include <freeradius-devel/udpfromto.h> +#endif + +#ifdef HAVE_SYS_RESOURCE_H +#include <sys/resource.h> +#endif + +#ifdef HAVE_NET_IF_H +#include <net/if.h> +#endif + +#ifdef HAVE_FCNTL_H +#include <fcntl.h> +#endif + +#ifdef HAVE_SYS_STAT_H +#include <sys/stat.h> +#endif + +#ifdef WITH_TLS +#include <netinet/tcp.h> + +# ifdef __APPLE__ +# if !defined(SOL_TCP) && defined(IPPROTO_TCP) +# define SOL_TCP IPPROTO_TCP +# endif +# endif + +#endif + +#ifdef DEBUG_PRINT_PACKET +static void print_packet(RADIUS_PACKET *packet) +{ + char src[256], dst[256]; + + ip_ntoh(&packet->src_ipaddr, src, sizeof(src)); + ip_ntoh(&packet->dst_ipaddr, dst, sizeof(dst)); + + fprintf(stderr, "ID %d: %s %d -> %s %d\n", packet->id, + src, packet->src_port, dst, packet->dst_port); + +} +#endif + + +static rad_listen_t *listen_alloc(TALLOC_CTX *ctx, RAD_LISTEN_TYPE type); + +#ifdef WITH_COMMAND_SOCKET +#ifdef WITH_TCP +static int command_tcp_recv(rad_listen_t *listener); +static int command_tcp_send(rad_listen_t *listener, REQUEST *request); +static int command_write_magic(int newfd, listen_socket_t *sock); +#endif +#endif + +#ifdef WITH_COA_TUNNEL +static int listen_coa_init(void); +#endif + +static fr_protocol_t master_listen[]; + +#ifdef WITH_DYNAMIC_CLIENTS +static void client_timer_free(void *ctx) +{ + RADCLIENT *client = ctx; + + client_free(client); +} +#endif + +/* + * Find a per-socket client. + */ +RADCLIENT *client_listener_find(rad_listen_t *listener, + fr_ipaddr_t const *ipaddr, uint16_t src_port) +{ +#ifdef WITH_DYNAMIC_CLIENTS + int rcode; + REQUEST *request; + RADCLIENT *created; +#endif + time_t now; + RADCLIENT *client; + RADCLIENT_LIST *clients; + listen_socket_t *sock; + + rad_assert(listener != NULL); + rad_assert(ipaddr != NULL); + + sock = listener->data; + clients = sock->clients; + + /* + * This HAS to have been initialized previously. + */ + rad_assert(clients != NULL); + + client = client_find(clients, ipaddr, sock->proto); + if (!client) { + char name[256], buffer[128]; + +#ifdef WITH_DYNAMIC_CLIENTS + unknown: /* used only for dynamic clients */ +#endif + + /* + * DoS attack quenching, but only in daemon mode. + * If they're running in debug mode, show them + * every packet. + */ + if (rad_debug_lvl == 0) { + static time_t last_printed = 0; + + now = time(NULL); + if (last_printed == now) return NULL; + + last_printed = now; + } + + listener->print(listener, name, sizeof(name)); + + radlog(L_ERR, "Ignoring request to %s from unknown client %s port %d" +#ifdef WITH_TCP + " proto %s" +#endif + , name, inet_ntop(ipaddr->af, &ipaddr->ipaddr, + buffer, sizeof(buffer)), src_port +#ifdef WITH_TCP + , (sock->proto == IPPROTO_UDP) ? "udp" : "tcp" +#endif + ); + return NULL; + } + +#ifndef WITH_DYNAMIC_CLIENTS + return client; /* return the found client. */ +#else + + /* + * No server defined, and it's not dynamic. Return it. + */ + if (!client->client_server && !client->dynamic) return client; + + now = time(NULL); + + /* + * It's a dynamically generated client, check it. + */ + if (client->dynamic && (src_port != 0)) { +#ifdef HAVE_SYS_STAT_H + char const *filename; +#endif + fr_event_list_t *el; + struct timeval when; + + /* + * Lives forever. Return it. + */ + if (client->lifetime == 0) return client; + + /* + * Rate-limit the deletion of known clients. + * This makes them last a little longer, but + * prevents the server from melting down if (say) + * 10k clients all expire at once. + */ + if (now == client->last_new_client) return client; + + /* + * It's not dead yet. Return it. + */ + if ((client->created + client->lifetime) > now) return client; + +#ifdef HAVE_SYS_STAT_H + /* + * The client was read from a file, and the file + * hasn't changed since the client was created. + * Just renew the creation time, and continue. + * We don't need to re-load the same information. + */ + if (client->cs && + (filename = cf_section_filename(client->cs)) != NULL) { + struct stat buf; + + if ((stat(filename, &buf) >= 0) && + (buf.st_mtime < client->created)) { + client->created = now; + return client; + } + } +#endif + + + /* + * Delete the client from the known list. + */ + client_delete(clients, client); + + /* + * Add a timer to free the client 20s after it's already timed out. + */ + el = radius_event_list_corral(EVENT_CORRAL_MAIN); + + gettimeofday(&when, NULL); + when.tv_sec += main_config.max_request_time + 20; + + /* + * If this fails, we leak memory. That's better than crashing... + */ + (void) fr_event_insert(el, client_timer_free, client, &when, &client->ev); + + /* + * Go find the enclosing network again. + */ + client = client_find(clients, ipaddr, sock->proto); + + /* + * WTF? + */ + if (!client) goto unknown; + if (!client->client_server) goto unknown; + + /* + * At this point, 'client' is the enclosing + * network that configures where dynamic clients + * can be defined. + */ + rad_assert(client->dynamic == 0); + + } else if (!client->dynamic && client->rate_limit) { + /* + * The IP is unknown, so we've found an enclosing + * network. Enable DoS protection. We only + * allow one new client per second. Known + * clients aren't subject to this restriction. + */ + if (now == client->last_new_client) goto unknown; + } + + client->last_new_client = now; + + request = request_alloc(NULL); + if (!request) goto unknown; + + request->listener = listener; + request->client = client; + request->packet = rad_recv(NULL, listener->fd, 0x02); /* MSG_PEEK */ + if (!request->packet) { /* badly formed, etc */ + talloc_free(request); + if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror()); + goto unknown; + } + (void) talloc_steal(request, request->packet); + request->reply = rad_alloc_reply(request, request->packet); + if (!request->reply) { + talloc_free(request); + goto unknown; + } + gettimeofday(&request->packet->timestamp, NULL); + request->number = 0; + request->priority = listener->type; + request->server = client->client_server; + request->root = &main_config; + + /* + * Run a fake request through the given virtual server. + * Look for FreeRADIUS-Client-IP-Address + * FreeRADIUS-Client-Secret + * ... + * + * and create the RADCLIENT structure from that. + */ + RDEBUG("server %s {", request->server); + + rcode = process_authorize(0, request); + + RDEBUG("} # server %s", request->server); + + switch (rcode) { + case RLM_MODULE_OK: + case RLM_MODULE_UPDATED: + break; + + /* + * Likely a fatal error we want to warn the user about + */ + case RLM_MODULE_INVALID: + case RLM_MODULE_FAIL: + ERROR("Virtual-Server %s returned %s, creating dynamic client failed", request->server, + fr_int2str(mod_rcode_table, rcode, "<INVALID>")); + talloc_free(request); + goto unknown; + + /* + * Probably the result of policy, or the client not existing. + */ + default: + DEBUG("Virtual-Server %s returned %s, ignoring client", request->server, + fr_int2str(mod_rcode_table, rcode, "<INVALID>")); + talloc_free(request); + goto unknown; + } + + /* + * If the client was updated by rlm_dynamic_clients, + * don't create the client from attribute-value pairs. + */ + if (request->client == client) { + created = client_afrom_request(clients, request); + } else { + created = request->client; + + /* + * This frees the client if it isn't valid. + */ + if (!client_add_dynamic(clients, client, created)) goto unknown; + } + + request->server = client->server; + exec_trigger(request, NULL, "server.client.add", false); + + talloc_free(request); + + if (!created) goto unknown; + + return created; +#endif +} + +static int listen_bind(rad_listen_t *this); + +#ifdef WITH_COA_TUNNEL +static void listener_coa_update(rad_listen_t *this, VALUE_PAIR *vps); +#endif + +/* + * Process and reply to a server-status request. + * Like rad_authenticate and rad_accounting this should + * live in it's own file but it's so small we don't bother. + */ +int rad_status_server(REQUEST *request) +{ + int rcode = RLM_MODULE_OK; + DICT_VALUE *dval; + +#ifdef WITH_TLS + if (request->listener->tls) { + listen_socket_t *sock = request->listener->data; + + if (sock->state == LISTEN_TLS_CHECKING) { + int autz_type = PW_AUTZ_TYPE; + char const *name = "Autz-Type"; + + if (request->listener->type == RAD_LISTEN_ACCT) { + autz_type = PW_ACCT_TYPE; + name = "Acct-Type"; + } + + RDEBUG("(TLS) Checking connection to see if it is authorized."); + + dval = dict_valbyname(autz_type, 0, "New-TLS-Connection"); + if (dval) { + rcode = process_authorize(dval->value, request); + } else { + rcode = RLM_MODULE_OK; + RWDEBUG("(TLS) Did not find '%s New-TLS-Connection' - defaulting to accept", name); + } + + if ((rcode == RLM_MODULE_OK) || (rcode == RLM_MODULE_UPDATED)) { + RDEBUG("(TLS) Connection is authorized"); + request->reply->code = PW_CODE_ACCESS_ACCEPT; + } else { + RWDEBUG("(TLS) Connection is not authorized - closing TCP socket."); + request->reply->code = PW_CODE_ACCESS_REJECT; + } + + return 0; + } + } +#endif + +#ifdef WITH_STATS + /* + * Full statistics are available only on a statistics + * socket. + */ + if (request->listener->type == RAD_LISTEN_NONE) { + request_stats_reply(request); + } +#endif + + switch (request->listener->type) { +#ifdef WITH_STATS + case RAD_LISTEN_NONE: +#endif + case RAD_LISTEN_AUTH: + dval = dict_valbyname(PW_AUTZ_TYPE, 0, "Status-Server"); + if (dval) { + rcode = process_authorize(dval->value, request); + } else { + rcode = RLM_MODULE_OK; + } + + switch (rcode) { + case RLM_MODULE_OK: + case RLM_MODULE_UPDATED: + request->reply->code = PW_CODE_ACCESS_ACCEPT; + +#ifdef WITH_COA_TUNNEL + if (request->listener->send_coa) listener_coa_update(request->listener, request->packet->vps); +#endif + break; + + case RLM_MODULE_FAIL: + case RLM_MODULE_HANDLED: + request->reply->code = 0; /* don't reply */ + break; + + default: + case RLM_MODULE_REJECT: + request->reply->code = PW_CODE_ACCESS_REJECT; + break; + } + break; + +#ifdef WITH_ACCOUNTING + case RAD_LISTEN_ACCT: + dval = dict_valbyname(PW_ACCT_TYPE, 0, "Status-Server"); + if (dval) { + rcode = process_accounting(dval->value, request); + } else { + rcode = RLM_MODULE_OK; + } + + switch (rcode) { + case RLM_MODULE_OK: + case RLM_MODULE_UPDATED: + request->reply->code = PW_CODE_ACCOUNTING_RESPONSE; + +#ifdef WITH_COA_TUNNEL + if (request->listener->send_coa) listener_coa_update(request->listener, request->packet->vps); +#endif + break; + + default: + request->reply->code = 0; /* don't reply */ + break; + } + break; +#endif + +#ifdef WITH_COA + /* + * This is a vendor extension. Suggested by Glen + * Zorn in IETF 72, and rejected by the rest of + * the WG. We like it, so it goes in here. + */ + case RAD_LISTEN_COA: + dval = dict_valbyname(PW_RECV_COA_TYPE, 0, "Status-Server"); + if (dval) { + rcode = process_recv_coa(dval->value, request); + } else { + rcode = RLM_MODULE_OK; + } + + switch (rcode) { + case RLM_MODULE_OK: + case RLM_MODULE_UPDATED: + request->reply->code = PW_CODE_COA_ACK; + break; + + default: + request->reply->code = 0; /* don't reply */ + break; + } + break; +#endif + + default: + return 0; + } + + return 0; +} + +#ifdef WITH_TCP +static int dual_tcp_recv(rad_listen_t *listener) +{ + int rcode; + RADIUS_PACKET *packet; + RAD_REQUEST_FUNP fun = NULL; + listen_socket_t *sock = listener->data; + RADCLIENT *client = sock->client; + + rad_assert(client != NULL); + + if (listener->status != RAD_LISTEN_STATUS_KNOWN) return 0; + + /* + * Allocate a packet for partial reads. + */ + if (!sock->packet) { + sock->packet = rad_alloc(sock, false); + if (!sock->packet) return 0; + + sock->packet->sockfd = listener->fd; + sock->packet->src_ipaddr = sock->other_ipaddr; + sock->packet->src_port = sock->other_port; + sock->packet->dst_ipaddr = sock->my_ipaddr; + sock->packet->dst_port = sock->my_port; + sock->packet->proto = sock->proto; + } + + /* + * Grab the packet currently being processed. + */ + packet = sock->packet; + + rcode = fr_tcp_read_packet(packet, 0); + + /* + * Still only a partial packet. Put it back, and return, + * so that we'll read more data when it's ready. + */ + if (rcode == 0) { + return 0; + } + + if (rcode == -1) { /* error reading packet */ + char buffer[256]; + + ERROR("Invalid packet from %s port %d, closing socket: %s", + ip_ntoh(&packet->src_ipaddr, buffer, sizeof(buffer)), + packet->src_port, fr_strerror()); + } + + if (rcode < 0) { /* error or connection reset */ + listener->status = RAD_LISTEN_STATUS_EOL; + + /* + * Tell the event handler that an FD has disappeared. + */ + DEBUG("Client has closed connection"); + radius_update_listener(listener); + + /* + * Do NOT free the listener here. It's in use by + * a request, and will need to hang around until + * all of the requests are done. + * + * It is instead free'd in remove_from_request_hash() + */ + return 0; + } + + /* + * Some sanity checks, based on the packet code. + */ + switch (packet->code) { + case PW_CODE_ACCESS_REQUEST: + if (listener->type != RAD_LISTEN_AUTH) goto bad_packet; + FR_STATS_INC(auth, total_requests); + fun = rad_authenticate; + break; + +#ifdef WITH_ACCOUNTING + case PW_CODE_ACCOUNTING_REQUEST: + if (listener->type != RAD_LISTEN_ACCT) { + /* + * Allow auth + dual. Disallow + * everything else. + */ + if (!((listener->type == RAD_LISTEN_AUTH) && + (listener->dual))) { + goto bad_packet; + } + } + FR_STATS_INC(acct, total_requests); + fun = rad_accounting; + break; +#endif + + case PW_CODE_STATUS_SERVER: + if (!main_config.status_server) { + FR_STATS_INC(auth, total_unknown_types); + WARN("Ignoring Status-Server request due to security configuration"); + rad_free(&sock->packet); + return 0; + } + fun = rad_status_server; + break; + + default: + bad_packet: + FR_STATS_INC(auth, total_unknown_types); + + DEBUG("Invalid packet code %d sent from client %s port %d : IGNORED", + packet->code, client->shortname, packet->src_port); + rad_free(&sock->packet); + return 0; + } /* switch over packet types */ + + if (!request_receive(NULL, listener, packet, client, fun)) { + FR_STATS_INC(auth, total_packets_dropped); + rad_free(&sock->packet); + return 0; + } + + sock->packet = NULL; /* we have no need for more partial reads */ + return 1; +} + +#ifdef WITH_TLS +typedef struct { + char const *name; + SSL_CTX *ctx; +} fr_realm_ctx_t; /* hack from tls. */ + +static int tls_sni_callback(SSL *ssl, UNUSED int *al, void *arg) +{ + fr_tls_server_conf_t *conf = arg; + char const *name, *p; + int type; + fr_realm_ctx_t my_r, *r; + REQUEST *request; + char buffer[PATH_MAX]; + + /* + * No SNI, that's fine. + */ + type = SSL_get_servername_type(ssl); + if (type < 0) return SSL_TLSEXT_ERR_OK; + + /* + * No realms configured, just use the default context. + */ + if (!conf->realms) return SSL_TLSEXT_ERR_OK; + + name = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); + if (!name) return SSL_TLSEXT_ERR_OK; + + /* + * RFC Section 6066 Section 3 says that the names are + * ASCII, without a trailing dot. i.e. punycode. + */ + for (p = name; *p != '\0'; p++) { + if (*p == '-') continue; + if (*p == '.') continue; + if ((*p >= 'A') && (*p <= 'Z')) continue; + if ((*p >= 'a') && (*p <= 'z')) continue; + if ((*p >= '0') && (*p <= '9')) continue; + + /* + * Anything else, fail. + */ + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + + /* + * Too long, fail. + */ + if ((p - name) > 255) return SSL_TLSEXT_ERR_ALERT_FATAL; + + snprintf(buffer, sizeof(buffer), "%s/%s.pem", conf->realm_dir, name); + + my_r.name = buffer; + r = fr_hash_table_finddata(conf->realms, &my_r); + + /* + * If found, switch certs. Otherwise use the default + * one. + */ + if (r) (void) SSL_set_SSL_CTX(ssl, r->ctx); + + /* + * Set an attribute saying which server has been selected. + */ + request = (REQUEST *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_REQUEST); + if (request) { + (void) pair_make_config("TLS-Server-Name-Indication", name, T_OP_SET); + } + + return SSL_TLSEXT_ERR_OK; +} +#endif + +#ifdef WITH_RADIUSV11 +static const unsigned char radiusv11_alpn_protos[] = { + 10, 'r', 'a', 'd', 'i', 'u', 's', '/', '1', '.', '1', +}; + +/* + * On the server, get the ALPN list requested by the client. + */ +static int radiusv11_server_alpn_cb(SSL *ssl, + const unsigned char **out, + unsigned char *outlen, + const unsigned char *in, + unsigned int inlen, + void *arg) +{ + rad_listen_t *this = arg; + listen_socket_t *sock = this->data; + unsigned char **hack; + const unsigned char *server; + unsigned int server_len, i; + int rcode; + REQUEST *request; + + request = (REQUEST *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_REQUEST); + fr_assert(request != NULL); + + fr_assert(inlen > 0); + + memcpy(&hack, &out, sizeof(out)); /* const issues */ + + /* + * The RADIUSv11 configuration for this socket is a combination of what we require, and what we + * require of the client. + */ + switch (this->radiusv11) { + /* + * If we forbid RADIUSv11, then we never advertised it via ALPN, and this callback should + * never have been registered. + */ + case FR_RADIUSV11_FORBID: + *out = NULL; + *outlen = 0; + return SSL_TLSEXT_ERR_OK; + + case FR_RADIUSV11_ALLOW: + case FR_RADIUSV11_REQUIRE: + server = radiusv11_alpn_protos; + server_len = sizeof(radiusv11_alpn_protos); + break; + } + + for (i = 0; i < inlen; i += in[0] + 1) { + RDEBUG("(TLS) ALPN sent by client is \"%.*s\"", in[i], &in[i + 1]); + } + + /* + * Select the next protocol. + */ + rcode = SSL_select_next_proto(hack, outlen, server, server_len, in, inlen); + if (rcode == OPENSSL_NPN_NEGOTIATED) { + server = *out; + + /* + * Tell our socket which protocol we negotiated. + */ + fr_assert(*outlen == 10); + sock->radiusv11 = (server[9] == '1'); + + RDEBUG("(TLS) ALPN server negotiated application protocol \"%.*s\"", (int) *outlen, server); + return SSL_TLSEXT_ERR_OK; + } + + /* + * No common ALPN. + */ + RDEBUG("(TLS) ALPN failure - no protocols in common"); + return SSL_TLSEXT_ERR_ALERT_FATAL; +} + +int fr_radiusv11_client_init(fr_tls_server_conf_t *tls); +int fr_radiusv11_client_get_alpn(rad_listen_t *listener); + +int fr_radiusv11_client_init(fr_tls_server_conf_t *tls) +{ + switch (tls->radiusv11) { + case FR_RADIUSV11_ALLOW: + case FR_RADIUSV11_REQUIRE: + if (SSL_CTX_set_alpn_protos(tls->ctx, radiusv11_alpn_protos, sizeof(radiusv11_alpn_protos)) != 0) { + ERROR("Failed setting RADIUSv11 negotiation flags"); + return -1; + } + break; + + default: + break; + } + + return 0; +} + +int fr_radiusv11_client_get_alpn(rad_listen_t *listener) +{ + const unsigned char *data; + unsigned int len; + listen_socket_t *sock = listener->data; + + SSL_get0_alpn_selected(sock->ssn->ssl, &data, &len); + if (!data) { + DEBUG("(TLS) ALPN home server did not send any application protocol"); + if (listener->radiusv11 == FR_RADIUSV11_REQUIRE) { + DEBUG("(TLS) We have 'radiusv11 = require', but the home server has not negotiated it - closing socket"); + return -1; + } + + DEBUG("(TLS) ALPN assuming historical RADIUS"); + return 0; + } + + DEBUG("(TLS) ALPN home server sent application protocol \"%.*s\"", (int) len, data); + + if (len != 10) { + radiusv11_unknown: + DEBUG("(TLS) ALPN home server sent unknown application protocol - closing connection"); + return -1; + } + + /* + * Should always be "radius/1.1". The server MUST echo back one of the strings + * we sent. If it doesn't, it's a bad server. + */ + if (memcmp(data, "radius/1.1", 10) != 0) goto radiusv11_unknown; + + /* + * Double-check what the server sent us. It SHOULD be sane, but it never hurts to check. + */ + switch (listener->radiusv11) { + case FR_RADIUSV11_FORBID: + DEBUG("(TLS) ALPN home server sent \"radius/v1.1\" but we forbid it - closing connection to home server"); + return -1; + + case FR_RADIUSV11_ALLOW: + case FR_RADIUSV11_REQUIRE: + DEBUG("(TLS) ALPN using \"radius/1.1\""); + sock->radiusv11 = true; + break; + } + + sock->alpn_checked = true; + return 0; +} +#endif + + +static int dual_tcp_accept(rad_listen_t *listener) +{ + int newfd; + uint16_t src_port; + rad_listen_t *this; + socklen_t salen; + struct sockaddr_storage src; + listen_socket_t *sock; + fr_ipaddr_t src_ipaddr; + RADCLIENT *client = NULL; + + salen = sizeof(src); + + DEBUG2(" ... new connection request on TCP socket"); + + newfd = accept(listener->fd, (struct sockaddr *) &src, &salen); + if (newfd < 0) { + /* + * Non-blocking sockets must handle this. + */ +#ifdef EWOULDBLOCK + if (errno == EWOULDBLOCK) { + return 0; + } +#endif + + DEBUG2(" ... failed to accept connection"); + return -1; + } + + if (!fr_sockaddr2ipaddr(&src, salen, &src_ipaddr, &src_port)) { + close(newfd); + DEBUG2(" ... unknown address family"); + return 0; + } + + /* + * Enforce client IP address checks on accept, not on + * every packet. + */ + if ((client = client_listener_find(listener, + &src_ipaddr, src_port)) == NULL) { + close(newfd); + FR_STATS_INC(auth, total_invalid_requests); + return 0; + } + +#ifdef WITH_TLS + /* + * Enforce security restrictions. + * + * This shouldn't be necessary in practice. However, it + * serves as a double-check on configurations. Marking a + * client as "tls required" means that any accidental + * exposure of the client to non-TLS traffic is + * prevented. + */ + if (client->tls_required && !listener->tls) { + INFO("Ignoring connection to TLS socket from non-TLS client"); + close(newfd); + return 0; + } + +#ifdef WITH_RADIUSV11 + if (listener->tls) { + switch (listener->tls->radiusv11) { + case FR_RADIUSV11_FORBID: + if (client->radiusv11 == FR_RADIUSV11_REQUIRE) { + INFO("Ignoring new connection as client is marked as 'radiusv11 = require', and this socket has 'radiusv11 = forbid'"); + close(newfd); + return 0; + } + break; + + case FR_RADIUSV11_ALLOW: + /* + * We negotiate it as per the client recommendations (forbid, allow, require) + */ + break; + + case FR_RADIUSV11_REQUIRE: + if (client->radiusv11 == FR_RADIUSV11_FORBID) { + INFO("Ignoring new connection as client is marked as 'radiusv11 = forbid', and this socket has 'radiusv11 = require'"); + close(newfd); + return 0; + } + break; + } + } +#endif + +#endif + + /* + * Enforce max_connections on client && listen section. + */ + if ((client->limit.max_connections != 0) && + (client->limit.max_connections == client->limit.num_connections)) { + /* + * FIXME: Print client IP/port, and server IP/port. + */ + INFO("Ignoring new connection due to client max_connections (%d)", client->limit.max_connections); + close(newfd); + return 0; + } + + sock = listener->data; + if ((sock->limit.max_connections != 0) && + (sock->limit.max_connections == sock->limit.num_connections)) { + /* + * FIXME: Print client IP/port, and server IP/port. + */ + INFO("Ignoring new connection due to socket max_connections"); + close(newfd); + return 0; + } + client->limit.num_connections++; + sock->limit.num_connections++; + + /* + * Add the new listener. We require a new context here, + * because the allocations for the packet, etc. in the + * child listener will be done in a child thread. + */ + this = listen_alloc(NULL, listener->type); + if (!this) return -1; + + /* + * Copy everything, including the pointer to the socket + * information. + */ + sock = this->data; + memcpy(this->data, listener->data, sizeof(*sock)); + memcpy(this, listener, sizeof(*this)); + this->next = NULL; + this->data = sock; /* fix it back */ + + sock->parent = listener->data; + sock->other_ipaddr = src_ipaddr; + sock->other_port = src_port; + sock->client = client; + sock->opened = sock->last_packet = time(NULL); + + /* + * Set the limits. The defaults are the parent limits. + * Client limits on max_connections are enforced dynamically. + * Set the MINIMUM of client/socket idle timeout or lifetime. + */ + memcpy(&sock->limit, &sock->parent->limit, sizeof(sock->limit)); + + if (client->limit.idle_timeout && + ((sock->limit.idle_timeout == 0) || + (client->limit.idle_timeout < sock->limit.idle_timeout))) { + sock->limit.idle_timeout = client->limit.idle_timeout; + } + + if (client->limit.lifetime && + ((sock->limit.lifetime == 0) || + (client->limit.lifetime < sock->limit.lifetime))) { + sock->limit.lifetime = client->limit.lifetime; + } + + this->fd = newfd; + this->status = RAD_LISTEN_STATUS_INIT; + + this->parent = listener; + if (!rbtree_insert(listener->children, this)) { + ERROR("Failed inserting TCP socket into parent list."); + } + +#ifdef WITH_COMMAND_SOCKET + if (this->type == RAD_LISTEN_COMMAND) { + this->recv = command_tcp_recv; + this->send = command_tcp_send; + command_write_magic(this->fd, sock); + } else +#endif + { + + this->recv = dual_tcp_recv; + +#ifdef WITH_TLS + if (this->tls) { + this->recv = dual_tls_recv; + this->send = dual_tls_send; + + /* + * Set up SNI callback. We don't do it + * in the main TLS code, because EAP + * doesn't need or use SNI. + */ + SSL_CTX_set_tlsext_servername_callback(this->tls->ctx, tls_sni_callback); + SSL_CTX_set_tlsext_servername_arg(this->tls->ctx, this->tls); +#ifdef WITH_RADIUSV11 + /* + * Default is "forbid" (0). In which case we don't set any ALPN callbacks, and + * the ServerHello does not contain an ALPN section. + */ + if (client->radiusv11 != FR_RADIUSV11_FORBID) { + SSL_CTX_set_alpn_select_cb(this->tls->ctx, radiusv11_server_alpn_cb, this); + DEBUG("(TLS) ALPN radiusv11 = allow / require"); + } else { + DEBUG("(TLS) ALPN radiusv11 = forbid"); + } +#endif + } +#endif + } + +#ifdef WITH_COA_TUNNEL + /* + * Originate CoA requests to a NAS. + */ + if (this->send_coa) { + home_server_t *home; + + rad_assert(this->type != RAD_LISTEN_PROXY); + + this->proxy_send = dual_tls_send_coa_request; + this->proxy_encode = master_listen[RAD_LISTEN_PROXY].encode; + this->proxy_decode = master_listen[RAD_LISTEN_PROXY].decode; + + /* + * Automatically create a home server for this + * client. There MAY be one already one for that + * IP in the configuration files, but it will not + * have this particular port. + */ + sock->home = home = talloc_zero(this, home_server_t); + home->ipaddr = sock->other_ipaddr; + home->port = sock->other_port; + home->proto = sock->proto; + home->secret = sock->client->secret; + + home->coa_irt = this->coa_irt; + home->coa_mrt = this->coa_mrt; + home->coa_mrc = this->coa_mrc; + home->coa_mrd = this->coa_mrd; + home->recv_coa_server = this->server; + } +#endif + + /* + * FIXME: set O_NONBLOCK on the accept'd fd. + * See djb's portability rants for details. + */ + + /* + * Tell the event loop that we have a new FD. + * This can be called from a child thread... + */ + radius_update_listener(this); + + return 0; +} +#endif + +/* + * Ensure that we always keep the correct counters. + */ +#ifdef WITH_TCP +static void common_socket_free(rad_listen_t *this) +{ + listen_socket_t *sock = this->data; + + if (sock->proto != IPPROTO_TCP) return; + + /* + * Decrement the number of connections. + */ + if (sock->parent && (sock->parent->limit.num_connections > 0)) { + sock->parent->limit.num_connections--; + } + if (sock->client && sock->client->limit.num_connections > 0) { + sock->client->limit.num_connections--; + } + if (sock->home && sock->home->limit.num_connections > 0) { + sock->home->limit.num_connections--; + } +} +#else +#define common_socket_free NULL +#endif + +/* + * This function is stupid and complicated. + */ +int common_socket_print(rad_listen_t const *this, char *buffer, size_t bufsize) +{ + size_t len; + listen_socket_t *sock = this->data; + char const *name = master_listen[this->type].name; + +#define FORWARD len = strlen(buffer); if (len >= (bufsize + 1)) return 0;buffer += len;bufsize -= len +#define ADDSTRING(_x) strlcpy(buffer, _x, bufsize);FORWARD + + ADDSTRING(name); + +#ifdef WITH_TCP + if (this->dual) { + ADDSTRING("+acct"); + } +#endif + +#ifdef WITH_COA_TUNNEL + if (this->send_coa) { + ADDSTRING("+coa"); + } +#endif + + if (sock->interface) { + ADDSTRING(" interface "); + ADDSTRING(sock->interface); + } + +#ifdef WITH_TCP + if (this->recv == dual_tcp_accept) { + ADDSTRING(" proto tcp"); + } +#endif + +#ifdef WITH_TCP + /* + * TCP sockets get printed a little differently, to make + * it clear what's going on. + */ + if (sock->client) { + ADDSTRING(" from client ("); + ip_ntoh(&sock->other_ipaddr, buffer, bufsize); + FORWARD; + + ADDSTRING(", "); + snprintf(buffer, bufsize, "%d", sock->other_port); + FORWARD; + ADDSTRING(") -> ("); + + if ((sock->my_ipaddr.af == AF_INET) && + (sock->my_ipaddr.ipaddr.ip4addr.s_addr == htonl(INADDR_ANY))) { + strlcpy(buffer, "*", bufsize); + } else { + ip_ntoh(&sock->my_ipaddr, buffer, bufsize); + } + FORWARD; + + ADDSTRING(", "); + snprintf(buffer, bufsize, "%d", sock->my_port); + FORWARD; + + if (this->server) { + ADDSTRING(", virtual-server="); + ADDSTRING(this->server); + } + + ADDSTRING(")"); + + return 1; + } + +#ifdef WITH_PROXY + /* + * Maybe it's a socket that we opened to a home server. + */ + if ((sock->proto == IPPROTO_TCP) && + (this->type == RAD_LISTEN_PROXY)) { + ADDSTRING(" ("); + ip_ntoh(&sock->my_ipaddr, buffer, bufsize); + FORWARD; + + ADDSTRING(", "); + snprintf(buffer, bufsize, "%d", sock->my_port); + FORWARD; + ADDSTRING(") -> home_server ("); + + if ((sock->other_ipaddr.af == AF_INET) && + (sock->other_ipaddr.ipaddr.ip4addr.s_addr == htonl(INADDR_ANY))) { + strlcpy(buffer, "*", bufsize); + } else { + ip_ntoh(&sock->other_ipaddr, buffer, bufsize); + } + FORWARD; + + ADDSTRING(", "); + snprintf(buffer, bufsize, "%d", sock->other_port); + FORWARD; + + ADDSTRING(")"); + + return 1; + } +#endif /* WITH_PROXY */ +#endif /* WITH_TCP */ + + ADDSTRING(" address "); + + if ((sock->my_ipaddr.af == AF_INET) && + (sock->my_ipaddr.ipaddr.ip4addr.s_addr == htonl(INADDR_ANY))) { + strlcpy(buffer, "*", bufsize); + } else { + ip_ntoh(&sock->my_ipaddr, buffer, bufsize); + } + FORWARD; + + ADDSTRING(" port "); + snprintf(buffer, bufsize, "%d", sock->my_port); + FORWARD; + +#ifdef WITH_TLS + if (this->tls) { + ADDSTRING(" (TLS)"); + FORWARD; + } +#endif + + if (this->server) { + ADDSTRING(" bound to server "); + strlcpy(buffer, this->server, bufsize); + } + +#undef ADDSTRING +#undef FORWARD + + return 1; +} + +static CONF_PARSER performance_config[] = { + { "skip_duplicate_checks", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rad_listen_t, nodup), NULL }, + + { "synchronous", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rad_listen_t, synchronous), NULL }, + + { "workers", FR_CONF_OFFSET(PW_TYPE_INTEGER, rad_listen_t, workers), NULL }, + CONF_PARSER_TERMINATOR +}; + + +static CONF_PARSER limit_config[] = { + { "max_pps", FR_CONF_OFFSET(PW_TYPE_INTEGER, listen_socket_t, max_rate), NULL }, + +#ifdef WITH_TCP + { "max_connections", FR_CONF_OFFSET(PW_TYPE_INTEGER, listen_socket_t, limit.max_connections), "16" }, + { "lifetime", FR_CONF_OFFSET(PW_TYPE_INTEGER, listen_socket_t, limit.lifetime), "0" }, + { "idle_timeout", FR_CONF_OFFSET(PW_TYPE_INTEGER, listen_socket_t, limit.idle_timeout), STRINGIFY(30) }, +#endif + CONF_PARSER_TERMINATOR +}; + +#ifdef WITH_COA_TUNNEL +static CONF_PARSER coa_config[] = { + { "irt", FR_CONF_OFFSET(PW_TYPE_INTEGER, rad_listen_t, coa_irt), STRINGIFY(2) }, + { "mrt", FR_CONF_OFFSET(PW_TYPE_INTEGER, rad_listen_t, coa_mrt), STRINGIFY(16) }, + { "mrc", FR_CONF_OFFSET(PW_TYPE_INTEGER, rad_listen_t, coa_mrc), STRINGIFY(5) }, + { "mrd", FR_CONF_OFFSET(PW_TYPE_INTEGER, rad_listen_t, coa_mrd), STRINGIFY(30) }, + CONF_PARSER_TERMINATOR +}; +#endif + +#ifdef WITH_TCP +/* + * TLS requires child threads to handle the listeners. Which + * means that we need a separate talloc context per child thread. + * Which means that we need to manually clean up the child + * listeners. Which means we need to manually track them. + * + * All child thread linking/unlinking is done in the master + * thread. If we care, we can later add a mutex for the parent + * listener. + */ +static int listener_cmp(void const *one, void const *two) +{ + if (one < two) return -1; + if (one > two) return +1; + return 0; +} + +static int listener_unlink(UNUSED void *ctx, UNUSED void *data) +{ + return 2; /* unlink this node from the tree */ +} +#endif + + +/* + * Parse an authentication or accounting socket. + */ +int common_socket_parse(CONF_SECTION *cs, rad_listen_t *this) +{ + int rcode; + uint16_t listen_port; + fr_ipaddr_t ipaddr; + listen_socket_t *sock = this->data; + char const *section_name = NULL; + CONF_SECTION *client_cs, *parentcs; + CONF_SECTION *subcs; + CONF_PAIR *cp; + + this->cs = cs; + + /* + * Try IPv4 first + */ + memset(&ipaddr, 0, sizeof(ipaddr)); + ipaddr.ipaddr.ip4addr.s_addr = htonl(INADDR_NONE); + + rcode = cf_item_parse(cs, "ipaddr", FR_ITEM_POINTER(PW_TYPE_COMBO_IP_ADDR, &ipaddr), NULL); + if (rcode < 0) return -1; + if (rcode != 0) rcode = cf_item_parse(cs, "ipv4addr", FR_ITEM_POINTER(PW_TYPE_IPV4_ADDR, &ipaddr), NULL); + if (rcode < 0) return -1; + if (rcode != 0) rcode = cf_item_parse(cs, "ipv6addr", FR_ITEM_POINTER(PW_TYPE_IPV6_ADDR, &ipaddr), NULL); + if (rcode < 0) return -1; + if (rcode != 0) { + cf_log_err_cs(cs, "No address specified in listen section"); + return -1; + } + + rcode = cf_item_parse(cs, "port", FR_ITEM_POINTER(PW_TYPE_SHORT, &listen_port), "0"); + if (rcode < 0) return -1; + + rcode = cf_item_parse(cs, "recv_buff", PW_TYPE_INTEGER, &sock->recv_buff, NULL); + if (rcode < 0) return -1; + + sock->proto = IPPROTO_UDP; + + if (cf_pair_find(cs, "proto")) { +#ifndef WITH_TCP + cf_log_err_cs(cs, + "System does not support the TCP protocol. Delete this line from the configuration file"); + return -1; +#else + char const *proto = NULL; +#ifdef WITH_TLS + CONF_SECTION *tls; +#endif + + rcode = cf_item_parse(cs, "proto", FR_ITEM_POINTER(PW_TYPE_STRING, &proto), "udp"); + if (rcode < 0) return -1; + + if (!proto || strcmp(proto, "udp") == 0) { + sock->proto = IPPROTO_UDP; + + } else if (strcmp(proto, "tcp") == 0) { + sock->proto = IPPROTO_TCP; + + } else { + cf_log_err_cs(cs, + "Unknown proto name \"%s\"", proto); + return -1; + } + + /* + * TCP requires a destination IP for sockets. + * UDP doesn't, so it's allowed. + */ +#ifdef WITH_PROXY + if ((this->type == RAD_LISTEN_PROXY) && + (sock->proto != IPPROTO_UDP)) { + cf_log_err_cs(cs, + "Proxy listeners can only listen on proto = udp"); + return -1; + } +#endif /* WITH_PROXY */ + +#ifdef WITH_TLS + tls = cf_section_sub_find(cs, "tls"); + + if (tls) { + /* + * Don't allow TLS configurations for UDP sockets. + */ + if (sock->proto != IPPROTO_TCP) { + cf_log_err_cs(cs, + "TLS transport is not available for UDP sockets"); + return -1; + } + + /* + * Add support for http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt + */ + rcode = cf_item_parse(cs, "proxy_protocol", FR_ITEM_POINTER(PW_TYPE_BOOLEAN, &this->proxy_protocol), NULL); + if (rcode < 0) return -1; + + /* + * Allow non-blocking for TLS sockets + */ + rcode = cf_item_parse(cs, "nonblock", FR_ITEM_POINTER(PW_TYPE_BOOLEAN, &this->nonblock), NULL); + if (rcode < 0) return -1; + + /* + * If unset, set to default. + */ + if (listen_port == 0) listen_port = PW_RADIUS_TLS_PORT; + + this->tls = tls_server_conf_parse(tls); + if (!this->tls) { + return -1; + } + +#ifdef HAVE_PTHREAD_H + if (pthread_mutex_init(&sock->mutex, NULL) < 0) { + rad_assert(0 == 1); + listen_free(&this); + return 0; + } +#endif + + rcode = cf_item_parse(cs, "check_client_connections", FR_ITEM_POINTER(PW_TYPE_BOOLEAN, &this->check_client_connections), "no"); + if (rcode < 0) return -1; + +#ifdef WITH_RADIUSV11 + if (this->tls->radiusv11_name) { + rcode = fr_str2int(radiusv11_types, this->tls->radiusv11_name, -1); + if (rcode < 0) { + cf_log_err_cs(cs, "Invalid value for 'radiusv11'"); + return -1; + } + + this->radiusv11 = this->tls->radiusv11 = rcode; + } +#endif + } +#else /* WITH_TLS */ + /* + * Built without TLS. Disallow it. + */ + if (cf_section_sub_find(cs, "tls")) { + cf_log_err_cs(cs, + "TLS transport is not available in this executable"); + return -1; + } +#endif /* WITH_TLS */ + +#endif /* WITH_TCP */ + + /* + * No "proto" field. Disallow TLS. + */ + } else if (cf_section_sub_find(cs, "tls")) { + cf_log_err_cs(cs, + "TLS transport is not available in this \"listen\" section"); + return -1; + } + + /* + * Magical tuning methods! + */ + subcs = cf_section_sub_find(cs, "performance"); + if (subcs) { + rcode = cf_section_parse(subcs, this, + performance_config); + if (rcode < 0) return -1; + + if (this->synchronous && sock->max_rate) { + WARN("Setting 'max_pps' is incompatible with 'synchronous'. Disabling 'max_pps'"); + sock->max_rate = 0; + } + + if (!this->synchronous && this->workers) { + WARN("Setting 'workers' requires 'synchronous'. Disabling 'workers'"); + this->workers = 0; + } + } + + subcs = cf_section_sub_find(cs, "limit"); + if (subcs) { + rcode = cf_section_parse(subcs, sock, + limit_config); + if (rcode < 0) return -1; + + if (sock->max_rate && ((sock->max_rate < 10) || (sock->max_rate > 1000000))) { + cf_log_err_cs(cs, + "Invalid value for \"max_pps\""); + return -1; + } + +#ifdef WITH_TCP + if ((sock->limit.idle_timeout > 0) && (sock->limit.idle_timeout < 5)) { + WARN("Setting idle_timeout to 5"); + sock->limit.idle_timeout = 5; + } + + if ((sock->limit.lifetime > 0) && (sock->limit.lifetime < 5)) { + WARN("Setting lifetime to 5"); + sock->limit.lifetime = 5; + } + + if ((sock->limit.lifetime > 0) && (sock->limit.idle_timeout > sock->limit.lifetime)) { + WARN("Setting idle_timeout to 0"); + sock->limit.idle_timeout = 0; + } + + /* + * Force no duplicate detection for TCP sockets. + */ + if (sock->proto == IPPROTO_TCP) { + this->nodup = true; + } + + } else { + sock->limit.max_connections = 60; + sock->limit.idle_timeout = 30; + sock->limit.lifetime = 0; +#endif + } + + sock->my_ipaddr = ipaddr; + sock->my_port = listen_port; + +#ifdef WITH_PROXY + if (check_config) { + /* + * Until there is a side effects free way of forwarding a + * request to another virtual server, this check is invalid, + * and should be left disabled. + */ +#if 0 + if (home_server_find(&sock->my_ipaddr, sock->my_port, sock->proto)) { + char buffer[128]; + + ERROR("We have been asked to listen on %s port %d, which is also listed as a " + "home server. This can create a proxy loop", + ip_ntoh(&sock->my_ipaddr, buffer, sizeof(buffer)), sock->my_port); + return -1; + } +#endif + return 0; /* don't do anything */ + } +#endif + + /* + * If we can bind to interfaces, do so, + * else don't. + */ + cp = cf_pair_find(cs, "interface"); + if (cp) { + char const *value = cf_pair_value(cp); + if (!value) { + cf_log_err_cs(cs, + "No interface name given"); + return -1; + } + sock->interface = value; + } + +#ifdef WITH_DHCP + /* + * If we can do broadcasts.. + */ + cp = cf_pair_find(cs, "broadcast"); + if (cp) { +#ifndef SO_BROADCAST + cf_log_err_cs(cs, + "System does not support broadcast sockets. Delete this line from the configuration file"); + return -1; +#else + if (this->type != RAD_LISTEN_DHCP) { + cf_log_err_cp(cp, + "Broadcast can only be set for DHCP listeners. Delete this line from the configuration file"); + return -1; + } + + char const *value = cf_pair_value(cp); + if (!value) { + cf_log_err_cs(cs, + "No broadcast value given"); + return -1; + } + + /* + * Hack... whatever happened to cf_section_parse? + */ + sock->broadcast = (strcmp(value, "yes") == 0); +#endif + } +#endif + + /* + * And bind it to the port. + */ + if (listen_bind(this) < 0) { + char buffer[128]; + cf_log_err_cs(cs, + "Error binding to port for %s port %d", + ip_ntoh(&sock->my_ipaddr, buffer, sizeof(buffer)), + sock->my_port); + return -1; + } + +#ifdef WITH_PROXY + /* + * Proxy sockets don't have clients. + */ + if (this->type == RAD_LISTEN_PROXY) return 0; +#endif + + /* + * The more specific configurations are preferred to more + * generic ones. + */ + client_cs = NULL; + parentcs = cf_top_section(cs); + rcode = cf_item_parse(cs, "clients", FR_ITEM_POINTER(PW_TYPE_STRING, §ion_name), NULL); + if (rcode < 0) return -1; /* bad string */ + if (rcode == 0) { + /* + * Explicit list given: use it. + */ + client_cs = cf_section_sub_find_name2(parentcs, "clients", section_name); + if (!client_cs) { + client_cs = cf_section_find(section_name); + } + if (!client_cs) { + cf_log_err_cs(cs, + "Failed to find clients %s {...}", + section_name); + return -1; + } + } /* else there was no "clients = " entry. */ + + /* + * The "listen" section wasn't given an explicit client list. + * Look for (a) clients in this virtual server, or + * (b) the global client list. + */ + if (!client_cs) { + CONF_SECTION *server_cs; + + server_cs = cf_section_sub_find_name2(parentcs, + "server", + this->server); + /* + * Found a "server foo" section. If there are clients + * in it, use them. + */ + if (server_cs && + (cf_section_sub_find(server_cs, "client") != NULL)) { + client_cs = server_cs; + } + } + + /* + * Still nothing. Look for global clients. + */ + if (!client_cs) client_cs = parentcs; + +#ifdef WITH_TLS + sock->clients = client_list_parse_section(client_cs, (this->tls != NULL)); +#else + sock->clients = client_list_parse_section(client_cs, false); +#endif + if (!sock->clients) { + cf_log_err_cs(cs, + "Failed to load clients for this listen section"); + return -1; + } + +#ifdef WITH_TCP + if (sock->proto == IPPROTO_TCP) { + /* + * Re-write the listener receive function to + * allow us to accept the socket. + */ + this->recv = dual_tcp_accept; + + /* + * @todo - add a free function? Though this only + * matters when we're tearing down the server, so + * perhaps it's less relevant. + */ + this->children = rbtree_create(this, listener_cmp, NULL, 0); + if (!this->children) { + cf_log_err_cs(cs, "Failed to create child list for TCP socket."); + return -1; + } + } +#endif + + return 0; +} + +/* + * Send a response packet + */ +static int common_socket_send(rad_listen_t *listener, REQUEST *request) +{ + rad_assert(request->listener == listener); + rad_assert(listener->send == common_socket_send); + + if (request->reply->code == 0) return 0; + +#ifdef WITH_UDPFROMTO + /* + * Overwrite the src ip address on the outbound packet + * with the one specified by the client. + * This is useful to work around broken DSR implementations + * and other routing issues. + */ + if (request->client->src_ipaddr.af != AF_UNSPEC) { + request->reply->src_ipaddr = request->client->src_ipaddr; + } +#endif + + if (rad_send(request->reply, request->packet, + request->client->secret) < 0) { + RERROR("Failed sending reply: %s", + fr_strerror()); + return -1; + } + return 0; +} + + +#ifdef WITH_PROXY +/* + * Send a packet to a home server. + * + * FIXME: have different code for proxy auth & acct! + */ +static int proxy_socket_send(rad_listen_t *listener, REQUEST *request) +{ + rad_assert(request->proxy_listener == listener); + rad_assert(listener->proxy_send == proxy_socket_send); + + if (rad_send(request->proxy, NULL, + request->home_server->secret) < 0) { + RERROR("Failed sending proxied request: %s", + fr_strerror()); + return -1; + } + + return 0; +} +#endif + +#ifdef WITH_STATS +/* + * Check if an incoming request is "ok" + * + * It takes packets, not requests. It sees if the packet looks + * OK. If so, it does a number of sanity checks on it. + */ +static int stats_socket_recv(rad_listen_t *listener) +{ + ssize_t rcode; + int code; + uint16_t src_port; + RADIUS_PACKET *packet; + RADCLIENT *client = NULL; + fr_ipaddr_t src_ipaddr; + + rcode = rad_recv_header(listener->fd, &src_ipaddr, &src_port, &code); + if (rcode < 0) return 0; + + FR_STATS_INC(auth, total_requests); + + if (rcode < 20) { /* RADIUS_HDR_LEN */ + if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror()); + FR_STATS_INC(auth, total_malformed_requests); + return 0; + } + + if ((client = client_listener_find(listener, + &src_ipaddr, src_port)) == NULL) { + rad_recv_discard(listener->fd); + FR_STATS_INC(auth, total_invalid_requests); + return 0; + } + + FR_STATS_TYPE_INC(client->auth.total_requests); + + /* + * We only understand Status-Server on this socket. + */ + if (code != PW_CODE_STATUS_SERVER) { + DEBUG("Ignoring packet code %d sent to Status-Server port", + code); + rad_recv_discard(listener->fd); + FR_STATS_INC(auth, total_unknown_types); + return 0; + } + + /* + * Now that we've sanity checked everything, receive the + * packet. + */ + packet = rad_recv(NULL, listener->fd, 1); /* require message authenticator */ + if (!packet) { + FR_STATS_INC(auth, total_malformed_requests); + if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror()); + return 0; + } + + if (!request_receive(NULL, listener, packet, client, rad_status_server)) { + FR_STATS_INC(auth, total_packets_dropped); + rad_free(&packet); + return 0; + } + + return 1; +} +#endif + + +/* + * Check if an incoming request is "ok" + * + * It takes packets, not requests. It sees if the packet looks + * OK. If so, it does a number of sanity checks on it. + */ +static int auth_socket_recv(rad_listen_t *listener) +{ + ssize_t rcode; + int code; + uint16_t src_port; + RADIUS_PACKET *packet; + RAD_REQUEST_FUNP fun = NULL; + RADCLIENT *client = NULL; + fr_ipaddr_t src_ipaddr; + TALLOC_CTX *ctx; + + rcode = rad_recv_header(listener->fd, &src_ipaddr, &src_port, &code); + if (rcode < 0) return 0; + + FR_STATS_INC(auth, total_requests); + + if (rcode < 20) { /* RADIUS_HDR_LEN */ + if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror()); + FR_STATS_INC(auth, total_malformed_requests); + return 0; + } + + if ((client = client_listener_find(listener, + &src_ipaddr, src_port)) == NULL) { + rad_recv_discard(listener->fd); + FR_STATS_INC(auth, total_invalid_requests); + return 0; + } + + /* + * Some sanity checks, based on the packet code. + */ + switch (code) { + case PW_CODE_ACCESS_REQUEST: + FR_STATS_TYPE_INC(client->auth.total_requests); + fun = rad_authenticate; + break; + + case PW_CODE_STATUS_SERVER: + if (!main_config.status_server) { + rad_recv_discard(listener->fd); + FR_STATS_INC(auth, total_unknown_types); + WARN("Ignoring Status-Server request due to security configuration"); + return 0; + } + fun = rad_status_server; + break; + + default: + rad_recv_discard(listener->fd); + FR_STATS_INC(auth, total_unknown_types); + + if (DEBUG_ENABLED) ERROR("Receive - Invalid packet code %d sent to authentication port from " + "client %s port %d", code, client->shortname, src_port); + return 0; + } /* switch over packet types */ + + ctx = talloc_pool(NULL, main_config.talloc_pool_size); + if (!ctx) { + rad_recv_discard(listener->fd); + FR_STATS_INC(auth, total_packets_dropped); + return 0; + } + talloc_set_name_const(ctx, "auth_listener_pool"); + + /* + * Now that we've sanity checked everything, receive the + * packet. + */ + packet = rad_recv(ctx, listener->fd, client->message_authenticator); + if (!packet) { + FR_STATS_INC(auth, total_malformed_requests); + if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror()); + talloc_free(ctx); + return 0; + } + +#ifdef __APPLE__ +#ifdef WITH_UDPFROMTO + /* + * This is a NICE Mac OSX bug. Create an interface with + * two IP address, and then configure one listener for + * each IP address. Send thousands of packets to one + * address, and some will show up on the OTHER socket. + * + * This hack works ONLY if the clients are global. If + * each listener has the same client IP, but with + * different secrets, then it will fail the rad_recv() + * check above, and there's nothing you can do. + */ + { + listen_socket_t *sock = listener->data; + rad_listen_t *other; + + other = listener_find_byipaddr(&packet->dst_ipaddr, + packet->dst_port, sock->proto); + if (other) listener = other; + } +#endif +#endif + + if (!request_receive(ctx, listener, packet, client, fun)) { + FR_STATS_INC(auth, total_packets_dropped); + talloc_free(ctx); + return 0; + } + + return 1; +} + + +#ifdef WITH_ACCOUNTING +/* + * Receive packets from an accounting socket + */ +static int acct_socket_recv(rad_listen_t *listener) +{ + ssize_t rcode; + int code; + uint16_t src_port; + RADIUS_PACKET *packet; + RAD_REQUEST_FUNP fun = NULL; + RADCLIENT *client = NULL; + fr_ipaddr_t src_ipaddr; + TALLOC_CTX *ctx; + + rcode = rad_recv_header(listener->fd, &src_ipaddr, &src_port, &code); + if (rcode < 0) return 0; + + FR_STATS_INC(acct, total_requests); + + if (rcode < 20) { /* RADIUS_HDR_LEN */ + if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror()); + FR_STATS_INC(acct, total_malformed_requests); + return 0; + } + + if ((client = client_listener_find(listener, + &src_ipaddr, src_port)) == NULL) { + rad_recv_discard(listener->fd); + FR_STATS_INC(acct, total_invalid_requests); + return 0; + } + + /* + * Some sanity checks, based on the packet code. + */ + switch (code) { + case PW_CODE_ACCOUNTING_REQUEST: + FR_STATS_TYPE_INC(client->acct.total_requests); + fun = rad_accounting; + break; + + case PW_CODE_STATUS_SERVER: + if (!main_config.status_server) { + rad_recv_discard(listener->fd); + FR_STATS_INC(acct, total_unknown_types); + + WARN("Ignoring Status-Server request due to security configuration"); + return 0; + } + fun = rad_status_server; + break; + + default: + rad_recv_discard(listener->fd); + FR_STATS_INC(acct, total_unknown_types); + + DEBUG("Invalid packet code %d sent to a accounting port from client %s port %d : IGNORED", + code, client->shortname, src_port); + return 0; + } /* switch over packet types */ + + ctx = talloc_pool(NULL, main_config.talloc_pool_size); + if (!ctx) { + rad_recv_discard(listener->fd); + FR_STATS_INC(acct, total_packets_dropped); + return 0; + } + talloc_set_name_const(ctx, "acct_listener_pool"); + + /* + * Now that we've sanity checked everything, receive the + * packet. + */ + packet = rad_recv(ctx, listener->fd, 0); + if (!packet) { + FR_STATS_INC(acct, total_malformed_requests); + if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror()); + talloc_free(ctx); + return 0; + } + + /* + * There can be no duplicate accounting packets. + */ + if (!request_receive(ctx, listener, packet, client, fun)) { + FR_STATS_INC(acct, total_packets_dropped); + rad_free(&packet); + talloc_free(ctx); + return 0; + } + + return 1; +} +#endif + + +#ifdef WITH_COA +static int do_proxy(REQUEST *request) +{ + VALUE_PAIR *vp; + + if (request->in_proxy_hash || + (request->proxy_reply && (request->proxy_reply->code != 0))) { + return 0; + } + + vp = fr_pair_find_by_num(request->config, PW_HOME_SERVER_POOL, 0, TAG_ANY); + + if (vp) { + if (!home_pool_byname(vp->vp_strvalue, HOME_TYPE_COA)) { + REDEBUG2("Cannot proxy to unknown pool %s", + vp->vp_strvalue); + return -1; + } + + return 1; + } + + /* + * We have a destination IP address. It will (later) proxied. + */ + vp = fr_pair_find_by_num(request->config, PW_PACKET_DST_IP_ADDRESS, 0, TAG_ANY); + if (!vp) vp = fr_pair_find_by_num(request->config, PW_PACKET_DST_IPV6_ADDRESS, 0, TAG_ANY); + +#ifdef WITH_COA_TUNNEL + if (!vp) vp = fr_pair_find_by_num(request->config, PW_PROXY_TO_ORIGINATING_REALM, 0, TAG_ANY); +#endif + + if (!vp) return 0; + + return 1; +} + +/* + * Receive a CoA packet. + */ +int rad_coa_recv(REQUEST *request) +{ + int rcode = RLM_MODULE_OK; + int ack, nak; + int proxy_status; + VALUE_PAIR *vp; + + /* + * Get the correct response + */ + switch (request->packet->code) { + case PW_CODE_COA_REQUEST: + ack = PW_CODE_COA_ACK; + nak = PW_CODE_COA_NAK; + break; + + case PW_CODE_DISCONNECT_REQUEST: + ack = PW_CODE_DISCONNECT_ACK; + nak = PW_CODE_DISCONNECT_NAK; + break; + + default: /* shouldn't happen */ + return RLM_MODULE_FAIL; + } + +#ifdef WITH_PROXY +#define WAS_PROXIED (request->proxy) +#else +#define WAS_PROXIED (0) +#endif + + if (!WAS_PROXIED) { + /* + * RFC 5176 Section 3.3. If we have a CoA-Request + * with Service-Type = Authorize-Only, it MUST + * have a State attribute in it. + */ + vp = fr_pair_find_by_num(request->packet->vps, PW_SERVICE_TYPE, 0, TAG_ANY); + if (request->packet->code == PW_CODE_COA_REQUEST) { + if (vp && (vp->vp_integer == PW_AUTHORIZE_ONLY)) { + vp = fr_pair_find_by_num(request->packet->vps, PW_STATE, 0, TAG_ANY); + if (!vp || (vp->vp_length == 0)) { + REDEBUG("CoA-Request with Service-Type = Authorize-Only MUST contain a State attribute"); + request->reply->code = PW_CODE_COA_NAK; + return RLM_MODULE_FAIL; + } + } + } else if (vp) { + /* + * RFC 5176, Section 3.2. + */ + REDEBUG("Disconnect-Request MUST NOT contain a Service-Type attribute"); + request->reply->code = PW_CODE_DISCONNECT_NAK; + return RLM_MODULE_FAIL; + } + + rcode = process_recv_coa(0, request); + switch (rcode) { + case RLM_MODULE_FAIL: + case RLM_MODULE_INVALID: + case RLM_MODULE_REJECT: + case RLM_MODULE_USERLOCK: + default: + request->reply->code = nak; + break; + + case RLM_MODULE_HANDLED: + return rcode; + + case RLM_MODULE_NOOP: + case RLM_MODULE_NOTFOUND: + case RLM_MODULE_OK: + case RLM_MODULE_UPDATED: + proxy_status = do_proxy(request); + if (proxy_status == 1) return RLM_MODULE_OK; + + if (proxy_status < 0) { + request->reply->code = nak; + } else { + request->reply->code = ack; + } + break; + } + + } + +#ifdef WITH_PROXY + else if (request->proxy_reply) { + /* + * Start the reply code with the proxy reply + * code. + */ + request->reply->code = request->proxy_reply->code; + } +#endif + + /* + * Copy State from the request to the reply. + * See RFC 5176 Section 3.3. + */ + vp = fr_pair_list_copy_by_num(request->reply, request->packet->vps, PW_STATE, 0, TAG_ANY); + if (vp) fr_pair_add(&request->reply->vps, vp); + + /* + * We may want to over-ride the reply. + */ + if (request->reply->code) { + rcode = process_send_coa(0, request); + switch (rcode) { + /* + * We need to send CoA-NAK back if Service-Type + * is Authorize-Only. Rely on the user's policy + * to do that. We're not a real NAS, so this + * restriction doesn't (ahem) apply to us. + */ + case RLM_MODULE_FAIL: + case RLM_MODULE_INVALID: + case RLM_MODULE_REJECT: + case RLM_MODULE_USERLOCK: + default: + /* + * Over-ride an ACK with a NAK + */ + request->reply->code = nak; + break; + + case RLM_MODULE_HANDLED: + return rcode; + + case RLM_MODULE_NOOP: + case RLM_MODULE_NOTFOUND: + case RLM_MODULE_OK: + case RLM_MODULE_UPDATED: + /* + * Do NOT over-ride a previously set value. + * Otherwise an "ok" here will re-write a + * NAK to an ACK. + */ + if (request->reply->code == 0) { + request->reply->code = ack; + } + break; + } + } + + return RLM_MODULE_OK; +} + + +/* + * Check if an incoming request is "ok" + * + * It takes packets, not requests. It sees if the packet looks + * OK. If so, it does a number of sanity checks on it. + */ +static int coa_socket_recv(rad_listen_t *listener) +{ + ssize_t rcode; + int code; + uint16_t src_port; + RADIUS_PACKET *packet; + RAD_REQUEST_FUNP fun = NULL; + RADCLIENT *client = NULL; + fr_ipaddr_t src_ipaddr; + TALLOC_CTX *ctx; + + rcode = rad_recv_header(listener->fd, &src_ipaddr, &src_port, &code); + if (rcode < 0) return 0; + + if (rcode < 20) { /* RADIUS_HDR_LEN */ + if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror()); + FR_STATS_INC(coa, total_malformed_requests); + return 0; + } + + if ((client = client_listener_find(listener, + &src_ipaddr, src_port)) == NULL) { + rad_recv_discard(listener->fd); + FR_STATS_INC(coa, total_requests); + FR_STATS_INC(coa, total_invalid_requests); + return 0; + } + + /* + * Some sanity checks, based on the packet code. + */ + switch (code) { + case PW_CODE_COA_REQUEST: + FR_STATS_INC(coa, total_requests); + fun = rad_coa_recv; + break; + + case PW_CODE_DISCONNECT_REQUEST: + FR_STATS_INC(dsc, total_requests); + fun = rad_coa_recv; + break; + + default: + rad_recv_discard(listener->fd); + FR_STATS_INC(coa, total_unknown_types); + DEBUG("Invalid packet code %d sent to coa port from client %s port %d : IGNORED", + code, client->shortname, src_port); + return 0; + } /* switch over packet types */ + + ctx = talloc_pool(NULL, main_config.talloc_pool_size); + if (!ctx) { + rad_recv_discard(listener->fd); + FR_STATS_INC(coa, total_packets_dropped); + return 0; + } + talloc_set_name_const(ctx, "coa_socket_recv_pool"); + + /* + * Now that we've sanity checked everything, receive the + * packet. + */ + packet = rad_recv(ctx, listener->fd, client->message_authenticator); + if (!packet) { + FR_STATS_INC(coa, total_malformed_requests); + if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror()); + talloc_free(ctx); + return 0; + } + + if (!request_receive(ctx, listener, packet, client, fun)) { + FR_STATS_INC(coa, total_packets_dropped); + rad_free(&packet); + talloc_free(ctx); + return 0; + } + + return 1; +} +#endif + +#ifdef WITH_PROXY +/* + * Recieve packets from a proxy socket. + */ +static int proxy_socket_recv(rad_listen_t *listener) +{ + RADIUS_PACKET *packet; +#ifdef WITH_TCP + listen_socket_t *sock; +#endif + char buffer[128]; + + packet = rad_recv(NULL, listener->fd, 0); + if (!packet) { + if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror()); + return 0; + } + + switch (packet->code) { + case PW_CODE_ACCESS_ACCEPT: + case PW_CODE_ACCESS_CHALLENGE: + case PW_CODE_ACCESS_REJECT: + break; + +#ifdef WITH_ACCOUNTING + case PW_CODE_ACCOUNTING_RESPONSE: + break; +#endif + +#ifdef WITH_COA + case PW_CODE_DISCONNECT_ACK: + case PW_CODE_DISCONNECT_NAK: + case PW_CODE_COA_ACK: + case PW_CODE_COA_NAK: + break; +#endif + + default: + /* + * FIXME: Update MIB for packet types? + */ + ERROR("Invalid packet code %d sent to a proxy port " + "from home server %s port %d - ID %d : IGNORED", + packet->code, + ip_ntoh(&packet->src_ipaddr, buffer, sizeof(buffer)), + packet->src_port, packet->id); +#ifdef WITH_STATS + listener->stats.total_unknown_types++; +#endif + rad_free(&packet); + return 0; + } + +#ifdef WITH_TCP + sock = listener->data; + packet->proto = sock->proto; +#endif + + if (!request_proxy_reply(packet)) { +#ifdef WITH_STATS + listener->stats.total_packets_dropped++; +#endif + rad_free(&packet); + return 0; + } + + return 1; +} + +#ifdef WITH_TCP +/* + * Recieve packets from a proxy socket. + */ +static int proxy_socket_tcp_recv(rad_listen_t *listener) +{ + int rcode; + RADIUS_PACKET *packet; + listen_socket_t *sock = listener->data; + char buffer[256]; + + if (listener->status != RAD_LISTEN_STATUS_KNOWN) return 0; + + if (!sock->packet) { + sock->packet = rad_alloc(sock, false); + if (!sock->packet) return 0; + + sock->packet->sockfd = listener->fd; + sock->packet->src_ipaddr = sock->other_ipaddr; + sock->packet->src_port = sock->other_port; + sock->packet->dst_ipaddr = sock->my_ipaddr; + sock->packet->dst_port = sock->my_port; + sock->packet->proto = sock->proto; + } + + packet = sock->packet; + + rcode = fr_tcp_read_packet(packet, 0); + + /* + * Still only a partial packet. Put it back, and return, + * so that we'll read more data when it's ready. + */ + if (rcode == 0) { + return 0; + } + + if (rcode == -1) { /* error reading packet */ + ERROR("Invalid packet from %s port %d, closing socket: %s", + ip_ntoh(&packet->src_ipaddr, buffer, sizeof(buffer)), + packet->src_port, fr_strerror()); + } + + if (rcode < 0) { /* error or connection reset */ + listener->status = RAD_LISTEN_STATUS_EOL; + + /* + * Tell the event handler that an FD has disappeared. + */ + DEBUG("Home server %s port %d has closed connection", + ip_ntoh(&packet->src_ipaddr, buffer, sizeof(buffer)), + packet->src_port); + + radius_update_listener(listener); + + /* + * Do NOT free the listener here. It's in use by + * a request, and will need to hang around until + * all of the requests are done. + * + * It is instead free'd in remove_from_request_hash() + */ + return 0; + } + + sock->packet = NULL; /* we have no need for more partial reads */ + + /* + * FIXME: Client MIB updates? + */ + switch (packet->code) { + case PW_CODE_ACCESS_ACCEPT: + case PW_CODE_ACCESS_CHALLENGE: + case PW_CODE_ACCESS_REJECT: + break; + +#ifdef WITH_ACCOUNTING + case PW_CODE_ACCOUNTING_RESPONSE: + break; +#endif + + default: + /* + * FIXME: Update MIB for packet types? + */ + ERROR("Invalid packet code %d sent to a proxy port " + "from home server %s port %d - ID %d : IGNORED", + packet->code, + ip_ntoh(&packet->src_ipaddr, buffer, sizeof(buffer)), + packet->src_port, packet->id); + rad_free(&packet); + return 0; + } + + + /* + * FIXME: Have it return an indication of packets that + * are OK to ignore (dups, too late), versus ones that + * aren't OK to ignore (unknown response, spoofed, etc.) + * + * Close the socket on bad packets... + */ + if (!request_proxy_reply(packet)) { + rad_free(&packet); + return 0; + } + + sock->opened = sock->last_packet = time(NULL); + + return 1; +} +#endif +#endif + +#ifdef WITH_TLS +#define TLS_UNUSED +#else +#define TLS_UNUSED UNUSED +#endif + +static int client_socket_encode(TLS_UNUSED rad_listen_t *listener, REQUEST *request) +{ +#ifdef WITH_TLS + /* + * Don't encode fake packets. + */ + listen_socket_t *sock = listener->data; + if (sock->state == LISTEN_TLS_CHECKING) return 0; + +#ifdef WITH_RADIUSV11 + request->reply->radiusv11 = sock->radiusv11; +#endif + +#endif + + if (!request->reply->code) return 0; + + if (request->reply->data) return 0; /* already encoded */ + + if (rad_encode(request->reply, request->packet, request->client->secret) < 0) { + RERROR("Failed encoding packet: %s", fr_strerror()); + + return -1; + } + + if (request->reply->data_len > (MAX_PACKET_LEN - 100)) { + RWDEBUG("Packet is large, and possibly truncated - %zd vs max %d", + request->reply->data_len, MAX_PACKET_LEN); + } + + if (rad_sign(request->reply, request->packet, request->client->secret) < 0) { + RERROR("Failed signing packet: %s", fr_strerror()); + + return -1; + } + + return 0; +} + + +static int client_socket_decode(UNUSED rad_listen_t *listener, REQUEST *request) +{ +#ifdef WITH_TLS + listen_socket_t *sock = request->listener->data; + +#ifdef WITH_RADIUSV11 + request->packet->radiusv11 = sock->radiusv11; +#endif +#endif + + if (rad_verify(request->packet, NULL, + request->client->secret) < 0) { + return -1; + } + +#ifdef WITH_TLS + /* + * FIXME: Add the rest of the TLS parameters, too? But + * how do we separate EAP-TLS parameters from RADIUS/TLS + * parameters? + */ + if (sock->ssn && sock->ssn->ssl) { +#ifdef PSK_MAX_IDENTITY_LEN + const char *identity = SSL_get_psk_identity(sock->ssn->ssl); + if (identity) { + RDEBUG("Retrieved psk identity: %s", identity); + pair_make_request("TLS-PSK-Identity", identity, T_OP_SET); + } +#endif + } +#endif + + return rad_decode(request->packet, NULL, + request->client->secret); +} + +#ifdef WITH_PROXY +#ifdef WITH_RADIUSV11 +#define RADIUSV11_UNUSED +#else +#define RADIUSV11_UNUSED UNUSED +#endif + +static int proxy_socket_encode(RADIUSV11_UNUSED rad_listen_t *listener, REQUEST *request) +{ +#ifdef WITH_RADIUSV11 + listen_socket_t *sock = listener->data; + + request->proxy->radiusv11 = sock->radiusv11; +#endif + + if (rad_encode(request->proxy, NULL, request->home_server->secret) < 0) { + RERROR("Failed encoding proxied packet: %s", fr_strerror()); + + return -1; + } + + if (request->proxy->data_len > (MAX_PACKET_LEN - 100)) { + RWDEBUG("Packet is large, and possibly truncated - %zd vs max %d", + request->proxy->data_len, MAX_PACKET_LEN); + } + + if (rad_sign(request->proxy, NULL, request->home_server->secret) < 0) { + RERROR("Failed signing proxied packet: %s", fr_strerror()); + + return -1; + } + + return 0; +} + + +static int proxy_socket_decode(UNUSED rad_listen_t *listener, REQUEST *request) +{ +#ifdef WITH_RADIUSV11 + listen_socket_t *sock = listener->data; + + request->proxy_reply->radiusv11 = sock->radiusv11; +#endif + + /* + * rad_verify is run in event.c, received_proxy_response() + */ + + return rad_decode(request->proxy_reply, request->proxy, + request->home_server->secret); +} +#endif + +#include "command.c" + +/* + * Temporarily NOT const! + */ +static fr_protocol_t master_listen[RAD_LISTEN_MAX] = { +#ifdef WITH_STATS + { RLM_MODULE_INIT, "status", sizeof(listen_socket_t), NULL, + common_socket_parse, NULL, + stats_socket_recv, common_socket_send, + common_socket_print, client_socket_encode, client_socket_decode }, +#else + /* + * This always gets defined. + */ + { RLM_MODULE_INIT, "status", 0, NULL, + NULL, NULL, NULL, NULL, NULL, NULL, NULL}, /* RAD_LISTEN_NONE */ +#endif + +#ifdef WITH_PROXY + /* proxying */ + { RLM_MODULE_INIT, "proxy", sizeof(listen_socket_t), NULL, + common_socket_parse, common_socket_free, + proxy_socket_recv, proxy_socket_send, + common_socket_print, proxy_socket_encode, proxy_socket_decode }, +#else + { 0, "proxy", 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }, +#endif + + /* authentication */ + { RLM_MODULE_INIT, "auth", sizeof(listen_socket_t), NULL, + common_socket_parse, common_socket_free, + auth_socket_recv, common_socket_send, + common_socket_print, client_socket_encode, client_socket_decode }, + +#ifdef WITH_ACCOUNTING + /* accounting */ + { RLM_MODULE_INIT, "acct", sizeof(listen_socket_t), NULL, + common_socket_parse, common_socket_free, + acct_socket_recv, common_socket_send, + common_socket_print, client_socket_encode, client_socket_decode}, +#else + { 0, "acct", 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }, +#endif + +#ifdef WITH_DETAIL + /* detail */ + { RLM_MODULE_INIT, "detail", sizeof(listen_detail_t), NULL, + detail_parse, detail_free, + detail_recv, detail_send, + detail_print, detail_encode, detail_decode }, +#endif + + /* vlan query protocol */ + { 0, "vmps", 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }, + + /* dhcp query protocol */ + { 0, "dhcp", 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }, + +#ifdef WITH_COMMAND_SOCKET + /* TCP command socket */ + { RLM_MODULE_INIT, "control", sizeof(fr_command_socket_t), NULL, + command_socket_parse, command_socket_free, + command_domain_accept, command_domain_send, + command_socket_print, command_socket_encode, command_socket_decode }, +#else + { 0, "command", 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }, +#endif + +#ifdef WITH_COA + /* Change of Authorization */ + { RLM_MODULE_INIT, "coa", sizeof(listen_socket_t), NULL, + common_socket_parse, NULL, + coa_socket_recv, common_socket_send, + common_socket_print, client_socket_encode, client_socket_decode }, +#else + { 0, "coa", 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }, +#endif + +}; + + + +/* + * Binds a listener to a socket. + */ +static int listen_bind(rad_listen_t *this) +{ + int rcode; + struct sockaddr_storage salocal; + socklen_t salen; + listen_socket_t *sock = this->data; +#ifndef WITH_TCP +#define proto_for_port "udp" +#define sock_type SOCK_DGRAM +#else + char const *proto_for_port = "udp"; + int sock_type = SOCK_DGRAM; + + if (sock->proto == IPPROTO_TCP) { +#ifdef WITH_VMPS + if (this->type == RAD_LISTEN_VQP) { + ERROR("VQP does not support TCP transport"); + return -1; + } +#endif + + proto_for_port = "tcp"; + sock_type = SOCK_STREAM; + } +#endif + + /* + * If the port is zero, then it means the appropriate + * thing from /etc/services. + */ + if (sock->my_port == 0) { + struct servent *svp; + + switch (this->type) { + case RAD_LISTEN_AUTH: + svp = getservbyname ("radius", proto_for_port); + if (svp != NULL) { + sock->my_port = ntohs(svp->s_port); + } else { + sock->my_port = PW_AUTH_UDP_PORT; + } + break; + +#ifdef WITH_ACCOUNTING + case RAD_LISTEN_ACCT: + svp = getservbyname ("radacct", proto_for_port); + if (svp != NULL) { + sock->my_port = ntohs(svp->s_port); + } else { + sock->my_port = PW_ACCT_UDP_PORT; + } + break; +#endif + +#ifdef WITH_PROXY + case RAD_LISTEN_PROXY: + /* leave it at zero */ + break; +#endif + +#ifdef WITH_VMPS + case RAD_LISTEN_VQP: + sock->my_port = 1589; + break; +#endif + +#ifdef WITH_COMMAND_SOCKET + case RAD_LISTEN_COMMAND: + sock->my_port = PW_RADMIN_PORT; + break; +#endif + +#ifdef WITH_COA + case RAD_LISTEN_COA: + svp = getservbyname ("radius-dynauth", "udp"); + if (svp != NULL) { + sock->my_port = ntohs(svp->s_port); + } else { + sock->my_port = PW_COA_UDP_PORT; + } + break; +#endif + +#ifdef WITH_DHCP + case RAD_LISTEN_DHCP: + svp = getservbyname ("bootps", "udp"); + if (svp != NULL) { + sock->my_port = ntohs(svp->s_port); + } else { + sock->my_port = 67; + } + break; +#endif + + default: + WARN("Internal sanity check failed in binding to socket. Ignoring problem"); + return -1; + } + } + + /* + * Don't open sockets if we're checking the config. + */ + if (check_config) { + this->fd = -1; + return 0; + } + + /* + * Copy fr_socket() here, as we may need to bind to a device. + */ + this->fd = socket(sock->my_ipaddr.af, sock_type, 0); + if (this->fd < 0) { + char buffer[256]; + + this->print(this, buffer, sizeof(buffer)); + + ERROR("Failed opening %s: %s", buffer, fr_syserror(errno)); + return -1; + } + +#ifdef FD_CLOEXEC + /* + * We don't want child processes inheriting these + * file descriptors. + */ + rcode = fcntl(this->fd, F_GETFD); + if (rcode >= 0) { + if (fcntl(this->fd, F_SETFD, rcode | FD_CLOEXEC) < 0) { + close(this->fd); + ERROR("Failed setting close on exec: %s", fr_syserror(errno)); + return -1; + } + } +#endif + + /* + * Bind to a device BEFORE touching IP addresses. + */ + if (sock->interface) { +#ifdef SO_BINDTODEVICE + struct ifreq ifreq; + + memset(&ifreq, 0, sizeof(ifreq)); + strlcpy(ifreq.ifr_name, sock->interface, sizeof(ifreq.ifr_name)); + + rad_suid_up(); + rcode = setsockopt(this->fd, SOL_SOCKET, SO_BINDTODEVICE, + (char *)&ifreq, sizeof(ifreq)); + rad_suid_down(); + if (rcode < 0) { + close(this->fd); + ERROR("Failed binding to interface %s: %s", + sock->interface, fr_syserror(errno)); + return -1; + } /* else it worked. */ +#else +#ifdef HAVE_STRUCT_SOCKADDR_IN6 +#ifdef HAVE_NET_IF_H + /* + * Odds are that any system supporting "bind to + * device" also supports IPv6, so this next bit + * isn't necessary. But it's here for + * completeness. + * + * If we're doing IPv6, and the scope hasn't yet + * been defined, set the scope to the scope of + * the interface. + */ + if (sock->my_ipaddr.af == AF_INET6) { + if (sock->my_ipaddr.scope == 0) { + sock->my_ipaddr.scope = if_nametoindex(sock->interface); + if (sock->my_ipaddr.scope == 0) { + close(this->fd); + ERROR("Failed finding interface %s: %s", + sock->interface, fr_syserror(errno)); + return -1; + } + } /* else scope was defined: we're OK. */ + } else +#endif +#endif + /* + * IPv4: no link local addresses, + * and no bind to device. + */ + { + close(this->fd); + ERROR("Failed binding to interface %s: \"bind to device\" is unsupported", sock->interface); + return -1; + } +#endif + } + +#ifdef WITH_TCP + if (sock->proto == IPPROTO_TCP) { + int on = 1; + + if (setsockopt(this->fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) { + close(this->fd); + ERROR("Failed to reuse address: %s", fr_syserror(errno)); + return -1; + } + } +#endif + +#if defined(WITH_TCP) && defined(WITH_UDPFROMTO) + else /* UDP sockets get UDPfromto */ +#endif + +#ifdef WITH_UDPFROMTO + /* + * Initialize udpfromto for all sockets. + */ + if (udpfromto_init(this->fd) != 0) { + ERROR("Failed initializing udpfromto: %s", + fr_syserror(errno)); + close(this->fd); + return -1; + } +#endif + + /* + * Set up sockaddr stuff. + */ + if (!fr_ipaddr2sockaddr(&sock->my_ipaddr, sock->my_port, &salocal, &salen)) { + close(this->fd); + return -1; + } + +#ifdef HAVE_STRUCT_SOCKADDR_IN6 + if (sock->my_ipaddr.af == AF_INET6) { + /* + * Listening on '::' does NOT get you IPv4 to + * IPv6 mapping. You've got to listen on an IPv4 + * address, too. This makes the rest of the server + * design a little simpler. + */ +#ifdef IPV6_V6ONLY + + if (IN6_IS_ADDR_UNSPECIFIED(&sock->my_ipaddr.ipaddr.ip6addr)) { + int on = 1; + + if (setsockopt(this->fd, IPPROTO_IPV6, IPV6_V6ONLY, + (char *)&on, sizeof(on)) < 0) { + ERROR("Failed setting socket to IPv6 " + "only: %s", fr_syserror(errno)); + + close(this->fd); + return -1; + } + } +#endif /* IPV6_V6ONLY */ + } +#endif /* HAVE_STRUCT_SOCKADDR_IN6 */ + + if (sock->my_ipaddr.af == AF_INET) { +#if (defined(IP_MTU_DISCOVER) && defined(IP_PMTUDISC_DONT)) || defined(IP_DONTFRAG) + int flag; +#endif + +#if defined(IP_MTU_DISCOVER) && defined(IP_PMTUDISC_DONT) + + /* + * Disable PMTU discovery. On Linux, this + * also makes sure that the "don't fragment" + * flag is zero. + */ + flag = IP_PMTUDISC_DONT; + if (setsockopt(this->fd, IPPROTO_IP, IP_MTU_DISCOVER, + &flag, sizeof(flag)) < 0) { + ERROR("Failed disabling PMTU discovery: %s", + fr_syserror(errno)); + + close(this->fd); + return -1; + } +#endif + +#if defined(IP_DONTFRAG) + /* + * Ensure that the "don't fragment" flag is zero. + */ + flag = 0; + if (setsockopt(this->fd, IPPROTO_IP, IP_DONTFRAG, + &flag, sizeof(flag)) < 0) { + ERROR("Failed setting don't fragment flag: %s", + fr_syserror(errno)); + + close(this->fd); + return -1; + } +#endif + } + +#ifdef WITH_DHCP +#ifdef SO_BROADCAST + if (sock->broadcast) { + int on = 1; + + if (setsockopt(this->fd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)) < 0) { + ERROR("Can't set broadcast option: %s", + fr_syserror(errno)); + return -1; + } + } +#endif +#endif + +#ifdef SO_RCVBUF + if (sock->recv_buff > 0) { + int opt; + + opt = sock->recv_buff; + if (setsockopt(this->fd, SOL_SOCKET, SO_RCVBUF, &opt, sizeof(int)) < 0) { + WARN("Failed setting 'recv_buf': %s", fr_syserror(errno)); + } + } +#endif + + /* + * May be binding to priviledged ports. + */ + if (sock->my_port != 0) { + rad_suid_up(); + rcode = bind(this->fd, (struct sockaddr *) &salocal, salen); + rad_suid_down(); + if (rcode < 0) { + char buffer[256]; + close(this->fd); + + this->print(this, buffer, sizeof(buffer)); + ERROR("Failed binding to %s: %s\n", + buffer, fr_syserror(errno)); + return -1; + } + + /* + * FreeBSD jail issues. We bind to 0.0.0.0, but the + * kernel instead binds us to a 1.2.3.4. If this + * happens, notice, and remember our real IP. + */ + { + struct sockaddr_storage src; + socklen_t sizeof_src = sizeof(src); + + memset(&src, 0, sizeof_src); + if (getsockname(this->fd, (struct sockaddr *) &src, + &sizeof_src) < 0) { + ERROR("Failed getting socket name: %s", + fr_syserror(errno)); + return -1; + } + + if (!fr_sockaddr2ipaddr(&src, sizeof_src, + &sock->my_ipaddr, &sock->my_port)) { + ERROR("Socket has unsupported address family"); + return -1; + } + } + } + +#ifdef WITH_TCP + if (sock->proto == IPPROTO_TCP) { + /* + * Woker threads are blocking. + * + * Otherwise, they're non-blocking. + */ + if (!this->workers) { + if (fr_nonblock(this->fd) < 0) { + close(this->fd); + ERROR("Failed setting non-blocking on socket: %s", + fr_syserror(errno)); + return -1; + } + } + + /* + * Allow a backlog of 8 listeners, but only for incoming interfaces. + */ +#ifdef WITH_PROXY + if (this->type != RAD_LISTEN_PROXY) +#endif + if (listen(this->fd, 8) < 0) { + close(this->fd); + ERROR("Failed in listen(): %s", fr_syserror(errno)); + return -1; + } + } +#endif + + /* + * Mostly for proxy sockets. + */ + sock->other_ipaddr.af = sock->my_ipaddr.af; + +/* + * Don't screw up other people. + */ +#undef proto_for_port +#undef sock_type + + return 0; +} + + +static int _listener_free(rad_listen_t *this) +{ + /* + * Other code may have eaten the FD. + */ + if (this->fd >= 0) close(this->fd); + + if (master_listen[this->type].free) { + master_listen[this->type].free(this); + } + +#ifdef WITH_TCP + if ((this->type == RAD_LISTEN_AUTH) +#ifdef WITH_ACCT + || (this->type == RAD_LISTEN_ACCT) +#endif +#ifdef WITH_PROXY + || (this->type == RAD_LISTEN_PROXY) +#endif +#ifdef WITH_COMMAND_SOCKET + || ((this->type == RAD_LISTEN_COMMAND) && + (((fr_command_socket_t *) this->data)->magic != COMMAND_SOCKET_MAGIC)) +#endif + ) { + + /* + * Remove the child from the parent tree. + */ + if (this->parent) { + rbtree_deletebydata(this->parent->children, this); + } + + /* + * Delete / close all of the children, too! + */ + if (this->children) { + rbtree_walk(this->children, RBTREE_DELETE_ORDER, listener_unlink, this); + } + +#ifdef WITH_TLS + /* + * Note that we do NOT free this->tls, as the + * pointer is parented by its CONF_SECTION. It + * may be used by multiple listeners. + */ + if (this->tls) { + listen_socket_t *sock = this->data; + + rad_assert(talloc_parent(sock) == this); + rad_assert(sock->ev == NULL); + + rad_assert(!sock->ssn || (talloc_parent(sock->ssn) == sock)); + rad_assert(!sock->request || (talloc_parent(sock->request) == sock)); + + if (sock->home && sock->home->listeners) (void) rbtree_deletebydata(sock->home->listeners, this); + +#ifdef HAVE_PTHREAD_H + pthread_mutex_destroy(&(sock->mutex)); +#endif + + } +#endif /* WITH_TLS */ + } +#endif /* WITH_TCP */ + + return 0; +} + + +/* + * Allocate & initialize a new listener. + */ +static rad_listen_t *listen_alloc(TALLOC_CTX *ctx, RAD_LISTEN_TYPE type) +{ + rad_listen_t *this; + + this = talloc_zero(ctx, rad_listen_t); + + this->type = type; + this->recv = master_listen[this->type].recv; + this->send = master_listen[this->type].send; + this->print = master_listen[this->type].print; + + if (type != RAD_LISTEN_PROXY) { + this->encode = master_listen[this->type].encode; + this->decode = master_listen[this->type].decode; + } else { + this->send = NULL; /* proxy packets shouldn't call this! */ + this->proxy_send = master_listen[this->type].send; + this->proxy_encode = master_listen[this->type].encode; + this->proxy_decode = master_listen[this->type].decode; + } + + talloc_set_destructor(this, _listener_free); + + this->data = talloc_zero_array(this, uint8_t, master_listen[this->type].inst_size); + + return this; +} + +#ifdef WITH_PROXY + +/* + * Externally visible function for creating a new proxy LISTENER. + * + * Not thread-safe, but all calls to it are protected by the + * proxy mutex in event.c + */ +rad_listen_t *proxy_new_listener(TALLOC_CTX *ctx, home_server_t *home, uint16_t src_port) +{ + time_t now; + rad_listen_t *this; + listen_socket_t *sock; + char buffer[256]; + + if (!home) return NULL; + + rad_assert(home->virtual_server == NULL); /* we only open real sockets */ + + if ((home->limit.max_connections > 0) && + (home->limit.num_connections >= home->limit.max_connections)) { + RATE_LIMIT(INFO("Home server %s has too many open connections (%d)", + home->log_name, home->limit.max_connections)); + return NULL; + } + + now = time(NULL); + if (home->last_failed_open == now) { + WARN("Suppressing attempt to open socket to 'down' home server"); + return NULL; + } + + this = listen_alloc(ctx, RAD_LISTEN_PROXY); + + sock = this->data; + sock->other_ipaddr = home->ipaddr; + sock->other_port = home->port; + sock->home = home; + + sock->my_ipaddr = home->src_ipaddr; + sock->my_port = src_port; + sock->proto = home->proto; + + /* + * For error messages. + */ + this->print(this, buffer, sizeof(buffer)); + +#ifdef WITH_TCP + sock->opened = sock->last_packet = now; + + if (home->proto == IPPROTO_TCP) { + this->recv = proxy_socket_tcp_recv; + + /* + * FIXME: connect() is blocking! + * We do this with the proxy mutex locked, which may + * cause large delays! + * + * http://www.developerweb.net/forum/showthread.php?p=13486 + */ + this->fd = fr_socket_client_tcp(&home->src_ipaddr, + &home->ipaddr, home->port, false); + + /* + * Set max_requests, lifetime, and idle_timeout from the home server. + */ + sock->limit = home->limit; + } else +#endif + this->fd = fr_socket(&home->src_ipaddr, src_port); + + if (this->fd < 0) { + this->print(this, buffer,sizeof(buffer)); + ERROR("Failed opening new proxy socket '%s' : %s", + buffer, fr_strerror()); + home->last_failed_open = now; + listen_free(&this); + return NULL; + } + + +#ifdef WITH_TCP +#ifdef WITH_TLS + if ((home->proto == IPPROTO_TCP) && home->tls) { + DEBUG("(TLS) Trying new outgoing proxy connection to %s", buffer); + + /* + * Set SNI, if configured. + * + * The OpenSSL API says the filename is "char + * const *", but some versions have it as "void + * *", without the "const". So we un-const it + * here through various C magic. + */ + if (home->tls->client_hostname) { + (void) SSL_set_tlsext_host_name(sock->ssn->ssl, (void *) (uintptr_t) home->tls->client_hostname); + } + +#ifdef WITH_RADIUSV11 + this->radiusv11 = home->tls->radiusv11; +#endif + + this->nonblock |= home->nonblock; + + /* + * Set non-blocking if it's configured. + */ + if (this->nonblock) { + if (fr_nonblock(this->fd) < 0) { + ERROR("(TLS) Failed setting nonblocking for proxy socket '%s' - %s", buffer, fr_strerror()); + goto error; + } + + rad_assert(home->listeners != NULL); + + if (!rbtree_insert(home->listeners, this)) { + ERROR("(TLS) Failed adding tracking informtion for proxy socket '%s'", buffer); + goto error; + } + +#ifdef TCP_NODELAY + /* + * Also set TCP_NODELAY, to force the data to be written quickly. + */ + if (sock->proto == IPPROTO_TCP) { + int on = 1; + + if (setsockopt(this->fd, SOL_TCP, TCP_NODELAY, &on, sizeof(on)) < 0) { + ERROR("(TLS) Failed to set TCP_NODELAY: %s", fr_syserror(errno)); + goto error; + } + } +#endif + } + + /* + * This is blocking. :( + */ + sock->ssn = tls_new_client_session(sock, home->tls, this->fd, &sock->certs); + if (!sock->ssn) { + ERROR("(TLS) Failed opening connection on proxy socket '%s'", buffer); + goto error; + } + +#ifdef WITH_RADIUSV11 + /* + * Must not have alpn_checked yet. This code only runs for blocking sockets. + */ + if (sock->ssn->connected && (fr_radiusv11_client_get_alpn(this) < 0)) { + goto error; + } +#endif + + sock->connect_timeout = home->connect_timeout; + + this->recv = proxy_tls_recv; + this->proxy_send = proxy_tls_send; + +#ifdef HAVE_PTHREAD_H + if (pthread_mutex_init(&sock->mutex, NULL) < 0) { + rad_assert(0 == 1); + listen_free(&this); + return 0; + } +#endif + + /* + * Make sure that this listener is associated with the home server. + * + * Since it's TCP+TLS, this socket can only be associated with one home server. + */ + +#ifdef WITH_COA_TUNNEL + if (home->recv_coa) { + RADCLIENT *client; + + this->send_coa = true; + + /* + * Don't set this->send_coa, as we are + * not sending CoA-Request packets to + * this home server. Instead, we are + * receiving CoA packets from this home + * server. + */ + this->send = proxy_tls_send_reply; + this->encode = master_listen[RAD_LISTEN_AUTH].encode; + this->decode = master_listen[RAD_LISTEN_AUTH].decode; + + /* + * Automatically create a client for this + * home server. There MAY be one already + * one for that IP in the configuration + * files, but there's no guarantee that + * it exists. + * + * The only real reason to use an + * existing client is to track various + * statistics. + */ + sock->client = client = talloc_zero(sock, RADCLIENT); + client->ipaddr = sock->other_ipaddr; + client->src_ipaddr = sock->my_ipaddr; + client->longname = client->shortname = talloc_typed_strdup(client, home->name); + client->secret = talloc_typed_strdup(client, home->secret); + client->nas_type = "none"; + client->server = talloc_typed_strdup(client, home->recv_coa_server); + } +#endif + } +#endif +#endif + /* + * Figure out which port we were bound to. + */ + if (sock->my_port == 0) { + struct sockaddr_storage src; + socklen_t sizeof_src = sizeof(src); + + memset(&src, 0, sizeof_src); + if (getsockname(this->fd, (struct sockaddr *) &src, + &sizeof_src) < 0) { + ERROR("Failed getting socket name for '%s': %s", + buffer, fr_syserror(errno)); + error: + close(this->fd); + home->last_failed_open = now; + listen_free(&this); + return NULL; + } + + if (!fr_sockaddr2ipaddr(&src, sizeof_src, + &sock->my_ipaddr, &sock->my_port)) { + ERROR("Socket has unsupported address family for '%s'", buffer); + goto error; + } + + this->print(this, buffer, sizeof(buffer)); + } + + if (rad_debug_lvl >= 3) { + DEBUG("Opened new proxy socket '%s'", buffer); + } + + home->limit.num_connections++; + + return this; +} +#endif + +static const FR_NAME_NUMBER listen_compare[] = { +#ifdef WITH_STATS + { "status", RAD_LISTEN_NONE }, +#endif + { "auth", RAD_LISTEN_AUTH }, +#ifdef WITH_COA_TUNNEL + { "auth+coa", RAD_LISTEN_AUTH }, +#endif +#ifdef WITH_ACCOUNTING + { "acct", RAD_LISTEN_ACCT }, + { "auth+acct", RAD_LISTEN_AUTH }, +#ifdef WITH_COA_TUNNEL + { "auth+acct+coa", RAD_LISTEN_AUTH }, +#endif +#endif +#ifdef WITH_DETAIL + { "detail", RAD_LISTEN_DETAIL }, +#endif +#ifdef WITH_PROXY + { "proxy", RAD_LISTEN_PROXY }, +#endif +#ifdef WITH_VMPS + { "vmps", RAD_LISTEN_VQP }, +#endif +#ifdef WITH_DHCP + { "dhcp", RAD_LISTEN_DHCP }, +#endif +#ifdef WITH_COMMAND_SOCKET + { "control", RAD_LISTEN_COMMAND }, +#endif +#ifdef WITH_COA + { "coa", RAD_LISTEN_COA }, +#endif + { NULL, 0 }, +}; + +static int _free_proto_handle(fr_dlhandle *handle) +{ + dlclose(*handle); + return 0; +} + +static rad_listen_t *listen_parse(CONF_SECTION *cs, char const *server) +{ + int type, rcode; + char const *listen_type; + rad_listen_t *this; + CONF_PAIR *cp; + char const *value; + fr_dlhandle handle; + CONF_SECTION *server_cs; + char const *p; + char buffer[32]; + + cp = cf_pair_find(cs, "type"); + if (!cp) { + cf_log_err_cs(cs, + "No type specified in listen section"); + return NULL; + } + + value = cf_pair_value(cp); + if (!value) { + cf_log_err_cp(cp, + "Type cannot be empty"); + return NULL; + } + + snprintf(buffer, sizeof(buffer), "proto_%s", value); + handle = fr_dlopenext(buffer); + if (handle) { + fr_protocol_t *proto; + fr_dlhandle *marker; + + proto = dlsym(handle, buffer); + if (!proto) { +#if 0 + cf_log_err_cs(cs, + "Failed linking to protocol %s : %s\n", + value, dlerror()); +#endif + dlclose(handle); + return NULL; + } + + type = fr_str2int(listen_compare, value, -1); + rad_assert(type >= 0); /* shouldn't be able to compile an invalid type */ + + memcpy(&master_listen[type], proto, sizeof(*proto)); + + /* + * Ensure handle gets closed if config section gets freed + */ + marker = talloc(cs, fr_dlhandle); + *marker = handle; + talloc_set_destructor(marker, _free_proto_handle); + + if (master_listen[type].magic != RLM_MODULE_INIT) { + ERROR("Failed to load protocol '%s', it has the wrong version.", + master_listen[type].name); + return NULL; + } + } + + cf_log_info(cs, "listen {"); + + listen_type = NULL; + rcode = cf_item_parse(cs, "type", FR_ITEM_POINTER(PW_TYPE_STRING, &listen_type), ""); + if (rcode < 0) return NULL; + if (rcode == 1) { + cf_log_err_cs(cs, + "No type specified in listen section"); + return NULL; + } + + type = fr_str2int(listen_compare, listen_type, -1); + if (type < 0) { + cf_log_err_cs(cs, + "Invalid type \"%s\" in listen section.", + listen_type); + return NULL; + } + + /* + * DHCP and VMPS *must* be loaded dynamically. + */ + if (master_listen[type].magic != RLM_MODULE_INIT) { + ERROR("Cannot load protocol '%s', as the required library does not exist", + master_listen[type].name); + return NULL; + } + + /* + * Allow listen sections in the default config to + * refer to a server. + */ + if (!server) { + rcode = cf_item_parse(cs, "virtual_server", FR_ITEM_POINTER(PW_TYPE_STRING, &server), NULL); + if (rcode < 0) return NULL; + } + +#ifdef WITH_PROXY + /* + * We were passed a virtual server, so the caller is + * defining a proxy listener inside of a virtual server. + * This isn't allowed right now. + */ + else if (type == RAD_LISTEN_PROXY) { + ERROR("Error: listen type \"proxy\" Cannot appear in a virtual server section"); + return NULL; + } +#endif + + /* + * Set up cross-type data. + */ + this = listen_alloc(cs, type); + this->server = server; + this->fd = -1; + +#ifdef WITH_TCP + /* + * Add special flags '+' for "auth+acct". + */ + p = strchr(listen_type, '+'); + if (p) { + if (strncmp(p + 1, "acct", 4) == 0) { + this->dual = true; +#ifdef WITH_COA_TUNNEL + p += 5; + } + + if (strcmp(p, "+coa") == 0) { + this->send_coa = true; +#endif + } + } +#endif + + /* + * Call per-type parser. + */ + if (master_listen[type].parse(cs, this) < 0) { + listen_free(&this); + return NULL; + } + + server_cs = cf_section_sub_find_name2(main_config.config, "server", + this->server); + if (!server_cs && this->server) { + cf_log_err_cs(cs, "No such server \"%s\"", this->server); + listen_free(&this); + return NULL; + } + +#ifdef WITH_COA_TUNNEL + if (this->send_coa) { + CONF_SECTION *coa; + + if (!this->tls) { + cf_log_err_cs(cs, "TLS is required in order to use \"+coa\""); + listen_free(&this); + return NULL; + } + + /* + * Parse the configuration if it exists. + */ + coa = cf_section_sub_find(cs, "coa"); + if (coa) { + rcode = cf_section_parse(cs, this, coa_config); + if (rcode < 0) { + listen_free(&this); + return NULL; + } + } + + /* + * Use the same boundary checks as for home + * server. See realm_home_server_sanitize(). + */ + FR_INTEGER_BOUND_CHECK("coa_irt", this->coa_irt, >=, 1); + FR_INTEGER_BOUND_CHECK("coa_irt", this->coa_irt, <=, 5); + + FR_INTEGER_BOUND_CHECK("coa_mrc", this->coa_mrc, <=, 20); + + FR_INTEGER_BOUND_CHECK("coa_mrt", this->coa_mrt, <=, 30); + + FR_INTEGER_BOUND_CHECK("coa_mrd", this->coa_mrd, >=, 5); + FR_INTEGER_BOUND_CHECK("coa_mrd", this->coa_mrd, <=, 60); + } +#endif /* WITH_COA_TUNNEL */ + + cf_log_info(cs, "}"); + + return this; +} + +#ifdef HAVE_PTHREAD_H +/* + * A child thread which does NOTHING other than read and process + * packets. + */ +static void *recv_thread(void *arg) +{ + rad_listen_t *this = arg; + + while (1) { + this->recv(this); + } + + return NULL; +} +#endif + + +/* + * Generate a list of listeners. Takes an input list of + * listeners, too, so we don't close sockets with waiting packets. + */ +int listen_init(CONF_SECTION *config, rad_listen_t **head, bool spawn_flag) +{ + bool override = false; + CONF_SECTION *cs = NULL; + rad_listen_t **last; + rad_listen_t *this; + fr_ipaddr_t server_ipaddr; + uint16_t auth_port = 0; + + /* + * We shouldn't be called with a pre-existing list. + */ + rad_assert(head && (*head == NULL)); + + memset(&server_ipaddr, 0, sizeof(server_ipaddr)); + + last = head; + server_ipaddr.af = AF_UNSPEC; + + /* + * If the port is specified on the command-line, + * it over-rides the configuration file. + * + * FIXME: If argv[0] == "vmpsd", then don't listen on auth/acct! + */ + if (main_config.port > 0) { + auth_port = main_config.port; + + /* + * -p X but no -i Y on the command-line. + */ + if (main_config.myip.af == AF_UNSPEC) { + ERROR("The command-line says \"-p %d\", but there is no associated IP address to use", + main_config.port); + return -1; + } + } + + /* + * If the IP address was configured on the command-line, + * use that as the "bind_address" + */ + if (main_config.myip.af != AF_UNSPEC) { + listen_socket_t *sock; + + memcpy(&server_ipaddr, &main_config.myip, + sizeof(server_ipaddr)); + override = true; + +#ifdef WITH_VMPS + if (strcmp(main_config.name, "vmpsd") == 0) { + this = listen_alloc(config, RAD_LISTEN_VQP); + if (!auth_port) auth_port = 1589; + } else +#endif + this = listen_alloc(config, RAD_LISTEN_AUTH); + + sock = this->data; + + sock->my_ipaddr = server_ipaddr; + sock->my_port = auth_port; + + sock->clients = client_list_parse_section(config, false); + if (!sock->clients) { + cf_log_err_cs(config, + "Failed to find any clients for this listen section"); + listen_free(&this); + return -1; + } + + if (listen_bind(this) < 0) { + listen_free(head); + ERROR("There appears to be another RADIUS server running on the authentication port %d", sock->my_port); + listen_free(&this); + return -1; + } + auth_port = sock->my_port; /* may have been updated in listen_bind */ + if (override) { + cs = cf_section_sub_find_name2(config, "server", + main_config.name); + if (cs) this->server = main_config.name; + } + + *last = this; + last = &(this->next); + +#ifdef WITH_VMPS + /* + * No acct for vmpsd + */ + if (strcmp(main_config.name, "vmpsd") == 0) goto add_sockets; +#endif + +#ifdef WITH_ACCOUNTING + /* + * Open Accounting Socket. + * + * If we haven't already gotten acct_port from + * /etc/services, then make it auth_port + 1. + */ + this = listen_alloc(config, RAD_LISTEN_ACCT); + sock = this->data; + + /* + * Create the accounting socket. + * + * The accounting port is always the + * authentication port + 1 + */ + sock->my_ipaddr = server_ipaddr; + sock->my_port = auth_port + 1; + + sock->clients = client_list_parse_section(config, false); + if (!sock->clients) { + cf_log_err_cs(config, + "Failed to find any clients for this listen section"); + return -1; + } + + if (listen_bind(this) < 0) { + listen_free(&this); + listen_free(head); + ERROR("There appears to be another RADIUS server running on the accounting port %d", sock->my_port); + return -1; + } + + if (override) { + cs = cf_section_sub_find_name2(config, "server", + main_config.name); + if (cs) this->server = main_config.name; + } + + *last = this; + last = &(this->next); +#endif + } + + /* + * They specified an IP on the command-line, ignore + * all listen sections except the one in '-n'. + */ + if (main_config.myip.af != AF_UNSPEC) { + CONF_SECTION *subcs; + char const *name2 = cf_section_name2(cs); + + cs = cf_section_sub_find_name2(config, "server", + main_config.name); + if (!cs) goto add_sockets; + + /* + * Should really abstract this code... + */ + for (subcs = cf_subsection_find_next(cs, NULL, "listen"); + subcs != NULL; + subcs = cf_subsection_find_next(cs, subcs, "listen")) { + this = listen_parse(subcs, name2); + if (!this) { + listen_free(head); + return -1; + } + + *last = this; + last = &(this->next); + } /* loop over "listen" directives in server <foo> */ + + goto add_sockets; + } + + /* + * Walk through the "listen" sections, if they exist. + */ + for (cs = cf_subsection_find_next(config, NULL, "listen"); + cs != NULL; + cs = cf_subsection_find_next(config, cs, "listen")) { + this = listen_parse(cs, NULL); + if (!this) { + listen_free(head); + return -1; + } + + *last = this; + last = &(this->next); + } + + /* + * Check virtual servers for "listen" sections, too. + * + * FIXME: Move to virtual server init? + */ + for (cs = cf_subsection_find_next(config, NULL, "server"); + cs != NULL; + cs = cf_subsection_find_next(config, cs, "server")) { + CONF_SECTION *subcs; + char const *name2 = cf_section_name2(cs); + + for (subcs = cf_subsection_find_next(cs, NULL, "listen"); + subcs != NULL; + subcs = cf_subsection_find_next(cs, subcs, "listen")) { + this = listen_parse(subcs, name2); + if (!this) { + listen_free(head); + return -1; + } + + *last = this; + last = &(this->next); + } /* loop over "listen" directives in virtual servers */ + } /* loop over virtual servers */ + +add_sockets: + /* + * No sockets to receive packets, this is an error. + * proxying is pointless. + */ + if (!*head) { + ERROR("The server is not configured to listen on any ports. Cannot start"); + return -1; + } + + /* + * Print out which sockets we're listening on, and + * add them to the event list. + */ + for (this = *head; this != NULL; this = this->next) { +#ifdef WITH_TLS + if (!check_config && !spawn_flag && this->tls) { + cf_log_err_cs(this->cs, "Threading must be enabled for TLS sockets to function properly"); + cf_log_err_cs(this->cs, "You probably need to do '%s -fxx -l stdout' for debugging", + main_config.name); + return -1; + } +#endif + if (!check_config) { + if (this->workers && !spawn_flag) { + WARN("Setting 'workers' requires 'synchronous'. Disabling 'workers'"); + this->workers = 0; + } + + if (this->workers) { +#ifdef HAVE_PTHREAD_H + int rcode; + uint32_t i; + char buffer[256]; + + this->print(this, buffer, sizeof(buffer)); + + for (i = 0; i < this->workers; i++) { + pthread_t id; + + /* + * FIXME: create detached? + */ + rcode = pthread_create(&id, 0, recv_thread, this); + if (rcode != 0) { + ERROR("Thread create failed: %s", + fr_syserror(rcode)); + fr_exit(1); + } + + DEBUG("Thread %d for %s\n", i, buffer); + } +#else + WARN("Setting 'workers' requires 'synchronous'. Disabling 'workers'"); + this->workers = 0; +#endif + + } else { + radius_update_listener(this); + } + + } + } + + /* + * Haven't defined any sockets. Die. + */ + if (!*head) return -1; + +#ifdef WITH_COA_TUNNEL + if (listen_coa_init() < 0) return -1; +#endif + + return 0; +} + +/* + * Free a linked list of listeners; + */ +void listen_free(rad_listen_t **head) +{ + rad_listen_t *this; + + if (!head || !*head) return; + + this = *head; + while (this) { + rad_listen_t *next = this->next; + talloc_free(this); + this = next; + } + + *head = NULL; +} + +#ifdef WITH_STATS +RADCLIENT_LIST *listener_find_client_list(fr_ipaddr_t const *ipaddr, uint16_t port, int proto) +{ + rad_listen_t *this; + + for (this = main_config.listen; this != NULL; this = this->next) { + listen_socket_t *sock; + + if ((this->type != RAD_LISTEN_AUTH) +#ifdef WITH_ACCOUNTING + && (this->type != RAD_LISTEN_ACCT) +#endif +#ifdef WITH_COA + && (this->type != RAD_LISTEN_COA) +#endif + ) continue; + + sock = this->data; + + if (sock->my_port != port) continue; + if (sock->proto != proto) continue; + if (fr_ipaddr_cmp(ipaddr, &sock->my_ipaddr) != 0) continue; + + return sock->clients; + } + + return NULL; +} +#endif + +rad_listen_t *listener_find_byipaddr(fr_ipaddr_t const *ipaddr, uint16_t port, int proto) +{ + rad_listen_t *this; + + for (this = main_config.listen; this != NULL; this = this->next) { + listen_socket_t *sock; + + sock = this->data; + + if (sock->my_port != port) continue; + if (sock->proto != proto) continue; + if (fr_ipaddr_cmp(ipaddr, &sock->my_ipaddr) != 0) continue; + + return this; + } + + /* + * Failed to find a specific one. Find INADDR_ANY + */ + for (this = main_config.listen; this != NULL; this = this->next) { + listen_socket_t *sock; + + sock = this->data; + + if (sock->my_port != port) continue; + if (sock->proto != proto) continue; + if (!fr_inaddr_any(&sock->my_ipaddr)) continue; + + return this; + } + + return NULL; +} + +#ifdef WITH_COA_TUNNEL +/* + * This is easier than putting ifdef's everywhere. And + * realistically, there aren't many systems which have OpenSSL, + * but not pthreads. + */ +#ifndef HAVE_PTHREAD_H +#error CoA tunnels require pthreads +#endif + +#include <pthread.h> + +static rbtree_t *coa_tree = NULL; + +/* + * We have an RB tree of keys, and within each key, a hash table + * of one or more listeners associated with that key. + */ +typedef struct { + char const *key; + fr_hash_table_t *ht; + + pthread_mutex_t mutex; /* per key, to lower contention */ +} coa_key_t; + +typedef struct { + coa_key_t *coa_key; + rad_listen_t *listener; +} coa_entry_t; + +static int coa_key_cmp(void const *one, void const *two) +{ + coa_key_t const *a = one; + coa_key_t const *b = two; + + return strcmp(a->key, b->key); +} + +static void coa_key_free(void *data) +{ + coa_key_t *coa_key = data; + + pthread_mutex_destroy(&coa_key->mutex); + fr_hash_table_free(coa_key->ht); + talloc_free(coa_key); +} + +static uint32_t coa_entry_hash(void const *data) +{ + coa_entry_t const *a = (coa_entry_t const *) data; + + return fr_hash(&a->listener, sizeof(a->listener)); +} + +static int coa_entry_cmp(void const *one, void const *two) +{ + coa_entry_t const *a = one; + coa_entry_t const *b = two; + + return memcmp(&a->listener, &b->listener, sizeof(a->listener)); +} + +/* + * Delete the entry, without holding the parents lock. + */ +static void coa_entry_free(void *data) +{ + talloc_free(data); +} + +static int coa_entry_destructor(coa_entry_t *entry) +{ + pthread_mutex_lock(&entry->coa_key->mutex); + fr_hash_table_delete(entry->coa_key->ht, entry); + pthread_mutex_unlock(&entry->coa_key->mutex); + + return 0; +} + +static int listen_coa_init(void) +{ + /* + * We will be looking up listeners by key. Each key + * points us to a list of listeners. Each key has it's + * own mutex, so that it's thread-safe. + */ + coa_tree = rbtree_create(NULL, coa_key_cmp, coa_key_free, RBTREE_FLAG_LOCK); + if (!coa_tree) { + ERROR("Failed creating internal tracking tree for Originating-Realm-Key"); + return -1; + } + + return 0; +} + +void listen_coa_free(void) +{ + /* + * If we are freeing the tree, then all of the listeners + * must have been freed first. + */ + rad_assert(rbtree_num_elements(coa_tree) == 0); + rbtree_free(coa_tree); + coa_tree = NULL; +} + +/* + * Adds a listener to the hash of listeners, based on key. + */ +void listen_coa_add(rad_listen_t *this, char const *key) +{ + int tries = 0; + coa_key_t my_key, *coa_key; + coa_entry_t *entry; + + rad_assert(this->send_coa); + rad_assert(this->parent); + rad_assert(!this->key); + + /* + * Find the key. If we can't find it, then create it. + */ + my_key.key = key; + +retry: + coa_key = rbtree_finddata(coa_tree, &my_key); + if (!coa_key) { + coa_key = talloc_zero(NULL, coa_key_t); + if (!coa_key) return; + coa_key->key = talloc_strdup(coa_key, key); + if (!coa_key->key) { + fail: + talloc_free(coa_key); + return; + } + + /* + * Create the hash table of listeners. + */ + coa_key->ht = fr_hash_table_create(coa_entry_hash, coa_entry_cmp, coa_entry_free); + if (!coa_key->ht) goto fail; + + if (!rbtree_insert(coa_tree, coa_key)) { + talloc_free(coa_key); + + /* + * The lookups are mutex protected, but + * if there's time between the lookup and + * the insert, another thread may have + * created the node. In which case we + * try again. + */ + if (tries < 3) goto retry; + tries++; + return; + } + + (void) pthread_mutex_init(&coa_key->mutex, NULL); + } + + /* + * No need to strdup() this, coa_key will only be removed + * after the listener has been removed. + */ + if (!this->key) this->key = coa_key->key; + + entry = talloc_zero(this, coa_entry_t); + if (!entry) return; + talloc_set_destructor(entry, coa_entry_destructor); + + entry->coa_key = coa_key; + entry->listener = this; + + /* + * Insert the entry into the hash table. + */ + pthread_mutex_lock(&coa_key->mutex); + fr_hash_table_insert(coa_key->ht, entry); + pthread_mutex_unlock(&coa_key->mutex); +} + +/* + * Find an active listener by key. + * + * This function will update request->home_server, and + * request->proxy_listener. + */ +int listen_coa_find(REQUEST *request, char const *key) +{ + coa_key_t my_key, *coa_key; + rad_listen_t *this, *found; + listen_socket_t *sock; + fr_hash_iter_t iter; + + /* + * Find the key. If we can't find it, then error out. + */ + memcpy(&my_key.key, &key, sizeof(key)); /* const issues */ + coa_key = rbtree_finddata(coa_tree, &my_key); + if (!coa_key) return -1; + + /* + * We've found it. Now find a listener which has free + * IDs. i.e. where the number of used IDs is less tahn + * 256. + */ + found = NULL; + pthread_mutex_lock(&coa_key->mutex); + for (this = fr_hash_table_iter_init(coa_key->ht, &iter); + this != NULL; + this = fr_hash_table_iter_next(coa_key->ht, &iter)) { + if (this->blocked) continue; + + if (this->dead) continue; + + if (!found) { + if (this->num_ids_used < 256) { + found = this; + } + + /* + * Skip listeners which have all used IDs. + */ + continue; + } + + /* + * Try to spread the load across all available + * sockets. + */ + if (found->num_ids_used > this->num_ids_used) { + found = this; + continue; + } + + /* + * If they are equal, pick one at random. + * + * @todo - pick one with equal probability from + * among the ones with the same IDs used. This + * algorithm prefers the first one. + */ + if (found->num_ids_used == this->num_ids_used) { + if ((fr_rand() & 0x01) == 0) { + found = this; + continue; + } + } + } + + pthread_mutex_unlock(&coa_key->mutex); + if (!found) return -1; + + request->proxy_listener = found; + + sock = found->data; + request->home_server = sock->home; + return 0; +} + +/* + * Check for an active listener by key. + */ +static bool listen_coa_exists(rad_listen_t *this, char const *key) +{ + coa_key_t my_key, *coa_key; + coa_entry_t my_entry, *entry; + + /* + * Find the key. If we can't find it, then error out. + */ + memcpy(&my_key.key, &key, sizeof(key)); /* const issues */ + coa_key = rbtree_finddata(coa_tree, &my_key); + if (!coa_key) return false; + + my_entry.listener = this; + pthread_mutex_lock(&coa_key->mutex); + entry = fr_hash_table_finddata(coa_key->ht, &my_entry); + pthread_mutex_unlock(&coa_key->mutex); + + return (entry != NULL); +} + +/* + * Delete a listener entry. + */ +static void listen_coa_delete(rad_listen_t *this, char const *key) +{ + coa_key_t my_key, *coa_key; + coa_entry_t my_entry; + + /* + * Find the key. If we can't find it, then error out. + */ + memcpy(&my_key.key, &key, sizeof(key)); /* const issues */ + coa_key = rbtree_finddata(coa_tree, &my_key); + if (!coa_key) return; + + my_entry.listener = this; + pthread_mutex_lock(&coa_key->mutex); + (void) fr_hash_table_delete(coa_key->ht, &my_entry); + pthread_mutex_unlock(&coa_key->mutex); +} + + +static void listener_coa_update(rad_listen_t *this, VALUE_PAIR *vps) +{ + VALUE_PAIR *vp; + vp_cursor_t cursor; + + fr_cursor_init(&cursor, &vps); + + /* + * Add or delete Operator-Name realms + */ + while ((vp = fr_cursor_next_by_num(&cursor, PW_OPERATOR_NAME, 0, TAG_ANY)) != NULL) { + if (vp->vp_length <= 1) continue; + + if (vp->vp_strvalue[0] == '+') { + if (listen_coa_exists(this, vp->vp_strvalue)) continue; + + listen_coa_add(this, vp->vp_strvalue); + continue; + } + + if (vp->vp_strvalue[0] == '-') { + listen_coa_delete(this, vp->vp_strvalue); + continue; + } + } +} +#endif diff --git a/src/main/log.c b/src/main/log.c new file mode 100644 index 0000000..1ca2f91 --- /dev/null +++ b/src/main/log.c @@ -0,0 +1,923 @@ +/* + * This program 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$ + * + * @brief Logging functions used by the server core. + * @file main/log.c + * + * @copyright 2000,2006 The FreeRADIUS server project + * @copyright 2000 Miquel van Smoorenburg <miquels@cistron.nl> + * @copyright 2000 Alan DeKok <aland@ox.org> + * @copyright 2001 Chad Miller <cmiller@surfsouth.com> + */ +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/rad_assert.h> + +#ifdef HAVE_SYS_STAT_H +# include <sys/stat.h> +#endif + +#include <fcntl.h> + +#ifdef HAVE_SYSLOG_H +# include <syslog.h> +#endif + +#include <sys/file.h> + +#ifdef HAVE_PTHREAD_H +#include <pthread.h> +#endif + +log_lvl_t rad_debug_lvl = 0; //!< Global debugging level +static bool rate_limit = true; //!< Whether repeated log entries should be rate limited + +/** Maps log categories to message prefixes + */ +static const FR_NAME_NUMBER levels[] = { + { ": Debug: ", L_DBG }, + { ": Auth: ", L_AUTH }, + { ": Proxy: ", L_PROXY }, + { ": Info: ", L_INFO }, + { ": Warning: ", L_WARN }, + { ": Acct: ", L_ACCT }, + { ": Error: ", L_ERR }, + { ": WARNING: ", L_DBG_WARN }, + { ": ERROR: ", L_DBG_ERR }, + { ": WARNING: ", L_DBG_WARN_REQ }, + { ": ERROR: ", L_DBG_ERR_REQ }, + { NULL, 0 } +}; + +/** @name VT100 escape sequences + * + * These sequences may be written to VT100 terminals to change the + * colour and style of the text. + * + @code{.c} + fprintf(stdout, VTC_RED "This text will be coloured red" VTC_RESET); + @endcode + * @{ + */ +#define VTC_RED "\x1b[31m" //!< Colour following text red. +#define VTC_YELLOW "\x1b[33m" //!< Colour following text yellow. +#define VTC_BOLD "\x1b[1m" //!< Embolden following text. +#define VTC_RESET "\x1b[0m" //!< Reset terminal text to default style/colour. +/** @} */ + +/** Maps log categories to VT100 style/colour escape sequences + */ +static const FR_NAME_NUMBER colours[] = { + { "", L_DBG }, + { VTC_BOLD, L_AUTH }, + { VTC_BOLD, L_PROXY }, + { VTC_BOLD, L_INFO }, + { VTC_BOLD, L_ACCT }, + { VTC_RED, L_ERR }, + { VTC_BOLD VTC_YELLOW, L_WARN }, + { VTC_BOLD VTC_RED, L_DBG_ERR }, + { VTC_BOLD VTC_YELLOW, L_DBG_WARN }, + { VTC_BOLD VTC_RED, L_DBG_ERR_REQ }, + { VTC_BOLD VTC_YELLOW, L_DBG_WARN_REQ }, + { NULL, 0 } +}; + +/** Syslog facility table + * + * Maps syslog facility keywords, to the syslog facility macros defined + * in the system's syslog.h. + * + * @note Not all facilities are supported by every operating system. + * If a facility is unavailable it will not appear in the table. + */ +const FR_NAME_NUMBER syslog_facility_table[] = { +#ifdef LOG_KERN + { "kern", LOG_KERN }, +#endif +#ifdef LOG_USER + { "user", LOG_USER }, +#endif +#ifdef LOG_MAIL + { "mail", LOG_MAIL }, +#endif +#ifdef LOG_DAEMON + { "daemon", LOG_DAEMON }, +#endif +#ifdef LOG_AUTH + { "auth", LOG_AUTH }, +#endif +#ifdef LOG_LPR + { "lpr", LOG_LPR }, +#endif +#ifdef LOG_NEWS + { "news", LOG_NEWS }, +#endif +#ifdef LOG_UUCP + { "uucp", LOG_UUCP }, +#endif +#ifdef LOG_CRON + { "cron", LOG_CRON }, +#endif +#ifdef LOG_AUTHPRIV + { "authpriv", LOG_AUTHPRIV }, +#endif +#ifdef LOG_FTP + { "ftp", LOG_FTP }, +#endif +#ifdef LOG_LOCAL0 + { "local0", LOG_LOCAL0 }, +#endif +#ifdef LOG_LOCAL1 + { "local1", LOG_LOCAL1 }, +#endif +#ifdef LOG_LOCAL2 + { "local2", LOG_LOCAL2 }, +#endif +#ifdef LOG_LOCAL3 + { "local3", LOG_LOCAL3 }, +#endif +#ifdef LOG_LOCAL4 + { "local4", LOG_LOCAL4 }, +#endif +#ifdef LOG_LOCAL5 + { "local5", LOG_LOCAL5 }, +#endif +#ifdef LOG_LOCAL6 + { "local6", LOG_LOCAL6 }, +#endif +#ifdef LOG_LOCAL7 + { "local7", LOG_LOCAL7 }, +#endif + { NULL, -1 } +}; + +/** Syslog severity table + * + * Maps syslog severity keywords, to the syslog severity macros defined + * in the system's syslog.h file. + * + */ +const FR_NAME_NUMBER syslog_severity_table[] = { +#ifdef LOG_EMERG + { "emergency", LOG_EMERG }, +#endif +#ifdef LOG_ALERT + { "alert", LOG_ALERT }, +#endif +#ifdef LOG_CRIT + { "critical", LOG_CRIT }, +#endif +#ifdef LOG_ERR + { "error", LOG_ERR }, +#endif +#ifdef LOG_WARNING + { "warning", LOG_WARNING }, +#endif +#ifdef LOG_NOTICE + { "notice", LOG_NOTICE }, +#endif +#ifdef LOG_INFO + { "info", LOG_INFO }, +#endif +#ifdef LOG_DEBUG + { "debug", LOG_DEBUG }, +#endif + { NULL, -1 } +}; + +const FR_NAME_NUMBER log_str2dst[] = { + { "null", L_DST_NULL }, + { "files", L_DST_FILES }, + { "syslog", L_DST_SYSLOG }, + { "stdout", L_DST_STDOUT }, + { "stderr", L_DST_STDERR }, + { NULL, L_DST_NUM_DEST } +}; + +bool log_dates_utc = false; + +fr_log_t default_log = { + .colourise = false, //!< Will be set later. Should be off before we do terminal detection. + .fd = STDOUT_FILENO, + .dst = L_DST_STDOUT, + .file = NULL, + .debug_file = NULL, +}; + +static int stderr_fd = -1; //!< The original unmolested stderr file descriptor +static int stdout_fd = -1; //!< The original unmolested stdout file descriptor + +static char const spaces[] = " "; + +/** On fault, reset STDOUT and STDERR to something useful + * + * @return 0 + */ +static int _restore_std(UNUSED int sig) +{ + if ((stderr_fd > 0) && (stdout_fd > 0)) { + dup2(stderr_fd, STDOUT_FILENO); + dup2(stdout_fd, STDERR_FILENO); + return 0; + } + + if (default_log.fd > 0) { + dup2(default_log.fd, STDOUT_FILENO); + dup2(default_log.fd, STDERR_FILENO); + return 0; + } + + return 0; +} + +/** Initialise file descriptors based on logging destination + * + * @param log Logger to manipulate. + * @param daemonize Whether the server is starting as a daemon. + * @return 0 on success -1 on failure. + */ +int radlog_init(fr_log_t *log, bool daemonize) +{ + int devnull; + + rate_limit = daemonize; + + /* + * If we're running in foreground mode, save STDIN / + * STDERR as higher FDs, which won't get used by anyone + * else. When we fork/exec a program, it's STD FDs will + * get set to pipes. We later set STDOUT / STDERR to + * /dev/null, so that any library trying to write to them + * doesn't screw anything up. + * + * Then, when something goes wrong, restore them so that + * any debugger called from the panic action has access + * to STDOUT / STDERR. + */ + if (!daemonize) { + fr_fault_set_cb(_restore_std); + + stdout_fd = dup(STDOUT_FILENO); + stderr_fd = dup(STDERR_FILENO); + } + + devnull = open("/dev/null", O_RDWR); + if (devnull < 0) { + fr_strerror_printf("Error opening /dev/null: %s", fr_syserror(errno)); + return -1; + } + + /* + * STDOUT & STDERR go to /dev/null, unless we have "-x", + * then STDOUT & STDERR go to the "-l log" destination. + * + * The complexity here is because "-l log" can go to + * STDOUT or STDERR, too. + */ + if (log->dst == L_DST_STDOUT) { + setlinebuf(stdout); + log->fd = STDOUT_FILENO; + + /* + * If we're debugging, allow STDERR to go to + * STDOUT too, for executed programs, + */ + if (rad_debug_lvl) { + dup2(STDOUT_FILENO, STDERR_FILENO); + } else { + dup2(devnull, STDERR_FILENO); + } + + } else if (log->dst == L_DST_STDERR) { + setlinebuf(stderr); + log->fd = STDERR_FILENO; + + /* + * If we're debugging, allow STDOUT to go to + * STDERR too, for executed programs, + */ + if (rad_debug_lvl) { + dup2(STDERR_FILENO, STDOUT_FILENO); + } else { + dup2(devnull, STDOUT_FILENO); + } + + } else if (log->dst == L_DST_SYSLOG) { + /* + * Discard STDOUT and STDERR no matter what the + * status of debugging. Syslog isn't a file + * descriptor, so we can't use it. + */ + dup2(devnull, STDOUT_FILENO); + dup2(devnull, STDERR_FILENO); + + } else if (rad_debug_lvl) { + /* + * If we're debugging, allow STDOUT and STDERR to + * go to the log file. + */ + dup2(log->fd, STDOUT_FILENO); + dup2(log->fd, STDERR_FILENO); + + } else { + /* + * Not debugging, and the log isn't STDOUT or + * STDERR. Ensure that we move both of them to + * /dev/null, so that the calling terminal can + * exit, and the output from executed programs + * doesn't pollute STDOUT / STDERR. + */ + dup2(devnull, STDOUT_FILENO); + dup2(devnull, STDERR_FILENO); + } + + close(devnull); + + fr_fault_set_log_fd(log->fd); + + return 0; +} + +/** Send a server log message to its destination + * + * @param type of log message. + * @param msg with printf style substitution tokens. + * @param ap Substitution arguments. + */ +int vradlog(log_type_t type, char const *msg, va_list ap) +{ + unsigned char *p; + char buffer[10240]; /* The largest config item size, then extra for prefixes and suffixes */ + char *unsan; + size_t len; + int colourise = default_log.colourise; + + /* + * If we don't want any messages, then + * throw them away. + */ + if (default_log.dst == L_DST_NULL) { + return 0; + } + + buffer[0] = '\0'; + len = 0; + + if (colourise) { + len += strlcpy(buffer + len, fr_int2str(colours, type, ""), sizeof(buffer) - len) ; + if (len == 0) { + colourise = false; + } + } + + /* + * Mark the point where we treat the buffer as unsanitized. + */ + unsan = buffer + len; + + /* + * Don't print timestamps to syslog, it does that for us. + * Don't print timestamps and error types for low levels + * of debugging. + * + * Print timestamps for non-debugging, and for high levels + * of debugging. + */ + if (default_log.dst != L_DST_SYSLOG) { + if ((rad_debug_lvl != 1) && (rad_debug_lvl != 2)) { + time_t timeval; + + timeval = time(NULL); + CTIME_R(&timeval, buffer + len, sizeof(buffer) - len - 1); + + len = strlen(buffer); + len += strlcpy(buffer + len, fr_int2str(levels, type, ": "), sizeof(buffer) - len); + } else goto add_prefix; + } else { + add_prefix: + if (len < sizeof(buffer)) switch (type) { + case L_DBG_WARN: + len += strlcpy(buffer + len, "WARNING: ", sizeof(buffer) - len); + break; + + case L_DBG_ERR: + len += strlcpy(buffer + len, "ERROR: ", sizeof(buffer) - len); + break; + + default: + break; + } + } + + if (len < sizeof(buffer)) { + vsnprintf(buffer + len, sizeof(buffer) - len - 1, msg, ap); + len += strlen(buffer + len); + } + + /* + * Filter out control chars and non UTF8 chars + */ + for (p = (unsigned char *)unsan; *p != '\0'; p++) { + int clen; + + switch (*p) { + case '\r': + case '\n': + *p = ' '; + break; + + case '\t': + continue; + + default: + clen = fr_utf8_char(p, -1); + if (!clen) { + *p = '?'; + continue; + } + p += (clen - 1); + break; + } + } + + if (colourise && (len < sizeof(buffer))) { + len += strlcpy(buffer + len, VTC_RESET, sizeof(buffer) - len); + } + + if (len < (sizeof(buffer) - 2)) { + buffer[len] = '\n'; + buffer[len + 1] = '\0'; + } else { + buffer[sizeof(buffer) - 2] = '\n'; + buffer[sizeof(buffer) - 1] = '\0'; + } + + switch (default_log.dst) { + +#ifdef HAVE_SYSLOG_H + case L_DST_SYSLOG: + switch (type) { + case L_DBG: + case L_DBG_WARN: + case L_DBG_ERR: + case L_DBG_ERR_REQ: + case L_DBG_WARN_REQ: + type = LOG_DEBUG; + break; + + case L_AUTH: + case L_PROXY: + case L_ACCT: + type = LOG_NOTICE; + break; + + case L_INFO: + type = LOG_INFO; + break; + + case L_WARN: + type = LOG_WARNING; + break; + + case L_ERR: + type = LOG_ERR; + break; + } + syslog(type, "%s", buffer); + break; +#endif + + case L_DST_FILES: + case L_DST_STDOUT: + case L_DST_STDERR: + return write(default_log.fd, buffer, strlen(buffer)); + + default: + case L_DST_NULL: /* should have been caught above */ + break; + } + + return 0; +} + +/** Send a server log message to its destination + * + * @param type of log message. + * @param msg with printf style substitution tokens. + * @param ... Substitution arguments. + */ +int radlog(log_type_t type, char const *msg, ...) +{ + va_list ap; + int r = 0; + + va_start(ap, msg); + + /* + * Non-debug message, or debugging is enabled. Log it. + */ + if (((type & L_DBG) == 0) || (rad_debug_lvl > 0)) { + r = vradlog(type, msg, ap); + } + va_end(ap); + + return r; +} + +/** Send a server log message to its destination without evaluating its debug level + * + * @param type of log message. + * @param msg with printf style substitution tokens. + * @param ... Substitution arguments. + */ +static int radlog_always(log_type_t type, char const *msg, ...) CC_HINT(format (printf, 2, 3)); +static int radlog_always(log_type_t type, char const *msg, ...) +{ + va_list ap; + int r; + + va_start(ap, msg); + r = vradlog(type, msg, ap); + va_end(ap); + + return r; +} + +/** Whether a server debug message should be logged + * + * @param type of message. + * @param lvl of debugging this message should be logged at. + * @return true if message should be logged, else false. + */ +inline bool debug_enabled(log_type_t type, log_lvl_t lvl) +{ + if ((type & L_DBG) && (lvl <= rad_debug_lvl)) return true; + + return false; +} + +/** Whether rate limiting is enabled + */ +bool rate_limit_enabled(void) +{ + if (rate_limit || (rad_debug_lvl < 1)) return true; + + return false; +} + +/** Whether a request specific debug message should be logged + * + * @param type of message. + * @param lvl of debugging this message should be logged at. + * @param request The current request. + * @return true if message should be logged, else false. + */ +inline bool radlog_debug_enabled(log_type_t type, log_lvl_t lvl, REQUEST *request) +{ + /* + * It's a debug class message, note this doesn't mean it's a debug type message. + * + * For example it could be a RIDEBUG message, which would be an informational message, + * instead of an RDEBUG message which would be a debug debug message. + * + * There is log function, but the request debug level isn't high enough. + * OR, we're in debug mode, and the global debug level isn't high enough, + * then don't log the message. + */ + if ((type & L_DBG) && + ((request->log.func && (lvl <= request->log.lvl)) || + ((rad_debug_lvl != 0) && (lvl <= rad_debug_lvl)))) { + return true; + } + + return false; +} + +/** Send a log message to its destination, possibly including fields from the request + * + * @param type of log message, #L_ERR, #L_WARN, #L_INFO, #L_DBG. + * @param lvl Minimum required server or request level to output this message. + * @param request The current request. + * @param msg with printf style substitution tokens. + * @param ap Substitution arguments. + */ +void vradlog_request(log_type_t type, log_lvl_t lvl, REQUEST *request, char const *msg, va_list ap) +{ + size_t len = 0; + char const *filename = default_log.file; + FILE *fp = NULL; + + char buffer[10240]; /* The largest config item size, then extra for prefixes and suffixes */ + + char *p; + char const *extra = ""; + uint8_t indent; + va_list aq; + + /* + * Debug messages get treated specially. + */ + if ((type & L_DBG) != 0) { + + if (!radlog_debug_enabled(type, lvl, request)) { + return; + } + + /* + * Use the debug output file, if specified, + * otherwise leave it as the default log file. + */ +#ifdef WITH_COMMAND_SOCKET + filename = default_log.debug_file; + if (!filename) +#endif + { + filename = default_log.file; + } + } + + if (filename) { + radlog_func_t rl = request->log.func; + + request->log.func = NULL; + + /* + * This is SLOW! Doing it for every log message + * in every request is NOT recommended! + */ + if (radius_xlat(buffer, sizeof(buffer), request, filename, rad_filename_escape, NULL) < 0) return; + request->log.func = rl; + + /* + * Ensure the directory structure exists, for + * where we're going to write the log file. + */ + p = strrchr(buffer, FR_DIR_SEP); + if (p) { + *p = '\0'; + if (rad_mkdir(buffer, S_IRWXU, -1, -1) < 0) { + ERROR("Failed creating %s: %s", buffer, fr_syserror(errno)); + return; + } + *p = FR_DIR_SEP; + } + + fp = fopen(buffer, "a"); + } + + /* + * If we don't copy the original ap we get a segfault from vasprintf. This is apparently + * due to ap sometimes being implemented with a stack offset which is invalidated if + * ap is passed into another function. See here: + * http://julipedia.meroh.net/2011/09/using-vacopy-to-safely-pass-ap.html + * + * I don't buy that explanation, but doing a va_copy here does prevent SEGVs seen when + * running unit tests which generate errors under CI. + */ + va_copy(aq, ap); + vsnprintf(buffer + len, sizeof(buffer) - len, msg, aq); + va_end(aq); + + /* + * Make sure the indent isn't set to something crazy + */ + indent = request->log.indent > sizeof(spaces) ? + sizeof(spaces) : + request->log.indent; + + /* + * Logging to a file descriptor + */ + if (fp) { + char time_buff[64]; /* The current timestamp */ + + time_t timeval; + timeval = time(NULL); + +#ifdef HAVE_GMTIME_R + if (log_dates_utc) { + struct tm utc; + gmtime_r(&timeval, &utc); + ASCTIME_R(&utc, time_buff, sizeof(time_buff)); + } else +#endif + { + CTIME_R(&timeval, time_buff, sizeof(time_buff)); + } + + /* + * Strip trailing new lines + */ + p = strrchr(time_buff, '\n'); + if (p) p[0] = '\0'; + + if (request->module && (request->module[0] != '\0')) { + fprintf(fp, "(%u) %s%s%s: %.*s%s\n", + request->number, time_buff, fr_int2str(levels, type, ""), + request->module, indent, spaces, buffer); + } else { + fprintf(fp, "(%u) %s%s%.*s%s\n", + request->number, time_buff, fr_int2str(levels, type, ""), + indent, spaces, buffer); + } + fclose(fp); + return; + } + + /* + * Logging everywhere else + */ + if (!DEBUG_ENABLED3) switch (type) { + case L_DBG_WARN: + extra = "WARNING: "; + type = L_DBG_WARN_REQ; + break; + + case L_DBG_ERR: + extra = "ERROR: "; + type = L_DBG_ERR_REQ; + break; + default: + break; + } + + if (request->module && (request->module[0] != '\0')) { + radlog_always(type, "(%u) %s: %.*s%s%s", request->number, + request->module, indent, spaces, extra, buffer); + } else { + radlog_always(type, "(%u) %.*s%s%s", request->number, + indent, spaces, extra, buffer); + } +} + +/** Martial variadic log arguments into a va_list and pass to normal logging functions + * + * @see radlog_request_error for more details. + * + * @param type the log category. + * @param lvl of debugging this message should be logged at. + * @param request The current request. + * @param msg with printf style substitution tokens. + * @param ... Substitution arguments. + */ +void radlog_request(log_type_t type, log_lvl_t lvl, REQUEST *request, char const *msg, ...) +{ + va_list ap; + + if (!request->log.func && !(type & L_DBG)) return; + + va_start(ap, msg); + if (request->log.func) request->log.func(type, lvl, request, msg, ap); + else if (!(type & L_DBG)) vradlog_request(type, lvl, request, msg, ap); + va_end(ap); +} + +/** Martial variadic log arguments into a va_list and pass to error logging functions + * + * This could all be done in a macro, but it turns out some implementations of the + * variadic macros do not work at all well if the va_list being written to is further + * up the stack (which is required as you still need a function to convert the elipses + * into a va_list). + * + * So, we use this small wrapper function instead, which will hopefully guarantee + * consistent behaviour. + * + * @param type the log category. + * @param lvl of debugging this message should be logged at. + * @param request The current request. + * @param msg with printf style substitution tokens. + * @param ... Substitution arguments. + */ +void radlog_request_error(log_type_t type, log_lvl_t lvl, REQUEST *request, char const *msg, ...) +{ + va_list ap; + + va_start(ap, msg); + if (request->log.func) request->log.func(type, lvl, request, msg, ap); + else if (!(type & L_DBG)) vradlog_request(type, lvl, request, msg, ap); + vmodule_failure_msg(request, msg, ap); + va_end(ap); +} + +/** Write the string being parsed, and a marker showing where the parse error occurred + * + * @param type the log category. + * @param lvl of debugging this message should be logged at. + * @param request The current request. + * @param msg string we were parsing. + * @param idx The position of the marker relative to the string. + * @param error What the parse error was. + */ +void radlog_request_marker(log_type_t type, log_lvl_t lvl, REQUEST *request, + char const *msg, size_t idx, char const *error) +{ + char const *prefix = ""; + uint8_t indent; + + if (idx >= sizeof(spaces)) { + size_t offset = (idx - (sizeof(spaces) - 1)) + (sizeof(spaces) * 0.75); + idx -= offset; + msg += offset; + + prefix = "... "; + } + + /* + * Don't want format markers being indented + */ + indent = request->log.indent; + request->log.indent = 0; + + radlog_request(type, lvl, request, "%s%s", prefix, msg); + radlog_request(type, lvl, request, "%s%.*s^ %s", prefix, (int) idx, spaces, error); + + request->log.indent = indent; +} + + +/** Canonicalize error strings, removing tabs, and generate spaces for error marker + * + * @note talloc_free must be called on the buffer returned in spaces and text + * + * Used to produce error messages such as this: + @verbatim + I'm a string with a parser # error + ^ Unexpected character in string + @endverbatim + * + * With code resembling this: + @code{.c} + ERROR("%s", parsed_str); + ERROR("%s^ %s", space, text); + @endcode + * + * @todo merge with above function (radlog_request_marker) + * + * @param sp Where to write a dynamically allocated buffer of spaces used to indent the error text. + * @param text Where to write the canonicalized version of msg (the error text). + * @param ctx to allocate the spaces and text buffers in. + * @param slen of error marker. Expects negative integer value, as returned by parse functions. + * @param msg to canonicalize. + */ +void fr_canonicalize_error(TALLOC_CTX *ctx, char **sp, char **text, ssize_t slen, char const *msg) +{ + size_t offset, skip = 0; + char *spbuf, *p; + char *value; + + offset = -slen; + + /* + * Ensure that the error isn't indented + * too far. + */ + if (offset > 45) { + skip = offset - 40; + offset -= skip; + value = talloc_strdup(ctx, msg + skip); + memcpy(value, "...", 3); + + } else { + value = talloc_strdup(ctx, msg); + } + + spbuf = talloc_array(ctx, char, offset + 1); + memset(spbuf, ' ', offset); + spbuf[offset] = '\0'; + + /* + * Smash tabs to spaces for the input string. + */ + for (p = value; *p != '\0'; p++) { + if (*p == '\t') *p = ' '; + } + + + /* + * Ensure that there isn't too much text after the error. + */ + if (strlen(value) > 100) { + memcpy(value + 95, "... ", 5); + } + + *sp = spbuf; + *text = value; +} + diff --git a/src/main/mainconfig.c b/src/main/mainconfig.c new file mode 100644 index 0000000..227ae4a --- /dev/null +++ b/src/main/mainconfig.c @@ -0,0 +1,1420 @@ +/* + * mainconf.c Handle the server's configuration. + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2002,2006-2007 The FreeRADIUS server project + * Copyright 2002 Alan DeKok <aland@ox.org> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/modules.h> +#include <freeradius-devel/modpriv.h> +#include <freeradius-devel/rad_assert.h> + +#include <sys/stat.h> +#include <pwd.h> +#include <grp.h> + +#ifdef HAVE_SYSLOG_H +# include <syslog.h> +#endif + +#ifdef HAVE_FCNTL_H +# include <fcntl.h> +#endif + +#ifdef HAVE_SYSTEMD +# include <systemd/sd-daemon.h> +#endif + +main_config_t main_config; //!< Main server configuration. +extern fr_cond_t *debug_condition; +fr_cond_t *debug_condition = NULL; //!< Condition used to mark packets up for checking. +bool event_loop_started = false; //!< Whether the main event loop has been started yet. + +typedef struct cached_config_t { + struct cached_config_t *next; + time_t created; + CONF_SECTION *cs; +} cached_config_t; + +static cached_config_t *cs_cache = NULL; + +/* + * Temporary local variables for parsing the configuration + * file. + */ +#ifdef HAVE_SETUID +/* + * Systems that have set/getresuid also have setuid. + */ +static uid_t server_uid = 0; +static gid_t server_gid = 0; +static char const *uid_name = NULL; +static char const *gid_name = NULL; +#endif +static char const *chroot_dir = NULL; +static bool allow_core_dumps = false; +static char const *radlog_dest = NULL; + +/* + * These are not used anywhere else.. + */ +static char const *localstatedir = NULL; +static char const *prefix = NULL; +static char const *my_name = NULL; +static char const *sbindir = NULL; +static char const *run_dir = NULL; +static char const *syslog_facility = NULL; +static bool do_colourise = false; + +static char const *radius_dir = NULL; //!< Path to raddb directory + +/********************************************************************** + * + * We need to figure out where the logs go, before doing anything + * else. This is so that the log messages go to the correct + * place. + * + * BUT, we want the settings from the command line to over-ride + * the ones in the configuration file. So, these items are + * parsed ONLY if there is no "-l foo" on the command line. + * + **********************************************************************/ + +/* + * Log destinations + */ +static const CONF_PARSER startup_log_config[] = { + { "destination", FR_CONF_POINTER(PW_TYPE_STRING, &radlog_dest), "files" }, + { "syslog_facility", FR_CONF_POINTER(PW_TYPE_STRING, &syslog_facility), STRINGIFY(0) }, + + { "localstatedir", FR_CONF_POINTER(PW_TYPE_STRING, &localstatedir), "${prefix}/var"}, + { "logdir", FR_CONF_POINTER(PW_TYPE_STRING, &radlog_dir), "${localstatedir}/log"}, + { "file", FR_CONF_POINTER(PW_TYPE_STRING, &main_config.log_file), "${logdir}/radius.log" }, + { "requests", FR_CONF_POINTER(PW_TYPE_STRING | PW_TYPE_DEPRECATED, &default_log.file), NULL }, + CONF_PARSER_TERMINATOR +}; + + +/* + * Basic configuration for the server. + */ +static const CONF_PARSER startup_server_config[] = { + { "log", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) startup_log_config }, + + { "name", FR_CONF_POINTER(PW_TYPE_STRING, &my_name), "radiusd"}, + { "prefix", FR_CONF_POINTER(PW_TYPE_STRING, &prefix), "/usr/local"}, + + { "log_file", FR_CONF_POINTER(PW_TYPE_STRING, &main_config.log_file), NULL }, + { "log_destination", FR_CONF_POINTER(PW_TYPE_STRING, &radlog_dest), NULL }, + { "use_utc", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &log_dates_utc), NULL }, + CONF_PARSER_TERMINATOR +}; + + +/********************************************************************** + * + * Now that we've parsed the log destination, AND the security + * items, we can parse the rest of the configuration items. + * + **********************************************************************/ +static const CONF_PARSER log_config[] = { + { "stripped_names", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &log_stripped_names),"no" }, + { "auth", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &main_config.log_auth), "no" }, + { "auth_accept", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &main_config.log_accept), NULL}, + { "auth_reject", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &main_config.log_reject), NULL}, + { "auth_badpass", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &main_config.log_auth_badpass), "no" }, + { "auth_goodpass", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &main_config.log_auth_goodpass), "no" }, + { "msg_badpass", FR_CONF_POINTER(PW_TYPE_STRING, &main_config.auth_badpass_msg), NULL}, + { "msg_goodpass", FR_CONF_POINTER(PW_TYPE_STRING, &main_config.auth_goodpass_msg), NULL}, + { "colourise",FR_CONF_POINTER(PW_TYPE_BOOLEAN, &do_colourise), NULL }, + { "use_utc", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &log_dates_utc), NULL }, + { "msg_denied", FR_CONF_POINTER(PW_TYPE_STRING, &main_config.denied_msg), "You are already logged in - access denied" }, + { "suppress_secrets", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &main_config.suppress_secrets), NULL }, + CONF_PARSER_TERMINATOR +}; + + +/* + * Security configuration for the server. + */ +static const CONF_PARSER security_config[] = { + { "max_attributes", FR_CONF_POINTER(PW_TYPE_INTEGER, &fr_max_attributes), STRINGIFY(0) }, + { "reject_delay", FR_CONF_POINTER(PW_TYPE_TIMEVAL, &main_config.reject_delay), STRINGIFY(0) }, + { "status_server", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &main_config.status_server), "no"}, +#ifdef ENABLE_OPENSSL_VERSION_CHECK + { "allow_vulnerable_openssl", FR_CONF_POINTER(PW_TYPE_STRING, &main_config.allow_vulnerable_openssl), "no"}, +#endif + CONF_PARSER_TERMINATOR +}; + +static const CONF_PARSER resources[] = { + /* + * Don't set a default here. It's set in the code, below. This means that + * the config item will *not* get printed out in debug mode, so that no one knows + * it exists. + */ + { "talloc_pool_size", FR_CONF_POINTER(PW_TYPE_INTEGER, &main_config.talloc_pool_size), NULL }, + CONF_PARSER_TERMINATOR +}; + +static const CONF_PARSER server_config[] = { + /* + * FIXME: 'prefix' is the ONLY one which should be + * configured at compile time. Hard-coding it here is + * bad. It will be cleaned up once we clean up the + * hard-coded defines for the locations of the various + * files. + */ + { "name", FR_CONF_POINTER(PW_TYPE_STRING, &my_name), "radiusd"}, + { "prefix", FR_CONF_POINTER(PW_TYPE_STRING, &prefix), "/usr/local"}, + { "localstatedir", FR_CONF_POINTER(PW_TYPE_STRING, &localstatedir), "${prefix}/var"}, + { "sbindir", FR_CONF_POINTER(PW_TYPE_STRING, &sbindir), "${prefix}/sbin"}, + { "logdir", FR_CONF_POINTER(PW_TYPE_STRING, &radlog_dir), "${localstatedir}/log"}, + { "run_dir", FR_CONF_POINTER(PW_TYPE_STRING, &run_dir), "${localstatedir}/run/${name}"}, + { "libdir", FR_CONF_POINTER(PW_TYPE_STRING, &radlib_dir), "${prefix}/lib"}, + { "radacctdir", FR_CONF_POINTER(PW_TYPE_STRING, &radacct_dir), "${logdir}/radacct" }, + { "panic_action", FR_CONF_POINTER(PW_TYPE_STRING, &main_config.panic_action), NULL}, + { "hostname_lookups", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &fr_dns_lookups), "no" }, + { "max_request_time", FR_CONF_POINTER(PW_TYPE_INTEGER, &main_config.max_request_time), STRINGIFY(MAX_REQUEST_TIME) }, + { "cleanup_delay", FR_CONF_POINTER(PW_TYPE_INTEGER, &main_config.cleanup_delay), STRINGIFY(CLEANUP_DELAY) }, + { "max_requests", FR_CONF_POINTER(PW_TYPE_INTEGER, &main_config.max_requests), STRINGIFY(MAX_REQUESTS) }, + { "postauth_client_lost", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &main_config.postauth_client_lost), "no" }, + { "pidfile", FR_CONF_POINTER(PW_TYPE_STRING, &main_config.pid_file), "${run_dir}/radiusd.pid"}, + { "checkrad", FR_CONF_POINTER(PW_TYPE_STRING, &main_config.checkrad), "${sbindir}/checkrad" }, + + { "debug_level", FR_CONF_POINTER(PW_TYPE_INTEGER, &main_config.debug_level), "0"}, + +#ifdef WITH_PROXY + { "proxy_requests", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &main_config.proxy_requests), "yes" }, +#endif + { "log", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) log_config }, + + { "resources", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) resources }, + + /* + * People with old configs will have these. They are listed + * AFTER the "log" section, so if they exist in radiusd.conf, + * it will prefer "log_foo = bar" to "log { foo = bar }". + * They're listed with default values of NULL, so that if they + * DON'T exist in radiusd.conf, then the previously parsed + * values for "log { foo = bar}" will be used. + */ + { "log_auth", FR_CONF_POINTER(PW_TYPE_BOOLEAN | PW_TYPE_DEPRECATED, &main_config.log_auth), NULL }, + { "log_auth_badpass", FR_CONF_POINTER(PW_TYPE_BOOLEAN | PW_TYPE_DEPRECATED, &main_config.log_auth_badpass), NULL }, + { "log_auth_goodpass", FR_CONF_POINTER(PW_TYPE_BOOLEAN | PW_TYPE_DEPRECATED, &main_config.log_auth_goodpass), NULL }, + { "log_stripped_names", FR_CONF_POINTER(PW_TYPE_BOOLEAN | PW_TYPE_DEPRECATED, &log_stripped_names), NULL }, + + { "security", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) security_config }, + CONF_PARSER_TERMINATOR +}; + + +/********************************************************************** + * + * The next few items are here as a "bootstrap" for security. + * They allow the server to switch users, chroot, while still + * opening the various output files with the correct permission. + * + * It's rare (or impossible) to have parse errors here, so we + * don't worry too much about that. In contrast, when we parse + * the rest of the configuration, we CAN get parse errors. We + * want THOSE parse errors to go to the log file, and we want the + * log file to have the correct permissions. + * + **********************************************************************/ +static const CONF_PARSER bootstrap_security_config[] = { +#ifdef HAVE_SETUID + { "user", FR_CONF_POINTER(PW_TYPE_STRING, &uid_name), NULL }, + { "group", FR_CONF_POINTER(PW_TYPE_STRING, &gid_name), NULL }, +#endif + { "chroot", FR_CONF_POINTER(PW_TYPE_STRING, &chroot_dir), NULL }, + { "allow_core_dumps", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &allow_core_dumps), "no" }, + CONF_PARSER_TERMINATOR +}; + +static const CONF_PARSER bootstrap_config[] = { + { "security", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) bootstrap_security_config }, + + { "name", FR_CONF_POINTER(PW_TYPE_STRING, &my_name), "radiusd"}, + { "prefix", FR_CONF_POINTER(PW_TYPE_STRING, &prefix), "/usr/local"}, + { "localstatedir", FR_CONF_POINTER(PW_TYPE_STRING, &localstatedir), "${prefix}/var"}, + + { "logdir", FR_CONF_POINTER(PW_TYPE_STRING, &radlog_dir), "${localstatedir}/log"}, + { "run_dir", FR_CONF_POINTER(PW_TYPE_STRING, &run_dir), "${localstatedir}/run/${name}"}, + + /* + * For backwards compatibility. + */ +#ifdef HAVE_SETUID + { "user", FR_CONF_POINTER(PW_TYPE_STRING | PW_TYPE_DEPRECATED, &uid_name), NULL }, + { "group", FR_CONF_POINTER(PW_TYPE_STRING | PW_TYPE_DEPRECATED, &gid_name), NULL }, +#endif + { "chroot", FR_CONF_POINTER(PW_TYPE_STRING | PW_TYPE_DEPRECATED, &chroot_dir), NULL }, + { "allow_core_dumps", FR_CONF_POINTER(PW_TYPE_BOOLEAN | PW_TYPE_DEPRECATED, &allow_core_dumps), NULL }, + CONF_PARSER_TERMINATOR +}; + + +static size_t config_escape_func(UNUSED REQUEST *request, char *out, size_t outlen, char const *in, UNUSED void *arg) +{ + size_t len = 0; + static char const disallowed[] = "%{}\\'\"`"; + + while (in[0]) { + /* + * Non-printable characters get replaced with their + * mime-encoded equivalents. + */ + if ((in[0] < 32)) { + if (outlen <= 3) break; + + snprintf(out, outlen, "=%02X", (unsigned char) in[0]); + in++; + out += 3; + outlen -= 3; + len += 3; + continue; + + } else if (strchr(disallowed, *in) != NULL) { + if (outlen <= 2) break; + + out[0] = '\\'; + out[1] = *in; + in++; + out += 2; + outlen -= 2; + len += 2; + continue; + } + + /* + * Only one byte left. + */ + if (outlen <= 1) { + break; + } + + /* + * Allowed character. + */ + *out = *in; + out++; + in++; + outlen--; + len++; + } + *out = '\0'; + return len; +} + +/* + * Xlat for %{config:section.subsection.attribute} + */ +static ssize_t xlat_config(UNUSED void *instance, REQUEST *request, char const *fmt, char *out, size_t outlen) +{ + char const *value; + CONF_PAIR *cp; + CONF_ITEM *ci; + char buffer[1024]; + + /* + * Expand it safely. + */ + if (radius_xlat(buffer, sizeof(buffer), request, fmt, config_escape_func, NULL) < 0) { + return 0; + } + + ci = cf_reference_item(request->root->config, + request->root->config, buffer); + if (!ci || !cf_item_is_pair(ci)) { + REDEBUG("Config item \"%s\" does not exist", fmt); + *out = '\0'; + return -1; + } + + cp = cf_item_to_pair(ci); + + /* + * Ensure that we only copy what's necessary. + * + * If 'outlen' is too small, then the output is chopped to fit. + */ + value = cf_pair_value(cp); + if (!value) { + out[0] = '\0'; + return 0; + } + + if (outlen > strlen(value)) { + outlen = strlen(value) + 1; + } + + strlcpy(out, value, outlen); + + return strlen(out); +} + + +/* + * Xlat for %{client:foo} + */ +static ssize_t xlat_client(UNUSED void *instance, REQUEST *request, char const *fmt, char *out, size_t outlen) +{ + char const *value = NULL; + CONF_PAIR *cp; + + if (!fmt || !out || (outlen < 1)) return 0; + + if (!request->client) { + RWDEBUG("No client associated with this request"); + *out = '\0'; + return 0; + } + + cp = cf_pair_find(request->client->cs, fmt); + if (!cp || !(value = cf_pair_value(cp))) { + if (strcmp(fmt, "shortname") == 0 && request->client->shortname) { + value = request->client->shortname; + } + else if (strcmp(fmt, "nas_type") == 0 && request->client->nas_type) { + value = request->client->nas_type; + } else { + *out = '\0'; + return 0; + } + } + + strlcpy(out, value, outlen); + + return strlen(out); +} + +/* + * Xlat for %{getclient:<ipaddr>.foo} + */ +static ssize_t xlat_getclient(UNUSED void *instance, REQUEST *request, char const *fmt, char *out, size_t outlen) +{ + char const *value = NULL; + char buffer[INET6_ADDRSTRLEN], *q; + char const *p = fmt; + fr_ipaddr_t ip; + CONF_PAIR *cp; + RADCLIENT *client = NULL; + + if (!fmt || !out || (outlen < 1)) return 0; + + q = strrchr(p, '.'); + if (!q || (q == p) || (((size_t)(q - p)) > sizeof(buffer))) { + REDEBUG("Invalid client string"); + goto error; + } + + strlcpy(buffer, p, (q + 1) - p); + if (fr_pton(&ip, buffer, -1, AF_UNSPEC, false) < 0) { + REDEBUG("\"%s\" is not a valid IPv4 or IPv6 address", buffer); + goto error; + } + + fmt = q + 1; + + client = client_find(NULL, &ip, IPPROTO_IP); + if (!client) { + RDEBUG("No client found with IP \"%s\"", buffer); + *out = '\0'; + return 0; + } + + cp = cf_pair_find(client->cs, fmt); + if (!cp || !(value = cf_pair_value(cp))) { + if (strcmp(fmt, "shortname") == 0) { + strlcpy(out, request->client->shortname, outlen); + return strlen(out); + } + *out = '\0'; + return 0; + } + + strlcpy(out, value, outlen); + return strlen(out); + + error: + *out = '\0'; + return -1; +} + +/* + * Common xlat for listeners + */ +static ssize_t xlat_listen_common(REQUEST *request, rad_listen_t *listen, + char const *fmt, char *out, size_t outlen) +{ + char const *value = NULL; + CONF_PAIR *cp; + + if (!fmt || !out || (outlen < 1)) return 0; + + if (!listen) { + RWDEBUG("No listener associated with this request"); + *out = '\0'; + return 0; + } + + /* + * When TLS is configured, we *require* the use of TLS. + */ + if (strcmp(fmt, "tls") == 0) { +#ifdef WITH_TLS + if (listen->tls) { + strlcpy(out, "yes", outlen); + return strlen(out); + } +#endif + + strlcpy(out, "no", outlen); + return strlen(out); + } + +#ifdef WITH_TLS + /* + * Look for TLS certificate data. + */ + if (strncmp(fmt, "TLS-", 4) == 0) { + VALUE_PAIR *vp; + listen_socket_t *sock = listen->data; + + if (!listen->tls) { + RDEBUG("Listener is not using TLS. TLS attributes are not available"); + *out = '\0'; + return 0; + } + + for (vp = sock->certs; vp != NULL; vp = vp->next) { + if (strcmp(fmt, vp->da->name) == 0) { + return vp_prints_value(out, outlen, vp, 0); + } + } + + RDEBUG("Unknown TLS attribute \"%s\"", fmt); + *out = '\0'; + return 0; + } +#else + if (strncmp(fmt, "TLS-", 4) == 0) { + RDEBUG("Server is not built with TLS support"); + *out = '\0'; + return 0; + } +#endif + +#ifdef WITH_COA_TUNNEL + /* + * Look for RADSEC CoA tunnel key. + */ + if (listen->key && (strcmp(fmt, "Originating-Realm-Key") == 0)) { + strlcpy(out, listen->key, outlen); + return strlen(out); + } +#endif + + cp = cf_pair_find(listen->cs, fmt); + if (!cp || !(value = cf_pair_value(cp))) { + RDEBUG("Listener does not contain config item \"%s\"", fmt); + *out = '\0'; + return 0; + } + + strlcpy(out, value, outlen); + + return strlen(out); +} + + +/* + * Xlat for %{listen:foo} + */ +static ssize_t xlat_listen(UNUSED void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + return xlat_listen_common(request, request->listener, fmt, out, outlen); +} + +/* + * Xlat for %{proxy_listen:foo} + */ +static ssize_t xlat_proxy_listen(UNUSED void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + if (!request->proxy_listener) { + *out = '\0'; + return 0; + } + + return xlat_listen_common(request, request->proxy_listener, fmt, out, outlen); +} + +#ifdef HAVE_SETUID +/* + * Do chroot, if requested. + * + * Switch UID and GID to what is specified in the config file + */ +static int switch_users(CONF_SECTION *cs) +{ + bool do_suid = false; + bool do_sgid = false; + + /* + * Get the current maximum for core files. Do this + * before anything else so as to ensure it's properly + * initialized. + */ + if (fr_set_dumpable_init() < 0) { + return 0; + } + + /* + * Don't do chroot/setuid/setgid if we're in debugging + * as non-root. + */ + if (rad_debug_lvl && (getuid() != 0)) return 1; + + if (cf_section_parse(cs, NULL, bootstrap_config) < 0) { + fr_strerror_printf("Failed to parse user/group information."); + return 0; + } + +#ifdef HAVE_GRP_H + /* + * Get the correct GID for the server. + */ + server_gid = getgid(); + + if (gid_name) { + struct group *gr; + + gr = getgrnam(gid_name); + if (!gr) { + fr_strerror_printf("Cannot get ID for group %s: %s", + gid_name, fr_syserror(errno)); + return 0; + } + + if (server_gid != gr->gr_gid) { + server_gid = gr->gr_gid; + do_sgid = true; + } + } +#endif + + /* + * Get the correct UID for the server. + */ + server_uid = getuid(); + + if (uid_name) { + struct passwd *user; + + if (rad_getpwnam(cs, &user, uid_name) < 0) { + fr_strerror_printf("Cannot get passwd entry for user %s: %s", + uid_name, fr_strerror()); + return 0; + } + + /* + * We're not the correct user. Go set that. + */ + if (server_uid != user->pw_uid) { + server_uid = user->pw_uid; + do_suid = true; +#ifdef HAVE_INITGROUPS + if (initgroups(uid_name, server_gid) < 0) { + fr_strerror_printf("Cannot initialize supplementary group list for user %s: %s", + uid_name, fr_syserror(errno)); + talloc_free(user); + return 0; + } +#endif + } + + talloc_free(user); + } + + /* + * Do chroot BEFORE changing UIDs. + */ + if (chroot_dir) { + if (chroot(chroot_dir) < 0) { + fr_strerror_printf("Failed to perform chroot to %s: %s", + chroot_dir, fr_syserror(errno)); + return 0; + } + + /* + * Note that we leave chdir alone. It may be + * OUTSIDE of the root. This allows us to read + * the configuration from "-d ./etc/raddb", with + * the chroot as "./chroot/" for example. After + * the server has been loaded, it does a "cd + * ${logdir}" below, so that core files (if any) + * go to a logging directory. + * + * This also allows the configuration of the + * server to be outside of the chroot. If the + * server is statically linked, then the only + * things needed inside of the chroot are the + * logging directories. + */ + } + +#ifdef HAVE_GRP_H + /* + * Set the GID. Don't bother checking it. + */ + if (do_sgid) { + if (setgid(server_gid) < 0){ + fr_strerror_printf("Failed setting group to %s: %s", + gid_name, fr_syserror(errno)); + return 0; + } + } +#endif + + /* + * The directories for PID files and logs must exist. We + * need to create them if we're told to write files to + * those directories. + * + * Because this creation is new in 3.0.9, it's a soft + * fail. + * + */ + if (main_config.write_pid) { + char *my_dir; + + my_dir = talloc_strdup(NULL, run_dir); + if (rad_mkdir(my_dir, 0750, server_uid, server_gid) < 0) { + DEBUG("Failed to create run_dir %s: %s", + my_dir, strerror(errno)); + } + talloc_free(my_dir); + } + + if (default_log.dst == L_DST_FILES) { + char *my_dir; + + my_dir = talloc_strdup(NULL, radlog_dir); + if (rad_mkdir(my_dir, 0750, server_uid, server_gid) < 0) { + DEBUG("Failed to create logdir %s: %s", + my_dir, strerror(errno)); + } + talloc_free(my_dir); + } + + /* + * If we don't already have a log file open, open one + * now. We may not have been logging anything yet. The + * server normally starts up fairly quietly. + */ + if ((default_log.dst == L_DST_FILES) && + (default_log.fd < 0)) { + default_log.fd = open(main_config.log_file, + O_WRONLY | O_APPEND | O_CREAT, 0640); + if (default_log.fd < 0) { + fr_strerror_printf("Failed to open log file %s: %s\n", + main_config.log_file, fr_syserror(errno)); + return 0; + } + } + + /* + * If we need to change UID, ensure that the log files + * have the correct owner && group. + * + * We have to do this because some log files MAY already + * have been written as root. We need to change them to + * have the correct ownership before proceeding. + */ + if ((do_suid || do_sgid) && + (default_log.dst == L_DST_FILES)) { + if (fchown(default_log.fd, server_uid, server_gid) < 0) { + fr_strerror_printf("Cannot change ownership of log file %s: %s\n", + main_config.log_file, fr_syserror(errno)); + return 0; + } + } + + /* + * Once we're done with all of the privileged work, + * permanently change the UID. + */ + if (do_suid) { + rad_suid_set_down_uid(server_uid); + rad_suid_down(); + } + + /* + * This also clears the dumpable flag if core dumps + * aren't allowed. + */ + if (fr_set_dumpable(allow_core_dumps) < 0) { + WARN("Failed to allow core dumps - %s", fr_strerror()); + } + + if (allow_core_dumps) { + INFO("Core dumps are enabled"); + } + + return 1; +} +#endif /* HAVE_SETUID */ + +/** Set the global radius config directory. + * + * @param ctx Where to allocate the memory for the path string. + * @param path to config dir root e.g. /usr/local/etc/raddb + */ +void set_radius_dir(TALLOC_CTX *ctx, char const *path) +{ + if (radius_dir) { + char *p; + + memcpy(&p, &radius_dir, sizeof(p)); + talloc_free(p); + radius_dir = NULL; + } + if (path) radius_dir = talloc_strdup(ctx, path); +} + +/** Get the global radius config directory. + * + * @return the global radius config directory. + */ +char const *get_radius_dir(void) +{ + return radius_dir; +} + +static int _dlhandle_free(void **dl_handle) +{ + dlclose(*dl_handle); + return 0; +} + +/* + * Read config files. + * + * This function can ONLY be called from the main server process. + */ +int main_config_init(void) +{ + char const *p = NULL; + CONF_SECTION *cs, *subcs; + struct stat statbuf; + cached_config_t *cc; + char buffer[1024]; + + if (stat(radius_dir, &statbuf) < 0) { + ERROR("Errors reading %s: %s", + radius_dir, fr_syserror(errno)); + return -1; + } + +#ifdef S_IWOTH + if ((statbuf.st_mode & S_IWOTH) != 0) { + ERROR("Configuration directory %s is globally writable. Refusing to start due to insecure configuration.", + radius_dir); + return -1; + } +#endif + +#if 0 && defined(S_IROTH) + if (statbuf.st_mode & S_IROTH != 0) { + ERROR("Configuration directory %s is globally readable. Refusing to start due to insecure configuration.", + radius_dir); + return -1; + } +#endif + INFO("Starting - reading configuration files ..."); + + /* + * We need to load the dictionaries before reading the + * configuration files. This is because of the + * pre-compilation in conffile.c. That should probably + * be fixed to be done as a second stage. + */ + if (!main_config.dictionary_dir) { + main_config.dictionary_dir = DICTDIR; + } + + /* + * About sizeof(REQUEST) + sizeof(RADIUS_PACKET) * 2 + sizeof(VALUE_PAIR) * 400 + * + * Which should be enough for many configurations. + */ + main_config.talloc_pool_size = 8 * 1024; /* default */ + + /* + * Read the distribution dictionaries first, then + * the ones in raddb. + */ + DEBUG2("including dictionary file %s/%s", main_config.dictionary_dir, RADIUS_DICTIONARY); + if (dict_init(main_config.dictionary_dir, RADIUS_DICTIONARY) != 0) { + ERROR("Errors reading dictionary: %s", + fr_strerror()); + return -1; + } + +#define DICT_READ_OPTIONAL(_d, _n) \ +do {\ + switch (dict_read(_d, _n)) {\ + case -1:\ + ERROR("Errors reading %s/%s: %s", _d, _n, fr_strerror());\ + return -1;\ + case 0:\ + DEBUG2("including dictionary file %s/%s", _d,_n);\ + break;\ + default:\ + break;\ + }\ +} while (0) + + /* + * Try to load protocol-specific dictionaries. It's OK + * if they don't exist. + */ +#ifdef WITH_DHCP + DICT_READ_OPTIONAL(main_config.dictionary_dir, "dictionary.dhcp"); +#endif + +#ifdef WITH_VMPS + DICT_READ_OPTIONAL(main_config.dictionary_dir, "dictionary.vqp"); +#endif + + /* + * It's OK if this one doesn't exist. + */ + DICT_READ_OPTIONAL(radius_dir, RADIUS_DICTIONARY); + + cs = cf_section_alloc(NULL, "main", NULL); + if (!cs) return -1; + + /* + * Add a 'feature' subsection off the main config + * We check if it's defined first, as the user may + * have defined their own feature flags, or want + * to manually override the ones set by modules + * or the server. + */ + subcs = cf_section_sub_find(cs, "feature"); + if (!subcs) { + subcs = cf_section_alloc(cs, "feature", NULL); + if (!subcs) return -1; + + cf_section_add(cs, subcs); + } + version_init_features(subcs); + + /* + * Add a 'version' subsection off the main config + * We check if it's defined first, this is for + * backwards compatibility. + */ + subcs = cf_section_sub_find(cs, "version"); + if (!subcs) { + subcs = cf_section_alloc(cs, "version", NULL); + if (!subcs) return -1; + cf_section_add(cs, subcs); + } + version_init_numbers(subcs); + + /* Read the configuration file */ + snprintf(buffer, sizeof(buffer), "%.200s/%.50s.conf", radius_dir, main_config.name); + if (cf_file_read(cs, buffer) < 0) { + ERROR("Errors reading or parsing %s", buffer); + failure: + talloc_free(cs); + return -1; + } + + /* + * Parse environment variables first. + */ + subcs = cf_section_sub_find(cs, "ENV"); + if (subcs) { + char const *attr, *value; + CONF_PAIR *cp; + CONF_ITEM *ci; + + for (ci = cf_item_find_next(subcs, NULL); + ci != NULL; + ci = cf_item_find_next(subcs, ci)) { + + if (cf_item_is_data(ci)) continue; + + if (!cf_item_is_pair(ci)) { + cf_log_err(ci, "Unexpected item in ENV section"); + goto failure; + } + + cp = cf_item_to_pair(ci); + if (cf_pair_operator(cp) != T_OP_EQ) { + cf_log_err(ci, "Invalid operator for item in ENV section"); + goto failure; + } + + attr = cf_pair_attr(cp); + value = cf_pair_value(cp); + if (!value) { + if (unsetenv(attr) < 0) { + cf_log_err(ci, "Failed deleting environment variable %s: %s", + attr, fr_syserror(errno)); + goto failure; + } + } else { + void *handle; + void **handle_p; + + if (setenv(attr, value, 1) < 0) { + cf_log_err(ci, "Failed setting environment variable %s: %s", + attr, fr_syserror(errno)); + goto failure; + } + + /* + * Hacks for LD_PRELOAD. + */ + if (strcmp(attr, "LD_PRELOAD") != 0) continue; + + handle = dlopen(value, RTLD_NOW | RTLD_GLOBAL); + if (!handle) { + cf_log_err(ci, "Failed loading library %s: %s", value, dlerror()); + goto failure; + } + + /* + * Wrap the pointer, so we can set a destructor. + */ + MEM(handle_p = talloc(NULL, void *)); + *handle_p = handle; + talloc_set_destructor(handle_p, _dlhandle_free); + + (void) cf_data_add(subcs, value, handle, NULL); + } + } /* loop over pairs in ENV */ + } /* there's an ENV subsection */ + + /* + * If there was no log destination set on the command line, + * set it now. + */ + if (default_log.dst == L_DST_NULL) { + default_log.dst = L_DST_STDERR; + default_log.fd = STDERR_FILENO; + + if (cf_section_parse(cs, NULL, startup_server_config) == -1) { + fprintf(stderr, "%s: Error: Failed to parse log{} section.\n", + main_config.name); + cf_file_free(cs); + return -1; + } + + if (!radlog_dest) { + fprintf(stderr, "%s: Error: No log destination specified.\n", + main_config.name); + cf_file_free(cs); + return -1; + } + + default_log.fd = -1; + default_log.dst = fr_str2int(log_str2dst, radlog_dest, + L_DST_NUM_DEST); + if (default_log.dst == L_DST_NUM_DEST) { + fprintf(stderr, "%s: Error: Unknown log_destination %s\n", + main_config.name, radlog_dest); + cf_file_free(cs); + return -1; + } + + if (default_log.dst == L_DST_SYSLOG) { + /* + * Make sure syslog_facility isn't NULL + * before using it + */ + if (!syslog_facility) { + fprintf(stderr, "%s: Error: Syslog chosen but no facility was specified\n", + main_config.name); + cf_file_free(cs); + return -1; + } + main_config.syslog_facility = fr_str2int(syslog_facility_table, syslog_facility, -1); + if (main_config.syslog_facility < 0) { + fprintf(stderr, "%s: Error: Unknown syslog_facility %s\n", + main_config.name, syslog_facility); + cf_file_free(cs); + return -1; + } + +#ifdef HAVE_SYSLOG_H + /* + * Call openlog only once, when the + * program starts. + */ + openlog(main_config.name, LOG_PID, main_config.syslog_facility); +#endif + + } else if (default_log.dst == L_DST_FILES) { + if (!main_config.log_file) { + fprintf(stderr, "%s: Error: Specified \"files\" as a log destination, but no log filename was given!\n", + main_config.name); + cf_file_free(cs); + return -1; + } + } + } + +#ifdef HAVE_SETUID + /* + * Switch users as early as possible. + */ + if (!switch_users(cs)) { + fprintf(stderr, "%s: ERROR - %s\n", main_config.name, fr_strerror()); + fr_exit(1); + } +#endif + + /* + * This allows us to figure out where, relative to + * radiusd.conf, the other configuration files exist. + */ + if (cf_section_parse(cs, NULL, server_config) < 0) return -1; + + /* + * Fix up log_auth, and log_accept and log_reject + */ + if (main_config.log_auth) { + main_config.log_accept = main_config.log_reject = true; + } + + /* + * We ignore colourization of output until after the + * configuration files have been parsed. + */ + p = getenv("TERM"); + if (do_colourise && p && isatty(default_log.fd) && strstr(p, "xterm")) { + default_log.colourise = true; + } else { + default_log.colourise = false; + } + + /* + * Starting the server, WITHOUT "-x" on the + * command-line: use whatever is in the config + * file. + */ + if (rad_debug_lvl == 0) { + rad_debug_lvl = main_config.debug_level; + } + fr_debug_lvl = rad_debug_lvl; + + FR_INTEGER_COND_CHECK("max_request_time", main_config.max_request_time, + (main_config.max_request_time != 0), 100); + + /* + * reject_delay can be zero. OR 1 though 10. + */ + if ((main_config.reject_delay.tv_sec != 0) || (main_config.reject_delay.tv_usec != 0)) { + FR_TIMEVAL_BOUND_CHECK("reject_delay", &main_config.reject_delay, >=, 1, 0); + } + FR_TIMEVAL_BOUND_CHECK("reject_delay", &main_config.reject_delay, <=, 10, 0); + + FR_INTEGER_BOUND_CHECK("cleanup_delay", main_config.cleanup_delay, <=, 30); + + FR_INTEGER_BOUND_CHECK("resources.talloc_pool_size", main_config.talloc_pool_size, >=, 2 * 1024); + FR_INTEGER_BOUND_CHECK("resources.talloc_pool_size", main_config.talloc_pool_size, <=, 1024 * 1024); + + /* + * Set default initial request processing delay to 1/3 of a second. + * Will be updated by the lowest response window across all home servers, + * if it is less than this. + */ + main_config.init_delay.tv_sec = 0; + main_config.init_delay.tv_usec = 2* (1000000 / 3); + + /* + * Free the old configuration items, and replace them + * with the new ones. + * + * Note that where possible, we do atomic switch-overs, + * to ensure that the pointers are always valid. + */ + rad_assert(main_config.config == NULL); + root_config = main_config.config = cs; + + DEBUG2("%s: #### Loading Realms and Home Servers ####", main_config.name); + if (!realms_init(cs)) { + return -1; + } + + DEBUG2("%s: #### Loading Clients ####", main_config.name); + if (!client_list_parse_section(cs, false)) { + return -1; + } + + /* + * Register the %{config:section.subsection} xlat function. + */ + xlat_register("config", xlat_config, NULL, NULL); + xlat_register("client", xlat_client, NULL, NULL); + xlat_register("getclient", xlat_getclient, NULL, NULL); + xlat_register("listen", xlat_listen, NULL, NULL); + xlat_register("proxy_listen", xlat_proxy_listen, NULL, NULL); + + /* + * Go update our behaviour, based on the configuration + * changes. + */ + + /* + * Sanity check the configuration for internal + * consistency. + */ + FR_TIMEVAL_BOUND_CHECK("reject_delay", &main_config.reject_delay, <=, main_config.cleanup_delay, 0); + + if (chroot_dir) { + if (chdir(radlog_dir) < 0) { + ERROR("Failed to 'chdir %s' after chroot: %s", + radlog_dir, fr_syserror(errno)); + return -1; + } + } + + cc = talloc_zero(NULL, cached_config_t); + if (!cc) return -1; + + cc->cs = talloc_steal(cc ,cs); + rad_assert(cs_cache == NULL); + cs_cache = cc; + + /* Clear any unprocessed configuration errors */ + (void) fr_strerror(); + + return 0; +} + +/* + * Free the configuration. Called only when the server is exiting. + */ +int main_config_free(void) +{ + virtual_servers_free(0); + + /* + * Clean up the configuration data + * structures. + */ + client_list_free(NULL); + realms_free(); + listen_free(&main_config.listen); + + /* + * Frees current config and any previous configs. + */ + TALLOC_FREE(cs_cache); + dict_free(); + + return 0; +} + +void hup_logfile(void) +{ + int fd, old_fd; + + if (default_log.dst != L_DST_FILES) return; + + fd = open(main_config.log_file, + O_WRONLY | O_APPEND | O_CREAT, 0640); + if (fd >= 0) { + /* + * Atomic swap. We'd like to keep the old + * FD around so that callers don't + * suddenly find the FD closed, and the + * writes go nowhere. But that's hard to + * do. So... we have the case where a + * log message *might* be lost on HUP. + */ + old_fd = default_log.fd; + default_log.fd = fd; + close(old_fd); + } +} + +static int hup_callback(void *ctx, void *data) +{ + CONF_SECTION *modules = ctx; + CONF_SECTION *cs = data; + CONF_SECTION *parent; + char const *name; + module_instance_t *mi; + + /* + * Files may be defined in sub-sections of a module + * config. Walk up the tree until we find the module + * definition. + */ + parent = cf_item_parent(cf_section_to_item(cs)); + while (parent != modules) { + cs = parent; + parent = cf_item_parent(cf_section_to_item(cs)); + + /* + * Something went wrong. Oh well... + */ + if (!parent) return 0; + } + + name = cf_section_name2(cs); + if (!name) name = cf_section_name1(cs); + + mi = module_find(modules, name); + if (!mi) return 0; + + if ((mi->entry->module->type & RLM_TYPE_HUP_SAFE) == 0) return 0; + + if (!module_hup_module(mi->cs, mi, time(NULL))) return 0; + + return 1; +} + +void main_config_hup(void) +{ + int rcode; + cached_config_t *cc; + CONF_SECTION *cs; + time_t when; + char buffer[1024]; + + static time_t last_hup = 0; + + /* + * Re-open the log file. If we can't, then keep logging + * to the old log file. + * + * The "open log file" code is here rather than in log.c, + * because it makes that function MUCH simpler. + */ + hup_logfile(); + + /* + * Only check the config files every few seconds. + */ + when = time(NULL); + if ((last_hup + 2) >= when) { + INFO("HUP - Last HUP was too recent. Ignoring"); + return; + } + last_hup = when; + + rcode = cf_file_changed(cs_cache->cs, hup_callback); + if (rcode == CF_FILE_NONE) { + INFO("HUP - No files changed. Ignoring"); + return; + } + + if (rcode == CF_FILE_ERROR) { + INFO("HUP - Cannot read configuration files. Ignoring"); + return; + } + + /* + * No config files have changed. + */ + if ((rcode & CF_FILE_CONFIG) == 0) { + if ((rcode & CF_FILE_MODULE) != 0) { + INFO("HUP - Files loaded by a module have changed."); + + /* + * FIXME: reload the module. + */ + + } + return; + } + + cs = cf_section_alloc(NULL, "main", NULL); + if (!cs) return; + +#ifdef HAVE_SYSTEMD + sd_notify(0, "RELOADING=1"); +#endif + + /* Read the configuration file */ + snprintf(buffer, sizeof(buffer), "%.200s/%.50s.conf", radius_dir, main_config.name); + + INFO("HUP - Re-reading configuration files"); + if (cf_file_read(cs, buffer) < 0) { + ERROR("Failed to re-read or parse %s", buffer); + talloc_free(cs); + return; + } + + cc = talloc_zero(cs_cache, cached_config_t); + if (!cc) { + ERROR("Out of memory"); + return; + } + + /* + * Save the current configuration. Note that we do NOT + * free older ones. We should probably do so at some + * point. Doing so will require us to mark which modules + * are still in use, and which aren't. Modules that + * can't be HUPed always use the original configuration. + * Modules that can be HUPed use one of the newer + * configurations. + */ + cc->created = time(NULL); + cc->cs = talloc_steal(cc, cs); + cc->next = cs_cache; + cs_cache = cc; + + INFO("HUP - loading modules"); + + /* + * Prefer the new module configuration. + */ + modules_hup(cf_section_sub_find(cs, "modules")); + + /* + * Load new servers BEFORE freeing old ones. + */ + virtual_servers_load(cs); + + virtual_servers_free(cc->created - (main_config.max_request_time * 4)); + +#ifdef HAVE_SYSTEMD + /* + * If RELOADING=1 event is sent then it needed also a "READY=1" notification + * when it completed reloading its configuration. + */ + sd_notify(0, "READY=1"); +#endif +} diff --git a/src/main/map.c b/src/main/map.c new file mode 100644 index 0000000..e59fcec --- /dev/null +++ b/src/main/map.c @@ -0,0 +1,1717 @@ +/* + * This program 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$ + * + * @brief map / template functions + * @file main/map.c + * + * @ingroup AVP + * + * @copyright 2013 The FreeRADIUS server project + * @copyright 2013 Alan DeKok <aland@freeradius.org> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/rad_assert.h> + +#include <ctype.h> + +#ifdef DEBUG_MAP +static void map_dump(REQUEST *request, vp_map_t const *map) +{ + RDEBUG(">>> MAP TYPES LHS: %s, RHS: %s", + fr_int2str(tmpl_names, map->lhs->type, "???"), + fr_int2str(tmpl_names, map->rhs->type, "???")); + + if (map->rhs) { + RDEBUG(">>> MAP NAMES %s %s", map->lhs->name, map->rhs->name); + } +} +#endif + + +/** re-parse a map where the lhs is an unknown attribute. + * + * + * @param map to process. + * @param rhs_type quotation type around rhs. + * @param rhs string to re-parse. + */ +bool map_cast_from_hex(vp_map_t *map, FR_TOKEN rhs_type, char const *rhs) +{ + size_t len; + ssize_t rlen; + uint8_t *ptr; + char const *p; + pair_lists_t list; + + DICT_ATTR const *da; + VALUE_PAIR *vp; + vp_tmpl_t *vpt; + + rad_assert(map != NULL); + + rad_assert(map->lhs != NULL); + rad_assert(map->lhs->type == TMPL_TYPE_ATTR); + + rad_assert(map->rhs == NULL); + rad_assert(rhs != NULL); + + VERIFY_MAP(map); + + /* + * If the attribute is still unknown, go parse the RHS. + */ + da = dict_attrbyvalue(map->lhs->tmpl_da->attr, map->lhs->tmpl_da->vendor); + if (!da || da->flags.is_unknown) return false; + + /* + * If the RHS is something OTHER than an octet + * string, go parse it as that. + */ + if (rhs_type != T_BARE_WORD) return false; + if ((rhs[0] != '0') || (tolower((uint8_t)rhs[1]) != 'x')) return false; + if (!rhs[2]) return false; + + len = strlen(rhs + 2); + + ptr = talloc_array(map, uint8_t, len >> 1); + if (!ptr) return false; + + len = fr_hex2bin(ptr, len >> 1, rhs + 2, len); + + /* + * If we can't parse it, or if it's malformed, + * it's still unknown. + */ + rlen = data2vp(NULL, NULL, NULL, NULL, da, ptr, len, len, &vp); + talloc_free(ptr); + + if (rlen < 0) return false; + + if ((size_t) rlen < len) { + free_vp: + fr_pair_list_free(&vp); + return false; + } + + /* + * Was still parsed as an unknown attribute. + */ + if (vp->da->flags.is_unknown) goto free_vp; + + /* + * Set the RHS to the PARSED name, not the crap octet + * string which was input. + */ + map->rhs = tmpl_alloc(map, TMPL_TYPE_DATA, NULL, 0); + if (!map->rhs) goto free_vp; + + map->rhs->tmpl_data_type = da->type; + map->rhs->tmpl_data_length = vp->vp_length; + if (vp->da->flags.is_pointer) { + if (vp->da->type == PW_TYPE_STRING) { + map->rhs->tmpl_data_value.ptr = talloc_bstrndup(map->rhs, vp->data.ptr, vp->vp_length); + } else { + map->rhs->tmpl_data_value.ptr = talloc_memdup(map->rhs, vp->data.ptr, vp->vp_length); + } + } else { + memcpy(&map->rhs->tmpl_data_value, &vp->data, sizeof(map->rhs->tmpl_data_value)); + } + map->rhs->name = vp_aprints_value(map->rhs, vp, '"'); + map->rhs->len = talloc_array_length(map->rhs->name) - 1; + + /* + * Set the LHS to the REAL attribute name. + */ + vpt = tmpl_alloc(map, TMPL_TYPE_ATTR, map->lhs->tmpl_da->name, -1); + memcpy(&vpt->data.attribute, &map->lhs->data.attribute, sizeof(vpt->data.attribute)); + vpt->tmpl_da = da; + + /* + * Be sure to keep the "&control:" or "control:" prefix. + * If it's there, we re-generate it from whatever was in + * the original name, including the '&'. + */ + p = map->lhs->name; + if (*p == '&') p++; + len = radius_list_name(&list, p, PAIR_LIST_UNKNOWN); + + if (list != PAIR_LIST_UNKNOWN) { + rad_const_free(vpt->name); + + vpt->name = talloc_asprintf(vpt, "%.*s:%s", + (int) len, map->lhs->name, + map->lhs->tmpl_da->name); + vpt->len = strlen(vpt->name); + } + + talloc_free(map->lhs); + map->lhs = vpt; + + fr_pair_list_free(&vp); + + VERIFY_MAP(map); + + return true; +} + +/** Convert CONFIG_PAIR (which may contain refs) to vp_map_t. + * + * Treats the left operand as an attribute reference + * @verbatim<request>.<list>.<attribute>@endverbatim + * + * Treatment of left operand depends on quotation, barewords are treated as + * attribute references, double quoted values are treated as expandable strings, + * single quoted values are treated as literal strings. + * + * Return must be freed with talloc_free + * + * @param[in] ctx for talloc. + * @param[in] out Where to write the pointer to the new value_pair_map_struct. + * @param[in] cp to convert to map. + * @param[in] dst_request_def The default request to insert unqualified + * attributes into. + * @param[in] dst_list_def The default list to insert unqualified attributes + * into. + * @param[in] src_request_def The default request to resolve attribute + * references in. + * @param[in] src_list_def The default list to resolve unqualified attributes + * in. + * @return vp_map_t if successful or NULL on error. + */ +int map_afrom_cp(TALLOC_CTX *ctx, vp_map_t **out, CONF_PAIR *cp, + request_refs_t dst_request_def, pair_lists_t dst_list_def, + request_refs_t src_request_def, pair_lists_t src_list_def) +{ + vp_map_t *map; + char const *attr, *value; + ssize_t slen; + FR_TOKEN type; + + *out = NULL; + + if (!cp) return -1; + + map = talloc_zero(ctx, vp_map_t); + map->op = cf_pair_operator(cp); + map->ci = cf_pair_to_item(cp); + + attr = cf_pair_attr(cp); + value = cf_pair_value(cp); + if (!value) { + cf_log_err_cp(cp, "Missing attribute value"); + goto error; + } + + /* + * LHS may be an expansion (that expands to an attribute reference) + * or an attribute reference. Quoting determines which it is. + */ + type = cf_pair_attr_type(cp); + switch (type) { + case T_DOUBLE_QUOTED_STRING: + case T_BACK_QUOTED_STRING: + slen = tmpl_afrom_str(ctx, &map->lhs, attr, talloc_array_length(attr) - 1, + type, dst_request_def, dst_list_def, true); + if (slen <= 0) { + char *spaces, *text; + + marker: + fr_canonicalize_error(ctx, &spaces, &text, slen, attr); + cf_log_err_cp(cp, "%s", text); + cf_log_err_cp(cp, "%s^ %s", spaces, fr_strerror()); + + talloc_free(spaces); + talloc_free(text); + goto error; + } + break; + + case T_BARE_WORD: + /* + * Foo = %{...} + * + * Not allowed! + */ + if ((attr[0] == '%') && (attr[1] == '{')) { + cf_log_err_cp(cp, "Bare expansions are not permitted. They must be in a double-quoted string."); + goto error; + } + /* FALL-THROUGH */ + + default: + slen = tmpl_afrom_attr_str(ctx, &map->lhs, attr, dst_request_def, dst_list_def, true, true); + if (slen <= 0) { + cf_log_err_cp(cp, "Failed parsing attribute reference"); + + goto marker; + } + + if (tmpl_define_unknown_attr(map->lhs) < 0) { + cf_log_err_cp(cp, "Failed creating attribute %s: %s", + map->lhs->name, fr_strerror()); + goto error; + } + + break; + } + + /* + * RHS might be an attribute reference. + */ + type = cf_pair_value_type(cp); + + if ((map->lhs->type == TMPL_TYPE_ATTR) && + map->lhs->tmpl_da->flags.is_unknown && + !map_cast_from_hex(map, type, value)) { + goto error; + + } else { + slen = tmpl_afrom_str(map, &map->rhs, value, strlen(value), type, src_request_def, src_list_def, true); + if (slen < 0) goto marker; + if (tmpl_define_unknown_attr(map->rhs) < 0) { + cf_log_err_cp(cp, "Failed creating attribute %s: %s", map->rhs->name, fr_strerror()); + goto error; + } + } + if (!map->rhs) { + cf_log_err_cp(cp, "%s", fr_strerror()); + goto error; + } + + if (map->rhs->type == TMPL_TYPE_ATTR) { + /* + * We cannot assign a count to an attribute. That must + * be done in an xlat. + */ + if (map->rhs->tmpl_num == NUM_COUNT) { + cf_log_err_cp(cp, "Cannot assign from a count"); + goto error; + } + + if (map->rhs->tmpl_da->flags.virtual) { + cf_log_err_cp(cp, "Virtual attributes must be in an expansion such as \"%%{%s}\".", map->rhs->tmpl_da->name); + goto error; + } + } + + VERIFY_MAP(map); + + *out = map; + + return 0; + +error: + talloc_free(map); + return -1; +} + +/** Convert an 'update' config section into an attribute map. + * + * Uses 'name2' of section to set default request and lists. + * + * @param[in] cs the update section + * @param[out] out Where to store the head of the map. + * @param[in] dst_list_def The default destination list, usually dictated by + * the section the module is being called in. + * @param[in] src_list_def The default source list, usually dictated by the + * section the module is being called in. + * @param[in] validate map using this callback (may be NULL). + * @param[in] ctx to pass to callback. + * @param[in] max number of mappings to process. + * @return -1 on error, else 0. + */ +int map_afrom_cs(vp_map_t **out, CONF_SECTION *cs, + pair_lists_t dst_list_def, pair_lists_t src_list_def, + map_validate_t validate, void *ctx, + unsigned int max) +{ + char const *cs_list, *p; + + request_refs_t request_def = REQUEST_CURRENT; + + CONF_ITEM *ci; + CONF_PAIR *cp; + + unsigned int total = 0; + vp_map_t **tail, *map; + TALLOC_CTX *parent; + + *out = NULL; + tail = out; + + /* + * The first map has cs as the parent. + * The rest have the previous map as the parent. + */ + parent = cs; + + ci = cf_section_to_item(cs); + + cs_list = p = cf_section_name2(cs); + if (cs_list) { + p += radius_request_name(&request_def, p, REQUEST_CURRENT); + if (request_def == REQUEST_UNKNOWN) { + cf_log_err(ci, "Default request specified in mapping section is invalid"); + return -1; + } + + dst_list_def = fr_str2int(pair_lists, p, PAIR_LIST_UNKNOWN); + if (dst_list_def == PAIR_LIST_UNKNOWN) { + cf_log_err(ci, "Default list \"%s\" specified " + "in mapping section is invalid", p); + return -1; + } + } + + for (ci = cf_item_find_next(cs, NULL); + ci != NULL; + ci = cf_item_find_next(cs, ci)) { + if (total++ == max) { + cf_log_err(ci, "Map size exceeded"); + error: + TALLOC_FREE(*out); + return -1; + } + + if (!cf_item_is_pair(ci)) { + cf_log_err(ci, "Entry is not in \"attribute = value\" format"); + goto error; + } + + cp = cf_item_to_pair(ci); + if (map_afrom_cp(parent, &map, cp, request_def, dst_list_def, REQUEST_CURRENT, src_list_def) < 0) { + goto error; + } + + VERIFY_MAP(map); + + /* + * Check the types in the map are valid + */ + if (validate && (validate(map, ctx) < 0)) goto error; + + parent = *tail = map; + tail = &(map->next); + } + + return 0; + +} + + +/** Convert strings to vp_map_t + * + * Treatment of operands depends on quotation, barewords are treated + * as attribute references, double quoted values are treated as + * expandable strings, single quoted values are treated as literal + * strings. + * + * Return must be freed with talloc_free + * + * @param[in] ctx for talloc + * @param[out] out Where to store the head of the map. + * @param[in] lhs of the operation + * @param[in] lhs_type type of the LHS string + * @param[in] op the operation to perform + * @param[in] rhs of the operation + * @param[in] rhs_type type of the RHS string + * @param[in] dst_request_def The default request to insert unqualified + * attributes into. + * @param[in] dst_list_def The default list to insert unqualified attributes + * into. + * @param[in] src_request_def The default request to resolve attribute + * references in. + * @param[in] src_list_def The default list to resolve unqualified attributes + * in. + * @return vp_map_t if successful or NULL on error. + */ +int map_afrom_fields(TALLOC_CTX *ctx, vp_map_t **out, char const *lhs, FR_TOKEN lhs_type, + FR_TOKEN op, char const *rhs, FR_TOKEN rhs_type, + request_refs_t dst_request_def, + pair_lists_t dst_list_def, + request_refs_t src_request_def, + pair_lists_t src_list_def) +{ + ssize_t slen; + vp_map_t *map; + + map = talloc_zero(ctx, vp_map_t); + + slen = tmpl_afrom_str(map, &map->lhs, lhs, strlen(lhs), lhs_type, dst_request_def, dst_list_def, true); + if (slen < 0) { + error: + talloc_free(map); + return -1; + } + + map->op = op; + + if ((map->lhs->type == TMPL_TYPE_ATTR) && + map->lhs->tmpl_da->flags.is_unknown && + map_cast_from_hex(map, rhs_type, rhs)) { + return 0; + } + + slen = tmpl_afrom_str(map, &map->rhs, rhs, strlen(rhs), rhs_type, src_request_def, src_list_def, true); + if (slen < 0) goto error; + + VERIFY_MAP(map); + + *out = map; + + return 0; +} + +/** Convert a value pair string to valuepair map + * + * Takes a valuepair string with list and request qualifiers and converts it into a + * vp_map_t. + * + * @param ctx where to allocate the map. + * @param out Where to write the new map (must be freed with talloc_free()). + * @param vp_str string to parse. + * @param dst_request_def to use if attribute isn't qualified. + * @param dst_list_def to use if attribute isn't qualified. + * @param src_request_def to use if attribute isn't qualified. + * @param src_list_def to use if attribute isn't qualified. + * @return 0 on success, < 0 on error. + */ +int map_afrom_attr_str(TALLOC_CTX *ctx, vp_map_t **out, char const *vp_str, + request_refs_t dst_request_def, pair_lists_t dst_list_def, + request_refs_t src_request_def, pair_lists_t src_list_def) +{ + char const *p = vp_str; + FR_TOKEN quote; + + VALUE_PAIR_RAW raw; + vp_map_t *map = NULL; + + quote = gettoken(&p, raw.l_opand, sizeof(raw.l_opand), false); + switch (quote) { + case T_BARE_WORD: + break; + + case T_INVALID: + error: + return -1; + + default: + fr_strerror_printf("Left operand must be an attribute"); + return -1; + } + + raw.op = getop(&p); + if (raw.op == T_INVALID) goto error; + + raw.quote = gettoken(&p, raw.r_opand, sizeof(raw.r_opand), false); + if (raw.quote == T_INVALID) goto error; + if (!fr_str_tok[raw.quote]) { + fr_strerror_printf("Right operand must be an attribute or string"); + return -1; + } + + if (map_afrom_fields(ctx, &map, raw.l_opand, T_BARE_WORD, raw.op, raw.r_opand, raw.quote, + dst_request_def, dst_list_def, src_request_def, src_list_def) < 0) { + return -1; + } + + rad_assert(map != NULL); + *out = map; + + VERIFY_MAP(map); + + return 0; +} + +/** Compare map where LHS is #TMPL_TYPE_ATTR + * + * Compares maps by lhs->tmpl_da, lhs->tmpl_tag, lhs->tmpl_num + * + * @note both map->lhs must be #TMPL_TYPE_ATTR. + * + * @param a first map. + * @param b second map. + */ +int8_t map_cmp_by_lhs_attr(void const *a, void const *b) +{ + vp_tmpl_t const *my_a = ((vp_map_t const *)a)->lhs; + vp_tmpl_t const *my_b = ((vp_map_t const *)b)->lhs; + + VERIFY_TMPL(my_a); + VERIFY_TMPL(my_b); + + uint8_t cmp; + + rad_assert(my_a->type == TMPL_TYPE_ATTR); + rad_assert(my_b->type == TMPL_TYPE_ATTR); + + cmp = fr_pointer_cmp(my_a->tmpl_da, my_b->tmpl_da); + if (cmp != 0) return cmp; + + if (my_a->tmpl_tag < my_b->tmpl_tag) return -1; + + if (my_a->tmpl_tag > my_b->tmpl_tag) return 1; + + if (my_a->tmpl_num < my_b->tmpl_num) return -1; + + if (my_a->tmpl_num > my_b->tmpl_num) return 1; + + return 0; +} + +static void map_sort_split(vp_map_t *source, vp_map_t **front, vp_map_t **back) +{ + vp_map_t *fast; + vp_map_t *slow; + + /* + * Stopping condition - no more elements left to split + */ + if (!source || !source->next) { + *front = source; + *back = NULL; + + return; + } + + /* + * Fast advances twice as fast as slow, so when it gets to the end, + * slow will point to the middle of the linked list. + */ + slow = source; + fast = source->next; + + while (fast) { + fast = fast->next; + if (fast) { + slow = slow->next; + fast = fast->next; + } + } + + *front = source; + *back = slow->next; + slow->next = NULL; +} + +static vp_map_t *map_sort_merge(vp_map_t *a, vp_map_t *b, fr_cmp_t cmp) +{ + vp_map_t *result = NULL; + + if (!a) return b; + if (!b) return a; + + /* + * Compare things in the maps + */ + if (cmp(a, b) <= 0) { + result = a; + result->next = map_sort_merge(a->next, b, cmp); + } else { + result = b; + result->next = map_sort_merge(a, b->next, cmp); + } + + return result; +} + +/** Sort a linked list of #vp_map_t using merge sort + * + * @param[in,out] maps List of #vp_map_t to sort. + * @param[in] cmp to sort with + */ +void map_sort(vp_map_t **maps, fr_cmp_t cmp) +{ + vp_map_t *head = *maps; + vp_map_t *a; + vp_map_t *b; + + /* + * If there's 0-1 elements it must already be sorted. + */ + if (!head || !head->next) { + return; + } + + map_sort_split(head, &a, &b); /* Split into sublists */ + map_sort(&a, cmp); /* Traverse left */ + map_sort(&b, cmp); /* Traverse right */ + + /* + * merge the two sorted lists together + */ + *maps = map_sort_merge(a, b, cmp); +} + +/** Process map which has exec as a src + * + * Evaluate maps which specify exec as a src. This may be used by various sorts of update sections, + * and so has been broken out into it's own function. + * + * @param[in,out] ctx to allocate new #VALUE_PAIR (s) in. + * @param[out] out Where to write the #VALUE_PAIR (s). + * @param[in] request structure (used only for talloc). + * @param[in] map the map. The LHS (dst) must be TMPL_TYPE_ATTR or TMPL_TYPE_LIST. The RHS (src) + * must be TMPL_TYPE_EXEC. + * @return -1 on failure, 0 on success. + */ +static int map_exec_to_vp(TALLOC_CTX *ctx, VALUE_PAIR **out, REQUEST *request, vp_map_t const *map) +{ + int result; + char *expanded = NULL; + char answer[1024]; + VALUE_PAIR **input_pairs = NULL; + VALUE_PAIR *output_pairs = NULL; + + *out = NULL; + + VERIFY_MAP(map); + + rad_assert(map->rhs->type == TMPL_TYPE_EXEC); + rad_assert((map->lhs->type == TMPL_TYPE_ATTR) || (map->lhs->type == TMPL_TYPE_LIST)); + + /* + * We always put the request pairs into the environment + */ + input_pairs = radius_list(request, PAIR_LIST_REQUEST); + + /* + * Automagically switch output type depending on our destination + * If dst is a list, then we create attributes from the output of the program + * if dst is an attribute, then we create an attribute of that type and then + * call fr_pair_value_from_str on the output of the script. + */ + result = radius_exec_program(ctx, answer, sizeof(answer), + (map->lhs->type == TMPL_TYPE_LIST) ? &output_pairs : NULL, + request, map->rhs->name, input_pairs ? *input_pairs : NULL, + true, true, EXEC_TIMEOUT); + talloc_free(expanded); + if (result != 0) { + talloc_free(output_pairs); + return -1; + } + + switch (map->lhs->type) { + case TMPL_TYPE_LIST: + if (!output_pairs) { + REDEBUG("No valid attributes received from program"); + return -2; + } + *out = output_pairs; + return 0; + + case TMPL_TYPE_ATTR: + { + VALUE_PAIR *vp; + + vp = fr_pair_afrom_da(ctx, map->lhs->tmpl_da); + if (!vp) return -1; + vp->op = map->op; + vp->tag = map->lhs->tmpl_tag; + if (fr_pair_value_from_str(vp, answer, -1) < 0) { + fr_pair_list_free(&vp); + return -2; + } + *out = vp; + + return 0; + } + + default: + rad_assert(0); + } + + return -1; +} + +/** Convert a map to a VALUE_PAIR. + * + * @param[in,out] ctx to allocate #VALUE_PAIR (s) in. + * @param[out] out Where to write the #VALUE_PAIR (s), which may be NULL if not found + * @param[in] request The current request. + * @param[in] map the map. The LHS (dst) has to be #TMPL_TYPE_ATTR or #TMPL_TYPE_LIST. + * @param[in] uctx unused. + * @return + * - 0 on success. + * - -1 on failure. + */ +int map_to_vp(TALLOC_CTX *ctx, VALUE_PAIR **out, REQUEST *request, vp_map_t const *map, UNUSED void *uctx) +{ + int rcode = 0; + ssize_t len; + VALUE_PAIR *vp = NULL, *new, *found = NULL; + REQUEST *context = request; + vp_cursor_t cursor; + ssize_t slen; + char *str; + + *out = NULL; + + VERIFY_MAP(map); + rad_assert(map->lhs != NULL); + rad_assert(map->rhs != NULL); + + rad_assert((map->lhs->type == TMPL_TYPE_LIST) || (map->lhs->type == TMPL_TYPE_ATTR)); + + /* + * Special case for !*, we don't need to parse RHS as this is a unary operator. + */ + if (map->op == T_OP_CMP_FALSE) return 0; + + /* + * List to list found, this is a special case because we don't need + * to allocate any attributes, just finding the current list, and change + * the op. + */ + if ((map->lhs->type == TMPL_TYPE_LIST) && (map->rhs->type == TMPL_TYPE_LIST)) { + VALUE_PAIR **from = NULL; + + if (radius_request(&context, map->rhs->tmpl_request) == 0) { + from = radius_list(context, map->rhs->tmpl_list); + } + if (!from) return 0; + + found = fr_pair_list_copy(ctx, *from); + + /* + * List to list copy is empty if the src list has no attributes. + */ + if (!found) return 0; + + for (vp = fr_cursor_init(&cursor, &found); + vp; + vp = fr_cursor_next(&cursor)) { + vp->op = T_OP_ADD; + } + + *out = found; + + return 0; + } + + /* + * And parse the RHS + */ + switch (map->rhs->type) { + case TMPL_TYPE_XLAT_STRUCT: + rad_assert(map->lhs->type == TMPL_TYPE_ATTR); + rad_assert(map->lhs->tmpl_da); /* We need to know which attribute to create */ + rad_assert(map->rhs->tmpl_xlat != NULL); + + new = fr_pair_afrom_da(ctx, map->lhs->tmpl_da); + if (!new) return -1; + + str = NULL; + slen = radius_axlat_struct(&str, request, map->rhs->tmpl_xlat, NULL, NULL); + if (slen < 0) { + rcode = slen; + goto error; + } + + /* + * We do the debug printing because radius_axlat_struct + * doesn't have access to the original string. It's been + * mangled during the parsing to xlat_exp_t + */ + RDEBUG2("EXPAND %s", map->rhs->name); + RDEBUG2(" --> %s", str); + + rcode = fr_pair_value_from_str(new, str, -1); + talloc_free(str); + if (rcode < 0) { + fr_pair_list_free(&new); + goto error; + } + new->op = map->op; + new->tag = map->lhs->tmpl_tag; + *out = new; + break; + + case TMPL_TYPE_XLAT: + rad_assert(map->lhs->type == TMPL_TYPE_ATTR); + rad_assert(map->lhs->tmpl_da); /* We need to know which attribute to create */ + + new = fr_pair_afrom_da(ctx, map->lhs->tmpl_da); + if (!new) return -1; + + str = NULL; + slen = radius_axlat(&str, request, map->rhs->name, NULL, NULL); + if (slen < 0) { + rcode = slen; + goto error; + } + + rcode = fr_pair_value_from_str(new, str, -1); + talloc_free(str); + if (rcode < 0) { + fr_pair_list_free(&new); + goto error; + } + new->op = map->op; + new->tag = map->lhs->tmpl_tag; + *out = new; + break; + + case TMPL_TYPE_LITERAL: + rad_assert(map->lhs->type == TMPL_TYPE_ATTR); + rad_assert(map->lhs->tmpl_da); /* We need to know which attribute to create */ + + new = fr_pair_afrom_da(ctx, map->lhs->tmpl_da); + if (!new) return -1; + + if (fr_pair_value_from_str(new, map->rhs->name, -1) < 0) { + rcode = 0; + goto error; + } + new->op = map->op; + new->tag = map->lhs->tmpl_tag; + *out = new; + break; + + case TMPL_TYPE_ATTR: + { + vp_cursor_t from; + + rad_assert(((map->lhs->type == TMPL_TYPE_ATTR) && map->lhs->tmpl_da) || + ((map->lhs->type == TMPL_TYPE_LIST) && !map->lhs->tmpl_da)); + + /* + * @todo should log error, and return -1 for v3.1 (causes update to fail) + */ + if (tmpl_copy_vps(ctx, &found, request, map->rhs) < 0) return 0; + + vp = fr_cursor_init(&from, &found); + + /* + * Src/Dst attributes don't match, convert src attributes + * to match dst. + */ + if ((map->lhs->type == TMPL_TYPE_ATTR) && + (map->rhs->tmpl_da->type != map->lhs->tmpl_da->type)) { + vp_cursor_t to; + + (void) fr_cursor_init(&to, out); + for (; vp; vp = fr_cursor_next(&from)) { + new = fr_pair_afrom_da(ctx, map->lhs->tmpl_da); + if (!new) return -1; + + len = value_data_cast(new, &new->data, new->da->type, new->da, + vp->da->type, vp->da, &vp->data, vp->vp_length); + if (len < 0) { + REDEBUG("Attribute conversion failed: %s", fr_strerror()); + fr_pair_list_free(&found); + fr_pair_list_free(&new); + return -1; + } + + new->vp_length = len; + vp = fr_cursor_remove(&from); + talloc_free(vp); + + if (new->da->type == PW_TYPE_STRING) { + rad_assert(new->vp_strvalue != NULL); + } + + new->op = map->op; + new->tag = map->lhs->tmpl_tag; + fr_cursor_insert(&to, new); + } + return 0; + } + + /* + * Otherwise we just need to fixup the attribute types + * and operators + */ + for (; vp; vp = fr_cursor_next(&from)) { + vp->da = map->lhs->tmpl_da; + vp->op = map->op; + vp->tag = map->lhs->tmpl_tag; + } + *out = found; + } + break; + + case TMPL_TYPE_DATA: + rad_assert(map->lhs->tmpl_da); + rad_assert(map->lhs->type == TMPL_TYPE_ATTR); + rad_assert(map->lhs->tmpl_da->type == map->rhs->tmpl_data_type); + + new = fr_pair_afrom_da(ctx, map->lhs->tmpl_da); + if (!new) return -1; + + len = value_data_copy(new, &new->data, new->da->type, &map->rhs->tmpl_data_value, + map->rhs->tmpl_data_length); + if (len < 0) goto error; + + new->vp_length = len; + new->op = map->op; + new->tag = map->lhs->tmpl_tag; + *out = new; + + VERIFY_MAP(map); + break; + + /* + * This essentially does the same as rlm_exec xlat, except it's non-configurable. + * It's only really here as a convenience for people who expect the contents of + * backticks to be executed in a shell. + * + * exec string is xlat expanded and arguments are shell escaped. + */ + case TMPL_TYPE_EXEC: + return map_exec_to_vp(ctx, out, request, map); + + default: + rad_assert(0); /* Should have been caught at parse time */ + + error: + fr_pair_list_free(&vp); + return rcode; + } + + return 0; +} + +#define DEBUG_OVERWRITE(_old, _new) \ +do {\ + if (RDEBUG_ENABLED3) {\ + char *old = vp_aprints_value(request, _old, '"');\ + char *new = vp_aprints_value(request, _new, '"');\ + RDEBUG3("Overwriting value \"%s\" with \"%s\"", old, new);\ + talloc_free(old);\ + talloc_free(new);\ + }\ +} while (0) + +/** Convert vp_map_t to VALUE_PAIR(s) and add them to a REQUEST. + * + * Takes a single vp_map_t, resolves request and list identifiers + * to pointers in the current request, then attempts to retrieve module + * specific value(s) using callback, and adds the resulting values to the + * correct request/list. + * + * @param request The current request. + * @param map specifying destination attribute and location and src identifier. + * @param func to retrieve module specific values and convert them to + * VALUE_PAIRS. + * @param ctx to be passed to func. + * @return -1 if the operation failed, -2 in the source attribute wasn't valid, 0 on success. + */ +int map_to_request(REQUEST *request, vp_map_t const *map, radius_map_getvalue_t func, void *ctx) +{ + int rcode = 0; + int num; + VALUE_PAIR **list, *vp, *dst, *head = NULL; + bool found = false; + REQUEST *context; + TALLOC_CTX *parent; + vp_cursor_t dst_list, src_list; + + vp_map_t exp_map; + vp_tmpl_t exp_lhs; + + VERIFY_MAP(map); + rad_assert(map->lhs != NULL); + rad_assert(map->rhs != NULL); + + /* + * Preprocessing of the LHS of the map. + */ + switch (map->lhs->type) { + /* + * Already in the correct form. + */ + case TMPL_TYPE_LIST: + case TMPL_TYPE_ATTR: + break; + + /* + * Everything else gets expanded, then re-parsed as an + * attribute reference. + */ + case TMPL_TYPE_XLAT: + case TMPL_TYPE_XLAT_STRUCT: + case TMPL_TYPE_EXEC: + { + char *attr; + ssize_t slen; + + slen = tmpl_aexpand(request, &attr, request, map->lhs, NULL, NULL); + if (slen <= 0) { + REDEBUG("Left side \"%.*s\" of map failed expansion", (int)map->lhs->len, map->lhs->name); + return -1; + } + + slen = tmpl_from_attr_str(&exp_lhs, attr, REQUEST_CURRENT, PAIR_LIST_REQUEST, false, false) ; + if (slen <= 0) { + REDEBUG("Left side \"%.*s\" expansion not an attribute reference: %s", + (int)map->lhs->len, map->lhs->name, fr_strerror()); + talloc_free(attr); + return -1; + } + rad_assert((exp_lhs.type == TMPL_TYPE_ATTR) || (exp_lhs.type == TMPL_TYPE_LIST)); + + memcpy(&exp_map, map, sizeof(exp_map)); + exp_map.lhs = &exp_lhs; + map = &exp_map; + } + break; + + default: + rad_assert(0); + break; + } + + + /* + * Sanity check inputs. We can have a list or attribute + * as a destination. + */ + if ((map->lhs->type != TMPL_TYPE_LIST) && + (map->lhs->type != TMPL_TYPE_ATTR)) { + REDEBUG("Left side \"%.*s\" of map should be an attr or list but is an %s", + (int)map->lhs->len, map->lhs->name, + fr_int2str(tmpl_names, map->lhs->type, "<INVALID>")); + return -2; + } + + context = request; + if (radius_request(&context, map->lhs->tmpl_request) < 0) { + REDEBUG("Mapping \"%.*s\" -> \"%.*s\" invalid in this context", + (int)map->rhs->len, map->rhs->name, (int)map->lhs->len, map->lhs->name); + return -2; + } + + /* + * If there's no CoA packet and we're updating it, + * auto-allocate it. + */ + if (((map->lhs->tmpl_list == PAIR_LIST_COA) || + (map->lhs->tmpl_list == PAIR_LIST_DM)) && !request->coa) { + if (request->parent) { + REDEBUG("You can only do 'update coa' when processing a packet which was received from the network"); + return -2; + } + + if ((request->packet->code == PW_CODE_COA_REQUEST) || + (request->packet->code == PW_CODE_DISCONNECT_REQUEST)) { + REDEBUG("You cannot do 'update coa' when processing a CoA / Disconnect request. Use 'update request' instead."); + return -2; + } + + if (!request_alloc_coa(context)) { + REDEBUG("Failed to create a CoA/Disconnect Request message"); + return -2; + } + context->coa->proxy->code = (map->lhs->tmpl_list == PAIR_LIST_COA) ? + PW_CODE_COA_REQUEST : + PW_CODE_DISCONNECT_REQUEST; + } + + list = radius_list(context, map->lhs->tmpl_list); + if (!list) { + REDEBUG("Mapping \"%.*s\" -> \"%.*s\" invalid in this context", + (int)map->rhs->len, map->rhs->name, (int)map->lhs->len, map->lhs->name); + + return -2; + } + + parent = radius_list_ctx(context, map->lhs->tmpl_list); + if (!parent) { + REDEBUG("Unable to set parent list"); + return -1; + } + + /* + * The callback should either return -1 to signify operations error, + * -2 when it can't find the attribute or list being referenced, or + * 0 to signify success. It may return "success", but still have no + * VPs to work with. + */ + if (map->rhs->type != TMPL_TYPE_NULL) { + rcode = func(parent, &head, request, map, ctx); + if (rcode < 0) { + rad_assert(!head); + return rcode; + } + if (!head) { + RDEBUG2("No attributes updated for RHS %s", map->rhs->name); + return rcode; + } + } else { + if (rad_debug_lvl) map_debug_log(request, map, NULL); + } + + /* + * Print the VPs + */ + for (vp = fr_cursor_init(&src_list, &head); + vp; + vp = fr_cursor_next(&src_list)) { + VERIFY_VP(vp); + + if (rad_debug_lvl) map_debug_log(request, map, vp); + } + + /* + * The destination is a list (which is a completely different set of operations) + */ + if (map->lhs->type == TMPL_TYPE_LIST) { + switch (map->op) { + case T_OP_CMP_FALSE: + /* We don't need the src VPs (should just be 'ANY') */ + rad_assert(!head); + + /* Clear the entire dst list */ + fr_pair_list_free(list); + + if (map->lhs->tmpl_list == PAIR_LIST_REQUEST) { + context->username = NULL; + context->password = NULL; + } + return 0; + + case T_OP_SET: + if (map->rhs->type == TMPL_TYPE_LIST) { + fr_pair_list_free(list); + *list = head; + head = NULL; + } else { /* FALL-THROUGH */ + case T_OP_EQ: + rad_assert(map->rhs->type == TMPL_TYPE_EXEC); + /* FALL-THROUGH */ + case T_OP_ADD: + fr_pair_list_move(parent, list, &head, map->op); + fr_pair_list_free(&head); + } + goto finish; + case T_OP_PREPEND: + fr_pair_list_move(parent, list, &head, T_OP_PREPEND); + fr_pair_list_free(&head); + goto finish; + + default: + fr_pair_list_free(&head); + return -1; + } + } + + /* + * Find the destination attribute. We leave with either + * the dst_list and vp pointing to the attribute or the VP + * being NULL (no attribute at that index). + */ + num = map->lhs->tmpl_num; + (void) fr_cursor_init(&dst_list, list); + if ((num != NUM_ANY) && (num > 0)) { + while ((dst = fr_cursor_next_by_da(&dst_list, map->lhs->tmpl_da, map->lhs->tmpl_tag))) { + if (num <= 0) break; + num--; + } + } else { + dst = fr_cursor_next_by_da(&dst_list, map->lhs->tmpl_da, map->lhs->tmpl_tag); + } + rad_assert(!dst || (map->lhs->tmpl_da == dst->da)); + + /* + * The destination is an attribute + */ + switch (map->op) { + default: + break; + /* + * !* - Remove all attributes which match dst in the specified list. + * This doesn't use attributes returned by the func(), and immediately frees them. + */ + case T_OP_CMP_FALSE: + /* We don't need the src VPs (should just be 'ANY') */ + rad_assert(!head); + if (!dst) return 0; + + /* + * Wildcard: delete all of the matching ones, based on tag. + */ + if (map->lhs->tmpl_num == NUM_ANY) { + fr_pair_delete_by_num(list, map->lhs->tmpl_da->attr, map->lhs->tmpl_da->vendor, map->lhs->tmpl_tag); + dst = NULL; + /* + * We've found the Nth one. Delete it, and only it. + */ + } else { + dst = fr_cursor_remove(&dst_list); + fr_pair_list_free(&dst); + } + + /* + * Check that the User-Name and User-Password + * caches point to the correct attribute. + */ + goto finish; + + /* + * -= - Delete attributes in the dst list which match any of the + * src_list attributes. + * + * This operation has two modes: + * - If map->lhs->tmpl_num > 0, we check each of the src_list attributes against + * the dst attribute, to see if any of their values match. + * - If map->lhs->tmpl_num == NUM_ANY, we compare all instances of the dst attribute + * against each of the src_list attributes. + */ + case T_OP_SUB: + /* We didn't find any attributes earlier */ + if (!dst) { + fr_pair_list_free(&head); + return 0; + } + + /* + * Instance specific[n] delete + */ + if (map->lhs->tmpl_num != NUM_ANY) { + for (vp = fr_cursor_first(&src_list); + vp; + vp = fr_cursor_next(&src_list)) { + head->op = T_OP_CMP_EQ; + rcode = radius_compare_vps(request, vp, dst); + if (rcode == 0) { + dst = fr_cursor_remove(&dst_list); + fr_pair_list_free(&dst); + found = true; + } + } + fr_pair_list_free(&head); + if (!found) return 0; + goto finish; + } + + /* + * All instances[*] delete + */ + for (dst = fr_cursor_current(&dst_list); + dst; + dst = fr_cursor_next_by_da(&dst_list, map->lhs->tmpl_da, map->lhs->tmpl_tag)) { + for (vp = fr_cursor_first(&src_list); + vp; + vp = fr_cursor_next(&src_list)) { + head->op = T_OP_CMP_EQ; + rcode = radius_compare_vps(request, vp, dst); + if (rcode == 0) { + dst = fr_cursor_remove(&dst_list); + fr_pair_list_free(&dst); + found = true; + } + } + } + fr_pair_list_free(&head); + if (!found) return 0; + goto finish; + } + + /* + * Another fixup pass to set tags on attributes were about to insert + */ + if (map->lhs->tmpl_tag != TAG_ANY) { + for (vp = fr_cursor_init(&src_list, &head); + vp; + vp = fr_cursor_next(&src_list)) { + vp->tag = map->lhs->tmpl_tag; + } + } + + switch (map->op) { + /* + * = - Set only if not already set + */ + case T_OP_EQ: + if (dst) { + RDEBUG3("Refusing to overwrite (use :=)"); + fr_pair_list_free(&head); + return 0; + } + + /* Insert first instance (if multiple) */ + fr_cursor_first(&src_list); + fr_cursor_insert(&dst_list, fr_cursor_remove(&src_list)); + /* Free any we didn't insert */ + fr_pair_list_free(&head); + break; + + /* + * := - Overwrite existing attribute with last src_list attribute + */ + case T_OP_SET: + /* Wind to last instance */ + fr_cursor_last(&src_list); + if (dst) { + DEBUG_OVERWRITE(dst, fr_cursor_current(&src_list)); + dst = fr_cursor_replace(&dst_list, fr_cursor_remove(&src_list)); + fr_pair_list_free(&dst); + } else { + fr_cursor_insert(&dst_list, fr_cursor_remove(&src_list)); + } + /* Free any we didn't insert */ + fr_pair_list_free(&head); + break; + + /* + * ^= - Prepend src_list attributes to the destination + */ + case T_OP_PREPEND: + fr_pair_prepend(list, head); + head = NULL; + break; + + /* + * += - Add all src_list attributes to the destination + */ + case T_OP_ADD: + /* Insert all the instances! (if multiple) */ + fr_pair_add(list, head); + head = NULL; + break; + + /* + * Filter operators + */ + case T_OP_REG_NE: + case T_OP_NE: + case T_OP_REG_EQ: + case T_OP_CMP_EQ: + case T_OP_GE: + case T_OP_GT: + case T_OP_LE: + case T_OP_LT: + { + VALUE_PAIR *a, *b; + + fr_pair_list_sort(&head, fr_pair_cmp_by_da_tag); + fr_pair_list_sort(list, fr_pair_cmp_by_da_tag); + + fr_cursor_first(&dst_list); + + for (b = fr_cursor_first(&src_list); + b; + b = fr_cursor_next(&src_list)) { + found = false; + + for (a = fr_cursor_current(&dst_list); + a; + a = fr_cursor_next(&dst_list)) { + int8_t cmp; + + cmp = fr_pair_cmp_by_da_tag(a, b); /* attribute and tag match */ + if (cmp > 0) break; + else if (cmp < 0) continue; + + /* + * The LHS exists. We need to + * limit it's value based on the + * operator, and on the value of + * the RHS. + */ + cmp = (value_data_cmp_op(map->op, a->da->type, &a->data, a->vp_length, b->da->type, &b->data, b->vp_length) == 0); + if (cmp == 1) switch (map->op) { + + /* + * Keep only matching attributes. + */ + default: + case T_OP_REG_NE: + case T_OP_NE: + case T_OP_REG_EQ: + case T_OP_CMP_EQ: + a = fr_cursor_remove(&dst_list); + talloc_free(a); + break; + + /* + * Keep matching + * attribute, and enforce + * matching values. + */ + case T_OP_GE: + case T_OP_GT: + case T_OP_LE: + case T_OP_LT: + DEBUG_OVERWRITE(a, b); + (void) value_data_copy(a, &a->data, a->da->type, + &b->data, b->vp_length); + found = true; + break; + } + } + + /* + * End of the dst list. + */ + if (!a) { + if (found) break; + + switch (map->op) { + default: + break; + + /* + * It wasn't found. Insert it with the given value. + */ + case T_OP_GE: + case T_OP_GT: + case T_OP_LE: + case T_OP_LT: + (void) fr_cursor_insert(&dst_list, fr_pair_copy(parent, b)); + break; + } + break; + } + } + fr_pair_list_free(&head); + } + break; + + default: + rad_assert(0); /* Should have been caught be the caller */ + return -1; + } + +finish: + rad_assert(!head); + + /* + * Update the cached username && password. This is code + * we execute on EVERY update (sigh) so that SOME modules + * MIGHT NOT have to do the search themselves. + * + * TBH, we should probably make each module just do the + * search themselves. + */ + if (map->lhs->tmpl_list == PAIR_LIST_REQUEST) { + context->username = NULL; + context->password = NULL; + + for (vp = fr_cursor_init(&src_list, list); + vp; + vp = fr_cursor_next(&src_list)) { + + if (vp->da->vendor != 0) continue; + if (vp->da->flags.has_tag) continue; + + if (!context->username && (vp->da->attr == PW_USER_NAME)) { + context->username = vp; + continue; + } + + if (vp->da->attr == PW_STRIPPED_USER_NAME) { + context->username = vp; + continue; + } + + if (vp->da->attr == PW_USER_PASSWORD) { + context->password = vp; + continue; + } + } + } + return 0; +} + +/** Check whether the destination of a map is currently valid + * + * @param request The current request. + * @param map to check. + * @return true if the map resolves to a request and list else false. + */ +bool map_dst_valid(REQUEST *request, vp_map_t const *map) +{ + REQUEST *context = request; + + VERIFY_MAP(map); + + if (radius_request(&context, map->lhs->tmpl_request) < 0) return false; + if (!radius_list(context, map->lhs->tmpl_list)) return false; + + return true; +} + +/** Print a map to a string + * + * @param[out] buffer for the output string + * @param[in] bufsize of the buffer + * @param[in] map to print + * @return the size of the string printed + */ +size_t map_prints(char *buffer, size_t bufsize, vp_map_t const *map) +{ + size_t len; + DICT_ATTR const *da = NULL; + char *p = buffer; + char *end = buffer + bufsize; + + VERIFY_MAP(map); + + if (map->lhs->type == TMPL_TYPE_ATTR) da = map->lhs->tmpl_da; + + len = tmpl_prints(buffer, bufsize, map->lhs, da); + p += len; + + *(p++) = ' '; + strlcpy(p, fr_token_name(map->op), end - p); + p += strlen(p); + *(p++) = ' '; + + /* + * The RHS doesn't matter for many operators + */ + if ((map->op == T_OP_CMP_TRUE) || + (map->op == T_OP_CMP_FALSE)) { + strlcpy(p, "ANY", (end - p)); + p += strlen(p); + return p - buffer; + } + + rad_assert(map->rhs != NULL); + + if ((map->lhs->type == TMPL_TYPE_ATTR) && + (map->lhs->tmpl_da->type == PW_TYPE_STRING) && + (map->rhs->type == TMPL_TYPE_LITERAL)) { + *(p++) = '\''; + len = tmpl_prints(p, end - p, map->rhs, da); + p += len; + *(p++) = '\''; + *p = '\0'; + } else { + len = tmpl_prints(p, end - p, map->rhs, da); + p += len; + } + + return p - buffer; +} + +/* + * Debug print a map / VP + */ +void map_debug_log(REQUEST *request, vp_map_t const *map, VALUE_PAIR const *vp) +{ + char *value; + char buffer[1024]; + + VERIFY_MAP(map); + rad_assert(map->lhs != NULL); + rad_assert(map->rhs != NULL); + + rad_assert(vp || (map->rhs->type == TMPL_TYPE_NULL)); + + switch (map->rhs->type) { + /* + * Just print the value being assigned + */ + default: + case TMPL_TYPE_LITERAL: + vp_prints_value(buffer, sizeof(buffer), vp, map->rhs->quote); + value = buffer; + break; + + case TMPL_TYPE_XLAT: + case TMPL_TYPE_XLAT_STRUCT: + vp_prints_value(buffer, sizeof(buffer), vp, map->rhs->quote); + value = buffer; + break; + + case TMPL_TYPE_DATA: + vp_prints_value(buffer, sizeof(buffer), vp, map->rhs->quote); + value = buffer; + break; + + /* + * For the lists, we can't use the original name, and have to + * rebuild it using tmpl_prints, for each attribute we're + * copying. + */ + case TMPL_TYPE_LIST: + { + char attr[256]; + char quote = '\0'; + vp_tmpl_t vpt; + /* + * Fudge a temporary tmpl that describes the attribute we're copying + * this is a combination of the original list tmpl, and values from + * the VALUE_PAIR. This way, we get tag info included. + */ + memcpy(&vpt, map->rhs, sizeof(vpt)); + vpt.tmpl_da = vp->da; + vpt.tmpl_tag = vp->tag; + vpt.type = TMPL_TYPE_ATTR; + + /* + * Not appropriate to use map->rhs->quote here, as that's the quoting + * around the list ref. The attribute value has no quoting, so we choose + * the quoting based on the data type, and whether it's printable. + */ + if (vp->da->type == PW_TYPE_STRING) quote = is_printable(vp->vp_strvalue, + vp->vp_length) ? '\'' : '"'; + vp_prints_value(buffer, sizeof(buffer), vp, quote); + tmpl_prints(attr, sizeof(attr), &vpt, vp->da); + value = talloc_typed_asprintf(request, "%s -> %s", attr, buffer); + } + break; + + case TMPL_TYPE_ATTR: + { + char quote = '\0'; + + /* + * Not appropriate to use map->rhs->quote here, as that's the quoting + * around the attr ref. The attribute value has no quoting, so we choose + * the quoting based on the data type, and whether it's printable. + */ + if (vp->da->type == PW_TYPE_STRING) quote = is_printable(vp->vp_strvalue, + vp->vp_length) ? '\'' : '"'; + vp_prints_value(buffer, sizeof(buffer), vp, quote); + value = talloc_typed_asprintf(request, "%.*s -> %s", (int)map->rhs->len, map->rhs->name, buffer); + } + break; + + case TMPL_TYPE_NULL: + strcpy(buffer, "ANY"); + value = buffer; + break; + } + + switch (map->lhs->type) { + case TMPL_TYPE_LIST: + RDEBUG("%.*s:%s %s %s", (int)map->lhs->len, map->lhs->name, vp ? vp->da->name : "", + fr_int2str(fr_tokens, vp ? vp->op : map->op, "<INVALID>"), value); + break; + + case TMPL_TYPE_ATTR: + RDEBUG("%s %s %s", map->lhs->name, + fr_int2str(fr_tokens, vp ? vp->op : map->op, "<INVALID>"), value); + break; + + default: + RDEBUG("map %s = %s", fr_int2str(tmpl_names, map->lhs->type, "???"), value); + break; + } + + if (value != buffer) talloc_free(value); +} diff --git a/src/main/modcall.c b/src/main/modcall.c new file mode 100644 index 0000000..aa6abf8 --- /dev/null +++ b/src/main/modcall.c @@ -0,0 +1,4041 @@ +/* + * @name modcall.c + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2000,2006 The FreeRADIUS server project + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/modpriv.h> +#include <freeradius-devel/modcall.h> +#include <freeradius-devel/parser.h> +#include <freeradius-devel/rad_assert.h> + + +/* mutually-recursive static functions need a prototype up front */ +static modcallable *do_compile_modgroup(modcallable *, + rlm_components_t, CONF_SECTION *, + int, int, int); + +/* Actions may be a positive integer (the highest one returned in the group + * will be returned), or the keyword "return", represented here by + * MOD_ACTION_RETURN, to cause an immediate return. + * There's also the keyword "reject", represented here by MOD_ACTION_REJECT + * to cause an immediate reject. */ +#define MOD_ACTION_RETURN (-1) +#define MOD_ACTION_REJECT (-2) + +/* Here are our basic types: modcallable, modgroup, and modsingle. For an + * explanation of what they are all about, see doc/configurable_failover.rst */ +struct modcallable { + modcallable *parent; + struct modcallable *next; + char const *name; + char const *debug_name; + enum { MOD_SINGLE = 1, MOD_GROUP, MOD_LOAD_BALANCE, MOD_REDUNDANT_LOAD_BALANCE, +#ifdef WITH_UNLANG + MOD_IF, MOD_ELSE, MOD_ELSIF, MOD_UPDATE, MOD_SWITCH, MOD_CASE, + MOD_FOREACH, MOD_BREAK, MOD_RETURN, +#endif + MOD_POLICY, MOD_REFERENCE, MOD_XLAT } type; + rlm_components_t method; + int actions[RLM_MODULE_NUMCODES]; +}; + +#define MOD_LOG_OPEN_BRACE RDEBUG2("%s {", c->debug_name) + +#define MOD_LOG_CLOSE_BRACE RDEBUG2("} # %s = %s", c->debug_name, fr_int2str(mod_rcode_table, result, "<invalid>")) + +typedef struct { + modcallable mc; /* self */ + enum { + GROUPTYPE_SIMPLE = 0, + GROUPTYPE_REDUNDANT, + GROUPTYPE_COUNT + } grouptype; /* after mc */ + modcallable *children; + modcallable *tail; /* of the children list */ + CONF_SECTION *cs; + vp_map_t *map; /* update */ + vp_tmpl_t *vpt; /* switch */ + fr_cond_t *cond; /* if/elsif */ + bool done_pass2; +} modgroup; + +typedef struct { + modcallable mc; + module_instance_t *modinst; +} modsingle; + +typedef struct { + modcallable mc; + char const *ref_name; + CONF_SECTION *ref_cs; +} modref; + +typedef struct { + modcallable mc; + int exec; + char *xlat_name; +} modxlat; + +/* Simple conversions: modsingle and modgroup are subclasses of modcallable, + * so we often want to go back and forth between them. */ +static modsingle *mod_callabletosingle(modcallable *p) +{ + rad_assert(p->type==MOD_SINGLE); + return (modsingle *)p; +} +static modgroup *mod_callabletogroup(modcallable *p) +{ + rad_assert((p->type > MOD_SINGLE) && (p->type <= MOD_POLICY)); + + return (modgroup *)p; +} +static modcallable *mod_singletocallable(modsingle *p) +{ + return (modcallable *)p; +} +static modcallable *mod_grouptocallable(modgroup *p) +{ + return (modcallable *)p; +} + +static modref *mod_callabletoref(modcallable *p) +{ + rad_assert(p->type==MOD_REFERENCE); + return (modref *)p; +} +static modcallable *mod_reftocallable(modref *p) +{ + return (modcallable *)p; +} + +static modxlat *mod_callabletoxlat(modcallable *p) +{ + rad_assert(p->type==MOD_XLAT); + return (modxlat *)p; +} +static modcallable *mod_xlattocallable(modxlat *p) +{ + return (modcallable *)p; +} + +/* modgroups are grown by adding a modcallable to the end */ +static void add_child(modgroup *g, modcallable *c) +{ + if (!c) return; + + (void) talloc_steal(g, c); + + if (!g->children) { + g->children = g->tail = c; + } else { + rad_assert(g->tail->next == NULL); + g->tail->next = c; + g->tail = c; + } + + c->parent = mod_grouptocallable(g); +} + +/* Here's where we recognize all of our keywords: first the rcodes, then the + * actions */ +const FR_NAME_NUMBER mod_rcode_table[] = { + { "reject", RLM_MODULE_REJECT }, + { "fail", RLM_MODULE_FAIL }, + { "ok", RLM_MODULE_OK }, + { "handled", RLM_MODULE_HANDLED }, + { "invalid", RLM_MODULE_INVALID }, + { "userlock", RLM_MODULE_USERLOCK }, + { "notfound", RLM_MODULE_NOTFOUND }, + { "noop", RLM_MODULE_NOOP }, + { "updated", RLM_MODULE_UPDATED }, + { NULL, 0 } +}; + + +/* + * Compile action && rcode for later use. + */ +static int compile_action(modcallable *c, CONF_PAIR *cp) +{ + int action; + char const *attr, *value; + + attr = cf_pair_attr(cp); + value = cf_pair_value(cp); + if (!value) return 0; + + if (!strcasecmp(value, "return")) + action = MOD_ACTION_RETURN; + + else if (!strcasecmp(value, "break")) + action = MOD_ACTION_RETURN; + + else if (!strcasecmp(value, "reject")) + action = MOD_ACTION_REJECT; + + else if (strspn(value, "0123456789")==strlen(value)) { + action = atoi(value); + + /* + * Don't allow priority zero, for future use. + */ + if (action == 0) return 0; + } else { + cf_log_err_cp(cp, "Unknown action '%s'.\n", + value); + return 0; + } + + if (strcasecmp(attr, "default") != 0) { + int rcode; + + rcode = fr_str2int(mod_rcode_table, attr, -1); + if (rcode < 0) { + cf_log_err_cp(cp, + "Unknown module rcode '%s'.\n", + attr); + return 0; + } + c->actions[rcode] = action; + + } else { /* set all unset values to the default */ + int i; + + for (i = 0; i < RLM_MODULE_NUMCODES; i++) { + if (!c->actions[i]) c->actions[i] = action; + } + } + + return 1; +} + +/* Some short names for debugging output */ +static char const * const comp2str[] = { + "authenticate", + "authorize", + "preacct", + "accounting", + "session", + "pre-proxy", + "post-proxy", + "post-auth" +#ifdef WITH_COA + , + "recv-coa", + "send-coa" +#endif +}; + +#ifdef HAVE_PTHREAD_H +/* + * Lock the mutex for the module + */ +static void safe_lock(module_instance_t *instance) +{ + if (instance->mutex) + pthread_mutex_lock(instance->mutex); +} + +/* + * Unlock the mutex for the module + */ +static void safe_unlock(module_instance_t *instance) +{ + if (instance->mutex) + pthread_mutex_unlock(instance->mutex); +} +#else +/* + * No threads: these functions become NULL's. + */ +#define safe_lock(foo) +#define safe_unlock(foo) +#endif + +static rlm_rcode_t CC_HINT(nonnull) call_modsingle(rlm_components_t component, modsingle *sp, REQUEST *request) +{ + int blocked; + int indent = request->log.indent; + char const *old; + + /* + * If the request should stop, refuse to do anything. + */ + blocked = (request->master_state == REQUEST_STOP_PROCESSING); + if (blocked) return RLM_MODULE_NOOP; + + RDEBUG3("modsingle[%s]: calling %s (%s)", + comp2str[component], sp->modinst->name, + sp->modinst->entry->name); + request->log.indent = 0; + + if (sp->modinst->force) { + request->rcode = sp->modinst->code; + goto fail; + } + + /* + * For logging unresponsive children. + */ + old = request->module; + request->module = sp->modinst->name; + + safe_lock(sp->modinst); + request->rcode = sp->modinst->entry->module->methods[component](sp->modinst->insthandle, request); + safe_unlock(sp->modinst); + + request->module = old; + + /* + * Wasn't blocked, and now is. Complain! + */ + blocked = (request->master_state == REQUEST_STOP_PROCESSING); + if (blocked) { + RWARN("Module %s became unblocked", sp->modinst->entry->name); + } + + fail: + request->log.indent = indent; + RDEBUG3("modsingle[%s]: returned from %s (%s)", + comp2str[component], sp->modinst->name, + sp->modinst->entry->name); + + return request->rcode; +} + +static int default_component_results[MOD_COUNT] = { + RLM_MODULE_REJECT, /* AUTH */ + RLM_MODULE_NOTFOUND, /* AUTZ */ + RLM_MODULE_NOOP, /* PREACCT */ + RLM_MODULE_NOOP, /* ACCT */ + RLM_MODULE_FAIL, /* SESS */ + RLM_MODULE_NOOP, /* PRE_PROXY */ + RLM_MODULE_NOOP, /* POST_PROXY */ + RLM_MODULE_NOOP /* POST_AUTH */ +#ifdef WITH_COA + , + RLM_MODULE_NOOP, /* RECV_COA_TYPE */ + RLM_MODULE_NOOP /* SEND_COA_TYPE */ +#endif +}; + + +extern char const *unlang_keyword[]; + +char const *unlang_keyword[] = { + "", + "single", + "group", + "load-balance group", + "redundant-load-balance group", +#ifdef WITH_UNLANG + "if", + "else", + "elsif", + "update", + "switch", + "case", + "foreach", + "break", + "return", +#endif + "policy", + "reference", + "xlat", + NULL +}; + +static char const modcall_spaces[] = " "; + +#define MODCALL_STACK_MAX (32) + +/* + * Don't call the modules recursively. Instead, do them + * iteratively, and manage the call stack ourselves. + */ +typedef struct modcall_stack_entry_t { + rlm_rcode_t result; + int priority; + int unwind; /* unwind to this one if it exists */ + modcallable *c; +} modcall_stack_entry_t; + + +static bool modcall_recurse(REQUEST *request, rlm_components_t component, int depth, + modcall_stack_entry_t *entry, bool do_next_sibling); + +/* + * Call a child of a block. + */ +static void modcall_child(REQUEST *request, rlm_components_t component, int depth, + modcall_stack_entry_t *entry, modcallable *c, + rlm_rcode_t *result, bool do_next_sibling) +{ + modcall_stack_entry_t *next; + + if (depth >= MODCALL_STACK_MAX) { + ERROR("Internal sanity check failed: module stack is too deep"); + fr_exit(1); + } + + /* + * Initialize the childs stack frame. + */ + next = entry + 1; + next->c = c; + next->result = entry->result; + next->priority = 0; + next->unwind = 0; + + if (!modcall_recurse(request, component, + depth, next, do_next_sibling)) { + *result = RLM_MODULE_FAIL; + return; + } + + /* + * Unwind back up the stack + */ + if (next->unwind != 0) { + entry->unwind = next->unwind; + } + + *result = next->result; + + return; +} + + +/* + * Interpret the various types of blocks. + */ +static bool modcall_recurse(REQUEST *request, rlm_components_t component, int depth, + modcall_stack_entry_t *entry, bool do_next_sibling) +{ + bool if_taken, was_if; + modcallable *c; + int priority; + rlm_rcode_t result; + + was_if = if_taken = false; + result = RLM_MODULE_UNKNOWN; + RINDENT(); + +redo: + priority = -1; + c = entry->c; + + /* + * Nothing more to do. Return the code and priority + * which was set by the caller. + */ + if (!c) goto finish; + + if (fr_debug_lvl >= 3) { + VERIFY_REQUEST(request); + } + + rad_assert(c->debug_name != NULL); /* if this happens, all bets are off. */ + + /* + * We've been asked to stop. Do so. + */ + if ((request->master_state == REQUEST_STOP_PROCESSING) || + (request->parent && + (request->parent->master_state == REQUEST_STOP_PROCESSING))) { + entry->result = RLM_MODULE_FAIL; + entry->priority = 9999; + goto finish; + } + +#ifdef WITH_UNLANG + /* + * Handle "if" conditions. + */ + if (c->type == MOD_IF) { + int condition; + modgroup *g; + + mod_if: + g = mod_callabletogroup(c); + rad_assert(g->cond != NULL); + + RDEBUG2("%s %s{", unlang_keyword[c->type], c->name); + + /* + * Use "result" UNLESS it wasn't set, in which + * case we use the previous result on the stack. + */ + condition = radius_evaluate_cond(request, result != RLM_MODULE_UNKNOWN ? result : entry->result, 0, g->cond); + if (condition < 0) { + condition = false; + REDEBUG("Failed retrieving values required to evaluate condition"); + } else { + RDEBUG2("%s %s -> %s", + unlang_keyword[c->type], + c->name, condition ? "TRUE" : "FALSE"); + } + + /* + * Didn't pass. Remember that. + */ + if (!condition) { + was_if = true; + if_taken = false; + goto next_sibling; + } + + /* + * We took the "if". Go recurse into its' children. + */ + was_if = true; + if_taken = true; + goto do_children; + } /* MOD_IF */ + + /* + * "else" if the previous "if" was taken. + * "if" if the previous if wasn't taken. + */ + if (c->type == MOD_ELSIF) { + if (!was_if) goto elsif_error; + + /* + * Like MOD_ELSE, but allow for a later "else" + */ + if (if_taken) { + RDEBUG2("... skipping %s: Preceding \"if\" was taken", + unlang_keyword[c->type]); + was_if = true; + if_taken = true; + goto next_sibling; + } + + /* + * Check the "if" condition. + */ + goto mod_if; + } /* MOD_ELSIF */ + + /* + * "else" for a preceding "if". + */ + if (c->type == MOD_ELSE) { + if (!was_if) { /* error */ + elsif_error: + RDEBUG2("... skipping %s: No preceding \"if\"", + unlang_keyword[c->type]); + goto next_sibling; + } + + if (if_taken) { + RDEBUG2("... skipping %s: Preceding \"if\" was taken", + unlang_keyword[c->type]); + was_if = false; + if_taken = false; + goto next_sibling; + } + + /* + * We need to process it. Go do that. + */ + was_if = false; + if_taken = false; + goto do_children; + } /* MOD_ELSE */ + + /* + * We're no longer processing if/else/elsif. Reset the + * trackers for those conditions. + */ + was_if = false; + if_taken = false; +#endif /* WITH_UNLANG */ + + if (c->type == MOD_SINGLE) { + modsingle *sp; + + /* + * Process a stand-alone child, and fall through + * to dealing with it's parent. + */ + sp = mod_callabletosingle(c); + + result = call_modsingle(c->method, sp, request); + RDEBUG2("[%s] = %s", c->name ? c->name : "", + fr_int2str(mod_rcode_table, result, "<invalid>")); + goto calculate_result; + } /* MOD_SINGLE */ + +#ifdef WITH_UNLANG + /* + * Update attribute(s) + */ + if (c->type == MOD_UPDATE) { + int rcode; + modgroup *g = mod_callabletogroup(c); + vp_map_t *map; + + MOD_LOG_OPEN_BRACE; + RINDENT(); + for (map = g->map; map != NULL; map = map->next) { + rcode = map_to_request(request, map, map_to_vp, NULL); + if (rcode < 0) { + result = (rcode == -2) ? RLM_MODULE_INVALID : RLM_MODULE_FAIL; + REXDENT(); + MOD_LOG_CLOSE_BRACE; + goto calculate_result; + } + } + REXDENT(); + result = RLM_MODULE_NOOP; + MOD_LOG_CLOSE_BRACE; + goto calculate_result; + } /* MOD_IF */ + + /* + * Loop over a set of attributes. + */ + if (c->type == MOD_FOREACH) { + int i, foreach_depth = -1; + VALUE_PAIR *vps, *vp; + modcall_stack_entry_t *next = NULL; + vp_cursor_t copy; + modgroup *g = mod_callabletogroup(c); + + if (depth >= MODCALL_STACK_MAX) { + ERROR("Internal sanity check failed: module stack is too deep"); + fr_exit(1); + } + + /* + * Figure out how deep we are in nesting by looking at request_data + * stored previously. + */ + for (i = 0; i < 8; i++) { + if (!request_data_reference(request, (void *)radius_get_vp, i)) { + foreach_depth = i; + break; + } + } + + if (foreach_depth < 0) { + REDEBUG("foreach Nesting too deep!"); + result = RLM_MODULE_FAIL; + goto calculate_result; + } + + /* + * Copy the VPs from the original request, this ensures deterministic + * behaviour if someone decides to add or remove VPs in the set were + * iterating over. + */ + if (tmpl_copy_vps(request, &vps, request, g->vpt) < 0) { /* nothing to loop over */ + MOD_LOG_OPEN_BRACE; + result = RLM_MODULE_NOOP; + MOD_LOG_CLOSE_BRACE; + goto calculate_result; + } + + rad_assert(vps != NULL); + fr_cursor_init(©, &vps); + + RDEBUG2("foreach %s ", c->name); + + /* + * This is the actual body of the foreach loop + */ + for (vp = fr_cursor_first(©); + vp != NULL; + vp = fr_cursor_next(©)) { +#ifndef NDEBUG + if (fr_debug_lvl >= 2) { + char buffer[1024]; + + vp_prints_value(buffer, sizeof(buffer), vp, '"'); + RDEBUG2("# Foreach-Variable-%d = %s", foreach_depth, buffer); + } +#endif + + /* + * Add the vp to the request, so that + * xlat.c, xlat_foreach() can find it. + */ + request_data_add(request, (void *)radius_get_vp, foreach_depth, &vp, false); + + /* + * Initialize the childs stack frame. + */ + next = entry + 1; + next->c = g->children; + next->result = entry->result; + next->priority = 0; + next->unwind = 0; + + if (!modcall_recurse(request, component, depth + 1, next, true)) { + break; + } + + /* + * We've been asked to unwind to the + * enclosing "foreach". We're here, so + * we can stop unwinding. + */ + if (next->unwind == MOD_BREAK) { + entry->unwind = 0; + break; + } + + /* + * Unwind all the way. + */ + if (next->unwind == MOD_RETURN) { + entry->unwind = MOD_RETURN; + break; + } + } /* loop over VPs */ + + /* + * Free the copied vps and the request data + * If we don't remove the request data, something could call + * the xlat outside of a foreach loop and trigger a segv. + */ + fr_pair_list_free(&vps); + request_data_get(request, (void *)radius_get_vp, foreach_depth); + + rad_assert(next != NULL); + result = next->result; + priority = next->priority; + MOD_LOG_CLOSE_BRACE; + goto calculate_result; + } /* MOD_FOREACH */ + + /* + * Break out of a "foreach" loop, or return from a nested + * group. + */ + if ((c->type == MOD_BREAK) || (c->type == MOD_RETURN)) { + int i; + VALUE_PAIR **copy_p; + + RDEBUG2("%s", unlang_keyword[c->type]); + + for (i = 8; i >= 0; i--) { + copy_p = request_data_get(request, (void *)radius_get_vp, i); + if (copy_p) { + if (c->type == MOD_BREAK) { + RDEBUG2("# break Foreach-Variable-%d", i); + break; + } + } + } + + /* + * Leave result / priority on the stack, and stop processing the section. + */ + entry->unwind = c->type; + goto finish; + } /* MOD_BREAK */ + +#endif /* WITH_UNLANG */ + + /* + * Child is a group that has children of it's own. + */ + if ((c->type == MOD_GROUP) || (c->type == MOD_POLICY) +#ifdef WITH_UNLANG + || (c->type == MOD_CASE) +#endif + ) { + modgroup *g; + +#ifdef WITH_UNLANG + do_children: +#endif + g = mod_callabletogroup(c); + + /* + * This should really have been caught in the + * compiler, and the node never generated. But + * doing that requires changing it's API so that + * it returns a flag instead of the compiled + * MOD_GROUP. + */ + if (!g->children) { + if (c->type == MOD_CASE) { + result = RLM_MODULE_NOOP; + goto calculate_result; + } + + RDEBUG2("%s { ... } # empty sub-section is ignored", c->name); + goto next_sibling; + } + + MOD_LOG_OPEN_BRACE; + modcall_child(request, component, + depth + 1, entry, g->children, + &result, true); + MOD_LOG_CLOSE_BRACE; + goto calculate_result; + } /* MOD_GROUP */ + +#ifdef WITH_UNLANG + if (c->type == MOD_SWITCH) { + modcallable *this, *found, *null_case; + modgroup *g, *h; + fr_cond_t cond; + value_data_t data; + vp_map_t map; + vp_tmpl_t vpt; + + MOD_LOG_OPEN_BRACE; + + g = mod_callabletogroup(c); + + memset(&cond, 0, sizeof(cond)); + memset(&map, 0, sizeof(map)); + + cond.type = COND_TYPE_MAP; + cond.data.map = ↦ + + map.op = T_OP_CMP_EQ; + map.ci = cf_section_to_item(g->cs); + + rad_assert(g->vpt != NULL); + + null_case = found = NULL; + data.ptr = NULL; + + /* + * The attribute doesn't exist. We can skip + * directly to the default 'case' statement. + */ + if ((g->vpt->type == TMPL_TYPE_ATTR) && (tmpl_find_vp(NULL, request, g->vpt) < 0)) { + find_null_case: + for (this = g->children; this; this = this->next) { + rad_assert(this->type == MOD_CASE); + + h = mod_callabletogroup(this); + if (h->vpt) continue; + + found = this; + break; + } + + goto do_null_case; + } + + /* + * Expand the template if necessary, so that it + * is evaluated once instead of for each 'case' + * statement. + */ + if ((g->vpt->type == TMPL_TYPE_XLAT_STRUCT) || + (g->vpt->type == TMPL_TYPE_XLAT) || + (g->vpt->type == TMPL_TYPE_EXEC)) { + char *p; + ssize_t len; + + len = tmpl_aexpand(request, &p, request, g->vpt, NULL, NULL); + if (len < 0) goto find_null_case; + data.strvalue = p; + tmpl_init(&vpt, TMPL_TYPE_LITERAL, data.strvalue, len); + } + + /* + * Find either the exact matching name, or the + * "case {...}" statement. + */ + for (this = g->children; this; this = this->next) { + rad_assert(this->type == MOD_CASE); + + h = mod_callabletogroup(this); + + /* + * Remember the default case + */ + if (!h->vpt) { + if (!null_case) null_case = this; + continue; + } + + /* + * If we're switching over an attribute + * AND we haven't pre-parsed the data for + * the case statement, then cast the data + * to the type of the attribute. + */ + if ((g->vpt->type == TMPL_TYPE_ATTR) && + (h->vpt->type != TMPL_TYPE_DATA)) { + map.rhs = g->vpt; + map.lhs = h->vpt; + cond.cast = g->vpt->tmpl_da; + + /* + * Remove unnecessary casting. + */ + if ((h->vpt->type == TMPL_TYPE_ATTR) && + (g->vpt->tmpl_da->type == h->vpt->tmpl_da->type)) { + cond.cast = NULL; + } + + /* + * Use the pre-expanded string. + */ + } else if ((g->vpt->type == TMPL_TYPE_XLAT_STRUCT) || + (g->vpt->type == TMPL_TYPE_XLAT) || + (g->vpt->type == TMPL_TYPE_EXEC)) { + map.rhs = h->vpt; + map.lhs = &vpt; + cond.cast = NULL; + + /* + * Else evaluate the 'switch' statement. + */ + } else { + map.rhs = h->vpt; + map.lhs = g->vpt; + cond.cast = NULL; + } + + if (radius_evaluate_map(request, RLM_MODULE_UNKNOWN, 0, + &cond) == 1) { + found = this; + break; + } + } + + if (!found) found = null_case; + + do_null_case: + talloc_free(data.ptr); + modcall_child(request, component, depth + 1, entry, found, &result, true); + MOD_LOG_CLOSE_BRACE; + goto calculate_result; + } /* MOD_SWITCH */ +#endif + + if ((c->type == MOD_LOAD_BALANCE) || + (c->type == MOD_REDUNDANT_LOAD_BALANCE)) { + uint32_t count = 0; + modcallable *this, *found; + modgroup *g; + + MOD_LOG_OPEN_BRACE; + + g = mod_callabletogroup(c); + found = g->children; + rad_assert(g->children != NULL); + + /* + * Choose a child at random. + */ + for (this = g->children; this; this = this->next) { + count++; + + if ((count * (fr_rand() & 0xffff)) < (uint32_t) 0x10000) { + found = this; + } + } + + if (c->type == MOD_LOAD_BALANCE) { + modcall_child(request, component, + depth + 1, entry, found, + &result, false); + + } else { + this = found; + + do { + modcall_child(request, component, + depth + 1, entry, this, + &result, false); + if (this->actions[result] == MOD_ACTION_RETURN) { + priority = -1; + break; + } + + this = this->next; + if (!this) this = g->children; + } while (this != found); + } + MOD_LOG_CLOSE_BRACE; + goto calculate_result; + } /* MOD_LOAD_BALANCE */ + + /* + * Reference another virtual server. + * + * This should really be deleted, and replaced with a + * more abstracted / functional version. + */ + if (c->type == MOD_REFERENCE) { + modref *mr = mod_callabletoref(c); + char const *server = request->server; + + if (server == mr->ref_name) { + RWDEBUG("Suppressing recursive call to server %s", server); + goto next_sibling; + } + + request->server = mr->ref_name; + RDEBUG("server %s { # nested call", mr->ref_name); + result = indexed_modcall(component, 0, request); + RDEBUG("} # server %s with nested call", mr->ref_name); + request->server = server; + goto calculate_result; + } /* MOD_REFERENCE */ + + /* + * xlat a string without doing anything else + * + * This should really be deleted, and replaced with a + * more abstracted / functional version. + */ + if (c->type == MOD_XLAT) { + modxlat *mx = mod_callabletoxlat(c); + char buffer[128]; + + if (!mx->exec) { + radius_xlat(buffer, sizeof(buffer), request, mx->xlat_name, NULL, NULL); + } else { + RDEBUG("`%s`", mx->xlat_name); + radius_exec_program(request, NULL, 0, NULL, request, mx->xlat_name, request->packet->vps, + false, true, EXEC_TIMEOUT); + } + + goto next_sibling; + } /* MOD_XLAT */ + + /* + * Add new module types here. + */ + +calculate_result: +#if 0 + RDEBUG("(%s, %d) ? (%s, %d)", + fr_int2str(mod_rcode_table, result, "<invalid>"), + priority, + fr_int2str(mod_rcode_table, entry->result, "<invalid>"), + entry->priority); +#endif + + + rad_assert(result != RLM_MODULE_UNKNOWN); + + /* + * The child's action says return. Do so. + */ + if ((c->actions[result] == MOD_ACTION_RETURN) && + (priority <= 0)) { + entry->result = result; + goto finish; + } + + /* + * If "reject", break out of the loop and return + * reject. + */ + if (c->actions[result] == MOD_ACTION_REJECT) { + entry->result = RLM_MODULE_REJECT; + goto finish; + } + + /* + * The array holds a default priority for this return + * code. Grab it in preference to any unset priority. + */ + if (priority < 0) { + priority = c->actions[result]; + } + + /* + * We're higher than any previous priority, remember this + * return code and priority. + */ + if (priority > entry->priority) { + entry->result = result; + entry->priority = priority; + } + +#ifdef WITH_UNLANG + /* + * If we're processing a "case" statement, we return once + * it's done, rather than going to the next "case" statement. + */ + if (c->type == MOD_CASE) goto finish; +#endif + + /* + * If we've been told to stop processing + * it, do so. + */ + if (entry->unwind == MOD_BREAK) { + RDEBUG2("# unwind to enclosing foreach"); + goto finish; + } + + if (entry->unwind == MOD_RETURN) { + goto finish; + } + +next_sibling: + if (do_next_sibling) { + entry->c = entry->c->next; + + if (entry->c) goto redo; + } + +finish: + /* + * And we're done! + */ + REXDENT(); + return true; +} + + +/** Call a module, iteratively, with a local stack, rather than recursively + * + * What did Paul Graham say about Lisp...? + */ +int modcall(rlm_components_t component, modcallable *c, REQUEST *request) +{ + modcall_stack_entry_t stack[MODCALL_STACK_MAX]; + +#ifndef NDEBUG + memset(stack, 0, sizeof(stack)); +#endif + /* + * Set up the initial stack frame. + */ + stack[0].c = c; + stack[0].result = default_component_results[component]; + stack[0].priority = 0; + stack[0].unwind = 0; + + /* + * Call the main handler. + */ + if (!modcall_recurse(request, component, 0, &stack[0], true)) { + return RLM_MODULE_FAIL; + } + + /* + * Return the result. + */ + return stack[0].result; +} + + +#if 0 +static char const *action2str(int action) +{ + static char buf[32]; + if(action==MOD_ACTION_RETURN) + return "return"; + if(action==MOD_ACTION_REJECT) + return "reject"; + snprintf(buf, sizeof buf, "%d", action); + return buf; +} + +/* If you suspect a bug in the parser, you'll want to use these dump + * functions. dump_tree should reproduce a whole tree exactly as it was found + * in radiusd.conf, but in long form (all actions explicitly defined) */ +static void dump_mc(modcallable *c, int indent) +{ + int i; + + if(c->type==MOD_SINGLE) { + modsingle *single = mod_callabletosingle(c); + DEBUG("%.*s%s {", indent, "\t\t\t\t\t\t\t\t\t\t\t", + single->modinst->name); + } else if ((c->type > MOD_SINGLE) && (c->type <= MOD_POLICY)) { + modgroup *g = mod_callabletogroup(c); + modcallable *p; + DEBUG("%.*s%s {", indent, "\t\t\t\t\t\t\t\t\t\t\t", + unlang_keyword[c->type]); + for(p = g->children;p;p = p->next) + dump_mc(p, indent+1); + } /* else ignore it for now */ + + for(i = 0; i<RLM_MODULE_NUMCODES; ++i) { + DEBUG("%.*s%s = %s", indent+1, "\t\t\t\t\t\t\t\t\t\t\t", + fr_int2str(mod_rcode_table, i, "<invalid>"), + action2str(c->actions[i])); + } + + DEBUG("%.*s}", indent, "\t\t\t\t\t\t\t\t\t\t\t"); +} + +static void dump_tree(rlm_components_t comp, modcallable *c) +{ + DEBUG("[%s]", comp2str[comp]); + dump_mc(c, 0); +} +#else +#define dump_tree(a, b) +#endif + +/* These are the default actions. For each component, the group{} block + * behaves like the code from the old module_*() function. redundant{} + * are based on my guesses of what they will be used for. --Pac. */ +static const int +defaultactions[MOD_COUNT][GROUPTYPE_COUNT][RLM_MODULE_NUMCODES] = +{ + /* authenticate */ + { + /* group */ + { + MOD_ACTION_RETURN, /* reject */ + 1, /* fail */ + MOD_ACTION_RETURN, /* ok */ + MOD_ACTION_RETURN, /* handled */ + 1, /* invalid */ + MOD_ACTION_RETURN, /* userlock */ + MOD_ACTION_RETURN, /* notfound */ + 1, /* noop */ + 1 /* updated */ + }, + /* redundant */ + { + MOD_ACTION_RETURN, /* reject */ + 1, /* fail */ + MOD_ACTION_RETURN, /* ok */ + MOD_ACTION_RETURN, /* handled */ + MOD_ACTION_RETURN, /* invalid */ + MOD_ACTION_RETURN, /* userlock */ + MOD_ACTION_RETURN, /* notfound */ + MOD_ACTION_RETURN, /* noop */ + MOD_ACTION_RETURN /* updated */ + } + }, + /* authorize */ + { + /* group */ + { + MOD_ACTION_RETURN, /* reject */ + MOD_ACTION_RETURN, /* fail */ + 3, /* ok */ + MOD_ACTION_RETURN, /* handled */ + MOD_ACTION_RETURN, /* invalid */ + MOD_ACTION_RETURN, /* userlock */ + 1, /* notfound */ + 2, /* noop */ + 4 /* updated */ + }, + /* redundant */ + { + MOD_ACTION_RETURN, /* reject */ + 1, /* fail */ + MOD_ACTION_RETURN, /* ok */ + MOD_ACTION_RETURN, /* handled */ + MOD_ACTION_RETURN, /* invalid */ + MOD_ACTION_RETURN, /* userlock */ + MOD_ACTION_RETURN, /* notfound */ + MOD_ACTION_RETURN, /* noop */ + MOD_ACTION_RETURN /* updated */ + } + }, + /* preacct */ + { + /* group */ + { + MOD_ACTION_RETURN, /* reject */ + MOD_ACTION_RETURN, /* fail */ + 2, /* ok */ + MOD_ACTION_RETURN, /* handled */ + MOD_ACTION_RETURN, /* invalid */ + MOD_ACTION_RETURN, /* userlock */ + MOD_ACTION_RETURN, /* notfound */ + 1, /* noop */ + 3 /* updated */ + }, + /* redundant */ + { + MOD_ACTION_RETURN, /* reject */ + 1, /* fail */ + MOD_ACTION_RETURN, /* ok */ + MOD_ACTION_RETURN, /* handled */ + MOD_ACTION_RETURN, /* invalid */ + MOD_ACTION_RETURN, /* userlock */ + MOD_ACTION_RETURN, /* notfound */ + MOD_ACTION_RETURN, /* noop */ + MOD_ACTION_RETURN /* updated */ + } + }, + /* accounting */ + { + /* group */ + { + MOD_ACTION_RETURN, /* reject */ + MOD_ACTION_RETURN, /* fail */ + 2, /* ok */ + MOD_ACTION_RETURN, /* handled */ + MOD_ACTION_RETURN, /* invalid */ + MOD_ACTION_RETURN, /* userlock */ + MOD_ACTION_RETURN, /* notfound */ + 1, /* noop */ + 3 /* updated */ + }, + /* redundant */ + { + 1, /* reject */ + 1, /* fail */ + MOD_ACTION_RETURN, /* ok */ + MOD_ACTION_RETURN, /* handled */ + 1, /* invalid */ + 1, /* userlock */ + 1, /* notfound */ + 2, /* noop */ + 4 /* updated */ + } + }, + /* checksimul */ + { + /* group */ + { + MOD_ACTION_RETURN, /* reject */ + 1, /* fail */ + MOD_ACTION_RETURN, /* ok */ + MOD_ACTION_RETURN, /* handled */ + MOD_ACTION_RETURN, /* invalid */ + MOD_ACTION_RETURN, /* userlock */ + MOD_ACTION_RETURN, /* notfound */ + MOD_ACTION_RETURN, /* noop */ + MOD_ACTION_RETURN /* updated */ + }, + /* redundant */ + { + MOD_ACTION_RETURN, /* reject */ + 1, /* fail */ + MOD_ACTION_RETURN, /* ok */ + MOD_ACTION_RETURN, /* handled */ + MOD_ACTION_RETURN, /* invalid */ + MOD_ACTION_RETURN, /* userlock */ + MOD_ACTION_RETURN, /* notfound */ + MOD_ACTION_RETURN, /* noop */ + MOD_ACTION_RETURN /* updated */ + } + }, + /* pre-proxy */ + { + /* group */ + { + MOD_ACTION_RETURN, /* reject */ + MOD_ACTION_RETURN, /* fail */ + 3, /* ok */ + MOD_ACTION_RETURN, /* handled */ + MOD_ACTION_RETURN, /* invalid */ + MOD_ACTION_RETURN, /* userlock */ + 1, /* notfound */ + 2, /* noop */ + 4 /* updated */ + }, + /* redundant */ + { + MOD_ACTION_RETURN, /* reject */ + 1, /* fail */ + MOD_ACTION_RETURN, /* ok */ + MOD_ACTION_RETURN, /* handled */ + MOD_ACTION_RETURN, /* invalid */ + MOD_ACTION_RETURN, /* userlock */ + MOD_ACTION_RETURN, /* notfound */ + MOD_ACTION_RETURN, /* noop */ + MOD_ACTION_RETURN /* updated */ + } + }, + /* post-proxy */ + { + /* group */ + { + MOD_ACTION_RETURN, /* reject */ + MOD_ACTION_RETURN, /* fail */ + 3, /* ok */ + MOD_ACTION_RETURN, /* handled */ + MOD_ACTION_RETURN, /* invalid */ + MOD_ACTION_RETURN, /* userlock */ + 1, /* notfound */ + 2, /* noop */ + 4 /* updated */ + }, + /* redundant */ + { + MOD_ACTION_RETURN, /* reject */ + 1, /* fail */ + MOD_ACTION_RETURN, /* ok */ + MOD_ACTION_RETURN, /* handled */ + MOD_ACTION_RETURN, /* invalid */ + MOD_ACTION_RETURN, /* userlock */ + MOD_ACTION_RETURN, /* notfound */ + MOD_ACTION_RETURN, /* noop */ + MOD_ACTION_RETURN /* updated */ + } + }, + /* post-auth */ + { + /* group */ + { + MOD_ACTION_RETURN, /* reject */ + MOD_ACTION_RETURN, /* fail */ + 3, /* ok */ + MOD_ACTION_RETURN, /* handled */ + MOD_ACTION_RETURN, /* invalid */ + MOD_ACTION_RETURN, /* userlock */ + 1, /* notfound */ + 2, /* noop */ + 4 /* updated */ + }, + /* redundant */ + { + MOD_ACTION_RETURN, /* reject */ + 1, /* fail */ + MOD_ACTION_RETURN, /* ok */ + MOD_ACTION_RETURN, /* handled */ + MOD_ACTION_RETURN, /* invalid */ + MOD_ACTION_RETURN, /* userlock */ + MOD_ACTION_RETURN, /* notfound */ + MOD_ACTION_RETURN, /* noop */ + MOD_ACTION_RETURN /* updated */ + } + } +#ifdef WITH_COA + , + /* recv-coa */ + { + /* group */ + { + MOD_ACTION_RETURN, /* reject */ + MOD_ACTION_RETURN, /* fail */ + 3, /* ok */ + MOD_ACTION_RETURN, /* handled */ + MOD_ACTION_RETURN, /* invalid */ + MOD_ACTION_RETURN, /* userlock */ + 1, /* notfound */ + 2, /* noop */ + 4 /* updated */ + }, + /* redundant */ + { + MOD_ACTION_RETURN, /* reject */ + 1, /* fail */ + MOD_ACTION_RETURN, /* ok */ + MOD_ACTION_RETURN, /* handled */ + MOD_ACTION_RETURN, /* invalid */ + MOD_ACTION_RETURN, /* userlock */ + MOD_ACTION_RETURN, /* notfound */ + MOD_ACTION_RETURN, /* noop */ + MOD_ACTION_RETURN /* updated */ + } + }, + /* send-coa */ + { + /* group */ + { + MOD_ACTION_RETURN, /* reject */ + MOD_ACTION_RETURN, /* fail */ + 3, /* ok */ + MOD_ACTION_RETURN, /* handled */ + MOD_ACTION_RETURN, /* invalid */ + MOD_ACTION_RETURN, /* userlock */ + 1, /* notfound */ + 2, /* noop */ + 4 /* updated */ + }, + /* redundant */ + { + MOD_ACTION_RETURN, /* reject */ + 1, /* fail */ + MOD_ACTION_RETURN, /* ok */ + MOD_ACTION_RETURN, /* handled */ + MOD_ACTION_RETURN, /* invalid */ + MOD_ACTION_RETURN, /* userlock */ + MOD_ACTION_RETURN, /* notfound */ + MOD_ACTION_RETURN, /* noop */ + MOD_ACTION_RETURN /* updated */ + } + } +#endif +}; + +static const int authtype_actions[GROUPTYPE_COUNT][RLM_MODULE_NUMCODES] = +{ + /* group */ + { + MOD_ACTION_RETURN, /* reject */ + MOD_ACTION_RETURN, /* fail */ + 4, /* ok */ + MOD_ACTION_RETURN, /* handled */ + MOD_ACTION_RETURN, /* invalid */ + MOD_ACTION_RETURN, /* userlock */ + 1, /* notfound */ + 2, /* noop */ + 3 /* updated */ + }, + /* redundant */ + { + MOD_ACTION_RETURN, /* reject */ + 1, /* fail */ + MOD_ACTION_RETURN, /* ok */ + MOD_ACTION_RETURN, /* handled */ + MOD_ACTION_RETURN, /* invalid */ + MOD_ACTION_RETURN, /* userlock */ + MOD_ACTION_RETURN, /* notfound */ + MOD_ACTION_RETURN, /* noop */ + MOD_ACTION_RETURN /* updated */ + } +}; + +/** Validate and fixup a map that's part of an update section. + * + * @param map to validate. + * @param ctx data to pass to fixup function (currently unused). + * @return 0 if valid else -1. + */ +int modcall_fixup_update(vp_map_t *map, UNUSED void *ctx) +{ + CONF_PAIR *cp = cf_item_to_pair(map->ci); + /* + * Anal-retentive checks. + */ + if (DEBUG_ENABLED3) { + if ((map->lhs->type == TMPL_TYPE_ATTR) && (map->lhs->name[0] != '&')) { + WARN("%s[%d]: Please change attribute reference to '&%s %s ...'", + cf_pair_filename(cp), cf_pair_lineno(cp), + map->lhs->name, fr_int2str(fr_tokens, map->op, "<INVALID>")); + } + + if ((map->rhs->type == TMPL_TYPE_ATTR) && (map->rhs->name[0] != '&')) { + WARN("%s[%d]: Please change attribute reference to '... %s &%s'", + cf_pair_filename(cp), cf_pair_lineno(cp), + fr_int2str(fr_tokens, map->op, "<INVALID>"), map->rhs->name); + } + } + + /* + * Values used by unary operators should be literal ANY + * + * We then free the template and alloc a NULL one instead. + */ + if (map->op == T_OP_CMP_FALSE) { + if ((map->rhs->type != TMPL_TYPE_LITERAL) || (strcmp(map->rhs->name, "ANY") != 0)) { + WARN("%s[%d] Wildcard deletion MUST use '!* ANY'", + cf_pair_filename(cp), cf_pair_lineno(cp)); + } + + TALLOC_FREE(map->rhs); + + map->rhs = tmpl_alloc(map, TMPL_TYPE_NULL, NULL, 0); + } + + /* + * Lots of sanity checks for insane people... + */ + + /* + * What exactly where you expecting to happen here? + */ + if ((map->lhs->type == TMPL_TYPE_ATTR) && + (map->rhs->type == TMPL_TYPE_LIST)) { + cf_log_err(map->ci, "Can't copy list into an attribute"); + return -1; + } + + /* + * Depending on the attribute type, some operators are disallowed. + */ + if ((map->lhs->type == TMPL_TYPE_ATTR) && (!fr_assignment_op[map->op] && !fr_equality_op[map->op])) { + cf_log_err(map->ci, "Invalid operator \"%s\" in update section. " + "Only assignment or filter operators are allowed", + fr_int2str(fr_tokens, map->op, "<INVALID>")); + return -1; + } + + if (map->lhs->type == TMPL_TYPE_LIST) { + /* + * Can't copy an xlat expansion or literal into a list, + * we don't know what type of attribute we'd need + * to create. + * + * The only exception is where were using a unary + * operator like !*. + */ + if (map->op != T_OP_CMP_FALSE) switch (map->rhs->type) { + case TMPL_TYPE_XLAT: + case TMPL_TYPE_LITERAL: + cf_log_err(map->ci, "Can't copy value into list (we don't know which attribute to create)"); + return -1; + + default: + break; + } + + /* + * Only += and :=, and !*, and ^= operators are supported + * for lists. + */ + switch (map->op) { + case T_OP_CMP_FALSE: + break; + + case T_OP_ADD: + if ((map->rhs->type != TMPL_TYPE_LIST) && + (map->rhs->type != TMPL_TYPE_EXEC)) { + cf_log_err(map->ci, "Invalid source for list assignment '%s += ...'", map->lhs->name); + return -1; + } + break; + + case T_OP_SET: + if (map->rhs->type == TMPL_TYPE_EXEC) { + WARN("%s[%d]: Please change ':=' to '=' for list assignment", + cf_pair_filename(cp), cf_pair_lineno(cp)); + } + + if (map->rhs->type != TMPL_TYPE_LIST) { + cf_log_err(map->ci, "Invalid source for list assignment '%s := ...'", map->lhs->name); + return -1; + } + break; + + case T_OP_EQ: + if (map->rhs->type != TMPL_TYPE_EXEC) { + cf_log_err(map->ci, "Invalid source for list assignment '%s = ...'", map->lhs->name); + return -1; + } + break; + + case T_OP_PREPEND: + if ((map->rhs->type != TMPL_TYPE_LIST) && + (map->rhs->type != TMPL_TYPE_EXEC)) { + cf_log_err(map->ci, "Invalid source for list assignment '%s ^= ...'", map->lhs->name); + return -1; + } + break; + + default: + cf_log_err(map->ci, "Operator \"%s\" not allowed for list assignment", + fr_int2str(fr_tokens, map->op, "<INVALID>")); + return -1; + } + } + + /* + * If the map has a unary operator there's no further + * processing we need to, as RHS is unused. + */ + if (map->op == T_OP_CMP_FALSE) return 0; + + /* + * If LHS is an attribute, and RHS is a literal, we can + * preparse the information into a TMPL_TYPE_DATA. + * + * Unless it's a unary operator in which case we + * ignore map->rhs. + */ + if ((map->lhs->type == TMPL_TYPE_ATTR) && (map->rhs->type == TMPL_TYPE_LITERAL)) { + /* + * It's a literal string, just copy it. + * Don't escape anything. + */ + if (!cf_new_escape && + (map->lhs->tmpl_da->type == PW_TYPE_STRING) && + (cf_pair_value_type(cp) == T_SINGLE_QUOTED_STRING)) { + tmpl_cast_in_place_str(map->rhs); + + } else { + /* + * RHS is hex, try to parse it as + * type-specific data. + */ + if (map->lhs->auto_converted && + (map->rhs->name[0] == '0') && (map->rhs->name[1] == 'x') && + (map->rhs->len > 2) && ((map->rhs->len & 0x01) == 0)) { + vp_tmpl_t *vpt = map->rhs; + map->rhs = NULL; + + if (!map_cast_from_hex(map, T_BARE_WORD, vpt->name)) { + map->rhs = vpt; + cf_log_err(map->ci, "Cannot parse RHS hex as the data type of the attribute %s", map->lhs->tmpl_da->name); + return -1; + } + talloc_free(vpt); + + } else if (tmpl_cast_in_place(map->rhs, map->lhs->tmpl_da->type, map->lhs->tmpl_da) < 0) { + cf_log_err(map->ci, "%s", fr_strerror()); + return -1; + } + + /* + * Fixup LHS da if it doesn't match the type + * of the RHS. + */ + if (map->lhs->tmpl_da->type != map->rhs->tmpl_data_type) { + DICT_ATTR const *da; + + da = dict_attrbytype(map->lhs->tmpl_da->attr, map->lhs->tmpl_da->vendor, + map->rhs->tmpl_data_type); + if (!da) { + cf_log_err(map->ci, "Cannot find %s variant of attribute \"%s\"", + fr_int2str(dict_attr_types, map->rhs->tmpl_data_type, + "<INVALID>"), map->lhs->tmpl_da->name); + return -1; + } + map->lhs->tmpl_da = da; + } + } + } /* else we can't precompile the data */ + + return 0; +} + + +#ifdef WITH_UNLANG +static modcallable *do_compile_modupdate(modcallable *parent, rlm_components_t component, + CONF_SECTION *cs, char const *name2) +{ + int rcode; + modgroup *g; + modcallable *csingle; + + vp_map_t *head; + + /* + * This looks at cs->name2 to determine which list to update + */ + rcode = map_afrom_cs(&head, cs, PAIR_LIST_REQUEST, PAIR_LIST_REQUEST, modcall_fixup_update, NULL, 128); + if (rcode < 0) return NULL; /* message already printed */ + if (!head) { + cf_log_err_cs(cs, "'update' sections cannot be empty"); + return NULL; + } + + g = talloc_zero(parent, modgroup); + csingle = mod_grouptocallable(g); + + csingle->parent = parent; + csingle->next = NULL; + + if (name2) { + csingle->name = name2; + } else { + csingle->name = ""; + } + csingle->type = MOD_UPDATE; + csingle->method = component; + + memcpy(csingle->actions, defaultactions[component][GROUPTYPE_SIMPLE], + sizeof(csingle->actions)); + + g->grouptype = GROUPTYPE_SIMPLE; + g->children = NULL; + g->cs = cs; + g->map = talloc_steal(g, head); + + return csingle; +} + + +static modcallable *do_compile_modswitch (modcallable *parent, rlm_components_t component, CONF_SECTION *cs) +{ + CONF_ITEM *ci; + FR_TOKEN type; + char const *name2; + bool had_seen_default = false; + modcallable *csingle; + modgroup *g; + ssize_t slen; + vp_tmpl_t *vpt; + + name2 = cf_section_name2(cs); + if (!name2) { + cf_log_err_cs(cs, "You must specify a variable to switch over for 'switch'"); + return NULL; + } + + if (!cf_item_find_next(cs, NULL)) { + cf_log_err_cs(cs, "'switch' statements cannot be empty"); + return NULL; + } + + /* + * Create the template. If we fail, AND it's a bare word + * with &Foo-Bar, it MAY be an attribute defined by a + * module. Allow it for now. The pass2 checks below + * will fix it up. + */ + type = cf_section_name2_type(cs); + slen = tmpl_afrom_str(cs, &vpt, name2, strlen(name2), type, REQUEST_CURRENT, PAIR_LIST_REQUEST, true); + if ((slen < 0) && ((type != T_BARE_WORD) || (name2[0] != '&'))) { + char *spaces, *text; + + fr_canonicalize_error(cs, &spaces, &text, slen, fr_strerror()); + + cf_log_err_cs(cs, "Syntax error"); + cf_log_err_cs(cs, "%s", name2); + cf_log_err_cs(cs, "%s^ %s", spaces, text); + + talloc_free(spaces); + talloc_free(text); + + return NULL; + } + + /* + * Otherwise a NULL vpt may refer to an attribute defined + * by a module. That is checked in pass 2. + */ + + if (vpt->type == TMPL_TYPE_LIST) { + cf_log_err_cs(cs, "Syntax error: Cannot switch over list '%s'", name2); + return NULL; + } + + /* + * Warn about confusing things. + */ + if ((vpt->type == TMPL_TYPE_ATTR) && (*name2 != '&')) { + WARN("%s[%d]: Please change \"switch %s\" to \"switch &%s\"", + cf_section_filename(cs), cf_section_lineno(cs), + name2, name2); + } + + /* + * Walk through the children of the switch section, + * ensuring that they're all 'case' statements + */ + for (ci = cf_item_find_next(cs, NULL); + ci != NULL; + ci = cf_item_find_next(cs, ci)) { + CONF_SECTION *subcs; + char const *name1; + + if (!cf_item_is_section(ci)) { + if (!cf_item_is_pair(ci)) continue; + + cf_log_err(ci, "\"switch\" sections can only have \"case\" subsections"); + talloc_free(vpt); + return NULL; + } + + subcs = cf_item_to_section(ci); /* can't return NULL */ + name1 = cf_section_name1(subcs); + + if (strcmp(name1, "case") != 0) { + cf_log_err(ci, "\"switch\" sections can only have \"case\" subsections"); + talloc_free(vpt); + return NULL; + } + + name2 = cf_section_name2(subcs); + if (!name2) { + if (!had_seen_default) { + had_seen_default = true; + continue; + } + + cf_log_err(ci, "Cannot have two 'default' case statements"); + talloc_free(vpt); + return NULL; + } + } + + csingle = do_compile_modgroup(parent, component, cs, + GROUPTYPE_SIMPLE, + GROUPTYPE_SIMPLE, + MOD_SWITCH); + if (!csingle) { + talloc_free(vpt); + return NULL; + } + + g = mod_callabletogroup(csingle); + g->vpt = talloc_steal(g, vpt); + + return csingle; +} + +static modcallable *do_compile_modcase(modcallable *parent, rlm_components_t component, CONF_SECTION *cs) +{ + int i; + char const *name2; + modcallable *csingle; + modgroup *g; + vp_tmpl_t *vpt; + + if (!parent || (parent->type != MOD_SWITCH)) { + cf_log_err_cs(cs, "\"case\" statements may only appear within a \"switch\" section"); + return NULL; + } + + /* + * case THING means "match THING" + * case means "match anything" + */ + name2 = cf_section_name2(cs); + if (name2) { + ssize_t slen; + FR_TOKEN type; + + type = cf_section_name2_type(cs); + + slen = tmpl_afrom_str(cs, &vpt, name2, strlen(name2), type, REQUEST_CURRENT, PAIR_LIST_REQUEST, true); + if ((slen < 0) && ((type != T_BARE_WORD) || (name2[0] != '&'))) { + char *spaces, *text; + + fr_canonicalize_error(cs, &spaces, &text, slen, fr_strerror()); + + cf_log_err_cs(cs, "Syntax error"); + cf_log_err_cs(cs, "%s", name2); + cf_log_err_cs(cs, "%s^ %s", spaces, text); + + talloc_free(spaces); + talloc_free(text); + + return NULL; + } + + if (vpt->type == TMPL_TYPE_LIST) { + cf_log_err_cs(cs, "Syntax error: Cannot match list '%s'", name2); + return NULL; + } + + /* + * Otherwise a NULL vpt may refer to an attribute defined + * by a module. That is checked in pass 2. + */ + + } else { + vpt = NULL; + } + + csingle = do_compile_modgroup(parent, component, cs, + GROUPTYPE_SIMPLE, + GROUPTYPE_SIMPLE, + MOD_CASE); + if (!csingle) { + talloc_free(vpt); + return NULL; + } + + /* + * The interpretor expects this to be NULL for the + * default case. do_compile_modgroup sets it to name2, + * unless name2 is NULL, in which case it sets it to name1. + */ + csingle->name = name2; + + g = mod_callabletogroup(csingle); + g->vpt = talloc_steal(g, vpt); + + /* + * Set all of it's codes to return, so that + * when we pick a 'case' statement, we don't + * fall through to processing the next one. + */ + for (i = 0; i < RLM_MODULE_NUMCODES; i++) { + csingle->actions[i] = MOD_ACTION_RETURN; + } + + return csingle; +} + +static modcallable *do_compile_modforeach(modcallable *parent, + rlm_components_t component, CONF_SECTION *cs) +{ + FR_TOKEN type; + char const *name2; + modcallable *csingle; + modgroup *g; + ssize_t slen; + vp_tmpl_t *vpt; + + name2 = cf_section_name2(cs); + if (!name2) { + cf_log_err_cs(cs, + "You must specify an attribute to loop over in 'foreach'"); + return NULL; + } + + if (!cf_item_find_next(cs, NULL)) { + cf_log_err_cs(cs, "'foreach' blocks cannot be empty"); + return NULL; + } + + /* + * Create the template. If we fail, AND it's a bare word + * with &Foo-Bar, it MAY be an attribute defined by a + * module. Allow it for now. The pass2 checks below + * will fix it up. + */ + type = cf_section_name2_type(cs); + slen = tmpl_afrom_str(cs, &vpt, name2, strlen(name2), type, REQUEST_CURRENT, PAIR_LIST_REQUEST, true); + if ((slen < 0) && ((type != T_BARE_WORD) || (name2[0] != '&'))) { + char *spaces, *text; + + fr_canonicalize_error(cs, &spaces, &text, slen, fr_strerror()); + + cf_log_err_cs(cs, "Syntax error"); + cf_log_err_cs(cs, "%s", name2); + cf_log_err_cs(cs, "%s^ %s", spaces, text); + + talloc_free(spaces); + talloc_free(text); + + return NULL; + } + + /* + * If we don't have a negative return code, we must have a vpt + * (mostly to quiet coverity). + */ + rad_assert(vpt); + + if ((vpt->type != TMPL_TYPE_ATTR) && (vpt->type != TMPL_TYPE_LIST)) { + cf_log_err_cs(cs, "MUST use attribute or list reference in 'foreach'"); + return NULL; + } + + /* + * Fix up the template to iterate over all instances of + * the attribute. In a perfect consistent world, users would do + * foreach &attr[*], but that's taking the consistency thing a bit far. + */ + vpt->tmpl_num = NUM_ALL; + + csingle = do_compile_modgroup(parent, component, cs, + GROUPTYPE_SIMPLE, GROUPTYPE_SIMPLE, + MOD_FOREACH); + + if (!csingle) { + talloc_free(vpt); + return NULL; + } + + g = mod_callabletogroup(csingle); + g->vpt = vpt; + + return csingle; +} + +static modcallable *do_compile_modbreak(modcallable *parent, + rlm_components_t component, CONF_ITEM const *ci) +{ + CONF_SECTION const *cs = NULL; + + for (cs = cf_item_parent(ci); + cs != NULL; + cs = cf_item_parent(cf_section_to_item(cs))) { + if (strcmp(cf_section_name1(cs), "foreach") == 0) { + break; + } + } + + if (!cs) { + cf_log_err(ci, "'break' can only be used in a 'foreach' section"); + return NULL; + } + + return do_compile_modgroup(parent, component, NULL, + GROUPTYPE_SIMPLE, GROUPTYPE_SIMPLE, + MOD_BREAK); +} +#endif + +static modcallable *do_compile_modserver(modcallable *parent, + rlm_components_t component, CONF_ITEM *ci, + char const *name, + CONF_SECTION *cs, + char const *server) +{ + modcallable *csingle; + CONF_SECTION *subcs; + modref *mr; + + subcs = cf_section_sub_find_name2(cs, comp2str[component], NULL); + if (!subcs) { + cf_log_err(ci, "Server %s has no %s section", + server, comp2str[component]); + return NULL; + } + + mr = talloc_zero(parent, modref); + + csingle = mod_reftocallable(mr); + csingle->parent = parent; + csingle->next = NULL; + csingle->name = name; + csingle->type = MOD_REFERENCE; + csingle->method = component; + + memcpy(csingle->actions, defaultactions[component][GROUPTYPE_SIMPLE], + sizeof(csingle->actions)); + + mr->ref_name = strdup(server); + mr->ref_cs = cs; + + return csingle; +} + +static modcallable *do_compile_modxlat(modcallable *parent, + rlm_components_t component, char const *fmt) +{ + modcallable *csingle; + modxlat *mx; + + mx = talloc_zero(parent, modxlat); + + csingle = mod_xlattocallable(mx); + csingle->parent = parent; + csingle->next = NULL; + csingle->name = "expand"; + csingle->type = MOD_XLAT; + csingle->method = component; + + memcpy(csingle->actions, defaultactions[component][GROUPTYPE_SIMPLE], + sizeof(csingle->actions)); + + mx->xlat_name = talloc_strdup(mx, fmt); + if (!mx->xlat_name) { + talloc_free(mx); + return NULL; + } + + if (fmt[0] != '%') { + char *p; + mx->exec = true; + + strcpy(mx->xlat_name, fmt + 1); + p = strrchr(mx->xlat_name, '`'); + if (p) *p = '\0'; + } + + return csingle; +} + +/* + * redundant, etc. can refer to modules or groups, but not much else. + */ +static int all_children_are_modules(CONF_SECTION *cs, char const *name) +{ + CONF_ITEM *ci; + + for (ci=cf_item_find_next(cs, NULL); + ci != NULL; + ci=cf_item_find_next(cs, ci)) { + /* + * If we're a redundant, etc. group, then the + * intention is to call modules, rather than + * processing logic. These checks aren't + * *strictly* necessary, but they keep the users + * from doing crazy things. + */ + if (cf_item_is_section(ci)) { + CONF_SECTION *subcs = cf_item_to_section(ci); + char const *name1 = cf_section_name1(subcs); + + if ((strcmp(name1, "if") == 0) || + (strcmp(name1, "else") == 0) || + (strcmp(name1, "elsif") == 0) || + (strcmp(name1, "update") == 0) || + (strcmp(name1, "switch") == 0) || + (strcmp(name1, "case") == 0)) { + cf_log_err(ci, "%s sections cannot contain a \"%s\" statement", + name, name1); + return 0; + } + continue; + } + + if (cf_item_is_pair(ci)) { + CONF_PAIR *cp = cf_item_to_pair(ci); + if (cf_pair_value(cp) != NULL) { + cf_log_err(ci, + "Entry with no value is invalid"); + return 0; + } + } + } + + return 1; +} + +/** Load a named module from "instantiate" or "policy". + * + * If it's "foo.method", look for "foo", and return "method" as the method + * we wish to use, instead of the input component. + * + * @param[out] pcomponent Where to write the method we found, if any. If no method is specified + * will be set to MOD_COUNT. + * @param[in] real_name Complete name string e.g. foo.authorize. + * @param[in] virtual_name Virtual module name e.g. foo. + * @param[in] method_name Method override (may be NULL) or the method name e.g. authorize. + * @return the CONF_SECTION specifying the virtual module. + */ +static CONF_SECTION *virtual_module_find_cs(rlm_components_t *pcomponent, + char const *real_name, char const *virtual_name, char const *method_name) +{ + CONF_SECTION *cs, *subcs; + rlm_components_t method = *pcomponent; + char buffer[256]; + + /* + * Turn the method name into a method enum. + */ + if (method_name) { + rlm_components_t i; + + for (i = MOD_AUTHENTICATE; i < MOD_COUNT; i++) { + if (strcmp(comp2str[i], method_name) == 0) break; + } + + if (i != MOD_COUNT) { + method = i; + } else { + method_name = NULL; + virtual_name = real_name; + } + } + + /* + * Look for "foo" in the "instantiate" section. If we + * find it, AND there's no method name, we've found the + * right thing. + * + * Return it to the caller, with the updated method. + */ + cs = cf_section_find("instantiate"); + if (cs) { + /* + * Found "foo". Load it as "foo", or "foo.method". + */ + subcs = cf_section_sub_find_name2(cs, NULL, virtual_name); + if (subcs) { + *pcomponent = method; + return subcs; + } + } + + /* + * Look for it in "policy". + * + * If there's no policy section, we can't do anything else. + */ + cs = cf_section_find("policy"); + if (!cs) return NULL; + + /* + * "foo.authorize" means "load policy "foo" as method "authorize". + * + * And bail out if there's no policy "foo". + */ + if (method_name) { + subcs = cf_section_sub_find_name2(cs, NULL, virtual_name); + if (subcs) *pcomponent = method; + + return subcs; + } + + /* + * "foo" means "look for foo.component" first, to allow + * method overrides. If that's not found, just look for + * a policy "foo". + * + */ + snprintf(buffer, sizeof(buffer), "%s.%s", + virtual_name, comp2str[method]); + subcs = cf_section_sub_find_name2(cs, NULL, buffer); + if (subcs) return subcs; + + return cf_section_sub_find_name2(cs, NULL, virtual_name); +} + + +/* + * Compile one entry of a module call. + */ +static modcallable *do_compile_modsingle(modcallable *parent, + rlm_components_t component, CONF_ITEM *ci, + int grouptype, + char const **modname) +{ + char const *modrefname, *p; + modsingle *single; + modcallable *csingle; + module_instance_t *this; + CONF_SECTION *cs, *subcs, *modules; + CONF_SECTION *loop; + char const *realname; + rlm_components_t method = component; + + if (cf_item_is_section(ci)) { + char const *name2; + + cs = cf_item_to_section(ci); + modrefname = cf_section_name1(cs); + name2 = cf_section_name2(cs); + if (!name2) name2 = ""; + + /* + * group{}, redundant{}, or append{} may appear + * where a single module instance was expected. + * In that case, we hand it off to + * compile_modgroup + */ + if (strcmp(modrefname, "group") == 0) { + *modname = name2; + return do_compile_modgroup(parent, component, cs, + GROUPTYPE_SIMPLE, + grouptype, MOD_GROUP); + + } else if (strcmp(modrefname, "redundant") == 0) { + *modname = name2; + + if (!all_children_are_modules(cs, modrefname)) { + return NULL; + } + + return do_compile_modgroup(parent, component, cs, + GROUPTYPE_REDUNDANT, + grouptype, MOD_GROUP); + + } else if (strcmp(modrefname, "load-balance") == 0) { + *modname = name2; + + if (!all_children_are_modules(cs, modrefname)) { + return NULL; + } + + return do_compile_modgroup(parent, component, cs, + GROUPTYPE_SIMPLE, + grouptype, MOD_LOAD_BALANCE); + + } else if (strcmp(modrefname, "redundant-load-balance") == 0) { + *modname = name2; + + if (!all_children_are_modules(cs, modrefname)) { + return NULL; + } + + return do_compile_modgroup(parent, component, cs, + GROUPTYPE_REDUNDANT, + grouptype, MOD_REDUNDANT_LOAD_BALANCE); + +#ifdef WITH_UNLANG + } else if (strcmp(modrefname, "if") == 0) { + if (!cf_section_name2(cs)) { + cf_log_err(ci, "'if' without condition"); + return NULL; + } + + *modname = name2; + csingle= do_compile_modgroup(parent, component, cs, + GROUPTYPE_SIMPLE, + grouptype, MOD_IF); + if (!csingle) return NULL; + *modname = name2; + + return csingle; + + } else if (strcmp(modrefname, "elsif") == 0) { + if (parent && + ((parent->type == MOD_LOAD_BALANCE) || + (parent->type == MOD_REDUNDANT_LOAD_BALANCE))) { + cf_log_err(ci, "'elsif' cannot be used in this section"); + return NULL; + } + + if (!cf_section_name2(cs)) { + cf_log_err(ci, "'elsif' without condition"); + return NULL; + } + + *modname = name2; + return do_compile_modgroup(parent, component, cs, + GROUPTYPE_SIMPLE, + grouptype, MOD_ELSIF); + + } else if (strcmp(modrefname, "else") == 0) { + if (parent && + ((parent->type == MOD_LOAD_BALANCE) || + (parent->type == MOD_REDUNDANT_LOAD_BALANCE))) { + cf_log_err(ci, "'else' cannot be used in this section section"); + return NULL; + } + + if (cf_section_name2(cs)) { + cf_log_err(ci, "Cannot have conditions on 'else'"); + return NULL; + } + + *modname = name2; + return do_compile_modgroup(parent, component, cs, + GROUPTYPE_SIMPLE, + grouptype, MOD_ELSE); + + } else if (strcmp(modrefname, "update") == 0) { + *modname = name2; + + return do_compile_modupdate(parent, component, cs, + name2); + + } else if (strcmp(modrefname, "switch") == 0) { + *modname = name2; + + return do_compile_modswitch (parent, component, cs); + + } else if (strcmp(modrefname, "case") == 0) { + *modname = name2; + + return do_compile_modcase(parent, component, cs); + + } else if (strcmp(modrefname, "foreach") == 0) { + *modname = name2; + + return do_compile_modforeach(parent, component, cs); + +#endif + } /* else it's something like sql { fail = 1 ...} */ + + } else if (!cf_item_is_pair(ci)) { /* CONF_DATA or some such */ + return NULL; + + /* + * Else it's a module reference, with updated return + * codes. + */ + } else { + CONF_PAIR *cp = cf_item_to_pair(ci); + modrefname = cf_pair_attr(cp); + + /* + * Actions (ok = 1), etc. are orthogonal to just + * about everything else. + */ + if (cf_pair_value(cp) != NULL) { + cf_log_err(ci, "Entry is not a reference to a module"); + return NULL; + } + + /* + * In-place xlat's via %{...}. + * + * This should really be removed from the server. + */ + if (((modrefname[0] == '%') && (modrefname[1] == '{')) || + (modrefname[0] == '`')) { + return do_compile_modxlat(parent, component, + modrefname); + } + } + +#ifdef WITH_UNLANG + /* + * These can't be over-ridden. + */ + if (strcmp(modrefname, "break") == 0) { + if (!cf_item_is_pair(ci)) { + cf_log_err(ci, "Invalid use of 'break' as section name."); + return NULL; + } + + return do_compile_modbreak(parent, component, ci); + } + + if (strcmp(modrefname, "return") == 0) { + if (!cf_item_is_pair(ci)) { + cf_log_err(ci, "Invalid use of 'return' as section name."); + return NULL; + } + + return do_compile_modgroup(parent, component, NULL, + GROUPTYPE_SIMPLE, GROUPTYPE_SIMPLE, + MOD_RETURN); + } +#endif + + /* + * Run a virtual server. This is really terrible and + * should be deleted. + */ + if (strncmp(modrefname, "server[", 7) == 0) { + char buffer[256]; + + if (!cf_item_is_pair(ci)) { + cf_log_err(ci, "Invalid syntax"); + return NULL; + } + + strlcpy(buffer, modrefname + 7, sizeof(buffer)); + p = strrchr(buffer, ']'); + if (!p || p[1] != '\0' || (p == buffer)) { + cf_log_err(ci, "Invalid server reference in \"%s\".", modrefname); + return NULL; + } + + buffer[p - buffer] = '\0'; + + cs = cf_section_sub_find_name2(NULL, "server", buffer); + if (!cs) { + cf_log_err(ci, "No such server \"%s\".", buffer); + return NULL; + } + + /* + * Ignore stupid attempts to over-ride the return + * code. + */ + return do_compile_modserver(parent, component, ci, + modrefname, cs, buffer); + } + + /* + * We now have a name. It can be one of two forms. A + * bare module name, or a section named for the module, + * with over-rides for the return codes. + * + * The name can refer to a real module, in the "modules" + * section. In that case, the name will be either the + * first or second name of the sub-section of "modules". + * + * Or, the name can refer to a policy, in the "policy" + * section. In that case, the name will be first name of + * the sub-section of "policy". Unless it's a "redudant" + * block... + * + * Or, the name can refer to a "module.method", in which + * case we're calling a different method than normal for + * this section. + * + * Or, the name can refer to a virtual module, in the + * "instantiate" section. In that case, the name will be + * the first of the sub-section of "instantiate". Unless + * it's a "redudant" block... + * + * We try these in sequence, from the bottom up. This is + * so that things in "instantiate" and "policy" can + * over-ride calls to real modules. + */ + + + /* + * Try: + * + * instantiate { ... name { ...} ... } + * instantiate { ... name.method { ...} ... } + * policy { ... name { .. } .. } + * policy { ... name.method { .. } .. } + * + * The only difference between things in "instantiate" + * and "policy" is that "instantiate" will cause modules + * to be instantiated in a particular order. + */ + subcs = NULL; + p = strrchr(modrefname, '.'); + if (!p) { + subcs = virtual_module_find_cs(&method, modrefname, modrefname, NULL); + } else { + char buffer[256]; + + strlcpy(buffer, modrefname, sizeof(buffer)); + buffer[p - modrefname] = '\0'; + + subcs = virtual_module_find_cs(&method, modrefname, buffer, buffer + (p - modrefname) + 1); + } + + /* + * Check that we're not creating a loop. We may + * be compiling an "sql" module reference inside + * of an "sql" policy. If so, we allow the + * second "sql" to refer to the module. + */ + for (loop = cf_item_parent(ci); + loop && subcs; + loop = cf_item_parent(cf_section_to_item(loop))) { + if (loop == subcs) { + subcs = NULL; + } + } + + /* + * We've found the relevant entry. It MUST be a + * sub-section. + * + * However, it can be a "redundant" block, or just a + * section name. + */ + if (subcs) { + /* + * modules.c takes care of ensuring that this is: + * + * group foo { ... + * load-balance foo { ... + * redundant foo { ... + * redundant-load-balance foo { ... + * + * We can just recurs to compile the section as + * if it was found here. + */ + if (cf_section_name2(subcs)) { + csingle = do_compile_modsingle(parent, + method, + cf_section_to_item(subcs), + grouptype, + modname); + } else { + /* + * We have: + * + * foo { ... + * + * So we compile it like it was: + * + * group foo { ... + */ + csingle = do_compile_modgroup(parent, + method, + subcs, + GROUPTYPE_SIMPLE, + grouptype, MOD_GROUP); + } + + /* + * Return the compiled thing if we can. + */ + if (!csingle) return NULL; + if (cf_item_is_pair(ci)) return csingle; + + /* + * Else we have a reference to a policy, and that reference + * over-rides the return codes for the policy! + */ + goto action_override; + } + + /* + * Not a virtual module. It must be a real module. + */ + modules = cf_section_find("modules"); + this = NULL; + realname = modrefname; + + if (modules) { + /* + * Try to load the optional module. + */ + if (realname[0] == '-') realname++; + + /* + * As of v3, the "modules" section contains + * modules we use. Configuration for other + * modules belongs in raddb/mods-available/, + * which isn't loaded into the "modules" section. + */ + this = module_instantiate_method(modules, realname, &method); + if (this) goto allocate_csingle; + + /* + * We were asked to MAYBE load it and it + * doesn't exist. Return a soft error. + */ + if (realname != modrefname) { + *modname = modrefname; + return NULL; + } + } + + /* + * Can't de-reference it to anything. Ugh. + */ + *modname = NULL; + cf_log_err(ci, "Failed to find \"%s\" as a module or policy.", modrefname); + cf_log_err(ci, "Please verify that the configuration exists in %s/mods-enabled/%s.", get_radius_dir(), modrefname); + return NULL; + + /* + * We know it's all OK, allocate the structures, and fill + * them in. + */ +allocate_csingle: + /* + * Check if the module in question has the necessary + * component. + */ + if (!this->entry->module->methods[method]) { + cf_log_err(ci, "\"%s\" modules aren't allowed in '%s' sections -- they have no such method.", this->entry->module->name, + comp2str[method]); + return NULL; + } + + single = talloc_zero(parent, modsingle); + single->modinst = this; + *modname = this->entry->module->name; + + csingle = mod_singletocallable(single); + csingle->parent = parent; + csingle->next = NULL; + if (!parent || (component != MOD_AUTHENTICATE)) { + memcpy(csingle->actions, defaultactions[component][grouptype], + sizeof csingle->actions); + } else { /* inside Auth-Type has different rules */ + memcpy(csingle->actions, authtype_actions[grouptype], + sizeof csingle->actions); + } + rad_assert(modrefname != NULL); + csingle->name = realname; + csingle->type = MOD_SINGLE; + csingle->method = method; + +action_override: + /* + * Over-ride the default return codes of the module. + */ + if (cf_item_is_section(ci)) { + CONF_ITEM *csi; + + cs = cf_item_to_section(ci); + for (csi=cf_item_find_next(cs, NULL); + csi != NULL; + csi=cf_item_find_next(cs, csi)) { + + if (cf_item_is_section(csi)) { + cf_log_err(csi, "Subsection of module instance call not allowed"); + talloc_free(csingle); + return NULL; + } + + if (!cf_item_is_pair(csi)) continue; + + if (!compile_action(csingle, cf_item_to_pair(csi))) { + talloc_free(csingle); + return NULL; + } + } + } + + return csingle; +} + +modcallable *compile_modsingle(TALLOC_CTX *ctx, + modcallable **parent, + rlm_components_t component, CONF_ITEM *ci, + char const **modname) +{ + modcallable *ret; + + if (!*parent) { + modcallable *c; + modgroup *g; + CONF_SECTION *parentcs; + + g = talloc_zero(ctx, modgroup); + memset(g, 0, sizeof(*g)); + g->grouptype = GROUPTYPE_SIMPLE; + c = mod_grouptocallable(g); + c->next = NULL; + memcpy(c->actions, + defaultactions[component][GROUPTYPE_SIMPLE], + sizeof(c->actions)); + + parentcs = cf_item_parent(ci); + c->name = cf_section_name2(parentcs); + if (!c->name) { + c->name = cf_section_name1(parentcs); + } + + c->type = MOD_GROUP; + c->method = component; + g->children = NULL; + + *parent = mod_grouptocallable(g); + } + + ret = do_compile_modsingle(*parent, component, ci, + GROUPTYPE_SIMPLE, + modname); + dump_tree(component, ret); + return ret; +} + + +/* + * Internal compile group code. + */ +static modcallable *do_compile_modgroup(modcallable *parent, + rlm_components_t component, CONF_SECTION *cs, + int grouptype, int parentgrouptype, int mod_type) +{ + int i; + modgroup *g; + modcallable *c; + CONF_ITEM *ci; + + g = talloc_zero(parent, modgroup); + g->grouptype = grouptype; + g->children = NULL; + g->cs = cs; + + c = mod_grouptocallable(g); + c->parent = parent; + c->type = mod_type; + c->next = NULL; + memset(c->actions, 0, sizeof(c->actions)); + + if (!cs) { /* only for "break" and "return" */ + c->name = ""; + goto set_codes; + } + + /* + * Remember the name for printing, etc. + * + * FIXME: We may also want to put the names into a + * rbtree, so that groups can reference each other... + */ + c->name = cf_section_name2(cs); + if (!c->name) { + c->name = cf_section_name1(cs); + if ((strcmp(c->name, "group") == 0) || + (strcmp(c->name, "redundant") == 0)) { + c->name = ""; + } else if (c->type == MOD_GROUP) { + c->type = MOD_POLICY; + } + } + +#ifdef WITH_UNLANG + /* + * Do load-time optimizations + */ + if ((c->type == MOD_IF) || (c->type == MOD_ELSIF) || (c->type == MOD_ELSE)) { + modgroup *f, *p; + + rad_assert(parent != NULL); + + if (c->type == MOD_IF) { + g->cond = cf_data_find(g->cs, "if"); + rad_assert(g->cond != NULL); + + check_if: + if (g->cond->type == COND_TYPE_FALSE) { + INFO(" # Skipping contents of '%s' as it is always 'false' -- %s:%d", + unlang_keyword[g->mc.type], + cf_section_filename(g->cs), cf_section_lineno(g->cs)); + goto set_codes; + } + + } else if (c->type == MOD_ELSIF) { + + g->cond = cf_data_find(g->cs, "if"); + rad_assert(g->cond != NULL); + + rad_assert(parent != NULL); + p = mod_callabletogroup(parent); + + if (!p->tail) goto elsif_fail; + + /* + * We're in the process of compiling the + * section, so the parent's tail is the + * previous "if" statement. + */ + f = mod_callabletogroup(p->tail); + if ((f->mc.type != MOD_IF) && + (f->mc.type != MOD_ELSIF)) { + elsif_fail: + cf_log_err_cs(g->cs, "Invalid location for 'elsif'. There is no preceding 'if' statement"); + talloc_free(g); + return NULL; + } + + /* + * If we took the previous condition, we + * don't need to take this one. + * + * We reset our condition to 'true', so + * that subsequent sections can check + * that they don't need to be executed. + */ + if (f->cond->type == COND_TYPE_TRUE) { + skip_true: + INFO(" # Skipping contents of '%s' as previous '%s' is always 'true' -- %s:%d", + unlang_keyword[g->mc.type], + unlang_keyword[f->mc.type], + cf_section_filename(g->cs), cf_section_lineno(g->cs)); + g->cond = f->cond; + goto set_codes; + } + goto check_if; + + } else { + rad_assert(c->type == MOD_ELSE); + + rad_assert(parent != NULL); + p = mod_callabletogroup(parent); + + if (!p->tail) goto else_fail; + + f = mod_callabletogroup(p->tail); + if ((f->mc.type != MOD_IF) && + (f->mc.type != MOD_ELSIF)) { + else_fail: + cf_log_err_cs(g->cs, "Invalid location for 'else'. There is no preceding 'if' statement"); + talloc_free(g); + return NULL; + } + + /* + * If we took the previous condition, we + * don't need to take this one. + */ + if (f->cond->type == COND_TYPE_TRUE) goto skip_true; + } + + /* + * Else we need to compile this section + */ + } +#endif + + /* + * Loop over the children of this group. + */ + for (ci=cf_item_find_next(cs, NULL); + ci != NULL; + ci=cf_item_find_next(cs, ci)) { + + /* + * Sections are references to other groups, or + * to modules with updated return codes. + */ + if (cf_item_is_section(ci)) { + char const *junk = NULL; + modcallable *single; + CONF_SECTION *subcs = cf_item_to_section(ci); + + single = do_compile_modsingle(c, component, ci, + grouptype, &junk); + if (!single) { + cf_log_err(ci, "Failed to parse \"%s\" subsection.", + cf_section_name1(subcs)); + talloc_free(c); + return NULL; + } + add_child(g, single); + + } else if (!cf_item_is_pair(ci)) { /* CONF_DATA */ + continue; + + } else { + char const *attr, *value; + CONF_PAIR *cp = cf_item_to_pair(ci); + + attr = cf_pair_attr(cp); + value = cf_pair_value(cp); + + /* + * A CONF_PAIR is either a module + * instance with no actions + * specified ... + */ + if (!value) { + modcallable *single; + char const *junk = NULL; + + single = do_compile_modsingle(c, + component, + ci, + grouptype, + &junk); + if (!single) { + if (cf_item_is_pair(ci) && + cf_pair_attr(cf_item_to_pair(ci))[0] == '-') { + continue; + } + + cf_log_err(ci, + "Failed to parse \"%s\" entry.", + attr); + talloc_free(c); + return NULL; + } + add_child(g, single); + + /* + * Or a module instance with action. + */ + } else if (!compile_action(c, cp)) { + talloc_free(c); + return NULL; + } /* else it worked */ + } + } + +set_codes: + /* + * Set the default actions, if they haven't already been + * set. + */ + for (i = 0; i < RLM_MODULE_NUMCODES; i++) { + if (!c->actions[i]) { + if (!parent || (component != MOD_AUTHENTICATE)) { + c->actions[i] = defaultactions[component][parentgrouptype][i]; + } else { /* inside Auth-Type has different rules */ + c->actions[i] = authtype_actions[parentgrouptype][i]; + } + } + } + + switch (c->type) { + default: + break; + + case MOD_GROUP: + if (grouptype != GROUPTYPE_REDUNDANT) break; + /* FALL-THROUGH */ + + case MOD_LOAD_BALANCE: + case MOD_REDUNDANT_LOAD_BALANCE: + if (!g->children) { + cf_log_err_cs(g->cs, "%s sections cannot be empty", + cf_section_name1(g->cs)); + talloc_free(c); + return NULL; + } + } + + /* + * FIXME: If there are no children, return NULL? + */ + return mod_grouptocallable(g); +} + +modcallable *compile_modgroup(modcallable *parent, + rlm_components_t component, CONF_SECTION *cs) +{ + modcallable *ret = do_compile_modgroup(parent, component, cs, + GROUPTYPE_SIMPLE, + GROUPTYPE_SIMPLE, MOD_GROUP); + + if (rad_debug_lvl > 3) { + modcall_debug(ret, 2); + } + + return ret; +} + +void add_to_modcallable(modcallable *parent, modcallable *this) +{ + modgroup *g; + + rad_assert(this != NULL); + rad_assert(parent != NULL); + + g = mod_callabletogroup(parent); + + add_child(g, this); +} + + +#ifdef WITH_UNLANG +static bool pass2_xlat_compile(CONF_ITEM const *ci, vp_tmpl_t **pvpt, bool convert, + DICT_ATTR const *da) +{ + ssize_t slen; + char *fmt; + char const *error; + xlat_exp_t *head; + vp_tmpl_t *vpt; + + vpt = *pvpt; + + rad_assert(vpt->type == TMPL_TYPE_XLAT); + + fmt = talloc_typed_strdup(vpt, vpt->name); + slen = xlat_tokenize(vpt, fmt, &head, &error); + + if (slen < 0) { + char *spaces, *text; + + fr_canonicalize_error(vpt, &spaces, &text, slen, vpt->name); + + cf_log_err(ci, "Failed parsing expanded string:"); + cf_log_err(ci, "%s", text); + cf_log_err(ci, "%s^ %s", spaces, error); + + talloc_free(spaces); + talloc_free(text); + return false; + } + + /* + * Convert %{Attribute-Name} to &Attribute-Name + */ + if (convert) { + vp_tmpl_t *attr; + + attr = xlat_to_tmpl_attr(talloc_parent(vpt), head); + if (attr) { + /* + * If it's a virtual attribute, leave it + * alone. + */ + if (attr->tmpl_da->flags.virtual) { + talloc_free(attr); + return true; + } + + /* + * If the attribute is of incompatible + * type, leave it alone. + */ + if (da && (da->type != attr->tmpl_da->type)) { + talloc_free(attr); + return true; + } + + if (cf_item_is_pair(ci)) { + CONF_PAIR *cp = cf_item_to_pair(ci); + + WARN("%s[%d]: Please change \"%%{%s}\" to &%s", + cf_pair_filename(cp), cf_pair_lineno(cp), + attr->name, attr->name); + } else { + CONF_SECTION *cs = cf_item_to_section(ci); + + WARN("%s[%d]: Please change \"%%{%s}\" to &%s", + cf_section_filename(cs), cf_section_lineno(cs), + attr->name, attr->name); + } + TALLOC_FREE(*pvpt); + *pvpt = attr; + return true; + } + } + + /* + * Re-write it to be a pre-parsed XLAT structure. + */ + vpt->type = TMPL_TYPE_XLAT_STRUCT; + vpt->tmpl_xlat = head; + + return true; +} + + +#ifdef HAVE_REGEX +static bool pass2_regex_compile(CONF_ITEM const *ci, vp_tmpl_t *vpt) +{ + ssize_t slen; + regex_t *preg; + + rad_assert(vpt->type == TMPL_TYPE_REGEX); + + /* + * It's a dynamic expansion. We can't expand the string, + * but we can pre-parse it as an xlat struct. In that + * case, we convert it to a pre-compiled XLAT. + * + * This is a little more complicated than it needs to be + * because radius_evaluate_map() keys off of the src + * template type, instead of the operators. And, the + * pass2_xlat_compile() function expects to get passed an + * XLAT instead of a REGEX. + */ + if (strchr(vpt->name, '%')) { + vpt->type = TMPL_TYPE_XLAT; + return pass2_xlat_compile(ci, &vpt, false, NULL); + } + + slen = regex_compile(vpt, &preg, vpt->name, vpt->len, + vpt->tmpl_iflag, vpt->tmpl_mflag, true, false); + if (slen <= 0) { + char *spaces, *text; + + fr_canonicalize_error(vpt, &spaces, &text, slen, vpt->name); + + cf_log_err(ci, "Invalid regular expression:"); + cf_log_err(ci, "%s", text); + cf_log_err(ci, "%s^ %s", spaces, fr_strerror()); + + talloc_free(spaces); + talloc_free(text); + + return false; + } + + vpt->type = TMPL_TYPE_REGEX_STRUCT; + vpt->tmpl_preg = preg; + + return true; +} +#endif + +static bool pass2_fixup_undefined(CONF_ITEM const *ci, vp_tmpl_t *vpt) +{ + DICT_ATTR const *da; + + rad_assert(vpt->type == TMPL_TYPE_ATTR_UNDEFINED); + + da = dict_attrbyname(vpt->tmpl_unknown_name); + if (!da) { + cf_log_err(ci, "Unknown attribute '%s'", vpt->tmpl_unknown_name); + return false; + } + + vpt->tmpl_da = da; + vpt->type = TMPL_TYPE_ATTR; + return true; +} + +static bool pass2_callback(void *ctx, fr_cond_t *c) +{ + vp_map_t *map; + vp_tmpl_t *vpt; + + /* + * These don't get optimized. + */ + if ((c->type == COND_TYPE_TRUE) || + (c->type == COND_TYPE_FALSE)) { + return true; + } + + /* + * Call children. + */ + if (c->type == COND_TYPE_CHILD) return pass2_callback(ctx, c->data.child); + + /* + * A few simple checks here. + */ + if (c->type == COND_TYPE_EXISTS) { + if (c->data.vpt->type == TMPL_TYPE_XLAT) { + return pass2_xlat_compile(c->ci, &c->data.vpt, true, NULL); + } + + rad_assert(c->data.vpt->type != TMPL_TYPE_REGEX); + + /* + * The existence check might have been &Foo-Bar, + * where Foo-Bar is defined by a module. + */ + if (c->pass2_fixup == PASS2_FIXUP_ATTR) { + if (!pass2_fixup_undefined(c->ci, c->data.vpt)) return false; + c->pass2_fixup = PASS2_FIXUP_NONE; + } + + /* + * Convert virtual &Attr-Foo to "%{Attr-Foo}" + */ + vpt = c->data.vpt; + if ((vpt->type == TMPL_TYPE_ATTR) && vpt->tmpl_da->flags.virtual) { + vpt->tmpl_xlat = xlat_from_tmpl_attr(vpt, vpt); + vpt->type = TMPL_TYPE_XLAT_STRUCT; + } + + return true; + } + + /* + * And tons of complicated checks. + */ + rad_assert(c->type == COND_TYPE_MAP); + + map = c->data.map; /* shorter */ + + /* + * Auth-Type := foo + * + * Where "foo" is dynamically defined. + */ + if (c->pass2_fixup == PASS2_FIXUP_TYPE) { + if (!dict_valbyname(map->lhs->tmpl_da->attr, + map->lhs->tmpl_da->vendor, + map->rhs->name)) { + cf_log_err(map->ci, "Invalid reference to non-existent %s %s { ... }", + map->lhs->tmpl_da->name, + map->rhs->name); + return false; + } + + /* + * These guys can't have a paircompare fixup applied. + */ + c->pass2_fixup = PASS2_FIXUP_NONE; + return true; + } + + if (c->pass2_fixup == PASS2_FIXUP_ATTR) { + if (map->lhs->type == TMPL_TYPE_ATTR_UNDEFINED) { + if (!pass2_fixup_undefined(map->ci, map->lhs)) return false; + } + + if (map->rhs->type == TMPL_TYPE_ATTR_UNDEFINED) { + if (!pass2_fixup_undefined(map->ci, map->rhs)) return false; + } + + c->pass2_fixup = PASS2_FIXUP_NONE; + } + + /* + * Just in case someone adds a new fixup later. + */ + rad_assert((c->pass2_fixup == PASS2_FIXUP_NONE) || + (c->pass2_fixup == PASS2_PAIRCOMPARE)); + + /* + * Precompile xlat's + */ + if (map->lhs->type == TMPL_TYPE_XLAT) { + /* + * Compile the LHS to an attribute reference only + * if the RHS is a literal. + * + * @todo v3.1: allow anything anywhere. + */ + if (map->rhs->type != TMPL_TYPE_LITERAL) { + if (!pass2_xlat_compile(map->ci, &map->lhs, false, NULL)) { + return false; + } + } else { + if (!pass2_xlat_compile(map->ci, &map->lhs, true, NULL)) { + return false; + } + + /* + * Attribute compared to a literal gets + * the literal cast to the data type of + * the attribute. + * + * The code in parser.c did this for + * + * &Attr == data + * + * But now we've just converted "%{Attr}" + * to &Attr, so we've got to do it again. + */ + if ((map->lhs->type == TMPL_TYPE_ATTR) && + (map->rhs->type == TMPL_TYPE_LITERAL)) { + /* + * RHS is hex, try to parse it as + * type-specific data. + */ + if (map->lhs->auto_converted && + (map->rhs->name[0] == '0') && (map->rhs->name[1] == 'x') && + (map->rhs->len > 2) && ((map->rhs->len & 0x01) == 0)) { + vpt = map->rhs; + map->rhs = NULL; + + if (!map_cast_from_hex(map, T_BARE_WORD, vpt->name)) { + map->rhs = vpt; + cf_log_err(map->ci, "Cannot parse RHS hex as the data type of the attribute %s", map->lhs->tmpl_da->name); + return -1; + } + talloc_free(vpt); + + } else if ((map->rhs->len > 0) || + (map->op != T_OP_CMP_EQ) || + (map->lhs->tmpl_da->type == PW_TYPE_STRING) || + (map->lhs->tmpl_da->type == PW_TYPE_OCTETS)) { + + if (tmpl_cast_in_place(map->rhs, map->lhs->tmpl_da->type, map->lhs->tmpl_da) < 0) { + cf_log_err(map->ci, "Failed to parse data type %s from string: %s", + fr_int2str(dict_attr_types, map->lhs->tmpl_da->type, "<UNKNOWN>"), + map->rhs->name); + return false; + } /* else the cast was successful */ + + } else { /* RHS is empty, it's just a check for empty / non-empty string */ + vpt = talloc_steal(c, map->lhs); + map->lhs = NULL; + talloc_free(c->data.map); + + /* + * "%{Foo}" == '' ---> !Foo + * "%{Foo}" != '' ---> Foo + */ + c->type = COND_TYPE_EXISTS; + c->data.vpt = vpt; + c->negate = !c->negate; + + WARN("%s[%d]: Please change (\"%%{%s}\" %s '') to %c&%s", + cf_section_filename(cf_item_to_section(c->ci)), + cf_section_lineno(cf_item_to_section(c->ci)), + vpt->name, c->negate ? "==" : "!=", + c->negate ? '!' : ' ', vpt->name); + + /* + * No more RHS, so we can't do more optimizations + */ + return true; + } + } + } + } + + if (map->rhs->type == TMPL_TYPE_XLAT) { + /* + * Convert the RHS to an attribute reference only + * if the LHS is an attribute reference, AND is + * of the same type as the RHS. + * + * We can fix this when the code in evaluate.c + * can handle strings on the LHS, and attributes + * on the RHS. For now, the code in parser.c + * forbids this. + */ + if (map->lhs->type == TMPL_TYPE_ATTR) { + DICT_ATTR const *da = c->cast; + + if (!c->cast) da = map->lhs->tmpl_da; + + if (!pass2_xlat_compile(map->ci, &map->rhs, true, da)) { + return false; + } + + } else { + if (!pass2_xlat_compile(map->ci, &map->rhs, false, NULL)) { + return false; + } + } + } + + /* + * Convert bare refs to %{Foreach-Variable-N} + */ + if ((map->lhs->type == TMPL_TYPE_LITERAL) && + (strncmp(map->lhs->name, "Foreach-Variable-", 17) == 0)) { + char *fmt; + ssize_t slen; + + fmt = talloc_asprintf(map->lhs, "%%{%s}", map->lhs->name); + slen = tmpl_afrom_str(map, &vpt, fmt, talloc_array_length(fmt) - 1, + T_DOUBLE_QUOTED_STRING, REQUEST_CURRENT, PAIR_LIST_REQUEST, true); + if (slen < 0) { + char *spaces, *text; + + fr_canonicalize_error(map->ci, &spaces, &text, slen, fr_strerror()); + + cf_log_err(map->ci, "Failed converting %s to xlat", map->lhs->name); + cf_log_err(map->ci, "%s", fmt); + cf_log_err(map->ci, "%s^ %s", spaces, text); + + talloc_free(spaces); + talloc_free(text); + talloc_free(fmt); + + return false; + } + talloc_free(map->lhs); + map->lhs = vpt; + } + +#ifdef HAVE_REGEX + if (map->rhs->type == TMPL_TYPE_REGEX) { + if (!pass2_regex_compile(map->ci, map->rhs)) { + return false; + } + } + rad_assert(map->lhs->type != TMPL_TYPE_REGEX); +#endif + + /* + * Convert &Packet-Type to "%{Packet-Type}", because + * these attributes don't really exist. The code to + * find an attribute reference doesn't work, but the + * xlat code does. + */ + vpt = c->data.map->lhs; + if ((vpt->type == TMPL_TYPE_ATTR) && vpt->tmpl_da->flags.virtual) { + if (!c->cast) c->cast = vpt->tmpl_da; + vpt->tmpl_xlat = xlat_from_tmpl_attr(vpt, vpt); + vpt->type = TMPL_TYPE_XLAT_STRUCT; + } + + /* + * Convert RHS to expansions, too. + */ + vpt = c->data.map->rhs; + if ((vpt->type == TMPL_TYPE_ATTR) && vpt->tmpl_da->flags.virtual) { + vpt->tmpl_xlat = xlat_from_tmpl_attr(vpt, vpt); + vpt->type = TMPL_TYPE_XLAT_STRUCT; + } + + /* + * @todo v3.1: do the same thing for the RHS... + */ + + /* + * Only attributes can have a paircompare registered, and + * they can only be with the current REQUEST, and only + * with the request pairs. + */ + if ((map->lhs->type != TMPL_TYPE_ATTR) || + (map->lhs->tmpl_request != REQUEST_CURRENT) || + (map->lhs->tmpl_list != PAIR_LIST_REQUEST)) { + return true; + } + + if (!radius_find_compare(map->lhs->tmpl_da)) return true; + + if (map->rhs->type == TMPL_TYPE_REGEX) { + cf_log_err(map->ci, "Cannot compare virtual attribute %s via a regex", + map->lhs->name); + return false; + } + + if (c->cast) { + cf_log_err(map->ci, "Cannot cast virtual attribute %s", + map->lhs->name); + return false; + } + + if (map->op != T_OP_CMP_EQ) { + cf_log_err(map->ci, "Must use '==' for comparisons with virtual attribute %s", + map->lhs->name); + return false; + } + + /* + * Mark it as requiring a paircompare() call, instead of + * fr_pair_cmp(). + */ + c->pass2_fixup = PASS2_PAIRCOMPARE; + + return true; +} + + +/* + * Compile the RHS of update sections to xlat_exp_t + */ +static bool modcall_pass2_update(modgroup *g) +{ + vp_map_t *map; + + for (map = g->map; map != NULL; map = map->next) { + if (map->rhs->type == TMPL_TYPE_XLAT) { + rad_assert(map->rhs->tmpl_xlat == NULL); + + /* + * FIXME: compile to attribute && handle + * the conversion in map_to_vp(). + */ + if (!pass2_xlat_compile(map->ci, &map->rhs, false, NULL)) { + return false; + } + } + + rad_assert(map->rhs->type != TMPL_TYPE_REGEX); + + /* + * Deal with undefined attributes now. + */ + if (map->lhs->type == TMPL_TYPE_ATTR_UNDEFINED) { + if (!pass2_fixup_undefined(map->ci, map->lhs)) return false; + } + + if (map->rhs->type == TMPL_TYPE_ATTR_UNDEFINED) { + if (!pass2_fixup_undefined(map->ci, map->rhs)) return false; + } + } + + return true; +} +#endif + +/* + * Do a second-stage pass on compiling the modules. + */ +bool modcall_pass2(modcallable *mc) +{ + ssize_t slen; + char const *name2; + modcallable *c; + modgroup *g; + + for (c = mc; c != NULL; c = c->next) { + switch (c->type) { + default: + rad_assert(0 == 1); + break; + +#ifdef WITH_UNLANG + case MOD_UPDATE: + g = mod_callabletogroup(c); + if (g->done_pass2) goto do_next; + + name2 = cf_section_name2(g->cs); + if (!name2) { + c->debug_name = unlang_keyword[c->type]; + } else { + c->debug_name = talloc_asprintf(c, "update %s", name2); + } + + if (!modcall_pass2_update(g)) { + return false; + } + g->done_pass2 = true; + break; + + case MOD_XLAT: /* @todo: pre-parse xlat's */ + case MOD_REFERENCE: + case MOD_BREAK: + case MOD_RETURN: +#endif + + case MOD_SINGLE: + c->debug_name = c->name; + break; /* do nothing */ + +#ifdef WITH_UNLANG + case MOD_IF: + case MOD_ELSIF: + g = mod_callabletogroup(c); + if (g->done_pass2) goto do_next; + + name2 = cf_section_name2(g->cs); + c->debug_name = talloc_asprintf(c, "%s %s", unlang_keyword[c->type], name2); + + /* + * The compilation code takes care of + * simplifying 'true' and 'false' + * conditions. For others, we have to do + * a second pass to parse && compile + * xlats. + */ + if (!((g->cond->type == COND_TYPE_TRUE) || + (g->cond->type == COND_TYPE_FALSE))) { + if (!fr_condition_walk(g->cond, pass2_callback, NULL)) { + return false; + } + } + + if (!modcall_pass2(g->children)) return false; + g->done_pass2 = true; + break; +#endif + +#ifdef WITH_UNLANG + case MOD_SWITCH: + g = mod_callabletogroup(c); + if (g->done_pass2) goto do_next; + + name2 = cf_section_name2(g->cs); + c->debug_name = talloc_asprintf(c, "%s %s", unlang_keyword[c->type], name2); + + /* + * We had &Foo-Bar, where Foo-Bar is + * defined by a module. + */ + if (!g->vpt) { + rad_assert(c->name != NULL); + rad_assert(c->name[0] == '&'); + rad_assert(cf_section_name2_type(g->cs) == T_BARE_WORD); + + slen = tmpl_afrom_str(g->cs, &g->vpt, c->name, strlen(c->name), + cf_section_name2_type(g->cs), + REQUEST_CURRENT, PAIR_LIST_REQUEST, true); + if (slen < 0) { + char *spaces, *text; + + parse_error: + fr_canonicalize_error(g->cs, &spaces, &text, slen, fr_strerror()); + + cf_log_err_cs(g->cs, "Syntax error"); + cf_log_err_cs(g->cs, "%s", c->name); + cf_log_err_cs(g->cs, "%s^ %s", spaces, text); + + talloc_free(spaces); + talloc_free(text); + + return false; + } + + goto do_children; + } + + /* + * Statically compile xlats + */ + if (g->vpt->type == TMPL_TYPE_XLAT) { + if (!pass2_xlat_compile(cf_section_to_item(g->cs), + &g->vpt, true, NULL)) { + return false; + } + + goto do_children; + } + + /* + * Convert virtual &Attr-Foo to "%{Attr-Foo}" + */ + if ((g->vpt->type == TMPL_TYPE_ATTR) && g->vpt->tmpl_da->flags.virtual) { + g->vpt->tmpl_xlat = xlat_from_tmpl_attr(g->vpt, g->vpt); + g->vpt->type = TMPL_TYPE_XLAT_STRUCT; + } + + /* + * We may have: switch Foo-Bar { + * + * where Foo-Bar is an attribute defined + * by a module. Since there's no leading + * &, it's parsed as a literal. But if + * we can parse it as an attribute, + * switch to using that. + */ + if (g->vpt->type == TMPL_TYPE_LITERAL) { + vp_tmpl_t *vpt; + + slen = tmpl_afrom_str(g->cs, &vpt, c->name, strlen(c->name), cf_section_name2_type(g->cs), + REQUEST_CURRENT, PAIR_LIST_REQUEST, true); + if (slen < 0) goto parse_error; + if (vpt->type == TMPL_TYPE_ATTR) { + talloc_free(g->vpt); + g->vpt = vpt; + } + + goto do_children; + } + + /* + * Warn about old-style configuration. + * + * DEPRECATED: switch User-Name { ... + * ALLOWED : switch &User-Name { ... + */ + if ((g->vpt->type == TMPL_TYPE_ATTR) && + (c->name[0] != '&')) { + WARN("%s[%d]: Please change %s to &%s", + cf_section_filename(g->cs), + cf_section_lineno(g->cs), + c->name, c->name); + } + + do_children: + if (!modcall_pass2(g->children)) return false; + g->done_pass2 = true; + break; + + case MOD_CASE: + g = mod_callabletogroup(c); + if (g->done_pass2) goto do_next; + + name2 = cf_section_name2(g->cs); + if (!name2) { + c->debug_name = unlang_keyword[c->type]; + } else { + c->debug_name = talloc_asprintf(c, "%s %s", unlang_keyword[c->type], name2); + } + + rad_assert(c->parent != NULL); + rad_assert(c->parent->type == MOD_SWITCH); + + /* + * The statement may refer to an + * attribute which doesn't exist until + * all of the modules have been loaded. + * Check for that now. + */ + if (!g->vpt && c->name && + (c->name[0] == '&') && + (cf_section_name2_type(g->cs) == T_BARE_WORD)) { + slen = tmpl_afrom_str(g->cs, &g->vpt, c->name, strlen(c->name), + cf_section_name2_type(g->cs), + REQUEST_CURRENT, PAIR_LIST_REQUEST, true); + if (slen < 0) goto parse_error; + } + + /* + * We have "case {...}". There's no + * argument, so we don't need to check + * it. + */ + if (!g->vpt) goto do_children; + + /* + * Do type-specific checks on the case statement + */ + if (g->vpt->type == TMPL_TYPE_LITERAL) { + modgroup *f; + + f = mod_callabletogroup(mc->parent); + rad_assert(f->vpt != NULL); + + /* + * We're switching over an + * attribute. Check that the + * values match. + */ + if (f->vpt->type == TMPL_TYPE_ATTR) { + rad_assert(f->vpt->tmpl_da != NULL); + + if (tmpl_cast_in_place(g->vpt, f->vpt->tmpl_da->type, f->vpt->tmpl_da) < 0) { + cf_log_err_cs(g->cs, "Invalid argument for case statement: %s", + fr_strerror()); + return false; + } + } + + goto do_children; + } + + if (g->vpt->type == TMPL_TYPE_ATTR_UNDEFINED) { + if (!pass2_fixup_undefined(cf_section_to_item(g->cs), g->vpt)) { + return false; + } + } + + /* + * Compile and sanity check xlat + * expansions. + */ + if (g->vpt->type == TMPL_TYPE_XLAT) { + modgroup *f; + + f = mod_callabletogroup(mc->parent); + rad_assert(f->vpt != NULL); + + /* + * Don't expand xlat's into an + * attribute of a different type. + */ + if (f->vpt->type == TMPL_TYPE_ATTR) { + if (!pass2_xlat_compile(cf_section_to_item(g->cs), + &g->vpt, true, f->vpt->tmpl_da)) { + return false; + } + } else { + if (!pass2_xlat_compile(cf_section_to_item(g->cs), + &g->vpt, true, NULL)) { + return false; + } + } + } + + /* + * Virtual attribute fixes for "case" statements, too. + */ + if ((g->vpt->type == TMPL_TYPE_ATTR) && g->vpt->tmpl_da->flags.virtual) { + g->vpt->tmpl_xlat = xlat_from_tmpl_attr(g->vpt, g->vpt); + g->vpt->type = TMPL_TYPE_XLAT_STRUCT; + } + + if (!modcall_pass2(g->children)) return false; + g->done_pass2 = true; + break; + + case MOD_FOREACH: + g = mod_callabletogroup(c); + if (g->done_pass2) goto do_next; + + name2 = cf_section_name2(g->cs); + c->debug_name = talloc_asprintf(c, "%s %s", unlang_keyword[c->type], name2); + + /* + * Already parsed, handle the children. + */ + if (g->vpt) goto check_children; + + /* + * We had &Foo-Bar, where Foo-Bar is + * defined by a module. + */ + rad_assert(c->name != NULL); + rad_assert(c->name[0] == '&'); + rad_assert(cf_section_name2_type(g->cs) == T_BARE_WORD); + + /* + * The statement may refer to an + * attribute which doesn't exist until + * all of the modules have been loaded. + * Check for that now. + */ + slen = tmpl_afrom_str(g->cs, &g->vpt, c->name, strlen(c->name), cf_section_name2_type(g->cs), + REQUEST_CURRENT, PAIR_LIST_REQUEST, true); + if (slen < 0) goto parse_error; + + check_children: + rad_assert((g->vpt->type == TMPL_TYPE_ATTR) || (g->vpt->type == TMPL_TYPE_LIST)); + if (g->vpt->tmpl_num != NUM_ALL) { + cf_log_err_cs(g->cs, "MUST NOT use instance selectors in 'foreach'"); + return false; + } + if (!modcall_pass2(g->children)) return false; + g->done_pass2 = true; + break; + + case MOD_ELSE: + c->debug_name = unlang_keyword[c->type]; + goto do_recurse; + + case MOD_POLICY: + g = mod_callabletogroup(c); + c->debug_name = talloc_asprintf(c, "%s %s", unlang_keyword[c->type], cf_section_name1(g->cs)); + goto do_recurse; +#endif + + case MOD_GROUP: + case MOD_LOAD_BALANCE: + case MOD_REDUNDANT_LOAD_BALANCE: + c->debug_name = unlang_keyword[c->type]; + +#ifdef WITH_UNLANG + do_recurse: +#endif + g = mod_callabletogroup(c); + if (!g->cs) { + c->debug_name = mc->name; /* for authorize, etc. */ + + } else if (c->type == MOD_GROUP) { /* for Auth-Type, etc. */ + char const *name1 = cf_section_name1(g->cs); + + if (strcmp(name1, unlang_keyword[c->type]) != 0) { + name2 = cf_section_name2(g->cs); + + if (!name2) { + c->debug_name = name1; + } else { + c->debug_name = talloc_asprintf(c, "%s %s", name1, name2); + } + } + } + + if (g->done_pass2) goto do_next; + if (!modcall_pass2(g->children)) return false; + g->done_pass2 = true; + break; + } + + do_next: + rad_assert(c->debug_name != NULL); + } + + return true; +} + +void modcall_debug(modcallable *mc, int depth) +{ + modcallable *this; + modgroup *g; + vp_map_t *map; + char buffer[1024]; + + for (this = mc; this != NULL; this = this->next) { + switch (this->type) { + default: + break; + + case MOD_SINGLE: { + modsingle *single = mod_callabletosingle(this); + + DEBUG("%.*s%s", depth, modcall_spaces, + single->modinst->name); + } + break; + +#ifdef WITH_UNLANG + case MOD_UPDATE: + g = mod_callabletogroup(this); + DEBUG("%.*s%s {", depth, modcall_spaces, + unlang_keyword[this->type]); + + for (map = g->map; map != NULL; map = map->next) { + map_prints(buffer, sizeof(buffer), map); + DEBUG("%.*s%s", depth + 1, modcall_spaces, buffer); + } + + DEBUG("%.*s}", depth, modcall_spaces); + break; + + case MOD_ELSE: + g = mod_callabletogroup(this); + DEBUG("%.*s%s {", depth, modcall_spaces, + unlang_keyword[this->type]); + modcall_debug(g->children, depth + 1); + DEBUG("%.*s}", depth, modcall_spaces); + break; + + case MOD_IF: + case MOD_ELSIF: + g = mod_callabletogroup(this); + fr_cond_sprint(buffer, sizeof(buffer), g->cond); + DEBUG("%.*s%s (%s) {", depth, modcall_spaces, + unlang_keyword[this->type], buffer); + modcall_debug(g->children, depth + 1); + DEBUG("%.*s}", depth, modcall_spaces); + break; + + case MOD_SWITCH: + case MOD_CASE: + g = mod_callabletogroup(this); + tmpl_prints(buffer, sizeof(buffer), g->vpt, NULL); + DEBUG("%.*s%s %s {", depth, modcall_spaces, + unlang_keyword[this->type], buffer); + modcall_debug(g->children, depth + 1); + DEBUG("%.*s}", depth, modcall_spaces); + break; + + case MOD_POLICY: + case MOD_FOREACH: + g = mod_callabletogroup(this); + DEBUG("%.*s%s %s {", depth, modcall_spaces, + unlang_keyword[this->type], this->name); + modcall_debug(g->children, depth + 1); + DEBUG("%.*s}", depth, modcall_spaces); + break; + + case MOD_BREAK: + DEBUG("%.*sbreak", depth, modcall_spaces); + break; + +#endif + case MOD_GROUP: + g = mod_callabletogroup(this); + DEBUG("%.*s%s {", depth, modcall_spaces, + unlang_keyword[this->type]); + modcall_debug(g->children, depth + 1); + DEBUG("%.*s}", depth, modcall_spaces); + break; + + + case MOD_LOAD_BALANCE: + case MOD_REDUNDANT_LOAD_BALANCE: + g = mod_callabletogroup(this); + DEBUG("%.*s%s {", depth, modcall_spaces, + unlang_keyword[this->type]); + modcall_debug(g->children, depth + 1); + DEBUG("%.*s}", depth, modcall_spaces); + break; + } + } +} + +int modcall_pass2_condition(fr_cond_t *c) +{ + if (!fr_condition_walk(c, pass2_callback, NULL)) return -1; + + return 0; +} diff --git a/src/main/modules.c b/src/main/modules.c new file mode 100644 index 0000000..fd4334d --- /dev/null +++ b/src/main/modules.c @@ -0,0 +1,2302 @@ +/* + * modules.c Radius module support. + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2003,2006 The FreeRADIUS server project + * Copyright 2000 Alan DeKok <aland@ox.org> + * Copyright 2000 Alan Curry <pacman@world.std.com> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/modpriv.h> +#include <freeradius-devel/modcall.h> +#include <freeradius-devel/parser.h> +#include <freeradius-devel/rad_assert.h> + +/** Path to search for modules in + * + */ +char const *radlib_dir = NULL; + +typedef struct indexed_modcallable { + rlm_components_t comp; + int idx; + modcallable *modulelist; +} indexed_modcallable; + +typedef struct virtual_server_t { + char const *name; + time_t created; + int can_free; + CONF_SECTION *cs; + rbtree_t *components; + modcallable *mc[MOD_COUNT]; + CONF_SECTION *subcs[MOD_COUNT]; + struct virtual_server_t *next; +} virtual_server_t; + +/* + * Keep a hash of virtual servers, so that we can reload them. + */ +#define VIRTUAL_SERVER_HASH_SIZE (256) +static virtual_server_t *virtual_servers[VIRTUAL_SERVER_HASH_SIZE]; + +static rbtree_t *module_tree = NULL; + +static rbtree_t *instance_tree = NULL; + +struct fr_module_hup_t { + module_instance_t *mi; + time_t when; + void *insthandle; + fr_module_hup_t *next; +}; + +/* + * Ordered by component + */ +const section_type_value_t section_type_value[MOD_COUNT] = { + { "authenticate", "Auth-Type", PW_AUTH_TYPE }, + { "authorize", "Autz-Type", PW_AUTZ_TYPE }, + { "preacct", "Pre-Acct-Type", PW_PRE_ACCT_TYPE }, + { "accounting", "Acct-Type", PW_ACCT_TYPE }, + { "session", "Session-Type", PW_SESSION_TYPE }, + { "pre-proxy", "Pre-Proxy-Type", PW_PRE_PROXY_TYPE }, + { "post-proxy", "Post-Proxy-Type", PW_POST_PROXY_TYPE }, + { "post-auth", "Post-Auth-Type", PW_POST_AUTH_TYPE } +#ifdef WITH_COA + , + { "recv-coa", "Recv-CoA-Type", PW_RECV_COA_TYPE }, + { "send-coa", "Send-CoA-Type", PW_SEND_COA_TYPE } +#endif +}; + +#ifndef RTLD_NOW +#define RTLD_NOW (0) +#endif +#ifndef RTLD_LOCAL +#define RTLD_LOCAL (0) +#endif + +/** Check if the magic number in the module matches the one in the library + * + * This is used to detect potential ABI issues caused by running with modules which + * were built for a different version of the server. + * + * @param cs being parsed. + * @param module being loaded. + * @returns 0 on success, -1 if prefix mismatch, -2 if version mismatch, -3 if commit mismatch. + */ +static int check_module_magic(CONF_SECTION *cs, module_t const *module) +{ +#ifdef HAVE_DLADDR + Dl_info dl_info; + dladdr(module, &dl_info); +#endif + + if (MAGIC_PREFIX(module->magic) != MAGIC_PREFIX(RADIUSD_MAGIC_NUMBER)) { +#ifdef HAVE_DLADDR + cf_log_err_cs(cs, "Failed loading module rlm_%s from file %s", module->name, dl_info.dli_fname); +#endif + cf_log_err_cs(cs, "Application and rlm_%s magic number (prefix) mismatch." + " application: %x module: %x", module->name, + MAGIC_PREFIX(RADIUSD_MAGIC_NUMBER), + MAGIC_PREFIX(module->magic)); + return -1; + } + + if (MAGIC_VERSION(module->magic) != MAGIC_VERSION(RADIUSD_MAGIC_NUMBER)) { +#ifdef HAVE_DLADDR + cf_log_err_cs(cs, "Failed loading module rlm_%s from file %s", module->name, dl_info.dli_fname); +#endif + cf_log_err_cs(cs, "Application and rlm_%s magic number (version) mismatch." + " application: %lx module: %lx", module->name, + (unsigned long) MAGIC_VERSION(RADIUSD_MAGIC_NUMBER), + (unsigned long) MAGIC_VERSION(module->magic)); + return -2; + } + + if (MAGIC_COMMIT(module->magic) != MAGIC_COMMIT(RADIUSD_MAGIC_NUMBER)) { +#ifdef HAVE_DLADDR + cf_log_err_cs(cs, "Failed loading module rlm_%s from file %s", module->name, dl_info.dli_fname); +#endif + cf_log_err_cs(cs, "Application and rlm_%s magic number (commit) mismatch." + " application: %lx module: %lx", module->name, + (unsigned long) MAGIC_COMMIT(RADIUSD_MAGIC_NUMBER), + (unsigned long) MAGIC_COMMIT(module->magic)); + return -3; + } + + return 0; +} + +fr_dlhandle fr_dlopenext(char const *name) +{ + int flags = RTLD_NOW; + void *handle; + char buffer[2048]; + char *env; + char const *search_path; +#ifdef RTLD_GLOBAL + if (strcmp(name, "rlm_perl") == 0) { + flags |= RTLD_GLOBAL; + } else +#endif + flags |= RTLD_LOCAL; +#if defined(RTLD_DEEPBIND) && !defined(__SANITIZE_ADDRESS__) + flags |= RTLD_DEEPBIND; +#endif + +#ifndef NDEBUG + /* + * Bind all the symbols *NOW* so we don't hit errors later + */ + flags |= RTLD_NOW; +#endif + + /* + * Apple removed support for DYLD_LIBRARY_PATH in rootless mode. + */ + env = getenv("FR_LIBRARY_PATH"); + if (env) { + DEBUG3("Ignoring libdir as FR_LIBRARY_PATH set. Module search path will be: %s", env); + search_path = env; + } else { + search_path = radlib_dir; + } + + /* + * Prefer loading our libraries by absolute path. + */ + if (search_path) { + char *error; + char *ctx, *paths, *path; + char *p; + + fr_strerror(); + + ctx = paths = talloc_strdup(NULL, search_path); + while ((path = strsep(&paths, ":")) != NULL) { + /* + * Trim the trailing slash + */ + p = strrchr(path, '/'); + if (p && ((p[1] == '\0') || (p[1] == ':'))) *p = '\0'; + + path = talloc_asprintf(ctx, "%s/%s%s", path, name, LT_SHREXT); + + DEBUG4("Loading %s with path: %s", name, path); + + handle = dlopen(path, flags); + if (handle) { + talloc_free(ctx); + return handle; + } + error = dlerror(); + + fr_strerror_printf("%s%s\n", fr_strerror(), error); + DEBUG4("Loading %s failed: %s - %s", name, error, + (access(path, R_OK) < 0) ? fr_syserror(errno) : "No access errors"); + talloc_free(path); + } + talloc_free(ctx); + } + + DEBUG4("Loading library using linker search path(s)"); + if (DEBUG_ENABLED4) { +#ifdef __APPLE__ + env = getenv("LD_LIBRARY_PATH"); + if (env) { + DEBUG4("LD_LIBRARY_PATH : %s", env); + } + env = getenv("DYLD_LIBRARY_PATH"); + if (env) { + DEBUG4("DYLB_LIBRARY_PATH : %s", env); + } + env = getenv("DYLD_FALLBACK_LIBRARY_PATH"); + if (env) { + DEBUG4("DYLD_FALLBACK_LIBRARY_PATH : %s", env); + } + env = getcwd(buffer, sizeof(buffer)); + if (env) { + DEBUG4("Current directory : %s", env); + } +#else + env = getenv("LD_LIBRARY_PATH"); + if (env) { + DEBUG4("LD_LIBRARY_PATH : %s", env); + } + DEBUG4("Defaults : /lib:/usr/lib"); +#endif + } + + strlcpy(buffer, name, sizeof(buffer)); + /* + * FIXME: Make this configurable... + */ + strlcat(buffer, LT_SHREXT, sizeof(buffer)); + + handle = dlopen(buffer, flags); + if (!handle) { + char *error = dlerror(); + + DEBUG4("Failed with error: %s", error); + /* + * Don't overwrite the previous message + * It's likely to contain a better error. + */ + if (!radlib_dir) fr_strerror_printf("%s", dlerror()); + return NULL; + } + return handle; +} + +void *fr_dlsym(fr_dlhandle handle, char const *symbol) +{ + return dlsym(handle, symbol); +} + +int fr_dlclose(fr_dlhandle handle) +{ + if (!handle) return 0; + + return dlclose(handle); +} + +char const *fr_dlerror(void) +{ + return dlerror(); +} + +static int virtual_server_idx(char const *name) +{ + uint32_t hash; + + if (!name) return 0; + + hash = fr_hash_string(name); + + return hash & (VIRTUAL_SERVER_HASH_SIZE - 1); +} + +static virtual_server_t *virtual_server_find(char const *name) +{ + rlm_rcode_t rcode; + virtual_server_t *server; + + rcode = virtual_server_idx(name); + for (server = virtual_servers[rcode]; + server != NULL; + server = server->next) { + if (!name && !server->name) break; + + if ((name && server->name) && + (strcmp(name, server->name) == 0)) break; + } + + return server; +} + +static int _virtual_server_free(virtual_server_t *server) +{ + if (server->components) rbtree_free(server->components); + return 0; +} + +void virtual_servers_free(time_t when) +{ + int i; + virtual_server_t **last; + + for (i = 0; i < VIRTUAL_SERVER_HASH_SIZE; i++) { + virtual_server_t *server, *next; + + last = &virtual_servers[i]; + for (server = virtual_servers[i]; + server != NULL; + server = next) { + next = server->next; + + /* + * If we delete it, fix the links so that + * we don't orphan anything. Also, + * delete it if it's old, AND a newer one + * was defined. + * + * Otherwise, the last pointer gets set to + * the one we didn't delete. + */ + if ((when == 0) || + ((server->created < when) && server->can_free)) { + *last = server->next; + talloc_free(server); + } else { + last = &(server->next); + } + } + } +} + +static int indexed_modcallable_cmp(void const *one, void const *two) +{ + indexed_modcallable const *a = one; + indexed_modcallable const *b = two; + + if (a->comp < b->comp) return -1; + if (a->comp > b->comp) return +1; + + return a->idx - b->idx; +} + + +/* + * Compare two module entries + */ +static int module_instance_cmp(void const *one, void const *two) +{ + module_instance_t const *a = one; + module_instance_t const *b = two; + + return strcmp(a->name, b->name); +} + + +static void module_instance_free_old(UNUSED CONF_SECTION *cs, module_instance_t *node, time_t when) +{ + fr_module_hup_t *mh, **last; + + /* + * Walk the list, freeing up old instances. + */ + last = &(node->mh); + while (*last) { + mh = *last; + + /* + * Free only every 60 seconds. + */ + if ((when - mh->when) < 60) { + last = &(mh->next); + continue; + } + + talloc_free(mh->insthandle); + + *last = mh->next; + talloc_free(mh); + } +} + + +/* + * Free a module instance. + */ +static void module_instance_free(void *data) +{ + module_instance_t *module = talloc_get_type_abort(data, module_instance_t); + + module_instance_free_old(module->cs, module, time(NULL) + 100); + +#ifdef HAVE_PTHREAD_H + if (module->mutex) { + /* + * FIXME + * The mutex MIGHT be locked... + * we'll check for that later, I guess. + */ + pthread_mutex_destroy(module->mutex); + talloc_free(module->mutex); + } +#endif + + xlat_unregister(module->name, NULL, module->insthandle); + + /* + * Remove all xlat's registered to module instance. + */ + if (module->insthandle) { + /* + * Remove any registered paircompares. + */ + paircompare_unregister_instance(module->insthandle); + + xlat_unregister_module(module->insthandle); + } + talloc_free(module); +} + + +/* + * Compare two module entries + */ +static int module_entry_cmp(void const *one, void const *two) +{ + module_entry_t const *a = one; + module_entry_t const *b = two; + + return strcmp(a->name, b->name); +} + +/* + * Free a module entry. + */ +static int _module_entry_free(module_entry_t *this) +{ +#ifndef NDEBUG + /* + * Don't dlclose() modules if we're doing memory + * debugging. This removes the symbols needed by + * valgrind. + */ + if (!main_config.debug_memory) +#endif + dlclose(this->handle); /* ignore any errors */ + return 0; +} + + +/* + * Remove the module lists. + */ +int modules_free(void) +{ + rbtree_free(instance_tree); + rbtree_free(module_tree); + + return 0; +} + + +/* + * dlopen() a module. + */ +static module_entry_t *module_dlopen(CONF_SECTION *cs, char const *module_name) +{ + module_entry_t myentry; + module_entry_t *node; + void *handle = NULL; + module_t const *module; + + strlcpy(myentry.name, module_name, sizeof(myentry.name)); + node = rbtree_finddata(module_tree, &myentry); + if (node) return node; + + /* + * Link to the module's rlm_FOO{} structure, the same as + * the module name. + */ + +#if !defined(WITH_LIBLTDL) && defined(HAVE_DLFCN_H) && defined(RTLD_SELF) + module = dlsym(RTLD_SELF, module_name); + if (module) goto open_self; +#endif + + /* + * Keep the handle around so we can dlclose() it. + */ + handle = fr_dlopenext(module_name); + if (!handle) { + cf_log_err_cs(cs, "Failed to link to module '%s': %s", module_name, fr_strerror()); + return NULL; + } + + DEBUG3("Loaded %s, checking if it's valid", module_name); + + module = dlsym(handle, module_name); + if (!module) { + cf_log_err_cs(cs, "Failed linking to %s structure: %s", module_name, dlerror()); + dlclose(handle); + return NULL; + } + +#if !defined(WITH_LIBLTDL) && defined (HAVE_DLFCN_H) && defined(RTLD_SELF) + open_self: +#endif + /* + * Before doing anything else, check if it's sane. + */ + if (check_module_magic(cs, module) < 0) { + dlclose(handle); + return NULL; + } + + /* make room for the module type */ + node = talloc_zero(cs, module_entry_t); + talloc_set_destructor(node, _module_entry_free); + strlcpy(node->name, module_name, sizeof(node->name)); + node->module = module; + node->handle = handle; + + cf_log_module(cs, "Loaded module %s", module_name); + + /* + * Add the module as "rlm_foo-version" to the configuration + * section. + */ + if (!rbtree_insert(module_tree, node)) { + ERROR("Failed to cache module %s", module_name); + dlclose(handle); + talloc_free(node); + return NULL; + } + + return node; +} + +/** Parse module's configuration section and setup destructors + * + */ +static int module_conf_parse(module_instance_t *node, void **handle) +{ + *handle = NULL; + + /* + * If there is supposed to be instance data, allocate it now. + * Also parse the configuration data, if required. + */ + if (node->entry->module->inst_size) { + *handle = talloc_zero_array(node, uint8_t, node->entry->module->inst_size); + rad_assert(*handle); + + talloc_set_name(*handle, "rlm_%s_t", + node->entry->module->name ? node->entry->module->name : "config"); + + if (node->entry->module->config && + (cf_section_parse(node->cs, *handle, node->entry->module->config) < 0)) { + cf_log_err_cs(node->cs,"Invalid configuration for module \"%s\"", node->name); + talloc_free(*handle); + + return -1; + } + + /* + * Set the destructor. + */ + if (node->entry->module->detach) { + talloc_set_destructor(*handle, node->entry->module->detach); + } + } + + return 0; +} + +/** Bootstrap a module. + * + * Load the module shared library, allocate instance memory for it, + * parse the module configuration, and call the modules "bootstrap" method. + */ +static module_instance_t *module_bootstrap(CONF_SECTION *cs) +{ + char const *name1, *name2, *askedname; + module_instance_t *node, myNode; + char module_name[256]; + + /* + * Figure out which module we want to load. + */ + name1 = cf_section_name1(cs); + askedname = name2 = cf_section_name2(cs); + if (!askedname) { + askedname = name1; + name2 = ""; + } + + strlcpy(myNode.name, askedname, sizeof(myNode.name)); + + /* + * See if the module already exists. + */ + node = rbtree_finddata(instance_tree, &myNode); + if (node) { + ERROR("Duplicate module \"%s %s { ... }\", in file %s:%d and file %s:%d", + name1, name2, + cf_section_filename(cs), + cf_section_lineno(cs), + cf_section_filename(node->cs), + cf_section_lineno(node->cs)); + return NULL; + } + + /* + * Hang the node struct off of the configuration + * section. If the CS is free'd the instance will be + * free'd, too. + */ + node = talloc_zero(instance_tree, module_instance_t); + node->cs = cs; + strlcpy(node->name, askedname, sizeof(node->name)); + + /* + * Names in the "modules" section aren't prefixed + * with "rlm_", so we add it here. + */ + snprintf(module_name, sizeof(module_name), "rlm_%s", name1); + + /* + * Load the module shared library. + */ + node->entry = module_dlopen(cs, module_name); + if (!node->entry) { + talloc_free(node); + return NULL; + } + + cf_log_module(cs, "Loading module \"%s\" from file %s", node->name, + cf_section_filename(cs)); + + /* + * Parse the modules configuration. + */ + if (module_conf_parse(node, &node->insthandle) < 0) { + talloc_free(node); + return NULL; + } + + /* + * Bootstrap the module. + */ + if (node->entry->module->bootstrap && + ((node->entry->module->bootstrap)(cs, node->insthandle) < 0)) { + cf_log_err_cs(cs, "Instantiation failed for module \"%s\"", node->name); + talloc_free(node); + return NULL; + } + + /* + * Remember the module for later. + */ + rbtree_insert(instance_tree, node); + + return node; +} + + +/** Find an existing module instance. + * + */ +module_instance_t *module_find(CONF_SECTION *modules, char const *askedname) +{ + char const *instname; + module_instance_t myNode; + + if (!modules) return NULL; + + /* + * Look for the real name. Ignore the first character, + * which tells the server "it's OK for this module to not + * exist." + */ + instname = askedname; + if (instname[0] == '-') instname++; + + strlcpy(myNode.name, instname, sizeof(myNode.name)); + + return rbtree_finddata(instance_tree, &myNode); +} + + +/** Load a module, and instantiate it. + * + */ +module_instance_t *module_instantiate(CONF_SECTION *modules, char const *askedname) +{ + module_instance_t *node; + + /* + * Find the module. If it's not there, do nothing. + */ + node = module_find(modules, askedname); + if (!node) { + ERROR("Cannot find module \"%s\"", askedname); + return NULL; + } + + /* + * The module is already instantiated. Return it. + */ + if (node->instantiated) return node; + + /* + * Now that ALL modules are instantiated, and ALL xlats + * are defined, go compile the config items marked as XLAT. + */ + if (node->entry->module->config && + (cf_section_parse_pass2(node->cs, node->insthandle, + node->entry->module->config) < 0)) { + return NULL; + } + + /* + * Call the instantiate method, if any. + */ + if (node->entry->module->instantiate) { + cf_log_module(node->cs, "Instantiating module \"%s\" from file %s", node->name, + cf_section_filename(node->cs)); + + /* + * Call the module's instantiation routine. + */ + if ((node->entry->module->instantiate)(node->cs, node->insthandle) < 0) { + cf_log_err_cs(node->cs, "Instantiation failed for module \"%s\"", node->name); + + return NULL; + } + } + +#ifdef HAVE_PTHREAD_H + /* + * If we're threaded, check if the module is thread-safe. + * + * If it isn't, we create a mutex. + */ + if ((node->entry->module->type & RLM_TYPE_THREAD_UNSAFE) != 0) { + node->mutex = talloc_zero(node, pthread_mutex_t); + + /* + * Initialize the mutex. + */ + pthread_mutex_init(node->mutex, NULL); + } +#endif + + node->instantiated = true; + node->last_hup = time(NULL); /* don't let us load it, then immediately hup it */ + + return node; +} + + +module_instance_t *module_instantiate_method(CONF_SECTION *modules, char const *name, rlm_components_t *method) +{ + char *p; + rlm_components_t i; + module_instance_t *mi; + + /* + * If the module exists, ensure it's instantiated. + * + * Doing it this way avoids complaints from + * module_instantiate() + */ + mi = module_find(modules, name); + if (mi) return module_instantiate(modules, name); + + /* + * Find out which method is being used. + */ + p = strrchr(name, '.'); + if (!p) return NULL; + + p++; + + /* + * Find the component. + */ + for (i = MOD_AUTHENTICATE; i < MOD_COUNT; i++) { + if (strcmp(p, section_type_value[i].section) == 0) { + char buffer[256]; + + strlcpy(buffer, name, sizeof(buffer)); + buffer[p - name - 1] = '\0'; + + mi = module_find(modules, buffer); + if (mi) { + if (method) *method = i; + return module_instantiate(modules, buffer); + } + } + } + + /* + * Not found. + */ + return NULL; +} + + +/** Resolve polymorphic item's from a module's CONF_SECTION to a subsection in another module + * + * This allows certain module sections to reference module sections in other instances + * of the same module and share CONF_DATA associated with them. + * + * @verbatim +example { + data { + ... + } +} + +example inst { + data = example +} + * @endverbatim + * + * @param out where to write the pointer to a module's config section. May be NULL on success, indicating the config + * item was not found within the module CONF_SECTION, or the chain of module references was followed and the + * module at the end of the chain did not a subsection. + * @param module CONF_SECTION. + * @param name of the polymorphic sub-section. + * @return 0 on success with referenced section, 1 on success with local section, or -1 on failure. + */ +int find_module_sibling_section(CONF_SECTION **out, CONF_SECTION *module, char const *name) +{ + static bool loop = true; /* not used, we just need a valid pointer to quiet static analysis */ + + CONF_PAIR *cp; + CONF_SECTION *cs; + + module_instance_t *inst; + char const *inst_name; + +#define FIND_SIBLING_CF_KEY "find_sibling" + + *out = NULL; + + /* + * Is a real section (not referencing sibling module). + */ + cs = cf_section_sub_find(module, name); + if (cs) { + *out = cs; + + return 0; + } + + /* + * Item omitted completely from module config. + */ + cp = cf_pair_find(module, name); + if (!cp) return 0; + + if (cf_data_find(module, FIND_SIBLING_CF_KEY)) { + cf_log_err_cp(cp, "Module reference loop found"); + + return -1; + } + cf_data_add(module, FIND_SIBLING_CF_KEY, &loop, NULL); + + /* + * Item found, resolve it to a module instance. + * This triggers module loading, so we don't have + * instantiation order issues. + */ + inst_name = cf_pair_value(cp); + inst = module_instantiate(cf_item_parent(cf_section_to_item(module)), inst_name); + + /* + * Remove the config data we added for loop + * detection. + */ + cf_data_remove(module, FIND_SIBLING_CF_KEY); + if (!inst) { + cf_log_err_cp(cp, "Unknown module instance \"%s\"", inst_name); + + return -1; + } + + /* + * Check the module instances are of the same type. + */ + if (strcmp(cf_section_name1(inst->cs), cf_section_name1(module)) != 0) { + cf_log_err_cp(cp, "Referenced module is a rlm_%s instance, must be a rlm_%s instance", + cf_section_name1(inst->cs), cf_section_name1(module)); + + return -1; + } + + *out = cf_section_sub_find(inst->cs, name); + + return 1; +} + +static indexed_modcallable *lookup_by_index(rbtree_t *components, + rlm_components_t comp, int idx) +{ + indexed_modcallable myc; + + myc.comp = comp; + myc.idx = idx; + + return rbtree_finddata(components, &myc); +} + +/* + * Create a new sublist. + */ +static indexed_modcallable *new_sublist(CONF_SECTION *cs, + rbtree_t *components, rlm_components_t comp, int idx) +{ + indexed_modcallable *c; + + c = lookup_by_index(components, comp, idx); + + /* It is an error to try to create a sublist that already + * exists. It would almost certainly be caused by accidental + * duplication in the config file. + * + * index 0 is the exception, because it is used when we want + * to collect _all_ listed modules under a single index by + * default, which is currently the case in all components + * except authenticate. */ + if (c) { + if (idx == 0) { + return c; + } + return NULL; + } + + c = talloc_zero(cs, indexed_modcallable); + c->modulelist = NULL; + c->comp = comp; + c->idx = idx; + + if (!rbtree_insert(components, c)) { + talloc_free(c); + return NULL; + } + + return c; +} + +rlm_rcode_t indexed_modcall(rlm_components_t comp, int idx, REQUEST *request) +{ + rlm_rcode_t rcode; + modcallable *list = NULL; + virtual_server_t *server; + + /* + * Hack to find the correct virtual server. + */ + server = virtual_server_find(request->server); + if (!server) { + RDEBUG("No such virtual server \"%s\"", request->server); + return RLM_MODULE_FAIL; + } + + if (idx == 0) { + list = server->mc[comp]; + if (!list) { + if (server->name) { + RDEBUG3("Empty %s section in virtual server \"%s\". Using default return values.", + section_type_value[comp].section, server->name); + } else { + RDEBUG3("Empty %s section. Using default return values.", section_type_value[comp].section); + } + } + } else { + indexed_modcallable *this; + + this = lookup_by_index(server->components, comp, idx); + if (this) { + list = this->modulelist; + } else { + RDEBUG2("%s sub-section not found. Ignoring.", section_type_value[comp].typename); + } + } + + if (server->subcs[comp]) { + if (idx == 0) { + RDEBUG("# Executing section %s from file %s", + section_type_value[comp].section, + cf_section_filename(server->subcs[comp])); + } else { + RDEBUG("# Executing group from file %s", + cf_section_filename(server->subcs[comp])); + } + } + request->component = section_type_value[comp].section; + rcode = modcall(comp, list, request); + request->component = "<core>"; + + return rcode; +} + +/* + * Load a sub-module list, as found inside an Auth-Type foo {} + * block + */ +static int load_subcomponent_section(CONF_SECTION *cs, + rbtree_t *components, + DICT_ATTR const *da, rlm_components_t comp) +{ + indexed_modcallable *subcomp; + modcallable *ml; + DICT_VALUE *dval; + char const *name2 = cf_section_name2(cs); + + /* + * Sanity check. + */ + if (!name2) { + return 1; + } + + DEBUG("Compiling %s %s for attr %s", cf_section_name1(cs), name2, da->name); + + /* + * Compile the group. + */ + ml = compile_modgroup(NULL, comp, cs); + if (!ml) { + return 0; + } + + /* + * We must assign a numeric index to this subcomponent. + * It is generated and placed in the dictionary + * automatically. If it isn't found, it's a serious + * error. + */ + dval = dict_valbyname(da->attr, da->vendor, name2); + if (!dval) { + talloc_free(ml); + cf_log_err_cs(cs, + "The %s attribute has no VALUE defined for %s", + section_type_value[comp].typename, name2); + return 0; + } + + subcomp = new_sublist(cs, components, comp, dval->value); + if (!subcomp) { + talloc_free(ml); + return 1; + } + + /* + * Link it into the talloc hierarchy. + */ + subcomp->modulelist = talloc_steal(subcomp, ml); + return 1; /* OK */ +} + +/* + * Don't complain too often. + */ +#define MAX_IGNORED (32) +static int last_ignored = -1; +static char const *ignored[MAX_IGNORED]; + +static int load_component_section(CONF_SECTION *cs, + rbtree_t *components, rlm_components_t comp) +{ + modcallable *this; + CONF_ITEM *modref; + int idx; + indexed_modcallable *subcomp; + char const *modname; + DICT_ATTR const *da; + + /* + * Find the attribute used to store VALUEs for this section. + */ + da = dict_attrbyvalue(section_type_value[comp].attr, 0); + if (!da) { + cf_log_err_cs(cs, + "No such attribute %s", + section_type_value[comp].typename); + return -1; + } + + /* + * Loop over the entries in the named section, loading + * the sections this time. + */ + for (modref = cf_item_find_next(cs, NULL); + modref != NULL; + modref = cf_item_find_next(cs, modref)) { + char const *name1; + CONF_PAIR *cp = NULL; + CONF_SECTION *scs = NULL; + + if (cf_item_is_section(modref)) { + scs = cf_item_to_section(modref); + + name1 = cf_section_name1(scs); + + if (strcmp(name1, + section_type_value[comp].typename) == 0) { + if (!load_subcomponent_section(scs, + components, + da, + comp)) { + + return -1; /* FIXME: memleak? */ + } + continue; + } + + cp = NULL; + + } else if (cf_item_is_pair(modref)) { + cp = cf_item_to_pair(modref); + + } else { + continue; /* ignore it */ + } + + /* + * Look for Auth-Type foo {}, which are special + * cases of named sections, and allowable ONLY + * at the top-level. + * + * i.e. They're not allowed in a "group" or "redundant" + * subsection. + */ + if (comp == MOD_AUTHENTICATE) { + DICT_VALUE *dval; + char const *modrefname = NULL; + + if (cp) { + modrefname = cf_pair_attr(cp); + } else { + modrefname = cf_section_name2(scs); + if (!modrefname) { + cf_log_err_cs(cs, + "Errors parsing %s sub-section.\n", + cf_section_name1(scs)); + return -1; + } + } + if (*modrefname == '-') modrefname++; + + dval = dict_valbyname(PW_AUTH_TYPE, 0, modrefname); + if (!dval) { + /* + * It's a section, but nothing we + * recognize. Die! + */ + cf_log_err_cs(cs, + "Unknown Auth-Type \"%s\" in %s sub-section.", + modrefname, section_type_value[comp].section); + return -1; + } + idx = dval->value; + } else { + /* See the comment in new_sublist() for explanation + * of the special index 0 */ + idx = 0; + } + + subcomp = new_sublist(cs, components, comp, idx); + if (!subcomp) continue; + + /* + * Try to compile one entry. + */ + this = compile_modsingle(subcomp, &subcomp->modulelist, comp, modref, &modname); + + /* + * It's OK for the module to not exist. + */ + if (!this && modname && (modname[0] == '-')) { + int i; + + if (last_ignored < 0) { + save_complain: + last_ignored++; + ignored[last_ignored] = modname; + + complain: + WARN("Ignoring \"%s\" (see raddb/mods-available/README.rst)", modname + 1); + continue; + } + + if (last_ignored >= MAX_IGNORED) goto complain; + + for (i = 0; i <= last_ignored; i++) { + if (strcmp(ignored[i], modname) == 0) { + break; + } + } + + if (i > last_ignored) goto save_complain; + continue; + } + + if (!this) { + cf_log_err_cs(cs, + "Errors parsing %s section.\n", + cf_section_name1(cs)); + return -1; + } + + if (rad_debug_lvl > 2) modcall_debug(this, 2); + + add_to_modcallable(subcomp->modulelist, this); + } + + + return 0; +} + +static int load_byserver(CONF_SECTION *cs) +{ + rlm_components_t comp; + bool found; + char const *name = cf_section_name2(cs); + rbtree_t *components; + virtual_server_t *server = NULL; + indexed_modcallable *c; + bool is_bare; + + if (name) { + cf_log_info(cs, "server %s { # from file %s", + name, cf_section_filename(cs)); + } else { + cf_log_info(cs, "server { # from file %s", + cf_section_filename(cs)); + } + + is_bare = (cf_item_parent(cf_section_to_item(cs)) == NULL); + + server = talloc_zero(cs, virtual_server_t); + server->name = name; + server->created = time(NULL); + server->cs = cs; + server->components = components = rbtree_create(server, indexed_modcallable_cmp, NULL, 0); + if (!components) { + ERROR("Failed to initialize components"); + + error: + if (rad_debug_lvl == 0) { + ERROR("Failed to load virtual server %s", + (name != NULL) ? name : "<default>"); + } + return -1; + } + talloc_set_destructor(server, _virtual_server_free); + + /* + * Loop over all of the known components, finding their + * configuration section, and loading it. + */ + found = false; + for (comp = 0; comp < MOD_COUNT; ++comp) { + CONF_SECTION *subcs; + + subcs = cf_section_sub_find(cs, + section_type_value[comp].section); + if (!subcs) continue; + + if (is_bare) { + cf_log_err_cs(subcs, "The %s section should be inside of a 'server { ... }' block!", + section_type_value[comp].section); + } + + if (cf_item_find_next(subcs, NULL) == NULL) continue; + + /* + * Skip pre/post-proxy sections if we're not + * proxying. + */ + if ( +#ifdef WITH_PROXY + !main_config.proxy_requests && +#endif + ((comp == MOD_PRE_PROXY) || + (comp == MOD_POST_PROXY))) { + continue; + } + +#ifndef WITH_ACCOUNTING + if (comp == MOD_ACCOUNTING) continue; +#endif + +#ifndef WITH_SESSION_MGMT + if (comp == MOD_SESSION) continue; +#endif + + if (rad_debug_lvl <= 3) { + cf_log_module(cs, "Loading %s {...}", + section_type_value[comp].section); + } else { + DEBUG(" %s {", section_type_value[comp].section); + } + + if (load_component_section(subcs, components, comp) < 0) { + goto error; + } + + if (rad_debug_lvl > 3) { + DEBUG(" } # %s", section_type_value[comp].section); + } + + /* + * Cache a default, if it exists. Some people + * put empty sections for some reason... + */ + c = lookup_by_index(components, comp, 0); + if (c) server->mc[comp] = c->modulelist; + + server->subcs[comp] = subcs; + + found = true; + } /* loop over components */ + + /* + * We haven't loaded any of the normal sections. Maybe we're + * supposed to load the vmps section. + * + * This is a bit of a hack... + */ + if (!found) do { +#if defined(WITH_VMPS) || defined(WITH_DHCP) || defined(WITH_TLS) + CONF_SECTION *subcs; +#endif +#if defined(WITH_DHCP) || defined(WITH_TLS) + DICT_ATTR const *da; +#endif + +#ifdef WITH_VMPS + subcs = cf_section_sub_find(cs, "vmps"); + if (subcs) { + cf_log_module(cs, "Loading vmps {...}"); + if (load_component_section(subcs, components, + MOD_POST_AUTH) < 0) { + goto error; + } + c = lookup_by_index(components, + MOD_POST_AUTH, 0); + if (c) server->mc[MOD_POST_AUTH] = c->modulelist; + break; + } +#endif + +#ifdef WITH_TLS + /* + * It's OK to not have TLS cache sections. + */ + da = dict_attrbyname("TLS-Cache-Method"); + subcs = cf_section_sub_find_name2(cs, "cache", "load"); + if (subcs && !load_subcomponent_section(subcs, + components, + da, + MOD_POST_AUTH)) { + goto error; /* FIXME: memleak? */ + } + + subcs = cf_section_sub_find_name2(cs, "cache", "save"); + if (subcs && !load_subcomponent_section(subcs, + components, + da, + MOD_POST_AUTH)) { + goto error; /* FIXME: memleak? */ + } + + subcs = cf_section_sub_find_name2(cs, "cache", "clear"); + if (subcs && !load_subcomponent_section(subcs, + components, + da, + MOD_POST_AUTH)) { + goto error; /* FIXME: memleak? */ + } + + subcs = cf_section_sub_find_name2(cs, "cache", "refresh"); + if (subcs && !load_subcomponent_section(subcs, + components, + da, + MOD_POST_AUTH)) { + goto error; /* FIXME: memleak? */ + } +#endif + +#ifdef WITH_DHCP + /* + * It's OK to not have DHCP. + */ + subcs = cf_subsection_find_next(cs, NULL, "dhcp"); + if (!subcs) break; + + da = dict_attrbyname("DHCP-Message-Type"); + + /* + * Handle each DHCP Message type separately. + */ + while (subcs) { + char const *name2 = cf_section_name2(subcs); + + if (name2) { + cf_log_module(cs, "Loading dhcp %s {...}", name2); + } else { + cf_log_module(cs, "Loading dhcp {...}"); + } + if (!load_subcomponent_section(subcs, + components, + da, + MOD_POST_AUTH)) { + goto error; /* FIXME: memleak? */ + } + c = lookup_by_index(components, + MOD_POST_AUTH, 0); + if (c) server->mc[MOD_POST_AUTH] = c->modulelist; + + subcs = cf_subsection_find_next(cs, subcs, "dhcp"); + } +#endif + + + } while (0); + + if (name) { + cf_log_info(cs, "} # server %s", name); + } else { + cf_log_info(cs, "} # server"); + } + + if (rad_debug_lvl == 0) { + INFO("Loaded virtual server %s", + (name != NULL) ? name : "<default>"); + } + + /* + * Now that it is OK, insert it into the list. + * + * This is thread-safe... + */ + comp = virtual_server_idx(name); + server->next = virtual_servers[comp]; + virtual_servers[comp] = server; + + /* + * Mark OLDER ones of the same name as being unused. + */ + server = server->next; + while (server) { + if ((!name && !server->name) || + (name && server->name && + (strcmp(server->name, name) == 0))) { + server->can_free = true; + break; + } + server = server->next; + } + + return 0; +} + + +static int pass2_cb(UNUSED void *ctx, void *data) +{ + indexed_modcallable *this = data; + + if (!modcall_pass2(this->modulelist)) return -1; + + return 0; +} + + +/* + * Load all of the virtual servers. + */ +int virtual_servers_load(CONF_SECTION *config) +{ + CONF_SECTION *cs; + virtual_server_t *server; + static bool first_time = true; + + DEBUG2("%s: #### Loading Virtual Servers ####", main_config.name); + + /* + * If we have "server { ...}", then there SHOULD NOT be + * bare "authorize", etc. sections. if there is no such + * server, then try to load the old-style sections first. + * + * In either case, load the "default" virtual server first. + * this matches better with users expectations. + */ + cs = cf_section_find_name2(cf_subsection_find_next(config, NULL, + "server"), + "server", NULL); + if (cs) { + if (load_byserver(cs) < 0) { + return -1; + } + } else { + if (load_byserver(config) < 0) { + return -1; + } + } + + /* + * Load all of the virtual servers. + */ + for (cs = cf_subsection_find_next(config, NULL, "server"); + cs != NULL; + cs = cf_subsection_find_next(config, cs, "server")) { + char const *name2; + + name2 = cf_section_name2(cs); + if (!name2) continue; /* handled above */ + + server = virtual_server_find(name2); + if (server && + (cf_top_section(server->cs) == config)) { + ERROR("Duplicate virtual server \"%s\" in file %s:%d and file %s:%d", + server->name, + cf_section_filename(server->cs), + cf_section_lineno(server->cs), + cf_section_filename(cs), + cf_section_lineno(cs)); + return -1; + } + + if (load_byserver(cs) < 0) { + /* + * Once we successfully started once, + * continue loading the OTHER servers, + * even if one fails. + */ + if (!first_time) continue; + return -1; + } + } + + /* + * Try to compile the "authorize", etc. sections which + * aren't in a virtual server. + */ + server = virtual_server_find(NULL); + if (server) { + int i; + + for (i = MOD_AUTHENTICATE; i < MOD_COUNT; i++) { + if (!modcall_pass2(server->mc[i])) return -1; + } + + if (server->components && + (rbtree_walk(server->components, RBTREE_IN_ORDER, + pass2_cb, NULL) != 0)) { + return -1; + } + } + + /* + * Now that we've loaded everything, run pass 2 over the + * conditions and xlats. + */ + for (cs = cf_subsection_find_next(config, NULL, "server"); + cs != NULL; + cs = cf_subsection_find_next(config, cs, "server")) { + int i; + char const *name2; + + name2 = cf_section_name2(cs); + + server = virtual_server_find(name2); + if (!server) continue; + + for (i = MOD_AUTHENTICATE; i < MOD_COUNT; i++) { + if (!modcall_pass2(server->mc[i])) return -1; + } + + if (server->components && + (rbtree_walk(server->components, RBTREE_IN_ORDER, + pass2_cb, NULL) != 0)) { + return -1; + } + } + + /* + * If we succeed the first time around, remember that. + */ + first_time = false; + + return 0; +} + +int module_hup_module(CONF_SECTION *cs, module_instance_t *node, time_t when) +{ + void *insthandle; + fr_module_hup_t *mh; + + if (!node || + node->entry->module->bootstrap || + !node->entry->module->instantiate || + ((node->entry->module->type & RLM_TYPE_HUP_SAFE) == 0)) { + return 1; + } + + /* + * Silently ignore multiple HUPs within a short time period. + */ + if ((node->last_hup + 2) >= when) return 1; + node->last_hup = when; + + /* + * Clear any old instances before attempting to reload + */ + module_instance_free_old(cs, node, when); + + cf_log_module(cs, "Trying to reload module \"%s\"", node->name); + + /* + * Parse the module configuration, and setup destructors so the + * module's detach method is called when it's instance data is + * about to be freed. + */ + if (module_conf_parse(node, &insthandle) < 0) { + cf_log_err_cs(cs, "HUP failed for module \"%s\" (parsing config failed). " + "Using old configuration", node->name); + + return 0; + } + + if ((node->entry->module->instantiate)(cs, insthandle) < 0) { + cf_log_err_cs(cs, "HUP failed for module \"%s\". Using old configuration.", node->name); + talloc_free(insthandle); + + return 0; + } + + INFO(" Module: Reloaded module \"%s\"", node->name); + + /* + * Save the old instance handle for later deletion. + */ + mh = talloc_zero(cs, fr_module_hup_t); + mh->mi = node; + mh->when = when; + mh->insthandle = node->insthandle; + mh->next = node->mh; + node->mh = mh; + + /* + * Replace the instance handle while the module is running. + */ + node->insthandle = insthandle; + + /* + * FIXME: Set a timeout to come back in 60s, so that + * we can pro-actively clean up the old instances. + */ + + return 1; +} + + +int modules_hup(CONF_SECTION *modules) +{ + time_t when; + CONF_ITEM *ci; + CONF_SECTION *cs; + module_instance_t *node; + + if (!modules) return 0; + + when = time(NULL); + + /* + * Loop over the modules + */ + for (ci=cf_item_find_next(modules, NULL); + ci != NULL; + ci=cf_item_find_next(modules, ci)) { + char const *instname; + module_instance_t myNode; + + /* + * If it's not a section, ignore it. + */ + if (!cf_item_is_section(ci)) continue; + + cs = cf_item_to_section(ci); + instname = cf_section_name2(cs); + if (!instname) instname = cf_section_name1(cs); + + strlcpy(myNode.name, instname, sizeof(myNode.name)); + node = rbtree_finddata(instance_tree, &myNode); + + module_hup_module(cs, node, when); + } + + return 1; +} + + +static int define_type(CONF_SECTION *cs, DICT_ATTR const *da, char const *name) +{ + uint32_t value; + DICT_VALUE *dval; + + /* + * Allow for conditionally loaded types + */ + if (*name == '-') name++; + + /* + * If the value already exists, don't + * create it again. + */ + dval = dict_valbyname(da->attr, da->vendor, name); + if (dval) { + if (dval->value == 0) { + ERROR("The dictionaries must not define VALUE %s %s 0", + da->name, name); + return 0; + } + return 1; + } + + /* + * Create a new unique value with a + * meaningless number. You can't look at + * it from outside of this code, so it + * doesn't matter. The only requirement + * is that it's unique. + */ + do { + value = (fr_rand() & 0x00ffffff) + 1; + } while (dict_valbyattr(da->attr, da->vendor, value)); + + cf_log_module(cs, "Creating %s = %s", da->name, name); + if (dict_addvalue(name, da->name, value) < 0) { + ERROR("%s", fr_strerror()); + return 0; + } + + return 1; +} + +/* + * Define Auth-Type, etc. in a server. + */ +static bool server_define_types(CONF_SECTION *cs) +{ + rlm_components_t comp; + + /* + * Loop over all of the components + */ + for (comp = 0; comp < MOD_COUNT; ++comp) { + CONF_SECTION *subcs, *type_cs; + DICT_ATTR const *da; + + subcs = cf_section_sub_find(cs, + section_type_value[comp].section); + if (!subcs) continue; + + if (cf_item_find_next(subcs, NULL) == NULL) continue; + + /* + * Find the attribute used to store VALUEs for this section. + */ + da = dict_attrbyvalue(section_type_value[comp].attr, 0); + if (!da) { + cf_log_err_cs(subcs, + "No such attribute %s", + section_type_value[comp].typename); + return false; + } + + /* + * Define dynamic types, so that others can reference + * them. + * + * First, bare modules for 'authenticate'. + * Second, Auth-Type, etc. + */ + if (section_type_value[comp].attr == PW_AUTH_TYPE) { + CONF_ITEM *modref; + + for (modref = cf_item_find_next(subcs, NULL); + modref != NULL; + modref = cf_item_find_next(subcs, modref)) { + CONF_PAIR *cp; + + if (!cf_item_is_pair(modref)) continue; + + cp = cf_item_to_pair(modref); + if (!define_type(cs, da, cf_pair_attr(cp))) { + return false; + } + + /* + * Check for duplicates + */ + if (rad_debug_lvl) { + CONF_PAIR *cp2; + CONF_SECTION *cs2; + + cp2 = cf_pair_find(subcs, cf_pair_attr(cp)); + rad_assert(cp2 != NULL); + if (cp2 != cp) { + WARN("%s[%d]: Duplicate module '%s'", + cf_pair_filename(cp2), + cf_pair_lineno(cp2), + cf_pair_attr(cp)); + } + + cs2 = cf_section_sub_find_name2(subcs, section_type_value[comp].typename, cf_pair_attr(cp)); + if (cs2) { + WARN("%s[%d]: Duplicate Auth-Type '%s'", + cf_section_filename(cs2), + cf_section_lineno(cs2), + cf_pair_attr(cp)); + } + } + + } + } + + /* + * And loop over the type names + */ + for (type_cs = cf_subsection_find_next(subcs, NULL, section_type_value[comp].typename); + type_cs != NULL; + type_cs = cf_subsection_find_next(subcs, type_cs, section_type_value[comp].typename)) { + if (!define_type(cs, da, cf_section_name2(type_cs))) { + return false; + } + + if (rad_debug_lvl) { + CONF_SECTION *cs2; + + cs2 = cf_section_sub_find_name2(subcs, section_type_value[comp].typename, cf_section_name2(type_cs)); + rad_assert(cs2 != NULL); + if (cs2 != type_cs) { + WARN("%s[%d]: Duplicate Auth-Type '%s'", + cf_section_filename(cs2), + cf_section_lineno(cs2), + cf_section_name2(cs2)); + } + } + } + } /* loop over components */ + + return true; +} + +extern char const *unlang_keyword[]; + +static bool is_reserved_word(const char *name) +{ + int i; + + if (!name || !*name) return false; + + for (i = 1; unlang_keyword[i] != NULL; i++) { + if (strcmp(name, unlang_keyword[i]) == 0) return true; + } + + return false; +} + + +/* + * Parse the module config sections, and load + * and call each module's init() function. + */ +int modules_init(CONF_SECTION *config) +{ + CONF_ITEM *ci, *next; + CONF_SECTION *cs, *modules; + + /* + * Set up the internal module struct. + */ + module_tree = rbtree_create(NULL, module_entry_cmp, NULL, 0); + if (!module_tree) { + ERROR("Failed to initialize modules\n"); + return -1; + } + + instance_tree = rbtree_create(NULL, module_instance_cmp, + module_instance_free, 0); + if (!instance_tree) { + ERROR("Failed to initialize modules\n"); + return -1; + } + + memset(virtual_servers, 0, sizeof(virtual_servers)); + + /* + * Remember where the modules were stored. + */ + modules = cf_section_sub_find(config, "modules"); + if (!modules) { + WARN("Cannot find a \"modules\" section in the configuration file!"); + } + + /* + * Load dictionaries. + */ + for (cs = cf_subsection_find_next(config, NULL, "server"); + cs != NULL; + cs = cf_subsection_find_next(config, cs, "server")) { +#if defined(WITH_DHCP) || defined(WITH_VMPS) + CONF_SECTION *subcs; + DICT_ATTR const *da; +#endif + +#ifdef WITH_VMPS + /* + * Auto-load the VMPS/VQP dictionary. + */ + subcs = cf_section_sub_find(cs, "vmps"); + if (subcs) { + da = dict_attrbyname("VQP-Packet-Type"); + if (!da) { + if (dict_read(main_config.dictionary_dir, "dictionary.vqp") < 0) { + ERROR("Failed reading dictionary.vqp: %s", + fr_strerror()); + return -1; + } + cf_log_module(cs, "Loading dictionary.vqp"); + + da = dict_attrbyname("VQP-Packet-Type"); + if (!da) { + ERROR("No VQP-Packet-Type in dictionary.vqp"); + return -1; + } + } + } +#endif + +#ifdef WITH_DHCP + /* + * Auto-load the DHCP dictionary. + */ + subcs = cf_subsection_find_next(cs, NULL, "dhcp"); + if (subcs) { + da = dict_attrbyname("DHCP-Message-Type"); + if (!da) { + cf_log_module(cs, "Loading dictionary.dhcp"); + if (dict_read(main_config.dictionary_dir, "dictionary.dhcp") < 0) { + ERROR("Failed reading dictionary.dhcp: %s", + fr_strerror()); + return -1; + } + + da = dict_attrbyname("DHCP-Message-Type"); + if (!da) { + ERROR("No DHCP-Message-Type in dictionary.dhcp"); + return -1; + } + } + } +#endif + /* + * Else it's a RADIUS virtual server, and the + * dictionaries are already loaded. + */ + + /* + * Root through each virtual server, defining + * Autz-Type and Auth-Type. This is so that the + * modules can reference a particular type. + */ + if (!server_define_types(cs)) return -1; + } + + DEBUG2("%s: #### Instantiating modules ####", main_config.name); + + cf_log_info(config, " modules {"); + + /* + * Loop over module definitions, looking for duplicates. + * + * This is O(N^2) in the number of modules, but most + * systems should have less than 100 modules. + */ + for (ci = cf_item_find_next(modules, NULL); + ci != NULL; + ci = next) { + char const *name1; + CONF_SECTION *subcs; + module_instance_t *node; + + next = cf_item_find_next(modules, ci); + + if (!cf_item_is_section(ci)) continue; + + subcs = cf_item_to_section(ci); + + node = module_bootstrap(subcs); + if (!node) return -1; + + if (!next || !cf_item_is_section(next)) continue; + + name1 = cf_section_name1(subcs); + + if (is_reserved_word(name1)) { + cf_log_err_cs(subcs, "Module cannot be named for an 'unlang' keyword"); + return -1; + } + } + + /* + * Look for the 'instantiate' section, which tells us + * the instantiation order of the modules, and also allows + * us to load modules with no authorize/authenticate/etc. + * sections. + */ + cs = cf_section_sub_find(config, "instantiate"); + if (cs) { + CONF_PAIR *cp; + module_instance_t *module; + char const *name; + + cf_log_info(cs, " instantiate {"); + + /* + * Loop over the items in the 'instantiate' section. + */ + for (ci=cf_item_find_next(cs, NULL); + ci != NULL; + ci=cf_item_find_next(cs, ci)) { + /* + * Skip sections and "other" stuff. + * Sections will be handled later, if + * they're referenced at all... + */ + if (cf_item_is_pair(ci)) { + cp = cf_item_to_pair(ci); + name = cf_pair_attr(cp); + + module = module_instantiate(modules, name); + if (!module && (name[0] != '-')) { + return -1; + } + } + + /* + * Can only be "redundant" or + * "load-balance" or + * "redundant-load-balance" + */ + if (cf_item_is_section(ci)) { + bool all_same = true; + module_t const *last = NULL; + CONF_SECTION *subcs; + CONF_ITEM *subci; + + subcs = cf_item_to_section(ci); + name = cf_section_name1(subcs); + + /* + * Groups, etc. must have a name. + */ + if (((strcmp(name, "group") == 0) || + (strcmp(name, "redundant") == 0) || + (strcmp(name, "redundant-load-balance") == 0) || + strcmp(name, "load-balance") == 0)) { + name = cf_section_name2(subcs); + if (!name) { + cf_log_err_cs(subcs, "Subsection must have a name"); + return -1; + } + + if (is_reserved_word(name)) { + cf_log_err_cs(subcs, "Instantiate sections cannot be named for an 'unlang' keyword"); + return -1; + } + } else { + if (is_reserved_word(name)) { + cf_log_err_cs(subcs, "Instantiate sections cannot be named for an 'unlang' keyword"); + return -1; + } + } + + /* + * Ensure that the modules we reference here exist. + */ + for (subci=cf_item_find_next(subcs, NULL); + subci != NULL; + subci=cf_item_find_next(subcs, subci)) { + if (cf_item_is_pair(subci)) { + cp = cf_item_to_pair(subci); + if (cf_pair_value(cp)) { + cf_log_err(subci, "Cannot set return codes in a %s block", + cf_section_name1(subcs)); + return -1; + } + + /* + * Allow "foo.authorize" in subsections. + */ + module = module_instantiate_method(modules, cf_pair_attr(cp), NULL); + if (!module) { + return -1; + } + + if (all_same) { + if (!last) { + last = module->entry->module; + } else if (last != module->entry->module) { + last = NULL; + all_same = false; + } + } + } else { + all_same = false; + } + + /* + * Don't check subsections for now. + */ + } /* loop over modules in a "redundant foo" section */ + + /* + * Register a redundant xlat + */ + if (all_same) { + if (!xlat_register_redundant(cf_item_to_section(ci))) { + WARN("%s[%d] Not registering expansions for %s", + cf_section_filename(subcs), cf_section_lineno(subcs), + cf_section_name2(subcs)); + } + } + } /* handle subsections */ + } /* loop over the "instantiate" section */ + + cf_log_info(cs, " }"); + } /* if there's an 'instantiate' section. */ + + /* + * Now that we've loaded the explicitly ordered modules, + * load everything in the "modules" section. This is + * because we've now split up the modules into + * mods-enabled. + */ + for (ci=cf_item_find_next(modules, NULL); + ci != NULL; + ci=next) { + char const *name; + module_instance_t *module; + CONF_SECTION *subcs; + + next = cf_item_find_next(modules, ci); + + if (!cf_item_is_section(ci)) continue; + + subcs = cf_item_to_section(ci); + name = cf_section_name2(subcs); + if (!name) name = cf_section_name1(subcs); + + module = module_instantiate(modules, name); + if (!module) return -1; + } + cf_log_info(config, " } # modules"); + + if (virtual_servers_load(config) < 0) return -1; + + return 0; +} + +/* + * Call all authorization modules until one returns + * somethings else than RLM_MODULE_OK + */ +rlm_rcode_t process_authorize(int autz_type, REQUEST *request) +{ + return indexed_modcall(MOD_AUTHORIZE, autz_type, request); +} + +/* + * Authenticate a user/password with various methods. + */ +rlm_rcode_t process_authenticate(int auth_type, REQUEST *request) +{ + return indexed_modcall(MOD_AUTHENTICATE, auth_type, request); +} + +#ifdef WITH_ACCOUNTING +/* + * Do pre-accounting for ALL configured sessions + */ +rlm_rcode_t module_preacct(REQUEST *request) +{ + return indexed_modcall(MOD_PREACCT, 0, request); +} + +/* + * Do accounting for ALL configured sessions + */ +rlm_rcode_t process_accounting(int acct_type, REQUEST *request) +{ + return indexed_modcall(MOD_ACCOUNTING, acct_type, request); +} +#endif + +#ifdef WITH_SESSION_MGMT +/* + * See if a user is already logged in. + * + * Returns: 0 == OK, 1 == double logins, 2 == multilink attempt + */ +int process_checksimul(int sess_type, REQUEST *request, int maxsimul) +{ + rlm_rcode_t rcode; + + if(!request->username) + return 0; + + request->simul_count = 0; + request->simul_max = maxsimul; + request->simul_mpp = 1; + + rcode = indexed_modcall(MOD_SESSION, sess_type, request); + + if (rcode != RLM_MODULE_OK) { + /* FIXME: Good spot for a *rate-limited* warning to the log */ + return 0; + } + + return (request->simul_count < maxsimul) ? 0 : request->simul_mpp; +} +#endif + +#ifdef WITH_PROXY +/* + * Do pre-proxying for ALL configured sessions + */ +rlm_rcode_t process_pre_proxy(int type, REQUEST *request) +{ + return indexed_modcall(MOD_PRE_PROXY, type, request); +} + +/* + * Do post-proxying for ALL configured sessions + */ +rlm_rcode_t process_post_proxy(int type, REQUEST *request) +{ + return indexed_modcall(MOD_POST_PROXY, type, request); +} +#endif + +/* + * Do post-authentication for ALL configured sessions + */ +rlm_rcode_t process_post_auth(int postauth_type, REQUEST *request) +{ + return indexed_modcall(MOD_POST_AUTH, postauth_type, request); +} + +#ifdef WITH_COA +rlm_rcode_t process_recv_coa(int recv_coa_type, REQUEST *request) +{ + return indexed_modcall(MOD_RECV_COA, recv_coa_type, request); +} + +rlm_rcode_t process_send_coa(int send_coa_type, REQUEST *request) +{ + return indexed_modcall(MOD_SEND_COA, send_coa_type, request); +} +#endif diff --git a/src/main/pair.c b/src/main/pair.c new file mode 100644 index 0000000..3725ba1 --- /dev/null +++ b/src/main/pair.c @@ -0,0 +1,911 @@ +/* + * This program 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$ + * + * @brief Valuepair functions that are radiusd-specific and as such do not + * belong in the library. + * @file main/pair.c + * + * @ingroup AVP + * + * @copyright 2000,2006 The FreeRADIUS server project + * @copyright 2000 Alan DeKok <aland@ox.org> + */ + +RCSID("$Id$") + +#include <ctype.h> + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/rad_assert.h> + +struct cmp { + DICT_ATTR const *attribute; + DICT_ATTR const *from; + bool first_only; + void *instance; /* module instance */ + RAD_COMPARE_FUNC compare; + struct cmp *next; +}; +static struct cmp *cmp; + +/** Compares check and vp by value. + * + * Does not call any per-attribute comparison function, but does honour + * check.operator. Basically does "vp.value check.op check.value". + * + * @param request Current request. + * @param check rvalue, and operator. + * @param vp lvalue. + * @return 0 if check and vp are equal, -1 if vp value is less than check value, 1 is vp value is more than check + * value, -2 on error. + */ +#ifdef HAVE_REGEX +int radius_compare_vps(REQUEST *request, VALUE_PAIR *check, VALUE_PAIR *vp) +#else +int radius_compare_vps(UNUSED REQUEST *request, VALUE_PAIR *check, VALUE_PAIR *vp) +#endif +{ + int ret = 0; + + /* + * Check for =* and !* and return appropriately + */ + if (check->op == T_OP_CMP_TRUE) return 0; + if (check->op == T_OP_CMP_FALSE) return 1; + +#ifdef HAVE_REGEX + if ((check->op == T_OP_REG_EQ) || (check->op == T_OP_REG_NE)) { + ssize_t slen; + regex_t *preg = NULL; + regmatch_t rxmatch[REQUEST_MAX_REGEX + 1]; /* +1 for %{0} (whole match) capture group */ + size_t nmatch = sizeof(rxmatch) / sizeof(regmatch_t); + + char *expr = NULL, *value = NULL; + char const *expr_p, *value_p; + + if (!vp) return -2; + + if (check->da->type == PW_TYPE_STRING) { + expr_p = check->vp_strvalue; + } else { + expr_p = expr = vp_aprints_value(request, check, '\0'); + } + + if (vp->da->type == PW_TYPE_STRING) { + value_p = vp->vp_strvalue; + } else { + value_p = value = vp_aprints_value(request, vp, '\0'); + } + + if (!expr_p || !value_p) { + REDEBUG("Error stringifying operand for regular expression"); + + regex_error: + talloc_free(preg); + talloc_free(expr); + talloc_free(value); + return -2; + } + + /* + * Include substring matches. + */ + slen = regex_compile(request, &preg, expr_p, talloc_array_length(expr_p) - 1, false, false, true, true); + if (slen <= 0) { + REMARKER(expr_p, -slen, fr_strerror()); + + goto regex_error; + } + + slen = regex_exec(preg, value_p, talloc_array_length(value_p) - 1, rxmatch, &nmatch); + if (slen < 0) { + RERROR("%s", fr_strerror()); + + goto regex_error; + } + + if (check->op == T_OP_REG_EQ) { + /* + * Add in %{0}. %{1}, etc. + */ + regex_sub_to_request(request, &preg, value_p, talloc_array_length(value_p) - 1, + rxmatch, nmatch); + ret = (slen == 1) ? 0 : -1; + } else { + ret = (slen != 1) ? 0 : -1; + } + + talloc_free(preg); + talloc_free(expr); + talloc_free(value); + goto finish; + } +#endif + + /* + * Attributes must be of the same type. + * + * FIXME: deal with type mismatch properly if one side contain + * ABINARY, OCTETS or STRING by converting the other side to + * a string + * + */ + if (vp->da->type != check->da->type) return -1; + + /* + * Tagged attributes are equal if and only if both the + * tag AND value match. + */ + if (check->da->flags.has_tag && !TAG_EQ(check->tag, vp->tag)) { + ret = ((int) vp->tag) - ((int) check->tag); + if (ret != 0) goto finish; + } + + /* + * Not a regular expression, compare the types. + */ + switch (check->da->type) { +#ifdef WITH_ASCEND_BINARY + /* + * Ascend binary attributes can be treated + * as opaque objects, I guess... + */ + case PW_TYPE_ABINARY: +#endif + case PW_TYPE_OCTETS: + if (vp->vp_length != check->vp_length) { + ret = 1; /* NOT equal */ + break; + } + ret = memcmp(vp->vp_strvalue, check->vp_strvalue, + vp->vp_length); + break; + + case PW_TYPE_STRING: + ret = strcmp(vp->vp_strvalue, + check->vp_strvalue); + break; + + case PW_TYPE_BYTE: + ret = vp->vp_byte - check->vp_byte; + break; + case PW_TYPE_SHORT: + ret = vp->vp_short - check->vp_short; + break; + case PW_TYPE_INTEGER: + ret = vp->vp_integer - check->vp_integer; + break; + + case PW_TYPE_INTEGER64: + /* + * Don't want integer overflow! + */ + if (vp->vp_integer64 < check->vp_integer64) { + ret = -1; + } else if (vp->vp_integer64 > check->vp_integer64) { + ret = +1; + } else { + ret = 0; + } + break; + + case PW_TYPE_SIGNED: + if (vp->vp_signed < check->vp_signed) { + ret = -1; + } else if (vp->vp_signed > check->vp_signed) { + ret = +1; + } else { + ret = 0; + } + break; + + case PW_TYPE_DATE: + ret = vp->vp_date - check->vp_date; + break; + + case PW_TYPE_IPV4_ADDR: + ret = ntohl(vp->vp_ipaddr) - ntohl(check->vp_ipaddr); + break; + + case PW_TYPE_IPV6_ADDR: + ret = memcmp(&vp->vp_ipv6addr, &check->vp_ipv6addr, sizeof(vp->vp_ipv6addr)); + break; + + case PW_TYPE_IPV4_PREFIX: + case PW_TYPE_IPV6_PREFIX: + ret = fr_pair_cmp_op(check->op, vp, check); + if (ret == -1) return -2; // error + if (check->op == T_OP_LT || check->op == T_OP_LE) + ret = (ret == 1) ? -1 : 1; + else if (check->op == T_OP_GT || check->op == T_OP_GE) + ret = (ret == 1) ? 1 : -1; + else if (check->op == T_OP_CMP_EQ) + ret = (ret == 1) ? 0 : -1; + break; + + case PW_TYPE_IFID: + ret = memcmp(vp->vp_ifid, check->vp_ifid, sizeof(vp->vp_ifid)); + break; + + default: + break; + } + +finish: + if (ret > 0) return 1; + if (ret < 0) return -1; + return 0; +} + + +/** Compare check and vp. May call the attribute comparison function. + * + * Unlike radius_compare_vps() this function will call any attribute-specific + * comparison functions registered. + * + * @param request Current request. + * @param req list pairs. + * @param check item to compare. + * @param check_pairs list. + * @param reply_pairs list. + * @return 0 if check and vp are equal, -1 if vp value is less than check value, 1 is vp value is more than check + * value. + */ +int radius_callback_compare(REQUEST *request, VALUE_PAIR *req, + VALUE_PAIR *check, VALUE_PAIR *check_pairs, + VALUE_PAIR **reply_pairs) +{ + struct cmp *c; + + /* + * Check for =* and !* and return appropriately + */ + if (check->op == T_OP_CMP_TRUE) return 0; + if (check->op == T_OP_CMP_FALSE) return 1; + + /* + * See if there is a special compare function. + * + * FIXME: use new RB-Tree code. + */ + for (c = cmp; c; c = c->next) { + if (c->attribute == check->da) { + return (c->compare)(c->instance, request, req, check, + check_pairs, reply_pairs); + } + } + + if (!req) return -1; /* doesn't exist, don't compare it */ + + return radius_compare_vps(request, check, req); +} + + +/** Find a comparison function for two attributes. + * + * @todo this should probably take DA's. + * @param attribute to find comparison function for. + * @return true if a comparison function was found, else false. + */ +int radius_find_compare(DICT_ATTR const *attribute) +{ + struct cmp *c; + + for (c = cmp; c; c = c->next) { + if (c->attribute == attribute) { + return true; + } + } + + return false; +} + + +/** See what attribute we want to compare with. + * + * @param attribute to find comparison function for. + * @param from reference to compare with + * @return true if the comparison callback require a matching attribue in the request, else false. + */ +static bool otherattr(DICT_ATTR const *attribute, DICT_ATTR const **from) +{ + struct cmp *c; + + for (c = cmp; c; c = c->next) { + if (c->attribute == attribute) { + *from = c->from; + return c->first_only; + } + } + + *from = attribute; + return false; +} + +/** Register a function as compare function. + * + * @param name the attribute comparison to register + * @param from the attribute we want to compare with. Normally this is the same as attribute. + * If null call the comparison function on every attributes in the request if first_only is false + * @param first_only will decide if we loop over the request attributes or stop on the first one + * @param func comparison function + * @param instance argument to comparison function + * @return 0 + */ +int paircompare_register_byname(char const *name, DICT_ATTR const *from, + bool first_only, RAD_COMPARE_FUNC func, void *instance) +{ + ATTR_FLAGS flags; + DICT_ATTR const *da; + + memset(&flags, 0, sizeof(flags)); + flags.compare = 1; + + da = dict_attrbyname(name); + if (da) { + if (!da->flags.compare) { + fr_strerror_printf("Attribute '%s' already exists.", name); + return -1; + } + } else if (from) { + if (dict_addattr(name, -1, 0, from->type, flags) < 0) { + fr_strerror_printf("Failed creating attribute '%s'", name); + return -1; + } + + da = dict_attrbyname(name); + if (!da) { + fr_strerror_printf("Failed finding attribute '%s'", name); + return -1; + } + + DEBUG("Creating attribute %s", name); + } + + return paircompare_register(da, from, first_only, func, instance); +} + +/** Register a function as compare function. + * + * @param attribute to register comparison function for. + * @param from the attribute we want to compare with. Normally this is the same as attribute. + * If null call the comparison function on every attributes in the request if first_only is false + * @param first_only will decide if we loop over the request attributes or stop on the first one + * @param func comparison function + * @param instance argument to comparison function + * @return 0 + */ +int paircompare_register(DICT_ATTR const *attribute, DICT_ATTR const *from, + bool first_only, RAD_COMPARE_FUNC func, void *instance) +{ + struct cmp *c; + + rad_assert(attribute != NULL); + + paircompare_unregister(attribute, func); + + c = rad_malloc(sizeof(struct cmp)); + + c->compare = func; + c->attribute = attribute; + c->from = from; + c->first_only = first_only; + c->instance = instance; + c->next = cmp; + cmp = c; + + return 0; +} + +/** Unregister comparison function for an attribute + * + * @param attribute dict reference to unregister for. + * @param func comparison function to remove. + */ +void paircompare_unregister(DICT_ATTR const *attribute, RAD_COMPARE_FUNC func) +{ + struct cmp *c, *last; + + last = NULL; + for (c = cmp; c; c = c->next) { + if (c->attribute == attribute && c->compare == func) { + break; + } + last = c; + } + + if (c == NULL) return; + + if (last != NULL) { + last->next = c->next; + } else { + cmp = c->next; + } + + free(c); +} + +/** Unregister comparison function for a module + * + * All paircompare() functions for this module will be unregistered. + * + * @param instance the module instance + */ +void paircompare_unregister_instance(void *instance) +{ + struct cmp *c, **tail; + + tail = &cmp; + while ((c = *tail) != NULL) { + if (c->instance == instance) { + *tail = c->next; + free(c); + continue; + } + + tail = &(c->next); + } +} + +/** Compare two pair lists except for the password information. + * + * For every element in "check" at least one matching copy must be present + * in "reply". + * + * @param[in] request Current request. + * @param[in] req_list request valuepairs. + * @param[in] check Check/control valuepairs. + * @param[in,out] rep_list Reply value pairs. + * + * @return 0 on match. + */ +int paircompare(REQUEST *request, VALUE_PAIR *req_list, VALUE_PAIR *check, + VALUE_PAIR **rep_list) +{ + vp_cursor_t cursor; + VALUE_PAIR *check_item; + VALUE_PAIR *auth_item = NULL; + DICT_ATTR const *from; + + int result = 0; + int compare; + bool first_only; + + for (check_item = fr_cursor_init(&cursor, &check); + check_item; + check_item = fr_cursor_next(&cursor)) { + /* + * If the user is setting a configuration value, + * then don't bother comparing it to any attributes + * sent to us by the user. It ALWAYS matches. + */ + if ((check_item->op == T_OP_SET) || + (check_item->op == T_OP_ADD)) { + continue; + } + + if (!check_item->da->vendor) switch (check_item->da->attr) { + /* + * Attributes we skip during comparison. + * These are "server" check items. + */ + case PW_CRYPT_PASSWORD: + case PW_AUTH_TYPE: + case PW_AUTZ_TYPE: + case PW_ACCT_TYPE: + case PW_SESSION_TYPE: + case PW_STRIP_USER_NAME: + continue; + + /* + * IF the password attribute exists, THEN + * we can do comparisons against it. If not, + * then the request did NOT contain a + * User-Password attribute, so we CANNOT do + * comparisons against it. + * + * This hack makes CHAP-Password work.. + */ + case PW_USER_PASSWORD: + if (check_item->op == T_OP_CMP_EQ) { + WARN("Found User-Password == \"...\""); + WARN("Are you sure you don't mean Cleartext-Password?"); + WARN("See \"man rlm_pap\" for more information"); + } + if (fr_pair_find_by_num(req_list, PW_USER_PASSWORD, 0, TAG_ANY) == NULL) { + continue; + } + break; + } + + /* + * See if this item is present in the request. + */ + first_only = otherattr(check_item->da, &from); + + auth_item = req_list; + try_again: + if (!first_only) { + while (auth_item != NULL) { + VERIFY_VP(auth_item); + if ((auth_item->da == from) || (!from)) { + break; + } + auth_item = auth_item->next; + } + } + + /* + * Not found, it's not a match. + */ + if (auth_item == NULL) { + /* + * Didn't find it. If we were *trying* + * to not find it, then we succeeded. + */ + if (check_item->op == T_OP_CMP_FALSE) { + continue; + } else { + return -1; + } + } + + /* + * Else we found it, but we were trying to not + * find it, so we failed. + */ + if (check_item->op == T_OP_CMP_FALSE) { + return -1; + } + + /* + * We've got to xlat the string before doing + * the comparison. + */ + radius_xlat_do(request, check_item); + + /* + * OK it is present now compare them. + */ + compare = radius_callback_compare(request, auth_item, + check_item, check, rep_list); + + switch (check_item->op) { + case T_OP_EQ: + default: + RWDEBUG("Invalid operator '%s' for item %s: reverting to '=='", + fr_int2str(fr_tokens, check_item->op, "<INVALID>"), check_item->da->name); + /* FALL-THROUGH */ + case T_OP_CMP_TRUE: + case T_OP_CMP_FALSE: + case T_OP_CMP_EQ: + if (compare != 0) result = -1; + break; + + case T_OP_NE: + if (compare == 0) result = -1; + break; + + case T_OP_LT: + if (compare >= 0) result = -1; + break; + + case T_OP_GT: + if (compare <= 0) result = -1; + break; + + case T_OP_LE: + if (compare > 0) result = -1; + break; + + case T_OP_GE: + if (compare < 0) result = -1; + break; + +#ifdef HAVE_REGEX + case T_OP_REG_EQ: + case T_OP_REG_NE: + if (compare != 0) result = -1; + break; +#endif + } /* switch over the operator of the check item */ + + /* + * This attribute didn't match, but maybe there's + * another of the same attribute, which DOES match. + */ + if ((result != 0) && (!first_only)) { + fr_assert(auth_item != NULL); + auth_item = auth_item->next; + result = 0; + goto try_again; + } + + } /* for every entry in the check item list */ + + return result; +} + +/** Expands an attribute marked with fr_pair_mark_xlat + * + * Writes the new value to the vp. + * + * @param request Current request. + * @param vp to expand. + * @return 0 if successful else -1 (on xlat failure) or -2 (on parse failure). + * On failure pair will still no longer be marked for xlat expansion. + */ +int radius_xlat_do(REQUEST *request, VALUE_PAIR *vp) +{ + ssize_t slen; + + char *expanded = NULL; + if (vp->type != VT_XLAT) return 0; + + vp->type = VT_DATA; + + slen = radius_axlat(&expanded, request, vp->value.xlat, NULL, NULL); + rad_const_free(vp->value.xlat); + vp->value.xlat = NULL; + if (slen < 0) { + return -1; + } + + /* + * Parse the string into a new value. + * + * If the VALUE_PAIR is being used in a regular expression + * then we just want to copy the new value in unmolested. + */ + if ((vp->op == T_OP_REG_EQ) || (vp->op == T_OP_REG_NE)) { + fr_pair_value_strsteal(vp, expanded); + return 0; + } + + if (fr_pair_value_from_str(vp, expanded, -1) < 0){ + talloc_free(expanded); + return -2; + } + + talloc_free(expanded); + + return 0; +} + +/** Create a VALUE_PAIR and add it to a list of VALUE_PAIR s + * + * @note This function ALWAYS returns. If we're OOM, then it causes the + * @note server to exit, so you don't need to check the return value. + * + * @param[in] ctx for talloc + * @param[out] vps List to add new VALUE_PAIR to, if NULL will just + * return VALUE_PAIR. + * @param[in] attribute number. + * @param[in] vendor number. + * @return a new VLAUE_PAIR or causes server to exit on error. + */ +VALUE_PAIR *radius_pair_create(TALLOC_CTX *ctx, VALUE_PAIR **vps, + unsigned int attribute, unsigned int vendor) +{ + VALUE_PAIR *vp; + + vp = fr_pair_afrom_num(ctx, attribute, vendor); + if (!vp) { + ERROR("No memory!"); + rad_assert("No memory" == NULL); + fr_exit_now(1); + } + + if (vps) fr_pair_add(vps, vp); + + return vp; +} + +/** Print a single valuepair to stderr or error log. + * + * @param[in] vp list to print. + */ +void debug_pair(VALUE_PAIR *vp) +{ + if (!vp || !rad_debug_lvl || !fr_log_fp) return; + + vp_print(fr_log_fp, vp); +} + +/** Print a single valuepair to stderr or error log. + * + * @param[in] level Debug level (1-4). + * @param[in] request to read logging params from. + * @param[in] vp to print. + * @param[in] prefix (optional). + */ +void rdebug_pair(log_lvl_t level, REQUEST *request, VALUE_PAIR *vp, char const *prefix) +{ + char buffer[768]; + if (!vp || !request || !request->log.func) return; + + if (!radlog_debug_enabled(L_DBG, level, request)) return; + + if (vp->da->flags.secret && request->root && request->root->suppress_secrets && (rad_debug_lvl < 3)) { + RDEBUGX(level, "%s%s = <<< secret >>>", prefix ? prefix : "", vp->da->name); + return; + } + + vp_prints(buffer, sizeof(buffer), vp); + RDEBUGX(level, "%s%s", prefix ? prefix : "", buffer); +} + +/** Print a list of VALUE_PAIRs. + * + * @param[in] level Debug level (1-4). + * @param[in] request to read logging params from. + * @param[in] vp to print. + * @param[in] prefix (optional). + */ +void rdebug_pair_list(log_lvl_t level, REQUEST *request, VALUE_PAIR *vp, char const *prefix) +{ + vp_cursor_t cursor; + char buffer[768]; + if (!vp || !request || !request->log.func) return; + + if (!radlog_debug_enabled(L_DBG, level, request)) return; + + RINDENT(); + for (vp = fr_cursor_init(&cursor, &vp); + vp; + vp = fr_cursor_next(&cursor)) { + VERIFY_VP(vp); + + if (vp->da->flags.secret && request->root && request->root->suppress_secrets && (rad_debug_lvl < 3)) { + RDEBUGX(level, "%s%s = <<< secret >>>", prefix ? prefix : "", vp->da->name); + continue; + } + + vp_prints(buffer, sizeof(buffer), vp); + RDEBUGX(level, "%s%s", prefix ? prefix : "", buffer); + } + REXDENT(); +} + +/** Print a list of protocol VALUE_PAIRs. + * + * @param[in] level Debug level (1-4). + * @param[in] request to read logging params from. + * @param[in] vp to print. + */ +void rdebug_proto_pair_list(log_lvl_t level, REQUEST *request, VALUE_PAIR *vp) +{ + vp_cursor_t cursor; + char buffer[768]; + if (!vp || !request || !request->log.func) return; + + if (!radlog_debug_enabled(L_DBG, level, request)) return; + + RINDENT(); + for (vp = fr_cursor_init(&cursor, &vp); + vp; + vp = fr_cursor_next(&cursor)) { + VERIFY_VP(vp); + if ((vp->da->vendor == 0) && + ((vp->da->attr & 0xFFFF) > 0xff)) continue; + + if (vp->da->flags.secret && request->root && request->root->suppress_secrets && (rad_debug_lvl < 3)) { + RDEBUGX(level, "%s = <<< secret >>>", vp->da->name); + continue; + } + + vp_prints(buffer, sizeof(buffer), vp); + RDEBUGX(level, "%s", buffer); + } + REXDENT(); +} + +/** Return a VP from the specified request. + * + * @param out where to write the pointer to the resolved VP. + * Will be NULL if the attribute couldn't be resolved. + * @param request current request. + * @param name attribute name including qualifiers. + * @return -4 if either the attribute or qualifier were invalid, and the same error codes as tmpl_find_vp for other + * error conditions. + */ +int radius_get_vp(VALUE_PAIR **out, REQUEST *request, char const *name) +{ + int rcode; + vp_tmpl_t vpt; + + *out = NULL; + + if (tmpl_from_attr_str(&vpt, name, REQUEST_CURRENT, PAIR_LIST_REQUEST, false, false) <= 0) { + return -4; + } + + rcode = tmpl_find_vp(out, request, &vpt); + + return rcode; +} + +/** Copy VP(s) from the specified request. + * + * @param ctx to alloc new VALUE_PAIRs in. + * @param out where to write the pointer to the copied VP. + * Will be NULL if the attribute couldn't be resolved. + * @param request current request. + * @param name attribute name including qualifiers. + * @return -4 if either the attribute or qualifier were invalid, and the same error codes as tmpl_find_vp for other + * error conditions. + */ +int radius_copy_vp(TALLOC_CTX *ctx, VALUE_PAIR **out, REQUEST *request, char const *name) +{ + int rcode; + vp_tmpl_t vpt; + + *out = NULL; + + if (tmpl_from_attr_str(&vpt, name, REQUEST_CURRENT, PAIR_LIST_REQUEST, false, false) <= 0) { + return -4; + } + + rcode = tmpl_copy_vps(ctx, out, request, &vpt); + + return rcode; +} + +void module_failure_msg(REQUEST *request, char const *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vmodule_failure_msg(request, fmt, ap); + va_end(ap); +} + +/** Add a module failure message VALUE_PAIR to the request + */ +void vmodule_failure_msg(REQUEST *request, char const *fmt, va_list ap) +{ + char *p; + VALUE_PAIR *vp; + va_list aq; + + if (!fmt || !request || !request->packet) { + return; + } + + /* + * If we don't copy the original ap we get a segfault from vasprintf. This is apparently + * due to ap sometimes being implemented with a stack offset which is invalidated if + * ap is passed into another function. See here: + * http://julipedia.meroh.net/2011/09/using-vacopy-to-safely-pass-ap.html + * + * I don't buy that explanation, but doing a va_copy here does prevent SEGVs seen when + * running unit tests which generate errors under CI. + */ + va_copy(aq, ap); + p = talloc_vasprintf(request, fmt, aq); + va_end(aq); + + MEM(vp = pair_make_request("Module-Failure-Message", NULL, T_OP_ADD)); + if (request->module && (request->module[0] != '\0')) { + fr_pair_value_sprintf(vp, "%s: %s", request->module, p); + } else { + fr_pair_value_sprintf(vp, "%s", p); + } + talloc_free(p); +} diff --git a/src/main/parser.c b/src/main/parser.c new file mode 100644 index 0000000..e337b94 --- /dev/null +++ b/src/main/parser.c @@ -0,0 +1,1809 @@ +/* + * parser.c Parse various things + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2013 Alan DeKok <aland@freeradius.org> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/parser.h> +#include <freeradius-devel/rad_assert.h> + +#include <ctype.h> + +#define PW_CAST_BASE (1850) + +static const FR_NAME_NUMBER allowed_return_codes[] = { + { "reject", 1 }, + { "fail", 1 }, + { "ok", 1 }, + { "handled", 1 }, + { "invalid", 1 }, + { "userlock", 1 }, + { "notfound", 1 }, + { "noop", 1 }, + { "updated", 1 }, + { NULL, 0 } +}; + +/* + * This file shouldn't use any functions from the server core. + */ + +size_t fr_cond_sprint(char *buffer, size_t bufsize, fr_cond_t const *in) +{ + size_t len; + char *p = buffer; + char *end = buffer + bufsize - 1; + fr_cond_t const *c = in; + + rad_assert(bufsize > 0); + +next: + rad_assert(p < end); + + if (!c) { + p[0] = '\0'; + return 0; + } + + /* + * Don't overflow the output buffer. + */ + if ((end - p) < 2) { + p[0] = '\0'; + return 0; + } + + if (c->negate) { + *(p++) = '!'; /* FIXME: only allow for child? */ + } + + switch (c->type) { + case COND_TYPE_EXISTS: + rad_assert(c->data.vpt != NULL); + if (c->cast) { + snprintf(p, end - p, "<%s>", fr_int2str(dict_attr_types, + c->cast->type, "??")); + p += strlen(p); + } + + len = tmpl_prints(p, end - p, c->data.vpt, NULL); + p += len; + break; + + case COND_TYPE_MAP: + rad_assert(c->data.map != NULL); +#if 0 + *(p++) = '['; /* for extra-clear debugging */ +#endif + if (c->cast) { + snprintf(p, end - p, "<%s>", fr_int2str(dict_attr_types, + c->cast->type, "??")); + p += strlen(p); + } + + len = map_prints(p, end - p, c->data.map); + p += len; +#if 0 + *(p++) = ']'; +#endif + break; + + case COND_TYPE_CHILD: + rad_assert(c->data.child != NULL); + *(p++) = '('; + len = fr_cond_sprint(p, end - p, c->data.child); + p += len; + *(p++) = ')'; + break; + + case COND_TYPE_TRUE: + strlcpy(buffer, "true", bufsize); + return strlen(buffer); + + case COND_TYPE_FALSE: + strlcpy(buffer, "false", bufsize); + return strlen(buffer); + + default: + *buffer = '\0'; + return 0; + } + + if (c->next_op == COND_NONE) { + rad_assert(c->next == NULL); + *p = '\0'; + return p - buffer; + } + + if (c->next_op == COND_AND) { + strlcpy(p, " && ", end - p); + p += strlen(p); + + } else if (c->next_op == COND_OR) { + strlcpy(p, " || ", end - p); + p += strlen(p); + + } else { + rad_assert(0 == 1); + } + + c = c->next; + goto next; +} + + +static ssize_t condition_tokenize_string(TALLOC_CTX *ctx, char **out, char const **error, char const *start, + FR_TOKEN *op) +{ + char const *p = start; + char *q; + + switch (*p++) { + default: + return -1; + + case '"': + *op = T_DOUBLE_QUOTED_STRING; + break; + + case '\'': + *op = T_SINGLE_QUOTED_STRING; + break; + + case '`': + *op = T_BACK_QUOTED_STRING; + break; + + case '/': + *op = T_OP_REG_EQ; /* a bit of a hack. */ + break; + + } + + *out = talloc_array(ctx, char, strlen(start) - 1); /* + 2 - 1 */ + if (!*out) return -1; + + q = *out; + + while (*p) { + if (*p == *start) { + /* + * Call the STANDARD parse function to figure out what the string is. + */ + if (cf_new_escape) { + ssize_t slen; + value_data_t data; + char quote = *start; + PW_TYPE src_type = PW_TYPE_STRING; + + /* + * Regex compilers can handle escapes. So we don't do it. + */ + if (quote == '/') quote = '\0'; + + slen = value_data_from_str(ctx, &data, &src_type, NULL, start + 1, p - (start + 1), quote); + if (slen < 0) { + *error = "error parsing string"; + return slen - 1; + } + + talloc_free(*out); + *out = talloc_steal(ctx, data.ptr); + data.strvalue = NULL; + } else { + char *out2; + + *(q++) = '\0'; /* terminate the output string */ + + out2 = talloc_realloc(ctx, *out, char, (q - *out)); + if (!out2) { + *error = "Out of memory"; + return -1; + } + *out = out2; + } + + p++; + return (p - start); + } + + if (*p == '\\') { + if (!p[1]) { + p++; + *error = "End of string after escape"; + return -(p - start); + } + + /* + * Hacks for backwards compatibility + */ + if (cf_new_escape) { + if (p[1] == start[0]) { /* Convert '\'' --> ' */ + p++; + } else { + *(q++) = *(p++); + } + + } else { + switch (p[1]) { + case 'r': + *q++ = '\r'; + break; + case 'n': + *q++ = '\n'; + break; + case 't': + *q++ = '\t'; + break; + default: + *q++ = p[1]; + break; + } + p += 2; + continue; + } + + } + *(q++) = *(p++); + } + + *error = "Unterminated string"; + return -1; +} + +static ssize_t condition_tokenize_word(TALLOC_CTX *ctx, char const *start, char **out, + FR_TOKEN *op, char const **error) +{ + size_t len; + char const *p = start; + + if ((*p == '"') || (*p == '\'') || (*p == '`') || (*p == '/')) { + return condition_tokenize_string(ctx, out, error, start, op); + } + + *op = T_BARE_WORD; + if (*p == '&') p++; /* special-case &User-Name */ + + while (*p) { + /* + * The LHS should really be limited to only a few + * things. For now, we allow pretty much anything. + */ + if (*p == '\\') { + *error = "Unexpected escape"; + return -(p - start); + } + + /* + * ("foo") is valid. + */ + if (*p == ')') { + break; + } + + /* + * Spaces or special characters delineate the word + */ + if (isspace((uint8_t) *p) || (*p == '&') || (*p == '|') || + (*p == '!') || (*p == '=') || (*p == '<') || (*p == '>')) { + break; + } + + if ((*p == '"') || (*p == '\'') || (*p == '`')) { + *error = "Unexpected start of string"; + return -(p - start); + } + + p++; + } + + len = p - start; + if (!len) { + *error = "Empty string is invalid"; + return 0; + } + + *out = talloc_array(ctx, char, len + 1); + memcpy(*out, start, len); + (*out)[len] = '\0'; + return len; +} + + +static ssize_t condition_tokenize_cast(char const *start, DICT_ATTR const **pda, char const **error) +{ + char const *p = start; + char const *q; + PW_TYPE cast; + + while (isspace((uint8_t) *p)) p++; /* skip spaces before condition */ + + if (*p != '<') return 0; + p++; + + q = p; + while (*q && *q != '>') q++; + + cast = fr_substr2int(dict_attr_types, p, PW_TYPE_INVALID, q - p); + if (cast == PW_TYPE_INVALID) { + *error = "Invalid data type in cast"; + return -(p - start); + } + + /* + * We can only cast to basic data types. Complex ones + * are forbidden. + */ + switch (cast) { +#ifdef WITH_ASCEND_BINARY + case PW_TYPE_ABINARY: +#endif + case PW_TYPE_COMBO_IP_ADDR: + case PW_TYPE_TLV: + case PW_TYPE_EXTENDED: + case PW_TYPE_LONG_EXTENDED: + case PW_TYPE_EVS: + case PW_TYPE_VSA: + *error = "Forbidden data type in cast"; + return -(p - start); + + default: + break; + } + + *pda = dict_attrbyvalue(PW_CAST_BASE + cast, 0); + if (!*pda) { + *error = "Cannot cast to this data type"; + return -(p - start); + } + + q++; + + while (isspace((uint8_t) *q)) q++; /* skip spaces after cast */ + + return q - start; +} + +static bool condition_check_types(fr_cond_t *c, PW_TYPE lhs_type) +{ + /* + * SOME integer mismatch is OK. If the LHS has a large type, + * and the RHS has a small type, it's OK. + * + * If the LHS has a small type, and the RHS has a large type, + * then add a cast to the LHS. + */ + if (lhs_type == PW_TYPE_INTEGER64) { + if ((c->data.map->rhs->tmpl_da->type == PW_TYPE_INTEGER) || + (c->data.map->rhs->tmpl_da->type == PW_TYPE_SHORT) || + (c->data.map->rhs->tmpl_da->type == PW_TYPE_BYTE)) { + c->cast = NULL; + return true; + } + } + + if (lhs_type == PW_TYPE_INTEGER) { + if ((c->data.map->rhs->tmpl_da->type == PW_TYPE_SHORT) || + (c->data.map->rhs->tmpl_da->type == PW_TYPE_BYTE)) { + c->cast = NULL; + return true; + } + + if (c->data.map->rhs->tmpl_da->type == PW_TYPE_INTEGER64) { + c->cast = c->data.map->rhs->tmpl_da; + return true; + } + } + + if (lhs_type == PW_TYPE_SHORT) { + if (c->data.map->rhs->tmpl_da->type == PW_TYPE_BYTE) { + c->cast = NULL; + return true; + } + + if ((c->data.map->rhs->tmpl_da->type == PW_TYPE_INTEGER64) || + (c->data.map->rhs->tmpl_da->type == PW_TYPE_INTEGER)) { + c->cast = c->data.map->rhs->tmpl_da; + return true; + } + } + + if (lhs_type == PW_TYPE_BYTE) { + if ((c->data.map->rhs->tmpl_da->type == PW_TYPE_INTEGER64) || + (c->data.map->rhs->tmpl_da->type == PW_TYPE_INTEGER) || + (c->data.map->rhs->tmpl_da->type == PW_TYPE_SHORT)) { + c->cast = c->data.map->rhs->tmpl_da; + return true; + } + } + + if ((lhs_type == PW_TYPE_IPV4_PREFIX) && + (c->data.map->rhs->tmpl_da->type == PW_TYPE_IPV4_ADDR)) { + return true; + } + + if ((lhs_type == PW_TYPE_IPV6_PREFIX) && + (c->data.map->rhs->tmpl_da->type == PW_TYPE_IPV6_ADDR)) { + return true; + } + + /* + * Same checks as above, but with the types swapped, and + * with explicit cast for the interpretor. + */ + if ((lhs_type == PW_TYPE_IPV4_ADDR) && + (c->data.map->rhs->tmpl_da->type == PW_TYPE_IPV4_PREFIX)) { + c->cast = c->data.map->rhs->tmpl_da; + return true; + } + + if ((lhs_type == PW_TYPE_IPV6_ADDR) && + (c->data.map->rhs->tmpl_da->type == PW_TYPE_IPV6_PREFIX)) { + c->cast = c->data.map->rhs->tmpl_da; + return true; + } + + return false; +} + + +/* + * Less code means less bugs + */ +#define return_P(_x) *error = _x;goto return_p +#define return_0(_x) *error = _x;goto return_0 +#define return_lhs(_x) *error = _x;goto return_lhs +#define return_rhs(_x) *error = _x;goto return_rhs +#define return_SLEN goto return_slen + + +/** Tokenize a conditional check + * + * @param[in] ctx for talloc + * @param[in] ci for CONF_ITEM + * @param[in] start the start of the string to process. Should be "(..." + * @param[in] brace look for a closing brace + * @param[out] pcond pointer to the returned condition structure + * @param[out] error the parse error (if any) + * @param[in] flags do one/two pass + * @return length of the string skipped, or when negative, the offset to the offending error + */ +static ssize_t condition_tokenize(TALLOC_CTX *ctx, CONF_ITEM *ci, char const *start, bool brace, + fr_cond_t **pcond, char const **error, int flags) +{ + ssize_t slen, tlen; + char const *p = start; + char const *lhs_p, *rhs_p; + fr_cond_t *c; + char *lhs, *rhs; + FR_TOKEN op, lhs_type, rhs_type; + + c = talloc_zero(ctx, fr_cond_t); + + rad_assert(c != NULL); + lhs = rhs = NULL; + lhs_type = rhs_type = T_INVALID; + + while (isspace((uint8_t) *p)) p++; /* skip spaces before condition */ + + if (!*p) { + return_P("Empty condition is invalid"); + } + + /* + * !COND + */ + if (*p == '!') { + p++; + c->negate = true; + while (isspace((uint8_t) *p)) p++; /* skip spaces after negation */ + + /* + * Just for stupidity + */ + if (*p == '!') { + return_P("Double negation is invalid"); + } + } + + /* + * (COND) + */ + if (*p == '(') { + p++; + + /* + * We've already eaten one layer of + * brackets. Go recurse to get more. + */ + c->type = COND_TYPE_CHILD; + c->ci = ci; + slen = condition_tokenize(c, ci, p, true, &c->data.child, error, flags); + if (slen <= 0) { + return_SLEN; + } + + if (!c->data.child) { + return_P("Empty condition is invalid"); + } + + p += slen; + while (isspace((uint8_t) *p)) p++; /* skip spaces after (COND)*/ + + } else { /* it's a bare FOO==BAR */ + /* + * We didn't see anything special. The condition must be one of + * + * FOO + * FOO OP BAR + */ + + /* + * Grab the LHS + */ + if (*p == '/') { + return_P("Conditional check cannot begin with a regular expression"); + } + + slen = condition_tokenize_cast(p, &c->cast, error); + if (slen < 0) { + return_SLEN; + } + p += slen; + +#ifndef __clang_analyzer__ + lhs_p = p; +#endif + slen = condition_tokenize_word(c, p, &lhs, &lhs_type, error); + if (slen <= 0) { + return_SLEN; + } + p += slen; + + +#ifdef __clang_analyzer__ + if (!lhs) return_P("Internal error"); +#endif + + /* + * If the LHS is 0xabcdef... automatically cast it to octets + */ + if (!c->cast && (lhs_type == T_BARE_WORD) && + (lhs[0] == '0') && (lhs[1] == 'x') && + ((slen & 0x01) == 0)) { + if (slen == 2) { + return_P("Empty octet string is invalid"); + } + + c->cast = dict_attrbyvalue(PW_CAST_BASE + PW_TYPE_OCTETS, 0); + } + + while (isspace((uint8_t)*p)) p++; /* skip spaces after LHS */ + + /* + * We may (or not) have an operator + */ + + + /* + * (FOO) + */ + if (*p == ')') { + /* + * don't skip the brace. We'll look for it later. + */ + goto exists; + + /* + * FOO + */ + } else if (!*p) { + if (brace) { + return_P("No closing brace at end of string"); + } + + goto exists; + + /* + * FOO && ... + */ + } else if (((p[0] == '&') && (p[1] == '&')) || + ((p[0] == '|') && (p[1] == '|'))) { + + exists: + if (c->cast) { + return_0("Cannot do cast for existence check"); + } + + c->type = COND_TYPE_EXISTS; + c->ci = ci; + + tlen = tmpl_afrom_str(c, &c->data.vpt, lhs, talloc_array_length(lhs) - 1, + lhs_type, REQUEST_CURRENT, PAIR_LIST_REQUEST, false); + if (tlen < 0) { + p = lhs_p - tlen; + return_P(fr_strerror()); + } + + rad_assert(c->data.vpt->type != TMPL_TYPE_REGEX); + + if (c->data.vpt->type == TMPL_TYPE_ATTR_UNDEFINED) { + c->pass2_fixup = PASS2_FIXUP_ATTR; + } + + } else { /* it's an operator */ +#ifdef HAVE_REGEX + bool regex = false; + bool iflag = false; + bool mflag = false; +#endif + vp_map_t *map; + + /* + * The next thing should now be a comparison operator. + */ + c->type = COND_TYPE_MAP; + c->ci = ci; + + switch (*p) { + default: + return_P("Invalid text. Expected comparison operator"); + + case '!': + if (p[1] == '=') { + op = T_OP_NE; + p += 2; + +#ifdef HAVE_REGEX + } else if (p[1] == '~') { + regex = true; + + op = T_OP_REG_NE; + p += 2; +#endif + + } else if (p[1] == '*') { + if (lhs_type != T_BARE_WORD) { + return_P("Cannot use !* on a string"); + } + + op = T_OP_CMP_FALSE; + p += 2; + + } else { + goto invalid_operator; + } + break; + + case '=': + if (p[1] == '=') { + op = T_OP_CMP_EQ; + p += 2; + +#ifdef HAVE_REGEX + } else if (p[1] == '~') { + regex = true; + + op = T_OP_REG_EQ; + p += 2; +#endif + + } else if (p[1] == '*') { + if (lhs_type != T_BARE_WORD) { + return_P("Cannot use =* on a string"); + } + + op = T_OP_CMP_TRUE; + p += 2; + + } else { + invalid_operator: + return_P("Invalid operator"); + } + + break; + + case '<': + if (p[1] == '=') { + op = T_OP_LE; + p += 2; + + } else { + op = T_OP_LT; + p++; + } + break; + + case '>': + if (p[1] == '=') { + op = T_OP_GE; + p += 2; + + } else { + op = T_OP_GT; + p++; + } + break; + } + + while (isspace((uint8_t) *p)) p++; /* skip spaces after operator */ + + if (!*p) { + return_P("Expected text after operator"); + } + + /* + * Cannot have a cast on the RHS. + * But produce good errors, too. + */ + if (*p == '<') { + DICT_ATTR const *cast_da; + + slen = condition_tokenize_cast(p, &cast_da, error); + if (slen < 0) { + return_SLEN; + } + +#ifdef __clang_analyzer__ + if (!cast_da) return_P("Internal error"); +#endif + + if (!c->cast) { + return_P("Unexpected cast"); + } + + if (c->cast != cast_da) { + return_P("Cannot cast to a different data type"); + } + + return_P("Unnecessary cast"); + } + + /* + * Grab the RHS + */ + rhs_p = p; + slen = condition_tokenize_word(c, p, &rhs, &rhs_type, error); + if (slen <= 0) { + return_SLEN; + } + +#ifdef HAVE_REGEX + /* + * Sanity checks for regexes. + */ + if (regex) { + if (*p != '/') { + return_P("Expected regular expression"); + } + for (;;) { + switch (p[slen]) { + /* + * /foo/i + */ + case 'i': + iflag = true; + slen++; + continue; + + /* + * /foo/m + */ + case 'm': + mflag = true; + slen++; + continue; + + default: + break; + } + break; + } + } else if (!regex && (*p == '/')) { + return_P("Unexpected regular expression"); + } + +#endif + /* + * Duplicate map_from_fields here, as we + * want to separate parse errors in the + * LHS from ones in the RHS. + */ + c->data.map = map = talloc_zero(c, vp_map_t); + + tlen = tmpl_afrom_str(map, &map->lhs, lhs, talloc_array_length(lhs) - 1, + lhs_type, REQUEST_CURRENT, PAIR_LIST_REQUEST, false); + if (tlen < 0) { + p = lhs_p - tlen; + return_P(fr_strerror()); + } + + if (tmpl_define_unknown_attr(map->lhs) < 0) { + return_lhs("Failed defining attribute"); + return_lhs: + if (lhs) talloc_free(lhs); + if (rhs) talloc_free(rhs); + talloc_free(c); + return -(lhs_p - start); + } + + map->op = op; + + /* + * If the RHS is 0xabcdef... automatically cast it to octets + * unless the LHS is an attribute of type octets, or an + * integer type. + */ + if (!c->cast && (rhs_type == T_BARE_WORD) && + (rhs[0] == '0') && (rhs[1] == 'x') && + ((slen & 0x01) == 0)) { + if (slen == 2) { + return_P("Empty octet string is invalid"); + } + + if ((map->lhs->type != TMPL_TYPE_ATTR) || + !((map->lhs->tmpl_da->type == PW_TYPE_OCTETS) || + (map->lhs->tmpl_da->type == PW_TYPE_BYTE) || + (map->lhs->tmpl_da->type == PW_TYPE_SHORT) || + (map->lhs->tmpl_da->type == PW_TYPE_INTEGER) || + (map->lhs->tmpl_da->type == PW_TYPE_INTEGER64))) { + c->cast = dict_attrbyvalue(PW_CAST_BASE + PW_TYPE_OCTETS, 0); + } + } + + if ((map->lhs->type == TMPL_TYPE_ATTR) && + map->lhs->tmpl_da->flags.is_unknown && + map_cast_from_hex(map, rhs_type, rhs)) { + /* do nothing */ + + } else { + tlen = tmpl_afrom_str(map, &map->rhs, rhs, talloc_array_length(rhs) - 1, rhs_type, + REQUEST_CURRENT, PAIR_LIST_REQUEST, false); + if (tlen < 0) { + p = rhs_p - tlen; + return_P(fr_strerror()); + } + + if (tmpl_define_unknown_attr(map->rhs) < 0) { + return_rhs("Failed defining attribute"); + } + } + + /* + * Unknown attributes get marked up for pass2. + */ + if ((c->data.map->lhs->type == TMPL_TYPE_ATTR_UNDEFINED) || + (c->data.map->rhs->type == TMPL_TYPE_ATTR_UNDEFINED)) { + c->pass2_fixup = PASS2_FIXUP_ATTR; + } + +#ifdef HAVE_REGEX + if (c->data.map->rhs->type == TMPL_TYPE_REGEX) { + c->data.map->rhs->tmpl_iflag = iflag; + c->data.map->rhs->tmpl_mflag = mflag; + } +#endif + + /* + * Save the CONF_ITEM for later. + */ + c->data.map->ci = ci; + + /* + * @todo: check LHS and RHS separately, to + * get better errors + */ + if ((c->data.map->rhs->type == TMPL_TYPE_LIST) || + (c->data.map->lhs->type == TMPL_TYPE_LIST)) { + return_0("Cannot use list references in condition"); + } + + /* + * Check cast type. We can have the RHS + * a string if the LHS has a cast. But + * if the RHS is an attr, it MUST be the + * same type as the LHS. + */ + if (c->cast) { + if ((c->data.map->rhs->type == TMPL_TYPE_ATTR) && + (c->cast->type != c->data.map->rhs->tmpl_da->type)) { + if (condition_check_types(c, c->cast->type)) { + goto keep_going; + } + + goto same_type; + } + +#ifdef HAVE_REGEX + if (c->data.map->rhs->type == TMPL_TYPE_REGEX) { + return_0("Cannot use cast with regex comparison"); + } +#endif + + /* + * The LHS is a literal which has been cast to a data type. + * Cast it to the appropriate data type. + */ + if ((c->data.map->lhs->type == TMPL_TYPE_LITERAL) && + (tmpl_cast_in_place(c->data.map->lhs, c->cast->type, c->cast) < 0)) { + *error = "Failed to parse field"; + if (lhs) talloc_free(lhs); + if (rhs) talloc_free(rhs); + talloc_free(c); + return -(lhs_p - start); + } + + /* + * The RHS is a literal, and the LHS has been cast to a data + * type. + */ + if ((c->data.map->lhs->type == TMPL_TYPE_DATA) && + (c->data.map->rhs->type == TMPL_TYPE_LITERAL) && + (tmpl_cast_in_place(c->data.map->rhs, c->cast->type, c->cast) < 0)) { + return_rhs("Failed to parse field"); + } + + /* + * We may be casting incompatible + * types. We check this based on + * their size. + */ + if (c->data.map->lhs->type == TMPL_TYPE_ATTR) { + /* + * dst.min == src.min + * dst.max == src.max + */ + if ((dict_attr_sizes[c->cast->type][0] == dict_attr_sizes[c->data.map->lhs->tmpl_da->type][0]) && + (dict_attr_sizes[c->cast->type][1] == dict_attr_sizes[c->data.map->lhs->tmpl_da->type][1])) { + goto cast_ok; + } + + /* + * Run-time parsing of strings. + * Run-time copying of octets. + */ + if ((c->data.map->lhs->tmpl_da->type == PW_TYPE_STRING) || + (c->data.map->lhs->tmpl_da->type == PW_TYPE_OCTETS)) { + goto cast_ok; + } + + /* + * ifid to integer64 is OK + */ + if ((c->data.map->lhs->tmpl_da->type == PW_TYPE_IFID) && + (c->cast->type == PW_TYPE_INTEGER64)) { + goto cast_ok; + } + + /* + * ipaddr to ipv4prefix is OK + */ + if ((c->data.map->lhs->tmpl_da->type == PW_TYPE_IPV4_ADDR) && + (c->cast->type == PW_TYPE_IPV4_PREFIX)) { + goto cast_ok; + } + + /* + * ipv6addr to ipv6prefix is OK + */ + if ((c->data.map->lhs->tmpl_da->type == PW_TYPE_IPV6_ADDR) && + (c->cast->type == PW_TYPE_IPV6_PREFIX)) { + goto cast_ok; + } + + /* + * integer64 to ethernet is OK. + */ + if ((c->data.map->lhs->tmpl_da->type == PW_TYPE_INTEGER64) && + (c->cast->type == PW_TYPE_ETHERNET)) { + goto cast_ok; + } + + /* + * dst.max < src.min + * dst.min > src.max + */ + if ((dict_attr_sizes[c->cast->type][1] < dict_attr_sizes[c->data.map->lhs->tmpl_da->type][0]) || + (dict_attr_sizes[c->cast->type][0] > dict_attr_sizes[c->data.map->lhs->tmpl_da->type][1])) { + return_0("Cannot cast to attribute of incompatible size"); + } + } + + cast_ok: + /* + * Casting to a redundant type means we don't need the cast. + * + * Do this LAST, as the rest of the code above assumes c->cast + * is not NULL. + */ + if ((c->data.map->lhs->type == TMPL_TYPE_ATTR) && + (c->cast->type == c->data.map->lhs->tmpl_da->type)) { + c->cast = NULL; + } + + } else { + vp_tmpl_t *vpt; + + /* + * Two attributes? They must be of the same type + */ + if ((c->data.map->rhs->type == TMPL_TYPE_ATTR) && + (c->data.map->lhs->type == TMPL_TYPE_ATTR) && + (c->data.map->lhs->tmpl_da->type != c->data.map->rhs->tmpl_da->type)) { + if (condition_check_types(c, c->data.map->lhs->tmpl_da->type)) { + goto keep_going; + } + + same_type: + return_0("Attribute comparisons must be of the same data type"); + } + + /* + * Without a cast, we can't compare "foo" to User-Name, + * it has to be done the other way around. + */ + if ((c->data.map->rhs->type == TMPL_TYPE_ATTR) && + (c->data.map->lhs->type != TMPL_TYPE_ATTR)) { + *error = "Cannot use attribute reference on right side of condition"; + return_0: + if (lhs) talloc_free(lhs); + if (rhs) talloc_free(rhs); + talloc_free(c); + return 0; + } + + /* + * Invalid: User-Name == bob + * Valid: User-Name == "bob" + * + * There's no real reason for + * this, other than consistency. + */ + if ((c->data.map->lhs->type == TMPL_TYPE_ATTR) && + (c->data.map->rhs->type != TMPL_TYPE_ATTR) && + (c->data.map->lhs->tmpl_da->type == PW_TYPE_STRING) && + (c->data.map->op != T_OP_CMP_TRUE) && + (c->data.map->op != T_OP_CMP_FALSE) && + (rhs_type == T_BARE_WORD)) { + return_rhs("Must have string as value for attribute"); + } + + /* + * Quotes around non-string + * attributes mean that it's + * either xlat, or an exec. + */ + if ((c->data.map->lhs->type == TMPL_TYPE_ATTR) && + (c->data.map->rhs->type != TMPL_TYPE_ATTR) && + (c->data.map->lhs->tmpl_da->type != PW_TYPE_STRING) && + (c->data.map->lhs->tmpl_da->type != PW_TYPE_OCTETS) && + (c->data.map->lhs->tmpl_da->type != PW_TYPE_DATE) && + (rhs_type == T_SINGLE_QUOTED_STRING)) { + *error = "Value must be an unquoted string"; + return_rhs: + if (lhs) talloc_free(lhs); + if (rhs) talloc_free(rhs); + talloc_free(c); + return -(rhs_p - start); + } + + /* + * The LHS has been cast to a data type, and the RHS is a + * literal. Cast the RHS to the type of the cast. + */ + if (c->cast && (c->data.map->rhs->type == TMPL_TYPE_LITERAL) && + (tmpl_cast_in_place(c->data.map->rhs, c->cast->type, c->cast) < 0)) { + return_rhs("Failed to parse field"); + } + + /* + * The LHS is an attribute, and the RHS is a literal. Cast the + * RHS to the data type of the LHS. + * + * Note: There's a hack in here to always parse RHS as the + * equivalent prefix type if the LHS is an IP address. + * + * This allows Framed-IP-Address < 192.168.0.0./24 + */ + if ((c->data.map->lhs->type == TMPL_TYPE_ATTR) && + (c->data.map->rhs->type == TMPL_TYPE_LITERAL)) { + PW_TYPE type = c->data.map->lhs->tmpl_da->type; + + switch (c->data.map->lhs->tmpl_da->type) { + case PW_TYPE_IPV4_ADDR: + if (strchr(c->data.map->rhs->name, '/') != NULL) { + type = PW_TYPE_IPV4_PREFIX; + c->cast = dict_attrbyvalue(PW_CAST_BASE + type, 0); + } + break; + + case PW_TYPE_IPV6_ADDR: + if (strchr(c->data.map->rhs->name, '/') != NULL) { + type = PW_TYPE_IPV6_PREFIX; + c->cast = dict_attrbyvalue(PW_CAST_BASE + type, 0); + } + break; + + default: + break; + } + + if (tmpl_cast_in_place(c->data.map->rhs, type, c->data.map->lhs->tmpl_da) < 0) { + DICT_ATTR const *da = c->data.map->lhs->tmpl_da; + + if ((da->vendor == 0) && + ((da->attr == PW_AUTH_TYPE) || + (da->attr == PW_AUTZ_TYPE) || + (da->attr == PW_ACCT_TYPE) || + (da->attr == PW_SESSION_TYPE) || + (da->attr == PW_POST_AUTH_TYPE) || + (da->attr == PW_PRE_PROXY_TYPE) || + (da->attr == PW_POST_PROXY_TYPE) || + (da->attr == PW_PRE_ACCT_TYPE) || + (da->attr == PW_RECV_COA_TYPE) || + (da->attr == PW_SEND_COA_TYPE))) { + /* + * The types for these attributes are dynamically allocated + * by modules.c, so we can't enforce strictness here. + */ + c->pass2_fixup = PASS2_FIXUP_TYPE; + } else { + return_rhs("Failed to parse value for attribute"); + } + } + + /* + * Stupid WiMAX shit. + * Cast the LHS to the + * type of the RHS. + */ + if (c->data.map->lhs->tmpl_da->type == PW_TYPE_COMBO_IP_ADDR) { + DICT_ATTR const *da; + + da = dict_attrbytype(c->data.map->lhs->tmpl_da->attr, + c->data.map->lhs->tmpl_da->vendor, + c->data.map->rhs->tmpl_data_type); + if (!da) { + return_rhs("Cannot find type for attribute"); + } + c->data.map->lhs->tmpl_da = da; + } + } /* attr to literal comparison */ + + /* + * The RHS will turn into... something. Allow for prefixes + * there, too. + */ + if ((c->data.map->lhs->type == TMPL_TYPE_ATTR) && + ((c->data.map->rhs->type == TMPL_TYPE_XLAT) || + (c->data.map->rhs->type == TMPL_TYPE_XLAT_STRUCT) || + (c->data.map->rhs->type == TMPL_TYPE_EXEC))) { + if (c->data.map->lhs->tmpl_da->type == PW_TYPE_IPV4_ADDR) { + c->cast = dict_attrbyvalue(PW_CAST_BASE + PW_TYPE_IPV4_PREFIX, 0); + } + + if (c->data.map->lhs->tmpl_da->type == PW_TYPE_IPV6_ADDR) { + c->cast = dict_attrbyvalue(PW_CAST_BASE + PW_TYPE_IPV6_PREFIX, 0); + } + } + + /* + * If the LHS is a bare word, AND it looks like + * an attribute, try to parse it as such. + * + * This allows LDAP-Group and SQL-Group to work. + * + * The real fix is to just read the config files, + * and do no parsing until after all of the modules + * are loaded. But that has issues, too. + */ + if ((c->data.map->lhs->type == TMPL_TYPE_LITERAL) && (lhs_type == T_BARE_WORD)) { + int hyphens = 0; + bool may_be_attr = true; + size_t i; + ssize_t attr_slen; + + /* + * Backwards compatibility: Allow Foo-Bar, + * e.g. LDAP-Group and SQL-Group. + */ + for (i = 0; i < c->data.map->lhs->len; i++) { + if (!dict_attr_allowed_chars[(unsigned char) c->data.map->lhs->name[i]]) { + may_be_attr = false; + break; + } + + if (c->data.map->lhs->name[i] == '-') { + hyphens++; + } + } + + if (!hyphens || (hyphens > 3)) may_be_attr = false; + + if (may_be_attr) { + attr_slen = tmpl_afrom_attr_str(c->data.map, &vpt, lhs, + REQUEST_CURRENT, PAIR_LIST_REQUEST, + true, true); + if ((attr_slen > 0) && (vpt->len == c->data.map->lhs->len)) { + talloc_free(c->data.map->lhs); + c->data.map->lhs = vpt; + c->pass2_fixup = PASS2_FIXUP_ATTR; + } + } + } + } /* we didn't have a cast */ + + keep_going: + p += slen; + + while (isspace((uint8_t) *p)) p++; /* skip spaces after RHS */ + } /* parse OP RHS */ + } /* parse a condition (COND) or FOO OP BAR*/ + + /* + * ...COND) + */ + if (*p == ')') { + if (!brace) { + return_P("Unexpected closing brace"); + } + + p++; + while (isspace((uint8_t) *p)) p++; /* skip spaces after closing brace */ + goto done; + } + + /* + * End of string is now allowed. + */ + if (!*p) { + if (brace) { + return_P("No closing brace at end of string"); + } + + goto done; + } + + if (!(((p[0] == '&') && (p[1] == '&')) || + ((p[0] == '|') && (p[1] == '|')))) { + *error = "Unexpected text after condition"; + return_p: + if (lhs) talloc_free(lhs); + if (rhs) talloc_free(rhs); + talloc_free(c); + return -(p - start); + } + + /* + * Recurse to parse the next condition. + */ + c->next_op = p[0]; + p += 2; + + /* + * May still be looking for a closing brace. + */ + slen = condition_tokenize(c, ci, p, brace, &c->next, error, flags); + if (slen <= 0) { + return_slen: + if (lhs) talloc_free(lhs); + if (rhs) talloc_free(rhs); + talloc_free(c); + return slen - (p - start); + } + p += slen; + +done: + /* + * Normalize the condition before returning. + * + * We collapse multiple levels of braces to one. Then + * convert maps to literals. Then literals to true/false + * statements. Then true/false ||/&& followed by other + * conditions to just conditions. + * + * Order is important. The more complex cases are + * converted to simpler ones, from the most complex cases + * to the simplest ones. + */ + + /* + * (FOO) --> FOO + * (FOO) ... --> FOO ... + */ + if ((c->type == COND_TYPE_CHILD) && !c->data.child->next) { + fr_cond_t *child; + + child = talloc_steal(ctx, c->data.child); + c->data.child = NULL; + + child->next = talloc_steal(child, c->next); + c->next = NULL; + + child->next_op = c->next_op; + + /* + * Set the negation properly + */ + if ((c->negate && !child->negate) || + (!c->negate && child->negate)) { + child->negate = true; + } else { + child->negate = false; + } + + lhs = rhs = NULL; + talloc_free(c); + c = child; + } + + /* + * (FOO ...) --> FOO ... + * + * But don't do !(FOO || BAR) --> !FOO || BAR + * Because that's different. + */ + if ((c->type == COND_TYPE_CHILD) && + !c->next && !c->negate) { + fr_cond_t *child; + + child = talloc_steal(ctx, c->data.child); + c->data.child = NULL; + + lhs = rhs = NULL; + talloc_free(c); + c = child; + } + + /* + * Convert maps to literals. Convert one form of map to + * a standardized form. This doesn't make any + * theoretical difference, but it does mean that the + * run-time evaluation has fewer cases to check. + */ + if (c->type == COND_TYPE_MAP) do { + VERIFY_MAP(c->data.map); + + /* + * !FOO !~ BAR --> FOO =~ BAR + */ + if (c->negate && (c->data.map->op == T_OP_REG_NE)) { + c->negate = false; + c->data.map->op = T_OP_REG_EQ; + } + + /* + * FOO !~ BAR --> !FOO =~ BAR + */ + if (!c->negate && (c->data.map->op == T_OP_REG_NE)) { + c->negate = true; + c->data.map->op = T_OP_REG_EQ; + } + + /* + * !FOO != BAR --> FOO == BAR + */ + if (c->negate && (c->data.map->op == T_OP_NE)) { + c->negate = false; + c->data.map->op = T_OP_CMP_EQ; + } + + /* + * This next one catches "LDAP-Group != foo", + * which doesn't work as-is, but this hack fixes + * it. + * + * FOO != BAR --> !FOO == BAR + */ + if (!c->negate && (c->data.map->op == T_OP_NE)) { + c->negate = true; + c->data.map->op = T_OP_CMP_EQ; + } + + /* + * FOO =* BAR --> FOO + * FOO !* BAR --> !FOO + * + * FOO may be a string, or a delayed attribute + * reference. + */ + if ((c->data.map->op == T_OP_CMP_TRUE) || + (c->data.map->op == T_OP_CMP_FALSE)) { + vp_tmpl_t *vpt; + + vpt = talloc_steal(c, c->data.map->lhs); + c->data.map->lhs = NULL; + + /* + * Invert the negation bit. + */ + if (c->data.map->op == T_OP_CMP_FALSE) { + c->negate = !c->negate; + } + + TALLOC_FREE(c->data.map); + + c->type = COND_TYPE_EXISTS; + c->data.vpt = vpt; + break; /* it's no longer a map */ + } + + /* + * Both are data (IP address, integer, etc.) + * + * We can do the evaluation here, so that it + * doesn't need to be done at run time + */ + if ((c->data.map->lhs->type == TMPL_TYPE_DATA) && + (c->data.map->rhs->type == TMPL_TYPE_DATA)) { + int rcode; + + rad_assert(c->cast != NULL); + + rcode = radius_evaluate_map(NULL, 0, 0, c); + TALLOC_FREE(c->data.map); + c->cast = NULL; + if (rcode) { + c->type = COND_TYPE_TRUE; + } else { + c->type = COND_TYPE_FALSE; + } + + break; /* it's no longer a map */ + } + + /* + * Both are literal strings. They're not parsed + * as TMPL_TYPE_DATA because there's no cast to an + * attribute. + * + * We can do the evaluation here, so that it + * doesn't need to be done at run time + */ + if ((c->data.map->rhs->type == TMPL_TYPE_LITERAL) && + (c->data.map->lhs->type == TMPL_TYPE_LITERAL) && + !c->pass2_fixup) { + int rcode; + + rad_assert(c->cast == NULL); + + rcode = radius_evaluate_map(NULL, 0, 0, c); + if (rcode) { + c->type = COND_TYPE_TRUE; + } else { + DEBUG3("OPTIMIZING (%s %s %s) --> FALSE", + c->data.map->lhs->name, + fr_int2str(fr_tokens, c->data.map->op, "??"), + c->data.map->rhs->name); + c->type = COND_TYPE_FALSE; + } + + /* + * Free map after using it above. + */ + TALLOC_FREE(c->data.map); + break; + } + + /* + * <ipaddr>"foo" CMP &Attribute-Name The cast may + * not be necessary, and we can re-write it so + * that the attribute reference is on the LHS. + */ + if (c->cast && + (c->data.map->rhs->type == TMPL_TYPE_ATTR) && + (c->cast->type == c->data.map->rhs->tmpl_da->type) && + (c->data.map->lhs->type != TMPL_TYPE_ATTR)) { + vp_tmpl_t *tmp; + + tmp = c->data.map->rhs; + c->data.map->rhs = c->data.map->lhs; + c->data.map->lhs = tmp; + + c->cast = NULL; + + switch (c->data.map->op) { + case T_OP_CMP_EQ: + /* do nothing */ + break; + + case T_OP_LE: + c->data.map->op = T_OP_GE; + break; + + case T_OP_LT: + c->data.map->op = T_OP_GT; + break; + + case T_OP_GE: + c->data.map->op = T_OP_LE; + break; + + case T_OP_GT: + c->data.map->op = T_OP_LT; + break; + + default: + return_0("Internal sanity check failed 1"); + } + + /* + * This must have been parsed into TMPL_TYPE_DATA. + */ + rad_assert(c->data.map->rhs->type != TMPL_TYPE_LITERAL); + } + + } while (0); + + /* + * Existence checks. We short-circuit static strings, + * too. + * + * FIXME: the data types should be in the template, too. + * So that we know where a literal came from. + * + * "foo" is NOT the same as 'foo' or a bare foo. + */ + if (c->type == COND_TYPE_EXISTS) { + VERIFY_TMPL(c->data.vpt); + + switch (c->data.vpt->type) { + case TMPL_TYPE_XLAT: + case TMPL_TYPE_ATTR: + case TMPL_TYPE_ATTR_UNDEFINED: + case TMPL_TYPE_LIST: + case TMPL_TYPE_EXEC: + break; + + /* + * 'true' and 'false' are special strings + * which mean themselves. + * + * For integers, 0 is false, all other + * integers are true. + * + * For strings, '' and "" are false. + * 'foo' and "foo" are true. + * + * The str2tmpl function takes care of + * marking "%{foo}" as TMPL_TYPE_XLAT, so + * the strings here are fixed at compile + * time. + * + * `exec` and "%{...}" are left alone. + * + * Bare words must be module return + * codes. + */ + case TMPL_TYPE_LITERAL: + if ((strcmp(c->data.vpt->name, "true") == 0) || + (strcmp(c->data.vpt->name, "1") == 0)) { + c->type = COND_TYPE_TRUE; + TALLOC_FREE(c->data.vpt); + + } else if ((strcmp(c->data.vpt->name, "false") == 0) || + (strcmp(c->data.vpt->name, "0") == 0)) { + c->type = COND_TYPE_FALSE; + TALLOC_FREE(c->data.vpt); + + } else if (!*c->data.vpt->name) { + c->type = COND_TYPE_FALSE; + TALLOC_FREE(c->data.vpt); + + } else if ((lhs_type == T_SINGLE_QUOTED_STRING) || + (lhs_type == T_DOUBLE_QUOTED_STRING)) { + c->type = COND_TYPE_TRUE; + TALLOC_FREE(c->data.vpt); + + } else if (lhs_type == T_BARE_WORD) { + int rcode; + bool zeros = true; + char const *q; + + for (q = c->data.vpt->name; + *q != '\0'; + q++) { + if (!isdigit((uint8_t) *q)) { + break; + } + if (*q != '0') zeros = false; + } + + /* + * It's all digits, and therefore + * 'false' if zero, and 'true' otherwise. + */ + if (!*q) { + if (zeros) { + c->type = COND_TYPE_FALSE; + } else { + c->type = COND_TYPE_TRUE; + } + TALLOC_FREE(c->data.vpt); + break; + } + + /* + * Allow &Foo-Bar where Foo-Bar is an attribute + * defined by a module. + */ + if (c->pass2_fixup == PASS2_FIXUP_ATTR) { + break; + } + + rcode = fr_str2int(allowed_return_codes, + c->data.vpt->name, 0); + if (!rcode) { + return_0("Expected a module return code"); + } + } + + /* + * Else lhs_type==T_INVALID, and this + * node was made by promoting a child + * which had already been normalized. + */ + break; + + case TMPL_TYPE_DATA: + return_0("Cannot use data here"); + + default: + return_0("Internal sanity check failed 2"); + } + } + + /* + * !TRUE -> FALSE + */ + if (c->type == COND_TYPE_TRUE) { + if (c->negate) { + c->negate = false; + c->type = COND_TYPE_FALSE; + } + } + + /* + * !FALSE -> TRUE + */ + if (c->type == COND_TYPE_FALSE) { + if (c->negate) { + c->negate = false; + c->type = COND_TYPE_TRUE; + } + } + + /* + * true && FOO --> FOO + */ + if ((c->type == COND_TYPE_TRUE) && + (c->next_op == COND_AND)) { + fr_cond_t *next; + + next = talloc_steal(ctx, c->next); + c->next = NULL; + + lhs = rhs = NULL; + talloc_free(c); + c = next; + } + + /* + * false && FOO --> false + */ + if ((c->type == COND_TYPE_FALSE) && + (c->next_op == COND_AND)) { + talloc_free(c->next); + c->next = NULL; + c->next_op = COND_NONE; + } + + /* + * false || FOO --> FOO + */ + if ((c->type == COND_TYPE_FALSE) && + (c->next_op == COND_OR)) { + fr_cond_t *next; + + next = talloc_steal(ctx, c->next); + c->next = NULL; + + lhs = rhs = NULL; + talloc_free(c); + c = next; + } + + /* + * true || FOO --> true + */ + if ((c->type == COND_TYPE_TRUE) && + (c->next_op == COND_OR)) { + talloc_free(c->next); + c->next = NULL; + c->next_op = COND_NONE; + } + + if (lhs) talloc_free(lhs); + if (rhs) talloc_free(rhs); + + *pcond = c; + return p - start; +} + +/** Tokenize a conditional check + * + * @param[in] ctx for talloc + * @param[in] ci for CONF_ITEM + * @param[in] start the start of the string to process. Should be "(..." + * @param[out] head the parsed condition structure + * @param[out] error the parse error (if any) + * @param[in] flags do one/two pass + * @return length of the string skipped, or when negative, the offset to the offending error + */ +ssize_t fr_condition_tokenize(TALLOC_CTX *ctx, CONF_ITEM *ci, char const *start, fr_cond_t **head, char const **error, int flags) +{ + return condition_tokenize(ctx, ci, start, false, head, error, flags); +} + +/* + * Walk in order. + */ +bool fr_condition_walk(fr_cond_t *c, bool (*callback)(void *, fr_cond_t *), void *ctx) +{ + while (c) { + /* + * Process this one, exit on error. + */ + if (!callback(ctx, c)) return false; + + switch (c->type) { + case COND_TYPE_INVALID: + return false; + + case COND_TYPE_EXISTS: + case COND_TYPE_MAP: + case COND_TYPE_TRUE: + case COND_TYPE_FALSE: + break; + + case COND_TYPE_CHILD: + /* + * Walk over the child. + */ + if (!fr_condition_walk(c->data.child, callback, ctx)) { + return false; + } + } + + /* + * No sibling, stop. + */ + if (c->next_op == COND_NONE) break; + + /* + * process the next sibling + */ + c = c->next; + } + + return true; +} diff --git a/src/main/process.c b/src/main/process.c new file mode 100644 index 0000000..ed77839 --- /dev/null +++ b/src/main/process.c @@ -0,0 +1,6457 @@ +/* + * This program 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 process.c + * @brief Defines the state machines that control how requests are processed. + * + * @copyright 2012 The FreeRADIUS server project + * @copyright 2012 Alan DeKok <aland@deployingradius.com> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/process.h> +#include <freeradius-devel/modules.h> +#include <freeradius-devel/state.h> + +#include <freeradius-devel/rad_assert.h> + +#ifdef WITH_DETAIL +#include <freeradius-devel/detail.h> +#endif + +#include <signal.h> +#include <fcntl.h> + +#ifdef HAVE_SYS_WAIT_H +# include <sys/wait.h> +#endif + +#ifdef HAVE_SYSTEMD_WATCHDOG +# include <systemd/sd-daemon.h> +#endif + +extern pid_t radius_pid; +extern fr_cond_t *debug_condition; + +#ifdef HAVE_SYSTEMD_WATCHDOG +struct timeval sd_watchdog_interval; +static fr_event_t *sd_watchdog_ev; +#endif + +static bool spawn_flag = false; +static bool just_started = true; +time_t fr_start_time = (time_t)-1; +static rbtree_t *pl = NULL; +static fr_event_list_t *el = NULL; + +fr_event_list_t *radius_event_list_corral(UNUSED event_corral_t hint) { + /* Currently we do not run a second event loop for modules. */ + return el; +} + +static char const *action_codes[] = { + "INVALID", + "run", + "done", + "dup", + "timer", +#ifdef WITH_PROXY + "proxy-reply", +#endif + "request was cancelled", + "conflicting packet was received", + "max_time was reached", + "internal failure", + "cleanup_delay was reached", + "CoA packet was cancelled, and not sent", +}; + +#ifdef DEBUG_STATE_MACHINE +# define TRACE_STATE_MACHINE \ +if (rad_debug_lvl) do { \ + struct timeval debug_tv; \ + gettimeofday(&debug_tv, NULL); \ + debug_tv.tv_sec -= fr_start_time; \ + printf("(%u) %d.%06d ********\tSTATE %s action %s live M-%s C-%s\t********\n",\ + request->number, (int) debug_tv.tv_sec, (int) debug_tv.tv_usec, \ + __FUNCTION__, action_codes[action], master_state_names[request->master_state], \ + child_state_names[request->child_state]); \ +} while (0) + +static char const *master_state_names[REQUEST_MASTER_NUM_STATES] = { + "?", + "active", + "stop-processing", + "counted" +}; + +static char const *child_state_names[REQUEST_CHILD_NUM_STATES] = { + "?", + "queued", + "running", + "proxied", + "reject-delay", + "cleanup-delay", + "done" +}; + +#else +# define TRACE_STATE_MACHINE {} +#endif + +static NEVER_RETURNS void _rad_panic(char const *file, unsigned int line, char const *msg) +{ + ERROR("%s[%u]: %s", file, line, msg); + fr_exit_now(1); +} + +#define rad_panic(x) _rad_panic(__FILE__, __LINE__, x) + +/** Declare a state in the state machine + * + * Expands to the start of a function definition for a given state. + * + * @param _x the name of the state. + */ +#define STATE_MACHINE_DECL(_x) static void _x(REQUEST *request, int action) + +static void request_timer(void *ctx); + +/** Insert #REQUEST back into the event heap, to continue executing at a future time + * + * @param file the state machine timer call occurred in. + * @param line the state machine timer call occurred on. + * @param request to set add the timer event for. + * @param when the event should fine. + * @param action to perform when we resume processing the request. + */ +static inline void state_machine_timer(char const *file, int line, REQUEST *request, + struct timeval *when, fr_state_action_t action) +{ + request->timer_action = action; + if (!fr_event_insert(el, request_timer, request, when, &request->ev)) { + _rad_panic(file, line, "Failed to insert event"); + } +} + +/** @copybrief state_machine_timer + * + * @param _x the action to perform when we resume processing the request. + */ +#define STATE_MACHINE_TIMER(_x) state_machine_timer(__FILE__, __LINE__, request, &when, _x) + +/* + * We need a different VERIFY_REQUEST macro in process.c + * To avoid the race conditions with the master thread + * checking the REQUEST whilst it's being worked on by + * the child. + */ +#if defined(WITH_VERIFY_PTR) && defined(HAVE_PTHREAD_H) +# undef VERIFY_REQUEST +# define VERIFY_REQUEST(_x) if (pthread_equal(pthread_self(), _x->child_pid) != 0) verify_request(__FILE__, __LINE__, _x) +#endif + +/** + * @section request_timeline + * + * Time sequence of a request + * @code + * + * RQ-----------------P=============================Y-J-C + * ::::::::::::::::::::::::::::::::::::::::::::::::::::::::M + * @endcode + * + * - R: received. Duplicate detection is done, and request is + * cached. + * + * - Q: Request is placed onto a queue for child threads to pick up. + * If there are no child threads, the request goes immediately + * to P. + * + * - P: Processing the request through the modules. + * + * - Y: Reply is ready. Rejects MAY be delayed here. All other + * replies are sent immediately. + * + * - J: Reject is sent "response_delay" after the reply is ready. + * + * - C: For Access-Requests, After "cleanup_delay", the request is + * deleted. Accounting-Request packets go directly from Y to C. + * + * - M: Max request time. If the request hits this timer, it is + * forcibly stopped. + * + * Other considerations include duplicate and conflicting + * packets. When a dupicate packet is received, it is ignored + * until we've reached Y, as no response is ready. If the reply + * is a reject, duplicates are ignored until J, when we're ready + * to send the reply. In between the reply being sent (Y or J), + * and C, the server responds to duplicates by sending the cached + * reply. + * + * Conflicting packets are sent in 2 situations. + * + * The first is in between R and Y. In that case, we consider + * it as a hint that we're taking too long, and the NAS has given + * up on the request. We then behave just as if the M timer was + * reached, and we discard the current request. This allows us + * to process the new one. + * + * The second case is when we're at Y, but we haven't yet + * finished processing the request. This is a race condition in + * the threading code (avoiding locks is faster). It means that + * a thread has actually encoded and sent the reply, and that the + * NAS has responded with a new packet. The server can then + * safely mark the current request as "OK to delete", and behaves + * just as if the M timer was reached. This usually happens only + * in high-load situations. + * + * Duplicate packets are sent when the NAS thinks we're taking + * too long, and wants a reply. From R-Y, duplicates are + * ignored. From Y-J (for Access-Rejects), duplicates are also + * ignored. From Y-C, duplicates get a duplicate reply. *And*, + * they cause the "cleanup_delay" time to be extended. This + * extension means that we're more likely to send a duplicate + * reply (if we have one), or to suppress processing the packet + * twice if we didn't reply to it. + * + * All functions in this file should be thread-safe, and should + * assume thet the REQUEST structure is being accessed + * simultaneously by the main thread, and by the child worker + * threads. This means that timers, etc. cannot be updated in + * the child thread. + * + * Instead, the master thread periodically calls request->process + * with action TIMER. It's up to the individual functions to + * determine how to handle that. They need to check if they're + * being called from a child thread or the master, and then do + * different things based on that. + */ +#ifdef WITH_PROXY +static fr_packet_list_t *proxy_list = NULL; +static TALLOC_CTX *proxy_ctx = NULL; +#endif + +#ifdef HAVE_PTHREAD_H +# ifdef WITH_PROXY +static pthread_mutex_t proxy_mutex; +static bool proxy_no_new_sockets = false; +# endif + +# define PTHREAD_MUTEX_LOCK if (spawn_flag) pthread_mutex_lock +# define PTHREAD_MUTEX_UNLOCK if (spawn_flag) pthread_mutex_unlock + +static pthread_t NO_SUCH_CHILD_PID; +# define NO_CHILD_THREAD request->child_pid = NO_SUCH_CHILD_PID + +#else +/* + * This is easier than ifdef's throughout the code. + */ +# define PTHREAD_MUTEX_LOCK(_x) +# define PTHREAD_MUTEX_UNLOCK(_x) +# define NO_CHILD_THREAD +#endif + +#ifdef HAVE_PTHREAD_H +static bool we_are_master(void) +{ + if (spawn_flag && + (pthread_equal(pthread_self(), NO_SUCH_CHILD_PID) == 0)) { + return false; + } + + return true; +} + +/* + * Assertions are debug checks. + */ +# ifndef NDEBUG +# define ASSERT_MASTER if (!we_are_master()) rad_panic("We are not master") +# endif +#else + +/* + * No threads: we're always master. + */ +# define we_are_master(_x) (1) +#endif /* HAVE_PTHREAD_H */ + +#ifndef ASSERT_MASTER +# define ASSERT_MASTER +#endif + +/* + * Make state transitions simpler. + */ +#define FINAL_STATE(_x) NO_CHILD_THREAD; request->component = "<" #_x ">"; request->module = ""; request->child_state = _x + + +static void event_new_fd(rad_listen_t *this); + +/* + * We need mutexes around the event FD list *only* in certain + * cases. + */ +#if defined (HAVE_PTHREAD_H) && (defined(WITH_PROXY) || defined(WITH_TCP)) +static rad_listen_t *new_listeners = NULL; + +static pthread_mutex_t fd_mutex; +# define FD_MUTEX_LOCK if (spawn_flag) pthread_mutex_lock +# define FD_MUTEX_UNLOCK if (spawn_flag) pthread_mutex_unlock + +void radius_update_listener(rad_listen_t *this) +{ + /* + * Just do it ourselves. + */ + if (we_are_master()) { + event_new_fd(this); + return; + } + + FD_MUTEX_LOCK(&fd_mutex); + + /* + * If it's already in the list, don't add it again. + */ + if (this->next) { + FD_MUTEX_UNLOCK(&fd_mutex); + return; + } + + /* + * Otherwise, add it to the list + */ + this->next = new_listeners; + new_listeners = this; + FD_MUTEX_UNLOCK(&fd_mutex); + radius_signal_self(RADIUS_SIGNAL_SELF_NEW_FD); +} +#else +void radius_update_listener(rad_listen_t *this) +{ + /* + * No threads. Just insert it. + */ + event_new_fd(this); +} +/* + * This is easier than ifdef's throughout the code. + */ +# define FD_MUTEX_LOCK(_x) +# define FD_MUTEX_UNLOCK(_x) +#endif + +/* + * Emit a systemd watchdog notification and reschedule the event. + */ +#ifdef HAVE_SYSTEMD_WATCHDOG +typedef struct { + fr_event_list_t *el; + struct timeval when; +} sd_watchdog_data_t; + +static sd_watchdog_data_t sdwd; + +static void sd_watchdog_event(void *ctx) +{ + sd_watchdog_data_t *s = (sd_watchdog_data_t *)ctx; + + DEBUG("Emitting systemd watchdog notification"); + sd_notify(0, "WATCHDOG=1"); + + timeradd(&s->when, &sd_watchdog_interval, &s->when); + if (!fr_event_insert(s->el, sd_watchdog_event, ctx, &s->when, &sd_watchdog_ev)) { + rad_panic("Failed to insert event"); + } +} +#endif + +static int request_num_counter = 1; +#ifdef WITH_PROXY +static int request_will_proxy(REQUEST *request) CC_HINT(nonnull); +static int request_proxy(REQUEST *request) CC_HINT(nonnull); +STATE_MACHINE_DECL(request_ping) CC_HINT(nonnull); + +STATE_MACHINE_DECL(request_response_delay) CC_HINT(nonnull); +STATE_MACHINE_DECL(request_cleanup_delay) CC_HINT(nonnull); +STATE_MACHINE_DECL(request_running) CC_HINT(nonnull); +STATE_MACHINE_DECL(request_done) CC_HINT(nonnull); + +STATE_MACHINE_DECL(proxy_no_reply) CC_HINT(nonnull); +STATE_MACHINE_DECL(proxy_running) CC_HINT(nonnull); +STATE_MACHINE_DECL(proxy_wait_for_reply) CC_HINT(nonnull); + +static int process_proxy_reply(REQUEST *request, RADIUS_PACKET *reply) CC_HINT(nonnull (1)); +static void remove_from_proxy_hash(REQUEST *request) CC_HINT(nonnull); +static void remove_from_proxy_hash_nl(REQUEST *request, bool yank) CC_HINT(nonnull); +static int insert_into_proxy_hash(REQUEST *request) CC_HINT(nonnull); +static int setup_post_proxy_fail(REQUEST *request); +#endif + +static REQUEST *request_setup(TALLOC_CTX *ctx, rad_listen_t *listener, RADIUS_PACKET *packet, + RADCLIENT *client, RAD_REQUEST_FUNP fun); +static int request_pre_handler(REQUEST *request, UNUSED int action) CC_HINT(nonnull); + +#ifdef WITH_COA +static void request_coa_originate(REQUEST *request) CC_HINT(nonnull); +STATE_MACHINE_DECL(coa_wait_for_reply) CC_HINT(nonnull); +STATE_MACHINE_DECL(coa_no_reply) CC_HINT(nonnull); +STATE_MACHINE_DECL(coa_running) CC_HINT(nonnull); +static void coa_separate(REQUEST *request, bool retransmit) CC_HINT(nonnull); +# define COA_SEPARATE if (request->coa) coa_separate(request->coa, true); +#else +# define COA_SEPARATE +#endif + +#define CHECK_FOR_STOP do { if (request->master_state == REQUEST_STOP_PROCESSING) {request_done(request, FR_ACTION_CANCELLED);return;}} while (0) + +#undef USEC +#define USEC (1000000) + +#define INSERT_EVENT(_function, _ctx) if (!fr_event_insert(el, _function, _ctx, &((_ctx)->when), &((_ctx)->ev))) { _rad_panic(__FILE__, __LINE__, "Failed to insert event"); } + +static void tv_add(struct timeval *tv, int usec_delay) +{ + if (usec_delay >= USEC) { + tv->tv_sec += usec_delay / USEC; + usec_delay %= USEC; + } + tv->tv_usec += usec_delay; + + if (tv->tv_usec >= USEC) { + tv->tv_sec += tv->tv_usec / USEC; + tv->tv_usec %= USEC; + } +} + +/* + * Debug the packet if requested. + */ +static void debug_packet(REQUEST *request, RADIUS_PACKET *packet, bool received) +{ + char src_ipaddr[128]; + char dst_ipaddr[128]; + + if (!packet) return; + if (!RDEBUG_ENABLED) return; + +#ifdef WITH_DETAIL + /* + * Don't print IP addresses for detail files. + */ + if (request->listener && + (request->listener->type == RAD_LISTEN_DETAIL)) return; + +#endif + /* + * Client-specific debugging re-prints the input + * packet into the client log. + * + * This really belongs in a utility library + */ + if (is_radius_code(packet->code)) { + RDEBUG("%s %s Id %u from %s%s%s:%i to %s%s%s:%i length %zu", + received ? "Received" : "Sent", + fr_packet_codes[packet->code], + packet->id, + packet->src_ipaddr.af == AF_INET6 ? "[" : "", + inet_ntop(packet->src_ipaddr.af, + &packet->src_ipaddr.ipaddr, + src_ipaddr, sizeof(src_ipaddr)), + packet->src_ipaddr.af == AF_INET6 ? "]" : "", + packet->src_port, + packet->dst_ipaddr.af == AF_INET6 ? "[" : "", + inet_ntop(packet->dst_ipaddr.af, + &packet->dst_ipaddr.ipaddr, + dst_ipaddr, sizeof(dst_ipaddr)), + packet->dst_ipaddr.af == AF_INET6 ? "]" : "", + packet->dst_port, + packet->data_len); + } else { + RDEBUG("%s code %u Id %u from %s%s%s:%i to %s%s%s:%i length %zu\n", + received ? "Received" : "Sent", + packet->code, + packet->id, + packet->src_ipaddr.af == AF_INET6 ? "[" : "", + inet_ntop(packet->src_ipaddr.af, + &packet->src_ipaddr.ipaddr, + src_ipaddr, sizeof(src_ipaddr)), + packet->src_ipaddr.af == AF_INET6 ? "]" : "", + packet->src_port, + packet->dst_ipaddr.af == AF_INET6 ? "[" : "", + inet_ntop(packet->dst_ipaddr.af, + &packet->dst_ipaddr.ipaddr, + dst_ipaddr, sizeof(dst_ipaddr)), + packet->dst_ipaddr.af == AF_INET6 ? "]" : "", + packet->dst_port, + packet->data_len); + } + + if (received) { + rdebug_pair_list(L_DBG_LVL_1, request, packet->vps, NULL); + } else { + rdebug_proto_pair_list(L_DBG_LVL_1, request, packet->vps); + } +} + + +/*********************************************************************** + * + * Start of RADIUS server state machine. + * + ***********************************************************************/ + +static struct timeval *request_response_window(REQUEST *request) +{ + VERIFY_REQUEST(request); + + rad_assert(request->home_server != NULL); + + if (request->client) { + /* + * The client hasn't set the response window. Return + * either the home server one, if set, or the global one. + */ + if (!timerisset(&request->client->response_window)) { + return &request->home_server->response_window; + } + + if (timercmp(&request->client->response_window, + &request->home_server->response_window, <)) { + return &request->client->response_window; + } + } + + return &request->home_server->response_window; +} + +/* + * Determine initial request processing delay. + */ +static int request_init_delay(REQUEST *request) +{ + struct timeval half_response_window; + + VERIFY_REQUEST(request); + + /* Allow client response window to lower initial delay */ + if (timerisset(&request->client->response_window)) { + half_response_window.tv_sec = request->client->response_window.tv_sec >> 1; + half_response_window.tv_usec = + ((request->client->response_window.tv_sec & 1) * USEC + + request->client->response_window.tv_usec) >> 1; + if (timercmp(&half_response_window, &request->root->init_delay, <)) + return (int)half_response_window.tv_sec * USEC + + (int)half_response_window.tv_usec; + } + + return (int)request->root->init_delay.tv_sec * USEC + + (int)request->root->init_delay.tv_usec; +} + +/* + * Callback for ALL timer events related to the request. + */ +static void request_timer(void *ctx) +{ + REQUEST *request = talloc_get_type_abort(ctx, REQUEST); + int action; + + action = request->timer_action; + + TRACE_STATE_MACHINE; + + request->process(request, action); +} + +/* + * Wrapper for talloc pools. If there's no parent, just free the + * request. If there is a parent, free the parent INSTEAD of the + * request. + */ +static void request_free(REQUEST *request) +{ + void *ptr; + + rad_assert(request->ev == NULL); + rad_assert(!request->in_request_hash); + rad_assert(!request->in_proxy_hash); + + if ((request->options & RAD_REQUEST_OPTION_CTX) == 0) { + talloc_free(request); + return; + } + + ptr = talloc_parent(request); + rad_assert(ptr != NULL); + talloc_free(ptr); +} + + +#ifdef WITH_PROXY +#ifdef WITH_TLS +void proxy_listener_freeze(rad_listen_t *listener, fr_event_fd_handler_t write_handler) +{ + PTHREAD_MUTEX_LOCK(&proxy_mutex); + if (!fr_packet_list_socket_freeze(proxy_list, + listener->fd)) { + ERROR("Fatal error freezing socket: %s", fr_strerror()); + fr_exit(1); + } + + listener->blocked = true; + + if (fr_event_fd_write_handler(el, 0, listener->fd, write_handler, listener) < 0) { + ERROR("Fatal error freezing socket: %s", fr_strerror()); + fr_exit(1); + } + + PTHREAD_MUTEX_UNLOCK(&proxy_mutex); +} + +void proxy_listener_thaw(rad_listen_t *listener) +{ + PTHREAD_MUTEX_LOCK(&proxy_mutex); + if (!fr_packet_list_socket_thaw(proxy_list, + listener->fd)) { + ERROR("Fatal error freezing socket: %s", fr_strerror()); + fr_exit(1); + } + + listener->blocked = false; + + if (fr_event_fd_write_handler(el, 0, listener->fd, NULL, listener) < 0) { + ERROR("Fatal error freezing socket: %s", fr_strerror()); + fr_exit(1); + } + + PTHREAD_MUTEX_UNLOCK(&proxy_mutex); +} +#endif /* WITH_TLS */ + +static void proxy_reply_too_late(REQUEST *request) +{ + char buffer[128]; + + RDEBUG2("Reply from home server %s port %d - ID: %d arrived too late. Try increasing 'retry_delay' or 'max_request_time'", + inet_ntop(request->proxy->dst_ipaddr.af, + &request->proxy->dst_ipaddr.ipaddr, + buffer, sizeof(buffer)), + request->proxy->dst_port, request->proxy->id); +} +#endif + + +/** Mark a request DONE and clean it up. + * + * When a request is DONE, it can have ties to a number of other + * portions of the server. The request hash, proxy hash, events, + * child threads, etc. This function takes care of either cleaning + * up the request, or managing the timers to wait for the ties to be + * removed. + * + * \dot + * digraph done { + * stopped -> done + * done -> done [ label = "still running" ]; + * } + * \enddot + */ +static void request_done(REQUEST *request, int original) +{ + struct timeval now, when; + int action = original; + + VERIFY_REQUEST(request); + + TRACE_STATE_MACHINE; + + /* + * Force this no matter what. + */ + request->process = request_done; + +#ifdef WITH_DETAIL + /* + * Tell the detail listener that we're done. + */ + if (request->listener && + (request->listener->type == RAD_LISTEN_DETAIL) && + (request->simul_max != 1)) { + request->simul_max = 1; + request->listener->send(request->listener, + request); + } +#endif + +#ifdef HAVE_PTHREAD_H + /* + * If called from a child thread, mark ourselves as done, + * and wait for the master thread timer to clean us up. + */ + if (!we_are_master()) { + FINAL_STATE(REQUEST_DONE); + return; + } +#endif + + /* + * Mark the request as STOP. + */ + request->master_state = REQUEST_STOP_PROCESSING; + +#ifdef WITH_PROXY + /* + * Walk through the server pool to see if we need to mark + * connections as dead. + */ + if (request->home_pool) { + fr_event_now(el, &now); + if (request->home_pool->last_serviced < now.tv_sec) { + int i; + + request->home_pool->last_serviced = now.tv_sec; + + for (i = 0; i < request->home_pool->num_home_servers; i++) { + home_server_t *home = request->home_pool->servers[i]; + + if (home->state == HOME_STATE_CONNECTION_FAIL) { + mark_home_server_dead(home, &now, false); + } + } + } + } +#endif + + /* + * If it was administratively canceled, then it's done. + */ + if (action >= FR_ACTION_CANCELLED) { + action = FR_ACTION_DONE; + +#ifdef WITH_COA + /* + * Don't touch request->coa, it's in the middle + * of being processed... + */ + } else { + /* + * Move the CoA request to its own handler, but + * only if the request finished normally, and was + * not administratively canceled. + */ + if (request->coa) { + coa_separate(request->coa, true); + } else if (request->parent && (request->parent->coa == request)) { + coa_separate(request, true); + } +#endif + } + + /* + * It doesn't hurt to send duplicate replies. All other + * signals are ignored, as the request will be cleaned up + * soon anyways. + */ + switch (action) { + case FR_ACTION_DUP: +#ifdef WITH_DETAIL + rad_assert(request->listener != NULL); +#endif + if (request->reply->code != 0) { + request->listener->send(request->listener, request); + return; + } else { + RDEBUG("No reply. Ignoring retransmit"); + } + break; + + /* + * Mark the request as done. + */ + case FR_ACTION_DONE: +#ifdef HAVE_PTHREAD_H + /* + * If the child is still running, leave it alone. + */ + if (spawn_flag && (request->child_state <= REQUEST_RUNNING)) { + break; + } +#endif + +#ifdef DEBUG_STATE_MACHINE + if (rad_debug_lvl) printf("(%u) ********\tSTATE %s C-%s -> C-%s\t********\n", + request->number, __FUNCTION__, + child_state_names[request->child_state], + child_state_names[REQUEST_DONE]); +#endif + request->child_state = REQUEST_DONE; + break; + + /* + * Called when the child is taking too long to + * finish. We've already marked it "please + * stop", so we don't complain any more. + */ + case FR_ACTION_TIMER: + break; + +#ifdef WITH_PROXY + case FR_ACTION_PROXY_REPLY: + proxy_reply_too_late(request); + break; +#endif + + default: + break; + } + + /* + * Remove it from the request hash. + */ + if (request->in_request_hash) { + if (!rbtree_deletebydata(pl, &request->packet)) { + rad_assert(0 == 1); + } + request->in_request_hash = false; + } + +#ifdef WITH_PROXY + /* + * Wait for the proxy ID to expire. This allows us to + * avoid re-use of proxy IDs for a while. + */ + if (request->in_proxy_hash) { + rad_assert(request->proxy != NULL); + + fr_event_now(el, &now); + when = request->proxy->timestamp; + +#ifdef WITH_COA + if (((request->proxy->code == PW_CODE_COA_REQUEST) || + (request->proxy->code == PW_CODE_DISCONNECT_REQUEST)) && + (request->packet->code != request->proxy->code)) { + when.tv_sec += request->home_server->coa_mrd; + } else +#endif + timeradd(&when, request_response_window(request), &when); + + /* + * We haven't received all responses, AND there's still + * time to wait. Do so. + */ + if ((request->num_proxied_requests > request->num_proxied_responses) && +#ifdef WITH_TCP + (request->home_server->proto != IPPROTO_TCP) && +#endif + timercmp(&now, &when, <)) { + RDEBUG("Waiting for more responses from the home server"); + goto wait_some_more; + } + + /* + * Time to remove it. + */ + remove_from_proxy_hash(request); + } +#endif + +#ifdef HAVE_PTHREAD_H + /* + * If there's no children, we can mark the request as done. + */ + if (!spawn_flag) request->child_state = REQUEST_DONE; +#endif + + /* + * If the child is still running, wait for it to be finished. + */ + if (request->child_state <= REQUEST_RUNNING) { + gettimeofday(&now, NULL); +#ifdef WITH_PROXY + wait_some_more: +#endif + when = now; + if (request->delay < (USEC / 3)) request->delay = USEC / 3; + tv_add(&when, request->delay); + request->delay += request->delay >> 1; + if (request->delay > (10 * USEC)) request->delay = 10 * USEC; + + STATE_MACHINE_TIMER(FR_ACTION_TIMER); + return; + } + +#ifdef HAVE_PTHREAD_H + rad_assert(request->child_pid == NO_SUCH_CHILD_PID); +#endif + +#ifdef WITH_COA + /* + * Now that the child is done, free the CoA packet. If + * the CoA is running, it's already been separated. + */ + if (request->coa) TALLOC_FREE(request->coa); +#endif + + + /* + * @todo: do final states for TCP sockets, too? + */ + request_stats_final(request); +#ifdef WITH_TCP + if (request->listener) { + request->listener->count--; + + /* + * If we're the last one, remove the listener now. + */ + if ((request->listener->count == 0) && + (request->listener->status >= RAD_LISTEN_STATUS_FROZEN)) { + event_new_fd(request->listener); + } + } +#endif + + if (request->packet) { + RDEBUG2("Cleaning up request packet ID %u with timestamp +%d due to %s", + request->packet->id, + (unsigned int) (request->timestamp - fr_start_time), + action_codes[original]); + } /* else don't print anything */ + + ASSERT_MASTER; + fr_event_delete(el, &request->ev); + request_free(request); +} + + +static void request_cleanup_delay_init(REQUEST *request) +{ + struct timeval now, when; + + VERIFY_REQUEST(request); + + /* + * Do cleanup delay ONLY for RADIUS packets from a real + * client. Everything else just gets cleaned up + * immediately. + */ + if (request->packet->dst_port == 0) goto done; + + /* + * Accounting packets shouldn't be retransmitted. They + * should always be updated with Acct-Delay-Time. + */ +#ifdef WITH_ACCOUNTING + if (request->packet->code == PW_CODE_ACCOUNTING_REQUEST) goto done; +#endif + +#ifdef WITH_DHCP + if (request->listener->type == RAD_LISTEN_DHCP) goto done; +#endif + +#ifdef WITH_VMPS + if (request->listener->type == RAD_LISTEN_VQP) goto done; +#endif + + if (!request->root->cleanup_delay) goto done; + + gettimeofday(&now, NULL); + + rad_assert(request->reply->timestamp.tv_sec != 0); + when = request->reply->timestamp; + + request->delay = request->root->cleanup_delay; + when.tv_sec += request->delay; + + /* + * Set timer for when we need to clean it up. + */ + if (timercmp(&when, &now, >)) { +#ifdef DEBUG_STATE_MACHINE + if (rad_debug_lvl) printf("(%u) ********\tNEXT-STATE %s -> %s\n", request->number, __FUNCTION__, "request_cleanup_delay"); +#endif + request->process = request_cleanup_delay; + + if (!we_are_master()) { + FINAL_STATE(REQUEST_CLEANUP_DELAY); + return; + } + + /* + * Update this if we can, otherwise let the timers pick it up. + */ + request->child_state = REQUEST_CLEANUP_DELAY; +#ifdef HAVE_PTHREAD_H + rad_assert(request->child_pid == NO_SUCH_CHILD_PID); +#endif + STATE_MACHINE_TIMER(FR_ACTION_TIMER); + return; + } + + /* + * Otherwise just clean it up. + */ +done: + request_done(request, FR_ACTION_DONE); +} + + +/* + * Enforce max_request_time. + */ +static bool request_max_time(REQUEST *request) +{ + struct timeval now, when; + rad_assert(request->magic == REQUEST_MAGIC); +#ifdef DEBUG_STATE_MACHINE + int action = FR_ACTION_TIMER; +#endif + + VERIFY_REQUEST(request); + + TRACE_STATE_MACHINE; + ASSERT_MASTER; + + /* + * The child thread has acknowledged it's done. + * Transition to the DONE state. + * + * If the request was marked STOP, then the "check for + * stop" macro already took care of it. + */ + if (request->child_state == REQUEST_DONE) { + request_done(request, FR_ACTION_DONE); + return true; + } + + /* + * The request is still running. Enforce max_request_time. + */ + fr_event_now(el, &now); + when = request->packet->timestamp; + when.tv_sec += request->root->max_request_time; + + /* + * Taking too long: tell it to die. + */ + if (timercmp(&now, &when, >=)) { +#ifdef HAVE_PTHREAD_H + /* + * If there's a child thread processing it, + * complain. + */ + if (spawn_flag && + (pthread_equal(request->child_pid, NO_SUCH_CHILD_PID) == 0)) { + ERROR("Unresponsive child for request %u, in component %s module %s", + request->number, + request->component ? request->component : "<core>", + request->module ? request->module : "<core>"); + request->max_time = true; + + exec_trigger(request, NULL, "server.thread.unresponsive", true); + } +#endif + /* + * Tell the request that it's done. + */ + request_done(request, FR_ACTION_MAX_TIME); + return true; + } + + /* + * Sleep for some more. We HOPE that the child will + * become responsive at some point in the future. We do + * this by adding 50% to the current timer. + */ + when = now; + tv_add(&when, request->delay); + request->delay += request->delay >> 1; + STATE_MACHINE_TIMER(FR_ACTION_TIMER); + return false; +} + +static void request_queue_or_run(REQUEST *request, + fr_request_process_t process) +{ +#ifdef DEBUG_STATE_MACHINE + int action = FR_ACTION_TIMER; +#endif + + VERIFY_REQUEST(request); + + TRACE_STATE_MACHINE; + + /* + * Do this here so that fewer other functions need to do + * it. + */ + if (request->master_state == REQUEST_STOP_PROCESSING) { +#ifdef DEBUG_STATE_MACHINE + if (rad_debug_lvl) printf("(%u) ********\tSTATE %s M-%s causes C-%s-> C-%s\t********\n", + request->number, __FUNCTION__, + master_state_names[request->master_state], + child_state_names[request->child_state], + child_state_names[REQUEST_DONE]); +#endif + request_done(request, FR_ACTION_CANCELLED); + return; + } + + request->process = process; + + if (we_are_master()) { + struct timeval when; + + /* + * (re) set the initial delay. + */ + request->delay = request_init_delay(request); + if (request->delay > USEC) request->delay = USEC; + gettimeofday(&when, NULL); + tv_add(&when, request->delay); + request->delay += request->delay >> 1; + + STATE_MACHINE_TIMER(FR_ACTION_TIMER); + +#ifdef HAVE_PTHREAD_H + if (spawn_flag) { + /* + * A child thread will eventually pick it up. + */ + if (request_enqueue(request)) return; + + /* + * Otherwise we're not going to do anything with + * it... + */ + request_done(request, FR_ACTION_INTERNAL_FAILURE); + return; + } +#endif + } + + request->child_state = REQUEST_RUNNING; + request->process(request, FR_ACTION_RUN); + +#ifdef WNOHANG + /* + * Requests that care about child process exit + * codes have already either called + * rad_waitpid(), or they've given up. + */ + while (waitpid(-1, NULL, WNOHANG) > 0); +#endif +} + +void request_inject(REQUEST *request) +{ + request_queue_or_run(request, request_running); +} + + +static void request_dup(REQUEST *request) +{ + ERROR("(%u) Ignoring duplicate packet from " + "client %s port %d - ID: %u due to unfinished request " + "in component %s module %s", + request->number, request->client->shortname, + request->packet->src_port,request->packet->id, + request->component, request->module); +} + + +/** Sit on a request until it's time to clean it up. + * + * A NAS may not see a response from the server. When the NAS + * retransmits, we want to be able to send a cached reply back. The + * alternative is to re-process the packet, which does bad things for + * EAP, among others. + * + * IF we do see a NAS retransmit, we extend the cleanup delay, + * because the NAS might miss our cached reply. + * + * Otherwise, once we reach cleanup_delay, we transition to DONE. + * + * \dot + * digraph cleanup_delay { + * cleanup_delay; + * send_reply [ label = "send_reply\nincrease cleanup delay" ]; + * + * cleanup_delay -> send_reply [ label = "DUP" ]; + * send_reply -> cleanup_delay; + * cleanup_delay -> proxy_reply_too_late [ label = "PROXY_REPLY", arrowhead = "none" ]; + * cleanup_delay -> cleanup_delay [ label = "TIMER < timeout" ]; + * cleanup_delay -> done [ label = "TIMER >= timeout" ]; + * } + * \enddot + */ +static void request_cleanup_delay(REQUEST *request, int action) +{ + struct timeval when, now; + + VERIFY_REQUEST(request); + + TRACE_STATE_MACHINE; + ASSERT_MASTER; + COA_SEPARATE; + CHECK_FOR_STOP; + + switch (action) { + case FR_ACTION_DUP: + if (request->reply->code != 0) { + DEBUG("(%u) Sending duplicate reply to " + "client %s port %d - ID: %u", + request->number, request->client->shortname, + request->packet->src_port,request->packet->id); + request->listener->send(request->listener, request); + } else { + RDEBUG("No reply. Ignoring retransmit"); + } + + /* + * Double the cleanup_delay to catch retransmits. + */ + when = request->reply->timestamp; + request->delay += request->delay; + when.tv_sec += request->delay; + + STATE_MACHINE_TIMER(FR_ACTION_TIMER); + break; + +#ifdef WITH_PROXY + case FR_ACTION_PROXY_REPLY: + proxy_reply_too_late(request); + break; +#endif + + case FR_ACTION_TIMER: + fr_event_now(el, &now); + + rad_assert(request->root->cleanup_delay > 0); + + when = request->reply->timestamp; + when.tv_sec += request->root->cleanup_delay; + + if (timercmp(&when, &now, >)) { +#ifdef DEBUG_STATE_MACHINE + if (rad_debug_lvl) printf("(%u) ********\tNEXT-STATE %s -> %s\n", request->number, __FUNCTION__, "request_cleanup_delay"); +#endif + STATE_MACHINE_TIMER(FR_ACTION_TIMER); + return; + } /* else it's time to clean up */ + + request_done(request, FR_ACTION_CLEANUP_DELAY); + break; + + default: + RDEBUG3("%s: Ignoring action %s", __FUNCTION__, action_codes[action]); + break; + } +} + + +/** Sit on a request until it's time to respond to it. + * + * For security reasons, rejects (and maybe some other) packets are + * delayed for a while before we respond. This delay means that + * badly behaved NASes don't hammer the server with authentication + * attempts. + * + * Otherwise, once we reach response_delay, we send the reply, and + * transition to cleanup_delay. + * + * \dot + * digraph response_delay { + * response_delay -> proxy_reply_too_late [ label = "PROXY_REPLY", arrowhead = "none" ]; + * response_delay -> response_delay [ label = "DUP, TIMER < timeout" ]; + * response_delay -> send_reply [ label = "TIMER >= timeout" ]; + * send_reply -> cleanup_delay; + * } + * \enddot + */ +static void request_response_delay(REQUEST *request, int action) +{ + struct timeval when, now; + + VERIFY_REQUEST(request); + + TRACE_STATE_MACHINE; + ASSERT_MASTER; + COA_SEPARATE; + CHECK_FOR_STOP; + + switch (action) { + case FR_ACTION_DUP: + RDEBUG("(%u) Discarding duplicate request from " + "client %s port %d - ID: %u due to delayed response", + request->number, request->client->shortname, + request->packet->src_port,request->packet->id); + break; + +#ifdef WITH_PROXY + case FR_ACTION_PROXY_REPLY: + proxy_reply_too_late(request); + break; +#endif + + case FR_ACTION_TIMER: + fr_event_now(el, &now); + + /* + * See if it's time to send the reply. If not, + * we wait some more. + */ + when = request->reply->timestamp; + + tv_add(&when, request->response_delay.tv_sec * USEC); + tv_add(&when, request->response_delay.tv_usec); + + if (timercmp(&when, &now, >)) { +#ifdef DEBUG_STATE_MACHINE + if (rad_debug_lvl) printf("(%u) ********\tNEXT-STATE %s -> %s\n", request->number, __FUNCTION__, "request_response_delay"); +#endif + STATE_MACHINE_TIMER(FR_ACTION_TIMER); + return; + } /* else it's time to send the reject */ + + RDEBUG2("Sending delayed response"); + request->listener->encode(request->listener, request); + debug_packet(request, request->reply, false); + request->listener->send(request->listener, request); + + /* + * Clean up the request. + */ + request_cleanup_delay_init(request); + break; + + default: + RDEBUG3("%s: Ignoring action %s", __FUNCTION__, action_codes[action]); + break; + } +} + + +static int request_pre_handler(REQUEST *request, UNUSED int action) +{ + int rcode; + + VERIFY_REQUEST(request); + + TRACE_STATE_MACHINE; + + if (request->master_state == REQUEST_STOP_PROCESSING) return 0; + + /* + * Don't decode the packet if it's an internal "fake" + * request. Instead, just return so that the caller can + * process it. + */ + if (request->packet->dst_port == 0) { + request->username = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY); + request->password = fr_pair_find_by_num(request->packet->vps, PW_USER_PASSWORD, 0, TAG_ANY); + return 1; + } + + if (!request->packet->vps) { /* FIXME: check for correct state */ + rcode = request->listener->decode(request->listener, request); + +#ifdef WITH_UNLANG + if (debug_condition) { + /* + * Ignore parse errors. + */ + if (radius_evaluate_cond(request, RLM_MODULE_OK, 0, debug_condition) == 1) { + request->log.lvl = L_DBG_LVL_2; + request->log.func = vradlog_request; + } + } +#endif + + debug_packet(request, request->packet, true); + } else { + rcode = 0; + } + + if (rcode < 0) { + RATE_LIMIT(INFO("Dropping packet without response because of error: %s (from client %s)", fr_strerror(), request->client->shortname)); + request->reply->offset = -2; /* bad authenticator */ + return 0; + } + + if (!request->username) { + request->username = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY); + } + + return 1; +} + + +/** Do the final processing of a request before we reply to the NAS. + * + * Various cleanups, suppress responses, copy Proxy-State, and set + * response_delay or cleanup_delay; + */ +static void request_finish(REQUEST *request, int action) +{ + VALUE_PAIR *vp; + + VERIFY_REQUEST(request); + + TRACE_STATE_MACHINE; + CHECK_FOR_STOP; + + (void) action; /* -Wunused */ + +#ifdef WITH_COA + /* + * Don't do post-auth if we're a CoA request originated + * from an Access-Request. See request_alloc_coa() for + * details. + */ + if ((request->options & RAD_REQUEST_OPTION_COA) != 0) goto done; +#endif + + /* + * Override the response code if a control:Response-Packet-Type attribute is present. + */ + vp = fr_pair_find_by_num(request->config, PW_RESPONSE_PACKET_TYPE, 0, TAG_ANY); + if (vp) { + if (vp->vp_integer == 256) { + RDEBUG2("Not responding to request"); + fr_pair_delete_by_num(&request->reply->vps, PW_RESPONSE_PACKET_TYPE, 0, TAG_ANY); + request->reply->code = 0; + } else { + request->reply->code = vp->vp_integer; + } + } + /* + * Catch Auth-Type := Reject BEFORE proxying the packet. + */ + else if (request->packet->code == PW_CODE_ACCESS_REQUEST) { + if (request->reply->code == 0) { + vp = fr_pair_find_by_num(request->config, PW_AUTH_TYPE, 0, TAG_ANY); + if (!vp || (vp->vp_integer != 5)) { + RDEBUG2("There was no response configured: " + "rejecting request"); + } + + request->reply->code = PW_CODE_ACCESS_REJECT; + } + } + + /* + * Copy Proxy-State from the request to the reply. + */ + vp = fr_pair_list_copy_by_num(request->reply, request->packet->vps, + PW_PROXY_STATE, 0, TAG_ANY); + if (vp) fr_pair_add(&request->reply->vps, vp); + + /* + * Call Post-Auth for Access-Request packets. + */ + if (request->packet->code == PW_CODE_ACCESS_REQUEST) { + rad_postauth(request); + + vp = fr_pair_find_by_num(request->config, PW_RESPONSE_PACKET_TYPE, 0, TAG_ANY); + if (vp && (vp->vp_integer == 256)) { + RDEBUG2("Not responding to request"); + request->reply->code = 0; + } + } + +#ifdef WITH_COA + /* + * Maybe originate a CoA request. + */ + if ((action == FR_ACTION_RUN) && !request->proxy && request->coa) { + request_coa_originate(request); + } +#endif + + /* + * Clean up. These are no longer needed. + */ + gettimeofday(&request->reply->timestamp, NULL); + + /* + * Fake packets get marked as "done", and have the + * proxy-reply section deal with the reply attributes. + * We therefore don't free the reply attributes. + */ + if (request->packet->dst_port == 0) { + RDEBUG("Finished internally proxied request."); + FINAL_STATE(REQUEST_DONE); + return; + } + +#ifdef WITH_DETAIL + /* + * Always send the reply to the detail listener. + */ + if (request->listener->type == RAD_LISTEN_DETAIL) { + request->simul_max = 1; + + /* + * But only print the reply if there is one. + */ + if (request->reply->code != 0) { + debug_packet(request, request->reply, false); + } + + request->listener->send(request->listener, request); + goto done; + } +#endif + + /* + * Ignore all "do not respond" packets. + * Except for the detail ones, which need to ping + * the detail file reader so that it will retransmit. + */ + if (!request->reply->code) { + RDEBUG("Not sending reply to client."); + goto done; + } + + /* + * If it's not in the request hash, we MIGHT not want to + * send a reply. + * + * If duplicate packets are allowed, then then only + * reason to NOT be in the request hash is because we + * don't want to send a reply. + * + * FIXME: this is crap. The rest of the state handling + * should use a different field so that we don't have two + * meanings for it. + * + * Otherwise duplicates are forbidden, and the request is + * SUPPOSED to avoid the request hash. + * + * In that case, we need to send a reply. + */ + if (!request->in_request_hash && + !request->listener->nodup) { + RDEBUG("Suppressing reply to client."); + goto done; + } + + /* + * See if we need to delay an Access-Reject packet. + */ + if ((request->packet->code == PW_CODE_ACCESS_REQUEST) && + (request->reply->code == PW_CODE_ACCESS_REJECT) && + (request->root->reject_delay.tv_sec > 0)) { + request->response_delay = request->root->reject_delay; + + vp = fr_pair_find_by_num(request->reply->vps, PW_FREERADIUS_RESPONSE_DELAY, 0, TAG_ANY); + if (vp) { + if (vp->vp_integer <= 10) { + request->response_delay.tv_sec = vp->vp_integer; + } else { + request->response_delay.tv_sec = 10; + } + request->response_delay.tv_usec = 0; + } else { + vp = fr_pair_find_by_num(request->reply->vps, PW_FREERADIUS_RESPONSE_DELAY_USEC, 0, TAG_ANY); + if (vp) { + if (vp->vp_integer <= 10 * USEC) { + request->response_delay.tv_sec = vp->vp_integer / USEC; + request->response_delay.tv_usec = vp->vp_integer % USEC; + } else { + request->response_delay.tv_sec = 10; + request->response_delay.tv_usec = 0; + } + } + } + +#ifdef WITH_PROXY + /* + * If we timed out a proxy packet, don't delay + * the reject any more. + */ + if (request->proxy && !request->proxy_reply) { + request->response_delay.tv_sec = 0; + request->response_delay.tv_usec = 0; + } +#endif + } + + /* + * Send the reply. + */ + if ((request->response_delay.tv_sec == 0) && + (request->response_delay.tv_usec == 0)) { + + /* + * Don't print a reply if there's none to send. + */ + if (request->reply->code != 0) { + if (rad_debug_lvl && request->state && + (request->reply->code == PW_CODE_ACCESS_ACCEPT)) { + if (!fr_pair_find_by_num(request->packet->vps, PW_STATE, 0, TAG_ANY)) { + RWDEBUG2("Unused attributes found in &session-state:"); + } + } + + request->listener->encode(request->listener, request); + debug_packet(request, request->reply, false); + request->listener->send(request->listener, request); + } + + done: + RDEBUG2("Finished request"); + request_cleanup_delay_init(request); + + } else { + /* + * Encode and sign it here, so that the master + * thread can just send the encoded data, which + * means it does less work. + */ + RDEBUG2("Delaying response for %d.%06d seconds", + (int) request->response_delay.tv_sec, (int) request->response_delay.tv_usec); + request->listener->encode(request->listener, request); + request->process = request_response_delay; + + FINAL_STATE(REQUEST_RESPONSE_DELAY); + } +} + +/** Process a request from a client. + * + * The outcome might be that the request is proxied. + * + * \dot + * digraph running { + * running -> running [ label = "TIMER < max_request_time" ]; + * running -> done [ label = "TIMER >= max_request_time" ]; + * running -> proxy [ label = "proxied" ]; + * running -> dup [ label = "DUP", arrowhead = "none" ]; + * } + * \enddot + */ +static void request_running(REQUEST *request, int action) +{ + int rcode; + + VERIFY_REQUEST(request); + + TRACE_STATE_MACHINE; + CHECK_FOR_STOP; + + switch (action) { + case FR_ACTION_TIMER: + (void) request_max_time(request); + break; + + case FR_ACTION_DUP: + request_dup(request); + break; + + case FR_ACTION_RUN: + if (!request_pre_handler(request, action)) { +#ifdef DEBUG_STATE_MACHINE + if (rad_debug_lvl) printf("(%u) ********\tSTATE %s failed in pre-handler C-%s -> C-%s\t********\n", + request->number, __FUNCTION__, + child_state_names[request->child_state], + child_state_names[REQUEST_DONE]); +#endif + FINAL_STATE(REQUEST_DONE); + break; + } + + rad_assert(request->handle != NULL); + request->handle(request); + +#ifdef WITH_PROXY + /* + * We may need to send a proxied request. + */ + rcode = request_will_proxy(request); + if (rcode == 1) { +#ifdef DEBUG_STATE_MACHINE + if (rad_debug_lvl) printf("(%u) ********\tWill Proxy\t********\n", request->number); +#endif + /* + * If this fails, it + * takes care of setting + * up the post proxy fail + * handler. + */ + retry_proxy: + if (request_proxy(request) < 0) { + if (request->home_server && request->home_server->virtual_server) goto req_finished; + + if (request->home_pool && request->home_server && + HOME_SERVER_IS_DEAD(request->home_server)) { + VALUE_PAIR *vp; + REALM *realm = NULL; + home_server_t *home = NULL; + + vp = fr_pair_find_by_num(request->config, PW_PROXY_TO_REALM, 0, TAG_ANY); + if (vp) realm = realm_find2(vp->vp_strvalue); + + /* + * Since request->home_server is dead, + * this function won't pick the same home server as before. + */ + if (realm) home = home_server_ldb(vp->vp_strvalue, request->home_pool, request); + if (home) { + home_server_update_request(home, request); + goto retry_proxy; + } + } + + (void) setup_post_proxy_fail(request); + process_proxy_reply(request, NULL); + goto req_finished; + } + + } else if (rcode < 0) { + /* + * No live home servers, run Post-Proxy-Type Fail. + */ + (void) setup_post_proxy_fail(request); + process_proxy_reply(request, NULL); + goto req_finished; + } else +#endif + { +#ifdef DEBUG_STATE_MACHINE + if (rad_debug_lvl) printf("(%u) ********\tFinished\t********\n", request->number); +#endif + +#ifdef WITH_PROXY + req_finished: +#endif + request_finish(request, action); + } + break; + + default: + RDEBUG3("%s: Ignoring action %s", __FUNCTION__, action_codes[action]); + break; + } +} + +int request_receive(TALLOC_CTX *ctx, rad_listen_t *listener, RADIUS_PACKET *packet, + RADCLIENT *client, RAD_REQUEST_FUNP fun) +{ + uint32_t count; + RADIUS_PACKET **packet_p; + REQUEST *request = NULL; + struct timeval now; + listen_socket_t *sock = NULL; + + VERIFY_PACKET(packet); + + /* + * Set the last packet received. + */ + gettimeofday(&now, NULL); + + packet->timestamp = now; + +#ifdef WITH_ACCOUNTING + if (listener->type != RAD_LISTEN_DETAIL) +#endif + +#ifdef WITH_TCP + { + sock = listener->data; + sock->last_packet = now.tv_sec; + + packet->proto = sock->proto; + } +#endif + + /* + * Skip everything if required. + */ + if (listener->nodup) goto skip_dup; + + packet_p = rbtree_finddata(pl, &packet); + if (packet_p) { + rad_child_state_t child_state; + char const *old_module; + + request = fr_packet2myptr(REQUEST, packet, packet_p); + rad_assert(request->in_request_hash); + child_state = request->child_state; + old_module = request->module; + + /* + * Same src/dst ip/port, length, and + * authentication vector: must be a duplicate. + */ + if ((request->packet->data_len == packet->data_len) && + (memcmp(request->packet->vector, packet->vector, + sizeof(packet->vector)) == 0)) { + +#ifdef WITH_STATS + switch (packet->code) { + case PW_CODE_ACCESS_REQUEST: + FR_STATS_INC(auth, total_dup_requests); + break; + +#ifdef WITH_ACCOUNTING + case PW_CODE_ACCOUNTING_REQUEST: + FR_STATS_INC(acct, total_dup_requests); + break; +#endif +#ifdef WITH_COA + case PW_CODE_COA_REQUEST: + FR_STATS_INC(coa, total_dup_requests); + break; + + case PW_CODE_DISCONNECT_REQUEST: + FR_STATS_INC(dsc, total_dup_requests); + break; +#endif + + default: + break; + } +#endif /* WITH_STATS */ + + /* + * Tell the state machine that there's a + * duplicate request. + */ + request->process(request, FR_ACTION_DUP); + return 0; /* duplicate of live request */ + } + + /* + * Mark the request as done ASAP, and before we + * log anything. The child may stop processing + * the request just as we're logging the + * complaint. + */ + request_done(request, FR_ACTION_CONFLICT); + request = NULL; + + /* + * It's a new request, not a duplicate. If the + * old one is done, then we can clean it up. + */ + if (child_state <= REQUEST_RUNNING) { + /* + * The request is still QUEUED or RUNNING. That's a problem. + */ + ERROR("Received conflicting packet from " + "client %s port %d - ID: %u due to " + "unfinished request in module %s. Giving up on old request.", + client->shortname, + packet->src_port, packet->id, + old_module); + +#ifdef WITH_STATS + switch (packet->code) { + case PW_CODE_ACCESS_REQUEST: + FR_STATS_INC(auth, total_conflicts); + break; + +#ifdef WITH_ACCOUNTING + case PW_CODE_ACCOUNTING_REQUEST: + FR_STATS_INC(acct, total_conflicts); + break; +#endif +#ifdef WITH_COA + case PW_CODE_COA_REQUEST: + FR_STATS_INC(coa, total_conflicts); + break; + + case PW_CODE_DISCONNECT_REQUEST: + FR_STATS_INC(dsc, total_conflicts); + break; +#endif + + default: + break; + } +#endif /* WITH_STATS */ + } + } /* else the new packet is unique */ + + /* + * Quench maximum number of outstanding requests. + */ + if (main_config.max_requests && + ((count = rbtree_num_elements(pl)) > main_config.max_requests)) { + RATE_LIMIT(ERROR("Dropping request (%d is too many): from client %s port %d - ID: %d", count, + client->shortname, + packet->src_port, packet->id); + WARN("Please check the configuration file.\n" + "\tThe value for 'max_requests' is probably set too low.\n")); + + exec_trigger(NULL, NULL, "server.max_requests", true); + return 0; + } + +skip_dup: + /* + * Rate-limit the incoming packets + */ + if (sock && sock->max_rate) { + uint32_t pps; + + pps = rad_pps(&sock->rate_pps_old, &sock->rate_pps_now, &sock->rate_time, &now); + if (pps > sock->max_rate) { + DEBUG("Dropping request due to rate limiting"); + return 0; + } + sock->rate_pps_now++; + } + + /* + * Allocate a pool for the request. + */ + if (!ctx) { + ctx = talloc_pool(NULL, main_config.talloc_pool_size); + if (!ctx) return 0; + talloc_set_name_const(ctx, "request_receive_pool"); + + /* + * The packet is still allocated from a different + * context, but oh well. + */ + (void) talloc_steal(ctx, packet); + } + + request = request_setup(ctx, listener, packet, client, fun); + if (!request) { + talloc_free(ctx); + return 1; + } + + /* + * Mark it as a "real" request with a context. + */ + request->options |= RAD_REQUEST_OPTION_CTX; + + /* + * Remember the request in the list. + */ + if (!listener->nodup) { + if (!rbtree_insert(pl, &request->packet)) { + RERROR("Failed to insert request in the list of live requests: discarding it"); + request_done(request, FR_ACTION_INTERNAL_FAILURE); + return 1; + } + + request->in_request_hash = true; + } + + /* + * Process it. Send a response, and free it. + */ + if (listener->synchronous) { +#ifdef WITH_DETAIL + rad_assert(listener->type != RAD_LISTEN_DETAIL); +#endif + + request->listener->decode(request->listener, request); + request->username = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY); + request->password = fr_pair_find_by_num(request->packet->vps, PW_USER_PASSWORD, 0, TAG_ANY); + + fun(request); + + if (request->reply->code != 0) { + request->listener->send(request->listener, request); + } else { + RDEBUG("Not sending reply"); + } + + /* + * Don't do delayed reject. Oh well. + */ + request_free(request); + return 1; + } + + /* + * Otherwise, insert it into the state machine. + * The child threads will take care of processing it. + */ + request_queue_or_run(request, request_running); + + return 1; +} + + +static REQUEST *request_setup(TALLOC_CTX *ctx, rad_listen_t *listener, RADIUS_PACKET *packet, + RADCLIENT *client, RAD_REQUEST_FUNP fun) +{ + REQUEST *request; + + /* + * Create and initialize the new request. + */ + request = request_alloc(ctx); + if (!request) { + ERROR("No memory"); + return NULL; + } + request->reply = rad_alloc_reply(request, packet); + if (!request->reply) { + ERROR("No memory"); + talloc_free(request); + return NULL; + } + + request->listener = listener; + request->client = client; + request->packet = talloc_steal(request, packet); + request->number = request_num_counter++; + request->priority = listener->type; + request->master_state = REQUEST_ACTIVE; + request->child_state = REQUEST_RUNNING; +#ifdef DEBUG_STATE_MACHINE + if (rad_debug_lvl) printf("(%u) ********\tSTATE %s C-%s -> C-%s\t********\n", + request->number, __FUNCTION__, + child_state_names[request->child_state], + child_state_names[REQUEST_RUNNING]); +#endif + request->handle = fun; + NO_CHILD_THREAD; + +#ifdef WITH_STATS + request->listener->stats.last_packet = request->packet->timestamp.tv_sec; + if (packet->code == PW_CODE_ACCESS_REQUEST) { + request->client->auth.last_packet = request->packet->timestamp.tv_sec; + radius_auth_stats.last_packet = request->packet->timestamp.tv_sec; +#ifdef WITH_ACCOUNTING + } else if (packet->code == PW_CODE_ACCOUNTING_REQUEST) { + request->client->acct.last_packet = request->packet->timestamp.tv_sec; + radius_acct_stats.last_packet = request->packet->timestamp.tv_sec; +#endif + } +#endif /* WITH_STATS */ + + /* + * Status-Server packets go to the head of the queue. + */ + if (request->packet->code == PW_CODE_STATUS_SERVER) request->priority = 0; + + /* + * Set virtual server identity + */ + if (client->server) { + request->server = client->server; + } else if (listener->server) { + request->server = listener->server; + } else { + request->server = NULL; + } + + request->root = &main_config; +#ifdef WITH_TCP + request->listener->count++; +#endif + + /* + * The request passes many of our sanity checks. + * From here on in, if anything goes wrong, we + * send a reject message, instead of dropping the + * packet. + */ + + /* + * Build the reply template from the request. + */ + + request->reply->sockfd = request->packet->sockfd; + request->reply->dst_ipaddr = request->packet->src_ipaddr; + request->reply->src_ipaddr = request->packet->dst_ipaddr; + request->reply->dst_port = request->packet->src_port; + request->reply->src_port = request->packet->dst_port; + request->reply->id = request->packet->id; + request->reply->code = 0; /* UNKNOWN code */ + memcpy(request->reply->vector, request->packet->vector, + sizeof(request->reply->vector)); + request->reply->vps = NULL; + request->reply->data = NULL; + request->reply->data_len = 0; + + return request; +} + +#ifdef WITH_TCP +/*********************************************************************** + * + * TCP Handlers. + * + ***********************************************************************/ + +/* + * Timer function for all TCP sockets. + */ +static void tcp_socket_timer(void *ctx) +{ + rad_listen_t *listener = talloc_get_type_abort(ctx, rad_listen_t); + listen_socket_t *sock = listener->data; + struct timeval end, now; + char buffer[256]; + fr_socket_limit_t *limit; + + ASSERT_MASTER; + + if (listener->status != RAD_LISTEN_STATUS_KNOWN) return; + + fr_event_now(el, &now); + + limit = &sock->limit; + + /* + * If we enforce a lifetime, do it now. + */ + if (limit->lifetime > 0) { + end.tv_sec = sock->opened + limit->lifetime; + end.tv_usec = 0; + + if (timercmp(&end, &now, <=)) { + listener->print(listener, buffer, sizeof(buffer)); + DEBUG("Reached maximum lifetime on socket %s", buffer); + + do_close: + +#ifdef WITH_PROXY + /* + * Proxy sockets get frozen, so that we don't use + * them for new requests. But we do keep them + * open to listen for replies to requests we had + * previously sent. + */ + if (listener->type == RAD_LISTEN_PROXY +#ifdef WITH_COA_TUNNEL + || listener->send_coa +#endif + ) { + PTHREAD_MUTEX_LOCK(&proxy_mutex); + if (!fr_packet_list_socket_freeze(proxy_list, + listener->fd)) { + ERROR("Fatal error freezing socket: %s", fr_strerror()); + fr_exit(1); + } + PTHREAD_MUTEX_UNLOCK(&proxy_mutex); + } +#endif + + /* + * Mark the socket as "don't use if at all possible". + */ + listener->status = RAD_LISTEN_STATUS_FROZEN; + + /* + * If it's blocked, then push all of the requests to other sockets. + */ +#ifdef WITH_TLS + if (listener->blocked) { + listener->status = RAD_LISTEN_STATUS_REMOVE_NOW; + } +#endif + + event_new_fd(listener); + return; + } + } else { + end = now; + end.tv_sec += 3600; + } + + /* + * Enforce an idle timeout. + */ + if (limit->idle_timeout > 0) { + struct timeval idle; + + rad_assert(sock->last_packet != 0); + idle.tv_sec = sock->last_packet + limit->idle_timeout; + idle.tv_usec = 0; + + if (timercmp(&idle, &now, <=)) { + listener->print(listener, buffer, sizeof(buffer)); + DEBUG("Reached idle timeout on socket %s", buffer); + goto do_close; + } + + /* + * Enforce the minimum of idle timeout or lifetime. + */ + if (timercmp(&idle, &end, <)) { + end = idle; + } + } + + /* + * Wake up at t + 0.5s. The code above checks if the timers + * are <= t. This addition gives us a bit of leeway. + */ + end.tv_usec = USEC / 2; + + ASSERT_MASTER; + if (!fr_event_insert(el, tcp_socket_timer, listener, &end, &sock->ev)) { + rad_panic("Failed to insert event"); + } +} + + +#ifdef WITH_PROXY +/* + * Called by socket_del to remove requests with this socket + */ +static int eol_proxy_listener(void *ctx, void *data) +{ + rad_listen_t *this = talloc_get_type_abort(ctx, rad_listen_t); + RADIUS_PACKET **proxy_p = data; + REQUEST *request; + + request = fr_packet2myptr(REQUEST, proxy, proxy_p); + if (request->proxy_listener != this) return 0; + + /* + * The normal "remove_from_proxy_hash" tries to grab the + * proxy mutex. We already have it held, so grabbing it + * again will cause a deadlock. Instead, call the "no + * lock" version of the function. + */ + rad_assert(request->in_proxy_hash == true); + remove_from_proxy_hash_nl(request, false); + + /* + * Don't mark it as DONE. The client can retransmit, and + * the packet SHOULD be re-proxied somewhere else. + * + * Return "2" means that the rbtree code will remove it + * from the tree, and we don't need to do it ourselves. + */ + return 2; +} +#endif /* WITH_PROXY */ + +static int eol_listener(void *ctx, void *data) +{ + rad_listen_t *this = talloc_get_type_abort(ctx, rad_listen_t); + RADIUS_PACKET **packet_p = data; + REQUEST *request; + + request = fr_packet2myptr(REQUEST, packet, packet_p); + if (request->listener != this) return 0; + + request->master_state = REQUEST_STOP_PROCESSING; + request->process = request_done; + + return 0; +} +#endif /* WITH_TCP */ + +#ifdef WITH_PROXY +/*********************************************************************** + * + * Proxy handlers for the state machine. + * + ***********************************************************************/ + +/* + * Called with the proxy mutex held + */ +static void remove_from_proxy_hash_nl(REQUEST *request, bool yank) +{ + VERIFY_REQUEST(request); + + if (!request->in_proxy_hash) return; + +#ifdef COA_TUNNEL + /* + * Track how many IDs are used. This information + * helps the listen_coa_find() function get a + * listener which has free IDs. + */ + rad_assert(request->proxy_listener->num_ids_used > 0); + request->proxy_listener->num_ids_used--; +#endif + + fr_packet_list_id_free(proxy_list, request->proxy, yank); + request->in_proxy_hash = false; + + /* + * On the FIRST reply, decrement the count of outstanding + * requests. Note that this is NOT the count of sent + * packets, but whether or not the home server has + * responded at all. + */ + if (request->home_server && + request->home_server->currently_outstanding) { + request->home_server->currently_outstanding--; + + /* + * If we're NOT sending it packets, AND it's been + * a while since we got a response, then we don't + * know if it's alive or dead. + */ + if ((request->home_server->currently_outstanding == 0) && + (request->home_server->state == HOME_STATE_ALIVE)) { + struct timeval when, now; + + when.tv_sec = request->home_server->last_packet_recv ; + when.tv_usec = 0; + + timeradd(&when, request_response_window(request), &when); + gettimeofday(&now, NULL); + + /* + * last_packet + response_window + * + * We *administratively* mark the home + * server as "unknown" state, because we + * haven't seen a packet for a while. + */ + if (timercmp(&now, &when, >)) { + request->home_server->state = HOME_STATE_UNKNOWN; + request->home_server->last_packet_sent = 0; + request->home_server->last_packet_recv = 0; + } + } + } + + if (request->proxy_listener) { + request->proxy_listener->count--; + } + request->proxy_listener = NULL; + + /* + * Got from YES in hash, to NO, not in hash while we hold + * the mutex. This guarantees that when another thread + * grabs the mutex, the "not in hash" flag is correct. + */ +} + +static void remove_from_proxy_hash(REQUEST *request) +{ + VERIFY_REQUEST(request); + + /* + * Check this without grabbing the mutex because it's a + * lot faster that way. + */ + if (!request->in_proxy_hash) return; + +#ifdef WITH_TCP + /* + * Status-Server packets aren't removed from the proxy hash. They're reused. + * + * Unless we're tearing down the listener. + */ + if ((request->proxy->proto == IPPROTO_TCP) && (request->proxy->code == PW_CODE_STATUS_SERVER) && + request->proxy_listener && (request->proxy_listener->status < RAD_LISTEN_STATUS_EOL)) { + return; + } +#endif + + /* + * The "not in hash" flag is definitive. However, if the + * flag says that it IS in the hash, there might still be + * a race condition where it isn't. + */ + PTHREAD_MUTEX_LOCK(&proxy_mutex); + + if (!request->in_proxy_hash) { + PTHREAD_MUTEX_UNLOCK(&proxy_mutex); + return; + } + + remove_from_proxy_hash_nl(request, true); + + PTHREAD_MUTEX_UNLOCK(&proxy_mutex); +} + +static int insert_into_proxy_hash(REQUEST *request) +{ + char buf[128]; + int tries; + bool success = false; + void *proxy_listener; +#ifdef WITH_COA_TUNNEL + bool reverse_coa = request->proxy_listener && (request->proxy_listener->type != RAD_LISTEN_PROXY); +#endif + + VERIFY_REQUEST(request); + + rad_assert(request->proxy != NULL); + rad_assert(request->home_server != NULL); + rad_assert(proxy_list != NULL); + + + PTHREAD_MUTEX_LOCK(&proxy_mutex); + proxy_listener = request->proxy_listener; /* may or may not be NULL */ + request->num_proxied_requests = 1; + request->num_proxied_responses = 0; + + for (tries = 0; tries < 2; tries++) { + rad_listen_t *this; + listen_socket_t *sock; + + RDEBUG3("proxy: Trying to allocate ID (%d/2)", tries); + success = fr_packet_list_id_alloc(proxy_list, + request->home_server->proto, + &request->proxy, &proxy_listener); + if (success) break; + +#ifdef WITH_COA_TUNNEL + /* + * Can't allocate an ID here, try to grab another + * listener by key. + */ + if (reverse_coa) { + int rcode; + VALUE_PAIR *vp; + + /* + * Find the Originating-Realm key, which + * might not be the same as + * proxy_listener->key. + */ + vp = fr_pair_find_by_num(request->config, PW_PROXY_TO_ORIGINATING_REALM, 0, TAG_ANY); + if (!vp) break; + + /* + * We don't want to hold multiple mutexes + * at the same time. + */ + PTHREAD_MUTEX_UNLOCK(&proxy_mutex); + rcode = listen_coa_find(request, vp->vp_strvalue); + PTHREAD_MUTEX_LOCK(&proxy_mutex); + if (rcode < 0) continue; + break; + } +#endif + + if (tries > 0) continue; /* try opening new socket only once */ + +#ifdef HAVE_PTHREAD_H + if (proxy_no_new_sockets) break; +#endif + + RDEBUG3("proxy: Trying to open a new listener to the home server"); + this = proxy_new_listener(proxy_ctx, request->home_server, 0); + if (!this) { + request->home_server->state = HOME_STATE_CONNECTION_FAIL; + PTHREAD_MUTEX_UNLOCK(&proxy_mutex); + goto fail; + } + + request->proxy->src_port = 0; /* Use any new socket */ + proxy_listener = this; + + sock = this->data; + if (!fr_packet_list_socket_add(proxy_list, this->fd, + sock->proto, +#ifdef WITH_RADIUSV11 + sock->radiusv11, +#endif + &sock->other_ipaddr, sock->other_port, + this)) { + +#ifdef HAVE_PTHREAD_H + proxy_no_new_sockets = true; +#endif + PTHREAD_MUTEX_UNLOCK(&proxy_mutex); + + /* + * This is bad. However, the + * packet list now supports 256 + * open sockets, which should + * minimize this problem. + */ + ERROR("Failed adding proxy socket: %s", + fr_strerror()); + goto fail; + } + +#ifdef COA_TUNNEL + /* + * Track how many IDs are used. This information + * helps the listen_coa_find() function get a + * listener which has free IDs. + */ + request->proxy_listener->num_ids_used++; +#endif + + /* + * Add it to the event loop. Ensure that we have + * only one mutex locked at a time. + */ + PTHREAD_MUTEX_UNLOCK(&proxy_mutex); + radius_update_listener(this); + PTHREAD_MUTEX_LOCK(&proxy_mutex); + } + + if (!proxy_listener || !success) { + PTHREAD_MUTEX_UNLOCK(&proxy_mutex); + REDEBUG2("proxy: Failed allocating Id for proxied request"); + fail: + request->proxy_listener = NULL; + request->in_proxy_hash = false; + return 0; + } + +#ifndef WITH_RADIUSV11 + rad_assert(request->proxy->id >= 0); +#endif + + request->proxy_listener = proxy_listener; + request->in_proxy_hash = true; + RDEBUG3("proxy: request is now in proxy hash"); + + /* + * Keep track of maximum outstanding requests to a + * particular home server. 'max_outstanding' is + * enforced in home_server_ldb(), in realms.c. + */ + request->home_server->currently_outstanding++; + + request->proxy_listener->count++; + + PTHREAD_MUTEX_UNLOCK(&proxy_mutex); + + RDEBUG3("proxy: allocating destination %s port %d - Id %d", + inet_ntop(request->proxy->dst_ipaddr.af, + &request->proxy->dst_ipaddr.ipaddr, buf, sizeof(buf)), + request->proxy->dst_port, + request->proxy->id); + + return 1; +} + +static int process_proxy_reply(REQUEST *request, RADIUS_PACKET *reply) +{ + int rcode; + int post_proxy_type = 0; + VALUE_PAIR *vp; + char const *old_server; +#ifdef WITH_COA_TUNNEL + bool reverse_coa = false; +#endif + + VERIFY_REQUEST(request); + + /* + * There may be a proxy reply, but it may be too late. + */ + if ((request->home_server && !request->home_server->virtual_server) && !request->proxy_listener) return 0; + + /* + * Delete any reply we had accumulated until now. + */ + RDEBUG2("Clearing existing &reply: attributes"); + fr_pair_list_free(&request->reply->vps); + + /* + * Run the packet through the post-proxy stage, + * BEFORE playing games with the attributes. + */ + vp = fr_pair_find_by_num(request->config, PW_POST_PROXY_TYPE, 0, TAG_ANY); + if (vp) { + post_proxy_type = vp->vp_integer; + /* + * If we have a proxy_reply, and it was a reject, or a NAK + * setup Post-Proxy <type>. + * + * If the <type> doesn't have a section, then the Post-Proxy + * section is ignored. + */ + } else if (reply) { + DICT_VALUE *dval = NULL; + + switch (reply->code) { + case PW_CODE_ACCESS_REJECT: + dval = dict_valbyname(PW_POST_PROXY_TYPE, 0, "Reject"); + if (dval) post_proxy_type = dval->value; + break; + + case PW_CODE_DISCONNECT_NAK: + dval = dict_valbyname(PW_POST_PROXY_TYPE, 0, fr_packet_codes[reply->code]); + if (dval) post_proxy_type = dval->value; + break; + + case PW_CODE_COA_NAK: + dval = dict_valbyname(PW_POST_PROXY_TYPE, 0, fr_packet_codes[reply->code]); + if (dval) post_proxy_type = dval->value; + break; + + default: + break; + } + + /* + * Create config:Post-Proxy-Type + */ + if (dval) { + vp = radius_pair_create(request, &request->config, PW_POST_PROXY_TYPE, 0); + vp->vp_integer = dval->value; + } + } + + if (post_proxy_type > 0) RDEBUG2("Found Post-Proxy-Type %s", + dict_valnamebyattr(PW_POST_PROXY_TYPE, 0, post_proxy_type)); + +#ifdef WITH_COA_TUNNEL + /* + * Cache this, as request->proxy_listener will be + * NULL after removing the request from the proxy + * hash. + */ + if (request->proxy_listener) reverse_coa = request->proxy_listener->type != RAD_LISTEN_PROXY; +#endif + + if (reply) { + VERIFY_PACKET(reply); + + /* + * Decode the packet if required. + */ + if (request->proxy_listener) { + rcode = request->proxy_listener->proxy_decode(request->proxy_listener, request); + debug_packet(request, reply, true); + + /* + * Pro-actively remove it from the proxy hash. + * This is later than in 2.1.x, but it means that + * the replies are authenticated before being + * removed from the hash. + */ + if ((rcode == 0) && + (request->num_proxied_requests <= request->num_proxied_responses)) { + remove_from_proxy_hash(request); + } + } else { + rad_assert(!request->in_proxy_hash); + } + } else if (request->in_proxy_hash) { + remove_from_proxy_hash(request); + } + + + /* + * Run the request through the virtual server for the + * home server, OR through the virtual server for the + * home server pool. + */ + old_server = request->server; + if (request->home_server && request->home_server->virtual_server) { + request->server = request->home_server->virtual_server; + +#ifdef WITH_COA_TUNNEL + } else if (reverse_coa) { + rad_assert((request->proxy->code == PW_CODE_COA_REQUEST) || + (request->proxy->code == PW_CODE_DISCONNECT_REQUEST)); + rad_assert(request->home_server != NULL); + rad_assert(request->home_server->recv_coa_server != NULL); + request->server = request->home_server->recv_coa_server; +#endif + + } else if (request->home_pool && request->home_pool->virtual_server) { + request->server = request->home_pool->virtual_server; + } + + /* + * Run the request through the given virtual server. + */ + RDEBUG2("server %s {", request->server); + RINDENT(); + rcode = process_post_proxy(post_proxy_type, request); + REXDENT(); + RDEBUG2("}"); + request->server = old_server; + +#ifdef WITH_COA + if (request->proxy && request->packet->code == request->proxy->code) { + /* + * Don't run the next bit if we originated a CoA + * packet, after receiving an Access-Request or + * Accounting-Request. + */ +#endif + + /* + * There may NOT be a proxy reply, as we may be + * running Post-Proxy-Type = Fail. + */ + if (reply) { + fr_pair_add(&request->reply->vps, fr_pair_list_copy(request->reply, reply->vps)); + + /* + * Delete the Proxy-State Attributes from + * the reply. These include Proxy-State + * attributes from us and remote server. + */ + fr_pair_delete_by_num(&request->reply->vps, PW_PROXY_STATE, 0, TAG_ANY); + + } else { + vp = fr_pair_find_by_num(request->config, PW_RESPONSE_PACKET_TYPE, 0, TAG_ANY); + if (vp && (vp->vp_integer != 256)) { + request->proxy_reply = rad_alloc_reply(request, request->proxy); + request->proxy_reply->code = vp->vp_integer; + } + } +#ifdef WITH_COA + } +#endif + switch (rcode) { + default: /* Don't do anything */ + break; + case RLM_MODULE_FAIL: + return 0; + + case RLM_MODULE_HANDLED: + return 0; + } + + return 1; +} + +static void mark_home_server_alive(REQUEST *request, home_server_t *home) +{ + char buffer[128]; + + home->state = HOME_STATE_ALIVE; + home->response_timeouts = 0; + exec_trigger(request, home->cs, "home_server.alive", false); + home->currently_outstanding = 0; + home->num_sent_pings = 0; + home->num_received_pings = 0; + gettimeofday(&home->revive_time, NULL); + + fr_event_delete(el, &home->ev); + + RPROXY("Marking home server %s port %d alive", + inet_ntop(request->proxy->dst_ipaddr.af, + &request->proxy->dst_ipaddr.ipaddr, + buffer, sizeof(buffer)), + request->proxy->dst_port); +} + + +int request_proxy_reply(RADIUS_PACKET *packet) +{ + RADIUS_PACKET **proxy_p; + REQUEST *request; + struct timeval now; + char buffer[128]; + + VERIFY_PACKET(packet); + + PTHREAD_MUTEX_LOCK(&proxy_mutex); + proxy_p = fr_packet_list_find_byreply(proxy_list, packet); + + if (!proxy_p) { + PTHREAD_MUTEX_UNLOCK(&proxy_mutex); + PROXY("No outstanding request was found for %s packet from host %s port %d - ID %u", + fr_packet_codes[packet->code], + inet_ntop(packet->src_ipaddr.af, + &packet->src_ipaddr.ipaddr, + buffer, sizeof(buffer)), + packet->src_port, packet->id); + return 0; + } + + request = fr_packet2myptr(REQUEST, proxy, proxy_p); + + PTHREAD_MUTEX_UNLOCK(&proxy_mutex); + + /* + * No reply, BUT the current packet fails verification: + * ignore it. This does the MD5 calculations in the + * server core, but I guess we can fix that later. + */ + if (!request->proxy_reply) { + if (!request->home_server) { + proxy_reply_too_late(request); + return 0; + } + + if (rad_verify(packet, request->proxy, + request->home_server->secret) != 0) { + DEBUG("Ignoring spoofed proxy reply. Signature is invalid"); + return 0; + } + } + + /* + * The home server sent us a packet which doesn't match + * something we have: ignore it. This is done only to + * catch the case of broken systems. + */ + if (request->proxy_reply && + (memcmp(request->proxy_reply->vector, + packet->vector, + sizeof(request->proxy_reply->vector)) != 0)) { + RDEBUG2("Ignoring conflicting proxy reply"); + return 0; + } + + /* + * This shouldn't happen, but threads and race + * conditions. + */ + if (!request->proxy_listener || !request->proxy_listener->data) { + proxy_reply_too_late(request); + return 0; + } + + gettimeofday(&now, NULL); + + /* + * Status-Server packets don't count as real packets. + */ + if (request->proxy->code != PW_CODE_STATUS_SERVER) { +#ifdef WITH_TCP + listen_socket_t *sock = request->proxy_listener->data; + + sock->last_packet = now.tv_sec; +#endif + request->home_server->last_packet_recv = now.tv_sec; + } + + request->num_proxied_responses++; + + /* + * If we have previously seen a reply, ignore the + * duplicate. + */ + if (request->proxy_reply) { + RDEBUG2("Discarding duplicate reply from host %s port %d - ID: %d", + inet_ntop(packet->src_ipaddr.af, + &packet->src_ipaddr.ipaddr, + buffer, sizeof(buffer)), + packet->src_port, packet->id); + return 0; + } + + /* + * Call the state machine to do something useful with the + * request. + */ + request->proxy_reply = talloc_steal(request, packet); + packet->timestamp = now; + request->priority = RAD_LISTEN_PROXY; + +#ifdef WITH_STATS + /* + * Update the proxy listener stats here, because only one + * thread accesses that at a time. The home_server and + * main proxy_*_stats structures are updated once the + * request is cleaned up. + */ + request->proxy_listener->stats.total_responses++; + + request->home_server->stats.last_packet = packet->timestamp.tv_sec; + request->proxy_listener->stats.last_packet = packet->timestamp.tv_sec; + + switch (request->proxy->code) { + case PW_CODE_ACCESS_REQUEST: + proxy_auth_stats.last_packet = packet->timestamp.tv_sec; + + if (request->proxy_reply->code == PW_CODE_ACCESS_ACCEPT) { + request->proxy_listener->stats.total_access_accepts++; + + } else if (request->proxy_reply->code == PW_CODE_ACCESS_REJECT) { + request->proxy_listener->stats.total_access_rejects++; + + } else if (request->proxy_reply->code == PW_CODE_ACCESS_CHALLENGE) { + request->proxy_listener->stats.total_access_challenges++; + } + break; + +#ifdef WITH_ACCOUNTING + case PW_CODE_ACCOUNTING_REQUEST: + request->proxy_listener->stats.total_responses++; + proxy_acct_stats.last_packet = packet->timestamp.tv_sec; + break; + +#endif + +#ifdef WITH_COA + case PW_CODE_COA_REQUEST: + request->proxy_listener->stats.total_responses++; + proxy_coa_stats.last_packet = packet->timestamp.tv_sec; + break; + + case PW_CODE_DISCONNECT_REQUEST: + request->proxy_listener->stats.total_responses++; + proxy_dsc_stats.last_packet = packet->timestamp.tv_sec; + break; + +#endif + default: + break; + } +#endif + + /* + * If we hadn't been sending the home server packets for + * a while, just mark it alive. Or, if it was zombie, + * it's now responded, and is therefore alive. + */ + if ((request->home_server->state == HOME_STATE_UNKNOWN) || + (request->home_server->state == HOME_STATE_ZOMBIE)) { + mark_home_server_alive(request, request->home_server); + } + + /* + * Tell the request state machine that we have a proxy + * reply. Depending on the function, this should either + * ignore it, or process it. + */ + request->process(request, FR_ACTION_PROXY_REPLY); + + return 1; +} + + +static int setup_post_proxy_fail(REQUEST *request) +{ + DICT_VALUE const *dval = NULL; + VALUE_PAIR *vp; + RADIUS_PACKET *packet; + + VERIFY_REQUEST(request); + + packet = request->proxy ? request->proxy : request->packet; + + if (packet->code == PW_CODE_ACCESS_REQUEST) { + dval = dict_valbyname(PW_POST_PROXY_TYPE, 0, + "Fail-Authentication"); +#ifdef WITH_ACCOUNTING + } else if (packet->code == PW_CODE_ACCOUNTING_REQUEST) { + dval = dict_valbyname(PW_POST_PROXY_TYPE, 0, + "Fail-Accounting"); +#endif + +#ifdef WITH_COA + } else if (packet->code == PW_CODE_COA_REQUEST) { + dval = dict_valbyname(PW_POST_PROXY_TYPE, 0, "Fail-CoA"); + + } else if (packet->code == PW_CODE_DISCONNECT_REQUEST) { + dval = dict_valbyname(PW_POST_PROXY_TYPE, 0, "Fail-Disconnect"); +#endif + } else { + WARN("Unknown packet type in Post-Proxy-Type Fail: ignoring"); + return 0; + } + + if (!dval) dval = dict_valbyname(PW_POST_PROXY_TYPE, 0, "Fail"); + + if (!dval) { + fr_pair_delete_by_num(&request->config, PW_POST_PROXY_TYPE, 0, TAG_ANY); + return 0; + } + + vp = fr_pair_find_by_num(request->config, PW_POST_PROXY_TYPE, 0, TAG_ANY); + if (!vp) vp = radius_pair_create(request, &request->config, + PW_POST_PROXY_TYPE, 0); + vp->vp_integer = dval->value; + + return 1; +} + + +/** Process a request after the proxy has timed out. + * + * Run the packet through Post-Proxy-Type Fail + * + * \dot + * digraph proxy_no_reply { + * proxy_no_reply; + * + * proxy_no_reply -> dup [ label = "DUP", arrowhead = "none" ]; + * proxy_no_reply -> timer [ label = "TIMER < max_request_time" ]; + * proxy_no_reply -> proxy_reply_too_late [ label = "PROXY_REPLY" arrowhead = "none"]; + * proxy_no_reply -> process_proxy_reply [ label = "RUN" ]; + * proxy_no_reply -> done [ label = "TIMER >= timeout" ]; + * } + * \enddot + */ +static void proxy_no_reply(REQUEST *request, int action) +{ + VERIFY_REQUEST(request); + + TRACE_STATE_MACHINE; + CHECK_FOR_STOP; + + switch (action) { + case FR_ACTION_DUP: + request_dup(request); + break; + + case FR_ACTION_TIMER: + (void) request_max_time(request); + break; + + case FR_ACTION_PROXY_REPLY: + proxy_reply_too_late(request); + break; + + case FR_ACTION_RUN: + if (process_proxy_reply(request, NULL)) { + request->handle(request); + } + request_finish(request, action); + break; + + default: + RDEBUG3("%s: Ignoring action %s", __FUNCTION__, action_codes[action]); + break; + } +} + +/** Process the request after receiving a proxy reply. + * + * Throught the post-proxy section, and the through the handler + * function. + * + * \dot + * digraph proxy_running { + * proxy_running; + * + * proxy_running -> dup [ label = "DUP", arrowhead = "none" ]; + * proxy_running -> timer [ label = "TIMER < max_request_time" ]; + * proxy_running -> process_proxy_reply [ label = "RUN" ]; + * proxy_running -> done [ label = "TIMER >= timeout" ]; + * } + * \enddot + */ +static void proxy_running(REQUEST *request, int action) +{ + VERIFY_REQUEST(request); + + TRACE_STATE_MACHINE; + CHECK_FOR_STOP; + + switch (action) { + case FR_ACTION_DUP: + request_dup(request); + break; + + case FR_ACTION_TIMER: + (void) request_max_time(request); + break; + + case FR_ACTION_RUN: + if (process_proxy_reply(request, request->proxy_reply)) { + request->handle(request); + } + request_finish(request, action); + break; + + default: /* duplicate proxy replies are suppressed */ + RDEBUG3("%s: Ignoring action %s", __FUNCTION__, action_codes[action]); + break; + } +} + +/** Determine if a #REQUEST needs to be proxied, and perform pre-proxy operations + * + * Whether a request will be proxied is determined by the attributes present + * in request->config. If any of the following attributes are found, the + * request may be proxied. + * + * The key attributes are: + * - PW_PROXY_TO_REALM - Specifies a realm the request should be proxied to. + * - PW_HOME_SERVER_POOL - Specifies a specific home server pool to proxy to. + * - PW_HOME_SERVER_NAME - Specifies a home server by name + * - PW_PACKET_DST_IP_ADDRESS - Specifies a home server by IPv4 address + * - PW_PACKET_DST_IPV6_ADDRESS - Specifies a home server by IPv5 address + * + * Certain packet types such as #PW_CODE_STATUS_SERVER will never be proxied. + * + * If request should be proxied, will: + * - Add request:Proxy-State + * - Strip the current username value of its realm (depending on config) + * - Create a CHAP-Challenge from the original request vector, if one doesn't already + * exist. + * - Call the pre-process section in the current server, or in the virtual server + * associated with the home server pool we're proxying to. + * + * @todo A lot of this logic is RADIUS specific, and should be moved out into a protocol + * specific function. + * + * @param request The #REQUEST to evaluate for proxying. + * @return 0 if not proxying, 1 if request should be proxied, -1 on error. + */ +static int request_will_proxy(REQUEST *request) +{ + int rcode, pre_proxy_type = 0; + char const *realmname = NULL; + VALUE_PAIR *vp, *strippedname; + home_server_t *home; + REALM *realm = NULL; + home_pool_t *pool = NULL; + char const *old_server; + + VERIFY_REQUEST(request); + + if (!request->root->proxy_requests) { + return 0; + } + if (request->packet->dst_port == 0) return 0; + if (request->packet->code == PW_CODE_STATUS_SERVER) return 0; + if (request->in_proxy_hash) return 0; + + /* + * FIXME: for 3.0, allow this only for rejects? + */ + if (request->reply->code != 0) return 0; + + vp = fr_pair_find_by_num(request->config, PW_PROXY_TO_REALM, 0, TAG_ANY); + if (vp) { + realm = realm_find2(vp->vp_strvalue); + if (!realm) { + REDEBUG2("Cannot proxy to unknown realm %s", + vp->vp_strvalue); + return 0; + } + + realmname = vp->vp_strvalue; + + /* + * Figure out which pool to use. + */ + if (request->packet->code == PW_CODE_ACCESS_REQUEST) { + DEBUG3("Using home pool auth for realm %s", realm->name); + pool = realm->auth_pool; + +#ifdef WITH_ACCOUNTING + } else if (request->packet->code == PW_CODE_ACCOUNTING_REQUEST) { + DEBUG3("Using home pool acct for realm %s", realm->name); + pool = realm->acct_pool; +#endif + +#ifdef WITH_COA + } else if ((request->packet->code == PW_CODE_COA_REQUEST) || + (request->packet->code == PW_CODE_DISCONNECT_REQUEST)) { + DEBUG3("Using home pool coa for realm %s", realm->name); + pool = realm->coa_pool; +#endif + + } else { + return 0; + } + + } else if ((vp = fr_pair_find_by_num(request->config, PW_HOME_SERVER_POOL, 0, TAG_ANY)) != NULL) { + int pool_type; + + DEBUG3("Using Home-Server-Pool %s", vp->vp_strvalue); + + switch (request->packet->code) { + case PW_CODE_ACCESS_REQUEST: + pool_type = HOME_TYPE_AUTH; + break; + +#ifdef WITH_ACCOUNTING + case PW_CODE_ACCOUNTING_REQUEST: + pool_type = HOME_TYPE_ACCT; + break; +#endif + +#ifdef WITH_COA + case PW_CODE_COA_REQUEST: + case PW_CODE_DISCONNECT_REQUEST: + pool_type = HOME_TYPE_COA; + break; +#endif + + default: + return 0; + } + + pool = home_pool_byname(vp->vp_strvalue, pool_type); + + /* + * Send it directly to a home server (i.e. NAS) + */ + } else if (((vp = fr_pair_find_by_num(request->config, PW_PACKET_DST_IP_ADDRESS, 0, TAG_ANY)) != NULL) || + ((vp = fr_pair_find_by_num(request->config, PW_PACKET_DST_IPV6_ADDRESS, 0, TAG_ANY)) != NULL)) { + uint16_t dst_port; + fr_ipaddr_t dst_ipaddr; + + memset(&dst_ipaddr, 0, sizeof(dst_ipaddr)); + + if (vp->da->attr == PW_PACKET_DST_IP_ADDRESS) { + dst_ipaddr.af = AF_INET; + dst_ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr; + dst_ipaddr.prefix = 32; + } else { + dst_ipaddr.af = AF_INET6; + memcpy(&dst_ipaddr.ipaddr.ip6addr, &vp->vp_ipv6addr, sizeof(vp->vp_ipv6addr)); + dst_ipaddr.prefix = 128; + } + + vp = fr_pair_find_by_num(request->config, PW_PACKET_DST_PORT, 0, TAG_ANY); + if (!vp) { + if (request->packet->code == PW_CODE_ACCESS_REQUEST) { + dst_port = PW_AUTH_UDP_PORT; + +#ifdef WITH_ACCOUNTING + } else if (request->packet->code == PW_CODE_ACCOUNTING_REQUEST) { + dst_port = PW_ACCT_UDP_PORT; +#endif + +#ifdef WITH_COA + } else if ((request->packet->code == PW_CODE_COA_REQUEST) || + (request->packet->code == PW_CODE_DISCONNECT_REQUEST)) { + dst_port = PW_COA_UDP_PORT; +#endif + } else { /* shouldn't happen for RADIUS... */ + return 0; + } + + } else { + dst_port = vp->vp_integer; + } + + /* + * Find the home server. + */ + home = home_server_find(&dst_ipaddr, dst_port, IPPROTO_UDP); + if (!home) home = home_server_find(&dst_ipaddr, dst_port, IPPROTO_TCP); + if (!home) { + char buffer[256]; + + RWDEBUG("No such home server %s port %u", + inet_ntop(dst_ipaddr.af, &dst_ipaddr.ipaddr, buffer, sizeof(buffer)), + (unsigned int) dst_port); + return 0; + } + + /* + * The home server is alive (or may be alive). + * Send the packet to the IP. + */ + if (!HOME_SERVER_IS_DEAD(home)) goto do_home; + + /* + * The home server is dead. If you wanted + * fail-over, you should have proxied to a pool. + * Sucks to be you. + */ + + return 0; + + } else if ((vp = fr_pair_find_by_num(request->config, PW_HOME_SERVER_NAME, 0, TAG_ANY)) != NULL) { + int type; + + switch (request->packet->code) { + case PW_CODE_ACCESS_REQUEST: + type = HOME_TYPE_AUTH; + break; + +#ifdef WITH_ACCOUNTING + case PW_CODE_ACCOUNTING_REQUEST: + type = HOME_TYPE_ACCT; + break; +#endif + +#ifdef WITH_COA + case PW_CODE_COA_REQUEST: + case PW_CODE_DISCONNECT_REQUEST: + type = HOME_TYPE_COA; + break; +#endif + + default: + return 0; + } + + /* + * Find the home server by name. + */ + home = home_server_byname(vp->vp_strvalue, type); + if (!home) { + RWDEBUG("No such home server %s", vp->vp_strvalue); + return 0; + } + + /* + * The home server is alive (or may be alive). + * Send the packet to the IP. + */ + if (!HOME_SERVER_IS_DEAD(home)) goto do_home; + + /* + * The home server is dead. If you wanted + * fail-over, you should have proxied to a pool. + * Sucks to be you. + */ + + return 0; + +#ifdef WITH_COA_TUNNEL + } else if (((request->packet->code == PW_CODE_COA_REQUEST) || + (request->packet->code == PW_CODE_DISCONNECT_REQUEST)) && + ((vp = fr_pair_find_by_num(request->config, PW_PROXY_TO_ORIGINATING_REALM, 0, TAG_ANY)) != NULL)) { + + /* + * This function will set request->home_server, + * and also request->proxy_listener. + */ + if (listen_coa_find(request, vp->vp_strvalue) < 0) { + vp_cursor_t cursor; + + (void) fr_cursor_init(&cursor, &request->config); /* already checked it above */ + + while ((vp = fr_cursor_next(&cursor)) != NULL) { + if (listen_coa_find(request, vp->vp_strvalue) == 0) break; + } + + /* + * Not found. + */ + return 0; + } + + /* + * Initialise request->proxy, and copy VPs over. + */ + home_server_update_request(request->home_server, request); + goto add_proxy_state; +#endif + } else { + + return 0; + } + + if (!pool) { + RWDEBUG2("Cancelling proxy as no home pool exists"); + return 0; + } + + if (request->listener->synchronous) { + WARN("Cannot proxy a request which is from a 'synchronous' socket"); + return 0; + } + + request->home_pool = pool; + + home = home_server_ldb(realmname, pool, request); + + if (!home) { + REDEBUG2("Failed to find live home server: Cancelling proxy"); + return -1; + } + +do_home: + home_server_update_request(home, request); + +#ifdef WITH_COA + /* + * Once we've decided to proxy a request, we cannot send + * a CoA packet. So we free up any CoA packet here. + */ + if (request->coa) request_done(request->coa, FR_ACTION_COA_CANCELLED); +#endif + + /* + * Remember that we sent the request to a Realm. + */ + if (realmname && !fr_pair_find_by_num(request->packet->vps, PW_REALM, 0, TAG_ANY)) { + pair_make_request("Realm", realmname, T_OP_EQ); + } + + /* + * Strip the name, if told to. + * + * Doing it here catches the case of proxied tunneled + * requests. + */ + if (realm && (realm->strip_realm == true) && + (strippedname = fr_pair_find_by_num(request->proxy->vps, PW_STRIPPED_USER_NAME, 0, TAG_ANY)) != NULL) { + /* + * If there's a Stripped-User-Name attribute in + * the request, then use THAT as the User-Name + * for the proxied request, instead of the + * original name. + * + * This is done by making a copy of the + * Stripped-User-Name attribute, turning it into + * a User-Name attribute, deleting the + * Stripped-User-Name and User-Name attributes + * from the vps list, and making the new + * User-Name the head of the vps list. + */ + vp = fr_pair_find_by_num(request->proxy->vps, PW_USER_NAME, 0, TAG_ANY); + if (!vp) { + vp_cursor_t cursor; + vp = radius_pair_create(NULL, NULL, + PW_USER_NAME, 0); + rad_assert(vp != NULL); /* handled by above function */ + /* Insert at the START of the list */ + /* FIXME: Can't make assumptions about ordering */ + fr_cursor_init(&cursor, &vp); + fr_cursor_merge(&cursor, request->proxy->vps); + request->proxy->vps = vp; + } + fr_pair_value_strcpy(vp, strippedname->vp_strvalue); + + /* + * Do NOT delete Stripped-User-Name. + */ + } + + /* + * If there is no PW_CHAP_CHALLENGE attribute but + * there is a PW_CHAP_PASSWORD we need to add it + * since we can't use the request authenticator + * anymore - we changed it. + */ + if ((request->packet->code == PW_CODE_ACCESS_REQUEST) && + fr_pair_find_by_num(request->proxy->vps, PW_CHAP_PASSWORD, 0, TAG_ANY) && + fr_pair_find_by_num(request->proxy->vps, PW_CHAP_CHALLENGE, 0, TAG_ANY) == NULL) { + vp = radius_pair_create(request->proxy, &request->proxy->vps, PW_CHAP_CHALLENGE, 0); + fr_pair_value_memcpy(vp, request->packet->vector, sizeof(request->packet->vector)); + } + + /* + * The RFC's say we have to do this, but FreeRADIUS + * doesn't need it. + */ +#ifdef WITH_COA_TUNNEL +add_proxy_state: +#endif + + vp = radius_pair_create(request->proxy, &request->proxy->vps, PW_PROXY_STATE, 0); + fr_pair_value_sprintf(vp, "%u", request->packet->id); + + /* + * Should be done BEFORE inserting into proxy hash, as + * pre-proxy may use this information, or change it. + */ + request->proxy->code = request->packet->code; + + /* + * Call the pre-proxy routines. + */ + vp = fr_pair_find_by_num(request->config, PW_PRE_PROXY_TYPE, 0, TAG_ANY); + if (vp) { + DICT_VALUE const *dval = dict_valbyattr(vp->da->attr, vp->da->vendor, vp->vp_integer); + /* Must be a validation issue */ + rad_assert(dval); + RDEBUG2("Found Pre-Proxy-Type %s", dval->name); + pre_proxy_type = vp->vp_integer; + } + + /* + * Run the request through the virtual server for the + * home server, OR through the virtual server for the + * home server pool. + */ + old_server = request->server; + if (request->home_server && request->home_server->virtual_server) { + request->server = request->home_server->virtual_server; + +#ifdef WITH_COA_TUNNEL + } else if (request->proxy_listener && (request->proxy_listener->type != RAD_LISTEN_PROXY)) { + rad_assert((request->packet->code == PW_CODE_COA_REQUEST) || + (request->packet->code == PW_CODE_DISCONNECT_REQUEST)); + rad_assert(request->home_server != NULL); + rad_assert(request->home_server->recv_coa_server != NULL); + request->server = request->home_server->recv_coa_server; +#endif + + } else { + char buffer[128]; + + RDEBUG2("Starting proxy to home server %s port %d", + inet_ntop(request->proxy->dst_ipaddr.af, + &request->proxy->dst_ipaddr.ipaddr, + buffer, sizeof(buffer)), + request->proxy->dst_port); + + if (request->home_pool && request->home_pool->virtual_server) { + request->server = request->home_pool->virtual_server; + } + } + + /* + * Run the request through the given virtual server. + */ + RDEBUG2("server %s {", request->server); + RINDENT(); + rcode = process_pre_proxy(pre_proxy_type, request); + REXDENT(); + RDEBUG2("}"); + request->server = old_server; + + switch (rcode) { + case RLM_MODULE_FAIL: + case RLM_MODULE_INVALID: + case RLM_MODULE_NOTFOUND: + case RLM_MODULE_USERLOCK: + default: + /* FIXME: debug print failed stuff */ + return -1; + + case RLM_MODULE_REJECT: + case RLM_MODULE_HANDLED: + return 0; + + /* + * Only proxy the packet if the pre-proxy code succeeded. + */ + case RLM_MODULE_NOOP: + case RLM_MODULE_OK: + case RLM_MODULE_UPDATED: + return 1; + } +} + +static int proxy_to_virtual_server(REQUEST *request) +{ + REQUEST *fake; + + if (request->packet->dst_port == 0) { + WARN("Cannot proxy an internal request"); + return 0; + } + + DEBUG("Proxying to virtual server %s", + request->home_server->virtual_server); + + /* + * Packets to virtual servers don't get + * retransmissions sent to them. And the virtual + * server is run ONLY if we have no child + * threads, or we're running in a child thread. + */ + rad_assert(!spawn_flag || !we_are_master()); + + fake = request_alloc_fake(request); + + fake->packet->vps = fr_pair_list_copy(fake->packet, request->packet->vps); + talloc_free(request->proxy); + + fake->server = request->home_server->virtual_server; + fake->handle = request->handle; + fake->process = NULL; /* should never be run for anything */ + + /* + * Run the virtual server. + */ + request_running(fake, FR_ACTION_RUN); + + request->proxy = talloc_steal(request, fake->packet); + fake->packet = NULL; + request->proxy_reply = talloc_steal(request, fake->reply); + fake->reply = NULL; + + talloc_free(fake); + + /* + * No reply code, toss the reply we have, + * and do post-proxy-type Fail. + */ + if (!request->proxy_reply->code) { + TALLOC_FREE(request->proxy_reply); + setup_post_proxy_fail(request); + } + + /* + * Do the proxy reply (if any) + */ + if (process_proxy_reply(request, request->proxy_reply)) { + request->handle(request); + } + + return -1; /* so we call request_finish */ +} + + +static int request_proxy(REQUEST *request) +{ + char buffer[128]; + + VERIFY_REQUEST(request); + + rad_assert(request->parent == NULL); + + if (request->master_state == REQUEST_STOP_PROCESSING) return 0; + +#ifdef WITH_COA + if (request->coa) { + RWDEBUG("Cannot proxy and originate CoA packets at the same time. Cancelling CoA request"); + request_done(request->coa, FR_ACTION_COA_CANCELLED); + } +#endif + + if (!request->home_server) { + RWDEBUG("No home server selected"); + return -1; + } + + /* + * The request may need sending to a virtual server. + * This code is more than a little screwed up. The rest + * of the state machine doesn't handle parent / child + * relationships well. i.e. if the child request takes + * too long, the core will mark the *parent* as "stop + * processing". And the child will continue without + * knowing anything... + * + * So, we have some horrible hacks to get around that. + */ + if (request->home_server->virtual_server) return proxy_to_virtual_server(request); + + /* + * We're actually sending a proxied packet. Do that now. + */ + if (!request->in_proxy_hash && !insert_into_proxy_hash(request)) { + RPROXY("Failed to insert request into the proxy list"); + return -1; + } + +#ifndef WITH_RADIUSV11 + rad_assert(request->proxy->id >= 0); +#endif + + if (rad_debug_lvl) { + struct timeval *response_window; + + response_window = request_response_window(request); + +#ifdef WITH_TLS + if (request->home_server->tls) { +#ifdef WITH_RADIUSV11 + listen_socket_t *sock = request->proxy_listener->data; + + if (sock->radiusv11) { + fr_pair_delete_by_num(&request->proxy->vps, PW_MESSAGE_AUTHENTICATOR, 0, TAG_ANY); + } +#endif + + RDEBUG2("Proxying request to home server %s port %d (TLS) timeout %d.%06d", + inet_ntop(request->proxy->dst_ipaddr.af, + &request->proxy->dst_ipaddr.ipaddr, + buffer, sizeof(buffer)), + request->proxy->dst_port, + (int) response_window->tv_sec, (int) response_window->tv_usec); + } else +#endif + RDEBUG2("Proxying request to home server %s port %d timeout %d.%06d", + inet_ntop(request->proxy->dst_ipaddr.af, + &request->proxy->dst_ipaddr.ipaddr, + buffer, sizeof(buffer)), + request->proxy->dst_port, + (int) response_window->tv_sec, (int) response_window->tv_usec); + + + } + + gettimeofday(&request->proxy->timestamp, NULL); + request->home_server->last_packet_sent = request->proxy->timestamp.tv_sec; + + /* + * Encode the packet before we do anything else. + */ + request->proxy_listener->proxy_encode(request->proxy_listener, request); + debug_packet(request, request->proxy, false); + + /* + * Set the state function, then the state, no child, and + * send the packet. + * + * The order here is different from other state changes + * due to race conditions with replies from the home + * server. + */ + request->process = proxy_wait_for_reply; + request->child_state = REQUEST_PROXIED; + request->component = "<REQUEST_PROXIED>"; + request->module = ""; + NO_CHILD_THREAD; + + /* + * And send the packet. + */ + request->proxy_listener->proxy_send(request->proxy_listener, request); + return 1; +} + +/* + * Proxy the packet as if it was new. + */ +static int request_proxy_anew(REQUEST *request) +{ + home_server_t *home; + + VERIFY_REQUEST(request); + + /* + * Delete the request from the proxy list. + * + * The packet list code takes care of ensuring that IDs + * aren't reused until all 256 IDs have been used. So + * there's a 1/256 chance of re-using the same ID when + * we're sending to the same home server. Which is + * acceptable. + */ + remove_from_proxy_hash(request); + + /* + * Find a live home server for the request. + */ + home = home_server_ldb(NULL, request->home_pool, request); + if (!home) { + REDEBUG2("Failed to find live home server for request"); + post_proxy_fail: + if (setup_post_proxy_fail(request)) { + request_queue_or_run(request, proxy_running); + } else { + gettimeofday(&request->reply->timestamp, NULL); + request_cleanup_delay_init(request); + } + return 0; + } + +#ifdef WITH_ACCOUNTING + /* + * Update the Acct-Delay-Time attribute, since the LAST + * time we tried to retransmit this packet. + */ + if (request->packet->code == PW_CODE_ACCOUNTING_REQUEST) { + VALUE_PAIR *vp; + + vp = fr_pair_find_by_num(request->proxy->vps, PW_ACCT_DELAY_TIME, 0, TAG_ANY); + if (!vp) vp = radius_pair_create(request->proxy, + &request->proxy->vps, + PW_ACCT_DELAY_TIME, 0); + if (vp) { + struct timeval now; + + gettimeofday(&now, NULL); + vp->vp_integer += now.tv_sec - request->proxy->timestamp.tv_sec; + } + } +#endif + + /* + * May have failed over to a "fallback" virtual server. + * If so, run that instead of doing proxying to a real + * server. + */ + if (home->virtual_server) { + request->home_server = home; + TALLOC_FREE(request->proxy); + + (void) proxy_to_virtual_server(request); + return 0; + } + + home_server_update_request(home, request); + + if (!insert_into_proxy_hash(request)) { + RPROXY("Failed to insert retransmission into the proxy list"); + goto post_proxy_fail; + } + + /* + * Free the old packet, to force re-encoding + */ + talloc_free(request->proxy->data); + request->proxy->data = NULL; + request->proxy->data_len = 0; + + if (request_proxy(request) != 1) goto post_proxy_fail; + + return 1; +} + + +/** Ping a home server. + * + */ +static void request_ping(REQUEST *request, int action) +{ + home_server_t *home = request->home_server; + char buffer[128]; + + VERIFY_REQUEST(request); + + TRACE_STATE_MACHINE; + ASSERT_MASTER; + + switch (action) { + case FR_ACTION_TIMER: + ERROR("No response to status check %d ID %u for home server %s port %d", + request->number, + request->proxy->id, + inet_ntop(request->proxy->dst_ipaddr.af, + &request->proxy->dst_ipaddr.ipaddr, + buffer, sizeof(buffer)), + request->proxy->dst_port); + remove_from_proxy_hash(request); + break; + + case FR_ACTION_PROXY_REPLY: + rad_assert(request->in_proxy_hash); + + request->home_server->num_received_pings++; + RPROXY("Received response to status check %d ID %u (%d in current sequence)", + request->number, request->proxy->id, home->num_received_pings); + + /* + * Remove the request from any hashes + */ + fr_event_delete(el, &request->ev); + remove_from_proxy_hash(request); + + /* + * The control socket may have marked the home server as + * alive. OR, it may have suddenly started responding to + * requests again. If so, don't re-do the "make alive" + * work. + */ + if (home->state == HOME_STATE_ALIVE) break; + + /* + * It's dead, and we haven't received enough ping + * responses to mark it "alive". Wait a bit. + * + * If it's zombie, we mark it alive immediately. + */ + if (HOME_SERVER_IS_DEAD(home) && + (home->num_received_pings < home->num_pings_to_alive)) { + return; + } + + /* + * Mark it alive and delete any outstanding + * pings. + */ + mark_home_server_alive(request, home); + break; + + default: + RDEBUG3("%s: Ignoring action %s", __FUNCTION__, action_codes[action]); + break; + } + + rad_assert(!request->in_request_hash); + rad_assert(!request->in_proxy_hash); + rad_assert(request->ev == NULL); + NO_CHILD_THREAD; + request_done(request, FR_ACTION_DONE); +} + +/* + * Add +/- 2s of jitter, as suggested in RFC 3539 + * and in RFC 5080. + */ +static void add_jitter(struct timeval *when) +{ + uint32_t jitter; + + when->tv_sec -= 2; + + jitter = fr_rand(); + jitter ^= (jitter >> 10); + jitter &= ((1 << 22) - 1); /* 22 bits of 1 */ + + /* + * Add in ~ (4 * USEC) of jitter. + */ + tv_add(when, jitter); +} + +/* + * Called from start of zombie period, OR after control socket + * marks the home server dead. + */ +static void ping_home_server(void *ctx) +{ + home_server_t *home = talloc_get_type_abort(ctx, home_server_t); + REQUEST *request; + VALUE_PAIR *vp; + struct timeval when, now; + + if ((home->state == HOME_STATE_ALIVE) || + (home->ev != NULL)) { + return; + } + + gettimeofday(&now, NULL); + ASSERT_MASTER; + + /* + * We've run out of zombie time. Mark it dead. + */ + if (home->state == HOME_STATE_ZOMBIE) { + when = home->zombie_period_start; + when.tv_sec += home->zombie_period; + + if (timercmp(&when, &now, <)) { + DEBUG("PING: Zombie period is over for home server %s", home->log_name); + mark_home_server_dead(home, &now, false); + } + } + + /* + * We're not supposed to be pinging it. Just wake up + * when we're supposed to mark it dead. + */ + if (home->ping_check == HOME_PING_CHECK_NONE) { + if (home->state == HOME_STATE_ZOMBIE) { + home->when = home->zombie_period_start; + home->when.tv_sec += home->zombie_period; + INSERT_EVENT(ping_home_server, home); + } + + /* + * Else mark_home_server_dead will set a timer + * for revive_interval. + */ + return; + } + + + request = request_alloc(NULL); + if (!request) return; + request->number = request_num_counter++; + NO_CHILD_THREAD; + + request->proxy = rad_alloc(request, true); + request->root = &main_config; + rad_assert(request->proxy != NULL); + + if (home->ping_check == HOME_PING_CHECK_STATUS_SERVER) { + request->proxy->code = PW_CODE_STATUS_SERVER; + + fr_pair_make(request->proxy, &request->proxy->vps, + "Message-Authenticator", "0x00", T_OP_SET); + + } else if ((home->type == HOME_TYPE_AUTH) || + (home->type == HOME_TYPE_AUTH_ACCT)) { + request->proxy->code = PW_CODE_ACCESS_REQUEST; + + fr_pair_make(request->proxy, &request->proxy->vps, + "User-Name", home->ping_user_name, T_OP_SET); + fr_pair_make(request->proxy, &request->proxy->vps, + "User-Password", home->ping_user_password, T_OP_SET); + fr_pair_make(request->proxy, &request->proxy->vps, + "Service-Type", "Authenticate-Only", T_OP_SET); + fr_pair_make(request->proxy, &request->proxy->vps, + "Message-Authenticator", "0x00", T_OP_SET); + +#ifdef WITH_ACCOUNTING + } else if (home->type == HOME_TYPE_ACCT) { + request->proxy->code = PW_CODE_ACCOUNTING_REQUEST; + + fr_pair_make(request->proxy, &request->proxy->vps, + "User-Name", home->ping_user_name, T_OP_SET); + fr_pair_make(request->proxy, &request->proxy->vps, + "Acct-Status-Type", "Stop", T_OP_SET); + fr_pair_make(request->proxy, &request->proxy->vps, + "Acct-Session-Id", "00000000", T_OP_SET); + vp = fr_pair_make(request->proxy, &request->proxy->vps, + "Event-Timestamp", "0", T_OP_SET); + vp->vp_date = now.tv_sec; +#endif + + } else { + /* + * Unkown home server type. + */ + talloc_free(request); + return; + } + + vp = fr_pair_make(request->proxy, &request->proxy->vps, + "NAS-Identifier", "", T_OP_SET); + if (vp) { + fr_pair_value_sprintf(vp, "Status Check %u. Are you alive?", + home->num_sent_pings); + } + +#ifdef WITH_TCP + request->proxy->proto = home->proto; +#endif + request->proxy->src_ipaddr = home->src_ipaddr; + request->proxy->dst_ipaddr = home->ipaddr; + request->proxy->dst_port = home->port; + request->home_server = home; +#ifdef DEBUG_STATE_MACHINE + if (rad_debug_lvl) printf("(%u) ********\tSTATE %s C-%s -> C-%s\t********\n", request->number, __FUNCTION__, + child_state_names[request->child_state], + child_state_names[REQUEST_DONE]); + if (rad_debug_lvl) printf("(%u) ********\tNEXT-STATE %s -> %s\n", request->number, __FUNCTION__, "request_ping"); +#endif +#ifdef HAVE_PTHREAD_H + rad_assert(request->child_pid == NO_SUCH_CHILD_PID); +#endif + request->child_state = REQUEST_PROXIED; + request->process = request_ping; + + rad_assert(request->proxy_listener == NULL); + + if (!insert_into_proxy_hash(request)) { + RPROXY("Failed to insert status check %d into proxy list. Discarding it.", + request->number); + + rad_assert(!request->in_request_hash); + rad_assert(!request->in_proxy_hash); + rad_assert(request->ev == NULL); + talloc_free(request); + return; + } + + /* + * Set up the timer callback. + */ + when = now; + when.tv_sec += home->ping_timeout; + + DEBUG("PING: Waiting %u seconds for response to ping", + home->ping_timeout); + + STATE_MACHINE_TIMER(FR_ACTION_TIMER); + home->num_sent_pings++; + + rad_assert(request->proxy_listener != NULL); + request->proxy_listener->proxy_encode(request->proxy_listener, request); + debug_packet(request, request->proxy, false); + request->proxy_listener->proxy_send(request->proxy_listener, + request); + + /* + * Add +/- 2s of jitter, as suggested in RFC 3539 + * and in the Issues and Fixes draft. + */ + home->when = now; + home->when.tv_sec += home->ping_interval; + + add_jitter(&home->when); + + DEBUG("PING: Next status packet in %u seconds", home->ping_interval); + INSERT_EVENT(ping_home_server, home); +} + +static void home_trigger(home_server_t *home, char const *trigger) +{ + REQUEST *my_request; + RADIUS_PACKET *my_packet; + + my_request = talloc_zero(NULL, REQUEST); + my_packet = talloc_zero(my_request, RADIUS_PACKET); + my_request->proxy = my_packet; + my_packet->dst_ipaddr = home->ipaddr; + my_packet->src_ipaddr = home->src_ipaddr; + + exec_trigger(my_request, home->cs, trigger, false); + talloc_free(my_request); +} + +static void mark_home_server_zombie(home_server_t *home, struct timeval *now, struct timeval *response_window) +{ + time_t start; + char buffer[128]; + + ASSERT_MASTER; + + rad_assert((home->state == HOME_STATE_ALIVE) || + (home->state == HOME_STATE_UNKNOWN)); + + /* + * We've received a real packet recently. Don't mark the + * server as zombie until we've received NO packets for a + * while. The "1/4" of zombie period was chosen rather + * arbitrarily. It's a balance between too short, which + * gives quick fail-over and fail-back, or too long, + * where the proxy still sends packets to an unresponsive + * home server. + */ + start = now->tv_sec - ((home->zombie_period + 3) / 4); + if (home->last_packet_recv >= start) { + DEBUG("Received reply from home server %d seconds ago. Might not be zombie.", + (int) (now->tv_sec - home->last_packet_recv)); + return; + } + + home->state = HOME_STATE_ZOMBIE; + home_trigger(home, "home_server.zombie"); + + /* + * Set the home server to "zombie", as of the time + * calculated above. + */ + home->zombie_period_start.tv_sec = start; + home->zombie_period_start.tv_usec = USEC / 2; + + fr_event_delete(el, &home->ev); + + home->num_sent_pings = 0; + home->num_received_pings = 0; + + PROXY( "Marking home server %s port %d as zombie (it has not responded in %d.%06d seconds).", + inet_ntop(home->ipaddr.af, &home->ipaddr.ipaddr, + buffer, sizeof(buffer)), + home->port, (int) response_window->tv_sec, (int) response_window->tv_usec); + + ping_home_server(home); +} + + +void revive_home_server(void *ctx) +{ + home_server_t *home = talloc_get_type_abort(ctx, home_server_t); + char buffer[128]; + + home->state = HOME_STATE_ALIVE; + home->response_timeouts = 0; + home_trigger(home, "home_server.alive"); + home->currently_outstanding = 0; + gettimeofday(&home->revive_time, NULL); + + /* + * Delete any outstanding events. + */ + ASSERT_MASTER; + if (home->ev) fr_event_delete(el, &home->ev); + + PROXY( "Marking home server %s port %d alive again... we have no idea if it really is alive or not.", + inet_ntop(home->ipaddr.af, &home->ipaddr.ipaddr, + buffer, sizeof(buffer)), + home->port); +} + +#ifdef WITH_TLS +static int eol_home_listener(UNUSED void *ctx, void *data) +{ + rad_listen_t *this = talloc_get_type_abort(data, rad_listen_t); + + /* + * The socket isn't blocked, we can still use it. + * + * i.e. the home server is dead for a reason OTHER than + * "all available sockets are blocked". + * + * We can still ping the home server via sockets which + * are writable. + */ + if (!this->blocked) return 0; + + this->status = RAD_LISTEN_STATUS_EOL; + + FD_MUTEX_LOCK(&fd_mutex); + this->next = new_listeners; + new_listeners = this; + FD_MUTEX_UNLOCK(&fd_mutex); + + return 1; /* alway delete from this tree */ +} +#endif + +void mark_home_server_dead(home_server_t *home, struct timeval *when, bool down) +{ + int previous_state = home->state; + char buffer[128]; + + PROXY( "Marking home server %s port %d as dead.", + inet_ntop(home->ipaddr.af, &home->ipaddr.ipaddr, + buffer, sizeof(buffer)), + home->port); + + home->state = HOME_STATE_IS_DEAD; + home_trigger(home, "home_server.dead"); + +#ifdef WITH_TLS + /* + * If the home server is dead, then close all of the sockets associated with it. + * + * Note that the "EOL listener" code expects to _also_ + * delete the listeners. At which point we end up with a + * mutex locked twice, and bad things happen. The + * solution is to move the listeners to the global + * "waiting for update" list, and then notify ourselves + * that there are listeners waiting to be updated. + */ + if (home->listeners) { + ASSERT_MASTER; + + rbtree_walk(home->listeners, RBTREE_DELETE_ORDER, eol_home_listener, NULL); + radius_signal_self(RADIUS_SIGNAL_SELF_NEW_FD); + } +#endif + + /* + * Administratively down - don't do anything to bring it + * up. + */ + if (down) { + home->state = HOME_STATE_ADMIN_DOWN; + return; + } + + /* + * Ping it if configured, AND we can ping it. + */ + if ((home->ping_check != HOME_PING_CHECK_NONE) && + (previous_state != HOME_STATE_CONNECTION_FAIL)) { + /* + * If the control socket marks us dead, start + * pinging. Otherwise, we already started + * pinging when it was marked "zombie". + */ + if (previous_state == HOME_STATE_ALIVE) { + ping_home_server(home); + } else { + DEBUG("PING: Already pinging home server %s", home->log_name); + } + + } else { + /* + * Revive it after a fixed period of time. This + * is very, very, bad. + */ + home->when = *when; + home->when.tv_sec += home->revive_interval; + + DEBUG("PING: Reviving home server %s in %u seconds", home->log_name, home->revive_interval); + ASSERT_MASTER; + INSERT_EVENT(revive_home_server, home); + } +} + +/** Wait for a reply after proxying a request. + * + * Retransmit the proxied packet, or time out and go to + * proxy_no_reply. Mark the home server unresponsive, etc. + * + * If we do receive a reply, we transition to proxy_running. + * + * \dot + * digraph proxy_wait_for_reply { + * proxy_wait_for_reply; + * + * proxy_wait_for_reply -> retransmit_proxied_request [ label = "DUP", arrowhead = "none" ]; + * proxy_wait_for_reply -> proxy_no_reply [ label = "TIMER >= response_window" ]; + * proxy_wait_for_reply -> timer [ label = "TIMER < max_request_time" ]; + * proxy_wait_for_reply -> proxy_running [ label = "PROXY_REPLY" arrowhead = "none"]; + * proxy_wait_for_reply -> done [ label = "TIMER >= max_request_time" ]; + * } + * \enddot + */ +static void proxy_wait_for_reply(REQUEST *request, int action) +{ + struct timeval now, when; + struct timeval *response_window = NULL; + home_server_t *home = request->home_server; + char buffer[128]; + + VERIFY_REQUEST(request); + + TRACE_STATE_MACHINE; + CHECK_FOR_STOP; + + rad_assert(request->packet->code != PW_CODE_STATUS_SERVER); + rad_assert(request->home_server != NULL); + + gettimeofday(&now, NULL); + + switch (action) { + case FR_ACTION_DUP: + /* + * The request was proxied to a virtual server. + * Ignore the retransmit. + */ + if (request->home_server->virtual_server) return; + + /* + * Failed connections get the home server marked + * as dead. + */ + if (home->state == HOME_STATE_CONNECTION_FAIL) { + mark_home_server_dead(home, &now, false); + } + + /* + * We have a reply, ignore the retransmit. + */ + if (request->proxy_reply) return; + + /* + * Use a new connection when the home server is + * dead, or when there's no proxy listener, or + * when the listener is failed or dead. + * + * If the listener is known or frozen, use it for + * retransmits. + */ + if (HOME_SERVER_IS_DEAD(home) || + !request->proxy_listener || + (request->proxy_listener->status >= RAD_LISTEN_STATUS_EOL)) { + request_proxy_anew(request); + return; + } + +#ifdef WITH_TCP + /* + * The home server is still alive, but TCP. We + * rely on TCP to get the request and reply back. + * So there's no need to retransmit. + */ + if (home->proto == IPPROTO_TCP) { + DEBUG2("Suppressing duplicate proxied request (tcp) to home server %s port %d proto TCP - ID: %d", + inet_ntop(request->proxy->dst_ipaddr.af, + &request->proxy->dst_ipaddr.ipaddr, + buffer, sizeof(buffer)), + request->proxy->dst_port, + request->proxy->id); + return; + } +#endif + + /* + * More than one retransmit a second is stupid, + * and should be suppressed by the proxy. + */ + when = request->proxy->timestamp; + when.tv_sec++; + + if (timercmp(&now, &when, <)) { + DEBUG2("Suppressing duplicate proxied request (too fast) to home server %s port %d proto TCP - ID: %d", + inet_ntop(request->proxy->dst_ipaddr.af, + &request->proxy->dst_ipaddr.ipaddr, + buffer, sizeof(buffer)), + request->proxy->dst_port, + request->proxy->id); + return; + } + +#ifdef WITH_ACCOUNTING + /* + * If we update the Acct-Delay-Time, we need to + * get a new ID. + */ + if ((request->packet->code == PW_CODE_ACCOUNTING_REQUEST) && + fr_pair_find_by_num(request->proxy->vps, PW_ACCT_DELAY_TIME, 0, TAG_ANY)) { + request_proxy_anew(request); + return; + } +#endif + + RDEBUG2("Sending duplicate proxied request to home server %s port %d - ID: %d", + inet_ntop(request->proxy->dst_ipaddr.af, + &request->proxy->dst_ipaddr.ipaddr, + buffer, sizeof(buffer)), + request->proxy->dst_port, + request->proxy->id); + request->num_proxied_requests++; + + rad_assert(request->proxy_listener != NULL); + FR_STATS_TYPE_INC(home->stats.total_requests); + home->last_packet_sent = now.tv_sec; + request->proxy->timestamp = now; + debug_packet(request, request->proxy, false); + request->proxy_listener->proxy_send(request->proxy_listener, request); + break; + + case FR_ACTION_TIMER: + /* + * Failed connections get the home server marked + * as dead. + */ + if (home->state == HOME_STATE_CONNECTION_FAIL) { + mark_home_server_dead(home, &now, false); + } + + response_window = request_response_window(request); + +#ifdef WITH_TCP + if (!request->proxy_listener || + (request->proxy_listener->status >= RAD_LISTEN_STATUS_EOL)) { + remove_from_proxy_hash(request); + + when = request->packet->timestamp; + when.tv_sec += request->root->max_request_time; + + if (timercmp(&when, &now, >)) { + RDEBUG("Waiting for client retransmission in order to do a proxy retransmit"); + STATE_MACHINE_TIMER(FR_ACTION_TIMER); + return; + } + } else +#endif + { + /* + * Wake up "response_window" time in the future. + * i.e. when MY packet hasn't received a response. + * + * Note that we DO NOT mark the home server as + * zombie if it doesn't respond to us. It may be + * responding to other (better looking) packets. + */ + when = request->proxy->timestamp; + timeradd(&when, response_window, &when); + + /* + * Not at the response window. Set the timer for + * that. + */ + if (timercmp(&when, &now, >)) { + struct timeval diff; + timersub(&when, &now, &diff); + + RDEBUG("Expecting proxy response no later than %d.%06d seconds from now", + (int) diff.tv_sec, (int) diff.tv_usec); + STATE_MACHINE_TIMER(FR_ACTION_TIMER); + return; + } + } + + RDEBUG("No proxy response, giving up on request and marking it done"); + + /* + * If we haven't received any packets for + * "response_window", then mark the home server + * as zombie. + * + * This check should really be part of a home + * server state machine. + */ + if ((home->state == HOME_STATE_ALIVE) || + (home->state == HOME_STATE_UNKNOWN)) { + home->response_timeouts++; + if (home->response_timeouts >= home->max_response_timeouts) + mark_home_server_zombie(home, &now, response_window); + } + + FR_STATS_TYPE_INC(home->stats.total_timeouts); + if (home->type == HOME_TYPE_AUTH) { + if (request->proxy_listener) FR_STATS_TYPE_INC(request->proxy_listener->stats.total_timeouts); + FR_STATS_TYPE_INC(proxy_auth_stats.total_timeouts); + } +#ifdef WITH_ACCT + else if (home->type == HOME_TYPE_ACCT) { + if (request->proxy_listener) FR_STATS_TYPE_INC(request->proxy_listener->stats.total_timeouts); + FR_STATS_TYPE_INC(proxy_acct_stats.total_timeouts); + } +#endif +#ifdef WITH_COA + else if (home->type == HOME_TYPE_COA) { + if (request->proxy_listener) FR_STATS_TYPE_INC(request->proxy_listener->stats.total_timeouts); + + if (request->packet->code == PW_CODE_COA_REQUEST) { + FR_STATS_TYPE_INC(proxy_coa_stats.total_timeouts); + } else { + FR_STATS_TYPE_INC(proxy_dsc_stats.total_timeouts); + } + } +#endif + + /* + * There was no response within the window. Stop + * the request. If the client retransmitted, it + * may have failed over to another home server. + * But that one may be dead, too. + * + * The extra verbose message if we have a username, + * is extremely useful if the proxy is part of a chain + * and the final home server, is not the one we're + * proxying to. + */ + if (request->username) { + RERROR("Failing proxied request for user \"%s\", due to lack of any response from home " + "server %s port %d", + request->username->vp_strvalue, + inet_ntop(request->proxy->dst_ipaddr.af, + &request->proxy->dst_ipaddr.ipaddr, + buffer, sizeof(buffer)), + request->proxy->dst_port); + } else { + RERROR("Failing proxied request, due to lack of any response from home server %s port %d", + inet_ntop(request->proxy->dst_ipaddr.af, + &request->proxy->dst_ipaddr.ipaddr, + buffer, sizeof(buffer)), + request->proxy->dst_port); + } + + if (setup_post_proxy_fail(request)) { + request_queue_or_run(request, proxy_no_reply); + } else { + gettimeofday(&request->reply->timestamp, NULL); + request_cleanup_delay_init(request); + } + break; + + /* + * We received a new reply. Go process it. + */ + case FR_ACTION_PROXY_REPLY: + request_queue_or_run(request, proxy_running); + break; + + default: + RDEBUG3("%s: Ignoring action %s", __FUNCTION__, action_codes[action]); + break; + } +} +#endif /* WITH_PROXY */ + + +/*********************************************************************** + * + * CoA code + * + ***********************************************************************/ +#ifdef WITH_COA +/* + * See if we need to originate a CoA request. + */ +static void request_coa_originate(REQUEST *request) +{ + int rcode, pre_proxy_type = 0; + VALUE_PAIR *vp; + REQUEST *coa; + fr_ipaddr_t ipaddr; + char const *old_server; + char buffer[256]; + + VERIFY_REQUEST(request); + + rad_assert(request->coa != NULL); + rad_assert(request->proxy == NULL); + rad_assert(!request->in_proxy_hash); + rad_assert(request->proxy_reply == NULL); + + /* + * Check whether we want to originate one, or cancel one. + */ + vp = fr_pair_find_by_num(request->config, PW_SEND_COA_REQUEST, 0, TAG_ANY); + if (!vp) { + vp = fr_pair_find_by_num(request->coa->proxy->vps, PW_SEND_COA_REQUEST, 0, TAG_ANY); + } + + if (vp) { + if (vp->vp_integer == 0) { + fail: + TALLOC_FREE(request->coa); + return; + } + } + + if (!main_config.proxy_requests) { + RWDEBUG("Cannot originate CoA packets unless 'proxy_requests = yes'"); + TALLOC_FREE(request->coa); + return; + } + + coa = request->coa; + coa->listener = NULL; /* copied here by request_alloc_fake(), but not needed */ + +#ifdef WITH_COA_TUNNEL + /* + * Proxy-To-Originating-Realm is preferred to any other + * method of originating CoA requests. + */ + vp = fr_pair_find_by_num(coa->proxy->vps, PW_PROXY_TO_ORIGINATING_REALM, 0, TAG_ANY); + if (vp) { + /* + * This function will set request->home_server, + * and also request->proxy_listener. + */ + if (listen_coa_find(coa, vp->vp_strvalue) < 0) { + RWDEBUG("Unknown Originating realm '%s'", vp->vp_strvalue); + return; + } + + goto set_packet_type; + } +#endif + + /* + * src_ipaddr will be set up in proxy_encode. + */ + memset(&ipaddr, 0, sizeof(ipaddr)); + vp = fr_pair_find_by_num(coa->proxy->vps, PW_PACKET_DST_IP_ADDRESS, 0, TAG_ANY); + if (vp) { + ipaddr.af = AF_INET; + ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr; + ipaddr.prefix = 32; + } else if ((vp = fr_pair_find_by_num(coa->proxy->vps, PW_PACKET_DST_IPV6_ADDRESS, 0, TAG_ANY)) != NULL) { + ipaddr.af = AF_INET6; + ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr; + ipaddr.prefix = 128; + } else if ((vp = fr_pair_find_by_num(coa->proxy->vps, PW_HOME_SERVER_POOL, 0, TAG_ANY)) != NULL) { + coa->home_pool = home_pool_byname(vp->vp_strvalue, + HOME_TYPE_COA); + if (!coa->home_pool) { + RWDEBUG2("No such home_server_pool %s", + vp->vp_strvalue); + goto fail; + } + + /* + * Prefer the pool to one server + */ + } else if (request->client->coa_home_pool) { + coa->home_pool = request->client->coa_home_pool; + + } else if (request->client->coa_home_server) { + coa->home_server = request->client->coa_home_server; + + } else { + /* + * If all else fails, send it to the client that + * originated this request. + */ + memcpy(&ipaddr, &request->packet->src_ipaddr, sizeof(ipaddr)); + } + + /* + * Use the pool, if it exists. + */ + if (coa->home_pool) { + coa->home_server = home_server_ldb(NULL, coa->home_pool, coa); + if (!coa->home_server) { + RWDEBUG("No live home server for home_server_pool %s", coa->home_pool->name); + goto fail; + } + home_server_update_request(coa->home_server, coa); + + } else if (!coa->home_server) { + uint16_t port = PW_COA_UDP_PORT; + + vp = fr_pair_find_by_num(coa->proxy->vps, PW_PACKET_DST_PORT, 0, TAG_ANY); + if (vp) port = vp->vp_integer; + + coa->home_server = home_server_find(&ipaddr, port, IPPROTO_UDP); + if (!coa->home_server) { + RWDEBUG2("Unknown destination %s:%d for CoA request.", + inet_ntop(ipaddr.af, &ipaddr.ipaddr, + buffer, sizeof(buffer)), port); + goto fail; + } + } + +#ifdef WITH_COA_TUNNEL +set_packet_type: +#endif + vp = fr_pair_find_by_num(coa->proxy->vps, PW_PACKET_TYPE, 0, TAG_ANY); + if (vp) { + switch (vp->vp_integer) { + case PW_CODE_COA_REQUEST: + case PW_CODE_DISCONNECT_REQUEST: + coa->proxy->code = vp->vp_integer; + break; + + default: + DEBUG("Cannot set CoA Packet-Type to code %d", + vp->vp_integer); + goto fail; + } + } + + if (!coa->proxy->code) coa->proxy->code = PW_CODE_COA_REQUEST; + + /* + * The rest of the server code assumes that + * request->packet && request->reply exist. Copy them + * from the original request. + */ + rad_assert(coa->packet != NULL); + rad_assert(coa->packet->vps == NULL); + + coa->packet = rad_copy_packet(coa, request->packet); + coa->reply = rad_copy_packet(coa, request->reply); + + coa->config = fr_pair_list_copy(coa, request->config); + coa->num_coa_requests = 0; + coa->number = request->number; /* it's associated with the same request */ + + /* + * Call the pre-proxy routines. + */ + vp = fr_pair_find_by_num(request->config, PW_PRE_PROXY_TYPE, 0, TAG_ANY); + if (vp) { + DICT_VALUE const *dval = dict_valbyattr(vp->da->attr, vp->da->vendor, vp->vp_integer); + /* Must be a validation issue */ + rad_assert(dval); + RDEBUG2("Found Pre-Proxy-Type %s", dval->name); + pre_proxy_type = vp->vp_integer; + } + + /* + * Run the request through the virtual server for the + * home server, OR through the virtual server for the + * home server pool. + */ + old_server = request->server; + if (coa->home_server && coa->home_server->virtual_server) { + coa->server = coa->home_server->virtual_server; + +#ifdef WITH_COA_TUNNEL + } else if (coa->proxy_listener && (coa->proxy_listener->type != RAD_LISTEN_PROXY)) { + rad_assert((coa->proxy->code == PW_CODE_COA_REQUEST) || + (coa->proxy->code == PW_CODE_DISCONNECT_REQUEST)); + rad_assert(coa->home_server != NULL); + rad_assert(coa->home_server->recv_coa_server != NULL); + coa->server = coa->home_server->recv_coa_server; +#endif + + } else if (coa->home_pool && coa->home_pool->virtual_server) { + coa->server = coa->home_pool->virtual_server; + } + + RDEBUG2("server %s {", coa->server); + RINDENT(); + rcode = process_pre_proxy(pre_proxy_type, coa); + REXDENT(); + RDEBUG2("}"); + coa->server = old_server; + + switch (rcode) { + default: + goto fail; + + /* + * Only send the CoA packet if the pre-proxy code succeeded. + */ + case RLM_MODULE_NOOP: + case RLM_MODULE_OK: + case RLM_MODULE_UPDATED: + break; + } + + /* + * Source IP / port is set when the proxy socket + * is chosen. + */ + coa->proxy->dst_ipaddr = coa->home_server->ipaddr; + coa->proxy->dst_port = coa->home_server->port; + + if (!insert_into_proxy_hash(coa)) { + radlog_request(L_PROXY, 0, coa, "Failed to insert CoA request into proxy list"); + goto fail; + } + + /* + * We CANNOT divorce the CoA request from the parent + * request. This function is running in a child thread, + * and we need access to the main event loop in order to + * to add the timers for the CoA packet. + * + * Instead, we wait for the timer on the parent request + * to fire. + */ + gettimeofday(&coa->proxy->timestamp, NULL); + coa->packet->timestamp = coa->proxy->timestamp; /* for max_request_time */ + coa->home_server->last_packet_sent = coa->proxy->timestamp.tv_sec; + coa->delay = 0; /* need to calculate a new delay */ + + /* + * If requested, put a State attribute into the packet, + * and cache the VPS. + */ + fr_state_put_vps(coa, NULL, coa->packet); + + /* + * Encode the packet before we do anything else. + */ + coa->proxy_listener->proxy_encode(coa->proxy_listener, coa); + debug_packet(coa, coa->proxy, false); + +#ifdef DEBUG_STATE_MACHINE + if (rad_debug_lvl) printf("(%u) ********\tSTATE %s C-%s -> C-%s\t********\n", request->number, __FUNCTION__, + child_state_names[request->child_state], + child_state_names[REQUEST_PROXIED]); +#endif + + /* + * Set the state function, then the state, no child, and + * send the packet. + */ + coa->process = coa_wait_for_reply; + coa->child_state = REQUEST_PROXIED; + +#ifdef HAVE_PTHREAD_H + coa->child_pid = NO_SUCH_CHILD_PID; +#endif + + if (we_are_master()) coa_separate(request->coa, true); + + /* + * And send the packet. + */ + coa->proxy_listener->proxy_send(coa->proxy_listener, coa); +} + + +static void coa_retransmit(REQUEST *request) +{ + uint32_t delay, frac; + struct timeval now, when, mrd; + char buffer[128]; + + VERIFY_REQUEST(request); + + /* + * Don't do fail-over. This is a 3.1 feature. + */ + if (!request->home_server || + HOME_SERVER_IS_DEAD(request->home_server) || + request->proxy_reply || + !request->proxy_listener || + (request->proxy_listener->status >= RAD_LISTEN_STATUS_EOL)) { + request_done(request, FR_ACTION_COA_CANCELLED); + return; + } + + fr_event_now(el, &now); + + /* + * Home server has gone away. The request is done. + */ + if (!request->home_server) { + RDEBUG("No home server for CoA packet. Failing it."); + goto fail; + } + + if (request->delay == 0) { + /* + * Implement re-transmit algorithm as per RFC 5080 + * Section 2.2.1. + * + * We want IRT + RAND*IRT + * or 0.9 IRT + rand(0,.2) IRT + * + * 2^20 ~ USEC, and we want 2. + * rand(0,0.2) USEC ~ (rand(0,2^21) / 10) + */ + delay = (fr_rand() & ((1 << 22) - 1)) / 10; + request->delay = delay * request->home_server->coa_irt; + delay = request->home_server->coa_irt * USEC; + delay -= delay / 10; + delay += request->delay; + request->delay = delay; + + when = request->proxy->timestamp; + tv_add(&when, delay); + + if (timercmp(&when, &now, >)) { + STATE_MACHINE_TIMER(FR_ACTION_TIMER); + return; + } + } + + /* + * Retransmit CoA request. + */ + + /* + * Cap count at MRC, if it is non-zero. + */ + if (request->home_server->coa_mrc && + (request->num_coa_requests >= request->home_server->coa_mrc)) { + RERROR("Failing request - originate-coa ID %u, due to lack of any response from coa server %s port %d", + request->proxy->id, + inet_ntop(request->proxy->dst_ipaddr.af, + &request->proxy->dst_ipaddr.ipaddr, + buffer, sizeof(buffer)), + request->proxy->dst_port); + + fail: + if (setup_post_proxy_fail(request)) { + request_queue_or_run(request, coa_no_reply); + } else { + request_done(request, FR_ACTION_DONE); + } + return; + } + + /* + * RFC 5080 Section 2.2.1 + * + * RT = 2*RTprev + RAND*RTprev + * = 1.9 * RTprev + rand(0,.2) * RTprev + * = 1.9 * RTprev + rand(0,1) * (RTprev / 5) + */ + delay = fr_rand(); + delay ^= (delay >> 16); + delay &= 0xffff; + frac = request->delay / 5; + delay = ((frac >> 16) * delay) + (((frac & 0xffff) * delay) >> 16); + + delay += (2 * request->delay) - (request->delay / 10); + + /* + * Cap delay at MRT, if MRT is non-zero. + */ + if (request->home_server->coa_mrt && + (delay > (request->home_server->coa_mrt * USEC))) { + int mrt_usec = request->home_server->coa_mrt * USEC; + + /* + * delay = MRT + RAND * MRT + * = 0.9 MRT + rand(0,.2) * MRT + */ + delay = fr_rand(); + delay ^= (delay >> 15); + delay &= 0x1ffff; + delay = ((mrt_usec >> 16) * delay) + (((mrt_usec & 0xffff) * delay) >> 16); + delay += mrt_usec - (mrt_usec / 10); + } + + request->delay = delay; + when = now; + tv_add(&when, request->delay); + mrd = request->proxy->timestamp; + mrd.tv_sec += request->home_server->coa_mrd; + + /* + * Cap duration at MRD. + */ + if (timercmp(&mrd, &when, <)) { + when = mrd; + } + STATE_MACHINE_TIMER(FR_ACTION_TIMER); + + request->num_coa_requests++; /* is NOT reset by code 3 lines above! */ + + FR_STATS_TYPE_INC(request->home_server->stats.total_requests); + + RDEBUG2("Sending duplicate CoA request to home server %s port %d - ID: %d", + inet_ntop(request->proxy->dst_ipaddr.af, + &request->proxy->dst_ipaddr.ipaddr, + buffer, sizeof(buffer)), + request->proxy->dst_port, + request->proxy->id); + + request->proxy_listener->proxy_send(request->proxy_listener, + request); +} + + +/* + * Enforce maximum time for CoA packets + */ +static bool coa_max_time(REQUEST *request) +{ + struct timeval now, when; + rad_assert(request->magic == REQUEST_MAGIC); +#ifdef DEBUG_STATE_MACHINE + int action = FR_ACTION_TIMER; +#endif + int mrd; + + VERIFY_REQUEST(request); + + TRACE_STATE_MACHINE; + ASSERT_MASTER; + + /* + * The child thread has acknowledged it's done. + * Transition to the DONE state. + * + * If the request was marked STOP, then the "check for + * stop" macro already took care of it. + */ + if (request->child_state == REQUEST_DONE) { + done: + request->max_time = true; + request_done(request, FR_ACTION_MAX_TIME); + return true; + } + + /* + * The request is still running. Enforce max_request_time. + * + * Note that the *proxy* timestamp is the one we use, as + * that's when the CoA packet was sent. + * + * Note also that if there's an error, the home server + * may not exist. + */ + fr_event_now(el, &now); + when = request->proxy->timestamp; + if (request->home_server && (request->process != coa_running)) { + mrd = request->home_server->coa_mrd; + } else { + mrd = request->root->max_request_time; + } + when.tv_sec += mrd; + + /* + * Taking too long: tell it to die. + */ + if (timercmp(&now, &when, >=)) { + char buffer[256]; + + if (request->process != coa_running) { + RERROR("Failing request - originate-coa ID %u, due to lack of any response from coa server %s port %d within %d seconds", + request->proxy->id, + inet_ntop(request->proxy->dst_ipaddr.af, + &request->proxy->dst_ipaddr.ipaddr, + buffer, sizeof(buffer)), + request->proxy->dst_port, + mrd); + request_done(request, FR_ACTION_DONE); + return true; + } + +#ifdef HAVE_PTHREAD_H + /* + * If there's a child thread processing it, + * complain. + */ + if (spawn_flag && + (pthread_equal(request->child_pid, NO_SUCH_CHILD_PID) == 0)) { + RERROR("Unresponsive child for originate-coa, in component %s module %s", + request->component ? request->component : "<core>", + request->module ? request->module : "<core>"); + exec_trigger(request, NULL, "server.thread.unresponsive", true); + } else +#endif + { + RERROR("originate-coa hit max_request_time. Cancelling it."); + } + + /* + * Tell the request that it's done. + */ + goto done; + } + + /* + * Let coa_retransmit() handle the retransmission timers. + */ + if (request->process != coa_running) return false; + + /* + * Sleep for some more. We HOPE that the child will + * become responsive at some point in the future. We do + * this by adding 50% to the current timer. + */ + when = now; + tv_add(&when, request->delay); + request->delay += request->delay >> 1; + STATE_MACHINE_TIMER(FR_ACTION_TIMER); + return false; +} + + +/** Wait for a reply after originating a CoA a request. + * + * Retransmit the proxied packet, or time out and go to + * coa_no_reply. Mark the home server unresponsive, etc. + * + * If we do receive a reply, we transition to coa_running. + * + * \dot + * digraph coa_wait_for_reply { + * coa_wait_for_reply; + * + * coa_wait_for_reply -> coa_no_reply [ label = "TIMER >= response_window" ]; + * coa_wait_for_reply -> timer [ label = "TIMER < max_request_time" ]; + * coa_wait_for_reply -> coa_running [ label = "PROXY_REPLY" arrowhead = "none"]; + * coa_wait_for_reply -> done [ label = "TIMER >= max_request_time" ]; + * } + * \enddot + */ +static void coa_wait_for_reply(REQUEST *request, int action) +{ + VERIFY_REQUEST(request); + + TRACE_STATE_MACHINE; + ASSERT_MASTER; + CHECK_FOR_STOP; + + if (request->parent) coa_separate(request, false); + + switch (action) { + case FR_ACTION_TIMER: + if (coa_max_time(request)) break; + + coa_retransmit(request); + break; + + case FR_ACTION_PROXY_REPLY: + /* + * Reset the initial delay for checking if we + * should still run. + */ + request->delay = (int)request->root->init_delay.tv_sec * USEC + + (int)request->root->init_delay.tv_usec; + + request_queue_or_run(request, coa_running); + break; + + default: + RDEBUG3("%s: Ignoring action %s", __FUNCTION__, action_codes[action]); + break; + } +} + +static void coa_separate(REQUEST *request, bool retransmit) +{ + VERIFY_REQUEST(request); +#ifdef DEBUG_STATE_MACHINE + int action = FR_ACTION_TIMER; +#endif + + TRACE_STATE_MACHINE; + ASSERT_MASTER; + + rad_assert(request->parent != NULL); + rad_assert(request->parent->coa == request); + rad_assert(request->ev == NULL); + rad_assert(!request->in_request_hash); + rad_assert(request->coa == NULL); + + (void) talloc_steal(NULL, request); + request->parent->coa = NULL; + request->parent = NULL; + + if (retransmit && (request->delay == 0) && !request->proxy_reply) { + coa_retransmit(request); + } +} + + +/** Process a request after the CoA has timed out. + * + * Run the packet through Post-Proxy-Type Fail + * + * \dot + * digraph coa_no_reply { + * coa_no_reply; + * + * coa_no_reply -> dup [ label = "DUP", arrowhead = "none" ]; + * coa_no_reply -> timer [ label = "TIMER < max_request_time" ]; + * coa_no_reply -> coa_reply_too_late [ label = "PROXY_REPLY" arrowhead = "none"]; + * coa_no_reply -> process_proxy_reply [ label = "RUN" ]; + * coa_no_reply -> done [ label = "TIMER >= timeout" ]; + * } + * \enddot + */ +static void coa_no_reply(REQUEST *request, int action) +{ + char buffer[128]; + + VERIFY_REQUEST(request); + + TRACE_STATE_MACHINE; + CHECK_FOR_STOP; + + switch (action) { + case FR_ACTION_TIMER: + (void) coa_max_time(request); + break; + + case FR_ACTION_PROXY_REPLY: /* too late! */ + RDEBUG2("Reply from CoA server %s port %d - ID: %d arrived too late.", + inet_ntop(request->proxy->src_ipaddr.af, + &request->proxy->src_ipaddr.ipaddr, + buffer, sizeof(buffer)), + request->proxy->dst_port, request->proxy->id); + break; + + case FR_ACTION_RUN: + if (process_proxy_reply(request, NULL)) { + request->handle(request); + } + request_done(request, FR_ACTION_DONE); + break; + + default: + RDEBUG3("%s: Ignoring action %s", __FUNCTION__, action_codes[action]); + break; + } +} + + +/** Process the request after receiving a coa reply. + * + * Throught the post-proxy section, and the through the handler + * function. + * + * \dot + * digraph coa_running { + * coa_running; + * + * coa_running -> timer [ label = "TIMER < max_request_time" ]; + * coa_running -> process_proxy_reply [ label = "RUN" ]; + * coa_running -> done [ label = "TIMER >= timeout" ]; + * } + * \enddot + */ +static void coa_running(REQUEST *request, int action) +{ + VERIFY_REQUEST(request); + + TRACE_STATE_MACHINE; + CHECK_FOR_STOP; + + switch (action) { + case FR_ACTION_TIMER: + (void) coa_max_time(request); + break; + + case FR_ACTION_RUN: + if (process_proxy_reply(request, request->proxy_reply)) { + request->handle(request); + } + request_done(request, FR_ACTION_DONE); + break; + + default: + RDEBUG3("%s: Ignoring action %s", __FUNCTION__, action_codes[action]); + break; + } +} +#endif /* WITH_COA */ + +/*********************************************************************** + * + * End of the State machine. Start of additional helper code. + * + ***********************************************************************/ + +/*********************************************************************** + * + * Event handlers. + * + ***********************************************************************/ +static void event_socket_handler(fr_event_list_t *xel, UNUSED int fd, void *ctx) +{ + rad_listen_t *listener = talloc_get_type_abort(ctx, rad_listen_t); + + rad_assert(xel == el); + + if ((listener->fd < 0) +#ifdef WITH_DETAIL +#ifndef WITH_DETAIL_THREAD + && (listener->type != RAD_LISTEN_DETAIL) +#endif +#endif + ) { + char buffer[256]; + + listener->print(listener, buffer, sizeof(buffer)); + ERROR("FATAL: Asked to read from closed socket: %s", + buffer); + + rad_panic("Socket was closed on us!"); + fr_exit_now(1); + } + + listener->recv(listener); +} + +#ifdef WITH_DETAIL +#ifdef WITH_DETAIL_THREAD +#else +/* + * This function is called periodically to see if this detail + * file is available for reading. + */ +static void event_poll_detail(void *ctx) +{ + int delay; + rad_listen_t *this = talloc_get_type_abort(ctx, rad_listen_t); + struct timeval when, now; + listen_detail_t *detail = this->data; + + rad_assert(this->type == RAD_LISTEN_DETAIL); + + redo: + event_socket_handler(el, this->fd, this); + + fr_event_now(el, &now); + when = now; + + /* + * Backdoor API to get the delay until the next poll + * time. + */ + delay = this->encode(this, NULL); + if (delay == 0) goto redo; + + tv_add(&when, delay); + + ASSERT_MASTER; + if (!fr_event_insert(el, event_poll_detail, this, + &when, &detail->ev)) { + ERROR("Failed creating handler"); + fr_exit(1); + } +} +#endif /* WITH_DETAIL_THREAD */ +#endif /* WITH_DETAIL */ + +static void event_status(struct timeval *wake) +{ + if (rad_debug_lvl == 0) { + if (just_started) { + INFO("Ready to process requests"); + just_started = false; + } + return; + } + + if (!wake) { + INFO("Ready to process requests"); + + } else if ((wake->tv_sec != 0) || + (wake->tv_usec >= 100000)) { + DEBUG("Waking up in %d.%01u seconds.", + (int) wake->tv_sec, (unsigned int) wake->tv_usec / 100000); + } + + + /* + * FIXME: Put this somewhere else, where it isn't called + * all of the time... + */ + + if (!spawn_flag) { + int argval; + + /* + * If there are no child threads, then there may + * be child processes. In that case, wait for + * their exit status, and throw that exit status + * away. This helps get rid of zxombie children. + */ + while (waitpid(-1, &argval, WNOHANG) > 0) { + /* do nothing */ + } + } +} + +static void listener_free_cb(void *ctx) +{ + rad_listen_t *this = talloc_get_type_abort(ctx, rad_listen_t); + listen_socket_t *sock = this->data; + char buffer[1024]; + + if (this->count > 0) { + struct timeval when; + + fr_event_now(el, &when); + when.tv_sec += 3; + + ASSERT_MASTER; + if (!fr_event_insert(el, listener_free_cb, this, &when, + &(sock->ev))) { + rad_panic("Failed to insert event"); + } + + return; + } + + /* + * It's all free, close the socket. + */ + + this->print(this, buffer, sizeof(buffer)); + DEBUG("... cleaning up socket %s", buffer); + rad_assert(this->next == NULL); +#ifdef WITH_TCP + fr_event_delete(el, &sock->ev); +#endif + talloc_free(this); +} + +#ifdef WITH_TCP +#ifdef WITH_PROXY +static int proxy_eol_cb(void *ctx, void *data) +{ + struct timeval when; + REQUEST *request = fr_packet2myptr(REQUEST, proxy, data); + + if (request->proxy_listener != ctx) return 0; + + /* + * We don't care if it's being processed in a child thread. + */ + +#ifdef WITH_ACCOUNTING + /* + * Accounting packets should be deleted immediately. + * They will never be retransmitted by the client. + */ + if (request->proxy->code == PW_CODE_ACCOUNTING_REQUEST) { + RDEBUG("Stopping request due to failed connection to home server"); + request->master_state = REQUEST_STOP_PROCESSING; + } +#endif + + /* + * Reset the timer to be now, so that the request is + * quickly updated. But spread the requests randomly + * over the next second, so that we don't overload the + * server. + */ + fr_event_now(el, &when); + tv_add(&when, fr_rand() % USEC); + STATE_MACHINE_TIMER(FR_ACTION_TIMER); + + /* + * Don't delete it from the list. + */ + return 0; +} +#endif /* WITH_PROXY */ +#endif /* WITH_TCP */ + +static void event_new_fd(rad_listen_t *this) +{ + char buffer[1024]; + listen_socket_t *sock = NULL; + + ASSERT_MASTER; + + if (this->status == RAD_LISTEN_STATUS_KNOWN) return; + + this->print(this, buffer, sizeof(buffer)); + + if (this->type != RAD_LISTEN_DETAIL) { + sock = this->data; + rad_assert(sock != NULL); + } + + if (this->status == RAD_LISTEN_STATUS_INIT) { + if (just_started) { + DEBUG("Listening on %s", buffer); + +#ifdef WITH_PROXY + } else if (this->type == RAD_LISTEN_PROXY) { + home_server_t *home = sock->home; + + if (home && home->limit.max_connections) { + INFO(" ... adding new socket %s (%u of %u)", buffer, + home->limit.num_connections, home->limit.max_connections); + } else { + INFO(" ... adding new socket %s", buffer); + } +#endif + } else { + INFO(" ... adding new socket %s", buffer); + } + + switch (this->type) { +#ifdef WITH_DETAIL + /* + * Detail files are always known, and aren't + * put into the socket event loop. + */ + case RAD_LISTEN_DETAIL: + this->status = RAD_LISTEN_STATUS_KNOWN; + +#ifndef WITH_DETAIL_THREAD + /* + * Set up the first poll interval. + */ + event_poll_detail(this); + return; +#else + break; /* add the FD to the list */ +#endif +#endif /* WITH_DETAIL */ + +#ifdef WITH_PROXY + /* + * Add it to the list of sockets we can use. + * Server sockets (i.e. auth/acct) are never + * added to the packet list. + */ + case RAD_LISTEN_PROXY: +#ifdef WITH_TCP + rad_assert(sock != NULL); + rad_assert((sock->proto == IPPROTO_UDP) || (sock->home != NULL)); + + /* + * Add timers to outgoing child sockets, if necessary. + */ + if (sock->proto == IPPROTO_TCP && sock->opened && + (sock->home->limit.lifetime || sock->home->limit.idle_timeout)) { + struct timeval when; + + when.tv_sec = sock->opened + 1; + when.tv_usec = 0; + + ASSERT_MASTER; + if (!fr_event_insert(el, tcp_socket_timer, this, &when, + &(sock->ev))) { + rad_panic("Failed to insert event"); + } + } + + /* + * Run a callback to do any specific + * signalling on "connection up". + * + * For TLS sockets and WITH_COA_TUNNEL, + * this function should be similar to + * ping_home_server(), except that it + * should send a Status-Server packet, + * with Originating-Realm-Key as a VSA. + */ +// process_listener_up(this); + +#endif /* WITH_TCP */ + break; +#endif /* WITH_PROXY */ + + /* + * FIXME: put idle timers on command sockets. + */ + + default: +#ifdef WITH_TCP + /* + * Add timers to incoming child sockets, if necessary. + */ + if (sock->proto == IPPROTO_TCP && sock->opened && + (sock->limit.lifetime || sock->limit.idle_timeout)) { + struct timeval when; + + when.tv_sec = sock->opened + 1; + when.tv_usec = 0; + + ASSERT_MASTER; + if (!fr_event_insert(el, tcp_socket_timer, this, &when, + &(sock->ev))) { + ERROR("Failed adding timer for socket: %s", fr_strerror()); + fr_exit(1); + } + } + +#ifdef WITH_COA_TUNNEL + /* + * If we're allowed to send CoA requests + * back down this incoming socket, then + * add the socket to the proxy listener + * list. We need to check for "parent", + * as the main incoming listener has + * "send_coa" set, but it just calls + * accept(), and doesn't actually send + * any packets. + */ + if (this->send_coa && this->parent) { + PTHREAD_MUTEX_LOCK(&proxy_mutex); + if (!fr_packet_list_socket_add(proxy_list, this->fd, + sock->proto, +#ifdef WITH_RADIUSV11 + sock->radiusv11, +#endif + &sock->other_ipaddr, sock->other_port, + this)) { + ERROR("Failed adding coa proxy socket"); + fr_exit_now(1); + } + PTHREAD_MUTEX_UNLOCK(&proxy_mutex); + } +#endif /* WITH_COA_TUNNEL */ + +#endif /* WITH_TCP */ + break; + } /* switch over listener types */ + + /* + * All sockets: add the FD to the event handler. + */ + insert_fd: + if (fr_event_fd_insert(el, 0, this->fd, + event_socket_handler, this)) { + this->status = RAD_LISTEN_STATUS_KNOWN; + return; + } + + /* + * Print out which socket failed. + * + * If we're trying to add the socket, then + * forcibly remove it immediately, without any + * additional cleanups. There cannot, and MUST + * NOT be any packets associated with the socket. + */ + this->print(this, buffer, sizeof(buffer)); + ERROR("Failed adding event handler for socket %s: %s", buffer, fr_strerror()); + this->status = RAD_LISTEN_STATUS_EOL; + goto listener_is_eol; + } /* end of INIT */ + + if (this->status == RAD_LISTEN_STATUS_PAUSE) { + fr_event_fd_delete(el, 0, this->fd); + return; + } + + if (this->status == RAD_LISTEN_STATUS_RESUME) goto insert_fd; + +#ifdef WITH_TCP + /* + * The socket has reached a timeout. Try to close it. + */ + if (this->status == RAD_LISTEN_STATUS_FROZEN) { + /* + * Requests are still using the socket. Wait for + * them to finish. + */ + if (this->count > 0) { + struct timeval when; + + /* + * Try again to clean up the socket in 30 + * seconds. + */ + gettimeofday(&when, NULL); + when.tv_sec += 30; + + ASSERT_MASTER; + if (!fr_event_insert(el, + (fr_event_callback_t) event_new_fd, + this, &when, &sock->ev)) { + rad_panic("Failed to insert event"); + } + + return; + } + + fr_event_fd_delete(el, 0, this->fd); + this->status = RAD_LISTEN_STATUS_REMOVE_NOW; + } + + /* + * The socket has had a catastrophic error. Close it. + */ + if (this->status == RAD_LISTEN_STATUS_EOL) { + /* + * Remove it from the list of live FD's. + */ + fr_event_fd_delete(el, 0, this->fd); + + listener_is_eol: +#ifdef WITH_PROXY + /* + * Tell all requests using this socket that the socket is dead. + */ + if (this->type == RAD_LISTEN_PROXY +#ifdef WITH_COA_TUNNEL + || (this->send_coa && this->parent) +#endif + ) { + PTHREAD_MUTEX_LOCK(&proxy_mutex); + if (!fr_packet_list_socket_freeze(proxy_list, + this->fd)) { + ERROR("Fatal error freezing socket: %s", fr_strerror()); + fr_exit(1); + } + + if (this->count > 0) { + fr_packet_list_walk(proxy_list, this, proxy_eol_cb); + } + PTHREAD_MUTEX_UNLOCK(&proxy_mutex); + } +#endif /* WITH_PROXY */ + + /* + * Requests are still using the socket. Wait for + * them to finish. + */ + if (this->count > 0) { + struct timeval when; + + /* + * Try again to clean up the socket in 30 + * seconds. + */ + gettimeofday(&when, NULL); + when.tv_sec += 30; + + ASSERT_MASTER; + if (!fr_event_insert(el, + (fr_event_callback_t) event_new_fd, + this, &when, &sock->ev)) { + rad_panic("Failed to insert event"); + } + + return; + } + + /* + * No one is using the socket. We can remove it now. + */ + this->status = RAD_LISTEN_STATUS_REMOVE_NOW; + } /* socket is at EOL */ +#endif /* WITH_TCP */ + + if (this->dead) goto wait_some_more; + + /* + * Nuke the socket. + */ + if (this->status == RAD_LISTEN_STATUS_REMOVE_NOW) { + int devnull; + + this->dead = true; + + /* + * Re-open the socket, pointing it to /dev/null. + * This means that all writes proceed without + * blocking, and all reads return "no data". + * + * This leaves the socket active, so any child + * threads won't go insane. But it means that + * they cannot send or receive any packets. + * + * This is EXTRA work in the normal case, when + * sockets are closed without error. But it lets + * us have one simple processing method for all + * sockets. + */ + devnull = open("/dev/null", O_RDWR); + if (devnull < 0) { + ERROR("FATAL failure opening /dev/null: %s", + fr_syserror(errno)); + fr_exit(1); + } + if (dup2(devnull, this->fd) < 0) { + ERROR("FATAL failure closing socket: %s", + fr_syserror(errno)); + fr_exit(1); + } + close(devnull); + +#ifdef WITH_DETAIL + rad_assert(this->type != RAD_LISTEN_DETAIL); +#endif + +#ifdef WITH_TCP +#ifdef WITH_PROXY + /* + * The socket is dead. Force all proxied packets + * to stop using it. And then remove it from the + * list of outgoing sockets. + */ + if (this->type == RAD_LISTEN_PROXY +#ifdef WITH_COA_TUNNEL + || (this->send_coa && this->parent) +#endif + ) { + home_server_t *home; + sock = this->data; + + home = sock->home; + if (!home || !home->limit.max_connections) { + INFO(" ... shutting down socket %s", buffer); + } else { + INFO(" ... shutting down socket %s (%u of %u)", buffer, + home->limit.num_connections, home->limit.max_connections); + } + + PTHREAD_MUTEX_LOCK(&proxy_mutex); + fr_packet_list_walk(proxy_list, this, eol_proxy_listener); + + if (!fr_packet_list_socket_del(proxy_list, this->fd)) { + ERROR("Fatal error removing socket %s: %s", + buffer, fr_strerror()); + fr_exit(1); + } + +#ifdef WITH_TLS + /* + * Remove this socket from the list of sockets assocated with this home server. + * + * This MUST be done with the proxy mutex locked! + */ + if (home && home->tls) { + fr_assert(home->listeners); + + (void) rbtree_deletebydata(home->listeners, this); + } +#endif + + PTHREAD_MUTEX_UNLOCK(&proxy_mutex); + +#ifdef WITH_COA_TUNNEL + /* + * Clean up the proxied packets AND the + * normal one. + */ + if (this->send_coa && this->parent) goto shutdown; +#endif + + } else +#endif /* WITH_PROXY */ + { +#ifdef WITH_COA_TUNNEL + shutdown: +#endif + INFO(" ... shutting down socket %s", buffer); + + /* + * EOL all requests using this socket. + */ + rbtree_walk(pl, RBTREE_DELETE_ORDER, eol_listener, this); + } + + /* + * No child threads, clean it up now. + */ + if (!spawn_flag) { + ASSERT_MASTER; + + if (this->type != RAD_LISTEN_DETAIL && sock && sock->ev) { + fr_event_delete(el, &sock->ev); + } + listen_free(&this); + return; + } + + /* + * Wait until all requests using this socket are done. + */ + wait_some_more: + listener_free_cb(this); +#endif /* WITH_TCP */ + } + + return; +} + +/*********************************************************************** + * + * Signal handlers. + * + ***********************************************************************/ + +static void handle_signal_self(int flag) +{ + ASSERT_MASTER; + + if ((flag & (RADIUS_SIGNAL_SELF_EXIT | RADIUS_SIGNAL_SELF_TERM)) != 0) { + if ((flag & RADIUS_SIGNAL_SELF_EXIT) != 0) { + INFO("Signalled to exit"); + fr_event_loop_exit(el, 1); + } else { + INFO("Signalled to terminate"); + fr_event_loop_exit(el, 2); + } + + return; + } /* else exit/term flags weren't set */ + + /* + * Tell the even loop to stop processing. + */ + if ((flag & RADIUS_SIGNAL_SELF_HUP) != 0) { + time_t when; + static time_t last_hup = 0; + + when = time(NULL); + if ((int) (when - last_hup) < 5) { + INFO("Ignoring HUP (less than 5s since last one)"); + return; + } + + INFO("Received HUP signal"); + + last_hup = when; + + exec_trigger(NULL, NULL, "server.signal.hup", true); + fr_event_loop_exit(el, 0x80); + } + +#if defined(WITH_DETAIL) && !defined(WITH_DETAIL_THREAD) + if ((flag & RADIUS_SIGNAL_SELF_DETAIL) != 0) { + rad_listen_t *this; + + /* + * FIXME: O(N) loops suck. + */ + for (this = main_config.listen; + this != NULL; + this = this->next) { + if (this->type != RAD_LISTEN_DETAIL) continue; + + /* + * This one didn't send the signal, skip + * it. + */ + if (!this->decode(this, NULL)) continue; + + /* + * Go service the interrupt. + */ + event_poll_detail(this); + } + } +#endif + +#if defined(WITH_PROXY) && defined(HAVE_PTHREAD_H) + /* + * There are new listeners in the list. Run + * event_new_fd() on them. + */ + if ((flag & RADIUS_SIGNAL_SELF_NEW_FD) != 0) { + rad_listen_t *this, *next; + + FD_MUTEX_LOCK(&fd_mutex); + + /* + * FIXME: unlock the mutex before calling + * event_new_fd()? + */ + for (this = new_listeners; this != NULL; this = next) { + next = this->next; + this->next = NULL; + + event_new_fd(this); + } + + new_listeners = NULL; + FD_MUTEX_UNLOCK(&fd_mutex); + } +#endif +} + +#ifndef HAVE_PTHREAD_H +void radius_signal_self(int flag) +{ + if (flag == RADIUS_SIGNAL_SELF_TERM) { + main_config.exiting = true; + } + + return handle_signal_self(flag); +} + +#else +static int self_pipe[2] = { -1, -1 }; + +/* + * Inform ourselves that we received a signal. + */ +void radius_signal_self(int flag) +{ + ssize_t rcode; + uint8_t buffer[16]; + + if (flag == RADIUS_SIGNAL_SELF_TERM) { + main_config.exiting = true; + } + + /* + * The read MUST be non-blocking for this to work. + */ + rcode = read(self_pipe[0], buffer, sizeof(buffer)); + if (rcode > 0) { + ssize_t i; + + for (i = 0; i < rcode; i++) { + buffer[0] |= buffer[i]; + } + } else { + buffer[0] = 0; + } + + buffer[0] |= flag; + + if (write(self_pipe[1], buffer, 1) < 0) fr_exit(0); +} + + +static void event_signal_handler(UNUSED fr_event_list_t *xel, + UNUSED int fd, UNUSED void *ctx) +{ + ssize_t i, rcode; + uint8_t buffer[32]; + + rcode = read(self_pipe[0], buffer, sizeof(buffer)); + if (rcode <= 0) return; + + /* + * Merge pending signals. + */ + for (i = 0; i < rcode; i++) { + buffer[0] |= buffer[i]; + } + + handle_signal_self(buffer[0]); +} +#endif /* HAVE_PTHREAD_H */ + +/*********************************************************************** + * + * Bootstrapping code. + * + ***********************************************************************/ + +/* + * Externally-visibly functions. + */ +int radius_event_init(TALLOC_CTX *ctx) { + el = fr_event_list_create(ctx, event_status); + if (!el) return 0; + +#ifdef HAVE_SYSTEMD_WATCHDOG + if (sd_watchdog_interval.tv_sec || sd_watchdog_interval.tv_usec) { + struct timeval now; + + fr_event_now(el, &now); + + sdwd.when = now; + sdwd.el = el; + + sd_watchdog_event(&sdwd); + } +#endif + + return 1; +} + +static int packet_entry_cmp(void const *one, void const *two) +{ + RADIUS_PACKET const * const *a = one; + RADIUS_PACKET const * const *b = two; + + return fr_packet_cmp(*a, *b); +} + +#ifdef WITH_PROXY +/* + * They haven't defined a proxy listener. Automatically + * add one for them, with the correct address family. + */ +static void create_default_proxy_listener(int af) +{ + uint16_t port = 0; + home_server_t home; + listen_socket_t *sock; + rad_listen_t *this; + + memset(&home, 0, sizeof(home)); + + /* + * Open a default UDP port + */ + home.proto = IPPROTO_UDP; + port = 0; + + /* + * Set the address family. + */ + home.src_ipaddr.af = af; + home.ipaddr.af = af; + + /* + * Get the correct listener. + */ + this = proxy_new_listener(proxy_ctx, &home, port); + if (!this) { + fr_exit_now(1); + } + + sock = this->data; + if (!fr_packet_list_socket_add(proxy_list, this->fd, + sock->proto, +#ifdef WITH_RADIUSV11 + sock->radiusv11, +#endif + &sock->other_ipaddr, sock->other_port, + this)) { + ERROR("Failed adding proxy socket"); + fr_exit_now(1); + } + + /* + * Insert the FD into list of FDs to listen on. + */ + radius_update_listener(this); +} + +/* + * See if we automatically need to open a proxy socket. + */ +static void check_proxy(rad_listen_t *head) +{ + bool defined_proxy; + bool has_v4, has_v6; + rad_listen_t *this; + + if (check_config) return; + if (!main_config.proxy_requests) { + DEBUG3("Cannot proxy packets unless 'proxy_requests = yes'"); + return; + } + if (!head) return; +#ifdef WITH_TCP + if (!home_servers_udp) return; +#endif + + /* + * We passed "-i" on the command line. Use that address + * family for the proxy socket. + */ + if (main_config.myip.af != AF_UNSPEC) { + create_default_proxy_listener(main_config.myip.af); + return; + } + + defined_proxy = has_v4 = has_v6 = false; + + /* + * Figure out if we need to open a proxy socket, and if + * so, which one. + */ + for (this = head; this != NULL; this = this->next) { + listen_socket_t *sock; + + switch (this->type) { + case RAD_LISTEN_PROXY: + defined_proxy = true; + break; + + case RAD_LISTEN_AUTH: +#ifdef WITH_ACCT + case RAD_LISTEN_ACCT: +#endif +#ifdef WITH_COA + case RAD_LISTEN_COA: +#endif + sock = this->data; + if (sock->my_ipaddr.af == AF_INET) has_v4 = true; + if (sock->my_ipaddr.af == AF_INET6) has_v6 = true; + break; + + default: + break; + } + } + + /* + * Assume they know what they're doing. + */ + if (defined_proxy) return; + + if (has_v4) create_default_proxy_listener(AF_INET); + + if (has_v6) create_default_proxy_listener(AF_INET6); +} +#endif + +int radius_event_start(CONF_SECTION *cs, bool have_children) +{ + rad_listen_t *head = NULL; + + if (fr_start_time != (time_t)-1) return 0; + + time(&fr_start_time); + + if (!check_config) { + /* + * radius_event_init() must be called first + */ + rad_assert(el); + + pl = rbtree_create(NULL, packet_entry_cmp, NULL, 0); + if (!pl) return 0; /* leak el */ + } + + request_num_counter = 0; + +#ifdef WITH_PROXY + if (main_config.proxy_requests && !check_config) { + /* + * Create the tree for managing proxied requests and + * responses. + */ + proxy_list = fr_packet_list_create(1); + if (!proxy_list) return 0; + +#ifdef HAVE_PTHREAD_H + if (pthread_mutex_init(&proxy_mutex, NULL) != 0) { + ERROR("FATAL: Failed to initialize proxy mutex: %s", + fr_syserror(errno)); + fr_exit(1); + } +#endif + + /* + * The "init_delay" is set to "response_window". + * Reset it to half of "response_window" in order + * to give the event loop enough time to service + * the event before hitting "response_window". + */ + main_config.init_delay.tv_usec += (main_config.init_delay.tv_sec & 0x01) * USEC; + main_config.init_delay.tv_usec >>= 1; + main_config.init_delay.tv_sec >>= 1; + + proxy_ctx = talloc_init("proxy"); + } +#endif + + /* + * Move all of the thread calls to this file? + * + * It may be best for the mutexes to be in this file... + */ + spawn_flag = have_children; + +#ifdef HAVE_PTHREAD_H + NO_SUCH_CHILD_PID = pthread_self(); /* not a child thread */ + + /* + * Initialize the threads ONLY if we're spawning, AND + * we're running normally. + */ + if (have_children && !check_config && + (thread_pool_init(cs, &spawn_flag) < 0)) { + fr_exit(1); + } +#endif + + if (check_config) { + DEBUG("%s: #### Skipping IP addresses and Ports ####", + main_config.name); + if (listen_init(cs, &head, spawn_flag) < 0) { + fflush(NULL); + fr_exit(1); + } + return 1; + } + +#ifdef HAVE_PTHREAD_H + /* + * Child threads need a pipe to signal us, as do the + * signal handlers. + */ + if (pipe(self_pipe) < 0) { + ERROR("Error opening internal pipe: %s", fr_syserror(errno)); + fr_exit(1); + } + if ((fcntl(self_pipe[0], F_SETFL, O_NONBLOCK) < 0) || + (fcntl(self_pipe[0], F_SETFD, FD_CLOEXEC) < 0)) { + ERROR("Error setting internal flags: %s", fr_syserror(errno)); + fr_exit(1); + } + if ((fcntl(self_pipe[1], F_SETFL, O_NONBLOCK) < 0) || + (fcntl(self_pipe[1], F_SETFD, FD_CLOEXEC) < 0)) { + ERROR("Error setting internal flags: %s", fr_syserror(errno)); + fr_exit(1); + } + DEBUG4("Created signal pipe. Read end FD %i, write end FD %i", self_pipe[0], self_pipe[1]); + + if (!fr_event_fd_insert(el, 0, self_pipe[0], event_signal_handler, el)) { + ERROR("Failed creating signal pipe handler: %s", fr_strerror()); + fr_exit(1); + } +#endif + + DEBUG("%s: #### Opening IP addresses and Ports ####", main_config.name); + + /* + * The server temporarily switches to an unprivileged + * user very early in the bootstrapping process. + * However, some sockets MAY require privileged access + * (bind to device, or to port < 1024, or to raw + * sockets). Those sockets need to call suid up/down + * themselves around the functions that need a privileged + * uid. + */ + if (listen_init(cs, &head, spawn_flag) < 0) { + fr_exit_now(1); + } + + main_config.listen = head; + +#ifdef WITH_PROXY + check_proxy(head); +#endif + + /* + * At this point, no one has any business *ever* going + * back to root uid. + */ + rad_suid_down_permanent(); + + return 1; +} + + +#ifdef WITH_PROXY +static int proxy_delete_cb(UNUSED void *ctx, void *data) +{ + REQUEST *request = fr_packet2myptr(REQUEST, proxy, data); + + VERIFY_REQUEST(request); + + request->master_state = REQUEST_STOP_PROCESSING; + +#ifdef HAVE_PTHREAD_H + if (pthread_equal(request->child_pid, NO_SUCH_CHILD_PID) == 0) return 0; +#endif + + /* + * If it's queued we can't delete it from the queue. + * + * Otherwise, it's OK to delete it. Even RUNNING, because + * that will get caught by the check above. + */ + if (request->child_state == REQUEST_QUEUED) return 0; + + request->in_proxy_hash = false; + + if (!request->in_request_hash) { + request_done(request, FR_ACTION_CANCELLED); + } + + /* + * Delete it from the list. + */ + return 2; +} +#endif + + +static int request_delete_cb(UNUSED void *ctx, void *data) +{ + REQUEST *request = fr_packet2myptr(REQUEST, packet, data); + + VERIFY_REQUEST(request); + + request->master_state = REQUEST_STOP_PROCESSING; + + /* + * Not done, or the child thread is still processing it. + */ + if (request->child_state < REQUEST_RESPONSE_DELAY) return 0; /* continue */ + +#ifdef HAVE_PTHREAD_H + if (pthread_equal(request->child_pid, NO_SUCH_CHILD_PID) == 0) return 0; +#endif + +#ifdef WITH_PROXY + rad_assert(request->in_proxy_hash == false); +#endif + + request->in_request_hash = false; + ASSERT_MASTER; + if (request->ev) fr_event_delete(el, &request->ev); + + if (main_config.memory_report) { + RDEBUG2("Cleaning up request packet ID %u with timestamp +%d", + request->packet->id, + (unsigned int) (request->timestamp - fr_start_time)); + } + +#ifdef WITH_COA + if (request->coa) { + rad_assert(!request->coa->in_proxy_hash); + } +#endif + + request_free(request); + + /* + * Delete it from the list, and continue; + */ + return 2; +} + + +void radius_event_free(void) +{ + ASSERT_MASTER; + +#ifdef WITH_PROXY + /* + * There are requests in the proxy hash that aren't + * referenced from anywhere else. Remove them first. + */ + if (proxy_list) { + fr_packet_list_walk(proxy_list, NULL, proxy_delete_cb); + } +#endif + + rbtree_walk(pl, RBTREE_DELETE_ORDER, request_delete_cb, NULL); + + if (spawn_flag) { + /* + * Now that all requests have been marked "please stop", + * ensure that all of the threads have exited. + */ +#ifdef HAVE_PTHREAD_H + thread_pool_stop(); +#endif + + /* + * Walk the lists again, ensuring that all + * requests are done. + */ + if (main_config.memory_report) { + int num; + +#ifdef WITH_PROXY + if (proxy_list) { + fr_packet_list_walk(proxy_list, NULL, proxy_delete_cb); + num = fr_packet_list_num_elements(proxy_list); + if (num > 0) { + ERROR("Proxy list has %d requests still in it.", num); + } + } +#endif + + rbtree_walk(pl, RBTREE_DELETE_ORDER, request_delete_cb, NULL); + num = rbtree_num_elements(pl); + if (num > 0) { + ERROR("Request list has %d requests still in it.", num); + } + } + } + + rbtree_free(pl); + pl = NULL; + +#ifdef WITH_PROXY + fr_packet_list_free(proxy_list); + proxy_list = NULL; + + if (proxy_ctx) talloc_free(proxy_ctx); +#endif + + TALLOC_FREE(el); + + if (debug_condition) talloc_free(debug_condition); +} + +int radius_event_process(void) +{ + if (!el) return 0; + + return fr_event_loop(el); +} diff --git a/src/main/radattr.c b/src/main/radattr.c new file mode 100644 index 0000000..8accd0d --- /dev/null +++ b/src/main/radattr.c @@ -0,0 +1,1123 @@ +/* + * radattr.c RADIUS Attribute debugging tool. + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2010 Alan DeKok <aland@freeradius.org> + */ + +RCSID("$Id$") + +#include <freeradius-devel/libradius.h> + +typedef struct REQUEST REQUEST; + +#include <freeradius-devel/parser.h> +#include <freeradius-devel/xlat.h> +#include <freeradius-devel/conf.h> +#include <freeradius-devel/radpaths.h> +#include <freeradius-devel/dhcp.h> + +#include <ctype.h> + +#ifdef HAVE_GETOPT_H +# include <getopt.h> +#endif + +#include <assert.h> + +#include <freeradius-devel/log.h> +extern log_lvl_t rad_debug_lvl; + +#include <sys/wait.h> +#ifdef HAVE_PTHREAD_H +pid_t rad_fork(void); +pid_t rad_waitpid(pid_t pid, int *status); + +pid_t rad_fork(void) +{ + return fork(); +} + +pid_t rad_waitpid(pid_t pid, int *status) +{ + return waitpid(pid, status, 0); +} +#endif + +static TALLOC_CTX *autofree; + +static ssize_t xlat_test(UNUSED void *instance, UNUSED REQUEST *request, + UNUSED char const *fmt, UNUSED char *out, UNUSED size_t outlen) +{ + return 0; +} + +static RADIUS_PACKET access_request = { + .sockfd = -1, + .id = 0, + .code = PW_CODE_ACCESS_REQUEST, + .vector = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }, +}; + +static RADIUS_PACKET access_accept = { + .sockfd = -1, + .id = 0, + .code = PW_CODE_ACCESS_ACCEPT, + .vector = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }, +}; + +static RADIUS_PACKET coa_request = { + .sockfd = -1, + .id = 0, + .code = PW_CODE_COA_REQUEST, + .vector = { 0 }, +}; + +static RADIUS_PACKET *my_original = &access_request; +static RADIUS_PACKET *my_packet = &access_accept; + +static char const *my_secret = "testing123"; + +/* + * End of hacks for xlat + * + **********************************************************************/ + +static int encode_tlv(char *buffer, uint8_t *output, size_t outlen); + +static char const hextab[] = "0123456789abcdef"; + +static int encode_data_string(char *buffer, + uint8_t *output, size_t outlen) +{ + int length = 0; + char *p; + + p = buffer + 1; + + while (*p && (outlen > 0)) { + if (*p == '"') { + return length; + } + + if (*p != '\\') { + *(output++) = *(p++); + outlen--; + length++; + continue; + } + + switch (p[1]) { + default: + *(output++) = p[1]; + break; + + case 'n': + *(output++) = '\n'; + break; + + case 'r': + *(output++) = '\r'; + break; + + case 't': + *(output++) = '\t'; + break; + } + + outlen--; + length++; + } + + fprintf(stderr, "String is not terminated\n"); + return 0; +} + +static int encode_data_tlv(char *buffer, char **endptr, + uint8_t *output, size_t outlen) +{ + int depth = 0; + int length; + char *p; + + for (p = buffer; *p != '\0'; p++) { + if (*p == '{') depth++; + if (*p == '}') { + depth--; + if (depth == 0) break; + } + } + + if (*p != '}') { + fprintf(stderr, "No trailing '}' in string starting " + "with \"%s\"\n", + buffer); + return 0; + } + + *endptr = p + 1; + *p = '\0'; + + p = buffer + 1; + while (isspace((uint8_t) *p)) p++; + + length = encode_tlv(p, output, outlen); + if (length == 0) return 0; + + return length; +} + +static int encode_hex(char *p, uint8_t *output, size_t outlen) +{ + int length = 0; + while (*p) { + char *c1, *c2; + + while (isspace((uint8_t) *p)) p++; + + if (!*p) break; + + if(!(c1 = memchr(hextab, tolower((uint8_t) p[0]), 16)) || + !(c2 = memchr(hextab, tolower((uint8_t) p[1]), 16))) { + fprintf(stderr, "Invalid data starting at " + "\"%s\"\n", p); + return 0; + } + + *output = ((c1 - hextab) << 4) + (c2 - hextab); + output++; + length++; + p += 2; + + outlen--; + if (outlen == 0) { + fprintf(stderr, "Too much data\n"); + return 0; + } + } + + return length; +} + + +static int encode_data(char *p, uint8_t *output, size_t outlen) +{ + int length; + + if (!isspace((uint8_t) *p)) { + fprintf(stderr, "Invalid character following attribute " + "definition\n"); + return 0; + } + + while (isspace((uint8_t) *p)) p++; + + if (*p == '{') { + int sublen; + char *q; + + length = 0; + + do { + while (isspace((uint8_t) *p)) p++; + if (!*p) { + if (length == 0) { + fprintf(stderr, "No data\n"); + return 0; + } + + break; + } + + sublen = encode_data_tlv(p, &q, output, outlen); + if (sublen == 0) return 0; + + length += sublen; + output += sublen; + outlen -= sublen; + p = q; + } while (*q); + + return length; + } + + if (*p == '"') { + length = encode_data_string(p, output, outlen); + return length; + } + + length = encode_hex(p, output, outlen); + + if (length == 0) { + fprintf(stderr, "Empty string\n"); + return 0; + } + + return length; +} + +static int decode_attr(char *buffer, char **endptr) +{ + long attr; + + attr = strtol(buffer, endptr, 10); + if (*endptr == buffer) { + fprintf(stderr, "No valid number found in string " + "starting with \"%s\"\n", buffer); + return 0; + } + + if (!**endptr) { + fprintf(stderr, "Nothing follows attribute number\n"); + return 0; + } + + if ((attr <= 0) || (attr > 256)) { + fprintf(stderr, "Attribute number is out of valid " + "range\n"); + return 0; + } + + return (int) attr; +} + +static int decode_vendor(char *buffer, char **endptr) +{ + long vendor; + + if (*buffer != '.') { + fprintf(stderr, "Invalid separator before vendor id\n"); + return 0; + } + + vendor = strtol(buffer + 1, endptr, 10); + if (*endptr == (buffer + 1)) { + fprintf(stderr, "No valid vendor number found\n"); + return 0; + } + + if (!**endptr) { + fprintf(stderr, "Nothing follows vendor number\n"); + return 0; + } + + if ((vendor <= 0) || (vendor > (1 << 24))) { + fprintf(stderr, "Vendor number is out of valid range\n"); + return 0; + } + + if (**endptr != '.') { + fprintf(stderr, "Invalid data following vendor number\n"); + return 0; + } + (*endptr)++; + + return (int) vendor; +} + +static int encode_tlv(char *buffer, uint8_t *output, size_t outlen) +{ + int attr; + int length; + char *p; + + attr = decode_attr(buffer, &p); + if (attr == 0) return 0; + + output[0] = attr; + output[1] = 2; + + if (*p == '.') { + p++; + length = encode_tlv(p, output + 2, outlen - 2); + + } else { + length = encode_data(p, output + 2, outlen - 2); + } + + if (length == 0) return 0; + if (length > (255 - 2)) { + fprintf(stderr, "TLV data is too long\n"); + return 0; + } + + output[1] += length; + + return length + 2; +} + +static int encode_vsa(char *buffer, uint8_t *output, size_t outlen) +{ + int vendor; + int length; + char *p; + + vendor = decode_vendor(buffer, &p); + if (vendor == 0) return 0; + + output[0] = 0; + output[1] = (vendor >> 16) & 0xff; + output[2] = (vendor >> 8) & 0xff; + output[3] = vendor & 0xff; + + length = encode_tlv(p, output + 4, outlen - 4); + if (length == 0) return 0; + if (length > (255 - 6)) { + fprintf(stderr, "VSA data is too long\n"); + return 0; + } + + + return length + 4; +} + +static int encode_evs(char *buffer, uint8_t *output, size_t outlen) +{ + int vendor; + int attr; + int length; + char *p; + + vendor = decode_vendor(buffer, &p); + if (vendor == 0) return 0; + + attr = decode_attr(p, &p); + if (attr == 0) return 0; + + output[0] = 0; + output[1] = (vendor >> 16) & 0xff; + output[2] = (vendor >> 8) & 0xff; + output[3] = vendor & 0xff; + output[4] = attr; + + length = encode_data(p, output + 5, outlen - 5); + if (length == 0) return 0; + + return length + 5; +} + +static int encode_extended(char *buffer, + uint8_t *output, size_t outlen) +{ + int attr; + int length; + char *p; + + attr = decode_attr(buffer, &p); + if (attr == 0) return 0; + + output[0] = attr; + + if (attr == 26) { + length = encode_evs(p, output + 1, outlen - 1); + } else { + length = encode_data(p, output + 1, outlen - 1); + } + if (length == 0) return 0; + if (length > (255 - 3)) { + fprintf(stderr, "Extended Attr data is too long\n"); + return 0; + } + + return length + 1; +} + +static int encode_long_extended(char *buffer, + uint8_t *output, size_t outlen) +{ + int attr; + int length, total; + char *p; + + attr = decode_attr(buffer, &p); + if (attr == 0) return 0; + + /* output[0] is the extended attribute */ + output[1] = 4; + output[2] = attr; + output[3] = 0; + + if (attr == 26) { + length = encode_evs(p, output + 4, outlen - 4); + if (length == 0) return 0; + + output[1] += 5; + length -= 5; + } else { + length = encode_data(p, output + 4, outlen - 4); + } + if (length == 0) return 0; + + total = 0; + while (1) { + int sublen = 255 - output[1]; + + if (length <= sublen) { + output[1] += length; + total += output[1]; + break; + } + + length -= sublen; + + memmove(output + 255 + 4, output + 255, length); + memcpy(output + 255, output, 4); + + output[1] = 255; + output[3] |= 0x80; + + output += 255; + output[1] = 4; + total += 255; + } + + return total; +} + +static int encode_rfc(char *buffer, uint8_t *output, size_t outlen) +{ + int attr; + int length, sublen; + char *p; + + attr = decode_attr(buffer, &p); + if (attr == 0) return 0; + + length = 2; + output[0] = attr; + output[1] = 2; + + if (attr == 26) { + sublen = encode_vsa(p, output + 2, outlen - 2); + + } else if ((attr < 241) || (attr > 246)) { + sublen = encode_data(p, output + 2, outlen - 2); + + } else { + if (*p != '.') { + fprintf(stderr, "Invalid data following " + "attribute number\n"); + return 0; + } + + if (attr < 245) { + sublen = encode_extended(p + 1, + output + 2, outlen - 2); + } else { + + /* + * Not like the others! + */ + return encode_long_extended(p + 1, output, outlen); + } + } + if (sublen == 0) return 0; + if (sublen > (255 -2)) { + fprintf(stderr, "RFC Data is too long\n"); + return 0; + } + + output[1] += sublen; + return length + sublen; +} + +static void parse_condition(char const *input, char *output, size_t outlen) +{ + ssize_t slen; + char const *error = NULL; + fr_cond_t *cond; + + slen = fr_condition_tokenize(NULL, NULL, input, &cond, &error, FR_COND_ONE_PASS); + if (slen <= 0) { + snprintf(output, outlen, "ERROR offset %d %s", (int) -slen, error); + return; + } + + input += slen; + if (*input != '\0') { + talloc_free(cond); + snprintf(output, outlen, "ERROR offset %d 'Too much text'", (int) slen); + return; + } + + fr_cond_sprint(output, outlen, cond); + + talloc_free(cond); +} + +static void parse_xlat(char const *input, char *output, size_t outlen) +{ + ssize_t slen; + char const *error = NULL; + char *fmt = talloc_typed_strdup(autofree, input); + xlat_exp_t *head; + + slen = xlat_tokenize(autofree, fmt, &head, &error); + if (slen <= 0) { + snprintf(output, outlen, "ERROR offset %d '%s'", (int) -slen, error); + return; + } + + if (input[slen] != '\0') { + snprintf(output, outlen, "ERROR offset %d 'Too much text'", (int) slen); + talloc_free(fmt); + return; + } + + xlat_sprint(output, outlen, head); + talloc_free(fmt); +} + +static void process_file(const char *root_dir, char const *filename) +{ + int lineno; + size_t i, outlen; + ssize_t len, data_len; + FILE *fp; + char input[8192], buffer[8192]; + char output[8192]; + char directory[8192]; + uint8_t *attr, data[2048]; + + if (strcmp(filename, "-") == 0) { + fp = stdin; + directory[0] = '\0'; + + } else { + if (root_dir && *root_dir) { + snprintf(directory, sizeof(directory), "%s/%s", root_dir, filename); + } else { + strlcpy(directory, filename, sizeof(directory)); + } + + fp = fopen(directory, "r"); + if (!fp) { + fprintf(stderr, "Error opening %s: %s\n", + directory, fr_syserror(errno)); + exit(1); + } + + filename = directory; + } + + lineno = 0; + *output = '\0'; + data_len = 0; + + while (fgets(buffer, sizeof(buffer), fp) != NULL) { + char *p = strchr(buffer, '\n'); + VALUE_PAIR *vp, *head; + VALUE_PAIR **tail = &head; + + lineno++; + head = NULL; + + if (!p) { + if (!feof(fp)) { + fprintf(stderr, "Line %d too long in %s\n", + lineno, directory); + exit(1); + } + } else { + *p = '\0'; + } + + /* + * Comments, with hacks for User-Name[#] + */ + p = strchr(buffer, '#'); + if (p && ((p == buffer) || + ((p > buffer) && (p[-1] != '[')))) *p = '\0'; + + p = buffer; + while (isspace((uint8_t) *p)) p++; + if (!*p) continue; + + DEBUG2("%s[%d]: %s\n", filename, lineno, buffer); + + strlcpy(input, p, sizeof(input)); + + if (strncmp(p, "raw ", 4) == 0) { + outlen = encode_rfc(p + 4, data, sizeof(data)); + if (outlen == 0) { + fprintf(stderr, "Parse error in line %d of %s\n", + lineno, directory); + exit(1); + } + + print_hex: + if (outlen == 0) { + output[0] = 0; + continue; + } + + if (outlen > sizeof(data)) outlen = sizeof(data); + + if (outlen >= (sizeof(output) / 2)) { + outlen = (sizeof(output) / 2) - 1; + } + + data_len = outlen; + for (i = 0; i < outlen; i++) { + if (sizeof(output) < (3*i)) break; + + snprintf(output + 3*i, sizeof(output) - (3*i) - 1, + "%02x ", data[i]); + } + outlen = strlen(output); + output[outlen - 1] = '\0'; + continue; + } + + if (strncmp(p, "data ", 5) == 0) { + if (strcmp(p + 5, output) != 0) { + fprintf(stderr, "Mismatch at line %d of %s\n\tgot : %s\n\texpected : %s\n", + lineno, directory, output, p + 5); + exit(1); + } + continue; + } + + if (strncmp(p, "packet ", 7) == 0) { + p += 7; + if (strncmp(p, "access_accept", 13) == 0) { + my_packet = &access_accept; + } else if (strncmp(p, "coa_request", 11) == 0) { + my_packet = &coa_request; + } else { + fprintf(stderr, "Unsupported packet type at line %d of %s: %s\n", + lineno, directory, p); + exit(1); + } + continue; + } + if (strncmp(p, "original ", 9) == 0) { + p += 9; + if (strncmp(p, "null", 4) == 0) { + my_original = NULL; + } else if (strncmp(p, "access_request", 14) == 0) { + my_original = &access_request; + } else { + fprintf(stderr, "Unsupported original type at line %d of %s: %s\n", + lineno, directory, p); + exit(1); + } + continue; + } + + if (strncmp(p, "encode ", 7) == 0) { + if (strcmp(p + 7, "-") == 0) { + p = output; + } else { + p += 7; + } + + if (fr_pair_list_afrom_str(autofree, p, &head) != T_EOL) { + strlcpy(output, fr_strerror(), sizeof(output)); + continue; + } + + attr = data; + vp = head; + while (vp) { + VALUE_PAIR **pvp = &vp; + VALUE_PAIR const **qvp; + + memcpy(&qvp, &pvp, sizeof(pvp)); + + len = rad_vp2attr(my_packet, my_original, my_secret, qvp, + attr, data + sizeof(data) - attr); + if (len < 0) { + fprintf(stderr, "Failed encoding %s: %s\n", + vp->da->name, fr_strerror()); + fr_pair_list_free(&head); + exit(1); + } + + attr += len; + if (len == 0) break; + } + + fr_pair_list_free(&head); + outlen = attr - data; + goto print_hex; + } + + if (strncmp(p, "decode ", 7) == 0) { + ssize_t my_len; + + if (strcmp(p + 7, "-") == 0) { + attr = data; + len = data_len; + } else { + attr = data; + len = encode_hex(p + 7, data, sizeof(data)); + if (len == 0) { + fprintf(stderr, "Failed decoding hex string at line %d of %s\n", lineno, directory); + exit(1); + } + } + + my_len = 0; + while (len > 0) { + vp = NULL; + my_len = rad_attr2vp(autofree, my_packet, my_original, my_secret, attr, len, &vp); + if (my_len < 0) { + fr_pair_list_free(&head); + break; + } + + if (my_len > len) { + fprintf(stderr, "Internal sanity check failed at %d\n", __LINE__); + exit(1); + } + + *tail = vp; + while (vp) { + tail = &(vp->next); + vp = vp->next; + } + + attr += my_len; + len -= my_len; + } + + /* + * Output may be an error, and we ignore + * it if so. + */ + if (head) { + vp_cursor_t cursor; + p = output; + for (vp = fr_cursor_init(&cursor, &head); + vp; + vp = fr_cursor_next(&cursor)) { + vp_prints(p, sizeof(output) - (p - output), vp); + p += strlen(p); + + if (vp->next) { + strcpy(p, ", "); + p += 2; + } + } + + fr_pair_list_free(&head); + } else if (my_len < 0) { + strlcpy(output, fr_strerror(), sizeof(output)); + + } else { /* zero-length attribute */ + *output = '\0'; + } + + continue; + } + +#ifdef WITH_DHCP + /* + * And some DHCP tests + */ + if (strncmp(p, "encode-dhcp ", 12) == 0) { + vp_cursor_t cursor; + + if (strcmp(p + 12, "-") == 0) { + p = output; + } else { + p += 12; + } + + if (fr_pair_list_afrom_str(NULL, p, &head) != T_EOL) { + strlcpy(output, fr_strerror(), sizeof(output)); + continue; + } + + fr_cursor_init(&cursor, &head); + + + attr = data; + vp = head; + + while ((vp = fr_cursor_current(&cursor))) { + len = fr_dhcp_encode_option(NULL, attr, data + sizeof(data) - attr, &cursor); + if (len < 0) { + fprintf(stderr, "Failed encoding %s: %s\n", + vp->da->name, fr_strerror()); + exit(1); + } + attr += len; + }; + + fr_pair_list_free(&head); + outlen = attr - data; + goto print_hex; + } + + if (strncmp(p, "decode-dhcp ", 12) == 0) { + ssize_t my_len; + + if (strcmp(p + 12, "-") == 0) { + attr = data; + len = data_len; + } else { + attr = data; + len = encode_hex(p + 12, data, sizeof(data)); + if (len == 0) { + fprintf(stderr, "Failed decoding hex string at line %d of %s\n", lineno, directory); + exit(1); + } + } + + my_len = fr_dhcp_decode_options(NULL, &head, attr, len); + + /* + * Output may be an error, and we ignore + * it if so. + */ + if (head) { + vp_cursor_t cursor; + p = output; + for (vp = fr_cursor_init(&cursor, &head); + vp; + vp = fr_cursor_next(&cursor)) { + vp_prints(p, sizeof(output) - (p - output), vp); + p += strlen(p); + + if (vp->next) {strcpy(p, ", "); + p += 2; + } + } + + fr_pair_list_free(&head); + } else if (my_len < 0) { + strlcpy(output, fr_strerror(), sizeof(output)); + + } else { /* zero-length attribute */ + *output = '\0'; + } + continue; + } +#endif + + if (strncmp(p, "attribute ", 10) == 0) { + p += 10; + + if (fr_pair_list_afrom_str(NULL, p, &head) != T_EOL) { + strlcpy(output, fr_strerror(), sizeof(output)); + continue; + } + + vp_prints(output, sizeof(output), head); + + fr_pair_list_free(&head); + continue; + } + + if (strncmp(p, "$INCLUDE ", 9) == 0) { + char *q; + + p += 9; + while (isspace((uint8_t) *p)) p++; + + q = strrchr(directory, '/'); + if (q) { + *q = '\0'; + process_file(directory, p); + *q = '/'; + } else { + process_file(NULL, p); + } + continue; + } + + if (strncmp(p, "condition ", 10) == 0) { + p += 10; + parse_condition(p, output, sizeof(output)); + continue; + } + + if (strncmp(p, "xlat ", 5) == 0) { + p += 5; + parse_xlat(p, output, sizeof(output)); + continue; + } + + fprintf(stderr, "Unknown input at line %d of %s\n", + lineno, directory); + exit(1); + } + + if (fp != stdin) fclose(fp); +} + +/** Dump all of the dictionary entries as + * + * ALIAS name OID + * + * To create dictionaries which allow files to be used with v4. + * + * rm -rf alias;mkdir alias;./build/make/jlibtool --mode=execute ./build/bin/radattr -D ./share/ -A | sort -n -k6 -k7 -k8 -k9 -k10 -k11 | gawk '{printf "%s\t%-40s\t%s\n", $1, $2, $3 >> "alias/alias." tolower($5) }' + * + * And then post-process each file to remove the comments. + * + * Note that we have to use GNU Awk, as OSX awk doesn't like redirection to a file which includes a variable. + */ +static int dump_aliases(void *ctx, void *data) +{ + DICT_ATTR *da = data; + FILE *fp = ctx; + int nest, attr, dv_type; + DICT_VENDOR *dv; + char buffer[1024]; + + if (!da->vendor || (da->vendor > FR_MAX_VENDOR)) return 0; + + dv = dict_vendorbyvalue(da->vendor); + dv_type = dv->type; + + (void) dict_print_oid(buffer, sizeof(buffer), da); + fprintf(fp, "ALIAS\t%s\t%s # %s %u", da->name, buffer, dv->name, da->vendor); + + attr = da->attr; + switch (dv_type) { + default: + case 1: + fprintf(fp, " %u", attr & 0xff); + + /* + * Only these ones are bit-packed. + */ + for (nest = 1; nest <= fr_attr_max_tlv; nest++) { + if (((attr >> fr_attr_shift[nest]) & fr_attr_mask[nest]) == 0) break; + + fprintf(fp, " %u", + (attr >> fr_attr_shift[nest]) & fr_attr_mask[nest]); + } + break; + + case 2: + fprintf(fp, " %u", attr & 0xffff); + break; + + case 4: + fprintf(fp, " %u", attr); + break; + } + + printf("\n"); + + return 0; +} + +static void NEVER_RETURNS usage(void) +{ + fprintf(stderr, "usage: radattr [OPTS] filename\n"); + fprintf(stderr, " -d <raddb> Set user dictionary directory (defaults to " RADDBDIR ").\n"); + fprintf(stderr, " -D <dictdir> Set main dictionary directory (defaults to " DICTDIR ").\n"); + fprintf(stderr, " -x Debugging mode.\n"); + fprintf(stderr, " -M Show talloc memory report.\n"); + + exit(1); +} + +int main(int argc, char *argv[]) +{ + int c; + bool report = false; + bool dump_alias = false; + char const *radius_dir = RADDBDIR; + char const *dict_dir = DICTDIR; + int *inst = &c; + +DIAG_OFF(deprecated-declarations) + autofree = talloc_autofree_context(); +DIAG_ON(deprecated-declarations) + + cf_new_escape = true; /* fix the tests */ + +#ifndef NDEBUG + if (fr_fault_setup(getenv("PANIC_ACTION"), argv[0]) < 0) { + fr_perror("radattr"); + exit(EXIT_FAILURE); + } +#endif + + while ((c = getopt(argc, argv, "Ad:D:xMh")) != EOF) switch (c) { + case 'A': + dump_alias = true; + break; + case 'd': + radius_dir = optarg; + break; + case 'D': + dict_dir = optarg; + break; + case 'x': + fr_debug_lvl++; + rad_debug_lvl = fr_debug_lvl; + break; + case 'M': + report = true; + break; + case 'h': + default: + usage(); + } + argc -= (optind - 1); + argv += (optind - 1); + + /* + * Mismatch between the binary and the libraries it depends on + */ + if (fr_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) { + fr_perror("radattr"); + return 1; + } + + if (dict_init(dict_dir, RADIUS_DICTIONARY) < 0) { + fr_perror("radattr"); + return 1; + } + + if (dict_read(radius_dir, RADIUS_DICTIONARY) == -1) { + fr_perror("radattr"); + return 1; + } + + if (xlat_register("test", xlat_test, NULL, inst) < 0) { + fprintf(stderr, "Failed registering xlat"); + return 1; + } + + if (dump_alias) { + (void) dict_walk(dump_aliases, stdout); + return 0; + } + + if (argc < 2) { + process_file(NULL, "-"); + + } else { + process_file(NULL, argv[1]); + } + + if (report) { + dict_free(); + fr_log_talloc_report(NULL); + } + + return 0; +} diff --git a/src/main/radattr.mk b/src/main/radattr.mk new file mode 100644 index 0000000..1a184bd --- /dev/null +++ b/src/main/radattr.mk @@ -0,0 +1,10 @@ +TARGET := radattr +SOURCES := radattr.c + +TGT_PREREQS := libfreeradius-server.a libfreeradius-radius.a + +ifneq "$(WITH_DHCP)" "no" +TGT_PREREQS += libfreeradius-dhcp.a +endif + +TGT_LDLIBS := $(LIBS) diff --git a/src/main/radclient.c b/src/main/radclient.c new file mode 100644 index 0000000..49da461 --- /dev/null +++ b/src/main/radclient.c @@ -0,0 +1,1712 @@ +/* + * radclient.c General radius packet debug tool. + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2000,2006,2014 The FreeRADIUS server project + * Copyright 2000 Miquel van Smoorenburg <miquels@cistron.nl> + * Copyright 2000 Alan DeKok <aland@ox.org> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radclient.h> +#include <freeradius-devel/radpaths.h> +#include <freeradius-devel/udpfromto.h> +#include <freeradius-devel/conf.h> +#ifdef HAVE_OPENSSL_SSL_H +#include <openssl/ssl.h> +#include <freeradius-devel/openssl3.h> +#endif +#include <ctype.h> + +#ifdef HAVE_GETOPT_H +# include <getopt.h> +#endif + +#include <assert.h> + +USES_APPLE_DEPRECATED_API + +typedef struct REQUEST REQUEST; /* to shut up warnings about mschap.h */ + +#include "smbdes.h" +#include "mschap.h" + +static int retries = 3; +static float timeout = 5; +static char const *secret = NULL; +static bool do_output = true; + +static rc_stats_t stats; + +static uint16_t server_port = 0; +static int packet_code = PW_CODE_UNDEFINED; +static fr_ipaddr_t server_ipaddr; +static int resend_count = 1; +static bool done = true; +static bool print_filename = false; + +static fr_ipaddr_t client_ipaddr; +static uint16_t client_port = 0; + +static int sockfd; + +#ifdef WITH_TCP +static char const *proto = NULL; +#endif +static int ipproto = IPPROTO_UDP; + +static rbtree_t *filename_tree = NULL; +static fr_packet_list_t *pl = NULL; + +static int sleep_time = -1; + +static rc_request_t *request_head = NULL; +static rc_request_t *rc_request_tail = NULL; + +static char const *radclient_version = "radclient version " RADIUSD_VERSION_STRING +#ifdef RADIUSD_VERSION_COMMIT +" (git #" STRINGIFY(RADIUSD_VERSION_COMMIT) ")" +#endif +#ifndef ENABLE_REPRODUCIBLE_BUILDS +", built on " __DATE__ " at " __TIME__ +#endif +; + +static void NEVER_RETURNS usage(void) +{ + fprintf(stderr, "Usage: radclient [options] server[:port] <command> [<secret>]\n"); + + fprintf(stderr, " <command> One of auth, acct, status, coa, disconnect or auto.\n"); + fprintf(stderr, " -4 Use IPv4 address of server\n"); + fprintf(stderr, " -6 Use IPv6 address of server.\n"); + fprintf(stderr, " -c <count> Send each packet 'count' times.\n"); + fprintf(stderr, " -d <raddb> Set user dictionary directory (defaults to " RADDBDIR ").\n"); + fprintf(stderr, " -D <dictdir> Set main dictionary directory (defaults to " DICTDIR ").\n"); + fprintf(stderr, " -f <file>[:<file>] Read packets from file, not stdin.\n"); + fprintf(stderr, " If a second file is provided, it will be used to verify responses\n"); + fprintf(stderr, " -F Print the file name, packet number and reply code.\n"); + fprintf(stderr, " -h Print usage help information.\n"); + fprintf(stderr, " -n <num> Send N requests/s\n"); + fprintf(stderr, " -p <num> Send 'num' packets from a file in parallel.\n"); + fprintf(stderr, " -q Do not print anything out.\n"); + fprintf(stderr, " -r <retries> If timeout, retry sending the packet 'retries' times.\n"); + fprintf(stderr, " -s Print out summary information of auth results.\n"); + fprintf(stderr, " -S <file> read secret from file, not command line.\n"); + fprintf(stderr, " -t <timeout> Wait 'timeout' seconds before retrying (may be a floating point number).\n"); + fprintf(stderr, " -v Show program version information.\n"); + fprintf(stderr, " -x Debugging mode.\n"); + +#ifdef WITH_TCP + fprintf(stderr, " -P <proto> Use proto (tcp or udp) for transport.\n"); +#endif + + exit(1); +} + +static const FR_NAME_NUMBER request_types[] = { + { "auth", PW_CODE_ACCESS_REQUEST }, + { "challenge", PW_CODE_ACCESS_CHALLENGE }, + { "acct", PW_CODE_ACCOUNTING_REQUEST }, + { "status", PW_CODE_STATUS_SERVER }, + { "disconnect", PW_CODE_DISCONNECT_REQUEST }, + { "coa", PW_CODE_COA_REQUEST }, + { "auto", PW_CODE_UNDEFINED }, + + { NULL, 0} +}; + +/* + * Free a radclient struct, which may (or may not) + * already be in the list. + */ +static int _rc_request_free(rc_request_t *request) +{ + rc_request_t *prev, *next; + + prev = request->prev; + next = request->next; + + if (prev) { + assert(request_head != request); + prev->next = next; + } else if (request_head) { + assert(request_head == request); + request_head = next; + } + + if (next) { + assert(rc_request_tail != request); + next->prev = prev; + } else if (rc_request_tail) { + assert(rc_request_tail == request); + rc_request_tail = prev; + } + + return 0; +} + +#if defined(OPENSSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L +# include <openssl/provider.h> + +static OSSL_PROVIDER *openssl_default_provider = NULL; +static OSSL_PROVIDER *openssl_legacy_provider = NULL; + +static int openssl3_init(void) +{ + /* + * Load the default provider for most algorithms + */ + openssl_default_provider = OSSL_PROVIDER_load(NULL, "default"); + if (!openssl_default_provider) { + ERROR("(TLS) Failed loading default provider"); + return -1; + } + + /* + * Needed for MD4 + * + * https://www.openssl.org/docs/man3.0/man7/migration_guide.html#Legacy-Algorithms + */ + openssl_legacy_provider = OSSL_PROVIDER_load(NULL, "legacy"); + if (!openssl_legacy_provider) { + ERROR("(TLS) Failed loading legacy provider"); + return -1; + } + + return 0; +} + +static void openssl3_free(void) +{ + if (openssl_default_provider && !OSSL_PROVIDER_unload(openssl_default_provider)) { + ERROR("Failed unloading default provider"); + } + openssl_default_provider = NULL; + + if (openssl_legacy_provider && !OSSL_PROVIDER_unload(openssl_legacy_provider)) { + ERROR("Failed unloading legacy provider"); + } + openssl_legacy_provider = NULL; +} +#else +#define openssl3_init() +#define openssl3_free() +#endif + + + +static int mschapv1_encode(RADIUS_PACKET *packet, VALUE_PAIR **request, + char const *password) +{ + int rcode; + unsigned int i; + uint8_t *p; + VALUE_PAIR *challenge, *reply; + uint8_t nthash[16]; + + fr_pair_delete_by_num(&packet->vps, PW_MSCHAP_CHALLENGE, VENDORPEC_MICROSOFT, TAG_ANY); + fr_pair_delete_by_num(&packet->vps, PW_MSCHAP_RESPONSE, VENDORPEC_MICROSOFT, TAG_ANY); + + challenge = fr_pair_afrom_num(packet, PW_MSCHAP_CHALLENGE, VENDORPEC_MICROSOFT); + if (!challenge) { + return 0; + } + + fr_pair_add(request, challenge); + challenge->vp_length = 8; + challenge->vp_octets = p = talloc_array(challenge, uint8_t, challenge->vp_length); + for (i = 0; i < challenge->vp_length; i++) { + p[i] = fr_rand(); + } + + reply = fr_pair_afrom_num(packet, PW_MSCHAP_RESPONSE, VENDORPEC_MICROSOFT); + if (!reply) { + return 0; + } + + fr_pair_add(request, reply); + reply->vp_length = 50; + reply->vp_octets = p = talloc_array(reply, uint8_t, reply->vp_length); + memset(p, 0, reply->vp_length); + + p[1] = 0x01; /* NT hash */ + + rcode = mschap_ntpwdhash(nthash, password); + if (rcode < 0) return 0; + + smbdes_mschap(nthash, challenge->vp_octets, p + 26); + return 1; +} + + +static int getport(char const *name) +{ + struct servent *svp; + + svp = getservbyname(name, "udp"); + if (!svp) return 0; + + return ntohs(svp->s_port); +} + +/* + * Set a port from the request type if we don't already have one + */ +static void radclient_get_port(PW_CODE type, uint16_t *port) +{ + switch (type) { + default: + case PW_CODE_ACCESS_REQUEST: + case PW_CODE_ACCESS_CHALLENGE: + case PW_CODE_STATUS_SERVER: + if (*port == 0) *port = getport("radius"); + if (*port == 0) *port = PW_AUTH_UDP_PORT; + return; + + case PW_CODE_ACCOUNTING_REQUEST: + if (*port == 0) *port = getport("radacct"); + if (*port == 0) *port = PW_ACCT_UDP_PORT; + return; + + case PW_CODE_DISCONNECT_REQUEST: + if (*port == 0) *port = PW_POD_UDP_PORT; + return; + + case PW_CODE_COA_REQUEST: + if (*port == 0) *port = PW_COA_UDP_PORT; + return; + + case PW_CODE_UNDEFINED: + if (*port == 0) *port = 0; + return; + } +} + +/* + * Resolve a port to a request type + */ +static PW_CODE radclient_get_code(uint16_t port) +{ + /* + * getport returns 0 if the service doesn't exist + * so we need to return early, to avoid incorrect + * codes. + */ + if (port == 0) return PW_CODE_UNDEFINED; + + if ((port == getport("radius")) || (port == PW_AUTH_UDP_PORT) || (port == PW_AUTH_UDP_PORT_ALT)) { + return PW_CODE_ACCESS_REQUEST; + } + if ((port == getport("radacct")) || (port == PW_ACCT_UDP_PORT) || (port == PW_ACCT_UDP_PORT_ALT)) { + return PW_CODE_ACCOUNTING_REQUEST; + } + if (port == PW_COA_UDP_PORT) return PW_CODE_COA_REQUEST; + if (port == PW_POD_UDP_PORT) return PW_CODE_DISCONNECT_REQUEST; + + return PW_CODE_UNDEFINED; +} + + +static bool already_hex(VALUE_PAIR *vp) +{ + size_t i; + + if (!vp || (vp->da->type != PW_TYPE_OCTETS)) return true; + + /* + * If it's 17 octets, it *might* be already encoded. + * Or, it might just be a 17-character password (maybe UTF-8) + * Check it for non-printable characters. The odds of ALL + * of the characters being 32..255 is (1-7/8)^17, or (1/8)^17, + * or 1/(2^51), which is pretty much zero. + */ + for (i = 0; i < vp->vp_length; i++) { + if (vp->vp_octets[i] < 32) { + return true; + } + } + + return false; +} + + +/* + * Initialize a radclient data structure and add it to + * the global linked list. + */ +static int radclient_init(TALLOC_CTX *ctx, rc_file_pair_t *files) +{ + FILE *packets, *filters = NULL; + + vp_cursor_t cursor; + VALUE_PAIR *vp; + rc_request_t *request; + bool packets_done = false; + uint64_t num = 0; + + assert(files->packets != NULL); + + /* + * Determine where to read the VP's from. + */ + if (strcmp(files->packets, "-") != 0) { + packets = fopen(files->packets, "r"); + if (!packets) { + ERROR("Error opening %s: %s", files->packets, strerror(errno)); + return 0; + } + + /* + * Read in the pairs representing the expected response. + */ + if (files->filters) { + filters = fopen(files->filters, "r"); + if (!filters) { + ERROR("Error opening %s: %s", files->filters, strerror(errno)); + fclose(packets); + return 0; + } + } + } else { + packets = stdin; + } + + + /* + * Loop until the file is done. + */ + do { + /* + * Allocate it. + */ + request = talloc_zero(ctx, rc_request_t); + if (!request) { + ERROR("Out of memory"); + goto error; + } + + request->packet = rad_alloc(request, true); + if (!request->packet) { + ERROR("Out of memory"); + goto error; + } + + request->packet->src_ipaddr = client_ipaddr; + request->packet->src_port = client_port; + request->packet->dst_ipaddr = server_ipaddr; + request->packet->dst_port = server_port; +#ifdef WITH_TCP + request->packet->proto = ipproto; +#endif + + request->files = files; + request->packet->id = -1; /* allocate when sending */ + request->num = num++; + + /* + * Read the request VP's. + */ + if (fr_pair_list_afrom_file(request->packet, &request->packet->vps, packets, &packets_done) < 0) { + char const *input; + + if ((files->packets[0] == '-') && (files->packets[1] == '\0')) { + input = "stdin"; + } else { + input = files->packets; + } + + REDEBUG("Error parsing \"%s\"", input); + goto error; + } + + /* + * Skip empty entries + */ + if (!request->packet->vps) { + talloc_free(request); + continue; + } + + /* + * Read in filter VP's. + */ + if (filters) { + bool filters_done; + + if (fr_pair_list_afrom_file(request, &request->filter, filters, &filters_done) < 0) { + REDEBUG("Error parsing \"%s\"", files->filters); + goto error; + } + + if (filters_done && !packets_done) { + REDEBUG("Differing number of packets/filters in %s:%s " + "(too many requests))", files->packets, files->filters); + goto error; + } + + if (!filters_done && packets_done) { + REDEBUG("Differing number of packets/filters in %s:%s " + "(too many filters))", files->packets, files->filters); + goto error; + } + + /* + * xlat expansions aren't supported here + */ + for (vp = fr_cursor_init(&cursor, &request->filter); + vp; + vp = fr_cursor_next(&cursor)) { + if (vp->type == VT_XLAT) { + vp->type = VT_DATA; + vp->vp_strvalue = vp->value.xlat; + vp->vp_length = talloc_array_length(vp->vp_strvalue) - 1; + } + + if (vp->da->vendor == 0 ) switch (vp->da->attr) { + case PW_RESPONSE_PACKET_TYPE: + case PW_PACKET_TYPE: + fr_cursor_remove(&cursor); /* so we don't break the filter */ + request->filter_code = vp->vp_integer; + talloc_free(vp); + + default: + break; + } + } + + /* + * This allows efficient list comparisons later + */ + fr_pair_list_sort(&request->filter, fr_pair_cmp_by_da_tag); + } + + /* + * Process special attributes + */ + for (vp = fr_cursor_init(&cursor, &request->packet->vps); + vp; + vp = fr_cursor_next(&cursor)) { + /* + * Double quoted strings get marked up as xlat expansions, + * but we don't support that in request. + */ + if (vp->type == VT_XLAT) { + vp->type = VT_DATA; + vp->vp_strvalue = vp->value.xlat; + vp->vp_length = talloc_array_length(vp->vp_strvalue) - 1; + } + + if (!vp->da->vendor) switch (vp->da->attr) { + default: + break; + + /* + * Allow it to set the packet type in + * the attributes read from the file. + */ + case PW_PACKET_TYPE: + request->packet->code = vp->vp_integer; + break; + + case PW_RESPONSE_PACKET_TYPE: + request->filter_code = vp->vp_integer; + break; + + case PW_PACKET_DST_PORT: + request->packet->dst_port = (vp->vp_integer & 0xffff); + break; + + case PW_PACKET_DST_IP_ADDRESS: + request->packet->dst_ipaddr.af = AF_INET; + request->packet->dst_ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr; + request->packet->dst_ipaddr.prefix = 32; + break; + + case PW_PACKET_DST_IPV6_ADDRESS: + request->packet->dst_ipaddr.af = AF_INET6; + request->packet->dst_ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr; + request->packet->dst_ipaddr.prefix = 128; + break; + + case PW_PACKET_SRC_PORT: + if ((vp->vp_integer < 1024) || + (vp->vp_integer > 65535)) { + ERROR("Invalid value '%u' for Packet-Src-Port", vp->vp_integer); + goto error; + } + request->packet->src_port = (vp->vp_integer & 0xffff); + break; + + case PW_PACKET_SRC_IP_ADDRESS: + request->packet->src_ipaddr.af = AF_INET; + request->packet->src_ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr; + request->packet->src_ipaddr.prefix = 32; + break; + + case PW_PACKET_SRC_IPV6_ADDRESS: + request->packet->src_ipaddr.af = AF_INET6; + request->packet->src_ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr; + request->packet->src_ipaddr.prefix = 128; + break; + + case PW_DIGEST_REALM: + case PW_DIGEST_NONCE: + case PW_DIGEST_METHOD: + case PW_DIGEST_URI: + case PW_DIGEST_QOP: + case PW_DIGEST_ALGORITHM: + case PW_DIGEST_BODY_DIGEST: + case PW_DIGEST_CNONCE: + case PW_DIGEST_NONCE_COUNT: + case PW_DIGEST_USER_NAME: + /* overlapping! */ + { + DICT_ATTR const *da; + uint8_t *p, *q; + + p = talloc_array(vp, uint8_t, vp->vp_length + 2); + + memcpy(p + 2, vp->vp_octets, vp->vp_length); + p[0] = vp->da->attr - PW_DIGEST_REALM + 1; + vp->vp_length += 2; + p[1] = vp->vp_length; + + da = dict_attrbyvalue(PW_DIGEST_ATTRIBUTES, 0); + if (!da) { + ERROR("Out of memory"); + goto error; + } + vp->da = da; + + /* + * Re-do fr_pair_value_memsteal ourselves, + * because we play games with + * vp->da, and fr_pair_value_memsteal goes + * to GREAT lengths to sanitize + * and fix and change and + * double-check the various + * fields. + */ + memcpy(&q, &vp->vp_octets, sizeof(q)); + talloc_free(q); + + vp->vp_octets = talloc_steal(vp, p); + vp->type = VT_DATA; + + VERIFY_VP(vp); + } + break; + + /* + * Cache this for later. + */ + case PW_CLEARTEXT_PASSWORD: + request->password = vp; + break; + + /* + * Keep a copy of the the password attribute. + */ + case PW_CHAP_PASSWORD: + /* + * If it's already hex, do nothing. + */ + if ((vp->vp_length == 17) && + (already_hex(vp))) break; + + /* + * CHAP-Password is octets, so it may not be zero terminated. + */ + request->password = fr_pair_make(request->packet, &request->packet->vps, "Cleartext-Password", + "", T_OP_EQ); + fr_pair_value_bstrncpy(request->password, vp->vp_strvalue, vp->vp_length); + break; + + case PW_USER_PASSWORD: + case PW_MS_CHAP_PASSWORD: + request->password = fr_pair_make(request->packet, &request->packet->vps, "Cleartext-Password", + vp->vp_strvalue, T_OP_EQ); + break; + + case PW_RADCLIENT_TEST_NAME: + request->name = vp->vp_strvalue; + break; + } + } /* loop over the VP's we read in */ + + /* + * Use the default set on the command line + */ + if (request->packet->code == PW_CODE_UNDEFINED) request->packet->code = packet_code; + + /* + * Default to the filename + */ + if (!request->name) request->name = request->files->packets; + + /* + * Automatically set the response code from the request code + * (if one wasn't already set). + */ + if (request->filter_code == PW_CODE_UNDEFINED) { + switch (request->packet->code) { + case PW_CODE_ACCESS_REQUEST: + request->filter_code = PW_CODE_ACCESS_ACCEPT; + break; + + case PW_CODE_ACCOUNTING_REQUEST: + request->filter_code = PW_CODE_ACCOUNTING_RESPONSE; + break; + + case PW_CODE_COA_REQUEST: + request->filter_code = PW_CODE_COA_ACK; + break; + + case PW_CODE_DISCONNECT_REQUEST: + request->filter_code = PW_CODE_DISCONNECT_ACK; + break; + + case PW_CODE_STATUS_SERVER: + switch (radclient_get_code(request->packet->dst_port)) { + case PW_CODE_ACCESS_REQUEST: + request->filter_code = PW_CODE_ACCESS_ACCEPT; + break; + + case PW_CODE_ACCOUNTING_REQUEST: + request->filter_code = PW_CODE_ACCOUNTING_RESPONSE; + break; + + default: + request->filter_code = PW_CODE_UNDEFINED; + break; + } + break; + + case PW_CODE_UNDEFINED: + REDEBUG("Both Packet-Type and Response-Packet-Type undefined, specify at least one, " + "or a well known RADIUS port"); + goto error; + + default: + REDEBUG("Can't determine expected Response-Packet-Type for Packet-Type %i", + request->packet->code); + goto error; + } + /* + * Automatically set the request code from the response code + * (if one wasn't already set). + */ + } else if (request->packet->code == PW_CODE_UNDEFINED) { + switch (request->filter_code) { + case PW_CODE_ACCESS_ACCEPT: + case PW_CODE_ACCESS_REJECT: + request->packet->code = PW_CODE_ACCESS_REQUEST; + break; + + case PW_CODE_ACCOUNTING_RESPONSE: + request->packet->code = PW_CODE_ACCOUNTING_REQUEST; + break; + + case PW_CODE_DISCONNECT_ACK: + case PW_CODE_DISCONNECT_NAK: + request->packet->code = PW_CODE_DISCONNECT_REQUEST; + break; + + case PW_CODE_COA_ACK: + case PW_CODE_COA_NAK: + request->packet->code = PW_CODE_COA_REQUEST; + break; + + default: + REDEBUG("Can't determine expected Packet-Type for Response-Packet-Type %i", + request->filter_code); + goto error; + } + } + + /* + * Automatically set the dst port (if one wasn't already set). + */ + if (request->packet->dst_port == 0) { + radclient_get_port(request->packet->code, &request->packet->dst_port); + if (request->packet->dst_port == 0) { + REDEBUG("Can't determine destination port"); + goto error; + } + } + + /* + * Add it to the tail of the list. + */ + if (!request_head) { + assert(rc_request_tail == NULL); + request_head = request; + request->prev = NULL; + } else { + assert(rc_request_tail->next == NULL); + rc_request_tail->next = request; + request->prev = rc_request_tail; + } + rc_request_tail = request; + request->next = NULL; + + /* + * Set the destructor so it removes itself from the + * request list when freed. We don't set this until + * the packet is actually in the list, else we trigger + * the asserts in the free callback. + */ + talloc_set_destructor(request, _rc_request_free); + } while (!packets_done); /* loop until the file is done. */ + + if (packets != stdin) fclose(packets); + if (filters) fclose(filters); + + /* + * And we're done. + */ + return 1; + +error: + talloc_free(request); + + if (packets != stdin) fclose(packets); + if (filters) fclose(filters); + + return 0; +} + + +/* + * Sanity check each argument. + */ +static int radclient_sane(rc_request_t *request) +{ + if (request->packet->dst_port == 0) { + request->packet->dst_port = server_port; + } + if (request->packet->dst_ipaddr.af == AF_UNSPEC) { + if (server_ipaddr.af == AF_UNSPEC) { + ERROR("No server was given, and request %" PRIu64 " in file %s did not contain " + "Packet-Dst-IP-Address", request->num, request->files->packets); + return -1; + } + request->packet->dst_ipaddr = server_ipaddr; + } + if (request->packet->code == 0) { + if (packet_code == -1) { + ERROR("Request was \"auto\", and request %" PRIu64 " in file %s did not contain Packet-Type", + request->num, request->files->packets); + return -1; + } + request->packet->code = packet_code; + } + request->packet->sockfd = -1; + + return 0; +} + + +/* + * For request handling. + */ +static int filename_cmp(void const *one, void const *two) +{ + int cmp; + + rc_file_pair_t const *a = one; + rc_file_pair_t const *b = two; + + cmp = strcmp(a->packets, b->packets); + if (cmp != 0) return cmp; + + return strcmp(a->filters, b->filters); +} + +static int filename_walk(UNUSED void *context, void *data) +{ + rc_file_pair_t *files = data; + + /* + * Read request(s) from the file. + */ + if (!radclient_init(files, files)) return -1; /* stop walking */ + + return 0; +} + + +/* + * Deallocate packet ID, etc. + */ +static void deallocate_id(rc_request_t *request) +{ + if (!request || !request->packet || + (request->packet->id < 0)) { + return; + } + + /* + * One more unused RADIUS ID. + */ + fr_packet_list_id_free(pl, request->packet, true); + + /* + * If we've already sent a packet, free up the old one, + * and ensure that the next packet has a unique + * authentication vector. + */ + if (request->packet->data) TALLOC_FREE(request->packet->data); + if (request->reply) rad_free(&request->reply); +} + +/* + * Send one packet. + */ +static int send_one_packet(rc_request_t *request) +{ + assert(request->done == false); + + /* + * Remember when we have to wake up, to re-send the + * request, of we didn't receive a reply. + */ + if ((sleep_time == -1) || (sleep_time > (int) timeout)) sleep_time = (int) timeout; + + /* + * Haven't sent the packet yet. Initialize it. + */ + if (request->packet->id == -1) { + int i; + bool rcode; + + assert(request->reply == NULL); + + /* + * Didn't find a free packet ID, we're not done, + * we don't sleep, and we stop trying to process + * this packet. + */ + retry: + request->packet->src_ipaddr.af = server_ipaddr.af; + rcode = fr_packet_list_id_alloc(pl, ipproto, &request->packet, NULL); + if (!rcode) { + int mysockfd; + +#ifdef WITH_TCP + if (proto) { + mysockfd = fr_socket_client_tcp(NULL, + &request->packet->dst_ipaddr, + request->packet->dst_port, false); + if (mysockfd < 0) { + ERROR("Failed opening socket"); + exit(1); + } + } else +#endif + { + mysockfd = fr_socket(&client_ipaddr, 0); + if (mysockfd < 0) { + ERROR("Failed opening socket"); + exit(1); + } + +#ifdef WITH_UDPFROMTO + if (udpfromto_init(mysockfd) < 0) { + ERROR("Failed initializing socket"); + exit(1); + } +#endif + } + if (!fr_packet_list_socket_add(pl, mysockfd, ipproto, +#ifdef WITH_RADIUSV11 + false, +#endif + &request->packet->dst_ipaddr, + request->packet->dst_port, NULL)) { + ERROR("Can't add new socket"); + exit(1); + } + goto retry; + } + + assert(request->packet->id != -1); + assert(request->packet->data == NULL); + + for (i = 0; i < 4; i++) { + ((uint32_t *) request->packet->vector)[i] = fr_rand(); + } + + /* + * Update the password, so it can be encrypted with the + * new authentication vector. + */ + if (request->password) { + VALUE_PAIR *vp; + + if ((vp = fr_pair_find_by_num(request->packet->vps, PW_USER_PASSWORD, 0, TAG_ANY)) != NULL) { + fr_pair_value_strcpy(vp, request->password->vp_strvalue); + + } else if ((vp = fr_pair_find_by_num(request->packet->vps, PW_CHAP_PASSWORD, 0, TAG_ANY)) != NULL) { + uint8_t buffer[17]; + + rad_chap_encode(request->packet, buffer, fr_rand() & 0xff, request->password); + fr_pair_value_memcpy(vp, buffer, 17); + + } else if (fr_pair_find_by_num(request->packet->vps, PW_MS_CHAP_PASSWORD, 0, TAG_ANY) != NULL) { + mschapv1_encode(request->packet, &request->packet->vps, request->password->vp_strvalue); + + } else { + DEBUG("WARNING: No password in the request"); + } + } + + request->timestamp = time(NULL); + request->tries = 1; + request->resend++; + + } else { /* request->packet->id >= 0 */ + time_t now = time(NULL); + + /* + * FIXME: Accounting packets are never retried! + * The Acct-Delay-Time attribute is updated to + * reflect the delay, and the packet is re-sent + * from scratch! + */ + + /* + * Not time for a retry, do so. + */ + if ((now - request->timestamp) < timeout) { + /* + * When we walk over the tree sending + * packets, we update the minimum time + * required to sleep. + */ + if ((sleep_time == -1) || + (sleep_time > (now - request->timestamp))) { + sleep_time = now - request->timestamp; + } + return 0; + } + + /* + * We're not trying later, maybe the packet is done. + */ + if (request->tries == retries) { + assert(request->packet->id >= 0); + + /* + * Delete the request from the tree of + * outstanding requests. + */ + fr_packet_list_yank(pl, request->packet); + + RDEBUG("No reply from server for ID %d socket %d", + request->packet->id, request->packet->sockfd); + deallocate_id(request); + + /* + * Normally we mark it "done" when we've received + * the reply, but this is a special case. + */ + if (request->resend == resend_count) { + request->done = true; + } + stats.lost++; + return -1; + } + + /* + * We are trying later. + */ + request->timestamp = now; + request->tries++; + } + + /* + * Send the packet. + */ + if (rad_send(request->packet, NULL, secret) < 0) { + REDEBUG("Failed to send packet for ID %d", request->packet->id); + deallocate_id(request); + request->done = true; + return -1; + } + + if (fr_log_fp) { + fr_packet_header_print(fr_log_fp, request->packet, false); + if (fr_debug_lvl > 0) vp_printlist(fr_log_fp, request->packet->vps); + } + + return 0; +} + +/* + * Receive one packet, maybe. + */ +static int recv_one_packet(int wait_time) +{ + fd_set set; + struct timeval tv; + rc_request_t *request; + RADIUS_PACKET *reply, **packet_p; + volatile int max_fd; + + /* And wait for reply, timing out as necessary */ + FD_ZERO(&set); + + max_fd = fr_packet_list_fd_set(pl, &set); + if (max_fd < 0) exit(1); /* no sockets to listen on! */ + + tv.tv_sec = (wait_time <= 0) ? 0 : wait_time; + tv.tv_usec = 0; + + /* + * No packet was received. + */ + if (select(max_fd, &set, NULL, NULL, &tv) <= 0) return 0; + + /* + * Look for the packet. + */ + reply = fr_packet_list_recv(pl, &set); + if (!reply) { + ERROR("Received bad packet"); +#ifdef WITH_TCP + /* + * If the packet is bad, we close the socket. + * I'm not sure how to do that now, so we just + * die... + */ + if (proto) exit(1); +#endif + return -1; /* bad packet */ + } + + packet_p = fr_packet_list_find_byreply(pl, reply); + if (!packet_p) { + ERROR("Received reply to request we did not send. (id=%d socket %d)", + reply->id, reply->sockfd); + rad_free(&reply); + return -1; /* got reply to packet we didn't send */ + } + request = fr_packet2myptr(rc_request_t, packet, packet_p); + + /* + * Fails the signature validation: not a real reply. + * FIXME: Silently drop it and listen for another packet. + */ + if (rad_verify(reply, request->packet, secret) < 0) { + REDEBUG("Reply verification failed"); + stats.lost++; + goto packet_done; /* shared secret is incorrect */ + } + + if (print_filename) { + RDEBUG("%s response code %d", request->files->packets, reply->code); + } + + deallocate_id(request); + request->reply = reply; + reply = NULL; + + /* + * If this fails, we're out of memory. + */ + if (rad_decode(request->reply, request->packet, secret) != 0) { + REDEBUG("Reply decode failed"); + stats.lost++; + goto packet_done; + } + + if (fr_log_fp) { + fr_packet_header_print(fr_log_fp, request->reply, true); + if (fr_debug_lvl > 0) vp_printlist(fr_log_fp, request->reply->vps); + } + + /* + * Increment counters... + */ + switch (request->reply->code) { + case PW_CODE_ACCESS_ACCEPT: + case PW_CODE_ACCOUNTING_RESPONSE: + case PW_CODE_COA_ACK: + case PW_CODE_DISCONNECT_ACK: + stats.accepted++; + break; + + case PW_CODE_ACCESS_CHALLENGE: + break; + + default: + stats.rejected++; + } + + /* + * If we had an expected response code, check to see if the + * packet matched that. + */ + if ((request->filter_code != PW_CODE_UNDEFINED) && (request->reply->code != request->filter_code)) { + fr_strerror_printf(NULL); + + if (is_radius_code(request->reply->code)) { + REDEBUG("%s: Expected %s got %s", request->name, fr_packet_codes[request->filter_code], + fr_packet_codes[request->reply->code]); + } else { + REDEBUG("%s: Expected %u got %i", request->name, request->filter_code, + request->reply->code); + } + stats.failed++; + /* + * Check if the contents of the packet matched the filter + */ + } else if (!request->filter) { + stats.passed++; + } else { + VALUE_PAIR const *failed[2]; + + fr_pair_list_sort(&request->reply->vps, fr_pair_cmp_by_da_tag); + if (fr_pair_validate(failed, request->filter, request->reply->vps)) { + RDEBUG("%s: Response passed filter", request->name); + stats.passed++; + } else { + fr_pair_validate_debug(request, failed); + REDEBUG("%s: Response for failed filter", request->name); + stats.failed++; + } + } + + if (request->resend == resend_count) { + request->done = true; + } + +packet_done: + rad_free(&request->reply); + rad_free(&reply); /* may be NULL */ + + return 0; +} + +DIAG_OFF(deprecated-declarations) +int main(int argc, char **argv) +{ + int c; + char const *radius_dir = RADDBDIR; + char const *dict_dir = DICTDIR; + char filesecret[256]; + FILE *fp; + int do_summary = false; + int persec = 0; + int parallel = 1; + rc_request_t *this; + int force_af = AF_UNSPEC; + + /* + * It's easier having two sets of flags to set the + * verbosity of library calls and the verbosity of + * radclient. + */ + fr_debug_lvl = 0; + fr_log_fp = stdout; + +#ifndef NDEBUG + if (fr_fault_setup(getenv("PANIC_ACTION"), argv[0]) < 0) { + fr_perror("radclient"); + exit(EXIT_FAILURE); + } +#endif + + talloc_set_log_stderr(); + + filename_tree = rbtree_create(NULL, filename_cmp, NULL, 0); + if (!filename_tree) { + oom: + ERROR("Out of memory"); + exit(1); + } + + while ((c = getopt(argc, argv, "46c:d:D:f:Fhn:p:qr:sS:t:vx" +#ifdef WITH_TCP + "P:" +#endif + )) != EOF) switch (c) { + case '4': + force_af = AF_INET; + break; + + case '6': + force_af = AF_INET6; + break; + + case 'c': + if (!isdigit((uint8_t) *optarg)) usage(); + + resend_count = atoi(optarg); + + if (resend_count < 1) usage(); + break; + + case 'D': + dict_dir = optarg; + break; + + case 'd': + radius_dir = optarg; + break; + + case 'f': + { + char const *p; + rc_file_pair_t *files; + + files = talloc(talloc_autofree_context(), rc_file_pair_t); + if (!files) goto oom; + + p = strchr(optarg, ':'); + if (p) { + files->packets = talloc_strndup(files, optarg, p - optarg); + if (!files->packets) goto oom; + files->filters = p + 1; + } else { + files->packets = optarg; + files->filters = NULL; + } + rbtree_insert(filename_tree, (void *) files); + } + break; + + case 'F': + print_filename = true; + break; + + case 'n': + persec = atoi(optarg); + if (persec <= 0) usage(); + break; + + /* + * Note that sending MANY requests in + * parallel can over-run the kernel + * queues, and Linux will happily discard + * packets. So even if the server responds, + * the client may not see the reply. + */ + case 'p': + parallel = atoi(optarg); + if (parallel <= 0) usage(); + break; + +#ifdef WITH_TCP + case 'P': + proto = optarg; + if (strcmp(proto, "tcp") != 0) { + if (strcmp(proto, "udp") == 0) { + proto = NULL; + } else { + usage(); + } + } else { + ipproto = IPPROTO_TCP; + } + break; + +#endif + + case 'q': + do_output = false; + fr_log_fp = NULL; /* no output from you, either! */ + break; + + case 'r': + if (!isdigit((uint8_t) *optarg)) usage(); + retries = atoi(optarg); + if ((retries == 0) || (retries > 1000)) usage(); + break; + + case 's': + do_summary = true; + break; + + case 'S': + { + char *p; + fp = fopen(optarg, "r"); + if (!fp) { + ERROR("Error opening %s: %s", optarg, fr_syserror(errno)); + exit(1); + } + if (fgets(filesecret, sizeof(filesecret), fp) == NULL) { + ERROR("Error reading %s: %s", optarg, fr_syserror(errno)); + exit(1); + } + fclose(fp); + + /* truncate newline */ + p = filesecret + strlen(filesecret) - 1; + while ((p >= filesecret) && + (*p < ' ')) { + *p = '\0'; + --p; + } + + if (strlen(filesecret) < 2) { + ERROR("Secret in %s is too short", optarg); + exit(1); + } + secret = filesecret; + } + break; + + case 't': + if (!isdigit((uint8_t) *optarg)) + usage(); + timeout = atof(optarg); + break; + + case 'v': + fr_debug_lvl = 1; + DEBUG("%s", radclient_version); + exit(0); + + case 'x': + fr_debug_lvl++; + break; + + case 'h': + default: + usage(); + } + argc -= (optind - 1); + argv += (optind - 1); + + if ((argc < 3) || ((secret == NULL) && (argc < 4))) { + ERROR("Insufficient arguments"); + usage(); + } + /* + * Mismatch between the binary and the libraries it depends on + */ + if (fr_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) { + fr_perror("radclient"); + return 1; + } + + if (dict_init(dict_dir, RADIUS_DICTIONARY) < 0) { + fr_perror("radclient"); + return 1; + } + + if (dict_read(radius_dir, RADIUS_DICTIONARY) == -1) { + fr_perror("radclient"); + return 1; + } + fr_strerror(); /* Clear the error buffer */ + + /* + * Get the request type + */ + if (!isdigit((uint8_t) argv[2][0])) { + packet_code = fr_str2int(request_types, argv[2], -2); + if (packet_code == -2) { + ERROR("Unrecognised request type \"%s\"", argv[2]); + usage(); + } + } else { + packet_code = atoi(argv[2]); + } + + /* + * Resolve hostname. + */ + if (strcmp(argv[1], "-") != 0) { + if (fr_pton_port(&server_ipaddr, &server_port, argv[1], -1, force_af, true) < 0) { + ERROR("%s", fr_strerror()); + exit(1); + } + + /* + * Work backwards from the port to determine the packet type + */ + if (packet_code == PW_CODE_UNDEFINED) packet_code = radclient_get_code(server_port); + } + radclient_get_port(packet_code, &server_port); + + /* + * Add the secret. + */ + if (argv[3]) secret = argv[3]; + + /* + * If no '-f' is specified, we're reading from stdin. + */ + if (rbtree_num_elements(filename_tree) == 0) { + rc_file_pair_t *files; + + files = talloc_zero(talloc_autofree_context(), rc_file_pair_t); + files->packets = "-"; + if (!radclient_init(files, files)) { + exit(1); + } + } + + /* + * Walk over the list of filenames, creating the requests. + */ + if (rbtree_walk(filename_tree, RBTREE_IN_ORDER, filename_walk, NULL) != 0) { + ERROR("Failed parsing input files"); + exit(1); + } + + /* + * No packets read. Die. + */ + if (!request_head) { + ERROR("Nothing to send"); + exit(1); + } + + openssl3_init(); + + /* + * Bind to the first specified IP address and port. + * This means we ignore later ones. + */ + if (request_head->packet->src_ipaddr.af == AF_UNSPEC) { + memset(&client_ipaddr, 0, sizeof(client_ipaddr)); + client_ipaddr.af = server_ipaddr.af; + } else { + client_ipaddr = request_head->packet->src_ipaddr; + } + + client_port = request_head->packet->src_port; + +#ifdef WITH_TCP + if (proto) { + sockfd = fr_socket_client_tcp(NULL, &server_ipaddr, server_port, false); + if (sockfd < 0) { + ERROR("Error opening socket"); + exit(1); + } + } else +#endif + { + sockfd = fr_socket(&client_ipaddr, client_port); + if (sockfd < 0) { + ERROR("Error opening socket"); + exit(1); + } + +#ifdef WITH_UDPFROMTO + if (udpfromto_init(sockfd) < 0) { + ERROR("Failed initializing socket"); + exit(1); + } +#endif + } + + pl = fr_packet_list_create(1); + if (!pl) { + ERROR("Out of memory"); + exit(1); + } + + if (!fr_packet_list_socket_add(pl, sockfd, ipproto, +#ifdef WITH_RADIUSV11 + false, +#endif + &server_ipaddr, server_port, NULL)) { + ERROR("Out of memory"); + exit(1); + } + + /* + * Walk over the list of packets, sanity checking + * everything. + */ + for (this = request_head; this != NULL; this = this->next) { + this->packet->src_ipaddr = client_ipaddr; + this->packet->src_port = client_port; + if (radclient_sane(this) != 0) { + exit(1); + } + } + + /* + * Walk over the packets to send, until + * we're all done. + * + * FIXME: This currently busy-loops until it receives + * all of the packets. It should really have some sort of + * send packet, get time to wait, select for time, etc. + * loop. + */ + do { + int n = parallel; + rc_request_t *next; + char const *filename = NULL; + + done = true; + sleep_time = -1; + + /* + * Walk over the packets, sending them. + */ + + for (this = request_head; this != NULL; this = next) { + next = this->next; + + /* + * If there's a packet to receive, + * receive it, but don't wait for a + * packet. + */ + recv_one_packet(0); + + /* + * This packet is done. Delete it. + */ + if (this->done) { + talloc_free(this); + continue; + } + + /* + * Packets from multiple '-f' are sent + * in parallel. + * + * Packets from one file are sent in + * series, unless '-p' is specified, in + * which case N packets from each file + * are sent in parallel. + */ + if (this->files->packets != filename) { + filename = this->files->packets; + n = parallel; + } + + if (n > 0) { + n--; + + /* + * Send the current packet. + */ + if (send_one_packet(this) < 0) { + talloc_free(this); + break; + } + + /* + * Wait a little before sending + * the next packet, if told to. + */ + if (persec) { + struct timeval tv; + + /* + * Don't sleep elsewhere. + */ + sleep_time = 0; + + if (persec == 1) { + tv.tv_sec = 1; + tv.tv_usec = 0; + } else { + tv.tv_sec = 0; + tv.tv_usec = 1000000/persec; + } + + /* + * Sleep for milliseconds, + * portably. + * + * If we get an error or + * a signal, treat it like + * a normal timeout. + */ + select(0, NULL, NULL, NULL, &tv); + } + + /* + * If we haven't sent this packet + * often enough, we're not done, + * and we shouldn't sleep. + */ + if (this->resend < resend_count) { + done = false; + sleep_time = 0; + } + } else { /* haven't sent this packet, we're not done */ + assert(this->done == false); + assert(this->reply == NULL); + done = false; + } + } + + /* + * Still have outstanding requests. + */ + if (fr_packet_list_num_elements(pl) > 0) { + done = false; + } else { + sleep_time = 0; + } + + /* + * Nothing to do until we receive a request, so + * sleep until then. Once we receive one packet, + * we go back, and walk through the whole list again, + * sending more packets (if necessary), and updating + * the sleep time. + */ + if (!done && (sleep_time > 0)) { + recv_one_packet(sleep_time); + } + } while (!done); + + rbtree_free(filename_tree); + fr_packet_list_free(pl); + while (request_head) TALLOC_FREE(request_head); + dict_free(); + + if (do_summary) { + printf("Packet summary:\n" + "\tAccepted : %" PRIu64 "\n" + "\tRejected : %" PRIu64 "\n" + "\tLost : %" PRIu64 "\n" + "\tPassed filter : %" PRIu64 "\n" + "\tFailed filter : %" PRIu64 "\n", + stats.accepted, + stats.rejected, + stats.lost, + stats.passed, + stats.failed + ); + } + + if ((stats.lost > 0) || (stats.failed > 0)) { + exit(1); + } + + openssl3_free(); + + exit(0); +} +DIAG_ON(deprecated-declarations) diff --git a/src/main/radclient.mk b/src/main/radclient.mk new file mode 100644 index 0000000..0db0a8a --- /dev/null +++ b/src/main/radclient.mk @@ -0,0 +1,8 @@ +TARGET := radclient +SOURCES := radclient.c ${top_srcdir}/src/modules/rlm_mschap/smbdes.c \ + ${top_srcdir}/src/modules/rlm_mschap/mschap.c + +TGT_PREREQS := libfreeradius-radius.a + +SRC_CFLAGS := -I${top_srcdir}/src/modules/rlm_mschap +TGT_LDLIBS := $(LIBS) diff --git a/src/main/radiusd.c b/src/main/radiusd.c new file mode 100644 index 0000000..f2acec7 --- /dev/null +++ b/src/main/radiusd.c @@ -0,0 +1,794 @@ +/* + * radiusd.c Main loop of the radius server. + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2000-2019 The FreeRADIUS server project + * Copyright 1999,2000 Miquel van Smoorenburg <miquels@cistron.nl> + * Copyright 2000 Alan DeKok <aland@ox.org> + * Copyright 2000 Alan Curry <pacman-radius@cqc.com> + * Copyright 2000 Jeff Carneal <jeff@apex.net> + * Copyright 2000 Chad Miller <cmiller@surfsouth.com> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/modules.h> +#include <freeradius-devel/state.h> +#include <freeradius-devel/rad_assert.h> +#include <freeradius-devel/autoconf.h> + +#include <sys/file.h> + +#include <fcntl.h> +#include <ctype.h> + +#ifdef HAVE_GETOPT_H +# include <getopt.h> +#endif + +#ifdef HAVE_SYS_WAIT_H +# include <sys/wait.h> +#endif +#ifndef WEXITSTATUS +# define WEXITSTATUS(stat_val) ((unsigned)(stat_val) >> 8) +#endif +#ifndef WIFEXITED +# define WIFEXITED(stat_val) (((stat_val) & 255) == 0) +#endif + +#ifdef HAVE_SYSTEMD +# include <systemd/sd-daemon.h> +#endif + +/* + * Global variables. + */ +char const *radacct_dir = NULL; +char const *radlog_dir = NULL; + +bool log_stripped_names; + +char const *radiusd_version = "FreeRADIUS Version " RADIUSD_VERSION_STRING +#ifdef RADIUSD_VERSION_COMMIT +" (git #" STRINGIFY(RADIUSD_VERSION_COMMIT) ")" +#endif +", for host " HOSTINFO +#ifndef ENABLE_REPRODUCIBLE_BUILDS +", built on " __DATE__ " at " __TIME__ +#endif +; + +static pid_t radius_pid; + +/* + * Configuration items. + */ + +/* + * Static functions. + */ +static void usage(int); + +static void sig_fatal (int); +#ifdef SIGHUP +static void sig_hup (int); +#endif + +/* + * The main guy. + */ +int main(int argc, char *argv[]) +{ + int rcode = EXIT_SUCCESS; + int status; + int argval; + bool spawn_flag = true; + bool display_version = false; + int flag = 0; + int from_child[2] = {-1, -1}; + char *p; + fr_state_t *state = NULL; + + /* + * We probably don't want to free the talloc autofree context + * directly, so we'll allocate a new context beneath it, and + * free that before any leak reports. + */ + TALLOC_CTX *autofree = talloc_init("main"); + +#ifdef OSFC2 + set_auth_parameters(argc, argv); +#endif + +#ifdef WIN32 + { + WSADATA wsaData; + if (WSAStartup(MAKEWORD(2, 0), &wsaData)) { + fprintf(stderr, "%s: Unable to initialize socket library.\n", + main_config.name); + exit(EXIT_FAILURE); + } + } +#endif + + rad_debug_lvl = 0; + set_radius_dir(autofree, RADIUS_DIR); + + /* + * Ensure that the configuration is initialized. + */ + memset(&main_config, 0, sizeof(main_config)); + main_config.myip.af = AF_UNSPEC; + main_config.port = 0; + main_config.daemonize = true; + + p = strrchr(argv[0], FR_DIR_SEP); + if (!p) { + main_config.name = argv[0]; + } else { + main_config.name = p + 1; + } + + /* + * Don't put output anywhere until we get told a little + * more. + */ + default_log.dst = L_DST_NULL; + default_log.fd = -1; + main_config.log_file = NULL; + + /* Process the options. */ + while ((argval = getopt(argc, argv, "Cd:D:fhi:l:mMn:p:PstvxX")) != EOF) { + + switch (argval) { + case 'C': + check_config = true; + spawn_flag = false; + main_config.daemonize = false; + break; + + case 'd': + set_radius_dir(autofree, optarg); + break; + + case 'D': + main_config.dictionary_dir = talloc_typed_strdup(autofree, optarg); + break; + + case 'f': + main_config.daemonize = false; + break; + + case 'h': + usage(0); + break; + + case 'l': + if (strcmp(optarg, "stdout") == 0) { + goto do_stdout; + } + main_config.log_file = strdup(optarg); + default_log.dst = L_DST_FILES; + default_log.fd = open(main_config.log_file, + O_WRONLY | O_APPEND | O_CREAT, 0640); + if (default_log.fd < 0) { + fprintf(stderr, "%s: Failed to open log file %s: %s\n", + main_config.name, main_config.log_file, fr_syserror(errno)); + exit(EXIT_FAILURE); + } + fr_log_fp = fdopen(default_log.fd, "a"); + break; + + case 'i': + if (ip_hton(&main_config.myip, AF_UNSPEC, optarg, false) < 0) { + fprintf(stderr, "%s: Invalid IP Address or hostname \"%s\"\n", + main_config.name, optarg); + exit(EXIT_FAILURE); + } + flag |= 1; + break; + + case 'n': + main_config.name = optarg; + break; + + case 'm': + main_config.debug_memory = true; + break; + + case 'M': + main_config.memory_report = true; + main_config.debug_memory = true; + break; + + case 'p': + { + unsigned long port; + + port = strtoul(optarg, 0, 10); + if ((port == 0) || (port > UINT16_MAX)) { + fprintf(stderr, "%s: Invalid port number \"%s\"\n", + main_config.name, optarg); + exit(EXIT_FAILURE); + } + + main_config.port = (uint16_t) port; + flag |= 2; + } + break; + + case 'P': + /* Force the PID to be written, even in -f mode */ + main_config.write_pid = true; + break; + + case 's': /* Single process mode */ + spawn_flag = false; + main_config.daemonize = false; + break; + + case 't': /* no child threads */ + spawn_flag = false; + break; + + case 'v': + display_version = true; + break; + + case 'X': + spawn_flag = false; + main_config.daemonize = false; + rad_debug_lvl += 2; + main_config.log_auth = true; + main_config.log_auth_badpass = true; + main_config.log_auth_goodpass = true; + do_stdout: + fr_log_fp = stdout; + default_log.dst = L_DST_STDOUT; + default_log.fd = STDOUT_FILENO; + break; + + case 'x': + rad_debug_lvl++; + break; + + default: + usage(1); + break; + } + } + + /* + * Mismatch between the binary and the libraries it depends on. + */ + if (fr_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) { + fr_perror("%s", main_config.name); + exit(EXIT_FAILURE); + } + + if (rad_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) exit(EXIT_FAILURE); + + /* + * Mismatch between build time OpenSSL and linked SSL, better to die + * here than segfault later. + */ +#ifdef HAVE_OPENSSL_CRYPTO_H + if (ssl_check_consistency() < 0) exit(EXIT_FAILURE); +#endif + + if (flag && (flag != 0x03)) { + fprintf(stderr, "%s: The options -i and -p cannot be used individually.\n", + main_config.name); + exit(EXIT_FAILURE); + } + + /* + * According to the talloc peeps, no two threads may modify any part of + * a ctx tree with a common root without synchronisation. + * + * So we can't run with a null context and threads. + */ + if (main_config.memory_report) { + if (spawn_flag) { + fprintf(stderr, "%s: The server cannot produce memory reports (-M) in threaded mode\n", + main_config.name); + exit(EXIT_FAILURE); + } + talloc_enable_null_tracking(); + } else { + talloc_disable_null_tracking(); + } + + /* + * Better here, so it doesn't matter whether we get passed -xv or -vx. + */ + if (display_version) { + if (rad_debug_lvl == 0) rad_debug_lvl = 1; + fr_log_fp = stdout; + default_log.dst = L_DST_STDOUT; + default_log.fd = STDOUT_FILENO; + + INFO("%s: %s", main_config.name, radiusd_version); + version_print(); + exit(EXIT_SUCCESS); + } + + if (rad_debug_lvl) version_print(); + + /* + * Under linux CAP_SYS_PTRACE is usually only available before setuid/setguid, + * so we need to check whether we can attach before calling those functions + * (in main_config_init()). + */ + fr_store_debug_state(); + + /* + * Initialising OpenSSL once, here, is safer than having individual modules do it. + */ +#ifdef HAVE_OPENSSL_CRYPTO_H + if (tls_global_init(spawn_flag, check_config) < 0) exit(EXIT_FAILURE); +#endif + + /* + * Write the PID always if we're running as a daemon. + */ + if (main_config.daemonize) main_config.write_pid = true; + + /* + * Read the configuration files, BEFORE doing anything else. + */ + if (main_config_init() < 0) exit(EXIT_FAILURE); + + /* + * This is very useful in figuring out why the panic_action didn't fire. + */ + INFO("%s", fr_debug_state_to_msg(fr_debug_state)); + + /* + * Check for vulnerabilities in the version of libssl were linked against. + */ +#if defined(HAVE_OPENSSL_CRYPTO_H) && defined(ENABLE_OPENSSL_VERSION_CHECK) + if (tls_global_version_check(main_config.allow_vulnerable_openssl) < 0) exit(EXIT_FAILURE); +#endif + + fr_talloc_fault_setup(); + + /* + * Set the panic action (if required) + */ + { + char const *panic_action = NULL; + + panic_action = getenv("PANIC_ACTION"); + if (!panic_action) panic_action = main_config.panic_action; + + if (panic_action && (fr_fault_setup(panic_action, argv[0]) < 0)) { + fr_perror("%s", main_config.name); + exit(EXIT_FAILURE); + } + } + + /* + * The systemd watchdog enablement must be checked before we + * daemonize, but the notifications can come from any process. + */ +#ifdef HAVE_SYSTEMD_WATCHDOG + if (!check_config) { + uint64_t usec; + + if ((sd_watchdog_enabled(0, &usec) > 0) && (usec > 0)) { + usec /= 2; + fr_timeval_from_usec(&sd_watchdog_interval, usec); + + INFO("systemd watchdog interval is %ld.%.2ld secs", sd_watchdog_interval.tv_sec, sd_watchdog_interval.tv_usec); + } else { + INFO("systemd watchdog is disabled"); + } + } +#else + /* + * Some users get frustrated due to can't handle the service using "systemctl start radiusd" + * even when the SO supports systemd. The reason is because the FreeRADIUS version was built + * without the proper support. + * + * Then, as can be seen in https://www.systutorials.com/docs/linux/man/3-sd_notify/ + * We could assume that if find the NOTIFY_SOCKET, it's because we are under systemd. + * + */ + if (getenv("NOTIFY_SOCKET")) + WARN("Built without support for systemd watchdog, but running under systemd."); +#endif + +#ifndef __MINGW32__ + /* + * Disconnect from session + */ + if (main_config.daemonize) { + pid_t pid; + int devnull; + + /* + * Really weird things happen if we leave stdin open and call things like + * system() later. + */ + devnull = open("/dev/null", O_RDWR); + if (devnull < 0) { + ERROR("Failed opening /dev/null: %s", fr_syserror(errno)); + exit(EXIT_FAILURE); + } + dup2(devnull, STDIN_FILENO); + + close(devnull); + + if (pipe(from_child) != 0) { + ERROR("Couldn't open pipe for child status: %s", fr_syserror(errno)); + exit(EXIT_FAILURE); + } + + pid = fork(); + if (pid < 0) { + ERROR("Couldn't fork: %s", fr_syserror(errno)); + exit(EXIT_FAILURE); + } + + /* + * The parent exits, so the child can run in the background. + * + * As the child can still encounter an error during initialisation + * we do a blocking read on a pipe between it and the parent. + * + * Just before entering the event loop the child will send a success + * or failure message to the parent, via the pipe. + */ + if (pid > 0) { + uint8_t ret = 0; + int stat_loc; + + /* So the pipe is correctly widowed if the child exits */ + close(from_child[1]); + + /* + * The child writes a 0x01 byte on success, and closes + * the pipe on error. + */ + if ((read(from_child[0], &ret, 1) < 0)) { + ret = 0; + } + + /* For cleanliness... */ + close(from_child[0]); + + /* Don't turn children into zombies */ + if (!ret) { + waitpid(pid, &stat_loc, WNOHANG); + exit(EXIT_FAILURE); + } + +#ifdef HAVE_SYSTEMD + /* + * Update the systemd MAINPID to be our child, + * as the parent is about to exit. + */ + sd_notifyf(0, "MAINPID=%lu", (unsigned long)pid); +#endif + + exit(EXIT_SUCCESS); + } + + /* so the pipe is correctly widowed if the parent exits?! */ + close(from_child[0]); +# ifdef HAVE_SETSID + setsid(); +# endif + } +#endif + + /* + * Ensure that we're using the CORRECT pid after forking, NOT the one + * we started with. + */ + radius_pid = getpid(); + + /* + * Initialize any event loops just enough so module instantiations can + * add fd/event to them, but do not start them yet. + * + * This has to be done post-fork in case we're using kqueue, where the + * queue isn't inherited by the child process. + */ + if (!radius_event_init(autofree)) exit(EXIT_FAILURE); + + /* + * Load the modules + */ + if (modules_init(main_config.config) < 0) exit(EXIT_FAILURE); + + /* + * Redirect stderr/stdout as appropriate. + */ + if (radlog_init(&default_log, main_config.daemonize) < 0) { + ERROR("%s", fr_strerror()); + exit(EXIT_FAILURE); + } + + event_loop_started = true; + + /* + * Start the event loop(s) and threads. + */ + radius_event_start(main_config.config, spawn_flag); + + /* + * Now that we've set everything up, we can install the signal + * handlers. Before this, if we get any signal, we don't know + * what to do, so we might as well do the default, and die. + */ +#ifdef SIGPIPE + signal(SIGPIPE, SIG_IGN); +#endif + + if ((fr_set_signal(SIGHUP, sig_hup) < 0) || + (fr_set_signal(SIGTERM, sig_fatal) < 0)) { + set_signal_error: + ERROR("%s", fr_strerror()); + exit(EXIT_FAILURE); + } + + /* + * If we're debugging, then a CTRL-C will cause the server to die + * immediately. Use SIGTERM to shut down the server cleanly in + * that case. + */ + if (fr_set_signal(SIGINT, sig_fatal) < 0) goto set_signal_error; + +#ifdef SIGQUIT + if (main_config.debug_memory || (rad_debug_lvl == 0)) { + if (fr_set_signal(SIGQUIT, sig_fatal) < 0) goto set_signal_error; + } +#endif + + /* + * Everything seems to have loaded OK, exit gracefully. + */ + if (check_config) { + DEBUG("Configuration appears to be OK"); + + /* for -C -m|-M */ + if (main_config.debug_memory) goto cleanup; + + exit(EXIT_SUCCESS); + } + +#ifdef WITH_STATS + radius_stats_init(0); +#endif + + /* + * Write the PID after we've forked, so that we write the correct one. + */ + if (main_config.write_pid) { + FILE *fp; + + fp = fopen(main_config.pid_file, "w"); + if (fp != NULL) { + /* + * @fixme What about following symlinks, + * and having it over-write a normal file? + */ + fprintf(fp, "%d\n", (int) radius_pid); + fclose(fp); + } else { + ERROR("Failed creating PID file %s: %s", main_config.pid_file, fr_syserror(errno)); + exit(EXIT_FAILURE); + } + } + + exec_trigger(NULL, NULL, "server.start", false); + + /* + * Inform the parent (who should still be waiting) that the rest of + * initialisation went OK, and that it should exit with a 0 status. + * If we don't get this far, then we just close the pipe on exit, and the + * parent gets a read failure. + */ + if (main_config.daemonize) { + if (write(from_child[1], "\001", 1) < 0) { + WARN("Failed informing parent of successful start: %s", + fr_syserror(errno)); + } + close(from_child[1]); + } + + /* + * Clear the libfreeradius error buffer. + */ + fr_strerror(); + + /* + * Initialise the state rbtree (used to link multiple rounds of challenges). + */ + state = fr_state_init(NULL); + +#ifdef HAVE_SYSTEMD + { + int ret_notif; + + ret_notif = sd_notify(0, "READY=1\nSTATUS=Processing requests"); + if (ret_notif < 0) + WARN("Failed notifying systemd that process is READY: %s", fr_syserror(ret_notif)); + } +#endif + + /* + * Process requests until HUP or exit. + */ + while ((status = radius_event_process()) == 0x80) { +#ifdef WITH_STATS + radius_stats_init(1); +#endif + main_config_hup(); + } + if (status < 0) { + ERROR("Exiting due to internal error: %s", fr_strerror()); + rcode = EXIT_FAILURE; + } else { + INFO("Exiting normally"); + } + + /* + * Ignore the TERM signal: we're about to die. + */ + signal(SIGTERM, SIG_IGN); + + /* + * Fire signal and stop triggers after ignoring SIGTERM, so handlers are + * not killed with the rest of the process group, below. + */ + if (status == 2) exec_trigger(NULL, NULL, "server.signal.term", true); + exec_trigger(NULL, NULL, "server.stop", false); + + /* + * Send a TERM signal to all associated processes + * (including us, which gets ignored.) + */ +#ifndef __MINGW32__ + if (spawn_flag) kill(-radius_pid, SIGTERM); +#endif + + /* + * We're exiting, so we can delete the PID file. + * (If it doesn't exist, we can ignore the error returned by unlink) + */ + if (main_config.daemonize) unlink(main_config.pid_file); + + radius_event_free(); + +cleanup: + /* + * Detach any modules. + */ + modules_free(); + + xlat_free(); /* modules may have xlat's */ + + fr_state_delete(state); + + /* + * Free the configuration items. + */ + main_config_free(); + +#ifdef WITH_COA_TUNNEL + /* + * This should be after freeing all of the listeners. + */ + listen_coa_free(); +#endif + +#ifdef WIN32 + WSACleanup(); +#endif + +#ifdef HAVE_OPENSSL_CRYPTO_H + tls_global_cleanup(); +#endif + + /* + * So we don't see autofreed memory in the talloc report + */ + talloc_free(autofree); + + if (main_config.memory_report) { + INFO("Allocated memory at time of report:"); + fr_log_talloc_report(NULL); + talloc_disable_null_tracking(); + } + + return rcode; +} + + +/* + * Display the syntax for starting this program. + */ +static void NEVER_RETURNS usage(int status) +{ + FILE *output = status?stderr:stdout; + + fprintf(output, "Usage: %s [options]\n", main_config.name); + fprintf(output, "Options:\n"); + fprintf(output, " -C Check configuration and exit.\n"); + fprintf(stderr, " -d <raddb> Set configuration directory (defaults to " RADDBDIR ").\n"); + fprintf(stderr, " -D <dictdir> Set main dictionary directory (defaults to " DICTDIR ").\n"); + fprintf(output, " -f Run as a foreground process, not a daemon.\n"); + fprintf(output, " -h Print this help message.\n"); + fprintf(output, " -i <ipaddr> Listen on ipaddr ONLY.\n"); + fprintf(output, " -l <log_file> Logging output will be written to this file.\n"); + fprintf(output, " -m On SIGINT or SIGQUIT clean up all used memory instead of just exiting.\n"); + fprintf(output, " -n <name> Read raddb/name.conf instead of raddb/radiusd.conf.\n"); + fprintf(output, " -p <port> Listen on port ONLY.\n"); + fprintf(output, " -P Always write out PID, even with -f.\n"); + fprintf(output, " -s Do not spawn child processes to handle requests (same as -ft).\n"); + fprintf(output, " -t Disable threads.\n"); + fprintf(output, " -v Print server version information.\n"); + fprintf(output, " -X Turn on full debugging (similar to -tfxxl stdout).\n"); + fprintf(output, " -x Turn on additional debugging (-xx gives more debugging).\n"); + exit(status); +} + + +/* + * We got a fatal signal. + */ +static void sig_fatal(int sig) +{ + if (getpid() != radius_pid) _exit(sig); + + switch (sig) { + case SIGTERM: + radius_signal_self(RADIUS_SIGNAL_SELF_TERM); + break; + + case SIGINT: +#ifdef SIGQUIT + case SIGQUIT: +#endif + if (main_config.debug_memory || main_config.memory_report) { + radius_signal_self(RADIUS_SIGNAL_SELF_TERM); + break; + } + /* FALL-THROUGH */ + + default: + fr_exit(sig); + } +} + +#ifdef SIGHUP +/* + * We got the hangup signal. + * Re-read the configuration files. + */ +static void sig_hup(UNUSED int sig) +{ + reset_signal(SIGHUP, sig_hup); + + radius_signal_self(RADIUS_SIGNAL_SELF_HUP); +} +#endif diff --git a/src/main/radiusd.mk b/src/main/radiusd.mk new file mode 100644 index 0000000..651413a --- /dev/null +++ b/src/main/radiusd.mk @@ -0,0 +1,21 @@ +TARGET := radiusd +SOURCES := acct.c auth.c client.c crypt.c files.c \ + listen.c mainconfig.c modules.c modcall.c \ + radiusd.c state.c stats.c soh.c connection.c \ + session.c threads.c channel.c \ + process.c realms.c detail.c +ifneq ($(OPENSSL_LIBS),) +SOURCES += cb.c tls.c tls_listen.c +endif + +SRC_CFLAGS := -DHOSTINFO=\"${HOSTINFO}\" + +TGT_INSTALLDIR := ${sbindir} +TGT_LDLIBS := $(LIBS) $(OPENSSL_LIBS) $(SYSTEMD_LIBS) $(LCRYPT) +TGT_PREREQS := libfreeradius-server.a libfreeradius-radius.a + +# Libraries can't depend on libraries (oops), so make the binary +# depend on the EAP code... +ifneq "$(filter rlm_eap_%,${ALL_TGTS})" "" +TGT_PREREQS += libfreeradius-eap.a +endif diff --git a/src/main/radlast.in b/src/main/radlast.in new file mode 100755 index 0000000..69867bb --- /dev/null +++ b/src/main/radlast.in @@ -0,0 +1,7 @@ +#! /bin/sh + +prefix=@prefix@ +localstatedir=@localstatedir@ +logdir=@logdir@ + +exec last -f $logdir/radwtmp "$@" diff --git a/src/main/radlast.mk b/src/main/radlast.mk new file mode 100644 index 0000000..766ce1f --- /dev/null +++ b/src/main/radlast.mk @@ -0,0 +1,5 @@ +install: $(R)$(bindir)/radlast + +$(R)$(bindir)/radlast: src/main/radlast | $(R)$(bindir) + @echo INSTALL $(notdir $<) + @$(INSTALL) -m 755 $< $(R)$(bindir) diff --git a/src/main/radmin.c b/src/main/radmin.c new file mode 100644 index 0000000..badc186 --- /dev/null +++ b/src/main/radmin.c @@ -0,0 +1,773 @@ +/* + * radmin.c RADIUS Administration tool. + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2012 The FreeRADIUS server project + * Copyright 2012 Alan DeKok <aland@deployingradius.com> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/md5.h> +#include <freeradius-devel/channel.h> + +#include <pwd.h> +#include <grp.h> + +#ifdef HAVE_GETOPT_H +# include <getopt.h> +#endif + +#ifdef HAVE_SYS_STAT_H +# include <sys/stat.h> +#endif + +#ifndef READLINE_MAX_HISTORY_LINES +# define READLINE_MAX_HISTORY_LINES 1000 +#endif + +#ifdef HAVE_LIBREADLINE + +# include <stdio.h> +#if defined(HAVE_READLINE_READLINE_H) +# include <readline/readline.h> +# define USE_READLINE (1) +#elif defined(HAVE_READLINE_H) +# include <readline.h> +# define USE_READLINE (1) +#endif /* !defined(HAVE_READLINE_H) */ + +#ifdef HAVE_READLINE_HISTORY +# if defined(HAVE_READLINE_HISTORY_H) +# include <readline/history.h> +# define USE_READLINE_HISTORY (1) +# elif defined(HAVE_HISTORY_H) +# include <history.h> +# define USE_READLINE_HISTORY (1) +#endif /* defined(HAVE_READLINE_HISTORY_H) */ + +#endif /* HAVE_READLINE_HISTORY */ + +#endif /* HAVE_LIBREADLINE */ + +/* + * For configuration file stuff. + */ +static char const *progname = "radmin"; +static char const *radmin_version = "radmin version " RADIUSD_VERSION_STRING +#ifdef RADIUSD_VERSION_COMMIT +" (git #" STRINGIFY(RADIUSD_VERSION_COMMIT) ")" +#endif +#ifndef ENABLE_REPRODUCIBLE_BUILDS +", built on " __DATE__ " at " __TIME__ +#endif +; + + +/* + * The rest of this is because the conffile.c, etc. assume + * they're running inside of the server. And we don't (yet) + * have a "libfreeradius-server", or "libfreeradius-util". + */ +main_config_t main_config; + +static bool echo = false; +static char const *secret = "testing123"; + +#include <sys/wait.h> + +#ifdef HAVE_PTHREAD_H +pid_t rad_fork(void) +{ + return fork(); +} +pid_t rad_waitpid(pid_t pid, int *status) +{ + return waitpid(pid, status, 0); +} +#endif + +static void NEVER_RETURNS usage(int status) +{ + FILE *output = status ? stderr : stdout; + fprintf(output, "Usage: %s [ args ]\n", progname); + fprintf(output, " -d raddb_dir Configuration files are in \"raddbdir/*\".\n"); + fprintf(output, " -D <dictdir> Set main dictionary directory (defaults to " DICTDIR ").\n"); + fprintf(output, " -e command Execute 'command' and then exit.\n"); + fprintf(output, " -E Echo commands as they are being executed.\n"); + fprintf(output, " -f socket_file Open socket_file directly, without reading radius.conf\n"); + fprintf(output, " -h Print usage help information.\n"); + fprintf(output, " -i input_file Read commands from 'input_file'.\n"); + fprintf(output, " -n name Read raddb/name.conf instead of raddb/radiusd.conf\n"); + fprintf(output, " -q Quiet mode.\n"); + fprintf(output, " -v Show program version information.\n"); + + exit(status); +} + +static int client_socket(char const *server) +{ + int sockfd; + uint16_t port; + fr_ipaddr_t ipaddr; + char *p, buffer[1024]; + + strlcpy(buffer, server, sizeof(buffer)); + + p = strchr(buffer, ':'); + if (!p) { + port = PW_RADMIN_PORT; + } else { + port = atoi(p + 1); + *p = '\0'; + } + + if (ip_hton(&ipaddr, AF_INET, buffer, false) < 0) { + fprintf(stderr, "%s: Failed looking up host %s: %s\n", + progname, buffer, fr_syserror(errno)); + exit(1); + } + + sockfd = fr_socket_client_tcp(NULL, &ipaddr, port, false); + if (sockfd < 0) { + fprintf(stderr, "%s: Failed opening socket %s: %s\n", + progname, server, fr_syserror(errno)); + exit(1); + } + + return sockfd; +} + +static ssize_t do_challenge(int sockfd) +{ + ssize_t r; + fr_channel_type_t channel; + uint8_t challenge[16]; + + challenge[0] = 0; + + /* + * When connecting over a socket, the server challenges us. + */ + r = fr_channel_read(sockfd, &channel, challenge, sizeof(challenge)); + if (r <= 0) return r; + + if ((r != 16) || (channel != FR_CHANNEL_AUTH_CHALLENGE)) { + fprintf(stderr, "%s: Failed to read challenge.\n", + progname); + exit(1); + } + + fr_hmac_md5(challenge, (uint8_t const *) secret, strlen(secret), + challenge, sizeof(challenge)); + + r = fr_channel_write(sockfd, FR_CHANNEL_AUTH_RESPONSE, challenge, sizeof(challenge)); + if (r <= 0) return r; + + /* + * If the server doesn't like us, he just closes the + * socket. So we don't look for an ACK. + */ + + return r; +} + + +/* + * Returns -1 on error. 0 on connection failed. +1 on OK. + */ +static ssize_t run_command(int sockfd, char const *command, + char *buffer, size_t bufsize) +{ + ssize_t r; + uint32_t status; + fr_channel_type_t channel; + + if (echo) { + fprintf(stdout, "%s\n", command); + } + + /* + * Write the text to the socket. + */ + r = fr_channel_write(sockfd, FR_CHANNEL_STDIN, command, strlen(command)); + if (r <= 0) return r; + + while (true) { + r = fr_channel_read(sockfd, &channel, buffer, bufsize - 1); + if (r <= 0) return r; + + buffer[r] = '\0'; /* for C strings */ + + switch (channel) { + case FR_CHANNEL_STDOUT: + fprintf(stdout, "%s", buffer); + break; + + case FR_CHANNEL_STDERR: + fprintf(stderr, "ERROR: %s", buffer); + break; + + case FR_CHANNEL_CMD_STATUS: + if (r < 4) return 1; + + memcpy(&status, buffer, sizeof(status)); + status = ntohl(status); + return status; + + default: + fprintf(stderr, "Unexpected response\n"); + return -1; + } + } + + /* never gets here */ +} + +static int do_connect(int *out, char const *file, char const *server) +{ + int sockfd; + ssize_t r; + fr_channel_type_t channel; + char buffer[65536]; + + uint32_t magic; + + /* + * Close stale file descriptors + */ + if (*out != -1) { + close(*out); + *out = -1; + } + + if (file) { + /* + * FIXME: Get destination from command line, if possible? + */ + sockfd = fr_socket_client_unix(file, false); + if (sockfd < 0) { + fr_perror("radmin"); + if (errno == ENOENT) { + fprintf(stderr, "Perhaps you need to run the commands:"); + fprintf(stderr, "\tcd /etc/raddb\n"); + fprintf(stderr, "\tln -s sites-available/control-socket " + "sites-enabled/control-socket\n"); + fprintf(stderr, "and then re-start the server?\n"); + } + return -1; + } + } else { + sockfd = client_socket(server); + } + + /* + * Only works for BSD, but Linux allows us + * to mask SIGPIPE, so that's fine. + */ +#ifdef SO_NOSIGPIPE + { + int set = 1; + + setsockopt(sockfd, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int)); + } +#endif + + /* + * Set up the initial header data. + */ + magic = 0xf7eead16; + magic = htonl(magic); + memcpy(buffer, &magic, sizeof(magic)); + memset(buffer + sizeof(magic), 0, sizeof(magic)); + + r = fr_channel_write(sockfd, FR_CHANNEL_INIT_ACK, buffer, 8); + if (r <= 0) { + do_close: + fprintf(stderr, "%s: Error in socket: %s\n", + progname, fr_syserror(errno)); + close(sockfd); + return -1; + } + + r = fr_channel_read(sockfd, &channel, buffer + 8, 8); + if (r <= 0) goto do_close; + + if ((r != 8) || (channel != FR_CHANNEL_INIT_ACK) || + (memcmp(buffer, buffer + 8, 8) != 0)) { + fprintf(stderr, "%s: Incompatible versions\n", progname); + close(sockfd); + return -1; + } + + if (server && secret) { + r = do_challenge(sockfd); + if (r <= 0) goto do_close; + } + + *out = sockfd; + + return 0; +} + +#define MAX_COMMANDS (4) + +int main(int argc, char **argv) +{ + int argval; + bool quiet = false; + int sockfd = -1; + char *line = NULL; + ssize_t len; + char const *file = NULL; + char const *name = "radiusd"; + char *p, buffer[65536]; + char const *input_file = NULL; + FILE *inputfp = stdin; + char const *server = NULL; + + char const *radius_dir = RADIUS_DIR; + char const *dict_dir = DICTDIR; +#ifdef USE_READLINE_HISTORY + char history_file[PATH_MAX]; +#endif + + char *commands[MAX_COMMANDS]; + int num_commands = -1; + + int exit_status = EXIT_SUCCESS; + +#ifndef NDEBUG + if (fr_fault_setup(getenv("PANIC_ACTION"), argv[0]) < 0) { + fr_perror("radmin"); + exit(EXIT_FAILURE); + } +#endif + + talloc_set_log_stderr(); + + if ((progname = strrchr(argv[0], FR_DIR_SEP)) == NULL) { + progname = argv[0]; + } else { + progname++; + } + + while ((argval = getopt(argc, argv, "d:D:hi:e:Ef:n:qs:vS")) != EOF) { + switch (argval) { + case 'd': + if (file) { + fprintf(stderr, "%s: -d and -f cannot be used together.\n", progname); + exit(1); + } + if (server) { + fprintf(stderr, "%s: -d and -s cannot be used together.\n", progname); + exit(1); + } + radius_dir = optarg; + break; + + case 'D': + dict_dir = optarg; + break; + + case 'e': + num_commands++; /* starts at -1 */ + if (num_commands >= MAX_COMMANDS) { + fprintf(stderr, "%s: Too many '-e'\n", + progname); + exit(1); + } + + commands[num_commands] = optarg; + break; + + case 'E': + echo = true; + break; + + case 'f': + radius_dir = NULL; + file = optarg; + break; + + default: + case 'h': + usage(0); /* never returns */ + + case 'i': +#ifdef __clang_analyzer__ + if (!optarg) exit(1); +#endif + + if (strcmp(optarg, "-") != 0) { + input_file = optarg; + } + quiet = true; + break; + + case 'n': + name = optarg; + break; + + case 'q': + quiet = true; + break; + + case 's': + if (file) { + fprintf(stderr, "%s: -s and -f cannot be used together.\n", progname); + usage(1); + } + radius_dir = NULL; + server = optarg; + break; + + case 'S': + secret = NULL; + break; + + case 'v': + printf("%s\n", radmin_version); + exit(EXIT_SUCCESS); + } + } + + /* + * Mismatch between the binary and the libraries it depends on + */ + if (fr_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) { + fr_perror("radmin"); + exit(1); + } + + if (radius_dir) { + int rcode; + CONF_SECTION *cs, *subcs; + uid_t uid; + gid_t gid; + char const *uid_name = NULL; + char const *gid_name = NULL; + struct passwd *pwd; + struct group *grp; + + file = NULL; /* MUST read it from the conffile now */ + + snprintf(buffer, sizeof(buffer), "%s/%s.conf", radius_dir, name); + + /* + * Need to read in the dictionaries, else we may get + * validation errors when we try and parse the config. + */ + if (dict_init(dict_dir, RADIUS_DICTIONARY) < 0) { + fr_perror("radmin"); + exit(64); + } + + if (dict_read(radius_dir, RADIUS_DICTIONARY) == -1) { + fr_perror("radmin"); + exit(64); + } + + cs = cf_section_alloc(NULL, "main", NULL); + if (!cs) exit(1); + + if (cf_file_read(cs, buffer) < 0) { + fprintf(stderr, "%s: Errors reading or parsing %s\n", progname, buffer); + talloc_free(cs); + usage(1); + } + + uid = getuid(); + gid = getgid(); + + subcs = NULL; + while ((subcs = cf_subsection_find_next(cs, subcs, "listen")) != NULL) { + char const *value; + CONF_PAIR *cp = cf_pair_find(subcs, "type"); + + if (!cp) continue; + + value = cf_pair_value(cp); + if (!value) continue; + + if (strcmp(value, "control") != 0) continue; + + /* + * Now find the socket name (sigh) + */ + rcode = cf_item_parse(subcs, "socket", FR_ITEM_POINTER(PW_TYPE_STRING, &file), NULL); + if (rcode < 0) { + fprintf(stderr, "%s: Failed parsing listen section 'socket'\n", progname); + exit(1); + } + + if (!file) { + fprintf(stderr, "%s: No path given for socket\n", progname); + usage(1); + } + + /* + * If we're root, just use the first one we find + */ + if (uid == 0) break; + + /* + * Check UID and GID. + */ + rcode = cf_item_parse(subcs, "uid", FR_ITEM_POINTER(PW_TYPE_STRING, &uid_name), NULL); + if (rcode < 0) { + fprintf(stderr, "%s: Failed parsing listen section 'uid'\n", progname); + exit(1); + } + + if (!uid_name) break; + + pwd = getpwnam(uid_name); + if (!pwd) { + fprintf(stderr, "%s: Failed getting UID for user %s: %s\n", progname, uid_name, strerror(errno)); + exit(1); + } + + if (uid != pwd->pw_uid) continue; + + rcode = cf_item_parse(subcs, "gid", FR_ITEM_POINTER(PW_TYPE_STRING, &gid_name), NULL); + if (rcode < 0) { + fprintf(stderr, "%s: Failed parsing listen section 'gid'\n", progname); + exit(1); + } + + if (!gid_name) break; + + grp = getgrnam(gid_name); + if (!grp) { + fprintf(stderr, "%s: Failed getting GID for group %s: %s\n", progname, gid_name, strerror(errno)); + exit(1); + } + + if (gid != grp->gr_gid) continue; + + break; + } + + if (!file) { + fprintf(stderr, "%s: Could not find control socket in %s\n", progname, buffer); + exit(1); + } + } + + if (input_file) { + inputfp = fopen(input_file, "r"); + if (!inputfp) { + fprintf(stderr, "%s: Failed opening %s: %s\n", progname, input_file, fr_syserror(errno)); + exit(1); + } + } + + if (!file && !server) { + fprintf(stderr, "%s: Must use one of '-d' or '-f' or '-s'\n", + progname); + exit(1); + } + + /* + * Check if stdin is a TTY only if input is from stdin + */ + if (input_file && !quiet && !isatty(STDIN_FILENO)) quiet = true; + +#ifdef USE_READLINE + if (!quiet) { +#ifdef USE_READLINE_HISTORY + using_history(); + stifle_history(READLINE_MAX_HISTORY_LINES); + + snprintf(history_file, sizeof(history_file), "%s/%s", getenv("HOME"), ".radmin_history"); + read_history(history_file); +#endif + rl_bind_key('\t', rl_insert); + } +#endif + + /* + * Prevent SIGPIPEs from terminating the process + */ + signal(SIGPIPE, SIG_IGN); + + if (do_connect(&sockfd, file, server) < 0) exit(1); + + /* + * Run one command. + */ + if (num_commands >= 0) { + int i; + + for (i = 0; i <= num_commands; i++) { + len = run_command(sockfd, commands[i], buffer, sizeof(buffer)); + if (len < 0) exit(1); + + if (len == FR_CHANNEL_FAIL) exit_status = EXIT_FAILURE; + } + exit(exit_status); + } + + if (!quiet) { + printf("%s - FreeRADIUS Server administration tool.\n", radmin_version); + printf("Copyright (C) 2008-2019 The FreeRADIUS server project and contributors.\n"); + printf("There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A\n"); + printf("PARTICULAR PURPOSE.\n"); + printf("You may redistribute copies of FreeRADIUS under the terms of the\n"); + printf("GNU General Public License v2.\n"); + } + + /* + * FIXME: Do login? + */ + + while (1) { + int retries; + +#ifndef USE_READLINE + if (!quiet) { + printf("radmin> "); + fflush(stdout); + } +#else + if (!quiet) { + line = readline("radmin> "); + + if (!line) break; + + if (!*line) { + free(line); + continue; + } + +#ifdef USE_READLINE_HISTORY + add_history(line); + write_history(history_file); +#endif + } else /* quiet, or no readline */ +#endif + { + line = fgets(buffer, sizeof(buffer), inputfp); + if (!line) break; + + p = strchr(buffer, '\n'); + if (!p) { + fprintf(stderr, "%s: Input line too long\n", + progname); + exit(1); + } + + *p = '\0'; + + /* + * Strip off leading spaces. + */ + for (p = line; *p != '\0'; p++) { + if ((p[0] == ' ') || + (p[0] == '\t')) { + line = p + 1; + continue; + } + + if (p[0] == '#') { + line = NULL; + break; + } + + break; + } + + /* + * Comments: keep going. + */ + if (!line) continue; + + /* + * Strip off CR / LF + */ + for (p = line; *p != '\0'; p++) { + if ((p[0] == '\r') || + (p[0] == '\n')) { + p[0] = '\0'; + break; + } + } + } + + if (strcmp(line, "reconnect") == 0) { + if (do_connect(&sockfd, file, server) < 0) exit(1); + line = NULL; + continue; + } + + if (strncmp(line, "secret ", 7) == 0) { + if (!secret) { + secret = line + 7; + do_challenge(sockfd); + } + line = NULL; + continue; + } + + /* + * Exit, done, etc. + */ + if ((strcmp(line, "exit") == 0) || + (strcmp(line, "quit") == 0)) { + break; + } + + if (server && !secret) { + fprintf(stderr, "ERROR: You must enter 'secret <SECRET>' before running any commands\n"); + line = NULL; + continue; + } + + retries = 0; + retry: + len = run_command(sockfd, line, buffer, sizeof(buffer)); + if (len < 0) { + if (!quiet) fprintf(stderr, "... reconnecting ...\n"); + + if (do_connect(&sockfd, file, server) < 0) { + exit(1); + } + + retries++; + if (retries < 2) goto retry; + + fprintf(stderr, "Failed to connect to server\n"); + exit(1); + + } else if (len == FR_CHANNEL_SUCCESS) { + break; + + } else if (len == FR_CHANNEL_FAIL) { + exit_status = EXIT_FAILURE; + } + } + + fprintf(stdout, "\n"); + + if (inputfp != stdin) fclose(inputfp); + + return exit_status; +} + diff --git a/src/main/radmin.mk b/src/main/radmin.mk new file mode 100644 index 0000000..9e58e6b --- /dev/null +++ b/src/main/radmin.mk @@ -0,0 +1,7 @@ +TARGET := radmin + +SOURCES := radmin.c channel.c + +TGT_INSTALLDIR := ${sbindir} +TGT_PREREQS := libfreeradius-server.a libfreeradius-radius.a +TGT_LDLIBS := $(LIBS) $(LIBREADLINE) diff --git a/src/main/radsniff.c b/src/main/radsniff.c new file mode 100644 index 0000000..e0d2b65 --- /dev/null +++ b/src/main/radsniff.c @@ -0,0 +1,2683 @@ +/* + * 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 radsniff.c + * @brief Capture, filter, and generate statistics for RADIUS traffic + * + * @copyright 2013 Arran Cudbard-Bell <a.cudbardb@freeradius.org> + * @copyright 2006 The FreeRADIUS server project + * @copyright 2006 Nicolas Baradakis <nicolas.baradakis@cegetel.net> + */ + +RCSID("$Id$") + +#define _LIBRADIUS 1 +#include <time.h> +#include <math.h> +#include <freeradius-devel/libradius.h> +#include <freeradius-devel/event.h> + +#include <freeradius-devel/radpaths.h> +#include <freeradius-devel/conf.h> +#include <freeradius-devel/pcap.h> +#include <freeradius-devel/radsniff.h> + +#ifdef HAVE_COLLECTDC_H +# include <collectd/client.h> +#endif + +#define RS_ASSERT(_x) if (!(_x) && !fr_assert(_x)) exit(1) + +static rs_t *conf; +static struct timeval start_pcap = {0, 0}; +static char timestr[50]; + +static rbtree_t *request_tree = NULL; +static rbtree_t *link_tree = NULL; +static fr_event_list_t *events; +static bool cleanup; + +static int self_pipe[2] = {-1, -1}; //!< Signals from sig handlers + +typedef int (*rbcmp)(void const *, void const *); + +static char const *radsniff_version = "radsniff version " RADIUSD_VERSION_STRING +#ifdef RADIUSD_VERSION_COMMIT +" (git #" STRINGIFY(RADIUSD_VERSION_COMMIT) ")" +#endif +#ifndef ENABLE_REPRODUCIBLE_BUILDS +", built on " __DATE__ " at " __TIME__ +#endif +; + +static int rs_useful_codes[] = { + PW_CODE_ACCESS_REQUEST, //!< RFC2865 - Authentication request + PW_CODE_ACCESS_ACCEPT, //!< RFC2865 - Access-Accept + PW_CODE_ACCESS_REJECT, //!< RFC2865 - Access-Reject + PW_CODE_ACCOUNTING_REQUEST, //!< RFC2866 - Accounting-Request + PW_CODE_ACCOUNTING_RESPONSE, //!< RFC2866 - Accounting-Response + PW_CODE_ACCESS_CHALLENGE, //!< RFC2865 - Access-Challenge + PW_CODE_STATUS_SERVER, //!< RFC2865/RFC5997 - Status Server (request) + PW_CODE_STATUS_CLIENT, //!< RFC2865/RFC5997 - Status Server (response) + PW_CODE_DISCONNECT_REQUEST, //!< RFC3575/RFC5176 - Disconnect-Request + PW_CODE_DISCONNECT_ACK, //!< RFC3575/RFC5176 - Disconnect-Ack (positive) + PW_CODE_DISCONNECT_NAK, //!< RFC3575/RFC5176 - Disconnect-Nak (not willing to perform) + PW_CODE_COA_REQUEST, //!< RFC3575/RFC5176 - CoA-Request + PW_CODE_COA_ACK, //!< RFC3575/RFC5176 - CoA-Ack (positive) + PW_CODE_COA_NAK, //!< RFC3575/RFC5176 - CoA-Nak (not willing to perform) +}; + +static const FR_NAME_NUMBER rs_events[] = { + { "received", RS_NORMAL }, + { "norsp", RS_LOST }, + { "rtx", RS_RTX }, + { "noreq", RS_UNLINKED }, + { "reused", RS_REUSED }, + { "error", RS_ERROR }, + { NULL , -1 } +}; + +static void NEVER_RETURNS usage(int status); + +/** Fork and kill the parent process, writing out our PID + * + * @param pidfile the PID file to write our PID to + */ +static void rs_daemonize(char const *pidfile) +{ + FILE *fp; + pid_t pid, sid; + + pid = fork(); + if (pid < 0) { + exit(EXIT_FAILURE); + } + + /* + * Kill the parent... + */ + if (pid > 0) { + close(self_pipe[0]); + close(self_pipe[1]); + exit(EXIT_SUCCESS); + } + + /* + * Continue as the child. + */ + + /* Create a new SID for the child process */ + sid = setsid(); + if (sid < 0) { + exit(EXIT_FAILURE); + } + + /* + * Change the current working directory. This prevents the current + * directory from being locked; hence not being able to remove it. + */ + if ((chdir("/")) < 0) { + exit(EXIT_FAILURE); + } + + /* + * And write it AFTER we've forked, so that we write the + * correct PID. + */ + fp = fopen(pidfile, "w"); + if (fp != NULL) { + fprintf(fp, "%d\n", (int) sid); + fclose(fp); + } else { + ERROR("Failed creating PID file %s: %s", pidfile, fr_syserror(errno)); + exit(EXIT_FAILURE); + } + + /* + * Close stdout and stderr if they've not been redirected. + */ + if (isatty(fileno(stdout))) { + if (!freopen("/dev/null", "w", stdout)) { + exit(EXIT_FAILURE); + } + } + + if (isatty(fileno(stderr))) { + if (!freopen("/dev/null", "w", stderr)) { + exit(EXIT_FAILURE); + } + } +} + +#define USEC 1000000 +static void rs_tv_sub(struct timeval const *end, struct timeval const *start, struct timeval *elapsed) +{ + elapsed->tv_sec = end->tv_sec - start->tv_sec; + if (elapsed->tv_sec > 0) { + elapsed->tv_sec--; + elapsed->tv_usec = USEC; + } else { + elapsed->tv_usec = 0; + } + elapsed->tv_usec += end->tv_usec; + elapsed->tv_usec -= start->tv_usec; + + if (elapsed->tv_usec >= USEC) { + elapsed->tv_usec -= USEC; + elapsed->tv_sec++; + } +} + +static void rs_tv_add_ms(struct timeval const *start, unsigned long interval, struct timeval *result) { + result->tv_sec = start->tv_sec + (interval / 1000); + result->tv_usec = start->tv_usec + ((interval % 1000) * 1000); + + if (result->tv_usec > USEC) { + result->tv_usec -= USEC; + result->tv_sec++; + } +} + +static void rs_time_print(char *out, size_t len, struct timeval const *t) +{ + size_t ret; + struct timeval now; + uint32_t usec; + + if (!t) { + gettimeofday(&now, NULL); + t = &now; + } + + ret = strftime(out, len, "%Y-%m-%d %H:%M:%S", localtime(&t->tv_sec)); + if (ret >= len) { + return; + } + + usec = t->tv_usec; + + if (usec) { + while (usec < 100000) usec *= 10; + snprintf(out + ret, len - ret, ".%i", usec); + } else { + snprintf(out + ret, len - ret, ".000000"); + } +} + +static size_t rs_prints_csv(char *out, size_t outlen, char const *in, size_t inlen) +{ + char const *start = out; + uint8_t const *str = (uint8_t const *) in; + + if (!in) { + if (outlen) { + *out = '\0'; + } + + return 0; + } + + if (inlen == 0) { + inlen = strlen(in); + } + + while ((inlen > 0) && (outlen > 2)) { + /* + * Escape double quotes with... MORE DOUBLE QUOTES! + */ + if (*str == '"') { + *out++ = '"'; + outlen--; + } + + /* + * Safe chars which require no escaping + */ + if ((*str == '\r') || (*str == '\n') || ((*str >= '\x20') && (*str <= '\x7E'))) { + *out++ = *str++; + outlen--; + inlen--; + + continue; + } + + /* + * Everything else is dropped + */ + str++; + inlen--; + } + *out = '\0'; + + return out - start; +} + +static void rs_packet_print_csv_header(void) +{ + char buffer[2048]; + char *p = buffer; + int i; + + ssize_t len, s = sizeof(buffer); + + len = strlcpy(p, "\"Status\",\"Count\",\"Time\",\"Latency\",\"Type\",\"Interface\"," + "\"Src IP\",\"Src Port\",\"Dst IP\",\"Dst Port\",\"ID\",", s); + p += len; + s -= len; + + if (s <= 0) return; + + for (i = 0; i < conf->list_da_num; i++) { + char const *in; + + *p++ = '"'; + s -= 1; + if (s <= 0) return; + + for (in = conf->list_da[i]->name; *in; in++) { + *p++ = *in; + s -= len; + if (s <= 0) return; + } + + *p++ = '"'; + s -= 1; + if (s <= 0) return; + *p++ = ','; + s -= 1; + if (s <= 0) return; + } + + *--p = '\0'; + + fprintf(stdout , "%s\n", buffer); +} + +static void rs_packet_print_csv(uint64_t count, rs_status_t status, fr_pcap_t *handle, RADIUS_PACKET *packet, + UNUSED struct timeval *elapsed, struct timeval *latency, UNUSED bool response, + bool body) +{ + char const *status_str; + char buffer[2048]; + char *p = buffer; + + char src[INET6_ADDRSTRLEN]; + char dst[INET6_ADDRSTRLEN]; + + ssize_t len, s = sizeof(buffer); + + inet_ntop(packet->src_ipaddr.af, &packet->src_ipaddr.ipaddr, src, sizeof(src)); + inet_ntop(packet->dst_ipaddr.af, &packet->dst_ipaddr.ipaddr, dst, sizeof(dst)); + + status_str = fr_int2str(rs_events, status, NULL); + RS_ASSERT(status_str); + + len = snprintf(p, s, "%s,%" PRIu64 ",%s,", status_str, count, timestr); + p += len; + s -= len; + + if (s <= 0) return; + + if (latency) { + len = snprintf(p, s, "%u.%03u,", + (unsigned int) latency->tv_sec, ((unsigned int) latency->tv_usec / 1000)); + p += len; + s -= len; + } else { + *p = ','; + p += 1; + s -= 1; + } + + if (s <= 0) return; + + /* Status, Type, Interface, Src, Src port, Dst, Dst port, ID */ + if (is_radius_code(packet->code)) { + len = snprintf(p, s, "%s,%s,%s,%i,%s,%i,%i,", fr_packet_codes[packet->code], handle->name, + src, packet->src_port, dst, packet->dst_port, packet->id); + } else { + len = snprintf(p, s, "%u,%s,%s,%i,%s,%i,%i,", packet->code, handle->name, + src, packet->src_port, dst, packet->dst_port, packet->id); + } + p += len; + s -= len; + + if (s <= 0) return; + + if (body) { + int i; + VALUE_PAIR *vp; + + for (i = 0; i < conf->list_da_num; i++) { + vp = fr_pair_find_by_da(packet->vps, conf->list_da[i], TAG_ANY); + if (vp && (vp->vp_length > 0)) { + if (conf->list_da[i]->type == PW_TYPE_STRING) { + *p++ = '"'; + s--; + if (s <= 0) return; + + len = rs_prints_csv(p, s, vp->vp_strvalue, vp->vp_length); + p += len; + s -= len; + if (s <= 0) return; + + *p++ = '"'; + s--; + if (s <= 0) return; + } else { + len = vp_prints_value(p, s, vp, 0); + p += len; + s -= len; + if (s <= 0) return; + } + } + + *p++ = ','; + s -= 1; + if (s <= 0) return; + } + } else { + s -= conf->list_da_num; + if (s <= 0) return; + + memset(p, ',', conf->list_da_num); + p += conf->list_da_num; + } + + *--p = '\0'; + fprintf(stdout , "%s\n", buffer); +} + +static void rs_packet_print_fancy(uint64_t count, rs_status_t status, fr_pcap_t *handle, RADIUS_PACKET *packet, + struct timeval *elapsed, struct timeval *latency, bool response, bool body) +{ + char buffer[2048]; + char *p = buffer; + + char src[INET6_ADDRSTRLEN]; + char dst[INET6_ADDRSTRLEN]; + + ssize_t len, s = sizeof(buffer); + + inet_ntop(packet->src_ipaddr.af, &packet->src_ipaddr.ipaddr, src, sizeof(src)); + inet_ntop(packet->dst_ipaddr.af, &packet->dst_ipaddr.ipaddr, dst, sizeof(dst)); + + /* Only print out status str if something's not right */ + if (status != RS_NORMAL) { + char const *status_str; + + status_str = fr_int2str(rs_events, status, NULL); + RS_ASSERT(status_str); + + len = snprintf(p, s, "** %s ** ", status_str); + p += len; + s -= len; + if (s <= 0) return; + } + + if (is_radius_code(packet->code)) { + len = snprintf(p, s, "%s Id %i %s:%s:%d %s %s:%i ", + fr_packet_codes[packet->code], + packet->id, + handle->name, + response ? dst : src, + response ? packet->dst_port : packet->src_port, + response ? "<-" : "->", + response ? src : dst , + response ? packet->src_port : packet->dst_port); + } else { + len = snprintf(p, s, "%u Id %i %s:%s:%i %s %s:%i ", + packet->code, + packet->id, + handle->name, + response ? dst : src, + response ? packet->dst_port : packet->src_port, + response ? "<-" : "->", + response ? src : dst , + response ? packet->src_port : packet->dst_port); + } + p += len; + s -= len; + if (s <= 0) return; + + if (elapsed) { + len = snprintf(p, s, "+%u.%03u ", + (unsigned int) elapsed->tv_sec, ((unsigned int) elapsed->tv_usec / 1000)); + p += len; + s -= len; + if (s <= 0) return; + } + + if (latency) { + len = snprintf(p, s, "+%u.%03u ", + (unsigned int) latency->tv_sec, ((unsigned int) latency->tv_usec / 1000)); + p += len; + s -= len; + if (s <= 0) return; + } + + *--p = '\0'; + + RIDEBUG("%s", buffer); + + if (body) { + /* + * Print out verbose HEX output + */ + if (conf->print_packet && (fr_debug_lvl > 3)) { + rad_print_hex(packet); + } + + if (conf->print_packet && (fr_debug_lvl > 1)) { + char vector[(AUTH_VECTOR_LEN * 2) + 1]; + + if (packet->vps) { + fr_pair_list_sort(&packet->vps, fr_pair_cmp_by_da_tag); + vp_printlist(fr_log_fp, packet->vps); + } + + fr_bin2hex(vector, packet->vector, AUTH_VECTOR_LEN); + INFO("\tAuthenticator-Field = 0x%s", vector); + } + } +} + +static inline void rs_packet_print(rs_request_t *request, uint64_t count, rs_status_t status, fr_pcap_t *handle, + RADIUS_PACKET *packet, struct timeval *elapsed, struct timeval *latency, + bool response, bool body) +{ + if (!conf->logger) return; + + if (request) request->logged = true; + conf->logger(count, status, handle, packet, elapsed, latency, response, body); +} + +static void rs_stats_print(rs_latency_t *stats, PW_CODE code) +{ + int i; + bool have_rt = false; + + for (i = 0; i <= RS_RETRANSMIT_MAX; i++) { + if (stats->interval.rt[i]) { + have_rt = true; + } + } + + if (!stats->interval.received && !have_rt && !stats->interval.reused) { + return; + } + + if (stats->interval.received || stats->interval.linked) { + INFO("%s counters:", fr_packet_codes[code]); + if (stats->interval.received > 0) { + INFO("\tTotal : %.3lf/s" , stats->interval.received); + } + } + + if (stats->interval.linked > 0) { + INFO("\tLinked : %.3lf/s", stats->interval.linked); + INFO("\tUnlinked : %.3lf/s", stats->interval.unlinked); + INFO("%s latency:", fr_packet_codes[code]); + INFO("\tHigh : %.3lfms", stats->interval.latency_high); + INFO("\tLow : %.3lfms", stats->interval.latency_low); + INFO("\tAverage : %.3lfms", stats->interval.latency_average); + INFO("\tMA : %.3lfms", stats->latency_smoothed); + } + + if (have_rt || stats->interval.lost || stats->interval.reused) { + INFO("%s retransmits & loss:", fr_packet_codes[code]); + + if (stats->interval.lost) { + INFO("\tLost : %.3lf/s", stats->interval.lost); + } + + if (stats->interval.reused) { + INFO("\tID Reused : %.3lf/s", stats->interval.reused); + } + + for (i = 0; i <= RS_RETRANSMIT_MAX; i++) { + if (!stats->interval.rt[i]) { + continue; + } + + if (i != RS_RETRANSMIT_MAX) { + INFO("\tRT (%i) : %.3lf/s", i, stats->interval.rt[i]); + } else { + INFO("\tRT (%i+) : %.3lf/s", i, stats->interval.rt[i]); + } + } + } +} + +/** Query libpcap to see if it dropped any packets + * + * We need to check to see if libpcap dropped any packets and if it did, we need to stop stats output for long + * enough for inaccurate statistics to be cleared out. + * + * @param in pcap handle to check. + * @param interval time between checks (used for debug output) + * @return 0, no drops, -1 we couldn't check, -2 dropped because of buffer exhaustion, -3 dropped because of NIC. + */ +static int rs_check_pcap_drop(fr_pcap_t *in, int interval) { + int ret = 0; + struct pcap_stat pstats; + + if (pcap_stats(in->handle, &pstats) != 0) { + ERROR("%s failed retrieving pcap stats: %s", in->name, pcap_geterr(in->handle)); + return -1; + } + + INFO("\t%s%*s: %.3lf/s", in->name, (int) (10 - strlen(in->name)), "", + ((double) (pstats.ps_recv - in->pstats.ps_recv)) / interval); + + if (pstats.ps_drop - in->pstats.ps_drop > 0) { + ERROR("%s dropped %i packets: Buffer exhaustion", in->name, pstats.ps_drop - in->pstats.ps_drop); + ret = -2; + } + + if (pstats.ps_ifdrop - in->pstats.ps_ifdrop > 0) { + ERROR("%s dropped %i packets: Interface", in->name, pstats.ps_ifdrop - in->pstats.ps_ifdrop); + ret = -3; + } + + in->pstats = pstats; + + return ret; +} + +/** Update smoothed average + * + */ +static void rs_stats_process_latency(rs_latency_t *stats) +{ + /* + * If we didn't link any packets during this interval, we don't have a value to return. + * returning 0 is misleading as it would be like saying the latency had dropped to 0. + * We instead set NaN which libcollectd converts to a 'U' or unknown value. + * + * This will cause gaps in graphs, but is completely legitimate as we are missing data. + * This is unfortunately an effect of being just a passive observer. + */ + if (stats->interval.linked_total == 0) { + double unk = strtod("NAN()", (char **) NULL); + + stats->interval.latency_average = unk; + stats->interval.latency_high = unk; + stats->interval.latency_low = unk; + + /* + * We've not yet been able to determine latency, so latency_smoothed is also NaN + */ + if (stats->latency_smoothed_count == 0) { + stats->latency_smoothed = unk; + } + return; + } + + if (stats->interval.linked_total && stats->interval.latency_total) { + stats->interval.latency_average = (stats->interval.latency_total / stats->interval.linked_total); + } + + if (isnan(stats->latency_smoothed)) { + stats->latency_smoothed = 0; + } + if (stats->interval.latency_average > 0) { + stats->latency_smoothed_count++; + stats->latency_smoothed += ((stats->interval.latency_average - stats->latency_smoothed) / + ((stats->latency_smoothed_count < 100) ? stats->latency_smoothed_count : 100)); + } +} + +static void rs_stats_process_counters(rs_latency_t *stats) +{ + int i; + + stats->interval.received = ((long double) stats->interval.received_total) / conf->stats.interval; + stats->interval.linked = ((long double) stats->interval.linked_total) / conf->stats.interval; + stats->interval.unlinked = ((long double) stats->interval.unlinked_total) / conf->stats.interval; + stats->interval.reused = ((long double) stats->interval.reused_total) / conf->stats.interval; + stats->interval.lost = ((long double) stats->interval.lost_total) / conf->stats.interval; + + for (i = 0; i < RS_RETRANSMIT_MAX; i++) { + stats->interval.rt[i] = ((long double) stats->interval.rt_total[i]) / conf->stats.interval; + } +} + +/** Process stats for a single interval + * + */ +static void rs_stats_process(void *ctx) +{ + size_t i; + size_t rs_codes_len = (sizeof(rs_useful_codes) / sizeof(*rs_useful_codes)); + fr_pcap_t *in_p; + rs_update_t *this = ctx; + rs_stats_t *stats = this->stats; + struct timeval now; + + gettimeofday(&now, NULL); + + stats->intervals++; + + INFO("######### Stats Iteration %i #########", stats->intervals); + + /* + * Verify that none of the pcap handles have dropped packets. + */ + INFO("Interface capture rate:"); + for (in_p = this->in; + in_p; + in_p = in_p->next) { + if (rs_check_pcap_drop(in_p, conf->stats.interval) < 0) { + ERROR("Muting stats for the next %i milliseconds", conf->stats.timeout); + + rs_tv_add_ms(&now, conf->stats.timeout, &stats->quiet); + goto clear; + } + } + + if ((stats->quiet.tv_sec + (stats->quiet.tv_usec / 1000000.0)) - + (now.tv_sec + (now.tv_usec / 1000000.0)) > 0) { + INFO("Stats muted because of warmup, or previous error"); + goto clear; + } + + /* + * Latency stats need a bit more work to calculate the SMA. + * + * No further work is required for codes. + */ + for (i = 0; i < rs_codes_len; i++) { + rs_stats_process_latency(&stats->exchange[rs_useful_codes[i]]); + rs_stats_process_counters(&stats->exchange[rs_useful_codes[i]]); + if (fr_debug_lvl > 0) { + rs_stats_print(&stats->exchange[rs_useful_codes[i]], rs_useful_codes[i]); + } + } + +#ifdef HAVE_COLLECTDC_H + /* + * Update stats in collectd using the complex structures we + * initialised earlier. + */ + if ((conf->stats.out == RS_STATS_OUT_COLLECTD) && conf->stats.handle) { + rs_stats_collectd_do_stats(conf, conf->stats.tmpl, &now); + } +#endif + + clear: + /* + * Rinse and repeat... + */ + for (i = 0; i < rs_codes_len; i++) { + memset(&stats->exchange[rs_useful_codes[i]].interval, 0, + sizeof(stats->exchange[rs_useful_codes[i]].interval)); + } + + { + static fr_event_t *event; + + now.tv_sec += conf->stats.interval; + now.tv_usec = 0; + + if (!fr_event_insert(this->list, rs_stats_process, ctx, &now, &event)) { + ERROR("Failed inserting stats interval event"); + } + } +} + + +/** Update latency statistics for request/response and forwarded packets + * + */ +static void rs_stats_update_latency(rs_latency_t *stats, struct timeval *latency) +{ + double lint; + + stats->interval.linked_total++; + /* More useful is this in milliseconds */ + lint = (latency->tv_sec + (latency->tv_usec / 1000000.0)) * 1000; + if (lint > stats->interval.latency_high) { + stats->interval.latency_high = lint; + } + if (!stats->interval.latency_low || (lint < stats->interval.latency_low)) { + stats->interval.latency_low = lint; + } + stats->interval.latency_total += lint; + +} + +/** Copy a subset of attributes from one list into the other + * + * Should be O(n) if all the attributes exist. List must be pre-sorted. + */ +static int rs_get_pairs(TALLOC_CTX *ctx, VALUE_PAIR **out, VALUE_PAIR *vps, DICT_ATTR const *da[], int num) +{ + vp_cursor_t list_cursor, out_cursor; + VALUE_PAIR *match, *last_match, *copy; + uint64_t count = 0; + int i; + + last_match = vps; + + fr_cursor_init(&list_cursor, &last_match); + fr_cursor_init(&out_cursor, out); + for (i = 0; i < num; i++) { + match = fr_cursor_next_by_da(&list_cursor, da[i], TAG_ANY); + if (!match) { + fr_cursor_init(&list_cursor, &last_match); + continue; + } + + do { + copy = fr_pair_copy(ctx, match); + if (!copy) { + fr_pair_list_free(out); + return -1; + } + fr_cursor_insert(&out_cursor, copy); + last_match = match; + + count++; + } while ((match = fr_cursor_next_by_da(&list_cursor, da[i], TAG_ANY))); + } + + return count; +} + +static int _request_free(rs_request_t *request) +{ + bool ret; + + /* + * If were attempting to cleanup the request, and it's no longer in the request_tree + * something has gone very badly wrong. + */ + if (request->in_request_tree) { + ret = rbtree_deletebydata(request_tree, request); + RS_ASSERT(ret); + } + + if (request->in_link_tree) { + ret = rbtree_deletebydata(link_tree, request); + RS_ASSERT(ret); + } + + if (request->event) { + ret = fr_event_delete(events, &request->event); + RS_ASSERT(ret); + } + + rad_free(&request->packet); + rad_free(&request->expect); + rad_free(&request->linked); + + return 0; +} + +static void rs_packet_cleanup(rs_request_t *request) +{ + + RADIUS_PACKET *packet = request->packet; + uint64_t count = request->id; + + RS_ASSERT(request->stats_req); + RS_ASSERT(!request->rt_rsp || request->stats_rsp); + RS_ASSERT(packet); + + /* + * Don't pollute stats or print spurious messages as radsniff closes. + */ + if (cleanup) { + talloc_free(request); + return; + } + + if (RIDEBUG_ENABLED()) { + rs_time_print(timestr, sizeof(timestr), &request->when); + } + + /* + * Were at packet cleanup time which is when the packet was received + timeout + * and it's not been linked with a forwarded packet or a response. + * + * We now count it as lost. + */ + if (!request->silent_cleanup) { + if (!request->linked) { + if (!request->stats_req) return; + + request->stats_req->interval.lost_total++; + + if (conf->event_flags & RS_LOST) { + /* @fixme We should use flags in the request to indicate whether it's been dumped + * to a PCAP file or logged yet, this simplifies the body logging logic */ + rs_packet_print(request, request->id, RS_LOST, request->in, packet, NULL, NULL, false, + conf->filter_response_vps || !(conf->event_flags & RS_NORMAL)); + } + } + + if ((request->in->type == PCAP_INTERFACE_IN) && request->logged) { + RDEBUG("Cleaning up request packet ID %i", request->expect->id); + } + } + + /* + * Now the request is done, we can update the retransmission stats + */ + if (request->rt_req > RS_RETRANSMIT_MAX) { + request->stats_req->interval.rt_total[RS_RETRANSMIT_MAX]++; + } else { + request->stats_req->interval.rt_total[request->rt_req]++; + } + + if (request->rt_rsp) { + if (request->rt_rsp > RS_RETRANSMIT_MAX) { + request->stats_rsp->interval.rt_total[RS_RETRANSMIT_MAX]++; + } else { + request->stats_rsp->interval.rt_total[request->rt_rsp]++; + } + } + + talloc_free(request); +} + +static void _rs_event(void *ctx) +{ + rs_request_t *request = talloc_get_type_abort(ctx, rs_request_t); + request->event = NULL; + rs_packet_cleanup(request); +} + +/** Wrapper around fr_packet_cmp to strip off the outer request struct + * + */ +static int rs_packet_cmp(rs_request_t const *a, rs_request_t const *b) +{ + return fr_packet_cmp(a->expect, b->expect); +} + +static inline int rs_response_to_pcap(rs_event_t *event, rs_request_t *request, struct pcap_pkthdr const *header, + uint8_t const *data) +{ + if (!event->out) return 0; + + /* + * If we're filtering by response then the requests then the capture buffer + * associated with the request should contain buffered request packets. + */ + if (conf->filter_response && request) { + rs_capture_t *start; + + /* + * Record the current position in the header + */ + start = request->capture_p; + + /* + * Buffer hasn't looped set capture_p to the start of the buffer + */ + if (!start->header) request->capture_p = request->capture; + + /* + * If where capture_p points to, has a header set, write out the + * packet to the PCAP file, looping over the buffer until we + * hit our start point. + */ + if (request->capture_p->header) do { + pcap_dump((void *)event->out->dumper, request->capture_p->header, + request->capture_p->data); + TALLOC_FREE(request->capture_p->header); + TALLOC_FREE(request->capture_p->data); + + /* Reset the pointer to the start of the circular buffer */ + if (request->capture_p++ >= + (request->capture + + sizeof(request->capture) / sizeof(*request->capture))) { + request->capture_p = request->capture; + } + } while (request->capture_p != start); + } + + /* + * Now log the response + */ + pcap_dump((void *)event->out->dumper, header, data); + + return 0; +} + +static inline int rs_request_to_pcap(rs_event_t *event, rs_request_t *request, struct pcap_pkthdr const *header, + uint8_t const *data) +{ + if (!event->out) return 0; + + /* + * If we're filtering by response, then we need to wait to write out the requests + */ + if (conf->filter_response) { + /* Free the old capture */ + if (request->capture_p->header) { + talloc_free(request->capture_p->header); + TALLOC_FREE(request->capture_p->data); + } + + if (!(request->capture_p->header = talloc(request, struct pcap_pkthdr))) return -1; + if (!(request->capture_p->data = talloc_array(request, uint8_t, header->caplen))) { + TALLOC_FREE(request->capture_p->header); + return -1; + } + memcpy(request->capture_p->header, header, sizeof(struct pcap_pkthdr)); + memcpy(request->capture_p->data, data, header->caplen); + + /* Reset the pointer to the start of the circular buffer */ + if (++request->capture_p >= + (request->capture + + sizeof(request->capture) / sizeof(*request->capture))) { + request->capture_p = request->capture; + } + return 0; + } + + pcap_dump((void *)event->out->dumper, header, data); + + return 0; +} + +/* This is the same as immediately scheduling the cleanup event */ +#define RS_CLEANUP_NOW(_x, _s)\ + {\ + _x->silent_cleanup = _s;\ + _x->when = header->ts;\ + rs_packet_cleanup(_x);\ + _x = NULL;\ + } while (0) + +static void rs_packet_process(uint64_t count, rs_event_t *event, struct pcap_pkthdr const *header, uint8_t const *data) +{ + rs_stats_t *stats = event->stats; + struct timeval elapsed = {0, 0}; + struct timeval latency; + + /* + * Pointers into the packet data we just received + */ + ssize_t len; + uint8_t const *p = data; + + ip_header_t const *ip = NULL; /* The IP header */ + ip_header6_t const *ip6 = NULL; /* The IPv6 header */ + udp_header_t const *udp; /* The UDP header */ + uint8_t version; /* IP header version */ + bool response; /* Was it a response code */ + + decode_fail_t reason; /* Why we failed decoding the packet */ + static uint64_t captured = 0; + + rs_status_t status = RS_NORMAL; /* Any special conditions (RTX, Unlinked, ID-Reused) */ + RADIUS_PACKET *current; /* Current packet were processing */ + rs_request_t *original = NULL; + + rs_request_t search; + + memset(&search, 0, sizeof(search)); + + if (!start_pcap.tv_sec) { + start_pcap = header->ts; + } + + if (RIDEBUG_ENABLED()) { + rs_time_print(timestr, sizeof(timestr), &header->ts); + } + + len = fr_pcap_link_layer_offset(data, header->caplen, event->in->link_layer); + if (len < 0) { + REDEBUG("Failed determining link layer header offset"); + return; + } + p += len; + + version = (p[0] & 0xf0) >> 4; + switch (version) { + case 4: + ip = (ip_header_t const *)p; + len = (0x0f & ip->ip_vhl) * 4; /* ip_hl specifies length in 32bit words */ + p += len; + break; + + case 6: + ip6 = (ip_header6_t const *)p; + p += sizeof(ip_header6_t); + + break; + + default: + REDEBUG("IP version invalid %i", version); + return; + } + + /* + * End of variable length bits, do basic check now to see if packet looks long enough + */ + len = (p - data) + sizeof(udp_header_t) + sizeof(radius_packet_t); /* length value */ + if ((size_t) len > header->caplen) { + REDEBUG("Packet too small, we require at least %zu bytes, captured %i bytes", + (size_t) len, header->caplen); + return; + } + + /* + * UDP header validation. + */ + udp = (udp_header_t const *)p; + { + uint16_t udp_len; + ssize_t diff; + + udp_len = ntohs(udp->len); + diff = udp_len - (header->caplen - (p - data)); + /* Truncated data */ + if (diff > 0) { + REDEBUG("Packet too small by %zi bytes, UDP header + Payload should be %hu bytes", + diff, udp_len); + return; + } + +#if 0 + /* + * It seems many probes add trailing garbage to the end + * of each capture frame. This has been observed with + * the F5 and Netscout. + * + * Leaving the code here in case it's ever needed for + * debugging. + */ + else if (diff < 0) { + REDEBUG("Packet too big by %zi bytes, UDP header + Payload should be %hu bytes", + diff * -1, udp_len); + return; + } +#endif + } + if ((version == 4) && conf->verify_udp_checksum) { + uint16_t expected; + + expected = fr_udp_checksum((uint8_t const *) udp, ntohs(udp->len), udp->checksum, + ip->ip_src, ip->ip_dst); + if (udp->checksum != expected) { + REDEBUG("UDP checksum invalid, packet: 0x%04hx calculated: 0x%04hx", + ntohs(udp->checksum), ntohs(expected)); + /* Not a fatal error */ + } + } + p += sizeof(udp_header_t); + + /* + * With artificial talloc memory limits there's a good chance we can + * recover once some requests timeout, so make an effort to deal + * with allocation failures gracefully. + */ + current = rad_alloc(conf, false); + if (!current) { + REDEBUG("Failed allocating memory to hold decoded packet"); + rs_tv_add_ms(&header->ts, conf->stats.timeout, &stats->quiet); + return; + } + + current->timestamp = header->ts; + current->data_len = header->caplen - (p - data); + memcpy(¤t->data, &p, sizeof(current->data)); + + /* + * Populate IP/UDP fields from PCAP data + */ + if (ip) { + current->src_ipaddr.af = AF_INET; + current->src_ipaddr.ipaddr.ip4addr.s_addr = ip->ip_src.s_addr; + + current->dst_ipaddr.af = AF_INET; + current->dst_ipaddr.ipaddr.ip4addr.s_addr = ip->ip_dst.s_addr; + } else { + current->src_ipaddr.af = AF_INET6; + memcpy(current->src_ipaddr.ipaddr.ip6addr.s6_addr, ip6->ip_src.s6_addr, + sizeof(current->src_ipaddr.ipaddr.ip6addr.s6_addr)); + + current->dst_ipaddr.af = AF_INET6; + memcpy(current->dst_ipaddr.ipaddr.ip6addr.s6_addr, ip6->ip_dst.s6_addr, + sizeof(current->dst_ipaddr.ipaddr.ip6addr.s6_addr)); + } + + current->src_port = ntohs(udp->src); + current->dst_port = ntohs(udp->dst); + + if (!rad_packet_ok(current, 0, &reason)) { + REDEBUG("%s", fr_strerror()); + if (conf->event_flags & RS_ERROR) { + rs_packet_print(NULL, count, RS_ERROR, event->in, current, &elapsed, NULL, false, false); + } + rad_free(¤t); + + return; + } + + switch (current->code) { + case PW_CODE_ACCOUNTING_RESPONSE: + case PW_CODE_ACCESS_REJECT: + case PW_CODE_ACCESS_ACCEPT: + case PW_CODE_ACCESS_CHALLENGE: + case PW_CODE_COA_NAK: + case PW_CODE_COA_ACK: + case PW_CODE_DISCONNECT_NAK: + case PW_CODE_DISCONNECT_ACK: + case PW_CODE_STATUS_CLIENT: + { + /* look for a matching request and use it for decoding */ + search.expect = current; + original = rbtree_finddata(request_tree, &search); + + /* + * Verify this code is allowed + */ + if (conf->filter_response_code && (conf->filter_response_code != current->code)) { + drop_response: + RDEBUG2("Response dropped by filter"); + rad_free(¤t); + + /* We now need to cleanup the original request too */ + if (original) { + RS_CLEANUP_NOW(original, true); + } + return; + } + + /* + * Only decode attributes if we want to print them or filter on them + * rad_packet_ok does checks to verify the packet is actually valid. + */ + if (conf->decode_attrs) { + int ret; + FILE *log_fp = fr_log_fp; + + fr_log_fp = NULL; + ret = rad_decode(current, original ? original->expect : NULL, conf->radius_secret); + fr_log_fp = log_fp; + if (ret != 0) { + rad_free(¤t); + REDEBUG("Failed decoding"); + return; + } + } + + /* + * Check if we've managed to link it to a request + */ + if (original) { + /* + * Now verify the packet passes the attribute filter + */ + if (conf->filter_response_vps) { + fr_pair_list_sort(¤t->vps, fr_pair_cmp_by_da_tag); + if (!fr_pair_validate_relaxed(NULL, conf->filter_response_vps, current->vps)) { + goto drop_response; + } + } + + /* + * Is this a retransmission? + */ + if (original->linked) { + status = RS_RTX; + original->rt_rsp++; + + rad_free(&original->linked); + fr_event_delete(event->list, &original->event); + /* + * ...nope it's the first response to a request. + */ + } else { + original->stats_rsp = &stats->exchange[current->code]; + } + + /* + * Insert a callback to remove the request and response + * from the tree after the timeout period. + * The delay is so we can detect retransmissions. + */ + original->linked = talloc_steal(original, current); + rs_tv_add_ms(&header->ts, conf->stats.timeout, &original->when); + if (!fr_event_insert(event->list, _rs_event, original, &original->when, + &original->event)) { + REDEBUG("Failed inserting new event"); + /* + * Delete the original request/event, it's no longer valid + * for statistics. + */ + talloc_free(original); + return; + } + /* + * No request seen, or request was dropped by attribute filter + */ + } else { + /* + * If conf->filter_request_vps are set assume the original request was dropped, + * the alternative is maintaining another 'filter', but that adds + * complexity, reduces max capture rate, and is generally a PITA. + */ + if (conf->filter_request) { + rad_free(¤t); + RDEBUG2("Original request dropped by filter"); + return; + } + + status = RS_UNLINKED; + stats->exchange[current->code].interval.unlinked_total++; + } + + rs_response_to_pcap(event, original, header, data); + response = true; + break; + } + + case PW_CODE_ACCOUNTING_REQUEST: + case PW_CODE_ACCESS_REQUEST: + case PW_CODE_COA_REQUEST: + case PW_CODE_DISCONNECT_REQUEST: + case PW_CODE_STATUS_SERVER: + { + /* + * Verify this code is allowed + */ + if (conf->filter_request_code && (conf->filter_request_code != current->code)) { + drop_request: + + RDEBUG2("Request dropped by filter"); + rad_free(¤t); + + return; + } + + /* + * Only decode attributes if we want to print them or filter on them + * rad_packet_ok does checks to verify the packet is actually valid. + */ + if (conf->decode_attrs) { + int ret; + FILE *log_fp = fr_log_fp; + + fr_log_fp = NULL; + ret = rad_decode(current, NULL, conf->radius_secret); + fr_log_fp = log_fp; + + if (ret != 0) { + rad_free(¤t); + REDEBUG("Failed decoding"); + return; + } + + fr_pair_list_sort(¤t->vps, fr_pair_cmp_by_da_tag); + } + + /* + * Save the request for later matching + */ + search.expect = rad_alloc_reply(current, current); + if (!search.expect) { + REDEBUG("Failed allocating memory to hold expected reply"); + rs_tv_add_ms(&header->ts, conf->stats.timeout, &stats->quiet); + rad_free(¤t); + return; + } + search.expect->code = current->code; + + if ((conf->link_da_num > 0) && current->vps) { + int ret; + ret = rs_get_pairs(current, &search.link_vps, current->vps, conf->link_da, + conf->link_da_num); + if (ret < 0) { + ERROR("Failed extracting RTX linking pairs from request"); + rad_free(¤t); + return; + } + } + + /* + * If we have linking attributes set, attempt to find a request in the linking tree. + */ + if (search.link_vps) { + rs_request_t *tuple; + + original = rbtree_finddata(link_tree, &search); + tuple = rbtree_finddata(request_tree, &search); + + /* + * If the packet we matched using attributes is not the same + * as the packet in the request tree, then we need to clean up + * the packet in the request tree. + */ + if (tuple && (original != tuple)) { + RS_CLEANUP_NOW(tuple, true); + } + /* + * Detect duplicates using the normal 5-tuple of src/dst ips/ports id + */ + } else { + original = rbtree_finddata(request_tree, &search); + if (original && (memcmp(original->expect->vector, current->vector, + sizeof(original->expect->vector)) != 0)) { + /* + * ID reused before the request timed out (which may be an issue)... + */ + if (!original->linked) { + status = RS_REUSED; + stats->exchange[current->code].interval.reused_total++; + /* Occurs regularly downstream of proxy servers (so don't complain) */ + RS_CLEANUP_NOW(original, true); + /* + * ...and before we saw a response (which may be a bigger issue). + */ + } else { + RS_CLEANUP_NOW(original, false); + } + /* else it's a proper RTX with the same src/dst id authenticator/nonce */ + } + } + + /* + * Now verify the packet passes the attribute filter + */ + if (conf->filter_request_vps) { + if (!fr_pair_validate_relaxed(NULL, conf->filter_request_vps, current->vps)) { + goto drop_request; + } + } + + /* + * Is this a retransmission? + */ + if (original) { + status = RS_RTX; + original->rt_req++; + + rad_free(&original->packet); + + /* We may of seen the response, but it may of been lost upstream */ + rad_free(&original->linked); + + original->packet = talloc_steal(original, current); + + /* Request may need to be reinserted as the 5 tuple of the response may of changed */ + if (rs_packet_cmp(original, &search) != 0) { + rbtree_deletebydata(request_tree, original); + } + + rad_free(&original->expect); + original->expect = talloc_steal(original, search.expect); + + /* Disarm the timer for the cleanup event for the original request */ + fr_event_delete(event->list, &original->event); + /* + * ...nope it's a new request. + */ + } else { + original = talloc_zero(conf, rs_request_t); + talloc_set_destructor(original, _request_free); + + original->id = count; + original->in = event->in; + original->stats_req = &stats->exchange[current->code]; + + /* Set the packet pointer to the start of the buffer*/ + original->capture_p = original->capture; + + original->packet = talloc_steal(original, current); + original->expect = talloc_steal(original, search.expect); + + if (search.link_vps) { + bool ret; + vp_cursor_t cursor; + VALUE_PAIR *vp; + + for (vp = fr_cursor_init(&cursor, &search.link_vps); + vp; + vp = fr_cursor_next(&cursor)) { + fr_pair_steal(original, search.link_vps); + } + original->link_vps = search.link_vps; + + /* We should never have conflicts */ + ret = rbtree_insert(link_tree, original); + RS_ASSERT(ret); + original->in_link_tree = true; + } + + /* + * Special case for when were filtering by response, + * we never count any requests as lost, because we + * don't know what the response to that request would + * of been. + */ + if (conf->filter_response_vps) { + original->silent_cleanup = true; + } + } + + if (!original->in_request_tree) { + bool ret; + + /* We should never have conflicts */ + ret = rbtree_insert(request_tree, original); + RS_ASSERT(ret); + original->in_request_tree = true; + } + + /* + * Insert a callback to remove the request from the tree + */ + original->packet->timestamp = header->ts; + rs_tv_add_ms(&header->ts, conf->stats.timeout, &original->when); + if (!fr_event_insert(event->list, _rs_event, original, + &original->when, &original->event)) { + REDEBUG("Failed inserting new event"); + + talloc_free(original); + return; + } + rs_request_to_pcap(event, original, header, data); + response = false; + break; + } + + default: + REDEBUG("Unsupported code %i", current->code); + rad_free(¤t); + + return; + } + + rs_tv_sub(&header->ts, &start_pcap, &elapsed); + + /* + * Increase received count + */ + stats->exchange[current->code].interval.received_total++; + + /* + * It's a linked response + */ + if (original && original->linked) { + rs_tv_sub(¤t->timestamp, &original->packet->timestamp, &latency); + + /* + * Update stats for both the request and response types. + * + * This isn't useful for things like Access-Requests, but will be useful for + * CoA and Disconnect Messages, as we get the average latency across both + * response types. + * + * It also justifies allocating PW_CODE_MAX instances of rs_latency_t. + */ + rs_stats_update_latency(&stats->exchange[current->code], &latency); + rs_stats_update_latency(&stats->exchange[original->expect->code], &latency); + + /* + * Were filtering on response, now print out the full data from the request + */ + if (conf->filter_response && RIDEBUG_ENABLED() && (conf->event_flags & RS_NORMAL)) { + rs_time_print(timestr, sizeof(timestr), &original->packet->timestamp); + rs_tv_sub(&original->packet->timestamp, &start_pcap, &elapsed); + rs_packet_print(original, original->id, RS_NORMAL, original->in, + original->packet, &elapsed, NULL, false, true); + rs_tv_sub(&header->ts, &start_pcap, &elapsed); + rs_time_print(timestr, sizeof(timestr), &header->ts); + } + + if (conf->event_flags & status) { + rs_packet_print(original, count, status, event->in, current, + &elapsed, &latency, response, true); + } + /* + * It's the original request + * + * If were filtering on responses we can only indicate we received it on response, or timeout. + */ + } else if (!conf->filter_response && (conf->event_flags & status)) { + rs_packet_print(original, original ? original->id : count, status, event->in, + current, &elapsed, NULL, response, true); + } + + fflush(fr_log_fp); + + /* + * If it's a unlinked response, we need to free it explicitly, as it will + * not be done by the event queue. + */ + if (response && !original) { + rad_free(¤t); + } + + captured++; + /* + * We've hit our capture limit, break out of the event loop + */ + if ((conf->limit > 0) && (captured >= conf->limit)) { + INFO("Captured %" PRIu64 " packets, exiting...", captured); + fr_event_loop_exit(events, 1); + } +} + +static void rs_got_packet(fr_event_list_t *el, int fd, void *ctx) +{ + static uint64_t count = 0; /* Packets seen */ + rs_event_t *event = ctx; + pcap_t *handle = event->in->handle; + + int i; + int ret; + const uint8_t *data; + struct pcap_pkthdr *header; + + /* + * Consume entire capture, interleaving not currently possible + */ + if ((event->in->type == PCAP_FILE_IN) || (event->in->type == PCAP_STDIO_IN)) { + while (!fr_event_loop_exiting(el)) { + struct timeval now; + + ret = pcap_next_ex(handle, &header, &data); + if (ret == 0) { + /* No more packets available at this time */ + return; + } + if (ret == -2) { + DEBUG("Done reading packets (%s)", event->in->name); + fr_event_fd_delete(events, 0, fd); + + /* Signal pipe takes one slot which is why this is == 1 */ + if (fr_event_list_num_fds(events) == 1) { + fr_event_loop_exit(events, 1); + } + + return; + } + if (ret < 0) { + ERROR("Error requesting next packet, got (%i): %s", ret, pcap_geterr(handle)); + return; + } + + do { + now = header->ts; + } while (fr_event_run(el, &now) == 1); + count++; + + rs_packet_process(count, event, header, data); + } + return; + } + + /* + * Consume multiple packets from the capture buffer. + * We occasionally need to yield to allow events to run. + */ + for (i = 0; i < RS_FORCE_YIELD; i++) { + ret = pcap_next_ex(handle, &header, &data); + if (ret == 0) { + /* No more packets available at this time */ + return; + } + if (ret < 0) { + ERROR("Error requesting next packet, got (%i): %s", ret, pcap_geterr(handle)); + return; + } + + count++; + rs_packet_process(count, event, header, data); + } +} + +static void _rs_event_status(struct timeval *wake) +{ + if (wake && ((wake->tv_sec != 0) || (wake->tv_usec >= 100000))) { + DEBUG2("Waking up in %d.%01u seconds.", (int) wake->tv_sec, (unsigned int) wake->tv_usec / 100000); + + if (RIDEBUG_ENABLED()) { + rs_time_print(timestr, sizeof(timestr), wake); + } + } +} + +/** Compare requests using packet info and lists of attributes + * + */ +static int rs_rtx_cmp(rs_request_t const *a, rs_request_t const *b) +{ + int rcode; + + RS_ASSERT(a->link_vps); + RS_ASSERT(b->link_vps); + + rcode = (int) a->expect->code - (int) b->expect->code; + if (rcode != 0) return rcode; + + rcode = a->expect->sockfd - b->expect->sockfd; + if (rcode != 0) return rcode; + + rcode = fr_ipaddr_cmp(&a->expect->src_ipaddr, &b->expect->src_ipaddr); + if (rcode != 0) return rcode; + + rcode = fr_ipaddr_cmp(&a->expect->dst_ipaddr, &b->expect->dst_ipaddr); + if (rcode != 0) return rcode; + + return fr_pair_list_cmp(a->link_vps, b->link_vps); +} + +static int rs_build_dict_list(DICT_ATTR const **out, size_t len, char *list) +{ + size_t i = 0; + char *p, *tok; + + p = list; + while ((tok = strsep(&p, "\t ,")) != NULL) { + DICT_ATTR const *da; + if ((*tok == '\t') || (*tok == ' ') || (*tok == '\0')) { + continue; + } + + if (i == len) { + ERROR("Too many attributes, maximum allowed is %zu", len); + return -1; + } + + da = dict_attrbyname(tok); + if (!da) { + ERROR("Error parsing attribute name \"%s\"", tok); + return -1; + } + + out[i] = da; + i++; + } + + /* + * This allows efficient list comparisons later + */ + if (i > 1) fr_quick_sort((void const **)out, 0, i - 1, fr_pointer_cmp); + + return i; +} + +static int rs_build_filter(VALUE_PAIR **out, char const *filter) +{ + vp_cursor_t cursor; + VALUE_PAIR *vp; + FR_TOKEN code; + + code = fr_pair_list_afrom_str(conf, filter, out); + if (code == T_INVALID) { + ERROR("Invalid RADIUS filter \"%s\" (%s)", filter, fr_strerror()); + return -1; + } + + if (!*out) { + ERROR("Empty RADIUS filter '%s'", filter); + return -1; + } + + for (vp = fr_cursor_init(&cursor, out); + vp; + vp = fr_cursor_next(&cursor)) { + /* + * xlat expansion isn't supported here + */ + if (vp->type == VT_XLAT) { + vp->type = VT_DATA; + vp->vp_strvalue = vp->value.xlat; + vp->vp_length = talloc_array_length(vp->vp_strvalue) - 1; + } + } + + /* + * This allows efficient list comparisons later + */ + fr_pair_list_sort(out, fr_pair_cmp_by_da_tag); + + return 0; +} + +static int rs_build_event_flags(int *flags, FR_NAME_NUMBER const *map, char *list) +{ + size_t i = 0; + char *p, *tok; + + p = list; + while ((tok = strsep(&p, "\t ,")) != NULL) { + int flag; + + if ((*tok == '\t') || (*tok == ' ') || (*tok == '\0')) { + continue; + } + + *flags |= flag = fr_str2int(map, tok, -1); + if (flag < 0) { + ERROR("Invalid flag \"%s\"", tok); + return -1; + } + + i++; + } + + return i; +} + +/** Callback for when the request is removed from the request tree + * + * @param request being removed. + */ +static void _unmark_request(void *request) +{ + rs_request_t *this = request; + this->in_request_tree = false; +} + +/** Callback for when the request is removed from the link tree + * + * @param request being removed. + */ +static void _unmark_link(void *request) +{ + rs_request_t *this = request; + this->in_link_tree = false; +} + +#ifdef HAVE_COLLECTDC_H +/** Re-open the collectd socket + * + */ +static void rs_collectd_reopen(void *ctx) +{ + fr_event_list_t *list = ctx; + static fr_event_t *event; + struct timeval now, when; + + if (rs_stats_collectd_open(conf) == 0) { + DEBUG2("Stats output socket (re)opened"); + return; + } + + ERROR("Will attempt to re-establish connection in %i ms", RS_SOCKET_REOPEN_DELAY); + + gettimeofday(&now, NULL); + rs_tv_add_ms(&now, RS_SOCKET_REOPEN_DELAY, &when); + if (!fr_event_insert(list, rs_collectd_reopen, list, &when, &event)) { + ERROR("Failed inserting re-open event"); + RS_ASSERT(0); + } +} +#endif + +/** Write the last signal to the signal pipe + * + * @param sig raised + */ +static void rs_signal_self(int sig) +{ + if (write(self_pipe[1], &sig, sizeof(sig)) < 0) { + ERROR("Failed writing signal %s to pipe: %s", strsignal(sig), fr_syserror(errno)); + exit(EXIT_FAILURE); + } +} + +/** Read the last signal from the signal pipe + * + */ +static void rs_signal_action( +#ifndef HAVE_COLLECTDC_H +UNUSED +#endif +fr_event_list_t *list, int fd, UNUSED void *ctx) +{ + int sig; + ssize_t ret; + + ret = read(fd, &sig, sizeof(sig)); + if (ret < 0) { + ERROR("Failed reading signal from pipe: %s", fr_syserror(errno)); + exit(EXIT_FAILURE); + } + + if (ret != sizeof(sig)) { + ERROR("Failed reading signal from pipe: " + "Expected signal to be %zu bytes but only read %zu byes", sizeof(sig), ret); + exit(EXIT_FAILURE); + } + + switch (sig) { +#ifdef HAVE_COLLECTDC_H + case SIGPIPE: + rs_collectd_reopen(list); + break; +#endif + + case SIGINT: + case SIGTERM: + case SIGQUIT: + DEBUG2("Signalling event loop to exit"); + fr_event_loop_exit(events, 1); + break; + + default: + ERROR("Unhandled signal %s", strsignal(sig)); + exit(EXIT_FAILURE); + } +} + +static void NEVER_RETURNS usage(int status) +{ + FILE *output = status ? stderr : stdout; + fprintf(output, "Usage: radsniff [options][stats options] -- [pcap files]\n"); + fprintf(output, "options:\n"); + fprintf(output, " -a List all interfaces available for capture.\n"); + fprintf(output, " -c <count> Number of packets to capture.\n"); + fprintf(output, " -C Enable UDP checksum validation.\n"); + fprintf(output, " -d <directory> Set dictionary directory.\n"); + fprintf(output, " -d <raddb> Set configuration directory (defaults to " RADDBDIR ").\n"); + fprintf(output, " -D <dictdir> Set main dictionary directory (defaults to " DICTDIR ").\n"); + fprintf(output, " -e <event>[,<event>] Only log requests with these event flags.\n"); + fprintf(output, " Event may be one of the following:\n"); + fprintf(output, " - received - a request or response.\n"); + fprintf(output, " - norsp - seen for a request.\n"); + fprintf(output, " - rtx - of a request that we've seen before.\n"); + fprintf(output, " - noreq - could be matched with the response.\n"); + fprintf(output, " - reused - ID too soon.\n"); + fprintf(output, " - error - decoding the packet.\n"); + fprintf(output, " -f <filter> PCAP filter (default is 'udp port <port> or <port + 1> or 3799')\n"); + fprintf(output, " -h This help message.\n"); + fprintf(output, " -i <interface> Capture packets from interface (defaults to all if supported).\n"); + fprintf(output, " -I <file> Read packets from file (overrides input of -F).\n"); + fprintf(output, " -l <attr>[,<attr>] Output packet sig and a list of attributes.\n"); + fprintf(output, " -L <attr>[,<attr>] Detect retransmissions using these attributes to link requests.\n"); + fprintf(output, " -m Don't put interface(s) into promiscuous mode.\n"); + fprintf(output, " -p <port> Filter packets by port (default is 1812).\n"); + fprintf(output, " -P <pidfile> Daemonize and write out <pidfile>.\n"); + fprintf(output, " -q Print less debugging information.\n"); + fprintf(output, " -r <filter> RADIUS attribute request filter.\n"); + fprintf(output, " -R <filter> RADIUS attribute response filter.\n"); + fprintf(output, " -s <secret> RADIUS secret.\n"); + fprintf(output, " -S Write PCAP data to stdout.\n"); + fprintf(output, " -v Show program version information.\n"); + fprintf(output, " -w <file> Write output packets to file.\n"); + fprintf(output, " -x Print more debugging information.\n"); + fprintf(output, "stats options:\n"); + fprintf(output, " -W <interval> Periodically write out statistics every <interval> seconds.\n"); + fprintf(output, " -T <timeout> How many milliseconds before the request is counted as lost " + "(defaults to %i).\n", RS_DEFAULT_TIMEOUT); +#ifdef HAVE_COLLECTDC_H + fprintf(output, " -N <prefix> The instance name passed to the collectd plugin.\n"); + fprintf(output, " -O <server> Write statistics to this collectd server.\n"); +#endif + exit(status); +} + +int main(int argc, char *argv[]) +{ + fr_pcap_t *in = NULL, *in_p; + fr_pcap_t **in_head = ∈ + fr_pcap_t *out = NULL; + + int ret = 1; /* Exit status */ + + char errbuf[PCAP_ERRBUF_SIZE]; /* Error buffer */ + int port = 1812; + + char buffer[1024]; + + int opt; + char const *radius_dir = RADDBDIR; + char const *dict_dir = DICTDIR; + + rs_stats_t stats; + + fr_debug_lvl = 1; + fr_log_fp = stdout; + + /* + * Useful if using radsniff as a long running stats daemon + */ +#ifndef NDEBUG + if (fr_fault_setup(getenv("PANIC_ACTION"), argv[0]) < 0) { + fr_perror("radsniff"); + exit(EXIT_FAILURE); + } +#endif + + talloc_set_log_stderr(); + + conf = talloc_zero(NULL, rs_t); + RS_ASSERT(conf); + + /* + * We don't really want probes taking down machines + */ +#ifdef HAVE_TALLOC_SET_MEMLIMIT + /* + * @fixme causes hang in talloc steal + */ + //talloc_set_memlimit(conf, 524288000); /* 50 MB */ +#endif + + /* + * Set some defaults + */ + conf->print_packet = true; + conf->limit = 0; + conf->promiscuous = true; +#ifdef HAVE_COLLECTDC_H + conf->stats.prefix = RS_DEFAULT_PREFIX; +#endif + conf->radius_secret = RS_DEFAULT_SECRET; + conf->logger = NULL; + +#ifdef HAVE_COLLECTDC_H + conf->stats.prefix = RS_DEFAULT_PREFIX; +#endif + + /* + * Get options + */ + while ((opt = getopt(argc, argv, "ab:c:Cd:D:e:Ff:hi:I:l:L:mp:P:qr:R:s:Svw:xXW:T:P:N:O:")) != EOF) { + switch (opt) { + case 'a': + { + pcap_if_t *all_devices = NULL; + pcap_if_t *dev_p; + + if (pcap_findalldevs(&all_devices, errbuf) < 0) { + ERROR("Error getting available capture devices: %s", errbuf); + goto finish; + } + + int i = 1; + for (dev_p = all_devices; + dev_p; + dev_p = dev_p->next) { + INFO("%i.%s", i++, dev_p->name); + } + ret = 0; + pcap_freealldevs(all_devices); + goto finish; + } + + /* super secret option */ + case 'b': + conf->buffer_pkts = atoi(optarg); + if (conf->buffer_pkts == 0) { + ERROR("Invalid buffer length \"%s\"", optarg); + usage(1); + } + break; + + case 'c': + conf->limit = atoi(optarg); + if (conf->limit == 0) { + ERROR("Invalid number of packets \"%s\"", optarg); + usage(1); + } + break; + + /* udp checksum */ + case 'C': + conf->verify_udp_checksum = true; + break; + + case 'd': + radius_dir = optarg; + break; + + case 'D': + dict_dir = optarg; + break; + + case 'e': + if (rs_build_event_flags((int *) &conf->event_flags, rs_events, optarg) < 0) { + usage(64); + } + break; + + case 'f': + conf->pcap_filter = optarg; + break; + + case 'h': + usage(0); /* never returns */ + + case 'i': + *in_head = fr_pcap_init(conf, optarg, PCAP_INTERFACE_IN); + if (!*in_head) goto finish; + in_head = &(*in_head)->next; + conf->from_dev = true; + break; + + case 'I': + *in_head = fr_pcap_init(conf, optarg, PCAP_FILE_IN); + if (!*in_head) { + goto finish; + } + in_head = &(*in_head)->next; + conf->from_file = true; + break; + + case 'l': + conf->list_attributes = optarg; + break; + + case 'L': + conf->link_attributes = optarg; + break; + + case 'm': + conf->promiscuous = false; + break; + + case 'p': + port = atoi(optarg); + break; + + case 'P': + conf->daemonize = true; + conf->pidfile = optarg; + break; + + case 'q': + if (fr_debug_lvl > 0) { + fr_debug_lvl--; + } + break; + + case 'r': + conf->filter_request = optarg; + break; + + case 'R': + conf->filter_response = optarg; + break; + + case 's': + conf->radius_secret = optarg; + break; + + case 'S': + conf->to_stdout = true; + break; + + case 'v': +#ifdef HAVE_COLLECTDC_H + INFO("%s, %s, collectdclient version %s", radsniff_version, pcap_lib_version(), + lcc_version_string()); +#else + INFO("%s %s", radsniff_version, pcap_lib_version()); +#endif + exit(EXIT_SUCCESS); + + case 'w': + out = fr_pcap_init(conf, optarg, PCAP_FILE_OUT); + if (!out) { + ERROR("Failed creating pcap file \"%s\"", optarg); + exit(EXIT_FAILURE); + } + conf->to_file = true; + break; + + case 'x': + case 'X': + fr_debug_lvl++; + break; + + case 'W': + conf->stats.interval = atoi(optarg); + conf->print_packet = false; + if (conf->stats.interval <= 0) { + ERROR("Stats interval must be > 0"); + usage(64); + } + break; + + case 'T': + conf->stats.timeout = atoi(optarg); + if (conf->stats.timeout <= 0) { + ERROR("Timeout value must be > 0"); + usage(64); + } + break; + +#ifdef HAVE_COLLECTDC_H + case 'N': + conf->stats.prefix = optarg; + break; + + case 'O': + conf->stats.collectd = optarg; + conf->stats.out = RS_STATS_OUT_COLLECTD; + break; +#endif + default: + usage(64); + } + } + + /* + * Mismatch between the binary and the libraries it depends on + */ + if (fr_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) { + fr_perror("radsniff"); + exit(EXIT_FAILURE); + } + + /* Useful for file globbing */ + while (optind < argc) { + *in_head = fr_pcap_init(conf, argv[optind], PCAP_FILE_IN); + if (!*in_head) { + goto finish; + } + in_head = &(*in_head)->next; + conf->from_file = true; + optind++; + } + + /* Is stdin not a tty? If so it's probably a pipe */ + if (!isatty(fileno(stdin))) { + conf->from_stdin = true; + } + + /* What's the point in specifying -F ?! */ + if (conf->from_stdin && conf->from_file && conf->to_file) { + usage(64); + } + + /* Can't read from both... */ + if (conf->from_file && conf->from_dev) { + usage(64); + } + + /* Reading from file overrides stdin */ + if (conf->from_stdin && (conf->from_file || conf->from_dev)) { + conf->from_stdin = false; + } + + /* Writing to file overrides stdout */ + if (conf->to_file && conf->to_stdout) { + conf->to_stdout = false; + } + + if (conf->to_stdout) { + out = fr_pcap_init(conf, "stdout", PCAP_STDIO_OUT); + if (!out) { + goto finish; + } + } + + if (conf->from_stdin) { + *in_head = fr_pcap_init(conf, "stdin", PCAP_STDIO_IN); + if (!*in_head) { + goto finish; + } + in_head = &(*in_head)->next; + } + + if (conf->stats.interval && !conf->stats.out) { + conf->stats.out = RS_STATS_OUT_STDIO; + } + + if (conf->stats.timeout == 0) { + conf->stats.timeout = RS_DEFAULT_TIMEOUT; + } + + /* + * If were writing pcap data, or CSV to stdout we *really* don't want to send + * logging there as well. + */ + if (conf->to_stdout || conf->list_attributes) { + fr_log_fp = stderr; + } + + if (conf->list_attributes) { + conf->logger = rs_packet_print_csv; + } else if (fr_debug_lvl > 0) { + conf->logger = rs_packet_print_fancy; + } + +#if !defined(HAVE_PCAP_FOPEN_OFFLINE) || !defined(HAVE_PCAP_DUMP_FOPEN) + if (conf->from_stdin || conf->to_stdout) { + ERROR("PCAP streams not supported"); + goto finish; + } +#endif + + if (!conf->pcap_filter) { + snprintf(buffer, sizeof(buffer), "udp port %d or %d or %d", + port, port + 1, 3799); + conf->pcap_filter = buffer; + } + + if (dict_init(dict_dir, RADIUS_DICTIONARY) < 0) { + fr_perror("radsniff"); + ret = 64; + goto finish; + } + + if (dict_read(radius_dir, RADIUS_DICTIONARY) == -1) { + fr_perror("radsniff"); + ret = 64; + goto finish; + } + + fr_strerror(); /* Clear out any non-fatal errors */ + + if (conf->list_attributes) { + conf->list_da_num = rs_build_dict_list(conf->list_da, sizeof(conf->list_da) / sizeof(*conf->list_da), + conf->list_attributes); + if (conf->list_da_num < 0) { + usage(64); + } + rs_packet_print_csv_header(); + } + + if (conf->link_attributes) { + conf->link_da_num = rs_build_dict_list(conf->link_da, sizeof(conf->link_da) / sizeof(*conf->link_da), + conf->link_attributes); + if (conf->link_da_num < 0) { + usage(64); + } + + link_tree = rbtree_create(conf, (rbcmp) rs_rtx_cmp, _unmark_link, 0); + if (!link_tree) { + ERROR("Failed creating RTX tree"); + goto finish; + } + } + + if (conf->filter_request) { + vp_cursor_t cursor; + VALUE_PAIR *type; + + if (rs_build_filter(&conf->filter_request_vps, conf->filter_request) < 0) { + usage(64); + } + + fr_cursor_init(&cursor, &conf->filter_request_vps); + type = fr_cursor_next_by_num(&cursor, PW_PACKET_TYPE, 0, TAG_ANY); + if (type) { + fr_cursor_remove(&cursor); + conf->filter_request_code = type->vp_integer; + talloc_free(type); + } + } + + if (conf->filter_response) { + vp_cursor_t cursor; + VALUE_PAIR *type; + + if (rs_build_filter(&conf->filter_response_vps, conf->filter_response) < 0) { + usage(64); + } + + fr_cursor_init(&cursor, &conf->filter_response_vps); + type = fr_cursor_next_by_num(&cursor, PW_PACKET_TYPE, 0, TAG_ANY); + if (type) { + fr_cursor_remove(&cursor); + conf->filter_response_code = type->vp_integer; + talloc_free(type); + } + } + + /* + * Default to logging and capturing all events + */ + if (conf->event_flags == 0) { + DEBUG("Logging all events"); + memset(&conf->event_flags, 0xff, sizeof(conf->event_flags)); + } + + /* + * If we need to list attributes, link requests using attributes, filter attributes + * or print the packet contents, we need to decode the attributes. + * + * But, if were just logging requests, or graphing packets, we don't need to decode + * attributes. + */ + if (conf->list_da_num || conf->link_da_num || conf->filter_response_vps || conf->filter_request_vps || + conf->print_packet) { + conf->decode_attrs = true; + } + + /* + * Setup the request tree + */ + request_tree = rbtree_create(conf, (rbcmp) rs_packet_cmp, _unmark_request, 0); + if (!request_tree) { + ERROR("Failed creating request tree"); + goto finish; + } + + /* + * Get the default capture device + */ + if (!conf->from_stdin && !conf->from_file && !conf->from_dev) { + pcap_if_t *all_devices; /* List of all devices libpcap can listen on */ + pcap_if_t *dev_p; + + if (pcap_findalldevs(&all_devices, errbuf) < 0) { + ERROR("Error getting available capture devices: %s", errbuf); + goto finish; + } + + if (!all_devices) { + ERROR("No capture files specified and no live interfaces available"); + ret = 64; + goto finish; + } + + for (dev_p = all_devices; + dev_p; + dev_p = dev_p->next) { + int link_layer; + + /* Don't use the any devices, it's horribly broken */ + if (!strcmp(dev_p->name, "any")) continue; + + link_layer = fr_pcap_if_link_layer(errbuf, dev_p); + if (link_layer < 0) { + DEBUG2("Skipping %s: %s", dev_p->name, errbuf); + continue; + } + + if (!fr_pcap_link_layer_supported(link_layer)) { + DEBUG2("Skipping %s: datalink type %s not supported", + dev_p->name, pcap_datalink_val_to_name(link_layer)); + continue; + } + + *in_head = fr_pcap_init(conf, dev_p->name, PCAP_INTERFACE_IN); + in_head = &(*in_head)->next; + } + conf->from_auto = true; + conf->from_dev = true; + + pcap_freealldevs(all_devices); + + INFO("Defaulting to capture on all interfaces"); + } + + /* + * Print captures values which will be used + */ + if (fr_debug_lvl > 2) { + DEBUG2("Sniffing with options:"); + if (conf->from_dev) { + char *buff = fr_pcap_device_names(conf, in, ' '); + DEBUG2(" Device(s) : [%s]", buff); + talloc_free(buff); + } + if (out) { + DEBUG2(" Writing to : [%s]", out->name); + } + if (conf->limit > 0) { + DEBUG2(" Capture limit (packets) : [%" PRIu64 "]", conf->limit); + } + DEBUG2(" PCAP filter : [%s]", conf->pcap_filter); + DEBUG2(" RADIUS secret : [%s]", conf->radius_secret); + + if (conf->filter_request_code) { + DEBUG2(" RADIUS request code : [%s]", fr_packet_codes[conf->filter_request_code]); + } + + if (conf->filter_request_vps){ + DEBUG2(" RADIUS request filter :"); + vp_printlist(fr_log_fp, conf->filter_request_vps); + } + + if (conf->filter_response_code) { + DEBUG2(" RADIUS response code : [%s]", fr_packet_codes[conf->filter_response_code]); + } + + if (conf->filter_response_vps){ + DEBUG2(" RADIUS response filter :"); + vp_printlist(fr_log_fp, conf->filter_response_vps); + } + } + + /* + * Setup collectd templates + */ +#ifdef HAVE_COLLECTDC_H + if (conf->stats.out == RS_STATS_OUT_COLLECTD) { + size_t i; + rs_stats_tmpl_t *tmpl, **next; + + if (rs_stats_collectd_open(conf) < 0) { + exit(EXIT_FAILURE); + } + + next = &conf->stats.tmpl; + + for (i = 0; i < (sizeof(rs_useful_codes) / sizeof(*rs_useful_codes)); i++) { + tmpl = rs_stats_collectd_init_latency(conf, next, conf, "exchanged", + &stats.exchange[rs_useful_codes[i]], + rs_useful_codes[i]); + if (!tmpl) { + ERROR("Error allocating memory for stats template"); + goto finish; + } + next = &(tmpl->next); + } + } +#endif + + /* + * This actually opens the capture interfaces/files (we just allocated the memory earlier) + */ + { + fr_pcap_t *tmp; + fr_pcap_t **tmp_p = &tmp; + + for (in_p = in; + in_p; + in_p = in_p->next) { + in_p->promiscuous = conf->promiscuous; + in_p->buffer_pkts = conf->buffer_pkts; + if (fr_pcap_open(in_p) < 0) { + ERROR("Failed opening pcap handle (%s): %s", in_p->name, fr_strerror()); + if (conf->from_auto || (in_p->type == PCAP_FILE_IN)) { + continue; + } + + goto finish; + } + + if (!fr_pcap_link_layer_supported(in_p->link_layer)) { + ERROR("Failed opening pcap handle (%s): Datalink type %s not supported", + in_p->name, pcap_datalink_val_to_name(in_p->link_layer)); + goto finish; + } + + if (conf->pcap_filter) { + if (fr_pcap_apply_filter(in_p, conf->pcap_filter) < 0) { + ERROR("Failed applying filter"); + goto finish; + } + } + + *tmp_p = in_p; + tmp_p = &(in_p->next); + } + *tmp_p = NULL; + in = tmp; + + if (!in) { + ERROR("No PCAP sources available"); + exit(EXIT_FAILURE); + } + + /* Clear any irrelevant errors */ + fr_strerror(); + } + + /* + * Open our output interface (if we have one); + */ + if (out) { + out->link_layer = -1; /* Infer output link type from input */ + + for (in_p = in; + in_p; + in_p = in_p->next) { + if (out->link_layer < 0) { + out->link_layer = in_p->link_layer; + continue; + } + + if (out->link_layer != in_p->link_layer) { + ERROR("Asked to write to output file, but inputs do not have the same link type"); + ret = 64; + goto finish; + } + } + + RS_ASSERT(out->link_layer >= 0); + + if (fr_pcap_open(out) < 0) { + ERROR("Failed opening pcap output (%s): %s", out->name, fr_strerror()); + goto finish; + } + } + + /* + * Setup and enter the main event loop. Who needs libev when you can roll your own... + */ + { + struct timeval now; + rs_update_t update; + + char *buff; + + memset(&stats, 0, sizeof(stats)); + memset(&update, 0, sizeof(update)); + + events = fr_event_list_create(conf, _rs_event_status); + if (!events) { + ERROR(); + goto finish; + } + + /* + * Initialise the signal handler pipe + */ + if (pipe(self_pipe) < 0) { + ERROR("Couldn't open signal pipe: %s", fr_syserror(errno)); + exit(EXIT_FAILURE); + } + + if (!fr_event_fd_insert(events, 0, self_pipe[0], rs_signal_action, events)) { + ERROR("Failed inserting signal pipe descriptor: %s", fr_strerror()); + goto finish; + } + + /* + * Now add fd's for each of the pcap sessions we opened + */ + for (in_p = in; + in_p; + in_p = in_p->next) { + rs_event_t *event; + + event = talloc_zero(events, rs_event_t); + event->list = events; + event->in = in_p; + event->out = out; + event->stats = &stats; + + if (!fr_event_fd_insert(events, 0, in_p->fd, rs_got_packet, event)) { + ERROR("Failed inserting file descriptor"); + goto finish; + } + } + + buff = fr_pcap_device_names(conf, in, ' '); + DEBUG("Sniffing on (%s)", buff); + talloc_free(buff); + + gettimeofday(&now, NULL); + + /* + * Insert our stats processor + */ + if (conf->stats.interval) { + static fr_event_t *event; + + update.list = events; + update.stats = &stats; + update.in = in; + + now.tv_sec += conf->stats.interval; + now.tv_usec = 0; + if (!fr_event_insert(events, rs_stats_process, (void *) &update, &now, &event)) { + ERROR("Failed inserting stats event"); + } + + INFO("Muting stats for the next %i milliseconds (warmup)", conf->stats.timeout); + rs_tv_add_ms(&now, conf->stats.timeout, &stats.quiet); + } + } + + + /* + * Do this as late as possible so we can return an error code if something went wrong. + */ + if (conf->daemonize) { + rs_daemonize(conf->pidfile); + } + + /* + * Setup signal handlers so we always exit gracefully, ensuring output buffers are always + * flushed. + */ + fr_set_signal(SIGPIPE, rs_signal_self); + fr_set_signal(SIGINT, rs_signal_self); + fr_set_signal(SIGTERM, rs_signal_self); +#ifdef SIGQUIT + fr_set_signal(SIGQUIT, rs_signal_self); +#endif + + fr_event_loop(events); /* Enter the main event loop */ + + DEBUG("Done sniffing"); + + finish: + + cleanup = true; + + /* + * Free all the things! This also closes all the sockets and file descriptors + */ + talloc_free(conf); + + if (conf->daemonize) { + unlink(conf->pidfile); + } + + return ret; +} diff --git a/src/main/radsniff.mk.in b/src/main/radsniff.mk.in new file mode 100644 index 0000000..3de7ebc --- /dev/null +++ b/src/main/radsniff.mk.in @@ -0,0 +1,13 @@ +PCAP_LIBS := @PCAP_LIBS@ + +ifneq ($(PCAP_LIBS),) +TARGET := radsniff +else +TARGET := +endif + +SOURCES := radsniff.c collectd.c + +TGT_PREREQS := libfreeradius-radius.a +TGT_LDLIBS := $(LIBS) $(PCAP_LIBS) $(COLLECTDC_LIBS) +TGT_LDFLAGS := $(LDFLAGS) $(PCAP_LDFLAGS) $(COLLECTDC_LDFLAGS) diff --git a/src/main/radtest.in b/src/main/radtest.in new file mode 100644 index 0000000..6b71032 --- /dev/null +++ b/src/main/radtest.in @@ -0,0 +1,135 @@ +#! /bin/sh +# +# radtest Emulate the user interface of the old +# radtest that used to be part of FreeRADIUS. +# +# Version: $Id$ +# + +prefix="@prefix@" +exec_prefix="@exec_prefix@" +bindir="@bindir@" + +usage() { + echo "Usage: radtest [OPTIONS] user passwd radius-server[:port] nas-port-number secret [ppphint] [nasname]" >&2 + echo " -d RADIUS_DIR Set radius directory" >&2 + echo " -t <type> Set authentication method" >&2 + echo " type can be pap, chap, mschap, or eap-md5" >&2 + echo " -P protocol Select udp (default) or tcp" >&2 + echo " -x Enable debug output" >&2 + echo " -4 Use IPv4 for the NAS address (default)" >&2 + echo " -6 Use IPv6 for the NAS address" >&2 + exit 1 +} + +radclient=$bindir/radclient +if [ ! -x "$radclient" ] && [ -x ./radclient ] +then + radclient=./radclient +fi + +# radeapclient is used for EAP-MD5. +radeapclient=$bindir/radeapclient + +OPTIONS= +PASSWORD="User-Password" +NAS_ADDR_ATTR="NAS-IP-Address" + +# We need at LEAST these many options +if [ $# -lt 5 ] +then + usage +fi + +# Parse new command-line options +while [ `echo "$1" | cut -c 1` = "-" ] +do + case "$1" in + -4) + OPTIONS="$OPTIONS -4" + NAS_ADDR_ATTR="NAS-IP-Address" + shift + ;; + -6) + OPTIONS="$OPTIONS -6" + NAS_ADDR_ATTR="NAS-IPv6-Address" + shift + ;; + -d) + OPTIONS="$OPTIONS -d $2" + shift;shift + ;; + -P) + OPTIONS="$OPTIONS -P $2" + shift;shift + ;; + -x) + OPTIONS="$OPTIONS -x" + shift + ;; + + -t) + shift; + case "$1" in + pap) + PASSWORD="User-Password" + ;; + chap) + PASSWORD="CHAP-Password" + ;; + mschap) + PASSWORD="MS-CHAP-Password" + ;; + eap-md5) + PASSWORD="Cleartext-Password" + if [ ! -x "$radeapclient" ] + then + echo "radtest: No 'radeapclient' program was found. Cannot perform EAP-MD5." >&1 + exit 1 + fi + radclient="$radeapclient" + ;; + *) + usage + ;; + esac + shift + ;; + + *) + usage + ;; + esac +done + +# Check that there are enough options left over. +if [ $# -lt 5 ] || [ $# -gt 7 ] +then + usage +fi + +if [ "$7" ] +then + nas=$7 +else + nas=`(hostname || uname -n) 2>/dev/null | sed 1q` +fi + +( + echo "User-Name = \"$1\"" + echo "$PASSWORD = \"$2\"" + echo "$NAS_ADDR_ATTR = $nas" + echo "NAS-Port = $4" + echo "Message-Authenticator = 0x00" + if [ "$radclient" = "$radeapclient" ] + then + echo "EAP-Code = Response" + echo "EAP-Type-Identity = \"$1\"" + fi + if [ "$6" != "" -a "$6" != "0" ] + then + echo "Framed-Protocol = PPP" + fi +) | $radclient $OPTIONS -x $3 auth "$5" + +exit $? diff --git a/src/main/radtest.mk b/src/main/radtest.mk new file mode 100644 index 0000000..3adc133 --- /dev/null +++ b/src/main/radtest.mk @@ -0,0 +1,5 @@ +install: $(R)$(bindir)/radtest + +$(R)$(bindir)/radtest: src/main/radtest | $(R)$(bindir) + @echo INSTALL $(notdir $<) + @$(INSTALL) -m 755 $< $(R)$(bindir) diff --git a/src/main/radwho.c b/src/main/radwho.c new file mode 100644 index 0000000..d534760 --- /dev/null +++ b/src/main/radwho.c @@ -0,0 +1,565 @@ +/*@-skipposixheaders@*/ +/* + * radwho.c Show who is logged in on the terminal servers. + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2000,2006 The FreeRADIUS server project + * Copyright 2000 Alan DeKok <aland@ox.org> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/sysutmp.h> +#include <freeradius-devel/radutmp.h> + +#include <pwd.h> +#include <sys/stat.h> +#include <ctype.h> + +/* + * Header above output and format. + */ +static char const *hdr1 = +"Login Name What TTY When From Location"; + +static char const *hdr2 = +"Login Port What When From Location"; + +static char const *eol = "\n"; +static int showname = -1; +static int showptype = 0; +static int showcid = 0; +static char const *progname = "radwho"; +char const *radlog_dir = NULL; + +static char const *radutmp_file = NULL; +static char const *raddb_dir = RADDBDIR; +static char const *dict_dir = DICTDIR; + +char const *radacct_dir = NULL; + +bool log_stripped_names; + +static char const *radwho_version = "radwho version " RADIUSD_VERSION_STRING +#ifdef RADIUSD_VERSION_COMMIT +" (git #" STRINGIFY(RADIUSD_VERSION_COMMIT) ")" +#endif +#ifndef ENABLE_REPRODUCIBLE_BUILDS +", built on " __DATE__ " at " __TIME__ +#endif +; + +/* + * Global, for log.c to use. + */ +main_config_t main_config; + +#include <sys/wait.h> +#ifdef HAVE_PTHREAD_H +pid_t rad_fork(void) +{ + return fork(); +} + +pid_t rad_waitpid(pid_t pid, int *status) +{ + return waitpid(pid, status, 0); +} +#endif + +static struct radutmp_config_t { + char const *radutmp_fn; +} radutmpconfig; + +static const CONF_PARSER module_config[] = { + { "filename", FR_CONF_POINTER(PW_TYPE_FILE_INPUT, &radutmpconfig.radutmp_fn), RADUTMP }, + CONF_PARSER_TERMINATOR +}; + +/* + * Get fullname of a user. + */ +static char *fullname(char *username) +{ +#ifdef HAVE_PWD_H + struct passwd *pwd; + char *s; + + if ((pwd = getpwnam(username)) != NULL) { + if ((s = strchr(pwd->pw_gecos, ',')) != NULL) *s = 0; + return pwd->pw_gecos; + } +#endif + + return username; +} + +/* + * Return protocol type. + */ +static char const *proto(int id, int porttype) +{ + static char buf[8]; + + if (showptype) { + if (!strchr("ASITX", porttype)) + porttype = ' '; + if (id == 'S') + snprintf(buf, sizeof(buf), "SLP %c", porttype); + else if (id == 'P') + snprintf(buf, sizeof(buf), "PPP %c", porttype); + else + snprintf(buf, sizeof(buf), "shl %c", porttype); + return buf; + } + if (id == 'S') return "SLIP"; + if (id == 'P') return "PPP"; + return "shell"; +} + +/* + * Return a time in the form day hh:mm + */ +static char *dotime(time_t t) +{ + char *s = ctime(&t); + + if (showname) { + strlcpy(s + 4, s + 11, 6); + s[9] = 0; + } else { + strlcpy(s + 4, s + 8, 9); + s[12] = 0; + } + + return s; +} + + +/* + * Print address of NAS. + */ +static char const *hostname(char *buf, size_t buflen, uint32_t ipaddr) +{ + /* + * WTF is this code for? + */ + if (ipaddr == 0 || ipaddr == (uint32_t)-1 || ipaddr == (uint32_t)-2) + return ""; + + return inet_ntop(AF_INET, &ipaddr, buf, buflen); + +} + + +/* + * Print usage message and exit. + */ +static void NEVER_RETURNS usage(int status) +{ + FILE *output = status?stderr:stdout; + + fprintf(output, "Usage: radwho [-d raddb] [-cfihnprRsSZ] [-N nas] [-P nas_port] [-u user] [-U user]\n"); + fprintf(output, " -c Show caller ID, if available.\n"); + fprintf(output, " -d Set the raddb directory (default is %s).\n", RADIUS_DIR); + fprintf(output, " -F <file> Use radutmp <file>.\n"); + fprintf(output, " -i Show session ID.\n"); + fprintf(output, " -n No full name.\n"); + fprintf(output, " -N <nas-ip-address> Show entries matching the given NAS IP address.\n"); + fprintf(output, " -p Show port type.\n"); + fprintf(output, " -P <port> Show entries matching the given nas port.\n"); + fprintf(output, " -r Print output as raw comma-delimited data.\n"); + fprintf(output, " -R Print output as RADIUS attributes and values.\n"); + fprintf(output, " includes ALL information from the radutmp record.\n"); + fprintf(output, " -s Show full name.\n"); + fprintf(output, " -S Hide shell users from radius.\n"); + fprintf(output, " -u <user> Show entries matching the given user.\n"); + fprintf(output, " -U <user> Like -u, but case-sensitive.\n"); + fprintf(output, " -v Show program version information.\n"); + fprintf(output, " -Z Include accounting stop information in radius output. Requires -R.\n"); + exit(status); +} + + +/* + * Main program + */ +int main(int argc, char **argv) +{ + CONF_SECTION *maincs, *cs; + FILE *fp; + struct radutmp rt; + char othername[256]; + char nasname[1024]; + char session_id[sizeof(rt.session_id)+1]; + int hideshell = 0; + int showsid = 0; + int rawoutput = 0; + int radiusoutput = 0; /* Radius attributes */ + char const *portind; + int c; + unsigned int portno; + char buffer[2048]; + char const *user = NULL; + int user_cmp = 0; + time_t now = 0; + uint32_t nas_port = ~0; + uint32_t nas_ip_address = INADDR_NONE; + int zap = 0; + + raddb_dir = RADIUS_DIR; + +#ifndef NDEBUG + if (fr_fault_setup(getenv("PANIC_ACTION"), argv[0]) < 0) { + fr_perror("radwho"); + exit(EXIT_FAILURE); + } +#endif + + talloc_set_log_stderr(); + + while((c = getopt(argc, argv, "d:D:fF:nN:sSipP:crRu:U:vZ")) != EOF) switch (c) { + case 'd': + raddb_dir = optarg; + break; + case 'D': + dict_dir = optarg; + break; + case 'F': + radutmp_file = optarg; + break; + case 'h': + usage(0); /* never returns */ + + case 'S': + hideshell = 1; + break; + case 'n': + showname = 0; + break; + case 'N': + if (inet_pton(AF_INET, optarg, &nas_ip_address) < 0) { + usage(1); + } + break; + case 's': + showname = 1; + break; + case 'i': + showsid = 1; + break; + case 'p': + showptype = 1; + break; + case 'P': + nas_port = atoi(optarg); + break; + case 'c': + showcid = 1; + showname = 1; + break; + case 'r': + rawoutput = 1; + break; + case 'R': + radiusoutput = 1; + now = time(NULL); + break; + case 'u': + user = optarg; + user_cmp = 0; + break; + case 'U': + user = optarg; + user_cmp = 1; + break; + case 'v': + printf("%s\n", radwho_version); + exit(EXIT_SUCCESS); + case 'Z': + zap = 1; + break; + + default: + usage(1); /* never returns */ + } + + /* + * Mismatch between the binary and the libraries it depends on + */ + if (fr_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) { + fr_perror("radwho"); + return 1; + } + + if (dict_init(dict_dir, RADIUS_DICTIONARY) < 0) { + fr_perror("radwho"); + return 1; + } + + if (dict_read(raddb_dir, RADIUS_DICTIONARY) == -1) { + fr_perror("radwho"); + return 1; + } + fr_strerror(); /* Clear the error buffer */ + + /* + * Be safe. + */ + if (zap && !radiusoutput) zap = 0; + + /* + * zap EVERYONE, but only on this nas + */ + if (zap && !user && (~nas_port == 0)) { + /* + * We need to know which NAS to zap users in. + */ + if (nas_ip_address == INADDR_NONE) usage(1); + + printf("Acct-Status-Type = Accounting-Off\n"); + printf("NAS-IP-Address = %s\n", + hostname(buffer, sizeof(buffer), nas_ip_address)); + printf("Acct-Delay-Time = 0\n"); + exit(0); /* don't bother printing anything else */ + } + + if (radutmp_file) goto have_radutmp; + + /* + * Initialize main_config + */ + memset(&main_config, 0, sizeof(main_config)); + + /* Read radiusd.conf */ + maincs = cf_section_alloc(NULL, "main", NULL); + if (!maincs) exit(1); + + snprintf(buffer, sizeof(buffer), "%.200s/radiusd.conf", raddb_dir); + if (cf_file_read(maincs, buffer) < 0) { + fprintf(stderr, "%s: Error reading or parsing radiusd.conf\n", argv[0]); + talloc_free(maincs); + exit(1); + } + + cs = cf_section_sub_find(maincs, "modules"); + if (!cs) { + fprintf(stderr, "%s: No modules section found in radiusd.conf\n", argv[0]); + exit(1); + } + /* Read the radutmp section of radiusd.conf */ + cs = cf_section_sub_find_name2(cs, "radutmp", NULL); + if (!cs) { + fprintf(stderr, "%s: No configuration information in radutmp section of radiusd.conf\n", argv[0]); + exit(1); + } + + cf_section_parse(cs, NULL, module_config); + + /* Assign the correct path for the radutmp file */ + radutmp_file = radutmpconfig.radutmp_fn; + + have_radutmp: + if (showname < 0) showname = 1; + + /* + * Show the users logged in on the terminal server(s). + */ + if ((fp = fopen(radutmp_file, "r")) == NULL) { + fprintf(stderr, "%s: Error reading %s: %s\n", + progname, radutmp_file, fr_syserror(errno)); + return 0; + } + + /* + * Don't print the headers if raw or RADIUS + */ + if (!rawoutput && !radiusoutput) { + fputs(showname ? hdr1 : hdr2, stdout); + fputs(eol, stdout); + } + + /* + * Read the file, printing out active entries. + */ + while (fread(&rt, sizeof(rt), 1, fp) == 1) { + char name[sizeof(rt.login) + 1]; + + if (rt.type != P_LOGIN) continue; /* hide logout sessions */ + + /* + * We don't show shell users if we are + * fingerd, as we have done that above. + */ + if (hideshell && !strchr("PCS", rt.proto)) + continue; + + /* + * Print out sessions only for the given user. + */ + if (user) { /* only for a particular user */ + if (((user_cmp == 0) && + (strncasecmp(rt.login, user, strlen(user)) != 0)) || + ((user_cmp == 1) && + (strncmp(rt.login, user, strlen(user)) != 0))) { + continue; + } + } + + /* + * Print out only for the given NAS port. + */ + if (~nas_port != 0) { + if (rt.nas_port != nas_port) continue; + } + + /* + * Print out only for the given NAS IP address + */ + if (nas_ip_address != INADDR_NONE) { + if (rt.nas_address != nas_ip_address) continue; + } + + memcpy(session_id, rt.session_id, sizeof(rt.session_id)); + session_id[sizeof(rt.session_id)] = 0; + + if (!rawoutput && rt.nas_port > (showname ? 999 : 99999)) { + portind = ">"; + portno = (showname ? 999 : 99999); + } else { + portind = "S"; + portno = rt.nas_port; + } + + /* + * Print output as RADIUS attributes + */ + if (radiusoutput) { + memcpy(nasname, rt.login, sizeof(rt.login)); + nasname[sizeof(rt.login)] = '\0'; + + fr_prints(buffer, sizeof(buffer), nasname, -1, '"'); + printf("User-Name = \"%s\"\n", buffer); + + fr_prints(buffer, sizeof(buffer), session_id, -1, '"'); + printf("Acct-Session-Id = \"%s\"\n", buffer); + + if (zap) printf("Acct-Status-Type = Stop\n"); + + printf("NAS-IP-Address = %s\n", + hostname(buffer, sizeof(buffer), + rt.nas_address)); + printf("NAS-Port = %u\n", rt.nas_port); + + switch (rt.proto) { + case 'S': + printf("Service-Type = Framed-User\n"); + printf("Framed-Protocol = SLIP\n"); + break; + + case 'P': + printf("Service-Type = Framed-User\n"); + printf("Framed-Protocol = PPP\n"); + break; + + default: + printf("Service-type = Login-User\n"); + break; + } + if (rt.framed_address != INADDR_NONE) { + printf("Framed-IP-Address = %s\n", + hostname(buffer, sizeof(buffer), + rt.framed_address)); + } + + /* + * Some sanity checks on the time + */ + if ((rt.time <= now) && + (now - rt.time) <= (86400 * 365)) { + printf("Acct-Session-Time = %" PRId64 "\n", (int64_t) (now - rt.time)); + } + + if (rt.caller_id[0] != '\0') { + memcpy(nasname, rt.caller_id, + sizeof(rt.caller_id)); + nasname[sizeof(rt.caller_id)] = '\0'; + + fr_prints(buffer, sizeof(buffer), nasname, -1, '"'); + printf("Calling-Station-Id = \"%s\"\n", buffer); + } + + printf("\n"); /* separate entries with a blank line */ + continue; + } + + /* + * Show the fill name, or not. + */ + memcpy(name, rt.login, sizeof(rt.login)); + name[sizeof(rt.login)] = '\0'; + + if (showname) { + if (rawoutput == 0) { + printf("%-10.10s %-17.17s %-5.5s %s%-3u %-9.9s %-15.15s %-.19s%s", + name, + showcid ? rt.caller_id : + (showsid? session_id : fullname(rt.login)), + proto(rt.proto, rt.porttype), + portind, portno, + dotime(rt.time), + hostname(nasname, sizeof(nasname), rt.nas_address), + hostname(othername, sizeof(othername), rt.framed_address), eol); + } else { + printf("%s,%s,%s,%s%u,%s,%s,%s%s", + name, + showcid ? rt.caller_id : + (showsid? session_id : fullname(rt.login)), + proto(rt.proto, rt.porttype), + portind, portno, + dotime(rt.time), + hostname(nasname, sizeof(nasname), rt.nas_address), + hostname(othername, sizeof(othername), rt.framed_address), eol); + } + } else { + if (rawoutput == 0) { + printf("%-10.10s %s%-5u %-6.6s %-13.13s %-15.15s %-.28s%s", + name, + portind, portno, + proto(rt.proto, rt.porttype), + dotime(rt.time), + hostname(nasname, sizeof(nasname), rt.nas_address), + hostname(othername, sizeof(othername), rt.framed_address), + eol); + } else { + printf("%s,%s%u,%s,%s,%s,%s%s", + name, + portind, portno, + proto(rt.proto, rt.porttype), + dotime(rt.time), + hostname(nasname, sizeof(nasname), rt.nas_address), + hostname(othername, sizeof(othername), rt.framed_address), + eol); + } + } + } + fclose(fp); + + return 0; +} diff --git a/src/main/radwho.mk b/src/main/radwho.mk new file mode 100644 index 0000000..5d9a92a --- /dev/null +++ b/src/main/radwho.mk @@ -0,0 +1,5 @@ +TARGET := radwho +SOURCES := radwho.c + +TGT_PREREQS := libfreeradius-server.a libfreeradius-radius.a +TGT_LDLIBS := $(LIBS) diff --git a/src/main/radzap b/src/main/radzap new file mode 100755 index 0000000..f05f253 --- /dev/null +++ b/src/main/radzap @@ -0,0 +1,54 @@ +#!/bin/sh +# +# $Id$ +# + +usage() { + echo "Usage: radzap [options] server[:port] secret" >&2 + echo " -h Print usage help information." + echo " -d raddb_directory: directory where radiusd.conf is located." + echo " -D dict_directory: directory where the dictionaries are located." + echo " -N nas_ip_address: IP address of the NAS to zap." + echo " -P nas_port: NAS port that the user is logged into." + echo " -u username: Name of user to zap (case insensitive)." + echo " -U username: like -u, but case-sensitive." + echo " -x Enable debugging output." + exit ${1:-0} +} + +while test "$#" != "0" +do + case $1 in + -h) usage;; + + -d) OPTS="$OPTS -d $2";shift;shift;; + + -D) OPTS="$OPTS -D $2";shift;shift;; + + -N) NAS_IP_ADDR="-N $2";shift;shift;; + + -P) NAS_PORT="-P $2";shift;shift;; + + -u) USER_NAME="-u $2";shift;shift;; + + -U) USER_NAME="-U $2";shift;shift;; + + -x) DEBUG="-x";shift;; + + *) break;; + + esac +done + +if test "$#" != "2"; then + usage 1 >&2 +fi + + +SERVER=$1 +SECRET=$2 + +# +# Radzap is now a wrapper around radwho & radclient. +# +radwho -ZR $OPTS $NAS_IP_ADDR $NAS_PORT $USER_NAME | radclient $DEBUG $OPTS -f - $SERVER acct $SECRET diff --git a/src/main/radzap.mk b/src/main/radzap.mk new file mode 100644 index 0000000..bd0eb6d --- /dev/null +++ b/src/main/radzap.mk @@ -0,0 +1,5 @@ +install: $(R)$(bindir)/radzap + +$(R)$(bindir)/radzap: src/main/radzap | $(R)$(bindir) + @echo INSTALL $(notdir $<) + @$(INSTALL) -m 755 $< $(R)$(bindir) diff --git a/src/main/realms.c b/src/main/realms.c new file mode 100644 index 0000000..2959d82 --- /dev/null +++ b/src/main/realms.c @@ -0,0 +1,3247 @@ +/* + * realms.c Realm handling code + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2007 The FreeRADIUS server project + * Copyright 2007 Alan DeKok <aland@deployingradius.com> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/realms.h> +#include <freeradius-devel/rad_assert.h> + +#include <sys/stat.h> + +#include <ctype.h> +#include <fcntl.h> + +static rbtree_t *realms_byname = NULL; +#ifdef WITH_TCP +bool home_servers_udp = false; +#endif + +#ifdef HAVE_DIRENT_H +#include <dirent.h> +#endif + +#ifdef HAVE_REGEX +typedef struct realm_regex realm_regex_t; + +/** Regular expression associated with a realm + * + */ +struct realm_regex { + REALM *realm; //!< The realm this regex matches. + regex_t *preg; //!< The pre-compiled regular expression. + realm_regex_t *next; //!< The next realm in the list of regular expressions. +}; +static realm_regex_t *realms_regex = NULL; +#endif /* HAVE_REGEX */ + +struct realm_config { + CONF_SECTION *cs; +#ifdef HAVE_DIRENT_H + char const *directory; +#endif + uint32_t dead_time; + uint32_t retry_count; + uint32_t retry_delay; + bool dynamic; + bool fallback; + bool wake_all_if_all_dead; +}; + +static const FR_NAME_NUMBER home_server_types[] = { + { "auth", HOME_TYPE_AUTH }, + { "acct", HOME_TYPE_ACCT }, + { "auth+acct", HOME_TYPE_AUTH_ACCT }, + { "coa", HOME_TYPE_COA }, +#ifdef WITH_COA_TUNNEL + { "auth+coa", HOME_TYPE_AUTH_COA }, + { "auth+acct+coa", HOME_TYPE_AUTH_ACCT_COA }, +#endif + { NULL, 0 } +}; + +static const FR_NAME_NUMBER home_ping_check[] = { + { "none", HOME_PING_CHECK_NONE }, + { "status-server", HOME_PING_CHECK_STATUS_SERVER }, + { "request", HOME_PING_CHECK_REQUEST }, + { NULL, 0 } +}; + +static const FR_NAME_NUMBER home_proto[] = { + { "UDP", IPPROTO_UDP }, + { "TCP", IPPROTO_TCP }, + { NULL, 0 } +}; + +#ifdef WITH_RADIUSV11 +extern int fr_radiusv11_client_init(fr_tls_server_conf_t *tls); +#endif + +static realm_config_t *realm_config = NULL; + +#ifdef WITH_PROXY +static rbtree_t *home_servers_byaddr = NULL; +static rbtree_t *home_servers_byname = NULL; +#ifdef WITH_STATS +int home_server_max_number = 0; +static rbtree_t *home_servers_bynumber = NULL; +#endif + +static rbtree_t *home_pools_byname = NULL; + +/* + * Map the proxy server configuration parameters to variables. + */ +static const CONF_PARSER proxy_config[] = { + { "retry_delay", FR_CONF_OFFSET(PW_TYPE_INTEGER, realm_config_t, retry_delay), STRINGIFY(RETRY_DELAY) }, + + { "retry_count", FR_CONF_OFFSET(PW_TYPE_INTEGER, realm_config_t, retry_count), STRINGIFY(RETRY_COUNT) }, + + { "default_fallback", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, realm_config_t, fallback), "no" }, + + { "dynamic", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, realm_config_t, dynamic), NULL }, + +#ifdef HAVE_DIRENT_H + { "directory", FR_CONF_OFFSET(PW_TYPE_STRING, realm_config_t, directory), NULL }, +#endif + + { "dead_time", FR_CONF_OFFSET(PW_TYPE_INTEGER, realm_config_t, dead_time), STRINGIFY(DEAD_TIME) }, + + { "wake_all_if_all_dead", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, realm_config_t, wake_all_if_all_dead), "no" }, + CONF_PARSER_TERMINATOR +}; +#endif + +static int realm_name_cmp(void const *one, void const *two) +{ + REALM const *a = one; + REALM const *b = two; + + return strcasecmp(a->name, b->name); +} + + +#ifdef WITH_PROXY +static void home_server_free(void *data) +{ + home_server_t *home = talloc_get_type_abort(data, home_server_t); + + talloc_free(home); +} + +static int home_server_name_cmp(void const *one, void const *two) +{ + home_server_t const *a = one; + home_server_t const *b = two; + + if (a->type < b->type) return -1; + if (a->type > b->type) return +1; + + return strcasecmp(a->name, b->name); +} + +static int home_server_addr_cmp(void const *one, void const *two) +{ + int rcode; + home_server_t const *a = one; + home_server_t const *b = two; + + if (a->virtual_server && !b->virtual_server) return -1; + if (!a->virtual_server && b->virtual_server) return +1; + if (a->virtual_server && b->virtual_server) { + rcode = a->type - b->type; + if (rcode != 0) return rcode; + return strcmp(a->virtual_server, b->virtual_server); + } + + if (a->port < b->port) return -1; + if (a->port > b->port) return +1; + +#ifdef WITH_TCP + if (a->proto < b->proto) return -1; + if (a->proto > b->proto) return +1; +#endif + + rcode = fr_ipaddr_cmp(&a->src_ipaddr, &b->src_ipaddr); + if (rcode != 0) return rcode; + + return fr_ipaddr_cmp(&a->ipaddr, &b->ipaddr); +} + +#ifdef WITH_STATS +static int home_server_number_cmp(void const *one, void const *two) +{ + home_server_t const *a = one; + home_server_t const *b = two; + + return (a->number - b->number); +} +#endif + +static int home_pool_name_cmp(void const *one, void const *two) +{ + home_pool_t const *a = one; + home_pool_t const *b = two; + + if (a->server_type < b->server_type) return -1; + if (a->server_type > b->server_type) return +1; + + return strcasecmp(a->name, b->name); +} + + +static size_t xlat_cs(CONF_SECTION *cs, char const *fmt, char *out, size_t outlen) +{ + char const *value = NULL; + + if (!fmt) { + DEBUG("No configuration item requested. Ignoring."); + + *out = '\0'; + return 0; + } + + /* + * Instance name + */ + if (strcmp(fmt, "instance") == 0) { + value = cf_section_name2(cs); + if (!value) { + *out = '\0'; + return 0; + } + } else { + CONF_PAIR *cp; + + cp = cf_pair_find(cs, fmt); + if (!cp || !(value = cf_pair_value(cp))) { + *out = '\0'; + return 0; + } + } + + strlcpy(out, value, outlen); + + return strlen(out); +} + + +/* + * Xlat for %{home_server:foo} + */ +static ssize_t xlat_home_server(UNUSED void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + if (!request->home_server) { + RWDEBUG("No home_server associated with this request"); + + *out = '\0'; + return 0; + } + + if (!fmt) { + RWDEBUG("No configuration item requested. Ignoring."); + + *out = '\0'; + return 0; + } + + if (strcmp(fmt, "state") == 0) { + char const *state; + + switch (request->home_server->state) { + case HOME_STATE_ALIVE: + state = "alive"; + break; + + case HOME_STATE_ZOMBIE: + state = "zombie"; + break; + + case HOME_STATE_IS_DEAD: + state = "dead"; + break; + + case HOME_STATE_CONNECTION_FAIL: + state = "fail"; + break; + + case HOME_STATE_ADMIN_DOWN: + state = "down"; + break; + + default: + state = "unknown"; + break; + } + + strlcpy(out, state, outlen); + return strlen(out); + } + + return xlat_cs(request->home_server->cs, fmt, out, outlen); +} + + +/* + * Xlat for %{home_server_pool:foo} + */ +static ssize_t xlat_server_pool(UNUSED void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + if (!request->home_pool) { + RWDEBUG("No home_pool associated with this request"); + + *out = '\0'; + return 0; + } + + if (!fmt) { + RWDEBUG("No configuration item requested. Ignoring."); + + *out = '\0'; + return 0; + } + + if (strcmp(fmt, "state") == 0) { + char const *state; + + if (request->home_pool->in_fallback) { + state = "fallback"; + + } else { + state = "alive"; + } + + strlcpy(out, state, outlen); + return strlen(out); + } + + return xlat_cs(request->home_pool->cs, fmt, out, outlen); +} + + +/* + * Xlat for %{home_server_dynamic:foo} + */ +static ssize_t xlat_home_server_dynamic(UNUSED void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + int type; + char const *p, *name; + home_server_t *home; + char buffer[1024]; + + if (outlen < 2) return 0; + + switch (request->packet->code) { + case PW_CODE_ACCESS_REQUEST: + type = HOME_TYPE_AUTH; + break; + +#ifdef WITH_ACCOUNTING + case PW_CODE_ACCOUNTING_REQUEST: + type = HOME_TYPE_ACCT; + break; +#endif + +#ifdef WITH_COA + case PW_CODE_COA_REQUEST: + case PW_CODE_DISCONNECT_REQUEST: + type = HOME_TYPE_COA; + break; +#endif + + default: + *out = '\0'; + return 0; + } + + p = fmt; + while (isspace((uint8_t) *p)) p++; + + /* + * Allow for dynamic strings as arguments. + */ + if (*p == '&') { + VALUE_PAIR *vp; + + if ((radius_get_vp(&vp, request, p) < 0) || !vp || + (vp->da->type != PW_TYPE_STRING)) { + return -1; + } + name = vp->vp_strvalue; + + } else if (*p == '%') { + if (radius_xlat(buffer, sizeof(buffer), request, p, NULL, NULL) < 0) { + return -1; + } + name = buffer; + + } else { + name = p; + } + + home = home_server_byname(name, type); + if (!home) { + *out = '\0'; + return 0; + } + + /* + * 1 for dynamic, 0 for static + */ + out[0] = '0' + home->dynamic; + out[1] = '\0'; + + return 1; +} +#endif + +void realms_free(void) +{ +#ifdef WITH_PROXY +# ifdef WITH_STATS + rbtree_free(home_servers_bynumber); + home_servers_bynumber = NULL; +# endif + + rbtree_free(home_servers_byname); + home_servers_byname = NULL; + + rbtree_free(home_servers_byaddr); + home_servers_byaddr = NULL; + + rbtree_free(home_pools_byname); + home_pools_byname = NULL; +#endif + + rbtree_free(realms_byname); + realms_byname = NULL; + + realm_pool_free(NULL); + + talloc_free(realm_config); + realm_config = NULL; +} + + +#ifdef WITH_PROXY +static CONF_PARSER limit_config[] = { + { "max_connections", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, limit.max_connections), "16" }, + { "max_requests", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, limit.max_requests), "0" }, + { "lifetime", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, limit.lifetime), "0" }, + { "idle_timeout", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, limit.idle_timeout), "0" }, + CONF_PARSER_TERMINATOR +}; + +#ifdef WITH_COA +static CONF_PARSER home_server_coa[] = { + { "irt", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, coa_irt), STRINGIFY(2) }, + { "mrt", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, coa_mrt), STRINGIFY(16) }, + { "mrc", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, coa_mrc), STRINGIFY(5) }, + { "mrd", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, coa_mrd), STRINGIFY(30) }, + CONF_PARSER_TERMINATOR +}; + + + +#ifdef WITH_COA_TUNNEL +static CONF_PARSER home_server_recv_coa[] = { + { "virtual_server", FR_CONF_OFFSET(PW_TYPE_STRING, home_server_t, recv_coa_server), NULL }, + CONF_PARSER_TERMINATOR +}; +#endif + +#endif + +static CONF_PARSER home_server_config[] = { + { "nonblock", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, home_server_t, nonblock), "no" }, + { "ipaddr", FR_CONF_OFFSET(PW_TYPE_COMBO_IP_ADDR, home_server_t, ipaddr), NULL }, + { "ipv4addr", FR_CONF_OFFSET(PW_TYPE_IPV4_ADDR, home_server_t, ipaddr), NULL }, + { "ipv6addr", FR_CONF_OFFSET(PW_TYPE_IPV6_ADDR, home_server_t, ipaddr), NULL }, + { "virtual_server", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_NOT_EMPTY, home_server_t, virtual_server), NULL }, + + { "port", FR_CONF_OFFSET(PW_TYPE_SHORT, home_server_t, port), "0" }, + + { "type", FR_CONF_OFFSET(PW_TYPE_STRING, home_server_t, type_str), NULL }, + +#ifdef WITH_TCP + { "proto", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_NOT_EMPTY, home_server_t, proto_str), NULL }, +#endif + + { "secret", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_SECRET, home_server_t, secret), NULL }, + + { "src_ipaddr", FR_CONF_OFFSET(PW_TYPE_STRING, home_server_t, src_ipaddr_str), NULL }, + + { "response_window", FR_CONF_OFFSET(PW_TYPE_TIMEVAL, home_server_t, response_window), "30" }, + { "response_timeouts", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, max_response_timeouts), "1" }, + { "max_outstanding", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, max_outstanding), "65536" }, + + { "zombie_period", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, zombie_period), "40" }, + + { "status_check", FR_CONF_OFFSET(PW_TYPE_STRING, home_server_t, ping_check_str), "none" }, + { "ping_check", FR_CONF_OFFSET(PW_TYPE_STRING, home_server_t, ping_check_str), NULL }, + + { "ping_interval", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, ping_interval), "30" }, + { "check_interval", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, ping_interval), NULL }, + + { "check_timeout", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, ping_timeout), "4" }, + { "status_check_timeout", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, ping_timeout), NULL }, + + { "num_answers_to_alive", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, num_pings_to_alive), "3" }, + { "revive_interval", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, revive_interval), "300" }, + + { "username", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_NOT_EMPTY, home_server_t, ping_user_name), NULL }, + { "password", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_NOT_EMPTY, home_server_t, ping_user_password), NULL }, + +#ifdef WITH_STATS + { "historic_average_window", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, ema.window), NULL }, +#endif + + { "limit", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) limit_config }, + +#ifdef WITH_COA + { "coa", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) home_server_coa }, +#ifdef WITH_COA_TUNNEL + { "recv_coa", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) home_server_recv_coa }, +#endif +#endif + + CONF_PARSER_TERMINATOR +}; + + +static void null_free(UNUSED void *data) +{ +} + +/* + * Ensure that all of the parameters in the home server are OK. + */ +void realm_home_server_sanitize(home_server_t *home, CONF_SECTION *cs) +{ + CONF_SECTION *parent = NULL; + + FR_INTEGER_BOUND_CHECK("max_outstanding", home->max_outstanding, >=, 8); + FR_INTEGER_BOUND_CHECK("max_outstanding", home->max_outstanding, <=, 65536*16); + + FR_INTEGER_BOUND_CHECK("ping_interval", home->ping_interval, >=, 6); + FR_INTEGER_BOUND_CHECK("ping_interval", home->ping_interval, <=, 120); + + FR_TIMEVAL_BOUND_CHECK("response_window", &home->response_window, >=, 0, 1000); + FR_TIMEVAL_BOUND_CHECK("response_window", &home->response_window, <=, + main_config.max_request_time, 0); + FR_TIMEVAL_BOUND_CHECK("response_window", &home->response_window, <=, 60, 0); + + FR_INTEGER_BOUND_CHECK("response_timeouts", home->max_response_timeouts, >=, 1); + FR_INTEGER_BOUND_CHECK("response_timeouts", home->max_response_timeouts, <=, 1000); + + /* + * Track the minimum response window, so that we can + * correctly set the timers in process.c + */ + if (timercmp(&main_config.init_delay, &home->response_window, >)) { + main_config.init_delay = home->response_window; + } + + FR_INTEGER_BOUND_CHECK("zombie_period", home->zombie_period, >=, 1); + FR_INTEGER_BOUND_CHECK("zombie_period", home->zombie_period, <=, 120); + FR_INTEGER_BOUND_CHECK("zombie_period", home->zombie_period, >=, (uint32_t) home->response_window.tv_sec); + + FR_INTEGER_BOUND_CHECK("num_pings_to_alive", home->num_pings_to_alive, >=, 3); + FR_INTEGER_BOUND_CHECK("num_pings_to_alive", home->num_pings_to_alive, <=, 10); + + FR_INTEGER_BOUND_CHECK("check_timeout", home->ping_timeout, >=, 1); + FR_INTEGER_BOUND_CHECK("check_timeout", home->ping_timeout, <=, 10); + + FR_INTEGER_BOUND_CHECK("revive_interval", home->revive_interval, >=, 10); + FR_INTEGER_BOUND_CHECK("revive_interval", home->revive_interval, <=, 3600); + +#ifdef WITH_COA + FR_INTEGER_BOUND_CHECK("coa_irt", home->coa_irt, >=, 1); + FR_INTEGER_BOUND_CHECK("coa_irt", home->coa_irt, <=, 5); + + FR_INTEGER_BOUND_CHECK("coa_mrc", home->coa_mrc, <=, 20); + + FR_INTEGER_BOUND_CHECK("coa_mrt", home->coa_mrt, <=, 30); + + FR_INTEGER_BOUND_CHECK("coa_mrd", home->coa_mrd, >=, 5); + FR_INTEGER_BOUND_CHECK("coa_mrd", home->coa_mrd, <=, 60); +#endif + + FR_INTEGER_BOUND_CHECK("max_connections", home->limit.max_connections, <=, 1024); + +#ifdef WITH_TCP + /* + * UDP sockets can't be connection limited. + */ + if (home->proto != IPPROTO_TCP) home->limit.max_connections = 0; +#endif + + if ((home->limit.idle_timeout > 0) && (home->limit.idle_timeout < 5)) + home->limit.idle_timeout = 5; + if ((home->limit.lifetime > 0) && (home->limit.lifetime < 5)) + home->limit.lifetime = 5; + if ((home->limit.lifetime > 0) && (home->limit.idle_timeout > home->limit.lifetime)) + home->limit.idle_timeout = 0; + + /* + * Make sure that this is set. + */ + if (home->src_ipaddr.af == AF_UNSPEC) { + home->src_ipaddr.af = home->ipaddr.af; + } + + parent = cf_item_parent(cf_section_to_item(cs)); + if (parent && strcmp(cf_section_name1(parent), "server") == 0) { + home->parent_server = cf_section_name2(parent); + } +} + +/** Insert a new home server into the various internal lookup trees + * + * @param home server to add. + * @param cs That defined the home server. + * @return true on success else false. + */ +static bool home_server_insert(home_server_t *home, CONF_SECTION *cs) +{ + if (home->name && !rbtree_insert(home_servers_byname, home)) { + cf_log_err_cs(cs, "Internal error %d adding home server %s", __LINE__, home->log_name); + return false; + } + + if (!home->virtual_server && !rbtree_insert(home_servers_byaddr, home)) { + rbtree_deletebydata(home_servers_byname, home); + cf_log_err_cs(cs, "Internal error %d adding home server %s", __LINE__, home->log_name); + return false; + } + +#ifdef WITH_STATS + home->number = home_server_max_number++; + if (!rbtree_insert(home_servers_bynumber, home)) { + rbtree_deletebydata(home_servers_byname, home); + if (home->ipaddr.af != AF_UNSPEC) { + rbtree_deletebydata(home_servers_byname, home); + } + cf_log_err_cs(cs, "Internal error %d adding home server %s", __LINE__, home->log_name); + return false; + } +#endif + + return true; +} + +/** Add an already allocate home_server_t to the various trees + * + * @param home server to add. + * @return true on success, else false on error. + */ +bool realm_home_server_add(home_server_t *home) +{ + /* + * The structs aren't mutex protected. Refuse to destroy + * the server. + */ + if (event_loop_started && !realm_config->dynamic) { + ERROR("Failed to add dynamic home server, \"dynamic = yes\" must be set in proxy.conf"); + return false; + } + + if (home->name && (rbtree_finddata(home_servers_byname, home) != NULL)) { + cf_log_err_cs(home->cs, "Duplicate home server name %s", home->name); + return false; + } + + if (!home->virtual_server && (rbtree_finddata(home_servers_byaddr, home) != NULL)) { + char buffer[INET6_ADDRSTRLEN + 3]; + + inet_ntop(home->ipaddr.af, &home->ipaddr.ipaddr, buffer, sizeof(buffer)); + + cf_log_err_cs(home->cs, "Duplicate home server address%s%s%s: %s:%s%s/%i", + home->name ? " (already in use by " : "", + home->name ? home->name : "", + home->name ? ")" : "", + buffer, + fr_int2str(home_proto, home->proto, "<INVALID>"), +#ifdef WITH_TLS + home->tls ? "+tls" : "", +#else + "", +#endif + home->port); + + return false; + } + + if (!home_server_insert(home, home->cs)) return false; + + /* + * Dual home servers cause us to auto-create an + * accounting server for UDP sockets, and leave + * everything alone for TLS sockets. + */ + if (home->dual +#ifdef WITH_TLS + && !home->tls +#endif +) { + home_server_t *home2 = talloc(talloc_parent(home), home_server_t); + + memcpy(home2, home, sizeof(*home2)); + + home2->type = HOME_TYPE_ACCT; + home2->dual = true; + home2->port++; + + home2->ping_user_password = NULL; + home2->cs = home->cs; + home2->parent_server = home->parent_server; + + if (!home_server_insert(home2, home->cs)) { + talloc_free(home2); + return false; + } + } + +#ifdef WITH_COA_TUNNEL + if (home->recv_coa) { + if (!home->tls) { + ERROR("TLS is required in order to accept CoA requests from a home server"); + return false; + } + + if (!home->recv_coa_server) { + ERROR("A 'virtual_server' configuration is required in order to accept CoA requests from a home server"); + return false; + } + } +#endif + + /* + * Mark it as already processed + */ + cf_data_add(home->cs, "home_server", (void *)null_free, null_free); + + return true; +} + +#ifdef WITH_TLS +/* + * The listeners are always different. And we always look them up by *known* listener. And not "find me some random thing". + */ +static int listener_cmp(void const *one, void const *two) +{ + if (one < two) return -1; + if (one > two) return +1; + + return 0; +} +#endif + +/** Alloc a new home server defined by a CONF_SECTION + * + * @param ctx to allocate home_server_t in. + * @param rc Realm config, may be NULL in which case the global realm_config will be used. + * @param cs Configuration section containing home server parameters. + * @return a new home_server_t alloced in the context of the realm_config, or NULL on error. + */ +home_server_t *home_server_afrom_cs(TALLOC_CTX *ctx, realm_config_t *rc, CONF_SECTION *cs) +{ + home_server_t *home; + CONF_SECTION *tls; + + if (!rc) rc = realm_config; /* Use the global config */ + + home = talloc_zero(ctx, home_server_t); + home->name = cf_section_name2(cs); + home->log_name = talloc_typed_strdup(home, home->name); + home->cs = cs; + home->state = HOME_STATE_UNKNOWN; + home->proto = IPPROTO_UDP; + + /* + * Parse the configuration into the home server + * struct. + */ + if (cf_section_parse(cs, home, home_server_config) < 0) goto error; + + /* + * It has an IP address, it must be a remote server. + */ + if (cf_pair_find(cs, "ipaddr") || cf_pair_find(cs, "ipv4addr") || cf_pair_find(cs, "ipv6addr")) { + if (fr_inaddr_any(&home->ipaddr) == 1) { + cf_log_err_cs(cs, "Wildcard '*' addresses are not permitted for home servers"); + goto error; + } + + if (!home->log_name) { + char buffer[INET6_ADDRSTRLEN + 3]; + + fr_ntop(buffer, sizeof(buffer), &home->ipaddr); + + home->log_name = talloc_asprintf(home, "%s:%i", buffer, home->port); + } + /* + * If it has a 'virtual_Server' config item, it's + * a loopback into a virtual server. + */ + } else if (cf_pair_find(cs, "virtual_server") != NULL) { + home->ipaddr.af = AF_UNSPEC; /* mark ipaddr as unused */ + + if (!home->virtual_server) { + cf_log_err_cs(cs, "Invalid value for virtual_server"); + goto error; + } + + /* + * Try and find a "server" section off the root of + * the config with a name that matches the + * virtual_server. + */ + if (!rc) goto error; + + if (!cf_section_sub_find_name2(rc->cs, "server", home->virtual_server)) { + cf_log_err_cs(cs, "No such server %s", home->virtual_server); + goto error; + } + + home->secret = ""; + home->log_name = talloc_typed_strdup(home, home->virtual_server); + /* + * Otherwise it's an invalid config section and we + * raise an error. + */ + } else { + cf_log_err_cs(cs, "No ipaddr, ipv4addr, ipv6addr, or virtual_server defined " + "for home server"); + error: + talloc_free(home); + return false; + } + + { + home_type_t type = HOME_TYPE_AUTH_ACCT; + + if (home->type_str) type = fr_str2int(home_server_types, home->type_str, HOME_TYPE_INVALID); + + home->type = type; + + switch (type) { + case HOME_TYPE_AUTH_ACCT: + home->dual = true; + break; + + case HOME_TYPE_AUTH: + case HOME_TYPE_ACCT: + break; + +#ifdef WITH_COA + case HOME_TYPE_COA: + if (home->virtual_server != NULL) { + cf_log_err_cs(cs, "Home servers of type \"coa\" cannot point to a virtual server"); + goto error; + } + break; + +#ifdef WITH_COA_TUNNEL + case HOME_TYPE_AUTH_ACCT_COA: + home->dual = true; + home->recv_coa = true; + break; + + case HOME_TYPE_AUTH_COA: + home->recv_coa = true; + break; +#endif +#endif + + case HOME_TYPE_INVALID: + cf_log_err_cs(cs, "Invalid type \"%s\" for home server %s", home->type_str, home->log_name); + goto error; + } + } + + { + home_ping_check_t type = HOME_PING_CHECK_NONE; + + if (home->ping_check_str) type = fr_str2int(home_ping_check, home->ping_check_str, + HOME_PING_CHECK_INVALID); + + switch (type) { + case HOME_PING_CHECK_STATUS_SERVER: + case HOME_PING_CHECK_NONE: + break; + + case HOME_PING_CHECK_REQUEST: + if (!home->ping_user_name) { + cf_log_err_cs(cs, "You must supply a 'username' to enable status_check=request"); + goto error; + } + + if (((home->type == HOME_TYPE_AUTH) || +#ifdef WITH_COA_TUNNEL + (home->type == HOME_TYPE_AUTH_COA) || + (home->type == HOME_TYPE_AUTH_ACCT_COA) || +#endif + (home->type == HOME_TYPE_AUTH_ACCT)) && !home->ping_user_password) { + cf_log_err_cs(cs, "You must supply a 'password' to enable status_check=request"); + goto error; + } + + break; + + case HOME_PING_CHECK_INVALID: + cf_log_err_cs(cs, "Invalid status_check \"%s\" for home server %s", + home->ping_check_str, home->log_name); + goto error; + } + + home->ping_check = type; + } + + { + int proto = IPPROTO_UDP; + + if (home->proto_str) proto = fr_str2int(home_proto, home->proto_str, -1); + + switch (proto) { + case IPPROTO_UDP: +#ifdef WITH_TCP + home_servers_udp = true; +#endif + break; + + case IPPROTO_TCP: +#ifndef WITH_TCP + cf_log_err_cs(cs, "Server not built with support for RADIUS over TCP"); + goto error; +#endif + if ((home->ping_check != HOME_PING_CHECK_NONE) && + (home->ping_check != HOME_PING_CHECK_STATUS_SERVER)) { + cf_log_err_cs(cs, "Only 'status_check = status-server' is allowed for home " + "servers with 'proto = tcp'"); + goto error; + } + break; + + default: + cf_log_err_cs(cs, "Unknown proto \"%s\"", home->proto_str); + goto error; + } + + home->proto = proto; + } + + if (!home->virtual_server && rbtree_finddata(home_servers_byaddr, home)) { + cf_log_err_cs(cs, "Duplicate home server"); + goto error; + } + + /* + * Check the TLS configuration. + */ + tls = cf_section_sub_find(cs, "tls"); +#ifndef WITH_TLS + if (tls) { + cf_log_err_cs(cs, "TLS transport is not available in this executable"); + goto error; + } +#endif + + /* + * Check the reverse CoA configuration. + */ +#ifdef WITH_COA_TUNNEL + if (home->recv_coa) { + if (!tls) { + ERROR("TLS is required in order to accept CoA requests from a home server"); + goto error; + } + + if (!home->recv_coa_server) { + ERROR("A 'virtual_server' configuration is required in order to accept CoA requests from a home server"); + goto error; + } + + /* + * Try and find a 'server' section off the root of + * the config with a name that matches the coa + * virtual_server. + */ + if (!rc) { + ERROR("Dynamic home servers cannot accept CoA requests"); + goto error; + } + + if (!cf_section_sub_find_name2(rc->cs, "server", home->recv_coa_server)) { + cf_log_err_cs(cs, "No such coa server %s", home->recv_coa_server); + goto error; + } + } +#endif + + /* + * If were doing RADSEC (tls+tcp) the secret should default + * to radsec, else a secret must be set. + */ + if (!home->secret) { +#ifdef WITH_TLS + if (tls && (home->proto == IPPROTO_TCP)) { + home->secret = "radsec"; + } else +#endif + { + cf_log_err_cs(cs, "No shared secret defined for home server %s", home->log_name); + goto error; + } + } + + /* + * Virtual servers have some TLS restrictions. + */ + if (home->virtual_server) { + if (tls) { + cf_log_err_cs(cs, "Virtual home_servers cannot have a \"tls\" subsection"); + goto error; + } + } else { + /* + * If the home is not a virtual server, guess the port + * and look up the source ip address. + */ + rad_assert(home->ipaddr.af != AF_UNSPEC); + +#ifdef WITH_TLS + if (tls && (home->proto != IPPROTO_TCP)) { + cf_log_err_cs(cs, "TLS transport is not available for UDP sockets"); + goto error; + } +#endif + + /* + * Set the default port if necessary. + */ + if (home->port == 0) { + char buffer[INET6_ADDRSTRLEN + 3]; + + /* + * For RADSEC we use the special RADIUS over TCP/TLS port + * for both accounting and authentication, but for some + * bizarre reason for RADIUS over plain TCP we use separate + * ports 1812 and 1813. + */ +#ifdef WITH_TLS + if (tls) { + home->port = PW_RADIUS_TLS_PORT; + } else +#endif + switch (home->type) { + default: + rad_assert(0); + /* FALL-THROUGH */ + + /* + * One is added to get the accounting port + * for home->dual. + */ + case HOME_TYPE_AUTH_ACCT: + case HOME_TYPE_AUTH: + home->port = PW_AUTH_UDP_PORT; + break; + + case HOME_TYPE_ACCT: + home->port = PW_ACCT_UDP_PORT; + break; + + case HOME_TYPE_COA: + home->port = PW_COA_UDP_PORT; + break; + } + + /* + * Now that we have a real port, use that. + */ + rad_const_free(home->log_name); + + fr_ntop(buffer, sizeof(buffer), &home->ipaddr); + + home->log_name = talloc_asprintf(home, "%s:%i", buffer, home->port); + } + + /* + * If we have a src_ipaddr_str resolve it to + * the same address family as the destination + * IP. + */ + if (home->src_ipaddr_str) { + if (ip_hton(&home->src_ipaddr, home->ipaddr.af, home->src_ipaddr_str, false) < 0) { + cf_log_err_cs(cs, "Failed parsing src_ipaddr"); + goto error; + } + /* + * Source isn't specified, set it to the + * correct address family, but leave it as + * zeroes. + */ + } else { + home->src_ipaddr.af = home->ipaddr.af; + } + +#ifdef WITH_TLS + /* + * Parse the SSL client configuration. + */ + if (tls) { + int rcode; + + home->tls = tls_client_conf_parse(tls); + if (!home->tls) { + goto error; + } + + /* + * Connection timeouts for outgoing TLS connections. + */ + + rcode = cf_item_parse(tls, "connect_timeout", FR_ITEM_POINTER(PW_TYPE_INTEGER, &home->connect_timeout), NULL); + if (rcode < 0) goto error; + + if (!home->connect_timeout || (home->connect_timeout > 30)) home->connect_timeout = 30; + + home->listeners = rbtree_create(home, listener_cmp, NULL, RBTREE_FLAG_LOCK); + if (!home->listeners) goto error; + +#ifdef WITH_RADIUSV11 + if (home->tls->radiusv11_name) { + rcode = fr_str2int(radiusv11_types, home->tls->radiusv11_name, -1); + if (rcode < 0) { + cf_log_err_cs(cs, "Invalid value for 'radiusv11'"); + goto error; + } + + home->tls->radiusv11 = rcode; + + if (fr_radiusv11_client_init(home->tls) < 0) { + cf_log_err_cs(cs, "Failed setting OpenSSL callbacks for radiusv11"); + goto error; + } + } +#endif + + } +#endif + } /* end of parse home server */ + + realm_home_server_sanitize(home, cs); + + return home; +} + +/** Fixup a client configuration section to specify a home server + * + * This is used to create the equivalent CoA home server entry for a client, + * so that the server can originate CoA messages. + * + * The server section automatically inherits the following fields from the client: + * - ipaddr/ipv4addr/ipv6addr + * - secret + * - src_ipaddr + * + * @note new CONF_SECTION will be allocated in the context of the client, but the client + * CONF_SECTION will not be modified. + * + * @param client CONF_SECTION to inherit values from. + * @return a new server CONF_SCTION, or a pointer to the existing CONF_SECTION in the client. + */ +CONF_SECTION *home_server_cs_afrom_client(CONF_SECTION *client) +{ + CONF_SECTION *server, *cs; + CONF_PAIR *cp; + + /* + * Alloc a plain home server for both cases + * + * There's no way these can be referenced by a pool, + * and they may conflict with home servers in proxy.conf + * so it's easier to not set a name. + */ + + /* + * + * Duplicate the server section, so we don't mangle + * the client CONF_SECTION we were passed. + */ + cs = cf_section_sub_find(client, "coa_server"); + if (cs) { + server = cf_section_dup(client, cs, "home_server", NULL, true); + } else { + server = cf_section_alloc(client, "home_server", cf_section_name2(client)); + } + + if (!cs || (!cf_pair_find(cs, "ipaddr") && !cf_pair_find(cs, "ipv4addr") && !cf_pair_find(cs, "ipv6addr"))) { + cp = cf_pair_find(client, "ipaddr"); + if (!cp) cp = cf_pair_find(client, "ipv4addr"); + if (!cp) cp = cf_pair_find(client, "ipv6addr"); + + cf_pair_add(server, cf_pair_dup(server, cp)); + } + + if (!cs || !cf_pair_find(cs, "secret")) { + cp = cf_pair_find(client, "secret"); + if (cp) cf_pair_add(server, cp); + } + + if (!cs || !cf_pair_find(cs, "src_ipaddr")) { + cp = cf_pair_find(client, "src_ipaddr"); + if (cp) cf_pair_add(server, cf_pair_dup(server, cp)); + } + + if (!cs || !(cp = cf_pair_find(cs, "type"))) { + cp = cf_pair_alloc(server, "type", "coa", T_OP_EQ, T_BARE_WORD, T_SINGLE_QUOTED_STRING); + if (cp) cf_pair_add(server, cf_pair_dup(server, cp)); + } else if (strcmp(cf_pair_value(cp), "coa") != 0) { + talloc_free(server); + cf_log_err_cs(server, "server.type must be \"coa\""); + return NULL; + } + + return server; +} + +static home_pool_t *server_pool_alloc(char const *name, home_pool_type_t type, + home_type_t server_type, int num_home_servers) +{ + home_pool_t *pool; + + pool = rad_malloc(sizeof(*pool) + (sizeof(pool->servers[0]) * num_home_servers)); + if (!pool) return NULL; /* just for pairanoia */ + + memset(pool, 0, sizeof(*pool) + (sizeof(pool->servers[0]) * num_home_servers)); + + pool->name = name; + pool->type = type; + pool->server_type = server_type; + pool->num_home_servers = num_home_servers; + + return pool; +} + +/* + * Ensure any home_server clauses in a home_server_pool section reference + * defined home servers, which should already have been created, regardless + * of where they appear in the configuration. + */ +static int pool_check_home_server(UNUSED realm_config_t *rc, CONF_PAIR *cp, + char const *name, home_type_t server_type, + home_server_t **phome) +{ + home_server_t myhome, *home; + + if (!name) { + cf_log_err_cp(cp, + "No value given for home_server"); + return 0; + } + + myhome.name = name; + myhome.type = server_type; + home = rbtree_finddata(home_servers_byname, &myhome); + if (home) { + *phome = home; + return 1; + } + + switch (server_type) { + case HOME_TYPE_AUTH: + case HOME_TYPE_ACCT: + myhome.type = HOME_TYPE_AUTH_ACCT; + home = rbtree_finddata(home_servers_byname, &myhome); +#ifdef WITH_COA_TUNNEL + if (!home) { + myhome.type = HOME_TYPE_AUTH_COA; + home = rbtree_finddata(home_servers_byname, &myhome); + if(!home) { + myhome.type = HOME_TYPE_AUTH_ACCT_COA; + home = rbtree_finddata(home_servers_byname, &myhome); + } + } +#endif + if (home) { + *phome = home; + return 1; + } + break; + + default: + break; + } + + cf_log_err_cp(cp, "Unknown home_server \"%s\".", name); + return 0; +} + + +#ifndef HAVE_PTHREAD_H +void realm_pool_free(home_pool_t *pool) +{ + if (!event_loop_started) return; + if (!realm_config->dynamic) return; + + talloc_free(pool); +} +#else /* HAVE_PTHREAD_H */ +typedef struct pool_list_t pool_list_t; + +struct pool_list_t { + pool_list_t *next; + home_pool_t *pool; + time_t when; +}; + +static bool pool_free_init = false; +static pthread_mutex_t pool_free_mutex; +static pool_list_t *pool_list = NULL; + +void realm_pool_free(home_pool_t *pool) +{ + int i; + time_t now; + pool_list_t *this, **last; + + if (!event_loop_started) return; + if (!realm_config->dynamic) return; + + if (pool) { + /* + * Double-check that the realm wasn't loaded from the + * configuration files. + */ + for (i = 0; i < pool->num_home_servers; i++) { + if (pool->servers[i]->cs) { + rad_assert(0 == 1); + return; + } + } + } + + if (!pool_free_init) { + pthread_mutex_init(&pool_free_mutex, NULL); + pool_free_init = true; + } + + /* + * Ensure only one caller at a time is freeing a pool. + */ + pthread_mutex_lock(&pool_free_mutex); + + /* + * Free all of the pools. + */ + if (!pool) { + while ((this = pool_list) != NULL) { + pool_list = this->next; + talloc_free(this->pool); + talloc_free(this); + } + pthread_mutex_unlock(&pool_free_mutex); + return; + } + + now = time(NULL); + + /* + * Free the oldest pool(s) + */ + while ((this = pool_list) != NULL) { + if (this->when > now) break; + + pool_list = this->next; + talloc_free(this->pool); + talloc_free(this); + } + + /* + * Add this pool to the end of the list. + */ + for (last = &pool_list; + *last != NULL; + last = &((*last))->next) { + /* do nothing */ + } + + *last = this = talloc(NULL, pool_list_t); + if (!this) { + talloc_free(pool); /* hope for the best */ + pthread_mutex_unlock(&pool_free_mutex); + return; + } + + this->next = NULL; + this->when = now + 300; + this->pool = pool; + pthread_mutex_unlock(&pool_free_mutex); +} +#endif /* HAVE_PTHREAD_H */ + +int realm_pool_add(home_pool_t *pool, CONF_SECTION *cs) +{ + home_pool_t *old; + + old = rbtree_finddata(home_pools_byname, pool); + if (old) { + cf_log_err_cs(cs, "Cannot add duplicate home server %s, original is at %s[%d]", pool->name, + cf_section_filename(old->cs), cf_section_lineno(old->cs)); + return 0; + } + + /* + * The structs aren't mutex protected. Refuse to destroy + * the server. + */ + if (event_loop_started && !realm_config->dynamic) { + DEBUG("Must set \"dynamic = true\" in proxy.conf"); + return 0; + } + + if (!rbtree_insert(home_pools_byname, pool)) { + rad_assert("Internal sanity check failed" == NULL); + return 0; + } + + return 1; +} + +static int server_pool_add(realm_config_t *rc, + CONF_SECTION *cs, home_type_t server_type, bool do_print) +{ + char const *name2; + home_pool_t *pool = NULL; + char const *value; + CONF_PAIR *cp; + int num_home_servers; + home_server_t *home; + + name2 = cf_section_name1(cs); + if (!name2 || ((strcasecmp(name2, "server_pool") != 0) && + (strcasecmp(name2, "home_server_pool") != 0))) { + cf_log_err_cs(cs, + "Section is not a home_server_pool"); + return 0; + } + + name2 = cf_section_name2(cs); + if (!name2) { + cf_log_err_cs(cs, + "Server pool section is missing a name"); + return 0; + } + + /* + * Count the home servers and initalize them. + */ + num_home_servers = 0; + for (cp = cf_pair_find(cs, "home_server"); + cp != NULL; + cp = cf_pair_find_next(cs, cp, "home_server")) { + num_home_servers++; + + if (!pool_check_home_server(rc, cp, cf_pair_value(cp), + server_type, &home)) { + return 0; + } + } + + if (num_home_servers == 0) { + cf_log_err_cs(cs, + "No home servers defined in pool %s", + name2); + goto error; + } + + pool = server_pool_alloc(name2, HOME_POOL_FAIL_OVER, server_type, + num_home_servers); + if (!pool) { + cf_log_err_cs(cs, "Failed allocating memory for pool"); + goto error; + } + pool->cs = cs; + + + /* + * Fallback servers must be defined, and must be + * virtual servers. + */ + cp = cf_pair_find(cs, "fallback"); + if (cp) { +#ifdef WITH_COA + if (server_type == HOME_TYPE_COA) { + cf_log_err_cs(cs, "Home server pools of type \"coa\" cannot have a fallback virtual server"); + goto error; + } +#endif + + if (!pool_check_home_server(rc, cp, cf_pair_value(cp), server_type, &pool->fallback)) { + goto error; + } + + if (!pool->fallback->virtual_server) { + cf_log_err_cs(cs, "Fallback home_server %s does NOT contain a virtual_server directive", + pool->fallback->log_name); + goto error; + } + } + + if (do_print) cf_log_info(cs, " home_server_pool %s {", name2); + + cp = cf_pair_find(cs, "type"); + if (cp) { + static FR_NAME_NUMBER pool_types[] = { + { "load-balance", HOME_POOL_LOAD_BALANCE }, + + { "fail-over", HOME_POOL_FAIL_OVER }, + { "fail_over", HOME_POOL_FAIL_OVER }, + + { "round-robin", HOME_POOL_LOAD_BALANCE }, + { "round_robin", HOME_POOL_LOAD_BALANCE }, + + { "client-balance", HOME_POOL_CLIENT_BALANCE }, + { "client-port-balance", HOME_POOL_CLIENT_PORT_BALANCE }, + { "keyed-balance", HOME_POOL_KEYED_BALANCE }, + { NULL, 0 } + }; + + value = cf_pair_value(cp); + if (!value) { + cf_log_err_cp(cp, + "No value given for type"); + goto error; + } + + pool->type = fr_str2int(pool_types, value, 0); + if (!pool->type) { + cf_log_err_cp(cp, + "Unknown type \"%s\".", + value); + goto error; + } + + if (do_print) cf_log_info(cs, "\ttype = %s", value); + } + + cp = cf_pair_find(cs, "virtual_server"); + if (cp) { + pool->virtual_server = cf_pair_value(cp); + if (!pool->virtual_server) { + cf_log_err_cp(cp, "No value given for virtual_server"); + goto error; + } + + if (do_print) { + cf_log_info(cs, "\tvirtual_server = %s", pool->virtual_server); + } + + if (!cf_section_sub_find_name2(rc->cs, "server", pool->virtual_server)) { + cf_log_err_cp(cp, "No such server %s", pool->virtual_server); + goto error; + } + + } + + num_home_servers = 0; + for (cp = cf_pair_find(cs, "home_server"); + cp != NULL; + cp = cf_pair_find_next(cs, cp, "home_server")) { + home_server_t myhome; + + value = cf_pair_value(cp); + + memset(&myhome, 0, sizeof(myhome)); + myhome.name = value; + myhome.type = server_type; + + home = rbtree_finddata(home_servers_byname, &myhome); + if (!home) { + switch (server_type) { + case HOME_TYPE_AUTH: + case HOME_TYPE_ACCT: + myhome.type = HOME_TYPE_AUTH_ACCT; + home = rbtree_finddata(home_servers_byname, &myhome); +#ifdef WITH_COA_TUNNEL + if (!home) { + myhome.type = HOME_TYPE_AUTH_COA; + home = rbtree_finddata(home_servers_byname, &myhome); + if (!home) { + myhome.type = HOME_TYPE_AUTH_ACCT_COA; + home = rbtree_finddata(home_servers_byname, &myhome); + } + } +#endif + break; + + default: + break; + } + } + + if (!home) { + ERROR("Failed to find home server %s", value); + goto error; + } + + if (do_print) cf_log_info(cs, "\thome_server = %s", home->name); + pool->servers[num_home_servers++] = home; + } /* loop over home_server's */ + + if (pool->fallback && do_print) { + cf_log_info(cs, "\tfallback = %s", pool->fallback->name); + } + + if (!realm_pool_add(pool, cs)) goto error; + + if (do_print) cf_log_info(cs, " }"); + + cf_data_add(cs, "home_server_pool", pool, free); + + rad_assert(pool->server_type != 0); + + return 1; + + error: + if (do_print) cf_log_info(cs, " }"); + free(pool); + return 0; +} +#endif + +static int old_server_add(realm_config_t *rc, CONF_SECTION *cs, + char const *realm, + char const *name, char const *secret, + home_pool_type_t ldflag, home_pool_t **pool_p, + home_type_t type, char const *server) +{ +#ifdef WITH_PROXY + int i, insert_point, num_home_servers; + home_server_t myhome, *home; + home_pool_t mypool, *pool; + CONF_SECTION *subcs; +#else + (void) rc; /* -Wunused */ + (void) realm; + (void) secret; + (void) ldflag; + (void) type; + (void) server; +#endif + + /* + * LOCAL realms get sanity checked, and nothing else happens. + */ + if (strcmp(name, "LOCAL") == 0) { + if (*pool_p) { + cf_log_err_cs(cs, "Realm \"%s\" cannot be both LOCAL and remote", name); + return 0; + } + return 1; + } + +#ifndef WITH_PROXY + return 0; /* Not proxying. Can't do non-LOCAL realms */ + +#else + mypool.name = realm; + mypool.server_type = type; + pool = rbtree_finddata(home_pools_byname, &mypool); + if (pool) { + if (pool->type != ldflag) { + cf_log_err_cs(cs, "Inconsistent ldflag for server pool \"%s\"", name); + return 0; + } + + if (pool->server_type != type) { + cf_log_err_cs(cs, "Inconsistent home server type for server pool \"%s\"", name); + return 0; + } + } + + myhome.name = name; + myhome.type = type; + home = rbtree_finddata(home_servers_byname, &myhome); + if (home) { + WARN("Please use pools instead of authhost and accthost"); + + if (secret && (strcmp(home->secret, secret) != 0)) { + cf_log_err_cs(cs, "Inconsistent shared secret for home server \"%s\"", name); + return 0; + } + + if (home->type != type) { + cf_log_err_cs(cs, "Inconsistent type for home server \"%s\"", name); + return 0; + } + + /* + * Don't check for duplicate home servers. If + * the user specifies that, well, they can do it. + * + * Allowing duplicates means that all of the + * realm->server[] entries are filled, which is + * what the rest of the code assumes. + */ + } + + /* + * If we do have a pool, check that there is room to + * insert the home server we've found, or the one that we + * create here. + * + * Note that we insert it into the LAST available + * position, in order to maintain the same order as in + * the configuration files. + */ + insert_point = -1; + if (pool) { + for (i = pool->num_home_servers - 1; i >= 0; i--) { + if (pool->servers[i]) break; + + if (!pool->servers[i]) { + insert_point = i; + } + } + + if (insert_point < 0) { + cf_log_err_cs(cs, "No room in pool to add home server \"%s\". Please update the realm configuration to use the new-style home servers and server pools.", name); + return 0; + } + } + + /* + * No home server, allocate one. + */ + if (!home) { + char const *p; + char *q; + + home = talloc_zero(rc, home_server_t); + home->name = name; + home->type = type; + home->secret = secret; + home->cs = cs; + home->proto = IPPROTO_UDP; + + p = strchr(name, ':'); + if (!p) { + if (type == HOME_TYPE_AUTH) { + home->port = PW_AUTH_UDP_PORT; + } else { + home->port = PW_ACCT_UDP_PORT; + } + + p = name; + q = NULL; + + } else if (p == name) { + cf_log_err_cs(cs, "Invalid hostname %s", name); + talloc_free(home); + return 0; + } else { + unsigned long port = strtoul(p + 1, NULL, 0); + if ((port == 0) || (port > 65535)) { + cf_log_err_cs(cs, "Invalid port %s", p + 1); + talloc_free(home); + return 0; + } + + home->port = (uint16_t)port; + q = talloc_array(home, char, (p - name) + 1); + memcpy(q, name, (p - name)); + q[p - name] = '\0'; + p = q; + } + + if (!server) { + if (ip_hton(&home->ipaddr, AF_UNSPEC, p, false) < 0) { + cf_log_err_cs(cs, + "Failed looking up hostname %s.", + p); + talloc_free(home); + talloc_free(q); + return 0; + } + home->src_ipaddr.af = home->ipaddr.af; + } else { + home->ipaddr.af = AF_UNSPEC; + home->virtual_server = server; + } + talloc_free(q); + + /* + * Use the old-style configuration. + */ + home->max_outstanding = 65535*16; + home->zombie_period = rc->retry_delay * rc->retry_count; + if (home->zombie_period < 2) home->zombie_period = 30; + home->response_window.tv_sec = home->zombie_period - 1; + home->response_window.tv_usec = 0; + + home->ping_check = HOME_PING_CHECK_NONE; + + home->revive_interval = rc->dead_time; + + if (rbtree_finddata(home_servers_byaddr, home)) { + cf_log_err_cs(cs, "Home server %s has the same IP address and/or port as another home server.", name); + talloc_free(home); + return 0; + } + + if (!rbtree_insert(home_servers_byname, home)) { + cf_log_err_cs(cs, "Internal error %d adding home server %s.", __LINE__, name); + talloc_free(home); + return 0; + } + + if (!rbtree_insert(home_servers_byaddr, home)) { + rbtree_deletebydata(home_servers_byname, home); + cf_log_err_cs(cs, "Internal error %d adding home server %s.", __LINE__, name); + talloc_free(home); + return 0; + } + +#ifdef WITH_STATS + home->number = home_server_max_number++; + if (!rbtree_insert(home_servers_bynumber, home)) { + rbtree_deletebydata(home_servers_byname, home); + if (home->ipaddr.af != AF_UNSPEC) { + rbtree_deletebydata(home_servers_byname, home); + } + cf_log_err_cs(cs, + "Internal error %d adding home server %s.", + __LINE__, name); + talloc_free(home); + return 0; + } +#endif + } + + /* + * We now have a home server, see if we can insert it + * into pre-existing pool. + */ + if (insert_point >= 0) { + rad_assert(pool != NULL); + pool->servers[insert_point] = home; + return 1; + } + + rad_assert(pool == NULL); + rad_assert(home != NULL); + + /* + * Count the old-style realms of this name. + */ + num_home_servers = 0; + for (subcs = cf_section_find_next(cs, NULL, "realm"); + subcs != NULL; + subcs = cf_section_find_next(cs, subcs, "realm")) { + char const *this = cf_section_name2(subcs); + + if (!this || (strcmp(this, realm) != 0)) continue; + num_home_servers++; + } + + if (num_home_servers == 0) { + cf_log_err_cs(cs, "Internal error counting pools for home server %s.", name); + talloc_free(home); + return 0; + } + + pool = server_pool_alloc(realm, ldflag, type, num_home_servers); + if (!pool) { + cf_log_err_cs(cs, "Out of memory"); + return 0; + } + + pool->cs = cs; + + pool->servers[0] = home; + + if (!rbtree_insert(home_pools_byname, pool)) { + rad_assert("Internal sanity check failed" == NULL); + return 0; + } + + *pool_p = pool; + + return 1; +#endif +} + +static int old_realm_config(realm_config_t *rc, CONF_SECTION *cs, REALM *r) +{ + char const *host; + char const *secret = NULL; + home_pool_type_t ldflag; + CONF_PAIR *cp; + + cp = cf_pair_find(cs, "ldflag"); + ldflag = HOME_POOL_FAIL_OVER; + if (cp) { + host = cf_pair_value(cp); + if (!host) { + cf_log_err_cp(cp, "No value specified for ldflag"); + return 0; + } + + if (strcasecmp(host, "fail_over") == 0) { + cf_log_info(cs, "\tldflag = fail_over"); + + } else if (strcasecmp(host, "round_robin") == 0) { + ldflag = HOME_POOL_LOAD_BALANCE; + cf_log_info(cs, "\tldflag = round_robin"); + + } else { + cf_log_err_cs(cs, "Unknown value \"%s\" for ldflag", host); + return 0; + } + } /* else don't print it. */ + + /* + * Allow old-style if it doesn't exist, or if it exists and + * it's LOCAL. + */ + cp = cf_pair_find(cs, "authhost"); + if (cp) { + host = cf_pair_value(cp); + if (!host) { + cf_log_err_cp(cp, "No value specified for authhost"); + return 0; + } + + if (strcmp(host, "LOCAL") != 0) { + cp = cf_pair_find(cs, "secret"); + if (!cp) { + cf_log_err_cs(cs, "No shared secret supplied for realm: %s", r->name); + return 0; + } + + secret = cf_pair_value(cp); + if (!secret) { + cf_log_err_cp(cp, "No value specified for secret"); + return 0; + } + } + + cf_log_info(cs, "\tauthhost = %s", host); + + if (!old_server_add(rc, cs, r->name, host, secret, ldflag, + &r->auth_pool, HOME_TYPE_AUTH, NULL)) { + return 0; + } + } + + cp = cf_pair_find(cs, "accthost"); + if (cp) { + host = cf_pair_value(cp); + if (!host) { + cf_log_err_cp(cp, "No value specified for accthost"); + return 0; + } + + /* + * Don't look for a secret again if it was found + * above. + */ + if ((strcmp(host, "LOCAL") != 0) && !secret) { + cp = cf_pair_find(cs, "secret"); + if (!cp) { + cf_log_err_cs(cs, "No shared secret supplied for realm: %s", r->name); + return 0; + } + + secret = cf_pair_value(cp); + if (!secret) { + cf_log_err_cp(cp, "No value specified for secret"); + return 0; + } + } + + cf_log_info(cs, "\taccthost = %s", host); + + if (!old_server_add(rc, cs, r->name, host, secret, ldflag, + &r->acct_pool, HOME_TYPE_ACCT, NULL)) { + return 0; + } + } + + cp = cf_pair_find(cs, "virtual_server"); + if (cp) { + host = cf_pair_value(cp); + if (!host) { + cf_log_err_cp(cp, "No value specified for virtual_server"); + return 0; + } + + cf_log_info(cs, "\tvirtual_server = %s", host); + + if (!old_server_add(rc, cs, r->name, host, "", ldflag, + &r->auth_pool, HOME_TYPE_AUTH, host)) { + return 0; + } + if (!old_server_add(rc, cs, r->name, host, "", ldflag, + &r->acct_pool, HOME_TYPE_ACCT, host)) { + return 0; + } + } + + if (secret) { + if (rad_debug_lvl <= 2) { + cf_log_info(cs, "\tsecret = <<< secret >>>"); + } else { + cf_log_info(cs, "\tsecret = %s", secret); + } + } + + return 1; + +} + + +#ifdef WITH_PROXY +static int add_pool_to_realm(realm_config_t *rc, CONF_SECTION *cs, + char const *name, home_pool_t **dest, + home_type_t server_type, bool do_print) +{ + home_pool_t mypool, *pool; + + mypool.name = name; + mypool.server_type = server_type; + + pool = rbtree_finddata(home_pools_byname, &mypool); + if (!pool) { + CONF_SECTION *pool_cs; + + pool_cs = cf_section_sub_find_name2(rc->cs, + "home_server_pool", + name); + if (!pool_cs) { + pool_cs = cf_section_sub_find_name2(rc->cs, + "server_pool", + name); + } + if (!pool_cs) { + cf_log_err_cs(cs, "Failed to find home_server_pool \"%s\"", name); + return 0; + } + + if (!server_pool_add(rc, pool_cs, server_type, do_print)) { + return 0; + } + + pool = rbtree_finddata(home_pools_byname, &mypool); + if (!pool) { + ERROR("Internal sanity check failed in add_pool_to_realm"); + return 0; + } + } + + if (pool->server_type != server_type) { + cf_log_err_cs(cs, "Incompatible home_server_pool \"%s\" (mixed auth_pool / acct_pool)", name); + return 0; + } + + *dest = pool; + + return 1; +} +#endif + + +static int realm_add(realm_config_t *rc, CONF_SECTION *cs) +{ + char const *name2; + REALM *r = NULL; + CONF_PAIR *cp; +#ifdef WITH_PROXY + home_pool_t *auth_pool, *acct_pool; + char const *auth_pool_name, *acct_pool_name; +#ifdef WITH_COA + char const *coa_pool_name; + home_pool_t *coa_pool; +#endif +#endif + + name2 = cf_section_name1(cs); + if (!name2 || (strcasecmp(name2, "realm") != 0)) { + cf_log_err_cs(cs, "Section is not a realm"); + return 0; + } + + name2 = cf_section_name2(cs); + if (!name2) { + cf_log_err_cs(cs, "Realm section is missing the realm name"); + return 0; + } + +#ifdef WITH_PROXY + auth_pool = acct_pool = NULL; + auth_pool_name = acct_pool_name = NULL; +#ifdef WITH_COA + coa_pool = NULL; + coa_pool_name = NULL; +#endif + + /* + * Prefer new configuration to old one. + */ + cp = cf_pair_find(cs, "pool"); + if (!cp) cp = cf_pair_find(cs, "home_server_pool"); + if (cp) auth_pool_name = cf_pair_value(cp); + if (cp && auth_pool_name) { + acct_pool_name = auth_pool_name; + if (!add_pool_to_realm(rc, cs, + auth_pool_name, &auth_pool, + HOME_TYPE_AUTH, 1)) { + return 0; + } + if (!add_pool_to_realm(rc, cs, + auth_pool_name, &acct_pool, + HOME_TYPE_ACCT, 0)) { + return 0; + } + } + + cp = cf_pair_find(cs, "auth_pool"); + if (cp) auth_pool_name = cf_pair_value(cp); + if (cp && auth_pool_name) { + if (auth_pool) { + cf_log_err_cs(cs, "Cannot use \"pool\" and \"auth_pool\" at the same time"); + return 0; + } + if (!add_pool_to_realm(rc, cs, + auth_pool_name, &auth_pool, + HOME_TYPE_AUTH, 1)) { + return 0; + } + } + + cp = cf_pair_find(cs, "acct_pool"); + if (cp) acct_pool_name = cf_pair_value(cp); + if (cp && acct_pool_name) { + bool do_print = true; + + if (acct_pool) { + cf_log_err_cs(cs, "Cannot use \"pool\" and \"acct_pool\" at the same time"); + return 0; + } + + if (!auth_pool || + (auth_pool_name && + (strcmp(auth_pool_name, acct_pool_name) != 0))) { + do_print = true; + } + + if (!add_pool_to_realm(rc, cs, + acct_pool_name, &acct_pool, + HOME_TYPE_ACCT, do_print)) { + return 0; + } + } + +#ifdef WITH_COA + cp = cf_pair_find(cs, "coa_pool"); + if (cp) coa_pool_name = cf_pair_value(cp); + if (cp && coa_pool_name) { + bool do_print = true; + + if (!add_pool_to_realm(rc, cs, + coa_pool_name, &coa_pool, + HOME_TYPE_COA, do_print)) { + return 0; + } + } +#endif +#endif + + cf_log_info(cs, " realm %s {", name2); + +#ifdef WITH_PROXY + /* + * The realm MAY already exist if it's an old-style realm. + * In that case, merge the old-style realm with this one. + */ + r = realm_find2(name2); + if (r && (strcmp(r->name, name2) == 0)) { + if (cf_pair_find(cs, "auth_pool") || + cf_pair_find(cs, "acct_pool")) { + cf_log_err_cs(cs, "Duplicate realm \"%s\"", name2); + goto error; + } + + if (!old_realm_config(rc, cs, r)) { + goto error; + } + + cf_log_info(cs, " } # realm %s", name2); + return 1; + } +#endif + + r = talloc_zero(rc, REALM); + r->name = name2; + r->strip_realm = true; +#ifdef WITH_PROXY + r->auth_pool = auth_pool; + r->acct_pool = acct_pool; +#ifdef WITH_COA + r->coa_pool = coa_pool; +#endif + + if (auth_pool_name && + (auth_pool_name == acct_pool_name)) { /* yes, ptr comparison */ + cf_log_info(cs, "\tpool = %s", auth_pool_name); + } else { + if (auth_pool_name) cf_log_info(cs, "\tauth_pool = %s", auth_pool_name); + if (acct_pool_name) cf_log_info(cs, "\tacct_pool = %s", acct_pool_name); +#ifdef WITH_COA + if (coa_pool_name) cf_log_info(cs, "\tcoa_pool = %s", coa_pool_name); +#endif + } +#endif + + cp = cf_pair_find(cs, "nostrip"); + if (cp && (cf_pair_value(cp) == NULL)) { + r->strip_realm = false; + cf_log_info(cs, "\tnostrip"); + } + + /* + * We're a new-style realm. Complain if we see the old + * directives. + */ + if (r->auth_pool || r->acct_pool) { + if (((cp = cf_pair_find(cs, "authhost")) != NULL) || + ((cp = cf_pair_find(cs, "accthost")) != NULL) || + ((cp = cf_pair_find(cs, "secret")) != NULL) || + ((cp = cf_pair_find(cs, "ldflag")) != NULL)) { + WARN("Ignoring old-style configuration entry \"%s\" in realm \"%s\"", cf_pair_attr(cp), r->name); + } + + + /* + * The realm MAY be an old-style realm, as there + * was no auth_pool or acct_pool. Double-check + * it, just to be safe. + */ + } else if (!old_realm_config(rc, cs, r)) { + goto error; + } + + if (!realm_realm_add(r, cs)) { + goto error; + } + + cf_log_info(cs, " }"); + + return 1; + + error: + cf_log_info(cs, " } # realm %s", name2); + return 0; +} + +#ifdef HAVE_REGEX +int realm_realm_add(REALM *r, CONF_SECTION *cs) +#else +int realm_realm_add(REALM *r, UNUSED CONF_SECTION *cs) +#endif +{ + /* + * The structs aren't mutex protected. Refuse to destroy + * the server. + */ + if (event_loop_started && !realm_config->dynamic) { + DEBUG("Must set \"dynamic = true\" in proxy.conf"); + return 0; + } + +#ifdef HAVE_REGEX + /* + * It's a regex. Sanity check it, and add it to a + * separate list. + */ + if (r->name[0] == '~') { + ssize_t slen; + realm_regex_t *rr, **last; + + rr = talloc(r, realm_regex_t); + + /* + * Include substring matches. + */ + slen = regex_compile(rr, &rr->preg, r->name + 1, strlen(r->name) - 1, true, false, false, false); + if (slen <= 0) { + char *spaces, *text; + + fr_canonicalize_error(r, &spaces, &text, slen, r->name + 1); + + cf_log_err_cs(cs, "Invalid regular expression:"); + cf_log_err_cs(cs, "%s", text); + cf_log_err_cs(cs, "%s^ %s", spaces, fr_strerror()); + + talloc_free(spaces); + talloc_free(text); + talloc_free(rr); + + return 0; + } + + last = &realms_regex; + while (*last) last = &((*last)->next); /* O(N^2)... sue me. */ + + rr->realm = r; + rr->next = NULL; + + *last = rr; + return 1; + } +#endif + + if (!rbtree_insert(realms_byname, r)) { + rad_assert("Internal sanity check failed" == NULL); + return 0; + } + + return 1; +} + +#ifdef WITH_COA + +static int pool_peek_type(CONF_SECTION *config, CONF_SECTION *cs) +{ + int home; + char const *name, *type; + CONF_PAIR *cp; + CONF_SECTION *server_cs; + + cp = cf_pair_find(cs, "home_server"); + if (!cp) { + cf_log_err_cs(cs, "Pool does not contain a \"home_server\" entry"); + return HOME_TYPE_INVALID; + } + + name = cf_pair_value(cp); + if (!name) { + cf_log_err_cp(cp, "home_server entry does not reference a home server"); + return HOME_TYPE_INVALID; + } + + server_cs = cf_section_sub_find_name2(config, "home_server", name); + if (!server_cs) { + cf_log_err_cp(cp, "home_server \"%s\" does not exist", name); + return HOME_TYPE_INVALID; + } + + cp = cf_pair_find(server_cs, "type"); + if (!cp) { + cf_log_err_cs(server_cs, "home_server %s does not contain a \"type\" entry", name); + return HOME_TYPE_INVALID; + } + + type = cf_pair_value(cp); + if (!type) { + cf_log_err_cs(server_cs, "home_server %s contains an empty \"type\" entry", name); + return HOME_TYPE_INVALID; + } + + home = fr_str2int(home_server_types, type, HOME_TYPE_INVALID); + if (home == HOME_TYPE_INVALID) { + cf_log_err_cs(server_cs, "home_server %s contains an invalid \"type\" entry of value \"%s\"", name, type); + return HOME_TYPE_INVALID; + } + + return home; /* 'cause we miss it so much */ +} +#endif + +int realms_init(CONF_SECTION *config) +{ + CONF_SECTION *cs; + int flags = 0; +#ifdef WITH_PROXY + CONF_SECTION *server_cs; +#endif + realm_config_t *rc; + + if (event_loop_started) return 1; + + rc = talloc_zero(NULL, realm_config_t); + rc->cs = config; + +#ifdef WITH_PROXY + cs = cf_subsection_find_next(config, NULL, "proxy"); + if (cs) { + if (cf_section_parse(cs, rc, proxy_config) < 0) { + ERROR("Failed parsing proxy section"); + goto error; + } + } else { + rc->dead_time = DEAD_TIME; + rc->retry_count = RETRY_COUNT; + rc->retry_delay = RETRY_DELAY; + rc->fallback = false; + rc->dynamic = false; + rc->wake_all_if_all_dead= 0; + } + + if (rc->dynamic) { + flags = RBTREE_FLAG_LOCK; + } + + home_servers_byaddr = rbtree_create(NULL, home_server_addr_cmp, home_server_free, flags); + if (!home_servers_byaddr) goto error; + + home_servers_byname = rbtree_create(NULL, home_server_name_cmp, NULL, flags); + if (!home_servers_byname) goto error; + +#ifdef WITH_STATS + home_servers_bynumber = rbtree_create(NULL, home_server_number_cmp, NULL, flags); + if (!home_servers_bynumber) goto error; +#endif + + home_pools_byname = rbtree_create(NULL, home_pool_name_cmp, NULL, flags); + if (!home_pools_byname) goto error; + + for (cs = cf_subsection_find_next(config, NULL, "home_server"); + cs != NULL; + cs = cf_subsection_find_next(config, cs, "home_server")) { + home_server_t *home; + + home = home_server_afrom_cs(rc, rc, cs); + if (!home) goto error; + if (!realm_home_server_add(home)) goto error; + } + + /* + * Loop over virtual servers to find home servers which + * are defined in them. + */ + for (server_cs = cf_subsection_find_next(config, NULL, "server"); + server_cs != NULL; + server_cs = cf_subsection_find_next(config, server_cs, "server")) { + for (cs = cf_subsection_find_next(server_cs, NULL, "home_server"); + cs != NULL; + cs = cf_subsection_find_next(server_cs, cs, "home_server")) { + home_server_t *home; + + home = home_server_afrom_cs(rc, rc, cs); + if (!home) goto error; + if (!realm_home_server_add(home)) goto error; + } + } +#endif + + /* + * Now create the realms, which point to the home servers + * and home server pools. + */ + realms_byname = rbtree_create(NULL, realm_name_cmp, NULL, flags); + if (!realms_byname) goto error; + + for (cs = cf_subsection_find_next(config, NULL, "realm"); + cs != NULL; + cs = cf_subsection_find_next(config, cs, "realm")) { + if (!realm_add(rc, cs)) { + error: + realms_free(); + /* + * Must be called after realms_free as home_servers + * parented by rc are in trees freed by realms_free() + */ + talloc_free(rc); + return 0; + } + } + +#ifdef WITH_COA + /* + * CoA pools aren't necessarily tied to realms. + */ + for (cs = cf_subsection_find_next(config, NULL, "home_server_pool"); + cs != NULL; + cs = cf_subsection_find_next(config, cs, "home_server_pool")) { + int type; + + /* + * Pool was already loaded. + */ + if (cf_data_find(cs, "home_server_pool")) continue; + + type = pool_peek_type(config, cs); + if (type == HOME_TYPE_INVALID) goto error; + if (!server_pool_add(rc, cs, type, true)) goto error; + } +#endif + +#ifdef WITH_PROXY + xlat_register("home_server", xlat_home_server, NULL, NULL); + xlat_register("home_server_pool", xlat_server_pool, NULL, NULL); + xlat_register("home_server_dynamic", xlat_home_server_dynamic, NULL, NULL); +#endif + + realm_config = rc; + +#ifdef HAVE_DIRENT_H + if (!rc->dynamic) { + if (rc->directory) { + WARN("Ignoring 'directory' as dynamic home servers were not configured."); + } + } else { + DIR *dir; + struct dirent *dp; + + if (!rc->directory) { + WARN("Ignoring \"dynamic = true\" due to not set \"directory\" in proxy.conf"); + return 1; + } + + DEBUG2("including files in directory %s", rc->directory); + + dir = opendir(rc->directory); + if (!dir) { + cf_log_err_cs(config, "Error reading directory %s: %s", + rc->directory, fr_syserror(errno)); + goto error; + } + + /* + * Read the directory, ignoring "." files. + */ + while ((dp = readdir(dir)) != NULL) { + char const *p; + char conf_file[PATH_MAX]; + + if (dp->d_name[0] == '.') continue; + + /* + * Skip the TLS configuration. + */ + if (strcmp(dp->d_name, "tls.conf") == 0) continue; + + /* + * Check for valid characters + */ + for (p = dp->d_name; *p != '\0'; p++) { + if (isalpha((uint8_t)*p) || + isdigit((uint8_t)*p) || + (*p == '-') || + (*p == '_') || + (*p == '.')) continue; + break; + } + if (*p != '\0') continue; + + snprintf(conf_file, sizeof(conf_file), "%s/%s", rc->directory, dp->d_name); + if (home_server_afrom_file(conf_file) < 0) { + ERROR("Failed reading home_server from %s - %s", + conf_file, fr_strerror()); + closedir(dir); + goto error; + } + } + closedir(dir); + } +#endif + + return 1; +} + +/* + * Find a realm where "name" might be the regex. + */ +REALM *realm_find2(char const *name) +{ + REALM myrealm; + REALM *realm; + + if (!name) name = "NULL"; + + myrealm.name = name; + realm = rbtree_finddata(realms_byname, &myrealm); + if (realm) return realm; + +#ifdef HAVE_REGEX + if (realms_regex) { + realm_regex_t *this; + + for (this = realms_regex; this != NULL; this = this->next) { + if (strcmp(this->realm->name, name) == 0) { + return this->realm; + } + } + } +#endif + + /* + * Couldn't find a realm. Look for DEFAULT. + */ + myrealm.name = "DEFAULT"; + return rbtree_finddata(realms_byname, &myrealm); +} + + +/* + * Find a realm in the REALM list. + */ +REALM *realm_find(char const *name) +{ + REALM myrealm; + REALM *realm; + + if (!name) name = "NULL"; + + myrealm.name = name; + realm = rbtree_finddata(realms_byname, &myrealm); + if (realm) return realm; + +#ifdef HAVE_REGEX + if (realms_regex) { + realm_regex_t *this; + + for (this = realms_regex; + this != NULL; + this = this->next) { + int compare; + + compare = regex_exec(this->preg, name, strlen(name), NULL, NULL); + if (compare < 0) { + ERROR("Failed performing realm comparison: %s", fr_strerror()); + return NULL; + } + if (compare == 1) return this->realm; + } + } +#endif + + /* + * Couldn't find a realm. Look for DEFAULT. + */ + myrealm.name = "DEFAULT"; + return rbtree_finddata(realms_byname, &myrealm); +} + + +#ifdef WITH_PROXY + +/* + * Allocate the proxy list if it doesn't already exist, and copy request + * VPs into it. Setup src/dst IP addresses based on home server, and + * calculate and add the message-authenticator. + * + * This is a distinct function from home_server_ldb, as not all home_server_t + * lookups result in the *CURRENT* request being proxied, + * as in rlm_replicate, and this may trigger asserts elsewhere in the + * server. + */ +void home_server_update_request(home_server_t *home, REQUEST *request) +{ + + /* + * Allocate the proxy packet, only if it wasn't + * already allocated by a module. This check is + * mainly to support the proxying of EAP-TTLS and + * EAP-PEAP tunneled requests. + * + * In those cases, the EAP module creates a + * "fake" request, and recursively passes it + * through the authentication stage of the + * server. The module then checks if the request + * was supposed to be proxied, and if so, creates + * a proxy packet from the TUNNELED request, and + * not from the EAP request outside of the + * tunnel. + * + * The proxy then works like normal, except that + * the response packet is "eaten" by the EAP + * module, and encapsulated into an EAP packet. + */ + if (!request->proxy) { + request->proxy = rad_alloc(request, true); + if (!request->proxy) { + ERROR("no memory"); + fr_exit(1); + } + + /* + * Copy the request, then look up name + * and plain-text password in the copy. + * + * Note that the User-Name attribute is + * the *original* as sent over by the + * client. The Stripped-User-Name + * attribute is the one hacked through + * the 'hints' file. + */ + request->proxy->vps = fr_pair_list_copy(request->proxy, + request->packet->vps); + } + + /* + * Update the various fields as appropriate. + */ + request->proxy->src_ipaddr = home->src_ipaddr; + request->proxy->src_port = 0; + request->proxy->dst_ipaddr = home->ipaddr; + request->proxy->dst_port = home->port; +#ifdef WITH_TCP + request->proxy->proto = home->proto; +#endif + request->home_server = home; + + /* + * Access-Requests have a Message-Authenticator added, + * unless one already exists. + */ + if ((request->packet->code == PW_CODE_ACCESS_REQUEST) && +#ifdef WITH_RADIUSV11 + !request->proxy->radiusv11 && +#endif + !fr_pair_find_by_num(request->proxy->vps, PW_MESSAGE_AUTHENTICATOR, 0, TAG_ANY)) { + fr_pair_make(request->proxy, &request->proxy->vps, + "Message-Authenticator", "0x00", + T_OP_SET); + } +} + +home_server_t *home_server_ldb(char const *realmname, + home_pool_t *pool, REQUEST *request) +{ + int start; + int count; + home_server_t *found = NULL; + home_server_t *zombie = NULL; + VALUE_PAIR *vp; + uint32_t hash; + + /* + * Determine how to pick choose the home server. + */ + switch (pool->type) { + + + /* + * For load-balancing by client IP address, we + * pick a home server by hashing the client IP. + * + * This isn't as even a load distribution as + * tracking the State attribute, but it's better + * than nothing. + */ + case HOME_POOL_CLIENT_BALANCE: + switch (request->packet->src_ipaddr.af) { + case AF_INET: + hash = fr_hash(&request->packet->src_ipaddr.ipaddr.ip4addr, + sizeof(request->packet->src_ipaddr.ipaddr.ip4addr)); + break; + + case AF_INET6: + hash = fr_hash(&request->packet->src_ipaddr.ipaddr.ip6addr, + sizeof(request->packet->src_ipaddr.ipaddr.ip6addr)); + break; + + default: + hash = 0; + break; + } + start = hash % pool->num_home_servers; + break; + + case HOME_POOL_CLIENT_PORT_BALANCE: + switch (request->packet->src_ipaddr.af) { + case AF_INET: + hash = fr_hash(&request->packet->src_ipaddr.ipaddr.ip4addr, + sizeof(request->packet->src_ipaddr.ipaddr.ip4addr)); + break; + + case AF_INET6: + hash = fr_hash(&request->packet->src_ipaddr.ipaddr.ip6addr, + sizeof(request->packet->src_ipaddr.ipaddr.ip6addr)); + break; + + default: + hash = 0; + break; + } + hash = fr_hash_update(&request->packet->src_port, + sizeof(request->packet->src_port), hash); + start = hash % pool->num_home_servers; + break; + + case HOME_POOL_KEYED_BALANCE: + if ((vp = fr_pair_find_by_num(request->config, PW_LOAD_BALANCE_KEY, 0, TAG_ANY)) != NULL) { + hash = fr_hash(vp->vp_strvalue, vp->vp_length); + start = hash % pool->num_home_servers; + break; + } + /* FALL-THROUGH */ + + case HOME_POOL_LOAD_BALANCE: + case HOME_POOL_FAIL_OVER: + start = 0; + break; + + default: /* this shouldn't happen... */ + start = 0; + break; + + } + + /* + * Starting with the home server we chose, loop through + * all home servers. If the current one is dead, skip + * it. If it is too busy, skip it. + * + * Otherwise, use it. + */ + for (count = 0; count < pool->num_home_servers; count++) { + home_server_t *home = pool->servers[(start + count) % pool->num_home_servers]; + + if (!home) continue; + + /* + * Skip dead home servers. + * + * Home servers that are unknown, alive, or zombie + * are used for proxying. + */ + if (HOME_SERVER_IS_DEAD(home)) { + continue; + } + + /* + * This home server is too busy. Choose another one. + */ + if (home->currently_outstanding >= home->max_outstanding) { + continue; + } + +#ifdef WITH_DETAIL + /* + * We read the packet from a detail file, AND it + * came from this server. Don't re-proxy it + * there. + */ + if (request->listener && + (request->listener->type == RAD_LISTEN_DETAIL) && + (request->packet->code == PW_CODE_ACCOUNTING_REQUEST) && + (fr_ipaddr_cmp(&home->ipaddr, &request->packet->src_ipaddr) == 0)) { + continue; + } +#endif + + /* + * Default virtual: ignore homes tied to a + * virtual. + */ + if (!request->server && home->parent_server) { + continue; + } + + /* + * A virtual AND home is tied to virtual, + * ignore ones which don't match. + */ + if (request->server && home->parent_server && + strcmp(request->server, home->parent_server) != 0) { + continue; + } + + /* + * Allow request->server && !home->parent_server + * + * i.e. virtuals can proxy to globally defined + * homes. + */ + + /* + * It's zombie, so we remember the first zombie + * we find, but we don't mark it as a "live" + * server. + */ + if (home->state == HOME_STATE_ZOMBIE) { + if (!zombie) zombie = home; + continue; + } + + /* + * We've found the first "live" one. Use that. + */ + if (pool->type != HOME_POOL_LOAD_BALANCE) { + found = home; + break; + } + + /* + * Otherwise we're doing some kind of load balancing. + * If we haven't found one yet, pick this one. + */ + if (!found) { + found = home; + continue; + } + + RDEBUG3("PROXY %s %d\t%s %d", + found->log_name, found->currently_outstanding, + home->log_name, home->currently_outstanding); + + /* + * Prefer this server if it's less busy than the + * one we had previously found. + */ + if (home->currently_outstanding < found->currently_outstanding) { + RDEBUG3("PROXY Choosing %s: It's less busy than %s", + home->log_name, found->log_name); + found = home; + continue; + } + + /* + * Ignore servers which are busier than the one + * we found. + */ + if (home->currently_outstanding > found->currently_outstanding) { + RDEBUG3("PROXY Skipping %s: It's busier than %s", + home->log_name, found->log_name); + continue; + } + + /* + * From the list of servers which have the same + * load, choose one at random. + */ + if (((count + 1) * (fr_rand() & 0xffff)) < (uint32_t) 0x10000) { + found = home; + } + } /* loop over the home servers */ + + /* + * We have no live servers, BUT we have a zombie. Use + * the zombie as a last resort. + */ + if (!found && zombie) { + found = zombie; + zombie = NULL; + } + + /* + * There's a fallback if they're all dead. + */ + if (!found && pool->fallback) { + found = pool->fallback; + + WARN("Home server pool %s failing over to fallback %s", + pool->name, found->virtual_server); + if (pool->in_fallback) goto update_and_return; + + pool->in_fallback = true; + + /* + * Run the trigger once an hour saying that + * they're all dead. + */ + if ((pool->time_all_dead + 3600) < request->timestamp) { + pool->time_all_dead = request->timestamp; + exec_trigger(request, pool->cs, "home_server_pool.fallback", false); + } + } + + if (found) { + update_and_return: + if ((found != pool->fallback) && pool->in_fallback) { + pool->in_fallback = false; + exec_trigger(request, pool->cs, "home_server_pool.normal", false); + } + + return found; + } + + /* + * No live match found, and no fallback to the "DEFAULT" + * realm. We fix this by blindly marking all servers as + * "live". But only do it for ones that don't support + * "pings", as they will be marked live when they + * actually are live. + */ + if (!realm_config->fallback && + realm_config->wake_all_if_all_dead) { + for (count = 0; count < pool->num_home_servers; count++) { + home_server_t *home = pool->servers[count]; + + if (!home) continue; + + if (HOME_SERVER_IS_DEAD(home) && + (home->ping_check == HOME_PING_CHECK_NONE)) { + home->state = HOME_STATE_ALIVE; + home->response_timeouts = 0; + if (!found) found = home; + } + } + + if (found) goto update_and_return; + } + + /* + * Still nothing. Look up the DEFAULT realm, but only + * if we weren't looking up the NULL or DEFAULT realms. + */ + if (realm_config->fallback && + realmname && + (strcmp(realmname, "NULL") != 0) && + (strcmp(realmname, "DEFAULT") != 0)) { + REALM *rd = realm_find("DEFAULT"); + + if (!rd) return NULL; + + pool = NULL; + if (request->packet->code == PW_CODE_ACCESS_REQUEST) { + pool = rd->auth_pool; + + } else if (request->packet->code == PW_CODE_ACCOUNTING_REQUEST) { + pool = rd->acct_pool; + } + if (!pool) return NULL; + + RDEBUG2("PROXY - realm %s has no live home servers. Falling back to the DEFAULT realm.", realmname); + return home_server_ldb(rd->name, pool, request); + } + + /* + * Still haven't found anything. Oh well. + */ + return NULL; +} + + +home_server_t *home_server_find(fr_ipaddr_t *ipaddr, uint16_t port, +#ifndef WITH_TCP + UNUSED +#endif + int proto) +{ + home_server_t myhome; + + memset(&myhome, 0, sizeof(myhome)); + myhome.ipaddr = *ipaddr; + myhome.src_ipaddr.af = ipaddr->af; + myhome.port = port; +#ifdef WITH_TCP + myhome.proto = proto; +#else + myhome.proto = IPPROTO_UDP; +#endif + myhome.virtual_server = NULL; /* we're not called for internal proxying */ + + return rbtree_finddata(home_servers_byaddr, &myhome); +} + +home_server_t *home_server_find_bysrc(fr_ipaddr_t *ipaddr, uint16_t port, + int proto, + fr_ipaddr_t *src_ipaddr) +{ + home_server_t myhome; + + if (!src_ipaddr) return home_server_find(ipaddr, port, proto); + + if (src_ipaddr->af != ipaddr->af) return NULL; + + memset(&myhome, 0, sizeof(myhome)); + myhome.ipaddr = *ipaddr; + myhome.src_ipaddr = *src_ipaddr; + myhome.port = port; +#ifdef WITH_TCP + myhome.proto = proto; +#else + myhome.proto = IPPROTO_UDP; +#endif + myhome.virtual_server = NULL; /* we're not called for internal proxying */ + + return rbtree_finddata(home_servers_byaddr, &myhome); +} + +#ifdef WITH_COA +home_server_t *home_server_byname(char const *name, int type) +{ + home_server_t myhome; + + memset(&myhome, 0, sizeof(myhome)); + myhome.type = type; + myhome.name = name; + + return rbtree_finddata(home_servers_byname, &myhome); +} +#endif + +#ifdef WITH_STATS +home_server_t *home_server_bynumber(int number) +{ + home_server_t myhome; + + memset(&myhome, 0, sizeof(myhome)); + myhome.number = number; + myhome.virtual_server = NULL; /* we're not called for internal proxying */ + + return rbtree_finddata(home_servers_bynumber, &myhome); +} +#endif + +home_pool_t *home_pool_byname(char const *name, int type) +{ + home_pool_t mypool; + + memset(&mypool, 0, sizeof(mypool)); + mypool.name = name; + mypool.server_type = type; + return rbtree_finddata(home_pools_byname, &mypool); +} + +int home_server_afrom_file(char const *filename) +{ + CONF_SECTION *cs, *subcs; + char const *p; + home_server_t *home; + + if (!realm_config->dynamic) { + fr_strerror_printf("Must set \"dynamic = true\" in proxy.conf for dynamic home servers to work"); + return -1; + } + + cs = cf_section_alloc(NULL, "home", filename); + if (!cs) { + fr_strerror_printf("Failed allocating memory"); + return -1; + } + + if (cf_file_read(cs, filename) < 0) { + fr_strerror_printf("Failed reading file %s", filename); + error: + talloc_free(cs); + return -1; + } + + p = strrchr(filename, '/'); + if (p) { + p++; + } else { + p = filename; + } + + subcs = cf_section_sub_find_name2(cs, "home_server", p); + if (!subcs) { + fr_strerror_printf("No 'home_server %s' definition in the file.", p); + goto error; + } + + home = home_server_afrom_cs(realm_config, realm_config, subcs); + if (!home) { + fr_strerror_printf("Failed parsing configuration to a home_server structure"); + goto error; + } + + home->dynamic = true; + + if (home->virtual_server) { + fr_strerror_printf("Dynamic home_server '%s' cannot have 'server = %s' configuration item", p, home->virtual_server); + talloc_free(home); + goto error; + } + + if (home->dual) { + fr_strerror_printf("Dynamic home_server '%s' is missing 'type', or it is set to 'auth+acct'. Please specify 'type = auth' or 'type = acct', etc.", p); + talloc_free(home); + goto error; + } + +#ifdef COA_TUNNEL + if (home->recv_coa) { + fr_strerror_printf("Dynamic home_server '%s' cannot receive CoA requests'", p); + talloc_free(home); + goto error; + } +#endif + + if (!realm_home_server_add(home)) { + fr_strerror_printf("Failed adding home_server to the internal data structures"); + talloc_free(home); + goto error; + } + + return 0; +} + +int home_server_delete(char const *name, char const *type_name) +{ + home_server_t *home; + int type; + char const *p; + + if (!realm_config->dynamic) { + fr_strerror_printf("Must set 'dynamic' in proxy.conf for dynamic home servers to work"); + return -1; + } + + type = fr_str2int(home_server_types, type_name, HOME_TYPE_INVALID); + if (type == HOME_TYPE_INVALID) { + fr_strerror_printf("Unknown home_server type '%s'", type_name); + return -1; + } + + p = strrchr(name, '/'); + if (p) { + p++; + } else { + p = name; + } + + home = home_server_byname(p, type); + if (!home) { + fr_strerror_printf("Failed to find home_server %s", p); + return -1; + } + + if (!home->dynamic) { + fr_strerror_printf("Cannot delete static home_server %s", p); + return -1; + } + + (void) rbtree_deletebydata(home_servers_byname, home); + (void) rbtree_deletebydata(home_servers_byaddr, home); +#ifdef WITH_STATS + (void) rbtree_deletebydata(home_servers_bynumber, home); +#endif + + /* + * Leak home, and home->cs. Oh well. + */ + return 0; +} +#endif diff --git a/src/main/regex.c b/src/main/regex.c new file mode 100644 index 0000000..f66414c --- /dev/null +++ b/src/main/regex.c @@ -0,0 +1,279 @@ +/* + * This program 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/regex.c + * @brief Regular expression functions used by the server library. + * + * @copyright 2014 The FreeRADIUS server project + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/rad_assert.h> + +#ifdef HAVE_REGEX + +#define REQUEST_DATA_REGEX (0xadbeef00) + +typedef struct regcapture { +#ifdef HAVE_PCRE + regex_t *preg; //!< Compiled pattern. +#endif + char const *value; //!< Original string. + regmatch_t *rxmatch; //!< Match vectors. + size_t nmatch; //!< Number of match vectors. +} regcapture_t; + +/** Adds subcapture values to request data + * + * Allows use of %{n} expansions. + * + * @note After calling regex_sub_to_request *preg may no longer be valid and + * should be passed to talloc_free. + * + * @param request Current request. + * @param preg Compiled pattern. May be set to NULL if reparented to the regcapture struct. + * @param value The original value. + * @param rxmatch Pointers into value. + * @param nmatch Sizeof rxmatch. + */ +void regex_sub_to_request(REQUEST *request, regex_t **preg, char const *value, size_t len, + regmatch_t rxmatch[], size_t nmatch) +{ + regcapture_t *old_sc, *new_sc; /* lldb doesn't like new *sigh* */ + char *p; + + /* + * Clear out old_sc matches + */ + old_sc = request_data_get(request, request, REQUEST_DATA_REGEX); + if (old_sc) { + DEBUG4("Clearing %zu old matches", old_sc->nmatch); + talloc_free(old_sc); + } else { + DEBUG4("No old matches"); + } + + if (nmatch == 0) return; + + rad_assert(preg && *preg); + rad_assert(rxmatch); + + DEBUG4("Adding %zu matches", nmatch); + + /* + * Add new_sc matches + */ + MEM(new_sc = talloc(request, regcapture_t)); + + MEM(new_sc->rxmatch = talloc_memdup(new_sc, rxmatch, sizeof(rxmatch[0]) * nmatch)); + talloc_set_type(new_sc->rxmatch, regmatch_t[]); + + MEM(p = talloc_array(new_sc, char, len + 1)); + memcpy(p, value, len); + p[len] = '\0'; + new_sc->value = p; + new_sc->nmatch = nmatch; + +#ifdef HAVE_PCRE + if (!(*preg)->precompiled) { + new_sc->preg = talloc_steal(new_sc, *preg); + *preg = NULL; + } else { + new_sc->preg = *preg; + } +#endif + + request_data_add(request, request, REQUEST_DATA_REGEX, new_sc, true); +} + +# ifdef HAVE_PCRE +/** Extract a subcapture value from the request + * + * @note This is the PCRE variant of the function. + * + * @param ctx To allocate subcapture buffer in. + * @param out Where to write the subcapture string. + * @param request to extract. + * @param num Subcapture index (0 for entire match). + * @return 0 on success, -1 on notfound. + */ +int regex_request_to_sub(TALLOC_CTX *ctx, char **out, REQUEST *request, uint32_t num) +{ + regcapture_t *cap; + char const *p; + int ret; + + cap = request_data_reference(request, request, REQUEST_DATA_REGEX); + if (!cap) { + RDEBUG4("No subcapture data found"); + *out = NULL; + return -1; + } + + ret = pcre_get_substring(cap->value, (int *)cap->rxmatch, (int)cap->nmatch, num, &p); + switch (ret) { + case PCRE_ERROR_NOMEMORY: + MEM(NULL); + /* FALL-THROUGH */ + + /* + * Not finding a substring is fine + */ + case PCRE_ERROR_NOSUBSTRING: + RDEBUG4("%i/%zu Not found", num, cap->nmatch); + *out = NULL; + return -1; + + default: + if (ret < 0) { + *out = NULL; + return -1; + } + + /* + * Check libpcre really is using our overloaded + * malloc/free talloc wrappers. + */ + p = (char *)talloc_get_type_abort(p, uint8_t); + talloc_set_type(p, char *); + talloc_steal(ctx, p); + memcpy(out, &p, sizeof(*out)); + + RDEBUG4("%i/%zu Found: %s (%zu)", num, cap->nmatch, p, talloc_array_length(p)); + + return 0; + } +} + +/** Extract a named subcapture value from the request + * + * @note This is the PCRE variant of the function. + * + * @param ctx To allocate subcapture buffer in. + * @param out Where to write the subcapture string. + * @param request to extract. + * @param name of subcapture. + * @return 0 on success, -1 on notfound. + */ +int regex_request_to_sub_named(TALLOC_CTX *ctx, char **out, REQUEST *request, char const *name) +{ + regcapture_t *cap; + char const *p; + int ret; + + cap = request_data_reference(request, request, REQUEST_DATA_REGEX); + if (!cap) { + RDEBUG4("No subcapture data found"); + *out = NULL; + return -1; + } + + ret = pcre_get_named_substring(cap->preg->compiled, cap->value, + (int *)cap->rxmatch, (int)cap->nmatch, name, &p); + switch (ret) { + case PCRE_ERROR_NOMEMORY: + MEM(NULL); + /* FALL-THROUGH */ + + /* + * Not finding a substring is fine + */ + case PCRE_ERROR_NOSUBSTRING: + RDEBUG4("No named capture group \"%s\"", name); + *out = NULL; + return -1; + + default: + if (ret < 0) { + *out = NULL; + return -1; + } + + /* + * Check libpcre really is using our overloaded + * malloc/free talloc wrappers. + */ + p = (char *)talloc_get_type_abort(p, uint8_t); + talloc_set_type(p, char *); + talloc_steal(ctx, p); + memcpy(out, &p, sizeof(*out)); + + RDEBUG4("Found \"%s\": %s (%zu)", name, p, talloc_array_length(p)); + + return 0; + } +} +# else +/** Extract a subcapture value from the request + * + * @note This is the POSIX variant of the function. + * + * @param ctx To allocate subcapture buffer in. + * @param out Where to write the subcapture string. + * @param request to extract. + * @param num Subcapture index (0 for entire match). + * @return 0 on success, -1 on notfound. + */ +int regex_request_to_sub(TALLOC_CTX *ctx, char **out, REQUEST *request, uint32_t num) +{ + regcapture_t *cap; + char *p; + char const *start; + size_t len; + + cap = request_data_reference(request, request, REQUEST_DATA_REGEX); + if (!cap) { + RDEBUG4("No subcapture data found"); + *out = NULL; + return -1; + } + + /* + * Greater than our capture array + * + * -1 means no value in this capture group. + */ + if ((num >= cap->nmatch) || (cap->rxmatch[num].rm_eo == -1) || (cap->rxmatch[num].rm_so == -1)) { + RDEBUG4("%i/%zu Not found", num, cap->nmatch); + *out = NULL; + return -1; + } + + /* + * Sanity checks on the offsets + */ + rad_assert(cap->rxmatch[num].rm_eo <= (regoff_t)talloc_array_length(cap->value)); + rad_assert(cap->rxmatch[num].rm_so <= (regoff_t)talloc_array_length(cap->value)); + + start = cap->value + cap->rxmatch[num].rm_so; + len = cap->rxmatch[num].rm_eo - cap->rxmatch[num].rm_so; + + RDEBUG4("%i/%zu Found: %.*s (%zu)", num, cap->nmatch, (int)len, start, len); + MEM(p = talloc_array(ctx, char, len + 1)); + memcpy(p, start, len); + p[len] = '\0'; + + *out = p; + + return 0; +} +# endif +#endif diff --git a/src/main/session.c b/src/main/session.c new file mode 100644 index 0000000..ddec8ff --- /dev/null +++ b/src/main/session.c @@ -0,0 +1,262 @@ +/* + * session.c session management + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2000,2006 The FreeRADIUS server project + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/modules.h> +#include <freeradius-devel/rad_assert.h> + +#ifdef HAVE_SYS_WAIT_H +#include <sys/wait.h> +#endif + +#ifdef WITH_SESSION_MGMT +/* + * End a session by faking a Stop packet to all accounting modules. + */ +int session_zap(REQUEST *request, fr_ipaddr_t const *nasaddr, uint32_t nas_port, + char const *nas_port_id, char const *user, + char const *sessionid, uint32_t cliaddr, char proto, + int session_time) +{ + REQUEST *stopreq; + VALUE_PAIR *vp; + int ret; + + stopreq = request_alloc_fake(request); + rad_assert(stopreq != NULL); + rad_assert(stopreq->packet != NULL); + stopreq->packet->code = PW_CODE_ACCOUNTING_REQUEST; /* just to be safe */ + stopreq->listener = request->listener; + + /* Hold your breath */ +#define PAIR(n,v,e) do { \ + if(!(vp = fr_pair_afrom_num(stopreq->packet,n, 0))) { \ + talloc_free(stopreq); \ + ERROR("no memory"); \ + return 0; \ + } \ + vp->e = v; \ + fr_pair_add(&(stopreq->packet->vps), vp); \ + } while(0) + +#define INTPAIR(n,v) PAIR(n,v,vp_integer) + +#define IPPAIR(n,v) PAIR(n,v,vp_ipaddr) + +#define IPV6PAIR(n,v) PAIR(n,v,vp_ipv6addr) + +#define STRINGPAIR(n,v) do { \ + if(!(vp = fr_pair_afrom_num(stopreq->packet,n, 0))) { \ + talloc_free(stopreq); \ + ERROR("no memory"); \ + return 0; \ + } \ + fr_pair_value_strcpy(vp, v); \ + fr_pair_add(&(stopreq->packet->vps), vp); \ + } while(0) + + INTPAIR(PW_ACCT_STATUS_TYPE, PW_STATUS_STOP); + + if (nasaddr->af == AF_INET) { + IPPAIR(PW_NAS_IP_ADDRESS, nasaddr->ipaddr.ip4addr.s_addr); + } else { + IPV6PAIR(PW_NAS_IPV6_ADDRESS, nasaddr->ipaddr.ip6addr); + } + + INTPAIR(PW_EVENT_TIMESTAMP, 0); + vp->vp_date = time(NULL); + INTPAIR(PW_ACCT_DELAY_TIME, 0); + + STRINGPAIR(PW_USER_NAME, user); + stopreq->username = vp; + + if (!nas_port_id) { + INTPAIR(PW_NAS_PORT, nas_port); + } else { + STRINGPAIR(PW_NAS_PORT_ID, nas_port_id); + } + STRINGPAIR(PW_ACCT_SESSION_ID, sessionid); + if(proto == 'P') { + INTPAIR(PW_SERVICE_TYPE, PW_FRAMED_USER); + INTPAIR(PW_FRAMED_PROTOCOL, PW_PPP); + } else if(proto == 'S') { + INTPAIR(PW_SERVICE_TYPE, PW_FRAMED_USER); + INTPAIR(PW_FRAMED_PROTOCOL, PW_SLIP); + } else { + INTPAIR(PW_SERVICE_TYPE, PW_LOGIN_USER); /* A guess, really */ + } + if(cliaddr != 0) + IPPAIR(PW_FRAMED_IP_ADDRESS, cliaddr); + INTPAIR(PW_ACCT_SESSION_TIME, session_time); + INTPAIR(PW_ACCT_INPUT_OCTETS, 0); + INTPAIR(PW_ACCT_OUTPUT_OCTETS, 0); + INTPAIR(PW_ACCT_INPUT_PACKETS, 0); + INTPAIR(PW_ACCT_OUTPUT_PACKETS, 0); + + stopreq->password = NULL; + + RDEBUG("Running Accounting section for automatically created accounting 'stop'"); + rdebug_pair_list(L_DBG_LVL_1, request, request->packet->vps, NULL); + ret = rad_accounting(stopreq); + + /* + * We've got to clean it up by hand, because no one else will. + */ + talloc_free(stopreq); + + return ret; +} + +#ifndef __MINGW32__ + +/* + * Check one terminal server to see if a user is logged in. + * + * Return values: + * 0 The user is off-line. + * 1 The user is logged in. + * 2 Some error occured. + */ +int rad_check_ts(fr_ipaddr_t const *nasaddr, uint32_t nas_port, char const *nas_port_id, char const *user, + char const *session_id) +{ + pid_t pid, child_pid; + int status; + char address[64]; + char port[11]; + RADCLIENT *cl; + + /* + * Find NAS type. + */ + cl = client_find_old(nasaddr); + if (!cl) { + /* + * Unknown NAS, so trusting radutmp. + */ + DEBUG2("checkrad: Unknown NAS %s, not checking", + inet_ntop(nasaddr->af, &(nasaddr->ipaddr), address, sizeof(address))); + return 1; + } + + /* + * No nas_type, or nas type 'other', trust radutmp. + */ + if (!cl->nas_type || (cl->nas_type[0] == '\0') || + (strcmp(cl->nas_type, "other") == 0)) { + DEBUG2("checkrad: No NAS type, or type \"other\" not checking"); + return 1; + } + + /* + * Fork. + */ + if ((pid = rad_fork()) < 0) { /* do wait for the fork'd result */ + ERROR("Accounting: Failed in fork(): Cannot run checkrad\n"); + return 2; + } + + if (pid > 0) { + child_pid = rad_waitpid(pid, &status); + + /* + * It's taking too long. Stop waiting for it. + * + * Don't bother to kill it, as we don't care what + * happens to it now. + */ + if (child_pid == 0) { + ERROR("Check-TS: timeout waiting for checkrad"); + return 2; + } + + if (child_pid < 0) { + ERROR("Check-TS: unknown error in waitpid()"); + return 2; + } + + return WEXITSTATUS(status); + } + + /* + * We don't close fd's 0, 1, and 2. If we're in debugging mode, + * then they should go to stdout (etc), along with the other + * server log messages. + * + * If we're not in debugging mode, then the code in radiusd.c + * takes care of connecting fd's 0, 1, and 2 to /dev/null. + */ + closefrom(3); + + inet_ntop(nasaddr->af, &(nasaddr->ipaddr), address, sizeof(address)); + + if (!nas_port_id) { + snprintf(port, sizeof(port), "%u", nas_port); + nas_port_id = port; + } + +#ifdef __EMX__ + /* OS/2 can't directly execute scripts then we call the command + processor to execute checkrad + */ + execl(getenv("COMSPEC"), "", "/C","checkrad", cl->nas_type, address, nas_port_id, + user, session_id, NULL); +#else + execl(main_config.checkrad, "checkrad", cl->nas_type, address, nas_port_id, + user, session_id, NULL); +#endif + ERROR("Check-TS: exec %s: %s", main_config.checkrad, fr_syserror(errno)); + + /* + * Exit - 2 means "some error occured". + */ + exit(2); +} +#else +int rad_check_ts(fr_ipaddr_t const *nasaddr, UNUSED unsigned int nas_port, + UNUSED char const *user, UNUSED char const *session_id) +{ + ERROR("Simultaneous-Use is not supported"); + return 2; +} +#endif + +#else +/* WITH_SESSION_MGMT */ + +int session_zap(UNUSED REQUEST *request, fr_ipaddr_t const *nasaddr, UNUSED uint32_t nas_port, + UNUSED char const *nas_port_id, UNUSED char const *user, + UNUSED char const *sessionid, UNUSED uint32_t cliaddr, UNUSED char proto, + UNUSED int session_time) +{ + return RLM_MODULE_FAIL; +} + +int rad_check_ts(fr_ipaddr_t const *nasaddr, UNUSED unsigned int nas_port, + UNUSED char const *nas_port_id, UNUSED char const *user, UNUSED char const *session_id) +{ + ERROR("Simultaneous-Use is not supported"); + return 2; +} +#endif diff --git a/src/main/soh.c b/src/main/soh.c new file mode 100644 index 0000000..754ad84 --- /dev/null +++ b/src/main/soh.c @@ -0,0 +1,675 @@ +/* + * This program 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 soh.c + * @brief Implements the MS-SOH parsing code. This is called from rlm_eap_peap + * + * @copyright 2010 Phil Mayers <p.mayers@imperial.ac.uk> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/soh.h> +#include <freeradius-devel/rad_assert.h> + +/* + * This code implements parsing of MS-SOH data into FreeRadius AVPs + * allowing for FreeRadius MS-NAP policies + */ + +/** + * EAP-SOH packet + */ +typedef struct { + uint16_t tlv_type; /**< ==7 for EAP-SOH */ + uint16_t tlv_len; + uint32_t tlv_vendor; + + /** + * @name soh-payload + * @brief either an soh request or response */ + uint16_t soh_type; /**< ==2 for request, 1 for response */ + uint16_t soh_len; + + /* an soh-response may now follow... */ +} eap_soh; + +/** + * SOH response payload + * Send by client to server + */ +typedef struct { + uint16_t outer_type; + uint16_t outer_len; + uint32_t vendor; + uint16_t inner_type; + uint16_t inner_len; +} soh_response; + +/** + * SOH mode subheader + * Typical microsoft binary blob nonsense + */ +typedef struct { + uint16_t outer_type; + uint16_t outer_len; + uint32_t vendor; + uint8_t corrid[24]; + uint8_t intent; + uint8_t content_type; +} soh_mode_subheader; + +/** + * SOH type-length-value header + */ +typedef struct { + uint16_t tlv_type; + uint16_t tlv_len; +} soh_tlv; + +/** Read big-endian 2-byte unsigned from p + * + * caller must ensure enough data exists at "p" + */ +uint16_t soh_pull_be_16(uint8_t const *p) { + uint16_t r; + + r = *p++ << 8; + r += *p++; + + return r; +} + +/** Read big-endian 3-byte unsigned from p + * + * caller must ensure enough data exists at "p" + */ +uint32_t soh_pull_be_24(uint8_t const *p) { + uint32_t r; + + r = *p++ << 16; + r += *p++ << 8; + r += *p++; + + return r; +} + +/** Read big-endian 4-byte unsigned from p + * + * caller must ensure enough data exists at "p" + */ +uint32_t soh_pull_be_32(uint8_t const *p) { + uint32_t r; + + r = *p++ << 24; + r += *p++ << 16; + r += *p++ << 8; + r += *p++; + + return r; +} + +static int eapsoh_mstlv(REQUEST *request, uint8_t const *p, unsigned int data_len) CC_HINT(nonnull); + +/** Parses the MS-SOH type/value (note: NOT type/length/value) data and update the sohvp list + * + * See section 2.2.4 of MS-SOH. Because there's no "length" field we CANNOT just skip + * unknown types; we need to know their length ahead of time. Therefore, we abort + * if we find an unknown type. Note that sohvp may still have been modified in the + * failure case. + * + * @param request Current request + * @param p binary blob + * @param data_len length of blob + * @return 1 on success, 0 on failure + */ +static int eapsoh_mstlv(REQUEST *request, uint8_t const *p, unsigned int data_len) +{ + VALUE_PAIR *vp; + uint8_t c; + int t; + + while (data_len > 0) { + c = *p++; + data_len--; + + switch (c) { + case 1: + /* MS-Machine-Inventory-Packet + * MS-SOH section 2.2.4.1 + */ + if (data_len < 18) { + RDEBUG("insufficient data for MS-Machine-Inventory-Packet"); + return 0; + } + data_len -= 18; + + vp = pair_make_request("SoH-MS-Machine-OS-vendor", "Microsoft", T_OP_EQ); + if (!vp) return 0; + + vp = pair_make_request("SoH-MS-Machine-OS-version", NULL, T_OP_EQ); + if (!vp) return 0; + + vp->vp_integer = soh_pull_be_32(p); p+=4; + + vp = pair_make_request("SoH-MS-Machine-OS-release", NULL, T_OP_EQ); + if (!vp) return 0; + + vp->vp_integer = soh_pull_be_32(p); p+=4; + + vp = pair_make_request("SoH-MS-Machine-OS-build", NULL, T_OP_EQ); + if (!vp) return 0; + + vp->vp_integer = soh_pull_be_32(p); p+=4; + + vp = pair_make_request("SoH-MS-Machine-SP-version", NULL, T_OP_EQ); + if (!vp) return 0; + + vp->vp_integer = soh_pull_be_16(p); p+=2; + + vp = pair_make_request("SoH-MS-Machine-SP-release", NULL, T_OP_EQ); + if (!vp) return 0; + + vp->vp_integer = soh_pull_be_16(p); p+=2; + + vp = pair_make_request("SoH-MS-Machine-Processor", NULL, T_OP_EQ); + if (!vp) return 0; + + vp->vp_integer = soh_pull_be_16(p); p+=2; + break; + + case 2: + /* MS-Quarantine-State - FIXME: currently unhandled + * MS-SOH 2.2.4.1 + * + * 1 byte reserved + * 1 byte flags + * 8 bytes NT Time field (100-nanosec since 1 Jan 1601) + * 2 byte urilen + * N bytes uri + */ + p += 10; + t = soh_pull_be_16(p); /* t == uri len */ + p += 2; + p += t; + data_len -= 12 + t; + break; + + case 3: + /* MS-Packet-Info + * MS-SOH 2.2.4.3 + */ + RDEBUG3("SoH MS-Packet-Info %s vers=%i", *p & 0x10 ? "request" : "response", *p & 0xf); + p++; + data_len--; + break; + + case 4: + /* MS-SystemGenerated-Ids - FIXME: currently unhandled + * MS-SOH 2.2.4.4 + * + * 2 byte length + * N bytes (3 bytes IANA enterprise# + 1 byte component id#) + */ + t = soh_pull_be_16(p); + p += 2; + p += t; + data_len -= 2 + t; + break; + + case 5: + /* MS-MachineName + * MS-SOH 2.2.4.5 + * + * 1 byte namelen + * N bytes name + */ + t = soh_pull_be_16(p); + p += 2; + + vp = pair_make_request("SoH-MS-Machine-Name", NULL, T_OP_EQ); + if (!vp) return 0; + + fr_pair_value_bstrncpy(vp, p, t); + + p += t; + data_len -= 2 + t; + break; + + case 6: + /* MS-CorrelationId + * MS-SOH 2.2.4.6 + * + * 24 bytes opaque binary which we might, in future, have + * to echo back to the client in a final SoHR + */ + vp = pair_make_request("SoH-MS-Correlation-Id", NULL, T_OP_EQ); + if (!vp) return 0; + + fr_pair_value_memcpy(vp, p, 24); + p += 24; + data_len -= 24; + break; + + case 7: + /* MS-Installed-Shvs - FIXME: currently unhandled + * MS-SOH 2.2.4.7 + * + * 2 bytes length + * N bytes (3 bytes IANA enterprise# + 1 byte component id#) + */ + t = soh_pull_be_16(p); + p += 2; + p += t; + data_len -= 2 + t; + break; + + case 8: + /* MS-Machine-Inventory-Ex + * MS-SOH 2.2.4.8 + * + * 4 bytes reserved + * 1 byte product type (client=1 domain_controller=2 server=3) + */ + p += 4; + vp = pair_make_request("SoH-MS-Machine-Role", NULL, T_OP_EQ); + if (!vp) return 0; + + vp->vp_integer = *p; + p++; + data_len -= 5; + break; + + default: + RDEBUG("SoH Unknown MS TV %i stopping", c); + return 0; + } + } + return 1; +} +/** Convert windows Health Class status into human-readable string + * + * Tedious, really, really tedious... + */ +static char const* clientstatus2str(uint32_t hcstatus) { + switch (hcstatus) { + /* this lot should all just be for windows updates */ + case 0xff0005: + return "wua-ok"; + + case 0xff0006: + return "wua-missing"; + + case 0xff0008: + return "wua-not-started"; + + case 0xc0ff000c: + return "wua-no-wsus-server"; + + case 0xc0ff000d: + return "wua-no-wsus-clientid"; + + case 0xc0ff000e: + return "wua-disabled"; + + case 0xc0ff000f: + return "wua-comm-failure"; + + /* these next 3 are for all health-classes */ + case 0xc0ff0002: + return "not-installed"; + + case 0xc0ff0003: + return "down"; + + case 0xc0ff0018: + return "not-started"; + } + return NULL; +} + +/** Convert a Health Class into a string + * + */ +static char const* healthclass2str(uint8_t hc) { + switch (hc) { + case 0: + return "firewall"; + + case 1: + return "antivirus"; + + case 2: + return "antispyware"; + + case 3: + return "updates"; + + case 4: + return "security-updates"; + } + return NULL; +} + +/** Parse the MS-SOH response in data and update sohvp + * + * Note that sohvp might still have been updated in event of a failure. + * + * @param request Current request + * @param data MS-SOH blob + * @param data_len length of MS-SOH blob + * + * @return 0 on success, -1 on failure + * + */ +int soh_verify(REQUEST *request, uint8_t const *data, unsigned int data_len) { + + VALUE_PAIR *vp; + eap_soh hdr; + soh_response resp; + soh_mode_subheader mode; + soh_tlv tlv; + int curr_shid=-1, curr_shid_c=-1, curr_hc=-1; + + rad_assert(request->packet != NULL); + + hdr.tlv_type = soh_pull_be_16(data); data += 2; + hdr.tlv_len = soh_pull_be_16(data); data += 2; + hdr.tlv_vendor = soh_pull_be_32(data); data += 4; + + if (hdr.tlv_type != 7 || hdr.tlv_vendor != 0x137) { + RDEBUG("SoH payload is %i %08x not a ms-vendor packet", hdr.tlv_type, hdr.tlv_vendor); + return -1; + } + + hdr.soh_type = soh_pull_be_16(data); data += 2; + hdr.soh_len = soh_pull_be_16(data); data += 2; + if (hdr.soh_type != 1) { + RDEBUG("SoH tlv %04x is not a response", hdr.soh_type); + return -1; + } + + /* FIXME: check for sufficient data */ + resp.outer_type = soh_pull_be_16(data); data += 2; + resp.outer_len = soh_pull_be_16(data); data += 2; + resp.vendor = soh_pull_be_32(data); data += 4; + resp.inner_type = soh_pull_be_16(data); data += 2; + resp.inner_len = soh_pull_be_16(data); data += 2; + + + if (resp.outer_type!=7 || resp.vendor != 0x137) { + RDEBUG("SoH response outer type %i/vendor %08x not recognised", resp.outer_type, resp.vendor); + return -1; + } + switch (resp.inner_type) { + case 1: + /* no mode sub-header */ + RDEBUG("SoH without mode subheader"); + break; + + case 2: + mode.outer_type = soh_pull_be_16(data); data += 2; + mode.outer_len = soh_pull_be_16(data); data += 2; + mode.vendor = soh_pull_be_32(data); data += 4; + memcpy(mode.corrid, data, 24); data += 24; + mode.intent = data[0]; + mode.content_type = data[1]; + data += 2; + + if (mode.outer_type != 7 || mode.vendor != 0x137 || mode.content_type != 0) { + RDEBUG3("SoH mode subheader outer type %i/vendor %08x/content type %i invalid", mode.outer_type, mode.vendor, mode.content_type); + return -1; + } + RDEBUG3("SoH with mode subheader"); + break; + + default: + RDEBUG("SoH invalid inner type %i", resp.inner_type); + return -1; + } + + /* subtract off the relevant amount of data */ + if (resp.inner_type==2) { + data_len = resp.inner_len - 34; + } else { + data_len = resp.inner_len; + } + + /* TLV + * MS-SOH 2.2.1 + * See also 2.2.3 + * + * 1 bit mandatory + * 1 bit reserved + * 14 bits tlv type + * 2 bytes tlv length + * N bytes payload + * + */ + while (data_len >= 4) { + tlv.tlv_type = soh_pull_be_16(data); data += 2; + tlv.tlv_len = soh_pull_be_16(data); data += 2; + + data_len -= 4; + + switch (tlv.tlv_type) { + case 2: + /* System-Health-Id TLV + * MS-SOH 2.2.3.1 + * + * 3 bytes IANA/SMI vendor code + * 1 byte component (i.e. within vendor, which SoH component + */ + curr_shid = soh_pull_be_24(data); + curr_shid_c = data[3]; + RDEBUG2("SoH System-Health-ID vendor %08x component=%i", curr_shid, curr_shid_c); + break; + + case 7: + /* Vendor-Specific packet + * MS-SOH 2.2.3.3 + * + * 4 bytes vendor, supposedly ignored by NAP + * N bytes payload; for Microsoft component#0 this is the MS TV stuff + */ + if (curr_shid==0x137 && curr_shid_c==0) { + RDEBUG2("SoH MS type-value payload"); + eapsoh_mstlv(request, data + 4, tlv.tlv_len - 4); + } else { + RDEBUG2("SoH unhandled vendor-specific TLV %08x/component=%i %i bytes payload", + curr_shid, curr_shid_c, tlv.tlv_len); + } + break; + + case 8: + /* Health-Class + * MS-SOH 2.2.3.5.6 + * + * 1 byte integer + */ + RDEBUG2("SoH Health-Class %i", data[0]); + curr_hc = data[0]; + break; + + case 9: + /* Software-Version + * MS-SOH 2.2.3.5.7 + * + * 1 byte integer + */ + RDEBUG2("SoH Software-Version %i", data[0]); + break; + + case 11: + /* Health-Class status + * MS-SOH 2.2.3.5.9 + * + * variable data; for the MS System Health vendor, these are 4-byte + * integers which are a really, really dumb format: + * + * 28 bits ignore + * 1 bit - 1==product snoozed + * 1 bit - 1==microsoft product + * 1 bit - 1==product up-to-date + * 1 bit - 1==product enabled + */ + RDEBUG2("SoH Health-Class-Status - current shid=%08x component=%i", curr_shid, curr_shid_c); + + if (curr_shid == 0x137 && curr_shid_c == 128) { + char const *s, *t; + uint32_t hcstatus = soh_pull_be_32(data); + + RDEBUG2("SoH Health-Class-Status microsoft DWORD=%08x", hcstatus); + + vp = pair_make_request("SoH-MS-Windows-Health-Status", NULL, T_OP_EQ); + if (!vp) return 0; + + switch (curr_hc) { + case 4: + /* security updates */ + s = "security-updates"; + switch (hcstatus) { + case 0xff0005: + fr_pair_value_sprintf(vp, "%s ok all-installed", s); + break; + + case 0xff0006: + fr_pair_value_sprintf(vp, "%s warn some-missing", s); + break; + + case 0xff0008: + fr_pair_value_sprintf(vp, "%s warn never-started", s); + break; + + case 0xc0ff000c: + fr_pair_value_sprintf(vp, "%s error no-wsus-srv", s); + break; + + case 0xc0ff000d: + fr_pair_value_sprintf(vp, "%s error no-wsus-clid", s); + break; + + case 0xc0ff000e: + fr_pair_value_sprintf(vp, "%s warn wsus-disabled", s); + break; + + case 0xc0ff000f: + fr_pair_value_sprintf(vp, "%s error comm-failure", s); + break; + + case 0xc0ff0010: + fr_pair_value_sprintf(vp, "%s warn needs-reboot", s); + break; + + default: + fr_pair_value_sprintf(vp, "%s error %08x", s, hcstatus); + break; + } + break; + + case 3: + /* auto updates */ + s = "auto-updates"; + switch (hcstatus) { + case 1: + fr_pair_value_sprintf(vp, "%s warn disabled", s); + break; + + case 2: + fr_pair_value_sprintf(vp, "%s ok action=check-only", s); + break; + + case 3: + fr_pair_value_sprintf(vp, "%s ok action=download", s); + break; + + case 4: + fr_pair_value_sprintf(vp, "%s ok action=install", s); + break; + + case 5: + fr_pair_value_sprintf(vp, "%s warn unconfigured", s); + break; + + case 0xc0ff0003: + fr_pair_value_sprintf(vp, "%s warn service-down", s); + break; + + case 0xc0ff0018: + fr_pair_value_sprintf(vp, "%s warn never-started", s); + break; + + default: + fr_pair_value_sprintf(vp, "%s error %08x", s, hcstatus); + break; + } + break; + + default: + /* other - firewall, antivirus, antispyware */ + s = healthclass2str(curr_hc); + if (s) { + /* bah. this is vile. stupid microsoft + */ + if (hcstatus & 0xff000000) { + /* top octet non-zero means an error + * FIXME: is this always correct? MS-WSH 2.2.8 is unclear + */ + t = clientstatus2str(hcstatus); + if (t) { + fr_pair_value_sprintf(vp, "%s error %s", s, t); + } else { + fr_pair_value_sprintf(vp, "%s error %08x", s, hcstatus); + } + } else { + fr_pair_value_sprintf(vp, + "%s ok snoozed=%i microsoft=%i up2date=%i enabled=%i", + s, + hcstatus & 0x8 ? 1 : 0, + hcstatus & 0x4 ? 1 : 0, + hcstatus & 0x2 ? 1 : 0, + hcstatus & 0x1 ? 1 : 0 + ); + } + } else { + fr_pair_value_sprintf(vp, "%i unknown %08x", curr_hc, hcstatus); + } + break; + } + } else { + vp = pair_make_request("SoH-MS-Health-Other", NULL, T_OP_EQ); + if (!vp) return 0; + + /* FIXME: what to do with the payload? */ + fr_pair_value_sprintf(vp, "%08x/%i ?", curr_shid, curr_shid_c); + } + break; + + default: + RDEBUG("SoH Unknown TLV %i len=%i", tlv.tlv_type, tlv.tlv_len); + break; + } + + data += tlv.tlv_len; + data_len -= tlv.tlv_len; + } + + return 0; +} diff --git a/src/main/state.c b/src/main/state.c new file mode 100644 index 0000000..3700062 --- /dev/null +++ b/src/main/state.c @@ -0,0 +1,713 @@ +/* + * This program 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$ + * + * @brief Multi-packet state handling + * @file main/state.c + * + * @ingroup AVP + * + * @copyright 2014 The FreeRADIUS server project + */ +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/state.h> +#include <freeradius-devel/md5.h> +#include <freeradius-devel/rad_assert.h> +#include <freeradius-devel/process.h> + +typedef struct state_entry_t { + uint8_t state[AUTH_VECTOR_LEN]; + + time_t cleanup; + struct state_entry_t *prev; + struct state_entry_t *next; + + int tries; + + TALLOC_CTX *ctx; + VALUE_PAIR *vps; + + char *server; + unsigned int request_number; + RADCLIENT *request_client; + main_config_t *request_root; + + void *opaque; + void (*free_opaque)(void *opaque); +} state_entry_t; + +struct fr_state_t { + rbtree_t *tree; + + state_entry_t *head, *tail; + +#ifdef HAVE_PTHREAD_H + pthread_mutex_t mutex; +#endif +}; + +static fr_state_t global_state; + +#define STATE_FREE(_x) if (_x != &global_state) talloc_free(_x) + +#ifdef HAVE_PTHREAD_H + +#define PTHREAD_MUTEX_LOCK pthread_mutex_lock +#define PTHREAD_MUTEX_UNLOCK pthread_mutex_unlock + +#else +/* + * This is easier than ifdef's throughout the code. + */ +#define PTHREAD_MUTEX_LOCK(_x) +#define PTHREAD_MUTEX_UNLOCK(_x) + +#endif + +/* + * rbtree callback. + */ +static int state_entry_cmp(void const *one, void const *two) +{ + state_entry_t const *a = one; + state_entry_t const *b = two; + + return memcmp(a->state, b->state, sizeof(a->state)); +} + +static bool state_entry_link(fr_state_t *state, state_entry_t *entry) +{ + if (!rbtree_insert(state->tree, entry)) { + return false; + } + + /* + * Link it to the end of the list, which is implicitely + * ordered by cleanup time. + */ + if (!state->head) { + entry->prev = entry->next = NULL; + state->head = state->tail = entry; + } else { + rad_assert(state->tail != NULL); + + entry->prev = state->tail; + state->tail->next = entry; + + entry->next = NULL; + state->tail = entry; + } + + return true; +} + +static void state_entry_unlink(fr_state_t *state, state_entry_t *entry) +{ + state_entry_t *prev, *next; + + prev = entry->prev; + next = entry->next; + + if (prev) { + rad_assert(state->head != entry); + prev->next = next; + } else if (state->head) { + rad_assert(state->head == entry); + state->head = next; + } + + if (next) { + rad_assert(state->tail != entry); + next->prev = prev; + } else if (state->tail) { + rad_assert(state->tail == entry); + state->tail = prev; + } + + rbtree_deletebydata(state->tree, entry); +} + +/* + * When an entry is free'd, it's removed from the linked list of + * cleanup timers. + */ +static void state_entry_free(fr_state_t *state, state_entry_t *entry) +{ + /* + * If we're deleting the whole tree, don't bother doing + * all of the fixups. + */ + if (!state || !state->tree) return; + + state_entry_unlink(state, entry); + + if (entry->opaque) { + entry->free_opaque(entry->opaque); + } + +#ifdef WITH_VERIFY_PTR + (void) talloc_get_type_abort(entry, state_entry_t); +#endif + if (entry->ctx) talloc_free(entry->ctx); + + talloc_free(entry); +} + +fr_state_t *fr_state_init(TALLOC_CTX *ctx) +{ + fr_state_t *state; + + if (!ctx) { + state = &global_state; + if (state->tree) return state; + } else { + state = talloc_zero(ctx, fr_state_t); + if (!state) return 0; + } + +#ifdef HAVE_PTHREAD_H + if (pthread_mutex_init(&state->mutex, NULL) != 0) { + STATE_FREE(state); + return NULL; + } +#endif + + state->tree = rbtree_create(NULL, state_entry_cmp, NULL, 0); + if (!state->tree) { + STATE_FREE(state); + return NULL; + } + + return state; +} + +void fr_state_delete(fr_state_t *state) +{ + rbtree_t *my_tree; + + if (!state) return; + + PTHREAD_MUTEX_LOCK(&state->mutex); + + /* + * Tell the talloc callback to NOT delete the entry from + * the tree. We're deleting the entire tree. + */ + my_tree = state->tree; + state->tree = NULL; + + rbtree_free(my_tree); + PTHREAD_MUTEX_UNLOCK(&state->mutex); + + STATE_FREE(state); +} + +/* + * Create a fake request, based on what we know about the + * session that has expired, and inject it into the server to + * allow final logging or cleaning up. + */ +static REQUEST *fr_state_cleanup_request(state_entry_t *entry) +{ + REQUEST *request; + RADIUS_PACKET *packet = NULL; + RADIUS_PACKET *reply_packet = NULL; + VALUE_PAIR *vp; + + /* + * Allocate a new fake request with enough to keep + * the rest of the server happy. + */ + request = request_alloc(NULL); + if (unlikely(!request)) return NULL; + + packet = rad_alloc(request, false); + if (unlikely(!packet)) { + error: + TALLOC_FREE(reply_packet); + TALLOC_FREE(packet); + TALLOC_FREE(request); + return NULL; + } + + reply_packet = rad_alloc(request, false); + if (unlikely(!reply_packet)) goto error; + + /* + * Move the server from the state entry over to the + * request. Clearing it in the state means this + * function will never be called again. + */ + request->server = talloc_steal(request, entry->server); + entry->server = NULL; + + /* + * Build the fake request with the limited + * information we have from the state. + */ + request->packet = packet; + request->reply = reply_packet; + request->number = entry->request_number; + request->client = entry->request_client; + request->root = entry->request_root; + request->handle = rad_postauth; + + /* + * Move session-state VPS over, after first freeing the + * separately-parented state_ctx that was allocated along with the + * fake request. + */ + talloc_free(request->state_ctx); + request->state_ctx = entry->ctx; + request->state = entry->vps; + + entry->ctx = NULL; + entry->vps = NULL; + + /* + * Set correct Post-Auth-Type section + */ + fr_pair_delete_by_num(&request->config, PW_POST_AUTH_TYPE, 0, TAG_ANY); + vp = pair_make_config("Post-Auth-Type", "Client-Lost", T_OP_SET); + if (unlikely(!vp)) goto error; + + VERIFY_REQUEST(request); + return request; +} + +/* + * Check state for old entries that need to be cleaned up. If + * they are old enough then move them from the global state + * list to a list of entries to clean up (outside the mutex). + * Called with the mutex held. + */ +static state_entry_t *fr_state_cleanup_find(fr_state_t *state) +{ + time_t now = time(NULL); + state_entry_t *entry, *next; + state_entry_t *head = NULL, **tail = &head; + + for (entry = state->head; entry != NULL; entry = next) { + next = entry->next; + + /* + * Unused. We can delete it, even if now isn't + * the time to clean it up. + */ + if (!entry->ctx && !entry->opaque) { + state_entry_free(state, entry); + continue; + } + + /* + * Not yet time to clean it up. + */ + if (entry->cleanup > now) { + continue; + } + + /* + * We're not running the "client lost" section. + * Just nuke the entry now. + */ + if (!main_config.postauth_client_lost) { + state_entry_free(state, entry); + continue; + } + + /* + * Old enough that the request has been removed. + * We can add it to the cleanup list. + */ + state_entry_unlink(state, entry); + entry->prev = entry->next = NULL; + (*tail) = entry; + tail = &entry->next; + } + + return head; +} + +/* + * Inject all requests in cleanup list for cleanup post-auth + */ +static void fr_state_cleanup(state_entry_t *head) +{ + state_entry_t *entry, *next; + + if (!head) return; + + for (entry = head; entry != NULL; entry = next) { + REQUEST *request; + + next = entry->next; + + request = fr_state_cleanup_request(entry); + if (request) { + RDEBUG2("No response from client, cleaning up expired state"); + RDEBUG2("Restoring &session-state"); + + /* + * @todo - print out message + * saying where the handler was + * in the process? i.e. "sent + * server cert", etc. This will + * require updating the EAP code + * to put a new attribute into + * the session state list. + */ + + rdebug_pair_list(L_DBG_LVL_2, request, request->state, "&session-state:"); + + request_inject(request); + } + + talloc_free(entry); + } +} + + +/* + * Create a new entry. Called with the mutex held. + */ +static state_entry_t *fr_state_entry_create(fr_state_t *state, REQUEST *request, RADIUS_PACKET *packet, state_entry_t *old) +{ + size_t i; + uint32_t x; + time_t now = time(NULL); + VALUE_PAIR *vp; + state_entry_t *entry; + + /* + * Limit the size of the cache based on how many requests + * we can handle at the same time. + */ + if (rbtree_num_elements(state->tree) >= main_config.max_requests * 2) { + return NULL; + } + + /* + * Allocate a new one. + */ + entry = talloc_zero(state->tree, state_entry_t); + if (!entry) return NULL; + + /* + * Limit the lifetime of this entry based on how long the + * server takes to process a request. Doing it this way + * isn't perfect, but it's reasonable, and it's one less + * thing for an administrator to configure. + */ + entry->cleanup = now + main_config.max_request_time * 2; + + /* + * Hacks for EAP, until we convert EAP to using the state API. + * + * The EAP module creates it's own State attribute, so we + * want to use that one in preference to one we create. + */ + vp = fr_pair_find_by_num(packet->vps, PW_STATE, 0, TAG_ANY); + + /* + * If possible, base the new one off of the old one. + */ + if (old) { + entry->tries = old->tries + 1; + + /* + * Track State + */ + if (!vp) { + memcpy(entry->state, old->state, sizeof(entry->state)); + + entry->state[1] = entry->state[0] ^ entry->tries; + entry->state[8] = entry->state[2] ^ ((((uint32_t) HEXIFY(RADIUSD_VERSION)) >> 16) & 0xff); + entry->state[10] = entry->state[2] ^ ((((uint32_t) HEXIFY(RADIUSD_VERSION)) >> 8) & 0xff); + entry->state[12] = entry->state[2] ^ (((uint32_t) HEXIFY(RADIUSD_VERSION)) & 0xff); + } + + /* + * The old one isn't used any more, so we can free it. + */ + if (!old->opaque) state_entry_free(state, old); + + } else if (!vp) { + /* + * 16 octets of randomness should be enough to + * have a globally unique state. + */ + for (i = 0; i < sizeof(entry->state) / sizeof(x); i++) { + x = fr_rand(); + memcpy(entry->state + (i * 4), &x, sizeof(x)); + } + } + + /* + * If EAP created a State, use that. Otherwise, use the + * one we created above. + */ + if (vp) { + /* + * Assume our own State first. + */ + if (vp->vp_length == sizeof(entry->state)) { + memcpy(entry->state, vp->vp_octets, sizeof(entry->state)); + + /* + * Too big? Get the MD5 hash, in order + * to depend on the entire contents of State. + */ + } else if (vp->vp_length > sizeof(entry->state)) { + fr_md5_calc(entry->state, vp->vp_octets, vp->vp_length); + + /* + * Too small? Use the whole thing, and + * set the rest of entry->state to zero. + */ + } else { + memcpy(entry->state, vp->vp_octets, vp->vp_length); + memset(&entry->state[vp->vp_length], 0, sizeof(entry->state) - vp->vp_length); + } + } else { + vp = fr_pair_afrom_num(packet, PW_STATE, 0); + fr_pair_value_memcpy(vp, entry->state, sizeof(entry->state)); + fr_pair_add(&packet->vps, vp); + } + + /* Make unique for different virtual servers handling same request + */ + if (request->server) { + /* + * Make unique for different virtual servers handling same request + */ + *((uint32_t *)(&entry->state[4])) ^= fr_hash_string(request->server); + + /* + * Copy server to state in case it's needed for cleanup + */ + entry->server = talloc_strdup(entry, request->server); + entry->request_number = request->number; + entry->request_client = request->client; + entry->request_root = request->root; + } + + if (!state_entry_link(state, entry)) { + talloc_free(entry); + return NULL; + } + + return entry; +} + + +/* + * Find the entry, based on the State attribute. + */ +static state_entry_t *fr_state_find(fr_state_t *state, const char *server, RADIUS_PACKET *packet) +{ + VALUE_PAIR *vp; + state_entry_t *entry, my_entry; + + vp = fr_pair_find_by_num(packet->vps, PW_STATE, 0, TAG_ANY); + if (!vp) return NULL; + + /* + * Assume our own State first. + */ + if (vp->vp_length == sizeof(my_entry.state)) { + memcpy(my_entry.state, vp->vp_octets, sizeof(my_entry.state)); + + /* + * Too big? Get the MD5 hash, in order + * to depend on the entire contents of State. + */ + } else if (vp->vp_length > sizeof(my_entry.state)) { + fr_md5_calc(my_entry.state, vp->vp_octets, vp->vp_length); + + /* + * Too small? Use the whole thing, and + * set the rest of my_entry.state to zero. + */ + } else { + memcpy(my_entry.state, vp->vp_octets, vp->vp_length); + memset(&my_entry.state[vp->vp_length], 0, sizeof(my_entry.state) - vp->vp_length); + } + + /* Make unique for different virtual servers handling same request + */ + if (server) *((uint32_t *)(&my_entry.state[4])) ^= fr_hash_string(server); + + entry = rbtree_finddata(state->tree, &my_entry); + +#ifdef WITH_VERIFY_PTR + if (entry) (void) talloc_get_type_abort(entry, state_entry_t); +#endif + + return entry; +} + +/* + * Called when sending Access-Accept or Access-Reject, so + * that all State is discarded. + */ +void fr_state_discard(REQUEST *request, RADIUS_PACKET *original) +{ + state_entry_t *entry; + fr_state_t *state = &global_state; + + fr_pair_list_free(&request->state); + request->state = NULL; + + PTHREAD_MUTEX_LOCK(&state->mutex); + entry = fr_state_find(state, request->server, original); + if (entry) state_entry_free(state, entry); + PTHREAD_MUTEX_UNLOCK(&state->mutex); +} + +/* + * Get the VPS from the state. + */ +void fr_state_get_vps(REQUEST *request, RADIUS_PACKET *packet) +{ + state_entry_t *entry; + fr_state_t *state = &global_state; + TALLOC_CTX *old_ctx = NULL; + + /* + * No State, don't do anything. + */ + if (!fr_pair_find_by_num(request->packet->vps, PW_STATE, 0, TAG_ANY)) { + RDEBUG3("session-state: No State attribute"); + return; + } + + rad_assert(request->state == NULL); + + PTHREAD_MUTEX_LOCK(&state->mutex); + entry = fr_state_find(state, request->server, packet); + + /* + * This has to be done in a mutex lock, because talloc + * isn't thread-safe. + */ + if (entry) { + RDEBUG2("Restoring &session-state"); + + if (request->state_ctx) old_ctx = request->state_ctx; + + request->state_ctx = entry->ctx; + request->state = entry->vps; + + entry->ctx = NULL; + entry->vps = NULL; + + rdebug_pair_list(L_DBG_LVL_2, request, request->state, "&session-state:"); + + } else { + RDEBUG2("session-state: No cached attributes"); + } + + PTHREAD_MUTEX_UNLOCK(&state->mutex); + + /* + * Free this outside of the mutex for less contention. + */ + if (old_ctx) talloc_free(old_ctx); + + VERIFY_REQUEST(request); + return; +} + + +/* + * Put request->state into the State attribute. Put the State + * attribute into the vps list. Delete the original entry, if it + * exists. + */ +bool fr_state_put_vps(REQUEST *request, RADIUS_PACKET *original, RADIUS_PACKET *packet) +{ + state_entry_t *entry, *old; + fr_state_t *state = &global_state; + state_entry_t *cleanup_list; + + if (!request->state) { + size_t i; + uint32_t x; + VALUE_PAIR *vp; + uint8_t buffer[16]; + + RDEBUG3("session-state: Nothing to cache"); + + if (packet->code != PW_CODE_ACCESS_CHALLENGE) return true; + + vp = fr_pair_find_by_num(packet->vps, PW_STATE, 0, TAG_ANY); + if (vp) return true; + + /* + * + */ + for (i = 0; i < sizeof(buffer) / sizeof(x); i++) { + x = fr_rand(); + memcpy(buffer + (i * 4), &x, sizeof(x)); + } + + vp = fr_pair_afrom_num(packet, PW_STATE, 0); + fr_pair_value_memcpy(vp, buffer, sizeof(buffer)); + fr_pair_add(&packet->vps, vp); + + return true; + } + + RDEBUG2("session-state: Saving cached attributes"); + rdebug_pair_list(L_DBG_LVL_1, request, request->state, NULL); + + PTHREAD_MUTEX_LOCK(&state->mutex); + + cleanup_list = fr_state_cleanup_find(state); + + if (original) { + old = fr_state_find(state, request->server, original); + } else { + old = NULL; + } + + /* + * Create a new entry and add it to the list. + */ + entry = fr_state_entry_create(state, request, packet, old); + if (!entry) { + PTHREAD_MUTEX_UNLOCK(&state->mutex); + fr_state_cleanup(cleanup_list); + return false; + } + + rad_assert(entry->ctx == NULL); + entry->ctx = request->state_ctx; + entry->vps = request->state; + + request->state_ctx = NULL; + request->state = NULL; + + PTHREAD_MUTEX_UNLOCK(&state->mutex); + fr_state_cleanup(cleanup_list); + + VERIFY_REQUEST(request); + return true; +} diff --git a/src/main/stats.c b/src/main/stats.c new file mode 100644 index 0000000..a5c672e --- /dev/null +++ b/src/main/stats.c @@ -0,0 +1,1028 @@ +/* + * stats.c Internal statistics handling. + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2008 The FreeRADIUS server project + * Copyright 2008 Alan DeKok <aland@deployingradius.com> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/rad_assert.h> + +#ifdef WITH_STATS + +#define USEC (1000000) +#define EMA_SCALE (100) +#define F_EMA_SCALE (1000000) + +static struct timeval start_time; +static struct timeval hup_time; + +#define FR_STATS_INIT { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, \ + { 0, 0, 0, 0, 0, 0, 0, 0 }} + +fr_stats_t radius_auth_stats = FR_STATS_INIT; +#ifdef WITH_ACCOUNTING +fr_stats_t radius_acct_stats = FR_STATS_INIT; +#endif +#ifdef WITH_COA +fr_stats_t radius_coa_stats = FR_STATS_INIT; +fr_stats_t radius_dsc_stats = FR_STATS_INIT; +#endif + +#ifdef WITH_PROXY +fr_stats_t proxy_auth_stats = FR_STATS_INIT; +#ifdef WITH_ACCOUNTING +fr_stats_t proxy_acct_stats = FR_STATS_INIT; +#endif +#ifdef WITH_COA +fr_stats_t proxy_coa_stats = FR_STATS_INIT; +fr_stats_t proxy_dsc_stats = FR_STATS_INIT; +#endif +#endif + +static void stats_time(fr_stats_t *stats, struct timeval *start, + struct timeval *end) +{ + struct timeval diff; + uint32_t delay; + + if ((start->tv_sec == 0) || (end->tv_sec == 0) || + (end->tv_sec < start->tv_sec)) return; + + rad_tv_sub(end, start, &diff); + + if (diff.tv_sec >= 10) { + stats->elapsed[7]++; + } else { + int i; + uint32_t cmp; + + delay = (diff.tv_sec * USEC) + diff.tv_usec; + + cmp = 10; + for (i = 0; i < 7; i++) { + if (delay < cmp) { + stats->elapsed[i]++; + break; + } + cmp *= 10; + } + } +} + +void request_stats_final(REQUEST *request) +{ + rad_listen_t *listener; + + if (request->master_state == REQUEST_COUNTED) return; + + if (!request->listener) return; + if (!request->client) return; + + if ((request->listener->type != RAD_LISTEN_NONE) && +#ifdef WITH_ACCOUNTING + (request->listener->type != RAD_LISTEN_ACCT) && +#endif +#ifdef WITH_COA + (request->listener->type != RAD_LISTEN_COA) && +#endif + (request->listener->type != RAD_LISTEN_AUTH)) return; + + /* don't count statistic requests */ + if (request->packet->code == PW_CODE_STATUS_SERVER) + return; + + /* + * Deal with TCP / TLS issues. The statistics are kept in the parent socket. + */ + listener = request->listener; + if (listener->parent) listener = listener->parent; + +#undef INC_AUTH +#define INC_AUTH(_x) radius_auth_stats._x++;listener->stats._x++;request->client->auth._x++; + +#undef INC_ACCT +#ifdef WITH_ACCOUNTING +#define INC_ACCT(_x) radius_acct_stats._x++;listener->stats._x++;request->client->acct._x++ +#else +#define INC_ACCT(_x) +#endif + +#undef INC_COA +#ifdef WITH_COA +#define INC_COA(_x) radius_coa_stats._x++;listener->stats._x++;request->client->coa._x++ +#else +#define INC_COA(_x) +#endif + +#undef INC_DSC +#ifdef WITH_DSC +#define INC_DSC(_x) radius_dsc_stats._x++;listener->stats._x++;request->client->dsc._x++ +#else +#define INC_DSC(_x) +#endif + + /* + * Update the statistics. + * + * Note that we do NOT do this in a child thread. + * Instead, we update the stats when a request is + * deleted, because only the main server thread calls + * this function, which makes it thread-safe. + */ + if (request->reply && (request->packet->code != PW_CODE_STATUS_SERVER)) switch (request->reply->code) { + case PW_CODE_ACCESS_ACCEPT: + INC_AUTH(total_access_accepts); + + auth_stats: + INC_AUTH(total_responses); + + /* + * FIXME: Do the time calculations once... + */ + stats_time(&radius_auth_stats, + &request->packet->timestamp, + &request->reply->timestamp); + stats_time(&request->client->auth, + &request->packet->timestamp, + &request->reply->timestamp); + stats_time(&listener->stats, + &request->packet->timestamp, + &request->reply->timestamp); + break; + + case PW_CODE_ACCESS_REJECT: + INC_AUTH(total_access_rejects); + goto auth_stats; + + case PW_CODE_ACCESS_CHALLENGE: + INC_AUTH(total_access_challenges); + goto auth_stats; + +#ifdef WITH_ACCOUNTING + case PW_CODE_ACCOUNTING_RESPONSE: + INC_ACCT(total_responses); + stats_time(&radius_acct_stats, + &request->packet->timestamp, + &request->reply->timestamp); + stats_time(&request->client->acct, + &request->packet->timestamp, + &request->reply->timestamp); + break; +#endif + +#ifdef WITH_COA + case PW_CODE_COA_ACK: + INC_COA(total_access_accepts); + coa_stats: + INC_COA(total_responses); + stats_time(&request->client->coa, + &request->packet->timestamp, + &request->reply->timestamp); + break; + + case PW_CODE_COA_NAK: + INC_COA(total_access_rejects); + goto coa_stats; + + case PW_CODE_DISCONNECT_ACK: + INC_DSC(total_access_accepts); + dsc_stats: + INC_DSC(total_responses); + stats_time(&request->client->dsc, + &request->packet->timestamp, + &request->reply->timestamp); + break; + + case PW_CODE_DISCONNECT_NAK: + INC_DSC(total_access_rejects); + goto dsc_stats; +#endif + + /* + * No response, it must have been a bad + * authenticator. + */ + case 0: + if (request->packet->code == PW_CODE_ACCESS_REQUEST) { + if (request->reply->offset == -2) { + INC_AUTH(total_bad_authenticators); + } else { + INC_AUTH(total_packets_dropped); + } + } else if (request->packet->code == PW_CODE_ACCOUNTING_REQUEST) { + if (request->reply->offset == -2) { + INC_ACCT(total_bad_authenticators); + } else { + INC_ACCT(total_packets_dropped); + } + } + break; + + default: + break; + } + +#ifdef WITH_PROXY + if (!request->proxy || !request->home_server) goto done; /* simplifies formatting */ + + switch (request->proxy->code) { + case PW_CODE_ACCESS_REQUEST: + proxy_auth_stats.total_requests += request->num_proxied_requests; + request->home_server->stats.total_requests += request->num_proxied_requests; + break; + +#ifdef WITH_ACCOUNTING + case PW_CODE_ACCOUNTING_REQUEST: + proxy_acct_stats.total_requests += request->num_proxied_requests; + request->home_server->stats.total_requests += request->num_proxied_requests; + break; +#endif + +#ifdef WITH_COA + case PW_CODE_COA_REQUEST: + proxy_coa_stats.total_requests += request->num_proxied_requests; + request->home_server->stats.total_requests += request->num_proxied_requests; + break; + + case PW_CODE_DISCONNECT_REQUEST: + proxy_dsc_stats.total_requests += request->num_proxied_requests; + request->home_server->stats.total_requests += request->num_proxied_requests; + break; +#endif + + default: + break; + } + + if (!request->proxy_reply) goto done; /* simplifies formatting */ + +#undef INC +#define INC(_x) proxy_auth_stats._x += request->num_proxied_responses; request->home_server->stats._x += request->num_proxied_responses; + + switch (request->proxy_reply->code) { + case PW_CODE_ACCESS_ACCEPT: + INC(total_access_accepts); + proxy_stats: + INC(total_responses); + stats_time(&proxy_auth_stats, + &request->proxy->timestamp, + &request->proxy_reply->timestamp); + stats_time(&request->home_server->stats, + &request->proxy->timestamp, + &request->proxy_reply->timestamp); + break; + + case PW_CODE_ACCESS_REJECT: + INC(total_access_rejects); + goto proxy_stats; + + case PW_CODE_ACCESS_CHALLENGE: + INC(total_access_challenges); + goto proxy_stats; + +#ifdef WITH_ACCOUNTING + case PW_CODE_ACCOUNTING_RESPONSE: + proxy_acct_stats.total_responses++; + request->home_server->stats.total_responses++; + stats_time(&proxy_acct_stats, + &request->proxy->timestamp, + &request->proxy_reply->timestamp); + stats_time(&request->home_server->stats, + &request->proxy->timestamp, + &request->proxy_reply->timestamp); + break; +#endif + +#ifdef WITH_COA + case PW_CODE_COA_ACK: + case PW_CODE_COA_NAK: + proxy_coa_stats.total_responses++; + request->home_server->stats.total_responses++; + stats_time(&proxy_coa_stats, + &request->proxy->timestamp, + &request->proxy_reply->timestamp); + stats_time(&request->home_server->stats, + &request->proxy->timestamp, + &request->proxy_reply->timestamp); + break; + + case PW_CODE_DISCONNECT_ACK: + case PW_CODE_DISCONNECT_NAK: + proxy_dsc_stats.total_responses++; + request->home_server->stats.total_responses++; + stats_time(&proxy_dsc_stats, + &request->proxy->timestamp, + &request->proxy_reply->timestamp); + stats_time(&request->home_server->stats, + &request->proxy->timestamp, + &request->proxy_reply->timestamp); + break; +#endif + + default: + proxy_auth_stats.total_unknown_types++; + request->home_server->stats.total_unknown_types++; + break; + } + + done: +#endif /* WITH_PROXY */ + + + if (request->max_time) { + RADCLIENT *client = request->client; + + switch (request->packet->code) { + case PW_CODE_ACCESS_REQUEST: + FR_STATS_INC(auth, unresponsive_child); + break; + +#ifdef WITH_ACCOUNTING + case PW_CODE_ACCOUNTING_REQUEST: + FR_STATS_INC(acct, unresponsive_child); + break; +#endif +#ifdef WITH_COA + case PW_CODE_COA_REQUEST: + FR_STATS_INC(coa, unresponsive_child); + break; + + case PW_CODE_DISCONNECT_REQUEST: + FR_STATS_INC(dsc, unresponsive_child); + break; +#endif + + default: + break; + } + } + + request->master_state = REQUEST_COUNTED; +} + +typedef struct fr_stats2vp { + int attribute; + size_t offset; +} fr_stats2vp; + +/* + * Authentication + */ +static fr_stats2vp authvp[] = { + { PW_FREERADIUS_TOTAL_ACCESS_REQUESTS, offsetof(fr_stats_t, total_requests) }, + { PW_FREERADIUS_TOTAL_ACCESS_ACCEPTS, offsetof(fr_stats_t, total_access_accepts) }, + { PW_FREERADIUS_TOTAL_ACCESS_REJECTS, offsetof(fr_stats_t, total_access_rejects) }, + { PW_FREERADIUS_TOTAL_ACCESS_CHALLENGES, offsetof(fr_stats_t, total_access_challenges) }, + { PW_FREERADIUS_TOTAL_AUTH_RESPONSES, offsetof(fr_stats_t, total_responses) }, + { PW_FREERADIUS_TOTAL_AUTH_DUPLICATE_REQUESTS, offsetof(fr_stats_t, total_dup_requests) }, + { PW_FREERADIUS_TOTAL_AUTH_MALFORMED_REQUESTS, offsetof(fr_stats_t, total_malformed_requests) }, + { PW_FREERADIUS_TOTAL_AUTH_INVALID_REQUESTS, offsetof(fr_stats_t, total_bad_authenticators) }, + { PW_FREERADIUS_TOTAL_AUTH_DROPPED_REQUESTS, offsetof(fr_stats_t, total_packets_dropped) }, + { PW_FREERADIUS_TOTAL_AUTH_UNKNOWN_TYPES, offsetof(fr_stats_t, total_unknown_types) }, + { PW_FREERADIUS_TOTAL_AUTH_CONFLICTS, offsetof(fr_stats_t, total_conflicts) }, + { 0, 0 } +}; + + +#ifdef WITH_PROXY +/* + * Proxied authentication requests. + */ +static fr_stats2vp proxy_authvp[] = { + { PW_FREERADIUS_TOTAL_PROXY_ACCESS_REQUESTS, offsetof(fr_stats_t, total_requests) }, + { PW_FREERADIUS_TOTAL_PROXY_ACCESS_ACCEPTS, offsetof(fr_stats_t, total_access_accepts) }, + { PW_FREERADIUS_TOTAL_PROXY_ACCESS_REJECTS, offsetof(fr_stats_t, total_access_rejects) }, + { PW_FREERADIUS_TOTAL_PROXY_ACCESS_CHALLENGES, offsetof(fr_stats_t, total_access_challenges) }, + { PW_FREERADIUS_TOTAL_PROXY_AUTH_RESPONSES, offsetof(fr_stats_t, total_responses) }, + { PW_FREERADIUS_TOTAL_PROXY_AUTH_DUPLICATE_REQUESTS, offsetof(fr_stats_t, total_dup_requests) }, + { PW_FREERADIUS_TOTAL_PROXY_AUTH_MALFORMED_REQUESTS, offsetof(fr_stats_t, total_malformed_requests) }, + { PW_FREERADIUS_TOTAL_PROXY_AUTH_INVALID_REQUESTS, offsetof(fr_stats_t, total_bad_authenticators) }, + { PW_FREERADIUS_TOTAL_PROXY_AUTH_DROPPED_REQUESTS, offsetof(fr_stats_t, total_packets_dropped) }, + { PW_FREERADIUS_TOTAL_PROXY_AUTH_UNKNOWN_TYPES, offsetof(fr_stats_t, total_unknown_types) }, + { 0, 0 } +}; +#endif + + +#ifdef WITH_ACCOUNTING +/* + * Accounting + */ +static fr_stats2vp acctvp[] = { + { PW_FREERADIUS_TOTAL_ACCOUNTING_REQUESTS, offsetof(fr_stats_t, total_requests) }, + { PW_FREERADIUS_TOTAL_ACCOUNTING_RESPONSES, offsetof(fr_stats_t, total_responses) }, + { PW_FREERADIUS_TOTAL_ACCT_DUPLICATE_REQUESTS, offsetof(fr_stats_t, total_dup_requests) }, + { PW_FREERADIUS_TOTAL_ACCT_MALFORMED_REQUESTS, offsetof(fr_stats_t, total_malformed_requests) }, + { PW_FREERADIUS_TOTAL_ACCT_INVALID_REQUESTS, offsetof(fr_stats_t, total_bad_authenticators) }, + { PW_FREERADIUS_TOTAL_ACCT_DROPPED_REQUESTS, offsetof(fr_stats_t, total_packets_dropped) }, + { PW_FREERADIUS_TOTAL_ACCT_UNKNOWN_TYPES, offsetof(fr_stats_t, total_unknown_types) }, + { PW_FREERADIUS_TOTAL_ACCT_CONFLICTS, offsetof(fr_stats_t, total_conflicts) }, + { 0, 0 } +}; + +#ifdef WITH_PROXY +static fr_stats2vp proxy_acctvp[] = { + { PW_FREERADIUS_TOTAL_PROXY_ACCOUNTING_REQUESTS, offsetof(fr_stats_t, total_requests) }, + { PW_FREERADIUS_TOTAL_PROXY_ACCOUNTING_RESPONSES, offsetof(fr_stats_t, total_responses) }, + { PW_FREERADIUS_TOTAL_PROXY_ACCT_DUPLICATE_REQUESTS, offsetof(fr_stats_t, total_dup_requests) }, + { PW_FREERADIUS_TOTAL_PROXY_ACCT_MALFORMED_REQUESTS, offsetof(fr_stats_t, total_malformed_requests) }, + { PW_FREERADIUS_TOTAL_PROXY_ACCT_INVALID_REQUESTS, offsetof(fr_stats_t, total_bad_authenticators) }, + { PW_FREERADIUS_TOTAL_PROXY_ACCT_DROPPED_REQUESTS, offsetof(fr_stats_t, total_packets_dropped) }, + { PW_FREERADIUS_TOTAL_PROXY_ACCT_UNKNOWN_TYPES, offsetof(fr_stats_t, total_unknown_types) }, + { 0, 0 } +}; +#endif +#endif + +static fr_stats2vp client_authvp[] = { + { PW_FREERADIUS_TOTAL_ACCESS_REQUESTS, offsetof(fr_stats_t, total_requests) }, + { PW_FREERADIUS_TOTAL_ACCESS_ACCEPTS, offsetof(fr_stats_t, total_access_accepts) }, + { PW_FREERADIUS_TOTAL_ACCESS_REJECTS, offsetof(fr_stats_t, total_access_rejects) }, + { PW_FREERADIUS_TOTAL_ACCESS_CHALLENGES, offsetof(fr_stats_t, total_access_challenges) }, + { PW_FREERADIUS_TOTAL_AUTH_RESPONSES, offsetof(fr_stats_t, total_responses) }, + { PW_FREERADIUS_TOTAL_AUTH_DUPLICATE_REQUESTS, offsetof(fr_stats_t, total_dup_requests) }, + { PW_FREERADIUS_TOTAL_AUTH_MALFORMED_REQUESTS, offsetof(fr_stats_t, total_malformed_requests) }, + { PW_FREERADIUS_TOTAL_AUTH_INVALID_REQUESTS, offsetof(fr_stats_t, total_bad_authenticators) }, + { PW_FREERADIUS_TOTAL_AUTH_DROPPED_REQUESTS, offsetof(fr_stats_t, total_packets_dropped) }, + { PW_FREERADIUS_TOTAL_AUTH_UNKNOWN_TYPES, offsetof(fr_stats_t, total_unknown_types) }, + { 0, 0 } +}; + +#ifdef WITH_ACCOUNTING +static fr_stats2vp client_acctvp[] = { + { PW_FREERADIUS_TOTAL_ACCOUNTING_REQUESTS, offsetof(fr_stats_t, total_requests) }, + { PW_FREERADIUS_TOTAL_ACCOUNTING_RESPONSES, offsetof(fr_stats_t, total_responses) }, + { PW_FREERADIUS_TOTAL_ACCT_DUPLICATE_REQUESTS, offsetof(fr_stats_t, total_dup_requests) }, + { PW_FREERADIUS_TOTAL_ACCT_MALFORMED_REQUESTS, offsetof(fr_stats_t, total_malformed_requests) }, + { PW_FREERADIUS_TOTAL_ACCT_INVALID_REQUESTS, offsetof(fr_stats_t, total_bad_authenticators) }, + { PW_FREERADIUS_TOTAL_ACCT_DROPPED_REQUESTS, offsetof(fr_stats_t, total_packets_dropped) }, + { PW_FREERADIUS_TOTAL_ACCT_UNKNOWN_TYPES, offsetof(fr_stats_t, total_unknown_types) }, + { 0, 0 } +}; +#endif + +static void request_stats_addvp(REQUEST *request, + fr_stats2vp *table, fr_stats_t *stats) +{ + int i; + uint64_t counter; + VALUE_PAIR *vp; + + for (i = 0; table[i].attribute != 0; i++) { + vp = radius_pair_create(request->reply, &request->reply->vps, + table[i].attribute, VENDORPEC_FREERADIUS); + if (!vp) continue; + + counter = *(uint64_t *) (((uint8_t *) stats) + table[i].offset); + vp->vp_integer = counter; + } +} + +static void stats_error(REQUEST *request, char const *msg) +{ + VALUE_PAIR *vp; + + vp = radius_pair_create(request->reply, &request->reply->vps, + PW_FREERADIUS_STATS_ERROR, VENDORPEC_FREERADIUS); + if (!vp) return; + + fr_pair_value_strcpy(vp, msg); +} + + +void request_stats_reply(REQUEST *request) +{ + VALUE_PAIR *flag, *vp; + + /* + * Statistics are available ONLY on a "status" port. + */ + rad_assert(request->packet->code == PW_CODE_STATUS_SERVER); + rad_assert(request->listener->type == RAD_LISTEN_NONE); + + flag = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATISTICS_TYPE, VENDORPEC_FREERADIUS, TAG_ANY); + if (!flag || (flag->vp_integer == 0)) return; + + /* + * Authentication. + */ + if (((flag->vp_integer & 0x01) != 0) && + ((flag->vp_integer & 0xc0) == 0)) { + request_stats_addvp(request, authvp, &radius_auth_stats); + } + +#ifdef WITH_ACCOUNTING + /* + * Accounting + */ + if (((flag->vp_integer & 0x02) != 0) && + ((flag->vp_integer & 0xc0) == 0)) { + request_stats_addvp(request, acctvp, &radius_acct_stats); + } +#endif + +#ifdef WITH_PROXY + /* + * Proxied authentication requests. + */ + if (((flag->vp_integer & 0x04) != 0) && + ((flag->vp_integer & 0x20) == 0)) { + request_stats_addvp(request, proxy_authvp, &proxy_auth_stats); + } + +#ifdef WITH_ACCOUNTING + /* + * Proxied accounting requests. + */ + if (((flag->vp_integer & 0x08) != 0) && + ((flag->vp_integer & 0x20) == 0)) { + request_stats_addvp(request, proxy_acctvp, &proxy_acct_stats); + } +#endif +#endif + + /* + * Internal server statistics + */ + if ((flag->vp_integer & 0x10) != 0) { + vp = radius_pair_create(request->reply, &request->reply->vps, + PW_FREERADIUS_STATS_START_TIME, VENDORPEC_FREERADIUS); + if (vp) vp->vp_date = start_time.tv_sec; + vp = radius_pair_create(request->reply, &request->reply->vps, + PW_FREERADIUS_STATS_HUP_TIME, VENDORPEC_FREERADIUS); + if (vp) vp->vp_date = hup_time.tv_sec; + +#ifdef HAVE_PTHREAD_H + int i, array[RAD_LISTEN_MAX], stats[3]; + + thread_pool_queue_stats(array, stats); + + for (i = 0; i <= 4; i++) { + vp = radius_pair_create(request->reply, &request->reply->vps, + PW_FREERADIUS_QUEUE_LEN_INTERNAL + i, VENDORPEC_FREERADIUS); + + if (!vp) continue; + vp->vp_integer = array[i]; + } + + for (i = 0; i < 2; i++) { + vp = radius_pair_create(request->reply, &request->reply->vps, + PW_FREERADIUS_QUEUE_PPS_IN + i, VENDORPEC_FREERADIUS); + + if (!vp) continue; + vp->vp_integer = stats[i]; + } + + thread_pool_thread_stats(stats); + + for (i = 0; i < 3; i++) { + vp = radius_pair_create(request->reply, &request->reply->vps, + PW_FREERADIUS_STATS_THREADS_ACTIVE + i, VENDORPEC_FREERADIUS); + + if (!vp) continue; + vp->vp_integer = stats[i]; + } +#endif + } + + /* + * For a particular client. + */ + if ((flag->vp_integer & 0x20) != 0) { + fr_ipaddr_t ipaddr; + VALUE_PAIR *server_ip, *server_port = NULL; + RADCLIENT *client = NULL; + RADCLIENT_LIST *cl = NULL; + + /* + * See if we need to look up the client by server + * socket. + */ + server_ip = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_SERVER_IP_ADDRESS, VENDORPEC_FREERADIUS, TAG_ANY); + if (server_ip) { + server_port = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_SERVER_PORT, VENDORPEC_FREERADIUS, TAG_ANY); + + if (server_port) { + ipaddr.af = AF_INET; + ipaddr.ipaddr.ip4addr.s_addr = server_ip->vp_ipaddr; + cl = listener_find_client_list(&ipaddr, server_port->vp_integer, IPPROTO_UDP); + + /* + * Not found: don't do anything + */ + if (!cl) return; + } +#ifdef AF_INET6 + } else { + server_ip = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_SERVER_IPV6_ADDRESS, VENDORPEC_FREERADIUS, TAG_ANY); + if (server_ip) { + server_port = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_SERVER_PORT, VENDORPEC_FREERADIUS, TAG_ANY); + if (server_port) { + ipaddr.af = AF_INET6; + ipaddr.ipaddr.ip6addr = server_ip->vp_ipv6addr; + cl = listener_find_client_list(&ipaddr, server_port->vp_integer, IPPROTO_UDP); + + /* + * Not found: don't do anything + */ + if (!cl) return; + } + } +#endif /* AF_INET6 */ + } + + + vp = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_CLIENT_IP_ADDRESS, VENDORPEC_FREERADIUS, TAG_ANY); + if (vp) { + memset(&ipaddr, 0, sizeof(ipaddr)); + ipaddr.af = AF_INET; + ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr; + client = client_find(cl, &ipaddr, IPPROTO_UDP); +#ifdef WITH_TCP + if (!client) { + client = client_find(cl, &ipaddr, IPPROTO_TCP); + } +#endif + +#ifdef AF_INET6 + } else if ((vp = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_CLIENT_IPV6_ADDRESS, VENDORPEC_FREERADIUS, TAG_ANY)) != NULL) { + memset(&ipaddr, 0, sizeof(ipaddr)); + ipaddr.af = AF_INET6; + ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr; + client = client_find(cl, &ipaddr, IPPROTO_UDP); +#ifdef WITH_TCP + if (!client) { + client = client_find(cl, &ipaddr, IPPROTO_TCP); + } +#endif +#endif /* AF_INET6 */ + + /* + * Else look it up by number. + */ + } else if ((vp = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_CLIENT_NUMBER, VENDORPEC_FREERADIUS, TAG_ANY)) != NULL) { + client = client_findbynumber(cl, vp->vp_integer); + } + + if (client) { + /* + * If found, echo it back, along with + * the requested statistics. + */ + fr_pair_add(&request->reply->vps, fr_pair_copy(request->reply, vp)); + + /* + * When retrieving client by number, also + * echo back it's IP address. + */ + if (vp->da->type == PW_TYPE_INTEGER) { + if (client->ipaddr.af == AF_INET) { + vp = radius_pair_create(request->reply, + &request->reply->vps, + PW_FREERADIUS_STATS_CLIENT_IP_ADDRESS, VENDORPEC_FREERADIUS); + if (vp) { + vp->vp_ipaddr = client->ipaddr.ipaddr.ip4addr.s_addr; + } + + if (client->ipaddr.prefix != 32) { + vp = radius_pair_create(request->reply, + &request->reply->vps, + PW_FREERADIUS_STATS_CLIENT_NETMASK, VENDORPEC_FREERADIUS); + if (vp) { + vp->vp_integer = client->ipaddr.prefix; + } + } + } + +#ifdef AF_INET6 + if (client->ipaddr.af == AF_INET6) { + vp = radius_pair_create(request->reply, + &request->reply->vps, + PW_FREERADIUS_STATS_CLIENT_IPV6_ADDRESS, VENDORPEC_FREERADIUS); + if (vp) { + vp->vp_ipv6addr = client->ipaddr.ipaddr.ip6addr; + } + + if (client->ipaddr.prefix != 128) { + vp = radius_pair_create(request->reply, + &request->reply->vps, + PW_FREERADIUS_STATS_CLIENT_NETMASK, VENDORPEC_FREERADIUS); + if (vp) { + vp->vp_integer = client->ipaddr.prefix; + } + } + } +#endif /* AF_INET6 */ + } + + if (server_ip) { + fr_pair_add(&request->reply->vps, + fr_pair_copy(request->reply, server_ip)); + } + if (server_port) { + fr_pair_add(&request->reply->vps, + fr_pair_copy(request->reply, server_port)); + } + + if ((flag->vp_integer & 0x01) != 0) { + request_stats_addvp(request, client_authvp, + &client->auth); + } +#ifdef WITH_ACCOUNTING + if ((flag->vp_integer & 0x02) != 0) { + request_stats_addvp(request, client_acctvp, + &client->acct); + } +#endif + } else { + /* + * No such client. + */ + stats_error(request, "No such client"); + } + } + + /* + * For a particular "listen" socket. + */ + if (((flag->vp_integer & 0x40) != 0) && + ((flag->vp_integer & 0x03) != 0)) { + rad_listen_t *this; + VALUE_PAIR *server_ip, *server_port; + fr_ipaddr_t ipaddr; + + /* + * See if we need to look up the server by socket + * socket. + */ + server_port = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_SERVER_PORT, VENDORPEC_FREERADIUS, TAG_ANY); + if (!server_port) return; + + server_ip = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_SERVER_IP_ADDRESS, VENDORPEC_FREERADIUS, TAG_ANY); + if (server_ip) { + ipaddr.af = AF_INET; + ipaddr.ipaddr.ip4addr.s_addr = server_ip->vp_ipaddr; +#ifdef AF_INET6 + } else if ((server_ip = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_SERVER_IPV6_ADDRESS, VENDORPEC_FREERADIUS, TAG_ANY)) != NULL) { + ipaddr.af = AF_INET6; + ipaddr.ipaddr.ip6addr = server_ip->vp_ipv6addr; +#endif /* AF_INET6 */ + } else { + stats_error(request, "No listener IP address supplied"); + } + + /* + * Not found: don't do anything + */ + this = listener_find_byipaddr(&ipaddr, server_port->vp_integer, IPPROTO_UDP); +#ifdef WITH_TCP + if (!this) this = listener_find_byipaddr(&ipaddr, server_port->vp_integer, IPPROTO_TCP); +#endif + if (!this) { + stats_error(request, "No such listener"); + return; + } + + fr_pair_add(&request->reply->vps, + fr_pair_copy(request->reply, server_ip)); + fr_pair_add(&request->reply->vps, + fr_pair_copy(request->reply, server_port)); + + if ((flag->vp_integer & 0x01) != 0) { + if ((request->listener->type == RAD_LISTEN_AUTH) || + (request->listener->type == RAD_LISTEN_NONE)) { + request_stats_addvp(request, authvp, &this->stats); + } else { + stats_error(request, "Listener is not auth"); + } + } + +#ifdef WITH_ACCOUNTING + if ((flag->vp_integer & 0x02) != 0) { + if ((request->listener->type == RAD_LISTEN_ACCT) || + (request->listener->type == RAD_LISTEN_NONE)) { + request_stats_addvp(request, acctvp, &this->stats); + } else { + stats_error(request, "Listener is not acct"); + } + } +#endif + } + +#ifdef WITH_PROXY + /* + * Home servers. + */ + if (((flag->vp_integer & 0x80) != 0) && + ((flag->vp_integer & 0x03) != 0)) { + home_server_t *home; + VALUE_PAIR *server_ip, *server_port; + fr_ipaddr_t ipaddr; + + server_port = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_SERVER_PORT, VENDORPEC_FREERADIUS, TAG_ANY); + if (!server_port) { + stats_error(request, "No home server port supplied"); + return; + } + +#ifndef NDEBUG + memset(&ipaddr, 0, sizeof(ipaddr)); +#endif + + /* + * See if we need to look up the server by socket + * socket. + */ + server_ip = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_SERVER_IP_ADDRESS, VENDORPEC_FREERADIUS, TAG_ANY); + if (server_ip) { + ipaddr.af = AF_INET; + ipaddr.prefix = 32; + ipaddr.ipaddr.ip4addr.s_addr = server_ip->vp_ipaddr; +#ifdef AF_INET6 + } else if ((server_ip = fr_pair_find_by_num(request->packet->vps, PW_FREERADIUS_STATS_SERVER_IPV6_ADDRESS, VENDORPEC_FREERADIUS, TAG_ANY)) != NULL) { + ipaddr.af = AF_INET6; + ipaddr.ipaddr.ip6addr = server_ip->vp_ipv6addr; +#endif /* AF_INET6 */ + } else { + stats_error(request, "No home server IP supplied"); + return; + } + + /* + * Not found: don't do anything + */ + home = home_server_find(&ipaddr, server_port->vp_integer, IPPROTO_UDP); +#ifdef WITH_TCP + if (!home) home = home_server_find(&ipaddr, server_port->vp_integer, IPPROTO_TCP); +#endif + if (!home) { + stats_error(request, "Failed to find home server IP"); + return; + } + + fr_pair_add(&request->reply->vps, + fr_pair_copy(request->reply, server_ip)); + fr_pair_add(&request->reply->vps, + fr_pair_copy(request->reply, server_port)); + + vp = radius_pair_create(request->reply, &request->reply->vps, + PW_FREERADIUS_STATS_SERVER_OUTSTANDING_REQUESTS, VENDORPEC_FREERADIUS); + if (vp) vp->vp_integer = home->currently_outstanding; + + vp = radius_pair_create(request->reply, &request->reply->vps, + PW_FREERADIUS_STATS_SERVER_STATE, VENDORPEC_FREERADIUS); + if (vp) vp->vp_integer = home->state; + + if ((home->state == HOME_STATE_ALIVE) && + (home->revive_time.tv_sec != 0)) { + vp = radius_pair_create(request->reply, &request->reply->vps, + PW_FREERADIUS_STATS_SERVER_TIME_OF_LIFE, VENDORPEC_FREERADIUS); + if (vp) vp->vp_date = home->revive_time.tv_sec; + } + + if ((home->state == HOME_STATE_ALIVE) && + (home->ema.window > 0)) { + vp = radius_pair_create(request->reply, + &request->reply->vps, + PW_FREERADIUS_SERVER_EMA_WINDOW, VENDORPEC_FREERADIUS); + if (vp) vp->vp_integer = home->ema.window; + vp = radius_pair_create(request->reply, + &request->reply->vps, + PW_FREERADIUS_SERVER_EMA_USEC_WINDOW_1, VENDORPEC_FREERADIUS); + if (vp) vp->vp_integer = home->ema.ema1 / EMA_SCALE; + vp = radius_pair_create(request->reply, + &request->reply->vps, + PW_FREERADIUS_SERVER_EMA_USEC_WINDOW_10, VENDORPEC_FREERADIUS); + if (vp) vp->vp_integer = home->ema.ema10 / EMA_SCALE; + + } + + if (home->state == HOME_STATE_IS_DEAD) { + vp = radius_pair_create(request->reply, &request->reply->vps, + PW_FREERADIUS_STATS_SERVER_TIME_OF_DEATH, VENDORPEC_FREERADIUS); + if (vp) vp->vp_date = home->zombie_period_start.tv_sec + home->zombie_period; + } + + /* + * Show more information... + * + * FIXME: do this for clients, too! + */ + vp = radius_pair_create(request->reply, &request->reply->vps, + PW_FREERADIUS_STATS_LAST_PACKET_RECV, VENDORPEC_FREERADIUS); + if (vp) vp->vp_date = home->last_packet_recv; + + vp = radius_pair_create(request->reply, &request->reply->vps, + PW_FREERADIUS_STATS_LAST_PACKET_SENT, VENDORPEC_FREERADIUS); + if (vp) vp->vp_date = home->last_packet_sent; + + if ((flag->vp_integer & 0x01) != 0) { + if (home->type == HOME_TYPE_AUTH) { + request_stats_addvp(request, proxy_authvp, + &home->stats); + } else { + stats_error(request, "Home server is not auth"); + } + } + +#ifdef WITH_ACCOUNTING + if ((flag->vp_integer & 0x02) != 0) { + if (home->type == HOME_TYPE_ACCT) { + request_stats_addvp(request, proxy_acctvp, + &home->stats); + } else { + stats_error(request, "Home server is not acct"); + } + } +#endif + } +#endif /* WITH_PROXY */ +} + +void radius_stats_init(int flag) +{ + if (!flag) { + gettimeofday(&start_time, NULL); + hup_time = start_time; /* it's just nicer this way */ + } else { + gettimeofday(&hup_time, NULL); + } +} + +void radius_stats_ema(fr_stats_ema_t *ema, + struct timeval *start, struct timeval *end) +{ + int micro; + time_t tdiff; +#ifdef WITH_STATS_DEBUG + static int n = 0; +#endif + if (ema->window == 0) return; + + rad_assert(start->tv_sec <= end->tv_sec); + + /* + * Initialize it. + */ + if (ema->f1 == 0) { + if (ema->window > 10000) ema->window = 10000; + + ema->f1 = (2 * F_EMA_SCALE) / (ema->window + 1); + ema->f10 = (2 * F_EMA_SCALE) / ((10 * ema->window) + 1); + } + + + tdiff = start->tv_sec; + tdiff -= end->tv_sec; + + micro = (int) tdiff; + if (micro > 40) micro = 40; /* don't overflow 32-bit ints */ + micro *= USEC; + micro += start->tv_usec; + micro -= end->tv_usec; + + micro *= EMA_SCALE; + + if (ema->ema1 == 0) { + ema->ema1 = micro; + ema->ema10 = micro; + } else { + int diff; + + diff = ema->f1 * (micro - ema->ema1); + ema->ema1 += (diff / 1000000); + + diff = ema->f10 * (micro - ema->ema10); + ema->ema10 += (diff / 1000000); + } + + +#ifdef WITH_STATS_DEBUG + DEBUG("time %d %d.%06d\t%d.%06d\t%d.%06d\n", + n, micro / PREC, (micro / EMA_SCALE) % USEC, + ema->ema1 / PREC, (ema->ema1 / EMA_SCALE) % USEC, + ema->ema10 / PREC, (ema->ema10 / EMA_SCALE) % USEC); + n++; +#endif +} + +#endif /* WITH_STATS */ diff --git a/src/main/threads.c b/src/main/threads.c new file mode 100644 index 0000000..a187106 --- /dev/null +++ b/src/main/threads.c @@ -0,0 +1,1697 @@ +/* + * threads.c request threading support + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2000,2006 The FreeRADIUS server project + * Copyright 2000 Alan DeKok <aland@ox.org> + */ + +RCSID("$Id$") +USES_APPLE_DEPRECATED_API /* OpenSSL API has been deprecated by Apple */ + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/process.h> + +#ifdef HAVE_STDATOMIC_H +#include <freeradius-devel/atomic_queue.h> +#endif + +#include <freeradius-devel/rad_assert.h> + +/* + * Other OS's have sem_init, OS X doesn't. + */ +#ifdef HAVE_SEMAPHORE_H +#include <semaphore.h> +#endif + +#ifdef __APPLE__ +#ifdef WITH_GCD +#include <dispatch/dispatch.h> +#endif +#include <mach/task.h> +#include <mach/mach_init.h> +#include <mach/semaphore.h> + +#ifndef WITH_GCD +#undef sem_t +#define sem_t semaphore_t +#undef sem_init +#define sem_init(s,p,c) semaphore_create(mach_task_self(),s,SYNC_POLICY_FIFO,c) +#undef sem_wait +#define sem_wait(s) semaphore_wait(*s) +#undef sem_post +#define sem_post(s) semaphore_signal(*s) +#endif /* WITH_GCD */ +#endif /* __APPLE__ */ + +#ifdef HAVE_SYS_WAIT_H +#include <sys/wait.h> +#endif + +#ifdef HAVE_PTHREAD_H + +#ifdef HAVE_OPENSSL_CRYPTO_H +#include <openssl/crypto.h> +#endif +#ifdef HAVE_OPENSSL_ERR_H +#include <openssl/err.h> +#endif +#ifdef HAVE_OPENSSL_EVP_H +#include <openssl/evp.h> +#endif + +#ifndef WITH_GCD +#define SEMAPHORE_LOCKED (0) + +#define THREAD_RUNNING (1) +#define THREAD_CANCELLED (2) +#define THREAD_EXITED (3) + +#define NUM_FIFOS RAD_LISTEN_MAX + +#ifndef HAVE_STDALIGN_H +#undef HAVE_STDATOMIC_H +#endif + +#ifdef HAVE_STDATOMIC_H +#define CAS_INCR(_x) do { uint32_t num; \ + num = load(_x); \ + if (cas_incr(_x, num)) break; \ + } while (true) + +#define CAS_DECR(_x) do { uint32_t num; \ + num = load(_x); \ + if (cas_decr(_x, num)) break; \ + } while (true) +#endif + +/* + * A data structure which contains the information about + * the current thread. + */ +typedef struct THREAD_HANDLE { + struct THREAD_HANDLE *prev; //!< Previous thread handle (in the linked list). + struct THREAD_HANDLE *next; //!< Next thread handle (int the linked list). + pthread_t pthread_id; //!< pthread_id. + int thread_num; //!< Server thread number, 1...number of threads. + int status; //!< Is the thread running or exited? + unsigned int request_count; //!< The number of requests that this thread has handled. + time_t timestamp; //!< When the thread started executing. + REQUEST *request; +} THREAD_HANDLE; + +#endif /* WITH_GCD */ + +#ifdef WNOHANG +typedef struct thread_fork_t { + pid_t pid; + int status; + int exited; +} thread_fork_t; +#endif + + +#ifdef WITH_STATS +typedef struct fr_pps_t { + uint32_t pps_old; + uint32_t pps_now; + uint32_t pps; + time_t time_old; +} fr_pps_t; +#endif + + +/* + * A data structure to manage the thread pool. There's no real + * need for a data structure, but it makes things conceptually + * easier. + */ +typedef struct THREAD_POOL { +#ifndef WITH_GCD + THREAD_HANDLE *head; + THREAD_HANDLE *tail; + + uint32_t total_threads; + + uint32_t max_thread_num; + uint32_t start_threads; + uint32_t max_threads; + uint32_t min_spare_threads; + uint32_t max_spare_threads; + uint32_t max_requests_per_thread; + uint32_t request_count; + time_t time_last_spawned; + uint32_t cleanup_delay; + bool stop_flag; +#endif /* WITH_GCD */ + bool spawn_flag; + +#ifdef WNOHANG + pthread_mutex_t wait_mutex; + fr_hash_table_t *waiters; +#endif + +#ifdef WITH_GCD + dispatch_queue_t queue; +#else + +#ifdef WITH_STATS + fr_pps_t pps_in, pps_out; +#ifdef WITH_ACCOUNTING + bool auto_limit_acct; +#endif +#endif + + /* + * All threads wait on this semaphore, for requests + * to enter the queue. + */ + sem_t semaphore; + + uint32_t max_queue_size; + +#ifndef HAVE_STDATOMIC_H + /* + * To ensure only one thread at a time touches the queue. + */ + pthread_mutex_t queue_mutex; + + uint32_t active_threads; /* protected by queue_mutex */ + uint32_t exited_threads; + uint32_t num_queued; + fr_fifo_t *fifo[NUM_FIFOS]; +#else + atomic_uint32_t active_threads; + atomic_uint32_t exited_threads; + fr_atomic_queue_t *queue[NUM_FIFOS]; +#endif /* STDATOMIC */ +#endif /* WITH_GCD */ +} THREAD_POOL; + +static THREAD_POOL thread_pool; +static bool pool_initialized = false; + +#ifndef WITH_GCD +static time_t last_cleaned = 0; + +static void thread_pool_manage(time_t now); +#endif + +#ifndef WITH_GCD +/* + * A mapping of configuration file names to internal integers + */ +static const CONF_PARSER thread_config[] = { + { "start_servers", FR_CONF_POINTER(PW_TYPE_INTEGER, &thread_pool.start_threads), "5" }, + { "max_servers", FR_CONF_POINTER(PW_TYPE_INTEGER, &thread_pool.max_threads), "32" }, + { "min_spare_servers", FR_CONF_POINTER(PW_TYPE_INTEGER, &thread_pool.min_spare_threads), "3" }, + { "max_spare_servers", FR_CONF_POINTER(PW_TYPE_INTEGER, &thread_pool.max_spare_threads), "10" }, + { "max_requests_per_server", FR_CONF_POINTER(PW_TYPE_INTEGER, &thread_pool.max_requests_per_thread), "0" }, + { "cleanup_delay", FR_CONF_POINTER(PW_TYPE_INTEGER, &thread_pool.cleanup_delay), "5" }, + { "max_queue_size", FR_CONF_POINTER(PW_TYPE_INTEGER, &thread_pool.max_queue_size), "65536" }, +#ifdef WITH_STATS +#ifdef WITH_ACCOUNTING + { "auto_limit_acct", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &thread_pool.auto_limit_acct), NULL }, +#endif +#endif + CONF_PARSER_TERMINATOR +}; +#endif + +#if defined(HAVE_OPENSSL_CRYPTO_H) && defined(HAVE_CRYPTO_SET_LOCKING_CALLBACK) + +/* + * If we're linking against OpenSSL, then it is the + * duty of the application, if it is multithreaded, + * to provide OpenSSL with appropriate thread id + * and mutex locking functions + * + * Note: this only implements static callbacks. + * OpenSSL does not use dynamic locking callbacks + * right now, but may in the future, so we will have + * to add them at some point. + */ +static pthread_mutex_t *ssl_mutexes = NULL; + +static void ssl_locking_function(int mode, int n, UNUSED char const *file, UNUSED int line) +{ + rad_assert(&ssl_mutexes[n] != NULL); + + if (mode & CRYPTO_LOCK) { + pthread_mutex_lock(&ssl_mutexes[n]); + } else { + pthread_mutex_unlock(&ssl_mutexes[n]); + } +} + +/* + * Create the TLS mutexes. + */ +int tls_mutexes_init(void) +{ + int i, num; + + rad_assert(ssl_mutexes == NULL); + + num = CRYPTO_num_locks(); + + ssl_mutexes = rad_malloc(num * sizeof(pthread_mutex_t)); + if (!ssl_mutexes) { + ERROR("Error allocating memory for SSL mutexes!"); + return -1; + } + + for (i = 0; i < num; i++) { + pthread_mutex_init(&ssl_mutexes[i], NULL); + } + + CRYPTO_set_locking_callback(ssl_locking_function); + + return 0; +} + +static void tls_mutexes_destroy(void) +{ +#ifdef HAVE_CRYPTO_SET_LOCKING_CALLBACK + int i, num; + + rad_assert(ssl_mutex != NULL); + + num = CRYPTO_num_locks(); + + for (i = 0; i < num; i++) { + pthread_mutex_destroy(&ssl_mutexes[i]); + } + free(ssl_mutexes); + + CRYPTO_set_locking_callback(NULL); +#endif +} +#else +#define tls_mutexes_destroy() +#endif + +#ifdef WNOHANG +/* + * We don't want to catch SIGCHLD for a host of reasons. + * + * - exec_wait means that someone, somewhere, somewhen, will + * call waitpid(), and catch the child. + * + * - SIGCHLD is delivered to a random thread, not the one that + * forked. + * + * - if another thread catches the child, we have to coordinate + * with the thread doing the waiting. + * + * - if we don't waitpid() for non-wait children, they'll be zombies, + * and will hang around forever. + * + */ +static void reap_children(void) +{ + pid_t pid; + int status; + thread_fork_t mytf, *tf; + + + pthread_mutex_lock(&thread_pool.wait_mutex); + + do { + retry: + pid = waitpid(0, &status, WNOHANG); + if (pid <= 0) break; + + mytf.pid = pid; + tf = fr_hash_table_finddata(thread_pool.waiters, &mytf); + if (!tf) goto retry; + + tf->status = status; + tf->exited = 1; + } while (fr_hash_table_num_elements(thread_pool.waiters) > 0); + + pthread_mutex_unlock(&thread_pool.wait_mutex); +} +#else +#define reap_children() +#endif /* WNOHANG */ + +#ifndef WITH_GCD +/* + * Add a request to the list of waiting requests. + * This function gets called ONLY from the main handler thread... + * + * This function should never fail. + */ +int request_enqueue(REQUEST *request) +{ + bool managed = false; + + rad_assert(pool_initialized == true); + + /* + * If we haven't checked the number of child threads + * in a while, OR if the thread pool appears to be full, + * go manage it. + */ + if (last_cleaned < request->timestamp) { + thread_pool_manage(request->timestamp); + managed = true; + } + +#ifdef HAVE_STDATOMIC_H + if (!managed) { + uint32_t num; + + num = load(thread_pool.active_threads); + if (num == thread_pool.total_threads) { + thread_pool_manage(request->timestamp); + managed = true; + } + + if (!managed) { + num = load(thread_pool.exited_threads); + if (num > 0) { + thread_pool_manage(request->timestamp); + } + } + } + + /* + * Use atomic queues where possible. They're substantially faster than mutexes. + */ + request->component = "<core>"; + request->module = "<queue>"; + request->child_state = REQUEST_QUEUED; + + /* + * Push the request onto the appropriate fifo for that + */ + if (!fr_atomic_queue_push(thread_pool.queue[request->priority], request)) { + ERROR("!!! ERROR !!! Failed inserting request %d into the queue", request->number); + return 0; + } + +#else /* no atomic queues */ + + if (!managed && + ((thread_pool.active_threads == thread_pool.total_threads) || + (thread_pool.exited_threads > 0))) { + thread_pool_manage(request->timestamp); + } + + pthread_mutex_lock(&thread_pool.queue_mutex); + +#ifdef WITH_STATS +#ifdef WITH_ACCOUNTING + if (thread_pool.auto_limit_acct) { + struct timeval now; + + /* + * Throw away accounting requests if we're too + * busy. The NAS should retransmit these, and no + * one should notice. + * + * In contrast, we always try to process + * authentication requests. Those are more time + * critical, and it's harder to determine which + * we can throw away, and which we can keep. + * + * We allow the queue to get half full before we + * start worrying. Even then, we still require + * that the rate of input packets is higher than + * the rate of outgoing packets. i.e. the queue + * is growing. + * + * Once that happens, we roll a dice to see where + * the barrier is for "keep" versus "toss". If + * the queue is smaller than the barrier, we + * allow it. If the queue is larger than the + * barrier, we throw the packet away. Otherwise, + * we keep it. + * + * i.e. the probability of throwing the packet + * away increases from 0 (queue is half full), to + * 100 percent (queue is completely full). + * + * A probabilistic approach allows us to process + * SOME of the new accounting packets. + */ + if ((request->packet->code == PW_CODE_ACCOUNTING_REQUEST) && + (thread_pool.num_queued > (thread_pool.max_queue_size / 2)) && + (thread_pool.pps_in.pps_now > thread_pool.pps_out.pps_now)) { + uint32_t prob; + uint32_t keep; + + /* + * Take a random value of how full we + * want the queue to be. It's OK to be + * half full, but we get excited over + * anything more than that. + */ + keep = (thread_pool.max_queue_size / 2); + prob = fr_rand() & ((1 << 10) - 1); + keep *= prob; + keep >>= 10; + keep += (thread_pool.max_queue_size / 2); + + /* + * If the queue is larger than our dice + * roll, we throw the packet away. + */ + if (thread_pool.num_queued > keep) { + pthread_mutex_unlock(&thread_pool.queue_mutex); + return 0; + } + } + + gettimeofday(&now, NULL); + + /* + * Calculate the instantaneous arrival rate into + * the queue. + */ + thread_pool.pps_in.pps = rad_pps(&thread_pool.pps_in.pps_old, + &thread_pool.pps_in.pps_now, + &thread_pool.pps_in.time_old, + &now); + + thread_pool.pps_in.pps_now++; + } +#endif /* WITH_ACCOUNTING */ +#endif + + thread_pool.request_count++; + + if (thread_pool.num_queued >= thread_pool.max_queue_size) { + pthread_mutex_unlock(&thread_pool.queue_mutex); + + /* + * Mark the request as done. + */ + RATE_LIMIT(ERROR("Something is blocking the server. There are %d packets in the queue, " + "waiting to be processed. Ignoring the new request.", thread_pool.num_queued)); + return 0; + } + + request->component = "<core>"; + request->module = "<queue>"; + request->child_state = REQUEST_QUEUED; + + /* + * Push the request onto the appropriate fifo for that + */ + if (!fr_fifo_push(thread_pool.fifo[request->priority], request)) { + pthread_mutex_unlock(&thread_pool.queue_mutex); + ERROR("!!! ERROR !!! Failed inserting request %d into the queue", request->number); + return 0; + } + + thread_pool.num_queued++; + + pthread_mutex_unlock(&thread_pool.queue_mutex); +#endif + + /* + * There's one more request in the queue. + * + * Note that we're not touching the queue any more, so + * the semaphore post is outside of the mutex. This also + * means that when the thread wakes up and tries to lock + * the mutex, it will be unlocked, and there won't be + * contention. + */ + sem_post(&thread_pool.semaphore); + + return 1; +} + +/* + * Remove a request from the queue. + */ +static int request_dequeue(REQUEST **prequest) +{ + time_t blocked; + static time_t last_complained = 0; + static time_t total_blocked = 0; + int num_blocked = 0; +#ifndef HAVE_STDATOMIC_H + RAD_LISTEN_TYPE start; +#endif + RAD_LISTEN_TYPE i; + REQUEST *request = NULL; + reap_children(); + + rad_assert(pool_initialized == true); + +#ifdef HAVE_STDATOMIC_H +retry: + for (i = 0; i < NUM_FIFOS; i++) { + if (!fr_atomic_queue_pop(thread_pool.queue[i], (void **) &request)) continue; + + rad_assert(request != NULL); + + VERIFY_REQUEST(request); + + if (request->master_state != REQUEST_STOP_PROCESSING) { + break; + } + + /* + * This entry was marked to be stopped. Acknowledge it. + */ + request->child_state = REQUEST_DONE; + } + + /* + * Popping might fail. If so, return. + */ + if (!request) return 0; + +#else + pthread_mutex_lock(&thread_pool.queue_mutex); + +#ifdef WITH_STATS +#ifdef WITH_ACCOUNTING + if (thread_pool.auto_limit_acct) { + struct timeval now; + + gettimeofday(&now, NULL); + + /* + * Calculate the instantaneous departure rate + * from the queue. + */ + thread_pool.pps_out.pps = rad_pps(&thread_pool.pps_out.pps_old, + &thread_pool.pps_out.pps_now, + &thread_pool.pps_out.time_old, + &now); + thread_pool.pps_out.pps_now++; + } +#endif +#endif + + /* + * Clear old requests from all queues. + * + * We only do one pass over the queue, in order to + * amortize the work across the child threads. Since we + * do N checks for one request de-queued, the old + * requests will be quickly cleared. + */ + for (i = 0; i < NUM_FIFOS; i++) { + request = fr_fifo_peek(thread_pool.fifo[i]); + if (!request) continue; + + VERIFY_REQUEST(request); + + if (request->master_state != REQUEST_STOP_PROCESSING) { + continue; + } + + /* + * This entry was marked to be stopped. Acknowledge it. + */ + request = fr_fifo_pop(thread_pool.fifo[i]); + rad_assert(request != NULL); + VERIFY_REQUEST(request); + request->child_state = REQUEST_DONE; + thread_pool.num_queued--; + } + + start = 0; + retry: + /* + * Pop results from the top of the queue + */ + for (i = start; i < NUM_FIFOS; i++) { + request = fr_fifo_pop(thread_pool.fifo[i]); + if (request) { + VERIFY_REQUEST(request); + start = i; + break; + } + } + + if (!request) { + pthread_mutex_unlock(&thread_pool.queue_mutex); + *prequest = NULL; + return 0; + } + + rad_assert(thread_pool.num_queued > 0); + thread_pool.num_queued--; +#endif /* HAVE_STD_ATOMIC_H */ + + *prequest = request; + + rad_assert(*prequest != NULL); + rad_assert(request->magic == REQUEST_MAGIC); + + request->component = "<core>"; + request->module = ""; + request->child_state = REQUEST_RUNNING; + + /* + * If the request has sat in the queue for too long, + * kill it. + * + * The main clean-up code can't delete the request from + * the queue, and therefore won't clean it up until we + * have acknowledged it as "done". + */ + if (request->master_state == REQUEST_STOP_PROCESSING) { + request->module = "<done>"; + request->child_state = REQUEST_DONE; + goto retry; + } + + /* + * The thread is currently processing a request. + */ +#ifdef HAVE_STDATOMIC_H + CAS_INCR(thread_pool.active_threads); +#else + thread_pool.active_threads++; +#endif + + blocked = time(NULL); + if (!request->proxy && (blocked - request->timestamp) > 5) { + total_blocked++; + if (last_complained < blocked) { + last_complained = blocked; + blocked -= request->timestamp; + num_blocked = total_blocked; + } else { + blocked = 0; + } + } else { + total_blocked = 0; + blocked = 0; + } + +#ifndef HAVE_STDATOMIC_H + pthread_mutex_unlock(&thread_pool.queue_mutex); +#endif + + if (blocked) { + ERROR("%d requests have been waiting in the processing queue for %d seconds. Check that all databases are running properly!", + num_blocked, (int) blocked); + } + + return 1; +} + + +/* + * The main thread handler for requests. + * + * Wait on the semaphore until we have it, and process the request. + */ +static void *request_handler_thread(void *arg) +{ + THREAD_HANDLE *self = (THREAD_HANDLE *) arg; + + /* + * Loop forever, until told to exit. + */ + do { + /* + * Wait to be signalled. + */ + DEBUG2("Thread %d waiting to be assigned a request", + self->thread_num); + re_wait: + if (sem_wait(&thread_pool.semaphore) != 0) { + /* + * Interrupted system call. Go back to + * waiting, but DON'T print out any more + * text. + */ + if ((errno == EINTR) || (errno == EAGAIN)) { + DEBUG2("Re-wait %d", self->thread_num); + goto re_wait; + } + ERROR("Thread %d failed waiting for semaphore: %s: Exiting\n", + self->thread_num, fr_syserror(errno)); + break; + } + + DEBUG2("Thread %d got semaphore", self->thread_num); + +#ifdef HAVE_OPENSSL_ERR_H + /* + * Clear the error queue for the current thread. + */ + ERR_clear_error(); +#endif + + /* + * The server is exiting. Don't dequeue any + * requests. + */ + if (thread_pool.stop_flag) break; + + /* + * Try to grab a request from the queue. + * + * It may be empty, in which case we fail + * gracefully. + */ + if (!request_dequeue(&self->request)) continue; + + self->request->child_pid = self->pthread_id; + self->request_count++; + + DEBUG2("Thread %d handling request %d, (%d handled so far)", + self->thread_num, self->request->number, + self->request_count); + +#ifndef HAVE_STDATOMIC_H +#ifdef WITH_ACCOUNTING + if ((self->request->packet->code == PW_CODE_ACCOUNTING_REQUEST) && + thread_pool.auto_limit_acct) { + VALUE_PAIR *vp; + REQUEST *request = self->request; + + vp = radius_pair_create(request, &request->config, + 181, VENDORPEC_FREERADIUS); + if (vp) vp->vp_integer = thread_pool.pps_in.pps; + + vp = radius_pair_create(request, &request->config, + 182, VENDORPEC_FREERADIUS); + if (vp) vp->vp_integer = thread_pool.pps_in.pps; + + vp = radius_pair_create(request, &request->config, + 183, VENDORPEC_FREERADIUS); + if (vp) { + vp->vp_integer = thread_pool.max_queue_size - thread_pool.num_queued; + vp->vp_integer *= 100; + vp->vp_integer /= thread_pool.max_queue_size; + } + } +#endif +#endif + + self->request->process(self->request, FR_ACTION_RUN); + self->request = NULL; + +#ifdef HAVE_STDATOMIC_H + CAS_DECR(thread_pool.active_threads); +#else + /* + * Update the active threads. + */ + pthread_mutex_lock(&thread_pool.queue_mutex); + rad_assert(thread_pool.active_threads > 0); + thread_pool.active_threads--; + pthread_mutex_unlock(&thread_pool.queue_mutex); +#endif + + /* + * If the thread has handled too many requests, then make it + * exit. + */ + if ((thread_pool.max_requests_per_thread > 0) && + (self->request_count >= thread_pool.max_requests_per_thread)) { + DEBUG2("Thread %d handled too many requests", + self->thread_num); + break; + } + } while (self->status != THREAD_CANCELLED); + + DEBUG2("Thread %d exiting...", self->thread_num); + +#ifdef HAVE_OPENSSL_ERR_H + /* + * If we linked with OpenSSL, the application + * must remove the thread's error queue before + * exiting to prevent memory leaks. + */ +#if OPENSSL_VERSION_NUMBER < 0x10000000L + ERR_remove_state(0); +#elif OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER) + ERR_remove_thread_state(NULL); +#endif +#endif + +#ifdef HAVE_STDATOMIC_H + CAS_INCR(thread_pool.exited_threads); +#else + pthread_mutex_lock(&thread_pool.queue_mutex); + thread_pool.exited_threads++; + pthread_mutex_unlock(&thread_pool.queue_mutex); +#endif + + /* + * Do this as the LAST thing before exiting. + */ + self->request = NULL; + self->status = THREAD_EXITED; + exec_trigger(NULL, NULL, "server.thread.stop", true); + + return NULL; +} + +/* + * Take a THREAD_HANDLE, delete it from the thread pool and + * free its resources. + * + * This function is called ONLY from the main server thread, + * ONLY after the thread has exited. + */ +static void delete_thread(THREAD_HANDLE *handle) +{ + THREAD_HANDLE *prev; + THREAD_HANDLE *next; + + rad_assert(handle->request == NULL); + + DEBUG2("Deleting thread %d", handle->thread_num); + + prev = handle->prev; + next = handle->next; + rad_assert(thread_pool.total_threads > 0); + thread_pool.total_threads--; + + /* + * Remove the handle from the list. + */ + if (prev == NULL) { + rad_assert(thread_pool.head == handle); + thread_pool.head = next; + } else { + prev->next = next; + } + + if (next == NULL) { + rad_assert(thread_pool.tail == handle); + thread_pool.tail = prev; + } else { + next->prev = prev; + } + + /* + * Free the handle, now that it's no longer referencable. + */ + free(handle); +} + + +/* + * Spawn a new thread, and place it in the thread pool. + * + * The thread is started initially in the blocked state, waiting + * for the semaphore. + */ +static THREAD_HANDLE *spawn_thread(time_t now, int do_trigger) +{ + int rcode; + THREAD_HANDLE *handle; + + /* + * Ensure that we don't spawn too many threads. + */ + if (thread_pool.total_threads >= thread_pool.max_threads) { + DEBUG2("Thread spawn failed. Maximum number of threads (%d) already running.", thread_pool.max_threads); + return NULL; + } + + /* + * Allocate a new thread handle. + */ + handle = (THREAD_HANDLE *) rad_malloc(sizeof(THREAD_HANDLE)); + memset(handle, 0, sizeof(THREAD_HANDLE)); + handle->prev = NULL; + handle->next = NULL; + handle->thread_num = thread_pool.max_thread_num++; + handle->request_count = 0; + handle->status = THREAD_RUNNING; + handle->timestamp = time(NULL); + + /* + * Create the thread joinable, so that it can be cleaned up + * using pthread_join(). + * + * Note that the function returns non-zero on error, NOT + * -1. The return code is the error, and errno isn't set. + */ + rcode = pthread_create(&handle->pthread_id, 0, request_handler_thread, handle); + if (rcode != 0) { + free(handle); + ERROR("Thread create failed: %s", + fr_syserror(rcode)); + return NULL; + } + + /* + * One more thread to go into the list. + */ + thread_pool.total_threads++; + DEBUG2("Thread spawned new child %d. Total threads in pool: %d", + handle->thread_num, thread_pool.total_threads); + if (do_trigger) exec_trigger(NULL, NULL, "server.thread.start", true); + + /* + * Add the thread handle to the tail of the thread pool list. + */ + if (thread_pool.tail) { + thread_pool.tail->next = handle; + handle->prev = thread_pool.tail; + thread_pool.tail = handle; + } else { + rad_assert(thread_pool.head == NULL); + thread_pool.head = thread_pool.tail = handle; + } + + /* + * Update the time we last spawned a thread. + */ + thread_pool.time_last_spawned = now; + + /* + * Fire trigger if maximum number of threads reached + */ + if (thread_pool.total_threads >= thread_pool.max_threads) + exec_trigger(NULL, NULL, "server.thread.max_threads", true); + + /* + * And return the new handle to the caller. + */ + return handle; +} +#endif /* WITH_GCD */ + + +#ifdef WNOHANG +static uint32_t pid_hash(void const *data) +{ + thread_fork_t const *tf = data; + + return fr_hash(&tf->pid, sizeof(tf->pid)); +} + +static int pid_cmp(void const *one, void const *two) +{ + thread_fork_t const *a = one; + thread_fork_t const *b = two; + + return (a->pid - b->pid); +} +#endif + +/* + * Allocate the thread pool, and seed it with an initial number + * of threads. + * + * FIXME: What to do on a SIGHUP??? + */ +DIAG_OFF(deprecated-declarations) +int thread_pool_init(CONF_SECTION *cs, bool *spawn_flag) +{ +#ifndef WITH_GCD + uint32_t i; + int rcode; +#endif + CONF_SECTION *pool_cf; + time_t now; +#ifdef HAVE_STDATOMIC_H + int num; + TALLOC_CTX *autofree; + + autofree = talloc_autofree_context(); +#endif + + now = time(NULL); + + rad_assert(spawn_flag != NULL); + rad_assert(*spawn_flag == true); + rad_assert(pool_initialized == false); /* not called on HUP */ + + pool_cf = cf_subsection_find_next(cs, NULL, "thread"); +#ifdef WITH_GCD + if (pool_cf) WARN("Built with Grand Central Dispatch. Ignoring 'thread' subsection"); +#else + if (!pool_cf) *spawn_flag = false; +#endif + + /* + * Initialize the thread pool to some reasonable values. + */ + memset(&thread_pool, 0, sizeof(THREAD_POOL)); +#ifndef WITH_GCD + thread_pool.head = NULL; + thread_pool.tail = NULL; + thread_pool.total_threads = 0; + thread_pool.max_thread_num = 1; + thread_pool.cleanup_delay = 5; + thread_pool.stop_flag = false; +#endif + thread_pool.spawn_flag = *spawn_flag; + + /* + * Don't bother initializing the mutexes or + * creating the hash tables. They won't be used. + */ + if (!*spawn_flag) return 0; + +#ifdef WNOHANG + if ((pthread_mutex_init(&thread_pool.wait_mutex,NULL) != 0)) { + ERROR("FATAL: Failed to initialize wait mutex: %s", + fr_syserror(errno)); + return -1; + } + + /* + * Create the hash table of child PID's + */ + thread_pool.waiters = fr_hash_table_create(pid_hash, + pid_cmp, + free); + if (!thread_pool.waiters) { + ERROR("FATAL: Failed to set up wait hash"); + return -1; + } +#endif + +#ifndef WITH_GCD + if (cf_section_parse(pool_cf, NULL, thread_config) < 0) { + return -1; + } + + /* + * Catch corner cases. + */ + if (thread_pool.min_spare_threads < 1) + thread_pool.min_spare_threads = 1; + if (thread_pool.max_spare_threads < 1) + thread_pool.max_spare_threads = 1; + if (thread_pool.max_spare_threads < thread_pool.min_spare_threads) + thread_pool.max_spare_threads = thread_pool.min_spare_threads; + if (thread_pool.max_threads == 0) + thread_pool.max_threads = 256; + if ((thread_pool.max_queue_size < 2) || (thread_pool.max_queue_size > 1024*1024)) { + ERROR("FATAL: max_queue_size value must be in range 2-1048576"); + return -1; + } + + if (thread_pool.start_threads > thread_pool.max_threads) { + ERROR("FATAL: start_servers (%i) must be <= max_servers (%i)", + thread_pool.start_threads, thread_pool.max_threads); + return -1; + } +#endif /* WITH_GCD */ + + /* + * The pool has already been initialized. Don't spawn + * new threads, and don't forget about forked children. + */ + if (pool_initialized) { + return 0; + } + +#ifndef WITH_GCD + /* + * Initialize the queue of requests. + */ + memset(&thread_pool.semaphore, 0, sizeof(thread_pool.semaphore)); + rcode = sem_init(&thread_pool.semaphore, 0, SEMAPHORE_LOCKED); + if (rcode != 0) { + ERROR("FATAL: Failed to initialize semaphore: %s", + fr_syserror(errno)); + return -1; + } + +#ifndef HAVE_STDATOMIC_H + rcode = pthread_mutex_init(&thread_pool.queue_mutex,NULL); + if (rcode != 0) { + ERROR("FATAL: Failed to initialize queue mutex: %s", + fr_syserror(errno)); + return -1; + } +#else + num = 0; + store(thread_pool.active_threads, num); + store(thread_pool.exited_threads, num); +#endif + + /* + * Allocate multiple fifos. + */ + for (i = 0; i < NUM_FIFOS; i++) { +#ifdef HAVE_STDATOMIC_H + thread_pool.queue[i] = fr_atomic_queue_alloc(autofree, thread_pool.max_queue_size); + if (!thread_pool.queue[i]) { + ERROR("FATAL: Failed to set up request fifo"); + return -1; + } +#else + thread_pool.fifo[i] = fr_fifo_create(NULL, thread_pool.max_queue_size, NULL); + if (!thread_pool.fifo[i]) { + ERROR("FATAL: Failed to set up request fifo"); + return -1; + } +#endif + } +#endif + +#ifndef WITH_GCD + /* + * Create a number of waiting threads. + * + * If we fail while creating them, do something intelligent. + */ + for (i = 0; i < thread_pool.start_threads; i++) { + if (spawn_thread(now, 0) == NULL) { + return -1; + } + } +#else + thread_pool.queue = dispatch_queue_create("org.freeradius.threads", NULL); + if (!thread_pool.queue) { + ERROR("Failed creating dispatch queue: %s", fr_syserror(errno)); + fr_exit(1); + } +#endif + + DEBUG2("Thread pool initialized"); + pool_initialized = true; + return 0; +} +DIAG_ON(deprecated-declarations) + +/* + * Stop all threads in the pool. + */ +void thread_pool_stop(void) +{ +#ifndef WITH_GCD + int i; + int total_threads; + THREAD_HANDLE *handle; + THREAD_HANDLE *next; + + if (!pool_initialized) return; + + /* + * Set pool stop flag. + */ + thread_pool.stop_flag = true; + + /* + * Wakeup all threads to make them see stop flag. + */ + total_threads = thread_pool.total_threads; + for (i = 0; i != total_threads; i++) { + sem_post(&thread_pool.semaphore); + } + + /* + * Join and free all threads. + */ + for (handle = thread_pool.head; handle; handle = next) { + next = handle->next; + pthread_join(handle->pthread_id, NULL); + delete_thread(handle); + } + + for (i = 0; i < NUM_FIFOS; i++) { +#ifdef HAVE_STDATOMIC_H + fr_atomic_queue_free(&thread_pool.queue[i]); +#else + fr_fifo_free(thread_pool.fifo[i]); +#endif + } + +#ifdef WNOHANG + fr_hash_table_free(thread_pool.waiters); +#endif + + /* + * We're no longer threaded. Remove the mutexes and free + * the memory. + */ + tls_mutexes_destroy(); +#endif +} + + +#ifdef WITH_GCD +int request_enqueue(REQUEST *request) +{ + dispatch_block_t block; + + block = ^{ + request->process(request, FR_ACTION_RUN); + }; + + dispatch_async(thread_pool.queue, block); + + return 1; +} +#endif + +#ifndef WITH_GCD +/* + * Check the min_spare_threads and max_spare_threads. + * + * If there are too many or too few threads waiting, then we + * either create some more, or delete some. + */ +static void thread_pool_manage(time_t now) +{ + uint32_t spare; + int i, total; + THREAD_HANDLE *handle, *next; + uint32_t active_threads; + + /* + * Loop over the thread pool, deleting exited threads. + */ + for (handle = thread_pool.head; handle; handle = next) { + next = handle->next; + + /* + * Maybe we've asked the thread to exit, and it + * has agreed. + */ + if (handle->status == THREAD_EXITED) { + pthread_join(handle->pthread_id, NULL); + delete_thread(handle); + +#ifdef HAVE_STDATOMIC_H + CAS_DECR(thread_pool.exited_threads); +#else + pthread_mutex_lock(&thread_pool.queue_mutex); + thread_pool.exited_threads--; + pthread_mutex_unlock(&thread_pool.queue_mutex); +#endif + } + } + + /* + * We don't need a mutex lock here, as we're reading + * active_threads, and not modifying it. We want a close + * approximation of the number of active threads, and this + * is good enough. + */ +#ifdef HAVE_STDATOMIC_H + active_threads = load(thread_pool.active_threads); +#else + active_threads = thread_pool.active_threads; +#endif + spare = thread_pool.total_threads - active_threads; + if (rad_debug_lvl) { + static uint32_t old_total = 0; + static uint32_t old_active = 0; + + if ((old_total != thread_pool.total_threads) || (old_active != active_threads)) { + DEBUG2("Threads: total/active/spare threads = %d/%d/%d", + thread_pool.total_threads, active_threads, spare); + old_total = thread_pool.total_threads; + old_active = active_threads; + } + } + + /* + * If there are too few spare threads. Go create some more. + */ + if ((thread_pool.total_threads < thread_pool.max_threads) && + (spare < thread_pool.min_spare_threads)) { + total = thread_pool.min_spare_threads - spare; + + if ((total + thread_pool.total_threads) > thread_pool.max_threads) { + total = thread_pool.max_threads - thread_pool.total_threads; + } + + DEBUG2("Threads: Spawning %d spares", total); + + /* + * Create a number of spare threads. + */ + for (i = 0; i < total; i++) { + handle = spawn_thread(now, 1); + if (handle == NULL) { + return; + } + } + + return; /* there aren't too many spare threads */ + } + + /* + * Only delete spare threads if we haven't already done + * so this second. + */ + if (now == last_cleaned) { + return; + } + last_cleaned = now; + + /* + * Only delete the spare threads if sufficient time has + * passed since we last created one. This helps to minimize + * the amount of create/delete cycles. + */ + if ((now - thread_pool.time_last_spawned) < (int)thread_pool.cleanup_delay) { + return; + } + + /* + * If there are too many spare threads, delete one. + * + * Note that we only delete ONE at a time, instead of + * wiping out many. This allows the excess servers to + * be slowly reaped, just in case the load spike comes again. + */ + if (spare > thread_pool.max_spare_threads) { + + spare -= thread_pool.max_spare_threads; + + DEBUG2("Threads: deleting 1 spare out of %d spares", spare); + + /* + * Walk through the thread pool, deleting the + * first idle thread we come across. + */ + for (handle = thread_pool.head; (handle != NULL) && (spare > 0) ; handle = next) { + next = handle->next; + + /* + * If the thread is not handling a + * request, but still live, then tell it + * to exit. + * + * It will eventually wake up, and realize + * it's been told to commit suicide. + */ + if ((handle->request == NULL) && + (handle->status == THREAD_RUNNING)) { + handle->status = THREAD_CANCELLED; + /* + * Post an extra semaphore, as a + * signal to wake up, and exit. + */ + sem_post(&thread_pool.semaphore); + spare--; + break; + } + } + } + + /* + * Otherwise everything's kosher. There are not too few, + * or too many spare threads. Exit happily. + */ + return; +} +#endif /* WITH_GCD */ + +#ifdef WNOHANG +/* + * Thread wrapper for fork(). + */ +pid_t rad_fork(void) +{ + pid_t child_pid; + + if (!pool_initialized) return fork(); + + reap_children(); /* be nice to non-wait thingies */ + + if (fr_hash_table_num_elements(thread_pool.waiters) >= 1024) { + return -1; + } + + /* + * Fork & save the PID for later reaping. + */ + child_pid = fork(); + if (child_pid > 0) { + int rcode; + thread_fork_t *tf; + + tf = rad_malloc(sizeof(*tf)); + memset(tf, 0, sizeof(*tf)); + + tf->pid = child_pid; + + pthread_mutex_lock(&thread_pool.wait_mutex); + rcode = fr_hash_table_insert(thread_pool.waiters, tf); + pthread_mutex_unlock(&thread_pool.wait_mutex); + + if (!rcode) { + ERROR("Failed to store PID, creating what will be a zombie process %d", + (int) child_pid); + free(tf); + } + } + + /* + * Return whatever we were told. + */ + return child_pid; +} + + +/* + * Wait 10 seconds at most for a child to exit, then give up. + */ +pid_t rad_waitpid(pid_t pid, int *status) +{ + int i; + thread_fork_t mytf, *tf; + + if (!pool_initialized) return waitpid(pid, status, 0); + + if (pid <= 0) return -1; + + mytf.pid = pid; + + pthread_mutex_lock(&thread_pool.wait_mutex); + tf = fr_hash_table_finddata(thread_pool.waiters, &mytf); + pthread_mutex_unlock(&thread_pool.wait_mutex); + + if (!tf) return -1; + + for (i = 0; i < 100; i++) { + reap_children(); + + if (tf->exited) { + *status = tf->status; + + pthread_mutex_lock(&thread_pool.wait_mutex); + fr_hash_table_delete(thread_pool.waiters, &mytf); + pthread_mutex_unlock(&thread_pool.wait_mutex); + return pid; + } + usleep(100000); /* sleep for 1/10 of a second */ + } + + /* + * 10 seconds have passed, give up on the child. + */ + pthread_mutex_lock(&thread_pool.wait_mutex); + fr_hash_table_delete(thread_pool.waiters, &mytf); + pthread_mutex_unlock(&thread_pool.wait_mutex); + + return 0; +} +#else +/* + * No rad_fork or rad_waitpid + */ +#endif + +void thread_pool_queue_stats(int array[RAD_LISTEN_MAX], int pps[2]) +{ + int i; + +#ifndef WITH_GCD + if (pool_initialized) { + struct timeval now; + + for (i = 0; i < RAD_LISTEN_MAX; i++) { +#ifndef HAVE_STDATOMIC_H + array[i] = fr_fifo_num_elements(thread_pool.fifo[i]); +#else + array[i] = 0; +#endif + } + + gettimeofday(&now, NULL); + + pps[0] = rad_pps(&thread_pool.pps_in.pps_old, + &thread_pool.pps_in.pps_now, + &thread_pool.pps_in.time_old, + &now); + pps[1] = rad_pps(&thread_pool.pps_out.pps_old, + &thread_pool.pps_out.pps_now, + &thread_pool.pps_out.time_old, + &now); + + } else +#endif /* WITH_GCD */ + { + for (i = 0; i < RAD_LISTEN_MAX; i++) { + array[i] = 0; + } + + pps[0] = pps[1] = 0; + } +} + +void thread_pool_thread_stats(int stats[3]) +{ +#ifndef WITH_GCD + if (pool_initialized) { + /* + * We don't need a mutex lock here as we only want to + * read a close approximation of the number of active + * threads, and not modify it. + */ +#ifdef HAVE_STDATOMIC_H + stats[0] = load(thread_pool.active_threads); +#else + stats[0] = thread_pool.active_threads; +#endif + stats[1] = thread_pool.total_threads; + stats[2] = thread_pool.max_threads; + } else +#endif /* WITH_GCD */ + { + stats[0] = stats[1] = stats[2] = 0; + } +} +#endif /* HAVE_PTHREAD_H */ + +static void time_free(void *data) +{ + free(data); +} + +void exec_trigger(REQUEST *request, CONF_SECTION *cs, char const *name, int quench) +{ + CONF_SECTION *subcs; + CONF_ITEM *ci; + CONF_PAIR *cp; + char const *attr; + char const *value; + VALUE_PAIR *vp; + bool alloc = false; + + /* + * Use global "trigger" section if no local config is given. + */ + if (!cs) { + cs = main_config.config; + attr = name; + } else { + /* + * Try to use pair name, rather than reference. + */ + attr = strrchr(name, '.'); + if (attr) { + attr++; + } else { + attr = name; + } + } + + /* + * Find local "trigger" subsection. If it isn't found, + * try using the global "trigger" section, and reset the + * reference to the full path, rather than the sub-path. + */ + subcs = cf_section_sub_find(cs, "trigger"); + if (!subcs && (cs != main_config.config)) { + subcs = cf_section_sub_find(main_config.config, "trigger"); + attr = name; + } + + if (!subcs) return; + + ci = cf_reference_item(subcs, main_config.config, attr); + if (!ci) { + ERROR("No such item in trigger section: %s", attr); + return; + } + + if (!cf_item_is_pair(ci)) { + ERROR("Trigger is not a configuration variable: %s", attr); + return; + } + + cp = cf_item_to_pair(ci); + if (!cp) return; + + value = cf_pair_value(cp); + if (!value) { + ERROR("Trigger has no value: %s", name); + return; + } + + /* + * May be called for Status-Server packets. + */ + vp = NULL; + if (request && request->packet) vp = request->packet->vps; + + /* + * Perform periodic quenching. + */ + if (quench) { + time_t *last_time; + + last_time = cf_data_find(cs, value); + if (!last_time) { + last_time = rad_malloc(sizeof(*last_time)); + *last_time = 0; + + if (cf_data_add(cs, value, last_time, time_free) < 0) { + free(last_time); + last_time = NULL; + } + } + + /* + * Send the quenched traps at most once per second. + */ + if (last_time) { + time_t now = time(NULL); + if (*last_time == now) return; + + *last_time = now; + } + } + + /* + * radius_exec_program always needs a request. + */ + if (!request) { + request = request_alloc(NULL); + alloc = true; + } + + DEBUG("Trigger %s -> %s", name, value); + + radius_exec_program(request, NULL, 0, NULL, request, value, vp, false, true, 0); + + if (alloc) talloc_free(request); +} diff --git a/src/main/tls.c b/src/main/tls.c new file mode 100644 index 0000000..c8cae3b --- /dev/null +++ b/src/main/tls.c @@ -0,0 +1,5420 @@ +/* + * tls.c + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2001 hereUare Communications, Inc. <raghud@hereuare.com> + * Copyright 2003 Alan DeKok <aland@freeradius.org> + * Copyright 2006 The FreeRADIUS server project + */ + +RCSID("$Id$") +USES_APPLE_DEPRECATED_API /* OpenSSL API has been deprecated by Apple */ + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/process.h> +#include <freeradius-devel/modules.h> +#include <freeradius-devel/rad_assert.h> + +#ifdef HAVE_SYS_STAT_H +#include <sys/stat.h> +#endif + +#ifdef HAVE_FCNTL_H +#include <fcntl.h> +#endif + +#ifdef HAVE_DIRENT_H +#include <dirent.h> +#endif + +#ifdef HAVE_UTIME_H +#include <utime.h> +#endif +#include <ctype.h> + +#ifdef WITH_TLS +# ifdef HAVE_OPENSSL_RAND_H +# include <openssl/rand.h> +# endif + +# ifdef HAVE_OPENSSL_OCSP_H +# include <openssl/ocsp.h> +# endif + +# ifdef HAVE_OPENSSL_EVP_H +# include <openssl/evp.h> +# endif +# include <openssl/ssl.h> + +#if OPENSSL_VERSION_NUMBER >= 0x30000000L +# include <openssl/provider.h> + +static OSSL_PROVIDER *openssl_default_provider = NULL; +static OSSL_PROVIDER *openssl_legacy_provider = NULL; +#endif + +#define LOG_PREFIX "tls" + +#if OPENSSL_VERSION_NUMBER >= 0x30000000L +#define ERR_get_error_line(_file, _line) ERR_get_error_all(_file, _line, NULL, NULL, NULL) + +#define FIPS_mode(_x) EVP_default_properties_is_fips_enabled(NULL) +#define PEM_read_bio_DHparams(_bio, _x, _y, _z) PEM_read_bio_Parameters(_bio, &dh) +#define SSL_CTX_set0_tmp_dh_pkey(_ctx, _dh) SSL_CTX_set_tmp_dh(_ctx, _dh) +#define DH EVP_PKEY +#define DH_free(_dh) +#endif + +#ifdef ENABLE_OPENSSL_VERSION_CHECK +typedef struct libssl_defect { + uint64_t high; + uint64_t low; + + char const *id; + char const *name; + char const *comment; +} libssl_defect_t; + +/* Record critical defects in libssl here, new versions of OpenSSL to older versions of OpenSSL. */ +static libssl_defect_t libssl_defects[] = +{ + { + .low = 0x01010001f, /* 1.1.0a */ + .high = 0x01010001f, /* 1.1.0a */ + .id = "CVE-2016-6309", + .name = "OCSP status request extension", + .comment = "For more information see https://www.openssl.org/news/secadv/20160926.txt" + }, + { + .low = 0x01010000f, /* 1.1.0 */ + .high = 0x01010000f, /* 1.1.0 */ + .id = "CVE-2016-6304", + .name = "OCSP status request extension", + .comment = "For more information see https://www.openssl.org/news/secadv/20160922.txt" + }, + { + .low = 0x01000209f, /* 1.0.2i */ + .high = 0x01000209f, /* 1.0.2i */ + .id = "CVE-2016-7052", + .name = "OCSP status request extension", + .comment = "For more information see https://www.openssl.org/news/secadv/20160926.txt" + }, + { + .low = 0x01000200f, /* 1.0.2 */ + .high = 0x01000208f, /* 1.0.2h */ + .id = "CVE-2016-6304", + .name = "OCSP status request extension", + .comment = "For more information see https://www.openssl.org/news/secadv/20160922.txt" + }, + { + .low = 0x01000100f, /* 1.0.1 */ + .high = 0x01000114f, /* 1.0.1t */ + .id = "CVE-2016-6304", + .name = "OCSP status request extension", + .comment = "For more information see https://www.openssl.org/news/secadv/20160922.txt" + }, + { + .low = 0x010001000, /* 1.0.1 */ + .high = 0x01000106f, /* 1.0.1f */ + .id = "CVE-2014-0160", + .name = "Heartbleed", + .comment = "For more information see http://heartbleed.com" + }, +}; +#endif /* ENABLE_OPENSSL_VERSION_CHECK */ + +FR_NAME_NUMBER const fr_tls_status_table[] = { + { "invalid", FR_TLS_INVALID }, + { "request", FR_TLS_REQUEST }, + { "response", FR_TLS_RESPONSE }, + { "success", FR_TLS_SUCCESS }, + { "fail", FR_TLS_FAIL }, + { "noop", FR_TLS_NOOP }, + + { "start", FR_TLS_START }, + { "ok", FR_TLS_OK }, + { "ack", FR_TLS_ACK }, + { "first fragment", FR_TLS_FIRST_FRAGMENT }, + { "more fragments", FR_TLS_MORE_FRAGMENTS }, + { "length included", FR_TLS_LENGTH_INCLUDED }, + { "more fragments with length", FR_TLS_MORE_FRAGMENTS_WITH_LENGTH }, + { "handled", FR_TLS_HANDLED }, + { NULL , -1}, +}; + +/* index we use to store cached session VPs + * needs to be dynamic so we can supply a "free" function + */ +int fr_tls_ex_index_vps = -1; +int fr_tls_ex_index_certs = -1; + +/* Session */ +static void session_close(tls_session_t *ssn); +static void session_init(tls_session_t *ssn); + +/* record */ +static void record_init(record_t *buf); +static void record_close(record_t *buf); +static unsigned int record_plus(record_t *buf, void const *ptr, + unsigned int size); +static unsigned int record_minus(record_t *buf, void *ptr, + unsigned int size); + +typedef struct { + char const *name; + SSL_CTX *ctx; +} fr_realm_ctx_t; + +DIAG_OFF(format-nonliteral) +/** Print errors in the TLS thread local error stack + * + * Drains the thread local OpenSSL error queue, and prints out errors. + * + * @param[in] request The current request (may be NULL). + * @param[in] msg Error message describing the operation being attempted. + * @param[in] ap Arguments for msg. + * @return the number of errors drained from the stack. + */ +static int tls_verror_log(REQUEST *request, char const *msg, va_list ap) +{ + unsigned long error; + char *p; + int in_stack = 0; + char buffer[256]; + + int line; + char const *file; + + /* + * Pop the first error, so ERR_peek_error() + * can be used to determine if there are + * multiple errors. + */ + error = ERR_get_error_line(&file, &line); + + if (msg) { + p = talloc_vasprintf(request, msg, ap); + + /* + * Single line mode (there's only one error) + */ + if (error && !ERR_peek_error()) { + ERR_error_string_n(error, buffer, sizeof(buffer)); + + /* Extra verbose */ + if ((request && RDEBUG_ENABLED3) || DEBUG_ENABLED3) { + ROPTIONAL(REDEBUG, ERROR, "(TLS) %s: %s[%i]:%s", p, file, line, buffer); + } else { + ROPTIONAL(REDEBUG, ERROR, "(TLS) %s: %s", p, buffer); + } + + talloc_free(p); + + return 1; + } + + /* + * Print the error we were given, irrespective + * of whether there were any OpenSSL errors. + */ + ROPTIONAL(RERROR, ERROR, "(TLS) %s", p); + talloc_free(p); + } + + /* + * Stack mode (there are multiple errors) + */ + if (!error) return 0; + do { + ERR_error_string_n(error, buffer, sizeof(buffer)); + /* Extra verbose */ + if ((request && RDEBUG_ENABLED3) || DEBUG_ENABLED3) { + ROPTIONAL(REDEBUG, ERROR, "(TLS) %s[%i]:%s", file, line, buffer); + } else { + ROPTIONAL(REDEBUG, ERROR, "(TLS) %s", buffer); + } + in_stack++; + } while ((error = ERR_get_error_line(&file, &line))); + + return in_stack; +} +DIAG_ON(format-nonliteral) + +/** Print errors in the TLS thread local error stack + * + * Drains the thread local OpenSSL error queue, and prints out errors. + * + * @param[in] request The current request (may be NULL). + * @param[in] msg Error message describing the operation being attempted. + * @param[in] ... Arguments for msg. + * @return the number of errors drained from the stack. + */ +int tls_error_log(REQUEST *request, char const *msg, ...) +{ + va_list ap; + int ret; + + va_start(ap, msg); + ret = tls_verror_log(request, msg, ap); + va_end(ap); + + return ret; +} + +/** Print errors raised by OpenSSL I/O functions + * + * Drains the thread local OpenSSL error queue, and prints out errors + * based on the SSL handle and the return code of the I/O function. + * + * OpenSSL lists I/O functions to be: + * - SSL_connect + * - SSL_accept + * - SSL_do_handshake + * - SSL_read + * - SSL_peek + * - SSL_write + * + * @param request The current request (may be NULL). + * @param session The current tls_session. + * @param ret from the I/O operation. + * @param msg Error message describing the operation being attempted. + * @param ... Arguments for msg. + * @return + * - 0 TLS session cannot continue. + * - 1 TLS session may still be viable. + */ +int tls_error_io_log(REQUEST *request, tls_session_t *session, int ret, char const *msg, ...) +{ + int error; + va_list ap; + + if (ERR_peek_error()) { + va_start(ap, msg); + tls_verror_log(request, msg, ap); + va_end(ap); + } + + error = SSL_get_error(session->ssl, ret); + switch (error) { + /* + * These seem to be harmless and already "dealt + * with" by our non-blocking environment. NB: + * "ZERO_RETURN" is the clean "error" + * indicating a successfully closed SSL + * tunnel. We let this happen because our IO + * loop should not appear to have broken on + * this condition - and outside the IO loop, the + * "shutdown" state is checked. + * + * Don't print anything if we ignore the error. + */ + case SSL_ERROR_NONE: + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + case SSL_ERROR_WANT_X509_LOOKUP: + case SSL_ERROR_ZERO_RETURN: + break; + + /* + * These seem to be indications of a genuine + * error that should result in the SSL tunnel + * being regarded as "dead". + */ + case SSL_ERROR_SYSCALL: + ROPTIONAL(REDEBUG, ERROR, "(TLS) System call (I/O) error (%i)", ret); + return 0; + + case SSL_ERROR_SSL: + ROPTIONAL(REDEBUG, ERROR, "(TLS) Protocol error (%i)", ret); + return 0; + + /* + * For any other errors that (a) exist, and (b) + * crop up - we need to interpret what to do with + * them - so "politely inform" the caller that + * the code needs updating here. + */ + default: + ROPTIONAL(REDEBUG, ERROR, "(TLS) Session error %i (%i)", error, ret); + return 0; + } + + return 1; +} + +#ifdef PSK_MAX_IDENTITY_LEN +static bool identity_is_safe(const char *identity) +{ + char c; + + if (!identity) return true; + + while ((c = *(identity++)) != '\0') { + if (isalpha((uint8_t) c) || isdigit((uint8_t) c) || isspace((uint8_t) c) || + (c == '@') || (c == '-') || (c == '_') || (c == '.')) { + continue; + } + + return false; + } + + return true; +} + +/* + * When a client uses TLS-PSK to talk to a server, this callback + * is used by the server to determine the PSK to use. + */ +static unsigned int psk_server_callback(SSL *ssl, const char *identity, + unsigned char *psk, + unsigned int max_psk_len) +{ + unsigned int psk_len = 0; + fr_tls_server_conf_t *conf; + REQUEST *request; + + conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssl, + FR_TLS_EX_INDEX_CONF); + if (!conf) return 0; + + request = (REQUEST *)SSL_get_ex_data(ssl, + FR_TLS_EX_INDEX_REQUEST); + if (request && conf->psk_query) { + size_t hex_len; + VALUE_PAIR *vp, **certs; + TALLOC_CTX *talloc_ctx; + char buffer[2 * PSK_MAX_PSK_LEN + 4]; /* allow for too-long keys */ + + /* + * The passed identity is weird. Deny it. + */ + if (!identity_is_safe(identity)) { + RWDEBUG("(TLS) Invalid characters in PSK identity %s", identity); + return 0; + } + + vp = pair_make_request("TLS-PSK-Identity", identity, T_OP_SET); + if (!vp) return 0; + + certs = (VALUE_PAIR **)SSL_get_ex_data(ssl, fr_tls_ex_index_certs); + talloc_ctx = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_TALLOC); + fr_assert(certs != NULL); /* pointer to sock->certs */ + fr_assert(talloc_ctx != NULL); /* sock */ + + fr_pair_add(certs, fr_pair_copy(talloc_ctx, vp)); + + hex_len = radius_xlat(buffer, sizeof(buffer), request, conf->psk_query, + NULL, NULL); + if (!hex_len) { + RWDEBUG("(TLS) PSK expansion returned an empty string."); + return 0; + } + + /* + * The returned key is truncated at MORE than + * OpenSSL can handle. That way we can detect + * the truncation, and complain about it. + */ + if (hex_len > (2 * max_psk_len)) { + RWDEBUG("(TLS) Returned PSK is too long (%u > %u)", + (unsigned int) hex_len, 2 * max_psk_len); + return 0; + } + + /* + * Leave the TLS-PSK-Identity in the request, and + * convert the expansion from printable string + * back to hex. + */ + return fr_hex2bin(psk, max_psk_len, buffer, hex_len); + } + + if (!conf->psk_identity) { + DEBUG("No static PSK identity set. Rejecting the user"); + return 0; + } + + /* + * No REQUEST, or no dynamic query. Just look for a + * static identity. + */ + if (strcmp(identity, conf->psk_identity) != 0) { + ERROR("(TKS) Supplied PSK identity %s does not match configuration. Rejecting.", + identity); + return 0; + } + + psk_len = strlen(conf->psk_password); + if (psk_len > (2 * max_psk_len)) return 0; + + return fr_hex2bin(psk, max_psk_len, conf->psk_password, psk_len); +} + +static unsigned int psk_client_callback(SSL *ssl, UNUSED char const *hint, + char *identity, unsigned int max_identity_len, + unsigned char *psk, unsigned int max_psk_len) +{ + unsigned int psk_len; + fr_tls_server_conf_t *conf; + + conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssl, + FR_TLS_EX_INDEX_CONF); + if (!conf) return 0; + + psk_len = strlen(conf->psk_password); + if (psk_len > (2 * max_psk_len)) return 0; + + strlcpy(identity, conf->psk_identity, max_identity_len); + + return fr_hex2bin(psk, max_psk_len, conf->psk_password, psk_len); +} + +#endif + +#define MAX_SESSION_SIZE (256) + + +void tls_session_id(SSL_SESSION *ssn, char *buffer, size_t bufsize) +{ +#if OPENSSL_VERSION_NUMBER < 0x10001000L + size_t size; + + size = ssn->session_id_length; + if (size > bufsize) size = bufsize; + + fr_bin2hex(buffer, ssn->session_id, size); +#else + unsigned int size; + uint8_t const *p; + + p = SSL_SESSION_get_id(ssn, &size); + if (size > bufsize) size = bufsize; + + fr_bin2hex(buffer, p, size); + +#endif +} + +static int _tls_session_free(tls_session_t *ssn) +{ + /* + * Free any opaque TTLS or PEAP data. + */ + if ((ssn->opaque) && (ssn->free_opaque)) { + ssn->free_opaque(ssn->opaque); + ssn->opaque = NULL; + } + + session_close(ssn); + + return 0; +} + +#if OPENSSL_VERSION_NUMBER >= 0x10101000L && !defined(LIBRESSL_VERSION_NUMBER) +/* + * By setting the environment variable SSLKEYLOGFILE to a filename keying + * material will be exported that you may use with Wireshark to decode any + * TLS flows. Please see the following for more details: + * + * https://gitlab.com/wireshark/wireshark/-/wikis/TLS#tls-decryption + * + * An example logging session is (you should delete the file on each run): + * + * rm -f /tmp/sslkey.log; env SSLKEYLOGFILE=/tmp/sslkey.log freeradius -X | tee /tmp/debug + */ +static void tls_keylog_cb(UNUSED const SSL *ssl, const char *line) +{ + int fd; + size_t len; + const char *filename; + // less than _POSIX_PIPE_BUF (512) guarantees writes are atomic for O_APPEND + char buffer[64 + 2*SSL3_RANDOM_SIZE + 2*SSL_MAX_MASTER_KEY_LENGTH]; + + filename = getenv("SSLKEYLOGFILE"); + if (!filename) return; + + len = strlen(line); + if ((len + 1) > sizeof(buffer)) { + DEBUG("SSLKEYLOGFILE buffer not large enough, max %lu, required %lu", sizeof(buffer), len + 1); + return; + } + + memcpy(buffer, line, len); + buffer[len] = '\n'; + + fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR); + if (fd < 0) { + fr_strerror_printf("Failed to open file %s: %s", filename, strerror(errno)); + return; + } + + if (write(fd, buffer, len + 1) == -1) { + DEBUG("Failed to write to file %s: %s", filename, strerror(errno)); + } + + close(fd); +} +#endif + +tls_session_t *tls_new_client_session(TALLOC_CTX *ctx, fr_tls_server_conf_t *conf, int fd, VALUE_PAIR **certs) +{ + int ret; + int verify_mode; + tls_session_t *ssn = NULL; + REQUEST *request; + + ssn = talloc_zero(ctx, tls_session_t); + if (!ssn) return NULL; + + talloc_set_destructor(ssn, _tls_session_free); + + ssn->ctx = conf->ctx; + ssn->mtu = conf->fragment_size; + ssn->conf = conf; + + SSL_CTX_set_mode(ssn->ctx, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER | SSL_MODE_AUTO_RETRY); + + ssn->ssl = SSL_new(ssn->ctx); + if (!ssn->ssl) { + talloc_free(ssn); + return NULL; + } + + request = request_alloc(ssn); + request->packet = rad_alloc(request, false); + request->reply = rad_alloc(request, false); + + SSL_set_ex_data(ssn->ssl, FR_TLS_EX_INDEX_REQUEST, (void *)request); + + if (conf->fix_cert_order) { + SSL_set_ex_data(ssn->ssl, FR_TLS_EX_INDEX_FIX_CERT_ORDER, (void *) &conf->fix_cert_order); + } + + /* + * Add the message callback to identify what type of + * message/handshake is passed + */ + SSL_set_msg_callback(ssn->ssl, cbtls_msg); + SSL_set_msg_callback_arg(ssn->ssl, ssn); + SSL_set_info_callback(ssn->ssl, cbtls_info); + + /* + * Always verify the peer certificate. + */ + DEBUG2("Requiring Server certificate"); + verify_mode = SSL_VERIFY_PEER; + verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + SSL_set_verify(ssn->ssl, verify_mode, cbtls_verify); + + SSL_set_ex_data(ssn->ssl, FR_TLS_EX_INDEX_CONF, (void *)conf); + SSL_set_ex_data(ssn->ssl, FR_TLS_EX_INDEX_SSN, (void *)ssn); + if (certs) SSL_set_ex_data(ssn->ssl, fr_tls_ex_index_certs, (void *)certs); + + SSL_set_fd(ssn->ssl, fd); + + ret = SSL_connect(ssn->ssl); + if (ret < 0) { + switch (SSL_get_error(ssn->ssl, ret)) { + default: + break; + + case SSL_ERROR_WANT_READ: + ssn->connected = false; + return ssn; + + case SSL_ERROR_WANT_WRITE: + ssn->connected = false; + return ssn; + } + } + + if (ret <= 0) { + tls_error_io_log(NULL, ssn, ret, "Failed in connecting TLS session."); + talloc_free(ssn); + + return NULL; + } + + ssn->connected = true; + return ssn; +} + + +/** Create a new TLS session + * + * Configures a new TLS session, configuring options, setting callbacks etc... + * + * @param ctx to alloc session data in. Should usually be NULL unless the lifetime of the + * session is tied to another talloc'd object. + * @param conf to use to configure the tls session. + * @param request The current #REQUEST. + * @param client_cert Whether to require a client_cert. + * @param allow_tls13 Whether to allow or forbid TLS 1.3. + * @return a new session on success, or NULL on error. + */ +tls_session_t *tls_new_session(TALLOC_CTX *ctx, fr_tls_server_conf_t *conf, REQUEST *request, bool client_cert, +#ifndef TLS1_3_VERSION + UNUSED +#endif + bool allow_tls13) +{ + tls_session_t *state = NULL; + SSL *new_tls = NULL; + int verify_mode = 0; + VALUE_PAIR *vp; + X509_STORE *new_cert_store; + + rad_assert(request != NULL); + + RDEBUG2("(TLS) Initiating new session"); + + /* + * Replace X509 store if it is time to update CRLs/certs in ca_path + */ + if (conf->ca_path_reload_interval > 0 && conf->ca_path_last_reload + conf->ca_path_reload_interval <= request->timestamp) { + pthread_mutex_lock(&conf->mutex); + /* recheck conf->ca_path_last_reload because it may be inaccurate without mutex */ + if (conf->ca_path_last_reload + conf->ca_path_reload_interval <= request->timestamp) { + RDEBUG2("Flushing X509 store to re-read data from ca_path dir"); + + if ((new_cert_store = fr_init_x509_store(conf)) == NULL) { + RERROR("(TLS) Error replacing X509 store, out of memory (?)"); + } else { + if (conf->old_x509_store) X509_STORE_free(conf->old_x509_store); + /* + * Swap empty store with the old one. + */ +#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER) + conf->old_x509_store = SSL_CTX_get_cert_store(conf->ctx); + /* Bump refcnt so the store is kept allocated till next store replacement */ + X509_STORE_up_ref(conf->old_x509_store); + SSL_CTX_set_cert_store(conf->ctx, new_cert_store); +#else + /* + * We do not use SSL_CTX_set_cert_store() call here because + * we are not sure that old X509 store is not in the use by some + * thread (i.e. cert check in progress). + * Keep it allocated till next store replacement. + */ + conf->old_x509_store = conf->ctx->cert_store; + conf->ctx->cert_store = new_cert_store; +#endif + conf->ca_path_last_reload = request->timestamp; + } + } + pthread_mutex_unlock(&conf->mutex); + } + + new_tls = SSL_new(conf->ctx); + if (new_tls == NULL) { + tls_error_log(request, "Error creating new TLS session"); + return NULL; + } + +#ifdef TLS1_3_VERSION + /* + * Disallow TLS 1.3 for FAST. + * + * We need another magic configuration option to allow + * it. + */ + if (!allow_tls13 && (conf->max_version == TLS1_3_VERSION)) { + WARN("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + WARN("!! FORCING MAXIMUM TLS VERSION TO TLS 1.2 !!"); + WARN("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + WARN("!! There is no standard for using this EAP method with TLS 1.3"); + WARN("!! Please set tls_max_version = \"1.2\""); + WARN("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + + if (SSL_set_max_proto_version(new_tls, TLS1_2_VERSION) == 0) { + tls_error_log(request, "Failed limiting maximum version to TLS 1.2"); + return NULL; + } + } +#endif + + /* We use the SSL's "app_data" to indicate a call-back */ + SSL_set_app_data(new_tls, NULL); + + if ((state = talloc_zero(ctx, tls_session_t)) == NULL) { + RERROR("(TLS) Error allocating memory for SSL state"); + return NULL; + } + session_init(state); + talloc_set_destructor(state, _tls_session_free); + + state->ctx = conf->ctx; + state->ssl = new_tls; + state->conf = conf; + +#if OPENSSL_VERSION_NUMBER >= 0x10101000L && !defined(LIBRESSL_VERSION_NUMBER) + /* + * Set the keylog file if the admin requested it. + */ + if (getenv("SSLKEYLOGFILE") != NULL) SSL_CTX_set_keylog_callback(state->ctx, tls_keylog_cb); +#endif + + /* + * Initialize callbacks + */ + state->record_init = record_init; + state->record_close = record_close; + state->record_plus = record_plus; + state->record_minus = record_minus; + + /* + * Create & hook the BIOs to handle the dirty side of the + * SSL. This is *very important* as we want to handle + * the transmission part. Now the only IO interface + * that SSL is aware of, is our defined BIO buffers. + * + * This means that all SSL IO is done to/from memory, + * and we can update those BIOs from the packets we've + * received. + */ + state->into_ssl = BIO_new(BIO_s_mem()); + state->from_ssl = BIO_new(BIO_s_mem()); + SSL_set_bio(state->ssl, state->into_ssl, state->from_ssl); + + /* + * Add the message callback to identify what type of + * message/handshake is passed + */ + SSL_set_msg_callback(new_tls, cbtls_msg); + SSL_set_msg_callback_arg(new_tls, state); + SSL_set_info_callback(new_tls, cbtls_info); + +#if OPENSSL_VERSION_NUMBER >= 0x10100000L + /* + * Allow policies to load context-specific certificate chains. + */ + vp = fr_pair_find_by_num(request->config, PW_TLS_SESSION_CERT_FILE, 0, TAG_ANY); + if (vp) { + VALUE_PAIR *key = fr_pair_find_by_num(request->config, PW_TLS_SESSION_CERT_PRIVATE_KEY_FILE, 0, TAG_ANY); + if (!key) key = vp; + + RDEBUG2("(TLS) Loading session certificate file \"%s\"", vp->vp_strvalue); + + if (conf->realms) { + fr_realm_ctx_t my_r, *r; + + /* + * Use a pre-existing SSL CTX, if + * available. Note that due to OpenSSL + * issues, this really changes only the + * certificate files, and leaves all + * other fields alone. e.g. you can't + * select a different TLS version. + * + * This is fine for our purposes in v3. + * Due to how we build them, the various + * additional SSL_CTXs are identical to + * the main one, except for certs. + */ + my_r.name = vp->vp_strvalue; + r = fr_hash_table_finddata(conf->realms, &my_r); + if (r) { + (void) SSL_set_SSL_CTX(state->ssl, r->ctx); + goto after_chain; + } + + /* + * Else fall through to trying to dynamically load the certs. + */ + } + + if (conf->file_type) { + if (SSL_use_certificate_chain_file(state->ssl, vp->vp_strvalue) != 1) { + tls_error_log(request, "Failed loading TLS session certificate \"%s\"", + vp->vp_strvalue); + error: + talloc_free(state); + return NULL; + } + } else { + if (SSL_use_certificate_file(state->ssl, vp->vp_strvalue, SSL_FILETYPE_ASN1) != 1) { + tls_error_log(request, "Failed loading TLS session certificate \"%s\"", + vp->vp_strvalue); + goto error; + } + } + + /* + * Note that there is either no password, or it + * has to be the same as what's in the + * configuration. + * + * There is just no additional security to + * putting a password into the same file system + * as the private key. + */ + if (SSL_use_PrivateKey_file(state->ssl, key->vp_strvalue, SSL_FILETYPE_PEM) != 1) { + tls_error_log(request, "Failed loading TLS session certificate \"%s\"", + key->vp_strvalue); + goto error; + } + + if (SSL_check_private_key(state->ssl) != 1) { + tls_error_log(request, "Failed validating TLS session certificate \"%s\"", + vp->vp_strvalue); + goto error; + } + } +after_chain: +#endif + + /* + * In Server mode we only accept. + */ + SSL_set_accept_state(state->ssl); + + /* + * Verify the peer certificate, if asked. + */ + if (client_cert) { + RDEBUG2("(TLS) Setting verify mode to require certificate from client"); + verify_mode = SSL_VERIFY_PEER; + verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + verify_mode |= SSL_VERIFY_CLIENT_ONCE; + } + SSL_set_verify(state->ssl, verify_mode, cbtls_verify); + + SSL_set_ex_data(state->ssl, FR_TLS_EX_INDEX_CONF, (void *)conf); + SSL_set_ex_data(state->ssl, FR_TLS_EX_INDEX_SSN, (void *)state); + state->length_flag = conf->include_length; + + /* + * We use default fragment size, unless the Framed-MTU + * tells us it's too big. Note that we do NOT account + * for the EAP-TLS headers if conf->fragment_size is + * large, because that config item looks to be confusing. + * + * i.e. it should REALLY be called MTU, and the code here + * should figure out what that means for TLS fragment size. + * asking the administrator to know the internal details + * of EAP-TLS in order to calculate fragment sizes is + * just too much. + */ + state->mtu = conf->fragment_size; +#define EAP_TLS_MAGIC_OVERHEAD (63) + + /* + * If the packet contains an MTU, then use that. We + * trust the admin! + */ + vp = fr_pair_find_by_num(request->packet->vps, PW_FRAMED_MTU, 0, TAG_ANY); + if (vp) { + if ((vp->vp_integer > 100) && (vp->vp_integer < state->mtu)) { + state->mtu = vp->vp_integer; + } + + } else if (request->parent) { + /* + * If there's a parent request, we look for what + * MTU was set there. Then, we use an MTU which + * accounts for the extra overhead of nesting EAP + * + TLS inside of EAP + TLS. + */ + vp = fr_pair_find_by_num(request->parent->state, PW_FRAMED_MTU, 0, TAG_ANY); + if (vp && (vp->vp_integer > (100 + EAP_TLS_MAGIC_OVERHEAD)) && (vp->vp_integer <= state->mtu)) { + state->mtu = vp->vp_integer - EAP_TLS_MAGIC_OVERHEAD; + } + } + + /* + * Cache / update the Framed-MTU in the session-state + * list. + */ + vp = fr_pair_find_by_num(request->state, PW_FRAMED_MTU, 0, TAG_ANY); + if (!vp) { + vp = fr_pair_afrom_num(request->state_ctx, PW_FRAMED_MTU, 0); + fr_pair_add(&request->state, vp); + } + if (vp) vp->vp_integer = state->mtu; + + if (conf->session_cache_enable) state->allow_session_resumption = true; /* otherwise it's false */ + + return state; +} + +/* + * We are the server, we always get the dirty data + * (Handshake data is also considered as dirty data) + * During handshake, since SSL API handles itself, + * After clean-up, dirty_out will be filled with + * the data required for handshaking. So we check + * if dirty_out is empty then we simply send it back. + * As of now, if handshake is successful, then we keep going, + * otherwise we fail. + * + * Fill the Bio with the dirty data to clean it + * Get the cleaned data from SSL, if it is not Handshake data + */ +int tls_handshake_recv(REQUEST *request, tls_session_t *ssn) +{ + int err; + + if (ssn->invalid_hb_used) { + REDEBUG("(TLS) OpenSSL Heartbeat attack detected. Closing connection"); + return 0; + } + + if (ssn->dirty_in.used > 0) { + err = BIO_write(ssn->into_ssl, ssn->dirty_in.data, ssn->dirty_in.used); + if (err != (int) ssn->dirty_in.used) { + REDEBUG("(TLS) Failed writing %zd bytes to SSL BIO: %d", ssn->dirty_in.used, err); + record_init(&ssn->dirty_in); + return 0; + } + record_init(&ssn->dirty_in); + } + + err = SSL_read(ssn->ssl, ssn->clean_out.data + ssn->clean_out.used, + sizeof(ssn->clean_out.data) - ssn->clean_out.used); + if (err > 0) { + ssn->clean_out.used += err; + return 1; + } + + if (!tls_error_io_log(request, ssn, err, "Failed reading from OpenSSL")) return 0; + + /* Some Extra STATE information for easy debugging */ + if (!ssn->is_init_finished && SSL_is_init_finished(ssn->ssl)) { + VALUE_PAIR *vp; + char const *str_version; + + RDEBUG2("(TLS) Connection Established"); + ssn->is_init_finished = true; + + vp = fr_pair_afrom_num(request->state_ctx, PW_TLS_SESSION_CIPHER_SUITE, 0); + if (vp) { + fr_pair_value_strcpy(vp, SSL_CIPHER_get_name(SSL_get_current_cipher(ssn->ssl))); + fr_pair_add(&request->state, vp); + RINDENT(); + rdebug_pair(L_DBG_LVL_2, request, vp, NULL); + REXDENT(); + } + + switch (SSL_version(ssn->ssl)) { + case SSL2_VERSION: + str_version = "SSL 2.0"; + break; + case SSL3_VERSION: + str_version = "SSL 3.0"; + break; + case TLS1_VERSION: + str_version = "TLS 1.0"; + break; +#ifdef TLS1_1_VERSION + case TLS1_1_VERSION: + str_version = "TLS 1.1"; + break; +#endif +#ifdef TLS1_2_VERSION + case TLS1_2_VERSION: + str_version = "TLS 1.2"; + break; +#endif +#ifdef TLS1_3_VERSION + case TLS1_3_VERSION: + str_version = "TLS 1.3"; + break; +#endif + default: + str_version = "UNKNOWN"; + break; + } + + vp = fr_pair_afrom_num(request->state_ctx, PW_TLS_SESSION_VERSION, 0); + if (vp) { + fr_pair_value_strcpy(vp, str_version); + fr_pair_add(&request->state, vp); + RINDENT(); + rdebug_pair(L_DBG_LVL_2, request, vp, NULL); + REXDENT(); + } + } + else if (SSL_in_init(ssn->ssl)) { RDEBUG2("(TLS) In Handshake Phase"); } + else if (SSL_in_before(ssn->ssl)) { RDEBUG2("(TLS) Before Handshake Phase"); } + else if (SSL_in_accept_init(ssn->ssl)) { RDEBUG2("(TLS) In Accept mode"); } + else if (SSL_in_connect_init(ssn->ssl)) { RDEBUG2("(TLS) In Connect mode"); } + +#if OPENSSL_VERSION_NUMBER >= 0x10001000L + /* + * Cache the SSL_SESSION pointer. + */ + if (!ssn->ssl_session) { + ssn->ssl_session = SSL_get_session(ssn->ssl); + + /* + * Some versions of OpenSSL don't allow you to + * get the session before the init is finished. + * In that case, this error is a soft fail. + * + * If the session init is finished, then failure + * to get the session is a hard fail. + */ + if (!ssn->ssl_session && ssn->is_init_finished) { + RDEBUG("(TLS) Failed getting session"); + return 0; + } + } + +#else +#error You must use a newer version of OpenSSL +#endif + + err = BIO_ctrl_pending(ssn->from_ssl); + if (err > 0) { + err = BIO_read(ssn->from_ssl, ssn->dirty_out.data, + sizeof(ssn->dirty_out.data)); + if (err > 0) { + RDEBUG3("(TLS) got %d bytes of data", err); + ssn->dirty_out.used = err; + + } else if (BIO_should_retry(ssn->from_ssl)) { + record_init(&ssn->dirty_in); + RDEBUG2("(TLS) Asking for more data in tunnel."); + return 1; + + } else { + tls_error_log(NULL, "Error reading from OpenSSL"); + record_init(&ssn->dirty_in); + return 0; + } + } else { + RDEBUG2("(TLS) Application data."); + /* Its clean application data, leave whatever is in the buffer */ +#if 0 + record_init(&ssn->clean_out); +#endif + } + + /* We are done with dirty_in, reinitialize it */ + record_init(&ssn->dirty_in); + return 1; +} + +/* + * Take cleartext user data, and encrypt it into the output buffer, + * to send to the client at the other end of the SSL connection. + */ +int tls_handshake_send(REQUEST *request, tls_session_t *ssn) +{ + int err; + + /* + * If there's un-encrypted data in 'clean_in', then write + * that data to the SSL session, and then call the BIO function + * to get that encrypted data from the SSL session, into + * a buffer which we can then package into an EAP packet. + * + * Based on Server's logic this clean_in is expected to + * contain the data to send to the client. + */ + if (ssn->clean_in.used > 0) { + int written; + + written = SSL_write(ssn->ssl, ssn->clean_in.data, ssn->clean_in.used); + record_minus(&ssn->clean_in, NULL, written); + + /* Get the dirty data from Bio to send it */ + err = BIO_read(ssn->from_ssl, ssn->dirty_out.data + ssn->dirty_out.used, + sizeof(ssn->dirty_out.data) - ssn->dirty_out.used); + if (err > 0) { + ssn->dirty_out.used += err; + } else { + if (!tls_error_io_log(request, ssn, err, "Failed writing to OpenSSL")) { + return 0; + } + } + } + + return 1; +} + +static void session_init(tls_session_t *ssn) +{ + ssn->ssl = NULL; + ssn->into_ssl = ssn->from_ssl = NULL; + record_init(&ssn->clean_in); + record_init(&ssn->clean_out); + record_init(&ssn->dirty_in); + record_init(&ssn->dirty_out); + + memset(&ssn->info, 0, sizeof(ssn->info)); + + ssn->mtu = 0; + ssn->fragment = false; + ssn->tls_msg_len = 0; + ssn->length_flag = false; + ssn->opaque = NULL; + ssn->free_opaque = NULL; +} + +static void session_close(tls_session_t *ssn) +{ + if (ssn->ssl) { + SSL_set_quiet_shutdown(ssn->ssl, 1); + SSL_shutdown(ssn->ssl); + + SSL_free(ssn->ssl); + ssn->ssl = NULL; + } + + record_close(&ssn->clean_in); + record_close(&ssn->clean_out); + record_close(&ssn->dirty_in); + record_close(&ssn->dirty_out); + session_init(ssn); +} + +static void record_init(record_t *rec) +{ + rec->used = 0; +} + +static void record_close(record_t *rec) +{ + rec->used = 0; +} + + +/* + * Copy data to the intermediate buffer, before we send + * it somewhere. + */ +static unsigned int record_plus(record_t *rec, void const *ptr, + unsigned int size) +{ + unsigned int added = MAX_RECORD_SIZE - rec->used; + + if(added > size) + added = size; + if(added == 0) + return 0; + memcpy(rec->data + rec->used, ptr, added); + rec->used += added; + return added; +} + +/* + * Take data from the buffer, and give it to the caller. + */ +static unsigned int record_minus(record_t *rec, void *ptr, + unsigned int size) +{ + unsigned int taken = rec->used; + + if(taken > size) + taken = size; + if(taken == 0) + return 0; + if(ptr) + memcpy(ptr, rec->data, taken); + rec->used -= taken; + + /* + * This is pretty bad... + */ + if (rec->used > 0) memmove(rec->data, rec->data + taken, rec->used); + + return taken; +} + +void tls_session_information(tls_session_t *tls_session) +{ + char const *str_write_p, *str_version, *str_content_type = ""; + char const *str_details1 = "", *str_details2= ""; + char const *details = NULL; + REQUEST *request; + VALUE_PAIR *vp; + char content_type[16], alert_buf[16]; + char buffer[32]; + + /* + * Don't print this out in the normal course of + * operations. + */ + if (rad_debug_lvl == 0) return; + + /* + * OpenSSL calls this function with 'pseudo' content + * types. The user doesn't care about them, so suppress them. + */ + if (tls_session->info.content_type > UINT8_MAX) return; + + request = SSL_get_ex_data(tls_session->ssl, FR_TLS_EX_INDEX_REQUEST); + if (!request) return; + + str_write_p = tls_session->info.origin ? "(TLS) send" : "(TLS) recv"; + +#define FROM_CLIENT (tls_session->info.origin == 0) + + switch (SSL_version(tls_session->ssl)) { + case SSL2_VERSION: + str_version = "SSL 2.0 "; + break; + case SSL3_VERSION: + str_version = "SSL 3.0 "; + break; + case TLS1_VERSION: + str_version = "TLS 1.0 "; + break; +#ifdef TLS1_1_VERSION + case TLS1_1_VERSION: + str_version = "TLS 1.1 "; + break; +#endif +#ifdef TLS1_2_VERSION + case TLS1_2_VERSION: + str_version = "TLS 1.2 "; + break; +#endif +#ifdef TLS1_3_VERSION + case TLS1_3_VERSION: + str_version = "TLS 1.3 "; + break; +#endif + + default: + sprintf(buffer, "UNKNOWN TLS VERSION '%04X'", SSL_version(tls_session->ssl)); + str_version = buffer; + break; + } + + if (1) { + switch (tls_session->info.content_type) { + case SSL3_RT_CHANGE_CIPHER_SPEC: + str_content_type = "ChangeCipherSpec"; + break; + + case SSL3_RT_ALERT: + str_content_type = "Alert"; + break; + + case SSL3_RT_HANDSHAKE: + str_content_type = "Handshake"; + break; + + case SSL3_RT_APPLICATION_DATA: + str_content_type = "ApplicationData"; + break; + + default: + snprintf(content_type, sizeof(content_type), "content=%d", tls_session->info.content_type); + str_content_type = content_type; + break; + } + + if (tls_session->info.content_type == SSL3_RT_ALERT) { + str_details1 = ", ???"; + + if (tls_session->info.record_len == 2) { + + switch (tls_session->info.alert_level) { + case SSL3_AL_WARNING: + str_details1 = ", warning"; + break; + case SSL3_AL_FATAL: + str_details1 = ", fatal"; + break; + } + + str_details2 = " ???"; + details = "there is a failure inside the TLS protocol exchange"; + + switch (tls_session->info.alert_description) { + case SSL3_AD_CLOSE_NOTIFY: + str_details2 = " close_notify"; + details = "the connection has been closed, and no further TLS exchanges will take place"; + break; + + case SSL3_AD_UNEXPECTED_MESSAGE: + str_details2 = " unexpected_message"; + break; + + case SSL3_AD_BAD_RECORD_MAC: + str_details2 = " bad_record_mac"; + break; + + case TLS1_AD_DECRYPTION_FAILED: + str_details2 = " decryption_failed"; + break; + + case TLS1_AD_RECORD_OVERFLOW: + str_details2 = " record_overflow"; + break; + + case SSL3_AD_DECOMPRESSION_FAILURE: + str_details2 = " decompression_failure"; + break; + + case SSL3_AD_HANDSHAKE_FAILURE: + str_details2 = " handshake_failure"; + break; + + case SSL3_AD_NO_CERTIFICATE: + str_details2 = " no_certificate"; + details = "the server did not present a certificate to the client"; + break; + + case SSL3_AD_BAD_CERTIFICATE: + str_details2 = " bad_certificate"; + details = "it believes the server certificate is invalid or malformed"; + break; + + case SSL3_AD_UNSUPPORTED_CERTIFICATE: + str_details2 = " unsupported_certificate"; + details = "it does not understand the certificate presented by the server"; + break; + + case SSL3_AD_CERTIFICATE_REVOKED: + str_details2 = " certificate_revoked"; + details = "it believes that the server certificate has been revoked"; + break; + + case SSL3_AD_CERTIFICATE_EXPIRED: + str_details2 = " certificate_expired"; + details = "it believes that the server certificate has expired. Either renew the server certificate, or check the time on the client"; + break; + + case SSL3_AD_CERTIFICATE_UNKNOWN: + str_details2 = " certificate_unknown"; + details = "it does not recognize the server certificate"; + break; + + case SSL3_AD_ILLEGAL_PARAMETER: + str_details2 = " illegal_parameter"; + break; + + case TLS1_AD_UNKNOWN_CA: + str_details2 = " unknown_ca"; + details = "it does not recognize the CA used to issue the server certificate. Please update the client so that it knows about the CA"; + break; + + case TLS1_AD_ACCESS_DENIED: + str_details2 = " access_denied"; + break; + + case TLS1_AD_DECODE_ERROR: + str_details2 = " decode_error"; + break; + + case TLS1_AD_DECRYPT_ERROR: + str_details2 = " decrypt_error"; + break; + + case TLS1_AD_EXPORT_RESTRICTION: + str_details2 = " export_restriction"; + break; + + case TLS1_AD_PROTOCOL_VERSION: + str_details2 = " protocol_version"; + details = "the client does not accept the version of TLS negotiated by the server"; + +#ifdef TLS1_3_VERSION + /* + * Complain about OpenSSL bugs. + */ + if ((SSL_version(tls_session->ssl) > tls_session->conf->max_version) && + (rad_debug_lvl > 0)) { + WARN("TLS 1.3 has been negotiated even though it was disabled. This is an OpenSSL Bug."); + WARN("Please set: cipher_list = \"DEFAULT@SECLEVEL=1\" in the tls {...} section."); + } +#endif + break; + + case TLS1_AD_INSUFFICIENT_SECURITY: + str_details2 = " insufficient_security"; + break; + + case TLS1_AD_INTERNAL_ERROR: + str_details2 = " internal_error"; + break; + + case TLS1_AD_USER_CANCELLED: + str_details2 = " user_canceled"; + break; + + case TLS1_AD_NO_RENEGOTIATION: + str_details2 = " no_renegotiation"; + break; + +#ifdef TLS13_AD_MISSING_EXTENSIONS + case TLS13_AD_MISSING_EXTENSIONS: + str_details2 = " missing_extensions"; + details = "the server did not present a TLS extension which the client expected to be present. Please check the TLS libraries on the client and server for compatibility"; + break; +#endif + +#ifdef TLS13_AD_CERTIFICATE_REQUIRED + case TLS13_AD_CERTIFICATE_REQUIRED: + str_details2 = " certificate_required"; + details = "the server did not present a certificate"; + break; +#endif + +#ifdef TLS1_AD_UNSUPPORTED_EXTENSION + case TLS1_AD_UNSUPPORTED_EXTENSION: + str_details2 = " unsupported_extension"; + details = "the server has sent a TLS message which the client does not recognize. Please check the TLS libraries on the client and server for compatibility"; + break; +#endif + +#ifdef TLS1_AD_CERTIFICATE_UNOBTAINABLE + case TLS1_AD_CERTIFICATE_UNOBTAINABLE: + str_details2 = " certificate_unobtainable"; + break; +#endif + +#ifdef TLS1_AD_UNRECOGNIZED_NAME + case TLS1_AD_UNRECOGNIZED_NAME: + str_details2 = " unrecognized_name"; + break; +#endif + +#ifdef TLS1_AD_BAD_CERTIFICATE_STATUS_RESPONSE + case TLS1_AD_BAD_CERTIFICATE_STATUS_RESPONSE: + str_details2 = " bad_certificate_status_response"; + break; +#endif + +#ifdef TLS1_AD_BAD_CERTIFICATE_HASH_VALUE + case TLS1_AD_BAD_CERTIFICATE_HASH_VALUE: + str_details2 = " bad_certificate_hash_value"; + break; +#endif + +#ifdef TLS1_AD_UNKNOWN_PSK_IDENTITY + case TLS1_AD_UNKNOWN_PSK_IDENTITY: + str_details2 = " unknown_psk_identity"; + break; +#endif + +#ifdef TLS1_AD_NO_APPLICATION_PROTOCOL + case TLS1_AD_NO_APPLICATION_PROTOCOL: + str_details2 = " no_application_protocol"; + break; +#endif + } + } + } + + if (tls_session->info.content_type == SSL3_RT_HANDSHAKE) { + str_details1 = ""; + + if (tls_session->info.record_len > 0) switch (tls_session->info.handshake_type) { + case SSL3_MT_HELLO_REQUEST: + str_details1 = ", HelloRequest"; + break; + + case SSL3_MT_CLIENT_HELLO: + str_details1 = ", ClientHello"; + break; + + case SSL3_MT_SERVER_HELLO: + str_details1 = ", ServerHello"; + break; + +#ifdef SSL3_MT_NEWSESSION_TICKET + case SSL3_MT_NEWSESSION_TICKET: + str_details1 = ", NewSessionTicket"; + break; +#endif + +#ifdef SSL3_MT_ENCRYPTED_EXTENSIONS + case SSL3_MT_ENCRYPTED_EXTENSIONS: + str_details1 = ", EncryptedExtensions"; + break; +#endif + + case SSL3_MT_CERTIFICATE: + str_details1 = ", Certificate"; + break; + + case SSL3_MT_SERVER_KEY_EXCHANGE: + str_details1 = ", ServerKeyExchange"; + break; + + case SSL3_MT_CERTIFICATE_REQUEST: + str_details1 = ", CertificateRequest"; + break; + + case SSL3_MT_SERVER_DONE: + str_details1 = ", ServerHelloDone"; + break; + + case SSL3_MT_CERTIFICATE_VERIFY: + str_details1 = ", CertificateVerify"; + break; + + case SSL3_MT_CLIENT_KEY_EXCHANGE: + str_details1 = ", ClientKeyExchange"; + break; + + case SSL3_MT_FINISHED: + str_details1 = ", Finished"; + break; + +#ifdef SSL3_MT_KEY_UPDATE + case SSL3_MT_KEY_UPDATE: + str_content_type = "KeyUpdate"; + break; +#endif + + default: + snprintf(alert_buf, sizeof(alert_buf), ", type=%d", tls_session->info.handshake_type); + str_details1 = alert_buf; + break; + } + } + } + + snprintf(tls_session->info.info_description, + sizeof(tls_session->info.info_description), + "%s %s%s%s%s", + str_write_p, str_version, str_content_type, + str_details1, str_details2); + + /* + * Cache the TLS session information in the session-state + * list, so it can be accessed by Post-Auth-Type + * Client-Lost { ... } + */ + vp = fr_pair_afrom_num(request->state_ctx, PW_TLS_SESSION_INFORMATION, 0); + if (vp) { + fr_pair_value_strcpy(vp, tls_session->info.info_description); + fr_pair_add(&request->state, vp); + } + + RDEBUG2("%s", tls_session->info.info_description); + + if (FROM_CLIENT && details) RDEBUG2("(TLS) The client is informing us that %s.", details); +} + +static CONF_PARSER cache_config[] = { + { "enable", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, session_cache_enable), "no" }, + + { "lifetime", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, session_lifetime), "24" }, + { "name", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, session_id_name), NULL }, + + { "max_entries", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, session_cache_size), "255" }, + { "persist_dir", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, session_cache_path), NULL }, + { "virtual_server", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, session_cache_server), NULL }, + CONF_PARSER_TERMINATOR +}; + +static CONF_PARSER verify_config[] = { + { "skip_if_ocsp_ok", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, verify_skip_if_ocsp_ok), "no" }, + { "tmpdir", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, verify_tmp_dir), NULL }, + { "client", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, verify_client_cert_cmd), NULL }, + CONF_PARSER_TERMINATOR +}; + +#ifdef HAVE_OPENSSL_OCSP_H +static CONF_PARSER ocsp_config[] = { + { "enable", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, ocsp_enable), "no" }, + { "override_cert_url", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, ocsp_override_url), "no" }, + { "url", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, ocsp_url), NULL }, + { "use_nonce", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, ocsp_use_nonce), "yes" }, + { "timeout", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, ocsp_timeout), "yes" }, + { "softfail", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, ocsp_softfail), "no" }, + CONF_PARSER_TERMINATOR +}; +#endif + +static CONF_PARSER tls_server_config[] = { + { "verify_depth", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, verify_depth), "0" }, + { "CA_path", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT | PW_TYPE_DEPRECATED, fr_tls_server_conf_t, ca_path), NULL }, + { "ca_path", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, fr_tls_server_conf_t, ca_path), NULL }, + { "pem_file_type", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, file_type), "yes" }, + { "private_key_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, fr_tls_server_conf_t, private_key_file), NULL }, + { "certificate_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, fr_tls_server_conf_t, certificate_file), NULL }, + { "CA_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT | PW_TYPE_DEPRECATED, fr_tls_server_conf_t, ca_file), NULL }, + { "ca_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, fr_tls_server_conf_t, ca_file), NULL }, + { "private_key_password", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_SECRET, fr_tls_server_conf_t, private_key_password), NULL }, +#ifdef PSK_MAX_IDENTITY_LEN + { "psk_identity", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, psk_identity), NULL }, + { "psk_hexphrase", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_SECRET, fr_tls_server_conf_t, psk_password), NULL }, + { "psk_query", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, psk_query), NULL }, +#endif + { "dh_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, fr_tls_server_conf_t, dh_file), NULL }, + { "random_file", FR_CONF_OFFSET(PW_TYPE_FILE_EXISTS, fr_tls_server_conf_t, random_file), NULL }, + { "fragment_size", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, fragment_size), "1024" }, + { "include_length", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, include_length), "yes" }, + { "auto_chain", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, auto_chain), "yes" }, + { "disable_single_dh_use", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, disable_single_dh_use), NULL }, + { "check_crl", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, check_crl), "no" }, +#ifdef X509_V_FLAG_CRL_CHECK_ALL + { "check_all_crl", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, check_all_crl), "no" }, +#endif + { "ca_path_reload_interval", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, ca_path_reload_interval), "0" }, + { "allow_expired_crl", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, allow_expired_crl), NULL }, + { "check_cert_cn", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, check_cert_cn), NULL }, + { "cipher_list", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, cipher_list), NULL }, + { "cipher_server_preference", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, cipher_server_preference), NULL }, + { "check_cert_issuer", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, check_cert_issuer), NULL }, + { "require_client_cert", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, require_client_cert), NULL }, + +#if OPENSSL_VERSION_NUMBER >= 0x10101000L + { "sigalgs_list", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, sigalgs_list), NULL }, +#endif + +#if OPENSSL_VERSION_NUMBER >= 0x10100000L + { "reject_unknown_intermediate_ca", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, disallow_untrusted), .dflt = "no", }, +#endif + +#if OPENSSL_VERSION_NUMBER >= 0x0090800fL +#ifndef OPENSSL_NO_ECDH + { "ecdh_curve", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, ecdh_curve), "prime256v1" }, +#endif +#endif + +#ifdef SSL_OP_NO_TLSv1 + { "disable_tlsv1", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, disable_tlsv1), NULL }, +#endif + +#ifdef SSL_OP_NO_TLSv1_1 + { "disable_tlsv1_1", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, disable_tlsv1_1), NULL }, +#endif + +#ifdef SSL_OP_NO_TLSv1_2 + { "disable_tlsv1_2", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, disable_tlsv1_2), NULL }, +#endif + + { "tls_max_version", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, tls_max_version), NULL }, + + { "tls_min_version", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, tls_min_version), +#if defined(TLS1_2_VERSION) + "1.2" +#elif defined(TLS1_1_VERSION) + "1.1" +#else + "1.0" +#endif + }, + +#ifdef WITH_RADIUSV11 + { "radiusv1_1", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, radiusv11_name), NULL }, +#endif + + { "realm_dir", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, realm_dir), NULL }, + + { "cache", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) cache_config }, + + { "verify", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) verify_config }, + +#ifdef HAVE_OPENSSL_OCSP_H + { "ocsp", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) ocsp_config }, +#endif + CONF_PARSER_TERMINATOR +}; + + +static CONF_PARSER tls_client_config[] = { + { "verify_depth", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, verify_depth), "0" }, + { "ca_path", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, fr_tls_server_conf_t, ca_path), NULL }, + { "pem_file_type", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, file_type), "yes" }, + { "private_key_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, fr_tls_server_conf_t, private_key_file), NULL }, + { "certificate_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, fr_tls_server_conf_t, certificate_file), NULL }, + { "ca_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, fr_tls_server_conf_t, ca_file), NULL }, + { "private_key_password", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_SECRET, fr_tls_server_conf_t, private_key_password), NULL }, + { "dh_file", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, dh_file), NULL }, + { "random_file", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, random_file), NULL }, + { "fragment_size", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, fragment_size), "1024" }, + { "include_length", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, include_length), "yes" }, + { "check_crl", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, check_crl), "no" }, + { "check_cert_cn", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, check_cert_cn), NULL }, + { "cipher_list", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, cipher_list), NULL }, + { "check_cert_issuer", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, check_cert_issuer), NULL }, + { "ca_path_reload_interval", FR_CONF_OFFSET(PW_TYPE_INTEGER, fr_tls_server_conf_t, ca_path_reload_interval), "0" }, + + { "fix_cert_order", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, fix_cert_order), NULL }, + +#if OPENSSL_VERSION_NUMBER >= 0x0090800fL +#ifndef OPENSSL_NO_ECDH + { "ecdh_curve", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, ecdh_curve), "prime256v1" }, +#endif +#endif + +#ifdef SSL_OP_NO_TLSv1 + { "disable_tlsv1", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, disable_tlsv1), NULL }, +#endif + +#ifdef SSL_OP_NO_TLSv1_1 + { "disable_tlsv1_1", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, disable_tlsv1_1), NULL }, +#endif + +#ifdef SSL_OP_NO_TLSv1_2 + { "disable_tlsv1_2", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, fr_tls_server_conf_t, disable_tlsv1_2), NULL }, +#endif + + { "tls_max_version", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, tls_max_version), NULL }, + + { "tls_min_version", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, tls_min_version), +#if defined(TLS1_2_VERSION) + "1.2" +#elif defined(TLS1_1_VERSION) + "1.1" +#else + "1.0" +#endif + }, + +#ifdef WITH_RADIUSV11 + { "radiusv1_1", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, radiusv11_name), NULL }, +#endif + + { "hostname", FR_CONF_OFFSET(PW_TYPE_STRING, fr_tls_server_conf_t, client_hostname), NULL }, + + CONF_PARSER_TERMINATOR +}; + + +/* + * TODO: Check for the type of key exchange * like conf->dh_key + */ +static int load_dh_params(SSL_CTX *ctx, char *file) +{ + DH *dh = NULL; + BIO *bio; + + /* + * Prior to trying to load the file, check what OpenSSL will do with it. + * + * Certain downstreams (such as RHEL) will ignore user-provided dhparams + * in FIPS mode, unless the specified parameters are FIPS-approved. + * However, since OpenSSL >= 1.1.1 will automatically select parameters + * anyways, there's no point in attempting to load them. + * + * Change suggested by @t8m + */ +#if OPENSSL_VERSION_NUMBER >= 0x10101000L + if (FIPS_mode() > 0) { + WARN(LOG_PREFIX ": Ignoring user-selected DH parameters in FIPS mode. Using defaults."); + file = NULL; + } + + /* + * No dh file, set auto context. + */ + if (!file) { + if (!SSL_CTX_set_dh_auto(ctx, 1)) { + ERROR(LOG_PREFIX ": Unable to set DH parameters"); + return -1; + } + + return 0; + } + + WARN(LOG_PREFIX ": Setting DH parameters from %s - this is no longer necessary.", file); + WARN(LOG_PREFIX ": You should comment out the 'dh_file' configuration item."); + +#else + if (!file) { + WARN(LOG_PREFIX ": Cannot set DH parameters. DH cipher suites may not work."); + return 0; + } +#endif + + + if ((bio = BIO_new_file(file, "r")) == NULL) { + ERROR(LOG_PREFIX ": Unable to open DH file - %s", file); + return -1; + } + + dh = PEM_read_bio_DHparams(bio, NULL, NULL, NULL); + BIO_free(bio); + if (!dh) { + WARN(LOG_PREFIX ": Unable to set DH parameters. DH cipher suites may not work!"); + WARN(LOG_PREFIX ": Fix this by running the OpenSSL command listed in eap.conf"); + return 0; + } + + if (SSL_CTX_set_tmp_dh(ctx, dh) < 0) { + ERROR(LOG_PREFIX ": Unable to set DH parameters"); + DH_free(dh); + return -1; + } + + DH_free(dh); + return 0; +} + + +/* + * Print debugging messages, and free data. + */ +static void cbtls_remove_session(SSL_CTX *ctx, SSL_SESSION *sess) +{ + char buffer[2 * MAX_SESSION_SIZE + 1]; + fr_tls_server_conf_t *conf; + + tls_session_id(sess, buffer, MAX_SESSION_SIZE); + + conf = (fr_tls_server_conf_t *)SSL_CTX_get_app_data(ctx); + if (!conf) { + DEBUG(LOG_PREFIX ": Failed to find TLS configuration in session"); + return; + } + + { + int rv; + char filename[3 * MAX_SESSION_SIZE + 1]; + + DEBUG2(LOG_PREFIX ": Removing session %s from the cache", buffer); + + /* remove session and any cached VPs */ + snprintf(filename, sizeof(filename), "%s%c%s.asn1", + conf->session_cache_path, FR_DIR_SEP, buffer); + rv = unlink(filename); + if (rv != 0) { + DEBUG2(LOG_PREFIX ": Could not remove persisted session file %s: %s", + filename, fr_syserror(errno)); + } + /* VPs might be absent; might not have been written to disk yet */ + snprintf(filename, sizeof(filename), "%s%c%s.vps", + conf->session_cache_path, FR_DIR_SEP, buffer); + unlink(filename); + } + + return; +} + +static int cbtls_new_session(SSL *ssl, SSL_SESSION *sess) +{ + char buffer[2 * MAX_SESSION_SIZE + 1]; + fr_tls_server_conf_t *conf; + unsigned char *sess_blob = NULL; + + REQUEST *request = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_REQUEST); + + conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_CONF); + if (!conf) { + RWDEBUG("(TLS) Failed to find TLS configuration in session"); + return 0; + } + + tls_session_id(sess, buffer, MAX_SESSION_SIZE); + + { + int fd, rv, todo, blob_len; + char filename[3 * MAX_SESSION_SIZE + 1]; + unsigned char *p; + + RDEBUG2("Serialising session %s, and storing in cache", buffer); + + /* find out what length data we need */ + blob_len = i2d_SSL_SESSION(sess, NULL); + if (blob_len < 1) { + /* something went wrong */ + if (request) RWDEBUG("(TLS) Session serialisation failed, could not determine required buffer length"); + return 0; + } + + /* Do not convert to TALLOC - Thread safety */ + /* alloc and convert to ASN.1 */ + sess_blob = malloc(blob_len); + if (!sess_blob) { + RWDEBUG("(TLS) Session serialisation failed, couldn't allocate buffer (%d bytes)", blob_len); + return 0; + } + /* openssl mutates &p */ + p = sess_blob; + rv = i2d_SSL_SESSION(sess, &p); + if (rv != blob_len) { + if (request) RWDEBUG("(TLS) Session serialisation failed"); + goto error; + } + + /* open output file */ + snprintf(filename, sizeof(filename), "%s%c%s.asn1", + conf->session_cache_path, FR_DIR_SEP, buffer); + fd = open(filename, O_RDWR|O_CREAT|O_EXCL, S_IWUSR); + if (fd < 0) { + if (request) RERROR("(TLS) Session serialisation failed, failed opening session file %s: %s", + filename, fr_syserror(errno)); + goto error; + } + + /* + * Set the filename to be temporarily write-only. + */ + if (request) { + VALUE_PAIR *vp; + + vp = fr_pair_afrom_num(request->state_ctx, PW_TLS_CACHE_FILENAME, 0); + if (vp) { + fr_pair_value_strcpy(vp, filename); + fr_pair_add(&request->state, vp); + } + } + + todo = blob_len; + p = sess_blob; + while (todo > 0) { + rv = write(fd, p, todo); + if (rv < 1) { + if (request) RWDEBUG("(TLS) Failed writing session: %s", fr_syserror(errno)); + close(fd); + goto error; + } + p += rv; + todo -= rv; + } + close(fd); + if (request) RWDEBUG("(TLS) Wrote session %s to %s (%d bytes)", buffer, filename, blob_len); + } + +error: + free(sess_blob); + + return 0; +} + +/** Convert OpenSSL's ASN1_TIME to an epoch time + * + * @param[out] out Where to write the time_t. + * @param[in] asn1 The ASN1_TIME to convert. + * @return + * - 0 success. + * - -1 on failure. + */ +static int ocsp_asn1time_to_epoch(time_t *out, char const *asn1) +{ + struct tm t; + char const *p = asn1, *end = p + strlen(p); + + memset(&t, 0, sizeof(t)); + + if ((end - p) <= 13) { + if ((end - p) < 2) { + fr_strerror_printf("ASN1 date string too short, expected 2 additional bytes, got %zu bytes", + end - p); + return -1; + } + + t.tm_year = (*(p++) - '0') * 10; + t.tm_year += (*(p++) - '0'); + if (t.tm_year < 70) t.tm_year += 100; + } else { + t.tm_year = (*(p++) - '0') * 1000; + t.tm_year += (*(p++) - '0') * 100; + t.tm_year += (*(p++) - '0') * 10; + t.tm_year += (*(p++) - '0'); + t.tm_year -= 1900; + } + + if ((end - p) < 4) { + fr_strerror_printf("ASN1 string too short, expected 10 additional bytes, got %zu bytes", + end - p); + return -1; + } + + t.tm_mon = (*(p++) - '0') * 10; + t.tm_mon += (*(p++) - '0') - 1; // -1 since January is 0 not 1. + t.tm_mday = (*(p++) - '0') * 10; + t.tm_mday += (*(p++) - '0'); + + if ((end - p) < 2) goto done; + t.tm_hour = (*(p++) - '0') * 10; + t.tm_hour += (*(p++) - '0'); + + if ((end - p) < 2) goto done; + t.tm_min = (*(p++) - '0') * 10; + t.tm_min += (*(p++) - '0'); + + if ((end - p) < 2) goto done; + t.tm_sec = (*(p++) - '0') * 10; + t.tm_sec += (*(p++) - '0'); + + /* Apparently OpenSSL converts all timestamps to UTC? Maybe? */ +done: + *out = timegm(&t); + return 0; +} + +#if OPENSSL_VERSION_NUMBER < 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER) +static SSL_SESSION *cbtls_get_session(SSL *ssl, unsigned char *data, int len, int *copy) +#else +static SSL_SESSION *cbtls_get_session(SSL *ssl, const unsigned char *data, int len, int *copy) +#endif +{ + size_t size; + char buffer[2 * MAX_SESSION_SIZE + 1]; + fr_tls_server_conf_t *conf; + TALLOC_CTX *talloc_ctx; + + SSL_SESSION *sess = NULL; + unsigned char *sess_data = NULL; + PAIR_LIST *pairlist = NULL; + + REQUEST *request = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_REQUEST); + + rad_assert(request != NULL); + + size = len; + if (size > MAX_SESSION_SIZE) size = MAX_SESSION_SIZE; + + fr_bin2hex(buffer, data, size); + + RDEBUG2("Peer requested cached session: %s", buffer); + + *copy = 0; + + conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_CONF); + if (!conf) { + RWDEBUG("(TLS) Failed to find TLS configuration in session"); + return NULL; + } + + talloc_ctx = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_TALLOC); + + { + int rv, fd, todo; + char filename[3 * MAX_SESSION_SIZE + 1]; + + unsigned char const **o; + unsigned char **p; + uint8_t *q; + + struct stat st; + VALUE_PAIR *vps = NULL; + VALUE_PAIR *vp; + + /* load the actual SSL session */ + snprintf(filename, sizeof(filename), "%s%c%s.asn1", conf->session_cache_path, FR_DIR_SEP, buffer); + fd = open(filename, O_RDONLY); + if (fd < 0) { + RWDEBUG("(TLS) No persisted session file %s: %s", filename, fr_syserror(errno)); + goto error; + } + + rv = fstat(fd, &st); + if (rv < 0) { + RWDEBUG("(TLS) Failed stating persisted session file %s: %s", filename, fr_syserror(errno)); + close(fd); + goto error; + } + + sess_data = talloc_array(NULL, unsigned char, st.st_size); + if (!sess_data) { + RWDEBUG("(TLS) Failed allocating buffer for persisted session (%d bytes)", (int) st.st_size); + close(fd); + goto error; + } + + q = sess_data; + todo = st.st_size; + while (todo > 0) { + rv = read(fd, q, todo); + if (rv < 1) { + RWDEBUG("(TLS) Failed reading persisted session: %s", fr_syserror(errno)); + close(fd); + goto error; + } + todo -= rv; + q += rv; + } + close(fd); + + /* + * OpenSSL mutates what's passed in, so we assign sess_data to q, + * so the value of q gets mutated, and not the value of sess_data. + * + * We then need a pointer to hold &q, but it can't be const, because + * clang complains about lack of consting in nested pointer types. + * + * So we memcpy the value of that pointer, to one that + * does have a const, which we then pass into d2i_SSL_SESSION *sigh*. + */ + q = sess_data; + p = &q; + memcpy(&o, &p, sizeof(o)); + sess = d2i_SSL_SESSION(NULL, o, st.st_size); + if (!sess) { + RWDEBUG("(TLS) Failed loading persisted session: %s", ERR_error_string(ERR_get_error(), NULL)); + goto error; + } + + /* read in the cached VPs from the .vps file */ + snprintf(filename, sizeof(filename), "%s%c%s.vps", + conf->session_cache_path, FR_DIR_SEP, buffer); + rv = pairlist_read(talloc_ctx, filename, &pairlist, 1); + if (rv < 0) { + /* not safe to un-persist a session w/o VPs */ + RWDEBUG("(TLS) Failed loading persisted VPs for session %s", buffer); + SSL_SESSION_free(sess); + sess = NULL; + goto error; + } + + /* + * Enforce client certificate expiration. + */ + vp = fr_pair_find_by_num(pairlist->reply, PW_TLS_CLIENT_CERT_EXPIRATION, 0, TAG_ANY); + if (vp) { + time_t expires; + + if (ocsp_asn1time_to_epoch(&expires, vp->vp_strvalue) < 0) { + RDEBUG2("Failed getting certificate expiration, removing cache entry for session %s - %s", buffer, fr_strerror()); + SSL_SESSION_free(sess); + sess = NULL; + goto error; + } + + if (expires <= request->timestamp) { + RDEBUG2("Certificate has expired, removing cache entry for session %s", buffer); + SSL_SESSION_free(sess); + sess = NULL; + goto error; + } + + /* + * Account for Session-Timeout, if it's available. + */ + vp = fr_pair_find_by_num(request->reply->vps, PW_SESSION_TIMEOUT, 0, TAG_ANY); + if (vp) { + if ((request->timestamp + vp->vp_integer) > expires) { + vp->vp_integer = expires - request->timestamp; + RWDEBUG2("(TLS) Updating Session-Timeout to %u, due to impending certificate expiration", + vp->vp_integer); + } + } + } + + /* + * Resumption MUST use the same EAP type as from + * the original packet. + */ + vp = fr_pair_find_by_num(pairlist->reply, PW_EAP_TYPE, 0, TAG_ANY); + if (vp) { + VALUE_PAIR *type = fr_pair_find_by_num(request->packet->vps, PW_EAP_TYPE, 0, TAG_ANY); + + if (type && (type->vp_integer != vp->vp_integer)) { + REDEBUG("Resumption has changed EAP types for session %s", buffer); + REDEBUG("Rejecting session due to protocol violations"); + goto error; + } + } + + /* move the cached VPs into the session */ + fr_pair_list_mcopy_by_num(talloc_ctx, &vps, &pairlist->reply, 0, 0, TAG_ANY); + + SSL_SESSION_set_ex_data(sess, fr_tls_ex_index_vps, vps); + RDEBUG("Successfully restored session %s", buffer); + rdebug_pair_list(L_DBG_LVL_2, request, vps, "reply:"); + + /* + * The "restore VPs from OpenSSL cache" code is + * now in eaptls_process() + */ + } +error: + if (sess_data) talloc_free(sess_data); + if (pairlist) pairlist_free(&pairlist); + + return sess; +} + +static size_t tls_session_id_binary(SSL_SESSION *ssn, uint8_t *buffer, size_t bufsize) +{ +#if OPENSSL_VERSION_NUMBER < 0x10001000L + size_t size; + + size = ssn->session_id_length; + if (size > bufsize) size = bufsize; + + memcpy(buffer, ssn->session_id, size); + return size; +#else + unsigned int size; + uint8_t const *p; + + p = SSL_SESSION_get_id(ssn, &size); + if (size > bufsize) size = bufsize; + + memcpy(buffer, p, size); + return size; +#endif +} + +/* + * From TLS-Cache-Method + * + * All of the save / clear / load callbacks are done with any + * OpenSSL locks *unlocked*. So says the OpenSSL code. + */ +#define CACHE_SAVE (1) +#define CACHE_LOAD (2) +#define CACHE_CLEAR (3) +#define CACHE_REFRESH (4) + +static REQUEST *cache_init_fake_request(fr_tls_server_conf_t const *conf, SSL_SESSION *sess, SSL *ssl, + uint8_t const *data, size_t size) +{ + VALUE_PAIR *vp; + REQUEST *fake, *request = NULL; + uint8_t buffer[MAX_SESSION_SIZE]; + + if (sess) { + size = tls_session_id_binary(sess, buffer, sizeof(buffer)); + data = buffer; + } + + /* + * We get called essentially at random by OpenSSL, with + * no information other than the session ID. As a + * result, we have to manually set up our own request. + */ + if (ssl) request = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_REQUEST); + + if (request) { + fake = request_alloc_fake(request); + } else { + fake = request_alloc(NULL); + fake->packet = rad_alloc(fake, false); + fake->reply = rad_alloc(fake, false); + } + + vp = fr_pair_afrom_num(fake->packet, PW_TLS_SESSION_ID, 0); + if (!vp) { + talloc_free(fake); + return NULL; + } + + fr_pair_value_memcpy(vp, data, size); + fr_pair_add(&fake->packet->vps, vp); + + fake->server = conf->session_cache_server; + + return fake; +} + +/* + * Clear cached data + */ +static void cbtls_cache_clear(SSL_CTX *ctx, SSL_SESSION *sess) +{ + fr_tls_server_conf_t *conf; + REQUEST *fake; + + conf = (fr_tls_server_conf_t *)SSL_CTX_get_app_data(ctx); + if (!conf) { + DEBUG(LOG_PREFIX ": Failed to find TLS configuration in session"); + return; + } + + /* + * Find the SSL ID from the session, and delete it. + * + * Don't bother with any parent request. We're in a + * timer callback, and there is no request available. + */ + fake = cache_init_fake_request(conf, sess, NULL, NULL, 0); + if (!fake) return; + + /* + * Use &request:TLS-Session-Id to clear the cache entry. + */ + (void) process_post_auth(CACHE_CLEAR, fake); + talloc_free(fake); + return; +} + +/* + * OpenSSL calls this function in order to save the session + * BEFORE it has sent the final TLS success. So our process here + * is to say "yes, we saved it", and then do the *actual* saving + * after the TLS success has been sent. + */ +static int cbtls_cache_save(UNUSED SSL *ssl, UNUSED SSL_SESSION *sess) +{ + return 0; +} + +static int cbtls_cache_save_vps(SSL *ssl, SSL_SESSION *sess, VALUE_PAIR *vps) +{ + fr_tls_server_conf_t *conf; + VALUE_PAIR *vp; + REQUEST *fake = NULL; + size_t size, rv; + uint8_t *p, *sess_blob = NULL; + + conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_CONF); + if (!conf) return 0; + + /* + * Find the SSL ID from the session, and save it. + * + * Save anything from the parent request. + */ + fake = cache_init_fake_request(conf, sess, ssl, NULL, 0); + if (!fake) return 0; + + /* find out what length data we need */ + size = i2d_SSL_SESSION(sess, NULL); + if (size < 1) return 0; + + /* Do not convert to TALLOC - it's passed to OpenSSL */ + /* alloc and convert to ASN.1 */ + MEM(sess_blob = malloc(size)); + + /* openssl mutates &p */ + p = sess_blob; + rv = i2d_SSL_SESSION(sess, &p); + if (rv != size) goto error; + + vp = fr_pair_afrom_num(fake->state_ctx, PW_TLS_SESSION_DATA, 0); + if (!vp) goto error; + + fr_pair_value_memcpy(vp, sess_blob, size); + fr_pair_add(&fake->state, vp); + + if (vps) fr_pair_add(&fake->reply->vps, fr_pair_list_copy(fake->reply, vps)); + + /* + * Use &request:TLS-Session-Id to save the + * &session-state:TLS-Session-Data values. + * + * The current &reply: list is the list of VPs which + * should be cached. + * + * Any other attributes which need to be saved can be + * read from the &outer.reply: list. + */ + (void) process_post_auth(CACHE_SAVE, fake); + +error: + if (fake) talloc_free(fake); + free(sess_blob); + + return 0; +} + +static int cbtls_cache_refresh(SSL *ssl, SSL_SESSION *sess) +{ + fr_tls_server_conf_t *conf; + REQUEST *fake = NULL; + + conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_CONF); + if (!conf) return 0; + + /* + * Find the SSL ID from the session, and save it. + * + * Save anything from the parent request. + */ + fake = cache_init_fake_request(conf, sess, ssl, NULL, 0); + if (!fake) return 0; + /* + * Use &request:TLS-Session-Id to update the cache + * entry so that it doesn't not expire. + */ + (void) process_post_auth(CACHE_REFRESH, fake); + + talloc_free(fake); + + return 0; +} + +#if OPENSSL_VERSION_NUMBER < 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER) +static SSL_SESSION *cbtls_cache_load(SSL *ssl, unsigned char *data, int len, int *copy) +#else +static SSL_SESSION *cbtls_cache_load(SSL *ssl, const unsigned char *data, int len, int *copy) +#endif +{ + fr_tls_server_conf_t *conf; + size_t size; + uint8_t const *p; + VALUE_PAIR *vp, *vps; + TALLOC_CTX *talloc_ctx; + SSL_SESSION *sess = NULL; + REQUEST *fake = NULL; + REQUEST *request = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_REQUEST); + char buffer[2 * MAX_SESSION_SIZE + 1]; + + conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_CONF); + if (!conf) return NULL; + + rad_assert(request); + + size = len; + if (size > MAX_SESSION_SIZE) size = MAX_SESSION_SIZE; + + if (fr_debug_lvl > 1) { + fr_bin2hex(buffer, data, size); + RDEBUG2("Peer requested cached session: %s", buffer); + } + + *copy = 0; + + /* + * Take the given SSL ID, and create a fake request. + * + * Don't bother parenting it from another request. We do + * this for a number of reasons. + * + * One is that rest of the code expects that the VPs will + * be added to fr_tls_ex_index_vps. So we don't want to + * be poking the request directly, as that will result in + * a change of behavior. + * + * The larger reason is that we do _not_ want to actually + * update the reply, until such time as we know that the + * user has been authenticated. + */ + fake = cache_init_fake_request(conf, NULL, NULL, data, size); + if (!fake) return 0; + + /* + * Use &request:TLS-Session-Id to load the cached + * session. + * + * The "cache load { ...}" section should put the reply + * attributes into the &reply: list, and the + * &session-state:TLS-Session-Data attribute. + * + * Why? Because v4 does it that way, and there aren't + * really good reasons for doing it differently. + */ + (void) process_post_auth(CACHE_LOAD, fake); + + /* + * Enforce client certificate expiration. + */ + vp = fr_pair_find_by_num(fake->reply->vps, PW_TLS_CLIENT_CERT_EXPIRATION, 0, TAG_ANY); + if (vp) { + time_t expires; + + if (ocsp_asn1time_to_epoch(&expires, vp->vp_strvalue) < 0) { + RDEBUG2("Failed getting certificate expiration, removing cache entry for session %s - %s", buffer, fr_strerror()); + SSL_SESSION_free(sess); + sess = NULL; + goto error; + } + + if (expires <= request->timestamp) { + RDEBUG2("Certificate has expired, removing cache entry for session %s", buffer); + SSL_SESSION_free(sess); + sess = NULL; + goto error; + } + + /* + * Account for Session-Timeout, if it's available. + */ + vp = fr_pair_find_by_num(request->reply->vps, PW_SESSION_TIMEOUT, 0, TAG_ANY); + if (vp) { + if ((request->timestamp + vp->vp_integer) > expires) { + vp->vp_integer = expires - request->timestamp; + RWDEBUG2("(TLS) Updating Session-Timeout to %u, due to impending certificate expiration", + vp->vp_integer); + } + } + } + + /* + * Try to de-serialize the session data. + */ + vp = fr_pair_find_by_num(fake->state, PW_TLS_SESSION_DATA, 0, TAG_ANY); + if (!vp) { + RWDEBUG("(TLS) Failed to find TLS-Session-Data in 'session-state' list for session %s", buffer); + goto error; + } + + /* + * OpenSSL mutates what's passed in, so we assign sess_data to q, + * so the value of q gets mutated, and not the value of sess_data. + * + * We then need a pointer to hold &q, but it can't be const, because + * clang complains about lack of consting in nested pointer types. + * + * So we memcpy the value of that pointer, to one that + * does have a const, which we then pass into d2i_SSL_SESSION *sigh*. + */ + p = vp->vp_octets; + sess = d2i_SSL_SESSION(NULL, &p, vp->vp_length); + if (!sess) { + RWDEBUG("(TLS) Failed loading persisted session: %s", ERR_error_string(ERR_get_error(), NULL)); + goto error; + } + + talloc_ctx = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_TALLOC); + vps = NULL; + + /* move the cached VPs into the session */ + fr_pair_list_mcopy_by_num(talloc_ctx, &vps, &fake->reply->vps, 0, 0, TAG_ANY); + + SSL_SESSION_set_ex_data(sess, fr_tls_ex_index_vps, vps); + RDEBUG("Successfully restored session %s", buffer); + rdebug_pair_list(L_DBG_LVL_2, request, vps, "reply:"); + + /* + * The "restore VPs from OpenSSL cache" code is + * now in eaptls_process() + */ + +error: + if (fake) talloc_free(fake); + + return sess; +} + +#ifdef HAVE_OPENSSL_OCSP_H + +/** Extract components of OCSP responser URL from a certificate + * + * @param[in] cert to extract URL from. + * @param[out] host_out Portion of the URL (must be freed with free()). + * @param[out] port_out Port portion of the URL (must be freed with free()). + * @param[out] path_out Path portion of the URL (must be freed with free()). + * @param[out] is_https Whether the responder should be contacted using https. + * @return + * - 0 if no valid URL is contained in the certificate. + * - 1 if a URL was found and parsed. + * - -1 if at least one URL was found, but none could be parsed. + */ +static int ocsp_parse_cert_url(X509 *cert, char **host_out, char **port_out, + char **path_out, int *is_https) +{ + int i; + bool found_uri = false; + + AUTHORITY_INFO_ACCESS *aia; + ACCESS_DESCRIPTION *ad; + + aia = X509_get_ext_d2i(cert, NID_info_access, NULL, NULL); + + for (i = 0; i < sk_ACCESS_DESCRIPTION_num(aia); i++) { + ad = sk_ACCESS_DESCRIPTION_value(aia, i); + if (OBJ_obj2nid(ad->method) != NID_ad_OCSP) continue; + if (ad->location->type != GEN_URI) continue; + found_uri = true; + + if (OCSP_parse_url((char *) ad->location->d.ia5->data, host_out, + port_out, path_out, is_https)) return 1; + } + return found_uri ? -1 : 0; +} + +/* + * This function sends a OCSP request to a defined OCSP responder + * and checks the OCSP response for correctness. + */ + +/* Maximum leeway in validity period: default 5 minutes */ +#define MAX_VALIDITY_PERIOD (5 * 60) + +typedef enum { + OCSP_STATUS_FAILED = 0, + OCSP_STATUS_OK = 1, + OCSP_STATUS_SKIPPED = 2, +} ocsp_status_t; + +static ocsp_status_t ocsp_check(REQUEST *request, X509_STORE *store, X509 *issuer_cert, X509 *client_cert, + fr_tls_server_conf_t *conf) +{ + OCSP_CERTID *certid; + OCSP_REQUEST *req; + OCSP_RESPONSE *resp = NULL; + OCSP_BASICRESP *bresp = NULL; + char *host = NULL; + char *port = NULL; + char *path = NULL; + char hostheader[1024]; + int use_ssl = -1; + long nsec = MAX_VALIDITY_PERIOD, maxage = -1; + BIO *cbio, *bio_out; + ocsp_status_t ocsp_status = OCSP_STATUS_FAILED; + int status; + ASN1_GENERALIZEDTIME *rev = NULL, *thisupd, *nextupd; + int reason; +#if OPENSSL_VERSION_NUMBER >= 0x1000003f + OCSP_REQ_CTX *ctx; + int rc; + struct timeval now; + struct timeval when; +#endif + VALUE_PAIR *vp; + + if (issuer_cert == NULL) { + RWDEBUG("(TLS) Could not get issuer certificate"); + goto skipped; + } + + /* + * Create OCSP Request + */ + certid = OCSP_cert_to_id(NULL, client_cert, issuer_cert); + req = OCSP_REQUEST_new(); + OCSP_request_add0_id(req, certid); + if (conf->ocsp_use_nonce) OCSP_request_add1_nonce(req, NULL, 8); + + /* + * Send OCSP Request and get OCSP Response + */ + + /* Get OCSP responder URL */ + if (conf->ocsp_override_url) { + char *url; + + use_ocsp_url: + memcpy(&url, &conf->ocsp_url, sizeof(url)); + /* Reading the libssl src, they do a strdup on the URL, so it could of been const *sigh* */ + OCSP_parse_url(url, &host, &port, &path, &use_ssl); + if (!host || !port || !path) { + RWDEBUG("(TLS) ocsp: Host or port or path missing from configured URL \"%s\". Not doing OCSP", url); + goto skipped; + } + } else { + int ret; + + ret = ocsp_parse_cert_url(client_cert, &host, &port, &path, &use_ssl); + switch (ret) { + case -1: + RWDEBUG("(TLS) ocsp: Invalid URL in certificate. Not doing OCSP"); + break; + + case 0: + if (conf->ocsp_url) { + RWDEBUG("(TLS) ocsp: No OCSP URL in certificate, falling back to configured URL"); + goto use_ocsp_url; + } + RWDEBUG("(TLS) ocsp: No OCSP URL in certificate. Not doing OCSP"); + goto skipped; + + case 1: + break; + } + } + + RDEBUG2("ocsp: Using responder URL \"http://%s:%s%s\"", host, port, path); + + /* Check host and port length are sane, then create Host: HTTP header */ + if ((strlen(host) + strlen(port) + 2) > sizeof(hostheader)) { + RWDEBUG("(TLS) ocsp: Host and port too long"); + goto skipped; + } + snprintf(hostheader, sizeof(hostheader), "%s:%s", host, port); + + /* Setup BIO socket to OCSP responder */ + cbio = BIO_new_connect(host); + + bio_out = NULL; + if (rad_debug_lvl) { + if (default_log.dst == L_DST_STDOUT) { + bio_out = BIO_new_fp(stdout, BIO_NOCLOSE); + } else if (default_log.dst == L_DST_STDERR) { + bio_out = BIO_new_fp(stderr, BIO_NOCLOSE); + } + } + + BIO_set_conn_port(cbio, port); +#if OPENSSL_VERSION_NUMBER < 0x1000003f + BIO_do_connect(cbio); + + /* Send OCSP request and wait for response */ + resp = OCSP_sendreq_bio(cbio, path, req); + if (!resp) { + REDEBUG("ocsp: Couldn't get OCSP response"); + ocsp_status = OCSP_STATUS_SKIPPED; + goto ocsp_end; + } +#else + if (conf->ocsp_timeout) + BIO_set_nbio(cbio, 1); + + rc = BIO_do_connect(cbio); + if ((rc <= 0) && ((!conf->ocsp_timeout) || !BIO_should_retry(cbio))) { + REDEBUG("ocsp: Couldn't connect to OCSP responder"); + ocsp_status = OCSP_STATUS_SKIPPED; + goto ocsp_end; + } + + ctx = OCSP_sendreq_new(cbio, path, NULL, -1); + if (!ctx) { + REDEBUG("ocsp: Couldn't create OCSP request"); + ocsp_status = OCSP_STATUS_SKIPPED; + goto ocsp_end; + } + + if (!OCSP_REQ_CTX_add1_header(ctx, "Host", hostheader)) { + REDEBUG("ocsp: Couldn't set Host header"); + ocsp_status = OCSP_STATUS_SKIPPED; + goto ocsp_end; + } + + if (!OCSP_REQ_CTX_set1_req(ctx, req)) { + REDEBUG("ocsp: Couldn't add data to OCSP request"); + ocsp_status = OCSP_STATUS_SKIPPED; + goto ocsp_end; + } + + gettimeofday(&when, NULL); + when.tv_sec += conf->ocsp_timeout; + + do { + rc = OCSP_sendreq_nbio(&resp, ctx); + if (conf->ocsp_timeout) { + gettimeofday(&now, NULL); + if (!timercmp(&now, &when, <)) + break; + } + } while ((rc == -1) && BIO_should_retry(cbio)); + + if (conf->ocsp_timeout && (rc == -1) && BIO_should_retry(cbio)) { + REDEBUG("ocsp: Response timed out"); + ocsp_status = OCSP_STATUS_SKIPPED; + goto ocsp_end; + } + + OCSP_REQ_CTX_free(ctx); + + if (rc == 0) { + REDEBUG("ocsp: Couldn't get OCSP response"); + ocsp_status = OCSP_STATUS_SKIPPED; + goto ocsp_end; + } +#endif + + /* Verify OCSP response status */ + status = OCSP_response_status(resp); + if (status != OCSP_RESPONSE_STATUS_SUCCESSFUL) { + REDEBUG("ocsp: Response status: %s", OCSP_response_status_str(status)); + goto ocsp_end; + } + bresp = OCSP_response_get1_basic(resp); + if (!bresp) { + RDEBUG("ocsp: Failed parsing response"); + goto ocsp_end; + } + + if (conf->ocsp_use_nonce && OCSP_check_nonce(req, bresp)!=1) { + REDEBUG("ocsp: Response has wrong nonce value"); + goto ocsp_end; + } + if (OCSP_basic_verify(bresp, NULL, store, 0)!=1){ + REDEBUG("ocsp: Couldn't verify OCSP basic response"); + goto ocsp_end; + } + + /* Verify OCSP cert status */ + if (!OCSP_resp_find_status(bresp, certid, &status, &reason, &rev, &thisupd, &nextupd)) { + REDEBUG("ocsp: No Status found"); + goto ocsp_end; + } + + if (!OCSP_check_validity(thisupd, nextupd, nsec, maxage)) { + if (bio_out) { + BIO_puts(bio_out, "WARNING: Status times invalid.\n"); + ERR_print_errors(bio_out); + } + goto ocsp_end; + } + + if (bio_out) { + BIO_puts(bio_out, "\tThis Update: "); + ASN1_GENERALIZEDTIME_print(bio_out, thisupd); + BIO_puts(bio_out, "\n"); + if (nextupd) { + BIO_puts(bio_out, "\tNext Update: "); + ASN1_GENERALIZEDTIME_print(bio_out, nextupd); + BIO_puts(bio_out, "\n"); + } + } + + switch (status) { + case V_OCSP_CERTSTATUS_GOOD: + RDEBUG2("ocsp: Cert status: good"); + vp = pair_make_request("TLS-OCSP-Cert-Valid", NULL, T_OP_SET); + vp->vp_integer = 1; /* yes */ + ocsp_status = OCSP_STATUS_OK; + break; + + default: + /* REVOKED / UNKNOWN */ + REDEBUG("ocsp: Cert status: %s", OCSP_cert_status_str(status)); + if (reason != -1) REDEBUG("ocsp: Reason: %s", OCSP_crl_reason_str(reason)); + + if (bio_out && rev) { + BIO_puts(bio_out, "\tRevocation Time: "); + ASN1_GENERALIZEDTIME_print(bio_out, rev); + BIO_puts(bio_out, "\n"); + } + break; + } + +ocsp_end: + /* Free OCSP Stuff */ + OCSP_REQUEST_free(req); + OCSP_RESPONSE_free(resp); + free(host); + free(port); + free(path); + BIO_free_all(cbio); + if (bio_out) BIO_free(bio_out); + OCSP_BASICRESP_free(bresp); + + switch (ocsp_status) { + case OCSP_STATUS_OK: + RDEBUG2("ocsp: Certificate is valid"); + break; + + case OCSP_STATUS_SKIPPED: + skipped: + vp = pair_make_request("TLS-OCSP-Cert-Valid", NULL, T_OP_SET); + vp->vp_integer = 2; /* skipped */ + if (conf->ocsp_softfail) { + RWDEBUG("(TLS) ocsp: Unable to check certificate, assuming it's valid"); + RWDEBUG("(TLS) ocsp: This may be insecure"); + + /* Remove OpenSSL errors from queue or handshake will fail */ + while (ERR_get_error()); + + ocsp_status = OCSP_STATUS_SKIPPED; + } else { + REDEBUG("(TLS) ocsp: Unable to check certificate, failing"); + ocsp_status = OCSP_STATUS_FAILED; + } + break; + + default: + vp = pair_make_request("TLS-OCSP-Cert-Valid", NULL, T_OP_SET); + vp->vp_integer = 0; /* no */ + REDEBUG("(TLS) ocsp: Certificate has been expired/revoked"); + break; + } + + return ocsp_status; +} +#endif /* HAVE_OPENSSL_OCSP_H */ + +/* + * For creating certificate attributes. + */ +static char const *cert_attr_names[9][2] = { + { "TLS-Client-Cert-Serial", "TLS-Cert-Serial" }, + { "TLS-Client-Cert-Expiration", "TLS-Cert-Expiration" }, + { "TLS-Client-Cert-Subject", "TLS-Cert-Subject" }, + { "TLS-Client-Cert-Issuer", "TLS-Cert-Issuer" }, + { "TLS-Client-Cert-Common-Name", "TLS-Cert-Common-Name" }, + { "TLS-Client-Cert-Subject-Alt-Name-Email", "TLS-Cert-Subject-Alt-Name-Email" }, + { "TLS-Client-Cert-Subject-Alt-Name-Dns", "TLS-Cert-Subject-Alt-Name-Dns" }, + { "TLS-Client-Cert-Subject-Alt-Name-Upn", "TLS-Cert-Subject-Alt-Name-Upn" }, + { "TLS-Client-Cert-Valid-Since", "TLS-Cert-Valid-Since" } +}; + +#define FR_TLS_SERIAL (0) +#define FR_TLS_EXPIRATION (1) +#define FR_TLS_SUBJECT (2) +#define FR_TLS_ISSUER (3) +#define FR_TLS_CN (4) +#define FR_TLS_SAN_EMAIL (5) +#define FR_TLS_SAN_DNS (6) +#define FR_TLS_SAN_UPN (7) +#define FR_TLS_VALID_SINCE (8) + +static const char *cert_names[2] = { + "client", "server", +}; + +/* + * Before trusting a certificate, you must make sure that the + * certificate is 'valid'. There are several steps that your + * application can take in determining if a certificate is + * valid. Commonly used steps are: + * + * 1.Verifying the certificate's signature, and verifying that + * the certificate has been issued by a trusted Certificate + * Authority. + * + * 2.Verifying that the certificate is valid for the present date + * (i.e. it is being presented within its validity dates). + * + * 3.Verifying that the certificate has not been revoked by its + * issuing Certificate Authority, by checking with respect to a + * Certificate Revocation List (CRL). + * + * 4.Verifying that the credentials presented by the certificate + * fulfill additional requirements specific to the application, + * such as with respect to access control lists or with respect + * to OCSP (Online Certificate Status Processing). + * + * NOTE: This callback will be called multiple times based on the + * depth of the root certificate chain + */ +int cbtls_verify(int ok, X509_STORE_CTX *ctx) +{ + char subject[1024]; /* Used for the subject name */ + char issuer[1024]; /* Used for the issuer name */ + char attribute[1024]; + char value[1024]; + char common_name[1024]; + char cn_str[1024]; + char buf[64]; + X509 *client_cert; +#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER) + const STACK_OF(X509_EXTENSION) *ext_list; +#else + STACK_OF(X509_EXTENSION) *ext_list; +#endif + SSL *ssl; + int err, depth, lookup, loc; + fr_tls_server_conf_t *conf; + int my_ok = ok; + + ASN1_INTEGER *sn = NULL; + ASN1_TIME *asn_time = NULL; + VALUE_PAIR **certs; + char **identity; +#ifdef HAVE_OPENSSL_OCSP_H + X509_STORE *ocsp_store = NULL; + X509 *issuer_cert; + bool do_verify = false; +#endif + VALUE_PAIR *vp; + TALLOC_CTX *talloc_ctx; + + REQUEST *request; + + client_cert = X509_STORE_CTX_get_current_cert(ctx); + err = X509_STORE_CTX_get_error(ctx); + depth = X509_STORE_CTX_get_error_depth(ctx); + + lookup = depth; + + /* + * Retrieve the pointer to the SSL of the connection currently treated + * and the application specific data stored into the SSL object. + */ + ssl = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx()); + conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_CONF); + if (!conf) return 1; + + request = (REQUEST *)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_REQUEST); + rad_assert(request != NULL); + certs = (VALUE_PAIR **)SSL_get_ex_data(ssl, fr_tls_ex_index_certs); + + identity = (char **)SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_IDENTITY); +#ifdef HAVE_OPENSSL_OCSP_H + ocsp_store = conf->ocsp_store; +#endif + + talloc_ctx = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_TALLOC); + + /* + * Log client/issuing cert. If there's an error, log + * issuing cert. + * + * Inbound: 0 = client, 1 = server (intermediate CA), 2 = issuing CA + * Outbound: 0 = server, 2 = issuing CA. + * + * Our array of certificates uses 0 for client, and 1 for server. We + * also ignore subsequent certs. + */ + if (lookup > 1) { + if (!my_ok) lookup = 1; + + } else if (lookup == 0) { + /* + * This flag is only set for outbound + * connections. And then allows us to remap SSL + * offset 0 (server) to our offset 1 (also + * server). + */ + lookup = (SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_FIX_CERT_ORDER) != NULL); + } + + /* + * Get the Serial Number + */ + buf[0] = '\0'; + sn = X509_get_serialNumber(client_cert); + + RDEBUG2("(TLS) Creating attributes from %s certificate", cert_names[lookup ]); + RINDENT(); + + /* + * For this next bit, we create the attributes *only* if + * we're at the client or issuing certificate. + */ + if (certs && + (lookup <= 1) && sn && ((size_t) sn->length < (sizeof(buf) / 2))) { + char *p = buf; + int i; + + for (i = 0; i < sn->length; i++) { + sprintf(p, "%02x", (unsigned int)sn->data[i]); + p += 2; + } + vp = fr_pair_make(talloc_ctx, certs, cert_attr_names[FR_TLS_SERIAL][lookup], buf, T_OP_SET); + rdebug_pair(L_DBG_LVL_2, request, vp, NULL); + } + + /* + * Get the Expiration Date + */ + buf[0] = '\0'; + asn_time = X509_get_notAfter(client_cert); + if (certs && (lookup <= 1) && asn_time && + (asn_time->length < (int) sizeof(buf))) { + memcpy(buf, (char*) asn_time->data, asn_time->length); + buf[asn_time->length] = '\0'; + vp = fr_pair_make(talloc_ctx, certs, cert_attr_names[FR_TLS_EXPIRATION][lookup], buf, T_OP_SET); + rdebug_pair(L_DBG_LVL_2, request, vp, NULL); + } + + /* + * Get the Valid Since Date + */ + buf[0] = '\0'; + asn_time = X509_get_notBefore(client_cert); + if (certs && (lookup <= 1) && asn_time && + (asn_time->length < (int) sizeof(buf))) { + memcpy(buf, (char*) asn_time->data, asn_time->length); + buf[asn_time->length] = '\0'; + vp = fr_pair_make(talloc_ctx, certs, cert_attr_names[FR_TLS_VALID_SINCE][lookup], buf, T_OP_SET); + rdebug_pair(L_DBG_LVL_2, request, vp, NULL); + } + + /* + * Get the Subject & Issuer + */ + subject[0] = issuer[0] = '\0'; + X509_NAME_oneline(X509_get_subject_name(client_cert), subject, + sizeof(subject)); + subject[sizeof(subject) - 1] = '\0'; + if (certs && (lookup <= 1) && subject[0]) { + vp = fr_pair_make(talloc_ctx, certs, cert_attr_names[FR_TLS_SUBJECT][lookup], subject, T_OP_SET); + rdebug_pair(L_DBG_LVL_2, request, vp, NULL); + } + + X509_NAME_oneline(X509_get_issuer_name(client_cert), issuer, + sizeof(issuer)); + issuer[sizeof(issuer) - 1] = '\0'; + if (certs && (lookup <= 1) && issuer[0]) { + vp = fr_pair_make(talloc_ctx, certs, cert_attr_names[FR_TLS_ISSUER][lookup], issuer, T_OP_SET); + rdebug_pair(L_DBG_LVL_2, request, vp, NULL); + } + + /* + * Get the Common Name, if there is a subject. + */ + X509_NAME_get_text_by_NID(X509_get_subject_name(client_cert), + NID_commonName, common_name, sizeof(common_name)); + common_name[sizeof(common_name) - 1] = '\0'; + if (certs && (lookup <= 1) && common_name[0] && subject[0]) { + vp = fr_pair_make(talloc_ctx, certs, cert_attr_names[FR_TLS_CN][lookup], common_name, T_OP_SET); + rdebug_pair(L_DBG_LVL_2, request, vp, NULL); + } + + /* + * Get the RFC822 Subject Alternative Name + */ + loc = X509_get_ext_by_NID(client_cert, NID_subject_alt_name, -1); + if (certs && (lookup <= 1) && (loc >= 0)) { + X509_EXTENSION *ext = NULL; + GENERAL_NAMES *names = NULL; + int i; + + if ((ext = X509_get_ext(client_cert, loc)) && + (names = X509V3_EXT_d2i(ext))) { + for (i = 0; i < sk_GENERAL_NAME_num(names); i++) { + GENERAL_NAME *name = sk_GENERAL_NAME_value(names, i); + + switch (name->type) { +#ifdef GEN_EMAIL + case GEN_EMAIL: + vp = fr_pair_make(talloc_ctx, certs, cert_attr_names[FR_TLS_SAN_EMAIL][lookup], + (char const *) ASN1_STRING_get0_data(name->d.rfc822Name), T_OP_SET); + rdebug_pair(L_DBG_LVL_2, request, vp, NULL); + break; +#endif /* GEN_EMAIL */ +#ifdef GEN_DNS + case GEN_DNS: + vp = fr_pair_make(talloc_ctx, certs, cert_attr_names[FR_TLS_SAN_DNS][lookup], + (char const *) ASN1_STRING_get0_data(name->d.dNSName), T_OP_SET); + rdebug_pair(L_DBG_LVL_2, request, vp, NULL); + break; +#endif /* GEN_DNS */ +#ifdef GEN_OTHERNAME + case GEN_OTHERNAME: + /* look for a MS UPN */ + if (NID_ms_upn == OBJ_obj2nid(name->d.otherName->type_id)) { + /* we've got a UPN - Must be ASN1-encoded UTF8 string */ + if (name->d.otherName->value->type == V_ASN1_UTF8STRING) { + vp = fr_pair_make(talloc_ctx, certs, cert_attr_names[FR_TLS_SAN_UPN][lookup], + (char const *) ASN1_STRING_get0_data(name->d.otherName->value->value.utf8string), T_OP_SET); + rdebug_pair(L_DBG_LVL_2, request, vp, NULL); + break; + } else { + RWARN("Invalid UPN in Subject Alt Name (should be UTF-8)"); + break; + } + } + break; +#endif /* GEN_OTHERNAME */ + default: + /* XXX TODO handle other SAN types */ + break; + } + } + } + if (names != NULL) + GENERAL_NAMES_free(names); + } + + /* + * If the CRL has expired, that might still be OK. + */ + if (!my_ok && + (conf->allow_expired_crl) && + (err == X509_V_ERR_CRL_HAS_EXPIRED)) { + my_ok = 1; + X509_STORE_CTX_set_error( ctx, 0 ); + } + + if (!my_ok) { + char const *p = X509_verify_cert_error_string(err); + RERROR("(TLS) OpenSSL says error %d : %s", err, p); + REXDENT(); + + /* + * Copy certs even on failure so that they can be logged. + */ + if (certs && request) fr_pair_add(&request->packet->vps, fr_pair_list_copy(request->packet, *certs)); + + return my_ok; + } + + if (lookup == 0) { +#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER) + ext_list = X509_get0_extensions(client_cert); +#else + X509_CINF *client_inf; + client_inf = client_cert->cert_info; + ext_list = client_inf->extensions; +#endif + } else { + ext_list = NULL; + } + + /* + * Grab the X509 extensions, and create attributes out of them. + * For laziness, we re-use the OpenSSL names + */ + if (certs && (sk_X509_EXTENSION_num(ext_list) > 0)) { + int i, len; + EXTENDED_KEY_USAGE *eku; + char *p; + BIO *out; + + out = BIO_new(BIO_s_mem()); + strlcpy(attribute, "TLS-Client-Cert-", sizeof(attribute)); + + for (i = 0; i < sk_X509_EXTENSION_num(ext_list); i++) { + ASN1_OBJECT *obj; + X509_EXTENSION *ext; + + ext = sk_X509_EXTENSION_value(ext_list, i); + + obj = X509_EXTENSION_get_object(ext); + i2a_ASN1_OBJECT(out, obj); + len = BIO_read(out, attribute + 16 , sizeof(attribute) - 16 - 1); + if (len <= 0) continue; + + attribute[16 + len] = '\0'; + + for (p = attribute + 16; *p != '\0'; p++) { + if (*p == ' ') *p = '-'; + } + + if (X509V3_EXT_get(ext)) { /* Known extension, converting value into plain string */ + X509V3_EXT_print(out, ext, 0, 0); + len = BIO_read(out, value, sizeof(value) - 1); + if (len <= 0) continue; + value[len] = '\0'; + } else { + /* + * An extension not known to OpenSSL, dump it's value as a value of an unknown attribute. + */ + value[0] = '0'; + value[1] = 'x'; + const unsigned char *srcp; +#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER) + const ASN1_STRING *srcasn1p; + srcasn1p = X509_EXTENSION_get_data(ext); + srcp = ASN1_STRING_get0_data(srcasn1p); +#else + ASN1_STRING *srcasn1p; + srcasn1p = X509_EXTENSION_get_data(ext); + srcp = ASN1_STRING_data(srcasn1p); +#endif + int asn1len = ASN1_STRING_length(srcasn1p); + /* 3 comes from '0x' + \0 */ + if ((size_t)(asn1len << 1) >= sizeof(value) - 3) { + RDEBUG("Value of '%s' attribute is too long to be stored, it will be truncated", attribute); + asn1len = (sizeof(value) - 3) >> 1; + } + fr_bin2hex(value + 2, srcp, asn1len); + } + + vp = fr_pair_make(talloc_ctx, certs, attribute, value, T_OP_ADD); + if (!vp) { + RDEBUG3("Skipping %s += '%s'. Please check that both the " + "attribute and value are defined in the dictionaries", + attribute, value); + } else { + /* + * rdebug_pair_list indents (so pre REXDENT()) + */ + REXDENT(); + rdebug_pair_list(L_DBG_LVL_2, request, vp, NULL); + RINDENT(); + } + } + + BIO_free_all(out); + + /* Export raw EKU OIDs to allow matching a single OID regardless of its name */ + eku = X509_get_ext_d2i(client_cert, NID_ext_key_usage, NULL, NULL); + if (eku != NULL) { + for (i = 0; i < sk_ASN1_OBJECT_num(eku); i++) { + len = OBJ_obj2txt(value, sizeof(value), sk_ASN1_OBJECT_value(eku, i), 1); + if ((len > 0) && ((unsigned) len < sizeof(value))) { + vp = fr_pair_make(talloc_ctx, certs, + "TLS-Client-Cert-X509v3-Extended-Key-Usage-OID", + value, T_OP_ADD); + rdebug_pair(L_DBG_LVL_2, request, vp, NULL); + } + else { + RDEBUG("Failed to get EKU OID at index %d", i); + } + } + EXTENDED_KEY_USAGE_free(eku); + } + } + + REXDENT(); + + switch (X509_STORE_CTX_get_error(ctx)) { + case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT: + RERROR("(TLS) unable to get issuer certificate for issuer=%s", issuer); + break; + + case X509_V_ERR_CERT_NOT_YET_VALID: + RERROR("(TLS) Failed with certificate not yet valid."); + break; + + case X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD: + RERROR("(TLS) Failed with error in certificate 'not before' field."); +#if 0 + ASN1_TIME_print(bio_err, X509_get_notBefore(ctx->current_cert)); +#endif + break; + + case X509_V_ERR_CERT_HAS_EXPIRED: + RERROR("(TLS) Failed with certificate has expired."); + break; + + case X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD: + RERROR("(TLS) Failed with err in certificate 'no after' field.."); + break; + +#if 0 + ASN1_TIME_print(bio_err, X509_get_notAfter(ctx->current_cert)); +#endif + break; + } + + /* + * If we're at the actual client cert, apply additional + * checks. + */ + if (depth == 0) { + tls_session_t *ssn = SSL_get_ex_data(ssl, FR_TLS_EX_INDEX_SSN); +#if OPENSSL_VERSION_NUMBER >= 0x10100000L + STACK_OF(X509)* untrusted = NULL; +#endif + + rad_assert(ssn != NULL); + +#if OPENSSL_VERSION_NUMBER >= 0x10100000L + /* + * See if there are any untrusted certificates. + * If so, complain about them. + */ + untrusted = X509_STORE_CTX_get0_untrusted(ctx); + if (untrusted) { + if (conf->disallow_untrusted || RDEBUG_ENABLED2) { + int i; + + WARN("Certificate chain - %i cert(s) untrusted", + X509_STORE_CTX_get_num_untrusted(ctx)); + for (i = sk_X509_num(untrusted); i > 0 ; i--) { + X509 *this_cert = sk_X509_value(untrusted, i - 1); + + X509_NAME_oneline(X509_get_subject_name(this_cert), subject, sizeof(subject)); + subject[sizeof(subject) - 1] = '\0'; + + WARN("(TLS) untrusted certificate with depth [%i] subject name %s", + i - 1, subject); + } + } + + if (conf->disallow_untrusted) { + AUTH(LOG_PREFIX ": There are untrusted certificates in the certificate chain. Rejecting."); + my_ok = 0; + } + } +#endif + + /* + * If the conf tells us to, check cert issuer + * against the specified value and fail + * verification if they don't match. + */ + if (my_ok && conf->check_cert_issuer && + (strcmp(issuer, conf->check_cert_issuer) != 0)) { + AUTH(LOG_PREFIX ": Certificate issuer (%s) does not match specified value (%s)!", + issuer, conf->check_cert_issuer); + my_ok = 0; + } + + /* + * If the conf tells us to, check the CN in the + * cert against xlat'ed value, but only if the + * previous checks passed. + */ + if (my_ok && conf->check_cert_cn) { + if (radius_xlat(cn_str, sizeof(cn_str), request, conf->check_cert_cn, NULL, NULL) < 0) { + /* if this fails, fail the verification */ + my_ok = 0; + } else { + RDEBUG2("checking certificate CN (%s) with xlat'ed value (%s)", common_name, cn_str); + if (strcmp(cn_str, common_name) != 0) { + AUTH(LOG_PREFIX ": Certificate CN (%s) does not match specified value (%s)!", + common_name, cn_str); + my_ok = 0; + } + } + } /* check_cert_cn */ + +#ifdef HAVE_OPENSSL_OCSP_H + if (my_ok) { + /* + * No OCSP, allow external verification. + */ + if (!conf->ocsp_enable) { + do_verify = true; + + } else { + RDEBUG2("Starting OCSP Request"); + + /* + * If we don't have an issuer, then we can't send + * and OCSP request, but pass the NULL issuer in + * so ocsp_check can decide on the correct + * return code. + */ + issuer_cert = X509_STORE_CTX_get0_current_issuer(ctx); + + /* + * Do the full OCSP checks. + * + * If they fail, don't run the external verify. We don't want + * to allow admins to force authentication success for bad + * certificates. + * + * If the OCSP checks succeed, check whether we still want to + * run the external verification routine. If it's marked as + * "skip verify on OK", then we don't do verify. + */ + my_ok = ocsp_check(request, ocsp_store, issuer_cert, client_cert, conf); + if (my_ok != OCSP_STATUS_FAILED) { + do_verify = !conf->verify_skip_if_ocsp_ok; + } + } + } +#endif + + if ((my_ok != OCSP_STATUS_FAILED) +#ifdef HAVE_OPENSSL_OCSP_H + && do_verify +#endif + ) while (conf->verify_client_cert_cmd) { + char filename[3 * MAX_SESSION_SIZE + 1]; + int fd; + FILE *fp; + + snprintf(filename, sizeof(filename), "%s/%s.client.XXXXXXXX", + conf->verify_tmp_dir, main_config.name); + fd = mkstemp(filename); + if (fd < 0) { + RDEBUG("Failed creating file in %s: %s", + conf->verify_tmp_dir, fr_syserror(errno)); + break; + } + + fp = fdopen(fd, "w"); + if (!fp) { + close(fd); + RDEBUG("Failed opening file %s: %s", + filename, fr_syserror(errno)); + break; + } + + if (!PEM_write_X509(fp, client_cert)) { + fclose(fp); + RDEBUG("Failed writing certificate to file"); + goto do_unlink; + } + fclose(fp); + + if (!pair_make_request("TLS-Client-Cert-Filename", + filename, T_OP_SET)) { + RDEBUG("Failed creating TLS-Client-Cert-Filename"); + + goto do_unlink; + } + + RDEBUG("Verifying client certificate: %s", conf->verify_client_cert_cmd); + if (radius_exec_program(request, NULL, 0, NULL, request, conf->verify_client_cert_cmd, + request->packet->vps, + true, true, EXEC_TIMEOUT) != 0) { + AUTH(LOG_PREFIX ": Certificate CN (%s) fails external verification!", common_name); + my_ok = 0; + + } else if (request) { + RDEBUG("Client certificate CN %s passed external validation", common_name); + } + + do_unlink: + unlink(filename); + break; + } + + /* + * Track that we've verified the client certificate. + */ + ssn->client_cert_ok = (my_ok == 1); + } /* depth == 0 */ + + /* + * Copy certs to request even on failure, so that the + * user can log them. + */ + if (certs && request && !my_ok) { + fr_pair_add(&request->packet->vps, fr_pair_list_copy(request->packet, *certs)); + } + + if (RDEBUG_ENABLED3) { + RDEBUG3("(TLS) chain-depth : %d", depth); + RDEBUG3("(TLS) error : %d", err); + + if (identity) RDEBUG3("identity : %s", *identity); + RDEBUG3("(TLS) common name : %s", common_name); + RDEBUG3("(TLS) subject : %s", subject); + RDEBUG3("(TLS) issuer : %s", issuer); + RDEBUG3("(TLS) verify return : %d", my_ok); + } + + return (my_ok != 0); +} + + +/* + * Configure a X509 CA store to verify OCSP or client repsonses + * + * - Load the trusted CAs + * - Load the trusted issuer certificates + * - Configure CRLs check if needed + */ +X509_STORE *fr_init_x509_store(fr_tls_server_conf_t *conf) +{ + X509_STORE *store = X509_STORE_new(); + + if (store == NULL) return NULL; + + /* Load the CAs we trust */ + if (conf->ca_file || conf->ca_path) + if (!X509_STORE_load_locations(store, conf->ca_file, conf->ca_path)) { + tls_error_log(NULL, "Error reading Trusted root CA list \"%s\"", conf->ca_file); + X509_STORE_free(store); + return NULL; + } + +#ifdef X509_V_FLAG_CRL_CHECK + if (conf->check_crl) + X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK); +#endif +#ifdef X509_V_FLAG_CRL_CHECK_ALL + if (conf->check_all_crl) + X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK_ALL); +#endif + +#if defined(X509_V_FLAG_PARTIAL_CHAIN) + X509_STORE_set_flags(store, X509_V_FLAG_PARTIAL_CHAIN); +#endif + + return store; +} + +#if OPENSSL_VERSION_NUMBER >= 0x0090800fL +#ifndef OPENSSL_NO_ECDH +static int set_ecdh_curve(SSL_CTX *ctx, char const *ecdh_curve, bool disable_single_dh_use) +{ + if (!disable_single_dh_use) { + SSL_CTX_set_options(ctx, SSL_OP_SINGLE_ECDH_USE); + } + + if (!ecdh_curve) return 0; + +#if OPENSSL_VERSION_NUMBER >= 0x1000200fL + /* + * A colon-separated list of curves. + */ + if (*ecdh_curve) { + char *list; + + memcpy(&list, &ecdh_curve, sizeof(list)); /* const issues */ + + if (SSL_CTX_set1_curves_list(ctx, list) == 0) { + ERROR(LOG_PREFIX ": Unknown ecdh_curve \"%s\"", ecdh_curve); + return -1; + } + } + + (void) SSL_CTX_set_ecdh_auto(ctx, 1); +#else + /* + * Use APIs for older versions of OpenSSL. + */ + { + int nid; + EC_KEY *ecdh; + + nid = OBJ_sn2nid(ecdh_curve); + if (!nid) { + ERROR(LOG_PREFIX ": Unknown ecdh_curve \"%s\"", ecdh_curve); + return -1; + } + + ecdh = EC_KEY_new_by_curve_name(nid); + if (!ecdh) { + ERROR(LOG_PREFIX ": Unable to create new curve \"%s\"", ecdh_curve); + return -1; + } + + SSL_CTX_set_tmp_ecdh(ctx, ecdh); + + EC_KEY_free(ecdh); + } +#endif + + return 0; +} +#endif +#endif + +#if defined(HAVE_OPENSSL_CRYPTO_H) && defined(HAVE_CRYPTO_SET_LOCKING_CALLBACK) +#define TLS_UNUSED +#else +#define TLS_UNUSED UNUSED +#endif + +/** Add all the default ciphers and message digests reate our context. + * + * This should be called exactly once from main, before reading the main config + * or initialising any modules. + */ +int tls_global_init(TLS_UNUSED bool spawn_flag, TLS_UNUSED bool check) +{ + SSL_load_error_strings(); /* readable error messages (examples show call before library_init) */ + SSL_library_init(); /* initialize library */ + OpenSSL_add_all_algorithms(); /* required for SHA2 in OpenSSL < 0.9.8o and 1.0.0.a */ + CONF_modules_load_file(NULL, NULL, 0); + + /* + * Initialize the index for the certificates. + */ + fr_tls_ex_index_certs = SSL_SESSION_get_ex_new_index(0, NULL, NULL, NULL, NULL); + +#if defined(HAVE_OPENSSL_CRYPTO_H) && defined(HAVE_CRYPTO_SET_LOCKING_CALLBACK) + /* + * If we're linking with OpenSSL too, then we need + * to set up the mutexes and enable the thread callbacks. + * + * 'check' and not 'check_config' because it's a global, + * and we don't want to have tls.c depend on globals. + */ + if (spawn_flag && !check && (tls_mutexes_init() < 0)) { + ERROR("(TLS) FATAL: Failed to set up SSL mutexes"); + return -1; + } +#endif + +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + /* + * Load the default provider for most algorithms + */ + openssl_default_provider = OSSL_PROVIDER_load(NULL, "default"); + if (!openssl_default_provider) { + ERROR("(TLS) Failed loading default provider"); + return -1; + } + + /* + * Needed for MD4 + * + * https://www.openssl.org/docs/man3.0/man7/migration_guide.html#Legacy-Algorithms + */ + openssl_legacy_provider = OSSL_PROVIDER_load(NULL, "legacy"); + if (!openssl_legacy_provider) { + ERROR("(TLS) Failed loading legacy provider"); + return -1; + } +#endif + + return 0; +} + +#ifdef ENABLE_OPENSSL_VERSION_CHECK +/** Check for vulnerable versions of libssl + * + * @param acknowledged The highest CVE number a user has confirmed is not present in the system's libssl. + * @return 0 if the CVE specified by the user matches the most recent CVE we have, else -1. + */ +int tls_global_version_check(char const *acknowledged) +{ + uint64_t v; + bool bad = false; + size_t i; + + if (strcmp(acknowledged, "yes") == 0) return 0; + + /* Check for bad versions */ + v = (uint64_t) SSLeay(); + + for (i = 0; i < (sizeof(libssl_defects) / sizeof(*libssl_defects)); i++) { + libssl_defect_t *defect = &libssl_defects[i]; + + if ((v >= defect->low) && (v <= defect->high)) { + /* + * If the CVE is acknowledged, allow it. + */ + if (!bad && (strcmp(acknowledged, defect->id) == 0)) return 0; + + ERROR("Refusing to start with libssl version %s (in range %s)", + ssl_version(), ssl_version_range(defect->low, defect->high)); + ERROR("Security advisory %s (%s)", defect->id, defect->name); + ERROR("%s", defect->comment); + + /* + * Only warn about the first one... + */ + if (!bad) { + INFO("Once you have verified libssl has been correctly patched, " + "set security.allow_vulnerable_openssl = '%s'", defect->id); + + bad = true; + } + } + } + + if (bad) return -1; + + return 0; +} +#endif + +/** Free any memory alloced by libssl + * + */ +void tls_global_cleanup(void) +{ +#if OPENSSL_VERSION_NUMBER < 0x10000000L + ERR_remove_state(0); +#elif OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER) + ERR_remove_thread_state(NULL); +#endif +#ifndef OPENSSL_NO_ENGINE + ENGINE_cleanup(); +#endif + +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + if (openssl_default_provider && !OSSL_PROVIDER_unload(openssl_default_provider)) { + ERROR("Failed unloading default provider"); + } + openssl_default_provider = NULL; + + if (openssl_legacy_provider && !OSSL_PROVIDER_unload(openssl_legacy_provider)) { + ERROR("Failed unloading legacy provider"); + } + openssl_legacy_provider = NULL; +#endif + + CONF_modules_unload(1); + ERR_free_strings(); + EVP_cleanup(); + CRYPTO_cleanup_all_ex_data(); +} + + +/* + * Map version strings to OpenSSL macros. + */ +static const FR_NAME_NUMBER version2int[] = { + { "1.0", TLS1_VERSION }, +#ifdef TLS1_1_VERSION + { "1.1", TLS1_1_VERSION }, +#endif +#ifdef TLS1_2_VERSION + { "1.2", TLS1_2_VERSION }, +#endif +#ifdef TLS1_3_VERSION + { "1.3", TLS1_3_VERSION }, +#endif + { NULL, 0 } +}; + +#if OPENSSL_VERSION_NUMBER >= 0x10100000L +#ifdef TLS1_3_VERSION +#define CHECK_FOR_PSK_CERTS (1) +#endif +#endif + +/** Create SSL context + * + * - Load the trusted CAs + * - Load the Private key & the certificate + * - Set the Context options & Verify options + */ +SSL_CTX *tls_init_ctx(fr_tls_server_conf_t *conf, int client, char const *chain_file, char const *private_key_file) +{ + SSL_CTX *ctx; + X509_STORE *certstore; + int verify_mode = SSL_VERIFY_NONE; + int ctx_options = 0, ctx_available = 0; + int type; +#ifdef CHECK_FOR_PSK_CERTS + bool psk_and_certs = false; +#endif + int min_version; + int max_version; + + /* + * SHA256 is in all versions of OpenSSL, but isn't + * initialized by default. It's needed for WiMAX + * certificates. + */ +#ifdef HAVE_OPENSSL_EVP_SHA256 + EVP_add_digest(EVP_sha256()); +#endif + + ctx = SSL_CTX_new(SSLv23_method()); /* which is really "all known SSL / TLS methods". Idiots. */ + if (!ctx) { + tls_error_log(NULL, "Failed creating OpenSSL context"); + return NULL; + } + + /* + * Save the config on the context so that callbacks which + * only get SSL_CTX* e.g. session persistence, can get it + */ + SSL_CTX_set_app_data(ctx, conf); + + /* + * Identify the type of certificates that needs to be loaded + */ + if (conf->file_type) { + type = SSL_FILETYPE_PEM; + } else { + type = SSL_FILETYPE_ASN1; + } + + /* + * Set the password to load private key + */ + if (conf->private_key_password) { +#ifdef __APPLE__ + /* + * We don't want to put the private key password in eap.conf, so check + * for our special string which indicates we should get the password + * programmatically. + */ + char const* special_string = "Apple:UseCertAdmin"; + if (strncmp(conf->private_key_password, special_string, strlen(special_string)) == 0) { + char cmd[256]; + char *password; + long const max_password_len = 128; + snprintf(cmd, sizeof(cmd) - 1, "/usr/sbin/certadmin --get-private-key-passphrase \"%s\"", + conf->private_key_file); + + DEBUG2(LOG_PREFIX ": Getting private key passphrase using command \"%s\"", cmd); + + FILE* cmd_pipe = popen(cmd, "r"); + if (!cmd_pipe) { + ERROR(LOG_PREFIX ": %s command failed: Unable to get private_key_password", cmd); + ERROR(LOG_PREFIX ": Error reading private_key_file %s", conf->private_key_file); + return NULL; + } + + rad_const_free(conf->private_key_password); + password = talloc_array(conf, char, max_password_len); + if (!password) { + ERROR(LOG_PREFIX ": Can't allocate space for private_key_password"); + ERROR(LOG_PREFIX ": Error reading private_key_file %s", conf->private_key_file); + pclose(cmd_pipe); + return NULL; + } + + fgets(password, max_password_len, cmd_pipe); + pclose(cmd_pipe); + + /* Get rid of newline at end of password. */ + password[strlen(password) - 1] = '\0'; + + DEBUG3(LOG_PREFIX ": Password from command = \"%s\"", password); + conf->private_key_password = password; + } +#endif + + { + char *password; + + memcpy(&password, &conf->private_key_password, sizeof(password)); + SSL_CTX_set_default_passwd_cb_userdata(ctx, password); + SSL_CTX_set_default_passwd_cb(ctx, cbtls_password); + } + } + +#ifdef PSK_MAX_IDENTITY_LEN + /* + * A dynamic query exists. There MUST NOT be a + * statically configured identity and password. + */ + if (conf->psk_query) { + if (!*conf->psk_query) { + ERROR(LOG_PREFIX ": Invalid PSK Configuration: psk_query cannot be empty"); + return NULL; + } + + if (conf->psk_identity && *conf->psk_identity) { + ERROR(LOG_PREFIX ": Invalid PSK Configuration: psk_identity and psk_query cannot be used at the same time."); + return NULL; + } + + if (conf->psk_password && *conf->psk_password) { + ERROR(LOG_PREFIX ": Invalid PSK Configuration: psk_password and psk_query cannot be used at the same time."); + return NULL; + } + + if (client) { + ERROR(LOG_PREFIX ": Invalid PSK Configuration: psk_query cannot be used for outgoing connections"); + return NULL; + } + + /* + * Now check that if PSK is being used, that the config is valid. + */ + } else if (conf->psk_identity) { + if (!*conf->psk_identity) { + ERROR(LOG_PREFIX ": Invalid PSK Configuration: psk_identity is empty"); + return NULL; + } + + + if (!conf->psk_password || !*conf->psk_password) { + ERROR(LOG_PREFIX ": Invalid PSK Configuration: psk_identity is set, but there is no psk_password"); + return NULL; + } + + } else if (conf->psk_password) { + ERROR(LOG_PREFIX ": Invalid PSK Configuration: psk_password is set, but there is no psk_identity"); + return NULL; + } + + /* + * Set the server PSK callback if necessary. + */ + if (!client && (conf->psk_identity || conf->psk_query)) { + SSL_CTX_set_psk_server_callback(ctx, psk_server_callback); + } + + /* + * Do more sanity checking if we have a PSK identity. We + * check the password, and convert it to it's final form. + */ + if (conf->psk_identity) { + size_t psk_len, hex_len; + uint8_t buffer[PSK_MAX_PSK_LEN]; + + if (client) { + SSL_CTX_set_psk_client_callback(ctx, + psk_client_callback); + } + + if (!conf->psk_password || !*conf->psk_password) { + ERROR(LOG_PREFIX ": psk_hexphrase cannot be empty"); + return NULL; + } + + psk_len = strlen(conf->psk_password); + if (strlen(conf->psk_password) > (2 * PSK_MAX_PSK_LEN)) { + ERROR(LOG_PREFIX ": psk_hexphrase is too long (max %d)", PSK_MAX_PSK_LEN); + return NULL; + } + + /* + * Check the password now, so that we don't have + * errors at run-time. + */ + hex_len = fr_hex2bin(buffer, sizeof(buffer), conf->psk_password, psk_len); + if (psk_len != (2 * hex_len)) { + ERROR(LOG_PREFIX ": psk_hexphrase is not all hex"); + return NULL; + } + +#ifdef CHECK_FOR_PSK_CERTS + /* + * RFC 8446 says: + * + * When authenticating via a certificate, the server will send the + * Certificate (Section 4.4.2) and CertificateVerify (Section 4.4.3) + * messages. In TLS 1.3 as defined by this document, either a PSK or + * a certificate is always used, but not both. Future documents may + * define how to use them together. + */ + if (((conf->psk_identity || conf->psk_password || conf->psk_query)) && + (conf->certificate_file || conf->private_key_password || conf->private_key_file)) { + psk_and_certs = true; + } +#endif + + goto post_ca; + } +#else + (void) client; /* -Wunused */ +#endif + + /* + * Load our keys and certificates + * + * If certificates are of type PEM then we can make use + * of cert chain authentication using openssl api call + * SSL_CTX_use_certificate_chain_file. Please see how + * the cert chain needs to be given in PEM from + * openSSL.org + */ + if (!chain_file) chain_file = conf->certificate_file; + if (!chain_file) goto load_ca; + + if (type == SSL_FILETYPE_PEM) { + if (!(SSL_CTX_use_certificate_chain_file(ctx, chain_file))) { + tls_error_log(NULL, "Failed reading certificate file \"%s\"", + chain_file); + return NULL; + } + + } else if (!(SSL_CTX_use_certificate_file(ctx, chain_file, type))) { + tls_error_log(NULL, "Failed reading certificate file \"%s\"", + chain_file); + return NULL; + } + +load_ca: + /* + * Load the CAs we trust and configure CRL checks if needed + */ + if (conf->ca_file || conf->ca_path) { + if ((certstore = fr_init_x509_store(conf)) == NULL ) return NULL; + SSL_CTX_set_cert_store(ctx, certstore); + } else { +#if defined(X509_V_FLAG_PARTIAL_CHAIN) + X509_STORE_set_flags(SSL_CTX_get_cert_store(ctx), X509_V_FLAG_PARTIAL_CHAIN); +#endif + } + + if (conf->ca_file && *conf->ca_file) SSL_CTX_set_client_CA_list(ctx, SSL_load_client_CA_file(conf->ca_file)); + + conf->ca_path_last_reload = time(NULL); + conf->old_x509_store = NULL; + + /* + * Disable reloading of cert store if we're not using CA path + */ + if (!conf->ca_path) conf->ca_path_reload_interval = 0; + + if (conf->ca_path_reload_interval > 0 && conf->ca_path_reload_interval < 300) { + DEBUG2("ca_path_reload_interval is set too low, reset it to 300"); + conf->ca_path_reload_interval = 300; + } + + /* Load private key */ + if (!private_key_file) private_key_file = conf->private_key_file; + if (private_key_file) { + if (!(SSL_CTX_use_PrivateKey_file(ctx, private_key_file, type))) { + tls_error_log(NULL, "Failed reading private key file \"%s\"", + private_key_file); + return NULL; + } + + /* + * Check if the loaded private key is the right one + */ + if (!SSL_CTX_check_private_key(ctx)) { + ERROR(LOG_PREFIX ": Private key does not match the certificate public key"); + return NULL; + } + } + +#ifdef PSK_MAX_IDENTITY_LEN +post_ca: +#endif + + /* + * We never want SSLv2 or SSLv3. + */ + ctx_options |= SSL_OP_NO_SSLv2; + ctx_options |= SSL_OP_NO_SSLv3; + + /* + * If set then dummy Change Cipher Spec (CCS) messages are sent in + * TLSv1.3. This has the effect of making TLSv1.3 look more like TLSv1.2 + * so that middleboxes that do not understand TLSv1.3 will not drop + * the connection. This isn't needed for EAP-TLS, so we disable it. + * + * EAP (hopefully) does not have middlebox deployments + */ +#ifdef SSL_OP_ENABLE_MIDDLEBOX_COMPAT + ctx_options &= ~SSL_OP_ENABLE_MIDDLEBOX_COMPAT; +#endif + + /* + * SSL_CTX_set_(min|max)_proto_version was included in OpenSSL 1.1.0 + * + * This version already defines macros for TLS1_2_VERSION and + * below, so we don't need to check for them explicitly. + * + * TLS1_3_VERSION is available in OpenSSL 1.1.1. + */ + + /* + * Get the max version from the configuration files. + */ + if (conf->tls_max_version && *conf->tls_max_version) { + max_version = fr_str2int(version2int, conf->tls_max_version, 0); + if (!max_version) { + ERROR("Invalid value for tls_max_version '%s'", conf->tls_max_version); + return NULL; + } + } else { + /* + * Pick the maximum version available at compile + * time. + */ +#if defined(TLS1_3_VERSION) +#ifdef WITH_RADIUSV11 + /* + * RADIUS 1.1 requires TLS 1.3 or later. + */ + if (conf->radiusv11) { + max_version = TLS1_3_VERSION; + } else +#endif + + + max_version = TLS1_2_VERSION; /* yes, we only use TLS 1.3 if it's EXPLICITELY ENABLED */ +#elif defined(TLS1_2_VERSION) + max_version = TLS1_2_VERSION; +#elif defined(TLS1_1_VERSION) + max_version = TLS1_1_VERSION; +#else + max_version = TLS1_VERSION; +#endif + } + + /* + * Get the min version from the configuration files. + */ + if (conf->tls_min_version && *conf->tls_min_version) { + min_version = fr_str2int(version2int, conf->tls_min_version, 0); + if (!min_version) { + ERROR("Unknown or unsupported value for tls_min_version '%s'", conf->tls_min_version); + return NULL; + } + } else { +#ifdef WITH_RADIUSV11 + /* + * RADIUS 1.1 requires TLS 1.3 or later. + */ + if (conf->radiusv11) { + min_version = TLS1_3_VERSION; + } else +#endif + /* + * Allow TLS 1.0. It is horribly insecure, but + * some systems still use it. + */ + min_version = TLS1_VERSION; + } + + /* + * Compare the two. + */ + if ((min_version > max_version) || (max_version < min_version)) { + ERROR("tls_min_version '%s' must be <= tls_max_version '%s'", + conf->tls_min_version, conf->tls_max_version); + return NULL; + } + +#ifdef CHECK_FOR_PSK_CERTS + /* + * Disable TLS 1.3 when using PSKs and certs. + * This doesn't work. + * + * It's best to disable the offending + * configuration and warn about it. The + * alternative is to have the admin wonder why it + * doesn't work. + * + * Note that the admin can over-ride this by + * setting "min_version = max_version = 1.3" + */ + if (psk_and_certs && + (min_version < TLS1_3_VERSION) && (max_version >= TLS1_3_VERSION)) { + max_version = TLS1_2_VERSION; + radlog(L_DBG | L_WARN, "Disabling TLS 1.3 due to PSK and certificates being configured simultaneously. This is not supported by the standards."); + } +#endif + + /* + * No one should be using TLS 1.0 or TLS 1.1 any more + * + * If TLS1.2 isn't defined by OpenSSL, then we _know_ + * it's an insecure version of OpenSSL. + */ +#ifdef TLS1_2_VERSION + if (max_version < TLS1_2_VERSION) +#endif + { + if (rad_debug_lvl) { + WARN(LOG_PREFIX ": The configuration allows TLS 1.0 and/or TLS 1.1. We STRONGLY recommned using only TLS 1.2 for security"); + WARN(LOG_PREFIX ": Please set: tls_min_version = '1.2'"); + } + } + +#ifdef SSL_OP_NO_TLSv1 + /* + * Check min / max against the old-style "disable" flag. + */ + if (conf->disable_tlsv1) { + if (min_version == TLS1_VERSION) { + ERROR(LOG_PREFIX ": 'disable_tlsv1' is set, but 'min_version = 1.0'. These cannot both be true."); + return NULL; + } + if (max_version == TLS1_VERSION) { + ERROR(LOG_PREFIX ": 'disable_tlsv1' is set, but 'max_version = 1.0'. These cannot both be true."); + return NULL; + } + ctx_options |= SSL_OP_NO_TLSv1; + } + + if (min_version > TLS1_VERSION) ctx_options |= SSL_OP_NO_TLSv1; + + ctx_available |= SSL_OP_NO_TLSv1; +#endif + +#ifdef SSL_OP_NO_TLSv1_1 + /* + * Check min / max against the old-style "disable" flag. + */ + if (conf->disable_tlsv1_1) { + if (min_version <= TLS1_1_VERSION) { + ERROR(LOG_PREFIX ": 'disable_tlsv1_1' is set, but 'min_version <= 1.1'. These cannot both be true."); + return NULL; + } + if (max_version == TLS1_1_VERSION) { + ERROR(LOG_PREFIX ": 'disable_tlsv1_1' is set, but 'max_version = 1.1'. These cannot both be true."); + return NULL; + } + ctx_options |= SSL_OP_NO_TLSv1_1; + } + + if (min_version > TLS1_1_VERSION) ctx_options |= SSL_OP_NO_TLSv1_1; + if (max_version < TLS1_1_VERSION) ctx_options |= SSL_OP_NO_TLSv1_1; + + ctx_available |= SSL_OP_NO_TLSv1_1; +#endif + +#ifdef SSL_OP_NO_TLSv1_2 + /* + * Check min / max against the old-style "disable" flag. + */ + if (conf->disable_tlsv1_2) { + if (min_version <= TLS1_2_VERSION) { + ERROR(LOG_PREFIX ": 'disable_tlsv1_2' is set, but 'min_version <= 1.2'. These cannot both be true."); + return NULL; + } + if (max_version == TLS1_2_VERSION) { + ERROR(LOG_PREFIX ": 'disable_tlsv1_1' is set, but 'max_version = 1.2'. These cannot both be true."); + return NULL; + } + ctx_options |= SSL_OP_NO_TLSv1_2; + } + ctx_available |= SSL_OP_NO_TLSv1_2; + + if (min_version > TLS1_2_VERSION) ctx_options |= SSL_OP_NO_TLSv1_2; + if (max_version < TLS1_2_VERSION) ctx_options |= SSL_OP_NO_TLSv1_2; +#endif + +#ifdef SSL_OP_NO_TLSv1_3 + ctx_available |= SSL_OP_NO_TLSv1_3; + if (min_version > TLS1_3_VERSION) ctx_options |= SSL_OP_NO_TLSv1_3; + if (max_version < TLS1_3_VERSION) ctx_options |= SSL_OP_NO_TLSv1_3; +#endif + + +#ifdef WITH_RADIUSV11 + /* + * RADIUS 1.1 requires TLS 1.3 or later. + */ + if (conf->radiusv11 && (min_version < TLS1_3_VERSION)) { + ERROR(LOG_PREFIX ": Please set 'tls_min_version = 1.2' or greater to use 'radiusv1_1 = true'"); + return NULL; + } +#endif + + /* + * Set the cipher list if we were told to do so. We do + * this before setting min/max TLS version. In a sane + * world, OpenSSL would error out if we set the max TLS + * version to something which was unsupported by the + * current security level. However, this is OpenSSL. If + * you set conflicting options, it doesn't give an error. + * Instead, it just picks something to do. + */ + if (conf->cipher_list) { + if (!SSL_CTX_set_cipher_list(ctx, conf->cipher_list)) { + tls_error_log(NULL, "Failed setting cipher list"); + return NULL; + } + } + +#if OPENSSL_VERSION_NUMBER >= 0x10101000L + if (conf->sigalgs_list) { + char *list; + + memcpy(&list, &(conf->sigalgs_list), sizeof(list)); /* const issues */ + + if (SSL_CTX_set1_sigalgs_list(ctx, list) == 0) { + tls_error_log(NULL, "Failed setting signature list '%s'", conf->sigalgs_list); + return NULL; + } + } +#endif + + /* + * Tell OpenSSL PRETTY PLEASE MAY WE USE TLS 1.1. + * + * Because saying "use TLS 1.1" isn't enough. We have to + * send it flowers and cake. + */ + if (min_version <= TLS1_1_VERSION) { +#if OPENSSL_VERSION_NUMBER >= 0x10101000L + int seclevel = SSL_CTX_get_security_level(ctx); + int required;; + +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + required = 0; +#else + required = 1; +#endif + + if (seclevel != required) { + WARN(LOG_PREFIX ": In order to use TLS 1.0 and/or TLS 1.1, you likely need to set: cipher_list = \"DEFAULT@SECLEVEL=%d\"", required); + } + +#else + /* + * No API to get the security level. Just guess based on the string in the cipher_list. + */ + if (conf->cipher_list && + !strstr(conf->cipher_list, "DEFAULT@SECLEVEL=1")) { + WARN(LOG_PREFIX ": In order to use TLS 1.0 and/or TLS 1.1, you likely need to set: cipher_list = \"DEFAULT@SECLEVEL=1\""); + } +#endif + } + +#if OPENSSL_VERSION_NUMBER >= 0x10100000L + if (conf->disable_tlsv1) { + WARN(LOG_PREFIX ": Please use 'tls_min_version' and 'tls_max_version' instead of 'disable_tlsv1'"); + } + if (conf->disable_tlsv1_1) { + WARN(LOG_PREFIX ": Please use 'tls_min_version' and 'tls_max_version' instead of 'disable_tlsv1_1'"); + } + if (conf->disable_tlsv1_2) { + WARN(LOG_PREFIX ": Please use 'tls_min_version' and 'tls_max_version' instead of 'disable_tlsv1_2'"); + } + + ctx_options &= ~(ctx_available); /* clear these flags, as they're not needed. */ + + if (!SSL_CTX_set_max_proto_version(ctx, max_version)) { + ERROR("Failed setting TLS maximum version"); + return NULL; + } + if (!SSL_CTX_set_min_proto_version(ctx, min_version)) { + ERROR("Failed setting TLS minimum version"); + return NULL; + } +#endif /* OpenSSL version < 1.1.0 */ + + if ((ctx_options & ctx_available) == ctx_available) { + ERROR(LOG_PREFIX ": You have disabled all available TLS versions. EAP will not work"); + return NULL; + } + + /* + * Cache min / max TLS version so that we can + * programatically disable TLS 1.3 for TTLS, PEAP, and + * FAST. + */ + conf->min_version = min_version; + conf->max_version = max_version; + +#ifdef SSL_OP_NO_TICKET + ctx_options |= SSL_OP_NO_TICKET; +#endif + + if (!conf->disable_single_dh_use) { + /* + * SSL_OP_SINGLE_DH_USE must be used in order to prevent + * small subgroup attacks and forward secrecy. Always + * using SSL_OP_SINGLE_DH_USE has an impact on the + * computer time needed during negotiation, but it is not + * very large. + */ + ctx_options |= SSL_OP_SINGLE_DH_USE; + } + + /* + * SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS to work around issues + * in Windows Vista client. + * http://www.openssl.org/~bodo/tls-cbc.txt + * http://www.nabble.com/(RADIATOR)-Radiator-Version-3.16-released-t2600070.html + */ + ctx_options |= SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS; + + if (conf->cipher_server_preference) { + /* + * SSL_OP_CIPHER_SERVER_PREFERENCE to follow best practice + * of nowday's TLS: do not allow poorly-selected ciphers from + * client to take preference + */ + ctx_options |= SSL_OP_CIPHER_SERVER_PREFERENCE; + } + + SSL_CTX_set_options(ctx, ctx_options); + + /* + * TLS 1.3 introduces the concept of early data (also known as zero + * round trip data or 0-RTT data). Early data allows a client to send + * data to a server in the first round trip of a connection, without + * waiting for the TLS handshake to complete if the client has spoken + * to the same server recently. This doesn't work for EAP, so we + * disable early data. + * + */ +#if OPENSSL_VERSION_NUMBER >= 0x10101000L + SSL_CTX_set_max_early_data(ctx, 0); +#endif + + /* + * TODO: Set the RSA & DH + * SSL_CTX_set_tmp_rsa_callback(ctx, cbtls_rsa); + * SSL_CTX_set_tmp_dh_callback(ctx, cbtls_dh); + */ + + /* + * set the message callback to identify the type of + * message. For every new session, there can be a + * different callback argument. + * + * SSL_CTX_set_msg_callback(ctx, cbtls_msg); + */ + + /* + * Set eliptical curve crypto configuration. + */ +#if OPENSSL_VERSION_NUMBER >= 0x0090800fL +#ifndef OPENSSL_NO_ECDH + if (set_ecdh_curve(ctx, conf->ecdh_curve, conf->disable_single_dh_use) < 0) { + return NULL; + } +#endif +#endif + + /* + * OpenSSL will automatically create certificate chains, + * unless we tell it to not do that. The problem is that + * it sometimes gets the chains right from a certificate + * signature view, but wrong from the clients view. + */ + if (!conf->auto_chain) { + SSL_CTX_set_mode(ctx, SSL_MODE_NO_AUTO_CHAIN); + } + + /* Set Info callback */ + SSL_CTX_set_info_callback(ctx, cbtls_info); + + /* + * Callbacks, etc. for session resumption. + */ + if (conf->session_cache_enable) { + /* + * Cache sessions on disk if requested. + */ + if (conf->session_cache_path && *conf->session_cache_path) { + SSL_CTX_sess_set_new_cb(ctx, cbtls_new_session); + SSL_CTX_sess_set_get_cb(ctx, cbtls_get_session); + SSL_CTX_sess_set_remove_cb(ctx, cbtls_remove_session); + } + + /* + * Or run the cache through a virtual server. + */ + if (conf->session_cache_server && *conf->session_cache_server) { + SSL_CTX_sess_set_new_cb(ctx, cbtls_cache_save); + SSL_CTX_sess_set_get_cb(ctx, cbtls_cache_load); + SSL_CTX_sess_set_remove_cb(ctx, cbtls_cache_clear); + } + + SSL_CTX_set_quiet_shutdown(ctx, 1); + if (fr_tls_ex_index_vps < 0) + fr_tls_ex_index_vps = SSL_SESSION_get_ex_new_index(0, NULL, NULL, NULL, NULL); + } + + /* + * Check the certificates for revocation. + */ +#ifdef X509_V_FLAG_CRL_CHECK + if (conf->check_crl) { + certstore = SSL_CTX_get_cert_store(ctx); + if (certstore == NULL) { + tls_error_log(NULL, "Error reading Certificate Store"); + return NULL; + } + X509_STORE_set_flags(certstore, X509_V_FLAG_CRL_CHECK); + +#ifdef X509_V_FLAG_USE_DELTAS + /* + * If set, delta CRLs (if present) are used to + * determine certificate status. If not set + * deltas are ignored. + * + * So it's safe to always set this flag. + */ + X509_STORE_set_flags(certstore, X509_V_FLAG_USE_DELTAS); +#endif + +#ifdef X509_V_FLAG_CRL_CHECK_ALL + if (conf->check_all_crl) + X509_STORE_set_flags(certstore, X509_V_FLAG_CRL_CHECK_ALL); +#endif + } +#endif + + /* + * Set verify modes + * Always verify the peer certificate + */ + verify_mode |= SSL_VERIFY_PEER; + verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + verify_mode |= SSL_VERIFY_CLIENT_ONCE; + SSL_CTX_set_verify(ctx, verify_mode, cbtls_verify); + + if (conf->verify_depth) { + SSL_CTX_set_verify_depth(ctx, conf->verify_depth); + } + +#ifndef LIBRESSL_VERSION_NUMBER + /* Load randomness */ + if (conf->random_file) { + if (!(RAND_load_file(conf->random_file, 1024*10))) { + tls_error_log(NULL, "Failed loading randomness"); + return NULL; + } + } +#endif + + /* + * Setup session caching + */ + if (conf->session_cache_enable) { + /* + * Create a unique context Id per EAP-TLS configuration. + */ + if (conf->session_id_name) { + snprintf(conf->session_context_id, sizeof(conf->session_context_id), + "FR eap %s", conf->session_id_name); + } else { + snprintf(conf->session_context_id, sizeof(conf->session_context_id), + "FR eap %p", conf); + } + + /* + * Cache it, DON'T auto-clear it, and disable the internal OpenSSL session cache. + */ + SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_SERVER | SSL_SESS_CACHE_NO_AUTO_CLEAR | SSL_SESS_CACHE_NO_INTERNAL); + + SSL_CTX_set_session_id_context(ctx, + (unsigned char *) conf->session_context_id, + (unsigned int) strlen(conf->session_context_id)); + + /* + * Our lifetime is in hours, this is in seconds. + */ + SSL_CTX_set_timeout(ctx, conf->session_lifetime * 3600); + + /* + * Set the maximum number of entries in the + * session cache. + */ + SSL_CTX_sess_set_cache_size(ctx, conf->session_cache_size); + +#if OPENSSL_VERSION_NUMBER >= 0x10101000L && !defined(LIBRESSL_VERSION_NUMBER) + SSL_CTX_set_num_tickets(ctx, 1); +#endif + + } else { + SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF); + +#if OPENSSL_VERSION_NUMBER >= 0x10101000L && !defined(LIBRESSL_VERSION_NUMBER) + /* + * This controls the number of stateful or stateless tickets + * generated with TLS 1.3. In OpenSSL 1.1.1 it's also + * required to disable sending session tickets, + * SSL_SESS_CACHE_OFF is not good enough. + */ + SSL_CTX_set_num_tickets(ctx, 0); +#endif + } + + return ctx; +} + + +/* + * Free TLS client/server config + * Should not be called outside this code, as a callback is + * added to automatically free the data when the CONF_SECTION + * is freed. + */ +static int _tls_server_conf_free(fr_tls_server_conf_t *conf) +{ + if (conf->ctx) SSL_CTX_free(conf->ctx); + + if (conf->cache_ht) fr_hash_table_free(conf->cache_ht); + + pthread_mutex_destroy(&conf->mutex); + +#ifdef HAVE_OPENSSL_OCSP_H + if (conf->ocsp_store) X509_STORE_free(conf->ocsp_store); + conf->ocsp_store = NULL; +#endif + + if (conf->realms) fr_hash_table_free(conf->realms); + +#ifndef NDEBUG + memset(conf, 0, sizeof(*conf)); +#endif + return 0; +} + +fr_tls_server_conf_t *tls_server_conf_alloc(TALLOC_CTX *ctx) +{ + fr_tls_server_conf_t *conf; + + conf = talloc_zero(ctx, fr_tls_server_conf_t); + if (!conf) { + ERROR(LOG_PREFIX ": Out of memory"); + return NULL; + } + + talloc_set_destructor(conf, _tls_server_conf_free); + + return conf; +} + +static uint32_t store_hash(void const *data) +{ + DICT_ATTR const *da = data; + return fr_hash(&da, sizeof(da)); +} + +static int store_cmp(void const *a, void const *b) +{ + DICT_ATTR const *one = a; + DICT_ATTR const *two = b; + + return (one < two) - (one > two); +} + +static uint32_t realm_hash(void const *data) +{ + fr_realm_ctx_t const *r = data; + + return fr_hash_string(r->name); +} + +static int realm_cmp(void const *a, void const *b) +{ + fr_realm_ctx_t const *one = a; + fr_realm_ctx_t const *two = b; + + return strcmp(one->name, two->name); +} + +static void realm_free(void *data) +{ + fr_realm_ctx_t *r = data; + + SSL_CTX_free(r->ctx); +} + +static int tls_realms_load(fr_tls_server_conf_t *conf) +{ + fr_hash_table_t *ht; + DIR *dir; + struct dirent *dp; + char buffer[PATH_MAX]; + char buffer2[PATH_MAX]; + + ht = fr_hash_table_create(realm_hash, realm_cmp, realm_free); + if (!ht) return -1; + + dir = opendir(conf->realm_dir); + if (!dir) { + ERROR("Error reading directory %s: %s", conf->realm_dir, fr_syserror(errno)); + error: + if (dir) closedir(dir); + fr_hash_table_free(ht); + return -1; + } + + /* + * Read only the PEM files + */ + while ((dp = readdir(dir)) != NULL) { + char *p; + struct stat stat_buf; + SSL_CTX *ctx; + fr_realm_ctx_t *r; + char const *private_key_file = buffer; + + if (dp->d_name[0] == '.') continue; + + p = strrchr(dp->d_name, '.'); + if (!p) continue; + + if (memcmp(p, ".pem", 5) != 0) continue; /* must END in .pem */ + + snprintf(buffer, sizeof(buffer), "%s/%s", conf->realm_dir, dp->d_name); /* ignore directories */ + if ((stat(buffer, &stat_buf) != 0) || + S_ISDIR(stat_buf.st_mode)) continue; + + strcpy(buffer2, buffer); + p = strchr(buffer2, '.'); /* which must be there... */ + if (!p) continue; + + /* + * If there's a key file, then use that. + * Otherwise assume that the private key is in + * the chain file. + */ + strcpy(p, ".key"); + if (stat(buffer2, &stat_buf) != 0) private_key_file = buffer2; + + ctx = tls_init_ctx(conf, 1, buffer, private_key_file); + if (!ctx) goto error; + + r = talloc_zero(conf, fr_realm_ctx_t); + if (!r) { + SSL_CTX_free(ctx); + goto error; + } + + r->name = talloc_strdup(r, buffer); + r->ctx = ctx; + + if (fr_hash_table_insert(ht, r) < 0) { + ERROR("Failed inserting certificate file %s into hash table", buffer); + goto error; + } + } + + conf->realms = ht; + closedir(dir); + + return 0; +} + + +fr_tls_server_conf_t *tls_server_conf_parse(CONF_SECTION *cs) +{ + fr_tls_server_conf_t *conf; + + /* + * If cs has already been parsed there should be a cached copy + * of conf already stored, so just return that. + */ + conf = cf_data_find(cs, "tls-conf"); + if (conf) { + DEBUG(LOG_PREFIX ": Using cached TLS configuration from previous invocation"); + return conf; + } + + conf = tls_server_conf_alloc(cs); + + if (cf_section_parse(cs, conf, tls_server_config) < 0) { + error: + talloc_free(conf); + return NULL; + } + + /* + * Save people from their own stupidity. + */ + if (conf->fragment_size < 100) conf->fragment_size = 100; + + /* + * Disallow sessions of more than 7 days, as per RFC + * 8446. + * + * Note that we also enforce this on TLS 1.2, etc. + * Because there's just no reason to have month-long TLS + * sessions. + */ + if (conf->session_lifetime > (7 * 24)) conf->session_lifetime = 7 * 24; + + /* + * Only check for certificate things if we don't have a + * PSK query. + */ +#ifdef PSK_MAX_IDENTITY_LEN + if (conf->psk_identity) { + if (conf->private_key_file) { + WARN(LOG_PREFIX ": Ignoring private key file due to psk_identity being used"); + } + + if (conf->certificate_file) { + WARN(LOG_PREFIX ": Ignoring certificate file due to psk_identity being used"); + } + + } else +#endif + { + if (!conf->private_key_file) { + ERROR(LOG_PREFIX ": TLS Server requires a private key file"); + goto error; + } + + if (!conf->certificate_file) { + ERROR(LOG_PREFIX ": TLS Server requires a certificate file"); + goto error; + } + } + + /* + * Initialize configuration mutex + */ + pthread_mutex_init(&conf->mutex, NULL); + + /* + * Initialize TLS + */ + conf->ctx = tls_init_ctx(conf, 0, NULL, NULL); + if (conf->ctx == NULL) { + goto error; + } + + if (conf->session_cache_enable) { + CONF_SECTION *subcs; + CONF_ITEM *ci; + + subcs = cf_section_sub_find(cs, "cache"); + if (!subcs) goto skip_list; + subcs = cf_section_sub_find(subcs, "store"); + if (!subcs) goto skip_list; + + /* + * Largely taken from rlm_detail for laziness. + */ + conf->cache_ht = fr_hash_table_create(store_hash, store_cmp, NULL); + + for (ci = cf_item_find_next(subcs, NULL); + ci != NULL; + ci = cf_item_find_next(subcs, ci)) { + char const *attr; + DICT_ATTR const *da; + + if (!cf_item_is_pair(ci)) continue; + + attr = cf_pair_attr(cf_item_to_pair(ci)); + if (!attr) continue; /* pair-anoia */ + + da = dict_attrbyname(attr); + if (!da) { + ERROR(LOG_PREFIX ": TLS Server requires a certificate file"); + goto error; + } + + /* + * Be kind to minor mistakes. + */ + if (fr_hash_table_finddata(conf->cache_ht, da)) { + WARN(LOG_PREFIX ": Ignoring duplicate entry '%s'", attr); + continue; + } + + + if (!fr_hash_table_insert(conf->cache_ht, da)) { + ERROR(LOG_PREFIX ": Failed inserting '%s' into cache list", attr); + goto error; + } + } + + /* + * If we didn't suppress anything, delete the hash table. + */ + if (fr_hash_table_num_elements(conf->cache_ht) == 0) { + fr_hash_table_free(conf->cache_ht); + conf->cache_ht = NULL; + } + } + +skip_list: + +#ifdef HAVE_OPENSSL_OCSP_H + /* + * Initialize OCSP Revocation Store + */ + if (conf->ocsp_enable) { + conf->ocsp_store = fr_init_x509_store(conf); + if (conf->ocsp_store == NULL) goto error; + } +#endif /*HAVE_OPENSSL_OCSP_H*/ + + { + char *dh_file; + + memcpy(&dh_file, &conf->dh_file, sizeof(dh_file)); + if (load_dh_params(conf->ctx, dh_file) < 0) { + goto error; + } + } + + if (conf->verify_tmp_dir) { + if (chmod(conf->verify_tmp_dir, S_IRWXU) < 0) { + ERROR(LOG_PREFIX ": Failed changing permissions on %s: %s", + conf->verify_tmp_dir, fr_syserror(errno)); + goto error; + } + } + + if (conf->verify_client_cert_cmd && !conf->verify_tmp_dir) { + ERROR(LOG_PREFIX ": You MUST set the 'tmpdir' directory in order to use '%s' cmd", conf->verify_client_cert_cmd); + goto error; + } + +#ifdef SSL_OP_NO_TLSv1_2 + /* + * OpenSSL 1.0.1f and 1.0.1g get the MS-MPPE keys wrong. + */ +#if (OPENSSL_VERSION_NUMBER >= 0x1010106L) && (OPENSSL_VERSION_NUMBER <= 0x1010107L) + conf->disable_tlsv1_2 = true; + WARN(LOG_PREFIX ": Disabling TLSv1.2 due to OpenSSL bugs"); +#endif +#endif + + /* + * Load certificates and private keys from the realm directory. + */ + if (conf->realm_dir && (tls_realms_load(conf) < 0)) goto error; + + /* + * Cache conf in cs in case we're asked to parse this again. + */ + cf_data_add(cs, "tls-conf", conf, NULL); + + return conf; +} + +fr_tls_server_conf_t *tls_client_conf_parse(CONF_SECTION *cs) +{ + fr_tls_server_conf_t *conf; + + conf = cf_data_find(cs, "tls-conf"); + if (conf) { + DEBUG2(LOG_PREFIX ": Using cached TLS configuration from previous invocation"); + return conf; + } + + conf = tls_server_conf_alloc(cs); + + if (cf_section_parse(cs, conf, tls_client_config) < 0) { + error: + talloc_free(conf); + return NULL; + } + + /* + * Save people from their own stupidity. + */ + if (conf->fragment_size < 100) conf->fragment_size = 100; + + /* + * Initialize TLS + */ + conf->ctx = tls_init_ctx(conf, 1, NULL, NULL); + if (conf->ctx == NULL) { + goto error; + } + + { + char *dh_file; + + memcpy(&dh_file, &conf->dh_file, sizeof(dh_file)); + if (load_dh_params(conf->ctx, dh_file) < 0) { + goto error; + } + } + + cf_data_add(cs, "tls-conf", conf, NULL); + + return conf; +} + + +int tls_success(tls_session_t *ssn, REQUEST *request) +{ + VALUE_PAIR *vp, *vps = NULL; + fr_tls_server_conf_t *conf; + TALLOC_CTX *talloc_ctx; + + conf = (fr_tls_server_conf_t *)SSL_get_ex_data(ssn->ssl, FR_TLS_EX_INDEX_CONF); + rad_assert(conf != NULL); + + talloc_ctx = SSL_get_ex_data(ssn->ssl, FR_TLS_EX_INDEX_TALLOC); + + /* + * If there's no session resumption, delete the entry + * from the cache. This means either it's disabled + * globally for this SSL context, OR we were told to + * disable it for this user. + * + * This also means you can't turn it on just for one + * user. + */ + if ((!ssn->allow_session_resumption) || + (((vp = fr_pair_find_by_num(request->config, PW_ALLOW_SESSION_RESUMPTION, 0, TAG_ANY)) != NULL) && + (vp->vp_integer == 0))) { + SSL_CTX_remove_session(ssn->ctx, + ssn->ssl_session); + ssn->allow_session_resumption = false; + + /* + * If we're in a resumed session and it's + * not allowed, + */ + if (SSL_session_reused(ssn->ssl)) { + RDEBUG("(TLS) cache - Forcibly stopping session resumption as it is administratively disabled."); + return -1; + } + + /* + * Else resumption IS allowed, so we store the + * user data in the cache. + */ + } else if ((!SSL_session_reused(ssn->ssl)) || ssn->session_not_resumed) { + VALUE_PAIR **certs; + char buffer[2 * MAX_SESSION_SIZE + 1]; + + tls_session_id(ssn->ssl_session, buffer, MAX_SESSION_SIZE); + + RDEBUG("(TLS) cache - Setting up attributes for session resumption"); + + vp = fr_pair_list_copy_by_num(talloc_ctx, request->reply->vps, PW_USER_NAME, 0, TAG_ANY); + if (vp) fr_pair_add(&vps, vp); + + vp = fr_pair_list_copy_by_num(talloc_ctx, request->packet->vps, PW_STRIPPED_USER_NAME, 0, TAG_ANY); + if (vp) fr_pair_add(&vps, vp); + + vp = fr_pair_list_copy_by_num(talloc_ctx, request->packet->vps, PW_STRIPPED_USER_DOMAIN, 0, TAG_ANY); + if (vp) fr_pair_add(&vps, vp); + + vp = fr_pair_list_copy_by_num(talloc_ctx, request->packet->vps, PW_EAP_TYPE, 0, TAG_ANY); + if (vp) fr_pair_add(&vps, vp); + + vp = fr_pair_list_copy_by_num(talloc_ctx, request->reply->vps, PW_CHARGEABLE_USER_IDENTITY, 0, TAG_ANY); + if (vp) fr_pair_add(&vps, vp); + + vp = fr_pair_list_copy_by_num(talloc_ctx, request->reply->vps, PW_CACHED_SESSION_POLICY, 0, TAG_ANY); + if (vp) fr_pair_add(&vps, vp); + + if (conf->cache_ht) { + vp_cursor_t cursor; + + /* Write each attribute/value to the log file */ + for (vp = fr_cursor_init(&cursor, &request->reply->vps); + vp; + vp = fr_cursor_next(&cursor)) { + VALUE_PAIR *copy; + + if (!fr_hash_table_finddata(conf->cache_ht, vp->da)) { + continue; + } + + copy = fr_pair_copy(talloc_ctx, vp); + if (copy) fr_pair_add(&vps, copy); + } + } + + /* + * Hmm... the certs should probably be session data. + */ + certs = (VALUE_PAIR **)SSL_get_ex_data(ssn->ssl, fr_tls_ex_index_certs); + if (certs) { + /* + * @todo: some go into reply, others into + * request + */ + fr_pair_add(&vps, fr_pair_list_copy(talloc_ctx, *certs)); + + vp = fr_pair_find_by_num(vps, PW_TLS_CLIENT_CERT_EXPIRATION, 0, TAG_ANY); + if (vp) { + time_t expires; + + if (ocsp_asn1time_to_epoch(&expires, vp->vp_strvalue) < 0) { + RDEBUG2("Failed getting certificate expiration, removing cache entry for session %s", buffer); + SSL_CTX_remove_session(ssn->ctx, ssn->ssl_session); + return -1; + } + + if (expires <= request->timestamp) { + RDEBUG2("Certificate has expired, removing cache entry for session %s", buffer); + SSL_CTX_remove_session(ssn->ctx, ssn->ssl_session); + return -1; + } + + /* + * Account for Session-Timeout, if it's available. + */ + vp = fr_pair_find_by_num(request->reply->vps, PW_SESSION_TIMEOUT, 0, TAG_ANY); + if (vp) { + if ((request->timestamp + vp->vp_integer) > expires) { + vp->vp_integer = expires - request->timestamp; + RWDEBUG2("(TLS) Updating Session-Timeout to %u, due to impending certificate expiration", + vp->vp_integer); + } + } + } + } + + if (vps) { + SSL_SESSION_set_ex_data(ssn->ssl_session, fr_tls_ex_index_vps, vps); + rdebug_pair_list(L_DBG_LVL_2, request, vps, " caching "); + + if (conf->session_cache_path) { + /* write the VPs to the cache file */ + char filename[3 * MAX_SESSION_SIZE + 1], buf[1024]; + FILE *vp_file; + + RDEBUG2("Saving session %s in the disk cache", buffer); + + snprintf(filename, sizeof(filename), "%s%c%s.vps", conf->session_cache_path, + FR_DIR_SEP, buffer); + vp_file = fopen(filename, "w"); + if (vp_file == NULL) { + RWDEBUG("(TLS) Could not write session VPs to persistent cache: %s", + fr_syserror(errno)); + } else { + VALUE_PAIR *prev = NULL; + vp_cursor_t cursor; + /* generate a dummy user-style entry which is easy to read back */ + fprintf(vp_file, "# SSL cached session\n"); + fprintf(vp_file, "%s\n\t", buffer); + + for (vp = fr_cursor_init(&cursor, &vps); + vp; + vp = fr_cursor_next(&cursor)) { + /* + * Terminate the previous line. + */ + if (prev) fprintf(vp_file, ",\n\t"); + + /* + * Write this one. + */ + vp_prints(buf, sizeof(buf), vp); + fputs(buf, vp_file); + prev = vp; + } + + /* + * Terminate the final line. + */ + fprintf(vp_file, "\n"); + fclose(vp_file); + } + + } else if (conf->session_cache_server) { + cbtls_cache_save_vps(ssn->ssl, ssn->ssl_session, vps); + + } else { + RDEBUG("Failed to find 'persist_dir' in TLS configuration. Session will not be cached on disk."); + } + } else { + RDEBUG2("No information to cache: session caching will be disabled for session %s", buffer); + SSL_CTX_remove_session(ssn->ctx, ssn->ssl_session); + } + + /* + * Else the session WAS allowed. Copy the cached reply. + */ + } else { + RDEBUG("(TLS) cache - Refreshing entry for session resumption"); + + /* + * The "restore VPs from OpenSSL cache" code is + * now in eaptls_process() + */ + if (conf->session_cache_path) { + char buffer[2 * MAX_SESSION_SIZE + 1]; + +#if OPENSSL_VERSION_NUMBER >= 0x10001000L +#ifdef TLS1_3_VERSION + /* + * OpenSSL frees the underlying session out from + * under us in TLS 1.3. + */ + if (SSL_version(ssn->ssl) == TLS1_3_VERSION) ssn->ssl_session = SSL_get_session(ssn->ssl); +#endif +#endif + + tls_session_id(ssn->ssl_session, buffer, MAX_SESSION_SIZE); + + /* "touch" the cached session/vp file */ + char filename[3 * MAX_SESSION_SIZE + 1]; + + snprintf(filename, sizeof(filename), "%s%c%s.asn1", + conf->session_cache_path, FR_DIR_SEP, buffer); + utime(filename, NULL); + snprintf(filename, sizeof(filename), "%s%c%s.vps", + conf->session_cache_path, FR_DIR_SEP, buffer); + utime(filename, NULL); + } + + if (conf->session_cache_server) { + cbtls_cache_refresh(ssn->ssl, ssn->ssl_session); + } + + /* + * Mark the request as resumed. + */ + pair_make_request("EAP-Session-Resumed", "1", T_OP_SET); + RDEBUG(" &request:EAP-Session-Resumed := 1"); + } + + return 0; +} + + +void tls_fail(tls_session_t *ssn) +{ + /* + * Force the session to NOT be cached. + */ + SSL_CTX_remove_session(ssn->ctx, ssn->ssl_session); +} + +fr_tls_status_t tls_application_data(tls_session_t *ssn, REQUEST *request) + +{ + int err; + VALUE_PAIR **certs; + + /* + * Decrypt the complete record. + */ + if (ssn->dirty_in.used > 0) { + err = BIO_write(ssn->into_ssl, ssn->dirty_in.data, + ssn->dirty_in.used); + if (err != (int) ssn->dirty_in.used) { + REDEBUG("(TLS) Failed writing %zd bytes to SSL BIO: %d", ssn->dirty_in.used, err); + record_init(&ssn->dirty_in); + return FR_TLS_FAIL; + } + + record_init(&ssn->dirty_in); + } + + /* + * tls_handshake_recv() may read application data. So + * don't touch clean_out. But only if the BIO_write() + * above didn't do anything. + */ + else if (ssn->clean_out.used > 0) { + RDEBUG("(TLS) We already have %zd bytes of application data, processing it.", + (ssn->clean_out.used)); + goto add_certs; + } + + /* + * Read (and decrypt) the tunneled data from the + * SSL session, and put it into the decrypted + * data buffer. + */ + err = SSL_read(ssn->ssl, ssn->clean_out.data + ssn->clean_out.used, + sizeof(ssn->clean_out.data) - ssn->clean_out.used); + if (err <= 0) { + int code; + + RDEBUG3("(TLS) SSL_read Error"); + + code = SSL_get_error(ssn->ssl, err); + switch (code) { + case SSL_ERROR_WANT_READ: + if (ssn->clean_out.used > 0) { /* just process what application data we have */ + err = 0; + break; + } + + RDEBUG("(TLS) OpenSSL says that it needs to read more data."); + return FR_TLS_MORE_FRAGMENTS; + + case SSL_ERROR_WANT_WRITE: + if (ssn->clean_out.used > 0) { /* just process what application data we have */ + err = 0; + break; + } + + REDEBUG("(TLS) Error in fragmentation logic: SSL_WANT_WRITE"); + return FR_TLS_FAIL; + + case SSL_ERROR_NONE: + RDEBUG2("(TLS) No application data received. Assuming handshake is continuing..."); + err = 0; + break; + + case SSL_ERROR_ZERO_RETURN: + RDEBUG2("(TLS) Other end closed the TLS tunnel."); + return FR_TLS_FAIL; + + default: + REDEBUG("(TLS) Error in fragmentation logic - code %d", code); + tls_error_io_log(request, ssn, err, "Failed reading application data from OpenSSL"); + return FR_TLS_FAIL; + } + } + + /* + * Passed all checks, successfully decrypted data + */ + ssn->clean_out.used += err; + +add_certs: + /* + * Add the certificates to intermediate packets, so that + * the inner tunnel policies can use them. + */ + certs = (VALUE_PAIR **)SSL_get_ex_data(ssn->ssl, fr_tls_ex_index_certs); + + if (certs) fr_pair_add(&request->packet->vps, fr_pair_list_copy(request->packet, *certs)); + + return FR_TLS_OK; +} + + +/* + * Acknowledge received is for one of the following messages sent earlier + * 1. Handshake completed Message, so now send, EAP-Success + * 2. Alert Message, now send, EAP-Failure + * 3. Fragment Message, now send, next Fragment + */ +fr_tls_status_t tls_ack_handler(tls_session_t *ssn, REQUEST *request) +{ + if (ssn == NULL){ + REDEBUG("(TLS) Unexpected ACK received: No ongoing SSL session"); + return FR_TLS_INVALID; + } + if (!ssn->info.initialized) { + RDEBUG("(TLS) No SSL info available. Waiting for more SSL data"); + return FR_TLS_REQUEST; + } + + if ((ssn->info.content_type == handshake) && (ssn->info.origin == 0)) { + REDEBUG("(TLS) Unexpected ACK received: We sent no previous messages"); + return FR_TLS_INVALID; + } + + switch (ssn->info.content_type) { + case alert: + RDEBUG2("(TLS) Peer ACKed our alert"); + return FR_TLS_FAIL; + + case handshake: + if (ssn->dirty_out.used > 0) { + RDEBUG2("(TLS) Peer ACKed our handshake fragment"); + /* Fragmentation handler, send next fragment */ + return FR_TLS_REQUEST; + } + + if (ssn->is_init_finished || SSL_is_init_finished(ssn->ssl)) { + RDEBUG2("(TLS) Peer ACKed our handshake fragment. handshake is finished"); + + /* + * From now on all the content is + * application data set it here as nobody else + * sets it. + */ + ssn->info.content_type = application_data; + return FR_TLS_SUCCESS; + } /* else more data to send */ + + REDEBUG("(TLS) Cannot continue, as the peer is misbehaving."); + return FR_TLS_FAIL; + + case application_data: + RDEBUG2("(TLS) Peer ACKed our application data fragment"); + return FR_TLS_REQUEST; + + /* + * For the rest of the conditions, switch over + * to the default section below. + */ + default: + REDEBUG("(TLS) Invalid ACK received: %d", ssn->info.content_type); + return FR_TLS_INVALID; + } +} +#endif /* WITH_TLS */ + diff --git a/src/main/tls_listen.c b/src/main/tls_listen.c new file mode 100644 index 0000000..fa8c382 --- /dev/null +++ b/src/main/tls_listen.c @@ -0,0 +1,1568 @@ +/* + * tls.c + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2001 hereUare Communications, Inc. <raghud@hereuare.com> + * Copyright 2003 Alan DeKok <aland@freeradius.org> + * Copyright 2006 The FreeRADIUS server project + */ + +RCSID("$Id$") +USES_APPLE_DEPRECATED_API /* OpenSSL API has been deprecated by Apple */ + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/process.h> +#include <freeradius-devel/rad_assert.h> + +#ifdef HAVE_SYS_STAT_H +#include <sys/stat.h> +#endif + +#ifdef WITH_TCP +#ifdef WITH_TLS +#ifdef HAVE_OPENSSL_RAND_H +#include <openssl/rand.h> +#endif + +#ifdef HAVE_OPENSSL_OCSP_H +#include <openssl/ocsp.h> +#endif + +#ifdef HAVE_PTHREAD_H +#define PTHREAD_MUTEX_LOCK pthread_mutex_lock +#define PTHREAD_MUTEX_UNLOCK pthread_mutex_unlock +#else +#define PTHREAD_MUTEX_LOCK(_x) +#define PTHREAD_MUTEX_UNLOCK(_x) +#endif + +static void dump_hex(char const *msg, uint8_t const *data, size_t data_len) +{ + size_t i; + + if (rad_debug_lvl < 3) return; + + printf("%s %d\n", msg, (int) data_len); + if (data_len > 256) data_len = 256; + + for (i = 0; i < data_len; i++) { + if ((i & 0x0f) == 0x00) printf ("%02x: ", (unsigned int) i); + printf("%02x ", data[i]); + if ((i & 0x0f) == 0x0f) printf ("\n"); + } + printf("\n"); + fflush(stdout); +} + +static void tls_socket_close(rad_listen_t *listener) +{ + listen_socket_t *sock = listener->data; + + SSL_shutdown(sock->ssn->ssl); + + listener->status = RAD_LISTEN_STATUS_EOL; + listener->tls = NULL; /* parent owns this! */ + + /* + * Tell the event handler that an FD has disappeared. + */ + DEBUG("(TLS) Closing connection"); + radius_update_listener(listener); + + /* + * Do NOT free the listener here. It may be in use by + * a request, and will need to hang around until + * all of the requests are done. + * + * It is instead free'd when all of the requests using it + * are done. + */ +} + +static void tls_write_available(fr_event_list_t *el, int sock, void *ctx); + +static int CC_HINT(nonnull) tls_socket_write(rad_listen_t *listener) +{ + ssize_t rcode; + listen_socket_t *sock = listener->data; + + /* + * It's not writable, so we don't bother writing to it. + */ + if (listener->blocked) return 0; + + /* + * Write as much as possible. + */ + rcode = write(listener->fd, sock->ssn->dirty_out.data, sock->ssn->dirty_out.used); + if (rcode <= 0) { +#ifdef EWOULDBLOCK + /* + * Writing to the socket would cause it to block. + * As a result, we just mark it as "don't use" + * until such time as it becomes writable. + */ + if (errno == EWOULDBLOCK) { + proxy_listener_freeze(listener, tls_write_available); + return 0; + } +#endif + + + ERROR("(TLS) Error writing to socket: %s", fr_syserror(errno)); + + tls_socket_close(listener); + return -1; + } + + /* + * All of the data was written. It's fine. + */ + if ((size_t) rcode == sock->ssn->dirty_out.used) { + sock->ssn->dirty_out.used = 0; + return 0; + } + + /* + * Move the data to the start of the buffer. + * + * Yes, this is horrible. But doing this means that we + * don't have to modify the rest of the code which mangles dirty_out, and assumes that the write offset is always &data[used]. + */ + memmove(&sock->ssn->dirty_out.data[0], &sock->ssn->dirty_out.data[rcode], sock->ssn->dirty_out.used - rcode); + sock->ssn->dirty_out.used -= rcode; + + return 0; +} + +static void tls_write_available(UNUSED fr_event_list_t *el, UNUSED int fd, void *ctx) +{ + rad_listen_t *listener = ctx; + listen_socket_t *sock = listener->data; + + proxy_listener_thaw(listener); + + PTHREAD_MUTEX_LOCK(&sock->mutex); + (void) tls_socket_write(listener); + PTHREAD_MUTEX_UNLOCK(&sock->mutex); +} + + +/* + * Check for PROXY protocol. Once that's done, clear + * listener->proxy_protocol. + */ +static int proxy_protocol_check(rad_listen_t *listener, REQUEST *request) +{ + listen_socket_t *sock = listener->data; + uint8_t const *p, *end, *eol; + int af, argc, src_port, dst_port; + unsigned long num; + fr_ipaddr_t src, dst; + char *argv[5], *eos; + ssize_t rcode; + RADCLIENT *client; + + /* + * Begin by trying to fill the buffer. + */ + rcode = read(request->packet->sockfd, + sock->ssn->dirty_in.data + sock->ssn->dirty_in.used, + sizeof(sock->ssn->dirty_in.data) - sock->ssn->dirty_in.used); + if (rcode < 0) { + if (errno == EINTR) return 0; + RDEBUG("(TLS) Closing PROXY socket from client port %u due to read error - %s", sock->other_port, fr_syserror(errno)); + return -1; + } + + if (rcode == 0) { + DEBUG("(TLS) Closing PROXY socket from client port %u - other end closed connection", sock->other_port); + return -1; + } + + /* + * We've read data, scan the buffer for a CRLF. + */ + sock->ssn->dirty_in.used += rcode; + + dump_hex("READ FROM PROXY PROTOCOL SOCKET", sock->ssn->dirty_in.data, sock->ssn->dirty_in.used); + + p = sock->ssn->dirty_in.data; + + /* + * CRLF MUST be within the first 107 bytes. + */ + if (sock->ssn->dirty_in.used < 107) { + end = p + sock->ssn->dirty_in.used; + } else { + end = p + 107; + } + eol = NULL; + + /* + * Scan for CRLF. + */ + while ((p + 1) < end) { + if ((p[0] == 0x0d) && (p[1] == 0x0a)) { + eol = p; + break; + } + + /* + * Other control characters, or non-ASCII data. + * That's a problem. + */ + if ((*p < ' ') || (*p >= 0x80)) { + invalid_data: + DEBUG("(TLS) Closing PROXY socket from client port %u - received invalid data", sock->other_port); + return -1; + } + + p++; + } + + /* + * No CRLF, keep reading until we have it. + */ + if (!eol) return 0; + + p = sock->ssn->dirty_in.data; + + /* + * Let's see if the PROXY line is well-formed. + */ + if ((eol - p) < 14) goto invalid_data; + + /* + * We only support TCP4 and TCP6. + */ + if (memcmp(p, "PROXY TCP", 9) != 0) goto invalid_data; + + p += 9; + + if (*p == '4') { + af = AF_INET; + + } else if (*p == '6') { + af = AF_INET6; + + } else goto invalid_data; + + p++; + if (*p != ' ') goto invalid_data; + p++; + + sock->ssn->dirty_in.data[eol - sock->ssn->dirty_in.data] = '\0'; /* overwite the CRLF */ + + /* + * Parse the fields (being a little forgiving), while + * checking for too many / too few fields. + */ + argc = str2argv((char *) &sock->ssn->dirty_in.data[p - sock->ssn->dirty_in.data], (char **) &argv, 5); + if (argc != 4) goto invalid_data; + + memset(&src, 0, sizeof(src)); + memset(&dst, 0, sizeof(dst)); + + if (fr_pton(&src, argv[0], -1, af, false) < 0) goto invalid_data; + if (fr_pton(&dst, argv[1], -1, af, false) < 0) goto invalid_data; + + num = strtoul(argv[2], &eos, 10); + if (num > 65535) goto invalid_data; + if (*eos) goto invalid_data; + src_port = num; + + num = strtoul(argv[3], &eos, 10); + if (num > 65535) goto invalid_data; + if (*eos) goto invalid_data; + dst_port = num; + + /* + * And copy the various fields around. + */ + sock->haproxy_src_ipaddr = sock->other_ipaddr; + sock->haproxy_src_port = sock->other_port; + + sock->haproxy_dst_ipaddr = sock->my_ipaddr; + sock->haproxy_dst_port = sock->my_port; + + sock->my_ipaddr = dst; + sock->my_port = dst_port; + + sock->other_ipaddr = src; + sock->other_port = src_port; + + /* + * Print out what we've changed. Note that the TCP + * socket address family and the PROXY address family may + * be different! + */ + if (RDEBUG_ENABLED) { + char src_buf[128], dst_buf[128]; + + RDEBUG("(TLS) Received PROXY protocol connection from client %s:%s -> %s:%s, via proxy %s:%u -> %s:%u", + argv[0], argv[2], argv[1], argv[3], + inet_ntop(af, &sock->haproxy_src_ipaddr.ipaddr, src_buf, sizeof(src_buf)), + sock->haproxy_src_port, + inet_ntop(af, &sock->haproxy_dst_ipaddr.ipaddr, dst_buf, sizeof(dst_buf)), + sock->haproxy_dst_port); + } + + /* + * Ensure that the source IP indicated by the PROXY + * protocol is a known TLS client. + */ + if ((client = client_listener_find(listener, &src, src_port)) == NULL || + client->proto != IPPROTO_TCP) { + RDEBUG("(TLS) Unknown client %s - dropping PROXY protocol connection", argv[0]); + return -1; + } + + /* + * Use the client indicated by the proxy. + */ + sock->client = client; + + /* + * Fix up the current request so that the first packet's + * src/dst is valid. Subsequent packets will get the + * clients IP from the listener and listen_sock + * structures. + */ + request->packet->dst_ipaddr = dst; + request->packet->dst_port = dst_port; + request->packet->src_ipaddr = src; + request->packet->src_port = src_port; + + /* + * Move any remaining TLS data to the start of the buffer. + */ + eol += 2; + end = sock->ssn->dirty_in.data + sock->ssn->dirty_in.used; + if (eol < end) { + memmove(sock->ssn->dirty_in.data, eol, end - eol); + sock->ssn->dirty_in.used = end - eol; + } else { + sock->ssn->dirty_in.used = 0; + } + + /* + * It's no longer a PROXY protocol, but just straight TLS. + */ + listener->proxy_protocol = false; + + return 1; +} + +static int tls_socket_recv(rad_listen_t *listener) +{ + bool doing_init = false, already_read = false; + ssize_t rcode; + RADIUS_PACKET *packet; + REQUEST *request; + listen_socket_t *sock = listener->data; + fr_tls_status_t status; + RADCLIENT *client = sock->client; + + if (!sock->packet) { + sock->packet = rad_alloc(sock, false); + if (!sock->packet) return 0; + + sock->packet->sockfd = listener->fd; + sock->packet->src_ipaddr = sock->other_ipaddr; + sock->packet->src_port = sock->other_port; + sock->packet->dst_ipaddr = sock->my_ipaddr; + sock->packet->dst_port = sock->my_port; + + if (sock->request) sock->request->packet = talloc_steal(sock->request, sock->packet); + } + + /* + * Allocate a REQUEST for debugging, and initialize the TLS session. + */ + if (!sock->request) { + sock->request = request = request_alloc(sock); + if (!sock->request) { + ERROR("Out of memory"); + return 0; + } + + rad_assert(request->packet == NULL); + rad_assert(sock->packet != NULL); + request->packet = talloc_steal(request, sock->packet); + + request->component = "<tls-connect>"; + + request->reply = rad_alloc(request, false); + if (!request->reply) return 0; + + rad_assert(sock->ssn == NULL); + + sock->ssn = tls_new_session(sock, listener->tls, sock->request, + listener->tls->require_client_cert, true); + if (!sock->ssn) { + TALLOC_FREE(sock->request); + sock->packet = NULL; + return 0; + } + + SSL_set_ex_data(sock->ssn->ssl, FR_TLS_EX_INDEX_REQUEST, (void *)request); + SSL_set_ex_data(sock->ssn->ssl, fr_tls_ex_index_certs, (void *) &sock->certs); + SSL_set_ex_data(sock->ssn->ssl, FR_TLS_EX_INDEX_TALLOC, sock); + + sock->ssn->quick_session_tickets = true; /* we don't have inner-tunnel authentication */ + + doing_init = true; + } + + rad_assert(sock->request != NULL); + rad_assert(sock->request->packet != NULL); + rad_assert(sock->packet != NULL); + rad_assert(sock->ssn != NULL); + + request = sock->request; + + /* + * Bypass ALL of the TLS stuff until we've read the PROXY + * header. + * + * If the PROXY header checks pass, then the flag is + * cleared, as we don't need it any more. + */ + if (listener->proxy_protocol) { + rcode = proxy_protocol_check(listener, request); + if (rcode < 0) { + RDEBUG("(TLS) Closing PROXY TLS socket from client port %u", sock->other_port); + tls_socket_close(listener); + return 0; + } + if (rcode == 0) return 1; + + /* + * The buffer might already have data. In that + * case, we don't want to do a blocking read + * later. + */ + already_read = (sock->ssn->dirty_in.used > 0); + } + + if (sock->state == LISTEN_TLS_SETUP) { + RDEBUG3("(TLS) Setting connection state to RUNNING"); + sock->state = LISTEN_TLS_RUNNING; + + if (sock->ssn->clean_out.used < 20) { + goto get_application_data; + } + + goto read_application_data; + } + + RDEBUG3("(TLS) Reading from socket %d", request->packet->sockfd); + PTHREAD_MUTEX_LOCK(&sock->mutex); + + /* + * If there is pending application data, as set up by + * SSL_peek(), read that before reading more data from + * the socket. + */ + if (SSL_pending(sock->ssn->ssl)) { + RDEBUG3("(TLS) Reading pending buffered data"); + sock->ssn->dirty_in.used = 0; + goto check_for_setup; + } + + if (!already_read) { + rcode = read(request->packet->sockfd, + sock->ssn->dirty_in.data, + sizeof(sock->ssn->dirty_in.data)); + if ((rcode < 0) && (errno == ECONNRESET)) { + do_close: + DEBUG("(TLS) Closing socket from client port %u", sock->other_port); + tls_socket_close(listener); + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + return 0; + } + + if (rcode < 0) { + RDEBUG("(TLS) Error reading socket: %s", fr_syserror(errno)); + goto do_close; + } + + /* + * Normal socket close. + */ + if (rcode == 0) { + RDEBUG("(TLS) Client has closed the TCP connection"); + goto do_close; + } + + sock->ssn->dirty_in.used = rcode; + } + + dump_hex("READ FROM SSL", sock->ssn->dirty_in.data, sock->ssn->dirty_in.used); + + /* + * Catch attempts to use non-SSL. + */ + if (doing_init && (sock->ssn->dirty_in.data[0] != handshake)) { + RDEBUG("(TLS) Non-TLS data sent to TLS socket: closing"); + goto do_close; + } + + /* + * If we need to do more initialization, do that here. + */ +check_for_setup: + if (!sock->ssn->is_init_finished) { + if (!tls_handshake_recv(request, sock->ssn)) { + RDEBUG("(TLS) Failed in TLS handshake receive"); + goto do_close; + } + + /* + * More ACK data to send. Do so. + */ + if (sock->ssn->dirty_out.used > 0) { + RDEBUG3("(TLS) Writing to socket %d", listener->fd); + tls_socket_write(listener); + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + return 0; + } + + /* + * If SSL handshake still isn't finished, then there + * is more data to read. Release the mutex and + * return so this function will be called again + */ + if (!SSL_is_init_finished(sock->ssn->ssl)) { + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + return 0; + } + } + + /* + * Run the request through a virtual server in + * order to see if we like the certificate + * presented by the client. + */ + if (sock->state == LISTEN_TLS_INIT) { + if (!SSL_is_init_finished(sock->ssn->ssl)) { + RDEBUG("(TLS) OpenSSL says that the TLS session is still negotiating, but there's no more data to send!"); + goto do_close; + } + + sock->ssn->is_init_finished = true; + if (!listener->check_client_connections) { + sock->state = LISTEN_TLS_RUNNING; + goto get_application_data; + } + + request->packet->vps = fr_pair_list_copy(request->packet, sock->certs); + + /* + * Fake out a Status-Server packet, which + * does NOT have a Message-Authenticator, + * or any other contents. + */ + request->packet->code = PW_CODE_STATUS_SERVER; + request->packet->data = talloc_zero_array(request->packet, uint8_t, 20); + request->packet->data[0] = PW_CODE_STATUS_SERVER; + request->packet->data[3] = 20; + request->listener = listener; + sock->state = LISTEN_TLS_CHECKING; + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + + /* + * Don't read from the socket until the request + * returns. + */ + listener->status = RAD_LISTEN_STATUS_PAUSE; + radius_update_listener(listener); + + return 1; + } + + /* + * Try to get application data. + */ +get_application_data: + /* + * More data to send. Do so. + */ + if (sock->ssn->dirty_out.used > 0) { + RDEBUG3("(TLS) Writing to socket %d", listener->fd); + rcode = tls_socket_write(listener); + if (rcode < 0) { + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + return rcode; + } + } + + status = tls_application_data(sock->ssn, request); + RDEBUG3("(TLS) Application data status %d", status); + + /* + * Some kind of failure. Close the socket. + */ + if (status == FR_TLS_FAIL) { + DEBUG("(TLS) Unable to recover from TLS error, closing socket from client port %u", sock->other_port); + tls_socket_close(listener); + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + return 0; + } + + if (status == FR_TLS_MORE_FRAGMENTS) { + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + return 0; + } + + if (sock->ssn->clean_out.used == 0) { + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + return 0; + } + + /* + * Hold application data if we're not yet in the RUNNING + * state. + */ + if (sock->state != LISTEN_TLS_RUNNING) { + RDEBUG3("(TLS) Holding application data until setup is complete"); + return 0; + } + +read_application_data: + /* + * We now have a bunch of application data. + */ + dump_hex("TUNNELED DATA > ", sock->ssn->clean_out.data, sock->ssn->clean_out.used); + + /* + * If the packet is a complete RADIUS packet, return it to + * the caller. Otherwise... + */ + if ((sock->ssn->clean_out.used < 20) || + (((sock->ssn->clean_out.data[2] << 8) | sock->ssn->clean_out.data[3]) != (int) sock->ssn->clean_out.used)) { + RDEBUG("(TLS) Received bad packet: Length %zd contents %d", + sock->ssn->clean_out.used, + (sock->ssn->clean_out.data[2] << 8) | sock->ssn->clean_out.data[3]); + goto do_close; + } + + packet = sock->packet; + packet->data = talloc_array(packet, uint8_t, sock->ssn->clean_out.used); + packet->data_len = sock->ssn->clean_out.used; + sock->ssn->record_minus(&sock->ssn->clean_out, packet->data, packet->data_len); + packet->vps = NULL; + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + +#ifdef WITH_RADIUSV11 + packet->radiusv11 = sock->radiusv11; +#endif + + if (!rad_packet_ok(packet, 0, NULL)) { + if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror()); + DEBUG("(TLS) Closing TLS socket from client"); + PTHREAD_MUTEX_LOCK(&sock->mutex); + tls_socket_close(listener); + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + return 0; /* do_close unlocks the mutex */ + } + + /* + * Copied from src/lib/radius.c, rad_recv(); + */ + if (fr_debug_lvl) { + char host_ipaddr[128]; + + if (is_radius_code(packet->code)) { + RDEBUG("(TLS): %s packet from host %s port %d, id=%d, length=%d", + fr_packet_codes[packet->code], + inet_ntop(packet->src_ipaddr.af, + &packet->src_ipaddr.ipaddr, + host_ipaddr, sizeof(host_ipaddr)), + packet->src_port, + packet->id, (int) packet->data_len); + } else { + RDEBUG("(TLS): Packet from host %s port %d code=%d, id=%d, length=%d", + inet_ntop(packet->src_ipaddr.af, + &packet->src_ipaddr.ipaddr, + host_ipaddr, sizeof(host_ipaddr)), + packet->src_port, + packet->code, + packet->id, (int) packet->data_len); + } + } + + FR_STATS_INC(auth, total_requests); + + return 1; +} + + +int dual_tls_recv(rad_listen_t *listener) +{ + RADIUS_PACKET *packet; + RAD_REQUEST_FUNP fun = NULL; + listen_socket_t *sock = listener->data; + RADCLIENT *client = sock->client; + BIO *rbio; +#ifdef WITH_COA_TUNNEL + bool is_reply = false; +#endif + + if (listener->status != RAD_LISTEN_STATUS_KNOWN) return 0; + +redo: + if (!tls_socket_recv(listener)) { + return 0; + } + + rad_assert(sock->packet != NULL); + rad_assert(sock->ssn != NULL); + rad_assert(client != NULL); + + packet = talloc_steal(NULL, sock->packet); + sock->request->packet = NULL; + sock->packet = NULL; + + /* + * Some sanity checks, based on the packet code. + * + * "auth+acct" are marked as "auth", with the "dual" flag + * set. + */ + switch (packet->code) { + case PW_CODE_ACCESS_REQUEST: + if (listener->type != RAD_LISTEN_AUTH) goto bad_packet; + FR_STATS_INC(auth, total_requests); + fun = rad_authenticate; + break; + +#ifdef WITH_ACCOUNTING + case PW_CODE_ACCOUNTING_REQUEST: + if (listener->type != RAD_LISTEN_ACCT) { + /* + * Allow auth + dual. Disallow + * everything else. + */ + if (!((listener->type == RAD_LISTEN_AUTH) && + (listener->dual))) { + goto bad_packet; + } + } + FR_STATS_INC(acct, total_requests); + fun = rad_accounting; + break; +#endif + +#ifdef WITH_COA + case PW_CODE_COA_REQUEST: + if (listener->type != RAD_LISTEN_COA) goto bad_packet; + FR_STATS_INC(coa, total_requests); + fun = rad_coa_recv; + break; + + case PW_CODE_DISCONNECT_REQUEST: + if (listener->type != RAD_LISTEN_COA) goto bad_packet; + FR_STATS_INC(dsc, total_requests); + fun = rad_coa_recv; + break; + +#ifdef WITH_COA_TUNNEL + case PW_CODE_COA_ACK: + case PW_CODE_COA_NAK: + if (!listener->send_coa) goto bad_packet; + is_reply = true; + break; +#endif +#endif + + case PW_CODE_STATUS_SERVER: + if (!main_config.status_server +#ifdef WITH_TLS + && !listener->check_client_connections +#endif + ) { + FR_STATS_INC(auth, total_unknown_types); + WARN("Ignoring Status-Server request due to security configuration"); + rad_free(&packet); + return 0; + } + fun = rad_status_server; + break; + + default: + bad_packet: + FR_STATS_INC(auth, total_unknown_types); + + DEBUG("(TLS) Invalid packet code %d sent from client %s port %d : IGNORED", + packet->code, client->shortname, packet->src_port); + rad_free(&packet); + return 0; + } /* switch over packet types */ + +#ifdef WITH_COA_TUNNEL + if (is_reply) { + if (!request_proxy_reply(packet)) { + rad_free(&packet); + return 0; + } + } else +#endif + + if (!request_receive(NULL, listener, packet, client, fun)) { + FR_STATS_INC(auth, total_packets_dropped); + rad_free(&packet); + return 0; + } + + /* + * Check for more application data. + * + * If there is pending SSL data, "peek" at the + * application data. If we get at least one byte of + * application data, go back to tls_socket_recv(). + * SSL_peek() will set SSL_pending(), and + * tls_socket_recv() will read another packet. + */ + rbio = SSL_get_rbio(sock->ssn->ssl); + if (BIO_ctrl_pending(rbio)) { + char buf[1]; + int peek = SSL_peek(sock->ssn->ssl, buf, 1); + + if (peek > 0) { + DEBUG("(TLS) more TLS records after dual_tls_recv"); + goto redo; + } + } + + return 1; +} + + +/* + * Send a response packet + */ +int dual_tls_send(rad_listen_t *listener, REQUEST *request) +{ + listen_socket_t *sock = listener->data; + + VERIFY_REQUEST(request); + + rad_assert(request->listener == listener); + rad_assert(listener->send == dual_tls_send); + + if (listener->status != RAD_LISTEN_STATUS_KNOWN) return 0; + + /* + * See if the policies allowed this connection. + */ + if (sock->state == LISTEN_TLS_CHECKING) { + if (request->reply->code != PW_CODE_ACCESS_ACCEPT) { + listener->status = RAD_LISTEN_STATUS_EOL; + listener->tls = NULL; /* parent owns this! */ + + /* + * Tell the event handler that an FD has disappeared. + */ + radius_update_listener(listener); + return 0; + } + + /* + * Resume reading from the listener. + */ + listener->status = RAD_LISTEN_STATUS_RESUME; + radius_update_listener(listener); + + rad_assert(sock->request->packet != request->packet); + + sock->state = LISTEN_TLS_SETUP; + (void) dual_tls_recv(listener); + return 0; + } + + /* + * Accounting reject's are silently dropped. + * + * We do it here to avoid polluting the rest of the + * code with this knowledge + */ + if (request->reply->code == 0) return 0; + +#ifdef WITH_COA_TUNNEL + /* + * Save the key, if we haven't already done that. + */ + if (listener->send_coa && !listener->key) { + VALUE_PAIR *vp = NULL; + + vp = fr_pair_find_by_num(request->config, PW_ORIGINATING_REALM_KEY, 0, TAG_ANY); + if (vp) { + RDEBUG("Adding send CoA listener with key %s", vp->vp_strvalue); + listen_coa_add(request->listener, vp->vp_strvalue); + } + } +#endif + + /* + * Pack the VPs + */ + if (rad_encode(request->reply, request->packet, + request->client->secret) < 0) { + RERROR("Failed encoding packet: %s", fr_strerror()); + return 0; + } + + if (request->reply->data_len > (MAX_PACKET_LEN - 100)) { + RWARN("Packet is large, and possibly truncated - %zd vs max %d", + request->reply->data_len, MAX_PACKET_LEN); + } + + /* + * Sign the packet. + */ + if (rad_sign(request->reply, request->packet, + request->client->secret) < 0) { + RERROR("Failed signing packet: %s", fr_strerror()); + return 0; + } + + PTHREAD_MUTEX_LOCK(&sock->mutex); + + /* + * Write the packet to the SSL buffers. + */ + sock->ssn->record_plus(&sock->ssn->clean_in, + request->reply->data, request->reply->data_len); + + dump_hex("TUNNELED DATA < ", sock->ssn->clean_in.data, sock->ssn->clean_in.used); + + /* + * Do SSL magic to get encrypted data. + */ + tls_handshake_send(request, sock->ssn); + + /* + * And finally write the data to the socket. + */ + if (sock->ssn->dirty_out.used > 0) { + dump_hex("WRITE TO SSL", sock->ssn->dirty_out.data, sock->ssn->dirty_out.used); + + RDEBUG3("(TLS) Writing to socket %d", listener->fd); + tls_socket_write(listener); + } + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + + return 0; +} + +#ifdef WITH_COA_TUNNEL +/* + * Send a CoA request to a NAS, as a proxied packet. + * + * The proxied packet MUST already have been encoded. + */ +int dual_tls_send_coa_request(rad_listen_t *listener, REQUEST *request) +{ + listen_socket_t *sock = listener->data; + + VERIFY_REQUEST(request); + + rad_assert(listener->proxy_send == dual_tls_send_coa_request); + + if (listener->status != RAD_LISTEN_STATUS_KNOWN) return 0; + + rad_assert(request->proxy->data); + + if (request->proxy->data_len > (MAX_PACKET_LEN - 100)) { + RWARN("Packet is large, and possibly truncated - %zd vs max %d", + request->proxy->data_len, MAX_PACKET_LEN); + } + + PTHREAD_MUTEX_LOCK(&sock->mutex); + + /* + * Write the packet to the SSL buffers. + */ + sock->ssn->record_plus(&sock->ssn->clean_in, + request->proxy->data, request->proxy->data_len); + + dump_hex("TUNNELED DATA < ", sock->ssn->clean_in.data, sock->ssn->clean_in.used); + + /* + * Do SSL magic to get encrypted data. + */ + tls_handshake_send(request, sock->ssn); + + /* + * And finally write the data to the socket. + */ + if (sock->ssn->dirty_out.used > 0) { + dump_hex("WRITE TO SSL", sock->ssn->dirty_out.data, sock->ssn->dirty_out.used); + + RDEBUG3("(TLS) Writing to socket %d", listener->fd); + tls_socket_write(listener); + } + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + + return 0; +} +#endif + +static int try_connect(listen_socket_t *sock) +{ + int ret; + time_t now; + + now = time(NULL); + if ((sock->opened + sock->connect_timeout) < now) { + tls_error_io_log(NULL, sock->ssn, 0, "Timeout in SSL_connect"); + return -1; + } + + ret = SSL_connect(sock->ssn->ssl); + if (ret <= 0) { + switch (SSL_get_error(sock->ssn->ssl, ret)) { + default: + tls_error_io_log(NULL, sock->ssn, ret, "Failed in " STRINGIFY(__FUNCTION__) " (SSL_connect)"); + return -1; + + case SSL_ERROR_WANT_READ: + DEBUG3("(TLS) SSL_connect() returned WANT_READ"); + return 2; + + case SSL_ERROR_WANT_WRITE: + DEBUG3("(TLS) SSL_connect() returned WANT_WRITE"); + return 2; + } + } + + sock->ssn->connected = true; + return 1; +} + + +#ifdef WITH_PROXY +#ifdef WITH_RADIUSV11 +extern int fr_radiusv11_client_get_alpn(rad_listen_t *listener); +#endif + +/* + * Read from the SSL socket. Safe with either blocking or + * non-blocking IO. This level of complexity is probably not + * necessary, as each packet gets put into one SSL application + * record. When SSL has a full record, we should be able to read + * the entire packet via one SSL_read(). + * + * When SSL has a partial record, SSL_read() will return + * WANT_READ or WANT_WRITE, and zero application data. + * + * Called with the mutex held. + */ +static ssize_t proxy_tls_read(rad_listen_t *listener) +{ + int rcode; + size_t length; + uint8_t *data; + listen_socket_t *sock = listener->data; + + if (!sock->ssn->connected) { + rcode = try_connect(sock); + if (rcode <= 0) return rcode; + + if (rcode == 2) return 0; /* more negotiation needed */ + +#ifdef WITH_RADIUSV11 + if (!sock->alpn_checked && (fr_radiusv11_client_get_alpn(listener) < 0)) { + tls_socket_close(listener); + return -1; + } +#endif + } + + if (sock->ssn->clean_out.used) { + DEBUG3("(TLS) proxy writing %zu to socket", sock->ssn->clean_out.used); + /* + * Write to SSL. + */ + rcode = SSL_write(sock->ssn->ssl, sock->ssn->clean_out.data, sock->ssn->clean_out.used); + if (rcode > 0) { + if ((size_t) rcode < sock->ssn->clean_out.used) { + memmove(sock->ssn->clean_out.data, sock->ssn->clean_out.data + rcode, + sock->ssn->clean_out.used - rcode); + sock->ssn->clean_out.used -= rcode; + } else { + sock->ssn->clean_out.used = 0; + } + } + } + + /* + * Get the maximum size of data to receive. + */ + if (!sock->data) sock->data = talloc_array(sock, uint8_t, + sock->ssn->mtu); + + data = sock->data; + + if (sock->partial < 4) { + rcode = SSL_read(sock->ssn->ssl, data + sock->partial, + 4 - sock->partial); + if (rcode <= 0) { + int err = SSL_get_error(sock->ssn->ssl, rcode); + switch (err) { + + case SSL_ERROR_WANT_READ: + DEBUG3("(TLS) OpenSSL returned WANT_READ"); + return 0; + + case SSL_ERROR_WANT_WRITE: + DEBUG3("(TLS) OpenSSL returned WANT_WRITE"); + return 0; + + case SSL_ERROR_ZERO_RETURN: + /* remote end sent close_notify, send one back */ + SSL_shutdown(sock->ssn->ssl); + /* FALL-THROUGH */ + + case SSL_ERROR_SYSCALL: + do_close: + return -1; + + case SSL_ERROR_SSL: + DEBUG("(TLS) Home server has closed the connection"); + goto do_close; + + default: + tls_error_log(NULL, "Failed in proxy receive with OpenSSL error %d", err); + goto do_close; + } + } + + sock->partial = rcode; + } /* try reading the packet header */ + + if (sock->partial < 4) return 0; /* read more data */ + + length = (data[2] << 8) | data[3]; + + /* + * Do these checks only once, when we read the header. + */ + if (sock->partial == 4) { + DEBUG3("Proxy received header saying we have a packet of %u bytes", + (unsigned int) length); + + /* + * FIXME: allocate a RADIUS_PACKET, and set + * "data" to be as large as necessary. + */ + if (length > sock->ssn->mtu) { + INFO("Received packet will be too large! Set \"fragment_size = %u\"", + (data[2] << 8) | data[3]); + goto do_close; + } + } + + /* + * Try to read some more. + */ + if (sock->partial < length) { + rcode = SSL_read(sock->ssn->ssl, data + sock->partial, + length - sock->partial); + if (rcode <= 0) { + int err = SSL_get_error(sock->ssn->ssl, rcode); + switch (err) { + + case SSL_ERROR_WANT_READ: + DEBUG3("(TLS) OpenSSL returned WANT_READ"); + return 0; + + case SSL_ERROR_WANT_WRITE: + DEBUG3("(TLS) OpenSSL returned WANT_WRITE"); + return 0; + + case SSL_ERROR_ZERO_RETURN: + /* remote end sent close_notify, send one back */ + SSL_shutdown(sock->ssn->ssl); + goto do_close; + + case SSL_ERROR_SSL: + DEBUG("(TLS) Home server has closed the connection"); + goto do_close; + + default: + DEBUG("(TLS) Unexpected OpenSSL error %d", err); + goto do_close; + } + } + + sock->partial += rcode; + } + + /* + * If we're not done, say so. + * + * Otherwise, reset the partially read data flag, and say + * we have a packet. + */ + if (sock->partial < length) { + return 0; + } + + sock->partial = 0; /* we've now read the packet */ + return length; +} + + +int proxy_tls_recv(rad_listen_t *listener) +{ + listen_socket_t *sock = listener->data; + char buffer[256]; + RADIUS_PACKET *packet; + uint8_t *data; + ssize_t data_len; +#ifdef WITH_COA_TUNNEL + bool is_request = false; + RADCLIENT *client = sock->client; +#endif + + if (listener->status != RAD_LISTEN_STATUS_KNOWN) return 0; + + rad_assert(sock->ssn != NULL); + + DEBUG3("Proxy SSL socket has data to read"); + PTHREAD_MUTEX_LOCK(&sock->mutex); + data_len = proxy_tls_read(listener); + if (data_len < 0) { + tls_socket_close(listener); + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + DEBUG("Closing TLS socket to home server"); + return 0; + } + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + + if (data_len == 0) return 0; /* not done yet */ + + data = sock->data; + + packet = rad_alloc(sock, false); + packet->sockfd = listener->fd; + packet->src_ipaddr = sock->other_ipaddr; + packet->src_port = sock->other_port; + packet->dst_ipaddr = sock->my_ipaddr; + packet->dst_port = sock->my_port; + packet->code = data[0]; + packet->id = data[1]; + packet->data_len = data_len; + packet->data = talloc_array(packet, uint8_t, packet->data_len); + memcpy(packet->data, data, packet->data_len); + memcpy(packet->vector, packet->data + 4, 16); + +#ifdef WITH_RADIUSV11 + packet->radiusv11 = sock->radiusv11; + + if (sock->radiusv11) { + uint32_t id; + + memcpy(&id, data + 4, sizeof(id)); + packet->id = ntohl(id); + } + +#endif + + /* + * FIXME: Client MIB updates? + */ + switch (packet->code) { + case PW_CODE_ACCESS_ACCEPT: + case PW_CODE_ACCESS_CHALLENGE: + case PW_CODE_ACCESS_REJECT: + break; + +#ifdef WITH_ACCOUNTING + case PW_CODE_ACCOUNTING_RESPONSE: + break; +#endif + +#ifdef WITH_COA + case PW_CODE_COA_ACK: + case PW_CODE_COA_NAK: + case PW_CODE_DISCONNECT_ACK: + case PW_CODE_DISCONNECT_NAK: + break; + +#ifdef WITH_COA_TUNNEL + case PW_CODE_COA_REQUEST: + if (!listener->send_coa) goto bad_packet; + FR_STATS_INC(coa, total_requests); + is_request = true; + break; + + case PW_CODE_DISCONNECT_REQUEST: + if (!listener->send_coa) goto bad_packet; + FR_STATS_INC(dsc, total_requests); + is_request = true; + break; +#endif +#endif + + default: +#ifdef WITH_COA_TUNNEL + bad_packet: +#endif + /* + * FIXME: Update MIB for packet types? + */ + ERROR("Invalid packet code %d sent to a proxy port " + "from home server %s port %d - ID %d : IGNORED", + packet->code, + ip_ntoh(&packet->src_ipaddr, buffer, sizeof(buffer)), + packet->src_port, packet->id); + rad_free(&packet); + return 0; + } + +#ifdef WITH_COA_TUNNEL + if (is_request) { + if (!request_receive(NULL, listener, packet, client, rad_coa_recv)) { + FR_STATS_INC(auth, total_packets_dropped); + rad_free(&packet); + return 0; + } + } else +#endif + if (!request_proxy_reply(packet)) { + rad_free(&packet); + return 0; + } + + return 1; +} + + +int proxy_tls_send(rad_listen_t *listener, REQUEST *request) +{ + int rcode; + listen_socket_t *sock = listener->data; + + VERIFY_REQUEST(request); + + if ((listener->status != RAD_LISTEN_STATUS_INIT) && + (listener->status != RAD_LISTEN_STATUS_KNOWN)) return 0; + + /* + * Normal proxying calls us with the data already + * encoded. The "ping home server" code does not. So, + * if there's no packet, encode it here. + */ + if (!request->proxy->data) { + request->proxy_listener->proxy_encode(request->proxy_listener, + request); + } + + rad_assert(sock->ssn != NULL); + + if (!sock->ssn->connected) { + PTHREAD_MUTEX_LOCK(&sock->mutex); + rcode = try_connect(sock); + if (rcode <= 0) { + tls_socket_close(listener); + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + return rcode; + } + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + + /* + * More negotiation is needed, but remember to + * save this packet to an intermediate buffer. + * Once the SSL connection is established, the + * later code writes the packet to the + * connection. + */ + if (rcode == 2) { + PTHREAD_MUTEX_LOCK(&sock->mutex); + if ((sock->ssn->clean_out.used + request->proxy->data_len) > MAX_RECORD_SIZE) { + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + RERROR("(TLS) Too much data buffered during SSL_connect()"); + listener->status = RAD_LISTEN_STATUS_EOL; + radius_update_listener(listener); + return -1; + } + + memcpy(sock->ssn->clean_out.data + sock->ssn->clean_out.used, request->proxy->data, request->proxy->data_len); + sock->ssn->clean_out.used += request->proxy->data_len; + RDEBUG3("(TLS) Writing %zu bytes for later (total %zu)", request->proxy->data_len, sock->ssn->clean_out.used); + + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + return 0; + } + +#ifdef WITH_RADIUSV11 + if (!sock->alpn_checked && (fr_radiusv11_client_get_alpn(listener) < 0)) { + listener->status = RAD_LISTEN_STATUS_EOL; + radius_update_listener(listener); + return -1; + } +#endif + } + + DEBUG3("Proxy is writing %u bytes to SSL", + (unsigned int) request->proxy->data_len); + PTHREAD_MUTEX_LOCK(&sock->mutex); + + /* + * We may have previously cached data on SSL_connect(), which now needs to be written to the home server. + */ + if (sock->ssn->clean_out.used > 0) { + if ((sock->ssn->clean_out.used + request->proxy->data_len) > MAX_RECORD_SIZE) { + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + RERROR("(TLS) Too much data buffered after SSL_connect()"); + listener->status = RAD_LISTEN_STATUS_EOL; + radius_update_listener(listener); + return -1; + } + + /* + * Add in our packet. + */ + memcpy(sock->ssn->clean_out.data + sock->ssn->clean_out.used, request->proxy->data, request->proxy->data_len); + sock->ssn->clean_out.used += request->proxy->data_len; + + /* + * Write to SSL. + */ + DEBUG3("(TLS) proxy writing %zu to socket", sock->ssn->clean_out.used); + + rcode = SSL_write(sock->ssn->ssl, sock->ssn->clean_out.data, sock->ssn->clean_out.used); + if (rcode > 0) { + if ((size_t) rcode < sock->ssn->clean_out.used) { + memmove(sock->ssn->clean_out.data, sock->ssn->clean_out.data + rcode, + sock->ssn->clean_out.used - rcode); + sock->ssn->clean_out.used -= rcode; + } else { + sock->ssn->clean_out.used = 0; + } + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + return 1; + } + } else { + rcode = SSL_write(sock->ssn->ssl, request->proxy->data, + request->proxy->data_len); + } + if (rcode < 0) { + int err; + + err = ERR_get_error(); + switch (err) { + case SSL_ERROR_NONE: + break; + + case SSL_ERROR_WANT_READ: + DEBUG3("(TLS) OpenSSL returned WANT_READ"); + break; + + case SSL_ERROR_WANT_WRITE: + DEBUG3("(TLS) OpenSSL returned WANT_WRITE"); + break; + + default: + tls_error_log(NULL, "Failed in proxy send with OpenSSL error %d", err); + DEBUG("(TLS) Closing socket to home server"); + tls_socket_close(listener); + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + return 0; + } + } + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + + return 1; +} + +#ifdef WITH_COA_TUNNEL +int proxy_tls_send_reply(rad_listen_t *listener, REQUEST *request) +{ + int rcode; + listen_socket_t *sock = listener->data; + + VERIFY_REQUEST(request); + + rad_assert(sock->ssn->connected); + + if ((listener->status != RAD_LISTEN_STATUS_INIT && + (listener->status != RAD_LISTEN_STATUS_KNOWN))) return 0; + + /* + * Pack the VPs + */ + if (rad_encode(request->reply, request->packet, + request->client->secret) < 0) { + RERROR("Failed encoding packet: %s", fr_strerror()); + return 0; + } + + if (request->reply->data_len > (MAX_PACKET_LEN - 100)) { + RWARN("Packet is large, and possibly truncated - %zd vs max %d", + request->reply->data_len, MAX_PACKET_LEN); + } + + /* + * Sign the packet. + */ + if (rad_sign(request->reply, request->packet, + request->client->secret) < 0) { + RERROR("Failed signing packet: %s", fr_strerror()); + return 0; + } + + rad_assert(sock->ssn != NULL); + + DEBUG3("Proxy is writing %u bytes to SSL", + (unsigned int) request->reply->data_len); + PTHREAD_MUTEX_LOCK(&sock->mutex); + rcode = SSL_write(sock->ssn->ssl, request->reply->data, + request->reply->data_len); + if (rcode < 0) { + int err; + + err = ERR_get_error(); + switch (err) { + case SSL_ERROR_NONE: + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + DEBUG3("(TLS) SSL_write() returned %s", ERR_reason_error_string(err)); + break; /* let someone else retry */ + + default: + tls_error_log(NULL, "Failed in proxy send with OpenSSL error %d", err); + DEBUG("Closing TLS socket to home server"); + tls_socket_close(listener); + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + return 0; + } + } + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + + return 1; +} +#endif /* WITH_COA_TUNNEL */ +#endif /* WITH_PROXY */ + +#endif /* WITH_TLS */ +#endif /* WITH_TCP */ diff --git a/src/main/tmpl.c b/src/main/tmpl.c new file mode 100644 index 0000000..6ec2598 --- /dev/null +++ b/src/main/tmpl.c @@ -0,0 +1,2399 @@ +/* + * This program 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$ + * + * @brief #VALUE_PAIR template functions + * @file main/tmpl.c + * + * @ingroup AVP + * + * @copyright 2014-2015 The FreeRADIUS server project + */ +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/rad_assert.h> + +#include <ctype.h> + +/** Map #tmpl_type_t values to descriptive strings + */ +FR_NAME_NUMBER const tmpl_names[] = { + { "literal", TMPL_TYPE_LITERAL }, + { "xlat", TMPL_TYPE_XLAT }, + { "attr", TMPL_TYPE_ATTR }, + { "unknown attr", TMPL_TYPE_ATTR_UNDEFINED }, + { "list", TMPL_TYPE_LIST }, + { "regex", TMPL_TYPE_REGEX }, + { "exec", TMPL_TYPE_EXEC }, + { "data", TMPL_TYPE_DATA }, + { "parsed xlat", TMPL_TYPE_XLAT_STRUCT }, + { "parsed regex", TMPL_TYPE_REGEX_STRUCT }, + { "null", TMPL_TYPE_NULL }, + { NULL, 0 } +}; + +/** Map keywords to #pair_lists_t values + */ +const FR_NAME_NUMBER pair_lists[] = { + { "request", PAIR_LIST_REQUEST }, + { "reply", PAIR_LIST_REPLY }, + { "control", PAIR_LIST_CONTROL }, /* New name should have priority */ + { "config", PAIR_LIST_CONTROL }, + { "session-state", PAIR_LIST_STATE }, +#ifdef WITH_PROXY + { "proxy-request", PAIR_LIST_PROXY_REQUEST }, + { "proxy-reply", PAIR_LIST_PROXY_REPLY }, +#endif +#ifdef WITH_COA + { "coa", PAIR_LIST_COA }, + { "coa-reply", PAIR_LIST_COA_REPLY }, + { "disconnect", PAIR_LIST_DM }, + { "disconnect-reply", PAIR_LIST_DM_REPLY }, +#endif + { NULL , -1 } +}; + +/** Map keywords to #request_refs_t values + */ +const FR_NAME_NUMBER request_refs[] = { + { "outer", REQUEST_OUTER }, + { "current", REQUEST_CURRENT }, + { "parent", REQUEST_PARENT }, + { NULL , -1 } +}; + +/** @name Parse list and request qualifiers to #pair_lists_t and #request_refs_t values + * + * These functions also resolve #pair_lists_t and #request_refs_t values to #REQUEST + * structs and the head of #VALUE_PAIR lists in those structs. + * + * For adding new #VALUE_PAIR to the lists, the #radius_list_ctx function can be used + * to obtain the appropriate TALLOC_CTX pointer. + * + * @note These don't really have much to do with #vp_tmpl_t. They're in the same + * file as they're used almost exclusively by the tmpl_* functions. + * @{ + */ + +/** Resolve attribute name to a #pair_lists_t value. + * + * Check the name string for #pair_lists qualifiers and write a #pair_lists_t value + * for that list to out. This value may be passed to #radius_list, along with the current + * #REQUEST, to get a pointer to the actual list in the #REQUEST. + * + * If we're sure we've definitely found a list qualifier token delimiter (``:``) but the + * string doesn't match a #radius_list qualifier, return 0 and write #PAIR_LIST_UNKNOWN + * to out. + * + * If we can't find a string that looks like a request qualifier, set out to def, and + * return 0. + * + * @note #radius_list_name should be called before passing a name string that may + * contain qualifiers to #dict_attrbyname. + * + * @param[out] out Where to write the list qualifier. + * @param[in] name String containing list qualifiers to parse. + * @param[in] def the list to return if no qualifiers were found. + * @return 0 if no valid list qualifier could be found, else the number of bytes consumed. + * The caller may then advanced the name pointer by the value returned, to get the + * start of the attribute name (if any). + * + * @see pair_list + * @see radius_list + */ +size_t radius_list_name(pair_lists_t *out, char const *name, pair_lists_t def) +{ + char const *p = name; + char const *q; + + /* This should never be a NULL pointer */ + rad_assert(name); + + /* + * Try and determine the end of the token + */ + for (q = p; dict_attr_allowed_chars[(uint8_t) *q]; q++); + + switch (*q) { + /* + * It's a bareword made up entirely of dictionary chars + * check and see if it's a list qualifier, and if it's + * not, return the def and say we couldn't parse + * anything. + */ + case '\0': + *out = fr_substr2int(pair_lists, p, PAIR_LIST_UNKNOWN, (q - p)); + if (*out != PAIR_LIST_UNKNOWN) return q - p; + *out = def; + return 0; + + /* + * It may be a list qualifier delimiter. Because of tags + * We need to check that it doesn't look like a tag suffix. + * We do this by looking at the chars between ':' and the + * next token delimiter, and seeing if they're all digits. + */ + case ':': + { + char const *d = q + 1; + + if (isdigit((uint8_t) *d)) { + while (isdigit((uint8_t) *d)) d++; + + /* + * Char after the number string + * was a token delimiter, so this is a + * tag, not a list qualifier. + */ + if (!dict_attr_allowed_chars[(uint8_t) *d]) { + *out = def; + return 0; + } + } + + *out = fr_substr2int(pair_lists, p, PAIR_LIST_UNKNOWN, (q - p)); + if (*out == PAIR_LIST_UNKNOWN) return 0; + + return (q + 1) - name; /* Consume the list and delimiter */ + } + + default: + *out = def; + return 0; + } +} + +/** Resolve attribute #pair_lists_t value to an attribute list. + * + * The value returned is a pointer to the pointer of the HEAD of a #VALUE_PAIR list in the + * #REQUEST. If the head of the list changes, the pointer will still be valid. + * + * @param[in] request containing the target lists. + * @param[in] list #pair_lists_t value to resolve to #VALUE_PAIR list. Will be NULL if list + * name couldn't be resolved. + * @return a pointer to the HEAD of a list in the #REQUEST. + * + * @see tmpl_cursor_init + * @see fr_cursor_init + */ +VALUE_PAIR **radius_list(REQUEST *request, pair_lists_t list) +{ + if (!request) return NULL; + + switch (list) { + /* Don't add default */ + case PAIR_LIST_UNKNOWN: + break; + + case PAIR_LIST_REQUEST: + if (!request->packet) return NULL; + return &request->packet->vps; + + case PAIR_LIST_REPLY: + if (!request->reply) return NULL; + return &request->reply->vps; + + case PAIR_LIST_CONTROL: + return &request->config; + + case PAIR_LIST_STATE: + return &request->state; + +#ifdef WITH_PROXY + case PAIR_LIST_PROXY_REQUEST: + if (!request->proxy) break; + return &request->proxy->vps; + + case PAIR_LIST_PROXY_REPLY: + if (!request->proxy_reply) break; + return &request->proxy_reply->vps; +#endif +#ifdef WITH_COA + case PAIR_LIST_COA: + if (request->coa && + (request->coa->proxy->code == PW_CODE_COA_REQUEST)) { + return &request->coa->proxy->vps; + } + break; + + case PAIR_LIST_COA_REPLY: + if (request->coa && /* match reply with request */ + (request->coa->proxy->code == PW_CODE_COA_REQUEST) && + request->coa->proxy_reply) { + return &request->coa->proxy_reply->vps; + } + break; + + case PAIR_LIST_DM: + if (request->coa && + (request->coa->proxy->code == PW_CODE_DISCONNECT_REQUEST)) { + return &request->coa->proxy->vps; + } + break; + + case PAIR_LIST_DM_REPLY: + if (request->coa && /* match reply with request */ + (request->coa->proxy->code == PW_CODE_DISCONNECT_REQUEST) && + request->coa->proxy_reply) { + return &request->coa->proxy_reply->vps; + } + break; +#endif + } + + RWDEBUG2("List \"%s\" is not available", + fr_int2str(pair_lists, list, "<INVALID>")); + + return NULL; +} + +/** Resolve a list to the #RADIUS_PACKET holding the HEAD pointer for a #VALUE_PAIR list + * + * Returns a pointer to the #RADIUS_PACKET that holds the HEAD pointer of a given list, + * for the current #REQUEST. + * + * @param[in] request To resolve list in. + * @param[in] list #pair_lists_t value to resolve to #RADIUS_PACKET. + * @return a #RADIUS_PACKET on success, else NULL. + * + * @see radius_list + */ +RADIUS_PACKET *radius_packet(REQUEST *request, pair_lists_t list) +{ + switch (list) { + /* Don't add default */ + case PAIR_LIST_STATE: + case PAIR_LIST_CONTROL: + case PAIR_LIST_UNKNOWN: + return NULL; + + case PAIR_LIST_REQUEST: + return request->packet; + + case PAIR_LIST_REPLY: + return request->reply; + +#ifdef WITH_PROXY + case PAIR_LIST_PROXY_REQUEST: + return request->proxy; + + case PAIR_LIST_PROXY_REPLY: + return request->proxy_reply; +#endif + +#ifdef WITH_COA + case PAIR_LIST_COA: + case PAIR_LIST_DM: + return request->coa->proxy; + + case PAIR_LIST_COA_REPLY: + case PAIR_LIST_DM_REPLY: + return request->coa->proxy_reply; +#endif + } + + return NULL; +} + +/** Return the correct TALLOC_CTX to alloc #VALUE_PAIR in, for a list + * + * Allocating new #VALUE_PAIR in the context of a #REQUEST is usually wrong. + * #VALUE_PAIR should be allocated in the context of a #RADIUS_PACKET, so that if the + * #RADIUS_PACKET is freed before the #REQUEST, the associated #VALUE_PAIR lists are + * freed too. + * + * @param[in] request containing the target lists. + * @param[in] list #pair_lists_t value to resolve to TALLOC_CTX. + * @return a TALLOC_CTX on success, else NULL. + * + * @see radius_list + */ +TALLOC_CTX *radius_list_ctx(REQUEST *request, pair_lists_t list) +{ + if (!request) return NULL; + + switch (list) { + case PAIR_LIST_REQUEST: + return request->packet; + + case PAIR_LIST_REPLY: + return request->reply; + + case PAIR_LIST_CONTROL: + return request; + + case PAIR_LIST_STATE: + return request->state_ctx; + +#ifdef WITH_PROXY + case PAIR_LIST_PROXY_REQUEST: + return request->proxy; + + case PAIR_LIST_PROXY_REPLY: + return request->proxy_reply; +#endif + +#ifdef WITH_COA + case PAIR_LIST_COA: + if (!request->coa) return NULL; + rad_assert(request->coa->proxy != NULL); + if (request->coa->proxy->code != PW_CODE_COA_REQUEST) return NULL; + return request->coa->proxy; + + case PAIR_LIST_COA_REPLY: + if (!request->coa) return NULL; + rad_assert(request->coa->proxy != NULL); + if (request->coa->proxy->code != PW_CODE_COA_REQUEST) return NULL; + return request->coa->proxy_reply; + + case PAIR_LIST_DM: + if (!request->coa) return NULL; + rad_assert(request->coa->proxy != NULL); + if (request->coa->proxy->code != PW_CODE_DISCONNECT_REQUEST) return NULL; + return request->coa->proxy; + + case PAIR_LIST_DM_REPLY: + if (!request->coa) return NULL; + rad_assert(request->coa->proxy != NULL); + if (request->coa->proxy->code != PW_CODE_DISCONNECT_REQUEST) return NULL; + return request->coa->proxy_reply; +#endif + /* Don't add default */ + case PAIR_LIST_UNKNOWN: + break; + } + + return NULL; +} + +/** Resolve attribute name to a #request_refs_t value. + * + * Check the name string for qualifiers that reference a parent #REQUEST. + * + * If we find a string that matches a #request_refs qualifier, return the number of chars + * we consumed. + * + * If we're sure we've definitely found a list qualifier token delimiter (``*``) but the + * qualifier doesn't match one of the #request_refs qualifiers, return 0 and set out to + * #REQUEST_UNKNOWN. + * + * If we can't find a string that looks like a request qualifier, set out to def, and + * return 0. + * + * @param[out] out The #request_refs_t value the name resolved to (or #REQUEST_UNKNOWN). + * @param[in] name of attribute. + * @param[in] def default request ref to return if no request qualifier is present. + * @return 0 if no valid request qualifier could be found, else the number of bytes consumed. + * The caller may then advanced the name pointer by the value returned, to get the + * start of the attribute list or attribute name(if any). + * + * @see radius_list_name + * @see request_refs + */ +size_t radius_request_name(request_refs_t *out, char const *name, request_refs_t def) +{ + char const *p, *q; + + p = name; + /* + * Try and determine the end of the token + */ + for (q = p; dict_attr_allowed_chars[(uint8_t) *q] && (*q != '.') && (*q != '-'); q++); + + /* + * First token delimiter wasn't a '.' + */ + if (*q != '.') { + *out = def; + return 0; + } + + *out = fr_substr2int(request_refs, name, REQUEST_UNKNOWN, q - p); + if (*out == REQUEST_UNKNOWN) return 0; + + return (q + 1) - p; +} + +/** Resolve a #request_refs_t to a #REQUEST. + * + * Sometimes #REQUEST structs may be chained to each other, as is the case + * when internally proxying EAP. This function resolves a #request_refs_t + * to a #REQUEST higher in the chain than the current #REQUEST. + * + * @see radius_list + * @param[in,out] context #REQUEST to start resolving from, and where to write + * a pointer to the resolved #REQUEST back to. + * @param[in] name (request) to resolve. + * @return 0 if request is valid in this context, else -1. + */ +int radius_request(REQUEST **context, request_refs_t name) +{ + REQUEST *request = *context; + + switch (name) { + case REQUEST_CURRENT: + return 0; + + case REQUEST_PARENT: /* for future use in request chaining */ + case REQUEST_OUTER: + if (!request->parent) { + return -1; + } + *context = request->parent; + break; + + case REQUEST_UNKNOWN: + default: + rad_assert(0); + return -1; + } + + return 0; +} +/** @} */ + +/** @name Alloc or initialise #vp_tmpl_t + * + * @note Should not usually be called outside of tmpl_* functions, use one of + * the tmpl_*from_* functions instead. + * @{ + */ + +/** Initialise stack allocated #vp_tmpl_t + * + * @note Name is not strdupe'd or memcpy'd so must be available, and must not change + * for the lifetime of the #vp_tmpl_t. + * + * @param[out] vpt to initialise. + * @param[in] type to set in the #vp_tmpl_t. + * @param[in] name of the #vp_tmpl_t. + * @param[in] len The length of the buffer (or a substring of the buffer) pointed to by name. + * If < 0 strlen will be used to determine the length. + * @return a pointer to the initialised #vp_tmpl_t. The same value as + * vpt. + */ +vp_tmpl_t *tmpl_init(vp_tmpl_t *vpt, tmpl_type_t type, char const *name, ssize_t len) +{ + rad_assert(vpt); + rad_assert(type != TMPL_TYPE_UNKNOWN); + rad_assert(type <= TMPL_TYPE_NULL); + + memset(vpt, 0, sizeof(vp_tmpl_t)); + vpt->type = type; + + if (name) { + vpt->name = name; + vpt->len = len < 0 ? strlen(name) : + (size_t) len; + } + return vpt; +} + +/** Create a new heap allocated #vp_tmpl_t + * + * @param[in,out] ctx to allocate in. + * @param[in] type to set in the #vp_tmpl_t. + * @param[in] name of the #vp_tmpl_t (will be copied to a new talloc buffer parented + * by the #vp_tmpl_t). + * @param[in] len The length of the buffer (or a substring of the buffer) pointed to by name. + * If < 0 strlen will be used to determine the length. + * @return the newly allocated #vp_tmpl_t. + */ +vp_tmpl_t *tmpl_alloc(TALLOC_CTX *ctx, tmpl_type_t type, char const *name, ssize_t len) +{ + vp_tmpl_t *vpt; + + rad_assert(type != TMPL_TYPE_UNKNOWN); + rad_assert(type <= TMPL_TYPE_NULL); + + vpt = talloc_zero(ctx, vp_tmpl_t); + if (!vpt) return NULL; + vpt->type = type; + if (name) { + vpt->name = talloc_bstrndup(vpt, name, len < 0 ? strlen(name) : (size_t)len); + vpt->len = talloc_array_length(vpt->name) - 1; + } + + return vpt; +} +/* @} **/ + +/** @name Create new #vp_tmpl_t from a string + * + * @{ + */ +/** Parse a string into a TMPL_TYPE_ATTR_* or #TMPL_TYPE_LIST type #vp_tmpl_t + * + * @note The name field is just a copy of the input pointer, if you know that string might be + * freed before you're done with the #vp_tmpl_t use #tmpl_afrom_attr_str + * instead. + * + * @param[out] vpt to modify. + * @param[in] name of attribute including #request_refs and #pair_lists qualifiers. + * If only #request_refs and #pair_lists qualifiers are found, a #TMPL_TYPE_LIST + * #vp_tmpl_t will be produced. + * @param[in] request_def The default #REQUEST to set if no #request_refs qualifiers are + * found in name. + * @param[in] list_def The default list to set if no #pair_lists qualifiers are found in + * name. + * @param[in] allow_unknown If true attributes in the format accepted by + * #dict_unknown_from_substr will be allowed, even if they're not in the main + * dictionaries. + * If an unknown attribute is found a #TMPL_TYPE_ATTR #vp_tmpl_t will be + * produced with the unknown #DICT_ATTR stored in the ``unknown.da`` buffer. + * This #DICT_ATTR will have its ``flags.is_unknown`` field set to true. + * If #tmpl_from_attr_substr is being called on startup, the #vp_tmpl_t may be + * passed to #tmpl_define_unknown_attr to add the unknown attribute to the main + * dictionary. + * If the unknown attribute is not added to the main dictionary the #vp_tmpl_t + * cannot be used to search for a #VALUE_PAIR in a #REQUEST. + * @param[in] allow_undefined If true, we don't generate a parse error on unknown attributes. + * If an unknown attribute is found a #TMPL_TYPE_ATTR_UNDEFINED #vp_tmpl_t + * will be produced. + * @return <= 0 on error (offset as negative integer), > 0 on success + * (number of bytes parsed). + * + * @see REMARKER to produce pretty error markers from the return value. + */ +ssize_t tmpl_from_attr_substr(vp_tmpl_t *vpt, char const *name, + request_refs_t request_def, pair_lists_t list_def, + bool allow_unknown, bool allow_undefined) +{ + char const *p; + long num; + char *q; + tmpl_type_t type = TMPL_TYPE_ATTR; + + value_pair_tmpl_attr_t attr; /* So we don't fill the tmpl with junk and then error out */ + + memset(vpt, 0, sizeof(*vpt)); + memset(&attr, 0, sizeof(attr)); + + p = name; + + if (*p == '&') p++; + + p += radius_request_name(&attr.request, p, request_def); + if (attr.request == REQUEST_UNKNOWN) { + fr_strerror_printf("Invalid request qualifier"); + return -(p - name); + } + + /* + * Finding a list qualifier is optional + */ + p += radius_list_name(&attr.list, p, list_def); + if (attr.list == PAIR_LIST_UNKNOWN) { + fr_strerror_printf("Invalid list qualifier"); + return -(p - name); + } + + attr.tag = TAG_ANY; + attr.num = NUM_ANY; + + /* + * This may be just a bare list, but it can still + * have instance selectors and tag selectors. + */ + switch (*p) { + case '\0': + type = TMPL_TYPE_LIST; + attr.num = NUM_ALL; /* Hack - Should be removed once tests are updated */ + goto finish; + + case '[': + type = TMPL_TYPE_LIST; + attr.num = NUM_ALL; /* Hack - Should be removed once tests are updated */ + goto do_num; + + default: + break; + } + + attr.da = dict_attrbyname_substr(&p); + if (!attr.da) { + char const *a; + + /* + * Record start of attribute in case we need to error out. + */ + a = p; + + fr_strerror(); /* Clear out any existing errors */ + + /* + * Attr-1.2.3.4 is OK. + */ + if (dict_unknown_from_substr((DICT_ATTR *)&attr.unknown.da, &p) == 0) { + /* + * Check what we just parsed really hasn't been defined + * in the main dictionaries. + * + * If it has, parsing is the same as if the attribute + * name had been used instead of its OID. + */ + attr.da = dict_attrbyvalue(((DICT_ATTR *)&attr.unknown.da)->attr, + ((DICT_ATTR *)&attr.unknown.da)->vendor); + if (attr.da) { + vpt->auto_converted = true; + goto do_num; + } + + if (!allow_unknown) { + fr_strerror_printf("Unknown attribute"); + return -(a - name); + } + + /* + * Unknown attributes can't be encoded, as we don't + * know how to encode them! + */ + attr.da = (DICT_ATTR *)&attr.unknown.da; + + goto do_num; /* unknown attributes can't have tags */ + } + + /* + * Can't parse it as an attribute, might be a literal string + * let the caller decide. + * + * Don't alter the fr_strerror buffer, should contain the parse + * error from dict_unknown_from_substr. + */ + if (!allow_undefined) return -(a - name); + + /* + * Copy the name to a field for later resolution + */ + type = TMPL_TYPE_ATTR_UNDEFINED; + for (q = attr.unknown.name; dict_attr_allowed_chars[(int) *p]; *q++ = *p++) { + if (q >= (attr.unknown.name + sizeof(attr.unknown.name) - 1)) { + fr_strerror_printf("Attribute name is too long"); + return -(p - name); + } + } + *q = '\0'; + + goto do_num; + } + + /* + * The string MIGHT have a tag. + */ + if (*p == ':') { + if (attr.da && !attr.da->flags.has_tag) { /* Lists don't have a da */ + fr_strerror_printf("Attribute '%s' cannot have a tag", attr.da->name); + return -(p - name); + } + + num = strtol(p + 1, &q, 10); + if ((num > 0x1f) || (num < 0)) { + fr_strerror_printf("Invalid tag value '%li' (should be between 0-31)", num); + return -((p + 1)- name); + } + + attr.tag = num; + p = q; + } + +do_num: + if (*p == '\0') goto finish; + + if (*p == '[') { + p++; + + switch (*p) { + case '#': + attr.num = NUM_COUNT; + p++; + break; + + case '*': + attr.num = NUM_ALL; + p++; + break; + + case 'n': + attr.num = NUM_LAST; + p++; + break; + + default: + num = strtol(p, &q, 10); + if (p == q) { + fr_strerror_printf("Array index is not an integer"); + return -(p - name); + } + + if ((num > 1000) || (num < 0)) { + fr_strerror_printf("Invalid array reference '%li' (should be between 0-1000)", num); + return -(p - name); + } + attr.num = num; + p = q; + break; + } + + if (*p != ']') { + fr_strerror_printf("No closing ']' for array index"); + return -(p - name); + } + p++; + } + +finish: + vpt->type = type; + vpt->name = name; + vpt->len = p - name; + + /* + * Copy over the attribute definition, now we're + * sure what we were passed is valid. + */ + memcpy(&vpt->data.attribute, &attr, sizeof(vpt->data.attribute)); + if ((vpt->type == TMPL_TYPE_ATTR) && attr.da->flags.is_unknown) { + vpt->tmpl_da = (DICT_ATTR *)&vpt->data.attribute.unknown.da; + } + + VERIFY_TMPL(vpt); + + return vpt->len; +} + +/** Parse a string into a TMPL_TYPE_ATTR_* or #TMPL_TYPE_LIST type #vp_tmpl_t + * + * @note Unlike #tmpl_from_attr_substr this function will error out if the entire + * name string isn't parsed. + * + * @copydetails tmpl_from_attr_substr + */ +ssize_t tmpl_from_attr_str(vp_tmpl_t *vpt, char const *name, + request_refs_t request_def, pair_lists_t list_def, + bool allow_unknown, bool allow_undefined) +{ + ssize_t slen; + + slen = tmpl_from_attr_substr(vpt, name, request_def, list_def, allow_unknown, allow_undefined); + if (slen <= 0) return slen; + if (name[slen] != '\0') { + /* This looks wrong, but it produces meaningful errors for unknown attrs with tags */ + fr_strerror_printf("Unexpected text after %s", fr_int2str(tmpl_names, vpt->type, "<INVALID>")); + return -slen; + } + + VERIFY_TMPL(vpt); + + return slen; +} + +/** Parse a string into a TMPL_TYPE_ATTR_* or #TMPL_TYPE_LIST type #vp_tmpl_t + * + * @param[in,out] ctx to allocate #vp_tmpl_t in. + * @param[out] out Where to write pointer to new #vp_tmpl_t. + * @param[in] name of attribute including #request_refs and #pair_lists qualifiers. + * If only #request_refs #pair_lists qualifiers are found, a #TMPL_TYPE_LIST + * #vp_tmpl_t will be produced. + * @param[in] request_def The default #REQUEST to set if no #request_refs qualifiers are + * found in name. + * @param[in] list_def The default list to set if no #pair_lists qualifiers are found in + * name. + * @param[in] allow_unknown If true attributes in the format accepted by + * #dict_unknown_from_substr will be allowed, even if they're not in the main + * dictionaries. + * If an unknown attribute is found a #TMPL_TYPE_ATTR #vp_tmpl_t will be + * produced with the unknown #DICT_ATTR stored in the ``unknown.da`` buffer. + * This #DICT_ATTR will have its ``flags.is_unknown`` field set to true. + * If #tmpl_from_attr_substr is being called on startup, the #vp_tmpl_t may be + * passed to #tmpl_define_unknown_attr to add the unknown attribute to the main + * dictionary. + * If the unknown attribute is not added to the main dictionary the #vp_tmpl_t + * cannot be used to search for a #VALUE_PAIR in a #REQUEST. + * @param[in] allow_undefined If true, we don't generate a parse error on unknown attributes. + * If an unknown attribute is found a #TMPL_TYPE_ATTR_UNDEFINED #vp_tmpl_t + * will be produced. + * @return <= 0 on error (offset as negative integer), > 0 on success + * (number of bytes parsed). + * + * @see REMARKER to produce pretty error markers from the return value. + */ +ssize_t tmpl_afrom_attr_substr(TALLOC_CTX *ctx, vp_tmpl_t **out, char const *name, + request_refs_t request_def, pair_lists_t list_def, + bool allow_unknown, bool allow_undefined) +{ + ssize_t slen; + vp_tmpl_t *vpt; + + MEM(vpt = talloc(ctx, vp_tmpl_t)); /* tmpl_from_attr_substr zeros it */ + + slen = tmpl_from_attr_substr(vpt, name, request_def, list_def, allow_unknown, allow_undefined); + if (slen <= 0) { + TALLOC_FREE(vpt); + return slen; + } + vpt->name = talloc_strndup(vpt, vpt->name, slen); + + VERIFY_TMPL(vpt); + + *out = vpt; + + return slen; +} + +/** Parse a string into a TMPL_TYPE_ATTR_* or #TMPL_TYPE_LIST type #vp_tmpl_t + * + * @note Unlike #tmpl_afrom_attr_substr this function will error out if the entire + * name string isn't parsed. + * + * @copydetails tmpl_afrom_attr_substr + */ +ssize_t tmpl_afrom_attr_str(TALLOC_CTX *ctx, vp_tmpl_t **out, char const *name, + request_refs_t request_def, pair_lists_t list_def, + bool allow_unknown, bool allow_undefined) +{ + ssize_t slen; + vp_tmpl_t *vpt; + + MEM(vpt = talloc(ctx, vp_tmpl_t)); /* tmpl_from_attr_substr zeros it */ + + slen = tmpl_from_attr_substr(vpt, name, request_def, list_def, allow_unknown, allow_undefined); + if (slen <= 0) { + TALLOC_FREE(vpt); + return slen; + } + if (name[slen] != '\0') { + /* This looks wrong, but it produces meaningful errors for unknown attrs with tags */ + fr_strerror_printf("Unexpected text after %s", fr_int2str(tmpl_names, vpt->type, "<INVALID>")); + TALLOC_FREE(vpt); + return -slen; + } + vpt->name = talloc_strndup(vpt, vpt->name, vpt->len); + + VERIFY_TMPL(vpt); + + *out = vpt; + + return slen; +} + +/** Convert an arbitrary string into a #vp_tmpl_t + * + * @note Unlike #tmpl_afrom_attr_str return code 0 doesn't necessarily indicate failure, + * may just mean a 0 length string was parsed. + * + * @note xlats and regexes are left uncompiled. This is to support the two pass parsing + * done by the modcall code. Compilation on pass1 of that code could fail, as + * attributes or xlat functions registered by modules may not be available (yet). + * + * @note For details of attribute parsing see #tmpl_from_attr_substr. + * + * @param[in,out] ctx To allocate #vp_tmpl_t in. + * @param[out] out Where to write the pointer to the new #vp_tmpl_t. + * @param[in] in String to convert to a #vp_tmpl_t. + * @param[in] inlen length of string to convert. + * @param[in] type of quoting around value. May be one of: + * - #T_BARE_WORD - If string begins with ``&`` produces #TMPL_TYPE_ATTR, + * #TMPL_TYPE_ATTR_UNDEFINED, #TMPL_TYPE_LIST or error. + * If string does not begin with ``&`` produces #TMPL_TYPE_LITERAL, + * #TMPL_TYPE_ATTR or #TMPL_TYPE_LIST. + * - #T_SINGLE_QUOTED_STRING - Produces #TMPL_TYPE_LITERAL + * - #T_DOUBLE_QUOTED_STRING - Produces #TMPL_TYPE_XLAT or #TMPL_TYPE_LITERAL (if + * string doesn't contain ``%``). + * - #T_BACK_QUOTED_STRING - Produces #TMPL_TYPE_EXEC + * - #T_OP_REG_EQ - Produces #TMPL_TYPE_REGEX + * @param[in] request_def The default #REQUEST to set if no #request_refs qualifiers are + * found in name. + * @param[in] list_def The default list to set if no #pair_lists qualifiers are found in + * name. + * @param[in] do_unescape whether or not we should do unescaping. Should be false if the + * caller already did it. + * @return <= 0 on error (offset as negative integer), > 0 on success + * (number of bytes parsed). + * @see REMARKER to produce pretty error markers from the return value. + * + * @see tmpl_from_attr_substr + */ +ssize_t tmpl_afrom_str(TALLOC_CTX *ctx, vp_tmpl_t **out, char const *in, size_t inlen, FR_TOKEN type, + request_refs_t request_def, pair_lists_t list_def, bool do_unescape) +{ + bool do_xlat; + char quote; + char const *p; + ssize_t slen; + PW_TYPE data_type = PW_TYPE_STRING; + vp_tmpl_t *vpt = NULL; + value_data_t data; + + switch (type) { + case T_BARE_WORD: + /* + * If we can parse it as an attribute, it's an attribute. + * Otherwise, treat it as a literal. + */ + quote = '\0'; + + slen = tmpl_afrom_attr_str(ctx, &vpt, in, request_def, list_def, true, (in[0] == '&')); + if ((in[0] == '&') && (slen <= 0)) return slen; + if (slen > 0) break; + goto parse; + + case T_SINGLE_QUOTED_STRING: + quote = '\''; + + parse: + if (cf_new_escape && do_unescape) { + slen = value_data_from_str(ctx, &data, &data_type, NULL, in, inlen, quote); + if (slen < 0) return 0; + + vpt = tmpl_alloc(ctx, TMPL_TYPE_LITERAL, data.strvalue, talloc_array_length(data.strvalue) - 1); + talloc_free(data.ptr); + } else { + vpt = tmpl_alloc(ctx, TMPL_TYPE_LITERAL, in, inlen); + } + vpt->quote = quote; + slen = vpt->len; + break; + + case T_DOUBLE_QUOTED_STRING: + do_xlat = false; + + p = in; + while (*p) { + if (do_unescape) { /* otherwise \ is just another character */ + if (*p == '\\') { + if (!p[1]) break; + p += 2; + continue; + } + } + + if (*p == '%') { + do_xlat = true; + break; + } + + p++; + } + + /* + * If the double quoted string needs to be + * expanded at run time, make it an xlat + * expansion. Otherwise, convert it to be a + * literal. + */ + if (cf_new_escape && do_unescape) { + slen = value_data_from_str(ctx, &data, &data_type, NULL, in, inlen, '"'); + if (slen < 0) return slen; + + if (do_xlat) { + vpt = tmpl_alloc(ctx, TMPL_TYPE_XLAT, data.strvalue, + talloc_array_length(data.strvalue) - 1); + } else { + vpt = tmpl_alloc(ctx, TMPL_TYPE_LITERAL, data.strvalue, + talloc_array_length(data.strvalue) - 1); + vpt->quote = '"'; + } + talloc_free(data.ptr); + } else { + if (do_xlat) { + vpt = tmpl_alloc(ctx, TMPL_TYPE_XLAT, in, inlen); + } else { + vpt = tmpl_alloc(ctx, TMPL_TYPE_LITERAL, in, inlen); + vpt->quote = '"'; + } + } + slen = vpt->len; + break; + + case T_BACK_QUOTED_STRING: + if (cf_new_escape && do_unescape) { + slen = value_data_from_str(ctx, &data, &data_type, NULL, in, inlen, '`'); + if (slen < 0) return slen; + + vpt = tmpl_alloc(ctx, TMPL_TYPE_EXEC, data.strvalue, talloc_array_length(data.strvalue) - 1); + talloc_free(data.ptr); + } else { + vpt = tmpl_alloc(ctx, TMPL_TYPE_EXEC, in, inlen); + } + slen = vpt->len; + break; + + case T_OP_REG_EQ: /* hack */ + vpt = tmpl_alloc(ctx, TMPL_TYPE_REGEX, in, inlen); + slen = vpt->len; + break; + + default: + rad_assert(0); + return 0; /* 0 is an error here too */ + } + + rad_assert((slen >= 0) && (vpt != NULL)); + + VERIFY_TMPL(vpt); + + *out = vpt; + + return slen; +} +/* @} **/ + +/** @name Cast or convert #vp_tmpl_t + * + * #tmpl_cast_in_place can be used to convert #TMPL_TYPE_LITERAL to a #TMPL_TYPE_DATA of a + * specified #PW_TYPE. + * + * #tmpl_cast_in_place_str does the same as #tmpl_cast_in_place, but will always convert to + * #PW_TYPE #PW_TYPE_STRING. + * + * #tmpl_cast_to_vp does the same as #tmpl_cast_in_place, but outputs a #VALUE_PAIR. + * + * #tmpl_define_unknown_attr converts a #TMPL_TYPE_ATTR with an unknown #DICT_ATTR to a + * #TMPL_TYPE_ATTR with a known #DICT_ATTR, by adding the unknown #DICT_ATTR to the main + * dictionary, and updating the ``tmpl_da`` pointer. + * @{ + */ + +/** Convert #vp_tmpl_t of type #TMPL_TYPE_LITERAL or #TMPL_TYPE_DATA to #TMPL_TYPE_DATA of type specified + * + * @note Conversion is done in place. + * @note Irrespective of whether the #vp_tmpl_t was #TMPL_TYPE_LITERAL or #TMPL_TYPE_DATA, + * on successful cast it will be #TMPL_TYPE_DATA. + * + * @param[in,out] vpt The template to modify. Must be of type #TMPL_TYPE_LITERAL + * or #TMPL_TYPE_DATA. + * @param[in] type to cast to. + * @param[in] enumv Enumerated dictionary values associated with a #DICT_ATTR. + * @return 0 on success, -1 on failure. + */ +int tmpl_cast_in_place(vp_tmpl_t *vpt, PW_TYPE type, DICT_ATTR const *enumv) +{ + ssize_t ret; + + VERIFY_TMPL(vpt); + + rad_assert(vpt != NULL); + rad_assert((vpt->type == TMPL_TYPE_LITERAL) || (vpt->type == TMPL_TYPE_DATA)); + + switch (vpt->type) { + case TMPL_TYPE_LITERAL: + /* + * Why do we pass a pointer to the tmpl type? Goddamn WiMAX. + */ + ret = value_data_from_str(vpt, &vpt->tmpl_data_value, &type, + enumv, vpt->name, vpt->len, '\0'); + if (ret < 0) { + VERIFY_TMPL(vpt); + return -1; + } + + vpt->tmpl_data_type = type; + vpt->type = TMPL_TYPE_DATA; + vpt->tmpl_data_length = (size_t) ret; + break; + + case TMPL_TYPE_DATA: + { + value_data_t new; + + if (type == vpt->tmpl_data_type) return 0; /* noop */ + + ret = value_data_cast(vpt, &new, type, enumv, vpt->tmpl_data_type, + NULL, &vpt->tmpl_data_value, vpt->tmpl_data_length); + if (ret < 0) return -1; + + /* + * Free old value buffers + */ + switch (vpt->tmpl_data_type) { + case PW_TYPE_STRING: + case PW_TYPE_OCTETS: + talloc_free(vpt->tmpl_data_value.ptr); + break; + + default: + break; + } + + memcpy(&vpt->tmpl_data_value, &new, sizeof(vpt->tmpl_data_value)); + vpt->tmpl_data_type = type; + vpt->tmpl_data_length = (size_t) ret; + } + break; + + default: + rad_assert(0); + } + + VERIFY_TMPL(vpt); + + return 0; +} + +/** Convert #vp_tmpl_t of type #TMPL_TYPE_LITERAL to #TMPL_TYPE_DATA of type #PW_TYPE_STRING + * + * @note Conversion is done in place. + * + * @param[in,out] vpt The template to modify. Must be of type #TMPL_TYPE_LITERAL. + */ +void tmpl_cast_in_place_str(vp_tmpl_t *vpt) +{ + rad_assert(vpt != NULL); + rad_assert(vpt->type == TMPL_TYPE_LITERAL); + + vpt->tmpl_data.vp_strvalue = talloc_typed_strdup(vpt, vpt->name); + rad_assert(vpt->tmpl_data.vp_strvalue != NULL); + + vpt->type = TMPL_TYPE_DATA; + vpt->tmpl_data_type = PW_TYPE_STRING; + vpt->tmpl_data_length = talloc_array_length(vpt->tmpl_data.vp_strvalue) - 1; +} + +/** Expand a #vp_tmpl_t to a string, parse it as an attribute of type cast, create a #VALUE_PAIR from the result + * + * @note Like #tmpl_expand, but produces a #VALUE_PAIR. + * + * @param out Where to write pointer to the new #VALUE_PAIR. + * @param request The current #REQUEST. + * @param vpt to cast. Must be one of the following types: + * - #TMPL_TYPE_LITERAL + * - #TMPL_TYPE_EXEC + * - #TMPL_TYPE_XLAT + * - #TMPL_TYPE_XLAT_STRUCT + * - #TMPL_TYPE_ATTR + * - #TMPL_TYPE_DATA + * @param cast type of #VALUE_PAIR to create. + * @return 0 on success, -1 on failure. + */ +int tmpl_cast_to_vp(VALUE_PAIR **out, REQUEST *request, + vp_tmpl_t const *vpt, DICT_ATTR const *cast) +{ + int rcode; + VALUE_PAIR *vp; + value_data_t data; + char *p; + + VERIFY_TMPL(vpt); + + *out = NULL; + + vp = fr_pair_afrom_da(request, cast); + if (!vp) return -1; + + if (vpt->type == TMPL_TYPE_DATA) { + VERIFY_VP(vp); + rad_assert(vp->da->type == vpt->tmpl_data_type); + + value_data_copy(vp, &vp->data, vpt->tmpl_data_type, &vpt->tmpl_data_value, vpt->tmpl_data_length); + *out = vp; + return 0; + } + + rcode = tmpl_aexpand(vp, &p, request, vpt, NULL, NULL); + if (rcode < 0) { + fr_pair_list_free(&vp); + return rcode; + } + data.strvalue = p; + + /* + * New escapes: strings are in binary form. + */ + if (cf_new_escape && (vp->da->type == PW_TYPE_STRING)) { + vp->data.ptr = talloc_steal(vp, data.ptr); + vp->vp_length = rcode; + + } else if (fr_pair_value_from_str(vp, data.strvalue, rcode) < 0) { + talloc_free(data.ptr); + fr_pair_list_free(&vp); + return -1; + } + + /* + * Copy over any additional fields needed... + */ + if ((vpt->type == TMPL_TYPE_ATTR) && vp->da->flags.has_tag) { + vp->tag = vpt->tmpl_tag; + } + + *out = vp; + return 0; +} + +/** Add an unknown #DICT_ATTR specified by a #vp_tmpl_t to the main dictionary + * + * @param vpt to add. ``tmpl_da`` pointer will be updated to point to the + * #DICT_ATTR inserted into the dictionary. + * @return 0 on success, -1 on failure. + */ +int tmpl_define_unknown_attr(vp_tmpl_t *vpt) +{ + DICT_ATTR const *da; + + if (!vpt) return -1; + + VERIFY_TMPL(vpt); + + if (vpt->type != TMPL_TYPE_ATTR) return 0; + + if (!vpt->tmpl_da->flags.is_unknown) return 0; + + da = dict_unknown_add(vpt->tmpl_da); + if (!da) return -1; + vpt->tmpl_da = da; + return 0; +} +/* @} **/ + +/** @name Resolve a #vp_tmpl_t outputting the result in various formats + * + * @{ + */ + +/** Expand a #vp_tmpl_t to a string writing the result to a buffer + * + * The intended use of #tmpl_expand and #tmpl_aexpand is for modules to easily convert a #vp_tmpl_t + * provided by the conf parser, into a usable value. + * The value returned should be raw and undoctored for #PW_TYPE_STRING and #PW_TYPE_OCTETS types, + * and the printable (string) version of the data for all others. + * + * Depending what arguments are passed, either copies the value to buff, or writes a pointer + * to a string buffer to out. This allows the most efficient access to the value resolved by + * the #vp_tmpl_t, avoiding unecessary string copies. + * + * @note This function is used where raw string values are needed, which may mean the string + * returned may be binary data or contain unprintable chars. #fr_prints or #fr_aprints should + * be used before using these values in debug statements. #is_printable can be used to check + * if the string only contains printable chars. + * + * @param out Where to write a pointer to the string buffer. On return may point to buff if + * buff was used to store the value. Otherwise will point to a #value_data_t buffer, + * or the name of the template. To force copying to buff, out should be NULL. + * @param buff Expansion buffer, may be NULL if out is not NULL, and processing #TMPL_TYPE_LITERAL + * or string types. + * @param bufflen Length of expansion buffer. + * @param request Current request. + * @param vpt to expand. Must be one of the following types: + * - #TMPL_TYPE_LITERAL + * - #TMPL_TYPE_EXEC + * - #TMPL_TYPE_XLAT + * - #TMPL_TYPE_XLAT_STRUCT + * - #TMPL_TYPE_ATTR + * - #TMPL_TYPE_DATA + * @param escape xlat escape function (only used for xlat types). + * @param escape_ctx xlat escape function data. + * @return -1 on error, else the length of data written to buff, or pointed to by out. + */ +ssize_t tmpl_expand(char const **out, char *buff, size_t bufflen, REQUEST *request, + vp_tmpl_t const *vpt, xlat_escape_t escape, void *escape_ctx) +{ + VALUE_PAIR *vp; + ssize_t slen = -1; /* quiet compiler */ + + VERIFY_TMPL(vpt); + + rad_assert(vpt->type != TMPL_TYPE_LIST); + + if (out) *out = NULL; + + switch (vpt->type) { + case TMPL_TYPE_LITERAL: + RDEBUG4("EXPAND TMPL LITERAL"); + + if (!out) { + rad_assert(buff); + memcpy(buff, vpt->name, vpt->len >= bufflen ? bufflen : vpt->len + 1); + } else { + *out = vpt->name; + } + return vpt->len; + + case TMPL_TYPE_EXEC: + { + RDEBUG4("EXPAND TMPL EXEC"); + rad_assert(buff); + if (radius_exec_program(request, buff, bufflen, NULL, request, vpt->name, NULL, + true, false, EXEC_TIMEOUT) != 0) { + return -1; + } + slen = strlen(buff); + if (out) *out = buff; + } + break; + + case TMPL_TYPE_XLAT: + RDEBUG4("EXPAND TMPL XLAT"); + rad_assert(buff); + /* Error in expansion, this is distinct from zero length expansion */ + slen = radius_xlat(buff, bufflen, request, vpt->name, escape, escape_ctx); + if (slen < 0) return slen; + if (out) *out = buff; + break; + + case TMPL_TYPE_XLAT_STRUCT: + RDEBUG4("EXPAND TMPL XLAT STRUCT"); + rad_assert(buff); + /* Error in expansion, this is distinct from zero length expansion */ + slen = radius_xlat_struct(buff, bufflen, request, vpt->tmpl_xlat, escape, escape_ctx); + if (slen < 0) { + return slen; + } + slen = strlen(buff); + if (out) *out = buff; + break; + + case TMPL_TYPE_ATTR: + { + int ret; + + RDEBUG4("EXPAND TMPL ATTR"); + rad_assert(buff); + ret = tmpl_find_vp(&vp, request, vpt); + if (ret < 0) return -2; + + if (out && ((vp->da->type == PW_TYPE_STRING) || (vp->da->type == PW_TYPE_OCTETS))) { + *out = vp->data.ptr; + slen = vp->vp_length; + } else { + if (out) *out = buff; + slen = vp_prints_value(buff, bufflen, vp, '\0'); + } + } + break; + + case TMPL_TYPE_DATA: + { + RDEBUG4("EXPAND TMPL DATA"); + + if (out && ((vpt->tmpl_data_type == PW_TYPE_STRING) || (vpt->tmpl_data_type == PW_TYPE_OCTETS))) { + *out = vpt->tmpl_data_value.ptr; + slen = vpt->tmpl_data_length; + } else { + if (out) *out = buff; + /** + * @todo tmpl_expand should accept an enumv da from the lhs of the map. + */ + slen = value_data_prints(buff, bufflen, vpt->tmpl_data_type, NULL, &vpt->tmpl_data_value, vpt->tmpl_data_length, '\0'); + } + } + break; + + /* + * We should never be expanding these. + */ + case TMPL_TYPE_UNKNOWN: + case TMPL_TYPE_NULL: + case TMPL_TYPE_LIST: + case TMPL_TYPE_REGEX: + case TMPL_TYPE_ATTR_UNDEFINED: + case TMPL_TYPE_REGEX_STRUCT: + rad_assert(0 == 1); + slen = -1; + break; + } + + if (slen < 0) return slen; + + +#if 0 + /* + * If we're doing correct escapes, we may have to re-parse the string. + * If the string is from another expansion, it needs re-parsing. + * Or, if it's from a "string" attribute, it needs re-parsing. + * Integers, IP addresses, etc. don't need re-parsing. + */ + if (cf_new_escape && (vpt->type != TMPL_TYPE_ATTR)) { + value_data_t vd; + int ret; + + PW_TYPE type = PW_TYPE_STRING; + + slen = value_data_from_str(ctx, &vd, &type, NULL, *out, slen, '"'); + talloc_free(*out); /* free the old value */ + *out = vd.ptr; + } +#endif + + if (vpt->type == TMPL_TYPE_XLAT_STRUCT) { + RDEBUG2("EXPAND %s", vpt->name); /* xlat_struct doesn't do this */ + RDEBUG2(" --> %s", buff); + } + + return slen; +} + +/** Expand a template to a string, allocing a new buffer to hold the string + * + * The intended use of #tmpl_expand and #tmpl_aexpand is for modules to easily convert a #vp_tmpl_t + * provided by the conf parser, into a usable value. + * The value returned should be raw and undoctored for #PW_TYPE_STRING and #PW_TYPE_OCTETS types, + * and the printable (string) version of the data for all others. + * + * This function will always duplicate values, whereas #tmpl_expand may return a pointer to an + * existing buffer. + * + * @note This function is used where raw string values are needed, which may mean the string + * returned may be binary data or contain unprintable chars. #fr_prints or #fr_aprints should + * be used before using these values in debug statements. #is_printable can be used to check + * if the string only contains printable chars. + * + * @note The type (char or uint8_t) can be obtained with talloc_get_type, and may be used as a + * hint as to how to process or print the data. + * + * @param ctx to allocate new buffer in. + * @param out Where to write pointer to the new buffer. + * @param request Current request. + * @param vpt to expand. Must be one of the following types: + * - #TMPL_TYPE_LITERAL + * - #TMPL_TYPE_EXEC + * - #TMPL_TYPE_XLAT + * - #TMPL_TYPE_XLAT_STRUCT + * - #TMPL_TYPE_ATTR + * - #TMPL_TYPE_DATA + * @param escape xlat escape function (only used for xlat types). + * @param escape_ctx xlat escape function data (only used for xlat types). + * @return + * - -1 on failure. + * - The length of data written to buff, or pointed to by out. + */ +ssize_t tmpl_aexpand(TALLOC_CTX *ctx, char **out, REQUEST *request, vp_tmpl_t const *vpt, + xlat_escape_t escape, void *escape_ctx) +{ + VALUE_PAIR *vp; + ssize_t slen = -1; /* quiet compiler */ + + rad_assert(vpt->type != TMPL_TYPE_LIST); + + VERIFY_TMPL(vpt); + + *out = NULL; + + switch (vpt->type) { + case TMPL_TYPE_LITERAL: + RDEBUG4("EXPAND TMPL LITERAL"); + *out = talloc_bstrndup(ctx, vpt->name, vpt->len); + return vpt->len; + + case TMPL_TYPE_EXEC: + { + char *buff = NULL; + + RDEBUG4("EXPAND TMPL EXEC"); + buff = talloc_array(ctx, char, 1024); + if (radius_exec_program(request, buff, 1024, NULL, request, vpt->name, NULL, + true, false, EXEC_TIMEOUT) != 0) { + TALLOC_FREE(buff); + return -1; + } + slen = strlen(buff); + *out = buff; + } + break; + + case TMPL_TYPE_XLAT: + RDEBUG4("EXPAND TMPL XLAT"); + /* Error in expansion, this is distinct from zero length expansion */ + slen = radius_axlat(out, request, vpt->name, escape, escape_ctx); + if (slen < 0) { + rad_assert(!*out); + return slen; + } + rad_assert(*out); + slen = strlen(*out); + break; + + case TMPL_TYPE_XLAT_STRUCT: + RDEBUG4("EXPAND TMPL XLAT STRUCT"); + /* Error in expansion, this is distinct from zero length expansion */ + slen = radius_axlat_struct(out, request, vpt->tmpl_xlat, escape, escape_ctx); + if (slen < 0) { + rad_assert(!*out); + return slen; + } + slen = strlen(*out); + break; + + case TMPL_TYPE_ATTR: + { + int ret; + + RDEBUG4("EXPAND TMPL ATTR"); + ret = tmpl_find_vp(&vp, request, vpt); + if (ret < 0) return -2; + + switch (vpt->tmpl_da->type) { + case PW_TYPE_STRING: + *out = talloc_bstrndup(ctx, vp->vp_strvalue, vp->vp_length); + if (!*out) return -1; + slen = vp->vp_length; + break; + + case PW_TYPE_OCTETS: + *out = talloc_memdup(ctx, vp->vp_octets, vp->vp_length); + if (!*out) return -1; + slen = vp->vp_length; + break; + + default: + *out = vp_aprints_value(ctx, vp, '\0'); + if (!*out) return -1; + slen = talloc_array_length(*out) - 1; + break; + } + } + break; + + case TMPL_TYPE_DATA: + { + RDEBUG4("EXPAND TMPL DATA"); + + switch (vpt->tmpl_data_type) { + case PW_TYPE_STRING: + *out = talloc_bstrndup(ctx, vpt->tmpl_data_value.strvalue, vpt->tmpl_data_length); + if (!*out) return -1; + slen = vpt->tmpl_data_length; + break; + + case PW_TYPE_OCTETS: + *out = talloc_memdup(ctx, vpt->tmpl_data_value.octets, vpt->tmpl_data_length); + if (!*out) return -1; + slen = vpt->tmpl_data_length; + break; + + default: + *out = value_data_aprints(ctx, vpt->tmpl_data_type, NULL, &vpt->tmpl_data_value, vpt->tmpl_data_length, '\0'); + if (!*out) return -1; + slen = talloc_array_length(*out) - 1; + break; + } + } + break; + + /* + * We should never be expanding these. + */ + case TMPL_TYPE_UNKNOWN: + case TMPL_TYPE_NULL: + case TMPL_TYPE_LIST: + case TMPL_TYPE_REGEX: + case TMPL_TYPE_ATTR_UNDEFINED: + case TMPL_TYPE_REGEX_STRUCT: + rad_assert(0 == 1); + slen = -1; + break; + } + + if (slen < 0) return slen; + + /* + * If we're doing correct escapes, we may have to re-parse the string. + * If the string is from another expansion, it needs re-parsing. + * Or, if it's from a "string" attribute, it needs re-parsing. + * Integers, IP addresses, etc. don't need re-parsing. + */ + if (cf_new_escape && (vpt->type != TMPL_TYPE_ATTR)) { + value_data_t vd; + + PW_TYPE type = PW_TYPE_STRING; + + slen = value_data_from_str(ctx, &vd, &type, NULL, *out, slen, '"'); + talloc_free(*out); /* free the old value */ + *out = vd.ptr; + } + + if (vpt->type == TMPL_TYPE_XLAT_STRUCT) { + RDEBUG2("EXPAND %s", vpt->name); /* xlat_struct doesn't do this */ + RDEBUG2(" --> %s", *out); + } + + return slen; +} + +/** Print a #vp_tmpl_t to a string + * + * @param[out] out Where to write the presentation format #vp_tmpl_t string. + * @param[in] outlen Size of output buffer. + * @param[in] vpt to print + * @param[in] values Used for integer attributes only. #DICT_ATTR to use when mapping integer + * values to strings. + * @return the size of the string written to the output buffer. + */ +size_t tmpl_prints(char *out, size_t outlen, vp_tmpl_t const *vpt, DICT_ATTR const *values) +{ + size_t len; + char c; + char const *p; + char *q = out; + + if (!vpt) { + *out = '\0'; + return 0; + } + + VERIFY_TMPL(vpt); + + switch (vpt->type) { + default: + return 0; + + case TMPL_TYPE_REGEX: + case TMPL_TYPE_REGEX_STRUCT: + c = '/'; + break; + + case TMPL_TYPE_XLAT: + case TMPL_TYPE_XLAT_STRUCT: + c = '"'; + break; + case TMPL_TYPE_LITERAL: /* single-quoted or bare word */ + /* + * Hack + */ + for (p = vpt->name; *p != '\0'; p++) { + if (*p == ' ') break; + if (*p == '\'') break; + if (!dict_attr_allowed_chars[(int) *p]) break; + } + + if (!*p) { + strlcpy(out, vpt->name, outlen); + return strlen(out); + } + + c = vpt->quote; + break; + + case TMPL_TYPE_EXEC: + c = '`'; + break; + + case TMPL_TYPE_LIST: + out[0] = '&'; + if (vpt->tmpl_request == REQUEST_CURRENT) { + snprintf(out + 1, outlen - 1, "%s:", + fr_int2str(pair_lists, vpt->tmpl_list, "")); + } else { + snprintf(out + 1, outlen - 1, "%s.%s:", + fr_int2str(request_refs, vpt->tmpl_request, ""), + fr_int2str(pair_lists, vpt->tmpl_list, "")); + } + len = strlen(out); + goto attr_inst_tag; + + case TMPL_TYPE_ATTR: + out[0] = '&'; + if (vpt->tmpl_request == REQUEST_CURRENT) { + if (vpt->tmpl_list == PAIR_LIST_REQUEST) { + strlcpy(out + 1, vpt->tmpl_da->name, outlen - 1); + } else { + snprintf(out + 1, outlen - 1, "%s:%s", + fr_int2str(pair_lists, vpt->tmpl_list, ""), + vpt->tmpl_da->name); + } + + } else { + snprintf(out + 1, outlen - 1, "%s.%s:%s", + fr_int2str(request_refs, vpt->tmpl_request, ""), + fr_int2str(pair_lists, vpt->tmpl_list, ""), + vpt->tmpl_da->name); + } + + len = strlen(out); + + attr_inst_tag: + if ((vpt->tmpl_tag == TAG_ANY) && (vpt->tmpl_num == NUM_ANY)) return len; + + q = out + len; + outlen -= len; + + if (vpt->tmpl_tag != TAG_ANY) { + snprintf(q, outlen, ":%d", vpt->tmpl_tag); + len = strlen(q); + q += len; + outlen -= len; + } + + switch (vpt->tmpl_num) { + case NUM_ANY: + break; + + case NUM_ALL: + snprintf(q, outlen, "[*]"); + len = strlen(q); + q += len; + break; + + case NUM_COUNT: + snprintf(q, outlen, "[#]"); + len = strlen(q); + q += len; + break; + + case NUM_LAST: + snprintf(q, outlen, "[n]"); + len = strlen(q); + q += len; + break; + + default: + snprintf(q, outlen, "[%i]", vpt->tmpl_num); + len = strlen(q); + q += len; + break; + } + + return (q - out); + + case TMPL_TYPE_ATTR_UNDEFINED: + out[0] = '&'; + if (vpt->tmpl_request == REQUEST_CURRENT) { + if (vpt->tmpl_list == PAIR_LIST_REQUEST) { + strlcpy(out + 1, vpt->tmpl_unknown_name, outlen - 1); + } else { + snprintf(out + 1, outlen - 1, "%s:%s", + fr_int2str(pair_lists, vpt->tmpl_list, ""), + vpt->tmpl_unknown_name); + } + + } else { + snprintf(out + 1, outlen - 1, "%s.%s:%s", + fr_int2str(request_refs, vpt->tmpl_request, ""), + fr_int2str(pair_lists, vpt->tmpl_list, ""), + vpt->tmpl_unknown_name); + } + + len = strlen(out); + + if (vpt->tmpl_num == NUM_ANY) { + return len; + } + + q = out + len; + outlen -= len; + + if (vpt->tmpl_num != NUM_ANY) { + snprintf(q, outlen, "[%i]", vpt->tmpl_num); + len = strlen(q); + q += len; + } + + return (q - out); + + case TMPL_TYPE_DATA: + return value_data_prints(out, outlen, vpt->tmpl_data_type, values, &vpt->tmpl_data_value, + vpt->tmpl_data_length, vpt->quote); + } + + if (outlen <= 3) { + *out = '\0'; + return 0; + } + + *(q++) = c; + + /* + * Print it with appropriate escaping + */ + if (cf_new_escape && (c == '/')) { + len = fr_prints(q, outlen - 3, vpt->name, vpt->len, '\0'); + } else { + len = fr_prints(q, outlen - 3, vpt->name, vpt->len, c); + } + + q += len; + *(q++) = c; + *q = '\0'; + + return q - out; +} + +/** Initialise a #vp_cursor_t to the #VALUE_PAIR specified by a #vp_tmpl_t + * + * This makes iterating over the one or more #VALUE_PAIR specified by a #vp_tmpl_t + * significantly easier. + * + * @param err May be NULL if no error code is required. Will be set to: + * - 0 on success. + * - -1 if no matching #VALUE_PAIR could be found. + * - -2 if list could not be found (doesn't exist in current #REQUEST). + * - -3 if context could not be found (no parent #REQUEST available). + * @param cursor to store iterator state. + * @param request The current #REQUEST. + * @param vpt specifying the #VALUE_PAIR type/tag or list to iterate over. + * @return the first #VALUE_PAIR specified by the #vp_tmpl_t, or NULL if no matching + * #VALUE_PAIR found, and NULL on error. + * + * @see tmpl_cursor_next + */ +VALUE_PAIR *tmpl_cursor_init(int *err, vp_cursor_t *cursor, REQUEST *request, vp_tmpl_t const *vpt) +{ + VALUE_PAIR **vps, *vp = NULL; + int num; + + VERIFY_TMPL(vpt); + + rad_assert((vpt->type == TMPL_TYPE_ATTR) || (vpt->type == TMPL_TYPE_LIST)); + + if (err) *err = 0; + + if (radius_request(&request, vpt->tmpl_request) < 0) { + if (err) *err = -3; + return NULL; + } + vps = radius_list(request, vpt->tmpl_list); + if (!vps) { + if (err) *err = -2; + return NULL; + } + (void) fr_cursor_init(cursor, vps); + + switch (vpt->type) { + /* + * May not may not be found, but it *is* a known name. + */ + case TMPL_TYPE_ATTR: + switch (vpt->tmpl_num) { + case NUM_ANY: + vp = fr_cursor_next_by_da(cursor, vpt->tmpl_da, vpt->tmpl_tag); + if (!vp) { + if (err) *err = -1; + return NULL; + } + VERIFY_VP(vp); + return vp; + + /* + * Get the last instance of a VALUE_PAIR. + */ + case NUM_LAST: + { + VALUE_PAIR *last = NULL; + + while ((vp = fr_cursor_next_by_da(cursor, vpt->tmpl_da, vpt->tmpl_tag))) { + VERIFY_VP(vp); + last = vp; + } + VERIFY_VP(last); + if (!last) break; + return last; + } + + /* + * Callers expect NUM_COUNT to setup the cursor to point + * to the first attribute in the list we're meant to be + * counting. + * + * It does not produce a virtual attribute containing the + * total number of attributes. + */ + case NUM_COUNT: + return fr_cursor_next_by_da(cursor, vpt->tmpl_da, vpt->tmpl_tag); + + default: + num = vpt->tmpl_num; + while ((vp = fr_cursor_next_by_da(cursor, vpt->tmpl_da, vpt->tmpl_tag))) { + VERIFY_VP(vp); + if (num-- <= 0) return vp; + } + break; + } + + if (err) *err = -1; + return NULL; + + case TMPL_TYPE_LIST: + switch (vpt->tmpl_num) { + case NUM_COUNT: + case NUM_ANY: + case NUM_ALL: + vp = fr_cursor_init(cursor, vps); + if (!vp) { + if (err) *err = -1; + return NULL; + } + VERIFY_VP(vp); + return vp; + + /* + * Get the last instance of a VALUE_PAIR. + */ + case NUM_LAST: + { + VALUE_PAIR *last = NULL; + + for (vp = fr_cursor_init(cursor, vps); + vp; + vp = fr_cursor_next(cursor)) { + VERIFY_VP(vp); + last = vp; + } + if (!last) break; + VERIFY_VP(last); + return last; + } + + default: + num = vpt->tmpl_num; + for (vp = fr_cursor_init(cursor, vps); + vp; + vp = fr_cursor_next(cursor)) { + VERIFY_VP(vp); + if (num-- <= 0) return vp; + } + break; + } + + break; + + default: + rad_assert(0); + } + + return vp; +} + +/** Returns the next #VALUE_PAIR specified by vpt + * + * @param cursor initialised with #tmpl_cursor_init. + * @param vpt specifying the #VALUE_PAIR type/tag to iterate over. + * Must be one of the following types: + * - #TMPL_TYPE_LIST + * - #TMPL_TYPE_ATTR + * @return NULL if no more matching #VALUE_PAIR of the specified type/tag are found. + */ +VALUE_PAIR *tmpl_cursor_next(vp_cursor_t *cursor, vp_tmpl_t const *vpt) +{ + rad_assert((vpt->type == TMPL_TYPE_ATTR) || (vpt->type == TMPL_TYPE_LIST)); + + VERIFY_TMPL(vpt); + + switch (vpt->type) { + /* + * May not may not be found, but it *is* a known name. + */ + case TMPL_TYPE_ATTR: + switch (vpt->tmpl_num) { + default: + return NULL; + + case NUM_ALL: + case NUM_COUNT: /* This cursor is being used to count matching attrs */ + break; + } + return fr_cursor_next_by_da(cursor, vpt->tmpl_da, vpt->tmpl_tag); + + case TMPL_TYPE_LIST: + switch (vpt->tmpl_num) { + default: + return NULL; + + case NUM_ALL: + case NUM_COUNT: /* This cursor is being used to count matching attrs */ + break; + } + return fr_cursor_next(cursor); + + default: + rad_assert(0); + return NULL; /* Older versions of GCC flag the lack of return as an error */ + } +} + +/** Copy pairs matching a #vp_tmpl_t in the current #REQUEST + * + * @param ctx to allocate new #VALUE_PAIR in. + * @param out Where to write the copied #VALUE_PAIR (s). + * @param request The current #REQUEST. + * @param vpt specifying the #VALUE_PAIR type/tag or list to copy. + * Must be one of the following types: + * - #TMPL_TYPE_LIST + * - #TMPL_TYPE_ATTR + * @return + * - -1 if no matching #VALUE_PAIR could be found. + * - -2 if list could not be found (doesn't exist in current #REQUEST). + * - -3 if context could not be found (no parent #REQUEST available). + * - -4 on memory allocation error. + */ +int tmpl_copy_vps(TALLOC_CTX *ctx, VALUE_PAIR **out, REQUEST *request, vp_tmpl_t const *vpt) +{ + VALUE_PAIR *vp; + vp_cursor_t from, to; + + VERIFY_TMPL(vpt); + + int err; + + rad_assert((vpt->type == TMPL_TYPE_ATTR) || (vpt->type == TMPL_TYPE_LIST)); + + *out = NULL; + + fr_cursor_init(&to, out); + + for (vp = tmpl_cursor_init(&err, &from, request, vpt); + vp; + vp = tmpl_cursor_next(&from, vpt)) { + vp = fr_pair_copy(ctx, vp); + if (!vp) { + fr_pair_list_free(out); + return -4; + } + fr_cursor_insert(&to, vp); + } + + return err; +} + +/** Returns the first VP matching a #vp_tmpl_t + * + * @param out where to write the retrieved vp. + * @param request The current #REQUEST. + * @param vpt specifying the #VALUE_PAIR type/tag to find. + * Must be one of the following types: + * - #TMPL_TYPE_LIST + * - #TMPL_TYPE_ATTR + * @return + * - -1 if no matching #VALUE_PAIR could be found. + * - -2 if list could not be found (doesn't exist in current #REQUEST). + * - -3 if context could not be found (no parent #REQUEST available). + */ +int tmpl_find_vp(VALUE_PAIR **out, REQUEST *request, vp_tmpl_t const *vpt) +{ + vp_cursor_t cursor; + VALUE_PAIR *vp; + + VERIFY_TMPL(vpt); + + int err; + + vp = tmpl_cursor_init(&err, &cursor, request, vpt); + if (out) *out = vp; + + return err; +} +/* @} **/ + +#ifdef WITH_VERIFY_PTR +/** Used to check whether areas of a vp_tmpl_t are zeroed out + * + * @param ptr Offset to begin checking at. + * @param len How many bytes to check. + * @return pointer to the first non-zero byte, or NULL if all bytes were zero. + */ +static uint8_t const *not_zeroed(uint8_t const *ptr, size_t len) +{ + size_t i; + + for (i = 0; i < len; i++) { + if (ptr[i] != 0x00) return ptr + i; + } + + return NULL; +} +#define CHECK_ZEROED(_x) not_zeroed((uint8_t const *)&_x + sizeof(_x), sizeof(vpt->data) - sizeof(_x)) + +/** Verify fields of a vp_tmpl_t make sense + * + * @note If the #vp_tmpl_t is invalid, causes the server to exit. + * + * @param file obtained with __FILE__. + * @param line obtained with __LINE__. + * @param vpt to check. + */ +void tmpl_verify(char const *file, int line, vp_tmpl_t const *vpt) +{ + rad_assert(vpt); + + if (vpt->type == TMPL_TYPE_UNKNOWN) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: vp_tmpl_t type was " + "TMPL_TYPE_UNKNOWN (uninitialised)", file, line); + fr_assert(0); + fr_exit_now(1); + } + + if (vpt->type > TMPL_TYPE_NULL) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: vp_tmpl_t type was %i " + "(outside range of tmpl_names)", file, line, vpt->type); + fr_assert(0); + fr_exit_now(1); + } + + /* + * Do a memcmp of the bytes after where the space allocated for + * the union member should have ended and the end of the union. + * These should always be zero if the union has been initialised + * properly. + * + * If they're still all zero, do TMPL_TYPE specific checks. + */ + switch (vpt->type) { + case TMPL_TYPE_NULL: + if (not_zeroed((uint8_t const *)&vpt->data, sizeof(vpt->data))) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_NULL " + "has non-zero bytes in its data union", file, line); + fr_assert(0); + fr_exit_now(1); + } + break; + + case TMPL_TYPE_LITERAL: + if (not_zeroed((uint8_t const *)&vpt->data, sizeof(vpt->data))) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_LITERAL " + "has non-zero bytes in its data union", file, line); + fr_assert(0); + fr_exit_now(1); + } + break; + + case TMPL_TYPE_XLAT: + case TMPL_TYPE_XLAT_STRUCT: + break; + +/* @todo When regexes get converted to xlat the flags field of the regex union is used + case TMPL_TYPE_XLAT: + if (not_zeroed((uint8_t const *)&vpt->data, sizeof(vpt->data))) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_XLAT " + "has non-zero bytes in its data union", file, line); + fr_assert(0); + fr_exit_now(1); + } + break; + + case TMPL_TYPE_XLAT_STRUCT: + if (CHECK_ZEROED(vpt->data.xlat)) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_XLAT_STRUCT " + "has non-zero bytes after the data.xlat pointer in the union", file, line); + fr_assert(0); + fr_exit_now(1); + } + break; +*/ + + case TMPL_TYPE_EXEC: + if (not_zeroed((uint8_t const *)&vpt->data, sizeof(vpt->data))) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_EXEC " + "has non-zero bytes in its data union", file, line); + fr_assert(0); + fr_exit_now(1); + } + break; + + case TMPL_TYPE_ATTR_UNDEFINED: + rad_assert(vpt->tmpl_da == NULL); + break; + + case TMPL_TYPE_ATTR: + if (CHECK_ZEROED(vpt->data.attribute)) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_ATTR " + "has non-zero bytes after the data.attribute struct in the union", + file, line); + fr_assert(0); + fr_exit_now(1); + } + + if (vpt->tmpl_da->flags.is_unknown) { + if (vpt->tmpl_da != (DICT_ATTR const *)&vpt->data.attribute.unknown.da) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_ATTR " + "da is marked as unknown, but does not point to the template's " + "unknown da buffer", file, line); + fr_assert(0); + fr_exit_now(1); + } + + } else { + DICT_ATTR const *da; + + /* + * Attribute may be present with multiple names + */ + da = dict_attrbyname(vpt->tmpl_da->name); + if (!da) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_ATTR " + "attribute \"%s\" (%s) not found in global dictionary", + file, line, vpt->tmpl_da->name, + fr_int2str(dict_attr_types, vpt->tmpl_da->type, "<INVALID>")); + fr_assert(0); + fr_exit_now(1); + } + + if ((da->type == PW_TYPE_COMBO_IP_ADDR) && (da->type != vpt->tmpl_da->type)) { + da = dict_attrbytype(vpt->tmpl_da->attr, vpt->tmpl_da->vendor, vpt->tmpl_da->type); + if (!da) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_ATTR " + "attribute \"%s\" variant (%s) not found in global dictionary", + file, line, vpt->tmpl_da->name, + fr_int2str(dict_attr_types, vpt->tmpl_da->type, "<INVALID>")); + fr_assert(0); + fr_exit_now(1); + } + } + + if (da != vpt->tmpl_da) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_ATTR " + "dictionary pointer %p \"%s\" (%s) " + "and global dictionary pointer %p \"%s\" (%s) differ", + file, line, + vpt->tmpl_da, vpt->tmpl_da->name, + fr_int2str(dict_attr_types, vpt->tmpl_da->type, "<INVALID>"), + da, da->name, + fr_int2str(dict_attr_types, da->type, "<INVALID>")); + fr_assert(0); + fr_exit_now(1); + } + } + break; + + case TMPL_TYPE_LIST: + if (CHECK_ZEROED(vpt->data.attribute)) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_LIST" + "has non-zero bytes after the data.attribute struct in the union", file, line); + fr_assert(0); + fr_exit_now(1); + } + + if (vpt->tmpl_da != NULL) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_LIST da pointer was NULL", file, line); + fr_assert(0); + fr_exit_now(1); + } + break; + + case TMPL_TYPE_DATA: + if (CHECK_ZEROED(vpt->data.literal)) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_DATA " + "has non-zero bytes after the data.literal struct in the union", + file, line); + fr_assert(0); + fr_exit_now(1); + } + + if (vpt->tmpl_data_type == PW_TYPE_INVALID) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_DATA type was " + "PW_TYPE_INVALID (uninitialised)", file, line); + fr_assert(0); + fr_exit_now(1); + } + + if (vpt->tmpl_data_type >= PW_TYPE_MAX) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_DATA type was " + "%i (outside the range of PW_TYPEs)", file, line, vpt->tmpl_data_type); + fr_assert(0); + fr_exit_now(1); + } + /* + * Unlike VALUE_PAIRs we can't guarantee that VALUE_PAIR_TMPL buffers will + * be talloced. They may be allocated on the stack or in global variables. + */ + switch (vpt->tmpl_data_type) { + case PW_TYPE_STRING: + if (vpt->tmpl_data.vp_strvalue[vpt->tmpl_data_length] != '\0') { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_DATA char buffer not \\0 " + "terminated", file, line); + fr_assert(0); + fr_exit_now(1); + } + break; + + case PW_TYPE_TLV: + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_DATA is of type TLV", + file, line); + fr_assert(0); + fr_exit_now(1); + + case PW_TYPE_OCTETS: + break; + + default: + if (vpt->tmpl_data_length == 0) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_DATA data pointer not NULL " + "but len field is zero", file, line); + fr_assert(0); + fr_exit_now(1); + } + } + + break; + + case TMPL_TYPE_REGEX: + /* + * iflag field is used for non compiled regexes too. + */ + if (CHECK_ZEROED(vpt->data.preg)) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_REGEX " + "has non-zero bytes after the data.preg struct in the union", file, line); + fr_assert(0); + fr_exit_now(1); + } + + if (vpt->tmpl_preg != NULL) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_REGEX " + "preg field was not nULL", file, line); + fr_assert(0); + fr_exit_now(1); + } + + if ((vpt->tmpl_iflag != true) && (vpt->tmpl_iflag != false)) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_REGEX " + "iflag field was neither true or false", file, line); + fr_assert(0); + fr_exit_now(1); + } + + if ((vpt->tmpl_mflag != true) && (vpt->tmpl_mflag != false)) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_REGEX " + "mflag field was neither true or false", file, line); + fr_assert(0); + fr_exit_now(1); + } + + break; + + case TMPL_TYPE_REGEX_STRUCT: + if (CHECK_ZEROED(vpt->data.preg)) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_REGEX_STRUCT " + "has non-zero bytes after the data.preg struct in the union", file, line); + fr_assert(0); + fr_exit_now(1); + } + + if (vpt->tmpl_preg == NULL) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_REGEX_STRUCT " + "comp field was NULL", file, line); + fr_assert(0); + fr_exit_now(1); + } + + if ((vpt->tmpl_iflag != true) && (vpt->tmpl_iflag != false)) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_REGEX_STRUCT " + "iflag field was neither true or false", file, line); + fr_assert(0); + fr_exit_now(1); + } + + if ((vpt->tmpl_mflag != true) && (vpt->tmpl_mflag != false)) { + FR_FAULT_LOG("CONSISTENCY CHECK FAILED %s[%u]: TMPL_TYPE_REGEX " + "mflag field was neither true or false", file, line); + fr_assert(0); + fr_exit_now(1); + } + break; + + case TMPL_TYPE_UNKNOWN: + rad_assert(0); + } +} +#endif diff --git a/src/main/unittest.c b/src/main/unittest.c new file mode 100644 index 0000000..72fdadc --- /dev/null +++ b/src/main/unittest.c @@ -0,0 +1,982 @@ +/* + * unittest.c Unit test wrapper for the RADIUS daemon. + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2000-2013 The FreeRADIUS server project + * Copyright 2013 Alan DeKok <aland@ox.org> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/modules.h> +#include <freeradius-devel/state.h> +#include <freeradius-devel/rad_assert.h> +#include <freeradius-devel/event.h> + +#ifdef HAVE_GETOPT_H +# include <getopt.h> +#endif + +#include <ctype.h> + +/* + * Global variables. + */ +char const *radacct_dir = NULL; +char const *radlog_dir = NULL; +bool log_stripped_names = false; + +static bool memory_report = false; +static bool filedone = false; + +char const *radiusd_version = "FreeRADIUS Version " RADIUSD_VERSION_STRING +#ifdef RADIUSD_VERSION_COMMIT +" (git #" STRINGIFY(RADIUSD_VERSION_COMMIT) ")" +#endif +", for host " HOSTINFO +#ifndef ENABLE_REPRODUCIBLE_BUILDS +", built on " __DATE__ " at " __TIME__ +#endif +; + +fr_event_list_t *el = NULL; + +/* + * Static functions. + */ +static void usage(int); + +void listen_free(UNUSED rad_listen_t **head) +{ + /* do nothing */ +} + +void request_inject(UNUSED REQUEST *request) +{ + /* do nothing */ +} + +#ifdef WITH_RADIUSV11 +int fr_radiusv11_client_init(UNUSED fr_tls_server_conf_t *tls); + +int fr_radiusv11_client_init(UNUSED fr_tls_server_conf_t *tls) +{ + return 0; +} +#endif + +static rad_listen_t *listen_alloc(void *ctx) +{ + rad_listen_t *this; + + this = talloc_zero(ctx, rad_listen_t); + if (!this) return NULL; + + this->type = RAD_LISTEN_AUTH; + this->recv = NULL; + this->send = NULL; + this->print = NULL; + this->encode = NULL; + this->decode = NULL; + + /* + * We probably don't care about this. We can always add + * fields later. + */ + this->data = talloc_zero(this, listen_socket_t); + if (!this->data) { + talloc_free(this); + return NULL; + } + + return this; +} + +static RADCLIENT *client_alloc(void *ctx) +{ + RADCLIENT *client; + + client = talloc_zero(ctx, RADCLIENT); + if (!client) return NULL; + + return client; +} + +static REQUEST *request_setup(FILE *fp) +{ + VALUE_PAIR *vp; + REQUEST *request; + vp_cursor_t cursor; + struct timeval now; + + /* + * Create and initialize the new request. + */ + request = request_alloc(NULL); + gettimeofday(&now, NULL); + request->timestamp = now.tv_sec; + + request->packet = rad_alloc(request, false); + if (!request->packet) { + ERROR("No memory"); + talloc_free(request); + return NULL; + } + request->packet->timestamp = now; + + request->reply = rad_alloc(request, false); + if (!request->reply) { + ERROR("No memory"); + talloc_free(request); + return NULL; + } + + request->listener = listen_alloc(request); + request->client = client_alloc(request); + + request->number = 0; + + request->master_state = REQUEST_ACTIVE; + request->child_state = REQUEST_RUNNING; + request->handle = NULL; + request->server = talloc_typed_strdup(request, "default"); + + request->root = &main_config; + + /* + * Read packet from fp + */ + if (fr_pair_list_afrom_file(request->packet, &request->packet->vps, fp, &filedone) < 0) { + fr_perror("unittest"); + talloc_free(request); + return NULL; + } + + /* + * Set the defaults for IPs, etc. + */ + request->packet->code = PW_CODE_ACCESS_REQUEST; + + request->packet->src_ipaddr.af = AF_INET; + request->packet->src_ipaddr.ipaddr.ip4addr.s_addr = htonl(INADDR_LOOPBACK); + request->packet->src_port = 18120; + + request->packet->dst_ipaddr.af = AF_INET; + request->packet->dst_ipaddr.ipaddr.ip4addr.s_addr = htonl(INADDR_LOOPBACK); + request->packet->dst_port = 1812; + + /* + * Copied from radclient + * + * Fix up Digest-Attributes issues + */ + for (vp = fr_cursor_init(&cursor, &request->packet->vps); + vp; + vp = fr_cursor_next(&cursor)) { + /* + * Double quoted strings get marked up as xlat expansions, + * but we don't support that here. + */ + if (vp->type == VT_XLAT) { + vp->vp_strvalue = vp->value.xlat; + vp->value.xlat = NULL; + vp->type = VT_DATA; + } + + if (!vp->da->vendor) switch (vp->da->attr) { + default: + break; + + /* + * Allow it to set the packet type in + * the attributes read from the file. + */ + case PW_PACKET_TYPE: + request->packet->code = vp->vp_integer; + break; + + case PW_PACKET_DST_PORT: + request->packet->dst_port = (vp->vp_integer & 0xffff); + break; + + case PW_PACKET_DST_IP_ADDRESS: + request->packet->dst_ipaddr.af = AF_INET; + request->packet->dst_ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr; + request->packet->dst_ipaddr.prefix = 32; + break; + + case PW_PACKET_DST_IPV6_ADDRESS: + request->packet->dst_ipaddr.af = AF_INET6; + request->packet->dst_ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr; + request->packet->dst_ipaddr.prefix = 128; + break; + + case PW_PACKET_SRC_PORT: + request->packet->src_port = (vp->vp_integer & 0xffff); + break; + + case PW_PACKET_SRC_IP_ADDRESS: + request->packet->src_ipaddr.af = AF_INET; + request->packet->src_ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr; + request->packet->src_ipaddr.prefix = 32; + break; + + case PW_PACKET_SRC_IPV6_ADDRESS: + request->packet->src_ipaddr.af = AF_INET6; + request->packet->src_ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr; + request->packet->src_ipaddr.prefix = 128; + break; + + case PW_CHAP_PASSWORD: { + int i, already_hex = 0; + + /* + * If it's 17 octets, it *might* be already encoded. + * Or, it might just be a 17-character password (maybe UTF-8) + * Check it for non-printable characters. The odds of ALL + * of the characters being 32..255 is (1-7/8)^17, or (1/8)^17, + * or 1/(2^51), which is pretty much zero. + */ + if (vp->vp_length == 17) { + for (i = 0; i < 17; i++) { + if (vp->vp_octets[i] < 32) { + already_hex = 1; + break; + } + } + } + + /* + * Allow the user to specify ASCII or hex CHAP-Password + */ + if (!already_hex) { + uint8_t *p; + size_t len, len2; + + len = len2 = vp->vp_length; + if (len2 < 17) len2 = 17; + + p = talloc_zero_array(vp, uint8_t, len2); + + memcpy(p, vp->vp_strvalue, len); + + rad_chap_encode(request->packet, + p, + fr_rand() & 0xff, vp); + vp->vp_octets = p; + vp->vp_length = 17; + } + } + break; + + case PW_DIGEST_REALM: + case PW_DIGEST_NONCE: + case PW_DIGEST_METHOD: + case PW_DIGEST_URI: + case PW_DIGEST_QOP: + case PW_DIGEST_ALGORITHM: + case PW_DIGEST_BODY_DIGEST: + case PW_DIGEST_CNONCE: + case PW_DIGEST_NONCE_COUNT: + case PW_DIGEST_USER_NAME: + /* overlapping! */ + { + DICT_ATTR const *da; + uint8_t *p, *q; + + p = talloc_array(vp, uint8_t, vp->vp_length + 2); + + memcpy(p + 2, vp->vp_octets, vp->vp_length); + p[0] = vp->da->attr - PW_DIGEST_REALM + 1; + vp->vp_length += 2; + p[1] = vp->vp_length; + + da = dict_attrbyvalue(PW_DIGEST_ATTRIBUTES, 0); + rad_assert(da != NULL); + vp->da = da; + + /* + * Re-do fr_pair_value_memsteal ourselves, + * because we play games with + * vp->da, and fr_pair_value_memsteal goes + * to GREAT lengths to sanitize + * and fix and change and + * double-check the various + * fields. + */ + memcpy(&q, &vp->vp_octets, sizeof(q)); + talloc_free(q); + + vp->vp_octets = talloc_steal(vp, p); + vp->type = VT_DATA; + + VERIFY_VP(vp); + } + + break; + } + } /* loop over the VP's we read in */ + + if (rad_debug_lvl) { + for (vp = fr_cursor_init(&cursor, &request->packet->vps); + vp; + vp = fr_cursor_next(&cursor)) { + /* + * Take this opportunity to verify all the VALUE_PAIRs are still valid. + */ + if (!talloc_get_type(vp, VALUE_PAIR)) { + ERROR("Expected VALUE_PAIR pointer got \"%s\"", talloc_get_name(vp)); + + fr_log_talloc_report(vp); + rad_assert(0); + } + + vp_print(fr_log_fp, vp); + } + fflush(fr_log_fp); + } + + /* + * Build the reply template from the request. + */ + request->reply->sockfd = request->packet->sockfd; + request->reply->dst_ipaddr = request->packet->src_ipaddr; + request->reply->src_ipaddr = request->packet->dst_ipaddr; + request->reply->dst_port = request->packet->src_port; + request->reply->src_port = request->packet->dst_port; + request->reply->id = request->packet->id; + request->reply->code = 0; /* UNKNOWN code */ + memcpy(request->reply->vector, request->packet->vector, + sizeof(request->reply->vector)); + request->reply->vps = NULL; + request->reply->data = NULL; + request->reply->data_len = 0; + + /* + * Debugging + */ + request->log.lvl = rad_debug_lvl; + request->log.func = vradlog_request; + + request->username = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY); + request->password = fr_pair_find_by_num(request->packet->vps, PW_USER_PASSWORD, 0, TAG_ANY); + + return request; +} + + +static void print_packet(FILE *fp, RADIUS_PACKET *packet) +{ + VALUE_PAIR *vp; + vp_cursor_t cursor; + + if (!packet) { + fprintf(fp, "\n"); + return; + } + + fprintf(fp, "%s\n", fr_packet_codes[packet->code]); + + for (vp = fr_cursor_init(&cursor, &packet->vps); + vp; + vp = fr_cursor_next(&cursor)) { + /* + * Take this opportunity to verify all the VALUE_PAIRs are still valid. + */ + if (!talloc_get_type(vp, VALUE_PAIR)) { + ERROR("Expected VALUE_PAIR pointer got \"%s\"", talloc_get_name(vp)); + + fr_log_talloc_report(vp); + rad_assert(0); + } + + vp_print(fp, vp); + } + fflush(fp); +} + + +#include <freeradius-devel/modpriv.h> + +/* + * %{poke:sql.foo=bar} + */ +static ssize_t xlat_poke(UNUSED void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + int i; + void *data, *base; + char *p, *q; + module_instance_t *mi; + char *buffer; + CONF_SECTION *modules; + CONF_PAIR *cp; + CONF_PARSER const *variables; + size_t len; + + rad_assert(outlen > 1); + rad_assert(request != NULL); + rad_assert(fmt != NULL); + rad_assert(out != NULL); + + *out = '\0'; + + modules = cf_section_sub_find(request->root->config, "modules"); + if (!modules) return 0; + + buffer = talloc_strdup(request, fmt); + if (!buffer) return 0; + + p = strchr(buffer, '.'); + if (!p) return 0; + + *(p++) = '\0'; + + mi = module_find(modules, buffer); + if (!mi) { + RDEBUG("Failed finding module '%s'", buffer); + fail: + talloc_free(buffer); + return 0; + } + + q = strchr(p, '='); + if (!q) { + RDEBUG("Failed finding '=' in string '%s'", fmt); + goto fail; + } + + *(q++) = '\0'; + + if (strchr(p, '.') != NULL) { + RDEBUG("Can't do sub-sections right now"); + goto fail; + } + + cp = cf_pair_find(mi->cs, p); + if (!cp) { + RDEBUG("No such item '%s'", p); + goto fail; + } + + /* + * Copy the old value to the output buffer, that way + * tests can restore it later, if they need to. + */ + len = strlcpy(out, cf_pair_value(cp), outlen); + + if (cf_pair_replace(mi->cs, cp, q) < 0) { + RDEBUG("Failed replacing pair"); + goto fail; + } + + base = mi->insthandle; + variables = mi->entry->module->config; + + /* + * Handle the known configuration parameters. + */ + for (i = 0; variables[i].name != NULL; i++) { + int ret; + + if (variables[i].type == PW_TYPE_SUBSECTION) continue; + /* else it's a CONF_PAIR */ + + /* + * Not the pair we want. Skip it. + */ + if (strcmp(variables[i].name, p) != 0) continue; + + if (variables[i].data) { + data = variables[i].data; /* prefer this. */ + } else if (base) { + data = ((char *)base) + variables[i].offset; + } else { + DEBUG2("Internal sanity check 2 failed in cf_section_parse"); + goto fail; + } + + /* + * Parse the pair we found, or a default value. + */ + ret = cf_item_parse(mi->cs, variables[i].name, variables[i].type, data, variables[i].dflt); + if (ret < 0) { + DEBUG2("Failed inserting new value into module instance data"); + goto fail; + } + break; /* we found it, don't do any more */ + } + + talloc_free(buffer); + + return len; +} + + +/* + * Read a file compose of xlat's and expected results + */ +static bool do_xlats(char const *filename, FILE *fp) +{ + int lineno = 0; + ssize_t len; + char *p; + char input[8192]; + char output[8192]; + REQUEST *request; + struct timeval now; + + /* + * Create and initialize the new request. + */ + request = request_alloc(NULL); + gettimeofday(&now, NULL); + request->timestamp = now.tv_sec; + + request->log.lvl = rad_debug_lvl; + request->log.func = vradlog_request; + + output[0] = '\0'; + + while (fgets(input, sizeof(input), fp) != NULL) { + lineno++; + + /* + * Ignore blank lines and comments + */ + p = input; + while (isspace((uint8_t) *p)) p++; + + if (*p < ' ') continue; + if (*p == '#') continue; + + p = strchr(p, '\n'); + if (!p) { + if (!feof(fp)) { + fprintf(stderr, "Line %d too long in %s\n", + lineno, filename); + TALLOC_FREE(request); + return false; + } + } else { + *p = '\0'; + } + + /* + * Look for "xlat" + */ + if (strncmp(input, "xlat ", 5) == 0) { + ssize_t slen; + char const *error = NULL; + char *fmt = talloc_typed_strdup(NULL, input + 5); + xlat_exp_t *head; + + slen = xlat_tokenize(fmt, fmt, &head, &error); + if (slen <= 0) { + talloc_free(fmt); + snprintf(output, sizeof(output), "ERROR offset %d '%s'", (int) -slen, error); + continue; + } + + if (input[slen + 5] != '\0') { + talloc_free(fmt); + snprintf(output, sizeof(output), "ERROR offset %d 'Too much text' ::%s::", (int) slen, input + slen + 5); + continue; + } + + len = radius_xlat_struct(output, sizeof(output), request, head, NULL, NULL); + if (len < 0) { + snprintf(output, sizeof(output), "ERROR expanding xlat: %s", fr_strerror()); + continue; + } + + TALLOC_FREE(fmt); /* also frees 'head' */ + continue; + } + + /* + * Look for "data". + */ + if (strncmp(input, "data ", 5) == 0) { + if (strcmp(input + 5, output) != 0) { + fprintf(stderr, "Mismatch at line %d of %s\n\tgot : %s\n\texpected : %s\n", + lineno, filename, output, input + 5); + TALLOC_FREE(request); + return false; + } + continue; + } + + fprintf(stderr, "Unknown keyword in %s[%d]\n", filename, lineno); + TALLOC_FREE(request); + return false; + } + + TALLOC_FREE(request); + return true; +} + +/* + * Dummy event_list_corral + */ +fr_event_list_t *radius_event_list_corral(UNUSED event_corral_t hint) { + if (!el) { + el = fr_event_list_create(NULL, NULL); + } + + return el; +} + +/* + * The main guy. + */ +int main(int argc, char *argv[]) +{ + int rcode = EXIT_SUCCESS; + int argval; + const char *input_file = NULL; + const char *output_file = NULL; + const char *filter_file = NULL; + FILE *fp; + REQUEST *request = NULL; + VALUE_PAIR *vp; + VALUE_PAIR *filter_vps = NULL; + bool xlat_only = false; + fr_state_t *state = NULL; + + fr_talloc_fault_setup(); + + /* + * If the server was built with debugging enabled always install + * the basic fatal signal handlers. + */ +#ifndef NDEBUG + if (fr_fault_setup(getenv("PANIC_ACTION"), argv[0]) < 0) { + fr_perror("unittest"); + exit(EXIT_FAILURE); + } +#endif + + rad_debug_lvl = 0; + set_radius_dir(NULL, RADIUS_DIR); + + /* + * Ensure that the configuration is initialized. + */ + memset(&main_config, 0, sizeof(main_config)); + main_config.myip.af = AF_UNSPEC; + main_config.port = 0; + main_config.name = "radiusd"; + + /* + * The tests should have only IPs, not host names. + */ + fr_hostname_lookups = false; + + /* + * We always log to stdout. + */ + fr_log_fp = stdout; + default_log.dst = L_DST_STDOUT; + default_log.fd = STDOUT_FILENO; + + /* Process the options. */ + while ((argval = getopt(argc, argv, "d:D:f:hi:mMn:o:O:xX")) != EOF) { + + switch (argval) { + case 'd': + set_radius_dir(NULL, optarg); + break; + + case 'D': + main_config.dictionary_dir = talloc_typed_strdup(NULL, optarg); + break; + + case 'f': + filter_file = optarg; + break; + + case 'h': + usage(0); + break; + + case 'i': + input_file = optarg; + break; + + case 'm': + main_config.debug_memory = true; + break; + + case 'M': + memory_report = true; + main_config.debug_memory = true; + break; + + case 'n': + main_config.name = optarg; + break; + + case 'o': + output_file = optarg; + break; + + case 'O': + if (strcmp(optarg, "xlat_only") == 0) { + xlat_only = true; + break; + } + + fprintf(stderr, "Unknown option '%s'\n", optarg); + exit(EXIT_FAILURE); + + case 'X': + rad_debug_lvl += 2; + main_config.log_auth = true; + main_config.log_auth_badpass = true; + main_config.log_auth_goodpass = true; + break; + + case 'x': + rad_debug_lvl++; + break; + + default: + usage(1); + break; + } + } + + if (rad_debug_lvl) version_print(); + fr_debug_lvl = rad_debug_lvl; + + /* + * Mismatch between the binary and the libraries it depends on + */ + if (fr_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) { + fr_perror("radiusd"); + exit(EXIT_FAILURE); + } + + /* + * Initialising OpenSSL once, here, is safer than having individual modules do it. + */ +#ifdef HAVE_OPENSSL_CRYPTO_H + tls_global_init(false, false); +#endif + + if (xlat_register("poke", xlat_poke, NULL, NULL) < 0) { + rcode = EXIT_FAILURE; + goto finish; + } + + /* Read the configuration files, BEFORE doing anything else. */ + if (main_config_init() < 0) { + rcode = EXIT_FAILURE; + goto finish; + } + + /* + * Load the modules + */ + if (modules_init(main_config.config) < 0) { + rcode = EXIT_FAILURE; + goto finish; + } + + state =fr_state_init(NULL); + + /* + * Set the panic action (if required) + */ + { + char const *panic_action = NULL; + + panic_action = getenv("PANIC_ACTION"); + if (!panic_action) panic_action = main_config.panic_action; + + if (panic_action && (fr_fault_setup(panic_action, argv[0]) < 0)) { + fr_perror("radiusd"); + exit(EXIT_FAILURE); + } + } + + setlinebuf(stdout); /* unbuffered output */ + + if (!input_file || (strcmp(input_file, "-") == 0)) { + fp = stdin; + } else { + fp = fopen(input_file, "r"); + if (!fp) { + fprintf(stderr, "Failed reading %s: %s\n", + input_file, fr_syserror(errno)); + goto finish; + } + } + + /* + * For simplicity, read xlat's. + */ + if (xlat_only) { + if (!do_xlats(input_file, fp)) rcode = EXIT_FAILURE; + if (input_file) fclose(fp); + goto finish; + } + + /* + * Grab the VPs from stdin, or from the file. + */ + request = request_setup(fp); + if (!request) { + fprintf(stderr, "Failed reading input: %s\n", fr_strerror()); + rcode = EXIT_FAILURE; + goto finish; + } + + /* + * No filter file, OR there's no more input, OR we're + * reading from a file, and it's different from the + * filter file. + */ + if (!filter_file || filedone || + ((input_file != NULL) && (strcmp(filter_file, input_file) != 0))) { + if (output_file) { + fclose(fp); + fp = NULL; + } + filedone = false; + } + + /* + * There is a filter file. If necessary, open it. If we + * already are reading it via "input_file", then we don't + * need to re-open it. + */ + if (filter_file) { + if (!fp) { + fp = fopen(filter_file, "r"); + if (!fp) { + fprintf(stderr, "Failed reading %s: %s\n", filter_file, strerror(errno)); + rcode = EXIT_FAILURE; + goto finish; + } + } + + + if (fr_pair_list_afrom_file(request, &filter_vps, fp, &filedone) < 0) { + fprintf(stderr, "Failed reading attributes from %s: %s\n", + filter_file, fr_strerror()); + rcode = EXIT_FAILURE; + goto finish; + } + + /* + * FIXME: loop over input packets. + */ + fclose(fp); + } + + rad_virtual_server(request); + + if (!output_file || (strcmp(output_file, "-") == 0)) { + fp = stdout; + } else { + fp = fopen(output_file, "w"); + if (!fp) { + fprintf(stderr, "Failed writing %s: %s\n", + output_file, fr_syserror(errno)); + exit(EXIT_FAILURE); + } + } + + print_packet(fp, request->reply); + + if (output_file) fclose(fp); + + /* + * Update the list with the response type. + */ + vp = radius_pair_create(request->reply, &request->reply->vps, + PW_RESPONSE_PACKET_TYPE, 0); + vp->vp_integer = request->reply->code; + + { + VALUE_PAIR const *failed[2]; + + if (filter_vps && !fr_pair_validate(failed, filter_vps, request->reply->vps)) { + fr_pair_validate_debug(request, failed); + fr_perror("Output file %s does not match attributes in filter %s (%s)", + output_file ? output_file : input_file, filter_file, fr_strerror()); + rcode = EXIT_FAILURE; + goto finish; + } + } + + INFO("Exiting normally"); + +finish: + talloc_free(request); + + /* + * Detach any modules. + */ + modules_free(); + + xlat_unregister("poke", xlat_poke, NULL); + + xlat_free(); /* modules may have xlat's */ + + fr_state_delete(state); + + /* + * Free the configuration items. + */ + main_config_free(); + + if (el) talloc_free(el); + + if (memory_report) { + INFO("Allocated memory at time of report:"); + fr_log_talloc_report(NULL); + } + + return rcode; +} + + +/* + * Display the syntax for starting this program. + */ +static void NEVER_RETURNS usage(int status) +{ + FILE *output = status?stderr:stdout; + + fprintf(output, "Usage: %s [options]\n", main_config.name); + fprintf(output, "Options:\n"); + fprintf(output, " -d raddb_dir Configuration files are in \"raddb_dir/*\".\n"); + fprintf(output, " -D dict_dir Dictionary files are in \"dict_dir/*\".\n"); + fprintf(output, " -f file Filter reply against attributes in 'file'.\n"); + fprintf(output, " -h Print this help message.\n"); + fprintf(output, " -i file File containing request attributes.\n"); + fprintf(output, " -m On SIGINT or SIGQUIT exit cleanly instead of immediately.\n"); + fprintf(output, " -n name Read raddb/name.conf instead of raddb/radiusd.conf.\n"); + fprintf(output, " -X Turn on full debugging.\n"); + fprintf(output, " -x Turn on additional debugging. (-xx gives more debugging).\n"); + exit(status); +} diff --git a/src/main/unittest.mk b/src/main/unittest.mk new file mode 100644 index 0000000..edd4f13 --- /dev/null +++ b/src/main/unittest.mk @@ -0,0 +1,25 @@ +TARGET := unittest +SOURCES := acct.c auth.c client.c crypt.c files.c \ + mainconfig.c modules.c modcall.c \ + unittest.c soh.c state.c connection.c \ + session.c threads.c version.c \ + realms.c + +ifneq ($(OPENSSL_LIBS),) +SOURCES += cb.c tls.c +endif + +SRC_CFLAGS := -DHOSTINFO=\"${HOSTINFO}\" +TGT_INSTALLDIR := +TGT_LDLIBS := $(LIBS) $(OPENSSL_LIBS) $(SYSTEMD_LIBS) $(LCRYPT) +TGT_PREREQS := libfreeradius-server.a libfreeradius-radius.a + +# Libraries can't depend on libraries (oops), so make the binary +# depend on the EAP code... +ifneq "$(filter rlm_eap_%,${ALL_TGTS})" "" +TGT_PREREQS += libfreeradius-eap.a +endif + +ifneq ($(MAKECMDGOALS),scan) +SRC_CFLAGS += -DBUILT_WITH_CPPFLAGS=\"$(CPPFLAGS)\" -DBUILT_WITH_CFLAGS=\"$(CFLAGS)\" -DBUILT_WITH_LDFLAGS=\"$(LDFLAGS)\" -DBUILT_WITH_LIBS=\"$(LIBS)\" +endif diff --git a/src/main/util.c b/src/main/util.c new file mode 100644 index 0000000..b216cc9 --- /dev/null +++ b/src/main/util.c @@ -0,0 +1,1732 @@ +/* + * util.c Various utility functions. + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 2000,2006 The FreeRADIUS server project + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/rad_assert.h> + +#include <ctype.h> + +#include <sys/stat.h> +#include <fcntl.h> + +/* + * The signal() function in Solaris 2.5.1 sets SA_NODEFER in + * sa_flags, which causes grief if signal() is called in the + * handler before the cause of the signal has been cleared. + * (Infinite recursion). + * + * The same problem appears on HPUX, so we avoid it, if we can. + * + * Using sigaction() to reset the signal handler fixes the problem, + * so where available, we prefer that solution. + */ + +void (*reset_signal(int signo, void (*func)(int)))(int) +{ +#ifdef HAVE_SIGACTION + struct sigaction act, oact; + + memset(&act, 0, sizeof(act)); + act.sa_handler = func; + sigemptyset(&act.sa_mask); + act.sa_flags = 0; +#ifdef SA_INTERRUPT /* SunOS */ + act.sa_flags |= SA_INTERRUPT; +#endif + if (sigaction(signo, &act, &oact) < 0) + return SIG_ERR; + return oact.sa_handler; +#else + + /* + * re-set by calling the 'signal' function, which + * may cause infinite recursion and core dumps due to + * stack growth. + * + * However, the system is too dumb to implement sigaction(), + * so we don't have a choice. + */ + signal(signo, func); + + return NULL; +#endif +} + +/* + * Per-request data, added by modules... + */ +struct request_data_t { + request_data_t *next; + + void *unique_ptr; + int unique_int; + void *opaque; + bool free_opaque; +}; + +/* + * Add opaque data (with a "free" function) to a REQUEST. + * + * The unique ptr is meant to be a module configuration, + * and the unique integer allows the caller to have multiple + * opaque data associated with a REQUEST. + */ +int request_data_add(REQUEST *request, void *unique_ptr, int unique_int, void *opaque, bool free_opaque) +{ + request_data_t *this, **last, *next; + + /* + * Some simple sanity checks. + */ + if (!request || !opaque) return -1; + + this = next = NULL; + for (last = &(request->data); + *last != NULL; + last = &((*last)->next)) { + if (((*last)->unique_ptr == unique_ptr) && + ((*last)->unique_int == unique_int)) { + this = *last; + next = this->next; + + /* + * If caller requires custom behaviour on free + * they must set a destructor. + */ + if (this->opaque && this->free_opaque) talloc_free(this->opaque); + + break; /* replace the existing entry */ + } + } + + /* + * Only alloc new memory if we're not replacing + * an existing entry. + */ + if (!this) this = talloc_zero(request, request_data_t); + if (!this) return -1; + + this->next = next; + this->unique_ptr = unique_ptr; + this->unique_int = unique_int; + this->opaque = opaque; + this->free_opaque = free_opaque; + + *last = this; + + return 0; +} + +/* + * Get opaque data from a request. + */ +void *request_data_get(REQUEST *request, void *unique_ptr, int unique_int) +{ + request_data_t **last; + + if (!request) return NULL; + + for (last = &(request->data); + *last != NULL; + last = &((*last)->next)) { + if (((*last)->unique_ptr == unique_ptr) && + ((*last)->unique_int == unique_int)) { + request_data_t *this; + void *ptr; + + this = *last; + ptr = this->opaque; + + /* + * Remove the entry from the list, and free it. + */ + *last = this->next; + talloc_free(this); + + return ptr; /* don't free it, the caller does that */ + } + } + + return NULL; /* wasn't found, too bad... */ +} + +/* + * Get opaque data from a request without removing it. + */ +void *request_data_reference(REQUEST *request, void *unique_ptr, int unique_int) +{ + request_data_t **last; + + for (last = &(request->data); + *last != NULL; + last = &((*last)->next)) { + if (((*last)->unique_ptr == unique_ptr) && + ((*last)->unique_int == unique_int)) { + return (*last)->opaque; + } + } + + return NULL; /* wasn't found, too bad... */ +} + +/** Create possibly many directories. + * + * @note that the input directory name is NOT treated as a constant. This is so that + * if an error is returned, the 'directory' ptr points to the name of the file + * which caused the error. + * + * @param dir path to directory to create. + * @param mode for new directories. + * @param uid to set on new directories, may be -1 to use effective uid. + * @param gid to set on new directories, may be -1 to use effective gid. + * @return 0 on success, -1 on error. Error available as errno. + */ +int rad_mkdir(char *dir, mode_t mode, uid_t uid, gid_t gid) +{ + int rcode, fd; + char *p; + + /* + * Try to make the dir. If it exists, chmod it. + * If a path doesn't exist, that's OK. Otherwise + * return with an error. + * + * Directories permissions are initially set so + * that only we should have access. This prevents + * an attacker removing them and swapping them + * out for a link to somewhere else. + * We change them to the correct permissions later. + */ + rcode = mkdir(dir, 0700); + if (rcode < 0) { + switch (errno) { + case EEXIST: + return 0; /* don't change permissions */ + + case ENOENT: + break; + + default: + return rcode; + } + + /* + * A component in the dir path doesn't + * exist. Look for the LAST dir name. Try + * to create that. If there's an error, we leave + * the dir path as the one at which the + * error occured. + */ + p = strrchr(dir, FR_DIR_SEP); + if (!p || (p == dir)) return -1; + + *p = '\0'; + rcode = rad_mkdir(dir, mode, uid, gid); + if (rcode < 0) return rcode; + + /* + * Reset the dir path, and try again to + * make the dir. + */ + *p = FR_DIR_SEP; + rcode = mkdir(dir, 0700); + if (rcode < 0) return rcode; + } /* else we successfully created the dir */ + + /* + * Set the permissions on the directory we created + * this should never fail unless there's a race. + */ + fd = open(dir, O_DIRECTORY); + if (fd < 0) return -1; + + rcode = fchmod(fd, mode); + if (rcode < 0) { + close(fd); + return rcode; + } + + if ((uid != (uid_t)-1) || (gid != (gid_t)-1)) { + rad_suid_up(); + rcode = fchown(fd, uid, gid); + rad_suid_down(); + } + close(fd); + + return rcode; +} + +/** Ensures that a filename cannot walk up the directory structure + * + * Also sanitizes control chars. + * + * @param request Current request (may be NULL). + * @param out Output buffer. + * @param outlen Size of the output buffer. + * @param in string to escape. + * @param arg Context arguments (unused, should be NULL). + */ +size_t rad_filename_make_safe(UNUSED REQUEST *request, char *out, size_t outlen, char const *in, UNUSED void *arg) +{ + char const *q = in; + char *p = out; + size_t left = outlen; + + while (*q) { + if (*q != '/') { + if (left < 2) break; + + /* + * Smash control characters and spaces to + * something simpler. + */ + if (*q < ' ') { + *(p++) = '_'; + q++; + continue; + } + + *(p++) = *(q++); + left--; + continue; + } + + /* + * For now, allow slashes in the expanded + * filename. This allows the admin to set + * attributes which create sub-directories. + * Unfortunately, it also allows users to send + * attributes which *may* end up creating + * sub-directories. + */ + if (left < 2) break; + *(p++) = *(q++); + + /* + * Get rid of ////../.././///.///..// + */ + redo: + /* + * Get rid of //// + */ + if (*q == '/') { + q++; + goto redo; + } + + /* + * Get rid of /./././ + */ + if ((q[0] == '.') && + (q[1] == '/')) { + q += 2; + goto redo; + } + + /* + * Get rid of /../../../ + */ + if ((q[0] == '.') && (q[1] == '.') && + (q[2] == '/')) { + q += 3; + goto redo; + } + } + *p = '\0'; + + return (p - out); +} + +/** Escapes the raw string such that it should be safe to use as part of a file path + * + * This function is designed to produce a string that's still readable but portable + * across the majority of file systems. + * + * For security reasons it cannot remove characters from the name, and must not allow + * collisions to occur between different strings. + * + * With that in mind '-' has been chosen as the escape character, and will be double + * escaped '-' -> '--' to avoid collisions. + * + * Escaping should be reversible if the original string needs to be extracted. + * + * @note function takes additional arguments so that it may be used as an xlat escape + * function but it's fine to call it directly. + * + * @note OSX/Unix/NTFS/VFAT have a max filename size of 255 bytes. + * + * @param request Current request (may be NULL). + * @param out Output buffer. + * @param outlen Size of the output buffer. + * @param in string to escape. + * @param arg Context arguments (unused, should be NULL). + */ +size_t rad_filename_escape(UNUSED REQUEST *request, char *out, size_t outlen, char const *in, UNUSED void *arg) +{ + size_t freespace = outlen; + + while (*in != '\0') { + size_t utf8_len; + + /* + * Encode multibyte UTF8 chars + */ + utf8_len = fr_utf8_char((uint8_t const *) in, -1); + if (utf8_len > 1) { + if (freespace <= (utf8_len * 3)) break; + + switch (utf8_len) { + case 2: + snprintf(out, freespace, "-%x-%x", in[0], in[1]); + break; + + case 3: + snprintf(out, freespace, "-%x-%x-%x", in[0], in[1], in[2]); + break; + + case 4: + snprintf(out, freespace, "-%x-%x-%x-%x", in[0], in[1], in[2], in[3]); + break; + } + + freespace -= (utf8_len * 3); + out += (utf8_len * 3); + in += utf8_len; + + continue; + } + + /* + * Safe chars + */ + if (((*in >= 'A') && (*in <= 'Z')) || + ((*in >= 'a') && (*in <= 'z')) || + ((*in >= '0') && (*in <= '9')) || + (*in == '_')) { + if (freespace <= 1) break; + + *out++ = *in++; + freespace--; + continue; + } + if (freespace <= 2) break; + + /* + * Double escape '-' (like \\) + */ + if (*in == '-') { + *out++ = '-'; + *out++ = '-'; + + freespace -= 2; + in++; + continue; + } + + /* + * Unsafe chars + */ + *out++ = '-'; + fr_bin2hex(out, (uint8_t const *)in++, 1); + out += 2; + freespace -= 3; + } + *out = '\0'; + + return outlen - freespace; +} + +/** Converts data stored in a file name back to its original form + * + * @param out Where to write the unescaped string (may be the same as in). + * @param outlen Length of the output buffer. + * @param in Input filename. + * @param inlen Length of input. + * @return number of bytes written to output buffer, or offset where parse error + * occurred on failure. + */ +ssize_t rad_filename_unescape(char *out, size_t outlen, char const *in, size_t inlen) +{ + char const *p, *end = in + inlen; + size_t freespace = outlen; + + for (p = in; p < end; p++) { + if (freespace <= 1) break; + + if (((*p >= 'A') && (*p <= 'Z')) || + ((*p >= 'a') && (*p <= 'z')) || + ((*p >= '0') && (*p <= '9')) || + (*p == '_')) { + *out++ = *p; + freespace--; + continue; + } + + if (p[0] == '-') { + /* + * End of input, '-' needs at least one extra char after + * it to be valid. + */ + if ((end - p) < 2) return in - p; + if (p[1] == '-') { + p++; + *out++ = '-'; + freespace--; + continue; + } + + /* + * End of input, '-' must be followed by <hex><hex> + * but there aren't enough chars left + */ + if ((end - p) < 3) return in - p; + + /* + * If hex2bin returns 0 the next two chars weren't hexits. + */ + if (fr_hex2bin((uint8_t *) out, 1, in, 1) == 0) return in - (p + 1); + in += 2; + out++; + freespace--; + } + + return in - p; /* offset we found the bad char at */ + } + *out = '\0'; + + return outlen - freespace; /* how many bytes were written */ +} + +/* + * Allocate memory, or exit. + * + * This call ALWAYS succeeds! + */ +void *rad_malloc(size_t size) +{ + void *ptr = malloc(size); + + if (ptr == NULL) { + ERROR("no memory"); + fr_exit(1); + } + + return ptr; +} + + +void rad_const_free(void const *ptr) +{ + void *tmp; + if (!ptr) return; + + memcpy(&tmp, &ptr, sizeof(tmp)); + talloc_free(tmp); +} + + +/* + * Logs an error message and aborts the program + * + */ + +void NEVER_RETURNS rad_assert_fail(char const *file, unsigned int line, char const *expr) +{ + ERROR("ASSERT FAILED %s[%u]: %s", file, line, expr); + fr_fault(SIGABRT); + fr_exit_now(1); +} + +/* + * Free a REQUEST struct. + */ +static int _request_free(REQUEST *request) +{ + rad_assert(!request->in_request_hash); +#ifdef WITH_PROXY + rad_assert(!request->in_proxy_hash); +#endif + rad_assert(!request->ev); + +#ifdef WITH_COA + rad_assert(request->coa == NULL); +#endif + +#ifndef NDEBUG + request->magic = 0x01020304; /* set the request to be nonsense */ +#endif + request->client = NULL; +#ifdef WITH_PROXY + request->home_server = NULL; +#endif + + /* + * This is parented separately. + */ + if (request->state_ctx) { + talloc_free(request->state_ctx); + } + + return 0; +} + +/* + * Create a new REQUEST data structure. + */ +REQUEST *request_alloc(TALLOC_CTX *ctx) +{ + REQUEST *request; + + request = talloc_zero(ctx, REQUEST); + if (!request) return NULL; + talloc_set_destructor(request, _request_free); +#ifndef NDEBUG + request->magic = REQUEST_MAGIC; +#endif +#ifdef WITH_PROXY + request->proxy = NULL; +#endif + request->reply = NULL; +#ifdef WITH_PROXY + request->proxy_reply = NULL; +#endif + request->config = NULL; + request->username = NULL; + request->password = NULL; + request->timestamp = time(NULL); + request->log.lvl = rad_debug_lvl; /* Default to global debug level */ + + request->module = ""; + request->component = "<core>"; + request->log.func = vradlog_request; + + request->state_ctx = talloc_init("session-state"); + + return request; +} + + +/* + * Create a new REQUEST, based on an old one. + * + * This function allows modules to inject fake requests + * into the server, for tunneled protocols like TTLS & PEAP. + */ +REQUEST *request_alloc_fake(REQUEST *request) +{ + REQUEST *fake; + + fake = request_alloc(request); + if (!fake) return NULL; + + fake->number = request->number; +#ifdef HAVE_PTHREAD_H + fake->child_pid = request->child_pid; +#endif + fake->parent = request; + fake->root = request->root; + fake->client = request->client; + + /* + * For new server support. + * + * FIXME: Key instead off of a "virtual server" data structure. + * + * FIXME: Permit different servers for inner && outer sessions? + */ + fake->server = request->server; + + fake->packet = rad_alloc(fake, true); + if (!fake->packet) { + talloc_free(fake); + return NULL; + } + + fake->reply = rad_alloc(fake, false); + if (!fake->reply) { + talloc_free(fake); + return NULL; + } + + fake->master_state = REQUEST_ACTIVE; + fake->child_state = REQUEST_RUNNING; + + /* + * Fill in the fake request. + */ + fake->packet->sockfd = -1; + fake->packet->src_ipaddr = request->packet->src_ipaddr; + fake->packet->src_port = request->packet->src_port; + fake->packet->dst_ipaddr = request->packet->dst_ipaddr; + fake->packet->dst_port = 0; + + /* + * This isn't STRICTLY required, as the fake request MUST NEVER + * be put into the request list. However, it's still reasonable + * practice. + */ + fake->packet->id = fake->number & 0xff; + fake->packet->code = request->packet->code; + fake->timestamp = request->timestamp; + fake->packet->timestamp = request->packet->timestamp; + + /* + * Required for new identity support + */ + fake->listener = request->listener; + + /* + * Fill in the fake reply, based on the fake request. + */ + fake->reply->sockfd = fake->packet->sockfd; + fake->reply->src_ipaddr = fake->packet->dst_ipaddr; + fake->reply->src_port = fake->packet->dst_port; + fake->reply->dst_ipaddr = fake->packet->src_ipaddr; + fake->reply->dst_port = fake->packet->src_port; + fake->reply->id = fake->packet->id; + fake->reply->code = 0; /* UNKNOWN code */ + + /* + * Copy debug information. + */ + memcpy(&(fake->log), &(request->log), sizeof(fake->log)); + fake->log.indent = 0; /* Apart from the indent which we reset */ + + return fake; +} + +#ifdef WITH_COA +static int null_handler(UNUSED REQUEST *request) +{ + return 0; +} + +REQUEST *request_alloc_coa(REQUEST *request) +{ + if (!request || request->coa) return NULL; + + /* + * Originate CoA requests only when necessary. + */ + if ((request->packet->code != PW_CODE_ACCESS_REQUEST) && + (request->packet->code != PW_CODE_ACCOUNTING_REQUEST)) return NULL; + + request->coa = request_alloc_fake(request); + if (!request->coa) return NULL; + + request->coa->handle = null_handler; + request->coa->options = RAD_REQUEST_OPTION_COA; /* is a CoA packet */ + request->coa->packet->code = 0; /* unknown, as of yet */ + request->coa->child_state = REQUEST_RUNNING; + request->coa->proxy = rad_alloc(request->coa, false); + if (!request->coa->proxy) { + TALLOC_FREE(request->coa); + return NULL; + } + + return request->coa; +} +#endif + +/* + * Copy a quoted string. + */ +int rad_copy_string(char *to, char const *from) +{ + int length = 0; + char quote = *from; + + do { + if (*from == '\\') { + *(to++) = *(from++); + length++; + } + *(to++) = *(from++); + length++; + } while (*from && (*from != quote)); + + if (*from != quote) return -1; /* not properly quoted */ + + *(to++) = quote; + length++; + *to = '\0'; + + return length; +} + +/* + * Copy a quoted string but without the quotes. The length + * returned is the number of chars written; the number of + * characters consumed is 2 more than this. + */ +int rad_copy_string_bare(char *to, char const *from) +{ + int length = 0; + char quote = *from; + + from++; + while (*from && (*from != quote)) { + if (*from == '\\') { + *(to++) = *(from++); + length++; + } + *(to++) = *(from++); + length++; + } + + if (*from != quote) return -1; /* not properly quoted */ + + *to = '\0'; + + return length; +} + + +/* + * Copy a %{} string. + */ +int rad_copy_variable(char *to, char const *from) +{ + int length = 0; + int sublen; + + *(to++) = *(from++); + length++; + + while (*from) { + switch (*from) { + case '"': + case '\'': + sublen = rad_copy_string(to, from); + if (sublen < 0) return sublen; + from += sublen; + to += sublen; + length += sublen; + break; + + case '}': /* end of variable expansion */ + *(to++) = *(from++); + *to = '\0'; + length++; + return length; /* proper end of variable */ + + case '\\': + *(to++) = *(from++); + *(to++) = *(from++); + length += 2; + break; + + case '%': /* start of variable expansion */ + if (from[1] == '{') { + *(to++) = *(from++); + length++; + + sublen = rad_copy_variable(to, from); + if (sublen < 0) return sublen; + from += sublen; + to += sublen; + length += sublen; + break; + } /* else FIXME: catch %%{ ?*/ + + /* FALL-THROUGH */ + default: + *(to++) = *(from++); + length++; + break; + } + } /* loop over the input string */ + + /* + * We ended the string before a trailing '}' + */ + + return -1; +} + +#ifndef USEC +#define USEC 1000000 +#endif + +uint32_t rad_pps(uint32_t *past, uint32_t *present, time_t *then, struct timeval *now) +{ + uint32_t pps; + + if (*then != now->tv_sec) { + *then = now->tv_sec; + *past = *present; + *present = 0; + } + + /* + * Bootstrap PPS by looking at a percentage of + * the previous PPS. This lets us take a moving + * count, without doing a moving average. If + * we're a fraction "f" (0..1) into the current + * second, we can get a good guess for PPS by + * doing: + * + * PPS = pps_now + pps_old * (1 - f) + * + * It's an instantaneous measurement, rather than + * a moving average. This will hopefully let it + * respond better to sudden spikes. + * + * Doing the calculations by thousands allows us + * to not overflow 2^32, AND to not underflow + * when we divide by USEC. + */ + pps = USEC - now->tv_usec; /* useconds left in previous second */ + pps /= 1000; /* scale to milliseconds */ + pps *= *past; /* multiply by past count to get fraction */ + pps /= 1000; /* scale to usec again */ + pps += *present; /* add in current count */ + + return pps; +} + +/** Split string into words and expand each one + * + * @param request Current request. + * @param cmd string to split. + * @param max_argc the maximum number of arguments to split into. + * @param argv Where to write the pointers into argv_buf. + * @param can_fail If false, stop processing if any of the xlat expansions fail. + * @param argv_buflen size of argv_buf. + * @param argv_buf temporary buffer we used to mangle/expand cmd. + * Pointers to offsets of this buffer will be written to argv. + * @return argc or -1 on failure. + */ + +int rad_expand_xlat(REQUEST *request, char const *cmd, + int max_argc, char const *argv[], bool can_fail, + size_t argv_buflen, char *argv_buf) +{ + char const *from; + char *to; + int argc = -1; + int i; + int left; + + if (strlen(cmd) > (argv_buflen - 1)) { + ERROR("rad_expand_xlat: Command line is too long"); + return -1; + } + + /* + * Check for bad escapes. + */ + if (cmd[strlen(cmd) - 1] == '\\') { + ERROR("rad_expand_xlat: Command line has final backslash, without a following character"); + return -1; + } + + strlcpy(argv_buf, cmd, argv_buflen); + + /* + * Split the string into argv's BEFORE doing radius_xlat... + */ + from = cmd; + to = argv_buf; + argc = 0; + while (*from) { + int length; + + /* + * Skip spaces. + */ + if ((*from == ' ') || (*from == '\t')) { + from++; + continue; + } + + argv[argc] = to; + argc++; + + if (argc >= (max_argc - 1)) break; + + /* + * Copy the argv over to our buffer. + */ + while (*from && (*from != ' ') && (*from != '\t')) { + if (to >= argv_buf + argv_buflen - 1) { + ERROR("rad_expand_xlat: Ran out of space in command line"); + return -1; + } + + switch (*from) { + case '"': + case '\'': + length = rad_copy_string_bare(to, from); + if (length < 0) { + ERROR("rad_expand_xlat: Invalid string passed as argument"); + return -1; + } + from += length+2; + to += length; + break; + + case '%': + if (from[1] == '{') { + *(to++) = *(from++); + + length = rad_copy_variable(to, from); + if (length < 0) { + ERROR("rad_expand_xlat: Invalid variable expansion passed as argument"); + return -1; + } + from += length; + to += length; + } else { /* FIXME: catch %%{ ? */ + *(to++) = *(from++); + } + break; + + case '\\': + if (from[1] == ' ') from++; + /* FALL-THROUGH */ + + default: + *(to++) = *(from++); + } + } /* end of string, or found a space */ + + *(to++) = '\0'; /* terminate the string */ + } + + /* + * We have to have SOMETHING, at least. + */ + if (argc <= 0) { + ERROR("rad_expand_xlat: Empty command line"); + return -1; + } + + /* + * Expand each string, as appropriate. + */ + left = argv_buf + argv_buflen - to; + for (i = 0; i < argc; i++) { + int sublen; + + /* + * Don't touch argv's which won't be translated. + */ + if (strchr(argv[i], '%') == NULL) continue; + + if (!request) continue; + + sublen = radius_xlat(to, left - 1, request, argv[i], NULL, NULL); + if (sublen <= 0) { + if (can_fail) { + /* + * Fail to be backwards compatible. + * + * It's yucky, but it won't break anything, + * and it won't cause security problems. + */ + sublen = 0; + } else { + ERROR("rad_expand_xlat: xlat failed"); + return -1; + } + } + + argv[i] = to; + to += sublen; + *(to++) = '\0'; + left -= sublen; + left--; + + if (left <= 0) { + ERROR("rad_expand_xlat: Ran out of space while expanding arguments"); + return -1; + } + } + argv[argc] = NULL; + + return argc; +} + +#ifndef NDEBUG +/* + * Verify a packet. + */ +static void verify_packet(char const *file, int line, REQUEST *request, RADIUS_PACKET *packet, char const *name) +{ + TALLOC_CTX *parent; + + if (!packet) { + fprintf(stderr, "CONSISTENCY CHECK FAILED %s[%i]: RADIUS_PACKET %s pointer was NULL", file, line, name); + fr_assert(0); + fr_exit_now(0); + } + + parent = talloc_parent(packet); + if (parent != request) { + ERROR("CONSISTENCY CHECK FAILED %s[%i]: Expected RADIUS_PACKET %s to be parented by %p (%s), " + "but parented by %p (%s)", file, line, name, request, talloc_get_name(request), + parent, parent ? talloc_get_name(parent) : "NULL"); + + fr_log_talloc_report(packet); + if (parent) fr_log_talloc_report(parent); + + rad_assert(0); + } + + VERIFY_PACKET(packet); + + if (!packet->vps) return; + +#ifdef WITH_VERIFY_PTR + fr_pair_list_verify(file, line, packet, packet->vps, name); +#endif +} +/* + * Catch horrible talloc errors. + */ +void verify_request(char const *file, int line, REQUEST *request) +{ + if (!request) { + fprintf(stderr, "CONSISTENCY CHECK FAILED %s[%i]: REQUEST pointer was NULL", file, line); + fr_assert(0); + fr_exit_now(0); + } + + (void) talloc_get_type_abort(request, REQUEST); + +#ifdef WITH_VERIFY_PTR + fr_pair_list_verify(file, line, request, request->config, "config"); + fr_pair_list_verify(file, line, request->state_ctx, request->state, "state"); +#endif + + if (request->packet) verify_packet(file, line, request, request->packet, "request"); + if (request->reply) verify_packet(file, line, request, request->reply, "reply"); +#ifdef WITH_PROXY + if (request->proxy) verify_packet(file, line, request, request->proxy, "proxy-request"); + if (request->proxy_reply) verify_packet(file, line, request, request->proxy_reply, "proxy-reply"); +#endif + +#ifdef WITH_COA + if (request->coa) { + void *parent; + + (void) talloc_get_type_abort(request->coa, REQUEST); + parent = talloc_parent(request->coa); + + rad_assert(parent == request); + + verify_request(file, line, request->coa); + } +#endif +} +#endif + +/** Convert mode_t into humanly readable permissions flags + * + * @author Jonathan Leffler. + * + * @param mode to convert. + * @param out Where to write the string to, must be exactly 10 bytes long. + */ +void rad_mode_to_str(char out[10], mode_t mode) +{ + static char const *rwx[] = {"---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"}; + + strcpy(&out[0], rwx[(mode >> 6) & 0x07]); + strcpy(&out[3], rwx[(mode >> 3) & 0x07]); + strcpy(&out[6], rwx[(mode & 7)]); + if (mode & S_ISUID) out[2] = (mode & 0100) ? 's' : 'S'; + if (mode & S_ISGID) out[5] = (mode & 0010) ? 's' : 'l'; + if (mode & S_ISVTX) out[8] = (mode & 0100) ? 't' : 'T'; + out[9] = '\0'; +} + +void rad_mode_to_oct(char out[5], mode_t mode) +{ + out[0] = '0' + ((mode >> 9) & 0x07); + out[1] = '0' + ((mode >> 6) & 0x07); + out[2] = '0' + ((mode >> 3) & 0x07); + out[3] = '0' + (mode & 0x07); + out[4] = '\0'; +} + +/** Resolve a uid to a passwd entry + * + * Resolves a uid to a passwd entry. The memory to hold the + * passwd entry is talloced under ctx, and must be freed when no + * longer required. + * + * @param ctx to allocate passwd entry in. + * @param out Where to write pointer to entry. + * @param uid to resolve. + * @return 0 on success, -1 on error. + */ +int rad_getpwuid(TALLOC_CTX *ctx, struct passwd **out, uid_t uid) +{ + static size_t len; + uint8_t *buff; + int ret; + + *out = NULL; + + /* + * We assume this won't change between calls, + * and that the value is the same, so races don't + * matter. + */ + if (len == 0) { +#ifdef _SC_GETPW_R_SIZE_MAX + long int sc_len; + + sc_len = sysconf(_SC_GETPW_R_SIZE_MAX); + if (sc_len <= 0) sc_len = 1024; + len = (size_t)sc_len; +#else + len = 1024; +#endif + } + + buff = talloc_array(ctx, uint8_t, sizeof(struct passwd) + len); + if (!buff) return -1; + + /* + * In some cases we may need to dynamically + * grow the string buffer. + */ + while ((ret = getpwuid_r(uid, (struct passwd *)buff, (char *)(buff + sizeof(struct passwd)), + talloc_array_length(buff) - sizeof(struct passwd), out)) == ERANGE) { + buff = talloc_realloc_size(ctx, buff, talloc_array_length(buff) * 2); + if (!buff) { + talloc_free(buff); + return -1; + } + } + + if ((ret != 0) || !*out) { + fr_strerror_printf("Failed resolving UID: %s", fr_syserror(ret)); + talloc_free(buff); + errno = ret; + return -1; + } + + talloc_set_type(buff, struct passwd); + *out = (struct passwd *)buff; + + return 0; +} + +/** Resolve a username to a passwd entry + * + * Resolves a username to a passwd entry. The memory to hold the + * passwd entry is talloced under ctx, and must be freed when no + * longer required. + * + * @param ctx to allocate passwd entry in. + * @param out Where to write pointer to entry. + * @param name to resolve. + * @return 0 on success, -1 on error. + */ +int rad_getpwnam(TALLOC_CTX *ctx, struct passwd **out, char const *name) +{ + static size_t len; + uint8_t *buff; + int ret; + + *out = NULL; + + /* + * We assume this won't change between calls, + * and that the value is the same, so races don't + * matter. + */ + if (len == 0) { +#ifdef _SC_GETPW_R_SIZE_MAX + long int sc_len; + + sc_len = sysconf(_SC_GETPW_R_SIZE_MAX); + if (sc_len <= 0) sc_len = 1024; + len = (size_t)sc_len; +#else + sc_len = 1024; +#endif + } + + buff = talloc_array(ctx, uint8_t, sizeof(struct passwd) + len); + if (!buff) return -1; + + /* + * In some cases we may need to dynamically + * grow the string buffer. + */ + while ((ret = getpwnam_r(name, (struct passwd *)buff, (char *)(buff + sizeof(struct passwd)), + talloc_array_length(buff) - sizeof(struct passwd), out)) == ERANGE) { + buff = talloc_realloc_size(ctx, buff, talloc_array_length(buff) * 2); + if (!buff) { + talloc_free(buff); + return -1; + } + } + + if ((ret != 0) || !*out) { + fr_strerror_printf("Failed resolving UID: %s", fr_syserror(ret)); + talloc_free(buff); + errno = ret; + return -1; + } + + talloc_set_type(buff, struct passwd); + *out = (struct passwd *)buff; + + return 0; +} + +/** Resolve a gid to a group database entry + * + * Resolves a gid to a group database entry. The memory to hold the + * group entry is talloced under ctx, and must be freed when no + * longer required. + * + * @param ctx to allocate passwd entry in. + * @param out Where to write pointer to entry. + * @param gid to resolve. + * @return 0 on success, -1 on error. + */ +int rad_getgrgid(TALLOC_CTX *ctx, struct group **out, gid_t gid) +{ + static size_t len; + uint8_t *buff; + int ret; + + *out = NULL; + + /* + * We assume this won't change between calls, + * and that the value is the same, so races don't + * matter. + */ + if (len == 0) { +#ifdef _SC_GETGR_R_SIZE_MAX + long int sc_len; + + sc_len = sysconf(_SC_GETGR_R_SIZE_MAX); + if (sc_len <= 0) sc_len = 1024; + len = (size_t)sc_len; +#else + sc_len = 1024; +#endif + } + + buff = talloc_array(ctx, uint8_t, sizeof(struct group) + len); + if (!buff) return -1; + + /* + * In some cases we may need to dynamically + * grow the string buffer. + */ + while ((ret = getgrgid_r(gid, (struct group *)buff, (char *)(buff + sizeof(struct group)), + talloc_array_length(buff) - sizeof(struct group), out)) == ERANGE) { + buff = talloc_realloc_size(ctx, buff, talloc_array_length(buff) * 2); + if (!buff) { + talloc_free(buff); + return -1; + } + } + + if ((ret != 0) || !*out) { + fr_strerror_printf("Failed resolving GID: %s", fr_syserror(ret)); + talloc_free(buff); + errno = ret; + return -1; + } + + talloc_set_type(buff, struct group); + *out = (struct group *)buff; + + return 0; +} + +/** Resolve a group name to a group database entry + * + * Resolves a group name to a group database entry. + * The memory to hold the group entry is talloced under ctx, + * and must be freed when no longer required. + * + * @param ctx to allocate passwd entry in. + * @param out Where to write pointer to entry. + * @param name to resolve. + * @return 0 on success, -1 on error. + */ +int rad_getgrnam(TALLOC_CTX *ctx, struct group **out, char const *name) +{ + static size_t len; + uint8_t *buff; + int ret; + + *out = NULL; + + /* + * We assume this won't change between calls, + * and that the value is the same, so races don't + * matter. + */ + if (len == 0) { +#ifdef _SC_GETGR_R_SIZE_MAX + long int sc_len; + + sc_len = sysconf(_SC_GETGR_R_SIZE_MAX); + if (sc_len <= 0) sc_len = 1024; + len = (size_t)sc_len; +#else + len = 1024; +#endif + } + + buff = talloc_array(ctx, uint8_t, sizeof(struct group) + len); + if (!buff) return -1; + + /* + * In some cases we may need to dynamically + * grow the string buffer. + */ + while ((ret = getgrnam_r(name, (struct group *)buff, (char *)(buff + sizeof(struct group)), + talloc_array_length(buff) - sizeof(struct group), out)) == ERANGE) { + buff = talloc_realloc_size(ctx, buff, talloc_array_length(buff) * 2); + if (!buff) { + talloc_free(buff); + return -1; + } + } + + if ((ret != 0) || !*out) { + fr_strerror_printf("Failed resolving GID: %s", fr_syserror(ret)); + talloc_free(buff); + errno = ret; + return -1; + } + + talloc_set_type(buff, struct group); + *out = (struct group *)buff; + + return 0; +} + +/** Resolve a group name to a GID + * + * @param ctx TALLOC_CTX for temporary allocations. + * @param name of group. + * @param out where to write gid. + * @return 0 on success, -1 on error; + */ +int rad_getgid(TALLOC_CTX *ctx, gid_t *out, char const *name) +{ + int ret; + struct group *result; + + ret = rad_getgrnam(ctx, &result, name); + if (ret < 0) return -1; + + *out = result->gr_gid; + talloc_free(result); + return 0; +} + +/** Print uid to a string + * + * @note The reason for taking a fixed buffer is pure laziness. + * It means the caller doesn't have to free the string. + * + * @note Will always \0 terminate the buffer, even on error. + * + * @param ctx TALLOC_CTX for temporary allocations. + * @param out Where to write the uid string. + * @param outlen length of output buffer. + * @param uid to resolve. + * @return 0 on success, -1 on failure. + */ +int rad_prints_uid(TALLOC_CTX *ctx, char *out, size_t outlen, uid_t uid) +{ + struct passwd *result; + + rad_assert(outlen > 0); + + *out = '\0'; + + if (rad_getpwuid(ctx, &result, uid) < 0) return -1; + strlcpy(out, result->pw_name, outlen); + talloc_free(result); + + return 0; +} + +/** Print gid to a string + * + * @note The reason for taking a fixed buffer is pure laziness. + * It means the caller doesn't have to free the string. + * + * @note Will always \0 terminate the buffer, even on error. + * + * @param ctx TALLOC_CTX for temporary allocations. + * @param out Where to write the uid string. + * @param outlen length of output buffer. + * @param gid to resolve. + * @return 0 on success, -1 on failure. + */ +int rad_prints_gid(TALLOC_CTX *ctx, char *out, size_t outlen, gid_t gid) +{ + struct group *result; + + rad_assert(outlen > 0); + + *out = '\0'; + + if (rad_getgrgid(ctx, &result, gid) < 0) return -1; + strlcpy(out, result->gr_name, outlen); + talloc_free(result); + + return 0; +} + +#ifdef HAVE_SETUID +static bool doing_setuid = false; +static uid_t suid_down_uid = (uid_t)-1; + +/** Set the uid and gid used when dropping privileges + * + * @note if this function hasn't been called, rad_suid_down will have no effect. + * + * @param uid to drop down to. + */ +void rad_suid_set_down_uid(uid_t uid) +{ + suid_down_uid = uid; + doing_setuid = true; +} + +# if defined(HAVE_SETRESUID) && defined (HAVE_GETRESUID) +void rad_suid_up(void) +{ + uid_t ruid, euid, suid; + + if (getresuid(&ruid, &euid, &suid) < 0) { + ERROR("Failed getting saved UID's"); + fr_exit_now(1); + } + + if (setresuid(-1, suid, -1) < 0) { + ERROR("Failed switching to privileged user"); + fr_exit_now(1); + } + + if (geteuid() != suid) { + ERROR("Switched to unknown UID"); + fr_exit_now(1); + } +} + +void rad_suid_down(void) +{ + if (!doing_setuid) return; + + if (setresuid(-1, suid_down_uid, geteuid()) < 0) { + struct passwd *passwd; + char const *name; + + name = (rad_getpwuid(NULL, &passwd, suid_down_uid) < 0) ? "unknown" : passwd->pw_name; + ERROR("Failed switching to uid %s: %s", name, fr_syserror(errno)); + talloc_free(passwd); + fr_exit_now(1); + } + + if (geteuid() != suid_down_uid) { + ERROR("Failed switching uid: UID is incorrect"); + fr_exit_now(1); + } + + fr_reset_dumpable(); +} + +void rad_suid_down_permanent(void) +{ + if (!doing_setuid) return; + + if (setresuid(suid_down_uid, suid_down_uid, suid_down_uid) < 0) { + struct passwd *passwd; + char const *name; + + name = (rad_getpwuid(NULL, &passwd, suid_down_uid) < 0) ? "unknown" : passwd->pw_name; + ERROR("Failed in permanent switch to uid %s: %s", name, fr_syserror(errno)); + talloc_free(passwd); + fr_exit_now(1); + } + + if (geteuid() != suid_down_uid) { + ERROR("Switched to unknown uid"); + fr_exit_now(1); + } + + fr_reset_dumpable(); +} +# else +/* + * Much less secure... + */ +void rad_suid_up(void) +{ + if (!doing_setuid) return; + + if (seteuid(0) < 0) { + ERROR("Failed switching up to euid 0: %s", fr_syserror(errno)); + fr_exit_now(1); + } + +} + +void rad_suid_down(void) +{ + if (!doing_setuid) return; + + if (geteuid() == suid_down_uid) return; + + if (seteuid(suid_down_uid) < 0) { + struct passwd *passwd; + char const *name; + + name = (rad_getpwuid(NULL, &passwd, suid_down_uid) < 0) ? "unknown": passwd->pw_name; + ERROR("Failed switching to euid %s: %s", name, fr_syserror(errno)); + talloc_free(passwd); + fr_exit_now(1); + } + + fr_reset_dumpable(); +} + +void rad_suid_down_permanent(void) +{ + if (!doing_setuid) return; + + /* + * Already done. Don't do anything else. + */ + if (getuid() == suid_down_uid) return; + + /* + * We're root, but running as a normal user. Fix that, + * so we can call setuid(). + */ + if (geteuid() == suid_down_uid) { + rad_suid_up(); + } + + if (setuid(suid_down_uid) < 0) { + struct passwd *passwd; + char const *name; + + name = (rad_getpwuid(NULL, &passwd, suid_down_uid) < 0) ? "unknown": passwd->pw_name; + ERROR("Failed switching permanently to uid %s: %s", name, fr_syserror(errno)); + talloc_free(passwd); + fr_exit_now(1); + } + + fr_reset_dumpable(); +} +# endif /* HAVE_SETRESUID && HAVE_GETRESUID */ +#else /* HAVE_SETUID */ +void rad_suid_set_down_uid(uid_t uid) +{ +} +void rad_suid_up(void) +{ +} +void rad_suid_down(void) +{ + fr_reset_dumpable(); +} +void rad_suid_down_permanent(void) +{ + fr_reset_dumpable(); +} +#endif /* HAVE_SETUID */ + +/** Alter the effective user id + * + * @param uid to set + * @return 0 on success -1 on failure. + */ +int rad_seuid(uid_t uid) +{ + if (seteuid(uid) < 0) { + struct passwd *passwd; + + if (rad_getpwuid(NULL, &passwd, uid) < 0) return -1; + fr_strerror_printf("Failed setting euid to %s", passwd->pw_name); + talloc_free(passwd); + + return -1; + } + return 0; +} + +/** Alter the effective user id + * + * @param gid to set + * @return 0 on success -1 on failure. + */ +int rad_segid(gid_t gid) +{ + if (setegid(gid) < 0) { + struct group *group; + + if (rad_getgrgid(NULL, &group, gid) < 0) return -1; + fr_strerror_printf("Failed setting egid to %s", group->gr_name); + talloc_free(group); + + return -1; + } + return 0; +} + +/** Determine the elapsed time between two timevals + * + * @param end timeval nearest to the present + * @param start timeval furthest from the present + * @param elapsed Where to write the elapsed time + */ +void rad_tv_sub(struct timeval const *end, struct timeval const *start, struct timeval *elapsed) +{ + elapsed->tv_sec = end->tv_sec - start->tv_sec; + if (elapsed->tv_sec > 0) { + elapsed->tv_sec--; + elapsed->tv_usec = USEC; + } else { + elapsed->tv_usec = 0; + } + elapsed->tv_usec += end->tv_usec; + elapsed->tv_usec -= start->tv_usec; + + if (elapsed->tv_usec >= USEC) { + elapsed->tv_usec -= USEC; + elapsed->tv_sec++; + } +} diff --git a/src/main/version.c b/src/main/version.c new file mode 100644 index 0000000..2fe3428 --- /dev/null +++ b/src/main/version.c @@ -0,0 +1,625 @@ +/* + * version.c Print version number and exit. + * + * Version: $Id$ + * + * This program 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 + * + * Copyright 1999-2019 The FreeRADIUS server project + * Copyright 2012 Alan DeKok <aland@ox.org> + * Copyright 2000 Chris Parker <cparker@starnetusa.com> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +USES_APPLE_DEPRECATED_API /* OpenSSL API has been deprecated by Apple */ + +static uint64_t libmagic = RADIUSD_MAGIC_NUMBER; +char const *radiusd_version_short = RADIUSD_VERSION_STRING; + +#ifdef HAVE_OPENSSL_CRYPTO_H +# include <openssl/crypto.h> +# include <openssl/opensslv.h> + +static long ssl_built = OPENSSL_VERSION_NUMBER; + +/** Check built and linked versions of OpenSSL match + * + * OpenSSL version number consists of: + * MNNFFPPS: major minor fix patch status + * + * Where status >= 0 && < 10 means beta, and status 10 means release. + * + * https://wiki.openssl.org/index.php/Versioning + * + * Startup check for whether the linked version of OpenSSL matches the + * version the server was built against. + * + * @return 0 if ok, else -1 + */ +int ssl_check_consistency(void) +{ + long ssl_linked; + + ssl_linked = SSLeay(); + + /* + * Major and minor versions mismatch, that's bad. + */ + if ((ssl_linked & 0xfff00000) != (ssl_built & 0xfff00000)) goto mismatch; + + /* + * 1.1.0 and later export all of the APIs we need, so we + * don't care about mismatches in fix / patch / status + * fields. If the major && minor fields match, that's + * good enough. + */ + if ((ssl_linked & 0xfff00000) >= 0x10100000) return 0; + + /* + * Before 1.1.0, we need all kinds of stupid checks to + * see if it might work. + */ + + /* + * Status mismatch always triggers error. + */ + if ((ssl_linked & 0x0000000f) != (ssl_built & 0x0000000f)) { + mismatch: + ERROR("libssl version mismatch. built: %lx linked: %lx", + (unsigned long) ssl_built, + (unsigned long) ssl_linked); + + return -1; + } + + /* + * Use the OpenSSH approach and relax fix checks after version + * 1.0.0 and only allow moving backwards within a patch + * series. + */ + if (ssl_built & 0xf0000000) { + if ((ssl_built & 0xfffff000) != (ssl_linked & 0xfffff000) || + (ssl_built & 0x00000ff0) > (ssl_linked & 0x00000ff0)) goto mismatch; + /* + * Before 1.0.0 we require the same major minor and fix version + * and ignore the patch number. + */ + } else if ((ssl_built & 0xfffff000) != (ssl_linked & 0xfffff000)) goto mismatch; + + return 0; +} + +/** Convert a version number to a text string + * + * @note Not thread safe. + * + * @param v version to convert. + * @return pointer to a static buffer containing the version string. + */ +char const *ssl_version_by_num(uint32_t v) +{ + /* 2 (%s) + 1 (.) + 2 (%i) + 1 (.) + 2 (%i) + 1 (c) + 8 (%s) + \0 */ + static char buffer[18]; + char *p = buffer; + + p += sprintf(p, "%u.%u.%u", + (0xf0000000 & v) >> 28, + (0x0ff00000 & v) >> 20, + (0x000ff000 & v) >> 12); + + if ((0x00000ff0 & v) >> 4) { + *p++ = (char) (0x60 + ((0x00000ff0 & v) >> 4)); + } + + *p++ = ' '; + + /* + * Development (0) + */ + if ((0x0000000f & v) == 0) { + strcpy(p, "dev"); + /* + * Beta (1-14) + */ + } else if ((0x0000000f & v) <= 14) { + sprintf(p, "beta %u", 0x0000000f & v); + } else { + strcpy(p, "release"); + } + + return buffer; +} + +/** Return the linked SSL version number as a string + * + * @return pointer to a static buffer containing the version string. + */ +char const *ssl_version_num(void) +{ + long ssl_linked; + + ssl_linked = SSLeay(); + return ssl_version_by_num((uint32_t)ssl_linked); +} + +/** Convert two openssl version numbers into a range string + * + * @note Not thread safe. + * + * @param low version to convert. + * @param high version to convert. + * @return pointer to a static buffer containing the version range string. + */ +char const *ssl_version_range(uint32_t low, uint32_t high) +{ + /* 12 (version) + 3 ( - ) + 12 (version) */ + static char buffer[28]; + char *p = buffer; + + p += strlcpy(p, ssl_version_by_num(low), sizeof(buffer)); + p += strlcpy(p, " - ", sizeof(buffer) - (p - buffer)); + strlcpy(p, ssl_version_by_num(high), sizeof(buffer) - (p - buffer)); + + return buffer; +} + +/** Print the current linked version of Openssl + * + * Print the currently linked version of the OpenSSL library. + * + * @note Not thread safe. + * @return pointer to a static buffer containing libssl version information. + */ +char const *ssl_version(void) +{ + static char buffer[256]; + + uint32_t v = SSLeay(); + + snprintf(buffer, sizeof(buffer), "%s 0x%.8x (%s)", + SSLeay_version(SSLEAY_VERSION), /* Not all builds include a useful version number */ + v, + ssl_version_by_num(v)); + + return buffer; +} +# else +int ssl_check_consistency(void) { + return 0; +} + +char const *ssl_version_num(void) +{ + return "not linked"; +} + +char const *ssl_version(void) +{ + return "not linked"; +} +#endif /* ifdef HAVE_OPENSSL_CRYPTO_H */ + +/** Check if the application linking to the library has the correct magic number + * + * @param magic number as defined by RADIUSD_MAGIC_NUMBER + * @returns 0 on success, -1 on prefix mismatch, -2 on version mismatch -3 on commit mismatch. + */ +int rad_check_lib_magic(uint64_t magic) +{ + if (MAGIC_PREFIX(magic) != MAGIC_PREFIX(libmagic)) { + ERROR("Application and libfreeradius-server magic number (prefix) mismatch." + " application: %x library: %x", + MAGIC_PREFIX(magic), MAGIC_PREFIX(libmagic)); + return -1; + } + + if (MAGIC_VERSION(magic) != MAGIC_VERSION(libmagic)) { + ERROR("Application and libfreeradius-server magic number (version) mismatch." + " application: %lx library: %lx", + (unsigned long) MAGIC_VERSION(magic), (unsigned long) MAGIC_VERSION(libmagic)); + return -2; + } + + if (MAGIC_COMMIT(magic) != MAGIC_COMMIT(libmagic)) { + ERROR("Application and libfreeradius-server magic number (commit) mismatch." + " application: %lx library: %lx", + (unsigned long) MAGIC_COMMIT(magic), (unsigned long) MAGIC_COMMIT(libmagic)); + return -3; + } + + return 0; +} + +/** Add a feature flag to the main configuration + * + * Add a feature flag (yes/no) to the 'feature' subsection + * off the main config. + * + * This allows the user to create configurations that work with + * across multiple environments. + * + * @param cs to add feature pair to. + * @param name of feature. + * @param enabled Whether the feature is present/enabled. + * @return 0 on success else -1. + */ +int version_add_feature(CONF_SECTION *cs, char const *name, bool enabled) +{ + if (!cs) return -1; + + if (!cf_pair_find(cs, name)) { + CONF_PAIR *cp; + + cp = cf_pair_alloc(cs, name, enabled ? "yes" : "no", + T_OP_SET, T_BARE_WORD, T_BARE_WORD); + if (!cp) return -1; + cf_pair_add(cs, cp); + } + + return 0; +} + +/** Add a library/server version pair to the main configuration + * + * Add a version number to the 'version' subsection off the main + * config. + * + * Because of the optimisations in the configuration parser, these + * may be checked using regular expressions without a performance + * penalty. + * + * The version pairs are there primarily to work around defects + * in libraries or the server. + * + * @param cs to add feature pair to. + * @param name of library or feature. + * @param version Humanly readable version text. + * @return 0 on success else -1. + */ +int version_add_number(CONF_SECTION *cs, char const *name, char const *version) +{ + CONF_PAIR *old; + + if (!cs) return -1; + + old = cf_pair_find(cs, name); + if (!old) { + CONF_PAIR *cp; + + cp = cf_pair_alloc(cs, name, version, T_OP_SET, T_BARE_WORD, T_SINGLE_QUOTED_STRING); + if (!cp) return -1; + + cf_pair_add(cs, cp); + } else { + WARN("Replacing user version.%s (%s) with %s", name, cf_pair_value(old), version); + + cf_pair_replace(cs, old, version); + } + + return 0; +} + + +/** Initialise core feature flags + * + * @param cs Where to add the CONF_PAIRS, if null pairs will be added + * to the 'feature' section of the main config. + */ +void version_init_features(CONF_SECTION *cs) +{ + version_add_feature(cs, "accounting", +#ifdef WITH_ACCOUNTING + true +#else + false +#endif + ); + + version_add_feature(cs, "authentication", true); + + version_add_feature(cs, "ascend-binary-attributes", +#ifdef WITH_ASCEND_BINARY + true +#else + false +#endif + ); + + version_add_feature(cs, "coa", +#ifdef WITH_COA + true +#else + false +#endif + ); + + + version_add_feature(cs, "recv-coa-from-home-server", +#ifdef WITH_COA_TUNNEL + true +#else + false +#endif + ); + + version_add_feature(cs, "control-socket", +#ifdef WITH_COMMAND_SOCKET + true +#else + false +#endif + ); + + + version_add_feature(cs, "detail", +#ifdef WITH_DETAIL + true +#else + false +#endif + ); + + version_add_feature(cs, "dhcp", +#ifdef WITH_DHCP + true +#else + false +#endif + ); + + version_add_feature(cs, "dynamic-clients", +#ifdef WITH_DYNAMIC_CLIENTS + true +#else + false +#endif + ); + + version_add_feature(cs, "osfc2", +#ifdef OSFC2 + true +#else + false +#endif + ); + + version_add_feature(cs, "proxy", +#ifdef WITH_PROXY + true +#else + false +#endif + ); + + version_add_feature(cs, "regex-pcre", +#ifdef HAVE_PCRE + true +#else + false +#endif + ); + +#if !defined(HAVE_PCRE) && defined(HAVE_REGEX) + version_add_feature(cs, "regex-posix", true); + version_add_feature(cs, "regex-posix-extended", +# ifdef HAVE_REG_EXTENDED + true +# else + false +# endif + ); +#else + version_add_feature(cs, "regex-posix", false); + version_add_feature(cs, "regex-posix-extended", false); +#endif + + version_add_feature(cs, "session-management", +#ifdef WITH_SESSION_MGMT + true +#else + false +#endif + ); + + version_add_feature(cs, "stats", +#ifdef WITH_STATS + true +#else + false +#endif + ); + + version_add_feature(cs, "systemd", +#ifdef HAVE_SYSTEMD + true +#else + false +#endif + ); + + version_add_feature(cs, "tcp", +#ifdef WITH_TCP + true +#else + false +#endif + ); + + version_add_feature(cs, "threads", +#ifdef WITH_THREADS + true +#else + false +#endif + ); + + version_add_feature(cs, "tls", +#ifdef WITH_TLS + true +#else + false +#endif + ); + + version_add_feature(cs, "unlang", +#ifdef WITH_UNLANG + true +#else + false +#endif + ); + + version_add_feature(cs, "vmps", +#ifdef WITH_VMPS + true +#else + false +#endif + ); + + version_add_feature(cs, "developer", +#ifndef NDEBUG + true +#else + false +#endif + ); +} + +/** Initialise core version flags + * + * @param cs Where to add the CONF_PAIRS, if null pairs will be added + * to the 'version' section of the main config. + */ +void version_init_numbers(CONF_SECTION *cs) +{ + char buffer[128]; + + version_add_number(cs, "freeradius-server", radiusd_version_short); + + snprintf(buffer, sizeof(buffer), "%i.%i.*", talloc_version_major(), talloc_version_minor()); + version_add_number(cs, "talloc", buffer); + + version_add_number(cs, "ssl", ssl_version_num()); + +#if defined(HAVE_REGEX) && defined(HAVE_PCRE) + version_add_number(cs, "pcre", pcre_version()); +#endif +} + +static char const *spaces = " "; /* 40 */ + +/* + * Display the revision number for this program + */ +void version_print(void) +{ + CONF_SECTION *features, *versions; + CONF_ITEM *ci; + CONF_PAIR *cp; + + if (DEBUG_ENABLED3) { + int max = 0, len; + + MEM(features = cf_section_alloc(NULL, "feature", NULL)); + version_init_features(features); + + MEM(versions = cf_section_alloc(NULL, "version", NULL)); + version_init_numbers(versions); + + DEBUG2("Server was built with: "); + + for (ci = cf_item_find_next(features, NULL); + ci; + ci = cf_item_find_next(features, ci)) { + len = talloc_array_length(cf_pair_attr(cf_item_to_pair(ci))); + if (max < len) max = len; + } + + for (ci = cf_item_find_next(versions, NULL); + ci; + ci = cf_item_find_next(versions, ci)) { + len = talloc_array_length(cf_pair_attr(cf_item_to_pair(ci))); + if (max < len) max = len; + } + + + for (ci = cf_item_find_next(features, NULL); + ci; + ci = cf_item_find_next(features, ci)) { + char const *attr; + + cp = cf_item_to_pair(ci); + attr = cf_pair_attr(cp); + + DEBUG2(" %s%.*s : %s", attr, + (int)(max - talloc_array_length(attr)), spaces, cf_pair_value(cp)); + } + + talloc_free(features); + + DEBUG2("Server core libs:"); + + for (ci = cf_item_find_next(versions, NULL); + ci; + ci = cf_item_find_next(versions, ci)) { + char const *attr; + + cp = cf_item_to_pair(ci); + attr = cf_pair_attr(cp); + + DEBUG2(" %s%.*s : %s", attr, + (int)(max - talloc_array_length(attr)), spaces, cf_pair_value(cp)); + } + + talloc_free(versions); + + DEBUG2("Endianness:"); +#if defined(FR_LITTLE_ENDIAN) + DEBUG2(" little"); +#elif defined(FR_BIG_ENDIAN) + DEBUG2(" big"); +#else + DEBUG2(" unknown"); +#endif + + DEBUG2("Compilation flags:"); +#ifdef BUILT_WITH_CPPFLAGS + DEBUG2(" cppflags : " BUILT_WITH_CPPFLAGS); +#endif +#ifdef BUILT_WITH_CFLAGS + DEBUG2(" cflags : " BUILT_WITH_CFLAGS); +#endif +#ifdef BUILT_WITH_LDFLAGS + DEBUG2(" ldflags : " BUILT_WITH_LDFLAGS); +#endif +#ifdef BUILT_WITH_LIBS + DEBUG2(" libs : " BUILT_WITH_LIBS); +#endif + DEBUG2(" "); + } + INFO("FreeRADIUS Version " RADIUSD_VERSION_STRING); + INFO("Copyright (C) 1999-2022 The FreeRADIUS server project and contributors"); + INFO("There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A"); + INFO("PARTICULAR PURPOSE"); + INFO("You may redistribute copies of FreeRADIUS under the terms of the"); + INFO("GNU General Public License"); + INFO("For more information about these matters, see the file named COPYRIGHT"); + + fflush(NULL); +} + diff --git a/src/main/xlat.c b/src/main/xlat.c new file mode 100644 index 0000000..4bd0a37 --- /dev/null +++ b/src/main/xlat.c @@ -0,0 +1,2696 @@ +/* + * This program 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 xlat.c + * @brief String expansion ("translation"). Implements %Attribute -> value + * + * @copyright 2000,2006 The FreeRADIUS server project + * @copyright 2000 Alan DeKok <aland@ox.org> + */ + +RCSID("$Id$") + +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/parser.h> +#include <freeradius-devel/rad_assert.h> +#include <freeradius-devel/base64.h> + +#include <ctype.h> + +typedef struct xlat_t { + char name[MAX_STRING_LEN]; //!< Name of the xlat expansion. + int length; //!< Length of name. + void *instance; //!< Module instance passed to xlat and escape functions. + xlat_func_t func; //!< xlat function. + xlat_escape_t escape; //!< Escape function to apply to dynamic input to func. + bool internal; //!< If true, cannot be redefined. +} xlat_t; + +typedef enum { + XLAT_LITERAL, //!< Literal string + XLAT_PERCENT, //!< Literal string with %v + XLAT_MODULE, //!< xlat module + XLAT_VIRTUAL, //!< virtual attribute + XLAT_ATTRIBUTE, //!< xlat attribute +#ifdef HAVE_REGEX + XLAT_REGEX, //!< regex reference +#endif + XLAT_ALTERNATE //!< xlat conditional syntax :- +} xlat_state_t; + +struct xlat_exp { + char const *fmt; //!< The format string. + size_t len; //!< Length of the format string. + + xlat_state_t type; //!< type of this expansion. + xlat_exp_t *next; //!< Next in the list. + + xlat_exp_t *child; //!< Nested expansion. + xlat_exp_t *alternate; //!< Alternative expansion if this one expanded to a zero length string. + + vp_tmpl_t attr; //!< An attribute template. + xlat_t const *xlat; //!< The xlat expansion to expand format with. +}; + +static rbtree_t *xlat_root = NULL; + +#ifdef WITH_UNLANG +static char const * const xlat_foreach_names[] = {"Foreach-Variable-0", + "Foreach-Variable-1", + "Foreach-Variable-2", + "Foreach-Variable-3", + "Foreach-Variable-4", + "Foreach-Variable-5", + "Foreach-Variable-6", + "Foreach-Variable-7", + "Foreach-Variable-8", + "Foreach-Variable-9", + NULL}; +#endif + + +static int xlat_inst[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; /* up to 10 for foreach */ + +static char *xlat_getvp(TALLOC_CTX *ctx, REQUEST *request, vp_tmpl_t const *vpt, + bool escape, bool return_null, char const *concat); + +/** Concatenation + * + */ +static ssize_t xlat_concat(UNUSED void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + ssize_t slen; + vp_tmpl_t vpt; + char *str; + char const *concat; + char buffer[2]; + + while (isspace((uint8_t) *fmt)) fmt++; + + slen = tmpl_from_attr_substr(&vpt, fmt, REQUEST_CURRENT, PAIR_LIST_REQUEST, false, false); + if (slen <= 0) { + RDEBUG("%s", fr_strerror()); + return -1; + } + + fmt += slen; + while (isspace((uint8_t) *fmt)) fmt++; + + if (!*fmt) { + concat = ","; + } else { + buffer[0] = *fmt; + buffer[1] = '\0'; + + concat = buffer; + } + + str = xlat_getvp(request, request, &vpt, true, true, concat); + if (!str) return 0; + + strlcpy(out, str, outlen); + talloc_free(str); + + return strlen(out); +} + +/** Print length of its RHS. + * + */ +static ssize_t xlat_strlen(UNUSED void *instance, UNUSED REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + snprintf(out, outlen, "%u", (unsigned int) strlen(fmt)); + return strlen(out); +} + +/** Print the size of the attribute in bytes. + * + */ +static ssize_t xlat_length(UNUSED void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + VALUE_PAIR *vp; + while (isspace((uint8_t) *fmt)) fmt++; + + if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) { + *out = '\0'; + return 0; + } + + snprintf(out, outlen, "%zu", vp->vp_length); + return strlen(out); +} + +/** Print data as integer, not as VALUE. + * + */ +static ssize_t xlat_integer(UNUSED void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + VALUE_PAIR *vp; + + uint64_t int64 = 0; /* Needs to be initialised to zero */ + uint32_t int32 = 0; /* Needs to be initialised to zero */ + + while (isspace((uint8_t) *fmt)) fmt++; + + if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) { + *out = '\0'; + return 0; + } + + switch (vp->da->type) { + case PW_TYPE_OCTETS: + case PW_TYPE_STRING: + if (vp->vp_length > 8) { + break; + } + + if (vp->vp_length > 4) { + memcpy(&int64, vp->vp_octets, vp->vp_length); + return snprintf(out, outlen, "%" PRIu64, htonll(int64)); + } + + memcpy(&int32, vp->vp_octets, vp->vp_length); + return snprintf(out, outlen, "%i", htonl(int32)); + + case PW_TYPE_INTEGER64: + return snprintf(out, outlen, "%" PRIu64, vp->vp_integer64); + + /* + * IP addresses are treated specially, as parsing functions assume the value + * is bigendian and will convert it for us. + */ + case PW_TYPE_IPV4_ADDR: + return snprintf(out, outlen, "%u", htonl(vp->vp_ipaddr)); + + case PW_TYPE_IPV4_PREFIX: + return snprintf(out, outlen, "%u", htonl((*(uint32_t *)(&vp->vp_ipv4prefix[2])))); + + case PW_TYPE_INTEGER: + return snprintf(out, outlen, "%u", vp->vp_integer); + + case PW_TYPE_DATE: + return snprintf(out, outlen, "%u", vp->vp_date); + + case PW_TYPE_BYTE: + return snprintf(out, outlen, "%u", (unsigned int) vp->vp_byte); + + case PW_TYPE_SHORT: + return snprintf(out, outlen, "%u", (unsigned int) vp->vp_short); + + /* + * Ethernet is weird... It's network related, so we assume to it should be + * bigendian. + */ + case PW_TYPE_ETHERNET: + memcpy(&int64, vp->vp_ether, vp->vp_length); + return snprintf(out, outlen, "%" PRIu64, htonll(int64)); + + case PW_TYPE_SIGNED: + return snprintf(out, outlen, "%i", vp->vp_signed); + + case PW_TYPE_IPV6_ADDR: + return fr_prints_uint128(out, outlen, ntohlll(*(uint128_t const *) &vp->vp_ipv6addr)); + + case PW_TYPE_IPV6_PREFIX: + return fr_prints_uint128(out, outlen, ntohlll(*(uint128_t const *) &vp->vp_ipv6prefix[2])); + + default: + break; + } + + REDEBUG("Type '%s' of length %zu cannot be converted to integer", + fr_int2str(dict_attr_types, vp->da->type, "???"), vp->vp_length); + *out = '\0'; + + return -1; +} + +/** Print data as hex, not as VALUE. + * + */ +static ssize_t xlat_hex(UNUSED void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + size_t i; + VALUE_PAIR *vp; + uint8_t const *p; + ssize_t ret; + size_t len; + value_data_t dst; + uint8_t const *buff = NULL; + + while (isspace((uint8_t) *fmt)) fmt++; + + if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) { + error: + *out = '\0'; + return -1; + } + + /* + * The easy case. + */ + if (vp->da->type == PW_TYPE_OCTETS) { + p = vp->vp_octets; + len = vp->vp_length; + /* + * Cast the value_data_t of the VP to an octets string and + * print that as hex. + */ + } else { + ret = value_data_cast(request, &dst, PW_TYPE_OCTETS, NULL, vp->da->type, + NULL, &vp->data, vp->vp_length); + if (ret < 0) { + REDEBUG("%s", fr_strerror()); + goto error; + } + len = (size_t) ret; + p = buff = dst.octets; + } + + rad_assert(p); + + /* + * Don't truncate the data. + */ + if (outlen < (len * 2)) { + rad_const_free(buff); + goto error; + } + + for (i = 0; i < len; i++) { + snprintf(out + 2*i, 3, "%02x", p[i]); + } + rad_const_free(buff); + + return len * 2; +} + +/** Return the tag of an attribute reference + * + */ +static ssize_t xlat_tag(UNUSED void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + VALUE_PAIR *vp; + + while (isspace((uint8_t) *fmt)) fmt++; + + if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) { + *out = '\0'; + return 0; + } + + if (!vp->da->flags.has_tag || !TAG_VALID(vp->tag)) { + *out = '\0'; + return 0; + } + + return snprintf(out, outlen, "%u", vp->tag); +} + +/** Return the vendor of an attribute reference + * + */ +static ssize_t xlat_vendor(UNUSED void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + VALUE_PAIR *vp; + DICT_VENDOR *vendor; + + while (isspace((uint8_t) *fmt)) fmt++; + + if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) { + *out = '\0'; + return 0; + } + + vendor = dict_vendorbyvalue(vp->da->vendor); + if (!vendor) { + *out = '\0'; + return 0; + } + strlcpy(out, vendor->name, outlen); + + return vendor->length; +} + +/** Return the vendor number of an attribute reference + * + */ +static ssize_t xlat_vendor_num(UNUSED void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + VALUE_PAIR *vp; + + while (isspace((uint8_t) *fmt)) fmt++; + + if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) { + *out = '\0'; + return 0; + } + + return snprintf(out, outlen, "%u", vp->da->vendor); +} + +/** Return the attribute name of an attribute reference + * + */ +static ssize_t xlat_attr(UNUSED void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + VALUE_PAIR *vp; + + while (isspace((uint8_t) *fmt)) fmt++; + + if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) { + *out = '\0'; + return 0; + } + strlcpy(out, vp->da->name, outlen); + + return strlen(vp->da->name); +} + +/** Return the attribute number of an attribute reference + * + */ +static ssize_t xlat_attr_num(UNUSED void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + VALUE_PAIR *vp; + + while (isspace((uint8_t) *fmt)) fmt++; + + if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) { + *out = '\0'; + return 0; + } + + return snprintf(out, outlen, "%u", vp->da->attr); +} + +/** Print out attribute info + * + * Prints out all instances of a current attribute, or all attributes in a list. + * + * At higher debugging levels, also prints out alternative decodings of the same + * value. This is helpful to determine types for unknown attributes of long + * passed vendors, or just crazy/broken NAS. + * + * This expands to a zero length string. + */ +static ssize_t xlat_debug_attr(UNUSED void *instance, REQUEST *request, char const *fmt, + char *out, UNUSED size_t outlen) +{ + VALUE_PAIR *vp; + vp_cursor_t cursor; + + vp_tmpl_t vpt; + + if (!RDEBUG_ENABLED2) { + *out = '\0'; + return -1; + } + + while (isspace((uint8_t) *fmt)) fmt++; + + if (tmpl_from_attr_str(&vpt, fmt, REQUEST_CURRENT, PAIR_LIST_REQUEST, false, false) <= 0) { + RDEBUG("%s", fr_strerror()); + return -1; + } + + RIDEBUG("Attributes matching \"%s\"", fmt); + + RINDENT(); + for (vp = tmpl_cursor_init(NULL, &cursor, request, &vpt); + vp; + vp = tmpl_cursor_next(&cursor, &vpt)) { + FR_NAME_NUMBER const *type; + char *value; + + value = vp_aprints_value(vp, vp, '\''); + if (vp->da->flags.has_tag) { + RIDEBUG2("&%s:%s:%i %s %s", + fr_int2str(pair_lists, vpt.tmpl_list, "<INVALID>"), + vp->da->name, + vp->tag, + fr_int2str(fr_tokens, vp->op, "<INVALID>"), + value); + } else { + RIDEBUG2("&%s:%s %s %s", + fr_int2str(pair_lists, vpt.tmpl_list, "<INVALID>"), + vp->da->name, + fr_int2str(fr_tokens, vp->op, "<INVALID>"), + value); + } + talloc_free(value); + + if (!RDEBUG_ENABLED3) continue; + + if (vp->da->vendor) { + DICT_VENDOR *dv; + + dv = dict_vendorbyvalue(vp->da->vendor); + RIDEBUG2("Vendor : %i (%s)", vp->da->vendor, dv ? dv->name : "unknown"); + } + RIDEBUG2("Type : %s", fr_int2str(dict_attr_types, vp->da->type, "<INVALID>")); + RIDEBUG2("Length : %zu", vp->vp_length); + + if (!RDEBUG_ENABLED4) continue; + + type = dict_attr_types; + while (type->name) { + int pad; + + value_data_t *dst = NULL; + + ssize_t ret; + + if ((PW_TYPE) type->number == vp->da->type) { + goto next_type; + } + + switch (type->number) { + case PW_TYPE_INVALID: /* Not real type */ + case PW_TYPE_MAX: /* Not real type */ + case PW_TYPE_EXTENDED: /* Not safe/appropriate */ + case PW_TYPE_LONG_EXTENDED: /* Not safe/appropriate */ + case PW_TYPE_TLV: /* Not safe/appropriate */ + case PW_TYPE_EVS: /* Not safe/appropriate */ + case PW_TYPE_VSA: /* @fixme We need special behaviour for these */ + case PW_TYPE_COMBO_IP_ADDR: /* Covered by IPv4 address IPv6 address */ + case PW_TYPE_COMBO_IP_PREFIX: /* Covered by IPv4 address IPv6 address */ + case PW_TYPE_TIMEVAL: /* Not a VALUE_PAIR type */ + goto next_type; + + default: + break; + } + + dst = talloc_zero(vp, value_data_t); + ret = value_data_cast(dst, dst, type->number, NULL, vp->da->type, vp->da, + &vp->data, vp->vp_length); + if (ret < 0) goto next_type; /* We expect some to fail */ + + value = value_data_aprints(dst, type->number, NULL, dst, (size_t)ret, '\''); + if (!value) goto next_type; + + if ((pad = (11 - strlen(type->name))) < 0) { + pad = 0; + } + + RINDENT(); + RDEBUG2("as %s%*s: %s", type->name, pad, " ", value); + REXDENT(); + talloc_free(value); + + next_type: + talloc_free(dst); + type++; + } + } + REXDENT(); + + *out = '\0'; + return 0; +} + +/** Processes fmt as a map string and applies it to the current request + * + * e.g. "%{map:&User-Name := 'foo'}" + * + * Allows sets of modifications to be cached and then applied. + * Useful for processing generic attributes from LDAP. + */ +static ssize_t xlat_map(UNUSED void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + vp_map_t *map = NULL; + int ret; + + if (map_afrom_attr_str(request, &map, fmt, + REQUEST_CURRENT, PAIR_LIST_REQUEST, + REQUEST_CURRENT, PAIR_LIST_REQUEST) < 0) { + REDEBUG("Failed parsing \"%s\" as map: %s", fmt, fr_strerror()); + return -1; + } + + RINDENT(); + ret = map_to_request(request, map, map_to_vp, NULL); + REXDENT(); + talloc_free(map); + if (ret < 0) return strlcpy(out, "0", outlen); + + return strlcpy(out, "1", outlen); +} + +/** Prints the current module processing the request + * + */ +static ssize_t xlat_module(UNUSED void *instance, REQUEST *request, + UNUSED char const *fmt, char *out, size_t outlen) +{ + strlcpy(out, request->module, outlen); + + return strlen(out); +} + +#if defined(HAVE_REGEX) && defined(HAVE_PCRE) +static ssize_t xlat_regex(UNUSED void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + char *p; + size_t len; + + if (regex_request_to_sub_named(request, &p, request, fmt) < 0) { + *out = '\0'; + return 0; + } + + len = talloc_array_length(p); + if (len > outlen) { + RDEBUG("Insufficient buffer space to write subcapture value, needed %zu bytes, have %zu bytes", + len, outlen); + return -1; + } + strlcpy(out, p, outlen); + + return len - 1; /* - \0 */ +} +#endif + +#ifdef WITH_UNLANG +/** Implements the Foreach-Variable-X + * + * @see modcall() + */ +static ssize_t xlat_foreach(void *instance, REQUEST *request, + UNUSED char const *fmt, char *out, size_t outlen) +{ + VALUE_PAIR **pvp; + size_t len; + + /* + * See modcall, "FOREACH" for how this works. + */ + pvp = (VALUE_PAIR **) request_data_reference(request, (void *)radius_get_vp, *(int*) instance); + if (!pvp || !*pvp) { + *out = '\0'; + return 0; + } + + len = vp_prints_value(out, outlen, *pvp, 0); + if (is_truncated(len, outlen)) { + RDEBUG("Insufficient buffer space to write foreach value"); + return -1; + } + + return len; +} +#endif + +/** Print data as string, if possible. + * + * If attribute "Foo" is defined as "octets" it will normally + * be printed as 0x0a0a0a. The xlat "%{string:Foo}" will instead + * expand to "\n\n\n" + */ +static ssize_t xlat_string(UNUSED void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + size_t len; + ssize_t ret; + VALUE_PAIR *vp; + uint8_t const *p; + + while (isspace((uint8_t) *fmt)) fmt++; + + if (outlen < 3) { + nothing: + *out = '\0'; + return 0; + } + + if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) goto nothing; + + ret = rad_vp2data(&p, vp); + if (ret < 0) { + return ret; + } + + switch (vp->da->type) { + case PW_TYPE_OCTETS: + len = fr_prints(out, outlen, (char const *) p, vp->vp_length, '"'); + break; + + /* + * Note that "%{string:...}" is NOT binary safe! + * It is explicitly used to get rid of embedded zeros. + */ + case PW_TYPE_STRING: + len = strlcpy(out, vp->vp_strvalue, outlen); + break; + + default: + len = fr_prints(out, outlen, (char const *) p, ret, '\0'); + break; + } + + return len; +} + +/** xlat expand string attribute value + * + */ +static ssize_t xlat_xlat(UNUSED void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + VALUE_PAIR *vp; + + while (isspace((uint8_t) *fmt)) fmt++; + + if (outlen < 3) { + nothing: + *out = '\0'; + return 0; + } + + if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) goto nothing; + + if (vp->da->type != PW_TYPE_STRING) goto nothing; + + return radius_xlat(out, outlen, request, vp->vp_strvalue, NULL, NULL); +} + +/** Dynamically change the debugging level for the current request + * + * Example %{debug:3} + */ +static ssize_t xlat_debug(UNUSED void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + int level = 0; + + /* + * Expand to previous (or current) level + */ + snprintf(out, outlen, "%d", request->log.lvl); + + /* + * Assume we just want to get the current value and NOT set it to 0 + */ + if (!*fmt) + goto done; + + level = atoi(fmt); + if (level == 0) { + request->log.lvl = RAD_REQUEST_LVL_NONE; + request->log.func = NULL; + } else { + if (level > 4) level = 4; + + request->log.lvl = level; + request->log.func = vradlog_request; + } + + done: + return strlen(out); +} + +/* + * Compare two xlat_t structs, based ONLY on the module name. + */ +static int xlat_cmp(void const *one, void const *two) +{ + xlat_t const *a = one; + xlat_t const *b = two; + + if (a->length != b->length) { + return a->length - b->length; + } + + return memcmp(a->name, b->name, a->length); +} + + +/* + * find the appropriate registered xlat function. + */ +static xlat_t *xlat_find(char const *name) +{ + xlat_t my_xlat; + + strlcpy(my_xlat.name, name, sizeof(my_xlat.name)); + my_xlat.length = strlen(my_xlat.name); + + return rbtree_finddata(xlat_root, &my_xlat); +} + + +/** Register an xlat function. + * + * @param[in] name xlat name. + * @param[in] func xlat function to be called. + * @param[in] escape function to sanitize any sub expansions passed to the xlat function. + * @param[in] instance of module that's registering the xlat function. + * @return 0 on success, -1 on failure + */ +int xlat_register(char const *name, xlat_func_t func, xlat_escape_t escape, void *instance) +{ + xlat_t *c; + xlat_t my_xlat; + rbnode_t *node; + + if (!name || !*name) { + DEBUG("xlat_register: Invalid xlat name"); + return -1; + } + + /* + * First time around, build up the tree... + * + * FIXME: This code should be hoisted out of this function, + * and into a global "initialization". But it isn't critical... + */ + if (!xlat_root) { +#ifdef WITH_UNLANG + int i; +#endif + + xlat_root = rbtree_create(NULL, xlat_cmp, NULL, RBTREE_FLAG_REPLACE); + if (!xlat_root) { + DEBUG("xlat_register: Failed to create tree"); + return -1; + } + +#ifdef WITH_UNLANG + for (i = 0; xlat_foreach_names[i] != NULL; i++) { + xlat_register(xlat_foreach_names[i], + xlat_foreach, NULL, &xlat_inst[i]); + c = xlat_find(xlat_foreach_names[i]); + rad_assert(c != NULL); + c->internal = true; + } +#endif + +#define XLAT_REGISTER(_x) xlat_register(STRINGIFY(_x), xlat_ ## _x, NULL, NULL); \ + c = xlat_find(STRINGIFY(_x)); \ + rad_assert(c != NULL); \ + c->internal = true + + XLAT_REGISTER(concat); + XLAT_REGISTER(integer); + XLAT_REGISTER(strlen); + XLAT_REGISTER(length); + XLAT_REGISTER(hex); + XLAT_REGISTER(tag); + XLAT_REGISTER(vendor); + XLAT_REGISTER(vendor_num); + XLAT_REGISTER(attr); + XLAT_REGISTER(attr_num); + XLAT_REGISTER(string); + XLAT_REGISTER(xlat); + XLAT_REGISTER(map); + XLAT_REGISTER(module); + XLAT_REGISTER(debug_attr); +#if defined(HAVE_REGEX) && defined(HAVE_PCRE) + XLAT_REGISTER(regex); +#endif + + xlat_register("debug", xlat_debug, NULL, &xlat_inst[0]); + c = xlat_find("debug"); + rad_assert(c != NULL); + c->internal = true; + } + + /* + * If it already exists, replace the instance. + */ + strlcpy(my_xlat.name, name, sizeof(my_xlat.name)); + my_xlat.length = strlen(my_xlat.name); + c = rbtree_finddata(xlat_root, &my_xlat); + if (c) { + if (c->internal) { + DEBUG("xlat_register: Cannot re-define internal xlat"); + return -1; + } + + c->func = func; + c->escape = escape; + c->instance = instance; + return 0; + } + + /* + * Doesn't exist. Create it. + */ + c = talloc_zero(xlat_root, xlat_t); + + c->func = func; + c->escape = escape; + strlcpy(c->name, name, sizeof(c->name)); + c->length = strlen(c->name); + c->instance = instance; + + node = rbtree_insert_node(xlat_root, c); + if (!node) { + talloc_free(c); + return -1; + } + + /* + * Ensure that the data is deleted when the node is + * deleted. + * + * @todo: Maybe this should be the other way around... + * when a thing IN the tree is deleted, it's automatically + * removed from the tree. But for now, this works. + */ + (void) talloc_steal(node, c); + return 0; +} + +/** Unregister an xlat function + * + * We can only have one function to call per name, so the passing of "func" + * here is extraneous. + * + * @param[in] name xlat to unregister. + * @param[in] func unused. + * @param[in] instance data. + */ +void xlat_unregister(char const *name, UNUSED xlat_func_t func, void *instance) +{ + xlat_t *c; + xlat_t my_xlat; + + if (!name || !xlat_root) return; + + strlcpy(my_xlat.name, name, sizeof(my_xlat.name)); + my_xlat.length = strlen(my_xlat.name); + + c = rbtree_finddata(xlat_root, &my_xlat); + if (!c) return; + + if (c->instance != instance) return; + + rbtree_deletebydata(xlat_root, c); +} + +static int xlat_unregister_callback(void *instance, void *data) +{ + xlat_t *c = (xlat_t *) data; + + if (c->instance != instance) return 0; /* keep walking */ + + return 2; /* delete it */ +} + +void xlat_unregister_module(void *instance) +{ + rbtree_walk(xlat_root, RBTREE_DELETE_ORDER, xlat_unregister_callback, instance); +} + +/* + * Internal redundant handler for xlats + */ +typedef enum xlat_redundant_type_t { + XLAT_INVALID = 0, + XLAT_REDUNDANT, + XLAT_LOAD_BALANCE, + XLAT_REDUNDANT_LOAD_BALANCE, +} xlat_redundant_type_t; + +typedef struct xlat_redundant_t { + xlat_redundant_type_t type; + uint32_t count; + CONF_SECTION *cs; +} xlat_redundant_t; + + +static ssize_t xlat_redundant(void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + xlat_redundant_t *xr = instance; + CONF_ITEM *ci; + char const *name; + xlat_t *xlat; + + rad_assert(xr->type == XLAT_REDUNDANT); + + /* + * Pick the first xlat which succeeds + */ + for (ci = cf_item_find_next(xr->cs, NULL); + ci != NULL; + ci = cf_item_find_next(xr->cs, ci)) { + ssize_t rcode; + + if (!cf_item_is_pair(ci)) continue; + + name = cf_pair_attr(cf_item_to_pair(ci)); + rad_assert(name != NULL); + + xlat = xlat_find(name); + if (!xlat) continue; + + rcode = xlat->func(xlat->instance, request, fmt, out, outlen); + if (rcode <= 0) continue; + return rcode; + } + + /* + * Everything failed. Oh well. + */ + *out = 0; + return 0; +} + + +static ssize_t xlat_load_balance(void *instance, REQUEST *request, + char const *fmt, char *out, size_t outlen) +{ + uint32_t count = 0; + xlat_redundant_t *xr = instance; + CONF_ITEM *ci; + CONF_ITEM *found = NULL; + char const *name; + xlat_t *xlat; + + /* + * Choose a child at random. + */ + for (ci = cf_item_find_next(xr->cs, NULL); + ci != NULL; + ci = cf_item_find_next(xr->cs, ci)) { + if (!cf_item_is_pair(ci)) continue; + count++; + + /* + * Replace the previously found one with a random + * new one. + */ + if ((count * (fr_rand() & 0xffff)) < (uint32_t) 0x10000) { + found = ci; + } + } + + /* + * Plain load balancing: do one child, and only one child. + */ + if (xr->type == XLAT_LOAD_BALANCE) { + name = cf_pair_attr(cf_item_to_pair(found)); + rad_assert(name != NULL); + + xlat = xlat_find(name); + if (!xlat) return -1; + + return xlat->func(xlat->instance, request, fmt, out, outlen); + } + + rad_assert(xr->type == XLAT_REDUNDANT_LOAD_BALANCE); + + /* + * Try the random one we found. If it fails, keep going + * through the rest of the children. + */ + ci = found; + do { + name = cf_pair_attr(cf_item_to_pair(ci)); + rad_assert(name != NULL); + + xlat = xlat_find(name); + if (xlat) { + ssize_t rcode; + + rcode = xlat->func(xlat->instance, request, fmt, out, outlen); + if (rcode > 0) return rcode; + } + + /* + * Go to the next one, wrapping around at the end. + */ + ci = cf_item_find_next(xr->cs, ci); + if (!ci) ci = cf_item_find_next(xr->cs, NULL); + } while (ci != found); + + return -1; +} + + +bool xlat_register_redundant(CONF_SECTION *cs) +{ + char const *name1, *name2; + xlat_redundant_t *xr; + + name1 = cf_section_name1(cs); + name2 = cf_section_name2(cs); + + if (!name2) return false; + + if (xlat_find(name2)) { + cf_log_err_cs(cs, "An expansion is already registered for this name"); + return false; + } + + xr = talloc_zero(cs, xlat_redundant_t); + if (!xr) return false; + + if (strcmp(name1, "redundant") == 0) { + xr->type = XLAT_REDUNDANT; + + } else if (strcmp(name1, "redundant-load-balance") == 0) { + xr->type = XLAT_REDUNDANT_LOAD_BALANCE; + + } else if (strcmp(name1, "load-balance") == 0) { + xr->type = XLAT_LOAD_BALANCE; + + } else { + return false; + } + + xr->cs = cs; + + /* + * Get the number of children for load balancing. + */ + if (xr->type == XLAT_REDUNDANT) { + if (xlat_register(name2, xlat_redundant, NULL, xr) < 0) { + talloc_free(xr); + return false; + } + + } else { + CONF_ITEM *ci; + + for (ci = cf_item_find_next(cs, NULL); + ci != NULL; + ci = cf_item_find_next(cs, ci)) { + if (!cf_item_is_pair(ci)) continue; + + if (!xlat_find(cf_pair_attr(cf_item_to_pair(ci)))) { + talloc_free(xr); + return false; + } + + xr->count++; + } + + if (xlat_register(name2, xlat_load_balance, NULL, xr) < 0) { + talloc_free(xr); + return false; + } + } + + return true; +} + + +/** Crappy temporary function to add attribute ref support to xlats + * + * This needs to die, and hopefully will die, when xlat functions accept + * xlat node structures. + * + * Provides either a pointer to a buffer which contains the value of the reference VALUE_PAIR + * in an architecture independent format. Or a pointer to the start of the fmt string. + * + * The pointer is only guaranteed to be valid between calls to xlat_fmt_to_ref, + * and so long as the source VALUE_PAIR is not freed. + * + * @param out where to write a pointer to the buffer to the data the xlat function needs to work on. + * @param request current request. + * @param fmt string. + * @returns the length of the data or -1 on error. + */ +ssize_t xlat_fmt_to_ref(uint8_t const **out, REQUEST *request, char const *fmt) +{ + VALUE_PAIR *vp; + + while (isspace((uint8_t) *fmt)) fmt++; + + if (fmt[0] == '&') { + if ((radius_get_vp(&vp, request, fmt) < 0) || !vp) { + *out = NULL; + return -1; + } + + return rad_vp2data(out, vp); + } + + *out = (uint8_t const *)fmt; + return strlen(fmt); +} + +/** De-register all xlat functions, used mainly for debugging. + * + */ +void xlat_free(void) +{ + rbtree_free(xlat_root); +} + +#ifdef DEBUG_XLAT +# define XLAT_DEBUG DEBUG3 +#else +# define XLAT_DEBUG(...) +#endif + +static ssize_t xlat_tokenize_expansion(TALLOC_CTX *ctx, char *fmt, xlat_exp_t **head, + char const **error); +static ssize_t xlat_tokenize_literal(TALLOC_CTX *ctx, char *fmt, xlat_exp_t **head, + bool brace, char const **error); +static size_t xlat_process(char **out, REQUEST *request, xlat_exp_t const * const head, + xlat_escape_t escape, void *escape_ctx); + +static ssize_t xlat_tokenize_alternation(TALLOC_CTX *ctx, char *fmt, xlat_exp_t **head, + char const **error) +{ + ssize_t slen; + char *p; + xlat_exp_t *node; + + rad_assert(fmt[0] == '%'); + rad_assert(fmt[1] == '{'); + rad_assert(fmt[2] == '%'); + rad_assert(fmt[3] == '{'); + + XLAT_DEBUG("ALTERNATE <-- %s", fmt); + + node = talloc_zero(ctx, xlat_exp_t); + node->type = XLAT_ALTERNATE; + + p = fmt + 2; + slen = xlat_tokenize_expansion(node, p, &node->child, error); + if (slen <= 0) { + talloc_free(node); + return slen - (p - fmt); + } + p += slen; + + if (p[0] != ':') { + talloc_free(node); + *error = "Expected ':' after first expansion"; + return -(p - fmt); + } + p++; + + if (p[0] != '-') { + talloc_free(node); + *error = "Expected '-' after ':'"; + return -(p - fmt); + } + p++; + + /* + * Allow the RHS to be empty as a special case. + */ + if (*p == '}') { + /* + * Hack up an empty string. + */ + node->alternate = talloc_zero(node, xlat_exp_t); + node->alternate->type = XLAT_LITERAL; + node->alternate->fmt = talloc_typed_strdup(node->alternate, ""); + *(p++) = '\0'; + + } else { + slen = xlat_tokenize_literal(node, p, &node->alternate, true, error); + if (slen <= 0) { + talloc_free(node); + return slen - (p - fmt); + } + + if (!node->alternate) { + talloc_free(node); + *error = "Empty expansion is invalid"; + return -(p - fmt); + } + p += slen; + } + + *head = node; + return p - fmt; +} + +static ssize_t xlat_tokenize_expansion(TALLOC_CTX *ctx, char *fmt, xlat_exp_t **head, + char const **error) +{ + ssize_t slen; + char *p, *q; + xlat_exp_t *node; + long num; + + rad_assert(fmt[0] == '%'); + rad_assert(fmt[1] == '{'); + + /* + * %{%{...}:-bar} + */ + if ((fmt[2] == '%') && (fmt[3] == '{')) return xlat_tokenize_alternation(ctx, fmt, head, error); + + XLAT_DEBUG("EXPANSION <-- %s", fmt); + node = talloc_zero(ctx, xlat_exp_t); + node->fmt = fmt + 2; + node->len = 0; + +#ifdef HAVE_REGEX + /* + * Handle regex's specially. + */ + p = fmt + 2; + num = strtol(p, &q, 10); + if (p != q && (*q == '}')) { + XLAT_DEBUG("REGEX <-- %s", fmt); + *q = '\0'; + + if ((num > REQUEST_MAX_REGEX) || (num < 0)) { + talloc_free(node); + *error = "Invalid regex reference. Must be in range 0-" STRINGIFY(REQUEST_MAX_REGEX); + return -2; + } + node->attr.tmpl_num = num; + + node->type = XLAT_REGEX; + *head = node; + + return (q - fmt) + 1; + } +#endif /* HAVE_REGEX */ + + /* + * %{Attr-Name} + * %{Attr-Name[#]} + * %{Tunnel-Password:1} + * %{Tunnel-Password:1[#]} + * %{request:Attr-Name} + * %{request:Tunnel-Password:1} + * %{request:Tunnel-Password:1[#]} + * %{mod:foo} + */ + + /* + * This is for efficiency, so we don't search for an xlat, + * when what's being referenced is obviously an attribute. + */ + p = fmt + 2; + for (q = p; *q != '\0'; q++) { + if (*q == ':') break; + + if (isspace((uint8_t) *q)) break; + + if (*q == '[') continue; + + if (*q == '}') break; + } + + /* + * Check for empty expressions %{} + */ + if ((*q == '}') && (q == p)) { + talloc_free(node); + *error = "Empty expression is invalid"; + return -(p - fmt); + } + + /* + * Might be a module name reference. + * + * If it's not, it's an attribute or parse error. + */ + if (*q == ':') { + *q = '\0'; + node->xlat = xlat_find(node->fmt); + if (node->xlat) { + /* + * %{mod:foo} + */ + node->type = XLAT_MODULE; + + p = q + 1; + XLAT_DEBUG("MOD <-- %s ... %s", node->fmt, p); + + slen = xlat_tokenize_literal(node, p, &node->child, true, error); + if (slen < 0) { + talloc_free(node); + return slen - (p - fmt); + } + p += slen; + + *head = node; + rad_assert(node->next == NULL); + + return p - fmt; + } + *q = ':'; /* Avoids a strdup */ + } + + /* + * The first token ends with: + * - '[' - Which is an attribute index, so it must be an attribute. + * - '}' - The end of the expansion, which means it was a bareword. + */ + slen = tmpl_from_attr_substr(&node->attr, p, REQUEST_CURRENT, PAIR_LIST_REQUEST, true, true); + if (slen <= 0) { + /* + * If the parse error occurred before the ':' + * then the error is changed to 'Unknown module', + * as it was more likely to be a bad module name, + * than a request qualifier. + */ + if ((*q == ':') && ((p + (slen * -1)) < q)) { + *error = "Unknown module"; + } else { + *error = fr_strerror(); + } + + talloc_free(node); + return slen - (p - fmt); + } + + /* + * Might be a virtual XLAT attribute + */ + if (node->attr.type == TMPL_TYPE_ATTR_UNDEFINED) { + node->xlat = xlat_find(node->attr.tmpl_unknown_name); + if (node->xlat && node->xlat->instance && !node->xlat->internal) { + talloc_free(node); + *error = "Missing content in expansion"; + return -(p - fmt) - slen; + } + + if (node->xlat) { + node->type = XLAT_VIRTUAL; + node->fmt = node->attr.tmpl_unknown_name; + + XLAT_DEBUG("VIRTUAL <-- %s", node->fmt); + *head = node; + rad_assert(node->next == NULL); + q++; + return q - fmt; + } + + talloc_free(node); + *error = "Unknown attribute"; + return -(p - fmt); + } + + /* + * Might be a list, too... + */ + node->type = XLAT_ATTRIBUTE; + p += slen; + + if (*p != '}') { + talloc_free(node); + *error = "No matching closing brace"; + return -1; /* second character of format string */ + } + *p++ = '\0'; + *head = node; + rad_assert(node->next == NULL); + + return p - fmt; +} + + +static ssize_t xlat_tokenize_literal(TALLOC_CTX *ctx, char *fmt, xlat_exp_t **head, + bool brace, char const **error) +{ + char *p; + xlat_exp_t *node; + + if (!*fmt) return 0; + + XLAT_DEBUG("LITERAL <-- %s", fmt); + + node = talloc_zero(ctx, xlat_exp_t); + node->fmt = fmt; + node->len = 0; + node->type = XLAT_LITERAL; + + p = fmt; + + while (*p) { + if (*p == '\\') { + if (!p[1]) { + talloc_free(node); + *error = "Invalid escape at end of string"; + return -(p - fmt); + } + + p += 2; + node->len += 2; + continue; + } + + /* + * Process the expansion. + */ + if ((p[0] == '%') && (p[1] == '{')) { + ssize_t slen; + + XLAT_DEBUG("EXPANSION-2 <-- %s", node->fmt); + + slen = xlat_tokenize_expansion(node, p, &node->next, error); + if (slen <= 0) { + talloc_free(node); + return slen - (p - fmt); + } + *p = '\0'; /* end the literal */ + p += slen; + + rad_assert(node->next != NULL); + + /* + * Short-circuit the recursive call. + * This saves another function call and + * memory allocation. + */ + if (!*p) break; + + /* + * "foo %{User-Name} bar" + * LITERAL "foo " + * EXPANSION User-Name + * LITERAL " bar" + */ + slen = xlat_tokenize_literal(node->next, p, &(node->next->next), brace, error); + rad_assert(slen != 0); + if (slen < 0) { + talloc_free(node); + return slen - (p - fmt); + } + + brace = false; /* it was found above, or else the above code errored out */ + p += slen; + break; /* stop processing the string */ + } + + /* + * Check for valid single-character expansions. + */ + if (p[0] == '%') { + ssize_t slen; + xlat_exp_t *next; + + if (!p[1] || !strchr("%}cdelmntCDGHIMSTYv", p[1])) { + talloc_free(node); + *error = "Invalid variable expansion"; + p++; + return - (p - fmt); + } + + next = talloc_zero(node, xlat_exp_t); + next->len = 1; + + switch (p[1]) { + case '%': + case '}': + next->fmt = talloc_strndup(next, p + 1, 1); + + XLAT_DEBUG("LITERAL-ESCAPED <-- %s", next->fmt); + next->type = XLAT_LITERAL; + break; + + default: + next->fmt = p + 1; + + XLAT_DEBUG("PERCENT <-- %c", *next->fmt); + next->type = XLAT_PERCENT; + break; + } + + node->next = next; + *p = '\0'; + p += 2; + + if (!*p) break; + + /* + * And recurse. + */ + slen = xlat_tokenize_literal(node->next, p, &(node->next->next), brace, error); + rad_assert(slen != 0); + if (slen < 0) { + talloc_free(node); + return slen - (p - fmt); + } + + brace = false; /* it was found above, or else the above code errored out */ + p += slen; + break; /* stop processing the string */ + } + + /* + * If required, eat the brace. + */ + if (brace && (*p == '}')) { + brace = false; + *p = '\0'; + p++; + break; + } + + p++; + node->len++; + } + + /* + * We were told to look for a brace, but we ran off of + * the end of the string before we found one. + */ + if (brace) { + *error = "Missing closing brace at end of string"; + return -(p - fmt); + } + + /* + * Squash zero-width literals + */ + if (node->len > 0) { + *head = node; + + } else { + (void) talloc_steal(ctx, node->next); + *head = node->next; + talloc_free(node); + } + + return p - fmt; +} + + +static char const xlat_tabs[] = " "; + +static void xlat_tokenize_debug(xlat_exp_t const *node, int lvl) +{ + rad_assert(node != NULL); + + if (lvl >= (int) sizeof(xlat_tabs)) lvl = sizeof(xlat_tabs); + + while (node) { + switch (node->type) { + case XLAT_LITERAL: + DEBUG("%.*sliteral --> %s", lvl, xlat_tabs, node->fmt); + break; + + case XLAT_PERCENT: + DEBUG("%.*spercent --> %c", lvl, xlat_tabs, node->fmt[0]); + break; + + case XLAT_ATTRIBUTE: + rad_assert(node->attr.tmpl_da != NULL); + DEBUG("%.*sattribute --> %s", lvl, xlat_tabs, node->attr.tmpl_da->name); + rad_assert(node->child == NULL); + if ((node->attr.tmpl_tag != TAG_ANY) || (node->attr.tmpl_num != NUM_ANY)) { + DEBUG("%.*s{", lvl, xlat_tabs); + + DEBUG("%.*sref %d", lvl + 1, xlat_tabs, node->attr.tmpl_request); + DEBUG("%.*slist %d", lvl + 1, xlat_tabs, node->attr.tmpl_list); + + if (node->attr.tmpl_tag != TAG_ANY) { + DEBUG("%.*stag %d", lvl + 1, xlat_tabs, node->attr.tmpl_tag); + } + if (node->attr.tmpl_num != NUM_ANY) { + if (node->attr.tmpl_num == NUM_COUNT) { + DEBUG("%.*s[#]", lvl + 1, xlat_tabs); + } else if (node->attr.tmpl_num == NUM_ALL) { + DEBUG("%.*s[*]", lvl + 1, xlat_tabs); + } else { + DEBUG("%.*s[%d]", lvl + 1, xlat_tabs, node->attr.tmpl_num); + } + } + + DEBUG("%.*s}", lvl, xlat_tabs); + } + break; + + case XLAT_VIRTUAL: + rad_assert(node->fmt != NULL); + DEBUG("%.*svirtual --> %s", lvl, xlat_tabs, node->fmt); + break; + + case XLAT_MODULE: + rad_assert(node->xlat != NULL); + DEBUG("%.*sxlat --> %s", lvl, xlat_tabs, node->xlat->name); + if (node->child) { + DEBUG("%.*s{", lvl, xlat_tabs); + xlat_tokenize_debug(node->child, lvl + 1); + DEBUG("%.*s}", lvl, xlat_tabs); + } + break; + +#ifdef HAVE_REGEX + case XLAT_REGEX: + DEBUG("%.*sregex-var --> %d", lvl, xlat_tabs, node->attr.tmpl_num); + break; +#endif + + case XLAT_ALTERNATE: + DEBUG("%.*sXLAT-IF {", lvl, xlat_tabs); + xlat_tokenize_debug(node->child, lvl + 1); + DEBUG("%.*s}", lvl, xlat_tabs); + DEBUG("%.*sXLAT-ELSE {", lvl, xlat_tabs); + xlat_tokenize_debug(node->alternate, lvl + 1); + DEBUG("%.*s}", lvl, xlat_tabs); + break; + } + node = node->next; + } +} + +size_t xlat_sprint(char *buffer, size_t bufsize, xlat_exp_t const *node) +{ + size_t len; + char *p, *end; + + if (!node) { + *buffer = '\0'; + return 0; + } + + p = buffer; + end = buffer + bufsize; + + while (node) { + switch (node->type) { + case XLAT_LITERAL: + strlcpy(p, node->fmt, end - p); + p += strlen(p); + break; + + case XLAT_PERCENT: + p[0] = '%'; + p[1] = node->fmt[0]; + p += 2; + break; + + case XLAT_ATTRIBUTE: + *(p++) = '%'; + *(p++) = '{'; + + /* + * The node MAY NOT be an attribute. It + * may be a list. + */ + tmpl_prints(p, end - p, &node->attr, NULL); + if (*p == '&') { + memmove(p, p + 1, strlen(p + 1) + 1); + } + p += strlen(p); + *(p++) = '}'; + break; +#ifdef HAVE_REGEX + case XLAT_REGEX: + snprintf(p, end - p, "%%{%i}", node->attr.tmpl_num); + p += strlen(p); + break; +#endif + case XLAT_VIRTUAL: + *(p++) = '%'; + *(p++) = '{'; + strlcpy(p, node->fmt, end - p); + p += strlen(p); + *(p++) = '}'; + break; + + case XLAT_MODULE: + *(p++) = '%'; + *(p++) = '{'; + strlcpy(p, node->xlat->name, end - p); + p += strlen(p); + *(p++) = ':'; + rad_assert(node->child != NULL); + len = xlat_sprint(p, end - p, node->child); + p += len; + *(p++) = '}'; + break; + + case XLAT_ALTERNATE: + *(p++) = '%'; + *(p++) = '{'; + + len = xlat_sprint(p, end - p, node->child); + p += len; + + *(p++) = ':'; + *(p++) = '-'; + + len = xlat_sprint(p, end - p, node->alternate); + p += len; + + *(p++) = '}'; + break; + } + + + if (p == end) break; + + node = node->next; + } + + *p = '\0'; + + return p - buffer; +} + +ssize_t xlat_tokenize(TALLOC_CTX *ctx, char *fmt, xlat_exp_t **head, + char const **error) +{ + return xlat_tokenize_literal(ctx, fmt, head, false, error); +} + + +/** Tokenize an xlat expansion + * + * @param[in] request the input request. Memory will be attached here. + * @param[in] fmt the format string to expand + * @param[out] head the head of the xlat list / tree structure. + */ +static ssize_t xlat_tokenize_request(REQUEST *request, char const *fmt, xlat_exp_t **head) +{ + ssize_t slen; + char *tokens; + char const *error = NULL; + + *head = NULL; + + /* + * Copy the original format string to a buffer so that + * the later functions can mangle it in-place, which is + * much faster. + */ + tokens = talloc_typed_strdup(request, fmt); + if (!tokens) { + error = "Out of memory"; + return -1; + } + + slen = xlat_tokenize_literal(request, tokens, head, false, &error); + + /* + * Zero length expansion, return a zero length node. + */ + if (slen == 0) { + *head = talloc_zero(request, xlat_exp_t); + } + + /* + * Output something like: + * + * "format string" + * " ^ error was here" + */ + if (slen < 0) { + talloc_free(tokens); + + if (!error) error = "Unknown error"; + + REMARKER(fmt, -slen, error); + return slen; + } + + if (*head && (rad_debug_lvl > 2)) { + DEBUG("%s", fmt); + DEBUG("Parsed xlat tree:"); + xlat_tokenize_debug(*head, 0); + } + + /* + * All of the nodes point to offsets in the "tokens" + * string. Let's ensure that free'ing head will free + * "tokens", too. + */ + (void) talloc_steal(*head, tokens); + + return slen; +} + + +static char *xlat_getvp(TALLOC_CTX *ctx, REQUEST *request, vp_tmpl_t const *vpt, + bool escape, bool return_null, char const *concat) +{ + VALUE_PAIR *vp = NULL, *virtual = NULL; + RADIUS_PACKET *packet = NULL; + DICT_VALUE *dv; + char *ret = NULL; + + vp_cursor_t cursor; + char quote = escape ? '"' : '\0'; + + rad_assert((vpt->type == TMPL_TYPE_ATTR) || (vpt->type == TMPL_TYPE_LIST)); + + /* + * We only support count and concatenate operations on lists. + */ + if (vpt->type == TMPL_TYPE_LIST) { + vp = tmpl_cursor_init(NULL, &cursor, request, vpt); + goto do_print; + } + + /* + * See if we're dealing with an attribute in the request + * + * This allows users to manipulate virtual attributes as if + * they were real ones. + */ + vp = tmpl_cursor_init(NULL, &cursor, request, vpt); + if (vp) goto do_print; + + /* + * We didn't find the VP in a list. + * If it's not a virtual one, and we're not meant to + * be counting it, return. + */ + if (!vpt->tmpl_da->flags.virtual) { + if (vpt->tmpl_num == NUM_COUNT) goto do_print; + return NULL; + } + + /* + * Switch out the request to the one specified by the template + */ + if (radius_request(&request, vpt->tmpl_request) < 0) return NULL; + + /* + * Some non-packet expansions + */ + switch (vpt->tmpl_da->attr) { + default: + break; /* ignore them */ + + case PW_CLIENT_SHORTNAME: + if (vpt->tmpl_num == NUM_COUNT) goto count_virtual; + if (request->client && request->client->shortname) { + return talloc_typed_strdup(ctx, request->client->shortname); + } + return talloc_typed_strdup(ctx, "<UNKNOWN-CLIENT>"); + + case PW_REQUEST_PROCESSING_STAGE: + if (vpt->tmpl_num == NUM_COUNT) goto count_virtual; + if (request->component) { + return talloc_typed_strdup(ctx, request->component); + } + return talloc_typed_strdup(ctx, "server_core"); + + case PW_VIRTUAL_SERVER: + if (vpt->tmpl_num == NUM_COUNT) goto count_virtual; + if (!request->server) return NULL; + return talloc_typed_strdup(ctx, request->server); + + case PW_MODULE_RETURN_CODE: + if (vpt->tmpl_num == NUM_COUNT) goto count_virtual; + if (!request->rcode) return NULL; + return talloc_typed_strdup(ctx, fr_int2str(modreturn_table, request->rcode, "")); + } + + /* + * All of the attributes must now refer to a packet. + * If there's no packet, we can't print any attribute + * referencing it. + */ + packet = radius_packet(request, vpt->tmpl_list); + if (!packet) { + if (return_null) return NULL; + return vp_aprints_type(ctx, vpt->tmpl_da->type); + } + + vp = NULL; + switch (vpt->tmpl_da->attr) { + default: + break; + + case PW_PACKET_TYPE: + dv = dict_valbyattr(PW_PACKET_TYPE, 0, packet->code); + if (dv) return talloc_typed_strdup(ctx, dv->name); + return talloc_typed_asprintf(ctx, "%d", packet->code); + + case PW_RESPONSE_PACKET_TYPE: + { + int code = 0; + +#ifdef WITH_PROXY + if (request->proxy_reply && (!request->reply || !request->reply->code)) { + code = request->proxy_reply->code; + } else +#endif + if (request->reply) { + code = request->reply->code; + } + + if (!code) return NULL; + + if (code >= FR_MAX_PACKET_CODE) { + return talloc_typed_asprintf(ctx, "%d", packet->code); + } + + return talloc_typed_strdup(ctx, fr_packet_codes[code]); + } + + /* + * Virtual attributes which require a temporary VALUE_PAIR + * to be allocated. We can't use stack allocated memory + * because of the talloc checks sprinkled throughout the + * various VP functions. + */ + case PW_PACKET_AUTHENTICATION_VECTOR: + virtual = fr_pair_afrom_da(ctx, vpt->tmpl_da); + fr_pair_value_memcpy(virtual, packet->vector, sizeof(packet->vector)); + vp = virtual; + break; + + case PW_CLIENT_IP_ADDRESS: + case PW_PACKET_SRC_IP_ADDRESS: + if (packet->src_ipaddr.af == AF_INET) { + virtual = fr_pair_afrom_da(ctx, vpt->tmpl_da); + virtual->vp_ipaddr = packet->src_ipaddr.ipaddr.ip4addr.s_addr; + vp = virtual; + } + break; + + case PW_PACKET_DST_IP_ADDRESS: + if (packet->dst_ipaddr.af == AF_INET) { + virtual = fr_pair_afrom_da(ctx, vpt->tmpl_da); + virtual->vp_ipaddr = packet->dst_ipaddr.ipaddr.ip4addr.s_addr; + vp = virtual; + } + break; + + case PW_PACKET_SRC_IPV6_ADDRESS: + if (packet->src_ipaddr.af == AF_INET6) { + virtual = fr_pair_afrom_da(ctx, vpt->tmpl_da); + memcpy(&virtual->vp_ipv6addr, + &packet->src_ipaddr.ipaddr.ip6addr, + sizeof(packet->src_ipaddr.ipaddr.ip6addr)); + vp = virtual; + } + break; + + case PW_PACKET_DST_IPV6_ADDRESS: + if (packet->dst_ipaddr.af == AF_INET6) { + virtual = fr_pair_afrom_da(ctx, vpt->tmpl_da); + memcpy(&virtual->vp_ipv6addr, + &packet->dst_ipaddr.ipaddr.ip6addr, + sizeof(packet->dst_ipaddr.ipaddr.ip6addr)); + vp = virtual; + } + break; + + case PW_PACKET_SRC_PORT: + virtual = fr_pair_afrom_da(ctx, vpt->tmpl_da); + virtual->vp_integer = packet->src_port; + vp = virtual; + break; + + case PW_PACKET_DST_PORT: + virtual = fr_pair_afrom_da(ctx, vpt->tmpl_da); + virtual->vp_integer = packet->dst_port; + vp = virtual; + break; + } + + /* + * Fake various operations for virtual attributes. + */ + if (virtual) { + if (vpt->tmpl_num != NUM_ANY) switch (vpt->tmpl_num) { + /* + * [n] is NULL (we only have [0]) + */ + default: + goto finish; + /* + * [*] means only one. + */ + case NUM_ALL: + break; + + /* + * [#] means 1 (as there's only one) + */ + case NUM_COUNT: + count_virtual: + ret = talloc_strdup(ctx, "1"); + goto finish; + + /* + * [0] is fine (get the first instance) + */ + case 0: + break; + } + goto print; + } + +do_print: + switch (vpt->tmpl_num) { + /* + * Return a count of the VPs. + */ + case NUM_COUNT: + { + int count = 0; + + for (vp = tmpl_cursor_init(NULL, &cursor, request, vpt); + vp; + vp = tmpl_cursor_next(&cursor, vpt)) count++; + + return talloc_typed_asprintf(ctx, "%d", count); + } + + + /* + * Concatenate all values together, + * separated by commas. + */ + case NUM_ALL: + { + char *p, *q; + + if (!fr_cursor_current(&cursor)) return NULL; + p = vp_aprints_value(ctx, vp, quote); + if (!p) return NULL; + + while ((vp = tmpl_cursor_next(&cursor, vpt)) != NULL) { + q = vp_aprints_value(ctx, vp, quote); + if (!q) return NULL; + p = talloc_strdup_append(p, concat); + p = talloc_strdup_append(p, q); + } + + return p; + } + + default: + /* + * The cursor was set to the correct + * position above by tmpl_cursor_init. + */ + vp = fr_cursor_current(&cursor); + break; + } + + if (!vp) { + if (return_null) return NULL; + return vp_aprints_type(ctx, vpt->tmpl_da->type); + } + +print: + ret = vp_aprints_value(ctx, vp, quote); + +finish: + talloc_free(virtual); + return ret; +} + +#ifdef DEBUG_XLAT +static const char xlat_spaces[] = " "; +#endif + +static char *xlat_aprint(TALLOC_CTX *ctx, REQUEST *request, xlat_exp_t const * const node, + xlat_escape_t escape, void *escape_ctx, +#ifndef DEBUG_XLAT + UNUSED +#endif + int lvl) +{ + ssize_t rcode; + char *str = NULL, *child; + char const *p; + + XLAT_DEBUG("%.*sxlat aprint %d %s", lvl, xlat_spaces, node->type, node->fmt); + + switch (node->type) { + /* + * Don't escape this. + */ + case XLAT_LITERAL: + XLAT_DEBUG("%.*sxlat_aprint LITERAL", lvl, xlat_spaces); + return talloc_typed_strdup(ctx, node->fmt); + + /* + * Do a one-character expansion. + */ + case XLAT_PERCENT: + { + char *nl; + size_t freespace = 256; + struct tm ts; + time_t when; + int usec; + + XLAT_DEBUG("%.*sxlat_aprint PERCENT", lvl, xlat_spaces); + + str = talloc_array(ctx, char, freespace); /* @todo do better allocation */ + p = node->fmt; + + when = request->timestamp; + usec = 0; + if (request->packet) { + when = request->packet->timestamp.tv_sec; + usec = request->packet->timestamp.tv_usec; + } + + switch (*p) { + case '%': + str[0] = '%'; + str[1] = '\0'; + break; + + case 'c': /* current epoch time seconds */ + snprintf(str, freespace, "%" PRIu64, (uint64_t) time(NULL)); + break; + + case 'd': /* request day */ + if (!localtime_r(&when, &ts)) goto error; + strftime(str, freespace, "%d", &ts); + break; + + case 'e': /* request second */ + if (!localtime_r(&when, &ts)) goto error; + + snprintf(str, freespace, "%d", ts.tm_sec); + break; + + case 'l': /* request timestamp */ + snprintf(str, freespace, "%lu", + (unsigned long) when); + break; + + case 'm': /* request month */ + if (!localtime_r(&when, &ts)) goto error; + strftime(str, freespace, "%m", &ts); + break; + + case 'n': /* Request Number*/ + snprintf(str, freespace, "%u", request->number); + break; + + case 't': /* request timestamp */ + CTIME_R(&when, str, freespace); + nl = strchr(str, '\n'); + if (nl) *nl = '\0'; + break; + + case 'C': /* current epoch time microseconds */ + { + struct timeval tv; + + gettimeofday(&tv, NULL); + + snprintf(str, freespace, "%" PRIu64, (uint64_t) tv.tv_usec); + } + break; + + case 'D': /* request date */ + if (!localtime_r(&when, &ts)) goto error; + strftime(str, freespace, "%Y%m%d", &ts); + break; + + case 'G': /* request minute */ + if (!localtime_r(&when, &ts)) goto error; + strftime(str, freespace, "%M", &ts); + break; + + case 'H': /* request hour */ + if (!localtime_r(&when, &ts)) goto error; + strftime(str, freespace, "%H", &ts); + break; + + case 'I': /* Request ID */ + if (request->packet) { + snprintf(str, freespace, "%i", request->packet->id); + } + break; + + case 'M': /* request microsecond component */ + snprintf(str, freespace, "%06u", (unsigned int) usec); + break; + + case 'S': /* request timestamp in SQL format*/ + if (!localtime_r(&when, &ts)) goto error; + strftime(str, freespace, "%Y-%m-%d %H:%M:%S", &ts); + break; + + case 'T': /* request timestamp */ + if (!localtime_r(&when, &ts)) goto error; + nl = str + strftime(str, freespace, "%Y-%m-%d-%H.%M.%S", &ts); + rad_assert(((str + freespace) - nl) >= 8); + snprintf(nl, (str + freespace) - nl, ".%06d", usec); + break; + + case 'Y': /* request year */ + if (!localtime_r(&when, &ts)) { + error: + REDEBUG("Failed converting packet timestamp to localtime: %s", fr_syserror(errno)); + talloc_free(str); + return NULL; + } + strftime(str, freespace, "%Y", &ts); + break; + + case 'v': /* Version of code */ + RWDEBUG("%%v is deprecated and will be removed. Use ${version.freeradius-server}"); + snprintf(str, freespace, "%s", radiusd_version_short); + break; + + default: + rad_assert(0 == 1); + break; + } + } + break; + + case XLAT_ATTRIBUTE: + XLAT_DEBUG("%.*sxlat_aprint ATTRIBUTE", lvl, xlat_spaces); + + /* + * Some attributes are virtual <sigh> + */ + str = xlat_getvp(ctx, request, &node->attr, escape ? false : true, true, ","); + if (str) { + XLAT_DEBUG("%.*sEXPAND attr %s", lvl, xlat_spaces, node->attr.tmpl_da->name); + XLAT_DEBUG("%.*s ---> %s", lvl ,xlat_spaces, str); + } + break; + + case XLAT_VIRTUAL: + XLAT_DEBUG("xlat_aprint VIRTUAL"); + str = talloc_array(ctx, char, 2048); /* FIXME: have the module call talloc_typed_asprintf */ + rcode = node->xlat->func(node->xlat->instance, request, NULL, str, 2048); + if (rcode < 0) { + talloc_free(str); + return NULL; + } + RDEBUG2("EXPAND %s", node->xlat->name); + RDEBUG2(" --> %s", str); + + /* + * Resize the buffer to the correct size. + */ + if (rcode == 0) { + talloc_free(str); + str = talloc_strdup(ctx, ""); + } else if (rcode < 2047) { + child = talloc_memdup(ctx, str, rcode + 1); + talloc_free(str); + str = child; + } + break; + + case XLAT_MODULE: + XLAT_DEBUG("xlat_aprint MODULE"); + + if (node->child) { + if (xlat_process(&child, request, node->child, node->xlat->escape, node->xlat->instance) == 0) { + return NULL; + } + + XLAT_DEBUG("%.*sEXPAND mod %s %s", lvl, xlat_spaces, node->fmt, node->child->fmt); + } else { + XLAT_DEBUG("%.*sEXPAND mod %s", lvl, xlat_spaces, node->fmt); + child = talloc_typed_strdup(ctx, ""); + } + + XLAT_DEBUG("%.*s ---> %s", lvl, xlat_spaces, child); + + /* + * Smash \n --> CR. + * + * The OUTPUT of xlat is a "raw" string. The INPUT is a printable string. + * + * This is really the reverse of fr_prints(). + */ + if (cf_new_escape && *child) { + ssize_t slen; + PW_TYPE type; + value_data_t data; + + type = PW_TYPE_STRING; + slen = value_data_from_str(request, &data, &type, NULL, child, talloc_array_length(child) - 1, '"'); + if (slen <= 0) { + talloc_free(child); + return NULL; + } + + talloc_free(child); + child = data.ptr; + + } else { + char *q; + + p = q = child; + while (*p) { + if (*p == '\\') switch (p[1]) { + default: + *(q++) = p[1]; + p += 2; + continue; + + case 'n': + *(q++) = '\n'; + p += 2; + continue; + + case 't': + *(q++) = '\t'; + p += 2; + continue; + } + + *(q++) = *(p++); + } + *q = '\0'; + } + + str = talloc_array(ctx, char, 2048); /* FIXME: have the module call talloc_typed_asprintf */ + *str = '\0'; /* Be sure the string is NULL terminated, we now only free on error */ + + rcode = node->xlat->func(node->xlat->instance, request, child, str, 2048); + talloc_free(child); + if (rcode < 0) { + talloc_free(str); + return NULL; + } + break; + +#ifdef HAVE_REGEX + case XLAT_REGEX: + XLAT_DEBUG("%.*sxlat_aprint REGEX", lvl, xlat_spaces); + if (regex_request_to_sub(ctx, &str, request, node->attr.tmpl_num) < 0) return NULL; + + break; +#endif + + case XLAT_ALTERNATE: + XLAT_DEBUG("%.*sxlat_aprint ALTERNATE", lvl, xlat_spaces); + rad_assert(node->child != NULL); + rad_assert(node->alternate != NULL); + + /* + * Call xlat_process recursively. The child / + * alternate nodes may have "next" pointers, and + * those need to be expanded. + */ + if (xlat_process(&str, request, node->child, escape, escape_ctx) > 0) { + XLAT_DEBUG("%.*sALTERNATE got first string: %s", lvl, xlat_spaces, str); + } else { + (void) xlat_process(&str, request, node->alternate, escape, escape_ctx); + XLAT_DEBUG("%.*sALTERNATE got alternate string %s", lvl, xlat_spaces, str); + } + break; + } + + /* + * If there's no data, return that, instead of an empty string. + */ + if (str && !str[0]) { + talloc_free(str); + return NULL; + } + + /* + * Escape the non-literals we found above. + */ + if (str && escape) { + size_t len; + char *escaped; + + len = talloc_array_length(str) * 3; + + escaped = talloc_array(ctx, char, len); + escape(request, escaped, len, str, escape_ctx); + talloc_free(str); + str = escaped; + } + + return str; +} + + +static size_t xlat_process(char **out, REQUEST *request, xlat_exp_t const * const head, + xlat_escape_t escape, void *escape_ctx) +{ + int i, list; + size_t total; + char **array, *answer; + xlat_exp_t const *node; + + *out = NULL; + + /* + * There are no nodes to process, so the result is a zero + * length string. + */ + if (!head) { + *out = talloc_zero_array(request, char, 1); + return 0; + } + + /* + * Hack for speed. If it's one expansion, just allocate + * that and return, instead of allocating an intermediary + * array. + */ + if (!head->next) { + /* + * Pass the MAIN escape function. Recursive + * calls will call node-specific escape + * functions. + */ + answer = xlat_aprint(request, request, head, escape, escape_ctx, 0); + if (!answer) { + *out = talloc_zero_array(request, char, 1); + return 0; + } + *out = answer; + return strlen(answer); + } + + list = 0; /* FIXME: calculate this once */ + for (node = head; node != NULL; node = node->next) { + list++; + } + + array = talloc_array(request, char *, list); + if (!array) return -1; + + for (node = head, i = 0; node != NULL; node = node->next, i++) { + array[i] = xlat_aprint(array, request, node, escape, escape_ctx, 0); /* may be NULL */ + } + + total = 0; + for (i = 0; i < list; i++) { + if (array[i]) total += strlen(array[i]); /* FIXME: calculate strlen once */ + } + + if (!total) { + talloc_free(array); + *out = talloc_zero_array(request, char, 1); + return 0; + } + + answer = talloc_array(request, char, total + 1); + + total = 0; + for (i = 0; i < list; i++) { + size_t len; + + if (array[i]) { + len = strlen(array[i]); + memcpy(answer + total, array[i], len); + total += len; + } + } + answer[total] = '\0'; + talloc_free(array); /* and child entries */ + + *out = answer; + return total; +} + + +/** Replace %whatever in a string. + * + * See 'doc/configuration/variables.rst' for more information. + * + * @param[out] out Where to write pointer to output buffer. + * @param[in] outlen Size of out. + * @param[in] request current request. + * @param[in] node the xlat structure to expand + * @param[in] escape function to escape final value e.g. SQL quoting. + * @param[in] escape_ctx pointer to pass to escape function. + * @return length of string written @bug should really have -1 for failure + */ +static ssize_t xlat_expand_struct(char **out, size_t outlen, REQUEST *request, xlat_exp_t const *node, + xlat_escape_t escape, void *escape_ctx) +{ + char *buff; + ssize_t len; + + rad_assert(node != NULL); + + len = xlat_process(&buff, request, node, escape, escape_ctx); + if ((len < 0) || !buff) { + rad_assert(buff == NULL); + if (*out) *out[0] = '\0'; + return len; + } + + len = strlen(buff); + + /* + * If out doesn't point to an existing buffer + * copy the pointer to our buffer over. + */ + if (!*out) { + *out = buff; + return len; + } + + /* + * Otherwise copy the malloced buffer to the fixed one. + */ + strlcpy(*out, buff, outlen); + talloc_free(buff); + return len; +} + +static ssize_t xlat_expand(char **out, size_t outlen, REQUEST *request, char const *fmt, + xlat_escape_t escape, void *escape_ctx) CC_HINT(nonnull (1, 3, 4)); + +/** Replace %whatever in a string. + * + * See 'doc/configuration/variables.rst' for more information. + * + * @param[out] out Where to write pointer to output buffer. + * @param[in] outlen Size of out. + * @param[in] request current request. + * @param[in] fmt string to expand. + * @param[in] escape function to escape final value e.g. SQL quoting. + * @param[in] escape_ctx pointer to pass to escape function. + * @return length of string written @bug should really have -1 for failure + */ +static ssize_t xlat_expand(char **out, size_t outlen, REQUEST *request, char const *fmt, + xlat_escape_t escape, void *escape_ctx) +{ + ssize_t len; + xlat_exp_t *node; + + /* + * Give better errors than the old code. + */ + len = xlat_tokenize_request(request, fmt, &node); + if (len == 0) { + if (*out) { + *out[0] = '\0'; + } else { + *out = talloc_zero_array(request, char, 1); + } + return 0; + } + + if (len < 0) { + if (*out) *out[0] = '\0'; + return -1; + } + + len = xlat_expand_struct(out, outlen, request, node, escape, escape_ctx); + talloc_free(node); + + RDEBUG2("EXPAND %s", fmt); + RDEBUG2(" --> %s", *out); + + return len; +} + +/** Try to convert an xlat to a tmpl for efficiency + * + * @param ctx to allocate new vp_tmpl_t in. + * @param node to convert. + * @return NULL if unable to convert (not necessarily error), or a new vp_tmpl_t. + */ +vp_tmpl_t *xlat_to_tmpl_attr(TALLOC_CTX *ctx, xlat_exp_t *node) +{ + vp_tmpl_t *vpt; + + if (node->next || (node->type != XLAT_ATTRIBUTE) || (node->attr.type != TMPL_TYPE_ATTR)) return NULL; + + /* + * Concat means something completely different as an attribute reference + * Count isn't implemented. + */ + if ((node->attr.tmpl_num == NUM_COUNT) || (node->attr.tmpl_num == NUM_ALL)) return NULL; + + vpt = tmpl_alloc(ctx, TMPL_TYPE_ATTR, node->fmt, -1); + if (!vpt) return NULL; + memcpy(&vpt->data, &node->attr.data, sizeof(vpt->data)); + + VERIFY_TMPL(vpt); + + return vpt; +} + +/** Try to convert attr tmpl to an xlat for &attr[*] and artificially constructing expansions + * + * @param ctx to allocate new xlat_expt_t in. + * @param vpt to convert. + * @return NULL if unable to convert (not necessarily error), or a new vp_tmpl_t. + */ +xlat_exp_t *xlat_from_tmpl_attr(TALLOC_CTX *ctx, vp_tmpl_t *vpt) +{ + xlat_exp_t *node; + + if (vpt->type != TMPL_TYPE_ATTR) return NULL; + + node = talloc_zero(ctx, xlat_exp_t); + node->type = XLAT_ATTRIBUTE; + node->fmt = talloc_bstrndup(node, vpt->name, vpt->len); + tmpl_init(&node->attr, TMPL_TYPE_ATTR, node->fmt, talloc_array_length(node->fmt) - 1); + memcpy(&node->attr.data, &vpt->data, sizeof(vpt->data)); + + return node; +} + +ssize_t radius_xlat(char *out, size_t outlen, REQUEST *request, char const *fmt, xlat_escape_t escape, void *ctx) +{ + return xlat_expand(&out, outlen, request, fmt, escape, ctx); +} + +ssize_t radius_xlat_struct(char *out, size_t outlen, REQUEST *request, xlat_exp_t const *xlat, xlat_escape_t escape, void *ctx) +{ + return xlat_expand_struct(&out, outlen, request, xlat, escape, ctx); +} + +ssize_t radius_axlat(char **out, REQUEST *request, char const *fmt, xlat_escape_t escape, void *ctx) +{ + *out = NULL; + return xlat_expand(out, 0, request, fmt, escape, ctx); +} + +ssize_t radius_axlat_struct(char **out, REQUEST *request, xlat_exp_t const *xlat, xlat_escape_t escape, void *ctx) +{ + *out = NULL; + return xlat_expand_struct(out, 0, request, xlat, escape, ctx); +} |