fix(wifi): Zugriffsschutz und AP-Daten beim Verlassen des Standalone-Modus

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-<mac4>".

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.
This commit is contained in:
Michael Sauer
2026-10-02 19:32:19 +02:00
parent 813d861cd1
commit ef321b37ef
2 changed files with 79 additions and 16 deletions
+1
View File
@@ -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-<mac4>` (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
+78 -16
View File
@@ -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-<mac4>"
* 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-<mac4>" 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-<mac4>" 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-<mac4>".
* 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. */