summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDenys Fedoryshchenko <denys.f@collabora.com>2026-01-05 00:08:12 +0200
committerGitHub <noreply@github.com>2026-01-05 00:08:12 +0200
commitb199d14484e5b8d07dd6a1cd7b865dd84f09e73a (patch)
treed8b33fc2d803b5a9a911a81e628931b3d5a8478b
parentdd41ff523be4b88c70a3d36baa43914983721b63 (diff)
parent2550fe6cd9de9734cbc41179931fd029d8d132d6 (diff)
downloadaccel-ppp-b199d14484e5b8d07dd6a1cd7b865dd84f09e73a.tar.gz
accel-ppp-b199d14484e5b8d07dd6a1cd7b865dd84f09e73a.zip
Merge pull request #285 from nuclearcat/add-coa-security
radius: Implement DM/CoA security hardening by restricting source ips
-rw-r--r--README1
-rw-r--r--accel-pppd/accel-ppp.conf1
-rw-r--r--accel-pppd/accel-ppp.conf.54
-rw-r--r--accel-pppd/radius/dm_coa.c9
-rw-r--r--accel-pppd/radius/radius.c203
-rw-r--r--accel-pppd/radius/radius_p.h2
6 files changed, 218 insertions, 2 deletions
diff --git a/README b/README
index fe871188..c91a4e0b 100644
--- a/README
+++ b/README
@@ -64,6 +64,7 @@ or specify other location via KDIR.
Configuration
-------------
read man accel-ppp.conf
+For DM/CoA deployments, use the `dae-allowed` option to restrict source IPs.
Built-in shaper
diff --git a/accel-pppd/accel-ppp.conf b/accel-pppd/accel-ppp.conf
index 3f277a16..a7f66e5e 100644
--- a/accel-pppd/accel-ppp.conf
+++ b/accel-pppd/accel-ppp.conf
@@ -214,6 +214,7 @@ nas-ip-address=127.0.0.1
gw-ip-address=192.168.100.1
server=127.0.0.1,testing123,auth-port=1812,acct-port=1813,req-limit=50,fail-timeout=0,max-fail=10,weight=1
dae-server=127.0.0.1:3799,testing123
+#dae-allowed=127.0.0.1,192.0.2.0/24
verbose=1
#timeout=3
#max-try=3
diff --git a/accel-pppd/accel-ppp.conf.5 b/accel-pppd/accel-ppp.conf.5
index 22d67281..9339d030 100644
--- a/accel-pppd/accel-ppp.conf.5
+++ b/accel-pppd/accel-ppp.conf.5
@@ -1029,6 +1029,10 @@ Specifies IP address, port to bind and secret for Dynamic Authorization Extensio
- device name or VRF to bind the connection. By default, the device name is undefined.
.br
.TP
+.BI "dae-allowed=" ip[,ip/cidr][,ip[/cidr]...]
+Specifies allowed source IPv4 addresses or CIDR ranges for DM/CoA requests.
+.br
+.TP
.BI "dm_coa_secret=" secret (deprecated, use dae-server instead)
Specifies secret to use in DM/CoA communication.
.TP
diff --git a/accel-pppd/radius/dm_coa.c b/accel-pppd/radius/dm_coa.c
index 5f4220d1..df8cdf3e 100644
--- a/accel-pppd/radius/dm_coa.c
+++ b/accel-pppd/radius/dm_coa.c
@@ -267,6 +267,15 @@ static int dm_coa_read(struct triton_md_handler_t *h)
rad_packet_print(pack, NULL, log_debug);
}
+ if (rad_dae_src_check(addr.sin_addr.s_addr)) {
+ char ipbuf[INET_ADDRSTRLEN];
+ const char *ipstr;
+
+ ipstr = inet_ntop(AF_INET, &addr.sin_addr, ipbuf, sizeof(ipbuf));
+ log_warn("radius:dm_coa: source %s not allowed\n", ipstr ? ipstr : "unknown");
+ goto out_err_no_reply;
+ }
+
if (dm_coa_check_RA(pack, conf_dm_coa_secret)) {
log_warn("radius:dm_coa: RA validation failed\n");
goto out_err_no_reply;
diff --git a/accel-pppd/radius/radius.c b/accel-pppd/radius/radius.c
index 9fbbc467..1ef27f6d 100644
--- a/accel-pppd/radius/radius.c
+++ b/accel-pppd/radius/radius.c
@@ -2,6 +2,7 @@
#include <stdlib.h>
#include <stdarg.h>
#include <stdbool.h>
+#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
@@ -63,6 +64,15 @@ int conf_blast_protection;
static LIST_HEAD(sessions);
static pthread_rwlock_t sessions_lock = PTHREAD_RWLOCK_INITIALIZER;
+struct dae_allow_range {
+ struct list_head entry;
+ uint32_t begin;
+ uint32_t end;
+};
+
+static struct list_head *dae_allow_ranges;
+static pthread_rwlock_t dae_allow_lock = PTHREAD_RWLOCK_INITIALIZER;
+
static void *pd_key;
static struct ipdb_t ipdb;
@@ -111,7 +121,7 @@ static void parse_framed_route(struct radius_pd_t *rpd, const char *attr)
if (a == INADDR_NONE)
goto out_err;
mask = 33 - htonl(inet_addr(str));
- if (~((1<<(32 - mask)) - 1) != a)
+ if ((uint32_t)(~(((uint64_t)1 << (32 - mask)) - 1)) != a)
goto out_err;
} else if (*ptr2 == ' ' || *ptr2 == 0) {
char *ptr3;
@@ -157,6 +167,155 @@ out_err:
log_ppp_warn("radius: failed to parse Framed-Route=%s\n", attr);
}
+static char *trim_spaces(char *str)
+{
+ char *end;
+
+ while (isspace((unsigned char)*str))
+ ++str;
+
+ if (*str == '\0')
+ return str;
+
+ end = str + strlen(str) - 1;
+ while (end > str && isspace((unsigned char)*end)) {
+ *end = '\0';
+ --end;
+ }
+
+ return str;
+}
+
+static void dae_allow_clear(void)
+{
+ struct dae_allow_range *range;
+
+ if (!dae_allow_ranges)
+ return;
+
+ while (!list_empty(dae_allow_ranges)) {
+ range = list_first_entry(dae_allow_ranges, typeof(*range), entry);
+ list_del(&range->entry);
+ _free(range);
+ }
+
+ _free(dae_allow_ranges);
+ dae_allow_ranges = NULL;
+}
+
+static int dae_allow_add(uint32_t begin, uint32_t end)
+{
+ struct dae_allow_range *range;
+
+ if (!dae_allow_ranges)
+ return -1;
+
+ range = _malloc(sizeof(*range));
+ if (!range)
+ return -1;
+
+ range->begin = begin;
+ range->end = end;
+ list_add_tail(&range->entry, dae_allow_ranges);
+
+ return 0;
+}
+
+static int dae_allow_parse_token(const char *token, uint32_t *begin, uint32_t *end)
+{
+ struct in_addr base_addr;
+ uint8_t suffix;
+ size_t len;
+
+ len = u_parse_ip4cidr(token, &base_addr, &suffix);
+ if (len && token[len] == '\0') {
+ uint32_t addr_hbo = ntohl(base_addr.s_addr);
+ uint32_t mask = (uint64_t)0xffffffff << (32 - suffix);
+ uint32_t ip_min = addr_hbo & mask;
+
+ *begin = ip_min;
+ *end = addr_hbo | ~mask;
+ if (ip_min != addr_hbo) {
+ struct in_addr min_addr = { .s_addr = htonl(ip_min) };
+ char ipbuf[INET_ADDRSTRLEN];
+
+ log_warn("radius: dae-allowed network %s is equivalent to %s/%hhu\n",
+ token, u_ip4str(&min_addr, ipbuf), suffix);
+ }
+ return 0;
+ }
+
+ if (!inet_aton(token, &base_addr))
+ return -1;
+
+ *begin = ntohl(base_addr.s_addr);
+ *end = *begin;
+ return 0;
+}
+
+static int dae_allow_parse(const char *opt, int *entries)
+{
+ char *dup;
+ char *token;
+ char *saveptr;
+ int count = 0;
+
+ dup = _strdup(opt);
+ if (!dup)
+ return -1;
+
+ for (token = strtok_r(dup, ",", &saveptr); token;
+ token = strtok_r(NULL, ",", &saveptr)) {
+ char *trimmed = trim_spaces(token);
+ uint32_t begin;
+ uint32_t end;
+
+ if (*trimmed == '\0')
+ continue;
+
+ if (dae_allow_parse_token(trimmed, &begin, &end)) {
+ log_warn("radius: dae-allowed invalid entry \"%s\"\n", trimmed);
+ continue;
+ }
+
+ if (dae_allow_add(begin, end)) {
+ _free(dup);
+ return -1;
+ }
+
+ ++count;
+ }
+
+ _free(dup);
+ *entries = count;
+ return 0;
+}
+
+static int dae_allow_check(in_addr_t ipaddr)
+{
+ struct dae_allow_range *range;
+ uint32_t addr = ntohl(ipaddr);
+
+ list_for_each_entry(range, dae_allow_ranges, entry) {
+ if (addr >= range->begin && addr <= range->end)
+ return 0;
+ }
+
+ return -1;
+}
+
+int rad_dae_src_check(in_addr_t ipaddr)
+{
+ int res = 0;
+
+ pthread_rwlock_rdlock(&dae_allow_lock);
+ if (dae_allow_ranges)
+ res = dae_allow_check(ipaddr);
+ pthread_rwlock_unlock(&dae_allow_lock);
+
+ return res;
+}
+
/* Parse a RADIUS Framed-IPv6-Route string.
*
* Full format is like: "2001:db8::/32 fc00::1 2000"
@@ -1047,6 +1206,48 @@ static int load_config(void)
return -1;
}
+ {
+ int entries = 0;
+ int dae_allowed_present = 0;
+ int parse_rc = 0;
+
+ pthread_rwlock_wrlock(&dae_allow_lock);
+ dae_allow_clear();
+ opt = conf_get_opt("radius", "dae-allowed");
+ if (opt) {
+ dae_allowed_present = 1;
+ dae_allow_ranges = _malloc(sizeof(*dae_allow_ranges));
+ if (!dae_allow_ranges) {
+ log_emerg("radius: failed to allocate dae-allowed list\n");
+ parse_rc = -1;
+ goto dae_allow_unlock;
+ }
+ INIT_LIST_HEAD(dae_allow_ranges);
+ if (dae_allow_parse(opt, &entries)) {
+ log_emerg("radius: failed to parse dae-allowed\n");
+ dae_allow_clear();
+ parse_rc = -1;
+ goto dae_allow_unlock;
+ }
+ }
+
+ if (dae_allowed_present && entries == 0) {
+ log_warn("radius: dae-allowed has no valid entries, DAE source restrictions are disabled\n");
+ dae_allow_clear();
+ }
+
+ if (conf_dm_coa_secret && !dae_allow_ranges) {
+ if (dae_allowed_present)
+ log_warn("radius: dae-server configured without a valid dae-allowed list; this will become mandatory\n");
+ else
+ log_warn("radius: dae-server configured without dae-allowed; this will become mandatory\n");
+ }
+dae_allow_unlock:
+ pthread_rwlock_unlock(&dae_allow_lock);
+ if (parse_rc)
+ return -1;
+ }
+
opt = conf_get_opt("radius", "sid-in-auth");
if (opt)
conf_sid_in_auth = atoi(opt);
diff --git a/accel-pppd/radius/radius_p.h b/accel-pppd/radius/radius_p.h
index 06588ead..54effa41 100644
--- a/accel-pppd/radius/radius_p.h
+++ b/accel-pppd/radius/radius_p.h
@@ -240,6 +240,7 @@ void rad_packet_print(struct rad_packet_t *pack, struct rad_server_t *s, void (*
int rad_packet_send(struct rad_packet_t *pck, int fd, struct sockaddr_in *addr);
void dm_coa_cancel(struct radius_pd_t *pd);
+int rad_dae_src_check(in_addr_t ipaddr);
struct rad_server_t *rad_server_get(int);
struct rad_server_t *rad_server_get2(int, in_addr_t, int);
@@ -267,4 +268,3 @@ unsigned long stat_accm_get_cnt(struct stat_accm_t *);
unsigned long stat_accm_get_avg(struct stat_accm_t *);
#endif
-