/* sslh-select: mono-processus server # Copyright (C) 2007-2021 Yves Rutschle # # 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. # # The full text for the General Public License is here: # http://www.gnu.org/licenses/gpl.html */ /* Why use select(2) rather than poll(2)? * No real reason except that's how it was written at first. This article: * https://daniel.haxx.se/docs/poll-vs-select.html suggests that over a few * hundred file descriptors, both become very slow, so there is little * incentive to move to poll() to support more than FD_SETSIZE (which is 1024 * on many Linux. To support large numbers of descriptors, either use the fork * version, or we'll have to write a new version based on libev. */ #define __LINUX__ #include #include "common.h" #include "probe.h" #include "udp-listener.h" #include "collection.h" #include "gap.h" #include "log.h" const char* server_type = "sslh-select"; /* watcher type for a select() loop */ struct watchers { fd_set fds_r, fds_w; /* reference fd sets (used to init working copies) */ int max_fd; /* Highest fd number to pass to select() */ }; #define WATCHERS_TYPE_DEFINED /* To notify processes.h */ #include "processes.h" void watchers_init(watchers** w) { *w = malloc(sizeof(**w)); FD_ZERO(&(*w)->fds_r); FD_ZERO(&(*w)->fds_w); } void watchers_add_read(watchers* w, int fd) { FD_SET(fd, &w->fds_r); if (fd > w->max_fd) w->max_fd = fd + 1; } void watchers_del_read(watchers* w, int fd) { FD_CLR(fd, &w->fds_r); } void watchers_add_write(watchers* w, int fd) { FD_SET(fd, &w->fds_w); if (fd > w->max_fd) w->max_fd = fd + 1; } void watchers_del_write(watchers* w, int fd) { FD_CLR(fd, &w->fds_w); } /* To remove after moving UDP lookups to hash table */ int watchers_maxfd(watchers* w) { return w->max_fd; } /* /end watchers */ /* if fd becomes higher than FD_SETSIZE, things won't work so well with FD_SET * and FD_CLR. Need to drop connections if we go above that limit */ #warning strange things will happen if more than FD_SETSIZE descriptors are used /* This test is currently not done */ static int fd_is_in_range(int fd) { if (fd >= FD_SETSIZE) { print_message(msg_system_error, "too many open file descriptor to monitor them all -- dropping connection\n"); return 0; } return 1; } /* Check all connections to see if a UDP connections has timed out, then free * it. At the same time, keep track of the closest, next timeout. Only do the * search through connections if that timeout actually happened. If the * connection that would have timed out has had activity, it doesn't matter: we * go through connections to find the next timeout, which was needed anyway. */ static void udp_timeouts(struct loop_info* fd_info) { time_t now = time(NULL); if (now < fd_info->next_timeout) return; time_t next_timeout = INT_MAX; for (int i = 0; i < fd_info->watchers->max_fd; i++) { /* if it's either in read or write set, there is a connection * behind that file descriptor */ if (FD_ISSET(i, &fd_info->watchers->fds_r) || FD_ISSET(i, &fd_info->watchers->fds_w)) { struct connection* cnx = collection_get_cnx_from_fd(fd_info->collection, i); if (cnx) { time_t timeout = udp_timeout(cnx); if (!timeout) continue; /* Not a UDP connection */ if (cnx && (timeout <= now)) { print_message(msg_fd, "timed out UDP %d\n", cnx->target_sock); close(cnx->target_sock); watchers_del_read(fd_info->watchers, i); watchers_del_write(fd_info->watchers, i); collection_remove_cnx(fd_info->collection, cnx); } else { if (timeout < next_timeout) next_timeout = timeout; } } } } if (next_timeout != INT_MAX) fd_info->next_timeout = next_timeout; } /* Main loop: the idea is as follow: * - fds_r and fds_w contain the file descriptors to monitor in read and write * - When a file descriptor goes off, process it: read from it, write the data * to its corresponding pair. * - When a file descriptor blocks when writing, remove the read fd from fds_r, * move the data to a deferred buffer, and add the write fd to fds_w. Deferred * buffer is allocated dynamically. * - When we can write to a file descriptor that has deferred data, we try to * write as much as we can. Once all data is written, remove the fd from fds_w * and add its corresponding pair to fds_r, free the buffer. * * That way, each pair of file descriptor (read from one, write to the other) * is monitored either for read or for write, but never for both. */ void main_loop(struct listen_endpoint listen_sockets[], int num_addr_listen) { struct loop_info fd_info = {0}; fd_set readfds, writefds; /* working read and write fd sets */ struct timeval tv; int i, res; fd_info.num_probing = 0; fd_info.probing_list = gap_init(0); watchers_init(&fd_info.watchers); for (i = 0; i < num_addr_listen; i++) { watchers_add_read(fd_info.watchers, listen_sockets[i].socketfd); set_nonblock(listen_sockets[i].socketfd); } fd_info.collection = collection_init(fd_info.watchers->max_fd); while (1) { memset(&tv, 0, sizeof(tv)); tv.tv_sec = cfg.timeout; memcpy(&readfds, &fd_info.watchers->fds_r, sizeof(readfds)); memcpy(&writefds, &fd_info.watchers->fds_w, sizeof(writefds)); print_message(msg_fd, "selecting... max_fd=%d num_probing=%d\n", fd_info.watchers->max_fd, fd_info.num_probing); res = select(fd_info.watchers->max_fd, &readfds, &writefds, NULL, fd_info.num_probing ? &tv : NULL); if (res < 0) perror("select"); /* UDP timeouts: clear out connections after some idle time */ udp_timeouts(&fd_info); /* Check main socket for new connections */ for (i = 0; i < num_addr_listen; i++) { if (FD_ISSET(listen_sockets[i].socketfd, &readfds)) { cnx_accept_process(&fd_info, &listen_sockets[i]); /* don't also process it as a read socket */ FD_CLR(listen_sockets[i].socketfd, &readfds); } } /* Check all sockets for write activity */ for (i = 0; i < fd_info.watchers->max_fd; i++) { if (FD_ISSET(i, &writefds)) { cnx_write_process(&fd_info, i); } } /* Check sockets in probing state for timeouts */ for (i = 0; i < fd_info.num_probing; i++) { struct connection* cnx = gap_get(fd_info.probing_list, i); if (!cnx || cnx->state != ST_PROBING) { print_message(msg_int_error, "Inconsistent probing: cnx=%0xp\n", cnx); if (cnx) print_message(msg_int_error, "Inconsistent probing: state=%d\n", cnx); exit(1); } if (cnx->probe_timeout < time(NULL)) { print_message(msg_fd, "timeout slot %d\n", i); probing_read_process(cnx, &fd_info); } } /* Check all sockets for read activity */ for (i = 0; i < fd_info.watchers->max_fd; i++) { /* Check if it's active AND currently monitored (if a connection * died, it gets tidied, which closes both sockets, but readfs does * not know about that */ if (FD_ISSET(i, &readfds) && FD_ISSET(i, &fd_info.watchers->fds_r)) { cnx_read_process(&fd_info, i); } } } } void start_shoveler(int listen_socket) { print_message(msg_config_error, "inetd mode is not supported in select mode\n"); exit(1); } /* The actual main is in common.c: it's the same for both version of * the server */