diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-09 13:16:35 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-09 13:16:35 +0000 |
commit | e2bbf175a2184bd76f6c54ccf8456babeb1a46fc (patch) | |
tree | f0b76550d6e6f500ada964a3a4ee933a45e5a6f1 /vtysh/vtysh_main.c | |
parent | Initial commit. (diff) | |
download | frr-e2bbf175a2184bd76f6c54ccf8456babeb1a46fc.tar.xz frr-e2bbf175a2184bd76f6c54ccf8456babeb1a46fc.zip |
Adding upstream version 9.1.upstream/9.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vtysh/vtysh_main.c')
-rw-r--r-- | vtysh/vtysh_main.c | 747 |
1 files changed, 747 insertions, 0 deletions
diff --git a/vtysh/vtysh_main.c b/vtysh/vtysh_main.c new file mode 100644 index 0000000..c57e8c7 --- /dev/null +++ b/vtysh/vtysh_main.c @@ -0,0 +1,747 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Virtual terminal interface shell. + * Copyright (C) 2000 Kunihiro Ishiguro + */ + +#include <zebra.h> + +#include <sys/un.h> +#include <setjmp.h> +#include <pwd.h> +#include <sys/file.h> +#include <unistd.h> + +/* readline carries some ancient definitions around */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wstrict-prototypes" +#include <readline/readline.h> +#include <readline/history.h> +#pragma GCC diagnostic pop + +/* + * The append_history function only appears in newer versions + * of the readline library it appears like. Since we don't + * need this just silently ignore the code on these + * ancient platforms. + */ +#if !defined HAVE_APPEND_HISTORY +#define append_history(A, B) +#endif + +#include <lib/version.h> +#include "getopt.h" +#include "command.h" +#include "memory.h" +#include "linklist.h" +#include "libfrr.h" +#include "ferr.h" +#include "lib_errors.h" + +#include "vtysh/vtysh.h" +#include "vtysh/vtysh_user.h" + +/* VTY shell program name. */ +char *progname; + +/* SUID mode */ +static uid_t elevuid, realuid; +static gid_t elevgid, realgid; + +#define VTYSH_CONFIG_NAME "vtysh.conf" +#define FRR_CONFIG_NAME "frr.conf" + +/* Configuration file name and directory. */ +static char vtysh_config[MAXPATHLEN * 3]; +char frr_config[MAXPATHLEN * 3]; +char vtydir[MAXPATHLEN]; +static char history_file[MAXPATHLEN]; + +/* Flag for indicate executing child command. */ +int execute_flag = 0; + +/* Flag to indicate if in user/unprivileged mode. */ +int user_mode; + +/* Master of threads. */ +struct event_loop *master; + +/* Command logging */ +FILE *logfile; + +static void vtysh_rl_callback(char *line_read) +{ + HIST_ENTRY *last; + + rl_callback_handler_remove(); + + if (!line_read) { + vtysh_loop_exited = true; + return; + } + + /* If the line has any text in it, save it on the history. But only if + * last command in history isn't the same one. + */ + if (*line_read) { + using_history(); + last = previous_history(); + if (!last || strcmp(last->line, line_read) != 0) { + add_history(line_read); + append_history(1, history_file); + } + } + + vtysh_execute(line_read); + + if (!vtysh_loop_exited) + rl_callback_handler_install(vtysh_prompt(), vtysh_rl_callback); + + free(line_read); +} + +/* SIGTSTP handler. This function care user's ^Z input. */ +static void sigtstp(int sig) +{ + rl_callback_handler_remove(); + + /* Execute "end" command. */ + vtysh_execute("end"); + + if (!vtysh_loop_exited) + rl_callback_handler_install(vtysh_prompt(), vtysh_rl_callback); + + /* Initialize readline. */ + rl_initialize(); + printf("\n"); + rl_forced_update_display(); +} + +/* SIGINT handler. This function care user's ^Z input. */ +static void sigint(int sig) +{ + /* Check this process is not child process. */ + if (!execute_flag) { + rl_initialize(); + printf("\n"); + rl_forced_update_display(); + } +} + +/* Signale wrapper for vtysh. We don't use sigevent because + * vtysh doesn't use threads. TODO */ +static void vtysh_signal_set(int signo, void (*func)(int)) +{ + struct sigaction sig; + struct sigaction osig; + + sig.sa_handler = func; + sigemptyset(&sig.sa_mask); + sig.sa_flags = 0; +#ifdef SA_RESTART + sig.sa_flags |= SA_RESTART; +#endif /* SA_RESTART */ + + sigaction(signo, &sig, &osig); +} + +/* Initialization of signal handles. */ +static void vtysh_signal_init(void) +{ + vtysh_signal_set(SIGINT, sigint); + vtysh_signal_set(SIGTSTP, sigtstp); + vtysh_signal_set(SIGPIPE, SIG_IGN); +} + +/* Help information display. */ +static void usage(int status) +{ + if (status != 0) + fprintf(stderr, "Try `%s --help' for more information.\n", + progname); + else + printf("Usage : %s [OPTION...]\n\n" + "Integrated shell for FRR (version " FRR_VERSION + "). \n" + "Configured with:\n " FRR_CONFIG_ARGS + "\n\n" + "-b, --boot Execute boot startup configuration\n" + "-c, --command Execute argument as command\n" + "-d, --daemon Connect only to the specified daemon\n" + "-f, --inputfile Execute commands from specific file and exit\n" + "-E, --echo Echo prompt and command in -c mode\n" + "-C, --dryrun Check configuration for validity and exit\n" + "-m, --markfile Mark input file with context end\n" + " --vty_socket Override vty socket path\n" + " --config_dir Override config directory path\n" + "-N --pathspace Insert prefix into config & socket paths\n" + "-u --user Run as an unprivileged user\n" + "-w, --writeconfig Write integrated config (frr.conf) and exit\n" + "-H, --histfile Override history file\n" + "-t, --timestamp Print a timestamp before going to shell or reading the configuration\n" + " --no-fork Don't fork clients to handle daemons (slower for large configs)\n" + "-h, --help Display this help and exit\n\n" + "Note that multiple commands may be executed from the command\n" + "line by passing multiple -c args, or by embedding linefeed\n" + "characters in one or more of the commands.\n\n" + "Report bugs to %s\n", + progname, FRR_BUG_ADDRESS); + + exit(status); +} + +/* VTY shell options, we use GNU getopt library. */ +#define OPTION_VTYSOCK 1000 +#define OPTION_CONFDIR 1001 +#define OPTION_NOFORK 1002 +struct option longopts[] = { + {"boot", no_argument, NULL, 'b'}, + /* For compatibility with older zebra/quagga versions */ + {"eval", required_argument, NULL, 'e'}, + {"command", required_argument, NULL, 'c'}, + {"daemon", required_argument, NULL, 'd'}, + {"vty_socket", required_argument, NULL, OPTION_VTYSOCK}, + {"config_dir", required_argument, NULL, OPTION_CONFDIR}, + {"inputfile", required_argument, NULL, 'f'}, + {"histfile", required_argument, NULL, 'H'}, + {"echo", no_argument, NULL, 'E'}, + {"dryrun", no_argument, NULL, 'C'}, + {"help", no_argument, NULL, 'h'}, + {"noerror", no_argument, NULL, 'n'}, + {"mark", no_argument, NULL, 'm'}, + {"writeconfig", no_argument, NULL, 'w'}, + {"pathspace", required_argument, NULL, 'N'}, + {"user", no_argument, NULL, 'u'}, + {"timestamp", no_argument, NULL, 't'}, + {"no-fork", no_argument, NULL, OPTION_NOFORK}, + {0}}; + +bool vtysh_loop_exited; + +static struct event *vtysh_rl_read_thread; + +static void vtysh_rl_read(struct event *thread) +{ + bool *suppress_warnings = EVENT_ARG(thread); + + event_add_read(master, vtysh_rl_read, suppress_warnings, STDIN_FILENO, + &vtysh_rl_read_thread); + rl_callback_read_char(); +} + +/* Read a string, and return a pointer to it. Returns NULL on EOF. */ +static void vtysh_rl_run(void) +{ + struct event thread; + bool suppress_warnings = true; + + master = event_master_create(NULL); + + rl_callback_handler_install(vtysh_prompt(), vtysh_rl_callback); + event_add_read(master, vtysh_rl_read, &suppress_warnings, STDIN_FILENO, + &vtysh_rl_read_thread); + + while (!vtysh_loop_exited && event_fetch(master, &thread)) + event_call(&thread); + + if (!vtysh_loop_exited) + rl_callback_handler_remove(); + + event_master_free(master); +} + +static void log_it(const char *line) +{ + time_t t = time(NULL); + struct tm tmp; + const char *user = getenv("USER"); + char tod[64]; + + localtime_r(&t, &tmp); + if (!user) + user = "boot"; + + strftime(tod, sizeof(tod), "%Y%m%d-%H:%M.%S", &tmp); + + fprintf(logfile, "%s:%s %s\n", tod, user, line); +} + +static int flock_fd; + +static void vtysh_flock_config(const char *flock_file) +{ + int count = 0; + + flock_fd = open(flock_file, O_RDONLY, 0644); + if (flock_fd < 0) { + fprintf(stderr, "Unable to create lock file: %s, %s\n", + flock_file, safe_strerror(errno)); + return; + } + + while (count < 400 && (flock(flock_fd, LOCK_EX | LOCK_NB) < 0)) { + count++; + usleep(500000); + } + + if (count >= 400) + fprintf(stderr, + "Flock of %s failed, continuing this may cause issues\n", + flock_file); +} + +static void vtysh_unflock_config(void) +{ + flock(flock_fd, LOCK_UN); + close(flock_fd); +} + +void suid_on(void) +{ + if (elevuid != realuid && seteuid(elevuid)) { + perror("seteuid(on)"); + exit(1); + } + if (elevgid != realgid && setegid(elevgid)) { + perror("setegid(on)"); + exit(1); + } +} + +void suid_off(void) +{ + if (elevuid != realuid && seteuid(realuid)) { + perror("seteuid(off)"); + exit(1); + } + if (elevgid != realgid && setegid(realgid)) { + perror("setegid(off)"); + exit(1); + } +} + +/* VTY shell main routine. */ +int main(int argc, char **argv, char **env) +{ + char *p; + int opt; + int dryrun = 0; + int boot_flag = 0; + bool ts_flag = false; + bool no_fork = false; + const char *daemon_name = NULL; + const char *inputfile = NULL; + struct cmd_rec { + char *line; + struct cmd_rec *next; + } *cmd = NULL; + struct cmd_rec *tail = NULL; + int echo_command = 0; + int no_error = 0; + int markfile = 0; + int writeconfig = 0; + int ret = 0; + char *homedir = NULL; + int ditch_suid = 0; + char sysconfdir[MAXPATHLEN]; + const char *pathspace_arg = NULL; + char pathspace[MAXPATHLEN] = ""; + const char *histfile = NULL; + const char *histfile_env = getenv("VTYSH_HISTFILE"); + + /* SUID: drop down to calling user & go back up when needed */ + elevuid = geteuid(); + elevgid = getegid(); + realuid = getuid(); + realgid = getgid(); + suid_off(); + + user_mode = 0; /* may be set in options processing */ + + /* Preserve name of myself. */ + progname = ((p = strrchr(argv[0], '/')) ? ++p : argv[0]); + + strlcpy(sysconfdir, frr_sysconfdir, sizeof(sysconfdir)); + + frr_init_vtydir(); + strlcpy(vtydir, frr_vtydir, sizeof(vtydir)); + + /* Option handling. */ + while (1) { + opt = getopt_long(argc, argv, "be:c:d:nf:H:mEhCwN:ut", longopts, + 0); + + if (opt == EOF) + break; + + switch (opt) { + case 0: + break; + case 'b': + boot_flag = 1; + break; + case 'e': + case 'c': { + struct cmd_rec *cr; + cr = XMALLOC(MTYPE_TMP, sizeof(*cr)); + cr->line = optarg; + cr->next = NULL; + if (tail) + tail->next = cr; + else + cmd = cr; + tail = cr; + } break; + case OPTION_VTYSOCK: + ditch_suid = 1; /* option disables SUID */ + strlcpy(vtydir, optarg, sizeof(vtydir)); + break; + case OPTION_CONFDIR: + ditch_suid = 1; /* option disables SUID */ + snprintf(sysconfdir, sizeof(sysconfdir), "%s/", optarg); + break; + case OPTION_NOFORK: + no_fork = true; + break; + case 'N': + if (strchr(optarg, '/') || strchr(optarg, '.')) { + fprintf(stderr, + "slashes or dots are not permitted in the --pathspace option.\n"); + exit(1); + } + pathspace_arg = optarg; + snprintf(pathspace, sizeof(pathspace), "%s/", optarg); + break; + case 'd': + daemon_name = optarg; + break; + case 'f': + inputfile = optarg; + break; + case 'm': + markfile = 1; + break; + case 'n': + no_error = 1; + break; + case 'E': + echo_command = 1; + break; + case 'C': + dryrun = 1; + break; + case 'u': + user_mode = 1; + break; + case 't': + ts_flag = true; + break; + case 'w': + writeconfig = 1; + break; + case 'h': + usage(0); + break; + case 'H': + histfile = optarg; + break; + default: + usage(1); + break; + } + } + + /* No need for forks if we're talking to 1 daemon */ + if (daemon_name) + no_fork = true; + + if (ditch_suid) { + elevuid = realuid; + elevgid = realgid; + } + + if (markfile + writeconfig + dryrun + boot_flag > 1) { + fprintf(stderr, + "Invalid combination of arguments. Please specify at most one of:\n\t-b, -C, -m, -w\n"); + return 1; + } + if (inputfile && (writeconfig || boot_flag)) { + fprintf(stderr, + "WARNING: Combining the -f option with -b or -w is NOT SUPPORTED since its\nresults are inconsistent!\n"); + } + + snprintf(vtysh_config, sizeof(vtysh_config), "%s%s%s", sysconfdir, + pathspace, VTYSH_CONFIG_NAME); + snprintf(frr_config, sizeof(frr_config), "%s%s%s", sysconfdir, + pathspace, FRR_CONFIG_NAME); + + if (pathspace_arg) { + strlcat(vtydir, "/", sizeof(vtydir)); + strlcat(vtydir, pathspace_arg, sizeof(vtydir)); + } + + /* Initialize user input buffer. */ + setlinebuf(stdout); + + /* Signal and others. */ + vtysh_signal_init(); + + /* Make vty structure and register commands. */ + vtysh_init_vty(); + vtysh_init_cmd(); + vtysh_user_init(); + vtysh_config_init(); + + vty_init_vtysh(); + + if (!user_mode) { + /* Read vtysh configuration file before connecting to daemons. + * (file may not be readable to calling user in SUID mode) */ + suid_on(); + vtysh_apply_config(vtysh_config, dryrun, false); + suid_off(); + } + /* Error code library system */ + log_ref_init(); + lib_error_init(); + + if (markfile) { + if (!inputfile) { + fprintf(stderr, + "-f option MUST be specified with -m option\n"); + return 1; + } + return (vtysh_mark_file(inputfile)); + } + + /* Start execution only if not in dry-run mode */ + if (dryrun && !cmd) { + if (inputfile) { + ret = vtysh_apply_config(inputfile, dryrun, false); + } else { + ret = vtysh_apply_config(frr_config, dryrun, false); + } + + exit(ret); + } + + if (dryrun && cmd && cmd->line) { + if (!user_mode) + vtysh_execute("enable"); + while (cmd) { + struct cmd_rec *cr; + char *cmdnow = cmd->line, *next; + do { + next = strchr(cmdnow, '\n'); + if (next) + *next++ = '\0'; + + if (echo_command) + printf("%s%s\n", vtysh_prompt(), + cmdnow); + + ret = vtysh_execute_no_pager(cmdnow); + if (!no_error + && !(ret == CMD_SUCCESS + || ret == CMD_SUCCESS_DAEMON + || ret == CMD_WARNING)) + exit(1); + } while ((cmdnow = next) != NULL); + + cr = cmd; + cmd = cmd->next; + XFREE(MTYPE_TMP, cr); + } + exit(ret); + } + + /* Ignore error messages */ + if (no_error) { + if (freopen("/dev/null", "w", stdout) == NULL) { + fprintf(stderr, + "Exiting: Failed to duplicate stdout with -n option"); + exit(1); + } + } + + /* SUID: go back up elevated privs */ + suid_on(); + + /* Make sure we pass authentication before proceeding. */ + vtysh_auth(); + + /* Do not connect until we have passed authentication. */ + if (vtysh_connect_all(daemon_name) <= 0) { + fprintf(stderr, "Exiting: failed to connect to any daemons.\n"); + if (geteuid() != 0) + fprintf(stderr, + "Hint: if this seems wrong, try running me as a privileged user!\n"); + if (no_error) + exit(0); + else + exit(1); + } + + /* SUID: back down, don't need privs further on */ + suid_off(); + + if (writeconfig) { + if (user_mode) { + fprintf(stderr, + "writeconfig cannot be used when running as an unprivileged user.\n"); + if (no_error) + exit(0); + else + exit(1); + } + vtysh_execute("enable"); + return vtysh_write_config_integrated(); + } + + if (boot_flag) + inputfile = frr_config; + + if (inputfile || boot_flag) { + vtysh_flock_config(inputfile); + ret = vtysh_apply_config(inputfile, dryrun, !no_fork); + vtysh_unflock_config(); + + if (no_error) + ret = 0; + + exit(ret); + } + + /* + * Setup history file for use by both -c and regular input + * If we can't find the home directory, then don't store + * the history information. + * VTYSH_HISTFILE is preferred over command line + * argument (-H/--histfile). + */ + if (histfile_env) { + strlcpy(history_file, histfile_env, sizeof(history_file)); + } else if (histfile) { + strlcpy(history_file, histfile, sizeof(history_file)); + } else { + homedir = vtysh_get_home(); + if (homedir) + snprintf(history_file, sizeof(history_file), + "%s/.history_frr", homedir); + } + + if (strlen(history_file) > 0) { + if (read_history(history_file) != 0) { + int fp; + + fp = open(history_file, O_CREAT | O_EXCL, + S_IRUSR | S_IWUSR); + if (fp != -1) + close(fp); + + read_history(history_file); + } + } + + if (getenv("VTYSH_LOG")) { + const char *logpath = getenv("VTYSH_LOG"); + + logfile = fopen(logpath, "a"); + if (!logfile) { + fprintf(stderr, "Failed to open logfile (%s): %s\n", + logpath, strerror(errno)); + exit(1); + } + } + + /* If eval mode. */ + if (cmd && cmd->line) { + /* Enter into enable node. */ + if (!user_mode) + vtysh_execute("enable"); + + vtysh_add_timestamp = ts_flag; + + while (cmd != NULL) { + char *eol; + + while ((eol = strchr(cmd->line, '\n')) != NULL) { + *eol = '\0'; + + add_history(cmd->line); + append_history(1, history_file); + + if (echo_command) + printf("%s%s\n", vtysh_prompt(), + cmd->line); + + if (logfile) + log_it(cmd->line); + + ret = vtysh_execute_no_pager(cmd->line); + if (!no_error + && !(ret == CMD_SUCCESS + || ret == CMD_SUCCESS_DAEMON + || ret == CMD_WARNING)) + exit(1); + + cmd->line = eol + 1; + } + + add_history(cmd->line); + append_history(1, history_file); + + if (echo_command) + printf("%s%s\n", vtysh_prompt(), cmd->line); + + if (logfile) + log_it(cmd->line); + + /* + * Parsing logic for regular commands will be different + * than for those commands requiring further + * processing, such as cli instructions terminating + * with question-mark character. + */ + if (!vtysh_execute_command_questionmark(cmd->line)) + ret = CMD_SUCCESS; + else + ret = vtysh_execute_no_pager(cmd->line); + + if (!no_error + && !(ret == CMD_SUCCESS || ret == CMD_SUCCESS_DAEMON + || ret == CMD_WARNING)) + exit(1); + + { + struct cmd_rec *cr; + cr = cmd; + cmd = cmd->next; + XFREE(MTYPE_TMP, cr); + } + } + + history_truncate_file(history_file, 1000); + exit(0); + } + + vtysh_readline_init(); + + vty_hello(vty); + + /* Enter into enable node. */ + if (!user_mode) + vtysh_execute("enable"); + + vtysh_add_timestamp = ts_flag; + + /* Main command loop. */ + vtysh_rl_run(); + + vtysh_uninit(); + + history_truncate_file(history_file, 1000); + printf("\n"); + + /* Rest in peace. */ + exit(0); +} |