From 02685d49b67e742f595749dbf28c2429fabd518f Mon Sep 17 00:00:00 2001 From: "Roger A. Light" Date: Fri, 17 Sep 2021 11:33:09 +0100 Subject: [PATCH] Add `global_max_clients` option. This allows limiting client sessions globally on the broker. --- ChangeLog.txt | 2 + man/mosquitto.conf.5.xml | 26 +++++++ mosquitto.conf | 19 +++++- src/conf.c | 3 + src/handle_connect.c | 8 +++ src/mosquitto_broker_internal.h | 1 + test/broker/01-connect-global-max-clients.py | 72 ++++++++++++++++++++ test/broker/Makefile | 1 + test/broker/test.py | 1 + 9 files changed, 131 insertions(+), 2 deletions(-) create mode 100755 test/broker/01-connect-global-max-clients.py diff --git a/ChangeLog.txt b/ChangeLog.txt index 73a38eeb..081bf362 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -56,6 +56,8 @@ Broker: `enable_control_api` option. - Add support for `getPluginInfo` to mosquitto_ctrl. - Add `mosquitto_client_port()` function for plugins. +- Add `global_max_clients` option to allow limiting client sessions globally + on the broker. Client library: - Add MOSQ_OPT_DISABLE_SOCKETPAIR to allow the disabling of the socketpair diff --git a/man/mosquitto.conf.5.xml b/man/mosquitto.conf.5.xml index fda98c0d..eb7227d0 100644 --- a/man/mosquitto.conf.5.xml +++ b/man/mosquitto.conf.5.xml @@ -403,6 +403,32 @@ Reloaded on reload signal. + + count + + + The maximum number of client sessions to allow across + the whole broker. In this context a client session means + either a client currently connected via the network, or + a client that has clean_session = False (MQTT v3.x) and + is disconnected, or has disconnected and still hasn't + exceeded its session expiry interval (MQTT v5). + + + + See also the max_connections setting, which applies to + listeners. If you set + to 1000 and on a + listener to 10, then that means only 10 simultaneous + connections will be allowed at once, with an overall + maximum of 1000 client sessions. + + + This option applies globally. + Defaults to -1 (unlimited) + Reloaded on reload signal. + + file path diff --git a/mosquitto.conf b/mosquitto.conf index 4ef45146..191c8085 100644 --- a/mosquitto.conf +++ b/mosquitto.conf @@ -59,6 +59,19 @@ # retained message will always be published. This affects all listeners. #check_retain_source true +# The maximum number of client sessions to allow across the whole broker. In +# this context a client session means either a client currently connected via +# the network, or a client that has clean_session = False (MQTT v3.x) and is +# disconnected, or has disconnected and still hasn't exceeded its session +# expiry interval (MQTT v5). +# +# See also the max_connections setting, which applies to listeners. If you set +# global_max_clients to 1000 and max_connections on a listener to 10, then that +# means only 10 simultaneous connections will be allowed at once, with an +# overall maximum of 1000 client sessions. +# +#global_max_clients -1 + # QoS 1 and 2 messages will be allowed inflight per client until this limit # is exceeded. Defaults to 0. (No maximum) # See also max_inflight_messages @@ -256,8 +269,10 @@ #http_dir # The maximum number of client connections to allow. This is -# a per listener setting. -# Default is -1, which means unlimited connections. +# a per listener setting. Use global_max_clients if you wish to enforce a +# client limit across the whole broker. +# Default is -1, which means unlimited connections, unless otherwise limited by +# global_max_clients. # Note that other process limits mean that unlimited connections # are not really possible. Typically the default maximum number of # connections possible is around 1024. diff --git a/src/conf.c b/src/conf.c index b7f5f372..021478d9 100644 --- a/src/conf.c +++ b/src/conf.c @@ -192,6 +192,7 @@ static void config__init_reload(struct mosquitto__config *config) config->log_timestamp = true; mosquitto__free(config->log_timestamp_format); config->log_timestamp_format = NULL; + config->global_max_clients = -1; config->max_keepalive = 65535; config->max_packet_size = 0; config->max_inflight_messages = 20; @@ -1471,6 +1472,8 @@ static int config__read_file_core(struct mosquitto__config *config, bool reload, #else log__printf(NULL, MOSQ_LOG_WARNING, "Warning: $CONTROL support not available (enable_control_api)."); #endif + }else if(!strcmp(token, "global_max_clients")){ + if(conf__parse_int(&token, "global_max_clients", &config->global_max_clients, &saveptr)) return MOSQ_ERR_INVAL; }else if(!strcmp(token, "http_dir")){ #ifdef WITH_WEBSOCKETS if(reload) continue; /* Listeners not valid for reloading. */ diff --git a/src/handle_connect.c b/src/handle_connect.c index ba7ae845..901dd990 100644 --- a/src/handle_connect.c +++ b/src/handle_connect.c @@ -207,6 +207,14 @@ int connect__on_authorised(struct mosquitto *context, void *auth_data_out, uint1 do_disconnect(found_context, MOSQ_ERR_SUCCESS); } + if(db.config->global_max_clients > 0 && HASH_CNT(hh_id, db.contexts_by_id) >= (unsigned int)db.config->global_max_clients){ + if(context->protocol == mosq_p_mqtt5){ + send__connack(context, 0, MQTT_RC_SERVER_BUSY, NULL); + } + rc = MOSQ_ERR_INVAL; + goto error; + } + rc = acl__find_acls(context); if(rc){ free(auth_data_out); diff --git a/src/mosquitto_broker_internal.h b/src/mosquitto_broker_internal.h index f3e3f506..4bb2f2e4 100644 --- a/src/mosquitto_broker_internal.h +++ b/src/mosquitto_broker_internal.h @@ -273,6 +273,7 @@ struct mosquitto__config { int cmd_port_count; bool daemon; bool enable_control_api; + int global_max_clients; struct mosquitto__listener default_listener; struct mosquitto__listener *listeners; int listener_count; diff --git a/test/broker/01-connect-global-max-clients.py b/test/broker/01-connect-global-max-clients.py new file mode 100755 index 00000000..3ad021d0 --- /dev/null +++ b/test/broker/01-connect-global-max-clients.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 + +# Test whether global_max_clients works + +from mosq_test_helper import * + +def write_config(filename, port): + with open(filename, 'w') as f: + f.write("listener %d\n" % (port)) + f.write("allow_anonymous true\n") + f.write("global_max_clients 10\n") + +def do_test(): + rc = 1 + + connect_packets_ok = [] + connack_packets_ok = [] + connect_props = mqtt5_props.gen_uint32_prop(mqtt5_props.PROP_SESSION_EXPIRY_INTERVAL, 60) + for i in range(0, 10): + connect_packets_ok.append(mosq_test.gen_connect("max-conn-%d"%i, proto_ver=5, properties=connect_props)) + connack_packets_ok.append(mosq_test.gen_connack(rc=0, proto_ver=5)) + + connect_packet_bad = mosq_test.gen_connect("max-conn-bad", proto_ver=5) + connack_packet_bad = mosq_test.gen_connack(rc=mqtt5_rc.MQTT_RC_SERVER_BUSY, proto_ver=5, property_helper=False) + + port = mosq_test.get_port() + conf_file = os.path.basename(__file__).replace('.py', '.conf') + write_config(conf_file, port) + broker = mosq_test.start_broker(filename=os.path.basename(__file__), use_conf=True, port=port) + + socks = [] + try: + # Open all allowed connections, a limit of 10 + for i in range(0, 10): + socks.append(mosq_test.do_client_connect(connect_packets_ok[i], connack_packets_ok[i], port=port)) + + # Try to open an 11th connection + try: + sock_bad = mosq_test.do_client_connect(connect_packet_bad, connack_packet_bad, port=port) + except ConnectionResetError: + # Expected behaviour + pass + + # Close all allowed connections + for i in range(0, 10): + socks[i].close() + + ## Session expiry means those clients sessions are still active + + # Try to open an 11th connection + try: + sock_bad = mosq_test.do_client_connect(connect_packet_bad, connack_packet_bad, port=port) + except ConnectionResetError: + # Expected behaviour + pass + + rc = 0 + except mosq_test.TestError: + pass + except Exception as err: + print(err) + finally: + os.remove(conf_file) + broker.terminate() + broker.wait() + (stdo, stde) = broker.communicate() + if rc: + print(stde.decode('utf-8')) + exit(rc) + +do_test() +exit(0) diff --git a/test/broker/Makefile b/test/broker/Makefile index 305ca264..b7e76732 100644 --- a/test/broker/Makefile +++ b/test/broker/Makefile @@ -26,6 +26,7 @@ msg_sequence_test: ./01-connect-575314.py ./01-connect-allow-anonymous.py ./01-connect-disconnect-v5.py + ./01-connect-global-max-clients.py ./01-connect-max-connections.py ./01-connect-max-keepalive.py ./01-connect-uname-no-password-denied.py diff --git a/test/broker/test.py b/test/broker/test.py index 20e3bc19..0e986aa5 100755 --- a/test/broker/test.py +++ b/test/broker/test.py @@ -8,6 +8,7 @@ tests = [ (1, './01-connect-575314.py'), (1, './01-connect-allow-anonymous.py'), (1, './01-connect-disconnect-v5.py'), + (1, './01-connect-global-max-clients.py'), (1, './01-connect-max-connections.py'), (1, './01-connect-max-keepalive.py'), (1, './01-connect-uname-no-password-denied.py'),