diff --git a/utils/nfsdcltrack/Makefile.am b/utils/nfsdcltrack/Makefile.am
index 073a71b..afae455 100644
--- a/utils/nfsdcltrack/Makefile.am
+++ b/utils/nfsdcltrack/Makefile.am
@@ -4,11 +4,13 @@ man8_MANS	= nfsdcld.man
 EXTRA_DIST	= $(man8_MANS)
 
 AM_CFLAGS	+= -D_LARGEFILE64_SOURCE
-sbin_PROGRAMS	= nfsdcld
+sbin_PROGRAMS	= nfsdcld nfsdcltrack
 
 nfsdcld_SOURCES = nfsdcld.c sqlite.c
-
 nfsdcld_LDADD = ../../support/nfs/libnfs.a $(LIBEVENT) $(LIBSQLITE) $(LIBCAP)
 
+nfsdcltrack_SOURCES = nfsdcltrack.c sqlite.c
+nfsdcltrack_LDADD = ../../support/nfs/libnfs.a $(LIBSQLITE) $(LIBCAP)
+
 MAINTAINERCLEANFILES = Makefile.in
 
diff --git a/utils/nfsdcltrack/nfsdcltrack.c b/utils/nfsdcltrack/nfsdcltrack.c
new file mode 100644
index 0000000..db7e534
--- /dev/null
+++ b/utils/nfsdcltrack/nfsdcltrack.c
@@ -0,0 +1,435 @@
+/*
+ * nfsdcltrack.c -- NFSv4 client name tracking program
+ *
+ * Copyright (C) 2012 Jeff Layton <jlayton@redhat.com>
+ *
+ * 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., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif /* HAVE_CONFIG_H */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <getopt.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <libgen.h>
+#include <sys/inotify.h>
+#ifdef HAVE_SYS_CAPABILITY_H
+#include <sys/prctl.h>
+#include <sys/capability.h>
+#endif
+
+#include "xlog.h"
+#include "sqlite.h"
+
+#ifndef CLD_DEFAULT_STORAGEDIR
+#define CLD_DEFAULT_STORAGEDIR NFS_STATEDIR "/nfsdcltrack"
+#endif
+
+/* defined by RFC 3530 */
+#define NFS4_OPAQUE_LIMIT	1024
+
+/* private data structures */
+struct cltrack_cmd {
+	char *name;
+	bool needs_arg;
+	int (*func)(const char *arg);
+};
+
+/* forward declarations */
+static int cltrack_init(const char *unused);
+static int cltrack_create(const char *id);
+static int cltrack_remove(const char *id);
+static int cltrack_check(const char *id);
+static int cltrack_gracedone(const char *gracetime);
+
+/* global variables */
+static struct option longopts[] =
+{
+	{ "help", 0, NULL, 'h' },
+	{ "debug", 0, NULL, 'd' },
+	{ "foreground", 0, NULL, 'f' },
+	{ "storagedir", 1, NULL, 's' },
+	{ NULL, 0, 0, 0 },
+};
+
+static struct cltrack_cmd commands[] =
+{
+	{ "init", false, cltrack_init },
+	{ "create", true, cltrack_create },
+	{ "remove", true, cltrack_remove },
+	{ "check", true, cltrack_check },
+	{ "gracedone", true, cltrack_gracedone },
+	{ NULL, false, NULL },
+};
+
+static char *storagedir = CLD_DEFAULT_STORAGEDIR;
+
+/* common buffer for holding id4 blobs */
+static unsigned char blob[NFS4_OPAQUE_LIMIT];
+
+static void
+usage(char *progname)
+{
+	printf("%s [ -hfd ] [ -s dir ] < cmd > < arg >\n", progname);
+	printf("Where < cmd > is one of the following and takes the following < arg >:\n");
+	printf("    init\n");
+	printf("    create <nfs_client_id4>\n");
+	printf("    remove <nfs_client_id4>\n");
+	printf("    check  <nfs_client_id4>\n");
+	printf("    gracedone <epoch time>\n");
+}
+
+
+/**
+ * hex_to_bin - convert a hex digit to its real value
+ * @ch: ascii character represents hex digit
+ *
+ * hex_to_bin() converts one hex digit to its actual value or -1 in case of bad
+ * input.
+ *
+ * Note: borrowed from lib/hexdump.c in the Linux kernel sources.
+ */
+static int
+hex_to_bin(char ch)
+{
+	if ((ch >= '0') && (ch <= '9'))
+		return ch - '0';
+	ch = tolower(ch);
+	if ((ch >= 'a') && (ch <= 'f'))
+		return ch - 'a' + 10;
+	return -1;
+}
+
+/**
+ * hex_str_to_bin - convert a hexidecimal string into a binary blob
+ *
+ * @src: string of hex digit pairs
+ * @dst: destination buffer to hold binary data
+ * @dstsize: size of the destination buffer
+ *
+ * Walk a string of hex digit pairs and convert them into binary data. Returns
+ * the resulting length of the binary data or a negative error code. If the
+ * data will not fit in the buffer, it returns -ENOBUFS (but will likely have
+ * clobbered the dst buffer in the process of determining that). If there are
+ * non-hexidecimal characters in the src, or an odd number of them then it
+ * returns -EINVAL.
+ */
+static ssize_t
+hex_str_to_bin(const char *src, unsigned char *dst, ssize_t dstsize)
+{
+	unsigned char *tmpdst = dst;
+
+	while (*src) {
+		int hi, lo;
+
+		/* make sure we don't overrun the dst buffer */
+		if ((tmpdst - dst) >= dstsize)
+			return -ENOBUFS;
+
+		hi = hex_to_bin(*src++);
+
+		/* did we get an odd number of characters? */
+		if (!*src)
+			return -EINVAL;
+		lo = hex_to_bin(*src++);
+
+		/* one of the characters isn't a hex digit */
+		if (hi < 0 || lo < 0)
+			return -EINVAL;
+
+		/* now place it in the dst buffer */
+		*tmpdst++ = (hi << 4) | lo;
+	}
+
+	return (ssize_t)(tmpdst - dst);
+}
+
+/*
+ * This program will almost always be run with root privileges since the
+ * kernel will call out to run it. Drop all capabilities prior to doing
+ * anything important to limit the exposure to potential compromise.
+ *
+ * FIXME: should we setuid to a different user early on instead?
+ */
+static int
+cltrack_set_caps(void)
+{
+	int ret = 0;
+#ifdef HAVE_SYS_CAPABILITY_H
+	unsigned long i;
+	cap_t caps;
+
+	/* prune the bounding set to nothing */
+	for (i = 0; prctl(PR_CAPBSET_READ, i, 0, 0, 0) >= 0 ; ++i) {
+		ret = prctl(PR_CAPBSET_DROP, i, 0, 0, 0);
+		if (ret) {
+			xlog(L_ERROR, "Unable to prune capability %lu from "
+				      "bounding set: %m", i);
+			return -errno;
+		}
+	}
+
+	/* get a blank capset */
+	caps = cap_init();
+	if (caps == NULL) {
+		xlog(L_ERROR, "Unable to get blank capability set: %m");
+		return -errno;
+	}
+
+	/* reset the process capabilities */
+	if (cap_set_proc(caps) != 0) {
+		xlog(L_ERROR, "Unable to set process capabilities: %m");
+		ret = -errno;
+	}
+	cap_free(caps);
+#endif
+	return ret;
+}
+
+static int
+cltrack_init(const char __attribute__((unused)) *unused)
+{
+	int ret;
+
+	/*
+	 * see if the storagedir is writable by root w/o CAP_DAC_OVERRIDE.
+	 * If it isn't then give the user a warning but proceed as if
+	 * everything is OK. If the DB has already been created, then
+	 * everything might still work. If it doesn't exist at all, then
+	 * assume that the maindb init will be able to create it. Fail on
+	 * anything else.
+	 */
+	if (access(storagedir, W_OK) == -1) {
+		switch (errno) {
+		case EACCES:
+			xlog(L_WARNING, "Storage directory %s is not writable. "
+					"Should be owned by root and writable "
+					"by owner!", storagedir);
+			break;
+		case ENOENT:
+			/* ignore and assume that we can create dir as root */
+			break;
+		default:
+			xlog(L_ERROR, "Unexpected error when checking access "
+				      "on %s: %m", storagedir);
+			return -errno;
+		}
+	}
+
+	/* set up storage db */
+	ret = sqlite_maindb_init(storagedir);
+	if (ret) {
+		xlog(L_ERROR, "Failed to init database: %d", ret);
+		/*
+		 * Convert any error here into -EACCES. It's not truly
+		 * accurate in all cases, but it should cause the kernel to
+		 * stop upcalling until the problem is resolved.
+		 */
+		ret = -EACCES;
+	}
+	return ret;
+}
+
+static int
+cltrack_create(const char *id)
+{
+	int ret;
+	ssize_t len;
+
+	xlog(D_GENERAL, "%s: create client record.", __func__);
+
+	ret = sqlite_prepare_dbh(storagedir);
+	if (ret)
+		return ret;
+
+	len = hex_str_to_bin(id, blob, sizeof(blob));
+	if (len < 0)
+		return (int)len;
+
+	ret = sqlite_insert_client(blob, len);
+
+	return ret ? -EREMOTEIO : ret;
+}
+
+static int
+cltrack_remove(const char *id)
+{
+	int ret;
+	ssize_t len;
+
+	xlog(D_GENERAL, "%s: remove client record.", __func__);
+
+	ret = sqlite_prepare_dbh(storagedir);
+	if (ret)
+		return ret;
+
+	len = hex_str_to_bin(id, blob, sizeof(blob));
+	if (len < 0)
+		return (int)len;
+
+	ret = sqlite_remove_client(blob, len);
+
+	return ret ? -EREMOTEIO : ret;
+}
+
+static int
+cltrack_check(const char *id)
+{
+	int ret;
+	ssize_t len;
+
+	xlog(D_GENERAL, "%s: check client record", __func__);
+
+	ret = sqlite_prepare_dbh(storagedir);
+	if (ret)
+		return ret;
+
+	len = hex_str_to_bin(id, blob, sizeof(blob));
+	if (len < 0)
+		return (int)len;
+
+	ret = sqlite_check_client(blob, len);
+
+	return ret ? -EPERM : ret;
+}
+
+static int
+cltrack_gracedone(const char *timestr)
+{
+	int ret;
+	char *tail;
+	time_t gracetime;
+
+
+	ret = sqlite_prepare_dbh(storagedir);
+	if (ret)
+		return ret;
+
+	errno = 0;
+	gracetime = strtol(timestr, &tail, 0);
+
+	/* did the resulting value overflow? (Probably -ERANGE here) */
+	if (errno)
+		return -errno;
+
+	/* string wasn't fully converted */
+	if (*tail)
+		return -EINVAL;
+
+	xlog(D_GENERAL, "%s: grace done. gracetime=%ld", __func__, gracetime);
+
+	ret = sqlite_remove_unreclaimed(gracetime);
+
+	return ret ? -EREMOTEIO : ret;
+}
+
+static struct cltrack_cmd *
+find_cmd(char *cmdname)
+{
+	struct cltrack_cmd *current = &commands[0];
+
+	while (current->name) {
+		if (!strcmp(cmdname, current->name))
+			return current;
+		++current;
+	}
+
+	xlog(L_ERROR, "%s: '%s' doesn't match any known command",
+			__func__, cmdname);
+	return NULL;
+}
+
+int
+main(int argc, char **argv)
+{
+	char arg;
+	int rc = 0;
+	char *progname, *cmdarg = NULL;
+	struct cltrack_cmd *cmd;
+
+	progname = strdup(basename(argv[0]));
+	if (!progname) {
+		fprintf(stderr, "%s: unable to allocate memory.\n", argv[0]);
+		return 1;
+	}
+
+	xlog_syslog(1);
+	xlog_stderr(0);
+
+	/* process command-line options */
+	while ((arg = getopt_long(argc, argv, "hdfs:", longopts,
+				  NULL)) != EOF) {
+		switch (arg) {
+		case 'd':
+			xlog_config(D_ALL, 1);
+		case 'f':
+			xlog_syslog(0);
+			xlog_stderr(1);
+			break;
+		case 's':
+			storagedir = optarg;
+			break;
+		default:
+			usage(progname);
+			return 0;
+		}
+	}
+
+	xlog_open(progname);
+
+	/* we expect a command, at least */
+	if (optind >= argc) {
+		xlog(L_ERROR, "Missing command name\n");
+		rc = -EINVAL;
+		goto out;
+	}
+
+	/* drop all capabilities */
+	rc = cltrack_set_caps();
+	if (rc)
+		goto out;
+
+	cmd = find_cmd(argv[optind]);
+	if (!cmd) {
+		rc = -EINVAL;
+		goto out;
+	}
+
+	/* populate arg var if command needs it */
+	if (cmd->needs_arg) {
+		if (optind + 1 >= argc) {
+			xlog(L_ERROR, "Command %s requires an argument\n",
+				cmd->name);
+			rc = -EINVAL;
+			goto out;
+		}
+		cmdarg = argv[optind + 1];
+	}
+	rc = cmd->func(cmdarg);
+out:
+	free(progname);
+	return rc;
+}
