Chrome Extension Proxy Settings API Guide: Build VPN and Proxy Extensions

24 min read

Chrome Extension Proxy Settings API Guide: Build VPN and Proxy Extensions

Chrome Extension Proxy Settings API Guide: Build VPN and Proxy Extensions

The Chrome Proxy Settings API is one of the most powerful yet underutilized APIs available to extension developers. This comprehensive guide will walk you through everything you need to know to build sophisticated proxy and VPN-like extensions using Manifest V3. Whether you’re looking to create a simple proxy switcher or a full-featured VPN extension, this guide covers the essential concepts, implementation patterns, and best practices.

Understanding how to properly implement proxy functionality in Chrome extensions opens up numerous possibilities. From building privacy-focused extensions that route traffic through different servers to creating enterprise tools that manage corporate network configurations, the chrome.proxy API provides the foundation you need. In this guide, we’ll explore the full capabilities of this API, including the Proxy Config API, PAC (Proxy Auto-Config) scripts, and the various configuration modes available to developers.


Understanding Chrome’s Proxy API Architecture

Chrome provides the chrome.proxy API specifically for managing proxy settings within extensions. This API allows your extension to programmatically control how Chrome connects to the internet, enabling you to route browser traffic through proxy servers or implement sophisticated routing logic. The API is powerful but requires careful implementation to ensure proper functionality and user privacy.

The Proxy API Namespace

The chrome.proxy API consists of several key methods and events that work together to provide complete proxy management capabilities. Understanding these components is essential before diving into implementation. The primary methods include chrome.proxy.settings.set() for applying proxy configurations, chrome.proxy.settings.get() for retrieving current settings, and chrome.proxy.onProxyError for handling connection errors gracefully.

The API supports multiple proxy configuration modes, each suited for different use cases. Direct connection mode bypasses the proxy entirely, which is useful for certain network configurations. Manual proxy mode allows you to specify individual proxy servers for different protocols. PAC (Proxy Auto-Config) mode enables dynamic proxy selection through JavaScript functions. Finally, auto-detect mode uses WPAD (Web Proxy Auto-Discovery) to automatically find proxy configuration.

Required Permissions

Before you can use the chrome.proxy API, you must declare the appropriate permissions in your extension’s manifest.json file. The permission requirement is "proxy", which grants your extension access to the proxy management functionality. Additionally, you’ll need host permissions for any servers your extension will connect to during its operation.

Here is a sample manifest configuration for a proxy extension:

{
  "name": "My Proxy Extension",
  "version": "1.0",
  "manifest_version": 3,
  "permissions": [
    "proxy",
    "storage",
    "tabs"
  ],
  "host_permissions": [
    "<all_urls>"
  ],
  "action": {
    "default_popup": "popup.html"
  }
}

Note that in Manifest V3, the permissions structure differs from V2. The "proxy" permission goes in the "permissions" array, while broad host access for handling all URLs through the proxy goes in "host_permissions". This separation provides better security transparency to users during installation.


Proxy Configuration Modes Explained

Understanding the different proxy configuration modes is crucial for choosing the right approach for your extension. Each mode has distinct advantages and limitations that affect how traffic is routed and how flexible your proxy implementation can be.

Direct Connection Mode

Direct mode is the simplest configuration, where Chrome connects to destinations without any proxy intermediary. While this might seem counterintuitive for a proxy extension, direct mode serves important purposes. You might use it as a fallback when proxy connections fail, or when users want to temporarily disable proxy usage. The configuration is straightforward:

const directConfig = {
  mode: "direct"
};

chrome.proxy.settings.set(
  { value: directConfig },
  () => console.log('Direct mode enabled')
);

This configuration essentially removes any proxy settings, allowing Chrome to make direct connections. When implementing a full-featured proxy extension, you’ll likely toggle between this mode and your custom proxy configurations based on user preferences or automatic rules.

Manual Proxy Configuration

Manual proxy mode allows you to specify exact proxy server details for each protocol. This mode is ideal when you have specific proxy servers you want to use consistently, such as corporate proxies or pre-configured proxy services. Chrome supports separate proxy configurations for HTTP, HTTPS, FTP protocols, and a SOCKS proxy for general traffic.

