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.

Thích bài viết này?
9.332 9332 đánh giá của người dùng
Tuyệt vời!

AdGuard VPN cho Windows

Sử dụng bất kỳ trình duyệt hoặc ứng dụng nào và không bao giờ phải lo lắng về tình trạng ẩn danh của bạn nữa. Toàn bộ thế giới nằm trong tầm tay bạn với AdGuard VPN.
TẢI XUỐNG
Bằng cách tải xuống chương trình, bạn chấp nhận các điều khoản của Thỏa thuận cấp phép
Tìm hiểu thêm
Màn hình chính của AdGuard VPN cho Windows với kết nối VPN đang hoạt động.
Màn hình chính của AdGuard VPN dành cho Windows hiển thị trạng thái đã ngắt kết nối.
Màn hình Loại trừ với danh sách các trang web không sử dụng VPN.
9.332 9332 đánh giá của người dùng
Tuyệt vời!

AdGuard VPN cho Mac

Chỉ với hai cú nhấp chuột, hãy chọn một thành phố từ bất kỳ nơi nào trên thế giới — chúng tôi có hơn 75 vị trí — và dữ liệu của bạn sẽ vô hình với những con mắt tò mò.
TẢI XUỐNG
Bằng cách tải xuống chương trình, bạn chấp nhận các điều khoản của Thỏa thuận cấp phép
Tìm hiểu thêm
Màn hình chính của AdGuard VPN dành cho Mac với kết nối VPN đang hoạt động.
Màn hình chính của AdGuard VPN cho Mac hiển thị trạng thái đã ngắt kết nối.
Màn hình Loại trừ với danh sách các trang web không sử dụng VPN.
9.332 9332 đánh giá của người dùng
Tuyệt vời!

AdGuard VPN cho Android

AdGuard VPN cho Android cung cấp hàng chục vị trí tốc độ cao. Trình duyệt riêng tư hơn khi đang di chuyển. Chặn quảng cáo và trình theo dõi bằng tính năng Bảo Vệ Theo Dõi tích hợp
Tìm hiểu thêm
TẢI XUỐNG
Bằng cách tải xuống chương trình, bạn chấp nhận các điều khoản của Thỏa thuận cấp phép
Có sẵn trên Google Play
Bằng cách tải xuống chương trình, bạn chấp nhận các điều khoản của Thỏa thuận cấp phép
Màn hình chính của AdGuard VPN dành cho Android với kết nối VPN đang hoạt động.
Màn hình chính của AdGuard VPN dành cho Android hiển thị trạng thái đã ngắt kết nối.
Màn hình Vị trí hiển thị các máy chủ khả dụng nhanh nhất.
Màn hình Loại trừ với danh sách các trang web không sử dụng VPN.
Màn hình có mã QR để cài đặt AdGuard VPN.
9.332 9332 đánh giá của người dùng
Tuyệt vời!

AdGuard VPN cho iOS

Bảo vệ sự riêng tư của bạn và phát trực tuyến các chương trình yêu thích một cách ẩn danh với AdGuard VPN cho iOS. Chặn quảng cáo bằng tính năng Bảo Vệ Theo Dõi tích hợp sẵn
Tìm hiểu thêm
Có sẵn trên App Store
Bằng cách tải xuống chương trình, bạn chấp nhận các điều khoản của Thỏa thuận cấp phép
Màn hình chính của AdGuard VPN dành cho iOS với kết nối VPN đang hoạt động.
Màn hình chính của AdGuard VPN dành cho iOS hiển thị trạng thái đã ngắt kết nối.
Màn hình Vị trí hiển thị các máy chủ khả dụng nhanh nhất.
Màn hình Loại trừ với danh sách các trang web không sử dụng VPN.
Màn hình có mã QR để cài đặt AdGuard VPN.
9.332 9332 đánh giá của người dùng
Tuyệt vời!

AdGuard VPN cho Android TV

Khám phá AdGuard VPN cho Android TV! Tận hưởng khả năng phát trực tuyến liền mạch, bảo mật nâng cao và thiết lập dễ dàng.
TẢI XUỐNG
Bằng cách tải xuống chương trình, bạn chấp nhận các điều khoản của Thỏa thuận cấp phép
Có sẵn trên Google Play
Bằng cách tải xuống chương trình, bạn chấp nhận các điều khoản của Thỏa thuận cấp phép
Tìm hiểu thêm
Màn hình chính của AdGuard VPN dành cho Android TV với kết nối VPN đang hoạt động tới một máy chủ ở Helsinki, Phần Lan.
9.332 9332 đánh giá của người dùng
Tuyệt vời!

AdGuard VPN cho Chrome

AdGuard VPN cho Edge

AdGuard VPN cho Firefox

AdGuard VPN cho Opera

