/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License (the "License"). * You may not use this file except in compliance with the License. * * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE * or http://www.opensolaris.org/os/licensing. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at usr/src/OPENSOLARIS.LICENSE. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END */ /* * Copyright 2006 Sun Microsystems, Inc. All rights reserved. * Use is subject to license terms. */ #pragma ident "%Z%%M% %I% %E% SMI" /* * This code implements the Starcat Virtual Console host daemon (see cvcd(1M)). * It accepts one TCP connection at a time on a well-known port. Once a * connection is accepted, the console redirection driver (cvcdredir(7D)) is * opened, and console I/O is routed back and forth between the two file * descriptors (network and redirection driver). Per-socket IPsec is used to * secure the connection if it is enabled with the "-a", "-u" and or "-e" * command line options. */ #include #include #include #include #include #include #include #include /* Just to get FIONBIO... */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* * Header files for per-socket IPsec */ #include #include /* * The IPsec socket option struct, from ipsec(7P): * * typedef struct ipsec_req { * uint_t ipsr_ah_req; AH request * uint_t ipsr_esp_req; ESP request * uint_t ipsr_self_encap_req; Self-Encap request * uint8_t ipsr_auth_alg; Auth algs for AH * uint8_t ipsr_esp_alg; Encr algs for ESP * uint8_t ipsr_esp_auth_alg; Auth algs for ESP * } ipsec_req_t; * * The -a option sets the ipsr_auth_alg field. Allowable arguments * are "none", "md5", or "sha1". The -e option sets the ipsr_esp_alg * field. Allowable arguments are "none", "des", or "3des". "none" * is the default for both options. The -u option sets ipsr_esp_auth_alg. * Allowable arguments are the same as -a. * * The arguments ("md5", "des", etc.) are named so that they match * kmd(1m)'s accepted arguments which are listed on the SC in * /etc/opt/SUNWSMS/SMS/config/kmd_policy.cf. */ #define AH_REQ (IPSEC_PREF_REQUIRED | IPSEC_PREF_UNIQUE) #define ESP_REQ (IPSEC_PREF_REQUIRED | IPSEC_PREF_UNIQUE) #define SELF_ENCAP_REQ 0x0 /* * A type to hold the command line argument string used to select a * particular authentication header (AH) or encapsulating security * payload (ESP) algorithm and the ID used for that algorithm when * filling the ipsec_req_t structure which is passed to * setsockopt(3SOCKET). */ typedef struct cvcd_alg { char *arg_name; uint8_t alg_id; } cvcd_alg_t; /* * Misc. defines. */ #define NODENAME "/etc/nodename" #define NETWORK_PFD 0 #define REDIR_PFD 1 #define LISTEN_PFD 2 #define NUM_PFDS 3 /* * Function prototypes */ static void cvcd_set_priority(void); static int cvcd_init_host_socket(int port, uint8_t ah_auth_alg, uint8_t esp_encr_alg, uint8_t esp_auth_alg); static void cvcd_do_network_console(void); static void cvcd_err(int code, char *format, ...); static void cvcd_usage(void); static uint8_t cvcd_get_alg(cvcd_alg_t *algs, char *arg); static boolean_t cvcd_global_policy(void); /* * Globals */ static struct pollfd pfds[NUM_PFDS]; static char progname[MAXPATHLEN]; static int debug = 0; /* * Array of acceptable -a, -u and -e arguments. */ static cvcd_alg_t auth_algs_array[] = { { "none", SADB_AALG_NONE }, /* -a none or -u none */ { "md5", SADB_AALG_MD5HMAC }, /* -a md5 or -u md5 */ { "sha1", SADB_AALG_SHA1HMAC }, /* -a sha1 or -u sha1 */ { NULL, 0x0 } }, esp_algs_array[] = { { "none", SADB_EALG_NONE }, /* -e none */ { "des", SADB_EALG_DESCBC }, /* -e des */ { "3des", SADB_EALG_3DESCBC }, /* -e 3des */ { NULL, 0x0 } }; int main(int argc, char **argv) { int err; int opt; int tport = 0; char *hostname; struct utsname utsname; int fd; int i; struct servent *se; char prefix[256]; uint8_t ah_auth_alg = SADB_AALG_NONE; uint8_t esp_encr_alg = SADB_EALG_NONE; uint8_t esp_auth_alg = SADB_AALG_NONE; (void) setlocale(LC_ALL, ""); (void) strcpy(progname, argv[0]); #ifdef DEBUG while ((opt = getopt(argc, argv, "a:e:u:dp:")) != EOF) { #else while ((opt = getopt(argc, argv, "a:e:u:")) != EOF) { #endif switch (opt) { case 'a' : case 'u' : if (opt == 'a') ah_auth_alg = cvcd_get_alg( auth_algs_array, optarg); else esp_auth_alg = cvcd_get_alg( auth_algs_array, optarg); break; case 'e' : esp_encr_alg = cvcd_get_alg( esp_algs_array, optarg); break; #ifdef DEBUG case 'd' : debug = 1; break; case 'p' : tport = atoi(optarg); break; #endif /* DEBUG */ default : cvcd_usage(); exit(1); } } if (uname(&utsname) == -1) { perror("HOSTNAME not defined"); exit(1); } hostname = utsname.nodename; /* * hostname may still be NULL, depends on when cvcd was started * in the boot sequence. If it is NULL, try one more time * to get a hostname -> look in the /etc/nodename file. */ if (!strlen(hostname)) { /* * try to get the hostname from the /etc/nodename file * we reuse the utsname.nodename buffer here! hostname * already points to it. */ if ((fd = open(NODENAME, O_RDONLY)) > 0) { if ((i = read(fd, utsname.nodename, SYS_NMLN)) <= 0) { cvcd_err(LOG_WARNING, "failed to acquire hostname"); } else { utsname.nodename[i-1] = '\0'; } (void) close(fd); } } /* * If all attempts to get the hostname have failed, put something * meaningful in the buffer. */ if (!strlen(hostname)) { (void) strcpy(utsname.nodename, "(unknown)"); } /* * Must be root. */ if (debug == 0 && geteuid() != 0) { fprintf(stderr, "cvcd: Must be root"); exit(1); } /* * Daemonize... */ if (debug == 0) { closefrom(0); (void) chdir("/"); (void) umask(0); if (fork() != 0) { exit(0); } (void) setpgrp(); (void) sprintf(prefix, "%s-(HOSTNAME:%s)", progname, hostname); openlog(prefix, LOG_CONS | LOG_NDELAY, LOG_LOCAL0); } /* * Initialize the array of pollfds used to track the listening socket, * the connection to the console redirection driver, and the network * connection. */ (void) memset((void *)pfds, 0, NUM_PFDS * sizeof (struct pollfd)); for (i = 0; i < NUM_PFDS; i++) { pfds[i].fd = -1; } /* SPR 94004 */ (void) sigignore(SIGTERM); /* * SPR 83644: cvc and kadb are not compatible under heavy loads. * Fix: will give cvcd highest TS priority at execution time. */ cvcd_set_priority(); /* * If not already determined by a command-line flag, figure out which * port we're supposed to be listening on. */ if (tport == 0) { if ((se = getservbyname(CVCD_SERVICE, "tcp")) == NULL) { cvcd_err(LOG_ERR, "getservbyname(%s) not found", CVCD_SERVICE); exit(1); } tport = se->s_port; } if (debug == 1) { cvcd_err(LOG_DEBUG, "tport = %d, debug = %d", tport, debug); } /* * Attempt to initialize the socket we'll use to listen for incoming * connections. No need to check the return value, as the call will * exit if it fails. */ pfds[LISTEN_PFD].fd = cvcd_init_host_socket(tport, ah_auth_alg, esp_encr_alg, esp_auth_alg); /* * Now that we're all set up, we loop forever waiting for connections * (one at a time) and then driving network console activity over them. */ for (;;) { /* * Start by waiting for an incoming connection. */ do { pfds[LISTEN_PFD].events = POLLIN; err = poll(&(pfds[LISTEN_PFD]), 1, -1); if (err == -1) { cvcd_err(LOG_ERR, "poll: %s", strerror(errno)); exit(1); } if ((err > 0) && (pfds[LISTEN_PFD].revents & POLLIN)) { fd = accept(pfds[LISTEN_PFD].fd, NULL, NULL); if ((fd == -1) && (errno != EWOULDBLOCK)) { cvcd_err(LOG_ERR, "accept: %s", strerror(errno)); exit(1); } } } while (fd == -1); /* * We have a connection. Set the new socket nonblocking, and * initialize the appropriate pollfd. In theory, the new socket * is _already_ non-blocking because accept() is supposed to * hand us a socket with the same properties as the socket we're * listening on, but it won't hurt to make sure. */ opt = 1; err = ioctl(fd, FIONBIO, &opt); if (err == -1) { cvcd_err(LOG_ERR, "ioctl: %s", strerror(errno)); (void) close(fd); continue; } pfds[NETWORK_PFD].fd = fd; /* * Since we're ready to do network console stuff, go ahead and * open the Network Console redirection driver, which will * switch traffic from the IOSRAM path to the network path if * the network path has been selected in cvc. */ fd = open(CVCREDIR_DEV, O_RDWR|O_NDELAY); if (fd == -1) { cvcd_err(LOG_ERR, "open(redir): %s", strerror(errno)); exit(1); } pfds[REDIR_PFD].fd = fd; /* * We have a network connection and we have the redirection * driver open, so drive the network console until something * changes. */ cvcd_do_network_console(); /* * cvcd_do_network_console doesn't return until there's a * problem, so we need to close the network connection and the * redirection driver and start the whole loop over again. */ (void) close(pfds[NETWORK_PFD].fd); pfds[NETWORK_PFD].fd = -1; (void) close(pfds[REDIR_PFD].fd); pfds[REDIR_PFD].fd = -1; } /* NOTREACHED */ return (1); } /* * cvcd_get_alg * * Returns the ID of the first algorithm found in * the 'algs' array with a name matching 'arg'. If * there is no matching algorithm, the function does * not return. The 'algs' array must be terminated * by an entry containing a NULL 'arg_name' field. */ static uint8_t cvcd_get_alg(cvcd_alg_t *algs, char *arg) { cvcd_alg_t *alg; for (alg = algs; alg->arg_name != NULL && arg != NULL; alg++) { if (strncmp(alg->arg_name, arg, strlen(alg->arg_name) + 1) == 0) { return (alg->alg_id); } } cvcd_usage(); exit(1); /* NOTREACHED */ } /* * cvcd_set_priority * * DESCRIBE * SPR 83644: cvc and kadb are not compatible under heavy loads. * Fix: will give cvcd highest TS priority at execution time. */ static void cvcd_set_priority(void) { id_t pid, tsID; pcparms_t pcparms; tsparms_t *tsparmsp; short tsmaxpri; pcinfo_t info; pid = getpid(); pcparms.pc_cid = PC_CLNULL; tsparmsp = (tsparms_t *)pcparms.pc_clparms; /* Get scheduler properties for this PID */ if (priocntl(P_PID, pid, PC_GETPARMS, (caddr_t)&pcparms) == -1L) { cvcd_err(LOG_ERR, "Warning: can't set priority."); cvcd_err(LOG_ERR, "priocntl(GETPARMS): %s", strerror(errno)); return; } /* Get class ID and maximum priority for TS process class */ (void) strcpy(info.pc_clname, "TS"); if (priocntl(0L, 0L, PC_GETCID, (caddr_t)&info) == -1L) { cvcd_err(LOG_ERR, "Warning: can't set priority."); cvcd_err(LOG_ERR, "priocntl(GETCID): %s", strerror(errno)); return; } tsmaxpri = ((struct tsinfo *)info.pc_clinfo)->ts_maxupri; tsID = info.pc_cid; /* Print priority info in debug mode */ if (debug) { if (pcparms.pc_cid == tsID) { cvcd_err(LOG_DEBUG, "PID: %d, current priority: %d, Max priority: %d.", pid, tsparmsp->ts_upri, tsmaxpri); } } /* Change proc's priority to maxtspri */ pcparms.pc_cid = tsID; tsparmsp->ts_upri = tsmaxpri; tsparmsp->ts_uprilim = tsmaxpri; if (priocntl(P_PID, pid, PC_SETPARMS, (caddr_t)&pcparms) == -1L) { cvcd_err(LOG_ERR, "Warning: can't set priority."); cvcd_err(LOG_ERR, "priocntl(SETPARMS): %s", strerror(errno)); } /* Print new priority info in debug mode */ if (debug) { if (priocntl(P_PID, pid, PC_GETPARMS, (caddr_t)&pcparms) == -1L) { cvcd_err(LOG_ERR, "priocntl(GETPARMS): %s", strerror(errno)); } else { cvcd_err(LOG_DEBUG, "PID: %d, new priority: %d.", pid, tsparmsp->ts_upri); } } } /* * cvcd_init_host_socket * * Given a TCP port number, create and initialize a socket appropriate for * accepting incoming connections to that port. */ static int cvcd_init_host_socket(int port, uint8_t ah_auth_alg, uint8_t esp_encr_alg, uint8_t esp_auth_alg) { int err; int fd; int optval; int optlen = sizeof (optval); ipsec_req_t ipsec_req; /* For per-socket IPsec */ struct sockaddr_in6 sin6; /* IPv6 listen socket */ /* * Start by creating the socket, which needs to support IPv6. */ fd = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); if (fd == -1) { cvcd_err(LOG_ERR, "socket: %s", strerror(errno)); exit(1); } /* * Set the SO_REUSEADDR option, and make the socket non-blocking. */ optval = 1; err = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &optval, optlen); if (err == -1) { cvcd_err(LOG_ERR, "setsockopt: %s", strerror(errno)); exit(1); } err = ioctl(fd, FIONBIO, &optval); if (err == -1) { cvcd_err(LOG_ERR, "ioctl: %s", strerror(errno)); exit(1); } /* * Enable per-socket IPsec if the user specified an AH or ESP * algorithm to use and global policy is not in effect. */ if (!cvcd_global_policy() && (ah_auth_alg != SADB_AALG_NONE || esp_encr_alg != SADB_EALG_NONE || esp_auth_alg != SADB_AALG_NONE)) { bzero(&ipsec_req, sizeof (ipsec_req)); /* Hardcoded values */ ipsec_req.ipsr_self_encap_req = SELF_ENCAP_REQ; /* User defined */ ipsec_req.ipsr_auth_alg = ah_auth_alg; ipsec_req.ipsr_esp_alg = esp_encr_alg; if (ah_auth_alg != SADB_AALG_NONE) ipsec_req.ipsr_ah_req = AH_REQ; if (esp_encr_alg != SADB_EALG_NONE || esp_auth_alg != SADB_AALG_NONE) { ipsec_req.ipsr_esp_req = ESP_REQ; ipsec_req.ipsr_esp_auth_alg = esp_auth_alg; } err = setsockopt(fd, IPPROTO_IPV6, IPV6_SEC_OPT, (void *)&ipsec_req, sizeof (ipsec_req)); if (err == -1) { cvcd_err(LOG_ERR, "failed to enable per-socket IPsec"); cvcd_err(LOG_ERR, "setsockopt: %s", strerror(errno)); exit(1); } } /* * Bind the socket to our local address and port. */ bzero(&sin6, sizeof (sin6)); sin6.sin6_family = AF_INET6; sin6.sin6_port = htons(port); sin6.sin6_addr = in6addr_any; err = bind(fd, (struct sockaddr *)&sin6, sizeof (sin6)); if (err == -1) { cvcd_err(LOG_ERR, "bind: %s", strerror(errno)); exit(1); } /* * Indicate that we want to accept connections on this socket. Since we * only allow one connection at a time anyway, specify a maximum backlog * of 1. */ err = listen(fd, 1); if (err == -1) { cvcd_err(LOG_ERR, "listen: %s", strerror(errno)); exit(1); } return (fd); } /* * cvcd_do_network_console * * With established connections to the network and the redirection driver, * shuttle data between the two until something goes wrong. */ static void cvcd_do_network_console(void) { int i; int err; int count; short revents; int input_len = 0; int output_len = 0; int input_off = 0; int output_off = 0; char input_buf[MAXPKTSZ]; char output_buf[MAXPKTSZ]; for (;;) { /* * Wait for activity on any of the open file descriptors, which * includes the ability to write data if we have any to write. * If poll() fails, break out of the network console processing * loop. */ pfds[LISTEN_PFD].events = POLLIN; pfds[NETWORK_PFD].events = POLLIN; if (output_len != 0) { pfds[NETWORK_PFD].events |= POLLOUT; } pfds[REDIR_PFD].events = POLLIN; if (input_len != 0) { pfds[REDIR_PFD].events |= POLLOUT; } err = poll(pfds, NUM_PFDS, -1); if (err == -1) { cvcd_err(LOG_ERR, "poll: %s", strerror(errno)); break; } /* * If any errors or hangups were detected, or one of our file * descriptors is bad, bail out of the network console * processing loop. */ for (i = 0; i < NUM_PFDS; i++) { revents = pfds[i].revents; if (revents & (POLLERR | POLLHUP | POLLNVAL)) { cvcd_err(LOG_NOTICE, "poll: status on %s fd:%s%s%s", ((i == LISTEN_PFD) ? "listen" : ((i == NETWORK_PFD) ? "network" : "redir")), (revents & POLLERR) ? " error" : "", (revents & POLLHUP) ? " hangup" : "", (revents & POLLNVAL) ? " bad fd" : ""); goto fail; /* 'break' wouldn't work here */ } } /* * Start by rejecting any connection attempts, since we only * allow one network connection at a time. */ if (pfds[LISTEN_PFD].revents & POLLIN) { int fd; fd = accept(pfds[LISTEN_PFD].fd, NULL, NULL); if (fd > 0) { (void) close(fd); } } /* * If we have data waiting to be written in one direction or the * other, go ahead and try to send the data on its way. We're * going to attempt the writes regardless of whether the poll * indicated that the destinations are ready, because we want to * find out if either descriptor has a problem (e.g. broken * network link). * If an "unexpected" error is detected, give up and break out * of the network console processing loop. */ if (output_len != 0) { count = write(pfds[NETWORK_PFD].fd, &(output_buf[output_off]), output_len); if ((count == -1) && (errno != EAGAIN)) { cvcd_err(LOG_ERR, "write(network): %s", strerror(errno)); break; } else if (count > 0) { output_len -= count; if (output_len == 0) { output_off = 0; } else { output_off += count; } } } if (input_len != 0) { count = write(pfds[REDIR_PFD].fd, &(input_buf[input_off]), input_len); if ((count == -1) && (errno != EAGAIN)) { cvcd_err(LOG_ERR, "write(redir): %s", strerror(errno)); break; } else if (count > 0) { input_len -= count; if (input_len == 0) { input_off = 0; } else { input_off += count; } } } /* * Finally, take a look at each data source and, if there isn't * any residual data from that source still waiting to be * processed, see if more data can be read. We don't want to * read more data from a source if we haven't finished * processing the last data we read from it because doing so * would maximize the amount of data lost if the network console * failed or was closed. * If an "unexpected" error is detected, give up and break out * of the network console processing loop. * The call to read() appears to be in the habit of returning 0 * when you've read all of the data from a stream that has been * hung up, and poll apparently feels that that condition * justifies setting POLLIN, so we're going to treat 0 as an * error return from read(). */ if ((output_len == 0) && (pfds[REDIR_PFD].revents & POLLIN)) { count = read(pfds[REDIR_PFD].fd, output_buf, MAXPKTSZ); if (count <= 0) { /* * Reading 0 simply means there is no data * available, since this is a terminal. */ if ((count < 0) && (errno != EAGAIN)) { cvcd_err(LOG_ERR, "read(redir): %s", strerror(errno)); break; } } else { output_len = count; output_off = 0; } } if ((input_len == 0) && (pfds[NETWORK_PFD].revents & POLLIN)) { count = read(pfds[NETWORK_PFD].fd, input_buf, MAXPKTSZ); if (count <= 0) { /* * Reading 0 here implies a hangup, since this * is a non-blocking socket that poll() reported * as having data available. This will * typically occur when the console user drops * to OBP or intentially switches to IOSRAM * mode. */ if (count == 0) { cvcd_err(LOG_NOTICE, "read(network): hangup detected"); break; } else if (errno != EAGAIN) { cvcd_err(LOG_ERR, "read(network): %s", strerror(errno)); break; } } else { input_len = count; input_off = 0; } } } /* End forever loop */ /* * If we get here, something bad happened during an attempt to access * either the redirection driver or the network connection. There * doesn't appear to be any way to avoid the possibility of losing * console input and/or input in that case, so we should at least report * the loss if it happens. * XXX - We could do more, but is it worth the effort? Logging the * lost data would be pretty easy... actually preserving it * in the console flow would be a lot harder. We're more robust * than the previous generation at this point, at least, so * perhaps that's enough for now? */ fail: if (input_len != 0) { cvcd_err(LOG_ERR, "console input lost"); } if (output_len != 0) { cvcd_err(LOG_ERR, "console output lost"); } } static void cvcd_usage() { #if defined(DEBUG) (void) printf("%s [-d] [-p port] " "[-a none|md5|sha1] [-e none|des|3des] [-u none|md5|sha1]\n", progname); #else (void) printf("%s [-a none|md5|sha1] [-e none|des|3des] " "[-u none|md5|sha1]\n", progname); #endif /* DEBUG */ } /* * cvcd_err () * * Description: * Log messages via syslog daemon. * * Input: * code - logging code * format - messages to log * * Output: * void * */ static void cvcd_err(int code, char *format, ...) { va_list varg_ptr; char buf[MAXPKTSZ]; va_start(varg_ptr, format); (void) vsnprintf(buf, MAXPKTSZ, format, varg_ptr); va_end(varg_ptr); if (debug == 0) { syslog(code, buf); } else { (void) fprintf(stderr, "%s: %s\n", progname, buf); } } /* * has_cvcd_token * * Look for "?port [cvc_hostd|442]" in input buf. * Assume only a single thread calls here. */ static boolean_t has_cvcd_token(char *buf) { char *token; char *delims = "{} \t\n"; boolean_t port = B_FALSE; while ((token = strtok(buf, delims)) != NULL) { buf = NULL; if (port == B_TRUE) { if (strcmp(token, "cvc_hostd") == 0 || strcmp(token, "442") == 0) { return (B_TRUE); } else { return (B_FALSE); } } if (strlen(token) == 5) { token++; if (strcmp(token, "port") == 0) { port = B_TRUE; continue; } } } return (B_FALSE); } /* * cvcd_global_policy * * Check global policy file for cvcd entry. Just covers common cases. */ static boolean_t cvcd_global_policy() { FILE *fp; char buf[256]; boolean_t rv = B_FALSE; fp = fopen("/etc/inet/ipsecinit.conf", "r"); if (fp == NULL) return (B_FALSE); while (fgets(buf, sizeof (buf), fp) != NULL) { if (buf[0] == '#') continue; if (has_cvcd_token(buf)) { rv = B_TRUE; cvcd_err(LOG_NOTICE, "cvcd using global policy"); break; } } (void) fclose(fp); return (rv); }