const manualProxyConfig = {
  mode: "fixed_servers",
  rules: {
    proxyForHttp: {
      scheme: "http",
      host: "proxy.example.com",
      port: 8080
    },
    proxyForHttps: {
      scheme: "http",
      host: "proxy.example.com",
      port: 8080
    },
    bypassList: ["localhost", "127.0.0.1", "*.local"]
  }
};

chrome.proxy.settings.set(
  { value: manualProxyConfig },
  function() {
    if (chrome.runtime.lastError) {
      console.error(chrome.runtime.lastError);
    } else {
      console.log('Manual proxy configured successfully');
    }
  }
);

The bypassList parameter is particularly useful for excluding certain domains or IP addresses from proxy routing. This is important for local development, corporate intranets, or any resources that should be accessed directly without going through the proxy.

PAC Script Mode

Proxy Auto-Config (PAC) scripts provide the most flexible approach to proxy configuration. A PAC script is a JavaScript function that determines the appropriate proxy server for each URL. This enables sophisticated routing logic based on domain patterns, time of day, or any other criteria you can express in JavaScript.

const pacScriptConfig = {
  mode: "pac_script",
  pacScript: {
    data: `
      function FindProxyForURL(url, host) {
        // Direct connection for local addresses
        if (isPlainHostName(host) ||
            shExpMatch(host, "*.local") ||
            isInNet(dnsResolve(host), "10.0.0.0", "255.0.0.0") ||
            isInNet(dnsResolve(host), "172.16.0.0", "255.240.0.0") ||
            isInNet(dnsResolve(host), "192.168.0.0", "255.255.0.0")) {
          return "DIRECT";
        }

        // Use proxy for all other requests
        return "PROXY proxy.example.com:8080; DIRECT";
      }
    `
  }
};

chrome.proxy.settings.set(
  { value: pacScriptConfig },
  () => console.log('PAC script configured')
);

PAC scripts support several predefined JavaScript functions including isPlainHostName(), shExpMatch() for shell pattern matching, isInNet() for IP range checking, and dnsResolve() for DNS lookups. These functions enable complex routing logic that can adapt to different network conditions automatically.


Building a Complete Proxy Extension

Now let’s put together all the concepts into a functional proxy extension. We’ll create a popup-based extension that allows users to toggle proxy settings, select from predefined servers, and persist their preferences.

Background Service Worker Implementation

The background script serves as the central controller for your proxy extension. It handles the core logic of setting proxy configurations and managing user preferences.

// background.js
const DEFAULT_PROXY = {
  scheme: "http",
  host: "proxy.example.com",
  port: 8080
};

let currentProxy = null;

// Initialize extension state from storage
chrome.storage.local.get(['proxyConfig', 'enabled'], (result) => {
  if (result.enabled && result.proxyConfig) {
    applyProxyConfig(result.proxyConfig);
  }
});

function applyProxyConfig(config) {
  const proxyConfig = {
    mode: "fixed_servers",
    rules: {
      proxyForHttp: config,
      proxyForHttps: config,
      bypassList: config.bypassList || []
    }
  };

  chrome.proxy.settings.set(
    { value: proxyConfig },
    () => {
      currentProxy = config;
      // Notify all tabs of the change
      chrome.tabs.query({}, (tabs) => {
        tabs.forEach(tab => {
          chrome.tabs.sendMessage(tab.id, {
            type: 'PROXY_CHANGED',
            proxy: config
          }).catch(() => {});
        });
      });
    }
  );
}

// Handle messages from popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  switch (message.type) {
    case 'SET_PROXY':
      applyProxyConfig(message.config);
      // Save to storage
      chrome.storage.local.set({
        proxyConfig: message.config,
        enabled: true
      });
      sendResponse({ success: true });
      break;

    case 'DISABLE_PROXY':
      chrome.proxy.settings.set(
        { value: { mode: "direct" } },
        () => {
          chrome.storage.local.set({ enabled: false });
          sendResponse({ success: true });
        }
      );
      break;

    case 'GET_STATUS':
      chrome.proxy.settings.get({}, (config) => {
        sendResponse({
          enabled: config.value.mode !== "direct",
          config: currentProxy
        });
      });
      break;
  }

  return true; // Keep message channel open for async response
});

