From ded4b3926e15c0ba334c49233061a01261952927 Mon Sep 17 00:00:00 2001 From: Joachim Nilsson Date: Wed, 25 Feb 2015 10:17:10 +0100 Subject: [PATCH] Refactor inetd support, now with support for custom ports and iface filtering This patch adds support for custom port and interface filtering to inetd services, in a very condensed syntax: inetd ssh@eth0:222/tcp nowait [2345] /usr/sbin/sshd -i inetd ssh@eth1:22/tcp nowait [2345] /usr/sbin/sshd -i In this example eth0 and eth1 are allowed inbound interfaces for SSH connections, on port 222 and 22, respectively. Attempting to connect from any other interface is denied. Also, if the system is not in runlevel 2, 3, 4, or 5, ports 222 and 22 will not even be opened. If eth0 is your upstream interface you may want to avoid using the default port. To run ssh on port 222, and all others on port 22: inetd ssh@eth0:222/tcp nowait [2345] /usr/sbin/sshd -i inetd ssh/tcp nowait [2345] /usr/sbin/sshd -i This actually adds a deny rule for eth0 on ssh/tcp, implicitly. You can even list the services in the reverse order with the same result: inetd ssh/tcp nowait [2345] /usr/sbin/sshd -i inetd ssh@eth0:222/tcp nowait [2345] /usr/sbin/sshd -i There is no specific deny syntax available yet, see the TODO file for more details on how this can be implemented. Signed-off-by: Joachim Nilsson --- inetd.c | 321 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- inetd.h | 32 ++++++ svc.c | 150 +++++++++++++++----------- svc.h | 18 ++-- 4 files changed, 430 insertions(+), 91 deletions(-) diff --git a/inetd.c b/inetd.c index e3fb8211..49ef6818 100644 --- a/inetd.c +++ b/inetd.c @@ -26,20 +26,24 @@ #include #include +#include "libuev/uev.h" +#include "libite/lite.h" + +#include "inetd.h" #include "finit.h" #include "svc.h" #include "helpers.h" -#include "libuev/uev.h" + /* Socket callback, looks up correct svc and starts it as an inetd service */ static void socket_cb(uev_ctx_t *UNUSED(ctx), uev_t *w, void *arg, int UNUSED(events)) { svc_t *svc = (svc_t *)arg; - if (SVC_START != svc_enabled(svc, 1, NULL)) + if (SVC_START != svc_enabled(svc, -1, NULL)) return; - if (!svc->forking) + if (!svc->inetd.forking) uev_io_stop(w); svc_start(svc); @@ -54,32 +58,32 @@ static void spawn_socket(uev_ctx_t *ctx, svc_t *svc) socklen_t len = sizeof(struct sockaddr); struct sockaddr_in s; - if (!svc->sock_type) { + if (!svc->inetd.type) { FLOG_ERROR("Skipping invalid inetd service %s", svc->cmd); return; } - sd = socket(AF_INET, svc->sock_type | SOCK_NONBLOCK, svc->proto); + sd = socket(AF_INET, svc->inetd.type | SOCK_NONBLOCK, svc->inetd.proto); if (-1 == sd) { - FLOG_PERROR("Failed opening inetd socket type %d proto %d", svc->sock_type, svc->proto); + FLOG_PERROR("Failed opening inetd socket type %d proto %d", svc->inetd.type, svc->inetd.proto); return; } memset(&s, 0, sizeof(s)); s.sin_family = AF_INET; s.sin_addr.s_addr = INADDR_ANY; - s.sin_port = htons(svc->port); + s.sin_port = htons(svc->inetd.port); if (bind(sd, (struct sockaddr *)&s, len) < 0) { FLOG_PERROR("Failed binding to port %d, maybe another %s server is already running", - svc->port, svc->service); + svc->inetd.port, svc->inetd.name); close(sd); return; } - if (svc->port) { - if (svc->sock_type == SOCK_STREAM) { + if (svc->inetd.port) { + if (svc->inetd.type == SOCK_STREAM) { if (-1 == listen(sd, 20)) { - FLOG_PERROR("Failed listening to inetd service %s", svc->service); + FLOG_PERROR("Failed listening to inetd service %s", svc->inetd.name); close(sd); return; } @@ -93,8 +97,8 @@ static void spawn_socket(uev_ctx_t *ctx, svc_t *svc) } _d("Initializing inetd %s service %s type %d proto %d on socket %d ...", - svc->service, basename(svc->cmd), svc->sock_type, svc->proto, sd); - uev_io_init(ctx, &svc->watcher, socket_cb, svc, sd, UEV_READ); + svc->inetd.name, basename(svc->cmd), svc->inetd.type, svc->inetd.proto, sd); + uev_io_init(ctx, &svc->inetd.watcher, socket_cb, svc, sd, UEV_READ); } /* Peek into SOCK_DGRAM socket to figure out where an inbound packet comes from. */ @@ -162,13 +166,15 @@ int inetd_respawn(pid_t pid) svc_t *svc; for (svc = svc_iterator(1); svc; svc = svc_iterator(0)) { + inetd_t *inetd = &svc->inetd; + if (SVC_CMD_INETD != svc->type) continue; if (svc->pid == pid) { svc->pid = 0; - if (!svc->forking) - uev_io_set(&svc->watcher, svc->watcher.fd, UEV_READ); + if (!svc->inetd.forking) + uev_io_set(&inetd->watcher, inetd->watcher.fd, UEV_READ); return 1; /* It was us! */ } @@ -194,6 +200,291 @@ void inetd_runlevel(uev_ctx_t *ctx, int runlevel) } } +static int getent(char *service, char *proto, struct servent **sv, struct protoent **pv) +{ + *sv = getservbyname(service, proto); + if (!*sv) { + _pe("Invalid inetd %s/%s (service/proto), skipping", service, proto); + return errno = EINVAL; + } + + if (pv) { + *pv = getprotobyname((*sv)->s_proto); + if (!*pv) { + _pe("Cannot find proto %s, skipping.", (*sv)->s_proto); + return errno = EINVAL; + } + } + + return 0; +} + +/* + * Find exact match. + */ +inetd_filter_t *inetd_filter_find(inetd_t *inetd, char *ifname) +{ + inetd_filter_t *filter; + + if (!ifname) + ifname = ""; + + LIST_FOREACH(filter, &inetd->filters, link) { + _d("Checking filters for %s: '%s' vs '%s' (exact match) ...", + inetd->name, filter->ifname, ifname); + + if (!strcmp(filter->ifname, ifname)) + return filter; + } + + return NULL; +} + +/* + * First try exact match, then fall back to any match. + */ +inetd_filter_t *inetd_filter_match(inetd_t *inetd, char *ifname) +{ + inetd_filter_t *filter = inetd_filter_find(inetd, ifname); + + if (filter) + return filter; + + if (!ifname) + ifname = ""; + + LIST_FOREACH(filter, &inetd->filters, link) { + _d("Checking filters for %s: '%s' vs '%s' (any match) ...", + inetd->name, filter->ifname, ifname); + + if (!strlen(filter->ifname)) + return filter; + } + + return NULL; +} + +int inetd_allow_iface(inetd_t *inetd, char *ifname) +{ + inetd_filter_t *filter; + + if (!inetd) + return errno = EINVAL; + + filter = inetd_filter_match(inetd, ifname); + if (filter) { + _d("Filter %s for inetd %s already exists, skipping.", ifname ?: "*", inetd->name); + return 1; + } + + _d("Allow iface %s for service %s (port %d)", ifname ?: "*", inetd->name, inetd->port); + filter = calloc(1, sizeof(*filter)); + if (!filter) { + _e("Out of memory, cannot add filter to service %s", inetd->name); + return errno = ENOMEM; + } + + if (!ifname) + ifname = ""; + filter->deny = 0; + strlcpy(filter->ifname, ifname, sizeof(filter->ifname)); + LIST_INSERT_HEAD(&inetd->filters, filter, link); + + return 0; +} + +int inetd_deny_iface(inetd_t *inetd, char *ifname) +{ + inetd_filter_t *filter; + + if (!inetd) + return errno = EINVAL; + + /* Reset to NULL for debug output below */ + if (!ifname[0]) + ifname = NULL; + + filter = inetd_filter_find(inetd, ifname); + if (filter) { + _d("%s filter %s for inetd %s already exists, cannot set deny filter for same, skipping.", + filter->deny ? "Deny" : "Allow", ifname ?: "*", inetd->name); + return 1; + } + + _d("Deny iface %s for service %s (port %d)", ifname ?: "*", inetd->name, inetd->port); + filter = calloc(1, sizeof(*filter)); + if (!filter) { + _e("Out of memory, cannot add filter to service %s", inetd->name); + return errno = ENOMEM; + } + + if (!ifname) + ifname = ""; + filter->deny = 1; + strlcpy(filter->ifname, ifname, sizeof(filter->ifname)); + LIST_INSERT_HEAD(&inetd->filters, filter, link); + + return 0; +} + +int inetd_is_iface_allowed(inetd_t *inetd, char *ifname) +{ + inetd_filter_t *filter; + + if (!inetd) { + errno = EINVAL; + return 0; + } + + filter = inetd_filter_match(inetd, ifname); + if (filter) { + _d("Found matching filter for %s, deny: %d ... ", inetd->name, filter->deny); + return !filter->deny; + } + + _d("No matching filter for %s ... ", inetd->name); + + return 0; +} + +int inetd_match(inetd_t *inetd, char *service, char *proto, char *port) +{ + int cport = port ? atonum(port) : -1; + + if (!inetd || !service || !proto) + return errno = EINVAL; + + if (strncmp(inetd->name, service, sizeof(inetd->name))) + return 0; + + if (cport != -1) { + if (inetd->port == cport) + return 1; + } else { + struct servent *sv = NULL; + + if (getent(service, proto, &sv, NULL)) + return 0; + + if (inetd->port == ntohs(sv->s_port)) + return 1; + } + + return 0; +} + +static void deny_others(inetd_t *inetd) +{ + int first = 1; + svc_t *svc; + + while ((svc = svc_iterator(first))) { + inetd_filter_t *filter; + + first = 0; + + /* Skip non-inetd services */ + if (svc->type != SVC_CMD_INETD) + continue; + + /* Skip ourselves */ + if (&svc->inetd == inetd) + continue; + + /* Skip different service types (telnet != ssh) */ + if (strcmp(svc->inetd.name, inetd->name)) + continue; + + /* Deny all their interfaces from using my service */ + LIST_FOREACH(filter, &svc->inetd.filters, link) { + if (filter->deny) + continue; + + inetd_deny_iface(inetd, filter->ifname); + } + + /* Deny my interfaces from using their service ... */ + LIST_FOREACH(filter, &inetd->filters, link) { + if (filter->deny) + continue; + + inetd_deny_iface(&svc->inetd, filter->ifname); + } + } +} + + +/* + * This function is called to add a new, unique, inetd service. When an + * ifname is given as argument this means *only* run service on this + * interface. If a similar service runs on another port, this function + * must add this ifname as "deny" to that other service. + * + * Example: + * inetd ssh@eth0:222/tcp nowait [2345] /usr/sbin/sshd -i + * inetd ssh/tcp nowait [2345] /usr/sbin/sshd -i + * + * In this example eth0:222 is very specific, so when ssh/tcp (default) + * is added we must find the previous 'ssh' service and add its eth0 as + * deny, so we don't accept port 22 session on eth0. + * + * In the reverse case, where the default (ssh/tcp) entry is listed + * before the specific (eth0:222), the new inetd service must find the + * (any!) previous rule and add its ifname to their deny list. + * + * If equivalent service exists already svc_register() will instead call + * inetd_allow_iface(). + */ +int inetd_add(inetd_t *inetd, char *service, char *proto, char *ifname, char *port, int forking) +{ + int dport, cport = -1, result; + struct servent *sv = NULL; + struct protoent *pv = NULL; + + if (!inetd || !service || !proto) + return errno = EINVAL; + + result = getent(service, proto, &sv, &pv); + if (result) + return result; + + dport = ntohs(sv->s_port); + _d("Adding service %s (default port %d proto %s:%d custom port %s)", + service, dport, sv->s_proto, pv->p_proto, port ?: "N/A"); + + /* Save inetd service name */ + strlcpy(inetd->name, service, sizeof(inetd->name)); + + /* Naïve mapping tcp->stream, udp->dgram, other->dgram */ + if (!strcasecmp(sv->s_proto, "tcp")) + inetd->type = SOCK_STREAM; + else + inetd->type = SOCK_DGRAM; + + /* Check custom port */ + if (port) + cport = atonum(port); + + /* Compare with default/standard port */ + if (cport == -1 || cport == dport) { + inetd->std = 1; + inetd->port = dport; + } else { + inetd->std = 0; + inetd->port = cport; + } + + inetd->forking = !!forking; + inetd->proto = pv->p_proto; + + /* Poor man's tcpwrappers filtering */ + result = inetd_allow_iface(inetd, ifname); + + /* For each similar service, on other port, add their ifnames as deny to ours. */ + deny_others(inetd); + + return result; +} /** * Local Variables: diff --git a/inetd.h b/inetd.h index 8559b1dd..48c11b56 100644 --- a/inetd.h +++ b/inetd.h @@ -24,14 +24,46 @@ #ifndef FINIT_INETD_H_ #define FINIT_INETD_H_ +#include +#include + +#include "queue.h" #include "libuev/uev.h" + +typedef struct inetd_filter { + LIST_ENTRY(inetd_filter) link; + int deny; /* 0:allow, 1:deny */ + char ifname[IFNAMSIZ]; /* E.g., eth0 */ +} inetd_filter_t; + +typedef struct { + uev_t watcher; + + int type; /* Socket type: SOCK_STREAM/SOCK_DGRAM */ + int std; /* Standard proto/port from /etc/services */ + int proto; + int port; + int forking; + char name[10]; + int (*cmd)(int type); /* internal inetd service, like 'time' */ + + LIST_HEAD(, inetd_filter) filters; +} inetd_t; + int inetd_dgram_peek (int sd, char *ifname); int inetd_stream_peek (int sd, char *ifname); int inetd_respawn (pid_t pid); void inetd_runlevel(uev_ctx_t *ctx, int runlevel); +int inetd_match (inetd_t *inetd, char *service, char *proto, char *port); +int inetd_add (inetd_t *inetd, char *service, char *proto, char *ifname, char *port, int forking); + +inetd_filter_t *inetd_filter_find (inetd_t *inetd, char *ifname); +int inetd_allow_iface (inetd_t *inetd, char *ifname); +int inetd_is_iface_allowed (inetd_t *inetd, char *ifname); + #endif /* FINIT_INETD_H_ */ /** diff --git a/svc.c b/svc.c index 69e97979..ad274eea 100644 --- a/svc.c +++ b/svc.c @@ -25,7 +25,6 @@ #include #include #include -#include #include #include "finit.h" @@ -81,30 +80,6 @@ svc_t *svc_new(void) return svc; } -/** - * svc_find - Find a service object by its full path name - * @name: Full path name, e.g., /sbin/syslogd - * - * Returns: - * A pointer to an &svc_t object, or %NULL if not found. - */ -svc_t *svc_find(char *name) -{ - int i; - - __connect_shm(); - for (i = 0; i < svc_counter; i++) { - svc_t *svc = &services[i]; - - if (!strncmp(name, svc->cmd, strlen(svc->cmd))) { - _d("Found a matching svc for %s", name); - return svc; - } - } - - return NULL; -} - /** * svc_iterator - Iterates over all registered services. * @restart: Get first &svc_t object by setting @restart @@ -127,6 +102,53 @@ svc_t *svc_iterator(int restart) return &services[i++]; } +/** + * svc_find - Find a service object by its full path name + * @path: Full path name, e.g., /sbin/syslogd + * + * Returns: + * A pointer to an &svc_t object, or %NULL if not found. + */ +svc_t *svc_find(char *path) +{ + int first = 1; + svc_t *svc; + + while ((svc = svc_iterator(first))) { + first = 0; + + if (!strncmp(path, svc->cmd, strlen(svc->cmd))) { + _d("Found a matching svc for %s", path); + return svc; + } + } + + return NULL; +} + +svc_t *svc_find_inetd(char *path, char *service, char *proto, char *port) +{ + int first = 1; + svc_t *svc; + + while ((svc = svc_iterator(first))) { + first = 0; + + if (svc->type != SVC_CMD_INETD) + continue; + + if (strncmp(path, svc->cmd, strlen(svc->cmd))) + continue; + + if (inetd_match(&svc->inetd, service, proto, port)) { + _d("Found a matching svc for %s", path); + return svc; + } + } + + return NULL; +} + /** * svc_bootstrap - Start bootstrap services and tasks * @@ -262,12 +284,10 @@ void svc_runlevel(int newlevel) int svc_register(int type, char *line, char *username) { int i = 0, forking = 0; - char *service = NULL, *proto = NULL; + char *service = NULL, *proto = NULL, *iface = NULL, *port = NULL; char *cmd, *desc, *runlevels = NULL; svc_t *svc; plugin_t *plugin = NULL; - struct servent *sv = NULL; - struct protoent *pv = NULL; if (!line) { _e("Invalid input argument."); @@ -287,7 +307,7 @@ int svc_register(int type, char *line, char *username) while (cmd) { if (cmd[0] != '/' && strchr(cmd, '/')) - service = cmd; /* inetd port/proto */ + service = cmd; /* inetd service/proto */ else if (!strncasecmp(cmd, "nowait", 6)) forking = 1; else if (!strncasecmp(cmd, "wait", 4)) @@ -305,22 +325,18 @@ int svc_register(int type, char *line, char *username) goto incomplete; } + /* Example: inetd ssh@eth0:222/tcp */ if (service) { proto = strchr(service, '/'); *proto++ = 0; - sv = getservbyname(service, proto); - if (!sv) { - _pe("Invalid inetd %s/%s (service/proto), skipping", service, proto); - return errno = EINVAL; - } - pv = getprotobyname(sv->s_proto); - if (!pv) { - _pe("Cannot find proto %s, skipping.", sv->s_proto); - return errno = EINVAL; - } + port = strchr(service, ':'); + if (port) + *port++ = 0; - _d("Got service %s:%d proto %s:%d", service, ntohs(sv->s_port), sv->s_proto, pv->p_proto); + iface = strchr(service, '@'); + if (iface) + *iface++ = 0; } /* Find plugin that provides a callback for this inetd service */ @@ -332,6 +348,11 @@ int svc_register(int type, char *line, char *username) } } + /* Check if known inetd, then add ifname for filtering only. */ + svc = svc_find_inetd(cmd, service, proto, port); + if (svc && type == SVC_CMD_INETD) + return inetd_allow_iface(&svc->inetd, iface); + svc = svc_new(); if (!svc) { _e("Out of memory, cannot register service %s", cmd); @@ -353,23 +374,12 @@ int svc_register(int type, char *line, char *username) strlcpy(svc->username, username, sizeof(svc->username)); } - if (sv) { - strlcpy(svc->service, service, sizeof(svc->service)); - svc->port = ntohs(sv->s_port); - svc->proto = pv->p_proto; - - /* Naïve mapping tcp->stream, udp->dgram, other->dgram */ - if (!strcasecmp(sv->s_proto, "tcp")) - svc->sock_type = SOCK_STREAM; - else - svc->sock_type = SOCK_DGRAM; - - svc->forking = forking; - } + if (svc->type == SVC_CMD_INETD) + inetd_add(&svc->inetd, service, proto, iface, port, forking); if (plugin) { /* Internal plugin provides this service */ - svc->internal_cmd = plugin->inetd.cmd; + svc->inetd.cmd = plugin->inetd.cmd; } else { /* External program to call */ strlcpy(svc->cmd, cmd, sizeof(svc->cmd)); @@ -550,7 +560,7 @@ int svc_start(svc_t *svc) respawn = svc->pid != 0; /* Don't try and start service if it doesn't exist. */ - if (!fexist(svc->cmd) && !svc->internal_cmd) { + if (!fexist(svc->cmd) && !svc->inetd.cmd) { char msg[80]; snprintf(msg, sizeof(msg), "Service %s does not exist!", svc->cmd); @@ -565,17 +575,17 @@ int svc_start(svc_t *svc) return 0; if (SVC_CMD_INETD == svc->type) { - sd = svc->watcher.fd; + sd = svc->inetd.watcher.fd; - if (svc->sock_type == SOCK_STREAM) { + if (svc->inetd.type == SOCK_STREAM) { /* Open new client socket from server socket */ sd = accept(sd, NULL, NULL); if (sd < 0) { - FLOG_PERROR("Failed accepting inetd service %d/tcp", svc->port); + FLOG_PERROR("Failed accepting inetd service %d/tcp", svc->inetd.port); return 0; } - _d("New client socket %d accepted for inetd service %d/tcp", sd, svc->port); + _d("New client socket %d accepted for inetd service %d/tcp", sd, svc->inetd.port); /* Find ifname by means of getsockname() and getifaddrs() */ inetd_stream_peek(sd, ifname); @@ -584,9 +594,16 @@ int svc_start(svc_t *svc) inetd_dgram_peek(sd, ifname); } - /* XXX: Add poor man's tcpwrappers here, we now know inbound ifname ... */ + if (!inetd_is_iface_allowed(&svc->inetd, ifname)) { + FLOG_INFO("Service %s on port %d not allowed from interface %s.", + svc->inetd.name, svc->inetd.port, ifname); + if (svc->inetd.type == SOCK_STREAM) + close(sd); - FLOG_INFO("Starting inetd service %s for requst from iface %s ...", svc->service, ifname); + return 0; + } + + FLOG_INFO("Starting inetd service %s for requst from iface %s ...", svc->inetd.name, ifname); } else if (SVC_CMD_SERVICE != svc->type) print_desc("", svc->desc); @@ -652,17 +669,22 @@ int svc_start(svc_t *svc) _e("%starting %s: %s", respawn ? "Res" : "S", svc->cmd, buf); } - if (svc->internal_cmd) - status = svc->internal_cmd(svc->sock_type); + if (svc->inetd.cmd) + status = svc->inetd.cmd(svc->inetd.type); else status = execv(svc->cmd, args); /* XXX: Maybe use execve() to be able to launch scripts? */ + if (SVC_CMD_INETD == svc->type) { + if (svc->inetd.type == SOCK_STREAM) + close(sd); + } + exit(status); } svc->pid = pid; if (SVC_CMD_INETD == svc->type) { - if (svc->sock_type == SOCK_STREAM) + if (svc->inetd.type == SOCK_STREAM) close(sd); } else if (SVC_CMD_RUN == svc->type) diff --git a/svc.h b/svc.h index 83afceb8..17ea5d8c 100644 --- a/svc.h +++ b/svc.h @@ -31,6 +31,7 @@ #include /* shmat() */ #include /* pid_t */ +#include "inetd.h" #include "queue.h" /* BSD sys/queue.h API */ #include "libuev/uev.h" @@ -64,20 +65,12 @@ typedef struct svc { int reload; int runlevels; - /* Event loop handler, used for inetd services */ - uev_t watcher; - - /* For inetd services */ - int sock_type; - int port; - int proto; - int forking; - char service[10]; - int (*internal_cmd)(int sock_type); - /* Incremented for each restart by service monitor. */ unsigned int restart_counter; + /* For inetd services */ + inetd_t inetd; + /* Identity */ char username[MAX_USER_LEN]; char group[MAX_USER_LEN]; @@ -116,7 +109,8 @@ static inline svc_t *finit_svc_connect(void) } svc_t *svc_new (void); -svc_t *svc_find (char *name); +svc_t *svc_find (char *path); +svc_t *svc_find_inetd (char *path, char *service, char *proto, char *port); svc_t *svc_iterator (int restart); void svc_runlevel (int newlevel);