Initial import of Hugo site to Forgejo

This commit is contained in:
Iman Alipour 2026-07-28 12:47:20 +00:00
commit cb1ba0317f
1259 changed files with 236349 additions and 0 deletions

View file

@ -0,0 +1,746 @@
<!DOCTYPE html>
<html lang="en" dir="auto">
<head><meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="robots" content="noindex, nofollow">
<title>Blackout | AlipourIm journeys</title>
<meta name="keywords" content="">
<meta name="description" content="I tried to be more useful, but when I couldn&#39;t, I found another way.">
<meta name="author" content="Iman Alipour">
<link rel="canonical" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/blackout/">
<link crossorigin="anonymous" href="/assets/css/stylesheet.31b150d909186c48d6dbbf653acc2489945fa9ac2c8f8cd3fe8448e89e40fccf.css" integrity="sha256-MbFQ2QkYbEjW279lOswkiZRfqawsj4zT/oRI6J5A/M8=" rel="preload stylesheet" as="style">
<link rel="icon" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/favicon-32x32.png">
<link rel="apple-touch-icon" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/apple-touch-icon.png">
<link rel="mask-icon" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/safari-pinned-tab.svg">
<meta name="theme-color" content="#2e2e33">
<meta name="msapplication-TileColor" content="#2e2e33">
<link rel="alternate" hreflang="en" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/blackout/">
<noscript>
<style>
#theme-toggle,
.top-link {
display: none;
}
</style>
<style>
@media (prefers-color-scheme: dark) {
:root {
--theme: rgb(29, 30, 32);
--entry: rgb(46, 46, 51);
--primary: rgb(218, 218, 219);
--secondary: rgb(155, 156, 157);
--tertiary: rgb(65, 66, 68);
--content: rgb(196, 196, 197);
--code-block-bg: rgb(46, 46, 51);
--code-bg: rgb(55, 56, 62);
--border: rgb(51, 51, 51);
}
.list {
background: var(--theme);
}
.list:not(.dark)::-webkit-scrollbar-track {
background: 0 0;
}
.list:not(.dark)::-webkit-scrollbar-thumb {
border-color: var(--theme);
}
}
</style>
</noscript><script>
(function () {
window.goatcounter = { endpoint: "/count" };
const s = document.createElement("script");
s.async = true;
s.src = "/js/count.js";
document.head.appendChild(s);
})();
</script>
<script>
(function () {
const GC = "";
const nf = new Intl.NumberFormat();
const SING = "visit";
const PLUR = "visits";
function ready(fn) {
if (document.readyState === "complete" || document.readyState === "interactive") setTimeout(fn, 0);
else document.addEventListener("DOMContentLoaded", fn);
}
function gcPath() {
try {
return window.goatcounter && window.goatcounter.get_data
? window.goatcounter.get_data().p
: location.pathname;
} catch (_) {
return location.pathname;
}
}
async function fetchCount(path, period) {
try {
const q = new URLSearchParams({ t: String(Date.now()) });
if (period) q.set("start", period);
const url = `${GC}/counter/${encodeURIComponent(path)}.json?${q}`;
const r = await fetch(url, { credentials: "omit" });
if (r.status === 404) return 0;
if (!r.ok) throw 0;
const j = await r.json();
const n = Number(String(j.count).replace(/,/g, ""));
return Number.isFinite(n) ? n : 0;
} catch {
return null;
}
}
const labelVisits = (n) =>
n === null ? "—" : `${nf.format(n)} ${n === 1 ? SING : PLUR}`;
ready(async function () {
const post = document.querySelector(".post-single");
if (post) {
const meta = post.querySelector(
".post-header .post-meta, .post-header .entry-meta, .post-meta"
);
const visits = await fetchCount(gcPath());
if (meta) {
let span = meta.querySelector(".post-views");
if (!span) {
span = document.createElement("span");
span.className = "post-views";
meta.appendChild(span);
}
span.textContent = labelVisits(visits ?? 0);
}
}
const cards = document.querySelectorAll("article.post-entry");
await Promise.all(
Array.from(cards).map(async (card) => {
const meta = card.querySelector(".entry-footer, .post-meta");
const link = card.querySelector("a.entry-link, h2 a, .entry-title a");
if (!meta || !link) return;
try {
const u = new URL(link.getAttribute("href"), location.origin);
if (u.origin !== location.origin) return;
let span = meta.querySelector(".post-views");
if (!span) {
span = document.createElement("span");
span.className = "post-views";
meta.appendChild(span);
}
span.textContent = labelVisits((await fetchCount(u.pathname)) ?? 0);
} catch {}
})
);
const uvEl = document.getElementById("gc_site_visitors_week");
if (uvEl) {
const n = await fetchCount("TOTAL", "week");
uvEl.textContent = n === null ? "—" : nf.format(n);
}
});
})();
</script>
</head>
<body class="" id="top">
<script>
if (localStorage.getItem("pref-theme") === "dark") {
document.body.classList.add('dark');
} else if (localStorage.getItem("pref-theme") === "light") {
document.body.classList.remove('dark')
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('dark');
}
</script>
<header class="header">
<nav class="nav">
<div class="logo">
<a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/" accesskey="h" title="AlipourIm journeys (Alt + H)">AlipourIm journeys</a>
<div class="logo-switches">
<button id="theme-toggle" accesskey="t" title="(Alt + T)" aria-label="Toggle theme">
<svg id="moon" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
<svg id="sun" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</button>
</div>
</div>
<ul id="menu">
<li>
<a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/" title="Posts">
<span>Posts</span>
</a>
</li>
<li>
<a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/phd_journey/" title="PhD_journey">
<span>PhD_journey</span>
</a>
</li>
<li>
<a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/about/" title="About">
<span>About</span>
</a>
</li>
</ul>
</nav>
</header>
<main class="main">
<article class="post-single">
<header class="post-header">
<div class="breadcrumbs"><a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/">Home</a>&nbsp;»&nbsp;<a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/">Posts</a></div>
<h1 class="post-title entry-hint-parent">
Blackout
</h1>
<div class="post-description">
I tried to be more useful, but when I couldn&#39;t, I found another way.
</div>
<div class="post-meta"><span title='2026-01-26 07:21:51 +0000 UTC'>January 26, 2026</span>&nbsp;·&nbsp;8 min&nbsp;·&nbsp;Iman Alipour
<span class="post-meta-item">
<a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/blackout/#isso-thread" class="isso-comments-link">Comments</a>
</span>
</div>
</header>
<div class="post-content"><p>Ever since the latest Internet/communication blackout in Iran, I&rsquo;ve been trying to get access to my box inside of the network. I went so far as to beg people to ssh into my box and deploy something that would give me a backdoor to my box, but little to no luck came out of it. Although Arvancloud opened their panel briefly, giving me access to do some digging, I got stuck at putting some sort of binary client on my box from their online panel. I tried to b64 the binary file and copypaste it via clipboard, and this caused their panel to crash (or it was just bad timing) — they haven&rsquo;t opened it again.</p>
<p>After getting more and more frustrated, I decided to put my hardware to good use, and host residential-ish volunteer proxies for people wanting to reach the free internet. I chose to host my beloved Snowflake, Conduit (since it became a hot topic), and Lantern. Heres how you can do the same if you decide to do so.</p>
<p>One important vibe check before we start: Im not giving anyone a custom “backdoor” into <em>your</em> network, and you shouldnt either. The whole point of the tools below is that theyre designed to work as <strong>volunteer nodes</strong> inside larger networks (Tor / Psiphon / Lantern), not as random open proxies you expose yourself.</p>
<h1 id="running-anticensorship-infrastructure-at-home-raspberry-pi-server-whatever">Running AntiCensorship Infrastructure at Home (Raspberry Pi, server, whatever)<a hidden class="anchor" aria-hidden="true" href="#running-anticensorship-infrastructure-at-home-raspberry-pi-server-whatever">#</a></h1>
<p>I didnt set out to become a tiny piece of global internet infrastructure. I just had a couple of Raspberry Pis, a decent internet connection, and the uncomfortable feeling that access to information shouldnt depend on where you were born.</p>
<p>So I turned my Pis into helpers.</p>
<p>This post is about running <strong>three different anticensorship tools</strong> on Raspberry Pi hardware, quietly and continuously, without exposing scary open ports or babysitting terminals:</p>
<ul>
<li><strong>Psiphon Conduit</strong> to help Psiphon users automatically</li>
<li><strong>Tor Snowflake (standalone proxy)</strong> to help Tor users automatically</li>
<li><strong>Lantern Unbounded</strong> a browserbased volunteer bridge, daemonized so it runs forever</li>
</ul>
<p>Everything runs headless (or <em>headlessish</em>), survives reboots, and doesnt require me to log in every week just to check if its still alive.</p>
<hr>
<h2 id="the-philosophy-dont-be-a-public-server-be-a-volunteer-node">The philosophy: dont be a public server, be a volunteer node<a hidden class="anchor" aria-hidden="true" href="#the-philosophy-dont-be-a-public-server-be-a-volunteer-node">#</a></h2>
<p>A theme youll see throughout this setup is intentional modesty. Im not running an open proxy. Im not advertising an IP address. Im not routing half the internet through my house.</p>
<p>Instead, Im running software thats designed to safely integrate into bigger systems that already handle discovery, encryption, throttling, abuse prevention, and client UX.</p>
<p>Thats the difference between helping — and waking up to your ISP asking why your home IP suddenly became “a whole thing”.</p>
<hr>
<h2 id="what-you-need-one-time">What you need (one time)<a hidden class="anchor" aria-hidden="true" href="#what-you-need-one-time">#</a></h2>
<p>This guide assumes Ubuntu on ARM (Pi). It works on a normal server too.</p>
<p>First, install Docker (because containers are a gift):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt update
</span></span><span style="display:flex;"><span>sudo apt install -y curl
</span></span><span style="display:flex;"><span>curl -fsSL https://get.docker.com -o get-docker.sh
</span></span><span style="display:flex;"><span>sudo sh get-docker.sh
</span></span><span style="display:flex;"><span>sudo usermod -aG docker $USER
</span></span><span style="display:flex;"><span>newgrp docker
</span></span></code></pre></div><p>Now make yourself a clean playground under <code>/srv</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo mkdir -p /srv/<span style="color:#f92672">{</span>conduit,snowflake,lantern<span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>sudo chown -R $USER:$USER /srv/<span style="color:#f92672">{</span>conduit,snowflake,lantern<span style="color:#f92672">}</span>
</span></span></code></pre></div><p>I like <code>/srv</code> because it signals “service data lives here” and Im less likely to delete it while cleaning my home directory at 3am.</p>
<hr>
<h2 id="conduit-quietly-helping-psiphon-users-docker">Conduit: quietly helping Psiphon users (Docker)<a hidden class="anchor" aria-hidden="true" href="#conduit-quietly-helping-psiphon-users-docker">#</a></h2>
<p>Conduit is Psiphons volunteer “station” software. Once its running, Psiphons own network decides when and how clients use it. Theres nothing to configure, nothing to advertise, and no port forwarding required.</p>
<p>The important part is persistence. Conduit generates an identity key the first time it runs (<code>conduit_key.json</code>), and that identity builds reputation over time. If you lose it, you start from zero again.</p>
<h3 id="the-file-srvconduitdocker-composeyml">The file: <code>/srv/conduit/docker-compose.yml</code><a hidden class="anchor" aria-hidden="true" href="#the-file-srvconduitdocker-composeyml">#</a></h3>
<p>Create it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>cd /srv/conduit
</span></span><span style="display:flex;"><span>vim docker-compose.yml
</span></span></code></pre></div><p>Paste this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">conduit</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">image</span>: <span style="color:#ae81ff">ghcr.io/ssmirr/conduit/conduit:latest</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">conduit</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Keep keys and state across restarts</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">./data:/app/data</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Optional: if you want a different bandwidth limit / max clients,</span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># you can pass CLI flags here (these are examples).</span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># command: [&#34;conduit&#34;, &#34;start&#34;, &#34;-b&#34;, &#34;4&#34;, &#34;-c&#34;, &#34;8&#34;]</span>
</span></span></code></pre></div><p>Then bring it up:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker compose up -d
</span></span><span style="display:flex;"><span>docker logs -f conduit
</span></span></code></pre></div><p>When its working, youll see things like:</p>
<ul>
<li><code>[OK] Connected to Psiphon network</code></li>
<li>periodic <code>[STATS]</code> lines with Connecting/Connected counters (thats your “is anyone using this?” moment)</li>
</ul>
<p>If you ever want to stop it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker stop conduit
</span></span></code></pre></div><p>Or “disable but keep everything” (recommended):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker compose down
</span></span></code></pre></div><p>Or “delete it from orbit” (not recommended unless you enjoy rebuilding):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker rm -f conduit
</span></span></code></pre></div><hr>
<h2 id="snowflake-tor-but-even-quieter-docker-compose">Snowflake: Tor, but even quieter (Docker Compose)<a hidden class="anchor" aria-hidden="true" href="#snowflake-tor-but-even-quieter-docker-compose">#</a></h2>
<p>Snowflake serves Tor users. Its explicitly designed to work behind NAT and CGNAT, which makes it perfect for home connections. The Tor Project provides an official Docker Compose file that includes <strong>watchtower</strong> for automatic updates.</p>
<h3 id="the-file-srvsnowflakedocker-composeyml">The file: <code>/srv/snowflake/docker-compose.yml</code><a hidden class="anchor" aria-hidden="true" href="#the-file-srvsnowflakedocker-composeyml">#</a></h3>
<p>You can download the official file exactly like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>cd /srv/snowflake
</span></span><span style="display:flex;"><span>wget -O docker-compose.yml <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> <span style="color:#e6db74">&#34;https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake/-/raw/main/docker-compose.yml?ref_type=heads&#34;</span>
</span></span></code></pre></div><p>If youd rather create it yourself (its tiny), heres the same thing in readable YAML:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">snowflake-proxy</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">network_mode</span>: <span style="color:#ae81ff">host</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">image</span>: <span style="color:#ae81ff">containers.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake:latest</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">snowflake-proxy</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># For a full list of Snowflake proxy args, see the upstream docs.</span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Example if you ever need it:</span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># command: [&#34;-ephemeral-ports-range&#34;, &#34;30000:60000&#34;]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">watchtower</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">image</span>: <span style="color:#ae81ff">containrrr/watchtower</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">watchtower</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">/var/run/docker.sock:/var/run/docker.sock</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">command</span>: <span style="color:#ae81ff">snowflake-proxy</span>
</span></span></code></pre></div><p>Start it (with auto updates):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker compose up -d
</span></span><span style="display:flex;"><span>docker logs -f snowflake-proxy
</span></span></code></pre></div><p>If you see:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Proxy starting
</span></span><span style="display:flex;"><span>NAT type: restricted
</span></span></code></pre></div><p>…thats totally fine. “Restricted” is normal for home NAT. If its running, youre helping.</p>
<p>If you get the “container name already in use” error, it just means you already have a <code>snowflake-proxy</code> container from a previous attempt. Fix it with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker rm -f snowflake-proxy
</span></span><span style="display:flex;"><span>docker compose up -d
</span></span></code></pre></div><hr>
<h2 id="lantern-unbounded-a-browser-that-helps-people-systemd--xvfb">Lantern Unbounded: a browser that helps people (systemd + Xvfb)<a hidden class="anchor" aria-hidden="true" href="#lantern-unbounded-a-browser-that-helps-people-systemd--xvfb">#</a></h2>
<p>Lantern Unbounded is different: it runs in a browser (WASM + WebRTC), which makes volunteering frictionless… but also makes servers go “uhh… where is your display?”.</p>
<p>So we cheat — politely.</p>
<p>We run Chromium inside a virtual display (Xvfb) and supervise it with systemd. No monitor needed. No desktop clicking. It just… exists… and helps.</p>
<h3 id="install-what-we-need">Install what we need<a hidden class="anchor" aria-hidden="true" href="#install-what-we-need">#</a></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt update
</span></span><span style="display:flex;"><span>sudo apt install -y xvfb
</span></span><span style="display:flex;"><span>sudo apt install -y chromium-browser <span style="color:#f92672">||</span> sudo apt install -y chromium
</span></span></code></pre></div><h3 id="fix-the-x11-socket-dir-permissions-the-annoying-but-essential-part">Fix the X11 socket dir permissions (the annoying but essential part)<a hidden class="anchor" aria-hidden="true" href="#fix-the-x11-socket-dir-permissions-the-annoying-but-essential-part">#</a></h3>
<p>If you dont do this, Xvfb may throw:</p>
<p><code>_XSERVTransmkdir: ERROR: euid != 0, directory /tmp/.X11-unix will not be created.</code></p>
<p>Fix it once:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo chmod <span style="color:#ae81ff">1777</span> /tmp
</span></span><span style="display:flex;"><span>sudo mkdir -p /tmp/.X11-unix
</span></span><span style="display:flex;"><span>sudo chown root:root /tmp/.X11-unix
</span></span><span style="display:flex;"><span>sudo chmod <span style="color:#ae81ff">1777</span> /tmp/.X11-unix
</span></span></code></pre></div><h3 id="the-file-etcsystemdsystemunboundedservice">The file: <code>/etc/systemd/system/unbounded.service</code><a hidden class="anchor" aria-hidden="true" href="#the-file-etcsystemdsystemunboundedservice">#</a></h3>
<p>Pick your username. On my boxes, its either <code>hub</code> or <code>rpi</code>. Use your actual user.</p>
<p>Create the service:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo vim /etc/systemd/system/unbounded.service
</span></span></code></pre></div><p>Paste this, and <strong>replace <code>YOUR_USER</code></strong> with your username (e.g. <code>hub</code> or <code>rpi</code>):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[Unit]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Description</span><span style="color:#f92672">=</span><span style="color:#e6db74">Lantern Unbounded (Xvfb + Chromium)</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">After</span><span style="color:#f92672">=</span><span style="color:#e6db74">network-online.target</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Wants</span><span style="color:#f92672">=</span><span style="color:#e6db74">network-online.target</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[Service]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">User</span><span style="color:#f92672">=</span><span style="color:#e6db74">YOUR_USER</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Environment</span><span style="color:#f92672">=</span><span style="color:#e6db74">DISPLAY=:99</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Environment</span><span style="color:#f92672">=</span><span style="color:#e6db74">XDG_RUNTIME_DIR=/run/user/%U</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">ExecStart</span><span style="color:#f92672">=</span><span style="color:#e6db74">/usr/bin/bash -lc &#39;\
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> /usr/bin/Xvfb :99 -screen 0 1280x720x24 -nolisten tcp -ac &amp; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> sleep 1; \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> $(command -v chromium || command -v chromium-browser) \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> --no-first-run \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> --disable-breakpad \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> --disable-features=TranslateUI \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> --autoplay-policy=no-user-gesture-required \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> --use-fake-ui-for-media-stream \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> --disable-gpu \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> --no-sandbox \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> --app=https://unbounded.lantern.io/ \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> --kiosk \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> --window-size=1280,720 \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> --user-data-dir=/home/YOUR_USER/.config/unbounded-chromium \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> --disk-cache-dir=/home/YOUR_USER/.cache/unbounded-chromium \</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Restart</span><span style="color:#f92672">=</span><span style="color:#e6db74">always</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">RestartSec</span><span style="color:#f92672">=</span><span style="color:#e6db74">5</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">KillMode</span><span style="color:#f92672">=</span><span style="color:#e6db74">mixed</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[Install]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">WantedBy</span><span style="color:#f92672">=</span><span style="color:#e6db74">multi-user.target</span>
</span></span></code></pre></div><p>Enable and start:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo systemctl daemon-reload
</span></span><span style="display:flex;"><span>sudo systemctl enable --now unbounded.service
</span></span><span style="display:flex;"><span>systemctl status unbounded.service --no-pager
</span></span></code></pre></div><p>If you see <code>Active: active (running)</code>, youve won.</p>
<h3 id="it-runs-headless-ish-but-how-do-i-know-its-alive">“It runs headless-ish, but how do I know its alive?”<a hidden class="anchor" aria-hidden="true" href="#it-runs-headless-ish-but-how-do-i-know-its-alive">#</a></h3>
<p>The simplest “is it on?” checks:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>systemctl status unbounded.service --no-pager
</span></span><span style="display:flex;"><span>journalctl -u unbounded.service -f
</span></span></code></pre></div><p>And the “is it actually doing network things?” check:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo ss -tunap | egrep <span style="color:#e6db74">&#39;chrome|chromium|:443|:3478&#39;</span> <span style="color:#f92672">||</span> true
</span></span></code></pre></div><p>If you ever want to stop it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo systemctl stop unbounded.service
</span></span></code></pre></div><p>If you want it not to start on boot:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo systemctl disable unbounded.service
</span></span></code></pre></div><hr>
<h2 id="a-note-on-safety-legality-and-expectations">A note on safety, legality, and expectations<a hidden class="anchor" aria-hidden="true" href="#a-note-on-safety-legality-and-expectations">#</a></h2>
<p>None of this makes you anonymous or magically protected from local laws. Youre contributing bandwidth and connectivity to systems designed to reduce risk — but you should still understand your own environment, and you should still be comfortable with what youre running.</p>
<p>The good news is that all of these tools are designed so youre not manually granting access to random people. Youre volunteering into networks that already do the hard parts.</p>
<p>Thats the part I like.</p>
<hr>
<h2 id="the-quiet-satisfaction-of-it-all">The quiet satisfaction of it all<a hidden class="anchor" aria-hidden="true" href="#the-quiet-satisfaction-of-it-all">#</a></h2>
<p>Theres something deeply satisfying about knowing that a small box on a shelf is occasionally helping someone read, learn, or communicate when they otherwise couldnt.</p>
<p>No dashboard. No vanity metrics. Just the occasional log line, quietly scrolling by.</p>
<p>And honestly? Thats enough.</p>
<hr>
<h2 id="some-rants">Some rants<a hidden class="anchor" aria-hidden="true" href="#some-rants">#</a></h2>
<p>I was unable to hear from my parents in the first two weeks of the blackout. In fact, the first time I heard their voices was a few days ago. Ever since then we&rsquo;ve been able to hear each other over calls — not fancy digital ones, the shitty GSM stuff. I saw my mom&rsquo;s face for a few seconds on the first day she managed to connect to Psiphon, but after that no luck. :(</p>
<p>This is super sad and frustrating. I&rsquo;ve not really been writing anything due to this. When I came back from my congress + Vienna trip, I&rsquo;ve been dealing with this situation. It&rsquo;s just annoying. Let&rsquo;s cross our fingers for one of my fantasy mindcreated things come true. Things are so incredibly much better in my mind. I want to live in my mind and never come out. Uh&hellip; :(</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
<p><strong>Downloads:</strong>
<a href="/sources/posts/blackout.md">Markdown</a> ·
<a href="/sources/posts/blackout.md.asc">Signature (.asc)</a>
</p><details class="signature">
<summary>View OpenPGP signature</summary>
<pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">-----BEGIN PGP SIGNATURE-----
iQIzBAABCgAdFiEEVaKl3oR5K6zGyu4/tYgoUOBMjSoFAmpkbuAACgkQtYgoUOBM
jSoETRAAm6hrWmkHuZeV8JvwSruIuOLkb5LjziFswHUJ8eHrkS&#43;WczSN1mgw5rrx
A7pKwjInH&#43;uf/gs3u84Fx9rrgwPTfLQN&#43;&#43;&#43;iuDYobWddwFWvXyCpJ/nBene2i8Dr
EwLxgEHAAUEDVmhQLv0TkRdFwhc4Rsds5ajDZHgWzj1GPw6SLpH4QCe02fvBm4Xu
5E&#43;QArl1w47DLJMktoxCT/8tTRtEdls8hwu5WHRJmq3PLJmC9ScSrUmN3S9k3Nrj
Ue5mkkZB25fCojBfRkfska9iYsASi2WxuKLsoiqbRqvi2KdgZ7OIGZM5HRUf9WNH
XEBD36MCgXA3YEjZPhBrVbOXsqosa5MLiV7XD684K6YsKf37hxqZC7p&#43;XhtcHxwh
Pg6AkODzJuZJV2h75UhqHiLSB9xhpX1mtV8IaToyiGRjnLuDthEDsFe7JjejF2cx
EXK9Jop7SSqAbB95WsLiWZtvaBgmcyv7XLoe9v5xAm0HyQ97Jn84hnXB1d8QQon7
YYCMNgyLDMo7TlI4HPmgVQYU7/P4xbo6cBdOicif8N&#43;kj0Pf6uFQZ8TB&#43;/Grqsgo
xqyrVpCTo/FjabJc8ybN36GwuZVMXpkl3etf2Tmls4A4jDP6CsB5F9vcRnVHyeic
pihbZa4Gb9GZTdFmFAHuXVHyVU9APRAq9MMmrUJB9oJgvCOM&#43;Cw=
=t4W3
-----END PGP SIGNATURE-----
</pre>
</details><p><em>Verify locally:</em></p>
<pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">torsocks curl -fSLO http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/sources/posts/blackout.md
torsocks curl -fSLO http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/sources/posts/blackout.md.asc
gpg --verify blackout.md.asc blackout.md
</pre>
</div>
</div>
<footer class="post-footer">
<ul class="post-tags">
</ul>
<nav class="paginav">
<a class="prev" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/pi_service_touchup/">
<span class="title">« Prev</span>
<br>
<span>Nextcloud on a Raspberry Pi, Fronted by a Tiny VPS — Nginx Reverse Proxy, Trusted Proxies, and Basic Auth for Jellyfin</span>
</a>
<a class="next" href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/2025_highlight/">
<span class="title">Next »</span>
<br>
<span>2025 Highlight</span>
</a>
</nav>
</footer><div class="comments-switch" style="display:flex;gap:.5rem;margin:.5rem 0 1rem;">
<button id="btn-isso" type="button" aria-pressed="true">Anonymous (Isso)</button>
<button id="btn-giscus" type="button" aria-pressed="false">GitHub (Giscus)</button>
<a href="https://github.com/AlipourIm/blog-comments/discussions/categories/comments" target="_blank" rel="noopener">Open on GitHub ↗</a>
</div>
<div id="panel-giscus" style="display:none">
<section id="giscus-thread"></section>
</div>
<div id="panel-isso">
<section id="isso-thread"
data-isso-id="/posts/blackout/"
data-title="Blackout">
<noscript>Javascript needs to be activated to view comments.</noscript>
</section>
<script
src="/isso/js/embed.min.js"
data-isso="/isso/"
data-isso-css="false"
data-isso-lang="en"
data-isso-max-comments-nested="5"
data-isso-sorting="newest"
async>
</script>
<small class="isso-powered" style="display:block;margin:.5rem 0;color:var(--secondary,#888)">
Comments powered by <a href="https://isso-comments.de" target="_blank" rel="noopener">Isso</a>
</small>
</div>
<script>
(function () {
var panelIsso = document.getElementById('panel-isso');
var panelGisc = document.getElementById('panel-giscus');
var issoBtn = document.getElementById('btn-isso');
var giscBtn = document.getElementById('btn-giscus');
var gLoaded = false;
function isDark() {
var pref = localStorage.getItem('pref-theme');
if (pref === 'dark') return true;
if (pref === 'light') return false;
var html = document.documentElement, body = document.body;
if (
(html.classList && html.classList.contains('dark')) ||
(body.classList && body.classList.contains('dark')) ||
html.getAttribute('data-theme') === 'dark' ||
body.getAttribute('data-theme') === 'dark' ||
(html.classList && html.classList.contains('theme-dark')) ||
(body.classList && body.classList.contains('theme-dark'))
) return true;
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
}
function gTheme(){ return isDark() ? 'dark_dimmed' : 'noborder_light'; }
function loadGiscus() {
if (gLoaded) return;
var s = document.createElement('script');
s.src = 'https://giscus.app/client.js';
s.async = true;
s.crossOrigin = 'anonymous';
s.setAttribute('data-repo','AlipourIm/blog-comments');
s.setAttribute('data-repo-id','R_kgDOQGARyA');
s.setAttribute('data-category','Comments');
s.setAttribute('data-category-id','DIC_kwDOQGARyM4Cw3x-');
s.setAttribute('data-mapping','pathname');
s.setAttribute('data-strict','0');
s.setAttribute('data-reactions-enabled','1');
s.setAttribute('data-emit-metadata','0');
s.setAttribute('data-input-position','bottom');
s.setAttribute('data-lang','en');
s.setAttribute('data-theme', gTheme());
document.getElementById('giscus-thread').appendChild(s);
function reTheme(){
var iframe = document.querySelector('iframe.giscus-frame');
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage(
{ giscus: { setConfig: { theme: gTheme() } } },
'https://giscus.app'
);
}
}
var tbtn = document.getElementById('theme-toggle');
if (tbtn) tbtn.addEventListener('click', function(){ setTimeout(reTheme, 0); });
var mo = new MutationObserver(function(){ reTheme(); });
mo.observe(document.documentElement, { attributes:true, attributeFilter:['class','data-theme'] });
mo.observe(document.body, { attributes:true, attributeFilter:['class','data-theme'] });
gLoaded = true;
}
function show(which) {
var showIsso = which === 'isso';
panelIsso.style.display = showIsso ? 'block' : 'none';
panelGisc.style.display = showIsso ? 'none' : 'block';
if (issoBtn) issoBtn.setAttribute('aria-pressed', showIsso ? 'true' : 'false');
if (giscBtn) giscBtn.setAttribute('aria-pressed', showIsso ? 'false' : 'true');
if (!showIsso) loadGiscus();
}
if (issoBtn) issoBtn.addEventListener('click', function(e){ e.preventDefault(); show('isso'); });
if (giscBtn) giscBtn.addEventListener('click', function(e){ e.preventDefault(); show('giscus'); });
show('isso');
})();
</script>
</article>
</main>
<footer class="footer">
<span>&copy; 2026 <a href="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/">AlipourIm journeys</a></span> ·
<span>
Powered by
<a href="https://gohugo.io/" rel="noopener noreferrer" target="_blank">Hugo</a> &
<a href="https://github.com/adityatelange/hugo-PaperMod/" rel="noopener" target="_blank">PaperMod</a>
</span>
</footer>
<a href="#top" aria-label="go to top" title="Go to Top (Alt + G)" class="top-link" id="top-link" accesskey="g">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 6" fill="currentColor">
<path d="M12 6H0l6-6z" />
</svg>
</a>
<div class="site-meta-row" style="display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;">
<a href="/index.xml" rel="alternate" type="application/rss+xml" title="RSS"
class="rss-link" style="display:inline-flex;align-items:center;gap:.35rem;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M4 11a9 9 0 0 1 9 9" />
<path d="M4 4a16 16 0 0 1 16 16" />
<circle cx="5" cy="19" r="1" />
</svg>
<span>RSS</span>
</a>
<span aria-hidden="true">·</span>
<span>Visitors this week: <span id="gc_site_visitors_week"></span></span>
</div>
<script>
let menu = document.getElementById('menu')
if (menu) {
menu.scrollLeft = localStorage.getItem("menu-scroll-position");
menu.onscroll = function () {
localStorage.setItem("menu-scroll-position", menu.scrollLeft);
}
}
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener("click", function (e) {
e.preventDefault();
var id = this.getAttribute("href").substr(1);
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView({
behavior: "smooth"
});
} else {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView();
}
if (id === "top") {
history.replaceState(null, null, " ");
} else {
history.pushState(null, null, `#${id}`);
}
});
});
</script>
<script>
var mybutton = document.getElementById("top-link");
window.onscroll = function () {
if (document.body.scrollTop > 800 || document.documentElement.scrollTop > 800) {
mybutton.style.visibility = "visible";
mybutton.style.opacity = "1";
} else {
mybutton.style.visibility = "hidden";
mybutton.style.opacity = "0";
}
};
</script>
<script>
document.getElementById("theme-toggle").addEventListener("click", () => {
if (document.body.className.includes("dark")) {
document.body.classList.remove('dark');
localStorage.setItem("pref-theme", 'light');
} else {
document.body.classList.add('dark');
localStorage.setItem("pref-theme", 'dark');
}
})
</script>
<script>
document.querySelectorAll('pre > code').forEach((codeblock) => {
const container = codeblock.parentNode.parentNode;
const copybutton = document.createElement('button');
copybutton.classList.add('copy-code');
copybutton.innerHTML = 'copy';
function copyingDone() {
copybutton.innerHTML = 'copied!';
setTimeout(() => {
copybutton.innerHTML = 'copy';
}, 2000);
}
copybutton.addEventListener('click', (cb) => {
if ('clipboard' in navigator) {
navigator.clipboard.writeText(codeblock.textContent);
copyingDone();
return;
}
const range = document.createRange();
range.selectNodeContents(codeblock);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
try {
document.execCommand('copy');
copyingDone();
} catch (e) { };
selection.removeRange(range);
});
if (container.classList.contains("highlight")) {
container.appendChild(copybutton);
} else if (container.parentNode.firstChild == container) {
} else if (codeblock.parentNode.parentNode.parentNode.parentNode.parentNode.nodeName == "TABLE") {
codeblock.parentNode.parentNode.parentNode.parentNode.parentNode.appendChild(copybutton);
} else {
codeblock.parentNode.appendChild(copybutton);
}
});
</script>
</body>
</html>