It seems that Mac OS X does not come with Ident daemon. Naturally this means virtually nothing unless you want to avoid a ~ character in your IRC nick. Not wanting to look like a fool on #unix, though I never really am or were there (maybe a few times about 11 years ago), I decided to write my own. There are other solutions out there, but many of them don’t seem to work anymore. And some solutions fake it, though I don’t consider that an actual solution. I started wondering how one would go about matching up a combination of ports with a username. I had no idea where to start. I dug around the FreeBSD netstat source code, specifically the inet.c file, to see how netstat figured it out. Since Mac OS X uses FreeBSD as one of the OS components (in a simplistic explanation), I figured that would work. That code combined with digging around in various header files to pull just what I needed resulted in a C program that would give me a listing of all TCP port combinations and the UID associated with each – which is just incredibly useful and cool IMO :-)
At that same time I was reading my Apple Developer Connection documentation yet again and playing around with Xcode 4, which I do like BTW. I am totally obsessive about reading ADC documentation to the point that doing so seems nostalgic for some reason. I sat down yesterday and decided to write a Foundation command line application to really function as an identd. I’ve used the Notification Center to call a method when a file descriptor has data in the past. A simple search off the top of my head lead me to NSSocketPort as an easy way to start listening on a port. This doesn’t seem to be used much in example code related to writing network services, but I was aiming for a quick, simple implementation. I just needed to listen and get back the socket file descriptor so that I could get a notification for when a connection is accepted, which is conveniently handled with NSFileHandle. The main class of my identd daemon is IdentServer:
//
// IdentServer.m
// identd
//
// Created by Shaun Rowland on 5/12/11.
//
// Copyright 2011 Shaun Rowland. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
#import "IdentServer.h"
@implementation IdentServer
- (id)init {
self = [super init];
if (self) {
serverSocket = [[NSSocketPort alloc] initWithTCPPort:113];
socketHandle = [[NSFileHandle alloc] initWithFileDescriptor:
[serverSocket socket]
closeOnDealloc:NO];
[socketHandle acceptConnectionInBackgroundAndNotify];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleConnection:)
name:NSFileHandleConnectionAcceptedNotification
object:nil];
userInfo = [[UserInfo alloc] init];
}
return self;
}
- (void)dealloc {
[serverSocket release];
[socketHandle release];
[userInfo release];
}
- (void)handleConnection:(NSNotification *)notification {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSDictionary *userData = [notification userInfo];
NSFileHandle *connection =
[userData objectForKey:NSFileHandleNotificationFileHandleItem];
NSData *data = [connection availableData];
char *bytes = (char *)[data bytes];
if (bytes != NULL) {
NSString *query = [NSString stringWithCString:bytes
encoding:NSASCIIStringEncoding];
NSArray *ports = [query componentsSeparatedByString:@","];
NSString *response = [userInfo getInfoLocalPort:[[ports objectAtIndex:0]
intValue]
remotePort:[[ports objectAtIndex:1]
intValue]];
[connection writeData:[response dataUsingEncoding:NSASCIIStringEncoding]];
}
[socketHandle release];
socketHandle = [[NSFileHandle alloc] initWithFileDescriptor:
[serverSocket socket]
closeOnDealloc:NO];
[socketHandle acceptConnectionInBackgroundAndNotify];
[pool drain];
}
@end
The class listens on port 113, creates an NSFileHandle object based on the socket file descriptor, sets that NSFileHandle to accept the connection in the background and notify, and finally registers itself to handle the notification in the handleConnection: method. The handleConnection: method sets up its own NSAutoReleasePool so that it can clean up any autoreleased objects (otherwise they would never be released) and does the bulk of the work. This is my first time trying to use NSSocketPort, so there was some trial and error along with my ADC reading on the spot. I found that I had to set the socketHandle NSFileHandle object to not close on dealloc: or it would stop listening, which now makes sense after working on it. The connection NSFileHandle object represents the client connecting to the server. The handleConnection: method is very simplistic. It simply tries to get the available data on connection, which generally works because the protocol is extremely simple. It simply closes the connection if there is some kind of problem. The response is also just one line and then the connection is closed.
The IdentServer class allocates and uses one instance of the UserInfo class. The UserInfo class is mostly C code and does the interesting netstat related work:
//
// UserInfo.m
// identd
//
// Created by Shaun Rowland on 5/14/11.
//
// Copyright 2011 Shaun Rowland. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
/*-
* Copyright (c) 1983, 1988, 1993, 1995
* The Regents of the University of California. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgement:
* This product includes software developed by the University of
* California, Berkeley and its contributors.
* 4. Neither the name of the University nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
#import "UserInfo.h"
@implementation UserInfo
- (char *)getUsername:(uid_t)uid {
struct passwd *pw_entry;
pw_entry = getpwuid(uid);
endpwent();
if (pw_entry == NULL)
return NULL;
else
return pw_entry->pw_name;
}
- (NSString *)getInfoLocalPort:(int)localPort remotePort:(int)remotePort {
size_t len;
char *buf;
const char *mibvar = "net.inet.tcp.pcblist";
struct tcpcb *tp;
struct inpcb *inp;
struct xinpgen *xig, *oxig;
struct xsocket *so;
char *username;
// Default response, even if there was some kind of error. This is a very
// simple daemon right now. Ident isn't really critical.
NSString *response = [NSString stringWithFormat:@"%d, %d : ERROR : NO-USER\r\n",
localPort, remotePort];
if (sysctlbyname(mibvar, 0, &len, 0, 0) < 0) {
if (errno != ENOENT) {
return response;
}
}
if ((buf = malloc(len)) == NULL) {
return response;
}
if (sysctlbyname(mibvar, buf, &len, 0, 0) < 0) {
free(buf);
return response;
}
// This code mostly comes from netstat/inet.c in FreeBSD.
oxig = xig = (struct xinpgen *)buf;
for (xig = (struct xinpgen *)((char *)xig + xig->xig_len);
xig->xig_len > sizeof(struct xinpgen);
xig = (struct xinpgen *)((char *)xig + xig->xig_len)) {
tp = &((struct xtcpcb *)xig)->xt_tp;
inp = &((struct xtcpcb *)xig)->xt_inp;
so = &((struct xtcpcb *)xig)->xt_socket;
if (inp->inp_fport != 0) {
if ((so->xso_family == PF_INET || so->xso_family == PF_INET6)
&& so->so_type == SOCK_STREAM) {
if (localPort == ntohs((u_short)inp->inp_lport) &&
remotePort == ntohs((u_short)inp->inp_fport)) {
username = [self getUsername:so->so_uid];
if (username != NULL)
response = [NSString stringWithFormat:@"%d, %d : USERID : UNIX : %s\r\n",
localPort, remotePort, username];
}
}
}
}
free(buf);
return response;
}
@end
The FreeBSD copyright from netstat’s inet.c is in there because some of the code in the getInfoLocalPort:remotePort: method comes from there. It is fairly straight forward in reading system information using sysctlbyname() for the MIB named net.inet.tcp.pcblist. That was the big secret that I did not know about, nor do I know how I would have thought to look there. But now I know, and knowing is half the battle :-) The error checks and protocol implementation are extremely simple in this case as well. It will either say there is no user or it will give the correct user. There’s more to the real thing, but I was mainly interested in getting anything that really worked for the most part. I had no interest in faking it by just returning my own username no matter what. What fun would that be?
I doubt anyone implementing an identd would do it like this, but my goals were simple:
- Figure out how one would even get this data directly in the first place without faking it.
- Write it as a Foundation application using Objective-C because that’s what I am interested in.
- Figure out how to make it work with launchd.
- Figure out how to create an install package for it.
I am sure it isn’t the most efficient solution, but there is so much that the Foundation framework is doing for free. I know how to do those things, but this is much easier and more interesting at the moment. I like the idea of using some of those design patterns as well, such as the observer pattern through notifications. I’m also sure that my application is a very 0.x version as well. Even though it’s so short and seems to work, there could be some issues I’ve not seen yet. I did hit it pretty hard with a Ruby script to test though. Besides adapting the original C code I wrote to print out the netstat related data, I wrote this in one sitting. It has been a while since I’ve done Objective-C, and this is the first time I’ve tried to do network programming with the Foundation framework. I am much more sure about straight C in that case.
I found launchd interesting as well. I’m aware of how to create a real daemon by hand. I read about that in Advanced Programming in the UNIX Environment years ago and there is even a daemon() function in the FreeBSD C library (and thus Mac OS X). You aren’t supposed to do that with launchd however, and launchd is the way you’re supposed to write something like a daemon in Mac OS X. You create a property list file in /Library/LaunchDaemons for daemons provided by the system administrator. My property list is com.shaunrowland.identd:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Disabled</key>
<false/>
<key>KeepAlive</key>
<true/>
<key>Label</key>
<string>com.shaunrowland.identd</string>
<key>Program</key>
<string>/usr/local/sbin/identd</string>
<key>UserName</key>
<string>root</string>
</dict>
</plist>
Once that is in place, along with the actual binary itself, you can load and unload that property list directly with launchctl. I also finally figured out how to create an install package, though it is the first I’ve ever created. It does have a post-install script that takes care of launchd to get things working right away after installation though. The source code is in my Git repositories and the install package is available here. Again, this is a preliminary version that I’m testing for my own use. It’s available for anyone if they find it useful under a BSD license, and the identd manual page in /usr/local/man/man8 even tells you have to uninstall it. I don’t know if I am going to really do more work on this or not, but it was a useful exercise to get me geared up for Objective-C/Cocoa work. Right now my world is focused on Ruby, Rails, and Cocoa. I’m perfectly happy with that plan!