Menü
DE

AdGuard VPN Browsererweiterung ist auf Manifest V3 migriert

Manifest V3, die neue Chrome-API-Version, ist für viele Entwickler zu einem Hindernis geworden. Sie nimmt erhebliche Änderungen an den Regeln vor, die Browsererweiterungen befolgen müssen. Sie verursachen nicht nur einige Unannehmlichkeiten, sondern führen auch zu einem teilweisen Verlust der Funktionalität.

Im August 2022 veröffentlichte AdGuard die weltweit erste werbeblockierende Browsererweiterung auf Basis von Manifest V3. Die Entwicklung war eine Herausforderung: Wir hatten viele Probleme auf dem Weg dorthin. Damals haben wir jedoch bewiesen, dass Werbeblocker auch in der Realität von Manifest V3 eine Zukunft haben.

Trotz der Tatsache, dass Google die Lebensdauer der Manifest V2 Erweiterungen bis mindestens Januar 2024 verlängert hat, haben wir die Entwicklung der AdGuard VPN Browsererweiterung, die nach den Manifest V3 Regeln arbeitet, nicht aufgeschoben und sind bereit, sie Ihnen jetzt zu präsentieren.

AdGuard VPN MV3-Browsererweiterung

Mit unserer Erfahrung bei der Entwicklung von AdGuard MV3 haben wir nicht erwartet, dass die Arbeit an AdGuard VPN MV3 ein Spaziergang sein würde, und wir hatten Recht. Lassen Sie uns über die Probleme erzählen, auf die wir gestoßen sind, und wie wir sie gelöst haben.

Service Worker schläft ein

In der vorherigen Version von Manifest gab es eine Hintergrundseite, die es ermöglichte, die Erweiterung einmal nach der Installation oder beim Öffnen eines Browsers zu starten und im Hintergrund weiterzuarbeiten. In Manifest V3 wird die Hintergrundseite durch den Service Worker ersetzt.

Sie ermöglicht es, dass Erweiterungen nur dann im Hintergrund laufen, wenn der Nutzer sie braucht. Die Idee hinter Google Chrome ist es, den Verbrauch von Systemressourcen stark zu reduzieren. Und für selten genutzte Browsererweiterungen funktioniert das auch. Aber wenn eine Erweiterung dem Nutzer ständig helfen muss, wie z. B. AdGuard VPN, wird ein Service Worker, der häufig in den Ruhezustand geht und wieder aufwacht, das System eher belasten als entlasten.

Und das ist noch nicht alles. Chrome überwacht den Service Worker sorgfältig. Wenn die Erweiterung also innerhalb von 30 Sekunden keine API-Aufrufe oder Events tätigt, um zu verhindern, dass der Service Worker einschläft, hält der Browser das Skript einfach an.

Wenn Service Worker gestoppt wird, erzeugt AdGuard VPN Browsererweiterung mit vielen Fehlern. Zum Beispiel erhält sie nicht die neuesten Informationen über verfügbare Standorte, verbleibenden freien Datenverkehr, ausgeschlossene Dienste, andere mit dem Konto verbundene Benutzergeräte und verfügbare Boni; sie kann die Zugriffsschlüssel und das Tray-Symbol mit dem Status der Erweiterung nicht aktualisieren.

Die Lösung

Wir haben bereits erwähnt, dass Events und API-Aufrufe verhindern, dass Service Worker in den Ruhezustand übergeht. Diese können sein:

  • Meldungen von Popup-Fenstern, Optionsseiten oder Inhaltsskripten einer Erweiterung
  • Proxy-Authentifizierungsanfrage
  • das Öffnen eines Popups der Erweiterung
  • das Öffnen einer neuen Browser-Registerkarte, das Aktualisieren einer bestehenden Registerkarte oder das Umschalten zwischen Browser-Fenstern
  • Auswahl einer beliebigen Aktion aus dem Kontextmenü auf der Seite (Hinzufügen der Website zu den Ausschlüssen, Ändern des Ausschlussmodus)
  • Auslösung des über die Alarm-API eingestellten Timers (mehr dazu später)

