AdGuard VPN Browser Extension migrated to Manifest V3

Manifest V3, the new Chrome API version, has become a stumbling block for many developers. It makes significant changes to the rules that browser extensions must follow. Not only do they cause some inconvenience, but they also lead to a partial loss of functionality.

In August 2022, AdGuard released the world's first ad-blocking browser extension based on Manifest V3. The development was challenging: we faced many problems along the way. However, at that time we proved that there is a future for ad blockers even in the reality of Manifest V3.

Despite the fact that Google has extended the lifetime of Manifest V2 extensions until at least January 2024, we have not postponed the development of AdGuard VPN Browser Extension which works according to Manifest V3 rules, and we are ready to present it to you now.

AdGuard VPN MV3 Browser Extension

With our experience in developing the AdGuard MV3, we didn't expect working on AdGuard VPN MV3 to be a walk in the park, and we were right. Let's talk about the problems we encountered and how we solved them.

Service worker falling asleep

The previous version of Manifest had a background page that allowed the extension to run once after installation or when a browser was opened, and continue to work in the background. In Manifest V3, the background page is replaced by the service worker.

It allows extensions to run in the background only when the user needs them. The idea behind Google Chrome is to greatly reduce the consumption of system resources. And for infrequently used browser extensions, this will work. But if an extension has to help the user constantly, like AdGuard VPN, a service worker that frequently goes to sleep and wakes up is more likely to increase the load on the system than decrease it.

And that's not all. Chrome keeps a close eye on the service worker. So, if the extension doesn't make any API calls or events within 30 seconds to prevent the service worker from falling asleep, the browser will simply stop the script.

Stopping the service worker threatens AdGuard VPN Browser Extension with many errors. For example, it will not receive the latest information about available locations, remaining free traffic, excluded services, other user devices connected to the account, and available bonuses; it will not be able to update access keys and the tray icon with the extension status.

Solution

We mentioned above that events and API calls prevent the service worker from entering hibernation mode. These can be:

  • messages from an extension's popup, options page or content script
  • proxy authentication request
  • an extension's popup opening
  • a new browser tab opening, refreshing an existing tab, or switching between browser windows
  • choosing any action from the context menu on the page (adding the website to exclusions, changing the exclusions mode)
  • triggering of the timer set via Alarm API (more on that later)

In a nutshell, it works like this: the service worker logs the event listener, and the browser records the event. When the event happens, the browser sees if it has an extension that needs to be woken up in response to this event. If it does, it wakes the extension's service worker and passes the event to the listener.

In Manifest V2 environment, it didn't matter where in the code the event listener was located – its registration could occur asynchronously because the background page was always running and never restarted.

In Manifest V3 the service worker is restarted when an event occurs. In this case, in order for the event to hit the listener, it must be registered at the top level of the code (synchronously).

In the example below you can see how the listener for popup messages, options page and content scripts is now set:

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

And this example illustrates how the listener for context menu action events is set:

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

This shows how to set the listener for tab action events:

// 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));

The new Alarm API, which appeared in Manifest V3 in addition to setInterval and setTimeout, also helps to avoid some errors caused by the server worker falling asleep.

/**
 * 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 });
 };

Using the 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 implementation for MV3

However, the Alarm API has a rather inconvenient limitation: it is impossible to set a timer that works in 3-5-10 seconds but only in a minute or more.

Service worker waking up

When the service worker goes into hibernation mode, it unloads the data that was stored in RAM. So after waking up, it takes a while for the extension to initialize the modules: an inexcusably long 1.5-2 seconds on average. As you can see, there were no such problems with the background page, as it was only reloaded when the browser was restarted.

Solution

We added a module to store each state change from RAM. The last saved state AdGuard VPN uses when the service worker wakes up for quick recovery. This reduces the delay in restarting the service worker to 150-200 ms.

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>;
}

Extension state repository interface

Implementing this solution required moving changeable fields from classes to the state store. In their place, we added getters and setters to the classes.

For example, if the Fallback API used to look like this:

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;
   }
}

Now its appearance has changed:

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;
   }
}

Here are a few more examples:

class Endpoints implements EndpointsInterface {
   vpnInfo?: VpnExtensionInfoInterface;
}

Adding a state to the Endpoints class 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;
   }
}

Adding a state to the Endpoints class in Manifest V3

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

   vpnCredentials: CredentialsDataInterface | null;
}

Adding a state to the Credentials class 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();
   }
}

Adding a state to the Credentials class in Manifest V3

NetworkConnectionObserver implementation

The NetworkConnectionObserver is a class which monitors the network status (online/offline). It is used when an app does not get a response from the server and helps to identify the cause of the problem: no Internet connection from the user or an issue on the server side. Also, NetworkConnectionObserver monitors the user's transition from offline to online. In this case access rights are checked and the connection is restored if it was established before the user went offline.

In MV2, the NetworkConnectionObserver is implemented very simply. It listens for the online event and calls the callback passed to it.

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

Within MV3 the service worker does not have access to window, so the implementation is more complicated. A solution would be to use an offscreen document. This would create an invisible page with access to window and document and the ability to perform actions there that are not available to the service worker.

However, Google promises that in the future the offscreen document will fall asleep after a period of inactivity, similar to the service worker. This way, the NetworkConnectionObserver will not be able to listen to the online/offline event all the time, which will lead to various problems.

Now we have to use a not-so-subtle solution: every half second we check the state of navigator.online and call callback if it has changed from offline to online.

/**
 * 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;
 }

Websocket support issues

Now in Manifest V3, messages received by the extension over a websocket connection will not wake up the sleeping service worker. As a result, the extension may miss the message about the need to update backend server access keys and may not be able to handle error messages about reaching the traffic limit, the connected device limit, and non-routable domains.

Solution

The initial impulse was to go against Chrome's vision and make an unsleeping service worker. In fact, we did it. But then we decided to try to play by the rules of Manifest V3 and connect to the websocket only when the service worker is awake.

Despite the found solution, we really hope that Google will solve the problem that we reported and add the possibility to extend the service worker life with messages received over the websocket connection.

Early triggering of the onAuthRequired handler

We’ve also fixed an issue with a browser popup requesting proxy authorization. This used to happen when the session storage wasn't able to load the credentials before the onAuthRequired event handler fired at the service worker wakeup.

Solution

To fix this problem, we've added an asynchronous callback for the onAuthRequired handler. It is now set to wait until the session storage has loaded the credentials and entered them.

/**
 * Adds listener for the onAuthRequired event
 */
 private addOnAuthRequiredListener = () => {
    chrome.webRequest.onAuthRequired.addListener(
        this.onAuthRequiredHandler,
        { urls: ['<all_urls>'] },
        ['blocking'],
    );
 };

