It began as a question about building a VPN. It ended as a resolver I own - one that watches every device in the house, explains what it sees using a model running on my own GPU, blocks what I tell it to, and now travels with me. The VPN turned out to be the last four minutes of the day.
The honest version of "let's build a VPN" is that you don't build one. You configure WireGuard, which is a solved problem, and writing your own tunnel crypto is the single worst idea available. So the interesting question isn't how to move the traffic. It's what a tunnel lets you see.
The moment your traffic passes through a machine you control, something becomes visible that never was before: every name your devices ask for, timestamped, attributed to the device that asked. Not a summary. The actual list.
You cannot buy a network that explains itself. You have to sit in the middle of it.
The cheapest possible test came first: run a DNS resolver on a machine I own, point the private network's nameserver at it, and read the log. No client software, no configuration on the phone. Within minutes there was real data, and the idea would either be interesting or die for free.
Nobody was browsing. Three devices sat on a desk. Here is what they said, grouped by the company behind the name - which is itself the first real finding, because no per-domain view shows it.
| Organization | Queries | Where |
|---|---|---|
| 257 | ||
| Adobe | 253 | |
| Apple | 183 | |
| Meta | 98 | |
| Microsoft | 86 | |
| Taboola | 54 |
Bar segments are devices - laptop · desktop · phone.
Adobe and Google are effectively tied, but they are opposite phenomena on opposite machines. Adobe is 81% laptop - Creative Cloud checking in while nothing is open. Google is 68% desktop, and almost none of it is Google the search engine. It is the advertising layer.
Adobe was also invisible until the data was grouped. It arrives across four separate registered domains, so any per-domain ranking scatters it into fragments and buries it. Aggregation was where the signal turned out to live, and it needed no AI at all.
A whole class of Google endpoints named -pa that appear in no settings screen and no privacy policy summary. Two of them are outright advertising infrastructure, running on a phone in a pocket.
A raw list of 1,238 hostnames is not knowledge. dunamis-ethos508-prod-va6-856defacfb833db1.elb.us-east-1.amazonaws.com means nothing to anyone. The obvious move: point a model at the log and have the network narrate itself, locally, so nothing leaves the house.
Before trusting a word of its output, I built a control set: six domains I invented, mixed with ten real ones. The invented domains have exactly one correct answer - I don't know. I seeded them with tempting words like telemetry-hub and cdn to see what the model would take the bait on.
| Model & prompt | Invented domains confidently named | Real domains identified |
|---|---|---|
| qwen2.5:7b · naive prompt | 4 / 6 | 8 / 10 |
| llama3.1:8b · naive prompt | 6 / 6 | 10 / 10 |
| llama3.1:8b · strict prompt | 6 / 6 | 10 / 10 |
| qwen2.5:7b · revised prompt | 0 / 6 | 10 / 10 |
Lower is better in the middle column. It is the column that decides whether any of this is usable.
Llama invented a company for every single fabricated domain, at high confidence, and got worse when the prompt told it not to. It attributed domains I made up to ZorBax Corporation, Vorple Dyne, and - after being explicitly instructed to admit ignorance - to Agilent Technologies and Google LLC.
A model that scores 10/10 on real inputs is worthless if it also scores 10/10 on inputs that don't exist.
This is the part I'd want anyone to take away. Both models had perfect recall. Measured only on real domains, llama looked like the better choice - it's larger and better known. The failure was invisible until I tested it on things that were not there.
Not a bigger model, and not a sterner instruction. The fix was removing the evidence the model was cheating on. It was reading the words inside the hostname - telemetry, cdn, ads - and pattern-matching. But whoever registers a domain chooses those words. They are decoration, not evidence.
Point judgement at the registered domain alone, state plainly that the subdomain proves nothing, and ask a yes/no recall question before asking for any description. Zero false positives, full recall retained.
Even at 0/6, a 7B model gets company names subtly wrong on obscure ad-tech. It renamed adnxs.com (AppNexus) to "AdNexus Technologies" and crwdcntrl.net (Lotame) to "CrowdControl Technologies". The category was right both times. The company was fiction.
That's a stable, predictable failure - so the system is split along it. Attribution comes from a hand-built table of corporate domain families, which is the only thing that can know adobess.com and adobelogin.com are one company. The model fills the remainder. Every row in the interface carries which one answered.
| Organization | Queries | Source |
|---|---|---|
| Apple | 183 | Fact |
| Meta | 98 | Fact |
| Taboola | 54 | Fact |
| "TrinityAlpha Media" | 7 | Model |
| "AdNexus Technologies" | 6 | Model |
78% of attributed traffic resolves by the factual table. The remaining 22% inherits the model's error rate - and says so.
21% of hostnames come back unrecognised, and that number is left on the screen rather than filled in. It is the honest cost of a model that admits ignorance, and it is a far better number to look at than llama's confident fiction.
Seeing leads directly to wanting to stop things. But this resolver is the sole nameserver for every device in the house, so a bad rule doesn't hide an ad - it breaks the network. Three decisions made that survivable.
shadow resolves normally and records what it would have blocked. You read the blast radius before causing it. The model-driven rule - block anything the model called advertising - stayed in shadow until I had read all 89 hostnames it matched. Two were false positives: Adobe Creative Cloud extension infrastructure, and the Google Ads console. Both were rescued before the rule ever fired.
The first version returned NXDOMAIN, and pages began to stall. The log showed why: one hostname looked up 86 times in ten minutes. Ad scripts treat "no such name" as a reason to retry, and the retry storm is what hangs the page. A sinkhole address resolves instantly and fails the connection at once, so the site's own error path runs and the content appears.
I proved this the hard way by deploying invalid JSON by accident. DNS kept answering; blocking silently switched off. The failure mode is "protection stops", never "the house loses the internet".
The local networks. I blocked Taboola because that is what the desktop's traffic showed. Then the phone hit a national news site and nothing changed - because that site uses Outbrain. Underneath sat a tier of Israeli ad networks no public list had surfaced: somplo, kway, madatapro, eratead, valuad, browsi. Every one of them found by reading my own traffic rather than downloading someone else's list.
Apps hold themselves hostage to their own ads. One news app stopped starting entirely - stuck on its splash screen, forever, with no timeout. The startup trace showed the app pausing 2.7 seconds and then requesting an ad. It will not proceed until that ad loads. Blocking it doesn't remove an advert; it bricks the application.
The ad isn't in the app. The app is downstream of the ad.
The same shape appeared with an attribution SDK that gates app launch on a tracking call. Both had to be allowed by name - a deliberate, recorded trade, written into the rules file with the reason attached, rather than a silent exception.
The thing I set out to build in the morning took about four minutes at the end of the day, and only because everything underneath it already existed. The mesh between my machines is WireGuard - it was the substrate this whole project ran on, not a thing I needed to construct.
What was missing was an exit node: telling one machine it may route the whole internet on behalf of the others. One command on the desktop, one approval in the console, one tap on the phone.
| Phone, mobile data | Public address | IPv6 |
|---|---|---|
| Before | 2.54.xxx.xxx · mobile carrier | exposed |
| Exit node on | 77.127.xx.xx · home | not detected |
Measured on cellular with wifi off. The IPv6 row matters: an address that leaks around the tunnel defeats the point, and plenty of commercial products get this wrong.
The payoff isn't hiding. It's that everything built today became portable. On café wifi, in an airport, on cellular - the traffic exits from a machine I own, and the ad blocking comes along because DNS travels the same path. The network I can see is now the network I carry.
The VPN was the least interesting part. It usually is.
Adding another country means renting a small machine there and running the same command - it then appears in the phone's location picker next to home, for a few dollars a month, with no shared IPs and no logging policy to take on trust. Worth saying plainly, though: self-hosted exits do not beat streaming geo-blocks. Datacenter address ranges are blocklisted wholesale, and staying ahead of that is the one thing the commercial services genuinely do better.
It resolves names. It has no idea whether a lookup came from a browser, a news app or a background service. Except that an app starting up produces a burst of lookups within a few seconds, in nearly the same order every time. Cluster those bursts by which domains they contain and the recurring sessions fall out, one per application.
Which is how the tool found, on its own, the thing that had taken me half an hour of reading the raw log by hand:
| Session | Launches | Behaviour |
|---|---|---|
| N12 / Keshet | 10 | paused 1.8s, then asked for ad.doubleclick.net |
| Samsung home | 10 | paused 1.7s, then asked for a safeframe host |
A pause mid-launch, followed immediately by an advertising domain, is an app waiting on its advert before it will continue.
The second row I did not know about. The phone's own home screen does it too.
Once you know which lookups belong to one app, you can itemise them. Not a summary, not a score: the actual list, the way a till receipt lists a purchase. Every company, what it does, and whether the request went out.
The first version picked the longest launch and printed it, which produced a receipt containing Facebook, a weather service and a clock server. Those were not the app. They were neighbours in time. It now lists only what recurs across most launches, which is the difference between what an app does and what merely happened nearby.
Times you were asked: 0
I asked it to name the clusters. It refused on five of six, which is correct: a 7B model has genuinely never heard of biprx.mako.co.il. The one time it did answer it said "Google" about a Samsung cluster, and was wrong.
The negative result was worth more than the feature would have been.
So the model is not in this path. Names come from the curated table of corporate domains, extended with Israeli media. Local knowledge has to be a fact, not a guess.
Everything else here stops data leaving. The last piece does the opposite: it shows what that data would have said. The model is handed the log and told to act as an advertising data broker and build the audience profile it would sell.
Which is only worth anything if it cannot make things up. So every claim must cite hostnames, and each citation passes two gates: the hostname must really appear in the log, and it must actually bear on the claim, judged by majority vote.
| Claim | Cited evidence | Verdict |
|---|---|---|
| Works professionally with Adobe Creative Cloud | cc-api-data.adobe.io | kept 3/3 |
| Engages with Israeli media | ynet.co.il, mako.co.il | kept 3/3 |
| Uses Microsoft products | www.adobe.com | thrown out |
| Interest in cloud infrastructure | api.telegram.org | thrown out |
Nine claims proposed, five survived both gates.
The first version of the second gate was worse than having no gate at all. I wrote "be strict" in the prompt and gave only an example of rejection, and it deleted the Adobe finding, which was the most accurate and most valuable thing on the list. Balancing the examples and taking a majority of three votes fixed it.
A verifier that only ever rejects is not a verifier.
The same pattern appeared once more in the daily summary. Every model tested got the arithmetic wrong: one contradicted itself inside a single sentence, one turned 16% into "about a quarter", one called hostnames devices. So the numbers are written by code and the model contributes one sentence of phrasing, never sees a figure, and has its sentence discarded if a digit appears in it anyway.
Application naming deserves its caveat too. It works on a phone, where apps open and close and each launch is a distinct burst. It does not work on a laptop, whose traffic never stops long enough for a burst to mean anything. That is a limit of the method, not a bug, and the interface only shows clusters whose launches actually resemble each other.
The rest is structural, not a missing feature. DNS resolves names. It can stop an advert downloading; it cannot delete the container the page drew for it. Removing an element means editing the page as it renders, which happens inside the browser - a different layer entirely, and the right tool there is a browser blocker.
Which is the actual conclusion: these are two halves, not competitors. A browser extension cannot see the phone's LLM check-ins, its attribution SDK, or its telemetry. This cannot clean up a page. Run both.
Seven scheduled jobs run it unattended on a desktop in the corner: boot-start, auto-restart, survives reboots. Nothing is sent anywhere. The model, the log, the rules and the interface are all on hardware in the room.