The background script uses chrome.storage.local to persist user preferences across browser sessions. This ensures that when users restart Chrome, their proxy settings are automatically restored. The message passing system allows communication between the popup and background script, enabling real-time UI updates when proxy status changes.

The popup provides the user interface for interacting with your proxy extension. It should offer intuitive controls for enabling/disabling the proxy and selecting server configurations.

<!-- popup.html -->
<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      width: 300px;
      padding: 16px;
      font-family: -apple-system, BlinkMacSystemFont, sans-serif;
    }
    .header {
      font-size: 18px;
      font-weight: bold;
      margin-bottom: 16px;
    }
    .toggle-container {
      display: flex;
      align-items: center;
      margin-bottom: 16px;
    }
    .toggle {
      margin-right: 12px;
    }
    .server-list {
      border: 1px solid #ddd;
      border-radius: 6px;
      max-height: 200px;
      overflow-y: auto;
    }
    .server-item {
      padding: 10px;
      cursor: pointer;
      border-bottom: 1px solid #eee;
    }
    .server-item:hover {
      background: #f5f5f5;
    }
    .server-item.selected {
      background: #e8f0fe;
    }
    .server-name {
      font-weight: 500;
    }
    .server-details {
      font-size: 12px;
      color: #666;
    }
    .status {
      margin-top: 12px;
      padding: 8px;
      border-radius: 4px;
      font-size: 12px;
    }
    .status.connected {
      background: #e6f4ea;
      color: #137333;
    }
    .status.disconnected {
      background: #fce8e6;
      color: #c5221f;
    }
  </style>
</head>
<body>
  <div class="header">Proxy Settings</div>
  
  <div class="toggle-container">
    <input type="checkbox" id="proxyToggle" class="toggle">
    <label for="proxyToggle">Enable Proxy</label>
  </div>

  <div class="server-list" id="serverList">
    <!-- Server items populated by JavaScript -->
  </div>

  <div class="status" id="status">Disconnected</div>

  <script src="popup.js"></script>
</body>
</html>
// popup.js
const proxyServers = [
  { name: "US Server 1", host: "us1.proxy.example.com", port: 8080 },
  { name: "US Server 2", host: "us2.proxy.example.com", port: 8080 },
  { name: "Europe Server", host: "eu.proxy.example.com", port: 8080 },
  { name: "Asia Server", host: "asia.proxy.example.com", port: 8080 }
];

let selectedServer = null;

// Initialize popup
document.addEventListener('DOMContentLoaded', () => {
  renderServerList();
  updateStatus();
  
  document.getElementById('proxyToggle').addEventListener('change', handleToggle);
});

function renderServerList() {
  const list = document.getElementById('serverList');
  list.innerHTML = '';

  proxyServers.forEach((server, index) => {
    const item = document.createElement('div');
    item.className = 'server-item';
    item.innerHTML = `
      <div class="server-name">${server.name}</div>
      <div class="server-details">${server.host}:${server.port}</div>
    `;
    
    item.addEventListener('click', () => selectServer(server, item));
    list.appendChild(item);
  });
}

function selectServer(server, element) {
  // Update UI
  document.querySelectorAll('.server-item').forEach(el => {
    el.classList.remove('selected');
  });
  element.classList.add('selected');
  
  selectedServer = server;
  
  // Apply proxy configuration
  if (document.getElementById('proxyToggle').checked) {
    chrome.runtime.sendMessage({
      type: 'SET_PROXY',
      config: {
        scheme: "http",
        host: server.host,
        port: server.port,
        bypassList: []
      }
    });
  }
}

function handleToggle(event) {
  const enabled = event.target.checked;
  
  if (enabled) {
    if (!selectedServer) {
      selectedServer = proxyServers[0];
    }
    
    chrome.runtime.sendMessage({
      type: 'SET_PROXY',
      config: {
        scheme: "http",
        host: selectedServer.host,
        port: selectedServer.port
      }
    });
  } else {
    chrome.runtime.sendMessage({ type: 'DISABLE_PROXY' });
  }
  
  updateStatus();
}

