mirror of
https://github.com/yrutschle/sslh.git
synced 2026-10-22 18:12:10 +03:00
sslh-fork can now limit the number of concurrent connections for each incoming port
This commit is contained in:
1 parent
1dfd70c20c
commit
20290bbfa6
10 files changed
+164
-21
No files matched your search
@@ -185,6 +185,7 @@ static int start_listen_inet(struct listen_endpoint *sockfd[], int num_addr, str
|
||||
(*sockfd)[num_addr-1].socketfd = listen_single_addr(addr, cfg->keepalive, cfg->is_udp);
|
||||
(*sockfd)[num_addr-1].type = cfg->is_udp ? SOCK_DGRAM : SOCK_STREAM;
|
||||
(*sockfd)[num_addr-1].family = AF_INET;
|
||||
(*sockfd)[num_addr-1].endpoint_cfg = cfg;
|
||||
print_message(msg_config, "%d:\t%s\t[%s] [%s]\n", (*sockfd)[num_addr-1].socketfd, sprintaddr(buf, sizeof(buf), addr),
|
||||
cfg->keepalive ? "keepalive" : "",
|
||||
cfg->is_udp ? "udp" : "");
|
||||
@@ -219,6 +220,7 @@ static int start_listen_unix(struct listen_endpoint *sockfd[], int num_addr, str
|
||||
(*sockfd)[num_addr-1].socketfd = fd;
|
||||
(*sockfd)[num_addr-1].type = cfg->is_udp ? SOCK_DGRAM : SOCK_STREAM;
|
||||
(*sockfd)[num_addr-1].family = AF_INET;
|
||||
(*sockfd)[num_addr-1].endpoint_cfg = cfg;
|
||||
|
||||
return num_addr;
|
||||
}
|
||||
@@ -880,16 +882,23 @@ int check_access_rights(int in_socket, const char* service)
|
||||
return 0;
|
||||
}
|
||||
|
||||
volatile sig_atomic_t received_sigchld;
|
||||
|
||||
void sig_sigchld(int sig)
|
||||
{
|
||||
received_sigchld = 1;
|
||||
}
|
||||
|
||||
|
||||
void setup_signals(void)
|
||||
{
|
||||
int res;
|
||||
struct sigaction action;
|
||||
|
||||
/* Request no SIGCHLD is sent upon termination of
|
||||
* the children */
|
||||
/* Set SIGCHLD to sig_sigchld() */
|
||||
memset(&action, 0, sizeof(action));
|
||||
action.sa_handler = NULL;
|
||||
action.sa_flags = SA_NOCLDWAIT;
|
||||
action.sa_handler = sig_sigchld;
|
||||
action.sa_flags = SA_NOCLDSTOP;
|
||||
res = sigaction(SIGCHLD, &action, NULL);
|
||||
CHECK_RES_DIE(res, "sigaction");
|
||||
|
||||
|
||||
@@ -138,6 +138,8 @@ struct listen_endpoint {
|
||||
int socketfd; /* file descriptor of listening socket */
|
||||
int type; /* SOCK_DGRAM | SOCK_STREAM */
|
||||
int family; /* AF_INET | AF_UNIX */
|
||||
int num_connections; /* How many active connections on this endpoint */
|
||||
struct sslhcfg_listen_item* endpoint_cfg; /* the configuration item that corresponds to this endpoint */
|
||||
};
|
||||
|
||||
#define FD_CNXCLOSED 0
|
||||
@@ -201,4 +203,7 @@ void main_loop(struct listen_endpoint *listen_sockets, int num_addr_listen);
|
||||
void setup_landlock(void);
|
||||
|
||||
|
||||
/* sslh-fork.c or processes.c, depending on the model */
|
||||
void setup_sigchld(void);
|
||||
|
||||
#endif
|
||||
@@ -200,3 +200,12 @@ process both single-datagram protocols such as DNS, and
|
||||
connection-based protocols such as QUIC.
|
||||
|
||||
An example for supporting QUIC is shown in `example.cfg`.
|
||||
|
||||
|
||||
Limiting file descriptor consumption
|
||||
------------------------------------
|
||||
|
||||
There are various mechanisms to limit the number of
|
||||
concurrent connections, which allows to limit the usage of
|
||||
file descriptor. This is described in a separate
|
||||
[guide](doc/max_connections.md).
|
||||
@@ -0,0 +1,46 @@
|
||||
Limiting the number of connections
|
||||
----------------------------------
|
||||
|
||||
|
||||
As a network-facing service, `sslh` can be attacked by very
|
||||
simple denial-of-service attacks by creating a lot of
|
||||
concurrent connections. Protecting against such attacks is
|
||||
complicated from the server side. This attack will cause
|
||||
`sslh` to create a large number of file handles, which can
|
||||
cause exgagerated resource consumption.
|
||||
|
||||
This threat can be mitigated using several limitation
|
||||
mechanisms. Keep in mind that limiting the number of
|
||||
connections means that in case of an attack, the server
|
||||
resources are protected, but this might be at the expense of
|
||||
serving legitimate connections. In particular, in some cases
|
||||
this might mean SSH is no longer available.
|
||||
|
||||
There are several mechanisms to limit the number of
|
||||
connections:
|
||||
|
||||
- Use `ulimit` (see bash(3) or your shell's man page) to
|
||||
limit the number of file descriptors.
|
||||
|
||||
- In `sslh-select` and `sslh-ev`, you can set
|
||||
`max_connections` per protocol, and `sslh` will drop
|
||||
connections after probing if the count is exceeded.
|
||||
This should help in keeping SSH connections available even
|
||||
if an attacker is stuffing other protocols. Currently this
|
||||
does not work for forking protocols (support is planned)
|
||||
|
||||
- `sslh-fork` has an explicit design goal to be as simple as
|
||||
possible, which makes it impossible to implement limits per
|
||||
protocol (because protocol probing is performed after the
|
||||
process has forked). Instead, you can set `max_connections`
|
||||
for each `listen` entry. So the `sslh` process for each port
|
||||
will limit the number of concurrent connections to that
|
||||
port. Currently this is not supported in `sslh-select` and
|
||||
`sslh-ev` (support is planned).
|
||||
|
||||
|
||||
As of sslh 2.2.5, this is an experimental feature and not
|
||||
all use cases have been tested. If 2.2.5 does not exist yet,
|
||||
you must be on the Git's head, so it is even more
|
||||
experimtental! Do not hesitate to send feedback if you
|
||||
notice inconsistent behaviour.
|
||||
+7
-1
@@ -49,9 +49,15 @@ syslog_facility: "auth";
|
||||
|
||||
# List of interfaces on which we should listen
|
||||
# Options:
|
||||
# host: name of the interface to listen to
|
||||
# port: name of the service to listen to
|
||||
# is_udp: listen in the UDP domain (default is TCP)
|
||||
# is_unix: listen in the UNIX socket domain (default is TCP)
|
||||
# keepalive: set TCP_KEEPALIVE
|
||||
# max_connections: drop incoming connections after this count (sslh-fork only)
|
||||
listen:
|
||||
(
|
||||
{ host: "thelonious"; port: "443"; },
|
||||
{ host: "thelonious"; port: "443"; max_connections: 5; },
|
||||
{ host: "thelonious"; port: "8080"; keepalive: true; },
|
||||
{ host: "thelonious"; is_udp: true; port: "443"; },
|
||||
{ host: "/tmp/unix_socket"; is_unix: true; port: ""; }
|
||||
|
||||
+23
-2
@@ -1,5 +1,5 @@
|
||||
/* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README)
|
||||
* on Wed Jul 16 17:59:23 2025.
|
||||
* on Mon Aug 4 18:25:44 2025.
|
||||
|
||||
# conf2struct: generate libconf parsers that read to structs
|
||||
# Copyright (C) 2018-2024 Yves Rutschle
|
||||
@@ -809,7 +809,7 @@ static struct config_desc table_sslhcfg_protocols[] = {
|
||||
},
|
||||
{ 0 }
|
||||
};
|
||||
|
||||
|
||||
static struct config_desc table_sslhcfg_listen[] = {
|
||||
|
||||
|
||||
@@ -845,6 +845,22 @@ static struct config_desc table_sslhcfg_listen[] = {
|
||||
/* default_val*/ .default_val.def_string = NULL
|
||||
},
|
||||
|
||||
{
|
||||
/* name */ "max_connections",
|
||||
/* type */ CFG_INT,
|
||||
/* sub_group*/ NULL,
|
||||
/* arg_cl */ NULL,
|
||||
/* base_addr */ NULL,
|
||||
/* offset */ offsetof(struct sslhcfg_listen_item, max_connections),
|
||||
/* offset_len */ 0,
|
||||
/* offset_present */ offsetof(struct sslhcfg_listen_item, max_connections_is_present),
|
||||
/* size */ sizeof(int),
|
||||
/* array_type */ -1,
|
||||
/* mandatory */ 0,
|
||||
/* optional */ 1,
|
||||
/* default_val*/ .default_val.def_int = 0
|
||||
},
|
||||
|
||||
{
|
||||
/* name */ "is_udp",
|
||||
/* type */ CFG_BOOL,
|
||||
@@ -2484,6 +2500,11 @@ static void sslhcfg_listen_fprint(
|
||||
fprintf(out, "port: %s", sslhcfg_listen->port);
|
||||
fprintf(out, "\n");
|
||||
indent(out, depth);
|
||||
fprintf(out, "max_connections: %d", sslhcfg_listen->max_connections);
|
||||
if (! sslhcfg_listen->max_connections_is_present)
|
||||
fprintf(out, " <unset>");
|
||||
fprintf(out, "\n");
|
||||
indent(out, depth);
|
||||
fprintf(out, "is_udp: %d", sslhcfg_listen->is_udp);
|
||||
fprintf(out, "\n");
|
||||
indent(out, depth);
|
||||
|
||||
+3
-1
@@ -1,5 +1,5 @@
|
||||
/* Generated by conf2struct (https://www.rutschle.net/tech/conf2struct/README)
|
||||
* on Wed Jul 16 17:59:23 2025.
|
||||
* on Mon Aug 4 18:25:44 2025.
|
||||
|
||||
# conf2struct: generate libconf parsers that read to structs
|
||||
# Copyright (C) 2018-2024 Yves Rutschle
|
||||
@@ -43,6 +43,8 @@
|
||||
struct sslhcfg_listen_item {
|
||||
char* host;
|
||||
char* port;
|
||||
int max_connections_is_present;
|
||||
int max_connections;
|
||||
int is_udp;
|
||||
int is_unix;
|
||||
int keepalive;
|
||||
|
||||
+47
-3
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
sslh-fork: forking server
|
||||
|
||||
# Copyright (C) 2007-2021 Yves Rutschle
|
||||
# Copyright (C) 2007-2025 Yves Rutschle
|
||||
#
|
||||
# This program is free software; you can redistribute it
|
||||
# and/or modify it under the terms of the GNU General Public
|
||||
@@ -160,6 +160,35 @@ void set_listen_procname(struct listen_endpoint *listen_socket)
|
||||
#endif
|
||||
}
|
||||
|
||||
static void mask_sigchld(void)
|
||||
{
|
||||
struct sigaction action;
|
||||
|
||||
memset(&action, 0, sizeof(action));
|
||||
action.sa_handler = NULL;
|
||||
action.sa_flags = SA_NOCLDWAIT;
|
||||
int res = sigaction(SIGCHLD, &action, NULL);
|
||||
CHECK_RES_DIE(res, "sigaction");
|
||||
}
|
||||
|
||||
|
||||
extern volatile sig_atomic_t received_sigchld;
|
||||
/* EINTR, so we probably received a signal; check if it's SIGCHLD */
|
||||
static void process_signals(struct listen_endpoint* endpoint)
|
||||
{
|
||||
int chld;
|
||||
if (received_sigchld) {
|
||||
received_sigchld = 0;
|
||||
do {
|
||||
chld = waitpid(-1, NULL, WNOHANG);
|
||||
CHECK_RES_RETURN(chld, "waitpid", 0);
|
||||
if (chld) {
|
||||
endpoint->num_connections--;
|
||||
print_message(msg_fd, "child died, %d concurrent connections remaining\n", endpoint->num_connections);
|
||||
}
|
||||
} while (chld);
|
||||
}
|
||||
}
|
||||
|
||||
/* At least MacOS does not know these two options, so define them to something
|
||||
* equivalent for our use case */
|
||||
@@ -196,11 +225,24 @@ void tcp_listener(struct listen_endpoint* endpoint, int num_endpoints, int activ
|
||||
case ECONNABORTED:
|
||||
continue;
|
||||
|
||||
case EINTR:
|
||||
process_signals(endpoint);
|
||||
continue;
|
||||
|
||||
default: /* Otherwise, it's something wrong in our parameters, we fail */
|
||||
return;
|
||||
}
|
||||
}
|
||||
print_message(msg_fd, "accepted fd %d\n", in_socket);
|
||||
endpoint->num_connections++;
|
||||
print_message(msg_fd, "accepted fd %d (%d concurrent connections)\n", in_socket, endpoint->num_connections);
|
||||
if ((endpoint->endpoint_cfg->max_connections_is_present)
|
||||
&& (endpoint->num_connections > endpoint->endpoint_cfg->max_connections))
|
||||
{
|
||||
print_message(msg_fd, "too many connection, reclosing fd %d\n", in_socket);
|
||||
endpoint->num_connections--;
|
||||
close(in_socket);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch(fork()) {
|
||||
case -1: print_message(msg_system_error, "fork failed: err %d: %s\n", errno, strerror(errno));
|
||||
@@ -250,8 +292,10 @@ void main_loop(struct listen_endpoint listen_sockets[], int num_addr_listen)
|
||||
exit(0);
|
||||
break;
|
||||
|
||||
/* We're in the parent, we don't need to do anything */
|
||||
/* We're in the parent, which does nothing but wait to be killed by
|
||||
* SIGTERM, which it will send to its children */
|
||||
default:
|
||||
mask_sigchld();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ config: {
|
||||
items: (
|
||||
{ name: "host"; type: "string"; var: true; },
|
||||
{ name: "port"; type: "string"; var: true; },
|
||||
{ name: "max_connections"; type: "int"; optional: true; },
|
||||
{ name: "is_udp"; type: "bool"; default: false },
|
||||
{ name: "is_unix"; type: "bool"; default: false },
|
||||
{ name: "keepalive"; type: "bool"; default: false; }
|
||||
|
||||
@@ -19,7 +19,7 @@ verbose-config-error: 1; # print configuration errors
|
||||
verbose-connections: 1; # trace established incoming address to forward address
|
||||
verbose-connections-error: 1; # connection errors
|
||||
verbose-connections-try: 1; # connection attempts towards targets
|
||||
verbose-fd: 0; # file descriptor activity, open/close/whatnot
|
||||
verbose-fd: 1; # file descriptor activity, open/close/whatnot
|
||||
verbose-packets: 1; # hexdump packets on which probing is done
|
||||
verbose-probe-info: 0; # what's happening during the probe process
|
||||
verbose-probe-error: 1; # failures and problems during probing
|
||||
@@ -30,9 +30,9 @@ verbose-int-error: 1; # internal errors, the kind that should never happen
|
||||
# Options:
|
||||
listen:
|
||||
(
|
||||
{ host: "localhost"; port: "8080"; keepalive: true; },
|
||||
{ host: "localhost"; port: "8080"; keepalive: true; max_connections: 2; },
|
||||
{ host: "localhost"; port: "8081"; keepalive: true; },
|
||||
{ host: "ip4-localhost"; is_udp: true; port: "8086"; },
|
||||
# { host: "ip4-localhost"; is_udp: true; port: "8086"; },
|
||||
{ host: "/tmp/sslh.sock"; is_unix: true; port: ""; }
|
||||
);
|
||||
|
||||
@@ -52,12 +52,12 @@ max_connections: 1; },
|
||||
{ name: "xmpp"; host: "localhost"; port: "9009"; },
|
||||
{ name: "adb"; host: "localhost"; port: "9010"; },
|
||||
{ name: "syslog"; host: "localhost"; port: "9013"; },
|
||||
{ name: "regex"; host: "ip4-localhost"; is_udp: true; port: "9020";
|
||||
max_connections: 1;
|
||||
udp_timeout: 30;
|
||||
regex_patterns: [ "^foo" ];
|
||||
resolve_on_forward: true;
|
||||
},
|
||||
# { name: "regex"; host: "ip4-localhost"; is_udp: true; port: "9020";
|
||||
# max_connections: 1;
|
||||
# udp_timeout: 30;
|
||||
# regex_patterns: [ "^foo" ];
|
||||
# resolve_on_forward: true;
|
||||
# },
|
||||
{ name: "regex"; host: "localhost"; port: "9011";
|
||||
regex_patterns: [ "^foo", "^bar" ];
|
||||
minlength: 4;
|
||||
@@ -74,7 +74,7 @@ max_connections: 1; },
|
||||
{ name: "tls"; host: "localhost"; port: "9023"; alpn_protocols: [ "alpn3" ]; },
|
||||
{ name: "tls"; host: "localhost"; port: "9024"; sni_hostnames: [ "sni3" ]; },
|
||||
{ name: "tls"; host: "localhost"; port: "9025"; },
|
||||
{ name: "anyprot"; host: "ip4-localhost"; is_udp: true; port: "9999"; },
|
||||
# { name: "anyprot"; host: "ip4-localhost"; is_udp: true; port: "9999"; },
|
||||
{ name: "anyprot"; host: "localhost"; port: "9099"; }
|
||||
);
|
||||
|
||||
|
||||
Reference in new issue
Block a user