Kurz gesagt, funktioniert es so: Service Worker protokolliert den Event-Listener, und der Browser zeichnet das Event auf. Wenn das Event eintritt, prüft der Browser, ob er eine Erweiterung hat, die als Reaktion auf dieses Event aufgeweckt werden muss. Ist dies der Fall, weckt er den Service Worker der Erweiterung auf und übergibt das Event an den Listener.

In der Manifest-V2-Umgebung spielte es keine Rolle, wo im Code sich der Listener befand — die Events-Protokollierung konnte asynchron erfolgen, da die Hintergrundseite immer lief und nie neu gestartet wurde.

In Manifest V3 wird der Service Worker neu gestartet, wenn ein Event eintritt. In diesem Fall muss das Event auf der obersten Ebene des Codes (synchron) registriert werden, damit es den Listener erreicht.

Im folgenden Beispiel können Sie sehen, wie der Listener für Popup-Meldungen, Optionsseite und Inhaltsskripte jetzt eingestellt ist:

// add the listener for messages from popup, options page and userscripts
chrome.runtime.onMessage.addListener((event) => console.log(event));

Und dieses Beispiel zeigt, wie der Listener für Kontextmenü-Aktions-Events gesetzt wird:

// add the listener for context menu actions
chrome.contextMenus.onClicked.addListener((event) => console.log(event));

Hier wird gezeigt, wie man den Listener für Tab-Action-Events einstellt:

// add the listener for tab updated
chrome.tabs.onUpdated.addListener((event) => console.log(event));
// add the listener for tab activated
chrome.tabs.onActivated.addListener((event) => console.log(event));
// add the listener for tab activated from another window
chrome.windows.onFocusChanged.addListener((event) => console.log(event));

Die neue Alarm-API, die in Manifest V3 zusätzlich zu setInterval und setTimeout eingeführt wurde, hilft auch, einige Fehler zu vermeiden, die durch das Einschlafen des Server-Workers verursacht werden.

/**
* Creates an alarm with the specified name and delay
* @param alarmName
* @param delay in ms
*/
const createAlarm = (alarmName: string, delay: number) => {
   chrome.alarms.create(alarmName, { when: Date.now() + delay });
};

/**
* Creates a periodic alarm with the specified name and interval
* @param alarmName
* @param interval in minutes!
*/
const createPeriodicAlarm = (alarmName: string, interval: number) => {
   chrome.alarms.create(alarmName, { periodInMinutes: interval });
};

Das Verwenden von Alarm-API

/**
* setTimeout implementation
* @param callback
* @param timeout in ms
*/
setTimeout = (callback: () => void, timeout: number): number => {
   const timerId = this.generateId();
   alarmApi.createAlarm(`${timerId}`, timeout);
   alarmApi.onAlarmFires(`${timerId}`, callback);
   return timerId;
};

/**
* setInterval implementation
* @param callback
* @param interval in ms
*/
setInterval = (callback: () => void, interval: number): number => {
   const timerId = this.generateId();
   alarmApi.createPeriodicAlarm(`${timerId}`, this.convertMsToMin(interval));
   alarmApi.onAlarmFires(`${timerId}`, callback);
   return timerId;
};

Timer-Implementietung für MV3

Allerdings hat die Alarm-API eine ziemlich unangenehme Einschränkung: Es ist nicht möglich, einen Timer zu setzen, der in 3-5-10 Sekunden funktioniert, sondern nur in einer Minute oder mehr.

Service Worker wacht auf

Wenn der Service Worker in den Ruhezustand geht, werden die im RAM gespeicherten Daten entladen. Nach dem Aufwachen dauert es also eine Weile, bis die Erweiterung die Module initialisiert hat: im Durchschnitt 1,5-2 Sekunden. Wie Sie sehen können, gab es keine derartigen Probleme mit der Hintergrundseite, da sie nur neu geladen wurde, wenn der Browser neu gestartet wurde.

Die Lösung

Wir haben ein Modul hinzugefügt, das jede Zustandsänderung im RAM speichert. Der zuletzt gespeicherte Zustand wird von AdGuard VPN beim Aufwachen des Service Workers zur schnellen Wiederherstellung verwendet. Dadurch wird die Verzögerung beim Neustart des Service Workers auf 150-200 ms reduziert.

