Menú
ES
AdGuard VPN Blog La extensión de navegador AdGuard VPN ha migrado a Manifest V3

La extensión de navegador AdGuard VPN ha migrado a Manifest V3

Manifest V3, la nueva versión de la API de Chrome, se ha convertido en un gran inconveniente para muchos desarrolladores, ya que introduce cambios significativos que deben ser seguidos por las extensiones de navegador. Esto no solo causa algunos inconvenientes, sino que también resulta en una pérdida parcial de funcionalidades.

En agosto de 2022, AdGuard lanzó la primera extensión de bloqueo de anuncios para navegador basada en Manifest V3. Su desarrollo fue un verdadero desafío: enfrentamos muchos problemas a lo largo del camino. Aun así, logramos demostrar que existía un futuro para el bloqueo de anuncios incluso dentro de la realidad de Manifest V3.

Aunque Google ha extendido la vida útil de las extensiones Manifest V2 hasta enero de 2024, no perdimos tiempo y desarrollamos una extensión de navegador de AdGuard VPN que cumple con las reglas de Manifest V3, y estamos listos para presentarla ahora.

Extensión de navegador AdGuard VPN MV3

Después de desarrollar el bloqueador de anuncios AdGuard MV3, teníamos la certeza de que trabajar en AdGuard VPN MV3 no sería una tarea fácil. Estábamos en lo correcto. Vamos a hablar un poco más sobre los problemas que encontramos y cómo logramos resolverlos.

Service worker suspendido

La versión anterior del Manifesto tenía una página de fondo que permitía que la extensión se ejecutara justo después de la instalación o cuando se abría el navegador, continuando su funcionamiento en segundo plano. En Manifest V3, esta página en segundo plano es reemplazada por el service worker.

Esto permite que las extensiones se ejecuten en segundo plano solo cuando el usuario las necesita. La idea detrás de esto es reducir significativamente el consumo de recursos del sistema. Para extensiones de navegador poco utilizadas, esta es una excelente idea y realmente funciona. Sin embargo, si una extensión necesita ser utilizada constantemente por el usuario, como es el caso de AdGuard VPN, un service worker que se suspende y se activa con frecuencia puede sobrecargar el sistema en lugar de optimizarlo.

Y eso no es todo. Chrome monitorea el service worker. Entonces, si la extensión no realiza ninguna llamada a la API o no registra ningún evento en 30 segundos para evitar que el service worker entre en modo suspendido, el navegador simplemente detiene el script.

Detener el service worker puede llevar a varios errores en la extensión de navegador AdGuard VPN. Por ejemplo, no recibiría la última información sobre las ubicaciones disponibles, la cantidad de tráfico disponible, los servicios excluidos, otros dispositivos conectados a la cuenta y bonificaciones disponibles; no sería posible actualizar las claves de acceso y el icono del menú con el estado de la extensión.

Solución

Mencionamos anteriormente que los eventos y las llamadas a la API evitan que el service worker entre en modo de hibernación. Estas acciones incluyen:

  • Mensajes desde una ventana emergente de la extensión, página de opciones o script de contenido.
  • Solicitud de autenticación del proxy.
  • Apertura de una ventana emergente de la extensión.
  • Apertura de una nueva pestaña en el navegador, actualización de una pestaña existente o cambio de una ventana a otra.
  • Selección de cualquier acción del menú en la página (agregar un sitio a las exclusiones, cambiar el modo de exclusión).
  • Activación de una alarma a través de la API de Alarma (más información sobre esto próximamente).

En resumen, funciona de la siguiente manera: el service worker registra el escuchador de eventos y el navegador registra el evento. Cuando ocurre el evento, el navegador entiende que la extensión necesita ser "despertada" para responder a él. Por lo tanto, despierta al service worker de la extensión y transmite el evento al escuchador.

En el entorno de Manifest V2, no importaba dónde se encontrara el escuchador de eventos, su registro podía producirse de forma asíncrona porque la página en segundo plano siempre estaba funcionando y nunca se reiniciaba.

