2bf7306bb9
broken before because of too-strict filters. Move setup_signals under the seccomp filter to give it more testing coverage. Make the UDP datagram length check much more strict. If the read buffer does not match up with the header lengths exactly, it is discarded. Print a warning to syslog/stdout when ifchd execute_buffer() returns an error. Fix a regression introduced in ifchd that would cause the epoll handler to spin when a client connection closed.
952 lines
27 KiB
C
952 lines
27 KiB
C
/* ifchd.c - interface change daemon
|
|
*
|
|
* Copyright (c) 2004-2012 Nicholas J. Kain <njkain at gmail dot com>
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are met:
|
|
*
|
|
* - Redistributions of source code must retain the above copyright notice,
|
|
* this list of conditions and the following disclaimer.
|
|
*
|
|
* - 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 COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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 <unistd.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/un.h>
|
|
#include <sys/time.h>
|
|
#include <sys/types.h>
|
|
#include <sys/epoll.h>
|
|
#include <sys/signalfd.h>
|
|
#include <sys/stat.h>
|
|
#include <fcntl.h>
|
|
|
|
#include <time.h>
|
|
#include <pwd.h>
|
|
#include <grp.h>
|
|
|
|
#include <signal.h>
|
|
#include <errno.h>
|
|
|
|
#include <getopt.h>
|
|
|
|
#include "ifchd-defines.h"
|
|
#include "log.h"
|
|
#include "chroot.h"
|
|
#include "pidfile.h"
|
|
#include "signals.h"
|
|
#include "ifch_proto.h"
|
|
#include "strl.h"
|
|
#include "cap.h"
|
|
#include "io.h"
|
|
#include "linux.h"
|
|
#include "seccomp-bpf.h"
|
|
|
|
enum states {
|
|
STATE_NOTHING,
|
|
STATE_INTERFACE,
|
|
STATE_IP,
|
|
STATE_SUBNET,
|
|
STATE_TIMEZONE,
|
|
STATE_ROUTER,
|
|
STATE_DNS,
|
|
STATE_LPRSVR,
|
|
STATE_HOSTNAME,
|
|
STATE_DOMAIN,
|
|
STATE_IPTTL,
|
|
STATE_MTU,
|
|
STATE_BROADCAST,
|
|
STATE_NTPSVR,
|
|
STATE_WINS
|
|
};
|
|
|
|
struct ifchd_client clients[SOCK_QUEUE];
|
|
|
|
static int epollfd, signalFd;
|
|
/* Extra two event slots are for signalFd and the listen socket. */
|
|
static struct epoll_event events[SOCK_QUEUE+2];
|
|
|
|
int resolv_conf_fd = -1;
|
|
/* int ntp_conf_fd = -1; */
|
|
|
|
/* If true, allow HOSTNAME changes from dhcp server. */
|
|
int allow_hostname = 0;
|
|
|
|
static uid_t peer_uid;
|
|
static gid_t peer_gid;
|
|
static pid_t peer_pid;
|
|
|
|
static int gflags_verbose = 0;
|
|
|
|
static void writeordie(int fd, const char *buf, int len)
|
|
{
|
|
if (safe_write(fd, buf, len) == -1)
|
|
suicide("write returned error");
|
|
}
|
|
|
|
static void epoll_add(int fd)
|
|
{
|
|
struct epoll_event ev;
|
|
int r;
|
|
ev.events = EPOLLIN | EPOLLRDHUP | EPOLLERR | EPOLLHUP;
|
|
ev.data.fd = fd;
|
|
r = epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev);
|
|
if (r == -1)
|
|
suicide("epoll_add failed %s", strerror(errno));
|
|
}
|
|
|
|
static void epoll_del(int fd)
|
|
{
|
|
struct epoll_event ev;
|
|
int r;
|
|
ev.events = EPOLLIN | EPOLLRDHUP | EPOLLERR | EPOLLHUP;
|
|
ev.data.fd = fd;
|
|
r = epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, &ev);
|
|
if (r == -1)
|
|
suicide("epoll_del failed %s", strerror(errno));
|
|
}
|
|
|
|
static int enforce_seccomp(void)
|
|
{
|
|
struct sock_filter filter[] = {
|
|
VALIDATE_ARCHITECTURE,
|
|
EXAMINE_SYSCALL,
|
|
ALLOW_SYSCALL(read),
|
|
ALLOW_SYSCALL(write),
|
|
ALLOW_SYSCALL(sendto), // used for glibc syslog routines
|
|
ALLOW_SYSCALL(epoll_wait),
|
|
ALLOW_SYSCALL(epoll_ctl),
|
|
ALLOW_SYSCALL(close),
|
|
ALLOW_SYSCALL(socket),
|
|
ALLOW_SYSCALL(getsockopt),
|
|
ALLOW_SYSCALL(accept),
|
|
ALLOW_SYSCALL(listen),
|
|
ALLOW_SYSCALL(ioctl),
|
|
ALLOW_SYSCALL(fsync),
|
|
ALLOW_SYSCALL(lseek),
|
|
ALLOW_SYSCALL(truncate),
|
|
ALLOW_SYSCALL(fcntl),
|
|
ALLOW_SYSCALL(unlink),
|
|
ALLOW_SYSCALL(bind),
|
|
ALLOW_SYSCALL(chmod),
|
|
|
|
ALLOW_SYSCALL(rt_sigreturn),
|
|
#ifdef __NR_sigreturn
|
|
ALLOW_SYSCALL(sigreturn),
|
|
#endif
|
|
ALLOW_SYSCALL(exit_group),
|
|
ALLOW_SYSCALL(exit),
|
|
KILL_PROCESS,
|
|
};
|
|
struct sock_fprog prog = {
|
|
.len = (unsigned short)(sizeof filter / sizeof filter[0]),
|
|
.filter = filter,
|
|
};
|
|
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
|
|
return -1;
|
|
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog))
|
|
return -1;
|
|
return 0;
|
|
}
|
|
|
|
/* Writes a new resolv.conf based on the information we have received. */
|
|
static void write_resolve_conf(struct ifchd_client *cl)
|
|
{
|
|
const static char ns_str[] = "nameserver ";
|
|
const static char dom_str[] = "domain ";
|
|
const static char srch_str[] = "search ";
|
|
int r;
|
|
off_t off;
|
|
char buf[MAX_BUF];
|
|
|
|
if (resolv_conf_fd == -1)
|
|
return;
|
|
if (strlen(cl->namesvrs) == 0)
|
|
return;
|
|
|
|
if (lseek(resolv_conf_fd, 0, SEEK_SET) == -1)
|
|
return;
|
|
|
|
char *p = cl->namesvrs;
|
|
while (p && (*p != '\0')) {
|
|
char *q = strchr(p, ' ');
|
|
if (!q)
|
|
q = strchr(p, '\0');
|
|
else
|
|
*q++ = '\0';
|
|
strlcpy(buf, p, sizeof buf);
|
|
|
|
writeordie(resolv_conf_fd, ns_str, strlen(ns_str));
|
|
writeordie(resolv_conf_fd, buf, strlen(buf));
|
|
writeordie(resolv_conf_fd, "\n", 1);
|
|
|
|
p = q;
|
|
}
|
|
|
|
p = cl->domains;
|
|
int numdoms = 0;
|
|
while (p && (*p != '\0')) {
|
|
char *q = strchr(p, ' ');
|
|
if (!q)
|
|
q = strchr(p, '\0');
|
|
else
|
|
*q++ = '\0';
|
|
strlcpy(buf, p, sizeof buf);
|
|
|
|
if (numdoms == 0) {
|
|
writeordie(resolv_conf_fd, dom_str, strlen(dom_str));
|
|
writeordie(resolv_conf_fd, buf, strlen(buf));
|
|
} else {
|
|
if (numdoms == 1) {
|
|
writeordie(resolv_conf_fd, "\n", 1);
|
|
writeordie(resolv_conf_fd, srch_str, strlen(srch_str));
|
|
writeordie(resolv_conf_fd, buf, strlen(buf));
|
|
} else {
|
|
writeordie(resolv_conf_fd, " ", 1);
|
|
writeordie(resolv_conf_fd, buf, strlen(buf));
|
|
}
|
|
}
|
|
|
|
++numdoms;
|
|
p = q;
|
|
if (numdoms > 6)
|
|
break;
|
|
}
|
|
writeordie(resolv_conf_fd, "\n", 1);
|
|
|
|
off = lseek(resolv_conf_fd, 0, SEEK_CUR);
|
|
if (off == -1) {
|
|
log_line("write_resolve_conf: lseek returned error: %s\n",
|
|
strerror(errno));
|
|
return;
|
|
}
|
|
retry:
|
|
r = ftruncate(resolv_conf_fd, off);
|
|
if (r == -1) {
|
|
if (errno == EINTR)
|
|
goto retry;
|
|
log_line("write_resolve_conf: ftruncate returned error: %s\n",
|
|
strerror(errno));
|
|
return;
|
|
}
|
|
r = fsync(resolv_conf_fd);
|
|
if (r == -1) {
|
|
log_line("write_resolve_conf: fsync returned error: %s\n",
|
|
strerror(errno));
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* XXX: addme */
|
|
static void perform_timezone(struct ifchd_client *cl, char *str)
|
|
{}
|
|
|
|
/* Add a dns server to the /etc/resolv.conf -- we already have a fd. */
|
|
static void perform_dns(struct ifchd_client *cl, char *str)
|
|
{
|
|
if (!str || resolv_conf_fd == -1)
|
|
return;
|
|
strlcpy(cl->namesvrs, str, MAX_BUF);
|
|
write_resolve_conf(cl);
|
|
}
|
|
|
|
/* Updates for print daemons are too non-standard to be useful. */
|
|
static void perform_lprsvr(struct ifchd_client *cl, char *str)
|
|
{}
|
|
|
|
/* Sets machine hostname. */
|
|
static void perform_hostname(struct ifchd_client *cl, char *str)
|
|
{
|
|
if (!allow_hostname || !str)
|
|
return;
|
|
if (sethostname(str, strlen(str) + 1) == -1)
|
|
log_line("sethostname returned %s\n", strerror(errno));
|
|
}
|
|
|
|
/* update "domain" and "search" in /etc/resolv.conf */
|
|
static void perform_domain(struct ifchd_client *cl, char *str)
|
|
{
|
|
if (!str || resolv_conf_fd == -1)
|
|
return;
|
|
strlcpy(cl->domains, str, MAX_BUF);
|
|
write_resolve_conf(cl);
|
|
}
|
|
|
|
/* I don't think this can be done without a netfilter extension
|
|
* that isn't in the mainline kernels. */
|
|
static void perform_ipttl(struct ifchd_client *cl, char *str)
|
|
{}
|
|
|
|
/* XXX: addme */
|
|
static void perform_ntpsrv(struct ifchd_client *cl, char *str)
|
|
{}
|
|
|
|
/* Maybe Samba cares about this feature? I don't know. */
|
|
static void perform_wins(struct ifchd_client *cl, char *str)
|
|
{}
|
|
|
|
static void ifchd_client_init(struct ifchd_client *p)
|
|
{
|
|
p->fd = -1;
|
|
p->idle_time = time(NULL);
|
|
p->state = STATE_NOTHING;
|
|
|
|
memset(p->ibuf, 0, sizeof p->ibuf);
|
|
memset(p->ifnam, 0, sizeof p->ifnam);
|
|
memset(p->namesvrs, 0, sizeof p->namesvrs);
|
|
memset(p->domains, 0, sizeof p->domains);
|
|
}
|
|
|
|
static void ifchd_client_wipe(struct ifchd_client *p)
|
|
{
|
|
if (p->fd >= 0) {
|
|
epoll_del(p->fd);
|
|
close(p->fd);
|
|
}
|
|
ifchd_client_init(p);
|
|
}
|
|
|
|
static void ifchd_client_new(struct ifchd_client *p, int fd)
|
|
{
|
|
ifchd_client_wipe(p);
|
|
p->fd = fd;
|
|
epoll_add(fd);
|
|
}
|
|
|
|
/* Conditionally accepts a new connection and initializes data structures. */
|
|
static void add_sk(int sk)
|
|
{
|
|
int i;
|
|
|
|
if (authorized_peer(sk, peer_pid, peer_uid, peer_gid)) {
|
|
for (i = 0; i < SOCK_QUEUE; i++) {
|
|
struct ifchd_client *p = &clients[i];
|
|
if (p->fd == -1) {
|
|
ifchd_client_new(p, sk);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
close(sk);
|
|
}
|
|
|
|
/* Closes idle connections. */
|
|
static void close_idle_sk(void)
|
|
{
|
|
int i;
|
|
|
|
for (i=0; i<SOCK_QUEUE; i++) {
|
|
struct ifchd_client *p = &clients[i];
|
|
if (p->fd == -1)
|
|
continue;
|
|
if (time(NULL) - p->idle_time > CONN_TIMEOUT)
|
|
ifchd_client_wipe(p);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Returns -1 on fatal error.
|
|
*/
|
|
static int execute_buffer(struct ifchd_client *cl, char *newbuf)
|
|
{
|
|
char buf[MAX_BUF * 2];
|
|
char *p = buf, *endp;
|
|
|
|
memset(buf, 0, sizeof buf);
|
|
strlcat(buf, cl->ibuf, sizeof buf);
|
|
strlcat(buf, newbuf, sizeof buf);
|
|
|
|
for (;;) {
|
|
endp = p;
|
|
if (cl->state == STATE_NOTHING) {
|
|
char *colon = strchr(p, ':');
|
|
if (!colon)
|
|
break;
|
|
char *semi = strchr(p, ';');
|
|
if (semi && semi < colon) {
|
|
log_line("bad syntax: STATE_NOTHING and a ';' came before ':'");
|
|
return -1;
|
|
}
|
|
*colon = '\0';
|
|
endp = colon + 1;
|
|
} else {
|
|
char *semi = strchr(p, ';');
|
|
if (!semi)
|
|
break;
|
|
char *colon = strchr(p, ':');
|
|
if (colon && colon < semi) {
|
|
log_line("bad syntax: !STATE_NOTHING and a ':' came before ';'");
|
|
return -1;
|
|
}
|
|
*semi = '\0';
|
|
endp = semi + 1;
|
|
}
|
|
|
|
switch (cl->state) {
|
|
case STATE_NOTHING:
|
|
if (strncmp(p, CMD_INTERFACE, sizeof(CMD_INTERFACE)) == 0)
|
|
cl->state = STATE_INTERFACE;
|
|
if (strncmp(p, CMD_IP, sizeof(CMD_IP)) == 0)
|
|
cl->state = STATE_IP;
|
|
if (strncmp(p, CMD_SUBNET, sizeof(CMD_SUBNET)) == 0)
|
|
cl->state = STATE_SUBNET;
|
|
if (strncmp(p, CMD_TIMEZONE, sizeof(CMD_TIMEZONE)) == 0)
|
|
cl->state = STATE_TIMEZONE;
|
|
if (strncmp(p, CMD_ROUTER, sizeof(CMD_ROUTER)) == 0)
|
|
cl->state = STATE_ROUTER;
|
|
if (strncmp(p, CMD_DNS, sizeof(CMD_DNS)) == 0)
|
|
cl->state = STATE_DNS;
|
|
if (strncmp(p, CMD_LPRSVR, sizeof(CMD_LPRSVR)) == 0)
|
|
cl->state = STATE_LPRSVR;
|
|
if (strncmp(p, CMD_HOSTNAME, sizeof(CMD_HOSTNAME)) == 0)
|
|
cl->state = STATE_HOSTNAME;
|
|
if (strncmp(p, CMD_DOMAIN, sizeof(CMD_DOMAIN)) == 0)
|
|
cl->state = STATE_DOMAIN;
|
|
if (strncmp(p, CMD_IPTTL, sizeof(CMD_IPTTL)) == 0)
|
|
cl->state = STATE_IPTTL;
|
|
if (strncmp(p, CMD_MTU, sizeof(CMD_MTU)) == 0)
|
|
cl->state = STATE_MTU;
|
|
if (strncmp(p, CMD_BROADCAST, sizeof(CMD_BROADCAST)) == 0)
|
|
cl->state = STATE_BROADCAST;
|
|
if (strncmp(p, CMD_NTPSVR, sizeof(CMD_NTPSVR)) == 0)
|
|
cl->state = STATE_NTPSVR;
|
|
if (strncmp(p, CMD_WINS, sizeof(CMD_WINS)) == 0)
|
|
cl->state = STATE_WINS;
|
|
break;
|
|
|
|
case STATE_INTERFACE:
|
|
perform_interface(cl, p);
|
|
cl->state = STATE_NOTHING;
|
|
break;
|
|
|
|
case STATE_IP:
|
|
perform_ip(cl, p);
|
|
cl->state = STATE_NOTHING;
|
|
break;
|
|
|
|
case STATE_SUBNET:
|
|
perform_subnet(cl, p);
|
|
cl->state = STATE_NOTHING;
|
|
break;
|
|
|
|
case STATE_TIMEZONE:
|
|
perform_timezone(cl, p);
|
|
cl->state = STATE_NOTHING;
|
|
break;
|
|
|
|
case STATE_ROUTER:
|
|
perform_router(cl, p);
|
|
cl->state = STATE_NOTHING;
|
|
break;
|
|
|
|
case STATE_DNS:
|
|
perform_dns(cl, p);
|
|
cl->state = STATE_NOTHING;
|
|
break;
|
|
|
|
case STATE_LPRSVR:
|
|
perform_lprsvr(cl, p);
|
|
cl->state = STATE_NOTHING;
|
|
break;
|
|
|
|
case STATE_HOSTNAME:
|
|
perform_hostname(cl, p);
|
|
cl->state = STATE_NOTHING;
|
|
break;
|
|
|
|
case STATE_DOMAIN:
|
|
perform_domain(cl, p);
|
|
cl->state = STATE_NOTHING;
|
|
break;
|
|
|
|
case STATE_IPTTL:
|
|
perform_ipttl(cl, p);
|
|
cl->state = STATE_NOTHING;
|
|
break;
|
|
|
|
case STATE_MTU:
|
|
perform_mtu(cl, p);
|
|
cl->state = STATE_NOTHING;
|
|
break;
|
|
|
|
case STATE_BROADCAST:
|
|
perform_broadcast(cl, p);
|
|
cl->state = STATE_NOTHING;
|
|
break;
|
|
|
|
case STATE_NTPSVR:
|
|
perform_ntpsrv(cl, p);
|
|
cl->state = STATE_NOTHING;
|
|
break;
|
|
|
|
case STATE_WINS:
|
|
perform_wins(cl, p);
|
|
cl->state = STATE_NOTHING;
|
|
break;
|
|
|
|
default:
|
|
log_line("warning: invalid state in dispatch_work\n");
|
|
break;
|
|
}
|
|
p = endp;
|
|
}
|
|
size_t remsize = strlen(endp);
|
|
if (remsize > MAX_BUF - 1)
|
|
return -1;
|
|
strlcpy(cl->ibuf, endp, MAX_BUF);
|
|
return 0;
|
|
}
|
|
|
|
/* Opens a non-blocking listening socket with the appropriate properties. */
|
|
static int get_listen(void)
|
|
{
|
|
int lsock, ret;
|
|
static const struct sockaddr_un lsock_addr =
|
|
{
|
|
.sun_family = AF_UNIX,
|
|
.sun_path = "/var/state/ifchange"
|
|
};
|
|
|
|
lsock = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
if (lsock == -1)
|
|
suicide("dispatch_work - failed to create socket");
|
|
|
|
fcntl(lsock, F_SETFL, O_NONBLOCK);
|
|
|
|
(void) unlink("/var/state/ifchange");
|
|
ret = bind(lsock, (struct sockaddr *) &lsock_addr, sizeof(lsock_addr));
|
|
if (ret)
|
|
suicide("dispatch_work - failed to bind socket");
|
|
ret = chmod("/var/state/ifchange", S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
|
|
if (ret)
|
|
suicide("dispatch_work - failed to chmod socket");
|
|
ret = listen(lsock, SOCK_QUEUE);
|
|
if (ret)
|
|
suicide("dispatch_work - failed to listen on socket");
|
|
|
|
return lsock;
|
|
}
|
|
|
|
static void accept_conns(int *lsock)
|
|
{
|
|
int ret;
|
|
struct sockaddr_un sock_addr;
|
|
socklen_t sock_len = sizeof(sock_addr);
|
|
|
|
for(;;)
|
|
{
|
|
ret = accept(*lsock, (struct sockaddr *) &sock_addr, &sock_len);
|
|
if (ret != -1) {
|
|
add_sk(ret);
|
|
return;
|
|
}
|
|
switch (errno) {
|
|
case EAGAIN:
|
|
#ifdef LINUX
|
|
case ENETDOWN:
|
|
case EPROTO:
|
|
case ENOPROTOOPT:
|
|
case EHOSTDOWN:
|
|
case ENONET:
|
|
case EHOSTUNREACH:
|
|
case EOPNOTSUPP:
|
|
case ENETUNREACH:
|
|
#endif
|
|
return;
|
|
|
|
case EINTR:
|
|
continue;
|
|
|
|
case EBADF:
|
|
case ENOTSOCK:
|
|
case EINVAL:
|
|
log_line("warning: accept returned %s!\n", strerror(errno));
|
|
|
|
epoll_del(*lsock);
|
|
close(*lsock);
|
|
|
|
*lsock = get_listen();
|
|
epoll_add(*lsock);
|
|
return;
|
|
|
|
case ECONNABORTED:
|
|
case EMFILE:
|
|
case ENFILE:
|
|
log_line("warning: accept returned %s!\n", strerror(errno));
|
|
return;
|
|
|
|
default:
|
|
log_line("warning: accept returned a mysterious error: %s\n",
|
|
strerror(errno));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void setup_signals()
|
|
{
|
|
sigset_t mask;
|
|
sigemptyset(&mask);
|
|
sigaddset(&mask, SIGPIPE);
|
|
sigaddset(&mask, SIGUSR1);
|
|
sigaddset(&mask, SIGUSR2);
|
|
sigaddset(&mask, SIGTSTP);
|
|
sigaddset(&mask, SIGTTIN);
|
|
sigaddset(&mask, SIGCHLD);
|
|
sigaddset(&mask, SIGHUP);
|
|
sigaddset(&mask, SIGINT);
|
|
sigaddset(&mask, SIGTERM);
|
|
if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0)
|
|
suicide("sigprocmask failed");
|
|
signalFd = signalfd(-1, &mask, SFD_NONBLOCK);
|
|
if (signalFd < 0)
|
|
suicide("signalfd failed");
|
|
}
|
|
|
|
static void signal_dispatch()
|
|
{
|
|
int t, off = 0;
|
|
struct signalfd_siginfo si;
|
|
again:
|
|
t = read(signalFd, (char *)&si + off, sizeof si - off);
|
|
if (t < sizeof si - off) {
|
|
if (t < 0) {
|
|
if (t == EAGAIN || t == EWOULDBLOCK || t == EINTR)
|
|
goto again;
|
|
else
|
|
suicide("signalfd read error");
|
|
}
|
|
off += t;
|
|
}
|
|
switch (si.ssi_signo) {
|
|
case SIGINT:
|
|
case SIGTERM:
|
|
exit(EXIT_SUCCESS);
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void process_client_fd(int fd)
|
|
{
|
|
struct ifchd_client *cl = NULL;
|
|
int r;
|
|
char buf[MAX_BUF];
|
|
|
|
for (int j = 0; j < SOCK_QUEUE; ++j) {
|
|
if (clients[j].fd == fd) {
|
|
cl = &clients[j];
|
|
break;
|
|
}
|
|
}
|
|
if (!cl)
|
|
suicide("epoll returned pending read for untracked fd");
|
|
|
|
cl->idle_time = time(NULL);
|
|
memset(buf, '\0', sizeof buf);
|
|
|
|
r = safe_read(cl->fd, buf, sizeof buf - 1);
|
|
if (r == 0) {
|
|
// Remote end hung up.
|
|
goto fail;
|
|
} else if (r < 0) {
|
|
if (errno == EAGAIN || errno == EWOULDBLOCK)
|
|
return;
|
|
log_line("error reading from client fd: %s", strerror(errno));
|
|
goto fail;
|
|
}
|
|
|
|
if (execute_buffer(cl, buf) == -1) {
|
|
log_line("execute_buffer was passed invalid commands");
|
|
goto fail;
|
|
}
|
|
return;
|
|
fail:
|
|
ifchd_client_wipe(cl);
|
|
}
|
|
|
|
/* Core function that handles connections, gathers input, and calls
|
|
* the state machine to do actual work. */
|
|
static void dispatch_work(void)
|
|
{
|
|
int lsock;
|
|
|
|
/* Initialize all structures to blank state. */
|
|
for (int i = 0; i < SOCK_QUEUE; i++)
|
|
ifchd_client_init(&clients[i]);
|
|
|
|
lsock = get_listen();
|
|
|
|
epoll_add(lsock);
|
|
epoll_add(signalFd);
|
|
|
|
for (;;) {
|
|
int r = epoll_wait(epollfd, events, SOCK_QUEUE + 2, -1);
|
|
if (r == -1) {
|
|
if (errno == EINTR)
|
|
continue;
|
|
else
|
|
suicide("epoll_wait failed");
|
|
}
|
|
for (int i = 0; i < r; ++i) {
|
|
int fd = events[i].data.fd;
|
|
if (fd == lsock)
|
|
accept_conns(&lsock);
|
|
else if (fd == signalFd)
|
|
signal_dispatch();
|
|
else
|
|
process_client_fd(fd);
|
|
}
|
|
close_idle_sk();
|
|
}
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
int c, t;
|
|
uid_t uid = 0;
|
|
gid_t gid = 0;
|
|
char pidfile[MAX_PATH_LENGTH] = PID_FILE_DEFAULT;
|
|
char chrootd[MAX_PATH_LENGTH] = "";
|
|
char resolv_conf_d[MAX_PATH_LENGTH] = "";
|
|
char *p;
|
|
struct passwd *pws;
|
|
struct group *grp;
|
|
|
|
while (1) {
|
|
int option_index = 0;
|
|
static const struct option long_options[] = {
|
|
{"detach", 0, 0, 'd'},
|
|
{"nodetach", 0, 0, 'n'},
|
|
{"pidfile", 1, 0, 'p'},
|
|
{"quiet", 0, 0, 'q'},
|
|
{"chroot", 1, 0, 'c'},
|
|
{"resolve", 1, 0, 'r'},
|
|
{"hostname", 0, 0, 'o'},
|
|
{"user", 1, 0, 'u'},
|
|
{"group", 1, 0, 'g'},
|
|
{"cuser", 1, 0, 'U'},
|
|
{"cgroup", 1, 0, 'G'},
|
|
{"cpid", 1, 0, 'P'},
|
|
{"interface", 1, 0, 'i'},
|
|
{"help", 0, 0, 'h'},
|
|
{"version", 0, 0, 'v'},
|
|
{"verbose", 0, 0, 'V'},
|
|
{0, 0, 0, 0}
|
|
};
|
|
|
|
c = getopt_long(argc, argv, "dnp:qc:r:ou:g:U:G:P:i:hvV", long_options,
|
|
&option_index);
|
|
if (c == -1)
|
|
break;
|
|
|
|
switch (c) {
|
|
|
|
case 'h':
|
|
printf(
|
|
"ifchd %s, if change daemon. Licensed under 2-clause BSD.\n", IFCHD_VERSION);
|
|
printf(
|
|
"Copyright (C) 2004-2012 Nicholas J. Kain\n"
|
|
"Usage: ifchd [OPTIONS]\n"
|
|
" -d, --detach detach from TTY and daemonize\n"
|
|
" -n, --nodetach stay attached to TTY\n"
|
|
" -q, --quiet don't print to std(out|err) or log\n"
|
|
" -c, --chroot path where ifchd should chroot\n"
|
|
" -r, --resolve path to resolv.conf or equiv\n"
|
|
" -o, --hostname allow dhcp to set machine hostname\n"
|
|
" -p, --pidfile pidfile path\n");
|
|
printf(
|
|
" -u, --user user name that ifchd should run as\n"
|
|
" -g, --group group name that ifchd should run as\n"
|
|
" -U, --cuser user name of clients\n"
|
|
" -G, --cgroup group name of clients\n"
|
|
" -P, --cpid process id of client\n"
|
|
" -i, --interface ifchd clients may modify this interface\n"
|
|
" -V, --verbose log detailed messages\n"
|
|
" -h, --help print this help and exit\n"
|
|
" -v, --version print version information and exit\n");
|
|
exit(EXIT_FAILURE);
|
|
break;
|
|
|
|
case 'v':
|
|
printf(
|
|
"ifchd %s, if change daemon. Licensed under 2-clause BSD.\n", IFCHD_VERSION);
|
|
printf(
|
|
"Copyright (C) 2004-2012 Nicholas J. Kain\nAll rights reserved.\n"
|
|
"This is free software; see the source for copying conditions. There is NO\n"
|
|
"WARRANTY; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n");
|
|
exit(EXIT_FAILURE);
|
|
break;
|
|
|
|
case 'd':
|
|
gflags_detach = 1;
|
|
break;
|
|
|
|
case 'n':
|
|
gflags_detach = 0;
|
|
break;
|
|
|
|
case 'q':
|
|
gflags_quiet = 1;
|
|
break;
|
|
|
|
case 'c':
|
|
strlcpy(chrootd, optarg, MAX_PATH_LENGTH);
|
|
break;
|
|
|
|
case 'p':
|
|
strlcpy(pidfile, optarg, MAX_PATH_LENGTH);
|
|
break;
|
|
|
|
case 'r':
|
|
strlcpy(resolv_conf_d, optarg, MAX_PATH_LENGTH);
|
|
break;
|
|
|
|
case 'o':
|
|
allow_hostname = 1;
|
|
break;
|
|
|
|
case 'u':
|
|
t = (unsigned int) strtol(optarg, &p, 10);
|
|
if (*p != '\0') {
|
|
pws = getpwnam(optarg);
|
|
if (pws) {
|
|
uid = (int)pws->pw_uid;
|
|
if (!gid)
|
|
gid = (int)pws->pw_gid;
|
|
} else suicide("FATAL - Invalid uid specified.");
|
|
} else
|
|
uid = t;
|
|
break;
|
|
|
|
case 'g':
|
|
t = (unsigned int) strtol(optarg, &p, 10);
|
|
if (*p != '\0') {
|
|
grp = getgrnam(optarg);
|
|
if (grp) {
|
|
gid = (int)grp->gr_gid;
|
|
} else
|
|
suicide("FATAL - Invalid gid specified.");
|
|
} else
|
|
gid = t;
|
|
break;
|
|
|
|
case 'U':
|
|
t = (unsigned int) strtol(optarg, &p, 10);
|
|
if (*p != '\0') {
|
|
pws = getpwnam(optarg);
|
|
if (pws) {
|
|
peer_uid = (int)pws->pw_uid;
|
|
if (!peer_gid)
|
|
peer_gid = (int)pws->pw_gid;
|
|
} else
|
|
suicide("FATAL - Invalid uid specified.");
|
|
} else
|
|
peer_uid = t;
|
|
break;
|
|
|
|
case 'G':
|
|
t = (unsigned int) strtol(optarg, &p, 10);
|
|
if (*p != '\0') {
|
|
grp = getgrnam(optarg);
|
|
if (grp) {
|
|
peer_gid = (int)grp->gr_gid;
|
|
} else
|
|
suicide("FATAL - Invalid gid specified.");
|
|
} else
|
|
peer_gid = t;
|
|
break;
|
|
|
|
case 'P':
|
|
t = (unsigned int) strtol(optarg, &p, 10);
|
|
if (*p == '\0')
|
|
peer_pid = t;
|
|
break;
|
|
|
|
case 'i':
|
|
add_permitted_if(optarg);
|
|
break;
|
|
|
|
case 'V':
|
|
gflags_verbose = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (getuid())
|
|
suicide("FATAL - I need root for CAP_NET_ADMIN and chroot!");
|
|
|
|
if (gflags_detach)
|
|
if (daemon(0,0)) {
|
|
log_line("FATAL - detaching fork failed\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
if (file_exists(pidfile, "w") == -1) {
|
|
log_line("FATAL - cannot open pidfile for write!");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
write_pid(pidfile);
|
|
|
|
umask(077);
|
|
setup_signals();
|
|
|
|
/* If we are requested to update resolv.conf, preopen the fd before
|
|
* we drop root privileges, making sure that if we create
|
|
* resolv.conf, it will be world-readable.
|
|
*/
|
|
if (strncmp(resolv_conf_d, "", MAX_PATH_LENGTH)) {
|
|
umask(022);
|
|
resolv_conf_fd = open(resolv_conf_d, O_RDWR | O_CREAT, 644);
|
|
umask(077);
|
|
if (resolv_conf_fd == -1) {
|
|
suicide("FATAL - unable to open resolv.conf");
|
|
}
|
|
}
|
|
|
|
if (!strncmp(chrootd, "", MAX_PATH_LENGTH))
|
|
suicide("FATAL - No chroot path specified. Refusing to run.");
|
|
|
|
/* Note that failure cases are handled by called fns. */
|
|
imprison(chrootd);
|
|
set_cap(uid, gid, "cap_net_admin=ep");
|
|
drop_root(uid, gid);
|
|
|
|
/* Cover our tracks... */
|
|
memset(chrootd, '\0', sizeof(chrootd));
|
|
memset(resolv_conf_d, '\0', sizeof(resolv_conf_d));
|
|
memset(pidfile, '\0', sizeof(pidfile));
|
|
|
|
epollfd = epoll_create1(0);
|
|
if (epollfd == -1)
|
|
suicide("epoll_create1 failed");
|
|
|
|
if (enforce_seccomp())
|
|
log_line("seccomp filter cannot be installed");
|
|
|
|
dispatch_work();
|
|
|
|
/* Explicitly freed so memory debugger output has less static. */
|
|
for (size_t i = 0; i < SOCK_QUEUE; ++i)
|
|
ifchd_client_wipe(&clients[i]);
|
|
|
|
exit(EXIT_SUCCESS);
|
|
}
|