Chrome Extension Proxy Settings API: Build a VPN-Like Extension
Network proxy configuration is one of the most powerful yet underutilized features in Chrome extension development. The Chrome Proxy Settings API enables developers to create extensions that can route browser traffic through proxy servers, similar to how VPN services work. This capability opens up possibilities for building privacy tools, geo-spoofing extensions, corporate network handlers, and traffic management solutions.
In this comprehensive guide, we will explore everything you need to know to build a VPN-like Chrome extension using the Proxy Settings API. From understanding the underlying concepts to implementing real-world features, you will gain the knowledge and practical skills needed to create professional-grade proxy extensions.
Understanding Proxies and Their Role in Web Browsing
Before diving into the API details, it is essential to understand what proxies are and why they matter in the context of browser extensions.
What Is a Proxy Server?
A proxy server acts as an intermediary between your computer and the internet. When you use a proxy, your browser does not connect directly to websites. Instead, it sends requests to the proxy server, which then forwards them to the target website. The response travels back through the proxy before reaching your browser. This process hides your original IP address and allows you to route traffic through different network paths.
There are several types of proxy servers, each serving different purposes:
HTTP Proxies handle HTTP and HTTPS traffic. They are the most common type and work at the application layer. HTTP proxies can inspect and modify HTTP headers, making them useful for content filtering and caching.
SOCKS Proxies operate at a lower level than HTTP proxies and can handle any type of traffic, including FTP, SMTP, and peer-to-peer connections. SOCKS5 is the latest version and supports authentication and IPv6.
Transparent Proxies do not modify client requests or server responses. They are often used by organizations for content filtering without requiring client configuration.
Anonymous Proxies hide your IP address but may identify themselves as proxies. They provide basic privacy without complete anonymity.
Why Use Proxies in Chrome Extensions?
Chrome extensions can leverage proxies for numerous use cases:
-
Privacy and Anonymity: Route traffic through different IP addresses to mask your real location and identity.
-
Geo-Spoofing: Access content restricted to specific regions by appearing to browse from those locations.
-
Corporate Networks: Manage employee access to resources through company-controlled proxy servers.
-
Development and Testing: Test websites from different geographic locations without physically being there.
-
Load Balancing: Distribute traffic across multiple servers to improve performance and reliability.
The Chrome Proxy Settings API Overview
The Chrome Proxy Settings API, part of the chrome.proxy namespace, provides methods to manage Chrome’s proxy settings programmatically. This API is only available to extensions with proper permissions and cannot be used by regular web pages.
Required Permissions
To use the Proxy Settings API, you must declare the appropriate permission in your extension’s manifest.json file:
{
"name": "My Proxy Extension",
"version": "1.0",
"permissions": [
"proxy"
],
"host_permissions": [
"<all_urls>"
]
}
The proxy permission grants access to the chrome.proxy API, while <all_urls> is often necessary because proxy operations can affect any website.
Key API Methods
The chrome.proxy API provides several essential methods for managing proxy settings:
chrome.proxy.settings.set(object details, function callback): This method sets the proxy configuration. You can specify whether to use a fixed proxy server, a PAC (Proxy Auto-Config) script, or direct connections.
chrome.proxy.settings.get(function callback): Retrieves the current proxy settings. This is useful for saving user preferences before making changes.
chrome.proxy.settings.clear(function callback): Removes the extension’s proxy configuration and restores Chrome’s default settings.
Understanding Proxy Modes
The API supports several proxy modes that determine how traffic is routed:
direct: All connections are made directly without a proxy. This is Chrome’s default behavior.
auto_detect: Chrome automatically detects proxy settings using WPAD (Web Proxy Auto-Discovery).
pac_script: Uses a PAC (Proxy Auto-Config) script to determine which proxy to use for each URL.
fixed_servers: Routes all traffic through a fixed proxy server or chain of servers.
system: Uses Chrome’s system proxy settings, which are configured in the operating system.
Building a Complete Proxy Extension
Now that you understand the fundamentals, let us build a complete proxy extension with practical features. We will create an extension that allows users to configure proxies, toggle proxy on and off, and manage multiple proxy profiles.
Project Structure
Create the following file structure:
proxy-extension/
├── manifest.json
├── popup.html
├── popup.js
├── popup.css
├── background.js
├── options.html
├── options.js
└── icons/
├── icon16.png
├── icon48.png
└── icon128.png
Manifest Configuration
Here is a comprehensive manifest.json for our proxy extension:
{
"manifest_version": 3,
"name": "Proxy Master - VPN-Like Extension",
"version": "1.0.0",
"description": "Route your browser traffic through proxy servers with ease. Build VPN-like functionality into your Chrome extension.",
"permissions": [
"proxy",
"storage",
"notifications"
],
"host_permissions": [
"<all_urls>"
],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"background": {
"service_worker": "background.js"
},
"options_page": "options.html",
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}
The Background Service Worker
The background script manages the core proxy functionality and handles extension lifecycle events:
// background.js
// Default proxy configuration
const defaultConfig = {
mode: 'direct',
rules: null
};
// Current active proxy configuration
let currentProxyConfig = defaultConfig;
// Initialize extension
chrome.runtime.onInstalled.addListener(() => {
console.log('Proxy Master extension installed');
// Set up default storage
chrome.storage.local.set({
proxyProfiles: [],
activeProfile: null
});
});
// Apply proxy settings
function applyProxySettings(config) {
return new Promise((resolve, reject) => {
chrome.proxy.settings.set(
{ value: config, scope: 'regular' },
() => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else {
currentProxyConfig = config;
resolve();
}
}
);
});
}
// Clear proxy settings
function clearProxySettings() {
return new Promise((resolve, reject) => {
chrome.proxy.settings.clear(
{ scope: 'regular' },
() => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else {
currentProxyConfig = defaultConfig;
resolve();
}
}
);
});
});
// Handle messages from popup and options pages
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
switch (message.type) {
case 'APPLY_PROXY':
applyProxySettings(message.config)
.then(() => sendResponse({ success: true }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true;
case 'CLEAR_PROXY':
clearProxySettings()
.then(() => sendResponse({ success: true }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true;
case 'GET_STATUS':
chrome.proxy.settings.get(settings => {
sendResponse({
currentConfig: settings.value,
extensionConfig: currentProxyConfig
});
});
return true;
}
});
The Popup Interface
The popup provides the user interface for quick proxy management:
<!-- popup.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Proxy Master</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="container">
<header>
<h1>Proxy Master</h1>
<div class="status-indicator" id="statusIndicator">
<span class="status-dot"></span>
<span class="status-text">Disconnected</span>
</div>
</header>
<main>
<section class="quick-actions">
<button id="toggleProxy" class="btn btn-primary">
Enable Proxy
</button>
<button id="disableProxy" class="btn btn-secondary">
Disable Proxy
</button>
</section>
<section class="proxy-form">
<h2>Configure Proxy</h2>
<div class="form-group">
<label for="proxyType">Proxy Type</label>
<select id="proxyType">
<option value="http">HTTP</option>
<option value="https">HTTPS</option>
<option value="socks4">SOCKS4</option>
<option value="socks5">SOCKS5</option>
</select>
</div>
<div class="form-group">
<label for="proxyHost">Proxy Host</label>
<input type="text" id="proxyHost" placeholder="e.g., 192.168.1.1">
</div>
<div class="form-group">
<label for="proxyPort">Port</label>
<input type="number" id="proxyPort" placeholder="e.g., 8080">
</div>
<div class="form-group">
<label for="proxyUsername">Username (optional)</label>
<input type="text" id="proxyUsername" placeholder="Authentication">
</div>
<div class="form-group">
<label for="proxyPassword">Password (optional)</label>
<input type="password" id="proxyPassword" placeholder="Authentication">
</div>
<button id="saveAndApply" class="btn btn-accent">
Save & Apply
</button>
</section>
<section class="saved-profiles">
<h2>Saved Profiles</h2>
<div id="profilesList"></div>
</section>
</main>
<footer>
<a href="options.html" target="_blank">Advanced Options</a>
</footer>
</div>
<script src="popup.js"></script>
</body>
</html>
Popup Styling
Create an attractive and functional popup interface:
/* popup.css */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
width: 320px;
background: #f5f5f5;
color: #333;
}
.container {
padding: 16px;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e0e0e0;
}
h1 {
font-size: 18px;
font-weight: 600;
color: #1a73e8;
}
h2 {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
color: #555;
}
.status-indicator {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #ccc;
transition: background 0.3s ease;
}
.status-dot.active {
background: #34a853;
}
.status-dot.disconnected {
background: #ea4335;
}
main section {
background: white;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.quick-actions {
display: flex;
gap: 8px;
}
.btn {
flex: 1;
padding: 10px 16px;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-primary {
background: #1a73e8;
color: white;
}
.btn-primary:hover {
background: #1557b0;
}
.btn-secondary {
background: #e8f0fe;
color: #1a73e8;
}
.btn-secondary:hover {
background: #d2e3fc;
}
.btn-accent {
width: 100%;
background: #34a853;
color: white;
margin-top: 8px;
}
.btn-accent:hover {
background: #2d8e47;
}
.form-group {
margin-bottom: 12px;
}
.form-group label {
display: block;
font-size: 12px;
font-weight: 500;
margin-bottom: 4px;
color: #555;
}
.form-group input,
.form-group select {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
transition: border-color 0.2s ease;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #1a73e8;
}
.saved-profiles {
max-height: 150px;
overflow-y: auto;
}
.profile-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border-radius: 4px;
background: #f9f9f9;
margin-bottom: 6px;
cursor: pointer;
transition: background 0.2s ease;
}
.profile-item:hover {
background: #f0f0f0;
}
.profile-info {
font-size: 12px;
}
.profile-name {
font-weight: 500;
}
.profile-details {
color: #777;
font-size: 11px;
}
footer {
text-align: center;
padding-top: 8px;
}
footer a {
font-size: 12px;
color: #1a73e8;
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
Popup Logic
Implement the interactive functionality:
// popup.js
document.addEventListener('DOMContentLoaded', () => {
const toggleBtn = document.getElementById('toggleProxy');
const disableBtn = document.getElementById('disableProxy');
const saveApplyBtn = document.getElementById('saveAndApply');
const statusDot = document.querySelector('.status-dot');
const statusText = document.querySelector('.status-text');
const proxyType = document.getElementById('proxyType');
const proxyHost = document.getElementById('proxyHost');
const proxyPort = document.getElementById('proxyPort');
const proxyUsername = document.getElementById('proxyUsername');
const proxyPassword = document.getElementById('proxyPassword');
const profilesList = document.getElementById('profilesList');
// Check current proxy status
checkProxyStatus();
// Load saved profiles
loadProfiles();
// Toggle proxy on/off
toggleBtn.addEventListener('click', () => {
const config = buildProxyConfig();
if (!config) {
showNotification('Please enter proxy host and port', 'error');
return;
}
applyProxy(config);
});
// Disable proxy
disableBtn.addEventListener('click', () => {
clearProxy();
});
// Save and apply
saveApplyBtn.addEventListener('click', () => {
const config = buildProxyConfig();
if (!config) {
showNotification('Please enter proxy host and port', 'error');
return;
}
// Save to storage
saveProfile(config);
applyProxy(config);
});
function buildProxyConfig() {
const host = proxyHost.value.trim();
const port = proxyPort.value.trim();
if (!host || !port) {
return null;
}
const type = proxyType.value;
const scheme = type.startsWith('socks') ? type : 'https';
const config = {
mode: 'fixed_servers',
rules: {
singleProxy: {
scheme: scheme,
host: host,
port: parseInt(port)
},
bypassList: ['localhost', '127.0.0.1']
}
};
// Add authentication if provided
if (proxyUsername.value && proxyPassword.value) {
// Note: Chrome stores credentials separately
config.rules.singleProxy.username = proxyUsername.value;
config.rules.singleProxy.password = proxyPassword.value;
}
return config;
}
function applyProxy(config) {
chrome.runtime.sendMessage({
type: 'APPLY_PROXY',
config: config
}, response => {
if (response.success) {
updateStatus(true);
showNotification('Proxy enabled successfully', 'success');
} else {
showNotification('Failed to enable proxy: ' + response.error, 'error');
}
});
}
function clearProxy() {
chrome.runtime.sendMessage({
type: 'CLEAR_PROXY'
}, response => {
if (response.success) {
updateStatus(false);
showNotification('Proxy disabled', 'success');
} else {
showNotification('Failed to disable proxy', 'error');
}
});
}
function checkProxyStatus() {
chrome.runtime.sendMessage({ type: 'GET_STATUS' }, response => {
if (response && response.currentConfig) {
const isActive = response.currentConfig.mode === 'fixed_servers';
updateStatus(isActive);
}
});
}
function updateStatus(active) {
if (active) {
statusDot.classList.add('active');
statusDot.classList.remove('disconnected');
statusText.textContent = 'Connected';
toggleBtn.textContent = 'Reconnect';
} else {
statusDot.classList.remove('active');
statusDot.classList.add('disconnected');
statusText.textContent = 'Disconnected';
toggleBtn.textContent = 'Enable Proxy';
}
}
function saveProfile(config) {
const name = proxyHost.value + ':' + proxyPort.value;
const profile = {
name: name,
type: proxyType.value,
host: proxyHost.value,
port: proxyPort.value,
username: proxyUsername.value,
password: proxyPassword.value
};
chrome.storage.local.get(['proxyProfiles'], result => {
const profiles = result.proxyProfiles || [];
profiles.push(profile);
chrome.storage.local.set({ proxyProfiles: profiles }, () => {
loadProfiles();
});
});
}
function loadProfiles() {
chrome.storage.local.get(['proxyProfiles'], result => {
const profiles = result.proxyProfiles || [];
profilesList.innerHTML = '';
profiles.forEach((profile, index) => {
const item = document.createElement('div');
item.className = 'profile-item';
item.innerHTML = `
<div class="profile-info">
<div class="profile-name">${profile.name}</div>
<div class="profile-details">${profile.type.toUpperCase()}</div>
</div>
<button class="btn-apply-profile" data-index="${index}">Apply</button>
`;
item.querySelector('.btn-apply-profile').addEventListener('click', () => {
applyProfile(profile);
});
profilesList.appendChild(item);
});
});
}
function applyProfile(profile) {
const config = {
mode: 'fixed_servers',
rules: {
singleProxy: {
scheme: profile.type === 'socks5' ? 'socks5' :
profile.type === 'socks4' ? 'socks4' : 'https',
host: profile.host,
port: parseInt(profile.port)
},
bypassList: ['localhost', '127.0.0.1']
}
};
if (profile.username && profile.password) {
config.rules.singleProxy.username = profile.username;
config.rules.singleProxy.password = profile.password;
}
applyProxy(config);
}
function showNotification(message, type) {
// Simple notification implementation
console.log(`[${type.toUpperCase()}] ${message}`);
}
});
Advanced Proxy Features
Now that you have a functional proxy extension, let us explore advanced features that can make your extension more powerful.
Proxy Authentication Handling
Proxy authentication adds complexity because Chrome handles credentials differently. When a proxy requires authentication, Chrome will prompt the user with an authentication dialog. For a seamless experience, consider storing credentials in the extension’s secure storage:
// Secure credential handling
async function setupAuthenticatedProxy(host, port, username, password) {
const config = {
mode: 'fixed_servers',
rules: {
singleProxy: {
scheme: 'https',
host: host,
port: parseInt(port)
}
}
};
// Set the proxy configuration
await chrome.proxy.settings.set({ value: config, scope: 'regular' });
// Store credentials for auto-fill (note: not automatic, user must confirm)
chrome.storage.session.set({
pendingAuth: { host, port, username, password }
});
}
PAC Script Configuration
For more complex proxy rules, you can use Proxy Auto-Config (PAC) scripts:
// PAC script configuration
function setupPACScript(proxyHost, proxyPort) {
const pacScript = `
function FindProxyForURL(url, host) {
// Direct connection for local hosts
if (isPlainHostName(host) ||
shExpMatch(host, "localhost.*") ||
isIpAddress(host)) {
return "DIRECT";
}
// Route everything through the proxy
return "PROXY ${proxyHost}:${proxyPort}";
}
`;
const config = {
mode: 'pac_script',
pacScript: {
data: pacScript
}
};
return chrome.proxy.settings.set({ value: config, scope: 'regular' });
}
Handling Proxy Failures
Implement robust error handling for proxy connections:
// Error handling for proxy operations
chrome.proxy.onProxyError.addListener((details) => {
console.error('Proxy error:', details.error);
console.error('URL:', details.url);
// Implement fallback logic
if (details.error === 'net::ERR_PROXY_CONNECTION_FAILED') {
// Try alternative proxy or direct connection
fallbackToDirect();
}
});
function fallbackToDirect() {
chrome.proxy.settings.clear({ scope: 'regular' });
// Notify user
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon128.png',
title: 'Proxy Error',
message: 'Connection failed. Switched to direct connection.'
});
}
Testing Your Proxy Extension
Proper testing is crucial for proxy extensions. Here are the key testing strategies:
Unit Testing
Test individual functions in your extension:
// Test the buildProxyConfig function
function testBuildProxyConfig() {
const testCases = [
{
input: { host: '192.168.1.1', port: '8080', type: 'http' },
expected: { mode: 'fixed_servers', rules: { /* ... */ } }
}
];
testCases.forEach(tc => {
const result = buildProxyConfig(tc.input);
console.assert(
JSON.stringify(result) === JSON.stringify(tc.expected),
'Test failed'
);
});
}
Integration Testing
Test the full flow by enabling the extension and verifying network requests go through the proxy. Use browser developer tools to inspect network traffic and confirm proxy routing.
Cross-Browser Testing
If you plan to support Firefox or other browsers, test the extension with their respective proxy APIs, as they may have different implementations.
Best Practices and Security Considerations
When building proxy extensions, security should be your top priority:
-
Never Store Plain Text Passwords: Use encryption or the chrome.storage.session API for sensitive data.
-
Validate All Input: Always validate proxy hostnames and port numbers before applying settings.
-
Use Minimal Permissions: Only request the permissions your extension actually needs.
-
Implement Clear User Feedback: Always inform users when their traffic is being routed through a proxy.
-
Handle Errors Gracefully: Provide meaningful error messages and fallback options when proxy connections fail.
-
Respect User Privacy: Do not log or transmit user browsing data without explicit consent.
Conclusion
The Chrome Proxy Settings API is a powerful tool for building VPN-like extensions. In this guide, you have learned how to configure proxy settings, implement user interfaces for proxy management, handle authentication, and build robust error handling.
With these skills, you can create extensions that:
- Route browser traffic through custom proxy servers
- Manage multiple proxy profiles for different use cases
- Implement proxy authentication securely
- Handle connection errors gracefully
- Provide excellent user experience with intuitive interfaces
As you continue development, explore additional features like proxy rotation, traffic monitoring, and integration with third-party proxy services. The Chrome proxy API provides a solid foundation for building sophisticated network management tools that can compete with commercial VPN solutions.
Remember to test thoroughly, follow security best practices, and always prioritize user privacy. With the knowledge gained from this guide, you are well-equipped to build professional-grade proxy extensions that provide real value to users.