/* 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 #include #include #include #include #include #include #include #ifdef HAVE_FCNTL_H #include #endif #include #include #ifdef HAVE_SYS_TIME_H #include #endif #ifdef HAVE_UNISTD_H #include #endif #include #include "maxsess.h" #include "report.h" #include "utils.h" #include "cfgfile.h" #include "main.h" #include "do_acct.h" #include "parse.h" void maxsess_loginit TAC_ARGS((void)); /* Configurable: */ /* This is a shared file used to maintain a record of who's on */ #define WHOLOG_DEFAULT "/var/log/tac_who.log" /* * This is state kept per user/session */ struct peruser { char username[64]; /* User name */ char NAS_name[32]; /* NAS user logged into */ char NAS_port[32]; /* ...port on that NAS */ char NAC_address[32]; /* ...IP address of NAS */ }; char *wholog = WHOLOG_DEFAULT; /* * initialize wholog file for tracking of user logins/logouts from * accounting records. */ void maxsess_loginit() { 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); } } static char *portname TAC_ARGS((char *oldport)); /* * 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(oldport) char *oldport; { char *p = oldport; if (!strncmp(p, "Async", 5) || !strncmp(p, "tty", 3)) { while (!isdigit(*p) && *p) { ++p; } } if (!*p) { if (debug & DEBUG_ACCT_FLAG) report(LOG_DEBUG, "Maxsess -- Malformed portname: %s", oldport); return (oldport); } return (p); } static void write_record TAC_ARGS((char *name, FILE *fp, void *buf, int size, long int offset)); /* * Seek to offset and write a buffer into the file pointed to by fp */ static void write_record(name, fp, buf, size, offset) FILE *fp; long offset; int size; void *buf; char *name; { if (fseek(fp, offset, SEEK_SET) < 0) { report(LOG_ERR, "%s fd=%d Cannot seek to %ld %s", name, fileno(fp), offset, sys_errlist[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 TAC_ARGS((struct identity *idp)); static void process_stop_record(idp) 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 TAC_ARGS((struct identity *idp)); static void process_start_record(idp) 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); } void loguser TAC_ARGS((struct acct_rec *rec)); /* * Given a start or a stop accounting record, update the file of * records which tracks who's logged on and where. */ void loguser(rec) 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. */ static int timed_read TAC_ARGS((int fd, void *ptr, int nbytes, int timeout)); static int timed_read(fd, ptr, nbytes, timeout) int fd; void *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, sys_errlist[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, sys_errlist[errno]); return (-1); /* error */ } if (nread == 0) { return (-1); /* eof */ } return (nread); } /* NOTREACHED */ } /* * 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 TAC_ARGS((char *user, char *nas, struct identity *idp)); static int ckfinger(user, nas, idp) char *user, *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 != (u_long)-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, sys_errlist[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", sys_errlist[errno]); return (0); } if (connect(s, (struct sockaddr *) & sin, sizeof(sin)) < 0) { report(LOG_ERR, "ckfinger: connect failure %s", sys_errlist[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(*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; name = p + 13; for (i = 0; *name && !isspace(*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); } static int countusers_by_finger TAC_ARGS((struct identity *idp)); /* * 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(idp) 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); } static int countuser TAC_ARGS((struct identity *idp)); /* * Estimate how many sessions a named user currently owns by looking in * the wholog file. */ static int countuser(idp) 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); } static int is_async TAC_ARGS((char *portname)); /* * 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(portname) char *portname; { if (isdigit(*portname) || !strncmp(portname, "Async", 5) || !strncmp(portname, "tty", 3)) { return (1); } return (0); } int maxsess_check_count TAC_ARGS((char *user, struct author_data *data)); /* * See if this user can have more sessions. */ int maxsess_check_count(user, data) 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(S_user, 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); } 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); } /* 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); } #else /* MAXSESS */ TAC_SOURCEFILE_EMPTY #endif /* MAXSESS */