<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Notes on Vladimir Ermakov</title><link>https://vehq.ru/en/notes/</link><description>Recent content in Notes on Vladimir Ermakov</description><generator>Hugo</generator><language>en</language><lastBuildDate>Sat, 17 Oct 2026 00:00:00 +0300</lastBuildDate><atom:link href="https://vehq.ru/en/notes/index.xml" rel="self" type="application/rss+xml"/><item><title>obserwrt: flow export on OpenWrt after netobserv, pmacct and softflowd</title><link>https://vehq.ru/en/notes/2026/obserwrt/</link><pubDate>Sat, 17 Oct 2026 00:00:00 +0300</pubDate><guid>https://vehq.ru/en/notes/2026/obserwrt/</guid><description>&lt;p&gt;In August I wanted to see what actually goes through
&lt;a href="https://vehq.ru/en/notes/2026/awg-mesh/"&gt;the mesh&lt;/a&gt;: who talks to whom, over
which tunnel, how much. The collector side was easy to choose —
&lt;a href="https://github.com/akvorado/akvorado"&gt;Akvorado&lt;/a&gt; takes IPFIX, polls the
exporters over SNMP for interface names, and can take BGP routes over BMP. The
hard part turned out to be the exporter on the OpenWrt routers.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/vooon/obserwrt"&gt;obserwrt&lt;/a&gt; is where it ended: TC eBPF
programs count flows in a kernel map, a small agent reads the map and sends
IPFIX. The README describes it. This note is about how I got there — three
ready-made exporters first, then my own, and what broke along the way.&lt;/p&gt;</description></item><item><title>Backing up OpenWrt hosts and their VMs to Proxmox Backup Server</title><link>https://vehq.ru/en/notes/2026/openwrt-pbs-backup/</link><pubDate>Fri, 16 Oct 2026 00:00:00 +0300</pubDate><guid>https://vehq.ru/en/notes/2026/openwrt-pbs-backup/</guid><description>&lt;p&gt;Some of my OpenWrt boxes are x86 machines that also run a VM or two under
plain QEMU. Everything else in the homelab is backed up to
&lt;a href="https://www.proxmox.com/en/products/proxmox-backup-server/overview"&gt;Proxmox Backup Server&lt;/a&gt;,
so these should be too. Two packages in
&lt;a href="https://github.com/vooon/my-openwrt-feed"&gt;my OpenWrt feed&lt;/a&gt; do that.&lt;/p&gt;
&lt;h2 id="the-client"&gt;The client&lt;/h2&gt;
&lt;p&gt;PBS backups need &lt;code&gt;proxmox-backup-client&lt;/code&gt;, which is Rust and not in OpenWrt.
The feed&amp;rsquo;s &lt;a href="https://github.com/vooon/my-openwrt-feed/tree/master/proxmox-backup"&gt;proxmox-backup&lt;/a&gt;
package builds just the client from the upstream sources, with a few patches:
a client-only cargo build, no libsystemd, and the system zstd. So far I have
only tested it on x86_64.&lt;/p&gt;</description></item><item><title>siphon: getting CloudNativePG's WAL from a bucket into Proxmox Backup Server</title><link>https://vehq.ru/en/notes/2026/siphon/</link><pubDate>Fri, 09 Oct 2026 00:00:00 +0300</pubDate><guid>https://vehq.ru/en/notes/2026/siphon/</guid><description>&lt;p&gt;Everything in my homelab is backed up to Proxmox Backup Server: retention,
verification, and a copy on tape. In early October that stopped being true
for the databases, and it took me a week and a small Rust tool to fix it.
&lt;a href="https://github.com/vooon/siphon"&gt;siphon&lt;/a&gt; streams an S3 bucket into a PBS
snapshot and back; the README has the details. This note is about why it was
needed and what got in the way.&lt;/p&gt;</description></item><item><title>pathosd: what it took to announce an address only while the service is up</title><link>https://vehq.ru/en/notes/2026/pathosd/</link><pubDate>Wed, 07 Oct 2026 00:00:00 +0300</pubDate><guid>https://vehq.ru/en/notes/2026/pathosd/</guid><description>&lt;p&gt;Some services in my network run in several places at once — a DNS
resolver on every hub, the S3 gateway on more than one storage node. Clients
should see a single stable address, and routing should send them to the
nearest instance that actually works. In a routed network that&amp;rsquo;s anycast:
every instance announces the same address over BGP. The catch is the word
&lt;em&gt;works&lt;/em&gt; — a route has no idea whether the service behind it is alive.&lt;/p&gt;</description></item><item><title>Zone-o-Matic: how a DynDNS endpoint grew into the write side of my DNS</title><link>https://vehq.ru/en/notes/2026/zoneomatic/</link><pubDate>Mon, 05 Oct 2026 00:00:00 +0300</pubDate><guid>https://vehq.ru/en/notes/2026/zoneomatic/</guid><description>&lt;p&gt;My authoritative DNS is about as simple as it gets: CoreDNS on the hubs
serving plain zone files with the &lt;code&gt;file&lt;/code&gt; plugin, which reloads them when the
SOA serial changes. Simple to read, simple to back up, simple to diff in git.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/vooon/zoneomatic"&gt;Zone-o-Matic&lt;/a&gt; is the small Go service
that writes to those files. It did not start out as a plan — every API in it
arrived with a new client that needed to change a record.&lt;/p&gt;</description></item><item><title>Moving community.openwrt from shell to ucode</title><link>https://vehq.ru/en/notes/2026/community-openwrt-ucode/</link><pubDate>Fri, 02 Oct 2026 00:00:00 +0300</pubDate><guid>https://vehq.ru/en/notes/2026/community-openwrt-ucode/</guid><description>&lt;p&gt;I manage a small fleet of OpenWrt routers with Ansible: cloud routers, a VPS
and the boxes at home. The tool for that is
&lt;a href="https://github.com/ansible-collections/community.openwrt"&gt;community.openwrt&lt;/a&gt;,
the successor of the old &lt;code&gt;gekmihesg.openwrt&lt;/code&gt; role. Its trick is that the
router needs no Python: the modules used to be shell scripts that the
controller copied over SSH and ran with the router&amp;rsquo;s own BusyBox.&lt;/p&gt;
&lt;p&gt;That works, but it is slow, and speed is what pushed me to rewrite it. In
the shell &lt;code&gt;uci&lt;/code&gt; module every option meant a few more forks of &lt;code&gt;uci&lt;/code&gt; and
&lt;code&gt;jsonfilter&lt;/code&gt;, and my mesh needs a lot of options on every router — one task,
one round of forks each. On top of that, JSON is assembled from shell
strings, quoting is a minefield, and the modules are hard to test.&lt;/p&gt;</description></item><item><title>A homelab mesh over AmneziaWG</title><link>https://vehq.ru/en/notes/2026/awg-mesh/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0300</pubDate><guid>https://vehq.ru/en/notes/2026/awg-mesh/</guid><description>&lt;p&gt;My routers live in several places: a few hubs on rented servers in different
countries and a handful of sites — home, the homelab rack and a couple of others. They are
all OpenWrt, and they are tied together with
&lt;a href="https://github.com/amnezia-vpn"&gt;AmneziaWG&lt;/a&gt;, a WireGuard
fork whose packets don&amp;rsquo;t look like WireGuard on the wire. This note is about
the routing on top, not about the tunnels themselves.&lt;/p&gt;
&lt;h2 id="shape"&gt;Shape&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One tunnel per link.&lt;/strong&gt; Every link is its own point-to-point interface
with exactly one peer: the hubs are joined into a mesh, every site connects to every
hub. More interfaces, but each one is simple, has its own counters, and can
be restarted on its own.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPv6 inside the tunnels.&lt;/strong&gt; Each link gets a &lt;code&gt;/127&lt;/code&gt; plus a link-local
address. Every router has a loopback address that the rest of the network
uses to reach it. Loopbacks live on Linux dummy interfaces, which stock
OpenWrt can&amp;rsquo;t declare in &lt;code&gt;/etc/config/network&lt;/code&gt;, so I packaged a small netifd
protocol for them — &lt;a href="https://github.com/vooon/my-openwrt-feed/tree/master/netifd-proto-dummy"&gt;netifd-proto-dummy&lt;/a&gt;, with a
LuCI page in &lt;a href="https://github.com/vooon/my-openwrt-feed/tree/master/luci-proto-dummy"&gt;luci-proto-dummy&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Everything is in Ansible.&lt;/strong&gt; Interfaces, keys, firewall zones and routing
are generated from inventory with
&lt;a href="https://github.com/ansible-collections/community.openwrt"&gt;community.openwrt&lt;/a&gt;.
The tunnels themselves — keys, shared PSKs, peers, cleanup — are handled by
the &lt;code&gt;wg_interface&lt;/code&gt; and &lt;code&gt;wg_peer&lt;/code&gt; modules I wrote for exactly this mesh
(&lt;a href="https://github.com/ansible-collections/community.openwrt/pull/281"&gt;#281&lt;/a&gt;),
and the rest goes through batched &lt;code&gt;uci&lt;/code&gt; operations — see
&lt;a href="https://vehq.ru/en/notes/2026/community-openwrt-ucode/"&gt;the ucode note&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="routing"&gt;Routing&lt;/h2&gt;
&lt;p&gt;One &lt;a href="https://bird.network.cz/"&gt;BIRD&lt;/a&gt; daemon per router:&lt;/p&gt;</description></item><item><title>Watching BIRD on OpenWrt: metrics, ubus and a LuCI page</title><link>https://vehq.ru/en/notes/2026/bird-monitoring/</link><pubDate>Sat, 12 Sep 2026 00:00:00 +0300</pubDate><guid>https://vehq.ru/en/notes/2026/bird-monitoring/</guid><description>&lt;p&gt;Every router in &lt;a href="https://vehq.ru/en/notes/2026/awg-mesh/"&gt;my mesh&lt;/a&gt; runs
&lt;a href="https://bird.network.cz/"&gt;BIRD&lt;/a&gt;. &lt;code&gt;birdc&lt;/code&gt; over SSH is fine for one box and
tiring for ten, so I built three small packages that look at BIRD from three
sides: Prometheus, ubus and LuCI. All of them live in
&lt;a href="https://github.com/vooon/my-openwrt-feed"&gt;my OpenWrt feed&lt;/a&gt; and all of them
are written in ucode.&lt;/p&gt;
&lt;h2 id="birds-control-socket"&gt;BIRD&amp;rsquo;s control socket&lt;/h2&gt;
&lt;p&gt;BIRD has a simple line protocol on a unix socket — the same one &lt;code&gt;birdc&lt;/code&gt;
uses. You connect, read a banner, send a command and read the reply; every
reply line starts with a four-digit code followed by &lt;code&gt;-&lt;/code&gt; (more lines follow)
or a space (last line). That is easy to speak from ucode with the &lt;code&gt;socket&lt;/code&gt;
module, so none of the packages need &lt;code&gt;birdc&lt;/code&gt; or a separate exporter binary.&lt;/p&gt;</description></item><item><title>inotify-rsync: push files to other routers the moment they change</title><link>https://vehq.ru/en/notes/2026/inotify-rsync/</link><pubDate>Sun, 16 Aug 2026 00:00:00 +0300</pubDate><guid>https://vehq.ru/en/notes/2026/inotify-rsync/</guid><description>&lt;p&gt;Some files on my hubs are produced in one place: one hub renders a few
static files and downloads a few big ones, and the other hubs serve copies.
The copies should follow quickly. A cron job means either lag or a lot of
pointless runs, so I wanted rsync to fire when something actually changes.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/vooon/my-openwrt-feed/tree/master/inotify-rsync"&gt;inotify-rsync&lt;/a&gt;
does exactly that and nothing more: watch some directories, run rsync to a
destination when they change.&lt;/p&gt;</description></item></channel></rss>