Blocked services and safe search
A Policy can block whole services by name (blockedServices: [youtube, tiktok]), whole categories of services (blockedCategories: [ai]), and
force the search engines' safe-search mode (safeSearch: true). All of
them read the kudurru blocked-services and safe-search catalogue (decision
D60): Apache-2.0 data written for kudurru from each vendor's own
documentation and the services' first-party domains. The catalogue is
built into the binary (internal/listdata/catalogue/catalogue.json,
sources per entry in PROVENANCE.md next to it) and can be replaced at
runtime from a URL. An administrator can also opt in to AdGuard's
HostlistsRegistry services (GPL-3.0), which the instance then downloads
itself; kudurru does not ship them (see below).
Using it
apiVersion: kudurru.io/v1alpha1
kind: Policy
metadata:
name: kids
namespace: default
spec:
clientGroupRefs: [kids]
priority: 10
blockedServices: [youtube, tiktok, roblox]
blockedCategories: [ai, gambling] # every service in them, also ones added later
allowedServices: [huggingface] # exceptions inside a blocked category
safeSearch: true
safeSearchMode: moderate # optional; YouTube only
The dashboard's Policy page and the policy wizard's What step show the
catalogue as a logo grid: one tab per category with counts ("AI · 19 · 3
blocked"), a search over names and domains, a "Blocked only" filter, one
section per category with a "Block the whole category" switch, and the
SafeSearch switch in its own section. GET /api/v1/catalogue/services lists
the service IDs, GET /api/v1/catalogue/categories the categories,
GET /api/v1/catalogue/safesearch the engines, and GET /api/v1/catalogue
the version in service, where it came from and which sources are active
with their licences. All four need policies.read.
Services and engines carry icon (and iconOnDark for dark artwork), the
path of a logo built into the binary: GET /api/v1/catalogue/icons/{slug}.svg
(image/svg+xml, strong ETag, Cache-Control: public, max-age=86400).
GET /api/v1/catalogue/icons lists every logo, including the upstream DNS
providers the Upstreams page shows (cloudflare, google, adguard-home,
nextdns, mullvad, cisco for OpenDNS, control-d). Both routes are public:
the logos are static files of the build. They come from
homarr-labs/dashboard-icons
(Apache-2.0; commit, files and checksums in
internal/listdata/catalogue/icons/PROVENANCE.md), carry no script, event
handler or external reference, and are trademarks of their owners, shown
only to identify the service. An entry without a logo (Roblox, Fortnite,
9GAG, VK, Vimeo, Tinder, Pixabay, Minecraft, most dating, gambling, news
and crypto brands) has no icon; the dashboard draws a monogram.
Categories
| ID | Name | Services in the embedded catalogue |
|---|---|---|
ai | AI | ChatGPT (with the OpenAI API and Sora), Claude (with the Anthropic API), Google Gemini and AI Studio, Microsoft Copilot, Perplexity, Mistral Vibe (Le Chat), DeepSeek, Grok, Meta AI, Character.AI, Poe, Hugging Face, Midjourney, Stability AI, Runway, Suno, ElevenLabs, Replicate, Pi |
social | Social media | TikTok, Facebook, Instagram, Threads, Snapchat, X (Twitter), Reddit, Pinterest, Tumblr, LinkedIn, 9GAG, VK, Bluesky, BeReal, Mastodon (mastodon.social), Weibo, OK, Imgur, Lemon8 |
messaging | Messaging | WhatsApp, Discord, Telegram, Signal, Viber, WeChat, LINE, KakaoTalk |
video | Video and streaming | YouTube, Twitch, Netflix, Prime Video, Disney+, Hulu, Vimeo, HBO Max, Paramount+, Peacock, Apple TV+, Crunchyroll, Plex, Kick, Dailymotion, Pluto TV, Tubi, Bilibili |
music | Music and audio | Spotify, SoundCloud, Deezer, TIDAL, Pandora, Apple Music, YouTube Music |
gaming | Gaming | Steam, Epic Games, Fortnite, Roblox, Minecraft, Riot Games, VALORANT, Xbox network, PlayStation Network, Nintendo Switch Online, Battle.net, EA app, Ubisoft Connect, GOG, itch.io, HoYoverse, Supercell, Rockstar Games, PUBG |
shopping | Shopping | Amazon, eBay, Temu, SHEIN, AliExpress, Etsy, Walmart |
dating | Dating | Tinder, Bumble, Hinge, OkCupid, Grindr, Match, Badoo, Plenty of Fish |
gambling | Gambling and betting | bet365, DraftKings, FanDuel, William Hill, Betfair, PokerStars, Stake, Betway |
news | News | CNN, BBC, The New York Times, Fox News, The Guardian |
forums | Forums and Q&A | Quora, 4chan, Stack Overflow and Stack Exchange |
productivity | Productivity and cloud storage | Dropbox, Google Drive, OneDrive, iCloud, Box, MEGA, WeTransfer, Notion, Slack, Zoom, MediaFire |
vpn | VPN and proxy | NordVPN, ExpressVPN, Proton VPN, Surfshark, Private Internet Access, CyberGhost, Windscribe, TunnelBear, Hotspot Shield, Psiphon, Tor Project |
crypto | Cryptocurrency exchanges | Binance, Coinbase, Kraken, Crypto.com, Bybit, OKX, KuCoin |
other | Other | none; imported services without a matching category |
Adult sites have no service entries: no vendor-neutral source lists them. Use the Adult content category of the Block List Project instead (content categories, below).
Blocked services and categories
- Each service compiles like a block list named
service:<id>, with adblock rules (||youtube.com^, which blocks the domain and its subdomains). The policy's block response and block TTL apply. blockedCategoriesblocks every service the catalogue in service has in those categories, as if each were listed inblockedServices. A service a catalogue update adds to a blocked category is blocked from the next snapshot on, without editing the policy.allowedServicesare exceptions toblockedCategoriesonly: a service listed there and in a blocked category is not blocked by this policy. It does not cancel an ID inblockedServices, nor a block of another policy that applies to the same client. The dashboard writes it when a service tile under a blocked category is clicked.GET /api/v1/policies/{ns}/{name}/statereportsservices(blockedServices),categories(with their service counts and exceptions),allowedServicesandeffectiveServices: what the policy blocks now, each markedvia: serviceorvia: category.- The query log shows status
blocked_list, listservice:youtubeand the rule that matched.GET /blocklists/_search?domain=...reports service lists too. - Allow lists still win. Put
music.youtube.comon an allow list of the same (or any applied) policy and it resolves while the rest of YouTube is blocked. - The response-side checks apply as for any list: a CNAME into a blocked
service's domain is blocked (
blocked_cname). - An ID the catalogue does not have is skipped. The Policy reports
Degradedwith reasonUnknownService(an unknown category:UnknownCategory), andGET /info/messageshas a warning. IDs are not an enum in the CRD because the catalogue can change at runtime; the condition clears when a catalogue update adds the ID. - Only first-party domains are listed. Shared infrastructure stays
reachable: Microsoft, Google and Apple sign-in, generic CDNs and cloud
storage, Amazon retail for Prime Video. Where blocking one service needs
another, the entry says so in
requiresand the grid offers to block it too: Fortnite requires Epic Games, VALORANT requires Riot Games.notecarries caveats such as "VPN apps connect to their servers by IP address: this blocks the website, sign-up and app updates, not an established tunnel", andrelatednames services of the same vendor.
Content categories
The Block List Project (blocklistproject/Lists) publishes one large list per topic. The list wizard offers them as well-known lists, and the Policy page and the policy wizard as "content categories":
| Content category | List (one domain per line) |
|---|---|
| Gambling, Adult content, Drugs, Vaping, Piracy, Torrent sites | gambling, porn, drugs, vaping, piracy, torrent |
| Malware, Phishing, Scams, Fraud, Ransomware, Cryptojacking, Malicious redirects | malware, phishing, scam, fraud, ransomware, crypto, redirect |
| Ads, Tracking, Smart TV telemetry | ads, tracking, smart-tv |
Turning one on creates, if it does not exist yet, the BlockList
blp-<id> in the policy's namespace with one URL source,
https://blocklistproject.github.io/Lists/alt-version/<list>-nl.txt
(format domains), and adds it to the policy's blockListRefs. Nothing
is embedded: the list is downloaded and refreshed like any URL list
(blocking.refreshInterval). Turning it off removes the reference and
keeps the BlockList for other policies. The malware list has about 2.6
million names and needs several hundred MB of memory.
Licence, as checked at commit ee3bdbaddf49c756cf3340bb2e6a9dee5282d1c6
(2026-07-20): the repository's LICENSE is the Unlicense (public domain)
and the list headers say MIT; several lists merge third-party upstream
lists (config/lists.yml), the gambling and porn lists among them HaGeZi's
(GPL-3.0). kudurru names the URLs only; each instance downloads the lists.
Safe search
With safeSearch: true the blocking stage answers the engines' host
names with the vendor's documented safe target:
| Engine | Host names | Answer |
|---|---|---|
www.google.<tld> for every domain on Google's published supported-domains list | CNAME forcesafesearch.google.com | |
| YouTube | www.youtube.com, m.youtube.com, youtubei.googleapis.com, youtube.googleapis.com, www.youtube-nocookie.com | CNAME restrict.youtube.com (strict) or restrictmoderate.youtube.com (safeSearchMode: moderate) |
| Bing | www.bing.com, edgeservices.bing.com | CNAME strict.bing.com |
| DuckDuckGo | duckduckgo.com | CNAME safe.duckduckgo.com |
| Brave Search | search.brave.com | CNAME forcesafe.search.brave.com |
| Yandex | yandex.ru | A 213.180.193.56 (Yandex documents an address, not a host name) |
| Pixabay | pixabay.com | CNAME safesearch.pixabay.com |
- The answer is the CNAME followed by the target's own records of the
question type, resolved through the cache and the client's upstream
group (the policy's
upstreamGroupapplies). A CNAME question gets the CNAME alone. The synthesised records carry the block TTL, so turning safe search off takes effect as quickly as an unblock. - The query log shows status
blocked_safesearch(classblocked), listsafesearch:<engine>and ruleCNAME <target>. With EDNS the answer carries EDE 4 (Forged Answer) naming the engine. The metric iskudurru_block_decisions_total{action="rewrite",source="safesearch"}. - An allow list match for the name skips safe search, as it skips every block.
safeSearchModeonly changes YouTube; every other engine has one level.- Like the block lists, safe search follows
blocking.enabledand the temporary disable (POST /dns/blocking): when blocking is off, nothing is rewritten. - Only the documented host names are rewritten. Browsers that resolve search through DNS over HTTPS to another resolver, or a VPN, bypass it, as they bypass any DNS filter. Block the encrypted resolvers with a block list when that matters.
Updating the catalogue at runtime
| Setting | Default | |
|---|---|---|
blocking.catalogue.url | empty | URL of a catalogue document; empty serves the embedded one |
blocking.catalogue.refreshInterval | 24h | how often the leader downloads it (at least 1m) |
Both need a restart. The leader downloads the URL with a conditional GET
(If-None-Match, If-Modified-Since) through the same guarded HTTP client
as BlockList URLs: loopback, link-local and metadata addresses need
blocking.allowLocalListURLs. Replicas pull the leader's copy over the
internal listener (GET /internal/catalogue, sha256-verified) every 5
minutes and download the URL themselves only after the leader has been
unreachable for 10 minutes. The last good download is kept in
<node.dataDir>/catalogue/ and served at the next start.
A document is put in service only after it parses and validates. A
schemaVersion this build does not read, a malformed document, an HTTP
error or a network error keeps the catalogue in service: the last good
download, else the embedded one. GET /api/v1/catalogue reports origin, version,
digest and refresh.lastError.
Document format (schema version 2)
{
"schemaVersion": 2,
"version": "2026.10.12",
"license": "Apache-2.0",
"homepage": "https://...",
"categories": [
{
"id": "gaming",
"name": "Gaming",
"description": "Game stores, console networks and online games.",
"icon": "gamepad-2",
"order": 60
}
],
"services": [
{
"id": "fortnite",
"name": "Fortnite",
"category": "gaming",
"rules": ["||fortnite.com^"],
"source": "first-party domains of the service",
"requires": ["epicgames"],
"note": "Fortnite signs in and updates through Epic Games, so block Epic Games as well to stop it."
},
{
"id": "epicgames",
"icon": "epic-games",
"name": "Epic Games",
"category": "gaming",
"rules": ["||epicgames.com^", "||epicgames.dev^"],
"source": "first-party domains of the service",
"related": ["fortnite"]
}
],
"safeSearch": [
{
"id": "youtube",
"name": "YouTube",
"target": "restrict.youtube.com",
"moderateTarget": "restrictmoderate.youtube.com",
"domains": ["www.youtube.com", "m.youtube.com"],
"source": "https://knowledge.workspace.google.com/admin/youtube/control-youtube-content-available-to-users"
},
{
"id": "yandex",
"name": "Yandex",
"addresses": ["213.180.193.56"],
"domains": ["yandex.ru"],
"source": "https://yandex.com/support/search/schoolsearch.html"
}
]
}
Rules this build checks (catalogue.Parse):
schemaVersionis2or1. Version 1 documents (nocategories, no hints) are still read: their services' category IDs get the names, descriptions and icons of the embedded catalogue's categories, and a category the embedded one lacks gets a name made from its ID ("retro-games" becomes "Retro games") and sorts last;requires,relatedandnoteare ignored in them. Any other version is refused, never half-read. Fields this build does not know are ignored, so a version may grow additively.versionandlicenseare non-empty.versionis the date,YYYY.MM.DD.- Category, service and engine
ids match^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$and are unique within their kind;nameandsourceare non-empty (at most 256 bytes). - Categories (version 2):
descriptionis optional (at most 256 bytes);iconis optional and names a Lucide icon in kebab-case (gamepad-2); the dashboard draws a tag for a name it does not bundle.ordersorts them (ascending, then by ID). At most 100. - A service's
categorynames one of the document's categories (version 2). iconis optional on services and engines and matches the ID pattern: the slug of a logo built into the binary (GET /api/v1/catalogue/icons), for when it differs from theid. Without it an entry uses the logo named like itsid, if there is one. A slug this build does not have is not an error: the entry has no logo.requiresandrelated(version 2, optional, at most 20 each) name other services of the same document;noteis optional, at most 256 bytes.- A service has 1 to 1000
rules, each a single adblock rule that compiles to a block entry: no@@exceptions, no modifiers, no cosmetic rules, no comments. - An engine has either
target(with an optionalmoderateTarget) oraddresses(unicast IPv4 or IPv6), and at least onedomain. Host names are lower-case A-labels without the trailing dot. A domain is rewritten by one engine only and is never its own target. - Limits: 4 MiB per document, 1000 services, 100 categories, 100 engines, 10,000 safe-search domains.
URL contract for the data repository
D60 puts the catalogue in a separate public data repository owned by the
project. kudurru expects, at a stable URL such as
https://raw.githubusercontent.com/<owner>/<repo>/main/v2/catalogue.json:
- one document per schema version, under a path that names the version
(
v1/catalogue.json,v2/catalogue.json), so a release that reads version 3 can move tov3/while older releases keep reading theirs; this build reads bothv1/andv2/documents; Content-Type: application/json, anETagorLast-Modifiedheader for conditional GETs, and HTTPS;- every entry with its
source, a permissive licence (Apache-2.0 or CC0) inlicense, and a provenance file in the repository. Nothing derived from GPL tables (AdGuard's blocked-services and safe-search lists) or any other copyleft source.
Until that repository exists, leave blocking.catalogue.url empty: the
embedded catalogue is complete, and each release ships the catalogue
current at its build.
AdGuard's services (opt-in, GPL-3.0)
| Setting | Default | |
|---|---|---|
blocking.catalogue.adguardServicesURL | empty (off) | URL of AdGuard's HostlistsRegistry blocked-services file, e.g. https://raw.githubusercontent.com/AdguardTeam/HostlistsRegistry/main/assets/services.json |
AdGuard's HostlistsRegistry lists about 140 services. Its data is GPL-3.0, so kudurru neither ships, vendors, pre-fetches nor derives from it: the default is off, nothing of it is in the binary, the images or the tests (they use a hand-written document in the same format with example.com domains), and kudurru's own catalogue is written without consulting it. Setting the URL makes your instance download it, the same way it downloads a block list you configure. Whether the GPL's terms matter to you depends on how you use and share the data; kudurru does not distribute it.
What the opt-in does:
- The leader downloads the file on
blocking.catalogue.refreshInterval(conditional GET, through the guarded list client; loopback and link-local URLs needblocking.allowLocalListURLs). Replicas pull the leader's copy over the internal listener (GET /internal/catalogue/adguard, sha256-verified) and download it themselves only after the leader has been unreachable for 10 minutes. The last good download is kept in<node.dataDir>/catalogue/adguard-services.jsonand served at the next start; a failed or empty download keeps the previous one. The setting needs a restart. - The file's format: an object with
blocked_services, each{id, name, rules, group}(a bare array is read too,icon_svgis ignored and the dashboard draws a monogram), at most 16 MiB and 1000 entries. IDs become kudurru IDs with_replaced by-(amazon_streamingbecomesamazon-streaming). Only rules of the form||<name>^are kept, where the name is below a public suffix (||tiktok.com^, not||com^,||co.uk^, an IP address or a regex), so a broken or hostile file cannot make a service, and every policy that blocks its category, block every name. Other rules are dropped, and entries without a usable rule, name or ID are skipped (GET /api/v1/cataloguecounts both). - Imported services join the catalogue next to kudurru's own, with
origin: adguard,license: GPL-3.0andsource: adguard-hostlists-registry; the grid marks them "AdGuard · GPL-3.0". On an ID both have, kudurru's entry wins and the imported one is left out (counted asshadowed), so turning the opt-in on never changes what an existing policy'sblockedServicesblock. Policies name imported services by ID like any other; turning the opt-in off makes those IDsUnknownService. - AdGuard's groups map to categories:
aitoai,dating,gambling,gamingandshoppingto the same IDs,messengertomessaging,social_networktosocial,streamingtovideo;cdn,hosting,privacy,softwareand anything else toother. A blocked category covers the imported services in it too. GET /api/v1/cataloguelists the activesources:kudurru(Apache-2.0, embedded orblocking.catalogue.url) and, when configured,adguard(GPL-3.0) with its URL, digest, service count, shadowed and skipped entries and refresh state. The Settings page shows the same under the setting.