Ẩn vị trí thực sự của bạn và xuất hiện từ một nơi khác trên thế giới - truy cập bất kỳ nội dung nào mà không có giới hạn tốc độ và bảo vệ tính ẩn danh trên web của bạn.
Đến một vị trí khác chỉ bằng một cú nhấp chuột, ẩn IP của bạn và giúp bạn lướt web an toàn và ẩn danh.
Bảo vệ quyền riêng tư của bạn, ẩn vị trí thực của bạn và quyết định nơi bạn cần VPN và nơi bạn không!
Trở thành ninja trong trình duyệt Opera của bạn: di chuyển nhanh chóng đến bất kỳ nơi nào trên thế giới mà vẫn không bị chú ý.
Cài đặt
Bằng cách tải xuống chương trình, bạn chấp nhận các điều khoản của Thỏa thuận cấp phép
Tìm hiểu thêm
Cài đặt
Bằng cách tải xuống chương trình, bạn chấp nhận các điều khoản của Thỏa thuận cấp phép
Tìm hiểu thêm
Cài đặt
Bằng cách tải xuống chương trình, bạn chấp nhận các điều khoản của Thỏa thuận cấp phép
Tìm hiểu thêm
Cài đặt
Bằng cách tải xuống chương trình, bạn chấp nhận các điều khoản của Thỏa thuận cấp phép
Tìm hiểu thêm
Màn hình chính của tiện ích mở rộng trình duyệt AdGuard VPN với kết nối VPN đang hoạt động.
Màn hình Vị trí hiển thị các máy chủ khả dụng nhanh nhất.
9.332 9332 đánh giá của người dùng
Tuyệt vời!

AdGuard VPN cho bộ định tuyến

Cài đặt AdGuard VPN trên bộ định tuyến của bạn để bảo mật toàn bộ mạng của bạn. Quyết định thiết bị nào cần bảo vệ và khi nào
Tùy chọn này chỉ khả dụng với Đăng kí AdGuard VPN
Một bộ định tuyến có biểu tượng chiếc khiên với lỗ khóa.
9.332 9332 đánh giá của người dùng
Tuyệt vời!

AdGuard VPN cho Linux

Nhận VPN miễn phí tốt nhất cho Linux và tận hưởng việc lướt web mượt mà, bảo mật được nâng cao, mã hóa lưu lượng Internet và bảo vệ rò rỉ DNS. Chọn từ nhiều máy chủ VPN và truy cập các vị trí bạn muốn
Linh vật Tengu của AdGuard VPN cùng chú chim cánh cụt Tux của Linux đang ngồi trong bụi cây.
9.332 9332 đánh giá của người dùng
Tuyệt vời!

AdGuard VPN cho Apple TV

Khám phá AdGuard VPN cho Apple TV! Tận hưởng khả năng phát trực tuyến liền mạch, bảo mật nâng cao và thiết lập dễ dàng
Tùy chọn này chỉ khả dụng với Đăng kí AdGuard VPN
Thiết bị phát trực tuyến Apple TV có logo AdGuard VPN.
9.332 9332 đánh giá của người dùng
Tuyệt vời!

AdGuard VPN cho Xbox

Bảo vệ Xbox của bạn với AdGuard VPN và tận hưởng trò chơi trực tuyến liền mạch, bảo mật nâng cao và thiết lập dễ dàng
Tùy chọn này chỉ khả dụng với Đăng kí AdGuard VPN
Máy chơi game Microsoft Xbox và tay cầm có logo AdGuard VPN.
9.332 9332 đánh giá của người dùng
Tuyệt vời!

AdGuard VPN cho PS4/PS5

Bảo vệ PlayStation của bạn với AdGuard VPN và tận hưởng trò chơi trực tuyến liền mạch, bảo mật nâng cao và thiết lập dễ dàng. Chọn từ nhiều máy chủ VPN và truy cập các vị trí bạn muốn
Tính năng này chỉ khả dụng với đăng ký AdGuard VPN
Tùy chọn này chỉ khả dụng với Đăng kí AdGuard VPN
Máy chơi game Sony PlayStation và tay cầm có logo AdGuard VPN.
9.332 9332 đánh giá của người dùng
Tuyệt vời!

AdGuard VPN cho Chromecast

Cài đặt AdGuard VPN trên Google TV của bạn (Chromecast Gen 4) hoặc trên bộ định tuyến mạng của bạn (Chromecast Gen 3) và tận hưởng việc phát trực tuyến nội dung với Chromecast trong khi giữ ẩn danh trực tuyến và truy cập nội dung từ bất kỳ đâu. Đối với Chromecast Gen 3, bạn cần một gói đăng ký AdGuard VPN
Tùy chọn này chỉ khả dụng với Đăng kí AdGuard VPN
Thiết bị phát trực tuyến Google Chromecast có biểu tượng hình khiên với lỗ khóa.
AdGuard VPN
quá trình tải xuống bắt đầu
Nhấp vào nút có hình mũi tên để bắt đầu cài đặt.
Quét để cài đặt AdGuard VPN trên thiết bị di động của bạn
Màn hình có mã QR để cài đặt AdGuard VPN.