No Manifest V3, o service worker é reiniciado quando um evento ocorre. Neste caso, para que o evento chegue até o escutador, ele deve ser registrado de forma síncrona no nível superior do código.

En el siguiente ejemplo, puedes ver cómo se encuentran actualmente los escuchadores de mensajes de la ventana emergente, la página de opciones y los scripts de contenido:

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

Este ejemplo ilustra el escuchador de eventos de acción del menú contextual:

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

Esto muestra cómo configurar el escuchador de eventos de acción en pestañas:

// add the listener for tab updated
chrome.tabs.onUpdated.addListener((event) => console.log(event));
// add the listener for tabs 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));

La nueva API de Alarma, que apareció en Manifest V3 en adición a setInterval y setTimeout, también ayuda a evitar algunos errores causados por la hibernación del service worker.

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

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

Implementación de Timer para MV3

Sin embargo, la API de Alarma tiene una limitación un tanto inconveniente: es imposible establecer un temporizador que funcione en 3-5-10 segundos. El mínimo es de 1 minuto o más.

Despertando al service worker

Cuando el service worker entra en modo de hibernación, descarga los datos almacenados en la RAM. Por lo tanto, después de despertar, lleva un poco de tiempo inicializar los módulos de la extensión, aproximadamente 1,5-2 segundos en promedio. Como se puede ver, cuando la página en segundo plano todavía existía, este problema no ocurría, ya que solo se recargaba cuando se reiniciaba el navegador.

Solución

Hemos agregado un módulo para almacenar cada cambio de estado en la RAM. El último estado guardado es utilizado por AdGuard VPN cuando el service worker se despierta para una recuperación rápida. Esto reduce el retraso en la reinicialización del service worker a 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>;
}

Repositorio de la interfaz del estado de la extensión

Implementar esta solución requirió el movimiento de campos mutables de las clases al estado de almacenamiento. En su lugar, agregamos getters y setters a las clases.

Por ejemplo, el API Fallback era así:

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

Y ahora está así:

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

Aquí tienes otros ejemplos:

class Endpoints implements EndpointsInterface {
   vpnInfo?: VpnExtensionInfoInterface;
   ...
}

Añadir un estado a la clase Endpoints en 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;
   }
}

Añadir un estado a la clase Endpoints en Manifest V3

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

Añadir un estado a la clase Credentials en 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();
   }
}

Añadir un estado a la clase Credentials en Manifest V3

Implementación de NetworkConnectionObserver

NetworkConnectionObserver es una clase que monitorea el estado de la red (online/offline). Se utiliza cuando la aplicación no recibe una respuesta del servidor y ayuda a identificar la causa del problema: falta de conexión a Internet por parte del usuario o algún problema del lado del servidor. Además, NetworkConnectionObserver monitorea la transición del usuario de offline a online. En este caso, se verifica los derechos de acceso y se restaura la conexión si se había establecido antes de que el usuario quedara sin conexión.

En MV2, NetworkConnectionObserver se implementa de manera muy simple. Escucha el evento online y se comunica con la callback que se le pasó.

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

Dentro de MV3, el service worker no tiene acceso a window, por lo que su implementación es más complicada. Una solución sería utilizar un documento fuera de pantalla (offscreen). Esto crearía una página invisible con acceso a window y document, y la capacidad de realizar acciones allí que no están disponibles para el service worker.

Sin embargo, Google promete que en el futuro, el documento fuera de pantalla entrará en modo de hibernación después de un período de inactividad, similar al service worker. Esto significa que NetworkConnectionObserver no podrá escuchar el evento online/offline todo el tiempo, lo que puede llevar a varios problemas.

Ahora, necesitamos utilizar una solución menos sutil: verificamos el estado de navigator.online cada medio segundo y llamamos a la callback si hay un cambio de offline a 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;
}

Problema de compatibilidad con Websocket

Ahora, en el Manifest V3, los mensajes recibidos por la extensión sobre una conexión websocket no despertarán al service worker en estado de hibernación. Como resultado, la extensión puede perder el mensaje sobre la necesidad de actualizar las claves de acceso al servidor backend y puede no poder manejar mensajes de error sobre el límite de tráfico alcanzado, límite de dispositivos conectados y dominios no enrutados.

