Introduce Automated Frequency Coordination (AFC) support for UNII-5 and UNII-7 6GHz bands. AFC is required for Standard Power Devices (SPDs) to determine a lists of channels and EIRP/PSD powers that are available in the 6GHz spectrum. Signed-off-by: Lorenzo Bianconi <lorenzo@xxxxxxxxxx> --- hostapd/Makefile | 6 + hostapd/config_file.c | 274 ++++++++++++++ hostapd/defconfig | 3 + hostapd/hostapd.conf | 43 +++ src/ap/afc.c | 859 ++++++++++++++++++++++++++++++++++++++++++ src/ap/ap_config.c | 17 + src/ap/ap_config.h | 47 +++ src/ap/hostapd.c | 10 + src/ap/hostapd.h | 6 + src/drivers/driver.h | 13 + 10 files changed, 1278 insertions(+) create mode 100644 src/ap/afc.c diff --git a/hostapd/Makefile b/hostapd/Makefile index bfafe73b7..1fc0d0483 100644 --- a/hostapd/Makefile +++ b/hostapd/Makefile @@ -103,6 +103,12 @@ CFLAGS += -DCONFIG_TAXONOMY OBJS += ../src/ap/taxonomy.o endif +ifdef CONFIG_AFC +CFLAGS += -DCONFIG_AFC +OBJS += ../src/ap/afc.o +LIBS += -lcurl -ljson-c +endif + ifdef CONFIG_MODULE_TESTS CFLAGS += -DCONFIG_MODULE_TESTS OBJS += hapd_module_tests.o diff --git a/hostapd/config_file.c b/hostapd/config_file.c index 1af083917..c83357243 100644 --- a/hostapd/config_file.c +++ b/hostapd/config_file.c @@ -2436,6 +2436,191 @@ static int get_u16(const char *pos, int line, u16 *ret_val) #endif /* CONFIG_IEEE80211BE */ +#ifdef CONFIG_AFC +static int hostapd_afc_parse_cert_ids(struct hostapd_config *conf, char *pos) +{ + struct cert_id *c = NULL; + int i, count = 0; + + while (pos && pos[0]) { + char *p; + + c = os_realloc_array(c, count + 1, sizeof(*c)); + if (!c) + return -ENOMEM; + + i = count; + count++; + + p = os_strchr(pos, ':'); + if (!p) + goto error; + + *p++ = '\0'; + if (!p || !p[0]) + goto error; + + c[i].rulset = os_malloc(os_strlen(pos) + 1); + if (!c[i].rulset) + goto error; + + os_strlcpy(c[i].rulset, pos, os_strlen(pos) + 1); + pos = p; + p = os_strchr(pos, ','); + if (p) + *p++ = '\0'; + + c[i].id = os_malloc(os_strlen(pos) + 1); + if (!c[i].id) + goto error; + + os_strlcpy(c[i].id, pos, os_strlen(pos) + 1); + pos = p; + } + + conf->afc.n_cert_ids = count; + conf->afc.cert_ids = c; + + return 0; + +error: + for (i = 0; i < count; i++) { + os_free(c[i].rulset); + os_free(c[i].id); + } + os_free(c); + + return -ENOMEM; +} + + +static int hostapd_afc_parse_position_data(struct afc_linear_polygon **data, + unsigned int *n_linear_polygon_data, + char *pos) +{ + struct afc_linear_polygon *d = NULL; + int i, count = 0; + + while (pos && pos[0]) { + char *p, *end; + + d = os_realloc_array(d, count + 1, sizeof(*d)); + if (!d) + return -ENOMEM; + + i = count; + count++; + + p = os_strchr(pos, ':'); + if (!p) + goto error; + + *p++ = '\0'; + if (!p || !p[0]) + goto error; + + d[i].longitude = strtod(pos, &end); + if (*end) + goto error; + + pos = p; + p = os_strchr(pos, ','); + if (p) + *p++ = '\0'; + + d[i].latitude = strtod(pos, &end); + if (*end) + goto error; + + pos = p; + } + + *n_linear_polygon_data = count; + *data = d; + + return 0; + +error: + os_free(d); + return -ENOMEM; +} + + +static int hostapd_afc_parse_freq_range(struct hostapd_config *conf, char *pos) +{ + struct afc_freq_range *f = NULL; + int i, count = 0; + + while (pos && pos[0]) { + char *p; + + f = os_realloc_array(f, count + 1, sizeof(*f)); + if (!f) + return -ENOMEM; + + i = count; + count++; + + p = os_strchr(pos, ':'); + if (!p) + goto error; + + *p++ = '\0'; + if (!p || !p[0]) + goto error; + + f[i].low_freq = atoi(pos); + pos = p; + p = os_strchr(pos, ','); + if (p) + *p++ = '\0'; + + f[i].high_freq = atoi(pos); + pos = p; + } + + conf->afc.n_freq_range = count; + conf->afc.freq_range = f; + + return 0; + +error: + os_free(f); + return -ENOMEM; +} + + +static int hostapd_afc_parse_op_class(struct hostapd_config *conf, char *pos) +{ + unsigned int *oc = NULL; + int i, count = 0; + + while (pos && pos[0]) { + char *p; + + oc = os_realloc_array(oc, count + 1, sizeof(*oc)); + if (!oc) + return -ENOMEM; + + i = count; + count++; + + p = os_strchr(pos, ','); + if (p) + *p++ = '\0'; + + oc[i] = atoi(pos); + pos = p; + } + + conf->afc.n_op_class = count; + conf->afc.op_class = oc; + + return 0; +} +#endif /* CONFIG_AFC */ + + static int hostapd_config_fill(struct hostapd_config *conf, struct hostapd_bss_config *bss, const char *buf, char *pos, int line) @@ -3800,6 +3985,95 @@ static int hostapd_config_fill(struct hostapd_config *conf, return 1; } bss->unsol_bcast_probe_resp_interval = val; +#ifdef CONFIG_AFC + } else if (os_strcmp(buf, "afc_request_version") == 0) { + conf->afc.version = os_malloc(os_strlen(pos) + 1); + if (!conf->afc.version) + return 1; + + os_strlcpy(conf->afc.version, pos, os_strlen(pos) + 1); + } else if (os_strcmp(buf, "afc_request_id") == 0) { + conf->afc.request_id = os_malloc(os_strlen(pos) + 1); + if (!conf->afc.request_id) + return 1; + + os_strlcpy(conf->afc.request_id, pos, os_strlen(pos) + 1); + } else if (os_strcmp(buf, "afc_cert_ids") == 0) { + if (hostapd_afc_parse_cert_ids(conf, pos)) + return 1; + } else if (os_strcmp(buf, "afc_coordinator_url") == 0) { + conf->afc.url = os_malloc(os_strlen(pos) + 1); + if (!conf->afc.url) + return 1; + + os_strlcpy(conf->afc.url, pos, os_strlen(pos) + 1); + } else if (os_strcmp(buf, "afc_bearer_token") == 0) { + conf->afc.bearer_token = os_malloc(os_strlen(pos) + 1); + if (!conf->afc.bearer_token) + return 1; + + os_strlcpy(conf->afc.bearer_token, pos, os_strlen(pos) + 1); + } else if (os_strcmp(buf, "afc_serial_number") == 0) { + conf->afc.serial_number = os_malloc(os_strlen(pos) + 1); + if (!conf->afc.serial_number) + return 1; + + os_strlcpy(conf->afc.serial_number, pos, os_strlen(pos) + 1); + } else if (os_strcmp(buf, "afc_id") == 0) { + conf->afc.id = os_malloc(os_strlen(pos) + 1); + if (!conf->afc.id) + return 1; + + os_strlcpy(conf->afc.id, pos, os_strlen(pos) + 1); + } else if (os_strcmp(buf, "afc_location_type") == 0) { + conf->afc.location.type = atoi(pos); + if (conf->afc.location.type != ELLIPSE && + conf->afc.location.type != LINEAR_POLYGON && + conf->afc.location.type != RADIAL_POLYGON) + return 1; + } else if (os_strcmp(buf, "afc_linear_polygon") == 0) { + if (hostapd_afc_parse_position_data( + &conf->afc.location.linear_polygon_data, + &conf->afc.location.n_linear_polygon_data, + pos)) + return 1; + } else if (os_strcmp(buf, "afc_radial_polygon") == 0) { + if (hostapd_afc_parse_position_data( + (struct afc_linear_polygon **) + &conf->afc.location.radial_polygon_data, + &conf->afc.location.n_radial_polygon_data, + pos)) + return 1; + } else if (os_strcmp(buf, "afc_major_axis") == 0) { + conf->afc.location.major_axis = atoi(pos); + } else if (os_strcmp(buf, "afc_minor_axis") == 0) { + conf->afc.location.minor_axis = atoi(pos); + } else if (os_strcmp(buf, "afc_orientation") == 0) { + conf->afc.location.orientation = atoi(pos); + } else if (os_strcmp(buf, "afc_height") == 0) { + char *end; + + conf->afc.location.height = strtod(pos, &end); + if (*end) + return 1; + } else if (os_strcmp(buf, "afc_height_type") == 0) { + conf->afc.location.height_type = os_malloc(os_strlen(pos) + 1); + if (!conf->afc.location.height_type) + return 1; + + os_strlcpy(conf->afc.location.height_type, pos, + os_strlen(pos) + 1); + } else if (os_strcmp(buf, "afc_vertical_tolerance") == 0) { + conf->afc.location.vertical_tolerance = atoi(pos); + } else if (os_strcmp(buf, "afc_min_power") == 0) { + conf->afc.min_power = atoi(pos); + } else if (os_strcmp(buf, "afc_freq_range") == 0) { + if (hostapd_afc_parse_freq_range(conf, pos)) + return 1; + } else if (os_strcmp(buf, "afc_op_class") == 0) { + if (hostapd_afc_parse_op_class(conf, pos)) + return 1; +#endif /* CONFIG_AFC */ } else if (os_strcmp(buf, "mbssid") == 0) { int mbssid = atoi(pos); if (mbssid < 0 || mbssid > ENHANCED_MBSSID_ENABLED) { diff --git a/hostapd/defconfig b/hostapd/defconfig index 36c147682..5ced8c093 100644 --- a/hostapd/defconfig +++ b/hostapd/defconfig @@ -419,3 +419,6 @@ CONFIG_DPP2=y # DPP version 3 support (experimental and still changing; do not enable for # production use) #CONFIG_DPP3=y + +# Enable Automated Frequency Coordination for 6GHz outdoor +#CONFIG_AFC=y diff --git a/hostapd/hostapd.conf b/hostapd/hostapd.conf index 13576499a..a1f78d7ae 100644 --- a/hostapd/hostapd.conf +++ b/hostapd/hostapd.conf @@ -1005,6 +1005,49 @@ wmm_ac_vo_acm=0 # Valid range: 0..20 TUs; default is 0 (disabled) #unsol_bcast_probe_resp_interval=0 +##### Automated Frequency Coordination for 6GHz UNII-5 and UNII-7 bands ####### + +# AFC server URL +#afc_coordinator_url=https://afccoordinator.com +# +# AFC request identification parameters +#afc_request_version=1.1 +#afc_request_id=11235813 +#afc_bearer_token=bearer_token +#afc_serial_number=abcdefg +#afc_cert_ids=US_47_CFR_PART_15_SUBPART_E:CID000 +# +# AFC location type: +# 0 = ellipse +# 1 = linear polygon +# 2 = radial polygon +#afc_location_type=0 +# +# AFC ellipse or linear polygon coordinations +#afc_linear_polygon=-122.984157:37.425056 +# +# AFC radial polygon coordinations +#afc_radial_polygon=118.8:92.76,76.44:87.456,98.56:123.33 +# +# AFC ellipse major/minor axis and orientation +#afc_major_axis=100 +#afc_minor_axis=50 +#afc_orientation=70 +# +# AFC device elevation parameters +#afc_height=3.0 +#afc_height_type=AGL +#afc_vertical_tolerance=7 +# +# AFC minimum desired TX power (dbm) +#afc_min_power=24 +# +# AFC request frequency ranges +#afc_freq_range=5925:6425,6525:6875 +# +# AFC request operation classes +#afc_op_class=131,132,133,134,136 + ##### IEEE 802.11be related configuration ##################################### #ieee80211be: Whether IEEE 802.11be (EHT) is enabled diff --git a/src/ap/afc.c b/src/ap/afc.c new file mode 100644 index 000000000..b685f75f4 --- /dev/null +++ b/src/ap/afc.c @@ -0,0 +1,859 @@ +/* + * Automated Frequency Coordination + * Copyright (c) 2024, Lorenzo Bianconi <lorenzo@xxxxxxxxxx> + * + * This software may be distributed under the terms of the BSD license. + * See README for more details. + */ + +#include <curl/curl.h> +#include <json-c/json.h> +#include <time.h> + +#include "utils/includes.h" +#include "utils/common.h" +#include "utils/eloop.h" +#include "hostapd.h" +#include "acs.h" + +#define CURL_TIMEOUT 60 +#define HOSTAPD_AFC_RETRY_TIMEOUT 180 +#define HOSTAPD_AFC_TIMEOUT 86400 /* 24h */ + +struct afc_curl_ctx { + int timeout; + char *buf; + size_t buf_len; +}; + +static void hostapd_afc_timeout_handler(void *eloop_ctx, void *timeout_ctx); + + +static struct json_object * +hostapd_afc_build_location_request(struct hostapd_iface *iface) +{ + struct json_object *location_obj, *center_obj, *ellipse_obj; + struct json_object *elevation_obj, *str_obj; + struct hostapd_config *iconf = iface->conf; + bool is_ap_indoor = he_reg_is_indoor(iconf->he_6ghz_reg_pwr_type); + + location_obj = json_object_new_object(); + if (!location_obj) + return NULL; + + if (iconf->afc.location.type != LINEAR_POLYGON) { + struct afc_linear_polygon *lp = + &iconf->afc.location.linear_polygon_data[0]; + + ellipse_obj = json_object_new_object(); + if (!ellipse_obj) + goto error; + + center_obj = json_object_new_object(); + if (!center_obj) + goto error; + + json_object_object_add(ellipse_obj, "center", center_obj); + + str_obj = json_object_new_double(lp->longitude); + if (!str_obj) + goto error; + + json_object_object_add(center_obj, "longitude", str_obj); + str_obj = json_object_new_double(lp->latitude); + if (!str_obj) + goto error; + + json_object_object_add(center_obj, "latitude", str_obj); + + } + + switch (iconf->afc.location.type) { + case LINEAR_POLYGON: { + struct json_object *outer_boundary_obj; + int i; + + outer_boundary_obj = json_object_new_object(); + if (!outer_boundary_obj) + goto error; + + json_object_object_add(location_obj, "linearPolygon", + outer_boundary_obj); + ellipse_obj = json_object_new_array(); + if (!ellipse_obj) + goto error; + + json_object_object_add(outer_boundary_obj, "outerBoundary", + ellipse_obj); + for (i = 0; + i < iconf->afc.location.n_linear_polygon_data; i++) { + struct afc_linear_polygon *lp = + &iconf->afc.location.linear_polygon_data[i]; + + center_obj = json_object_new_object(); + if (!center_obj) + goto error; + + json_object_array_add(ellipse_obj, center_obj); + str_obj = json_object_new_double(lp->longitude); + if (!str_obj) + goto error; + + json_object_object_add(center_obj, "longitude", + str_obj); + str_obj = json_object_new_double(lp->latitude); + if (!str_obj) + goto error; + + json_object_object_add(center_obj, "latitude", + str_obj); + } + break; + } + case RADIAL_POLYGON: { + struct json_object *outer_boundary_obj; + int i; + + json_object_object_add(location_obj, "radialPolygon", + ellipse_obj); + + outer_boundary_obj = json_object_new_array(); + if (!outer_boundary_obj) + goto error; + + json_object_object_add(ellipse_obj, "outerBoundary", + outer_boundary_obj); + for (i = 0; + i < iconf->afc.location.n_radial_polygon_data; i++) { + struct afc_radial_polygon *rp = + &iconf->afc.location.radial_polygon_data[i]; + struct json_object *angle_obj; + + angle_obj = json_object_new_object(); + if (!angle_obj) + goto error; + + json_object_array_add(outer_boundary_obj, angle_obj); + + str_obj = json_object_new_double(rp->angle); + if (!str_obj) + goto error; + + json_object_object_add(angle_obj, "angle", str_obj); + str_obj = json_object_new_double(rp->length); + if (!str_obj) + goto error; + + json_object_object_add(angle_obj, "length", str_obj); + } + break; + } + case ELLIPSE: + default: + json_object_object_add(location_obj, "ellipse", ellipse_obj); + + str_obj = json_object_new_int(iconf->afc.location.major_axis); + if (!str_obj) + goto error; + + json_object_object_add(ellipse_obj, "majorAxis", str_obj); + str_obj = json_object_new_int(iconf->afc.location.minor_axis); + if (!str_obj) + goto error; + + json_object_object_add(ellipse_obj, "minorAxis", str_obj); + str_obj = json_object_new_int(iconf->afc.location.orientation); + if (!str_obj) + goto error; + + json_object_object_add(ellipse_obj, "orientation", str_obj); + break; + } + + elevation_obj = json_object_new_object(); + if (!elevation_obj) + goto error; + + json_object_object_add(location_obj, "elevation", + elevation_obj); + str_obj = json_object_new_double(iconf->afc.location.height); + if (!str_obj) + goto error; + + json_object_object_add(elevation_obj, "height", str_obj); + str_obj = json_object_new_string(iconf->afc.location.height_type); + if (!str_obj) + goto error; + + json_object_object_add(elevation_obj, "heightType", str_obj); + str_obj = json_object_new_int(iconf->afc.location.vertical_tolerance); + if (!str_obj) + goto error; + + json_object_object_add(elevation_obj, "verticalUncertainty", + str_obj); + str_obj = json_object_new_int(is_ap_indoor); + if (!str_obj) + goto error; + + json_object_object_add(location_obj, "indoorDeployment", str_obj); + + return location_obj; + +error: + json_object_put(location_obj); + return NULL; +} + + +static void +hostapd_afc_get_opclass_chan_list(struct hostapd_iface *iface, u8 op_class, + u8 *chan_list, u16 *n_chan_list) +{ + struct hostapd_hw_modes *mode = iface->current_mode; + int i, count = 0; + + memset(chan_list, 0, mode->num_channels * sizeof(*chan_list)); + for (i = 0; i < mode->num_channels; i++) { + struct hostapd_channel_data *chan = &mode->channels[i]; + + if (!is_6ghz_freq(chan->freq)) + continue; + + if (ieee80211_chan_to_freq(iface->conf->country, op_class, + chan->chan) < 0) + continue; + + chan_list[count++] = chan->chan; + } + *n_chan_list = count; +} + + +static struct json_object * +hostapd_afc_build_request(struct hostapd_iface *iface) +{ + struct json_object *l1_obj, *l2_obj, *la1_obj, *la2_obj; + struct json_object *s2_obj, *str_obj, *location_obj; + struct hostapd_hw_modes *mode = iface->current_mode; + struct hostapd_config *iconf = iface->conf; + u8 *chan_list = NULL; + int i; + + l1_obj = json_object_new_object(); + if (!l1_obj) + return NULL; + + if (iconf->afc.version) { + str_obj = json_object_new_string(iconf->afc.version); + if (!str_obj) + goto error; + + json_object_object_add(l1_obj, "version", str_obj); + } + + la1_obj = json_object_new_array(); + if (!la1_obj) + goto error; + + json_object_object_add(l1_obj, "availableSpectrumInquiryRequests", + la1_obj); + l2_obj = json_object_new_object(); + if (!l2_obj) + goto error; + + json_object_array_add(la1_obj, l2_obj); + if (iconf->afc.request_id) { + str_obj = json_object_new_string(iconf->afc.request_id); + if (!str_obj) + goto error; + + json_object_object_add(l2_obj, "requestId", str_obj); + } + + s2_obj = json_object_new_object(); + if (!s2_obj) + goto error; + + json_object_object_add(l2_obj, "deviceDescriptor", s2_obj); + if (iconf->afc.serial_number) { + str_obj = json_object_new_string(iconf->afc.serial_number); + if (!str_obj) + goto error; + + json_object_object_add(s2_obj, "serialNumber", str_obj); + } + + la2_obj = json_object_new_array(); + if (!la2_obj) + goto error; + + json_object_object_add(s2_obj, "certificationId", la2_obj); + for (i = 0; i < iconf->afc.n_cert_ids; i++) { + struct json_object *obj; + + obj = json_object_new_object(); + if (!obj) + goto error; + + json_object_array_add(la2_obj, obj); + str_obj = + json_object_new_string(iconf->afc.cert_ids[i].rulset); + if (!str_obj) + goto error; + + json_object_object_add(obj, "rulesetId", str_obj); + str_obj = json_object_new_string(iconf->afc.cert_ids[i].id); + if (!str_obj) + goto error; + + json_object_object_add(obj, "id", str_obj); + } + + location_obj = hostapd_afc_build_location_request(iface); + if (!location_obj) + goto error; + + json_object_object_add(l2_obj, "location", location_obj); + str_obj = json_object_new_int(iconf->afc.min_power); + if (!str_obj) + goto error; + + json_object_object_add(l2_obj, "minDesiredPower", str_obj); + + if (iconf->afc.n_freq_range) { + struct json_object *freq_obj; + + freq_obj = json_object_new_array(); + if (!freq_obj) + goto error; + + json_object_object_add(l2_obj, "inquiredFrequencyRange", + freq_obj); + for (i = 0; i < iconf->afc.n_freq_range; i++) { + struct afc_freq_range *fr = &iconf->afc.freq_range[i]; + struct json_object *obj; + + obj = json_object_new_object(); + if (!obj) + goto error; + + json_object_array_add(freq_obj, obj); + str_obj = json_object_new_int(fr->low_freq); + if (!str_obj) + goto error; + + json_object_object_add(obj, "lowFrequency", str_obj); + str_obj = json_object_new_int(fr->high_freq); + if (!str_obj) + goto error; + + json_object_object_add(obj, "highFrequency", str_obj); + } + } + + if (iconf->afc.n_op_class) { + struct json_object *op_class_list_obj; + + chan_list = os_malloc(mode->num_channels * sizeof(*chan_list)); + if (!chan_list) + goto error; + + op_class_list_obj = json_object_new_array(); + if (!op_class_list_obj) + goto error; + + json_object_object_add(l2_obj, "inquiredChannels", + op_class_list_obj); + for (i = 0; i < iconf->afc.n_op_class; i++) { + struct json_object *op_class_obj, *chan_list_obj; + u16 n_chan_list = 0; + int j; + + hostapd_afc_get_opclass_chan_list( + iface, iconf->afc.op_class[i], + chan_list, &n_chan_list); + if (!n_chan_list) + continue; + + op_class_obj = json_object_new_object(); + if (!op_class_obj) + goto error; + + json_object_array_add(op_class_list_obj, op_class_obj); + str_obj = json_object_new_int(iconf->afc.op_class[i]); + if (!str_obj) + goto error; + + json_object_object_add(op_class_obj, + "globalOperatingClass", + str_obj); + chan_list_obj = json_object_new_array(); + if (!chan_list_obj) + goto error; + + json_object_object_add(op_class_obj, "channelCfi", + chan_list_obj); + for (j = 0; j < n_chan_list; j++) { + str_obj = json_object_new_int(chan_list[j]); + if (!str_obj) + goto error; + + json_object_array_add(chan_list_obj, str_obj); + } + } + free(chan_list); + } + + wpa_printf(MSG_DEBUG, "Pending AFC request: %s", + json_object_get_string(l1_obj)); + + return l1_obj; + +error: + free(chan_list); + json_object_put(l1_obj); + + return NULL; +} + + +static int +hostad_afc_parse_available_freq_info(struct hostapd_iface *iface, + struct json_object *reply_elem_obj) +{ + struct hostapd_hw_modes *mode = iface->current_mode; + struct json_object *obj; + struct freq_range_elem { + int low_freq; + int high_freq; + int max_psd; + } *f = NULL; + int i, count = 0; + + if (!json_object_object_get_ex(reply_elem_obj, + "availableFrequencyInfo", &obj)) + return 0; + + for (i = 0; i < json_object_array_length(obj); i++) { + struct json_object *range_elem_obj, *freq_range_obj; + struct json_object *high_freq_obj, *low_freq_obj; + struct json_object *max_psd_obj; + + range_elem_obj = json_object_array_get_idx(obj, i); + if (!json_object_object_get_ex(range_elem_obj, + "frequencyRange", + &freq_range_obj)) + continue; + + if (!json_object_object_get_ex(freq_range_obj, + "lowFrequency", + &low_freq_obj)) + continue; + + if (!json_object_object_get_ex(freq_range_obj, + "highFrequency", + &high_freq_obj)) + continue; + + if (!json_object_object_get_ex(range_elem_obj, "maxPsd", + &max_psd_obj) && + !json_object_object_get_ex(range_elem_obj, "maxPSD", + &max_psd_obj)) + continue; + + f = os_realloc_array(f, count + 1, sizeof(*f)); + if (!f) + return -ENOMEM; + + f[count].low_freq = json_object_get_int(low_freq_obj); + f[count].high_freq = json_object_get_int(high_freq_obj); + f[count++].max_psd = json_object_get_int(max_psd_obj); + } + + for (i = 0; i < mode->num_channels; i++) { + struct hostapd_channel_data *chan = &mode->channels[i]; + int j; + + if (chan->flag & HOSTAPD_CHAN_DISABLED) + continue; + + if (!is_6ghz_freq(chan->freq)) + continue; + + for (j = 0; j < count; j++) { + if (chan->freq > f[j].low_freq && + chan->freq < f[j].high_freq) + break; + } + + if (j == count) { + chan->flag |= HOSTAPD_CHAN_DISABLED; + } else { + chan->flag &= ~HOSTAPD_CHAN_DISABLED; + chan->max_eirp_psd = f[j].max_psd; + } + } + free(f); + + return 0; +} + + +static int +hostad_afc_parse_available_chan_info(struct hostapd_iface *iface, + struct json_object *reply_elem_obj) +{ + struct json_object *obj; + int i; + + if (!json_object_object_get_ex(reply_elem_obj, + "availableChannelInfo", &obj)) + return 0; + + for (i = 0; i < json_object_array_length(obj); i++) { + struct hostapd_hw_modes *mode = iface->current_mode; + struct json_object *range_elem_obj, *op_class_obj; + struct json_object *chan_cfi_obj, *max_eirp_obj; + int j, ch, op_class, count = 0; + struct chan_info_elem { + int chan; + int power; + } *c = NULL; + + range_elem_obj = json_object_array_get_idx(obj, i); + if (!json_object_object_get_ex(range_elem_obj, + "globalOperatingClass", + &op_class_obj)) + continue; + + op_class = json_object_get_int(op_class_obj); + if (op_class != iface->conf->op_class) + continue; + + if (!json_object_object_get_ex(range_elem_obj, "maxEirp", + &max_eirp_obj)) + continue; + + if (!json_object_object_get_ex(range_elem_obj, "channelCfi", + &chan_cfi_obj)) + continue; + + for (ch = 0; + ch < json_object_array_length(chan_cfi_obj); ch++) { + struct json_object *pwr_obj; + struct json_object *ch_obj; + int channel, power; + + ch_obj = json_object_array_get_idx(chan_cfi_obj, ch); + if (!ch_obj) + continue; + + pwr_obj = json_object_array_get_idx(max_eirp_obj, ch); + if (!pwr_obj) + continue; + + channel = json_object_get_int(ch_obj); + power = json_object_get_int(pwr_obj); + + c = os_realloc_array(c, count + 1, sizeof(*c)); + if (!c) + return -ENOMEM; + + c[count].chan = channel; + c[count++].power = power; + } + + for (j = 0; j < mode->num_channels; j++) { + struct hostapd_channel_data *chan = &mode->channels[j]; + int n; + + if (chan->flag & HOSTAPD_CHAN_DISABLED) + continue; + + if (!is_6ghz_freq(chan->freq)) + continue; + + for (n = 0; n < count; n++) { + if (chan->chan == c[n].chan) + break; + } + + if (n == count) { + chan->flag |= HOSTAPD_CHAN_DISABLED; + } else { + chan->flag &= ~HOSTAPD_CHAN_DISABLED; + chan->max_eirp_power = c[n].power; + } + } + + free(c); + } + + return 0; +} + + +static int hostad_afc_get_timeout(struct json_object *obj) +{ + time_t t, now; + struct tm tm; + + if (sscanf(json_object_get_string(obj), "%d-%d-%dT%d:%d:%dZ", + &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, + &tm.tm_min, &tm.tm_sec) <= 0) + return HOSTAPD_AFC_TIMEOUT; + + tm.tm_year -= 1900; + tm.tm_mon -= 1; + tm.tm_isdst = -1; + t = mktime(&tm); + time(&now); + + return now > t ? HOSTAPD_AFC_RETRY_TIMEOUT : (t - now) * 80 / 100; +} + + +static int hostapd_afc_parse_reply(struct hostapd_iface *iface, + struct afc_curl_ctx *ctx) +{ + struct json_object *payload_obj, *reply_obj, *version_obj; + struct hostapd_config *iconf = iface->conf; + int i, ret = -EINVAL; + + wpa_printf(MSG_DEBUG, "Received AFC reply: %s", ctx->buf); + payload_obj = json_tokener_parse(ctx->buf); + if (!payload_obj) + return -EINVAL; + + if (!json_object_object_get_ex(payload_obj, "version", &version_obj)) + return -EINVAL; + + if (iconf->afc.version && + os_strcmp(iconf->afc.version, json_object_get_string(version_obj))) + return -EINVAL; + + if (!json_object_object_get_ex(payload_obj, + "availableSpectrumInquiryResponses", + &reply_obj)) + return -EINVAL; + + for (i = 0; i < json_object_array_length(reply_obj); i++) { + struct json_object *reply_elem_obj, *obj, *status_obj; + int j, status = -EINVAL; + + reply_elem_obj = json_object_array_get_idx(reply_obj, i); + if (!reply_elem_obj) + continue; + + if (!json_object_object_get_ex(reply_elem_obj, "requestId", + &obj)) + continue; + + if (iconf->afc.request_id && + os_strcmp(iconf->afc.request_id, + json_object_get_string(obj))) + continue; + + if (!json_object_object_get_ex(reply_elem_obj, "rulesetId", + &obj)) + continue; + + for (j = 0; j < iconf->afc.n_cert_ids; j++) { + if (!os_strcmp(iconf->afc.cert_ids[j].rulset, + json_object_get_string(obj))) + break; + } + + if (j == iconf->afc.n_cert_ids) + continue; + + if (!json_object_object_get_ex(reply_elem_obj, "response", + &obj)) + continue; + + if (json_object_object_get_ex(obj, "shortDescription", + &status_obj)) + wpa_printf(MSG_DEBUG, "AFC reply element %d: %s", + i, json_object_get_string(status_obj)); + + if (json_object_object_get_ex(obj, "responseCode", + &status_obj)) + status = json_object_get_int(status_obj); + + if (status < 0) + continue; + + if (hostad_afc_parse_available_freq_info(iface, + reply_elem_obj) || + hostad_afc_parse_available_chan_info(iface, + reply_elem_obj)) + continue; + + if (json_object_object_get_ex(reply_elem_obj, + "availabilityExpireTime", + &obj)) { + int timeout = hostad_afc_get_timeout(obj); + + if (timeout < ctx->timeout) + ctx->timeout = timeout; + } + + ret = status; + } + + return ret; +} + + +static size_t hostapd_afc_curl_cb_write(void *ptr, size_t size, size_t nmemb, + void *userdata) +{ + struct afc_curl_ctx *ctx = userdata; + char *buf; + + buf = os_realloc(ctx->buf, ctx->buf_len + size * nmemb + 1); + if (!buf) + return 0; + + ctx->buf = buf; + os_memcpy(buf + ctx->buf_len, ptr, size * nmemb); + buf[ctx->buf_len + size * nmemb] = '\0'; + ctx->buf_len += size * nmemb; + + return size * nmemb; +} + + +static int hostapd_afc_send_receive(struct hostapd_iface *iface) +{ + struct afc_curl_ctx ctx = { + .timeout = HOSTAPD_AFC_RETRY_TIMEOUT, + }; + struct hostapd_config *iconf = iface->conf; + struct curl_slist *headers = NULL; + json_object *json_obj; + int ret = -EINVAL; + CURL *curl; + + if (eloop_is_timeout_registered(hostapd_afc_timeout_handler, + iface, NULL)) + return 0; + + if (!iface->current_mode) + goto resched; + + if (!iconf->afc.url || !iconf->afc.bearer_token) + return -EINVAL; + + wpa_printf(MSG_DEBUG, "Sending AFC request to %s (freq %dMHz)", + iconf->afc.url, iface->freq); + + curl_global_init(CURL_GLOBAL_ALL); + curl = curl_easy_init(); + if (!curl) + return -ENOMEM; + + headers = curl_slist_append(headers, "Accept: application/json"); + headers = curl_slist_append(headers, + "Content-Type: application/json"); + headers = curl_slist_append(headers, "charset: utf-8"); + + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_URL, iconf->afc.url); + curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, + hostapd_afc_curl_cb_write); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ctx); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "libcrp/0.1"); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, CURL_TIMEOUT); + curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); + curl_easy_setopt(curl, CURLOPT_XOAUTH2_BEARER, + iconf->afc.bearer_token); + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_BEARER); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 1L); + + json_obj = hostapd_afc_build_request(iface); + if (!json_obj) + return -ENOMEM; + + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, + json_object_to_json_string(json_obj)); + + ret = curl_easy_perform(curl); + if (ret != CURLE_OK) { + wpa_printf(MSG_ERROR, "curl_easy_perform failed: %s\n", + curl_easy_strerror(ret)); + ret = -EINVAL; + goto out; + } + + ret = hostapd_afc_parse_reply(iface, &ctx); +out: + json_object_put(json_obj); + curl_easy_cleanup(curl); + curl_global_cleanup(); + free(ctx.buf); +resched: + eloop_register_timeout(ctx.timeout, 0, hostapd_afc_timeout_handler, + iface, NULL); + return ret; +} + + +int hostapd_afc_handle_request(struct hostapd_iface *iface) +{ + struct hostapd_config *iconf = iface->conf; + int ret; + + if (iface->afc_completed) + return 1; + + /* AFC is required just for standard power AP */ + if (!he_reg_is_sp(iconf->he_6ghz_reg_pwr_type)) + return 1; + + if (!is_6ghz_op_class(iconf->op_class) || !is_6ghz_freq(iface->freq)) + return 1; + + ret = hostapd_afc_send_receive(iface); + if (ret) + return ret; + + /* Trigger a ACS freq scan */ + iface->freq = 0; + iconf->channel = 0; + iface->afc_completed = true; + + if (acs_init(iface) != HOSTAPD_CHAN_ACS) { + wpa_printf(MSG_ERROR, "Could not start ACS"); + ret = -EINVAL; + } + + return ret; +} + + +static void hostapd_afc_timeout_handler(void *eloop_ctx, void *timeout_ctx) +{ + struct hostapd_iface *iface = eloop_ctx; + + if (!hostapd_afc_send_receive(iface)) { + struct hostapd_hw_modes *mode = iface->current_mode; + int i; + + for (i = 0; i < mode->num_channels; i++) { + struct hostapd_channel_data *chan = &mode->channels[i]; + + if (chan->freq == iface->freq && + !(chan->flag & HOSTAPD_CHAN_DISABLED)) + return; + } + } + + /* Toggle interface to trigger new AFC connection */ + iface->afc_completed = false; + hostapd_disable_iface(iface); + hostapd_enable_iface(iface); +} diff --git a/src/ap/ap_config.c b/src/ap/ap_config.c index f760e340b..209cde5f4 100644 --- a/src/ap/ap_config.c +++ b/src/ap/ap_config.c @@ -1029,6 +1029,23 @@ void hostapd_config_free(struct hostapd_config *conf) #endif /* CONFIG_ACS */ wpabuf_free(conf->lci); wpabuf_free(conf->civic); +#ifdef CONFIG_AFC + os_free(conf->afc.url); + os_free(conf->afc.bearer_token); + os_free(conf->afc.version); + os_free(conf->afc.request_id); + os_free(conf->afc.serial_number); + for (i = 0; i < conf->afc.n_cert_ids; i++) { + os_free(conf->afc.cert_ids[i].rulset); + os_free(conf->afc.cert_ids[i].id); + } + os_free(conf->afc.cert_ids); + os_free(conf->afc.location.height_type); + os_free(conf->afc.location.linear_polygon_data); + os_free(conf->afc.location.radial_polygon_data); + os_free(conf->afc.freq_range); + os_free(conf->afc.op_class); +#endif /* CONFIG_AFC */ os_free(conf); } diff --git a/src/ap/ap_config.h b/src/ap/ap_config.h index de02ddafd..2f1c22a2b 100644 --- a/src/ap/ap_config.h +++ b/src/ap/ap_config.h @@ -1217,6 +1217,53 @@ struct hostapd_config { MBSSID_ENABLED = 1, ENHANCED_MBSSID_ENABLED = 2, } mbssid; + +#ifdef CONFIG_AFC + struct { + char *version; + char *request_id; + char *url; + char *bearer_token; + char *serial_number; + char *id; + unsigned int n_cert_ids; + struct cert_id { + char *rulset; + char *id; + } *cert_ids; + struct { + enum afc_location_type { + ELLIPSE, + LINEAR_POLYGON, + RADIAL_POLYGON, + } type; + unsigned int n_linear_polygon_data; + struct afc_linear_polygon { + double longitude; + double latitude; + } *linear_polygon_data; + unsigned int n_radial_polygon_data; + struct afc_radial_polygon { + double length; + double angle; + } *radial_polygon_data; + int major_axis; + int minor_axis; + int orientation; + double height; + char *height_type; + int vertical_tolerance; + } location; + unsigned int n_freq_range; + struct afc_freq_range { + int low_freq; + int high_freq; + } *freq_range; + unsigned int n_op_class; + unsigned int *op_class; + int min_power; + } afc; +#endif /* CONFIG_AFC */ }; diff --git a/src/ap/hostapd.c b/src/ap/hostapd.c index cb464f670..9d2de4456 100644 --- a/src/ap/hostapd.c +++ b/src/ap/hostapd.c @@ -2406,6 +2406,16 @@ static int hostapd_setup_interface_complete_sync(struct hostapd_iface *iface, } #endif /* CONFIG_MESH */ +#ifdef CONFIG_AFC + /* check AFC for 6GHz channels. */ + res = hostapd_afc_handle_request(iface); + if (res <= 0) { + if (res < 0) + goto fail; + return res; + } +#endif /* CONFIG_AFC */ + if (!delay_apply_cfg && hostapd_set_freq(hapd, hapd->iconf->hw_mode, iface->freq, hapd->iconf->channel, diff --git a/src/ap/hostapd.h b/src/ap/hostapd.h index 3dba121c6..33b24ae5e 100644 --- a/src/ap/hostapd.h +++ b/src/ap/hostapd.h @@ -670,9 +670,15 @@ struct hostapd_iface { /* Configured freq of interface is NO_IR */ bool is_no_ir; + +#ifdef CONFIG_AFC + bool afc_completed; +#endif /* CONFIG_AFC */ }; /* hostapd.c */ +int hostapd_afc_handle_request(struct hostapd_iface *iface); + int hostapd_for_each_interface(struct hapd_interfaces *interfaces, int (*cb)(struct hostapd_iface *iface, void *ctx), void *ctx); diff --git a/src/drivers/driver.h b/src/drivers/driver.h index bff7502f1..072d7b28a 100644 --- a/src/drivers/driver.h +++ b/src/drivers/driver.h @@ -175,6 +175,19 @@ struct hostapd_channel_data { * punct_bitmap - RU puncturing bitmap */ u16 punct_bitmap; + +#ifdef CONFIG_AFC + /** + * max eirp power spectral density received from the AFC + * coordinator for the channel + */ + s8 max_eirp_psd; + /** + * max eirp power received from the AFC coordinator + * for the channel + */ + s8 max_eirp_power; +#endif }; #define HE_MAC_CAPAB_0 0 -- 2.43.0 _______________________________________________ Hostap mailing list Hostap@xxxxxxxxxxxxxxxxxxx http://lists.infradead.org/mailman/listinfo/hostap