Merge branch 'feat/netif_dhcps_captive_portal' into 'master'

feat(esp_netif): add support for DHCP Option 114 captive portal URI

Closes IDFGH-11885

See merge request espressif/esp-idf!30267
This commit is contained in:
David Čermák 2024-05-09 17:22:22 +08:00
commit e42f1c53e0
9 changed files with 134 additions and 17 deletions

View File

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2019-2023 Espressif Systems (Shanghai) CO LTD * SPDX-FileCopyrightText: 2019-2024 Espressif Systems (Shanghai) CO LTD
* *
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
@ -594,6 +594,23 @@ esp_err_t esp_netif_napt_disable(esp_netif_t *esp_netif);
/** /**
* @brief Set or Get DHCP server option * @brief Set or Get DHCP server option
* *
* @note Please note that not all combinations of identifiers and options are supported.
* Get operations:
* * IP_ADDRESS_LEASE_TIME
* * ESP_NETIF_SUBNET_MASK/REQUESTED_IP_ADDRESS (both options do the same, they reflect dhcps_lease_t)
* * ROUTER_SOLICITATION_ADDRESS
* * DOMAIN_NAME_SERVER
* Set operations:
* * IP_ADDRESS_LEASE_TIME
* * ESP_NETIF_SUBNET_MASK -- set operation is allowed only if the configured mask corresponds to the settings,
* if not, please use esp_netif_set_ip_info() to prevent misconfiguration of DHCPS.
* * REQUESTED_IP_ADDRESS -- if the address pool is enabled, a sanity check for start/end addresses is performed
* before setting.
* * ROUTER_SOLICITATION_ADDRESS
* * DOMAIN_NAME_SERVER
* * ESP_NETIF_CAPTIVEPORTAL_URI -- set operation copies the pointer to the URI, so it is owned by the application
* and needs to be maintained valid throughout the entire DHCP Server lifetime.
*
* @param[in] esp_netif Handle to esp-netif instance * @param[in] esp_netif Handle to esp-netif instance
* @param[in] opt_op ESP_NETIF_OP_SET to set an option, ESP_NETIF_OP_GET to get an option. * @param[in] opt_op ESP_NETIF_OP_SET to set an option, ESP_NETIF_OP_GET to get an option.
* @param[in] opt_id Option index to get or set, must be one of the supported enum values. * @param[in] opt_id Option index to get or set, must be one of the supported enum values.
@ -613,6 +630,16 @@ esp_netif_dhcps_option(esp_netif_t *esp_netif, esp_netif_dhcp_option_mode_t opt_
/** /**
* @brief Set or Get DHCP client option * @brief Set or Get DHCP client option
* *
* @note Please note that not all combinations of identifiers and options are supported.
* Get operations:
* * ESP_NETIF_IP_REQUEST_RETRY_TIME
* * ESP_NETIF_VENDOR_SPECIFIC_INFO -- only available if ESP_DHCP_DISABLE_VENDOR_CLASS_IDENTIFIER=n
* Set operations:
* * ESP_NETIF_IP_REQUEST_RETRY_TIME
* * ESP_NETIF_VENDOR_SPECIFIC_INFO -- only available if ESP_DHCP_DISABLE_VENDOR_CLASS_IDENTIFIER=n
* lwip layer creates its own copy of the supplied identifier.
* (the internal copy could be feed by calling dhcp_free_vendor_class_identifier())
*
* @param[in] esp_netif Handle to esp-netif instance * @param[in] esp_netif Handle to esp-netif instance
* @param[in] opt_op ESP_NETIF_OP_SET to set an option, ESP_NETIF_OP_GET to get an option. * @param[in] opt_op ESP_NETIF_OP_SET to set an option, ESP_NETIF_OP_GET to get an option.
* @param[in] opt_id Option index to get or set, must be one of the supported enum values. * @param[in] opt_id Option index to get or set, must be one of the supported enum values.

View File

@ -89,6 +89,7 @@ typedef enum{
ESP_NETIF_IP_REQUEST_RETRY_TIME = 52, /**< Request IP address retry counter */ ESP_NETIF_IP_REQUEST_RETRY_TIME = 52, /**< Request IP address retry counter */
ESP_NETIF_VENDOR_CLASS_IDENTIFIER = 60, /**< Vendor Class Identifier of a DHCP client */ ESP_NETIF_VENDOR_CLASS_IDENTIFIER = 60, /**< Vendor Class Identifier of a DHCP client */
ESP_NETIF_VENDOR_SPECIFIC_INFO = 43, /**< Vendor Specific Information of a DHCP server */ ESP_NETIF_VENDOR_SPECIFIC_INFO = 43, /**< Vendor Specific Information of a DHCP server */
ESP_NETIF_CAPTIVEPORTAL_URI = 114, /**< Captive Portal Identification */
} esp_netif_dhcp_option_id_t; } esp_netif_dhcp_option_id_t;
/** IP event declarations */ /** IP event declarations */

