summaryrefslogtreecommitdiffstats
path: root/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'src/main')
-rw-r--r--src/main/.gitignore13
-rw-r--r--src/main/acct.c186
-rw-r--r--src/main/all.mk3
-rw-r--r--src/main/auth.c890
-rw-r--r--src/main/cb.c247
-rw-r--r--src/main/channel.c231
-rw-r--r--src/main/checkrad.in1515
-rw-r--r--src/main/checkrad.mk5
-rw-r--r--src/main/client.c1562
-rw-r--r--src/main/collectd.c382
-rw-r--r--src/main/command.c3622
-rw-r--r--src/main/conffile.c3810
-rw-r--r--src/main/connection.c1505
-rw-r--r--src/main/crypt.c97
-rw-r--r--src/main/detail.c1266
-rw-r--r--src/main/evaluate.c1144
-rw-r--r--src/main/exec.c633
-rw-r--r--src/main/exfile.c544
-rw-r--r--src/main/files.c361
-rw-r--r--src/main/libfreeradius-server.mk22
-rw-r--r--src/main/listen.c4206
-rw-r--r--src/main/log.c923
-rw-r--r--src/main/mainconfig.c1404
-rw-r--r--src/main/map.c1712
-rw-r--r--src/main/modcall.c4041
-rw-r--r--src/main/modules.c2299
-rw-r--r--src/main/pair.c911
-rw-r--r--src/main/parser.c1809
-rw-r--r--src/main/process.c6344
-rw-r--r--src/main/radattr.c1111
-rw-r--r--src/main/radattr.mk10
-rw-r--r--src/main/radclient.c1704
-rw-r--r--src/main/radclient.mk8
-rw-r--r--src/main/radiusd.c793
-rw-r--r--src/main/radiusd.mk21
-rwxr-xr-xsrc/main/radlast.in7
-rw-r--r--src/main/radlast.mk5
-rw-r--r--src/main/radmin.c774
-rw-r--r--src/main/radmin.mk7
-rw-r--r--src/main/radsniff.c2683
-rw-r--r--src/main/radsniff.mk.in13
-rw-r--r--src/main/radtest.in135
-rw-r--r--src/main/radtest.mk5
-rw-r--r--src/main/radwho.c565
-rw-r--r--src/main/radwho.mk5
-rwxr-xr-xsrc/main/radzap54
-rw-r--r--src/main/radzap.mk5
-rw-r--r--src/main/realms.c3197
-rw-r--r--src/main/regex.c279
-rw-r--r--src/main/session.c254
-rw-r--r--src/main/soh.c675
-rw-r--r--src/main/state.c710
-rw-r--r--src/main/stats.c1005
-rw-r--r--src/main/threads.c1694
-rw-r--r--src/main/tls.c5350
-rw-r--r--src/main/tls_listen.c1347
-rw-r--r--src/main/tmpl.c2399
-rw-r--r--src/main/unittest.c973
-rw-r--r--src/main/unittest.mk25
-rw-r--r--src/main/util.c1732
-rw-r--r--src/main/version.c625
-rw-r--r--src/main/xlat.c2696
62 files changed, 72553 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..76f87b7
--- /dev/null
+++ b/src/main/auth.c
@@ -0,0 +1,890 @@
+/*
+ * 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));
+ }
+
+ 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..f8b2edb
--- /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 %i",
+ 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..b0b3e24
--- /dev/null
+++ b/src/main/client.c
@@ -0,0 +1,1562 @@
+/*
+ * This program is is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ * @file main/client.c
+ * @brief Manage clients allowed to communicate with the server.
+ *
+ * @copyright 2015 Arran Cudbard-Bell <a.cudbardb@freeradius.org>
+ * @copyright 2000,2006 The FreeRADIUS server project
+ * @copyright 2000 Alan DeKok <aland@ox.org>
+ * @copyright 2000 Miquel van Smoorenburg <miquels@cistron.nl>
+ */
+RCSID("$Id$")
+
+#include <freeradius-devel/radiusd.h>
+#include <freeradius-devel/rad_assert.h>
+
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <fcntl.h>
+
+#ifdef WITH_DYNAMIC_CLIENTS
+#ifdef HAVE_DIRENT_H
+#include <dirent.h>
+#endif
+#endif
+
+struct radclient_list {
+ char const *name; /* name of this list */
+ char const *server; /* virtual server associated with this client list */
+
+ /*
+ * FIXME: One set of trees for IPv4, and another for IPv6?
+ */
+ rbtree_t *trees[129]; /* for 0..128, inclusive. */
+ uint32_t min_prefix;
+
+ bool parsed;
+};
+
+
+#ifdef WITH_STATS
+static rbtree_t *tree_num = NULL; /* client numbers 0..N */
+static int tree_num_max = 0;
+#endif
+static RADCLIENT_LIST *root_clients = NULL;
+
+/*
+ * Callback for freeing a client.
+ */
+void client_free(RADCLIENT *client)
+{
+ if (!client) return;
+
+ talloc_free(client);
+}
+
+/*
+ * Callback for comparing two clients.
+ */
+static int client_ipaddr_cmp(void const *one, void const *two)
+{
+ RADCLIENT const *a = one;
+ RADCLIENT const *b = two;
+#ifndef WITH_TCP
+
+ return fr_ipaddr_cmp(&a->ipaddr, &b->ipaddr);
+#else
+ int rcode;
+
+ rcode = fr_ipaddr_cmp(&a->ipaddr, &b->ipaddr);
+ if (rcode != 0) return rcode;
+
+ /*
+ * Wildcard match
+ */
+ if ((a->proto == IPPROTO_IP) ||
+ (b->proto == IPPROTO_IP)) return 0;
+
+ return (a->proto - b->proto);
+#endif
+}
+
+#ifdef WITH_STATS
+static int client_num_cmp(void const *one, void const *two)
+{
+ RADCLIENT const *a = one;
+ RADCLIENT const *b = two;
+
+ return (a->number - b->number);
+}
+#endif
+
+/*
+ * Free a RADCLIENT list.
+ */
+void client_list_free(RADCLIENT_LIST *clients)
+{
+ int i;
+
+ if (!clients) clients = root_clients;
+ if (!clients) return; /* Clients may not have been initialised yet */
+
+ for (i = 0; i <= 128; i++) {
+ if (clients->trees[i]) rbtree_free(clients->trees[i]);
+ clients->trees[i] = NULL;
+ }
+
+ if (clients == root_clients) {
+#ifdef WITH_STATS
+ if (tree_num) rbtree_free(tree_num);
+ tree_num = NULL;
+ tree_num_max = 0;
+#endif
+ root_clients = NULL;
+ }
+
+#ifdef WITH_DYNAMIC_CLIENTS
+ /*
+ * FIXME: No fr_fifo_delete()
+ */
+#endif
+
+ talloc_free(clients);
+}
+
+/*
+ * Return a new, initialized, set of clients.
+ */
+RADCLIENT_LIST *client_list_init(CONF_SECTION *cs)
+{
+ RADCLIENT_LIST *clients = talloc_zero(cs, RADCLIENT_LIST);
+
+ if (!clients) return NULL;
+
+ clients->min_prefix = 128;
+
+ /*
+ * Associate the "clients" list with the virtual server.
+ */
+ if (cs && (cf_data_add(cs, "clients", clients, NULL) < 0)) {
+ ERROR("Failed to associate client list with section %s\n", cf_section_name1(cs));
+ client_list_free(clients);
+ return false;
+ }
+
+ return clients;
+}
+
+/** Add a client to a RADCLIENT_LIST
+ *
+ * @param clients list to add client to, may be NULL if global client list is being used.
+ * @param client to add.
+ * @return true on success, false on failure.
+ */
+bool client_add(RADCLIENT_LIST *clients, RADCLIENT *client)
+{
+ RADCLIENT *old;
+ char buffer[INET6_ADDRSTRLEN + 3];
+
+ if (!client) return false;
+
+ /*
+ * Initialize the global list, if not done already.
+ */
+ if (!root_clients) {
+ root_clients = client_list_init(NULL);
+ if (!root_clients) {
+ ERROR("Cannot add client - failed creating client list");
+ return false;
+ }
+ }
+
+ /*
+ * Hack to fixup wildcard clients
+ *
+ * If the IP is all zeros, with a 32 or 128 bit netmask
+ * assume the user meant to configure 0.0.0.0/0 instead
+ * of 0.0.0.0/32 - which would require the src IP of
+ * the client to be all zeros.
+ */
+ if (fr_inaddr_any(&client->ipaddr) == 1) switch (client->ipaddr.af) {
+ case AF_INET:
+ if (client->ipaddr.prefix == 32) client->ipaddr.prefix = 0;
+ break;
+
+ case AF_INET6:
+ if (client->ipaddr.prefix == 128) client->ipaddr.prefix = 0;
+ break;
+
+ default:
+ rad_assert(0);
+ }
+
+ fr_ntop(buffer, sizeof(buffer), &client->ipaddr);
+ DEBUG3("Adding client %s (%s) to prefix tree %i", buffer, client->longname, client->ipaddr.prefix);
+
+ /*
+ * If the client also defines a server, do that now.
+ */
+ if (client->defines_coa_server) if (!realm_home_server_add(client->coa_home_server)) return false;
+
+ /*
+ * If there's no client list, BUT there's a virtual
+ * server, try to add the client to the appropriate
+ * "clients" section for that virtual server.
+ */
+ if (!clients && client->server) {
+ CONF_SECTION *cs;
+ CONF_SECTION *subcs;
+ CONF_PAIR *cp;
+ char const *section_name;
+
+ cs = cf_section_sub_find_name2(main_config.config, "server", client->server);
+ if (!cs) {
+ ERROR("Cannot add client - virtual server %s does not exist", client->server);
+ return false;
+ }
+
+ /*
+ * If this server has no "listen" section, add the clients
+ * to the global client list.
+ */
+ subcs = cf_section_sub_find(cs, "listen");
+ if (!subcs) {
+ DEBUG("No 'listen' section in virtual server %s. Adding client to global client list",
+ client->server);
+ goto check_list;
+ }
+
+ cp = cf_pair_find(subcs, "clients");
+ if (!cp) {
+ DEBUG("No 'clients' configuration item in first listener of virtual server %s. Adding client to global client list",
+ client->server);
+ goto check_list;
+ }
+
+ /*
+ * Duplicate the lookup logic in common_socket_parse()
+ *
+ * Explicit list given: use it.
+ */
+ section_name = cf_pair_value(cp);
+ if (!section_name) goto check_list;
+
+ subcs = cf_section_sub_find_name2(main_config.config, "clients", section_name);
+ if (!subcs) {
+ subcs = cf_section_find(section_name);
+ }
+ if (!subcs) {
+ cf_log_err_cs(cs,
+ "Failed to find clients %s {...}",
+ section_name);
+ return false;
+ }
+
+ DEBUG("Adding client to client list %s", section_name);
+
+ /*
+ * If the client list already exists, use that.
+ * Otherwise, create a new client list.
+ *
+ * @todo - add the client to _all_ listeners?
+ */
+ clients = cf_data_find(subcs, "clients");
+ if (clients) goto check_list;
+
+ clients = client_list_init(subcs);
+ if (!clients) {
+ ERROR("Cannot add client - failed creating client list %s for server %s", section_name,
+ client->server);
+ return false;
+ }
+ }
+
+check_list:
+ if (!clients) clients = root_clients;
+ client->list = clients;
+
+ /*
+ * Create a tree for it.
+ */
+ if (!clients->trees[client->ipaddr.prefix]) {
+ clients->trees[client->ipaddr.prefix] = rbtree_create(clients, client_ipaddr_cmp, NULL, 0);
+ if (!clients->trees[client->ipaddr.prefix]) {
+ return false;
+ }
+ }
+
+#define namecmp(a) ((!old->a && !client->a) || (old->a && client->a && (strcmp(old->a, client->a) == 0)))
+
+ /*
+ * Cannot insert the same client twice.
+ */
+ old = rbtree_finddata(clients->trees[client->ipaddr.prefix], client);
+ if (old) {
+ /*
+ * If it's a complete duplicate, then free the new
+ * one, and return "OK".
+ */
+ if ((fr_ipaddr_cmp(&old->ipaddr, &client->ipaddr) == 0) &&
+ (old->ipaddr.prefix == client->ipaddr.prefix) &&
+ namecmp(longname) && namecmp(secret) &&
+ namecmp(shortname) && namecmp(nas_type) &&
+ namecmp(login) && namecmp(password) && namecmp(server) &&
+#ifdef WITH_DYNAMIC_CLIENTS
+ (old->lifetime == client->lifetime) &&
+ namecmp(client_server) &&
+#endif
+#ifdef WITH_COA
+ namecmp(coa_name) &&
+ (old->coa_home_server == client->coa_home_server) &&
+ (old->coa_home_pool == client->coa_home_pool) &&
+#endif
+ (old->message_authenticator == client->message_authenticator)) {
+ WARN("Ignoring duplicate client %s", client->longname);
+ client_free(client);
+ return true;
+ }
+
+ ERROR("Failed to add duplicate client %s", client->shortname);
+ return false;
+ }
+#undef namecmp
+
+ /*
+ * Other error adding client: likely is fatal.
+ */
+ if (!rbtree_insert(clients->trees[client->ipaddr.prefix], client)) {
+ return false;
+ }
+
+#ifdef WITH_STATS
+ if (!tree_num) {
+ tree_num = rbtree_create(clients, client_num_cmp, NULL, 0);
+ }
+
+#ifdef WITH_DYNAMIC_CLIENTS
+ /*
+ * More catching of clients added by rlm_sql.
+ *
+ * The sql modules sets the dynamic flag BEFORE calling
+ * us. The client_afrom_request() function sets it AFTER
+ * calling us.
+ */
+ if (client->dynamic && (client->lifetime == 0)) {
+ RADCLIENT *network;
+
+ /*
+ * If there IS an enclosing network,
+ * inherit the lifetime from it.
+ */
+ network = client_find(clients, &client->ipaddr, client->proto);
+ if (network) {
+ client->lifetime = network->lifetime;
+ }
+ }
+#endif
+
+ client->number = tree_num_max;
+ tree_num_max++;
+ if (tree_num) rbtree_insert(tree_num, client);
+#endif
+
+ if (client->ipaddr.prefix < clients->min_prefix) {
+ clients->min_prefix = client->ipaddr.prefix;
+ }
+
+ (void) talloc_steal(clients, client); /* reparent it */
+
+ return true;
+}
+
+
+#ifdef WITH_DYNAMIC_CLIENTS
+void client_delete(RADCLIENT_LIST *clients, RADCLIENT *client)
+{
+ if (!client) return;
+
+ if (!clients) clients = root_clients;
+
+ if (!client->dynamic) return;
+
+ rad_assert(client->ipaddr.prefix <= 128);
+
+#ifdef WITH_STATS
+ rbtree_deletebydata(tree_num, client);
+#endif
+ rbtree_deletebydata(clients->trees[client->ipaddr.prefix], client);
+}
+#endif
+
+#ifdef WITH_STATS
+/*
+ * Find a client in the RADCLIENTS list by number.
+ * This is a support function for the statistics code.
+ */
+RADCLIENT *client_findbynumber(RADCLIENT_LIST const *clients, int number)
+{
+ if (!clients) clients = root_clients;
+
+ if (!clients) return NULL;
+
+ if (number >= tree_num_max) return NULL;
+
+ if (tree_num) {
+ RADCLIENT myclient;
+
+ myclient.number = number;
+
+ return rbtree_finddata(tree_num, &myclient);
+ }
+
+ return NULL;
+}
+#else
+RADCLIENT *client_findbynumber(UNUSED const RADCLIENT_LIST *clients, UNUSED int number)
+{
+ return NULL;
+}
+#endif
+
+
+/*
+ * Find a client in the RADCLIENTS list.
+ */
+RADCLIENT *client_find(RADCLIENT_LIST const *clients, fr_ipaddr_t const *ipaddr, int proto)
+{
+ int32_t i, max_prefix;
+ RADCLIENT myclient;
+
+ if (!clients) clients = root_clients;
+
+ if (!clients || !ipaddr) return NULL;
+
+ switch (ipaddr->af) {
+ case AF_INET:
+ max_prefix = 32;
+ break;
+
+ case AF_INET6:
+ max_prefix = 128;
+ break;
+
+ default :
+ return NULL;
+ }
+
+ for (i = max_prefix; i >= (int32_t) clients->min_prefix; i--) {
+ void *data;
+
+ myclient.ipaddr = *ipaddr;
+ myclient.proto = proto;
+ fr_ipaddr_mask(&myclient.ipaddr, i);
+
+ if (!clients->trees[i]) continue;
+
+ data = rbtree_finddata(clients->trees[i], &myclient);
+ if (data) return data;
+ }
+
+ return NULL;
+}
+
+/*
+ * Old wrapper for client_find
+ */
+RADCLIENT *client_find_old(fr_ipaddr_t const *ipaddr)
+{
+ return client_find(root_clients, ipaddr, IPPROTO_UDP);
+}
+
+static fr_ipaddr_t cl_ipaddr;
+static uint32_t cl_netmask;
+static char const *cl_srcipaddr = NULL;
+static char const *hs_proto = NULL;
+
+#ifdef WITH_TCP
+static CONF_PARSER limit_config[] = {
+ { "max_connections", FR_CONF_OFFSET(PW_TYPE_INTEGER, RADCLIENT, limit.max_connections), "16" },
+
+ { "lifetime", FR_CONF_OFFSET(PW_TYPE_INTEGER, RADCLIENT, limit.lifetime), "0" },
+
+ { "idle_timeout", FR_CONF_OFFSET(PW_TYPE_INTEGER, RADCLIENT, limit.idle_timeout), "30" },
+
+ CONF_PARSER_TERMINATOR
+};
+#endif
+
+static const CONF_PARSER client_config[] = {
+ { "ipaddr", FR_CONF_POINTER(PW_TYPE_COMBO_IP_PREFIX, &cl_ipaddr), NULL },
+ { "ipv4addr", FR_CONF_POINTER(PW_TYPE_IPV4_PREFIX, &cl_ipaddr), NULL },
+ { "ipv6addr", FR_CONF_POINTER(PW_TYPE_IPV6_PREFIX, &cl_ipaddr), NULL },
+
+ { "netmask", FR_CONF_POINTER(PW_TYPE_INTEGER, &cl_netmask), NULL },
+
+ { "src_ipaddr", FR_CONF_POINTER(PW_TYPE_STRING, &cl_srcipaddr), NULL },
+
+ { "require_message_authenticator", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, RADCLIENT, message_authenticator), "no" },
+
+ { "secret", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_SECRET, RADCLIENT, secret), NULL },
+ { "shortname", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, shortname), NULL },
+
+ { "nas_type", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, nas_type), NULL },
+
+ { "login", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, login), NULL },
+ { "password", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, password), NULL },
+ { "virtual_server", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, server), NULL },
+ { "response_window", FR_CONF_OFFSET(PW_TYPE_TIMEVAL, RADCLIENT, response_window), NULL },
+
+#ifdef WITH_TCP
+ { "proto", FR_CONF_POINTER(PW_TYPE_STRING, &hs_proto), NULL },
+ { "limit", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) limit_config },
+#endif
+
+#ifdef WITH_DYNAMIC_CLIENTS
+ { "dynamic_clients", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, client_server), NULL },
+ { "lifetime", FR_CONF_OFFSET(PW_TYPE_INTEGER, RADCLIENT, lifetime), NULL },
+ { "rate_limit", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, RADCLIENT, rate_limit), NULL },
+#endif
+
+ CONF_PARSER_TERMINATOR
+};
+
+/** Create the linked list of clients from the new configuration type
+ *
+ */
+#ifdef WITH_TLS
+RADCLIENT_LIST *client_list_parse_section(CONF_SECTION *section, bool tls_required)
+#else
+RADCLIENT_LIST *client_list_parse_section(CONF_SECTION *section, UNUSED bool tls_required)
+#endif
+{
+ bool global = false, in_server = false;
+ CONF_SECTION *cs;
+ RADCLIENT *c = NULL;
+ RADCLIENT_LIST *clients = NULL;
+
+ /*
+ * Be forgiving. If there's already a clients, return
+ * it. Otherwise create a new one.
+ */
+ clients = cf_data_find(section, "clients");
+ if (clients) {
+ /*
+ * Modules are initialized before the listeners.
+ * Which means that we MIGHT have read clients
+ * from SQL before parsing this "clients"
+ * section. So there may already be a clients
+ * list.
+ *
+ * But the list isn't _our_ list that we parsed,
+ * so we still need to parse the clients here.
+ */
+ if (clients->parsed) return clients;
+ } else {
+ clients = client_list_init(section);
+ if (!clients) return NULL;
+ }
+
+ if (cf_top_section(section) == section) {
+ global = true;
+ clients->name = "global";
+ clients->server = NULL;
+ }
+
+ if (strcmp("server", cf_section_name1(section)) == 0) {
+ clients->name = NULL;
+ clients->server = cf_section_name2(section);
+ in_server = true;
+ }
+
+ for (cs = cf_subsection_find_next(section, NULL, "client");
+ cs;
+ cs = cf_subsection_find_next(section, cs, "client")) {
+ c = client_afrom_cs(cs, cs, in_server, false);
+ if (!c) {
+ error:
+ client_free(c);
+ client_list_free(clients);
+ return NULL;
+ }
+
+#ifdef WITH_TLS
+ /*
+ * TLS clients CANNOT use non-TLS listeners.
+ * non-TLS clients CANNOT use TLS listeners.
+ */
+ if (tls_required != c->tls_required) {
+ cf_log_err_cs(cs, "Client does not have the same TLS configuration as the listener");
+ goto error;
+ }
+#endif
+
+ /*
+ * FIXME: Add the client as data via cf_data_add,
+ * for migration issues.
+ */
+
+#ifdef WITH_DYNAMIC_CLIENTS
+#ifdef HAVE_DIRENT_H
+ if (c->client_server) {
+ char const *value;
+ CONF_PAIR *cp;
+ DIR *dir;
+ struct dirent *dp;
+ struct stat stat_buf;
+ char buf2[2048];
+
+ /*
+ * Find the directory where individual
+ * client definitions are stored.
+ */
+ cp = cf_pair_find(cs, "directory");
+ if (!cp) goto add_client;
+
+ value = cf_pair_value(cp);
+ if (!value) {
+ cf_log_err_cs(cs, "The \"directory\" entry must not be empty");
+ goto error;
+ }
+
+ DEBUG("including dynamic clients in %s", value);
+
+ dir = opendir(value);
+ if (!dir) {
+ cf_log_err_cs(cs, "Error reading directory %s: %s", value, fr_syserror(errno));
+ goto error;
+ }
+
+ /*
+ * Read the directory, ignoring "." files.
+ */
+ while ((dp = readdir(dir)) != NULL) {
+ char const *p;
+ RADCLIENT *dc;
+
+ if (dp->d_name[0] == '.') continue;
+
+ /*
+ * Check for valid characters
+ */
+ for (p = dp->d_name; *p != '\0'; p++) {
+ if (isalpha((int)*p) ||
+ isdigit((int)*p) ||
+ (*p == ':') ||
+ (*p == '.')) continue;
+ break;
+ }
+ if (*p != '\0') continue;
+
+ snprintf(buf2, sizeof(buf2), "%s/%s", value, dp->d_name);
+
+ if ((stat(buf2, &stat_buf) != 0) || S_ISDIR(stat_buf.st_mode)) continue;
+
+ dc = client_read(buf2, in_server, true);
+ if (!dc) {
+ cf_log_err_cs(cs, "Failed reading client file \"%s\"", buf2);
+ closedir(dir);
+ goto error;
+ }
+
+ /*
+ * Validate, and add to the list.
+ */
+ if (!client_add_dynamic(clients, c, dc)) {
+ closedir(dir);
+ goto error;
+ }
+ } /* loop over the directory */
+ closedir(dir);
+ }
+#endif /* HAVE_DIRENT_H */
+
+ add_client:
+#endif /* WITH_DYNAMIC_CLIENTS */
+ if (!client_add(clients, c)) {
+ cf_log_err_cs(cs, "Failed to add client %s", cf_section_name2(cs));
+ goto error;
+ }
+
+ }
+
+ /*
+ * Replace the global list of clients with the new one.
+ * The old one is still referenced from the original
+ * configuration, and will be freed when that is freed.
+ */
+ if (global) root_clients = clients;
+
+ clients->parsed = true;
+ return clients;
+}
+
+#ifdef WITH_DYNAMIC_CLIENTS
+/*
+ * We overload this structure a lot.
+ */
+static const CONF_PARSER dynamic_config[] = {
+ { "FreeRADIUS-Client-IP-Address", FR_CONF_OFFSET(PW_TYPE_IPV4_ADDR, RADCLIENT, ipaddr), NULL },
+ { "FreeRADIUS-Client-IPv6-Address", FR_CONF_OFFSET(PW_TYPE_IPV6_ADDR, RADCLIENT, ipaddr), NULL },
+ { "FreeRADIUS-Client-IP-Prefix", FR_CONF_OFFSET(PW_TYPE_IPV4_PREFIX, RADCLIENT, ipaddr), NULL },
+ { "FreeRADIUS-Client-IPv6-Prefix", FR_CONF_OFFSET(PW_TYPE_IPV6_PREFIX, RADCLIENT, ipaddr), NULL },
+ { "FreeRADIUS-Client-Src-IP-Address", FR_CONF_OFFSET(PW_TYPE_IPV4_ADDR, RADCLIENT, src_ipaddr), NULL },
+ { "FreeRADIUS-Client-Src-IPv6-Address", FR_CONF_OFFSET(PW_TYPE_IPV6_ADDR, RADCLIENT, src_ipaddr), NULL },
+
+ { "FreeRADIUS-Client-Require-MA", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, RADCLIENT, message_authenticator), NULL },
+
+ { "FreeRADIUS-Client-Secret", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, secret), "" },
+ { "FreeRADIUS-Client-Shortname", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, shortname), "" },
+ { "FreeRADIUS-Client-NAS-Type", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, nas_type), NULL },
+ { "FreeRADIUS-Client-Virtual-Server", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, server), NULL },
+
+ CONF_PARSER_TERMINATOR
+};
+
+/** Add a dynamic client
+ *
+ */
+bool client_add_dynamic(RADCLIENT_LIST *clients, RADCLIENT *master, RADCLIENT *c)
+{
+ char buffer[128];
+
+ /*
+ * No virtual server defined. Inherit the parent's
+ * definition.
+ */
+ if (master->server && !c->server) {
+ c->server = talloc_typed_strdup(c, master->server);
+ }
+
+ /*
+ * If the client network isn't global (not tied to a
+ * virtual server), then ensure that this clients server
+ * is the same as the enclosing networks virtual server.
+ */
+ if (master->server && (strcmp(master->server, c->server) != 0)) {
+ ERROR("Cannot add client %s/%i: Virtual server %s is not the same as the virtual server for the network",
+ ip_ntoh(&c->ipaddr, buffer, sizeof(buffer)), c->ipaddr.prefix, c->server);
+
+ goto error;
+ }
+
+ if (!client_add(clients, c)) {
+ ERROR("Cannot add client %s/%i: Internal error",
+ ip_ntoh(&c->ipaddr, buffer, sizeof(buffer)), c->ipaddr.prefix);
+
+ goto error;
+ }
+
+ /*
+ * Initialize the remaining fields.
+ */
+ c->dynamic = true;
+ c->lifetime = master->lifetime;
+ c->created = time(NULL);
+ c->longname = talloc_typed_strdup(c, c->shortname);
+
+ if (rad_debug_lvl <= 2) {
+ INFO("Adding client %s/%i",
+ ip_ntoh(&c->ipaddr, buffer, sizeof(buffer)), c->ipaddr.prefix);
+ } else {
+ INFO("Adding client %s/%i with shared secret \"%s\"",
+ ip_ntoh(&c->ipaddr, buffer, sizeof(buffer)), c->ipaddr.prefix, c->secret);
+ }
+ return true;
+
+error:
+ client_free(c);
+ return false;
+}
+
+/** Create a client CONF_SECTION using a mapping section to map values from a result set to client attributes
+ *
+ * If we hit a CONF_SECTION we recurse and process its CONF_PAIRS too.
+ *
+ * @note Caller should free CONF_SECTION passed in as out, on error.
+ * Contents of that section will be in an undefined state.
+ *
+ * @param[in,out] out Section to perform mapping on. Either the root of the client config, or a parent section
+ * (when this function is called recursively).
+ * Should be alloced with cf_section_alloc, or if there's a separate template section, the
+ * result of calling cf_section_dup on that section.
+ * @param[in] map section.
+ * @param[in] func to call to retrieve CONF_PAIR values. Must return a talloced buffer containing the value.
+ * @param[in] data to pass to func, usually a result pointer.
+ * @return 0 on success else -1 on error.
+ */
+int client_map_section(CONF_SECTION *out, CONF_SECTION const *map, client_value_cb_t func, void *data)
+{
+ CONF_ITEM const *ci;
+
+ for (ci = cf_item_find_next(map, NULL);
+ ci != NULL;
+ ci = cf_item_find_next(map, ci)) {
+ CONF_PAIR const *cp;
+ CONF_PAIR *old;
+ char *value;
+ char const *attr;
+
+ /*
+ * Recursively process map subsection
+ */
+ if (cf_item_is_section(ci)) {
+ CONF_SECTION *cs, *cc;
+
+ cs = cf_item_to_section(ci);
+ /*
+ * Use pre-existing section or alloc a new one
+ */
+ cc = cf_section_sub_find_name2(out, cf_section_name1(cs), cf_section_name2(cs));
+ if (!cc) {
+ cc = cf_section_alloc(out, cf_section_name1(cs), cf_section_name2(cs));
+ cf_section_add(out, cc);
+ if (!cc) return -1;
+ }
+
+ if (client_map_section(cc, cs, func, data) < 0) return -1;
+ continue;
+ }
+
+ cp = cf_item_to_pair(ci);
+ attr = cf_pair_attr(cp);
+
+ /*
+ * The callback can return 0 (success) and not provide a value
+ * in which case we skip the mapping pair.
+ *
+ * Or return -1 in which case we error out.
+ */
+ if (func(&value, cp, data) < 0) {
+ cf_log_err_cs(out, "Failed performing mapping \"%s\" = \"%s\"", attr, cf_pair_value(cp));
+ return -1;
+ }
+ if (!value) continue;
+
+ /*
+ * Replace an existing CONF_PAIR
+ */
+ old = cf_pair_find(out, attr);
+ if (old) {
+ cf_pair_replace(out, old, value);
+ talloc_free(value);
+ continue;
+ }
+
+ /*
+ * ...or add a new CONF_PAIR
+ */
+ cp = cf_pair_alloc(out, attr, value, T_OP_SET, T_BARE_WORD, T_SINGLE_QUOTED_STRING);
+ if (!cp) {
+ cf_log_err_cs(out, "Failed allocing pair \"%s\" = \"%s\"", attr, value);
+ talloc_free(value);
+ return -1;
+ }
+ talloc_free(value);
+ cf_item_add(out, cf_pair_to_item(cp));
+ }
+
+ return 0;
+}
+
+/** Allocate a new client from a config section
+ *
+ * @param ctx to allocate new clients in.
+ * @param cs to process as a client.
+ * @param in_server Whether the client should belong to a specific virtual server.
+ * @param with_coa If true and coa_home_server or coa_home_pool aren't specified automatically,
+ * create a coa home_server section and add it to the client CONF_SECTION.
+ * @return new RADCLIENT struct.
+ */
+RADCLIENT *client_afrom_cs(TALLOC_CTX *ctx, CONF_SECTION *cs, bool in_server, bool with_coa)
+{
+ RADCLIENT *c;
+ char const *name2;
+
+ name2 = cf_section_name2(cs);
+ if (!name2) {
+ cf_log_err_cs(cs, "Missing client name");
+ return NULL;
+ }
+
+ /*
+ * The size is fine.. Let's create the buffer
+ */
+ c = talloc_zero(ctx, RADCLIENT);
+ c->cs = cs;
+
+ memset(&cl_ipaddr, 0, sizeof(cl_ipaddr));
+ cl_netmask = 255;
+
+ if (cf_section_parse(cs, c, client_config) < 0) {
+ cf_log_err_cs(cs, "Error parsing client section");
+ error:
+ client_free(c);
+#ifdef WITH_TCP
+ hs_proto = NULL;
+ cl_srcipaddr = NULL;
+#endif
+
+ return NULL;
+ }
+
+ /*
+ * Global clients can set servers to use, per-server clients cannot.
+ */
+ if (in_server && c->server) {
+ cf_log_err_cs(cs, "Clients inside of an server section cannot point to a server");
+ goto error;
+ }
+
+ /*
+ * Allow the old method to specify "netmask". Just using "1.2.3.4" means it's a /32.
+ */
+ if (cl_netmask != 255) {
+ if ((cl_ipaddr.prefix != cl_netmask) &&
+ (((cl_ipaddr.af == AF_INET) && cl_ipaddr.prefix != 32) ||
+ ((cl_ipaddr.af == AF_INET6) && cl_ipaddr.prefix != 128))) {
+ cf_log_err_cs(cs, "Clients cannot use 'ipaddr/mask' and 'netmask' at the same time.");
+ goto error;
+ }
+
+ cl_ipaddr.prefix = cl_netmask;
+ }
+
+ /*
+ * Newer style client definitions with either ipaddr or ipaddr6
+ * config items.
+ */
+ if (cf_pair_find(cs, "ipaddr") || cf_pair_find(cs, "ipv4addr") || cf_pair_find(cs, "ipv6addr")) {
+ char buffer[128];
+
+ /*
+ * Sets ipv4/ipv6 address and prefix.
+ */
+ c->ipaddr = cl_ipaddr;
+
+ /*
+ * Set the long name to be the result of a reverse lookup on the IP address.
+ */
+ ip_ntoh(&c->ipaddr, buffer, sizeof(buffer));
+ c->longname = talloc_typed_strdup(c, buffer);
+
+ /*
+ * Set the short name to the name2.
+ */
+ if (!c->shortname) c->shortname = talloc_typed_strdup(c, name2);
+ /*
+ * No "ipaddr" or "ipv6addr", use old-style "client <ipaddr> {" syntax.
+ */
+ } else {
+ WARN("No 'ipaddr' or 'ipv4addr' or 'ipv6addr' field found in client %s. "
+ "Please fix your configuration", name2);
+ WARN("Support for old-style clients will be removed in a future release");
+
+#ifdef WITH_TCP
+ if (cf_pair_find(cs, "proto") != NULL) {
+ cf_log_err_cs(cs, "Cannot use 'proto' inside of old-style client definition");
+ goto error;
+ }
+#endif
+ if (fr_pton(&c->ipaddr, name2, -1, AF_UNSPEC, true) < 0) {
+ cf_log_err_cs(cs, "Failed parsing client name \"%s\" as ip address or hostname: %s", name2,
+ fr_strerror());
+ goto error;
+ }
+
+ c->longname = talloc_typed_strdup(c, name2);
+ if (!c->shortname) c->shortname = talloc_typed_strdup(c, c->longname);
+ }
+
+ c->proto = IPPROTO_UDP;
+ if (hs_proto) {
+ if (strcmp(hs_proto, "udp") == 0) {
+ hs_proto = NULL;
+
+#ifdef WITH_TCP
+ } else if (strcmp(hs_proto, "tcp") == 0) {
+ hs_proto = NULL;
+ c->proto = IPPROTO_TCP;
+# ifdef WITH_TLS
+ } else if (strcmp(hs_proto, "tls") == 0) {
+ hs_proto = NULL;
+ c->proto = IPPROTO_TCP;
+ c->tls_required = true;
+
+ } else if (strcmp(hs_proto, "radsec") == 0) {
+ hs_proto = NULL;
+ c->proto = IPPROTO_TCP;
+ c->tls_required = true;
+# endif
+ } else if (strcmp(hs_proto, "*") == 0) {
+ hs_proto = NULL;
+ c->proto = IPPROTO_IP; /* fake for dual */
+#endif
+ } else {
+ cf_log_err_cs(cs, "Unknown proto \"%s\".", hs_proto);
+ goto error;
+ }
+ }
+
+ /*
+ * If a src_ipaddr is specified, when we send the return packet
+ * we will use this address instead of the src from the
+ * request.
+ */
+ if (cl_srcipaddr) {
+#ifdef WITH_UDPFROMTO
+ switch (c->ipaddr.af) {
+ case AF_INET:
+ if (fr_pton4(&c->src_ipaddr, cl_srcipaddr, -1, true, false) < 0) {
+ cf_log_err_cs(cs, "Failed parsing src_ipaddr: %s", fr_strerror());
+ goto error;
+ }
+ break;
+
+ case AF_INET6:
+ if (fr_pton6(&c->src_ipaddr, cl_srcipaddr, -1, true, false) < 0) {
+ cf_log_err_cs(cs, "Failed parsing src_ipaddr: %s", fr_strerror());
+ goto error;
+ }
+ break;
+ default:
+ rad_assert(0);
+ }
+#else
+ WARN("Server not built with udpfromto, ignoring client src_ipaddr");
+#endif
+ cl_srcipaddr = NULL;
+ }
+
+ /*
+ * A response_window of zero is OK, and means that it's
+ * ignored by the rest of the server timers.
+ */
+ if (timerisset(&c->response_window)) {
+ FR_TIMEVAL_BOUND_CHECK("response_window", &c->response_window, >=, 0, 1000);
+ FR_TIMEVAL_BOUND_CHECK("response_window", &c->response_window, <=, 60, 0);
+ FR_TIMEVAL_BOUND_CHECK("response_window", &c->response_window, <=, main_config.max_request_time, 0);
+ }
+
+#ifdef WITH_DYNAMIC_CLIENTS
+ if (c->client_server) {
+ c->secret = talloc_typed_strdup(c, "testing123");
+
+ if (((c->ipaddr.af == AF_INET) && (c->ipaddr.prefix == 32)) ||
+ ((c->ipaddr.af == AF_INET6) && (c->ipaddr.prefix == 128))) {
+ cf_log_err_cs(cs, "Dynamic clients MUST be a network, not a single IP address");
+ goto error;
+ }
+
+ return c;
+ }
+#endif
+
+ if (!c->secret || (c->secret[0] == '\0')) {
+#ifdef WITH_DHCP
+ char const *value = NULL;
+ CONF_PAIR *cp = cf_pair_find(cs, "dhcp");
+
+ if (cp) value = cf_pair_value(cp);
+
+ /*
+ * Secrets aren't needed for DHCP.
+ */
+ if (value && (strcmp(value, "yes") == 0)) return c;
+#endif
+
+#ifdef WITH_TLS
+ /*
+ * If the client is TLS only, the secret can be
+ * omitted. When omitted, it's hard-coded to
+ * "radsec". See RFC 6614.
+ */
+ if (c->tls_required) {
+ c->secret = talloc_typed_strdup(cs, "radsec");
+ } else
+#endif
+
+ {
+ cf_log_err_cs(cs, "secret must be at least 1 character long");
+ goto error;
+ }
+ }
+
+#ifdef WITH_COA
+ {
+ CONF_PAIR *cp;
+
+ /*
+ * Point the client to the home server pool, OR to the
+ * home server. This gets around the problem of figuring
+ * out which port to use.
+ */
+ cp = cf_pair_find(cs, "coa_server");
+ if (cp) {
+ c->coa_name = cf_pair_value(cp);
+ c->coa_home_pool = home_pool_byname(c->coa_name, HOME_TYPE_COA);
+ if (!c->coa_home_pool) {
+ c->coa_home_server = home_server_byname(c->coa_name, HOME_TYPE_COA);
+ }
+ if (!c->coa_home_pool && !c->coa_home_server) {
+ cf_log_err_cs(cs, "No such home_server or home_server_pool \"%s\"", c->coa_name);
+ goto error;
+ }
+ /*
+ * If we're implicitly adding a CoA home server for
+ * every client, or there's a server subsection,
+ * create a home server CONF_SECTION and then parse
+ * it into a home_server_t.
+ */
+ } else if (with_coa || cf_section_sub_find(cs, "coa_server")) {
+ CONF_SECTION *server;
+ home_server_t *home;
+
+ if (((c->ipaddr.af == AF_INET) && (c->ipaddr.prefix != 32)) ||
+ ((c->ipaddr.af == AF_INET6) && (c->ipaddr.prefix != 128))) {
+ WARN("Subnets not supported for home servers. "
+ "Not adding client %s as home_server", name2);
+ goto done_coa;
+ }
+
+ server = home_server_cs_afrom_client(cs);
+ if (!server) goto error;
+
+ /*
+ * Must be allocated in the context of the client,
+ * as allocating using the context of the
+ * realm_config_t without a mutex, by one of the
+ * workers, would be bad.
+ */
+ home = home_server_afrom_cs(NULL, NULL, server);
+ if (!home) {
+ talloc_free(server);
+ goto error;
+ }
+
+ rad_assert(home->type == HOME_TYPE_COA);
+
+ c->coa_home_server = home;
+ c->defines_coa_server = true;
+ }
+ }
+done_coa:
+#endif
+
+#ifdef WITH_TCP
+ if ((c->proto == IPPROTO_TCP) || (c->proto == IPPROTO_IP)) {
+ if ((c->limit.idle_timeout > 0) && (c->limit.idle_timeout < 5))
+ c->limit.idle_timeout = 5;
+ if ((c->limit.lifetime > 0) && (c->limit.lifetime < 5))
+ c->limit.lifetime = 5;
+ if ((c->limit.lifetime > 0) && (c->limit.idle_timeout > c->limit.lifetime))
+ c->limit.idle_timeout = 0;
+ }
+#endif
+
+ return c;
+}
+
+/** Add a client from a result set (SQL)
+ *
+ * @todo This function should die. SQL should use client_afrom_cs.
+ *
+ * @param ctx Talloc context.
+ * @param identifier Client IP Address / IPv4 subnet / IPv6 subnet / FQDN.
+ * @param secret Client secret.
+ * @param shortname Client friendly name.
+ * @param type NAS-Type.
+ * @param server Virtual-Server to associate clients with.
+ * @param require_ma If true all packets from client must include a message-authenticator.
+ * @return The new client, or NULL on error.
+ */
+RADCLIENT *client_afrom_query(TALLOC_CTX *ctx, char const *identifier, char const *secret,
+ char const *shortname, char const *type, char const *server, bool require_ma)
+{
+ RADCLIENT *c;
+ char buffer[128];
+
+ c = talloc_zero(ctx, RADCLIENT);
+
+ if (fr_pton(&c->ipaddr, identifier, -1, AF_UNSPEC, true) < 0) {
+ ERROR("%s", fr_strerror());
+ talloc_free(c);
+
+ return NULL;
+ }
+
+#ifdef WITH_DYNAMIC_CLIENTS
+ c->dynamic = true;
+#endif
+ ip_ntoh(&c->ipaddr, buffer, sizeof(buffer));
+ c->longname = talloc_typed_strdup(c, buffer);
+
+ /*
+ * Other values (secret, shortname, nas_type, virtual_server)
+ */
+ c->secret = talloc_typed_strdup(c, secret);
+ if (shortname) c->shortname = talloc_typed_strdup(c, shortname);
+ if (type) c->nas_type = talloc_typed_strdup(c, type);
+ if (server) c->server = talloc_typed_strdup(c, server);
+ c->message_authenticator = require_ma;
+
+ return c;
+}
+
+/** Create a new client, consuming all attributes in the control list of the request
+ *
+ * @param clients list to add new client to.
+ * @param request Fake request.
+ * @return a new client on success, else NULL on error.
+ */
+RADCLIENT *client_afrom_request(RADCLIENT_LIST *clients, REQUEST *request)
+{
+ static int cnt;
+ int i, *pi;
+ char **p;
+ RADCLIENT *c;
+ char buffer[128];
+
+ vp_cursor_t cursor;
+ VALUE_PAIR *vp = NULL;
+
+ if (!clients || !request) return NULL;
+
+ /*
+ * Hack for the "dynamic_clients" module.
+ */
+ if (request->client->dynamic) {
+ c = request->client;
+ goto validate;
+ }
+
+ snprintf(buffer, sizeof(buffer), "dynamic%i", cnt++);
+
+ c = talloc_zero(clients, RADCLIENT);
+ c->cs = cf_section_alloc(NULL, "client", buffer);
+ talloc_steal(c, c->cs);
+ c->ipaddr.af = AF_UNSPEC;
+ c->src_ipaddr.af = AF_UNSPEC;
+
+ fr_cursor_init(&cursor, &request->config);
+
+ RDEBUG2("Converting control list to client fields");
+ RINDENT();
+ for (i = 0; dynamic_config[i].name != NULL; i++) {
+ DICT_ATTR const *da;
+ char *strvalue = NULL;
+ CONF_PAIR *cp = NULL;
+
+ da = dict_attrbyname(dynamic_config[i].name);
+ if (!da) {
+ RERROR("Cannot add client %s: attribute \"%s\" is not in the dictionary",
+ ip_ntoh(&request->packet->src_ipaddr, buffer, sizeof(buffer)),
+ dynamic_config[i].name);
+ error:
+ REXDENT();
+ talloc_free(vp);
+ client_free(c);
+ return NULL;
+ }
+
+ fr_cursor_first(&cursor);
+ if (!fr_cursor_next_by_da(&cursor, da, TAG_ANY)) {
+ /*
+ * Not required. Skip it.
+ */
+ if (!dynamic_config[i].dflt) continue;
+
+ RERROR("Cannot add client %s: Required attribute \"%s\" is missing",
+ ip_ntoh(&request->packet->src_ipaddr, buffer, sizeof(buffer)),
+ dynamic_config[i].name);
+ goto error;
+ }
+ vp = fr_cursor_remove(&cursor);
+
+ /*
+ * Freed at the same time as the vp.
+ */
+ strvalue = vp_aprints_value(vp, vp, '\'');
+
+ switch (dynamic_config[i].type) {
+ case PW_TYPE_IPV4_ADDR:
+ if (da->attr == PW_FREERADIUS_CLIENT_IP_ADDRESS) {
+ c->ipaddr.af = AF_INET;
+ c->ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr;
+ c->ipaddr.prefix = 32;
+ cp = cf_pair_alloc(c->cs, "ipv4addr", strvalue, T_OP_SET, T_BARE_WORD, T_BARE_WORD);
+ } else if (da->attr == PW_FREERADIUS_CLIENT_SRC_IP_ADDRESS) {
+#ifdef WITH_UDPFROMTO
+ RDEBUG2("src_ipaddr = %s", strvalue);
+ c->src_ipaddr.af = AF_INET;
+ c->src_ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr;
+ c->src_ipaddr.prefix = 32;
+ cp = cf_pair_alloc(c->cs, "src_ipaddr", strvalue, T_OP_SET, T_BARE_WORD, T_BARE_WORD);
+#else
+ RWARN("Server not built with udpfromto, ignoring FreeRADIUS-Client-Src-IP-Address");
+#endif
+ }
+
+ break;
+
+ case PW_TYPE_IPV6_ADDR:
+ if (da->attr == PW_FREERADIUS_CLIENT_IPV6_ADDRESS) {
+ c->ipaddr.af = AF_INET6;
+ c->ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr;
+ c->ipaddr.prefix = 128;
+ cp = cf_pair_alloc(c->cs, "ipv6addr", strvalue, T_OP_SET, T_BARE_WORD, T_BARE_WORD);
+ } else if (da->attr == PW_FREERADIUS_CLIENT_SRC_IPV6_ADDRESS) {
+#ifdef WITH_UDPFROMTO
+ c->src_ipaddr.af = AF_INET6;
+ c->src_ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr;
+ c->src_ipaddr.prefix = 128;
+ cp = cf_pair_alloc(c->cs, "src_addr", strvalue, T_OP_SET, T_BARE_WORD, T_BARE_WORD);
+#else
+ RWARN("Server not built with udpfromto, ignoring FreeRADIUS-Client-Src-IPv6-Address");
+#endif
+ }
+
+ break;
+
+ case PW_TYPE_IPV4_PREFIX:
+ if (da->attr == PW_FREERADIUS_CLIENT_IP_PREFIX) {
+ c->ipaddr.af = AF_INET;
+ memcpy(&c->ipaddr.ipaddr.ip4addr, &vp->vp_ipv4prefix[2],
+ sizeof(c->ipaddr.ipaddr.ip4addr.s_addr));
+ fr_ipaddr_mask(&c->ipaddr, (vp->vp_ipv4prefix[1] & 0x3f));
+ cp = cf_pair_alloc(c->cs, "ipv4addr", strvalue, T_OP_SET, T_BARE_WORD, T_BARE_WORD);
+ }
+
+ break;
+
+ case PW_TYPE_IPV6_PREFIX:
+ if (da->attr == PW_FREERADIUS_CLIENT_IPV6_PREFIX) {
+ c->ipaddr.af = AF_INET6;
+ memcpy(&c->ipaddr.ipaddr.ip6addr, &vp->vp_ipv6prefix[2],
+ sizeof(c->ipaddr.ipaddr.ip6addr));
+ fr_ipaddr_mask(&c->ipaddr, vp->vp_ipv6prefix[1]);
+ cp = cf_pair_alloc(c->cs, "ipv6addr", strvalue, T_OP_SET, T_BARE_WORD, T_BARE_WORD);
+ }
+
+ break;
+
+ case PW_TYPE_STRING:
+ {
+ CONF_PARSER const *parse;
+
+ /*
+ * Cache pointer to CONF_PAIR buffer in RADCLIENT struct
+ */
+ p = (char **) ((char *) c + dynamic_config[i].offset);
+ if (*p) TALLOC_FREE(*p);
+ if (!vp->vp_strvalue[0]) break;
+
+ /*
+ * We could reuse the CONF_PAIR buff, this just keeps things
+ * consistent between client_afrom_cs, and client_afrom_query.
+ */
+ *p = talloc_strdup(c, vp->vp_strvalue);
+
+ /*
+ * This is fairly nasty... In order to figure out the CONF_PAIR
+ * name associated with a field, find offsets that match between
+ * the dynamic_config CONF_PARSER table, and the client_config
+ * CONF_PARSER table.
+ *
+ * This is so that things that expect to find CONF_PAIRs in the
+ * client CONF_SECTION for fields like 'nas_type' can.
+ */
+ for (parse = client_config; parse->name; parse++) {
+ if (parse->offset == dynamic_config[i].offset) break;
+ }
+
+ if (!parse) break;
+
+ cp = cf_pair_alloc(c->cs, parse->name, strvalue, T_OP_SET, T_BARE_WORD, T_SINGLE_QUOTED_STRING);
+ }
+ break;
+
+ case PW_TYPE_BOOLEAN:
+ {
+ CONF_PARSER const *parse;
+
+ pi = (int *) ((bool *) ((char *) c + dynamic_config[i].offset));
+ *pi = vp->vp_integer;
+
+ /*
+ * Same nastiness as above.
+ */
+ for (parse = client_config; parse->name; parse++) {
+ if (parse->offset == dynamic_config[i].offset) break;
+ }
+ if (!parse) break;
+
+ cp = cf_pair_alloc(c->cs, parse->name, strvalue, T_OP_SET, T_BARE_WORD, T_BARE_WORD);
+ }
+ break;
+
+ default:
+ goto error;
+ }
+
+ if (!cp) {
+ RERROR("Error creating equivalent conf pair for %s", vp->da->name);
+ goto error;
+ }
+
+ if (cf_pair_attr_type(cp) == T_SINGLE_QUOTED_STRING) {
+ RDEBUG2("%s = '%s'", cf_pair_attr(cp), cf_pair_value(cp));
+ } else {
+ RDEBUG2("%s = %s", cf_pair_attr(cp), cf_pair_value(cp));
+ }
+ cf_pair_add(c->cs, cp);
+
+ talloc_free(vp);
+ }
+
+ fr_cursor_first(&cursor);
+ vp = fr_cursor_remove(&cursor);
+ if (vp) {
+ CONF_PAIR *cp;
+
+ do {
+ char *value;
+
+ value = vp_aprints_value(vp, vp, '\'');
+ if (!value) {
+ ERROR("Failed stringifying value of &control:%s", vp->da->name);
+ goto error;
+ }
+
+ if (vp->da->type == PW_TYPE_STRING) {
+ RDEBUG2("%s = '%s'", vp->da->name, value);
+ cp = cf_pair_alloc(c->cs, vp->da->name, value, T_OP_SET,
+ T_BARE_WORD, T_SINGLE_QUOTED_STRING);
+ } else {
+ RDEBUG2("%s = %s", vp->da->name, value);
+ cp = cf_pair_alloc(c->cs, vp->da->name, value, T_OP_SET,
+ T_BARE_WORD, T_BARE_WORD);
+ }
+ cf_pair_add(c->cs, cp);
+
+ talloc_free(vp);
+ } while ((vp = fr_cursor_remove(&cursor)));
+ }
+ REXDENT();
+
+validate:
+ if (c->ipaddr.af == AF_UNSPEC) {
+ RERROR("Cannot add client %s: No IP address was specified.",
+ ip_ntoh(&request->packet->src_ipaddr, buffer, sizeof(buffer)));
+
+ goto error;
+ }
+
+ {
+ fr_ipaddr_t addr;
+
+ /*
+ * Need to apply the same mask as we set for the client
+ * else clients created with FreeRADIUS-Client-IPv6-Prefix
+ * or FreeRADIUS-Client-IPv4-Prefix will fail this check.
+ */
+ addr = request->packet->src_ipaddr;
+ fr_ipaddr_mask(&addr, c->ipaddr.prefix);
+ if (fr_ipaddr_cmp(&addr, &c->ipaddr) != 0) {
+ char buf2[128];
+
+ RERROR("Cannot add client %s: Not in specified subnet %s/%i",
+ ip_ntoh(&request->packet->src_ipaddr, buffer, sizeof(buffer)),
+ ip_ntoh(&c->ipaddr, buf2, sizeof(buf2)), c->ipaddr.prefix);
+ goto error;
+ }
+ }
+
+ if (!c->secret || !*c->secret) {
+ RERROR("Cannot add client %s: No secret was specified",
+ ip_ntoh(&request->packet->src_ipaddr, buffer, sizeof(buffer)));
+ goto error;
+ }
+
+ if (!client_add_dynamic(clients, request->client, c)) {
+ return NULL;
+ }
+
+ if ((c->src_ipaddr.af != AF_UNSPEC) && (c->src_ipaddr.af != c->ipaddr.af)) {
+ RERROR("Cannot add client %s: Client IP and src address are different IP version",
+ ip_ntoh(&request->packet->src_ipaddr, buffer, sizeof(buffer)));
+
+ goto error;
+ }
+
+ return c;
+}
+
+/*
+ * Read a client definition from the given filename.
+ */
+RADCLIENT *client_read(char const *filename, int in_server, int flag)
+{
+ char const *p;
+ RADCLIENT *c;
+ CONF_SECTION *cs;
+ char buffer[256];
+
+ if (!filename) return NULL;
+
+ cs = cf_section_alloc(NULL, "main", NULL);
+ if (!cs) return NULL;
+
+ if (cf_file_read(cs, filename) < 0) {
+ talloc_free(cs);
+ return NULL;
+ }
+
+ cs = cf_section_sub_find(cs, "client");
+ if (!cs) {
+ ERROR("No \"client\" section found in client file");
+ return NULL;
+ }
+
+ c = client_afrom_cs(cs, cs, in_server, false);
+ if (!c) return NULL;
+
+ p = strrchr(filename, FR_DIR_SEP);
+ if (p) {
+ p++;
+ } else {
+ p = filename;
+ }
+
+ if (!flag) return c;
+
+ /*
+ * Additional validations
+ */
+ ip_ntoh(&c->ipaddr, buffer, sizeof(buffer));
+ if (strcmp(p, buffer) != 0) {
+ ERROR("Invalid client definition in %s: IP address %s does not match name %s", filename, buffer, p);
+ client_free(c);
+ return NULL;
+ }
+
+ return c;
+}
+#endif
+
diff --git a/src/main/collectd.c b/src/main/collectd.c
new file mode 100644
index 0000000..77f0db0
--- /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(*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..030416f
--- /dev/null
+++ b/src/main/command.c
@@ -0,0 +1,3622 @@
+/*
+ * 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 <libgen.h>
+#ifdef HAVE_INTTYPES_H
+#include <inttypes.h>
+#endif
+
+#ifdef HAVE_SYS_UN_H
+#include <sys/un.h>
+#ifndef SUN_LEN
+#define SUN_LEN(su) (sizeof(*(su)) - sizeof((su)->sun_path) + strlen((su)->sun_path))
+#endif
+#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) != NULL)) {
+ 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(*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;
+}
+#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,
+ "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 },
+#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..7fe6587
--- /dev/null
+++ b/src/main/conffile.c
@@ -0,0 +1,3810 @@
+/*
+ * 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;
+
+ 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;
+
+ 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));
+ }
+
+ cf_section_parse_init(subcs, (uint8_t *)base + variables[i].offset,
+ (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;
+ }
+
+ ret = cf_section_parse(subcs, (uint8_t *)base + variables[i].offset,
+ (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((int) *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((int) *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((int)*p) ||
+ isdigit((int)*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((int) *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(*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(*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(*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..b5a0eea
--- /dev/null
+++ b/src/main/connection.c
@@ -0,0 +1,1505 @@
+/*
+ * 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 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" },
+ { "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 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..ba214a2
--- /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((int) *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..e5ae41d
--- /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((int) *p)) {
+ *p = toupper(*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..529524d
--- /dev/null
+++ b/src/main/exfile.c
@@ -0,0 +1,544 @@
+/*
+ * 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)
+{
+ int i, found, tries, unused, oldest;
+ uint32_t hash;
+ time_t now;
+ struct stat st;
+
+ 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;
+
+ (void) lseek(found, 0, SEEK_END);
+ 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.
+ */
+ (void) lseek(ef->entries[i].fd, 0, SEEK_END);
+
+ /*
+ * 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..f191393
--- /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((int) *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((int) 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((int) *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((int) 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((int) 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((int) 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..b160d4f
--- /dev/null
+++ b/src/main/listen.c
@@ -0,0 +1,4206 @@
+/*
+ * 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 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
+
+
+ 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;
+ }
+
+#ifdef WITH_STATS
+ /*
+ * Full statistics are available only on a statistics
+ * socket.
+ */
+ if (request->listener->type == RAD_LISTEN_NONE) {
+ request_stats_reply(request);
+ }
+#endif
+
+ 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
+
+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;
+ }
+#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) {
+ /*
+ * 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);
+
+ this->recv = dual_tls_recv;
+ this->send = dual_tls_send;
+ }
+#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;
+
+ /*
+ * 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;
+ }
+#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, &section_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 an authentication response packet
+ */
+static int auth_socket_send(rad_listen_t *listener, REQUEST *request)
+{
+ rad_assert(request->listener == listener);
+ rad_assert(listener->send == auth_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_ACCOUNTING
+/*
+ * Send an accounting response packet (or not)
+ */
+static int acct_socket_send(rad_listen_t *listener, REQUEST *request)
+{
+ rad_assert(request->listener == listener);
+ rad_assert(listener->send == acct_socket_send);
+
+ /*
+ * 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_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;
+}
+#endif
+
+#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
+
+
+static int client_socket_encode(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;
+#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;
+#endif
+
+ if (rad_verify(request->packet, NULL,
+ request->client->secret) < 0) {
+ return -1;
+ }
+
+#ifdef WITH_TLS
+ sock = request->listener->data;
+ rad_assert(sock != NULL);
+
+ /*
+ * 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
+static int proxy_socket_encode(UNUSED rad_listen_t *listener, REQUEST *request)
+{
+ 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)
+{
+ /*
+ * 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, auth_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, auth_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, acct_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, auth_socket_send, /* CoA packets are same as auth */
+ 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));
+#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->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);
+ } 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");
+ }
+
+ /*
+ * 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);
+ home->last_failed_open = now;
+ listen_free(&this);
+ return NULL;
+ }
+
+ sock->connect_timeout = home->connect_timeout;
+
+ this->recv = proxy_tls_recv;
+ this->proxy_send = proxy_tls_send;
+
+#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));
+ 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);
+ home->last_failed_open = now;
+ listen_free(&this);
+ return NULL;
+ }
+
+ 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);
+ DEBUG("%p", &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->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..960a312
--- /dev/null
+++ b/src/main/mainconfig.c
@@ -0,0 +1,1404 @@
+/*
+ * 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;
+
+ for (vp = sock->certs; vp != NULL; vp = vp->next) {
+ if (strcmp(fmt, vp->da->name) == 0) {
+ return vp_prints_value(out, outlen, vp, 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..17988d2
--- /dev/null
+++ b/src/main/map.c
@@ -0,0 +1,1712 @@
+/*
+ * 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((int)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);
+ rad_assert(!attr);
+ 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->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) {
+ while ((dst = fr_cursor_next_by_da(&dst_list, map->lhs->tmpl_da, map->lhs->tmpl_tag))) {
+ if (num-- == 0) break;
+ }
+ } 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(&copy, &vps);
+
+ RDEBUG2("foreach %s ", c->name);
+
+ /*
+ * This is the actual body of the foreach loop
+ */
+ for (vp = fr_cursor_first(&copy);
+ vp != NULL;
+ vp = fr_cursor_next(&copy)) {
+#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;
+
+ 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..e18977d
--- /dev/null
+++ b/src/main/modules.c
@@ -0,0 +1,2299 @@
+/*
+ * 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;
+
+ 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);
+
+ module_instance_free_old(cs, node, when);
+
+ /*
+ * 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..7bafa8c
--- /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((int) *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((int) *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((int) *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((int) *p)) p++; /* skip spaces before condition */
+
+ if (!*p) {
+ return_P("Empty condition is invalid");
+ }
+
+ /*
+ * !COND
+ */
+ if (*p == '!') {
+ p++;
+ c->negate = true;
+ while (isspace((int) *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((int) *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((int)*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((int) *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((int) *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((int) *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((int) *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..410c571
--- /dev/null
+++ b/src/main/process.c
@@ -0,0 +1,6344 @@
+/*
+ * 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
+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)) {
+ 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 &&
+ (request->home_server->state >= HOME_STATE_IS_DEAD)) {
+ 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);
+
+ switch (listener->type) {
+#ifdef WITH_PROXY
+ case RAD_LISTEN_PROXY:
+ limit = &sock->home->limit;
+ break;
+#endif
+
+ case RAD_LISTEN_AUTH:
+#ifdef WITH_ACCOUNTING
+ case RAD_LISTEN_ACCT:
+#endif
+ limit = &sock->limit;
+ break;
+
+ default:
+ return;
+ }
+
+ /*
+ * 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;
+ 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,
+ &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;
+ }
+
+ rad_assert(request->proxy->id >= 0);
+
+ 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 &&
+ (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) {
+ REDEBUG3("Cannot proxy packets unless 'proxy_requests = yes'");
+ 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->state < HOME_STATE_IS_DEAD) 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->state < HOME_STATE_IS_DEAD) 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;
+ }
+
+ rad_assert(request->proxy->id >= 0);
+
+ if (rad_debug_lvl) {
+ struct timeval *response_window;
+
+ response_window = request_response_window(request);
+
+#ifdef WITH_TLS
+ if (request->home_server->tls) {
+ 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->state >= HOME_STATE_IS_DEAD) &&
+ (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->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);
+}
+
+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");
+
+ /*
+ * 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->state >= HOME_STATE_IS_DEAD) ||
+ !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->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 ||
+ (request->home_server->state >= HOME_STATE_IS_DEAD) ||
+ 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 */
+ }
+ }
+}
+
+#ifdef WITH_TCP
+static void listener_free_cb(void *ctx)
+{
+ rad_listen_t *this = talloc_get_type_abort(ctx, rad_listen_t);
+ char buffer[1024];
+
+ if (this->count > 0) {
+ struct timeval when;
+ listen_socket_t *sock = this->data;
+
+ 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);
+ talloc_free(this);
+}
+
+#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];
+
+ ASSERT_MASTER;
+
+ if (this->status == RAD_LISTEN_STATUS_KNOWN) return;
+
+ this->print(this, buffer, sizeof(buffer));
+
+ if (this->status == RAD_LISTEN_STATUS_INIT) {
+ listen_socket_t *sock = this->data;
+
+ rad_assert(sock != NULL);
+ 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->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,
+ &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;
+ listen_socket_t *sock = this->data;
+
+ /*
+ * 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;
+ listen_socket_t *sock = this->data;
+
+ /*
+ * 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 */
+
+ /*
+ * Nuke the socket.
+ */
+ if (this->status == RAD_LISTEN_STATUS_REMOVE_NOW) {
+ int devnull;
+#ifdef WITH_TCP
+ listen_socket_t *sock = this->data;
+ struct timeval when;
+#endif
+
+ /*
+ * 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;
+
+ 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);
+ }
+ 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);
+
+#ifdef WITH_COA_TUNNEL
+ /*
+ * Delete the listener from the set of
+ * listeners by key. This is done early,
+ * so that it won't be used while the
+ * cleanup timers are being run.
+ */
+ if (this->tls) this->dead = true;
+#endif
+ }
+
+ /*
+ * No child threads, clean it up now.
+ */
+ if (!spawn_flag) {
+ ASSERT_MASTER;
+ if (sock->ev) fr_event_delete(el, &sock->ev);
+ listen_free(&this);
+ return;
+ }
+
+ /*
+ * Wait until all requests using this socket are done.
+ */
+ gettimeofday(&when, NULL);
+ when.tv_sec += 3;
+
+ ASSERT_MASTER;
+ if (!fr_event_insert(el, listener_free_cb, this, &when,
+ &(sock->ev))) {
+ rad_panic("Failed to insert event");
+ }
+#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,
+ &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..cf0809c
--- /dev/null
+++ b/src/main/radattr.c
@@ -0,0 +1,1111 @@
+/*
+ * 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 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((int) *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((int) *p)) p++;
+
+ if (!*p) break;
+
+ if(!(c1 = memchr(hextab, tolower((int) p[0]), 16)) ||
+ !(c2 = memchr(hextab, tolower((int) 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((int) *p)) {
+ fprintf(stderr, "Invalid character following attribute "
+ "definition\n");
+ return 0;
+ }
+
+ while (isspace((int) *p)) p++;
+
+ if (*p == '{') {
+ int sublen;
+ char *q;
+
+ length = 0;
+
+ do {
+ while (isspace((int) *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(NULL, input);
+ xlat_exp_t *head;
+
+ slen = xlat_tokenize(fmt, 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);
+ 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((int) *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(NULL, 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());
+ 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(NULL, 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);
+ continue;
+ }
+
+ if (strncmp(p, "$INCLUDE ", 9) == 0) {
+ char *q;
+
+ p += 9;
+ while (isspace((int) *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;
+
+ 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..09d27c8
--- /dev/null
+++ b/src/main/radclient.c
@@ -0,0 +1,1704 @@
+/*
+ * 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,
+ &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;
+}
+
+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((int) *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((int) *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((int) *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((int) 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, &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);
+}
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..06b566d
--- /dev/null
+++ b/src/main/radiusd.c
@@ -0,0 +1,793 @@
+/*
+ * 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);
+ }
+
+ 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..d71a2dd
--- /dev/null
+++ b/src/main/radmin.c
@@ -0,0 +1,774 @@
+/*
+ * 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);
+ break;
+ }
+ }
+
+ /*
+ * 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(&current->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(&current);
+
+ 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(&current);
+
+ /* 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(&current);
+ 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(&current->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(&current);
+ 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(&current);
+
+ 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(&current);
+ REDEBUG("Failed decoding");
+ return;
+ }
+
+ fr_pair_list_sort(&current->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(&current);
+ 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(&current);
+ 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(&current);
+
+ 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(&current->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(&current);
+ }
+
+ 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 = &in;
+ 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..38b1ba9
--- /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`
+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..497c899
--- /dev/null
+++ b/src/main/realms.c
@@ -0,0 +1,3197 @@
+/*
+ * 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 }
+};
+
+
+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((int) *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[] = {
+ { "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;
+}
+
+/** 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;
+ }
+#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, UNUSED CONF_SECTION *cs)
+{
+ /*
+ * 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((int)*p) ||
+ isdigit((int)*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) &&
+ !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->state >= HOME_STATE_IS_DEAD) {
+ 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->state >= HOME_STATE_IS_DEAD) &&
+ (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..8dbf5a6
--- /dev/null
+++ b/src/main/session.c
@@ -0,0 +1,254 @@
+/*
+ * 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 *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;
+
+ INTPAIR(PW_NAS_PORT, nas_port);
+ 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 *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));
+ snprintf(port, sizeof(port), "%u", nas_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, port,
+ user, session_id, NULL);
+#else
+ execl(main_config.checkrad, "checkrad", cl->nas_type, address, port,
+ 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 *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 *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..3694fe6
--- /dev/null
+++ b/src/main/state.c
@@ -0,0 +1,710 @@
+/*
+ * 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
+ */
+ 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..8fe3e4a
--- /dev/null
+++ b/src/main/stats.c
@@ -0,0 +1,1005 @@
+/*
+ * 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)
+{
+ 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;
+
+#undef INC_AUTH
+#define INC_AUTH(_x) radius_auth_stats._x++;request->listener->stats._x++;request->client->auth._x++;
+
+#undef INC_ACCT
+#ifdef WITH_ACCOUNTING
+#define INC_ACCT(_x) radius_acct_stats._x++;request->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++;request->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++;request->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(&request->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) {
+ rad_listen_t *listener = request->listener;
+ 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], pps[2];
+
+ thread_pool_queue_stats(array, pps);
+
+ 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 = pps[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);
+ 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);
+ 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..a9bd63b
--- /dev/null
+++ b/src/main/threads.c
@@ -0,0 +1,1694 @@
+/*
+ * 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
+
+#ifdef HAVE_OPENSSL_CRYPTO_H
+
+/*
+ * 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;
+
+#ifdef HAVE_CRYPTO_SET_ID_CALLBACK
+static unsigned long get_ssl_id(void)
+{
+ unsigned long ret;
+ pthread_t thread = pthread_self();
+
+ if (sizeof(ret) >= sizeof(thread)) {
+ memcpy(&ret, &thread, sizeof(thread));
+ } else {
+ memcpy(&ret, &thread, sizeof(ret));
+ }
+
+ return ret;
+}
+
+/*
+ * Use preprocessor magic to get the right function and argument
+ * to use. This avoids ifdef's through the rest of the code.
+ */
+#if OPENSSL_VERSION_NUMBER < 0x10000000L
+#define ssl_id_function get_ssl_id
+#define set_id_callback CRYPTO_set_id_callback
+
+#else
+static void ssl_id_function(CRYPTO_THREADID *id)
+{
+ CRYPTO_THREADID_set_numeric(id, get_ssl_id());
+}
+#define set_id_callback CRYPTO_THREADID_set_callback
+#endif
+#endif
+
+#ifdef HAVE_CRYPTO_SET_LOCKING_CALLBACK
+static void ssl_locking_function(int mode, int n, UNUSED char const *file, UNUSED int line)
+{
+ if (mode & CRYPTO_LOCK) {
+ pthread_mutex_lock(&(ssl_mutexes[n]));
+ } else {
+ pthread_mutex_unlock(&(ssl_mutexes[n]));
+ }
+}
+#endif
+
+/*
+ * Create the TLS mutexes.
+ */
+int tls_mutexes_init(void)
+{
+ int i;
+
+ ssl_mutexes = rad_malloc(CRYPTO_num_locks() * sizeof(pthread_mutex_t));
+ if (!ssl_mutexes) {
+ ERROR("Error allocating memory for SSL mutexes!");
+ return -1;
+ }
+
+ for (i = 0; i < CRYPTO_num_locks(); i++) {
+ pthread_mutex_init(&(ssl_mutexes[i]), NULL);
+ }
+
+#ifdef HAVE_CRYPTO_SET_ID_CALLBACK
+ set_id_callback(ssl_id_function);
+#endif
+#ifdef HAVE_CRYPTO_SET_LOCKING_CALLBACK
+ CRYPTO_set_locking_callback(ssl_locking_function);
+#endif
+
+ return 0;
+}
+#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???
+ */
+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;
+#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_create(NULL, 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;
+}
+
+
+/*
+ * 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
+ talloc_free(thread_pool.queue[i]);
+#else
+ fr_fifo_free(thread_pool.fifo[i]);
+#endif
+ }
+
+#ifdef WNOHANG
+ fr_hash_table_free(thread_pool.waiters);
+#endif
+
+#ifdef HAVE_OPENSSL_CRYPTO_H
+ /*
+ * We're no longer threaded. Remove the mutexes and free
+ * the memory.
+ */
+#ifdef HAVE_CRYPTO_SET_ID_CALLBACK
+ set_id_callback(NULL);
+#endif
+#ifdef HAVE_CRYPTO_SET_LOCKING_CALLBACK
+ CRYPTO_set_locking_callback(NULL);
+#endif
+
+ free(ssl_mutexes);
+#endif
+
+#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;
+ }
+}
+#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..bd0a411
--- /dev/null
+++ b/src/main/tls.c
@@ -0,0 +1,5350 @@
+/*
+ * 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((int) c) || isdigit((int) c) || isspace((int) 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;
+ 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;
+
+ 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:
+ 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 TTLS, PEAP, and 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("!! FreeRADIUS only supports TLS 1.3 for special builds of wpa_supplicant and Windows");
+ WARN("!! This limitation is likely to change in late 2021.");
+ WARN("!! If you are using this version of FreeRADIUS after 2021, you will probably need to upgrade");
+ 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
+ },
+
+ { "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
+ },
+
+ { "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 {
+ 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
+ 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
+
+/** 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(bool spawn_flag, 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 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;
+ }
+
+#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 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_path) {
+ if ((certstore = fr_init_x509_store(conf)) == NULL ) return NULL;
+ SSL_CTX_set_cert_store(ctx, certstore);
+ }
+
+ 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)
+ 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 {
+ /*
+ * 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
+
+ /*
+ * 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->is_init_finished) && (ssn->dirty_out.used == 0)) {
+ 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 */
+
+ RDEBUG2("(TLS) Peer ACKed our handshake fragment");
+ /* Fragmentation handler, send next fragment */
+ return FR_TLS_REQUEST;
+
+ 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..5995fa3
--- /dev/null
+++ b/src/main/tls_listen.c
@@ -0,0 +1,1347 @@
+/*
+ * 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) 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()
+ */
+}
+
+static int CC_HINT(nonnull) tls_socket_write(rad_listen_t *listener, REQUEST *request)
+{
+ uint8_t *p;
+ ssize_t rcode;
+ listen_socket_t *sock = listener->data;
+
+ p = sock->ssn->dirty_out.data;
+
+ while (p < (sock->ssn->dirty_out.data + sock->ssn->dirty_out.used)) {
+ RDEBUG3("(TLS) Writing to socket %d", listener->fd);
+ rcode = write(listener->fd, p,
+ (sock->ssn->dirty_out.data + sock->ssn->dirty_out.used) - p);
+ if (rcode <= 0) {
+ RDEBUG("(TLS) Error writing to socket: %s", fr_syserror(errno));
+
+ tls_socket_close(listener);
+ return 0;
+ }
+ p += rcode;
+ }
+
+ sock->ssn->dirty_out.used = 0;
+
+ return 1;
+}
+
+/*
+ * 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) 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) {
+ tls_socket_write(listener, request);
+ 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;
+ 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:
+ status = tls_application_data(sock->ssn, request);
+ RDEBUG3("(TLS) Application data status %d", status);
+
+ 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);
+
+ 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);
+
+ tls_socket_write(listener, request);
+ }
+ 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);
+
+ tls_socket_write(listener, request);
+ }
+ 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");
+ goto fail;
+ }
+
+ ret = SSL_connect(sock->ssn->ssl);
+ if (ret < 0) {
+ switch (SSL_get_error(sock->ssn->ssl, ret)) {
+ default:
+ break;
+
+ case SSL_ERROR_WANT_READ:
+ case SSL_ERROR_WANT_WRITE:
+ return 0;
+ }
+ }
+
+ if (ret <= 0) {
+ tls_error_io_log(NULL, sock->ssn, ret, "Failed in " STRINGIFY(__FUNCTION__) " (SSL_connect)");
+
+ fail:
+ SSL_shutdown(sock->ssn->ssl);
+ TALLOC_FREE(sock->ssn);
+
+ return -1;
+ }
+
+ sock->ssn->connected = true;
+ return 1;
+}
+
+
+#ifdef WITH_PROXY
+/*
+ * 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;
+ }
+
+ /*
+ * 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:
+ case SSL_ERROR_WANT_WRITE:
+ return 0; /* do some more work later */
+
+ 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;
+
+ default:
+ tls_error_log(NULL, "Failed in proxy receive");
+
+ 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) {
+ switch (SSL_get_error(sock->ssn->ssl, rcode)) {
+ case SSL_ERROR_WANT_READ:
+ case SSL_ERROR_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;
+ default:
+ 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;
+
+ DEBUG3("Proxy SSL socket has data to read");
+ PTHREAD_MUTEX_LOCK(&sock->mutex);
+ data_len = proxy_tls_read(listener);
+ PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+
+ if (data_len < 0) {
+ DEBUG("Closing TLS socket to home server");
+ PTHREAD_MUTEX_LOCK(&sock->mutex);
+ tls_socket_close(listener);
+ PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+ return 0;
+ }
+
+ 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);
+
+ /*
+ * 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);
+ }
+
+ if (!sock->ssn->connected) {
+ PTHREAD_MUTEX_LOCK(&sock->mutex);
+ rcode = try_connect(sock);
+ PTHREAD_MUTEX_UNLOCK(&sock->mutex);
+ if (rcode <= 0) return rcode;
+ }
+
+ DEBUG3("Proxy is writing %u bytes to SSL",
+ (unsigned int) request->proxy->data_len);
+ PTHREAD_MUTEX_LOCK(&sock->mutex);
+ 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:
+ case SSL_ERROR_WANT_READ:
+ case SSL_ERROR_WANT_WRITE:
+ break; /* let someone else retry */
+
+ default:
+ tls_error_log(NULL, "Failed in proxy send");
+ 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;
+}
+
+#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;
+ }
+
+ 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:
+ break; /* let someone else retry */
+
+ default:
+ tls_error_log(NULL, "Failed in proxy send");
+ 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..abcd768
--- /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((int) *d)) {
+ while (isdigit((int) *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..50935c9
--- /dev/null
+++ b/src/main/unittest.c
@@ -0,0 +1,973 @@
+/*
+ * 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 */
+}
+
+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((int) *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..c7c7659
--- /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()
+{
+ 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..583686a
--- /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((int) *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((int) *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((int) *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((int) *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((int) *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((int) *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((int) *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((int) *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((int) *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((int) *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((int) *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((int) *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((int) *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((int) *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((int) *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);
+}