Building a Chrome Extension for DNS-Based Ad Blocking
Share this post
Building a Chrome Extension for DNS-Based Ad Blocking
Online advertisements have become increasingly intrusive, tracking user behavior across the web while consuming bandwidth and slowing down browsing. While traditional ad blockers work by filtering content at the browser level, DNS-based ad blocking offers a more powerful approach by preventing connections to ad servers at the network level. In this guide, we’ll build a Chrome extension that implements DNS-based ad blocking with customizable blocklists.
Understanding DNS-Based Ad Blocking
Before diving into code, let’s understand how DNS-based ad blocking works:
- When you type a URL in your browser, a DNS (Domain Name System) query translates that domain name into an IP address
- A DNS-based ad blocker intercepts these queries and checks them against a blocklist
- If the domain is on the blocklist (like
ads.example.com
), the request is either blocked or redirected to a local address - This prevents the browser from even connecting to ad servers, effectively blocking ads before they load
The advantages of this approach include:
- Efficiency: Blocks ads at the network level before resources are downloaded
- Comprehensive: Blocks ads across all applications, not just the browser
- Privacy: Reduces tracking by preventing connections to tracking servers
- Performance: Improves page load times and reduces bandwidth usage
Project Overview
Our Chrome extension will:
- Create a local DNS proxy that intercepts DNS requests
- Allow users to import and manage multiple blocklists
- Provide a user-friendly interface for configuration
- Offer statistics on blocked requests
- Function similarly to a VPN but focused on ad blocking
Step 1: Setting Up the Project
First, let’s create the basic structure for our Chrome extension:
dns-ad-blocker/
├── manifest.json
├── background.js
├── popup/
│ ├── popup.html
│ ├── popup.css
│ └── popup.js
├── options/
│ ├── options.html
│ ├── options.css
│ └── options.js
├── lib/
│ ├── dns-proxy.js
│ ├── blocklist-manager.js
│ └── statistics.js
└── assets/
├── icon-16.png
├── icon-48.png
└── icon-128.png
Now, let’s create the manifest file:
{
"manifest_version": 3,
"name": "DNS Ad Blocker",
"version": "1.0",
"description": "Block ads at the DNS level with customizable blocklists",
"permissions": [
"proxy",
"networking.config",
"storage",
"declarativeNetRequest",
"webRequest",
"webRequestBlocking",
"tabs"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup/popup.html",
"default_icon": {
"16": "assets/icon-16.png",
"48": "assets/icon-48.png",
"128": "assets/icon-128.png"
}
},
"options_page": "options/options.html",
"icons": {
"16": "assets/icon-16.png",
"48": "assets/icon-48.png",
"128": "assets/icon-128.png"
}
}
Step 2: Creating the DNS Proxy
The core of our extension is the DNS proxy that intercepts and filters requests. Let’s implement this in lib/dns-proxy.js
:
class DNSProxy {
constructor() {
this.enabled = false;
this.blockLists = [];
this.cachedBlockedDomains = new Set();
this.statistics = {
totalRequests: 0,
blockedRequests: 0,
lastUpdated: Date.now()
};
}
async initialize() {
// Load saved settings
const settings = await chrome.storage.local.get(['enabled', 'blockLists', 'statistics']);
this.enabled = settings.enabled || false;
this.blockLists = settings.blockLists || [];
this.statistics = settings.statistics || this.statistics;
// Compile blocklists into a single set for efficient lookups
await this.compileBlocklists();
// Set up proxy if enabled
if (this.enabled) {
await this.enableProxy();
}
}
async compileBlocklists() {
this.cachedBlockedDomains = new Set();
for (const list of this.blockLists) {
if (!list.enabled) continue;
try {
const domains = await this.fetchBlocklist(list.url);
domains.forEach(domain => this.cachedBlockedDomains.add(domain));
} catch (error) {
console.error(`Failed to fetch blocklist ${list.name}:`, error);
}
}
console.log(`Compiled ${this.cachedBlockedDomains.size} domains into blocklist`);
}
async fetchBlocklist(url) {
// Check if it's a built-in list or a URL
if (url.startsWith('built-in:')) {
return this.getBuiltInList(url.replace('built-in:', ''));
}
// Fetch the list from URL
const response = await fetch(url);
const text = await response.text();
// Parse the list based on format (hosts file, domain list, etc.)
return this.parseBlocklist(text);
}
parseBlocklist(text) {
// Split by lines and extract domains
const lines = text.split('\n');
const domains = [];
for (let line of lines) {
// Skip comments and empty lines
line = line.trim();
if (line === '' || line.startsWith('#')) continue;
// Handle hosts file format (127.0.0.1 domain.com)
if (line.startsWith('127.0.0.1') || line.startsWith('0.0.0.0')) {
const parts = line.split(/\s+/);
if (parts.length >= 2) {
domains.push(parts[1]);
}
continue;
}
// Simple domain list format
if (this.isValidDomain(line)) {
domains.push(line);
}
}
return domains;
}
isValidDomain(domain) {
// Basic domain validation
return /^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,}$/i.test(domain);
}
getBuiltInList(listName) {
// Provide some built-in lists
const builtInLists = {
'common-ads': [
'doubleclick.net',
'googlesyndication.com',
'googleadservices.com',
'adnxs.com',
'facebook.com',
// ... more domains
],
'trackers': [
'google-analytics.com',
'hotjar.com',
'clicktale.net',
// ... more domains
],
'malware': [
'malware.domain.com',
'phishing.example.com',
// ... more domains
]
};
return builtInLists[listName] || [];
}
async enableProxy() {
// First clear any existing proxy settings
await chrome.proxy.settings.clear({});
// Set up the PAC script for DNS blocking
const pacScript = {
pacScript: {
data: this.generatePACScript(),
mandatory: true
}
};
// Apply proxy settings
await chrome.proxy.settings.set({
value: pacScript,
scope: 'regular'
});
// Set up listeners for web requests to collect statistics
chrome.webRequest.onBeforeRequest.addListener(
this.handleRequest.bind(this),
{ urls: ["<all_urls>"] },
["blocking"]
);
this.enabled = true;
await this.saveSettings();
console.log('DNS Proxy enabled');
}
generatePACScript() {
// Convert the Set to an array of domains
const blockedDomains = Array.from(this.cachedBlockedDomains);
// Create a JavaScript PAC script for the proxy
return `
function FindProxyForURL(url, host) {
// List of blocked domains
const blockedDomains = ${JSON.stringify(blockedDomains)};
// Check if domain or any parent domain is in the blocklist
let parts = host.split('.');
for (let i = 0; i < parts.length - 1; i++) {
let domain = parts.slice(i).join('.');
if (blockedDomains.includes(domain)) {
return "PROXY 127.0.0.1:65535"; // Invalid proxy to block request
}
}
// Default: direct connection
return "DIRECT";
}
`;
}
async disableProxy() {
await chrome.proxy.settings.clear({});
// Remove web request listeners
chrome.webRequest.onBeforeRequest.removeListener(this.handleRequest);
this.enabled = false;
await this.saveSettings();
console.log('DNS Proxy disabled');
}
handleRequest(details) {
// Extract the domain from the URL
const url = new URL(details.url);
const domain = url.hostname;
this.statistics.totalRequests++;
// Check if domain is blocked
if (this.isDomainBlocked(domain)) {
this.statistics.blockedRequests++;
// Save statistics periodically (not on every request to avoid performance issues)
if (Date.now() - this.statistics.lastUpdated > 5000) { // 5 seconds
this.statistics.lastUpdated = Date.now();
this.saveStatistics();
}
// Cancel the request
return { cancel: true };
}
// Allow the request
return { cancel: false };
}
isDomainBlocked(domain) {
// Check domain and its parent domains
let parts = domain.split('.');
for (let i = 0; i < parts.length; i++) {
let checkDomain = parts.slice(i).join('.');
if (this.cachedBlockedDomains.has(checkDomain)) {
return true;
}
}
return false;
}
async toggleProxy() {
if (this.enabled) {
await this.disableProxy();
} else {
await this.enableProxy();
}
return this.enabled;
}
async addBlocklist(name, url, enabled = true) {
this.blockLists.push({ name, url, enabled });
await this.compileBlocklists();
if (this.enabled) {
// Update the proxy with new blocklist
await this.enableProxy();
}
await this.saveSettings();
return this.blockLists;
}
async removeBlocklist(index) {
if (index >= 0 && index < this.blockLists.length) {
this.blockLists.splice(index, 1);
await this.compileBlocklists();
if (this.enabled) {
// Update the proxy with new blocklist
await this.enableProxy();
}
await this.saveSettings();
}
return this.blockLists;
}
async toggleBlocklist(index) {
if (index >= 0 && index < this.blockLists.length) {
this.blockLists[index].enabled = !this.blockLists[index].enabled;
await this.compileBlocklists();
if (this.enabled) {
// Update the proxy with new blocklist
await this.enableProxy();
}
await this.saveSettings();
}
return this.blockLists;
}
async saveSettings() {
await chrome.storage.local.set({
enabled: this.enabled,
blockLists: this.blockLists
});
}
async saveStatistics() {
await chrome.storage.local.set({
statistics: this.statistics
});
}
async resetStatistics() {
this.statistics = {
totalRequests: 0,
blockedRequests: 0,
lastUpdated: Date.now()
};
await this.saveStatistics();
return this.statistics;
}
getStatistics() {
return { ...this.statistics };
}
}
// Export the class
export default DNSProxy;
Step 3: Blocklist Manager
Now, let’s implement a blocklist manager in lib/blocklist-manager.js
:
class BlocklistManager {
constructor(dnsProxy) {
this.dnsProxy = dnsProxy;
this.defaultLists = [
{ name: "EasyList", url: "https://easylist.to/easylist/easylist.txt", description: "General ad blocking list" },
{ name: "AdGuard DNS", url: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", description: "Optimized for DNS blocking" },
{ name: "Common Ads", url: "built-in:common-ads", description: "Built-in list of common ad domains" },
{ name: "Trackers", url: "built-in:trackers", description: "Built-in list of tracking domains" }
];
}
async initialize() {
// If no blocklists are configured, add default ones
if (this.dnsProxy.blockLists.length === 0) {
for (const list of this.defaultLists) {
await this.dnsProxy.addBlocklist(list.name, list.url);
}
}
}
getAvailableDefaultLists() {
return [...this.defaultLists];
}
async addList(name, url) {
return await this.dnsProxy.addBlocklist(name, url);
}
async removeList(index) {
return await this.dnsProxy.removeBlocklist(index);
}
async toggleList(index) {
return await this.dnsProxy.toggleBlocklist(index);
}
async importListFromFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async (e) => {
try {
const content = e.target.result;
const domains = this.dnsProxy.parseBlocklist(content);
if (domains.length === 0) {
reject(new Error("No valid domains found in the file"));
return;
}
// Create a temporary URL to store the domains
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
// Add the list
await this.dnsProxy.addBlocklist(file.name, url);
resolve({ name: file.name, domainsCount: domains.length });
} catch (error) {
reject(error);
}
};
reader.onerror = () => {
reject(new Error("Failed to read file"));
};
reader.readAsText(file);
});
}
getCurrentLists() {
return [...this.dnsProxy.blockLists];
}
}
export default BlocklistManager;
Step 4: Statistics Tracker
Let’s add a statistics tracker in lib/statistics.js
:
class Statistics {
constructor(dnsProxy) {
this.dnsProxy = dnsProxy;
this.startTime = Date.now();
}
getStats() {
const stats = this.dnsProxy.getStatistics();
const runningTime = Math.floor((Date.now() - this.startTime) / 1000); // in seconds
return {
...stats,
runningTime,
blockPercentage: stats.totalRequests > 0
? (stats.blockedRequests / stats.totalRequests * 100).toFixed(2)
: 0
};
}
async resetStats() {
this.startTime = Date.now();
return await this.dnsProxy.resetStatistics();
}
}
export default Statistics;
Step 5: Background Script
Now let’s implement the background script that ties everything together:
import DNSProxy from './lib/dns-proxy.js';
import BlocklistManager from './lib/blocklist-manager.js';
import Statistics from './lib/statistics.js';
// Create instances
const dnsProxy = new DNSProxy();
const blocklistManager = new BlocklistManager(dnsProxy);
const statistics = new Statistics(dnsProxy);
// Initialize the extension
async function initialize() {
await dnsProxy.initialize();
await blocklistManager.initialize();
// Set up listeners for messages from popup and options pages
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
handleMessage(message, sender, sendResponse);
return true; // Indicates we will send a response asynchronously
});
console.log('DNS Ad Blocker initialized');
}
// Handle messages from popup and options pages
async function handleMessage(message, sender, sendResponse) {
try {
let response;
switch (message.action) {
case 'getStatus':
response = {
enabled: dnsProxy.enabled,
statistics: statistics.getStats(),
blockLists: blocklistManager.getCurrentLists()
};
break;
case 'toggleProxy':
await dnsProxy.toggleProxy();
response = { enabled: dnsProxy.enabled };
break;
case 'addBlocklist':
response = {
blockLists: await blocklistManager.addList(message.name, message.url)
};
break;
case 'removeBlocklist':
response = {
blockLists: await blocklistManager.removeList(message.index)
};
break;
case 'toggleBlocklist':
response = {
blockLists: await blocklistManager.toggleList(message.index)
};
break;
case 'resetStatistics':
response = {
statistics: await statistics.resetStats()
};
break;
case 'getDefaultLists':
response = {
defaultLists: blocklistManager.getAvailableDefaultLists()
};
break;
default:
throw new Error(`Unknown action: ${message.action}`);
}
sendResponse({ success: true, ...response });
} catch (error) {
console.error('Error handling message:', error);
sendResponse({ success: false, error: error.message });
}
}
// Initialize the extension
initialize();
Step 6: Popup UI
Now let’s create a simple popup UI to control the extension:
<!-- popup/popup.html -->
<!DOCTYPE html>
<html>
<head>
<title>DNS Ad Blocker</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="container">
<div class="header">
<h1>DNS Ad Blocker</h1>
</div>
<div class="status-card">
<div class="toggle-container">
<span class="toggle-label">Ad Blocking</span>
<label class="toggle">
<input type="checkbox" id="proxyToggle">
<span class="slider round"></span>
</label>
</div>
<div class="status-label" id="statusLabel">Disabled</div>
</div>
<div class="statistics">
<div class="stat-item">
<div class="stat-label">Requests Blocked</div>
<div class="stat-value" id="blockedRequests">0</div>
</div>
<div class="stat-item">
<div class="stat-label">Total Requests</div>
<div class="stat-value" id="totalRequests">0</div>
</div>
<div class="stat-item">
<div class="stat-label">Block Percentage</div>
<div class="stat-value" id="blockPercentage">0%</div>
</div>
</div>
<div class="actions">
<button id="resetStatsBtn" class="btn btn-secondary">Reset Stats</button>
<button id="optionsBtn" class="btn btn-primary">Options</button>
</div>
<div class="active-lists">
<h2>Active Lists</h2>
<div class="list-container" id="activeLists">
<!-- Lists will be populated here -->
</div>
</div>
</div>
<script src="popup.js" type="module"></script>
</body>
</html>
And the JavaScript:
// popup/popup.js
document.addEventListener('DOMContentLoaded', initialize);
async function initialize() {
// Get initial status
const status = await sendMessage({ action: 'getStatus' });
if (status.success) {
updateUI(status);
} else {
showError('Failed to get status');
}
// Set up event listeners
document.getElementById('proxyToggle').addEventListener('change', toggleProxy);
document.getElementById('resetStatsBtn').addEventListener('click', resetStatistics);
document.getElementById('optionsBtn').addEventListener('click', openOptions);
// Refresh stats every 5 seconds
setInterval(refreshStats, 5000);
}
function updateUI(data) {
// Update toggle
const proxyToggle = document.getElementById('proxyToggle');
proxyToggle.checked = data.enabled;
// Update status label
const statusLabel = document.getElementById('statusLabel');
statusLabel.textContent = data.enabled ? 'Enabled' : 'Disabled';
statusLabel.className = `status-label ${data.enabled ? 'enabled' : 'disabled'}`;
// Update statistics
if (data.statistics) {
document.getElementById('totalRequests').textContent = data.statistics.totalRequests.toLocaleString();
document.getElementById('blockedRequests').textContent = data.statistics.blockedRequests.toLocaleString();
document.getElementById('blockPercentage').textContent = `${data.statistics.blockPercentage}%`;
}
// Update active lists
if (data.blockLists) {
const activeListsContainer = document.getElementById('activeLists');
activeListsContainer.innerHTML = '';
if (data.blockLists.length === 0) {
activeListsContainer.innerHTML = '<div class="empty-list">No blocklists added</div>';
} else {
data.blockLists.forEach((list, index) => {
const listItem = document.createElement('div');
listItem.className = `list-item ${list.enabled ? 'enabled' : 'disabled'}`;
listItem.innerHTML = `
<div class="list-name">${list.name}</div>
<div class="list-status">${list.enabled ? 'Enabled' : 'Disabled'}</div>
`;
activeListsContainer.appendChild(listItem);
});
}
}
}
async function toggleProxy() {
const result = await sendMessage({ action: 'toggleProxy' });
if (result.success) {
// Update UI with new status
const status = await sendMessage({ action: 'getStatus' });
if (status.success) {
updateUI(status);
}
} else {
showError('Failed to toggle proxy');
// Reset toggle to previous state
document.getElementById('proxyToggle').checked = !document.getElementById('proxyToggle').checked;
}
}
async function resetStatistics() {
const result = await sendMessage({ action: 'resetStatistics' });
if (result.success) {
// Update statistics in UI
const status = await sendMessage({ action: 'getStatus' });
if (status.success) {
updateUI(status);
}
} else {
showError('Failed to reset statistics');
}
}
function openOptions() {
chrome.runtime.openOptionsPage();
}
async function refreshStats() {
const status = await sendMessage({ action: 'getStatus' });
if (status.success) {
// Only update statistics, not the entire UI
if (status.statistics) {
document.getElementById('totalRequests').textContent = status.statistics.totalRequests.toLocaleString();
document.getElementById('blockedRequests').textContent = status.statistics.blockedRequests.toLocaleString();
document.getElementById('blockPercentage').textContent = `${status.statistics.blockPercentage}%`;
}
}
}
function showError(message) {
console.error(message);
// You could also show a visual error message in the popup
}
function sendMessage(message) {
return new Promise((resolve) => {
chrome.runtime.sendMessage(message, (response) => {
resolve(response || { success: false, error: 'No response' });
});
});
}
Step 7: Options Page
Let’s create an options page for managing blocklists:
<!-- options/options.html -->
<!DOCTYPE html>
<html>
<head>
<title>DNS Ad Blocker - Options</title>
<link rel="stylesheet" href="options.css">
</head>
<body>
<div class="container">
<div class="header">
<h1>DNS Ad Blocker - Options</h1>
</div>
<div class="settings-card">
<h2>Blocklists</h2>
<p>Add, remove, or toggle blocklists to customize your ad blocking experience.</p>
<div id="blocklistsContainer" class="blocklists">
<!-- Blocklists will be populated here -->
</div>
<div class="add-blocklist">
<h3>Add Blocklist</h3>
<div class="form-group">
<label for="listName">Name:</label>
<input type="text" id="listName" placeholder="Blocklist name">
</div>
<div class="form-group">
<label for="listUrl">URL:</label>
<input type="text" id="listUrl" placeholder="https://example.com/blocklist.txt">
</div>
<div class="form-actions">
<button id="addListBtn" class="btn btn-primary">Add List</button>
</div>
</div>
<div class="import-blocklist">
<h3>Import from File</h3>
<div class="form-group">
<label for="listFile">File:</label>
<input type="file" id="listFile" accept=".txt,.csv,.list">
</div>
<div class="form-actions">
<button id="importListBtn" class="btn btn-primary">Import</button>
</div>
</div>
</div>
<div class="settings-card">
<h2>Recommended Lists</h2>
<p>Add these popular blocklists to enhance your ad blocking:</p>
<div id="recommendedLists" class="recommended-lists">
<!-- Recommended lists will be populated here -->
</div>
</div>
<div class="footer">
<button id="backBtn" class="btn btn-secondary">Back</button>
</div>
</div>
<script src="options.js" type="module"></script>
</body>
</html>
And the JavaScript:
// options/options.js
document.addEventListener('DOMContentLoaded', initialize);
async function initialize() {
// Get current blocklists and status
const status = await sendMessage({ action: 'getStatus' });
const defaultLists = await sendMessage({ action: 'getDefaultLists' });
if (status.success) {
renderBlocklists(status.blockLists);
}
if (defaultLists.success) {
renderRecommendedLists(defaultLists.defaultLists, status.blockLists);
}
// Set up event listeners
document.getElementById('addListBtn').addEventListener('click', addBlocklist);
document.getElementById('importListBtn').addEventListener('click', importBlocklist);
document.getElementById('backBtn').addEventListener('click', () => window.close());
}
function renderBlocklists(blocklists) {
const container = document.getElementById('blocklistsContainer');
container.innerHTML = '';
if (!blocklists || blocklists.length === 0) {
container.innerHTML = '<div class="empty-list">No blocklists added</div>';
return;
}
blocklists.forEach((list, index) => {
const listItem = document.createElement('div');
listItem.className = `blocklist-item ${list.enabled ? 'enabled' : 'disabled'}`;
listItem.innerHTML = `
<div class="blocklist-info">
<div class="blocklist-name">${list.name}</div>
<div class="blocklist-url">${list.url}</div>
</div>
<div class="blocklist-actions">
<label class="toggle small">
<input type="checkbox" data-index="${index}" class="toggle-list" ${list.enabled ? 'checked' : ''}>
<span class="slider round"></span>
</label>
<button class="btn btn-icon remove-list" data-index="${index}">
<span class="icon">×</span>
</button>
</div>
`;
container.appendChild(listItem);
});
// Add event listeners for toggle and remove buttons
document.querySelectorAll('.toggle-list').forEach(toggle => {
toggle.addEventListener('change', toggleBlocklist);
});
document.querySelectorAll('.remove-list').forEach(button => {
button.addEventListener('click', removeBlocklist);
});
}
function renderRecommendedLists(recommendedLists, currentLists) {
const container = document.getElementById('recommendedLists');
container.innerHTML = '';
if (!recommendedLists || recommendedLists.length === 0) {
container.innerHTML = '<div class="empty-list">No recommended lists available</div>';
return;
}
// Get URLs of current lists to check if recommended lists are already added
const currentUrls = currentLists.map(list => list.url);
recommendedLists.forEach((list, index) => {
const isAdded = currentUrls.includes(list.url);
const listItem = document.createElement('div');
listItem.className = 'recommended-list-item';
listItem.innerHTML = `
<div class="recommended-list-info">
<div class="recommended-list-name">${list.name}</div>
<div class="recommended-list-description">${list.description}</div>
</div>
<div class="recommended-list-actions">
<button class="btn ${isAdded ? 'btn-secondary added' : 'btn-primary'}"
data-index="${index}"
${isAdded ? 'disabled' : ''}>
${isAdded ? 'Added' : 'Add'}
</button>
</div>
`;
container.appendChild(listItem);
});
// Add event listeners for add buttons
document.querySelectorAll('.recommended-list-actions .btn:not(.added)').forEach(button => {
button.addEventListener('click', (e) => {
const index = parseInt(e.target.dataset.index);
addRecommendedList(recommendedLists[index]);
});
});
}
async function addBlocklist() {
const nameInput = document.getElementById('listName');
const urlInput = document.getElementById('listUrl');
const name = nameInput.value.trim();
const url = urlInput.value.trim();
if (!name || !url) {
showError('Name and URL are required');
return;
}
const result = await sendMessage({
action: 'addBlocklist',
name,
url
});
if (result.success) {
// Clear inputs
nameInput.value = '';
urlInput.value = '';
// Update UI
renderBlocklists(result.blockLists);
showSuccess('Blocklist added successfully');
} else {
showError(`Failed to add blocklist: ${result.error}`);
}
}
async function importBlocklist() {
const fileInput = document.getElementById('listFile');
const file = fileInput.files[0];
if (!file) {
showError('Please select a file to import');
return;
}
try {
// Read file content
const content = await readFileContent(file);
// Add as a new blocklist
const result = await sendMessage({
action: 'addBlocklist',
name: file.name,
url: `data:text/plain;base64,${btoa(content)}`
});
if (result.success) {
// Clear input
fileInput.value = '';
// Update UI
renderBlocklists(result.blockLists);
showSuccess(`Imported ${file.name} successfully`);
} else {
showError(`Failed to import blocklist: ${result.error}`);
}
} catch (error) {
showError(`Error importing file: ${error.message}`);
}
}
function readFileContent(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
resolve(e.target.result);
};
reader.onerror = () => {
reject(new Error("Failed to read file"));
};
reader.readAsText(file);
});
}
async function toggleBlocklist(event) {
const index = parseInt(event.target.dataset.index);
const result = await sendMessage({
action: 'toggleBlocklist',
index
});
if (result.success) {
// Update UI
renderBlocklists(result.blockLists);
} else {
showError(`Failed to toggle blocklist: ${result.error}`);
// Reset toggle to previous state
event.target.checked = !event.target.checked;
}
}
async function removeBlocklist(event) {
const index = parseInt(event.target.dataset.index);
if (!confirm('Are you sure you want to remove this blocklist?')) {
return;
}
const result = await sendMessage({
action: 'removeBlocklist',
index
});
if (result.success) {
// Update UI
renderBlocklists(result.blockLists);
showSuccess('Blocklist removed successfully');
} else {
showError(`Failed to remove blocklist: ${result.error}`);
}
}
async function addRecommendedList(list) {
const result = await sendMessage({
action: 'addBlocklist',
name: list.name,
url: list.url
});
if (result.success) {
// Get latest status to update both lists
const status = await sendMessage({ action: 'getStatus' });
const defaultLists = await sendMessage({ action: 'getDefaultLists' });
if (status.success) {
renderBlocklists(status.blockLists);
}
if (defaultLists.success) {
renderRecommendedLists(defaultLists.defaultLists, status.blockLists);
}
showSuccess(`Added ${list.name} successfully`);
} else {
showError(`Failed to add ${list.name}: ${result.error}`);
}
}
function showError(message) {
console.error(message);
// You could also show a visual error message in the options page
alert(`Error: ${message}`);
}
function showSuccess(message) {
console.log(message);
// You could also show a visual success message in the options page
// For now, we'll just use alert, but in a real extension you'd want a nicer UI
alert(message);
}
function sendMessage(message) {
return new Promise((resolve) => {
chrome.runtime.sendMessage(message, (response) => {
resolve(response || { success: false, error: 'No response' });
});
});
}
Step 8: CSS Styling
Let’s add some styling for the popup:
/* popup/popup.css */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
width: 340px;
margin: 0;
padding: 0;
background-color: #f5f7fa;
color: #333;
}
.container {
padding: 16px;
}
.header {
text-align: center;
margin-bottom: 16px;
}
h1 {
font-size: 1.5rem;
margin: 0;
color: #2563eb;
}
.status-card {
background-color: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.toggle-container {
display: flex;
align-items: center;
}
.toggle-label {
margin-right: 12px;
font-weight: 500;
}
.toggle {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.toggle input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
}
input:checked + .slider {
background-color: #2563eb;
}
input:focus + .slider {
box-shadow: 0 0 1px #2563eb;
}
input:checked + .slider:before {
transform: translateX(26px);
}
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
.status-label {
font-weight: 600;
padding: 6px 12px;
border-radius: 4px;
}
.status-label.enabled {
color: #047857;
background-color: #d1fae5;
}
.status-label.disabled {
color: #b91c1c;
background-color: #fee2e2;
}
.statistics {
background-color: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 16px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.stat-item {
text-align: center;
}
.stat-label {
font-size: 0.8rem;
color: #6b7280;
margin-bottom: 4px;
}
.stat-value {
font-size: 1.2rem;
font-weight: 600;
color: #1f2937;
}
.actions {
display: flex;
justify-content: space-between;
margin-bottom: 16px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-primary {
background-color: #2563eb;
color: white;
}
.btn-primary:hover {
background-color: #1d4ed8;
}
.btn-secondary {
background-color: #e5e7eb;
color: #4b5563;
}
.btn-secondary:hover {
background-color: #d1d5db;
}
.active-lists {
background-color: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
h2 {
font-size: 1.1rem;
margin: 0 0 12px 0;
color: #374151;
}
.list-container {
max-height: 200px;
overflow-y: auto;
}
.list-item {
display: flex;
justify-content: space-between;
padding: 8px;
border-radius: 4px;
margin-bottom: 8px;
}
.list-item.enabled {
background-color: #f0f9ff;
}
.list-item.disabled {
background-color: #f3f4f6;
color: #9ca3af;
}
.list-name {
font-weight: 500;
}
.list-status {
font-size: 0.8rem;
color: #6b7280;
}
.empty-list {
text-align: center;
padding: 16px;
color: #9ca3af;
font-style: italic;
}
And for the options page:
/* options/options.css */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f7fa;
color: #333;
}
.container {
max-width: 800px;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 24px;
}
h1 {
font-size: 2rem;
margin: 0;
color: #2563eb;
}
.settings-card {
background-color: white;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 24px;
}
h2 {
font-size: 1.5rem;
margin: 0 0 16px 0;
color: #374151;
}
h3 {
font-size: 1.2rem;
margin: 24px 0 16px 0;
color: #4b5563;
}
p {
margin: 0 0 16px 0;
color: #6b7280;
}
.blocklists {
margin-bottom: 24px;
}
.blocklist-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
border-radius: 4px;
margin-bottom: 12px;
transition: background-color 0.2s;
}
.blocklist-item.enabled {
background-color: #f0f9ff;
border-left: 4px solid #2563eb;
}
.blocklist-item.disabled {
background-color: #f3f4f6;
color: #9ca3af;
border-left: 4px solid #e5e7eb;
}
.blocklist-name {
font-weight: 600;
margin-bottom: 4px;
}
.blocklist-url {
font-size: 0.8rem;
color: #6b7280;
word-break: break-all;
}
.blocklist-actions {
display: flex;
align-items: center;
}
.toggle.small {
width: 40px;
height: 20px;
margin-right: 12px;
}
.toggle.small .slider:before {
height: 14px;
width: 14px;
left: 3px;
bottom: 3px;
}
.toggle.small input:checked + .slider:before {
transform: translateX(20px);
}
.btn-icon {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
background-color: #fee2e2;
color: #b91c1c;
border-radius: 50%;
border: none;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-icon:hover {
background-color: #fecaca;
}
.icon {
font-size: 1.2rem;
font-weight: bold;
}
.form-group {
margin-bottom: 16px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
input[type="text"] {
width: 100%;
padding: 8px;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 1rem;
}
input[type="file"] {
width: 100%;
padding: 8px;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 1rem;
background-color: white;
}
.form-actions {
display: flex;
justify-content: flex-end;
}
.recommended-list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border: 1px solid #e5e7eb;
border-radius: 4px;
margin-bottom: 12px;
transition: background-color 0.2s;
}
.recommended-list-item:hover {
background-color: #f9fafb;
}
.recommended-list-name {
font-weight: 600;
margin-bottom: 4px;
}
.recommended-list-description {
font-size: 0.9rem;
color: #6b7280;
}
.footer {
display: flex;
justify-content: flex-end;
margin-top: 24px;
}
.empty-list {
text-align: center;
padding: 24px;
color: #9ca3af;
font-style: italic;
background-color: #f9fafb;
border-radius: 4px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-primary {
background-color: #2563eb;
color: white;
}
.btn-primary:hover {
background-color: #1d4ed8;
}
.btn-secondary {
background-color: #e5e7eb;
color: #4b5563;
}
.btn-secondary:hover {
background-color: #d1d5db;
}
.btn.added {
cursor: default;
}
Step 9: Testing and Debugging
When testing your DNS ad blocker extension, there are a few things to keep in mind:
- Chrome requires certain permissions to be granted for DNS manipulation
- Testing needs to be done in a real Chrome browser environment
- You’ll want to verify that blocklists are being correctly loaded and applied
Here’s a testing procedure you can follow:
- Load the extension in developer mode in Chrome
- Check the console for any errors during initialization
- Test toggling the extension on and off
- Add a custom blocklist and verify it’s properly loaded
- Visit websites known to have ads and verify they’re being blocked
- Check the statistics to ensure requests are being counted correctly
Step 10: Distribution
To prepare your extension for distribution, you’ll need to:
- Create a zip file of your extension directory
- Create a developer account on the Chrome Web Store
- Submit your extension for review, including screenshots and detailed descriptions
- Once approved, users can install it directly from the Web Store
Implementation Considerations and Limitations
There are some important considerations to keep in mind:
- Permission Requirements: Chrome requires extensive permissions for DNS manipulation, which may deter some users
- Chrome DNS API Changes: Chrome’s networking APIs change over time; this code may need updates for future Chrome versions
- Performance Impact: DNS blocking can impact browser performance; extensive testing is needed
- Security Implications: As a VPN-like extension, security is critical; consider having the code audited
Advanced Features for Future Versions
Here are some advanced features you could add in future versions:
- Custom DNS Server Support: Allow users to specify their own DNS servers
- Wildcard and Regex Blocking: Support more advanced blocking patterns
- Per-Site Whitelisting: Allow users to disable blocking for certain sites
- Block Element Picker: Let users visually select elements to block
- Synchronization: Allow blocklists to sync across devices via Chrome Sync
- Import/Export Settings: Allow users to backup and restore their configuration
- Advanced Analytics: Show more detailed statistics about blocking behavior
Conclusion
Building a Chrome extension that performs DNS-based ad blocking gives users powerful control over their browsing experience. By intercepting DNS requests and comparing them against customizable blocklists, this extension can effectively block ads, trackers, and malicious domains before they ever load.
The key advantages of this approach compared to traditional content blockers include improved privacy, better performance, and the ability to block ads across all applications when configured properly.
Remember that while ad blocking improves user experience, many websites rely on advertising revenue to provide free content. Consider implementing features like allowlisting to support websites you value while blocking intrusive ads elsewhere.
With the framework provided in this guide, you can build a robust ad-blocking solution that offers users the flexibility to customize their browsing experience according to their preferences.