hostapd: add experimental radius server
authorFelix Fietkau <nbd@nbd.name>
Thu, 16 Mar 2023 10:35:50 +0000 (11:35 +0100)
committerFelix Fietkau <nbd@nbd.name>
Tue, 1 Aug 2023 08:05:13 +0000 (10:05 +0200)
This can be used to run a standalone EAP server that can be used from
other APs. It uses json as user database format and can automatically
handle reload.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
package/network/services/hostapd/Makefile
package/network/services/hostapd/files/radius.clients [new file with mode: 0644]
package/network/services/hostapd/files/radius.config [new file with mode: 0644]
package/network/services/hostapd/files/radius.init [new file with mode: 0644]
package/network/services/hostapd/files/radius.users [new file with mode: 0644]
package/network/services/hostapd/patches/770-radius_server.patch [new file with mode: 0644]
package/network/services/hostapd/src/hostapd/radius.c [new file with mode: 0644]

index 0fe53dc8d4d6424d72f511bc618c137c3c9c0906..1d034a17526794030a79b0a8950d223e4ea0ff89 100644 (file)
@@ -148,7 +148,7 @@ define Package/hostapd/Default
   SUBMENU:=WirelessAPD
   TITLE:=IEEE 802.1x Authenticator
   URL:=http://hostap.epitest.fi/
-  DEPENDS:=$(DRV_DEPENDS) +hostapd-common +libubus
+  DEPENDS:=$(DRV_DEPENDS) +hostapd-common +libubus +libblobmsg-json
   EXTRA_DEPENDS:=hostapd-common (=$(PKG_VERSION)-$(PKG_RELEASE))
   USERID:=network=101:network=101
   PROVIDES:=hostapd
@@ -253,7 +253,7 @@ define Package/wpad/Default
   CATEGORY:=Network
   SUBMENU:=WirelessAPD
   TITLE:=IEEE 802.1x Auth/Supplicant
-  DEPENDS:=$(DRV_DEPENDS) +hostapd-common +libubus
+  DEPENDS:=$(DRV_DEPENDS) +hostapd-common +libubus +libblobmsg-json
   EXTRA_DEPENDS:=hostapd-common (=$(PKG_VERSION)-$(PKG_RELEASE))
   USERID:=network=101:network=101
   URL:=http://hostap.epitest.fi/
@@ -398,7 +398,7 @@ define Package/wpa-supplicant/Default
   SUBMENU:=WirelessAPD
   TITLE:=WPA Supplicant
   URL:=http://hostap.epitest.fi/wpa_supplicant/
-  DEPENDS:=$(DRV_DEPENDS) +hostapd-common +libubus
+  DEPENDS:=$(DRV_DEPENDS) +hostapd-common +libubus +libblobmsg-json
   EXTRA_DEPENDS:=hostapd-common (=$(PKG_VERSION)-$(PKG_RELEASE))
   USERID:=network=101:network=101
   PROVIDES:=wpa-supplicant
@@ -520,7 +520,7 @@ define Package/eapol-test/Default
   SECTION:=net
   SUBMENU:=WirelessAPD
   CATEGORY:=Network
-  DEPENDS:=$(DRV_DEPENDS) +libubus
+  DEPENDS:=$(DRV_DEPENDS) +libubus +libblobmsg-json
 endef
 
 define Package/eapol-test
@@ -585,7 +585,7 @@ TARGET_CPPFLAGS := \
        -D_GNU_SOURCE \
        $(if $(CONFIG_WPA_MSG_MIN_PRIORITY),-DCONFIG_MSG_MIN_PRIORITY=$(CONFIG_WPA_MSG_MIN_PRIORITY))
 
-TARGET_LDFLAGS += -lubox -lubus
+TARGET_LDFLAGS += -lubox -lubus -lblobmsg_json
 
 ifdef CONFIG_PACKAGE_kmod-cfg80211
   TARGET_LDFLAGS += -lm -lnl-tiny
@@ -674,8 +674,37 @@ define Build/Compile
        $(Build/Compile/$(BUILD_VARIANT))
 endef
 
+define Install/hostapd/full
+       $(INSTALL_DIR) $(1)/etc/init.d $(1)/etc/config $(1)/etc/radius
+       ln -sf hostapd $(1)/usr/sbin/hostapd-radius
+       $(INSTALL_BIN) ./files/radius.init $(1)/etc/init.d/radius
+       $(INSTALL_DATA) ./files/radius.config $(1)/etc/config/radius
+       $(INSTALL_DATA) ./files/radius.clients $(1)/etc/radius/clients
+       $(INSTALL_DATA) ./files/radius.users $(1)/etc/radius/users
+endef
+
+define Package/hostapd-full/conffiles
+/etc/config/radius
+/etc/radius
+endef
+
+ifeq ($(CONFIG_VARIANT),full)
+Package/wpad-mesh-openssl/conffiles = $(Package/hostapd-full/conffiles)
+Package/wpad-mesh-wolfssl/conffiles = $(Package/hostapd-full/conffiles)
+Package/wpad-mesh-mbedtls/conffiles = $(Package/hostapd-full/conffiles)
+Package/wpad/conffiles = $(Package/hostapd-full/conffiles)
+Package/wpad-openssl/conffiles = $(Package/hostapd-full/conffiles)
+Package/wpad-wolfssl/conffiles = $(Package/hostapd-full/conffiles)
+Package/wpad-mbedtls/conffiles = $(Package/hostapd-full/conffiles)
+Package/hostapd/conffiles = $(Package/hostapd-full/conffiles)
+Package/hostapd-openssl/conffiles = $(Package/hostapd-full/conffiles)
+Package/hostapd-wolfssl/conffiles = $(Package/hostapd-full/conffiles)
+Package/hostapd-mbedtls/conffiles = $(Package/hostapd-full/conffiles)
+endif
+
 define Install/hostapd
        $(INSTALL_DIR) $(1)/usr/sbin
