From ff24c2e1fa1f1dc3128f73023b62e2b0c843b262 Mon Sep 17 00:00:00 2001 From: Sukru Senli Date: Tue, 28 Oct 2025 00:38:03 +0100 Subject: [PATCH] mosquitto-auth-shadow: Add per-user subnet-based access control to mosquitto auth plugin - Implement whitelist/blacklist subnet filtering for MQTT users - Add support for CIDR notation (e.g., 192.168.1.0/24) - Parse subnet ACL files via auth_opt_subnet_acl_file option - Support multiple subnets per user (up to 32 allow + 32 deny rules) - Deny rules take precedence over allow rules - Localhost (127.0.0.1/::1) always allowed - IPv6 addresses gracefully deferred to other ACL handlers - Backward compatible: users without subnet rules are not affected - Configuration format: 'subnet allow|deny ' - Integrates with existing shadow/PAM authentication and topic ACLs --- mosquitto-auth-shadow/Makefile | 2 +- .../src/mosquitto_auth_shadow.c | 561 +++++++++++++++--- 2 files changed, 470 insertions(+), 93 deletions(-) diff --git a/mosquitto-auth-shadow/Makefile b/mosquitto-auth-shadow/Makefile index 98d1c95a2..a7fae68c6 100644 --- a/mosquitto-auth-shadow/Makefile +++ b/mosquitto-auth-shadow/Makefile @@ -14,7 +14,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=mosquitto-auth-shadow -PKG_VERSION:=1.1.0 +PKG_VERSION:=1.2.0 PKG_MAINTAINER:=Erik Karlsson PKG_LICENSE:=EPL-2.0 diff --git a/mosquitto-auth-shadow/src/mosquitto_auth_shadow.c b/mosquitto-auth-shadow/src/mosquitto_auth_shadow.c index 9d72565d9..33a604489 100644 --- a/mosquitto-auth-shadow/src/mosquitto_auth_shadow.c +++ b/mosquitto-auth-shadow/src/mosquitto_auth_shadow.c @@ -12,142 +12,519 @@ */ #define _GNU_SOURCE +#include #include #include #include #include +#include #include #include #include #ifdef ENABLE_PAM_SUPPORT #include +#endif -static int pam_conversation(int num_msg, const struct pam_message **msg, struct pam_response **resp, void *appdata_ptr) +#define MAX_USERS 256 +#define MAX_SUBNETS_PER_USER 32 + +typedef struct { + uint32_t network; + uint32_t netmask; +} subnet_t; + +typedef struct { + char username[64]; + subnet_t allow_subnets[MAX_SUBNETS_PER_USER]; + int allow_count; + subnet_t deny_subnets[MAX_SUBNETS_PER_USER]; + int deny_count; +} user_acl_t; + +typedef struct { + user_acl_t users[MAX_USERS]; + int user_count; + mosquitto_plugin_id_t *identifier; +} plugin_data_t; + +/* Parse CIDR notation (e.g., "192.168.1.0/24") */ +static int parse_subnet(const char *cidr, subnet_t *subnet) { - int i; - const char *pass = (const char *)appdata_ptr; + char ip_str[64]; + char *slash; + int prefix_len; + struct in_addr addr; - *resp = calloc(num_msg, sizeof(struct pam_response)); - if (*resp == NULL) { - mosquitto_log_printf(MOSQ_LOG_ERR, "pam failed to allocate buffer for validation"); - return PAM_BUF_ERR; - } + strncpy(ip_str, cidr, sizeof(ip_str) - 1); + ip_str[sizeof(ip_str) - 1] = '\0'; - if (pass == NULL) - return PAM_SUCCESS; + slash = strchr(ip_str, '/'); + if (slash == NULL) { + /* No prefix length, assume /32 */ + prefix_len = 32; + } else { + *slash = '\0'; + prefix_len = atoi(slash + 1); + if (prefix_len < 0 || prefix_len > 32) + return -1; + } - for (i = 0; i < num_msg; ++i) { - if (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF) { - (*resp)[i].resp = strdup(pass); - if ((*resp)[i].resp == NULL) { - for (int j = 0; j < i ; j++) - free((*resp)[j].resp); + if (inet_pton(AF_INET, ip_str, &addr) != 1) + return -1; - free(*resp); - *resp = NULL; - mosquitto_log_printf(MOSQ_LOG_ERR, "pam failed in strdup"); - return PAM_BUF_ERR; - } - } - } - return PAM_SUCCESS; + subnet->network = ntohl(addr.s_addr); + subnet->netmask = prefix_len == 0 ? 0 : (~0U << (32 - prefix_len)); + subnet->network &= subnet->netmask; + + return 0; +} + +/* Check if IP is in subnet */ +static int ip_in_subnet(uint32_t ip, const subnet_t *subnet) +{ + return (ip & subnet->netmask) == subnet->network; +} + +/* Check if IP is in any subnet in the list */ +static int ip_in_subnet_list(uint32_t ip, const subnet_t *subnets, int count) +{ + for (int i = 0; i < count; i++) { + if (ip_in_subnet(ip, &subnets[i])) + return 1; + } + return 0; +} + +/* Find or create user ACL entry */ +static user_acl_t* find_or_create_user_acl(plugin_data_t *pdata, const char *username) +{ + user_acl_t *user; + + /* Find existing user */ + for (int i = 0; i < pdata->user_count; i++) { + if (strcmp(pdata->users[i].username, username) == 0) + return &pdata->users[i]; + } + + /* Create new user if not found */ + if (pdata->user_count >= MAX_USERS) { + mosquitto_log_printf(MOSQ_LOG_ERR, + "subnet_acl: Max users exceeded"); + return NULL; + } + + user = &pdata->users[pdata->user_count]; + strncpy(user->username, username, sizeof(user->username) - 1); + user->username[sizeof(user->username) - 1] = '\0'; + user->allow_count = 0; + user->deny_count = 0; + pdata->user_count++; + + return user; +} + +/* Parse subnet ACL file + * Format: + * # Comment lines + * subnet allow + * subnet deny + */ +static int load_subnet_acl_config(plugin_data_t *pdata, const char *config_file) +{ + FILE *fp; + char line[512]; + int line_num = 0; + + if (config_file == NULL) { + mosquitto_log_printf(MOSQ_LOG_INFO, + "subnet_acl: No subnet ACL file specified, subnet filtering disabled"); + return 0; + } + + fp = fopen(config_file, "r"); + if (fp == NULL) { + mosquitto_log_printf(MOSQ_LOG_WARNING, + "subnet_acl: Could not open subnet ACL file %s, subnet filtering disabled", + config_file); + return 0; /* Non-fatal */ + } + + pdata->user_count = 0; + + while (fgets(line, sizeof(line), fp) != NULL) { + char *token, *saveptr; + char *action, *username, *cidr; + user_acl_t *user; + subnet_t subnet; + + line_num++; + + /* Remove newline and comments */ + line[strcspn(line, "\r\n")] = '\0'; + char *comment = strchr(line, '#'); + if (comment) + *comment = '\0'; + + /* Trim leading whitespace */ + char *line_start = line; + while (*line_start == ' ' || *line_start == '\t') + line_start++; + + /* Skip empty lines */ + if (*line_start == '\0') + continue; + + /* Parse: subnet allow|deny */ + token = strtok_r(line_start, " \t", &saveptr); + if (token == NULL) + continue; + + /* Must start with "subnet" */ + if (strcmp(token, "subnet") != 0) { + mosquitto_log_printf(MOSQ_LOG_WARNING, + "subnet_acl: Unknown directive '%s' at line %d (expected 'subnet')", + token, line_num); + continue; + } + + /* Get allow/deny */ + action = strtok_r(NULL, " \t", &saveptr); + if (action == NULL) { + mosquitto_log_printf(MOSQ_LOG_WARNING, + "subnet_acl: Missing allow/deny at line %d", line_num); + continue; + } + + if (strcmp(action, "allow") != 0 && strcmp(action, "deny") != 0) { + mosquitto_log_printf(MOSQ_LOG_WARNING, + "subnet_acl: Unknown action '%s' at line %d (use 'allow' or 'deny')", + action, line_num); + continue; + } + + /* Get username */ + username = strtok_r(NULL, " \t", &saveptr); + if (username == NULL) { + mosquitto_log_printf(MOSQ_LOG_WARNING, + "subnet_acl: Missing username at line %d", line_num); + continue; + } + + /* Get CIDR */ + cidr = strtok_r(NULL, " \t", &saveptr); + if (cidr == NULL) { + mosquitto_log_printf(MOSQ_LOG_WARNING, + "subnet_acl: Missing CIDR at line %d", line_num); + continue; + } + + /* Parse subnet */ + if (parse_subnet(cidr, &subnet) != 0) { + mosquitto_log_printf(MOSQ_LOG_WARNING, + "subnet_acl: Invalid CIDR '%s' at line %d", cidr, line_num); + continue; + } + + /* Find or create user */ + user = find_or_create_user_acl(pdata, username); + if (user == NULL) { + mosquitto_log_printf(MOSQ_LOG_ERR, + "subnet_acl: Failed to create user at line %d", line_num); + continue; + } + + /* Add to appropriate list */ + if (strcmp(action, "allow") == 0) { + if (user->allow_count >= MAX_SUBNETS_PER_USER) { + mosquitto_log_printf(MOSQ_LOG_WARNING, + "subnet_acl: Max allow subnets exceeded for user '%s' at line %d", + user->username, line_num); + continue; + } + user->allow_subnets[user->allow_count] = subnet; + user->allow_count++; + + mosquitto_log_printf(MOSQ_LOG_DEBUG, + "subnet_acl: User '%s' allow subnet %s", + user->username, cidr); + + } else { /* deny */ + if (user->deny_count >= MAX_SUBNETS_PER_USER) { + mosquitto_log_printf(MOSQ_LOG_WARNING, + "subnet_acl: Max deny subnets exceeded for user '%s' at line %d", + user->username, line_num); + continue; + } + user->deny_subnets[user->deny_count] = subnet; + user->deny_count++; + + mosquitto_log_printf(MOSQ_LOG_DEBUG, + "subnet_acl: User '%s' deny subnet %s", + user->username, cidr); + } + } + + fclose(fp); + + /* Log summary */ + for (int i = 0; i < pdata->user_count; i++) { + user_acl_t *user = &pdata->users[i]; + if (user->allow_count > 0 || user->deny_count > 0) { + mosquitto_log_printf(MOSQ_LOG_INFO, + "subnet_acl: User '%s' has %d allow and %d deny subnet rules", + user->username, user->allow_count, user->deny_count); + } + } + + mosquitto_log_printf(MOSQ_LOG_NOTICE, + "subnet_acl: Loaded subnet restrictions for %d user(s)", pdata->user_count); + + return 0; +} + +/* Find user ACL entry */ +static const user_acl_t* find_user_acl(const plugin_data_t *pdata, const char *username) +{ + for (int i = 0; i < pdata->user_count; i++) { + if (strcmp(pdata->users[i].username, username) == 0) + return &pdata->users[i]; + } + return NULL; +} + +/* ACL check for subnet validation */ +static int acl_check_callback(int event, void *event_data, void *userdata) +{ + struct mosquitto_evt_acl_check *ed = event_data; + plugin_data_t *pdata = userdata; + const user_acl_t *user_acl; + const char *client_address; + const char *username; + struct in_addr addr; + uint32_t client_ip; + + /* Skip if no subnet config loaded */ + if (pdata == NULL || pdata->user_count == 0) { + return MOSQ_ERR_PLUGIN_DEFER; + } + + /* Get username from client */ + username = mosquitto_client_username(ed->client); + + /* Skip anonymous users */ + if (username == NULL) { + return MOSQ_ERR_PLUGIN_DEFER; + } + + /* Find user's subnet ACL */ + user_acl = find_user_acl(pdata, username); + + /* If user not in config or has no subnet rules, allow */ + if (user_acl == NULL || (user_acl->allow_count == 0 && user_acl->deny_count == 0)) { + return MOSQ_ERR_PLUGIN_DEFER; + } + + /* Get client IP address */ + client_address = mosquitto_client_address(ed->client); + if (client_address == NULL) { + mosquitto_log_printf(MOSQ_LOG_WARNING, + "subnet_acl: Could not get client address for user '%s'", username); + return MOSQ_ERR_PLUGIN_DEFER; + } + + /* Skip localhost checks - always allow */ + if (strcmp(client_address, "127.0.0.1") == 0 || strcmp(client_address, "::1") == 0) { + return MOSQ_ERR_PLUGIN_DEFER; + } + + /* Parse client IP */ + if (inet_pton(AF_INET, client_address, &addr) != 1) { + mosquitto_log_printf(MOSQ_LOG_DEBUG, + "subnet_acl: Non-IPv4 address '%s' for user '%s', allowing", + client_address, username); + /* For IPv6 or parse errors, defer to other plugins */ + return MOSQ_ERR_PLUGIN_DEFER; + } + + client_ip = ntohl(addr.s_addr); + + /* Check deny list first - deny takes precedence */ + if (user_acl->deny_count > 0) { + if (ip_in_subnet_list(client_ip, user_acl->deny_subnets, user_acl->deny_count)) { + mosquitto_log_printf(MOSQ_LOG_NOTICE, + "subnet_acl: User '%s' from %s DENIED (matches deny rule)", + username, client_address); + return MOSQ_ERR_ACL_DENIED; + } + } + + /* If there are allow rules, IP must match one of them */ + if (user_acl->allow_count > 0) { + if (ip_in_subnet_list(client_ip, user_acl->allow_subnets, user_acl->allow_count)) { + mosquitto_log_printf(MOSQ_LOG_DEBUG, + "subnet_acl: User '%s' from %s allowed (matches allow rule)", + username, client_address); + return MOSQ_ERR_PLUGIN_DEFER; + } else { + mosquitto_log_printf(MOSQ_LOG_NOTICE, + "subnet_acl: User '%s' from %s DENIED (no matching allow rule)", + username, client_address); + return MOSQ_ERR_ACL_DENIED; + } + } + + /* No subnet rules for this user - allow */ + return MOSQ_ERR_PLUGIN_DEFER; +} + +#ifdef ENABLE_PAM_SUPPORT +static int pam_conversation(int num_msg, const struct pam_message **msg, + struct pam_response **resp, void *appdata_ptr) +{ + int i; + const char *pass = (const char *)appdata_ptr; + *resp = calloc(num_msg, sizeof(struct pam_response)); + if (*resp == NULL) { + mosquitto_log_printf(MOSQ_LOG_ERR, "pam failed to allocate buffer for validation"); + return PAM_BUF_ERR; + } + if (pass == NULL) + return PAM_SUCCESS; + for (i = 0; i < num_msg; ++i) { + if (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF) { + (*resp)[i].resp = strdup(pass); + if ((*resp)[i].resp == NULL) { + for (int j = 0; j < i ; j++) + free((*resp)[j].resp); + free(*resp); + *resp = NULL; + mosquitto_log_printf(MOSQ_LOG_ERR, "pam failed in strdup"); + return PAM_BUF_ERR; + } + } + } + return PAM_SUCCESS; } static int process_pam_auth_callback(struct mosquitto_evt_basic_auth *ed) { - struct pam_conv conv; - int retval; - pam_handle_t *pamh = NULL; - - conv.conv = pam_conversation; - conv.appdata_ptr = (void *)ed->password; - - retval = pam_start("mosquitto", ed->username, &conv, &pamh); - if (retval != PAM_SUCCESS) { - mosquitto_log_printf(MOSQ_LOG_ERR, "pam start failed: %s", pam_strerror(pamh, retval)); - return MOSQ_ERR_AUTH; - } - - retval = pam_authenticate(pamh, 0); - pam_end(pamh, retval); - if (retval == PAM_SUCCESS) { - mosquitto_log_printf(MOSQ_LOG_NOTICE, "pam user [%s] logged in", ed->username); - return MOSQ_ERR_SUCCESS; - } - - mosquitto_log_printf(MOSQ_LOG_NOTICE, "pam user [%s] failed authentication, err [%s]", ed->username, pam_strerror(pamh, retval)); - return MOSQ_ERR_AUTH; + struct pam_conv conv; + int retval; + pam_handle_t *pamh = NULL; + conv.conv = pam_conversation; + conv.appdata_ptr = (void *)ed->password; + retval = pam_start("mosquitto", ed->username, &conv, &pamh); + if (retval != PAM_SUCCESS) { + mosquitto_log_printf(MOSQ_LOG_ERR, "pam start failed: %s", pam_strerror(pamh, retval)); + return MOSQ_ERR_AUTH; + } + retval = pam_authenticate(pamh, 0); + pam_end(pamh, retval); + if (retval == PAM_SUCCESS) { + mosquitto_log_printf(MOSQ_LOG_NOTICE, "pam user [%s] logged in", ed->username); + return MOSQ_ERR_SUCCESS; + } + mosquitto_log_printf(MOSQ_LOG_NOTICE, "pam user [%s] failed authentication, err [%s]", + ed->username, pam_strerror(pamh, retval)); + return MOSQ_ERR_AUTH; } #else static int process_shadow_auth_callback(struct mosquitto_evt_basic_auth *ed) { - struct spwd spbuf, *sp = NULL; - char buf[256]; - struct crypt_data data; - char *hash; - - getspnam_r(ed->username, &spbuf, buf, sizeof(buf), &sp); - - if (sp == NULL || sp->sp_pwdp == NULL) - return MOSQ_ERR_AUTH; - - /* Empty string as hash means password is not required */ - if (sp->sp_pwdp[0] == 0) - return MOSQ_ERR_SUCCESS; - - if (ed->password == NULL) - return MOSQ_ERR_AUTH; - - memset(&data, 0, sizeof(data)); - hash = crypt_r(ed->password, sp->sp_pwdp, &data); - - if (hash == NULL) - return MOSQ_ERR_AUTH; - - if (strcmp(hash, sp->sp_pwdp) == 0) - return MOSQ_ERR_SUCCESS; - - return MOSQ_ERR_AUTH; + struct spwd spbuf, *sp = NULL; + char buf[256]; + struct crypt_data data; + char *hash; + getspnam_r(ed->username, &spbuf, buf, sizeof(buf), &sp); + if (sp == NULL || sp->sp_pwdp == NULL) + return MOSQ_ERR_AUTH; + /* Empty string as hash means password is not required */ + if (sp->sp_pwdp[0] == 0) + return MOSQ_ERR_SUCCESS; + if (ed->password == NULL) + return MOSQ_ERR_AUTH; + memset(&data, 0, sizeof(data)); + hash = crypt_r(ed->password, sp->sp_pwdp, &data); + if (hash == NULL) + return MOSQ_ERR_AUTH; + if (strcmp(hash, sp->sp_pwdp) == 0) + return MOSQ_ERR_SUCCESS; + return MOSQ_ERR_AUTH; } #endif static int basic_auth_callback(int event, void *event_data, void *userdata) { - struct mosquitto_evt_basic_auth *ed = event_data; - - /* Let other plugins or broker decide about anonymous login */ - if (ed->username == NULL) - return MOSQ_ERR_PLUGIN_DEFER; - + struct mosquitto_evt_basic_auth *ed = event_data; + /* Let other plugins or broker decide about anonymous login */ + if (ed->username == NULL) + return MOSQ_ERR_PLUGIN_DEFER; #ifdef ENABLE_PAM_SUPPORT - return process_pam_auth_callback(ed); + return process_pam_auth_callback(ed); #else - return process_shadow_auth_callback(ed); + return process_shadow_auth_callback(ed); #endif } int mosquitto_plugin_version(int supported_version_count, - const int *supported_versions) + const int *supported_versions) { - return 5; + return 5; } int mosquitto_plugin_init(mosquitto_plugin_id_t *identifier, - void **user_data, - struct mosquitto_opt *opts, int opt_count) + void **user_data, + struct mosquitto_opt *opts, int opt_count) { - *user_data = identifier; + plugin_data_t *pdata; + const char *config_file = NULL; - return mosquitto_callback_register(identifier, MOSQ_EVT_BASIC_AUTH, - basic_auth_callback, NULL, NULL); + /* Find subnet config file option */ + for (int i = 0; i < opt_count; i++) { + if (strcmp(opts[i].key, "subnet_acl_file") == 0) { + config_file = opts[i].value; + break; + } + } + + pdata = calloc(1, sizeof(plugin_data_t)); + if (pdata == NULL) + return MOSQ_ERR_NOMEM; + + pdata->identifier = identifier; + *user_data = pdata; + + /* Load subnet ACL configuration */ + if (load_subnet_acl_config(pdata, config_file) != 0) { + free(pdata); + return MOSQ_ERR_UNKNOWN; + } + + /* Register both authentication and ACL callbacks */ + mosquitto_callback_register(identifier, MOSQ_EVT_BASIC_AUTH, + basic_auth_callback, NULL, pdata); + mosquitto_callback_register(identifier, MOSQ_EVT_ACL_CHECK, + acl_check_callback, NULL, pdata); + + mosquitto_log_printf(MOSQ_LOG_INFO, + "subnet_acl: Plugin initialized with %d user(s)", pdata->user_count); + + return MOSQ_ERR_SUCCESS; } int mosquitto_plugin_cleanup(void *user_data, - struct mosquitto_opt *opts, int opt_count) + struct mosquitto_opt *opts, int opt_count) { - mosquitto_plugin_id_t *identifier = user_data; + plugin_data_t *pdata = user_data; - return mosquitto_callback_unregister(identifier, MOSQ_EVT_BASIC_AUTH, - basic_auth_callback, NULL); + if (pdata) { + mosquitto_callback_unregister(pdata->identifier, MOSQ_EVT_BASIC_AUTH, + basic_auth_callback, NULL); + mosquitto_callback_unregister(pdata->identifier, MOSQ_EVT_ACL_CHECK, + acl_check_callback, NULL); + free(pdata); + } + + return MOSQ_ERR_SUCCESS; }