/* * rlm_radutmp.c * * Version: $Id: rlm_radutmp.c,v 1.17 2003/03/18 05:56:46 aland Exp $ * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Copyright 2000 The FreeRADIUS server project * FIXME add copyrights */ #include "autoconf.h" #include #include #include #include #include #include #include #include #include #include "config.h" #include "radiusd.h" #include "radutmp.h" #include "modules.h" #define LOCK_LEN sizeof(struct radutmp) static const char porttypes[] = "ASITX"; /* * used for caching radutmp lookups in the accounting component. The * session (checksimul) component doesn't use it, but probably should. */ typedef struct nas_port { uint32_t nasaddr; int port; off_t offset; struct nas_port *next; } NAS_PORT; typedef struct rlm_radutmp_t { NAS_PORT *nas_port_list; char *filename; char *username; int case_sensitive; int check_nas; int permission; int callerid_ok; } rlm_radutmp_t; static CONF_PARSER module_config[] = { { "filename", PW_TYPE_STRING_PTR, offsetof(rlm_radutmp_t,filename), NULL, RADUTMP }, { "username", PW_TYPE_STRING_PTR, offsetof(rlm_radutmp_t,username), NULL, "%{User-Name}"}, { "case_sensitive", PW_TYPE_BOOLEAN, offsetof(rlm_radutmp_t,case_sensitive), NULL, "yes"}, { "check_with_nas", PW_TYPE_BOOLEAN, offsetof(rlm_radutmp_t,check_nas), NULL, "yes"}, { "perm", PW_TYPE_INTEGER, offsetof(rlm_radutmp_t,permission), NULL, "0644" }, { "callerid", PW_TYPE_BOOLEAN, offsetof(rlm_radutmp_t,callerid_ok), NULL, "no" }, { NULL, -1, 0, NULL, NULL } /* end the list */ }; static int radutmp_instantiate(CONF_SECTION *conf, void **instance) { rlm_radutmp_t *inst; inst = rad_malloc(sizeof(*inst)); if (cf_section_parse(conf, inst, module_config)) { free(inst); return -1; } inst->nas_port_list = NULL; *instance = inst; return 0; } /* * Detach. */ static int radutmp_detach(void *instance) { NAS_PORT *p, *next; rlm_radutmp_t *inst = instance; for (p = inst->nas_port_list ; p ; p=next) { next = p->next; free(p); } free(inst->filename); free(inst); return 0; } /* * Zap all users on a NAS from the radutmp file. */ static int radutmp_zap(rlm_radutmp_t *inst, const char *filename, uint32_t nasaddr, time_t t) { struct radutmp u; int fd; if (t == 0) time(&t); fd = open(filename, O_RDWR); if (fd < 0) { radlog(L_ERR, "rlm_radutmp: Error accessing file %s: %s", filename, strerror(errno)); return RLM_MODULE_FAIL; } /* * Lock the utmp file, prefer lockf() over flock(). */ rad_lockfd(fd, LOCK_LEN); /* * Find the entry for this NAS / portno combination. */ while (read(fd, &u, sizeof(u)) == sizeof(u)) { if ((nasaddr != 0 && nasaddr != u.nas_address) || u.type != P_LOGIN) continue; /* * Match. Zap it. */ if (lseek(fd, -(off_t)sizeof(u), SEEK_CUR) < 0) { radlog(L_ERR, "rlm_radutmp: radutmp_zap: negative lseek!"); lseek(fd, (off_t)0, SEEK_SET); } u.type = P_IDLE; u.time = t; write(fd, &u, sizeof(u)); } close(fd); /* and implicitely release the locks */ return 0; } /* * Lookup a NAS_PORT in the nas_port_list */ static NAS_PORT *nas_port_find(NAS_PORT *nas_port_list, uint32_t nasaddr, int port) { NAS_PORT *cl; for(cl = nas_port_list; cl; cl = cl->next) if (nasaddr == cl->nasaddr && port == cl->port) break; return cl; } /* * Store logins in the RADIUS utmp file. */ static int radutmp_accounting(void *instance, REQUEST *request) { struct radutmp ut, u; VALUE_PAIR *vp; int status = -1; uint32_t nas_address = 0; uint32_t framed_address = 0; int protocol = -1; time_t t; int fd; int just_an_update = 0; int port_seen = 0; int nas_port_type = 0; int off; rlm_radutmp_t *inst = instance; char buffer[256]; char filename[1024]; char ip_name[32]; /* 255.255.255.255 */ const char *nas; NAS_PORT *cache; int r; /* * Which type is this. */ if ((vp = pairfind(request->packet->vps, PW_ACCT_STATUS_TYPE)) == NULL) { radlog(L_ERR, "rlm_radutmp: No Accounting-Status-Type record."); return RLM_MODULE_NOOP; } status = vp->lvalue; /* * Look for weird reboot packets. * * ComOS (up to and including 3.5.1b20) does not send * standard PW_STATUS_ACCOUNTING_XXX messages. * * Check for: o no Acct-Session-Time, or time of 0 * o Acct-Session-Id of "00000000". * * We could also check for NAS-Port, that attribute * should NOT be present (but we don't right now). */ if ((status != PW_STATUS_ACCOUNTING_ON) && (status != PW_STATUS_ACCOUNTING_OFF)) do { int check1 = 0; int check2 = 0; if ((vp = pairfind(request->packet->vps, PW_ACCT_SESSION_TIME)) == NULL || vp->lvalue == 0) check1 = 1; if ((vp = pairfind(request->packet->vps, PW_ACCT_SESSION_ID)) != NULL && vp->length == 8 && memcmp(vp->strvalue, "00000000", 8) == 0) check2 = 1; if (check1 == 0 || check2 == 0) { #if 0 /* Cisco sometimes sends START records without username. */ radlog(L_ERR, "rlm_radutmp: no username in record"); return RLM_MODULE_FAIL; #else break; #endif } radlog(L_INFO, "rlm_radutmp: converting reboot records."); if (status == PW_STATUS_STOP) status = PW_STATUS_ACCOUNTING_OFF; if (status == PW_STATUS_START) status = PW_STATUS_ACCOUNTING_ON; } while(0); time(&t); memset(&ut, 0, sizeof(ut)); ut.porttype = 'A'; /* * First, find the interesting attributes. */ for (vp = request->packet->vps; vp; vp = vp->next) { switch (vp->attribute) { case PW_LOGIN_IP_HOST: case PW_FRAMED_IP_ADDRESS: framed_address = vp->lvalue; ut.framed_address = vp->lvalue; break; case PW_FRAMED_PROTOCOL: protocol = vp->lvalue; break; case PW_NAS_IP_ADDRESS: nas_address = vp->lvalue; ut.nas_address = vp->lvalue; break; case PW_NAS_PORT: ut.nas_port = vp->lvalue; port_seen = 1; break; case PW_ACCT_DELAY_TIME: ut.delay = vp->lvalue; break; case PW_ACCT_SESSION_ID: /* * If length > 8, only store the * last 8 bytes. */ off = vp->length - sizeof(ut.session_id); /* * Ascend is br0ken - it adds a \0 * to the end of any string. * Compensate. */ if (vp->length > 0 && vp->strvalue[vp->length - 1] == 0) off--; if (off < 0) off = 0; memcpy(ut.session_id, vp->strvalue + off, sizeof(ut.session_id)); break; case PW_NAS_PORT_TYPE: if (vp->lvalue >= 0 && vp->lvalue <= 4) ut.porttype = porttypes[vp->lvalue]; nas_port_type = vp->lvalue; break; case PW_CALLING_STATION_ID: if(inst->callerid_ok) strNcpy(ut.caller_id, (char *)vp->strvalue, sizeof(ut.caller_id)); break; } } /* * If we didn't find out the NAS address, use the * originator's IP address. */ if (nas_address == 0) { nas_address = request->packet->src_ipaddr; ut.nas_address = nas_address; nas = client_name(nas_address); /* MUST be a valid client */ } else { /* might be a client, might not be. */ RADCLIENT *cl; /* * Hack like 'client_name()', but with sane * fall-back. */ cl = client_find(nas_address); if (cl) { if (cl->shortname[0]) { nas = cl->shortname; } else { nas = cl->longname; } } else { /* * The NAS isn't a client, it's behind * a proxy server. In that case, just * get the IP address. */ nas = ip_ntoa(ip_name, nas_address); } } /* * Set the protocol field. */ if (protocol == PW_PPP) ut.proto = 'P'; else if (protocol == PW_SLIP) ut.proto = 'S'; else ut.proto = 'T'; ut.time = t - ut.delay; /* * Get the utmp filename, via xlat. */ radius_xlat(filename, sizeof(filename), inst->filename, request, NULL); /* * See if this was a reboot. * * Hmm... we may not want to zap all of the users when * the NAS comes up, because of issues with receiving * UDP packets out of order. */ if (status == PW_STATUS_ACCOUNTING_ON && nas_address) { radlog(L_INFO, "rlm_radutmp: NAS %s restarted (Accounting-On packet seen)", nas); radutmp_zap(inst, filename, nas_address, ut.time); return RLM_MODULE_OK; } if (status == PW_STATUS_ACCOUNTING_OFF && nas_address) { radlog(L_INFO, "rlm_radutmp: NAS %s rebooted (Accounting-Off packet seen)", nas); radutmp_zap(inst, filename, nas_address, ut.time); return RLM_MODULE_OK; } /* * If we don't know this type of entry pretend we succeeded. */ if (status != PW_STATUS_START && status != PW_STATUS_STOP && status != PW_STATUS_ALIVE) { radlog(L_ERR, "rlm_radutmp: NAS %s port %d unknown packet type %d)", nas, ut.nas_port, status); return RLM_MODULE_NOOP; } /* * Translate the User-Name attribute, or whatever else * they told us to use. */ *buffer = '\0'; radius_xlat(buffer, sizeof(buffer), inst->username, request, NULL); /* * Copy the previous translated user name. */ strncpy(ut.login, buffer, RUT_NAMESIZE); /* * Perhaps we don't want to store this record into * radutmp. We skip records: * * - without a NAS-Port (telnet / tcp access) * - with the username "!root" (console admin login) */ if (!port_seen) { DEBUG2(" rlm_radutmp: No NAS-Port seen. Cannot do anything."); DEBUG2(" rlm_radumtp: WARNING: checkrad will probably not work!"); return RLM_MODULE_NOOP; } if (strncmp(ut.login, "!root", RUT_NAMESIZE) == 0) { DEBUG2(" rlm_radutmp: Not recording administrative user"); return RLM_MODULE_NOOP; } /* * Enter into the radutmp file. */ fd = open(filename, O_RDWR|O_CREAT, inst->permission); if (fd < 0) { radlog(L_ERR, "rlm_radutmp: Error accessing file %s: %s", filename, strerror(errno)); return RLM_MODULE_FAIL; } /* * Lock the utmp file, prefer lockf() over flock(). */ rad_lockfd(fd, LOCK_LEN); /* * Find the entry for this NAS / portno combination. */ if ((cache = nas_port_find(inst->nas_port_list, ut.nas_address, ut.nas_port)) != NULL) { lseek(fd, (off_t)cache->offset, SEEK_SET); } r = 0; off = 0; while (read(fd, &u, sizeof(u)) == sizeof(u)) { off += sizeof(u); if (u.nas_address != ut.nas_address || u.nas_port != ut.nas_port) continue; if (status == PW_STATUS_STOP && strncmp(ut.session_id, u.session_id, sizeof(u.session_id)) != 0) { /* * Don't complain if this is not a * login record (some clients can * send _only_ logout records). */ if (u.type == P_LOGIN) radlog(L_ERR, "rlm_radutmp: Logout entry for NAS %s port %d has wrong ID", nas, u.nas_port); r = -1; break; } if (status == PW_STATUS_START && strncmp(ut.session_id, u.session_id, sizeof(u.session_id)) == 0 && u.time >= ut.time) { if (u.type == P_LOGIN) { radlog(L_INFO, "rlm_radutmp: Login entry for NAS %s port %d duplicate", nas, u.nas_port); r = -1; break; } radlog(L_ERR, "rlm_radutmp: Login entry for NAS %s port %d wrong order", nas, u.nas_port); r = -1; break; } /* * FIXME: the ALIVE record could need * some more checking, but anyway I'd * rather rewrite this mess -- miquels. */ if (status == PW_STATUS_ALIVE && strncmp(ut.session_id, u.session_id, sizeof(u.session_id)) == 0 && u.type == P_LOGIN) { /* * Keep the original login time. */ ut.time = u.time; if (u.login[0] != 0) just_an_update = 1; } if (lseek(fd, -(off_t)sizeof(u), SEEK_CUR) < 0) { radlog(L_ERR, "rlm_radutmp: negative lseek!"); lseek(fd, (off_t)0, SEEK_SET); off = 0; } else off -= sizeof(u); r = 1; break; } /* read the file until we find a match */ /* * Found the entry, do start/update it with * the information from the packet. */ if (r >= 0 && (status == PW_STATUS_START || status == PW_STATUS_ALIVE)) { /* * Remember where the entry was, because it's * easier than searching through the entire file. */ if (cache == NULL) { cache = rad_malloc(sizeof(NAS_PORT)); cache->nasaddr = ut.nas_address; cache->port = ut.nas_port; cache->offset = off; cache->next = inst->nas_port_list; inst->nas_port_list = cache; } ut.type = P_LOGIN; write(fd, &ut, sizeof(u)); } /* * The user has logged off, delete the entry by * re-writing it in place. */ if (status == PW_STATUS_STOP) { if (r > 0) { u.type = P_IDLE; u.time = ut.time; u.delay = ut.delay; write(fd, &u, sizeof(u)); } else if (r == 0) { radlog(L_ERR, "rlm_radutmp: Logout for NAS %s port %d, but no Login record", nas, ut.nas_port); r = -1; } } close(fd); /* and implicitely release the locks */ return RLM_MODULE_OK; } /* * See if a user is already logged in. Sets request->simul_count to the * current session count for this user and sets request->simul_mpp to 2 * if it looks like a multilink attempt based on the requested IP * address, otherwise leaves request->simul_mpp alone. * * Check twice. If on the first pass the user exceeds his * max. number of logins, do a second pass and validate all * logins by querying the terminal server (using eg. SNMP). */ static int radutmp_checksimul(void *instance, REQUEST *request) { struct radutmp u; int fd; VALUE_PAIR *vp; uint32_t ipno = 0; char *call_num = NULL; int rcode; rlm_radutmp_t *inst = instance; char login[256]; char filename[1024]; /* * Get the filename, via xlat. */ radius_xlat(filename, sizeof(filename), inst->filename, request, NULL); if ((fd = open(filename, O_RDWR)) < 0) { /* * If the file doesn't exist, then no users * are logged in. */ if (errno == ENOENT) { request->simul_count=0; return RLM_MODULE_OK; } /* * Error accessing the file. */ radlog(L_ERR, "rlm_radumtp: Error accessing file %s: %s", filename, strerror(errno)); return RLM_MODULE_FAIL; } *login = '\0'; radius_xlat(login, sizeof(login), inst->username, request, NULL); if (!*login) { return RLM_MODULE_NOOP; } /* * WTF? This is probably wrong... we probably want to * be able to check users across multiple session accounting * methods. */ request->simul_count = 0; /* * Loop over utmp, counting how many people MAY be logged in. */ while (read(fd, &u, sizeof(u)) == sizeof(u)) { if (((strncmp(login, u.login, RUT_NAMESIZE) == 0) || (!inst->case_sensitive && (strncasecmp(login, u.login, RUT_NAMESIZE) == 0))) && (u.type == P_LOGIN)) { ++request->simul_count; } } /* * The number of users logged in is OK, * OR, we've been told to not check the NAS. */ if ((request->simul_count < request->simul_max) || !inst->check_nas) { close(fd); return RLM_MODULE_OK; } lseek(fd, (off_t)0, SEEK_SET); /* * Setup some stuff, like for MPP detection. */ if ((vp = pairfind(request->packet->vps, PW_FRAMED_IP_ADDRESS)) != NULL) ipno = vp->lvalue; if ((vp = pairfind(request->packet->vps, PW_CALLING_STATION_ID)) != NULL) call_num = vp->strvalue; /* * lock the file while reading/writing. */ rad_lockfd(fd, LOCK_LEN); /* * FIXME: If we get a 'Start' for a user/nas/port which is * listed, but for which we did NOT get a 'Stop', then * it's not a duplicate session. This happens with * static IP's like DSL. */ request->simul_count = 0; while (read(fd, &u, sizeof(u)) == sizeof(u)) { if (((strncmp(login, u.login, RUT_NAMESIZE) == 0) || (!inst->case_sensitive && (strncasecmp(login, u.login, RUT_NAMESIZE) == 0))) && (u.type == P_LOGIN)) { char session_id[sizeof(u.session_id) + 1]; char utmp_login[sizeof(u.login) + 1]; strNcpy(session_id, u.session_id, sizeof(session_id)); /* * The login name MAY fill the whole field, * and thus won't be zero-filled. * * Note that we take the user name from * the utmp file, as that's the canonical * form. The 'login' variable may contain * a string which is an upper/lowercase * version of u.login. When we call the * routine to check the terminal server, * the NAS may be case sensitive. * * e.g. We ask if "bob" is using a port, * and the NAS says "no", because "BOB" * is using the port. */ strNcpy(utmp_login, u.login, sizeof(u.login)); /* * rad_check_ts may take seconds * to return, and we don't want * to block everyone else while * that's happening. */ rad_unlockfd(fd, LOCK_LEN); rcode = rad_check_ts(u.nas_address, u.nas_port, utmp_login, session_id); rad_lockfd(fd, LOCK_LEN); /* * Failed to check the terminal server for * duplicate logins: Return an error. */ if (rcode < 0) { close(fd); return RLM_MODULE_FAIL; } if (rcode == 1) { ++request->simul_count; /* * Does it look like a MPP attempt? */ if (strchr("SCPA", u.proto) && ipno && u.framed_address == ipno) request->simul_mpp = 2; else if (strchr("SCPA", u.proto) && call_num && !strncmp(u.caller_id,call_num,16)) request->simul_mpp = 2; } else { /* * False record - zap it. */ session_zap(request->packet->sockfd, u.nas_address, u.nas_port, login, session_id, u.framed_address, u.proto, 0); } } } close(fd); /* and implicitely release the locks */ return RLM_MODULE_OK; } /* globally exported name */ module_t rlm_radutmp = { "radutmp", 0, /* type: reserved */ NULL, /* initialization */ radutmp_instantiate, /* instantiation */ { NULL, /* authentication */ NULL, /* authorization */ NULL, /* preaccounting */ radutmp_accounting, /* accounting */ radutmp_checksimul, /* checksimul */ NULL, /* pre-proxy */ NULL, /* post-proxy */ NULL /* post-auth */ }, radutmp_detach, /* detach */ NULL, /* destroy */ };