From 33606a154753f6f50e711d185ee2728d3a32660e Mon Sep 17 00:00:00 2001 From: David Lamparter Date: Thu, 8 Aug 2019 19:25:39 +0200 Subject: [PATCH] watchfrr: add (network) namespace support This adds -N and --netns options to watchfrr, allowing it to start daemons with -N and switching network namespaces respectively. Signed-off-by: David Lamparter --- lib/libfrr.c | 10 +-- tools/etc/frr/daemons | 5 ++ tools/frrcommon.sh.in | 22 +++-- tools/frrinit.sh.in | 5 +- watchfrr/watchfrr.c | 181 +++++++++++++++++++++++++++++++++++++++++- 5 files changed, 205 insertions(+), 18 deletions(-) diff --git a/lib/libfrr.c b/lib/libfrr.c index b3df7de6d3..2597eb61e2 100644 --- a/lib/libfrr.c +++ b/lib/libfrr.c @@ -105,6 +105,7 @@ static const struct option lo_always[] = { {"daemon", no_argument, NULL, 'd'}, {"module", no_argument, NULL, 'M'}, {"profile", required_argument, NULL, 'F'}, + {"pathspace", required_argument, NULL, 'N'}, {"vty_socket", required_argument, NULL, OPTION_VTYSOCK}, {"moduledir", required_argument, NULL, OPTION_MODULEDIR}, {"log", required_argument, NULL, OPTION_LOG}, @@ -113,12 +114,13 @@ static const struct option lo_always[] = { {"command-log-always", no_argument, NULL, OPTION_LOGGING}, {NULL}}; static const struct optspec os_always = { - "hvdM:F:", + "hvdM:F:N:", " -h, --help Display this help and exit\n" " -v, --version Print program version\n" " -d, --daemon Runs in daemon mode\n" " -M, --module Load specified module\n" " -F, --profile Use specified configuration profile\n" + " -N, --pathspace Insert prefix into config & socket paths\n" " --vty_socket Override vty socket path\n" " --moduledir Override modules directory\n" " --log Set Logging to stdout, syslog, or file:\n" @@ -133,18 +135,16 @@ static const struct option lo_cfg_pid_dry[] = { #ifdef HAVE_SQLITE3 {"db_file", required_argument, NULL, OPTION_DB_FILE}, #endif - {"pathspace", required_argument, NULL, 'N'}, {"dryrun", no_argument, NULL, 'C'}, {"terminal", no_argument, NULL, 't'}, {NULL}}; static const struct optspec os_cfg_pid_dry = { - "f:i:CtN:", + "f:i:Ct", " -f, --config_file Set configuration file name\n" " -i, --pid_file Set process identifier file name\n" #ifdef HAVE_SQLITE3 " --db_file Set database file name\n" #endif - " -N, --pathspace Insert prefix into config & socket paths\n" " -C, --dryrun Check configuration for validity and exit\n" " -t, --terminal Open terminal session on stdio\n" " -d -t Daemonize after terminal session ends\n", @@ -428,8 +428,6 @@ static int frr_opt(int opt) di->config_file = optarg; break; case 'N': - if (di->flags & FRR_NO_CFG_PID_DRY) - return 1; if (di->pathspace) { fprintf(stderr, "-N/--pathspace option specified more than once!\n"); diff --git a/tools/etc/frr/daemons b/tools/etc/frr/daemons index 8bec3c5bb6..0221b0c19e 100644 --- a/tools/etc/frr/daemons +++ b/tools/etc/frr/daemons @@ -72,6 +72,11 @@ vrrpd_options=" -A 127.0.0.1" # The list of daemons to watch is automatically generated by the init script. #watchfrr_options="" +# To make watchfrr create/join the specified netns, use the following option: +#watchfrr_options="--netns" +# This only has an effect in /etc/frr//daemons, and you need to +# start FRR with "/usr/lib/frr/frrinit.sh start ". + # for debugging purposes, you can specify a "wrap" command to start instead # of starting the daemon directly, e.g. to use valgrind on ospfd: # ospfd_wrap="/usr/bin/valgrind" diff --git a/tools/frrcommon.sh.in b/tools/frrcommon.sh.in index 2955f74ce3..1203a2fe7a 100644 --- a/tools/frrcommon.sh.in +++ b/tools/frrcommon.sh.in @@ -16,10 +16,14 @@ # # This script should be installed in @CFG_SBIN@/frrcommon.sh +# FRR_PATHSPACE is passed in from watchfrr +suffix="${FRR_PATHSPACE:+/${FRR_PATHSPACE}}" +nsopt="${FRR_PATHSPACE:+-N ${FRR_PATHSPACE}}" + PATH=/bin:/usr/bin:/sbin:/usr/sbin D_PATH="@CFG_SBIN@" # /usr/lib/frr -C_PATH="@CFG_SYSCONF@" # /etc/frr -V_PATH="@CFG_STATE@" # /var/run/frr +C_PATH="@CFG_SYSCONF@${suffix}" # /etc/frr +V_PATH="@CFG_STATE@${suffix}" # /var/run/frr VTYSH="@vtysh_bin@" # /usr/bin/vtysh FRR_USER="@enable_user@" # frr FRR_GROUP="@enable_group@" # frr @@ -61,9 +65,9 @@ vtysh_b () { [ "$1" = "watchfrr" ] && return 0 [ -r "$C_PATH/frr.conf" ] || return 0 if [ -n "$1" ]; then - "$VTYSH" -b -n -d "$1" + "$VTYSH" `echo $nsopt` -b -n -d "$1" else - "$VTYSH" -b -n + "$VTYSH" `echo $nsopt` -b -n fi } @@ -156,7 +160,7 @@ daemon_start() { instopt="${inst:+-n $inst}" eval args="\$${daemon}_options" - if eval "$all_wrap $wrap $bin -d $frr_global_options $instopt $args"; then + if eval "$all_wrap $wrap $bin $nsopt -d $frr_global_options $instopt $args"; then log_success_msg "Started $dmninst" vtysh_b "$daemon" else @@ -292,9 +296,11 @@ load_old_config() { } . "$C_PATH/daemons" -load_old_config "$C_PATH/daemons.conf" -load_old_config "/etc/default/frr" -load_old_config "/etc/sysconfig/frr" +if [ -z "$FRR_PATHSPACE" ]; then + load_old_config "$C_PATH/daemons.conf" + load_old_config "/etc/default/frr" + load_old_config "/etc/sysconfig/frr" +fi if { declare -p watchfrr_options 2>/dev/null || true; } | grep -q '^declare \-a'; then log_warning_msg "watchfrr_options contains a bash array value." \ diff --git a/tools/frrinit.sh.in b/tools/frrinit.sh.in index 423d6b9b1d..539ab7d816 100644 --- a/tools/frrinit.sh.in +++ b/tools/frrinit.sh.in @@ -30,6 +30,9 @@ else } fi +# "/usr/lib/frr/frrinit.sh start somenamespace" +FRR_PATHSPACE="$2" + self="`dirname $0`" if [ -r "$self/frrcommon.sh" ]; then . "$self/frrcommon.sh" @@ -105,7 +108,7 @@ reload) NEW_CONFIG_FILE="${2:-$C_PATH/frr.conf}" [ ! -r $NEW_CONFIG_FILE ] && log_failure_msg "Unable to read new configuration file $NEW_CONFIG_FILE" && exit 1 - "$RELOAD_SCRIPT" --reload "$NEW_CONFIG_FILE" + "$RELOAD_SCRIPT" --reload "$NEW_CONFIG_FILE" `echo $nsopt` exit $? ;; diff --git a/watchfrr/watchfrr.c b/watchfrr/watchfrr.c index db327a0fa2..d1003ad5fa 100644 --- a/watchfrr/watchfrr.c +++ b/watchfrr/watchfrr.c @@ -29,6 +29,7 @@ #include "lib_errors.h" #include "zlog_targets.h" #include "network.h" +#include "printfrr.h" #include #include @@ -174,6 +175,7 @@ struct daemon { #define OPTION_MINRESTART 2000 #define OPTION_MAXRESTART 2001 #define OPTION_DRY 2002 +#define OPTION_NETNS 2003 static const struct option longopts[] = { {"daemon", no_argument, NULL, 'd'}, @@ -190,6 +192,9 @@ static const struct option longopts[] = { {"max-restart-interval", required_argument, NULL, OPTION_MAXRESTART}, {"pid-file", required_argument, NULL, 'p'}, {"blank-string", required_argument, NULL, 'b'}, +#ifdef GNU_LINUX + {"netns", optional_argument, NULL, OPTION_NETNS}, +#endif {"help", no_argument, NULL, 'h'}, {"version", no_argument, NULL, 'v'}, {NULL, 0, NULL, 0}}; @@ -244,7 +249,12 @@ Otherwise, the interval is doubled (but capped at the -M value).\n\n", -d, --daemon Run in daemon mode. In this mode, error messages are sent\n\ to syslog instead of stdout.\n\ -S, --statedir Set the vty socket directory (default is %s)\n\ --l, --loglevel Set the logging level (default is %d).\n\ +-N, --pathspace Insert prefix into config & socket paths\n" +#ifdef GNU_LINUX +" --netns Create and/or use Linux network namespace. If no name is\n" +" given, uses the value from `-N`.\n" +#endif +"-l, --loglevel Set the logging level (default is %d).\n\ The value should range from %d (LOG_EMERG) to %d (LOG_DEBUG),\n\ but it can be set higher than %d if extra-verbose debugging\n\ messages are desired.\n\ @@ -704,7 +714,7 @@ static void daemon_send_ready(int exitcode) frr_detach(); - snprintf(started, sizeof(started), "%s%s", frr_vtydir, + snprintf(started, sizeof(started), "%s/%s", frr_vtydir, "watchfrr.started"); fp = fopen(started, "w"); if (fp) @@ -1102,6 +1112,148 @@ static int startup_timeout(struct thread *t_wakeup) return 0; } +#ifdef GNU_LINUX + +#include +#include + +#define NETNS_RUN_DIR "/var/run/netns" + +static void netns_create(int dirfd, const char *nsname) +{ + /* make /var/run/netns shared between mount namespaces + * just like iproute2 sets it up + */ + if (mount("", NETNS_RUN_DIR, "none", MS_SHARED | MS_REC, NULL)) { + if (errno != EINVAL) { + perror("mount"); + exit(1); + } + + if (mount(NETNS_RUN_DIR, NETNS_RUN_DIR, "none", + MS_BIND | MS_REC, NULL)) { + perror("mount"); + exit(1); + } + + if (mount("", NETNS_RUN_DIR, "none", MS_SHARED | MS_REC, + NULL)) { + perror("mount"); + exit(1); + } + } + + /* need an empty file to mount on top of */ + int nsfd = openat(dirfd, nsname, O_CREAT | O_RDONLY | O_EXCL, 0); + + if (nsfd < 0) { + fprintf(stderr, "failed to create \"%s/%s\": %s\n", + NETNS_RUN_DIR, nsname, strerror(errno)); + exit(1); + } + close(nsfd); + + if (unshare(CLONE_NEWNET)) { + perror("unshare"); + unlinkat(dirfd, nsname, 0); + exit(1); + } + + char *dstpath = asprintfrr(MTYPE_TMP, "%s/%s", NETNS_RUN_DIR, nsname); + + /* bind-mount so the namespace has a name and is persistent */ + if (mount("/proc/self/ns/net", dstpath, "none", MS_BIND, NULL) < 0) { + fprintf(stderr, "failed to bind-mount netns to \"%s\": %s\n", + dstpath, strerror(errno)); + unlinkat(dirfd, nsname, 0); + exit(1); + } + + XFREE(MTYPE_TMP, dstpath); +} + +static void netns_setup(const char *nsname) +{ + int dirfd, nsfd; + + dirfd = open(NETNS_RUN_DIR, O_DIRECTORY | O_RDONLY); + if (dirfd < 0) { + if (errno == ENOTDIR) { + fprintf(stderr, "error: \"%s\" is not a directory!\n", + NETNS_RUN_DIR); + exit(1); + } else if (errno == ENOENT) { + if (mkdir(NETNS_RUN_DIR, 0755)) { + fprintf(stderr, "error: \"%s\": mkdir: %s\n", + NETNS_RUN_DIR, strerror(errno)); + exit(1); + } + dirfd = open(NETNS_RUN_DIR, O_DIRECTORY | O_RDONLY); + if (dirfd < 0) { + fprintf(stderr, "error: \"%s\": opendir: %s\n", + NETNS_RUN_DIR, strerror(errno)); + exit(1); + } + } else { + fprintf(stderr, "error: \"%s\": %s\n", + NETNS_RUN_DIR, strerror(errno)); + exit(1); + } + } + + nsfd = openat(dirfd, nsname, O_RDONLY); + if (nsfd < 0 && errno != ENOENT) { + fprintf(stderr, "error: \"%s/%s\": %s\n", + NETNS_RUN_DIR, nsname, strerror(errno)); + exit(1); + } + if (nsfd < 0) + netns_create(dirfd, nsname); + else { + if (setns(nsfd, CLONE_NEWNET)) { + perror("setns"); + exit(1); + } + close(nsfd); + } + close(dirfd); + + /* make sure loopback is up... weird things happen otherwise. + * ioctl is perfectly fine for this, don't need netlink... + */ + int sockfd; + struct ifreq ifr = { }; + + strlcpy(ifr.ifr_name, "lo", sizeof(ifr.ifr_name)); + + sockfd = socket(AF_INET, SOCK_DGRAM, 0); + if (sockfd < 0) { + perror("socket"); + exit(1); + } + if (ioctl(sockfd, SIOCGIFFLAGS, &ifr)) { + perror("ioctl(SIOCGIFFLAGS, \"lo\")"); + exit(1); + } + if (!(ifr.ifr_flags & IFF_UP)) { + ifr.ifr_flags |= IFF_UP; + if (ioctl(sockfd, SIOCSIFFLAGS, &ifr)) { + perror("ioctl(SIOCSIFFLAGS, \"lo\")"); + exit(1); + } + } + close(sockfd); +} + +#else /* !GNU_LINUX */ + +static void netns_setup(const char *nsname) +{ + fprintf(stderr, "network namespaces are only available on Linux\n"); + exit(1); +} +#endif + static void watchfrr_init(int argc, char **argv) { const char *special = "zebra"; @@ -1191,11 +1343,13 @@ int main(int argc, char **argv) { int opt; const char *blankstr = NULL; + const char *netns = NULL; + bool netns_en = false; frr_preinit(&watchfrr_di, argc, argv); progname = watchfrr_di.progname; - frr_opt_add("b:dk:l:i:p:r:S:s:t:T:" DEPRECATED_OPTIONS, longopts, ""); + frr_opt_add("b:di:k:l:N:p:r:S:s:t:T:" DEPRECATED_OPTIONS, longopts, ""); gs.restart.name = "all"; while ((opt = frr_getopt(argc, argv, NULL)) != EOF) { @@ -1260,6 +1414,16 @@ int main(int argc, char **argv) frr_help_exit(1); } } break; + case OPTION_NETNS: + netns_en = true; + if (strchr(optarg, '/')) { + fprintf(stderr, + "invalid network namespace name \"%s\" (may not contain slashes)\n", + optarg); + frr_help_exit(1); + } + netns = optarg; + break; case 'i': { char garbage[3]; int period; @@ -1351,6 +1515,17 @@ int main(int argc, char **argv) gs.restart.interval = gs.min_restart_interval; + /* env variable for the processes that we start */ + if (watchfrr_di.pathspace) + setenv("FRR_PATHSPACE", watchfrr_di.pathspace, 1); + else + unsetenv("FRR_PATHSPACE"); + + if (netns_en && !netns) + netns = watchfrr_di.pathspace; + if (netns_en && netns && netns[0]) + netns_setup(netns); + master = frr_init(); watchfrr_error_init(); watchfrr_init(argc, argv); -- 2.39.5