onAuthRequired handler with a synchronous callback

/**
 * Adds listener for the onAuthRequired event
 */
 private addOnAuthRequiredListener = () => {
    chrome.webRequest.onAuthRequired.addListener(
        this.onAuthRequiredHandler,
        { urls: ['<all_urls>'] },
        ['asyncBlocking'],
    );
 };

onAuthRequired handler with an asynchronous callback

In conclusion

Manifest V3 is far from being the smartest and most stable environment. Nevertheless, you can already install AdGuard VPN MV3 Browser Extension and try it out. As always, we rely heavily on feedback: if you find a bug, please report it on GitHub.

Ți-a plăcut această postare?
9.332 9332 recenzii
Excelent!

AdGuard VPN pentru Windows

Folosiți orice browser sau aplicație și nu vă mai faceți niciodată griji cu privire la anonimatul dvs. O lume întreagă este la îndemâna dvs. cu AdGuard VPN.
Descărcați
Prin descărcarea programului, acceptați termenii Acordului de licență
Aflați mai multe
Ecranul principal al AdGuard VPN pentru Windows cu o conexiune VPN activă.
Ecranul principal al AdGuard VPN pentru Windows, care afișează starea deconectată.
Ecranul Excluderi cu o listă de site-uri web care nu utilizează VPN-ul.
9.332 9332 recenzii
Excelent!

AdGuard VPN pentru Mac

Cu doar două clicuri, selectați un oraș de oriunde din lume — avem 75+ de locații — și datele dvs. sunt invizibile pentru ochii curioși.
Descărcați
Prin descărcarea programului, acceptați termenii Acordului de licență
Aflați mai multe
Ecranul principal al AdGuard VPN pentru Mac, cu o conexiune VPN activă.
Ecranul principal al AdGuard VPN pentru Mac, care afișează starea deconectată.
Ecranul Excluderi cu o listă de site-uri web care nu utilizează VPN-ul.
9.332 9332 recenzii
Excelent!

AdGuard VPN pentru Android

AdGuard VPN pentru Android oferă zeci de locații rapide. Navigați cu mai multă intimitate atunci când sunteți în mișcare. Blocați reclamele și trackerele cu Protecția DNS integrată
Aflați mai multe
Descărcați
Prin descărcarea programului, acceptați termenii Acordului de licență
Google Play
Prin descărcarea programului, acceptați termenii Acordului de licență
Ecranul principal al aplicației AdGuard VPN pentru Android, cu o conexiune VPN activă.
Ecranul principal al aplicației AdGuard VPN pentru Android, care afișează starea deconectată.
Ecranul Locații care afișează cele mai rapide servere disponibile.
Ecranul Excluderi cu o listă de site-uri web care nu utilizează VPN-ul.
Ecranul cu un cod QR pentru instalarea AdGuard VPN.
9.332 9332 recenzii
Excelent!

AdGuard VPN pentru iOS

Protejați-vă confidențialitatea și redați în streaming emisiunile preferate în mod anonim cu AdGuard VPN pentru iOS. Blocați reclamele cu protecția DNS încorporată
Aflați mai multe
App Store
Prin descărcarea programului, acceptați termenii Acordului de licență
Ecranul principal al AdGuard VPN pentru iOS, cu o conexiune VPN activă.
Ecranul principal al AdGuard VPN pentru iOS, care afișează starea deconectată.
Ecranul Locații care afișează cele mai rapide servere disponibile.
Ecranul Excluderi cu o listă de site-uri web care nu utilizează VPN-ul.
Ecranul cu un cod QR pentru instalarea AdGuard VPN.
9.332 9332 recenzii
Excelent!

