summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDenys Fedoryshchenko <denys.f@collabora.com>2026-05-13 22:44:13 +0300
committerDenys Fedoryshchenko <denys.f@collabora.com>2026-05-14 10:04:56 +0300
commitd5a0c7da380ff2efc359b939ac9abd5ffc10df69 (patch)
tree8f577ff0bddf26ad89d5fb9dfcef2e5ff41fd5f1
parentec10b09d60f2f2e5cd20f4c934791fe71e9be265 (diff)
downloadaccel-ppp-d5a0c7da380ff2efc359b939ac9abd5ffc10df69.tar.gz
accel-ppp-d5a0c7da380ff2efc359b939ac9abd5ffc10df69.zip
metrics: gate listener with allowed_ips ACL
Parse the `allowed_ips` option in [metrics] as a comma-separated list of IPv4 CIDR entries. Both the bracketed form allowed_ips = ["1.2.3.4/32", "5.6.7.0/24"] and a bare comma-separated form are accepted; surrounding whitespace, matched quotes, and the optional [ ] are stripped. A bare address without a prefix is treated as /32. When the list is empty (option missing or empty value), all peers are allowed and behavior is unchanged. Otherwise serv_read() rejects any peer that doesn't match a configured CIDR by closing the freshly accepted socket before allocating client state, so scanners get nothing more than a TCP reset. Signed-off-by: Denys Fedoryshchenko <denys.f@collabora.com>
-rw-r--r--accel-pppd/extra/metrics.c164
1 files changed, 164 insertions, 0 deletions
diff --git a/accel-pppd/extra/metrics.c b/accel-pppd/extra/metrics.c
index 15ad16da..aa054fdb 100644
--- a/accel-pppd/extra/metrics.c
+++ b/accel-pppd/extra/metrics.c
@@ -13,6 +13,7 @@
#include "events.h"
#include "log.h"
#include "list.h"
+#include "utils.h"
#include "memdebug.h"
@@ -32,8 +33,15 @@ struct metrics_client_t {
unsigned int disconnect:1;
};
+struct metrics_acl_t {
+ struct list_head entry;
+ uint32_t net; /* host byte order */
+ uint32_t mask; /* host byte order */
+};
+
static enum metrics_format conf_format = METRICS_FORMAT_PROMETHEUS;
static char *conf_address;
+static LIST_HEAD(conf_allowed);
static struct triton_context_t serv_ctx;
static struct triton_md_handler_t serv_hnd;
@@ -90,12 +98,154 @@ out:
return ret;
}
+static void free_acl(struct list_head *head)
+{
+ struct metrics_acl_t *acl;
+
+ while (!list_empty(head)) {
+ acl = list_first_entry(head, typeof(*acl), entry);
+ list_del(&acl->entry);
+ _free(acl);
+ }
+}
+
+/* Strip surrounding whitespace, optional matching single/double quotes,
+ * and any trailing comma. Returns NULL if the token becomes empty.
+ */
+static char *clean_token(char *s)
+{
+ char *end;
+ size_t len;
+
+ while (*s == ' ' || *s == '\t')
+ s++;
+
+ len = strlen(s);
+ while (len && (s[len - 1] == ' ' || s[len - 1] == '\t' ||
+ s[len - 1] == ',' || s[len - 1] == '\r' ||
+ s[len - 1] == '\n'))
+ s[--len] = 0;
+
+ if (len >= 2 && ((s[0] == '"' && s[len - 1] == '"') ||
+ (s[0] == '\'' && s[len - 1] == '\''))) {
+ s[len - 1] = 0;
+ s++;
+ len -= 2;
+ }
+
+ while (*s == ' ' || *s == '\t')
+ s++;
+ end = s + strlen(s);
+ while (end > s && (end[-1] == ' ' || end[-1] == '\t'))
+ *--end = 0;
+
+ return *s ? s : NULL;
+}
+
+/* TODO: IPv6 support. Only IPv4 CIDR or a bare IPv4 address are accepted;
+ * IPv6 entries in allowed_ips are rejected at parse time. ip_allowed() and
+ * struct metrics_acl_t store the network in a 32-bit host-order word, so
+ * adding IPv6 here will also need the matching widening downstream.
+ */
+static int parse_acl_entry(const char *str, struct metrics_acl_t **out)
+{
+ struct metrics_acl_t *acl;
+ struct in_addr addr;
+ uint8_t prefix;
+ uint32_t mask;
+
+ if (!u_parse_ip4cidr(str, &addr, &prefix)) {
+ /* Accept a bare IP as /32 */
+ if (inet_pton(AF_INET, str, &addr) != 1)
+ return -1;
+ prefix = 32;
+ }
+
+ acl = _malloc(sizeof(*acl));
+ if (!acl)
+ return -1;
+
+ mask = prefix ? (uint32_t)0xffffffffu << (32 - prefix) : 0;
+ acl->net = ntohl(addr.s_addr) & mask;
+ acl->mask = mask;
+ *out = acl;
+ return 0;
+}
+
+/* Parse `allowed_ips` value. Accepts:
+ * "1.2.3.4/32, 5.6.7.0/24"
+ * ["1.2.3.4/32", "5.6.7.0/24"]
+ * On success the supplied list is populated and 0 is returned. Returns -1 on
+ * any parse error; in that case the partial list is freed.
+ */
+static int parse_allowed_ips(const char *value, struct list_head *list)
+{
+ char *buf, *p, *tok;
+ int ret = -1;
+
+ if (!value || !*value)
+ return 0;
+
+ buf = strdup(value);
+ if (!buf)
+ return -1;
+
+ p = buf;
+ while (*p == ' ' || *p == '\t')
+ p++;
+ if (*p == '[')
+ p++;
+ {
+ size_t len = strlen(p);
+ while (len && (p[len - 1] == ' ' || p[len - 1] == '\t' ||
+ p[len - 1] == ']' || p[len - 1] == '\r' ||
+ p[len - 1] == '\n'))
+ p[--len] = 0;
+ }
+
+ while ((tok = strsep(&p, ",")) != NULL) {
+ struct metrics_acl_t *acl;
+ char *clean = clean_token(tok);
+
+ if (!clean)
+ continue;
+ if (parse_acl_entry(clean, &acl) < 0) {
+ log_error("metrics: invalid entry in allowed_ips: '%s'\n", clean);
+ free_acl(list);
+ goto out;
+ }
+ list_add_tail(&acl->entry, list);
+ }
+
+ ret = 0;
+out:
+ free(buf);
+ return ret;
+}
+
+static int ip_allowed(uint32_t addr_nbo)
+{
+ struct metrics_acl_t *acl;
+ uint32_t addr;
+
+ if (list_empty(&conf_allowed))
+ return 1;
+
+ addr = ntohl(addr_nbo);
+ list_for_each_entry(acl, &conf_allowed, entry) {
+ if ((addr & acl->mask) == acl->net)
+ return 1;
+ }
+ return 0;
+}
+
static int load_config(void)
{
const char *opt;
enum metrics_format fmt = METRICS_FORMAT_PROMETHEUS;
char *address = NULL;
struct sockaddr_in dummy;
+ LIST_HEAD(new_allowed);
opt = conf_get_opt("metrics", "format");
if (opt && parse_format(opt, &fmt) < 0) {
@@ -118,11 +268,20 @@ static int load_config(void)
return -1;
}
+ opt = conf_get_opt("metrics", "allowed_ips");
+ if (opt && parse_allowed_ips(opt, &new_allowed) < 0) {
+ _free(address);
+ return -1;
+ }
+
conf_format = fmt;
if (conf_address)
_free(conf_address);
conf_address = address;
+ free_acl(&conf_allowed);
+ list_replace_init(&new_allowed, &conf_allowed);
+
return 0;
}
@@ -291,6 +450,11 @@ static int serv_read(struct triton_md_handler_t *h)
continue;
}
+ if (!ip_allowed(addr.sin_addr.s_addr)) {
+ close(sock);
+ continue;
+ }
+
if (fcntl(sock, F_SETFL, O_NONBLOCK)) {
log_error("metrics: failed to set nonblocking mode: %s\n", strerror(errno));
close(sock);