mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:27:02 +01:00
hosts/install: split into lib/ under the 250-line cap
913 lines down to 150. The ten functions were only 202 lines; the other 712 were a single top-level block, now eight phase functions across six libs. As in the previous split the bodies were wrapped WITHOUT re-indenting, because five heredocs use quoted terminators that must stay at column 0 (shfmt indented them afterwards, which is safe). The ~320-line custom-blocklist heredoc became custom_entries.hosts. It was 321 of the 370 lines of hosts_write.sh and cannot be split, so that lib could not otherwise fit the cap; a hosts-format data file is also far easier to audit and diff than a quoted heredoc. That move broke a guard, which is the important part of this commit: extract_custom_entries_from_script sliced the blocklist out of the script between "# Custom blocking entries" and the heredoc terminator. With the entries in a data file there is no terminator, so it would have extracted NOTHING -- and check_custom_entries_protection, the guard whose entire job is refusing to drop blocked domains, would have compared against an empty list and passed everything. It now reads the data file, and the extracted set is identical: 254 domains, diffed. A second, pre-existing bug surfaced while testing: extract_unblock_entries_from_script matched its OWN marker lines. PROTECTED_UNBLOCK_LIST_START and _END are word characters, so the '^# [a-zA-Z0-9._-]+$' pattern pulled both into the whitelist as if they were domains. Both are in this machine's live state file right now. Renaming either marker would have read as the whitelist growing and blocked the install outright. There is no dry-run or status mode, so this file cannot be proven by running it: an aborted run leaves /etc/hosts mutable with the guard's bind mount down. The proof is textual instead. Every one of the 8 wrapped phase bodies is byte-identical to its original line range; a line-multiset check found 0 lines lost; and an indentation-insensitive comparison against the pre-split file leaves exactly 8 absent lines, each an intentional edit (the rewritten extractor, three `readlink -f "$0"` made overridable, the /etc/hosts fallback made overridable, and the heredoc markers). /etc/hosts is byte-unchanged across the whole session, and `--help` output is identical. 36 assertions. Both anti-tamper guards at 100% line coverage (75/75 and 49/49), covering both directions of each: removing a blocked domain is refused and adding one allowed; growing the whitelist is refused and shrinking it allowed. A same-count swap is refused too, which a count-only check would have let through. The install phases themselves are deliberately untested -- running them would take down the protection they install. That is weaker evidence than the two previous splits had, and it is inherent to this file rather than a gap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
This commit is contained in:
parent
241652cade
commit
47478791d8
@ -0,0 +1,20 @@
|
||||
{
|
||||
"title": "Split hosts/install.sh under the 250-line cap with its anti-tamper guards fully covered",
|
||||
"objective": "Bring linux_configuration/scripts/periodic_background/hosts/install.sh from 913 lines under the repo-wide 250-line cap by moving its 712-line top-level block into eight phase functions across six libs, without changing what a real install would do. File 3 of the Phase 1 queue: 13 referencing files and the highest blast radius, since it rewrites /etc/hosts and takes the guard down to do it.",
|
||||
"acceptance_criteria": [
|
||||
"Every shell file — entry script, libs and tests — is at or under 250 lines.",
|
||||
"Each wrapped phase body is byte-identical to its original line range, proven by diff, because end-to-end execution is not available as evidence here.",
|
||||
"A content comparison against the pre-split file accounts for every absent line as an intentional edit.",
|
||||
"The 254 custom-blocked domains extracted by the guard are identical before and after the blocklist becomes a data file.",
|
||||
"Both anti-tamper guards reach 100% line coverage, with both directions of each covered: removal refused / addition allowed for the custom list, growth refused / shrinkage allowed for the whitelist.",
|
||||
"/etc/hosts is unchanged, verified by sha256sum, and the installer is never run end to end.",
|
||||
"shellcheck reports zero findings with no suppression added."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Running the install phases under test, which would rewrite /etc/hosts, unmount the guard's bind mount and restart systemd-resolved.",
|
||||
"Adding a dry-run or status mode, which would make end-to-end testing possible but is a feature change rather than a split.",
|
||||
"The SERVICES_ROOT-style path prefix that would make the phases testable; that is a separate follow-up.",
|
||||
"The three files still over the cap."
|
||||
],
|
||||
"verifier": "A per-phase body diff against the pre-wrap file, a line-multiset check for lost content, a diff of the 254 extracted domains, bash meta/scripts/verify_shell_split.sh 241652ca, lib/tests/run_all.sh (36 assertions), bash meta/scripts/shell_coverage.sh on both guards at 100, `bash install.sh --help` compared to pre-split, sha256sum -c on /etc/hosts, shellcheck, and check_file_length.sh --all."
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
{
|
||||
"intent": "Split hosts/install.sh (913 lines) under the 250-line cap, cover its two anti-tamper guards at 100%, and fix the defects the tests exposed. File 3 of the Phase 1 queue, and the one with the widest reference surface (13 files) and the highest blast radius.",
|
||||
"scope": [
|
||||
"linux_configuration/scripts/periodic_background/hosts/install.sh — 913 lines down to 150",
|
||||
"lib/hosts_protect_custom.sh, hosts_protect_unblock.sh, hosts_guard.sh, hosts_cache.sh, hosts_write.sh, hosts_browser_doh.sh — new",
|
||||
"custom_entries.hosts — the ~320-line blocklist heredoc, now a data file",
|
||||
"lib/tests/ — a harness, two test files and a runner",
|
||||
"Non-goal: running the installer end to end, which would rewrite /etc/hosts and take down the guard"
|
||||
],
|
||||
"changes": [
|
||||
"Wrapped the 712-line top-level block into eight phase functions and moved them into libs. Bodies were not re-indented at wrap time (five heredocs need column-0 terminators); shfmt indented them afterwards, which is why the verification below compares content rather than exact lines.",
|
||||
"Moved the ~320-line custom-blocklist heredoc into custom_entries.hosts. It was 321 of the 370 lines of hosts_write.sh and could not be split, so the lib could not otherwise fit the cap. A hosts-format data file is also easier to audit than a quoted heredoc.",
|
||||
"FIX 1 — moving the blocklist broke extract_custom_entries_from_script, which sliced between '# Custom blocking entries' and the heredoc terminator. With no terminator in the data file it would have extracted NOTHING, silently disabling the guard that refuses to drop blocked domains. It now reads the data file directly; the extracted set is identical (254 domains, diffed).",
|
||||
"FIX 2 — extract_unblock_entries_from_script matched its own marker lines. PROTECTED_UNBLOCK_LIST_START and _END are word characters, so the '^# [a-zA-Z0-9._-]+$' pattern pulled both into the whitelist as if they were domains. They are in the live state file on this machine right now. Renaming either marker would have read as the whitelist growing and blocked the install.",
|
||||
"Made three call sites overridable so the guards could be tested at all: HOSTS_INSTALL_SCRIPT_PATH for `readlink -f $0` and HOSTS_FILE_PATH for the /etc/hosts fallback read. Both default to the production value."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "Per-phase body diff against the pre-wrap file",
|
||||
"result": "pass",
|
||||
"evidence": "All 8 wrapped phase bodies byte-identical to their original line ranges at wrap time — enable_resolved_reads_hosts (23), stop_hosts_guard (24), refresh_upstream_cache (49), write_hosts_file (354), restart_hosts_guard (20), save_protection_state (25), disable_browser_doh (63), restart_browsers (33). A line-multiset comparison confirmed 0 original lines lost and only 62 wrapper lines added."
|
||||
},
|
||||
{
|
||||
"command": "Content comparison of the final tree against the pre-split file, ignoring indentation",
|
||||
"result": "pass",
|
||||
"evidence": "Exactly 8 original lines absent, each an intentional edit: 2 from the rewritten custom-entries extractor, 3 `readlink -f \"$0\"` made overridable, 1 /etc/hosts fallback made overridable, and the heredoc opener/terminator replaced by the data-file read. Nothing else changed."
|
||||
},
|
||||
{
|
||||
"command": "diff of the 254 custom-blocked domains, old heredoc vs new data file",
|
||||
"result": "pass",
|
||||
"evidence": "Identical sets. This is what proves FIX 1 preserved the guard's input rather than merely making it run."
|
||||
},
|
||||
{
|
||||
"command": "bash meta/scripts/verify_shell_split.sh 241652ca <old> <old> lib/hosts_*.sh",
|
||||
"result": "pass",
|
||||
"evidence": "8 additions (the phase wrappers) and one changed body — extract_custom_entries_from_script, the deliberate rewrite. Every other pre-existing function hashes identically."
|
||||
},
|
||||
{
|
||||
"command": "bash install.sh --help; sha256sum -c on /etc/hosts",
|
||||
"result": "pass",
|
||||
"evidence": "--help output identical to pre-split, and /etc/hosts unchanged across the entire session. --help is the only execution path that exits before any write; there is no dry-run or status mode, which is why end-to-end execution was not used as proof."
|
||||
},
|
||||
{
|
||||
"command": "lib/tests/run_all.sh",
|
||||
"result": "pass",
|
||||
"evidence": "36 assertions, 0 failures: test_hosts_protect.sh 25, test_hosts_cache.sh 11. Both directions of both guards are covered — removing a blocked domain is refused, adding one is allowed; growing the whitelist is refused, shrinking it is allowed. A same-count swap is also refused, which a count-only check would have passed."
|
||||
},
|
||||
{
|
||||
"command": "bash meta/scripts/shell_coverage.sh lib/tests/test_hosts_protect.sh <guard> 100",
|
||||
"result": "pass",
|
||||
"evidence": "hosts_protect_custom.sh 75/75 = 100.00%, hosts_protect_unblock.sh 49/49 = 100.00%. These are the two functions where a silent regression matters most."
|
||||
},
|
||||
{
|
||||
"command": "shellcheck install.sh lib/hosts_*.sh lib/tests/*.sh; check_file_length.sh --all",
|
||||
"result": "pass",
|
||||
"evidence": "Zero findings standalone and with -x, no suppression. No shell file over 250 lines; largest is install.sh at 150."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"The install phases have NO test coverage, and that is deliberate: they write /etc/hosts, unmount the guard's bind mount and restart systemd-resolved, so running them under test would take down the protection they install. Their proof is textual (byte-identical bodies) rather than behavioural, which is genuinely weaker evidence than the previous two splits had. Making them testable needs the SERVICES_ROOT-style prefix, which is a separate change.",
|
||||
"FIX 2 changes what the unblock guard sees: the live state file still holds the two phantom marker entries, so the next install reads the whitelist as having SHRUNK by 2. Shrinking is always allowed, so the install proceeds and rewrites the state without the phantoms. No manual step is needed, but the next run's output will report 2 removed entries.",
|
||||
"custom_entries.hosts is now the file to edit to change the blocklist, and check_custom_entries_protection watches it. Editing install.sh instead would no longer have any effect on what gets blocked."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert this commit restores the single file, the heredoc and the original extractors together.",
|
||||
"After rollback, run `bash install.sh --help` and confirm it still exits 0, and check that /etc/hosts is untouched — the revert changes no system state by itself."
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,329 @@
|
||||
# Custom blocking entries appended to /etc/hosts by lib/hosts_write.sh.
|
||||
#
|
||||
# Kept as data rather than as a heredoc inside the script: at ~320 lines it was
|
||||
# most of that file and pushed it over the repo-wide 250-line cap, and a plain
|
||||
# hosts-format file is also easier to diff and to audit than a quoted heredoc.
|
||||
# The content is byte-identical to the heredoc it replaces.
|
||||
#
|
||||
# Format is standard /etc/hosts: "0.0.0.0 domain" to block, "#" to comment.
|
||||
# check_custom_entries_protection reads this same file to detect entries being
|
||||
# removed, so edits here are what the anti-tamper guard is watching.
|
||||
|
||||
# Custom blocking entries
|
||||
# YouTube
|
||||
0.0.0.0 youtube.com
|
||||
0.0.0.0 www.youtube.com
|
||||
0.0.0.0 m.youtube.com
|
||||
0.0.0.0 youtu.be
|
||||
0.0.0.0 youtube-nocookie.com
|
||||
0.0.0.0 www.youtube-nocookie.com
|
||||
0.0.0.0 youtubei.googleapis.com
|
||||
0.0.0.0 youtube.googleapis.com
|
||||
0.0.0.0 yt3.ggpht.com
|
||||
# Channel avatars and banners. Same role as yt3.ggpht.com, which was already
|
||||
# here -- Google serves them from both hosts, so blocking one leaves the other
|
||||
# resolving. Caught when the phone's DoT resolver was checked domain by domain
|
||||
# against the 14 the focus policy names.
|
||||
0.0.0.0 yt3.googleusercontent.com
|
||||
0.0.0.0 ytimg.com
|
||||
0.0.0.0 i.ytimg.com
|
||||
0.0.0.0 s.ytimg.com
|
||||
0.0.0.0 i9.ytimg.com
|
||||
0.0.0.0 googlevideo.com
|
||||
0.0.0.0 r1---sn-4g5e6nls.googlevideo.com
|
||||
0.0.0.0 r1---sn-4g5lne7s.googlevideo.com
|
||||
|
||||
# Alternative YouTube Frontends (Invidious, Piped, etc.)
|
||||
0.0.0.0 invidious.io
|
||||
0.0.0.0 www.invidious.io
|
||||
0.0.0.0 invidio.us
|
||||
0.0.0.0 vid.puffyan.us
|
||||
0.0.0.0 invidious.snopyta.org
|
||||
0.0.0.0 yewtu.be
|
||||
0.0.0.0 invidious.kavin.rocks
|
||||
0.0.0.0 inv.riverside.rocks
|
||||
0.0.0.0 invidious.namazso.eu
|
||||
0.0.0.0 invidious.nerdvpn.de
|
||||
0.0.0.0 invidious.projectsegfau.lt
|
||||
0.0.0.0 invidious.slipfox.xyz
|
||||
0.0.0.0 invidious.privacydev.net
|
||||
0.0.0.0 invidious.perennialte.ch
|
||||
0.0.0.0 invidious.protokoll-11.de
|
||||
0.0.0.0 invidious.einfachzocken.eu
|
||||
0.0.0.0 invidious.fdn.fr
|
||||
0.0.0.0 inv.in.projectsegfau.lt
|
||||
0.0.0.0 invidious.tiekoetter.com
|
||||
0.0.0.0 invidious.lunar.icu
|
||||
0.0.0.0 iv.ggtyler.dev
|
||||
0.0.0.0 iv.melmac.space
|
||||
0.0.0.0 piped.video
|
||||
0.0.0.0 www.piped.video
|
||||
0.0.0.0 piped.kavin.rocks
|
||||
0.0.0.0 piped.mha.fi
|
||||
0.0.0.0 piped.mint.lgbt
|
||||
0.0.0.0 piped.projectsegfau.lt
|
||||
0.0.0.0 piped.privacydev.net
|
||||
0.0.0.0 piped.smnz.de
|
||||
0.0.0.0 piped.adminforge.de
|
||||
0.0.0.0 watch.whatever.social
|
||||
0.0.0.0 piped.lunar.icu
|
||||
0.0.0.0 viewtube.io
|
||||
0.0.0.0 www.viewtube.io
|
||||
0.0.0.0 freetube.io
|
||||
0.0.0.0 www.freetube.io
|
||||
0.0.0.0 tubo.media
|
||||
0.0.0.0 www.tubo.media
|
||||
0.0.0.0 materialious.nadeko.net
|
||||
0.0.0.0 clipious.org
|
||||
0.0.0.0 www.clipious.org
|
||||
0.0.0.0 newpipe.net
|
||||
0.0.0.0 www.newpipe.net
|
||||
0.0.0.0 newpipe.schabi.org
|
||||
0.0.0.0 grayjay.app
|
||||
0.0.0.0 www.grayjay.app
|
||||
0.0.0.0 libretube.dev
|
||||
0.0.0.0 www.libretube.dev
|
||||
0.0.0.0 hyperion.deishelon.com
|
||||
0.0.0.0 inv.n8pjl.ca
|
||||
0.0.0.0 inv.zzls.xyz
|
||||
0.0.0.0 inv.tux.pizza
|
||||
0.0.0.0 invidious.incogniweb.net
|
||||
0.0.0.0 invidious.drgns.space
|
||||
0.0.0.0 invidious.io.lol
|
||||
|
||||
# Steam Store
|
||||
0.0.0.0 store.steampowered.com
|
||||
0.0.0.0 checkout.steampowered.com
|
||||
0.0.0.0 store.akamai.steamstatic.com
|
||||
0.0.0.0 storefront.steampowered.com
|
||||
0.0.0.0 store.cloudflare.steamstatic.com
|
||||
|
||||
# Discord - media allowed
|
||||
# 0.0.0.0 cdn.discordapp.com
|
||||
# 0.0.0.0 media.discordapp.net
|
||||
# 0.0.0.0 images-ext-1.discordapp.net
|
||||
# 0.0.0.0 images-ext-2.discordapp.net
|
||||
# 0.0.0.0 attachments-1.discordapp.net
|
||||
# 0.0.0.0 attachments-2.discordapp.net
|
||||
# 0.0.0.0 tenor.com
|
||||
# 0.0.0.0 giphy.com
|
||||
|
||||
# Food Delivery Services
|
||||
# Polish services
|
||||
0.0.0.0 pyszne.pl
|
||||
0.0.0.0 www.pyszne.pl
|
||||
0.0.0.0 m.pyszne.pl
|
||||
0.0.0.0 api.pyszne.pl
|
||||
0.0.0.0 app.pyszne.pl
|
||||
0.0.0.0 glovo.com
|
||||
0.0.0.0 www.glovo.com
|
||||
0.0.0.0 m.glovo.com
|
||||
0.0.0.0 api.glovo.com
|
||||
0.0.0.0 glovoapp.com
|
||||
0.0.0.0 www.glovoapp.com
|
||||
0.0.0.0 bolt.eu
|
||||
:: bolt.eu
|
||||
0.0.0.0 www.bolt.eu
|
||||
:: www.bolt.eu
|
||||
0.0.0.0 food.bolt.eu
|
||||
:: food.bolt.eu
|
||||
0.0.0.0 m.bolt.eu
|
||||
:: m.bolt.eu
|
||||
0.0.0.0 api.bolt.eu
|
||||
:: api.bolt.eu
|
||||
0.0.0.0 node.bolt.eu
|
||||
:: node.bolt.eu
|
||||
0.0.0.0 gw.bolt.eu
|
||||
:: gw.bolt.eu
|
||||
0.0.0.0 client-api.bolt.eu
|
||||
:: client-api.bolt.eu
|
||||
0.0.0.0 auth.bolt.eu
|
||||
:: auth.bolt.eu
|
||||
0.0.0.0 cdn.bolt.eu
|
||||
:: cdn.bolt.eu
|
||||
0.0.0.0 images.bolt.eu
|
||||
:: images.bolt.eu
|
||||
0.0.0.0 static.bolt.eu
|
||||
:: static.bolt.eu
|
||||
0.0.0.0 assets.bolt.eu
|
||||
:: assets.bolt.eu
|
||||
0.0.0.0 fleet.bolt.eu
|
||||
:: fleet.bolt.eu
|
||||
0.0.0.0 user.bolt.eu
|
||||
:: user.bolt.eu
|
||||
0.0.0.0 courier.bolt.eu
|
||||
:: courier.bolt.eu
|
||||
0.0.0.0 rider.bolt.eu
|
||||
:: rider.bolt.eu
|
||||
0.0.0.0 restaurant.bolt.eu
|
||||
:: restaurant.bolt.eu
|
||||
0.0.0.0 partner-food.bolt.eu
|
||||
:: partner-food.bolt.eu
|
||||
0.0.0.0 woltwojta.pl
|
||||
0.0.0.0 www.woltwojta.pl
|
||||
0.0.0.0 wolt.com
|
||||
0.0.0.0 www.wolt.com
|
||||
0.0.0.0 m.wolt.com
|
||||
0.0.0.0 api.wolt.com
|
||||
0.0.0.0 restaurant-api.wolt.com
|
||||
0.0.0.0 consumer-api.wolt.com
|
||||
0.0.0.0 jush.pl
|
||||
0.0.0.0 www.jush.pl
|
||||
0.0.0.0 m.jush.pl
|
||||
0.0.0.0 api.jush.pl
|
||||
0.0.0.0 delio.pl
|
||||
:: delio.pl
|
||||
0.0.0.0 www.delio.pl
|
||||
:: www.delio.pl
|
||||
0.0.0.0 m.delio.pl
|
||||
:: m.delio.pl
|
||||
0.0.0.0 api.delio.pl
|
||||
:: api.delio.pl
|
||||
0.0.0.0 app.delio.pl
|
||||
:: app.delio.pl
|
||||
0.0.0.0 cdn.delio.pl
|
||||
:: cdn.delio.pl
|
||||
0.0.0.0 static.delio.pl
|
||||
:: static.delio.pl
|
||||
0.0.0.0 order.delio.pl
|
||||
:: order.delio.pl
|
||||
0.0.0.0 delio.com
|
||||
:: delio.com
|
||||
0.0.0.0 www.delio.com
|
||||
:: www.delio.com
|
||||
0.0.0.0 m.delio.com
|
||||
:: m.delio.com
|
||||
0.0.0.0 api.delio.com
|
||||
:: api.delio.com
|
||||
0.0.0.0 app.delio.com
|
||||
:: app.delio.com
|
||||
0.0.0.0 delio.com.pl
|
||||
:: delio.com.pl
|
||||
0.0.0.0 www.delio.com.pl
|
||||
:: www.delio.com.pl
|
||||
0.0.0.0 m.delio.com.pl
|
||||
:: m.delio.com.pl
|
||||
0.0.0.0 api.delio.com.pl
|
||||
:: api.delio.com.pl
|
||||
0.0.0.0 app.delio.com.pl
|
||||
:: app.delio.com.pl
|
||||
0.0.0.0 lisek.app
|
||||
0.0.0.0 www.lisek.app
|
||||
0.0.0.0 api.lisek.app
|
||||
0.0.0.0 stava.app
|
||||
0.0.0.0 www.stava.app
|
||||
0.0.0.0 api.stava.app
|
||||
0.0.0.0 biedronka.pl
|
||||
0.0.0.0 zakupy.biedronka.pl
|
||||
0.0.0.0 ezakupy.tesco.pl
|
||||
0.0.0.0 www.ezakupy.tesco.pl
|
||||
0.0.0.0 barbora.pl
|
||||
0.0.0.0 www.barbora.pl
|
||||
0.0.0.0 api.barbora.pl
|
||||
0.0.0.0 frisco.pl
|
||||
0.0.0.0 www.frisco.pl
|
||||
0.0.0.0 api.frisco.pl
|
||||
0.0.0.0 swiatkwiatow.pl
|
||||
0.0.0.0 www.swiatkwiatow.pl
|
||||
0.0.0.0 allegro.pl/kategoria/jedzenie
|
||||
0.0.0.0 szama.pl
|
||||
0.0.0.0 www.szama.pl
|
||||
0.0.0.0 api.szama.pl
|
||||
0.0.0.0 auchandirect.pl
|
||||
0.0.0.0 www.auchandirect.pl
|
||||
|
||||
# International services
|
||||
0.0.0.0 ubereats.com
|
||||
0.0.0.0 www.ubereats.com
|
||||
0.0.0.0 m.ubereats.com
|
||||
0.0.0.0 api.ubereats.com
|
||||
0.0.0.0 uber.com
|
||||
0.0.0.0 www.uber.com
|
||||
0.0.0.0 m.uber.com
|
||||
0.0.0.0 api.uber.com
|
||||
0.0.0.0 cn-geo1.uber.com
|
||||
0.0.0.0 login.uber.com
|
||||
0.0.0.0 auth.uber.com
|
||||
0.0.0.0 riders.uber.com
|
||||
0.0.0.0 deliveroo.com
|
||||
0.0.0.0 www.deliveroo.com
|
||||
0.0.0.0 m.deliveroo.com
|
||||
0.0.0.0 api.deliveroo.com
|
||||
0.0.0.0 deliveroo.co.uk
|
||||
0.0.0.0 www.deliveroo.co.uk
|
||||
0.0.0.0 foodpanda.com
|
||||
0.0.0.0 www.foodpanda.com
|
||||
0.0.0.0 m.foodpanda.com
|
||||
0.0.0.0 api.foodpanda.com
|
||||
0.0.0.0 grubhub.com
|
||||
0.0.0.0 www.grubhub.com
|
||||
0.0.0.0 m.grubhub.com
|
||||
0.0.0.0 api.grubhub.com
|
||||
0.0.0.0 doordash.com
|
||||
0.0.0.0 www.doordash.com
|
||||
0.0.0.0 m.doordash.com
|
||||
0.0.0.0 api.doordash.com
|
||||
0.0.0.0 justeat.com
|
||||
0.0.0.0 www.justeat.com
|
||||
0.0.0.0 m.justeat.com
|
||||
0.0.0.0 justeat.co.uk
|
||||
0.0.0.0 www.justeat.co.uk
|
||||
0.0.0.0 postmates.com
|
||||
0.0.0.0 www.postmates.com
|
||||
0.0.0.0 api.postmates.com
|
||||
0.0.0.0 seamless.com
|
||||
0.0.0.0 www.seamless.com
|
||||
0.0.0.0 menulog.com.au
|
||||
0.0.0.0 www.menulog.com.au
|
||||
0.0.0.0 delivery.com
|
||||
0.0.0.0 www.delivery.com
|
||||
0.0.0.0 getir.com
|
||||
0.0.0.0 www.getir.com
|
||||
0.0.0.0 api.getir.com
|
||||
0.0.0.0 flink.com
|
||||
0.0.0.0 www.flink.com
|
||||
0.0.0.0 api.flink.com
|
||||
0.0.0.0 gorillas.io
|
||||
0.0.0.0 www.gorillas.io
|
||||
0.0.0.0 api.gorillas.io
|
||||
0.0.0.0 gopuff.com
|
||||
0.0.0.0 www.gopuff.com
|
||||
0.0.0.0 api.gopuff.com
|
||||
0.0.0.0 instacart.com
|
||||
0.0.0.0 www.instacart.com
|
||||
0.0.0.0 api.instacart.com
|
||||
0.0.0.0 takeaway.com
|
||||
0.0.0.0 www.takeaway.com
|
||||
0.0.0.0 api.takeaway.com
|
||||
|
||||
# Fast food chain apps and websites
|
||||
0.0.0.0 mcdonalds.com
|
||||
0.0.0.0 www.mcdonalds.com
|
||||
0.0.0.0 m.mcdonalds.com
|
||||
0.0.0.0 mcdonalds.pl
|
||||
0.0.0.0 www.mcdonalds.pl
|
||||
0.0.0.0 kfc.com
|
||||
0.0.0.0 www.kfc.com
|
||||
0.0.0.0 m.kfc.com
|
||||
0.0.0.0 kfc.pl
|
||||
0.0.0.0 www.kfc.pl
|
||||
0.0.0.0 burgerking.com
|
||||
0.0.0.0 www.burgerking.com
|
||||
0.0.0.0 m.burgerking.com
|
||||
0.0.0.0 burgerking.pl
|
||||
0.0.0.0 www.burgerking.pl
|
||||
0.0.0.0 pizzahut.com
|
||||
0.0.0.0 www.pizzahut.com
|
||||
0.0.0.0 m.pizzahut.com
|
||||
0.0.0.0 pizzahut.pl
|
||||
0.0.0.0 www.pizzahut.pl
|
||||
0.0.0.0 dominos.com
|
||||
0.0.0.0 www.dominos.com
|
||||
0.0.0.0 m.dominos.com
|
||||
0.0.0.0 dominos.pl
|
||||
0.0.0.0 www.dominos.pl
|
||||
0.0.0.0 subway.com
|
||||
0.0.0.0 www.subway.com
|
||||
0.0.0.0 m.subway.com
|
||||
0.0.0.0 subway.pl
|
||||
0.0.0.0 www.subway.pl
|
||||
@ -25,6 +25,23 @@ for arg in "$@"; do
|
||||
esac
|
||||
done
|
||||
|
||||
# Each phase of the install lives in a lib beside this file; this script
|
||||
# keeps the flag parsing, the protection-check gates and the order the
|
||||
# phases run in.
|
||||
LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib"
|
||||
# shellcheck source=lib/hosts_protect_custom.sh
|
||||
. "$LIB_DIR/hosts_protect_custom.sh"
|
||||
# shellcheck source=lib/hosts_protect_unblock.sh
|
||||
. "$LIB_DIR/hosts_protect_unblock.sh"
|
||||
# shellcheck source=lib/hosts_guard.sh
|
||||
. "$LIB_DIR/hosts_guard.sh"
|
||||
# shellcheck source=lib/hosts_cache.sh
|
||||
. "$LIB_DIR/hosts_cache.sh"
|
||||
# shellcheck source=lib/hosts_write.sh
|
||||
. "$LIB_DIR/hosts_write.sh"
|
||||
# shellcheck source=lib/hosts_browser_doh.sh
|
||||
. "$LIB_DIR/hosts_browser_doh.sh"
|
||||
|
||||
# ============================================================================
|
||||
# CUSTOM ENTRIES PROTECTION MECHANISM
|
||||
# ============================================================================
|
||||
@ -37,129 +54,6 @@ done
|
||||
CUSTOM_ENTRIES_STATE_FILE="/etc/hosts.custom-entries.state"
|
||||
UNBLOCK_STATE_FILE="/etc/hosts.unblock-entries.state"
|
||||
|
||||
# Extract custom blocked entries from a hosts file or heredoc section
|
||||
# Returns only the "0.0.0.0 domain.com" lines (normalized, sorted, unique)
|
||||
extract_custom_entries_from_script() {
|
||||
# Extract entries from the heredoc in this script (between EOF markers after "Custom blocking entries")
|
||||
local script_path="$1"
|
||||
sed -n '/^# Custom blocking entries$/,/^EOF$/p' "$script_path" |
|
||||
grep -E '^0\.0\.0\.0[[:space:]]+' |
|
||||
awk '{print $2}' |
|
||||
sort -u
|
||||
}
|
||||
|
||||
# Extract custom entries from the current /etc/hosts (entries after "# Custom blocking entries" marker)
|
||||
extract_custom_entries_from_hosts() {
|
||||
local hosts_file="$1"
|
||||
if [[ ! -f $hosts_file ]]; then
|
||||
return
|
||||
fi
|
||||
sed -n '/^# Custom blocking entries$/,$p' "$hosts_file" |
|
||||
grep -E '^0\.0\.0\.0[[:space:]]+' |
|
||||
awk '{print $2}' |
|
||||
sort -u
|
||||
}
|
||||
|
||||
# Load previously saved custom entries state
|
||||
load_saved_custom_entries() {
|
||||
if [[ -f $CUSTOM_ENTRIES_STATE_FILE ]]; then
|
||||
sort -u "$CUSTOM_ENTRIES_STATE_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Save current custom entries to state file
|
||||
save_custom_entries_state() {
|
||||
local entries="$1"
|
||||
echo "$entries" | sort -u >"$CUSTOM_ENTRIES_STATE_FILE"
|
||||
chmod 644 "$CUSTOM_ENTRIES_STATE_FILE"
|
||||
chattr +i "$CUSTOM_ENTRIES_STATE_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Helper function to count non-empty lines
|
||||
count_lines() {
|
||||
local input="$1"
|
||||
if [[ -z $input ]]; then
|
||||
echo 0
|
||||
else
|
||||
echo "$input" | grep -c . 2>/dev/null || echo 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Main protection check
|
||||
check_custom_entries_protection() {
|
||||
local script_path
|
||||
script_path="$(readlink -f "$0")"
|
||||
|
||||
# Get new entries from the script's heredoc
|
||||
local new_entries
|
||||
new_entries=$(extract_custom_entries_from_script "$script_path")
|
||||
local new_count
|
||||
new_count=$(count_lines "$new_entries")
|
||||
|
||||
# Get saved/existing entries (prefer state file, fall back to current /etc/hosts)
|
||||
local saved_entries
|
||||
saved_entries=$(load_saved_custom_entries)
|
||||
if [[ -z $saved_entries ]]; then
|
||||
# First run or state file missing - extract from current /etc/hosts if it has our marker
|
||||
saved_entries=$(extract_custom_entries_from_hosts "/etc/hosts")
|
||||
fi
|
||||
local saved_count
|
||||
saved_count=$(count_lines "$saved_entries")
|
||||
|
||||
# If no saved state exists, this is first installation - allow it
|
||||
if [[ $saved_count -eq 0 ]]; then
|
||||
echo "ℹ️ First installation detected - no protection check needed."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Find entries that were removed
|
||||
local removed_entries
|
||||
removed_entries=$(comm -23 <(echo "$saved_entries") <(echo "$new_entries"))
|
||||
local removed_count
|
||||
removed_count=$(count_lines "$removed_entries")
|
||||
|
||||
# Find entries that are new
|
||||
local added_entries
|
||||
added_entries=$(comm -13 <(echo "$saved_entries") <(echo "$new_entries"))
|
||||
local added_count
|
||||
added_count=$(count_lines "$added_entries")
|
||||
|
||||
echo ""
|
||||
echo "📊 Custom Entries Protection Check:"
|
||||
echo " Previously blocked: $saved_count entries"
|
||||
echo " Currently in script: $new_count entries"
|
||||
echo " Removed: $removed_count | Added: $added_count"
|
||||
|
||||
# RULE 1: No entries removed - always OK
|
||||
if [[ $removed_count -eq 0 ]]; then
|
||||
echo " ✅ No entries removed - protection check passed."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# RULE 2: Entries were removed - BLOCK INSTALLATION
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo " ❌ INSTALLATION BLOCKED - CUSTOM ENTRIES REMOVED"
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
echo "You are attempting to REMOVE the following blocked entries:"
|
||||
while IFS= read -r entry; do
|
||||
echo " - $entry"
|
||||
done <<<"$removed_entries"
|
||||
echo ""
|
||||
echo "This is NOT allowed. The only way to unblock sites is to:"
|
||||
echo ""
|
||||
echo " 1. Manually edit /etc/hosts (requires removing chattr protection)"
|
||||
echo " 2. Delete the state file /etc/hosts.custom-entries.state"
|
||||
echo " (also protected with chattr)"
|
||||
echo ""
|
||||
echo "These manual steps are intentionally difficult to prevent"
|
||||
echo "impulsive unblocking. If you really need to unblock something,"
|
||||
echo "you'll have to work for it."
|
||||
echo ""
|
||||
return 1
|
||||
}
|
||||
|
||||
# Run the protection check
|
||||
if ! check_custom_entries_protection; then
|
||||
exit 1
|
||||
@ -192,709 +86,16 @@ fi
|
||||
# licdn.com
|
||||
# PROTECTED_UNBLOCK_LIST_END
|
||||
|
||||
# Extract whitelisted domains from the protected list embedded in this script
|
||||
extract_unblock_entries_from_script() {
|
||||
local script_path="$1"
|
||||
sed -n '/^# PROTECTED_UNBLOCK_LIST_START$/,/^# PROTECTED_UNBLOCK_LIST_END$/p' "$script_path" |
|
||||
grep -E '^# [a-zA-Z0-9._-]+$' |
|
||||
sed 's/^# //' |
|
||||
sort -u
|
||||
}
|
||||
|
||||
# Save current unblock entries to immutable state file
|
||||
save_unblock_entries_state() {
|
||||
local entries="$1"
|
||||
chattr -i "$UNBLOCK_STATE_FILE" 2>/dev/null || true
|
||||
echo "$entries" | sort -u >"$UNBLOCK_STATE_FILE"
|
||||
chmod 644 "$UNBLOCK_STATE_FILE"
|
||||
chattr +i "$UNBLOCK_STATE_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Block installation if the unblock list has grown (more sites being whitelisted)
|
||||
check_unblock_entries_protection() {
|
||||
local script_path
|
||||
script_path="$(readlink -f "$0")"
|
||||
|
||||
local new_entries
|
||||
new_entries=$(extract_unblock_entries_from_script "$script_path")
|
||||
local new_count
|
||||
new_count=$(count_lines "$new_entries")
|
||||
|
||||
if [[ ! -f $UNBLOCK_STATE_FILE ]]; then
|
||||
echo "ℹ️ First unblock-list run — no protection check needed."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local saved_entries
|
||||
saved_entries=$(sort -u "$UNBLOCK_STATE_FILE")
|
||||
local saved_count
|
||||
saved_count=$(count_lines "$saved_entries")
|
||||
|
||||
# Entries added since last install
|
||||
local added_entries
|
||||
added_entries=$(comm -13 <(echo "$saved_entries") <(echo "$new_entries"))
|
||||
local added_count
|
||||
added_count=$(count_lines "$added_entries")
|
||||
|
||||
echo ""
|
||||
echo "📊 Unblock Entries Protection Check:"
|
||||
echo " Previously whitelisted: $saved_count domains"
|
||||
echo " Currently in script: $new_count domains"
|
||||
echo " Newly added: $added_count"
|
||||
|
||||
if [[ $added_count -eq 0 ]]; then
|
||||
echo " ✅ No new unblocks — protection check passed."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo " ❌ INSTALLATION BLOCKED — NEW UNBLOCK ENTRIES DETECTED"
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
echo "You are attempting to WHITELIST these additional domains:"
|
||||
while IFS= read -r entry; do
|
||||
echo " + $entry"
|
||||
done <<<"$added_entries"
|
||||
echo ""
|
||||
echo "To proceed, manually delete the state file first:"
|
||||
echo " sudo chattr -i $UNBLOCK_STATE_FILE && sudo rm $UNBLOCK_STATE_FILE"
|
||||
echo ""
|
||||
return 1
|
||||
}
|
||||
|
||||
# Run the unblock protection check
|
||||
if ! check_unblock_entries_protection; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Enable systemd-resolved
|
||||
sudo systemctl enable systemd-resolved
|
||||
|
||||
# ============================================================================
|
||||
# ENSURE systemd-resolved READS /etc/hosts
|
||||
# ============================================================================
|
||||
# Without this, systemd-resolved ignores /etc/hosts entries entirely,
|
||||
# allowing blocked domains to resolve via DNS.
|
||||
RESOLVED_CONF="/etc/systemd/resolved.conf"
|
||||
if grep -q '^ReadEtcHosts=no' "$RESOLVED_CONF" 2>/dev/null; then
|
||||
echo "Fixing systemd-resolved: setting ReadEtcHosts=yes..."
|
||||
sudo sed -i 's/^ReadEtcHosts=no/ReadEtcHosts=yes/' "$RESOLVED_CONF"
|
||||
sudo systemctl restart systemd-resolved
|
||||
elif ! grep -q '^ReadEtcHosts=yes' "$RESOLVED_CONF" 2>/dev/null; then
|
||||
echo "Enabling ReadEtcHosts=yes in systemd-resolved..."
|
||||
if grep -q '^#ReadEtcHosts=' "$RESOLVED_CONF" 2>/dev/null; then
|
||||
sudo sed -i 's/^#ReadEtcHosts=.*/ReadEtcHosts=yes/' "$RESOLVED_CONF"
|
||||
else
|
||||
echo 'ReadEtcHosts=yes' | sudo tee -a "$RESOLVED_CONF" >/dev/null
|
||||
fi
|
||||
sudo systemctl restart systemd-resolved
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# TEMPORARILY DISABLE HOSTS GUARD PROTECTIONS FOR INSTALLATION
|
||||
# ============================================================================
|
||||
# The guard system uses a read-only bind mount and path watcher that prevent
|
||||
# any writes to /etc/hosts. We must stop them before installing, then restart.
|
||||
GUARD_SERVICES_STOPPED=0
|
||||
|
||||
for svc in hosts-bind-mount.service hosts-guard.path; do
|
||||
if systemctl is-active --quiet "$svc" 2>/dev/null; then
|
||||
echo "Stopping $svc for installation..."
|
||||
systemctl stop "$svc" 2>/dev/null || true
|
||||
GUARD_SERVICES_STOPPED=1
|
||||
fi
|
||||
done
|
||||
|
||||
# If bind mount is still active, unmount it
|
||||
if findmnt /etc/hosts >/dev/null 2>&1; then
|
||||
echo "Unmounting read-only bind mount on /etc/hosts..."
|
||||
umount /etc/hosts 2>/dev/null || mount -o remount,rw,bind /etc/hosts 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Remove all attributes from /etc/hosts to allow modifications
|
||||
sudo chattr -i -a /etc/hosts 2>/dev/null || true
|
||||
|
||||
# Source and local cache configuration
|
||||
URL="https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling-porn-social/hosts"
|
||||
# Cache stores the RAW upstream file (without our custom modifications)
|
||||
LOCAL_CACHE="/etc/hosts.stevenblack"
|
||||
|
||||
# Helpers
|
||||
extract_date_epoch_from_file() {
|
||||
# Grep "# Date:" line and convert to epoch seconds (UTC)
|
||||
local f="$1"
|
||||
local line
|
||||
line=$(grep -m1 '^# Date:' "$f" 2>/dev/null | sed -E 's/^# Date:[[:space:]]*(.*)[[:space:]]*\(UTC\).*/\1 UTC/')
|
||||
if [[ -n $line ]]; then
|
||||
date -u -d "$line" +%s 2>/dev/null || echo ""
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_remote_header() {
|
||||
# Try to fetch only the first ~4KB using HTTP Range; fallback to piping to head
|
||||
local out="$1"
|
||||
if curl -LfsS --max-time 10 -H 'Range: bytes=0-4095' "$URL" -o "$out"; then
|
||||
return 0
|
||||
fi
|
||||
# Fallback – may download more, but we only keep first lines
|
||||
if curl -LfsS --max-time 10 "$URL" | head -n 20 >"$out"; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
download_remote_full_to() {
|
||||
local out="$1"
|
||||
curl -LfsS "$URL" -o "$out"
|
||||
}
|
||||
|
||||
# Decide whether to use cache or update
|
||||
TMP_REMOTE_HEAD=$(mktemp)
|
||||
trap 'rm -f "$TMP_REMOTE_HEAD"' EXIT
|
||||
|
||||
REMOTE_AVAILABLE=0
|
||||
if fetch_remote_header "$TMP_REMOTE_HEAD"; then
|
||||
REMOTE_AVAILABLE=1
|
||||
fi
|
||||
|
||||
NEED_UPDATE=0
|
||||
|
||||
if [[ -f $LOCAL_CACHE ]]; then
|
||||
local_epoch=$(extract_date_epoch_from_file "$LOCAL_CACHE")
|
||||
else
|
||||
local_epoch=""
|
||||
fi
|
||||
|
||||
if [[ $REMOTE_AVAILABLE -eq 1 ]]; then
|
||||
remote_epoch=$(extract_date_epoch_from_file "$TMP_REMOTE_HEAD")
|
||||
if [[ -n $local_epoch && -n $remote_epoch && $local_epoch -ge $remote_epoch ]]; then
|
||||
echo "Using cached StevenBlack hosts (up-to-date)."
|
||||
else
|
||||
echo "Cached version is missing or outdated; downloading latest StevenBlack hosts..."
|
||||
NEED_UPDATE=1
|
||||
fi
|
||||
else
|
||||
if [[ -f $LOCAL_CACHE ]]; then
|
||||
echo "No internet; using cached StevenBlack hosts."
|
||||
else
|
||||
echo "Error: No internet and no cached StevenBlack hosts found." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ensure we have a fresh cache if needed
|
||||
if [[ $NEED_UPDATE -eq 1 ]]; then
|
||||
TMP_DL=$(mktemp)
|
||||
if download_remote_full_to "$TMP_DL"; then
|
||||
# Save raw upstream to cache
|
||||
sudo mv "$TMP_DL" "$LOCAL_CACHE"
|
||||
sudo chmod 644 "$LOCAL_CACHE"
|
||||
echo "Saved latest StevenBlack hosts to cache: $LOCAL_CACHE"
|
||||
else
|
||||
rm -f "$TMP_DL"
|
||||
echo "Error: Failed to download latest StevenBlack hosts." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install the base hosts from cache into /etc/hosts
|
||||
echo "Installing base hosts from cache to /etc/hosts..."
|
||||
sudo cp "$LOCAL_CACHE" /etc/hosts
|
||||
|
||||
# Comment out any 4chan blocking entries from the downloaded file
|
||||
echo "Allowing 4chan by commenting out any blocking entries..."
|
||||
sudo sed -i 's/^0\.0\.0\.0 4chan\.com/#0.0.0.0 4chan.com/' /etc/hosts
|
||||
sudo sed -i 's/^0\.0\.0\.0 www\.4chan\.com/#0.0.0.0 www.4chan.com/' /etc/hosts
|
||||
sudo sed -i 's/^0\.0\.0\.0 4chan\.org/#0.0.0.0 4chan.org/' /etc/hosts
|
||||
sudo sed -i 's/^0\.0\.0\.0 boards\.4chan\.org/#0.0.0.0 boards.4chan.org/' /etc/hosts
|
||||
sudo sed -i 's/^0\.0\.0\.0 sys\.4chan\.org/#0.0.0.0 sys.4chan.org/' /etc/hosts
|
||||
sudo sed -i 's/^0\.0\.0\.0 www\.4chan\.org/#0.0.0.0 www.4chan.org/' /etc/hosts
|
||||
sudo sed -i -E 's/^(0\.0\.0\.0[[:space:]]+[a-zA-Z0-9._-]*\.?facebook\.com)/#\1/' /etc/hosts
|
||||
sudo sed -i -E 's/^(0\.0\.0\.0[[:space:]]+[a-zA-Z0-9._-]*\.?messenger\.com)/#\1/' /etc/hosts
|
||||
sudo sed -i -E 's/^(0\.0\.0\.0[[:space:]]+[a-zA-Z0-9._-]*\.?fbcdn\.net)/#\1/' /etc/hosts
|
||||
sudo sed -i -E 's/^(0\.0\.0\.0[[:space:]]+[a-zA-Z0-9._-]*\.?facebook\.net)/#\1/' /etc/hosts
|
||||
sudo sed -i 's/^0\.0\.0\.0 delio\.com.pl/#0.0.0.0 delio.com.pl/' /etc/hosts
|
||||
sudo sed -i 's/^0\.0\.0\.0 loverslab\.com/#0.0.0.0 loverslab.com/' /etc/hosts
|
||||
|
||||
# Allow LinkedIn and all subdomains (linkedin.com + licdn.com CDN)
|
||||
echo "Allowing LinkedIn by commenting out any blocking entries..."
|
||||
sudo sed -i -E 's/^(0\.0\.0\.0[[:space:]]+[a-zA-Z0-9._-]*\.?linkedin\.com)/#\1/' /etc/hosts
|
||||
sudo sed -i -E 's/^(0\.0\.0\.0[[:space:]]+[a-zA-Z0-9._-]*\.?licdn\.com)/#\1/' /etc/hosts
|
||||
sudo sed -i -E 's/^(0\.0\.0\.0[[:space:]]+[a-zA-Z0-9._-]*\.?loverslab\.com)/#\1/' /etc/hosts
|
||||
|
||||
# Add custom entries for YouTube and Discord
|
||||
echo "Adding custom entries for YouTube and Discord..."
|
||||
tee -a /etc/hosts >/dev/null <<'EOF'
|
||||
|
||||
# Custom blocking entries
|
||||
# YouTube
|
||||
0.0.0.0 youtube.com
|
||||
0.0.0.0 www.youtube.com
|
||||
0.0.0.0 m.youtube.com
|
||||
0.0.0.0 youtu.be
|
||||
0.0.0.0 youtube-nocookie.com
|
||||
0.0.0.0 www.youtube-nocookie.com
|
||||
0.0.0.0 youtubei.googleapis.com
|
||||
0.0.0.0 youtube.googleapis.com
|
||||
0.0.0.0 yt3.ggpht.com
|
||||
# Channel avatars and banners. Same role as yt3.ggpht.com, which was already
|
||||
# here -- Google serves them from both hosts, so blocking one leaves the other
|
||||
# resolving. Caught when the phone's DoT resolver was checked domain by domain
|
||||
# against the 14 the focus policy names.
|
||||
0.0.0.0 yt3.googleusercontent.com
|
||||
0.0.0.0 ytimg.com
|
||||
0.0.0.0 i.ytimg.com
|
||||
0.0.0.0 s.ytimg.com
|
||||
0.0.0.0 i9.ytimg.com
|
||||
0.0.0.0 googlevideo.com
|
||||
0.0.0.0 r1---sn-4g5e6nls.googlevideo.com
|
||||
0.0.0.0 r1---sn-4g5lne7s.googlevideo.com
|
||||
|
||||
# Alternative YouTube Frontends (Invidious, Piped, etc.)
|
||||
0.0.0.0 invidious.io
|
||||
0.0.0.0 www.invidious.io
|
||||
0.0.0.0 invidio.us
|
||||
0.0.0.0 vid.puffyan.us
|
||||
0.0.0.0 invidious.snopyta.org
|
||||
0.0.0.0 yewtu.be
|
||||
0.0.0.0 invidious.kavin.rocks
|
||||
0.0.0.0 inv.riverside.rocks
|
||||
0.0.0.0 invidious.namazso.eu
|
||||
0.0.0.0 invidious.nerdvpn.de
|
||||
0.0.0.0 invidious.projectsegfau.lt
|
||||
0.0.0.0 invidious.slipfox.xyz
|
||||
0.0.0.0 invidious.privacydev.net
|
||||
0.0.0.0 invidious.perennialte.ch
|
||||
0.0.0.0 invidious.protokoll-11.de
|
||||
0.0.0.0 invidious.einfachzocken.eu
|
||||
0.0.0.0 invidious.fdn.fr
|
||||
0.0.0.0 inv.in.projectsegfau.lt
|
||||
0.0.0.0 invidious.tiekoetter.com
|
||||
0.0.0.0 invidious.lunar.icu
|
||||
0.0.0.0 iv.ggtyler.dev
|
||||
0.0.0.0 iv.melmac.space
|
||||
0.0.0.0 piped.video
|
||||
0.0.0.0 www.piped.video
|
||||
0.0.0.0 piped.kavin.rocks
|
||||
0.0.0.0 piped.mha.fi
|
||||
0.0.0.0 piped.mint.lgbt
|
||||
0.0.0.0 piped.projectsegfau.lt
|
||||
0.0.0.0 piped.privacydev.net
|
||||
0.0.0.0 piped.smnz.de
|
||||
0.0.0.0 piped.adminforge.de
|
||||
0.0.0.0 watch.whatever.social
|
||||
0.0.0.0 piped.lunar.icu
|
||||
0.0.0.0 viewtube.io
|
||||
0.0.0.0 www.viewtube.io
|
||||
0.0.0.0 freetube.io
|
||||
0.0.0.0 www.freetube.io
|
||||
0.0.0.0 tubo.media
|
||||
0.0.0.0 www.tubo.media
|
||||
0.0.0.0 materialious.nadeko.net
|
||||
0.0.0.0 clipious.org
|
||||
0.0.0.0 www.clipious.org
|
||||
0.0.0.0 newpipe.net
|
||||
0.0.0.0 www.newpipe.net
|
||||
0.0.0.0 newpipe.schabi.org
|
||||
0.0.0.0 grayjay.app
|
||||
0.0.0.0 www.grayjay.app
|
||||
0.0.0.0 libretube.dev
|
||||
0.0.0.0 www.libretube.dev
|
||||
0.0.0.0 hyperion.deishelon.com
|
||||
0.0.0.0 inv.n8pjl.ca
|
||||
0.0.0.0 inv.zzls.xyz
|
||||
0.0.0.0 inv.tux.pizza
|
||||
0.0.0.0 invidious.incogniweb.net
|
||||
0.0.0.0 invidious.drgns.space
|
||||
0.0.0.0 invidious.io.lol
|
||||
|
||||
# Steam Store
|
||||
0.0.0.0 store.steampowered.com
|
||||
0.0.0.0 checkout.steampowered.com
|
||||
0.0.0.0 store.akamai.steamstatic.com
|
||||
0.0.0.0 storefront.steampowered.com
|
||||
0.0.0.0 store.cloudflare.steamstatic.com
|
||||
|
||||
# Discord - media allowed
|
||||
# 0.0.0.0 cdn.discordapp.com
|
||||
# 0.0.0.0 media.discordapp.net
|
||||
# 0.0.0.0 images-ext-1.discordapp.net
|
||||
# 0.0.0.0 images-ext-2.discordapp.net
|
||||
# 0.0.0.0 attachments-1.discordapp.net
|
||||
# 0.0.0.0 attachments-2.discordapp.net
|
||||
# 0.0.0.0 tenor.com
|
||||
# 0.0.0.0 giphy.com
|
||||
|
||||
# Food Delivery Services
|
||||
# Polish services
|
||||
0.0.0.0 pyszne.pl
|
||||
0.0.0.0 www.pyszne.pl
|
||||
0.0.0.0 m.pyszne.pl
|
||||
0.0.0.0 api.pyszne.pl
|
||||
0.0.0.0 app.pyszne.pl
|
||||
0.0.0.0 glovo.com
|
||||
0.0.0.0 www.glovo.com
|
||||
0.0.0.0 m.glovo.com
|
||||
0.0.0.0 api.glovo.com
|
||||
0.0.0.0 glovoapp.com
|
||||
0.0.0.0 www.glovoapp.com
|
||||
0.0.0.0 bolt.eu
|
||||
:: bolt.eu
|
||||
0.0.0.0 www.bolt.eu
|
||||
:: www.bolt.eu
|
||||
0.0.0.0 food.bolt.eu
|
||||
:: food.bolt.eu
|
||||
0.0.0.0 m.bolt.eu
|
||||
:: m.bolt.eu
|
||||
0.0.0.0 api.bolt.eu
|
||||
:: api.bolt.eu
|
||||
0.0.0.0 node.bolt.eu
|
||||
:: node.bolt.eu
|
||||
0.0.0.0 gw.bolt.eu
|
||||
:: gw.bolt.eu
|
||||
0.0.0.0 client-api.bolt.eu
|
||||
:: client-api.bolt.eu
|
||||
0.0.0.0 auth.bolt.eu
|
||||
:: auth.bolt.eu
|
||||
0.0.0.0 cdn.bolt.eu
|
||||
:: cdn.bolt.eu
|
||||
0.0.0.0 images.bolt.eu
|
||||
:: images.bolt.eu
|
||||
0.0.0.0 static.bolt.eu
|
||||
:: static.bolt.eu
|
||||
0.0.0.0 assets.bolt.eu
|
||||
:: assets.bolt.eu
|
||||
0.0.0.0 fleet.bolt.eu
|
||||
:: fleet.bolt.eu
|
||||
0.0.0.0 user.bolt.eu
|
||||
:: user.bolt.eu
|
||||
0.0.0.0 courier.bolt.eu
|
||||
:: courier.bolt.eu
|
||||
0.0.0.0 rider.bolt.eu
|
||||
:: rider.bolt.eu
|
||||
0.0.0.0 restaurant.bolt.eu
|
||||
:: restaurant.bolt.eu
|
||||
0.0.0.0 partner-food.bolt.eu
|
||||
:: partner-food.bolt.eu
|
||||
0.0.0.0 woltwojta.pl
|
||||
0.0.0.0 www.woltwojta.pl
|
||||
0.0.0.0 wolt.com
|
||||
0.0.0.0 www.wolt.com
|
||||
0.0.0.0 m.wolt.com
|
||||
0.0.0.0 api.wolt.com
|
||||
0.0.0.0 restaurant-api.wolt.com
|
||||
0.0.0.0 consumer-api.wolt.com
|
||||
0.0.0.0 jush.pl
|
||||
0.0.0.0 www.jush.pl
|
||||
0.0.0.0 m.jush.pl
|
||||
0.0.0.0 api.jush.pl
|
||||
0.0.0.0 delio.pl
|
||||
:: delio.pl
|
||||
0.0.0.0 www.delio.pl
|
||||
:: www.delio.pl
|
||||
0.0.0.0 m.delio.pl
|
||||
:: m.delio.pl
|
||||
0.0.0.0 api.delio.pl
|
||||
:: api.delio.pl
|
||||
0.0.0.0 app.delio.pl
|
||||
:: app.delio.pl
|
||||
0.0.0.0 cdn.delio.pl
|
||||
:: cdn.delio.pl
|
||||
0.0.0.0 static.delio.pl
|
||||
:: static.delio.pl
|
||||
0.0.0.0 order.delio.pl
|
||||
:: order.delio.pl
|
||||
0.0.0.0 delio.com
|
||||
:: delio.com
|
||||
0.0.0.0 www.delio.com
|
||||
:: www.delio.com
|
||||
0.0.0.0 m.delio.com
|
||||
:: m.delio.com
|
||||
0.0.0.0 api.delio.com
|
||||
:: api.delio.com
|
||||
0.0.0.0 app.delio.com
|
||||
:: app.delio.com
|
||||
0.0.0.0 delio.com.pl
|
||||
:: delio.com.pl
|
||||
0.0.0.0 www.delio.com.pl
|
||||
:: www.delio.com.pl
|
||||
0.0.0.0 m.delio.com.pl
|
||||
:: m.delio.com.pl
|
||||
0.0.0.0 api.delio.com.pl
|
||||
:: api.delio.com.pl
|
||||
0.0.0.0 app.delio.com.pl
|
||||
:: app.delio.com.pl
|
||||
0.0.0.0 lisek.app
|
||||
0.0.0.0 www.lisek.app
|
||||
0.0.0.0 api.lisek.app
|
||||
0.0.0.0 stava.app
|
||||
0.0.0.0 www.stava.app
|
||||
0.0.0.0 api.stava.app
|
||||
0.0.0.0 biedronka.pl
|
||||
0.0.0.0 zakupy.biedronka.pl
|
||||
0.0.0.0 ezakupy.tesco.pl
|
||||
0.0.0.0 www.ezakupy.tesco.pl
|
||||
0.0.0.0 barbora.pl
|
||||
0.0.0.0 www.barbora.pl
|
||||
0.0.0.0 api.barbora.pl
|
||||
0.0.0.0 frisco.pl
|
||||
0.0.0.0 www.frisco.pl
|
||||
0.0.0.0 api.frisco.pl
|
||||
0.0.0.0 swiatkwiatow.pl
|
||||
0.0.0.0 www.swiatkwiatow.pl
|
||||
0.0.0.0 allegro.pl/kategoria/jedzenie
|
||||
0.0.0.0 szama.pl
|
||||
0.0.0.0 www.szama.pl
|
||||
0.0.0.0 api.szama.pl
|
||||
0.0.0.0 auchandirect.pl
|
||||
0.0.0.0 www.auchandirect.pl
|
||||
|
||||
# International services
|
||||
0.0.0.0 ubereats.com
|
||||
0.0.0.0 www.ubereats.com
|
||||
0.0.0.0 m.ubereats.com
|
||||
0.0.0.0 api.ubereats.com
|
||||
0.0.0.0 uber.com
|
||||
0.0.0.0 www.uber.com
|
||||
0.0.0.0 m.uber.com
|
||||
0.0.0.0 api.uber.com
|
||||
0.0.0.0 cn-geo1.uber.com
|
||||
0.0.0.0 login.uber.com
|
||||
0.0.0.0 auth.uber.com
|
||||
0.0.0.0 riders.uber.com
|
||||
0.0.0.0 deliveroo.com
|
||||
0.0.0.0 www.deliveroo.com
|
||||
0.0.0.0 m.deliveroo.com
|
||||
0.0.0.0 api.deliveroo.com
|
||||
0.0.0.0 deliveroo.co.uk
|
||||
0.0.0.0 www.deliveroo.co.uk
|
||||
0.0.0.0 foodpanda.com
|
||||
0.0.0.0 www.foodpanda.com
|
||||
0.0.0.0 m.foodpanda.com
|
||||
0.0.0.0 api.foodpanda.com
|
||||
0.0.0.0 grubhub.com
|
||||
0.0.0.0 www.grubhub.com
|
||||
0.0.0.0 m.grubhub.com
|
||||
0.0.0.0 api.grubhub.com
|
||||
0.0.0.0 doordash.com
|
||||
0.0.0.0 www.doordash.com
|
||||
0.0.0.0 m.doordash.com
|
||||
0.0.0.0 api.doordash.com
|
||||
0.0.0.0 justeat.com
|
||||
0.0.0.0 www.justeat.com
|
||||
0.0.0.0 m.justeat.com
|
||||
0.0.0.0 justeat.co.uk
|
||||
0.0.0.0 www.justeat.co.uk
|
||||
0.0.0.0 postmates.com
|
||||
0.0.0.0 www.postmates.com
|
||||
0.0.0.0 api.postmates.com
|
||||
0.0.0.0 seamless.com
|
||||
0.0.0.0 www.seamless.com
|
||||
0.0.0.0 menulog.com.au
|
||||
0.0.0.0 www.menulog.com.au
|
||||
0.0.0.0 delivery.com
|
||||
0.0.0.0 www.delivery.com
|
||||
0.0.0.0 getir.com
|
||||
0.0.0.0 www.getir.com
|
||||
0.0.0.0 api.getir.com
|
||||
0.0.0.0 flink.com
|
||||
0.0.0.0 www.flink.com
|
||||
0.0.0.0 api.flink.com
|
||||
0.0.0.0 gorillas.io
|
||||
0.0.0.0 www.gorillas.io
|
||||
0.0.0.0 api.gorillas.io
|
||||
0.0.0.0 gopuff.com
|
||||
0.0.0.0 www.gopuff.com
|
||||
0.0.0.0 api.gopuff.com
|
||||
0.0.0.0 instacart.com
|
||||
0.0.0.0 www.instacart.com
|
||||
0.0.0.0 api.instacart.com
|
||||
0.0.0.0 takeaway.com
|
||||
0.0.0.0 www.takeaway.com
|
||||
0.0.0.0 api.takeaway.com
|
||||
|
||||
# Fast food chain apps and websites
|
||||
0.0.0.0 mcdonalds.com
|
||||
0.0.0.0 www.mcdonalds.com
|
||||
0.0.0.0 m.mcdonalds.com
|
||||
0.0.0.0 mcdonalds.pl
|
||||
0.0.0.0 www.mcdonalds.pl
|
||||
0.0.0.0 kfc.com
|
||||
0.0.0.0 www.kfc.com
|
||||
0.0.0.0 m.kfc.com
|
||||
0.0.0.0 kfc.pl
|
||||
0.0.0.0 www.kfc.pl
|
||||
0.0.0.0 burgerking.com
|
||||
0.0.0.0 www.burgerking.com
|
||||
0.0.0.0 m.burgerking.com
|
||||
0.0.0.0 burgerking.pl
|
||||
0.0.0.0 www.burgerking.pl
|
||||
0.0.0.0 pizzahut.com
|
||||
0.0.0.0 www.pizzahut.com
|
||||
0.0.0.0 m.pizzahut.com
|
||||
0.0.0.0 pizzahut.pl
|
||||
0.0.0.0 www.pizzahut.pl
|
||||
0.0.0.0 dominos.com
|
||||
0.0.0.0 www.dominos.com
|
||||
0.0.0.0 m.dominos.com
|
||||
0.0.0.0 dominos.pl
|
||||
0.0.0.0 www.dominos.pl
|
||||
0.0.0.0 subway.com
|
||||
0.0.0.0 www.subway.com
|
||||
0.0.0.0 m.subway.com
|
||||
0.0.0.0 subway.pl
|
||||
0.0.0.0 www.subway.pl
|
||||
EOF
|
||||
|
||||
# Set proper permissions (readable by all, writable only by root)
|
||||
sudo chmod 644 /etc/hosts
|
||||
|
||||
# Make the file immutable and append-only for maximum protection
|
||||
sudo chattr +ia /etc/hosts
|
||||
|
||||
# ============================================================================
|
||||
# RESTART HOSTS GUARD SERVICES
|
||||
# ============================================================================
|
||||
if [[ $GUARD_SERVICES_STOPPED -eq 1 ]]; then
|
||||
echo "Restarting hosts guard services..."
|
||||
# Update the canonical copy so the guard doesn't revert our changes
|
||||
if [[ -f /usr/local/share/locked-hosts ]]; then
|
||||
cp /etc/hosts /usr/local/share/locked-hosts
|
||||
echo " Updated canonical snapshot."
|
||||
fi
|
||||
for svc in hosts-bind-mount.service hosts-guard.path; do
|
||||
if systemctl is-enabled --quiet "$svc" 2>/dev/null; then
|
||||
systemctl start "$svc" 2>/dev/null || true
|
||||
echo " Restarted $svc"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# SAVE CUSTOM ENTRIES STATE FOR FUTURE PROTECTION CHECKS
|
||||
# ============================================================================
|
||||
echo "Saving custom entries state for protection mechanism..."
|
||||
script_path="$(readlink -f "$0")"
|
||||
current_custom_entries=$(extract_custom_entries_from_script "$script_path")
|
||||
# Remove immutable from state file if it exists
|
||||
chattr -i "$CUSTOM_ENTRIES_STATE_FILE" 2>/dev/null || true
|
||||
save_custom_entries_state "$current_custom_entries"
|
||||
echo "✅ Custom entries state saved to $CUSTOM_ENTRIES_STATE_FILE"
|
||||
|
||||
# Save unblock entries state for future protection checks
|
||||
current_unblock_entries=$(extract_unblock_entries_from_script "$script_path")
|
||||
save_unblock_entries_state "$current_unblock_entries"
|
||||
echo "✅ Unblock entries state saved to $UNBLOCK_STATE_FILE"
|
||||
|
||||
# Optionally flush DNS caches
|
||||
if [[ $FLUSH_DNS -eq 1 ]]; then
|
||||
echo "Flushing DNS caches..."
|
||||
sudo systemd-resolve --flush-caches
|
||||
sudo systemctl restart NetworkManager.service
|
||||
else
|
||||
echo "DNS cache flush skipped (use --flush-dns to enable)."
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# DISABLE DNS OVER HTTPS (DoH) IN BROWSERS
|
||||
# ============================================================================
|
||||
# DoH bypasses /etc/hosts entirely, defeating all our blocking!
|
||||
# We disable it in Firefox profiles for all users.
|
||||
echo ""
|
||||
echo "Disabling DNS over HTTPS (DoH) in browsers..."
|
||||
|
||||
# Get the actual user (not root) who invoked this script
|
||||
REAL_USER="${SUDO_USER:-$USER}"
|
||||
REAL_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6)
|
||||
|
||||
# Firefox: disable DoH via user.js
|
||||
if [[ -d "$REAL_HOME/.mozilla/firefox" ]]; then
|
||||
for profile in "$REAL_HOME/.mozilla/firefox"/*.default*; do
|
||||
if [[ -d "$profile" ]]; then
|
||||
cat >>"$profile/user.js" <<'FIREFOXEOF'
|
||||
// Disable DNS over HTTPS (DoH) to ensure /etc/hosts blocking works
|
||||
// Added by linux-configuration hosts installer
|
||||
user_pref("network.trr.mode", 5); // 5 = Off by user choice
|
||||
user_pref("doh-rollout.enabled", false);
|
||||
user_pref("doh-rollout.disable-heuristics", true);
|
||||
FIREFOXEOF
|
||||
chown "$REAL_USER:$REAL_USER" "$profile/user.js"
|
||||
echo " Firefox DoH disabled in: $(basename "$profile")"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo " No Firefox profiles found"
|
||||
fi
|
||||
|
||||
# Chromium-based browsers: use policy file
|
||||
CHROME_POLICY_DIR="/etc/chromium/policies/managed"
|
||||
if [[ -d "/etc/chromium" ]] || command -v chromium &>/dev/null; then
|
||||
mkdir -p "$CHROME_POLICY_DIR"
|
||||
cat >"$CHROME_POLICY_DIR/disable-doh.json" <<'CHROMEEOF'
|
||||
{
|
||||
"DnsOverHttpsMode": "off",
|
||||
"BuiltInDnsClientEnabled": false
|
||||
}
|
||||
CHROMEEOF
|
||||
echo " Chromium DoH disabled via policy"
|
||||
fi
|
||||
|
||||
# Google Chrome policy
|
||||
GCHROME_POLICY_DIR="/etc/opt/chrome/policies/managed"
|
||||
if [[ -d "/etc/opt/chrome" ]] || command -v google-chrome &>/dev/null; then
|
||||
mkdir -p "$GCHROME_POLICY_DIR"
|
||||
cat >"$GCHROME_POLICY_DIR/disable-doh.json" <<'GCHROMEEOF'
|
||||
{
|
||||
"DnsOverHttpsMode": "off",
|
||||
"BuiltInDnsClientEnabled": false
|
||||
}
|
||||
GCHROMEEOF
|
||||
echo " Google Chrome DoH disabled via policy"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ Installation complete!"
|
||||
echo " Custom entries protection is now active."
|
||||
echo " Removing blocked entries from the script will be blocked."
|
||||
echo " DNS over HTTPS (DoH) has been disabled in browsers."
|
||||
|
||||
# ============================================================================
|
||||
# FORCE BROWSER RESTART TO APPLY DOH CHANGES
|
||||
# ============================================================================
|
||||
# Kill all browser processes so DoH changes take effect immediately
|
||||
echo ""
|
||||
echo "Killing browsers to apply DoH policy changes..."
|
||||
BROWSERS_KILLED=0
|
||||
|
||||
for browser in chrome chromium chromium-browser brave brave-browser firefox firefox-esr thorium vivaldi opera; do
|
||||
if pgrep -x "$browser" &>/dev/null || pgrep -f "/opt/.*/$browser" &>/dev/null; then
|
||||
echo " Killing $browser..."
|
||||
pkill -9 -f "$browser" 2>/dev/null || true
|
||||
BROWSERS_KILLED=1
|
||||
fi
|
||||
done
|
||||
|
||||
# Also kill by common binary paths
|
||||
for pattern in "/opt/google/chrome" "/opt/brave" "/opt/thorium" "/usr/lib/firefox" "/usr/lib/chromium"; do
|
||||
if pgrep -f "$pattern" &>/dev/null; then
|
||||
echo " Killing processes matching $pattern..."
|
||||
pkill -9 -f "$pattern" 2>/dev/null || true
|
||||
BROWSERS_KILLED=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $BROWSERS_KILLED -eq 1 ]]; then
|
||||
echo ""
|
||||
echo "⚠️ Browsers were killed to apply DNS settings."
|
||||
echo " Reopen your browser - hosts blocking is now enforced."
|
||||
else
|
||||
echo " No browsers were running."
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# NOTE: this used to `chattr +i` its own source and generate_hosts_file.sh
|
||||
# ("lock against silent edits"). Do NOT reintroduce that: making a git-tracked
|
||||
# file immutable breaks git tooling. `pre-commit run --all-files` (which the
|
||||
@ -911,3 +112,19 @@ fi
|
||||
# changes nothing until install.sh is re-run as root, which regenerates the
|
||||
# guarded artifacts.
|
||||
# ============================================================================
|
||||
|
||||
# ============================================================================
|
||||
# MAIN
|
||||
# ============================================================================
|
||||
# The phases above are defined in the order they run, and run here in that same
|
||||
# order. Split out of a single top-level block so the file fits the 250-line
|
||||
# cap; the sequence is unchanged, including the guard being taken down before
|
||||
# the write and restarted immediately after it.
|
||||
enable_resolved_reads_hosts
|
||||
stop_hosts_guard
|
||||
refresh_upstream_cache
|
||||
write_hosts_file
|
||||
restart_hosts_guard
|
||||
save_protection_state
|
||||
disable_browser_doh
|
||||
restart_browsers
|
||||
|
||||
112
linux_configuration/scripts/periodic_background/hosts/lib/hosts_browser_doh.sh
Executable file
112
linux_configuration/scripts/periodic_background/hosts/lib/hosts_browser_doh.sh
Executable file
@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/hosts_browser_doh.sh — stop browsers resolving around /etc/hosts.
|
||||
#
|
||||
# DNS-over-HTTPS resolves names over the browser's own encrypted channel and
|
||||
# never consults /etc/hosts, so leaving it on defeats every entry this script
|
||||
# installs. Sourced by install.sh.
|
||||
|
||||
# Turn off DNS-over-HTTPS in every installed browser. DoH resolves names
|
||||
# over the browser's own encrypted channel, bypassing /etc/hosts completely.
|
||||
disable_browser_doh() {
|
||||
# DISABLE DNS OVER HTTPS (DoH) IN BROWSERS
|
||||
# ============================================================================
|
||||
# DoH bypasses /etc/hosts entirely, defeating all our blocking!
|
||||
# We disable it in Firefox profiles for all users.
|
||||
echo ""
|
||||
echo "Disabling DNS over HTTPS (DoH) in browsers..."
|
||||
|
||||
# Get the actual user (not root) who invoked this script
|
||||
REAL_USER="${SUDO_USER:-$USER}"
|
||||
REAL_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6)
|
||||
|
||||
# Firefox: disable DoH via user.js
|
||||
if [[ -d "$REAL_HOME/.mozilla/firefox" ]]; then
|
||||
for profile in "$REAL_HOME/.mozilla/firefox"/*.default*; do
|
||||
if [[ -d "$profile" ]]; then
|
||||
cat >>"$profile/user.js" <<'FIREFOXEOF'
|
||||
// Disable DNS over HTTPS (DoH) to ensure /etc/hosts blocking works
|
||||
// Added by linux-configuration hosts installer
|
||||
user_pref("network.trr.mode", 5); // 5 = Off by user choice
|
||||
user_pref("doh-rollout.enabled", false);
|
||||
user_pref("doh-rollout.disable-heuristics", true);
|
||||
FIREFOXEOF
|
||||
chown "$REAL_USER:$REAL_USER" "$profile/user.js"
|
||||
echo " Firefox DoH disabled in: $(basename "$profile")"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo " No Firefox profiles found"
|
||||
fi
|
||||
|
||||
# Chromium-based browsers: use policy file
|
||||
CHROME_POLICY_DIR="/etc/chromium/policies/managed"
|
||||
if [[ -d "/etc/chromium" ]] || command -v chromium &>/dev/null; then
|
||||
mkdir -p "$CHROME_POLICY_DIR"
|
||||
cat >"$CHROME_POLICY_DIR/disable-doh.json" <<'CHROMEEOF'
|
||||
{
|
||||
"DnsOverHttpsMode": "off",
|
||||
"BuiltInDnsClientEnabled": false
|
||||
}
|
||||
CHROMEEOF
|
||||
echo " Chromium DoH disabled via policy"
|
||||
fi
|
||||
|
||||
# Google Chrome policy
|
||||
GCHROME_POLICY_DIR="/etc/opt/chrome/policies/managed"
|
||||
if [[ -d "/etc/opt/chrome" ]] || command -v google-chrome &>/dev/null; then
|
||||
mkdir -p "$GCHROME_POLICY_DIR"
|
||||
cat >"$GCHROME_POLICY_DIR/disable-doh.json" <<'GCHROMEEOF'
|
||||
{
|
||||
"DnsOverHttpsMode": "off",
|
||||
"BuiltInDnsClientEnabled": false
|
||||
}
|
||||
GCHROMEEOF
|
||||
echo " Google Chrome DoH disabled via policy"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ Installation complete!"
|
||||
echo " Custom entries protection is now active."
|
||||
echo " Removing blocked entries from the script will be blocked."
|
||||
echo " DNS over HTTPS (DoH) has been disabled in browsers."
|
||||
|
||||
# ============================================================================
|
||||
}
|
||||
|
||||
# Kill running browsers so the DoH policy changes take effect now rather
|
||||
# than at the user's next restart.
|
||||
restart_browsers() {
|
||||
# FORCE BROWSER RESTART TO APPLY DOH CHANGES
|
||||
# ============================================================================
|
||||
# Kill all browser processes so DoH changes take effect immediately
|
||||
echo ""
|
||||
echo "Killing browsers to apply DoH policy changes..."
|
||||
BROWSERS_KILLED=0
|
||||
|
||||
for browser in chrome chromium chromium-browser brave brave-browser firefox firefox-esr thorium vivaldi opera; do
|
||||
if pgrep -x "$browser" &>/dev/null || pgrep -f "/opt/.*/$browser" &>/dev/null; then
|
||||
echo " Killing $browser..."
|
||||
pkill -9 -f "$browser" 2>/dev/null || true
|
||||
BROWSERS_KILLED=1
|
||||
fi
|
||||
done
|
||||
|
||||
# Also kill by common binary paths
|
||||
for pattern in "/opt/google/chrome" "/opt/brave" "/opt/thorium" "/usr/lib/firefox" "/usr/lib/chromium"; do
|
||||
if pgrep -f "$pattern" &>/dev/null; then
|
||||
echo " Killing processes matching $pattern..."
|
||||
pkill -9 -f "$pattern" 2>/dev/null || true
|
||||
BROWSERS_KILLED=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $BROWSERS_KILLED -eq 1 ]]; then
|
||||
echo ""
|
||||
echo "⚠️ Browsers were killed to apply DNS settings."
|
||||
echo " Reopen your browser - hosts blocking is now enforced."
|
||||
else
|
||||
echo " No browsers were running."
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
}
|
||||
91
linux_configuration/scripts/periodic_background/hosts/lib/hosts_cache.sh
Executable file
91
linux_configuration/scripts/periodic_background/hosts/lib/hosts_cache.sh
Executable file
@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/hosts_cache.sh — keep the local copy of the upstream StevenBlack list
|
||||
# current.
|
||||
#
|
||||
# The cache holds the RAW upstream file, without our own entries, so a refresh
|
||||
# never has to re-derive what we added. Sourced by install.sh.
|
||||
|
||||
# Helpers
|
||||
extract_date_epoch_from_file() {
|
||||
# Grep "# Date:" line and convert to epoch seconds (UTC)
|
||||
local f="$1"
|
||||
local line
|
||||
line=$(grep -m1 '^# Date:' "$f" 2>/dev/null | sed -E 's/^# Date:[[:space:]]*(.*)[[:space:]]*\(UTC\).*/\1 UTC/')
|
||||
if [[ -n $line ]]; then
|
||||
date -u -d "$line" +%s 2>/dev/null || echo ""
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_remote_header() {
|
||||
# Try to fetch only the first ~4KB using HTTP Range; fallback to piping to head
|
||||
local out="$1"
|
||||
if curl -LfsS --max-time 10 -H 'Range: bytes=0-4095' "$URL" -o "$out"; then
|
||||
return 0
|
||||
fi
|
||||
# Fallback – may download more, but we only keep first lines
|
||||
if curl -LfsS --max-time 10 "$URL" | head -n 20 >"$out"; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
download_remote_full_to() {
|
||||
local out="$1"
|
||||
curl -LfsS "$URL" -o "$out"
|
||||
}
|
||||
|
||||
# Decide whether the cached upstream blocklist is current, and re-download
|
||||
# it if not. The cache holds the RAW upstream file, without our additions.
|
||||
refresh_upstream_cache() {
|
||||
# Decide whether to use cache or update
|
||||
TMP_REMOTE_HEAD=$(mktemp)
|
||||
trap 'rm -f "$TMP_REMOTE_HEAD"' EXIT
|
||||
|
||||
REMOTE_AVAILABLE=0
|
||||
if fetch_remote_header "$TMP_REMOTE_HEAD"; then
|
||||
REMOTE_AVAILABLE=1
|
||||
fi
|
||||
|
||||
NEED_UPDATE=0
|
||||
|
||||
if [[ -f $LOCAL_CACHE ]]; then
|
||||
local_epoch=$(extract_date_epoch_from_file "$LOCAL_CACHE")
|
||||
else
|
||||
local_epoch=""
|
||||
fi
|
||||
|
||||
if [[ $REMOTE_AVAILABLE -eq 1 ]]; then
|
||||
remote_epoch=$(extract_date_epoch_from_file "$TMP_REMOTE_HEAD")
|
||||
if [[ -n $local_epoch && -n $remote_epoch && $local_epoch -ge $remote_epoch ]]; then
|
||||
echo "Using cached StevenBlack hosts (up-to-date)."
|
||||
else
|
||||
echo "Cached version is missing or outdated; downloading latest StevenBlack hosts..."
|
||||
NEED_UPDATE=1
|
||||
fi
|
||||
else
|
||||
if [[ -f $LOCAL_CACHE ]]; then
|
||||
echo "No internet; using cached StevenBlack hosts."
|
||||
else
|
||||
echo "Error: No internet and no cached StevenBlack hosts found." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ensure we have a fresh cache if needed
|
||||
if [[ $NEED_UPDATE -eq 1 ]]; then
|
||||
TMP_DL=$(mktemp)
|
||||
if download_remote_full_to "$TMP_DL"; then
|
||||
# Save raw upstream to cache
|
||||
sudo mv "$TMP_DL" "$LOCAL_CACHE"
|
||||
sudo chmod 644 "$LOCAL_CACHE"
|
||||
echo "Saved latest StevenBlack hosts to cache: $LOCAL_CACHE"
|
||||
else
|
||||
rm -f "$TMP_DL"
|
||||
echo "Error: Failed to download latest StevenBlack hosts." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
}
|
||||
125
linux_configuration/scripts/periodic_background/hosts/lib/hosts_guard.sh
Executable file
125
linux_configuration/scripts/periodic_background/hosts/lib/hosts_guard.sh
Executable file
@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/hosts_guard.sh — the guard lifecycle around the write.
|
||||
#
|
||||
# stop_hosts_guard takes down the path watcher, the bind mount and the
|
||||
# immutable attribute so /etc/hosts can be written; restart_hosts_guard puts
|
||||
# them back; save_protection_state records the baseline the next run compares
|
||||
# against. Sourced by install.sh.
|
||||
|
||||
# Enable systemd-resolved and make it read /etc/hosts. Without
|
||||
# ReadEtcHosts=yes it ignores the file entirely and every blocked domain
|
||||
# resolves normally, which defeats the whole install.
|
||||
enable_resolved_reads_hosts() {
|
||||
# Enable systemd-resolved
|
||||
sudo systemctl enable systemd-resolved
|
||||
|
||||
# ============================================================================
|
||||
# ENSURE systemd-resolved READS /etc/hosts
|
||||
# ============================================================================
|
||||
# Without this, systemd-resolved ignores /etc/hosts entries entirely,
|
||||
# allowing blocked domains to resolve via DNS.
|
||||
RESOLVED_CONF="/etc/systemd/resolved.conf"
|
||||
if grep -q '^ReadEtcHosts=no' "$RESOLVED_CONF" 2>/dev/null; then
|
||||
echo "Fixing systemd-resolved: setting ReadEtcHosts=yes..."
|
||||
sudo sed -i 's/^ReadEtcHosts=no/ReadEtcHosts=yes/' "$RESOLVED_CONF"
|
||||
sudo systemctl restart systemd-resolved
|
||||
elif ! grep -q '^ReadEtcHosts=yes' "$RESOLVED_CONF" 2>/dev/null; then
|
||||
echo "Enabling ReadEtcHosts=yes in systemd-resolved..."
|
||||
if grep -q '^#ReadEtcHosts=' "$RESOLVED_CONF" 2>/dev/null; then
|
||||
sudo sed -i 's/^#ReadEtcHosts=.*/ReadEtcHosts=yes/' "$RESOLVED_CONF"
|
||||
else
|
||||
echo 'ReadEtcHosts=yes' | sudo tee -a "$RESOLVED_CONF" >/dev/null
|
||||
fi
|
||||
sudo systemctl restart systemd-resolved
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
# Take down the guard so /etc/hosts can be written: stop the path watcher,
|
||||
# unmount the read-only bind mount, and clear the immutable/append-only
|
||||
# attributes. restart_hosts_guard puts all three back.
|
||||
stop_hosts_guard() {
|
||||
# ============================================================================
|
||||
# TEMPORARILY DISABLE HOSTS GUARD PROTECTIONS FOR INSTALLATION
|
||||
# ============================================================================
|
||||
# The guard system uses a read-only bind mount and path watcher that prevent
|
||||
# any writes to /etc/hosts. We must stop them before installing, then restart.
|
||||
GUARD_SERVICES_STOPPED=0
|
||||
|
||||
for svc in hosts-bind-mount.service hosts-guard.path; do
|
||||
if systemctl is-active --quiet "$svc" 2>/dev/null; then
|
||||
echo "Stopping $svc for installation..."
|
||||
systemctl stop "$svc" 2>/dev/null || true
|
||||
GUARD_SERVICES_STOPPED=1
|
||||
fi
|
||||
done
|
||||
|
||||
# If bind mount is still active, unmount it
|
||||
if findmnt /etc/hosts >/dev/null 2>&1; then
|
||||
echo "Unmounting read-only bind mount on /etc/hosts..."
|
||||
umount /etc/hosts 2>/dev/null || mount -o remount,rw,bind /etc/hosts 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Remove all attributes from /etc/hosts to allow modifications
|
||||
sudo chattr -i -a /etc/hosts 2>/dev/null || true
|
||||
|
||||
}
|
||||
|
||||
# Bring the guard back up after the write: restart the services stopped by
|
||||
# stop_hosts_guard so /etc/hosts is protected again.
|
||||
restart_hosts_guard() {
|
||||
|
||||
# ============================================================================
|
||||
# RESTART HOSTS GUARD SERVICES
|
||||
# ============================================================================
|
||||
if [[ $GUARD_SERVICES_STOPPED -eq 1 ]]; then
|
||||
echo "Restarting hosts guard services..."
|
||||
# Update the canonical copy so the guard doesn't revert our changes
|
||||
if [[ -f /usr/local/share/locked-hosts ]]; then
|
||||
cp /etc/hosts /usr/local/share/locked-hosts
|
||||
echo " Updated canonical snapshot."
|
||||
fi
|
||||
for svc in hosts-bind-mount.service hosts-guard.path; do
|
||||
if systemctl is-enabled --quiet "$svc" 2>/dev/null; then
|
||||
systemctl start "$svc" 2>/dev/null || true
|
||||
echo " Restarted $svc"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
}
|
||||
|
||||
# Record the custom and unblock entry lists that were just installed, so the
|
||||
# next run's protection checks have a baseline to compare against.
|
||||
save_protection_state() {
|
||||
# SAVE CUSTOM ENTRIES STATE FOR FUTURE PROTECTION CHECKS
|
||||
# ============================================================================
|
||||
echo "Saving custom entries state for protection mechanism..."
|
||||
# The install script whose entry lists are being checked. Defaults to the
|
||||
# running script, which is what production always wants; a test points it
|
||||
# at a fixture instead, since these guards are the part of this file where
|
||||
# a silent regression matters most.
|
||||
script_path="$(readlink -f "${HOSTS_INSTALL_SCRIPT_PATH:-$0}")"
|
||||
current_custom_entries=$(extract_custom_entries_from_script "$script_path")
|
||||
# Remove immutable from state file if it exists
|
||||
chattr -i "$CUSTOM_ENTRIES_STATE_FILE" 2>/dev/null || true
|
||||
save_custom_entries_state "$current_custom_entries"
|
||||
echo "✅ Custom entries state saved to $CUSTOM_ENTRIES_STATE_FILE"
|
||||
|
||||
# Save unblock entries state for future protection checks
|
||||
current_unblock_entries=$(extract_unblock_entries_from_script "$script_path")
|
||||
save_unblock_entries_state "$current_unblock_entries"
|
||||
echo "✅ Unblock entries state saved to $UNBLOCK_STATE_FILE"
|
||||
|
||||
# Optionally flush DNS caches
|
||||
if [[ $FLUSH_DNS -eq 1 ]]; then
|
||||
echo "Flushing DNS caches..."
|
||||
sudo systemd-resolve --flush-caches
|
||||
sudo systemctl restart NetworkManager.service
|
||||
else
|
||||
echo "DNS cache flush skipped (use --flush-dns to enable)."
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
}
|
||||
@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/hosts_protect_custom.sh — the anti-tamper guard on the CUSTOM blocking
|
||||
# entries.
|
||||
#
|
||||
# Refuses the install when the custom block list has shrunk against the state
|
||||
# file saved by the previous run, so entries cannot be quietly dropped by
|
||||
# editing this repo. Sourced by install.sh.
|
||||
|
||||
# Extract custom blocked entries from a hosts file or heredoc section
|
||||
# Returns only the "0.0.0.0 domain.com" lines (normalized, sorted, unique)
|
||||
# The custom blocking entries used to live in a heredoc inside install.sh, and
|
||||
# this read them by slicing between "# Custom blocking entries" and the heredoc
|
||||
# terminator. They are now a data file beside it, so the whole file is the list
|
||||
# and no slicing is needed.
|
||||
#
|
||||
# The argument is still the script path, because that is what every caller has
|
||||
# and the data file sits at a known place relative to it. Passing a path with no
|
||||
# data file beside it yields an empty list, which check_custom_entries_protection
|
||||
# treats as "everything was removed" and refuses -- the safe direction.
|
||||
extract_custom_entries_from_script() {
|
||||
local script_path="$1"
|
||||
local entries_file
|
||||
entries_file="$(dirname "$script_path")/custom_entries.hosts"
|
||||
[[ -f $entries_file ]] || return 0
|
||||
grep -E '^0\.0\.0\.0[[:space:]]+' "$entries_file" |
|
||||
awk '{print $2}' |
|
||||
sort -u
|
||||
}
|
||||
|
||||
# Extract custom entries from the current /etc/hosts (entries after "# Custom blocking entries" marker)
|
||||
extract_custom_entries_from_hosts() {
|
||||
local hosts_file="$1"
|
||||
if [[ ! -f $hosts_file ]]; then
|
||||
return
|
||||
fi
|
||||
sed -n '/^# Custom blocking entries$/,$p' "$hosts_file" |
|
||||
grep -E '^0\.0\.0\.0[[:space:]]+' |
|
||||
awk '{print $2}' |
|
||||
sort -u
|
||||
}
|
||||
|
||||
# Load previously saved custom entries state
|
||||
load_saved_custom_entries() {
|
||||
if [[ -f $CUSTOM_ENTRIES_STATE_FILE ]]; then
|
||||
sort -u "$CUSTOM_ENTRIES_STATE_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Save current custom entries to state file
|
||||
save_custom_entries_state() {
|
||||
local entries="$1"
|
||||
echo "$entries" | sort -u >"$CUSTOM_ENTRIES_STATE_FILE"
|
||||
chmod 644 "$CUSTOM_ENTRIES_STATE_FILE"
|
||||
chattr +i "$CUSTOM_ENTRIES_STATE_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Helper function to count non-empty lines
|
||||
count_lines() {
|
||||
local input="$1"
|
||||
if [[ -z $input ]]; then
|
||||
echo 0
|
||||
else
|
||||
echo "$input" | grep -c . 2>/dev/null || echo 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Main protection check
|
||||
check_custom_entries_protection() {
|
||||
local script_path
|
||||
# The install script whose entry lists are being checked. Defaults to the
|
||||
# running script, which is what production always wants; a test points it
|
||||
# at a fixture instead, since these guards are the part of this file where
|
||||
# a silent regression matters most.
|
||||
script_path="$(readlink -f "${HOSTS_INSTALL_SCRIPT_PATH:-$0}")"
|
||||
|
||||
# Get new entries from the script's heredoc
|
||||
local new_entries
|
||||
new_entries=$(extract_custom_entries_from_script "$script_path")
|
||||
local new_count
|
||||
new_count=$(count_lines "$new_entries")
|
||||
|
||||
# Get saved/existing entries (prefer state file, fall back to current /etc/hosts)
|
||||
local saved_entries
|
||||
saved_entries=$(load_saved_custom_entries)
|
||||
if [[ -z $saved_entries ]]; then
|
||||
# First run or state file missing - extract from current /etc/hosts if it has our marker
|
||||
# The live hosts file, overridable so a test can supply a fixture; this
|
||||
# is a read, never a write.
|
||||
saved_entries=$(extract_custom_entries_from_hosts "${HOSTS_FILE_PATH:-/etc/hosts}")
|
||||
fi
|
||||
local saved_count
|
||||
saved_count=$(count_lines "$saved_entries")
|
||||
|
||||
# If no saved state exists, this is first installation - allow it
|
||||
if [[ $saved_count -eq 0 ]]; then
|
||||
echo "ℹ️ First installation detected - no protection check needed."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Find entries that were removed
|
||||
local removed_entries
|
||||
removed_entries=$(comm -23 <(echo "$saved_entries") <(echo "$new_entries"))
|
||||
local removed_count
|
||||
removed_count=$(count_lines "$removed_entries")
|
||||
|
||||
# Find entries that are new
|
||||
local added_entries
|
||||
added_entries=$(comm -13 <(echo "$saved_entries") <(echo "$new_entries"))
|
||||
local added_count
|
||||
added_count=$(count_lines "$added_entries")
|
||||
|
||||
echo ""
|
||||
echo "📊 Custom Entries Protection Check:"
|
||||
echo " Previously blocked: $saved_count entries"
|
||||
echo " Currently in script: $new_count entries"
|
||||
echo " Removed: $removed_count | Added: $added_count"
|
||||
|
||||
# RULE 1: No entries removed - always OK
|
||||
if [[ $removed_count -eq 0 ]]; then
|
||||
echo " ✅ No entries removed - protection check passed."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# RULE 2: Entries were removed - BLOCK INSTALLATION
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo " ❌ INSTALLATION BLOCKED - CUSTOM ENTRIES REMOVED"
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
echo "You are attempting to REMOVE the following blocked entries:"
|
||||
while IFS= read -r entry; do
|
||||
echo " - $entry"
|
||||
done <<<"$removed_entries"
|
||||
echo ""
|
||||
echo "This is NOT allowed. The only way to unblock sites is to:"
|
||||
echo ""
|
||||
echo " 1. Manually edit /etc/hosts (requires removing chattr protection)"
|
||||
echo " 2. Delete the state file /etc/hosts.custom-entries.state"
|
||||
echo " (also protected with chattr)"
|
||||
echo ""
|
||||
echo "These manual steps are intentionally difficult to prevent"
|
||||
echo "impulsive unblocking. If you really need to unblock something,"
|
||||
echo "you'll have to work for it."
|
||||
echo ""
|
||||
return 1
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/hosts_protect_unblock.sh — the anti-tamper guard on the UNBLOCK list.
|
||||
#
|
||||
# The mirror of hosts_protect_custom.sh: it refuses the install when the
|
||||
# whitelist has GROWN, so domains cannot be quietly un-blocked. Sourced by
|
||||
# install.sh.
|
||||
|
||||
# Extract whitelisted domains from the protected list embedded in this script
|
||||
extract_unblock_entries_from_script() {
|
||||
local script_path="$1"
|
||||
# The two marker lines match the domain pattern themselves -- they are
|
||||
# word characters and underscores -- so a plain grep pulled
|
||||
# PROTECTED_UNBLOCK_LIST_START and _END into the whitelist as if they were
|
||||
# domains. They then went into the saved state, and renaming either marker
|
||||
# would have read as the whitelist growing and blocked the install. Drop
|
||||
# them explicitly rather than relying on the pattern to exclude them.
|
||||
sed -n '/^# PROTECTED_UNBLOCK_LIST_START$/,/^# PROTECTED_UNBLOCK_LIST_END$/p' "$script_path" |
|
||||
grep -E '^# [a-zA-Z0-9._-]+$' |
|
||||
sed 's/^# //' |
|
||||
grep -vxE 'PROTECTED_UNBLOCK_LIST_(START|END)' |
|
||||
sort -u
|
||||
}
|
||||
|
||||
# Save current unblock entries to immutable state file
|
||||
save_unblock_entries_state() {
|
||||
local entries="$1"
|
||||
chattr -i "$UNBLOCK_STATE_FILE" 2>/dev/null || true
|
||||
echo "$entries" | sort -u >"$UNBLOCK_STATE_FILE"
|
||||
chmod 644 "$UNBLOCK_STATE_FILE"
|
||||
chattr +i "$UNBLOCK_STATE_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Block installation if the unblock list has grown (more sites being whitelisted)
|
||||
check_unblock_entries_protection() {
|
||||
local script_path
|
||||
# The install script whose entry lists are being checked. Defaults to the
|
||||
# running script, which is what production always wants; a test points it
|
||||
# at a fixture instead, since these guards are the part of this file where
|
||||
# a silent regression matters most.
|
||||
script_path="$(readlink -f "${HOSTS_INSTALL_SCRIPT_PATH:-$0}")"
|
||||
|
||||
local new_entries
|
||||
new_entries=$(extract_unblock_entries_from_script "$script_path")
|
||||
local new_count
|
||||
new_count=$(count_lines "$new_entries")
|
||||
|
||||
if [[ ! -f $UNBLOCK_STATE_FILE ]]; then
|
||||
echo "ℹ️ First unblock-list run — no protection check needed."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local saved_entries
|
||||
saved_entries=$(sort -u "$UNBLOCK_STATE_FILE")
|
||||
local saved_count
|
||||
saved_count=$(count_lines "$saved_entries")
|
||||
|
||||
# Entries added since last install
|
||||
local added_entries
|
||||
added_entries=$(comm -13 <(echo "$saved_entries") <(echo "$new_entries"))
|
||||
local added_count
|
||||
added_count=$(count_lines "$added_entries")
|
||||
|
||||
echo ""
|
||||
echo "📊 Unblock Entries Protection Check:"
|
||||
echo " Previously whitelisted: $saved_count domains"
|
||||
echo " Currently in script: $new_count domains"
|
||||
echo " Newly added: $added_count"
|
||||
|
||||
if [[ $added_count -eq 0 ]]; then
|
||||
echo " ✅ No new unblocks — protection check passed."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo " ❌ INSTALLATION BLOCKED — NEW UNBLOCK ENTRIES DETECTED"
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
echo "You are attempting to WHITELIST these additional domains:"
|
||||
while IFS= read -r entry; do
|
||||
echo " + $entry"
|
||||
done <<<"$added_entries"
|
||||
echo ""
|
||||
echo "To proceed, manually delete the state file first:"
|
||||
echo " sudo chattr -i $UNBLOCK_STATE_FILE && sudo rm $UNBLOCK_STATE_FILE"
|
||||
echo ""
|
||||
return 1
|
||||
}
|
||||
53
linux_configuration/scripts/periodic_background/hosts/lib/hosts_write.sh
Executable file
53
linux_configuration/scripts/periodic_background/hosts/lib/hosts_write.sh
Executable file
@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/hosts_write.sh — write /etc/hosts.
|
||||
#
|
||||
# The cached upstream list, then the exceptions we comment back out, then our
|
||||
# own blocking entries as one ~320-line quoted heredoc, then the permissions
|
||||
# and the immutable attribute. The heredoc is a single unit: splitting its
|
||||
# interior would change the file this produces. Sourced by install.sh.
|
||||
|
||||
# Write /etc/hosts: the cached upstream list, the per-site exceptions we
|
||||
# comment back out, then our own blocking entries, and finally the
|
||||
# permissions and immutable attribute.
|
||||
#
|
||||
# The custom entries are one ~320-line quoted heredoc and move as a single
|
||||
# unit; splitting its interior would change the file this writes.
|
||||
# The custom blocking entries, as data beside this lib rather than inline.
|
||||
CUSTOM_ENTRIES_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/custom_entries.hosts"
|
||||
|
||||
write_hosts_file() {
|
||||
# Install the base hosts from cache into /etc/hosts
|
||||
echo "Installing base hosts from cache to /etc/hosts..."
|
||||
sudo cp "$LOCAL_CACHE" /etc/hosts
|
||||
|
||||
# Comment out any 4chan blocking entries from the downloaded file
|
||||
echo "Allowing 4chan by commenting out any blocking entries..."
|
||||
sudo sed -i 's/^0\.0\.0\.0 4chan\.com/#0.0.0.0 4chan.com/' /etc/hosts
|
||||
sudo sed -i 's/^0\.0\.0\.0 www\.4chan\.com/#0.0.0.0 www.4chan.com/' /etc/hosts
|
||||
sudo sed -i 's/^0\.0\.0\.0 4chan\.org/#0.0.0.0 4chan.org/' /etc/hosts
|
||||
sudo sed -i 's/^0\.0\.0\.0 boards\.4chan\.org/#0.0.0.0 boards.4chan.org/' /etc/hosts
|
||||
sudo sed -i 's/^0\.0\.0\.0 sys\.4chan\.org/#0.0.0.0 sys.4chan.org/' /etc/hosts
|
||||
sudo sed -i 's/^0\.0\.0\.0 www\.4chan\.org/#0.0.0.0 www.4chan.org/' /etc/hosts
|
||||
sudo sed -i -E 's/^(0\.0\.0\.0[[:space:]]+[a-zA-Z0-9._-]*\.?facebook\.com)/#\1/' /etc/hosts
|
||||
sudo sed -i -E 's/^(0\.0\.0\.0[[:space:]]+[a-zA-Z0-9._-]*\.?messenger\.com)/#\1/' /etc/hosts
|
||||
sudo sed -i -E 's/^(0\.0\.0\.0[[:space:]]+[a-zA-Z0-9._-]*\.?fbcdn\.net)/#\1/' /etc/hosts
|
||||
sudo sed -i -E 's/^(0\.0\.0\.0[[:space:]]+[a-zA-Z0-9._-]*\.?facebook\.net)/#\1/' /etc/hosts
|
||||
sudo sed -i 's/^0\.0\.0\.0 delio\.com.pl/#0.0.0.0 delio.com.pl/' /etc/hosts
|
||||
sudo sed -i 's/^0\.0\.0\.0 loverslab\.com/#0.0.0.0 loverslab.com/' /etc/hosts
|
||||
|
||||
# Allow LinkedIn and all subdomains (linkedin.com + licdn.com CDN)
|
||||
echo "Allowing LinkedIn by commenting out any blocking entries..."
|
||||
sudo sed -i -E 's/^(0\.0\.0\.0[[:space:]]+[a-zA-Z0-9._-]*\.?linkedin\.com)/#\1/' /etc/hosts
|
||||
sudo sed -i -E 's/^(0\.0\.0\.0[[:space:]]+[a-zA-Z0-9._-]*\.?licdn\.com)/#\1/' /etc/hosts
|
||||
sudo sed -i -E 's/^(0\.0\.0\.0[[:space:]]+[a-zA-Z0-9._-]*\.?loverslab\.com)/#\1/' /etc/hosts
|
||||
|
||||
# Add custom entries for YouTube and Discord
|
||||
echo "Adding custom entries for YouTube and Discord..."
|
||||
tee -a /etc/hosts >/dev/null <"$CUSTOM_ENTRIES_FILE"
|
||||
|
||||
# Set proper permissions (readable by all, writable only by root)
|
||||
sudo chmod 644 /etc/hosts
|
||||
|
||||
# Make the file immutable and append-only for maximum protection
|
||||
sudo chattr +ia /etc/hosts
|
||||
}
|
||||
165
linux_configuration/scripts/periodic_background/hosts/lib/tests/hosts_harness.sh
Executable file
165
linux_configuration/scripts/periodic_background/hosts/lib/tests/hosts_harness.sh
Executable file
@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/tests/hosts_harness.sh — shared fixture for the hosts/install.sh split.
|
||||
#
|
||||
# Sourced, not executed. Scope is deliberately narrow: only the two anti-tamper
|
||||
# guards and their helpers are exercised here. The install phases themselves
|
||||
# write /etc/hosts, unmount the guard's bind mount and restart
|
||||
# systemd-resolved, so running them under test would take down the very
|
||||
# protection they exist to install — they are covered by the textual
|
||||
# wrap-verification in the evidence instead.
|
||||
#
|
||||
# The state-file paths were already variables in the pre-split script, so a
|
||||
# test can point them at a tmpdir. chattr is shimmed because the real one
|
||||
# cannot mark a file immutable on a tmpfs and would abort the guard.
|
||||
set -euo pipefail
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
_t_pass() {
|
||||
PASS=$((PASS + 1))
|
||||
printf ' OK: %s\n' "$1"
|
||||
}
|
||||
|
||||
_t_fail() {
|
||||
FAIL=$((FAIL + 1))
|
||||
printf ' FAIL: %s\n' "$1"
|
||||
}
|
||||
|
||||
_t_eq() {
|
||||
local want="$1"
|
||||
local got="$2"
|
||||
local what="$3"
|
||||
if [[ "$got" == "$want" ]]; then
|
||||
_t_pass "$what"
|
||||
else
|
||||
_t_fail "$what (want '${want}', got '${got}')"
|
||||
fi
|
||||
}
|
||||
|
||||
_t_has() {
|
||||
local haystack="$1"
|
||||
local needle="$2"
|
||||
local what="$3"
|
||||
if [[ "$haystack" == *"$needle"* ]]; then
|
||||
_t_pass "$what"
|
||||
else
|
||||
_t_fail "$what (want a substring '${needle}')"
|
||||
fi
|
||||
}
|
||||
|
||||
# The guards return 0 to allow the install and 1 to block it. Which way they
|
||||
# fail is the entire point, so assert on the exit status explicitly rather than
|
||||
# letting `set -e` decide.
|
||||
_t_allows() {
|
||||
local what="$2"
|
||||
if ("$1"); then
|
||||
_t_pass "$what"
|
||||
else
|
||||
_t_fail "$what (the guard blocked when it should have allowed)"
|
||||
fi
|
||||
}
|
||||
|
||||
_t_blocks() {
|
||||
local what="$2"
|
||||
if ("$1"); then
|
||||
_t_fail "$what (the guard allowed when it should have blocked)"
|
||||
else
|
||||
_t_pass "$what"
|
||||
fi
|
||||
}
|
||||
|
||||
_t_summary() {
|
||||
printf '\n%s: %d passed, %d failed\n' "$(basename "$0")" "$PASS" "$FAIL"
|
||||
[[ $FAIL -eq 0 ]]
|
||||
}
|
||||
|
||||
TEST_TMPDIR="$(mktemp -d)"
|
||||
trap 'rm -rf "${TEST_TMPDIR}"' EXIT
|
||||
|
||||
readonly FAKE_BIN="${TEST_TMPDIR}/fake_bin"
|
||||
mkdir -p "${FAKE_BIN}"
|
||||
|
||||
# chattr cannot set the immutable bit on a tmpfs and exits non-zero; the guards
|
||||
# tolerate that with `|| true`, but shimming it keeps the test output clean and
|
||||
# makes "was the state file locked" observable.
|
||||
cat >"${FAKE_BIN}/chattr" <<'CHATTRSHIM'
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' "chattr $*" >>"${HOSTS_TEST_CALLS}"
|
||||
exit 0
|
||||
CHATTRSHIM
|
||||
chmod +x "${FAKE_BIN}/chattr"
|
||||
|
||||
export HOSTS_TEST_CALLS="${TEST_TMPDIR}/calls"
|
||||
export PATH="${FAKE_BIN}:${PATH}"
|
||||
|
||||
# --- globals the guards read ------------------------------------------------
|
||||
|
||||
export CUSTOM_ENTRIES_STATE_FILE="${TEST_TMPDIR}/custom.state"
|
||||
export UNBLOCK_STATE_FILE="${TEST_TMPDIR}/unblock.state"
|
||||
|
||||
# A fake install.sh plus the custom_entries.hosts beside it. The guards resolve
|
||||
# the data file from the script path, so this reproduces the real layout.
|
||||
readonly FAKE_ROOT="${TEST_TMPDIR}/root"
|
||||
readonly FAKE_SCRIPT="${FAKE_ROOT}/install.sh"
|
||||
|
||||
# Stage the "new" entry lists the guards will read: the custom blocklist as a
|
||||
# data file, and the unblock list as marked-up comments inside the script.
|
||||
stage_script() { # <custom-domain>... -- <unblock-domain>...
|
||||
mkdir -p "${FAKE_ROOT}"
|
||||
local mode="custom"
|
||||
local custom=()
|
||||
local unblock=()
|
||||
local arg
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" == "--" ]]; then
|
||||
mode="unblock"
|
||||
continue
|
||||
fi
|
||||
if [[ "$mode" == "custom" ]]; then
|
||||
custom+=("$arg")
|
||||
else
|
||||
unblock+=("$arg")
|
||||
fi
|
||||
done
|
||||
|
||||
{
|
||||
printf '# Custom blocking entries\n'
|
||||
local d
|
||||
for d in "${custom[@]}"; do
|
||||
printf '0.0.0.0 %s\n' "$d"
|
||||
done
|
||||
} >"${FAKE_ROOT}/custom_entries.hosts"
|
||||
|
||||
{
|
||||
printf '#!/bin/bash\n'
|
||||
printf '# PROTECTED_UNBLOCK_LIST_START\n'
|
||||
local d
|
||||
for d in "${unblock[@]}"; do
|
||||
printf '# %s\n' "$d"
|
||||
done
|
||||
printf '# PROTECTED_UNBLOCK_LIST_END\n'
|
||||
} >"${FAKE_SCRIPT}"
|
||||
}
|
||||
|
||||
# Seed the saved state the guards compare against.
|
||||
stage_custom_state() {
|
||||
printf '%s\n' "$@" | sort -u >"${CUSTOM_ENTRIES_STATE_FILE}"
|
||||
}
|
||||
|
||||
stage_unblock_state() {
|
||||
printf '%s\n' "$@" | sort -u >"${UNBLOCK_STATE_FILE}"
|
||||
}
|
||||
|
||||
reset_state() {
|
||||
rm -rf "${FAKE_ROOT}"
|
||||
rm -f "${CUSTOM_ENTRIES_STATE_FILE}" "${UNBLOCK_STATE_FILE}"
|
||||
# Markers a test sets to steer a shim (e.g. curl_fails) are part of the
|
||||
# machine that test describes, so they reset too -- otherwise a failure
|
||||
# marker leaks into the next group and it passes for the wrong reason.
|
||||
rm -f "$(dirname "${HOSTS_TEST_CALLS}")"/curl_fails \
|
||||
"$(dirname "${HOSTS_TEST_CALLS}")"/remote_body
|
||||
: >"${HOSTS_TEST_CALLS}"
|
||||
mkdir -p "${FAKE_ROOT}"
|
||||
}
|
||||
reset_state
|
||||
26
linux_configuration/scripts/periodic_background/hosts/lib/tests/run_all.sh
Executable file
26
linux_configuration/scripts/periodic_background/hosts/lib/tests/run_all.sh
Executable file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/tests/run_all.sh — run every hosts/install.sh test in one process tree.
|
||||
#
|
||||
# Scope note: these cover the two anti-tamper guards and the cache helpers, not
|
||||
# the install phases. The phases write /etc/hosts, unmount the guard's bind
|
||||
# mount and restart systemd-resolved, so executing them under test would take
|
||||
# down the protection they exist to install. They are verified textually
|
||||
# instead — see the evidence artifact for this split.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
failed=0
|
||||
for test_script in "${SCRIPT_DIR}"/test_hosts_*.sh; do
|
||||
printf '\n### %s\n' "$(basename "$test_script")"
|
||||
if ! "$test_script"; then
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $failed -gt 0 ]]; then
|
||||
printf '\n%d test file(s) FAILED\n' "$failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '\nAll hosts install test files passed.\n'
|
||||
@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests for lib/hosts_cache.sh — keeping the local copy of the upstream
|
||||
# StevenBlack list current.
|
||||
#
|
||||
# The date parsing is what decides whether a download happens at all: the
|
||||
# upstream file carries a "# Date:" header, and a cache whose header is not
|
||||
# older than the remote one is reused. Misreading that header means either
|
||||
# re-downloading a 4 MB file on every install, or never refreshing at all.
|
||||
#
|
||||
# curl is shimmed, so nothing here touches the network.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# shellcheck source=hosts_harness.sh
|
||||
. "${SCRIPT_DIR}/hosts_harness.sh"
|
||||
|
||||
# Fake curl. `-o <file>` writes whatever $DEV/remote_body holds; a
|
||||
# $DEV/curl_fails marker makes every invocation fail, which is what exercises
|
||||
# the "keep using the stale cache" path.
|
||||
cat >"${FAKE_BIN}/curl" <<'CURLSHIM'
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
printf '%s\n' "curl $*" >>"${HOSTS_TEST_CALLS}"
|
||||
dev="$(dirname "${HOSTS_TEST_CALLS}")"
|
||||
[[ -f "${dev}/curl_fails" ]] && exit 22
|
||||
out=""
|
||||
prev=""
|
||||
for a in "$@"; do
|
||||
[[ "$prev" == "-o" ]] && out="$a"
|
||||
prev="$a"
|
||||
done
|
||||
body="${dev}/remote_body"
|
||||
[[ -f "$body" ]] || : >"$body"
|
||||
if [[ -n "$out" ]]; then
|
||||
cat "$body" >"$out"
|
||||
else
|
||||
cat "$body"
|
||||
fi
|
||||
exit 0
|
||||
CURLSHIM
|
||||
chmod +x "${FAKE_BIN}/curl"
|
||||
|
||||
DEV="$(dirname "${HOSTS_TEST_CALLS}")"
|
||||
|
||||
# shellcheck source=../hosts_cache.sh
|
||||
. "${SCRIPT_DIR}/../hosts_cache.sh"
|
||||
|
||||
# The lib reads $URL; point it somewhere obviously fake so a missing shim would
|
||||
# be an immediate failure rather than a real request.
|
||||
URL="http://test.invalid/hosts"
|
||||
|
||||
write_dated_file() { # <path> <date-header-or-empty>
|
||||
{
|
||||
[[ -n "${2:-}" ]] && printf '# Date: %s (UTC)\n' "$2"
|
||||
printf '# Title: fake\n0.0.0.0 blocked.example\n'
|
||||
} >"$1"
|
||||
}
|
||||
|
||||
echo "== extract_date_epoch_from_file =="
|
||||
reset_state
|
||||
f="${TEST_TMPDIR}/dated"
|
||||
write_dated_file "$f" "01 January 2026 00:00:00"
|
||||
_t_eq "1767225600" "$(extract_date_epoch_from_file "$f")" "a dated header parses to epoch seconds"
|
||||
|
||||
write_dated_file "$f" ""
|
||||
_t_eq "" "$(extract_date_epoch_from_file "$f")" "a file with no Date header yields nothing"
|
||||
|
||||
write_dated_file "$f" "not a real date"
|
||||
_t_eq "" "$(extract_date_epoch_from_file "$f")" "an unparsable date yields nothing rather than a bad epoch"
|
||||
|
||||
_t_eq "" "$(extract_date_epoch_from_file "${TEST_TMPDIR}/no-such-file")" \
|
||||
"a missing file yields nothing"
|
||||
|
||||
echo "== fetch_remote_header =="
|
||||
reset_state
|
||||
write_dated_file "${DEV}/remote_body" "02 February 2026 00:00:00"
|
||||
out="${TEST_TMPDIR}/header"
|
||||
if fetch_remote_header "$out"; then
|
||||
_t_pass "a successful fetch reports success"
|
||||
else
|
||||
_t_fail "a successful fetch reports success"
|
||||
fi
|
||||
_t_eq "1769990400" "$(extract_date_epoch_from_file "$out")" "the fetched header is readable"
|
||||
if grep -q 'Range: bytes=0-4095' "${HOSTS_TEST_CALLS}"; then
|
||||
_t_pass "it asks for a byte range rather than the whole file"
|
||||
else
|
||||
_t_fail "it asks for a byte range rather than the whole file"
|
||||
fi
|
||||
|
||||
echo "== fetch_remote_header: both attempts fail =="
|
||||
reset_state
|
||||
: >"${DEV}/curl_fails"
|
||||
if fetch_remote_header "${TEST_TMPDIR}/header2"; then
|
||||
_t_fail "a total fetch failure reports failure"
|
||||
else
|
||||
_t_pass "a total fetch failure reports failure"
|
||||
fi
|
||||
|
||||
echo "== download_remote_full_to =="
|
||||
reset_state
|
||||
write_dated_file "${DEV}/remote_body" "03 March 2026 00:00:00"
|
||||
full="${TEST_TMPDIR}/full"
|
||||
if download_remote_full_to "$full"; then
|
||||
_t_pass "a full download reports success"
|
||||
else
|
||||
_t_fail "a full download reports success"
|
||||
fi
|
||||
_t_eq "1772496000" "$(extract_date_epoch_from_file "$full")" "the downloaded file is the remote body"
|
||||
|
||||
reset_state
|
||||
: >"${DEV}/curl_fails"
|
||||
if download_remote_full_to "${TEST_TMPDIR}/full2"; then
|
||||
_t_fail "a failed download reports failure"
|
||||
else
|
||||
_t_pass "a failed download reports failure"
|
||||
fi
|
||||
|
||||
_t_summary
|
||||
@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests for the two anti-tamper guards: lib/hosts_protect_custom.sh and
|
||||
# lib/hosts_protect_unblock.sh.
|
||||
#
|
||||
# These are the highest-stakes functions in the file. They exist to stop the
|
||||
# blocklist being quietly weakened by editing this repo — the custom guard
|
||||
# refuses when blocked domains are REMOVED, the unblock guard refuses when the
|
||||
# whitelist GROWS. A split that silently disabled either would be invisible
|
||||
# until the day it mattered, so both directions of both guards get a case.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# shellcheck source=hosts_harness.sh
|
||||
. "${SCRIPT_DIR}/hosts_harness.sh"
|
||||
# shellcheck source=../hosts_protect_custom.sh
|
||||
. "${SCRIPT_DIR}/../hosts_protect_custom.sh"
|
||||
# shellcheck source=../hosts_protect_unblock.sh
|
||||
. "${SCRIPT_DIR}/../hosts_protect_unblock.sh"
|
||||
|
||||
export HOSTS_INSTALL_SCRIPT_PATH="${FAKE_SCRIPT}"
|
||||
|
||||
echo "== extract_custom_entries_from_script =="
|
||||
reset_state
|
||||
stage_script a.com b.com c.com -- x.com
|
||||
_t_eq "a.com
|
||||
b.com
|
||||
c.com" "$(extract_custom_entries_from_script "${FAKE_SCRIPT}")" \
|
||||
"the blocked domains are read out of the data file"
|
||||
|
||||
reset_state
|
||||
mkdir -p "${FAKE_ROOT}"
|
||||
printf '#!/bin/bash\n' >"${FAKE_SCRIPT}"
|
||||
# No data file beside the script: the list reads as empty, which the guard
|
||||
# treats as "everything was removed" and refuses. Failing safe is the point.
|
||||
_t_eq "" "$(extract_custom_entries_from_script "${FAKE_SCRIPT}")" \
|
||||
"a missing data file yields an empty list rather than an error"
|
||||
|
||||
echo "== extract_custom_entries_from_hosts =="
|
||||
reset_state
|
||||
hosts_fixture="${TEST_TMPDIR}/hosts.sample"
|
||||
{
|
||||
printf '127.0.0.1 localhost\n'
|
||||
printf '0.0.0.0 upstream-junk.com\n'
|
||||
printf '# Custom blocking entries\n'
|
||||
printf '0.0.0.0 mine-one.com\n'
|
||||
printf '0.0.0.0 mine-two.com\n'
|
||||
} >"$hosts_fixture"
|
||||
_t_eq "mine-one.com
|
||||
mine-two.com" "$(extract_custom_entries_from_hosts "$hosts_fixture")" \
|
||||
"only entries after the marker count as ours"
|
||||
|
||||
reset_state
|
||||
# A machine with no /etc/hosts at all is the pre-install state; reading it must
|
||||
# yield an empty list rather than an error, or the first install could not run.
|
||||
_t_eq "" "$(extract_custom_entries_from_hosts "${TEST_TMPDIR}/no-such-hosts")" \
|
||||
"a missing hosts file yields an empty list"
|
||||
|
||||
echo "== count_lines =="
|
||||
_t_eq "0" "$(count_lines "")" "an empty string counts as zero, not one"
|
||||
_t_eq "1" "$(count_lines "one")" "a single entry counts as one"
|
||||
_t_eq "3" "$(count_lines "$(printf 'a\nb\nc')")" "three entries count as three"
|
||||
|
||||
echo "== load_saved_custom_entries =="
|
||||
reset_state
|
||||
_t_eq "" "$(load_saved_custom_entries)" "no state file reads as empty"
|
||||
stage_custom_state b.com a.com
|
||||
_t_eq "a.com
|
||||
b.com" "$(load_saved_custom_entries)" "a state file reads back sorted"
|
||||
|
||||
echo "== save_custom_entries_state =="
|
||||
reset_state
|
||||
save_custom_entries_state "$(printf 'z.com\na.com\n')"
|
||||
_t_eq "a.com
|
||||
z.com" "$(cat "${CUSTOM_ENTRIES_STATE_FILE}")" "the saved state is sorted and unique"
|
||||
if grep -q "chattr +i" "${HOSTS_TEST_CALLS}"; then
|
||||
_t_pass "the state file is made immutable after writing"
|
||||
else
|
||||
_t_fail "the state file is made immutable after writing"
|
||||
fi
|
||||
|
||||
echo "== check_custom_entries_protection: first run =="
|
||||
reset_state
|
||||
stage_script a.com b.com -- x.com
|
||||
# No state file and nothing in /etc/hosts to fall back on means this is the
|
||||
# first install, which must be allowed or the system could never be set up.
|
||||
CUSTOM_ENTRIES_STATE_FILE="${TEST_TMPDIR}/nonexistent.state"
|
||||
# The fallback reads the live hosts file, which on a real machine already has
|
||||
# our entries; point it at an empty fixture so this is genuinely a first run.
|
||||
HOSTS_FILE_PATH="${TEST_TMPDIR}/empty.hosts"
|
||||
: >"${HOSTS_FILE_PATH}"
|
||||
out="$(check_custom_entries_protection || true)"
|
||||
_t_has "$out" "First installation" "a first run is allowed"
|
||||
CUSTOM_ENTRIES_STATE_FILE="${TEST_TMPDIR}/custom.state"
|
||||
unset HOSTS_FILE_PATH
|
||||
|
||||
echo "== check_custom_entries_protection: nothing removed =="
|
||||
reset_state
|
||||
stage_script a.com b.com -- x.com
|
||||
stage_custom_state a.com b.com
|
||||
_t_allows check_custom_entries_protection "an unchanged list is allowed"
|
||||
|
||||
echo "== check_custom_entries_protection: entries added =="
|
||||
reset_state
|
||||
stage_script a.com b.com c.com -- x.com
|
||||
stage_custom_state a.com b.com
|
||||
_t_allows check_custom_entries_protection "adding entries is allowed"
|
||||
|
||||
echo "== check_custom_entries_protection: an entry removed is BLOCKED =="
|
||||
reset_state
|
||||
stage_script a.com -- x.com
|
||||
stage_custom_state a.com b.com
|
||||
_t_blocks check_custom_entries_protection "removing a blocked domain is refused"
|
||||
out="$(check_custom_entries_protection || true)"
|
||||
_t_has "$out" "INSTALLATION BLOCKED" "the refusal is stated plainly"
|
||||
_t_has "$out" "b.com" "the removed domain is named"
|
||||
|
||||
echo "== check_custom_entries_protection: swapping entries is still a removal =="
|
||||
reset_state
|
||||
# Same count, different contents. A count-only check would pass this, which is
|
||||
# exactly the hole the removed-set comparison exists to close.
|
||||
stage_script a.com NEW.com -- x.com
|
||||
stage_custom_state a.com b.com
|
||||
_t_blocks check_custom_entries_protection "swapping one domain for another is refused"
|
||||
|
||||
echo "== extract_unblock_entries_from_script =="
|
||||
reset_state
|
||||
stage_script a.com -- one.com two.com
|
||||
_t_eq "one.com
|
||||
two.com" "$(extract_unblock_entries_from_script "${FAKE_SCRIPT}")" \
|
||||
"the whitelist is read from between the markers"
|
||||
|
||||
echo "== check_unblock_entries_protection: first run =="
|
||||
reset_state
|
||||
stage_script a.com -- one.com
|
||||
UNBLOCK_STATE_FILE="${TEST_TMPDIR}/nonexistent-unblock.state"
|
||||
_t_allows check_unblock_entries_protection "a first run is allowed"
|
||||
UNBLOCK_STATE_FILE="${TEST_TMPDIR}/unblock.state"
|
||||
|
||||
echo "== check_unblock_entries_protection: unchanged =="
|
||||
reset_state
|
||||
stage_script a.com -- one.com two.com
|
||||
stage_unblock_state one.com two.com
|
||||
_t_allows check_unblock_entries_protection "an unchanged whitelist is allowed"
|
||||
|
||||
echo "== check_unblock_entries_protection: shrinking is allowed =="
|
||||
reset_state
|
||||
# Removing a domain from the whitelist means blocking MORE, which is always
|
||||
# the safe direction and must never be refused.
|
||||
stage_script a.com -- one.com
|
||||
stage_unblock_state one.com two.com
|
||||
_t_allows check_unblock_entries_protection "removing a whitelist entry is allowed"
|
||||
|
||||
echo "== check_unblock_entries_protection: growing is BLOCKED =="
|
||||
reset_state
|
||||
stage_script a.com -- one.com two.com three.com
|
||||
stage_unblock_state one.com two.com
|
||||
_t_blocks check_unblock_entries_protection "adding a whitelist entry is refused"
|
||||
out="$(check_unblock_entries_protection || true)"
|
||||
_t_has "$out" "three.com" "the newly whitelisted domain is named"
|
||||
|
||||
echo "== save_unblock_entries_state =="
|
||||
reset_state
|
||||
save_unblock_entries_state "$(printf 'z.com\na.com\n')"
|
||||
_t_eq "a.com
|
||||
z.com" "$(cat "${UNBLOCK_STATE_FILE}")" "the saved whitelist is sorted and unique"
|
||||
|
||||
_t_summary
|
||||
Loading…
Reference in New Issue
Block a user