# Copyright 2025 GNOME Foundation, Inc.
# SPDX-License-Identifier: CC-BY-SA-3.0

Web filtering feature design
===

Use cases
---

 * Allow parents to filter out age-inappropriate content on the web when it’s
   accessed by child accounts
   - This could involve preventing access to entire websites
   - Or categories of content
   - Or forcing websites to enter a ‘restricted mode’ where they only present
     certain kinds of content to the child (for example,
     [YouTube restricted mode](https://support.google.com/youtube/answer/174084?hl=en),
     or search engine ‘safe search’ modes)
 * Be reasonably resistant to a child’s attempts to circumvent web filtering,
   but this is not a hard security boundary
 * Not require the malcontent project to curate a filter list
   - It’s a lot of maintenance
   - We are not experts in this
   - But also not be too tied to a particular filter list, because they seem to
     come and go quite often
 * Work for all apps on the system
   - Many apps contain web views in one form or another
   - Some apps are native, some apps already have network sandboxing, but all
     need a consistent filter otherwise child accounts will use the app with the
     laxest filtering to access the internet
   - Needs to support three major web browsers for coverage of enough users:
     Epiphany, Firefox, Chromium
 * Do not break normal web browsing and security
   - Forcing TLS off, injecting traffic, spoofing root certificates, etc. are
     all approaches which some software has taken which fundamentally break web
     security
   - [Split DNS](https://blogs.gnome.org/mcatanzaro/2020/12/17/understanding-systemd-resolved-split-dns-and-vpn-configuration/)
     needs to be supported for requests which aren’t filtered
 * Support different web filters for different user accounts
   - For example, one child account might be younger and need more restrictive
     filtering; another account might only need ad blocking enabled
 * Allow web filtering to be temporarily bypassed with parental consent
   - For example, for a limited time period
 * Be easy to set up, in particular not require home network reconfiguration
   - Users who can do this have already set up their own pihole; we need to
     target the users who can’t do that

Architecture
---

 1. Insert an NSS module into `/etc/nsswitch.conf` at a high priority (e.g. just
    below `files` and `myhostname`)
    - NSS module reads per-user blocklists from files and either sinkholes
      request (or redirects to safe version, e.g. YouTube) or defers it to the
      next resolver in the NSS module list
    - This will need to be done on the system and in the relevant flatpak
      runtimes (they [ship their own `nsswitch.conf`](https://gitlab.com/freedesktop-sdk/freedesktop-sdk/-/blob/master/files/nsswitch-flatpak.conf?ref_type=heads))
 2. Leave the system DNS configuration untouched; verify that non-administrator
    users can’t change or override this
 3. Build the blocklist from various trusted sources of blocklists; these may
    change over time, as we won’t strongly tie ourselves to one blocklist
    provider
 4. malcontent orchestration component which builds blocklists for (3) and
    installs them in a place accessible to the NSS module
 5. Browser plugin which detects a DNS sinkhole and shows an error page which
    can call D-Bus methods on (4) to temporarily allow a page, etc.
    - Need to do further research to see if we can provide any information out
      of band in DNS sinkhole response, or whether browser plugin will need to
      query (6) out of band with site information, and display that (and do
      nothing if (6) says the site is not on the blocklist)
 6. Unconditionally add a canary domain to the blocklist to trigger browsers to
    disable DNS-over-HTTPS (DoH), so they use the NSS name resolver rather than
    their own code
    - See [‘Options for Providers of Parental Controls’](https://blog.mozilla.org/futurereleases/2019/09/06/whats-next-in-making-dns-over-https-the-default/)
 7. As we’re doing the blocking at the NSS layer, there should be no caching, so
    temporarily allowing a website would be a case of temporarily adding an
    allowlist file
    - malcontent D-Bus service and escalation agent to temporarily modify the
      allowlist and hang around (or arm a systemd timer to reset) until it
      should be blocked again
 8. Future work: run user’s session inside a network namespace (which exposes
    all interfaces unmodified) purely so we can add
    [`nftables`](man:nftables(8)) rules to block DNS traffic
    - i.e. Require all DNS goes through NSS
    - This will act as a backstop to block apps which are on very old flatpak
      runtimes (without our `nsswitch.conf` modifications) or those trying to
      circumvent the filtering
 9. Long-term future: the NSS module could potentially be superseded by
    [configuration within systemd-resolved itself](https://github.com/systemd/systemd/issues/17791)

---

One alternative DNS-based approach which was considered was to use a local
[`dnsmasq`](man:dnsmasq(8)) instance per child user, and use
[`iptables`](man:iptables(8)) rules to redirect all DNS traffic from inside a
network namespace for the child user to it. Filter rules would be implemented by
`dnsmasq` hosts files, and would have to have very short time-to-live (TTL)
times so they can be expired quickly for blocks to be bypassed with parental
consent.

The `iptables` rules and orchestration of processes inside and outwith of
network namespaces for the child user looked to be quite complex and fragile,
and the approach would not have handled child account processes which talked to
`systemd-resolved` directly (via varlink) or via the glibc NSS APIs.

---

Another alternative approach which was considered was to use a proxy-based
filter, [`privoxy`](https://www.privoxy.org/), running as a process outside a
network namespace for the child account’s processes. `nftables` rules would
redirect all traffic from the child account’s network namespace through the
`privoxy` process, and it would perform the filtering.

This would allow for in-page filtering and rewriting, which would in particular
allow for nice “access denied” pages. However, historically a lot of apps do not
work well with proxies, and it means that all network traffic would pass through
an extra process, significantly impacting on performance. Various web services
are already explicitly set up to work with DNS filtering (e.g. YouTube
restricted mode, various search engines’ safe search), which we could not use
from a proxy filter.

Data storage
---

The NSS module runs in-process in the child account’s processes, and name
resolution needs to be fast, so this leads to a single compiled filter list per
child user, readable (but not writeable) by their processes. As the NSS module
is unconditionally loaded in almost every process on the system, it must have
no dependencies (apart from libc), which means that the compiled filter list has
to be stored in a file (rather than accountsservice or over D-Bus).

Filters are provided online and can only be updated online. They are typically
split by use case, so one blocklist provider might provide several different
filters, one for advertising, one for gambling, and one for adult sites (for
example). These would need to be combined, and combined with any overrides set
by the parent, to build a compiled filter list for the child.

Filter providers issue periodic updates to their filters, so the system needs a
way to regularly download and apply those to the compiled filter lists.

There seems to be no authenticity or integrity protections applied to filters
from common providers, other than the filter being provided over HTTPS. We could
do trust-on-first-use for the TLS certificate for a filter provider, but TLS
certificates are rotated on such a frequent basis that this wouldn’t work.

If filter lists are configured as URIs, they must therefore be HTTPS. `file:`
URIs should also be disallowed, as implementing them would require the filter
updater to have arbitrary access to the local file system. That seems an
unnecessary risk for a process which also has to have network access.

So we end up with the following:
 - Configuration per child account stored in accountsservice, which lists the
   URIs of the blocklists and allowlists to be applied to their account, plus a
   boolean of whether to force safe search, and block/allow overrides set by
   the parent
 - Cache of downloaded filter lists, to reduce network traffic when updating
   compiled filter lists periodically
 - Per-user compiled filter list in a location readable by the NSS module

Filter updater
---

Given the above data storage requirements, a system daemon process is needed to
regularly download updates to the filters, and compile them into a compiled
filter list for each user.

This could run in two modes:
 1. On demand when a parent updates the filter settings for a specific child
    account, via a D-Bus call.
 2. Periodically, on a systemd timer, to check for updates to the filter lists
    for all child accounts.

In the second mode, there will need to be a notification mechanism to tell the
parent if a download failed (for example because a filter list is no longer
available). This will need to work even if the parent isn’t currently logged in,
so would need to be in a file rather than (for example) a D-Bus signal.

Flatpak
---

With the prevalence of sandboxed flatpak apps, web filtering needs to be
integrated into the flatpak sandbox so that it’s guaranteed to be enabled for
all flatpak apps, regardless of which runtime they’re using.

In particular, that means:
 * Binding the malcontent NSS module from the host into the sandbox.
 * Ensuring it’s findable by the runtime loader, i.e. is listed correctly in
   `ld.so.conf`/`ld.so.cache` or `LD_LIBRARY_PATH`.
 * Overriding the `/etc/nsswitch.conf` configuration from the flatpak runtime to
   add `malcontent` to the `hosts:` line from it.
 * Binding the filter lists directory from the host into the sandbox.

There’s a more detailed writeup of this in https://github.com/flatpak/flatpak/pull/6436.

References
---

 * YouTube restricted mode:
   - https://support.google.com/youtube/answer/174084?hl=en
   - https://support.google.com/a/answer/6214622?hl=en#zippy=%2Coption-dns
   - https://www.youtube.com/check_content_restrictions
 * Various DNS filters:
   - https://nextdns.io/
   - https://www.opendns.com/home-internet-security/
   - https://adguard-dns.io/en/welcome.html
 * Various existing FOSS projects:
   - https://gitlab.com/ctparentalgroup/CTparental (in particular, uses e2guardian for proxied content filtering)
     - It is also a 4000 line bash script though
   - https://sourceforge.net/projects/keexybox/ (appears to be dead?)
   - https://pi-hole.net/
   - https://github.com/endlessm/eos-theme/tree/master/safe-defaults
   - https://extensions.gnome.org/extension/7831/blocker/
     - Uses https://hblock.molinero.dev/
   - https://www.privoxy.org/
     - Packaged on Fedora
   - https://eblocker.org/en/
   - https://addons.mozilla.org/en-GB/firefox/addon/foxfilter/
 * Related network configuration guides:
   - [Example of configuring pihole](https://github.com/drewwats/Pi-Hole-Parental-Controls/blob/master/safesearch.sh)
   - https://www.monotux.tech/posts/2024/08/dnsmasq-netfilter/
   - https://lowb1rd.github.io/005-run-your-own-dns-over-https-doh-server-with-nginx.html
   - https://openwrt.org/docs/guide-user/services/dns/dot_dnsmasq_stubby
   - https://blogs.gnome.org/mcatanzaro/2020/12/17/understanding-systemd-resolved-split-dns-and-vpn-configuration/
 * Chromium’s DNS code is documented [here](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/net/dns/README.md)