AdGuard VPN pentru Android TV

Descoperiți AdGuard VPN pentru Android TV! Bucurați-vă de streaming fără întreruperi, securitate îmbunătățită și configurare ușoară.
Descărcați
Prin descărcarea programului, acceptați termenii Acordului de licență
Google Play
Prin descărcarea programului, acceptați termenii Acordului de licență
Aflați mai multe
Ecranul principal al AdGuard VPN pentru Android TV, cu o conexiune VPN activă la un server din Helsinki, Finlanda.
9.332 9332 recenzii
Excelent!

AdGuard VPN pentru Chrome

AdGuard VPN pentru Edge

AdGuard VPN pentru Firefox

AdGuard VPN pentru Opera

Ascundeți-vă locația reală și apăreți din alt loc al lumii - accesați orice conținut fără limite de viteză și păstrați-vă anonimatul pe internet.
Ajungeți într-o altă locație cu un singur clic, ascundeți-vă IP-ul și faceți-vă navigarea pe web sigură și anonimă.
Protejați-vă confidențialitatea, ascundeți-vă locația reală și decideți unde aveți nevoie de VPN și unde nu!
Fiți un ninja în browserul Opera: deplasați-vă rapid în orice parte a lumii și treceți neobservat.
Instalați
Prin descărcarea programului, acceptați termenii Acordului de licență
Aflați mai multe
Instalați
Prin descărcarea programului, acceptați termenii Acordului de licență
Aflați mai multe
Instalați
Prin descărcarea programului, acceptați termenii Acordului de licență
Aflați mai multe
Instalați
Prin descărcarea programului, acceptați termenii Acordului de licență
Aflați mai multe
Ecranul principal al extensiei de browser AdGuard VPN cu o conexiune VPN activă.
Ecranul Locații care afișează cele mai rapide servere disponibile.
9.332 9332 recenzii
Excelent!

AdGuard VPN pentru routere

Instalați AdGuard VPN pe router pentru a vă securiza întreaga rețea. Decideți ce dispozitive să protejați și când
Această opțiune este disponibilă doar cu un abonament VPN AdGuard
Un router cu o pictogramă în formă de scut cu gaură de cheie.
9.332 9332 recenzii
Excelent!

AdGuard VPN pentru Linux

Obțineți cel mai bun VPN gratuit pentru Linux și bucurați-vă de navigare web fără întreruperi, securitate sporită, criptarea traficului Internet și protecție împotriva scurgerilor DNS. Alegeți dintre mai multe servere VPN și accesați locațiile dorite
Mascota AdGuard VPN, Tengu, alături de pinguinul Linux, Tux, stând în tufișuri.
9.332 9332 recenzii
Excelent!

AdGuard VPN pentru Apple TV

Descoperă AdGuard VPN pentru Apple TV! Bucurați-vă de streaming fără probleme, securitate îmbunătățită și configurare ușoară
Această opțiune este disponibilă doar cu un abonament VPN AdGuard
Un dispozitiv de streaming Apple TV cu sigla AdGuard VPN.
9.332 9332 recenzii
Excelent!

AdGuard VPN pentru Xbox

Protejați-vă Xbox cu AdGuard VPN și bucurați-vă de jocuri online fără întreruperi, securitate sporită și configurare ușoară
Această opțiune este disponibilă doar cu un abonament VPN AdGuard
O consolă Microsoft Xbox și un controler cu sigla AdGuard VPN.
9.332 9332 recenzii
Excelent!

AdGuard VPN pentru PS4/PS5

Protejați-vă PlayStation-ul cu AdGuard VPN și bucurați-vă de jocuri online fără întreruperi, securitate sporită și configurare ușoară. Alegeți din mai multe servere VPN și accesați locațiile dorite
Această funcționalitate este disponibilă doar cu un abonament AdGuard VPN
Această opțiune este disponibilă doar cu un abonament VPN AdGuard
O consolă Sony PlayStation și un controler cu sigla AdGuard VPN.
9.332 9332 recenzii
Excelent!

AdGuard VPN pentru Chromecast

Instalați AdGuard VPN pe Google TV (Chromecast Gen 4) sau pe routerul dvs. de rețea (Chromecast Gen 3) și bucurați-vă de streaming de conținut cu Chromecast în timp ce rămâneți anonim online și accesați conținut din orice loc. Pentru Chromecast Gen 3, aveți nevoie de un abonament AdGuard VPN
Această opțiune este disponibilă doar cu un abonament VPN AdGuard
Un dispozitiv de streaming Google Chromecast cu o pictogramă scut cu gaură de cheie.
AdGuard VPN
descărcarea a început
Faceți clic pe butonul indicat de săgeată pentru a începe instalarea.
Scanați pentru a instala AdGuard VPN pe dispozitivul dvs. mobil
Ecranul cu un cod QR pentru instalarea AdGuard VPN.