Building a Chrome Extension for Proxy Switching
Switching proxies in Chrome normally means diving into system settings or using PAC files. A custom Chrome extension puts proxy switching in your toolbar — one click to switch between profiles, automatic proxy assignment per domain, and visual indicators showing your current proxy status.
This tutorial builds a complete proxy switching extension using Chrome’s Manifest V3 API.
Features
- One-click proxy switching from toolbar popup
- Multiple proxy profiles (home, work, country-specific)
- Auto-switch rules by domain pattern
- Current IP display in popup
- Connection status indicator
- Import/export proxy configurations
- SOCKS5 and HTTP/HTTPS proxy support
Extension Structure
proxy-switcher/
├── manifest.json
├── background.js
├── popup/
│ ├── popup.html
│ ├── popup.css
│ └── popup.js
├── options/
│ ├── options.html
│ └── options.js
├── icons/
│ ├── icon16.png
│ ├── icon48.png
│ └── icon128.png
└── utils/
└── proxy.jsManifest
{
"manifest_version": 3,
"name": "Proxy Switcher Pro",
"version": "1.0.0",
"description": "Switch proxies with one click. Manage profiles and auto-switch rules.",
"permissions": [
"proxy",
"storage",
"webRequest",
"tabs",
"activeTab"
],
"host_permissions": ["<all_urls>"],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup/popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"options_page": "options/options.html",
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}Background Service Worker
The background script manages proxy configuration through Chrome’s proxy API.
// background.js
// Default profiles
const DEFAULT_PROFILES = {
direct: {
name: 'Direct (No Proxy)',
mode: 'direct',
color: '#4CAF50',
},
};
// Apply proxy configuration
async function applyProxy(profile) {
if (profile.mode === 'direct') {
await chrome.proxy.settings.set({
value: { mode: 'direct' },
scope: 'regular',
});
updateBadge('OFF', '#4CAF50');
return;
}
if (profile.mode === 'fixed_servers') {
const config = {
mode: 'fixed_servers',
rules: {
singleProxy: {
scheme: profile.scheme || 'http',
host: profile.host,
port: profile.port,
},
bypassList: profile.bypassList || [
'localhost',
'127.0.0.1',
'*.local',
],
},
};
await chrome.proxy.settings.set({
value: config,
scope: 'regular',
});
updateBadge('ON', profile.color || '#2196F3');
}
if (profile.mode === 'pac_script') {
await chrome.proxy.settings.set({
value: {
mode: 'pac_script',
pacScript: { data: profile.pacScript },
},
scope: 'regular',
});
updateBadge('PAC', '#FF9800');
}
}
function updateBadge(text, color) {
chrome.action.setBadgeText({ text });
chrome.action.setBadgeBackgroundColor({ color });
}
// Handle proxy authentication
chrome.webRequest.onAuthRequired.addListener(
(details, callback) => {
chrome.storage.local.get(['activeProfile', 'profiles'], (data) => {
const profile = data.profiles?.[data.activeProfile];
if (profile?.username && profile?.password) {
callback({
authCredentials: {
username: profile.username,
password: profile.password,
},
});
} else {
callback({});
}
});
},
{ urls: ['<all_urls>'] },
['asyncBlocking']
);
// Auto-switch rules
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (changeInfo.url) {
const data = await chrome.storage.local.get(['rules', 'profiles']);
const rules = data.rules || [];
const profiles = data.profiles || {};
for (const rule of rules) {
if (matchesRule(changeInfo.url, rule.pattern)) {
const profile = profiles[rule.profileId];
if (profile) {
await applyProxy(profile);
break;
}
}
}
}
});
function matchesRule(url, pattern) {
try {
if (pattern.startsWith('/') && pattern.endsWith('/')) {
const regex = new RegExp(pattern.slice(1, -1));
return regex.test(url);
}
return url.includes(pattern);
} catch {
return false;
}
}
// Listen for messages from popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'setProxy') {
chrome.storage.local.get(['profiles'], async (data) => {
const profile = data.profiles?.[message.profileId];
if (profile) {
await applyProxy(profile);
await chrome.storage.local.set({
activeProfile: message.profileId,
});
sendResponse({ success: true });
}
});
return true;
}
if (message.action === 'getStatus') {
chrome.proxy.settings.get({ incognito: false }, (config) => {
sendResponse({ config });
});
return true;
}
});
// Initialize on install
chrome.runtime.onInstalled.addListener(async () => {
const data = await chrome.storage.local.get(['profiles']);
if (!data.profiles) {
await chrome.storage.local.set({
profiles: DEFAULT_PROFILES,
activeProfile: 'direct',
rules: [],
});
}
updateBadge('OFF', '#4CAF50');
});Popup Interface
<!-- popup/popup.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="container">
<h2>Proxy Switcher</h2>
<div id="current-status" class="status-bar">
<span id="status-text">Loading...</span>
<span id="current-ip" class="ip-display"></span>
</div>
<div id="profiles-list" class="profiles"></div>
<div class="actions">
<button id="add-profile" class="btn btn-add">+ Add Profile</button>
<button id="open-options" class="btn btn-settings">Settings</button>
</div>
<div id="add-form" class="form hidden">
<input id="profile-name" placeholder="Profile Name" />
<select id="profile-scheme">
<option value="http">HTTP</option>
<option value="https">HTTPS</option>
<option value="socks5">SOCKS5</option>
<option value="socks4">SOCKS4</option>
</select>
<input id="profile-host" placeholder="Host (e.g., proxy.example.com)" />
<input id="profile-port" placeholder="Port (e.g., 8080)" type="number" />
<input id="profile-user" placeholder="Username (optional)" />
<input id="profile-pass" placeholder="Password (optional)" type="password" />
<div class="form-actions">
<button id="save-profile" class="btn btn-save">Save</button>
<button id="cancel-add" class="btn btn-cancel">Cancel</button>
</div>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>/* popup/popup.css */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 320px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #1a1a2e;
color: #eee;
}
.container { padding: 16px; }
h2 { font-size: 16px; margin-bottom: 12px; color: #e94560; }
.status-bar {
background: #16213e;
padding: 10px 12px;
border-radius: 6px;
margin-bottom: 12px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
}
.ip-display { color: #0f3460; font-family: monospace; font-size: 12px; }
.profile-item {
background: #16213e;
padding: 10px 12px;
margin-bottom: 6px;
border-radius: 6px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: background 0.2s;
border-left: 3px solid transparent;
}
.profile-item:hover { background: #1a2744; }
.profile-item.active { border-left-color: #e94560; background: #1a2744; }
.profile-item .name { font-size: 13px; font-weight: 500; }
.profile-item .info { font-size: 11px; color: #888; }
.profile-item .delete { color: #666; cursor: pointer; padding: 2px 6px; }
.profile-item .delete:hover { color: #e94560; }
.btn {
padding: 8px 14px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
margin-right: 6px;
}
.btn-add { background: #e94560; color: white; }
.btn-settings { background: #333; color: #ccc; }
.btn-save { background: #4CAF50; color: white; }
.btn-cancel { background: #666; color: white; }
.actions { margin-top: 12px; }
.form {
margin-top: 12px;
background: #16213e;
padding: 12px;
border-radius: 6px;
}
.form input, .form select {
width: 100%;
padding: 8px;
margin-bottom: 8px;
background: #0f3460;
border: 1px solid #333;
color: #eee;
border-radius: 4px;
font-size: 12px;
}
.form-actions { display: flex; gap: 8px; }
.hidden { display: none; }// popup/popup.js
document.addEventListener('DOMContentLoaded', async () => {
await loadProfiles();
await checkCurrentIP();
document.getElementById('add-profile').addEventListener('click', () => {
document.getElementById('add-form').classList.toggle('hidden');
});
document.getElementById('cancel-add').addEventListener('click', () => {
document.getElementById('add-form').classList.add('hidden');
});
document.getElementById('save-profile').addEventListener('click', saveProfile);
document.getElementById('open-options').addEventListener('click', () => {
chrome.runtime.openOptionsPage();
});
});
async function loadProfiles() {
const data = await chrome.storage.local.get(['profiles', 'activeProfile']);
const profiles = data.profiles || {};
const active = data.activeProfile || 'direct';
const container = document.getElementById('profiles-list');
container.innerHTML = '';
for (const [id, profile] of Object.entries(profiles)) {
const div = document.createElement('div');
div.className = `profile-item ${id === active ? 'active' : ''}`;
div.innerHTML = `
<div>
<div class="name">${profile.name}</div>
<div class="info">${profile.mode === 'direct' ? 'No proxy' : `${profile.scheme || 'http'}://${profile.host}:${profile.port}`}</div>
</div>
${id !== 'direct' ? `<span class="delete" data-id="${id}">×</span>` : ''}
`;
div.addEventListener('click', (e) => {
if (!e.target.classList.contains('delete')) {
switchProxy(id);
}
});
const deleteBtn = div.querySelector('.delete');
if (deleteBtn) {
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
deleteProfile(id);
});
}
container.appendChild(div);
}
}
async function switchProxy(profileId) {
chrome.runtime.sendMessage(
{ action: 'setProxy', profileId },
async (response) => {
if (response?.success) {
await loadProfiles();
setTimeout(checkCurrentIP, 1000);
}
}
);
}
async function saveProfile() {
const name = document.getElementById('profile-name').value;
const scheme = document.getElementById('profile-scheme').value;
const host = document.getElementById('profile-host').value;
const port = parseInt(document.getElementById('profile-port').value);
const username = document.getElementById('profile-user').value;
const password = document.getElementById('profile-pass').value;
if (!name || !host || !port) {
alert('Name, host, and port are required');
return;
}
const id = name.toLowerCase().replace(/\s+/g, '-');
const data = await chrome.storage.local.get(['profiles']);
const profiles = data.profiles || {};
profiles[id] = {
name,
mode: 'fixed_servers',
scheme,
host,
port,
username: username || undefined,
password: password || undefined,
color: '#2196F3',
};
await chrome.storage.local.set({ profiles });
document.getElementById('add-form').classList.add('hidden');
await loadProfiles();
}
async function deleteProfile(id) {
const data = await chrome.storage.local.get(['profiles', 'activeProfile']);
delete data.profiles[id];
if (data.activeProfile === id) {
data.activeProfile = 'direct';
chrome.runtime.sendMessage({ action: 'setProxy', profileId: 'direct' });
}
await chrome.storage.local.set({
profiles: data.profiles,
activeProfile: data.activeProfile,
});
await loadProfiles();
}
async function checkCurrentIP() {
const statusText = document.getElementById('status-text');
const ipDisplay = document.getElementById('current-ip');
try {
const response = await fetch('https://httpbin.org/ip');
const data = await response.json();
ipDisplay.textContent = data.origin;
statusText.textContent = 'Connected';
statusText.style.color = '#4CAF50';
} catch {
ipDisplay.textContent = '';
statusText.textContent = 'Connection error';
statusText.style.color = '#f44336';
}
}Auto-Switch Rules
Configure rules in the options page to automatically switch proxies based on the URL pattern:
// options/options.js
async function addRule() {
const pattern = document.getElementById('rule-pattern').value;
const profileId = document.getElementById('rule-profile').value;
if (!pattern || !profileId) return;
const data = await chrome.storage.local.get(['rules']);
const rules = data.rules || [];
rules.push({ pattern, profileId, enabled: true });
await chrome.storage.local.set({ rules });
loadRules();
}
// Example rules:
// Pattern: "amazon.com" -> Profile: "us-proxy"
// Pattern: "/\.jp$/" -> Profile: "japan-proxy"
// Pattern: "netflix.com" -> Profile: "uk-proxy"Loading the Extension
- Open
chrome://extensions/ - Enable “Developer mode”
- Click “Load unpacked” and select the
proxy-switcher/directory - The extension icon appears in your toolbar
Internal Links
- Building a Proxy Checker Tool — test proxies before adding
- Proxy Setup Guide for Chrome — manual Chrome proxy configuration
- Anti-Detect Browser Proxy Guides — advanced browser fingerprinting
- Best Proxy Extensions for Chrome — compare with existing extensions
- Proxy Pool Manager — serve proxies via API
FAQ
Does this extension work with Manifest V3?
Yes. This extension is built for Manifest V3, which is required for all new Chrome extensions. The proxy API, storage API, and webRequest API all work with Manifest V3’s service worker architecture.
Can the extension handle SOCKS5 proxies?
Yes. Chrome’s proxy API supports HTTP, HTTPS, SOCKS4, and SOCKS5 proxy protocols. Set the scheme field to “socks5” in the profile configuration. The extension passes the scheme to chrome.proxy.settings.
How do auto-switch rules work?
When you navigate to a URL, the background service worker checks it against your rules in order. The first matching rule applies its proxy profile. Rules support plain string matching (the URL contains the pattern) or regex patterns wrapped in forward slashes.
Will proxy credentials be stored securely?
Chrome’s storage.local is sandboxed per extension but not encrypted. For sensitive credentials, consider using Chrome’s storage.session (cleared when the browser closes) or prompting for credentials each session. Never sync credentials with storage.sync.
Can I use PAC scripts for advanced routing?
Yes. The extension supports PAC (Proxy Auto-Configuration) scripts through Chrome’s pac_script mode. Write a PAC script that returns different proxies based on URL patterns, and the extension will apply it. PAC scripts offer the most flexible routing but are harder to debug.
- Build an Anti-Detection Test Suite: Verify Browser Stealth
- Build a Proxy Rotator in Python: Complete Tutorial
- AJAX Request Interception: Scraping API Calls Directly
- Bandwidth Optimization for Proxies: Reduce Costs & Increase Speed
- How to Configure Proxies on iPhone and Android
- How to Use Proxies in Node.js (Axios, Fetch, Puppeteer)
- Build an Anti-Detection Test Suite: Verify Browser Stealth
- Build a Proxy Rotator in Python: Complete Tutorial
- AJAX Request Interception: Scraping API Calls Directly
- Bandwidth Optimization for Proxies: Reduce Costs & Increase Speed
- How to Configure Proxies on iPhone and Android
- How to Use Proxies in Node.js (Axios, Fetch, Puppeteer)
- Build an Anti-Detection Test Suite: Verify Browser Stealth
- Build a News Crawler in Python: Step-by-Step Tutorial
- AJAX Request Interception: Scraping API Calls Directly
- Azure Functions for Serverless Web Scraping: the Complete Guide
- How to Configure Proxies on iPhone and Android
- How to Use Proxies in Node.js (Axios, Fetch, Puppeteer)
Related Reading
- Build an Anti-Detection Test Suite: Verify Browser Stealth
- Build a News Crawler in Python: Step-by-Step Tutorial
- AJAX Request Interception: Scraping API Calls Directly
- Azure Functions for Serverless Web Scraping: the Complete Guide
- How to Configure Proxies on iPhone and Android
- How to Use Proxies in Node.js (Axios, Fetch, Puppeteer)