Files
ocserv/src/main.c
2013-02-05 21:03:40 +01:00

620 lines
14 KiB
C

/*
* Copyright (C) 2013 Nikos Mavrogiannopoulos
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#include <config.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/select.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netdb.h>
#include <signal.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <cloexec.h>
#include <gnutls/x509.h>
#include <tlslib.h>
#include "ipc.h"
#include <main.h>
#include <vpn.h>
#include <cookies.h>
#include <tun.h>
#include <list.h>
int syslog_open = 0;
static unsigned int terminate = 0;
static unsigned int need_maintainance = 0;
static unsigned int need_children_cleanup = 0;
static void tls_log_func(int level, const char *str)
{
syslog(LOG_DEBUG, "TLS[<%d>]: %s", level, str);
}
/* Returns 0 on success or negative value on error.
*/
static int
listen_ports(struct cfg_st* config, struct listen_list_st *list, const char *node,
int listen_port, int socktype)
{
struct addrinfo hints, *res, *ptr;
char portname[6];
int s, y;
char buf[512];
struct listen_list_st *tmp;
INIT_LIST_HEAD(&list->list);
snprintf(portname, sizeof(portname), "%d", listen_port);
memset(&hints, 0, sizeof(hints));
hints.ai_socktype = socktype;
hints.ai_flags = AI_PASSIVE
#ifdef AI_ADDRCONFIG
| AI_ADDRCONFIG
#endif
;
s = getaddrinfo(node, portname, &hints, &res);
if (s != 0) {
fprintf(stderr, "getaddrinfo() failed: %s\n",
gai_strerror(s));
return -1;
}
for (ptr = res; ptr != NULL; ptr = ptr->ai_next) {
#ifndef HAVE_IPV6
if (ptr->ai_family != AF_INET)
continue;
#endif
if (config->foreground != 0)
fprintf(stderr, "listening on %s...\n",
human_addr(ptr->ai_addr, ptr->ai_addrlen,
buf, sizeof(buf)));
s = socket(ptr->ai_family, ptr->ai_socktype,
ptr->ai_protocol);
if (s < 0) {
perror("socket() failed");
continue;
}
#if defined(HAVE_IPV6) && !defined(_WIN32)
if (ptr->ai_family == AF_INET6) {
y = 1;
/* avoid listen on ipv6 addresses failing
* because already listening on ipv4 addresses: */
setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY,
(const void *) &y, sizeof(y));
}
#endif
if (socktype == SOCK_STREAM) {
y = 1;
if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR,
(const void *) &y, sizeof(y)) < 0) {
perror("setsockopt() failed");
close(s);
continue;
}
} else {
#if defined(IP_DONTFRAG)
y = 1;
if (setsockopt(s, IPPROTO_IP, IP_DONTFRAG,
(const void *) &y, sizeof(y)) < 0)
perror("setsockopt(IP_DF) failed");
#elif defined(IP_MTU_DISCOVER)
y = IP_PMTUDISC_DO;
if (setsockopt(s, IPPROTO_IP, IP_MTU_DISCOVER,
(const void *) &y, sizeof(y)) < 0)
perror("setsockopt(IP_DF) failed");
#endif
}
set_cloexec_flag (s, 1);
if (bind(s, ptr->ai_addr, ptr->ai_addrlen) < 0) {
perror("bind() failed");
close(s);
continue;
}
if (socktype == SOCK_STREAM) {
if (listen(s, 10) < 0) {
perror("listen() failed");
exit(1);
}
}
tmp = calloc(1, sizeof(struct listen_list_st));
tmp->fd = s;
list_add(&(tmp->list), &(list->list));
}
fflush(stderr);
freeaddrinfo(res);
return 0;
}
static void cleanup_children(main_server_st *s)
{
int status;
pid_t pid;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
if (WEXITSTATUS(status) != 0 ||
(WIFSIGNALED(status) && WTERMSIG(status) == SIGSEGV)) {
if (WIFSIGNALED(status))
syslog(LOG_ERR, "Child %u died with sigsegv\n", (unsigned)pid);
}
}
need_children_cleanup = 0;
}
static void handle_children(int signo)
{
need_children_cleanup = 1;
}
static void handle_alarm(int signo)
{
need_maintainance = 1;
}
static void tls_audit_log_func(gnutls_session_t session, const char *str)
{
worker_st * ws;
if (session == NULL)
syslog(LOG_AUTH, "Warning: %s", str);
else {
ws = gnutls_session_get_ptr(session);
oclog(ws, LOG_ERR, "Warning: %s", str);
}
}
static int verify_certificate_cb(gnutls_session_t session)
{
unsigned int status;
int ret;
worker_st * ws;
ws = gnutls_session_get_ptr(session);
if (ws == NULL) {
syslog(LOG_ERR, "%s:%d: Could not obtain worker state.", __func__, __LINE__);
return -1;
}
/* This verification function uses the trusted CAs in the credentials
* structure. So you must have installed one or more CA certificates.
*/
ret = gnutls_certificate_verify_peers2(session, &status);
if (ret < 0) {
oclog(ws, LOG_ERR, "Error verifying client certificate");
return GNUTLS_E_CERTIFICATE_ERROR;
}
if (status != 0) {
#if GNUTLS_VERSION_NUMBER > 0x030106
type = gnutls_certificate_type_get(session);
ret =
gnutls_certificate_verification_status_print(status, type,
&out, 0);
if (ret < 0)
return GNUTLS_E_CERTIFICATE_ERROR;
oclog(ws, LOG_INFO, "Client certificate verification failed: %s", out.data);
gnutls_free(out.data);
#else
oclog(ws, LOG_INFO, "Client certificate verification failed.");
#endif
return GNUTLS_E_CERTIFICATE_ERROR;
} else {
oclog(ws, LOG_INFO, "Client certificate verification succeeded");
}
/* notify gnutls to continue handshake normally */
return 0;
}
static void drop_privileges(struct cfg_st *config)
{
int ret, e;
if (config->chroot_dir) {
ret = chroot(config->chroot_dir);
if (ret != 0) {
e = errno;
syslog(LOG_ERR, "Cannot chroot to %s: %s", config->chroot_dir, strerror(e));
exit(1);
}
}
if (config->gid != -1 && (getgid() == 0 || getegid() == 0)) {
ret = setgid(config->gid);
if (ret < 0) {
e = errno;
syslog(LOG_ERR, "Cannot set gid to %d: %s\n",
(int) config->gid, strerror(e));
exit(1);
}
}
if (config->uid != -1 && (getuid() == 0 || geteuid() == 0)) {
ret = setuid(config->uid);
if (ret < 0) {
e = errno;
syslog(LOG_ERR, "Cannot set uid to %d: %s\n",
(int) config->uid, strerror(e));
exit(1);
}
}
}
/* clears the server llist and clist. To be used after fork() */
void clear_lists(main_server_st *s)
{
struct list_head *cq;
struct list_head *pos;
struct listen_list_st *ltmp;
struct proc_list_st *ctmp;
list_for_each_safe(pos, cq, &s->llist->list) {
ltmp = list_entry(pos, struct listen_list_st, list);
close(ltmp->fd);
list_del(&ltmp->list);
}
list_for_each_safe(pos, cq, &s->clist->list) {
ctmp = list_entry(pos, struct proc_list_st, list);
if (ctmp->fd >= 0)
close(ctmp->fd);
list_del(&ctmp->list);
}
tls_cache_deinit(s->tls_db);
}
static void kill_children(struct proc_list_st* clist)
{
struct list_head *pos;
struct proc_list_st *ctmp;
list_for_each(pos, &clist->list) {
ctmp = list_entry(pos, struct proc_list_st, list);
if (ctmp->pid != -1)
kill(ctmp->pid, SIGTERM);
}
}
static void remove_proc(struct proc_list_st *ctmp)
{
/* close the intercomm fd */
if (ctmp->fd >= 0)
close(ctmp->fd);
ctmp->fd = -1;
ctmp->pid = -1;
if (ctmp->lease)
ctmp->lease->in_use = 0;
list_del(&ctmp->list);
}
static void handle_term(int signo)
{
/* kill all children */
terminate = 1;
}
int main(int argc, char** argv)
{
int fd, pid, e;
struct tls_st creds;
struct listen_list_st llist;
struct proc_list_st clist;
struct listen_list_st *ltmp;
struct proc_list_st *ctmp;
struct list_head *cq;
struct list_head *pos;
struct tun_st tun;
fd_set rd;
int val, n = 0, ret;
struct timeval tv;
int cmd_fd[2];
struct worker_st ws;
struct cfg_st config;
unsigned active_clients = 0;
main_server_st s;
tls_cache_db_st* tls_db;
INIT_LIST_HEAD(&clist.list);
tun_st_init(&tun);
tls_cache_init(&config, &tls_db);
signal(SIGINT, handle_term);
signal(SIGTERM, handle_term);
signal(SIGPIPE, SIG_IGN);
signal(SIGHUP, SIG_IGN);
signal(SIGCHLD, handle_children);
signal(SIGALRM, handle_alarm);
/* load configuration */
ret = cmd_parser(argc, argv, &config);
if (ret < 0) {
fprintf(stderr, "Error in arguments\n");
exit(1);
}
if (getuid() != 0) {
fprintf(stderr, "This server requires root access to operate.\n");
exit(1);
}
s.config = &config;
s.tun = &tun;
s.tls_db = tls_db;
s.llist = &llist;
s.clist = &clist;
/* Listen to network ports */
ret = listen_ports(&config, &llist, config.name, config.port, SOCK_STREAM);
if (ret < 0) {
fprintf(stderr, "Cannot listen to specified ports\n");
exit(1);
}
/* Initialize GnuTLS */
gnutls_global_set_audit_log_function(tls_audit_log_func);
if (config.tls_debug) {
gnutls_global_set_log_function(tls_log_func);
gnutls_global_set_log_level(9);
}
ret = gnutls_global_init();
GNUTLS_FATAL_ERR(ret);
ret = gnutls_certificate_allocate_credentials(&creds.xcred);
GNUTLS_FATAL_ERR(ret);
#warning Handle keys using the communication interface
ret =
gnutls_certificate_set_x509_key_file(creds.xcred, config.cert,
config.key,
GNUTLS_X509_FMT_PEM);
if (ret < 0) {
fprintf(stderr, "Error setting the certificate (%s) or key (%s) files.\n",
config.cert, config.key);
exit(1);
}
if (config.ca != NULL) {
ret =
gnutls_certificate_set_x509_trust_file(creds.xcred,
config.ca,
GNUTLS_X509_FMT_PEM);
if (ret < 0) {
fprintf(stderr, "Error setting the CA (%s) file.\n",
config.ca);
exit(1);
}
printf("Processed %d CA certificate(s).\n", ret);
}
if (config.crl != NULL) {
ret =
gnutls_certificate_set_x509_crl_file(creds.xcred,
config.crl,
GNUTLS_X509_FMT_PEM);
GNUTLS_FATAL_ERR(ret);
}
if (config.cert_req != GNUTLS_CERT_IGNORE) {
gnutls_certificate_set_verify_function(creds.xcred,
verify_certificate_cb);
}
ret = gnutls_priority_init(&creds.cprio, config.priorities, NULL);
GNUTLS_FATAL_ERR(ret);
memset(&ws, 0, sizeof(ws));
if (config.foreground == 0)
daemon(0, 0);
#define MAINTAINANCE_TIME (config.cookie_validity + 300)
alarm(MAINTAINANCE_TIME);
openlog("ocserv", LOG_PID|LOG_NDELAY, LOG_LOCAL0);
syslog_open = 1;
for (;;) {
if (terminate != 0) {
kill_children(&clist);
sleep(1);
exit(0);
}
FD_ZERO(&rd);
list_for_each(pos, &llist.list) {
ltmp = list_entry(pos, struct listen_list_st, list);
val = fcntl(ltmp->fd, F_GETFL, 0);
if ((val == -1)
|| (fcntl(ltmp->fd, F_SETFL, val | O_NONBLOCK) <
0)) {
perror("fcntl()");
exit(1);
}
FD_SET(ltmp->fd, &rd);
n = MAX(n, ltmp->fd);
}
list_for_each(pos, &clist.list) {
ctmp = list_entry(pos, struct proc_list_st, list);
FD_SET(ctmp->fd, &rd);
n = MAX(n, ctmp->fd);
}
tv.tv_usec = 0;
tv.tv_sec = 10;
ret = select(n + 1, &rd, NULL, NULL, &tv);
if (ret == -1 && errno == EINTR)
continue;
if (ret < 0) {
e = errno;
syslog(LOG_ERR, "Error in select(): %s",
strerror(e));
exit(1);
}
/* Check for new connections to accept */
list_for_each(pos, &llist.list) {
ltmp = list_entry(pos, struct listen_list_st, list);
if (FD_ISSET(ltmp->fd, &rd)) {
ws.remote_addr_len = sizeof(ws.remote_addr);
fd = accept(ltmp->fd, (void*)&ws.remote_addr, &ws.remote_addr_len);
if (fd < 0) {
syslog(LOG_ERR,
"Error in accept(): %s",
strerror(errno));
continue;
}
set_cloexec_flag (fd, 1);
if (config.max_clients > 0 && active_clients >= config.max_clients) {
close(fd);
syslog(LOG_INFO, "Reached maximum client limit (active: %u)", active_clients);
break;
}
/* Create a command socket */
ret = socketpair(AF_UNIX, SOCK_STREAM, 0, cmd_fd);
if (ret < 0) {
syslog(LOG_ERR, "Error creating command socket");
close(fd);
break;
}
pid = fork();
if (pid == 0) { /* child */
/* Drop privileges after this point */
drop_privileges(&config);
/* close any open descriptors before
* running the server
*/
close(cmd_fd[0]);
clear_lists(&s);
ws.config = &config;
ws.cmd_fd = cmd_fd[1];
ws.tun_fd = -1;
ws.conn_fd = fd;
ws.creds = &creds;
vpn_server(&ws);
exit(0);
} else if (pid == -1) {
fork_failed:
close(cmd_fd[0]);
} else { /* parent */
ctmp = calloc(1, sizeof(struct proc_list_st));
if (ctmp == NULL) {
kill(pid, SIGTERM);
goto fork_failed;
}
memcpy(&ctmp->remote_addr, &ws.remote_addr, ws.remote_addr_len);
ctmp->remote_addr_len = ws.remote_addr_len;
ctmp->pid = pid;
ctmp->fd = cmd_fd[0];
set_cloexec_flag (cmd_fd[0], 1);
list_add(&(ctmp->list), &(clist.list));
active_clients++;
}
close(cmd_fd[1]);
close(fd);
}
}
/* Check for any pending commands */
list_for_each_safe(pos, cq, &clist.list) {
ctmp = list_entry(pos, struct proc_list_st, list);
if (FD_ISSET(ctmp->fd, &rd)) {
ret = handle_commands(&s, ctmp);
if (ret < 0) {
if (ret == -2) {
/* received a bad command from worker */
kill(ctmp->pid, SIGTERM);
}
call_disconnect_script(&s, ctmp);
remove_proc(ctmp);
active_clients--;
}
}
}
/* Check if we need to expire any cookies */
if (need_maintainance != 0) {
need_maintainance = 0;
pid = fork();
if (pid == 0) { /* child */
syslog(LOG_INFO, "Performing maintainance");
clear_lists(&s);
expire_cookies(&s);
expire_tls_sessions(&s);
exit(0);
}
alarm(MAINTAINANCE_TIME);
}
if (need_children_cleanup != 0) {
cleanup_children(&s);
}
}
return 0;
}