summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.well-known/host-meta.xml4
-rw-r--r--Makefile17
-rw-r--r--_comments.sh3
-rw-r--r--_config.yml7
-rw-r--r--_data/news.yml4
-rw-r--r--_drafts/bird-cpu-usage.md170
-rw-r--r--_includes/commentNotice.html35
-rw-r--r--_includes/header.html1
-rw-r--r--_layouts.gemini/default.html1
-rw-r--r--_layouts.gemini/home.html16
-rw-r--r--_layouts.gemini/page.html9
-rw-r--r--_layouts.gemini/post.html19
-rw-r--r--_layouts.gemini/postlist.html63
-rw-r--r--_layouts/postlist.html10
-rw-r--r--_posts/2009-02-07-techtalk-einen-gameboy-auf-dem-pc.md18
-rw-r--r--_posts/2021-03-23-winvm.md1
-rw-r--r--_posts/2021-04-25-networkstuff.md1
-rw-r--r--_posts/2021-06-03-fakir-teardown.md1
-rw-r--r--_posts/2021-06-27-wpa-enterprise-unifi.md1
-rw-r--r--_posts/2021-09-22-problems-updating-proxmox.md1
-rw-r--r--_posts/2021-10-28-rant-devuan-upgrade.md1
-rw-r--r--_posts/2022-01-20-server-stuff.md2
-rw-r--r--_posts/2022-04-03-backporting-pipewire-in-devuan.md2
-rw-r--r--_posts/2024-08-02-use-a-second-signal-account-kinda.md7
-rw-r--r--_posts/2024-09-04-convention-report-east-12-2024.md1
-rw-r--r--_posts/2025-01-01-2024-review.md1
-rw-r--r--_posts/2025-01-01-my-static-blog-now-has-activitypub.md1
-rw-r--r--_posts/2025-01-12-migrating-dns-servers.md289
-rw-r--r--_posts/2025-02-02-dn42-put-it-in-a-box-linux-network-namespace.md134
-rw-r--r--_posts/2025-03-06-monitoring-with-uptime-kuma.md49
-rw-r--r--_posts/2025-03-18-games-i-play_ed.md112
-rw-r--r--_posts/2025-04-05-random-thought-smartphones-with-old-technology.md20
-rw-r--r--_posts/2025-04-06-i-don-t-get-kubernetes.md38
-rw-r--r--_posts/2025-05-11-table-tennis.md14
-rw-r--r--_posts/2025-05-25-cpu-usage-problems-again.md16
-rw-r--r--_posts/2025-05-29-furdew-valley.md24
-rw-r--r--_posts/2025-06-16-oops-dn42-stops-working.md73
-rw-r--r--_posts/2025-06-22-blog-now-on-gemini-with-dn42.md39
-rw-r--r--assets/main.scss6
-rw-r--r--assets/mscomm/comments.js311
-rw-r--r--assets/mscomm/styles.css92
-rw-r--r--contact.html2
-rw-r--r--dn42.md2
-rw-r--r--feed.gmi22
-rw-r--r--links.md15
-rw-r--r--projects.md33
-rw-r--r--readme.txt3
-rw-r--r--search.html29
48 files changed, 1683 insertions, 37 deletions
diff --git a/.well-known/host-meta.xml b/.well-known/host-meta.xml
new file mode 100644
index 0000000..3f156a0
--- /dev/null
+++ b/.well-known/host-meta.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+ <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
+ <Link rel="lrdd" type="application/json" template="https://blog.uvokchee.de/.well-known/webfinger?resource={uri}"></Link>
+ </XRD> \ No newline at end of file
diff --git a/Makefile b/Makefile
index 8971d98..bb05be9 100644
--- a/Makefile
+++ b/Makefile
@@ -21,3 +21,20 @@ watch: clean
ipfs: clean
JEKYLL_ENV=ipfs bundle exec jekyll build
cd _site && cp all.html index.html && rm google* && sed -i -e 's|="/|="|g' *.html && tar cvf ../ipfs.tar $$(grep -L redirect *.html) assets index.html
+
+gemini: clean
+ mv _layouts _layouts.bak
+ mv _layouts.gemini _layouts
+ sed -i.bak -e '/import/ d' assets/main.scss
+ sed -i.bak -e '/seo/ d' _includes/head.html
+ cp _config.yml _config.yml.bak
+ awk '/pagination/ {replace=1;} /enabled/ && replace==1 {replace=0; print " enabled: false"; next;} {print;}' _config.yml > x
+ sed -i -e '/theme/ d' x
+ mv x _config.yml
+ JEKYLL_ENV=gemini bundle3.1 exec jekyll build
+ mv _config.yml.bak _config.yml
+ mv assets/main.scss.bak assets/main.scss
+ mv _includes/head.html.bak _includes/head.html
+ mv _layouts _layouts.gemini
+ mv _layouts.bak _layouts
+ rm -rf _site/category
diff --git a/_comments.sh b/_comments.sh
new file mode 100644
index 0000000..3c29a1a
--- /dev/null
+++ b/_comments.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+[[ "$1" = "-c" ]] && scp hetzner:/var/www/hatsu/hatsu.sqlite3 .
+sqlite3 -box hatsu.sqlite3 'SELECT id, in_reply_to_root FROM post where in_reply_to IS NOT NULL;'
diff --git a/_config.yml b/_config.yml
index 80f282c..911fae5 100644
--- a/_config.yml
+++ b/_config.yml
@@ -49,7 +49,7 @@ minima:
mastodon:
- username: uvok
- instance: furry.engineer
+ instance: woof.tech
#show_excerpts: true
@@ -71,4 +71,7 @@ autopages:
collections:
enabled: false
-cssversion: "2023120301"
+cssversion: "2024010301"
+
+feed:
+ posts_limit: 20
diff --git a/_data/news.yml b/_data/news.yml
index 4684514..b73a5e9 100644
--- a/_data/news.yml
+++ b/_data/news.yml
@@ -1,8 +1,8 @@
-- id: AdBlock
+- id: AdBanner
content: >-
<div>
<div style="font-weight: bold">You don't seem to be using an ad blocker.</div>
- <div>Please consider installing one.</div>
+ <div>Please consider installing one. I would recommend uBlock Origin.</div>
</div>
- id: news1
content: >-
diff --git a/_drafts/bird-cpu-usage.md b/_drafts/bird-cpu-usage.md
new file mode 100644
index 0000000..c0fa5b7
--- /dev/null
+++ b/_drafts/bird-cpu-usage.md
@@ -0,0 +1,170 @@
+---
+layout: post
+title: Bird CPU usage
+date: 2025-01-07 19:06 +0100
+categories: tech
+lang: en
+---
+
+**Preface**: This is kind of a "bug hunt log". I extensively posted on Mastodon
+about it, but wanted to keep the progress in writing as well. This might lead
+like random rambling in some parts, because I edited and rearranged the
+paragraphs of this post a few times. Sorry for that.
+
+**Preface 2**: Starting point: In September 2024 I upgraded to bird2
+`2.15.1-1~bpo12+1` - probably a self-built backport of mine. This, I "had to
+to", since I wanted the Babel metrics measurement, which is not present in
+Debian stable (`2.0.12-7`). Yeah, I won't complain. For 99 % of the stuff I do,
+Debian stable is *perfect*. I want a server without regularly breaking things,
+after all. Only recently (2015-01-07), I added the bird / network.cz repos and
+installed bird2 from there.
+
+Several times already, I noticed this in my Munin monitoring:
+
+{% image
+img="https://pics.uvokchee.de/upload/2025/01/07/20250107180512-c1453895.png"
+alt="RRD tool graphic showing a high CPU usage" %}
+
+I found it strange, but had no time to inspect further.
+
+Recently, I tried to investigate what happens on the server there. htop showed
+high CPU usage for both bird and dnsmasq (always together) in these times.
+
+Fuming (angry) a bit, I went with a brute-force approach:
+
+```
+#!/bin/bash
+
+# Configuration
+THRESHOLD=1.0
+# 3 hours
+DURATION=$((3*80*80))
+MAIL_TO="lolnope"
+SUBJECT="High Load Average Alert"
+BODY="The load average has been above ${THRESHOLD} for more than 3 hours."
+REBOOT_CMD="/sbin/reboot"
+
+# Function to check the load average
+check_load() {
+ # 15 min loadavg
+ loadavg=$(awk '{print $3}' /proc/loadavg)
+ echo "$(date): Current Load Average: $loadavg"
+
+ if (( $(echo "$loadavg > $THRESHOLD" | bc -l) )); then
+ echo "$(date): Load average is above threshold."
+ return 0
+ else
+ echo "$(date): Load average is below threshold."
+ return 1
+ fi
+}
+
+# Monitor the load average
+start_time=$(date +%s)
+while true; do
+ if check_load; then
+ current_time=$(date +%s)
+ elapsed_time=$((current_time - start_time))
+
+ if [ "$elapsed_time" -gt "$DURATION" ]; then
+ echo "$(date): Load average has been above threshold for more than 3 hours."
+
+ # Send alert email
+ (echo "$BODY"; ps -e -o %cpu,%mem,cmd --sort pcpu | tail) | mail -s "$SUBJECT" "$MAIL_TO"
+
+ # Reboot the server - room for improvement
+# systemctl stop bird
+# systemctl start bird
+ $REBOOT_CMD
+ break
+ fi
+ else
+ start_time=$(date +%s)
+ fi
+ sleep 300 # Check every 5 minutes
+done
+
+```
+
+Specifically, the output of ps
+
+```
+22.7 2.7 /usr/sbin/bird -f -u bird -g bird
+33.3 0.1 ps -e -o %cpu,%mem,cmd --sort pcpu
+37.4 0.0 /usr/sbin/dnsmasq -x /run/dnsmasq/dnsmasq.pid -u dnsmasq -7 /etc/dnsmasq.d,.dpkg-dist,.dpkg-old,.dpkg-new,.bak --local-service --trust-anchor=.,20326,8,2,e06d44b80b8f1d39a95c0b0d7c65d08458e880409bbc683457104237c7f8ec8d
+```
+
+confirmed the quick look at htop - although the "percentage" is a bit weird.
+From the manpage:
+
+> Currently, it is the CPU time used divided by the time the process has been
+> running (cputime/realtime ratio), expressed as a percentage.
+
+(So if the process runs "long enough" and only starts misbehaving after a year,
+it won't show up?).
+
+I asked an LLM what to do, in addition to strace, and it suggested perf.
+Unfortunately, this requires debug symbols [1]. [And while Debian does provide
+debug symbols](https://wiki.debian.org/HowToGetABacktrace) - it doesn't for
+dnsmasq (yet) in bookworm. Luckily, the nice people at labs.nic.cz provide a
+dbgsym package in their Bird(2) Debian repository.
+
+Aside, the script above could benefit from some improvements, instead of
+rebooting, I could actually record and strace and perf when this happens, and
+only then reboot / restart the service.
+
+Now, stracing dnsmasq (when "idle") reveals some recvmsg of type
+`RTM_NEWROUTE`. I have *no idea* why dnsmasq would need that. But I already
+*assume* the high CPU usage occurs when Bird exports lots of routes to the
+kernel. *I have no hecking clue why dnsmasq would need to use the CPU at the
+same time. If someone can enlighten me, **please** message me.*
+
+Also, in journalctl, I see lots of the infamous `Kernel dropped some netlink
+messages, will resync on next scan.` messages at times - the message apparently
+nobody has a solution to, and even though there are mailing list posts telling
+to sysctl `net.core.rmem_default`, I doesn't seem to yield a solution.
+
+I actually did a `perf record` of bird when the CPU usage was high, and I saw
+that `rt_event` was running 33% of the time. I don't know what to make of that.
+
+So… step-by-step? I have both DN42 and clearnet bird running on this VPS, in
+parallel. So maybe start by disabling one of these? Or, I have a better idea,
+keep it enabled, and disable all protocols! (`birdc disable \"*\"`). That
+helped, until midnight. When suddenly the CPU went up again. WTF? Let's have a
+look at `birdc s p`. All protocols up. Huh!?!?
+
+Let's investigate further:
+
+* Oooo, log rotation happens at midnight
+* Fuck, I specified the same log files for both bird's
+
+Well, log rotation. Which I added myself. This does a `birdc configure`
+afterward. Which means the protocols go up again, because I disabled them on the
+command line, not in the config.
+
+Ungh. Okay, this is getting really ugly. `systemctl disable --now bird-clear`.
+Now let's run this for a few days...
+
+That seem to have helped. I now decided to edit the clearnet config and disable
+all "external" protocols (to IXPs), keeping Babel, RPKI, and IBGP enabled.
+Immediately after starting bird-clearnet, the CPU usage went up again. To be
+expected, for the initial sync. But it kept being high. So, I disabled the RPKI
+protocols as well and... suddenly, the CPU usage is down???
+
+(2025-01-09) So, now, the RPKI stuff... I saw that the connection to one of my
+RPKI servers wouldn't work (Fort Validator), and Fort complained about `Bad RTR
+version: 2`. The [Bird
+docs](https://bird.network.cz/?get_doc&v=20&f=bird-6.html) mention a `max
+version` option, and with that, the connection worked. And the CPU usage was
+down. *However*, this was not "the root issue". This option is only present
+since bird 2.16, which I only installed on 2025-01-07 [2]. And I had the CPU
+problems back in November last year! So this is "something completely
+different", it appears. I currently have the BGP sessions to virtual IXPs
+disabled, and will run it like this for a few days. The average load is 0.4.
+Still too high, compared to my other VPS.
+
+[1] At least when I want to see the binaries function names. Kernel symbols
+seem to show up fine. \
+[2] Which is pretty annoying, when you chase one bug, you change something in
+your config, and then something else breaks. That's on me.
+
diff --git a/_includes/commentNotice.html b/_includes/commentNotice.html
index b1be501..a46dadf 100644
--- a/_includes/commentNotice.html
+++ b/_includes/commentNotice.html
@@ -1,19 +1,44 @@
<section>
+ {% assign base_url = 'https://blog.uvokchee.de/notice/' %}
+ {% assign url = page.url %}
+ {% assign domain = 'https://blog.uvokchee.de' %}
+ {% assign full_url = domain | append: page.url %}
+ {% capture encoded_url %}{{ full_url | base64_encode }}{% endcapture %}
+ {% assign src_url = base_url | append: encoded_url %}
+
<h2>Kommentare / Comments</h2>
- <p>Kommentare werden von mir selbst auf einem anderen Server über <a href="https://isso-comments.de/">Isso</a> gehostet.</p>
+ <h3>Isso</h3>
+
+ <p>Kommentare werden von mir selbst auf einem anderen Server über <a href="https://isso-comments.de/">Isso</a>
+ gehostet.</p>
<p>Comments are hosted by myself on another server, powered by <a href="https://isso-comments.de/">Isso</a>.</p>
- <script data-isso="//c.uvokchee.de/"
- data-isso-require-author=true
- data-isso-vote=false
+ <script data-isso="//c.uvokchee.de/" data-isso-require-author=true data-isso-vote=false
src="//c.uvokchee.de/js/embed.min.js"></script>
<section id="isso-thread">
{% if jekyll.environment == "development" %}
{% include testc.html %}
{% endif %}
</section>
+
+ <h3>Fediverse</h3>
+ <script type="module">
+ import Comments from '{{ "/assets/mscomm/comments.js" | relative_url }}'
+ export function loadComments() {
+ document.getElementById('load-comments-btn').remove();
+ customElements.define('oom-comments', Comments);
+ }
+ window.loadComments = loadComments;
+ </script>
+ <link rel="stylesheet" href="{{ "/assets/mscomm/styles.css" | relative_url }}?v={{ site.cssversion }}">
+ <oom-comments src="{{ src_url }}">
+ <button id="load-comments-btn" class="load-comments-btn" onclick="loadComments()">Load Comments from
+ ActivityPub</button>
+ </oom-comments>
+ <p style="margin-top: 1em"><a href="{{ src_url }}">ActivityPub-Link</a></p>
+
<noscript>
<p>Um Kommentare zu hinterlassen, ist leider JavaScript nötig.</p>
- <p>Unfortunately, JavaScript is required to leave comments</p>
+ <p>Unfortunately, JavaScript is required to leave comments.</p>
</noscript>
</section>
diff --git a/_includes/header.html b/_includes/header.html
index 492dcd5..86e812e 100644
--- a/_includes/header.html
+++ b/_includes/header.html
@@ -13,7 +13,6 @@
</label>
<div class="trigger" lang="en">
{% include_cached navlinks.html %}
- <a class="page-link" href="https://uvokchee.de/wiki/">Wiki</a>
<a class="rss-subscribe page-link" href="{{ "/feed.xml" | relative_url }}">RSS feed</a>
</div>
</nav>
diff --git a/_layouts.gemini/default.html b/_layouts.gemini/default.html
new file mode 100644
index 0000000..cddd070
--- /dev/null
+++ b/_layouts.gemini/default.html
@@ -0,0 +1 @@
+{{ content }}
diff --git a/_layouts.gemini/home.html b/_layouts.gemini/home.html
new file mode 100644
index 0000000..4e877fb
--- /dev/null
+++ b/_layouts.gemini/home.html
@@ -0,0 +1,16 @@
+---
+layout: default
+---
+
+{{ content }}
+
+ {%- if site.posts.size > 0 -%}
+ {%- for post in site.posts -%}
+ {%- assign date_format = site.minima.date_format | default: "%b %-d, %Y" -%}
+ <span class="post-meta">{{ post.date | date: date_format }}
+ {%- if post.lang == 'de' %} (DE){%- endif -%}
+ <a class="post-link" href="{{ post.url | relative_url }}">
+ {{ post.title | escape }}
+ </a>
+ {%- endfor -%}
+ {%- endif -%}
diff --git a/_layouts.gemini/page.html b/_layouts.gemini/page.html
new file mode 100644
index 0000000..dda953c
--- /dev/null
+++ b/_layouts.gemini/page.html
@@ -0,0 +1,9 @@
+---
+layout: default
+---
+ <h1 class="post-title">{{ page.title | escape }}</h1>
+
+ <div class="post-content">
+ {{ content }}
+ </div>
+
diff --git a/_layouts.gemini/post.html b/_layouts.gemini/post.html
new file mode 100644
index 0000000..d3a93ae
--- /dev/null
+++ b/_layouts.gemini/post.html
@@ -0,0 +1,19 @@
+---
+layout: default
+---
+ <h1 class="post-title p-name" itemprop="name headline">{{ page.title | escape }}</h1>
+ <p class="post-meta">
+ <p>
+ {%- assign date_format = site.minima.date_format | default: "%b %-d, %Y" -%}
+ {%- assign date_format = "%A, %e. %B %Y, %k:%M" -%}
+ Published: {{ page.date | date: date_format }}
+
+ {%- if page.last_modified_at -%}
+ , Updated: {{ page.last_modified_at | date: date_format }}
+ {%- endif -%}
+ </p>
+
+ </p>
+ <div class="post-content e-content" itemprop="articleBody">
+ {{ content }}
+ </div>
diff --git a/_layouts.gemini/postlist.html b/_layouts.gemini/postlist.html
new file mode 100644
index 0000000..7be4784
--- /dev/null
+++ b/_layouts.gemini/postlist.html
@@ -0,0 +1,63 @@
+---
+layout: default
+---
+
+<p>
+ This Gemini site has been converted without all-to-much effort from
+ my Jekyll blog, which uses Markdown as input.
+ As the conversion process Markdown > HTML > Markdown > Gemini is lossy,
+ the result looks somewhat unsatisfactory.
+ I haven't found a good conversion process that saves me the effort of
+ manually rewriting everything. If you know something, please let me know.
+</p>
+
+<p>Navigation</p>
+
+ {% assign spag = site.pages | sort: "order" %}
+ {% for my_page in spag %}
+ {% if my_page.title and my_page.content and my_page.url != "/index.html" and my_page.in_navbar %}
+<a href="{{ my_page.url | relative_url }}">{{ my_page.title | escape }}</a>
+ {% endif %}
+ {% endfor %}
+<a href="/feed.gmi">Feed link</a>
+
+ {%- if page.title -%}
+ <h1 class="page-heading">{{ page.title }}</h1>
+ {%- endif -%}
+
+ {%- if site.posts.size > 0 -%}
+
+ {% assign paginator = site %}
+
+ {%- for post in paginator.posts -%}
+ {%- assign showsep = false -%}
+ {%- assign date_format = site.minima.date_format | default: "%b %-d, %Y" -%}
+ {%- if post.lang == 'de' %} (DE) {%- endif -%}
+ <div lang="{{ post.lang | default: site.lang | default: "en" }}">
+ <h2>
+ {{ post.title | escape }}
+ </h2>
+ <span class="post-meta">Published: {{ post.date | date: date_format }}</span>
+<p>
+ {%- if forloop.first and paginator.page == 1 or paginator == site -%}
+ {{ post.content }}
+ {%- assign showsep = true -%}
+ {%- elsif site.show_excerpts -%}
+ {{ post.excerpt }}
+ {%- assign showsep = true -%}
+ {%- elsif post.description -%}
+ {{ post.description }}
+ {%- assign showsep = true -%}
+ {%- endif -%}
+</p>
+ <a class="post-link" href="{{ post.url | relative_url }}">Link to full post</a>
+
+ </div>
+ {%- if showsep -%}
+ <hr class="postsep" />
+ {%- endif -%}
+ {%- endfor -%}
+
+ {%- endif -%}
+
+</div>
diff --git a/_layouts/postlist.html b/_layouts/postlist.html
index 67a1d48..b6b29ef 100644
--- a/_layouts/postlist.html
+++ b/_layouts/postlist.html
@@ -39,6 +39,9 @@ layout: default
{%- elsif site.show_excerpts -%}
{{ post.excerpt }}
{%- assign showsep = true -%}
+ {%- elsif post.description -%}
+ {{ post.description }}
+ {%- assign showsep = true -%}
{%- endif -%}
</div>
</li>
@@ -56,9 +59,14 @@ layout: default
<script>
document.addEventListener("DOMContentLoaded", function() {
- const ids = [ "AdBlock", "news1" ];
+ {%- assign news_ids = site.data.news | map: 'id' | jsonify %}
+
+ const ids = {{ news_ids }};
for (currId of ids) {
const currentBanner = document.getElementById(currId);
+ if (!currentBanner) {
+ continue;
+ }
const closeLink = currentBanner.firstElementChild;
const currentNewsClosed = localStorage.getItem(currId+"closed");
if (currentNewsClosed === "true") {
diff --git a/_posts/2009-02-07-techtalk-einen-gameboy-auf-dem-pc.md b/_posts/2009-02-07-techtalk-einen-gameboy-auf-dem-pc.md
index 2c556d6..f02d62a 100644
--- a/_posts/2009-02-07-techtalk-einen-gameboy-auf-dem-pc.md
+++ b/_posts/2009-02-07-techtalk-einen-gameboy-auf-dem-pc.md
@@ -103,19 +103,13 @@ UPDATE (30.4.2009):
### Emulatoren
-- <a href="https://de.wikipedia.org/wiki/Emulator">Emulator</a> auf
-Wikipedia
-- <a href="https://emulator-zone.com/">Emulator Zone</a> Eine
-Übersicht über Spielkonsolenemulatoren.
-- _VisualBoyAdvance (toter Link entfernt)_
-Homepage des Emulators "VisualBoyAdvance"
+- <a href="https://de.wikipedia.org/wiki/Emulator">Emulator</a> auf Wikipedia
+- <a href="https://emulator-zone.com/">Emulator Zone</a> Eine Übersicht über Spielkonsolenemulatoren.
+- _VisualBoyAdvance (toter Link entfernt)_ Homepage des Emulators "VisualBoyAdvance"
- [SourceForge-Projektseite vom Emulator](https://sourceforge.net/projects/vba/)
### Spiele
-- <a href="https://de.wikipedia.org/wiki/ROM_%28Spielmodul%29">ROM</a>
-auf Wikipedia
-- _PDRoms / Gameboy (toter Link entfernt)_
-Legale ROMs für den GameBoy
-- _PDRoms / GameBoy Advance (toter Link entfernt)_
-Legale ROMs für den GameBoy Advance
+- <a href="https://de.wikipedia.org/wiki/ROM_%28Spielmodul%29">ROM</a> auf Wikipedia
+- _PDRoms / Gameboy (toter Link entfernt)_ Legale ROMs für den GameBoy
+- _PDRoms / GameBoy Advance (toter Link entfernt)_ Legale ROMs für den GameBoy Advance
diff --git a/_posts/2021-03-23-winvm.md b/_posts/2021-03-23-winvm.md
index 528d636..3ee73f4 100644
--- a/_posts/2021-03-23-winvm.md
+++ b/_posts/2021-03-23-winvm.md
@@ -4,6 +4,7 @@ title: Windows VM under Devuan with QEMU / libvirt
date: 2021-03-23 20:32 +0100
categories: tech
description: How to run a Windows VM under Devuan with QEMU / libvirt
+lang: en
---
So, I'm running Devuan Beowulf (Debian Buster based). Recently, I was in the need of running a
diff --git a/_posts/2021-04-25-networkstuff.md b/_posts/2021-04-25-networkstuff.md
index 3152f3f..7b2cff6 100644
--- a/_posts/2021-04-25-networkstuff.md
+++ b/_posts/2021-04-25-networkstuff.md
@@ -4,6 +4,7 @@ title: Network stuff
date: 2021-04-25 21:50 +0200
categories: tech
description: "I do some experiments with networking stuff. Proxmox and running OpenWRT in a VM."
+lang: en
---
Been a long time.
diff --git a/_posts/2021-06-03-fakir-teardown.md b/_posts/2021-06-03-fakir-teardown.md
index 1de8bf9..75f609d 100644
--- a/_posts/2021-06-03-fakir-teardown.md
+++ b/_posts/2021-06-03-fakir-teardown.md
@@ -3,6 +3,7 @@ layout: post
title: Fakir Robert Teardown
date: 2021-06-03 20:39 +0200
description: Ein Teardown eines alten Staubsauger-Roboters
+lang: de
---
Da ich mir demnächst mal einen neuen gebrauchten Saugroboter holen werde,
diff --git a/_posts/2021-06-27-wpa-enterprise-unifi.md b/_posts/2021-06-27-wpa-enterprise-unifi.md
index 61c49a7..36b9e6e 100644
--- a/_posts/2021-06-27-wpa-enterprise-unifi.md
+++ b/_posts/2021-06-27-wpa-enterprise-unifi.md
@@ -4,6 +4,7 @@ title: WPA Enterprise mit Unifi Access Points
date: 2021-06-27 20:28 +0200
categories: tech
description: "Wie man WPA Enterprise einrichtet, mit Unifi WiFi Access Points und Software, die unter Linux läuft"
+lang: de
---
Ich hab mal einen kurzen Artikel geschrieben, wie man WPA Enterprise
diff --git a/_posts/2021-09-22-problems-updating-proxmox.md b/_posts/2021-09-22-problems-updating-proxmox.md
index 3885b79..7353d67 100644
--- a/_posts/2021-09-22-problems-updating-proxmox.md
+++ b/_posts/2021-09-22-problems-updating-proxmox.md
@@ -2,6 +2,7 @@
layout: post
title: Network gone after updating Proxmox
date: 2021-09-22 20:59 +0200
+lang: en
---
So, after upgrading Proxmox from version 6.x to 7.x, the network
diff --git a/_posts/2021-10-28-rant-devuan-upgrade.md b/_posts/2021-10-28-rant-devuan-upgrade.md
index 100dd3b..c3da9b0 100644
--- a/_posts/2021-10-28-rant-devuan-upgrade.md
+++ b/_posts/2021-10-28-rant-devuan-upgrade.md
@@ -3,6 +3,7 @@ layout: post
title: 'Rant: Devuan Upgrade'
date: 2021-10-28 19:58 +0200
categories: tech
+lang: en
---
Gna. So I upgraded my Devuan installation from Beowulf to Chimaera. Process was a bit tricky,
diff --git a/_posts/2022-01-20-server-stuff.md b/_posts/2022-01-20-server-stuff.md
index 40d39a1..f5f053f 100644
--- a/_posts/2022-01-20-server-stuff.md
+++ b/_posts/2022-01-20-server-stuff.md
@@ -3,7 +3,7 @@ layout: post
title: Server stuff
date: 2022-01-20 18:55 +0100
category: tech
-language: en
+lang: en
description: "I experiment with SPF, DMARC and DKIM."
---
diff --git a/_posts/2022-04-03-backporting-pipewire-in-devuan.md b/_posts/2022-04-03-backporting-pipewire-in-devuan.md
index 1d20df2..c0a9fe3 100644
--- a/_posts/2022-04-03-backporting-pipewire-in-devuan.md
+++ b/_posts/2022-04-03-backporting-pipewire-in-devuan.md
@@ -3,7 +3,7 @@ layout: post
title: Backporting Pipewire in Devuan
date: 2022-04-03 17:45 +0200
category: tech
-language: en
+lang: en
---
**Update 2023-09-07**: With Devuan Daedelus Pipewire is in the
diff --git a/_posts/2024-08-02-use-a-second-signal-account-kinda.md b/_posts/2024-08-02-use-a-second-signal-account-kinda.md
index 90b7687..a9753ba 100644
--- a/_posts/2024-08-02-use-a-second-signal-account-kinda.md
+++ b/_posts/2024-08-02-use-a-second-signal-account-kinda.md
@@ -2,6 +2,7 @@
layout: post
title: Use a second Signal account (kinda)
date: 2024-08-02 18:48 +0200
+last_modified_at: 2025-05-29 16:50 +0200
lang: en
categories: ["tech"]
description: "How to use a second Signal account on your Smartphone"
@@ -52,3 +53,9 @@ already, so not a huge disadvantage).
But hey, maybe this is still useful for some people.
+**Update 2025-05-29**
+
+Actually, you can also use multiple accounts on the Desktop client.
+Since it's just an Electron app (?), you only need a second Launcher
+with the executable and arguments being
+`/opt/Signal/signal-desktop --user-data-dir=<whatever you want> %U`.
diff --git a/_posts/2024-09-04-convention-report-east-12-2024.md b/_posts/2024-09-04-convention-report-east-12-2024.md
index 7c71898..31785a8 100644
--- a/_posts/2024-09-04-convention-report-east-12-2024.md
+++ b/_posts/2024-09-04-convention-report-east-12-2024.md
@@ -3,6 +3,7 @@ layout: post
title: 'Convention Report: EAST 12 (2024)'
date: 2024-09-04 20:00 +0200
description: My convention report of the furry convention "EAST 12" in 2024
+lang: en
---
It's time for another convention report.
I've been going to the EAST convention since EAST 7 (except EAST 10, which was
diff --git a/_posts/2025-01-01-2024-review.md b/_posts/2025-01-01-2024-review.md
index 5d0d433..80cf09d 100644
--- a/_posts/2025-01-01-2024-review.md
+++ b/_posts/2025-01-01-2024-review.md
@@ -4,6 +4,7 @@ title: '2024: Review'
date: 2025-01-01 10:47 +0100
lang: "en"
categories: "life"
+description: "I muse over what I did in 2024"
---
So, already wanted to do this yesterday, but I think today works as well.
diff --git a/_posts/2025-01-01-my-static-blog-now-has-activitypub.md b/_posts/2025-01-01-my-static-blog-now-has-activitypub.md
index 62feced..1a5af4d 100644
--- a/_posts/2025-01-01-my-static-blog-now-has-activitypub.md
+++ b/_posts/2025-01-01-my-static-blog-now-has-activitypub.md
@@ -5,6 +5,7 @@ date: 2025-01-01 18:56 +0100
last_modified_at: 2025-01-01 19:34 +0100
categories: ["tech"]
lang: "en"
+description: "A short praise for Hatsu, thanks to which my posts now appear in the Fediverse."
---
Thanks to [Hatsu](https://github.com/importantimport/hatsu), my blog now has ActivityPub support.
diff --git a/_posts/2025-01-12-migrating-dns-servers.md b/_posts/2025-01-12-migrating-dns-servers.md
new file mode 100644
index 0000000..a0bf09b
--- /dev/null
+++ b/_posts/2025-01-12-migrating-dns-servers.md
@@ -0,0 +1,289 @@
+---
+layout: post
+title: Migrating DNS providers
+lang: en
+categories: tech
+date: 2025-01-12 16:49 +0100
+last_modified_at: 2025-01-15 19:57 +0100
+redirect_from: /2025/01/2025-01-12-migrating-dns-servers.html
+description: "An in-depth look into how to switch DNS providers without downtime."
+---
+
+**Update 2025-01-15:** Add links to tools, add clarification for DNSSEC tool,
+add clarification for validation errors/warnings.
+
+## Preface
+
+[As I posted on Mastodon](https://furry.engineer/@uvok/113780013806190576),
+
+* the DNS registrar (where you register the domain at),
+* the DNS hoster / provider (which is responsible for answering DNS requests for
+ your domain) and
+* the (web) hosting service
+
+can be three different entities, even though for most beginner projects these
+are the same.
+
+I either bought or migrated my domain over to [INWX](https://www.inwx.de/en)
+quite some while ago. However, their web interface for editing records is a bit
+clunky. And while they do provide an API, it's a bit awkward (XML based, and
+yes, there are ready-made packages for various programming languages). [^1]
+
+With [DN42](https://blog.uvokchee.de/dn42.html), I already used
+[PowerDNS](https://doc.powerdns.com/authoritative/) a lot. I actually just use
+edit-zone and edit the zone[^2] in an editor, that's my preferred way. Definitely
+preferred to some web interface. It would be nice to do this to my main domains
+as well. Also, this is another nice "have a technical task to procrastinate
+other stuff" thing ;3. Well, I like playing with tech. This is by no means
+essential.
+
+## Glossary
+
+Quick and dirty glossary, for the purpose of reading this article. It's not
+100% accurate and complete, but should be enough to understand the article.
+
+* **Domain Name**: Basically "the name of a server" (what you see in the address
+ bar of the browser, e.g. "blog.uvokchee.de").
+* **DNS**: The "Domain Name System". Very simplified, translates domain names
+ to IP addresses (which computers use to connect to each other).
+ To be more precise, it's like a lookup in a database (-> DNS record).
+* **TLD**: Top-Level Domain. It's "the last part of the domain name" (e.g.
+ ".de").
+* **Registry**: These run the TLDs.
+* **Registrar**: These sell you domain names from the registries. As an "end
+ user", you can't talk to a registry yourself.
+* **DNSSEC**: DNS is a plain-text, insecure protocol, from way back when the
+ internet consisted of a handful of trustworthy people. DNSSEC tries to prevent
+ malicious parties from sending wrong information in response to DNS requests.
+ It does so by signing the responses with a cryptographic key, of which the
+ public part is stored in the registry.
+* **NSEC, NSEC3**: Basically these are "negative replies", i.e. "this domain
+ does not exist". These are signed as well. If not present, malicious parties
+ could send back replies to sites they don't want to be reachable as "doesn't
+ exist" all the time.
+* **Zone**: A zone file contains all the -> DNS records for a specific domain.
+* **DNS record**: Like "a row in the database" which belongs to your domain. Can
+ be an address, but also things like text, PGP keys, pointer to other domain
+ names, etc..
+* **TTL**: Time To Live. The time (in seconds) a reply to a DNS request may be
+ cached by a server. For example, a TTL of 3600 seconds means that a (caching)
+ DNS server can cache a record for 1 hour before checking with the
+ -> authoritative server again.
+* **Authoritative Server:** The DNS server that holds the official and most
+ up-to-date records for a specific domain.
+ * **Primary**: The primary source of truth.
+ * **Secondary**: Mirrors the information from the primary. There can be more
+ than one.
+* **Recursion**: DNS is hierarchical. There are fixed "root servers" which serve
+ the nameservers for all the TLDs. The TLDs provide the nameservers for the
+ domains "below them". A recursive server "walks along this path" to answer
+ requests. At some point, it arrives at the authoritative server.
+* **Bogus reply**: A reply with invalid signature, either because it has been
+ tampered with, or because of wrong configuration.
+* **AXFR**: A request type for a -> Domain zone transfer.
+* **Domain Zone Transfer**: The process of copying the contents of a zone from
+ one DNS server to another.
+
+## Migration and DNSSEC
+
+So, why not migrate my domain over to my PowerDNS setup? Well, I want some
+availability, and I don't trust myself enough not to fuck things up. When my
+server is shut down, DNS requests should still be answered. But hey, that's
+where secondaries come in! So, I manage the zone on my server, as primary, and
+let another provider mirror its contents. These will actually "serve the zone".
+
+Now, only… there's the "problem" of DNSSEC, in conjunction with the DNS
+propagation time and TTLs. If I simply set the nameservers (in the de zone) to
+a newly created zone by PowerDNS, the DNSSEC keys will be either not present,
+or different. I can't set *additional* keys at INWX easily, I can only switch
+to manual keys (by switching off DNSSEC first). Which would lead to a downtime
+of up to one day, because people requesting records from my domain (such as
+myself) would receive bogus replies in that time. I want to avoid that.
+
+## Steps for migration
+
+But, I think I found a solution for that. This are the steps which I had to take
+for the registrar INWX and my uvokchee.de domain. The process might be different
+for other TLDs and other registrars.
+
+1. Download the zone data from the INWX web interface
+1. Create the zone in PowerDNS and set various settings
+
+ #!/bin/bash
+ zone="example.com"
+ pdnsutil create-zone $zone
+ pdnsutil set-kind $zone primary
+ pdnsutil secure-zone $zone
+ pdnsutil set-presigned $zone
+ pdnsutil set-meta $zone ALLOW-AXFR-FROM <ip of secondaries>
+ # optional? see below for a discussion
+ pdnsutil set-nsec3 $zone $parameters
+1. Filter and import the zone. PowerDNS will actually complain about the NSEC3
+ records otherwise once you open the zone in your editor.
+
+ # any other regex that filters only NSEC3 records, but not RRSIG NSEC3,
+ # work as well
+ grep -vw "IN NSEC3" ./dl-zone.txt > imp-zone.txt
+ pdnsutil load-zone $zone ./imp-zone.txt
+
+ This should keep the RRSIGs for the NSEC3 records. This doesn't help with
+ the errors I receive later, though…
+
+1. Set up the secondaries. I went with [Hurricane
+ Electric](https://dns.he.net/), but I had to write them an e-mail. I couldn't
+ add the zone as secondary myself, because the web interface requires the
+ nameservers in the parent zone (?) already to be set to HE, but I wanted a
+ zero-downtime migration.
+1. Wait for the DNS provider to AXFR.
+1. (optional) check with [dnsviz](https://dnsviz.net/) whether you get any
+ errors. You should set the nameserver on the "Analyze" tab, and then set the
+ "Additional trusted keys:" to what `dig example.com DNSKEY` tells you. At
+ this point, I got warnings/errors regarding the "Denial of existence" /
+ NSEC3 records, and some errors because the RRSIGs can't be checked. (Once
+ you set a manual nameserver, the tool doesn't seem to check for the parent
+ zone anymore? I'm unclear on that).
+1. Try to set the new nameservers (nsx.he.net) in the registry (e.g. DENIC) via
+ the INWX web interface ("external nameservers").
+1. Receive an "UPDATE FAILED". Read the error message.
+
+ ERROR: 53300102912 Nameserver error [ERROR: 118 Inconsistent set of NS
+ RRs (NS, IP, NS host names) (ns5.he.net, 2001:470:500::2, ['ns.inwx.de',
+ 'ns2.inwx.de', 'ns3.inwx.eu'])]
+
+ Realize you fucked up.
+ You need to set the new nameservers within your own zone first. This seems
+ to be a requirement for DENIC at any case.
+1. Add the NS entries to the HE nameservers within your zone in the INWX web
+ interface. [^4]
+1. Download and import the zone file again. Wait for the AXFR.
+1. Retry sending the nameservers to the registry. Wait until success.
+1. At this point, I checked again with dnsviz. With the default options,
+ everything was fine. However, when I enabled the "Denial of existence:"
+ option, I received some *extremely scary looking error messages*. I can only
+ guess this is because PowerDNS can't generate these records? I *have no
+ clue*. Any useful information will be gladly accepted. I choose to ignore it.
+
+ NSEC3 proving non-existence of l2v1y.7t5is.uvokchee.de/A: No RRSIG covering the RRset was returned in the response. See RFC 4035, Sec. 3.1.1. (216.218.130.2, 216.218.131.2, 216.218.132.2, 2001:470:100::2, 2001:470:200::2, 2001:470:300::2, UDP_-_EDNS0_4096_D_KN)
+ NSEC3 proving non-existence of uvokchee.de/CNAME: No RRSIG covering the RRset was returned in the response. See RFC 4035, Sec. 3.1.1. (216.218.130.2, 216.218.131.2, 216.218.132.2, 2001:470:100::2, 2001:470:200::2, 2001:470:300::2, UDP_-_EDNS0_4096_D_KN)
+
+ As of 2015-01-11, I read the [PowerDNS
+ docs](https://doc.powerdns.com/authoritative/dnssec/migration.html#from-existing-dnssec-non-powerdns-setups-pre-signed)
+ again. Specifically, I need to set `pdnsutil set-nsec3 $zone <whatever
+ NSEC3PARAM says>`. Then, PowerDNS gives back SERVFAILs, because, of course,
+ PowerDNS can't sign the reply, because it doesn't have the keys to sign the
+ NSEC3 replies (??? I guess ???). So now, instead of sending back an unsigned
+ reply, it sends back an error.
+
+ I imported the RRSIGs for these NSEC3 records, though, at least in a second
+ attempt. I tried all combinations of `(set-nsec3|unset-nsec3)` and `(import
+ NSEC3-RRSIG|don't import NSEC3-RRSIG)`. All variants failed. I have no clue
+ how this is supposed to work cleanly. [^7]
+
+ **Update 2025-01-15**: The GitHub issue linked below (#9263) actually
+ contains a "solution"/workaround for this. The order of commands needs to be
+ 1) Import, 2) set-nsec3, 3) rectify, 4) set-presigned.
+
+1. Wait at least 24 hours (TTLs, DNS propagation time).
+1. Let PowerDNS output its own keys it generated for the zone. Unfortunately,
+ `pdnsutil export-zone-dnskey $zone $keynr` *does not output a completely
+ valid record*, nor does `pdnsutil export-zone-ds $zone`. These outputs are
+ missing the TTLs in the second column, at least with PowerDNS 4.7.3 in the
+ Debian stable repos. You have to add those yourself. I saw some tools like
+ dnsviz break when you enter the records as-is.
+
+ **Important**, I had to temporarily run `unset-presigned $zone` (see
+ below) before running `pdnsutil export-zone-ds`, so PowerDNS actually outputs
+ the hash of the new key(s)! Otherwise it will only show the current keys
+ signature. Otherwise, you may also use online tools that convert the DNSKEY
+ to a DS record.
+1. Have a copy of the *current* DS / DNSKEY records as well (`dig` is your
+ friend).
+1. Set the DNSSEC from "auto" to "manual" in the INWX web interface.
+ * Delete all keys from the domain.
+ * Add DNSSEC again for the domain, in manual mode.
+ * Add DNSKEY and DS records of the old keys.
+ * Add key, insert DNSKEY and DS records emitted by pdnsutil for the new
+ key.
+
+ As far as I know, this will be sent to the parent zone. [^8]
+1. After I got an email from INWX confirming the new DNSSEC entries, I ran
+ dnsviz again and nearly got an heart attack, because there were a lot of
+ errors and red exclamation marks. Actually, the DNSSEC was still valid (i.e.
+ not returning bogus records). The tool only complained about
+
+ The DS RRset for the zone included algorithm 13 (ECDSAP256SHA256), but no
+ RRSIG with algorithm 13 covering the RRset was returned in the response.
+
+ I *think* dnsviz expects both `RRSIG`s, i.e. both keys must sign the replies?
+ That shouldn't have an effect on the reachability, otherwise the tool would
+ report BOGUS records?
+
+ **Update 2025-01-15**: Yep. As stated in the IETF document linked in the
+ references:
+
+ > Current reading is that an algorithm rollover requires a full validation
+ > with all algorithms involved, whereas a key rollover will work whenever
+ > data can be validated using either key ([RFC4035], section 2.2).
+
+ N.B.: So if you *really* want to avoid that, you need to tell PowerDNS to
+ create an KSK/ZSK pair (with the same parameters?) and use these, and only
+ later switch over to algorithm 13.
+1. Wait at least 24 hours (TTLs, DNS propagation time). - Depending on the
+ previous DS records TTL.
+1. Switch to automatic signing by PowerDNS. I guess at this point you must or should
+ * stop PowerDNS, [^5]
+ * `pdnsutil unset-presigned $zone`,
+ * `pdnsutil unset-nsec3 $zone`,
+ * edit the zone (clearing out the RRSIGs you imported. If I understood
+ the PowerDNS docs correctly, there will be trouble [^6] if you don't. Oh,
+ don't forget the NSEC3PARAM record, any leftover (C)DNSKEYS and (C)DS,
+ while also increasing the serial (otherwise the AXFR might not take
+ place), [^9]
+ * `pdnsutil rectify-zone $zone`
+ * start PowerDNS,
+ * (optional) notify the secondaries
+1. At this point, you should be done. And I *guess* at this point you could
+ already delete the other DS/DNSKEY via the registrar?
+
+By the way, I'll choose to use NSEC instead of NSEC3. NSEC3 seems like too much
+of a headache for me, also, domain names are not secrets.
+
+## Further references
+
+* [PowerDNS docs](https://doc.powerdns.com/authoritative/)
+* IETF Draft: [Changing DNS Operators for DNSSEC signed
+ Zones](https://datatracker.ietf.org/doc/html/draft-koch-dnsop-dnssec-operator-change-06)
+* [DNSSEC visualizer](https://dnsviz.net/)
+* [Verisign Labs DNSSEC debugger](https://dnssec-debugger.verisignlabs.com/)
+
+## Footnotes
+
+[^1]: Okay, it's not like I edit my records that often.
+
+[^2]:
+ For the purposes of this blog article, I think you can freely substitute
+ zone with domain in your head, I probably won't use the words correctly 100% of
+ the time either.
+
+[^4]:
+ I think that shouldn't to any damage. DNS recursion should always ask the parent
+ zone?
+
+[^5]:
+ Avoiding a spurious notify or AXFR directly after editing? No idea if needed.
+
+[^6]: such as duplicate RRSIG replies.
+
+[^7]:
+ There are two GitHub issues similar to this:
+ [#9263](https://github.com/PowerDNS/pdns/issues/9263) and
+ [#8892](https://github.com/PowerDNS/pdns/issues/8892).
+
+[^8]:
+ Actually, I have no idea why INWX wants me to enter both the DS and the
+ DNSKEY. Only the DS gets entered in the DENIC servers.
+
+[^9]:
+ e.g. `:g/\<\(RRSIG\|NSEC3PARAM\|DS\|CDS\|DNSKEY\|CDNSKEY\)\>/d` in vim.
+<!-- vim: set ft=markdown tw=80 ai tabstop=4 shiftwidth=4 expandtab: -->
+
diff --git a/_posts/2025-02-02-dn42-put-it-in-a-box-linux-network-namespace.md b/_posts/2025-02-02-dn42-put-it-in-a-box-linux-network-namespace.md
new file mode 100644
index 0000000..3a43f1a
--- /dev/null
+++ b/_posts/2025-02-02-dn42-put-it-in-a-box-linux-network-namespace.md
@@ -0,0 +1,134 @@
+---
+layout: post
+title: 'DN42: Put it in a box (Linux network namespace)'
+date: 2025-02-02 17:10 +0100
+last_modified_at: 2025-06-18 19:12 +0200
+lang: "en"
+categories: "tech"
+description: "I explain how I put my Autonomous System in a network namespace."
+---
+
+What did I do the previous week? What robbed me of my sleep at night, keeping my
+thoughts churning? Let's look at "doing containers the hard way".
+
+For some time, I had CPU usage problems on my VPS. Looking at `htop`, I always
+saw two bird instances and dnsmasq as "offenders". (I run both my clearnet AS as
+well as my DN42 AS on this box) [^1]. I was originally going to write a blog article
+about that, but it's still in my drafts folder. Here's the gist:
+
+- All these processes receive "netlink messages".
+- Probably the IBGP session for my AS generates lots of these netlink messages.
+- The other bird process (for DN42) and dnsmasq suffer from it.
+
+One possible solution is to put the processes into a container (Docker, Podman,
+LXC, LXD, Incus, …). However, I'm on a budget. I have only 40 GB of storage on
+my Hetzner VPS and don't want to waste it with duplicate file systems. However, I
+can use the *technology* these container (runtimes/engines) use: Linux
+namespaces.
+
+I won't cover the basics here. There are other blog articles from other people
+who are *far better* at explaining that (e.g. from
+[anracon](https://linux-blog.anracom.com/2017/10/30/fun-with-veth-devices-linux-bridges-and-vlans-in-unnamed-linux-network-namespaces-i/)).
+
+Suffice to say, I can put DN42 in a box, which put the network in isolation. To
+a degree. I still need to communicate with the outside world. I dumped my setup
+[in a git repo](https://git.uvok.de/ansible/tree/roles/linux-ns/files).
+
+[jamesits](https://github.com/Jamesits/systemd-named-netns) provides some nice
+templates for setting up a network namespace. I used that template, but tweaked
+some parts to they suit me better [^2]. Also, using the Debian systemd files, I
+manually created new ones and made sure the processes use the separate namespace.
+I admit to never having particularly liked systemd, but at least the service
+manager is pretty nice.
+
+I also felt pretty clever coming up with [this setup
+script](https://git.uvok.de/ansible/tree/roles/linux-ns/files/usrlocalbin/dn42-route-namespace.sh)
+which gets called by systemd and then calls itself again, but in the newly created
+namespace.
+
+Inside the namespace run:
+
+- The Wireguard interfaces to my peers,
+- Tinc (which I only have because I want to have a broadcast-capable VPN),
+- PowerDNS (a separate instance from "the outer VPS one", serving my DN42 domain),
+- BIRD2,
+- And a looking glass
+
+*Outside* the namespace run:
+
+- dnsmasq, which allows me to resolve my DN42 domains "from the clearnet"
+ (from within a Wireguard net)
+- Nginx, which serves my DN42 website
+
+**Update 2025-06-18**: nginx now runs inside the namespace as well.
+
+It took me a while and some internet searches to come up with the firewall
+rules. On my VPS itself I use ufw, for the network namespace, I *could probably*
+make this work as well, but I decided to use "iptables", or rather, the wrapper
+scripts which provide the same syntax but use nftables in the background. This
+is because with this namespace separation, I don't have to worry about
+"cross-leaking" network traffic between DN42 and the clearnet. So the rules get
+a lot simpler
+
+I was happy that both DNS and the website work, when I realized that the default
+policy for FORWARD was ALLOW. I found it suitable to set it to DROP, and
+suddenly, it didn't work anymore, because I was employing destination NAT:
+
+```
+*nat
+-A PREROUTING -d fd3e:bc05:2d6::80/128 -p tcp --dport 80 -j DNAT --to-destination fcee::1
+-A PREROUTING -d fd3e:bc05:2d6::80/128 -p tcp --dport 443 -j DNAT --to-destination fcee::1
+```
+
+This is because the webserver is running on the "outer" VPS, not in the network
+namespace (hey, it took me long enough to finish this up as it is). This is
+probably a relatively small adjustment.
+
+With an additional forward rule, everything is happy again:
+
+```
+*filter
+-A FORWARD -s fd00::/8 -d fcee::1/128 -j ACCEPT
+-A FORWARD -s fcee::1/128 -d fd00::/8 -j ACCEPT
+```
+
+**Update 2025-06-18**: I modified this several times since then.
+Since I also need to take care of actual routing/forwarding within DN42,
+I now ended up with
+
+```
+-A FORWARD -s fd00::/8 -d fd00::/8 -j ACCEPT
+-A FORWARD -i eth0 -d fd00::/8 -j ACCEPT
+-A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
+```
+
+i.e. I don't rely on the source address being fcee::1 anymore.
+
+Also, I also feel pretty clever for making sure I can access DN42 from my
+clearnet:
+
+```
+*mangle
+-A PREROUTING -i eth0 -j MARK --set-mark 0x4242
+COMMIT
+
+*nat
+-A POSTROUTING -d fd00::/8 -m mark --mark 0x4242 -j MASQUERADE
+COMMIT
+```
+
+I am actually not sure if the latter needs an "-i (all wg interfaces)" or "! -i
+(internal interfaces)" to avoid unnecessary NATting for when I access a service
+in the namespace. But nevertheless, I achieved a working state. Best of all, I
+can reuse some of the work if I ever want to put my clearnet AS in a namespace,
+too.
+
+[^1]:
+ An AS is in ["autonomous system"]({% post_url
+ 2023-08-18-networking-adventure-my-own-ipv6-prefix-and-as %}), [DN42]({% link
+ dn42.md %}) is a "lab environment" for networking.
+
+[^2]:
+ For example, I have no idea why they unmount and mount the netns path
+ manually. Probably this fixes some bug of an old systemd version?
+
diff --git a/_posts/2025-03-06-monitoring-with-uptime-kuma.md b/_posts/2025-03-06-monitoring-with-uptime-kuma.md
new file mode 100644
index 0000000..0d1a6d1
--- /dev/null
+++ b/_posts/2025-03-06-monitoring-with-uptime-kuma.md
@@ -0,0 +1,49 @@
+---
+layout: post
+title: Monitoring with Uptime Kuma
+date: 2025-03-06 18:55 +0100
+lang: en
+description: "A short praise for Uptime Kuma, offering uptime monitoring."
+---
+
+[Recently](https://furry.engineer/@uvok/114026509150081909), I wanted to do add
+some kind of monitoring to my servers. The prevalent reason was monitoring my
+AS, which I accidentally broke by shutting down one server, or the hosting
+provider had an outage (This was because I didn't configure a fallback default
+route, but that's another story).
+
+So, I took a look at the monitoring landscape. And immediately wanted to drop
+the idea again, because there were too many choices. And had no intention to
+install and test them all, and no energy to read dozens of websites with user
+reports.
+
+But then I said, "wait. Let's not do it the overthinking way again". So I took
+the first best/simple thing and chose [Uptime
+Kuma](https://github.com/louislam/uptime-kuma). I actually wanted to drop it
+right away once I saw it's running on NodeJS (ehhh). But then I decided to spin
+up an LXC on my Proxmox anyway.
+
+And lo and behold, it works:
+
+{% image
+img="https://pics.uvokchee.de/_data/i/upload/2025/03/06/20250306175521-4763fdf7-me.png"
+alt="Screenshot of Uptime Kuma - showing the ping times of various servers."
+%}
+
+And it even has a certificate validity check.
+
+{% image
+img="https://pics.uvokchee.de/_data/i/upload/2025/03/06/20250306175521-7805c0bb-me.png"
+alt="Screenshot of Uptime Kuma - showing the HTTPS monitor, including the
+remaining certificate validity time."
+%}
+
+
+It runs in my home network, so I can actually detect whether my AS is reachable
+(to a degree). And it brings various notification options, from which I chose
+Telegram and Matrix.
+
+I actually got an outage notification this week, on my Hetzner servers. No idea
+what happened there, though, I didn't receive an outage report / notification
+from them.
+
diff --git a/_posts/2025-03-18-games-i-play_ed.md b/_posts/2025-03-18-games-i-play_ed.md
new file mode 100644
index 0000000..9e2c805
--- /dev/null
+++ b/_posts/2025-03-18-games-i-play_ed.md
@@ -0,0 +1,112 @@
+---
+layout: post
+title: Computer games I (used to) play
+lang: en
+categories: game
+date: 2025-03-18 10:17 +0100
+description: "I muse over computer games I played recently(-ish)."
+---
+
+I'm not big on computer games. Usually "I don't have the time" to deal with
+them. Or rather, spending so much time on the computer at work, I'm quickly fed
+up with sitting on the computer. Also, I get frustrated quickly at times, when
+I can't make progress or repeatedly fail. So I usually only play computer games
+on vacation.
+
+So, as for games I played (recently-ish)? Roughly sorted by time last played
+ascending:
+
+- Waaaaaaaaaaaaaaay back in my childhood (in no specific order):
+ - [Civilization](https://archive.org/details/msdos_sid_meier_civilization)
+ [^civ1], [^civ2]. Yes, the original one! If I remember correctly, I always
+ got destroyed sooner or later… - [FreeCiv](https://www.freeciv.org/) is an
+ open source game inspired by it.
+ - [3D Pinball Space Cadet](https://alula.github.io/SpaceCadetPinball/). I
+ spent hours on that one. Never quite succeeding on hitting the targets…
+ - Anno 1602. I think the first game I searched for cheats to improve my
+ chances… - [Unknown Horizons](https://unknown-horizons.org) is a similar
+ game in open source. [^unkhor]
+ - SimCity 2000 [^simcit] -
+ [LinCity-NG](https://github.com/lincity-ng/lincity-ng) is a similar open
+ source game.
+ - Need For Speed III
+ - Empire Earth (quite like Age Of Empires, as I found out later) - As far as
+ open source goes, [0 A.D.](https://play0ad.com/) is probably the closest?
+ - "Die Völker" (apparently, "Alien Nations" in English, "The Nations" was
+ actually the sequel. Yeah, what?). I sunk quite some time into that one as
+ well.
+ - "Funk-Flitzer", which was on a "Mickey Maus" comic magazine. Go search for
+ "3-D Ultra Radio Control Racers".
+ - Worms 2 - [Hedgewars](http://www.hedgewars.org/) seems to be the open source
+ alternative.
+ - Moorhuhn, Winter Edition
+- Sometime later deep in the past: Minecraft (I stopped way before Microsoft
+ acquired it), recently followed by [Minetest, or rather,
+ Luanti](https://www.luanti.org/) (Though, "it's just not the same anymore".
+ I'm not sure what exactly it is, maybe my changed mindset, maybe I liked the
+ original soundtrack and game elements more…).
+- [Starbound](https://playstarbound.com/), very shortly
+- [OpenTTD](https://www.openttd.org/): Trains and buses! Though, admittedly, I
+ never quite "got it" (why do you have to manually "park" passengers on train
+ stations, instead of unloading them completely, so they continue taking the
+ train?)
+- [Dwarf Fortress](https://www.bay12games.com/dwarves/). Go with
+ [LNP](https://dwarffortresswiki.org/index.php/Utility:Lazy_Newb_Pack) if you
+ wanna give it a try, or the Steam version.
+- [Stardew Valley](https://www.stardewvalley.net/) (on Android tablet)
+- [Factorio](https://www.factorio.com/) - Shaking my head over the people
+ arguing over how to perfectly optimize everything ;). I disabled biters in
+ recent play-throughs, it got to annoying for me.
+- [Terraria](https://terraria.org/). Another one where I had to resort to the
+ Wiki for the bosses.
+- [Cattails: Wildwood Story](https://cattailsgame.com/). Got through the main
+ story, had to resort to YouTube for looking up how to beat the final boss.
+- [Turing Complete](https://turingcomplete.game/)
+- And, the most recent one,
+ [dotAge](https://www.michelepirovano.com/dotage.html)
+
+The latter [^dotnote1] one is a "turn-based village builder", with challenges. I
+*guess* I can *kinda* compare it to Dwarf Fortress, as in, it can be incredibly
+hard, and you have to build a civilization [^dotdwarf] and take care of it, as
+in terms of food and drink, for example. Only you don't have to deal with goblin
+or forgotten beast invasion, but, basically, nature. And DF doesn't have tech
+research… Okay, maybe they're not comparable after all.
+
+The first play-throughs are rather rough, since you will only unlock additional
+content, such as farming, with play time. So your first villages will cease to
+exist quite quickly, I think. That's expected. Probably to show you the failure
+modes?
+
+From personal experience: Don't build some new buildings too quickly. The game
+allows you to build them even though they're "useless" at that specific point in
+the game, and you need additional tech research to make them usable.
+
+{% linked_image
+img="https://pics.uvokchee.de/_data/i/upload/2025/03/17/20250317185230-40c102c2-me.jpg"
+url="https://pics.uvokchee.de/upload/2025/03/17/20250317185230-40c102c2.jpg"
+alt="Screenshot of the game dotAge, of fairly early game play."
+%}
+
+[^dotnote1]:
+ Yes, kinda unfair I only go into detail into one. I wanted to start
+ "reviewing" only this one, but then thought, "well, might as well list all
+ games I played so far". Maybe that's a chance for future blog posts.
+
+[^simcit]:
+ I didn't like SimCity 3000 as much. And *don't get me started with Cities:
+ Skylines*.
+
+[^dotdwarf]:
+ General advice seems to be not to go over 50 in dotAge. In Dwarf Fortress,
+ you have to deal with the pesky Nobles instead. Something similar seems to
+ exist in dotAge as well?
+
+[^civ1]: Go drop archive.org a small donation, won't you?
+
+[^civ2]: You can actually still play it thanks to [DOSBox](https://www.dosbox.com/)
+
+[^unkhor]:
+ Not sure about the current state. The port to the Godot
+ engine seems still to be a WIP. Last blog post is quite some time ago, but
+ the [GitHub repo](https://github.com/unknown-horizons/godot-port) is active.
+
diff --git a/_posts/2025-04-05-random-thought-smartphones-with-old-technology.md b/_posts/2025-04-05-random-thought-smartphones-with-old-technology.md
new file mode 100644
index 0000000..4649817
--- /dev/null
+++ b/_posts/2025-04-05-random-thought-smartphones-with-old-technology.md
@@ -0,0 +1,20 @@
+---
+layout: post
+title: 'Random Thought: Smartphones with old technology'
+date: 2025-04-05 20:23 +0200
+lang: en
+categories: tech
+---
+
+How would smartphones look with old technology? What if we never invented flash
+storage? (Leave aside the answer "the smartphone would not have been
+possible"). Like, what if they were using tapes / tape drives as primary
+storage? Or floppy disks?
+
+&lt;insert AI generated image here - hah, you wish. I'll have none of that on my
+blog.&gt;
+
+This thought was presented to me while listening to the ["Stay
+Forever"](https://www.stayforever.de/) podcast, a German-language podcasts about
+old games and their technology.
+
diff --git a/_posts/2025-04-06-i-don-t-get-kubernetes.md b/_posts/2025-04-06-i-don-t-get-kubernetes.md
new file mode 100644
index 0000000..28224b4
--- /dev/null
+++ b/_posts/2025-04-06-i-don-t-get-kubernetes.md
@@ -0,0 +1,38 @@
+---
+layout: post
+title: I don't get Kubernetes
+date: 2025-04-06 15:59 +0200
+categories: tech
+lang: en
+---
+
+No real content, just me ranting.
+
+There are [some ;)](https://xeiaso.net/talks/2025/surreal-joy-homelab/) who take
+great joy in Kubernetes. I never really understood how I could use it.
+
+Okay, so, very simplified, K8s let's you say "I wanna run these services (Docker
+containers) on my cluster (various computers/servers), optionally with
+redundancy"? Well, I only have one "server" in my "homelab", and then 3 VPS or
+so. Only one of them runs something in Docker/Podman.
+
+I *can't imagine*/don't understand how I would run, say, Nextcloud in K8s. I
+*want* to know which server is currently running it, and where the data is
+stored. Especially with something like Nextcloud, I want to have the Nextcloud
+application, the database, and the data store on **one** VPS, without any
+indirections, without any communication over the internet or a VPN, via some
+proxy software. From what little I've heard about K8s (forgive me my ignorance),
+I had more questions that I could answer. K8s seems really complex, because it
+~~has~~ can have so many moving parts (yes, you can probably run a very basic
+setup, most of the components seem optional). I don't want my database suddenly
+moved around to somewhere else.
+
+That is to say: I never had a use case of trying out K8s. No *actual, real* one.
+I could probably construct one if I tried really hard. But, eh. "My current
+setup works".
+
+My attempt to run K8s on my Proxmox via LXC all failed, even if they're
+privileged containers, probably because I need to mount Linux Namespaces, or
+load specific kernel modules on Proxmox itself so they can be loaded in the LXC
+containers as well (???).
+
diff --git a/_posts/2025-05-11-table-tennis.md b/_posts/2025-05-11-table-tennis.md
new file mode 100644
index 0000000..8d9ce82
--- /dev/null
+++ b/_posts/2025-05-11-table-tennis.md
@@ -0,0 +1,14 @@
+---
+layout: post
+title: Table Tennis!
+date: 2025-05-11 08:16 +0200
+lang: en
+categories: life
+---
+
+Recently, I was at a seminar. I learned a lot. It was an exquisite group of inquisitive people.
+But we also had some free time in which we played some table tennis.
+I have no idea how to play it besides "hit the ball with the racket".
+Actually makes me want to join the local sports club for once, which does have a
+table tennis department.
+
diff --git a/_posts/2025-05-25-cpu-usage-problems-again.md b/_posts/2025-05-25-cpu-usage-problems-again.md
new file mode 100644
index 0000000..beb905a
--- /dev/null
+++ b/_posts/2025-05-25-cpu-usage-problems-again.md
@@ -0,0 +1,16 @@
+---
+layout: post
+title: CPU usage problems on my VPS again
+date: 2025-05-25 19:29 +0200
+lang: en
+categories: tech
+---
+
+<a href="{% post_url 2025-02-02-dn42-put-it-in-a-box-linux-network-namespace
+%}">As reported previously</a>, I had problems with the CPU usage on my VPS
+again, even after namespace isolation. This time, it was dnsmasq causing the
+loadavg to rise. "This is a problem for future me to solve properly", is the
+motto. For the time being, I simply added a service restart when I detect a high
+loadavg. Works well enough. I'll keep monitoring it to see if this occurs more
+than once a week or so.
+
diff --git a/_posts/2025-05-29-furdew-valley.md b/_posts/2025-05-29-furdew-valley.md
new file mode 100644
index 0000000..cbdf420
--- /dev/null
+++ b/_posts/2025-05-29-furdew-valley.md
@@ -0,0 +1,24 @@
+---
+layout: post
+title: Furdew Valley
+date: 2025-05-29 15:21 +0200
+lang: en
+categories: gaming
+---
+
+[In my last blog post]({% post_url 2025-03-18-games-i-play_ed %}) about gaming,
+I think I already mentioned I played Stardew Valley. Back then, on an Android
+tablet. Due to [Pan on Mastodon](https://woof.tech/@Pan_H_Shiroi@toot.cat)
+streaming that game recently, I picked it up again.
+
+I found it can even be modded! The mod in question is
+[Animals and Myths Pack](https://www.moddrop.com/stardew-valley/mods/1119098-fs-shs-animals-and-myths-pack).
+
+{% image
+ img="https://pics.uvokchee.de/upload/2025/05/29/20250529132445-6dbbe14d.jpg"
+ alt="Screenshot of modded Stardew Valley, showing the player character with anthro legs
+ and a fox tail"
+%}
+
+That adds to the fun. I might even get a mod that adds NPC lines. The build-in
+ones can get monotonous after some time.
diff --git a/_posts/2025-06-16-oops-dn42-stops-working.md b/_posts/2025-06-16-oops-dn42-stops-working.md
new file mode 100644
index 0000000..26d4ed7
--- /dev/null
+++ b/_posts/2025-06-16-oops-dn42-stops-working.md
@@ -0,0 +1,73 @@
+---
+layout: post
+title: Oops, DN42 stopped working
+date: 2025-06-16 20:39 +0200
+last_modified_at: 2025-06-17 18:12 +0200
+lang: en
+categories: tech
+---
+
+As you might know, I participate in [DN42]({% link dn42.md %}).
+I have a somewhat peculiar setup, in
+which some VPS run the routing daemons, and my home router simply has a VPN
+connection to them and statically routes everything `fd00::/8` to them. The router
+runs OpenWRT, and has dnsmasq setup to resolve DN42 domains via the DN42 anycast
+servers. I set this up *months ago*, it worked, I was happy, and didn't need it
+since.
+
+Cue last weekend. "Suddenly" the resolution didn't work anymore. It simply timed
+out. So I connected to my VPS (which is running DN42
+[in a namespace]({%post_url 2025-02-02-dn42-put-it-in-a-box-linux-network-namespace %}))
+and took a look at tcpdump. Inside the namespace, I saw the strange lines
+
+```
+18:49:05.296629 eth0 In IP6 fd7a:115c:a1e0::xxx > fd42:d42:d42:53::1.53: 42631+ [1au] AAAA? wiki.dn42. (50)
+18:49:05.296679 kioubit Out IP6 fd3e:bc05:2d6::80.50255 > fd42:d42:d42:53::1.53: 42631+ [1au] AAAA? wiki.dn42. (50)
+18:49:05.302946 tinc_dn42 In IP6 fd42:d42:d42:53::1.53 > fd3e:bc05:2d6::80.50255: 42631 1/0/1 AAAA fd42:d42:d42:80::1 (66)
+18:49:05.302990 kioubit Out IP6 fd3e:bc05:2d6::80 > fd42:d42:d42:53::1: ICMP6, destination unreachable, unreachable route fd3e:bc05:2d6::80, length 122
+```
+
+And all the time I was thinking… "huh??? Why is `fd3e:bc05:2d6::80` unreachable???
+It is **clearly** in the `ip -6 a` output!!!". I looked through all the
+iptables statistics and couldn't find the culprit.
+
+A join in the DN42 IRC and some back and forth later, someone suggested
+"Hey, what's with the `fd7a:…` address? Is there a route for *that*?".
+
+And of course, no, it wasn't! I was so focused on the ICMP6 message that I
+didn't notice the incoming line. As you can read in my other article linked
+above, I perform NAT. Of course in that case it probably wouldn't make sense for the
+ICMP6 message to tell someone that there's no route for the original IP (before NAT).
+
+**So, but… where does the `fd7a:…` address come from?**<br/>
+The answer is [Tailscale]({% post_url 2024-12-08-trying-out-tailscale %}).
+Unfortunately, they decided to use the `fd00::/8` IP range, which collides with DN42.
+I didn't do any DN42 stuff since installing it, so I didn't notice that.<br/>
+**But… why is that address used at all for the DNS request?**<br/>
+Weeeeell… I found out as well. It comes from OpenWRT. I simply set up
+a static route there, and Linux does its best to determine the source address for the
+DNS request. And it seems the Tailscale one was a closer match than
+the address from my own DN42 prefix.<br/>
+**So, how to fix that?**<br/>
+[It's not as easy as you think!](https://github.com/openwrt/openwrt/issues/13720).
+Merely using the "source" option in the config file would work for IPv4, but
+for IPv6 it has a different meaning!<br/>
+**But why does it say "no route to host" instead of using a default route?**<br/>
+Because I configured bird to insert an "unreachable" route for the `fd::/8` prefix
+to avoid leaking traffic.
+```
+# ip -6 route show fd00::/8 table dn42
+unreachable fd00::/8 dev lo proto bird src fd3e:bc05:2d6::1 metric 500 pref medium
+```
+
+At this point, it's Monday evening. Unnerved, I threw my hands in the air and
+simply put a line of
+```
+ip -6 route replace fd00::/8 ... src <my-openwrt-dn42-address>
+```
+in `/etc/rc.local` and called it a day. My setup
+works again, and I learned something again. Don't just look at the last line,
+look at the whole picture. A lesson I actually already learned in the
+ubuntuusers forums when asking for help compiling a package and only posted the
+last few make output lines, which of course didn't contain the actual
+compilation error…
diff --git a/_posts/2025-06-22-blog-now-on-gemini-with-dn42.md b/_posts/2025-06-22-blog-now-on-gemini-with-dn42.md
new file mode 100644
index 0000000..fa46a58
--- /dev/null
+++ b/_posts/2025-06-22-blog-now-on-gemini-with-dn42.md
@@ -0,0 +1,39 @@
+---
+layout: post
+title: Blog now on Gemini - with DN42
+date: 2025-06-22 17:42 +0200
+---
+
+The Gemini protocol is a minimal protocol to serve content, and is
+the "spiritual successor" to the Gopher protocol.
+(Do not confuse with Google's AI Gemini. I hate naming collissions),
+
+It took some tinkering, especially because Gemini's markup language
+("Gemtext")
+is deliberately
+minimal, and Jekyll can't output/convert Markdown to Gemini, so I had to
+take "the ugly route" to convert the "finished" Jekyll HTML page to Gemtext
+via the below-mentioned tool, which basically does
+HTML > Markdown > Gemtext.
+I can't directly use the raw Markdown files of my blog, since I use
+Liquid tags.
+
+Anyway, my blog is now accessible in "Geminispace", alas, only within DN42,
+because I'm too lazy right now to do it properly. (With the DN42 namespace
+isolation and all). And also, it's a really "nerdy" protocol, with I don't
+know how many users.
+
+`gemini://uvok.dn42/`
+
+Addendum: What I really like is the "feed" specification, which basically consists
+of separate lines of <br/> `=> $link $date $heading`, <br/> and browsers like
+Lagrange can subscribe
+to these (like RSS feeds, but the format is much simpler!).
+
+- [Gemini Protocol on Wikipedia](https://en.wikipedia.org/wiki/Gemini_(protocol))
+- [Gopher Protocol on Wikipedia](https://en.wikipedia.org/wiki/Gopher_(protocol))
+- [Gemini Protocol homepage](https://geminiprotocol.net/)
+- [Gemini "Quickstart"](https://geminiquickst.art/)
+- [Gemtext intro](https://gemini.flounder.online/docs/gemtext.gmi)
+- [Html2Gemini](https://github.com/Aarontheissueguy/Html2GeminiPy)
+- [Lagrange browser](https://git.skyjake.fi/gemini/lagrange)
diff --git a/assets/main.scss b/assets/main.scss
index ff45870..4ff4a71 100644
--- a/assets/main.scss
+++ b/assets/main.scss
@@ -53,7 +53,6 @@ a:visited {
background: #eeeeee;
margin-bottom: 1em;
position: relative;
- z-index: -1;
}
.banner-close-button {
@@ -67,3 +66,8 @@ hr.postsep {
margin: auto;
margin-bottom: 1em;
}
+
+/* site nav above news items */
+.site-nav {
+ z-index: 10;
+}
diff --git a/assets/mscomm/comments.js b/assets/mscomm/comments.js
new file mode 100644
index 0000000..57b9793
--- /dev/null
+++ b/assets/mscomm/comments.js
@@ -0,0 +1,311 @@
+// © https://phosphoricons.com/
+export const icons = {
+ reblog:
+ `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 256 256" fill="none" stroke="currentColor" stroke-width="24"><polyline points="200 88 224 64 200 40"/><path d="M32,128A64,64,0,0,1,96,64H224"/><polyline points="56 168 32 192 56 216"/><path d="M224,128a64,64,0,0,1-64,64H32"/></svg>`,
+ favourite:
+ `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 256 256" fill="currentColor"><path d="M240,94c0,70-103.79,126.66-108.21,129a8,8,0,0,1-7.58,0C119.79,220.66,16,164,16,94A62.07,62.07,0,0,1,78,32c20.65,0,38.73,8.88,50,23.89C139.27,40.88,157.35,32,178,32A62.07,62.07,0,0,1,240,94Z"/></svg>`,
+ author:
+ `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 256 256" fill="currentColor" class="comment-author"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm45.66,85.66-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32Z"></path></svg>`,
+
+ // @ https://simpleicons.org/
+ mastodon:
+ `<svg role="img" width="16" height="16" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Mastodon</title><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>`,
+
+ pleroma:
+ `<svg role="img" width="16" height="16" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Pleroma</title><path d="M6.36 0A1.868 1.868 0 004.49 1.868V24h5.964V0zm7.113 0v12h4.168a1.868 1.868 0 001.868-1.868V0zm0 18.036V24h4.168a1.868 1.868 0 001.868-1.868v-4.096Z"/></svg>`,
+
+ bluesky:
+ `<svg role="img" width="16" height="16" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Bluesky</title><path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z"/></svg>`,
+};
+
+export default class SocialComments extends HTMLElement {
+ comments = {};
+
+ async connectedCallback() {
+ const lang = this.closest("[lang]")?.lang || navigator.language || "en";
+
+ this.dateTimeFormatter = new Intl.DateTimeFormat(lang, {
+ dateStyle: "medium",
+ timeStyle: "short",
+ });
+
+ const mastodon = this.getAttribute("mastodon") || this.getAttribute("src");
+ const bluesky = this.getAttribute("bluesky");
+
+ await Promise.all([
+ mastodon && this.#fetchMastodon(new URL(mastodon)),
+ bluesky && this.#fetchBluesky(new URL(bluesky)),
+ ]);
+
+ this.refresh();
+ }
+
+ refresh() {
+ const comments = [
+ ...this.comments.mastodon || [],
+ ...this.comments.bluesky || [],
+ ].sort(
+ (a, b) => new Date(a.createdAt) - new Date(b.createdAt),
+ );
+
+ if (comments.length) {
+ this.innerHTML = "";
+ this.render(this, comments);
+ }
+ }
+
+ async #fetchBluesky(url) {
+ const { pathname } = url;
+
+ const [, handle, rkey] = pathname.match(
+ /\/profile\/([\w\.]+)\/post\/(\w+)/,
+ );
+
+ if (!handle || !rkey) {
+ return;
+ }
+
+ const options = {
+ ttl: Number(this.getAttribute("cache") || 0),
+ };
+
+ const didData = await fetchJSON(
+ `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${handle}`,
+ options,
+ );
+ const uri = `at://${didData.did}/app.bsky.feed.post/${rkey}`;
+
+ this.comments.bluesky = dataFromBluesky(
+ await fetchJSON(
+ `https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${uri}`,
+ options,
+ ),
+ );
+ }
+
+ async #fetchMastodon(url) {
+ const { origin, pathname } = url;
+ let id;
+
+ const source = pathname.includes("/notice/") ? "pleroma" : "mastodon";
+
+ if (source === "pleroma") {
+ [, id] = pathname.match(/^\/notice\/([^\/?#]+)/);
+ } else {
+ [, id] = pathname.match(/\/(\d+)$/);
+ }
+
+ if (!id) {
+ return;
+ }
+
+ const token = this.getAttribute("token");
+ const options = {
+ ttl: Number(this.getAttribute("cache") || 0),
+ };
+ if (token) {
+ options.headers = {
+ Authorization: `Bearer ${token}`,
+ };
+ }
+
+ const user = url.pathname.split("/")[1];
+ const author = `${user}@${url.hostname}`;
+
+ const comments = dataFromMastodon(
+ await fetchJSON(
+ new URL(`${origin}/api/v1/statuses/${id}/context`),
+ options,
+ ),
+ author,
+ source,
+ );
+
+ this.comments.mastodon = comments.filter((comment) =>
+ comment.parent === id
+ );
+ }
+
+ render(container, replies) {
+ const ul = document.createElement("ul");
+
+ for (const reply of replies) {
+ const comment = document.createElement("li");
+ comment.innerHTML = this.renderComment(reply);
+
+ if (reply.replies.length) {
+ this.render(comment, reply.replies);
+ }
+ ul.appendChild(comment);
+ }
+
+ container.appendChild(ul);
+ }
+
+ renderComment(comment) {
+ return `
+ <article class="comment" id="comment-${comment.id}">
+ <footer class="comment-footer">
+ <a href="${comment.author.url}" class="comment-user">
+ <img class="comment-avatar" src="${comment.author.avatar}" alt="${comment.author.alt}'s avatar" width="200" height="200">
+ ${comment.isMine ? icons.author : ""}
+ <strong class="comment-username">
+ ${comment.author.name}
+ </strong>
+ <em class="comment-useraddress">${comment.author.handler}</em>
+ </a>
+ <a href="${comment.url}" class="comment-address">
+ <time class="comment-time" title="${comment.createdAt.toISOString()}">
+ ${this.dateTimeFormatter.format(comment.createdAt)}
+ ${icons[comment.source]}
+ </time>
+ </a>
+ </footer>
+ <div class="comment-body">
+ ${comment.content}
+
+ <p class="comment-counts">
+ ${
+ comment.boosts ? `<span>${icons.reblog} ${comment.boosts}</span>` : ""
+ }
+ ${
+ comment.likes ? `<span>${icons.favourite} ${comment.likes}</span>` : ""
+ }
+ </p>
+ </div>
+ </article>
+ `;
+ }
+}
+
+function formatEmojis(html, emojis) {
+ emojis.forEach(({ shortcode, static_url, url }) => {
+ html = html.replace(
+ `:${shortcode}:`,
+ `<picture>
+ <source srcset="${url}" media="(prefers-reduced-motion: no-preference)">
+ <img src="${static_url}" alt=":${shortcode}:" title=":${shortcode}:" width="16" height="16">
+ </picture>`,
+ );
+ });
+ return html;
+}
+
+async function fetchJSON(url, options = {}) {
+ const headers = new Headers();
+
+ if (options.headers) {
+ for (const [key, value] of Object.entries(options.headers)) {
+ headers.set(key, value);
+ }
+ }
+
+ if (typeof caches === "undefined") {
+ return await (await fetch(url), { headers }).json();
+ }
+
+ const cache = await caches.open("mastodon-comments");
+ let cached = await cache.match(url);
+
+ if (cached && options.ttl) {
+ const cacheTime = new Date(cached.headers.get("x-cached-at"));
+ const diff = Date.now() - cacheTime.getTime();
+
+ if (diff <= options.ttl * 1000) {
+ return await cached.json();
+ }
+ }
+
+ try {
+ const response = await fetch(url, { headers });
+ const body = await response.json();
+
+ cached = new Response(JSON.stringify(body));
+ cached.headers.set("x-cached-at", new Date());
+ cached.headers.set("content-type", "application/json; charset=utf-8");
+ await cache.put(url, cached);
+ return body;
+ } catch {
+ if (cached) {
+ return await cached.json();
+ }
+ }
+}
+
+function dataFromMastodon(data, author, source) {
+ const comments = new Map();
+
+ // Transform data to a more usable format
+ for (const comment of data.descendants) {
+ if (comment.visibility !== "public") {
+ continue;
+ }
+
+ const { account } = comment;
+ const handler = `@${account.username}@${new URL(account.url).hostname}`;
+ comments.set(comment.id, {
+ id: comment.id,
+ isMine: author === handler,
+ source,
+ url: comment.url,
+ parent: comment.in_reply_to_id,
+ createdAt: new Date(comment.created_at),
+ content: formatEmojis(comment.content, comment.emojis),
+ author: {
+ name: formatEmojis(account.display_name, account.emojis),
+ handler,
+ url: account.url,
+ avatar: account.avatar_static,
+ alt: account.display_name,
+ },
+ boosts: comment.reblogs_count,
+ likes: comment.favourites_count,
+ replies: [],
+ });
+ }
+
+ // Group comments by parent
+ for (const comment of comments.values()) {
+ if (comment.parent && comments.has(comment.parent)) {
+ comments.get(comment.parent).replies.push(comment);
+ }
+ }
+
+ return Array.from(comments.values());
+}
+
+function dataFromBluesky(data) {
+ const { thread } = data;
+
+ return blueskyComments(
+ thread.post.author.did,
+ thread.post.cid,
+ thread.replies,
+ );
+}
+
+function blueskyComments(author, parent, comments) {
+ return comments.map((reply) => {
+ const { post, replies } = reply;
+ const rkey = post.uri.split("/").pop();
+ return {
+ id: post.cid,
+ isMine: post.author.did === author,
+ source: "bluesky",
+ url: `https://bsky.app/profile/${post.author.handle}/post/${rkey}`,
+ parent,
+ createdAt: new Date(post.record.createdAt),
+ content: post.record.text,
+ author: {
+ name: post.author.displayName,
+ handler: post.author.handle,
+ url: `https://bsky.app/profile/${post.author.handle}`,
+ avatar: post.author.avatar,
+ alt: post.author.displayName,
+ },
+ boosts: post.repostCount,
+ likes: post.likeCount,
+ replies: blueskyComments(author, post.cid, replies || []),
+ };
+ });
+}
diff --git a/assets/mscomm/styles.css b/assets/mscomm/styles.css
new file mode 100644
index 0000000..61d4d0d
--- /dev/null
+++ b/assets/mscomm/styles.css
@@ -0,0 +1,92 @@
+oom-comments {
+ display: block;
+ /*padding: 2em;*/
+}
+oom-comments ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+oom-comments li {
+ margin: 32px 0;
+}
+oom-comments article {
+ max-width: 600px;
+}
+oom-comments ul ul {
+ margin-left: 64px;
+}
+oom-comments .comment-avatar {
+ width: 50px;
+ height: 50px;
+ border-radius: 6px;
+ float: left;
+ margin-right: 14px;
+ box-shadow: 0 0 1px #0009;
+}
+oom-comments .comment-user {
+ color: currentColor;
+ text-decoration: none;
+ display: block;
+ position: relative;
+}
+oom-comments .comment-author {
+ position: absolute;
+ left: 35px;
+ top: 35px;
+ background: white;
+ border-radius: 50%;
+ width: 20px;
+ height: 20px;
+ color: gray;
+}
+oom-comments .comment-user:hover .comment-username {
+ text-decoration: underline;
+}
+oom-comments .comment-username {
+ margin-right: 0.5em;
+}
+oom-comments .comment-useraddress {
+ color: gray;
+ font-size: small;
+ font-style: normal;
+}
+oom-comments .comment-time {
+ font-size: small;
+ display: flex;
+ align-items: center;
+ column-gap: 0.4em;
+}
+oom-comments .comment-time svg {
+ width: 1em;
+ height: 1em;
+ fill: gray;
+}
+oom-comments .comment-address {
+ color: currentColor;
+ text-decoration: none;
+ display: block;
+ margin-top: 0.25em;
+}
+oom-comments .comment-address:hover {
+ text-decoration: underline;
+}
+oom-comments .comment-body {
+ margin-top: 0.5em;
+ margin-left: 64px;
+ line-height: 1.5;
+}
+oom-comments .comment-body p {
+ margin: 0.5em 0;
+}
+oom-comments .comment-counts {
+ display: flex;
+ column-gap: 1em;
+ font-size: small;
+}
+oom-comments .comment-counts > span {
+ display: flex;
+ align-items: center;
+ column-gap: 0.3em;
+ color: gray;
+}
diff --git a/contact.html b/contact.html
index e4e39f3..8c8fc4c 100644
--- a/contact.html
+++ b/contact.html
@@ -20,6 +20,6 @@ lang: "en"
Signal: uvok_cheetah.10
</li>
<li>
- <a href="https://furry.engineer/@uvok" target="_blank" rel="noreferrer">Mastodon</a>
+ <a href="https://woof.tech/@uvok" target="_blank" rel="noreferrer">uvok@woof.tech</a> on Mastodon
</li>
</ul>
diff --git a/dn42.md b/dn42.md
index 0015f23..9d6fca7 100644
--- a/dn42.md
+++ b/dn42.md
@@ -1,7 +1,7 @@
---
layout: page
title: DN42
-in_navbar: true
+in_navbar: false
order: 50
lang: "en"
---
diff --git a/feed.gmi b/feed.gmi
new file mode 100644
index 0000000..ac3e185
--- /dev/null
+++ b/feed.gmi
@@ -0,0 +1,22 @@
+---
+---
+ {% assign title = site.title | default: site.name %}
+
+ {% if title %}
+# {{ title | smartify | xml_escape }}
+ {% endif %}
+
+ {% if site.description %}
+## {{ site.description | xml_escape }}
+ {% endif %}
+
+ {% unless site.show_drafts %}
+ {% assign posts = site.posts | where_exp: "post", "post.draft != true" %}
+ {% endunless %}
+ {% assign posts = posts | sort: "date" | reverse %}
+ {% for post in posts %}
+ {% assign post_title = post.title | smartify | strip_html | normalize_whitespace | xml_escape %}
+
+=> {{ post.url }} {{ post.date | date: "%Y-%m-%d" }} {{ post_title }}
+ {% endfor %}
+
diff --git a/links.md b/links.md
index 1832bbe..650e786 100644
--- a/links.md
+++ b/links.md
@@ -5,4 +5,19 @@ in_navbar: true
lang: en
---
+## Electronics and such
+
- [Nandgame](https://nandgame.com/): Build a computer from basic logic gates. Great learning resource.
+- [Nand2tetris](https://www.nand2tetris.org/): In similar vain.
+- Also check out Ben Eater's YouTube channel and [website](https://eater.net/).
+- If you like maths, 3Blue1Brown is for you.
+
+## Random software I use / appreciate
+
+- [GrapheneOS](https://grapheneos.org/): Alternative OS for Google Pixel smartphones.
+- [F-Droid](https://f-droid.org/): Open Source App store for Android.
+- [Wireguard](https://www.wireguard.com/): My go-to "VPN" solution. (VPN as in
+ "connect computers/servers to a network", not "I wanna watch $SomeStramingService in
+ another country!!!!!!11111111!!!!!").
+- [OpenWRT](https://openwrt.org/): Router OS which runs my home network.
+
diff --git a/projects.md b/projects.md
new file mode 100644
index 0000000..c8350fb
--- /dev/null
+++ b/projects.md
@@ -0,0 +1,33 @@
+---
+layout: page
+title: Projects
+in_navbar: true
+order: 50
+lang: "en"
+---
+
+Some of my projects and sites:
+
+- This blog you're reading right now
+- [DN42]({% link dn42.md %})
+- [Wiki](https://uvokchee.de/wiki/)
+- [Funkwhale](https://fw.uvok.de/) (currently defunc)
+- [Personal Matrix and XMPP Server]({% link contact.html %})
+- Running an authoritative DNS server with [PowerDNS](https://www.powerdns.com/powerdns-community)
+- Running various VPS (all with Debian, of course)
+- Running [OpenWRT](https://openwrt.org/) in my home network (e.g. for tagged VLAN) [2]
+- Running [Proxmox](https://www.proxmox.com/en/) (hosting various LXC containers) in my home
+- Running an [RIPE ATLAS](https://atlas.ripe.net/docs/) probe - [software](https://github.com/RIPE-NCC/ripe-atlas-software-probe)
+- [OpenPGP WKD](https://wiki.gnupg.org/WKD) via DNS
+ (for shits and giggles - I don't really write mail, and PGP has it's usability problems)
+- [Git server](https://git.uvok.de/)
+ using [gitolite](https://gitolite.com/gitolite/index.html) [1]
+ and [cgit](https://git.zx2c4.com/cgit/)
+- My blog is now also on Gemini, although only accessible via DN42.
+ gemini://uvok.dn42
+
+[1] Can really recommend this if you *don't* want a full-blown Git hosting with
+ "UI" / CI etc. - just the bare-bones git repository hosting.
+ (Which saves you setting up the bare git repos manually, though). \
+[2] I tried OPNsense in the past, too. But at some point I ran into problems I couldn't fix.
+ Also, it was virtualized inside Proxmox. Not the optimal solution.
diff --git a/readme.txt b/readme.txt
new file mode 100644
index 0000000..a8e359f
--- /dev/null
+++ b/readme.txt
@@ -0,0 +1,3 @@
+URL scheme for piwigo:
+
+i.php?/upload/2021/06/04/20210604124815-78046f20-sm.jpg
diff --git a/search.html b/search.html
index fe400ce..2768f1a 100644
--- a/search.html
+++ b/search.html
@@ -10,10 +10,12 @@ lang: "en"
<script src="/assets/js/lunr.min.js"></script>
<script src="/assets/js/search.js"></script>
-<p>
-<input id="input_qterm"></input>
-<button id="qsubmit">Search</button>
-</p>
+<form id="searchForm">
+ <p>
+ <input id="input_qterm" type="text">
+ <button id="qsubmit" type="submit">Search</button>
+ </p>
+</form>
<h2>Results for <span id="qterm"></span></h1>
<div id="results">
@@ -45,10 +47,19 @@ lang: "en"
}
document.getElementById("qterm").textContent = queryterm;
}
- document.getElementById("qsubmit").onclick = function(ev) {
- const qbox = document.getElementById("input_qterm").value;
- if (qbox.length !== 0) {
- query(qbox);
- }
+
+ // Function to be called on submit
+ function submitSearch() {
+ console.log("Search submitted");
+ const qbox = document.getElementById("input_qterm").value;
+ if (qbox.length !== 0) {
+ query(qbox);
+ }
+ }
+
+ // Attach event handler to form submission
+ document.getElementById("searchForm").onsubmit = function(ev) {
+ ev.preventDefault(); // Prevent default form submission behavior
+ submitSearch();
};
</script>