interface StateStorageInterface {
   // Gets the value for the specified key from the session storage
   getItem<T>(key: StorageKey): T;


   // Sets the value for the specified key in the session storage
   setItem<T>(key: StorageKey, value: T): void;


   // Initializes the storage by loading the data from the session storage,
   // or creating a new storage with the default data if none exists.
   init(): Promise<void>;
}

Schnittstelle für das Repository der Erweiterung

Die Umsetzung dieser Lösung erforderte die Verlagerung von änderbaren Feldern von Klassen in den Zustandsspeicher. Stattdessen fügten wir Getter und Setter zu den Klassen hinzu.

Wenn die Fallback-API zum Beispiel wie folgt aussah:

export class FallbackApi {
   fallbackInfo: FallbackInfo;

   private setFallbackInfo(fallbackInfo: FallbackInfo) {
       this.fallbackInfo = fallbackInfo;
   }

   private async getFallbackInfo(): Promise<FallbackInfo> {
       if (FallbackApi.needsUpdate(this.fallbackInfo)) {
           await this.updateFallbackInfo();
       }

       return this.fallbackInfo;
   }
}

Jetzt hat sich sein Aussehen verändert:

export class FallbackApi {
   private get fallbackInfo(): FallbackInfo {
       return sessionState.getItem(StorageKey.FallbackInfo);
   }

   private set fallbackInfo(value: FallbackInfo) {
       sessionState.setItem(StorageKey.FallbackInfo, value);
   }

   private static needsUpdate(fallbackInfo: FallbackInfo): boolean {
       return fallbackInfo.expiresInMs < Date.now();
   }

   private async getFallbackInfo(): Promise<FallbackInfo> {
       if (this.fallbackInfo && FallbackApi.needsUpdate(this.fallbackInfo)) {
           await this.updateFallbackInfo();
           if (this.fallbackInfo) {
               return this.fallbackInfo;
           }
       }

       return this.fallbackInfo;
   }
}

Hier sind einige weitere Beispiele:

class Endpoints implements EndpointsInterface {
   vpnInfo?: VpnExtensionInfoInterface;
}

Hinzufügen eines Status zur Klasse Endpoints in Manifest V2

class Endpoints implements EndpointsInterface {
   #state: EndpointsState | undefined;

   private get vpnInfo(): VpnExtensionInfoInterface | null {
       return this.state.vpnInfo;
   }

   private set vpnInfo(vpnInfo: VpnExtensionInfoInterface | null) {
       this.state.vpnInfo = vpnInfo;
       sessionState.setItem(StorageKey.Endpoints, this.state);
   }

   public get state(): EndpointsState {
       if (!this.#state) {
           throw new Error('Endpoints API is not initialized');
       }

       return this.#state;
   }

   public set state(value: EndpointsState) {
       this.#state = value;
   }
}

Hinzufügen eines Status zur Klasse Endpoints in Manifest V3

export class Credentials implements CredentialsInterface {
   vpnToken: VpnTokenData | null;

   vpnCredentials: CredentialsDataInterface | null;
}

Hinzufügen eines Status zur Klasse Credentials in Manifest V2

export class Credentials implements CredentialsInterface {
   state: CredentialsState;

   saveCredentialsState = () => {
       sessionState.setItem(StorageKey.CredentialsState, this.state);
   };

   get vpnToken() {
       return this.state.vpnToken;
   }

   set vpnToken(vpnToken: VpnTokenData | null) {
       this.state.vpnToken = vpnToken;
       this.saveCredentialsState();
   }

   get vpnCredentials() {
       return this.state.vpnCredentials;
   }

   set vpnCredentials(vpnCredentials: CredentialsDataInterface | null) {
       this.state.vpnCredentials = vpnCredentials;
       this.saveCredentialsState();
   }
}

Hinzufügen eines Status zur Klasse Credentials in Manifest V3

Implementierung von NetworkConnectionObserver

