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 <username> <cidr>'
- Integrates with existing shadow/PAM authentication and topic ACLs
This commit is contained in:
Sukru Senli 2025-10-28 00:38:03 +01:00
parent 9944917399
commit ff24c2e1fa
2 changed files with 470 additions and 93 deletions

View file

@ -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 <erik.karlsson@genexis.eu>
PKG_LICENSE:=EPL-2.0

View file

@ -12,38 +12,391 @@
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <shadow.h>
#include <crypt.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <mosquitto.h>
#include <mosquitto_broker.h>
#include <mosquitto_plugin.h>
#ifdef ENABLE_PAM_SUPPORT
#include <security/pam_appl.h>
#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)
{
char ip_str[64];
char *slash;
int prefix_len;
struct in_addr addr;
strncpy(ip_str, cidr, sizeof(ip_str) - 1);
ip_str[sizeof(ip_str) - 1] = '\0';
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;
}
if (inet_pton(AF_INET, ip_str, &addr) != 1)
return -1;
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 <username> <cidr>
* subnet deny <username> <cidr>
*/
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 <username> <cidr> */
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");
@ -59,24 +412,21 @@ 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));
mosquitto_log_printf(MOSQ_LOG_NOTICE, "pam user [%s] failed authentication, err [%s]",
ed->username, pam_strerror(pamh, retval));
return MOSQ_ERR_AUTH;
}
#else
@ -86,28 +436,20 @@ static int process_shadow_auth_callback(struct mosquitto_evt_basic_auth *ed)
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
@ -115,11 +457,9 @@ static int process_shadow_auth_callback(struct mosquitto_evt_basic_auth *ed)
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;
#ifdef ENABLE_PAM_SUPPORT
return process_pam_auth_callback(ed);
#else
@ -137,17 +477,54 @@ int mosquitto_plugin_init(mosquitto_plugin_id_t *identifier,
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)
{
mosquitto_plugin_id_t *identifier = user_data;
plugin_data_t *pdata = user_data;
return mosquitto_callback_unregister(identifier, MOSQ_EVT_BASIC_AUTH,
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;
}