Wie man die Windows-Sitzungsisolierung versteht — Session 0, RDP und die gleichzeitige Nutzung durch mehrere Benutzer
· Go Komura · Session 0, Sitzungsisolierung, RDP, Remotedesktop, Windows-Dienste, Mehrbenutzerbetrieb, Windows, .NET, C#, Architektur, Technische Beratung
„Ein von einem Dienst ausgelöstes Dialogfeld ist für niemanden sichtbar.“ „In dem Moment, in dem sich jemand per RDP verbunden hat, sprang der Bildschirm an der physischen Maschine auf den Anmeldebildschirm um.“ „Der Einzelinstanzschutz sollte doppelte Starts verhindern, aber die Anmeldung als anderer Benutzer hat munter eine zweite Kopie gestartet.“ In Beratungseinsätzen zu Unternehmensanwendungen tauchen diese Symptome immer wieder auf, und hinter allen steckt dieselbe Lücke: ein unvollständiges Verständnis des Konzepts der Windows-„Sitzung“ (Session). Sitzungen durchziehen Dienstdesign, Remotedesktop-Betrieb und den Einzelinstanzschutz benannter Objekte gleichermaßen, dennoch ist es selten, dass dieses Thema an einer Stelle systematisch erklärt wird.
Dieser Artikel ordnet aus praktischer Sicht ein, warum die Session-0-Isolierung eingeführt wurde, wie sich Sitzungen bei einer RDP-Verbindung verhalten, wie benannte Objekte sitzungsweise isoliert werden, sowie die typischen Designfehler, die in Umgebungen mit gemeinsam genutzten PCs und RDS (Remotedesktopdienste) häufig auftreten.
1. Zuerst das Wichtigste
- Dienste laufen in einer dedizierten Sitzung namens Session 0, isoliert von der interaktiven Sitzung jedes angemeldeten Benutzers. Ein Dienst kann kein Dialogfeld direkt anzeigen, und selbst wenn er es versucht, bekommt der Benutzer es nie zu sehen.1
- Die offiziell empfohlene Lösung für einen Dienst, der eine Benutzeroberfläche benötigt, besteht darin, die UI in einen separaten Prozess (der in der Sitzung des Benutzers läuft) auszulagern und über IPC mit ihm zu kommunizieren. Das Starten eines UI-Programms als interaktiver Benutzer über die Aufgabenplanung ist eine weitere verfügbare Option.2
- Windows Server (RDS) ist rund um mehrere gleichzeitige Sitzungen aufgebaut, während Client-Betriebssysteme wie Windows 10/11 Pro grundsätzlich einsitzungsfähig sind. Abgesehen von der speziellen, für Azure Virtual Desktop konzipierten Edition „Enterprise Multisession“ sind gewöhnliche Client-Installationen nicht dafür ausgelegt, mehrere gleichzeitige interaktive Sitzungen zu hosten.3
- Benannte Kernelobjekte (Mutexe, Ereignisse, Semaphoren usw.) besitzen jeweils einen sitzungsbezogenen Namensraum. Wenn Sie ein Objekt auf eine einzige Instanz über die gesamte Maschine und über Sitzungen hinweg reduzieren möchten, müssen Sie den Namen explizit mit
Global\versehen — andernfalls verhält sich der Einzelinstanzschutz nicht wie erwartet. Benannte Pipes stehen außerhalb dieses Mechanismus: Standardmäßig verwenden sie einen separaten Namensraum, der maschinenweit (und remote, falls der Serverdienst läuft) erreichbar ist; wenn nach Sitzung oder Benutzer unterschieden werden muss, muss etwas wie die Sitzungs-ID in den Pipe-Namen selbst eingebettet werden.45 - Den Typ der aktuellen Sitzung können Sie über die Umgebungsvariable
SESSIONNAMEoderGetSystemMetrics(SM_REMOTESESSION)ermitteln. Es gibt jedoch Fälle — etwa bei Verwendung von RemoteFX vGPU —, in denen die Erkennung vom beabsichtigten Verhalten abweicht; verlassen Sie sich also nicht blind darauf.67 - Ob das Verhalten bei gemeinsam genutzten PCs/RDS in Ihre Anforderungen aufgenommen werden soll, ist etwas, das ganz zu Beginn der Entwurfsphase zu klären ist. Kollisionen bei Pfaden temporärer Dateien, durcheinandergebrachte AppData sowie gleichzeitiger Zugriff auf Geräte/Dongles sind die klassischen Störfälle, die genau in dem Moment ausbrechen, in dem eine für einen einzelnen eigenständigen PC gebaute Anwendung auf RDS bereitgestellt wird (siehe Kapitel 7).
2. Was Session-0-Isolierung ist — und warum die Trennung von Benutzersitzungen erfolgte
Vor Windows Vista / Windows Server 2008 teilten sich ein Dienst und der erste angemeldete Benutzer dieselbe Session 0. Prozesse innerhalb derselben Sitzung können unabhängig von ihrem jeweiligen Berechtigungsniveau Fenstermeldungen austauschen. Ein Prozess mit Standardrechten konnte eine präparierte Meldung an ein Fenster senden, das einem als SYSTEM laufenden Dienst gehörte, dessen Meldungsverarbeitung kapern und seine Rechte eskalieren — diese gesamte Klasse der Rechteeskalation, die Schwachstellen in der Fenstermeldungsverarbeitung ausnutzt, wurde in den 2000er-Jahren wiederholt gemeldet, und tatsächlich wurden im Lauf der Jahre mehrere Sicherheitslücken zur Rechteeskalation behoben, die Schwachstellen in der Fenstermeldungsverarbeitung des Windows-Kernels (win32k.sys) ausnutzten.8
Dieses Bild änderte sich ab Windows Vista. Session 0 wurde ausschließlich Diensten und Anwendungen vorbehalten, die nicht an eine interaktive Benutzersitzung gebunden sind, und der erste angemeldete Benutzer erhält nun Session 1, der nächste Session 2 und so weiter — jeweils in einer anderen Sitzung als der Dienst. Session 0 unterstützt keinen interaktiven Betrieb, und Dienste können weder Meldungen an Apps senden noch Apps an Dienste. Ein Dienst kann auch keine UI-Elemente wie ein Dialogfeld direkt anzeigen. Mit dieser Änderung wurde genau der Weg, über den ein Prozess mit Standardrechten Meldungen direkt in das Fenster eines Dienstes senden konnte, strukturell geschlossen.1
Mit anderen Worten: Die Session-0-Isolierung ist nicht bloß eine betriebliche Einschränkung nach dem Motto „Dienste laufen im Hintergrund, deshalb haben sie keinen Bildschirm“ — sie ist eine Sicherheitsentscheidung, die den Austausch von Fenstermeldungen über eine Privilegiengrenze hinweg strukturell blockiert. Der einzige Weg, der einem Dienst bleibt, um überhaupt eine Benutzeroberfläche zu zeigen, ist die Funktion WTSSendMessage, eine API, die in einer anderen Sitzung ein einfaches Meldungsfeld einblendet, sich aber nicht für komplexe Dialogfelder oder eine bidirektionale Interaktion eignet.1
3. Dienste können keine Benutzeroberfläche anzeigen — praktische Einschränkungen und Umgehungsmuster
Die praktische Konsequenz der Session-0-Isolierung ist einfach. Wenn ein Dienst MessageBox.Show aufruft oder versucht, ein Formular anzuzeigen, erscheint auf dem Bildschirm des angemeldeten Benutzers nichts. Schlimmer noch: Das ist eine klassische Ursache dafür, dass der gesamte Prozess hängen bleibt, während er auf unbestimmte Zeit auf einen OK-Button wartet, den niemand jemals anklicken kann. Beim Portieren von altem Code (etwas, das zu Windows-XP-Zeiten einmal als „interaktiver Dienst“ lief) in einen Dienst sollte immer geprüft werden, ob noch UI-Anzeigeaufrufe übriggeblieben sind.
Es gibt zwei offiziell dokumentierte Lösungen.2
- Die UI in einen separaten Prozess (der in der Sitzung des interaktiven Benutzers läuft) auslagern und über IPC kommunizieren. Der Dienst erledigt die Arbeit und gibt Ergebnisse oder Eingabeanfragen an den UI-Prozess weiter. Der UI-Prozess präsentiert sie dem Benutzer als Symbol im Benachrichtigungsbereich oder als Dialogfeld und gibt das Ergebnis der Operation an den Dienst zurück. Bei Verwendung von IPC wie benannten Pipes müssen die ACLs so entworfen werden, dass nichts unbeabsichtigt über das Netzwerk offengelegt wird. In Umgebungen, in denen mehrere Benutzer nebeneinander angemeldet sind, lautet die Empfehlung, Sitzungen zu unterscheiden, etwa indem die Sitzungs-ID in den Pipe-Namen eingebettet wird.2 Zur Wahl des IPC-Mechanismus siehe den am selben Tag veröffentlichten begleitenden Artikel „Eine Entscheidungstabelle für die Windows-Interprozesskommunikation“. Wie man den Dienst selbst baut, sowie betriebliche Entwurfsaspekte wie Starttyp, Ausführungskonto und Wiederherstellungsoptionen, werden in „So baut man einen Windows-Dienst“ behandelt.
- Ein Programm über die Aufgabenplanung als interaktiver Benutzer starten. Statt es dauerhaft als Dienst laufen zu lassen, startet diese Konfiguration ein Programm mit Benutzeroberfläche unter den eigenen Rechten und in der Sitzung des angemeldeten Benutzers. Ein Punkt, auf den hier zu achten ist: Eine „unter SYSTEM registrierte Aufgabe“ darf niemals diejenige sein, die die Benutzeroberfläche anzeigt. Das SYSTEM-Konto besitzt keine interaktiven Anmelderechte, und ein Benutzer kann ein unter SYSTEM-Rechten laufendes Programm oder eine solche Aufgabe weder sehen noch mit ihr interagieren.9 Das Modell erhöhter Aufgaben ist so gedacht, dass eine SYSTEM-Aufgabe — deren Sicherheitsdeskriptor es einem Standardbenutzer erlaubt, sie zu starten — nur die Operationen übernimmt, die Administratorrechte erfordern, während die Benutzeroberfläche selbst in einem separaten Programm unter den eigenen Rechten und in der Sitzung des angemeldeten Benutzers läuft (oder als dieselbe, unter dem Konto des Benutzers als Aufgabe registrierte Operation); das ist die gesamte Grundlage dieser Rollentrennung.10 Halten Sie die SYSTEM-Aufgabe strikt als „kopflosen Vermittler“ und lassen Sie die sichtbare Benutzeroberfläche immer in einem Prozess in der eigenen Sitzung des Benutzers laufen.
Unabhängig vom gewählten Ansatz ist der springende Punkt, von Anfang an im Entwurf zu verankern, dass „der Dienst hinter den Kulissen bleibt, die UI in einem separaten Prozess lebt“. Was man in der Praxis regelmäßig beobachtet: Der Versuch, dies nachträglich einzubauen, stößt auf bereits tief in den Dienst eingewachsenen UI-abhängigen Code, was die Kosten der Trennung in die Höhe treibt.
4. Wie sich Sitzungen über RDP verhalten — der Unterschied zwischen RDS und einem Client-Betriebssystem
Die Verwirrung „es ist derselbe RDP auf Server und Client, warum verhält es sich dann unterschiedlich?“ rührt daher, ob die Rolle Remotedesktopdienste (RDS) vorhanden ist oder nicht.
Windows Server mit der RDS-Rolle ist darauf ausgelegt, dass sich mehrere Benutzer gleichzeitig an einem einzigen Server anmelden, jeder in einer eigenständigen Sitzung mit eigenem Desktop und eigenen Apps arbeitet — eine Konfiguration, die von Grund auf auf mehreren gleichzeitigen Sitzungen aufbaut.11 Unter RDS unterstützen gewöhnliche administrative RDP-Verbindungen (für Administratoren) bis zu zwei Sitzungen ohne CAL, aber darüber hinauszugehen oder gewöhnlichen Benutzern eine gleichzeitige Verbindung zu erlauben, erfordert die Bereitstellung der Rolle RD-Sitzungshost zusammen mit den passenden RDS-CALs (Clientzugriffslizenzen).12 CALs gibt es in zwei Modellen — pro Gerät und pro Benutzer —, und die Lizenzdetails müssen für jede Umgebung gesondert geklärt werden.13
Ein gewöhnliches Client-Betriebssystem wie Windows 10/11 Pro oder Enterprise hingegen trägt die RDS-Rolle nicht und ist grundsätzlich einsitzungsfähig. Es gibt eine spezielle Client-Edition namens „Windows Enterprise Multisession“, die mehrere interaktive Sitzungen gleichzeitig halten kann, aber sie wird ausschließlich für Azure Virtual Desktop angeboten — gewöhnliche, lokal verteilte Client-Installationen sind nicht dafür ausgelegt, mehrere gleichzeitige Benutzersitzungen zu unterstützen.3 In der Praxis ist es, passend zur vertrauten Erfahrung „ich habe mich per RDP mit dem PC eines Kollegen verbunden, und der Bildschirm an dessen Schreibtisch sprang auf den Sperrbildschirm um“, am sichersten anzunehmen, dass auf Client-Betriebssystemen eine physische Konsolensitzung (jemand, der die Maschine lokal bedient) und eine Remoteverbindung im Prinzip nicht gleichzeitig koexistieren können.
Hier eine Zusammenfassung der Unterschiede, die für den Entwurf von Unternehmensanwendungen wichtig sind.
| Aspekt | Windows Server + RDS | Client-Betriebssystem (Windows 10/11 Pro usw.) |
|---|---|---|
| Gleichzeitige Sitzungen | Mehrere Benutzer können sich gleichzeitig anmelden11 | Grundsätzlich einsitzungsfähig. Mehrere interaktive Sitzungen werden außerhalb der ausschließlich für AVD bestimmten Multisession-Edition nicht unterstützt3 |
| Lizenzierung | Erfordert RD-Sitzungshost + RDS-CALs (über die zwei administrativen Verbindungen hinaus)12 | Die Remotedesktop-Funktion selbst ist im Betriebssystem enthalten, aber Lizenzanforderungen und zulässige Editionen müssen separat geklärt werden |
| Typischer Einsatz | Betrieb von Unternehmensanwendungen auf gemeinsam genutzten Terminals, Thin-Client-Umgebungen | Remote-Wartung eines persönlichen PCs, Zugriff im Homeoffice |
Diesen Unterschied nicht zu berücksichtigen — mit der Annahme „es hat auf dem Client-Betriebssystem meiner Entwicklungsmaschine funktioniert, also sollte es genauso in der RDS-Umgebung des gemeinsam genutzten Servers funktionieren“ — führt dazu, dass man die in Kapitel 7 behandelten, für Mehrbenutzer-Nebenläufigkeit spezifischen Fehler übersieht. Umgekehrt: Wenn man auf einem Client-Betriebssystem nur einen Einzelsitzungsbetrieb erwartet, muss man die Entwurfskosten für die Unterstützung gemeinsam genutzter PCs nicht von vornherein tragen. Diese Entscheidung sollte früh in der Anforderungsdefinition getroffen werden (Kapitel 8).
5. Die aktuelle Sitzung bestimmen
Es gibt viele Situationen, in denen man wissen möchte, in welcher Sitzung der eigene Prozess gerade läuft — etwa um einen aufwendigen Rendering-Effekt während einer Hintergrundverarbeitung zu unterdrücken oder eine bandbreitenintensive Funktion in einer Remotesitzung zu deaktivieren. Es gibt mehrere Möglichkeiten, dies zu bestimmen.
- Die Umgebungsvariable
SESSIONNAME: für die Konsolensitzung aufConsolegesetzt, oder für eine RDP-Verbindung auf einen mitRDP-Tcp#beginnenden Namen. Dies ist dieselbe Information, die in der vom Befehlqwinstaaufgelisteten SpalteSESSIONNAMEangezeigt wird.14 Für eine schnelle Prüfung nutzbar, aber da es keine offizielle API ist, sollte man sich nicht zu sehr darauf verlassen. GetSystemMetrics(SM_REMOTESESSION): eine Win32-API, die angibt, ob man sich in einer Remotesitzung befindet. Sie liefert einen von null verschiedenen Wert, wenn der aufrufende Prozess mit einer Terminaldienste-Clientsitzung verknüpft ist. Es gibt jedoch einen Vorbehalt: Ab Windows 8/Server 2012 meldet diese Funktion bei Remotesitzungen mit RemoteFX vGPU fälschlich eine Remotesitzung als lokal. In diesem Fall besteht die dokumentierte Umgehung darin, denGlassSessionId-Wert der Registrierung mit der aktuellen Sitzungs-ID zu vergleichen.67SystemParameters.IsRemoteSessionin .NET (WPF): stellt dieselbe Prüfung, dieGetSystemMetrics(SM_REMOTESESSION)durchführt, als Eigenschaft bereit, die man aus einer WPF-App lesen kann.15 Aus WinForms oder einer Konsolenanwendung müsste man sie direkt über P/Invoke aufrufen.WTSGetActiveConsoleSessionId: eine API, die die mit der physischen Konsole verbundene Sitzungs-ID liefert. Der Vergleich mit der eigenen Sitzungs-ID erlaubt die Feststellung: „Laufe ich auf der physischen Konsole?“ Beachten Sie, dass0xFFFFFFFFzurückgegeben wird, wenn niemand mit der physischen Konsole verbunden ist (also während eines Übergangs).16
Verwenden Sie WTSGetActiveConsoleSessionId nicht, um Sitzungen in einer RDS-Umgebung zu identifizieren. Getreu ihrem Namen liefert sie nur „die mit der physischen Konsole verbundene Sitzung“ — ein per RDP angemeldeter Benutzer wird nicht erfasst.16 Wenn ein Dienst wissen muss, in welcher Sitzung er seine begleitende UI anzeigen soll, reicht diese API aus, sofern man nur einen konsolenverbundenen Benutzer erwartet; in einer RDS-Umgebung jedoch, in der mehrere RDP-Benutzer gleichzeitig angemeldet sein können, sollte man stattdessen die laufenden Sitzungen mit WTSEnumerateSessions aufzählen17 oder einfach die von einer Sitzungswechsel-Benachrichtigung (WM_WTSSESSION_CHANGE) übergebene Sitzungs-ID verwenden. Diese Verwechslung ist eine klassische, schwer zu findende Ursache für Fehler, bei denen die Benachrichtigungs-UI speziell bei per RDP verbundenen Benutzern schlicht nicht erscheint — oder in der falschen Sitzung erscheint. Behalten Sie diese Unterscheidung fest im Blick, wenn Sie entscheiden, in welcher Sitzung der UI-Prozess für die in Kapitel 3 beschriebene IPC-Konfiguration gestartet werden soll.
6. Sitzungsbezogene Isolierung benannter Objekte
Benannte Kernelobjekte — Mutexe, Ereignisse, Semaphoren, wartbare Timer, Dateizuordnungsobjekte — besitzen jeweils einen von der Sitzung unabhängigen, eigenen Namensraum. Selbst wenn Sie in einer Sitzung einen Mutex namens MyAppMutex erstellen, erzeugt der Versuch, aus einer anderen Sitzung ein Objekt mit demselben Namen zu öffnen, ein völlig neues, eigenständiges Objekt. Für Prozesse, die überwiegend in Session 0 laufen, wie etwa Dienste, oder für Client/Server-Konfigurationen, die ein Objekt über mehrere Sitzungen hinweg teilen möchten, können Sie es explizit im globalen Namensraum platzieren, indem Sie den Namen mit Global\ versehen. Umgekehrt platziert das Präfix Local\ es explizit im Namensraum der eigenen Sitzung.4
Hier zählt es in der Praxis wirklich: der Mutex, der für den Einzelinstanzschutz verwendet wird.
- Wenn Sie nur „verhindern möchten, dass derselbe Benutzer innerhalb derselben Sitzung zwei Kopien der App startet“, genügt der Standard-Mutex-Name (ohne Präfix). Der sitzungsbezogene Namensraum greift automatisch, sodass die Sitzung eines anderen Benutzers als ein anderer Mutex behandelt wird.
- Wenn Sie möchten, dass „selbst bei mehreren gleichzeitig angemeldeten Benutzern in einer RDS-Umgebung die App auf eine einzige Instanz maschinenweit beschränkt wird“, können Sie dieses Ziel nur durch explizite Verwendung von
Global\erreichen. Bei der Standardeinstellung kann am Ende jeder Benutzer seine eigene Kopie starten — ein Fehler, der sich als „der Einzelinstanzschutz sollte eigentlich funktionieren, aber es wurden trotzdem mehrere Instanzen gestartet“ äußert.
Beachten Sie außerdem, dass ein Prozess außerhalb der Session 0 das Recht SeCreateGlobalPrivilege benötigt, um ein neues Dateizuordnungsobjekt oder ein symbolisches Verknüpfungsobjekt im globalen Namensraum zu erstellen (das bloße Öffnen eines vorhandenen Objekts erfordert es nicht). Diese Rechteprüfung gilt nur für Dateizuordnungen und symbolische Verknüpfungen — sie wird der Erstellung von Mutexen oder Ereignissen nicht auferlegt —, ist aber für jedes Design mit gemeinsam genutztem Speicher (eine speicherabgebildete Datei) im globalen Namensraum wichtig zu merken.4
Es gibt außerdem ein allgemeines Prinzip für Client/Server-Anwendungen: Die Empfehlung lautet, „eine Verbindung von einer Maschine“ nicht mit „einer Benutzersitzung“ gleichzusetzen. Da von derselben Maschine aus gleichzeitig mehrere Sitzungen (RDP-Verbindungen mehrerer Benutzer) aufgebaut werden können, wird serverseitig empfohlen, Sitzungen etwa mit ProcessIdToSessionId zu identifizieren und pro Sitzung einen separaten Kommunikationskanal bereitzustellen.18
7. Häufige Designfehler auf gemeinsam genutzten PCs und in RDS-Umgebungen
Es gibt eine Handvoll wiederkehrender Fehlermuster, die genau in dem Moment ausbrechen, in dem eine App, die nur mit Einzel-PC-, ausschließlich Konsolen-Nutzung im Sinn gebaut wurde, auf einem gemeinsam genutzten PC oder in einer RDS-Umgebung bereitgestellt wird.
- Kollisionen bei Pfaden temporärer Dateien. Standardmäßig legt RDS pro Sitzung einen separaten Temp-Ordner an (unter dem Profil des Benutzers, mit der Sitzungs-ID im Namen).19 Mit anderen Worten: Wenn Ihre App einfach die Umgebungsvariable
%TEMP%verwendet, profitiert sie kostenlos von dieser Isolierung. Die Probleme beginnen, wenn die App statt%TEMP%einen festen Pfad wieC:\Work\tempfest codiert. Wenn mehrere Sitzungen desselben Benutzerkontos oder mehrere Benutzer gleichzeitig in denselben festen Pfad schreiben, kämpfen sie natürlich um die Datei. - Durcheinandergebrachte AppData. Wo Benutzereinstellungen, Caches und Protokolle gespeichert werden, muss mit einem soliden Verständnis des Windows-Benutzerprofilmechanismus entschieden werden — lokal gegenüber servergespeichert und wann
%LOCALAPPDATA%statt%APPDATA%zu verwenden ist. Auf einem gemeinsam genutzten PC äußert sich ein nachlässiges Design in diesem Punkt tendenziell als „mein Bildschirm hat sich wegen der Einstellungen eines anderen geändert“ oder „das Protokoll wurde überschrieben“. Details finden Sie in „Eine Einführung in Windows-Benutzerprofile“. - Die Annahme gleichzeitigen Zugriffs auf Geräte, Dongles und serielle Schnittstellen. Der Zugriff auf ein clientseitiges lokales Gerät oder eine serielle Schnittstelle aus einer RDS-Sitzung läuft über einen Umleitungsmechanismus. Wenn Sie versuchen, mehreren Sitzungen gleichzeitigen Zugriff auf eine App zu geben, die von einem physischen USB-Dongle oder einem seriellen Kommunikationsgerät abhängt, stoßen Sie auf Probleme, bei denen entweder die Hardware selbst gleichzeitigen Zugriff von vornherein nicht unterstützt oder die Sichtbarkeit von der Umleitungskonfiguration abhängt und vorhanden sein kann oder auch nicht. Wie ein clientseitig angeschlossenes Gerät aus einer serverseitigen Sitzung erscheint, hängt von der Umleitungskonfiguration ab, was daher in der Entwurfsphase bestätigt werden muss.20
- Fest codierte Drucker und Laufwerke. Einen fest codierten Standarddruckernamen oder Laufwerksbuchstaben in einer App zu verwenden, bricht leicht in einer Umgebung mit gemeinsam genutztem PC zusammen, in der der umgeleitete Standarddrucker oder das zugeordnete Laufwerk je nach Benutzer unterschiedlich ist. Sicherer ist es, den Druckernamen zur Laufzeit abzufragen und Laufwerke, wo möglich, als UNC-Pfade zu behandeln.
- Fehlinterpretation von HKCU / des benutzerbezogenen Registrierungshives. Es ist auch zu beachten, dass
HKEY_CURRENT_USERan den angemeldeten Benutzer gebunden ist, nicht an die Sitzung. In einer Konfiguration, in der derselbe Benutzer mehrere Sitzungen hat (etwa bei gemeinsamer Nutzung desselben Kontos auf RDS), werden HKCU-Werte über alle Sitzungen dieses Benutzers hinweg geteilt — das Speichern eines Laufzeitzustands, der eigentlich sitzungsweise isoliert sein soll, in HKCU führt daher unbeabsichtigt zu einem Übergreifen auf andere Sitzungen. Informationen, die eine sitzungsweise Isolierung erfordern, sollten stattdessen die sitzungsbezogenen Namensräume aus Kapitel 6 oder einen Dateipfad verwenden, der die Sitzungs-ID einbettet.
All dies sind Fehler der Art, die man „auf einem einzelnen eigenständigen PC nie bemerken würde“ und die „erst zutage treten, wenn die App tatsächlich von mehreren Benutzern gleichzeitig genutzt wird“ — der typische Verlauf ist daher, dass eine Testumgebung, die nur aus einer einzigen eigenständigen Entwicklungsmaschine besteht, das Problem nie reproduziert und es erstmals auf einem gemeinsam genutzten PC oder in einer produktiven RDS-Umgebung auftritt.
8. Entscheidungstabelle — sollte die Unterstützung von „gemeinsam genutzten PCs/RDS“ Teil Ihrer Anforderungen sein?
Ob das Verhalten bei gemeinsam genutztem PC/RDS Teil Ihrer Anforderungen sein soll, ist während der Anforderungsdefinition zu klären, bevor die Implementierung beginnt. Sobald Sie sich entschieden haben, es einzubeziehen, nutzen Sie die folgenden Punkte als Checkliste.
| Aspekt | In Ordnung, nur eigenständigen PC/Konsole anzunehmen | Erfordert Unterstützung für gemeinsam genutzte PCs/RDS (mehrere Benutzer gleichzeitig) |
|---|---|---|
| Temporäre Dateien / Einstellungen | Ein fester Pfad richtet wenig echten Schaden an | Pro-Benutzer-/pro-Sitzung-Pfade wie %TEMP% / %LOCALAPPDATA% vorschreiben (Kapitel 7) |
| Benannte Objekte | In Ordnung, den Standard (Sitzungsnamensraum) beizubehalten | Wenn „eine Instanz maschinenweit“ benötigt wird, explizit Global\ verwenden. Wenn „eine pro Sitzung“ ausreicht, den Standard beibehalten. Wenn „eine pro Benutzer, über dessen Sitzungen hinweg“ gewünscht ist, Global\ mit der SID des Benutzers kombinieren (Kapitel 6; siehe auch den Artikel zum Einzelinstanzschutz) |
| Geräte / Dongles | Einfache, direkte lokale Verbindung | Unter Berücksichtigung entwerfen, dass der Zugriff über Umleitung läuft und gleichzeitiger Zugriff möglicherweise nicht möglich ist |
| UI und Dienste | Leicht, alles auf einem Desktop zu behalten | In einen separaten Prozess + IPC aufteilen, im Licht der Session-0-Isolierung (Kapitel 3) |
| Lizenzierung / Abrechnung | Einfach nach Maschinenanzahl zu zählen | Das Konzept gleichzeitiger Sitzungsanzahl und die Notwendigkeit von RDS-CALs während der Anforderungsdefinition klären13 |
Die Entscheidungsachse ist einfach: Klären Sie früh in der Entwicklung mit dem Auftraggeber, ob „diese App nur eigenständig auf dem PC einer einzelnen Person laufen muss“ oder ob „sie möglicherweise von mehreren Personen gleichzeitig auf einem gemeinsam genutzten Terminal oder in einer RDS-Umgebung verwendet wird“. Lassen Sie diesen Punkt vage, während die Implementierung voranschreitet, und Sie werden am Ende jeden der obigen Punkte von Grund auf neu bauen müssen.
9. Implementierungsbeispiel — Sitzungserkennung und Namensräume sinnvoll einsetzen
Hier ist, wie die Erkennungsmethoden und Mutex-Namensentscheidungen aus den Kapiteln 5 und 6 im .NET-Code zusammenkommen.
using System.Runtime.InteropServices;
internal static class SessionInfo
{
[DllImport("user32.dll")]
private static extern int GetSystemMetrics(int nIndex);
private const int SM_REMOTESESSION = 0x1000;
// Für eine WPF-App liefert System.Windows.SystemParameters.IsRemoteSession dasselbe Ergebnis
public static bool IsRemoteSession() => GetSystemMetrics(SM_REMOTESESSION) != 0;
// Schnelle, grobe Prüfung. Keine offizielle API, daher nur für Protokollierung/
// Diagnose verwenden, nicht für wichtige Verzweigungen
public static string? SessionName() =>
Environment.GetEnvironmentVariable("SESSIONNAME");
}
Der für den Einzelinstanzschutz verwendete Mutex muss drei Fälle unterscheiden: „eine maschinenweit“, „eine pro Sitzung“ und „eine pro Benutzer, über dessen Sitzungen hinweg“. Der dritte Fall (pro Benutzer, sitzungsübergreifend) lässt sich nicht allein mit dem Standard-Global\ erreichen — der Name muss auch die SID des Benutzers enthalten. Das wird ausführlich in „Doppelte Starts in Windows-Anwendungen verhindern“ behandelt.
// Erfordert: using System.Security.AccessControl; und using System.Security.Principal;
// (erfordert außerdem das NuGet-Paket System.Threading.AccessControl)
// Absicht: auch bei mehreren gleichzeitig angemeldeten Benutzern auf RDS/einem
// gemeinsam genutzten PC genau eine Instanz maschinenweit beibehalten.
// .NETs Mutex/MutexAcl.Create fordert intern SYNCHRONIZE und MUTEX_MODIFY_STATE
// an, dazu DELETE/READ_CONTROL/WRITE_DAC/WRITE_OWNER, selbst nur um einen
// vorhandenen Mutex zu öffnen. Das bedeutet: Sofern „Authentifizierte Benutzer“
// nicht das Äquivalent von FullControl gewährt wird, erhält ein legitimer
// Benutzer, der nicht der Ersteller ist, im Moment der createdNew-Prüfung eine
// UnauthorizedAccessException.
// (Der Kompromiss: Dies lässt die ACL auch später von jedem authentifizierten
// Benutzer änderbar. Ist das inakzeptabel, verzichten Sie auf die Mutex-Klasse
// und rufen Sie CreateMutexEx direkt über P/Invoke auf, wobei nur die minimal
// erforderlichen Zugriffsrechte angefordert werden)
//
// Vorsicht: Diese ACL gilt nur, wenn Sie derjenige sind, dem es gelungen ist, den
// Mutex zuerst zu erstellen. Ein Global\-Mutex mit festem Namen läuft Gefahr, von
// einem anderen Benutzer vorab erstellt zu werden (Namens-Squatting); in diesem
// Fall liefert createdGlobal false zurück, oder Sie erhalten je nach ACL der
// anderen Partei eine UnauthorizedAccessException. Wenn Sie andere wirklich
// aussperren müssen, kombinieren Sie einen schwer zu erratenden Namen mit einem
// weiteren Ausschlussmechanismus (Details siehe Kapitel 5 des begleitenden
// Artikels)
var globalMutexSecurity = new MutexSecurity();
globalMutexSecurity.AddAccessRule(new MutexAccessRule(
new SecurityIdentifier(WellKnownSidType.AuthenticatedUserSid, domainSid: null),
MutexRights.FullControl,
AccessControlType.Allow));
using var globalMutex = MutexAcl.Create(
initiallyOwned: false,
name: @"Global\KomuraSoft.MyApp.SingleInstance",
createdNew: out bool createdGlobal,
mutexSecurity: globalMutexSecurity);
// Absicht: eine Instanz pro Sitzung genügt (mehrere Sitzungen desselben Benutzers
// können durchaus separat behandelt werden).
// Im Standard-Sitzungsnamensraum belassen, wie er ist
using var perSessionMutex = new Mutex(
initiallyOwned: false,
name: "KomuraSoft.MyApp.SingleInstance",
createdNew: out bool createdPerSession);
if (!createdGlobal)
{
// Läuft bereits an anderer Stelle auf der Maschine, einschließlich anderer Sitzungen
return;
}
Das Erstellen eines mit Global\ versehenen Mutex erfordert an sich kein besonderes Recht (wie zuvor erwähnt, gilt die Rechteprüfung nur für das Neuerstellen von Dateizuordnungsobjekten und symbolischen Verknüpfungen).4 Es ist ein Fehler, den Mutex im Standard-Sitzungsnamensraum zu belassen, wenn die Absicht „einer pro Benutzer“ ist. Es kommt nicht selten vor, dass derselbe Benutzer gleichzeitig eine getrennte RDP-Sitzung und eine neue Konsolen-/RDP-Sitzung hält, und in diesem Fall wird jede Sitzung als separater Mutex behandelt — belässt man es also beim Standard, kann derselbe Benutzer am Ende eine zweite Instanz starten. Welche der Optionen „eine maschinenweit“, „eine pro Sitzung“ oder „eine pro Benutzer sitzungsübergreifend“ die korrekte Anforderung ist, hängt von der Natur der App ab; klären Sie die Anforderung daher, bevor Sie sich auf ein Benennungsschema festlegen.
10. Zusammenfassung
Eine „Sitzung“ unter Windows ist ein Konzept, das immer wieder in auf den ersten Blick unzusammenhängend erscheinenden Themen auftaucht — Dienstdesign, Remotedesktop-Betrieb, Einzelinstanzschutz für benannte Objekte. Die zentrale Struktur lässt sich auf drei Punkte reduzieren. Ein Dienst läuft in Session 0, einer speziellen, isolierten Sitzung, und kann keine Benutzeroberfläche direkt anzeigen. RDS ist rund um mehrere gleichzeitige Sitzungen aufgebaut, während ein gewöhnliches Client-Betriebssystem grundsätzlich einsitzungsfähig ist. Und benannte Objekte besitzen einen sitzungsbezogenen Namensraum, weshalb je nach Absicht beurteilt werden muss, ob Global\ erforderlich ist.
Ob das Verhalten bei gemeinsam genutztem PC/RDS Teil Ihrer Anforderungen ist, ist ein Thema, bei dem ein spätes Bemerken während der Implementierung viel Nacharbeit verursacht. Wir empfehlen dringend, während der Anforderungsdefinition zu klären, „wer diese App nutzen wird, auf welchen Maschinen und mit wie vielen Personen gleichzeitig“. Wenn Sie planen, eine bestehende App auf einer Umgebung mit gemeinsam genutztem PC oder RDS bereitzustellen, oder einen Fehler untersuchen müssen, bei dem „Dinge schiefgehen, wenn mehrere Benutzer die App nutzen“, ist es in der Regel schneller, anhand der tatsächlichen Konfiguration zu diagnostizieren — kontaktieren Sie uns gerne.
Verwandte Artikel
- So baut man einen Windows-Dienst — ein Worker-Service-Leitfaden
- Eine Entscheidungstabelle für die Windows-Interprozesskommunikation
- Doppelte Starts in Windows-Anwendungen verhindern — benannte Mutexe und das Aktivieren einer vorhandenen Instanz
- Eine Einführung in Windows-Benutzerprofile — AppData und NTUSER.DAT
- Eine konkrete Methode, um „nur die Operationen, die Administratorrechte benötigen“ in einer Windows-App zu isolieren
Verwandte Beratungsbereiche
KomuraSoft LLC (合同会社小村ソフト) übernimmt Entwurfsprüfungen für Windows-Anwendungen im Hinblick auf eine mögliche Bereitstellung auf gemeinsam genutzten PCs oder in RDS-Umgebungen, das Design der Dienst-/IPC-Trennung sowie die Ursachenermittlung bei Fehlern, die für die gleichzeitige Nutzung durch mehrere Benutzer spezifisch sind.
- Technische Beratung / Entwurfsprüfung
- Entwicklung von Windows-Anwendungen
- Modernisierung und Wartung bestehender Windows-Software
- Kontakt
Referenzen
-
Microsoft Learn, Service Changes for Windows Vista. Zum Hintergrund der Einführung der Session-0-Isolierung, der daraus resultierenden Unfähigkeit von Diensten und Anwendungen, Fenstermeldungen auszutauschen, und der Funktion
WTSSendMessageals Alternative. ↩ ↩2 ↩3 -
Microsoft Learn, Interactive Services. Dazu, warum ein Dienst nicht direkt mit einem Benutzer interagieren kann, zum Entwurf mit
CreateProcessAsUserzum Starten eines separaten GUI-Prozesses und zur Koordination über IPC, sowie zur Empfehlung, Pipe-Namen nach Sitzungs-ID zu unterscheiden. ↩ ↩2 ↩3 -
Microsoft Learn, Windows Enterprise multi-session FAQ. Zur Multisession-Fähigkeit — dem gleichzeitigen Halten mehrerer interaktiver Sitzungen —, die zuvor nur unter Windows Server möglich war und nun ausschließlich für Azure Virtual Desktop angeboten wird. ↩ ↩2 ↩3
-
Microsoft Learn, Kernel object namespaces. Zum sitzungsbezogenen Namensraum benannter Kernelobjekte, zu den Präfixen
Global\/Local\sowie dazu, dass für das Neuerstellen eines Dateizuordnungsobjekts oder eines symbolischen Verknüpfungsobjekts im globalen Namensraum das RechtSeCreateGlobalPrivilegeerforderlich ist. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Named Pipes. Dazu, dass benannte Pipes im Rahmen der Sicherheitsprüfungen von jedem Prozess aus zugänglich und remote erreichbar sind, sofern der Serverdienst läuft (die Grundlage dafür, warum es sich um einen anderen Mechanismus als die von Mutexen usw. verwendeten
Global\/Local\-Sitzungsnamensräume handelt). ↩ -
Microsoft Learn, GetSystemMetrics function (winuser.h). Zur Spezifikation der Erkennung einer Remotesitzung über
SM_REMOTESESSION. ↩ ↩2 -
Microsoft Learn, Detecting the Remote Desktop Services environment. Zu einem Codebeispiel für
GetSystemMetrics(SM_REMOTESESSION), zur Einschränkung, dass diese Funktion bei Verwendung von RemoteFX vGPU eine Remotesitzung fälschlich als lokal erkennt, sowie zur alternativen Erkennung über den RegistrierungsschlüsselGlassSessionId. ↩ ↩2 -
Microsoft Security Bulletin, MS12-034 - Windows and Messages Vulnerability (CVE-2012-0180). Zu einer Sicherheitslücke zur Rechteeskalation, die eine Schwachstelle in der Fenstermeldungsverarbeitung des Windows-Kernelmodustreibers (win32k.sys) ausnutzt (ein Beispiel für diese Klasse von Angriffen über Fenstermeldungen). ↩
-
Microsoft Learn, schtasks create. Dazu, dass das SYSTEM-Konto keine interaktiven Anmelderechte besitzt und ein Benutzer ein unter SYSTEM-Rechten laufendes Programm oder eine solche Aufgabe weder sehen noch mit ihr interagieren kann. ↩
-
Microsoft Learn, Elevated Task Model. Zur Konfiguration, bei der eine Standardbenutzer-Anwendung Operationen, die Administratorrechte erfordern, über eine als SYSTEM registrierte Aufgabe ausführt, deren Sicherheitsdeskriptor jedoch so eingerichtet ist, dass ein Standardbenutzer sie dennoch starten kann. ↩
-
Microsoft Learn, Remote Desktop Services overview in Windows Server. Dazu, dass RDS die Grundlage bildet, die mehrsitzungsfähige, sitzungsbasierte Desktops bereitstellt. ↩ ↩2
-
Microsoft Learn, Troubleshoot Remote desktop disconnected errors. Dazu, dass bis zu zwei gleichzeitige Remoteverbindungen zu administrativen Zwecken ohne RDS-CAL möglich sind und darüber hinaus die Rolle RD-Sitzungshost sowie passende RDS-CALs erforderlich sind. ↩ ↩2
-
Microsoft Learn, License Remote Desktop Services with Client Access Licenses (CALs). Zum Unterschied zwischen den RDS-CAL-Modellen pro Gerät und pro Benutzer sowie zum Ausstellungs-/Verfolgungsmechanismus des Lizenzservers. ↩ ↩2
-
Microsoft Learn, qwinsta. Dazu, dass die Spalte
SESSIONNAMEden einer Sitzung zugewiesenen Namen anzeigt (consolefür die Konsole oder ein mitrdp-tcp#beginnender Name für eine RDP-Verbindung). ↩ -
Microsoft Learn, SystemParameters.IsRemoteSession Property. Zur Eigenschaft, die von WPF aus die zu
SM_REMOTESESSIONäquivalente Prüfung bereitstellt. ↩ -
Microsoft Learn, WTSGetActiveConsoleSessionId function (winbase.h). Zum Abrufen der mit der physischen Konsole verbundenen Sitzungs-ID und zur Rückgabe von
0xFFFFFFFFwährend eines Übergangs. ↩ ↩2 -
Microsoft Learn, WTSEnumerateSessions function (wtsapi32.h). Zur Möglichkeit, alle Sitzungen auf einem RD-Sitzungshostserver aufzuzählen. ↩
-
Microsoft Learn, Client/Server application guidelines. Dazu, warum „eine Verbindung von einer Maschine“ nicht mit „einer Benutzersitzung“ gleichgesetzt werden darf, sowie zur Identifizierung von Sitzungen über
ProcessIdToSessionId. ↩ -
Microsoft Learn, Policy CSP - ADMX_TerminalServer (TS_TEMP_PER_SESSION). Dazu, dass Remotedesktopdienste standardmäßig einen separaten Temp-Ordner pro Sitzung anlegen (unter dem Benutzerprofil, mit der Sitzungs-ID im Namen). ↩
-
Microsoft Learn, Peripheral hardware guidelines. Dazu, wie clientseitige Laufwerke und Drucker aus einer Sitzung auf einem RD-Sitzungshostserver je nach Umleitungskonfiguration erscheinen, sowie zum Mechanismus, mit dem ein Gerätetreiber die Umleitung deaktivieren kann. ↩
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Infobereich-Symbole und Toast-Benachrichtigungen in Windows-Apps — Fallstricke von NotifyIcon und die richtige AppNotification-Wahl
Ein praktischer Leitfaden dafür, eine geschäftliche Windows-Anwendung im Infobereich (System Tray) resident zu halten und den Benutzer üb...
Wie lange laufen VB6-Anwendungen noch? — Support-Status der Laufzeitumgebung und ein praxisnaher Weg zur .NET-Migration
Wie lange laufen VB6-Anwendungen noch? Dieser Artikel ordnet die Asymmetrie zwischen der Support-Richtlinie für die VB6-Laufzeitumgebung ...
Japanische Ära, Feiertage und Abrechnungsstichtage in Business-Apps — Übergangsresistentes Design, JapaneseCalendar und Werktagsberechnung in der Praxis
Auf einem Beleg muss „Reiwa 8“ stehen, die Werktagsberechnung muss Feiertage ausschließen, die Zahlung ist am letzten Werktag des Folgemo...
Das OSI-Modell wirklich verstehen — eine einzelne HTTP-Anfrage in ihre sieben Schichten zerlegen
Wir verstehen das OSI-Modell anhand der Praxis statt durch Auswendiglernen. In C# bauen wir den Ethernet-Frame, der eine einzelne HTTP-GE...
Mehrsprachigkeit für WinForms/WPF-Anwendungen ── resx, Satelliten-Assemblies und Kulturumschaltung in der Praxis
Dieser Artikel behandelt die Mehrsprachigkeit von Windows-Desktopanwendungen aus Praxissicht: den Unterschied zwischen CurrentCulture und...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Windows-App-Entwicklung
Geschäftsanwendungen, Geräteintegration und Kommunikationstools von den Anforderungen bis zur Umsetzung.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Warum kann ein Windows-Dienst kein Dialogfeld anzeigen?
- Seit Windows Vista laufen Dienste in einer dedizierten Session 0, isoliert von der interaktiven Sitzung jedes angemeldeten Benutzers. Eine von einem Dienst ausgelöste Benutzeroberfläche ist daher für niemanden sichtbar — und ein MessageBox-Aufruf kann den Prozess auf unbestimmte Zeit blockieren, während er auf einen OK-Button wartet, den niemand anklicken kann. Diese Isolierung ist eine Sicherheitsentscheidung, die den Austausch von Fenstermeldungen über eine Privilegiengrenze hinweg strukturell blockiert und damit eine ganze Klasse von Privilegien-Eskalationsangriffen ausschließt. Die offiziell empfohlene Lösung besteht darin, die Benutzeroberfläche in einen separaten Prozess auszulagern, der in der Sitzung des Benutzers läuft, und über IPC — etwa benannte Pipes — mit dem Dienst zu kommunizieren; das Starten eines UI-Programms als interaktiver Benutzer über die Aufgabenplanung ist eine weitere dokumentierte Option.
- Können sich mehrere Benutzer gleichzeitig per RDP mit Windows 10 oder 11 verbinden?
- Nein — gewöhnliche Client-Betriebssysteme wie Windows 10/11 Pro oder Enterprise sind grundsätzlich einsitzungsfähig (single-session), sodass eine physische Konsolensitzung und eine Remoteverbindung im Prinzip nicht koexistieren können; eine RDP-Verbindung schaltet den lokalen Bildschirm auf den Sperrbildschirm um. Mehrere gleichzeitige interaktive Sitzungen erfordern Windows Server mit der Rolle Remotedesktopdienste, wofür zusätzlich zu den zwei administrativen Verbindungen die Rolle RD-Sitzungshost sowie RDS-CALs benötigt werden. Die einzige Client-Edition mit Mehrsitzungsunterstützung, Windows Enterprise Multisession, wird ausschließlich für Azure Virtual Desktop angeboten.
- Warum verhindert mein Einzelinstanz-Mutex keine Starts aus den Sitzungen anderer Benutzer?
- Benannte Kernelobjekte wie Mutexe, Ereignisse und Semaphoren besitzen jeweils einen sitzungsbezogenen Namensraum. Ein ohne Präfix erstellter Mutex landet im Namensraum der aktuellen Sitzung, sodass die Sitzung eines anderen Benutzers (oder sogar eine zweite Sitzung desselben Benutzers) als völlig neues, eigenständiges Objekt behandelt wird und der Einzelinstanzschutz stillschweigend versagt. Wenn Sie eine einzige Instanz über die gesamte Maschine und alle Sitzungen hinweg wünschen, müssen Sie den Namen explizit mit Global\ versehen. Wenn Sie eine Instanz pro Benutzer über dessen Sitzungen hinweg wünschen, kombinieren Sie Global\ mit der in den Namen eingebetteten SID des Benutzers.
- Wie kann meine Anwendung erkennen, ob sie in einer Remotedesktopsitzung läuft?
- Die Standardprüfung ist die Win32-API GetSystemMetrics(SM_REMOTESESSION), die für eine Remotesitzung einen von null verschiedenen Wert zurückgibt; WPF stellt dieselbe Prüfung über die Eigenschaft SystemParameters.IsRemoteSession bereit. Beachten Sie, dass diese Funktion bei Remotesitzungen mit RemoteFX vGPU eine Remotesitzung fälschlich als lokal meldet; die dokumentierte Umgehung besteht darin, den GlassSessionId-Wert der Registrierung mit der aktuellen Sitzungs-ID zu vergleichen. Die Umgebungsvariable SESSIONNAME (Console gegenüber RDP-Tcp#...) eignet sich für eine schnelle Diagnose, ist aber keine offizielle API. Verwenden Sie WTSGetActiveConsoleSessionId nicht, um RDP-Sitzungen zu identifizieren — sie liefert nur die an die physische Konsole angeschlossene Sitzung.
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.
Öffentliche Links