/* $OpenBSD: switchd.c,v 1.18 2019/07/23 00:31:31 schwarze Exp $ */ /* * Copyright (c) 2013-2016 Reyk Floeter * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "switchd.h" void parent_shutdown(struct switchd *); void parent_sig_handler(int, short, void *); int parent_dispatch_ofp(int, struct privsep_proc *, struct imsg *); int parent_dispatch_control(int, struct privsep_proc *, struct imsg *); int parent_configure(struct switchd *); int parent_reload(struct switchd *); void parent_connect(struct privsep *, struct switch_client *); void parent_connected(int, short, void *); void parent_disconnect(struct privsep *, struct switch_client *); __dead void usage(void); static struct privsep_proc procs[] = { { "ofp", PROC_OFP, NULL, ofp }, { "control", PROC_CONTROL, parent_dispatch_control, control }, { "ofcconn", PROC_OFCCONN, NULL, ofcconn } }; __dead void usage(void) { extern const char *__progname; fprintf(stderr, "usage: %s [-dnv] [-c cachesize] [-D macro=value] " "[-f file] [-t timeout]\n", __progname); exit(1); } int main(int argc, char *argv[]) { struct switchd *sc = NULL; struct privsep *ps = NULL; struct switch_server *srv; const char *errstr = NULL; int c; int debug = 0, verbose = 0; uint32_t opts = 0; unsigned int cache = SWITCHD_CACHE_MAX; unsigned int timeout = SWITCHD_CACHE_TIMEOUT; const char *conffile = SWITCHD_CONFIG; const char *errp, *title = NULL; enum privsep_procid proc_id = PROC_PARENT; int argc0 = argc, proc_instance = 0; log_init(1, LOG_DAEMON); while ((c = getopt(argc, argv, "c:dD:f:hI:nP:t:v")) != -1) { switch (c) { case 'c': cache = strtonum(optarg, 1, UINT32_MAX, &errstr); if (errstr != NULL) { log_warn("max cache size: %s", errstr); usage(); } break; case 'd': debug++; break; case 'D': if (cmdline_symset(optarg) < 0) log_warnx("could not parse macro definition %s", optarg); break; case 'f': conffile = optarg; break; case 'I': proc_instance = strtonum(optarg, 0, PROC_MAX_INSTANCES, &errp); if (errp) fatalx("invalid process instance"); break; case 'n': opts |= SWITCHD_OPT_NOACTION; break; case 'P': title = optarg; proc_id = proc_getid(procs, nitems(procs), title); if (proc_id == PROC_MAX) fatalx("invalid process name"); break; case 't': timeout = strtonum(optarg, 0, UINT32_MAX, &errstr); if (errstr != NULL) { log_warn("cache timeout: %s", errstr); usage(); } break; case 'v': verbose++; opts |= SWITCHD_OPT_VERBOSE; break; default: usage(); } } if ((sc = calloc(1, sizeof(*sc))) == NULL) fatal("calloc"); if (strlcpy(sc->sc_conffile, conffile, PATH_MAX) >= PATH_MAX) fatal("config file exceeds PATH_MAX"); sc->sc_cache_max = cache; sc->sc_cache_timeout = timeout; sc->sc_opts = opts; srv = &sc->sc_server; srv->srv_sc = sc; ps = &sc->sc_ps; ps->ps_env = sc; TAILQ_INIT(&ps->ps_rcsocks); TAILQ_INIT(&sc->sc_clients); if (parse_config(sc->sc_conffile, sc) == -1) { proc_kill(&sc->sc_ps); exit(1); } if (opts & SWITCHD_OPT_NOACTION) { fprintf(stderr, "configuration OK\n"); proc_kill(&sc->sc_ps); exit(0); } /* check for root privileges */ if (geteuid()) fatalx("need root privileges"); if ((ps->ps_pw = getpwnam(SWITCHD_USER)) == NULL) fatalx("unknown user " SWITCHD_USER); log_init(debug, LOG_DAEMON); log_setverbose(verbose); /* Configure the control socket */ ps->ps_csock.cs_name = SWITCHD_SOCKET; ps->ps_instance = proc_instance; if (title) ps->ps_title[proc_id] = title; /* Only the parent returns. */ proc_init(ps, procs, nitems(procs), debug, argc0, argv, proc_id); if (!debug && daemon(0, 0) == -1) fatal("failed to daemonize"); log_procinit("parent"); if (unveil("/", "r") == -1) fatal("unveil"); if (unveil("/dev", "rw") == -1) fatal("unveil"); /* * pledge in the parent process: * stdio - for malloc and basic I/O including events. * rpath - for reload to open and read the configuration files. * wpath - for accessing the /dev/switch device. * inet - for opening OpenFlow and device sockets. * dns - for resolving host in the configuration files. * sendfd - send sockets to child processes on reload. */ if (pledge("stdio rpath wpath inet dns sendfd", NULL) == -1) fatal("pledge"); event_init(); signal_set(&ps->ps_evsigint, SIGINT, parent_sig_handler, ps); signal_set(&ps->ps_evsigterm, SIGTERM, parent_sig_handler, ps); signal_set(&ps->ps_evsighup, SIGHUP, parent_sig_handler, ps); signal_set(&ps->ps_evsigpipe, SIGPIPE, parent_sig_handler, ps); signal_set(&ps->ps_evsigusr1, SIGUSR1, parent_sig_handler, ps); signal_add(&ps->ps_evsigint, NULL); signal_add(&ps->ps_evsigterm, NULL); signal_add(&ps->ps_evsighup, NULL); signal_add(&ps->ps_evsigpipe, NULL); signal_add(&ps->ps_evsigusr1, NULL); proc_connect(ps); if (parent_configure(sc) == -1) fatalx("configuration failed"); event_dispatch(); log_debug("%d parent exiting", getpid()); return (0); } int switchd_socket(struct sockaddr *sock, int reuseport) { int s = -1, val; struct linger lng; if ((s = socket(sock->sa_family, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP)) == -1) goto bad; /* * Socket options */ bzero(&lng, sizeof(lng)); if (setsockopt(s, SOL_SOCKET, SO_LINGER, &lng, sizeof(lng)) == -1) goto bad; if (reuseport) { val = 1; if (setsockopt(s, SOL_SOCKET, SO_REUSEPORT, &val, sizeof(int)) == -1) goto bad; } /* * TCP options */ val = 1; if (setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &val, sizeof(val)) == -1) goto bad; return (s); bad: if (s != -1) close(s); return (-1); } int switchd_listen(struct sockaddr *sock) { int s; if ((s = switchd_socket(sock, 1)) == -1) return (-1); if (bind(s, sock, sock->sa_len) == -1) goto bad; if (listen(s, 10) == -1) goto bad; return (s); bad: close(s); return (-1); } int switchd_tap(void) { char path[PATH_MAX]; int i, fd; for (i = 0; i < SWITCHD_MAX_TAP; i++) { snprintf(path, PATH_MAX, "/dev/tap%d", i); fd = open(path, O_RDWR | O_NONBLOCK); if (fd != -1) return (fd); } return (-1); } struct switch_connection * switchd_connbyid(struct switchd *sc, unsigned int id, unsigned int instance) { struct switch_connection *con; TAILQ_FOREACH(con, &sc->sc_conns, con_entry) { if (con->con_id == id && con->con_instance == instance) return (con); } return (NULL); } struct switch_connection * switchd_connbyaddr(struct switchd *sc, struct sockaddr *sa) { struct switch_connection *con; TAILQ_FOREACH(con, &sc->sc_conns, con_entry) { if (sockaddr_cmp((struct sockaddr *) &con->con_peer, sa, -1) == 0) return (con); } return (NULL); } void parent_sig_handler(int sig, short event, void *arg) { struct privsep *ps = arg; switch (sig) { case SIGHUP: log_info("%s: reload requested with SIGHUP", __func__); /* * This is safe because libevent uses async signal handlers * that run in the event loop and not in signal context. */ parent_reload(ps->ps_env); break; case SIGPIPE: log_info("%s: ignoring SIGPIPE", __func__); break; case SIGUSR1: log_info("%s: ignoring SIGUSR1", __func__); break; case SIGTERM: case SIGINT: parent_shutdown(ps->ps_env); break; default: fatalx("unexpected signal"); } } int parent_configure(struct switchd *sc) { struct switch_client *swc, *swcn; int fd; if ((fd = switchd_tap()) == -1) fatal("%s: tap", __func__); proc_compose_imsg(&sc->sc_ps, PROC_OFP, -1, IMSG_TAPFD, -1, fd, NULL, 0); TAILQ_FOREACH_SAFE(swc, &sc->sc_clients, swc_next, swcn) { parent_connect(&sc->sc_ps, swc); } return (0); } int parent_reload(struct switchd *sc) { struct switchd newconf; struct switch_client *swc, *oswc, *swcn; memset(&newconf, 0, sizeof(newconf)); TAILQ_INIT(&newconf.sc_clients); TAILQ_INIT(&newconf.sc_conns); if (parse_config(sc->sc_conffile, &newconf) != -1) { TAILQ_FOREACH_SAFE(swc, &sc->sc_clients, swc_next, swcn) { TAILQ_FOREACH(oswc, &newconf.sc_clients, swc_next) { if (sockaddr_cmp((struct sockaddr *) &oswc->swc_addr.swa_addr, (struct sockaddr *) &swc->swc_addr.swa_addr, -1) == 0) { TAILQ_REMOVE(&newconf.sc_clients, oswc, swc_next); break; } } if (oswc == NULL) { /* Removed */ parent_disconnect(&sc->sc_ps, swc); } else { /* Keep the existing one */ TAILQ_REMOVE(&newconf.sc_clients, oswc, swc_next); free(oswc); } } TAILQ_FOREACH_SAFE(swc, &newconf.sc_clients, swc_next, swcn) { TAILQ_REMOVE(&newconf.sc_clients, swc, swc_next); TAILQ_INSERT_TAIL(&sc->sc_clients, swc, swc_next); parent_connect(&sc->sc_ps, swc); } } return (0); } int parent_dispatch_control(int fd, struct privsep_proc *p, struct imsg *imsg) { struct switch_client *swc, *oswc; struct privsep *ps = p->p_ps; struct switchd *sc = ps->ps_env; switch (imsg->hdr.type) { case IMSG_CTL_CONNECT: case IMSG_CTL_DISCONNECT: IMSG_SIZE_CHECK(imsg, swc); /* Need to allocate it in case it is reused */ if ((swc = calloc(1, sizeof(*swc))) == NULL) { log_warnx("%s: calloc", __func__); return (0); } memcpy(swc, imsg->data, sizeof(*swc)); memset(&swc->swc_ev, 0, sizeof(swc->swc_ev)); if (imsg->hdr.type == IMSG_CTL_CONNECT) { TAILQ_INSERT_TAIL(&sc->sc_clients, swc, swc_next); parent_connect(p->p_ps, swc); } else { TAILQ_FOREACH(oswc, &sc->sc_clients, swc_next) { if (sockaddr_cmp((struct sockaddr *) &oswc->swc_addr.swa_addr, (struct sockaddr *) &swc->swc_addr.swa_addr, -1) == 0) { parent_disconnect(ps, oswc); break; } } if (oswc == NULL) log_warnx("client %s is not connected", print_host(&swc->swc_addr.swa_addr, NULL, 0)); free(swc); } return (0); default: break; } return (-1); } void parent_shutdown(struct switchd *sc) { proc_kill(&sc->sc_ps); free(sc); log_warnx("parent terminating"); exit(0); } void parent_connect(struct privsep *ps, struct switch_client *swc) { struct switchd *sc = ps->ps_env; struct sockaddr_storage *ss; struct sockaddr_un *un; struct sockaddr_in *sin4; struct sockaddr_in6 *sin6; int fd = -1; struct timeval tv; ss = &swc->swc_addr.swa_addr; if (ss->ss_len == 0) { log_warnx("%s: invalid address", __func__); goto fail; } swc->swc_arg = ps; memset(&swc->swc_ev, 0, sizeof(swc->swc_ev)); switch (ss->ss_family) { case AF_LOCAL: un = (struct sockaddr_un *)ss; /* restrict the opening path to /dev/switch* */ if (strncmp(un->sun_path, "/dev/switch", strlen("/dev/switch")) != 0) { log_warnx("%s: device path is wrong: %s", __func__, un->sun_path); goto fail; } if ((fd = open(un->sun_path, O_RDWR | O_NONBLOCK)) == -1) { log_warn("%s: failed to open %s", __func__, un->sun_path); goto fail; } break; case AF_INET: case AF_INET6: if (ss->ss_family == AF_INET) { sin4 = (struct sockaddr_in *)ss; if (sin4->sin_port == 0) sin4->sin_port = htons(SWITCHD_CTLR_PORT); } else if (ss->ss_family == AF_INET6) { sin6 = (struct sockaddr_in6 *)ss; if (sin6->sin6_port == 0) sin6->sin6_port = htons(SWITCHD_CTLR_PORT); } if ((fd = switchd_socket((struct sockaddr *)ss, 0)) == -1) { log_debug("%s: failed to get socket for %s", __func__, print_host(ss, NULL, 0)); goto fail; } retry: if (connect(fd, (struct sockaddr *)ss, ss->ss_len) == -1) { if (errno == EINTR) goto retry; if (errno == EINPROGRESS) { tv.tv_sec = SWITCHD_CONNECT_TIMEOUT; tv.tv_usec = 0; event_set(&swc->swc_ev, fd, EV_WRITE|EV_TIMEOUT, parent_connected, swc); event_add(&swc->swc_ev, &tv); return; } log_warn("%s: failed to connect to %s, fd %d", __func__, print_host(ss, NULL, 0), fd); goto fail; } break; } parent_connected(fd, 0, swc); return; fail: TAILQ_REMOVE(&sc->sc_clients, swc, swc_next); free(swc); } void parent_connected(int fd, short event, void *arg) { struct switch_client *swc = arg; struct privsep *ps = swc->swc_arg; struct switchd *sc = ps->ps_env; if (event & EV_TIMEOUT) { log_debug("%s: failed to connect to %s", __func__, print_host(&swc->swc_addr.swa_addr, NULL, 0)); TAILQ_REMOVE(&sc->sc_clients, swc, swc_next); free(swc); return; } switch (swc->swc_target.swa_type) { case SWITCH_CONN_LOCAL: proc_compose_imsg(ps, PROC_OFP, -1, IMSG_CTL_CONNECT, -1, fd, swc, sizeof(*swc)); break; case SWITCH_CONN_TLS: case SWITCH_CONN_TCP: proc_compose_imsg(ps, PROC_OFCCONN, -1, IMSG_CTL_CONNECT, -1, fd, swc, sizeof(*swc)); break; default: fatalx("not implemented"); } } void parent_disconnect(struct privsep *ps, struct switch_client *swc) { struct switchd *sc = ps->ps_env; enum privsep_procid target; TAILQ_REMOVE(&sc->sc_clients, swc, swc_next); target = swc->swc_target.swa_type == SWITCH_CONN_LOCAL ? PROC_OFP : PROC_OFCCONN; proc_compose(ps, target, IMSG_CTL_DISCONNECT, swc, sizeof(*swc)); free(swc); }