diff --git a/src/helpers/NetworkInterface.cpp b/src/helpers/NetworkInterface.cpp index dc0be519..ecf105ad 100644 --- a/src/helpers/NetworkInterface.cpp +++ b/src/helpers/NetworkInterface.cpp @@ -455,7 +455,8 @@ class AutomaticNetworkInterface final : public NetworkInterface { // Hardware availability and stored credentials are configuration. Current // link/DHCP state is runtime state and must not permanently suppress the // MQTT task that monitors for a late cable or lease. - return _ethernet_started || (wifi_ssid && wifi_ssid[0] != '\0'); + (void)wifi_ssid; + return true; } bool isAutomatic() const override { return true; } diff --git a/src/helpers/NetworkPolicy.h b/src/helpers/NetworkPolicy.h index 5d083eb4..fc16ab2d 100644 --- a/src/helpers/NetworkPolicy.h +++ b/src/helpers/NetworkPolicy.h @@ -38,7 +38,7 @@ static constexpr MQTTTransitionActions mqttActions(NetworkTransition transition) return { transition == NetworkTransition::Down || transition == NetworkTransition::Switched, transition == NetworkTransition::Up || transition == NetworkTransition::Switched, - transition == NetworkTransition::Up || transition == NetworkTransition::Switched, + transition == NetworkTransition::Switched, }; } @@ -160,7 +160,8 @@ static inline NetworkMedium automaticSelection( if (input.selected == NetworkMedium::WiFi) { if (input.ethernet_connected && - input.ethernet_stable_ms >= kEthernetFailbackStableMs) { + (!input.wifi_connected || + input.ethernet_stable_ms >= kEthernetFailbackStableMs)) { return NetworkMedium::Ethernet; } return NetworkMedium::WiFi; diff --git a/src/helpers/bridges/MQTTBridge.cpp b/src/helpers/bridges/MQTTBridge.cpp index cfbe3ad5..427106fa 100644 --- a/src/helpers/bridges/MQTTBridge.cpp +++ b/src/helpers/bridges/MQTTBridge.cpp @@ -1252,7 +1252,10 @@ void MQTTBridge::mqttTaskLoop() { // Wait a bit for WiFi to start connecting vTaskDelay(pdMS_TO_TICKS(1000)); - bool network_was_connected = _network->isConnected(); + // Treat the first post-settle sample as an edge. A fast Wi-Fi association + // may complete during the delay above, before the task has sampled state; + // initializing from the live value would then defer NTP until its retry. + bool network_was_connected = false; unsigned long last_ntp_attempt = 0; // Main task loop diff --git a/src/helpers/esp32/WebConfigServer.cpp b/src/helpers/esp32/WebConfigServer.cpp index 37740e8c..1aa638d0 100644 --- a/src/helpers/esp32/WebConfigServer.cpp +++ b/src/helpers/esp32/WebConfigServer.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -23,6 +24,24 @@ // so an untouched password field never overwrites the stored value. static const char SECRET_SENTINEL[] = "********"; +// esp_random() has a true entropy source only while RF is active. Ethernet +// LAN mode deliberately keeps Wi-Fi and Bluetooth down, so temporarily enable +// the bootloader RNG source while generating authentication secrets there. +class WebConfigEntropyGuard { + bool _enabled; + + public: + WebConfigEntropyGuard() + : _enabled(activeNetworkInterface().medium() == + NetworkMedium::Ethernet) { + if (_enabled) bootloader_random_enable(); + } + + ~WebConfigEntropyGuard() { + if (_enabled) bootloader_random_disable(); + } +}; + // Key classification (allowlist, secret detection, slot-prefix parsing) lives in // helpers/WebConfigKeys.h so it can be unit-tested on the host. Thin aliases keep // the call sites below readable. @@ -262,8 +281,11 @@ bool WebConfigServer::startLanMode(IPAddress ip, bool initial_setup, char reply[ } _initial_setup = initial_setup; if (_initial_setup) { - for (int i = 0; i < 3; ++i) { - sprintf(&_setup_code[i * 4], "%04X", (unsigned)(esp_random() & 0xffff)); + { + WebConfigEntropyGuard entropy; + for (int i = 0; i < 3; ++i) { + sprintf(&_setup_code[i * 4], "%04X", (unsigned)(esp_random() & 0xffff)); + } } _setup_code[12] = 0; } else { @@ -730,7 +752,13 @@ void WebConfigServer::handleLogin(AsyncWebServerRequest* req) { } _login_fails = 0; _login_lock_until = 0; - for (int i = 0; i < 4; i++) sprintf(&_session_token[i * 8], "%08lx", (unsigned long)esp_random()); + { + WebConfigEntropyGuard entropy; + for (int i = 0; i < 4; i++) { + sprintf(&_session_token[i * 8], "%08lx", + (unsigned long)esp_random()); + } + } _session_last_seen = now; AsyncWebServerResponse* res = req->beginResponse(200, "application/json", "{\"ok\":true}"); diff --git a/src/helpers/ethernet/ch390/CH390Config.h b/src/helpers/ethernet/ch390/CH390Config.h index 656603ba..05723067 100644 --- a/src/helpers/ethernet/ch390/CH390Config.h +++ b/src/helpers/ethernet/ch390/CH390Config.h @@ -17,7 +17,6 @@ static inline bool beginConfiguredCH390(const char* hostname = nullptr) { if (hostname && hostname[0] != '\0') { if (!CH390.setHostname(hostname)) { Serial.printf("Network: invalid Ethernet hostname %s\n", hostname); - return false; } } if (!CH390.begin(config)) return false; diff --git a/test/test_network_policy/test_network_policy.cpp b/test/test_network_policy/test_network_policy.cpp index b1131a14..6d5f1f36 100644 --- a/test/test_network_policy/test_network_policy.cpp +++ b/test/test_network_policy/test_network_policy.cpp @@ -9,11 +9,11 @@ TEST(NetworkPolicy, MqttDownDisconnectsSlotsWithoutRequestingImmediateRetry) { EXPECT_FALSE(actions.reset_reconnect_backoff); } -TEST(NetworkPolicy, MqttUpRequestsImmediateRetryWithoutDisconnectingSlots) { +TEST(NetworkPolicy, MqttUpRetriesWithoutClearingBrokerCircuitBreaker) { const auto actions = NetworkPolicy::mqttActions(NetworkTransition::Up); EXPECT_FALSE(actions.stop_started_slots); EXPECT_TRUE(actions.retry_disconnected_slots_now); - EXPECT_TRUE(actions.reset_reconnect_backoff); + EXPECT_FALSE(actions.reset_reconnect_backoff); } TEST(NetworkPolicy, NoTransitionHasNoMqttSideEffects) { @@ -69,6 +69,12 @@ TEST(NetworkPolicy, FailbackRequiresStableEthernet) { EXPECT_EQ(NetworkMedium::Ethernet, NetworkPolicy::automaticSelection(input)); } +TEST(NetworkPolicy, DeadWifiFailsBackToReadyEthernetImmediately) { + const NetworkPolicy::AutomaticSelectionInput input = { + NetworkMedium::WiFi, true, false, true, false, 0, 0}; + EXPECT_EQ(NetworkMedium::Ethernet, NetworkPolicy::automaticSelection(input)); +} + TEST(NetworkPolicy, SwitchingLockPinsTheCurrentMedium) { const NetworkPolicy::AutomaticSelectionInput input = { NetworkMedium::WiFi, true, true, true, true,