function updateStatus() {
  const status = document.getElementById('status');
  const enabled = document.getElementById('proxyToggle').checked;
  
  if (enabled) {
    status.textContent = `Connected via ${selectedServer?.name || 'proxy'}`;
    status.className = 'status connected';
  } else {
    status.textContent = 'Disconnected';
    status.className = 'status disconnected';
  }
}

Handling Proxy Errors and Fallbacks

Robust error handling is essential for any production-quality proxy extension. Users will inevitably encounter connection issues, and your extension should handle these gracefully while keeping users informed.

Error Detection and Reporting

// error-handler.js
chrome.proxy.onProxyError.addListener((details) => {
  console.error('Proxy error:', details.error);
  
  // Send error notification to popup if open
  chrome.runtime.sendMessage({
    type: 'PROXY_ERROR',
    error: details.error,
    fatal: details.fatal
  }).catch(() => {});
  
  // Log errors for debugging
  chrome.storage.local.get(['debugMode'], (result) => {
    if (result.debugMode) {
      const errorLog = {
        timestamp: new Date().toISOString(),
        error: details.error,
        fatal: details.fatal
      };
      
      chrome.storage.local.get(['errors'], (storage) => {
        const errors = storage.errors || [];
        errors.push(errorLog);
        // Keep last 100 errors
        if (errors.length > 100) {
          errors.shift();
        }
        chrome.storage.local.set({ errors });
      });
    }
  });
});

The onProxyError listener receives detailed information about connection failures. The fatal flag indicates whether Chrome had to fall back to a direct connection. For non-fatal errors, your extension can attempt retries or notify the user. Fatal errors typically indicate configuration problems that require user intervention.

Implementing Automatic Fallbacks

For a more resilient implementation, consider adding automatic fallback logic that attempts alternative connections when primary proxies fail.

// fallback-handler.js
class ProxyFallbackManager {
  constructor(servers) {
    this.servers = servers;
    this.currentIndex = 0;
    this.retryCount = 0;
    this.maxRetries = 3;
  }

  async tryNextProxy() {
    if (this.retryCount >= this.maxRetries) {
      console.error('All proxy retries exhausted');
      this.notifyUser('All proxy servers failed. Switching to direct connection.');
      await this.setDirectMode();
      return false;
    }

    this.currentIndex = (this.currentIndex + 1) % this.servers.length;
    const nextServer = this.servers[this.currentIndex];
    
    console.log(`Retrying with ${nextServer.name}`);
    await this.applyProxy(nextServer);
    this.retryCount++;
    
    return true;
  }

  async applyProxy(server) {
    const config = {
      mode: "fixed_servers",
      rules: {
        proxyForHttp: server,
        proxyForHttps: server
      }
    };

    return new Promise((resolve, reject) => {
      chrome.proxy.settings.set({ value: config }, () => {
        if (chrome.runtime.lastError) {
          reject(chrome.runtime.lastError);
        } else {
          resolve();
        }
      });
    });
  }

  async setDirectMode() {
    return new Promise((resolve) => {
      chrome.proxy.settings.set({ value: { mode: "direct" } }, resolve);
    });
  }

  notifyUser(message) {
    chrome.runtime.sendMessage({
      type: 'SHOW_NOTIFICATION',
      message: message
    }).catch(() => {});
  }
}

VPN Chrome Extension Implementation

Building a VPN-like extension requires additional considerations beyond basic proxy functionality. While true VPN implementations require kernel-level access outside the browser, you can create a functional “VPN-like” experience using Chrome’s proxy API combined with proper encryption and server infrastructure.

Architecture Overview

A VPN-style Chrome extension typically consists of three main components: the extension UI and logic, a proxy server infrastructure, and optional encryption layer. The extension routes all browser traffic through your proxy servers, effectively creating a secure tunnel for web browsing.

The key difference between a simple proxy extension and a VPN-style extension lies in the comprehensiveness of traffic routing. While basic proxy settings only affect browser traffic, VPN-style implementations often include additional features like DNS leak prevention, kill switches, and traffic encryption.

Advanced Features for VPN Extensions

When building VPN-like Chrome extensions, consider implementing these advanced features for a more complete user experience:

