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:

  1. User attempts to visit: sportsbet.com.au
  2. Device sends DNS query: "What's the IP for sportsbet.com.au?"
  3. Whistl VPN intercepts: Query inspected against blocklist
  4. Match found: Query blocked, intervention displayed
  5. 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

CategoryDomain CountExamples
Sports Betting2,500+sportsbet.com.au, tab.com.au, bet365.com
Casinos1,800+crown.com.au, star.com.au, jackpotcity.com
Poker Sites800+pokerstars.com, partypoker.com
Daily Fantasy400+draftkings.com, fandangosportsbook.com
Crypto Gambling1,200+stake.com, roobet.com, duelbits.com
Online Shopping5,000+amazon.com, ebay.com, temu.com (optional)
Fast Fashion800+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

ModeBattery Impact (hourly)
Active (screen on)2-3%
Background (VPN suspended)<0.5%
Sleep (no activity)<0.1%
Daily average4-6%

Bypass Prevention

Users may attempt to circumvent DNS blocking:

Common Bypass Attempts

MethodPrevention
Direct IP accessIP blocklist for known gambling servers
Alternative DNSVPN forces all DNS through Whistl
Proxy/VPN appsDetect and block other VPN connections
Mobile data switchVPN persists across network changes
HTTPS inspection bypassDNS-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:

MetricResult
DNS Query Latency (unblocked)<10ms
Block Decision Time<1ms
Blocklist Size12,547 domains
Memory Usage2.3 MB
Block Accuracy99.7%
False Positive Rate0.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 Free

Related: GPS Geofencing | 8-Step Negotiation Engine | Alternative Action Library