/* * 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 */ RCSID("$Id$") #include #include #include #include #include #ifdef HAVE_GETOPT_H # include #endif #ifdef HAVE_SYS_STAT_H # include #endif #ifndef READLINE_MAX_HISTORY_LINES # define READLINE_MAX_HISTORY_LINES 1000 #endif #ifdef HAVE_LIBREADLINE # include #if defined(HAVE_READLINE_READLINE_H) # include # define USE_READLINE (1) #elif defined(HAVE_READLINE_H) # include # define USE_READLINE (1) #endif /* !defined(HAVE_READLINE_H) */ #ifdef HAVE_READLINE_HISTORY # if defined(HAVE_READLINE_HISTORY_H) # include # define USE_READLINE_HISTORY (1) # elif defined(HAVE_HISTORY_H) # include # 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 #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 Set main dictionary directory (defaults to " DICTDIR ").\n"); fprintf(output, " -e command Execute 'command' and then exit.\n"); fprintf(output, " -E Echo commands as they are being executed.\n"); fprintf(output, " -f socket_file Open socket_file directly, without reading radius.conf\n"); fprintf(output, " -h Print usage help information.\n"); fprintf(output, " -i input_file Read commands from 'input_file'.\n"); fprintf(output, " -n name Read raddb/name.conf instead of raddb/radiusd.conf\n"); fprintf(output, " -q Quiet mode.\n"); fprintf(output, " -v Show program version information.\n"); exit(status); } static int client_socket(char const *server) { int sockfd; uint16_t port; fr_ipaddr_t ipaddr; char *p, buffer[1024]; strlcpy(buffer, server, sizeof(buffer)); p = strchr(buffer, ':'); if (!p) { port = PW_RADMIN_PORT; } else { port = atoi(p + 1); *p = '\0'; } if (ip_hton(&ipaddr, AF_INET, buffer, false) < 0) { fprintf(stderr, "%s: Failed looking up host %s: %s\n", progname, buffer, fr_syserror(errno)); exit(1); } sockfd = fr_socket_client_tcp(NULL, &ipaddr, port, false); if (sockfd < 0) { fprintf(stderr, "%s: Failed opening socket %s: %s\n", progname, server, fr_syserror(errno)); exit(1); } return sockfd; } static ssize_t do_challenge(int sockfd) { ssize_t r; fr_channel_type_t channel; uint8_t challenge[16]; challenge[0] = 0; /* * When connecting over a socket, the server challenges us. */ r = fr_channel_read(sockfd, &channel, challenge, sizeof(challenge)); if (r <= 0) return r; if ((r != 16) || (channel != FR_CHANNEL_AUTH_CHALLENGE)) { fprintf(stderr, "%s: Failed to read challenge.\n", progname); exit(1); } fr_hmac_md5(challenge, (uint8_t const *) secret, strlen(secret), challenge, sizeof(challenge)); r = fr_channel_write(sockfd, FR_CHANNEL_AUTH_RESPONSE, challenge, sizeof(challenge)); if (r <= 0) return r; /* * If the server doesn't like us, he just closes the * socket. So we don't look for an ACK. */ return r; } /* * Returns -1 on error. 0 on connection failed. +1 on OK. */ static ssize_t run_command(int sockfd, char const *command, char *buffer, size_t bufsize) { ssize_t r; uint32_t status; fr_channel_type_t channel; if (echo) { fprintf(stdout, "%s\n", command); } /* * Write the text to the socket. */ r = fr_channel_write(sockfd, FR_CHANNEL_STDIN, command, strlen(command)); if (r <= 0) return r; while (true) { r = fr_channel_read(sockfd, &channel, buffer, bufsize - 1); if (r <= 0) return r; buffer[r] = '\0'; /* for C strings */ switch (channel) { case FR_CHANNEL_STDOUT: fprintf(stdout, "%s", buffer); break; case FR_CHANNEL_STDERR: fprintf(stderr, "ERROR: %s", buffer); break; case FR_CHANNEL_CMD_STATUS: if (r < 4) return 1; memcpy(&status, buffer, sizeof(status)); status = ntohl(status); return status; default: fprintf(stderr, "Unexpected response\n"); return -1; } } /* never gets here */ } static int do_connect(int *out, char const *file, char const *server) { int sockfd; ssize_t r; fr_channel_type_t channel; char buffer[65536]; uint32_t magic; /* * Close stale file descriptors */ if (*out != -1) { close(*out); *out = -1; } if (file) { /* * FIXME: Get destination from command line, if possible? */ sockfd = fr_socket_client_unix(file, false); if (sockfd < 0) { fr_perror("radmin"); if (errno == ENOENT) { fprintf(stderr, "Perhaps you need to run the commands:"); fprintf(stderr, "\tcd /etc/raddb\n"); fprintf(stderr, "\tln -s sites-available/control-socket " "sites-enabled/control-socket\n"); fprintf(stderr, "and then re-start the server?\n"); } return -1; } } else { sockfd = client_socket(server); } /* * Only works for BSD, but Linux allows us * to mask SIGPIPE, so that's fine. */ #ifdef SO_NOSIGPIPE { int set = 1; setsockopt(sockfd, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int)); } #endif /* * Set up the initial header data. */ magic = 0xf7eead16; magic = htonl(magic); memcpy(buffer, &magic, sizeof(magic)); memset(buffer + sizeof(magic), 0, sizeof(magic)); r = fr_channel_write(sockfd, FR_CHANNEL_INIT_ACK, buffer, 8); if (r <= 0) { do_close: fprintf(stderr, "%s: Error in socket: %s\n", progname, fr_syserror(errno)); close(sockfd); return -1; } r = fr_channel_read(sockfd, &channel, buffer + 8, 8); if (r <= 0) goto do_close; if ((r != 8) || (channel != FR_CHANNEL_INIT_ACK) || (memcmp(buffer, buffer + 8, 8) != 0)) { fprintf(stderr, "%s: Incompatible versions\n", progname); close(sockfd); return -1; } if (server && secret) { r = do_challenge(sockfd); if (r <= 0) goto do_close; } *out = sockfd; return 0; } #define MAX_COMMANDS (4) int main(int argc, char **argv) { int argval; bool quiet = false; int sockfd = -1; char *line = NULL; ssize_t len; char const *file = NULL; char const *name = "radiusd"; char *p, buffer[65536]; char const *input_file = NULL; FILE *inputfp = stdin; char const *server = NULL; char const *radius_dir = RADIUS_DIR; char const *dict_dir = DICTDIR; #ifdef USE_READLINE_HISTORY char history_file[PATH_MAX]; #endif char *commands[MAX_COMMANDS]; int num_commands = -1; int exit_status = EXIT_SUCCESS; #ifndef NDEBUG if (fr_fault_setup(getenv("PANIC_ACTION"), argv[0]) < 0) { fr_perror("radmin"); exit(EXIT_FAILURE); } #endif talloc_set_log_stderr(); if ((progname = strrchr(argv[0], FR_DIR_SEP)) == NULL) { progname = argv[0]; } else { progname++; } while ((argval = getopt(argc, argv, "d:D:hi:e:Ef:n:qs:vS")) != EOF) { switch (argval) { case 'd': if (file) { fprintf(stderr, "%s: -d and -f cannot be used together.\n", progname); exit(1); } if (server) { fprintf(stderr, "%s: -d and -s cannot be used together.\n", progname); exit(1); } radius_dir = optarg; break; case 'D': dict_dir = optarg; break; case 'e': num_commands++; /* starts at -1 */ if (num_commands >= MAX_COMMANDS) { fprintf(stderr, "%s: Too many '-e'\n", progname); exit(1); } commands[num_commands] = optarg; break; case 'E': echo = true; break; case 'f': radius_dir = NULL; file = optarg; break; default: case 'h': usage(0); /* never returns */ case 'i': #ifdef __clang_analyzer__ if (!optarg) exit(1); #endif if (strcmp(optarg, "-") != 0) { input_file = optarg; } quiet = true; break; case 'n': name = optarg; break; case 'q': quiet = true; break; case 's': if (file) { fprintf(stderr, "%s: -s and -f cannot be used together.\n", progname); usage(1); } radius_dir = NULL; server = optarg; break; case 'S': secret = NULL; break; case 'v': printf("%s\n", radmin_version); exit(EXIT_SUCCESS); } } /* * Mismatch between the binary and the libraries it depends on */ if (fr_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) { fr_perror("radmin"); exit(1); } if (radius_dir) { int rcode; CONF_SECTION *cs, *subcs; uid_t uid; gid_t gid; char const *uid_name = NULL; char const *gid_name = NULL; struct passwd *pwd; struct group *grp; file = NULL; /* MUST read it from the conffile now */ snprintf(buffer, sizeof(buffer), "%s/%s.conf", radius_dir, name); /* * Need to read in the dictionaries, else we may get * validation errors when we try and parse the config. */ if (dict_init(dict_dir, RADIUS_DICTIONARY) < 0) { fr_perror("radmin"); exit(64); } if (dict_read(radius_dir, RADIUS_DICTIONARY) == -1) { fr_perror("radmin"); exit(64); } cs = cf_section_alloc(NULL, "main", NULL); if (!cs) exit(1); if (cf_file_read(cs, buffer) < 0) { fprintf(stderr, "%s: Errors reading or parsing %s\n", progname, buffer); talloc_free(cs); usage(1); } uid = getuid(); gid = getgid(); subcs = NULL; while ((subcs = cf_subsection_find_next(cs, subcs, "listen")) != NULL) { char const *value; CONF_PAIR *cp = cf_pair_find(subcs, "type"); if (!cp) continue; value = cf_pair_value(cp); if (!value) continue; if (strcmp(value, "control") != 0) continue; /* * Now find the socket name (sigh) */ rcode = cf_item_parse(subcs, "socket", FR_ITEM_POINTER(PW_TYPE_STRING, &file), NULL); if (rcode < 0) { fprintf(stderr, "%s: Failed parsing listen section 'socket'\n", progname); exit(1); } if (!file) { fprintf(stderr, "%s: No path given for socket\n", progname); usage(1); } /* * If we're root, just use the first one we find */ if (uid == 0) break; /* * Check UID and GID. */ rcode = cf_item_parse(subcs, "uid", FR_ITEM_POINTER(PW_TYPE_STRING, &uid_name), NULL); if (rcode < 0) { fprintf(stderr, "%s: Failed parsing listen section 'uid'\n", progname); exit(1); } if (!uid_name) break; pwd = getpwnam(uid_name); if (!pwd) { fprintf(stderr, "%s: Failed getting UID for user %s: %s\n", progname, uid_name, strerror(errno)); exit(1); } if (uid != pwd->pw_uid) continue; rcode = cf_item_parse(subcs, "gid", FR_ITEM_POINTER(PW_TYPE_STRING, &gid_name), NULL); if (rcode < 0) { fprintf(stderr, "%s: Failed parsing listen section 'gid'\n", progname); exit(1); } if (!gid_name) break; grp = getgrnam(gid_name); if (!grp) { fprintf(stderr, "%s: Failed getting GID for group %s: %s\n", progname, gid_name, strerror(errno)); exit(1); } if (gid != grp->gr_gid) continue; break; } if (!file) { fprintf(stderr, "%s: Could not find control socket in %s\n", progname, buffer); exit(1); } } if (input_file) { inputfp = fopen(input_file, "r"); if (!inputfp) { fprintf(stderr, "%s: Failed opening %s: %s\n", progname, input_file, fr_syserror(errno)); exit(1); } } if (!file && !server) { fprintf(stderr, "%s: Must use one of '-d' or '-f' or '-s'\n", progname); exit(1); } /* * Check if stdin is a TTY only if input is from stdin */ if (input_file && !quiet && !isatty(STDIN_FILENO)) quiet = true; #ifdef USE_READLINE if (!quiet) { #ifdef USE_READLINE_HISTORY using_history(); stifle_history(READLINE_MAX_HISTORY_LINES); snprintf(history_file, sizeof(history_file), "%s/%s", getenv("HOME"), ".radmin_history"); read_history(history_file); #endif rl_bind_key('\t', rl_insert); } #endif /* * Prevent SIGPIPEs from terminating the process */ signal(SIGPIPE, SIG_IGN); if (do_connect(&sockfd, file, server) < 0) exit(1); /* * Run one command. */ if (num_commands >= 0) { int i; for (i = 0; i <= num_commands; i++) { len = run_command(sockfd, commands[i], buffer, sizeof(buffer)); if (len < 0) exit(1); if (len == FR_CHANNEL_FAIL) exit_status = EXIT_FAILURE; } exit(exit_status); } if (!quiet) { printf("%s - FreeRADIUS Server administration tool.\n", radmin_version); printf("Copyright (C) 2008-2019 The FreeRADIUS server project and contributors.\n"); printf("There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A\n"); printf("PARTICULAR PURPOSE.\n"); printf("You may redistribute copies of FreeRADIUS under the terms of the\n"); printf("GNU General Public License v2.\n"); } /* * FIXME: Do login? */ while (1) { int retries; #ifndef USE_READLINE if (!quiet) { printf("radmin> "); fflush(stdout); } #else if (!quiet) { line = readline("radmin> "); if (!line) break; if (!*line) { free(line); continue; } #ifdef USE_READLINE_HISTORY add_history(line); write_history(history_file); #endif } else /* quiet, or no readline */ #endif { line = fgets(buffer, sizeof(buffer), inputfp); if (!line) break; p = strchr(buffer, '\n'); if (!p) { fprintf(stderr, "%s: Input line too long\n", progname); exit(1); } *p = '\0'; /* * Strip off leading spaces. */ for (p = line; *p != '\0'; p++) { if ((p[0] == ' ') || (p[0] == '\t')) { line = p + 1; continue; } if (p[0] == '#') { line = NULL; break; } break; } /* * Comments: keep going. */ if (!line) continue; /* * Strip off CR / LF */ for (p = line; *p != '\0'; p++) { if ((p[0] == '\r') || (p[0] == '\n')) { p[0] = '\0'; break; } } } if (strcmp(line, "reconnect") == 0) { if (do_connect(&sockfd, file, server) < 0) exit(1); line = NULL; continue; } if (strncmp(line, "secret ", 7) == 0) { if (!secret) { secret = line + 7; do_challenge(sockfd); } line = NULL; continue; } /* * Exit, done, etc. */ if ((strcmp(line, "exit") == 0) || (strcmp(line, "quit") == 0)) { break; } if (server && !secret) { fprintf(stderr, "ERROR: You must enter 'secret ' 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; }