mirror of https://github.com/facebook/tac_plus
604 lines
15 KiB
C
604 lines
15 KiB
C
/*
|
|
* $Id: maxsess.c,v 1.12 2009-07-16 18:13:19 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"
|
|
|
|
#if HAVE_CTYPE_H
|
|
# include <ctype.h>
|
|
#endif
|
|
#include <poll.h>
|
|
#include <signal.h>
|
|
|
|
char *wholog = TACPLUS_WHOLOGFILE;
|
|
|
|
static int timed_read(int, unsigned char *, int, int);
|
|
|
|
/*
|
|
* 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 */
|
|
memset(&pu, 0, 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 zero the memory, so the strncpy()'s will truncate
|
|
* long names and always leave a null-terminated string.
|
|
*/
|
|
memset(&pu, 0, 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.
|
|
*/
|
|
static int
|
|
timed_read(int fd, unsigned char *ptr, int nbytes, int timeout)
|
|
{
|
|
int nread;
|
|
struct pollfd pfds;
|
|
|
|
pfds.fd = fd;
|
|
pfds.events = POLLIN | POLLERR | POLLHUP | POLLNVAL;
|
|
|
|
while (1) {
|
|
int status = poll(&pfds, 1, timeout * 1000);
|
|
|
|
if (status == 0) {
|
|
status = errno;
|
|
report(LOG_DEBUG, "%s: timeout reading fd %d", session.peer, fd);
|
|
errno = status;
|
|
return(-1);
|
|
}
|
|
if (status < 0) {
|
|
if (errno == EINTR)
|
|
continue;
|
|
status = errno;
|
|
report(LOG_DEBUG, "%s: error in poll %s fd %d", session.peer,
|
|
strerror(errno), fd);
|
|
errno = status;
|
|
return(-1);
|
|
}
|
|
if (pfds.revents & (POLLERR | POLLHUP | POLLNVAL)) {
|
|
status = errno;
|
|
report(LOG_DEBUG, "%s: exception on fd %d", session.peer, fd);
|
|
errno = status;
|
|
return(-1);
|
|
}
|
|
if (!(pfds.revents & POLLIN)) {
|
|
status = errno;
|
|
report(LOG_DEBUG, "%s: spurious return from poll", session.peer);
|
|
errno = status;
|
|
continue;
|
|
}
|
|
nread = read(fd, ptr, nbytes);
|
|
|
|
if (nread < 0) {
|
|
if (errno == EINTR) {
|
|
continue;
|
|
}
|
|
status = errno;
|
|
report(LOG_DEBUG, "%s %s: error reading fd %d nread=%d %s",
|
|
session.peer, session.port, fd, nread, strerror(errno));
|
|
errno = status;
|
|
return(-1); /* error */
|
|
}
|
|
if (nread == 0) {
|
|
errno = 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.
|
|
*
|
|
* Returns the number of sessions/connections, or zero on error.
|
|
*/
|
|
static int
|
|
ckfinger(char *user, char *nas, struct identity *idp)
|
|
{
|
|
struct addrinfo hints, *res, *resp;
|
|
int count, s, bufsize, ecode;
|
|
char *buf, *p, *pn;
|
|
int incr = 4096, slop = 32;
|
|
char *curport = portname(idp->NAS_port);
|
|
char *name;
|
|
|
|
memset(&hints, 0, sizeof(struct addrinfo));
|
|
hints.ai_family = PF_UNSPEC;
|
|
hints.ai_socktype = SOCK_STREAM;
|
|
|
|
if ((ecode = getaddrinfo(nas, "finger", &hints, &res)) != 0) {
|
|
report(LOG_ERR, "ckfinger: getaddrinfo %s failure: %s", nas,
|
|
gai_strerror(ecode));
|
|
return(0);
|
|
}
|
|
|
|
ecode = 0;
|
|
for (resp = res; resp != NULL; resp = resp->ai_next) {
|
|
s = socket(resp->ai_family, resp->ai_socktype, resp->ai_protocol);
|
|
if (s < 0) {
|
|
if (errno == EAFNOSUPPORT || errno == EPROTONOSUPPORT)
|
|
continue;
|
|
report(LOG_ERR, "ckfinger: socket: %s", strerror(errno));
|
|
freeaddrinfo(res);
|
|
return(0);
|
|
}
|
|
if ((ecode = connect(s, resp->ai_addr, res->ai_addrlen)) < 0) {
|
|
close(s);
|
|
continue;
|
|
} else
|
|
break;
|
|
}
|
|
freeaddrinfo(res);
|
|
/* socket failure / no supported address families */
|
|
if (resp == NULL && ecode == 0) {
|
|
report(LOG_ERR, "ckfinger: socket: %s", strerror(errno));
|
|
return(0);
|
|
}
|
|
if (ecode != 0) {
|
|
report(LOG_ERR, "ckfinger: connect %s: %s", nas, strerror(errno));
|
|
return(0);
|
|
}
|
|
/* Read 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, (unsigned char *)(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;
|
|
name = p + 13;
|
|
/*
|
|
* If this is not IOS version 11, the username MAY begin at the
|
|
* 15th column in the line. So, skip up to 2 leading whitespaces.
|
|
*/
|
|
for (i = 0; i < 2; i++) {
|
|
if (! isspace((int)*name))
|
|
break;
|
|
}
|
|
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.
|
|
*/
|
|
int
|
|
countusers_by_finger(struct identity *idp)
|
|
{
|
|
FILE *fp;
|
|
struct peruser pu;
|
|
int x, naddr, nsess, n;
|
|
char **addrs;
|
|
|
|
fp = fopen(wholog, "r+");
|
|
if (fp == NULL) {
|
|
return(0);
|
|
}
|
|
|
|
/* 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, idp->username)) {
|
|
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, idp->username, idp->NAS_port);
|
|
}
|
|
n = ckfinger(idp->username, 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"), idp->username, pu.NAS_name);
|
|
}
|
|
nsess += n;
|
|
}
|
|
|
|
/* Clean up and return */
|
|
fclose(fp);
|
|
for (x = 0; x < naddr; ++x) {
|
|
free(addrs[x]);
|
|
}
|
|
free(addrs);
|
|
|
|
return(nsess);
|
|
}
|
|
|
|
/*
|
|
* Estimate how many sessions a named user currently owns by looking in
|
|
* the wholog file.
|
|
*/
|
|
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);
|
|
}
|