/* authd, a lightweight IPv6/IPv4 inetd RFC 1413 ident protocol daemon * Copyright 2004 by Red Hat, Inc. * * THIS PROGRAM IS RELEASED UNDER THE GPL WITH THE ADDITIONAL EXEMPTION * THAT COMPILING, LINKING, AND/OR USING OPENSSL IS ALLOWED. * * 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. * * Initial Author: Adrian Havill */ static const char RCSID[] = "$Revision: 1.18 $ $Date: 2004/07/28 16:04:05 $"; #include "config.h" #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if !defined NDEBUG #include #endif #define _(ascii_msgid) gettext(ascii_msgid) static struct { int log_mask; int abrupt, help, hybrid, resolve, verbose, xerror; // longopt bool is pint bool debug, error, log, number, other, Version; char *codeset, *ident, *lang, *mapped, *os, *passwd, *username; char *Encrypt, *Noident; unsigned timeout, fn; unsigned long long multiquery; } opt; static void log_printf(int level, const char *s, ...) { if (opt.debug || level != LOG_DEBUG) { va_list ap; if (s == NULL || *s == '\0') { s = strerror(errno); assert(strchr(s, '%') == NULL); } if (opt.log) { va_start(ap, s); vsyslog(level, s, ap); va_end(ap); } va_start(ap, s); if (vfprintf(level == LOG_INFO ? stdout : stderr, s, ap) < 0) { perror(program_invocation_name); level = INT_MIN; } va_end(ap); } if (level <= LOG_ERR) exit(EXIT_FAILURE); } #define debug(...) log_printf(LOG_DEBUG, __VA_ARGS__) #define show(...) log_printf(LOG_INFO, __VA_ARGS__) #define log_notice(...) log_printf(LOG_NOTICE, __VA_ARGS__) #define log_warning(...) log_printf(LOG_WARNING, __VA_ARGS__) #define handle_error(...) log_printf(LOG_ERR, __VA_ARGS__) #if !defined NDEBUG static const void *const LAST_VA_ARG = "FIXME"; // debugging sentinel #endif static char *vstrdup(const char *s, ...) { va_list ap; char *dup; assert(s != LAST_VA_ARG); va_start(ap, s); while (s == NULL || *s == '\0') { s = va_arg(ap, const char *); assert(s != LAST_VA_ARG); } va_end(ap); if ((dup = strdup(s)) == NULL) handle_error(NULL); return dup; } #if !defined NDEBUG #define vstrdup(s, ...) vstrdup((s) ,##__VA_ARGS__ ,LAST_VA_ARG) #endif static void show_help(void) { show(_("%s [options]... [request]...\n"), program_invocation_short_name); show(_(" -d\t\t\tOutput debug messages\n")); show(_(" -E[cipher]\t\tEncrypt username using [%s]\n"), DFL_CIPHER); show(_(" -e\t\t\tSend UNKNOWN-ERROR for common error messages\n")); show(_(" -l[mask]\t\tLog to syslog, [mask] for priority levels\n")); show(_(" -m[reps]\t\tAllow at most [unlimited] requests/connection\n")); show(_(" -N[basename]\t\tHide users with ~/[%s] file\n"), DFL_NO_IDENT); show(_(" -n\t\t\tSend uid number instead of username\n")); show(_(" -o\t\t\tSend OTHER instead of the operating system\n")); show(_(" -t[secs]\t\tTimeout request after [%u] seconds\n"), DFL_T_O); show(_(" -V\t\t\tPrint program and version information\n")); show(_(" --abrupt\t\tDisconnect without displaying error msgs\n")); show(_(" --codeset=rfc1340\tSend [charset] if reply is not ASCII\n")); show(_(" --fn[=fields]\t\tUse [all] gecos fields instead of username\n")); show(_(" --hybrid\t\tUse hybrid notation (ex. ::127.0.0.1) for IPv6\n")); show(_(" --ident[=basename]\tHide users with no ~/[%s]\n"), DFL_IDENT); show(_(" --lang=lc\t\tLocalize messages, charsets, & time to [locale]\n")); show(_(" --mapped=ipv6\t\tMap addresses with same top 96 bits to IPv4\n")); show(_(" --os[=rfc1340]\t\tUse [uname], not %s, as OS name\n"), DFL_OS); show(_(" --passwd=pathname\tUse line 1 of [%s] for crypto\n"), DFL_PASSWD); show(_(" --resolve\t\tUse host and service names if available\n")); show(_(" --username[=login]\tUse [%s] as username\n"), DFL_USERNAME); show(_(" --verbose\t\tAdd real uid, addresses/ports, & timestamps\n")); show(_(" --xerror\t\tSend more helpful error messages for rare errors\n")); } static void show_version(void) { show("%s-%s:\n\t%s\n\t%s\n", PACKAGE, VERSION, RCSID, CONTACT); } static bool is_print_ascii(const char *s) { assert(s != NULL); while (*s != '\0') if (!isascii(*s) || iscntrl(*s)) return false; else s++; return true; } #define is_in_range(lo,x,hi) ((lo) <= (x) && (x) <= (hi)) static bool is_rfc1413_token(const char *s) { assert(s != NULL); if (!is_print_ascii(s) || strchr(s, ':') != NULL) return false; return is_in_range((size_t) 1, strlen(s), (size_t) 64); } static bool is_bad_strto(const char *s, const char *endptr) { if (errno == ERANGE || errno == EINVAL) return true; return endptr == s || (*endptr != '\0' && !isspace(*endptr)); } static const int HEX_DIG = 2; // two digits in 8-bit base 16 octet static const size_t HEX_LEN_MAX = 32; // strlen() of /proc/net/tcp6 address static char *created_addr_hex(const unsigned char *addr, size_t addr_size) { char *addr_hex; size_t addr_hex_size = addr_size * HEX_DIG + sizeof '\0'; if ((addr_hex = calloc(addr_hex_size, sizeof(char))) != NULL) { size_t z = addr_size; char *p = addr_hex; while (z != 0) { int n = snprintf(p, addr_hex_size, "%.*hhX", HEX_DIG, addr[--z]); assert(n >= 0 && (size_t) n < addr_hex_size); addr_hex_size -= (size_t) n; p += (size_t) n; } } return addr_hex; } static char *created_pton_hex(const char *prefix) { struct in6_addr addr; const size_t SIZE = sizeof(addr.s6_addr); assert(prefix != NULL); if (inet_pton(AF_INET6, prefix, &addr) <= 0) return NULL; return created_addr_hex((const void *) addr.s6_addr, SIZE); } static void create_opt(int argc, char *argv[]) { enum { PRE_FIRST_LONGOPT = UCHAR_MAX, // no short opt value overlap CODESET_LONGOPT, IDENT_LONGOPT, FN_LONGOPT, LANG_LONGOPT, MAPPED_LONGOPT, OS_LONGOPT, PASSWD_LONGOPT, USERNAME_LONGOPT }; int c, i; const char *const IPV4_HEX_0 = "00000000"; const char *const S_FMT = _("%s: invalid argument to --%s: %s\n"); const char *const C_FMT = _("%s: invalid argument to -%c: %s\n"); const char *const SHORT_OPTS = "dE::ehl::m::N::not::V"; // pidentd compat const struct option LONG_OPTS[] = { { "abrupt", no_argument, &opt.abrupt, true }, { "codeset", required_argument, NULL, CODESET_LONGOPT }, { "fn", optional_argument, NULL, FN_LONGOPT }, { "help", no_argument, NULL, 'h' }, { "hybrid", no_argument, &opt.hybrid, true }, { "ident", optional_argument, NULL, IDENT_LONGOPT }, { "lang", required_argument, NULL, LANG_LONGOPT }, { "mapped", required_argument, NULL, MAPPED_LONGOPT }, { "os", optional_argument, NULL, OS_LONGOPT }, { "passwd", required_argument, NULL, PASSWD_LONGOPT }, { "resolve", no_argument, &opt.resolve, true }, { "usage", no_argument, NULL, 'h' }, { "username", optional_argument, NULL, USERNAME_LONGOPT }, { "verbose", no_argument, &opt.verbose, true }, { "xerror", no_argument, &opt.xerror, true }, { 0, 0, 0, 0 } }; assert(argc > 0 && argv != NULL); memset(&opt, 0, sizeof(opt)); opt.passwd = vstrdup(DFL_PASSWD); if ((opt.mapped = calloc(HEX_LEN_MAX + sizeof '\0', sizeof(char))) == NULL) handle_error(NULL); memset(opt.mapped, '0', HEX_LEN_MAX); opt.multiquery = 1; opt.timeout = UINT_MAX; while ((c = getopt_long(argc, argv, SHORT_OPTS, LONG_OPTS, &i)) != -1) { switch (c) { struct utsname os; unsigned long lu; char *endptr, *lc; case 'd': opt.debug = true; break; case 'E': free(opt.Encrypt); opt.Encrypt = vstrdup(optarg, DFL_CIPHER); break; case 'e': opt.error = true; break; case 'h': opt.help = true; break; case 'l': if (optarg != NULL) { lu = strtoul(optarg, &endptr, 0); if (lu > UINT_MAX || is_bad_strto(optarg, endptr)) handle_error(C_FMT, *argv, c, optarg); else opt.log_mask = (int) lu; setlogmask(opt.log_mask); } opt.log = true; break; case 'm': if (optarg != NULL) { opt.multiquery = strtoull(optarg, &endptr, 10); if (is_bad_strto(optarg, endptr)) handle_error(C_FMT, *argv, c, optarg); } else opt.multiquery = ULLONG_MAX; break; case 'n': opt.number = true; break; case 'N': free(opt.Noident); opt.Noident = vstrdup(optarg, DFL_NO_IDENT); break; case 'o': opt.other = true; break; case 't': lu = optarg == NULL ? DFL_T_O : strtoul(optarg, &endptr, 10); if (lu > UINT_MAX || is_bad_strto(optarg, endptr)) handle_error(C_FMT, *argv, c, optarg); else if (lu < 30) { log_notice(_("Timeout's too low; Raising to 30.\n")); lu = 30; } else if (lu != 0 && !is_in_range(60, lu, 180)) log_notice(_("Timeout should be from 60..180 seconds.\n")); opt.timeout = (unsigned) lu; break; case 'V': opt.Version = true; break; case CODESET_LONGOPT: if (!is_rfc1413_token(optarg)) handle_error(S_FMT, *argv, LONG_OPTS[i].name, optarg); free(opt.codeset); opt.codeset = vstrdup(optarg); break; case FN_LONGOPT: if (optarg != NULL) { lu = strtoul(optarg, &endptr, 10); if (lu > UINT_MAX || is_bad_strto(optarg, endptr)) handle_error(S_FMT, *argv, LONG_OPTS[i].name, optarg); else opt.fn = (unsigned) lu; } else opt.fn = UINT_MAX; break; case IDENT_LONGOPT: free(opt.ident); opt.ident = vstrdup(optarg, DFL_IDENT); break; case LANG_LONGOPT: lc = strlen(optarg) == 0 ? NULL : setlocale(LC_ALL, optarg); if (lc == NULL) handle_error(S_FMT, *argv, LONG_OPTS[i].name, optarg); else { free(opt.lang); opt.lang = vstrdup(lc); debug("LC_ALL = %s\n", opt.lang); } break; case MAPPED_LONGOPT: free(opt.mapped); if ((opt.mapped = created_pton_hex(optarg)) == NULL) handle_error(S_FMT, *argv, LONG_OPTS[i].name, optarg); if (strncmp(opt.mapped, IPV4_HEX_0, strlen(IPV4_HEX_0)) != 0) log_notice(_("Mapped lower 32 bits not 0; ignoring.\n")); debug("prefix map =%s/%s\n", IPV4_HEX_0, opt.mapped + 8); break; case OS_LONGOPT: if (optarg == NULL && uname(&os) != 0) handle_error(NULL); free(opt.os); opt.os = vstrdup(optarg, os.sysname); if (!is_rfc1413_token(opt.os)) handle_error(S_FMT, *argv, LONG_OPTS[i].name, optarg); break; case PASSWD_LONGOPT: free(opt.passwd); opt.passwd = vstrdup(optarg, DFL_PASSWD); if (strlen(opt.passwd) == 0) handle_error(S_FMT, *argv, LONG_OPTS[i].name, optarg); break; case USERNAME_LONGOPT: free(opt.username); opt.username = vstrdup(optarg, DFL_USERNAME); break; default: assert(0 == c); // GNU extensions ('\1' and ':') should be off break; case '?': exit(EXIT_FAILURE); } } if (optind < argc) opt.timeout = 0; // don't get client input if request(s) in cmd } static const char *const DELIM = ",: \t\r\n\v\f"; static unsigned long long get_tok_ullong(char *s, unsigned base) { unsigned long long ull = ULLONG_MAX; assert(base <= 36); if ((s = strtok(s, DELIM)) != NULL) { char *endptr; ull = strtoull(s, &endptr, (int) base); if ((errno == ERANGE && ull == ULLONG_MAX) || is_bad_strto(s, endptr)) errno = EINVAL; } else errno = EINVAL; return ull; } static unsigned long get_tok_uint(char *s, unsigned base) { unsigned long ul = ULONG_MAX; assert(base <= 36); if ((s = strtok(s, DELIM)) != NULL) { char *endptr; ul = strtoul(s, &endptr, (int) base); if (ul > UINT_MAX || is_bad_strto(s, endptr)) errno = EINVAL; } else errno = EINVAL; return ul; } static long get_tok_int(char *s, unsigned base) { long l = LONG_MAX; assert(base <= 36); if ((s = strtok(s, DELIM)) != NULL) { char *endptr; l = strtol(s, &endptr, (int) base); if (l > INT_MAX || is_bad_strto(s, endptr)) errno = EINVAL; } else errno = EINVAL; return l; } static void destroy_opt(void) { free(opt.codeset); free(opt.Encrypt); free(opt.ident); free(opt.lang); free(opt.Noident); free(opt.os); free(opt.passwd); free(opt.mapped); free(opt.username); memset(&opt, 0, sizeof(opt)); } typedef struct { unsigned long lport, rport; char *laddr, *raddr; } request_t; typedef int (*getsockaddr_t)(int, struct sockaddr *, socklen_t *); static char *created_connected_addr(int sockfd, getsockaddr_t getXXXXname) { struct sockaddr_storage name; socklen_t namelen = sizeof(name); char *addr_hex = NULL; if (getXXXXname(sockfd, (struct sockaddr *) &name, &namelen) == 0) { size_t addr_size = 0; const unsigned char *addr = NULL; switch (name.ss_family) { struct sockaddr_in *sin; struct sockaddr_in6 *sin6; case AF_INET: sin = (struct sockaddr_in *) &name; addr_size = sizeof(sin->sin_addr.s_addr); addr = (unsigned char *) &sin->sin_addr.s_addr; break; case AF_INET6: sin6 = (struct sockaddr_in6 *) &name; addr_size = sizeof(sin6->sin6_addr.s6_addr); addr = (unsigned char *) sin6->sin6_addr.s6_addr; break; default: errno = EAFNOSUPPORT; } addr_hex = created_addr_hex(addr, addr_size); } else if (errno == ENOTSOCK) errno = 0; // no error if invoked from cmdline return addr_hex; } static request_t created_request(int argc, char *argv[]) { request_t in; char *line = NULL; size_t n = 0; assert(argv != NULL && argc >= 1); if (optind >= argc) { if (fflush_unlocked(stdout) == EOF) handle_error(NULL); in.laddr = created_connected_addr(STDIN_FILENO, getsockname); in.raddr = created_connected_addr(STDIN_FILENO, getpeername); if (opt.timeout != UINT_MAX) alarm(opt.timeout); if (getline(&line, &n, stdin) == (ssize_t) -1) { if (feof_unlocked(stdin)) { free(line); exit(EXIT_SUCCESS); } assert(ferror(stdin)); handle_error(NULL); } alarm(0); } else { line = vstrdup(argv[(size_t) optind++]); in.laddr = in.raddr = NULL; } in.lport = get_tok_uint(line, 10); in.rport = get_tok_uint(NULL, 10); debug("local_address =%s:%04lX\n", in.laddr, in.lport); debug("rem_address =%s:%04lX\n", in.raddr, in.rport); free(line); return in; } static char *created_hostname(const char *node) { char *s; struct addrinfo *res, hints = { ai_flags: AI_CANONNAME }; assert(node != NULL && *node != '\0'); if (opt.resolve && getaddrinfo(node, NULL, &hints, &res) == 0) { s = vstrdup(res->ai_canonname, node); freeaddrinfo(res); } else s = vstrdup(node); return s; } static const unsigned long PORT_MIN = 1, PORT_MAX = 65535; static char *created_servicename(unsigned long port) { char *s; const char *const PROTO = "tcp", *const PORT_FMT = "%lu"; const int NS_PORT = (int) htons((uint16_t) port); struct servent *res = opt.resolve ? getservbyport(NS_PORT, PROTO) : NULL; assert(is_in_range(PORT_MIN, port, PORT_MAX)); if (res == NULL || res->s_name == NULL || *res->s_name == '\0') { if (asprintf(&s, PORT_FMT, port) < 0) handle_error(NULL); } else s = vstrdup(res->s_name); return s; } static char *get_created_tok_addr(const char *peer_addr_hex) { const char *const IPV6_0 = ":0000", *const IPV6_0_RUN = "::"; const size_t IPV6_SIZE_MAX = INET6_ADDRSTRLEN + sizeof '\0'; const size_t IPV6_0_RUN_LEN = strlen(IPV6_0_RUN); char *addr = calloc(sizeof ':' + IPV6_SIZE_MAX, sizeof(char)); char *addr_hex, *p = addr, *q, *zero = NULL; size_t zero_n = 0; if (addr != NULL) { if ((addr_hex = strtok(NULL, DELIM)) != NULL) { size_t z = strlen(addr_hex); const bool IS_IPV6 = z > 8; bool is_16_bits = true; if (peer_addr_hex != NULL) { char peer_128[HEX_LEN_MAX + sizeof '\0']; char host_128[HEX_LEN_MAX + sizeof '\0']; assert(opt.mapped != NULL); strcpy(peer_128, opt.mapped); strcpy(host_128, opt.mapped); /* If mapping IPV4 to IPV6 space is enabled, take only last 4 numbers of IPV6 */ if(opt.mapped[0]) { strncpy(host_128, addr_hex+z-8, 8); strncpy(peer_128, peer_addr_hex, 8); } else { strncpy(host_128, addr_hex, z); strncpy(peer_128, peer_addr_hex, strlen(peer_addr_hex)); } if (strcmp(peer_128, host_128) != 0) return addr; } // hex addr must have even number of digits if ((int) z & 1) { free(addr); errno = EINVAL; return NULL; } while (z > 1) { unsigned long ul; char *endptr; const bool IS_IPV4 = (opt.hybrid && z < 9) || !IS_IPV6; addr_hex[z] = '\0'; z -= HEX_DIG; ul = strtoul(addr_hex + z, &endptr, 16); if (is_bad_strto(addr_hex + z, endptr)) { free(addr); errno = EINVAL; return NULL; } if ((!IS_IPV4 || 6 == z) && is_16_bits) *p++ = ':'; if (IS_IPV4) p += sprintf(p, "%lu", ul); else p += sprintf(p, "%.*lX", HEX_DIG, ul); if (IS_IPV4 && z != 0) *p++ = '.'; is_16_bits = !is_16_bits; } } // compress 16-bit runs of zeros for (p = strstr(addr, IPV6_0); p != NULL; p = strstr(q, IPV6_0)) { size_t count = 0; const size_t IPV6_0_LEN = strlen(IPV6_0); for (q = p; strncmp(q, IPV6_0, IPV6_0_LEN) == 0; q += IPV6_0_LEN) count += IPV6_0_LEN; if (count > zero_n) { zero_n = count; zero = p; } } if (zero_n >= IPV6_0_RUN_LEN) { memcpy(zero, IPV6_0_RUN, IPV6_0_RUN_LEN); zero += IPV6_0_RUN_LEN; zero_n -= IPV6_0_RUN_LEN; if (zero[zero_n] == ':') zero_n++; memmove(zero, zero + zero_n, strlen(zero + zero_n) + 1); } // compress four digit hex sequences by removing leading zeros while ((zero = strstr(addr, ":0")) != NULL) if (strchr(++zero, ':') == NULL) break; else memmove(zero, zero + 1, strlen(zero)); if (addr[1] != ':') memmove(addr, addr + 1, IPV6_SIZE_MAX); // rm ':' prefix } return addr; } static bool is_regular_file(const char *dir, const char *name) { bool is_existing = false; assert(dir != NULL); if (name != NULL) { struct stat st; if (stat(dir, &st) == 0 && S_ISDIR(st.st_mode)) { char *s; const char *const PATHNAME_FMT = "%s/%s"; if (*name != '\0') { if (asprintf(&s, PATHNAME_FMT, dir, name) < 0) return false; is_existing = stat(s, &st) == 0 && S_ISREG(st.st_mode); free(s); } else errno = EISDIR; } else errno = ENOTDIR; if (errno == ENOENT) errno = 0; // don't report X-ERRNO-%d } return is_existing; } typedef struct { bool error; unsigned lport, rport; char *os, *enc, *s; } reply_t; static const size_t USERID_MAX_LEN = 512; // as per RFC1413 static const char *const TEXT_READ_MODE = "r"; // passphrase, /proc plaintext static bool get_info(reply_t *out, request_t in, const char *tcpname) { //unsigned long lport, rport, uid, status; FILE *stream; unsigned long lport, rport, status; FILE *stream; long uid; const unsigned long ESTABLISHED = 0x01; unsigned lineno = 0; char *laddr = NULL, *raddr = NULL; bool is_port_pair_found = false; char *created_verbose(const char *name, unsigned long id) { size_t n; time_t tod; char *s, when[USERID_MAX_LEN], *host1, *port1, *host2, *port2; const char *const UTC_FMT = "%FT%TZ", *const TZ_FMT = "(%a %EX %z/%Z)"; const char *const VERBOSE_FMT = "%s:%lu,%s,%s|%s,%s|%s"; if (time(&tod) == (time_t) -1) handle_error(NULL); n = strftime(when, sizeof(when), UTC_FMT, gmtime(&tod)); strftime(when + n, sizeof(when) - n, TZ_FMT, localtime(&tod)); host1 = created_hostname(laddr); port1 = created_servicename(lport); host2 = created_hostname(raddr); port2 = created_servicename(rport); asprintf(&s, VERBOSE_FMT, name, id, when, host1, port1, host2, port2); free(host1); free(port1); free(host2); free(port2); return s; } assert(out != NULL); if (tcpname == NULL) return false; if ((stream = fopen(tcpname, TEXT_READ_MODE)) == NULL) { // some systems won't have tcp6 (and rarely, plain IPv4 tcp) files in // /proc/net. The absence of either file isn't an error; although // the lack of both prevents authd from looking up anything if (errno == ENOENT) { if (opt.log) log_notice("%s: %s\n", tcpname, strerror(errno)); errno = 0; } else out->error = true; } else for (;;) { unsigned long sl, inode; uid_t euid; char line[PROC_MAX_LEN], tmp[PROC_MAX_LEN]; // FIXME: setting the buffer to be larger than normal is done to // lessen (but not eliminate) the chance of the proc file changing // while in the middle of reading it if (0 == lineno) { const size_t SIZE = (size_t) (BUFSIZ * PROC_SIZE); if (setvbuf(stream, NULL, _IOFBF, SIZE) != 0) { out->error = true; break; } } if (fgets_unlocked(line, (int) PROC_MAX_LEN, stream) == NULL) { if (ferror_unlocked(stream)) out->error = true; assert(feof(stream)); break; } else lineno++; // first line of /proc/net/tcp* is a header that looks similar to: // sl local rem st tx rx tr tm->when retrnsmt uid timeout inode if (1 == lineno) continue; // sample line from tcp (tcp6 just has a bigger ip address): // 0: 00000000:800B 00000000:0000 0A 0000:0000 00:0000 0000 0000 ... sl = get_tok_uint(strcpy(tmp, line), 10); // sl (base 10 uint) laddr = get_created_tok_addr(in.laddr); // local (little endian hex) lport = get_tok_uint(NULL, 16); // ... local port raddr = get_created_tok_addr(in.raddr); // remote (little endian hex) rport = get_tok_uint(NULL, 16); // ... remote port status = get_tok_uint(NULL, 16); // status (01 = ESTABLISHED) (void) get_tok_uint(NULL, 16); // tx_queue (void) get_tok_uint(NULL, 16); // rx_queue (void) get_tok_uint(NULL, 16); // tr (boolean) (void) get_tok_ullong(NULL, 16); // tm->when (unit: jiffies) strtok(NULL, DELIM); // retrnsmt //uid = get_tok_uint(NULL, 10); // uid (base 10 uint) uid = get_tok_int(NULL, 10); // uid (base 10 int) strtok(NULL, DELIM); // timeout inode = get_tok_uint(NULL, 10); // inode (base 10 uint) if (errno == EINVAL) { // format of /proc/net/tcp* has changed or it's a bogus file debug("%s:%u: %s", tcpname, lineno, line); // XXX: NL terminated? out->error = true; errno = 0; out->s = opt.xerror ? vstrdup("X-PROC") : NULL; break; } else if (errno != 0) { // most likely an out-of-memory error out->error = true; break; } else if (*laddr == '\0' || *raddr == '\0') { // host address doesn't match peer address, so skip it free(laddr); free(raddr); laddr = raddr = NULL; continue; } else euid = (uid_t) uid; if (lport == in.lport && rport == in.rport && status == ESTABLISHED) { struct passwd *pwd = getpwuid(euid); is_port_pair_found = true; debug("%-14s=sl:%lu uid:%lu inode:%lu\n", tcpname, sl, uid, inode); if (opt.username != NULL) { if ((pwd = getpwnam(opt.username)) == NULL) { out->error = true; out->s = opt.xerror ? vstrdup("X-NAME") : NULL; break; } else euid = pwd->pw_uid; } if (pwd == NULL) { // passwd db doesn't have uid in /proc/net/tcp* out->error = true; out->s = opt.xerror ? vstrdup("X-UID") : NULL; break; } else { bool hidden = opt.ident != NULL; if (is_regular_file(pwd->pw_dir, opt.ident)) hidden = false; if (is_regular_file(pwd->pw_dir, opt.Noident)) hidden = true; if (errno) { out->error = true; out->s = opt.xerror ? vstrdup("X-FILE") : NULL; break; } else if (hidden) { out->error = true; out->s = vstrdup("HIDDEN-USER"); break; } out->s = vstrdup(pwd->pw_name); if (opt.number) { const char *const UID_FMT = "%lu"; free(out->s); if (asprintf(&out->s, UID_FMT, (unsigned long) euid) < 0) { out->error = true; break; } } if (opt.fn != 0 && *pwd->pw_gecos != '\0') { char *s = vstrdup(pwd->pw_gecos); size_t n = 0; // gecos field usually just has 1 field, the full name-- // but sometimes the user's office, office phone and home // phone will be in csv format for (unsigned u = 0; u < opt.fn && s[n] != '\0'; u++, n++) n += strcspn(s + n, ","); s[--n] = '\0'; if (opt.number) { n = strcspn(s, ","); memmove(s, s + n, strlen(s + n) + 1); n = strlen(out->s) + strlen(s); if ((out->s = realloc(out->s, ++n)) == NULL) { out->error = true; break; } strcat(out->s, s); free(s); } else { free(out->s); out->s = s; } } } if (opt.verbose && !out->error) { char *brief = out->s; if ((out->s = created_verbose(brief, uid)) == NULL) out->error = true; else free(brief); } break; } else { free(laddr); free(raddr); laddr = raddr = NULL; } } free(laddr); free(raddr); if (stream != NULL && fclose(stream) == EOF) { out->error = true; out->s = NULL; } return is_port_pair_found; } typedef struct { const EVP_CIPHER *cipher; BIO *writer; BUF_MEM *buffer; unsigned char *key, *iv, salt[PKCS5_SALT_LEN]; } crypto_t; static bool initialize_crypto(crypto_t *x, const char *filename) { struct stat file; bool is_initialized = false; assert(filename != NULL && x != NULL); if (stat(filename, &file) == 0) { FILE *stream; ssize_t len; const EVP_MD *const HASH = EVP_md5(); // openssl compat: enc -pass const size_t KEY_SIZE = EVP_CIPHER_key_length(x->cipher); const size_t IV_SIZE = EVP_CIPHER_iv_length(x->cipher); unsigned char *pass = NULL; size_t z = 0; if (!S_ISREG(file.st_mode)) return false; // no dirs, devs, etc. if (file.st_mode & (S_IROTH | S_IWOTH)) return false; // no ------rw- if ((x->key = malloc(KEY_SIZE)) == NULL) return false; if ((x->iv = malloc(IV_SIZE)) == NULL) return false; if ((stream = fopen(filename, TEXT_READ_MODE)) == NULL) return false; if ((len = getline(&pass, &z, stream)) == (ssize_t) -1) return false; if (fclose(stream) == EOF) return false; if (len > 0 && pass[(size_t) (len - 1)] == '\n') pass[(size_t) --len] = '\0'; if (RAND_pseudo_bytes(x->salt, sizeof(x->salt)) <= 0) return false; EVP_BytesToKey(x->cipher, HASH, x->salt, pass, len, 1, x->key, x->iv); memset(pass, 0, len); // XXX: crypto erase free(pass); is_initialized = true; if (opt.debug) { char line[80] = { '\0' }, *p = line; int n; size_t size = sizeof(line); n = snprintf(p, size, "salt[%2zu bytes]=", sizeof(x->salt)); assert(n >= 0); for (z = 0; z < sizeof(x->salt) && n < (int) size; z++) { p += n; size -= n; n = snprintf(p, size, "%.*hhX", CHAR_BIT / 4, x->salt[z]); assert(n >= 0); } debug("%s\n", line); p = line; size = sizeof(line); n = snprintf(p, size, "key[%4d bits]=", (int) KEY_SIZE * CHAR_BIT); assert(n >= 0); for (z = 0; z < KEY_SIZE && n < (int) size; z++) { p += n; size -= n; n = snprintf(p, size, "%.*hhX", CHAR_BIT / 4, x->key[z]); assert(n >= 0); } debug("%s\n", line); p = line; size = sizeof(line); n = snprintf(p, size, "iv[%5d bits]=", (int) IV_SIZE * CHAR_BIT); assert(n >= 0); for (z = 0; z < IV_SIZE && n < (int) size; z++) { p += n; size -= n; n = snprintf(p, size, "%.*hhX", CHAR_BIT / 4, x->iv[z]); assert(n >= 0); } debug("%s\n", line); memset(line, 0, sizeof(line)); // XXX: crypto erase } } return is_initialized; } static char *created_ciphertext_b64(const char *s) { BIO *encoder; char *b64; const char *const MAGIC = "Salted__"; // openssl compat: enc -salt const char *const NL = "\n"; // is strcat()ed to plaintext crypto_t x = { cipher: NULL }; assert(s != NULL); OpenSSL_add_all_ciphers(); if ((x.cipher = EVP_get_cipherbyname(opt.Encrypt)) == NULL) return NULL; debug("cipher name =%s\n", EVP_CIPHER_name(x.cipher)); debug("cipher block =%d bytes\n", EVP_CIPHER_block_size(x.cipher)); switch (EVP_CIPHER_mode(x.cipher)) { case EVP_CIPH_ECB_MODE: debug("cipher mode =%s\n", "ECB"); break; case EVP_CIPH_CBC_MODE: debug("cipher mode =%s\n", "CBC"); break; case EVP_CIPH_CFB_MODE: debug("cipher mode =%s\n", "CFB"); break; case EVP_CIPH_OFB_MODE: debug("cipher mode =%s\n", "OFB"); break; case EVP_CIPH_STREAM_CIPHER: debug("cipher mode =%s\n", "stream"); } if (!initialize_crypto(&x, opt.passwd)) return NULL; if ((encoder = BIO_new(BIO_s_mem())) == NULL) return NULL; else x.writer = encoder; if ((encoder = BIO_new(BIO_f_base64())) == NULL) return NULL; else { BIO_set_flags(encoder, BIO_FLAGS_BASE64_NO_NL); x.writer = BIO_push(encoder, x.writer); if (BIO_write(x.writer, MAGIC, strlen(MAGIC)) <= 0) return NULL; if (BIO_write(x.writer, x.salt, sizeof(x.salt)) <= 0) return NULL; } if ((encoder = BIO_new(BIO_f_cipher())) == NULL) return NULL; else { BIO_set_cipher(encoder, x.cipher, x.key, x.iv, true); x.writer = BIO_push(encoder, x.writer); } if (BIO_write(x.writer, s, strlen(s)) <= 0) return NULL; if (BIO_write(x.writer, NL, strlen(NL)) <= 0) return NULL; if (BIO_flush(x.writer) <= 0) return NULL; BIO_get_mem_ptr(x.writer, &x.buffer); if ((b64 = calloc(x.buffer->length + 3, sizeof(char))) != NULL) { b64[0] = '['; // pidentd compat: base64 in brackets memcpy(b64 + 1, x.buffer->data, x.buffer->length); b64[x.buffer->length + 1] = ']'; } BIO_free_all(x.writer); EVP_cleanup(); free(x.key); free(x.iv); memset(&x, 0, sizeof(x)); // XXX: crypto erase return b64; } static reply_t created_reply(request_t in) { reply_t out = { lport: 0, rport: 0 }; bool is_invalid_port = false; unsigned attempts = 0; out.os = vstrdup(opt.other ? "OTHER" : opt.os, DFL_OS); out.enc = vstrdup(opt.codeset, nl_langinfo(CODESET), "X-UNKNOWN"); if (!is_in_range(PORT_MIN, in.lport, PORT_MAX)) { in.lport = 0; is_invalid_port = true; errno = 0; } if (!is_in_range(PORT_MIN, in.rport, PORT_MAX)) { in.rport = 0; is_invalid_port = true; errno = 0; } out.lport = (unsigned) in.lport; out.rport = (unsigned) in.rport; if (is_invalid_port) { out.s = vstrdup("INVALID-PORT"); out.error = true; } // FIXME: because the /proc/net/tcp*'s updated asyncronously, it's possible // to miss reading a pair when reading syncronously (even with a locked // read & a large enough buffer). To lessen the chance of a false negative, // inefficiently & unreliably re-read proc more than once if not found. else for (;;) { // TODO: port pair more likely to be IPv4 than IPv6, // so scan IPv4 first for better performance; should be configurable if (get_info(&out, in, PROC_V4) || get_info(&out, in, PROC_V6)) break; else if (attempts++ < PROC_RETRY) { if (usleep(PROC_SLEEP_US) == -1) handle_error(NULL); continue; } out.error = true; errno = 0; out.s = vstrdup("NO-USER"); break; } if (!out.error && opt.Encrypt != NULL) { char *encrypted = created_ciphertext_b64(out.s); memset(out.s, 0, strlen(out.s)); // XXX: crypto erase free(out.s); if (encrypted == NULL) { int n; char msg[USERID_MAX_LEN], *p = msg; unsigned long code; size_t size = USERID_MAX_LEN; out.error = true; if (opt.xerror) { n = snprintf(p, size, "%s", "X-CRYPTO"); assert(n >= 0); do if ((code = ERR_get_error()) != 0) { p += n; size = (size_t) n > size ? 0 : size - n; n = snprintf(p, size, "-%08lX", code); assert(n >= 0); } while (code != 0 && (size_t) n < size); out.s = vstrdup(msg); } else out.s = NULL; } else out.s = encrypted; } // RFC1413: when the info field isn't usable as-is, set OS to OTHER if (opt.fn != 0 || opt.verbose || opt.number || opt.Encrypt != NULL) { free(out.os); out.os = vstrdup("OTHER"); } return out; } static void send_reply(reply_t out) { char *s; int n; const char *fmt; // RFC 1413 says servers shouldn't use unnecessary space, but mimic // existing servers' behavior so inflexible clients work const char *const GOOD_FMT = "%u , %u : USERID : %s%.0s :%s\r\n"; const char *const GOOD_NONASCII_FMT = "%u , %u : USERID : %s , %s :%s\r\n"; const char *const BAD_FMT = "%u , %u : ERROR :%.0s%.0s%s%.0d\r\n"; if (out.s == NULL || (opt.error && *out.s != 'X')) { out.s = "UNKNOWN-ERROR"; out.error = true; } if (strlen(out.s) > USERID_MAX_LEN || strpbrk(out.s, "\r\n") != NULL) { out.error = true; out.s = opt.xerror ? "X-RFC1413" : NULL; } if (out.error) { fmt = BAD_FMT; if (opt.log && errno != 0) log_warning(NULL); if (opt.abrupt) goto abrupt; if (opt.xerror && errno != 0 && *out.s != 'X') out.s = "X-ERRNO-"; } else fmt = is_print_ascii(out.s) ? GOOD_FMT : GOOD_NONASCII_FMT; n = asprintf(&s, fmt, out.lport, out.rport, out.os, out.enc, out.s, errno); if (n < 0) handle_error(NULL); if (fputs_unlocked(s, stdout) == EOF) handle_error(NULL); else free(s); abrupt: if (out.error) exit(EXIT_FAILURE); // must exit to clean alloc res } static void destroy_reply(reply_t *out) { assert(out != NULL && !out->error); free(out->os); free(out->enc); free(out->s); memset(out, 0, sizeof(*out)); } static void destroy_request(request_t *in) { free(in->laddr); free(in->raddr); memset(in, 0, sizeof(*in)); } static void catch_signal(int which) { _Exit(which == SIGALRM ? EXIT_SUCCESS : EXIT_FAILURE); } int main(int argc, char *argv[]) { # if !defined NDEBUG mtrace(); # endif if (setlocale(LC_ALL, "") == NULL) abort(); errno = 0; /* not an error for some locale files to be missing */ if (signal(SIGALRM, catch_signal) == SIG_ERR) abort(); if (textdomain(PACKAGE) == NULL) handle_error(NULL); if (bindtextdomain(PACKAGE, NULL) == NULL) handle_error(NULL); openlog(PACKAGE, LOG_PID, LOG_DAEMON); create_opt(argc, argv); atexit(destroy_opt); if (opt.help) show_help(); else if (opt.Version) show_version(); else while (opt.multiquery-- != 0 && (opt.timeout != 0 || optind < argc)) { request_t in = created_request(argc, argv); reply_t out = created_reply(in); destroy_request(&in); send_reply(out); destroy_reply(&out); } return EXIT_SUCCESS; }