Der NetworkConnectionObserver ist eine Klasse, die den Netzwerkstatus (online/offline) überwacht. Sie wird verwendet, wenn eine Anwendung keine Antwort vom Server erhält und hilft, die Ursache des Problems zu ermitteln: keine Internetverbindung des Nutzers oder ein Problem auf der Serverseite. Außerdem überwacht der NetworkConnectionObserver den Übergang des Nutzers von offline zu online. In diesem Fall werden die Zugriffsrechte überprüft, und die Verbindung wird wiederhergestellt, wenn sie aufgebaut wurde, bevor der Benutzer offline ging.

In MV2 ist NetworkConnectionObserver sehr einfach implementiert. Er wartet auf das Event online und ruft den ihm übergebenen callback auf.

class NetworkConnectionObserver { constructor(callback: () => Promise<void>) { window.addEventListener('online', callback); } }

In MV3 hat der Service Worker keinen Zugriff auf window, so dass die Implementierung komplizierter ist. Eine Lösung wäre die Verwendung eines Offscreen-Dokuments. Damit würde eine unsichtbare Seite mit Zugriff auf window und document und der Möglichkeit, dort Aktionen durchzuführen, die dem Service Worker nicht zur Verfügung stehen, geschaffen.

Google verspricht jedoch, dass das Offscreen-Dokument in Zukunft nach einer gewissen Zeit der Inaktivität einschlafen wird, ähnlich wie der Service Worker. Auf diese Weise wird der NetworkConnectionObserver nicht in der Lage sein, die ganze Zeit auf das online/offline-Event zu hören, was zu verschiedenen Problemen führen wird.

Jetzt müssen wir eine nicht ganz so subtile Lösung verwenden: jede halbe Sekunde überprüfen wir den Status von navigator.online und rufen callback auf, wenn er sich von offline zu online geändert hat.

/**
* Starts checking if the network connection is online at a specified time interval.
*/
private startCheckIsOnline() {
   setInterval(() => {
       this.setIsOnline(navigator.onLine);
   }, this.CHECK_ONLINE_INTERVAL_MS);
}

/**
* Calls handler if network connection becomes online and sets isOnline value.
*/
private setIsOnline(isOnline: boolean) {
   if (isOnline && !this.isOnline) {
       this.onlineHandler(); // callback
   }
   this.isOnline = isOnline;
}

Probleme mit der WebSocket-Unterstützung

In Manifest V3 werden Nachrichten, die von der Erweiterung über eine WebSocket-Verbindung empfangen werden, den schlafenden Service Worker nicht aufwecken. Infolgedessen verpasst die Erweiterung möglicherweise die Nachricht, dass die Zugriffsschlüssel des Backend-Servers aktualisiert werden müssen, und kann Fehlermeldungen über das Erreichen des Datenverkehrslimits, des Limits für angeschlossene Geräte und nicht routbare Domains nicht verarbeiten.

Die Lösung

Der erste Impuls war, Chromes Vision zu widersprechen und einen nicht schlafenden Service Worker zu schaffen. Tatsächlich haben wir das getan. Aber dann beschlossen wir, uns an die Regeln von Manifest V3 zu halten und nur dann eine Verbindung zum WebSocket herzustellen, wenn der Service Worker wach ist.

Trotz der gefundenen Lösung hoffen wir wirklich, dass Google das Problem, das wir gemeldet haben, löst und die Möglichkeit hinzufügt, die Lebensdauer des Service Workers mit über die WebSocket-Verbindung empfangenen Nachrichten zu verlängern.

Frühzeitiges Auslösen des onAuthRequired-Handlers

Wir haben auch ein Problem mit einem Browser-Popup behoben, das eine Proxy-Autorisierung anforderte. Dieses Problem trat auf, wenn der Sitzungsspeicher nicht in der Lage war, die Anmeldedaten zu laden, bevor der onAuthRequired-Event-Handler beim Aufwachen des Service Workers ausgelöst wurde.

Die Lösung

Um dieses Problem zu beheben, haben wir einen asynchronen Callback für den Handler onAuthRequired hinzugefügt. Er ist nun so eingestellt, dass er wartet, bis der Sitzungsspeicher die Anmeldedaten geladen und eingegeben hat.

/**
     * Fügt Listener für das onAuthRequired Ereignis hinzu
     */
    private addOnAuthRequiredListener = () => {
        chrome.webRequest.onAuthRequired.addListener(
            this.onAuthRequiredHandler,
            { urls: [‘<all_urls>’] },
            [‘blocking’],
        );
    };

