From ef321b37efc7bc4b151c2f9e55431a707581510d Mon Sep 17 00:00:00 2001 From: Michael Sauer Date: Fri, 2 Oct 2026 19:32:19 +0200 Subject: [PATCH] fix(wifi): Zugriffsschutz und AP-Daten beim Verlassen des Standalone-Modus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C1 (Merge-Blocker): Schaltet jemand den Standalone-Modus ab, waehrend keine WLAN-Daten gespeichert sind, geht die Zustandsmaschine in den Onboarding-AP - nl_wifi_sm_open_access() liefert dann true, obwohl der AP noch mit Name und Passwort des Nutzers steht. Wer das AP-Passwort kennt, koennte system/settings und system/command ohne Token benutzen: Token setzen, OTA anstossen, Werksreset. Der Netz-Task reisst den AP in diesem Fall jetzt ausdruecklich ab und baut ihn mit den Vorgabedaten neu auf - vor execute(ev) und nicht ueber die Aktionsstruktur, weil execute() start_ap vor stop_ap ausfuehrt. Damit stimmt auch die README-Zusage wieder, der AP heisse danach "ninlight-". I1: Der zweite Rueckfall nach drei abgelehnten tls_wifi_softap_create() setzte ueber reset_ap_defaults() auch das Passwort auf das einkompilierte, das in der README oeffentlich dokumentiert ist - eine Standalone-Lampe stuende dann mit einem allgemein bekannten Passwort im Funkfeld, und sACN ist unauthentifiziert. reset_ap_defaults() ist dafuer in reset_ap_identity() (Name und Kanal) und die vollstaendige Rueckstellung aufgeteilt; der Rueckfall nimmt nur noch die Identitaet. Kanal und SSID sind ohnehin die plausiblen Gruende, die Passwortlaenge prueft die API selbst. I2: destroy_ap() liess s_ap_live_ssid/-pass/-channel stehen. Nach dem Verlassen des Standalone-Modus trugen diese Puffer weiter die Daten des Nutzers, und der erste Rueckfallzweig holte sie Minuten spaeter zurueck - die Lampe pendelte dann im 15-s-Takt zwischen Nutzerdaten und Vorgaben. Neu: forget_ap_live() in beiden Abschaltwegen (clear_standalone und enabled:false), nicht in destroy_ap() selbst, weil der Rueckfall auf zuletzt tragende Daten sonst auch im Neuaufbau-Fall verlorenginge, fuer den er gedacht ist. I3: s_ap_create_fails wurde nur bei Erfolg genullt und summierte damit ueber verschiedene Datensaetze - nach einer Korrektur von Kanal 13 auf 6 loeste schon ein einziger Fehlschlag den Rueckfall aus. Jetzt auch in destroy_ap() und bei jedem neuen Wunsch aus MSG_STANDALONE. Kleinigkeiten: nl_app_clear_standalone() wird auf seinen Rueckgabewert geprueft und ein Schreibfehler protokolliert (sonst steht die Lampe im WLAN und nach dem Neustart wieder im eigenen AP, ohne jede Spur). enabled:false auf einer Lampe, die gar nicht standalone war, erzwingt keine Neuverbindung mehr, sondern tut nichts und antwortet weiter mit 202. Abnahmeliste: der Gerätebeleg für C1 als offener Punkt. --- docs/hardware-checklist.md | 1 + src/net/nl_wifi_w600.c | 94 +++++++++++++++++++++++++++++++------- 2 files changed, 79 insertions(+), 16 deletions(-) diff --git a/docs/hardware-checklist.md b/docs/hardware-checklist.md index 99701ef..589bbdd 100644 --- a/docs/hardware-checklist.md +++ b/docs/hardware-checklist.md @@ -75,6 +75,7 @@ Für den echten Flottenbau `NINLIGHT_RECOVERY_PASSWORD` und `NINLIGHT_TOKEN` in - [x] REST über den AP antwortet auf `192.168.4.1:3333` — 2026-10-02 - [ ] Startpuls als Sichtprüfung: läuft beim Start rund 5 s und gibt danach das normale Einschaltverhalten frei; mit `pulse: false` entfällt er ganz — *Logik ist hostgetestet, am Gerät hat noch niemand hingesehen* - [ ] REST über den AP verlangt das Token (`GET system/settings` ohne Token → 401) +- [ ] `{"enabled":false}` **ohne** gespeicherte WLAN-Daten → der AP heißt danach wieder `ninlight-` (nicht mehr wie vom Nutzer gesetzt), und der Tokenschutz passt zur AP-Umschaltung: solange der Nutzer-AP steht, gilt das Token; sobald der Vorgabe-AP steht, ist `system/settings` wie im Onboarding offen. Gerätebeleg für C1 — bis dahin nur hostseitig durchdacht - [ ] WLAN eingeben, falsches Passwort → Gerät bleibt im eigenen AP mit eigenen Daten - [ ] WLAN eingeben, richtiges Passwort → Gerät im Heimnetz, nach einem Neustart **nicht** wieder im AP - [ ] Werksreset über 5× ein/aus → wieder unkonfigurierter Onboarding-AP diff --git a/src/net/nl_wifi_w600.c b/src/net/nl_wifi_w600.c index e1af294..9790321 100644 --- a/src/net/nl_wifi_w600.c +++ b/src/net/nl_wifi_w600.c @@ -115,16 +115,35 @@ void nl_wifi_disable_sdk_auto_connect(void) tls_wifi_disconnect(); } -/* Setzt Name, Passwort und Kanal des naechsten AP auf die Vorgaben zurueck: "ninlight-" - * mit dem einkompilierten Passwort auf Kanal 6. Beruehrt einen LAUFENDEN AP nicht - der - * behaelt seine Daten, bis er abgebaut und neu aufgebaut wird. */ -static void reset_ap_defaults(void) +/* Setzt NUR Name und Kanal des naechsten AP auf die Vorgaben zurueck: "ninlight-" auf + * Kanal 6. Das Passwort bleibt stehen - I1: ein Rueckfall darf das Passwort des Nutzers nicht + * durch das einkompilierte ersetzen, das in der README oeffentlich dokumentiert ist. Beruehrt + * einen LAUFENDEN AP nicht - der behaelt seine Daten, bis er abgebaut und neu aufgebaut wird. */ +static void reset_ap_identity(void) { s_ap_ssid[0] = '\0'; - copy(s_ap_pass, NINLIGHT_AP_PASSWORD, sizeof s_ap_pass); s_ap_channel = 6; } +/* I2: Vergisst, mit welchen Daten der zuletzt stehende AP aufgebaut wurde. Verlaesst die Lampe + * den Standalone-Modus, duerfen Name und Passwort des Nutzers nicht als "hat schon einmal + * getragen" liegenbleiben: der erste Rueckfallzweig in start_ap() holte sie sonst Minuten + * spaeter zurueck (etwa am Rueckfall-AP nach fuenf Minuten ohne Router), und die Lampe boete + * periodisch einen AP an, den der Nutzer laengst abgeschaltet hat. */ +static void forget_ap_live(void) +{ + s_ap_live_ssid[0] = s_ap_live_pass[0] = '\0'; + s_ap_live_channel = 0; +} + +/* Vollstaendige Rueckstellung auf die Vorgaben, einschliesslich des einkompilierten Passworts. + * Nur fuer den Weg AUS dem Standalone-Modus heraus, wo der Wunsch des Nutzers endet. */ +static void reset_ap_defaults(void) +{ + reset_ap_identity(); + copy(s_ap_pass, NINLIGHT_AP_PASSWORD, sizeof s_ap_pass); +} + /* Reisst den AP ab und raeumt dabei auch die Buchfuehrung auf. Laeuft nur im Netz-Task * (oder vor dessen Start) und nie unter s_lock - clear_softap_param() fasst SDK-Zustand an. */ static void destroy_ap(void) @@ -133,6 +152,10 @@ static void destroy_ap(void) s_ap_create_pending = false; s_ap_live = false; s_ap_event_seen = s_ap_mdns_done = false; + /* I3: Der Zaehler gehoert zu EINEM Datensatz. Ohne diese Nullung addieren sich die + * Fehlschlaege mit den alten Daten zu denen mit den neuen, und nach einer Korrektur + * (Kanal 13 -> Kanal 6) loeste schon ein einziger Fehlschlag den Rueckfall aus. */ + s_ap_create_fails = 0; clear_softap_param(); /* C2: every softap teardown clears the persisted SOFTAP bit */ } @@ -200,9 +223,14 @@ static void start_ap(void) copy(s_ap_ssid, s_ap_live_ssid, sizeof s_ap_ssid); copy(s_ap_pass, s_ap_live_pass, sizeof s_ap_pass); s_ap_channel = s_ap_live_channel; - } else if (s_ap_ssid[0] || s_ap_channel != 6 || strcmp(s_ap_pass, NINLIGHT_AP_PASSWORD) != 0) { + } else if (s_ap_ssid[0] || s_ap_channel != 6) { + /* I1: nur Name und Kanal. Das Passwort ist der unwahrscheinlichste Grund fuer ein + * abgelehntes tls_wifi_softap_create() - die Laenge prueft die API selbst, Kanal und + * SSID sind die plausiblen Kandidaten. Ein Rueckfall auf NINLIGHT_AP_PASSWORD stellte + * eine Standalone-Lampe mit einem oeffentlich dokumentierten Passwort ins Funkfeld, + * und sACN hoert dort unauthentifiziert mit. */ printf("wifi: AP create keeps failing, falling back to %s on ch 6\r\n", s_hostname); - reset_ap_defaults(); + reset_ap_identity(); } /* Sonst scheitern schon die Vorgaben - dann bleibt nur, es weiter zu versuchen. */ } @@ -248,11 +276,17 @@ static void execute(nl_wifi_actions_t a) printf("wifi: standalone mode cleared\r\n"); /* Dieselbe Rueckstellung wie beim Zurueckschalten ueber die API: sonst kaeme ein * spaeterer Rueckfall-AP (5 min ohne Station) mit Namen und Passwort des Nutzers - * hoch, obwohl standalone in den Einstellungen schon 0 ist. Gefahrlos, weil dieses - * Aktionsbuendel kein start_ap enthaelt - der noch laufende Standalone-AP bleibt - * unberuehrt, bis die Zustandsmaschine ihn abbaut. */ + * hoch, obwohl standalone in den Einstellungen schon 0 ist. forget_ap_live() raeumt + * dazu die Buchfuehrung ueber den zuletzt tragenden AP weg (I2) - sonst holte ihn + * der erste Rueckfallzweig in start_ap() nach drei abgelehnten Aufbauten zurueck. + * Der noch laufende Standalone-AP bleibt stehen, bis die Zustandsmaschine ihn + * abbaut: dieses Aktionsbuendel enthaelt kein start_ap. */ reset_ap_defaults(); - nl_app_clear_standalone(); + forget_ap_live(); + /* Schlaegt das Speichern fehl, steht die Lampe jetzt im WLAN, nach dem naechsten + * Neustart aber wieder in ihrem eigenen AP - ohne jede Spur. Wenigstens melden. */ + if (nl_app_clear_standalone() != 0) + printf("wifi: could not persist the cleared standalone flag\r\n"); } if (a.pulse_on) pulse(true); @@ -289,8 +323,8 @@ static void net_task(void *arg) nl_wifi_actions_t ev, tick; uint32_t m = 0, now; bool got_ip_event = false, connected_now, ap_wanted, unexpected_softap, - standalone_now; - bool recheck_ap = false; + standalone_now, open_now; + bool recheck_ap = false, rebuild_ap = false, ignored_off = false; memset(&ev, 0, sizeof ev); if (tls_os_queue_receive(s_queue, &msg, 0, HZ / 10) == TLS_OS_SUCCESS) @@ -319,15 +353,25 @@ static void net_task(void *arg) /* Steht der AP schon, liefert die Zustandsmaschine kein start_ap. Ob die * neuen Daten einen Neuaufbau verlangen, entscheidet start_ap() selbst. */ recheck_ap = true; + } else if (!s_sm.standalone) { + /* War die Lampe gar nicht standalone, gibt es nichts abzuschalten. Ohne diese + * Vorpruefung riefe leave_standalone() auf einer gewoehnlichen Stationslampe + * begin_connecting() und erzwaenge eine Neuverbindung ohne Anlass. Die Antwort + * bleibt 202: der gewuenschte Zustand gilt ja bereits. (standalone bleibt auch + * waehrend eines Onboarding-Tests gesetzt, der Test faellt also nicht hierher.) */ + ignored_off = true; } else { /* Zurueck auf die Vorgaben: ein spaeterer Rueckfall- oder Onboarding-AP ist * wieder der gewohnte "ninlight-" mit dem einkompilierten Passwort, - * genau wie bei einer Lampe, die nie standalone war. Das greift erst beim - * NAECHSTEN Aufbau - der noch laufende AP behaelt Name und Passwort, bis ihn - * die Zustandsmaschine abbaut. */ + * genau wie bei einer Lampe, die nie standalone war. */ reset_ap_defaults(); + forget_ap_live(); ev = nl_wifi_sm_leave_standalone(&s_sm, s_ssid[0] != '\0', now); + rebuild_ap = true; } + /* I3: Ein neuer Wunsch ist ein neuer Datensatz - die Fehlschlaege des alten + * duerfen nicht mitzaehlen. */ + s_ap_create_fails = 0; s_req_pending = false; } tick = nl_wifi_sm_tick(&s_sm, now); @@ -342,6 +386,7 @@ static void net_task(void *arg) unexpected_softap = m == (MSG_NETIF | NETIF_WIFI_SOFTAP_SUCCESS) && !ap_wanted; /* Nur die Lage merken; der Aufruf nach aussen folgt hinter der Entsperrung. */ standalone_now = s_sm.state == NL_WIFI_ST_STANDALONE; + open_now = nl_wifi_sm_open_access(&s_sm); unlock(); if (unexpected_softap) { @@ -351,6 +396,23 @@ static void net_task(void *arg) if (m & MSG_NETIF) printf("wifi: netif event %u\r\n", (unsigned)(m & 0xFFu)); + if (ignored_off) + printf("wifi: standalone was already off, nothing to do\r\n"); + /* C1: Beim Verlassen des Standalone-Modus OHNE gespeicherte WLAN-Daten geht die + * Zustandsmaschine in den Onboarding-AP - dort liefert open_access() true, waehrend + * der AP noch unveraendert mit Name und Passwort des Nutzers steht. Wer das + * AP-Passwort kennt, koennte dann ohne Token system/settings und system/command + * benutzen. Also hier hart abreissen und mit den Vorgabedaten neu aufbauen; damit + * stimmt auch die README-Zusage wieder, der AP heisse danach "ninlight-". + * Nicht ueber die Aktionsstruktur: execute() fuehrt start_ap VOR stop_ap aus, ein + * Buendel mit beiden Flags baute erst auf und risse dann ab. Und VOR execute(ev), + * damit dessen start_ap in die Gleichheitspruefung von start_ap() faellt, statt den + * eben gebauten AP ein zweites Mal abzureissen. */ + if (rebuild_ap && ap_wanted && open_now) { + printf("wifi: left standalone without credentials, rebuilding the AP with the defaults\r\n"); + destroy_ap(); + start_ap(); + } execute(ev); /* Nach einem Wechsel in den Standalone-Modus: start_ap() baut den AP neu auf, wenn * Name, Passwort oder Kanal sich geaendert haben, und tut sonst nichts. */