From a05ba58a2d04058244d281b4859abc941254764d Mon Sep 17 00:00:00 2001 From: Evgenij Spitsyn Date: Mon, 3 Aug 2026 23:42:05 +0300 Subject: [PATCH 1/4] Add optional TLS for the control-plane connection (#16) --- components/microlink/include/microlink.h | 9 + .../microlink/include/microlink_internal.h | 9 + components/microlink/src/ml_coord.c | 161 +++++++++++++++++- 3 files changed, 175 insertions(+), 4 deletions(-) diff --git a/components/microlink/include/microlink.h b/components/microlink/include/microlink.h index 9126a53..99ecfe7 100644 --- a/components/microlink/include/microlink.h +++ b/components/microlink/include/microlink.h @@ -45,6 +45,15 @@ typedef struct { uint32_t disco_heartbeat_ms; /* DISCO keepalive interval (default: 3000) */ uint32_t stun_interval_ms; /* STUN re-probe interval (default: 23000) */ uint32_t ctrl_watchdog_ms; /* Control plane watchdog timeout (default: 120000) */ + + /* Control-plane transport (issue #16). By default the coordination server is + * reached over plain TCP on port 80 - the Noise layer provides the + * encryption - which fails when the server sits behind an HTTPS reverse + * proxy (Caddy/nginx in front of Headscale) or is the hosted Tailscale + * service. Set ctrl_tls to wrap the coordination connection in TLS as well. + * ctrl_port overrides the TCP port (0 = 443 when ctrl_tls, otherwise 80). */ + bool ctrl_tls; + uint16_t ctrl_port; } microlink_config_t; /* Peer info (read-only snapshot) */ diff --git a/components/microlink/include/microlink_internal.h b/components/microlink/include/microlink_internal.h index add69c7..e9dcd12 100644 --- a/components/microlink/include/microlink_internal.h +++ b/components/microlink/include/microlink_internal.h @@ -379,6 +379,15 @@ struct microlink_s { /* Coordination socket (owned exclusively by coord task) */ int coord_sock; + /* Control-plane TLS (issue #16): when config.ctrl_tls is set, the coord + * connection is wrapped in TLS, mirroring the DERP setup, so it can reach a + * control server behind an HTTPS reverse proxy or the hosted Tailscale + * service. coord_tls_up tracks whether the context below is initialised. */ + bool coord_tls_up; + mbedtls_ssl_context coord_ssl; + mbedtls_ssl_config coord_ssl_conf; + mbedtls_entropy_context coord_entropy; + mbedtls_ctr_drbg_context coord_ctr_drbg; uint32_t h2_next_stream_id; /* Next H2 stream ID for endpoint updates (odd, starts at 7) */ /* WireGuard netif (owned exclusively by wg_mgr task) */ diff --git a/components/microlink/src/ml_coord.c b/components/microlink/src/ml_coord.c index 8da8d4d..c8a25ec 100644 --- a/components/microlink/src/ml_coord.c +++ b/components/microlink/src/ml_coord.c @@ -31,6 +31,8 @@ #include "lwip/sockets.h" #include "lwip/netdb.h" #include "mbedtls/base64.h" +#include "mbedtls/error.h" +#include "mbedtls/net_sockets.h" #include #include @@ -85,7 +87,66 @@ static int hex_to_bytes(const char *hex, uint8_t *bytes, size_t max_len) { * TCP I/O helpers for coord socket (owned exclusively by this task) * ========================================================================== */ +/* --- optional TLS wrapper for the coordination connection (issue #16) ------ * + * When config.ctrl_tls is set the coord socket is wrapped in TLS, so the + * control plane can sit behind an HTTPS reverse proxy or be the hosted Tailscale + * service. The BIO and the setup mirror the DERP connection (see ml_derp.c). */ + +#define COORD_TLS_TIMEOUT_MS 10000 + +static int coord_bio_send(void *ctx, const unsigned char *buf, size_t len) { + int fd = *(int *)ctx; + if (fd < 0) return MBEDTLS_ERR_NET_INVALID_CONTEXT; + int ret = (int)ml_write_sock(fd, buf, len); + if (ret < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) return MBEDTLS_ERR_SSL_WANT_WRITE; + if (errno == EPIPE || errno == ECONNRESET) return MBEDTLS_ERR_NET_CONN_RESET; + if (errno == EINTR) return MBEDTLS_ERR_SSL_WANT_WRITE; + return MBEDTLS_ERR_NET_SEND_FAILED; + } + return ret; +} + +static int coord_bio_recv(void *ctx, unsigned char *buf, size_t len, uint32_t timeout) { + int fd = *(int *)ctx; + if (fd < 0) return MBEDTLS_ERR_NET_INVALID_CONTEXT; + uint32_t t = timeout ? timeout : COORD_TLS_TIMEOUT_MS; + struct timeval tv = { .tv_sec = t / 1000, .tv_usec = (t % 1000) * 1000 }; + ml_setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + int ret = (int)ml_read_sock(fd, buf, len); + if (ret < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) return MBEDTLS_ERR_SSL_TIMEOUT; + if (errno == EPIPE || errno == ECONNRESET) return MBEDTLS_ERR_NET_CONN_RESET; + if (errno == EINTR) return MBEDTLS_ERR_SSL_WANT_READ; + return MBEDTLS_ERR_NET_RECV_FAILED; + } + return ret; +} + +/* Free the coord TLS context if one is up (before a reconnect, or on teardown). */ +static void coord_tls_free(microlink_t *ml) { + if (!ml->coord_tls_up) return; + mbedtls_ssl_free(&ml->coord_ssl); + mbedtls_ssl_config_free(&ml->coord_ssl_conf); + mbedtls_ctr_drbg_free(&ml->coord_ctr_drbg); + mbedtls_entropy_free(&ml->coord_entropy); + ml->coord_tls_up = false; +} + static int coord_send(microlink_t *ml, const uint8_t *data, size_t len) { + if (ml->config.ctrl_tls) { + size_t sent = 0; + while (sent < len) { + int n = mbedtls_ssl_write(&ml->coord_ssl, data + sent, len - sent); + if (n == MBEDTLS_ERR_SSL_WANT_READ || n == MBEDTLS_ERR_SSL_WANT_WRITE) continue; + if (n <= 0) { + ESP_LOGE(TAG, "coord_send (tls) failed: %d", n); + return -1; + } + sent += n; + } + return 0; + } /* Set send timeout to prevent indefinite blocking (v1 uses MSG_DONTWAIT) */ struct timeval snd_tv = { .tv_sec = 5, .tv_usec = 0 }; ml_setsockopt(ml->coord_sock, SOL_SOCKET, SO_SNDTIMEO, &snd_tv, sizeof(snd_tv)); @@ -104,6 +165,30 @@ static int coord_send(microlink_t *ml, const uint8_t *data, size_t len) { } static int coord_recv(microlink_t *ml, uint8_t *buf, size_t len) { + if (ml->config.ctrl_tls) { + size_t recvd = 0; + int retries = 0; + while (recvd < len) { + int n = mbedtls_ssl_read(&ml->coord_ssl, buf + recvd, len - recvd); + if (n == MBEDTLS_ERR_SSL_WANT_READ || n == MBEDTLS_ERR_SSL_WANT_WRITE || + n == MBEDTLS_ERR_SSL_TIMEOUT) { + if (recvd == 0) return -1; /* nothing consumed yet: caller can retry */ + if (++retries > 300) { /* ~3 s: must finish to keep frame alignment */ + ESP_LOGE(TAG, "coord_recv (tls) partial timeout: %d/%d", (int)recvd, (int)len); + return -1; + } + vTaskDelay(pdMS_TO_TICKS(10)); + continue; + } + if (n <= 0) { + ESP_LOGE(TAG, "coord_recv (tls) failed: %d", n); + return -1; + } + recvd += n; + retries = 0; + } + return 0; + } size_t recvd = 0; int retries = 0; while (recvd < len) { @@ -134,6 +219,21 @@ static int coord_recv(microlink_t *ml, uint8_t *buf, size_t len) { return 0; } +/* Read up to len bytes in one go (not a fixed-length read), TLS-aware. Used for + * the HTTP upgrade response and the proactive frames that may trail it. Returns + * the byte count, or <= 0 on error/timeout. Note: over TLS the read timeout is + * the config's (COORD_TLS_TIMEOUT_MS), not any SO_RCVTIMEO set by the caller. */ +static int coord_read_some(microlink_t *ml, uint8_t *buf, size_t len) { + if (ml->config.ctrl_tls) { + int n; + do { + n = mbedtls_ssl_read(&ml->coord_ssl, buf, len); + } while (n == MBEDTLS_ERR_SSL_WANT_READ || n == MBEDTLS_ERR_SSL_WANT_WRITE); + return n; + } + return (int)ml_recv(ml->coord_sock, buf, len, 0); +} + /* ============================================================================ * Noise-encrypted transport I/O * ========================================================================== */ @@ -222,12 +322,21 @@ static int noise_recv(microlink_t *ml, ml_noise_state_t *noise, static int do_tcp_connect(microlink_t *ml) { int64_t t_start = esp_timer_get_time(); + /* Drop any TLS context left from a previous connection before reconnecting. */ + coord_tls_free(ml); + + /* Control-plane port: explicit override, else 443 with TLS or 80 without. */ + int port = ml->config.ctrl_port ? ml->config.ctrl_port + : (ml->config.ctrl_tls ? 443 : 80); + char port_str[8]; + snprintf(port_str, sizeof(port_str), "%d", port); + ESP_LOGI(TAG, "Resolving %s...", CTRL_HOST(ml)); struct addrinfo hints = { .ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM }; struct addrinfo *res = NULL; - if (ml_getaddrinfo(CTRL_HOST(ml), "80", &hints, &res) != 0 || !res) { + if (ml_getaddrinfo(CTRL_HOST(ml), port_str, &hints, &res) != 0 || !res) { ESP_LOGE(TAG, "DNS resolve failed for %s", CTRL_HOST(ml)); return -1; } @@ -257,7 +366,7 @@ static int do_tcp_connect(microlink_t *ml) { ml_setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(keepintvl)); ml_setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, &keepcnt, sizeof(keepcnt)); - ESP_LOGI(TAG, "Connecting to %s:80...", CTRL_HOST(ml)); + ESP_LOGI(TAG, "Connecting to %s:%d...", CTRL_HOST(ml), port); if (ml_connect(sock, res->ai_addr, res->ai_addrlen) < 0) { ESP_LOGE(TAG, "TCP connect failed: %d", errno); @@ -272,6 +381,45 @@ static int do_tcp_connect(microlink_t *ml) { (t_tcp - t_dns) / 1000, (t_tcp - t_start) / 1000); ml->coord_sock = sock; + + /* Wrap the connection in TLS when the control plane requires it (issue #16). + * VERIFY_NONE matches the DERP connection: the ts2021 Noise handshake, not + * the TLS certificate, is what authenticates the control plane. */ + if (ml->config.ctrl_tls) { + mbedtls_ssl_init(&ml->coord_ssl); + mbedtls_ssl_config_init(&ml->coord_ssl_conf); + mbedtls_entropy_init(&ml->coord_entropy); + mbedtls_ctr_drbg_init(&ml->coord_ctr_drbg); + ml->coord_tls_up = true; /* context now needs freeing on the next reconnect */ + + mbedtls_ctr_drbg_seed(&ml->coord_ctr_drbg, mbedtls_entropy_func, + &ml->coord_entropy, NULL, 0); + mbedtls_ssl_config_defaults(&ml->coord_ssl_conf, MBEDTLS_SSL_IS_CLIENT, + MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT); + mbedtls_ssl_conf_authmode(&ml->coord_ssl_conf, MBEDTLS_SSL_VERIFY_NONE); + mbedtls_ssl_conf_rng(&ml->coord_ssl_conf, mbedtls_ctr_drbg_random, &ml->coord_ctr_drbg); + mbedtls_ssl_conf_read_timeout(&ml->coord_ssl_conf, COORD_TLS_TIMEOUT_MS); + mbedtls_ssl_setup(&ml->coord_ssl, &ml->coord_ssl_conf); + mbedtls_ssl_set_hostname(&ml->coord_ssl, CTRL_HOST(ml)); + mbedtls_ssl_set_bio(&ml->coord_ssl, &ml->coord_sock, coord_bio_send, NULL, coord_bio_recv); + + int64_t t_tls_start = esp_timer_get_time(); + int ret; + while ((ret = mbedtls_ssl_handshake(&ml->coord_ssl)) != 0) { + if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) continue; + char eb[128]; + mbedtls_strerror(ret, eb, sizeof(eb)); + ESP_LOGE(TAG, "coord TLS handshake failed: %s", eb); + ml_close_sock(sock); + ml->coord_sock = -1; + coord_tls_free(ml); + return -1; + } + ESP_LOGI(TAG, "[TIMING] coord TLS handshake: %lld ms", + (esp_timer_get_time() - t_tls_start) / 1000); + ESP_LOGI(TAG, "coord TLS established with %s", CTRL_HOST(ml)); + } + return 0; } @@ -334,7 +482,7 @@ static int do_noise_handshake(microlink_t *ml, ml_noise_state_t *noise) { uint8_t *resp = ml_psram_malloc(2048); if (!resp) return -1; - int total = ml_recv(ml->coord_sock, resp, 2047, 0); + int total = coord_read_some(ml, resp, 2047); if (total <= 0) { ESP_LOGE(TAG, "Handshake recv failed: %d (errno=%d)", total, errno); free(resp); @@ -468,7 +616,7 @@ static int do_noise_handshake(microlink_t *ml, ml_noise_state_t *noise) { extra_data = ml_psram_malloc(1024); if (extra_data) { - int n = ml_recv(ml->coord_sock, extra_data, 1024, 0); + int n = coord_read_some(ml, extra_data, 1024); if (n > 0) { extra_len = n; ESP_LOGI(TAG, "Read %d bytes of proactive frames from socket", extra_len); @@ -2586,6 +2734,11 @@ void ml_coord_task(void *arg) { ml_close_sock(ml->coord_sock); ml->coord_sock = -1; } + /* Free the TLS context too: mbedtls_ssl_free zeroizes the session key + * material, matching how the Noise state is wiped just below and the + * long-term keys in microlink_destroy. Otherwise a shutdown while connected + * over TLS would leak the context and leave its keys in freed heap. */ + coord_tls_free(ml); memset(&noise, 0, sizeof(noise)); ESP_LOGI(TAG, "Coord task exiting"); From 0c7f29eac344fcf204361946ff6ce913367ba76c Mon Sep 17 00:00:00 2001 From: Evgenij Spitsyn Date: Mon, 3 Aug 2026 23:42:43 +0300 Subject: [PATCH 2/4] Build on SoCs without a native WiFi radio (ESP32-P4) --- components/microlink/src/microlink.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/components/microlink/src/microlink.c b/components/microlink/src/microlink.c index 237f039..da9d393 100644 --- a/components/microlink/src/microlink.c +++ b/components/microlink/src/microlink.c @@ -12,6 +12,7 @@ #include "esp_timer.h" #include "esp_mac.h" #include "esp_wifi.h" +#include "soc/soc_caps.h" #include "nvs_flash.h" #include "nvs.h" #include "cJSON.h" @@ -306,12 +307,16 @@ esp_err_t microlink_start(microlink_t *ml) { ml->state = ML_STATE_WIFI_WAIT; - /* Set WiFi TX power if configured */ + /* Set WiFi TX power if configured. Only on SoCs with a native radio - the + * ESP32-P4 (Ethernet/cellular here) has no WiFi MAC, so the symbol is not + * even linkable there. */ +#if SOC_WIFI_SUPPORTED if (ml->config.wifi_tx_power_dbm > 0) { int8_t power_quarter_dbm = ml->config.wifi_tx_power_dbm * 4; esp_wifi_set_max_tx_power(power_quarter_dbm); ESP_LOGI(TAG, "WiFi TX power set to %d dBm", ml->config.wifi_tx_power_dbm); } +#endif #ifdef CONFIG_ML_ZERO_COPY_WG /* Zero-copy mode: raw lwIP PCB replaces BSD socket for DISCO/WG UDP. From 6b48960a39b8881e3e0ff185f3b428d0fe133e48 Mon Sep 17 00:00:00 2001 From: Evgenij Spitsyn Date: Mon, 3 Aug 2026 23:43:06 +0300 Subject: [PATCH 3/4] Advertise the local endpoint on Ethernet, not just WIFI_STA_DEF --- components/microlink/src/ml_wg_mgr.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/components/microlink/src/ml_wg_mgr.c b/components/microlink/src/ml_wg_mgr.c index ed40942..826edf4 100644 --- a/components/microlink/src/ml_wg_mgr.c +++ b/components/microlink/src/ml_wg_mgr.c @@ -1208,8 +1208,13 @@ static void disco_send_call_me_maybe(microlink_t *ml, int peer_idx) { plaintext[pt_len++] = DISCO_MSG_CALL_ME_MAYBE; plaintext[pt_len++] = 0; /* version */ - /* 1. Local LAN IP endpoint (critical for same-network peers) */ - esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); + /* 1. Local LAN IP endpoint (critical for same-network peers). Use whatever + * interface currently carries the default route (WiFi or Ethernet) rather + * than assuming WIFI_STA_DEF, so same-LAN direct paths work on Ethernet too. */ + esp_netif_t *netif = esp_netif_get_default_netif(); + if (!netif) { + netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); + } if (netif) { esp_netif_ip_info_t ip_info; if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK && ip_info.ip.addr != 0) { From b921dd190058dce92eabc1624e7550fa430e4901 Mon Sep 17 00:00:00 2001 From: Evgenij Spitsyn Date: Mon, 3 Aug 2026 23:43:27 +0300 Subject: [PATCH 4/4] Add an Ethernet example (ESP32-P4) --- .../clangd/index/main.c.C769994E2449B863.idx | Bin 0 -> 8064 bytes examples/eth_connect/CMakeLists.txt | 8 + examples/eth_connect/main/CMakeLists.txt | 5 + examples/eth_connect/main/main.c | 164 ++++++++++++++++++ examples/eth_connect/sdkconfig.defaults | 19 ++ 5 files changed, 196 insertions(+) create mode 100644 examples/eth_connect/.cache/clangd/index/main.c.C769994E2449B863.idx create mode 100644 examples/eth_connect/CMakeLists.txt create mode 100644 examples/eth_connect/main/CMakeLists.txt create mode 100644 examples/eth_connect/main/main.c create mode 100644 examples/eth_connect/sdkconfig.defaults diff --git a/examples/eth_connect/.cache/clangd/index/main.c.C769994E2449B863.idx b/examples/eth_connect/.cache/clangd/index/main.c.C769994E2449B863.idx new file mode 100644 index 0000000000000000000000000000000000000000..b9c346658e8060b6919c873316e8391a3fd68523 GIT binary patch literal 8064 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 F{{e2%Iwb%A literal 0 HcmV?d00001 diff --git a/examples/eth_connect/CMakeLists.txt b/examples/eth_connect/CMakeLists.txt new file mode 100644 index 0000000..338e847 --- /dev/null +++ b/examples/eth_connect/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.16) + +set(EXTRA_COMPONENT_DIRS + "../../components" +) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(microlink_eth_connect) diff --git a/examples/eth_connect/main/CMakeLists.txt b/examples/eth_connect/main/CMakeLists.txt new file mode 100644 index 0000000..00641a6 --- /dev/null +++ b/examples/eth_connect/main/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS "main.c" + INCLUDE_DIRS "." + REQUIRES microlink nvs_flash esp_eth esp_netif esp_event +) diff --git a/examples/eth_connect/main/main.c b/examples/eth_connect/main/main.c new file mode 100644 index 0000000..76a2774 --- /dev/null +++ b/examples/eth_connect/main/main.c @@ -0,0 +1,164 @@ +/* + * MicroLink over Ethernet (ESP32-P4) — minimal test for the #16 TLS control + * plane. Brings up the P4's internal EMAC (RMII, external clock), waits for a + * DHCP lease, then starts MicroLink with ctrl_tls=true so it can reach the + * hosted Tailscale coordination server (controlplane.tailscale.com, HTTPS-only). + * + * The auth key and device name come from Kconfig (idf.py menuconfig -> + * "MicroLink Configuration"): CONFIG_ML_TAILSCALE_AUTH_KEY, CONFIG_ML_DEVICE_NAME. + * + * Ethernet pin map is the Waveshare ESP32-P4 board (same as ESP-KVM). + */ +#include + +#include "esp_eth.h" +#include "esp_event.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "freertos/task.h" +#include "nvs_flash.h" +#include "soc/soc_caps.h" + +#include "microlink.h" + +static const char *TAG = "eth_ml"; + +/* Waveshare ESP32-P4 Ethernet (internal EMAC, RMII, clock fed in externally). */ +#define ETH_PHY_ADDR 1 +#define ETH_PHY_RST_GPIO 51 +#define ETH_RMII_CLK_GPIO 50 +#define ETH_RMII_TX_EN_GPIO 49 +#define ETH_RMII_TXD0_GPIO 34 +#define ETH_RMII_TXD1_GPIO 35 +#define ETH_RMII_CRS_DV_GPIO 28 +#define ETH_RMII_RXD0_GPIO 29 +#define ETH_RMII_RXD1_GPIO 30 +#define ETH_MDC_GPIO 31 +#define ETH_MDIO_GPIO 52 + +static EventGroupHandle_t s_net_events; +#define NET_GOT_IP BIT0 + +static void on_got_ip(void *arg, esp_event_base_t base, int32_t id, void *data) +{ + (void)arg; + (void)base; + (void)id; + ip_event_got_ip_t *e = (ip_event_got_ip_t *)data; + ESP_LOGI(TAG, "Ethernet got IP: " IPSTR, IP2STR(&e->ip_info.ip)); + xEventGroupSetBits(s_net_events, NET_GOT_IP); +} + +static void on_eth_event(void *arg, esp_event_base_t base, int32_t id, void *data) +{ + (void)arg; + (void)base; + (void)data; + if (id == ETHERNET_EVENT_CONNECTED) { + ESP_LOGI(TAG, "Ethernet link up"); + } else if (id == ETHERNET_EVENT_DISCONNECTED) { + ESP_LOGW(TAG, "Ethernet link down"); + } +} + +static esp_err_t eth_start(void) +{ + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); + eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); + phy_config.phy_addr = ETH_PHY_ADDR; + phy_config.reset_gpio_num = ETH_PHY_RST_GPIO; + + eth_esp32_emac_config_t emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG(); + emac_config.interface = EMAC_DATA_INTERFACE_RMII; + emac_config.clock_config.rmii.clock_mode = EMAC_CLK_EXT_IN; + emac_config.clock_config.rmii.clock_gpio = ETH_RMII_CLK_GPIO; +#if SOC_EMAC_USE_MULTI_IO_MUX + emac_config.emac_dataif_gpio.rmii.tx_en_num = ETH_RMII_TX_EN_GPIO; + emac_config.emac_dataif_gpio.rmii.txd0_num = ETH_RMII_TXD0_GPIO; + emac_config.emac_dataif_gpio.rmii.txd1_num = ETH_RMII_TXD1_GPIO; + emac_config.emac_dataif_gpio.rmii.crs_dv_num = ETH_RMII_CRS_DV_GPIO; + emac_config.emac_dataif_gpio.rmii.rxd0_num = ETH_RMII_RXD0_GPIO; + emac_config.emac_dataif_gpio.rmii.rxd1_num = ETH_RMII_RXD1_GPIO; +#endif + emac_config.smi_gpio.mdc_num = ETH_MDC_GPIO; + emac_config.smi_gpio.mdio_num = ETH_MDIO_GPIO; + + esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&emac_config, &mac_config); + esp_eth_phy_t *phy = esp_eth_phy_new_generic(&phy_config); + if (!mac || !phy) { + ESP_LOGE(TAG, "eth mac/phy alloc failed"); + return ESP_FAIL; + } + esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, phy); + esp_eth_handle_t eth_handle = NULL; + ESP_ERROR_CHECK(esp_eth_driver_install(ð_config, ð_handle)); + + esp_netif_config_t netif_cfg = ESP_NETIF_DEFAULT_ETH(); + esp_netif_t *eth_netif = esp_netif_new(&netif_cfg); + ESP_ERROR_CHECK(esp_netif_attach(eth_netif, esp_eth_new_netif_glue(eth_handle))); + ESP_ERROR_CHECK( + esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, on_got_ip, NULL)); + ESP_ERROR_CHECK( + esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, on_eth_event, NULL)); + ESP_ERROR_CHECK(esp_eth_start(eth_handle)); + return ESP_OK; +} + +static void on_state_change(microlink_t *ml, microlink_state_t state, void *user) +{ + (void)ml; + (void)user; + ESP_LOGI(TAG, "MicroLink state -> %d%s", (int)state, + state == ML_STATE_CONNECTED ? " (CONNECTED)" : ""); +} + +void app_main(void) +{ + esp_err_t err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ESP_ERROR_CHECK(nvs_flash_init()); + } + + s_net_events = xEventGroupCreate(); + ESP_ERROR_CHECK(eth_start()); + + ESP_LOGI(TAG, "Waiting for an Ethernet IP..."); + xEventGroupWaitBits(s_net_events, NET_GOT_IP, pdFALSE, pdTRUE, portMAX_DELAY); + + microlink_config_t config = { + .auth_key = CONFIG_ML_TAILSCALE_AUTH_KEY, + .device_name = CONFIG_ML_DEVICE_NAME, + .enable_derp = true, + .enable_stun = true, + .enable_disco = true, + .max_peers = 16, + /* #16: the hosted Tailscale control plane is HTTPS-only, so wrap the + * coordination connection in TLS. */ + .ctrl_tls = true, + }; + + microlink_t *ml = microlink_init(&config); + if (!ml) { + ESP_LOGE(TAG, "microlink_init failed (auth key set?)"); + return; + } + microlink_set_state_callback(ml, on_state_change, NULL); + + ESP_LOGI(TAG, "Starting MicroLink over Ethernet (ctrl_tls=on)..."); + ESP_ERROR_CHECK(microlink_start(ml)); + + while (!microlink_is_connected(ml)) { + vTaskDelay(pdMS_TO_TICKS(1000)); + } + ESP_LOGI(TAG, "CONNECTED — tailnet IP: %u.%u.%u.%u", + (unsigned)(microlink_get_vpn_ip(ml) >> 24) & 0xFF, + (unsigned)(microlink_get_vpn_ip(ml) >> 16) & 0xFF, + (unsigned)(microlink_get_vpn_ip(ml) >> 8) & 0xFF, + (unsigned)microlink_get_vpn_ip(ml) & 0xFF); +} diff --git a/examples/eth_connect/sdkconfig.defaults b/examples/eth_connect/sdkconfig.defaults new file mode 100644 index 0000000..c5ce1e4 --- /dev/null +++ b/examples/eth_connect/sdkconfig.defaults @@ -0,0 +1,19 @@ +# MicroLink over Ethernet — ESP32-P4 (Waveshare) +CONFIG_IDF_TARGET="esp32p4" + +# 32 MB HEX PSRAM (MicroLink allocates large buffers there) +CONFIG_SPIRAM=y +CONFIG_SPIRAM_MODE_HEX=y +CONFIG_SPIRAM_SPEED_200M=y + +# Ethernet: internal EMAC +CONFIG_ETH_USE_ESP32_EMAC=y +CONFIG_ETH_PHY_INTERFACE_RMII=y + +# mbedTLS: the coord/DERP TLS clients skip cert validation (Noise authenticates +# the control plane), so no cert bundle is needed here. +# ChaCha20-Poly1305 for the Noise layer (ml_noise.c). On WiFi SoCs the supplicant +# pulls these in transitively; the P4 has no WiFi, so enable them explicitly. +CONFIG_MBEDTLS_CHACHA20_C=y +CONFIG_MBEDTLS_POLY1305_C=y +CONFIG_MBEDTLS_CHACHAPOLY_C=y