/* * $Id: maxsess.c,v 1.9 2006/12/13 01:11:37 heas Exp $ * * Copyright (c) 1995-1998 by Cisco systems, Inc. * * Permission to use, copy, modify, and distribute this software for * any purpose and without fee is hereby granted, provided that this * copyright and permission notice appear on all copies of the * software and supporting documentation, the name of Cisco Systems, * Inc. not be used in advertising or publicity pertaining to * distribution of the program without specific prior permission, and * notice be given in supporting documentation that modification, * copying and distribution is by permission of Cisco Systems, Inc. * * Cisco Systems, Inc. makes no representations about the suitability * of this software for any purpose. THIS SOFTWARE IS PROVIDED ``AS * IS'' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, * WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE. */ #include "tac_plus.h" #ifdef MAXSESS #if HAVE_CTYPE_H # include #endif char *wholog = WHOLOG_DEFAULT; /* * initialize wholog file for tracking of user logins/logouts from * accounting records. */ void maxsess_loginit(void) { int fd; fd = open(wholog, O_CREAT | O_RDWR, 0600); if (fd < 0) { report(LOG_ERR, "Can't create: %s", wholog); } else { if (debug & DEBUG_MAXSESS_FLAG) { report(LOG_DEBUG, "Initialize %s", wholog); } close(fd); } } /* * Given a port description, return it in a canonical format. * * This piece of goo is to cover the fact that an async line in EXEC * mode is known as "ttyXX", but the same line doing PPP or SLIP is * known as "AsyncXX". */ static char * portname(char *oldport) { char *p = oldport; if (!strncmp(p, "Async", 5) || !strncmp(p, "tty", 3)) { while (!isdigit((int) *p) && *p) { ++p; } } if (!*p) { if (debug & DEBUG_ACCT_FLAG) report(LOG_DEBUG, "Maxsess -- Malformed portname: %s", oldport); return(oldport); } return(p); } /* * Seek to offset and write a buffer into the file pointed to by fp */ static void write_record(char *name, FILE *fp, void *buf, int size, long offset) { if (fseek(fp, offset, SEEK_SET) < 0) { report(LOG_ERR, "%s fd=%d Cannot seek to %d %s", name, fileno(fp), offset, strerror(errno)); } if (fwrite(buf, size, 1, fp) != 1) { report(LOG_ERR, "%s fd=%d Cannot write %d bytes", name, fileno(fp), size); } } static void process_stop_record(struct identity *idp) { int recnum; struct peruser pu; FILE *fp; char *nasport = portname(idp->NAS_port); /* If we can't access the file, skip all checks. */ fp = fopen(wholog, "r+"); if (fp == NULL) { report(LOG_ERR, "Can't open %s for updating", wholog); return; } tac_lockfd(wholog, fileno(fp)); for (recnum = 0; 1; recnum++) { fseek(fp, recnum * sizeof(struct peruser), SEEK_SET); if (fread(&pu, sizeof(pu), 1, fp) <= 0) { break; } /* A match for this record? */ if (!(STREQ(pu.NAS_name, idp->NAS_name) && STREQ(pu.NAS_port, nasport))) { continue; } /* A match. Zero out this record */ bzero(&pu, sizeof(pu)); write_record(wholog, fp, &pu, sizeof(pu), recnum * sizeof(struct peruser)); if (debug & DEBUG_MAXSESS_FLAG) { report(LOG_DEBUG, "STOP record -- clear %s entry %d for %s/%s", wholog, recnum, idp->username, nasport); } } fclose(fp); } static void process_start_record(struct identity *idp) { int recnum; int foundrec = -1; int freerec = -1; char *nasport = portname(idp->NAS_port); struct peruser pu; FILE *fp; /* If we can't access the file, skip all checks. */ fp = fopen(wholog, "r+"); if (fp == NULL) { report(LOG_ERR, "Can't open %s for updating", wholog); return; } tac_lockfd(wholog, fileno(fp)); for (recnum = 0; (fread(&pu, sizeof(pu), 1, fp) > 0); recnum++) { /* Match for this NAS/Port record? */ if (STREQ(pu.NAS_name, idp->NAS_name) && STREQ(pu.NAS_port, nasport)) { foundrec = recnum; break; } /* Found a free slot on the way */ if (pu.username[0] == '\0') { freerec = recnum; } } /* This is a START record, so write a new record or update the existing * one. Note that we bzero(), so the strncpy()'s will truncate long * names and always leave a null-terminated string. */ bzero(&pu, sizeof(pu)); strncpy(pu.username, idp->username, sizeof(pu.username) - 1); strncpy(pu.NAS_name, idp->NAS_name, sizeof(pu.NAS_name) - 1); strncpy(pu.NAS_port, nasport, sizeof(pu.NAS_port) - 1); strncpy(pu.NAC_address, idp->NAC_address, sizeof(pu.NAC_address) - 1); /* Already in DB? */ if (foundrec >= 0) { if (debug & DEBUG_MAXSESS_FLAG) { report(LOG_DEBUG, "START record -- overwrite existing %s entry %d for %s " "%s/%s", wholog, foundrec, pu.NAS_name, pu.username, pu.NAS_port); } write_record(wholog, fp, &pu, sizeof(pu), foundrec * sizeof(struct peruser)); fclose(fp); return; } /* Not found in DB, but we have a free slot */ if (freerec >= 0) { write_record(wholog, fp, &pu, sizeof(pu), freerec * sizeof(struct peruser)); if (debug & DEBUG_MAXSESS_FLAG) { report(LOG_DEBUG, "START record -- %s entry %d for %s %s/%s added", wholog, freerec, pu.NAS_name, pu.username, pu.NAS_port); } fclose(fp); return; } /* No free slot. Add record at the end */ write_record(wholog, fp, &pu, sizeof(pu), recnum * sizeof(struct peruser)); if (debug & DEBUG_MAXSESS_FLAG) { report(LOG_DEBUG, "START record -- %s entry %d for %s %s/%s added", wholog, recnum, pu.NAS_name, pu.username, pu.NAS_port); } fclose(fp); } /* * Given a start or a stop accounting record, update the file of * records which tracks who's logged on and where. */ void loguser(struct acct_rec *rec) { struct identity *idp; int i; /* We're only interested in start/stop records */ if ((rec->acct_type != ACCT_TYPE_START) && (rec->acct_type != ACCT_TYPE_STOP)) { return; } /* ignore command accounting records */ for (i = 0; i < rec->num_args; i++) { char *avpair = rec->args[i]; if ((strncmp(avpair, "cmd=", 4) == 0) && strlen(avpair) > 4) { return; } } /* Extract and store just the port number, since the port names are * different depending on whether this is an async interface or an exec * line. */ idp = rec->identity; switch (rec->acct_type) { case ACCT_TYPE_START: process_start_record(idp); return; case ACCT_TYPE_STOP: process_stop_record(idp); return; } } /* * Read up to n bytes from descriptor fd into array ptr with timeout t * seconds. * * Return -1 on error, eof or timeout. Otherwise return number of bytes read. */ int timed_read(int fd, u_char *ptr, int nbytes, int timeout) { int nread; fd_set readfds, exceptfds; struct timeval tout; tout.tv_sec = timeout; tout.tv_usec = 0; FD_ZERO(&readfds); FD_SET(fd, &readfds); FD_ZERO(&exceptfds); FD_SET(fd, &exceptfds); while (1) { int status = select(fd + 1, &readfds, (fd_set *) NULL, &exceptfds, &tout); if (status == 0) { report(LOG_DEBUG, "%s: timeout reading fd %d", session.peer, fd); return(-1); } if (status < 0) { if (errno == EINTR) continue; report(LOG_DEBUG, "%s: error in select %s fd %d", session.peer, strerror(errno), fd); return(-1); } if (FD_ISSET(fd, &exceptfds)) { report(LOG_DEBUG, "%s: exception on fd %d", session.peer, fd); return(-1); } if (!FD_ISSET(fd, &readfds)) { report(LOG_DEBUG, "%s: spurious return from select", session.peer); continue; } nread = read(fd, ptr, nbytes); if (nread < 0) { if (errno == EINTR) { continue; } report(LOG_DEBUG, "%s %s: error reading fd %d nread=%d %s", session.peer, session.port, fd, nread, strerror(errno)); return(-1); /* error */ } if (nread == 0) { return(-1); /* eof */ } return(nread); } /* NOTREACHED */ } #ifdef MAXSESS_FINGER /* * Contact a NAS (using finger) to check how many sessions this USER * is currently running on it. * * Note that typically you run this code when you are in the middle of * trying to login to a Cisco NAS on a given port. Because you are * part way through a login when you do this, you can get inconsistent * reports for that particular port about whether the user is * currently logged in on it or not, so we ignore output which claims * that the user is using that line currently. * * This is extremely Cisco specific -- finger formats appear to vary wildly. * The format we're expecting is: Line User Host(s) Idle Location 0 con 0 idle never 18 vty 0 usr0 idle 30 barley.cisco.com 19 vty 1 usr0 Virtual Exec 2 20 vty 2 idle 0 barley.cisco.com * Column zero contains a space or an asterisk character. The line number * starts at column 1 and is 3 digits wide. User names start at column 13, * with a maximum possible width of 10. */ static int ckfinger(char *user, char *nas, struct identity *idp) { struct sockaddr_in sin; struct servent *serv; int count, s, bufsize; char *buf, *p, *pn; int incr = 4096, slop = 32; u_long inaddr; char *curport = portname(idp->NAS_port); char *name; /* The finger service, aka port 79 */ serv = getservbyname("finger", "tcp"); if (serv) { sin.sin_port = serv->s_port; } else { sin.sin_port = 79; } /* Get IP addr for the NAS */ inaddr = inet_addr(nas); if (inaddr != -1) { /* A dotted decimal address */ bcopy(&inaddr, &sin.sin_addr, sizeof(inaddr)); sin.sin_family = AF_INET; } else { struct hostent *host = gethostbyname(nas); if (host == NULL) { report(LOG_ERR, "ckfinger: gethostbyname %s failure: %s", nas, strerror(errno)); return(0); } bcopy(host->h_addr, &sin.sin_addr, host->h_length); sin.sin_family = host->h_addrtype; } s = socket(AF_INET, SOCK_STREAM, 0); if (s < 0) { report(LOG_ERR, "ckfinger: socket: %s", strerror(errno)); return(0); } if (connect(s, (struct sockaddr *) & sin, sizeof(sin)) < 0) { report(LOG_ERR, "ckfinger: connect failure %s", strerror(errno)); close(s); return(0); } /* Read in the finger output into a single flat buffer */ buf = NULL; bufsize = 0; for (;;) { int x; buf = tac_realloc(buf, bufsize + incr + slop); x = timed_read(s, buf + bufsize, incr, 10); if (x <= 0) { break; } bufsize += x; } /* Done talking here */ close(s); buf[bufsize] = '\0'; if (bufsize <= 0) { report(LOG_ERR, "ckfinger: finger failure"); free(buf); return(0); } /* skip first line in buffer */ p = strchr(buf, '\n'); if (p) { p++; } p = strchr(p, '\n'); if (p) { p++; } /* Tally each time this user appears */ for (count = 0; p && *p; p = pn) { int i, len, nmlen; char nmbuf[11]; /* Find next line */ pn = strchr(p, '\n'); if (pn) { ++pn; } /* Calculate line length */ if (pn) { len = pn - p; } else { len = strlen(p); } /* Line too short -> ignore */ if (len < 14) { continue; } /* Always ignore the NAS/port we're currently trying to login on. */ if (isdigit((int) *curport)) { int thisport; if (sscanf(p + 1, " %d", &thisport) == 1) { if ((atoi(curport) == thisport) && !strcmp(idp->NAS_name, nas)) { if (debug & DEBUG_MAXSESS_FLAG) { report(LOG_DEBUG, "%s session on %s/%s discounted", user, idp->NAS_name, idp->NAS_port); } continue; } } } /* Extract username, up to 10 chars wide, starting at char 13 */ nmlen = 0; #if (TAC_IOS_VERSION == 11) name = p + 13; #else name = p + 15; #endif for (i = 0; *name && !isspace((int) *name) && (i < 10); i++) { nmbuf[nmlen++] = *name++; } nmbuf[nmlen++] = '\0'; /* If name matches, up the count */ if (STREQ(user, nmbuf)) { count++; if (debug & DEBUG_MAXSESS_FLAG) { char c = *pn; *pn = '\0'; report(LOG_DEBUG, "%s matches: %s", user, p); *pn = c; } } } free(buf); return(count); } /* * Verify how many sessions a user has according to the wholog file. * Use finger to contact each NAS that wholog says has this user * logged on. */ static int countusers_by_finger(struct identity *idp) { FILE *fp; struct peruser pu; int x, naddr, nsess, n; char **addrs, *uname; fp = fopen(wholog, "r+"); if (fp == NULL) { return(0); } uname = idp->username; /* Count sessions */ tac_lockfd(wholog, fileno(fp)); nsess = 0; naddr = 0; addrs = NULL; while (fread(&pu, sizeof(pu), 1, fp) > 0) { int dup; /* Ignore records for everyone except this user */ if (strcmp(pu.username, uname)) { continue; } /* Only check a given NAS once */ for (dup = 0, x = 0; x < naddr; ++x) { if (STREQ(addrs[x], pu.NAS_name)) { dup = 1; break; } } if (dup) { continue; } /* Add this address to our list */ addrs = (char **) tac_realloc((char *) addrs, (naddr + 1) * sizeof(char *)); addrs[naddr] = tac_strdup(pu.NAS_name); naddr += 1; /* Validate via finger */ if (debug & DEBUG_MAXSESS_FLAG) { report(LOG_DEBUG, "Running finger on %s for user %s/%s", pu.NAS_name, uname, idp->NAS_port); } n = ckfinger(uname, pu.NAS_name, idp); if (debug & DEBUG_MAXSESS_FLAG) { report(LOG_DEBUG, "finger reports %d active session%s for %s on %s", n, (n == 1 ? "" : "s"), uname, pu.NAS_name); } nsess += n; } /* Clean up and return */ fclose(fp); for (x = 0; x < naddr; ++x) { free(addrs[x]); } free(addrs); return(nsess); } #endif /* MAXSESS_FINGER */ /* * Estimate how many sessions a named user currently owns by looking in * the wholog file. */ static int countuser(struct identity *idp) { FILE *fp; struct peruser pu; int nsess; /* Access log */ fp = fopen(wholog, "r+"); if (fp == NULL) { return(0); } /* Count sessions. Skip any session associated with the current port. */ tac_lockfd(wholog, fileno(fp)); nsess = 0; while (fread(&pu, sizeof(pu), 1, fp) > 0) { /* Current user */ if (strcmp(pu.username, idp->username)) { continue; } /* skip current port on current NAS */ if (STREQ(portname(pu.NAS_port), portname(idp->NAS_port)) && STREQ(pu.NAS_name, idp->NAS_name)) { continue; } nsess += 1; } /* Clean up and return */ fclose(fp); return(nsess); } /* * is_async() * Tell if the named NAS port is an async-like device. * * Finger reports async users, but not ISDN ones (yay). So we can do * a "slow" double check for async, but not ISDN. */ static int is_async(char *portname) { if (isdigit((int) *portname) || !strncmp(portname, "Async", 5) || !strncmp(portname, "tty", 3)) { return(1); } return(0); } /* * See if this user can have more sessions. */ int maxsess_check_count(char *user, struct author_data *data) { int sess, maxsess; struct identity *id; /* No max session configured--don't check */ id = data->id; maxsess = cfg_get_intvalue(user, TAC_IS_USER, S_maxsess, TAC_PLUS_RECURSE); if (!maxsess) { if (debug & (DEBUG_MAXSESS_FLAG | DEBUG_AUTHOR_FLAG)) { report(LOG_DEBUG, "%s may run an unlimited number of sessions", user); } return(0); } /* Count sessions for this user by looking in our wholog file */ sess = countuser(id); if (debug & (DEBUG_MAXSESS_FLAG | DEBUG_AUTHOR_FLAG)) { report(LOG_DEBUG, "user %s is running %d out of a maximum of %d " "sessions", user, sess, maxsess); } #ifdef MAXSESS_FINGER if ((sess >= maxsess) && is_async(id->NAS_port)) { /* * If we have finger available, double check this count by contacting * the NAS */ sess = countusers_by_finger(id); } #endif /* If it's really too high, don't authorize more services */ if (sess >= maxsess) { char buf[80]; sprintf(buf, "Login failed; too many active sessions (%d maximum)", maxsess); data->msg = tac_strdup(buf); if (debug & (DEBUG_AUTHOR_FLAG | DEBUG_MAXSESS_FLAG)) { report(LOG_DEBUG, data->msg); } data->status = AUTHOR_STATUS_FAIL; data->output_args = NULL; data->num_out_args = 0; return(1); } return(0); } #endif /* MAXSESS */