// vpn-features.js
class VPNFeatures {
  // DNS leak prevention
  static async enableDnsLeakProtection(dnsServer) {
    const config = {
      mode: "fixed_servers",
      rules: {
        proxyForHttp: this.currentServer,
        proxyForHttps: this.currentServer,
        proxyForFtp: this.currentServer,
        bypassList: [] // Ensure all traffic goes through proxy
      }
    };
    
    // Force DNS resolution through proxy
    await chrome.proxy.settings.set({
      value: config,
      scope: 'regular'
    });
  }

  // Kill switch - block traffic if proxy fails
  static async enableKillSwitch() {
    // Store original settings
    await chrome.storage.local.set({
      originalSettings: await this.getCurrentSettings()
    });

    chrome.proxy.onProxyError.addListener(async (details) => {
      if (details.fatal) {
        // Block all traffic by setting invalid proxy
        await chrome.proxy.settings.set({
          value: {
            mode: "fixed_servers",
            rules: {
              proxyForHttp: { scheme: "invalid", host: "localhost", port: 0 }
            }
          }
        });
        
        // Notify user
        this.showNotification('Connection lost. Traffic blocked for security.');
      }
    });
  }

  // Traffic monitoring
  static async getBandwidthUsage() {
    // Implementation would connect to backend API
    // to track actual bandwidth through the proxy
    return new Promise((resolve) => {
      chrome.storage.local.get(['bandwidth'], (result) => {
        resolve(result.bandwidth || { uploaded: 0, downloaded: 0 });
      });
    });
  }
}

Security Best Practices

When implementing proxy functionality in Chrome extensions, security should be your top priority. Users trust your extension with their network traffic, and any vulnerabilities could have serious consequences.

Secure Proxy Server Communication

Always use HTTPS for proxy connections to prevent man-in-the-middle attacks. Never transmit credentials or sensitive data over unencrypted connections. If your proxy servers require authentication, use secure credential storage mechanisms and never hardcode credentials in your extension code.

// secure-auth.js
async function authenticateWithProxy(credentials) {
  // Use chrome.storage.session for sensitive, temporary storage
  // or chrome.storage.local with encryption for persistence
  await chrome.storage.session.set({
    proxyAuth: {
      username: credentials.username,
      // In production, use proper encryption
      token: await encrypt(credentials.password)
    }
  });
}

function getProxyAuth() {
  return chrome.storage.session.get(['proxyAuth']);
}

User Privacy Considerations

Be transparent about what data your extension collects and how it’s used. Avoid collecting unnecessary user data, and implement clear data retention policies. When implementing VPN-like features, ensure you’re not logging user browsing activity beyond what’s necessary for service operation.


Testing Your Proxy Extension

Proper testing is crucial for proxy extensions due to the complexity of network configurations and the potential impact on user browsing experience.

Local Testing Strategies

Test your extension thoroughly with various proxy configurations, network conditions, and error scenarios. Use Chrome’s developer tools to monitor network requests and verify traffic is being routed correctly.

// testing-utilities.js
async function verifyProxyConnection(proxyConfig) {
  // Test by making a request through the proxy
  try {
    const response = await fetch('https://httpbin.org/ip', {
      proxy: `http://${proxyConfig.host}:${proxyConfig.port}`,
      credentials: 'include'
    });
    
    const data = await response.json();
    console.log('Proxy working, IP:', data.origin);
    return true;
  } catch (error) {
    console.error('Proxy test failed:', error);
    return false;
  }
}

Conclusion

The Chrome Proxy Settings API provides powerful capabilities for building sophisticated proxy and VPN-like extensions. By understanding the different configuration modes, implementing proper error handling, and following security best practices, you can create reliable extensions that enhance user privacy and enable flexible network configurations.

Remember that successful proxy extensions require careful attention to user experience, robust error handling, and ongoing maintenance as network conditions and Chrome’s APIs evolve. Start with basic functionality, test thoroughly, and progressively add advanced features as you validate your core implementation.

The key to success lies in balancing functionality with simplicity. Users appreciate extensions that work reliably without requiring constant attention. By following the patterns and best practices outlined in this guide, you’re well-equipped to build professional-quality proxy extensions that serve your users effectively.


Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.

No previous article
No next article