sslh-fork can now limit the number of concurrent connections for each incoming port

This commit is contained in:
yrutschle committed 2025-08-05 18:15:58 +02:00
1 parent 1dfd70c20c
commit 20290bbfa6
10 files changed
+164 -21

No files matched your search

+13 -4
View File
@@ -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");
+5
View File
@@ -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
+9
View File
@@ -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).
+46
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
}
+1
View File
@@ -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; }
+10 -10
View File
@@ -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"; }
);