mirror of
https://dev.iopsys.eu/feed/iopsys.git
synced 2025-12-10 07:44:50 +01:00
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:
parent
9944917399
commit
ff24c2e1fa
2 changed files with 470 additions and 93 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue