diff options
Diffstat (limited to 'src/main/command.c')
-rw-r--r-- | src/main/command.c | 3622 |
1 files changed, 3622 insertions, 0 deletions
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 */ |