feat(esp_http_server): Adds support to bind an interface to server

Closes https://github.com/espressif/esp-idf/issues/17859
This commit is contained in:
Ashish Sharma
2026-03-17 15:22:45 +08:00
parent 6ed9d5bd43
commit 7e2e09c15c
5 changed files with 136 additions and 4 deletions
@@ -16,6 +16,7 @@
#include <esp_err.h>
#include <esp_event.h>
#include <esp_event_base.h>
#include <net/if.h>
#ifdef __cplusplus
extern "C" {
@@ -78,6 +79,7 @@ initializer that should be kept in sync
.keep_alive_idle = 0, \
.keep_alive_interval = 0, \
.keep_alive_count = 0, \
.if_name = NULL, \
.open_fn = NULL, \
.close_fn = NULL, \
.uri_match_fn = NULL \
@@ -239,6 +241,10 @@ typedef struct httpd_config {
int keep_alive_idle; /*!< Keep-alive idle time. Default is 5 (second) */
int keep_alive_interval;/*!< Keep-alive interval time. Default is 5 (second) */
int keep_alive_count; /*!< Keep-alive packet retry send count. Default is 3 counts */
struct ifreq *if_name; /*!< Bind server to a specific network interface.
If NULL, server listens on all interfaces (INADDR_ANY).
The pointer only needs to remain valid for the duration of
httpd_start() -- it is not referenced after that call returns. */
/**
* Custom session opening callback.
*
@@ -12,6 +12,7 @@
#include <esp_err.h>
#include <assert.h>
#include <netinet/tcp.h>
#include <net/if.h>
#include <esp_http_server.h>
#include "esp_httpd_priv.h"
@@ -386,6 +387,26 @@ static esp_err_t httpd_server_init(struct httpd_data *hd)
ESP_LOGW(TAG, LOG_FMT("error in setsockopt SO_REUSEADDR (%d)"), errno);
}
/* Bind to specific network interface if configured */
if (hd->config.if_name && hd->config.if_name->ifr_name[0] != 0) {
ESP_LOGI(TAG, LOG_FMT("Binding server to interface: %s"), hd->config.if_name->ifr_name);
#ifndef __APPLE__
if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, hd->config.if_name, sizeof(struct ifreq)) < 0) {
#else
int idx = if_nametoindex(hd->config.if_name->ifr_name);
if (idx == 0) {
ESP_LOGE(TAG, LOG_FMT("Invalid interface name %s"), hd->config.if_name->ifr_name);
close(fd);
return ESP_FAIL;
}
if (setsockopt(fd, IPPROTO_IP, IP_BOUND_IF, &idx, sizeof(idx)) < 0) {
#endif
ESP_LOGE(TAG, LOG_FMT("Failed to bind to interface %s (%d)"), hd->config.if_name->ifr_name, errno);
close(fd);
return ESP_FAIL;
}
}
int ret = bind(fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
if (ret < 0) {
ESP_LOGE(TAG, LOG_FMT("error in bind (%d)"), errno);
@@ -9,6 +9,8 @@
#include <esp_system.h>
#include <esp_http_server.h>
#include <esp_heap_caps.h>
#include <net/if.h>
#include "esp_log.h"
#include "unity.h"
#include "test_utils.h"
@@ -16,6 +18,8 @@
int pre_start_mem, post_stop_mem, post_stop_min_mem;
bool basic_sanity = true;
#define TAG "test_http_server"
esp_err_t null_func(httpd_req_t *req)
{
return ESP_OK;
@@ -160,7 +164,7 @@ TEST_CASE("Leak Test", "[HTTP SERVER]")
test_case_uses_tcpip();
task_count = uxTaskGetNumberOfTasks();
printf("Initial task count: %d\n", task_count);
ESP_LOGI(TAG, "Initial task count: %d\n", task_count);
pre_start_mem = esp_get_free_heap_size();
@@ -170,7 +174,7 @@ TEST_CASE("Leak Test", "[HTTP SERVER]")
unsigned num_tasks = uxTaskGetNumberOfTasks();
task_count++;
if (num_tasks != task_count) {
printf("Incorrect task count (starting): %d expected %d\n",
ESP_LOGE(TAG, "Incorrect task count (starting): %d expected %d\n",
num_tasks, task_count);
res = false;
}
@@ -178,14 +182,14 @@ TEST_CASE("Leak Test", "[HTTP SERVER]")
for (int i = 0; i < SERVER_INSTANCES; i++) {
if (httpd_stop(hd[i]) != ESP_OK) {
printf("Failed to stop httpd task %d\n", i);
ESP_LOGE(TAG, "Failed to stop httpd task %d\n", i);
res = false;
}
vTaskDelay(10);
unsigned num_tasks = uxTaskGetNumberOfTasks();
task_count--;
if (num_tasks != task_count) {
printf("Incorrect task count (stopping): %d expected %d\n",
ESP_LOGE(TAG, "Incorrect task count (stopping): %d expected %d\n",
num_tasks, task_count);
res = false;
}
@@ -290,6 +294,79 @@ TEST_CASE("Max Allowed Sockets Test", "[HTTP SERVER]")
TEST_ASSERT(httpd_start(&hd, &config) != ESP_OK);
}
TEST_CASE("Interface Binding Test", "[HTTP SERVER]")
{
test_case_uses_tcpip();
httpd_handle_t hd;
struct ifreq ifr;
/* Test 1: Server starts successfully with NULL if_name (default behavior - INADDR_ANY) */
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = 8080;
TEST_ASSERT(config.if_name == NULL);
TEST_ASSERT(httpd_start(&hd, &config) == ESP_OK);
TEST_ASSERT(httpd_stop(hd) == ESP_OK);
/* Test 2: Server starts successfully with loopback interface binding */
memset(&ifr, 0, sizeof(ifr));
strcpy(ifr.ifr_name, "lo0"); // Loopback interface
config.server_port = 8081;
config.if_name = &ifr;
/* On embedded systems, loopback may not exist, so we test both success and expected failure */
esp_err_t ret = httpd_start(&hd, &config);
if (ret == ESP_OK) {
/* If loopback exists and binding succeeds, verify we can stop cleanly */
TEST_ASSERT(httpd_stop(hd) == ESP_OK);
} else {
/* If loopback doesn't exist or binding fails, that's also acceptable */
/* The important part is that the server doesn't crash */
ESP_LOGI(TAG, "Loopback binding failed as expected on this platform\n");
}
/* Test 3: Server handles empty interface name (should bind to all interfaces) */
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_name[0] = '\0'; // Empty interface name
config.server_port = 8082;
config.if_name = &ifr;
/* Empty interface name should be ignored and server should start normally */
TEST_ASSERT(httpd_start(&hd, &config) == ESP_OK);
TEST_ASSERT(httpd_stop(hd) == ESP_OK);
/* Test 4: Server handles invalid interface name gracefully */
memset(&ifr, 0, sizeof(ifr));
strcpy(ifr.ifr_name, "nonex"); // Invalid interface
config.server_port = 8083;
config.if_name = &ifr;
/* Starting with invalid interface should fail gracefully */
ret = httpd_start(&hd, &config);
TEST_ASSERT(ret != ESP_OK);
if (ret != ESP_OK) {
/* Expected failure - invalid interface */
ESP_LOGI(TAG, "Invalid interface binding failed as expected\n");
} else {
/* On some platforms, the check might not happen until actual use */
httpd_stop(hd);
}
/* Test 5: Verify backward compatibility - multiple servers without interface binding */
httpd_handle_t hd1, hd2;
httpd_config_t config1 = HTTPD_DEFAULT_CONFIG();
httpd_config_t config2 = HTTPD_DEFAULT_CONFIG();
config1.server_port = 8084;
config1.ctrl_port = ESP_HTTPD_DEF_CTRL_PORT + 1;
config2.server_port = 8085;
config2.ctrl_port = ESP_HTTPD_DEF_CTRL_PORT + 2;
TEST_ASSERT(httpd_start(&hd1, &config1) == ESP_OK);
TEST_ASSERT(httpd_start(&hd2, &config2) == ESP_OK);
TEST_ASSERT(httpd_stop(hd1) == ESP_OK);
TEST_ASSERT(httpd_stop(hd2) == ESP_OK);
}
void app_main(void)
{
unity_run_menu();
@@ -207,6 +207,7 @@ typedef struct httpd_ssl_config httpd_ssl_config_t;
.keep_alive_idle = 0, \
.keep_alive_interval = 0, \
.keep_alive_count = 0, \
.if_name = NULL, \
.open_fn = NULL, \
.close_fn = NULL, \
.uri_match_fn = NULL \
@@ -21,6 +21,33 @@ Application Examples
- :example:`protocols/http_server/advanced_tests` demonstrates how to use the HTTP server for advanced testing.
Interface Binding
-----------------
By default, the server listens on all available interfaces (``INADDR_ANY``). This is the behavior when ``httpd_config_t.if_name`` is ``NULL``.
To bind the HTTP server to a specific network interface, set ``httpd_config_t.if_name`` to point to a ``struct ifreq`` with ``ifr_name`` populated (for example ``"eth0"``, ``"en0"``, or ``"lo"`` depending on platform).
.. code-block:: c
#include <net/if.h>
httpd_handle_t server = NULL;
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
struct ifreq ifr = {0};
strncpy(ifr.ifr_name, "eth0", sizeof(ifr.ifr_name) - 1);
ifr.ifr_name[sizeof(ifr.ifr_name) - 1] = '\0';
config.if_name = &ifr;
config.server_port = 80;
ESP_ERROR_CHECK(httpd_start(&server, &config));
Notes:
- ``if_name`` is only used during ``httpd_start()``. The ``ifreq`` object only needs to stay valid for the duration of that call.
Persistent Connections
----------------------