Module Name: src Committed By: christos Date: Sun Aug 29 09:54:18 UTC 2021
Modified Files: src/distrib/sets/lists/debug: mi src/distrib/sets/lists/tests: mi src/etc/mtree: NetBSD.dist.tests src/tests/usr.sbin: Makefile src/usr.sbin/inetd: Makefile inetd.8 inetd.c Added Files: src/tests/usr.sbin/inetd: Makefile inetd_ratelimit.conf t_inetd.c test_server.c src/usr.sbin/inetd: inetd.h parse_v2.c Log Message: Inetd enhancements by James Browning, Gabe Coffland, Alex Gavin, Solomon Ritzow Described in: https://www.mail-archive.com/tech-userlevel@netbsd.org/msg03114.html And developed in: https://github.com/ritzow/src/pull/1 >From their notes: All new functionality should be explained by the updated manpage. The manpage has been refactored a bit: A new section "Directives" has been added and the information about default hostnames and IPsec directives has been moved there, and the new file include directive information is also there. getconfigent has the most major changes. A newline is no longer read immediately, but is called only by a "goto more" (inside an if(false) block). This allows multiple definitions or directives to exist on a single line for anything that doesn't terminate using a newline. This means a key-values service definition can be followed by another key-values service definition, a positional definition, or an ipsec, hostname, or .include directive on the same line. memset is no longer used explicitly to clear the servtab structure, a function init_servtab() is used instead, which uses a C struct initializer. The servtab se_group field is its own allocation now, and not just a pointer into the user:group string. Refactored some stuff out of getconfigent to separate functions for use by parse_v2.c. These functions in inetd.c are named with the form parse_*() parse_v2.c only has code for parsing a key-values service definition into a provided servtab. It should not have anything that affects global state other than line and line_number. Some function prototypes, structures, and #defines have been moved from inetd.c to inetd.h. The function config_root replaces config as the function called on a config file load/reload. The code removed from the end of config(void) is now called in config_root, so it is not run on each recursive config call. setconfig(void) was removed and its code added into config_root because that is the only place it is called, and redundant checks for non-null globals were removed because they are always freed by endconfig. The fseek code was also removed because the config files are always closed by endconfig. Rate limiting code was updated to add a per-service per-IP rate limiting form. Some of that code was refactored out of other places into functions with names in the form rl_*() We have not added any of the license or version information to the new files parse_v2.c, parse_v2.h, and inetd.h and we have not updated the license or version info for inetd.c. Security related: The behavior when reading invalid IPsec strings has changed. Inetd no longer exits, it quits reading the current config file instead. Could this impact program security? We have not checked for memory leaks. Solomon tried to use dmalloc without success. getconfigent seemed to have a memory leak at each "goto more". It seems like inetd has never free'd allocated strings when throwing away erroneous service definitions during parsing (i.e. when "goto more" is called when parsing fields). OpenBSD's version calls freeconfig on "goto more" (https://github.com/openbsd/src/blob/c5eae130d6c937080c3d30d124e8c8b86db7d625/usr.sbin/inetd/inetd.c#L1049) but NetBSD only calls it when service definitions are no longer needed. This has been fixed. freeconfig is called immediately before any "goto more". There shouldn't be any time when a servtab is in an invalid state where freeconfig would break. To generate a diff of this commit: cvs rdiff -u -r1.359 -r1.360 src/distrib/sets/lists/debug/mi cvs rdiff -u -r1.1118 -r1.1119 src/distrib/sets/lists/tests/mi cvs rdiff -u -r1.187 -r1.188 src/etc/mtree/NetBSD.dist.tests cvs rdiff -u -r1.6 -r1.7 src/tests/usr.sbin/Makefile cvs rdiff -u -r0 -r1.1 src/tests/usr.sbin/inetd/Makefile \ src/tests/usr.sbin/inetd/inetd_ratelimit.conf \ src/tests/usr.sbin/inetd/t_inetd.c src/tests/usr.sbin/inetd/test_server.c cvs rdiff -u -r1.24 -r1.25 src/usr.sbin/inetd/Makefile cvs rdiff -u -r1.61 -r1.62 src/usr.sbin/inetd/inetd.8 cvs rdiff -u -r1.126 -r1.127 src/usr.sbin/inetd/inetd.c cvs rdiff -u -r0 -r1.1 src/usr.sbin/inetd/inetd.h \ 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/distrib/sets/lists/debug/mi diff -u src/distrib/sets/lists/debug/mi:1.359 src/distrib/sets/lists/debug/mi:1.360 --- src/distrib/sets/lists/debug/mi:1.359 Thu Aug 12 09:27:42 2021 +++ src/distrib/sets/lists/debug/mi Sun Aug 29 05:54:18 2021 @@ -1,4 +1,4 @@ -# $NetBSD: mi,v 1.359 2021/08/12 13:27:42 martin Exp $ +# $NetBSD: mi,v 1.360 2021/08/29 09:54:18 christos Exp $ ./etc/mtree/set.debug comp-sys-root ./usr/lib comp-sys-usr compatdir ./usr/lib/i18n/libBIG5_g.a comp-c-debuglib debuglib,compatfile @@ -2489,6 +2489,8 @@ ./usr/libdata/debug/usr/tests/usr.bin/id/h_id.debug tests-usr.bin-debug debug,atf,compattestfile ./usr/libdata/debug/usr/tests/usr.bin/mkdep/h_findcc.debug tests-usr.bin-debug debug,atf,compattestfile ./usr/libdata/debug/usr/tests/usr.bin/tar/h_tar.debug tests-usr.bin-debug debug,atf,compattestfile +./usr/libdata/debug/usr/tests/usr.sbin/inetd/t_inetd.debug tests-usr.sbin-debug debug,atf,compattestfile +./usr/libdata/debug/usr/tests/usr.sbin/inetd/test_server.debug tests-usr.sbin-debug debug,atf,compattestfile ./usr/libdata/debug/usr/tests/util/df/h_df.debug tests-obsolete obsolete,compattestfile ./usr/libdata/debug/usr/tests/util/id/h_id.debug tests-obsolete obsolete,compattestfile ./usr/libdata/debug/usr/tests/util/systrace/h_have_systrace.debug tests-obsolete obsolete,compattestfile Index: src/distrib/sets/lists/tests/mi diff -u src/distrib/sets/lists/tests/mi:1.1118 src/distrib/sets/lists/tests/mi:1.1119 --- src/distrib/sets/lists/tests/mi:1.1118 Sat Aug 28 15:45:18 2021 +++ src/distrib/sets/lists/tests/mi Sun Aug 29 05:54:18 2021 @@ -1,4 +1,4 @@ -# $NetBSD: mi,v 1.1118 2021/08/28 19:45:18 rillig Exp $ +# $NetBSD: mi,v 1.1119 2021/08/29 09:54:18 christos Exp $ # # Note: don't delete entries from here - mark them as "obsolete" instead. # @@ -201,6 +201,7 @@ ./usr/libdata/debug/usr/tests/usr.bin/netpgpverify tests-usr.bin-debug compattestfile,atf ./usr/libdata/debug/usr/tests/usr.bin/tar tests-usr.bin-debug compattestfile,atf ./usr/libdata/debug/usr/tests/usr.sbin tests-usr.sbin-debug compattestfile,atf +./usr/libdata/debug/usr/tests/usr.sbin/inetd tests-usr.sbin-debug compattestfile,atf ./usr/libdata/debug/usr/tests/util tests-obsolete obsolete ./usr/libdata/debug/usr/tests/util/df tests-obsolete obsolete ./usr/libdata/debug/usr/tests/util/id tests-obsolete obsolete @@ -7107,6 +7108,12 @@ ./usr/tests/usr.sbin/execsnoop/Atffile tests-usr.sbin-tests compattestfile,atf ./usr/tests/usr.sbin/execsnoop/Kyuafile tests-usr.sbin-tests compattestfile,atf,kyua ./usr/tests/usr.sbin/execsnoop/t_execsnoop tests-usr.sbin-tests compattestfile,atf +./usr/tests/usr.sbin/inetd tests-usr.sbin-tests compattestfile,atf +./usr/tests/usr.sbin/inetd/Atffile tests-usr.sbin-tests compattestfile,atf +./usr/tests/usr.sbin/inetd/Kyuafile tests-usr.sbin-tests compattestfile,atf,kyua +./usr/tests/usr.sbin/inetd/inetd_ratelimit.conf tests-usr.sbin-tests compattestfile,atf +./usr/tests/usr.sbin/inetd/t_inetd tests-usr.sbin-tests compattestfile,atf +./usr/tests/usr.sbin/inetd/test_server tests-usr.sbin-tests compattestfile,atf ./usr/tests/usr.sbin/mtree tests-usr.sbin-tests compattestfile,atf ./usr/tests/usr.sbin/mtree/Atffile tests-usr.sbin-tests compattestfile,atf ./usr/tests/usr.sbin/mtree/Kyuafile tests-usr.sbin-tests compattestfile,atf,kyua Index: src/etc/mtree/NetBSD.dist.tests diff -u src/etc/mtree/NetBSD.dist.tests:1.187 src/etc/mtree/NetBSD.dist.tests:1.188 --- src/etc/mtree/NetBSD.dist.tests:1.187 Thu Aug 12 07:50:42 2021 +++ src/etc/mtree/NetBSD.dist.tests Sun Aug 29 05:54:18 2021 @@ -1,4 +1,4 @@ -# $NetBSD: NetBSD.dist.tests,v 1.187 2021/08/12 11:50:42 martin Exp $ +# $NetBSD: NetBSD.dist.tests,v 1.188 2021/08/29 09:54:18 christos Exp $ ./usr/libdata/debug/usr/tests ./usr/libdata/debug/usr/tests/atf @@ -180,6 +180,7 @@ ./usr/libdata/debug/usr/tests/usr.bin/mkdep ./usr/libdata/debug/usr/tests/usr.bin/netpgpverify ./usr/libdata/debug/usr/tests/usr.sbin +./usr/libdata/debug/usr/tests/usr.sbin/inetd ./usr/tests ./usr/tests/atf ./usr/tests/atf/atf-c @@ -468,6 +469,7 @@ ./usr/tests/usr.sbin ./usr/tests/usr.sbin/cpuctl ./usr/tests/usr.sbin/execsnoop +./usr/tests/usr.sbin/inetd ./usr/tests/usr.sbin/mtree ./usr/tests/usr.sbin/opensnoop ./usr/tests/usr.sbin/stdethers Index: src/tests/usr.sbin/Makefile diff -u src/tests/usr.sbin/Makefile:1.6 src/tests/usr.sbin/Makefile:1.7 --- src/tests/usr.sbin/Makefile:1.6 Tue Jun 30 10:30:49 2020 +++ src/tests/usr.sbin/Makefile Sun Aug 29 05:54:18 2021 @@ -1,10 +1,11 @@ -# $NetBSD: Makefile,v 1.6 2020/06/30 14:30:49 jruoho Exp $ +# $NetBSD: Makefile,v 1.7 2021/08/29 09:54:18 christos Exp $ .include <bsd.own.mk> TESTSDIR= ${TESTSBASE}/usr.sbin TESTS_SUBDIRS+= cpuctl TESTS_SUBDIRS+= execsnoop +TESTS_SUBDIRS+= inetd TESTS_SUBDIRS+= mtree TESTS_SUBDIRS+= opensnoop TESTS_SUBDIRS+= stdethers Index: src/usr.sbin/inetd/Makefile diff -u src/usr.sbin/inetd/Makefile:1.24 src/usr.sbin/inetd/Makefile:1.25 --- src/usr.sbin/inetd/Makefile:1.24 Sun Mar 7 10:09:12 2021 +++ src/usr.sbin/inetd/Makefile Sun Aug 29 05:54:18 2021 @@ -1,15 +1,18 @@ # from: @(#)Makefile 8.1 (Berkeley) 6/6/93 -# $NetBSD: Makefile,v 1.24 2021/03/07 15:09:12 christos Exp $ +# $NetBSD: Makefile,v 1.25 2021/08/29 09:54:18 christos Exp $ .include <bsd.own.mk> USE_FORT?= yes # network server PROG= inetd -SRCS= inetd.c +SRCS= inetd.c parse_v2.c MAN= inetd.8 MLINKS= inetd.8 inetd.conf.5 +# Enables debug printouts when in debug mode +CPPFLAGS+=-DDEBUG_ENABLE + CPPFLAGS+=-DLIBWRAP # Use LIBWRAP_INTERNAL for libwrap checking of inetd's `internal' services. #CPPFLAGS+=-DLIBWRAP_INTERNAL Index: src/usr.sbin/inetd/inetd.8 diff -u src/usr.sbin/inetd/inetd.8:1.61 src/usr.sbin/inetd/inetd.8:1.62 --- src/usr.sbin/inetd/inetd.8:1.61 Thu Jul 20 03:42:54 2017 +++ src/usr.sbin/inetd/inetd.8 Sun Aug 29 05:54:18 2021 @@ -1,4 +1,4 @@ -.\" $NetBSD: inetd.8,v 1.61 2017/07/20 07:42:54 wiz Exp $ +.\" $NetBSD: inetd.8,v 1.62 2021/08/29 09:54:18 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 July 19, 2017 +.Dd August 29, 2021 .Dt INETD 8 .Os .Sh NAME @@ -106,12 +106,21 @@ The path given for this configuration fi the .Fl d option is also given on the command line. + +Services can be specified using the legacy `positional' notation or the +`key-values' notation described in the sections "Positional Notation" and +"Key-Values Notation" below. + +.Ss Positional Notation + There must be an entry for each field of the configuration file, with entries for each field separated by a tab or a space. -Comments are denoted by a ``#'' at the beginning of a line. +Comments are denoted by a ``#'' at the beginning of a line (see subsection +"Key-Values Notation" for defining comments in key-values definitions). There must be an entry for each field (except for one -special case, described below). +special case, described below). A positional definition is terminated by a +newline. The fields of the configuration file are as follows: .Pp .Bd -unfilled -offset indent -compact @@ -133,17 +142,10 @@ The single character .Dq \&* means .Dv INADDR_ANY : -all local addresses. -To avoid the need to repeat listen addresses over and over again, -listen addresses are inherited from line to line, and the listen -address can be changed without defining a service by including a line -containing just -a +all local addresses. The .Em listen-addr -followed by a colon. -The default (compatible with historical configuration files) is \&*. -To return to this behavior after configuring some services with -specific listen addresses, give \&* explicitly. +may be a host name, which will be resolved once, when the service +definition is read from the config file. .Pp Note that restricted listen addresses are meaningless and ignored for UNIX-domain services, and are not supported for @@ -192,6 +194,11 @@ For UNIX-domain (local) services, the is the path name to listen on. .Pp The +.Em service-spec +must not begin with a dot. See +.Sx Directives . +.Pp +The .Em socket-type should be one of .Dq stream , @@ -265,9 +272,9 @@ tcp,rcvbuf=64k,sndbuf=1m .Pp A literal value may be specified, or modified using .Sq k -to indicate kilobytes or +to indicate kibibytes or .Sq m -to indicate megabytes. +to indicate mebibytes. Socket buffer sizes may be specified for all services and protocols except for tcpmux services. .Pp @@ -277,8 +284,8 @@ entry is used to tell .Nm if it should wait for the server program to return, or continue processing connections on the socket. -If a datagram server connects -to its peer, freeing the socket so +If a datagram server reads a single datagram and connects +to its peer through a different socket, freeing the service's socket so .Nm can receive further messages on the socket, it is said to be a @@ -382,6 +389,244 @@ word should take the place of this entry. It is possible to quote an argument using either single or double quotes. This allows you to have, e.g., spaces in paths and parameters. + +.Ss Key-Values Notation + +In key-values notation, keys are separated from their associated values by `=', +values are separated by whitespace, and key-values options are separated by +commas. A service definition is terminated by a semicolon. Multiple definitions +may exist on a single line (and a line may end with a positional definition. +A key-values definition has the following form: + +.Bd -filled -offset indent +[listen-addr:]service-spec {on|off} <option> = [value1], +<option> = [value1] [value2] ..., <option> =, ...; +.Ed +.Pp +Values may be in quotes, and support the following escape sequences. +.Bl -hang -width "\xXX" -offset indent +.It Sy \e\e +Backslash. +.It Sy \en +Line feed. +.It Sy \et +Tab. +.It Sy \er +Carriage return. +.It Sy \e' +Single quote. +.It Sy \e" +Double quote. +.It Sy \exXX +Hexadecimal byte value, replace XX. +.El +.Pp +.Em [listen-addr:]service-spec +has the same form as in positional notation. If +.Em service-spec +is followed by +.Em on +then the service definition is active by default. If +.Em service-spec +is followed by +.Em off +then the service definition is parsed and errors are output to +the system log, but the service is not active and no sockets are created. + +Comments that exist between the initial on/off directive +and the closing semicolon may begin in any column and may exist on the same line +as non-comment text. Note: editor syntax highlighting may be misleading! + +Syntax and semantic error detection is performed on a best-effort basis. If an +error with a service definition is easily detectable, it will log the error +using +.Xr syslog 3 +and continue reading the configuration file if possible, skipping the erroneous +definition or file. +Otherwise, it is up to the user to write definitions that conform to the +documentation. Errors may be worded differently depending on the ordering of +options in the service definition. + +The following are the available values for +.Em <option>: +.Bl -hang -width "acceptfilter" +.It Sy bind +Set the listen address for this service. This can be an IPv4 or IPv6 address +or a hostname. + +.It Sy socktype +Equivalent to +.Em socket-type +in positional notation. +.Em socktype +is optional if +.Em protocol +is specified and is udp{4,6} or tcp{4,6}. + +.It Sy acceptfilter +An accept filter, equivalent to +.Em accept +in positional notation (see +.Xr accept_filter 9 +and SO_ACCEPTFITLER in +.Xr setsockopt 2 +). + +.It Sy protocol +Equivalent to +.Em protocol +in positional notation. +If specified as "tcp" or "udp" with no version specifier, the associated +hostname or +.Em bind +value is used +to determine the IP version. If the version is not specified and the hostname +string or +.Em bind +value is not an IPv4 or IPv6 address, the +service definition is invalid. + +.It Sy sndbuf +Equivalent to +.Em sndbuf +in positional notation. + +.It Sy recvbuf +Equivalent to +.Em recvbuf +in positional notation. + +.It Sy wait +The value +.Em yes +or +.Em no . +Equivalent to +.Em wait/nowait +in positional notation. This option is automatically determined for internal +services, and is mandatory for all others. + +.It Sy service_max +Equivalent to +.Em max +in positional notation. Defaults to 40 if not specified. + +.It Sy ip_max +Specifies the maximum number of server instances that may be spawned from +.Em inetd +within an interval of 60 seconds for a given IP address. + +.It Sy user +The user to run the program as. Equivalent to +.Em user +in positional notation. + +.It Sy group +The primary group to run the program as. Equivalent to +.Em group +in positional notation. + +.It Sy exec +The path to the program's executable or +.Dq internal +for a built-in service. +If not specified, this will be assumed to be +.Dq internal +is assumed (and will fail if +.Em socktype +is not specified). +internal +.It Sy args +The program arguments. By convention, the first argument should be the name of +the program. + +.It Sy ipsec +An IPsec policy string. Defaults to the global default setting. If +specified without a value (i.e. "ipsec=,"), IPsec will be disabled for this +service. See the +.Sx Directives +section for details. Currently only one value is allowed, so all IPsec policies +should be in a quoted string, separated by semicolons. +.El + +.Ss Directives +.\" Newlines needed for spacing, .Pp won't work. + + + +.Pp +<listen-addr>: +.Pp +To avoid the need to repeat listen addresses over and over again, +listen addresses are inherited from line to line, and the listen +address can be changed without defining a service by including a line +containing just +a +.Em listen-addr +followed by a colon. +The default (compatible with historical configuration files) is \&*. +To return to this behavior after configuring some services with +specific listen addresses, give \&* explicitly. +.Pp +.Li "#@" +[<IPsec policy>] [; [<IPsec policy>]] ... +.Pp +The implementation includes a tiny hack to support IPsec policy settings for +each socket. +A special form of the comment line, starting with +.Dq Li "#@" , +is used as a policy specifier. +The content of the above comment line will be treated as a IPsec policy string, +as described in +.Xr ipsec_set_policy 3 . +Multiple IPsec policy strings may be specified by using a semicolon +as a separator. +If conflicting policy strings are found in a single line, +the last string will take effect. IPsec policy strings are not parsed in +comments within a key-values service definition. +A +.Li "#@" +line affects all of the subsequent lines in the same config file, +so you may want to reset the IPsec policy by using a comment line containing +only +.Li "#@" +.Pq with no policy string . +.Pp +If an invalid IPsec policy string appears in a config file, +.Nm +logs an error message using +.Xr syslog 3 +and stops reading the current config file, but may continue reading +from other files not affected by the IPsec directive. +.Pp +\&.include <glob-path> +.Pp +Other files can be read by inetd by specifying an include directive in an inetd +config file. +.Em glob-path +is an +absolute path or a path relative (including parent directories) to the directory +containing the current config +file, and may contain glob patterns as specified by +.Xr glob 7 . +.Pp +To include a specific file, include the relative or absolute path of the file. +To include all files in a directory, +.Em glob-path +should be the directory of the files to include followed by "/*". +.Pp +The listening address and IPsec configuration strings of the current config file +are inherited by files included by this directive. +.Pp +Files included by this directive using a glob path match are not read in a +specific order. If a specific order is desired, files or directories should be +included individually without the use of glob patterns. Behavior is undefined if +multiple include directives include the same file and +this should be avoided. Circular references are caught by inetd. Anything after +.Em glob-path +on the same line is ignored. +.Em glob-path +may be in quotes. .Ss Internal Services .Nm provides several @@ -458,34 +703,6 @@ for access control facility configuratio wrappers do not affect/restrict .Tn UDP or internal services. -.Ss IPsec -The implementation includes a tiny hack to support IPsec policy settings for -each socket. -A special form of the comment line, starting with -.Dq Li "#@" , -is used as a policy specifier. -The content of the above comment line will be treated as a IPsec policy string, -as described in -.Xr ipsec_set_policy 3 . -Multiple IPsec policy strings may be specified by using a semicolon -as a separator. -If conflicting policy strings are found in a single line, -the last string will take effect. -A -.Li "#@" -line affects all of the following lines in -.Pa /etc/inetd.conf , -so you may want to reset the IPsec policy by using a comment line containing -only -.Li "#@" -.Pq with no policy string . -.Pp -If an invalid IPsec policy string appears in -.Pa /etc/inetd.conf , -.Nm -logs an error message using -.Xr syslog 3 -and terminates itself. .Ss IPv6 TCP/UDP behavior If you wish to run a server for both IPv4 and IPv6 traffic, you will need to run two separate processes for the same server program, @@ -496,7 +713,7 @@ using and .Dq tcp6 respectively. -Plain +In positional syntax, plain .Dq tcp means TCP on top of the current default IP version, which is, at this moment, IPv4. @@ -629,8 +846,21 @@ Provided you never have more than one en everything should work correctly (Note that default host address specifiers do apply to RPC lines with no explicit specifier.) .Pp +.Em tcpmux +on IPv6 is not tested enough. +.Pp +For automatic IP version detection in key-values syntax (see the +.Em protocol +key), addresses with an interface specifier in the form <address>%<iface> +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. .Sh SECURITY CONSIDERATIONS Enabling the .Dq echo , Index: src/usr.sbin/inetd/inetd.c diff -u src/usr.sbin/inetd/inetd.c:1.126 src/usr.sbin/inetd/inetd.c:1.127 --- src/usr.sbin/inetd/inetd.c:1.126 Fri Dec 27 04:22:20 2019 +++ src/usr.sbin/inetd/inetd.c Sun Aug 29 05:54:18 2021 @@ -1,4 +1,4 @@ -/* $NetBSD: inetd.c,v 1.126 2019/12/27 09:22:20 msaitoh Exp $ */ +/* $NetBSD: inetd.c,v 1.127 2021/08/29 09:54:18 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.126 2019/12/27 09:22:20 msaitoh Exp $"); +__RCSID("$NetBSD: inetd.c,v 1.127 2021/08/29 09:54:18 christos Exp $"); #endif #endif /* not lint */ @@ -187,12 +187,11 @@ __RCSID("$NetBSD: inetd.c,v 1.126 2019/1 #include <sys/param.h> #include <sys/stat.h> #include <sys/ioctl.h> -#include <sys/socket.h> -#include <sys/un.h> #include <sys/wait.h> -#include <sys/time.h> #include <sys/resource.h> #include <sys/event.h> +#include <sys/socket.h> + #ifndef NO_RPC #define RPC @@ -200,8 +199,6 @@ __RCSID("$NetBSD: inetd.c,v 1.126 2019/1 #include <net/if.h> -#include <netinet/in.h> -#include <arpa/inet.h> #ifdef RPC #include <rpc/rpc.h> #include <rpc/rpcb_clnt.h> @@ -209,10 +206,12 @@ __RCSID("$NetBSD: inetd.c,v 1.126 2019/1 #endif #include <ctype.h> +#include <err.h> #include <errno.h> #include <fcntl.h> +#include <glob.h> #include <grp.h> -#include <netdb.h> +#include <libgen.h> #include <pwd.h> #include <signal.h> #include <stdio.h> @@ -223,15 +222,7 @@ __RCSID("$NetBSD: inetd.c,v 1.126 2019/1 #include <util.h> #include <ifaddrs.h> -#include "pathnames.h" - -#ifdef IPSEC -#include <netipsec/ipsec.h> -#ifndef IPSEC_POLICY_IPSEC /* no ipsec support on old ipsec */ -#undef IPSEC -#endif -#include "ipsec.h" -#endif +#include "inetd.h" #ifdef LIBWRAP # include <tcpd.h> @@ -251,12 +242,9 @@ int allow_severity = LIBWRAP_ALLOW_FACIL int deny_severity = LIBWRAP_DENY_FACILITY|LIBWRAP_DENY_SEVERITY; #endif -#define TOOMANY 40 /* don't start more than TOOMANY */ -#define CNT_INTVL 60 /* servers in CNT_INTVL sec. */ +#define CNT_INTVL ((time_t)60) /* servers in CNT_INTVL sec. */ #define RETRYTIME (60*10) /* retry after bind or server fail */ -#define A_CNT(a) (sizeof (a) / sizeof (a[0])) - int debug; #ifdef LIBWRAP int lflag; @@ -280,56 +268,7 @@ struct rlimit rlim_ofile; struct kevent changebuf[64]; size_t changes; -struct servtab { - char *se_hostaddr; /* host address to listen on */ - char *se_service; /* name of service */ - int se_socktype; /* type of socket to use */ - int se_family; /* address family */ - char *se_proto; /* protocol used */ - int se_sndbuf; /* sndbuf size */ - int se_rcvbuf; /* rcvbuf size */ - int se_rpcprog; /* rpc program number */ - int se_rpcversl; /* rpc program lowest version */ - int se_rpcversh; /* rpc program highest version */ -#define isrpcservice(sep) ((sep)->se_rpcversl != 0) - pid_t se_wait; /* single threaded server */ - short se_checked; /* looked at during merge */ - char *se_user; /* user name to run as */ - char *se_group; /* group name to run as */ - struct biltin *se_bi; /* if built-in, description */ - char *se_server; /* server program */ -#define MAXARGV 64 - char *se_argv[MAXARGV+1]; /* program arguments */ -#ifdef IPSEC - char *se_policy; /* IPsec poilcy string */ -#endif - struct accept_filter_arg se_accf; /* accept filter for stream service */ - int se_fd; /* open descriptor */ - int se_type; /* type */ - union { - struct sockaddr se_un_ctrladdr; - struct sockaddr_in se_un_ctrladdr_in; - struct sockaddr_in6 se_un_ctrladdr_in6; - struct sockaddr_un se_un_ctrladdr_un; - } se_un; /* bound address */ -#define se_ctrladdr se_un.se_un_ctrladdr -#define se_ctrladdr_in se_un.se_un_ctrladdr_in -#define se_ctrladdr_un se_un.se_un_ctrladdr_un - int se_ctrladdr_size; - int se_max; /* max # of instances of this service */ - int se_count; /* number started since se_time */ - struct timeval se_time; /* start of se_count */ - struct servtab *se_next; -} *servtab; - -#define NORM_TYPE 0 -#define MUX_TYPE 1 -#define MUXPLUS_TYPE 2 -#define FAITH_TYPE 3 -#define ISMUX(sep) (((sep)->se_type == MUX_TYPE) || \ - ((sep)->se_type == MUXPLUS_TYPE)) -#define ISMUXPLUS(sep) ((sep)->se_type == MUXPLUS_TYPE) - +struct servtab *servtab; static void chargen_dg(int, struct servtab *); static void chargen_stream(int, struct servtab *); @@ -342,22 +281,19 @@ static void discard_stream(int, struct s 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 void freeconfig(struct servtab *); -static struct servtab *getconfigent(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 *); -static char *newstr(const char *); -static char *nextline(FILE *); +#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 int setconfig(void); static void setup(struct servtab *); -static char *sskip(char **); -static char *skip(char **); +static char *skip(char **); static void tcpmux(int, struct servtab *); __dead static void usage(void); static void register_rpc(struct servtab *); @@ -367,12 +303,30 @@ static void inetd_setproctitle(char *, i static void initring(void); static uint32_t machtime(void); static int port_good_dg(struct sockaddr *); -static int dg_broadcast(struct in_addr *); -static int my_kevent(const struct kevent *, size_t, struct kevent *, - size_t); -static struct kevent * allocchange(void); +static int dg_broadcast(struct in_addr *); +static int my_kevent(const struct kevent *, size_t, struct kevent *, size_t); +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 */ @@ -472,7 +426,7 @@ main(int argc, char *argv[]) rlim_ofile_cur = OPEN_MAX; } - for (n = 0; n < (int)A_CNT(my_signals); n++) { + for (n = 0; n < (int)__arraycount(my_signals); n++) { int signum; signum = my_signals[n]; @@ -495,10 +449,10 @@ main(int argc, char *argv[]) if (reload) { reload = 0; - config(); + config_root(); } - n = my_kevent(changebuf, changes, eventbuf, A_CNT(eventbuf)); + n = my_kevent(changebuf, changes, eventbuf, __arraycount(eventbuf)); changes = 0; for (ev = eventbuf; n > 0; ev++, n--) { @@ -526,20 +480,17 @@ main(int argc, char *argv[]) /* Paranoia */ if ((int)ev->ident != sep->se_fd) continue; - if (debug) - fprintf(stderr, "someone wants %s\n", - sep->se_service); + DPRINTF(SERV_FMT ": service requested" , SERV_PARAMS(sep)); if (!sep->se_wait && sep->se_socktype == SOCK_STREAM) { /* XXX here do the libwrap check-before-accept*/ ctrl = accept(sep->se_fd, NULL, NULL); - if (debug) - fprintf(stderr, "accept, ctrl %d\n", - ctrl); + DPRINTF(SERV_FMT ": accept, ctrl fd %d", + SERV_PARAMS(sep), ctrl); if (ctrl < 0) { if (errno != EINTR) syslog(LOG_WARNING, - "accept (for %s): %m", - sep->se_service); + SERV_FMT ": accept: %m", + SERV_PARAMS(sep)); continue; } } else @@ -562,31 +513,8 @@ spawn(struct servtab *sep, int ctrl) dofork = (sep->se_bi == 0 || sep->se_bi->bi_fork); #endif if (dofork) { - if (sep->se_count++ == 0) - (void)gettimeofday(&sep->se_time, NULL); - else if (sep->se_count >= sep->se_max) { - struct timeval now; - - (void)gettimeofday(&now, NULL); - if (now.tv_sec - sep->se_time.tv_sec > CNT_INTVL) { - sep->se_time = now; - sep->se_count = 1; - } else { - syslog(LOG_ERR, - "%s/%s max spawn rate (%d in %d seconds) " - "exceeded; service not started", - sep->se_service, sep->se_proto, - sep->se_max, CNT_INTVL); - if (!sep->se_wait && sep->se_socktype == - SOCK_STREAM) - close(ctrl); - close_sep(sep); - if (!timingout) { - timingout = 1; - alarm(RETRYTIME); - } - return; - } + if (rl_process(sep, ctrl)) { + return; } pid = fork(); if (pid < 0) { @@ -607,7 +535,7 @@ spawn(struct servtab *sep, int ctrl) if (pid == 0) { size_t n; - for (n = 0; n < A_CNT(my_signals); n++) + for (n = 0; n < __arraycount(my_signals); n++) (void) signal(my_signals[n], SIG_DFL); if (debug) setsid(); @@ -716,9 +644,8 @@ run_service(int ctrl, struct servtab *se } else if (sep->se_group) { (void) setgid((gid_t)grp->gr_gid); } - if (debug) - fprintf(stderr, "%d execl %s\n", - getpid(), sep->se_server); + DPRINTF("%d execl %s", + getpid(), sep->se_server); /* Set our control descriptor to not close-on-exec... */ if (fcntl(ctrl, F_SETFD, 0) < 0) syslog(LOG_ERR, "fcntl (%d, F_SETFD, 0): %m", ctrl); @@ -738,7 +665,7 @@ run_service(int ctrl, struct servtab *se reject: if (sep->se_socktype != SOCK_STREAM) recv(ctrl, buf, sizeof (buf), 0); - _exit(1); + _exit(EXIT_FAILURE); } } @@ -753,9 +680,7 @@ reapchild(void) pid = wait3(&status, WNOHANG, NULL); if (pid <= 0) break; - if (debug) - (void) fprintf(stderr, "%d reaped, status %#x\n", - pid, status); + DPRINTF("%d reaped, status %#x", pid, status); for (sep = servtab; sep != NULL; sep = sep->se_next) if (sep->se_wait == pid) { struct kevent *ev; @@ -772,31 +697,46 @@ reapchild(void) ev = allocchange(); EV_SET(ev, sep->se_fd, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, (intptr_t)sep); - if (debug) - fprintf(stderr, "restored %s, fd %d\n", - sep->se_service, sep->se_fd); + DPRINTF("restored %s, fd %d", + sep->se_service, sep->se_fd); } } } +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, **sepp; + struct servtab *sep, *cp; + /* + * Current position in line, used with key-values notation, + * saves cp across getconfigent calls. + */ + char *current_pos; size_t n; - - if (!setconfig()) { + + /* open config file from beginning */ + fconfig = fopen(CONFIG, "r"); + if(fconfig == NULL) { syslog(LOG_ERR, "%s: %m", CONFIG); return; } - for (sep = servtab; sep != NULL; sep = sep->se_next) - sep->se_checked = 0; - while ((cp = getconfigent()) != NULL) { + + /* 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 (strcmp(sep->se_service, cp->se_service) == 0 && - strcmp(sep->se_hostaddr, cp->se_hostaddr) == 0 && - strcmp(sep->se_proto, cp->se_proto) == 0 && - ISMUX(sep) == ISMUX(cp)) + if (is_same_service(sep, cp)) break; if (sep != NULL) { int i; @@ -821,30 +761,39 @@ config(void) SWAP(char *, sep->se_policy, cp->se_policy); #endif SWAP(int, cp->se_type, sep->se_type); - SWAP(int, cp->se_max, sep->se_max); + 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: address too long", - sep->se_service); + syslog(LOG_ERR, "%s/%s: address too long", + sep->se_service, sep->se_proto); sep->se_checked = 0; continue; } @@ -893,17 +842,17 @@ config(void) if (isrpcservice(sep) || ISMUX(sep)) port = "0"; else - port = sep->se_service; + port = sep->se_service; error = getaddrinfo(host, port, &hints, &res); if (error) { if (error == EAI_SERVICE) { /* gai_strerror not friendly enough */ - syslog(LOG_WARNING, "%s/%s: " + syslog(LOG_WARNING, SERV_FMT ": " "unknown service", - sep->se_service, sep->se_proto); + SERV_PARAMS(sep)); } else { - syslog(LOG_ERR, "%s/%s: %s: %s", - sep->se_service, sep->se_proto, + syslog(LOG_ERR, SERV_FMT ": %s: %s", + SERV_PARAMS(sep), sep->se_hostaddr, gai_strerror(error)); } @@ -912,8 +861,8 @@ config(void) } if (res->ai_next) { syslog(LOG_ERR, - "%s/%s: %s: resolved to multiple addr", - sep->se_service, sep->se_proto, + SERV_FMT ": %s: resolved to multiple addr", + SERV_PARAMS(sep), sep->se_hostaddr); sep->se_checked = 0; freeaddrinfo(res); @@ -937,9 +886,9 @@ config(void) rp = getrpcbyname(sep->se_service); if (rp == 0) { syslog(LOG_ERR, - "%s/%s: unknown service", - sep->se_service, - sep->se_proto); + SERV_FMT + ": unknown service", + SERV_PARAMS(sep)); sep->se_checked = 0; continue; } @@ -961,27 +910,6 @@ config(void) } } endconfig(); - /* - * Purge anything not looked at above. - */ - sepp = &servtab; - while ((sep = *sepp) != NULL) { - if (sep->se_checked) { - sepp = &sep->se_next; - 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); - if (debug) - print_service("FREE", sep); - freeconfig(sep); - free(sep); - } } static void @@ -1015,7 +943,7 @@ goaway(void) for (sep = servtab; sep != NULL; sep = sep->se_next) { if (sep->se_fd == -1) continue; - + switch (sep->se_family) { case AF_LOCAL: (void)unlink(sep->se_service); @@ -1044,17 +972,16 @@ setup(struct servtab *sep) struct kevent *ev; if ((sep->se_fd = socket(sep->se_family, sep->se_socktype, 0)) < 0) { - if (debug) - fprintf(stderr, "socket failed on %s/%s: %s\n", - sep->se_service, sep->se_proto, strerror(errno)); + DPRINTF("socket failed on " SERV_FMT ": %s", + SERV_PARAMS(sep), strerror(errno)); syslog(LOG_ERR, "%s/%s: socket: %m", sep->se_service, sep->se_proto); return; } /* Set all listening sockets to close-on-exec. */ if (fcntl(sep->se_fd, F_SETFD, FD_CLOEXEC) < 0) { - syslog(LOG_ERR, "%s/%s: fcntl(F_SETFD, FD_CLOEXEC): %m", - sep->se_service, sep->se_proto); + syslog(LOG_ERR, SERV_FMT ": fcntl(F_SETFD, FD_CLOEXEC): %m", + SERV_PARAMS(sep)); close(sep->se_fd); sep->se_fd = -1; return; @@ -1092,8 +1019,8 @@ setsockopt(fd, SOL_SOCKET, opt, &on, (so if (sep->se_policy != NULL) { int e = ipsecsetup(sep->se_family, sep->se_fd, sep->se_policy); if (e < 0) { - syslog(LOG_ERR, "%s/%s: ipsec setup failed", - sep->se_service, sep->se_proto); + syslog(LOG_ERR, SERV_FMT ": ipsec setup failed", + SERV_PARAMS(sep)); (void)close(sep->se_fd); sep->se_fd = -1; return; @@ -1101,13 +1028,11 @@ setsockopt(fd, SOL_SOCKET, opt, &on, (so } #endif - if (bind(sep->se_fd, &sep->se_ctrladdr, - (socklen_t)sep->se_ctrladdr_size) < 0) { - if (debug) - fprintf(stderr, "bind failed on %s/%s: %s\n", - sep->se_service, sep->se_proto, strerror(errno)); - syslog(LOG_ERR, "%s/%s: bind: %m", - sep->se_service, sep->se_proto); + if (bind(sep->se_fd, &sep->se_ctrladdr, sep->se_ctrladdr_size) < 0) { + DPRINTF(SERV_FMT ": bind failed: %s", + SERV_PARAMS(sep), strerror(errno)); + syslog(LOG_ERR, SERV_FMT ": bind: %m", + SERV_PARAMS(sep)); (void) close(sep->se_fd); sep->se_fd = -1; if (!timingout) { @@ -1134,9 +1059,7 @@ setsockopt(fd, SOL_SOCKET, opt, &on, (so if (maxsock > (int)(rlim_ofile_cur - FD_MARGIN)) bump_nofile(); } - if (debug) - fprintf(stderr, "registered %s on %d\n", - sep->se_server, sep->se_fd); + DPRINTF(SERV_FMT ": registered on fd %d", SERV_PARAMS(sep), sep->se_fd); } /* @@ -1144,12 +1067,16 @@ setsockopt(fd, SOL_SOCKET, opt, &on, (so */ static void close_sep(struct servtab *sep) -{ +{ + if (sep->se_fd >= 0) { (void) close(sep->se_fd); sep->se_fd = -1; } sep->se_count = 0; + if (sep->se_ip_max != SERVTAB_UNSPEC_SIZE_T) { + clear_ip_list(sep); + } } static void @@ -1169,8 +1096,8 @@ register_rpc(struct servtab *sep) } socklen = sizeof ss; if (getsockname(sep->se_fd, (struct sockaddr *)(void *)&ss, &socklen) < 0) { - syslog(LOG_ERR, "%s/%s: getsockname: %m", - sep->se_service, sep->se_proto); + syslog(LOG_ERR, SERV_FMT ": getsockname: %m", + SERV_PARAMS(sep)); return; } @@ -1178,10 +1105,9 @@ register_rpc(struct servtab *sep) nbuf.len = ss.ss_len; nbuf.maxlen = sizeof (struct sockaddr_storage); for (n = sep->se_rpcversl; n <= sep->se_rpcversh; n++) { - if (debug) - fprintf(stderr, "rpcb_set: %u %d %s %s\n", - sep->se_rpcprog, n, nconf->nc_netid, - taddr2uaddr(nconf, &nbuf)); + DPRINTF("rpcb_set: %u %d %s %s", + sep->se_rpcprog, n, nconf->nc_netid, + taddr2uaddr(nconf, &nbuf)); (void)rpcb_unset((unsigned int)sep->se_rpcprog, (unsigned int)n, nconf); if (!rpcb_set((unsigned int)sep->se_rpcprog, (unsigned int)n, nconf, &nbuf)) syslog(LOG_ERR, "rpcb_set: %u %d %s %s%s", @@ -1205,9 +1131,8 @@ unregister_rpc(struct servtab *sep) } for (n = sep->se_rpcversl; n <= sep->se_rpcversh; n++) { - if (debug) - fprintf(stderr, "rpcb_unset(%u, %d, %s)\n", - sep->se_rpcprog, n, nconf->nc_netid); + DPRINTF("rpcb_unset(%u, %d, %s)", + sep->se_rpcprog, n, nconf->nc_netid); if (!rpcb_unset((unsigned int)sep->se_rpcprog, (unsigned int)n, nconf)) syslog(LOG_ERR, "rpcb_unset(%u, %d, %s) failed\n", sep->se_rpcprog, n, nconf->nc_netid); @@ -1224,7 +1149,7 @@ enter(struct servtab *cp) sep = malloc(sizeof (*sep)); if (sep == NULL) { syslog(LOG_ERR, "Out of memory."); - exit(1); + exit(EXIT_FAILURE); } *sep = *cp; sep->se_fd = -1; @@ -1234,32 +1159,15 @@ enter(struct servtab *cp) return (sep); } -FILE *fconfig = NULL; -struct servtab serv; -char line[LINE_MAX]; +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 -static char *policy = NULL; -#endif - -static int -setconfig(void) -{ - if (defhost) - free(defhost); - defhost = newstr("*"); -#ifdef IPSEC - if (policy) - free(policy); - policy = NULL; +char *policy; #endif - if (fconfig != NULL) { - fseek(fconfig, 0L, SEEK_SET); - return (1); - } - fconfig = fopen(CONFIG, "r"); - return (fconfig != NULL); -} static void endconfig(void) @@ -1272,23 +1180,59 @@ endconfig(void) 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(void) +getconfigent(char **current_pos) { struct servtab *sep = &serv; - int argc, val; + 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; -more: - while ((cp = nextline(fconfig)) != NULL) { + /* + * 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[0] == '#' && cp[1] == '@') { + if (cp[1] == '@') { char *p; for (p = cp + 2; p && *p && isspace((unsigned char)*p); p++) ; @@ -1298,10 +1242,13 @@ more: policy = NULL; } else { if (ipsecsetup_test(p) < 0) { - syslog(LOG_ERR, - "%s: invalid ipsec policy \"%s\"", - CONFIG, p); - exit(1); + 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); @@ -1310,22 +1257,36 @@ more: } } #endif - if (*cp == '#' || *cp == '\0') - continue; - break; + + goto more; } - if (cp == NULL) - return (NULL); - /* - * clear the static buffer, since some fields (se_ctrladdr, - * for example) don't get initialized here. - */ - memset(sep, 0, sizeof *sep); + + /* Parse next token: listen-addr/hostname, service-spec, .include */ arg = skip(&cp); + if (cp == NULL) { - /* got an empty line containing just blanks/tabs. */ 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) { @@ -1348,8 +1309,10 @@ more: goto more; } } - } else - sep->se_hostaddr = newstr(defhost); + } 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 == '+') { @@ -1363,49 +1326,79 @@ more: sep->se_type = NORM_TYPE; } - arg = sskip(&cp); - if (strncmp(arg, "stream", sizeof("stream") - 1) == 0) { - char *accf, *accf_arg; + DPRINTCONF("Found service definition '%s'", sep->se_service); - sep->se_socktype = SOCK_STREAM; + /* on/off/socktype */ + arg = skip(&cp); + if (arg == NULL) { + LOG_TOO_FEW_ARGS(); + freeconfig(sep); + goto more; + } - /* one and only one accept filter */ - accf = strchr(arg, ':'); - if (accf) { - if (accf != strrchr(arg, ':') ||/* more than one */ - *(accf + 1) == '\0') { /* nothing beyond */ - sep->se_socktype = -1; - } else { - accf++; /* skip delimiter */ - strlcpy(sep->se_accf.af_name, accf, - sizeof(sep->se_accf.af_name)); - accf_arg = strchr(accf, ','); - if (accf_arg) { /* zero or one arg, no more */ - 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)); - } - } - } + /* 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) { - 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; + 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); + } + } - arg = sskip(&cp); + /* 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/"); @@ -1415,8 +1408,9 @@ more: #define MALFORMED(arg) \ do { \ - syslog(LOG_ERR, "%s: malformed buffer size option `%s'", \ + ERR("%s: malformed buffer size option `%s'", \ sep->se_service, (arg)); \ + freeconfig(sep); \ goto more; \ /*NOTREACHED*/ \ } while (/*CONSTCOND*/0) @@ -1435,8 +1429,9 @@ do { \ val *= 1024 * 1024; \ } \ if (val < 1) { \ - syslog(LOG_ERR, "%s: invalid buffer size `%s'", \ + ERR("%s: invalid buffer size `%s'", \ sep->se_service, (arg)); \ + freeconfig(sep); \ goto more; \ } \ /*NOTREACHED*/ \ @@ -1461,7 +1456,7 @@ do { \ if ((buf0 = strchr(sep->se_proto, ',')) != NULL) { /* Not meaningful for Tcpmux services. */ if (ISMUX(sep)) { - syslog(LOG_ERR, "%s: can't specify buffer sizes for " + ERR("%s: can't specify buffer sizes for " "tcpmux services", sep->se_service); goto more; } @@ -1476,8 +1471,7 @@ do { \ /* Make sure a 3rd one wasn't specified. */ if (strchr(buf1, ',') != NULL) { - syslog(LOG_ERR, "%s: too many buffer sizes", - sep->se_service); + ERR("%s: too many buffer sizes", sep->se_service); goto more; } @@ -1509,115 +1503,85 @@ do { \ #undef GETVAL #undef MALFORMED - if (strcmp(sep->se_proto, "unix") == 0) { - sep->se_family = AF_LOCAL; - } else { - val = (int)strlen(sep->se_proto); - if (!val) { - syslog(LOG_ERR, "%s: invalid protocol specified", - sep->se_service); - goto more; - } - 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: - sep->se_family = AF_INET; /*will become AF_INET6*/ - break; - } - if (strncmp(sep->se_proto, "rpc/", 4) == 0) { -#ifdef RPC - char *cp1, *ccp; - cp1 = strchr(sep->se_service, '/'); - if (cp1 == 0) { - syslog(LOG_ERR, "%s: no rpc version", - sep->se_service); - goto more; - } - *cp1++ = '\0'; - sep->se_rpcversl = sep->se_rpcversh = - (int)strtol(cp1, &ccp, 0); - if (ccp == cp1) { - badafterall: - syslog(LOG_ERR, "%s/%s: bad rpc version", - sep->se_service, cp1); - goto more; - } - if (*ccp == '-') { - cp1 = ccp + 1; - sep->se_rpcversh = (int)strtol(cp1, &ccp, 0); - if (ccp == cp1) - goto badafterall; - } -#else - syslog(LOG_ERR, "%s: rpc services not supported", - sep->se_service); - goto more; -#endif /* RPC */ - } + 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; } - arg = sskip(&cp); - { + + /* Rate limiting parsing */ { char *cp1; if ((cp1 = strchr(arg, ':')) == NULL) cp1 = strchr(arg, '.'); if (cp1 != NULL) { + int rstatus; *cp1++ = '\0'; - sep->se_max = atoi(cp1); + 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_max = TOOMANY; + sep->se_service_max = TOOMANY; + } + if (parse_wait(sep, strcmp(arg, "wait") == 0)) { + freeconfig(sep); + goto more; } - sep->se_wait = strcmp(arg, "wait") == 0; - if (ISMUX(sep)) { - /* - * 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)) { - syslog(LOG_ERR, - "%s: bad protocol for tcpmux service %s", - CONFIG, sep->se_service); - goto more; - } - if (sep->se_socktype != SOCK_STREAM) { - syslog(LOG_ERR, - "%s: bad socket type for tcpmux service %s", - CONFIG, sep->se_service); - 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, '.'); } - sep->se_user = newstr(sskip(&cp)); - if ((sep->se_group = strchr(sep->se_user, ':')) != NULL) - *sep->se_group++ = '\0'; - else if ((sep->se_group = strchr(sep->se_user, '.')) != NULL) - *sep->se_group++ = '\0'; - sep->se_server = newstr(sskip(&cp)); - if (strcmp(sep->se_server, "internal") == 0) { - struct biltin *bi; + if (separator == NULL) { + /* Only user was specified */ + sep->se_group = NULL; + } else { + *separator = '\0'; + sep->se_group = newstr(separator + 1); + } - 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 == 0) { - syslog(LOG_ERR, "internal service %s unknown", - sep->se_service); - goto more; - } - sep->se_bi = bi; - sep->se_wait = bi->bi_wait; - } else - sep->se_bi = NULL; + 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; arg = skip(&cp)) { if (argc < MAXARGV) @@ -1628,10 +1592,12 @@ do { \ #ifdef IPSEC sep->se_policy = policy ? newstr(policy) : NULL; #endif + /* getconfigent read a positional service def, move to next line */ + *current_pos = nextline(fconfig); return (sep); } -static void +void freeconfig(struct servtab *cp) { int i; @@ -1644,7 +1610,8 @@ freeconfig(struct servtab *cp) free(cp->se_proto); if (cp->se_user) free(cp->se_user); - /* Note: se_group is part of the newstr'ed se_user */ + if(cp->se_group) + free(cp->se_group); if (cp->se_server) free(cp->se_server); for (i = 0; i < MAXARGV; i++) @@ -1656,25 +1623,13 @@ freeconfig(struct servtab *cp) #endif } - /* - * Safe skip - if skip returns null, log a syntax error in the - * configuration file and exit. + * 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 * -sskip(char **cpp) -{ - char *cp; - - cp = skip(cpp); - if (cp == NULL) { - syslog(LOG_ERR, "%s: syntax error", CONFIG); - exit(1); - } - return (cp); -} - -static char * skip(char **cpp) { char *cp = *cpp; @@ -1692,13 +1647,14 @@ again: c = getc(fconfig); (void) ungetc(c, fconfig); - if (c == ' ' || c == '\t') + 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 && (quote || (*cp != ' ' && *cp != '\t'))) { if (*cp == '\'' || *cp == '"') { @@ -1720,27 +1676,32 @@ again: return (start); } -static char * +char * nextline(FILE *fd) { char *cp; - if (fgets(line, (int)sizeof(line), fd) == NULL) - return (NULL); + if (fgets(line, (int)sizeof(line), fd) == NULL) { + if (ferror(fd)) { + ERR("Error when reading next line: %s", strerror(errno)); + } + return NULL; + } cp = strchr(line, '\n'); if (cp) *cp = '\0'; - return (line); + line_number++; + return line; } -static char * +char * newstr(const char *cp) { char *dp; if ((dp = strdup((cp != NULL) ? cp : "")) != NULL) return (dp); syslog(LOG_ERR, "strdup: %m"); - exit(1); + exit(EXIT_FAILURE); /*NOTREACHED*/ } @@ -1948,8 +1909,7 @@ machtime(void) struct timeval tv; if (gettimeofday(&tv, NULL) < 0) { - if (debug) - fprintf(stderr, "Unable to get time of day\n"); + DPRINTF("Unable to get time of day"); return (0); } #define OFFSET ((uint32_t)25567 * 24*60*60) @@ -2025,6 +1985,7 @@ daytime_dg(int s, struct servtab *sep) (void) sendto(s, buffer, len, 0, sa, size); } +#ifdef DEBUG_ENABLE /* * print_service: * Dump relevant information to stderr @@ -2035,14 +1996,14 @@ print_service(const char *action, struct if (isrpcservice(sep)) fprintf(stderr, - "%s: %s rpcprog=%d, rpcvers = %d/%d, proto=%s, wait.max=%d.%d, user:group=%s:%s builtin=%lx server=%s" + "%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_max, sep->se_user, sep->se_group, + 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 ? sep->se_policy : "") @@ -2050,21 +2011,22 @@ print_service(const char *action, struct ); else fprintf(stderr, - "%s: %s proto=%s%s, wait.max=%d.%d, user:group=%s:%s builtin=%lx server=%s" + "%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_service, + action, sep->se_hostaddr, sep->se_service, sep->se_type == FAITH_TYPE ? "faith/" : "", sep->se_proto, - sep->se_wait, sep->se_max, sep->se_user, sep->se_group, + 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 ? sep->se_policy : "") #endif ); } +#endif static void usage(void) @@ -2074,7 +2036,7 @@ usage(void) #else (void)fprintf(stderr, "usage: %s [-d] [conf]\n", getprogname()); #endif - exit(1); + exit(EXIT_FAILURE); } @@ -2122,8 +2084,7 @@ tcpmux(int ctrl, struct servtab *sep) } service[len] = '\0'; - if (debug) - fprintf(stderr, "tcpmux: someone wants %s\n", service); + DPRINTF("tcpmux: %s: service requested", service); /* * Help is a required command, and lists available services, @@ -2155,7 +2116,7 @@ tcpmux(int ctrl, struct servtab *sep) } strwrite(ctrl, "-Service not available\r\n"); reject: - _exit(1); + _exit(EXIT_FAILURE); } /* @@ -2270,10 +2231,753 @@ my_kevent(const struct kevent *changelis static struct kevent * allocchange(void) { - if (changes == A_CNT(changebuf)) { - (void) my_kevent(changebuf, A_CNT(changebuf), NULL, 0); + if (changes == __arraycount(changebuf)) { + (void) my_kevent(changebuf, __arraycount(changebuf), NULL, 0); changes = 0; } return (&changebuf[changes++]); } + +static void +config_root() +{ + struct servtab *sep; + /* Uncheck services */ + for (sep = servtab; sep != NULL; sep = sep->se_next) { + sep->se_checked = 0; + } + 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) { + 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 == 0) { + 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) + 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) /* 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() { + /* 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((void *)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)) { + 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 */ + int 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 %zi seconds) " + "already met, closing until end of timeout in " + "%zu 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 = 1; + 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 " + "%zi 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) +{ + 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)) { + /* 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 */ + }; + int 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 && 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 */ + }; + int 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() +{ + 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 ": %zi 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; + } + } + return NULL; +} Added files: Index: src/tests/usr.sbin/inetd/Makefile diff -u /dev/null src/tests/usr.sbin/inetd/Makefile:1.1 --- /dev/null Sun Aug 29 05:54:18 2021 +++ src/tests/usr.sbin/inetd/Makefile Sun Aug 29 05:54:18 2021 @@ -0,0 +1,18 @@ +# $NetBSD: Makefile,v 1.1 2021/08/29 09:54:18 christos Exp $ + +.include <bsd.own.mk> + +TESTSDIR=${TESTSBASE}/usr.sbin/inetd + +TESTS_C += t_inetd + +#inetd service, supports dgram and stream via args +MKMAN = no +PROGS += test_server +BINDIR=${TESTSDIR} + +#Other files that should be copied to /usr/tests +FILESDIR=${TESTSDIR} +FILES=test_server inetd_ratelimit.conf + +.include <bsd.test.mk> Index: src/tests/usr.sbin/inetd/inetd_ratelimit.conf diff -u /dev/null src/tests/usr.sbin/inetd/inetd_ratelimit.conf:1.1 --- /dev/null Sun Aug 29 05:54:18 2021 +++ src/tests/usr.sbin/inetd/inetd_ratelimit.conf Sun Aug 29 05:54:18 2021 @@ -0,0 +1,101 @@ +# $NetBSD: inetd_ratelimit.conf,v 1.1 2021/08/29 09:54:18 christos Exp $ + +127.0.0.1: + +#DGRAM WAIT SERVICE + +#Test ip_max of 3 +5432 on + protocol = udp, + wait = yes, + user = root, + service_max = 5, + ip_max = 3, + exec = test_server, + args = test_server dgram wait; + +#Test ip_max of 0 +5433 on + protocol = udp, + wait = yes, + user = root, + ip_max = 0, + exec = test_server, + args = test_server dgram wait; + +#Test service_max of 2 +5434 on + protocol = udp, + wait = yes, + user = root, + service_max = 2, + exec = test_server, + args = test_server dgram wait; + +#Test service_max of 0 +5435 on + protocol = udp, + wait = yes, + user = root, + service_max = 0, + exec = test_server, + args = test_server dgram wait; + +#STREAM WAIT SERVICE + +#Test service_max of 2 +5434 on + protocol = tcp, + wait = yes, + user = root, + service_max = 2, + exec = test_server, + args = test_server stream wait; + +#Test service_max of 0 +5435 on + protocol = tcp, + wait = yes, + user = root, + service_max = 0, + exec = test_server, + args = test_server stream wait; + +#STREAM NOWAIT SERVICE + +#Test ip_max of 3 +5436 on + protocol = tcp, + wait = no, + user = root, + service_max = 5, + ip_max = 3, + exec = test_server, + args = test_server stream nowait; + +#Test ip_max of 0 +5437 on + protocol = tcp, + wait = no, + user = root, + ip_max = 0, + exec = test_server, + args = test_server stream nowait; + +#Test service_max of 2 +5438 on + protocol = tcp, + wait = no, + user = root, + service_max = 2, + exec = test_server, + args = test_server stream nowait; + +#Test service_max of 0 +5439 on + protocol = tcp, + wait = no, + user = root, + service_max = 0, + exec = test_server, + args = test_server stream nowait; Index: src/tests/usr.sbin/inetd/t_inetd.c diff -u /dev/null src/tests/usr.sbin/inetd/t_inetd.c:1.1 --- /dev/null Sun Aug 29 05:54:18 2021 +++ src/tests/usr.sbin/inetd/t_inetd.c Sun Aug 29 05:54:18 2021 @@ -0,0 +1,296 @@ +/* $NetBSD: t_inetd.c,v 1.1 2021/08/29 09:54:18 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: t_inetd.c,v 1.1 2021/08/29 09:54:18 christos Exp $"); + +#include <atf-c.h> +#include <spawn.h> +#include <errno.h> +#include <stdlib.h> +#include <signal.h> +#include <stdio.h> +#include <string.h> +#include <err.h> +#include <sys/wait.h> +#include <sys/time.h> +#include <sys/socket.h> +#include <netdb.h> + +#define CHECK_ERROR(expr) ATF_REQUIRE_MSG((expr) != -1,\ + "%s", strerror(errno)) + +#define TCP 6 +#define UDP 17 + +static pid_t run(const char *, char **); +static char *concat(const char *restrict, const char *restrict); +static void waitfor(pid_t, const char *); +static bool run_udp_client(const char *); +static int create_socket(const char *, const char *, int, int, time_t, struct sockaddr_storage *); +static bool run_tcp_client(const char *); + +/* This test should take around 5 to 7 seconds to complete. */ +ATF_TC(test_ratelimit); + +ATF_TC_HEAD(test_ratelimit, tc) +{ + atf_tc_set_md_var(tc, "descr", "Test inetd rate limiting values, " + "uses UDP/TCP ports 5432-5439 with localhost."); + /* Need to run as root so inetd can set uid */ + atf_tc_set_md_var(tc, "require.user", "root"); + atf_tc_set_md_var(tc, "require.progs", "inetd"); + /* Time out after 10 seconds, just in case */ + atf_tc_set_md_var(tc, "timeout", "10"); +} + +ATF_TC_BODY(test_ratelimit, tc) +{ + pid_t proc; + + /* Copy test server to relative path used in inetd_ratelimit.conf */ + atf_utils_copy_file( + concat(atf_tc_get_config_var(tc, "srcdir"), "/test_server"), + "test_server" + ); + + /* Run inetd in debug mode using specified config file */ + proc = run("inetd", (char*[]) { + "inetd", "-d", + concat(atf_tc_get_config_var(tc, "srcdir"), + "/inetd_ratelimit.conf"), + NULL + }); + + /* Wait for inetd to load services */ + CHECK_ERROR(sleep(1)); + + /* + * TODO test dgram/nowait? Specified in manpage but doesn't seem to + * work + */ + + /* dgram/wait ip_max of 3, should receive these 3 responses */ + for (int i = 0; i < 3; i++) { + ATF_REQUIRE(run_udp_client("5432")); + } + + /* Rate limiting should prevent a response to this request */ + ATF_REQUIRE(!run_udp_client("5432")); + + /* dgram/wait ip_max of 0 */ + ATF_REQUIRE(!run_udp_client("5433")); + + /* dgram/wait service_max of 2 */ + ATF_REQUIRE(run_udp_client("5434")); + ATF_REQUIRE(run_udp_client("5434")); + ATF_REQUIRE(!run_udp_client("5434")); + + /* dgram/wait service_max of 0 */ + ATF_REQUIRE(!run_udp_client("5435")); + + /* stream/wait service_max of 2 */ + ATF_REQUIRE(run_tcp_client("5434")); + ATF_REQUIRE(run_tcp_client("5434")); + ATF_REQUIRE(!run_tcp_client("5434")); + + /* stream/wait service_max of 0 */ + ATF_REQUIRE(!run_tcp_client("5435")); + + /* stream/nowait ip_max of 3 */ + for (int i = 0; i < 3; i++) { + ATF_REQUIRE(run_tcp_client("5436")); + } + ATF_REQUIRE(!run_tcp_client("5436")); + + /* stream/nowait ip_max of 0 */ + ATF_REQUIRE(!run_tcp_client("5437")); + + /* dgram/wait service_max of 2 */ + ATF_REQUIRE(run_tcp_client("5438")); + ATF_REQUIRE(run_tcp_client("5438")); + ATF_REQUIRE(!run_tcp_client("5438")); + + /* dgram/wait service_max of 0 */ + ATF_REQUIRE(!run_tcp_client("5439")); + + /* Exit inetd */ + CHECK_ERROR(kill(proc, SIGTERM)); + + waitfor(proc, "inetd"); +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, test_ratelimit); +} + +/* Return true if successfully received message, false if timeout */ +static bool +run_udp_client(const char *port) +{ + char buffer[] = "test"; + struct sockaddr_storage addr; + + int udp = create_socket("127.0.0.1", port, SOCK_DGRAM, UDP, 1, &addr); + + CHECK_ERROR(sendto(udp, buffer, sizeof(buffer), 0, + (struct sockaddr *)&addr, addr.ss_len)); + + struct iovec iov = { + .iov_base = buffer, + .iov_len = sizeof(buffer) + }; + + struct msghdr msg = { + .msg_name = &addr, + .msg_namelen = addr.ss_len, /* is this correct? */ + .msg_iov = &iov, + .msg_iovlen = 1 + }; + + struct mmsghdr msglist = { + .msg_hdr = &msg + }; + + ssize_t count = recvmsg(udp, &msg, 0); + if (count == -1) { + if (errno == EAGAIN) { + /* Timed out, return false */ + CHECK_ERROR(close(udp)); + return false; + } else { + /* All other errors fatal */ + CHECK_ERROR(-1); + } + } + CHECK_ERROR(close(udp)); + return true; +} + +/* Run localhost tcp echo, return true if successful, false if timeout/disconnect */ +static bool +run_tcp_client(const char *port) +{ + struct sockaddr_storage remote; + ssize_t count; + int tcp; + char buffer[] = "test"; + + tcp = create_socket("127.0.0.1", port, SOCK_STREAM, TCP, 1, &remote); + CHECK_ERROR(connect(tcp, (const struct sockaddr *)&remote, + remote.ss_len)); + CHECK_ERROR(send(tcp, buffer, sizeof(buffer), 0)); + count = recv(tcp, buffer, sizeof(buffer), 0); + if (count == -1) { + /* + * Connection reset by peer indicates the connection was + * dropped. EAGAIN indicates the timeout expired. Any other + * error is unexpected for this client program test. + */ + if(errno == ECONNRESET || errno == EAGAIN) { + return false; + } else { + CHECK_ERROR(-1); + return false; + } + } + + if (count == 0) { + /* socket was shutdown by inetd, no more data available */ + return false; + } + return true; +} + +/* + * Create a socket with the characteristics inferred by the args, return parsed + * socket address in dst. + */ +static int +create_socket(const char *address, const char *port, + int socktype, int proto, time_t timeout_sec, struct sockaddr_storage *dst) +{ + struct addrinfo hints = { + .ai_flags = AI_NUMERICHOST, + .ai_socktype = socktype, + .ai_protocol = proto + }; + struct addrinfo * res; + int error, fd; + + ATF_REQUIRE_EQ_MSG(error = getaddrinfo(address, port, &hints, &res), 0, + "%s", gai_strerror(error)); + + /* Make sure there is only one possible bind address */ + ATF_REQUIRE_MSG(res->ai_next == NULL, "Ambiguous create_socket args"); + CHECK_ERROR(fd = socket(res->ai_family, + res->ai_socktype, res->ai_protocol)); + struct timeval timeout = { timeout_sec }; + CHECK_ERROR(setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, + sizeof(timeout))); + memcpy(dst, res->ai_addr, res->ai_addrlen); + freeaddrinfo(res); + return fd; +} + +/* Run program with args */ +static pid_t +run(const char *prog, char **args) +{ + pid_t proc; + extern char **environ; + ATF_REQUIRE_EQ(posix_spawnp(&proc, prog, + NULL, NULL, args, environ), 0); + return proc; +} + +/* Wait for a process to exit, check return value */ +static void +waitfor(pid_t pid, const char *taskname) +{ + int status; + CHECK_ERROR(waitpid(pid, &status, WALLSIG) == pid); + + ATF_REQUIRE_EQ_MSG(WEXITSTATUS(status), EXIT_SUCCESS, + "%s failed with " + "exit status %d", taskname, WEXITSTATUS(status)); +} + +/* Concatenate two const strings, do not free arguments */ +static char * +concat(const char *restrict left, const char *restrict right) +{ + char *res; + if (asprintf(&res, "%s%s", left, right) == -1) + return NULL; + return res; +} Index: src/tests/usr.sbin/inetd/test_server.c diff -u /dev/null src/tests/usr.sbin/inetd/test_server.c:1.1 --- /dev/null Sun Aug 29 05:54:18 2021 +++ src/tests/usr.sbin/inetd/test_server.c Sun Aug 29 05:54:18 2021 @@ -0,0 +1,160 @@ +/* $NetBSD: test_server.c,v 1.1 2021/08/29 09:54:18 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: test_server.c,v 1.1 2021/08/29 09:54:18 christos Exp $"); + +#include <sys/socket.h> +#include <unistd.h> +#include <netdb.h> +#include <stdint.h> +#include <stdio.h> +#include <string.h> +#include <errno.h> +#include <stdlib.h> +#include <syslog.h> + +#define CHECK(expr) do {\ + if ((expr) == -1) {\ + syslog(LOG_ERR, "Error at %s:%d: %s", \ + __FILE__, __LINE__, \ + strerror(errno));\ + exit(EXIT_FAILURE);\ + }\ +} while(0); + +static void stream_nowait_service(void); +static void stream_wait_service(void); +static void dgram_wait_service(void); + +int +main(int argc, char **argv) +{ + + openlog("inetd_test_server", LOG_PID | LOG_NOWAIT, LOG_DAEMON); + + if (argc < 3) { + syslog(LOG_ERR, "Invalid arg count"); + exit(EXIT_FAILURE); + } + + /* Run the correct service according to the args */ + if (strcmp(argv[1], "dgram") == 0) { + if (strcmp(argv[2], "wait") == 0) { + dgram_wait_service(); + } else { + syslog(LOG_ERR, "Invalid arg %s", argv[2]); + exit(EXIT_FAILURE); + } + } else if (strcmp(argv[1], "stream") == 0) { + if (strcmp(argv[2], "wait") == 0) { + stream_wait_service(); + } else if (strcmp(argv[2], "nowait") == 0) { + stream_nowait_service(); + } else { + syslog(LOG_ERR, "Invalid arg %s", argv[2]); + exit(EXIT_FAILURE); + } + } else { + syslog(LOG_ERR, "Invalid args %s %s", argv[1], argv[2]); + exit(EXIT_FAILURE); + } + return 0; +} + +static void +stream_nowait_service() +{ + ssize_t count; + char buffer[10]; + CHECK(count = recv(0, buffer, sizeof(buffer), 0)); + syslog(LOG_WARNING, "Received stream/nowait message \"%.*s\"\n", + count, buffer); + CHECK(send(1, buffer, count, 0)); +} + +static void +stream_wait_service() +{ + struct sockaddr_storage addr; + ssize_t count; + int fd; + socklen_t addr_len; + char buffer[10]; + + CHECK(fd = accept(0, (struct sockaddr*)&addr, &addr_len)); + CHECK(count = recv(fd, buffer, sizeof(buffer), 0)); + syslog(LOG_WARNING, "Received stream/wait message \"%.*s\"\n", + count, buffer); + CHECK(send(fd, buffer, count, 0)); + CHECK(shutdown(fd, SHUT_RDWR)); + CHECK(close(fd)); +} + +static void +dgram_wait_service() +{ + char buffer[256]; + char name[NI_MAXHOST]; + socklen_t source_size; + struct sockaddr_storage addr; + + struct iovec store = { + .iov_base = &buffer, + .iov_len = sizeof(buffer) + }; + struct msghdr header = { + .msg_name = &addr, + .msg_namelen = sizeof(struct sockaddr_storage), + .msg_iov = &store, + .msg_iovlen = 1 + /* scatter/gather and control info is null */ + }; + int count; + + /* Peek so service can still get the packet */ + CHECK(count = recvmsg(0, &header, 0)); + + CHECK(sendto(1, buffer, count, 0, + (struct sockaddr*)(&addr), + addr.ss_len)); + + int error = getnameinfo((struct sockaddr*)&addr, + addr.ss_len, name, NI_MAXHOST, + NULL, 0, NI_NUMERICHOST); + + if (error) { + syslog(LOG_ERR, "getnameinfo error: %s\n", gai_strerror(error)); + exit(EXIT_FAILURE); + } + syslog(LOG_WARNING, "Received dgram/wait message \"%.*s\" from %s\n", + count, buffer, name); +} Index: src/usr.sbin/inetd/inetd.h diff -u /dev/null src/usr.sbin/inetd/inetd.h:1.1 --- /dev/null Sun Aug 29 05:54:18 2021 +++ src/usr.sbin/inetd/inetd.h Sun Aug 29 05:54:18 2021 @@ -0,0 +1,223 @@ +/* $NetBSD: inetd.h,v 1.1 2021/08/29 09:54:18 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. + */ + +#ifndef _INETD_H +#define _INETD_H + +#include <netinet/in.h> +#include <sys/socket.h> +#include <sys/time.h> +#include <sys/un.h> + +#include <arpa/inet.h> + +#include <netdb.h> + +#include "pathnames.h" + +#ifdef IPSEC +#include <netipsec/ipsec.h> +#ifndef IPSEC_POLICY_IPSEC /* no ipsec support on old ipsec */ +#undef IPSEC +#endif +#include "ipsec.h" +#endif + +typedef enum service_type { + NORM_TYPE = 0, + MUX_TYPE = 1, + MUXPLUS_TYPE = 2, + FAITH_TYPE = 3 +} service_type; + +#define ISMUXPLUS(sep) ((sep)->se_type == MUXPLUS_TYPE) +#define ISMUX(sep) (((sep)->se_type == MUX_TYPE) || ISMUXPLUS(sep)) + +#define TOOMANY 40 /* don't start more than TOOMANY */ + +#define CONF_ERROR_FMT "%s line %zu: " + +/* Log warning/error with 0 or variadic args with line number and file name */ + +#define ILV(prio, msg, ...) syslog(prio, CONF_ERROR_FMT msg ".", \ + CONFIG, line_number __VA_OPT__(,) __VA_ARGS__) + +#define WRN(msg, ...) ILV(LOG_WARNING, msg __VA_OPT__(,) __VA_ARGS__) +#define ERR(msg, ...) ILV(LOG_ERR, msg __VA_OPT__(,) __VA_ARGS__) + +/* Debug logging */ +#ifdef DEBUG_ENABLE +#define DPRINTF(fmt, ...) do {\ + if (debug) {\ + fprintf(stderr, fmt "\n" __VA_OPT__(,) __VA_ARGS__);\ + }\ +} while (0) +#else +#define DPRINTF(fmt, ...) __nothing +#endif + +#define DPRINTCONF(fmt, ...) DPRINTF(CONF_ERROR_FMT fmt,\ + CONFIG, line_number __VA_OPT__(,) __VA_ARGS__) + +#define STRINGIFY(x) #x +#define TOSTRING(x) STRINGIFY(x) + +struct servtab { + char *se_hostaddr; /* host address to listen on */ + char *se_service; /* name of service */ + int se_socktype; /* type of socket to use */ + sa_family_t se_family; /* address family */ + char *se_proto; /* protocol used */ + int se_sndbuf; /* sndbuf size */ + int se_rcvbuf; /* rcvbuf size */ + int se_rpcprog; /* rpc program number */ + int se_rpcversl; /* rpc program lowest version */ + int se_rpcversh; /* rpc program highest version */ +#define isrpcservice(sep) ((sep)->se_rpcversl != 0) + pid_t se_wait; /* single threaded server */ + short se_checked; /* looked at during merge */ + char *se_user; /* user name to run as */ + char *se_group; /* group name to run as */ + struct biltin *se_bi; /* if built-in, description */ + char *se_server; /* server program */ +#define MAXARGV 64 + char *se_argv[MAXARGV+1]; /* program arguments */ +#ifdef IPSEC + char *se_policy; /* IPsec poilcy string */ +#endif + struct accept_filter_arg se_accf; /* accept filter for stream service */ + int se_fd; /* open descriptor */ + service_type se_type; /* type */ + union { + struct sockaddr_storage se_ctrladdr_storage; /* ensure correctness of C struct initializer */ + 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_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 */ + time_t se_time; /* start of se_count and ip_max counts, in seconds from arbitrary point */ + struct servtab *se_next; +}; + +/* From inetd.c */ +int parse_protocol(struct servtab *); +int parse_wait(struct servtab *, int); +int parse_server(struct servtab *, const char *); +void parse_socktype(char *, struct servtab *); +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 config file path */ +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; + +/* Default IPsec policy for current config file */ +extern char *policy; + +/* From parse_v2.c */ + +typedef enum parse_v2_result {V2_SUCCESS, V2_SKIP, V2_ERROR} parse_v2_result; + +/* + * Parse a key-values service definition, starting at the token after + * on/off (i.e. parse a series of key-values pairs terminated by a semicolon). + * Fills the provided servtab structure. Does not call freeconfig on error. + */ +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 /dev/null src/usr.sbin/inetd/parse_v2.c:1.1 --- /dev/null Sun Aug 29 05:54:18 2021 +++ src/usr.sbin/inetd/parse_v2.c Sun Aug 29 05:54:18 2021 @@ -0,0 +1,1153 @@ +/* $NetBSD: parse_v2.c,v 1.1 2021/08/29 09:54:18 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: parse_v2.c,v 1.1 2021/08/29 09:54:18 christos Exp $"); + +#include <ctype.h> +#include <errno.h> +#include <limits.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> +#include <err.h> + +#include "inetd.h" +#include "ipsec.h" + +typedef enum values_state { + VALS_PARSING, VALS_END_KEY, VALS_END_DEF, VALS_ERROR +} values_state; + +/* Values parsing state */ +typedef struct val_parse_info { + char *cp; + /* Used so we can null-terminate values by overwriting ',' and ';' */ + //char terminal; + values_state state; +} val_parse_info, *vlist; + +/* The result of a call to parse_invoke_handler */ +typedef enum invoke_result { + INVOKE_SUCCESS, INVOKE_FINISH, INVOKE_ERROR +} invoke_result; + +/* The result of a parse of key handler values */ +typedef enum hresult { + KEY_HANDLER_FAILURE, KEY_HANDLER_SUCCESS +} hresult; + +/* v2 syntax key-value parsers */ +static hresult args_handler(struct servtab *, vlist); +static hresult bind_handler(struct servtab *, vlist); +static hresult exec_handler(struct servtab *, vlist); +static hresult filter_handler(struct servtab *, vlist); +static hresult group_handler(struct servtab *, vlist); +static hresult service_max_handler(struct servtab *, vlist); +static hresult ip_max_handler(struct servtab *, vlist); +static hresult protocol_handler(struct servtab *, vlist); +static hresult recv_buf_handler(struct servtab *, vlist); +static hresult send_buf_handler(struct servtab *, vlist); +static hresult socket_type_handler(struct servtab *, vlist); +static hresult unknown_handler(struct servtab *, vlist); +static hresult user_handler(struct servtab *, vlist); +static hresult wait_handler(struct servtab *, vlist); + +#ifdef IPSEC +static hresult ipsec_handler(struct servtab *, vlist); +#endif + +static invoke_result parse_invoke_handler(bool *, char **, struct servtab *); +static bool fill_default_values(struct servtab *); +static bool parse_quotes(char **); +static bool skip_whitespace(char **); +static int size_to_bytes(char *); +static bool infer_protocol_ip_version(struct servtab *); +static bool setup_internal(struct servtab *); +static void try_infer_socktype(struct servtab *); +char hex_to_bits(char); +#ifdef IPSEC +static void setup_ipsec(struct servtab *); +#endif +static inline void strmove(char *, size_t); + +/* v2 Key handlers infrastructure */ + +/* v2 syntax Handler function, which must parse all values for its key */ +typedef hresult (*key_handler_func)(struct servtab *, vlist); + +/* List of v2 syntax key handlers */ +static struct key_handler { + const char *name; + key_handler_func handler; +} key_handlers[] = { + { "bind", bind_handler }, + { "socktype", socket_type_handler }, + { "acceptfilter", filter_handler }, + { "protocol", protocol_handler }, + { "sndbuf", send_buf_handler }, + { "recvbuf", recv_buf_handler }, + { "wait", wait_handler }, + { "service_max", service_max_handler }, + { "user", user_handler }, + { "group", group_handler }, + { "exec", exec_handler }, + { "args", args_handler }, + { "ip_max", ip_max_handler }, +#ifdef IPSEC + { "ipsec", ipsec_handler } +#endif +}; + +/* Error Not Initialized */ +#define ENI(key) ERR("Required option '%s' not specified", (key)) + +#define WAIT_WRN "Option 'wait' for internal service '%s' was inferred" + +/* Too Few Arguemnts (values) */ +#define TFA(key) ERR("Option '%s' has too few arguments", (key)) + +/* Too Many Arguments (values) */ +#define TMA(key) ERR("Option '%s' has too many arguments", (key)) + +/* Too Many Definitions */ +#define TMD(key) ERR("Option '%s' is already specified", (key)) + +#define VALID_SOCKET_TYPES "stream, dgram, rdm, seqpacket, raw" + +parse_v2_result +parse_syntax_v2(struct servtab *sep, char **cpp) +{ + + /* Catch multiple semantic errors instead of skipping after one */ + 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)) { + case INVOKE_SUCCESS: + /* Keep reading more options in. */ + continue; + case INVOKE_FINISH: + /* + * Found a semicolon, do final checks and defaults + * and return. + * Skip whitespace after semicolon to end of line. + */ + while (isspace((unsigned char)**cpp)) { + (*cpp)++; + } + + if (is_valid_definition && fill_default_values(sep)) { + if (**cpp == '\0') { + *cpp = nextline(fconfig); + } + return V2_SUCCESS; + } + + DPRINTCONF("Ignoring invalid definition."); + /* Log the error for the starting line of the service */ + syslog(LOG_ERR, CONF_ERROR_FMT + "Ignoring invalid definition.", CONFIG, + line_number_start); + if (**cpp == '\0') { + *cpp = nextline(fconfig); + } + return V2_SKIP; + case INVOKE_ERROR: + DPRINTCONF("Syntax error; Exiting '%s'", CONFIG); + return V2_ERROR; + } + } +} + +/* + * Fill in any remaining values that should be inferred + * Log an error if a required parameter that isn't + * provided by user can't be inferred from other servtab data. + * Return true on success, false on failure. + */ +static bool +fill_default_values(struct servtab *sep) +{ + bool is_valid = true; + + if (sep->se_service_max == SERVTAB_UNSPEC_SIZE_T) { + /* Set default to same as in v1 syntax. */ + sep->se_service_max = TOOMANY; + } + + if (sep->se_hostaddr == NULL) { + /* Set hostaddr to default */ + sep->se_hostaddr = newstr(defhost); + } + + try_infer_socktype(sep); + + if (sep->se_server == NULL) { + /* If an executable is not specified, assume internal. */ + is_valid = setup_internal(sep) && is_valid; + } + + if (sep->se_socktype == SERVTAB_UNSPEC_VAL) { + /* Ensure socktype is specified (either set or inferred) */ + ENI("socktype"); + is_valid = false; + } + + if (sep->se_wait == SERVTAB_UNSPEC_VAL) { + /* Ensure wait is specified */ + ENI("wait"); + is_valid = false; + } + + if (sep->se_user == NULL) { + /* Ensure user is specified */ + ENI("user"); + is_valid = false; + } + + if (sep->se_proto == NULL) { + /* Ensure protocol is specified */ + ENI("protocol"); + is_valid = false; + } else { + is_valid = infer_protocol_ip_version(sep) && is_valid; + } + +#ifdef IPSEC + setup_ipsec(sep); +#endif + return is_valid; +} + +/* fill_default_values related functions */ +#ifdef IPSEC +static void +setup_ipsec(struct servtab *sep) +{ + if (sep->se_policy == NULL) { + /* Set to default global policy */ + sep->se_policy = policy; + } else if (*sep->se_policy == '\0') { + /* IPsec was intentionally disabled. */ + free(sep->se_policy); + sep->se_policy = NULL; + } +} +#endif + +static void +try_infer_socktype(struct servtab *sep) { + if (sep->se_socktype != SERVTAB_UNSPEC_VAL || sep->se_proto == NULL) { + return; + } + + /* Check values of se_proto udp, udp6, tcp, tcp6 to set dgram/stream */ + if (strncmp(sep->se_proto, "udp", 3) == 0) { + sep->se_socktype = SOCK_DGRAM; + } else if (strncmp(sep->se_proto, "tcp", 3) == 0) { + sep->se_socktype = SOCK_STREAM; + } +} + +static bool +setup_internal(struct servtab *sep) +{ + pid_t wait_prev = sep->se_wait; + if (parse_server(sep, "internal") != 0) { + ENI("exec"); + return false; + } + + if (wait_prev != SERVTAB_UNSPEC_VAL && wait_prev != sep->se_wait) { + /* If wait was already specified throw an error. */ + WRN(WAIT_WRN, sep->se_service); + } + return true; +} + +static bool +infer_protocol_ip_version(struct servtab *sep) +{ + struct in_addr tmp; + + if (strcmp("tcp", sep->se_proto) != 0 + && strcmp("udp", sep->se_proto) != 0 + && strcmp("rpc/tcp", sep->se_proto) != 0 + && strcmp("rpc/udp", sep->se_proto) != 0) { + return true; + } + + if (inet_pton(AF_INET, sep->se_hostaddr, &tmp)) { + sep->se_family = AF_INET; + return true; + } + + if (inet_pton(AF_INET6, sep->se_hostaddr, &tmp)) { + sep->se_family = AF_INET6; + return true; + } + + ERR("Address family of %s is ambigous or invalid. " + "Explicitly specify protocol", sep->se_hostaddr); + return false; +} + +/* + * Skips whitespaces, newline characters, and comments, + * and returns the next token. Returns false and logs error if an EOF is + * encountered. + */ +static bool +skip_whitespace(char **cpp) +{ + char *cp = *cpp; + + int line_start = line_number; + + for (;;) { + while (isblank((unsigned char)*cp)) + cp++; + + if (*cp == '\0' || *cp == '#') { + cp = nextline(fconfig); + + /* Should never expect EOF when skipping whitespace */ + if (cp == NULL) { + ERR("Early end of file after line %d", + line_start); + return false; + } + continue; + } + break; + } + + *cpp = cp; + return true; +} + +/* Get the key handler function pointer for the given name */ +static key_handler_func +get_handler(char *name) +{ + /* Call function to handle option parsing. */ + for (size_t i = 0; i < __arraycount(key_handlers); i++) { + if (strcmp(key_handlers[i].name, name) == 0) { + return key_handlers[i].handler; + } + } + return NULL; +} + +static inline void +strmove(char *buf, size_t off) +{ + memmove(buf, buf + off, strlen(buf + off) + 1); +} + +/* + * Perform an in-place parse of a single-line quoted string + * with escape sequences. Sets *cpp to the position after the quoted characters. + * Uses shell-style quote parsing. + */ +static bool +parse_quotes(char **cpp) +{ + char *cp = *cpp; + char quote = *cp; + + strmove(cp, 1); + while (*cp && quote) { + if (*cp == quote) { + quote = '\0'; + strmove(cp, 1); + continue; + } + + if (*cp == '\\') { + /* start is location of backslash */ + char *start = cp; + cp++; + switch (*cp) { + case 'x': { + char temp, bits; + if (((bits = hex_to_bits(*(cp + 1))) == -1) + || ((temp = hex_to_bits(*(cp + 2))) == -1)) { + ERR("Invalid hexcode sequence '%.4s'", + start); + return false; + } + bits <<= 4; + bits |= temp; + *start = bits; + strmove(cp, 3); + continue; + } + case '\\': + *start = '\\'; + break; + case 'n': + *start = '\n'; + break; + case 't': + *start = '\t'; + break; + case 'r': + *start = '\r'; + break; + case '\'': + *start = '\''; + break; + case '"': + *start = '"'; + break; + case '\0': + ERR("Dangling escape sequence backslash"); + return false; + default: + ERR("Unknown escape sequence '\\%c'", *cp); + return false; + } + strmove(cp, 1); + continue; + } + + /* Regular character, advance to the next one. */ + cp++; + } + + if (*cp == '\0' && quote) { + ERR("Unclosed quote"); + return false; + } + *cpp = cp; + return true; +} + +char +hex_to_bits(char in) +{ + switch(in) { + case '0'...'9': + return in - '0'; + case 'a'...'f': + return in - 'a' + 10; + case 'A'...'F': + return in - 'A' + 10; + default: + return -1; + } +} + +/* + * Parse the next value for a key handler and advance list->cp past the found + * value. Return NULL if there are no more values or there was an error + * during parsing, and set the list->state to the appropriate value. + */ +static char * +next_value(vlist list) +{ + char *cp = list->cp; + + if (list->state != VALS_PARSING) { + /* Already at the end of a values list, or there was an error.*/ + return NULL; + } + + if (!skip_whitespace(&cp)) { + list->state = VALS_ERROR; + return NULL; + } + + if (*cp == ',' || *cp == ';') { + /* Found end of args, but not immediately after value */ + list->state = (*cp == ',' ? VALS_END_KEY : VALS_END_DEF); + list->cp = cp + 1; + return NULL; + } + + /* Check for end of line */ + if (!skip_whitespace(&cp)) { + list->state = VALS_ERROR; + return NULL; + } + + /* + * Found the start of a potential value. Advance one character + * past the end of the value. + */ + char * start = cp; + while (!isblank((unsigned char)*cp) && *cp != '#' && + *cp != ',' && *cp != ';' && *cp != '\0' ) { + if (*cp == '"' || *cp == '\'') { + /* Found a quoted segment */ + if (!parse_quotes(&cp)) { + list->state = VALS_ERROR; + return NULL; + } + } else { + /* Find the end of the value */ + cp++; + } + } + + /* Handle comments next to unquoted values */ + if (*cp == '#') { + *cp = '\0'; + list->cp = cp; + return start; + } + + if (*cp == '\0') { + /* + * Value ends with end of line, so it is already NUL-terminated + */ + list->cp = cp; + return start; + } + + if (*cp == ',') { + list->state = VALS_END_KEY; + } else if (*cp == ';') { + list->state = VALS_END_DEF; + } + + *cp = '\0'; + /* Advance past null so we don't skip the rest of the line */ + list->cp = cp + 1; + return start; +} + +/* Parse key name and invoke associated handler */ +static invoke_result +parse_invoke_handler(bool *is_valid_definition, char **cpp, struct servtab *sep) +{ + char *key_name, save, *cp = *cpp; + int is_blank; + key_handler_func handler; + val_parse_info info; + + /* Skip any whitespace if it exists, otherwise do nothing */ + if (!skip_whitespace(&cp)) { + return INVOKE_ERROR; + } + + /* Starting character of key */ + key_name = cp; + + + /* alphabetical or underscore allowed in name */ + while (isalpha((unsigned char)*cp) || *cp == '_') { + cp++; + } + + is_blank = isblank((unsigned char)*cp); + + /* Get key handler and move to start of values */ + if (*cp != '=' && !is_blank && *cp != '#') { + ERR("Expected '=' but found '%c'", *cp); + return INVOKE_ERROR; + } + + save = *cp; + *cp = '\0'; + cp++; + + handler = get_handler(key_name); + + if (handler == NULL) { + ERR("Unknown option '%s'", key_name); + handler = unknown_handler; + } + + /* If blank or new line, still need to find the '=' or throw error */ + if (is_blank || save == '#') { + if (save == '#') { + cp = nextline(fconfig); + } + + skip_whitespace(&cp); + if (*cp != '=') { + ERR("Expected '=' but found '%c'", *cp); + return INVOKE_ERROR; + } + cp++; + } + + /* Skip whitespace to start of values */ + if (!skip_whitespace(&cp)) { + return INVOKE_ERROR; + } + + info = (val_parse_info) {cp, VALS_PARSING}; + + /* + * Read values for key and write into sep. + * If parsing is successful, all values for key must be read. + */ + if (handler(sep, &info) == KEY_HANDLER_FAILURE) { + /* + * Eat remaining values if an error happened + * so more errors can be caught. + */ + while (next_value(&info) != NULL) + continue; + *is_valid_definition = false; + } + + if (info.state == VALS_END_DEF) { + /* + * Exit definition handling for(;;). + * Set the position to the end of the definition, + * for multi-definition lines. + */ + *cpp = info.cp; + return INVOKE_FINISH; + } + if (info.state == VALS_ERROR) { + /* Parse error, stop reading config */ + return INVOKE_ERROR; + } + + *cpp = info.cp; + return INVOKE_SUCCESS; +} + +/* Return true if sep must be a built-in service */ +static bool +is_internal(struct servtab *sep) +{ + return sep->se_bi != NULL; +} + +/* + * Key-values handlers + */ + +static hresult +unknown_handler(struct servtab *sep, vlist values) +{ + /* Return failure for an unknown service name. */ + return KEY_HANDLER_FAILURE; +} + +/* Set listen address for this service */ +static hresult +bind_handler(struct servtab *sep, vlist values) +{ + if (sep->se_hostaddr != NULL) { + TMD("bind"); + return KEY_HANDLER_FAILURE; + } + + char *val = next_value(values); + sep->se_hostaddr = newstr(val); + if (next_value(values) != NULL) { + TMA("bind"); + return KEY_HANDLER_FAILURE; + } + return KEY_HANDLER_SUCCESS; +} + +static hresult +socket_type_handler(struct servtab *sep, vlist values) +{ + char *type = next_value(values); + if (type == NULL) { + TFA("socktype"); + return KEY_HANDLER_FAILURE; + } + + parse_socktype(type, sep); + + if (sep->se_socktype == -1) { + ERR("Invalid socket type '%s'. Valid: " VALID_SOCKET_TYPES, + type); + return KEY_HANDLER_FAILURE; + } + + if (next_value(values) != NULL) { + TMA("socktype"); + return KEY_HANDLER_FAILURE; + } + + return KEY_HANDLER_SUCCESS; +} + +/* Set accept filter SO_ACCEPTFILTER */ +static hresult +filter_handler(struct servtab *sep, vlist values) +{ + /* + * See: SO_ACCEPTFILTER https://man.netbsd.org/setsockopt.2 + * An accept filter can have one other argument. + * This code currently only supports one accept filter + * Also see parse_accept_filter(char* arg, struct servtab*sep) + */ + + char *af_name, *af_arg; + + af_name = next_value(values); + + if (af_name == NULL) { + TFA("filter"); + return KEY_HANDLER_FAILURE; + } + + /* Store af_name in se_accf.af_name, no newstr call */ + strlcpy(sep->se_accf.af_name, af_name, sizeof(sep->se_accf.af_name)); + + af_arg = next_value(values); + + if (af_arg != NULL) { + strlcpy(sep->se_accf.af_arg, af_arg, + sizeof(sep->se_accf.af_arg)); + if (next_value(values) != NULL) { + TMA("filter"); + return KEY_HANDLER_FAILURE; + } + } else { + /* Store null string */ + sep->se_accf.af_arg[0] = '\0'; + } + + return KEY_HANDLER_SUCCESS; +} + +/* Set protocol (udp, tcp, unix, etc.) */ +static hresult +protocol_handler(struct servtab *sep, vlist values) +{ + char *val; + + if ((val = next_value(values)) == NULL) { + TFA("protocol"); + return KEY_HANDLER_FAILURE; + } + + if (sep->se_type == NORM_TYPE && + strncmp(val, "faith/", strlen("faith/")) == 0) { + val += strlen("faith/"); + sep->se_type = FAITH_TYPE; + } + sep->se_proto = newstr(val); + + if (parse_protocol(sep)) + return KEY_HANDLER_FAILURE; + + if ((val = next_value(values)) != NULL) { + TMA("protocol"); + return KEY_HANDLER_FAILURE; + } + return KEY_HANDLER_SUCCESS; +} + +/* + * Convert a string number possible ending with k or m to an integer. + * Based on MALFORMED, GETVAL, and ASSIGN in getconfigent(void). + */ +static int +size_to_bytes(char *arg) +{ + char *tail; + int rstatus, count; + + count = (int)strtoi(arg, &tail, 10, 0, INT_MAX, &rstatus); + + if (rstatus && rstatus != ENOTSUP) { + ERR("Invalid buffer size '%s': %s", arg, strerror(rstatus)); + return -1; + } + + switch(tail[0]) { + case 'm': + if (__builtin_smul_overflow((int)count, 1024, &count)) { + ERR("Invalid buffer size '%s': Result too large", arg); + return -1; + } + /* FALLTHROUGH */ + case 'k': + if (__builtin_smul_overflow((int)count, 1024, &count)) { + ERR("Invalid buffer size '%s': Result too large", arg); + return -1; + } + /* FALLTHROUGH */ + case '\0': + return count; + default: + ERR("Invalid buffer size unit prefix"); + return -1; + } +} + +/* sndbuf size */ +static hresult +send_buf_handler(struct servtab *sep, vlist values) +{ + char *arg; + int buffer_size; + + if (ISMUX(sep)) { + ERR("%s: can't specify buffer sizes for tcpmux services", + sep->se_service); + return KEY_HANDLER_FAILURE; + } + + + if ((arg = next_value(values)) == NULL) { + TFA("sndbuf"); + return KEY_HANDLER_FAILURE; + } + + buffer_size = size_to_bytes(arg); + + if (buffer_size == -1) { + return KEY_HANDLER_FAILURE; + } + + if ((arg = next_value(values)) != NULL) { + TMA("sndbuf"); + return KEY_HANDLER_FAILURE; + } + + sep->se_sndbuf = buffer_size; + + return KEY_HANDLER_SUCCESS; +} + +/* recvbuf size */ +static hresult +recv_buf_handler(struct servtab *sep, vlist values) +{ + char *arg; + int buffer_size; + + if (ISMUX(sep)) { + ERR("%s: Cannot specify buffer sizes for tcpmux services", + sep->se_service); + return KEY_HANDLER_FAILURE; + } + + if ((arg = next_value(values)) == NULL){ + TFA("recvbuf"); + return KEY_HANDLER_FAILURE; + } + + buffer_size = size_to_bytes(arg); + + if (buffer_size == -1) { + return KEY_HANDLER_FAILURE; + } + + if ((arg = next_value(values)) != NULL) { + TMA("recvbuf"); + return KEY_HANDLER_FAILURE; + } + + sep->se_rcvbuf = buffer_size; + + return KEY_HANDLER_SUCCESS; + +} + +/* Same as wait in positional */ +static hresult +wait_handler(struct servtab *sep, vlist values) +{ + char *val; + pid_t wait; + + /* If 'wait' is specified after internal exec */ + + if (!is_internal(sep) && sep->se_wait != SERVTAB_UNSPEC_VAL) { + /* Prevent duplicate wait keys */ + TMD("wait"); + return KEY_HANDLER_FAILURE; + } + + val = next_value(values); + + if (val == NULL) { + TFA("wait"); + return KEY_HANDLER_FAILURE; + } + + if (strcmp(val, "yes") == 0) { + wait = true; + } else if (strcmp(val, "no") == 0) { + wait = false; + } else { + ERR("Invalid value '%s' for wait. Valid: yes, no", val); + return KEY_HANDLER_FAILURE; + } + + if (is_internal(sep) && wait != sep->se_wait) { + /* If wait was set for internal service check for correctness */ + WRN(WAIT_WRN, sep->se_service); + } else if (parse_wait(sep, wait)) { + return KEY_HANDLER_FAILURE; + } + + if ((val = next_value(values)) != NULL) { + TMA("wait"); + return KEY_HANDLER_FAILURE; + } + + return KEY_HANDLER_SUCCESS; +} + +/* Set max connections in interval rate-limit, same as max in positional */ +static hresult +service_max_handler(struct servtab *sep, vlist values) +{ + char *count_str; + int rstatus; + + if (sep->se_service_max != SERVTAB_UNSPEC_SIZE_T) { + TMD("service_max"); + return KEY_HANDLER_FAILURE; + } + + count_str = next_value(values); + + if (count_str == NULL) { + TFA("service_max"); + return KEY_HANDLER_FAILURE; + } + + size_t count = (size_t)strtou(count_str, NULL, 10, 0, + SERVTAB_COUNT_MAX, &rstatus); + + if (rstatus) { + ERR("Invalid service_max '%s': %s", count_str, + strerror(rstatus)); + return KEY_HANDLER_FAILURE; + } + + if (next_value(values) != NULL) { + TMA("service_max"); + return KEY_HANDLER_FAILURE; + } + + sep->se_service_max = count; + + return KEY_HANDLER_SUCCESS; +} + +static hresult +ip_max_handler(struct servtab *sep, vlist values) +{ + char *count_str; + int rstatus; + + if (sep->se_ip_max != SERVTAB_UNSPEC_SIZE_T) { + TMD("ip_max"); + return KEY_HANDLER_FAILURE; + } + + count_str = next_value(values); + + if (count_str == NULL) { + TFA("ip_max"); + return KEY_HANDLER_FAILURE; + } + + size_t count = (size_t)strtou(count_str, NULL, 10, 0, + SERVTAB_COUNT_MAX, &rstatus); + + if (rstatus) { + ERR("Invalid ip_max '%s': %s", count_str, strerror(rstatus)); + return KEY_HANDLER_FAILURE; + } + + if (next_value(values) != NULL) { + TMA("ip_max"); + return KEY_HANDLER_FAILURE; + } + + sep->se_ip_max = count; + + return KEY_HANDLER_SUCCESS; +} + +/* Set user to execute as */ +static hresult +user_handler(struct servtab *sep, vlist values) +{ + if (sep->se_user != NULL) { + TMD("user"); + return KEY_HANDLER_FAILURE; + } + + char *name = next_value(values); + + if (name == NULL) { + TFA("user"); + return KEY_HANDLER_FAILURE; + } + + sep->se_user = newstr(name); + + if (next_value(values) != NULL) { + TMA("user"); + return KEY_HANDLER_FAILURE; + } + + return KEY_HANDLER_SUCCESS; +} + +/* Set group to execute as */ +static hresult +group_handler(struct servtab *sep, vlist values) +{ + char *name = next_value(values); + + if (name == NULL) { + TFA("group"); + return KEY_HANDLER_FAILURE; + } + + sep->se_group = newstr(name); + + if (next_value(values) != NULL) { + TMA("group"); + return KEY_HANDLER_FAILURE; + } + + return KEY_HANDLER_SUCCESS; +} + +/* Handle program path or "internal" */ +static hresult +exec_handler(struct servtab *sep, vlist values) +{ + char *val; + + if ((val = next_value(values)) == NULL) { + TFA("exec"); + return KEY_HANDLER_FAILURE; + } + + pid_t wait_prev = sep->se_wait; + if (parse_server(sep, val)) + return KEY_HANDLER_FAILURE; + if (is_internal(sep) && wait_prev != SERVTAB_UNSPEC_VAL) { + /* + * Warn if the user specifies a value for an internal which + * is different + */ + if (wait_prev != sep->se_wait) { + WRN(WAIT_WRN, sep->se_service); + } + } + + if ((val = next_value(values)) != NULL) { + TMA("exec"); + return KEY_HANDLER_FAILURE; + } + + return KEY_HANDLER_SUCCESS; +} + +/* Handle program arguments */ +static hresult +args_handler(struct servtab *sep, vlist values) +{ + char *val; + int argc; + + if (sep->se_argv[0] != NULL) { + TMD("args"); + return KEY_HANDLER_FAILURE; + } + + argc = 0; + for (val = next_value(values); val != NULL; val = next_value(values)) { + if (argc >= MAXARGV) { + ERR("Must be fewer than " TOSTRING(MAXARGV) + " arguments"); + return KEY_HANDLER_FAILURE; + } + sep->se_argv[argc++] = newstr(val); + } + while (argc <= MAXARGV) + sep->se_argv[argc++] = NULL; + + return KEY_HANDLER_SUCCESS; + +} + +#ifdef IPSEC +/* + * ipsec_handler currently uses the ipsec.h utilities for parsing, requiring + * all policies as a single value. This handler could potentially allow multiple + * policies as separate values in the future, but strings would need to be + * concatenated so the existing ipsec.h functions continue to work and policies + * can continue to be stored in sep->policy. + */ +static hresult +ipsec_handler(struct servtab *sep, vlist values) +{ + if (sep->se_policy != NULL) { + TMD("ipsec"); + return KEY_HANDLER_FAILURE; + } + + char *ipsecstr = next_value(values); + + if (ipsecstr != NULL && ipsecsetup_test(ipsecstr) < 0) { + ERR("IPsec policy '%s' is invalid", ipsecstr); + return KEY_HANDLER_FAILURE; + } + + /* + * Use 'ipsec=' with no argument to disable ipsec for this service + * An empty string indicates that IPsec was disabled, handled in + * fill_default_values. + */ + sep->se_policy = policy ? newstr(ipsecstr) : newstr(""); + + if (next_value(values) != NULL) { + TMA("ipsec"); + /* Currently only one semicolon separated string is allowed */ + return KEY_HANDLER_FAILURE; + } + + return KEY_HANDLER_SUCCESS; +} +#endif