Module Name: src Committed By: christos Date: Tue Oct 12 19:08:04 UTC 2021
Modified Files: src/usr.sbin/inetd: Makefile inetd.8 inetd.c inetd.h parse_v2.c Added Files: src/usr.sbin/inetd: parse.c ratelimit.c Log Message: PR/56448: Solomon Ritzow: Various improvements. Rate limiting code has been moved to ratelimit.c. I renamed clear_ip_list to rl_clear_ip_list and broke the code up into more functions. I have also made the per-IP rate limiting allocation more efficient. IP addresses are now stored in their network format instead of a string from getnameinfo (see inetd.h struct rl_ip_node). malloc calls use only the space needed by the structure by using offsetof on union members (I suppose this can be a bit dangerous if not done correctly...). Per-IP rate limiting still supports textual comparison using getnameinfo for address families other than AF_INET and AF_INET6, but I don't think there are any that are actually compatible or used by inetd (I haven't tested UNIX sockets with a remote bound to another file, but I did test using IPv6 with the textual format by commenting out the IPv6 specific code, and it works properly). Still potentially handy for the future. The IP node list (se_rl_ip_list) now uses the <sys/queue.h> SLIST macros instead of a custom list. I've broken rl_process up into helper functions for each type of rate limiting and created a separate function for address stringification, for use with printouts from the -d flag. I tried to reduce stack memory use by moving printing code involving string buffers into separate functions. I haven't tested rl_ipv6_eq on a 32-bit system. The code for the positional syntax has also been moved to parse.c. Function try_biltin has been added to remove parse.c:parse_server's dependency on the biltin structure definition. File inetd.h has been updated with the proper function prototypes, and the servtab structure has been update with the new IP node SLIST. I also moved things around a bit. The way we (a peer and myself) formatted inetd.h previously was somewhat confusing. Function and global variable prototypes are now organized by the source file they are defined in. I also added a -f flag that I saw in another problem report (https://gnats.netbsd.org/12823) that I thought could be useful. It runs inetd in the foreground but without debug printouts or SO_DEBUG. I'm not completely sure about the line "if (foreground) setsid()" that I changed from "if (debug) setsid()". To generate a diff of this commit: cvs rdiff -u -r1.29 -r1.30 src/usr.sbin/inetd/Makefile cvs rdiff -u -r1.64 -r1.65 src/usr.sbin/inetd/inetd.8 cvs rdiff -u -r1.136 -r1.137 src/usr.sbin/inetd/inetd.c cvs rdiff -u -r1.3 -r1.4 src/usr.sbin/inetd/inetd.h cvs rdiff -u -r0 -r1.1 src/usr.sbin/inetd/parse.c \ src/usr.sbin/inetd/ratelimit.c cvs rdiff -u -r1.5 -r1.6 src/usr.sbin/inetd/parse_v2.c Please note that diffs are not public domain; they are subject to the copyright notices on the relevant files.
Modified files: Index: src/usr.sbin/inetd/Makefile diff -u src/usr.sbin/inetd/Makefile:1.29 src/usr.sbin/inetd/Makefile:1.30 --- src/usr.sbin/inetd/Makefile:1.29 Fri Sep 3 16:24:28 2021 +++ src/usr.sbin/inetd/Makefile Tue Oct 12 15:08:04 2021 @@ -1,12 +1,12 @@ # from: @(#)Makefile 8.1 (Berkeley) 6/6/93 -# $NetBSD: Makefile,v 1.29 2021/09/03 20:24:28 rillig Exp $ +# $NetBSD: Makefile,v 1.30 2021/10/12 19:08:04 christos Exp $ .include <bsd.own.mk> USE_FORT?= yes # network server PROG= inetd -SRCS= inetd.c parse_v2.c +SRCS= inetd.c parse.c parse_v2.c ratelimit.c MAN= inetd.8 MLINKS= inetd.8 inetd.conf.5 WARNS= 6 Index: src/usr.sbin/inetd/inetd.8 diff -u src/usr.sbin/inetd/inetd.8:1.64 src/usr.sbin/inetd/inetd.8:1.65 --- src/usr.sbin/inetd/inetd.8:1.64 Tue Aug 31 07:16:00 2021 +++ src/usr.sbin/inetd/inetd.8 Tue Oct 12 15:08:04 2021 @@ -1,4 +1,4 @@ -.\" $NetBSD: inetd.8,v 1.64 2021/08/31 11:16:00 wiz Exp $ +.\" $NetBSD: inetd.8,v 1.65 2021/10/12 19:08:04 christos Exp $ .\" .\" Copyright (c) 1998 The NetBSD Foundation, Inc. .\" All rights reserved. @@ -57,7 +57,7 @@ .\" .\" from: @(#)inetd.8 8.4 (Berkeley) 6/1/94 .\" -.Dd August 29, 2021 +.Dd October 12, 2021 .Dt INETD 8 .Os .Sh NAME @@ -92,7 +92,9 @@ The options available for .Nm : .Bl -tag -width Ds .It Fl d -Turns on debugging. +Turns on debugging and runs inetd in the foreground. +.It Fl f +Runs inetd in the foreground. .It Fl l Turns on libwrap connection logging. .El @@ -519,7 +521,12 @@ Defaults to 40 if not specified. .It Sy ip_max Specifies the maximum number of server instances that may be spawned from .Nm -within an interval of 60 seconds for a given IP address. +within an interval of 60 seconds for a given IP address. Other address types +may also work if supported by +.Xr getnameinfo 3 +, test thoroughly using -d. For example, connections from unnamed Unix sockets +do not work, but connections from named Unix sockets may work. However, there is +no way to only accept named Unix sockets. .It Sy user The user to run the program as. Equivalent to @@ -860,9 +867,6 @@ key), addresses with an interface specif are not currently supported, as addresses of that form are not parsed by .Xr inet_pton 3 . .Pp -.Dq tcpmux -on IPv6 is not tested enough. -.Pp If a positional service definition has an invalid parameter and extends across multiple lines using tab characters, the subsequent lines after the error are treated as new service definitions. Index: src/usr.sbin/inetd/inetd.c diff -u src/usr.sbin/inetd/inetd.c:1.136 src/usr.sbin/inetd/inetd.c:1.137 --- src/usr.sbin/inetd/inetd.c:1.136 Fri Sep 3 17:02:04 2021 +++ src/usr.sbin/inetd/inetd.c Tue Oct 12 15:08:04 2021 @@ -1,4 +1,4 @@ -/* $NetBSD: inetd.c,v 1.136 2021/09/03 21:02:04 rillig Exp $ */ +/* $NetBSD: inetd.c,v 1.137 2021/10/12 19:08:04 christos Exp $ */ /*- * Copyright (c) 1998, 2003 The NetBSD Foundation, Inc. @@ -66,7 +66,7 @@ __COPYRIGHT("@(#) Copyright (c) 1983, 19 #if 0 static char sccsid[] = "@(#)inetd.c 8.4 (Berkeley) 4/13/94"; #else -__RCSID("$NetBSD: inetd.c,v 1.136 2021/09/03 21:02:04 rillig Exp $"); +__RCSID("$NetBSD: inetd.c,v 1.137 2021/10/12 19:08:04 christos Exp $"); #endif #endif /* not lint */ @@ -191,6 +191,7 @@ __RCSID("$NetBSD: inetd.c,v 1.136 2021/0 #include <sys/resource.h> #include <sys/event.h> #include <sys/socket.h> +#include <sys/queue.h> #ifndef NO_RPC @@ -242,9 +243,7 @@ int allow_severity = LIBWRAP_ALLOW_FACIL int deny_severity = LIBWRAP_DENY_FACILITY|LIBWRAP_DENY_SEVERITY; #endif -#define CNT_INTVL ((time_t)60) /* servers in CNT_INTVL sec. */ -#define RETRYTIME (60*10) /* retry after bind or server fail */ - +static bool foreground; int debug; #ifdef LIBWRAP int lflag; @@ -272,32 +271,20 @@ struct servtab *servtab; static void chargen_dg(int, struct servtab *); static void chargen_stream(int, struct servtab *); -static void close_sep(struct servtab *); -static void config(void); static void daytime_dg(int, struct servtab *); static void daytime_stream(int, struct servtab *); static void discard_dg(int, struct servtab *); static void discard_stream(int, struct servtab *); static void echo_dg(int, struct servtab *); static void echo_stream(int, struct servtab *); -static void endconfig(void); -static struct servtab *enter(struct servtab *); -static struct servtab *getconfigent(char **); __dead static void goaway(void); static void machtime_dg(int, struct servtab *); static void machtime_stream(int, struct servtab *); -#ifdef DEBUG_ENABLE -static void print_service(const char *, struct servtab *); -#endif static void reapchild(void); static void retry(void); static void run_service(int, struct servtab *, int); -static void setup(struct servtab *); -static char *skip(char **); static void tcpmux(int, struct servtab *); __dead static void usage(void); -static void register_rpc(struct servtab *); -static void unregister_rpc(struct servtab *); static void bump_nofile(void); static void inetd_setproctitle(char *, int); static void initring(void); @@ -308,25 +295,6 @@ static int my_kevent(const struct kevent static struct kevent *allocchange(void); static int get_line(int, char *, int); static void spawn(struct servtab *, int); -static struct servtab init_servtab(void); -static int rl_process(struct servtab *, int); -static struct se_ip_list_node *rl_add(struct servtab *, char *); -static void rl_reset(struct servtab *, time_t); -static struct se_ip_list_node *rl_try_get_ip(struct servtab *, char *); -static void include_configs(char *); -static int glob_error(const char *, int); -static void read_glob_configs(char *); -static void prepare_next_config(const char*); -static bool is_same_service(const struct servtab *, const struct servtab *); -static char *gen_file_pattern(const char *, const char *); -static bool check_no_reinclude(const char *); -static void include_matched_path(char *); -static void purge_unchecked(void); -static void config_root(void); -static void clear_ip_list(struct servtab *); -static time_t rl_time(void); -static void rl_get_name(struct servtab *, int, char *); -static void rl_drop_connection(struct servtab *, int); struct biltin { const char *bi_service; /* internally provided service name */ @@ -356,9 +324,7 @@ struct biltin { { "chargen", SOCK_STREAM, true, false, chargen_stream }, { "chargen", SOCK_DGRAM, false, false, chargen_dg }, - { "tcpmux", SOCK_STREAM, true, false, tcpmux }, - - { NULL, 0, false, false, NULL } + { "tcpmux", SOCK_STREAM, true, false, tcpmux } }; /* list of "bad" ports. I.e. ports that are most obviously used for @@ -382,16 +348,20 @@ main(int argc, char *argv[]) while ((ch = getopt(argc, argv, #ifdef LIBWRAP - "dl" + "dfl" #else - "d" + "df" #endif )) != -1) switch(ch) { case 'd': + foreground = true; debug = true; options |= SO_DEBUG; break; + case 'f': + foreground = true; + break; #ifdef LIBWRAP case 'l': lflag = true; @@ -407,7 +377,7 @@ main(int argc, char *argv[]) if (argc > 0) CONFIG = argv[0]; - if (!debug) + if (!foreground) daemon(0, 0); openlog("inetd", LOG_PID | LOG_NOWAIT, LOG_DAEMON); pidfile(NULL); @@ -537,14 +507,15 @@ spawn(struct servtab *sep, int ctrl) for (n = 0; n < __arraycount(my_signals); n++) (void) signal(my_signals[n], SIG_DFL); - if (debug) + /* Don't put services in terminal session */ + if (foreground) setsid(); } } if (pid == 0) { run_service(ctrl, sep, dofork); if (dofork) - exit(0); + exit(EXIT_SUCCESS); } if (sep->se_wait == 0 && sep->se_socktype == SOCK_STREAM) close(ctrl); @@ -703,215 +674,6 @@ reapchild(void) } } -size_t line_number; - -/* - * Recursively merge loaded service definitions with any defined - * in the current or included config files. - */ -static void -config(void) -{ - struct servtab *sep, *cp; - /* - * Current position in line, used with key-values notation, - * saves cp across getconfigent calls. - */ - char *current_pos; - size_t n; - - /* open config file from beginning */ - fconfig = fopen(CONFIG, "r"); - if(fconfig == NULL) { - syslog(LOG_ERR, "%s: %m", CONFIG); - return; - } - - /* First call to nextline will advance line_number to 1 */ - line_number = 0; - - /* Start parsing at the beginning of the first line */ - current_pos = nextline(fconfig); - - while ((cp = getconfigent(¤t_pos)) != NULL) { - /* Find an already existing service definition */ - for (sep = servtab; sep != NULL; sep = sep->se_next) - if (is_same_service(sep, cp)) - break; - if (sep != NULL) { - int i; - -#define SWAP(type, a, b) {type c = a; a = b; b = c;} - - /* - * sep->se_wait may be holding the pid of a daemon - * that we're waiting for. If so, don't overwrite - * it unless the config file explicitly says don't - * wait. - */ - if (cp->se_bi == 0 && - (sep->se_wait == 1 || cp->se_wait == 0)) - sep->se_wait = cp->se_wait; - SWAP(char *, sep->se_user, cp->se_user); - SWAP(char *, sep->se_group, cp->se_group); - SWAP(char *, sep->se_server, cp->se_server); - for (i = 0; i < MAXARGV; i++) - SWAP(char *, sep->se_argv[i], cp->se_argv[i]); -#ifdef IPSEC - SWAP(char *, sep->se_policy, cp->se_policy); -#endif - SWAP(service_type, cp->se_type, sep->se_type); - SWAP(size_t, cp->se_service_max, sep->se_service_max); - SWAP(size_t, cp->se_ip_max, sep->se_ip_max); -#undef SWAP - if (isrpcservice(sep)) - unregister_rpc(sep); - sep->se_rpcversl = cp->se_rpcversl; - sep->se_rpcversh = cp->se_rpcversh; - freeconfig(cp); -#ifdef DEBUG_ENABLE - if (debug) - print_service("REDO", sep); -#endif - } else { - sep = enter(cp); -#ifdef DEBUG_ENABLE - if (debug) - print_service("ADD ", sep); -#endif - } - sep->se_checked = 1; - - /* - * Remainder of config(void) checks validity of servtab options - * and sets up the service by setting up sockets (in setup(servtab)). - */ - switch (sep->se_family) { - case AF_LOCAL: - if (sep->se_fd != -1) - break; - n = strlen(sep->se_service); - if (n >= sizeof(sep->se_ctrladdr_un.sun_path)) { - syslog(LOG_ERR, "%s/%s: address too long", - sep->se_service, sep->se_proto); - sep->se_checked = 0; - continue; - } - (void)unlink(sep->se_service); - strlcpy(sep->se_ctrladdr_un.sun_path, - sep->se_service, n + 1); - sep->se_ctrladdr_un.sun_family = AF_LOCAL; - sep->se_ctrladdr_size = (socklen_t)(n + - sizeof(sep->se_ctrladdr_un) - - sizeof(sep->se_ctrladdr_un.sun_path)); - if (!ISMUX(sep)) - setup(sep); - break; - case AF_INET: -#ifdef INET6 - case AF_INET6: -#endif - { - struct addrinfo hints, *res; - char *host; - const char *port; - int error; - int s; - - /* check if the family is supported */ - s = socket(sep->se_family, SOCK_DGRAM, 0); - if (s < 0) { - syslog(LOG_WARNING, - "%s/%s: %s: the address family is not " - "supported by the kernel", - sep->se_service, sep->se_proto, - sep->se_hostaddr); - sep->se_checked = false; - continue; - } - close(s); - - memset(&hints, 0, sizeof(hints)); - hints.ai_family = sep->se_family; - hints.ai_socktype = sep->se_socktype; - hints.ai_flags = AI_PASSIVE; - if (strcmp(sep->se_hostaddr, "*") == 0) - host = NULL; - else - host = sep->se_hostaddr; - if (isrpcservice(sep) || ISMUX(sep)) - port = "0"; - else - port = sep->se_service; - error = getaddrinfo(host, port, &hints, &res); - if (error != 0) { - if (error == EAI_SERVICE) { - /* gai_strerror not friendly enough */ - syslog(LOG_WARNING, SERV_FMT ": " - "unknown service", - SERV_PARAMS(sep)); - } else { - syslog(LOG_ERR, SERV_FMT ": %s: %s", - SERV_PARAMS(sep), - sep->se_hostaddr, - gai_strerror(error)); - } - sep->se_checked = false; - continue; - } - if (res->ai_next != NULL) { - syslog(LOG_ERR, - SERV_FMT ": %s: resolved to multiple addr", - SERV_PARAMS(sep), - sep->se_hostaddr); - sep->se_checked = false; - freeaddrinfo(res); - continue; - } - memcpy(&sep->se_ctrladdr, res->ai_addr, - res->ai_addrlen); - if (ISMUX(sep)) { - sep->se_fd = -1; - freeaddrinfo(res); - continue; - } - sep->se_ctrladdr_size = res->ai_addrlen; - freeaddrinfo(res); -#ifdef RPC - if (isrpcservice(sep)) { - struct rpcent *rp; - - sep->se_rpcprog = atoi(sep->se_service); - if (sep->se_rpcprog == 0) { - rp = getrpcbyname(sep->se_service); - if (rp == 0) { - syslog(LOG_ERR, - SERV_FMT - ": unknown service", - SERV_PARAMS(sep)); - sep->se_checked = false; - continue; - } - sep->se_rpcprog = rp->r_number; - } - if (sep->se_fd == -1 && !ISMUX(sep)) - setup(sep); - if (sep->se_fd != -1) - register_rpc(sep); - } else -#endif - { - if (sep->se_fd >= 0) - close_sep(sep); - if (sep->se_fd == -1 && !ISMUX(sep)) - setup(sep); - } - } - } - } - endconfig(); -} - static void retry(void) { @@ -959,10 +721,13 @@ goaway(void) (void)close(sep->se_fd); sep->se_fd = -1; } - exit(0); + + DPRINTF("Going away."); + + exit(EXIT_SUCCESS); } -static void +void setup(struct servtab *sep) { int on = 1; @@ -1065,7 +830,7 @@ setsockopt(fd, SOL_SOCKET, opt, &on, (so /* * Finish with a service and its socket. */ -static void +void close_sep(struct servtab *sep) { @@ -1075,11 +840,11 @@ close_sep(struct servtab *sep) } sep->se_count = 0; if (sep->se_ip_max != SERVTAB_UNSPEC_SIZE_T) { - clear_ip_list(sep); + rl_clear_ip_list(sep); } } -static void +void register_rpc(struct servtab *sep) { #ifdef RPC @@ -1117,7 +882,7 @@ register_rpc(struct servtab *sep) #endif /* RPC */ } -static void +void unregister_rpc(struct servtab *sep) { #ifdef RPC @@ -1140,561 +905,6 @@ unregister_rpc(struct servtab *sep) #endif /* RPC */ } - -static struct servtab * -enter(struct servtab *cp) -{ - struct servtab *sep; - - sep = malloc(sizeof (*sep)); - if (sep == NULL) { - syslog(LOG_ERR, "Out of memory."); - exit(EXIT_FAILURE); - } - *sep = *cp; - sep->se_fd = -1; - sep->se_rpcprog = -1; - sep->se_next = servtab; - servtab = sep; - return (sep); -} - -FILE *fconfig; -/* Temporary storage for new servtab */ -static struct servtab serv; -/* Current line from current config file */ -static char line[LINE_MAX]; -char *defhost; -#ifdef IPSEC -char *policy; -#endif - -static void -endconfig(void) -{ - if (fconfig != NULL) { - (void) fclose(fconfig); - fconfig = NULL; - } - if (defhost != NULL) { - free(defhost); - defhost = NULL; - } - -#ifdef IPSEC - if (policy != NULL) { - free(policy); - policy = NULL; - } -#endif - -} - -#define LOG_EARLY_ENDCONF() \ - ERR("Exiting %s early. Some services will be unavailable", CONFIG) - -#define LOG_TOO_FEW_ARGS() \ - ERR("Expected more arguments") - -/* Parse the next service and apply any directives, and returns it as servtab */ -static struct servtab * -getconfigent(char **current_pos) -{ - struct servtab *sep = &serv; - int argc, val; - char *cp, *cp0, *arg, *buf0, *buf1, *sz0, *sz1; - static char TCPMUX_TOKEN[] = "tcpmux/"; -#define MUX_LEN (sizeof(TCPMUX_TOKEN)-1) - char *hostdelim; - - /* - * Pre-condition: current_pos points into line, - * line contains config line. Continue where the last getconfigent left off. - * Allows for multiple service definitions per line. - */ - cp = *current_pos; - - if (false) { - /* - * Go to the next line, but only after attemting to read the current - * one! Keep reading until we find a valid definition or EOF. - */ -more: - cp = nextline(fconfig); - } - - if (cp == NULL) { - /* EOF or I/O error, let config() know to exit the file */ - return NULL; - } - - /* Comments and IPsec policies */ - if (cp[0] == '#') { -#ifdef IPSEC - /* lines starting with #@ is not a comment, but the policy */ - if (cp[1] == '@') { - char *p; - for (p = cp + 2; isspace((unsigned char)*p); p++) - ; - if (*p == '\0') { - if (policy) - free(policy); - policy = NULL; - } else { - if (ipsecsetup_test(p) < 0) { - ERR("Invalid IPsec policy \"%s\"", p); - LOG_EARLY_ENDCONF(); - /* - * Stop reading the current config to prevent services - * from being run without IPsec. - */ - return NULL; - } else { - if (policy) - free(policy); - policy = newstr(p); - } - } - } -#endif - - goto more; - } - - /* Parse next token: listen-addr/hostname, service-spec, .include */ - arg = skip(&cp); - - if (cp == NULL) { - goto more; - } - - if(arg[0] == '.') { - if (strcmp(&arg[1], "include") == 0) { - /* include directive */ - arg = skip(&cp); - if(arg == NULL) { - LOG_TOO_FEW_ARGS(); - return NULL; - } - include_configs(arg); - goto more; - } else { - ERR("Unknown directive '%s'", &arg[1]); - goto more; - } - } - - /* After this point, we might need to store data in a servtab */ - *sep = init_servtab(); - - /* Check for a host name. */ - hostdelim = strrchr(arg, ':'); - if (hostdelim != NULL) { - *hostdelim = '\0'; - if (arg[0] == '[' && hostdelim > arg && hostdelim[-1] == ']') { - hostdelim[-1] = '\0'; - sep->se_hostaddr = newstr(arg + 1); - } else - sep->se_hostaddr = newstr(arg); - arg = hostdelim + 1; - /* - * If the line is of the form `host:', then just change the - * default host for the following lines. - */ - if (*arg == '\0') { - arg = skip(&cp); - if (cp == NULL) { - free(defhost); - defhost = sep->se_hostaddr; - goto more; - } - } - } else { - /* No host address found, set it to NULL to indicate absence */ - sep->se_hostaddr = NULL; - } - if (strncmp(arg, TCPMUX_TOKEN, MUX_LEN) == 0) { - char *c = arg + MUX_LEN; - if (*c == '+') { - sep->se_type = MUXPLUS_TYPE; - c++; - } else - sep->se_type = MUX_TYPE; - sep->se_service = newstr(c); - } else { - sep->se_service = newstr(arg); - sep->se_type = NORM_TYPE; - } - - DPRINTCONF("Found service definition '%s'", sep->se_service); - - /* on/off/socktype */ - arg = skip(&cp); - if (arg == NULL) { - LOG_TOO_FEW_ARGS(); - freeconfig(sep); - goto more; - } - - /* Check for new v2 syntax */ - if (strcmp(arg, "on") == 0 || strncmp(arg, "on#", 3) == 0) { - - if (arg[2] == '#') { - cp = nextline(fconfig); - } - - switch(parse_syntax_v2(sep, &cp)) { - case V2_SUCCESS: - *current_pos = cp; - return sep; - case V2_SKIP: - /* Skip invalid definitions, freeconfig is called in parse_v2.c */ - *current_pos = cp; - freeconfig(sep); - goto more; - case V2_ERROR: - /* - * Unrecoverable error, stop reading. freeconfig is called - * in parse_v2.c - */ - LOG_EARLY_ENDCONF(); - freeconfig(sep); - return NULL; - } - } else if (strcmp(arg, "off") == 0 || strncmp(arg, "off#", 4) == 0) { - - if (arg[3] == '#') { - cp = nextline(fconfig); - } - - /* Parse syntax the same as with 'on', but ignore the result */ - switch(parse_syntax_v2(sep, &cp)) { - case V2_SUCCESS: - case V2_SKIP: - *current_pos = cp; - freeconfig(sep); - goto more; - case V2_ERROR: - /* Unrecoverable error, stop reading */ - LOG_EARLY_ENDCONF(); - freeconfig(sep); - return NULL; - } - } else { - /* continue parsing v1 */ - parse_socktype(arg, sep); - if (sep->se_socktype == SOCK_STREAM) { - parse_accept_filter(arg, sep); - } - if (sep->se_hostaddr == NULL) { - /* Set host to current default */ - sep->se_hostaddr = newstr(defhost); - } - } - - /* protocol */ - arg = skip(&cp); - if (arg == NULL) { - LOG_TOO_FEW_ARGS(); - freeconfig(sep); - goto more; - } - if (sep->se_type == NORM_TYPE && - strncmp(arg, "faith/", strlen("faith/")) == 0) { - arg += strlen("faith/"); - sep->se_type = FAITH_TYPE; - } - sep->se_proto = newstr(arg); - -#define MALFORMED(arg) \ -do { \ - ERR("%s: malformed buffer size option `%s'", \ - sep->se_service, (arg)); \ - freeconfig(sep); \ - goto more; \ -} while (false) - -#define GETVAL(arg) \ -do { \ - if (!isdigit((unsigned char)*(arg))) \ - MALFORMED(arg); \ - val = (int)strtol((arg), &cp0, 10); \ - if (cp0 != NULL) { \ - if (cp0[1] != '\0') \ - MALFORMED((arg)); \ - if (cp0[0] == 'k') \ - val *= 1024; \ - if (cp0[0] == 'm') \ - val *= 1024 * 1024; \ - } \ - if (val < 1) { \ - ERR("%s: invalid buffer size `%s'", \ - sep->se_service, (arg)); \ - freeconfig(sep); \ - goto more; \ - } \ -} while (false) - -#define ASSIGN(arg) \ -do { \ - if (strcmp((arg), "sndbuf") == 0) \ - sep->se_sndbuf = val; \ - else if (strcmp((arg), "rcvbuf") == 0) \ - sep->se_rcvbuf = val; \ - else \ - MALFORMED((arg)); \ -} while (false) - - /* - * Extract the send and receive buffer sizes before parsing - * the protocol. - */ - sep->se_sndbuf = sep->se_rcvbuf = 0; - buf0 = buf1 = sz0 = sz1 = NULL; - if ((buf0 = strchr(sep->se_proto, ',')) != NULL) { - /* Not meaningful for Tcpmux services. */ - if (ISMUX(sep)) { - ERR("%s: can't specify buffer sizes for " - "tcpmux services", sep->se_service); - goto more; - } - - /* Skip the , */ - *buf0++ = '\0'; - - /* Check to see if another socket buffer size was specified. */ - if ((buf1 = strchr(buf0, ',')) != NULL) { - /* Skip the , */ - *buf1++ = '\0'; - - /* Make sure a 3rd one wasn't specified. */ - if (strchr(buf1, ',') != NULL) { - ERR("%s: too many buffer sizes", sep->se_service); - goto more; - } - - /* Locate the size. */ - if ((sz1 = strchr(buf1, '=')) == NULL) - MALFORMED(buf1); - - /* Skip the = */ - *sz1++ = '\0'; - } - - /* Locate the size. */ - if ((sz0 = strchr(buf0, '=')) == NULL) - MALFORMED(buf0); - - /* Skip the = */ - *sz0++ = '\0'; - - GETVAL(sz0); - ASSIGN(buf0); - - if (buf1 != NULL) { - GETVAL(sz1); - ASSIGN(buf1); - } - } - -#undef ASSIGN -#undef GETVAL -#undef MALFORMED - - if (parse_protocol(sep)) { - freeconfig(sep); - goto more; - } - - /* wait/nowait:max */ - arg = skip(&cp); - if (arg == NULL) { - LOG_TOO_FEW_ARGS(); - freeconfig(sep); - goto more; - } - - /* Rate limiting parsing */ { - char *cp1; - if ((cp1 = strchr(arg, ':')) == NULL) - cp1 = strchr(arg, '.'); - if (cp1 != NULL) { - int rstatus; - *cp1++ = '\0'; - sep->se_service_max = (size_t)strtou(cp1, NULL, 10, 0, - SERVTAB_COUNT_MAX, &rstatus); - - if (rstatus != 0) { - if (rstatus != ERANGE) { - /* For compatibility with atoi parsing */ - sep->se_service_max = 0; - } - - WRN("Improper \"max\" value '%s', " - "using '%zu' instead: %s", - cp1, - sep->se_service_max, - strerror(rstatus)); - } - - } else - sep->se_service_max = TOOMANY; - } - if (parse_wait(sep, strcmp(arg, "wait") == 0)) { - freeconfig(sep); - goto more; - } - - /* Parse user:group token */ - arg = skip(&cp); - if(arg == NULL) { - LOG_TOO_FEW_ARGS(); - freeconfig(sep); - goto more; - } - char* separator = strchr(arg, ':'); - if (separator == NULL) { - /* Backwards compatibility, allow dot instead of colon */ - separator = strchr(arg, '.'); - } - - if (separator == NULL) { - /* Only user was specified */ - sep->se_group = NULL; - } else { - *separator = '\0'; - sep->se_group = newstr(separator + 1); - } - - sep->se_user = newstr(arg); - - /* Parser server-program (path to binary or "internal") */ - arg = skip(&cp); - if (arg == NULL) { - LOG_TOO_FEW_ARGS(); - freeconfig(sep); - goto more; - } - if (parse_server(sep, arg)) { - freeconfig(sep); - goto more; - } - - argc = 0; - for (arg = skip(&cp); cp != NULL; arg = skip(&cp)) { - if (argc < MAXARGV) - sep->se_argv[argc++] = newstr(arg); - } - while (argc <= MAXARGV) - sep->se_argv[argc++] = NULL; -#ifdef IPSEC - sep->se_policy = policy != NULL ? newstr(policy) : NULL; -#endif - /* getconfigent read a positional service def, move to next line */ - *current_pos = nextline(fconfig); - return (sep); -} - -void -freeconfig(struct servtab *cp) -{ - int i; - - free(cp->se_hostaddr); - free(cp->se_service); - free(cp->se_proto); - free(cp->se_user); - free(cp->se_group); - free(cp->se_server); - for (i = 0; i < MAXARGV; i++) - free(cp->se_argv[i]); -#ifdef IPSEC - free(cp->se_policy); -#endif -} - -/* - * Get next token *in the current service definition* from config file. - * Allows multi-line parse if single space or single tab-indented. - * Things in quotes are considered single token. - * Advances cp to next token. - */ -static char * -skip(char **cpp) -{ - char *cp = *cpp; - char *start; - char quote; - - if (*cpp == NULL) - return (NULL); - -again: - while (*cp == ' ' || *cp == '\t') - cp++; - if (*cp == '\0') { - int c; - - c = getc(fconfig); - (void) ungetc(c, fconfig); - if (c == ' ' || c == '\t') - if ((cp = nextline(fconfig)) != NULL) - goto again; - *cpp = NULL; - return (NULL); - } - start = cp; - /* Parse shell-style quotes */ - quote = '\0'; - while (*cp != '\0' && (quote != '\0' || (*cp != ' ' && *cp != '\t'))) { - if (*cp == '\'' || *cp == '"') { - if (quote != '\0' && *cp != quote) - cp++; - else { - if (quote != '\0') - quote = '\0'; - else - quote = *cp; - memmove(cp, cp+1, strlen(cp)); - } - } else - cp++; - } - if (*cp != '\0') - *cp++ = '\0'; - *cpp = cp; - return (start); -} - -char * -nextline(FILE *fd) -{ - char *cp; - - if (fgets(line, (int)sizeof(line), fd) == NULL) { - if (ferror(fd) != 0) { - ERR("Error when reading next line: %s", strerror(errno)); - } - return NULL; - } - cp = strchr(line, '\n'); - if (cp != NULL) - *cp = '\0'; - line_number++; - return line; -} - -char * -newstr(const char *cp) -{ - char *dp; - if ((dp = strdup((cp != NULL) ? cp : "")) != NULL) - return (dp); - syslog(LOG_ERR, "strdup: %m"); - exit(EXIT_FAILURE); - /*NOTREACHED*/ -} - static void inetd_setproctitle(char *a, int s) { @@ -1759,7 +969,7 @@ echo_stream(int s, struct servtab *sep) inetd_setproctitle(sep->se_service, s); while ((i = read(s, buffer, sizeof(buffer))) > 0 && write(s, buffer, (size_t)i) > 0) - ; + continue; } /* ARGSUSED */ @@ -1975,49 +1185,6 @@ daytime_dg(int s, struct servtab *sep) (void) sendto(s, buffer, (size_t)len, 0, sa, size); } -#ifdef DEBUG_ENABLE -/* - * print_service: - * Dump relevant information to stderr - */ -static void -print_service(const char *action, struct servtab *sep) -{ - - if (isrpcservice(sep)) - fprintf(stderr, - "%s: %s rpcprog=%d, rpcvers = %d/%d, proto=%s, wait.max=%d.%zu, user:group=%s:%s builtin=%lx server=%s" -#ifdef IPSEC - " policy=\"%s\"" -#endif - "\n", - action, sep->se_service, - sep->se_rpcprog, sep->se_rpcversh, sep->se_rpcversl, sep->se_proto, - sep->se_wait, sep->se_service_max, sep->se_user, sep->se_group, - (long)sep->se_bi, sep->se_server -#ifdef IPSEC - , (sep->se_policy != NULL ? sep->se_policy : "") -#endif - ); - else - fprintf(stderr, - "%s: %s:%s proto=%s%s, wait.max=%d.%zu, user:group=%s:%s builtin=%lx server=%s" -#ifdef IPSEC - " policy=%s" -#endif - "\n", - action, sep->se_hostaddr, sep->se_service, - sep->se_type == FAITH_TYPE ? "faith/" : "", - sep->se_proto, - sep->se_wait, sep->se_service_max, sep->se_user, sep->se_group, - (long)sep->se_bi, sep->se_server -#ifdef IPSEC - , (sep->se_policy != NULL ? sep->se_policy : "") -#endif - ); -} -#endif - static void usage(void) { @@ -2229,747 +1396,16 @@ allocchange(void) return (&changebuf[changes++]); } -static void -config_root(void) -{ - struct servtab *sep; - /* Uncheck services */ - for (sep = servtab; sep != NULL; sep = sep->se_next) { - sep->se_checked = false; - } - defhost = newstr("*"); -#ifdef IPSEC - policy = NULL; -#endif - fconfig = NULL; - config(); - purge_unchecked(); -} - -static void -purge_unchecked(void) -{ - struct servtab *sep, **sepp = &servtab; - int servtab_count = 0; - while ((sep = *sepp) != NULL) { - if (sep->se_checked) { - sepp = &sep->se_next; - servtab_count++; - continue; - } - *sepp = sep->se_next; - if (sep->se_fd >= 0) - close_sep(sep); - if (isrpcservice(sep)) - unregister_rpc(sep); - if (sep->se_family == AF_LOCAL) - (void)unlink(sep->se_service); -#ifdef DEBUG_ENABLE - if (debug) - print_service("FREE", sep); -#endif - freeconfig(sep); - free(sep); - } - DPRINTF("%d service(s) loaded.", servtab_count); -} - -static bool -is_same_service(const struct servtab *sep, const struct servtab *cp) -{ - return - strcmp(sep->se_service, cp->se_service) == 0 && - strcmp(sep->se_hostaddr, cp->se_hostaddr) == 0 && - strcmp(sep->se_proto, cp->se_proto) == 0 && - sep->se_family == cp->se_family && - ISMUX(sep) == ISMUX(cp); -} - -int -parse_protocol(struct servtab *sep) -{ - int val; - - if (strcmp(sep->se_proto, "unix") == 0) { - sep->se_family = AF_LOCAL; - } else { - val = (int)strlen(sep->se_proto); - if (val == 0) { - ERR("%s: invalid protocol specified", - sep->se_service); - return -1; - } - val = sep->se_proto[val - 1]; - switch (val) { - case '4': /*tcp4 or udp4*/ - sep->se_family = AF_INET; - break; -#ifdef INET6 - case '6': /*tcp6 or udp6*/ - sep->se_family = AF_INET6; - break; -#endif - default: - /* Use 'default' IP version which is IPv4, may eventually be - * changed to AF_INET6 */ - sep->se_family = AF_INET; - break; - } - if (strncmp(sep->se_proto, "rpc/", 4) == 0) { -#ifdef RPC - char *cp1, *ccp; - cp1 = strchr(sep->se_service, '/'); - if (cp1 == 0) { - ERR("%s: no rpc version", - sep->se_service); - return -1; - } - *cp1++ = '\0'; - sep->se_rpcversl = sep->se_rpcversh = - (int)strtol(cp1, &ccp, 0); - if (ccp == cp1) { - badafterall: - ERR("%s/%s: bad rpc version", - sep->se_service, cp1); - return -1; - } - if (*ccp == '-') { - cp1 = ccp + 1; - sep->se_rpcversh = (int)strtol(cp1, &ccp, 0); - if (ccp == cp1) - goto badafterall; - } -#else - ERR("%s: rpc services not supported", - sep->se_service); - return -1; -#endif /* RPC */ - } - } - return 0; -} - -int -parse_wait(struct servtab *sep, int wait) -{ - if (!ISMUX(sep)) { - sep->se_wait = wait; - return 0; - } - /* - * Silently enforce "nowait" for TCPMUX services since - * they don't have an assigned port to listen on. - */ - sep->se_wait = 0; - - if (strncmp(sep->se_proto, "tcp", 3)) { - ERR("bad protocol for tcpmux service %s", - sep->se_service); - return -1; - } - if (sep->se_socktype != SOCK_STREAM) { - ERR("bad socket type for tcpmux service %s", - sep->se_service); - return -1; - } - return 0; -} - -int -parse_server(struct servtab *sep, const char *arg){ - sep->se_server = newstr(arg); - if (strcmp(sep->se_server, "internal") != 0) { - sep->se_bi = NULL; - return 0; - } - struct biltin *bi; - - for (bi = biltins; bi->bi_service; bi++) - if (bi->bi_socktype == sep->se_socktype && - strcmp(bi->bi_service, sep->se_service) == 0) - break; - if (bi->bi_service == NULL) { - ERR("Internal service %s unknown", - sep->se_service); - return -1; - } - sep->se_bi = bi; - sep->se_wait = bi->bi_wait; - return 0; -} - -/* TODO test to make sure accept filter still works */ -void -parse_accept_filter(char *arg, struct servtab *sep) { - char *accf, *accf_arg; - /* one and only one accept filter */ - accf = strchr(arg, ':'); - if (accf == NULL) - return; - if (accf != strrchr(arg, ':') || *(accf + 1) == '\0') { - /* more than one || nothing beyond */ - sep->se_socktype = -1; - return; - } - - accf++; /* skip delimiter */ - strlcpy(sep->se_accf.af_name, accf, sizeof(sep->se_accf.af_name)); - accf_arg = strchr(accf, ','); - if (accf_arg == NULL) /* zero or one arg, no more */ - return; - - if (strrchr(accf, ',') != accf_arg) { - sep->se_socktype = -1; - } else { - accf_arg++; - strlcpy(sep->se_accf.af_arg, accf_arg, - sizeof(sep->se_accf.af_arg)); - } -} - -void -parse_socktype(char* arg, struct servtab* sep) -{ - /* stream socket may have an accept filter, only check first chars */ - if (strncmp(arg, "stream", sizeof("stream") - 1) == 0) - sep->se_socktype = SOCK_STREAM; - else if (strcmp(arg, "dgram") == 0) - sep->se_socktype = SOCK_DGRAM; - else if (strcmp(arg, "rdm") == 0) - sep->se_socktype = SOCK_RDM; - else if (strcmp(arg, "seqpacket") == 0) - sep->se_socktype = SOCK_SEQPACKET; - else if (strcmp(arg, "raw") == 0) - sep->se_socktype = SOCK_RAW; - else - sep->se_socktype = -1; -} - -static struct servtab -init_servtab(void) -{ - /* This does not set every field to default. See enter() as well */ - return (struct servtab) { - /* - * Set se_max to non-zero so uninitialized value is not - * a valid value. Useful in v2 syntax parsing. - */ - .se_service_max = SERVTAB_UNSPEC_SIZE_T, - .se_ip_max = SERVTAB_UNSPEC_SIZE_T, - .se_wait = SERVTAB_UNSPEC_VAL, - .se_socktype = SERVTAB_UNSPEC_VAL - /* All other fields initialized to 0 or null */ - }; -} - -/* Include directives bookkeeping structure */ -struct file_list { - /* Absolute path used for checking for circular references */ - char *abs; - /* Pointer to the absolute path of the parent config file, - * on the stack */ - struct file_list *next; -} *file_list_head; - -static void -include_configs(char *pattern) -{ - /* Allocate global per-config state on the thread stack */ - const char* save_CONFIG; - FILE *save_fconfig; - size_t save_line_number; - char *save_defhost; - struct file_list new_file; -#ifdef IPSEC - char *save_policy; -#endif - - /* Store current globals on the stack */ - save_CONFIG = CONFIG; - save_fconfig = fconfig; - save_line_number = line_number; - save_defhost = defhost; - new_file.abs = realpath(CONFIG, NULL); - new_file.next = file_list_head; -#ifdef IPSEC - save_policy = policy; -#endif - /* Put new_file at the top of the config stack */ - file_list_head = &new_file; - read_glob_configs(pattern); - free(new_file.abs); - /* Pop new_file off the stack */ - file_list_head = new_file.next; - - /* Restore global per-config state */ - CONFIG = save_CONFIG; - fconfig = save_fconfig; - line_number = save_line_number; - defhost = save_defhost; -#ifdef IPSEC - policy = save_policy; -#endif -} - -static void -prepare_next_config(const char *file_name) -{ - /* Setup new state that is normally only done in main */ - CONFIG = file_name; - - /* Inherit default host and IPsec policy */ - defhost = newstr(defhost); - -#ifdef IPSEC - policy = (policy == NULL) ? NULL : newstr(policy); -#endif -} - -static void -read_glob_configs(char *pattern) { - glob_t results; - char *full_pattern; - int glob_result; - full_pattern = gen_file_pattern(CONFIG, pattern); - - DPRINTCONF("Found include directive '%s'", full_pattern); - - glob_result = glob(full_pattern, GLOB_NOSORT, glob_error, &results); - switch(glob_result) { - case 0: - /* No glob errors */ - break; - case GLOB_ABORTED: - ERR("Error while searching for include files"); - break; - case GLOB_NOMATCH: - /* It's fine if no files were matched. */ - DPRINTCONF("No files matched pattern '%s'", full_pattern); - break; - case GLOB_NOSPACE: - ERR("Error when searching for include files: %s", - strerror(errno)); - break; - default: - ERR("Unknown glob(3) error %d", errno); - break; - } - free(full_pattern); - - for (size_t i = 0; i < results.gl_pathc; i++) { - include_matched_path(results.gl_pathv[i]); - } - - globfree(&results); -} - -static void -include_matched_path(char *glob_path) -{ - struct stat sb; - char *tmp; - - if (lstat(glob_path, &sb) != 0) { - ERR("Error calling stat on path '%s': %s", glob_path, - strerror(errno)); - return; - } - - if (!S_ISREG(sb.st_mode) && !S_ISLNK(sb.st_mode)) { - DPRINTCONF("'%s' is not a file.", glob_path); - ERR("The matched path '%s' is not a regular file", glob_path); - return; - } - - DPRINTCONF("Include '%s'", glob_path); - - if (S_ISLNK(sb.st_mode)) { - tmp = glob_path; - glob_path = realpath(tmp, NULL); - } - - /* Ensure the file is not being reincluded .*/ - if (check_no_reinclude(glob_path)) { - prepare_next_config(glob_path); - config(); - } else { - DPRINTCONF("File '%s' already included in current include " - "chain", glob_path); - WRN("Including file '%s' would cause a circular " - "dependency", glob_path); - } - - if (S_ISLNK(sb.st_mode)) { - free(glob_path); - glob_path = tmp; - } -} - -static bool -check_no_reinclude(const char *glob_path) -{ - struct file_list *cur = file_list_head; - char *abs_path = realpath(glob_path, NULL); - - if (abs_path == NULL) { - ERR("Error checking real path for '%s': %s", - glob_path, strerror(errno)); - return false; - } - - DPRINTCONF("Absolute path '%s'", abs_path); - - for (cur = file_list_head; cur != NULL; cur = cur->next) { - if (strcmp(cur->abs, abs_path) == 0) { - /* file included more than once */ - /* TODO relative or abs path for logging error? */ - free(abs_path); - return false; - } - } - free(abs_path); - return true; -} - -/* Resolve the pattern relative to the config file the pattern is from */ -static char * -gen_file_pattern(const char *cur_config, const char *pattern) -{ - if (pattern[0] == '/') { - /* Absolute paths don't need any normalization */ - return newstr(pattern); - } - - /* pattern is relative */ - /* Find the end of the file's directory */ - size_t i, last = 0; - for (i = 0; cur_config[i] != '\0'; i++) { - if (cur_config[i] == '/') { - last = i; - } - } - - if (last == 0) { - /* cur_config is just a filename, pattern already correct */ - return newstr(pattern); - } - - /* Relativize pattern to cur_config file's directory */ - char *full_pattern = malloc(last + 1 + strlen(pattern) + 1); - if (full_pattern == NULL) { - syslog(LOG_ERR, "Out of memory."); - exit(EXIT_FAILURE); - } - memcpy(full_pattern, cur_config, last); - full_pattern[last] = '/'; - strcpy(&full_pattern[last + 1], pattern); - return full_pattern; -} - -static int -glob_error(const char *path, int error) -{ - WRN("Error while resolving path '%s': %s", path, strerror(error)); - return 0; -} - -/* Return 0 on allow, -1 if connection should be blocked */ -static int -rl_process(struct servtab *sep, int ctrl) -{ - struct se_ip_list_node *node; - time_t now = 0; /* 0 prevents GCC from complaining */ - bool istimevalid = false; - char hbuf[NI_MAXHOST]; - - DPRINTF(SERV_FMT ": processing rate-limiting", - SERV_PARAMS(sep)); - DPRINTF(SERV_FMT ": se_service_max " - "%zu and se_count %zu", SERV_PARAMS(sep), - sep->se_service_max, sep->se_count); - - /* se_count is incremented if rl_process will return 0 */ - if (sep->se_count == 0) { - now = rl_time(); - sep->se_time = now; - istimevalid = true; - } - - if (sep->se_count >= sep->se_service_max) { - if(!istimevalid) { - now = rl_time(); - istimevalid = true; - } - - if (now - sep->se_time > CNT_INTVL) { - rl_reset(sep, now); - } else { - syslog(LOG_ERR, - SERV_FMT ": max spawn rate (%zu in %ji seconds) " - "already met, closing until end of timeout in " - "%ju seconds", - SERV_PARAMS(sep), - sep->se_service_max, - (intmax_t)CNT_INTVL, - (uintmax_t)RETRYTIME); - - DPRINTF(SERV_FMT ": service not started", - SERV_PARAMS(sep)); - - rl_drop_connection(sep, ctrl); - - /* Close the server for 10 minutes */ - close_sep(sep); - if (!timingout) { - timingout = true; - alarm(RETRYTIME); - } - - return -1; - } - } - - if (sep->se_ip_max != SERVTAB_UNSPEC_SIZE_T) { - rl_get_name(sep, ctrl, hbuf); - node = rl_try_get_ip(sep, hbuf); - if(node == NULL) { - node = rl_add(sep, hbuf); - } - - DPRINTF( - SERV_FMT ": se_ip_max %zu and ip_count %zu", - SERV_PARAMS(sep), sep->se_ip_max, node->count); - - if (node->count >= sep->se_ip_max) { - if (!istimevalid) { - /* - * Only get the clock time if we didn't - * already - */ - now = rl_time(); - istimevalid = true; - } - - if (now - sep->se_time > CNT_INTVL) { - rl_reset(sep, now); - node = rl_add(sep, hbuf); - } else { - if (debug && node->count == sep->se_ip_max) { - /* - * Only log first failed request to - * prevent DoS attack writing to system - * log - */ - syslog(LOG_ERR, SERV_FMT - ": max ip spawn rate (%zu in " - "%ji seconds) for " - "%." TOSTRING(NI_MAXHOST) "s " - "already met; service not started", - SERV_PARAMS(sep), - sep->se_ip_max, - (intmax_t)CNT_INTVL, - node->address); - } - - DPRINTF(SERV_FMT ": service not started", - SERV_PARAMS(sep)); - - rl_drop_connection(sep, ctrl); - /* - * Increment so debug-syslog message will - * trigger only once - */ - node->count++; - return -1; - } - } - node->count++; - } - - DPRINTF(SERV_FMT ": running service ", SERV_PARAMS(sep)); - - sep->se_count++; - return 0; -} - -/* Get the remote's IP address in textual form into hbuf of size NI_MAXHOST */ -static void -rl_get_name(struct servtab *sep, int ctrl, char *hbuf) +bool +try_biltin(struct servtab *sep) { - struct sockaddr_storage addr; - socklen_t len = sizeof(struct sockaddr_storage); - switch (sep->se_socktype) { - case SOCK_STREAM: - if (getpeername(ctrl, (struct sockaddr *)&addr, &len) != 0) { - /* error, log it and skip ip rate limiting */ - syslog(LOG_ERR, - SERV_FMT " failed to get peer name of the " - "connection", SERV_PARAMS(sep)); - exit(EXIT_FAILURE); - } - break; - case SOCK_DGRAM: { - struct msghdr header = { - .msg_name = &addr, - .msg_namelen = sizeof(struct sockaddr_storage), - /* scatter/gather and control info is null */ - }; - ssize_t count; - - /* Peek so service can still get the packet */ - count = recvmsg(ctrl, &header, MSG_PEEK); - if (count == -1) { - syslog(LOG_ERR, - "failed to get dgram source address: %s; exiting", - strerror(errno)); - exit(EXIT_FAILURE); - } - break; - } - default: - DPRINTF(SERV_FMT ": ip_max rate limiting not supported for " - "socktype", SERV_PARAMS(sep)); - syslog(LOG_ERR, SERV_FMT - ": ip_max rate limiting not supported for socktype", - SERV_PARAMS(sep)); - exit(EXIT_FAILURE); - } - - if (getnameinfo((struct sockaddr *)&addr, - addr.ss_len, hbuf, - NI_MAXHOST, NULL, 0, NI_NUMERICHOST)) { - /* error, log it and skip ip rate limiting */ - syslog(LOG_ERR, - SERV_FMT ": failed to get name info of the incoming " - "connection; exiting", - SERV_PARAMS(sep)); - exit(EXIT_FAILURE); - } -} - -static void -rl_drop_connection(struct servtab *sep, int ctrl) -{ - - if (sep->se_wait == 0 && sep->se_socktype == SOCK_STREAM) { - /* - * If the fd isn't a listen socket, - * close the individual connection too. - */ - close(ctrl); - return; - } - if (sep->se_socktype != SOCK_DGRAM) { - return; - } - /* - * Drop the single datagram the service would have - * consumed if nowait. If this is a wait service, this - * will consume 1 datagram, and further received packets - * will be removed in the same way. - */ - struct msghdr header = { - /* All fields null, just consume one message */ - }; - ssize_t count; - - count = recvmsg(ctrl, &header, 0); - if (count == -1) { - syslog(LOG_ERR, - SERV_FMT ": failed to consume nowait dgram: %s", - SERV_PARAMS(sep), strerror(errno)); - exit(EXIT_FAILURE); - } - DPRINTF(SERV_FMT ": dropped dgram message", - SERV_PARAMS(sep)); -} - -static time_t -rl_time(void) -{ - struct timespec time; - if(clock_gettime(CLOCK_MONOTONIC, &time) == -1) { - syslog(LOG_ERR, "clock_gettime for rate limiting failed: %s; " - "exiting", strerror(errno)); - /* Exit inetd if rate limiting fails */ - exit(EXIT_FAILURE); - } - return time.tv_sec; -} - -static struct se_ip_list_node* -rl_add(struct servtab *sep, char* ip) -{ - DPRINTF( - SERV_FMT ": add ip %s to rate limiting tracking", - SERV_PARAMS(sep), ip); - - /* - * TODO memory could be saved by using a variable length malloc - * with only the length of ip instead of the existing address field - * NI_MAXHOST in length. - */ - struct se_ip_list_node* temp = malloc(sizeof(*temp)); - if (temp == NULL) { - syslog(LOG_ERR, "Out of memory."); - exit(EXIT_FAILURE); - } - temp->count = 0; - temp->next = NULL; - strlcpy(temp->address, ip, sizeof(temp->address)); - - if (sep->se_ip_list_head == NULL) { - /* List empty, insert as head */ - sep->se_ip_list_head = temp; - } else { - /* List not empty, insert as head, point next to prev head */ - temp->next = sep->se_ip_list_head; - sep->se_ip_list_head = temp; - } - - return temp; -} - -static void -rl_reset(struct servtab *sep, time_t now) -{ - DPRINTF(SERV_FMT ": %ji seconds passed; resetting rate limiting ", - SERV_PARAMS(sep), (intmax_t)(now - sep->se_time)); - - sep->se_count = 0; - sep->se_time = now; - if (sep->se_ip_max != SERVTAB_UNSPEC_SIZE_T) { - clear_ip_list(sep); - } -} - -static void -clear_ip_list(struct servtab *sep) { - struct se_ip_list_node *curr, *next; - curr = sep->se_ip_list_head; - - while (curr != NULL) { - next = curr->next; - free(curr); - curr = next; - } - sep->se_ip_list_head = NULL; -} - -static struct se_ip_list_node * -rl_try_get_ip(struct servtab *sep, char *ip) -{ - struct se_ip_list_node *curr; - - DPRINTF( - SERV_FMT ": look up ip %s for ip_max rate limiting", - SERV_PARAMS(sep), ip); - - for (curr = sep->se_ip_list_head; curr != NULL; curr = curr->next) { - if (!strncmp(curr->address, ip, NI_MAXHOST)) { - /* IP addr match */ - return curr; + for(size_t i = 0; i < __arraycount(biltins); i++) { + if (biltins[i].bi_socktype == sep->se_socktype && + strcmp(biltins[i].bi_service, sep->se_service) == 0) { + sep->se_bi = &biltins[i]; + sep->se_wait = biltins[i].bi_wait; + return true; } } - return NULL; + return false; } Index: src/usr.sbin/inetd/inetd.h diff -u src/usr.sbin/inetd/inetd.h:1.3 src/usr.sbin/inetd/inetd.h:1.4 --- src/usr.sbin/inetd/inetd.h:1.3 Fri Sep 3 16:24:28 2021 +++ src/usr.sbin/inetd/inetd.h Tue Oct 12 15:08:04 2021 @@ -1,4 +1,4 @@ -/* $NetBSD: inetd.h,v 1.3 2021/09/03 20:24:28 rillig Exp $ */ +/* $NetBSD: inetd.h,v 1.4 2021/10/12 19:08:04 christos Exp $ */ /*- * Copyright (c) 1998, 2003 The NetBSD Foundation, Inc. @@ -66,12 +66,14 @@ #include <sys/socket.h> #include <sys/time.h> #include <sys/un.h> +#include <sys/queue.h> #include <arpa/inet.h> #include <netdb.h> #include <stdbool.h> + #include "pathnames.h" #ifdef IPSEC @@ -121,6 +123,21 @@ typedef enum service_type { #define STRINGIFY(x) #x #define TOSTRING(x) STRINGIFY(x) +/* "Unspecified" indicator value for servtabs (mainly used by v2 syntax) */ +#define SERVTAB_UNSPEC_VAL -1 + +#define SERVTAB_UNSPEC_SIZE_T SIZE_MAX + +#define SERVTAB_COUNT_MAX (SIZE_MAX - (size_t)1) + +/* Standard logging and debug print format for a servtab */ +#define SERV_FMT "%s/%s" +#define SERV_PARAMS(sep) sep->se_service,sep->se_proto + +/* rate limiting macros */ +#define CNT_INTVL ((time_t)60) /* servers in CNT_INTVL sec. */ +#define RETRYTIME (60*10) /* retry after bind or server fail */ + struct servtab { char *se_hostaddr; /* host address to listen on */ char *se_service; /* name of service */ @@ -148,30 +165,79 @@ struct servtab { int se_fd; /* open descriptor */ service_type se_type; /* type */ union { - struct sockaddr_storage se_ctrladdr_storage; /* ensure correctness of C struct initializer */ + /* ensure correctness of C struct initializer */ + struct sockaddr_storage se_ctrladdr_storage; struct sockaddr se_ctrladdr; struct sockaddr_in se_ctrladdr_in; struct sockaddr_in6 se_ctrladdr_in6; /* in6 is used by bind()/getaddrinfo */ struct sockaddr_un se_ctrladdr_un; }; /* bound address */ socklen_t se_ctrladdr_size; - size_t se_service_max; /* max # of instances of this service */ + size_t se_service_max; /* max # of instances of this service per minute */ size_t se_count; /* number of instances of this service started since se_time */ size_t se_ip_max; /* max # of instances of this service per ip per minute */ - struct se_ip_list_node { - struct se_ip_list_node *next; - size_t count; /* - * number of instances of this service started from - * this ip address since se_time (includes - * attempted starts if greater than se_ip_max) - */ - char address[NI_MAXHOST]; - } *se_ip_list_head; /* linked list of number of requests per ip */ + SLIST_HEAD(iplist, rl_ip_node) se_rl_ip_list; /* per-address (IP) rate limting */ time_t se_time; /* start of se_count and ip_max counts, in seconds from arbitrary point */ + + /* TODO convert to using SLIST */ struct servtab *se_next; }; -/* From inetd.c */ +struct rl_ip_node { + /* Linked list entries */ + SLIST_ENTRY(rl_ip_node) entries; + /* + * Number of service spawns from *_addr since se_time (includes + * attempted starts if greater than se_ip_max). + */ + size_t count; + union { + struct in_addr ipv4_addr; +#ifdef INET6 + /* align for efficient comparison in rl_try_get, could use 8 instead */ + struct in6_addr ipv6_addr __attribute__((aligned(16))); +#endif + /* + * other_addr is used for other address types besides the + * special cases (IPv4/IPv6), using getnameinfo. + */ + struct { + /* A field is required before the special array member */ + char _placeholder; + /* malloc'd storage varies with length of string */ + char other_addr[]; + }; + }; + /* + * Do not declare further members after union, offsetof is used to + * determine malloc size. + */ +}; + +/* + * From inetd.c + */ + +void setup(struct servtab *); +void close_sep(struct servtab *); +void register_rpc(struct servtab *); +void unregister_rpc(struct servtab *); +bool try_biltin(struct servtab *); + +/* Global debug mode boolean, enabled with -d */ +extern int debug; + +/* rate limit or other error timed out flag */ +extern int timingout; + +/* servtab linked list */ +extern struct servtab *servtab; + +/* + * From parse.c + */ + +void config_root(void); int parse_protocol(struct servtab *); int parse_wait(struct servtab *, int); int parse_server(struct servtab *, const char *); @@ -179,27 +245,32 @@ void parse_socktype(char *, struct serv void parse_accept_filter(char *, struct servtab *); char *nextline(FILE *); char *newstr(const char *); -void freeconfig(struct servtab *); -/* Global debug mode boolean, enabled with -d */ -extern int debug; +/* Current line number in current config file */ +extern size_t line_number; /* Current config file path */ -extern const char *CONFIG; +extern const char *CONFIG; /* Open config file */ extern FILE *fconfig; -/* Current line number in current config file */ -extern size_t line_number; - /* Default listening hostname/IP for current config file */ -extern char *defhost; +extern char *defhost; /* Default IPsec policy for current config file */ -extern char *policy; +extern char *policy; + +/* + * From ratelimit.c + */ -/* From parse_v2.c */ +int rl_process(struct servtab *, int); +void rl_clear_ip_list(struct servtab *); + +/* + * From parse_v2.c + */ typedef enum parse_v2_result {V2_SUCCESS, V2_SKIP, V2_ERROR} parse_v2_result; @@ -210,15 +281,4 @@ typedef enum parse_v2_result {V2_SUCCESS */ parse_v2_result parse_syntax_v2(struct servtab *, char **); -/* "Unspecified" indicator value for servtabs (mainly used by v2 syntax) */ -#define SERVTAB_UNSPEC_VAL -1 - -#define SERVTAB_UNSPEC_SIZE_T SIZE_MAX - -#define SERVTAB_COUNT_MAX (SIZE_MAX - (size_t)1) - -/* Standard logging and debug print format for a servtab */ -#define SERV_FMT "%s/%s" -#define SERV_PARAMS(sep) sep->se_service,sep->se_proto - #endif Index: src/usr.sbin/inetd/parse_v2.c diff -u src/usr.sbin/inetd/parse_v2.c:1.5 src/usr.sbin/inetd/parse_v2.c:1.6 --- src/usr.sbin/inetd/parse_v2.c:1.5 Fri Sep 3 16:24:28 2021 +++ src/usr.sbin/inetd/parse_v2.c Tue Oct 12 15:08:04 2021 @@ -1,4 +1,4 @@ -/* $NetBSD: parse_v2.c,v 1.5 2021/09/03 20:24:28 rillig Exp $ */ +/* $NetBSD: parse_v2.c,v 1.6 2021/10/12 19:08:04 christos Exp $ */ /*- * Copyright (c) 2021 The NetBSD Foundation, Inc. @@ -30,7 +30,7 @@ */ #include <sys/cdefs.h> -__RCSID("$NetBSD: parse_v2.c,v 1.5 2021/09/03 20:24:28 rillig Exp $"); +__RCSID("$NetBSD: parse_v2.c,v 1.6 2021/10/12 19:08:04 christos Exp $"); #include <ctype.h> #include <errno.h> @@ -153,11 +153,9 @@ parse_syntax_v2(struct servtab *sep, cha bool is_valid_definition = true; /* Line number of service for error logging. */ size_t line_number_start = line_number; - invoke_result result; for (;;) { - switch(result = - parse_invoke_handler(&is_valid_definition, cpp, sep)) { + switch(parse_invoke_handler(&is_valid_definition, cpp, sep)) { case INVOKE_SUCCESS: /* Keep reading more options in. */ continue; @@ -658,6 +656,7 @@ is_internal(struct servtab *sep) */ static hresult +/*ARGSUSED*/ unknown_handler(struct servtab *sep, vlist values) { /* Return failure for an unknown service name. */ Added files: Index: src/usr.sbin/inetd/parse.c diff -u /dev/null src/usr.sbin/inetd/parse.c:1.1 --- /dev/null Tue Oct 12 15:08:04 2021 +++ src/usr.sbin/inetd/parse.c Tue Oct 12 15:08:04 2021 @@ -0,0 +1,1376 @@ +/* $NetBSD: parse.c,v 1.1 2021/10/12 19:08:04 christos Exp $ */ + +/*- + * Copyright (c) 1998, 2003 The NetBSD Foundation, Inc. + * All rights reserved. + * + * This code is derived from software contributed to The NetBSD Foundation + * by Jason R. Thorpe of the Numerical Aerospace Simulation Facility, + * NASA Ames Research Center and by Matthias Scheler. + * + * 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 NETBSD FOUNDATION, INC. 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 FOUNDATION 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, 1991, 1993, 1994 + * 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. 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. + */ + +#include <sys/cdefs.h> +#ifndef lint +#if 0 +static char sccsid[] = "@(#)inetd.c 8.4 (Berkeley) 4/13/94"; +#else +__RCSID("$NetBSD: parse.c,v 1.1 2021/10/12 19:08:04 christos Exp $"); +#endif +#endif /* not lint */ + +/* + * This file contains code and state for loading and managing servtabs. + * The "positional" syntax parsing is performed in this file. See parse_v2.c + * for "key-values" syntax parsing. + */ + +#include <sys/param.h> +#include <sys/stat.h> +#include <sys/socket.h> +#include <sys/queue.h> + +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <glob.h> +#include <libgen.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> +#include <unistd.h> + +#include "inetd.h" + +static void config(void); +static void endconfig(void); +static struct servtab *enter(struct servtab *); +static struct servtab *getconfigent(char **); +#ifdef DEBUG_ENABLE +static void print_service(const char *, struct servtab *); +#endif +static struct servtab init_servtab(void); +static void include_configs(char *); +static int glob_error(const char *, int); +static void read_glob_configs(char *); +static void prepare_next_config(const char*); +static bool is_same_service(const struct servtab *, const struct servtab *); +static char *gen_file_pattern(const char *, const char *); +static bool check_no_reinclude(const char *); +static void include_matched_path(char *); +static void purge_unchecked(void); +static void freeconfig(struct servtab *); +static char *skip(char **); + +size_t line_number; +FILE *fconfig; +/* Temporary storage for new servtab */ +static struct servtab serv; +/* Current line from current config file */ +static char line[LINE_MAX]; +char *defhost; +#ifdef IPSEC +char *policy; +#endif + +/* + * Recursively merge loaded service definitions with any defined + * in the current or included config files. + */ +static void +config(void) +{ + struct servtab *sep, *cp; + /* + * Current position in line, used with key-values notation, + * saves cp across getconfigent calls. + */ + char *current_pos; + size_t n; + + /* open config file from beginning */ + fconfig = fopen(CONFIG, "r"); + if (fconfig == NULL) { + DPRINTF("Could not open file \"%s\": %s", + CONFIG, strerror(errno)); + syslog(LOG_ERR, "%s: %m", CONFIG); + return; + } + + /* First call to nextline will advance line_number to 1 */ + line_number = 0; + + /* Start parsing at the beginning of the first line */ + current_pos = nextline(fconfig); + + while ((cp = getconfigent(¤t_pos)) != NULL) { + /* Find an already existing service definition */ + for (sep = servtab; sep != NULL; sep = sep->se_next) + if (is_same_service(sep, cp)) + break; + if (sep != NULL) { + int i; + +#define SWAP(type, a, b) {type c = a; a = b; b = c;} + + /* + * sep->se_wait may be holding the pid of a daemon + * that we're waiting for. If so, don't overwrite + * it unless the config file explicitly says don't + * wait. + */ + if (cp->se_bi == 0 && + (sep->se_wait == 1 || cp->se_wait == 0)) + sep->se_wait = cp->se_wait; + SWAP(char *, sep->se_user, cp->se_user); + SWAP(char *, sep->se_group, cp->se_group); + SWAP(char *, sep->se_server, cp->se_server); + for (i = 0; i < MAXARGV; i++) + SWAP(char *, sep->se_argv[i], cp->se_argv[i]); +#ifdef IPSEC + SWAP(char *, sep->se_policy, cp->se_policy); +#endif + SWAP(service_type, cp->se_type, sep->se_type); + SWAP(size_t, cp->se_service_max, sep->se_service_max); + SWAP(size_t, cp->se_ip_max, sep->se_ip_max); +#undef SWAP + if (isrpcservice(sep)) + unregister_rpc(sep); + sep->se_rpcversl = cp->se_rpcversl; + sep->se_rpcversh = cp->se_rpcversh; + freeconfig(cp); +#ifdef DEBUG_ENABLE + if (debug) + print_service("REDO", sep); +#endif + } else { + sep = enter(cp); +#ifdef DEBUG_ENABLE + if (debug) + print_service("ADD ", sep); +#endif + } + sep->se_checked = 1; + + /* + * Remainder of config(void) checks validity of servtab options + * and sets up the service by setting up sockets + * (in setup(servtab)). + */ + switch (sep->se_family) { + case AF_LOCAL: + if (sep->se_fd != -1) + break; + n = strlen(sep->se_service); + if (n >= sizeof(sep->se_ctrladdr_un.sun_path)) { + syslog(LOG_ERR, "%s/%s: address too long", + sep->se_service, sep->se_proto); + sep->se_checked = 0; + continue; + } + (void)unlink(sep->se_service); + strlcpy(sep->se_ctrladdr_un.sun_path, + sep->se_service, n + 1); + sep->se_ctrladdr_un.sun_family = AF_LOCAL; + sep->se_ctrladdr_size = (socklen_t)(n + + sizeof(sep->se_ctrladdr_un) - + sizeof(sep->se_ctrladdr_un.sun_path)); + if (!ISMUX(sep)) + setup(sep); + break; + case AF_INET: +#ifdef INET6 + case AF_INET6: +#endif + { + struct addrinfo hints, *res; + char *host; + const char *port; + int error; + int s; + + /* check if the family is supported */ + s = socket(sep->se_family, SOCK_DGRAM, 0); + if (s < 0) { + syslog(LOG_WARNING, + "%s/%s: %s: the address family is not " + "supported by the kernel", + sep->se_service, sep->se_proto, + sep->se_hostaddr); + sep->se_checked = false; + continue; + } + close(s); + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = sep->se_family; + hints.ai_socktype = sep->se_socktype; + hints.ai_flags = AI_PASSIVE; + if (strcmp(sep->se_hostaddr, "*") == 0) + host = NULL; + else + host = sep->se_hostaddr; + if (isrpcservice(sep) || ISMUX(sep)) + port = "0"; + else + port = sep->se_service; + error = getaddrinfo(host, port, &hints, &res); + if (error != 0) { + if (error == EAI_SERVICE) { + /* gai_strerror not friendly enough */ + syslog(LOG_WARNING, SERV_FMT ": " + "unknown service", + SERV_PARAMS(sep)); + } else { + syslog(LOG_ERR, SERV_FMT ": %s: %s", + SERV_PARAMS(sep), + sep->se_hostaddr, + gai_strerror(error)); + } + sep->se_checked = false; + continue; + } + if (res->ai_next != NULL) { + syslog(LOG_ERR, SERV_FMT + ": %s: resolved to multiple addr", + SERV_PARAMS(sep), + sep->se_hostaddr); + sep->se_checked = false; + freeaddrinfo(res); + continue; + } + memcpy(&sep->se_ctrladdr, res->ai_addr, + res->ai_addrlen); + if (ISMUX(sep)) { + sep->se_fd = -1; + freeaddrinfo(res); + continue; + } + sep->se_ctrladdr_size = res->ai_addrlen; + freeaddrinfo(res); +#ifdef RPC + if (isrpcservice(sep)) { + struct rpcent *rp; + + sep->se_rpcprog = atoi(sep->se_service); + if (sep->se_rpcprog == 0) { + rp = getrpcbyname(sep->se_service); + if (rp == 0) { + syslog(LOG_ERR, + SERV_FMT + ": unknown service", + SERV_PARAMS(sep)); + sep->se_checked = false; + continue; + } + sep->se_rpcprog = rp->r_number; + } + if (sep->se_fd == -1 && !ISMUX(sep)) + setup(sep); + if (sep->se_fd != -1) + register_rpc(sep); + } else +#endif + { + if (sep->se_fd >= 0) + close_sep(sep); + if (sep->se_fd == -1 && !ISMUX(sep)) + setup(sep); + } + } + } + } + endconfig(); +} + +static struct servtab * +enter(struct servtab *cp) +{ + struct servtab *sep; + + sep = malloc(sizeof (*sep)); + if (sep == NULL) { + syslog(LOG_ERR, "Out of memory."); + exit(EXIT_FAILURE); + } + *sep = *cp; + sep->se_fd = -1; + sep->se_rpcprog = -1; + sep->se_next = servtab; + servtab = sep; + return (sep); +} + +static void +endconfig(void) +{ + if (fconfig != NULL) { + (void) fclose(fconfig); + fconfig = NULL; + } + if (defhost != NULL) { + free(defhost); + defhost = NULL; + } + +#ifdef IPSEC + if (policy != NULL) { + free(policy); + policy = NULL; + } +#endif + +} + +#define LOG_EARLY_ENDCONF() \ + ERR("Exiting %s early. Some services will be unavailable", CONFIG) + +#define LOG_TOO_FEW_ARGS() \ + ERR("Expected more arguments") + +/* Parse the next service and apply any directives, and returns it as servtab */ +static struct servtab * +getconfigent(char **current_pos) +{ + struct servtab *sep = &serv; + int argc, val; + char *cp, *cp0, *arg, *buf0, *buf1, *sz0, *sz1; + static char TCPMUX_TOKEN[] = "tcpmux/"; +#define MUX_LEN (sizeof(TCPMUX_TOKEN)-1) + char *hostdelim; + + /* + * Pre-condition: current_pos points into line, + * line contains config line. Continue where the last getconfigent + * left off. Allows for multiple service definitions per line. + */ + cp = *current_pos; + + if (/*CONSTCOND*/false) { + /* + * Go to the next line, but only after attemting to read the + * current one! Keep reading until we find a valid definition + * or EOF. + */ +more: + cp = nextline(fconfig); + } + + if (cp == NULL) { + /* EOF or I/O error, let config() know to exit the file */ + return NULL; + } + + /* Comments and IPsec policies */ + if (cp[0] == '#') { +#ifdef IPSEC + /* lines starting with #@ is not a comment, but the policy */ + if (cp[1] == '@') { + char *p; + for (p = cp + 2; isspace((unsigned char)*p); p++) + ; + if (*p == '\0') { + if (policy) + free(policy); + policy = NULL; + } else { + if (ipsecsetup_test(p) < 0) { + ERR("Invalid IPsec policy \"%s\"", p); + LOG_EARLY_ENDCONF(); + /* + * Stop reading the current config to + * prevent services from being run + * without IPsec. + */ + return NULL; + } else { + if (policy) + free(policy); + policy = newstr(p); + } + } + } +#endif + + goto more; + } + + /* Parse next token: listen-addr/hostname, service-spec, .include */ + arg = skip(&cp); + + if (cp == NULL) { + goto more; + } + + if(arg[0] == '.') { + if (strcmp(&arg[1], "include") == 0) { + /* include directive */ + arg = skip(&cp); + if(arg == NULL) { + LOG_TOO_FEW_ARGS(); + return NULL; + } + include_configs(arg); + goto more; + } else { + ERR("Unknown directive '%s'", &arg[1]); + goto more; + } + } + + /* After this point, we might need to store data in a servtab */ + *sep = init_servtab(); + + /* Check for a host name. */ + hostdelim = strrchr(arg, ':'); + if (hostdelim != NULL) { + *hostdelim = '\0'; + if (arg[0] == '[' && hostdelim > arg && hostdelim[-1] == ']') { + hostdelim[-1] = '\0'; + sep->se_hostaddr = newstr(arg + 1); + } else + sep->se_hostaddr = newstr(arg); + arg = hostdelim + 1; + /* + * If the line is of the form `host:', then just change the + * default host for the following lines. + */ + if (*arg == '\0') { + arg = skip(&cp); + if (cp == NULL) { + free(defhost); + defhost = sep->se_hostaddr; + goto more; + } + } + } else { + /* No host address found, set it to NULL to indicate absence */ + sep->se_hostaddr = NULL; + } + if (strncmp(arg, TCPMUX_TOKEN, MUX_LEN) == 0) { + char *c = arg + MUX_LEN; + if (*c == '+') { + sep->se_type = MUXPLUS_TYPE; + c++; + } else + sep->se_type = MUX_TYPE; + sep->se_service = newstr(c); + } else { + sep->se_service = newstr(arg); + sep->se_type = NORM_TYPE; + } + + DPRINTCONF("Found service definition '%s'", sep->se_service); + + /* on/off/socktype */ + arg = skip(&cp); + if (arg == NULL) { + LOG_TOO_FEW_ARGS(); + freeconfig(sep); + goto more; + } + + /* Check for new v2 syntax */ + if (strcmp(arg, "on") == 0 || strncmp(arg, "on#", 3) == 0) { + + if (arg[2] == '#') { + cp = nextline(fconfig); + } + + switch(parse_syntax_v2(sep, &cp)) { + case V2_SUCCESS: + *current_pos = cp; + return sep; + case V2_SKIP: + /* + * Skip invalid definitions, freeconfig is called in + * parse_v2.c + */ + *current_pos = cp; + freeconfig(sep); + goto more; + case V2_ERROR: + /* + * Unrecoverable error, stop reading. freeconfig + * is called in parse_v2.c + */ + LOG_EARLY_ENDCONF(); + freeconfig(sep); + return NULL; + } + } else if (strcmp(arg, "off") == 0 || strncmp(arg, "off#", 4) == 0) { + + if (arg[3] == '#') { + cp = nextline(fconfig); + } + + /* Parse syntax the same as with 'on', but ignore the result */ + switch(parse_syntax_v2(sep, &cp)) { + case V2_SUCCESS: + case V2_SKIP: + *current_pos = cp; + freeconfig(sep); + goto more; + case V2_ERROR: + /* Unrecoverable error, stop reading */ + LOG_EARLY_ENDCONF(); + freeconfig(sep); + return NULL; + } + } else { + /* continue parsing v1 */ + parse_socktype(arg, sep); + if (sep->se_socktype == SOCK_STREAM) { + parse_accept_filter(arg, sep); + } + if (sep->se_hostaddr == NULL) { + /* Set host to current default */ + sep->se_hostaddr = newstr(defhost); + } + } + + /* protocol */ + arg = skip(&cp); + if (arg == NULL) { + LOG_TOO_FEW_ARGS(); + freeconfig(sep); + goto more; + } + if (sep->se_type == NORM_TYPE && + strncmp(arg, "faith/", strlen("faith/")) == 0) { + arg += strlen("faith/"); + sep->se_type = FAITH_TYPE; + } + sep->se_proto = newstr(arg); + +#define MALFORMED(arg) \ +do { \ + ERR("%s: malformed buffer size option `%s'", \ + sep->se_service, (arg)); \ + freeconfig(sep); \ + goto more; \ +} while (false) + +#define GETVAL(arg) \ +do { \ + if (!isdigit((unsigned char)*(arg))) \ + MALFORMED(arg); \ + val = (int)strtol((arg), &cp0, 10); \ + if (cp0 != NULL) { \ + if (cp0[1] != '\0') \ + MALFORMED((arg)); \ + if (cp0[0] == 'k') \ + val *= 1024; \ + if (cp0[0] == 'm') \ + val *= 1024 * 1024; \ + } \ + if (val < 1) { \ + ERR("%s: invalid buffer size `%s'", \ + sep->se_service, (arg)); \ + freeconfig(sep); \ + goto more; \ + } \ +} while (false) + +#define ASSIGN(arg) \ +do { \ + if (strcmp((arg), "sndbuf") == 0) \ + sep->se_sndbuf = val; \ + else if (strcmp((arg), "rcvbuf") == 0) \ + sep->se_rcvbuf = val; \ + else \ + MALFORMED((arg)); \ +} while (false) + + /* + * Extract the send and receive buffer sizes before parsing + * the protocol. + */ + sep->se_sndbuf = sep->se_rcvbuf = 0; + buf0 = buf1 = sz0 = sz1 = NULL; + if ((buf0 = strchr(sep->se_proto, ',')) != NULL) { + /* Not meaningful for Tcpmux services. */ + if (ISMUX(sep)) { + ERR("%s: can't specify buffer sizes for " + "tcpmux services", sep->se_service); + goto more; + } + + /* Skip the , */ + *buf0++ = '\0'; + + /* Check to see if another socket buffer size was specified. */ + if ((buf1 = strchr(buf0, ',')) != NULL) { + /* Skip the , */ + *buf1++ = '\0'; + + /* Make sure a 3rd one wasn't specified. */ + if (strchr(buf1, ',') != NULL) { + ERR("%s: too many buffer sizes", + sep->se_service); + goto more; + } + + /* Locate the size. */ + if ((sz1 = strchr(buf1, '=')) == NULL) + MALFORMED(buf1); + + /* Skip the = */ + *sz1++ = '\0'; + } + + /* Locate the size. */ + if ((sz0 = strchr(buf0, '=')) == NULL) + MALFORMED(buf0); + + /* Skip the = */ + *sz0++ = '\0'; + + GETVAL(sz0); + ASSIGN(buf0); + + if (buf1 != NULL) { + GETVAL(sz1); + ASSIGN(buf1); + } + } + +#undef ASSIGN +#undef GETVAL +#undef MALFORMED + + if (parse_protocol(sep)) { + freeconfig(sep); + goto more; + } + + /* wait/nowait:max */ + arg = skip(&cp); + if (arg == NULL) { + LOG_TOO_FEW_ARGS(); + freeconfig(sep); + goto more; + } + + /* Rate limiting parsing */ { + char *cp1; + if ((cp1 = strchr(arg, ':')) == NULL) + cp1 = strchr(arg, '.'); + if (cp1 != NULL) { + int rstatus; + *cp1++ = '\0'; + sep->se_service_max = (size_t)strtou(cp1, NULL, 10, 0, + SERVTAB_COUNT_MAX, &rstatus); + + if (rstatus != 0) { + if (rstatus != ERANGE) { + /* For compatibility w/ atoi parsing */ + sep->se_service_max = 0; + } + + WRN("Improper \"max\" value '%s', " + "using '%zu' instead: %s", + cp1, + sep->se_service_max, + strerror(rstatus)); + } + + } else + sep->se_service_max = TOOMANY; + } + if (parse_wait(sep, strcmp(arg, "wait") == 0)) { + freeconfig(sep); + goto more; + } + + /* Parse user:group token */ + arg = skip(&cp); + if(arg == NULL) { + LOG_TOO_FEW_ARGS(); + freeconfig(sep); + goto more; + } + char* separator = strchr(arg, ':'); + if (separator == NULL) { + /* Backwards compatibility, allow dot instead of colon */ + separator = strchr(arg, '.'); + } + + if (separator == NULL) { + /* Only user was specified */ + sep->se_group = NULL; + } else { + *separator = '\0'; + sep->se_group = newstr(separator + 1); + } + + sep->se_user = newstr(arg); + + /* Parser server-program (path to binary or "internal") */ + arg = skip(&cp); + if (arg == NULL) { + LOG_TOO_FEW_ARGS(); + freeconfig(sep); + goto more; + } + if (parse_server(sep, arg)) { + freeconfig(sep); + goto more; + } + + argc = 0; + for (arg = skip(&cp); cp != NULL; arg = skip(&cp)) { + if (argc < MAXARGV) + sep->se_argv[argc++] = newstr(arg); + } + while (argc <= MAXARGV) + sep->se_argv[argc++] = NULL; +#ifdef IPSEC + sep->se_policy = policy != NULL ? newstr(policy) : NULL; +#endif + /* getconfigent read a positional service def, move to next line */ + *current_pos = nextline(fconfig); + return (sep); +} + +void +freeconfig(struct servtab *cp) +{ + int i; + + free(cp->se_hostaddr); + free(cp->se_service); + free(cp->se_proto); + free(cp->se_user); + free(cp->se_group); + free(cp->se_server); + for (i = 0; i < MAXARGV; i++) + free(cp->se_argv[i]); +#ifdef IPSEC + free(cp->se_policy); +#endif +} + +/* + * Get next token *in the current service definition* from config file. + * Allows multi-line parse if single space or single tab-indented. + * Things in quotes are considered single token. + * Advances cp to next token. + */ +static char * +skip(char **cpp) +{ + char *cp = *cpp; + char *start; + char quote; + + if (*cpp == NULL) + return (NULL); + +again: + while (*cp == ' ' || *cp == '\t') + cp++; + if (*cp == '\0') { + int c; + + c = getc(fconfig); + (void) ungetc(c, fconfig); + if (c == ' ' || c == '\t') + if ((cp = nextline(fconfig)) != NULL) + goto again; + *cpp = NULL; + return (NULL); + } + start = cp; + /* Parse shell-style quotes */ + quote = '\0'; + while (*cp != '\0' && (quote != '\0' || (*cp != ' ' && *cp != '\t'))) { + if (*cp == '\'' || *cp == '"') { + if (quote != '\0' && *cp != quote) + cp++; + else { + if (quote != '\0') + quote = '\0'; + else + quote = *cp; + memmove(cp, cp+1, strlen(cp)); + } + } else + cp++; + } + if (*cp != '\0') + *cp++ = '\0'; + *cpp = cp; + return (start); +} + +char * +nextline(FILE *fd) +{ + char *cp; + + if (fgets(line, (int)sizeof(line), fd) == NULL) { + if (ferror(fd) != 0) { + ERR("Error when reading next line: %s", + strerror(errno)); + } + return NULL; + } + cp = strchr(line, '\n'); + if (cp != NULL) + *cp = '\0'; + line_number++; + return line; +} + +char * +newstr(const char *cp) +{ + char *dp; + if ((dp = strdup((cp != NULL) ? cp : "")) != NULL) + return (dp); + syslog(LOG_ERR, "strdup: %m"); + exit(EXIT_FAILURE); + /*NOTREACHED*/ +} + +#ifdef DEBUG_ENABLE +/* + * print_service: + * Dump relevant information to stderr + */ +static void +print_service(const char *action, struct servtab *sep) +{ + + if (isrpcservice(sep)) + fprintf(stderr, + "%s: %s rpcprog=%d, rpcvers = %d/%d, proto=%s, " + "wait.max=%d.%zu, " + "user:group=%s:%s builtin=%lx server=%s" +#ifdef IPSEC + " policy=\"%s\"" +#endif + "\n", + action, sep->se_service, + sep->se_rpcprog, sep->se_rpcversh, sep->se_rpcversl, + sep->se_proto, sep->se_wait, sep->se_service_max, + sep->se_user, sep->se_group, + (long)sep->se_bi, sep->se_server +#ifdef IPSEC + , (sep->se_policy != NULL ? sep->se_policy : "") +#endif + ); + else + fprintf(stderr, + "%s: %s:%s proto=%s%s, wait.max=%d.%zu, user:group=%s:%s " + "builtin=%lx " + "server=%s" +#ifdef IPSEC + " policy=%s" +#endif + "\n", + action, sep->se_hostaddr, sep->se_service, + sep->se_type == FAITH_TYPE ? "faith/" : "", + sep->se_proto, + sep->se_wait, sep->se_service_max, sep->se_user, + sep->se_group, (long)sep->se_bi, sep->se_server +#ifdef IPSEC + , (sep->se_policy != NULL ? sep->se_policy : "") +#endif + ); +} +#endif + +void +config_root(void) +{ + struct servtab *sep; + /* Uncheck services */ + for (sep = servtab; sep != NULL; sep = sep->se_next) { + sep->se_checked = false; + } + defhost = newstr("*"); +#ifdef IPSEC + policy = NULL; +#endif + fconfig = NULL; + config(); + purge_unchecked(); +} + +static void +purge_unchecked(void) +{ + struct servtab *sep, **sepp = &servtab; + int servtab_count = 0; + while ((sep = *sepp) != NULL) { + if (sep->se_checked) { + sepp = &sep->se_next; + servtab_count++; + continue; + } + *sepp = sep->se_next; + if (sep->se_fd >= 0) + close_sep(sep); + if (isrpcservice(sep)) + unregister_rpc(sep); + if (sep->se_family == AF_LOCAL) + (void)unlink(sep->se_service); +#ifdef DEBUG_ENABLE + if (debug) + print_service("FREE", sep); +#endif + freeconfig(sep); + free(sep); + } + DPRINTF("%d service(s) loaded.", servtab_count); +} + +static bool +is_same_service(const struct servtab *sep, const struct servtab *cp) +{ + return + strcmp(sep->se_service, cp->se_service) == 0 && + strcmp(sep->se_hostaddr, cp->se_hostaddr) == 0 && + strcmp(sep->se_proto, cp->se_proto) == 0 && + sep->se_family == cp->se_family && + ISMUX(sep) == ISMUX(cp); +} + +int +parse_protocol(struct servtab *sep) +{ + int val; + + if (strcmp(sep->se_proto, "unix") == 0) { + sep->se_family = AF_LOCAL; + } else { + val = (int)strlen(sep->se_proto); + if (val == 0) { + ERR("%s: invalid protocol specified", + sep->se_service); + return -1; + } + val = sep->se_proto[val - 1]; + switch (val) { + case '4': /*tcp4 or udp4*/ + sep->se_family = AF_INET; + break; +#ifdef INET6 + case '6': /*tcp6 or udp6*/ + sep->se_family = AF_INET6; + break; +#endif + default: + /* + * Use 'default' IP version which is IPv4, may + * eventually be changed to AF_INET6 + */ + sep->se_family = AF_INET; + break; + } + if (strncmp(sep->se_proto, "rpc/", 4) == 0) { +#ifdef RPC + char *cp1, *ccp; + cp1 = strchr(sep->se_service, '/'); + if (cp1 == 0) { + ERR("%s: no rpc version", + sep->se_service); + return -1; + } + *cp1++ = '\0'; + sep->se_rpcversl = sep->se_rpcversh = + (int)strtol(cp1, &ccp, 0); + if (ccp == cp1) { + badafterall: + ERR("%s/%s: bad rpc version", + sep->se_service, cp1); + return -1; + } + if (*ccp == '-') { + cp1 = ccp + 1; + sep->se_rpcversh = (int)strtol(cp1, &ccp, 0); + if (ccp == cp1) + goto badafterall; + } +#else + ERR("%s: rpc services not supported", + sep->se_service); + return -1; +#endif /* RPC */ + } + } + return 0; +} + +int +parse_wait(struct servtab *sep, int wait) +{ + if (!ISMUX(sep)) { + sep->se_wait = wait; + return 0; + } + /* + * Silently enforce "nowait" for TCPMUX services since + * they don't have an assigned port to listen on. + */ + sep->se_wait = 0; + + if (strncmp(sep->se_proto, "tcp", 3)) { + ERR("bad protocol for tcpmux service %s", + sep->se_service); + return -1; + } + if (sep->se_socktype != SOCK_STREAM) { + ERR("bad socket type for tcpmux service %s", + sep->se_service); + return -1; + } + return 0; +} + +int +parse_server(struct servtab *sep, const char *arg) +{ + sep->se_server = newstr(arg); + if (strcmp(sep->se_server, "internal") != 0) { + sep->se_bi = NULL; + return 0; + } + + if (!try_biltin(sep)) { + ERR("Internal service %s unknown", sep->se_service); + return -1; + } + return 0; +} + +/* TODO test to make sure accept filter still works */ +void +parse_accept_filter(char *arg, struct servtab *sep) +{ + char *accf, *accf_arg; + /* one and only one accept filter */ + accf = strchr(arg, ':'); + if (accf == NULL) + return; + if (accf != strrchr(arg, ':') || *(accf + 1) == '\0') { + /* more than one || nothing beyond */ + sep->se_socktype = -1; + return; + } + + accf++; /* skip delimiter */ + strlcpy(sep->se_accf.af_name, accf, sizeof(sep->se_accf.af_name)); + accf_arg = strchr(accf, ','); + if (accf_arg == NULL) /* zero or one arg, no more */ + return; + + if (strrchr(accf, ',') != accf_arg) { + sep->se_socktype = -1; + } else { + accf_arg++; + strlcpy(sep->se_accf.af_arg, accf_arg, + sizeof(sep->se_accf.af_arg)); + } +} + +void +parse_socktype(char* arg, struct servtab* sep) +{ + /* stream socket may have an accept filter, only check first chars */ + if (strncmp(arg, "stream", sizeof("stream") - 1) == 0) + sep->se_socktype = SOCK_STREAM; + else if (strcmp(arg, "dgram") == 0) + sep->se_socktype = SOCK_DGRAM; + else if (strcmp(arg, "rdm") == 0) + sep->se_socktype = SOCK_RDM; + else if (strcmp(arg, "seqpacket") == 0) + sep->se_socktype = SOCK_SEQPACKET; + else if (strcmp(arg, "raw") == 0) + sep->se_socktype = SOCK_RAW; + else + sep->se_socktype = -1; +} + +static struct servtab +init_servtab(void) +{ + /* This does not set every field to default. See enter() as well */ + return (struct servtab) { + /* + * Set se_max to non-zero so uninitialized value is not + * a valid value. Useful in v2 syntax parsing. + */ + .se_service_max = SERVTAB_UNSPEC_SIZE_T, + .se_ip_max = SERVTAB_UNSPEC_SIZE_T, + .se_wait = SERVTAB_UNSPEC_VAL, + .se_socktype = SERVTAB_UNSPEC_VAL, + .se_rl_ip_list = SLIST_HEAD_INITIALIZER(se_ip_list_head) + /* All other fields initialized to 0 or null */ + }; +} + +/* Include directives bookkeeping structure */ +struct file_list { + /* Absolute path used for checking for circular references */ + char *abs; + /* Pointer to the absolute path of the parent config file, + * on the stack */ + struct file_list *next; +} *file_list_head; + +static void +include_configs(char *pattern) +{ + /* Allocate global per-config state on the thread stack */ + const char* save_CONFIG; + FILE *save_fconfig; + size_t save_line_number; + char *save_defhost; + struct file_list new_file; +#ifdef IPSEC + char *save_policy; +#endif + + /* Store current globals on the stack */ + save_CONFIG = CONFIG; + save_fconfig = fconfig; + save_line_number = line_number; + save_defhost = defhost; + new_file.abs = realpath(CONFIG, NULL); + new_file.next = file_list_head; +#ifdef IPSEC + save_policy = policy; +#endif + /* Put new_file at the top of the config stack */ + file_list_head = &new_file; + read_glob_configs(pattern); + free(new_file.abs); + /* Pop new_file off the stack */ + file_list_head = new_file.next; + + /* Restore global per-config state */ + CONFIG = save_CONFIG; + fconfig = save_fconfig; + line_number = save_line_number; + defhost = save_defhost; +#ifdef IPSEC + policy = save_policy; +#endif +} + +static void +prepare_next_config(const char *file_name) +{ + /* Setup new state that is normally only done in main */ + CONFIG = file_name; + + /* Inherit default host and IPsec policy */ + defhost = newstr(defhost); + +#ifdef IPSEC + policy = (policy == NULL) ? NULL : newstr(policy); +#endif +} + +static void +read_glob_configs(char *pattern) +{ + glob_t results; + char *full_pattern; + int glob_result; + full_pattern = gen_file_pattern(CONFIG, pattern); + + DPRINTCONF("Found include directive '%s'", full_pattern); + + glob_result = glob(full_pattern, GLOB_NOSORT, glob_error, &results); + switch(glob_result) { + case 0: + /* No glob errors */ + break; + case GLOB_ABORTED: + ERR("Error while searching for include files"); + break; + case GLOB_NOMATCH: + /* It's fine if no files were matched. */ + DPRINTCONF("No files matched pattern '%s'", full_pattern); + break; + case GLOB_NOSPACE: + ERR("Error when searching for include files: %s", + strerror(errno)); + break; + default: + ERR("Unknown glob(3) error %d", errno); + break; + } + free(full_pattern); + + for (size_t i = 0; i < results.gl_pathc; i++) { + include_matched_path(results.gl_pathv[i]); + } + + globfree(&results); +} + +static void +include_matched_path(char *glob_path) +{ + struct stat sb; + char *tmp; + + if (lstat(glob_path, &sb) != 0) { + ERR("Error calling stat on path '%s': %s", glob_path, + strerror(errno)); + return; + } + + if (!S_ISREG(sb.st_mode) && !S_ISLNK(sb.st_mode)) { + DPRINTCONF("'%s' is not a file.", glob_path); + ERR("The matched path '%s' is not a regular file", glob_path); + return; + } + + DPRINTCONF("Include '%s'", glob_path); + + if (S_ISLNK(sb.st_mode)) { + tmp = glob_path; + glob_path = realpath(tmp, NULL); + } + + /* Ensure the file is not being reincluded .*/ + if (check_no_reinclude(glob_path)) { + prepare_next_config(glob_path); + config(); + } else { + DPRINTCONF("File '%s' already included in current include " + "chain", glob_path); + WRN("Including file '%s' would cause a circular " + "dependency", glob_path); + } + + if (S_ISLNK(sb.st_mode)) { + free(glob_path); + glob_path = tmp; + } +} + +static bool +check_no_reinclude(const char *glob_path) +{ + struct file_list *cur = file_list_head; + char *abs_path = realpath(glob_path, NULL); + + if (abs_path == NULL) { + ERR("Error checking real path for '%s': %s", + glob_path, strerror(errno)); + return false; + } + + DPRINTCONF("Absolute path '%s'", abs_path); + + for (cur = file_list_head; cur != NULL; cur = cur->next) { + if (strcmp(cur->abs, abs_path) == 0) { + /* file included more than once */ + /* TODO relative or abs path for logging error? */ + free(abs_path); + return false; + } + } + free(abs_path); + return true; +} + +/* Resolve the pattern relative to the config file the pattern is from */ +static char * +gen_file_pattern(const char *cur_config, const char *pattern) +{ + if (pattern[0] == '/') { + /* Absolute paths don't need any normalization */ + return newstr(pattern); + } + + /* pattern is relative */ + /* Find the end of the file's directory */ + size_t i, last = 0; + for (i = 0; cur_config[i] != '\0'; i++) { + if (cur_config[i] == '/') { + last = i; + } + } + + if (last == 0) { + /* cur_config is just a filename, pattern already correct */ + return newstr(pattern); + } + + /* Relativize pattern to cur_config file's directory */ + char *full_pattern = malloc(last + 1 + strlen(pattern) + 1); + if (full_pattern == NULL) { + syslog(LOG_ERR, "Out of memory."); + exit(EXIT_FAILURE); + } + memcpy(full_pattern, cur_config, last); + full_pattern[last] = '/'; + strcpy(&full_pattern[last + 1], pattern); + return full_pattern; +} + +static int +glob_error(const char *path, int error) +{ + WRN("Error while resolving path '%s': %s", path, strerror(error)); + return 0; +} Index: src/usr.sbin/inetd/ratelimit.c diff -u /dev/null src/usr.sbin/inetd/ratelimit.c:1.1 --- /dev/null Tue Oct 12 15:08:04 2021 +++ src/usr.sbin/inetd/ratelimit.c Tue Oct 12 15:08:04 2021 @@ -0,0 +1,555 @@ +/* $NetBSD: ratelimit.c,v 1.1 2021/10/12 19:08:04 christos Exp $ */ + +/*- + * Copyright (c) 2021 The NetBSD Foundation, Inc. + * All rights reserved. + * + * This code is derived from software contributed to The NetBSD Foundation + * by James Browning, Gabe Coffland, Alex Gavin, and Solomon Ritzow. + * + * 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 NETBSD FOUNDATION, INC. 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 FOUNDATION 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. + */ +#include <sys/cdefs.h> +__RCSID("$NetBSD: ratelimit.c,v 1.1 2021/10/12 19:08:04 christos Exp $"); + +#include <sys/queue.h> + +#include <arpa/inet.h> + +#include <stdio.h> +#include <stdlib.h> +#include <syslog.h> +#include <unistd.h> +#include <string.h> +#include <errno.h> +#include <stddef.h> + +#include "inetd.h" + +union addr { + struct in_addr ipv4_addr; + /* ensure aligned for comparison in rl_ipv6_eq (already is on 64-bit) */ +#ifdef INET6 + struct in6_addr ipv6_addr __attribute__((aligned(16))); +#endif + char other_addr[NI_MAXHOST]; +}; + +static void rl_reset(struct servtab *, time_t); +static time_t rl_time(void); +static void rl_get_name(struct servtab *, int, union addr *); +static void rl_drop_connection(struct servtab *, int); +static struct rl_ip_node *rl_add(struct servtab *, union addr *); +static struct rl_ip_node *rl_try_get_ip(struct servtab *, union addr *); +static bool rl_ip_eq(struct servtab *, union addr *, struct rl_ip_node *); +#ifdef INET6 +static bool rl_ipv6_eq(struct in6_addr *, struct in6_addr *); +#endif +#ifdef DEBUG_ENABLE +static void rl_print_found_node(struct servtab *, struct rl_ip_node *); +#endif +static void rl_log_address_exceed(struct servtab *, struct rl_ip_node *); +static const char *rl_node_tostring(struct servtab *, struct rl_ip_node *, char[NI_MAXHOST]); +static bool rl_process_service_max(struct servtab *, int, time_t *); +static bool rl_process_ip_max(struct servtab *, int, time_t *); + +/* Return 0 on allow, -1 if connection should be blocked */ +int +rl_process(struct servtab *sep, int ctrl) +{ + time_t now = -1; + + DPRINTF(SERV_FMT ": processing rate-limiting", + SERV_PARAMS(sep)); + DPRINTF(SERV_FMT ": se_service_max " + "%zu and se_count %zu", SERV_PARAMS(sep), + sep->se_service_max, sep->se_count); + + if (sep->se_count == 0) { + now = rl_time(); + sep->se_time = now; + } + + if(!rl_process_service_max(sep, ctrl, &now) + || !rl_process_ip_max(sep, ctrl, &now)) { + return -1; + } + + DPRINTF(SERV_FMT ": running service ", SERV_PARAMS(sep)); + + /* se_count is only incremented if rl_process will return 0 */ + sep->se_count++; + return 0; +} + +/* + * Get the identifier for the remote peer based on sep->se_socktype and + * sep->se_family + */ +static void +rl_get_name(struct servtab *sep, int ctrl, union addr *out) +{ + union { + struct sockaddr_storage ss; + struct sockaddr sa; + struct sockaddr_in sin; + struct sockaddr_in6 sin6; + } addr; + + /* Get the sockaddr of socket ctrl */ + switch (sep->se_socktype) { + case SOCK_STREAM: { + socklen_t len = sizeof(struct sockaddr_storage); + if (getpeername(ctrl, &addr.sa, &len) == -1) { + /* error, log it and skip ip rate limiting */ + syslog(LOG_ERR, + SERV_FMT " failed to get peer name of the " + "connection", SERV_PARAMS(sep)); + exit(EXIT_FAILURE); + } + break; + } + case SOCK_DGRAM: { + struct msghdr header = { + .msg_name = &addr.sa, + .msg_namelen = sizeof(struct sockaddr_storage), + /* scatter/gather and control info is null */ + }; + ssize_t count; + + /* Peek so service can still get the packet */ + count = recvmsg(ctrl, &header, MSG_PEEK); + if (count == -1) { + syslog(LOG_ERR, + "failed to get dgram source address: %s; exiting", + strerror(errno)); + exit(EXIT_FAILURE); + } + break; + } + default: + DPRINTF(SERV_FMT ": ip_max rate limiting not supported for " + "socktype", SERV_PARAMS(sep)); + syslog(LOG_ERR, SERV_FMT + ": ip_max rate limiting not supported for socktype", + SERV_PARAMS(sep)); + exit(EXIT_FAILURE); + } + + /* Convert addr to to rate limiting address */ + switch (sep->se_family) { + case AF_INET: + out->ipv4_addr = addr.sin.sin_addr; + break; +#ifdef INET6 + case AF_INET6: + out->ipv6_addr = addr.sin6.sin6_addr; + break; +#endif + default: { + int res = getnameinfo(&addr.sa, + (socklen_t)addr.sa.sa_len, + out->other_addr, NI_MAXHOST, + NULL, 0, + NI_NUMERICHOST + ); + if (res != 0) { + syslog(LOG_ERR, + SERV_FMT ": failed to get name info of " + "the incoming connection: %s; exiting", + SERV_PARAMS(sep), gai_strerror(res)); + exit(EXIT_FAILURE); + } + break; + } + } +} + +static void +rl_drop_connection(struct servtab *sep, int ctrl) +{ + + if (sep->se_wait == 0 && sep->se_socktype == SOCK_STREAM) { + /* + * If the fd isn't a listen socket, + * close the individual connection too. + */ + close(ctrl); + return; + } + if (sep->se_socktype != SOCK_DGRAM) { + return; + } + /* + * Drop the single datagram the service would have + * consumed if nowait. If this is a wait service, this + * will consume 1 datagram, and further received packets + * will be removed in the same way. + */ + struct msghdr header = { + /* All fields null, just consume one message */ + }; + ssize_t count; + + count = recvmsg(ctrl, &header, 0); + if (count == -1) { + syslog(LOG_ERR, + SERV_FMT ": failed to consume nowait dgram: %s", + SERV_PARAMS(sep), strerror(errno)); + exit(EXIT_FAILURE); + } + DPRINTF(SERV_FMT ": dropped dgram message", + SERV_PARAMS(sep)); +} + +static time_t +rl_time(void) +{ + struct timespec ts; + if(clock_gettime(CLOCK_MONOTONIC, &ts) == -1) { + syslog(LOG_ERR, "clock_gettime for rate limiting failed: %s; " + "exiting", strerror(errno)); + /* Exit inetd if rate limiting fails */ + exit(EXIT_FAILURE); + } + return ts.tv_sec; +} + +/* Add addr to IP tracking or return NULL if malloc fails */ +static struct rl_ip_node * +rl_add(struct servtab *sep, union addr *addr) +{ + + struct rl_ip_node *node; + size_t node_size, bufsize; +#ifdef DEBUG_ENABLE + char buffer[NI_MAXHOST]; +#endif + + switch(sep->se_family) { + case AF_INET: + /* ip_node to end of IPv4 address */ + node_size = offsetof(struct rl_ip_node, ipv4_addr) + + sizeof(struct in_addr); + break; + case AF_INET6: + /* ip_node to end of IPv6 address */ + node_size = offsetof(struct rl_ip_node, ipv6_addr) + + sizeof(struct in6_addr); + break; + default: + /* ip_node to other_addr plus size of string + NULL */ + bufsize = strlen(addr->other_addr) + sizeof(char); + node_size = offsetof(struct rl_ip_node, other_addr) + bufsize; + break; + } + + node = malloc(node_size); + if (node == NULL) { + if(errno == ENOMEM) { + return NULL; + } else { + syslog(LOG_ERR, "malloc failed unexpectedly: %s", + strerror(errno)); + exit(EXIT_FAILURE); + } + } + + node->count = 0; + + /* copy the data into the new allocation */ + switch(sep->se_family) { + case AF_INET: + node->ipv4_addr = addr->ipv4_addr; + break; + case AF_INET6: + /* Hopefully this is inlined, means the same thing as memcpy */ + __builtin_memcpy(&node->ipv6_addr, &addr->ipv6_addr, + sizeof(struct in6_addr)); + break; + default: + strlcpy(node->other_addr, addr->other_addr, bufsize); + break; + } + + /* initializes 'entries' member to NULL automatically */ + SLIST_INSERT_HEAD(&sep->se_rl_ip_list, node, entries); + + DPRINTF(SERV_FMT ": add '%s' to rate limit tracking (%zu byte record)", + SERV_PARAMS(sep), rl_node_tostring(sep, node, buffer), node_size); + + return node; +} + +static void +rl_reset(struct servtab *sep, time_t now) +{ + DPRINTF(SERV_FMT ": %ji seconds passed; resetting rate limiting ", + SERV_PARAMS(sep), (intmax_t)(now - sep->se_time)); + + sep->se_count = 0; + sep->se_time = now; + if (sep->se_ip_max != SERVTAB_UNSPEC_SIZE_T) { + rl_clear_ip_list(sep); + } +} + +void +rl_clear_ip_list(struct servtab *sep) +{ + while (!SLIST_EMPTY(&sep->se_rl_ip_list)) { + struct rl_ip_node *node = SLIST_FIRST(&sep->se_rl_ip_list); + SLIST_REMOVE_HEAD(&sep->se_rl_ip_list, entries); + free(node); + } +} + +/* Get the node associated with addr, or NULL */ +static struct rl_ip_node * +rl_try_get_ip(struct servtab *sep, union addr *addr) +{ + + struct rl_ip_node *cur; + SLIST_FOREACH(cur, &sep->se_rl_ip_list, entries) { + if (rl_ip_eq(sep, addr, cur)) { + return cur; + } + } + + return NULL; +} + +/* Return true if passed service rate limiting checks, false if blocked */ +static bool +rl_process_service_max(struct servtab *sep, int ctrl, time_t *now) +{ + if (sep->se_count >= sep->se_service_max) { + if(*now == -1) { + /* Only get the clock time if we didn't already */ + *now = rl_time(); + } + + if (*now - sep->se_time > CNT_INTVL) { + rl_reset(sep, *now); + } else { + syslog(LOG_ERR, SERV_FMT + ": max spawn rate (%zu in %ji seconds) " + "already met; closing for %ju seconds", + SERV_PARAMS(sep), + sep->se_service_max, + (intmax_t)CNT_INTVL, + (uintmax_t)RETRYTIME); + DPRINTF(SERV_FMT + ": max spawn rate (%zu in %ji seconds) " + "already met; closing for %ju seconds", + SERV_PARAMS(sep), + sep->se_service_max, + (intmax_t)CNT_INTVL, + (uintmax_t)RETRYTIME); + + rl_drop_connection(sep, ctrl); + + /* Close the server for 10 minutes */ + close_sep(sep); + if (!timingout) { + timingout = true; + alarm(RETRYTIME); + } + + return false; + } + } + return true; +} + +/* Return true if passed IP rate limiting checks, false if blocked */ +static bool +rl_process_ip_max(struct servtab *sep, int ctrl, time_t *now) { + if (sep->se_ip_max != SERVTAB_UNSPEC_SIZE_T) { + struct rl_ip_node *node; + union addr addr; + + rl_get_name(sep, ctrl, &addr); + node = rl_try_get_ip(sep, &addr); + if (node == NULL) { + node = rl_add(sep, &addr); + if (node == NULL) { + /* If rl_add can't allocate, reject request */ + DPRINTF("Cannot allocate rl_ip_node"); + return false; + } + } +#ifdef DEBUG_ENABLE + else { + /* + * in a separate function to prevent large stack + * frame + */ + rl_print_found_node(sep, node); + } +#endif + + DPRINTF( + SERV_FMT ": se_ip_max %zu and ip_count %zu", + SERV_PARAMS(sep), sep->se_ip_max, node->count); + + if (node->count >= sep->se_ip_max) { + if (*now == -1) { + *now = rl_time(); + } + + if (*now - sep->se_time > CNT_INTVL) { + rl_reset(sep, *now); + node = rl_add(sep, &addr); + if (node == NULL) { + DPRINTF("Cannot allocate rl_ip_node"); + return false; + } + } else { + if (debug && node->count == sep->se_ip_max) { + /* + * Only log first failed request to + * prevent DoS attack writing to system + * log + */ + rl_log_address_exceed(sep, node); + } else { + DPRINTF(SERV_FMT + ": service not started", + SERV_PARAMS(sep)); + } + + rl_drop_connection(sep, ctrl); + /* + * Increment so debug-syslog message will + * trigger only once + */ + if (node->count < SIZE_MAX) { + node->count++; + } + return false; + } + } + node->count++; + } + return true; +} + +static bool +rl_ip_eq(struct servtab *sep, union addr *addr, struct rl_ip_node *cur) { + switch(sep->se_family) { + case AF_INET: + if (addr->ipv4_addr.s_addr == cur->ipv4_addr.s_addr) { + return true; + } + break; +#ifdef INET6 + case AF_INET6: + if(rl_ipv6_eq(&addr->ipv6_addr, &cur->ipv6_addr)) { + return true; + } + break; +#endif + default: + if (strncmp(cur->other_addr, addr->other_addr, NI_MAXHOST) + == 0) { + return true; + } + break; + } + return false; +} + +#ifdef INET6 +static bool +rl_ipv6_eq(struct in6_addr *a, struct in6_addr *b) +{ +#if UINTMAX_MAX >= UINT64_MAX + { /* requires 8 byte aligned structs */ + uint64_t *ap = (uint64_t *)a->s6_addr; + uint64_t *bp = (uint64_t *)b->s6_addr; + return (ap[0] == bp[0]) & (ap[1] == bp[1]); + } +#else + { /* requires 4 byte aligned structs */ + uint32_t *ap = (uint32_t *)a->s6_addr; + uint32_t *bp = (uint32_t *)b->s6_addr; + return ap[0] == bp[0] && ap[1] == bp[1] && + ap[2] == bp[2] && ap[3] == bp[3]; + } +#endif +} +#endif + +static const char * +rl_node_tostring(struct servtab *sep, struct rl_ip_node *node, + char buffer[NI_MAXHOST]) +{ + switch (sep->se_family) { + case AF_INET: +#ifdef INET6 + case AF_INET6: +#endif + /* ipv4_addr/ipv6_addr share same address */ + return inet_ntop(sep->se_family, (void*)&node->ipv4_addr, + (char*)buffer, NI_MAXHOST); + default: + return (char *)&node->other_addr; + } +} + +#ifdef DEBUG_ENABLE +/* Separate function due to large buffer size */ +static void +rl_print_found_node(struct servtab *sep, struct rl_ip_node *node) +{ + char buffer[NI_MAXHOST]; + DPRINTF(SERV_FMT ": found record for address '%s'", + SERV_PARAMS(sep), rl_node_tostring(sep, node, buffer)); +} +#endif + +/* Separate function due to large buffer sie */ +static void +rl_log_address_exceed(struct servtab *sep, struct rl_ip_node *node) +{ + char buffer[NI_MAXHOST]; + const char * name = rl_node_tostring(sep, node, buffer); + syslog(LOG_ERR, SERV_FMT + ": max ip spawn rate (%zu in " + "%ji seconds) for " + "'%." TOSTRING(NI_MAXHOST) "s' " + "already met; service not started", + SERV_PARAMS(sep), + sep->se_ip_max, + (intmax_t)CNT_INTVL, + name); + DPRINTF(SERV_FMT + ": max ip spawn rate (%zu in " + "%ji seconds) for " + "'%." TOSTRING(NI_MAXHOST) "s' " + "already met; service not started", + SERV_PARAMS(sep), + sep->se_ip_max, + (intmax_t)CNT_INTVL, + name); +}