mirror of https://github.com/moparisthebest/sslh
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
355 lines
11 KiB
355 lines
11 KiB
/* |
|
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 |
|
*/ |
|
|
|
|
|
|