View File

@ -2398,6 +2398,10 @@ esp_err_t esp_netif_dhcps_option_api(esp_netif_api_msg_t *msg)
} }
break; break;
} }
case ESP_NETIF_CAPTIVEPORTAL_URI: {
opt_info = (char *)opt->val;
break;
}
default: default:
break; break;

View File

@ -60,6 +60,7 @@
#define DHCP_OPTION_PERFORM_ROUTER_DISCOVERY 31 #define DHCP_OPTION_PERFORM_ROUTER_DISCOVERY 31
#define DHCP_OPTION_BROADCAST_ADDRESS 28 #define DHCP_OPTION_BROADCAST_ADDRESS 28
#define DHCP_OPTION_REQ_LIST 55 #define DHCP_OPTION_REQ_LIST 55
#define DHCP_OPTION_CAPTIVEPORTAL_URI 114
#define DHCP_OPTION_END 255 #define DHCP_OPTION_END 255
//#define USE_CLASS_B_NET 1 //#define USE_CLASS_B_NET 1
@ -135,6 +136,7 @@ struct dhcps_t {
dhcps_time_t dhcps_lease_time; dhcps_time_t dhcps_lease_time;
dhcps_offer_t dhcps_offer; dhcps_offer_t dhcps_offer;
dhcps_offer_t dhcps_dns; dhcps_offer_t dhcps_dns;
char *dhcps_captiveportal_uri;
dhcps_cb_t dhcps_cb; dhcps_cb_t dhcps_cb;
void* dhcps_cb_arg; void* dhcps_cb_arg;
struct udp_pcb *dhcps_pcb; struct udp_pcb *dhcps_pcb;
@ -164,6 +166,7 @@ dhcps_t *dhcps_new(void)
dhcps->dhcps_lease_time = DHCPS_LEASE_TIME_DEF; dhcps->dhcps_lease_time = DHCPS_LEASE_TIME_DEF;
dhcps->dhcps_offer = 0xFF; dhcps->dhcps_offer = 0xFF;
dhcps->dhcps_dns = 0x00; dhcps->dhcps_dns = 0x00;
dhcps->dhcps_captiveportal_uri = NULL;
dhcps->dhcps_pcb = NULL; dhcps->dhcps_pcb = NULL;
dhcps->state = DHCPS_HANDLE_CREATED; dhcps->state = DHCPS_HANDLE_CREATED;
return dhcps; return dhcps;
@ -238,6 +241,10 @@ void *dhcps_option_info(dhcps_t *dhcps, u8_t op_id, u32_t opt_len)
option_arg = &dhcps->dhcps_mask; option_arg = &dhcps->dhcps_mask;
} }
break;
case CAPTIVEPORTAL_URI:
option_arg = &dhcps->dhcps_captiveportal_uri;
break; break;
default: default:
break; break;
@ -292,6 +299,11 @@ err_t dhcps_set_option_info(dhcps_t *dhcps, u8_t op_id, void *opt_info, u32_t op
dhcps->dhcps_mask = *(ip4_addr_t *)opt_info; dhcps->dhcps_mask = *(ip4_addr_t *)opt_info;
} }
break;
case CAPTIVEPORTAL_URI:
dhcps->dhcps_captiveportal_uri = (char *)opt_info;
break;
default: default:
break; break;
@ -400,6 +412,7 @@ static u8_t *add_msg_type(u8_t *optptr, u8_t type)
*******************************************************************************/ *******************************************************************************/
static u8_t *add_offer_options(dhcps_t *dhcps, u8_t *optptr) static u8_t *add_offer_options(dhcps_t *dhcps, u8_t *optptr)
{ {
u32_t i;
ip4_addr_t ipadd; ip4_addr_t ipadd;
ipadd.addr = *((u32_t *) &dhcps->server_address); ipadd.addr = *((u32_t *) &dhcps->server_address);
@ -468,6 +481,17 @@ static u8_t *add_offer_options(dhcps_t *dhcps, u8_t *optptr)
*optptr++ = 0x05; *optptr++ = 0x05;
*optptr++ = 0xdc; *optptr++ = 0xdc;
if (dhcps->dhcps_captiveportal_uri) {
size_t length = strlen(dhcps->dhcps_captiveportal_uri);
*optptr++ = DHCP_OPTION_CAPTIVEPORTAL_URI;
*optptr++ = length;
for (i = 0; i < length; i++)
{
*optptr++ = dhcps->dhcps_captiveportal_uri[i];
}
}
*optptr++ = DHCP_OPTION_PERFORM_ROUTER_DISCOVERY; *optptr++ = DHCP_OPTION_PERFORM_ROUTER_DISCOVERY;
*optptr++ = 1; *optptr++ = 1;
*optptr++ = 0x00; *optptr++ = 0x00;
@ -1006,7 +1030,7 @@ static s16_t parse_msg(dhcps_t *dhcps, struct dhcps_msg *m, u16_t len)
dhcps->client_address.addr = dhcps->client_address_plus.addr; dhcps->client_address.addr = dhcps->client_address_plus.addr;
} }
if (flag == false) { // search the fisrt unused ip if (flag == false) { // search the first unused ip
if (first_address.addr < pdhcps_pool->ip.addr) { if (first_address.addr < pdhcps_pool->ip.addr) {
flag = true; flag = true;
} else { } else {

View File

@ -1,16 +1,8 @@
// Copyright 2017 Espressif Systems (Shanghai) PTE LTD /*
// * SPDX-FileCopyrightText: 2017-2024 Espressif Systems (Shanghai) CO LTD
// Licensed under the Apache License, Version 2.0 (the "License"); *
// you may not use this file except in compliance with the License. * SPDX-License-Identifier: Apache-2.0
// You may obtain a copy of the License at */
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once #pragma once
#ifdef __cplusplus #ifdef __cplusplus
@ -129,6 +121,7 @@ typedef enum
CLIENT_LAST_TRANSACTION_TIME = 91, CLIENT_LAST_TRANSACTION_TIME = 91,
ASSOCIATED_IP = 92, ASSOCIATED_IP = 92,
USER_AUTHENTICATION_PROTOCOL = 98, USER_AUTHENTICATION_PROTOCOL = 98,
CAPTIVEPORTAL_URI = 114,
AUTO_CONFIGURE = 116, AUTO_CONFIGURE = 116,
NAME_SERVICE_SEARCH = 117, NAME_SERVICE_SEARCH = 117,
SUBNET_SELECTION = 118, SUBNET_SELECTION = 118,

View File

@ -5,7 +5,11 @@
(See the README.md file in the upper level 'examples' directory for more information about examples.) (See the README.md file in the upper level 'examples' directory for more information about examples.)
This example demonstrates a simple captive portal that will redirect all DNS IP questions to point to the softAP and redirect all HTTP requests to the captive portal root page. Triggers captive portal (sign in) pop up on Android, iOS and Windows. Note that the example will not redirect HTTPS requests. This example demonstrates two methods of a captive portal, used to direct users to an authentication page or other necessary starting point before browsing.
One approach response to all DNS queries with the address of the softAP, and redirects all HTTP requests to the captive portal root page. This "funnelling" of DNS and traffic triggers the captive portal (sign in) to appear on Android, iOS, and Windows. Note that the example will not redirect HTTPS requests.
The other approach is a more modern method which includes a field in the DHCP offer (AKA DHCP Option 114), provided when the client is assigned an IP address, which specifies to the client where the captive portal is. This is advantageous because it doesn't require the overhead of DNS redirects and can work more reliably around HTTPS, HTST, and other security systems, as well as being more standards compliant. This feature is toggleable in the `Example Configuration`, but does not conflict with the DNS methodology -- these two methods work towards the same goal and can complement each other.
## How to Use Example ## How to Use Example
@ -27,6 +31,7 @@ In the `Example Configuration` menu:
* Set `SoftAP SSID` * Set `SoftAP SSID`
* Set `SoftAP Password` * Set `SoftAP Password`
* Set `Maximal STA connections` * Set `Maximal STA connections`
* Set `DHCP Captive portal` to enable or disable DHCP Option 114
### Build and Flash ### Build and Flash
@ -42,6 +47,9 @@ See the Getting Started Guide for full steps to configure and use ESP-IDF to bui
## Example Output ## Example Output
### ESP32 Output for DNS Redirect
``` ```
I (733) example: Set up softAP with IP: 192.168.4.1 I (733) example: Set up softAP with IP: 192.168.4.1
I (743) example: wifi_init_softap finished. SSID:'esp32_ssid' password:'esp32_pwd' I (743) example: wifi_init_softap finished. SSID:'esp32_ssid' password:'esp32_pwd'
@ -95,3 +103,27 @@ I (23473) example: Serve root
I (23503) example_dns_redirect_server: Received 48 bytes from 192.168.4.2 | DNS reply with len: 64 I (23503) example_dns_redirect_server: Received 48 bytes from 192.168.4.2 | DNS reply with len: 64
I (23513) example_dns_redirect_server: Waiting for data I (23513) example_dns_redirect_server: Waiting for data
``` ```
### `tcpdump` Output for DHCP Option 114
Note `URL (114)` with the AP address.
```
19:14:20.522698 c8:yy:yy:yy:yy:yy > 74:xx:xx:xx:xx:xx, ethertype IPv4 (0x0800), length 590: (tos 0x0, ttl 64, id 243, offset 0, flags [none], proto UDP (17), length 576)
192.168.4.1.67 > 192.168.4.2.68: [udp sum ok] BOOTP/DHCP, Reply, length 548, xid 0x76a26648, Flags [none] (0x0000)
Your-IP 192.168.4.2
Client-Ethernet-Address 74:xx:xx:xx:xx:xx
Vendor-rfc1048 Extensions
Magic Cookie 0x63825363
DHCP-Message (53), length 1: Offer
Subnet-Mask (1), length 4: 255.255.255.0
Lease-Time (51), length 4: 7200
Server-ID (54), length 4: 192.168.4.1
Default-Gateway (3), length 4: 192.168.4.1
Domain-Name-Server (6), length 4: 192.168.4.1
BR (28), length 4: 192.168.4.255
MTU (26), length 2: 1500
URL (114), length 18: "http://192.168.4.1"
Router-Discovery (31), length 1: N
Vendor-Option (43), length 6: 1.4.0.0.0.2
```

View File

@ -17,4 +17,10 @@ menu "Example Configuration"
default 4 default 4
help help
Max number of the STA connects to AP. Max number of the STA connects to AP.
config ESP_ENABLE_DHCP_CAPTIVEPORTAL
bool "DHCP Captive portal"
default y
help
Enables more modern DHCP-based Option 114 to provide clients with the captive portal URI
endmenu endmenu

View File

@ -79,6 +79,32 @@ static void wifi_init_softap(void)
EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS); EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
} }
#ifdef CONFIG_ESP_ENABLE_DHCP_CAPTIVEPORTAL
static void dhcp_set_captiveportal_url(void) {
// get the IP of the access point to redirect to
esp_netif_ip_info_t ip_info;
esp_netif_get_ip_info(esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"), &ip_info);
char ip_addr[16];
inet_ntoa_r(ip_info.ip.addr, ip_addr, 16);
ESP_LOGI(TAG, "Set up softAP with IP: %s", ip_addr);
// turn the IP into a URI
char* captiveportal_uri = (char*) malloc(32 * sizeof(char));
assert(captiveportal_uri && "Failed to allocate captiveportal_uri");
strcpy(captiveportal_uri, "http://");
strcat(captiveportal_uri, ip_addr);
// get a handle to configure DHCP with
esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
// set the DHCP option 114
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_netif_dhcps_stop(netif));
ESP_ERROR_CHECK(esp_netif_dhcps_option(netif, ESP_NETIF_OP_SET, ESP_NETIF_CAPTIVEPORTAL_URI, captiveportal_uri, strlen(captiveportal_uri)));
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_netif_dhcps_start(netif));
}
#endif // CONFIG_ESP_ENABLE_DHCP_CAPTIVEPORTAL
// HTTP GET Handler // HTTP GET Handler
static esp_err_t root_get_handler(httpd_req_t *req) static esp_err_t root_get_handler(httpd_req_t *req)
{ {
@ -155,6 +181,11 @@ void app_main(void)
// Initialise ESP32 in SoftAP mode // Initialise ESP32 in SoftAP mode
wifi_init_softap(); wifi_init_softap();
// Configure DNS-based captive portal, if configured
#ifdef CONFIG_ESP_ENABLE_DHCP_CAPTIVEPORTAL
dhcp_set_captiveportal_url();
#endif
// Start the server for the first time // Start the server for the first time
start_webserver(); start_webserver();

View File

@ -498,7 +498,6 @@ components/hal/spi_slave_hal_iram.c
components/idf_test/include/idf_performance.h components/idf_test/include/idf_performance.h
components/log/host_test/log_test/main/log_test.cpp components/log/host_test/log_test/main/log_test.cpp
components/lwip/apps/ping/ping.c components/lwip/apps/ping/ping.c
components/lwip/include/apps/dhcpserver/dhcpserver_options.h
components/lwip/include/apps/esp_ping.h components/lwip/include/apps/esp_ping.h
components/lwip/include/apps/ping/ping.h components/lwip/include/apps/ping/ping.h
components/mbedtls/esp_crt_bundle/test_gen_crt_bundle/test_gen_crt_bundle.py components/mbedtls/esp_crt_bundle/test_gen_crt_bundle/test_gen_crt_bundle.py