From: Felix Fietkau Date: Thu, 16 Mar 2023 10:35:50 +0000 (+0100) Subject: hostapd: add experimental radius server X-Git-Url: http://git.lede-project.org./?a=commitdiff_plain;h=57fbbf15cd3d1cceb5d10cdae8369421c2092223;p=openwrt%2Fstaging%2Fneocturne.git hostapd: add experimental radius server 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 --- diff --git a/package/network/services/hostapd/Makefile b/package/network/services/hostapd/Makefile index 0fe53dc8d4..1d034a1752 100644 --- a/package/network/services/hostapd/Makefile +++ b/package/network/services/hostapd/Makefile @@ -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 index 0000000000..3175dcfd04 --- /dev/null +++ b/package/network/services/hostapd/files/radius.clients @@ -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 index 0000000000..ad8730748b --- /dev/null +++ b/package/network/services/hostapd/files/radius.config @@ -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 index 0000000000..4c562c2473 --- /dev/null +++ b/package/network/services/hostapd/files/radius.init @@ -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 index 0000000000..03e2fc8fae --- /dev/null +++ b/package/network/services/hostapd/files/radius.users @@ -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 index 0000000000..3f0f40c0f7 --- /dev/null +++ b/package/network/services/hostapd/patches/770-radius_server.patch @@ -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 index 0000000000..362a22c276 --- /dev/null +++ b/package/network/services/hostapd/src/hostapd/radius.c @@ -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 +#include +#include +#include +#include + +#include +#include + +#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 \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; +}