/* sslh-select: mono-processus server # Copyright (C) 2007-2010 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 */ #define __LINUX__ #include "common.h" #include "probe.h" const char* server_type = "sslh-select"; /* cnx_num_alloc is the number of connection to allocate at once (at start-up, * and then every time we get too many simultaneous connections: e.g. start * with 100 slots, then if we get more than 100 connections allocate another * 100 slots, and so on). We never free up connection structures. We try to * allocate as many structures at once as will fit in one page (which is 102 * in sslh 1.9 on Linux on x86) */ static long cnx_num_alloc; /* Make the file descriptor non-block */ int set_nonblock(int fd) { int flags; flags = fcntl(fd, F_GETFL); CHECK_RES_RETURN(flags, "fcntl"); flags |= O_NONBLOCK; flags = fcntl(fd, F_SETFL, flags); CHECK_RES_RETURN(flags, "fcntl"); return flags; } int tidy_connection(struct connection *cnx, fd_set *fds, fd_set *fds2) { int i; for (i = 0; i < 2; i++) { if (cnx->q[i].fd != -1) { if (verbose) fprintf(stderr, "closing fd %d\n", cnx->q[i].fd); close(cnx->q[i].fd); FD_CLR(cnx->q[i].fd, fds); FD_CLR(cnx->q[i].fd, fds2); if (cnx->q[i].deferred_data) free(cnx->q[i].deferred_data); } } init_cnx(cnx); return 0; } /* 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 */ int fd_is_in_range(int fd) { if (fd >= FD_SETSIZE) { log_message(LOG_ERR, "too many open file descriptor to monitor them all -- dropping connection\n"); return 0; } return 1; } /* Accepts a connection from the main socket and assigns it to an empty slot. * If no slots are available, allocate another few. If that fails, drop the * connexion */ int accept_new_connection(int listen_socket, struct connection *cnx[], int* cnx_size) { int in_socket, free, i, res; struct connection *new; in_socket = accept(listen_socket, 0, 0); CHECK_RES_RETURN(in_socket, "accept"); if (!fd_is_in_range(in_socket)) return -1; res = set_nonblock(in_socket); if (res == -1) return -1; /* Find an empty slot */ for (free = 0; (free < *cnx_size) && ((*cnx)[free].q[0].fd != -1); free++) { /* nothing */ } if (free >= *cnx_size) { if (verbose) fprintf(stderr, "buying more slots from the slot machine.\n"); new = realloc(*cnx, (*cnx_size + cnx_num_alloc) * sizeof((*cnx)[0])); if (!new) { log_message(LOG_ERR, "unable to realloc -- dropping connection\n"); return -1; } *cnx = new; *cnx_size += cnx_num_alloc; for (i = free; i < *cnx_size; i++) { init_cnx(&(*cnx)[i]); } } (*cnx)[free].q[0].fd = in_socket; (*cnx)[free].state = ST_PROBING; (*cnx)[free].probe_timeout = time(NULL) + probing_timeout; if (verbose) fprintf(stderr, "accepted fd %d on slot %d\n", in_socket, free); return in_socket; } /* Connect queue 1 of connection to SSL; returns new file descriptor */ int connect_queue(struct connection *cnx, fd_set *fds_r, fd_set *fds_w) { struct queue *q = &cnx->q[1]; q->fd = connect_addr(cnx, cnx->q[0].fd); if ((q->fd != -1) && fd_is_in_range(q->fd)) { log_connection(cnx); set_nonblock(q->fd); flush_deferred(q); if (q->deferred_data) { FD_SET(q->fd, fds_w); } else { FD_SET(q->fd, fds_r); } return q->fd; } else { tidy_connection(cnx, fds_r, fds_w); return -1; } } /* shovels data from active fd to the other returns after one socket closed or operation would block */ void shovel(struct connection *cnx, int active_fd, fd_set *fds_r, fd_set *fds_w) { struct queue *read_q, *write_q; read_q = &cnx->q[active_fd]; write_q = &cnx->q[1-active_fd]; if (verbose) fprintf(stderr, "activity on fd%d\n", read_q->fd); switch(fd2fd(write_q, read_q)) { case -1: case FD_CNXCLOSED: tidy_connection(cnx, fds_r, fds_w); break; case FD_STALLED: FD_SET(write_q->fd, fds_w); FD_CLR(read_q->fd, fds_r); break; default: /* Nothing */ break; } } /* returns true if specified fd is initialised and present in fd_set */ int is_fd_active(int fd, fd_set* set) { if (fd == -1) return 0; return FD_ISSET(fd, set); } /* 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. Defered * 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(int listen_sockets[], int num_addr_listen) { fd_set fds_r, fds_w; /* reference fd sets (used to init the next 2) */ fd_set readfds, writefds; /* working read and write fd sets */ struct timeval tv; int max_fd, in_socket, i, j, res; struct connection *cnx; int num_cnx; /* Number of connections in *cnx */ int num_probing = 0; /* Number of connections currently probing * We use this to know if we need to time out of * select() */ FD_ZERO(&fds_r); FD_ZERO(&fds_w); for (i = 0; i < num_addr_listen; i++) { FD_SET(listen_sockets[i], &fds_r); set_nonblock(listen_sockets[i]); } max_fd = listen_sockets[num_addr_listen-1] + 1; cnx_num_alloc = getpagesize() / sizeof(struct connection); num_cnx = cnx_num_alloc; /* Start with a set pool of slots */ cnx = malloc(num_cnx * sizeof(struct connection)); for (i = 0; i < num_cnx; i++) init_cnx(&cnx[i]); while (1) { memset(&tv, 0, sizeof(tv)); tv.tv_sec = probing_timeout; memcpy(&readfds, &fds_r, sizeof(readfds)); memcpy(&writefds, &fds_w, sizeof(writefds)); if (verbose) fprintf(stderr, "selecting... max_fd=%d num_probing=%d\n", max_fd, num_probing); res = select(max_fd, &readfds, &writefds, NULL, num_probing ? &tv : NULL); if (res < 0) perror("select"); /* Check main socket for new connections */ for (i = 0; i < num_addr_listen; i++) { if (FD_ISSET(listen_sockets[i], &readfds)) { in_socket = accept_new_connection(listen_sockets[i], &cnx, &num_cnx); if (in_socket != -1) num_probing++; if (in_socket > 0) { FD_SET(in_socket, &fds_r); if (in_socket >= max_fd) max_fd = in_socket + 1;; } FD_CLR(listen_sockets[i], &readfds); } } /* Check all sockets for write activity */ for (i = 0; i < num_cnx; i++) { if (cnx[i].q[0].fd != -1) { for (j = 0; j < 2; j++) { if (is_fd_active(cnx[i].q[j].fd, &writefds)) { res = flush_deferred(&cnx[i].q[j]); if ((res == -1) && ((errno == EPIPE) || (errno == ECONNRESET))) { if (cnx[i].state == ST_PROBING) num_probing--; tidy_connection(&cnx[i], &fds_r, &fds_w); if (verbose) fprintf(stderr, "closed slot %d\n", i); } else { /* If no deferred data is left, stop monitoring the fd * for write, and restart monitoring the other one for reads*/ if (!cnx[i].q[j].deferred_data_size) { FD_CLR(cnx[i].q[j].fd, &fds_w); FD_SET(cnx[i].q[1-j].fd, &fds_r); } } } } } } /* Check all sockets for read activity */ for (i = 0; i < num_cnx; i++) { for (j = 0; j < 2; j++) { if (is_fd_active(cnx[i].q[j].fd, &readfds) || ((cnx[i].state == ST_PROBING) && (cnx[i].probe_timeout < time(NULL)))) { if (verbose) fprintf(stderr, "processing fd%d slot %d\n", j, i); switch (cnx[i].state) { case ST_PROBING: if (j == 1) { fprintf(stderr, "Activity on fd2 while probing, impossible\n"); dump_connection(&cnx[i]); exit(1); } /* If timed out it's SSH, otherwise the client sent * data so probe the protocol */ if ((cnx[i].probe_timeout < time(NULL))) { cnx[i].proto = timeout_protocol(); } else { res = probe_client_protocol(&cnx[i]); if (res == PROBE_AGAIN) continue; } num_probing--; cnx[i].state = ST_SHOVELING; /* libwrap check if required for this protocol */ if (cnx[i].proto->service && check_access_rights(in_socket, cnx[i].proto->service)) { tidy_connection(&cnx[i], &fds_r, &fds_w); res = -1; } else { res = connect_queue(&cnx[i], &fds_r, &fds_w); } if (res >= max_fd) max_fd = res + 1;; break; case ST_SHOVELING: shovel(&cnx[i], j, &fds_r, &fds_w); break; default: /* illegal */ log_message(LOG_ERR, "Illegal connection state %d\n", cnx[i].state); exit(1); } } } } } } void start_shoveler(int listen_socket) { fprintf(stderr, "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 */