Solución

La primera solución que consideramos fue ir en contra de la visión de Chrome y crear un service worker que no entra en modo de hibernación. Incluso llegamos a implementarlo. Sin embargo, luego decidimos seguir las reglas del Manifest V3 y conectar al websocket solo cuando el service worker esté activo.

Aunque hemos encontrado una solución, realmente esperamos que Google resuelva el problema que hemos reportado y agregue la posibilidad de extender la vida del service worker con mensajes recibidos a través de la conexión websocket.

Activación anticipada en el controlador de eventos onAuthRequired

También hemos resuelto un problema relacionado con la aparición de una ventana emergente en el navegador que solicita autorización de proxy. Esto ocurría cuando la sesión de almacenamiento no podía cargar las credenciales antes de que el controlador de eventos onAuthRequired se descartara después de la activación del service worker.

Solución

Para resolver este problema, hemos agregado un callback asíncrono al controlador onAuthRequired. Ahora está configurado para esperar hasta que la sesión de almacenamiento haya cargado las credenciales e insertadolas.

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

Controlador onAuthRequired con una devolución de llamada sincrónica

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

Controlador onAuthRequired con una devolución de llamada asíncrona

En conclusión

El Manifest V3 está lejos de ser un entorno estable e inteligente. Sin embargo, ya puedes instalar la extensión de navegador AdGuard VPN MV3 y probarla. Como siempre, agradecemos tus comentarios: si encuentras algún error, puedes informarlo en GitHub.

¿Te gustó esta publicación?

AdGuard VPN
para Windows

Utiliza cualquier navegador o aplicación y nunca te preocupe por tu anonimato de nuevo. El mundo entero está a tu alcance con AdGuard VPN.
Más información
Descargar
Al descargar el programa, aceptas los términos del acuerdo de licencia

AdGuard VPN
para Mac

En solo dos clics, selecciona una ciudad de cualquier parte del mundo, tenemos 65+ ubicaciones, y tus datos son invisibles a las miradas indiscretas de empresas y gobiernos.
Más información
Descargar
Al descargar el programa, aceptas los términos del acuerdo de licencia

AdGuard VPN
para iOS

Refuerza tu protección en línea llevándola contigo a donde vayas. Utiliza AdGuard VPN para disfrutar de tus películas y programas favoritos.
Más información
App Store
Al descargar el programa, aceptas los términos del acuerdo de licencia

AdGuard VPN
para Android

¡Mantén el anonimato allá donde vayas con AdGuard VPN! Docenas de ubicaciones, conexión rápida y confiable, todo en tu bolsillo.
Más información
Google Play
Al descargar el programa, aceptas los términos del acuerdo de licencia
Descargar
Al descargar el programa, aceptas los términos del acuerdo de licencia

AdGuard VPN
para Chrome

Oculta tu verdadera ubicación y emerge desde otro lugar del mundo: accede a cualquier contenido sin límites de velocidad y mantén tu anonimato en la red.
Más información
Instalar
Al descargar el programa, aceptas los términos del acuerdo de licencia

AdGuard VPN
para Edge

Ve a otra ubicación con un solo clic, oculta tu IP y haz que tu navegación por Internet sea segura y anónima.
Más información
Instalar
Al descargar el programa, aceptas los términos del acuerdo de licencia

AdGuard VPN
para Firefox

Protege tu privacidad, oculta tu ubicación real y elige dónde necesitas la VPN y dónde no.
Más información
Instalar
Al descargar el programa, aceptas los términos del acuerdo de licencia

AdGuard VPN
para Opera

Sé un ninja en tu navegador Opera: muévete rápidamente a cualquier parte del mundo y pasa desapercibido.
Más información
Instalar
Al descargar el programa, aceptas los términos del acuerdo de licencia
La descarga de AdGuard VPN
ha comenzado
Haz clic en el botón indicado por la flecha para iniciar la instalación.
Escanear para instalar AdGuard VPN en su dispositivo móvil