+       $(if $(findstring full,$(CONFIG_VARIANT)),$(Install/hostapd/full))
 endef
 
 define Install/supplicant
diff --git a/package/network/services/hostapd/files/radius.clients b/package/network/services/hostapd/files/radius.clients
new file mode 100644 (file)
index 0000000..3175dcf
--- /dev/null
@@ -0,0 +1 @@
+0.0.0.0/0 radius
diff --git a/package/network/services/hostapd/files/radius.config b/package/network/services/hostapd/files/radius.config
new file mode 100644 (file)
index 0000000..ad87307
--- /dev/null
@@ -0,0 +1,9 @@
+config radius
+       option disabled '1'
+       option ca_cert '/etc/radius/ca.pem'
+       option cert '/etc/radius/cert.pem'
+       option key '/etc/radius/key.pem'
+       option users '/etc/radius/users'
+       option clients '/etc/radius/clients'
+       option auth_port '1812'
+       option acct_port '1813'
diff --git a/package/network/services/hostapd/files/radius.init b/package/network/services/hostapd/files/radius.init
new file mode 100644 (file)
index 0000000..4c562c2
--- /dev/null
@@ -0,0 +1,42 @@
+#!/bin/sh /etc/rc.common
+
+START=30
+
+USE_PROCD=1
+NAME=radius
+
+radius_start() {
+       local cfg="$1"
+
+       config_get_bool disabled "$cfg" disabled 0
+
+       [ "$disabled" -gt 0 ] && return
+
+       config_get ca "$cfg" ca_cert
+       config_get key "$cfg" key
+       config_get cert "$cfg" cert
+       config_get users "$cfg" users
+       config_get clients "$cfg" clients
+       config_get auth_port "$cfg" auth_port 1812
+       config_get acct_port "$cfg" acct_port 1813
+       config_get identity "$cfg" identity "$(cat /proc/sys/kernel/hostname)"
+
+       procd_open_instance $cfg
+       procd_set_param command /usr/sbin/hostapd-radius \
+               -C "$ca" \
+               -c "$cert" -k "$key" \
+               -s "$clients" -u "$users" \
+               -p "$auth_port" -P "$acct_port" \
+               -i "$identity"
+       procd_close_instance
+}
+
+start_service() {
+       config_load radius
+       config_foreach radius_start radius
+}
+
+service_triggers()
+{
+       procd_add_reload_trigger "radius"
+}
diff --git a/package/network/services/hostapd/files/radius.users b/package/network/services/hostapd/files/radius.users
new file mode 100644 (file)
index 0000000..03e2fc8
--- /dev/null
@@ -0,0 +1,14 @@
+{
+       "phase1": {
+               "wildcard": [
+                       {
+                               "name": "*",
+                               "methods": [ "PEAP" ]
+                       }
+               ]
+       },
+       "phase2": {
+               "users": {
+               }
+       }
+}
diff --git a/package/network/services/hostapd/patches/770-radius_server.patch b/package/network/services/hostapd/patches/770-radius_server.patch
new file mode 100644 (file)
index 0000000..3f0f40c
--- /dev/null
@@ -0,0 +1,154 @@
+--- a/hostapd/Makefile
++++ b/hostapd/Makefile
+@@ -63,6 +63,10 @@ endif
+ OBJS += main.o
+ OBJS += config_file.o
++ifdef CONFIG_RADIUS_SERVER
++OBJS += radius.o
++endif
++
+ OBJS += ../src/ap/hostapd.o
+ OBJS += ../src/ap/wpa_auth_glue.o
+ OBJS += ../src/ap/drv_callbacks.o
+--- a/hostapd/main.c
++++ b/hostapd/main.c
+@@ -42,6 +42,7 @@ static struct hapd_global global;
+ static int daemonize = 0;
+ static char *pid_file = NULL;
++extern int radius_main(int argc, char **argv);
+ #ifndef CONFIG_NO_HOSTAPD_LOGGER
+ static void hostapd_logger_cb(void *ctx, const u8 *addr, unsigned int module,
+@@ -665,6 +666,11 @@ int main(int argc, char *argv[])
+       if (os_program_init())
+               return -1;
++#ifdef RADIUS_SERVER
++      if (strstr(argv[0], "radius"))
++              return radius_main(argc, argv);
++#endif
++
+       os_memset(&interfaces, 0, sizeof(interfaces));
+       interfaces.reload_config = hostapd_reload_config;
+       interfaces.config_read_cb = hostapd_config_read;
+--- a/src/radius/radius_server.c
++++ b/src/radius/radius_server.c
+@@ -63,6 +63,12 @@ struct radius_server_counters {
+       u32 unknown_acct_types;
+ };
++struct radius_accept_attr {
++      u8 type;
++      u16 len;
++      void *data;
++};
++
+ /**
+  * struct radius_session - Internal RADIUS server data for a session
+  */
+@@ -90,7 +96,7 @@ struct radius_session {
+       unsigned int macacl:1;
+       unsigned int t_c_filtering:1;
+-      struct hostapd_radius_attr *accept_attr;
++      struct radius_accept_attr *accept_attr;
+       u32 t_c_timestamp; /* Last read T&C timestamp from user DB */
+ };
+@@ -394,6 +400,7 @@ static void radius_server_session_free(s
+       radius_msg_free(sess->last_reply);
+       os_free(sess->username);
+       os_free(sess->nas_ip);
++      os_free(sess->accept_attr);
+       os_free(sess);
+       data->num_sess--;
+ }
+@@ -554,6 +561,36 @@ radius_server_erp_find_key(struct radius
+ }
+ #endif /* CONFIG_ERP */
++static struct radius_accept_attr *
++radius_server_copy_attr(const struct hostapd_radius_attr *data)
++{
++      const struct hostapd_radius_attr *attr;
++      struct radius_accept_attr *attr_new;
++      size_t data_size = 0;
++      void *data_buf;
++      int n_attr = 1;
++
++      for (attr = data; attr; attr = attr->next) {
++              n_attr++;
++              data_size += wpabuf_len(attr->val);
++      }
++
++      attr_new = os_zalloc(n_attr * sizeof(*attr) + data_size);
++      if (!attr_new)
++              return NULL;
++
++      data_buf = &attr_new[n_attr];
++      for (n_attr = 0, attr = data; attr; attr = attr->next) {
++              struct radius_accept_attr *cur = &attr_new[n_attr++];
++
++              cur->type = attr->type;
++              cur->len = wpabuf_len(attr->val);
++              cur->data = memcpy(data_buf, wpabuf_head(attr->val), cur->len);
++              data_buf += cur->len;
++      }
++
++      return attr_new;
++}
+ static struct radius_session *
+ radius_server_get_new_session(struct radius_server_data *data,
+@@ -607,7 +644,7 @@ radius_server_get_new_session(struct rad
+               eap_user_free(tmp);
+               return NULL;
+       }
+-      sess->accept_attr = tmp->accept_attr;
++      sess->accept_attr = radius_server_copy_attr(tmp->accept_attr);
+       sess->macacl = tmp->macacl;
+       eap_user_free(tmp);
+@@ -1118,11 +1155,10 @@ radius_server_encapsulate_eap(struct rad
+       }
+       if (code == RADIUS_CODE_ACCESS_ACCEPT) {
+-              struct hostapd_radius_attr *attr;
+-              for (attr = sess->accept_attr; attr; attr = attr->next) {
+-                      if (!radius_msg_add_attr(msg, attr->type,
+-                                               wpabuf_head(attr->val),
+-                                               wpabuf_len(attr->val))) {
++              struct radius_accept_attr *attr;
++              for (attr = sess->accept_attr; attr->data; attr++) {
++                      if (!radius_msg_add_attr(msg, attr->type, attr->data,
++                                               attr->len)) {
+                               wpa_printf(MSG_ERROR, "Could not add RADIUS attribute");
+                               radius_msg_free(msg);
+                               return NULL;
+@@ -1211,11 +1247,10 @@ radius_server_macacl(struct radius_serve
+       }
+       if (code == RADIUS_CODE_ACCESS_ACCEPT) {
+-              struct hostapd_radius_attr *attr;
+-              for (attr = sess->accept_attr; attr; attr = attr->next) {
+-                      if (!radius_msg_add_attr(msg, attr->type,
+-                                               wpabuf_head(attr->val),
+-                                               wpabuf_len(attr->val))) {
++              struct radius_accept_attr *attr;
++              for (attr = sess->accept_attr; attr->data; attr++) {
++                      if (!radius_msg_add_attr(msg, attr->type, attr->data,
++                                               attr->len)) {
+                               wpa_printf(MSG_ERROR, "Could not add RADIUS attribute");
+                               radius_msg_free(msg);
+                               return NULL;
+@@ -2512,7 +2547,7 @@ static int radius_server_get_eap_user(vo
+       ret = data->get_eap_user(data->conf_ctx, identity, identity_len,
+                                phase2, user);
+       if (ret == 0 && user) {
+-              sess->accept_attr = user->accept_attr;
++              sess->accept_attr = radius_server_copy_attr(user->accept_attr);
+               sess->remediation = user->remediation;
+               sess->macacl = user->macacl;
+               sess->t_c_timestamp = user->t_c_timestamp;
diff --git a/package/network/services/hostapd/src/hostapd/radius.c b/package/network/services/hostapd/src/hostapd/radius.c
new file mode 100644 (file)
index 0000000..362a22c
--- /dev/null
@@ -0,0 +1,715 @@
+#include "utils/includes.h"
+#include "utils/common.h"
+#include "utils/eloop.h"
+#include "crypto/crypto.h"
+#include "crypto/tls.h"
+
+#include "ap/ap_config.h"
+#include "eap_server/eap.h"
+#include "radius/radius.h"
+#include "radius/radius_server.h"
+#include "eap_register.h"
+
+#include <libubox/blobmsg_json.h>
+#include <libubox/blobmsg.h>
+#include <libubox/avl.h>
+#include <libubox/avl-cmp.h>
+#include <libubox/kvlist.h>
+
+#include <sys/stat.h>
+#include <fnmatch.h>
+
+#define VENDOR_ID_WISPR 14122
+#define VENDOR_ATTR_SIZE 6
+
+struct radius_parse_attr_data {
+       unsigned int vendor;
+       u8 type;
+       int size;
+       char format;
+       const char *data;
+};
+
+struct radius_parse_attr_state {
+       struct hostapd_radius_attr *prev;
+       struct hostapd_radius_attr *attr;
+       struct wpabuf *buf;
+       void *attrdata;
+};
+
+struct radius_user_state {
+       struct avl_node node;
+       struct eap_user data;
+};
+
+struct radius_user_data {
+       struct kvlist users;
+       struct avl_tree user_state;
+       struct blob_attr *wildcard;
+};
+
+struct radius_state {
+       struct radius_server_data *radius;
+       struct eap_config eap;
+
+       struct radius_user_data phase1, phase2;
+       const char *user_file;
+       time_t user_file_ts;
+
+       int n_attrs;
+       struct hostapd_radius_attr *attrs;
+};
+
+struct radius_config {
+       struct tls_connection_params tls;
+       struct radius_server_conf radius;
+};
+
+enum {
+       USER_ATTR_PASSWORD,
+       USER_ATTR_HASH,
+       USER_ATTR_SALT,
+       USER_ATTR_METHODS,
+       USER_ATTR_RADIUS,
+       USER_ATTR_VLAN,
+       USER_ATTR_MAX_RATE_UP,
+       USER_ATTR_MAX_RATE_DOWN,
+       __USER_ATTR_MAX
+};
+
+static void radius_tls_event(void *ctx, enum tls_event ev,
+                             union tls_event_data *data)
+{
+       switch (ev) {
+       case TLS_CERT_CHAIN_SUCCESS:
+               wpa_printf(MSG_DEBUG, "radius: remote certificate verification success");
+               break;
+       case TLS_CERT_CHAIN_FAILURE:
+               wpa_printf(MSG_INFO, "radius: certificate chain failure: reason=%d depth=%d subject='%s' err='%s'",
+                          data->cert_fail.reason,
+                          data->cert_fail.depth,
+                          data->cert_fail.subject,
+                          data->cert_fail.reason_txt);
+               break;
+       case TLS_PEER_CERTIFICATE:
+               wpa_printf(MSG_DEBUG, "radius: peer certificate: depth=%d serial_num=%s subject=%s",
+                          data->peer_cert.depth,
+                          data->peer_cert.serial_num ? data->peer_cert.serial_num : "N/A",
+                          data->peer_cert.subject);
+               break;
+       case TLS_ALERT:
+               if (data->alert.is_local)
+                       wpa_printf(MSG_DEBUG, "radius: local TLS alert: %s",
+                                  data->alert.description);
+               else
+                       wpa_printf(MSG_DEBUG, "radius: remote TLS alert: %s",
+                                  data->alert.description);
+               break;
+       case TLS_UNSAFE_RENEGOTIATION_DISABLED:
+               /* Not applicable to TLS server */
+               break;
+       }
+}
+
+static void radius_userdata_init(struct radius_user_data *u)
+{
+       kvlist_init(&u->users, kvlist_blob_len);
+       avl_init(&u->user_state, avl_strcmp, false, NULL);
+}
+
+static void radius_userdata_free(struct radius_user_data *u)
+{
+       struct radius_user_state *s, *tmp;
+
+       kvlist_free(&u->users);
+       free(u->wildcard);
+       u->wildcard = NULL;
+       avl_remove_all_elements(&u->user_state, s, node, tmp)
+               free(s);
+}
+
+static void
+radius_userdata_load(struct radius_user_data *u, struct blob_attr *data)
+{
+       enum {
+               USERSTATE_USERS,
+               USERSTATE_WILDCARD,
+               __USERSTATE_MAX,
+       };
+       static const struct blobmsg_policy policy[__USERSTATE_MAX] = {
+               [USERSTATE_USERS] = { "users", BLOBMSG_TYPE_TABLE },
+               [USERSTATE_WILDCARD] = { "wildcard", BLOBMSG_TYPE_ARRAY },
+       };
+       struct blob_attr *tb[__USERSTATE_MAX], *cur;
+       int rem;
+
+       if (!data)
+               return;
+
+       blobmsg_parse(policy, __USERSTATE_MAX, tb, blobmsg_data(data), blobmsg_len(data));
+
+       blobmsg_for_each_attr(cur, tb[USERSTATE_USERS], rem)
+               kvlist_set(&u->users, blobmsg_name(cur), cur);
+
+       if (tb[USERSTATE_WILDCARD])
+               u->wildcard = blob_memdup(tb[USERSTATE_WILDCARD]);
+}
+
+static void
+load_userfile(struct radius_state *s)
+{
+       enum {
+               USERDATA_PHASE1,
+               USERDATA_PHASE2,
+               __USERDATA_MAX
+       };
+       static const struct blobmsg_policy policy[__USERDATA_MAX] = {
+               [USERDATA_PHASE1] = { "phase1", BLOBMSG_TYPE_TABLE },
+               [USERDATA_PHASE2] = { "phase2", BLOBMSG_TYPE_TABLE },
+       };
+       struct blob_attr *tb[__USERDATA_MAX], *cur;
+       static struct blob_buf b;
+       struct stat st;
+       int rem;
+
+       if (stat(s->user_file, &st))
+               return;
+
+       if (s->user_file_ts == st.st_mtime)
+               return;
+
+       s->user_file_ts = st.st_mtime;
+       radius_userdata_free(&s->phase1);
+       radius_userdata_free(&s->phase2);
+
+       blob_buf_init(&b, 0);
+       blobmsg_add_json_from_file(&b, s->user_file);
+       blobmsg_parse(policy, __USERDATA_MAX, tb, blob_data(b.head), blob_len(b.head));
+       radius_userdata_load(&s->phase1, tb[USERDATA_PHASE1]);
+       radius_userdata_load(&s->phase2, tb[USERDATA_PHASE2]);
+
+       blob_buf_free(&b);
+}
+
+static struct blob_attr *
+radius_user_get(struct radius_user_data *s, const char *name)
+{
+       struct blob_attr *cur;
+       int rem;
+
+       cur = kvlist_get(&s->users, name);
+       if (cur)
+               return cur;
+
+       blobmsg_for_each_attr(cur, s->wildcard, rem) {
+               static const struct blobmsg_policy policy = {
+                       "name", BLOBMSG_TYPE_STRING
+               };
+               struct blob_attr *pattern;
+
+               if (blobmsg_type(cur) != BLOBMSG_TYPE_TABLE)
+                       continue;
+
+               blobmsg_parse(&policy, 1, &pattern, blobmsg_data(cur), blobmsg_len(cur));
+               if (!name)
+                       continue;
+
+               if (!fnmatch(blobmsg_get_string(pattern), name, 0))
+                       return cur;
+       }
+
+       return NULL;
+}
+
+static struct radius_parse_attr_data *
+radius_parse_attr(struct blob_attr *attr)
+{
+       static const struct blobmsg_policy policy[4] = {
+               { .type = BLOBMSG_TYPE_INT32 },
+               { .type = BLOBMSG_TYPE_INT32 },
+               { .type = BLOBMSG_TYPE_STRING },
+               { .type = BLOBMSG_TYPE_STRING },
+       };
+       static struct radius_parse_attr_data data;
+       struct blob_attr *tb[4];
+       const char *format;
+
+       blobmsg_parse_array(policy, ARRAY_SIZE(policy), tb, blobmsg_data(attr), blobmsg_len(attr));
+
+       if (!tb[0] || !tb[1] || !tb[2] || !tb[3])
+               return NULL;
+
+       format = blobmsg_get_string(tb[2]);
+       if (strlen(format) != 1)
+               return NULL;
+
+       data.vendor = blobmsg_get_u32(tb[0]);
+       data.type = blobmsg_get_u32(tb[1]);
+       data.format = format[0];
+       data.data = blobmsg_get_string(tb[3]);
+       data.size = strlen(data.data);
+
+       switch (data.format) {
+       case 's':
+               break;
+       case 'x':
+               if (data.size & 1)
+                       return NULL;
+               data.size /= 2;
+               break;
+       case 'd':
+               data.size = 4;
+               break;
+       default:
+               return NULL;
+       }
+
+       return &data;
+}
+
+static void
+radius_count_attrs(struct blob_attr **tb, int *n_attr, size_t *attr_size)
+{
+       struct blob_attr *data = tb[USER_ATTR_RADIUS];
+       struct blob_attr *cur;
+       int rem;
+
+       blobmsg_for_each_attr(cur, data, rem) {
+               struct radius_parse_attr_data *data;
+               size_t prev = *attr_size;
+
+               data = radius_parse_attr(cur);
+               if (!data)
+                       continue;
+
+               *attr_size += data->size;
+               if (data->vendor)
+                       *attr_size += VENDOR_ATTR_SIZE;
+
+               (*n_attr)++;
+       }
+
+       *n_attr += !!tb[USER_ATTR_VLAN] * 3 +
+                  !!tb[USER_ATTR_MAX_RATE_UP] +
+                  !!tb[USER_ATTR_MAX_RATE_DOWN];
+       *attr_size += !!tb[USER_ATTR_VLAN] * (4 + 4 + 5) +
+                     !!tb[USER_ATTR_MAX_RATE_UP] * (4 + VENDOR_ATTR_SIZE) +
+                     !!tb[USER_ATTR_MAX_RATE_DOWN] * (4 + VENDOR_ATTR_SIZE);
+}
+
+static void *
+radius_add_attr(struct radius_parse_attr_state *state,
+               u32 vendor, u8 type, u8 len)
+{
+       struct hostapd_radius_attr *attr;
+       struct wpabuf *buf;
+       void *val;
+
+       val = state->attrdata;
+
+       buf = state->buf++;
+       buf->buf = val;
+
+       attr = state->attr++;
+       attr->val = buf;
+       attr->type = type;
+
+       if (state->prev)
+               state->prev->next = attr;
+       state->prev = attr;
+
+       if (vendor) {
+               u8 *vendor_hdr = val + 4;
+
+               WPA_PUT_BE32(val, vendor);
+               vendor_hdr[0] = type;
+               vendor_hdr[1] = len + 2;
+
+               len += VENDOR_ATTR_SIZE;
+               val += VENDOR_ATTR_SIZE;
+               attr->type = RADIUS_ATTR_VENDOR_SPECIFIC;
+       }
+
+       buf->size = buf->used = len;
+       state->attrdata += len;
+
+       return val;
+}
+
+static void
+radius_parse_attrs(struct blob_attr **tb, struct radius_parse_attr_state *state)
+{
+       struct blob_attr *data = tb[USER_ATTR_RADIUS];
+       struct hostapd_radius_attr *prev = NULL;
+       struct blob_attr *cur;
+       int len, rem;
+       void *val;
+
+       if ((cur = tb[USER_ATTR_VLAN]) != NULL && blobmsg_get_u32(cur) < 4096) {
+               char buf[5];
+
+               val = radius_add_attr(state, 0, RADIUS_ATTR_TUNNEL_TYPE, 4);
+               WPA_PUT_BE32(val, RADIUS_TUNNEL_TYPE_VLAN);
+
+               val = radius_add_attr(state, 0, RADIUS_ATTR_TUNNEL_MEDIUM_TYPE, 4);
+               WPA_PUT_BE32(val, RADIUS_TUNNEL_MEDIUM_TYPE_802);
+
+               len = snprintf(buf, sizeof(buf), "%d", blobmsg_get_u32(cur));
+               val = radius_add_attr(state, 0, RADIUS_ATTR_TUNNEL_PRIVATE_GROUP_ID, len);
+               memcpy(val, buf, len);
+       }
+
+       if ((cur = tb[USER_ATTR_MAX_RATE_UP]) != NULL) {
+               val = radius_add_attr(state, VENDOR_ID_WISPR, 7, 4);
+               WPA_PUT_BE32(val, blobmsg_get_u32(cur));
+       }
+
+       if ((cur = tb[USER_ATTR_MAX_RATE_DOWN]) != NULL) {
+               val = radius_add_attr(state, VENDOR_ID_WISPR, 8, 4);
+               WPA_PUT_BE32(val, blobmsg_get_u32(cur));
+       }
+
+       blobmsg_for_each_attr(cur, data, rem) {
+               struct radius_parse_attr_data *data;
+               void *val;
+               int size;
+
+               data = radius_parse_attr(cur);
+               if (!data)
+                       continue;
+
+               val = radius_add_attr(state, data->vendor, data->type, data->size);
+               switch (data->format) {
+               case 's':
+                       memcpy(val, data->data, data->size);
+                       break;
+               case 'x':
+                       hexstr2bin(data->data, val, data->size);
+                       break;
+               case 'd':
+                       WPA_PUT_BE32(val, atoi(data->data));
+                       break;
+               }
+       }
+}
+
+static void
+radius_user_parse_methods(struct eap_user *eap, struct blob_attr *data)
+{
+       struct blob_attr *cur;
+       int rem, n = 0;
+
+       if (!data)
+               return;
+
+       blobmsg_for_each_attr(cur, data, rem) {
+               const char *method;
+
+               if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING)
+                       continue;
+
+               if (n == EAP_MAX_METHODS)
+                       break;
+
+               method = blobmsg_get_string(cur);
+               eap->methods[n].method = eap_server_get_type(method, &eap->methods[n].vendor);
+               if (eap->methods[n].vendor == EAP_VENDOR_IETF &&
+                   eap->methods[n].method == EAP_TYPE_NONE) {
+                       if (!strcmp(method, "TTLS-PAP")) {
+                               eap->ttls_auth |= EAP_TTLS_AUTH_PAP;
+                               continue;
+                       }
+                       if (!strcmp(method, "TTLS-CHAP")) {
+                               eap->ttls_auth |= EAP_TTLS_AUTH_CHAP;
+                               continue;
+                       }
+                       if (!strcmp(method, "TTLS-MSCHAP")) {
+                               eap->ttls_auth |= EAP_TTLS_AUTH_MSCHAP;
+                               continue;
+                       }
+                       if (!strcmp(method, "TTLS-MSCHAPV2")) {
+                               eap->ttls_auth |= EAP_TTLS_AUTH_MSCHAPV2;
+                               continue;
+                       }
+               }
+               n++;
+       }
+}
+
+static struct eap_user *
+radius_user_get_state(struct radius_user_data *u, struct blob_attr *data,
+                     const char *id)
+{
+       static const struct blobmsg_policy policy[__USER_ATTR_MAX] = {
+               [USER_ATTR_PASSWORD] = { "password", BLOBMSG_TYPE_STRING },
+               [USER_ATTR_HASH] = { "hash", BLOBMSG_TYPE_STRING },
+               [USER_ATTR_SALT] = { "salt", BLOBMSG_TYPE_STRING },
+               [USER_ATTR_METHODS] = { "methods", BLOBMSG_TYPE_ARRAY },
+               [USER_ATTR_RADIUS] = { "radius", BLOBMSG_TYPE_ARRAY },
+               [USER_ATTR_VLAN] = { "vlan-id", BLOBMSG_TYPE_INT32 },
+               [USER_ATTR_MAX_RATE_UP] = { "max-rate-up", BLOBMSG_TYPE_INT32 },
+               [USER_ATTR_MAX_RATE_DOWN] = { "max-rate-down", BLOBMSG_TYPE_INT32 },
+       };
+       struct blob_attr *tb[__USER_ATTR_MAX], *cur;
+       char *password_buf, *salt_buf, *name_buf;
+       struct radius_parse_attr_state astate = {};
+       struct hostapd_radius_attr *attr;
+       struct radius_user_state *state;
+       int pw_len = 0, salt_len = 0;
+       struct eap_user *eap;
+       struct wpabuf *val;
+       size_t attrsize = 0;
+       void *attrdata;
+       int n_attr = 0;
+
+       state = avl_find_element(&u->user_state, id, state, node);
+       if (state)
+               return &state->data;
+
+       blobmsg_parse(policy, __USER_ATTR_MAX, tb, blobmsg_data(data), blobmsg_len(data));
+
+       if ((cur = tb[USER_ATTR_SALT]) != NULL)
+               salt_len = strlen(blobmsg_get_string(cur)) / 2;
+       if ((cur = tb[USER_ATTR_HASH]) != NULL)
+               pw_len = strlen(blobmsg_get_string(cur)) / 2;
+       else if ((cur = tb[USER_ATTR_PASSWORD]) != NULL)
+               pw_len = blobmsg_len(cur) - 1;
+       radius_count_attrs(tb, &n_attr, &attrsize);
+
+       state = calloc_a(sizeof(*state), &name_buf, strlen(id) + 1,
+                        &password_buf, pw_len,
+                        &salt_buf, salt_len,
+                        &astate.attr, n_attr * sizeof(*astate.attr),
+                        &astate.buf, n_attr * sizeof(*astate.buf),
+                        &astate.attrdata, attrsize);
+       eap = &state->data;
+       eap->salt = salt_len ? salt_buf : NULL;
+       eap->salt_len = salt_len;
+       eap->password = pw_len ? password_buf : NULL;
+       eap->password_len = pw_len;
+       eap->force_version = -1;
+
+       if ((cur = tb[USER_ATTR_SALT]) != NULL)
+               hexstr2bin(blobmsg_get_string(cur), salt_buf, salt_len);
+       if ((cur = tb[USER_ATTR_PASSWORD]) != NULL)
+               memcpy(password_buf, blobmsg_get_string(cur), pw_len);
+       else if ((cur = tb[USER_ATTR_HASH]) != NULL) {
+               hexstr2bin(blobmsg_get_string(cur), password_buf, pw_len);
+               eap->password_hash = 1;
+       }
+       radius_user_parse_methods(eap, tb[USER_ATTR_METHODS]);
+
+       if (n_attr > 0) {
+               cur = tb[USER_ATTR_RADIUS];
+               eap->accept_attr = astate.attr;
+               radius_parse_attrs(tb, &astate);
+       }
+
+       state->node.key = strcpy(name_buf, id);
+       avl_insert(&u->user_state, &state->node);
+
+       return &state->data;
+
+free:
+       free(state);
+       return NULL;
+}
+
+static int radius_get_eap_user(void *ctx, const u8 *identity,
+                              size_t identity_len, int phase2,
+                              struct eap_user *user)
+{
+       struct radius_state *s = ctx;
+       struct radius_user_data *u = phase2 ? &s->phase2 : &s->phase1;
+       struct blob_attr *entry;
+       struct eap_user *data;
+       char *id;
+
+       if (identity_len > 512)
+               return -1;
+
+       load_userfile(s);
+
+       id = alloca(identity_len + 1);
+       memcpy(id, identity, identity_len);
+       id[identity_len] = 0;
+
+       entry = radius_user_get(u, id);
+       if (!entry)
+               return -1;
+
+       if (!user)
+               return 0;
+
+       data = radius_user_get_state(u, entry, id);
+       if (!data)
+               return -1;
+
+       *user = *data;
+       if (user->password_len > 0)
+               user->password = os_memdup(user->password, user->password_len);
+       if (user->salt_len > 0)
+               user->salt = os_memdup(user->salt, user->salt_len);
+       user->phase2 = phase2;
+
+       return 0;
+}
+
+static int radius_setup(struct radius_state *s, struct radius_config *c)
+{
+       struct eap_config *eap = &s->eap;
+       struct tls_config conf = {
+               .event_cb = radius_tls_event,
+               .tls_flags = TLS_CONN_DISABLE_TLSv1_3,
+               .cb_ctx = s,
+       };
+
+       eap->eap_server = 1;
+       eap->max_auth_rounds = 100;
+       eap->max_auth_rounds_short = 50;
+       eap->ssl_ctx = tls_init(&conf);
+       if (!eap->ssl_ctx) {
+               wpa_printf(MSG_INFO, "TLS init failed\n");
+               return 1;
+       }
+
+       if (tls_global_set_params(eap->ssl_ctx, &c->tls)) {
+               wpa_printf(MSG_INFO, "failed to set TLS parameters\n");
+               return 1;
+       }
+
+       c->radius.eap_cfg = eap;
+       c->radius.conf_ctx = s;
+       c->radius.get_eap_user = radius_get_eap_user;
+       s->radius = radius_server_init(&c->radius);
+       if (!s->radius) {
+               wpa_printf(MSG_INFO, "failed to initialize radius server\n");
+               return 1;
+       }
+
+       return 0;
+}
+
+static int radius_init(struct radius_state *s)
+{
+       memset(s, 0, sizeof(*s));
+       radius_userdata_init(&s->phase1);
+       radius_userdata_init(&s->phase2);
+}
+
+static void radius_deinit(struct radius_state *s)
+{
+       if (s->radius)
+               radius_server_deinit(s->radius);
+
+       if (s->eap.ssl_ctx)
+               tls_deinit(s->eap.ssl_ctx);
+
+       radius_userdata_free(&s->phase1);
+       radius_userdata_free(&s->phase2);
+}
+
+static int usage(const char *progname)
+{
+       fprintf(stderr, "Usage: %s <options>\n",
+               progname);
+}
+
+int radius_main(int argc, char **argv)
+{
+       static struct radius_state state = {};
+       static struct radius_config config = {};
+       const char *progname = argv[0];
+       int ret = 0;
+       int ch;
+
+       wpa_debug_setup_stdout();
+       wpa_debug_level = 0;
+
+       if (eloop_init()) {
+               wpa_printf(MSG_ERROR, "Failed to initialize event loop");
+               return 1;
+       }
+
+       eap_server_register_methods();
+       radius_init(&state);
+
+       while ((ch = getopt(argc, argv, "6C:c:d:i:k:K:p:P:s:u:")) != -1) {
+               switch (ch) {
+               case '6':
+                       config.radius.ipv6 = 1;
+                       break;
+               case 'C':
+                       config.tls.ca_cert = optarg;
+                       break;
+               case 'c':
+                       if (config.tls.client_cert2)
+                               return usage(progname);
+
+                       if (config.tls.client_cert)
+                               config.tls.client_cert2 = optarg;
+                       else
+                               config.tls.client_cert = optarg;
+                       break;
+               case 'd':
+                       config.tls.dh_file = optarg;
+                       break;
+               case 'i':
+                       state.eap.server_id = optarg;
+                       state.eap.server_id_len = strlen(optarg);
+                       break;
+               case 'k':
+                       if (config.tls.private_key2)
+                               return usage(progname);
+
+                       if (config.tls.private_key)
+                               config.tls.private_key2 = optarg;
+                       else
+                               config.tls.private_key = optarg;
+                       break;
+               case 'K':
+                       if (config.tls.private_key_passwd2)
+                               return usage(progname);
+
+                       if (config.tls.private_key_passwd)
+                               config.tls.private_key_passwd2 = optarg;
+                       else
+                               config.tls.private_key_passwd = optarg;
+                       break;
+               case 'p':
+                       config.radius.auth_port = atoi(optarg);
+                       break;
+               case 'P':
+                       config.radius.acct_port = atoi(optarg);
+                       break;
+               case 's':
+                       config.radius.client_file = optarg;
+                       break;
+               case 'u':
+                       state.user_file = optarg;
+                       break;
+               default:
+                       return usage(progname);
+               }
+       }
+
+       if (!config.tls.client_cert || !config.tls.private_key ||
+           !config.radius.client_file || !state.eap.server_id ||
+           !state.user_file) {
+               wpa_printf(MSG_INFO, "missing options\n");
+               goto out;
+       }
+
+       ret = radius_setup(&state, &config);
+       if (ret)
+               goto out;
+
+       load_userfile(&state);
+       eloop_run();
+
+out:
+       radius_deinit(&state);
+       os_program_deinit();
+
+       return ret;
+}