From 2550fe6cd9de9734cbc41179931fd029d8d132d6 Mon Sep 17 00:00:00 2001 From: Denys Fedoryshchenko Date: Sun, 21 Dec 2025 10:44:22 +0200 Subject: radius: Implement DM/CoA security hardening by restricting source ip addresses Signed-off-by: Denys Fedoryshchenko --- README | 1 + accel-pppd/accel-ppp.conf | 1 + accel-pppd/accel-ppp.conf.5 | 4 + accel-pppd/radius/dm_coa.c | 9 ++ accel-pppd/radius/radius.c | 203 ++++++++++++++++++++++++++++++++++++++++++- accel-pppd/radius/radius_p.h | 2 +- 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 #include #include +#include #include #include #include @@ -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 - -- cgit v1.2.3