onAuthRequired-Handler mit einem synchronen Callback

 /**
     * Fügt Listener für das onAuthRequired Ereignis hinzu
     */
    private addOnAuthRequiredListener = () => {
        chrome.webRequest.onAuthRequired.addListener(
            this.onAuthRequiredHandler,
            { urls: [‘<all_urls>’] },
            [‘asyncBlocking’],
        );
    };

onAuthRequired-Handler mit einem asynchronen Callback

Fazit

Manifest V3 ist weit davon entfernt, die intelligenteste und stabilste Umgebung zu sein. Dennoch können Sie die AdGuard VPN MV3 Browsererweiterung bereits installieren und ausprobieren. Wie immer freuen wir uns auf Feedback: Wenn Sie einen Fehler finden, melden Sie ihn bitte auf GitHub.

Hat Ihnen dieser Beitrag gefallen?

AdGuard VPN
für Windows

Verwenden Sie einen beliebigen Browser oder App und sorgen Sie sich nie wieder um Ihre Anonymität. Mit AdGuard VPN steht Ihnen die ganze Welt zur Verfügung.
Mehr erfahren
Herunterladen
Durch das Herunterladen akzeptieren Sie die Bedingungen des Lizenzvertrags

AdGuard VPN
für Mac

Wählen Sie mit nur zwei Klicks einen unserer 70+ Standorte auf der ganzen Welt aus, und Ihre Daten werden sicher vor den neugierigen Blicken der Unternehmen und Regierungen verborgen.
Mehr erfahren
Herunterladen
Durch das Herunterladen akzeptieren Sie die Bedingungen des Lizenzvertrags

AdGuard VPN
für iOS

Schützen Sie Ihre Daten überall im Internet. Genießen Sie Ihre Lieblingsfilme und -Serien und bleiben Sie dabei sicher mit AdGuard VPN!
Mehr erfahren
App Store
Durch das Herunterladen akzeptieren Sie die Bedingungen des Lizenzvertrags

AdGuard VPN
für Android

Bleiben Sie mit AdGuard VPN überall anonym! Dutzende von Standorten, schnelle und zuverlässige Verbindung – alles in Ihrer Tasche.
Mehr erfahren
Google Play
Durch das Herunterladen akzeptieren Sie die Bedingungen des Lizenzvertrags
Herunterladen
Durch das Herunterladen akzeptieren Sie die Bedingungen des Lizenzvertrags

AdGuard VPN
für Chrome

Verbergen Sie Ihren Standort und „bewegen“ Sie sich an einen beliebigen Ort der Welt, öffnen Sie alle Inhalte ohne Einschränkungen und bleiben Sie im Web anonym.
Mehr erfahren
Installieren
Durch das Herunterladen akzeptieren Sie die Bedingungen des Lizenzvertrags

AdGuard VPN
für Edge

Wechseln Sie mit einem Klick den Standort, verbergen Sie Ihre IP und machen Sie Ihr Surfen im Internet sicher und anonym.
Mehr erfahren
Installieren
Durch das Herunterladen akzeptieren Sie die Bedingungen des Lizenzvertrags

AdGuard VPN
für Firefox

Schützen Sie Ihre Privatsphäre, erhalten Sie Zugriff auf geografisch eingeschränkte Inhalte, entscheiden Sie, auf welchen Websites Sie das VPN benötigen und auf welchen nicht.
Mehr erfahren
Installieren
Durch das Herunterladen akzeptieren Sie die Bedingungen des Lizenzvertrags

AdGuard VPN
für Opera

Seien Sie ein Ninja in Ihrem Opera-Browser: Bewegen Sie sich schnell in jeden Teil der Welt und bleiben Sie unbemerkt.
Mehr erfahren
Installieren
Durch das Herunterladen akzeptieren Sie die Bedingungen des Lizenzvertrags
AdGuard VPN
wird heruntergeladen
Klicken Sie auf die Schaltfläche mit dem Pfeil, um die Installation zu starten.
Scannen Sie den QR-Code, um AdGuard VPN auf Ihrem Mobilgerät zu installieren