+++ /dev/null
-FreeRADIUS Password Caching for Unix modules
-
-If you are using FreeBSD or NetBSD, then you do NOT want to cache the
-passwords! See 'radiusd.conf', the comments in the 'unix' section,
-for more details.
-
-Acknowledgements
-
-Thanks to Alan DeKok for the initial idea, to Dr. Bob Pilgrim for
-implementation strategies, and to Miquel for putting it into FreeRADIUS.
-
-What does this caching do?
-
-It will add the capability to cache several of the system files that
-are used often by FreeRADIUS. These include: /etc/passwd, /etc/shadow
-(if necessary), and /etc/group.
-
-By caching the information in these files and storing it in a VERY efficient
-hash table, authentication lookups are sped up many times over. So, the
-short answer is that your authentication requests will be faster if you use
-caching, assuming you're using System authenication now. Read on for the
-long answer.
-
-How do I use it?
-
-Compile radiusd as you normally would, and then set 'cache=yes' in your
-radiusd.conf file.
-
-Then, if you want to monitor how the caching is doing (errors and such),
-'grep HASH /var/log/radius.log'.
-
-What are the advantages?
-
-Tremendous speed increase in authentication request handling. Orders of
-magnitude, literally.
-
-What are the disadvantages?
-
-As with anything else, there are a couple of trade-offs. However, these are
-quite modest, IMHO.
-
-First, memory consumption of your radiusd process will go up somewhat over
-normal operation. Depending on the size of your files, it could take several
-megabytes of RAM to build the hash table properly. However, I have gone to
-great lengths to ensure that there are no memory leaks. The size of the
-daemon process should stay constant, or grow linearly with the size of your
-system files. If you notice otherwise, send me an email with lots of detail
-about your system and I'll see what I can find.
-
-Second, you should kill -HUP the main radiusd process every so often out of
-cron. The reason for this is because the daemon will need to rebuild the
-cache to get passwords that you have changed for existing users. You will
-not have to -HUP it for new users in /etc/passwd to get authenticated, as if
-it can't find the user in the hash, it will automatically revert to the old
-(slow) lookup methods [getpwnam()]. However, if you change the password for
-a user, and the hash table contains that user but with the old password, it
-will simply think that the user is supplying the wrong password and deny
-them access. One thing I'd like to point out is that rebuilding the hash
-with the -HUP takes less than a second with our ~1MB /etc/passwd and shadow
-files. The slowest part of the -HUP is caused by the gethostby*() functions.
-All in all, a -HUP reconfig is virtually instant on our solaris boxes, and
-takes about 15 seconds on standard linux 2.x machines.
-
-Why was it written?
-
-In examining our radius servers, I noticed that there were several things
-slowing down an authentication requests. Please note that some of these
-things will depend on what you are doing in your raddb/users file, however,
-most of the things we were doing were quite common, so I feel that others
-may have similar setups. In any case, I'll go thru exactly what we were
-seeing, and how caching fixes it.
-
-Authentication Bottlenecks:
-*Group check items calling getgrnam()
-*DEFAULT users with System authentication calling getpwnam()
-*Simultaneous use check reading file /var/log/radutmp
-
-First, the auth request comes in, and radius (usually) forks a child to
-handle it. The child will then check the raddb/users file to see if the user
-is explicitly listed there. In our case, most the users were matched off of
-a DEFAULT system entry, meaning they would be looked up in the /etc/passwd
-and /etc/shadow files. However, first, we had to check for a couple of
-groups that aren't allowed access. In our case we had two DEFAULT group
-compare check items, meaning that the getgrnam() system call was called
-twice for every authentication request. Considering that the /etc/group file
-is not so large, that wasn't a huge factor. However, the /etc/group file is
-stored on disk, and was accessed through system calls, meaning it had to be
-opened and searched until a matching group was found (twice), slowing down
-authentication requestions somewhat.
-
-Ok, so we've established that there's a getgrnam() call for each DEFAULT
-user you have with a group check item. If you don't have any such DEFAULTs,
-no problem, you don't have those calls. However, nearly 99% of you probably
-have a line like the following in your raddb/users file, leading into the
-next bottleneck: DEFAULT Auth-Type = System, Simultaneous-Use = 1
-
-What does that mean? It means, for every auth request that comes in for
-which we don't have an explicit match in our raddb/users file, lookup the
-user in /etc/passwd and/or /etc/shadow using getpwnam() and getspwnam()
-system calls. So what's so bad about that? It works right? Well, yes, but
-it's inefficient because of the way getpwnam() works.
-
-Think for a second about what getpwnam() has to do. It is a linear search
-algorithm through an unsorted list stored on disk. It is an O(N) algorithm,
-for you computer science folks (barring any caching build into the function
-by particular OSs). So, basically, what it does is open the /etc/passwd
-file, start at line one, compare that user to the user we're looking for,
-and if there's no match, go to the next line and repeat. That means that on
-average, every auth request you're taking requires opening and reading half
-of your /etc/passwd file. Worst case is when a user not in the file is
-looked up, as you search the entire file without a match. Best case is when
-the user is the first one in the file, and average is in the middle
-somewhere.
-
-Are you saying, that doesn't sound so bad? Well, then perhaps caching
-isn't for you. In our case, we had a ~15,000 line passwd file, and anywhere
-from 20-40,000 logins daily. The getpwnam() call was taking by far the
-majority of the radius daemon's time, resulting in fairly poor
-authentication performance.
-
-NOTE: radutmp caching is disabled for now until we can find a better way
-to do it. Still, this next paragraph indicates why it needs doing.
-
-Lastly, if you're using the wonderful simultaneous-use check item in
-FreeRADIUS, you have another disk lookup to perform on your /var/log/radutmp
-file (actually, caching of this file just came available in b17. glad
-somebody else noticed it :) What happens is, if the user is authenticated
-properly, the daemon then opens the radutmp file, reads until it hits a line
-matching the user currently requesting authentication, and then either
-allows or denies access based on the presense or absense of a match. Thus,
-in the majority of cases, you read the entire file in for every
-authentication request (assuming that most of your users aren't trying to
-steal service from you :) .
-
-So, we've identified the problems. If you really care about how caching
-addresses those problems, keep reading.
-
-How does it work?
-
-The code will first open your /etc/passwd file and runs through it hashing
-all the usernames into a numeric index into my hash table. It then creates
-an entry into the hashtable at that index for every user in the file.
-Currently, it caches username, password, gid, and uid in the entry. Then, if
-you have shadow passwords, it will open your /etc/shadow file and put the
-passwords into the entry corresponding with the appropriate user. It is this
-hash table that allows us to eliminate the getpwnam() bottleneck. Read below
-for how to test it.
-
-Next, it will read the /etc/group file and build a singly linked list of it.
-It's not as fast as the hash, but the group file is usually small enough
-that it wouldn't make a difference. Just brining it into RAM should be good
-enough. This takes care of the getgrnam() bottleneck.
-
-NOTE: Again, radutmp caching is disabled. Disregard this next paragraph.
-
-Finally, it reads the /var/log/radutmp file and marks every user listed as
-logged in with a login variable stored in their hash entry. Then, every time
-a user logs in or out, that variable is updated so that we can avoid reading
-in /var/log/radutmp.
-
-So what, right? Well, understand that for every user lookup done on a
-system, you have currently to read half of your passwd file. Thus, the
-number of comparisons done per getpwnam() is the number of lines in your
-passwd file divided by two. So do 'wc -l /etc/passwd', divide that by two,
-and that's the number of comparisons done per user lookup on your system
-right now.
-
-Using the hash table method, I have achieved user lookups in RAM in near
-constant time. That is, using my passwd files (everyone's is different,
-obviously), I could find a user in the hash in 1.0673 comparisons (on
-average). Being that 1.0 is the lowest number of comparisons possible,
-that's a pretty significant thing. Again, for the computer science people,
-the hash function and table are wide enough such that 87.4% of users are
-stored singly in a bucket, 11.7% are stored with two users in a bucket, 0.8%
-are stored with 3 users in the bucket, and 0.03% are stored with more than 3
-users per bucket. That means that 87% of your lookups are done in one
-comparison, versus the number you got when you did the 'wc -l /etc/passwd' /
-2 above. This discussion has also neglected the fact that RAM searches are
-many, many times faster than disk searches. We'll let it speak for itself.
-
-But, you don't have to take my word for it. I have a test program that I've
-written that can show you the difference on your own system between the
-speed in lookups using the hash and getpwnam(). You can find the program
-cachetest.c at this end of this file. Simply save it to cachetest.c and
-and compile it with:
-
-gcc -o cachetest ./cachetest.c
-
-What it does is read in your passwd file, build a hash of it, and then
-perform the number of lookups you specify either with getpwnam() or using
-the hash lookup functions.
-
-Before performing tests, please note that the larger the passwd file, the
-larger the disparity between the two test methods. So you'll get a more
-accurate depiction of what the code will do for your system by running it
-on a passwd file on your radius server.
-
-I'd recommend doing the following tests with it:
-
-# Perform 1,000 lookups using getpwnam()
-./cachetest 1000 -s
-
-# Perform 1,000 lookups using the hash
-./cachetest 1000
-
-My guess, no real significant difference in the time taken to complete each
-of those. Next test.
-
-# Perform 10,000 lookups using getpwnam()
-./cachetest 10000 -s
-
-# Perform 10,000 lookups using the hash
-./cachetest 10000
-
-Unless you've got a real fast machine, you should have noticed a little bit
-of difference between the two that time. Next test
-
-# Perform 50,000 lookups using getpwnam()
-./cachetest 50000 -s
-
-# Perform 50,000 lookups using the hash
-./cachetest 50000
-
-Unless you have a cray, I'm real confident you noticed a difference that
-time. If the 50,000 user lookup didn't extraordinarily long on your system,
-try this last test.
-
-# Perform 100,000 lookups using getpwnam()
-./cachetest 100000 -s
-
-# Perform 100,000 lookups using the hash
-./cachetest 100000
-
-No doubt, you saw the difference here. Unless you want to be old and gray
-when you finish the getpwnam() tests, I'd recommend not going higher than
-100,000. Still, it's up to you. Go as high as you want on the hash test.
-1,000,000 hash lookups are done in about a second on my pentium 400 running
-linux.
-
-How will it help my radius server?
-
-Well, assuming you can live with the disadvantages listed above, it'll make
-it faster. If you haven't run the tests in the above section, I'd recommend
-doing so.
-
-Is it stable?
-
-As far as I can tell, yes. We've run it on both linux and solaris maches for
-about two weeks now, and had zero problems. If you notice a bug, by all
-means, let me know and I'll see if I can get a fix out. However, if you
-notice system degrading problems with it, you can always turn it off in
-your radiusd.conf file.
-
-Ok, that's it. If you made it this far, I'm impressed. Let me know what you
-think...
-
-jeff@apex.net
-
---------------------------------------------------------------------------
-
-/*
- * cachetest.c: Tests performance difference between user lookups in
- * the hash table vs. getpwnam()
- *
- * All users in the passwd/shadow files are stored in a hash table.
- * the hash lookup is VERY fast, generally 1.0673 comparisons per
- * lookup. For the unitiated, that's blazing. You can't have less
- * than one comparison, for example.
- *
- * (c) 1999 Author - Jeff Carneal, Apex Internet Services, Inc.
- */
-/*
- COMPILE: gcc -o cachetest cachetest.c
-*/
-#include<stdio.h>
-#include<fcntl.h>
-#include<grp.h>
-#include<unistd.h>
-#include<stdlib.h>
-#include<errno.h>
-#include<ctype.h>
-#include<sys/stat.h>
-#include<sys/types.h>
-#include<string.h>
-
-/* Misc definitions */
-#define BUFSIZE 1024
-#define MAXUSERNAME 20
-#define HASHTABLESIZE 100000
-#define PASSWDFILE "/etc/passwd"
-
-/* Structure definitions */
-struct mypasswd {
- char *pw_name; /* user name */
- char *pw_passwd; /* user password */
- uid_t pw_uid; /* user id */
- gid_t pw_gid; /* group id */
- int loggedin; /* number of logins */
- struct mypasswd *next; /* next */
-};
-
-/* Function prototypes */
-int buildHashTable(void);
-struct mypasswd *findHashUser(const char *user);
-int storeHashUser(struct mypasswd *new, int index);
-int hashUserName(const char *s);
-int doHashTest(int numusers, unsigned long numlookups);
-int usage(void);
-
-/* Make the tables global since so many functions rely on them */
-static struct mypasswd *hashtable[HASHTABLESIZE];
-static struct mygroup *grphead = NULL;
-
-char allusers[HASHTABLESIZE][MAXUSERNAME];
-int usegetpwnam=0;
-
-int main(int argc, char** argv) {
- int numusers=0;
- unsigned long numlookups=0;
-
- if(argc<2) {
- usage();
- }
-
- if(isdigit(argv[1][0])) {
- numlookups = atoi(argv[1]);
- } else {
- usage();
- }
- if((argv[2] != NULL) && !strncmp(argv[2],"-s",2)) {
- usegetpwnam = 1;
- }
-
- memset((char *)allusers, 0, MAXUSERNAME*HASHTABLESIZE);
-
- numusers = buildHashTable();
- if(numusers < 0) {
- printf("HASH: Error building hash table! Quitting...\n");
- exit(1);
- }
-
- doHashTest(numusers, numlookups);
- printf("Done!\n\n");
-
-}
-
-/* Builds the hash table up by storing passwd/shadow fields
- * in memory. Returns -1 on failure, 0 on success.
- */
-int buildHashTable(void) {
- FILE *passwd, *shadow;
- char buffer[BUFSIZE];
- char idtmp[10];
- char username[MAXUSERNAME];
- char *ptr, *bufptr;
- int len, hashindex, numread=0;
- struct mypasswd *new, *cur, *next;
-
- memset((char *)username, 0, MAXUSERNAME);
-
- /* Init hash array */
- memset((struct mypasswd *)hashtable, 0, (HASHTABLESIZE*(sizeof(struct mypasswd *))));
-
- if( (passwd = fopen(PASSWDFILE, "r")) == NULL) {
- printf("HASH: Can't open file %s: %s", PASSWDFILE, strerror(errno));
- return -1;
- } else {
- while(fgets(buffer, BUFSIZE , passwd) != (char *)NULL) {
- numread++;
-
- bufptr = buffer;
- /* Get usernames from password file */
- for(ptr = bufptr; *ptr!=':'; ptr++);
- len = ptr - bufptr;
- if((len+1) > MAXUSERNAME) {
- printf("HASH: Username too long in line: %s", buffer);
- }
- strncpy(username, buffer, len);
- username[len] = '\0';
- if(numread < HASHTABLESIZE) {
- strncpy(allusers[numread-1], username, MAXUSERNAME);
- }
-
- /* Hash the username */
- hashindex = hashUserName(username);
- /*printf("%s:%d\n", username, hashindex);*/
-
- /* Allocate space for structure to go in hashtable */
- if((new = (struct mypasswd *)malloc(sizeof(struct mypasswd))) == NULL) {
- printf("HASH: Out of memory!");
- return -1;
- }
- memset((struct mypasswd *)new, 0, sizeof(struct mypasswd));
-
- /* Put username into new structure */
- if((new->pw_name = (char *)malloc(strlen(username)+1)) == NULL) {
- printf("HASH: Out of memory!");
- return -1;
- }
- strncpy(new->pw_name, username, strlen(username)+1);
-
- /* Put passwords into array, if not shadowed */
- /* Get passwords from password file (shadow comes later) */
- ptr++;
- bufptr = ptr;
- while(*ptr!=':')
- ptr++;
-
- #if defined(NOSHADOW)
- /* Put passwords into new structure (*/
- len = ptr - bufptr;
- if((new->pw_passwd = (char *)malloc(len+1)) == NULL) {
- printf("HASH: Out of memory!");
- return -1;
- }
- strncpy(new->pw_passwd, bufptr, len);
- new->pw_passwd[len] = '\0';
- #endif /* NOSHADOW */
-
- /*
- * Put uid into structure. Not sure why, but
- * at least we'll have it later if we need it
- */
- ptr++;
- bufptr = ptr;
- while(*ptr!=':')
- ptr++;
- len = ptr - bufptr;
- strncpy(idtmp, bufptr, len);
- idtmp[len] = '\0';
- new->pw_uid = (uid_t)atoi(idtmp);
-
- /*
- * Put gid into structure.
- */
- ptr++;
- bufptr = ptr;
- while(*ptr!=':')
- ptr++;
- len = ptr - bufptr;
- strncpy(idtmp, bufptr, len);
- idtmp[len] = '\0';
- new->pw_gid = (gid_t)atoi(idtmp);
-
- /*
- * We're skipping name, home dir, and shell
- * as I can't think of any use for storing them
- */
-
- /*printf("User: %s, UID: %d, GID: %d\n", new->pw_name, new->pw_uid, new->pw_gid);*/
- /* Store user in the hash */
- storeHashUser(new, hashindex);
- } /* End while(fgets(buffer, BUFSIZE , passwd) != (char *)NULL) { */
- } /* End if */
- fclose(passwd);
-
- return numread;
-}
-
-/*
- * Looks up user in hashtable. If user can't be found, returns 0.
- * Otherwise returns a pointer to the structure for the user
- */
-struct mypasswd *findHashUser(const char *user) {
-
- struct mypasswd *cur;
- int index;
-
- /* first hash the username and get the index into the hashtable */
- index = hashUserName(user);
-
- cur = hashtable[index];
-
- while((cur != NULL) && (strcmp(cur->pw_name, user))) {
- cur = cur->next;
- }
-
- if(cur) {
- return cur;
- }
-
- return (struct mypasswd *)0;
-
-}
-
-/* Stores the username sent into the hashtable */
-int storeHashUser(struct mypasswd *new, int index) {
-
- /* store new record at beginning of list */
- new->next = hashtable[index];
- hashtable[index] = new;
-
- return 1;
-}
-
-/* Hashes the username sent to it and returns index into hashtable */
-int hashUserName(const char *s) {
- unsigned long hash = 0;
-
- while (*s != '\0') {
- hash = hash * 7907 + (unsigned char)*s++;
- }
-
- return (hash % HASHTABLESIZE);
-}
-
-int doHashTest(int numusers, unsigned long numlookups) {
- int i, numlookedup = 0;
-
- /* Use pokey getpwnam() syscall to find users */
- if(usegetpwnam) {
- printf("\nUsing getpwnam() lookup for %d user lookups over %d users found\n\n", numlookups, numusers);
- while(numlookedup < numlookups) {
- for(i=0; i<numusers; i++) {
- if(numlookedup >= numlookups)
- return 0;
-
- if(allusers[i] != NULL) {
- getpwnam(allusers[i]);
- numlookedup++;
- }
- }
- }
-
- /* Use blazing fast hash method to find user */
- } else {
- printf("\nUsing hash lookup for %d user lookups over %d users found\n\n", numlookups, numusers);
- while(numlookedup < numlookups) {
- for(i=0; i<numusers; i++) {
- if(numlookedup >= numlookups)
- return 0;
-
- if(allusers[i] != NULL) {
- findHashUser(allusers[i]);
- numlookedup++;
- }
- }
- }
- }
- return 0;
-}
-
-int usage() {
- printf("\nUsage: ./cachetest #lookups [-s]\n");
- printf("\n\tPerform 50,000 lookups using the hash method");
- printf("\n\tEx: ./cachetest 50000\n");
- printf("\n\tPerform 50,000 lookups using the getpwnam() syscall");
- printf("\n\tEx: ./cachetest 50000 -s\n\n");
- exit(1);
-
-}