DNS Filtering: Technical Implementation Deep Dive
Whistl's DNS filtering blocks gambling and shopping sites at the network level using VPN-based DNS interception. This technical deep dive explains packet tunnel protocols, DNS query inspection, blocklist management, and how intervention messages replace blocked pages—all while preserving privacy and minimising battery impact.
How DNS Filtering Works
DNS (Domain Name System) translates domain names to IP addresses. By intercepting DNS queries, Whistl can block access before connections are established:
- User attempts to visit: sportsbet.com.au
- Device sends DNS query: "What's the IP for sportsbet.com.au?"
- Whistl VPN intercepts: Query inspected against blocklist
- Match found: Query blocked, intervention displayed
- No match: Query forwarded to upstream DNS resolver
VPN Packet Tunnel Architecture
Whistl uses iOS Network Extension and Android VpnService to create a local VPN tunnel:
iOS Network Extension
import NetworkExtension
class PacketTunnelProvider: NEPacketTunnelProvider {
override func startTunnel(
options: [String: NSObject]?,
completionHandler: @escaping (Error?) -> Void
) {
// Configure tunnel settings
let tunnelSettings = NEPacketTunnelNetworkSettings(
tunnelRemoteAddress: "127.0.0.1"
)
// DNS settings - point to local DNS proxy
tunnelSettings.dnsSettings = NEDNSSettings(
servers: ["127.0.0.1"]
)
// Only route DNS traffic through tunnel
tunnelSettings.mtu = NSNumber(value: 1500)
// Apply settings
setTunnelNetworkSettings(tunnelSettings) { error in
if let error = error {
completionHandler(error)
return
}
// Start DNS proxy server
startDNSProxy()
completionHandler(nil)
}
}
private func startDNSProxy() {
let dnsProxy = DNSProxy()
dnsProxy.start(on: "127.0.0.1", port: 53)
}
}
Android VpnService
class WhistlVpnService : VpnService() {
private lateinit var interface: ParcelFileDescriptor
private lateinit var dnsProxy: DNSProxy
override fun onCreate() {
super.onCreate()
setupVpn()
}
private fun setupVpn() {
val builder = Builder()
.addAddress("127.0.0.1", 32)
.addDnsServer("127.0.0.1")
.addRoute("0.0.0.0", 0) // Route all traffic
.setMtu(1500)
.setSession("Whistl Protection")
interface = builder.establish()!!
// Start DNS proxy
dnsProxy = DNSProxy(this)
dnsProxy.start("127.0.0.1", 53)
}
}
DNS Query Inspection
The DNS proxy inspects each query before resolution:
DNS Packet Structure
struct DNSQuery {
let transactionID: UInt16 // Match response to query
let flags: UInt16 // Query type, recursion desired
let questionCount: UInt16 // Usually 1
let answerCount: UInt16 // 0 for queries
let authorityCount: UInt16 // 0
let additionalCount: UInt16 // 0
let questions: [DNSQuestion] // Domain name, type, class
}
struct DNSQuestion {
let name: String // e.g., "sportsbet.com.au"
let type: UInt16 // A (1), AAAA (28), CNAME (5), etc.
let class: UInt16 // IN (1) = Internet
}
Query Processing Pipeline
class DNSProxy {
private let blocklist: DomainBlocklist
private let upstreamDNS = ["8.8.8.8", "1.1.1.1"] // Fallback resolvers
func handleQuery(_ query: DNSQuery) -> DNSResponse? {
for question in query.questions {
let domain = question.name.lowercased()
// Check against blocklist
if blocklist.isBlocked(domain) {
// Return intervention response
return createInterventionResponse(
for: query,
domain: domain,
category: blocklist.getCategory(domain)
)
}
}
// Forward to upstream DNS
return forwardToUpstream(query)
}
}
Blocklist Management
Whistl maintains comprehensive gambling and shopping blocklists:
Blocklist Categories
| Category | Domain Count | Examples |
|---|---|---|
| Sports Betting | 2,500+ | sportsbet.com.au, tab.com.au, bet365.com |
| Casinos | 1,800+ | crown.com.au, star.com.au, jackpotcity.com |
| Poker Sites | 800+ | pokerstars.com, partypoker.com |
| Daily Fantasy | 400+ | draftkings.com, fandangosportsbook.com |
| Crypto Gambling | 1,200+ | stake.com, roobet.com, duelbits.com |
| Online Shopping | 5,000+ | amazon.com, ebay.com, temu.com (optional) |
| Fast Fashion | 800+ | shein.com, boohoo.com, fashionnova.com |
Blocklist Sources
- Curated lists: Manually verified gambling domains
- Community reports: User-submitted domains
- Regulatory lists: Licensed gambling operators by jurisdiction
- Pattern matching: Wildcard rules for subdomains
Blocklist Data Structure
{
"version": "2026.03.01",
"updated": "2026-03-05T00:00:00Z",
"categories": {
"gambling": {
"domains": [
"sportsbet.com.au",
"tab.com.au",
"*.bet365.com",
"crown.com.au"
],
"regex_patterns": [
".*bet.*\\.com$",
".*casino.*\\.com$",
".*poker.*\\.com$"
]
},
"shopping": {
"domains": [...],
"regex_patterns": [...]
}
},
"total_domains": 12547
}
Efficient Lookup
Blocklist uses trie data structure for O(m) lookup where m = domain length:
class DomainTrie {
private class Node {
var children: [Character: Node] = [:]
var isBlocked: Bool = false
var category: String?
}
private let root = Node()
func insert(_ domain: String, category: String) {
var current = root
// Reverse domain for efficient prefix matching
let reversed = domain.split(separator: ".").reversed().joined(separator: ".")
for char in reversed {
if current.children[char] == nil {
current.children[char] = Node()
}
current = current.children[char]!
}
current.isBlocked = true
current.category = category
}
func isBlocked(_ domain: String) -> Bool {
var current = root
let reversed = domain.split(separator: ".").reversed().joined(separator: ".")
for char in reversed {
guard let next = current.children[char] else {
return false
}
current = next
if current.isBlocked {
return true // Parent domain is blocked
}
}
return current.isBlocked
}
}
Intervention Response
When a domain is blocked, Whistl displays an intervention page instead:
Local Web Server
func createInterventionResponse(
for query: DNSQuery,
domain: String,
category: String
) -> DNSResponse {
// Return IP of local intervention server
return DNSResponse(
transactionID: query.transactionID,
answers: [
DNSRecord(
name: domain,
type: .A,
data: "127.0.0.1" // Localhost
)
]
)
}
// Local HTTP server serves intervention page
class InterventionServer {
private let server: HTTPServer
func handleRequest(_ request: HTTPRequest) -> HTTPResponse {
let intervention = InterventionGenerator.generate(
domain: request.host,
category: getCategory(request.host)
)
return HTTPResponse(
status: .ok,
headers: ["Content-Type": "text/html"],
body: intervention.html
)
}
}
Intervention Page Content
The intervention page includes:
- Risk indicator: Current composite risk score
- Blocking reason: "This site is blocked because..."
- 8-Step Negotiation: Interactive intervention flow
- Alternative actions: Suggested coping strategies
- Partner contact: Quick call/message button
- Crisis resources: Gambling Help: 1800 858 858
Privacy Architecture
Whistl's DNS filtering is designed for maximum privacy:
Local-Only Processing
- DNS queries inspected on-device: Never sent to Whistl servers
- Blocklist stored locally: Downloaded encrypted, stored in secure enclave
- No query logging: DNS history not retained
- Upstream DNS privacy: User's chosen resolver (Cloudflare, Google, etc.)
What Whistl Does NOT See
- Browsing history: Only DNS queries, not full URLs
- Page content: No access to page data
- Search queries: Search terms not visible
- Non-blocked domains: Only blocked domains logged (locally)
Transparency Report
Users can view blocked domain history:
- Last 24 hours: Full detail
- Last 7 days: Aggregated counts
- Last 30 days: Category summaries
- Export: Download complete history
Battery Optimisation
VPN-based DNS filtering has minimal battery impact:
Power-Efficient Design
- Local DNS proxy: No network round-trip for blocklist checks
- Efficient trie lookup: O(m) complexity, minimal CPU
- Background suspension: VPN pauses when app in background
- Smart routing: Only DNS traffic through tunnel
Battery Impact Measurements
| Mode | Battery Impact (hourly) |
|---|---|
| Active (screen on) | 2-3% |
| Background (VPN suspended) | <0.5% |
| Sleep (no activity) | <0.1% |
| Daily average | 4-6% |
Bypass Prevention
Users may attempt to circumvent DNS blocking:
Common Bypass Attempts
| Method | Prevention |
|---|---|
| Direct IP access | IP blocklist for known gambling servers |
| Alternative DNS | VPN forces all DNS through Whistl |
| Proxy/VPN apps | Detect and block other VPN connections |
| Mobile data switch | VPN persists across network changes |
| HTTPS inspection bypass | DNS-level blocking works regardless of HTTPS |
VPN Lock (iOS)
// Prevent VPN disconnection
let connection = NEVPNManager.shared()
connection.isOnDemandEnabled = true
let rules: [NEOnDemandRule] = [
NEOnDemandRuleConnect(interfaceType: .any),
NEOnDemandRuleDisconnect(interfaceType: .cellular) // Optional
]
connection.onDemandRules = rules
Performance Metrics
DNS filtering performance from production deployment:
| Metric | Result |
|---|---|
| DNS Query Latency (unblocked) | <10ms |
| Block Decision Time | <1ms |
| Blocklist Size | 12,547 domains |
| Memory Usage | 2.3 MB |
| Block Accuracy | 99.7% |
| False Positive Rate | 0.3% |
User Testimonials
"The DNS blocking is invisible until I try to visit a blocked site. Then it's like a bouncer stepping in. Perfect." — Jake, 31
"I tried to access a betting site on mobile data and Whistl still blocked it. No way around it. That's what I needed." — Marcus, 28
"Battery drain was my worry but it's maybe 5% per day. Totally worth it for the protection." — Emma, 26
Conclusion
Whistl's DNS filtering provides network-level protection against gambling and shopping sites through VPN-based DNS interception. By blocking queries before connections are established, the system prevents access while displaying supportive intervention messages.
All processing happens on-device, preserving privacy while delivering reliable protection across WiFi and mobile data.
Get Network-Level Protection
Whistl's DNS filtering blocks gambling sites at the network level. Download free and activate protection in minutes.
Download Whistl FreeRelated: GPS Geofencing | 8-Step Negotiation Engine | Alternative Action Library